1. 程式人生 > >select,poll,epoll的內部機制調研

select,poll,epoll的內部機制調研

在百度文庫中看到這個帖子,總體講的不錯,但是有點錯誤,所以轉帖過來並加以改正。

另外在《Linux裝置驅動程式》中也有關於poll,select和epoll在驅動層面的支援的描述,可以參考。

1 等待佇列實現原理

1.1 功能介紹

程序有多種狀態,當程序做好準備後,它就處於就緒狀態(TASK_RUNNING),放入執行佇列,等待核心排程器來排程。當然,同一時刻可能有多個程序進入就緒狀態,但是卻可能只有1個CPU是空閒的,所以最後能不能在CPU上執行,還要取決於優先順序等多種因素。當程序進行外部裝置的IO等待操作時,由於外部裝置的操作速度一般是非常慢的,所以程序會從就緒狀態變為等待狀態(休眠),進入等待佇列,把CPU讓給其它程序。直到IO操作完成,核心“喚醒”等待的程序,於是程序再度從等待狀態變為就緒狀態。

在使用者態,程序進行IO操作時,可以有多種處理方式,如阻塞式IO,非阻塞式IO,多路複用(select/poll/epoll),AIO(aio_read/aio_write)等等。這些操作在核心態都要用到等待佇列。

1.2 相關的結構體

typedef struct __wait_queue wait_queue_t;

struct __wait_queue {

unsigned int flags;

#define WQ_FLAG_EXCLUSIVE 0x01

struct task_struct * task;

wait_queue_func_t func;

struct list_head task_list;

};

這個是等待佇列的節點,其中task表示等待佇列節點對應的程序。func表示等待佇列的回撥函式,在程序被喚醒。在很多等待佇列裡,這個func函式指標預設為空函式。但是,在select/poll/epoll函式中,這個func函式指標不為空,並且扮演著重要的角色。

struct __wait_queue_head {

spinlock_t lock;

struct list_head task_list;

};

typedef struct __wait_queue_head wait_queue_head_t;

這個是等待佇列的頭部。其中task_list裡有指向下一個節點的指標。為了保證對等待佇列的操作是原子的,還需要一個自旋鎖lock。

這裡需要提一下核心佇列中被廣泛使用的結構體struct list_head。

struct list_head {

struct list_head *next, *prev;

};

這是一個雙向連結串列。在C++中,可以通過模板來實現一個通用的雙向連結串列list<T> mylist。使用時只要設定一下T的型別,就可以使用連結串列的各種方法了。在C中,雖然沒有模板,但是可以使用一種變通的辦法來實現雙向連結串列的模板。只要型別T裡包含list_head成員,那麼,就可以通過list_head方便地把所有的節點連結起來,並進行遍歷。通過list_head獲得型別T的內容也很方便,核心裡提供了container_of()巨集。核心也提供了一套API對list_head雙向連結串列進行各種操作。通過這種方式,核心實現了程式碼的複用,而不用為每個雙向連結串列寫一套相同的連結串列操作的API。理論上來說,這種型別的雙向連結串列可以連結各種型別的結構體,只要這種結構體包含list_head成員。這與C++的模板相比,顯得更加靈活。

1.3 實現原理

可以看到,等待佇列的核心是一個list_head組成的雙向連結串列。其中,第一個節點是佇列的頭,型別為wait_queue_head_t,裡面包含了一個list_head型別的成員task_list。而接下去的每個節點型別為 wait_queue_t,裡面也有一個list_head型別的成員task_list,並且有個指標指向等待的程序。通過這種方式,核心組織了一個等待佇列。那麼,這個等待佇列怎樣與一個事件關聯呢?在核心中,程序在檔案操作等事件上的等待,一定會有一個對應的等待佇列的結構體與之對應。例如,等待管道的檔案操作(在核心看來,管道也是一種檔案)的程序都放在管道對應inode.i_pipe->wait這個等待佇列中。這樣,如果管道檔案操作完成,就可以很方便地通過inode.i_pipe->wait喚醒等待的程序。在大部分情況下(如系統呼叫read),當前程序等待IO操作的完成,只要在核心堆疊中分配一個wait_queue_t的結構體,然後初始化,把task指向當前程序的task_struct,然後呼叫add_wait_queue()放入等待佇列即可。但是,在select/poll中,由於系統呼叫要監視多個檔案描述符的操作,因此要把當前程序放入多個檔案的等待佇列,並且要分配多個wait_queue_t結構體。這時候,在堆疊上分配是不合適的。因為核心堆疊很小。所以要通過動態分配的方式來分配wait_queue_t結構體。除了在一些結構體裡直接定義等待佇列的頭部,核心的訊號量機制也大量使用了等待佇列。訊號量是為了進行程序同步而引入的。與自旋鎖不同的是,當一個程序無法獲得訊號量時,它會把自己放到這個訊號量的等待佇列中,轉變為等待狀態。當其它程序釋放訊號量時,會喚醒等待的程序。

2 select 的實現原理(核心版本2.6.9)

2.1 功能介紹

select系統呼叫的功能是對多個檔案描述符進行監視,當有檔案描述符的檔案讀寫操作完成,發生異常或者超時,該呼叫會返回這些檔案描述符。

int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds,struct timeval *timeout);

2.2 關鍵的結構體:

這裡先介紹一些關鍵的結構體:

typedef struct {

unsigned long *in, *out, *ex;

unsigned long *res_in, *res_out, *res_ex;

} fd_set_bits;

這個結構體負責儲存select在使用者態的引數。在select()中,每一個檔案描述符用一個位表示,其中1表示這個檔案是被監視的。in, out, ex指向的bit陣列表示對應的讀,寫,異常檔案的描述符。res_in, res_out,res_ex指向的bit陣列表示對應的讀,寫,異常檔案的描述符的檢測結果。

struct poll_wqueues {

poll_table pt;

struct poll_table_page * table;

int error;

};

這是最主要的結構體,它儲存了select過程中的重要資訊。它包括了兩個最重要的結構體poll_table和struct poll_table_page。接下去看看這兩個結構體。

typedef void (*poll_queue_proc)(struct file *, wait_queue_head_t *, struct

poll_table_struct *);

struct poll_table_struct {

poll_queue_proc qproc;

} poll_table;

在執行select操作時,會用到回撥函式,poll_table就是用來儲存回撥函式的。這個回撥函式非常重要,因為當檔案執行poll操作時,一般都會呼叫這個回撥函式。所以,這個回撥函式非常重要,通常負責把程序放入等待佇列等關鍵操作。下面可以看到,在select中,這個回撥函式是__pollwait(),在epoll中,這個回撥函式是 ep_ptable_queue_proc。

struct poll_table_page {

struct poll_table_page * next;

struct poll_table_entry * entry;

struct poll_table_entry entries[0];

};

這個表記錄了在select過程中生成的所有等待佇列的結點。由於select要監視多個檔案描述符,並且要把當前程序放入這些描述符的等待佇列中,因此要分配等待佇列的節點。這些節點可能如此之多,以至於不可能像通常做的那樣,在堆疊中分配它們。所以,select以動態分配的方式把它儲存在poll_table_page中。儲存的方式是單向連結串列,每個節點以頁為單位,分配多個poll_table_entry項。

現在看一下poll_table_entry: poll table的表項

struct poll_table_entry {

struct file * filp;

wait_queue_t wait;

wait_queue_head_t * wait_address;

};

其中filp是select要監視的structfile結構體,wait_address是檔案操作的等待

佇列的隊首,wait是等待佇列的節點。

2.3 select 的實現

接下去介紹select的實現

在進行一系列引數檢查後,sys_select呼叫do_select()。該函式會遍歷所有需要監視的檔案描述符,然後呼叫f_op->poll(),這個操作會做兩件事:

