若是這篇文章說不清epoll的本質,那就過來掐死我吧!

轉載自:https://www.toutiao.com/i6683264188661367309/linux

目錄nginx

1、從網卡接收數聽說起redis

2、如何知道接收了數據?編程

3、進程阻塞爲何不佔用cpu資源?數組

4、內核接收網絡數據全過程服務器

5、同時監視多個socket的簡單方法網絡

6、epoll的設計思路數據結構

7、epoll的原理和流程架構

8、epoll的實現細節socket

9、結論

從事服務端開發,少不了要接觸網絡編程。epoll做爲linux下高性能網絡服務器的必備技術相當重要,nginx、redis、skynet和大部分遊戲服務器都使用到這一多路複用技術。

由於epoll的重要性,很多遊戲公司在招聘服務端同窗時,會問及epoll相關的問題。好比epoll和select的區別是什麼?epoll高效率的緣由是什麼?若是隻靠背誦,顯然不能算上深入的理解。

網上雖然也有很多講解epoll的文章,但要不是過於淺顯,就是陷入源碼解析,不多能有通俗易懂的。因而決定編寫此文,讓缺少專業背景知識的讀者也可以明白epoll的原理。文章核心思想是:

要讓讀者清晰明白EPOLL爲何性能好。

本文會從網卡接收數據的流程講起,串聯起CPU中斷、操做系統進程調度等知識;再一步步分析阻塞接收數據、select到epoll的進化過程;最後探究epoll的實現細節。

1、從網卡接收數聽說起

下圖是一個典型的計算機結構圖,計算機由CPU、存儲器(內存)、網絡接口等部件組成。瞭解epoll本質的第一步,要從硬件的角度看計算機怎樣接收網絡數據。

 

若是這篇文章說不清epoll的本質,那就過來掐死我吧!

計算機結構圖(圖片來源:linux內核徹底註釋之微型計算機組成結構)

 

下圖展現了網卡接收數據的過程。在①階段,網卡收到網線傳來的數據;通過②階段的硬件電路的傳輸;最終將數據寫入到內存中的某個地址上(③階段)。這個過程涉及到DMA傳輸、IO通路選擇等硬件有關的知識,但咱們只需知道:網卡會把接收到的數據寫入內存。

 

若是這篇文章說不清epoll的本質,那就過來掐死我吧!

網卡接收數據的過程

 

經過硬件傳輸,網卡接收的數據存放到內存中。操做系統就能夠去讀取它們。

2、如何知道接收了數據?

瞭解epoll本質的第二步,要從CPU的角度來看數據接收。要理解這個問題,要先了解一個概念——中斷。

計算機執行程序時,會有優先級的需求。好比,當計算機收到斷電信號時(電容能夠保存少量電量,供CPU運行很短的一小段時間),它應當即去保存數據,保存數據的程序具備較高的優先級。

通常而言,由硬件產生的信號須要cpu立馬作出迴應(否則數據可能就丟失),因此它的優先級很高。cpu理應中斷掉正在執行的程序,去作出響應;當cpu完成對硬件的響應後,再從新執行用戶程序。中斷的過程以下圖,和函數調用差很少。只不過函數調用是事先定好位置,而中斷的位置由「信號」決定。

 

若是這篇文章說不清epoll的本質,那就過來掐死我吧!

中斷程序調用

 

以鍵盤爲例,當用戶按下鍵盤某個按鍵時,鍵盤會給cpu的中斷引腳發出一個高電平。cpu可以捕獲這個信號,而後執行鍵盤中斷程序。下圖展現了各類硬件經過中斷與cpu交互。

 

若是這篇文章說不清epoll的本質,那就過來掐死我吧!

cpu中斷(圖片來源:net.pku.edu.cn)

 

如今能夠回答本節提出的問題了:當網卡把數據寫入到內存後,網卡向cpu發出一箇中斷信號,操做系統便能得知有新數據到來,再經過網卡中斷程序去處理數據。

3、進程阻塞爲何不佔用cpu資源?

瞭解epoll本質的第三步,要從操做系統進程調度的角度來看數據接收。阻塞是進程調度的關鍵一環,指的是進程在等待某事件(如接收到網絡數據)發生以前的等待狀態,recv、select和epoll都是阻塞方法。瞭解「進程阻塞爲何不佔用cpu資源?」,也就可以瞭解這一步。

爲簡單起見,咱們從普通的recv接收開始分析,先看看下面代碼:

 
  1. //建立

  2. socketint s = socket(AF_INET, SOCK_STREAM, 0);

  3. //綁定

  4. bind(s, ...)

  5. //監聽

  6. listen(s, ...)

  7. //接受客戶端鏈接

  8. int c = accept(s, ...)

  9. //接收客戶端數據

  10. recv(c, ...);

  11. //將數據打印出來

  12. printf(...)

