• 【操作系统】进程间的通信——管道


    进程间的通信—管道

    管道

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

    image-20220822185029945

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

    管道的创建

    • 函数原型: int pipe(int pipefd[2]);
    • 返回值:
      • 成功:返回0。
      • 失败:返回-1。
    • 注意:
      • 获取两个"文件描述符",分别对应管道的读端和写端。
      • fd[0]:为管道的读端;
      • fd[1]:为管道的写端;
      • 如果对fd[0]进行写操作,对fd[1]进行读操作,可能会导致不可预期的错误。

    管道的使用

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

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

    image-20220822203119551

    #include 
    #include 
    #include 
    
    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)); //写进去一个hello
    	printf("send information:%s\n", buff1);
    
    	bzero(buff2, sizeof(buff2));
    	read(fd[0], buff2, sizeof(buff2));//读出来hello
    	printf("received information:%s\n", buff2);
    
    	return 0;	
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    image-20220822203519377


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

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

    image-20220822203843540

    #include 
    #include 
    #include 
    
    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;	
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    image-20220823084848178

    • 注意: 可以看到,我们在父子进程中都打印了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, ...);
    
    • 1

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

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

    main3.c

    #include 
    #include 
    #include 
    #include 
    
    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;	
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    main3_2.c

    #include 
    #include 
    #include 
    #include 
    
    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;	
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    image-20220823102608188


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

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

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

    • 解释:主进程循环5次,给子进程发送数据。5次之后之后,子进程便无法收到来自于主进程的数据,read()开始阻塞。
    #include 
    #include 
    #include 
    
    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;	
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    image-20220823113611581


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

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

    如下所示,

    #include 
    #include 
    #include 
    
    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;	
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    image-20220823160151186


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

    实例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 
    #include 
    #include 
    #include 
    
    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;	
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    od.c

    #include 
    #include 
    
    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失败,返回-1
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    image-20220823170404331


    使用popen/pclose

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

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

    #include 
    #include 
    #define BUFF_SIZE   1024
    
    int 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;	
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    image-20220823174725877


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

    main7.c

    #include 
    #include 
    #include 
    
    #define BUFF_SIZE   1024
    
    int 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;	
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    p2.c

    #include 
    #include 
    #include 
    
    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    image-20220823183047187


    popen的原理

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

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

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


    popen的优缺点

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

  • 相关阅读:
    bootstrap表格
    阿里巴巴技术官甩出的SpringCloud笔记,GitHub标星已突破82k
    视频汇聚/安防视频监控云平台EasyCVR云端录像播放与下载的接口调用方法
    Qt的线程(两种QThread类的详细使用方式)「建议收藏」
    java计算机毕业设计基于node.js的预约上门维修服务系统
    Python Linux下编译
    golang数据结构与算法——稀疏数组、队列和链表
    R语言随机波动模型SV:马尔可夫蒙特卡罗法MCMC、正则化广义矩估计和准最大似然估计上证指数收益时间序列...
    c++中类的默认成员函数
    盘点敏捷项目失败的6个主要原因
  • 原文地址:https://blog.csdn.net/qq_51604330/article/details/126492589