1.檢視檔案操作的狀態,如果這些檔案操作完成或者有異常發生(下面統稱為“使用者感興趣的事件”),在對應的fdset中標記這些檔案描述符。

2.如果retval為0(在這一輪遍歷中,迄今為止,檔案沒有發生感興趣的事,這一點有些不明白,為什麼不是通知所有監視的並且沒有發生感興趣事件的檔案描述符,這樣返回得更快)並且沒有超時,那麼,通知這些檔案,讓他們在檔案操作完成時喚醒本程序。

如果發現檔案具體通知的方式是:通過__pollwait()把自己掛到各個等待佇列中。這樣,當有檔案操作完成時,select所在程序會被喚醒。這裡涉及到一個回撥函式__pollwait()。它是在什麼時候被註冊的呢?在進入for迴圈之前,有這樣一行程式碼:

poll_initwait(&table);

它的作用就是把poll_table中的回撥函式設定為__pollwait。對檔案描述符的遍歷的迴圈會繼續,直到發生以下事件:如果有檔案操作完成或者發生異常,或者超時,或者收到訊號,select會返回相應的值,否則,do_select會呼叫schedule_timeout()進入休眠,直到超時或者被再次喚醒(這表明有使用者感興趣的事件產生),然後重新執行for迴圈,但是這一次一定能跳出迴圈體。

通知的過程如下(以管道的poll函式為例,在pipe中,f_op->poll對應的函式是pipe_poll:):當do_select遍歷所有需要監視的檔案描述符時,假設有一個檔案描述符對應的是一個管道,那麼,它執行的f_op->poll實際上是pipe_poll。pipe_poll->poll_wait->__pollwait。最終__pollwait會把當前程序掛到對應檔案的inode中的檔案描述符中。當執行pipe_write對管道進行寫操作時,操作完成後會喚醒等待佇列中所有的程序。

2.4 效能分析

從中可以看出,select需要遍歷所有的檔案描述符,就遍歷操作而言,複雜度是O(N),N是最大檔案描述符加1。此外,select引數包括了所有的檔案描述符的資訊,所以select在遍歷檔案描述符時,需要檢查檔案描述符是不是自己感興趣的。

3 poll 的實現原理

3.1 功能介紹:

poll與select實現了相同的功能,只是引數型別不同。它的原型是:

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

可以看到,poll的引數中,直接列出了要監視的檔案描述符的資訊,而不像select一樣要列出從0開始到nfds-1的所有檔案描述符。這樣的好處是,poll不需要查詢很多無關的檔案描述符的資訊,在一定場合下效率會有所提高。

3.2 關鍵的結構體:

poll用到的很多結構體與select是一樣的,如struct poll_wqueues。這是因為

poll的實現機制與select沒有本質區別。poll也用到了一些不同的結構體,這是因為poll的引數型別與select不同,用來儲存引數的結構體也不同:

相關的資料結構:

struct poll_list {

struct poll_list *next;

int len;

struct pollfd entries[0];

};

這個結構體用來儲存被監視的檔案描述符的引數。其中struct pollfd entries[0]表示這是一個不定長陣列。

struct pollfd {

int fd;

short events;

short revents;

};

這個結構體記錄被監視的檔案描述符和它的狀態。

3.3 poll 的實現

poll的實現與select也十分類似。一個區別是使用的資料結構。poll中採用了poll_list結構體來記錄要監視的檔案描述符資訊。poll_list中,每個pollfd代表一個要監視的檔案描述符的資訊。這些pollfd以陣列的形式連結到poll_list連結串列中,每個陣列的元素個數不能超過POLLFD_PER_PAGE。在把引數拷貝到核心態之後,sys_poll會呼叫do_poll()。在do_poll()中,函式遍歷poll_list連結串列,然後呼叫do_pollfd()對每個poll_list節點中的pollfd陣列進行遍歷。在do_pollfd()中,檢查陣列中的每個fd,檢查的過程與select類似,呼叫fd對應的poll函式指標:

mask = file->f_op->poll(file, *pwait);

1.如果有必要,把當前程序掛到檔案操作對應的等待列隊中,同時也放到poll

table中。

2.檢查檔案操作狀態,儲存到mask變數中。

在遍歷了所有檔案描述符後,呼叫timeout = schedule_timeout(timeout);讓當前程序進入休眠狀態,直到超時或者有檔案操作完成,喚醒當前程序才返回。那麼,在 f_op->poll中做了些什麼呢?在sys_poll中有這樣一行程式碼:

poll_initwait(&table);

可以發現,在這裡,它註冊了和select()相同的回撥函式__pollwait(),內部的實現機制也是一樣的。在這裡就不重複說了。

3.4 效能分析:

poll的引數只包括了使用者感興趣的檔案資訊,所以poll在遍歷檔案描述符時不用像select一樣檢查檔案描述符是否是自己感興趣的。從這個意義上說,poll比select稍微要高效一些。前提是:要監視的檔案描述符不連續,非常離散。

poll與select共同的問題是,他們都是遍歷所有的檔案描述符。當要監視的檔案描述符很多,並且每次只返回很少的檔案描述符時,select/poll每次都要反覆地從使用者態拷貝檔案資訊,每次都要重新遍歷檔案描述符,而且每次都要把當前程序掛到對應事件的等待佇列和poll_table的等待佇列中。這裡事實上做了很多重複勞動。

4 epoll 的實現

4.1 功能介紹

epoll與select/poll不同的一點是,它是由一組系統呼叫組成。

int epoll_create(int size);

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

int epoll_wait(int epfd, struct epoll_event *events,

int maxevents, int timeout);

epoll相關係統呼叫是在Linux 2.5.44開始引入的。該系統呼叫針對傳統的select/poll系統呼叫的不足,設計上作了很大的改動。select/poll的缺點在於:

1.每次呼叫時要重複地從使用者態讀入引數。

2.每次呼叫時要重複地掃描檔案描述符。

3.每次在呼叫開始時,要把當前程序放入各個檔案描述符的等待佇列。在呼叫結束後,又把程序從各個等待佇列中刪除。

在實際應用中,select/poll監視的檔案描述符可能會非常多,如果每次只是返回一小部分,那麼,這種情況下select/poll顯得不夠高效。epoll的設計思路,是把select/poll單個的操作拆分為1個epoll_create+多個epoll_ctrl+一個wait。此外,核心針對epoll操作添加了一個檔案系統”eventpollfs”,每一個或者多個要監視的檔案描述符都有一個對應的eventpollfs檔案系統的inode節點,主要資訊儲存在eventpoll結構體中。而被監視的檔案的重要資訊則儲存在epitem結構體中。所以他們是一對多的關係。由於在執行epoll_create和epoll_ctrl時,已經把使用者態的資訊儲存到核心態了,所以之後即使反覆地呼叫epoll_wait,也不會重複地拷貝引數,掃描檔案描述符,反覆地把當前程序放入/放出等待佇列。這樣就避免了以上的三個缺點。接下去看看它們的實現:

4.2 關鍵結構體:

/* Wrapper struct used by poll queueing */

struct ep_pqueue {

poll_table pt;

struct epitem *epi;

};

這個結構體類似於select/poll中的struct poll_wqueues。由於epoll需要在核心

態儲存大量資訊,所以光光一個回撥函式指標已經不能滿足要求,所以在這裡引入了一個新的結構體struct epitem。

/*

* Each file descriptor added to the eventpoll interface will

* have an entry of this type linked to the hash.

*/

