linux的IPC进程通信-匿名管道

什么是管道

如果你使用过Linux的命令,那么对于管道这个名词你一定不会感觉到陌生,因为我们通常通过符号"|"来使用管道,但是管道的真正定义是什么呢?管道是一个进程连接数据流到另一个进程的通道,它通常是用作把一个进程的输出通过管道连接到另一个进程的输入。

举个例子,在shell中输入命令:ls -l | grep string,我们知道ls命令(其实也是一个进程)会把当前目录中的文件都列出来,但是它不会直接输出,而是把本来要输出到屏幕上的数据通过管道输出到grep这个进程中,作为grep这个进程的输入,然后这个进程对输入的信息进行筛选,把存在string的信息的字符串(以行为单位)打印在屏幕上。

使用popen函数

1.popen函数和pclose函数介绍,函数原型如下(C语言)

#include <stdio.h>
FILE* popen (const char *command, const char *open_mode);
int pclose(FILE *stream_to_close);

poen函数允许一个程序将另一个程序作为新进程来启动,并可以传递数据给它或者通过它接收数据。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调用将等待该进程结束。它返回关闭的文件流所在进程的退出码。

2、例子

很多时候,我们根本就不知道输出数据的长度,为了避免定义一个非常大的数组作为缓冲区,我们可以以块的方式来发送数据,一次读取一个块的数据并发送一个块的数据,直到把所有的数据都发送完。下面的例子就是采用这种方式的数据读取和发送方式。源文件名为popen.c,代码如下:

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h> 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);
}

执行编译

gcc -o popen popen.c

运行结果如下

[root@test test]# ./popen
-rw-r--r-- 1 admin admin 73231 Mar 24 15:52 composer.lock
-rw-r--r-- 1 root root 745 Mar 29 09:59 c_read.c
-rw-r--r-- 1 root root 773 Mar 29 09:58 c_write.c
-rw-r--r-- 1 admin admin 294 Mar 24 18:46 fork2.php
-rw-r--r-- 1 admin admin 225 Mar 24 17:48 fork3.php
-rw-r--r-- 1 root root 1028 Mar 29 11:39 popen.c
-rw-r--r-- 1 root root 566294 Mar 16 15:41 swoole-1.9.8.tgz

从运行结果来看,达到了信息筛选的目的。程序在进程ls中读取数据,再把数据发送到进程grep中进行筛选处理,相当于在shell中直接输入命令:ls -l | grep rwxrwxr-x。

3、popen的实现方式及优缺点

当请求popen调用运行一个程序时,它首先启动shell,即系统中的sh命令,然后将command字符串作为一个参数传递给它。

这样就带来了一个优点和一个缺点。优点是:在Linux中所有的参数扩展都是由shell来完成的。所以在启动程序(command中的命令程序)之前先启动shell来分析命令字符串,也就可以使各种shell扩展(如通配符)在程序启动之前就全部完成,这样我们就可以通过popen启动非常复杂的shell命令。

而它的缺点就是:对于每个popen调用,不仅要启动一个被请求的程序,还要启动一个shell,即每一个popen调用将启动两个进程,从效率和资源的角度看,popen函数的调用比正常方式要慢一些。

pipe调用

如果说popen是一个高级的函数,pipe则是一个底层的调用。与popen函数不同的是,它在两个进程之间传递数据不需要启动一个shell来解释请求命令,同时它还提供对读写数据的更多的控制。

pipe函数的原型如下:

#include <unistd.h>
int pipe(int file_descriptor[2]);

我们可以看到pipe函数的定义非常特别,该函数在数组中墙上两个新的文件描述符后返回0,如果返回返回-1,并设置errno来说明失败原因。

数组中的两个文件描述符以一种特殊的方式连接起来,数据基于先进先出的原则,写到file_descriptor[1]的所有数据都可以从file_descriptor[0]读回来。由于数据基于先进先出的原则,所以读取的数据和写入的数据是一致的。

特别提醒:

1、从函数的原型我们可以看到,它跟popen函数的一个重大区别是,popen函数是基于文件流(FILE)工作的,而pipe是基于文件描述符工作的,所以在使用pipe后,数据必须要用底层的read和write调用来读取和发送。

2、不要用file_descriptor[0]写数据,也不要用file_descriptor[1]读数据,其行为未定义的,但在有些系统上可能会返回-1表示调用失败。数据只能从file_descriptor[0]中读取,数据也只能写入到file_descriptor[1],不能倒过来。

例子:

