Linux/Unix系统IPC是各种进程间通信方式的统称,但是其中极少能在所有Linux/Unix系统实现中进行移植。随着POSIX和Open Group(X/Open)标准化的推进呵护影响的扩大,情况虽已得到改善,但差别仍然存在。一般来说,Linux/Unix常见的进程间通信方式有:管道、消息队列、信号、信号量、共享内存、套接字等。博主将在《进程间通信方式总结》系列博文中和大家一起探讨学习进程间通信的方式,并对其进行总结,让我们共同度过这段学习的美好时光。这里我们就以其中一种方式管道展开探讨,管道是Linux/Unix系统IPC的最古老形式,并且所有Linux/Unix系统都提供此种通信机制。管道又分为无名管道和有名管道,无名管道只能用于有亲缘关系的进程间通信,有名管道则可以用于非亲缘进程间的通信。本篇就以管道中的无名管道(又称匿名管道)为例,希望对你有所帮助。
popen
如果你使用过Linux的命令,那么对于管道这个名词你一定不会感觉到陌生,因为我们通常通过符号“|"来使用管道,但是管理的真正定义是什么呢?管道是一个进程连接数据流到另一个进程的通道,它通常是用来把一个进程的输出通过管道连接到另一个进程的输入。
FILE* popen (const char *command, const char *open_mode);
int pclose(FILE *stream_to_close);
popen函数允许一个程序将另一个程序作为新进程来启动,并可以传递数据给它或者通过它接收数据。command是要运行的程序名和相应的参数。open_mode只能是"r(只读)"和"w(只写)"其中之一。注意,popen函数的返回值是一个FILE类型的指针,而Linux把一切都视为文件,也就是说我们可以使用stdio I/O库中的文件处理函数来对其进行操作。
如果open_mode是"r",主调用程序就可以使用被调用程序的输出,通过函数返回的FILE指针,就可以通过stdio函数(如fread)来读取程序的输出;如果open_mode是"w",主调用程序就可以向被调用程序发送数据,即通过stdio函数(如fwrite)向被调用程序写数据,而被调用程序就可以在自己的标准输入中读取这些数据。
pclose函数用于关闭由popen创建出的关联文件流。pclose只在popen启动的进程结束后才返回,如果调用pclose时被调用进程仍在运行,pclose调用将等待该进程结束。它返回关闭的文件流所在进程的退出码。说了这么多,没有使用过popen的小伙伴可能已经晕了,下面我们就来举一个简单的栗子吧。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define BUFSIZE 1024
//标准流管道
int main()
{
//定义文件指针
FILE *fpr = NULL;
FILE *fpw = NULL;
//保存要执行的命令串
const char *cmdr = "ps x";
const char *cmdw = "grep pts";
//定义缓存区并初始化
char buf[BUFSIZE] = {0};
memset(buf, 0x00, BUFSIZE);
//创建标准流管道,将执行命令的结果装入管道
//参数1:执行的命令字符串
//参数2:打开的形式(只识别第一个字符)
//返回文件指针
if (NULL == (fpr = popen(cmdr, "r")))perror("popen"), exit(EXIT_FAILURE);
if (NULL == (fpw = popen(cmdw, "w")))perror("popen"), exit(EXIT_FAILURE);
int readlen = 0;
//从管道中读取数据存入buf中
while ((readlen = fread(buf, sizeof(char), BUFSIZE, fpr)) > 0)
{
buf[readlen] = '\0';
//将buf中的数据写入fpw对应的管道中,作为cmdw命令的输入数据
fwrite(buf, sizeof(char), readlen, fpw);
}
//关闭文件指针
pclose(fpr);
pclose(fpw);
return 0;
}
程序运行结果:
在程序中,popen(cmdr,"r")将cmdr命令执行的结果写入匿名管道,后面通过fpr从该匿名管道中读取数据。popen(cmdw, "w"),fpw将数据写入管道,作为cmdw命令执行的数据输入。从程序执行和“ps x | grep pts”执行结果的对比,我们发现他们的执行结果完全一样。相信你现在已经理解并掌握了popen的用法,下面我们来看看另一个无名管道的使用方式pipe。
pipe
int pipe(int fileds[2]);
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
intmain()
{
int fds[2] = {};
char buf[256] = "";
//创建匿名管道(只能在父子进程或亲缘进程之间进行通信)
if (-1 == pipe(fds)) strerror(errno),exit(1);
//创建进程
pid_t pid = fork();
if (-1 == pid) perror("fork:"),exit(1);
//子进程
if (0 == pid)
{
//关闭管道读描述符
close(fds[0]);
fprintf(stdout, "--------子进程从终端读取数据,并将读取数据输入管道中-------\n");
//从标准输入读取数据
fscanf(stdin, "%[^\n]", buf);
fscanf(stdin, "%*c");
//向管道中写入数据
write(fds[1], buf, sizeof(buf));
//关闭管道的写描述符
close(fds[1]);
}
else//父进程
{
//关闭管道的写描述符
close(fds[1]);
char buf[256] = "";
//从管道中读取数据
read(fds[0], buf, sizeof(buf));
fprintf(stdout, "--------父进程从管道中读取数据,并输出到终端--------\n");
fprintf(stdout, "%s\n", buf);
//关闭管道的读描述符
close(fds[0]);
}
return 0;
}
程序运行结果:
下面来介绍一种用管道来连接两个进程的更简洁方法,我们可以把文件描述符设置为一个已知值,一般是标准输入0或标准输出1。这样做最大的好处是可以调用标准程序,即那些不需要以文件描述符为参数的程序。
int dup(int file_descriptor);
int dup2(int file_descriptor_one, int file_descriptor_two);
dup调用创建一个新的文件描述符与作为它的参数的那个已有文件描述符指向同一个文件或管道。对于dup函数而言,新的文件描述总是取最小的可用值。而dup2所创建的新文件描述符或者与int file_descriptor_two相同,或者是第一个大于该参数的可用值。也就是说dup是从0开始找第一个可用的文件描述符,dup2则是从file_descriptor_two开始找第一个可用的文件描述符,此外dup2会先关闭file_descriptor_two指定的文件描述符。是不是感觉很晕哈,初次使用dup理解起来是要费点劲,让我们看个栗子先。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
/*
*
*子进程将ls输出到管道,父进程从管道中读取数据并显示到标准输出端
*
*
*/
int main()
{
int fds[2] = {};
//创建匿名管道(只能在父子进程或亲缘进程之间进行通信)
if (-1 == pipe(fds)) fprintf(stdout, “pipe:%s”, strerror(errno)),exit(1);
//创建进程
pid_t pid = vfork();
if (-1 == pid) perror("fork:"), exit(1);
//子进程
//子进程会复制父进程已打开的文件描述符
if (0 == pid)
{
//关闭管道读描述符
close(fds[0]);
//将标准输出与管道写入关联
//close(1)
//dup(fds[1]);
//此语句与上面两句等价
dup2(fds[1], 1);
//ls输出到标准输出端的数据实际输出到管道中
execlp("ls", "ls", "-l", NULL);
//关闭管道写描述符
close(fds[1]);
}
else//父进程
{
//sleep(5);
//父进程
//关闭管道写描述符
close(fds[1]);
//将标准输入与管道读入关联
dup2(fds[0], 0);
//wc从标准输入端读取数据,也就是从管道中读取数据
execlp("wc", "wc", "-l", NULL);
//关闭管道读描述符
close(fds[0]);
}
return 0;
}
程序运行结果:
程序的执行结果和“ls –l| wc –l”执行结果完全一致,从上面的程序中我们知道,dup2和dup的使用。在程序中,子进程将“ls -l”的执行数据输出,原本应该输出到标准输出,但由于dup2将标准输出和fds[1]关联,也就是将输出到标准输出的数据实际上都输入到管道中;符进程则将fds[0]和标准输入相关联,fds[0]从管道中读取fds[1]写入的数据,实际上就是标准输入读取fds[1]写入管道中的数据。在父进程中调用execlp函数替换当前进程,执行”wc -l”命令,而wc需要数据输入,这里刚好就是fds[1]写入管道中的数据,从fds[0]读取管道数据作为”wc -l”的输入。哈哈,是不是很easy啊,很开心吧,你又学会了一项新技能哦,离大牛更近了一步咯。
关于管道中的无名管道的学习我们就到此结束了,相信大家都有所收获,希望小伙伴们都已经理解并掌握了无名管道的常用方法。下一篇我们将继续总结管道,在那里我们将一起学习有名管道的常用方式。当然,如果你觉得对进程间通信的方式不胜了解,还有些许疑惑,请关注博主《进程间通信方式总结》系列博文,相信你在那里能找到答案。