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

简介: 【操作系统】进程间的通信——管道

进程间的通信—管道

管道

  • 进程间的通信(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 <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)); //写进去一个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;    
}

image-20220822203519377


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

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

image-20220822203843540

#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;    
}

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, ...);

当进程调用一种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;    
}

image-20220823102608188


实例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;    
}

image-20220823113611581


小示例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;    
}

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函数

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失败,返回-1
    return 0;
}

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

image-20220823174725877


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

main7.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#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;    
}

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;
}

image-20220823183047187


popen的原理

  • 先使用fork创建一个子进程,然后在子进程中使用exec执行指定外部程序,并返回一个文件指针(FILE*)给父进程。
  • 当使用"r"时,该FILE*指向外部进程的标准输出。
  • 当使用"w"时,该FILE*指向外部程序的标准输入。

popen的优缺点

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

相关文章
|
6天前
|
算法 调度 UED
探索操作系统核心:进程管理与调度
【9月更文挑战第28天】在数字世界的心脏跳动着无数进程,它们像是细胞一样构成了操作系统的生命体。本文将深入探讨操作系统中进程管理与调度的奥秘,揭示如何通过精心设计的数据结构和算法来维护系统的稳定性和效率。我们将从进程的基本概念出发,逐步解析进程状态转换、进程同步机制,以及进程调度策略,旨在为读者呈现一幅清晰、生动的操作系统内部工作机制图景。
|
2天前
|
算法 调度 UED
探索操作系统的心脏:进程调度算法
【9月更文挑战第32天】在数字世界的每一次心跳中,都隐藏着一个不为人知的英雄——进程调度算法。它默默地在后台运作,确保我们的命令得到快速响应,应用程序平稳运行。本文将带你走进操作系统的核心,一探进程调度的奥秘,并通过代码示例揭示其背后的智慧。准备好跟随我一起深入这趟技术之旅了吗?让我们开始吧!
|
4天前
|
算法 Linux 调度
深入理解操作系统的进程调度
【9月更文挑战第30天】本文将带你进入操作系统的核心—进程调度。我们将探讨其工作原理,分析几种常见的调度算法,并通过实际代码示例来揭示这些理论是如何在真实系统中实现的。无论你是初学者还是有经验的开发者,这篇文章都能帮助你更好地理解操作系统的这一关键组成部分。
|
4天前
|
消息中间件 算法 调度
探索操作系统核心:进程管理与调度策略
【9月更文挑战第30天】在数字化时代的心脏,操作系统扮演着至关重要的角色。本文将深入探讨操作系统的基石之一——进程管理,以及如何通过调度策略优化系统性能。我们将从进程的基本概念出发,逐步解析进程状态、进程控制和进程间通信等关键要素。同时,我们会探讨几种常见的进程调度算法,并分析它们的优缺点。最后,文章将展示一个简单的代码示例,以加深对理论部分的理解和应用。
|
5天前
|
算法 调度 UED
探索操作系统的心脏:进程管理与调度
【9月更文挑战第29天】在数字世界的海洋中,操作系统是支撑软件与硬件和谐共舞的桥梁。本文将深入探讨操作系统的核心功能—进程管理及其调度机制,揭示它们是如何影响计算机性能和用户体验的。通过浅显易懂的语言和生动的比喻,我们将一起遨游在进程的生命周期、调度算法以及优先级等概念之间,旨在为读者呈现一个清晰的操作系统内部运作图景。
17 6
|
4天前
|
算法 调度 开发者
深入理解操作系统之进程管理与调度
【9月更文挑战第30天】本文旨在通过浅显易懂的语言和具体代码示例,带领读者探索操作系统中进程管理的奥秘。我们将从进程的生命周期出发,逐步解析进程调度的核心概念,并通过实例展示如何实现简单的进程调度算法。无论你是初学者还是有一定基础的开发者,都能在这篇文章中找到有价值的信息,帮助你更好地理解和掌握进程管理与调度的知识。
15 4
|
7天前
|
算法 调度
操作系统的心脏:深入解析进程调度算法
本文旨在深入探讨现代操作系统中的核心功能之一——进程调度。进程调度算法是操作系统用于分配CPU时间片给各个进程的机制,以确保系统资源的高效利用和公平分配。本文将详细介绍几种主要的进程调度算法,包括先来先服务(FCFS)、短作业优先(SJF)、时间片轮转(RR)以及优先级调度(PS)。我们将分析每种算法的基本原理、优缺点及其适用场景。同时,本文还将讨论多级反馈队列(MFQ)调度算法,并探讨这些算法在实际应用中的表现及未来发展趋势。通过深入解析这些内容,希望能够为读者提供对操作系统进程调度机制的全面理解。
|
6天前
|
资源调度 算法 调度
深入浅出操作系统之进程与线程管理
【9月更文挑战第29天】在数字世界的庞大舞台上,操作系统扮演着不可或缺的角色,它如同一位精通多门艺术的导演,精心指挥着每一个进程和线程的演出。本文将通过浅显的语言,带你走进操作系统的内心世界,探索进程和线程的管理奥秘,让你对这位幕后英雄有更深的了解。
|
5天前
|
算法 调度 UED
深入理解操作系统:进程管理与调度策略
【9月更文挑战第29天】在数字世界的心脏,操作系统悄无声息地跳动着,它的健康直接关系到整个计算生态系统的活力。本文将带领读者穿梭于操作系统的微观世界,探索进程管理的奥秘和调度策略的智慧。我们将从进程的基本概念出发,逐步深入到进程的生命周期管理,最后探讨不同的进程调度算法及其对系统性能的影响。通过深入浅出的方式,让读者能够更好地理解并掌握操作系统中进程管理的核心知识。
|
6天前
|
算法 Linux 调度
深入理解操作系统中的进程调度
【9月更文挑战第28天】在操作系统的复杂世界中,进程调度是维持系统高效运作的关键。本文将深入浅出地探讨进程调度的核心概念及其对系统性能的影响。从进程调度的定义和目标出发,逐步解析不同类型的调度算法,并通过实际代码示例,揭示这些算法如何在真实系统中实施。无论你是初学者还是有经验的开发者,这篇文章都将为你提供宝贵的见解和知识。
下一篇
无影云桌面