這是一段最基礎的網絡編程代碼,先新建socket對象,依次調用bind、listen、accept,最後調用recv接收數據。recv是個阻塞方法,當程序運行到recv時,它會一直等待,直到接收到數據才往下執行。

插入:若是您還不太熟悉網絡編程,歡迎閱讀我編寫的《Unity3D網絡遊戲實戰(第2版)》,會有詳細的介紹。

那麼阻塞的原理是什麼?

工做隊列

操做系統爲了支持多任務,實現了進程調度的功能,會把進程分爲「運行」和「等待」等幾種狀態。運行狀態是進程得到cpu使用權,正在執行代碼的狀態;等待狀態是阻塞狀態,好比上述程序運行到recv時,程序會從運行狀態變爲等待狀態,接收到數據後又變回運行狀態。操做系統會分時執行各個運行狀態的進程,因爲速度很快,看上去就像是同時執行多個任務。

下圖中的計算機中運行着A、B、C三個進程,其中進程A執行着上述基礎網絡程序,一開始,這3個進程都被操做系統的工做隊列所引用,處於運行狀態,會分時執行。

 

若是這篇文章說不清epoll的本質,那就過來掐死我吧!

工做隊列中有A、B和C三個進程

 

等待隊列

當進程A執行到建立socket的語句時,操做系統會建立一個由文件系統管理的socket對象(以下圖)。這個socket對象包含了發送緩衝區、接收緩衝區、等待隊列等成員。等待隊列是個很是重要的結構,它指向全部須要等待該socket事件的進程。

 

若是這篇文章說不清epoll的本質,那就過來掐死我吧!

建立socket

 

當程序執行到recv時,操做系統會將進程A從工做隊列移動到該socket的等待隊列中(以下圖)。因爲工做隊列只剩下了進程B和C,依據進程調度,cpu會輪流執行這兩個進程的程序,不會執行進程A的程序。因此進程A被阻塞,不會往下執行代碼,也不會佔用cpu資源。

 

若是這篇文章說不清epoll的本質,那就過來掐死我吧!

socket的等待隊列

 

ps:操做系統添加等待隊列只是添加了對這個「等待中」進程的引用,以便在接收到數據時獲取進程對象、將其喚醒,而非直接將進程管理歸入本身之下。上圖爲了方便說明,直接將進程掛到等待隊列之下。

喚醒進程

當socket接收到數據後,操做系統將該socket等待隊列上的進程從新放回到工做隊列,該進程變成運行狀態,繼續執行代碼。也因爲socket的接收緩衝區已經有了數據,recv能夠返回接收到的數據。

4、內核接收網絡數據全過程

這一步,貫穿網卡、中斷、進程調度的知識,敘述阻塞recv下,內核接收數據全過程。

以下圖所示,進程在recv阻塞期間,計算機收到了對端傳送的數據(步驟①)。數據經由網卡傳送到內存(步驟②),而後網卡經過中斷信號通知cpu有數據到達,cpu執行中斷程序(步驟③)。此處的中斷程序主要有兩項功能,先將網絡數據寫入到對應socket的接收緩衝區裏面(步驟④),再喚醒進程A(步驟⑤),從新將進程A放入工做隊列中。

 

若是這篇文章說不清epoll的本質,那就過來掐死我吧!

內核接收數據全過程

 

喚醒進程的過程以下圖所示。

 

若是這篇文章說不清epoll的本質,那就過來掐死我吧!

喚醒進程

 

以上是內核接收數據全過程

這裏留有兩個思考題,你們先想想。

其一,操做系統如何知道網絡數據對應於哪一個socket?

其二,如何同時監視多個socket的數據?

(——我是分割線,想好了才能往下看哦~)

公佈答案的時刻到了。

第一個問題:由於一個socket對應着一個端口號,而網絡數據包中包含了ip和端口的信息,內核能夠經過端口號找到對應的socket。固然,爲了提升處理速度,操做系統會維護端口號到socket的索引結構,以快速讀取。

第二個問題是多路複用的重中之重,是本文後半部分的重點!

5、同時監視多個socket的簡單方法

服務端須要管理多個客戶端鏈接,而recv只能監視單個socket,這種矛盾下,人們開始尋找監視多個socket的方法。epoll的要義是高效的監視多個socket。從歷史發展角度看,必然先出現一種不過高效的方法,人們再加以改進。只有先理解了不過高效的方法,纔可以理解epoll的本質。

假如可以預先傳入一個socket列表,若是列表中的socket都沒有數據,掛起進程,直到有一個socket收到數據,喚醒進程。這種方法很直接,也是select的設計思想。

