Linux进程通信入门:匿名管道的原理、实现与应用场景

Linux系列


文章目录


前言

Linux进程间同通讯(IPC)是多个进程之间交换数据和协调行为的重要机制,是我们学习Linux操作系统中比较重要的一个模块。


一、进程通信的目的

Linux 进程间通信(IPC)的主要目的是让多个独立的进程能够协作、共享资源、交换数据或同步操作。这是现代操作系统中多任务、并行计算和分布式系统的核心基础。以下是具体的目的和场景:

  • 数据传输:一个进程需要将它的数据发送给另一个进程
  • 协同工作:两进程需要更具对方的执行结果来协同完成某种任务
  • 资源同步与互斥:避免多个进程同时访问共享资源导致数据竞争或状态不一致

二、进程通信的原理

2.1 进程通信是什么

进程通信的本质其实就是多个进程可以同时访问同一份"资源"。对于进程来说"资源"就是空间中存储的数据,所以要实现进程通信我们只需要让多个进程可以同时访问同一块空间即可。这里我们就以两个进程为例:

这时我们只需要做到让进程1向这块空间中写数据,进程2可以将进程1写入的数据从空间中取出,这样就完成了通讯工作。那么这块空间该由谁提供呢?由于进程必须保证独立性 (也就是说不论这个空间由哪个进程提供,另一个进程都不能对它访问,否则就破坏了独立性),也就注定了这块共享空间只能由第三方提供---------操作系统 ,那么这块空间就必须由操作系统管理了,空间的创建到释放都是由操作系统来完成的,所以我们对这块空间的访问,就变成了对操作系统的访问,而进程(代表用户)要想访问操作系统,只能通过系统调用接口进行。

为了满足通讯需求,一般操作系统都会有一个独立的通信模块。今天我们就来介绍基于文件级别的通讯方式------管道

2.2 匿名管道通讯的原理

根据上面的分析,我们首先要让两个进程看到同一份资源:

我们利用父进程创建子进程时的特点,这样就可以让两个进程(父和子)看到同一分空间,而我们的管道就是文件,它是内核文件,并不会向磁盘刷新内容,父子进程对他进行访问时并不需要在目录中查询,而是直接通过文件描述符查找,所以管道文件不用定义名字,因此被称为匿名管道。

到了这里我们仅解决了,让两个进程看到同一份空间,但是管道文件不支持以读写方式打开,也就是说我们上面的介绍,两个进程(父子进程)只能向管道中读,或只能向管道中写,而进程通讯一般为一个进程写,一个进程读。

所以进程在打开管道文件时,都会分别以读、写两种方式打开,当我们让父进程读取数据,子进程写入数据的形式通讯时(这个用户自己控制),父进程就关闭对应的写方式,子进程关闭对应的读方式,此时子进程就可以向管道文件的缓冲区写入数据供父进程读取。到了这里,我们就建立了进程通信的信道。

通过上面的介绍我们可以知道匿名管道通信的特点为:

  • 只能进行单向通信
  • 通信进程间需要有血缘关系,常用于父子

三、进程通讯的使用

首先我们先来认识需要用到的系统调用:

这个系统调用需要传递一个存储两个元素的整形数组,该参数为输出型参数,第一个元素为以读形式打开的文件描述符,第二从元素为以写形式打开的文件描述符。下面我们先实现一个简单的通信:
示例1:

c 复制代码
include<iostream>
#include<unistd.h>
#include<sys/types.h>
#include<string>
#include<cstring>
using namespace std;

int main()
{
    int pipefd[2];
    int n=pipe(pipefd);
    if(n<0)return 1;
    pid_t id=fork();//创建子进程
    if(id==0)//子进程
    {
        string str="Hello father process\n";
        close(pipefd[0]);//子关闭读权限
        write(pipefd[1],str.c_str(),strlen(str.c_str()));//向管道中写入数据
        exit(0);
    }
    close(pipefd[1]);//父关闭写权限
    char tmp[100]={0};
    read(pipefd[0],tmp,100);//从管道中读取数据
    cout<<tmp;
    return 0;
}

程序执行结果:

这给代码主要帮助我们理解上面介绍的,关闭文件描述符以及pipe()参数的作用。接下来我们会通过下面的代码示例,对我们的管道通信的四种情况进行总结。

情况1:读写端正常,管道为空,读端就要阻塞

c 复制代码
#include<iostream>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<cstdio>
#include<cstring>
using namespace std;

#define N 2
#define NUM 1024

void Write(int wfd)
{
    char buff[NUM];
    string str="Hello ,I am child,pid:";
    pid_t self=getpid();
    int num=0;
    while(true)
    {
        sleep(2);
        buff[0]=0;
        snprintf(buff,sizeof(buff),"%s%d---%d",str.c_str(),self,num++);//将要写入数据格式化到buff
        write(wfd,buff,strlen(buff));//将buff中的数据写入管道
    }
}

void Read(int rfd)
{
    char buff[NUM];
    while(true)
    {
        buff[0]=0;
        int n=read(rfd,buff,sizeof(buff));//从管道中读取数据
        if(n>0)
        {
            cout<<"father get message:"<<buff<<endl;
        }
        else if(n==0)
        {
            cout<<"father read fail"<<endl;
        }
        else break;
    }
}
int main()
{
    int pipefd[N];
    int n=pipe(pipefd);
    if(n<0)return 1;//调用失败程序返回
    pid_t id=fork();//创建子进程
    if(id<0)return 1;

    if(id==0)//child
    {
        close(pipefd[0]);//关闭读端
        Write(pipefd[1]);
        close(pipefd[1]);//这一步有没有都无所谓
        exit(0);
    }
    //father
    close(pipefd[1]);//关闭写端
    Read(pipefd[0]);
    close(pipefd[0]);

    return 0;
}


想要的效果不好展示,大家可以自己跑以下看看程序执行过程

上述代码,子进程在进程在向管道中写入数据时,每间隔两秒写入一次,而父进程会阻塞等待 ,当子进程写入完成后父进程才会读取并返回。我们可以看到程序并没有执行cout<<"father read fail"<<endl;这也证明了父进程会进行阻塞等待 ,等待子进程写入。
个人感觉这已经能说明问题了,不知道你能不能get到

情况二:读写端正常,管道被写满,写端就要阻塞

接下来我们对上面代码进行一点小改动:

c 复制代码
void Write(int wfd)
{
    char buff[NUM];
    string str="Hello ,I am child,pid:";
    pid_t self=getpid();
    int num=0;
    while(true)
    {
        buff[0]=0;
        snprintf(buff,sizeof(buff),"%s%d---%d",str.c_str(),self,num++);//将要写入数据格式化到buff
        write(wfd,buff,strlen(buff));//将buff中的数据写入管道
        cout<<"已经写入了:"<<num<<"条信息"<<endl;//记录写入次数
    }

}
void Read(int rfd)
{
    char buff[NUM];
    while(true)
    {
        buff[0]=0;
        sleep(10);
        int n=read(rfd,buff,sizeof(buff));//从管道中读取数据
        if(n>0)
        {
            cout<<"father get message:"<<buff<<endl;
        }
        else if(n==0)
        {
            cout<<"father read fail"<<endl;
        }
        else break;
    }
}

我们让父进程等待10秒再读取,子进程一直写入。

此时你会看到当子进程入定数量的数据后,就会停止写入进入阻塞状态,等待父进程的读取,父进程读取成功后,子进程才能继续写入,其原因就直管道被写满了。

情况三:读端正常,写端关闭,读端就会读到0,表明读到了文件(pipe)结尾,不会阻塞

c 复制代码
void Write(int wfd)
{
    char buff[NUM];
    string str="Hello ,I am child,pid:";
    pid_t self=getpid();
    int num=0;
    int cnt=5;//写入五次
    while(cnt--)
    {
        sleep(1);
        buff[0]=0;
        snprintf(buff,sizeof(buff),"%s%d---%d",str.c_str(),self,num++);//将要写入数据格式化到buff
        write(wfd,buff,strlen(buff));//将buff中的数据写入管道
    }
    cout<<"我是写端,我关闭了"<<endl;
}

void Read(int rfd)
{
    char buff[NUM];
    while(true)
    {
        buff[0]=0;
        sleep(1);
        int n=read(rfd,buff,sizeof(buff));//从管道中读取数据
        if(n>0)
        {
            cout<<"father get message:"<<buff<<endl;
        }
        else if(n==0)
        {
            cout<<"father read fail"<<endl;

        }
        else break;
    }
}

这个图效果就很好了,当写端关闭时,读端读取到文件末尾返回0执行cout<<"father get message:"<<buff<<endl;,子进程退出变为僵尸。

情况四:写端正常,读端关闭,操作系统通过信号杀掉正在写入的进程。

c 复制代码
#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>
#include <cstdlib> //stdlib.h
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

#define N 2
#define NUM 1024

using namespace std;

// child
void Writer(int wfd)
{
    string s = "hello, I am child";
    pid_t self = getpid();
    int number = 0;

    char buffer[NUM];
    while (true)
    {
        sleep(1);
        // 构建发送字符串
        buffer[0] = 0; // 字符串清空, 只是为了提醒阅读代码的人,我把这个数组当做字符串了
        snprintf(buffer, sizeof(buffer), "%s-%d-%d", s.c_str(), self, number++);
        // cout << buffer << endl;
        // 发送/写入给父进程, system call
        write(wfd, buffer, strlen(buffer)); // strlen(buffer) + 1???

    }
}

// father
void Reader(int rfd)
{
    char buffer[NUM];
    int cnt = 0;
    while(true)
    {
        buffer[0] = 0; 
        // system call
        ssize_t n = read(rfd, buffer, sizeof(buffer)); //sizeof != strlen
        if(n > 0)
        {
            buffer[n] = 0; // 0 == '\0'
            cout << "father get a message[" << getpid() << "]# " << buffer << endl;
        }
        else if(n == 0) 
        {
            printf("father read file done!\n");
            break;
        }
        else break;

        cnt++;
        if(cnt>5) break;
        // cout << "n: " << n << endl;
    }
}

int main()
{
    int pipefd[N] = {0};
    int n = pipe(pipefd);
    if (n < 0)
        return 1;
    // child -> w, father->r
    pid_t id = fork();
    if (id < 0)
        return 2;
    if (id == 0)
    {
        // child
        close(pipefd[0]);

        // IPC code
        Writer(pipefd[1]);

        close(pipefd[1]);
        exit(0);
    }
    // father
    close(pipefd[1]);

    // IPC code
    Reader(pipefd[0]); // 读取5s
    close(pipefd[0]);
    cout << "father close read fd: " << pipefd[0] << endl;
    sleep(5); //为了观察僵尸

    int status = 0;
    pid_t rid = waitpid(id, &status, 0);    
    if(rid < 0) return 3;

    cout << "wait child success: " << rid << " exit code: " << ((status>>8)&0xFF) << " exit signal: " << (status&0x7F) << endl;

    sleep(5);

    cout << "father quit" << endl;
    return 0;
}


可以看到当读端关闭后,操作系统就会将使用13号信号杀掉写端。

总结

1、具有血缘关系的进程才能使用匿名管道通讯

2、管道只能单向通信

3、父子进程是会进程协同的,同步与互斥的------保护管道文件的数据安全

4、管道是门面向字节流的

5、管道是基于文件的,而文件的生命周期是随进程的

相关推荐
_丿丨丨_15 分钟前
linux下的目录文件管理和基本文件管理的基本操作
linux·运维·服务器
在无清风43 分钟前
K8s是常用命令和解释
linux·容器·kubernetes
CodeWithMe1 小时前
【Linux C】简单bash设计
linux·c语言·bash
秃头的赌徒1 小时前
Docker 前瞻
linux·运维·服务器
normaling1 小时前
十一,Shell
linux
家庭云计算专家2 小时前
IPV6应用最后的钥匙:DDNS-GO 动态域名解析工具上手指南--家庭云计算专家
linux·服务器·云计算·编辑器
青山瀚海2 小时前
windows中搭建Ubuntu子系统
linux·windows·ubuntu·docker
xxxx1234453 小时前
Linux驱动开发-网络设备驱动
linux·运维·驱动开发
2401_861615283 小时前
debian转移根目录
linux·debian·电脑
刘若水4 小时前
Linux: 线程控制
linux·运维·服务器