譯自: https://medium.com/@copyconst...
Unix中I/O的基本組成元素是字節序列。大多數程序應用於字節流或I/O流。
進程經過描述符引用I/O流,也被稱做文件描述符。管道、文件、POSIX IPC's(消息隊列,信號量,共享內存),事件隊列等都是經過文件描述符引用I/O流。數組
描述符建立:網絡
描述符釋放:數據結構
當進程forks時,全部描述符都會複製到子進程中。若是任意描述符被標記爲close on exec,那麼當子進程execs以前,父進程forks以後,這些描述符將關閉而且在子進程中再也不可用。socket
使用描述符經過read、 write命令調用的數據轉換
函數
每一個描述符都指向內核中的File entry的數據結構。file entry爲每一個描述符維度了一個file offset。系統調用命令open建立file entry.spa
由fork建立的描述符被父子進程共享,在file entry中引用同一個offset。dup/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
由於多個描述符可能引用同一個file entry, file entry爲每一個描述符維護了一個file offset。read和write操做從這個file offset開始,而且在數據轉換以後file offset也將更新。offset決定了下次read write操做的位置。當進程終止時,內核將回收全部該進程所持有的描述符,若是此進程是引用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將阻塞。write和send也是如此。多數描述符的操做都是如此,可是磁盤文件除外,由於寫磁盤並非直接寫,而是經過內核的buffer cache。只有當open磁盤文件時使用O_SYNC標識才會同步寫磁盤。
任何描述符(pipes, FIFOs, sockets, terminals, pseudo-terminals等)均可以設置爲非阻塞模式。當一個描述符設置爲非阻塞模式時,對此描述符的I/O調用都將當即返回,即便此請求並不能立刻完成(請求完成期間將使進程阻塞)。返回值分爲下列狀況:
經過設置非延遲標識O_NONBLOCK將描述符設置爲非阻塞模式。這個標識也被叫作「open-file」狀態標識。
當進程經過描述符執行I/O操做時不被阻塞,稱爲描述符就緒。描述符就緒與操做是否會傳輸數據無關,而只與I/O操做是否能夠無阻塞執行相關。
當有I/O事件發生時描述符進行就緒狀態,例如新輸入的到達、socket鏈接完成或者當TCP將列隊中的數據傳輸後,socket的發送buffer出現可用容量時。
有兩種方式能夠判斷一個描述符是否進入就緒狀態——edge 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操做也沒執行
當描述符就緒時,進程將收到一個通知(一般是描述符上有新事件發生)。能夠把這種模式看做是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,一次大量的write或send也可能致使阻塞。
上面咱們只討論了一個進程只處理一個描述符的狀況。一般進程處理多個描述符。一個常見的場景是一個應用程序須要打印日誌,同時接收socket鏈接而且和其餘服務創建RPC鏈接。
有如下幾種多路複用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方式並不現實。
一般在一些「特例條件」下使用,此時處理信號的開銷低於不斷使用select/poll/epoll或kevent的polling操做。一個「特例條件」的場景是socket上的帶外(out-of-band)數據的到達。總之不經常使用。