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运行结果:

相关推荐
载数而行5201 小时前
Linux 10 防火墙
linux
草莓熊Lotso1 小时前
【Linux网络】深入理解 TCP 协议(二):序号机制、流量控制与连接管理
linux·运维·服务器·网络·c++·tcp/ip
云飞云共享云桌面2 小时前
SolidWorks服务器+云飞云共享云桌面 = 10人共享方案
linux·运维·服务器·网络·制造
Full Stack Developme10 小时前
JVM 与 Linux 交互的核心原理
linux·运维·jvm
HackTwoHub10 小时前
最新Nessus2026.6.8版本主机漏洞扫描/探测工具Windows/Linux
linux·运维·服务器·安全·web安全·网络安全·安全架构
qq_1631357510 小时前
Linux 【04-mkdir命令超详细教程】
linux
qq_1631357510 小时前
Linux 【08-mv命令超详细教程】
linux
qq_1631357513 小时前
Linux 【07-rm命令超详细教程】
linux
zh路西法14 小时前
【SSH 免密登录全流程】Windows Linux 通用方案
linux·windows·ssh