概述
管道最常見的地方是shell中,比如:
$ ls | wc -l
為了執(zhí)行上面的命令,shell創(chuàng)建了兩個(gè)進(jìn)程來分別執(zhí)行 ls 和 wc (通過 fork() 和 exec() 完成),如下:

從上圖可以看出,可以將管道看成是一組水管,它允許數(shù)據(jù)從一個(gè)進(jìn)程流向另一個(gè)進(jìn)程,這也是管道名稱的由來。
從上圖可以看出,由兩個(gè)進(jìn)程連接到了管道上,這樣寫入進(jìn)程 ls 就將其標(biāo)準(zhǔn)輸出(文件描述符為1)連接到來管道的寫入段,讀取進(jìn)程 wc 就將其標(biāo)準(zhǔn)輸入(文件描述符為0)連接到管道的讀取端。實(shí)際上,這兩個(gè)進(jìn)程并不知道管道的存在,它們只是從標(biāo)準(zhǔn)文件描述符中讀取和寫入數(shù)據(jù)。shell 必須要完成相關(guān)的工作。
一個(gè)管道是一個(gè)字節(jié)流
管道是一個(gè)字節(jié)流,即在使用管道時(shí)是不存在消息或者消息邊界的概念的:
從管道中讀取數(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ù)報(bào) socket。
從管道中讀取數(shù)據(jù)
試圖從一個(gè)當(dāng)前為空的管道中讀取數(shù)據(jù)將會(huì)被阻塞直至至少有一個(gè)字節(jié)被寫入到管道中為止。
如果管道的寫入端被關(guān)閉了,那么從管道中讀取數(shù)據(jù)的進(jìn)程在讀完管道中剩余的所有數(shù)據(jù)之后將會(huì)看到文件結(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)上最好也避免依賴這種語(yǔ)義。作為替代方案,可以使用 UNIX domain 流 socket 對(duì)(通過 socketpair() 系統(tǒng)調(diào)用來創(chuàng)建),它提供了一種標(biāo)準(zhǔn)的雙向通信機(jī)制,并且其語(yǔ)義與流管道是等價(jià)的。
可以確保寫入不超過 PIPE_BUF 字節(jié)的操作是原子的
如果多個(gè)進(jìn)程寫入同一個(gè)管道,那么如果它們?cè)谝粋€(gè)時(shí)刻寫入的數(shù)據(jù)量不超過 PIPE_BUF 字節(jié),那么就可以確保寫入的數(shù)據(jù)不會(huì)發(fā)生相互混合的情況。
SUSv3 要求 PIPE_BUF 至少為 _POSIX_PIPE_BUF(512)。一個(gè)實(shí)現(xiàn)應(yīng)該定義 PIPE_BUF(在
當(dāng)寫入管道的數(shù)據(jù)塊的大小超過了 PIPE_BUF 字節(jié),那么內(nèi)核可能會(huì)將數(shù)據(jù)分割成幾個(gè)較小的片段來傳輸,在讀者從管道中消耗數(shù)據(jù)時(shí)再附加上后繼的數(shù)據(jù)(write()調(diào)用會(huì)阻塞直到所有數(shù)據(jù)被寫入到管道為止)
當(dāng)只有一個(gè)進(jìn)程向管道寫入數(shù)據(jù)時(shí)(通常的情況),PIPE_BUF 的取值就沒有關(guān)系了
但如果有多個(gè)寫入進(jìn)程,那么大數(shù)據(jù)塊的寫入可能會(huì)被分解成任意大小的段(可能會(huì)小于 PIPE_BUF 字節(jié)),并且可能會(huì)出現(xiàn)與其他進(jìn)程寫入的數(shù)據(jù)交叉的現(xiàn)象
只有在數(shù)據(jù)被傳輸?shù)焦艿赖臅r(shí)候 PIPE_BUF 限制才會(huì)起作用。當(dāng)寫入的數(shù)據(jù)達(dá)到 PIPE_BUF 字節(jié)時(shí),write() 會(huì)在必要的時(shí)候阻塞知道管道中的可用空間足以原子的完成此操作。如果寫入的數(shù)據(jù)大于 PIPE_BUF 字節(jié),那么 write() 會(huì)盡可能的多傳輸數(shù)據(jù)以充滿整個(gè)管道,然后阻塞直到一些讀取進(jìn)程從管道中移除了數(shù)據(jù)。如果此類阻塞的 write() 被一個(gè)信號(hào)處理器中斷了,那么這個(gè)調(diào)用會(huì)被解除阻塞并返回成功傳輸?shù)焦艿乐械淖止?jié)數(shù),這個(gè)字節(jié)數(shù)會(huì)少于請(qǐng)求寫入的字節(jié)數(shù)(所謂的部分寫入)。
管道的容量是有限的
管道其實(shí)是一個(gè)在內(nèi)核內(nèi)存中維護(hù)的緩沖器,這個(gè)緩沖器的存儲(chǔ)能力是有限的。一旦管道被填滿之后,后繼向管道的寫入操作就會(huì)被阻塞直到讀者從管道中移除了一些數(shù)據(jù)為止。
SUSv3 并沒有規(guī)定管道的存儲(chǔ)能力。在早于 2.6.11 的 Linux 內(nèi)核中,管道的存儲(chǔ)能力與系統(tǒng)頁(yè)面的大小是一致的(如在 x86-32 上是 4096 字節(jié)),而從 Linux 2.6.11 起,管道的存儲(chǔ)能力是 65,536 字節(jié)。其他 UNIX 實(shí)現(xiàn)上的管道的存儲(chǔ)能力可能是不同的。
一般來講,一個(gè)應(yīng)用程序無需知道管道的實(shí)際存儲(chǔ)能力。如果需要防止寫者進(jìn)程阻塞,那么從管道中讀取數(shù)據(jù)的進(jìn)程應(yīng)該被設(shè)計(jì)成以盡可能快的速度從管道中讀取數(shù)據(jù)。
創(chuàng)建和使用管道
#includeint pipe(int fd[2]);
pipe() 創(chuàng)建一個(gè)新管道
成功的調(diào)用在數(shù)組 fd 中返回兩個(gè)打開的文件描述符,一個(gè)表示管道的讀取端 fd[0],一個(gè)表示管道的寫入端 fd[1]
調(diào)用 pipe() 函數(shù)時(shí),首先在內(nèi)核中開辟一塊緩沖區(qū)用于通信,它有一個(gè)讀端和一個(gè)寫端,然后通過 fd 參數(shù)傳出給用戶進(jìn)程兩個(gè)文件描述符,fd[0] 指向管道的讀端,fd[1] 指向管道的寫段。
不要用 fd[0] 寫數(shù)據(jù),也不要用 fd[1] 讀數(shù)據(jù),其行為未定義的,但在有些系統(tǒng)上可能會(huì)返回 -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)用會(huì)讀取的數(shù)據(jù)量為所請(qǐng)求的字節(jié)數(shù)與管道中當(dāng)前存在的字節(jié)數(shù)兩者之間的較小值。當(dāng)管道為空時(shí),讀取操作阻塞。
也可以在管道上使用 stdio 函數(shù)(printf()、scanf() 等),只需要首先使用 fdopen() 獲取一個(gè)與 filedes 中的某個(gè)描述符對(duì)應(yīng)的文件流即可。但在這樣做的時(shí)候需要解決 stdio 緩沖問題。
管道可以用于進(jìn)程內(nèi)部自己通信:

管道可以用于親緣關(guān)系(子進(jìn)程會(huì)繼承父進(jìn)程中的文件描述符的副本)進(jìn)程中通信:

不建議將單個(gè) pipe 用作全雙工的,或者不關(guān)閉用作半雙工而不關(guān)閉相應(yīng)的讀端/寫端,這樣很可能導(dǎo)致死鎖:如果兩個(gè)進(jìn)程同時(shí)試圖從管道中讀取數(shù)據(jù),那么就無法確定哪個(gè)進(jìn)程會(huì)首先讀取成功,從而產(chǎn)生兩個(gè)進(jìn)程競(jìng)爭(zhēng)數(shù)據(jù)了。要防止這種競(jìng)爭(zhēng)情況的出現(xiàn)就需要使用某種同步機(jī)制。這時(shí),就需要考慮死鎖問題了,因?yàn)槿绻麅蓚€(gè)進(jìn)程都試圖從空管道中讀取數(shù)據(jù)或者嘗試向已滿的管道中寫入數(shù)據(jù)就可能會(huì)發(fā)生死鎖。
如果我們想要一個(gè)雙向數(shù)據(jù)流時(shí),可以創(chuàng)建兩個(gè)管道,每個(gè)方向一個(gè)。
管道允許相關(guān)進(jìn)程間的通信
其實(shí)管道可以用于任意兩個(gè)甚至更多相關(guān)進(jìn)程之間的通信,只要在創(chuàng)建子進(jìn)程的系列 fork() 調(diào)用之前通過一個(gè)共同的祖先進(jìn)程創(chuàng)建管道即可。
關(guān)閉未使用管道文件描述符
關(guān)閉未使用管道文件描述符不僅僅是為了確保進(jìn)程不會(huì)消耗盡其文件描述符的限制。
從管道中讀取數(shù)據(jù)的進(jìn)程會(huì)關(guān)閉其持有的管道的寫入描述符,這樣當(dāng)其他進(jìn)程完成輸出并關(guān)閉其寫入描述符之后,讀者就能夠看到文件結(jié)束。反之,如果讀取的進(jìn)程沒有關(guān)閉管道的寫入端,那么在其他進(jìn)程關(guān)閉了寫入描述符之后,即使讀者已經(jīng)讀完了管道中的所有數(shù)據(jù),也不會(huì)看到文件結(jié)束。因?yàn)榇藭r(shí)內(nèi)核知道至少還有一個(gè)管道的寫入描述符打開著,從而導(dǎo)致 read() 阻塞。
當(dāng)一個(gè)進(jìn)程視圖向一個(gè)管道中寫入數(shù)據(jù)但沒有任何進(jìn)程擁有該管道的打開著的讀取描述符時(shí),內(nèi)核會(huì)向?qū)懭脒M(jìn)程發(fā)送一個(gè) SIGPIPE 信號(hào),默認(rèn)情況下,這個(gè)信號(hào)將會(huì)殺死進(jìn)程,但進(jìn)程可以選擇忽略或者設(shè)置信號(hào)處理器,這樣 write() 將因?yàn)?EPIPE 錯(cuò)誤而失敗。收到 SIGPIPE 信號(hào)和得到 EPIPE 錯(cuò)誤對(duì)于標(biāo)識(shí)管道的狀態(tài)是有意義的,這就是為什么需要關(guān)閉管道的未使用讀取描述符的原因。如果寫入進(jìn)程沒有關(guān)閉管道的讀取端,那么即使在其他進(jìn)程已經(jīng)關(guān)閉了管道的讀取端之后,寫入進(jìn)程仍然能夠向管道寫入數(shù)據(jù),最后寫入進(jìn)程會(huì)將數(shù)據(jù)充滿整個(gè)管道,后續(xù)的寫入請(qǐng)求會(huì)將永遠(yuǎn)阻塞。
使用管道連接過濾器
當(dāng)管道被創(chuàng)建之后,為管道的兩端分配的文件描述符是可用描述符中數(shù)值最小的兩個(gè),由于通常情況下,進(jìn)程已經(jīng)使用了描述符 0,1,2,因此會(huì)為管道分配一些數(shù)值更大的描述符。如果需要使用管道連接兩個(gè)過濾器(即從 stdin 讀取和寫入到 stdout),使得一個(gè)程序的標(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)輸出被綁定到管道的寫入端,而對(duì)應(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)建了一個(gè)管道,然后創(chuàng)建了一個(gè)子進(jìn)程來執(zhí)行 shell,而 shell 又創(chuàng)建了一個(gè)子進(jìn)程來執(zhí)行command字符串
mode 參數(shù)是一個(gè)字符串:
它確定調(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() 在成功時(shí)會(huì)返回可供 stdio 庫(kù)函數(shù)使用的文件流指針。當(dāng)發(fā)生錯(cuò)誤時(shí),popen() 會(huì)返回 NULL 并設(shè)置 errno 以標(biāo)示出發(fā)生錯(cuò)誤的原因
在 popen() 調(diào)用之后,調(diào)用進(jìn)程使用管道來讀取 command 的輸出或使用管道向其發(fā)送輸入。與使用 pipe() 創(chuàng)建的管道一樣,當(dāng)從管道中讀取數(shù)據(jù)時(shí),調(diào)用進(jìn)程在 command 關(guān)閉管道的寫入端之后會(huì)看到文件結(jié)束;當(dāng)向管道寫入數(shù)據(jù)時(shí),如果 command 已經(jīng)關(guān)閉了管道的讀取端,那么調(diào)用進(jìn)程就會(huì)收到 SIGPIPE 信號(hào)并得到 EPIPE 錯(cuò)誤
#includeint pclose ( FILE * stream);
一旦IO結(jié)束之后可以使用 pclose() 函數(shù)關(guān)閉管道并等待子進(jìn)程中的 shell 終止(不應(yīng)該使用 fclose() 函數(shù),因?yàn)樗粫?huì)等待子進(jìn)程。)
pclose() 在成功時(shí)會(huì)返回子進(jìn)程中 shell 的終止?fàn)顟B(tài)(即 shell 所執(zhí)行的最后一條命令的終止?fàn)顟B(tài),除非 shell 是被信號(hào)殺死的)
和 system() 一樣,如果無法執(zhí)行shell,那么 pclose() 會(huì)返回一個(gè)值就像子進(jìn)程中的 shell 通過調(diào)用 _exit(127) 來終止一樣
如果發(fā)生了其他錯(cuò)誤,那么 pclose() 返回 ?1。其中可能發(fā)生的一個(gè)錯(cuò)誤是無法取得終止?fàn)顟B(tài)
當(dāng)執(zhí)行等待以獲取子進(jìn)程中 shell 的狀態(tài)時(shí),SUSv3 要求 pclose() 與 system() 一樣,即在內(nèi)部的 waitpid() 調(diào)用被一個(gè)信號(hào)處理器中斷之后自動(dòng)重啟該調(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 來完成的。所以在啟動(dòng) command 命令之前程序先啟動(dòng) shell 來分析 command 字符串,就可以使用各種 shell 擴(kuò)展(比如通配符),這樣我們可以通過 popen() 調(diào)用非常復(fù)雜的 shell 命令
缺點(diǎn):對(duì)于每個(gè) popen() 調(diào)用,不僅要啟動(dòng)一個(gè)被請(qǐng)求的程序,還需要啟動(dòng)一個(gè) shell。即每一個(gè) popen() 將啟動(dòng)兩個(gè)進(jìn)程。從效率和資源的角度看,popen() 函數(shù)的調(diào)用比正常方式要慢一些
pipe() VS popen()
pipe()是一個(gè)底層調(diào)用,popen() 是一個(gè)高級(jí)的函數(shù)
pipe() 單純的創(chuàng)建管道,而 popen() 創(chuàng)建管道的同時(shí) fork() 子進(jìn)程
popen() 在兩個(gè)進(jìn)程中傳遞數(shù)據(jù)時(shí)需要調(diào)用 shell 來解釋請(qǐng)求命令;pipe() 在兩個(gè)進(jìn)程中傳遞數(shù)據(jù)不需要啟動(dòng) shell 來解釋請(qǐng)求命令,同時(shí)提供了對(duì)讀寫數(shù)據(jù)的更多控制(popen() 必須時(shí) shell 命令,pipe() 則無硬性要求)
popen() 函數(shù)是基于文件流(FILE)工作的,而 pipe() 是基于文件描述符工作的,所以在使用 pipe() 后,數(shù)據(jù)必須要用底層的read() 和 write() 調(diào)用來讀取和發(fā)送
管道和 stdio 緩沖
由于 popen() 調(diào)用返回的文件流指針沒有引用一個(gè)終端,因此 stdio 庫(kù)會(huì)對(duì)這種流應(yīng)用塊緩沖。這意味著當(dāng) mode 的值為 w 來調(diào)用 popen() 時(shí),默認(rèn)情況下只有當(dāng) stdio 緩沖區(qū)被充滿或者使用 pclose() 關(guān)閉了管道之后才會(huì)被發(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() 獲取一個(gè)與管道的寫入端對(duì)應(yīng)的 stdio 流時(shí)也可以使用這項(xiàng)技術(shù)
如果調(diào)用 popen() 的進(jìn)程正在從管道中讀取數(shù)據(jù)(即 mode 是 r),那么事情就不是那么簡(jiǎn)單了。在這樣情況下如果子進(jìn)程正在使用 stdio 庫(kù),那么——除非它顯式地調(diào)用了 fflush() 或 setbuf() ,其輸出只有在子進(jìn)程填滿 stdio 緩沖器或調(diào)用了 fclose() 之后才會(huì)對(duì)調(diào)用進(jìn)程可用。(如果正在從使用 pipe() 創(chuàng)建的管道中讀取數(shù)據(jù)并且向另一端寫入數(shù)據(jù)的進(jìn)程正在使用 stdio 庫(kù),那么同樣的規(guī)則也是適用的。)如果這是一個(gè)問題,那么能采取的措施就比較有限的,除非能夠修改在子進(jìn)程中運(yùn)行的程序的源代碼使之包含對(duì) setbuf() 或 fflush() 調(diào)用。
如果無法修改源代碼,那么可以使用偽終端來替換管道。一個(gè)偽終端是一個(gè) IPC 通道,對(duì)進(jìn)程來講它就像是一個(gè)終端。其結(jié)果是 stdio 庫(kù)會(huì)逐行輸出緩沖器中的數(shù)據(jù)。
命名管道(FIFO)
上述管道雖然實(shí)現(xiàn)了進(jìn)程間通信,但是它具有一定的局限性:
匿名管道只能是具有血緣關(guān)系的進(jìn)程之間通信
它只能實(shí)現(xiàn)一個(gè)進(jìn)程寫另一個(gè)進(jìn)程讀,而如果需要兩者同時(shí)進(jìn)行時(shí),就得重新打開一個(gè)管道
為了使任意兩個(gè)進(jìn)程之間能夠通信,就提出了命名管道(named pipe 或 FIFO):
FIFO 與管道的區(qū)別:FIFO 在文件系統(tǒng)中擁有一個(gè)名稱,并且其打開方式與打開一個(gè)普通文件一樣,能夠?qū)崿F(xiàn)任何兩個(gè)進(jìn)程之間通信。而匿名管道對(duì)于文件系統(tǒng)是不可見的,它僅限于在父子進(jìn)程之間的通信
一旦打開了 FIFO,就能在它上面使用與操作管道和其他文件的系統(tǒng)調(diào)用一樣的 IO 系統(tǒng)調(diào)用 read(),write(),close()。與管道一樣,F(xiàn)IFO 也有一個(gè)寫入端和讀取端,并且總是遵循先進(jìn)先出的原則,即第一個(gè)進(jìn)來的數(shù)據(jù)會(huì)第一個(gè)被讀走
與管道一樣,當(dāng)所有引用 FIFO 的描述符都關(guān)閉之后,所有未被讀取的數(shù)據(jù)都將被丟棄
使用 mkfifo 命令可以在 shell 中創(chuàng)建一個(gè) FIFO:
mkfifo [-m mode] pathname
pathname 是創(chuàng)建的 FIFO 的名稱,-m 選項(xiàng)指定權(quán)限 mode,其工作方式與 chmod 命令一樣
fstat() 和 stat() 函數(shù)會(huì)在 stat 結(jié)構(gòu)的 st_mode 字段返回 S_IFIFO,使用 ls -l 列出文件時(shí),F(xiàn)IFO 文件在第一列的類型為 p,ls -F 會(huì)在 FIFO 路徑名后面附加管道符 |
#include#include int mkfifo(const char *pathname,mode_t mode);
mode 參數(shù)指定了新 FIFO 的權(quán)限,這些權(quán)限會(huì)按照進(jìn)程的 umask 值來取掩碼
一旦創(chuàng)建了 FIFO,任何進(jìn)程都能夠打開它,只要它通過常規(guī)的文件權(quán)限檢測(cè)
使用 FIFO 時(shí)唯一明智的做法是在兩端分別設(shè)置一個(gè)讀取進(jìn)程和一個(gè)寫入進(jìn)程。這樣在默認(rèn)情況下,打開一個(gè) FIFO 以便讀取數(shù)據(jù)(open() O_RDONLY 標(biāo)記)將會(huì)阻塞直到另一個(gè)進(jìn)程打開 FIFO 以寫入數(shù)(open() O_WRONLY 標(biāo)記)為止。相應(yīng)地,打開一個(gè) FIFO 以寫入數(shù)據(jù)將會(huì)阻塞直到另一個(gè)進(jìn)程打開 FIFO 以讀取數(shù)據(jù)為止。換句話說,打開一個(gè) FIFO 會(huì)同步讀取進(jìn)程和寫入進(jìn)程。如果一個(gè) FIFO 的另一端已經(jīng)打開(可能是因?yàn)橐粚?duì)進(jìn)程已經(jīng)打開了 FIFO 的兩端),那么open() 調(diào)用會(huì)立即成功。
在大多數(shù) Unix 實(shí)現(xiàn)上(包含 Linux),當(dāng)打開一個(gè) FIFO 時(shí)可以通過指定 O_RDWR 標(biāo)記來繞過打開 FIFO 時(shí)的阻塞行為。這樣,open() 會(huì)立即返回,但無法使用返回的文件描述符在 FIFO 上讀取和寫入數(shù)據(jù)。這種做法破壞了 FIFO 的 IO 模型,SUSv3 明確指出以 O_RDWR 標(biāo)記打開一個(gè) FIFO 的結(jié)果是未知的,因此出于可移植性的原因,開發(fā)人員不應(yīng)該使用這項(xiàng)技術(shù)。對(duì)于那些需要避免在打開 FIFO 時(shí)發(fā)生阻塞的需求,open() 的 O_NONBLOCK 標(biāo)記提供了一種標(biāo)準(zhǔn)化的方法來完成這個(gè)任務(wù):
open(const char *path, O_RDONLY | O_NONBLOCK); open(const char *path, O_WRONLY | O_NONBLOCK);
在打開一個(gè) FIFO 時(shí)避免使用 O_RDWR 標(biāo)記還有另外一個(gè)原因,當(dāng)采用那種方式調(diào)用 open() 之后,調(diào)用進(jìn)程在從返回的文件描述符中讀取數(shù)據(jù)時(shí)永遠(yuǎn)都不會(huì)看到文件結(jié)束,因?yàn)橛肋h(yuǎn)都至少存在一個(gè)文件描述符被打開著以等待數(shù)據(jù)被寫入 FIFO,即進(jìn)程從中讀取數(shù)據(jù)的那個(gè)描述符。
使用 FIFO 和 tee 創(chuàng)建雙重管道線
shell 管道線的其中一個(gè)特征是它們是線性的,管道線中的每個(gè)進(jìn)程都能讀取前一個(gè)進(jìn)程產(chǎn)生的數(shù)據(jù)并將數(shù)據(jù)發(fā)送到其后一個(gè)進(jìn)程中,使用 FIFO 就能夠在管道線中創(chuàng)建子進(jìn)程,這樣除了將一個(gè)進(jìn)程的輸出發(fā)送給管道線中的后面一個(gè)進(jìn)程之外,還可以復(fù)制進(jìn)程的輸出并將數(shù)據(jù)發(fā)送到另一個(gè)進(jìn)程中,要完成這個(gè)任務(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)一個(gè)進(jìn)程打開一個(gè) FIFO 的一端時(shí),如果 FIFO 的另一端還沒有被打開,那么該進(jìn)程會(huì)被阻塞。但有些時(shí)候阻塞并不是期望的行為,而這可以通過在調(diào)用 open() 時(shí)指定 O_NONBLOCK 標(biāo)記來實(shí)現(xiàn)。
如果 FIFO 的另一端已經(jīng)被打開,那么 O_NONBLOCK 對(duì) open() 調(diào)用不會(huì)產(chǎn)生任何影響,它會(huì)像往常一樣立即成功地打開 FIFO。只有當(dāng) FIFO 的另一端還沒有被打開的時(shí)候 O_NONBLOCK 標(biāo)記才會(huì)起作用,而具體產(chǎn)生的影響則依賴于打開 FIFO 是用于讀取還是用于寫入的:
如果打開 FIFO 是為了讀取,并且 FIFO 的寫入端當(dāng)前已經(jīng)被打開,那么 open() 調(diào)用會(huì)立即成功(就像 FIFO 的另一端已經(jīng)被打開一樣)
如果打開 FIFO 是為了寫入,并且還沒有打開 FIFO 的另一端來讀取數(shù)據(jù),那么 open() 調(diào)用會(huì)失敗,并將 errno 設(shè)置為 ENXIO
為讀取而打開 FIFO 和為寫入而打開 FIFO 時(shí) O_NONBLOCK 標(biāo)記所起的作用不同是有原因的。當(dāng) FIFO 的另一個(gè)端沒有寫者時(shí)打開一個(gè) FIFO 以便讀取數(shù)據(jù)是沒有問題的,因?yàn)槿魏卧噲D從 FIFO 讀取數(shù)據(jù)的操作都不會(huì)返回任何數(shù)據(jù)。但當(dāng)試圖向沒有讀者的 FIFO 中寫入數(shù)據(jù)時(shí)將會(huì)導(dǎo)致 SIGPIPE 信號(hào)的產(chǎn)生以及 write() 返回 EPIPE 錯(cuò)誤。
在 FIFO 上調(diào)用 open() 的語(yǔ)義總結(jié)如下:

