在前面,介绍了一种进程间的通信方式:使用信号,我们创建通知事件,并通过它引起响应,但传递的信息只是一个信号值。这里将介绍另一种进程间通信的方式——匿名管道,通过它进程间可以交换更多有用的数据。
 
一、什么是管道
如果你使用过Linux的命令,那么对于管道这个名词你一定不会感觉到陌生,因为我们通常通过符号“|"来使用管道,但是管理的真正定义是什么呢?管道是一个进程连接数据流到另一个进程的通道,它通常是用作把一个进程的输出通过管道连接到另一个进程的输入。
 
举个例子,在shell中输入命令:ls -l | grep string,我们知道ls命令(其实也是一个进程)会把当前目录中的文件都列出来,但是它不会直接输出,而是把本来要输出到屏幕上的数据通过管道输出到grep这个进程中,作为grep这个进程的输入,然后这个进程对输入的信息进行筛选,把存在string的信息的字符串(以行为单位)打印在屏幕上。
 
二、使用popen函数
1、popen函数和pclose函数介绍
有静就有动,有开就有关,与此相同,与popen函数相对应的函数是pclose函数,它们的原型如下:
 
  1. #include <stdio.h>
  2. FILE* popen (const char *command, const char *open_mode);
  3. 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,代码如下:
 
  1. #include <unistd.h>
  2. #include <stdlib.h>
  3. #include <stdio.h>
  4. #include <string.h>
  5. int main()
  6. {
  7. FILE *read_fp = NULL;
  8. FILE *write_fp = NULL;
  9. char buffer[BUFSIZ + 1];
  10. int chars_read = 0;
  11. //初始化缓冲区
  12. memset(buffer, '\0', sizeof(buffer));
  13. //打开ls和grep进程
  14. read_fp = popen("ls -l", "r");
  15. write_fp = popen("grep rwxrwxr-x", "w");
  16. //两个进程都打开成功
  17. if(read_fp && write_fp)
  18. {
  19. //读取一个数据块
  20. chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);
  21. while(chars_read > 0)
  22. {
  23. buffer[chars_read] = '\0';
  24. //把数据写入grep进程
  25. fwrite(buffer, sizeof(char), chars_read, write_fp);
  26. //还有数据可读,循环读取数据,直到读完所有数据
  27. chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);
  28. }
  29. //关闭文件流
  30. pclose(read_fp);
  31. pclose(write_fp);
  32. exit(EXIT_SUCCESS);
  33. }
  34. exit(EXIT_FAILURE);
  35. }
运行结果如下:
从运行结果来看,达到了信息筛选的目的。程序在进程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函数的原型如下:
 
  1. #include <unistd.h>
  2. 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,代码如下:
 
  1. #include <unistd.h>
  2. #include <stdlib.h>
  3. #include <stdio.h>
  4. #include <string.h>
  5. int main()
  6. {
  7. int data_processed = 0;
  8. int filedes[2];
  9. const char data[] = "Hello pipe!";
  10. char buffer[BUFSIZ + 1];
  11. pid_t pid;
  12. //清空缓冲区
  13. memset(buffer, '\0', sizeof(buffer));
  14. if(pipe(filedes) == 0)
  15. {
  16. //创建管道成功
  17. //通过调用fork创建子进程
  18. pid = fork();
  19. if(pid == -1)
  20. {
  21. fprintf(stderr, "Fork failure");
  22. exit(EXIT_FAILURE);
  23. }
  24. if(pid == 0)
  25. {
  26. //子进程中
  27. //读取数据
  28. data_processed = read(filedes[0], buffer, BUFSIZ);
  29. printf("Read %d bytes: %s\n", data_processed, buffer);
  30. exit(EXIT_SUCCESS);
  31. }
  32. else
  33. {
  34. //父进程中
  35. //写数据
  36. data_processed = write(filedes[1], data, strlen(data));
  37. printf("Wrote %d bytes: %s\n", data_processed, data);
  38. //休眠2秒,主要是为了等子进程先结束,这样做也只是纯粹为了输出好看而已
  39. //父进程其实没有必要等等子进程结束
  40. sleep(2);
  41. exit(EXIT_SUCCESS);
  42. }
  43. }
  44. exit(EXIT_FAILURE);
  45. }
运行结果为:
 
可见,子进程读取了父进程写到filedes[1]中的数据,如果在父进程中没有sleep语句,父进程可能在子进程结束前结束,这样你可能将看到两个输入之间有一个命令提示符分隔。
 
四、把管道用作标准输入和标准输出
下面来介绍一种用管道来连接两个进程的更简洁方法,我们可以把文件描述符设置为一个已知值,一般是标准输入0或标准输出1。这样做最大的好处是可以调用标准程序,即那些不需要以文件描述符为参数的程序。
 
为了完成这个工作,我们还需要两个函数的辅助,它们分别是dup函数或dup2函数,它们的原型如下
 
  1. #include <unistd.h>
  2. int dup(int file_descriptor);
  3. 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,代码如下:
 
  1. #include <unistd.h>
  2. #include <stdlib.h>
  3. #include <stdio.h>
  4. #include <string.h>
  5. int main()
  6. {
  7. int data_processed = 0;
  8. int pipes[2];
  9. const char data[] = "123";
  10. pid_t pid;
  11. if(pipe(pipes) == 0)
  12. {
  13. pid = fork();
  14. if(pid == -1)
  15. {
  16. fprintf(stderr, "Fork failure!\n");
  17. exit(EXIT_FAILURE);
  18. }
  19. if(pid == 0)
  20. {
  21. //子进程中
  22. //使标准输入指向fildes[0]
  23. close(0);
  24. dup(pipes[0]);
  25. //关闭pipes[0]和pipes[1],只剩下标准输入
  26. close(pipes[0]);
  27. close(pipes[1]);
  28. //启动新进程od
  29. execlp("od", "od", "-c", 0);
  30. exit(EXIT_FAILURE);
  31. }
  32. else
  33. {
  34. //关闭pipes[0],因为父进程不用读取数据
  35. close(pipes[0]);
  36. data_processed = write(pipes[1], data, strlen(data));
  37. //写完数据后,关闭pipes[1]
  38. close(pipes[1]);
  39. printf("%d - Wrote %d bytes\n", getpid(), data_processed);
  40. }
  41. }
  42. exit(EXIT_SUCCESS);
  43. }
运行结果为:
 
从运行结果中可以看出od进程正确地完成了它的任务,与在shell中直接输入od -c和123的效果一样。
 
五、关于管道关闭后的读操作的讨论
现在有这样一个问题,假如父进程向管道file_pipe[1]写数据,而子进程在管道file_pipe[0]中读取数据,当父进程没有向file_pipe[1]写数据时,子进程则没有数据可读,则子进程会发生什么呢?再者父进程把file_pipe[1]关闭了,子进程又会有什么反应呢?
 
当写数据的管道没有关闭,而又没有数据可读时,read调用通常会阻塞,但是当写数据的管道关闭时,read调用将会返回0而不是阻塞。注意,这与读取一个无效的文件描述符不同,read一个无效的文件描述符返回-1。
 
六、匿名管道的缺陷
看了这么多相信大家也知道它的一个缺点,就是通信的进程,它们的关系一定是父子进程的关系,这就使得它的使用受到了一点的限制,但是我们可以使用命名管道来解决这个问题。命名管道将在下一篇文章:Linux进程间通信——使用命名管道中介绍。

Linux进程间通信——使用匿名管道的更多相关文章

  1. Linux进程间通信——使用命名管道

    在前一篇文章——Linux进程间通信——使用匿名管道中,我们看到了如何使用匿名管道来在进程之间传递数据,同时也看到了这个方式的一个缺陷,就是这些进程都由一个共同的祖先进程启动,这给我们在不相关的的进程 ...

  2. 练习--LINUX进程间通信之无名管道PIPE

    IBM上放的这个系统不错,刚好可以系统回温一下LINUX的系统知识. http://www.ibm.com/developerworks/cn/linux/l-ipc/part1/ 感觉年纪大了,前几 ...

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

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

  4. 《Linux 进程间通信》命名管道:FIFO

    命名管道的主要用途:不相关的进程之间交换数据. 命令行上创建命名管道: $ mkfifo filename  程序中创建命名管道: #include <sys/types.h> #incl ...

  5. Linux进程间通信(四):命名管道 mkfifo()、open()、read()、close()

    在前一篇文章—— Linux进程间通信 -- 使用匿名管道 中,我们看到了如何使用匿名管道来在进程之间传递数据,同时也看到了这个方式的一个缺陷,就是这些进程都由一个共同的祖先进程启动,这给我们在不相关 ...

  6. IPC——匿名管道

    Linux进程间通信——使用匿名管道 在前面,介绍了一种进程间的通信方式:使用信号,我们创建通知事件,并通过它引起响应,但传递的信息只是一个信号值.这里将介绍另一种进程间通信的方式——匿名管道,通过它 ...

  7. Linux进程间通信(三):匿名管道 popen()、pclose()、pipe()、close()、dup()、dup2()

    在前面,介绍了一种进程间的通信方式:使用信号,我们创建通知事件,并通过它引起响应,但传递的信息只是一个信号值.这里将介绍另一种进程间通信的方式——匿名管道,通过它进程间可以交换更多有用的数据. 一.什 ...

  8. linux的IPC进程通信方式-匿名管道(一)

    linux的IPC进程通信-匿名管道 什么是管道 如果你使用过Linux的命令,那么对于管道这个名词你一定不会感觉到陌生,因为我们通常通过符号"|"来使用管道,但是管道的真正定义是 ...

  9. Linux进程间通信(七):消息队列 msgget()、msgsend()、msgrcv()、msgctl()

    下面来说说如何用不用消息队列来进行进程间的通信,消息队列与命名管道有很多相似之处.有关命名管道的更多内容可以参阅我的另一篇文章:Linux进程间通信 -- 使用命名管道 一.什么是消息队列 消息队列提 ...

随机推荐

  1. javascript之Error

    一.Error()构造函数 构造函数:new Error(); new Error(message); 二.Error.message //人类可读的错误消息 语法:error.message; 三. ...

  2. 【转】ARM交叉编译工具链

    原文网址:http://www.veryarm.com/cross-tools 为什么要用交叉编译器? 交叉编译通俗地讲就是在一种平台上编译出能运行在体系结构不同的另一种平台上的程序,比如在PC平台( ...

  3. codecomb 2090【最小乘积路】

    题目描述 给定n个点的带权有向图,求从1到n的路径中边权之积最小的简单路径. 输入格式 第一行读入两个整数n,m,表示共n个点m条边. 接下来m行,每行三个正整数x,y,z,表示点x到点y有一条边权为 ...

  4. 学生管理系统(list)

    学生管理系统:学习了一点文件指针的操作和链表操作,以前总想搞下子,刚好碰到同学要做这个,自己瞎搞了一通. 实现功能:数据添加,查找,删除,插入,修改只是在查找加几句就没写. #include < ...

  5. 【POJ 1236 Network of Schools】强联通分量问题 Tarjan算法,缩点

    题目链接:http://poj.org/problem?id=1236 题意:给定一个表示n所学校网络连通关系的有向图.现要通过网络分发软件,规则是:若顶点u,v存在通路,发给u,则v可以通过网络从u ...

  6. rnqoj-49-加分二叉树-(区域动归+记忆化)

    区域动归的问题 #include<stdio.h> #include<string.h> #include<iostream> #include<algori ...

  7. OpenGL进阶(十一) - GLSL4.x中的数据传递

    in out 对于 vertex shader,每个顶点都会包含一次,它的主要工作时处理关于定点的数据,然后把结果传递到管线的下个阶段. 以前版本的GLSL,数据会通过一些内建变量,比如gl_Vert ...

  8. 【转】Linux内核调试方法总结

    目录[-] 一  调试前的准备 二  内核中的bug 三  内核调试配置选项 1  内核配置 2  调试原子操作 四  引发bug并打印信息 1  BUG()和BUG_ON() 2  dump_sta ...

  9. 通过项目逐步深入了解Mybatis<四>

    延迟加载 什么是延迟加载? resultMap可以实现高级映射(使用association.collection实现一对一及一对多映射),association.collection具备延迟加载功能. ...

  10. java学习笔记day06---匿名内部类

    1.匿名内部类:其实就是内部类的简化形式,它所体现的就是一个类或者接口的子类对象.前提:     内部类必须继承或实现外部类或接口. 格式:    new 父类&接口(){};    其实就是 ...