目录
1、概述
2、管道的创建
3、管道读写行为
3.1、管道读
3.2、管道写
4、管道用于兄弟进程之间的通讯
在linux中管道有两种,一是无名管道(匿名管道),第二种是有名管道;无名管道主要用于有血缘关系的父子进程间通信,有名管道则不受该限制,可用于任意进程之间的通信;这里我们主要学习无名管道。
1、概述
创建无名管道的函数如下:
#include <unistd.h>
int pipe(int pipefd[2]);
调用失败返回-1,成功返回0;调用成功时该函数会创建一个单向的管道用于进程之间的通讯,返回的管道包含读端和写端,其中fd[0]用于读,fd[1]用于写,写到fd[1]的数据会被内核保存到缓冲区,直到fd[0]读走数据。
2、管道的创建
代码如下:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<string.h>
int main(int argc, char *argv[]) {
pid_t fd[2];
int ret;
ret = pipe(fd);
if (-1 == ret) {
perror("pipe error!\n");
exit(1);
}
printf("Hello pipe, fd[0] = %d, fd[1] = %d\n", fd[0], fd[1]);
return 0;
}
很简单,直接调用pipe并传入一个文件描述符数组,如果出错则退出,调用成功则返回两个可用的文件描述符,其中fd[0]用于读,fd[1]用于写。
gcc编译:gcc pipe.c -o pipe,运行./pipe得出如下运行结果:
3、管道读写行为
由于管道是单向通讯的,所以在使用的时候会有一些限制。
- 管道读
如果管道中有数据,则读取实际能读到的数据;
如果管道中无数据,此时有两种情况:一是有管道写端,此时会阻塞直到写端写入数据;二是没有管道写端(写端被关闭),因为没有写端,永远不会有数据写入,此时返回0。
- 管道写
如果管道满了,则写阻塞,直到能写入。
如果管道未满,此时有两种情况:一是没有管道读端,此时异常终止(SIGPIPE导致);二是有读端,则返回实际写入的字节数。
下面简单列举几个例子,主要是父子进程之间通讯。
3.1、管道读
代码如下:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<string.h>
int main(int argc, char *argv[]) {
pid_t fd[2];
int ret;
char buf[1024];
char *info = "Hello Pipe(from read)\n";
ret = pipe(fd);
if (-1 == ret) {
perror("pipe error!\n");
exit(1);
}
ret = fork();
if (0 == ret) {
printf("I'm child\n");
close(fd[1]);
while ((ret = read(fd[0], &buf, 1024)) > 0) {
write(STDOUT_FILENO, &buf, ret);
}
close(fd[0]);
} else if (ret > 0) {
printf("I'm parent\n");
close(fd[0]);
write(fd[1], info, strlen(info));
close(fd[1]);
} else {
perror("fork error!\n");
exit(1);
}
return 0;
}
首先调用pipe创建了读写管道的读端和写端,然后调用fork创建了一个子进程,如果ret 为0,说明是在子进程,如果ret大于0,说明是在父进程,如果ret小于0则说明fork出错,退出程序。
代码中申明了一个字符串"Hello Pipe(from read)",由于父进程用于写,子进程用于读,所以父进程首先关闭了fd[0],然后用fd[1]往管道中写入"Hello Pipe(from read)",之后关闭fd[1]。
由于子进程用于读,所以一开始关闭了管道的写端fd[1],然后循环从fd[0]中读入数据存储到buf中并打印到屏幕,直到读完管道中的数据,最后关闭管道读端fd[0]。
运行结果如下:
上面演示的是管道有数据的情况,立刻就读出了管道中的数据;下面考虑以下两种情况
- 管道无数据 有写端
- 管道无数据 无写端
第一种情况,管道将会读阻塞,可以运行如下实例看看:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<string.h>
int main(int argc, char *argv[]) {
pid_t fd[2];
int ret;
char buf[1024];
char *info = "Hello Pipe(from read)\n";
ret = pipe(fd);
if (-1 == ret) {
perror("pipe error!\n");
exit(1);
}
ret = fork();
if (0 == ret) {
printf("I'm child\n");
close(fd[1]);
while ((ret = read(fd[0], &buf, 1024)) > 0) {
write(STDOUT_FILENO, &buf, ret);
}
close(fd[0]);
printf("After read\n");
} else if (ret > 0) {
printf("I'm parent\n");
close(fd[0]);
//write(fd[1], info, strlen(info));
sleep(10);
printf("After sleep\n");
close(fd[1]);
} else {
perror("fork error!\n");
exit(1);
}
return 0;
}
代码结构和之前一样,为了构造管道无数据的情况,父进程中没有写入数据,而是sleep(10),然后10s后输出"After sleep"且关闭管道写端fd[1];子进程保持不变,由于管道中一直没有数据,会导致子进程管道的读端一直阻塞直到父进程关闭管道写端fd[1]。
程序输出如下:
第二种情况,管道无数据无写端,此时读管道将直接返回0,因为确实没有数据可以读;其实第一种情况演示里面也包含了这种情况,管道关闭的时候,也就是没有写端,读端就返回了。
3.2、管道写
代码如下:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<string.h>
int main(int argc, char *argv[]) {
pid_t fd[2];
int ret;
char buf[1024];
char *info = "Hello Pipe(from read)\n";
ret = pipe(fd);
if (-1 == ret) {
perror("pipe error!\n");
exit(1);
}
ret = fork();
if (0 == ret) {
printf("I'm child\n");
close(fd[1]);
while ((ret = read(fd[0], &buf, 1024)) > 0) {
write(STDOUT_FILENO, &buf, ret);
}
close(fd[0]);
printf("After read\n");
} else if (ret > 0) {
printf("I'm parent\n");
close(fd[0]);
int n = write(fd[1], info, strlen(info));
printf("parent write: %d byte\n", n);
close(fd[1]);
} else {
perror("fork error!\n");
exit(1);
}
return 0;
}
逻辑结构与管道读一致,这里仅仅是在写管道的时候返回写入的管道字节数,运行结果如下:
上面演示了管道未满,且有读端的情况;那么没有读端的时候表现如何呢?前面我们说过管道未满,没有读端的时候写入管道将会报错,现在来看看这种情况,将代码改为如下:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<string.h>
int main(int argc, char *argv[]) {
pid_t fd[2];
int ret;
char buf[1024];
char *info = "Hello Pipe(from read)\n";
ret = pipe(fd);
if (-1 == ret) {
perror("pipe error!\n");
exit(1);
}
ret = fork();
if (0 == ret) {
printf("I'm child\n");
close(fd[1]);
/*
while ((ret = read(fd[0], &buf, 1024)) > 0) {
write(STDOUT_FILENO, &buf, ret);
}
*/
close(fd[0]);
printf("After close\n");
} else if (ret > 0) {
printf("I'm parent\n");
close(fd[0]);
sleep(2);
int n = write(fd[1], info, strlen(info));
printf("parent write: %d byte\n", n);
close(fd[1]);
} else {
perror("fork error!\n");
exit(1);
}
printf("end of process\n");
return 0;
}
子进程一开始就关闭了管道的读端和写端,父进程则一开始关闭读端,睡眠2s等待子进程关闭管道读端后再写入数据,此时就营造了有写端没读端的情况,在命令行模式输出如下:
只输出了一行"end of process",明显是有一个进程没有运行完整就退出了,我们在gdb下运行看看,输出如下:
在gdb下以及把进程的错误输出了,SIGPIPE导致了进程退出,其实也可以理解,因为没读端,此时写入的数据相当于是无用数据,内核干脆把问题抛了出来;开发者其实可以捕获这个信号然后进行自定义处理,此处不再展开。
管道写还有一种情况是,管道已经满了,这种情况如何演示呢?很遗憾,没法演示这种情况,因为内核会在管道快要满的时候动态扩容,此时管道会恢复到正常状况,对于应用来说就是管道可以一直写入,所以我们看不到这种情况。
4、管道用于兄弟进程之间的通讯
前面介绍的是用于父子进程之间的通讯,现在来看看用于兄弟进程之间如何通讯,也是在前面例子的基础上,我们在父进程中创建两个子进程,然后在两个子进程之间使用管道进行通讯,修改后的代码如下:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<string.h>
int main(int argc, char *argv[]) {
pid_t fd[2];
int ret, i;
char buf[1024];
pid_t status;
char *info = "Hello Pipe(from read)\n";
ret = pipe(fd);
if (-1 == ret) {
perror("pipe error!\n");
exit(1);
}
for (i = 0; i < 2; i++) {
ret = fork();
if (0 == ret) {
break;
} else if (ret < 0) {
perror("fork error!\n");
exit(1);
}
}
if (0 == i) { // child 1
printf("I'm child 1\n");
close(fd[1]);
while ((ret = read(fd[0], &buf, 1024)) > 0) {
write(STDOUT_FILENO, &buf, ret);
}
close(fd[0]);
} else if (1 == i) { // child 2
printf("I'm child 2\n");
close(fd[0]);
int n = write(fd[1], info, strlen(info));
close(fd[1]);
} else if (2 == i) {
printf("I'm parent\n");
//close(fd[0]);
//close(fd[1]);
for (int j = 0; j < 2; j++) {
status = wait(NULL);
printf("parent wait %d\n", status);
}
}
printf("end of process\n");
return 0;
}
子进程2写入,子进程1读出,所以子进程2一开始先关闭管道读端fd[0],然后往管道的写端fd[1]写入数据;子进程负责1读取数据,所以子进程1刚开始就关闭管道写入端fd[1],然后从管道读入端fd[0]读取数据,下面是运行结果:
从运行结果来看是有问题的,两个子进程加上一个父进程,应该输出3次"end of process",而这里只有一次,同时父进程也只回收了一个子进程;代码在父子进程通讯的时候正常,变成兄弟进程之后就不正常了,为什么会这样呢?
前面我们说过管道数据是单向流动的,从写端到读端;在当前我们的这个场景下,其实出除了两个子进程之间从子进程2向子进程1的数据流动,父进程同样也持有了管道的读写端,从而破坏了管道的单向流动,所以我们需要关闭父进程中的管道读写端,保证数据的单向流动,修改后的代码如下:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<string.h>
int main(int argc, char *argv[]) {
pid_t fd[2];
int ret, i;
char buf[1024];
pid_t status;
char *info = "Hello Pipe(from read)\n";
ret = pipe(fd);
if (-1 == ret) {
perror("pipe error!\n");
exit(1);
}
for (i = 0; i < 2; i++) {
ret = fork();
if (0 == ret) {
break;
} else if (ret < 0) {
perror("fork error!\n");
exit(1);
}
}
if (0 == i) { // child 1
printf("I'm child 1\n");
close(fd[1]);
while ((ret = read(fd[0], &buf, 1024)) > 0) {
write(STDOUT_FILENO, &buf, ret);
}
close(fd[0]);
} else if (1 == i) { // child 2
printf("I'm child 2\n");
close(fd[0]);
int n = write(fd[1], info, strlen(info));
close(fd[1]);
} else if (2 == i) {
printf("I'm parent\n");
close(fd[0]);
close(fd[1]);
for (int j = 0; j < 2; j++) {
status = wait(NULL);
printf("parent wait %d\n", status);
}
}
printf("end of process\n");
return 0;
}
运行结果如下:
如结果所示,此时所有进程都能正常终止了。