一、无名管道的概念
无名管道是一种特殊类型的文件,在内核中对应的资源即一段特殊内存空间,内核在这段空间中以循环队列的方式临时存入一个进程发送给另一个进程的信息,这段内核空间完全有操作系统管理和维护,应用程序只需要,也只能使用系统调用来访问它。
无名管道和普通文件有很大的差异:无名管道的内核资源在通信两进程退出后会自动释放。但是,在编程应用方式,具有和普通文件一样的特点,可以使用read/write等函数进行读写操作,只是读写的特点有一定的差异,另外,不能使用seek函数来修改当前的读写位置,因为管道要满足先入先出(FIFO)的原则。PIPE的特性可以总结为一下几点:
1.只能用在有亲缘关系进程间通讯;
2.无法seek,要保持先入先出(FIFO)原则;
3.PIPE有读写端,且是固定的,也就是说通讯时单项的;
4.在通信双方结束时PIPE自动消失。
二、创建无名管道
在使用无名管道钱需要先创建无名管道,其函数声明如下:
// come from /usr/include/unistd.h
int pipe(int pipides[2]);
参数pipides为一个整形数组,下表为2。如果函数执行成功,pipe将存储两个文件描述符于pipedes[0]和pipedes[1]中,它们分别指向管道的两端。如果系统调用失败,将返回-1。
无名管道是单工的,一个管道只能实现从一个进程向另外一个进程发送消息,pipedes[0]是读端,pipedes[1]是写段,两者的角色不能互换,即pipedes[0]必须是读端, pipedes[1]必须是写端。如果需要实现两进程双工通信,需要两个无名管道。
无名管道通信原理如下图所示:
三、读写无名管道
无名管道的读写和普通文件不一样。任何的进程读/写无名管道时必须确认还存在一个进程(这个进程可以是自己)可以可以访问PIPE的另一端。读/写管道使用系统调用read和write,两种默认以阻塞的方式读写管道,如果要修改这两个函数的行为,可以使用fcntl函数实现。
1.以阻塞方式读管道,如果当前管道没有一个进程(包括自己)可以访问写端,读操作将立即返回,并按如下操作。
a.如果管道无数据,立即返回0;
b.如果管道有数据,且数量大于要读出数据的数量,立即读出期望大小的数据;
c.如果管道有数据,数量小于要读出的数量,立即读出现有所有数据。
下面代码将说明第1种情况:
#include <stdio.h>
#include <unistd.h>
int main()
{
int p[2];
char buf[128] = {0};
int ret = -1;
pipe(p);
close(p[1]); // 断开当前进程与管道写端联系
// 阻塞读,无数据,无进程关联写端,立即返回
ret = read(p[0], buf, sizeof(buf));
printf("ret=%d, buf=%s\n", ret, buf);
return 0;
}
运行结果如下:
下面代码将说明第2、3种情况:
#include <stdio.h>
#include <unistd.h>
int main()
{
int p[2];
char buf[128] = {0};
int ret = -1;
pipe(p);
write(p[1], "helloworld", 10); // 写入10字节
close(p[1]); // 断开当前进程与管道写端联系
ret = read(p[0], buf, 3); // 阻塞读,无进程关联写端,有数据,且大于期望读出值
printf("first ret=%d, buf=%s\n", ret, buf);
ret = read(p[0], buf, 15); // 阻塞读,无进程关联写端,有数据,但小于期望读出值
printf("second ret=%d, buf=%s\n", ret, buf);
return 0;
}
运行结果如下所示:
2.以阻塞方式读管道,如果当前管道有一个进程(包括自己)可以访问写端,按如下操作。
a.管道中无任何数据,读操作阻塞;
b.管道中有数据,现有数据大小小于期望读出值,读出现有数据并返回;
c.管道中有数据,现有数据大小大于期望读出值,读出期望大小数据并返回。
下面代码将说明第1种情况:
#include <stdio.h>
#include <unistd.h>
int main()
{
int p[2];
char buf[128] = {0};
int ret = -1;
pipe(p);
// 阻塞读,无数据,直到有数据才返回
ret = read(p[0], buf, sizeof(buf));
printf("ret=%d, buf=%s\n", ret, buf);
return 0;
}
上述代码中read函数会一直阻塞直到管道中有数据才返回。如果在读管道的时候关闭写端,无弄管道中有无数据,read函数都将立即返回。
下面代码将说明第2、3种情况:
#include <stdio.h>
#include <unistd.h>
int main()
{
int p[2];
char buf[128] = {0};
int ret = -1;
pipe(p);
write(p[1], "helloworld", 10); // 写入10字节
ret = read(p[0], buf, 3); // 阻塞读,有数据,且大于期望读出值
printf("first ret=%d, buf=%s\n", ret, buf);
ret = read(p[0], buf, 15); // 阻塞读,有数据,但小于期望读出值
printf("second ret=%d, buf=%s\n", ret, buf);
return 0;
}
运行结果如下所示:
3.如果以阻塞的方式写无名管道,有以下情况:
a.如果没有进程(包括自己)可以访问读端,写操作将受到SIGPIPE信号,write函数返回-1;
b.如果当前有进程可以访问读端,且管道中有空间则写入成功;
c.如果当前有进程可以访问读端,如果管道空间已满,则阻塞写操作。管道的空间大小PIPE_BUF默认大小为4096,定义在limit.h文件中,如下所示:
// come from /usr/include/limit.h
#define PIPE_BUF 4096 /*bytes in atomic write to a pipe*/
4.如果以O_NDELAY或O_NONBLOCK设置了管道的读端,如果管道中有数据,将读取数据,没有数据,将立即返回-1,且置errno为EAGAIN错误。
5.如果以O_NDELAY或O_NONBLOCK设置了管道的写端,如果管道中有空间,将写入数据,如果没有足够的空间,将立即返回-1,且置errno为EAGAIN错误。
四、示例
本文中上面的代码均是在一个进程中操作,下面给出一个父子进程通过无名管道通信的示例:
#include <stdio.h>
#include <unistd.h>
int main()
{
int p[2];
char buf[128] = {0};
int ret = -1;
pipe(p);
pid_t pid = fork();
if (pid < 0) {
perror("fork error");
return -1;
}
if (0 == pid) { // 子进程
sleep(1);
close(p[0]); // 关闭子进程的读端
write(p[1], "hello world!", 12);
} else { // 父进程
close(p[1]); // 关闭父进程的写端
ret = read(p[0], buf, sizeof(buf));
printf("ret=%d buf=%s\n", ret, buf);
}
return 0;
}
运行结果如下所示: