linux中poll系統調用實作了對檔案描述符的輪詢,由于poll的實作問題,每當一個或者多個檔案描述符上有事件發生的時候,poll的核心并沒有什麼好的辦法可以知道到底是哪些檔案描述符上發生了事件,于是不得不采用周遊所有的fd_set中的檔案描述符的辦法,但是這種方式很低效,如果有很多的描述符但是隻有最後一個上發生了事件,那麼将會消耗很多的時間,于是出現了epoll,epoll本質就是應用喚醒回調函數,隻将被喚醒的wait隊列元素加入到一個表中,然後隻需要周遊該表的元素就可以了,如果還是上面的情況,那麼隻有一個wait元素被加入到表中,隻要在這個表中的元素上poll一下就完成了。
事情到此就結束了嗎?沒有!現有的epoll已經定位到了發生事件的具體的檔案描述符,但是一個描述符上可以監控很多的事件,如果監控的是read,然而write事件到來的時候也會将該描述符喚醒,那麼按照epoll的設計思想,是否可以定位到事件呢?是的,可以,這就是keyed-epoll更新檔的思想,但是在引入這個更新檔之前linux的方式就是一步一步來,先在傳統的poll機制上實作keyed擴充,這樣就做到了影響最小化,keyed就可以從一個擴充抽象成了一個機制,它就不再和poll機制綁定,其實它也沒有必要和poll機制綁定,于是單純的keyed更新檔就被提出來了,它就是實作了另外一套喚醒函數,這個系列函數的參數中添加了一個key參數,也就是加了一層判斷,隻有當key值符合一定條件時才會進行真正的喚醒,具體怎麼判斷key的邏輯,就由使用者來制定。千言萬語敵不過幾行代碼:
-static int pollwake(wait_queue_t *wait, unsigned mode, int sync, void *key)
+static int __pollwake(wait_queue_t *wait, unsigned mode, int sync, void *key)
{
struct poll_wqueues *pwq = wait->private;
DECLARE_WAITQUEUE(dummy_wait, pwq->polling_task);
@@ -194,6 +194,16 @@ static int pollwake(wait_queue_t *wait, unsigned mode, int sync, void *key)
return default_wake_function(&dummy_wait, mode, sync, key);
}
+static int pollwake(wait_queue_t *wait, unsigned mode, int sync, void *key)
+{
+ struct poll_table_entry *entry;
+
+ entry = container_of(wait, struct poll_table_entry, wait);
+ if (key && !((unsigned long)key & entry->key)) //如果事件與該entry無關,就不再執行喚醒操作
+ return 0;
+ return __pollwake(wait, mode, sync, key);
+}
/* Add a new entry */
static void __pollwait(struct file *filp, wait_queue_head_t *wait_address,
poll_table *p)
@@ -205,6 +215,7 @@ static void __pollwait(struct file *filp, wait_queue_head_t *wait_address,
get_file(filp);
entry->filp = filp;
entry->wait_address = wait_address;
+ entry->key = p->key;
init_waitqueue_func_entry(&entry->wait, pollwake);
entry->wait.private = pwq;
add_wait_queue(wait_address, &entry->wait);
@@ -418,8 +429,16 @@ int do_select(int n, fd_set_bits *fds, struct timespec *end_time)
if (file) {
f_op = file->f_op;
mask = DEFAULT_POLLMASK;
- if (f_op && f_op->poll)
+ if (f_op && f_op->poll) {
+ if (wait) { //在進行調用vfs的poll之前,先将需要監控的事件加入到key,在喚醒的時候要作為參考
+ wait->key = POLLEX_SET;
+ if (in & bit)
+ wait->key |= POLLIN_SET;
+ if (out & bit)
+ wait->key |= POLLOUT_SET;
+ }
mask = (*f_op->poll)(file, retval ? NULL : wait);
fput_light(file, fput_needed);
if ((mask & POLLIN_SET) && (in & bit)) {
res_in |= bit;
這個更新檔正如作者所說,節省了不少開銷,避免了不少喚醒操作,我個人認為,它的意義要比epoll還要大。如果一個程序監控fd1的read事件,另一個程序監控fd1的write事件,那麼這兩個程序都要加入到該fd1的vfs底層的的wait隊列中,一旦fd1上發生事件,則這兩個程序都要被喚醒而不管是什麼事件。打上這個更新檔之後,如果是read事件,那麼就隻用喚醒監控read事件的那個程序就可以了,節省了一半的喚醒動作,喚醒操作是一件很耗時的操作,因為涉及到搶占和切換,特别是毫無意義的喚醒更是要避免的,沒有事情無故喚醒别人是一件很不好的事,這個更新檔就是細化了事件檢測機制。如果将這個機制加入到epoll中,那更是如虎添翼,不但精确到了檔案描述符,更是精确到了檔案描述符的事件,在檔案描述符之下再檢測一個事件,這裡該更新檔和epoll的差別在于epoll節省了輪詢周遊的開銷但是避免不了喚醒,而keyed機制節省不了輪詢但是可以最小化喚醒操作。傳統的poll一旦被喚醒之後必須周遊所有poll清單的檔案描述符進而确定哪一個上有事件發生,而epoll不用;傳統的poll在底層,隻要有事件發生就會喚醒其睡眠隊列的所有程序而不管程序是否關心該事件,而keyed機制可以避免這種魯莽。兩個機制在諸多檔案描述符和諸多事件中定位到了一個檔案描述符的一個事件,可謂妙。
最後看一個linux中的層次問題,總有人說linux沒有實作核心級别的線程而隻有程序,可是clone中克隆的是什麼?是task_struct,task_struct是什麼?是程序嗎?不是,是線程嗎?不是,那麼它是什麼?它是程序和線程的超集,它比程序和線程的層次要高,包含程序和線程而不能說它是程序或者線程,是以不要按照windows中線程的意義來了解linux的實作,linux的架構其實很松散,打破了傳統作業系統理論對作業系統的規定。linux中統一的程序線程實作方式确實很好,很靈活,做到了正交化,意義就是程序和線程不再具有從屬關系,而是沒有關系,linux核心中沒有定義程序和線程,隻有task_struct,如果一個task_struct獨享資源,那麼它就是程序,如果很多task_struct共享資源,那麼每個task_struct就是一個線程然後它們組成一個程序,到底是什麼由是否共享資源這個第三方的開關而不是它們本身來定義,這樣很不錯,将内涵和外延分離開來。linux的方式可以實作很多的程序/線程的實作方式,這種低耦合高内聚的正交化機制是很強大的。内涵沒有意義,就是一個task_struct,加上一個有意義的“資源使用方式”這個第三方的政策就構成了外延--程序和線程
本文轉自 dog250 51CTO部落格,原文連結:http://blog.51cto.com/dog250/1273488