遊戲程式設計乾貨! 如果這篇文章說不清epoll的本質,那就過來掐死我吧!
目錄
一、從網路卡接收資料說起
二、如何知道接收了資料?
三、程式阻塞為什麼不佔用cpu資源?
四、核心接收網路資料全過程
五、同時監視多個socket的簡單方法
六、epoll的設計思路
七、epoll的原理和流程
八、epoll的實現細節
九、結論
從事服務端開發,少不了要接觸網路程式設計。epoll作為linux下高效能網路伺服器的必備技術至關重要,nginx、redis、skynet和大部分遊戲伺服器都使用到這一多路複用技術。
因為epoll的重要性,不少遊戲公司在招聘服務端同學時,會問及epoll相關的問題。比如epoll和select的區別是什麼?epoll高效率的原因是什麼?如果只靠背誦,顯然不能算上深刻的理解。
網上雖然也有不少講解epoll的文章,但要不是過於淺顯,就是陷入原始碼解析,很少能有通俗易懂的。於是決定編寫此文,讓缺乏專業背景知識的讀者也能夠明白epoll的原理。文章核心思想是:
要讓讀者清晰明白EPOLL為什麼效能好。
本文會從網路卡接收資料的流程講起,串聯起CPU中斷、作業系統程式排程等知識;再一步步分析阻塞接收資料、select到epoll的進化過程;最後探究epoll的實現細節。
一、從網路卡接收資料說起
下圖是一個典型的計算機結構圖,計算機由CPU、儲存器(記憶體)、網路介面等部件組成。瞭解epoll本質的第一步,要從硬體的角度看計算機怎樣接收網路資料。
計算機結構圖(圖片來源:linux核心完全註釋之微型計算機組成結構)
下圖展示了網路卡接收資料的過程。在①階段,網路卡收到網線傳來的資料;經過②階段的硬體電路的傳輸;最終將資料寫入到記憶體中的某個地址上(③階段)。這個過程涉及到DMA傳輸、IO通路選擇等硬體有關的知識,但我們只需知道:網路卡會把接收到的資料寫入記憶體。
網路卡接收資料的過程
通過硬體傳輸,網路卡接收的資料存放到記憶體中。作業系統就可以去讀取它們。
二、如何知道接收了資料?
瞭解epoll本質的第二步,要從CPU的角度來看資料接收。要理解這個問題,要先了解一個概念——中斷。
計算機執行程式時,會有優先順序的需求。比如,當計算機收到斷電訊號時(電容可以儲存少許電量,供CPU執行很短的一小段時間),它應立即去儲存資料,儲存資料的程式具有較高的優先順序。
一般而言,由硬體產生的訊號需要cpu立馬做出回應(不然資料可能就丟失),所以它的優先順序很高。cpu理應中斷掉正在執行的程式,去做出響應;當cpu完成對硬體的響應後,再重新執行使用者程式。中斷的過程如下圖,和函式呼叫差不多。只不過函式呼叫是事先定好位置,而中斷的位置由“訊號”決定。
中斷程式呼叫
以鍵盤為例,當使用者按下鍵盤某個按鍵時,鍵盤會給cpu的中斷引腳發出一個高電平。cpu能夠捕獲這個訊號,然後執行鍵盤中斷程式。下圖展示了各種硬體通過中斷與cpu互動。
cpu中斷(圖片來源:net.pku.edu.cn)
現在可以回答本節提出的問題了:當網路卡把資料寫入到記憶體後,網路卡向cpu發出一箇中斷訊號,作業系統便能得知有新資料到來,再通過網路卡中斷程式去處理資料。
三、程式阻塞為什麼不佔用cpu資源?
瞭解epoll本質的第三步,要從作業系統程式排程的角度來看資料接收。阻塞是程式排程的關鍵一環,指的是程式在等待某事件(如接收到網路資料)發生之前的等待狀態,recv、select和epoll都是阻塞方法。瞭解“程式阻塞為什麼不佔用cpu資源?”,也就能夠了解這一步。
為簡單起見,我們從普通的recv接收開始分析,先看看下面程式碼:
- //建立socketint s = socket(AF_INET, SOCK_STREAM, 0); //繫結bind(s, ...)//監聽listen(s, ...)//接受客戶端連線int c = accept(s, ...)//接收客戶端資料recv(c, ...);//將資料列印出來printf(...)
這是一段最基礎的網路程式設計程式碼,先新建socket物件,依次呼叫bind、listen、accept,最後呼叫recv接收資料。recv是個阻塞方法,當程式執行到recv時,它會一直等待,直到接收到資料才往下執行。
插入:如果您還不太熟悉網路程式設計,歡迎閱讀我編寫的《Unity3D網路遊戲實戰(第2版)》,會有詳細的介紹。
那麼阻塞的原理是什麼?
工作佇列
作業系統為了支援多工,實現了程式排程的功能,會把程式分為“執行”和“等待”等幾種狀態。執行狀態是程式獲得cpu使用權,正在執行程式碼的狀態;等待狀態是阻塞狀態,比如上述程式執行到recv時,程式會從執行狀態變為等待狀態,接收到資料後又變回執行狀態。作業系統會分時執行各個執行狀態的程式,由於速度很快,看上去就像是同時執行多個任務。
下圖中的計算機中執行著A、B、C三個程式,其中程式A執行著上述基礎網路程式,一開始,這3個程式都被作業系統的工作佇列所引用,處於執行狀態,會分時執行。
工作佇列中有A、B和C三個程式
等待佇列
當程式A執行到建立socket的語句時,作業系統會建立一個由檔案系統管理的socket物件(如下圖)。這個socket物件包含了傳送緩衝區、接收緩衝區、等待佇列等成員。等待佇列是個非常重要的結構,它指向所有需要等待該socket事件的程式。
建立socket
當程式執行到recv時,作業系統會將程式A從工作佇列移動到該socket的等待佇列中(如下圖)。由於工作佇列只剩下了程式B和C,依據程式排程,cpu會輪流執行這兩個程式的程式,不會執行程式A的程式。所以程式A被阻塞,不會往下執行程式碼,也不會佔用cpu資源。
socket的等待佇列
ps:作業系統新增等待佇列只是新增了對這個“等待中”程式的引用,以便在接收到資料時獲取程式物件、將其喚醒,而非直接將程式管理納入自己之下。上圖為了方便說明,直接將程式掛到等待佇列之下。
喚醒程式
當socket接收到資料後,作業系統將該socket等待佇列上的程式重新放回到工作佇列,該程式變成執行狀態,繼續執行程式碼。也由於socket的接收緩衝區已經有了資料,recv可以返回接收到的資料。
四、核心接收網路資料全過程
這一步,貫穿網路卡、中斷、程式排程的知識,敘述阻塞recv下,核心接收資料全過程。
如下圖所示,程式在recv阻塞期間,計算機收到了對端傳送的資料(步驟①)。資料經由網路卡傳送到記憶體(步驟②),然後網路卡通過中斷訊號通知cpu有資料到達,cpu執行中斷程式(步驟③)。此處的中斷程式主要有兩項功能,先將網路資料寫入到對應socket的接收緩衝區裡面(步驟④),再喚醒程式A(步驟⑤),重新將程式A放入工作佇列中。
核心接收資料全過程
喚醒程式的過程如下圖所示。
喚醒程式
以上是核心接收資料全過程
這裡留有兩個思考題,大家先想一想。
其一,作業系統如何知道網路資料對應於哪個socket?
其二,如何同時監視多個socket的資料?
(——我是分割線,想好了才能往下看哦~)
公佈答案的時刻到了。
第一個問題:因為一個socket對應著一個埠號,而網路資料包中包含了ip和埠的資訊,核心可以通過埠號找到對應的socket。當然,為了提高處理速度,作業系統會維護埠號到socket的索引結構,以快速讀取。
第二個問題是多路複用的重中之重,是本文後半部分的重點!
五、同時監視多個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收到資料,然後做出處理。
- int s = socket(AF_INET, SOCK_STREAM, 0); bind(s, ...)listen(s, ...)
- int fds[] = 存放需要監聽的socket
- while(1){
- int n = select(..., fds, ...)
- for(int i=0; i < fds.count; i++){
- if(FD_ISSET(fds[i], ...)){
- //fds[i]的資料處理
- }
- }}
select的流程
select的實現思路很直接。假如程式同時監視如下圖的sock1、sock2和sock3三個socket,那麼在呼叫select之後,作業系統把程式A分別加入這三個socket的等待佇列中。
作業系統把程式A分別加入這三個socket的等待佇列中
當任何一個socket收到資料後,中斷程式將喚起程式。下圖展示了sock2接收到了資料的處理流程。
ps:recv和select的中斷回撥可以設定成不同的內容。
sock2接收到了資料,中斷程式喚起程式A
所謂喚起程式,就是將程式從所有的等待佇列中移除,加入到工作佇列裡面。如下圖所示。
將程式A從所有等待佇列中移除,再加入到工作佇列裡面
經由這些步驟,當程式A被喚醒後,它知道至少有一個socket接收了資料。程式只需遍歷一遍socket列表,就可以得到就緒的socket。
這種簡單方式行之有效,在幾乎所有作業系統都有對應的實現。
但是簡單的方法往往有缺點,主要是:
其一,每次呼叫select都需要將程式加入到所有監視socket的等待佇列,每次喚醒都需要從每個佇列中移除。這裡涉及了兩次遍歷,而且每次都要將整個fds列表傳遞給核心,有一定的開銷。正是因為遍歷操作開銷大,出於效率的考量,才會規定select的最大監視數量,預設只能監視1024個socket。
其二,程式被喚醒後,程式並不知道哪些socket收到資料,還需要遍歷一次。
那麼,有沒有減少遍歷的方法?有沒有儲存就緒socket的方法?這兩個問題便是epoll技術要解決的。
補充說明:本節只解釋了select的一種情形。當程式呼叫select時,核心會先遍歷一遍socket,如果有一個以上的socket接收緩衝區有資料,那麼select直接返回,不會阻塞。這也是為什麼select的返回值有可能大於1的原因之一。如果沒有socket有資料,程式才會阻塞。
六、epoll的設計思路
epoll是在select出現N多年後才被發明的,是select和poll的增強版本。epoll通過以下一些措施來改進效率。
措施一:功能分離
select低效的原因之一是將“維護等待佇列”和“阻塞程式”兩個步驟合二為一。如下圖所示,每次呼叫select都需要這兩步操作,然而大多數應用場景中,需要監視的socket相對固定,並不需要每次都修改。epoll將這兩個操作分開,先用epoll_ctl維護等待佇列,再呼叫epoll_wait阻塞程式。顯而易見的,效率就能得到提升。
相比select,epoll拆分了功能
為方便理解後續的內容,我們先複習下epoll的用法。如下的程式碼中,先用epoll_create建立一個epoll物件epfd,再通過epoll_ctl將需要監視的socket新增到epfd中,最後呼叫epoll_wait等待資料。
- int s = socket(AF_INET, SOCK_STREAM, 0); bind(s, ...)listen(s, ...)
- int epfd = epoll_create(...);epoll_ctl(epfd, ...); //將所有需要監聽的socket新增到epfd中
- while(1){
- int n = epoll_wait(...)
- for(接收到資料的socket){
- //處理
- }}
功能分離,使得epoll有了優化的可能。
措施二:就緒列表
select低效的另一個原因在於程式不知道哪些socket收到資料,只能一個個遍歷。如果核心維護一個“就緒列表”,引用收到資料的socket,就能避免遍歷。如下圖所示,計算機共有三個socket,收到資料的sock2和sock3被rdlist(就緒列表)所引用。當程式被喚醒後,只要獲取rdlist的內容,就能夠知道哪些socket收到資料。
就緒列表示意圖
七、epoll的原理和流程
本節會以示例和圖表來講解epoll的原理和流程。
建立epoll物件
如下圖所示,當某個程式呼叫epoll_create方法時,核心會建立一個eventpoll物件(也就是程式中epfd所代表的物件)。eventpoll物件也是檔案系統中的一員,和socket一樣,它也會有等待佇列。
核心建立eventpoll物件
建立一個代表該epoll的eventpoll物件是必須的,因為核心要維護“就緒列表”等資料,“就緒列表”可以作為eventpoll的成員。
維護監視列表
建立epoll物件後,可以用epoll_ctl新增或刪除所要監聽的socket。以新增socket為例,如下圖,如果通過epoll_ctl新增sock1、sock2和sock3的監視,核心會將eventpoll新增到這三個socket的等待佇列中。
新增所要監聽的socket
當socket收到資料後,中斷程式會操作eventpoll物件,而不是直接操作程式。
接收資料
當socket收到資料後,中斷程式會給eventpoll的“就緒列表”新增socket引用。如下圖展示的是sock2和sock3收到資料後,中斷程式讓rdlist引用這兩個socket。
給就緒列表新增引用
eventpoll物件相當於是socket和程式之間的中介,socket的資料接收並不直接影響程式,而是通過改變eventpoll的就緒列表來改變程式狀態。
當程式執行到epoll_wait時,如果rdlist已經引用了socket,那麼epoll_wait直接返回,如果rdlist為空,阻塞程式。
阻塞和喚醒程式
假設計算機中正在執行程式A和程式B,在某時刻程式A執行到了epoll_wait語句。如下圖所示,核心會將程式A放入eventpoll的等待佇列中,阻塞程式。
epoll_wait阻塞程式
當socket接收到資料,中斷程式一方面修改rdlist,另一方面喚醒eventpoll等待佇列中的程式,程式A再次進入執行狀態(如下圖)。也因為rdlist的存在,程式A可以知道哪些socket發生了變化。
epoll喚醒程式
八、epoll的實現細節
至此,相信讀者對epoll的本質已經有一定的瞭解。但我們還留有一個問題,eventpoll的資料結構是什麼樣子?
再留兩個問題,就緒佇列應該應使用什麼資料結構?eventpoll應使用什麼資料結構來管理通過epoll_ctl新增或刪除的socket?
(——我是分割線,想好了才能往下看哦~)
如下圖所示,eventpoll包含了lock、mtx、wq(等待佇列)、rdlist等成員。rdlist和rbr是我們所關心的。
epoll原理示意圖,圖片來源:《深入理解Nginx:模組開發與架構解析(第二版)》,陶輝
就緒列表的資料結構
就緒列表引用著就緒的socket,所以它應能夠快速的插入資料。
程式可能隨時呼叫epoll_ctl新增監視socket,也可能隨時刪除。當刪除時,若該socket已經存放在就緒列表中,它也應該被移除。
所以就緒列表應是一種能夠快速插入和刪除的資料結構。雙向連結串列就是這樣一種資料結構,epoll使用雙向連結串列來實現就緒佇列(對應上圖的rdllist)。
索引結構
既然epoll將“維護監視佇列”和“程式阻塞”分離,也意味著需要有個資料結構來儲存監視的socket。至少要方便的新增和移除,還要便於搜尋,以避免重複新增。紅黑樹是一種自平衡二叉查詢樹,搜尋、插入和刪除時間複雜度都是O(log(N)),效率較好。epoll使用了紅黑樹作為索引結構(對應上圖的rbr)。
ps:因為作業系統要兼顧多種功能,以及由更多需要儲存的資料,rdlist並非直接引用socket,而是通過epitem間接引用,紅黑樹的節點也是epitem物件。同樣,檔案系統也並非直接引用著socket。為方便理解,本文中省略了一些間接結構。
九、結論
epoll在select和poll(poll和select基本一樣,有少量改進)的基礎引入了eventpoll作為中間層,使用了先進的資料結構,是一種高效的多路複用技術。
再留一點作業!
下表是個很常見的表,描述了select、poll和epoll的區別。讀完本文,讀者能否解釋select和epoll的時間複雜度為什麼是O(n)和O(1)?
select、poll和epoll的區別。圖片來源《Linux高效能伺服器程式設計》
既然說到網路程式設計,筆者的《Unity3D網路遊戲實戰(第2版)》是一本專門介紹如何開發多人網路遊戲的書籍,用例項介紹開發遊戲的全過程,非常實用,全書用一個大例子貫穿,真正的“實戰”教程。我還在規劃一本遊戲服務端的書籍,c++和lua方向,希望能夠做到深入淺出、實用、有效。書中對網路程式設計有詳細的講解,為了高質量,也許會在很長一段時間後才開始寫,歡迎給我些建議(aglab#foxmail點com)。
致謝:本文力圖詳細說明epoll的原理,特別感謝 ljhsaka、AllenKong12、雄爺、棠叔 等同事審閱了文章並給予修改意見。
作者:羅培羽
相關文章
- 遊戲設計的本質(一):數值的本質遊戲設計
- 那就這樣吧
- 06:動圖的本質!快起來,給我走兩步#python遊戲程式設計#紅傘傘Python遊戲程式設計
- 【乾貨】遊戲介面設計 (一)核心設計遊戲
- 【乾貨】遊戲介面設計 (五)表現設計遊戲
- 【乾貨】遊戲介面設計 (四)體驗設計遊戲
- 【乾貨】遊戲介面設計 (二)結構設計遊戲
- 【乾貨】遊戲介面設計 (三)資訊設計遊戲
- 乾貨:遊戲中“沙漠”場景的設計手法遊戲
- 戲說領域驅動設計(四)——本質論
- 遊戲技能該如何設計?這二篇乾貨帶你瞭解遊戲
- 等保分保傻傻分不清?乾貨來啦
- 【乾貨】如何設計一個Arkane遊戲的關卡遊戲
- QPS這麼高,那就來寫個多級快取吧快取
- 透過三消看遊戲本質遊戲
- Activity不用註冊?那就來Hook吧Hook
- 動作遊戲的本質及相關設計方法論遊戲
- 不知道怎麼提高程式碼質量?來看看這幾種設計模式吧!設計模式
- 從本質上來分析程式設計師為什麼要加班,原來是這個道理程式設計師
- 有個碼齡 10 年的程式設計師跟我說:“他程式設計從來不用滑鼠”,我說:程式設計師
- 阿里面試官:HashMap 熟悉吧?好的,那就來聊聊 Redis 字典吧!阿里面試HashMapRedis
- 本週搞幾件事情,說說你的計劃吧
- 一些乾貨:遊戲中常見“洞穴”場景的設計手法遊戲
- 天美策劃乾貨:怎樣做好遊戲的敘事設計?遊戲
- 90幀高質量寫實手遊的程式實現與美術設計【GDC2023乾貨】
- 【乾貨】程式設計師必逛的網站程式設計師網站
- 如果你還不瞭解Java類的載入過程,來看看這一篇吧Java
- 來說說mask吧
- 遊戲的特質:當我們說“play”的時候,究竟在說什麼?遊戲
- 區塊鏈上程式設計:DApp 開發實戰——來寫個競猜遊戲吧!區塊鏈程式設計APP遊戲
- 以後再有人說程式設計師懶,請把這篇文章給他看!程式設計師
- 遊戲設計&建築設計:未來,誰來規劃我們的虛擬空間?遊戲設計
- 乾貨:射擊遊戲中強烈的手感如何營造出來?遊戲
- 架構設計的本質架構
- 騰訊這2款新遊戲,黑屏看不清,網友卻說:玩哭了遊戲
- 說實話,玩過這款敘事解謎遊戲後,我想奶奶了......遊戲
- 【程式設計師乾貨】常用的免費API介面大全程式設計師API
- 併發程式設計概念大總結--乾貨程式設計