epoll 或者 kqueue 的原理是什麼?

為什麼epoll和kqueue可以用基於事件的方式,單線程的實現並發?我沒看過linux內核,對這方面一直有疑問……


可能我沒有說太明白,我知道您說的這些,我是想了解底層原理。在底層,linux內核是如何知道這些事件的,通過輪詢嗎?


原理其它答案講得差不多了,我就補一句,從kernel層面將,事件產生有可能不是由硬體中斷觸發的,在一定情況下kernel的確會輪詢,因為響應硬體中斷是一個成本比較高的操作。
以網卡為例,當數據量很少的時候,每來一個數據包網卡都回產生一個中斷,kernel響應這個中斷,從網卡緩衝區中讀出數據放進協議棧處理,當滿足一定條件時,kernel回調用戶代碼,這裡的「回調」一般情況下是指從一個kernel syscall中返回(在此之前用戶代碼一直處於block狀態)。
當數據量很大時,每個包都產生一個中斷就划不來了,此時kernel可以啟動interrupt coalescing機制,讓網卡做中斷合併,也就是說來足夠多的數據包或者等待一個timeout才會產生一個中斷,kernel在響應中斷時會把所有數據一起讀出來處理,這樣可以有效的降低中斷次數。
當數據量更大時,網卡緩衝區里幾乎總是有未處理的數據,此時kernel乾脆會禁掉網卡的中斷,切換到輪詢處理的模式,說白了就是跑一個忙循環不停地讀網卡緩衝區里的數據,這樣綜合開銷更低。


2013-10-27更新:由於此文陸陸續續收到贊同,而且其中有些地方並不完全正確,特在本文最後予以訂正

我不了解樓主的層次,我必須從很多基礎的概念開始構建這個答案,並且可能引申到很多別的問題。

首先我們來定義流的概念,一個流可以是文件,socket,pipe等等可以進行I/O操作的內核對象。
不管是文件,還是套接字,還是管道,我們都可以把他們看作流。
之後我們來討論I/O的操作,通過read,我們可以從流中讀入數據;通過write,我們可以往流寫入數據。現在假定一個情形,我們需要從流中讀數據,但是流中還沒有數據,(典型的例子為,客戶端要從socket讀如數據,但是伺服器還沒有把數據傳回來),這時候該怎麼辦?

  • 阻塞。阻塞是個什麼概念呢?比如某個時候你在等快遞,但是你不知道快遞什麼時候過來,而且你沒有別的事可以干(或者說接下來的事要等快遞來了才能做);那麼你可以去睡覺了,因為你知道快遞把貨送來時一定會給你打個電話(假定一定能叫醒你)。
  • 非阻塞輪詢。接著上面等快遞的例子,如果用忙輪詢的方法,那麼你需要知道快遞員的手機號,然後每分鐘給他掛個電話:「你到了沒?」

很明顯一般人不會用第二種做法,不僅顯很無腦,浪費話費不說,還佔用了快遞員大量的時間。
大部分程序也不會用第二種做法,因為第一種方法經濟而簡單,經濟是指消耗很少的CPU時間,如果線程睡眠了,就掉出了系統的調度隊列,暫時不會去瓜分CPU寶貴的時間片了。

為了了解阻塞是如何進行的,我們來討論緩衝區,以及內核緩衝區,最終把I/O事件解釋清楚。緩衝區的引入是為了減少頻繁I/O操作而引起頻繁的系統調用(你知道它很慢的),當你操作一個流時,更多的是以緩衝區為單位進行操作,這是相對於用戶空間而言。對於內核來說,也需要緩衝區。
假設有一個管道,進程A為管道的寫入方,B為管道的讀出方。

  1. 假設一開始內核緩衝區是空的,B作為讀出方,被阻塞著。然後首先A往管道寫入,這時候內核緩衝區由空的狀態變到非空狀態,內核就會產生一個事件告訴B該醒來了,這個事件姑且稱之為「緩衝區非空」。
  2. 但是「緩衝區非空」事件通知B後,B卻還沒有讀出數據;且內核許諾了不能把寫入管道中的數據丟掉這個時候,A寫入的數據會滯留在內核緩衝區中,如果內核也緩衝區滿了,B仍未開始讀數據,最終內核緩衝區會被填滿,這個時候會產生一個I/O事件,告訴進程A,你該等等(阻塞)了,我們把這個事件定義為「緩衝區滿」。
  3. 假設後來B終於開始讀數據了,於是內核的緩衝區空了出來,這時候內核會告訴A,內核緩衝區有空位了,你可以從長眠中醒來了,繼續寫數據了,我們把這個事件叫做「緩衝區非滿」
  4. 也許事件Y1已經通知了A,但是A也沒有數據寫入了,而B繼續讀出數據,知道內核緩衝區空了。這個時候內核就告訴B,你需要阻塞了!,我們把這個時間定為「緩衝區空」。

這四個情形涵蓋了四個I/O事件,緩衝區滿,緩衝區空,緩衝區非空,緩衝區非滿(注都是說的內核緩衝區,且這四個術語都是我生造的,僅為解釋其原理而造)。這四個I/O事件是進行阻塞同步的根本。(如果不能理解「同步」是什麼概念,請學習操作系統的鎖,信號量,條件變數等任務同步方面的相關知識)。

然後我們來說說阻塞I/O的缺點。但是阻塞I/O模式下,一個線程只能處理一個流的I/O事件。如果想要同時處理多個流,要麼多進程(fork),要麼多線程(pthread_create),很不幸這兩種方法效率都不高。
於是再來考慮非阻塞忙輪詢的I/O方式,我們發現我們可以同時處理多個流了(把一個流從阻塞模式切換到非阻塞模式再此不予討論):
while true {
for i in stream[]; {
if i has data
read until unavailable
}
}
我們只要不停的把所有流從頭到尾問一遍,又從頭開始。這樣就可以處理多個流了,但這樣的做法顯然不好,因為如果所有的流都沒有數據,那麼只會白白浪費CPU。這裡要補充一點,阻塞模式下,內核對於I/O事件的處理是阻塞或者喚醒,而非阻塞模式下則把I/O事件交給其他對象(後文介紹的select以及epoll)處理甚至直接忽略。

為了避免CPU空轉,可以引進了一個代理(一開始有一位叫做select的代理,後來又有一位叫做poll的代理,不過兩者的本質是一樣的)。這個代理比較厲害,可以同時觀察許多流的I/O事件,在空閑的時候,會把當前線程阻塞掉,當有一個或多個流有I/O事件時,就從阻塞態中醒來,於是我們的程序就會輪詢一遍所有的流(於是我們可以把「忙」字去掉了)。代碼長這樣:
while true {
select(streams[])
for i in streams[] {
if i has data
read until unavailable
}
}
於是,如果沒有I/O事件產生,我們的程序就會阻塞在select處。但是依然有個問題,我們從select那裡僅僅知道了,有I/O事件發生了,但卻並不知道是那幾個流(可能有一個,多個,甚至全部),我們只能無差別輪詢所有流,找出能讀出數據,或者寫入數據的流,對他們進行操作。
但是使用select,我們有O(n)的無差別輪詢複雜度,同時處理的流越多,每一次無差別輪詢時間就越長。再次
說了這麼多,終於能好好解釋epoll了
epoll可以理解為event poll,不同於忙輪詢和無差別輪詢,epoll之會把哪個流發生了怎樣的I/O事件通知我們。此時我們對這些流的操作都是有意義的。(複雜度降低到了O(k),k為產生I/O事件的流的個數,也有認為O(1)的[更新 1])
在討論epoll的實現細節之前,先把epoll的相關操作列出[更新 2]:

  • epoll_create 創建一個epoll對象,一般epollfd = epoll_create()
  • epoll_ctl (epoll_add/epoll_del的合體),往epoll對象中增加/刪除某一個流的某一個事件
    比如
    epoll_ctl(epollfd, EPOLL_CTL_ADD, socket, EPOLLIN);//有緩衝區內有數據時epoll_wait返回
    epoll_ctl(epollfd, EPOLL_CTL_DEL, socket, EPOLLOUT);//緩衝區可寫入時epoll_wait返回
  • epoll_wait(epollfd,...)等待直到註冊的事件發生

(註:當對一個非阻塞流的讀寫發生緩衝區滿或緩衝區空,write/read會返回-1,並設置errno=EAGAIN。而epoll只關心緩衝區非滿和緩衝區非空事件)。
一個epoll模式的代碼大概的樣子是:
while true {
active_stream[] = epoll_wait(epollfd)
for i in active_stream[] {
read or write till unavailable
}
}
限於篇幅,我只說這麼多,以揭示原理性的東西,至於epoll的使用細節,請參考man和google,實現細節,請參閱linux kernel source。
======================================
[更新1]: 原文為O(1),但實際上O(k)更為準確
[更新2]: 原文所列第二點說法讓人產生EPOLLIN/EPOLLOUT等同於「緩衝區非空」和「緩衝區非滿」的事件,但並非如此,詳細可以Google關於epoll的邊緣觸發和水平觸發。


第一部分:select和epoll的任務

關鍵詞:應用程序 文件句柄 用戶態 內核態 監控者

要比較epoll相比較select高效在什麼地方,就需要比較二者做相同事情的方法。

要完成對I/O流的復用需要完成如下幾個事情:

1.用戶態怎麼將文件句柄傳遞到內核態?

2.內核態怎麼判斷I/O流可讀可寫?

3.內核怎麼通知監控者有I/O流可讀可寫?

4.監控者如何找到可讀可寫的I/O流並傳遞給用戶態應用程序?

5.繼續循環時監控者怎樣重複上述步驟?

搞清楚上述的步驟也就能解開epoll高效的原因了。

select的做法:

步驟1的解法:select創建3個文件描述符集,並將這些文件描述符拷貝到內核中,這裡限制了文件句柄的最大的數量為1024(注意是全部傳入---第一次拷貝);

步驟2的解法:內核針對讀緩衝區和寫緩衝區來判斷是否可讀可寫,這個動作和select無關;

步驟3的解法:內核在檢測到文件句柄可讀/可寫時就產生中斷通知監控者select,select被內核觸發之後,就返回可讀可寫的文件句柄的總數;

步驟4的解法:select會將之前傳遞給內核的文件句柄再次從內核傳到用戶態(第2次拷貝),select返回給用戶態的只是可讀可寫的文件句柄總數,再使用FD_ISSET宏函數來檢測哪些文件I/O可讀可寫(遍歷);

步驟5的解法:select對於事件的監控是建立在內核的修改之上的,也就是說經過一次監控之後,內核會修改位,因此再次監控時需要再次從用戶態向內核態進行拷貝(第N次拷貝)

epoll的做法:

步驟1的解法:首先執行epoll_create在內核專屬於epoll的高速cache區,並在該緩衝區建立紅黑樹和就緒鏈表,用戶態傳入的文件句柄將被放到紅黑樹中(第一次拷貝)。

步驟2的解法:內核針對讀緩衝區和寫緩衝區來判斷是否可讀可寫,這個動作與epoll無關;

步驟3的解法:epoll_ctl執行add動作時除了將文件句柄放到紅黑樹上之外,還向內核註冊了該文件句柄的回調函數,內核在檢測到某句柄可讀可寫時則調用該回調函數,回調函數將文件句柄放到就緒鏈表。

步驟4的解法:epoll_wait只監控就緒鏈表就可以,如果就緒鏈表有文件句柄,則表示該文件句柄可讀可寫,並返回到用戶態(少量的拷貝);

步驟5的解法:由於內核不修改文件句柄的位,因此只需要在第一次傳入就可以重複監控,直到使用epoll_ctl刪除,否則不需要重新傳入,因此無多次拷貝。

簡單說:epoll是繼承了select/poll的I/O復用的思想,並在二者的基礎上從監控IO流、查找I/O事件等角度來提高效率,具體地說就是內核句柄列表、紅黑樹、就緒list鏈表來實現的。

第二部分:epoll詳解

先簡單回顧下如何使用C庫封裝的3個epoll系統調用吧。

  1. int epoll_create(int size);
  2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  3. int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);

使用起來很清晰:

A.epoll_create建立一個epoll對象。參數size是內核保證能夠正確處理的最大句柄數,多於這個最大數時內核可不保證效果。

B.epoll_ctl可以操作上面建立的epoll,例如,將剛建立的socket加入到epoll中讓其監控,或者把 epoll正在監控的某個socket句柄移出epoll,不再監控它等等(也就是將I/O流放到內核)。

C.epoll_wait在調用時,在給定的timeout時間內,當在監控的所有句柄中有事件發生時,就返回用戶態的進程(也就是在內核層面捕獲可讀寫的I/O事件)。

從上面的調用方式就可以看到epoll比select/poll的優越之處:

因為後者每次調用時都要傳遞你所要監控的所有socket給select/poll系統調用,這意味著需要將用戶態的socket列表copy到內核態,如果以萬計的句柄會導致每次都要copy幾十幾百KB的內存到內核態,非常低效。而我們調用epoll_wait時就相當於以往調用select/poll,但是這時卻不用傳遞socket句柄給內核,因為內核已經在epoll_ctl中拿到了要監控的句柄列表。

====&>select監控的句柄列表在用戶態,每次調用都需要從用戶態將句柄列表拷貝到內核態,但是epoll中句柄就是建立在內核中的,這樣就減少了內核和用戶態的拷貝,高效的原因之一。

所以,實際上在你調用epoll_create後,內核就已經在內核態開始準備幫你存儲要監控的句柄了,每次調用epoll_ctl只是在往內核的數據結構里塞入新的socket句柄。

在內核里,一切皆文件。所以,epoll向內核註冊了一個文件系統,用於存儲上述的被監控socket。當你調用epoll_create時,就會在這個虛擬的epoll文件系統里創建一個file結點。當然這個file不是普通文件,它只服務於epoll。

epoll在被內核初始化時(操作系統啟動),同時會開闢出epoll自己的內核高速cache區,用於安置每一個我們想監控的socket,這些socket會以紅黑樹的形式保存在內核cache里,以支持快速的查找、插入、刪除。這個內核高速cache區,就是建立連續的物理內存頁,然後在之上建立slab層,簡單的說,就是物理上分配好你想要的size的內存對象,每次使用時都是使用空閑的已分配好的對象。

epoll高效的原因:

這是由於我們在調用epoll_create時,內核除了幫我們在epoll文件系統里建了個file結點,在內核cache里建了個紅黑樹用於存儲以後epoll_ctl傳來的socket外,還會再建立一個list鏈表,用於存儲準備就緒的事件.

epoll_wait調用時,僅僅觀察這個list鏈表裡有沒有數據即可。有數據就返回,沒有數據就sleep,等到timeout時間到後即使鏈表沒數據也返回。所以,epoll_wait非常高效。而且,通常情況下即使我們要監控百萬計的句柄,大多一次也只返回很少量的準備就緒句柄而已,所以,epoll_wait僅需要從內核態copy少量的句柄到用戶態而已.

那麼,這個準備就緒list鏈表是怎麼維護的呢?

當我們執行epoll_ctl時,除了把socket放到epoll文件系統里file對象對應的紅黑樹上之外,還會給內核中斷處理程序註冊一個回調函數,告訴內核,如果這個句柄的中斷到了,就把它放到準備就緒list鏈表裡。所以,當一個socket上有數據到了,內核在把網卡上的數據copy到內核中後就來把socket插入到準備就緒鏈表裡了。

epoll綜合的執行過程:

如此,一棵紅黑樹,一張準備就緒句柄鏈表,少量的內核cache,就幫我們解決了大並發下的socket處理問題。執行epoll_create時,創建了紅黑樹和就緒鏈表,執行epoll_ctl時,如果增加socket句柄,則檢查在紅黑樹中是否存在,存在立即返回,不存在則添加到樹榦上,然後向內核註冊回調函數,用於當中斷事件來臨時向準備就緒鏈表中插入數據。執行epoll_wait時立刻返回準備就緒鏈表裡的數據即可。

epoll水平觸發和邊緣觸發的實現:

當一個socket句柄上有事件時,內核會把該句柄插入上面所說的準備就緒list鏈表,這時我們調用epoll_wait,會把準備就緒的socket拷貝到用戶態內存,然後清空準備就緒list鏈表, 最後,epoll_wait幹了件事,就是檢查這些socket,如果不是ET模式(就是LT模式的句柄了),並且這些socket上確實有未處理的事件時,又把該句柄放回到剛剛清空的準備就緒鏈表了,所以,非ET的句柄,只要它上面還有事件,epoll_wait每次都會返回。而ET模式的句柄,除非有新中斷到,即使socket上的事件沒有處理完,也是不會次次從epoll_wait返回的。

====&>區別就在於epoll_wait將socket返回到用戶態時是否情況就緒鏈表。

第三部分:epoll高效的本質

1.減少用戶態和內核態之間的文件句柄拷貝;

2.減少對可讀可寫文件句柄的遍歷;


收到了在知乎的第一個贊.
網卡設備對應一個中斷號, 當網卡收到網路端的消息的時候會向CPU發起中斷請求, 然後CPU處理該請求. 通過驅動程序 進而操作系統得到通知, 系統然後通知epoll, epoll通知用戶代碼. 大致流程是這樣.
-----------
在內核的最底層是中斷 類似系統回調的機制 不是輪詢, 在這個基礎上再去看 @藍形參 的回答.


感謝 @羊羊羊 評論刺激,結束長期醞釀~


搞清楚這個問題,有三個關鍵的操作系統背景知識需要理解:

  • 進程執行調度方法
  • 內核執行中斷上下文(Interrupt Context
  • Wait Queue

這裡挑重點過程說一下,忽略很多細節和邊緣 case,詳細過程網上很容易查找到,或者有能力的可以直接看內核源碼。

進程執行調度方法:操作系統總體上按照時間片來調度進程執行,進程執行調度狀態分為三種:Running、Ready 和 Block(具體狀態命名可能不是教科書準確)。等待資源就緒的進程會置為 Block 狀態(比如調用 accept 並阻塞的進程),資源就緒可以隨時運行的進程會放在每個 CPU 的調度隊列里,獲得當前 CPU 時間片運行中的進程是 Running 狀態,等待 CPU 時間片分配的進程是 Ready 狀態。

內核執行中斷上下文:內核在處理硬體中斷時,會直接打斷正在執行的 Running 狀態進程(包括系統調用),進行必要的內存拷貝和狀態更新(比如處理 TCP 握手),結束中斷處理後恢復運行被打斷的進程。

Wait QueueLinux 內核實現進程喚醒的關鍵數據結構。通常一個事件體有一個 wait queue,對這個事件體感興趣的進程或者系統會提供回調函數,並將自己註冊到這個事件體的 wait queue 上。當事件發生時,會調用註冊在 wait queue 上的回調函數。常見的回調函數是,將對這個事件感興趣的進程的調度狀態置為 Ready,於是在調度系統重新分配 CPU 時間片時,將該進程重新執行,從而實現進程等待資源就緒而喚醒的過程。


有了這三個基本的概念,那麼以網路 IO 為代表的事件是如何從網卡到內核,最終通知進程做相關處理的?epoll kqueue 其實是更複雜的設計和實現,基本原理是一致的。下面以 TCP 新建連接為例子,描述這一內核的處理過程:

  1. 網卡收到 SYN,觸發內核中斷,直接打斷當前執行的進程,CPU 進行中斷處理邏輯(不展開 NAPI 軟中斷過程),最終將該 SYN 連接信息保存在相應 listen socket半連接隊列里,並向對方發送 SYN-ACK,然後恢復運行被打斷的進程。
  2. 進程執行完當前作業,調用 accept 系統調用(阻塞)繼續處理新連接。accept 發現連接隊列當前沒有新連接後,於是在 listen socket wait queue 的上註冊喚醒自身進程的回調函數,然後內核將這個進程置為 Block 狀態,並讓出 CPU 執行其他 Ready 狀態的進程。
  3. 網卡收到 ACK,繼續觸發內核中斷,內核完成標準的三次握手,將連接從半連接隊列移入連接隊列,於是 listen socket 有可讀事件,內核調用 listen socketwait queue 的喚醒回調函數,將之前阻塞的 accept 進程置為 Ready 調度狀態。
  4. 在內核下一個 CPU 調度窗口來臨時,Ready 調度狀態的 accept 進程被選中執行,發現連接隊列有新連接,於是讀取連接信息,並最終返回給用戶態進程。

從上面過程可見,內核處理硬體事件是一個同步的過程,而把事件傳遞給用戶進程是一個非同步的過程

「epoll kqueue 其實是更複雜的設計和實現,基本原理是一致的」

如上所述,epoll kqueue 的原理不超越上面內核處理事件過程,今天有點晚了,後續繼續醞釀一下,有時間繼續作答~


select/poll是通過輪詢的方法來獲得就緒的狀態,調用select/poll後就阻塞住,直到有就緒的文件描述符,或者超時,或者被中斷。返回值是就緒的文件描述符的個數,需要遍歷作為參數傳入的文件描述符的位域或數組獲得哪個文件描述符。

epoll是通過後台中斷的方式來獲得就緒的狀態,調用epoll_create創建實例,調用epoll_ctl添加或刪除監控的文件描述符,調用epoll_wait阻塞住,直到有就緒的文件描述符,通過epoll_event參數返回就緒狀態的文件描述符和事件

簡單但不嚴謹的說:

  • 當調用epoll_ctl時,epoll就向底層(poll(),或tcp_poll())註冊了callback
  • 當文件描述符就緒時,callback函數就會被調用,callback函數就會把該文件描述符加入列表並喚醒epoll_wait
  • 當調用epoll_wait時,epoll只是簡單地檢查下列表是否為空,不為空就返回,為空就掛起,等待被喚醒。

通常來說select和poll屬於I/O multiplexing,而epoll可以算作signal driven I/O


挺爛大街的 用一個fd去管理註冊回調+參數傳遞的內核通知模型,居然可以拖到2.6內核才去實現。


根本是因為硬體就是真實世界。

那裡有非同步事件——中斷。

所以大概不需要輪詢這麼麻煩和低效。

用戶太太弱了


epoll的原理就是:
你把要監控讀寫的文件交給內核(epoll_add)
設置你關心的事件(epoll_ctl),比如讀事件
然後等(epoll_wait),此時,如果沒有哪個文件有你關心的事件,則休眠,直到有事件,被喚醒
然後返回那些事件

實現並發,還需要配合非阻塞的讀寫。這樣就可以一下搜集一大把文件(套接字),然後一下讀寫一大把文件(不會因為某個文件慢而阻塞),這樣來實現並發。


嚴格來說epoll也是輪詢,只不過是對產生了io事件的流進行輪詢,而不需要像selec或poll那樣對所有的流都進行輪詢。所以才被稱為epoll,說明其本質上還是輪詢(poll)。

至於linux內核是如何知道io事件發生的,這就不是輪詢而是中斷了。中斷的作用就相當於你這正在進行某項工作或者睡覺,突然電話鈴聲響了,你中斷了正在進行的工作或者從睡眠中驚醒開始接電話,電話鈴聲就相當於中斷事件發生。中斷其實就是通知,你不必通過輪詢去查詢某事件是否發生,就如同你不必不斷地打電話給快遞公司查詢你的快遞是否送達?而是由快遞員在你的快遞送達時按你家的門鈴或者打電話通知你。所以,linux內核不是通過輪詢而是通過中斷知道io事件發生並通知epoll進行處理,但如果有多個io事件同時發生,epoll還是要通過輪詢來處理的。


Epoll的優勢在於,由接收數據的OS來負責通知你有數據可以操作,因為OS是知道什麼時候有數據的。
仍然用藍形參的快遞例子來說,EPoll的優勢就是,你可以隨便做其他的事情,當有快遞來的時候,他給你打電話讓你來拿,你空了的時候下來拿就好了。
不像阻塞那樣需要一直在窗邊看著快遞來沒來,也不需要像select那樣不停地打電話問快遞來沒來。尤其是在快遞比較多的時候,select需要問快遞你沒有你的快遞,快遞說有的時候,你還需要逐個問某一個包裹到沒到;EPoll會直接告訴你,你的哪個包裹的快遞到了。


linux下的NIC driver都是在有數據時,通過介面通知os。os為節省資源,關中斷後,從driver buffer狂讀數據。然後,把處理給協議棧處理,拿tcp為例,當在某些狀態點時,會去wakeup相應的sock-&>socket_wq。wakeup的操作,就是執行掛在這個wq上的eppoll_entry的callback。這個callback就是把自己對應的epoll對象epitem掛到epollevent的readylist上。

此時,如果你調用了epoll_wait(),那就直接從epollevent的readylist上,把event複製給你就完了。

簡單的說,就是一個生產者,消費者的方式。底層生成數據,放到readylist,用戶用epoll_wait去取。


很多時候對一個東西的不理解是因為不知道別人當時為什麼這麼做。
我們從伺服器的角度出發,需要做這幾件事情:
1.請求什麼時候過來
2.請求中的數據什麼時候過來
知道了要做什麼,接下來的問題就是用什麼方法去做
方法1.用一個線程監視請求有沒有過來,再用一個線程去處理過來的過來請求中的數據。(一請求已線程模型)
方法2.用一個線程監視請求是否過來,請求中的數據是否過來所有這些事件,再用一個線程去處理對應的事件。(select模型)
至於其他的poll、epoll都是對select的優化,基本想法是一致的。
更上一層的Java Nio、netty都是對select這類模型的封裝。


epoll用紅黑樹存儲,用list存儲就緒事件(與select和poll不同,select是bitmap,poll是數組存儲),之所以是e-poll,是因為它是event事件驅動,也就是說就通知這方面而言是非同步的,但是unp中仍然將epoll定義為同步的多路復用io模型,因此在很多地方epoll又有人稱為偽非同步。


Still another key issue is synchronous (blocking) versus asynchronous (interrupt-driven)
transfers. Most physical I/O is asynchronous the CPU starts the transfer and goes off to do something else until the interrupt arrives. User programs are much easier to write if the I/O operations are blocking after a receive system call the program is automatically suspended until the data are available in the buffer. It is up to the operating system to make operations that are actually interrupt-driven look blocking to the user programs.

摘選自 Operating Systems Design and Implementation。

「絕大部分硬體I/O是非同步的,CPU發出開始傳送的指令然後就去做其他事情了,直到傳輸完成的中斷髮生」

這一段話解釋了一個問題。同步I/O是操作系統為了給程序員一個更符合人類直覺的編程範式,在系統底層來看,絕大多數I/O是非同步的。

現代操作系統是靠中斷來驅動的,不是靠忙輪詢。epoll在操作系統的底層實現還是依靠中斷來實現的,並不是輪詢。


FreeBSD Kqueue的實現原理


《TLPI》下冊,第63章,1088頁,「其他備選的I/O模型」


又不是理論意義上的並行


epoll本質不是Windows下爛大街的非同步通訊模式么?


推薦閱讀:

作為一個進大學才學編程的學生,如何能以後達到溫趙輪三位大神的水平?
從零基礎開始想發一篇深度學習的論文要提前準備什麼?寫論文的周期大概多久?
零基礎學習 Hadoop 該如何下手?
自學編程和計算機科班出身的差別在哪?
關於 Python 的經典入門書籍有哪些?

TAG:編程 | X 的原理 | 並發峰值 | epoll | kqueue | 多線程 |