进程间的通信—管道

管道

  • 进程间的通信(IPC-Inter-Process Communication)有多种方式,管道是其中最基本的方式。
  • 管道是半双工的,即是单向的。
  • 管道是FIFO(先进先出)的。
  • 在实际的多进程间通信时,可以理解为有一条管道,而每个进程都有两个可以使用管道的"端口",分别负责进行数据的读取与发送。
  • 单进程中的管道:int fd[2]
  • 使用文件描述符fd[1],向管道写数据。
  • 使用文件描述符fd[0],从管道中读数据。

  • 注意:
  • 单进程中的管道无实际用处管道用于多进程间通信

管道的创建

  • 函数原型: int pipe(int pipefd[2]);
  • 返回值:

    • 成功:返回0。
    • 失败:返回-1。
  • 注意:

    • 获取两个"文件描述符",分别对应管道的读端和写端。
    • fd[0]:为管道的读端;
    • fd[1]:为管道的写端;
    • 如果对fd[0]进行写操作,对fd[1]进行读操作,可能会导致不可预期的错误。

管道的使用

实例1: 单进程使用管道进行通信

  • 注意:创建管道后,获得该管道的两个文件描述符,不需要使用普通文件操作中的open操作。如下图所示:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>int main(void)
{int fd[2];int ret;char buff1[1024];char buff2[1024];ret = pipe(fd);if (ret !=0) {printf("create pipe failed!\n");exit(1);}strcpy(buff1, "Hello!");write(fd[1], buff1, strlen(buff1)); //写进去一个helloprintf("send information:%s\n", buff1);bzero(buff2, sizeof(buff2));read(fd[0], buff2, sizeof(buff2));//读出来helloprintf("received information:%s\n", buff2);return 0;
}


实例2: 多进程使用管道进行通信

  • 注意:创建管道之后,再创建子进程,此时一共有4个文件描述符,4个端口,父子进程分别有一个读端口和一个写端口,如下图所示:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>int main(void)
{int fd[2];int ret;char buff1[1024];char buff2[1024];pid_t pd;ret = pipe(fd);if (ret !=0) {printf("create pipe failed!\n");exit(1);}pd = fork();if (pd == -1) {printf("fork error!\n");exit(1);} else if (pd == 0) {//子进程先读在写bzero(buff2, sizeof(buff2));read(fd[0], buff2, sizeof(buff2));//read在没收到数据时会阻塞printf("process(%d) received information:%s,buff2's address:%p\n", getpid(), buff2,buff2);sleep(5);strcpy(buff1, "Hello Dad!");write(fd[1], buff1, strlen(buff1)); } else {//父进程先写再读strcpy(buff1, "Hello Kid");write(fd[1], buff1, strlen(buff1)); sleep(5);bzero(buff2, sizeof(buff2));read(fd[0], buff2, sizeof(buff2));printf("process(%d) received information:%s,buff2's address:%p\n", getpid(), buff2,buff2);}if (pd > 0) {wait();}return 0;
}

  • 注意: 可以看到,我们在父子进程中都打印了buff2的地址,发现打印出来的(虚拟)地址是相同的,但是,内容却不一样,一个是hello kid,一个是hello dad,实际上,是两个不同的地址
  • 在调用fork()函数创建子进程后,子进程会将父进程的所有资源都复制一遍

实例3: 子进程使用execl启动新程序时管道的使用

  • 功能详情:有两个程序p1与p2,二者使用管道进行通信,p1给p2发送一个字符,p2收到后打印到屏幕上。
  • 具体操作流程:
  • p1
    • 创建管道。
    • 创建子进程。
    • 在子进程中使用execl()函数,将子进程替换为程序p2。(在使用execl函数时,把管道的读端作为的参数。)
    • 在父进程中,通过管道给子进程发送字符串。
  • p2
    • 从参数中获取管道的读端(参数即p2的main函数的参数)。
    • 读管道。
    • 将读取到的字符串打印出来。
  • execl()函数原型
int execl(const char *path, const char *arg, ...);
  • 函数说明—— Linux下execl函数学习

当进程调用一种exec函数时,该进程完全由新程序代换,而新程序则从其main函数开始执行。因为调用exec并不创建新进程,所以前后的进程ID并未改变。exec只是用另一个新程序替换了当前进程的正文、数据、堆和栈段。

  • main函数参数中的argc与argv——【C++】main函数的参数 argcargv
  • argc:是argument count 的缩写,保存运行时传递给main函数的参数个数。
  • argv:是argument vector 的缩写,保存运行时传递main函数的参数,类型是一个字符指针数组,每个元素是一个字符指针,指向一个命令行参数。
  • 示例:

main3.c

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>int main(void) {int fd[2];int ret;char buff1[1024];char buff2[1024];pid_t pd;ret = pipe(fd);if (ret !=0) {printf("create pipe failed!\n");exit(1);}pd = fork();if (pd == -1) {printf("fork error!\n");exit(1);} else if (pd == 0) {//bzero(buff2, sizeof(buff2));sprintf(buff2, "%d", fd[0]);//读execl("main3_2", "main3_2", buff2, 0);//子进程被main3_2这个程序取代了printf("execl error!\n");exit(1);} else {strcpy(buff1, "Hello!");write(fd[1], buff1, strlen(buff1)); //写printf("process(%d) send information:%s\n", getpid(), buff1);}if (pd > 0) {wait();}return 0;
}

main3_2.c

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>int main(int argc, char* argv[])
{int fd;char buff[1024] = {0,};sscanf(argv[1], "%d", &fd);read(fd, buff, sizeof(buff));printf("Process(%d) received information:%s\n",  getpid(), buff);   return 0;
}


实例4: 关闭管道的读端/写端

  • 注意:以下所有情况在两个进程下,即一个主进程+一个子进程。

小示例1:主进程关闭写进程后,无法给子进程使用管道发送数据,此时子进程使用read函数进行数据的读取,如果 没有数据可读,则会进行阻塞,代码&结果如下所示:

  • 解释:主进程循环5次,给子进程发送数据。5次之后之后,子进程便无法收到来自于主进程的数据,read()开始阻塞。
#include <stdlib.h>
#include <stdio.h>
#include <string.h>int main(void) {int fd[2];int ret;char buff1[1024];char buff2[1024];pid_t pd;ret = pipe(fd);if (ret !=0) {printf("create pipe failed!\n");exit(1);}pd = fork();if (pd == -1) {printf("fork error!\n");exit(1);} else if (pd == 0) {for(;;){bzero(buff2, sizeof(buff2));sleep(3);read(fd[0], buff2, sizeof(buff2));printf("process(%d) received information:%s\n", getpid(), buff2);}} else {for(int i = 0;i<5;i++){strcpy(buff1, "Hello!");write(fd[1], buff1, strlen(buff1));sleep(3);printf("process(%d) send information:%s\n", getpid(), buff1);}}if (pd > 0) {wait();}return 0;
}


小示例2:管道间是"共享的",个人理解。注意,实际上,并不是同一个内存地址

读取数据时,管道读端的数据会越读越少,而在写入数据时,写入的数据会累加,添加到尾部。

如下所示,

#include <stdlib.h>
#include <stdio.h>
#include <string.h>int main(void) {int fd[2];int ret;char buff1[1024];char buff2[1024];pid_t pd;ret = pipe(fd);if (ret !=0) {printf("create pipe failed!\n");exit(1);}pd = fork();if (pd == -1) {printf("fork error!\n");exit(1);} else if (pd == 0) {for(;;){bzero(buff2, sizeof(buff2));sleep(3);strcpy(buff1, "Dad!");    //子进程写数据write(fd[1], buff1, strlen(buff1));}} else {for(int i = 0;i<5;i++){bzero(buff2, sizeof(buff2));strcpy(buff1, "Hello!");   //父进程写数据write(fd[1], buff1, strlen(buff1));sleep(10);//父进程读数据read(fd[0], buff2, sizeof(buff2));printf("dad process(%d) received information:%s\n", getpid(), buff2);sleep(3);}}if (pd > 0) {wait();}return 0;
}


  • 总结:

    • 没有数据可读后read会阻塞。
    • 例如:有两个进程,主进程给子进程发送数据,主进程的写端关闭了,无法给子进程再发送数据,那么子进程的read将会阻塞。
    • 关闭写端后,write并不会阻塞。这里要说明的是,不关闭写端,write也不会阻塞。
    • 关闭读端后,read就不会阻塞了。
    • 以上的关闭都是对一个进程而言每一个进程既有写端也有读端
    • 如果有多个进程,将每个进程的写端都关闭了,read()也将不会阻塞。
  • 小提示:
  • 为了避免不必要的麻烦,例如没有可读数据时read函数的阻塞,我们可以将没用的管道端口关闭。
  • 例如:如果主进程只负责写数据,子进程只负责读数据,可以将父进程的读端关闭,将子进程的写端关闭(当然要根据实际情况来),将这"4个端口"的管道,变成单向的"2个端口"的管道,如下图所示:

实例5: 把管道作为标准输入和标准输出

把管道作为标准输入和标准输出的优点:

  • 子进程使用exec启动新进程时,就不需要再把管道的文件描述符传递给新程序了。
  • 可以标准输入(或标准输出)的程序。

实现流程:

  1. 使用dup复制文件描述符。
  2. 用exec启动新程序后,原进程中已打开的文件描述符扔保持打开。即可共享原进程中的文件描述符。

补充:

  • dup函数
  • 功能:使用dup函数复制一份原来的文件描述符所指向的内容,并且使用当前系统(进程)可使用的最小文件描述符。
  • 示例:先关闭标准输入文件描述符,然后就使用dup复制当前某一文件描述符,再关闭原来的文件描述符,即可完成文件描述符的替换。
  • 函数原型: int dup(int oldfd);
  • 返回值:
    • 成功:返回新的文件描述符。
    • 失败:返回-1,并设置errno。
  • execlp函数
  • 功能:用exec函数可以把当前进程替换为一个新进程,且新进程与原进程有相同的PID。
  • 相关参考——linux系统编程之进程(五):exec系列函数(execl,execlp,execle,execv,execvp)使用

main5.c

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>int main(void) {int fd[2];int ret;char buff1[1024];char buff2[1024];pid_t pd;ret = pipe(fd);if (ret !=0) {printf("create pipe failed!\n");exit(1);}pd = fork();if (pd == -1) {printf("fork error!\n");exit(1);} else if (pd == 0) {//bzero(buff2, sizeof(buff2));//sprintf(buff2, "%d", fd[0]);close(fd[1]);close(0);//关闭标准输入文件描述符dup(fd[0]);//复制 fd[0] ,并且使用可用的最小的文件描述符作为此文件描述符//即,此子进程使用管道的读端替换标准输入文件描述符close(fd[0]);//关闭原来的读端execlp("./od.exe", "./od.exe", "-c", 0);//如果execlp执行成功,则下面不会执行printf("execl error!\n");exit(1);} else {close(fd[0]);//关闭读端//写strcpy(buff1, "Hello!");write(fd[1], buff1, strlen(buff1)); printf("send...\n");close(fd[1]);//关闭写端}return 0;
}

od.c

#include <stdio.h>
#include <stdlib.h>int main(void){       int ret = 0;char buff[80] = {0,};//scanf从标准输入读——在本实例中,实际上从管道从来的ret = scanf("%s", buff);printf("[ret: %d]buff=%s\n", ret, buff);ret = scanf("%s", buff);printf("[ret: %d]buff=%s\n", ret, buff);//第二次scanf失败,返回-1return 0;
}


使用popen/pclose

  • popen的作用:用于在两个进程之间传递数据:在程序A中使用popen调用程序B时,有两种用法:
  • 程序A读取程序B的输出(使用fread读取);
  • 程序A发送数据给程序B,以作为程序B的标准输入(使用fwirte写入)。
  • 函数原型:FILE *popen(const char *command, const char *type);
  • 返回值:
    • 成功:返回FILE*(文件指针)。
    • 失败:返回空。

实例1:读取外部程序的输出

#include <stdio.h>
#include <stdlib.h>
#define BUFF_SIZE   1024int main(void){FILE * file;char buff[BUFF_SIZE+1];int cnt;// system("ls -l > result.txt");file = popen("ls -l", "r");//以读的方式去读取ls -l这个程序输出的结果 if (!file) {//判断是否打开成功printf("fopen failed!\n");exit(1);}cnt = fread(buff, sizeof(char), BUFF_SIZE, file);//fread是从文件指针中读取if (cnt > 0) {buff[cnt] = '\0';printf("%s", buff);}   pclose(file);//关闭return 0;
}


实例2:把输出写到外部程序

main7.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>#define BUFF_SIZE   1024int main(void){FILE * file;char buff[BUFF_SIZE+1];int cnt;file = popen("./p2", "w");if (!file) {printf("fopen failed!\n");exit(1);}strcpy(buff, "hello world! i 'am 123456789testtest!!!");cnt = fwrite(buff, sizeof(char), strlen(buff), file);pclose(file);return 0;
}

p2.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>int main(int argc,char* argv[]){int fd;char buff[1024] =  {'\0'};int cnt = read(0,buff,sizeof(buff));if(cnt > 0)buff[cnt] = '\0';printf("receive: %s\n",buff);return 0;
}


popen的原理

  • 先使用fork创建一个子进程,然后在子进程中使用exec执行指定外部程序,并返回一个文件指针(FILE*)给父进程。

  • 当使用"r"时,该FILE*指向外部进程的标准输出。

  • 当使用"w"时,该FILE*指向外部程序的标准输入。


popen的优缺点

  • 优点:可以使用shell扩展(比如命令中可以使用通配符)。使用方便。
  • 缺点:每调用一次popen,将要启动两个进程(shell和被指定的程序)。资源消耗大。

