Linux进程间通信——使用匿名管道

简介: 在前面,介绍了一种进程间的通信方式:使用信号,我们创建通知事件,并通过它引起响应,但传递的信息只是一个信号值。这里将介绍另一种进程间通信的方式——匿名管道,通过它进程间可以交换更多有用的数据。   一、什么是管道 如果你使用过Linux的命令,那么对于管道这个名词你一定不会感觉到陌生,因为我们通常通过符号“|"来使用管道,但是管理的真正定义是什么呢?管道是一个进程连接数据流到另一个进程的通道,它通常是用作把一个进程的输出通过管道连接到另一个进程的输入。
在前面,介绍了一种进程间的通信方式:使用信号,我们创建通知事件,并通过它引起响应,但传递的信息只是一个信号值。这里将介绍另一种进程间通信的方式——匿名管道,通过它进程间可以交换更多有用的数据。
 
一、什么是管道
如果你使用过Linux的命令,那么对于管道这个名词你一定不会感觉到陌生,因为我们通常通过符号“|"来使用管道,但是管理的真正定义是什么呢?管道是一个进程连接数据流到另一个进程的通道,它通常是用作把一个进程的输出通过管道连接到另一个进程的输入。
 
举个例子,在shell中输入命令:ls -l | grep string,我们知道ls命令(其实也是一个进程)会把当前目录中的文件都列出来,但是它不会直接输出,而是把本来要输出到屏幕上的数据通过管道输出到grep这个进程中,作为grep这个进程的输入,然后这个进程对输入的信息进行筛选,把存在string的信息的字符串(以行为单位)打印在屏幕上。
 
二、使用popen函数
1、popen函数和pclose函数介绍
有静就有动,有开就有关,与此相同,与popen函数相对应的函数是pclose函数,它们的原型如下:
 
[cpp]  view plain copy print ?
 
  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,代码如下:
 
[cpp]  view plain copy print ?
 
  1. #include <unistd.h>  
  2. #include <stdlib.h>  
  3. #include <stdio.h>  
  4. #include <string.h>  
  5.   
  6. int main()  
  7. {  
  8.     FILE *read_fp = NULL;  
  9.     FILE *write_fp = NULL;  
  10.     char buffer[BUFSIZ + 1];  
  11.     int chars_read = 0;  
  12.       
  13.     //初始化缓冲区  
  14.     memset(buffer, '\0', sizeof(buffer));  
  15.     //打开ls和grep进程  
  16.     read_fp = popen("ls -l", "r");  
  17.     write_fp = popen("grep rwxrwxr-x", "w");  
  18.     //两个进程都打开成功  
  19.     if(read_fp && write_fp)  
  20.     {  
  21.         //读取一个数据块  
  22.         chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);  
  23.         while(chars_read > 0)  
  24.         {  
  25.             buffer[chars_read] = '\0';  
  26.             //把数据写入grep进程  
  27.             fwrite(buffer, sizeof(char), chars_read, write_fp);  
  28.             //还有数据可读,循环读取数据,直到读完所有数据  
  29.             chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);  
  30.         }  
  31.         //关闭文件流  
  32.         pclose(read_fp);  
  33.         pclose(write_fp);  
  34.         exit(EXIT_SUCCESS);  
  35.     }  
  36.     exit(EXIT_FAILURE);  
  37. }  
运行结果如下:
从运行结果来看,达到了信息筛选的目的。程序在进程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函数的原型如下:
 
[cpp]  view plain copy print ?
 
  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,代码如下:
 
[cpp]  view plain copy print ?
 
  1. #include <unistd.h>  
  2. #include <stdlib.h>  
  3. #include <stdio.h>  
  4. #include <string.h>  
  5.   
  6. int main()  
  7. {  
  8.     int data_processed = 0;  
  9.     int filedes[2];  
  10.     const char data[] = "Hello pipe!";  
  11.     char buffer[BUFSIZ + 1];  
  12.     pid_t pid;  
  13.     //清空缓冲区  
  14.     memset(buffer, '\0', sizeof(buffer));  
  15.   
  16.     if(pipe(filedes) == 0)  
  17.     {  
  18.         //创建管道成功  
  19.         //通过调用fork创建子进程  
  20.         pid = fork();  
  21.         if(pid == -1)  
  22.         {  
  23.             fprintf(stderr, "Fork failure");  
  24.             exit(EXIT_FAILURE);  
  25.         }  
  26.         if(pid == 0)  
  27.         {  
  28.             //子进程中  
  29.             //读取数据  
  30.             data_processed = read(filedes[0], buffer, BUFSIZ);  
  31.             printf("Read %d bytes: %s\n", data_processed, buffer);  
  32.             exit(EXIT_SUCCESS);  
  33.         }  
  34.         else  
  35.         {  
  36.             //父进程中  
  37.             //写数据  
  38.             data_processed = write(filedes[1], data, strlen(data));  
  39.             printf("Wrote %d bytes: %s\n", data_processed, data);  
  40.             //休眠2秒,主要是为了等子进程先结束,这样做也只是纯粹为了输出好看而已  
  41.             //父进程其实没有必要等等子进程结束  
  42.             sleep(2);  
  43.             exit(EXIT_SUCCESS);  
  44.         }  
  45.     }  
  46.     exit(EXIT_FAILURE);  
  47. }  
运行结果为:
 
可见,子进程读取了父进程写到filedes[1]中的数据,如果在父进程中没有sleep语句,父进程可能在子进程结束前结束,这样你可能将看到两个输入之间有一个命令提示符分隔。
 
四、把管道用作标准输入和标准输出
下面来介绍一种用管道来连接两个进程的更简洁方法,我们可以把文件描述符设置为一个已知值,一般是标准输入0或标准输出1。这样做最大的好处是可以调用标准程序,即那些不需要以文件描述符为参数的程序。
 
为了完成这个工作,我们还需要两个函数的辅助,它们分别是dup函数或dup2函数,它们的原型如下
 
[cpp]  view plain copy print ?
 
  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,代码如下:
 
[cpp]  view plain copy print ?
 
  1. #include <unistd.h>  
  2. #include <stdlib.h>  
  3. #include <stdio.h>  
  4. #include <string.h>  
  5.   
  6. int main()  
  7. {  
  8.     int data_processed = 0;  
  9.     int pipes[2];  
  10.     const char data[] = "123";  
  11.     pid_t pid;  
  12.   
  13.     if(pipe(pipes) == 0)  
  14.     {  
  15.         pid = fork();  
  16.         if(pid == -1)  
  17.         {  
  18.             fprintf(stderr, "Fork failure!\n");  
  19.             exit(EXIT_FAILURE);  
  20.         }  
  21.         if(pid == 0)  
  22.         {  
  23.             //子进程中  
  24.             //使标准输入指向fildes[0]  
  25.             close(0);  
  26.             dup(pipes[0]);  
  27.             //关闭pipes[0]和pipes[1],只剩下标准输入  
  28.             close(pipes[0]);  
  29.             close(pipes[1]);  
  30.             //启动新进程od  
  31.             execlp("od", "od", "-c", 0);  
  32.             exit(EXIT_FAILURE);  
  33.         }  
  34.         else  
  35.         {  
  36.             //关闭pipes[0],因为父进程不用读取数据  
  37.             close(pipes[0]);  
  38.             data_processed = write(pipes[1], data, strlen(data));  
  39.             //写完数据后,关闭pipes[1]  
  40.             close(pipes[1]);  
  41.             printf("%d - Wrote %d bytes\n", getpid(), data_processed);  
  42.         }  
  43.     }  
  44.     exit(EXIT_SUCCESS);  
  45. }  
运行结果为:
 
从运行结果中可以看出od进程正确地完成了它的任务,与在shell中直接输入od -c和123的效果一样。
 
五、关于管道关闭后的读操作的讨论
现在有这样一个问题,假如父进程向管道file_pipe[1]写数据,而子进程在管道file_pipe[0]中读取数据,当父进程没有向file_pipe[1]写数据时,子进程则没有数据可读,则子进程会发生什么呢?再者父进程把file_pipe[1]关闭了,子进程又会有什么反应呢?
 
当写数据的管道没有关闭,而又没有数据可读时,read调用通常会阻塞,但是当写数据的管道关闭时,read调用将会返回0而不是阻塞。注意,这与读取一个无效的文件描述符不同,read一个无效的文件描述符返回-1。
 
