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.