《Linux 进程间通信(IPC)全解析:从管道到共享内存,一步步掌握多进程协作核心(初篇)》
在 Linux 多进程编程中,进程间通信(IPC)是实现 “进程协作、数据共享、行为同步” 的核心基石。当多个独立进程需要像 “齿轮” 一样精密配合时,匿名管道、命名管道、共享内存、消息队列等 IPC 机制,就是连接它们的关键 “纽带”。本文将从管道原理与操作入手,逐步延伸到进程池编写、共享内存实践,再触及消息队列、信号量的设计逻辑,最终探究内核对 IPC 资源的管理方式,甚至带您思考 “C 语言


前引:在 Linux 多进程编程中,进程间通信(IPC)是实现 “进程协作、数据共享、行为同步” 的核心基石。当多个独立进程需要像 “齿轮” 一样精密配合时,匿名管道、命名管道、共享内存、消息队列等 IPC 机制,就是连接它们的关键 “纽带”。本文将从管道原理与操作入手,逐步延伸到进程池编写、共享内存实践,再触及消息队列、信号量的设计逻辑,最终探究内核对 IPC 资源的管理方式,甚至带您思考 “C 语言如何通过技巧实现多态思想与 IPC 的关联”,帮您系统构建 Linux IPC 知识体系!
注意:严重错误与讲解为本篇重点
目录
【一】VIsual Studio Code链接云主机
之前咱们编辑文档都是采用 Vim,现在为了追求更极致的体验,我们切换为 VS Code,下面是安装以及链接云主机教程:https://tthfc.qichkj.top/code/
(1)首先在官网选择下载版本完成安装,接着打开VS Code的插件商店:搜索Remote - SSH安装

(2)先点击“+”号,然后在搜索框链接云主机:ssh root@公网IP,然后回车,选择第一个路径


(3)然后点击连接

(4)可以在C盘的config文件查看信息

(5)然后输入密码完成连接即可

随后连接云主机之后需要在VS code的插件商店安装下面几个常用的插件:
C/C++编译:

简体中文界面:

主题:

文件图标:

头文件自动包含:

C/C++扩展包:

【二】进程通信介绍
进程通信无非就是进程之间形成数据共享、交互、控制等等,但是有一个问题:进程之间是独立的
实现进程通信那么就需要“操作系统”作为第三方给我们提供接口来完成,就算是操作系统,形成通信的同时保证进程的数据安全,也需要去制定通信规则,目前进程间通信发展出三个规则体系:
管道 System V进程间通信 POSIX进程间通信
【三】匿名管道
(1)介绍
什么是管道?将一个进程连接到另一个进程的数据流称为管道,我们今天学习匿名管道

匿名管道:对具有“亲缘关系”(比如父子进程、父孙进程...)进程之间实现通信,它不依赖文件 名、路径(即不具体的文件),而操作内存文件,仅依赖文件描述符直接通信
由此可见:通过 pipe() 形成的管道,本身就是一个临时缓冲区,配合重定向将一个进程输出弄到 管道(临时缓冲区),再从管道的读端获取管道内的数据
(2)特点理解
(1)亲缘关系
该种关系我们调用 fork()出来的即具有亲缘关系,但是完全单独的两个进程无法执行匿名管道
(2)内存文件
不是访问磁盘文件,即不需要路径、文件名等信息,只需要文件描述符直接访问内存文件
(3)单向性
例如:父进程通过 pipe()系统调用通过文件描述符打开了一个内存文件:
int pipe(int pipefd[2]);
参数:一个包含两个整数的数组,用于存储管道的读端和写端文件描述符:
pipefd[0]:固定为读端,用于通过read()系统调用读取管道数据pipefd[1]:固定为写端,用于通过write()系统调用向管道写入数据
(理解:你给它一个包含两个整型元素的数组,它用返回值给你填充)
返回值:
- 成功时返回
0,pipefd数组被正确填充 - 失败时返回
-1
作用:
pipe() 的本质是让内核创建一个内存缓冲区(管道核心),将两个文件描述符与该缓冲区绑定
即[0]为一个文件描述符,指向该内存文件的读端;[1]为一个文件描述符,指向该内存文件的写端
例如:

此时我们如果创建一个子进程,那么会出现如下情况:父子进程读端与写端都高度重复

因为单向性即让两个进程满足:一个读一个写,例如:

下面是我经过试验总结的结论:
(1)父子进程可以同时进行读写,但是每个进程每次只能选择读或者写
(2)如果一个进程阻塞了(比如写满了,休眠....),另外一个进程可能阻塞(一直等它)
(3)文件缓冲区是有大小的,因此不可能一直循环写入,肯定会阻塞情况
(4)已被读取的数据会被自动清理
(4)生命周期
不管是内存文件还是磁盘文件,都是通过进程来执行,只要进程关闭,对应文件生命周期结束
(5)字节流传输
字节流传输即读和写都是按照单个字节为读写标准的,你写完之后我再读,但可能出现以下情况:
(1)无消息边界:比如你写100个字节是连续的,但是由于读端有限制,每30个字节进行读取
(2)顺序保证:读端根据写端按顺序读取
(3)管道情况
(1)读写正常,但文件缓冲区是空的,导致读端阻塞
(2)读写正常,管道如果被写满(例如:管道存储有大小、缓冲区也有大小),写端就阻塞
(3)读写正常,写端关闭,读端就会读取0,表明读到文件末尾,不会被阻塞
【四】匿名管道运用
现在我们来使用匿名管道,理清逻辑顺序:首先打开vs code来创建文件装代码,指令来执行

(1)打开读写端
通过 pipe()系统调用通过文件描述符打开了一个内存文件:
int pipe(int pipefd[2]);
参数:一个包含两个整数的数组,用于存储管道的读端和写端文件描述符:
pipefd[0]:固定为读端,用于通过read()系统调用读取管道数据pipefd[1]:固定为写端,用于通过write()系统调用向管道写入数据(理解:你给它一个包含两个整型元素的数组,它用返回值给你填充)
返回值:
- 成功时返回
0,pipefd数组被正确填充- 失败时返回
-1作用:
pipe()的本质是让内核创建一个内存缓冲区(管道核心),将两个文件描述符与该缓冲区绑定即[0]为一个文件描述符,指向该内存文件的读端;[1]为一个文件描述符,指向该内存文件的写端
pipe()系统调用之后,此时父进程的3号下标指向读端,4号下标指向写端
(2)创建父子进程
通过fork()之后,父子进程的读端和写端重叠,我们需要保持单向性,以父写子读为例!
关闭父进程的3号文件符描述下标,关闭子进程的4号描述符下标
(3)然后各干各的事
举例:
父子进程各自执行自己的函数,父进程向文件缓冲区写入,子进程来读
(4)首尾工作
子进程关闭,父进程回收子进程,完成收尾
(5)完整代码
#include<iostream>
#include<unistd.h>
#include <sys/stat.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstring>
using namespace std;
#define MAX 5951
void Read(int pipefd)
{
char buff[MAX]={0};
while(1)
{
int pid=read(pipefd,buff,5951);
if(pid==-1)
{
break;
}
cout<<buff<<endl;
sleep(5);
}
return;
}
void Write(int pipefd)
{
//向缓冲区写入内容
int num=0;
const char* ptr="Hello Linux";
while(1)
{
int pid=write(pipefd,ptr,strlen(ptr));
if(pid==-1)
{
break;
}
cout<<"正在写入"<<num++<<endl;
sleep(5);
}
return;
}
int main()
{
int pipefd[2]={0};
//打开内存文件
int pf= pipe(pipefd);
if(pf==-1)
{
perror("pipe");
return 1;
}
//创建父子进程
int d=fork();
if(d==0)//子进程
{
//关闭4号描述符下标
close(pipefd[1]);
//执行函数
Read(pipefd[0]);
//关闭子进程
exit(0);
}
//父进程
//关闭3号描述符下标
close(pipefd[0]);
//执行函数
Write(pipefd[1]);
//回收子进程
int pid = waitpid(-1,NULL,0);
if(pid<=0)
{
perror("waitpid");
return 1;
}
else
{
cout<<"回收成功"<<endl;
}
return 0;
}
【五】打造进程池
解释:通过一个父进程创建出一堆的子进程,父进程想给哪个子进程派发任务谁就执行
(通关秘籍:对于这种进程相关的,无非都是使用fork()出子进程,然后系统调用,再统一回收)
大框架:我们先把通用框架弄出来,然后以派发任务为主(注意:多个子进程描述符会逐步改变)

#define MAX 5
int main()
{
//创建MAX个通信
for(int i=0;i<MAX;i++)
{
//创建一个匿名管道通信
int pipely[2]={0};
int pid = pipe(pipely);
if(pid==-1)
{
perror("pipe");
return 1;
}
//分割父子进程
int pid_t =fork();
if(pid_t==0)//子
{
//子进程关闭写通道
close(pipely[1]);
//子进程执行任务
//退出
exit(0);
}
else if(pid_t>0)//父
{
//父进程关闭读通道
close(pipely[0]);
//父进程派发任务
int count=MAX;
while(count--)
{
//回收子进程
int status;
int pid=waitpid(-1,&status,0);
if(pid>0)
{
//如果子进程是被信号杀死
if(WIFSIGNALED(status)&&WTERMSIG(status))
{
//打印信号
std::cout<<"子进程"<<getpid()<<"被"<<WTERMSIG(status)<<"号信号杀死"<<std::endl;
}
}
else
{
std::cout<<"没有子进程退出....."<<std::endl;
}
}
}
else
{
perror("fork");
return 1;
}
}
return 0;
}
(1)创建任务
我们通过函数指针来创建多个任务,随后放在vector容器里面:存储类型为 void (*)(void)