在打開一個(gè) FIFO 時(shí),使用 O_NOBLOCK 標(biāo)記存在兩個(gè)目的:
它允許單個(gè)進(jìn)程打開一個(gè) FIFO 的兩端,這個(gè)進(jìn)程首先會(huì)在打開 FIFO 時(shí)指定 O_NOBLOCK 標(biāo)記以便讀取數(shù)據(jù),接著打開 FIFO 以便寫入數(shù)據(jù)
它防止打開兩個(gè) FIFO 的進(jìn)程之間產(chǎn)生死鎖
例如,下面的情況將會(huì)發(fā)生死鎖:

非阻塞 read() 和 write()
O_NONBLOCK 標(biāo)記不僅會(huì)影響 open() 的語(yǔ)義,而且還會(huì)影響——因?yàn)樵诖蜷_的文件描述中這個(gè)標(biāo)記仍然被設(shè)置著——后續(xù)的 read() 和 write() 調(diào)用的語(yǔ)義。
有些時(shí)候需要修改一個(gè)已經(jīng)打開的 FIFO(或另一種類型的文件)的 O_NONBLOCK 標(biāo)記的狀態(tài),具體存在這個(gè)需求的場(chǎng)景包括以下幾種:
使用 O_NONBLOCK 打開了一個(gè) FIFO 但需要后續(xù)的 read() 和 write() 在阻塞模式下運(yùn)行
需要啟用從 pipe() 返回的一個(gè)文件描述符的非阻塞模式。更一般地,可能需要更改從除 open() 調(diào)用之外的其他調(diào)用中,如每個(gè)由 shell 運(yùn)行的新程序中自動(dòng)被打開的三個(gè)標(biāo)準(zhǔn)描述符的其中一個(gè)或 socket() 返回的文件描述符,取得的任意文件描述符的非阻塞狀態(tài)
出于一些應(yīng)用程序的特殊需求,需要切換一個(gè)文件描述符的 O_NONBLOCK 設(shè)置的開啟和關(guān)閉狀態(tài)
當(dāng)碰到上面的需求時(shí)可以使用 fcntl() 啟用或禁用打開著的文件的 O_NONBLOCK 狀態(tài)標(biāo)記。通過下面的代碼(忽略的錯(cuò)誤檢查)可以啟用這個(gè)標(biāo)記:
int flags; flags = fcntl(fd, F_GETFL); flags != O_NONBLOCK; fcntl(fd, F_SETFL, flags);
通過下面的代碼可以禁用這個(gè)標(biāo)記:
flags = fcntl(fd, F_GETFL); flags &= ~O_NONBLOCK; fcntl(fd, F_SETFL, flags);
管道和 FIFO 中 read() 和 write() 的語(yǔ)義
FIFO 上的 read() 操作:

只有當(dāng)沒有數(shù)據(jù)并且寫入端沒有被打開時(shí)阻塞和非阻塞讀取之間才存在差別。在這種情況下,普通的 read() 會(huì)被阻塞,而非阻塞 read() 會(huì)失敗并返回 EAGAIN 錯(cuò)誤。
當(dāng) O_NONBLOCK 標(biāo)記與 PIPE_BUF 限制共同起作用時(shí) O_NONBLOCK 標(biāo)記對(duì)象管道或 FIFO 寫入數(shù)據(jù)的影響會(huì)變得復(fù)雜。
FIFO 上的 write() 操作:

當(dāng)數(shù)據(jù)無法立即被傳輸時(shí) O_NONBLOCK 標(biāo)記會(huì)導(dǎo)致在一個(gè)管道或 FIFO 上的 write() 失敗(錯(cuò)誤是 EAGAIN)。這意味著當(dāng)寫入了 PIPE_BUF 字節(jié)之后,如果在管道或 FIFO 中沒有足夠的空間了,那么 write() 會(huì)失敗,因?yàn)閮?nèi)核無法立即完成這個(gè)操作并且無法執(zhí)行部分寫入,否則就會(huì)破壞不超過 PIPE_BUF 字節(jié)的寫入操作的原子性的要求
當(dāng)一次寫入的數(shù)據(jù)量超過 PIPE_BUF 字節(jié)時(shí),該寫入操作無需是原子的。因此,write() 會(huì)盡可能多地傳輸字節(jié)(部分寫)以充滿管道或 FIFO。在這種情況下,從 write() 返回的值是實(shí)際傳輸?shù)淖止?jié)數(shù),并且調(diào)用者隨后必須要進(jìn)行重試以寫入剩余的字節(jié)。但如果管道或 FIFO 已經(jīng)滿了,從而導(dǎo)致哪怕連一個(gè)字節(jié)都無法傳輸了,那么 write() 會(huì)失敗并返回 EAGAIN 錯(cuò)誤
審核編輯:湯梓紅
-
Linux
+關(guān)注
關(guān)注
88文章
11682瀏覽量
218577 -
fifo
+關(guān)注
關(guān)注
3文章
407瀏覽量
45606 -
命令
+關(guān)注
關(guān)注
5文章
747瀏覽量
23558 -
管道
+關(guān)注
關(guān)注
3文章
148瀏覽量
18356 -
Shell
+關(guān)注
關(guān)注
1文章
374瀏覽量
25281
原文標(biāo)題:Linux管道和FIFO應(yīng)用筆記
文章出處:【微信號(hào):strongerHuang,微信公眾號(hào):strongerHuang】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
Linux管道和FIFO應(yīng)用筆記
評(píng)論