管道是一种最基本的IPC机制,由pipe函数创建。
#include <unistd.h>
int pipe(int filedes[2]);
调用pipe函数时在内核中开辟一块缓冲区(称为管道)用于通信,它有一个读端一个写端,然后通过filedes参数传出给用户程序两个文件描述符,filedes[0]指向管道的读端,filedes[1]指向管道的写端(很好记,就像0是标准输入,1是标准输出一样)。
父子进程向管道写数据,子进程从管道读数据,流程如下:
1.父进程创建管道。
2.父进程fork出子进程,子进程的记忆里有父进程的文件描述符表。
3.父进程关闭读端,子进程关闭写端。这样父子进程之间,就建立起了一条单向的管道。
代码演示如下:
#include "./common/head.h" /*功能: *父子进程通过管道通信,父进程向管道写数据,子进程从管道读数据,并打印到终端上。 */ int main() { int fd[2]; if( pipe(fd) < 0){ //成功返回0,否则返回-1,且errno会被设置成合适的值 perror("pipe"); exit(1); } pid_t pid = fork(); if(pid < 0){ perror("fork"); exit(1); } if(pid){ //父进程 close(fd[0]); //关闭读端 write(fd[1], "hello pipe\n", 11); //向管道写端写数据 wait(NULL); //等待子进程结束 }else{ //子进程 close(fd[1]); //关闭写端 sleep(1); //等待父进程向管道写数据 char buff[20]; int n = read(fd[0], buff, 20); //从管道读端读数据 write(STDOUT_FILENO, buff, n); //打印到终端 } return 0; }
上面的例子是父进程把文件描述符传给子进程之后父子进程之间的通信,也可以父进程fork两次,把文件描述符传给两个子进程,然后两个子进程之间通信,总之需要通过fork传递文件描述符使两个进程都能访问同一管道,它们才能通信。
使用管道需要注意以下4种特殊情况(假设都是阻塞I/O操作,没有设置O_NONBLOCK标志):
1.如果所有指向管道的写端的文件描述符都关闭了,而仍然有进程从管道的读端读数据,那么管道中剩余的数据都被读取后,再次read会返回0,就像读到文件末尾一样。
代码演示:
#include "./common/head.h"
/*功能:
*当管道写端全部关闭,读端读完剩余数据后,再读read返回0,像读到文件末尾一样。
*/
int main()
{
int fd[2];
if( pipe(fd) < 0){
perror("pipe");
exit(1);
}
pid_t pid = fork();
if(pid < 0){
perror("fork");
exit(1);
}
if(pid){ //父进程
close(fd[0]); //关闭读端
write(fd[1], "hello pipe\n", 11); //向管道写端写数据
close(fd[1]); //所有写端都关闭了
wait(NULL); //等待子进程结束
}else{ //子进程
close(fd[1]); //关闭写端
sleep(1); //等待父进程向管道写数据
char buff[20];
int n = read(fd[0], buff, 20); //从管道读端读数据
printf("read %d bytes\n", n);
write(STDOUT_FILENO, buff, n); //打印到终端
//此时管道中的数据已被读完,再读read会返回0,就像读到文件末尾一样
n = read(fd[0], buff, 20);
printf("read %d bytes\n", n); //0
write(STDOUT_FILENO, buff, n); //不会打印,因为n是0,打印0个字符
}
return 0;
}
结果:
2. 如果所有指向管道写端的文件描述符没关闭,而持有管道写端的进程也没有向管道中写数据,这时有进程从管道读端读数据,那么管道中剩余的数据被读取后,再次read会阻塞,直到管道中有数据可读了才读取数据并返回。
代码演示:
#include "./common/head.h"
/*功能:
*当管道写端没有关闭,读端读完剩余数据后,再读read会阻塞。
*/
int main()
{
int fd[2];
if( pipe(fd) < 0){
perror("pipe");
exit(1);
}
pid_t pid = fork();
if(pid < 0){
perror("fork");
exit(1);
}
if(pid){ //父进程
close(fd[0]); //关闭读端
write(fd[1], "hello pipe\n", 11); //向管道写端写数据
wait(NULL); //等待子进程结束,阻塞在这里
}else{ //子进程
close(fd[1]); //关闭写端
sleep(1); //等待父进程向管道写数据
char buff[20];
int n = read(fd[0], buff, 20); //从管道读端读数据
printf("read %d bytes\n", n);
write(STDOUT_FILENO, buff, n); //打印到终端
//此时管道中的数据已被读完,read会阻塞在这里
n = read(fd[0], buff, 20);
printf("read %d bytes\n", n);
write(STDOUT_FILENO, buff, n);
}
return 0;
}
结果:
解析:
父进程向管道中写完数据后,wait函数阻塞在这里,等待子进程结束;子进程将管道中的数据读完后,read函数阻塞会在这里;现在两个进程都在这里死等,真是卧龙凤雏,都卧在一起了!
这时,Ctrl c结果了父进程(也会结果了子进程),/*会触发父进程将所有的文件描述符关闭,则管道所有的写端都被关闭,又回到了情况1,子进程read返回0。父进程死了,子进程会被1号进程收养。*/
3.如果所有指向管道读端的文件描述符都关闭了,这时有进程向管道的写端write,那么该进程会收到信号SIGPIPE,通常会导致进程异常终止。
#include "./common/head.h"
/*功能:
*当管道所有读端被关闭,再往管道里写,进程会收到SIGPIPE信号,异常终止。
*/
int main()
{
int fd[2];
if( pipe(fd) < 0){
perror("pipe");
exit(1);
}
pid_t pid = fork();
if(pid < 0){
perror("fork");
exit(1);
}
if(pid){ //父进程
sleep(1); //等待子进程关闭读端
close(fd[0]); //关闭读端
//此时所有读端都被关闭了,再向管道中写数据,进程会收到SIGPIPE信号,异常退出
write(fd[1], "hello pipe\n", 11);
//以下代码都不会被执行,因为在write时已经异常退出了
perror("write");
wait(NULL);
}else{ //子进程
close(fd[1]); //关闭写端
close(fd[0]); //关闭读端
}
return 0;
}
结果:
4.如果有指向管道读端的文件描述符没关闭,而持有管道读端的进程也没有从管道中读数据,这时有进程向管道写数据,那么管道被写满时再次write会阻塞,直到管道中有空位置了才写入数据并返回。
代码演示:
#include "./common/head.h"
/*功能:
*当管道有读端但是没有去读,写端一直写数据,则会写64k数据开始阻塞。
*当子进程结束后,管道所有读端被关闭,再往里写数据,会回到情况3,收到SIGPIPE异常退出。
*/
int main()
{
int fd[2];
if( pipe(fd) < 0){
perror("pipe");
exit(1);
}
pid_t pid = fork();
if(pid < 0){
perror("fork");
exit(1);
}
if(pid){ //父进程
sleep(1); //等待子进程关闭读端
close(fd[0]); //关闭读端
int count = 0;
while(count++){
write(fd[1], buff, 1024);
printf("count = %d\n", i);
}
wait(NULL);
}else{ //子进程
close(fd[1]); //关闭写端
sleep(10); //等待父进程向管道拼命写数据
}
return 0;
}
结果: