【Linux系统编程】IO多路复用之epoll
00. 目錄
文章目錄
- 00. 目錄
- 01. 概述
- 02. epoll函數(shù)
- 03. 程序示例
- 04. epoll優(yōu)缺點(diǎn)
- 05. 附錄
01. 概述
epoll是Linux下多路復(fù)用IO接口select/poll的增強(qiáng)版本,它能顯著提高程序在大量并發(fā)連接中只有少量活躍的情況下的系統(tǒng)CPU利用率,因?yàn)樗鼤?huì)復(fù)用文件描述符集合來(lái)傳遞結(jié)果而不用迫使開(kāi)發(fā)者每次等待事件之前都必須重新準(zhǔn)備要被偵聽(tīng)的文件描述符集合,另一點(diǎn)原因就是獲取事件的時(shí)候,它無(wú)須遍歷整個(gè)被偵聽(tīng)的描述符集,只要遍歷那些被內(nèi)核IO事件異步喚醒而加入Ready隊(duì)列的描述符集合就行了。
目前epoll是linux大規(guī)模并發(fā)網(wǎng)絡(luò)程序中的熱門(mén)首選模型。
02. epoll函數(shù)
epoll_create函數(shù):
int epoll_create(int size) 功能:該函數(shù)生成一個(gè) epoll 專(zhuān)用的文件描述符(創(chuàng)建一個(gè) epoll 的句柄)。 參數(shù):size: 用來(lái)告訴內(nèi)核這個(gè)監(jiān)聽(tīng)的數(shù)目一共有多大,參數(shù) size 并不是限制了 epoll 所能監(jiān)聽(tīng)的描述符最大個(gè)數(shù),只是對(duì)內(nèi)核初始分配內(nèi)部數(shù)據(jù)結(jié)構(gòu)的一個(gè)建議。使用完 epoll 后,必須調(diào)用 close() 關(guān)閉,否則可能導(dǎo)致 fd 被耗盡。 返回值:成功:epoll 專(zhuān)用的文件描述符失敗:-1epoll_ctl函數(shù):
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event) 功能:epoll 的事件注冊(cè)函數(shù),它不同于 select() 是在監(jiān)聽(tīng)事件時(shí)告訴內(nèi)核要監(jiān)聽(tīng)什么類(lèi)型的事件,而是在這里先注冊(cè)要監(jiān)聽(tīng)的事件類(lèi)型。 參數(shù):epfd: epoll 專(zhuān)用的文件描述符,epoll_create()的返回值op: 表示動(dòng)作,用三個(gè)宏來(lái)表示:EPOLL_CTL_ADD:注冊(cè)新的 fd 到 epfd 中;EPOLL_CTL_MOD:修改已經(jīng)注冊(cè)的fd的監(jiān)聽(tīng)事件;EPOLL_CTL_DEL:從 epfd 中刪除一個(gè) fd;fd: 需要監(jiān)聽(tīng)的文件描述符event: 告訴內(nèi)核要監(jiān)聽(tīng)什么事件,struct epoll_event 結(jié)構(gòu) 返回值:成功:0失敗:-1struct epoll_event 結(jié)構(gòu)如下:// 保存觸發(fā)事件的某個(gè)文件描述符相關(guān)的數(shù)據(jù)(與具體使用方式有關(guān))typedef union epoll_data {void *ptr;int fd;__uint32_t u32;__uint64_t u64;} epoll_data_t;// 感興趣的事件和被觸發(fā)的事件struct epoll_event {__uint32_t events; /* Epoll events */epoll_data_t data; /* User data variable */}; ?events 可以是以下幾個(gè)宏的集合:EPOLLIN :表示對(duì)應(yīng)的文件描述符可以讀(包括對(duì)端 SOCKET 正常關(guān)閉);EPOLLOUT:表示對(duì)應(yīng)的文件描述符可以寫(xiě);EPOLLPRI:表示對(duì)應(yīng)的文件描述符有緊急的數(shù)據(jù)可讀(這里應(yīng)該表示有帶外數(shù)據(jù)到來(lái));EPOLLERR:表示對(duì)應(yīng)的文件描述符發(fā)生錯(cuò)誤;EPOLLHUP:表示對(duì)應(yīng)的文件描述符被掛斷;EPOLLET :將 EPOLL 設(shè)為邊緣觸發(fā)(Edge Triggered)模式,這是相對(duì)于水平觸發(fā)(Level Triggered)來(lái)說(shuō)的。EPOLLONESHOT:只監(jiān)聽(tīng)一次事件,當(dāng)監(jiān)聽(tīng)完這次事件之后,如果還需要繼續(xù)監(jiān)聽(tīng)這個(gè) socket 的話(huà),需要再次把這個(gè) socket 加入到 EPOLL 隊(duì)列里epoll_wait函數(shù):
int epoll_wait( int epfd, struct epoll_event * events, int maxevents, int timeout ); 功能:等待事件的產(chǎn)生,收集在 epoll 監(jiān)控的事件中已經(jīng)發(fā)送的事件,類(lèi)似于 select() 調(diào)用。 參數(shù):epfd: epoll 專(zhuān)用的文件描述符,epoll_create()的返回值events: 分配好的 epoll_event 結(jié)構(gòu)體數(shù)組,epoll 將會(huì)把發(fā)生的事件賦值到events 數(shù)組中(events 不可以是空指針,內(nèi)核只負(fù)責(zé)把數(shù)據(jù)復(fù)制到這個(gè) events 數(shù)組中,不會(huì)去幫助我們?cè)谟脩?hù)態(tài)中分配內(nèi)存)。maxevents: maxevents 告訴內(nèi)核這個(gè) events 有多大 。timeout: 超時(shí)時(shí)間,單位為毫秒,為 -1 時(shí),函數(shù)為阻塞 返回值:成功:返回需要處理的事件數(shù)目,如返回 0 表示已超時(shí)。失敗:-1epoll 對(duì)文件描述符的操作有兩種模式:LT(level trigger)和 ET(edge trigger)。LT 模式是默認(rèn)模式,LT 模式與 ET 模式的區(qū)別如下:
LT 模式:當(dāng) epoll_wait 檢測(cè)到描述符事件發(fā)生并將此事件通知應(yīng)用程序,應(yīng)用程序可以不立即處理該事件。下次調(diào)用 epoll_wait 時(shí),會(huì)再次響應(yīng)應(yīng)用程序并通知此事件。
ET 模式:當(dāng) epoll_wait 檢測(cè)到描述符事件發(fā)生并將此事件通知應(yīng)用程序,應(yīng)用程序必須立即處理該事件。如果不處理,下次調(diào)用 epoll_wait 時(shí),不會(huì)再次響應(yīng)應(yīng)用程序并通知此事件。
ET 模式在很大程度上減少了 epoll 事件被重復(fù)觸發(fā)的次數(shù),因此效率要比 LT 模式高。epoll 工作在 ET 模式的時(shí)候,必須使用非阻塞套接口,以避免由于一個(gè)文件句柄的阻塞讀/阻塞寫(xiě)操作把處理多個(gè)文件描述符的任務(wù)餓死。
03. 程序示例
#include <sys/epoll.h> #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <stdlib.h>int main(int argc, char *argv[]) {int ret;int fd;ret = mkfifo("test_fifo", 0666); // 創(chuàng)建有名管道if(ret != 0){perror("mkfifo:");}fd = open("test_fifo", O_RDWR); // 讀寫(xiě)方式打開(kāi)管道if(fd < 0){perror("open fifo");return -1;}ret = 0;struct epoll_event event; // 告訴內(nèi)核要監(jiān)聽(tīng)什么事件struct epoll_event wait_event;int epfd = epoll_create(10); // 創(chuàng)建一個(gè) epoll 的句柄,參數(shù)要大于 0, 沒(méi)有太大意義if(-1 == epfd ){perror ("epoll_create");return -1;}event.data.fd = 0; // 標(biāo)準(zhǔn)輸入event.events = EPOLLIN; // 表示對(duì)應(yīng)的文件描述符可以讀// 事件注冊(cè)函數(shù),將標(biāo)準(zhǔn)輸入描述符 0 加入監(jiān)聽(tīng)事件ret = epoll_ctl(epfd, EPOLL_CTL_ADD, 0, &event);if(-1 == ret){perror("epoll_ctl");return -1;}event.data.fd = fd; // 有名管道event.events = EPOLLIN; // 表示對(duì)應(yīng)的文件描述符可以讀// 事件注冊(cè)函數(shù),將有名管道描述符 fd 加入監(jiān)聽(tīng)事件ret = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);if(-1 == ret){perror("epoll_ctl");return -1;}ret = 0;while(1){ // 監(jiān)視并等待多個(gè)文件(標(biāo)準(zhǔn)輸入,有名管道)描述符的屬性變化(是否可讀)// 沒(méi)有屬性變化,這個(gè)函數(shù)會(huì)阻塞,直到有變化才往下執(zhí)行,這里沒(méi)有設(shè)置超時(shí)ret = epoll_wait(epfd, &wait_event, 2, -1);//ret = epoll_wait(epfd, &wait_event, 2, 1000);if(ret == -1){ // 出錯(cuò)close(epfd);perror("epoll");}else if(ret > 0){ // 準(zhǔn)備就緒的文件描述符char buf[100] = {0};if( ( 0 == wait_event.data.fd ) && ( EPOLLIN == wait_event.events & EPOLLIN ) ){ // 標(biāo)準(zhǔn)輸入read(0, buf, sizeof(buf));printf("stdin buf = %s\n", buf);}else if( ( fd == wait_event.data.fd ) && ( EPOLLIN == wait_event.events & EPOLLIN ) ){ // 有名管道read(fd, buf, sizeof(buf));printf("fifo buf = %s\n", buf); } }else if(0 == ret){ // 超時(shí)printf("time out\n");}}close(epfd);return 0; }在 select/poll中,進(jìn)程只有在調(diào)用一定的方法后,內(nèi)核才對(duì)所有監(jiān)視的文件描述符進(jìn)行掃描,而 epoll() 事先通過(guò) epoll_ctl() 來(lái)注冊(cè)一個(gè)文件描述符,一旦基于某個(gè)文件描述符就緒時(shí),內(nèi)核會(huì)采用類(lèi)似 callback 的回調(diào)機(jī)制(軟件中斷 ),迅速激活這個(gè)文件描述符,當(dāng)進(jìn)程調(diào)用 epoll_wait() 時(shí)便得到通知。
04. epoll優(yōu)缺點(diǎn)
epoll 的優(yōu)點(diǎn)主要是一下幾個(gè)方面:
1)監(jiān)視的描述符數(shù)量不受限制,它所支持的 FD 上限是最大可以打開(kāi)文件的數(shù)目,這個(gè)數(shù)字一般遠(yuǎn)大于 2048,舉個(gè)例子,在 1GB 內(nèi)存的機(jī)器上大約是 10 萬(wàn)左右,具體數(shù)目可以 cat /proc/sys/fs/file-max 察看,一般來(lái)說(shuō)這個(gè)數(shù)目和系統(tǒng)內(nèi)存關(guān)系很大。select() 的最大缺點(diǎn)就是進(jìn)程打開(kāi)的 fd 是有數(shù)量限制的。這對(duì)于連接數(shù)量比較大的服務(wù)器來(lái)說(shuō)根本不能滿(mǎn)足。雖然也可以選擇多進(jìn)程的解決方案( Apache 就是這樣實(shí)現(xiàn)的),不過(guò)雖然 Linux 上面創(chuàng)建進(jìn)程的代價(jià)比較小,但仍舊是不可忽視的,加上進(jìn)程間數(shù)據(jù)同步遠(yuǎn)比不上線(xiàn)程間同步的高效,所以也不是一種完美的方案。
2)I/O 的效率不會(huì)隨著監(jiān)視 fd 的數(shù)量的增長(zhǎng)而下降。select(),poll() 實(shí)現(xiàn)需要自己不斷輪詢(xún)所有 fd 集合,直到設(shè)備就緒,期間可能要睡眠和喚醒多次交替。而 epoll 其實(shí)也需要調(diào)用 epoll_wait() 不斷輪詢(xún)就緒鏈表,期間也可能多次睡眠和喚醒交替,但是它是設(shè)備就緒時(shí),調(diào)用回調(diào)函數(shù),把就緒 fd 放入就緒鏈表中,并喚醒在 epoll_wait() 中進(jìn)入睡眠的進(jìn)程。雖然都要睡眠和交替,但是 select() 和 poll() 在“醒著”的時(shí)候要遍歷整個(gè) fd 集合,而 epoll 在“醒著”的時(shí)候只要判斷一下就緒鏈表是否為空就行了,這節(jié)省了大量的 CPU 時(shí)間。這就是回調(diào)機(jī)制帶來(lái)的性能提升。
3)select(),poll() 每次調(diào)用都要把 fd 集合從用戶(hù)態(tài)往內(nèi)核態(tài)拷貝一次,而 epoll 只要一次拷貝,這也能節(jié)省不少的開(kāi)銷(xiāo)。
05. 附錄
總結(jié)
以上是生活随笔為你收集整理的【Linux系统编程】IO多路复用之epoll的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 【Linux系统编程】IO多路复用之po
- 下一篇: 【Linux网络编程】网络协议入门