1. 程式人生 > >轉--select/poll/epoll到底是什麼一回事

轉--select/poll/epoll到底是什麼一回事

面試題:說說select/poll/epoll的區別。 
這是面試後臺開發時的高頻面試題,屬於網路程式設計和IO那一塊的知識。Android裡面的Handler訊息處理機制的底層實現就用到了epoll。 
為此,我在Google上看了很多相關文章,才大概搞懂是怎麼一回事。

背景知識
檔案描述符fd
檔案描述符(File descriptor)是電腦科學中的一個術語,是一個用於表述指向檔案的引用的抽象化概念。

檔案描述符在形式上是一個非負整數。實際上,它是一個索引值,指向核心為每一個程序所維護的該程序開啟檔案的記錄表。當程式開啟一個現有檔案或者建立一個新檔案時,核心向程序返回一個檔案描述符。在程式設計中,一些涉及底層的程式編寫往往會圍繞著檔案描述符展開。但是檔案描述符這一概念往往只適用於UNIX、Linux這樣的作業系統。在Linux系統中,流在核心中可以表示成檔案的形式。

IO模型
IO可以理解成對流的操作。

一般對於一個read操作發生時,它會經歷兩個階段。

第一個階段是等待資料準備。
第二個階段是真正讀取的過程,將資料從核心緩衝區拷貝到使用者程序緩衝區中,
而五種常見的IO模型也是圍繞這兩個階段來區分的。

同步模型(synchronous IO) 
阻塞IO(bloking IO)
非阻塞IO(non-blocking IO)
多路複用IO(multiplexing IO)
訊號驅動式IO(signal-driven IO)
非同步IO(asynchronous IO)
其中,IO多路複用就是一種機制,實現一個程序可以監視多個描述符,一旦某個描述符就緒,就能夠通知程式進行相應的讀寫操作。IO多路複用相比於多執行緒的優勢在於系統的開銷小,系統不必建立和維護程序或執行緒,免去了執行緒或程序的切換帶來的開銷。而作業系統支援IO多路複用的系統呼叫有select,poll和epoll。

select
先來看看select的函式宣告:

int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
1
fd_set是表示檔案描述符集合的資料結構。readfds,writefds和exceptfds分別對應三類檔案描述符集。當select被呼叫時,內部邏輯如下:

將3個fd集copy到核心,這裡限制了fd最大數量為1024
執行緒阻塞,直到超時或核心檢測到有fd可讀或可寫,核心會通知監控者select,select返回可讀或可寫的fd總數
那麼使用者程序如何找到可讀可寫的fd呢?select會將之前傳遞給核心的fd集從核心copy到使用者程序。使用者程序通過遍歷的方式找到可讀可寫的fd。
缺點:

copy次數過多,而且每次呼叫select方法都要進行fd集的copy操作
select監控fd數量有限
使用者程序通過遍歷的方式找到可讀寫的fd,時間複雜度為o(n),IO效率隨著fd數量增多而線性下降
poll
先來看看poll的函式宣告:

int poll (struct pollfd *fds, unsigned int nfds, int timeout);
1
pollfd是表示檔案描述符集合的資料結構。

struct pollfd {
    int fd; //檔案描述符
    short events; //監視的請求事件
    short revents; //已發生的事件
};
1
2
3
4
5
poll與select差不多,但poll的pollfd沒有最大數量的限制,可是IO效率依舊沒有提升orz。

epoll
select/poll都只有一個方法,而epoll的操作過程有3個方法,分別是epoll_create(), epoll_ctl(),epoll_wait()。

epoll_create()
int epoll_create(int size);//用於建立一個epoll的控制代碼,size是指監聽的描述符個數。
1
該方法會在核心建立專屬於epoll的高速cache區,並在該緩衝區建立紅黑樹和就緒連結串列,使用者態傳入的檔案控制代碼將被放到紅黑樹中。

epoll_ctl()
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
1
該方法對epoll_create()所建立的核心cache區進行操作的,操作物件是需要監聽的fd。

比如,把要監聽的fd註冊到cache內,那麼epoll_ctl()會將fd插入到紅黑樹中,並向核心註冊了該fd的回撥函式。核心在檢測到某fd可讀可寫時則呼叫該回調函式,而回調函式的工作是將fd放到就緒連結串列。

epoll_wait()
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);  
1
epoll_wait只需監控就緒連結串列,如果就緒連結串列有fd,則表示該fd可讀可寫,並返回給使用者態(少量的copy);

該函式返回需要處理的事件數目,如返回0表示已超時。

小結
執行epoll_create時,在建立了紅黑樹和就緒連結串列。執行epoll_ctl時,如果增加fd,則檢查在紅黑樹中是否存在,存在立即返回,不存在則新增到樹上,然後向核心註冊回撥函式,用於當中斷事件到來時向準備就緒連結串列中插入資料。執行epoll_wait時返回就緒連結串列裡的資料即可。

因此,epoll比select和poll高效的原因是:

減少了使用者態和核心態之間檔案控制代碼的copy
降低了在檔案控制代碼集中查詢的時間複雜度。用紅黑樹維護fd集,可以將查詢fd的時間複雜度降為o(logn)。
參考
https://www.zhihu.com/question/20122137
http://www.jianshu.com/p/dfd940e7fca2#
http://gityuan.com/2015/12/06/linux_epoll/