和前面文章的第一部分同樣,這些文字是爲了幫別人或者本身理清思路的,而不是所謂的源碼分析,想分析源碼的,仍是直接debug源碼最好,看任何文檔以及書都是下策。所以這類幫人理清思路的文章儘量的記成流水的方式,儘量的簡單明瞭。
算法
Linux 內核經過睡眠隊列來組織全部等待某個事件的task,而wakeup機制則能夠異步喚醒整個睡眠隊列上的task,每個睡眠隊列上的節點都擁有一個 callback,wakeup邏輯在喚醒睡眠隊列時,會遍歷該隊列鏈表上的每個節點,調用每個節點的callback,若是遍歷過程當中遇到某個節點 是排他節點,則終止遍歷,再也不繼續遍歷後面的節點。整體上的邏輯能夠用下面的僞代碼表示:
編程
define sleep_list; define wait_entry; wait_entry.task= current_task; wait_entry.callback = func1; if (something_not_ready); then # 進入阻塞路徑 add_entry_to_list(wait_entry, sleep_list); go on: schedule(); if (something_not_ready); then goto go_on; endif del_entry_from_list(wait_entry, sleep_list); endif ...
something_ready; for_each(sleep_list) as wait_entry; do wait_entry.callback(...); if(wait_entry.exclusion); then break; endif done
咱們只須要狠狠地關注這個callback機制,它能作的事真的不止select/poll/epoll,Linux的AIO也是它來作的,註冊了callback,你幾乎可讓一個阻塞路徑在被喚醒的時候作任何事情。通常而言,一個callback裏面都是如下的邏輯:
安全
common_callback_func(...) { do_something_private; wakeup_common; }
其中,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例程,該歷程對於「數據可讀」的判斷以下:
網絡
poll() { ... if (接收隊列不爲空) { ev |= POLL_IN; } ... }
當task調用select/poll的時候,若是沒有數據可讀,task會阻塞,此時它已經排入了全部N個socket的sleep_list,只要有一個socket來了數據,這個task就會被喚醒,接下來的事情就是
數據結構
for_each_N_socket as sk; do event.evt = sk.poll(...); event.sk = sk; put_event_to_user; done;
可見,只要有一個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邏輯爲:
socket
epoll_wakecallback(...) { add_this_socket_to_ready_list; wakeup_single_epoll_waitlist; }
爲此,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邏輯要作如下的例程:
ide
define wait_entry wait_entry.socket = this_socket; wait_entry.callback = epoll_wakecallback; add_entry_to_list(wait_entry, this_socket.sleep_list);
define single_wait_list define single_wait_entry single_wait_entry.callback = wakeup_common; single_wait_entry.task = current_task; if (ready_list_is_empty); then # 進入阻塞路徑 add_entry_to_list(single_wait_entry, single_wait_list); go on: schedule(); if (sready_list_is_empty); then goto go_on; endif del_entry_from_list(single_wait_entry, single_wait_list); endif for_each_ready_list as sk; do event.evt = sk.poll(...); event.sk = sk; put_event_to_user; done;
add_this_socket_to_ready_list; wakeup_single_wait_list;
綜合以上,能夠給出下面的關於epoll的流程圖,能夠對比本文第一部分的流程圖作比較函數
可 以看出,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_wait for_each_ready_list_item as entry; do remove_from_ready_list(entry); event = entry.poll(...); if (event) then put_user; if (LT) then # 如下一次poll的結論爲結果 add_entry_to_ready_list(entry); endif endif done
性能的區別主要體如今數據結構的組織以及算法上,對於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或者錯誤的時候,開始後續的通知。