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