高性能網絡服務器編程

下面來解釋下爲什麼Linux AIO 和Java AIO都不使用!

基本的IO編程過程(包括網絡IO和文件IO):

  1. 打開文件描述符(windows是handler,java是stream或channel)
  2. 多路捕獲(Multiplexe,即select和poll和epoll)IO可讀寫的狀態
  3. 可以讀寫的文件描述符進行IO讀寫

由於IO設備速度和CPU內存比速度會慢,爲了更好的利用CPU和內存,會開多線程,每個線程讀寫一個文件描述符。

高性能的網絡編程(即IO編程)

  1. 需要鬆綁IO連接和應用程序線程的對應關係,這就是非阻塞(nonblocking)、異步(asynchronous)的要求的由來(構造一個線程池,epoll監控到有數的fd,把fd傳入線程池,由這些worker thread來讀寫io)
  2. 需要高性能的OS對IO設備可讀寫(數據來了)的通知方式:從level-triggered notification到edge-triggered notification

select poll epoll

select poll epoll三個都是是Linux下 I/O 多路複用的具體實現,select 出現的最早,之後是 poll,再是 epoll。

不理解的朋友可以參考面試中的Linux中的介紹。

  • select和poll的效率都很低,它們的問題在於這兩個函數都需要將所有的文件描述符鏈表內容全部從用戶進程內存中複製到操作系統內核中,內核需要將所有文件描述符遍歷一遍,這個過程非常低效。
  • epoll很巧妙,它將用戶關心的描述符放到內核的一個事件表中,從而只需要在用戶空間和內核空間拷貝一次。它事先通過 epoll_ctl() 來註冊描述符,一旦基於某個描述符就緒時,內核會採用類似 callback 的回調機制,迅速激活這個描述符,當進程調用 epoll_wait() 時便得到通知。

epoll實現:三個核心點是:1、mmap,2、紅黑樹,3、rdlist(就緒描述符鏈表是一個雙向鏈表)

  1. mmap是共享內存,用戶進程和內核有一段地址(虛擬存儲器地址)映射到了同一塊物理地址上,這樣當內核要對描述符上的事件進行檢查的時候就不用來回的拷貝了。mmap映射內存必須是頁面大小的整數倍,面向流的設備不能進行mmap,mmap的實現和硬件有關。
  2. 紅黑樹是用來存儲這些描述符的,因爲紅黑樹的特性,就是良好的插入,查找,刪除性能O(lgN)。
  3. rdlist 就緒描述符鏈表這是一個雙鏈表,epoll_wait()函數返回的也是這個就緒鏈表。

內部用了一個紅黑樹記錄添加的socket,用了一個雙向鏈表接收內核觸發的事件。

epoll極其高效的原因:

  1. 在調用epoll_create時,內核除了幫我們在epoll文件系統裏建了個file結點,在內核cache裏建了個紅黑樹用於存儲以後epoll_ctl傳來的socket外,還會再建立一個list鏈表,用於存儲準備就緒的事件,當epoll_wait調用時,僅僅觀察這個list鏈表裏有沒有數據即可。有數據就返回,沒有數據就sleep,等到timeout時間到後即使鏈表沒數據也返回。所以,epoll_wait非常高效。
  2. 這個準備就緒list鏈表是怎麼維護的呢?當我們執行epoll_ctl時,除了把socket放到epoll文件系統裏file對象對應的紅黑樹上之外,還會給內核中斷處理程序註冊一個回調函數,告訴內核,如果這個句柄的中斷到了,就把它放到準備就緒list鏈表裏。所以,當一個socket上有數據到了,內核在把網卡上的數據copy到內核中後就來把socket插入到準備就緒鏈表裏了。(注:好好理解這句話!)
    從上面這句可以看出,epoll的基礎就是回調呀!
  3. 執行epoll_ctl時,如果增加socket句柄,則檢查在紅黑樹中是否存在,存在立即返回,不存在則添加到樹幹上,然後向內核註冊回調函數,用於當中斷事件來臨時向準備就緒鏈表中插入數據。執行epoll_wait時立刻返回準備就緒鏈表裏的數據即可。

內核通知模式

  1. level-triggered notification:當 epoll_wait() 檢測到描述符事件發生並將此事件通知應用程序,應用程序可以不立即處理該事件。下次調用 epoll_wait() 時,會再次響應應用程序並通知此事件。是默認的一種模式,並且同時支持 Blocking 和 No-Blocking。
  2. edge-triggered notification:當 epoll_wait() 檢測到描述符事件發生並將此事件通知應用程序,應用程序必須立即處理該事件。如果不處理,下次調用 epoll_wait() 時,不會再次響應應用程序並通知此事件。很大程度上減少了 epoll 事件被重複觸發的次數,因此效率要比 LT 模式高。只支持 No-Blocking,以避免由於一個文件句柄的阻塞讀/阻塞寫操作把處理多個文件描述符的任務餓死。

select 和 poll 都是隻支持level-triggered notification,而epoll支持edge-triggered notification。
注意還要打開epoll的edge-triggered notification。而java的NIO和NIO.2都只是用了epoll,沒有打開edge-triggered notification,所以不如JBoss的Netty。

AIO 的問題

select,poll,epoll都需要用一個函數去監控一大堆fd,AIO不需要了,你把fd告訴內核,應用程序無需等待,內核會通過信號等軟中斷告訴應用程序,數據來了,你直接讀了,所以,用了AIO可以廢棄select,poll,epoll。

但linux的AIO的實現方式是內核和應用共享一片內存區域,應用通過檢測這個內存區域(避免調用nonblocking的read、write函數來測試是否來數據,因爲即便調用nonblocking的read和write由於進程要切換用戶態和內核態,仍舊效率不高)來得知fd是否有數據,可是檢測內存區域畢竟不是實時的,你需要在線程裏構造一個監控內存的循環,設置sleep,總的效率不如epoll這樣的實時通知。

所以,AIO是渣,適合低併發的IO操作。所以java7引入的NIO.2引入的AIO對高併發的網絡IO設計程序來說,也是渣!
只有Netty的epoll+edge-triggered notification最牛,能在linux讓應用和OS取得最高效率的溝通。

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