27,28,29进程通信和匿名管道详解

从现在开始就开始用ubuntu和vscode了

先大概说一下Centos和ubutun,自己碰到的一些会用到的不同点

1.Ubuntu 默认 root 是 "锁定(禁用密码)" 状态,不能直接 su - root 登录;要sudo passwd root 给 root 设密码,就解锁了。CentOS 默认 root 有密码,可以直接 su - 切 root。

2.**Ubuntu 系统里自带的那个 ubuntu 用户 = 特权用户(有 sudo 权限)你自己新建的普通用户 = 普通用户(默认没 sudo 权限,要自己提权:**sudo usermod -aG sudo 你的用户名,)

3.包管理器

ubutun:sudo apt install 软件名

centos:sudo yum install 软件名

进程通信

为什么要进行进程间通信

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

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

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

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

怎么通信

通信的本质就是先让不同的进程先看到同一份资源("内存"),然后再有通信的条件

管道

什么是管道

• 管道是Unix中最古老的进程间通信的形式。

• 我们把从⼀个进程连接到另⼀个进程的⼀个数据流称为⼀个"管道"

匿名管道

以两张图来理解,通过匿名管道来理解进程通信

1.父进程调用pipe()创建管道

  • 内核会创建一个匿名管道(内存缓冲区),它**没有文件路径、没有文件名,只存在于内核内存中,**不会在磁盘上创建任何文件
  • 管道天生区分读端和写端:pipefd[0]绑定读端,pipefd[1]绑定写端。
  • 父进程的文件描述符表中,fd=3指向管道读端,fd=4指向管道写端。

2.父进程调用fork()创建子进程

  • 子进程会完整拷贝父进程的文件描述符表,所以子进程也有fd=3(读端)和fd=4(写端),且都指向同一个管道对象
  • 此时管道的引用计数变为 2,内核不会轻易释放这个缓冲区。
  • 这一步就解决了 "怎么让两个进程打开同一个管道" 的问题:靠的就是fork()的文件描述符继承机制。

3.父子进程关闭不用的一端,形成单向通信

  • 父进程 :关闭读端pipefd[0],只保留写端pipefd[1],负责往管道写数据。
  • 子进程 :关闭写端pipefd[1],只保留读端pipefd[0],负责从管道读数据。
  • 此时管道就变成了一个单向通道:父进程写的数据,子进程可以直接读到,实现了父子进程间的单项通信。

1点细节:管道的 "复用代码" 设计,这就是 Linux "一切皆文件" 的体现:

  • 管道和普通文件一样,都可以用read()/write()系统调用来操作。
  • 内核为管道单独设计了一套文件操作函数集(struct file_operations),但用户态不需要关心,只需要像操作普通文件一样用系统调用即可。

补充一个小知识点:

size_t = unsigned long(64 位无符号)

  • 32 位系统

    • int = 32 位
    • ssize_t = 32 位→ 这时候可以混用
  • 64 位系统(现在 99% 都是)

    • int = 32 位(永远不变)
    • ssize_t = 64 位 (等于 signed long)→ 长度不一样!用 int 会截断、出错!

又Linux的系统调用(read,write等等)返回值很大,所以用ssizet_t(有<0情况,不用size_t,很大可能越int的界,不用int),

通过一个demo代码,来演示一下

复制代码
#include <iostream>
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include<string.h>
using namespace std;
void ChildWrite(int wfd)
{
    char buffer[1024];
    int cnt=0;
    while(1)
    {
        snprintf(buffer,sizeof(buffer),"i am a child pid:%d,cnt:%d",getpid(),cnt++);
        write(wfd,buffer,strlen(buffer));//默认strlen没有把"/0"写进去
        sleep(1);
    }
}
void FatherRead(int rfd)
{
    char buffer[1024];
    while(1)
    {
        buffer[0]=0;
        ssize_t n=read(rfd,buffer,sizeof(buffer)-1);
        if(n>0)
        {
            buffer[n]=0;
            cout<<"child say"<<buffer<<endl;
        }

    }
}

int main()
{
    // 1.创建管道
    int fds[2] = {0}; // fds[0]:读端。fds[1]:写端
    int n = pipe(fds);
    if (n < 0)
    {
        cerr << "pipo error" << endl;
        return 1;
    }
    cout << fds[0] << endl;
    cout << fds[1] << endl;
    pid_t id = fork();
    if (id == 0)
    {
        // child
        // f-r,c-w,child要写就把读端关掉
        close(fds[0]);
        ChildWrite(fds[1]);
        close(fds[0]); // 为了代码的完整性,最后写完了也得关上写端
    }
    //f
    close(fds[1]);
    FatherRead(fds[0]);
    waitpid(id,nullptr,0);
    close(fds[0]);
    return 0;
}

5种特性

1.匿名管道通常用于有血缘关系的进程之间通信(父子或兄弟)

2.管道文件自带同步机制。写得慢,读就慢,写得快,读就快,即彼此读写会互相影响

3.管道是面向字节流的。(1)管道的 writeread 次数不要求一一对应 ,写 N 次的数据,可以被一次读完;写 1 次的数据,也可以被 N 次读完。(2)数据是连续的字节流,没有 "消息边界"读出来就是直接连续的一串,没有分界

4.管道是单向通信的,属于"半双工"的特殊情况

半双工:任何一个时刻,一个发,一个收(像正常说话,一个说一个听)

全双工:任何一个时刻,可以同时发收(像吵架,双方都说)

而管道一旦创建了就不能更换发收的对象了,就是说你创建的时候是发的那就一直是发的,是收的那就一直是收的

5.普通文件(管道也是普通文件)的生命周期是随进程的。一个进程退出和相应的文件的引用计数就减一,为0时,OS就自动关闭这个文件了

4种通信情况

1.写慢,读快。此时读端在等写端,写数据到管道的缓冲区时(缓冲区为空),就会处于阻塞

2.写快,读慢。此时读得慢,当写满了的时候,就不能再写了,就要等读端把数据读出去,写端此时处于阻塞

3.写关了,读继续。如果管道的缓冲区还有内容就都读完后返回0,没有就直接返回0,表示读到文件尾

4.读关了,写继续。管道已经没有任何接收数据的一方,继续写入只会浪费资源没有任何意义。OS 向写进程发送 SIGPIPE 信号终止进程。代码实现验证:

复制代码
void ChildWrite(int wfd)
{
    char buffer[1024];
    int cnt=0;
    while(1)
    {
        snprintf(buffer,sizeof(buffer),"i am a child pid:%d,cnt:%d",getpid(),cnt++);
        write(wfd,buffer,strlen(buffer));//默认strlen没有把"/0"写进去
        sleep(1);
    }
}
void FatherRead(int rfd)
{
    char buffer[1024];
    while(1)
    {
        buffer[0]=0;
        ssize_t n=read(rfd,buffer,sizeof(buffer)-1);
        if(n>0)
        {
            buffer[n]=0;
            cout<<"child say"<<buffer<<endl;
        }
        else if(n==0)
        {
            cout<<"n :"<<n<<endl;
            cout<<"child 退出,father也退出"<<endl;
            break;
        }
        else{
            break;
        }
//读一次就直接break,结束Fatherread函数,进而执行close(fd[0]),关闭读端
        break;
    }


}

int main()
{
    // 1.创建管道
    int fds[2] = {0}; // fds[0]:读端。fds[1]:写端
    int n = pipe(fds);
    if (n < 0)
    {
        cerr << "pipo error" << endl;
        return 1;
    }
    cout << fds[0] << endl;
    cout << fds[1] << endl;
    pid_t id = fork();
    if (id == 0)
    {
        // child
        // f-r,c-w,child要写就把读端关掉
        close(fds[0]);
        ChildWrite(fds[1]);
        close(fds[0]); // 为了代码的完整性,最后写完了也得关上写端
    }
    //f
    close(fds[1]);
    FatherRead(fds[0]);
    close(fds[0]);//放到等之前,读完就关
    sleep(5);//在进程回收前sleep方便看到Z+僵尸状态
    int status=0;
    int ret= waitpid(id,&status,0);
    if(ret>0)
    {
        printf("exit code:%d,exit signal:%d",(status>>8)&0xFF,status&0x7F);
        sleep(5);//父进程先sleep5s,不直接退出,方便看状态码结果
    }
    
    return 0;
}

运行结果:

可以看到退出码为13:

管道的容量

Linux 中匿名管道的缓冲区大小通常是 65536 字节(64KB)

原子写入

PIPE_BUF :POSIX 规定的原子写入阈值,在 Linux 上是 4096 字节

即当单次写入的数据量 < PIPE_BUF(Linux 上是 4096 字节)时,写入操作是原子的

就是说:多个进程同时往同一个管道写数据时,只要每次写的都小于 4096 字节,它们的数据不会互相穿插、混乱,操作系统会保证每次写入是一个完整的 "数据包"。

当单次写入的数据量 ≥ PIPE_BUF 时,写入就不是原子的了。 多个进程同时写的时候,数据可能会被操作系统 "拆分",导致不同进程的数据穿插在一起,出现 "混乱" 的情况。

eg:

进程 A 执行一次 write:AAA 进程 B 执行一次 write:BBB

合法(原子写入,无穿插) AAABBB / BBBAAA

绝对不会出现(穿插、拆分) AABAB / ABABB

进程池

1.池化技术

提前批量创建一批资源,统一管理、重复复用,不用每次用都「新建 + 销毁」,省时间、减开销

(池 = 资源池子(像游泳池,人反复进出,不用每次挖新池子)

优点:避免频繁创建销毁,提升运行效率。

进程池

场景:频繁创建子进程干活(如批量任务、管道读写)

  • 普通方式:来一个任务 fork() 一个子进程 → 干完 exit 销毁。 缺点:fork 开销大,频繁创建销毁很慢。
  • 进程池: 程序启动 预先创建 N 个子进程 放在池里,一直待命。 来了任务直接分配给空闲子进程,任务结束子进程不退出,放回池继续待命

和管道搭配:池内子进程统一通过管道 / 队列接收任务,不用反复创建进程 + 管道。

内存池

频繁 malloc /free 会产生内存碎片、效率低。 所以直接一次性申请一大块内存,内部切分复用,减少系统调用。

2.hpp

.hpp = 头文件 + 实现 合二为一

.hpp 文件里:声明、函数实现、类方法全部写在一起,不分文件。

因为 模板、内联函数、类内方法 天生不能拆分到 .cpp,所以可以采取这种方式

进程池代码实现

版本一

ProcessPool.hpp:

复制代码
#pragma once
#include <iostream>
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>
#include <vector>
#include "Task.hpp"
using namespace std;
// 先描述
class Channel
{
public:
    Channel(int fd, pid_t id)
        : _wfd(fd), _subid(id)
    {
        _name = "channel-" + to_string(_wfd) + "-" + to_string(_subid);
    }
    void Send(int code)
    {
        ssize_t n = write(_wfd, &code, sizeof(code));
        (void)n;
    }
    void Close()
    {
        close(_wfd); // 这里写宏观上,后面在ChannelManager循环的方式调用这个函数,把每一个写端都关了
        //为什么要这样关闭,后面通过循环把每一个写端都关了过后,等读完了过后就会break,进而技术work然后exit子进程退出
    }
    void Wait()
    {
        pid_t i = waitpid(_subid, nullptr, 0);
        (void)i;
    }
    ~Channel() {}
    int Fd() { return _wfd; }
    pid_t Subid() { return _subid; }
    string Name() { return _name; }

private:
    int _wfd;
    pid_t _subid;
    string _name;
};
const int gdefaultnum = 5;
// 再组织
class ChannelManager
{
public:
    ChannelManager()
        : _next(0)
    {
    }
    Channel Seclet()
    {
        _next %= gdefaultnum;

        return _channels[_next++];
    }
    void PrintChannel()
    {
        for (auto &channel : _channels)
        {
            cout << channel.Name() << endl;
        }
    }
    void ChannelManagerwait()
    {
        for (auto &e : _channels)
        {
            e.Wait();
            cout<<"回收"<<e.Name()<<endl;
        }
    }
    void ChannelManagerClose()
    {
        for (auto &e : _channels)
        {
            e.Close();
             cout<<"关闭"<<e.Name()<<endl;
        }
    }

    ~ChannelManager() {}
    void Insert(int wfd, pid_t subid)
    {
        // Channel c(wfd,subid);
        //_channels.push_back(c);这样写会有拷贝
        // push_back:先创建对象,再拷贝 / 移动到容器里
        // emplace_back:直接在容器内存里就地构造对象,无拷贝 / 无移动
        _channels.emplace_back(wfd, subid);
    }

private:
    vector<Channel> _channels;
    int _next;
};

class ProcessPool
{
public:
    ProcessPool(int num)
        : _process_num(num)
    {
        _tm.Register(PrintLog);//.hpp头文件+定义,所以可以找到PrintLog
        _tm.Register(DownLoad);
        _tm.Register(UpLoad);//不插入那么_task.size()就为0,Code()中返回code时就会直接模0,程序崩溃
    }

    void Work(int rfd)
    {
        int code = 0;
        while (1)
        {
            ssize_t n = read(rfd, &code, sizeof(code)); // 每次规定只读4个字节,即正确读到父进程写的任务码

            if (n > 0)
            {
                if (n != sizeof(code))
                {
                    continue;
                } // 没有正确读到就继续读

                // 根据读到的任务码来work
                printf("子进程[%d]收到一个任务码:%d\n", getpid(), code);
                _tm.Excute(code);
            }

            else if (n == 0)
            {
                printf("读到文件结尾了,子进程退出\n");
                break;
            }
            else
            {
                printf("读取失败");
                break;
            }
        }
    }

    bool start()
    {
        for (int i = 0; i < _process_num; i++)
        {

            // 1.创建管道
            int pipefd[2] = {0};
            int n = pipe(pipefd);
            if (n < 0)
                return false;
            // 2.创建子进程
            pid_t subid = fork();
            if (subid < 0)
                return false;
            else if (subid == 0)
            {
                // 子进程
                // 3.关闭不要的文件描述符
                close(pipefd[1]); // 子进程读
                Work(pipefd[0]);
                close(pipefd[0]);
                exit(0);
            }
            else
            {
                // 父进程
                // 3.关闭不要的文件描述符
                close(pipefd[0]);
                _cm.Insert(pipefd[1], subid); // 以子进程的pid和父进程写端文件描述符pipedfd[1],构建一条父进程-管道-子进程关系即
                // 通信信道,循环多次就构建多条这样的关系即多个通信信道
            }
        }
        return true;
    }
    void Run()
    {
        // 1.选一个信道
        Channel c = _cm.Seclet();
        cout<<"选择了一个子进程:"<<c.Name()<<endl;
        // 2.派发任务
        int taskcode = _tm.Code();
        c.Send(taskcode);
        cout<<"发送了一个任务码:"<<taskcode<<endl;
    }
    void Stop()
    {
        _cm.ChannelManagerClose();
        _cm.ChannelManagerwait();
    }

private:
    ChannelManager _cm;
    int _process_num;
    TaskManager _tm;
};

Main.cc:

复制代码
#include"ProcessPool.hpp"
#include<unistd.h>//这里还要再写一次#include<unistd.h>因为ProcessPool写了progma once
int main()
{
    ProcessPool pp(gdefaultnum);
    pp.start();
    int cnt=10;
    while(cnt--)
    {
        pp.Run();
        sleep(1);
    }
   
    pp.Stop();
    //说一下大概流程,先start会创建管道和子进程,然后子进程work(),父进程Insert(),子进程的work()里面read因为写端还没有写就会
    //阻塞等待,等到Run()时就会选择一个信道和send()任务码(即write任务码)等到子进程read后>0且==4就会Excute()执行任务,
    //最后Stop()先全部关闭w端后,如果管道缓冲区还有就读完后break没有就直接break反正就结束Work()然后exit子进程退出,最后再Stop()
    //里面的ChannelManagerWait()回收子进程


   
    return 0;
}

Task.hpp:

复制代码
#pragma once
#include <cstdio>
#include<iostream>
#include <unistd.h>
#include <vector>
#include <ctime>
#include<cstdlib>
using namespace std;
typedef void (*task_t)();
void DownLoad()
{
    printf("我是一个下载的任务\n");
}
void PrintLog()
{
    printf("我是一个打印日志的任务\n");
}
void UpLoad()
{
    printf("我是一个上传的任务\n");
}
class TaskManager
{
public:
    TaskManager()
    {
        srand(time(nullptr));
    }
    void Register(task_t t)
    {
        _tasks.push_back(t);
    }
    int Code()
    {
        
        return rand() % _tasks.size();
    }
    void Excute(int code)
    {
        if (code > 0 && code < _tasks.size())
        {
            _tasks[code]();
        }
    }
    ~TaskManager()
    {}

private:
    std::vector<task_t> _tasks;
};

Makefile:

复制代码
process_pool:Main.cc 
	g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm -f process_pool

运行结果:

2

但版本一有一个隐藏得很深的bug

这是我们版本一写的,即先全部关掉写端后,再全部回收

但如果改成关掉一个再回收一个呢?

运行结果:

我们发现最后并没有回收成功,而是一直卡在这,那这是为什么呢?

我们先来看一下

仅展示一下要修改的代码

方法2运行结果:

相关推荐
xexpertS21 分钟前
CI/CD 熔断机制:如何通过编排级熔断器提升开发者效率
java·linux·运维
xuefeiniao31 分钟前
CentOS + 宝塔服务器偶发 SSH、面板、网站全部无法访问排查记录(PHP-FPM 内存耗尽案例)
服务器·centos·ssh·php
带娃的IT创业者1 小时前
突破内存瓶颈:利用 Nvidia GPU 显存作为 Linux Swap 空间的深度实践
linux·运维·服务器·nvidia·swap·gpu显存
zy happy1 小时前
VMware虚拟机添加新的硬盘
java·linux·jvm·docker
LCG元1 小时前
嵌入式开发踩坑排查实战:从 STM32 外设初始化到 Linux SPI 驱动的 15 类硬件疑难故障深度复盘
linux·stm32·单片机
代码AC不AC1 小时前
【Linux】Socket编程TCP
linux·tcp·socket编程
扛枪的书生1 小时前
SeaweedFS 学习总结
linux
minglie12 小时前
香橙派zero3配个iic屏显示ip
linux·学习
默默提升实验室2 小时前
Ubuntu 22.04.5 配置静态IP,手动安装SSH
ubuntu
edwarddamon2 小时前
SSH 指定端口建立隧道:打通内网访问完整教程
linux·命令行