【Linux网络编程学习】I/O多路复用——epoll
此為??蚅inux C++課程和黑馬Linux系統(tǒng)編程筆記。
1. 關(guān)于epoll
epoll是Linux下多路復(fù)用IO接口select/poll的增強(qiáng)版本,它能顯著提高程序在大量并發(fā)連接中只有少量活躍的情況下的系統(tǒng)CPU利用率,因?yàn)樗鼤?huì)復(fù)用文件描述符集合來傳遞結(jié)果而不用迫使開發(fā)者每次等待事件之前都必須重新準(zhǔn)備要被偵聽的文件描述符集合,另一點(diǎn)原因就是獲取事件的時(shí)候,它無須遍歷整個(gè)被偵聽的描述符集,只要遍歷那些被內(nèi)核IO事件異步喚醒而加入Ready隊(duì)列的描述符集合就行了。
目前epoll是linux大規(guī)模并發(fā)網(wǎng)絡(luò)程序中的熱門首選模型。
epoll除了提供select/poll那種IO事件的水平觸發(fā)(Level Triggered)外,還提供了邊沿觸發(fā)(Edge Triggered),這就使得用戶空間程序有可能緩存IO狀態(tài),減少epoll_wait/epoll_pwait的調(diào)用,提高應(yīng)用程序效率。
2. epoll API介紹
2.1 創(chuàng)建epoll實(shí)例:epoll_create
#include <sys/epoll.h> int epoll_create(int size);功能:在內(nèi)核中創(chuàng)建一個(gè)新的epoll實(shí)例,并返回一個(gè)操縱該epoll的文件描述符,這個(gè)文件描述符和真正的文件沒有關(guān)系,僅僅是為了后續(xù)調(diào)用epoll而創(chuàng)建的。該函數(shù)調(diào)用后在內(nèi)核中創(chuàng)建了一個(gè)存儲(chǔ)事件的數(shù)據(jù)結(jié)構(gòu),這個(gè)數(shù)據(jù)結(jié)構(gòu)中有兩個(gè)比較重要的子結(jié)構(gòu),一個(gè)是需要檢測(cè)的文件描述符的信息(使用紅黑樹實(shí)現(xiàn)),還有一個(gè)是就緒列表,存放檢測(cè)到數(shù)據(jù)發(fā)送改變的文件描述符信息(使用雙向鏈表實(shí)現(xiàn)),關(guān)于epoll更詳細(xì)的內(nèi)部實(shí)現(xiàn)在這里不詳細(xì)討論。
參數(shù):size : 自從linux2.6.8之后,size參數(shù)是被忽略的。隨便寫一個(gè)數(shù),必須大于0。
返回值:
-1 : 失敗
> 0 : 用于操作epoll實(shí)例的文件描述符
2.2 注冊(cè)epoll的監(jiān)聽事件:epoll_ctl
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);功能:向內(nèi)核中的epoll實(shí)例中添加、修改、移除事件。epoll和select的一個(gè)顯著區(qū)別就在這里:select是在監(jiān)聽事件時(shí)告訴內(nèi)核要監(jiān)聽什么類型的事件,而epoll是在這里先注冊(cè)要監(jiān)聽的事件類型,然后再調(diào)用epoll_wait監(jiān)聽。
參數(shù):
- epfd : epoll實(shí)例對(duì)應(yīng)的文件描述符
- op : 要進(jìn)行什么操作
EPOLL_CTL_ADD: 添加
EPOLL_CTL_MOD: 修改
EPOLL_CTL_DEL: 刪除 - fd : 要檢測(cè)的文件描述符
- event : 檢測(cè)文件描述符什么事件,這里涉及到epoll_event,定義如下:
這里我們只需要關(guān)注兩個(gè)字段即可:events和data.fd:
其中events表示要檢測(cè)的事件,有以下選擇:
EPOLLIN :表示對(duì)應(yīng)的文件描述符可以讀(包括對(duì)端SOCKET正常關(guān)閉);
EPOLLOUT:表示對(duì)應(yīng)的文件描述符可以寫;
EPOLLPRI:表示對(duì)應(yīng)的文件描述符有緊急的數(shù)據(jù)可讀(這里應(yīng)該表示有帶外數(shù)據(jù)到來);
EPOLLERR:表示對(duì)應(yīng)的文件描述符發(fā)生錯(cuò)誤;
EPOLLHUP:表示對(duì)應(yīng)的文件描述符被掛斷;
EPOLLET: 將EPOLL設(shè)為邊緣觸發(fā)(Edge Triggered)模式,這是相對(duì)于水平觸發(fā)(Level Triggered)來說的。
EPOLLONESHOT:只監(jiān)聽一次事件,當(dāng)監(jiān)聽完這次事件之后,如果還需要繼續(xù)監(jiān)聽這個(gè)socket的話,需要再次把這個(gè)socket加入到EPOLL隊(duì)列里。
其中data.fd表示該事件對(duì)應(yīng)的socket的文件描述符。
返回值:
- 成功,返回發(fā)送變化的文件描述符的個(gè)數(shù) > 0
- 失敗 -1
2.3 監(jiān)聽事件:epoll_wait
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);功能:等待已注冊(cè)的事件發(fā)生,返回事件的數(shù)目,并將已觸發(fā)的事件寫入events數(shù)組(第二個(gè)參數(shù))中。
參數(shù):
- epfd : epoll實(shí)例對(duì)應(yīng)的文件描述符
- events : 傳出參數(shù),保存了發(fā)送了變化的文件描述符的信息,需要調(diào)用者先創(chuàng)建好
- maxevents : 第二個(gè)參數(shù)結(jié)構(gòu)體數(shù)組的大小
- timeout : 阻塞時(shí)間
- 0 : 不阻塞
- -1 : 阻塞,直到檢測(cè)到fd數(shù)據(jù)發(fā)生變化,解除阻塞
- > 0 : 阻塞的時(shí)長(zhǎng)(毫秒)
返回值:
- 成功,返回發(fā)送變化的文件描述符的個(gè)數(shù) > 0
- 失敗 -1
3. 示例程序
以下用epoll實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的服務(wù)端,把客戶端傳來的小寫字母轉(zhuǎn)換成大寫字母并傳回給客戶端。
/*用epoll實(shí)現(xiàn)一個(gè)簡(jiǎn)單的服務(wù)器-客戶端通信*/#include <stdio.h> #include <unistd.h> #include <arpa/inet.h> #include <stdlib.h> #include <pthread.h> #include <strings.h> #include <sys/epoll.h>// 設(shè)定一個(gè)服務(wù)器端口號(hào) #define SERV_IP "127.0.0.1" #define SERV_PORT 9999int main() {int lfd = socket(AF_INET, SOCK_STREAM, 0);struct sockaddr_in serv_addr;serv_addr.sin_family = AF_INET;serv_addr.sin_port = htons(SERV_PORT); // 注意轉(zhuǎn)化成網(wǎng)絡(luò)字節(jié)序inet_pton(AF_INET, SERV_IP, &serv_addr.sin_addr.s_addr); // 注意轉(zhuǎn)化成網(wǎng)絡(luò)字節(jié)序bind(lfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));listen(lfd, 128);int epfd = epoll_create(100); // 內(nèi)核創(chuàng)建epoll實(shí)例struct epoll_event epev;epev.events = EPOLLIN; // 要檢測(cè)lfd的讀事件epev.data.fd = lfd;// 注冊(cè)了對(duì)lfd的監(jiān)聽,此后如果不刪除這個(gè)注冊(cè)信息,每次調(diào)用epoll_wait都將監(jiān)聽lfd的讀事件(也就是客戶端的連接)epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev); struct epoll_event epevs[1024]; // 用作epoll_wait的第二個(gè)參數(shù)(傳出參數(shù)) while(1) {int ret = epoll_wait(epfd, epevs, 1024, -1); // 監(jiān)聽已注冊(cè)的事件,最后一個(gè)參數(shù)-1表示阻塞等待if(ret == -1) {perror("epoll_wait error");exit(-1);}// 一旦走到這里說明解除了阻塞,就是指epoll監(jiān)測(cè)到了事件的發(fā)生,遍歷每個(gè)事件:for(int i = 0; i < ret; ++i) {int curfd = epevs[i].data.fd; // 表示當(dāng)前觸發(fā)的事件對(duì)應(yīng)的fdif(curfd == lfd) { // 如果監(jiān)聽到lfd的讀事件了,說明有一個(gè)新客戶端建立連接struct sockaddr_in clie_addr;int clie_addr_len = sizeof(clie_addr); int cfd = accept(lfd, (struct sockaddr*)&clie_addr, &clie_addr_len);char clie_IP[BUFSIZ];printf("Client IP: %s, client port: %d connected\n", inet_ntop(AF_INET, &clie_addr.sin_addr.s_addr, clie_IP, sizeof(clie_IP)),ntohs(clie_addr.sin_port));epev.events = EPOLLIN; // 要檢測(cè)cfd的讀事件epev.data.fd = cfd;epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev); // 把對(duì)該cfd的讀事件監(jiān)聽注冊(cè)上,以后epoll會(huì)同時(shí)監(jiān)聽lfd和cfd} else { // 說明檢測(cè)到的是某個(gè)cfd的讀事件,讀該客戶端傳來的數(shù)據(jù)char buf[BUFSIZ] = {0};int len = read(curfd, buf, sizeof(buf));if(len > 0) {// 小寫轉(zhuǎn)大寫int i;for(i = 0; i < len; ++i) {if(buf[i] >= 'a' && buf[i] <= 'z') {buf[i] -= 32;}}write(curfd, buf, len); // 寫回給客戶端write(STDOUT_FILENO, buf, len);} else if(len == 0) {// 說明讀完了,客戶端已關(guān)閉,此時(shí)epoll已經(jīng)沒有必要繼續(xù)監(jiān)聽該cfd了epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);close(curfd);} else {perror("read error");exit(-1);}}}}close(lfd);close(epfd); // 別忘了關(guān)epfdreturn 0; }4. epoll的兩種觸發(fā)方式
EPOLL事件有兩種模型:
- Edge Triggered (ET) 邊緣觸發(fā):只有數(shù)據(jù)到來才觸發(fā),不管緩存區(qū)中是否還有數(shù)據(jù)。
- Level Triggered (LT) 水平觸發(fā):只要有數(shù)據(jù)都會(huì)觸發(fā)。
LT(level - triggered)是缺省的工作方式,并且同時(shí)支持 block 和 no-block socket。在這種做法中,內(nèi)核告訴你一個(gè)文件描述符是否就緒了,然后你可以對(duì)這個(gè)就緒的 fd 進(jìn)行 IO 操作。如果你不作任何操作,內(nèi)核還是會(huì)繼續(xù)通知你的。
ET(edge - triggered)是高速工作方式,只支持 no-block socket。在這種模式下,當(dāng)描述符從未就緒變?yōu)榫途w時(shí),內(nèi)核通過epoll告訴你。然后它會(huì)假設(shè)你知道文件描述符已經(jīng)就緒,并且不會(huì)再為那個(gè)文件描述符發(fā)送更多的就緒通知,直到你做了某些操作導(dǎo)致那個(gè)文件描述符不再為就緒狀態(tài)了。但是請(qǐng)注意,如果一直不對(duì)這個(gè) fd 作 IO 操作(從而導(dǎo)致它再次變成未就緒),內(nèi)核不會(huì)發(fā)送更多的通知(only once)。
ET 模式在很大程度上減少了 epoll 事件被重復(fù)觸發(fā)的次數(shù),因此效率要比 LT 模式高。epoll工作在 ET 模式的時(shí)候,必須使用非阻塞套接口,以避免由于一個(gè)文件句柄的阻塞讀/阻塞寫操作把處理多個(gè)文件描述符的任務(wù)餓死。
關(guān)于LT和ET的詳細(xì)介紹,以及為什么ET模式要搭配非阻塞IO,見這篇博客,寫的極好。
總結(jié)
以上是生活随笔為你收集整理的【Linux网络编程学习】I/O多路复用——epoll的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 成都欢乐谷马戏团表演时间
- 下一篇: 我的世界怎么不让水结冰