说明
- 管道是一种进程通信方式,可以实现进程之间数据传输。
- 管道是半双工,数据只能向一个方向流动,如需双向通信,可以创建起两个管道。
分类
- Linux支持两种管道机制:无名管道(PIPE)和有名管道(FIFO)。
- 区别:
- 有名管道在文件系统中提供了一个以文件形式存在的访问接口与之关联,而无名管道访问接口和数据传输都在内存中完成,其它进程无法访问,因此无名管道只能用于具有亲缘关系的进程之间的通信(父子进程或者兄弟进程之间),实现依赖于进程文件描述符传递,而有名管道可以用于任何进程之间。
无名管道
编程使用
- 创建管道
头文件: #include <unistd.h>
函数定义: int pipe(int filedes[2]);
函数说明: pipe()会建立管道,并将文件描述词由参数filedes数组返回。
filedes[0]为管道的读取端
filedes[1]为管道的写入端。
返回值: 若成功则返回零,否则返回-1,错误原因存于errno中。
错误代码:
EMFILE 进程已用完文件描述词最大量
ENFILE 系统已无文件描述词可用。
EFAULT 参数 filedes 数组地址不合法。
- 管道读/写
* 使用标准IO函数操作
read : 读取
write: 写
- 管道关闭
- 无名管道是使用标准的文件描述符,可以使用close关闭。
- 使用说明
- 在进程中使用fork创建新的进程,新进程会继承老进程中创建的文件描述符,包括创建的无名管道描述符,新老进程即可通过无名管道描述符进行单向通信。
有名管道
适用场景
- 由于管道的单向性,在一些需要双向通信的场景中并不是非常合适,当然可以创建两个管道来实现双向通信,但是比较麻烦,需要自己封装,不如直接采用别的机制。
- 嵌入式app或者服务器app等需要长时间后台运行并且无法中断的程序,可以通过有名管道给app发送一些调试或者控制命令,例如:
- 查询软件版本号,可以直接输出。
- 调试时dump一些运行信息。
- …
- 发送端使用方便,不需要额外实现发送端和创建链接等处理,用完即可关闭,像操作文件一样。
- 可以后台运行,不需要占用输入输出设备,也不需要终止程序运行,之前别的项目中,使用标准输入来获取控制和调试命令,也能实现效果,但是需要占用标准输入,无法后台执行。
- 由于是调试或者控制功能,运行结果可以不用返回给发送端,可以直接打印出来。
编程使用
接口说明
- 创建管道
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
* pathname : 管道文件路径,支持相对和绝对路径。
* mode_t : 访问权限
#include <fcntl.h> /* Definition of AT_* constants */
#include <sys/stat.h>
int mkfifoat(int dirfd, const char *pathname, mode_t mode);
* mkfifoat和mkfifo作用是一样的
* 如果pathname是绝对路径,dirfd将被忽略
* 如果pathname是相对路径,并且dirfd参数是一个打开目录的有效文件描述符,管道路径是相对dirfd打开的目录,而不是程序运行目录。
* 如果pathname是相对路径,并且dirfd是一个特殊值AT_FDCWD,管道路径是相对程序的运行目录,和mkfifo一样。
- 管道的打开/关闭和读/写
* 使用标准IO函数操作
open : 打开
read : 读取
write: 写
close: 关闭管道
- 管道删除
- 管道可以像文件一样删除,例如:使用remove或者unlink函数。
示例
- 由于管道的单向性,大部分开发人员在初次使用管道时会采用发送端以只写(O_WRONLY)的方式打开管道,接收端以只读(O_RDONLY)的方式打开管道,有一些需要注意的地方,如下:
- 采用默认的阻塞模式,以只读方式打开时会阻塞到其他进程以写方式打开管道;以只写方式打开会阻塞到其他进程以读方式打开管道。
- 循环获取输入,每次读取都需要打开管道,读取后再关闭管道,不能一次打开后,循环读取,因为发送端关闭后,接收端未关闭read会一直返回0,发送端再重新打开并且发送数据,接收端也可以接收到。
- 如果采用非阻塞模式(O_NONBLOCK),操作会立即返回,只读打开管道如果没有另外一个进程以写方式打开管道,那么只读打开将返回-1,并将errno设置成ENXIO
* 接收端
while (run){
int fd = open(FIFO_NAME, O_RDONLY);
ret = read(fd, buf, sizeof(buf));
if (ret > 0) {
//input handle
}
close(fd);
}
* 发送端
echo "xxx" > FIFO_NAME
改进方式
- 打开管道时,采用O_RDWR方式,并设置为非阻塞,这样就能只open一次,循环获取输入,并可使用io复用机制,像socket处理一样。
实现原理
- Linux平台上,管道可以看做内存中的文件,它的实现并没有定义专门的数据结构,有名管道是借助了文件系统的file结构和虚拟文件系统(VFS)的索引节点inode,通过将两个file结构指向同一个临时的VFS索引节点,而这个VFS索引节点指向固定大小的物理页面,无名管道类似,只是将file结构换成文件描述符。
- 但是管道又和普通的文件有少许不同,具体表现为:
- 管道的大小(Pipe capacity);管道是一个固定大小的内存缓冲区,不像文件那样可以增长,linux内核2.6.11版本之前为4096字节大小,之后为64KB, 可以通过以下代码确认。
int pipefd[2];
if(pipe(pipefd) < 0)
{
perror("pipe");
return -1;
}
int ret;
int size = 0;
int flags = fcntl(pipefd[1], F_GETFL);
fcntl(pipefd[1], F_SETFL, flags | O_NONBLOCK); // 设置为非阻塞
while (1)
{
ret = write(pipefd[1], "c", 1);
if (ret < 0)
{
perror("write");
break;
}
size++;
}
printf("size=%d\n", size);
* 运行结果
xxxx@chejiser:~$ ./test
write: Resource temporarily unavailable
size=65536
- 写缓冲区原子操作(PIPE_BUF);POSIX.1-2001规定写操作的字节少于PIPE_BUF大小是原子操作,超过PIPE_BUF大小就不一定了,该值可通过ulimit -p查看。
注意项
- 由于是固定的缓冲区大小,写管道时可能变满,当这种情况发生时,随后对管道的write()调用将默认地被阻塞,等待某些数据被读取,以便腾出足够的空间供write()调用写;读取进程也可能工作得比写进程快,当所有当前进程数据已被读取时,管道变空。当这种情况发生时,一个随后的read()调用将默认地被阻塞,等待某些数据被写入,这解决了read()调用返回文件结束的问题。
- 从管道读数据是一次性操作,数据一旦被读,它就从管道中被抛弃,释放空间以便写更多的数据。