六、匿名管道的缺陷
看了这么多相信大家也知道它的一个缺点,就是通信的进程,它们的关系一定是父子进程的关系,这就使得它的使用受到了一点的限制,但是我们可以使用命名管道来解决这个问题。命名管道将在下一篇文章: Linux进程间通信——使用命名管道中介绍。
目录
相关文章
|
13天前
|
缓存 监控 Linux
linux进程管理万字详解!!!
本文档介绍了Linux系统中进程管理、系统负载监控、内存监控和磁盘监控的基本概念和常用命令。主要内容包括: 1. **进程管理**: - **进程介绍**:程序与进程的关系、进程的生命周期、查看进程号和父进程号的方法。 - **进程监控命令**:`ps`、`pstree`、`pidof`、`top`、`htop`、`lsof`等命令的使用方法和案例。 - **进程管理命令**:控制信号、`kill`、`pkill`、`killall`、前台和后台运行、`screen`、`nohup`等命令的使用方法和案例。
44 4
linux进程管理万字详解!!!
|
4天前
|
存储 运维 监控
深入Linux基础:文件系统与进程管理详解
深入Linux基础:文件系统与进程管理详解
41 8
|
13天前
|
存储 Unix Linux
进程间通信方式-----管道通信
【10月更文挑战第29天】管道通信是一种重要的进程间通信机制,它为进程间的数据传输和同步提供了一种简单有效的方法。通过合理地使用管道通信,可以实现不同进程之间的协作,提高系统的整体性能和效率。
|
13天前
|
算法 Linux 定位技术
Linux内核中的进程调度算法解析####
【10月更文挑战第29天】 本文深入剖析了Linux操作系统的心脏——内核中至关重要的组成部分之一,即进程调度机制。不同于传统的摘要概述,我们将通过一段引人入胜的故事线来揭开进程调度算法的神秘面纱,展现其背后的精妙设计与复杂逻辑,让读者仿佛跟随一位虚拟的“进程侦探”,一步步探索Linux如何高效、公平地管理众多进程,确保系统资源的最优分配与利用。 ####
46 4
|
13天前
|
缓存 负载均衡 算法
Linux内核中的进程调度算法解析####
本文深入探讨了Linux操作系统核心组件之一——进程调度器,着重分析了其采用的CFS(完全公平调度器)算法。不同于传统摘要对研究背景、方法、结果和结论的概述,本文摘要将直接揭示CFS算法的核心优势及其在现代多核处理器环境下如何实现高效、公平的资源分配,同时简要提及该算法如何优化系统响应时间和吞吐量,为读者快速构建对Linux进程调度机制的认知框架。 ####
|
15天前
|
消息中间件 存储 Linux
|
21天前
|
运维 Linux
Linux查找占用的端口,并杀死进程的简单方法
通过上述步骤和命令,您能够迅速识别并根据实际情况管理Linux系统中占用特定端口的进程。为了获得更全面的服务器管理技巧和解决方案,提供了丰富的资源和专业服务,是您提升运维技能的理想选择。
23 1
|
1月前
|
算法 Linux 调度
深入理解Linux操作系统的进程管理
【10月更文挑战第9天】本文将深入浅出地介绍Linux系统中的进程管理机制,包括进程的概念、状态、调度以及如何在Linux环境下进行进程控制。我们将通过直观的语言和生动的比喻,让读者轻松掌握这一核心概念。文章不仅适合初学者构建基础,也能帮助有经验的用户加深对进程管理的理解。
21 1
|
1月前
|
消息中间件 Linux API
Linux c/c++之IPC进程间通信
这篇文章详细介绍了Linux下C/C++进程间通信(IPC)的三种主要技术:共享内存、消息队列和信号量,包括它们的编程模型、API函数原型、优势与缺点,并通过示例代码展示了它们的创建、使用和管理方法。
30 0
Linux c/c++之IPC进程间通信
|
3天前
|
Linux
在 Linux 系统中,“cd”命令用于切换当前工作目录
在 Linux 系统中,“cd”命令用于切换当前工作目录。本文详细介绍了“cd”命令的基本用法和常见技巧,包括使用“.”、“..”、“~”、绝对路径和相对路径,以及快速切换到上一次工作目录等。此外,还探讨了高级技巧,如使用通配符、结合其他命令、在脚本中使用,以及实际应用案例,帮助读者提高工作效率。
19 3