介紹
管道本質上就是一個文件,前面的進程以寫方式打開文件,后面的進程以讀方式打開。這樣前面寫完后面讀,于是就實現了通信。雖然實現形態上是文件,但是管道本身并不占用磁盤或者其他外部存儲的空間。在Linux的實現上,它占用的是內存空間。所以,Linux上的管道就是一個操作方式為文件的內存緩沖區。管道分類:匿名管道、命名管道
命令行中使用
1、mkfifo或mknod命令來創建一個命名管道
[root@VM-90-225-centos ~]# mkfifo pipe
[root@VM-90-225-centos ~]# ls -l pipe
prw-r--r-- 1 root root 0 Feb 28 21:02 pipe
我們現在讓一個進程寫這個管道文件:
echo 12345 > pipe
此時這個寫操作會阻塞,因為管道另一端沒有人讀。此時如果有進程讀這個管道,那么這個寫操作的阻塞才會解除:
[root@VM-90-225-centos ~]# cat pipe
12345
當我們cat完這個文件之后,另一端的echo命令也返回了.
Linux系統無論對于命名管道和匿名管道,底層都用的是同一種文件系統的操作行為,這種文件系統叫pipefs,可以通過下面命令查看是否具有這個系統:
[root@VM-90-225-centos ~]# cat /proc/filesystems |grep pipefs
nodev pipefs
nodev rpc_pipefs
系統編程中使用
匿名管道和命名管道分別叫做PIPE和FIFO,創建匿名管道的系統調用是pipe(),而創建命名管道的函數是mkfifo()。
匿名管道:
#include <unistd.h>
int pipe(int pipefd[2]);
這個方法將會創建出兩個文件描述符:
pipefd[0]是讀方式打開,作為管道的讀描述符。pipefd[1]是寫方式打開,作為管道的寫描述符。從管道寫端寫入的數據會被內核緩存直到有人從另一端讀取為止。
pipe示例一:簡單的寫入+讀出
#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);}// 從[1]寫入STRINGif (write(pipefd[1], STRING, strlen(STRING)) < 0) {perror("write()");exit(1);}// 從[0]讀出,結果存到bufif (read(pipefd[0], buf, BUFSIZ) < 0) {perror("write()");exit(1);}// 打印讀出來的結果printf("%s\n", buf);exit(0);
}
程序創建了一個管道,并且對管道寫了一個字符串之后從管道讀取,并打印在標準輸出上。
當然這不屬于進程間通信,實際情況中我們不會在單個進程中使用管道。
進程在pipe創建完管道之后,往往都要fork產生子進程。下面的demo是父子兩個進程使用一個管道可以完成半雙工通信。此時,父進程可以通過fd[1]給子進程發消息,子進程通過fd[0]讀。子進程也可以通過fd[1]給父進程發消息,父進程用fd[0]讀。
pipe示例二:父子進程半雙工通信
#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);}// fork產生子進程pid = fork();if (pid == -1) {perror("fork()");exit(1);}// fork()新進程返回0,舊進程返回新進程的進程ID。if (pid == 0) {/* this is child. */printf("Child pid is: %d\n", getpid());// 子進程會繼承父進程對應的文件描述符// 父進程先pipe創建管道之后,子進程也會得到同一個管道的讀寫文件描述符if (read(pipefd[0], buf, BUFSIZ) < 0) {perror("write()");exit(1);}printf("%s\n", buf);// 清空bufbzero(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);}// 等待1ssleep(1);// 清空bufbzero(buf, BUFSIZ);// 讀取管道消息到bufif (read(pipefd[0], buf, BUFSIZ) < 0) {perror("write()");exit(1);}printf("%s\n", buf);wait(NULL);}exit(0);
}
打印結果:
Parent pid is: 17697
Child pid is: 17702
Message from parent: My pid is: 17697
Message from child: My pid is: 17702
如果在vscode中debug的話是debug不到if (pid == 0) 的分支的,你只能debug主進程的流程。
從以上的demo中用同一個管道的父子進程可以分時給對方發送消息,我們可以看到對管道讀寫的一些特點:
1、在管道中沒有數據的情況下,對管道的讀操作會阻塞,直到管道內有數據為止。
2、當一次寫的數據量不超過管道容量的時候,對管道的寫操作一般不會阻塞,直接將要寫的數據寫入管道緩沖區即可。
管道實際上就是內核控制的一個內存緩沖區,既然是緩沖區,就有容量上限。我們把管道一次最多可以緩存的數據量大小叫做PIPESIZE。內核在處理管道數據的時候,底層也要調用類似read和write這樣的方法進行數據拷貝,這種內核操作每次可以操作的數據量也是有限的,一般的操作長度為一個page,即默認為4k字節。我們把每次可以操作的數據量長度叫做PIPEBUF。POSIX標準中,對PIPEBUF有長度限制,要求其最小長度不得低于512字節。PIPEBUF的作用是,內核在處理管道的時候,如果每次讀寫操作的數據長度不大于PIPEBUF時,保證其操作是原子的。而PIPESIZE的影響是,大于其長度的寫操作會被阻塞,直到當前管道中的數據被讀取為止。
在Linux 2.6.11之前,PIPESIZE和PIPEBUF實際上是一樣的。在這之后,Linux重新實現了一個管道緩存,并將它與寫操作的PIPEBUF實現成了不同的概念,形成了一個默認長度為65536字節的PIPESIZE,而PIPEBUF只影響相關讀寫操作的原子性。從Linux 2.6.35之后,在fcntl系統調用方法中實現了F_GETPIPE_SZ和F_SETPIPE_SZ操作,來分別查看當前管道容量和設置管道容量。管道容量容量上限可以在/proc/sys/fs/pipe-max-size進行設置。
在實際情境下,半雙工管道管道的兩端都可能有多個進程進行讀寫處理。如果再加上線程,則事情可能變得更復雜。實際上,我們在使用管道的時候,并不推薦這樣來用。管道推薦的使用方法是其單工模式:即只有兩個進程通信,一個進程只寫管道,另一個進程只讀管道。
pipe示例三:父子進程單工通信
這個程序實際上比上一個要簡單,父進程關閉管道的讀端,只寫管道。子進程關閉管道的寫端,只讀管道。
此時兩個進程就只用管道實現了一個單工通信,并且這種狀態下不用考慮多個進程同時對管道寫產生的數據交叉的問題,這是最經典的管道打開方式,也是我們推薦的管道使用方式。
#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);
}
命名管道
命名管道在底層的實現跟匿名管道完全一致,區別只是命名管道會有一個全局可見的文件名以供別人open打開使用。再程序中創建一個命名管道文件的方法有兩種,一種是使用mkfifo
函數。另一種是使用mknod
系統調用
fifo示例:
client端
/* 這是一個命名管道的實現demo,實現兩個進程間聊天功能* */
#include<stdio.h>
#include<unistd.h>
#include<sys/stat.h>
#include<errno.h>
#include<fcntl.h>
#include<string.h>int main()
{char *file = "./test.fifo";umask(0); //設置umask,僅在當前進程有效。if(mkfifo(file,0663)<0){if(errno == EEXIST){printf("fifo exist\n");}else {perror("mkfifo\n");return -1;}}int fd = open(file,O_WRONLY);if(fd<0){perror("open error");return -1;}printf("open fifo success!!!\n");while(1){printf("input: ");fflush(stdout);char buff[1024]={0};scanf("%s",buff);write(fd,buff,strlen(buff));}return 0;
}
server端:
#include<stdio.h>
#include<unistd.h>
#include<sys/stat.h>
#include<errno.h>
#include<fcntl.h>
#include<string.h>int main()
{char *file = "./test.fifo";umask(0); //設置umask,僅在當前進程有效。if(mkfifo(file,0663)<0){if(errno == EEXIST){printf("fifo exist\n");}else {perror("mkfifo\n");return -1;}}int fd = open(file,O_RDONLY);if(fd<0){perror("open error");return -1;}printf("open fifo success!!!\n");while(1){char buff[1024] = {0};int ret = read(fd,buff,1024);if(ret>0){printf("peer say:%s\n",buff);}}return 0;
}
然后在子目錄下編譯:
g++ ./server.cpp -o server
g++ ./client.cpp -o client
然后在兩個終端頁面上分別運行:
./server
./client
即可進行單向通信,一般工程中兩個進程建立兩個fifo就可以進行雙向通信了