【操作系统】进程间的通信——管道相关推荐

  1. 进程间的通信----管道

    前提:本文是基于Linux系统下的学习 用户态的进程是如何组织的呢? 所有的用户态进构成了一棵树.进程树. 进程树的树根是init.也就是1号进程.是用户态进程的祖宗进程. 如何查看进程树? pstr ...

  2. 网络编程之 进程间的通信之管道的使用

    如何使用管道是进程间通信的关键 博主先声明一下,关于处理进程创建以及销毁的方法.        "子进程究竟何时终止????调用waitpid函数后还要无休止的等待子进程终止吗???&quo ...

  3. 进程间的通信IPC(无名管道和命名管道)

    进程间的通信IPC介绍 进程间通信(IPC,InterProcess Communication)是指在不同进程之间传播或交换信息. IPC的方式通常有管道(包括无名管道和命名管道).消息队列.信号量 ...

  4. 进程间的通信——无名管道

    进程间的通信--无名管道 宗旨:技术的学习是有限的,分享的精神是无限的. 一.进程间的通信 (1)同主机进程间数据交互机制:无名管道(PIPE),有名管道(FIFO).消息队列和共享内存.无名管道多用 ...

  5. 命名管道(FIFO) Linux进程进程间的通信之命名管道(FIFO)

    Linux进程进程间的通信之命名管道(FIFO) 命名管道(FIFO),它和一般的管道一样.都是作为中间的邮递员来实现两个进程间的通信交流. 命名管道(FIFO)有几个特点: 1.命名管道(FIFO) ...

  6. 2022.8.31 进程中无名管道的特点,无名管道的创建,为何无名管道只能能够实现具有亲缘关系的进程间的通信,以及实现利用无名管道父进程给子进程发送消息的完整代码。

    无名管道通信 无名管道特点: (1):只能用于具有亲缘关系的进程之间的通信.(父子进程或兄弟进程) (2):是一个半双工的通信模式,具有固定的读端和写端.(fd[0]固定为读端,fd[1]固定为写端) ...

  7. 【《现代操作系统 第4版》】4、进程间的通信之互斥

    买面包问题 假设有两个人A.B要采购面包,首先查看冰箱中是否有面包,如果没有则离开家去超市购买面包,买来后把面包放到冰箱. 假设A.B的日程如下图所示.显然这会导致面包超买,如何保证最多只有一个人去买 ...

  8. linux进程间通讯-无名管道

    文章目录 无名管道 无名管道的创建 -- pipe函数 无名管道的读写规律 无名管道 无名管道概述 管道(pipe)又称无名管道. 无名管道是一种特殊类型的文件,在应用层体现为两个打开的文件描述符.任 ...

  9. Linux系统编程(三)进程间的通信

    Linux系统编程(三)进程间的通信 一.为什么需要进程之间的通信(IPC)? 二.管道 1.概念 2.特质 3.原理 4.局限性 5.代码 2.读入数据 三.共享存储映射 注意事项 父子进程通信 一 ...

最新文章

  1. vue-cli打包构建时常见的报错解决方案
  2. maven 引入本地 jar
  3. opencv3——ANN算法的使用
  4. java arraylist 函数_Java Extend ArrayList函数
  5. vue 跳转页面传参的时候参数值为undefined的解决方法
  6. java题目不会做那么解答_有几道JAVA的题目不会做 哪位高手来解答一下!谢
  7. PHP常用函数之文件系统处理
  8. Java黑皮书课后题第5章:*5.46(倒排一个字符串)编写一个程序,提示用户输入一个字符串,然后以反序显示该字符串
  9. android面试详解
  10. linux df du命令
  11. Ssm在线商城系统实战开发
  12. 01-06 Linux常用命令-统计
  13. 一个操作系统的实现01
  14. Markdown Cookbook by Eric
  15. C Primer Plus第二章总结
  16. 【文本处理】格式crs_stat输出
  17. 互联网协议以及网络分层
  18. W25QXX FLASH介绍
  19. php奖状,利用CSS布局做一个简单的荣誉证书(代码示例)
  20. 关于搞国外广告联盟的一些思路

热门文章

  1. Java设计模式之Builder模式
  2. hevc_nvenc 详细分析1
  3. 制作yocto的recipe的补丁的方法
  4. Python网络编程:E-mail服务(八) 实现抄送和密送功能
  5. Linux 账号与身份管理2
  6. 怎么压缩PDF文件大小?不会操作的快看过来
  7. 微信小程序实战(仿小米商城)
  8. 越狱苹果手机导出网易云音乐歌曲(以及缓存文件转换)
  9. 安卓蓝牙实现即时通讯功能
  10. 天嵌E9开发板tftp烧录eMMC教程(Android)