Linux进程间通信-管道(pipe)
本系列文章主要是学习记录Linux下进程间通信的方式。
常用的进程间通信方式:管道、FIFO、消息队列、信号量以及共享存储。
参考文档:《UNIX环境高级编程(第三版)》
参考视频:Linux进程通信 推荐看看,老师讲得很不错
Linux核心版本:2.6.32-431.el6.x86_64
注:本文档只是简单介绍IPC,更详细的内容请查看参考文档和相应视频。
本文介绍利用管道进行进程间的通信。
1 简介
管道是最古老的一种方式,局限性:
- 半双工方式,数据只能在一个方向上流动;
- 只能在具有公共祖先的两个进程间使用。
2 函数接口
1 #include <unistd.h> 2 int pipe(int pipefd[2]); 3 说明:创建一个pipe 4 返回值:成功返回0,出错返回-1 5 参数[out]:fd保存返回的两个文件描述符,fd[0]为读而打开,fd[1]为写而打开。fd[1]的输出是fd[0]的输入。
3 通信模型
通信模型一:进程先调用pipe,接着调用fork,从而创建从父进程到子进程的IPC通道。
通信模型二:从父进程到子进程的通道。父进程关闭管道的读端(fd[0]),子进程关闭写端(fd[1])。
当管道一端被关闭后,以下两条规则起作用:
当读(read)一个写端已被关闭的管道时,在所有数据都被读取后,read返回0,表示文件结束。
如果写(write)一个读端已被关闭的管道,则产生信号SIGPIPE。
4 读写特性
1)可通过打开两个管道来创建一个双向的管道;
2)管道是阻塞性的,当进程从管道中读取数据,若没有数据进程会阻塞;
3)当一个进程往管道中不断的写入数据但是没有进程去读取数据,此时只要管道没有满是可以的,但若管道满的则会报错。
5 测试代码
(1)实例1
创建一个从父进程到子进程的管道,并且父进程通过该管道向子进程传送数据。
1 #include <stdio.h> 2 #include <unistd.h> 3 4 #define MAXLINE 512 5 6 int main(void) 7 { 8 int n; 9 int fd[2]; 10 pid_t pid; 11 char line[MAXLINE]; 12 13 if (pipe(fd) < 0) { //创建管道 14 perror("pipe error!"); 15 return -1; 16 } 17 if ((pid = fork()) < 0) { //创建子进程 18 perror("fork error!"); 19 return -1; 20 } else if (pid > 0) { //父进程 21 close(fd[0]); //父进程关闭读管道 22 write(fd[1], "hello world\n", 12); //父进程向管道中写入数据 23 close(fd[1]); 24 wait(0); //等待子进程结束 25 } else { //子进程 26 close(fd[1]); //子进程关闭写管道 27 n = read(fd[0], line, MAXLINE); //子进程从管道中读取数据 28 write(STDOUT_FILENO, line, n); //标准输出 29 close(fd[0]); 30 } 31 32 return 0; 33 }
(2)实例2
使用pipe实现类似于:cat /etc/passwd | grep root这个命令。
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <stdlib.h> 4 5 char *cmd1[3] = {"/bin/cat", "/etc/passwd", NULL}; 6 char *cmd2[3] = {"/bin/grep", "root", NULL}; 7 8 int main(void) 9 { 10 int fd[2]; 11 int i = 0; 12 pid_t pid; 13 14 if (pipe(fd) < 0) { 15 perror("pipe error"); 16 exit(1); 17 } 18 19 for (i = 0; i < 2; i++) { 20 pid = fork(); 21 if (pid < 0) { 22 perror("fork error"); 23 exit(1); 24 } else if (pid == 0) { 25 if (i == 0) { //第一个子进程 26 //负责往管道写入数据 27 close(fd[0]); //关闭读端 28 //cat命令执行结果是标准输出,需要将标准输出重定向到管道写端 29 //下面命令执行的结果会写入到管道中,而不是输出到屏幕 30 if (dup2(fd[1], STDOUT_FILENO) != STDOUT_FILENO) { 31 perror("dup2 error"); 32 exit(1); 33 } 34 close(fd[1]); //已经复制了一份,原来的可以关闭 35 //调用exce函数执行cat命令 36 if (execvp(cmd1[0], cmd1) < 0) { 37 perror("execvp error"); 38 exit(1); 39 } 40 break; 41 } 42 if (i == 1) { //第二个子进程 43 //负责从管道读取数据 44 close(fd[1]); //关闭写端 45 //grep命令默认读取的内容来源于标准输入 46 //需要将标准输入重定向到管道的读端 47 //下面命令执行时从管道的读端读取内容,而不是从标准输入读取 48 if (dup2(fd[0], STDIN_FILENO) != STDIN_FILENO) { 49 perror("dup2 error"); 50 exit(1); 51 } 52 close(fd[0]); 53 //调用exce函数执行grep命令 54 if (execvp(cmd2[0], cmd2) < 0) { 55 perror("execvp error"); 56 exit(1); 57 } 58 break; 59 } 60 } else { //父进程,仅用于创建子进程 61 //等待子进程创建并回收 62 if (i == 1) { 63 //等待子进程全部创建完毕,才回收 64 close(fd[0]); 65 close(fd[1]); 66 wait(0); 67 wait(0); 68 } 69 } 70 } 71 72 return 0; 73 }
(3)实例3
使用pipe实现一个协同进程。
创建两个管道,父进程向管道1中写入数据,并从管道2中读取子进程计算出的结果值;
子进程从管道1中读取数据,并调用add程序进行累加,将累加的结果写入到管道2中。
add程序实现代码:
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <stdlib.h> 4 5 int main(void) 6 { 7 int x, y; 8 9 if (read(STDIN_FILENO, &x, sizeof(int)) < 0) { 10 perror("read error"); 11 } 12 if (read(STDIN_FILENO, &y, sizeof(int)) < 0) { 13 perror("read error"); 14 } 15 16 int result = x + y; 17 if (write(STDOUT_FILENO, &result, sizeof(int)) < sizeof(int)) { 18 perror("write error"); 19 } 20 21 return 0; 22 }
协同进程实现代码:
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <stdlib.h> 4 5 int main(void) 6 { 7 int fda[2], fdb[2]; 8 9 if ((pipe(fda) < 0) || (pipe(fdb) < 0)) { 10 perror("pipe error"); 11 exit(1); 12 } 13 14 pid_t pid; 15 pid = fork(); 16 if (pid < 0) { //子进程 17 perror("fork error"); 18 exit(1); 19 } else if (pid == 0) { 20 //1、子进程负责从管道a中读取父进程写入的累加参数x和y 21 //2、通过exec函数调用/bin/add程序进行累加 22 //3、将累加结果写入到管道b 23 close(fda[1]); 24 close(fdb[0]); 25 //将标准输入重定向到管道a的读端 26 //add程序中将从管道a中读取累加参数x和y 27 if (dup2(fda[0], STDIN_FILENO) != STDIN_FILENO) { 28 perror("dup2 error"); 29 } 30 //将标准输出重定向到管道b的写端 31 //add程序累加后的结果会写入到管道b 32 if (dup2(fdb[1], STDOUT_FILENO) != STDOUT_FILENO) { 33 perror("dup2 error"); 34 } 35 close(fda[0]); 36 close(fdb[1]); 37 if (execlp("bin/add", "bin/add", NULL) < 0) { 38 perror("execlp error"); 39 exit(1); 40 } 41 } else { //父进程 42 //1、从标准输入上读取累加参数x和y 43 //2、将x和y写入管道a 44 //3、从管道b中读取累加的结果并输出 45 close(fda[0]); 46 close(fdb[1]); 47 //1 48 int x, y; 49 printf("please input x and y: "); 50 scanf("%d %d", &x, &y); 51 //2 52 if (write(fda[1], &x, sizeof(int)) != sizeof(int)) { 53 perror("write error"); 54 } 55 if (write(fda[1], &y, sizeof(int)) != sizeof(int)) { 56 perror("write error"); 57 } 58 //3 59 int result = 0; 60 if (read(fdb[0], &result, sizeof(int)) != sizeof(int)) { //阻塞式读写 61 perror("read error"); 62 } 63 printf("add result is %d\n", result); 64 close(fda[1]); 65 close(fdb[0]); 66 wait(0); 67 } 68 69 70 return 0; 71 }
测试结果:
[root@192 ipc]# gcc -o bin/add add.c
[root@192 ipc]# gcc -o bin/co_pro c_process.c
[root@192 ipc]# ./bin/co_pro
please input x and y: 12 23
add result is 35
(4)案例4
实现一个不完整管道:当读一个写端已被关闭的管道时,在所有数据被读取后,read返回0,以表示到达了文件尾部。
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <stdlib.h> 4 5 /* 6 * 不完整管道:读取一个写端已经关闭的管道 7 */ 8 9 int main(void) 10 { 11 int fd[2]; 12 13 if (pipe(fd) < 0) { 14 perror("pipe error"); 15 exit(1); 16 } 17 pid_t pid; 18 if ((pid = fork()) < 0) { 19 perror("fork error"); 20 exit(1); 21 } else if (pid > 0) { //父进程 22 //父进程从不完整管道(写端关闭)中读取数据 23 sleep(5); //等子进程将管道写端关闭 24 close(fd[1]); 25 while (1) { 26 char c; 27 if (read(fd[0], &c, 1) == 0) { 28 printf("\nwrite-end of pipe closed\n"); 29 break; 30 } else { 31 printf("%c", c); 32 } 33 } 34 } else { //子进程 35 // 子进程负责将数据写入管道 36 close(fd[0]); 37 char *s = "1234"; 38 write(fd[1], s, sizeof(s)); 39 close(fd[1]); 40 } 41 42 return 0; 43 }
(5)案例5
实现一个不完整管道:当写一个读端被关闭的信号,则产生信号SIGPIPE,如果忽略该信号或捕捉该信号并从处理程序返回,则write返回-1,同时errno设置为EPIPE。
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <stdlib.h> 4 #include <string.h> 5 #include <errno.h> 6 #include <signal.h> 7 8 /* 9 * 不完整管道:写入一个读端已经被关闭的管道 10 */ 11 12 void sig_handler(int signo) 13 { 14 if (signo == SIGPIPE) { 15 printf("SIGPIPE occured\n"); 16 } 17 } 18 19 int main(void) 20 { 21 int fd[2]; 22 23 if (pipe(fd) < 0) { 24 perror("pipe error"); 25 exit(0); 26 } 27 28 pid_t pid; 29 if ((pid = fork()) < 0) { 30 perror("fork error"); 31 } else if (pid > 0) { //父进程 32 //父进程负责将数据写入到不完整管道(读端关闭)中 33 sleep(5); 34 close(fd[0]); 35 if (signal(SIGPIPE, sig_handler) == SIG_ERR) { 36 perror("signal sigpipe error"); 37 exit(1); 38 } 39 char *s = "1234"; 40 if (write(fd[1], s, sizeof(s)) != sizeof(s)) { 41 fprintf(stderr, "%s, %s\n", strerror(errno), (errno == EPIPE) ? "EPIPE" : ", unkown"); 42 } 43 close(fd[1]); 44 wait(0); 45 } else { //子进程 46 //关闭管道的读端 47 close(fd[0]); 48 close(fd[1]); 49 } 50 51 return 0; 52 }
6 标准库中的管道操作
函数实现的操作:创建一个管道,fork一个子进程,关闭未使用的管道端,执行一个shell运行命令,然后等待命令终止。
(1)函数原型
1 #include <stdio.h> 2 FILE *popen(const char *command, const char *type); 3 返回值:成功返回文件指针,出错返回NULL。 4 参数command:命令的路径。 5 参数type:读写特性,”r”或”w” 6 int pclose(FILE *stream);
函数popen先执行fork,然后调用exec执行command,并且返回一个标准I/O文件指针。
如果type是“r”,则文件指针连接到command的标准输出。
如果type是"w",则文件指针连接到command的标准输入。
(2)popen内部原理
(3)实例
1 #include <stdio.h> 2 #include <stdlib.h> 3 4 int main(void) 5 { 6 FILE *fp; 7 8 //命令执行的结果放置到fp指向的结构体缓存中 9 fp = popen("cat /etc/passwd", "r"); 10 char buf[512] = {0}; 11 while (fgets(buf, sizeof(buf), fp) != NULL) { 12 printf("%s", buf); 13 } 14 pclose(fp); 15 16 printf("----------------------------------\n"); 17 //为wr命令提供统计的数据 18 fp = popen("wc -l", "w"); 19 fprintf(fp, "1\n2\n3\n"); 20 pclose(fp); 21 22 return 0; 23 }