进程间通信--匿名管道【Linux】

前言

本文详细讲解了匿名管道的背景、原理和特点,并且站在文件描述符角度深度帮助理解管道,下面就让我们开始吧。

有兴趣的同学可以也以看看匿名管道的实际案例:
匿名管道实例–进程控制【Linux】
匿名管道实例–进程池【Linux】
命名管道详解:
进程间通信–命名管道【Linux】

一、进程间通信目的

  • 数据传输:一个进程需要将它的数据发送给另一个进程
  • 资源共享:多个进程之间共享同样的资源。
  • 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
  • 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

通信背景

1.由于进程是具有独立性的,进程想交互数据,成本会非常高。但是有些情况下需要多进程处理一件事情。
2.进程独立并不是彻底独立,有时候我们需要双方能够进行一定程度的信息交互。

我们要学的进程间通信,不是告诉我们如何通信,是他们两个如何先看到同一份资源。(文件,内存块…等方式)

在这里插入图片描述
两个进程同时访问磁盘上的一个文件进行读写

但由于进程在磁盘上读写太慢,所以进程间通信一般读写内存中的文件。

在这里插入图片描述
两个进程同时访问内存上的一个文件进行读写

二、管道

什么是管道

  • 管道是Unix中最古老的进程间通信的形式。
  • 我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”

2.1 原理

父进程指向一个管道文件,子进程继承父进程的指向关系,从而子进程也指向管道,进行通信。
在这里插入图片描述

2.2 特点

生活中管道的特点

  • 都是单向的
  • 管道是为了传输资源的 – 数据

所以计算机中的管道 – 单向的,传输数据的

三、匿名管道

3.1 站在文件描述符角度-深度理解管道

1.父进程创建管道,打开fd[0]fd[1]分别作为读写端

在这里插入图片描述

2.父进程fork出子进程,子进程继承读写端

在这里插入图片描述

3.父进程关闭fd[0],子进程关闭fd[1]

在这里插入图片描述

这里就会有以下三个问题:

1.为什么,父进程要分别打开读和写?
为了让子进程继承,让子进程不用再打开了。

2.为什么父子要关闭对应的读写?
管道必须是单向通信的。操作系统内部设计的管道就是单向的。

3.谁决定,父子关闭什么读写?
不是由管道本身决定的,而是由需求决定的。

3.2 实例代码

pipe (管道生成函数)原型

int pipe (int pipefd[2])

作用:创建两个管道文件描述符,保存在pipefd中。
参数:一个文件描述符fd数组,包含两个fd,其中pipefd[0]为读端,pipefd[1]为写端。
返回值:一个整数,0表示成功,-1表示失败。

例子:从父进程向管道写入数据,子进程读取数据

1.创建管道

int pipefd[2] = {0};
if (pipe(pipefd) != 0)
{
    cerr << "pipe error" << endl;
    return 1;
}

2.创建子进程

pid_t id = fork();
if (id < 0)
{
    cerr << "fork error" << endl;
    return 2;
}

子进程

else if (id == 0)
{
    // child
    // 子进程来进行读取,子进程就应该关掉写端
    close(pipefd[1]);
    char buffer[NUM];
    while (true)
    {
    	cout << "时间戳:" << (uint64_t)time(nullptr) << endl;
    	//子进程没有带sleep,为什么子进程也会休眠呢?
        memset(buffer, 0, sizeof(buffer));
        ssize_t s = read(pipefd[0], buffer, sizeof(buffer) - 1);
        //read的返回值大于0时读取成功,等于0时读取失败
        if (s > 0)
        {
            // 读取成功
            buffer[s] = '\0'; // 把最后一位设置为'\0',使字符串为C风格字符串
            cout << "子进程收到信息,内容是:" << buffer << endl;
        }
        else if (s == 0)
        {
    		// 当s为0时,父进程写完数据关闭写端,子进程读完剩下的数据
            cout << "父进程写完了,我也退出啦" << endl;
            break;
        }
        else{
            // do nothing
        }
    }
    close(pipefd[0]);
    exit(0);
}

父进程