首先,我们在原先的进程中创建一个管道,然后再调用fork创建一个新的进程,最后通过管道在两个进程之间传递数据。源文件名为pipe.c,代码如下:

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h> int main()
{
int data_processed = 0;
int filedes[2];
const char data[] = "Hello pipe!";
char buffer[BUFSIZ + 1];
pid_t pid;
//清空缓冲区
memset(buffer, '\0', sizeof(buffer)); if(pipe(filedes) == 0)
{
//创建管道成功
//通过调用fork创建子进程
pid = fork();
if(pid == -1)
{
fprintf(stderr, "Fork failure");
exit(EXIT_FAILURE);
}
if(pid == 0)
{
//子进程中
//读取数据
data_processed = read(filedes[0], buffer, BUFSIZ);
printf("Read %d bytes: %s\n", data_processed, buffer);
exit(EXIT_SUCCESS);
}
else
{
//父进程中
//写数据
data_processed = write(filedes[1], data, strlen(data));
printf("Wrote %d bytes: %s\n", data_processed, data);
//休眠2秒,主要是为了等子进程先结束,这样做也只是纯粹为了输出好看而已
//父进程其实没有必要等等子进程结束
sleep(2);
exit(EXIT_SUCCESS);
}
}
exit(EXIT_FAILURE);
}

运行结果

[root@test test]# gcc -o pipe pipe.c
[root@test test]# ./pipe
Wrote 11 bytes: Hello pipe!
Read 11 bytes: Hello pipe!

可见,子进程读取了父进程写到filedes[1]中的数据,如果在父进程中没有sleep语句,父进程可能在子进程结束前结束,这样你可能将看到两个输入之间有一个命令提示符分隔。

把管道用作标准输入和标准输出

下面来介绍一种用管道来连接两个进程的更简洁方法,我们可以把文件描述符设置为一个已知值,一般是标准输入0或标准输出1。这样做最大的好处是可以调用标准程序,即那些不需要以文件描述符为参数的程序。

为了完成这个工作,我们还需要两个函数的辅助,它们分别是dup函数或dup2函数,它们的原型如下

#include <unistd.h>
int dup(int file_descriptor);
int dup2(int file_descriptor_one, int file_descriptor_two);

dup调用创建一个新的文件描述符与作为它的参数的那个已有文件描述符指向同一个文件或管道。对于dup函数而言,新的文件描述总是取最小的可用值。而dup2所创建的新文件描述符或者与int file_descriptor_two相同,或者是第一个大于该参数的可用值。所以当我们首先关闭文件描述符0后调用dup,那么新的文件描述符将是数字0.

例子

在下面的例子中,首先打开管道,然后fork一个子进程,然后在子进程中,使标准输入指向读管道,然后关闭子进程中的读管道和写管道,只留下标准输入,最后调用execlp函数来启动一个新的进程od,但是od并不知道它的数据来源是管道还是终端。父进程则相对简单,它首先关闭读管道,然后在写管道中写入数据,再关闭写管道就完成了它的任务。源文件为pipe2.c,代码如下:

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h> int main()
{
int data_processed = 0;
int pipes[2];
const char data[] = "123";
pid_t pid; if(pipe(pipes) == 0)
{
pid = fork();
if(pid == -1)
{
fprintf(stderr, "Fork failure!\n");
exit(EXIT_FAILURE);
}
if(pid == 0)
{
//子进程中
//使标准输入指向fildes[0]
close(0);
dup(pipes[0]);
//关闭pipes[0]和pipes[1],只剩下标准输入
close(pipes[0]);
close(pipes[1]);
//启动新进程od
execlp("od", "od", "-c", 0);
exit(EXIT_FAILURE);
}
else
{
//关闭pipes[0],因为父进程不用读取数据
close(pipes[0]);
data_processed = write(pipes[1], data, strlen(data));
//写完数据后,关闭pipes[1]
close(pipes[1]);
printf("%d - Wrote %d bytes\n", getpid(), data_processed);
}
}
exit(EXIT_SUCCESS);
}
[root@cp01-mawmd-rd03 test]# ./pipe2
16546 - Wrote 3 bytes
[root@cp01-mawmd-rd03 test]# 0000000 1 2 3
0000003

从运行结果中可以看出od进程正确地完成了它的任务,与在shell中直接输入od -c和123的效果一样。

关于管道关闭后的读操作的讨论

现在有这样一个问题,假如父进程向管道file_pipe[1]写数据,而子进程在管道file_pipe[0]中读取数据,当父进程没有向file_pipe[1]写数据时,子进程则没有数据可读,则子进程会发生什么呢?再者父进程把file_pipe[1]关闭了,子进程又会有什么反应呢?

当写数据的管道没有关闭,而又没有数据可读时,read调用通常会阻塞,但是当写数据的管道关闭时,read调用将会返回0而不是阻塞。注意,这与读取一个无效的文件描述符不同,read一个无效的文件描述符返回-1。

六、匿名管道的缺陷

看了这么多相信大家也知道它的一个缺点,就是通信的进程,它们的关系一定是父子进程的关系,这就使得它的使用受到了一点的限制,但是我们可以使用命名管道来解决这个问题。命名管道将在下一篇文章:Linux进程间通信——使用命名管道中介绍。

引用:

本文转载自http://blog.csdn.net/ljianhui/article/details/10168031

仅供学习使用记录

linux的IPC进程通信方式-匿名管道(一)的更多相关文章

  1. C++ 进程和匿名管道使用学习

    平台 Windows10 + VS2015 学习内容 进程的创建使用(CreateProcess方式) 父子进程间匿名管道通信 相关函数及参数介绍 CreatePipe函数:该的原型为 CreateP ...

  2. Linux进程通信----匿名管道

    Linux进程通信中最为简单的方式是匿名管道 匿名管道的创建需要用到pipe函数,pipe函数参数为一个数组表示的文件描述字.这个数组有两个文件描 述字,第一个是用于读数据的文件描述符第二个是用于写数 ...

  3. linux下的进程通信之管道与FIFO

    概念:管道是由内核管理的一个缓冲区,相当于我们放入内存中的一个纸条.管道的一端连接一个进程的输出.这个进程会向管道中放入信息.管道的另一端连接一个进程的输入,这个进程取出被放入管道的信息. 优点:不需 ...

  4. Linux进程间通信IPC学习笔记之管道

    基础知识: 管道是最初的Unix IPC形式,可追溯到1973年的Unix第3版.使用其应注意两点: 1)没有名字: 2)用于共同祖先间的进程通信: 3)读写操作用read和write函数 #incl ...

  5. linux内核情景分析之匿名管道

    管道的机制由pipe()创建,由pipe()所建立的管道两端都在同一进程.所以必须在fork的配合下,才可以在具有亲缘关系的进程通信 /* * sys_pipe() is the normal C c ...

  6. Linux 系统编程 学习:02-进程间通信1:Unix IPC(1)管道

    Linux 系统编程 学习:02-进程间通信1:Unix IPC(1)管道 背景 上一讲我们介绍了创建子进程的方式.我们都知道,创建子进程是为了与父进程协作(或者是为了执行新的程序,参考 Linux ...

  7. 进程通信类型 管道是Linux支持的最初Unix IPC形式之一 命名管道 匿名管道

    管道 Linux环境进程间通信(一) https://www.ibm.com/developerworks/cn/linux/l-ipc/part1/index.html 管道及有名管道 郑彦兴200 ...

  8. 【网络编程基础】Linux下进程通信方式(共享内存,管道,消息队列,Socket)

    在网络课程中,有讲到Socket编程,对于tcp讲解的环节,为了加深理解,自己写了Linux下进程Socket通信,在学习的过程中,又接触到了其它的几种方式.记录一下. 管道通信(匿名,有名) 管道通 ...

  9. Linux进程通信之匿名管道

    进程间的通信方式 进程间的通信方式包括,管道.共享内存.信号.信号量.消息队列.套接字. 进程间通信的目的 进程间通信的主要目的是:数据传输.数据共享.事件通知.资源共享.进程控制等. 进程间通信之管 ...

随机推荐

  1. Hadoop生态圈-hive编写自定义函数

    Hadoop生态圈-hive编写自定义函数 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任.

  2. Hadoop基础-Hadoop快照管理

    Hadoop基础-Hadoop快照管理 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.快照的作用 快照可以迅速对文件(夹)进行备份,不产生新文件,使用差值存储,默认是禁用状态. ...

  3. Linux 常见文件及目录

    文件/etc//etc/passwd用户基本信息/etc/group用户组基本信息/etc/shadow/etc/passwd 文件的补充/etc/gshadow阴影口令套组中的组配置文件/etc/l ...

  4. [译]Quartz.NET 框架 教程(中文版)2.2.x 之第四课 更多关于Triggers

    第四课 更多关于Triggers 跟作业任务类似,触发器也非常容易使用,但是在你能够充分掌握Quartz之前,你需要知道并理解许多触发器的客户化的参数.前面已经提到过,有许多不同类型的触发器供你选择, ...

  5. linux查看及设置别名,权限,生成ssh秘钥

    1.alias :查看系统中所有的命令别名 2.设定别名 alias 别名='原命令' 3.删除别名 unalias 别名 4.使别名永久生效    vi  ~/.bashrc  写入这个文件中即可永 ...

  6. Linux/Unix系统编程手册 第二章:基本概念

    本章预热与后续系统编程有关的概念. 术语“操作系统”通常包含2种含义:一是指完整的软件包,包括管理计算机资源的核心组件,已经附带的标准软件:二是独指管理硬件的内核. 内核具有诸多概功能,包括: 进程管 ...

  7. Struts2笔记2--动态方法调用和Action接收请求方式

    动态方法调用(在请求的时候,再明确具体的响应方法,配置的时候不明确): LoginAction类中有两个方法some和second 1. 动态方法的调用(修改常量struts.enable.Dynam ...

  8. 64位linux安装32位校园网客户端

    下面的是ubuntu下和arch下的安装方法,ubuntu的转自网络, ubuntu: 下载客户端并解压 安装开发包 1 sudo -i 2 dpkg --add-architecture i386 ...

  9. vue中使用cookie记住用户上次选择(本次例子中为下拉框)

    最近工作中碰到一个需求,添加一条数据时,自动记住上次选择的下拉框的数据,刚开始觉得没思路,后来请教了项目组长,组长直接一句,这不很简单吧,直接用cookie,我:....... 好吧,都王的差不多了, ...

  10. MySQL修改datadir目录

    更改MySQL的数据目录由/usr/local/mysql/data/更换至/data/mysql/ 第一种方法修改my.cnf 1)关闭MySQL [root@test2 ~]# service m ...