struct epitem {

/* RB-Tree node used to link this structure to the eventpoll rb-tree */

struct rb_node rbn;

紅黑樹,用來儲存eventpoll

/* List header used to link this structure to the eventpoll ready list

*/

struct list_head rdllink;

雙向連結串列,用來儲存已經完成的eventpoll

/* The file descriptor information this item refers to */

struct epoll_filefd ffd;

這個結構體對應的被監聽的檔案描述符資訊

/* Number of active wait queue attached to poll operations */

int nwait;

poll操作中事件的個數

/* List containing poll wait queues */

struct list_head pwqlist;

雙向連結串列,儲存著被監視檔案的等待佇列,功能類似於select/poll中的poll_table

/* The "container" of this item */

struct eventpoll *ep;

指向eventpoll,多個epitem對應一個eventpoll

/* The structure that describe the interested events and the source fd

*/

struct epoll_event event;

記錄發生的事件和對應的fd

/*

* Used to keep track of the usage count of the structure. This avoids

* that the structure will desappear from underneath our processing.

*/

atomic_t usecnt;

引用計數

/* List header used to link this item to the "struct file" itemslist */

struct list_head fllink;

雙向連結串列,用來連結被監視的檔案描述符對應的struct file。因為file裡有f_ep_link,

用來儲存所有監視這個檔案的epoll節點

/* List header used to link the item to the transfer list */

struct list_head txlink;

雙向連結串列,用來儲存傳輸佇列

/*

* This is used during the collection/transfer of events to userspace

* to pin items empty events set.

*/

unsigned int revents;

檔案描述符的狀態,在收集和傳輸時用來鎖住空的事件集合

};

該結構體用來儲存與epoll節點關聯的多個檔案描述符,儲存的方式是使用紅黑樹實現的hash表。至於為什麼要儲存,下文有詳細解釋。它與被監聽的檔案描述符一一對應。

struct eventpoll {

/* Protect the this structure access */

rwlock_t lock;

讀寫鎖

/*

* This semaphore is used to ensure that files are not removed

* while epoll is using them. This is read-held during the event

* collection loop and it is write-held during the file cleanup

* path, the epoll file exit code and the ctl operations.

*/

struct rw_semaphore sem;

讀寫訊號量

/* Wait queue used by sys_epoll_wait() */

wait_queue_head_t wq;

/* Wait queue used by file->poll() */

wait_queue_head_t poll_wait;

/* List of ready file descriptors */

struct list_head rdllist;

已經完成的操作事件的佇列。

/* RB-Tree root used to store monitored fd structs */

struct rb_root rbr;

儲存epoll監視的檔案描述符

};

這個結構體儲存了epoll檔案描述符的擴充套件資訊,它被儲存在file結構體的private_data中。它與epoll檔案節點一一對應。通常一個epoll檔案節點對應多個被監視的檔案描述符。所以一個eventpoll結構體會對應多個epitem結構體。

那麼,epoll中的等待事件放在哪裡呢?見下面

/* Wait structure used by the poll hooks */

struct eppoll_entry {

/* List header used to link this structure to the "structepitem" */

struct list_head llink;

/* The "base" pointer is set to the container "structepitem" */

void *base;

/*

* Wait queue item that will be linked to the target file wait

* queue head.

*/

wait_queue_t wait;

/* The wait queue head that linked the "wait" wait queue item */

wait_queue_head_t *whead;

};

與select/poll的struct poll_table_entry相比,epoll的表示等待佇列節點的結構體只是稍有不同,與structpoll_table_entry比較一下。

struct poll_table_entry {

struct file * filp;

wait_queue_t wait;

wait_queue_head_t * wait_address;

};

由於epitem對應一個被監視的檔案,所以通過base可以方便地得到被監視的檔案資訊。又因為一個檔案可能有多個事件發生,所以用llink連結這些事件。

4.3 epoll_create 的實現

epoll_create()的功能是建立一個eventpollfs檔案系統的inode節點。具體由ep_getfd()完成。ep_getfd()先呼叫ep_eventpoll_inode()建立一個inode節點,然後呼叫d_alloc()為inode分配一個dentry。最後把file,dentry,inode三者關聯起來。在執行了ep_getfd()之後,它又呼叫了ep_file_init(),分配了eventpoll結構體,並把eventpoll的指標賦給file結構體,這樣eventpoll就與file結構體關聯起來了。需要注意的是epoll_create()的引數size實際上只是起參考作用,只要它不小於等於0,就並不限制這個epoll inode關聯的檔案描述符數量。

4.4 epoll_ctl 的實現

epoll_ctl的功能是實現一系列操作,如把檔案與eventpollfs檔案系統的inode節點關聯起來。這裡要介紹一下eventpoll結構體,它儲存在file->f_private中,記錄了eventpollfs檔案系統的inode節點的重要資訊,其中成員rbr儲存了該epoll檔案節點監視的所有檔案描述符。組織的方式是一棵紅黑樹,這種結構體在查詢節點時非常高效。首先它呼叫ep_find()從eventpoll中的紅黑樹獲得epitem結構體。然後根據op引數的不同而選擇不同的操作。如果op為EPOLL_CTL_ADD,那麼正常情況下epitem是不可能在eventpoll的紅黑樹中找到的,所以呼叫ep_insert建立一個epitem結構體並插入到對應的紅黑樹中。

ep_insert()首先分配一個epitem物件,對它初始化後,把它放入對應的紅黑樹。此外,這個函式還要作一個操作,就是把當前程序放入對應檔案操作的等待佇列。這一步是由下面的程式碼完成的。

init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);

。。。

revents = tfile->f_op->poll(tfile, &epq.pt);

函式先呼叫init_poll_funcptr註冊了一個回撥函式ep_ptable_queue_proc,這個函式會在呼叫f_op->poll時被執行。該函式分配一個epoll等待佇列結點eppoll_entry:一方面把它掛到檔案操作的等待佇列中,另一方面把它掛到epitem的佇列中。此外,它還註冊了一個等待佇列的回撥函式ep_poll_callback。當檔案操作完成,喚醒當前程序之前,會呼叫ep_poll_callback(),把eventpoll放到epitem的完成佇列中(註釋:通過檢視程式碼,此處應該是把epitem放到eventpoll的完成佇列,只有這樣才能在epoll_wait()中只要看eventpoll的完成佇列即可得到所有的完成檔案描述符),並喚醒等待程序。如果在執行f_op->poll以後,發現被監視的檔案操作已經完成了,那麼把它放在完成佇列中了,並立即把等待操作的那些程序喚醒。

4.5 epoll_wait 的實現

epoll_wait的工作是等待檔案操作完成並返回。它的主體是ep_poll(),該函式在for迴圈中(註釋:這裡沒有迴圈。)檢查epitem(註釋:這裡應該是eventpoll)中有沒有已經完成的事件,有的話就把結果返回。沒有的話呼叫schedule_timeout()進入休眠,直到程序被再度喚醒或者超時。

4.6 效能分析

epoll機制是針對select/poll的缺陷設計的。通過新引入的eventpollfs檔案系統,epoll把引數拷貝到核心態,在每次輪詢時不會重複拷貝。通過把操作拆分為epoll_create,epoll_ctl,epoll_wait,避免了重複地遍歷要監視的檔案描述符。此外,由於呼叫epoll的程序被喚醒後,只要直接從epitem的完成佇列中找出完成的事件,找出完成事件的複雜度由O(N)降到了O(1)。但是epoll的效能提高是有前提的,那就是監視的檔案描述符非常多,而且每次完成操作的檔案非常少。所以,epoll能否顯著提高效率,取決於實際的應用場景。這方面需要進一步測試。

5 參考文獻:

1.<Advanced Unix Environment Programing>

2.Using epoll() For Asynchronous Network Programming

http://blog.kovyrin.net/2006/04/13/epoll-asynchronous-network-programming/

3.<Linux核心原始碼情景分析>select相關章節。__