Linux下IPC(Interprocess Communication)方式之管道(pipe,fifo)
1. IPC方法
Linux环境下,进程地址空间相互独立,每个进程各自有不同的用户地址空间。任何一个进程的全局变量在另一个进程中都看不到,所以进程和进程之间不能相互访问,要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,InterProcess Communication)。
在进程间完成数据传递需要借助操作系统提供特殊的方法,如:文件、管道、信号、共享内存、消息队列、套接字、命名管道等。
随着计算机的蓬勃发展,一些方法由于自身设计缺陷被淘汰或者弃用。现今常用的进程间通信方式有:
① 管道 (使用最简单)
② 信号 (开销最小)
③ 共享映射区 (无血缘关系)
④ 本地套接字 (最稳定)
2. 管道
2.1 管道的概念
管道是一种最基本的IPC机制,作用于有血缘关系的进程之间,完成数据传递。调用pipe系统函数即可创建一个管道。有如下特质:
1. 其本质是一个伪文件(实为内核缓冲区)
2. 由两个文件描述符引用,一个表示读端,一个表示写端。
3. 规定数据从管道的写端流入管道,从读端流出。
管道的原理: 管道实为内核使用环形队列机制,借助内核缓冲区(4k)实现。
管道的局限性:
① 数据一旦被读走,便不在管道中存在,不可反复读取。
②由于管道采用半双工通信方式。因此,数据只能在一个方向上流动。
③ 只能在有公共祖先(有血缘)的进程间使用管道。
常见的通信方式有,单工通信、半双工通信、全双工通信。
2.2 pipe函数
创建管道:
int pipe(int pipefd[2]);
pipefd 读写文件描述符 0 代表 读, 1 代表 写
返回值 成功返回 0, 失败返回 -1.
函数调用成功返回r
/w
两个文件描述符。无需open
,但需手动close
。规定:fd[0] → r; fd[1] → w,就像0
对应标准输入
,1
对应标准输出
一样。向管道文件读写数据其实是在读写内核缓冲区。
管道创建成功以后,创建该管道的进程(父进程)同时掌握着管道的读端和写端。如何实现父子进程间通信呢?通常可以采用如下步骤:
- 父进程调用
pipe
函数创建管道,得到两个文件描述符fd[0]
、fd[1]
指向管道的读端和写端。 - 父进程调用
fork
创建子进程,那么子进程也有两个文件描述符指向同一管道。 - 父进程关闭管道读端,子进程关闭管道写端。父进程可以向管道中写入数据,子进程将管道中的数据读出。由于管道是利用环形队列实现的,数据从写端流入管道,从读端流出,这样就实现了进程间通信。
2.2.1 父子进程间通信简单举例
#include<stdio.h>
#include<unistd.h>
int main() {
//创建文件描述符
int fd[2];
//创建管道
pipe(fd);
pid_t pid = fork();
if (pid == 0) {
//son
sleep(3);
write(fd[1], "hello", 5);
}
else if (pid > 0) {
//father
//现有缓冲区,然后才能读
char buf[12] = { 0 };
//阻塞等待,哪怕子进程sleep了,也要等
int ret = read(fd[0], buf, sizeof(buf));
//说明读到了
if (ret > 0) {
write(STDOUT_FILEND, buf, ret);//STDOUT_FILEND是1的宏定义
}
}
return 0;
}
运行结果:
2.2.2 父子进程实现pipe通信,实现ps aux | grep bash 功能
由于之前代码我们创建的3父子进程都掌握着管道(pipe)的读写两端,因此有如下结构示意图
出现问题的测试代码:(我们希望子进程写入,父进程读取)
#include<stdio.h>
#include<unistd.h>
int main() {
//创建文件描述符
int fd[2];
//创建管道
pipe(fd);
pid_t pid = fork();
if (pid == 0) {
//son
//son执行ps命令
//1、先重定向
dup2(fd[1], STDOUT_FILENO);//标准输出重定向
//2、execlp NULL是哨兵,告诉命令,后面不会再有参数输入了
execlp("ps","ps","aux", NULL);
}
else if (pid > 0) {
//father
//1、先重定向
dup2(fd[0], STDIN_FILENO);//标准输出重定向
//2、execlp NULL是哨兵,告诉命令,后面不会再有参数输入了
execlp("grep","grep","bash", NULL);
}
return 0;
}
出现的问题:
子进程变成僵尸进程,父进程执行grep
之后一直在等待输入。
分析原因:
grep
命令的特性:等待标准输入,如果你输入的正确,则给你一个反馈。阻塞等待,因为grep
一直认为还有输入,除非是输入端的进程放弃了写入的机会。grep
阻塞等待示例,如图所示
上面代码出现问题,是因为虽然子进程死去了,但是父进程还掌握着管道写入端的句柄,所以grep
一直认为还会有输入,虽然此时读写两端都是父进程掌握。所以我们需要在代码中关闭子进程的读端,以及父进程对于管道的写端。
//关闭 读端
close(fd[0]);
//关闭 写端
close(fd[1]);
在使用管道时,应该把读写两端都规划好。这样数据的流向才稳定。
修正之后的代码:
#include<stdio.h>
#include<unistd.h>
int main() {
//创建文件描述符
int fd[2];
//创建管道
pipe(fd);
pid_t pid = fork();
if (pid == 0) {
//son
//son执行ps命令
//关闭 读端
close(fd[0]);
//1、先重定向
dup2(fd[1], STDOUT_FILENO);//标准输出重定向
//2、execlp NULL是哨兵,告诉命令,后面不会再有参数输入了
execlp("ps","ps","aux", NULL);
}
else if (pid > 0) {
//father
//关闭 写端
close(fd[1]);
//1、先重定向
dup2(fd[0], STDIN_FILENO);//标准输出重定向
//2、execlp NULL是哨兵,告诉命令,后面不会再有参数输入了
execlp("grep","grep","bash", NULL);
}
return 0;
}
运行结果:
2.3 管道的读写行为
读管道
- 写端全部关闭时——read读到0,相当于读到文件末尾
- 写端没有全部关闭时:
1.有数据——read读到数据
2.没有数据——read阻塞fcntl
函数可以更改阻塞为非阻塞
写管道
- 读端全部关闭——产生一个信号
SIGPIPE
,程序异常终止 - 读端未全部关闭时:
1.管道已满——write阻塞
2.管道未满——write正常写入
2.4 管道的大小和优劣
使用命令查看当前系统中创建管道文件所对应的内核缓冲区大小
ulimit -a
优点:简单
缺点:
- 只能有血缘关系之间的进程通信
- 父子进程单方向通信,如果需要双向通信,需要创建多个管道。
2.5 FIFO通信
FIFO常被称为命名管道,以区分管道(pipe)。管道(pipe)只能用于“有血缘关系”的进程间。但通过FIFO,不相关的进程也能交换数据,实现无血缘关系进程的通信。
FIFO是Linux基础文件类型中的一种。但,FIFO文件在磁盘上没有数据块,仅仅用来标识内核中一条通道。各进程可以打开这个文件进行read/write,实际上是在读写内核通道,这样就实现了进程间通信。
-
创建一个管道的伪文件
1.mkfifo myfifo 命令创建
2.也可以用函数int mkfifo(const char *pathname, mode_t mode);
-
内核会为fifo文件开辟一块缓冲区,操作fifo文件,可以操作缓冲区,实现进程间通信——实际上就是文件读写。
-
open函数的注意事项:打开fifo文件的时候,read端会阻塞等待write端open,write端同理,也会阻塞等待另外一端打开。
mkfifo myfifo 命令创建
注意:要放在linux系统文件夹里如果,如果放到windows文件夹会报错:
写端示例
#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<fcntl.h>
#include<string.h>
int main(int argc, char *argv[]) {
if (argc != 2) {
//如果运行时没有输入文件名,则会报错
printf("./a.out fifoname\n");
return -1;
}
//当前目录中存在myfifo文件
//打开fifo文件
int fd = open(argv[1], O_WRONLY);
//写
char buf[256];
int num = 1;
while (1) {
memset(buf, 0x00, sizeof(buf));
sprintf(buf, "xiaoming%04d", num++);
write(fd, buf, strlen(buf));
sleep(1);
}
//关闭描述符
close(fd);
return 0;
}
读端示例
#include<stdio.h>
#include<unistd.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
#include<string.h>
int main(int argc, char *argv[]) {
if (argc != 2) {
//如果运行时没有输入文件名,则会报错
printf("./a.out fifoname\n");
return -1;
}
int fd = open(argv[1], O_RDONLY);
char buf[256];
int ret;
while (1) {
//循环读
ret = read(fd, buf, sizeof(buf));
if (ret > 0) {
printf("read:%s\n", buf);
}
}
//关闭描述符
close(fd);
return 0;
}
读端和写端中的open函数会阻塞,之后对端的open函数也打开了,他才会停止阻塞行为
man 2 open 中有这么一句话应证了: