對epoll機制的學習理解v1
- 2021 年 10 月 20 日
- 筆記
- linux軟硬體性能優化, 作業系統
epoll機制
wrk用非阻塞多路復用IO技術創造出大量的連接,從而達到很好的壓力測試效果。epoll就是實現IO多路復用的關鍵。
本節是對epoll的本質的學習總結,進一步的參考資料為:
《深入理解Nginx:模組開發與架構解析(第二版)》,陶輝
首先分析網路數據接收模型。
電腦分為硬體中斷和軟體中斷,硬體中斷是由外接設備產生的,比如網卡,鍵盤,滑鼠等這些都是硬體設備。硬體設備向CPU發出中斷訊號,高電平訊號到達CPU引腳,觸發CPU立即執行中斷。軟中斷就是由程式產生的中斷。
當網卡收到外部網路發送過來的數據,網卡會做相應的處理,然後網卡發送數據到電腦記憶體中,之後向CPU發出硬體中斷訊號。CPU得到訊號後立即中斷當前任務,去處理網路數據,將記憶體中的網路數據寫入socket對象中,同時喚醒等待該數據的進程。
socket對象用於收發網路數據,socket對象由進程創建後被文件描述符指向,即fd指針。socket對象指定了「埠號」,而網路數據包里包含了埠號,這使得CPU可以準確將數據寫入對應的socket中。socket對象里有三個數據結構: 發送緩衝區,接收緩衝區和等待隊列。接收緩衝區就是負責接收記憶體中的數據,並且等待被進程處理。等待隊列實際上是指針,該指針指向進程時,表示進程處於等待狀態,於是CPU不會處理該進程,而是處理其他進程,直到該進程被中斷程式喚醒,同時中斷程式移除被監聽的socket上的等待隊列,這樣該進程重新加入進程的運行狀態,被CPU處理,這樣,進程拿到了socket緩衝區中的數據,recv這一環順利通過,可以執行下一步。
在早期,互聯網用戶少,因此一台伺服器每當被一個客戶端連接,就建立一個進程,該進程只監聽一個socket, 伺服器能夠承受住負載。當用戶越來越多時,一台伺服器仍然起大量進程已不現實。因此一個進程監聽多個連接的技術應運而生,這就是IO多路復用技術。
最早的IO多路復用技術的思路較為簡單,這就是select方法。
進程創建並監聽多個socket對象,這些socket對象的描述符被寫到數組fds中,進程執行系統調用select時,作業系統將進程放入每個socket的等待隊列,此時進程被阻塞。其中任意一個socket被寫入數據(實際上,喚醒工作是中斷程式做的),進程就會被喚醒,並遍歷fds中的socket對象,並讀取緩衝數據,從而繼續執行下去,此時進程處於運行態。
select方法讓一個進程等待再喚醒執行它的過程中,一共有3次遍歷,2次內核傳遞。讓進程處於等待狀態時,等待即阻塞,因為CPU執行其他進程去了,所以等待狀態下的進程不消耗CPU資源,該進程會被作業系統放入被監聽的所有的socket的等待隊列中,因此需要遍歷fds,遍歷之前需要把fds整個列表傳遞到內核去。等到設備接收到網路數據,進程被喚醒的時候,作業系統要將fds中每個socket的等待隊列中的進程指針清空,因此再一次遍歷,遍歷前仍然要傳遞fds到內核。涉及到進程的操作必然由內核執行,進程內部的執行則是用戶空間許可權,不需要記憶體干涉。最後一次遍歷,是進程遍歷fds上的socket(fds本來就在用戶態),直到找到有緩衝數據的。
這樣會帶來兩個問題,1.頻繁的內核傳遞,2.頻繁的遍歷。問題的根源在於,進程的每一次狀態更新就要重新傳遞fds以及遍歷(fds的狀態更新)。傳遞fds的原因顯而易見,每一次調用select都是一次獨立的監聽一群socket的行為,在實際場景下,fds中的socket並不會較大規模地變化,因此fds最好整個列表只傳一次,如果有修改,也只是對整個小增小減。遍歷既源於fds傳遞至內核後要讓fds中的每個socket和進程建立聯繫,也源於進程喚醒後要尋找到有緩衝數據的socket, 所以最好能進程和fds一次建立聯繫,然後進程能一次就找到需要的socket.
fds的狀態變化和進程狀態的變化是一起發生的,能不能讓它們分開發生?即進程的狀態變化不需要和fds重新建立連接?此外,進程也不知道fds哪個socket發生了變化,因為fds不存儲發生變化的資訊。進程既然要和fds每個socket發生關係,為什麼fds不派一個管理者代表來和進程溝通呢?這個管理者,就是event poll。
epoll就是在fds的基礎上,增加了一個eventpoll數據結構,進程創建fds之後,其中的socket都為空時(如果不為空,recv直接拿到socket數據,就不阻塞了),進入阻塞狀態,此時fds列表整體傳入內核。所有socket與eventpoll對象建立關係,即將eventpoll對象放入所有socket的等待列表裡。然後eventpoll對象的等待列表中放入進程。這是epoll方法下的進程阻塞模型,eventpoll不會頻繁地改變狀態,所以fds列表只傳一次。eventpoll還維護一個rdlist數組,當多個socket收到數據,內核中斷程式拿到了網路數據包中的五元組資訊,拿到了埠號,找到了socket對象,同時知道了socket對象的地址,於是在rdlist數組中寫入這些socket對象的地址。進程被喚醒時,被從eventpoll的等待列表裡移除,進程又讀取rdlist中的socket對象地址,直接找到收到數據的socket.
epoll的核心在於操作eventpoll管理進程狀態改變,只要傳遞一次fds,遍歷1次fds就可以阻塞進程,喚醒進程則只需操作一次eventpoll。極大降低了開銷。epoll的根本原理還是中間層原理。
參考
注1:等待隊列的真正意思是,該socket有個列表,裡面存儲了所有監聽該socket的進程的fd描述符。所以,可以有多個進程監聽同一個埠。
注2:網卡將數據寫入記憶體,中斷程式將記憶體中的數據寫入socket對象中。喚起進程的是中斷程式,中斷程式是硬中斷髮起後,被CPU執行的。喚起進程的同時,將所有等待隊列清空,清空後便可以CPU執行該進程,執行中,遍歷socket,如果哪個socket收到了數據,便處理哪一個recv. select,就是選擇,就是遍歷式地選擇。
注3:進程是被內核管理的,所以,操作進程,就必須將所涉及到的數據傳遞給內核。內核和應用空間的關係,理解成包圍和被包圍的關係更為合適。
其他小問題
什麼是事件?
事件是被進程所等待的數據。1個事件可以讓多個進程等待。
為什麼說等待了,就會阻塞呢?
因為進程A創建完socket之後,下一步到了recv方法,此時進程A被丟入(其實是生成一個等待中的引用)socket對象的等待隊列中去(記憶體),CPU就去執行其他的進程了。直到有socket事件被硬中斷傳入,CPU將其寫入記憶體,進程A才再次被喚醒。
為什麼阻塞是進程調度關鍵的一環?
阻塞又叫做等待狀態,等待什麼? 進程在等待某一個事件的發生,在等待時,無法進入下一步狀態。對於處理網路的進程,就是等待接收網路數據包。
eventpoll對象的數據結構是怎樣的?
eventpoll用到了紅黑樹。就緒列表需要快速地被加入和刪除,所以,就緒列表是紅黑樹。
為什麼select監視的最大socket數量是1024個?
因為select在每次進程狀態改變時候,要3次遍歷fds列表,2次將fds列表傳遞到內核,fds列表變大,即提升了遍歷時間,又因為複製更大的數據傳遞至內核,用戶空間到內核空間的複製傳輸開銷較大。所以限制了fds的大小。默認最大是1024.