epoll的本質(1)

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

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

一、從網卡接收數據說起
二、如何知道接收了數據?
三、進程阻塞爲什麼不佔用cpu資源?
四、內核接收網絡數據全過程
五、同時監視多個socket的簡單方法
六、epoll的設計思路
七、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接收開始分析,先看看下面代碼:

//創建socket
int 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可以返回接收到的數據。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章