概述
管道最常見的地方是shell中,比如:
$ ls | wc -l
為了執(zhí)行上面的命令,shell創(chuàng)建了兩個進(jìn)程來分別執(zhí)行 ls 和 wc (通過 fork() 和 exec() 完成),如下:
從上圖可以看出,可以將管道看成是一組水管,它允許數(shù)據(jù)從一個進(jìn)程流向另一個進(jìn)程,這也是管道名稱的由來。
從上圖可以看出,由兩個進(jìn)程連接到了管道上,這樣寫入進(jìn)程 ls 就將其標(biāo)準(zhǔn)輸出(文件描述符為1)連接到來管道的寫入段,讀取進(jìn)程 wc 就將其標(biāo)準(zhǔn)輸入(文件描述符為0)連接到管道的讀取端。實(shí)際上,這兩個進(jìn)程并不知道管道的存在,它們只是從標(biāo)準(zhǔn)文件描述符中讀取和寫入數(shù)據(jù)。shell 必須要完成相關(guān)的工作。
一個管道是一個字節(jié)流
管道是一個字節(jié)流,即在使用管道時是不存在消息或者消息邊界的概念的:
從管道中讀取數(shù)據(jù)的進(jìn)程可以讀取任意大小的數(shù)據(jù)塊,而不管寫入進(jìn)程寫入管道的數(shù)據(jù)塊的大小是什么
通過管道傳遞的數(shù)據(jù)是順序的,從管道中讀取出來的字節(jié)的順序與它們被寫入管道的順序是完全一樣的,在管道中無法使用 lseek() 來隨機(jī)的訪問數(shù)據(jù)
如果需要在管道中實(shí)現(xiàn)離散消息的概念,那么就必須要在應(yīng)用程序中完成這些工作。雖然這是可行的,但如果碰到這種需求的話最好使用其他 IPC 機(jī)制,如消息隊(duì)列和數(shù)據(jù)報 socket。
從管道中讀取數(shù)據(jù)
試圖從一個當(dāng)前為空的管道中讀取數(shù)據(jù)將會被阻塞直至至少有一個字節(jié)被寫入到管道中為止。
如果管道的寫入端被關(guān)閉了,那么從管道中讀取數(shù)據(jù)的進(jìn)程在讀完管道中剩余的所有數(shù)據(jù)之后將會看到文件結(jié)束(即 read() 返回 0)。
管道是單向的
在管道中數(shù)據(jù)的傳遞方向是單向的。管道的一端用于寫入,另一端則用于讀取。
在其他一些 UNIX 實(shí)現(xiàn)上,特別是那些從 System V Release 4 演化而來的系統(tǒng),管道是雙向的(所謂的流管道)。雙向管道并沒有在任何 UNIX 標(biāo)準(zhǔn)中進(jìn)行規(guī)定,因此即使在提供了雙向管道的實(shí)現(xiàn)上最好也避免依賴這種語義。作為替代方案,可以使用 UNIX domain 流 socket 對(通過 socketpair() 系統(tǒng)調(diào)用來創(chuàng)建),它提供了一種標(biāo)準(zhǔn)的雙向通信機(jī)制,并且其語義與流管道是等價的。
可以確保寫入不超過 PIPE_BUF 字節(jié)的操作是原子的
如果多個進(jìn)程寫入同一個管道,那么如果它們在一個時刻寫入的數(shù)據(jù)量不超過 PIPE_BUF 字節(jié),那么就可以確保寫入的數(shù)據(jù)不會發(fā)生相互混合的情況。
SUSv3 要求 PIPE_BUF 至少為 _POSIX_PIPE_BUF(512)。一個實(shí)現(xiàn)應(yīng)該定義 PIPE_BUF(在
當(dāng)寫入管道的數(shù)據(jù)塊的大小超過了 PIPE_BUF 字節(jié),那么內(nèi)核可能會將數(shù)據(jù)分割成幾個較小的片段來傳輸,在讀者從管道中消耗數(shù)據(jù)時再附加上后繼的數(shù)據(jù)(write()調(diào)用會阻塞直到所有數(shù)據(jù)被寫入到管道為止)
當(dāng)只有一個進(jìn)程向管道寫入數(shù)據(jù)時(通常的情況),PIPE_BUF 的取值就沒有關(guān)系了
但如果有多個寫入進(jìn)程,那么大數(shù)據(jù)塊的寫入可能會被分解成任意大小的段(可能會小于 PIPE_BUF 字節(jié)),并且可能會出現(xiàn)與其他進(jìn)程寫入的數(shù)據(jù)交叉的現(xiàn)象
只有在數(shù)據(jù)被傳輸?shù)焦艿赖臅r候 PIPE_BUF 限制才會起作用。當(dāng)寫入的數(shù)據(jù)達(dá)到 PIPE_BUF 字節(jié)時,write() 會在必要的時候阻塞知道管道中的可用空間足以原子的完成此操作。如果寫入的數(shù)據(jù)大于 PIPE_BUF 字節(jié),那么 write() 會盡可能的多傳輸數(shù)據(jù)以充滿整個管道,然后阻塞直到一些讀取進(jìn)程從管道中移除了數(shù)據(jù)。如果此類阻塞的 write() 被一個信號處理器中斷了,那么這個調(diào)用會被解除阻塞并返回成功傳輸?shù)焦艿乐械淖止?jié)數(shù),這個字節(jié)數(shù)會少于請求寫入的字節(jié)數(shù)(所謂的部分寫入)。
管道的容量是有限的
管道其實(shí)是一個在內(nèi)核內(nèi)存中維護(hù)的緩沖器,這個緩沖器的存儲能力是有限的。一旦管道被填滿之后,后繼向管道的寫入操作就會被阻塞直到讀者從管道中移除了一些數(shù)據(jù)為止。
SUSv3 并沒有規(guī)定管道的存儲能力。在早于 2.6.11 的 Linux 內(nèi)核中,管道的存儲能力與系統(tǒng)頁面的大小是一致的(如在 x86-32 上是 4096 字節(jié)),而從 Linux 2.6.11 起,管道的存儲能力是 65,536 字節(jié)。其他 UNIX 實(shí)現(xiàn)上的管道的存儲能力可能是不同的。
一般來講,一個應(yīng)用程序無需知道管道的實(shí)際存儲能力。如果需要防止寫者進(jìn)程阻塞,那么從管道中讀取數(shù)據(jù)的進(jìn)程應(yīng)該被設(shè)計成以盡可能快的速度從管道中讀取數(shù)據(jù)。
創(chuàng)建和使用管道
#includeint pipe(int fd[2]);
pipe() 創(chuàng)建一個新管道
成功的調(diào)用在數(shù)組 fd 中返回兩個打開的文件描述符,一個表示管道的讀取端 fd[0],一個表示管道的寫入端 fd[1]
調(diào)用 pipe() 函數(shù)時,首先在內(nèi)核中開辟一塊緩沖區(qū)用于通信,它有一個讀端和一個寫端,然后通過 fd 參數(shù)傳出給用戶進(jìn)程兩個文件描述符,fd[0] 指向管道的讀端,fd[1] 指向管道的寫段。
不要用 fd[0] 寫數(shù)據(jù),也不要用 fd[1] 讀數(shù)據(jù),其行為未定義的,但在有些系統(tǒng)上可能會返回 -1 表示調(diào)用失敗。數(shù)據(jù)只能從 fd[0] 中讀取,數(shù)據(jù)也只能寫入到fd[1],不能倒過來。
與所有文件描述符一樣,可以使用 read() 和 write() 系統(tǒng)調(diào)用來在管道上執(zhí)行 IO,一旦向管道的寫入端寫入數(shù)據(jù)之后立即就能從管道的讀取端讀取數(shù)據(jù)。管道上的 read() 調(diào)用會讀取的數(shù)據(jù)量為所請求的字節(jié)數(shù)與管道中當(dāng)前存在的字節(jié)數(shù)兩者之間的較小值。當(dāng)管道為空時,讀取操作阻塞。
也可以在管道上使用 stdio 函數(shù)(printf()、scanf() 等),只需要首先使用 fdopen() 獲取一個與 filedes 中的某個描述符對應(yīng)的文件流即可。但在這樣做的時候需要解決 stdio 緩沖問題。
管道可以用于進(jìn)程內(nèi)部自己通信:
管道可以用于親緣關(guān)系(子進(jìn)程會繼承父進(jìn)程中的文件描述符的副本)進(jìn)程中通信:
不建議將單個 pipe 用作全雙工的,或者不關(guān)閉用作半雙工而不關(guān)閉相應(yīng)的讀端/寫端,這樣很可能導(dǎo)致死鎖:如果兩個進(jìn)程同時試圖從管道中讀取數(shù)據(jù),那么就無法確定哪個進(jìn)程會首先讀取成功,從而產(chǎn)生兩個進(jìn)程競爭數(shù)據(jù)了。要防止這種競爭情況的出現(xiàn)就需要使用某種同步機(jī)制。這時,就需要考慮死鎖問題了,因?yàn)槿绻麅蓚€進(jìn)程都試圖從空管道中讀取數(shù)據(jù)或者嘗試向已滿的管道中寫入數(shù)據(jù)就可能會發(fā)生死鎖。
如果我們想要一個雙向數(shù)據(jù)流時,可以創(chuàng)建兩個管道,每個方向一個。
管道允許相關(guān)進(jìn)程間的通信
其實(shí)管道可以用于任意兩個甚至更多相關(guān)進(jìn)程之間的通信,只要在創(chuàng)建子進(jìn)程的系列 fork() 調(diào)用之前通過一個共同的祖先進(jìn)程創(chuàng)建管道即可。
關(guān)閉未使用管道文件描述符
關(guān)閉未使用管道文件描述符不僅僅是為了確保進(jìn)程不會消耗盡其文件描述符的限制。
從管道中讀取數(shù)據(jù)的進(jìn)程會關(guān)閉其持有的管道的寫入描述符,這樣當(dāng)其他進(jìn)程完成輸出并關(guān)閉其寫入描述符之后,讀者就能夠看到文件結(jié)束。反之,如果讀取的進(jìn)程沒有關(guān)閉管道的寫入端,那么在其他進(jìn)程關(guān)閉了寫入描述符之后,即使讀者已經(jīng)讀完了管道中的所有數(shù)據(jù),也不會看到文件結(jié)束。因?yàn)榇藭r內(nèi)核知道至少還有一個管道的寫入描述符打開著,從而導(dǎo)致 read() 阻塞。
當(dāng)一個進(jìn)程視圖向一個管道中寫入數(shù)據(jù)但沒有任何進(jìn)程擁有該管道的打開著的讀取描述符時,內(nèi)核會向?qū)懭脒M(jìn)程發(fā)送一個 SIGPIPE 信號,默認(rèn)情況下,這個信號將會殺死進(jìn)程,但進(jìn)程可以選擇忽略或者設(shè)置信號處理器,這樣 write() 將因?yàn)?EPIPE 錯誤而失敗。收到 SIGPIPE 信號和得到 EPIPE 錯誤對于標(biāo)識管道的狀態(tài)是有意義的,這就是為什么需要關(guān)閉管道的未使用讀取描述符的原因。如果寫入進(jìn)程沒有關(guān)閉管道的讀取端,那么即使在其他進(jìn)程已經(jīng)關(guān)閉了管道的讀取端之后,寫入進(jìn)程仍然能夠向管道寫入數(shù)據(jù),最后寫入進(jìn)程會將數(shù)據(jù)充滿整個管道,后續(xù)的寫入請求會將永遠(yuǎn)阻塞。
使用管道連接過濾器
當(dāng)管道被創(chuàng)建之后,為管道的兩端分配的文件描述符是可用描述符中數(shù)值最小的兩個,由于通常情況下,進(jìn)程已經(jīng)使用了描述符 0,1,2,因此會為管道分配一些數(shù)值更大的描述符。如果需要使用管道連接兩個過濾器(即從 stdin 讀取和寫入到 stdout),使得一個程序的標(biāo)準(zhǔn)輸出被重定向到管道中,就需要采用復(fù)制文件描述符技術(shù)。
int pfd[2]; pipe(pfd); close(STDOUT_FILENO); dup2(pfd[1],STDOUT_FILENO);
上面這些調(diào)用的最終結(jié)果是進(jìn)程的標(biāo)準(zhǔn)輸出被綁定到管道的寫入端,而對應(yīng)的一組調(diào)用可以用來將進(jìn)程的標(biāo)準(zhǔn)的輸入綁定到管道的讀取端上。
通過管道與 shell 命令進(jìn)行通信: popen()
#includeFILE *popen (const char *command, const char *mode);
pipe() 和 close() 是最底層的系統(tǒng)調(diào)用,它的進(jìn)一步封裝是 popen() 和 pclose()
popen()函數(shù)創(chuàng)建了一個管道,然后創(chuàng)建了一個子進(jìn)程來執(zhí)行 shell,而 shell 又創(chuàng)建了一個子進(jìn)程來執(zhí)行command字符串
mode 參數(shù)是一個字符串:
它確定調(diào)用進(jìn)程是從管道中讀取數(shù)據(jù)(mode 是 r)還是將數(shù)據(jù)寫入到管道中(mode 是 w)
由于管道是向的,因此無法在執(zhí)行的 command 中進(jìn)行雙向通信
mode 的取值確定了所執(zhí)行的命令的標(biāo)準(zhǔn)輸出是連接到管道的寫入端還是將其標(biāo)準(zhǔn)輸入連接到管道的讀取端
popen() 在成功時會返回可供 stdio 庫函數(shù)使用的文件流指針。當(dāng)發(fā)生錯誤時,popen() 會返回 NULL 并設(shè)置 errno 以標(biāo)示出發(fā)生錯誤的原因
在 popen() 調(diào)用之后,調(diào)用進(jìn)程使用管道來讀取 command 的輸出或使用管道向其發(fā)送輸入。與使用 pipe() 創(chuàng)建的管道一樣,當(dāng)從管道中讀取數(shù)據(jù)時,調(diào)用進(jìn)程在 command 關(guān)閉管道的寫入端之后會看到文件結(jié)束;當(dāng)向管道寫入數(shù)據(jù)時,如果 command 已經(jīng)關(guān)閉了管道的讀取端,那么調(diào)用進(jìn)程就會收到 SIGPIPE 信號并得到 EPIPE 錯誤
#includeint pclose ( FILE * stream);
一旦IO結(jié)束之后可以使用 pclose() 函數(shù)關(guān)閉管道并等待子進(jìn)程中的 shell 終止(不應(yīng)該使用 fclose() 函數(shù),因?yàn)樗粫却舆M(jìn)程。)
pclose() 在成功時會返回子進(jìn)程中 shell 的終止?fàn)顟B(tài)(即 shell 所執(zhí)行的最后一條命令的終止?fàn)顟B(tài),除非 shell 是被信號殺死的)
和 system() 一樣,如果無法執(zhí)行shell,那么 pclose() 會返回一個值就像子進(jìn)程中的 shell 通過調(diào)用 _exit(127) 來終止一樣
如果發(fā)生了其他錯誤,那么 pclose() 返回 ?1。其中可能發(fā)生的一個錯誤是無法取得終止?fàn)顟B(tài)
當(dāng)執(zhí)行等待以獲取子進(jìn)程中 shell 的狀態(tài)時,SUSv3 要求 pclose() 與 system() 一樣,即在內(nèi)部的 waitpid() 調(diào)用被一個信號處理器中斷之后自動重啟該調(diào)用。
與 system() 一樣,在特權(quán)進(jìn)程中永遠(yuǎn)都不應(yīng)該使用 popen()。
popen優(yōu)缺點(diǎn):
優(yōu)點(diǎn):在 Linux 中所有的參數(shù)擴(kuò)展都是由 shell 來完成的。所以在啟動 command 命令之前程序先啟動 shell 來分析 command 字符串,就可以使用各種 shell 擴(kuò)展(比如通配符),這樣我們可以通過 popen() 調(diào)用非常復(fù)雜的 shell 命令
缺點(diǎn):對于每個 popen() 調(diào)用,不僅要啟動一個被請求的程序,還需要啟動一個 shell。即每一個 popen() 將啟動兩個進(jìn)程。從效率和資源的角度看,popen() 函數(shù)的調(diào)用比正常方式要慢一些
pipe() VS popen()
pipe()是一個底層調(diào)用,popen() 是一個高級的函數(shù)
pipe() 單純的創(chuàng)建管道,而 popen() 創(chuàng)建管道的同時 fork() 子進(jìn)程
popen() 在兩個進(jìn)程中傳遞數(shù)據(jù)時需要調(diào)用 shell 來解釋請求命令;pipe() 在兩個進(jìn)程中傳遞數(shù)據(jù)不需要啟動 shell 來解釋請求命令,同時提供了對讀寫數(shù)據(jù)的更多控制(popen() 必須時 shell 命令,pipe() 則無硬性要求)
popen() 函數(shù)是基于文件流(FILE)工作的,而 pipe() 是基于文件描述符工作的,所以在使用 pipe() 后,數(shù)據(jù)必須要用底層的read() 和 write() 調(diào)用來讀取和發(fā)送
管道和 stdio 緩沖
由于 popen() 調(diào)用返回的文件流指針沒有引用一個終端,因此 stdio 庫會對這種流應(yīng)用塊緩沖。這意味著當(dāng) mode 的值為 w 來調(diào)用 popen() 時,默認(rèn)情況下只有當(dāng) stdio 緩沖區(qū)被充滿或者使用 pclose() 關(guān)閉了管道之后才會被發(fā)送到管道的另一端的子進(jìn)程。在很多情況下,這種處理方式是不存在問題的。但如果需要確保子進(jìn)程能夠立即從管道中接收數(shù)據(jù),那么就需要定期調(diào)用 fflush() 或使用 setbuf(fp, NULL) 調(diào)用禁用 stdio 緩沖。當(dāng)使用 pipe() 系統(tǒng)調(diào)用創(chuàng)建管道,然后使用 fdopen() 獲取一個與管道的寫入端對應(yīng)的 stdio 流時也可以使用這項(xiàng)技術(shù)
如果調(diào)用 popen() 的進(jìn)程正在從管道中讀取數(shù)據(jù)(即 mode 是 r),那么事情就不是那么簡單了。在這樣情況下如果子進(jìn)程正在使用 stdio 庫,那么——除非它顯式地調(diào)用了 fflush() 或 setbuf() ,其輸出只有在子進(jìn)程填滿 stdio 緩沖器或調(diào)用了 fclose() 之后才會對調(diào)用進(jìn)程可用。(如果正在從使用 pipe() 創(chuàng)建的管道中讀取數(shù)據(jù)并且向另一端寫入數(shù)據(jù)的進(jìn)程正在使用 stdio 庫,那么同樣的規(guī)則也是適用的。)如果這是一個問題,那么能采取的措施就比較有限的,除非能夠修改在子進(jìn)程中運(yùn)行的程序的源代碼使之包含對 setbuf() 或 fflush() 調(diào)用。
如果無法修改源代碼,那么可以使用偽終端來替換管道。一個偽終端是一個 IPC 通道,對進(jìn)程來講它就像是一個終端。其結(jié)果是 stdio 庫會逐行輸出緩沖器中的數(shù)據(jù)。
命名管道(FIFO)
上述管道雖然實(shí)現(xiàn)了進(jìn)程間通信,但是它具有一定的局限性:
匿名管道只能是具有血緣關(guān)系的進(jìn)程之間通信
它只能實(shí)現(xiàn)一個進(jìn)程寫另一個進(jìn)程讀,而如果需要兩者同時進(jìn)行時,就得重新打開一個管道
為了使任意兩個進(jìn)程之間能夠通信,就提出了命名管道(named pipe 或 FIFO):
FIFO 與管道的區(qū)別:FIFO 在文件系統(tǒng)中擁有一個名稱,并且其打開方式與打開一個普通文件一樣,能夠?qū)崿F(xiàn)任何兩個進(jìn)程之間通信。而匿名管道對于文件系統(tǒng)是不可見的,它僅限于在父子進(jìn)程之間的通信
一旦打開了 FIFO,就能在它上面使用與操作管道和其他文件的系統(tǒng)調(diào)用一樣的 IO 系統(tǒng)調(diào)用 read(),write(),close()。與管道一樣,F(xiàn)IFO 也有一個寫入端和讀取端,并且總是遵循先進(jìn)先出的原則,即第一個進(jìn)來的數(shù)據(jù)會第一個被讀走
與管道一樣,當(dāng)所有引用 FIFO 的描述符都關(guān)閉之后,所有未被讀取的數(shù)據(jù)都將被丟棄
使用 mkfifo 命令可以在 shell 中創(chuàng)建一個 FIFO:
mkfifo [-m mode] pathname
pathname 是創(chuàng)建的 FIFO 的名稱,-m 選項(xiàng)指定權(quán)限 mode,其工作方式與 chmod 命令一樣
fstat() 和 stat() 函數(shù)會在 stat 結(jié)構(gòu)的 st_mode 字段返回 S_IFIFO,使用 ls -l 列出文件時,F(xiàn)IFO 文件在第一列的類型為 p,ls -F 會在 FIFO 路徑名后面附加管道符 |
#include#include int mkfifo(const char *pathname,mode_t mode);
mode 參數(shù)指定了新 FIFO 的權(quán)限,這些權(quán)限會按照進(jìn)程的 umask 值來取掩碼
一旦創(chuàng)建了 FIFO,任何進(jìn)程都能夠打開它,只要它通過常規(guī)的文件權(quán)限檢測
使用 FIFO 時唯一明智的做法是在兩端分別設(shè)置一個讀取進(jìn)程和一個寫入進(jìn)程。這樣在默認(rèn)情況下,打開一個 FIFO 以便讀取數(shù)據(jù)(open() O_RDONLY 標(biāo)記)將會阻塞直到另一個進(jìn)程打開 FIFO 以寫入數(shù)(open() O_WRONLY 標(biāo)記)為止。相應(yīng)地,打開一個 FIFO 以寫入數(shù)據(jù)將會阻塞直到另一個進(jìn)程打開 FIFO 以讀取數(shù)據(jù)為止。換句話說,打開一個 FIFO 會同步讀取進(jìn)程和寫入進(jìn)程。如果一個 FIFO 的另一端已經(jīng)打開(可能是因?yàn)橐粚M(jìn)程已經(jīng)打開了 FIFO 的兩端),那么open() 調(diào)用會立即成功。
在大多數(shù) Unix 實(shí)現(xiàn)上(包含 Linux),當(dāng)打開一個 FIFO 時可以通過指定 O_RDWR 標(biāo)記來繞過打開 FIFO 時的阻塞行為。這樣,open() 會立即返回,但無法使用返回的文件描述符在 FIFO 上讀取和寫入數(shù)據(jù)。這種做法破壞了 FIFO 的 IO 模型,SUSv3 明確指出以 O_RDWR 標(biāo)記打開一個 FIFO 的結(jié)果是未知的,因此出于可移植性的原因,開發(fā)人員不應(yīng)該使用這項(xiàng)技術(shù)。對于那些需要避免在打開 FIFO 時發(fā)生阻塞的需求,open() 的 O_NONBLOCK 標(biāo)記提供了一種標(biāo)準(zhǔn)化的方法來完成這個任務(wù):
open(const char *path, O_RDONLY | O_NONBLOCK); open(const char *path, O_WRONLY | O_NONBLOCK);
在打開一個 FIFO 時避免使用 O_RDWR 標(biāo)記還有另外一個原因,當(dāng)采用那種方式調(diào)用 open() 之后,調(diào)用進(jìn)程在從返回的文件描述符中讀取數(shù)據(jù)時永遠(yuǎn)都不會看到文件結(jié)束,因?yàn)橛肋h(yuǎn)都至少存在一個文件描述符被打開著以等待數(shù)據(jù)被寫入 FIFO,即進(jìn)程從中讀取數(shù)據(jù)的那個描述符。
使用 FIFO 和 tee 創(chuàng)建雙重管道線
shell 管道線的其中一個特征是它們是線性的,管道線中的每個進(jìn)程都能讀取前一個進(jìn)程產(chǎn)生的數(shù)據(jù)并將數(shù)據(jù)發(fā)送到其后一個進(jìn)程中,使用 FIFO 就能夠在管道線中創(chuàng)建子進(jìn)程,這樣除了將一個進(jìn)程的輸出發(fā)送給管道線中的后面一個進(jìn)程之外,還可以復(fù)制進(jìn)程的輸出并將數(shù)據(jù)發(fā)送到另一個進(jìn)程中,要完成這個任務(wù)就需要使用 tee 命令,它將其從標(biāo)準(zhǔn)輸入中讀取到的數(shù)據(jù)復(fù)制兩份并輸出:一份寫入標(biāo)準(zhǔn)輸出,另一份寫入到通過命令行參數(shù)指定的文件中。
mkfifo myfifo wc -l < myfifo & ls -l | tee myfifo | sort -k5n
非阻塞 IO
當(dāng)一個進(jìn)程打開一個 FIFO 的一端時,如果 FIFO 的另一端還沒有被打開,那么該進(jìn)程會被阻塞。但有些時候阻塞并不是期望的行為,而這可以通過在調(diào)用 open() 時指定 O_NONBLOCK 標(biāo)記來實(shí)現(xiàn)。
如果 FIFO 的另一端已經(jīng)被打開,那么 O_NONBLOCK 對 open() 調(diào)用不會產(chǎn)生任何影響,它會像往常一樣立即成功地打開 FIFO。只有當(dāng) FIFO 的另一端還沒有被打開的時候 O_NONBLOCK 標(biāo)記才會起作用,而具體產(chǎn)生的影響則依賴于打開 FIFO 是用于讀取還是用于寫入的:
如果打開 FIFO 是為了讀取,并且 FIFO 的寫入端當(dāng)前已經(jīng)被打開,那么 open() 調(diào)用會立即成功(就像 FIFO 的另一端已經(jīng)被打開一樣)
如果打開 FIFO 是為了寫入,并且還沒有打開 FIFO 的另一端來讀取數(shù)據(jù),那么 open() 調(diào)用會失敗,并將 errno 設(shè)置為 ENXIO
為讀取而打開 FIFO 和為寫入而打開 FIFO 時 O_NONBLOCK 標(biāo)記所起的作用不同是有原因的。當(dāng) FIFO 的另一個端沒有寫者時打開一個 FIFO 以便讀取數(shù)據(jù)是沒有問題的,因?yàn)槿魏卧噲D從 FIFO 讀取數(shù)據(jù)的操作都不會返回任何數(shù)據(jù)。但當(dāng)試圖向沒有讀者的 FIFO 中寫入數(shù)據(jù)時將會導(dǎo)致 SIGPIPE 信號的產(chǎn)生以及 write() 返回 EPIPE 錯誤。
在 FIFO 上調(diào)用 open() 的語義總結(jié)如下:
在打開一個 FIFO 時,使用 O_NOBLOCK 標(biāo)記存在兩個目的:
它允許單個進(jìn)程打開一個 FIFO 的兩端,這個進(jìn)程首先會在打開 FIFO 時指定 O_NOBLOCK 標(biāo)記以便讀取數(shù)據(jù),接著打開 FIFO 以便寫入數(shù)據(jù)
它防止打開兩個 FIFO 的進(jìn)程之間產(chǎn)生死鎖
例如,下面的情況將會發(fā)生死鎖:
非阻塞 read() 和 write()
O_NONBLOCK 標(biāo)記不僅會影響 open() 的語義,而且還會影響——因?yàn)樵诖蜷_的文件描述中這個標(biāo)記仍然被設(shè)置著——后續(xù)的 read() 和 write() 調(diào)用的語義。
有些時候需要修改一個已經(jīng)打開的 FIFO(或另一種類型的文件)的 O_NONBLOCK 標(biāo)記的狀態(tài),具體存在這個需求的場景包括以下幾種:
使用 O_NONBLOCK 打開了一個 FIFO 但需要后續(xù)的 read() 和 write() 在阻塞模式下運(yùn)行
需要啟用從 pipe() 返回的一個文件描述符的非阻塞模式。更一般地,可能需要更改從除 open() 調(diào)用之外的其他調(diào)用中,如每個由 shell 運(yùn)行的新程序中自動被打開的三個標(biāo)準(zhǔn)描述符的其中一個或 socket() 返回的文件描述符,取得的任意文件描述符的非阻塞狀態(tài)
出于一些應(yīng)用程序的特殊需求,需要切換一個文件描述符的 O_NONBLOCK 設(shè)置的開啟和關(guān)閉狀態(tài)
當(dāng)碰到上面的需求時可以使用 fcntl() 啟用或禁用打開著的文件的 O_NONBLOCK 狀態(tài)標(biāo)記。通過下面的代碼(忽略的錯誤檢查)可以啟用這個標(biāo)記:
int flags; flags = fcntl(fd, F_GETFL); flags != O_NONBLOCK; fcntl(fd, F_SETFL, flags);
通過下面的代碼可以禁用這個標(biāo)記:
flags = fcntl(fd, F_GETFL); flags &= ~O_NONBLOCK; fcntl(fd, F_SETFL, flags);
管道和 FIFO 中 read() 和 write() 的語義
FIFO 上的 read() 操作:
只有當(dāng)沒有數(shù)據(jù)并且寫入端沒有被打開時阻塞和非阻塞讀取之間才存在差別。在這種情況下,普通的 read() 會被阻塞,而非阻塞 read() 會失敗并返回 EAGAIN 錯誤。
當(dāng) O_NONBLOCK 標(biāo)記與 PIPE_BUF 限制共同起作用時 O_NONBLOCK 標(biāo)記對象管道或 FIFO 寫入數(shù)據(jù)的影響會變得復(fù)雜。
FIFO 上的 write() 操作:
當(dāng)數(shù)據(jù)無法立即被傳輸時 O_NONBLOCK 標(biāo)記會導(dǎo)致在一個管道或 FIFO 上的 write() 失敗(錯誤是 EAGAIN)。這意味著當(dāng)寫入了 PIPE_BUF 字節(jié)之后,如果在管道或 FIFO 中沒有足夠的空間了,那么 write() 會失敗,因?yàn)閮?nèi)核無法立即完成這個操作并且無法執(zhí)行部分寫入,否則就會破壞不超過 PIPE_BUF 字節(jié)的寫入操作的原子性的要求
當(dāng)一次寫入的數(shù)據(jù)量超過 PIPE_BUF 字節(jié)時,該寫入操作無需是原子的。因此,write() 會盡可能多地傳輸字節(jié)(部分寫)以充滿管道或 FIFO。在這種情況下,從 write() 返回的值是實(shí)際傳輸?shù)淖止?jié)數(shù),并且調(diào)用者隨后必須要進(jìn)行重試以寫入剩余的字節(jié)。但如果管道或 FIFO 已經(jīng)滿了,從而導(dǎo)致哪怕連一個字節(jié)都無法傳輸了,那么 write() 會失敗并返回 EAGAIN 錯誤
審核編輯:湯梓紅
-
Linux
+關(guān)注
關(guān)注
87文章
11292瀏覽量
209329 -
fifo
+關(guān)注
關(guān)注
3文章
387瀏覽量
43649 -
命令
+關(guān)注
關(guān)注
5文章
683瀏覽量
22011 -
管道
+關(guān)注
關(guān)注
3文章
145瀏覽量
17963 -
Shell
+關(guān)注
關(guān)注
1文章
365瀏覽量
23357
原文標(biāo)題:Linux管道和FIFO應(yīng)用筆記
文章出處:【微信號:strongerHuang,微信公眾號:strongerHuang】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論