Linux 底层原理 —— epoll 与多路复用
引言
epoll 是 Linux 系統下高性能網絡服務的必備技術,很多面試中高頻出現的 Nginx、Redis 都使用了這一技術,本文總結 linux 多路復用模型的演變過程,看一看epoll 是如何實現高性能的。
一、相關基礎知識
1.1 文件描述符?
文件描述符:file descriptor,是Linux 內核為了高效管理已被打開的文件所創建的索引,形式上是一個非負整數,用于指代被打開的文件,所有執行I/O操作的系統調用都通過文件描述符。
最常見的文件描述符是 0 (標準輸入)、1 (標準輸出)、2(標準錯誤),它們被維護在進程的 fd 目錄下:
1.2 多路復用
在數據通信系統或計算機網絡系統中,為了有效的利用通信線路,希望一個信道同時傳輸多路信號,這就是多路復用技術。
采用多路復用技術能把多個信號組合起來在一條物理信道上進行傳輸,
二、多路復用模型演化過程
早期的 Socket 通信是 BIO,即阻塞 IO。
應用進程通過系統內核的 read()系統調用來訪問 Socket fd,由于是阻塞的,應用程序的線程是阻塞的,必須收到 fd 返回的數據響應后,才能夠繼續向下執行。那么,對于一個網絡程序,多個TCP請求過來時,應用進程必須分配對等數量的線程才能夠完成處理,因此,這會造成極大的資源浪費,例如CPU的效率,線程變多,忙于線程調度,而且線程的內存占用也會增多。
為了解決一系列資源浪費和效率問題,內核改進了IO方式,變為了NIO。
NIO是非阻塞式IO通信,即便沒有數據到達,系統調用也會立刻返回,告知應用進程沒有數據。此時,應用程序就可以使用一個線程處理多個 socket fd,但這需要應用程序輪詢訪問 fd 集合,每次都需要調用 read 并反復切換用戶態、內核態。這個時期的內核通過 NIO 的方式,用戶程序使用一個線程來操作,是同步處理的,因此可以稱為“同步非阻塞模型”。
隨著內核的發展,1983年加入了新的系統調用——select,它也是“多路復用”技術實現的第一個版本。使用?man 2 select 命令查看相關信息:
當應用程序需要監視多個 fd 時,會把一個 fd 集合傳給 select 接口(最多1024個fd),這需要將fd集合整個由用戶態拷貝到內核態,這個開銷在 fd 很多時會很大。
select 實際上是將用戶輪詢的操作移入了內核空間,內核依然需要循環每個 fd 進行判斷是否有數據到達。而 poll 的原理和 select 一樣,不過它支持的fd 數量要大于 1024。
于是,在2002年,epoll誕生,它是基于 select、poll 之上進行改進的線程安全的多路復用技術。
epoll內部使用了 mmap 共享了用戶和內核的部分空間,避免數據的來回拷貝。
基于事件驅動,epoll_ctl 注冊事件和callback回調函數,epoll_wait 只返回發生的事件(一般是讀或寫事件),避免像 select 和 poll 對事件的整體輪詢。
三、epoll 原理的深入理解
在一個典型的計算機結構圖中,網卡收到網線傳來的網絡數據,經過硬件電路的傳輸,最終將數據寫入到內存中的某個地址上。
通過硬件傳輸,網卡接收的數據存放到內存中,操作系統就可以去讀取它們。但是,忙碌的CPU并不會一直盯著網卡,這需要網卡在接收到數據之后,向CPU發送一個中斷信號(80中斷),告訴CPU有網絡數據到達。
計算機執行程序時,會有優先級的需求,比如,當計算機收到斷電信號時(電容可以保存少許電量,供cpu運行很短的一小段時間)它應立即去保存數據,保存數據的程序具有較高優先級。
一般而言,由硬件產生的信號需要CPU立馬作出回應(不然數據可能會丟失),所以它的優先級很高。網卡向CPU發出一個中斷信號,操作系統便能得知有新數據到來,再通過網卡中斷程序去處理數據。
理解 epoll還需要理解另一個非常重要的概念——進程阻塞。它的含義很簡單,就是進程停止執行,等待某個喚醒信號使其恢復運行狀態。但是阻塞的進程并不會占用CPU資源,這是為什么?
這就需要理解阻塞的本質——進程調度。
操作系統為了支持多任務,實現了進程調度的功能,會把進程分為“運行”和“等待”等幾種狀態。操作系統會分時執行各個運行狀態的進程,由于速度很快,看上去就像是同時執行多個任務。
下圖中計算機運行A、B、C三個進程,其中進程A執行著上述基礎網絡程序,一開始,這三個進程被操作系統的工作隊列所引用,處于運行狀態,會分時執行。
當進程A執行到創建socket語句時,操作系統會創建一個由文件系統管理的 socket 對象。這個socket 對象包含了發送緩沖區、接收緩沖區、等待隊列等成員。而等待隊列指向所有需要等待該socket事件的進程。
當程序執行到recv時,操作系統會將進程A從工作隊列移入該 socket 的等待隊列中,CPU就只會執行另外兩個B、C進程,而不會執行A進程。
所以,進程A被阻塞,不會往下執行代碼,也不會占用CPU資源。
當socket接收到數據后,操作系統將該socket等待隊列上的進程重新放回工作隊列,該進程就會再次變成運行狀態,繼續執行代碼。也由于socket的接收緩沖區已經有數據,recv可以返回接收到的數據。
那么內核接收網絡數據的全過程如下圖(其中,中斷程序主要兩項功能:先將網絡數據寫入對應socket的接收緩沖區,再喚醒進程A,即重新將A放入工作隊列):
服務端需要管理多個客戶端連接,而 recv 只能監視單個socket,這種矛盾下,人們開始尋找監視多個socket 的方法。epoll 的要義是高效的監視多個 socket 。
假如能夠預先傳入一個 socket 列表,如果列表中的 socket 都沒有數據,掛起進程(阻塞),直到有一個socket 收到數據,喚醒進程。這種方法很直接,也是 select 的設計思想。操作系統把進程A分別加入多個socket的等待隊列中,如下圖:
epoll 相對于 select ,主要有以下幾點改進措施:
措施一:功能分離
select 低效的原因之一是將“維護等待隊列” 和 “阻塞進程” 兩個步驟合二為一。每次調用 select 都需要這兩步操作,然而,大多數場景中,需要監聽的socket相對固定,并不需要每次都修改。epoll 將這兩個操作分開,先用 epoll_ctl 維護等待隊列,再調用 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){//處理} }措施二:就緒隊列
select 低效的另一個原因是程序不知道哪些 socket 收到數據,只能一個一個遍歷。加入內核維護一個“就緒列表” ,引用收到數據的 socket,就能避免遍歷:
epoll 系列總共有三個方法,貫穿數據接收和進程調度的始終:
1、epoll_create 會創建一個 eventpoll 對象,它也是文件系統中的一員,和socket 一樣,它也會有等待隊列。創建一個代表該 epoll 的 eventpoll 對象是必須的,因為內核需要維護“就緒列表” 等數據,可以作為 eventpoll 的成員。
2、epoll_ctl ,創建 epoll 對象之后,可以用 epoll_ctl 添加和刪除所要監聽的 socket。當socket 收到數據后,中斷程序會操作 eventpoll 對象,而不是直接操作進程。
當socket 收到數據后,中斷程序會給 eventpoll 的“就緒隊列” 添加 socket 引用:
eventpoll對象相當于是 socket 和進程之間的中介,socket 接收數據并不直接影響進程,而是通過改變 eventpoll 的就緒隊列來改變進程狀態。
3、epoll_wait,當程序執行到 epoll_wait 時,如果 rdlist 已經引用了socket,那么 epoll_wait 直接返回,如果 rdlist 為空,內核會將進程A放入 eventpoll 的等待隊列,阻塞進程。
當socket接收到數據,中斷程序一方面修改 rdlist,另一方面喚醒 eventpoll 等待隊列中的進程,進程A再次進入運行狀態。也因為 rdlist 的存在,進程A可以知道哪些 socket 發生了變化。
四、epoll的實現思考
了解了 epoll的工作原理,我們再來思考一下 epoll的內部存儲結構。
前面已經提到就緒列表,它引用著就緒的 socket ,所以應該具備快速插入的特性,因為程序可能隨時調用 epoll_ctl 添加監聽 socket,也可能隨時刪除。
所以,就緒列表應該是一種能夠快速插入和刪除的數據結構。雙向鏈表就是這樣一種數據結構,epoll 使用它來實現就緒隊列。
而維護監視的 socket ,采用的是紅黑樹,因為它的搜索、插入、刪除時間復雜度都是O(logN),效率較好。
總結
參考
《如果這篇文章說不清epoll的本質,那就過來掐死我吧!》
《阿里面試題 | Nginx 所使用的 epoll 模型是什么?》
總結
以上是生活随笔為你收集整理的Linux 底层原理 —— epoll 与多路复用的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 代码里无图片地址_项目实战:爬高清图片
- 下一篇: java 教室借用管理系统_[内附完整源