进程间的通信(1)(理解管道特性,匿名命名管道,进程池,systeam V共享内存是什么及优势)重点理解代码!

🎬 胖咕噜的稞达鸭个人主页
🔥 个人专栏 : 《数据结构《C++初阶高阶》
《Linux系统学习》
《算法日记》

⛺️技术的杠杆,撬动整个世界!


理解层面

为什么要进程间通信?

• 数据传输:一个进程需要将它的数据发送给另一个进程

• 资源共享:多个进程之间共享同样的资源。

• 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。

• 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

怎么通信?

进程间通信的本质:先让不同的进程看到同一份资源【内存】,然后才有通信的条件。

管道

什么是管道?

匿名管道通常用来父子进程。

真的管道,不需要磁盘。

管道原理:

我们是怎么保证两个进程打开的是同一个管道文件?

子进程继承父进程的文件描述符表。

现在创建代码来看父子进程是如何实现管道间的联系的:

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstdio>
#include <cstring>

void child_write(int wfd)
{
    char buffer[1024];
    int cnt = 0;
    while(true)
    {
        snprintf(buffer,sizeof(buffer),"I am  child,pid :%d,cnt:%d",getpid(),cnt++);
        write(wfd,buffer,strlen(buffer));
        sleep(1);
    }
}
void father_read(int rfd)
{
    char buffer[1024];
    while(true)
    {
        buffer[0] = 0;
        ssize_t n = read(rfd,buffer,sizeof(buffer) - 1);
        if(n > 0)
        {
            buffer[n] =0;
            std::cout <<"child say: " << buffer <<std::endl;
        }
    }
}
int main()
{
    //1.创建管道
    int fds[2] = {0};//fds[0]:读端,fds[1]:写端
    int n = pipe(fds);
    if(n < 0)
    {
        std::cerr <<"pipe error" << std::endl;
        return 1;
    }
    std::cout <<"fd[0]: " <<fds[0] <<std::endl;
    std::cout <<"fd[1]: " <<fds[1] <<std::endl;
   //2。创建子进程
   pid_t id = fork();
   if(id == 0)
   {
    //child
    //code
    //3.关闭不需要的读写端,形成通信信道    
    close(fds[0]);
    child_write(fds[1]);
    close(fds[1]);
    exit(0);
   }
   //3.关闭不需要的读写端,形成通信信道
   //f->r,c->w
   close(fds[1]);//父进程读
   father_read(fds[0]);
   waitpid(id,nullptr,0);
   close(fds[0]);
    return 0;
}
cpp 复制代码
ubuntu@VM-0-4-ubuntu:~/test/linux_-learning2026/lesson23$ make clean
rm -f testPipe	
ubuntu@VM-0-4-ubuntu:~/test/linux_-learning2026/lesson23$ make
g++ -o testPipe testPipe.cc
ubuntu@VM-0-4-ubuntu:~/test/linux_-learning2026/lesson23$ ./testPipe
fd[0]: 3
fd[1]: 4
child say: I am  child,pid :83722,cnt:0
child say: I am  child,pid :83722,cnt:1
child say: I am  child,pid :83722,cnt:2
child say: I am  child,pid :83722,cnt:3
child say: I am  child,pid :83722,cnt:4
child say: I am  child,pid :83722,cnt:5
管道的特性:
  1. 匿名管道:只能用来进行有血缘关系的进程进行进程间通信。(常用于父子);
  2. 管道文件,自带同步机制;(父进程读,子进程写,子进程不写,父进程就会阻塞);
  3. 管道是面向字节流;
  4. 管道是单项通信的;(任何一个时刻,一个发,一个收---半双工;任何一个时刻,可以同时发收---全双工)单项通信是属于半双工的一种特殊情况。
  5. (管道)文件的生命周期,是随进程的,进程不主动关了,管道文件就会一直在操作系统中出现。
4种通信情况
  1. 写的快,读得慢---写满了写就要阻塞等待(读完才能写)
  2. 写得慢,读得快---读完了就要等写出来(写出来才读)
  3. 写关了,读继续---读就会读到返回值为0的
  4. 读关闭,写继续---写端在写入毫无意义,OS不会做任何没有意义的事情的,此时OS就会杀掉(kill 13 SIGPIPE)

基于匿名管道---进程池

