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