前言
管道是UNIX系统IPC 的最古老形式,所有UNIX系统都提供此种通信机制。通常说的管道为匿名管道(pipe),下一篇讲述命名管道(fifo)。
管道有以下两种局限性:
- 历史上,它们是半双工的。现在,某些系统提供全双工管道,但是为了最佳的移植性,我们决不应预先假定系统支持全双工管道。
- 管道只能在具有公共祖先的两个进程(父子进程或兄弟进程)之间使用。
它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。
原型
#include <unistd.h>
int pipe(int fd[2]); // 返回值:若成功返回0,失败返回-1
经由fd返回两个文件描述符:fd[0] 为读而打开,fd[1]为写而打开。fd[1] 输出是 fd[0]的输入。
说明
下图描绘了半双工管道的方法。强调数据需要通过内核在管道中流动。
单个进程中的管道几乎没有任何用处,通常进程会先调用pipe创建管道,接着调用fork,从而创建从父进程到子进程的IPC 通道,反之亦然。如下图:
fork之后做什么取决于想要的数据流向。对于从父进程到子进程的管道,父进程关闭管道的读端(fd[0]),子进程关闭写端(fd[1]),如下图:
反之,对于从子进程到父进程的管道,父进程关闭管道的写端(fd[1]),子进程关闭读端(fd[0])。
当管道的一端关闭后,下列两条规则起作用:
- 当读(read)一个写端已被关闭的管道时,在所有数据都被读取后,read返回0,表示文件结束。
- 如果写(write)一个读端已被关闭的管道,则产生信号SIGPIPE。如果忽略该信号或者捕捉该信号并从其处理程序返回,则write返回-1,errno设置为EPIPE。
实例
#include <stdio.h>
#include <string.h>
#include <unistd.h> //include this header for pipe
int main()
{
printf("this is the main...\n");
int fd[2]; //the pipe needs 2 fds.
if (pipe(fd) < 0) {
printf("create pipe failed.\n");
goto exit;
}
char buff[20] = {0};
pid_t pid = fork();
if (pid < 0) {
printf("fork error\n");
} else if (pid == 0) {
printf("this is child process.\n");
close(fd[0]); //close read
write(fd[1], "hello world", 12);
} else {
printf("this is parent process. pid = %d\n", pid);
close(fd[1]); //close write
read(fd[0], buff, 20);
printf("read from child: %s, length: %d\n", buff, strlen(buff));
}
exit:
return 0;
}
首先通过pipe创建一个管道,在子进程中关闭了读端,在父进程中关闭了写端。描绘出子进程向父进程传输数据的流程。
运行结果:
./test
this is the main...
this is parent process. pid = 5958
this is child process.
read from child: hello world, length: 11
popen 和 pclose
常见的操作是创建一个连接到另一个进程的管道,然后读其输出或向其输入端发送数据,为此,标准I/O库提供了两个函数popen 和pclose。这两个函数实现的操作是:创建一个管道,fork一个子进程,关闭未使用的管道端,执行一个shell 运行命令,然后等到命令终止。
#include <stdio.h>
FILE* popen(const char *command, const char *open_mode);
返回值: 若成功,返回文件指针;若出错,返回NULL
int pclose(FILE *stream_to_close);
返回值:若成功,返回cmdstring的终止状态;若出错,返回-1
popen先执行fork,然后调用exec执行cmdstring,并且返回一个标准的I/O文件指针。如果type是“r”,则文件指针连接到cmdstring的标准输出。如果type 是“w”,则文件指针连接到cmdstring的标准输入。
pclose()函数用于关闭由popen创建出的文件流,等待命令终止,然后返回shell的终止状态。如果shell 不能被执行,则pclose返回的终止状态与shell已执行exit(127)一样。pclose()只在popen启动的进程结束后才返回,如果调用pclose()时被调用进程仍在运行,pclose()调用将等待该进程结束。它返回关闭的文件流所在进程的退出码。
cmdstring 由Bourne shell以下列方式执行:
sh -c cmdstring
这表示shell将扩展cmdstring的任何特殊字符。例如,可以使用:
fp = popen("ls *.c", "r");
或者
fp = popen("cmd 2>&1", "r");
实例:
int main()
{
FILE *read_fp = NULL;
FILE *write_fp = NULL;
char buffer[BUFSIZ + 1];
int chars_read = 0;
// 初始化缓冲区
memset(buffer, '\0', sizeof(buffer));
// 打开ls和grep进程
read_fp = popen("ls -l", "r");
write_fp = popen("grep rwxrwxr-x", "w");
// 两个进程都打开成功
if (read_fp && write_fp)
{
// 读取一个数据块
chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);
while (chars_read > 0)
{
buffer[chars_read] = '\0';
// 把数据写入grep进程
fwrite(buffer, sizeof(char), chars_read, write_fp);
// 还有数据可读,循环读取数据,直到读完所有数据
chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);
}
// 关闭文件流
pclose(read_fp);
pclose(write_fp);
exit(EXIT_SUCCESS);
}
exit(EXIT_FAILURE);
}
代码是查找权限是-rwxrwxr-x 的文件,并列出。结果如下:
-rwxrwxr-x 1 justinwei justinwei 9016 4月 18 16:34 test
-rwxrwxr-x 1 justinwei justinwei 8864 4月 18 16:51 test1
popen 优缺点:
当请求popen()调用运行一个程序时,它首先启动shell,即系统中的sh命令,然后将command字符串作为一个参数传递给它。
这样就带来了一个优点和一个缺点。
优点是:在Linux中所有的参数扩展都是由shell来完成的。所以在启动程序(command中的命令程序)之前先启动shell来分析命令字符串,也就可以使各种shell扩展(如通配符)在程序启动之前就全部完成,这样我们就可以通过popen()启动非常复杂的shell命令。
缺点是:对于每个popen()调用,不仅要启动一个被请求的程序,还要启动一个shell,即每一个popen()调用将启动两个进程,从效率和资源的角度看,popen()函数的调用比正常方式要慢一些。
如果说popen()是一个高级的函数,pipe()则是一个底层的调用。与popen()函数不同的是,它在两个进程之间传递数据不需要启动一个shell来解释请求命令,同时它还提供对读写数据的更多的控制。