如果父进程将所有的任务都给独一个子进程,那么这个子进程就会特别忙,其他子进程很闲。

选择一个信道,负载均衡的选择一个子进程,完成任务;

方式:

  1. 轮询;
  2. 随机选择
  3. channel 添加负载指标,父进程在分配任务的时候永远会选择负载指标最小的子进程进行分配。
cpp 复制代码
//main.cc
#include "ProcessPool.hpp"
int main()
{
    // 这个代码,有一个藏得比较深的bug --- TODO
    // 创建进程池对象
    ProcessPool pp(gdefaultnum);
    // 启动进程池
    pp.Start();
    // 自动派发任务
    int cnt = 10;
    while(cnt--)
    {
        pp.Run();
        sleep(1);
    }
    // 回收,结束进程池
    pp.Stop();
    return 0;
}
Makefile
cpp 复制代码
process_pool:Main.cc
	g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm -f process_pool

匿名管道 :只能用来进行真正有血缘关系的进程进行进程间通信(通常与父子)

命名管道

如果两个进程不相关。该如何进行通信呢?

进程A:打开了一个文件/a/b/c.txt

进程B:打开了一个文件/a/b/c.txt
问题:内核中操作系统会不会把这个/a/b/c.txt文件(inode/文件内容等)在内存中加载两次?

不会!

操作系统不干没必要的事情,通过打开同一个路径下的同一个文件,让不同的进程,看到了同一份资源。

所以,父子进程可以向同一个显示器文件进行打印。

server.cc:

设置权限掩码:umask(0),server.cc创建管道文件,链接clientserverserver既可以新建管道文件,又可以删除了。

读取文件,以只读方式打开文件;

正常读取文件,

cpp 复制代码
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include "comm.hpp"

int main()
{
    //创建管道文件
    NamedFifo fifo("/",FILENAME);
    //文件操作
    FileOper readerfile(PATH,FILENAME);
    readerfile.OpenForRead();
    readerfile.Read();
    readerfile.Close();
    return 0;
}

/* int main()//服务端要先创建管道,其次需要打开文件,再后来需要正常的读
{
    NamedFifo fifo(".", "fifo");
    umask(0);
    //新建管道
    int n = mkfifo(FIFO_FILE,0666);//进程掩码
    if(n < 0)
    {
        std::cerr << "mkfifo error " <<std::endl;
    }
    std::cout << "mkfifo sucess" <<std::endl;
    //打开文件
    int fd = open(FIFO_FILE,O_RDONLY);
    if(fd < 0)
    {
        std::cerr << "open fifo error" <<std::endl;
        return 2;
    }
    std::cout << "open fifo success" <<std::endl;
    //正常的读
    while(true)
    {
        char buffer[1024];
        int number = read(fd,buffer,sizeof(buffer) - 1);
        if(number > 0)
        {
            buffer[number] = 0;
            std::cout << "Client Say #" <<std::endl;
        }
        else if(number == 0)
        {
            std::cout << "Client   Quit? Me too "<< number <<std::endl;
            break;
        }
        else
        {
            std::cerr <<"read error" <<std::endl;
            break;
        }
    }
    close(fd);
    //删除管道
    int n = unlink(FIFO_FILE);
    if(n == 0)
    {
        std::cout << "remove fifo success" <<std::endl;
    }
    else
    {
        std::cout << "remove fifo failed" <<sd::endl;
    }
} */
client.cc:

客户端要写入文件

cpp 复制代码
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include "comm.hpp"

int main()//先描述在组织
{
    //write:客户端用来写入
    int fd = open(FIFO_FILE,O_WRONLY);
    if(fd < 0)
    {
        std::cerr << "open fifo error" <<std::endl;
        return 1;
    }
    std::cout << "open fifo success" <<std::endl;//打开成功

    //写入操作
    std::string message;
    int cnt = 1;//用来接收,消息编号,cnt表示接收到了多少条信息
    pid_t id = getpid();//用来标记进程的PID,哪个进程是接收的
    while(true)
    {
        std::cout << "Please Enter# ";
        std::getline(std::cin,message);
        message += (", message number :" + std::to_string(cnt++) + ",[" + std::to_string(id) +"]");
        write(fd,message.c_str(),message.size());
        //用户输入:Hello   实际发送:Hello, message number:1,[1234]
    }
    close(fd);
    return 0;
}
comm.hpp
cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <cstdio>