else
{
    // parent
    // 父进程来进行写入,就应该关闭读端
    close(pipefd[0]);
    const char *msg = "你好子进程,我是父进程,这次发送的信号编号是";
    int cnt = 0;
    while (cnt < 5)
    {
        char sendBuffer[1024];
        //用sprintf格式化senBuffer数组,类似于printf
        sprintf(sendBuffer, "%s : %d", msg, cnt);

        write(pipefd[1], sendBuffer, strlen(sendBuffer));
        sleep(1); // 这里是为了一会看现象明显
        cnt++;
    }
    close(pipefd[1]);
    cout << "父进程写完了" << endl;
}

父进程等待子进程退出

pid_t res = waitpid(id, nullptr, 0);
if (res > 0)
{
    cout << "等待子进程成功" << endl;
}
return 0;

源代码

#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>
#include <sys/wait.h>
#include <ctime>
using namespace std;

#define NUM 1024

int main()
{
    // 1.创建管道
    int pipefd[2] = {0};
    if (pipe(pipefd) != 0)
    {
        cerr << "pipe error" << endl;
        return 1;
    }
    // 2.创建子进程
    pid_t id = fork();
    if (id < 0)
    {
        cerr << "fork error" << endl;
        return 2;
    }
    else if (id == 0)
    {
        // child
        // 子进程来进行读取,子进程就应该关掉写端
        close(pipefd[1]);
        char buffer[NUM];
        while (true)
        {
            cout << "时间戳:" << (uint64_t)time(nullptr) << endl;
            //子进程没有带sleep,为什么子进程也会休眠呢?
            memset(buffer, 0, sizeof(buffer));
            ssize_t s = read(pipefd[0], buffer, sizeof(buffer) - 1);
            if (s > 0)
            {
                // 读取成功
                buffer[s] = '\0';
                cout << "子进程收到信息,内容是:" << buffer << endl;
            }
            else if (s == 0)
            {
                // 当s为0时,父进程写完数据关闭写端,子进程读完剩下的数据
                cout << "父进程写完了,我也退出啦" << endl;
                break;
            }
            else{
                // do nothing
            }
        }
        close(pipefd[0]);
        exit(0);
    }
    else
    {
        // parent
        // 父进程来进行写入,就应该关闭读端
        close(pipefd[0]);
        const char *msg = "你好子进程,我是父进程,这次发送的信号编号是";
        int cnt = 0;
        while (cnt < 5)
        {
            char sendBuffer[1024];
            sprintf(sendBuffer, "%s : %d", msg, cnt);

            write(pipefd[1], sendBuffer, strlen(sendBuffer));
            sleep(2); // 这里是为了一会看现象明显
            cnt++;
        }
        close(pipefd[1]);
        cout << "父进程写完了" << endl;
    }
    pid_t res = waitpid(id, nullptr, 0);
    if (res > 0)
    {
        cout << "等待子进程成功" << endl;
    }
    // cout << "fd[0]: " << pipefd[0] << endl;
    // cout << "fd[1]: " << pipefd[1] << endl;
    return 0;
}

输出样例

在这里插入图片描述

注意事项

  • 当父进程没有写入数据的时候,子进程在等。所以,父进程写入之后,子进程才能read(会返回)到数据,子进程打印读取数据要以父进程的节奏为主。

  • 父进程和子进程读写的时候是有一定的顺序性的。
     管道内部,没有数据,读端就必须阻塞等待(read)。
     管道内部,如果数据被写满,写端就必须阻塞等待(write)。
     阻塞等待时,父子进程会把自己放在管道的等待队列里。

  • 也就是说,pipe内部,自带访问控制机制(同步和互斥机制)。
    而在父子进程各自printf(向显示器写入)时没有顺序,此时缺乏访问控制。

四、特征总结

  1. 管道只能用来进行具有血缘关系的进程之间,进行进程间通信。常用于父子通信。
  2. 管道只能单向通信(内核实现决定的),是半双工的一种特殊情况。
  3. 管道自带同步机制 – 自带访问控制。
  4. 管道是面向字节流的,没有格式边界,需要用户来自定义区分内容的边界。
  5. 管道的生命周期,随进程退出而退出。

总结

本文详细介绍了进程间通信的一种方式 – 管道,介绍了匿名管道的原理和实现。大家也可以尝试去使用一下管道,这样可以更深入地了解其中的细节。喜欢的话,欢迎点赞支持和关注~

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值