任何IO事件處理能夠分爲兩個過程:等待就緒(缺數據或DMA Copy)、數據拷貝(CPU Copy),與之相對的是阻塞與非阻塞、同步與異步是兩組不一樣的概念。html
另外須要注意下面幾點: java
同步阻塞IO(BIO的處理過程)linux
同步阻塞IO(BIO)模型中,一個處理單元(進程或者線程)在一個IO事件的生命週期內只處理這一個事件。windows
一般的寫法就是單個線程在一個鏈接的生命週期內全程服務這個鏈接。這種寫法有下列問題:api
有下列模型/機制可供使用網絡
上述模型並不徹底獨立,是相輔相成的機制併發
考慮一個常見的場景:app
一個線程T 從標準輸入讀取內容,經過一個套接字輸送給服務端(IM場景)異步
這時這個線程T 同時持有兩個描述符(socket描述符、標準輸入描述符)jvm
若是沒有IO多路複用
當線程T 正在讀標準輸入時,服務端因異常關閉主動斷開了鏈接,此時線程T將感知不到
狀況就是下面咱們常見的代碼
1 while(read(stdin, buffer, len,size) > 0) { 2 write(svr_fd, buffer ,len); 3 }
IO多路複用是指:
調用IO 複用的api(select、pselect、poll、epoll等)時,其阻塞在多個文件描述符(套接字)上,這與普通的阻塞式IO函數如:read、write、close等不一樣,這些函數都是阻塞在一個文件描述符上。以select爲例,select等待多個文件描述符(套接字)上發生IO事件,能夠設置等待超時,select只返回描述符就緒的個數(通常可認爲是IO事件的個數),用戶須要遍掃描整個描述符集處理IO時間。僞代碼以下:
1 while(true){ 2 select(描述符集,超時值) 3 for(fd in 描述符集合){ 4 if ( fd has IO事件){ 5 處理IO事件 6 } 7 } 8 }
真實的select要比此複雜,其可指定本身關心的描述符集,分讀、寫、出錯三種描述符集。
Select的缺點很明顯,當描述符集很大時,遍歷一遍集合的耗時將會很大,所以會有一個FD_SETSIZE宏限制。後續的epoll則優化的此問題,只返回發生的IO事件及其關聯的描述符。
下圖是多路複用處理過程
非阻塞是指開啓描述符/socket的O_NONBLOCK標誌位。
對此類socket發出read、write等系統調用,如IO事件未就緒,則系統調用直接結束,而且errno將被設置爲EAGAIN( EWOULDBLOCK ),意指待會再試, 可結合輪詢構成一種可用的模型,但不多見。僞代碼以下:
1 while(true) { 2 ret=recv(描述符) 3 if(ret != 錯誤 && ret != 結束){ 4 處理IO事件 5 } 6 }
但NIO+輪詢並非一種好的選擇,頻繁的輪詢白白耗費CPU資源,還形成大量的上下文切換。故然後面提到的 select 、poll、epoll等等待就緒事件的方法實際都是阻塞的
信號驅動式IO是在NIO的基礎上,事先向內核註冊信號處理程序(設置回調),內核在IO就緒以後,將直接向進程發送SIGIO信號(執行回調),用戶進程能夠避免輪詢
縱觀各類讀寫的IO操做,都是首先等待內核準備好數據或準備好存放數據的內核空間,而後執行內核空間與用戶進程空間之間的數據拷貝。其中,信號驅動式IO模型就是在內存作好準備以後,向用戶進程發送SIGIO信號,通知用戶進程執行剩下的數據拷貝的操做。
實際上,SIGIO 通常只用在UDP協議,而TCP基本無效。
緣由是,UDP協議中能觸發SIGIO信號的IO事件只有兩種:
而 TCP中能觸發SIGIO的IO事件太多,且信號處理程序不能直接獲取到就緒的事件類型和事件源FD
而且,信號IO不適合註冊多個套接字(IO多路複用)
首先AIO是異步的, 且是非阻塞的. 相較於前幾種IO模型的最大的區別,在於其在IO處理過程當中的第二步:此模型將第二步(處理已就緒數據)一併交給內核處理。在全部事情作完後告知用戶進程(信號或者回調函數)
以讀爲例,過程如圖:
Linux 實現的AIO在網絡IO中通常也不使用,緣由有:
能夠看到異步IO實在內核已完成IO操做以後,才發起通知,時機不一樣於信號(事件)驅動式IO。Linux中異步IO系統調用皆以aio_*開頭。操做完成以後的通知方式能夠是信號,也能夠是用戶進程空間中的回調函數,皆可經過aiocb結構體設置。目前linux 雖然已有aio函數,可是即便是epoll並非基於aio, 這與windows iocp和FreeBSD的kqueue純異步的方案是不一樣的,廣泛的測試結果,epoll性能比iocp仍是有微小的差距。
常見的高性能IO函數select , epoll等處理流程如圖:
第二步也可使用MMAP來減小讀寫次數,但java中mmap只能映射本地文件(FileChannel),不支持映射socket
java程序還須要考慮jvm堆與native堆之間的數據拷貝,更爲複雜(DirectByteBuffer 在常說的native堆,FileChannel.map方法建立的MappedByteBuffer是虛擬地址映射的內核buffer)
關於sendfile, 在linux 2.4版本以前(https://www.ibm.com/developerworks/cn/java/j-zerocopy/)
Copy 一、sendfile引起 DMA 引擎將文件內容拷貝到一個讀取緩衝區。
Copy 二、而後由內核將數據拷貝到與輸出套接字相關聯的內核緩衝區。
Copy 三、數據的第三次複製發生在 DMA 引擎將數據從內核套接字緩衝區傳到協議引擎時
在linux2.4及之後的版本,內核爲此作了改進
Copy 一、sendfile引起 DMA 引擎將文件內容拷貝到內核緩衝區。
Copy 二、數據未被拷貝到套接字緩衝區。取而代之的是,只有包含關於數據的位置和長度的信息的描述符被追加到了套接字緩衝區。DMA 引擎直接把數據從內核緩衝區傳輸到協議引擎,從而消除了剩下的最後一次 CPU 拷貝。
另須要注意:
Epoll的優勢