#define PATH "."
#define FILENAME "fifo"
#define ERR_EXIT(m)   \
    do                  \
    {                   \
        perror(m);      \
        exit(EXIT_FAILURE);\
    }while (0)         \
    
class NamedFifo
{
public:
    NamedFifo(const std::string &path,const std::string &name)
    : _path(path),_name(name)
    {
        _fifoname = _path + "/" + _name;
        umask(0);
        //新建管道
        int n = mkfifo(_fifoname.c_str(),0666);
        if(n < 0)
        {
            ERR_EXIT("mkfifo");
        }
        else
        {
            //TODO
        }
        std::cout << "mkfifo success" <<std::endl;
    }
    
    ~NamedFifo()
    {
        int n = unlink(_fifoname.c_str());
        if(n == 0)
        {
            ERR_EXIT("unlink");
        }
        else
        {
            std::cout << "remove fifo failed" <<std::endl;
        }
    }

private:
    std::string _path;//文件路径
    std::string _name;//文件名字
    std::string _fifoname;//命名管道名称(命名管道也是一个文件)
};

class FileOper
{
public:
    FileOper(const std::string &path,const std::string &name)
    :_path(path),_name(name),_fd(-1)
    {
        _fifoname = _path + "/" + _name;
    }
    void OpenForRead()
    {
        //write没有执行open时,read就会在open内部阻塞
        _fd = open(_fifoname.c_str(),O_RDONLY);
        if(_fd < 0)
        {
            ERR_EXIT("open");
        }
        std::cout << "open fifo success" << std::endl;
    }
    void OpenForWrite()
    {
       _fd = open(_fifoname.c_str(),O_WRONLY);
       if(_fd < 0)
        {
            ERR_EXIT("open");
        }
        std::cout << "open fifo success" << std::endl;
    }
    void Write()
    {
        //写入
        std::string message;
        int cnt = 1;
        pid_t id = getpid();
        while(true)
        {
            std::cout << "Please Enter #";
            std::getline(std::cin,message);
            message += (", message number" + std::to_string(cnt++) + "[ message id" + std::to_string(id) + "]");
            write(_fd,message.c_str(),message.size());
        }
    }
    void Read()
    {
        while(true)
        {
            char buffer[1024];
            int number = read(_fd,buffer,sizeof(buffer) - 1);
            if(number > 0)
            {
                buffer[number] = 0;
                std::cout << "Client Say# " <<std::endl;
            }
            else if(number == 0)
            {
                std::cout <<"Client quit ! me too!" <<std::endl;
            }
            else
            {
                std::cerr << "read error" <<std::endl;
                break;
            }
        }
    }
    void Close()
    {
        if(_fd > 0)
            close(_fd);
    }
    ~FileOper()
    {}
private:
    std::string _name;
    std::string _path;
    std::string _fifoname;
    int _fd;
};
Makefile
cpp 复制代码
.PHONY:all
all:client server
client:client.cc
    g++ -o $@ $^ -std=c++11
server:server.cc
    g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
    rm -f client server

总体概述

这个程序实现了一个简单的客户端-服务器模型:

  • Server(服务器): 创建一个命名管道,然后以只读方式打开它,等待并读取客户端发来的消息。
  • Client(客户端) : 以只写方式打开同一个命名管道,然后从标准输入读取用户输入并发送给服务器。
    核心机制 : 命名管道是一个存在于文件系统中的特殊文件类型(类型为 p),它提供了一个进程间单向或半双工通信的通道。数据通过内核在管道中进行缓冲和传输。

1. comm.hpp - 公共头文件与类定义

这个文件定义了项目中使用的常量、错误处理宏和两个核心的类,用于封装命名管道的创建/销毁和文件操作。

a) 宏定义与常量
复制代码
 #define PATH "."          // 管道文件所在的目录,当前目录 
 #define FILENAME "fifo"   // 管道文件的名称
 #define ERR_EXIT(m) ...   // 错误处理宏,打印错误信息并退出程序`
b) NamedFifo

接口

  • 构造函数 NamedFifo(const std::string &path, const std::string &name) :
    • 功能 : 根据提供的路径和文件名构造完整的管道文件路径,并以 0666权限(受 umask(0)影响,最终权限为 666)创建一个命名管道文件。
    • 原理 : 内部调用 mkfifo()系统调用。如果创建失败(例如文件已存在),则通过 ERR_EXIT宏终止程序。
  • 析构函数 ~NamedFifo() :
    • 功能 : 在对象销毁时,自动删除(通过 unlink()系统调用)它所创建的命名管道文件。
    • 原理 : 确保资源被正确清理。
      实现细节与问题
      RAII(资源获取即初始化)思想: 该类试图遵循RAII原则,在构造函数中获取资源(创建管道),在析构函数中释放资源(删除管道)。
c) FileOper

接口

  • 构造函数 FileOper(...): 仅存储路径和文件名信息,不执行任何操作。
  • OpenForRead() :
    • 功能 : 以 O_RDONLY(只读)模式打开命名管道。
    • 原理 : 调用 open()系统调用。由于命名管道的特性,如果此时没有进程以写方式打开该管道,此 open调用会阻塞,直到有客户端打开管道进行写入。
  • OpenForWrite() :
    • 功能 : 以 O_WRONLY(只写)模式打开命名管道。
    • 原理 : 同样调用 open()。如果此时没有进程以读方式打开该管道,此调用也会阻塞,直到有服务器打开管道进行读取。
  • Read() :
    • 功能 : 在一个循环中,从已打开的文件描述符 _fd中读取数据。
    • 原理 : 使用 read()系统调用。当客户端关闭写端(如 Ctrl+C或程序结束)时,read会返回 0,循环会打印 "Client quit! me too!" 并退出。
  • Write() :
    • 功能 : 从标准输入读取用户输入,并加上进程ID和消息编号后,通过 write()系统调用写入管道。
  • Close(): 关闭文件描述符。
  • 析构函数 : 空,因为 Close()需要被显式调用。
    实现细节与问题
  1. 职责分离 : 这个类将"管道管理"(NamedFifo)和"文件I/O操作"(FileOper)分离开,设计上更清晰。

2. server.cc - 服务器端程序

实现的接口/逻辑

服务器端的 main函数是程序的入口点。它使用了 NamedFifoFileOper这两个类来完成其功能。
实现原理与流程

  1. 创建管道NamedFifo fifo("/", FILENAME);
  2. 准备读取FileOper readerfile(PATH, FILENAME);
    • 创建了一个文件操作对象。
  3. 打开管道readerfile.OpenForRead();
    • 以只读方式打开管道。这一步会发生阻塞,直到有客户端打开同一个管道进行写入。
  4. 读取数据readerfile.Read();
    • 进入无限循环,不断从管道中读取客户端发来的消息并打印(尽管打印语句有误)。当客户端关闭连接时,read返回0,服务器也随之退出循环。
  5. 关闭文件 : `readerfile.Close();
    • 关闭文件描述符。
  6. 清理管道NamedFifo对象 fifomain函数结束时超出作用域,其析构函数被调用,从而删除了管道文件。

关键细节

  • 阻塞行为 : 服务器的 OpenForRead和客户端的 open(在 FileOper::OpenForWrite中) 是相互阻塞 的。必须双方都执行 open,才能成功建立连接,程序才会继续往下执行。这是一种简单的同步机制。
  • 生命周期 : 管道的生命周期与 NamedFifo对象 fifo绑定。

3. client.cc - 客户端程序

实现的接口/逻辑

客户端的 main函数非常简单直接,它只使用了 FileOper类来打开管道并发送数据。
实现原理与流程

  1. 打开管道int fd = open(FIFO_FILE, O_WRONLY);
    • 注意,客户端没有 使用 NamedFifo类。它直接调用 open系统调用。这意味着管道必须已经存在 ,否则 open会失败。因此,必须先启动服务器来创建管道。
    • open调用会阻塞 ,直到服务器调用 OpenForRead打开了管道的另一端。
  2. 写入数据 : 进入一个循环,提示用户输入,然后将格式化的消息(包含消息序号和自身PID)通过 write系统调用写入管道。
  3. 关闭文件 : 循环是无限的,所以正常情况下 close不会被执行,除非用户强制终止程序(如 Ctrl+C)。当客户端进程终止时,其打开的文件描述符会被自动关闭,这会导致服务器端的 read调用返回 0,从而知道客户端已断开。
    关键细节
  • 依赖关系: 客户端依赖于服务器先创建好管道。这是一种紧耦合的设计。
  • 无清理责任: 客户端不负责创建或删除管道,只负责使用。

总结与运行流程

  1. 编译 : 将三个文件一起编译:g++ server.cc -o serverg++ client.cc -o client
  2. 运行
    • 在一个终端启动服务器:./server。此时它会创建管道,并在 OpenForRead阻塞
    • 在另一个终端启动客户端:./client。客户端的 open会先阻塞,但一旦服务器也执行了 open,双方的 open都会成功返回,程序继续。
  3. 通信: 在客户端终端输入文本并按回车,消息会通过命名管道发送到服务器,服务器将其(在修复打印错误后)显示出来。
  4. 结束 : 在客户端按 Ctrl+C强制退出,服务器检测到 read返回0,会打印退出信息,然后清理并退出,同时删除管道文件。

system V共享内存

是什么

共享内存 ​ 就是操作系统在物理内存中开辟出一块区域,然后允许两个或多个进程将这个区域 "映射"到它们各自的虚拟地址空间中。这样一来,这块内存对于这几个进程来说,就像是自己本地内存的一部分。

物理上只有一份数据拷贝,但在多个进程的虚拟地址空间中都有指向它的"指针"。

优势在哪里?(跟传统IPC方式对比)

"最快的IPC形式"的原因分析

要理解为什么它最快,我们必须对比其他IPC机制(如管道、消息队列、套接字)是如何工作的。

传统的IPC方式

我们以最常见的 管道​ 为例:

  1. 发送方进程 (比如进程A):
    • 调用 write()系统调用。
    • CPU从用户态切换到内核态。
    • 内核将数据从进程A的用户缓冲区 拷贝 到内核缓冲区(在内核空间中)。
    • 系统调用返回,CPU从内核态切换回用户态。
  2. 接收方进程 (比如进程B):
    • 调用 read()系统调用。
    • CPU再次从用户态切换到内核态。
    • 内核将数据从内核缓冲区 拷贝 到进程B的用户缓冲区。
    • 系统调用返回,CPU切换回用户态。
      整个过程发生了两次数据拷贝:
      进程A的用户空间 -> 内核空间 -> 进程B的用户空间
      每次系统调用还伴随着昂贵的 用户态/内核态上下文切换
共享内存的方式("零拷贝"模式)

使用共享内存时:

  1. 设置阶段 (只需一次,通常由一个进程创建并初始化共享内存,然后其他进程附加到它):
    • 进程调用 shmget()创建,shmat()附加到自己的地址空间。这也是系统调用,需要切换内核态。但这是一次性开销。
  2. 数据传输阶段
    • 进程A直接通过指针操作(例如 *ptr = data;)将数据写入共享内存区域。没有系统调用!
    • 进程B直接从同一个共享内存区域读取数据(例如 data = *ptr;)。*也没有系统调用!
      整个过程发生了零次数据拷贝和零次上下文切换!
      数据就在那里,进程A改了,进程B立刻就能看到(需要考虑同步问题,但这不影响数据传输本身的速度)。
相关推荐
电气铺二表姐137744166152 小时前
超市冷柜专区电能计量方案:高能耗设备独立监测与优化
运维·物联网·能源
Coder个人博客2 小时前
Linux6.19-ARM64 boot Makefile子模块深入分析
linux·车载系统·系统架构·系统安全·鸿蒙系统
德彪稳坐倒骑驴2 小时前
Sqoop入门常用命令
数据库·hadoop·sqoop
资深web全栈开发2 小时前
pg on delete 策略探讨
数据库·pg
玖日大大2 小时前
Milvus 深度解析:开源向量数据库的技术架构、实践指南与生态生态
数据库·开源·milvus
雪域迷影2 小时前
Node.js中使用node-redis库连接redis服务端并存储数据
数据库·redis·node.js
小北方城市网2 小时前
微服务接口设计实战指南:高可用、易维护的接口设计原则与规范
java·大数据·运维·python·微服务·fastapi·数据库架构
可爱又迷人的反派角色“yang”2 小时前
k8s(五)
linux·运维·docker·云原生·容器·kubernetes
爱吃生蚝的于勒2 小时前
【Linux】进程间通信之匿名管道
linux·运维·服务器·c语言·数据结构·c++·vim