最近簡單看了一把 linux-3.10.25 kernel中select/poll/epoll這個幾個IO事件檢測API的實現。此處做一些記錄。
其基本的原理是相同的,流程如下
- 先依次調用fd對應的struct file.f_op->poll()方法(如果有提供實現的話),嘗試檢查每個提供待檢測IO的fd是否已經有IO事件就緒
- 如果已經有IO事件就緒,則直接所收集到的IO事件返回,本次調用結束
- 如果暫時沒有IO事件就緒,則根據所給定的超時參數,選擇性地進入等待
- 如果超時參數指示不等待,則本次調用結束,無IO事件返回
- 如果超時參數指示等待(等待一段時間或持續等待),則將當前select/poll/epoll的調用任務掛起
- 當所檢測的fd任何一個有新的IO事件發生時,會將上述的處於等待的任務喚醒。任務被喚醒之后,重新執行1中的IO事件收集過程,將此時收集到的IO事件返回,本次的調用過程結束。
可以看出流程並不復雜,本文按照上述流程,先對select/poll的實現做進一步分析,epoll的實現要復雜一些,另外做敘述。
上述比較關鍵的地方是
1. 起初在無IO事件時,調用任務在被掛起之后,fd上有IO事件發生時,如何將掛起的任務喚醒?
接下來一poll()的實現過程,介紹這一過程的實現原理。
要實現喚醒過程,比較關鍵的步驟為
- 在上述初次調用 struct file.f_op->poll() 時,除了fd對應的 struct file 指針之外,還需要傳入一個 poll_table 類型的指針。 該 poll_table 具體是在 poll() 的實現過程中定義提供的。
- 在各個fd的struct file.f_op->poll()方法中,需要對應調用一下poll_wait()
其中關鍵
struct file.f_op->poll() 方法的作用是既是直接查詢fd上的IO事件
而 poll_wait() 既是實現根據需要將 poll() 的調用任務,選擇性的添加到fd的IO事件等待隊列中。
注意此處是“選擇性的添加”,為何如此說,得看 poll_wait() 的實現,其是一個inline函數,如下
static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p) { if (p && p->_qproc && wait_address) p->_qproc(filp, wait_address, p); }
定義在 include/linux/poll.h 中。
可以看出,當調用 struct file.f_op->poll() 時,poll_table 的 _qproc 成員有定義時,poll_wait() 的調用才有實際的效果。
而前面已經說了, “poll_table 具體是在 poll() 的實現過程中定義提供的”,具體是 poll_initwait(&table) 調用對 poll_table 進行初始化的。
再進一步看一下 poll_initwait() 實現,如下
1 void poll_initwait(struct poll_wqueues *pwq) 2 { 3 init_poll_funcptr(&pwq->pt, __pollwait); 4 pwq->polling_task = current; 5 pwq->triggered = 0; 6 pwq->error = 0; 7 pwq->table = NULL; 8 pwq->inline_index = 0; 9 }
其中
1 static inline void init_poll_funcptr(poll_table *pt, poll_queue_proc qproc) 2 { 3 pt->_qproc = qproc; 4 pt->_key = ~0UL; /* all events enabled */ 5 }
也就是說初次遍歷 poll 給定的諸fd,直至初始收集到IO事件,都會對 __pollwait() 調用一把。
實際上也正是在 __pollwait() 中實現將 poll() 的調用任務添加到 fd 的等待隊列中,並且指定了喚醒執行過程中的回調;並且初次對所有的fd調用過 struct file.f_op->poll() 之后,會將 poll_table._qproc 只為NULL,故而后續重新,調用 struct file.f_op->poll() 時不會重復的將調用線程添加到fd的等待隊列兩種。在 poll() 的實現中,該回調函數是 pollwake(),具體有 pollwake() 調用 kernel 的喚醒API(default_wake_function())將 poll() 的調用任務喚醒。
(由於之前具體是在 poll() 的實現中進行掛起的,所以自然喚醒過程也應該放在 poll() 的實現中了)
上面這些內容是介紹了 poll() 這一側為等待喚醒,所做的准備工作。接下來看看,當fd上有IO時間就緒時,是如何將那些對自身等待的任務喚醒的。
此處以 eventfd 的實現為例,其實現較為簡單,方便敘述。
先簡單說一下 eventfd 的特性,其內部維持了一個64位的計數器。當該計數器大於0時,fd上有可讀事件;當該計數器值小於 ULLONG_MAX 時,有可寫實現
看其實現代碼知道,其具體對該計數值更新過程發生在 eventfd_ctx_read() / eventfd_write() 中。
eventfd_ctx_read()操作之后,該內部計數器值減小,結尾如下代碼中片段會對的內部 wait_queue 中記錄的等待任務進行喚醒操作。
1 if (likely(res == 0)) { 2 eventfd_ctx_do_read(ctx, cnt); 3 if (waitqueue_active(&ctx->wqh)) 4 wake_up_locked_poll(&ctx->wqh, POLLOUT); 5 }
而在 eventfd_write() 操作完畢后,內部計數器值增大,結尾如下代碼片段會對對的內部 wait_queue 中記錄的等待任務進行喚醒操作。
1 if (likely(res > 0)) { 2 ctx->count += ucnt; 3 if (waitqueue_active(&ctx->wqh)) 4 wake_up_locked_poll(&ctx->wqh, POLLIN); 5 }
至此, 完整的喚醒通知過程也就介紹完成了。
select() 實現過程與 poll() 完全一致,不同的待檢測的fd和結果事件的返回方式不同而已。poll() 內部是使用鏈表進行記錄,而 select() 是使用的bit位序列進行記錄的而已。
詳細請看 fs/select.c 中看 poll() 和 select() 的實現代碼。
epoll() 的事件檢查方式與 poll() / select() 類似,一個明顯的差別是各個 fd 是注冊到 epoll 內部進行記錄管理的,而 poll/ select 需要每次從用戶態拷貝到 kernel 中,
且 epoll 內部對 fd 的記錄是用rbtree,並且還有自己自己的一些獨有的特點,這些內容將在另外的文章中進行描述。本文到此為止
~~ end ~~