进程间通信:匿名管道

目录

1.管道原理

2.管道的建立

3.管道的特点

[b.2 默认给读写端提供同步机制](#b.2 默认给读写端提供同步机制)

[a.2 如果管道文件被写满了,写端必须等待,直到有空间为止。](#a.2 如果管道文件被写满了,写端必须等待,直到有空间为止。)

a3:写端关闭,读端一直读,读端会读到read的返回值为0,表示读到文件结尾。

a4:读端关闭,写端会被操作系统关闭

4.管道与重定向的联系


1.管道原理

  • 进程间通信不能两个进程的数据直接拷贝,不满足进程的独立性原则,因为直接拷贝可以看到其他进程的数据。
  • 进程间通信的本质:不同的进程可以看到同一份资源,通常由os提供。
  • 进程独立性与文件系统没有关系。因为父子进程指向同一个标准输出,所有才能打印到同一个显示器。文件描述符表也属于进程部分 ,也会拷贝过来,但不会直接拷贝文件。父进程创建子进程时,我们可以先创建一个文件,用两个文件描述符指向,文件描述符表浅拷贝struct file *, 普通文件也可以被父子进程同时看到,就可以完成进程间通信。但是这种方式内存中的文件缓冲区会刷新到磁盘中,而我们不需要,这时需要这个文件只是纯内存级文件,称为管道文件。

管道文件的特点,只允许单向通信,双向通信需要建立两个管道,因为这样方便区分哪部分是要发送的,哪部分是要接受的。又因通信风格像管道所以被称为管道。

如何实现父子管道通信: 父进程先通过os读写同时指向一个管道文件,通过fork()创建子进程,此时父子进程同时可以读写这个管道文件,然后父进程关闭写,子进程关闭读,就实现了字子读父写的进程间通信。

同一个进程打开同一个文件读和写两次,内存中文件内容和属性会只存在一份,都是相同的。

struct file 是管理内存文件的结构,磁盘上是没有的,只会在内存中存在。 读方式和写方式两次打开文件时,因为 struct file 中读写位置 pos 是不同的,因此struct file 有两个,所以两个struct file*指向不同的struct file ,一个用来读取,一个写入,但他们会指向同一个方法集,inode 和文件缓冲区(内容)。

fork () 创建的子进程也会指向与父进程相同的 struct file。这时父子进程各关闭一个,由于struct file 中存在 引用计数cnt的原因,管道文件 struct file不会被关闭,使进程与文件系统解耦 。

2.管道的建立

使用系统调用创建一个无名管道。

cpp 复制代码
#include <unistd.h>

int pipe(int fd[2]);

fd 输出型参数,因为传参降维等于传入指针,fd[0] 表示读端,fd[1] 表示写端,都指向同一文件。成功返回0,出错返回错误码。头文件在c++中也要使用**.h**格式,因为是系统头文件,而C语言可以用 c 开头的格式。

cpp 复制代码
#include <iostream>
#include <cassert> 
#include <unistd.h>//系统头文件,只存在c语言风格的 .h

using namespace std;

int main()
{   
    int pipefd[2] = {0};
    int n = pipe(pipefd);//成功返回0,失败返回-1
    assert(n == 0);//debuge模式 g++编译时要加-g选项 下才会执行,release不会存在执行
    (void)n; //防止编译器告警 变量n未被使用

    cout << "pipefd[0]:" << pipefd[0] << ", pipefd[1]:" << pipefd[1] << endl; 

    // cout << "hello bit" <<endl;
    return 0;
}

建立一个子写父读的管道,子进程向管道写数据

cpp 复制代码
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork");
        return 1;
    }

    //子写父读
    if(id == 0)
    {
        //子进程
        close(pipefd[0]);//关闭读
        //write
        int cnt = 10;
        while(cnt)
        {
            char message[MAX] = {'\0'};
            //sizeof字符串包含\0=MAX,而strlen遇到\0停止
            snprintf(message,sizeof(message),"hello father,I am child,pid:%d ,cnt = %d",getpid(),cnt);//格式化输入到字符串中
            write(pipefd[1],message,strlen(message));//这里strlen最后\0不用加1,这是在文件中,不是c语言中没有联系
            cnt--;
            sleep(1);
        }
        
        //close(pipefd[1]);写不写都行,进程退出文件就会被关闭
        exit(0);
    }
    //父进程

    close(pipefd[1]);//关闭写
    //read
    char buffer[MAX] = {'\0'};
    while(true)
    {
        ssize_t n = read(pipefd[0],buffer,sizeof(buffer)-1);
        //希望读取sizeof-1个即MAX-1,这里-1方便为最后一个位置加 \0 //返回值为实际读取的字节数,即发送的strlen(message)
        if(n>0)
        {
            buffer[n] = '\0'; //当作字符串
            cout<< "child say: "<<buffer<< " to me!"<<endl;
        }
        pid_t rid = waitpid(id, nullptr, WNOHANG); // 阻塞等待
        if (rid == id)
        {
            cout << "wait success" << endl;
            break;
        }
    }

操作系统通过维护管道的内部数据结构读写指针 来判断应该读取哪些数据,通多输入数据的长度判断数据结尾指针,而不是\0,被读取后更新数据开始指针位置

3.管道的特点

父进程可以把代码和数据通过fork()给子进程,为什么还要进程间通信?

  1. 实现子进程给父进程数据

  2. 实现变化的数据进行通信
    a. 管道的4种情况

  3. 正常情况(读写端没有被关闭),如果管道没有数据了,读端 read 就必须等待,直到有数据为止

  4. 正常情况(读写端没有被关闭),如果管道被写满了,写端必须等待,直到有空间为 止。

  5. 写端关闭,读端一直去读,读端会读到read的返回值为0,表示读到文件结尾。

  6. 读端关闭,写端一直写,OS会将写端进程杀掉,通过向目标进程发送SIGPIPE(13)信号,终止目标进程。
    b. 管道的5种特性

  7. 匿名管道,可以允许具有血缘关系的进程之间进行进程间通信,常用于父子,也可以兄弟,爷孙通信,但也仅限于此。

  8. 匿名管道,默认给读写端提供同步机制(有顺序 )。子进程写了,父进程才能读。父进程读了,子进程再写

  9. 面向字节流的。不是写一个读一个,可能一次读完好几次的写入 ,或分几次读,读写次数没有严格控制对应。

  10. 管道的生命周期时随进程的。父进程和子进程都退出了,管道也会关闭。

  11. 管道是即不能同时读写,还是单向的。

b.2 默认给读写端提供同步机制

子进程运行下面代码,将数据写入管道文件。

cpp 复制代码
int cnt = 100000;
while(cnt)
{
      char message[MAX];//系统调用接口是c语言, 写代码要c,c++混编
      snprintf(message, sizeof(message), "hello father, I an child, pid: %d, cnt: %d", getpid(), cnt);//格式化输入到字符串message中。 
      cnt--;
      write(pipefd[1], message, strlen(message));//不用字符串最后的'\0', 不用string+1 。 写到文件pipefd[1],并没有规定文件大小,可以写很多。
      //sleep(1);   

      cout<< "write...." << cnt <<endl;
 }

只有子进程写完后,管道文件有数据,父进程才能读。

a.2 如果管道文件被写满了,写端必须等待,直到有空间为止。

我们将读端sleep(200),发现写端写到一定程度就会停止写入,即停止停止打印cnt。

子进程运行下面代码,写入管道文件:

cpp 复制代码
     //测量管道文件的大小
        int cnt = 0;
        while(true)
        {
            char c = 'a';
            write(pipefd[1],&c,1);
            cnt++;
            cout<<"write ..." << cnt <<endl;
        }

可以看到我使用的服务器 Linux 管道可以写入65536个字节。即64KB。

我们可是使用ulimit -a 显示目前资源限制的设定

**a3:**写端关闭,读端一直读,读端会读到read的返回值为0,表示读到文件结尾。

我们让子进程只写两条然后退出:

cpp 复制代码
int cnt = 2;
        while(cnt)
        {
            char message[MAX];//系统调用接口是c语言, 写代码要c,c++混编
            snprintf(message, sizeof(message), "hello father, I an child, pid: %d, cnt: %d", getpid(), cnt);//格式化输入到字符串message中。 
            cnt--;
            write(pipefd[1], message, strlen(message));//不用字符串最后的'\0', 不用string+1 。 写到文件pipefd[1],并没有规定文件大小,可以写很多。
            sleep(1);   
            cout<< "write...." << cnt <<endl;
        }
        
        cout<< "child close w piont" <<endl;
        //close(pipefd[1]);  进程退出自动会关闭
        exit(0); //后续只能是父进程

父进程一直读,并打印 read 返回值:正常情况会返回真实,没读到会阻塞。

cpp 复制代码
    char buffer[MAX];
    while(true)
    {
        //sleep(200);
        ssize_t n = read(pipefd[0],buffer, sizeof(buffer)-1);//读到buffer中, 希望读sizeof(buffer)-1,返回实际读的数量。
        if(n > 0)
        {
            buffer[n] = 0; //'\0',当做字符串结尾
            cout << getpid()  << ", child say: " << buffer << " to me!" <<endl;
        }

        cout<< "father return val(n): " << n <<endl;
    }

没数据时,父进程read会阻塞如果不阻塞,说明子进程把写端关闭了。我们可以增加一个 n==0 时的判断:

cpp 复制代码
char buffer[MAX];
    while(true)
    {
        //sleep(200);
        ssize_t n = read(pipefd[0],buffer, sizeof(buffer)-1);//读到buffer中, 希望读sizeof(buffer)-1,返回实际读的数量。
        if(n > 0)
        {
            buffer[n] = 0; //'\0',当做字符串结尾
            cout << getpid()  << ", child say: " << buffer << " to me!" <<endl;
        }
        else if(n == 0)
        {
            //没数据时,父进程read会阻塞。如果不阻塞,说明子进程把写端关闭了。
            cout<< "child quit, me too !" <<endl;
            break;
        }
        cout<< "father return val(n): " << n <<endl;
    }
    
    //close(pipefd[0]);  进程退出自动会关闭
    pid_t rid = waitpid(id,nullptr,0);//阻塞等待 
    if(rid == id)
    {
        cout << "wait success" <<endl;
    }

a4:读端关闭,写端会被操作系统关闭

我们让父进程只读一句就退出。子进程代码与 a3 代码相同。父进程代码:

cpp 复制代码
    char buffer[MAX];
    while(true)
    {
        sleep(1);
        ssize_t n = read(pipefd[0],buffer, sizeof(buffer)-1);//读到buffer中, 希望读sizeof(buffer)-1,返回实际读的数量。
        if(n > 0)
        {
            buffer[n] = 0; //'\0',当做字符串结尾
            cout << getpid()  << ", child say: " << buffer << " to me!" <<endl;
        }

        cout<< "father return val(n): " << n <<endl;
        break;//退出
    }

    cout<< "read point close" <<endl; 
    close(pipefd[0]);  //父进程主动关闭读端。进程退出自动会关闭

    sleep(10);

    pid_t rid = waitpid(id,nullptr,0);//阻塞等待 
    if(rid == id)
    {
        cout << "wait success" <<endl;
    }

运行结果:

我们使用任务管理器检测 mypipe 进程

bash 复制代码
while :; do ps axj | head -1 && ps axj |grep mypipe;sleep 1;done

发现在父进程关闭读之后,sleep(10)的时候,子进程102624 就已经称为僵尸进程,已经挂掉。对于操作系统来讲,读端关闭,写端就没有意义了,会将其杀掉。

如何证明子进程收到13号信号?

cpp 复制代码
    sleep(5);

    int status = 0;
    pid_t rid = waitpid(id,&status,0);//阻塞等待 
    if(rid == id)
    {
        cout << "wait success, child exit sig: " << (status&0x7F)  <<endl;//低七位终止信号
    }

通过 waitpid 的输出性参数的第7位得到子进程收到的终止信号

4.管道与重定向的联系

bash 复制代码
$ sleep 100 | sleep 200 | sleep 300   #代码没有实际意义

他们3个是兄弟进程,父进程都是bash,因为是兄弟间通信,管道是先创建的,所以3个子进程都能看到这两个管道,然后不同管道关闭不需要的读写端就行。

所以,当1号进程想向管道文件中写的时候,可以直接dup2(pipefd[1],1)进行输出重定向,而2号进程使用dup2(pipefd[0],0)进行输入重定向就可以完成从1号进程到2号进程的管道通信,2号进程到3号进程也是这样。然后1号进程的printf,puts,write 都会输入到管道,2号进程的scanf,getline,read都会从管道读取。

本篇结束!

相关推荐
爱吃泡芙的小白白2 小时前
使用Cursor来进行连接SSH远程主机中出现的问题(自用)
服务器·学习·ssh·cursor
小北方城市网2 小时前
Spring Cloud Gateway 生产级微内核架构设计与可插拔过滤器开发
java·大数据·linux·运维·spring boot·redis·分布式
wacpguo2 小时前
Ubuntu 24.04 安装 Docker
linux·ubuntu·docker
wan9zhixin2 小时前
解密六氟化硫气体检测仪在运维现场的多元应用场景
运维
Lenyiin2 小时前
Linux 进程控制
linux·运维·服务器
咕噜咕噜万2 小时前
测试用例执行进度实时同步工具指南:从流程打通到效率提效的全链路落地
运维·网络
超龄超能程序猿2 小时前
X-AnyLabeling 全功能操作指南
运维·yolo·计算机视觉
春日见2 小时前
Git 相关操作大全
linux·人工智能·驱动开发·git·算法·机器学习
BullSmall3 小时前
CloudDR 故障切换演练脚本模板(自动化执行版)
运维·自动化