高性能網絡IO模型

網絡IO的本質

任何IO事件處理能夠分爲兩個過程:等待就緒(缺數據或DMA Copy)、數據拷貝(CPU Copy),與之相對的是阻塞與非阻塞、同步與異步是兩組不一樣的概念。html

  • 是否阻塞體如今socket 屬性 O_NONBLOCK
  • 同步/異步體如今 IO讀寫api的區別上

另外須要注意下面幾點:java

  • IO 事件 , 對內核buffer而言,包括 緩衝區滿、緩衝區空、緩衝區非空、緩衝區非滿四種; 對tcp udp等協議而言,包括,已鏈接、關閉、半關閉、可讀、可寫、異常等等
  • 內核的buffer是爲了減小磁盤IO的次數,而用戶進程的buffer是爲了減小系統調用的次數
  • DMA Copy由DMA(Driect Memory Access)芯片獨立進行,不佔用CPU資源
  • 一次系統調用至少包含兩次內核與用戶進程上下文切換

同步阻塞IO(BIO)

同步阻塞IO(BIO的處理過程)linux

同步阻塞IO(BIO)模型中,一個處理單元(進程或者線程)在一個IO事件的生命週期內只處理這一個事件。windows

一般的寫法就是單個線程在一個鏈接的生命週期內全程服務這個鏈接。這種寫法有下列問題:api

  • 併發鏈接數受進程/線程數限制
  • 大量進程/線程wait閒置
  • 頻繁的上下文切換

高性能IO模型

有下列模型/機制可供使用bash

  • IO多路複用
  • 非阻塞IO(NIO)
  • 信號(事件)IO
  • 異步非阻塞IO(AIO)

上述模型並不徹底獨立,是相輔相成的機制網絡

IO多路複用

考慮一個常見的場景:併發

一個線程T 從標準輸入讀取內容,經過一個套接字輸送給服務端(IM場景)app

這時這個線程T 同時持有兩個描述符(socket描述符、標準輸入描述符)異步

若是沒有IO多路複用

當線程T 正在讀標準輸入時,服務端因異常關閉主動斷開了鏈接,此時線程T將感知不到

狀況就是下面咱們常見的代碼

while(read(stdin, buffer, len,size) > 0) {
    write(svr_fd, buffer ,len);
}
複製代碼

IO多路複用是指:

  • 使用調用select、pselect、poll、epoll_wait等函數,替代accept、read、write等函數阻塞在IO處理的第一步:等待就緒上
  • 一次IO系統調用能夠阻塞在多個描述符(套接字)上

調用IO 複用的api(select、pselect、poll、epoll等)時,其阻塞在多個文件描述符(套接字)上,這與普通的阻塞式IO函數如:read、write、close等不一樣,這些函數都是阻塞在一個文件描述符上。以select爲例,select等待多個文件描述符(套接字)上發生IO事件,能夠設置等待超時,select只返回描述符就緒的個數(通常可認爲是IO事件的個數),用戶須要遍掃描整個描述符集處理IO時間。僞代碼以下:

while(true){
     select(描述符集,超時值)
     for(fd in 描述符集合){         
        if ( fd has IO事件){
            處理IO事件
        }
    }
}
複製代碼

真實的select要比此複雜,其可指定本身關心的描述符集,分讀、寫、出錯三種描述符集。

Select的缺點很明顯,當描述符集很大時,遍歷一遍集合的耗時將會很大,所以會有一個FD_SETSIZE宏限制。後續的epoll則優化的此問題,只返回發生的IO事件及其關聯的描述符。

下圖是多路複用處理過程

非阻塞IO(NIO)

非阻塞是指開啓描述符/socket的O_NONBLOCK標誌位。

對此類socket發出read、write等系統調用,如IO事件未就緒,則系統調用直接結束,而且errno將被設置爲EAGAIN( EWOULDBLOCK ),意指待會再試, 可結合輪詢構成一種可用的模型,但不多見。僞代碼以下:

while(true) {
    ret=recv(描述符)
    if(ret != 錯誤 && ret != 結束){
        處理IO事件
    }
}
複製代碼

但NIO+輪詢並非一種好的選擇,頻繁的輪詢白白耗費CPU資源,還形成大量的上下文切換。故然後面提到的 select 、poll、epoll等等待就緒事件的方法實際都是阻塞的

信號(事件)驅動IO

