共計(jì) 7484 個(gè)字符,預(yù)計(jì)需要花費(fèi) 19 分鐘才能閱讀完成。
本文丸趣 TV 小編為大家詳細(xì)介紹“l(fā)inux 管道是什么及怎么使用”,內(nèi)容詳細(xì),步驟清晰,細(xì)節(jié)處理妥當(dāng),希望這篇“l(fā)inux 管道是什么及怎么使用”文章能幫助大家解決疑惑,下面跟著丸趣 TV 小編的思路慢慢深入,一起來(lái)學(xué)習(xí)新知識(shí)吧。
管道是 Linux 進(jìn)程間的一種通信方式,兩個(gè)進(jìn)程可以通過(guò)一個(gè)共享內(nèi)存區(qū)域來(lái)傳遞信息,并且管道中的數(shù)據(jù)只能是單向流動(dòng)的,也就是說(shuō)只能有固定的寫(xiě)進(jìn)程和讀進(jìn)程。目前在任何一個(gè) shell 中,都可以使用“|”連接兩個(gè)命令,shell 會(huì)將前后兩個(gè)進(jìn)程的輸入輸出用一個(gè)管道相連,以便達(dá)到進(jìn)程間通信的目的。
什么是管道?
管道,英文為 pipe。管道是 Linux 進(jìn)程間的一種通信方式,兩個(gè)進(jìn)程可以通過(guò)一個(gè)共享內(nèi)存區(qū)域來(lái)傳遞信息,并且管道中的數(shù)據(jù)只能是單向流動(dòng)的,也就是說(shuō)只能有固定的寫(xiě)進(jìn)程和讀進(jìn)程。
管道的發(fā)明人是道格拉斯. 麥克羅伊,這位也是 UNIX 上早期 shell 的發(fā)明人。他在發(fā)明了 shell 之后,發(fā)現(xiàn)系統(tǒng)操作執(zhí)行命令的時(shí)候,經(jīng)常有需求要將一個(gè)程序的輸出交給另一個(gè)程序進(jìn)行處理,這種操作可以使用輸入輸出重定向加文件搞定,比如:
[zorro@zorro-pc pipe]$ ls -l /etc/ etc.txt
[zorro@zorro-pc pipe]$ wc -l etc.txt
183 etc.txt
但是這樣未免顯得太麻煩了。所以,管道的概念應(yīng)運(yùn)而生。目前在任何一個(gè) shell 中,都可以使用“|”連接兩個(gè)命令,shell 會(huì)將前后兩個(gè)進(jìn)程的輸入輸出用一個(gè)管道相連,以便達(dá)到進(jìn)程間通信的目的:
[zorro@zorro-pc pipe]$ ls -l /etc/ | wc -l
183
對(duì)比以上兩種方法,我們也可以理解為,管道本質(zhì)上就是一個(gè)文件,前面的進(jìn)程以寫(xiě)方式打開(kāi)文件,后面的進(jìn)程以讀方式打開(kāi)。這樣前面寫(xiě)完后面讀,于是就實(shí)現(xiàn)了通信。實(shí)際上管道的設(shè)計(jì)也是遵循 UNIX 的“一切皆文件”設(shè)計(jì)原則的,它本質(zhì)上就是一個(gè)文件。Linux 系統(tǒng)直接把管道實(shí)現(xiàn)成了一種文件系統(tǒng),借助 VFS 給應(yīng)用程序提供操作接口。
雖然實(shí)現(xiàn)形態(tài)上是文件,但是管道本身并不占用磁盤(pán)或者其他外部存儲(chǔ)的空間。在 Linux 的實(shí)現(xiàn)上,它占用的是內(nèi)存空間。所以,Linux 上的管道就是一個(gè)操作方式為文件的內(nèi)存緩沖區(qū)。
管道的分類(lèi)和使用
Linux 上的管道分兩種類(lèi)型:
匿名管道
命名管道
這兩種管道也叫做有名或無(wú)名管道。匿名管道最常見(jiàn)的形態(tài)就是我們?cè)?shell 操作中最常用的”|”。它的特點(diǎn)是只能在父子進(jìn)程中使用,父進(jìn)程在產(chǎn)生子進(jìn)程前必須打開(kāi)一個(gè)管道文件,然后 fork 產(chǎn)生子進(jìn)程,這樣子進(jìn)程通過(guò)拷貝父進(jìn)程的進(jìn)程地址空間獲得同一個(gè)管道文件的描述符,以達(dá)到使用同一個(gè)管道通信的目的。此時(shí)除了父子進(jìn)程外,沒(méi)人知道這個(gè)管道文件的描述符,所以通過(guò)這個(gè)管道中的信息無(wú)法傳遞給其他進(jìn)程。這保證了傳輸數(shù)據(jù)的安全性,當(dāng)然也降低了管道了通用性,于是系統(tǒng)還提供了命名管道。
我們可以使用 mkfifo 或 mknod 命令來(lái)創(chuàng)建一個(gè)命名管道,這跟創(chuàng)建一個(gè)文件沒(méi)有什么區(qū)別:
[zorro@zorro-pc pipe]$ mkfifo pipe
[zorro@zorro-pc pipe]$ ls -l pipe
prw-r--r-- 1 zorro zorro 0 Jul 14 10:44 pipe
可以看到創(chuàng)建出來(lái)的文件類(lèi)型比較特殊,是 p 類(lèi)型。表示這是一個(gè)管道文件。有了這個(gè)管道文件,系統(tǒng)中就有了對(duì)一個(gè)管道的全局名稱(chēng),于是任何兩個(gè)不相關(guān)的進(jìn)程都可以通過(guò)這個(gè)管道文件進(jìn)行通信了。比如我們現(xiàn)在讓一個(gè)進(jìn)程寫(xiě)這個(gè)管道文件:
[zorro@zorro-pc pipe]$ echo xxxxxxxxxxxxxx pipe
此時(shí)這個(gè)寫(xiě)操作會(huì)阻塞,因?yàn)楣艿懒硪欢藳](méi)有人讀。這是內(nèi)核對(duì)管道文件定義的默認(rèn)行為。此時(shí)如果有進(jìn)程讀這個(gè)管道,那么這個(gè)寫(xiě)操作的阻塞才會(huì)解除:
[zorro@zorro-pc pipe]$ cat pipe
xxxxxxxxxxxxxx
大家可以觀察到,當(dāng)我們 cat 完這個(gè)文件之后,另一端的 echo 命令也返回了。這就是命名管道。
Linux 系統(tǒng)無(wú)論對(duì)于命名管道和匿名管道,底層都用的是同一種文件系統(tǒng)的操作行為,這種文件系統(tǒng)叫 pipefs。大家可以在 /etc/proc/filesystems 文件中找到你的系統(tǒng)是不是支持這種文件系統(tǒng):
[zorro@zorro-pc pipe]$ cat /proc/filesystems |grep pipefs
nodev pipefs
觀察完了如何在命令行中使用管道之后,我們?cè)賮?lái)看看如何在系統(tǒng)編程中使用管道。
PIPE
我們可以把匿名管道和命名管道分別叫做 PIPE 和 FIFO。這主要因?yàn)樵谙到y(tǒng)編程中,創(chuàng)建匿名管道的系統(tǒng)調(diào)用是 pipe(),而創(chuàng)建命名管道的函數(shù)是 mkfifo()。使用 mknod()系統(tǒng)調(diào)用并指定文件類(lèi)型為為 S_IFIFO 也可以創(chuàng)建一個(gè) FIFO。
使用 pipe()系統(tǒng)調(diào)用可以創(chuàng)建一個(gè)匿名管道,這個(gè)系統(tǒng)調(diào)用的原型為:
#include unistd.h
int pipe(int pipefd[2]);
這個(gè)方法將會(huì)創(chuàng)建出兩個(gè)文件描述符,可以使用 pipefd 這個(gè)數(shù)組來(lái)引用這兩個(gè)描述符進(jìn)行文件操作。pipefd[0]是讀方式打開(kāi),作為管道的讀描述符。pipefd[1]是寫(xiě)方式打開(kāi),作為管道的寫(xiě)描述符。從管道寫(xiě)端寫(xiě)入的數(shù)據(jù)會(huì)被內(nèi)核緩存直到有人從另一端讀取為止。我們來(lái)看一下如何在一個(gè)進(jìn)程中使用管道,雖然這個(gè)例子并沒(méi)有什么意義:
[zorro@zorro-pc pipe]$ cat pipe.c
#include stdlib.h
#include stdio.h
#include unistd.h
#include string.h
#define STRING hello world!
int main()
int pipefd[2];
char buf[BUFSIZ];
if (pipe(pipefd) == -1) { perror( pipe()
exit(1);
}
if (write(pipefd[1], STRING, strlen(STRING)) 0) { perror( write()
exit(1);
}
if (read(pipefd[0], buf, BUFSIZ) 0) { perror( write()
exit(1);
}
printf(%s\n , buf);
exit(0);
}
這個(gè)程序創(chuàng)建了一個(gè)管道,并且對(duì)管道寫(xiě)了一個(gè)字符串之后從管道讀取,并打印在標(biāo)準(zhǔn)輸出上。用一個(gè)圖來(lái)說(shuō)明這個(gè)程序的狀態(tài)就是這樣的:
一個(gè)進(jìn)程自己給自己發(fā)送消息這當(dāng)然不叫進(jìn)程間通信,所以實(shí)際情況中我們不會(huì)在單個(gè)進(jìn)程中使用管道。進(jìn)程在 pipe 創(chuàng)建完管道之后,往往都要 fork 產(chǎn)生子進(jìn)程,成為如下圖表示的樣子:
如圖中描述,fork 產(chǎn)生的子進(jìn)程會(huì)繼承父進(jìn)程對(duì)應(yīng)的文件描述符。利用這個(gè)特性,父進(jìn)程先 pipe 創(chuàng)建管道之后,子進(jìn)程也會(huì)得到同一個(gè)管道的讀寫(xiě)文件描述符。從而實(shí)現(xiàn)了父子兩個(gè)進(jìn)程使用一個(gè)管道可以完成半雙工通信。此時(shí),父進(jìn)程可以通過(guò) fd[1]給子進(jìn)程發(fā)消息,子進(jìn)程通過(guò) fd[0]讀。子進(jìn)程也可以通過(guò) fd[1]給父進(jìn)程發(fā)消息,父進(jìn)程用 fd[0]讀。程序?qū)嵗缦拢?/p>
[zorro@zorro-pc pipe]$ cat pipe_parent_child.c
#include stdlib.h
#include stdio.h
#include unistd.h
#include string.h
#include sys/types.h
#include sys/wait.h
#define STRING hello world!
int main()
int pipefd[2];
pid_t pid;
char buf[BUFSIZ];
if (pipe(pipefd) == -1) { perror( pipe()
exit(1);
}
pid = fork();
if (pid == -1) { perror( fork()
exit(1);
}
if (pid == 0) {
/* this is child. */
printf(Child pid is: %d\n , getpid());
if (read(pipefd[0], buf, BUFSIZ) 0) { perror( write()
exit(1);
}
printf(%s\n , buf);
bzero(buf, BUFSIZ);
snprintf(buf, BUFSIZ, Message from child: My pid is: %d , getpid());
if (write(pipefd[1], buf, strlen(buf)) 0) { perror( write()
exit(1);
}
} else {
/* this is parent */
printf(Parent pid is: %d\n , getpid());
snprintf(buf, BUFSIZ, Message from parent: My pid is: %d , getpid());
if (write(pipefd[1], buf, strlen(buf)) 0) { perror( write()
exit(1);
}
sleep(1);
bzero(buf, BUFSIZ);
if (read(pipefd[0], buf, BUFSIZ) 0) { perror( write()
exit(1);
}
printf(%s\n , buf);
wait(NULL);
}
exit(0);
}
父進(jìn)程先給子進(jìn)程發(fā)一個(gè)消息,子進(jìn)程接收到之后打印消息,之后再給父進(jìn)程發(fā)消息,父進(jìn)程再打印從子進(jìn)程接收到的消息。程序執(zhí)行效果:
[zorro@zorro-pc pipe]$ ./pipe_parent_child
Parent pid is: 8309
Child pid is: 8310
Message from parent: My pid is: 8309
Message from child: My pid is: 8310
從這個(gè)程序中我們可以看到,管道實(shí)際上可以實(shí)現(xiàn)一個(gè)半雙工通信的機(jī)制。使用同一個(gè)管道的父子進(jìn)程可以分時(shí)給對(duì)方發(fā)送消息。我們也可以看到對(duì)管道讀寫(xiě)的一些特點(diǎn),即:
在管道中沒(méi)有數(shù)據(jù)的情況下,對(duì)管道的讀操作會(huì)阻塞,直到管道內(nèi)有數(shù)據(jù)為止。當(dāng)一次寫(xiě)的數(shù)據(jù)量不超過(guò)管道容量的時(shí)候,對(duì)管道的寫(xiě)操作一般不會(huì)阻塞,直接將要寫(xiě)的數(shù)據(jù)寫(xiě)入管道緩沖區(qū)即可。
當(dāng)然寫(xiě)操作也不會(huì)再所有情況下都不阻塞。這里我們要先來(lái)了解一下管道的內(nèi)核實(shí)現(xiàn)。上文說(shuō)過(guò),管道實(shí)際上就是內(nèi)核控制的一個(gè)內(nèi)存緩沖區(qū),既然是緩沖區(qū),就有容量上限。我們把管道一次最多可以緩存的數(shù)據(jù)量大小叫做 PIPESIZE。內(nèi)核在處理管道數(shù)據(jù)的時(shí)候,底層也要調(diào)用類(lèi)似 read 和 write 這樣的方法進(jìn)行數(shù)據(jù)拷貝,這種內(nèi)核操作每次可以操作的數(shù)據(jù)量也是有限的,一般的操作長(zhǎng)度為一個(gè) page,即默認(rèn)為 4k 字節(jié)。我們把每次可以操作的數(shù)據(jù)量長(zhǎng)度叫做 PIPEBUF。POSIX 標(biāo)準(zhǔn)中,對(duì) PIPEBUF 有長(zhǎng)度限制,要求其最小長(zhǎng)度不得低于 512 字節(jié)。PIPEBUF 的作用是,內(nèi)核在處理管道的時(shí)候,如果每次讀寫(xiě)操作的數(shù)據(jù)長(zhǎng)度不大于 PIPEBUF 時(shí),保證其操作是原子的。而 PIPESIZE 的影響是,大于其長(zhǎng)度的寫(xiě)操作會(huì)被阻塞,直到當(dāng)前管道中的數(shù)據(jù)被讀取為止。
在 Linux 2.6.11 之前,PIPESIZE 和 PIPEBUF 實(shí)際上是一樣的。在這之后,Linux 重新實(shí)現(xiàn)了一個(gè)管道緩存,并將它與寫(xiě)操作的 PIPEBUF 實(shí)現(xiàn)成了不同的概念,形成了一個(gè)默認(rèn)長(zhǎng)度為 65536 字節(jié)的 PIPESIZE,而 PIPEBUF 只影響相關(guān)讀寫(xiě)操作的原子性。從 Linux 2.6.35 之后,在 fcntl 系統(tǒng)調(diào)用方法中實(shí)現(xiàn)了 F_GETPIPE_SZ 和 F_SETPIPE_SZ 操作,來(lái)分別查看當(dāng)前管道容量和設(shè)置管道容量。管道容量容量上限可以在 /proc/sys/fs/pipe-max-size 進(jìn)行設(shè)置。
#define BUFSIZE 65536
......
ret = fcntl(pipefd[1], F_GETPIPE_SZ);
if (ret 0) { perror( fcntl()
exit(1);
printf(PIPESIZE: %d\n , ret);
ret = fcntl(pipefd[1], F_SETPIPE_SZ, BUFSIZE);
if (ret 0) { perror( fcntl()
exit(1);
......
PIPEBUF 和 PIPESIZE 對(duì)管道操作的影響會(huì)因?yàn)楣艿烂枋龇欠癖辉O(shè)置為非阻塞方式而有行為變化,n 為要寫(xiě)入的數(shù)據(jù)量時(shí)具體為:
O_NONBLOCK 關(guān)閉,n = PIPE_BUF:
n 個(gè)字節(jié)的寫(xiě)入操作是原子操作,write 系統(tǒng)調(diào)用可能會(huì)因?yàn)楣艿廊萘?(PIPESIZE) 沒(méi)有足夠的空間存放 n 字節(jié)長(zhǎng)度而阻塞。
O_NONBLOCK 打開(kāi),n = PIPE_BUF:
如果有足夠的空間存放 n 字節(jié)長(zhǎng)度,write 調(diào)用會(huì)立即返回成功,并且對(duì)數(shù)據(jù)進(jìn)行寫(xiě)操作。空間不夠則立即報(bào)錯(cuò)返回,并且 errno 被設(shè)置為 EAGAIN。
O_NONBLOCK 關(guān)閉,n PIPE_BUF:
對(duì) n 字節(jié)的寫(xiě)入操作不保證是原子的,就是說(shuō)這次寫(xiě)入操作的數(shù)據(jù)可能會(huì)跟其他進(jìn)程寫(xiě)這個(gè)管道的數(shù)據(jù)進(jìn)行交叉。當(dāng)管道容量長(zhǎng)度低于要寫(xiě)的數(shù)據(jù)長(zhǎng)度的時(shí)候 write 操作會(huì)被阻塞。
O_NONBLOCK 打開(kāi),n PIPE_BUF:
如果管道空間已滿。write 調(diào)用報(bào)錯(cuò)返回并且 errno 被設(shè)置為 EAGAIN。如果沒(méi)滿,則可能會(huì)寫(xiě)入從 1 到 n 個(gè)字節(jié)長(zhǎng)度,這取決于當(dāng)前管道的剩余空間長(zhǎng)度,并且這些數(shù)據(jù)可能跟別的進(jìn)程的數(shù)據(jù)有交叉。
以上是在使用半雙工管道的時(shí)候要注意的事情,因?yàn)樵谶@種情況下,管道的兩端都可能有多個(gè)進(jìn)程進(jìn)行讀寫(xiě)處理。如果再加上線程,則事情可能變得更復(fù)雜。實(shí)際上,我們?cè)谑褂霉艿赖臅r(shí)候,并不推薦這樣來(lái)用。管道推薦的使用方法是其單工模式:即只有兩個(gè)進(jìn)程通信,一個(gè)進(jìn)程只寫(xiě)管道,另一個(gè)進(jìn)程只讀管道。實(shí)現(xiàn)為:
[zorro@zorro-pc pipe]$ cat pipe_parent_child2.c
#include stdlib.h
#include stdio.h
#include unistd.h
#include string.h
#include sys/types.h
#include sys/wait.h
#define STRING hello world!
int main()
int pipefd[2];
pid_t pid;
char buf[BUFSIZ];
if (pipe(pipefd) == -1) { perror( pipe()
exit(1);
}
pid = fork();
if (pid == -1) { perror( fork()
exit(1);
}
if (pid == 0) {
/* this is child. */
close(pipefd[1]);
printf(Child pid is: %d\n , getpid());
if (read(pipefd[0], buf, BUFSIZ) 0) { perror( write()
exit(1);
}
printf(%s\n , buf);
} else {
/* this is parent */
close(pipefd[0]);
printf(Parent pid is: %d\n , getpid());
snprintf(buf, BUFSIZ, Message from parent: My pid is: %d , getpid());
if (write(pipefd[1], buf, strlen(buf)) 0) { perror( write()
exit(1);
}
wait(NULL);
}
exit(0);
}
這個(gè)程序?qū)嶋H上比上一個(gè)要簡(jiǎn)單,父進(jìn)程關(guān)閉管道的讀端,只寫(xiě)管道。子進(jìn)程關(guān)閉管道的寫(xiě)端,只讀管道。整個(gè)管道的打開(kāi)效果最后成為下圖所示:
此時(shí)兩個(gè)進(jìn)程就只用管道實(shí)現(xiàn)了一個(gè)單工通信,并且這種狀態(tài)下不用考慮多個(gè)進(jìn)程同時(shí)對(duì)管道寫(xiě)產(chǎn)生的數(shù)據(jù)交叉的問(wèn)題,這是最經(jīng)典的管道打開(kāi)方式,也是我們推薦的管道使用方式。另外,作為一個(gè)程序員,即使我們了解了 Linux 管道的實(shí)現(xiàn),我們的代碼也不能依賴(lài)其特性,所以處理管道時(shí)該越界判斷還是要判斷,該錯(cuò)誤檢查還是要檢查,這樣代碼才能更健壯。
FIFO
命名管道在底層的實(shí)現(xiàn)跟匿名管道完全一致,區(qū)別只是命名管道會(huì)有一個(gè)全局可見(jiàn)的文件名以供別人 open 打開(kāi)使用。再程序中創(chuàng)建一個(gè)命名管道文件的方法有兩種,一種是使用 mkfifo 函數(shù)。另一種是使用 mknod 系統(tǒng)調(diào)用,例子如下:
[zorro@zorro-pc pipe]$ cat mymkfifo.c
#include stdio.h
#include sys/types.h
#include sys/stat.h
#include stdlib.h
int main(int argc, char *argv[])
if (argc != 2) {
fprintf(stderr, Argument error!\n
exit(1);
}
if (mkfifo(argv[1], 0600) 0) { perror( mkfifo()
exit(1);
}
if (mknod(argv[1], 0600|S_IFIFO, 0) 0) { perror( mknod()
exit(1);
}
exit(0);
}
我們使用第一個(gè)參數(shù)作為創(chuàng)建的文件路徑。創(chuàng)建完之后,其他進(jìn)程就可以使用 open()、read()、write()標(biāo)準(zhǔn)文件操作等方法進(jìn)行使用了。其余所有的操作跟匿名管道使用類(lèi)似。需要注意的是,無(wú)論命名還是匿名管道,它的文件描述都沒(méi)有偏移量的概念,所以不能用 lseek 進(jìn)行偏移量調(diào)整。
讀到這里,這篇“l(fā)inux 管道是什么及怎么使用”文章已經(jīng)介紹完畢,想要掌握這篇文章的知識(shí)點(diǎn)還需要大家自己動(dòng)手實(shí)踐使用過(guò)才能領(lǐng)會(huì),如果想了解更多相關(guān)內(nèi)容的文章,歡迎關(guān)注丸趣 TV 行業(yè)資訊頻道。
向 AI 問(wèn)一下細(xì)節(jié)
丸趣 TV 網(wǎng) – 提供最優(yōu)質(zhì)的資源集合!