爲方便理解,咱們先複習select的用法。在以下的代碼中,先準備一個數組(下面代碼中的fds),讓fds存放着全部須要監視的socket。而後調用select,若是fds中的全部socket都沒有數據,select會阻塞,直到有一個socket接收到數據,select返回,喚醒進程。用戶能夠遍歷fds,經過FD_ISSET判斷具體哪一個socket收到數據,而後作出處理。

 
  1. int s = socket(AF_INET, SOCK_STREAM, 0); bind(s, ...)listen(s, ...)

  2. int fds[] = 存放須要監聽的socket

  3. while(1){

  4. int n = select(..., fds, ...)

  5. for(int i=0; i < fds.count; i++){

  6. if(FD_ISSET(fds[i], ...)){

  7. //fds[i]的數據處理

  8. }

  9. }}

  10.  

select的流程

select的實現思路很直接。假如程序同時監視以下圖的sock一、sock2和sock3三個socket,那麼在調用select以後,操做系統把進程A分別加入這三個socket的等待隊列中。

 

若是這篇文章說不清epoll的本質,那就過來掐死我吧!

操做系統把進程A分別加入這三個socket的等待隊列中

 

當任何一個socket收到數據後,中斷程序將喚起進程。下圖展現了sock2接收到了數據的處理流程。

ps:recv和select的中斷回調能夠設置成不一樣的內容。

若是這篇文章說不清epoll的本質,那就過來掐死我吧!

sock2接收到了數據,中斷程序喚起進程A

 

所謂喚起進程,就是將進程從全部的等待隊列中移除,加入到工做隊列裏面。以下圖所示。

 

若是這篇文章說不清epoll的本質,那就過來掐死我吧!

將進程A從全部等待隊列中移除,再加入到工做隊列裏面

 

經由這些步驟,當進程A被喚醒後,它知道至少有一個socket接收了數據。程序只需遍歷一遍socket列表,就能夠獲得就緒的socket。

這種簡單方式行之有效,在幾乎全部操做系統都有對應的實現。

可是簡單的方法每每有缺點,主要是:

其一,每次調用select都須要將進程加入到全部監視socket的等待隊列,每次喚醒都須要從每一個隊列中移除。這裏涉及了兩次遍歷,並且每次都要將整個fds列表傳遞給內核,有必定的開銷。正是由於遍歷操做開銷大,出於效率的考量,纔會規定select的最大監視數量,默認只能監視1024個socket。

其二,進程被喚醒後,程序並不知道哪些socket收到數據,還須要遍歷一次。

那麼,有沒有減小遍歷的方法?有沒有保存就緒socket的方法?這兩個問題即是epoll技術要解決的。

補充說明:本節只解釋了select的一種情形。當程序調用select時,內核會先遍歷一遍socket,若是有一個以上的socket接收緩衝區有數據,那麼select直接返回,不會阻塞。這也是爲何select的返回值有可能大於1的緣由之一。若是沒有socket有數據,進程纔會阻塞。

6、epoll的設計思路

epoll是在select出現N多年後才被髮明的,是select和poll的加強版本。epoll經過如下一些措施來改進效率。

措施一:功能分離

select低效的緣由之一是將「維護等待隊列」和「阻塞進程」兩個步驟合二爲一。以下圖所示,每次調用select都須要這兩步操做,然而大多數應用場景中,須要監視的socket相對固定,並不須要每次都修改。epoll將這兩個操做分開,先用epoll_ctl維護等待隊列,再調用epoll_wait阻塞進程。顯而易見的,效率就能獲得提高。

 

若是這篇文章說不清epoll的本質,那就過來掐死我吧!

相比select,epoll拆分了功能

 

爲方便理解後續的內容,咱們先複習下epoll的用法。以下的代碼中,先用epoll_create建立一個epoll對象epfd,再經過epoll_ctl將須要監視的socket添加到epfd中,最後調用epoll_wait等待數據。

 
  1. int s = socket(AF_INET, SOCK_STREAM, 0); bind(s, ...)listen(s, ...)

  2. int epfd = epoll_create(...);epoll_ctl(epfd, ...); //將全部須要監聽的socket添加到epfd中

  3. while(1){

  4. int n = epoll_wait(...)

  5. for(接收到數據的socket){

  6. //處理

  7. }}

功能分離,使得epoll有了優化的可能。

措施二:就緒列表

select低效的另外一個緣由在於程序不知道哪些socket收到數據,只能一個個遍歷。若是內核維護一個「就緒列表」,引用收到數據的socket,就能避免遍歷。以下圖所示,計算機共有三個socket,收到數據的sock2和sock3被rdlist(就緒列表)所引用。當進程被喚醒後,只要獲取rdlist的內容,就可以知道哪些socket收到數據。

 

若是這篇文章說不清epoll的本質,那就過來掐死我吧!

就緒列表示意圖

 

7、epoll的原理和流程

本節會以示例和圖表來說解epoll的原理和流程。

