上篇回顧
四、內核接收網絡數據全過程
五、同時監視多個socket的簡單方法
六、epoll的設計思路
七、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)?