信號驅動式IO是在NIO的基礎上,事先向內核註冊信號處理程序(設置回調),內核在IO就緒以後,將直接向進程發送SIGIO信號(執行回調),用戶進程能夠避免輪詢

縱觀各類讀寫的IO操做,都是首先等待內核準備好數據或準備好存放數據的內核空間,而後執行內核空間與用戶進程空間之間的數據拷貝。其中,信號驅動式IO模型就是在內存作好準備以後,向用戶進程發送SIGIO信號,通知用戶進程執行剩下的數據拷貝的操做。

實際上,SIGIO 通常只用在UDP協議,而TCP基本無效。

緣由是,UDP協議中能觸發SIGIO信號的IO事件只有兩種:

  • 有數據報可讀
  • 套接字發生異步錯誤

而 TCP中能觸發SIGIO的IO事件太多,且信號處理程序不能直接獲取到就緒的事件類型和事件源FD

而且,信號IO不適合註冊多個套接字(IO多路複用)

異步非阻塞IO(AIO)

首先AIO是異步的, 且是非阻塞的. 相較於前幾種IO模型的最大的區別,在於其在IO處理過程當中的第二步:此模型將第二步(處理已就緒數據)一併交給內核處理。在全部事情作完後告知用戶進程(信號或者回調函數)

以讀爲例,過程如圖:

img

Linux 實現的AIO在網絡IO中通常也不使用,緣由有:

  • 很差實現IO多路複用(經過信號不能區分)
  • IO處理過程當中出現異經常使用戶進程很差干預
  • 內核進行CPU Copy一樣須要佔用CPU資源,高併發場景下性能提高有限

能夠看到異步IO實在內核已完成IO操做以後,才發起通知,時機不一樣於信號(事件)驅動式IO。Linux中異步IO系統調用皆以aio_*開頭。操做完成以後的通知方式能夠是信號,也能夠是用戶進程空間中的回調函數,皆可經過aiocb結構體設置。目前linux 雖然已有aio函數,可是即便是epoll並非基於aio, 這與windows iocp和FreeBSD的kqueue純異步的方案是不一樣的,廣泛的測試結果,epoll性能比iocp仍是有微小的差距。

高性能網絡IO實現

常見的高性能IO函數select , epoll等處理流程如圖:

img

第二步也可使用MMAP來減小讀寫次數,但java中mmap只能映射本地文件(FileChannel),不支持映射socket

java程序還須要考慮jvm堆與native堆之間的數據拷貝,更爲複雜(DirectByteBuffer 在常說的native堆,FileChannel.map方法建立的MappedByteBuffer是虛擬地址映射的內核buffer)

關於sendfile, 在linux 2.4版本以前(www.ibm.com/developerwo…)

Copy 一、sendfile引起 DMA 引擎將文件內容拷貝到一個讀取緩衝區。 Copy 二、而後由內核將數據拷貝到與輸出套接字相關聯的內核緩衝區。 Copy 三、數據的第三次複製發生在 DMA 引擎將數據從內核套接字緩衝區傳到協議引擎時

在linux2.4及之後的版本,內核爲此作了改進

Copy 一、sendfile引起 DMA 引擎將文件內容拷貝到內核緩衝區。 Copy 二、數據未被拷貝到套接字緩衝區。取而代之的是,只有包含關於數據的位置和長度的信息的描述符被追加到了套接字緩衝區。DMA 引擎直接把數據從內核緩衝區傳輸到協議引擎,從而消除了剩下的最後一次 CPU 拷貝。

另須要注意:

  • socket是否設置爲阻塞並不影響 selector函數是否阻塞.
  • socket是否必定要設置爲非阻塞呢? 這須要考慮是水平觸發仍是邊緣觸發,還須要考慮用戶程序是屢次仍是一次read\write
  • 儘可能將socket設置爲非阻塞,以防止read、write在第二步阻塞掉線程.

Epoll的優勢

  • fd集合直接存在內核cache,借mmap避免在用戶進程與內核間頻繁拷貝. 而select 、pselect、poll等API,都存在大量內核與用戶進程間的FD拷貝,而且須要用戶遍歷查找就緒FD
  • 使用紅黑樹結構化fd集合以保證高效插入、查找、刪除
  • epoll_wait 僅僅只是掃描已就緒fd鏈表(rdlist)

更多閱讀

掃碼關注本人公衆號

相關文章
相關標籤/搜索