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

news2024/11/27 14:29:52

img

Halo,这里是Ppeua。平时主要更新C语言,C++,数据结构算法…感兴趣就关注我吧!你定不会失望。

本篇导航

  • 0. 进程间通信原理
  • 1. 匿名管道
    • 1.1 通信原理
    • 1.2 接口介绍
  • 2. 命名管道
    • 2.1 接口介绍
  • 3. 共享内存
    • 3.1 通信原理
    • 3.2 接口介绍

在这里插入图片描述

0. 进程间通信原理

进程间的是相互独立的。那么想要让两个进程间进行通信,本质是让其看到同一份资源。因为进程具有独立性,所以大多时候让两个或多个进程看到同一份资源是最费力的

根据看到资源的方式不同,将进程通信划分为以下几种:

  1. 匿名管道通信
  2. 命名管道通信
  3. 共享内存

其中,匿名管道通信与命名管道通信的本质都是让进程看到同一份内存级文件

内存级文件是一个仅存储在内存中的文件.不会刷新到磁盘中

1. 匿名管道

1.1 通信原理

管道实际上是一份内存级文件,其被创建出来,通过文件的方式去访问.

内存模型如下:

image-20231211213419971

其中file_r为写缓冲区,file_w为读缓冲区(缓冲区本质上也为一个内存级文件)

创建管道时,系统会为其分配两个fd.一个为读端,一个为写端.但是 管道只能进行单向通信.

为了方便控制,通常情况下,我们会手动关闭我们不需要的那个fd.(以下为了方便测试,规定由父进程写读,子进程写)

那么在匿名管道通信时如何让多个进程看到同一份内存级文件呢?

子进程会继承父进程的大多数资源,file_struct也在其中.但因为操作系统节省资源的特性,文件并不会被创建多份

所以可以通过创建子进程的方法来让多个进程看到同一份资源

所以 匿名管道的特点之一:仅能在有关系的进程中进行通信(父子进程,兄弟进程)

创建一个子进程时,内存模型如下:

image-20231211214629837

(通信本质是让不同进程看到同一份资源,所以资源的准备需要在进程创建之前!!!)

此时子进程也能够访问这个内存级文件了.这时双方就可以根据fd,按照访问文件的方式去访问这个内存级文件.也就是可以进行通信

1.2 接口介绍

创建匿名管道使用的函数为 int pipe(int pipefd[2])

image-20231211215326099

image-20231211215432856

其中 **int pipefd[2]**为输出型参数 pipefd[0]为读端,pipefd[1]为写端

该接口创建完管道,并为用户分配所需读写端的fd,将其存入该数组后返回给用户.

如果创建成功则返回0,如果创建失败则返回-1,同时设置errno

这是一份简单的管道通代码.创建管道需要在创建子进程前才能被共享到!

#include<iostream>
#include<unistd.h>
#include<cstdio>
#include<sys/types.h>
#include<sys/wait.h>
#include<string>
using namespace std;
#define N 2
#define NUM 1024

void Read(int rfd)
{
    
    while (true) {
        char buffer[1024];
        int n=read(rfd,buffer,sizeof(buffer));
        if(n<=0)
        {
            cout<<"wait write into pipe"<<endl;
        }
        else {
            cout<<buffer;
        }
        
    }
}
void Write(int wfd)
{
    string example="i am a child,hello linux communitate  ";
    pid_t self=getpid();
    example+=to_string(self);
    int flag=example.size();
    int cnt=0;
    while(true)
    {
        example.erase(flag);
        example+=" "+to_string(cnt++)+"\n";
        int n=write(wfd,example.c_str(),example.length());
        if(n<=0)
        {
            cout<<"pipe close"<<endl;
            break;
        }
        sleep(1);
    }
}

int main()
{
    int pipefd[2];
    pipe(pipefd);
    if(pipe(pipefd)<0)
    {
        perror("create pipe failed\n");
    }
    cout<<"0: "<<pipefd[0]<<endl;
    cout<<"1: "<<pipefd[1]<<endl;
    pid_t id = fork();
    if(id==0) //0 read 1 write
    {
        //write
        close(pipefd[0]);
        Write(pipefd[1]);
    }
    else {
        //read
        close(pipefd[1]);
        Read(pipefd[0]); 
    }
    return 0;
}

image-20231211220606452

完成了匿名管道的通信.

管道通信为单向的.读端会将读取的内容从管道中取走.先写入的数据会被先取走(与队列的原理相似)


父进程会随着子进程发送信息的频率而读取信息.(上文写端进行了休眠,而读端并没有)

所以:

读写端正常.当管道中没有内容时,读端会阻塞等待


如果我们重复的写入一段内容而不读取呢?

void Write(int wfd)
{
    string example="i am a child,hello linux communitate  ";
    pid_t self=getpid();
    example+=to_string(self);
    int flag=example.size();
    int cnt=0;
    while(true)
    {
        example.erase(flag);
        example+=" "+to_string(cnt++)+"\n";
        int n=write(wfd,example.c_str(),example.length());
        if(n<=0)
        {
            cout<<"pipe close"<<endl;
            break;
        }
       	cout<<cnt<<endl;;
    }
}

将写端逻辑做出如上更改,当写不进去时,输出 “pipe full”;

void Read(int rfd)
{
    while(true){};
    while (true) {
        char buffer[1024];
        int n=read(rfd,buffer,sizeof(buffer));
        if(n<=0)
        {
            cout<<"wait write into pipe"<<endl;
        }
        else {
            cout<<buffer;
        }
        
    }
}

将读端做出如上更改.手动阻塞进程

image-20231211222740715

观察到写端阻塞,等待读端读取

所以

读写端正常.当管道写满时,写端会阻塞等待读端读取


将写端设置为一段时间后自动关闭.读端不会被阻塞.read返回0,可以根据这个特性做出行为

所以

写端被关闭,读端读到文件结尾,返回0.但此时不会被阻塞


将读端设置为一段时间后自动关闭.为了节省资源.写端将被操作系统关闭

void Read(int rfd)
{
    int cnt=5;
    while (cnt>0) {
        char buffer[1024];
        int n=read(rfd,buffer,sizeof(buffer));
        if(n<=0)
        {
            cout<<"wait write into pipe"<<endl;
        }
        else {
            cout<<buffer;
        }
        cnt--;
    }
    cout<<"read close"<<endl;
}
//main 中修改的部分
 //read
        close(pipefd[1]);
        Read(pipefd[0]);
        close(pipefd[0]);
        int status=0; 
        waitpid(id,&status,0);
        cout<<"receive signal : "<<(status& 0x7f)<<endl;

image-20231212125908227

收到13号信号,进程被终止.13号信号为SIGPIPE

image-20231212125942952

所以

读端被关闭.写端也被关闭


综上,匿名管道通信时有四种情况:

  1. 读写端正常.当管道中没有内容时,读端会阻塞等待
  2. 读写端正常.当管道写满时,写端会阻塞等待读端读取
  3. 写端被关闭,读端读到文件结尾,返回0.但此时不会被阻塞
  4. 读端被关闭.节省资源.写端也被关闭

所以我们可以得到匿名管道有以下特征:

  1. 具有血缘关系的进程才可以进行通信
  2. 管道只能单向通信
  3. 父子进程是会进程协同的,同步与互斥
  4. 管道是面向字节流的
  5. 管道基于内存级文件.其生命周期随进程

2. 命名管道

与匿名管道大同小异.都是基于文件级的通信,但是命名管道在指定路径下创建了一个具有名称的内存级文件.

这使得 没有血缘关系的进程也能够看到同一份资源,所以此时,不同的进程也可以进行通信了

2.1 接口介绍

我们可以使用mkfifo 依照创建文件的方法,在指定目录下创建出内存级文件.

image-20231212131141204

其表示文件属性的权限位,显示其为一个管道文件.

删除这个管道文件我们通常使用unlink

image-20231212131253908

在语言层面上,也为我们封装了该接口

image-20231212131352469

pathname:为指定路径 mode:为权限

#pragma once
#include <sys/stat.h>
#include <unistd.h>
#include"log.hpp"
#define FIFO_PATH "./myfifo"
class InitPipe{
public:
    InitPipe()
    {
        int n=mkfifo(FIFO_PATH,MODE);
        if(n!=0)
        {
            log(FATAL,"create pipe failed");
            exit(0);
        }
    }
    ~InitPipe()
    {
        unlink(FIFO_PATH);
    }
};

接口使用

为了避免每次退出进程时,还要去手动释放该管道文件.所以利用RAII的方式来存储管道文件

之后使用该管道时,根据读写文件那一套来即可.

sever.cpp:

#include"log.hpp"
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include "create_pipe.hpp"
int main(int argc,char * argv[])
{
    InitPipe pipe;
    Log log(SCREEN|CLASS_FILE);
    
    int fd=open(FIFO_PATH,O_RDONLY);
    if (fd < 0)
    {
        log(FATAL, "error string: %s, error code: %d", errno, errno);
    }
    while(true)
    {
        char sz[1024]={0};
        int x=read(fd,sz,sizeof(sz));
        if(x > 0)
        {
            cout<<"client say# "<< sz <<endl;
            log(INFO,sz);
        }
        else if(x==0)
        {
            
            break;
        }
        else break;
    }
 
}

client.cpp:

#include <fcntl.h>
#include<unistd.h>
#include<iostream>
#include<string>
using namespace std;
int main()
{
    int fd=open("./myfifo",O_WRONLY);
    if (fd < 0)
    {
        cout<<"failed"<<endl;
        exit(0);

    }
    string s1;
    while(true)
    {
        cout<<"client say @";
        getline(cin,s1);
        write(fd,s1.c_str(),s1.length());
    }
    
}

image-20231212132046069

(上文用到的log可以查看我下一篇博客的日志插件)

3. 共享内存

共享内存相较于前两种通信方式,速度上有明显的优势.

其不会涉及到复制写入/读取的内容.而是直接写入内存.而管道是将内容复制到文件当中,在复制出来

3.1 通信原理

共享内存的本质就是将一段真实的 物理内存,映射到PCB的共享内存当中.当多个进程映射同一个物理内存时,通过对内存数据直接的读写,就可以实现通信.

同样,我们需要使用系统调用接口去申请这段共享内存.使用系统调用接口去释放这段共享内存

共享内存没有像管道一样的同步互斥,需要用户自己去规定.

3.2 接口介绍

申请共享内存:

image-20231212133159479

key:可以理解为申请共享内存的一段密钥,该密钥在系统中是唯一的,就可以申请到唯一的一块共享内存,也是系统内核去校验两个共享是否相同的一个手段

通过ftok去申请:

image-20231212133445721

该函数是一个算法结合两个参数去生成一个唯一的key.所以这两个参数可以根据使用情况去定制.

若申请成功,则返回key,若申请失败则返回-1,并设置errno.

size:为申请的共享内存大小,一般为4096的整数倍.

shmflg具有以下两个值:

  1. IPC_CREAT (申请一段共享内存,若不存在则创建并返回shmid,若存在则返回shmid(用户级的key))
  2. IPC_CREAT | IPC_EXEL| 八进制权限信息 (申请一段共享内存,若不存在则创建并返回shmid,若存在则创建失败) 需要带上权限信息

为什么会有第二个选项呢?用来保证你申请的共享内存是一段新的,唯一被您使用的内存

shmid是什么?与上文的key类似,内核使用key去操作控制共享内存,而用户通过shmid完成如上操作

返回值为 成功返回shmid,失败返回-1,并设置errno

image-20231212134537899

获取共享内存地址.

char * address = (char *)shmat(shmid,nullptr,0);

image-20231212220619538

**取消挂接该地址.**若成功则返回0,失败返回-1

image-20231212220746275

对该共享内存进行控制,一般用来删除共享内存

cmd参数填上IPC_RMID 表示删除当前内存

下面是一个简单的示例demo:

config.hpp

#pragma once
#include <cerrno>
#include <cstring>
#include <sys/ipc.h>
#include<sys/types.h>
#include <sys/shm.h>
#include<stdlib.h>
#include<iostream>
using namespace std;
string PATH= "/tmp";
int MODE= 255;
#define SIZE 4096
class Init{
public:
    Init(){
        _key=ftok(PATH.c_str(), MODE);
        if(_key==-1)
        {
            cout<<"create failed"<<endl;
            strerror(errno);
            exit(0);
        }
    }
    int CreateShm()
    {
        //需要加上权限 否则创建失败
        int shmid=shmget(_key,SIZE,IPC_CREAT|IPC_EXCL|0666);
        if(shmid==-1)
        {
            cout<<"creatshm failed"<<endl;
            strerror(errno);
            exit(0);
        }
        return shmid;
    }
    int GetShm()
    {
        int shmid=shmget(_key,SIZE,IPC_CREAT|0666);
        if(shmid==-1)
        {
            cout<<"getshm failed"<<endl;
            strerror(errno);
            exit(0);
        }
        return shmid;
    }
    void destoryShmid(int shmid)
    {
        if(shmctl(shmid,IPC_RMID, nullptr)!=-1)
            cout<<"destory success";
    }
    
private:
    key_t _key;
};

processaa.cpp 接受方

#include "config.hpp"
#include <cstddef>
#include <sys/shm.h>
#include<iostream>
using namespace std;
int main()
{
    Init it;
    int shmid=it.GetShm();
    cout<<"get success a"<<endl;
    char * address = (char *)shmat(shmid,nullptr,0);
    while(true)
        cout<<address<<endl;
}

processbb.cpp 发送方

