管道
一个没有名字的管道只能用于父进程和它 fork 出的子进程之间的通信。
popen
函数用文件流来表示管道,pipe
函数用文件描述符来表示管道。
popen
可以实现通过|
符号将多个无关程序的输入输出串起来。
pipe
用一个int[2]
类型的数组来保存管道的读端[0]
和写端[1]
。
匿名管道是一种很古老的 IPC 形式,其特点是:
- 半双工:虽然父子进程都能用管道收发数据,但是同一时间管道内的数据只能向一个方向流动。就好像一条不限方向但是又只能并行通过一辆车的马路
- 父子进程通信
popen
stdio.h 中关于 popen
和 pclose
的声明如下:
FILE *popen (const char *command, const char *modes);
int pclose (FILE *stream);
popen
函数通过指定一个 shell 命令及其参数列表,在当前进程中启动一个子进程来执行指定的程序。
stdio.h 对 popen
函数的描述是:
Create a new stream connected to a pipe running the given command.
popen
会创建一个文件流,这个文件流被关联到了一个管道,这个管道连接了主进程和执行目标程序的子进程。
因为是用管道实现的,所以主进程可以向子进程发送数据,或者接受子进程发送过来的数据。
发送数据或者接收数据由 popen
的 modes
参数决定,该参数只能是"r"
或者"w"
:
"r"
: 主程序通过管道向子程序发送数据,主程序将数据写入文件流,子程序从文件流中读取数据"w"
: 主程序通过管道读取子程序发送的数据,子程序将数据写入流,主程序从流中读取数据
popen
函数返回的是一个 FILE 类型的指针,因此我们可以通过操纵文件的方式来发送和读取数据。
pclose
函数用来关闭由 popen
函数打开的文件流对象,这是一个安全的关闭方法:它只在子程序执行完毕后才会关闭文件流。
popen 本质上还是调用的系统 shell,通过 shell 的参数解析功能,可以执行参数非常复杂的子程序。与此同时,调用 shell 会在已经启动一个子进程时另外启动一个 shell 进程,即一次 popen 调用会启动两个进程,故其效率和资源较为浪费。
popen
是 stdio 提供的一个相对来说比较高级的函数,高级函数的普遍特点就是:用起来方便,但是会损失掉部分效率和性能。
pipe
相对于 popen
来说,unistd.h 提供了一个更加底层的 pipe
函数来实现管道传递数据:
int pipe(int file_descriptor[2]);
popen
执行的子程序需要启动一个额外的 shell 进程,同时其创建的文件流只能是只读的或者只写的。
pipe
函数没有上面的两个问题:不会启动额外的 shell 进程,可以更加细致的控制数据的读写。
可以预料的是,虽然更加底层的方法效率更高,但是它用起来会更麻烦,需要使用者了解更多的东西。
pipe
函数接收一个长度为 2 的整型数组,这个数组在传入 pipe
函数后会储存代表管道管道两端的文件描述符,同时 pipe
函数会返回管道启动状态:0 表示成功,-1 表示失败。
在使用 pipe
函数启动管道时并没有指定要执行的子程序,这是因为 pipe
函数执行子程序不是通过 shell 方式实现的,而是直接在源代码中指定子程序需要执行的代码。
pipe
会填充二元的文件描述符数组,使数组中的两个文件描述符以一种特殊的方式关联起来:基于先进先出(FIFO)的原则,写入 file_descriptor[1]
的数据可以从 file_descriptor[0]
原样读出。
popen
基于文件流工作,而 pipe
基于文件描述符工作。
按照规范,只能往 file_descriptor[1]
中写入数据,然后从 file_descriptor[0]
中读取数据。
下面是一个简单使用管道的例子:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#define MAX_LEN 128 // 从管道中读取的最大字节数
int main() {
// rwpipe[0] 是管道的读端, rwpipe[1] 是管道的写端
int rwpipe[2] = {};
// 创建管道,用 rwpipe 保存管道的两端
if (pipe(rwpipe) < 0) {
perror("create pipe failed\n");
return -1;
}
// fork 子进程
pid_t pid = fork();
// 返回值小于0说明创建子进程失败
if (pid < 0) {
perror("fork failed\n");
return -1;
}
// 返回值等于0说明下面的程序已经在子进程中执行了
else if (pid == 0) {
// 子进程关闭通信管道的读端,此时子进程只能往管道中写数据
close(rwpipe[0]);
// 子进程往通信管道中写一点东西
char s[] = "hello world";
write(rwpipe[1], s, sizeof(s));
}
// 返回值大于0说明下面的程序会在父进程中执行
else {
// 父进程关闭写端,然后父进程只能从管道中读数据
close(rwpipe[1]);
// 父进程从管道中读取数据
char line[MAX_LEN] = {}; // 读取的内容
int n = read(rwpipe[0], line, MAX_LEN); // 实际读取的字节数,小于MAX_LEN
printf("read %d bytes from pipe: %s\n", n, line);
}
return 0;
}
评论区