建立epoll對象

以下圖所示,當某個進程調用epoll_create方法時,內核會建立一個eventpoll對象(也就是程序中epfd所表明的對象)。eventpoll對象也是文件系統中的一員,和socket同樣,它也會有等待隊列。

 

若是這篇文章說不清epoll的本質,那就過來掐死我吧!

內核建立eventpoll對象

 

建立一個表明該epoll的eventpoll對象是必須的,由於內核要維護「就緒列表」等數據,「就緒列表」能夠做爲eventpoll的成員。

維護監視列表

建立epoll對象後,能夠用epoll_ctl添加或刪除所要監聽的socket。以添加socket爲例,以下圖,若是經過epoll_ctl添加sock一、sock2和sock3的監視,內核會將eventpoll添加到這三個socket的等待隊列中。

 

若是這篇文章說不清epoll的本質,那就過來掐死我吧!

添加所要監聽的socket

 

當socket收到數據後,中斷程序會操做eventpoll對象,而不是直接操做進程。

接收數據

當socket收到數據後,中斷程序會給eventpoll的「就緒列表」添加socket引用。以下圖展現的是sock2和sock3收到數據後,中斷程序讓rdlist引用這兩個socket。

 

若是這篇文章說不清epoll的本質,那就過來掐死我吧!

給就緒列表添加引用

 

eventpoll對象至關因而socket和進程之間的中介,socket的數據接收並不直接影響進程,而是經過改變eventpoll的就緒列表來改變進程狀態。

當程序執行到epoll_wait時,若是rdlist已經引用了socket,那麼epoll_wait直接返回,若是rdlist爲空,阻塞進程。

阻塞和喚醒進程

假設計算機中正在運行進程A和進程B,在某時刻進程A運行到了epoll_wait語句。以下圖所示,內核會將進程A放入eventpoll的等待隊列中,阻塞進程。

 

若是這篇文章說不清epoll的本質,那就過來掐死我吧!

epoll_wait阻塞進程

 

當socket接收到數據,中斷程序一方面修改rdlist,另外一方面喚醒eventpoll等待隊列中的進程,進程A再次進入運行狀態(以下圖)。也由於rdlist的存在,進程A能夠知道哪些socket發生了變化。

若是這篇文章說不清epoll的本質,那就過來掐死我吧!

epoll喚醒進程

 

8、epoll的實現細節

至此,相信讀者對epoll的本質已經有必定的瞭解。但咱們還留有一個問題,eventpoll的數據結構是什麼樣子?

再留兩個問題,就緒隊列應該應使用什麼數據結構?eventpoll應使用什麼數據結構來管理經過epoll_ctl添加或刪除的socket?

 

(——我是分割線,想好了才能往下看哦~)

以下圖所示,eventpoll包含了lock、mtx、wq(等待隊列)、rdlist等成員。rdlist和rbr是咱們所關心的。

 

若是這篇文章說不清epoll的本質,那就過來掐死我吧!

epoll原理示意圖,圖片來源:《深刻理解Nginx:模塊開發與架構解析(第二版)》,陶輝

 

就緒列表的數據結構

就緒列表引用着就緒的socket,因此它應可以快速的插入數據。

程序可能隨時調用epoll_ctl添加監視socket,也可能隨時刪除。當刪除時,若該socket已經存放在就緒列表中,它也應該被移除。

因此就緒列表應是一種可以快速插入和刪除的數據結構。雙向鏈表就是這樣一種數據結構,epoll使用雙向鏈表來實現就緒隊列(對應上圖的rdllist)。

索引結構

既然epoll將「維護監視隊列」和「進程阻塞」分離,也意味着須要有個數據結構來保存監視的socket。至少要方便的添加和移除,還要便於搜索,以免重複添加。紅黑樹是一種自平衡二叉查找樹,搜索、插入和刪除時間複雜度都是O(log(N)),效率較好。epoll使用了紅黑樹做爲索引結構(對應上圖的rbr)。

 

ps:由於操做系統要兼顧多種功能,以及由更多須要保存的數據,rdlist並不是直接引用socket,而是經過epitem間接引用,紅黑樹的節點也是epitem對象。一樣,文件系統也並不是直接引用着socket。爲方便理解,本文中省略了一些間接結構。

9、結論

epoll在select和poll(poll和select基本同樣,有少許改進)的基礎引入了eventpoll做爲中間層,使用了先進的數據結構,是一種高效的多路複用技術。

再留一點做業!

下表是個很常見的表,描述了select、poll和epoll的區別。讀完本文,讀者可否解釋select和epoll的時間複雜度爲何是O(n)和O(1)?

 

若是這篇文章說不清epoll的本質,那就過來掐死我吧!

select、poll和epoll的區別。圖片來源《Linux高性能服務器編程》

相關文章
相關標籤/搜索