和前面文章的第一部分一樣,這些文字是為了幫别人或者自己理清思路的,而不是所謂的源碼分析,想分析源碼的,還是直接debug源碼最好,看任何文檔以及書都是下策。是以這類幫人理清思路的文章盡可能的記成流水的方式,盡可能的簡單明了。
Linux 核心通過睡眠隊列來組織所有等待某個事件的task,而wakeup機制則可以異步喚醒整個睡眠隊列上的task,每一個睡眠隊列上的節點都擁有一個 callback,wakeup邏輯在喚醒睡眠隊列時,會周遊該隊列連結清單上的每一個節點,調用每一個節點的callback,如果周遊過程中遇到某個節點 是排他節點,則終止周遊,不再繼續周遊後面的節點。總體上的邏輯可以用下面的僞代碼表示:
我們隻需要狠狠地關注這個callback機制,它能做的事真的不止select/poll/epoll,Linux的AIO也是它來做的,注冊了callback,你幾乎可以讓一個阻塞路徑在被喚醒的時候做任何事情。一般而言,一個callback裡面都是以下的邏輯:
其中,do_something_private是wait_entry自己的自定義邏輯,而wakeup_common則是公共邏輯,旨在将該wait_entry的task加入到CPU的就緒task隊列,然後讓CPU去排程它。
現在留個思考,如果實作select/poll,應該在wait_entry的callback上做什麼文章呢?
.....
要 知道,在大多數情況下,要高效處理網絡資料,一個task一般會批量處理多個socket,哪個來了資料就去讀那個,這就意味着要公平對待所有這些 socket,你不可能阻塞在任何socket的“資料讀”上,也就是說你不能在阻塞模式下針對任何socket調用recv/recvfrom,這就是 多路複用socket的實質性需求。
假設有N個socket被同一個task處理,怎麼完成多路複用邏輯呢?很顯然,我們要等待“資料可讀”這個事件,而不是去等待“實際的資料”!!我們要 阻塞在事件上,該事件就是“N個socket中有一個或多個socket上有資料可讀”,也就是說,隻要這個阻塞解除,就意味着一定有資料可讀,意味着接 下來調用recv/recvform一定不會阻塞!另一方面,這個task要同時排入所有這些socket的sleep_list上,期待任意一個 socket隻要有資料可讀,都可以喚醒該task。
那麼,select/poll這類多路複用模型的設計就顯而易見了。
select/poll的設計非常簡單,為每一個socket引入一個poll例程,該曆程對于“資料可讀”的判斷如下:
當task調用select/poll的時候,如果沒有資料可讀,task會阻塞,此時它已經排入了所有N個socket的sleep_list,隻要有一個socket來了資料,這個task就會被喚醒,接下來的事情就是
可見,隻要有一個socket有資料可讀,整個N個socket就會被周遊一遍調用一遍poll函數,看看有沒有資料可讀,事實上, 當阻塞在select/poll的task被喚醒的時候,它根本不知道具體socket有資料可讀,它隻知道這些socket中至少有一個socket有 資料可讀,是以它需要周遊一遍,以示求證,周遊完成後,使用者态task可以根據傳回的結果集來對有事件發生的socket進行讀操作。
可見,select/poll非常原始,如果有100000個socket(誇張嗎?),有一個socket可讀,那麼系統不得不周遊一遍...是以 select隻限制了最多可以複用1024個socket,并且在Linux上這是宏控制的。select/poll隻是樸素地實作了socket的多路 複用,根本不适合大容量網絡伺服器的處理場景。其瓶頸在于,不能随着socket的增多而戰時擴充性。
既然一個wait_entry的callback可以做任意事,那麼能否讓其做的比select/poll場景下的wakeup_common更多呢?
為此,epoll準備了一個連結清單,叫做ready_list,所有處于ready_list中的socket,都是有事件的,對于資料讀而言,都是确實有 資料可讀的。epoll的wait_entry的callback要做的就是,将自己自行加入到這個ready_list中去,等待epoll_wait 傳回的時候,隻需要周遊ready_list即可。epoll_wait睡眠在一個單獨的隊列(single_epoll_waitlist)上,而不是 socket的睡眠隊列上。
和select/poll不同的是,使用epoll的task不需要同時排入所有多路複用socket的睡眠隊列,這些socket都擁有自己的隊 列,task隻需要睡眠在自己的單獨隊列中等待事件即可,每一個socket的wait_entry的callback邏輯為:
為此,epoll需要一個額外的調用,那就是epoll_ctrl ADD,将一個socket加入到epoll table中,它主要提供一個wakeup callback,将這個socket指定給一個epoll entry,同時會初始化該wait_entry的callback為epoll_wakecallback。整個epoll_wait以及協定棧的 wakeup邏輯如下所示:
協定棧喚醒socket的睡眠隊列
1.資料包排入了socket的接收隊列;;
2.喚醒socket的睡眠隊列,即調用各個wait_entry的callback;
3.callback将自己這個socket加入ready_list;
4.喚醒epoll_wait睡眠在的單獨隊列。
自 此,epoll_wait繼續前行,周遊調用ready_list裡面每一個socket的poll曆程,搜集事件。這個過程是例行的,因為這是必不可少 的,ready_list裡面每一個socket都有資料可讀,做不了無用功,這是和select/poll的本質差別(select/poll中,即便 沒有資料可讀,也要全部周遊一遍)。
總結一下,epoll邏輯要做以下的例程:
綜合以上,可以給出下面的關于epoll的流程圖,可以對比本文第一部分的流程圖做比較
<a href="http://s1.51cto.com/wyfs02/M02/79/BC/wKioL1aZ81-CEqlcAAL_CrGFxPo563.jpg" target="_blank"></a>
可 以看出,epoll和select/poll的本質差別就是,在發生事件的時候,每一個epoll item(也就是socket)都擁有自己單獨的一個wakeup callback,而對于select/poll而言,隻有一個!這就意味着epoll中,一個socket發生事件,可以調用其獨立的callback 來處理它自身。從宏觀上看,epoll的高效在于分離出了兩類睡眠等待,一個是epoll本身的睡眠等待,它等待的是“任意一個socket發生事 件”,即epoll_wait調用傳回的條件,它并不适合直接睡眠在socket的睡眠隊列上,如果真要這樣,到底睡誰呢?畢竟那麼多socket... 是以它隻睡自己。一個socket的睡眠隊列一定要僅僅和它自己相關,是以另一類睡眠等待是每一個socket自身的,它睡眠在自己的隊列上即可。
是時候提到ET和LT了,最大的争議在于哪個性能高,而不是到底怎麼用。各種文檔上都說ET高效,但事實上,根本不是這樣,對于實際而言,LT高效的同時,更安全。兩者到底什麼差別呢?
ET:隻有狀态發生變化的時候,才會通知,比如資料緩沖去從無到有的時候(不可讀-可讀),如果緩沖區裡面有資料,便不會一直通知;
LT:隻要緩沖區裡面有資料,就會一直通知。
查 了很多資料,得到的答案無非就是類似上述的,然而如果看Linux的實作,反而讓人對ET更加迷惑。什麼叫狀态發生變化呢?比如資料接收緩沖區裡面一次性 來了10個資料包,對比上述流程圖,很顯然會調用10次的wakeup操作,是不是意味着這個socket要被加入ready_list 10次呢?肯定不是這樣的,第二個資料包到來調用wakeup callback時,發現該socket已經在ready_list了,肯定不會再加了,此時epoll_wait傳回,使用者讀取了1個資料包之後,假設 程式有bug,便不再讀取了,此時緩沖區裡面還有9個資料包,問題來了,此時如果協定棧再排入一個包,到底是通知還是不通知呢??按照概念了解,不會通知 了,因為這不是“狀态的變化”,但是事實上在Linux上你試一下的話,發現是會通知的,因為隻要有包排入socket隊列,就會觸發wakeup callback,就會将socket放入ready_list中,對于ET而言,在epoll_wait傳回前,socket就已經從 ready_list中摘除了。是以,如果在ET模式下,你發現程式阻塞在epoll_wait了,并不能下結論說一定是資料包沒有收完一個原因導緻的, 也可能是資料包确實沒有收完,但如果此時來一個新的資料包,epoll_wait還是會傳回的,雖然這并沒有帶來緩沖去狀态的邊沿變化。
是以,對于緩沖區狀态的變化,不能簡單了解為有和無這麼簡單,而是資料包的到來和不到來。
ET和LT是中斷的概念,如果你把資料包的到來,即插入到socket接收隊列這件事了解成一個中斷事件,所謂的邊沿觸發不就是這個概念嗎?
在 代碼實作的邏輯上,ET和LT實作的差別在于LT一旦有事件則會一直加進ready_list,直到下一次的poll将其移出,然後在探測到感興趣事件後 再将其加進ready_list。由poll例程來判斷是否有事件,而不是完全依賴wakeup callback,這是真正意義的poll,即不斷輪詢!也就是說,LT模式是完全輪詢的,每次都會去poll一次,直到poll不到感興趣的事件,才會 歇息,此時就隻有資料包的到來可以重新依賴wakeup callback将其加入ready_list了。在實作上,從下面的代碼可以看出二者的差異。
性能的差別主要展現在資料結構的組織以及算法上,對于epoll而言,主要就是連結清單操作和 wakeup callback操作,對于ET而言,是wakeup callback将socket加入到ready_list,而對于LT而言,則除了wakeup callback可以将socket加入到ready_list之外,epoll_wait也可以将其為了下一次的poll加入到 ready_list,wakeup callback中反而有更少工作量,但這并不是性能差異的根本,性能差異的根本在于連結清單的周遊,如果有海量的socket采用LT模式,由于每次發生事 件後都會再次将其加入ready_list,那麼即便是該socket已經沒有事件了,還是會用一次poll來确認,這額外的一次對于無事件socket 沒有意義的周遊在ET上是沒有的。但是注意,周遊連結清單的性能消耗隻有在連結清單超長時才會展現,你覺得千兒八百的socket就會展現LT的劣勢嗎?誠 然,ET确實會減少資料可讀的通知次數,但這事實上并沒有帶來壓倒性的優勢。
LT确實比ET更容易使用,也不容易死鎖,還是建議用LT來正常程式設計,而不是用ET來偶爾炫技。
epoll 的ET在阻塞模式下,無法識别到隊列空事件,進而隻是阻塞在單獨一個socket的Recv而不是所有被監控socket的epoll_wait調用上, 雖然不會影響代碼的運作,隻要該socket有資料到來便好,但是會影響程式設計邏輯,這意味着解除了多路複用的武裝,造成大量socket的饑餓,即便有數 據了,也沒法讀。當然,對于LT而言,也有類似的問題,但是LT會激進地回報資料可讀,是以事件不會輕易因為你的程式設計錯誤而被丢棄。
對于LT而言,由于它會不斷回報,隻要有資料,你想什麼時候讀就可以什麼時候讀,它永遠有“下一次poll”的機會主動探知是否有資料可以繼續讀,即便使 用阻塞模式,隻要不要跨越阻塞邊界造成其他socket饑餓,讀多少資料均可以,但是對于ET而言,它在通知你的應用程式資料可讀後,雖然新的資料到來還 是會通知,但是你并不能控制新的資料一定會來以及什麼時候來,是以你必須讀完所有的資料才能離開,讀完所有的時候意味着你必須可以探知資料為空,是以也就 是說,你必須采用非阻塞模式,直到傳回EAGIN錯誤。
1.隊列緩沖區的大小包括skb結構體本身的長度,230左右
2.ET模式下,wakeup callback中将socket加入ready_list的次數 >= 收到資料包的個數,是以
多個資料報足夠快到達可能隻會觸發一次epoll wakeup callback的成功回調,此時隻會将socket添加進ready_list一次
=>造成隊列滿
=>後續的大封包加不進去
=>瓶塞效應
=>可以填補緩沖區剩餘hole的小封包可以觸發ET模式的epoll_wait傳回,如果最小長度就是1,那麼可以發送0長度的包引誘epoll_wait傳回
=>但是由于skb結構體的大小是固有大小,以上的引誘不能保證會成功。
3.epoll驚群,可以參考ngx的經驗
4.epoll也可借鑒NAPI關中斷的方案,直到Recv例程傳回EAGIN或者發生錯誤,epoll的wakeup callback不再被調用,這意味着隻要緩沖區不為空,就算來了新的資料包也不會通知了。
a.隻要socket的epoll wakeup callback被調用,禁掉後續的通知;
b.Recv例程在傳回EAGIN或者錯誤的時候,開始後續的通知。
本文轉自 dog250 51CTO部落格,原文連結:http://blog.51cto.com/dog250/1735579