(2)父进程分发任务
void Write(int pipely)
{
std::vector<void (*)(void)> task;
Storage(task);
srand(static_cast<unsigned int>(time(nullptr)));
//调用函数写向显示器,注意之前子进程是使用dup2调整了描述符下标的
int count=rand() % task.size();
task[count]();
}
(3)子进程读任务
void Read(int pipely)
{
char buff[200]={0};
int pid =read(pipely,buff,20);
if(pid==-1)
{
std::cout<<"读取失败!"<<std::endl;
return;
}
std::cout<<"读取到了:"<<buff<<std::endl;
}
(4)完成代码
这里我增加了一点对管道的理解:将显示器输出到管道的写端,子进程再从管道读端读
因为调用 pipe() 实现的管道本身就形成一个临时缓冲区
头文件:
#pragma once
#include<iostream>
#include<unistd.h>
#include <sys/stat.h>
#include <stdio.h>
#include <sys/types.h>
#include<vector>
#include <sys/wait.h>
#include <cstring>
#include <cstdlib>
#include <ctime>
//创建任务函数
void Path1()
{std::cout<<"正在加载游戏"<<std::endl;}
void Path2()
{std::cout<<"请登录游戏"<<std::endl;}
void Path3()
{std::cout<<"正在执行游戏"<<std::endl;}
void Path4()
{std::cout<<"已退出游戏"<<std::endl;}
//创建函数指针
//void (*task_t)(void);
//存储任务
void Storage(std::vector<void (*)(void)>& task)
{
task.push_back(Path1);
task.push_back(Path2);
task.push_back(Path3);
task.push_back(Path4);
}
函数实现:
首先在下面的代码中是有四处严重错误的,我们先看错误代码,看你是否注意到:
#include"chi_pipeline.h"
#define MAX 5
#define MAX_READ sizeof(void (*)())
void Read(int pipely)
{
char buff[200]={0};
int pid =read(pipely,buff,20);
if(pid==-1)
{
std::cout<<"读取失败!"<<std::endl;
return;
}
std::cout<<"读取到了:"<<buff<<std::endl;
}
void Write(int pipely)
{
std::vector<void (*)(void)> task;
Storage(task);
srand(static_cast<unsigned int>(time(nullptr)));
//调用函数写向显示器,注意之前子进程是使用dup2调整了描述符下标的
int count=rand() % task.size();
task[count]();
}
int main()
{
//创建MAX个通信
for(int i=1;i<MAX;i++)
{
//创建一个匿名管道通信
int pipely[2]={0};
int pid = pipe(pipely);
if(pid==-1)
{
perror("pipe");
return 1;
}
//分割父子进程
int pid_t =fork();
if(pid_t==0)//子
{
//子进程关闭写通道
close(pipely[1]);
//子进程执行任务
Read(pipely[0]);
//退出
exit(0);
}
else//父
{
//父进程关闭读通道
close(pipely[0]);
int fd = dup2(pipely[1],1);
if(fd==-1)
{
std::cout<<"更新描述符失败"<<std::endl;
}
//父进程派发任务
Write(pipely[1]);
//回收子进程
int status;
int pid=waitpid(-1,&status,0);
if(pid>0)
{
//如果子进程是被信号杀死
if(WIFSIGNALED(status)&&WTERMSIG(status))
{
//打印信号
std::cout<<"子进程"<<getpid()<<"被"<<WTERMSIG(status)<<"号信号杀死"<<std::endl;
}
}
else
{
std::cout<<"没有子进程退出....."<<std::endl;
}
}
}
return 0;
}
(5)严重错误讲解与修复
(1)第一个是变量命令的问题,抛开变量名重复不说,fork()的返回类型应该是 pid_t 类型
(2)time随机种子的变化应该是 1秒 变化一次,而 for 循环变化太快了,导致每次抽取任务一样
(3)在父进程重定向显示器输出到管道的写端后,应该及时复原,否则出现如下情况:
这里重定向的本质:终端输出->管道写端,执行第一次管道是没有问题的
但是如果不复原管道,执行第二次 Write 时,父进程会把数据输出到第一次管道的写端,但 是第一次管道已经被关闭了,就导致数据写不进去
(4)waitpid()的第一次参数,-1 表示回收任意子进程,而如果之前的子进程正在执行任务,可 能会直接被 waitpid 回收,因此第一次参数应该调整为 d(回收当前循环的子进程)
所以我们的代码应该修改为如下这样:


效果展示:

更多推荐




所有评论(0)