淺談Java網絡編程(一)——非阻塞I/O

譯自: https://medium.com/@copyconst...

文件描述符(descriptors)

Unix中I/O的基本組成元素是字節序列。大多數程序應用於字節流或I/O流。
進程經過描述符引用I/O流,也被稱做文件描述符。管道、文件、POSIX IPC's(消息隊列,信號量,共享內存),事件隊列等都是經過文件描述符引用I/O流。數組

建立和釋放描述符

描述符建立:網絡

  • 經過系統命令調用(open,pipe,socket等)建立;
  • 繼承自父進程。

描述符釋放:數據結構

  • 進程退出
  • 系統調用close
  • 標記爲close on exec的描述符在exec後釋放

Close-on-exec

當進程forks時,全部描述符都會複製到子進程中。若是任意描述符被標記爲close on exec,那麼當子進程execs以前,父進程forks以後,這些描述符將關閉而且在子進程中再也不可用。socket

使用描述符經過readwrite命令調用的數據轉換
函數

File Entry

每一個描述符都指向內核中的File entry的數據結構。file entry爲每一個描述符維度了一個file offset。系統調用命令open建立file entry.spa

Fork/Dup and File Entries

fork建立的描述符被父子進程共享,在file entry中引用同一個offsetdup/dup2的系統調用與此相似。3d

#include <unistd.h>  
#include <sys/stat.h>  
#include <fcntl.h>  
#include <stdio.h>

int main(char \*argv\[\]) {  
    int fd = open("abc.txt", O\_WRONLY | O\_CREAT | O\_TRUNC, 0666);  
    fork();  
    write(fd, "xyz", 3);  
    printf("%ld\\n", lseek(fd, 0, SEEK\_CUR));  
    close(fd);  
    return 0;  
}

運行結果指針

3
6

Offset-per-descriptor

由於多個描述符可能引用同一個file entry, file entry爲每一個描述符維護了一個file offset。read和write操做從這個file offset開始,而且在數據轉換以後file offset也將更新。offset決定了下次read write操做的位置。當進程終止時,內核將回收全部該進程所持有的描述符,若是此進程是引用file entry的最後一個進程,內核將回收整個file entry。日誌

剖析File Entry

每一個file entry包含:code

  • 類型
  • 函數指針數組。這個函數指針數組將通用的對描述符的操做轉換爲具體文件類型的實現。

稍微解釋下,全部的描述符都對外提供了一套通用的API操做,包含讀、寫、修改描述符模式、截斷描述符、ioctl操做、polling等。
針對不一樣類型的文件,這些操做都有所不一樣,而且有不一樣的實現。對sockets的讀操做與對pipes的讀操做就有所不一樣,即便它們高層次的API是同樣的。open命令並不在此列,由於不一樣類型的文件的open操做差別很是大。可是一旦file entry由open建立,剩下的操做均可以使用同一套通用的API

大多數的網絡通信使用sockets。sockets由描述符引用,做爲傳輸的終點。兩個進程能夠建立兩個sockets,經過鏈接這兩個sockets創建可靠的字節流傳輸。一旦鏈接創建,描述符可使用file offsets進行讀寫。內核能夠將一個進程的輸出重定向到另外一臺機器的另外一個進程。對於字節流鏈接,統一使用read write命令讀寫,但對於不一樣類型的消息(好比網絡數據包)使用不一樣的系統命令處理。

非阻塞描述符

默認狀況下,在沒有數據可用時,經過描述符read將阻塞。writesend也是如此。多數描述符的操做都是如此,可是磁盤文件除外,由於寫磁盤並非直接寫,而是經過內核的buffer cache。只有當open磁盤文件時使用O_SYNC標識才會同步寫磁盤。

任何描述符(pipes, FIFOs, sockets, terminals, pseudo-terminals等)均可以設置爲非阻塞模式。當一個描述符設置爲非阻塞模式時,對此描述符的I/O調用都將當即返回,即便此請求並不能立刻完成(請求完成期間將使進程阻塞)。返回值分爲下列狀況:

  • an error: 操做徹底不能完成
  • a partial count: 輸入或輸出能夠部分完成
  • the entire result: I/O操做能夠徹底完成

經過設置非延遲標識O_NONBLOCK將描述符設置爲非阻塞模式。這個標識也被叫作「open-file」狀態標識。

描述符就緒

當進程經過描述符執行I/O操做時不被阻塞,稱爲描述符就緒。描述符就緒與操做是否會傳輸數據無關,而只與I/O操做是否能夠無阻塞執行相關。

當有I/O事件發生時描述符進行就緒狀態,例如新輸入的到達、socket鏈接完成或者當TCP將列隊中的數據傳輸後,socket的發送buffer出現可用容量時。

有兩種方式能夠判斷一個描述符是否進入就緒狀態——edge triggered和level triggered

Level Triggered

