複製鏈接
請複製以下鏈接發送給好友

epoll

鎖定
epoll是Linux內核為處理大批量文件描述符而作了改進的poll,是Linux多路複用IO接口select/poll的增強版本,它能顯著提高程序在大量併發連接中只有少量活躍的情況下的系統CPU利用率。另一點原因就是獲取事件的時候,它無須遍歷整個被偵聽的描述符集,只要遍歷那些被內核IO事件異步喚醒而加入Ready隊列的描述符集合就行了。epoll除了提供select/poll那種IO事件的水平觸發(Level Triggered)外,還提供了邊緣觸發(Edge Triggered),這就使得用户空間程序有可能緩存IO狀態,減少epoll_wait/epoll_pwait的調用,提高應用程序效率。
外文名
epoll
優    點
內核微調等
工作方式
LT和ET
系統調用
epoll_create, epoll_ctl等

epoll優點

支持一個進程打開大數目的socket描述符
使用epoll進行高性能網絡編程 使用epoll進行高性能網絡編程
select 最不能忍受的是一個進程所打開的FD是有一定限制的,由FD_SETSIZE設置,默認值是1024。對於那些需要支持的上萬連接數目的IM服務器來説顯然太少了。這時候你一是可以選擇修改這個宏然後重新編譯服務器代碼,不過資料也同時指出這樣會帶來網絡效率的下降,二是可以選擇多進程的解決方案(傳統的Apache方案),不過雖然linux上面創建進程的代價比較小,但仍舊是不可忽視的,加上進程間數據同步遠比不上線程間同步的高效,所以也不是一種完美的方案。不過 epoll則沒有這個限制,它所支持的FD上限是最大可以打開文件的數目,這個數字一般遠大於2048,舉個例子,在1GB內存的機器上大約是10萬左右,具體數目可以cat /proc/sys/fs/file-max查看,一般來説這個數目和系統內存關係很大。
IO效率不隨FD數目增加而線性下降
傳統的select/poll另一個致命弱點就是當你擁有一個很大的socket集合,不過由於網絡延時,任一時間只有部分的socket是“活躍”的,但是select/poll每次調用都會線性掃描全部的集合,導致效率呈現線性下降。但是epoll不存在這個問題,它只會對“活躍”的socket進行操作---這是因為在內核實現中epoll是根據每個fd上面的callback函數實現的。那麼,只有“活躍”的socket才會主動的去調用 callback函數,其他idle狀態socket則不會,在這點上,epoll實現了一個“偽”AIO,因為這時候推動力在os內核。在一些 benchmark中,如果所有的socket基本上都是活躍的---比如一個高速LAN環境,epoll並不比select/poll有什麼效率,相反,如果過多使用epoll_ctl,效率相比還有稍微的下降。但是一旦使用idle connections模擬WAN環境,epoll的效率就遠在select/poll之上了。
沒有使用mmap加速內核與用户空間消息傳遞

epoll內核微調

這一點其實不算epoll的優點了,而是整個linux平台的優點。也許你可以懷疑linux平台,但是你無法迴避linux平台賦予你微調內核的能力。比如,內核TCP/IP協議棧使用內存池管理sk_buff結構,那麼可以在運行時期動態調整這個內存pool(skb_head_pool)的大小--- 通過echo XXXX>/proc/sys/net/core/hot_list_length完成。再比如listen函數的第2個參數(TCP完成3次握手數據包隊列長度),也可以根據你平台內存大小動態調整。更甚至在一個數據包個數目巨大但同時每個數據包本身大小卻很小的特殊系統上嘗試最新的NAPI網卡驅動架構。

epoll使用

令人高興的是,2.6內核的epoll比其2.5開發版本的/dev/epoll簡潔了許多,所以,大部分情況下,強大的東西往往是簡單的。唯一有點麻煩是epoll有2種工作方式:LT和ET。
LT(level triggered)是缺省的工作方式,並且同時支持block和no-block socket.在這種做法中,內核告訴你一個文件描述符是否就緒了,然後你可以對這個就緒的fd進行IO操作。如果你不作任何操作,內核還是會繼續通知你的,所以,這種模式編程出錯誤可能性要小一點。傳統的select/poll都是這種模型的代表。
ET (edge-triggered)是高速工作方式,只支持non-block socket。在這種模式下,當描述符從未就緒變為就緒時,內核通過epoll告訴你。然後它會假設你知道文件描述符已經就緒,並且不會再為那個文件描述符發送更多的就緒通知,直到你做了某些操作導致那個文件描述符不再為就緒狀態了(比如,你在發送,接收或者接收請求,或者發送接收的數據少於一定量時導致了一個EWOULDBLOCK 錯誤)。但是請注意,如果一直不對這個fd作IO操作(從而導致它再次變成未就緒),內核不會發送更多的通知(only once),不過在TCP協議中,ET模式的加速效用仍需要更多的benchmark確認。
ET和LT的區別就在這裏體現,LT事件不會丟棄,而是隻要讀buffer裏面有數據可以讓用户讀,則不斷的通知你。而ET則只在事件發生之時通知。可以簡單理解為LT是水平觸發,而ET則為邊緣觸發。LT模式只要有事件未處理就會觸發,而ET則只在高低電平變換時(即狀態從1到0或者0到1)觸發。

epoll系統調用

