目录
[一 系统及语言变化](#一 系统及语言变化)
[二 进程间通信简介](#二 进程间通信简介)
[1 是什么----本质前提](#1 是什么----本质前提)
[2 为什么要通信](#2 为什么要通信)
[3 怎么进行通信](#3 怎么进行通信)
[三 管道](#三 管道)
[1 什么是管道](#1 什么是管道)
[2 匿名管道原理](#2 匿名管道原理)
[3 demo代码](#3 demo代码)
[4 管道通信场景](#4 管道通信场景)
[四 实践:进程池实现](#四 实践:进程池实现)
[1 任务模块](#1 任务模块)
[2 子进程工作模块](#2 子进程工作模块)
[3 Channel 类(管道 + 子进程封装)](#3 Channel 类(管道 + 子进程封装))
[4 进程池核心(ProcessPool)](#4 进程池核心(ProcessPool))
[(1) Start () ------ 创建 N 个子进程 + N 条管道](#(1) Start () —— 创建 N 个子进程 + N 条管道)
[(2)PushTask () ------ 派发任务(负载均衡)](#(2)PushTask () —— 派发任务(负载均衡))
(3)Stop () ------ 关闭进程池Stop () —— 关闭进程池)
[5 main 函数(流程控制)](#5 main 函数(流程控制))
[6 进程池完整代码](#6 进程池完整代码)
一 系统及语言变化
从这节博客开始,我们就把语言切换为C++,系统由centos7 切换为ubentu 24.04/20.04 ,vscode远程开发
vim被vscode所替代;vim虽然写代码的效率高,但是不一定开发效率高;vscode是一款文件编辑器,它是一个轻量化,插件式的软件
vscode打开文件夹,是默认打开你的电脑里的特定目录
安装Remote - SSH 插件:
Remote - SSH 是 VS Code 的官方插件,核心作用是:通过 SSH 协议连接远程服务器 / 虚拟机,在本地 VS Code 里直接编辑、运行、调试远程代码
vscode和xhsell协调步骤:
**(1)**搜索插件Remote-SSH插件,安装(vscode中)

(2)出现远端资源管理器,点击后出现加号,点击输入 ssh @IP地址,之后选择一个配置文件,右下角就会出下降:已添加主机


(3)点击刷新,就会出现一台主机(左上角),点击这一排右边的任意一个按键,后输入主机密码,就链接成功



(4)在xshell中,mkdir xxx创建一个新目录
之后在VS中,打开文件夹,选择对应路径,当前打开就在家目录下;之后会再让输入一次密码,写入代码,用crtl+s保存


这样协调操作就完成了
vscode中,光标在哪一行,crtl+c,crtl+v 这一行就会直接复制,粘贴
crtl+~(波浪号):在vscode中出现命令行终端,在这一行下面,就可以输入Linux目录,就相当于一台小型的机器远端xshell
不推荐用vscode做本地和远端的调试,推荐使用cgbd
好了,现在我们可以进入进程间通信的学习了
二 进程间通信简介
1 是什么----本质前提
进程间通信指的就是两个或多个进程,进行互相传递信息的过程
进程具有独立性-->进程=内核数据结构+代码和数据,要规避进程之间的耦合关系
所以,至少在目前,一个进程把自己的数据,发送给另一个进程也是一件比较困难的事
但是父进程的全局变量不是能交给子进程吗? 但是这不是通信,是父给子的,且只能给全局变量,所以只能叫做继承数据,无法做到持续性通信
2 为什么要通信
进程间通信目的
数据传输:一个进程需要将它的数据发送给另一个进程
资源共享:多个进程之间共享同样的资源。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
进程控制:有些进程希望完全控制另一个进程的执行(如 Debug 进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
3 怎么进行通信
假如在操作系统有两个进程:A B,B不能看到A申请的空间,A也不能看B的,那么怎么进行通信呢?
创建一个进程交换的公共场所(本质是内存),由操作系统提供
进程间通信的本质前提:让不同的进程,看到同一份资源(资源由操作系统提供),我们后续进行进程间通信时,大部分时间都是想办法让进程看到同一份资源(利用系统调用)
操作系统必然提供系统调用
为通信接口制定标准:我们用到的是System V
复用文件部分代码,最小的通信代价:管道
常见的通信方式:管道(匿名管道,命名管道)
三 管道
1 什么是管道
管道是Unix中最古老的进程间通信的形式。 我们把从一个进程连接到另一个进程的一个数据流称为一个"管道"

结论1:父子进程为什么会向同一个显示器打印?因为指向了同一个文件

父进程写好一份文件之后,再创建子进程,子进程就能共享这份文件,一个向里面写入,一个向外读取
结论2:以文件形式继承给子进程,这种通信方案,叫做管道(说明管道本质是文件)
进程不能用open打开磁盘文件,需要系统调用
管道在设计之初,只允许进行单向数据通信,父进程关闭不需要文件描述符
管道特点1:基于文件的单向数据通信--->为了简单化设计,不需要考虑数据朝向问题
为什么父进程需要董事打开读写:为了让子进程继承读写打开的方式
2 匿名管道原理
cpp
#include <unistd.h>
功能:创建⼀匿名管道
原型
int pipe(int fd[2]);
参数
fd:⽂件描述符数组,其中fd[0]表⽰读端, fd[1]表⽰写端
返回值:成功返回0,失败返回错误代码
这两个文件描述符fd[0]和fd[1],会被填入当前进程的 files_struct 文件描述符表中(占下标3,4的位置),指向内核中同一个管道文件对象。
每个文件对象都有独立的文件偏移量 pos:读端的 pos 记录读取位置,写端的 pos 记录写入位置;
它们共享同一个内核缓冲区和 inode,数据会先写入缓冲区,再由读端从缓冲区读取

匿名管道的本质,是操作系统内核在内存中创建的一个临时文件,它具备以下特点:
没有磁盘路径、没有文件名,因此也被称为匿名管道;
不占用磁盘空间,数据仅存放在内核的缓冲区中;
拥有独立的 inode 节点,但该节点只存在于内存中,不持久化到磁盘
3 demo代码
我们来写一段代码,需求是:子进程进行写入,父进程读取,父子进程传递可变字符串
注意:C++文件后缀:.cc .cpp .cxx
(1)创建管道
管道头文件:
cpp
#include<unsitd.h>
输出型数组:
cpp
// pipefd[0] = 读端
// pipefd[1] = 写端
int pipefd[2] = {0};
pipe(pipefd);
(2)创建子进程
管道特点2:管道只能用来让具有血缘关系的进程进行进程间通信,常用于父子进程之间,进行进程间通信
cpp
pid_t id = fork();
(3)形成单向通信的管道
子进程想写:保留1,关闭0
cpp
closed(pipefd[0]);
父进程想读:保留0,关闭1
cpp
closed(pipefd[1]);
管道特点3:管道的本质是文件,一般文件,如果打开它的进程推出了,那么文件也会被系统自动的关闭!打开文件的生命周期随进程
父进程关闭,子进程不管-->管道还在维持,有相关文件没有结束
子进程写数据函数:WriteData,父进程读数据函数:ReadData
cpp
if(id == 0)
{
// 子进程:只写,关闭读端
close(pipefd[0]);
WriteData(pipefd[1]);
close(pipefd[1]);
exit(0);
}
else
{
// 父进程:只读,关闭写端
close(pipefd[1]);
ReadData(pipefd[0]);
close(pipefd[0]);
}
cpp
void WriteData(int wfd)
{
int cnt = 1;
pid_t id = getpid();
while(true)
{
sleep(1); // 每秒写一次
// 构造消息
std::string message = "hello father...";
message += ...;
// 向管道写
write(wfd, message.c_str(), message.size());
}
}
cpp
void ReadData(int rfd)
{
char inbuffer[1024];
while(true)
{
sleep(5); // 每5秒读一次
// 从管道读
ssize_t n = read(rfd, inbuffer, sizeof(inbuffer)-1);
if(n > 0)
{
inbuffer[n] = '\0';
std::cout << "读到:" << inbuffer << std::endl;
}
}
}
管道特点4:管道自己内部实现了:进程间同步
我们整理一下上面的特点:
1 :管道在设计之初,只允许进行单项数据通信
管道特点之一:基于文件的,单向数据通信
2 :管道只能用来让具有血缘关系的进程,进行进程间通信
常用于父子进程之间,进行进程间通信!
3: 管道的本质是文件,一般文件,如果打开它的进程退出了,那么文件也会被系统自动关闭!
打开的文件的生命周期随进程!
4:管道自己内部是实现了:进程间的同步!
5 :管道是面向字节流的!
4 管道通信场景
(1)场景1
写端很慢,读端很快,以慢的节奏来----父进程等待数据进入,即等待子进程进入
管道是面向字节流的:最朴素的认识-->读写次数不匹配,读端可以按照自己的需求读
读写次数匹配场景:发,取快递:别人给你发了几个快递,你就要取几个快递
读写不匹配场景:一次买了100吨水,但是每次的使用量不同,可能是10吨,也可能是几百毫升
(2)场景2
写端很快,读端很慢,读端会把写端写入的数据能一次读上了,就全部读上来
极端化场景:写端特别快,读端不读-->管道会被写慢,写满后写端怎么办?
写进程会被阻塞

我们这个代码的验证发现写到65536就不写了--->65536/1024=64
所以管道会被写满,最多写64KB,读端不读,写端一直写道把管道阻塞
(3)场景3
写端不写,关闭写端(close(fwd)),读端会怎么样?
读端把管道内容全部读完,会读到0,表示end of file,读到文件结尾
read:系统调用,read返回0,读到文件结尾

场景3是不通信,正常退出的场景
(4)场景4
写端一直写,读端不读了且关闭读端close(rwd)
操作系统不会做任何浪费时间和空间的事,写端一直写毫无意义,操作系统会通过信号杀掉写进程(13号信号,SIGPIPE)
什么时候用到管道

四 实践:进程池实现
用进程池比再去修改shell更有价值
池化技术:减少系统调用的次数,提高效率(调用系统调用成本高)
池:提高效率 进程池:预先创建子进程,有任务先处理任务,不需要有任务时,再创建子进程
进程池要先创建一批子进程,父进程想向哪个子进程写入,就向哪个子进程写入,控制写入
**任务码:**父进程发给子进程的 "指令编号",用来告诉子进程要执行什么任务。不是固定的系统概念,是自己定义的一个数字
父进程可以通过给任意一个子进程发送任务码的情况,写入代码
负载均衡:如果一直让一个子进程完成任务,其他子进程不安排任务,就会造成忙闲不均;把任务码均匀的撒到每一个子进程,让子进程以相同的压力工作,提高系统的稳定性--->做法可以有:随机数,轮询,计数器....
代码整体结构:
cpp
1. 任务模块:定义4种任务 + 任务码
2. 子进程工作逻辑:从管道读任务码,执行对应任务
3. Channel类:管理【子进程PID + 父进程写端fd】
4. 进程池类:创建N个子进程 + 管道,实现负载均衡
5. main函数:加载任务 → 启动进程池 → 推送任务 → 关闭进程池
1 任务模块
cpp
// 任务类型:用数字代表要做什么事 ------ 这就是【任务码】
#define LOG_TASK 0 // 打印日志
#define DOWNLOAD_TASK 1 // 下载
#define MYSQL_TASK 2 // 访问数据库
#define REDIS_TASK 3 // 访问redis
// 所有任务放到一个数组里
std::vector<task_t> gtasks;
// 加载任务:把函数放进任务列表
void LoadTask()
{
gtasks.push_back(printlog); // 下标0
gtasks.push_back(download); // 下标1
gtasks.push_back(readmysql); // 下标2
gtasks.push_back(writeredis); // 下标3
}
任务码 = 数字编号
0、1、2、3 分别代表一种任务
子进程收到数字,就执行对应函数
2 子进程工作模块
cpp
void Work(int rfd)
{
while (true)
{
int code = 0;
// 子进程阻塞读:等待父进程发任务码
ssize_t n = read(rfd, &code, sizeof(code));
if (n == sizeof(int))
{
// 执行任务码对应的任务
gtasks[code]();
}
else if (n == 0)
{
break; // 父进程关闭写端 → 子进程退出
}
}
}
子进程循环等待父进程发任务
读到任务码 code
直接调用 gtasks[code]() 执行任务
父进程关闭管道 → 子进程自动退出
3 Channel 类(管道 + 子进程封装)
cpp
class Channel
{
public:
Channel(int wfd, pid_t who)
: _wfd(wfd), // 父进程写端fd
_sub_process_id(who) // 子进程pid
{}
// 父进程通过这里发送【任务码】
void SendTask(int taskcode)
{
write(_wfd, &taskcode, sizeof(taskcode));
}
};
4 进程池核心(ProcessPool)
(1) Start () ------ 创建 N 个子进程 + N 条管道
cpp
void Start()
{
for (int i = 0; i < _number; i++)
{
// 1. 创建管道
int pipefd[2];
pipe(pipefd);
// 2. 创建子进程
pid_t id = fork();
if(id == 0) // 子进程
{
close(pipefd[1]); // 子进程关写端
Work(pipefd[0]); // 子进程进入工作循环
close(pipefd[0]);
exit(0);
}
else // 父进程
{
close(pipefd[0]); // 父进程关读端
_channels.emplace_back(pipefd[1], id);
}
}
}
创建 N 个子进程
每个子进程绑定一条独立管道
父进程只保留写端
子进程只保留读端
(2)PushTask () ------ 派发任务(负载均衡)
cpp
void PushTask(int taskcode)
{
int who = Next(); // 轮询选一个子进程
_channels[who].SendTask(taskcode); // 发任务码
}
轮询调度:依次给每个子进程发任务
发送的内容就是任务码(一个 int 数字)
(3)Stop () ------ 关闭进程池
cpp
void Stop()
{
for(auto &channel: _channels)
{
channel.Close(); // 关闭写端 → 子进程读到0退出
channel.Wait(); // 回收子进程
}
}
父进程关闭所有管道写端
子进程 read 返回 0 → 自动退出循环
父进程 wait 回收,防止僵尸进程
5 main 函数(流程控制)
cpp
int main()
{
// 1. 加载任务列表
LoadTask();
// 2. 创建进程池(例如5个子进程)
std::unique_ptr<ProcessPool> pp = make_unique<ProcessPool>(5);
// 3. 启动进程池:创建进程+管道
pp->Start();
// 4. 推送任务(任务码)
pp->PushTask(0);
pp->PushTask(1);
pp->PushTask(2);
pp->PushTask(3);
// 5. 关闭进程池
pp->Stop();
}
6 进程池完整代码
cpp
#include <iostream>
#include <string>
#include <vector>
#include <memory>
#include <functional>
#include <ctime>
#include <cstdlib>
#include <unistd.h>
#include <sys/wait.h>
#define __MAIN__
///////////////////////任务测试代码/////////////////////////
using task_t = std::function<void()>;
void printlog()
{
// sleep(1);
std::cout << "我是一个打印日志的任务, pid: " << getpid() << std::endl;
}
void download()
{
// sleep(1);
std::cout << "我是一个下载任务, pid: " << getpid() << std::endl;
}
void readmysql()
{
// sleep(1);
std::cout << "我是一个访问数据库的任务, pid: " << getpid() << std::endl;
}
void writeredis()
{
// sleep(1);
std::cout << "我是一个访问redis的任务, pid: " << getpid() << std::endl;
}
std::vector<task_t> gtasks;
void LoadTask()
{
gtasks.push_back(printlog);
gtasks.push_back(download);
gtasks.push_back(readmysql);
gtasks.push_back(writeredis);
}
// *: 输出型参数
// const &: 输入型参数
// &: 输入输出型
void RandomTask(std::vector<int> *out)
{
for (int i = 0; i < 50; i++)
{
int code = rand() % gtasks.size();
usleep(23223);
out->push_back(code);
}
}
#define LOG_TASK 0
#define DOWNLOAD_TASK 1
#define MYSQL_TASK 2
#define REDIS_TASK 3
std::string Task2String(int code)
{
switch (code)
{
case LOG_TASK:
return "printlog";
case DOWNLOAD_TASK:
return "download";
case MYSQL_TASK:
return "readmysql";
case REDIS_TASK:
return "writeredis";
default:
return "unknown";
}
}
///////////////////////进程池代码/////////////////////////
void Work(int rfd)
{
while (true)
{
int code = 0;
ssize_t n = read(rfd, &code, sizeof(code));
if (n == sizeof(int))
{
if (code >= 0 && code < gtasks.size())
{
gtasks[code]();
}
}
else if (n == 0)
{
break; // 子进程只要读到返回值为0, 表明父进程让我退出
}
else
{
break;
}
}
}
class Channel
{
public:
Channel(int wfd, pid_t who) : _wfd(wfd), _sub_process_id(who)
{
_name = "Channel-" + std::to_string(_sub_process_id) + "-" + std::to_string(_wfd);
}
int Fd()
{
return _wfd;
}
pid_t SubId()
{
return _sub_process_id;
}
std::string Name()
{
return _name;
}
void Close()
{
if (_wfd >= 0)
close(_wfd);
}
void Wait()
{
pid_t rid = waitpid(_sub_process_id, nullptr, 0);
(void)rid;
}
void SendTask(int taskcode)
{
ssize_t n = write(_wfd, &taskcode, sizeof(taskcode));
(void)n;
}
~Channel()
{
}
private:
int _wfd;
pid_t _sub_process_id;
std::string _name;
};
class ProcessPool
{
private:
int Next()
{
int choice = _next_choice;
_next_choice++;
_next_choice %= _channels.size();
return choice;
}
public:
ProcessPool(int number) : _number(number), _next_choice(0)
{
std::cout << "number: " << _number << std::endl;
}
// 父进程
void Start()
{
for (int i = 0; i < _number; i++)
{
// 1. 创建管道
int pipefd[2];
int n = pipe(pipefd);
if (n < 0)
{
perror("pipe");
exit(2);
}
// 2. 创建子进程
pid_t id = fork();
if (id < 0)
{
perror("fork");
exit(3);
}
else if (id == 0) // 子进程
{
// 关闭父进程历史的wfd!
for(auto &channel : _channels)
channel.Close();
close(pipefd[1]);
Work(pipefd[0]);
close(pipefd[0]);
exit(0);
}
else // 父进程
{
close(pipefd[0]);
// pipefd[1];
_channels.emplace_back(pipefd[1], id);
}
}
}
// 1. 什么任务?任务码决定
// 2. 任务给谁?属于进程池内部操作,负载均衡
void PushTask(int taskcode)
{
// 选择一个子进程
int who = Next();
_channels[who].SendTask(taskcode);
std::cout << "发送任务: " << Task2String(taskcode) << "[" << taskcode << "]" << " 给: " << _channels[who].Name() << std::endl;
}
void Stop()
{
// version 2 ???
for(auto &channel: _channels)
{
channel.Close();
channel.Wait();
std::cout << channel.Name() << " close and wait success!" << std::endl;
}
// version3
// int end = _channels.size() - 1;
// while(end >= 0)
// {
// _channels[end].Close();
// _channels[end].Wait();
// std::cout << _channels[end].Name() << " closea and wait success!" << std::endl;
// end--;
// }
// 内部bug!
// 1. 关闭wfd -- version1
// for (auto &channel : _channels)
// {
// channel.Close();
// std::cout << channel.Name() << " close success!" << std::endl;
// }
// // sleep(3);
// // // 2. 回收子进程
// for (auto &channel : _channels)
// {
// channel.Wait();
// std::cout << channel.Name() << " wait success!" << std::endl;
// }
}
void DebugPrint()
{
std::cout << "-------------------------------------" << std::endl;
for (auto &channel : _channels)
{
std::cout << channel.Fd() << std::endl;
std::cout << channel.SubId() << std::endl;
std::cout << channel.Name() << std::endl;
}
std::cout << "-------------------------------------" << std::endl;
}
~ProcessPool() {}
private:
std::vector<Channel> _channels;
int _number;
int _next_choice;
};
// 父进程
#ifdef __MAIN__
static void Usage(const std::string &proc)
{
std::cout << "Usage:\n\t" << proc << " process_number" << std::endl;
}
// ./process_pool 5
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(1);
}
int number = std::stoi(argv[1]);
// 0. 加载任务
srand(time(nullptr) ^ getpid());
LoadTask();
std::vector<int> task_codes;
RandomTask(&task_codes);
// 1. 创建进程池对象
std::unique_ptr<ProcessPool> pp = std::make_unique<ProcessPool>(number);
// 2. 启动进程池
pp->Start();
sleep(2);
// for (auto task : task_codes)
// {
// pp->PushTask(task);
// usleep(500000);
// }
// while(true)
// {
// // int code = 0;
// // std::cout << "Please Enter Your Task# ";
// // std::cin >> code;
// // if(code < 0 || code > gtasks.size())
// // {
// // std::cout << "任务码错误, 请重新输入"<< std::endl;
// // continue;
// // }
// pp->PushTask(code);
// }
pp->Stop();
return 0;
}
#endif