能夠把level triggered看做是拉模式(pull或poll模式)。爲了判斷一個描述符是否就緒,進程嘗試執行非阻塞的I/O操做。進程能夠執行任意次這樣的操做。這爲隨後的I/O操做提供了更多靈活性。好比,一個描述符進入就緒狀態,進程能夠讀取全部可用數據,也能夠不執行任何I/O操做,或者不讀取buffer中的全部數據。
下面舉例來看下

在t0時間,進程嘗試使用非阻塞描述符進行I/O操做。若是I/O操做阻塞,系統調用返回error。

在t1時刻,進程再一次執行I/O,假設此次操做也阻塞並返回error。

在t2時刻,進程又執行了I/O,假設也阻塞或返回error。

假設到了t3時刻,進程拉取描述符的狀態而且描述符就緒。進程能夠執行整個I/O操做(例如讀取socket上全部可用數據)

假設t4時刻,進程拉取描述符狀態但描述符並無就緒,此次調用將再次阻塞或返回error。

t5時刻,描述符就緒,進程只執行了部分I/O操做(例如只讀取一半可用數據)

t6時刻,描述符就緒,進程什麼I/O操做也沒執行

Edge Triggered

當描述符就緒時,進程將收到一個通知(一般是描述符上有新事件發生)。能夠把這種模式看做是push模式,這個描述符就緒的通知是被push給進程的。注意,push模式僅通知進程描述符已就緒,而不會通知其餘信息,好比有多少數據已到達socket的buffer中。

所以,經過這種方式進程只能獲取到不完整的數據,因此進程須要繼續進行操做。當每次獲得通知時,進程嘗試進行最多的I/O操做,若是不這樣作,進程不得不等到下一次獲得通知時才能獲取數據,即便在下一次通知到來前仍有部分數據可用。

下面舉例說明

在t2時刻,進程獲得描述符就緒的通知

可用的字節流存儲在buffer中,假設有1024個字節可讀。

假設進程只讀取了其中的500個字節

這意味着在t3 t4 t5時刻,buffer中仍然有524個字節可以使進程無阻塞地讀取。可是由於只有在它獲得下次通知時纔會執行I/O操做,這524個字節的數據在這期間將一直留在buffer中。

假設進程在t6時刻接到下次通知,buffer中又有1024個字節可用。此時buffer中可用的數據爲1548個字節——524字節是上次沒讀的,1024是新到達的。

假設進程此次讀取了1024字節。

這意味着在此次I/O操做結束後仍有524字節的數據留在buffer中,直到一次通知到來進程才能讀取到。

當一個描述符在通知來到時若是嘗試執行全部I/O操做,可能形成其餘描述符「飢餓」。即便使用level triggered,一次大量的writesend也可能致使阻塞。

多路複用I/O

上面咱們只討論了一個進程只處理一個描述符的狀況。一般進程處理多個描述符。一個常見的場景是一個應用程序須要打印日誌,同時接收socket鏈接而且和其餘服務創建RPC鏈接。

有如下幾種多路複用I/O方式:

  • 非阻塞I/O(描述符自己被標識爲非阻塞,操做可能部分完成)
  • 信號驅動I/O(當I/O狀態變化時通知擁有描述符的進程)
  • polling I/O(經過selectpoll系統調用,這二者都提供了level triggered方式的描述符就緒通知機制)
  • BSD 機制的內核事件polling(使用kevent系統調用)

非阻塞I/O的多路複用I/O

描述符

將全部描述符都設置爲非阻塞模式

進程

進程嘗試對描述符執行I/O操做,檢查是否有任意I/O操做返回error。

內核

內核在描述符上執行I/O操做,返回error或部分輸出或者是所有結果。

缺點

頻繁檢查:若是進程頻繁嘗試執行I/O操做,進程不得不持續地重複檢查描述符是否就緒的操做。在tight循環中這樣的busy-waiting可能會耗盡CPU週期。
不頻繁檢查:若是這樣的操做執行不頻繁,可能使進程對於有效的I/O事件長時間得不到響應。

什麼時候使用

對於輸出描述符(好比write)的操做並不老是阻塞的。在這種場景下,能夠首先嚐試執行I/O操做,若是返回error再回退到polling。當使用edge-triggered通知方式時也可使用這種方式,此時描述符設置爲非阻塞模式,進程一旦獲得一個I/O事件的通知,進程能夠重複執行I/O操做直到系統調用被阻塞(EAGAIN or EWOULDBLOCK)。

信號驅動I/O的多路複用I/O

描述符

當任意描述符上可執行I/O操做時,內核將發送通知給進程。

進程

進程等待任何描述符就緒的信號。

內核

跟蹤描述符列表,當任意描述符就緒時給進程發送信號通知。

缺點

捕獲信號的開銷較大,當大量I/O操做時使用信號驅動I/O方式並不現實。

什麼時候使用

一般在一些「特例條件」下使用,此時處理信號的開銷低於不斷使用select/poll/epollkevent的polling操做。一個「特例條件」的場景是socket上的帶外(out-of-band)數據的到達。總之不經常使用。

Polling I/O的多路複用I/O

描述符

相關文章
相關標籤/搜索