epoll相關的系統調用有:epoll_create, epoll_ctl和epoll_wait。Linux-2.6.19又引入了可以屏蔽指定信號的epoll_wait: epoll_pwait。至此epoll家族已全。其中epoll_create用來創建一個epoll文件描述符,epoll_ctl用來添加/修改/刪除需要偵聽的文件描述符及其事件,epoll_wait/epoll_pwait接收發生在被偵聽的描述符上的,用户感興趣的IO事件。epoll文件描述符用完後,直接用close關閉即可,非常方便。事實上,任何被偵聽的文件符只要其被關閉,那麼它也會自動從被偵聽的文件描述符集合中刪除,很是智能。
每次添加/修改/刪除被偵聽文件描述符都需要調用epoll_ctl,所以要儘量少地調用epoll_ctl,防止其所引來的開銷抵消其帶來的好處。有的時候,應用中可能存在大量的短連接(比如説Web服務器),epoll_ctl將被頻繁地調用,可能成為這個系統的瓶頸。
A:IO效率。
在大家苦苦的為在線人數的增長而導致的系統資源吃緊上的問題正在發愁的時候,Linux 2.6內核中提供的System Epoll為我們提供了一套完美的解決方案。傳統的select以及poll的效率會因為在線人數的線形遞增而導致呈二次乃至三次方的下降,這些直接導致了網絡服務器可以支持的人數有了個比較明顯的限制。
自從Linux提供了/dev/epoll的設備以及後來2.6內核中對/dev/epoll設備的訪問的封裝(System Epoll)之後,這種現象得到了大大的緩解。
那麼究竟如何來使用epoll呢?其實非常簡單。
通過在包含一個頭文件#include 以及幾個簡單的API將可以大大的提高你的網絡服務器的支持人數。
首先通過epoll_create(int maxfds)來創建一個epoll的句柄,其中maxfds為你epoll所支持的最大句柄數。這個函數會返回一個新的epoll句柄,之後的所有操作將通過這個句柄來進行操作。在用完之後,記得用close()來關閉這個創建出來的epoll句柄。
之後在你的網絡主循環裏面,每一幀的調用epoll_wait(int epfd, epoll_event *events, int max events, int timeout)來查詢所有的網絡接口,看哪一個可以讀,哪一個可以寫了。基本的語法為:
nfds=epoll_wait(kdpfd,events,maxevents,-1);
其中kdpfd為用epoll_create創建之後的句柄,events是一個epoll_event*的指針,當epoll_wait這個函數操作成功之後,epoll_events裏面將儲存所有的讀寫事件。maxevents是最大事件數量。最後一個timeout是epoll_wait的超時,為0的時候表示馬上返回,為-1的時候表示一直等下去,直到有事件發生,為任意正整數的時候表示等這麼長的時間,如果一直沒有事件,則返回。一般如果網絡主循環是單獨的線程的話,可以用-1來等,這樣可以保證一些效率,如果是和主邏輯在同一個線程的話,則可以用0來保證主循環的效率。
epoll_wait返回之後應該是一個循環,遍歷所有的事件:
    int epfd = epoll_create(POLL_SIZE);
    struct epoll_event ev;
    struct epoll_event *events = NULL;
    nfds = epoll_wait(epfd, events, 20, 500);
    {
        for (n = 0; n < nfds; ++n) {
            if (events[n].data.fd == listener) {
                //如果是主socket的事件的話,則表示
                //有新連接進入了,進行新連接的處理。
                client = accept(listener, (structsockaddr *)&local, &addrlen);
                if (client < 0) {
                    perror("accept");
                    continue;
                }
                setnonblocking(client);        //將新連接置於非阻塞模式
                ev.events = EPOLLIN | EPOLLET; //並且將新連接也加入EPOLL的監聽隊列。
                //注意,這裏的參數EPOLLIN|EPOLLET並沒有設置對寫socket的監聽,
                //如果有寫操作的話,這個時候epoll是不會返回事件的,如果要對寫操作
                //也監聽的話,應該是EPOLLIN|EPOLLOUT|EPOLLET
                ev.data.fd = client;
                if (epoll_ctl(epfd, EPOLL_CTL_ADD, client, &ev) < 0) {
                    //設置好event之後,將這個新的event通過epoll_ctl加入到epoll的監聽隊列裏面,
                    //這裏用EPOLL_CTL_ADD來加一個新的epoll事件,通過EPOLL_CTL_DEL來減少一個
                    //epoll事件,通過EPOLL_CTL_MOD來改變一個事件的監聽方式。
                    fprintf(stderr, "epollsetinsertionerror:fd=%d", client);
                    return -1;
                }
            }
            else if(event[n].events & EPOLLIN)
            {
                //如果是已經連接的用户,並且收到數據,
                //那麼進行讀入
                int sockfd_r;
                if ((sockfd_r = event[n].data.fd) < 0)
                    continue;
                read(sockfd_r, buffer, MAXSIZE);
                //修改sockfd_r上要處理的事件為EPOLLOUT
                ev.data.fd = sockfd_r;
                ev.events = EPOLLOUT | EPOLLET;
                epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd_r, &ev)
            }
            else if(event[n].events & EPOLLOUT)
            {
                //如果有數據發送
                int sockfd_w = events[n].data.fd;
                write(sockfd_w, buffer, sizeof(buffer));
                //修改sockfd_w上要處理的事件為EPOLLIN
                ev.data.fd = sockfd_w;
                ev.events = EPOLLIN | EPOLLET;
                epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd_w, &ev)
            }
            do_use_fd(events[n].data.fd);
        }
    }

對,epoll的操作就這麼簡單,總共不過4個API:epoll_create, epoll_ctl, epoll_wait和close。
如果您對epoll的效率還不太瞭解,請參考之前關於網絡遊戲的網絡編程等相關的文章。
【注1-start】(以上內容非本人提交)
範例源代碼目的是為了循環產生ET,不能反映idle connection的狀態,該代碼結構不可應用於實際。
【注1-end】