#include "config.hpp"
#include<iostream>
#include <cstddef>
using namespace std;
int main()
{
    Init it;
    int shmid=it.CreateShm();
    cout<<"create success b"<<endl;
    char * address = (char *)shmat(shmid,nullptr,0);
    cout<<"address success "<<endl;
    int cnt=5;
    while (cnt-->0) {
        fgets(address,4096,stdin);

    }
    it.destoryShmid(shmid);    
}

image-20230905164632777

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1433005.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java GC-常见垃圾回收器

目录 前言一、垃圾回收器分类二、垃圾回收器介绍1、Serial 收集器2、ParNew 收集器3、Parallel Scavenge 收集器4、Serial Old 收集器5、Parallel Old 收集器6、CMS 收集器&#xff08;多线程标记清除算法&#xff09;7、G1 收集器 三、项目中垃圾收集器选型 前言 Java的垃圾回…

零基础学编程系列,从入门到精通,中文编程开发语言工具下载,编程构件容器件之控制面板构件用法

零基础学编程系列&#xff0c;从入门到精通&#xff0c;中文编程开发语言工具下载&#xff0c;编程构件容器件之控制面板构件用法 一、前言 编程入门视频教程链接 https://edu.csdn.net/course/detail/39036 编程工具及实例源码文件下载可以点击最下方官网卡片——软件下载…

银河麒麟 aarch64 Mysql环境安装

一、操作系统版本信息 组件版本操作系统Kylin V10 (SP3) /(Lance)-aarch64-Build23/20230324Kernel4.19.90-52.22.v2207.ky10.aarch64MySQLmysql-8.3.0JDK1.8.0_312 二、MySQL下载 官网下载地址&#xff1a;https://dev.mysql.com/downloads/mysql/ 三、MySQL 安装 3.1 删…

幻兽帕鲁一键迁移到服务器,本地迁移存档后为什么又需要重新创建角色?

最近很多人在玩幻兽帕鲁的时候&#xff0c;想要把本地游戏存档迁移到服务器上继续玩&#xff0c;有些朋友却发现通过阿里云或者腾讯云上的一键迁移导入之后&#xff0c;进入游戏后发现又要重头开始玩。 这是为什么呢&#xff1f;其中可能的原因&#xff0c;我想就是因为它们的一…

spring boot3x登录开发-上(整合jwt)

⛰️个人主页: 蒾酒 &#x1f525;系列专栏&#xff1a;《spring boot实战》 &#x1f30a;山高路远&#xff0c;行路漫漫&#xff0c;终有归途。 目录 前置条件 jwt简介 导依赖 编写jwt工具类 1.配置项直接嵌入代码&#xff0c;通过类名.静态方法使用 2.配置项写到…

ROS2学习(一):Ubuntu 20.04 安装 ROS2(Galactic Geochelone)

文章目录 一、ROS2(Galactic Geochelone)介绍二、ROS2(Galactic Geochelone)安装1. 设置语言环境2. 配置ubuntu universe仓库3. 配置ros2 apt仓库4. 安装ros25. 安装情况测试 一、ROS2(Galactic Geochelone)介绍 官方文档 二、ROS2(Galactic Geochelone)安装 1. 设置语言环…

[嵌入式AI从0开始到入土]5_炼丹炉的搭建(基于wsl2_Ubuntu22.04)

[嵌入式AI从0开始到入土]嵌入式AI系列教程 注&#xff1a;等我摸完鱼再把链接补上 可以关注我的B站号工具人呵呵的个人空间&#xff0c;后期会考虑出视频教程&#xff0c;务必催更&#xff0c;以防我变身鸽王。 第一章 昇腾Altas 200 DK上手 第二章 下载昇腾案例并运行 第三章…

云上未来:探索云计算的技术变革与应用趋势

一、云计算的起源和演进 1.1 早期计算模型 在探讨云计算的起源和演进之前&#xff0c;理解早期的计算模型对于构建全面的视角至关重要。早期计算模型的发展奠定了云计算的基础&#xff0c;为其演进提供了技术和理念的支撑。 1.1.1 集中式计算模型 在计算技术的早期阶段&…

使用Python和HTTP代理进行API请求

Python&#xff0c;这个简单易学又功能强大的编程语言&#xff0c;在网络爬虫、数据分析、自动化任务等领域都有着广泛的应用。而当我们需要从外部网站获取数据时&#xff0c;API&#xff08;应用程序接口&#xff09;就成了我们的得力助手。但有时候&#xff0c;某些网站会对A…

问题:下面几个句子,是对排比修辞方法的论述,正确的是() #笔记#知识分享

问题&#xff1a;下面几个句子&#xff0c;是对排比修辞方法的论述&#xff0c;正确的是&#xff08;&#xff09; A&#xff0e;排比是结构上相同或相似的短语或句子构成&#xff0c;成串排列&#xff0c;以增强语势 B&#xff0e;排比是侧重于同一词语或同一句子一再出现&a…

WordPress Plugin HTML5 Video Player SQL注入漏洞复现(CVE-2024-1061)

0x01 产品简介 WordPress和WordPress plugin都是WordPress基金会的产品。WordPress是一套使用PHP语言开发的博客平台。该平台支持在PHP和MySQL的服务器上架设个人博客网站。WordPress plugin是一个应用插件。 0x02 漏洞概述 WordPress Plugin HTML5 Video Player 插件 get_v…

游戏视频录制软件推荐,打造专业电竞视频(3款)

随着游戏产业的快速发展&#xff0c;越来越多的玩家开始关注游戏视频录制软件。一款好的录制软件不仅可以帮助玩家记录游戏中的精彩瞬间&#xff0c;还可以让其与他人分享自己的游戏体验。接下来&#xff0c;我们将介绍三款热门的游戏视频录制软件&#xff0c;并对其进行详细的…

Electron实战(一):环境搭建/Hello World/打包exe

文章目录 Electron安装Node.jsNodeJs推荐配置开始Electron项目创建index.js文件创建src目录运行打包生成exe生成安装包踩坑 下一篇Electron实战(二)&#xff1a;将Node.js和UI能力&#xff08;app/BrowserWindow/dialog)等注入html Electron Electron是一个使用JavaScript, HT…

2024三掌柜赠书活动第八期:Web3与DAO:下一代互联网演进逻辑

目录 前言关于Web3和DAO关于《Web3与DAO&#xff1a;下一代互联网演进逻辑》编辑推荐内容简介作者简介精彩书评图书目录书中前言/序言《Web3与DAO&#xff1a;下一代互联网演进逻辑》全书速览结束语 前言 随着区块链技术的崛起&#xff0c;Web3和DAO成为了当前互联网领域炙手…

vivado 综合、时序分析

以下综合 运行完成后&#xff0c;将打开Synthesis Completed对话框&#xff0c;如下所示图形 选择其中一个选项&#xff1a; •运行实施&#xff1a;启动当前实施项目的实施设置。 •打开综合设计&#xff1a;打开综合网表、活动约束集和将目标设备集成到综合设计环境中&…

利用k8s Infra 容器,解决pod网络故障注入的问题

目录 一、infra容器作用 二、pod网络故障注入问题 三、充分利用pod infra容器 一、infra容器的作用 我们知道&#xff0c;在kubernetes中&#xff0c;pod中容器的资源隔离主要通过namespace和cgroup来实现。那如果我们需要为pod中的容器共享某种资源应该怎么做。kubernetes …

组合数学基础

隔板法 X 1 X 2 . . . X n m , X i > 0 X_1X_2...X_nm,\quad X_i>0 X1​X2​...Xn​m,Xi​>0 求方程解的个数 求方程解的个数 求方程解的个数 m 个球插入 n − 1 个板将 m 个球分成 n 份 m个球插入n-1个板将m个球分成n份 m个球插入n−1个板将m个球分成n份 方程…

如何排查常规软件问题 - 面向 Linux 初级用户的教程

笔者从 14 年做开源软件以来&#xff0c;接触了众多 Linux 新手用户&#xff0c;这里我为这类用户总结了一些常见的问题排查方法&#xff0c;希望能帮助到大家。如果你已经工作多年&#xff0c;对于下面提到的思路和方法应该非常熟悉&#xff0c;如果对某一条感到陌生&#xff…

视频美颜SDK开发指南:从入门到精通的技术实践

美颜SDK是一种强大的工具&#xff0c;它不仅仅可以让用户在实时视频中获得光滑的肌肤和自然的妆容&#xff0c;从简单的滤镜到复杂的人脸识别&#xff0c;美颜SDK涵盖了广泛的技术领域。 一、美颜SDK的基本原理 美颜SDK包括图像处理、人脸检测和识别、滤镜应用等方面。掌握这些…

瑞_23种设计模式_抽象工厂模式

文章目录 1 抽象工厂模式&#xff08;Abstract Factory Pattern&#xff09;1.1 概念1.2 介绍1.3 小结1.4 结构 2 案例一2.1 需求2.2 代码实现 3 案例二3.1 需求3.2 代码实现 4 总结4.1 抽象工厂模式优缺点4.2 抽象工厂模式使用场景4.3 抽象工厂模式 VS 工厂方法模式4.4 抽象工…