进程间通信:匿名管道

目录

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都会从管道读取。

本篇结束!

相关推荐
草莓熊Lotso3 小时前
Linux 文件描述符与重定向实战:从原理到 minishell 实现
android·linux·运维·服务器·数据库·c++·人工智能
历程里程碑3 小时前
Linux22 文件系统
linux·运维·c语言·开发语言·数据结构·c++·算法
wdfk_prog11 小时前
[Linux]学习笔记系列 -- [drivers][input]input
linux·笔记·学习
七夜zippoe11 小时前
CANN Runtime任务描述序列化与持久化源码深度解码
大数据·运维·服务器·cann
盟接之桥11 小时前
盟接之桥说制造:引流品 × 利润品,全球电商平台高效产品组合策略(供讨论)
大数据·linux·服务器·网络·人工智能·制造
忆~遂愿12 小时前
ops-cv 算子库深度解析:面向视觉任务的硬件优化与数据布局(NCHW/NHWC)策略
java·大数据·linux·人工智能
湘-枫叶情缘12 小时前
1990:种下那棵不落叶的树-第6集 圆明园的对话
linux·系统架构
Fcy64813 小时前
Linux下 进程(一)(冯诺依曼体系、操作系统、进程基本概念与基本操作)
linux·运维·服务器·进程
袁袁袁袁满13 小时前
Linux怎么查看最新下载的文件
linux·运维·服务器
代码游侠13 小时前
学习笔记——设备树基础
linux·运维·开发语言·单片机·算法