目录
[5.system V共享内存](#5.system V共享内存)
[6.system V消息队列](#6.system V消息队列)
[7.system V信号量](#7.system V信号量)
1.进程间通信介绍
进程通信的本质是:先让不同的进程看到同一份资源"内存",然后才有通信的条件
1.1进程间通信目的
数据传输:⼀个进程需要将它的数据发送给另⼀个进程
资源共享:多个进程之间共享同样的资源。
通知事件:⼀个进程需要向另⼀个或⼀组进程发送消息,通知它(它们)发⽣了某种事件(如进程终⽌时要通知⽗进程)。
进程控制:有些进程希望完全控制另⼀个进程的执⾏(如Debug进程),此时控制进程希望能够拦截另⼀个进程的所有陷⼊和异常,并能够及时知道它的状态改变。
1.2进程间通信发展
管道
System V进程间通信
POSIX进程间通信
1.3进程间通信分类
管道
匿名管道pipe
命名管道
System V IPC--- 是一种标准
System V 消息队列
System V 共享内存
System V 信号量
POSIX IPC
消息队列
共享内存
信号量
互斥量
条件变量
读写锁
2.管道
什么是管道
管道是Unix中最古⽼的进程间通信的形式。
我们把从⼀个进程连接到另⼀个进程的⼀个数据流称为⼀个"管道"
示例:一个简单的管道流程图
- 命令含义
who:列出当前登录到系统的所有用户信息。wc -l:统计输入数据的行数。|:管道操作符,它的作用是将前一个命令的标准输出(stdout),作为后一个命令的标准输入(stdin)。所以,
who | wc -l的最终效果是:统计当前登录到系统的用户数量。
执行流程
创建管道:Shell 首先在内核中创建一个匿名管道。这个管道是一个单向的、先进先出(FIFO)的内核缓冲区,用于进程间通信。
创建进程 :Shell 会创建两个子进程,分别执行
who和wc -l。重定向文件描述符 :
- 对于
who进程:将其标准输出(文件描述符 1)重定向到管道的写入端。这样,who输出的所有内容都被写入管道,而不是直接打印到终端。- 对于
wc -l进程:将其标准输入(文件描述符 0)重定向到管道的读取端。这样,wc -l就从管道中读取数据,而不是等待用户从键盘输入。执行命令 :两个进程开始并行执行。
who产生的输出通过管道源源不断地流向wc -l,wc -l则对这些数据进行行数统计。输出结果 :当
who执行完毕并关闭管道的写入端后,wc -l会读到文件结束符(EOF),完成统计并将最终结果(即用户数量)输出到终端(标准输出)。
3.匿名管道
通常用来做父子进程通信
include <unistd.h>
功能 : 创建⼀⽆名管道
原型
int pipe ( int fd[ 2 ]);
参数
fd :⽂件描述符数组 , 其中 fd[ 0 ] 表⽰读端 , fd[ 1 ] 表⽰写端
返回值 : 成功返回 0 ,失败返回错误代码
3.1实例代码
//////模拟实现管道
// // 从键盘读取数据,写到管道,从管道读取数据,写入屏幕
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <unistd.h>
int main(void)
{
int fsd[2]; // 管道的文件描述符数组
char buf[100]; // 缓冲区
int len; // 描述数据的长度
if (pipe(fsd) == -1) {
perror("创建管道失败!");
return 1;
}
// 从键盘读取数据,写入缓冲区
while (fgets(buf, sizeof(buf), stdin))
{
len = strlen(buf);
std::cout <<std::endl;
// 从缓冲区写入管道
if (write(fsd[1], buf, len) != len) // 读会返回写入的大小
{
perror("管道写失败!");
return 1;
}
// 清空缓冲区
memset(buf, 0, sizeof(buf));
// 从管道读取数据到缓冲区
if ((len = read(fsd[0], buf, sizeof(buf))) == -1) // 返回-1,表示读失败
{
perror("管道读失败!");
return 1;
}
// 从缓冲区写入数据到屏幕
if (write(1, buf, len) != len)
{
perror("写入屏幕失败!");
return 1;
}
std::cout <<std::endl;
}
return 0;
}
3.2用fork来共享内存管道原理
3.3站在文件描述符角度--深度理解管道
3.4站在内核角度--管道本质
3.5管道样例
3.5.1测试管道读写
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstring>
#include <cstdio>
using namespace std;
void ChildPipe(int wfds)
{
char buf[1024];
int cnt = 0;
while(true)
{
snprintf(buf,sizeof(buf),"I am a child,pid:%d,cnt:%d",getpid(),cnt++);
write(wfds,buf,sizeof(buf)-1);
sleep(1);
}
}
void FatherPipe(int rfds)
{
char buf[1024*4*4];
while(true)
{
buf[0] = 0;
ssize_t n = read(rfds,buf,sizeof(buf));
if(n > 0)
{
buf[n] = 0;
cout << "child say:" << buf << endl;
}
else if(n == 0)//不写了,就执行退出
{
cout << "子进程退出,我也退出!" << endl;
}
else
{
break;
}
break;//读一次就退出,测试打印读取退出码
}
}
int main()
{
//1创建管道
int fds[2];//fds[0]读端,fds[1]写端
if(pipe(fds) == -1)
{
cerr<<"pipe error!"<<endl;
exit(1);
}
cout<<"fds[0]"<<fds[0]<<endl;
cout<<"fds[1]"<<fds[1]<<endl;
pid_t id = fork();
while(id == 0)
{
//进入子进程---关闭不需要的读端
close(fds[0]);
ChildPipe(fds[1]);
close(fds[1]);
}
//3.父进程---关闭不需要的写端
close(fds[1]);
FatherPipe(fds[0]);
close(fds[0]);
sleep(5);
int status = 0;
int ret = waitpid(id,&status,0);
if(ret > 0)
{
printf("exit code:%d,exit signal:%d\n",WIFEXITED(status),WEXITSTATUS(status));
sleep(5);
}
return 0;
}
3.5.2创建进程池处理任务
一、核心设计理念
整个进程池的核心目标是:创建一组子进程作为 "工作节点",父进程通过管道向子进程分发任务,实现任务的异步执行和负载均衡。核心设计原则:
- 分层封装:把 "管道""管道管理""进程池" 拆成独立类,每层只做一件事;
- 职责分离:父进程负责 "创建 / 调度 / 发任务",子进程负责 "读任务 / 执行任务";
- 接口归一:用函数指针统一任务类型,用管道统一通信方式;
- 资源安全:严格管理文件描述符,避免继承导致的管道关闭失败、资源泄漏。
二、代码分层拆解(从底层到上层)
整个代码分为 4 层,层层依赖、各司其职:
- 基础任务层(Task.hpp,外部依赖)
- 核心 :通过
typedef void (*task_t)()定义 "无参无返" 的任务函数指针类型,封装PrintLog/Download/Upload等具体任务,以及TaskManager类(负责任务注册、随机选任务、执行任务);- 作用:把 "业务任务" 抽象成统一的 "任务码 + 函数指针",让进程池不用关心任务具体逻辑,只负责分发任务码。
- 管道通信层(Channel 类)
- 核心 :封装 "单个子进程的通信管道",核心属性是
_wfd(管道写端 fd,父进程用)、_subid(子进程 PID);- 核心方法 :
Send(int code):通过管道写端发送任务码(父进程给子进程发任务);Close():关闭当前管道的写端 fd;Wait():回收对应子进程的资源;- 作用:把 "管道 fd + 子进程 PID" 绑定成一个 "通信通道",屏蔽底层 fd 操作的细节,提供面向对象的通信接口。
- 管道管理层(ChaneelManager 类,注意拼写笔误 Channel)
- 核心 :管理所有
Channel对象,实现 "轮询选通道 + 批量关闭管道 + 批量回收子进程";- 核心逻辑 :
Insert():新增通道(父进程创建子进程后,把管道写端 + PID 注册进来);Select():轮询选择通道(返回Channel&,避免拷贝,保证操作原对象),实现负载均衡;CloseAll():批量关闭所有通道的写端(子进程用来清理继承的冗余 fd);CloseAndWait():批量关闭写端 + 回收子进程(父进程销毁时用);- 作用:把 "多个独立通道" 管理成一个 "通道池",提供批量操作和调度能力。
- 进程池核心层(ProcessPool 类)
- 核心:整合 "任务管理 + 管道管理",实现进程池的创建、任务分发、销毁全生命周期;
ProcessPool.hpp
#ifndef PROCESS_POOL_HPP
#define PROCESS_POOL_HPP
#include <iostream>
#include <string>
#include <unistd.h>
#include <vector>
#include <sys/types.h>
#include <sys/wait.h>
#include "Task.hpp"
using namespace std;
//先描述管道
class Channel
{
public:
Channel(int fd,pid_t id):_wfd(fd),_subid(id)
{
_name = "Channel-" + std::to_string(_wfd) + "-" + std::to_string(_subid);
}
void Send(int code)
{
ssize_t n = write(_wfd,&code,sizeof(code));
(void)n;
}
void Close()
{
close(_wfd);
}
void Wait()
{
pid_t rid = waitpid(_subid,nullptr,0);
(void)rid;
}
int Wfd() {return _wfd;}
pid_t Id() {return _subid;}
std::string Name() {return _name;}
~Channel(){}
private:
int _wfd;
pid_t _subid;
std::string _name;
};
//再组织管道
class ChaneelManager
{
public:
ChaneelManager():_next(0)
{}
void Insert(int wfd,pid_t subid)
{
_channels.emplace_back(wfd,subid);
}
void PrintChannel()
{
for(auto &channel : _channels)
{
cout << channel.Name() << endl;
}
}
Channel &Select()
{
//轮询选择一个信道/进程,保证负载均衡
auto &c = _channels[_next];
_next++;
_next %= _channels.size();//防止溢出
return c;
}
void CloseAll()
{
for(auto &channel : _channels)
{
channel.Close();
std::cout<<"关闭:"<< channel.Name() << std::endl;
}
}
void CloseAndWait()
{
//因为在进行创建子进程时,会导致后创建的子进程会继承前面父进程的文件描述符表,从而导致后面的子进程会指向
//前面的进程的写端导致正这关闭会失败,
for(auto &channel : _channels)
{
channel.Close();
std::cout<<"关闭:"<< channel.Name() << std::endl;
channel.Wait();
std::cout<<"回收子进程:"<< channel.Name() << std::endl;
}
}
~ChaneelManager(){}
private:
std::vector<Channel> _channels;
int _next;
};
const int gdefaultnum = 5;
class ProcessPool//进程池
{
public:
ProcessPool(int num):_process_num(num)
{
_tm.Rsgister(PrintLog);
_tm.Rsgister(Download);
_tm.Rsgister(Upload);
}
void Work(int rfd)
{
while(true)
{
int code =0;
ssize_t n = read(rfd,&code,sizeof(code));
if(n > 0)
{
//成功
if(n != sizeof(code))//判断code是否格式正确
continue;
cout << "子进程[" << getpid() << "]读取到了一个任务码" << code << endl;
_tm.Execute(code);
}
else if(n == 0)
{
//写端关闭,读到了结尾
cout << "子进程退出!" << endl;
break;
}
else
{
//失败
cout << "read error!" <<endl;
break;
}
}
}
bool Create()
{
//循环创建多个管道和子进程
for(int i =0;i<_process_num;i++)
{
int pipefd[2]={0};
int n = pipe(pipefd);
if(n < 0) return false;
pid_t pid = fork();
if(pid < 0) return false;
else if(pid == 0)
{
//因为在进行创建子进程时,会导致后创建的子进程会继承前面父进程的文件描述符表,从而导致后面的子进程会指向
//前面的进程的写端导致正这关闭会失败,
//让子进程自己关闭自己继承下来父进程指向哥哥进程的w端
//不会关闭父进程的写端,因为子进程操作的是自己的文件描述符表,不是父进程的
_cm.CloseAll();
//进入子进程---关闭写端
close(pipefd[1]);
Work(pipefd[0]);
close(pipefd[0]);
exit(0);
}
else
{
//进入父进程---关闭读端
close(pipefd[0]);
_cm.Insert(pipefd[1],pid);
// close(pipefd[1]);
}
}
return true;
}
void Debug()
{
_cm.PrintChannel();
}
void Run()
{
int taskcode = _tm.Code();
auto &c = _cm.Select();
cout << "选择了一个信道/进程:" << c.Name() << endl;
c.Send(taskcode);
cout << "发送了一个任务码:" << taskcode << endl;
}
void Stop()
{
//关闭父进程的写端
//回收所有子进程
_cm.CloseAndWait();
}
~ProcessPool(){}
private:
ChaneelManager _cm;
int _process_num;
TaskManager _tm;
};
#endif
Task.hpp
#pragma once
#include <iostream>
#include <vector>
#include <cstdlib>
using namespace std;
typedef void (*task_t)();//定义一种"无参数、无返回值的函数"类型,命名为 task_t,函数指针
void PrintLog()
{
std::cout<<"我是一个打印日志的任务"<<std::endl;
}
void Download()
{
std::cout<<"我是一个下载的任务"<<std::endl;
}
void Upload()
{
std::cout<<"我是一个上传的任务"<<std::endl;
}
class TaskManager
{
public:
TaskManager()
{
srand(time(nullptr));
}
void Rsgister(task_t t)//添加任务
{
_tasks.push_back(t);
}
int Code()//选择任务
{
return rand() % _tasks.size();
}
void Execute(int code)
{
if(code >= 0 && code < _tasks.size())
{
_tasks[code]();//通过函数指针调用函数,_tasks是存储task_t的容器,取出下标为code的元素,()是调用该函数指针指向的函数
}
}
~TaskManager()
{}
private:
std::vector<task_t> _tasks;//_tasks 是存储"无参数无返回值函数指针"的容器
};
Main.cc
#include <iostream>
#include "ProcessPool.hpp"
int main()
{
ProcessPool pp(gdefaultnum);
pp.Create();
int cnt = 10;
while(cnt--)
{
pp.Run();
sleep(1);
}
pp.Stop();
sleep(10);
return 0;
}
总结进程池代码的核心设计思路是:
- 抽象化:把 "管道" 抽象成 Channel,把 "任务" 抽象成函数指针,屏蔽底层细节;
- 管理层:用 ChannelManager 管理多个通道,实现批量操作和轮询调度;
- 解耦化:进程池只负责 "创建进程、分发任务、回收资源",不关心任务具体逻辑;
- 安全化:严格管理文件描述符,解决多进程继承 fd 导致的资源泄漏问题。
3.6管道读写规则
-当没有数据可读时
O_NONBLOCK disable:read调⽤阻塞,即进程暂停执⾏,⼀直等到有数据来到为⽌。
O_NONBLOCK enable:read调⽤返回-1,errno值为EAGAIN。
-当管道满的时候
O_NONBLOCK disable: write调⽤阻塞,直到有进程读⾛数据
O_NONBLOCK enable:调⽤返回-1,errno值为EAGAIN
-如果所有管道写端对应的⽂件描述符被关闭,则read返回0
-如果所有管道读端对应的⽂件描述符被关闭,则write操作会产⽣信号SIGPIPE,进⽽可能导-致write进程退出
-当要写⼊的数据量不⼤于PIPE_BUF时,linux将保证写⼊的原⼦性。
-当要写⼊的数据量⼤于PIPE_BUF时,linux将不再保证写⼊的原⼦性。
3.7管道特点
只能⽤于具有共同祖先的进程(具有亲缘关系的进程)之间进⾏通信;通常,⼀个管道由⼀个进程创建,然后该进程调⽤fork,此后⽗、⼦进程之间就可应⽤该管道。
管道提供流式服务
⼀般⽽⾔,进程退出,管道释放,所以管道的⽣命周期随进程
⼀般⽽⾔,内核会对管道操作进⾏同步与互斥
管道是半双⼯的,数据只能向⼀个⽅向流动;需要双⽅通信时,需要建⽴起两个管道
3.8验证管道通信的4种情况
4.命名管道
匿名管道应⽤的⼀个限制就是只能在 具有共同祖先(具有亲缘关系)的进程间通信 。
如果我们想在 不相关的进程之间交换数据 ,可以使⽤FIFO⽂件来做这项⼯作,它经常被称为 命名管道 。
命名管道是⼀种特殊类型的⽂件
4.1创建一个命名管道
命名管道可以从命令⾏上创建,命令⾏⽅法是使⽤下⾯这个命令:
$ mkfifo filename
命名管道也可以从程序⾥创建,相关函数有:
int mkfifo ( const char *filename, mode_t mode);1.
const char *filename
- 核心含义 :指定要创建的命名管道的路径和文件名(字符串形式)。
- 详细说明 :
- 这是一个指向字符串的常量指针(
const表示函数不会修改该字符串内容),可以是绝对路径 (如/tmp/my_fifo)或相对路径 (如./my_fifo)。2.
mode_t mode
- 核心含义 :指定新建命名管道的访问权限 ,类型为
mode_t(本质是无符号整数,通常用八进制表示)。0644所有者读写,其他只读
创建命名管道:
{
mkfifo( "p2" , 0644 );
unlink(p2);// 删除管道文件
return 0 ;
}
4.2匿名管道与命名管道的区别
匿名管道由pipe函数创建并打开。
命名管道由mkfifo函数创建,打开⽤open
FIFO(命名管道)与pipe(匿名管道)之间唯⼀的区别在它们创建与打开的⽅式不同,⼀但这些⼯作完成之后,它们具有相同的语义。
命名管道可以让两个不相关的进程进行通信
4.3命名管道的打开规则
如果当前打开操作是为读⽽打开FIFO时
O_NONBLOCK disable:阻塞直到有相应进程为写⽽打开该FIFO
O_NONBLOCK enable:⽴刻返回成功
如果当前打开操作是为写⽽打开FIFO时
O_NONBLOCK disable:阻塞直到有相应进程为读⽽打开该FIFO
O_NONBLOCK enable:⽴刻返回失败,错误码为ENXIO
实例1:用命名管道实现文件拷贝
读取文件,写入命名管道:
#include <iostream>#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstring>
#include <cstdio>
#include <fcntl.h>
#include <sys/stat.h>
using namespace std;
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while (0);
int main(int argc,char *argv[])
{
mkfifo("fifo",0644);
int fdin = open("abc",O_RDONLY);
if(fdin < 0) ERR_EXIT("inopen");
int fdout = open("fifo",O_WRONLY);
if(fdout < 0) ERR_EXIT("outopen");
char buf[1024];
int n = 0;
while((n = read(fdin,buf,sizeof(buf)))>0)
{
write(fdout,buf,n);
}
close(fdout);
close(fdin);
return 0;
}
读取管道,写入目标文件:#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstring>
#include <cstdio>
#include <fcntl.h>
#include <sys/stat.h>
using namespace std;
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while (0);
int main(int argc,char *argv[])
{
int fdout = open("abc.c",O_RDONLY | O_WRONLY | O_CREAT,0644);
if(fdout < 0) ERR_EXIT("openout");
int fdin = open("fifo",O_RDONLY);
if(fdin < 0) ERR_EXIT("openin");
char buf[1024];
int n;
while((n = read(fdin,buf,sizeof(buf))) > 0)
{
write(fdout,buf,n);
}
close(fdout);
close(fdin);
return 0;
}
实例2:用命名管道实现server&client通信
一、核心设计思路(全局视角)
你把命名管道的生命周期管理和数据读写操作拆分为两个独立类,整体遵循 "单一职责" 的设计原则,完整覆盖了命名管道从创建到销毁、从打开到读写的全流程:
- 分层设计:职责拆分
NameFifo 类 :专注于管道文件的生命周期管理
- 构造函数:负责创建命名管道(mkfifo),并处理权限(umask (0))
- 析构函数:负责销毁命名管道(unlink),保证资源自动释放
- 核心价值:把管道的 "创建 / 销毁" 封装成类的生命周期,避免手动管理导致的资源泄漏
FifoOper 类 :专注于管道的读写操作
- 构造函数:初始化管道路径,维护文件描述符(_fd)
- 核心方法:OpenForWrite/OpenForRead(打开管道)、Write/Read(数据传输)、Close(关闭文件描述符)
- 核心价值:把管道的 "打开 - 读写 - 关闭" 操作封装,屏蔽底层系统调用的细节
- 跨进程通信的核心逻辑
- 基于 Linux 命名管道的特性:读端阻塞等待写端打开,写端阻塞等待读端打开
- 采用 "单工通信" 模式:一个进程只写、一个进程只读,符合命名管道的基础使用场景
- 数据流转:写进程输入字符串 → 写入管道 → 读进程从管道读取 → 输出到终端
二、核心设计思想
- 面向对象(OOP)封装思想
这是代码最核心的设计思想,把 "管道" 这个实体抽象为类,解决了传统 C 语言裸用系统调用的痛点:
- 数据封装:把管道路径(_path)、文件名(_name)、完整路径(_fifoname)、文件描述符(_fd)等核心数据封装为私有成员,避免外部随意修改
- 行为封装:把 mkfifo、open、read/write、unlink 等系统调用封装为类的成员方法,对外只暴露简洁的接口(如 OpenForWrite、Write),降低使用成本
- 资源自动管理:利用 C++ 析构函数的特性,在 NameFifo 对象销毁时自动删除管道文件,避免 "僵尸管道文件" 残留
- 分层解耦思想
把 "管道生命周期管理" 和 "管道数据操作" 拆分为两个类,实现了职责解耦:
- NameFifo 只关心 "管道是否存在",不关心管道里传什么数据
- FifoOper 只关心 "怎么读写数据",不关心管道是怎么创建的
- 好处:修改其中一个类的逻辑(比如改管道创建权限),不会影响另一个类的功能,符合 "开闭原则"
- 遵循 Linux 系统编程范式
- 严格遵循命名管道的使用规则:先创建(mkfifo)→ 再打开(open)→ 再读写(read/write)→ 最后关闭 / 删除(close/unlink)
- 处理系统调用的返回值:检查 mkfifo、open、read/write 的返回值,打印错误提示(虽然可以更完善)
- 权限处理:通过 umask (0) 保证管道文件的权限符合预期(0666)
- 可复用性设计
- 通过宏定义(PATH、NAME)抽离管道的路径和文件名,方便全局修改
- 类的构造函数支持自定义路径和文件名,不是硬编码,可适配不同的管道命名需求
- FifoOper 类同时支持读和写操作,可复用为 "读进程" 或 "写进程" 的操作类
comm.hpp:
#pragma once
#include <iostream>
#include <string>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
using namespace std;
#define PATH "."
#define NAME "fifo"
class NameFifo
{
public:
NameFifo(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)
{
cout << "管道创建失败!" << endl;
}
else
{
cout << "管道创建成功!" << endl;
}
}
~NameFifo()
{
int n = unlink(_fifoname.c_str());
if(n < 0)
{
cout << "管道删除失败!" << endl;
}
else
{
cout << "管道删除成功!" << endl;
}
}
private:
std::string _path;//路径
std::string _name;//文件名
std::string _fifoname;//文件完整路径
};
class FifoOper
{
public:
FifoOper(const std::string &path,const std::string &name)
:_path(path),
_name(name),
_fd(-1)
{
_fifoname = _path + "/" + _name;
}
void OpenForWrite()
{
_fd = open(_fifoname.c_str(),O_WRONLY);//写
if(_fd < 0)
{
cout << "打开失败!" << endl;
}
else
{
cout << "打开成功" << endl;
}
}
void OpenForRead()
{
_fd = open(_fifoname.c_str(),O_RDONLY);//读
if(_fd < 0)
{
cout << "打开失败!" << endl;
}
else
{
cout << "打开成功" << endl;
}
}
void Write()
{
std::string message;
int cnt = 1;//写入的次数
pid_t id = getpid();
while(true)
{
cout << "Please write:";
std::getline(cin,message);
message += (",message number" + std::to_string(cnt++) +",[" + std::to_string(id) + "]" );
write(_fd,message.c_str(),message.size());
}
}
void Read()
{
while(true)
{
char buf[1024];
int n = read(_fd,buf,sizeof(buf));
if(n > 0)
{
buf[n] = 0;
cout << "client say:" << buf << endl;
}
else if(n == 0)
{
cout << "read over!" << endl;
break;
}
else
{
cout << "read error!" << endl;
break;
}
}
}
void Close()
{
if(_fd>0)
close(_fd);
}
~FifoOper()
{}
private:
std::string _path;//路径
std::string _name;//文件名
std::string _fifoname;//文件完整路径
int _fd;//文件描述符
};
server.cc:
#include "comm.hpp"
int main()
{
//创建管道文件
NameFifo fifo(".","fifo");
FifoOper readerfifo(".","fifo");
readerfifo.OpenForRead();
readerfifo.Read();
readerfifo.Close();
return 0;
}
client.cc:
#include "comm.hpp"
int main()
{
FifoOper writefifo(PATH,NAME);
writefifo.OpenForWrite();
writefifo.Write();
writefifo.Close();
return 0;
}
运行示例:
5.system V共享内存
System V IPC 是 Unix/Linux 系统中一套经典的进程间通信(IPC)机制 ,由 AT&T 在 System V Unix 中引入,Linux 完全兼容并广泛使用。它由内核统一管理三类对象:消息队列、共享内存、信号量,用于进程间数据交换、内存共享与同步互斥。
共享内存区是最快的IPC形式。⼀旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执⾏进⼊内核的系统调⽤来传递彼此的数据
5.1共享内存示意图

总结
- System V 共享内存的核心是「内核
shmid_ds结构体 + 物理内存段」的绑定关系,结构体管 "身份和规则",物理内存管 "数据存储";- 进程与共享内存的交互,本质是通过
shmid关联到内核结构体,再通过页表映射到物理内存;- 共享内存的 "共享" 是物理内存的共享,进程间无数据拷贝,这也是它效率最高的原因,而内核结构体则是实现这种共享的 "管理中枢"。
5.2共享内存数据结构
struct shmid_ds {
struct ipc_perm shm_perm ; /* operation perms */
int shm_segsz; /* size of segment (bytes) */
__kernel_time_t shm_atime; /* last attach time */
__kernel_time_t shm_dtime; /* last detach time */
__kernel_time_t shm_ctime; /* last change time */
__kernel_ipc_pid_t shm_cpid; /* pid of creator */
__kernel_ipc_pid_t shm_lpid; /* pid of last operator */
unsigned short shm_nattch; /* no. of current attaches */
unsigned short shm_unused; /* compatibility */
void shm_unused2; / ditto - used by DIPC */
void shm_unused3; / unused */
};
5.3共享内存函数
shmget () - 创建 / 获取共享内存段
中文名 :共享内存获取函数作用:向内核申请一块共享内存,或获取已存在的共享内存段的 ID。函数原型:
#include <sys/ipc.h> #include <sys/shm.h> int shmget(key_t key, size_t size, int shmflg);参数解析:
key:共享内存的全局标识(通常用ftok()生成,比如ftok(".", 1));特殊值IPC_PRIVATE表示创建仅当前进程可见的共享内存。size:共享内存的大小(字节),创建新内存时必须指定,获取已有内存时可设为 0。shmflg:标志位,常用组合:
IPC_CREAT:如果不存在则创建,存在则获取。IPC_EXCL | IPC_CREAT:确保创建新内存(如果已存在则报错,避免重复创建)。- 权限位:如
0666(同文件权限,代表所有用户可读可写)。返回值 :成功返回共享内存 ID(shmid),失败返回 -1(需查errno)。
shmat () - 映射共享内存到进程地址空间中文名 :共享内存附加函数作用:把内核中的共享内存段,映射到当前进程的虚拟地址空间,之后可像操作普通内存一样读写。
函数原型:
void *shmat(int shmid, const void *shmaddr, int shmflg);参数解析:
shmid:shmget()返回的共享内存 ID。shmaddr:指定映射到进程的地址,通常设为NULL(让内核自动分配,推荐)。shmflg:映射标志,常用:
0:默认,可读可写。SHM_RDONLY:只读映射(仅能读取共享内存,不能写入)。返回值 :成功返回映射后的内存起始地址,失败返回(void*)-1。
shmdt () - 解除共享内存映射中文名 :共享内存分离函数作用:将进程地址空间中的共享内存映射解除(只是断开关联,不会删除内核中的共享内存)。
函数原型:
int shmdt(const void *shmaddr);参数解析:
shmaddr:shmat()返回的映射地址。返回值:成功返回 0,失败返回 -1。
shmctl () - 控制共享内存(查询 / 删除 / 设置属性)中文名 :共享内存控制函数作用 :对共享内存段进行管理,最常用的是删除共享内存(否则内核会一直保留,直到重启)。
函数原型:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);参数解析:
shmid:共享内存 ID。cmd:操作命令,核心常用:
IPC_RMID:删除共享内存段(内核释放该内存,所有进程都无法再访问)。IPC_STAT:获取共享内存的属性(如大小、权限、关联进程数,存入buf)。IPC_SET:设置共享内存的属性(通过buf修改)。buf:存放属性的结构体指针,使用IPC_RMID时可设为NULL(无需传递属性)。返回值:成功返回 0,失败返回 -1。
实例1:共享内存实现通信
#ifndef COMM_HPP
#define COMM_HPP
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
using namespace std;
#define PATHNAME "."
#define PROJ_ID 0x6666
static int commShm(int size,int shmflg)
{
key_t key = ftok(PATHNAME,PROJ_ID);
if(key < 0)
{
perror("ftok error");
return -1;
}
int shmid = 0;
if((shmid = shmget(key,size,shmflg))<0)
{
perror("shmget error");
return -2;
}
return shmid;
}
int DestoryShm(int shmid)
{
if(shmctl(shmid,IPC_RMID,NULL) < 0)
{
perror("shmctl error");
return -1;
}
return 0;
}
int CreateShm(int size)
{
return commShm(size,IPC_CREAT|IPC_EXCL|0666);
}
int GetShm(int size)
{
return commShm(size,IPC_CREAT);
}
#endif
//server.cc#include "comm.hpp"
int main()
{
int shmid = CreateShm(4096);
char *addr = (char*)shmat(shmid,nullptr,0);
int i = 0;
while(i++ < 26)
{
cout << "client say:" << addr << endl;
sleep(1);
}
shmdt(addr);
DestoryShm(shmid);
return 0;
#include "comm.hpp"
int main()
{
int shmid = GetShm(4096);
char *addr = (char*)shmat(shmid,nullptr,0);
int i = 0;
while(i<26)
{
addr[i] = 'A' + i;
i++;
addr[i] = 0;
sleep(1);
}
shmdt(addr);
return 0;
}
实例2:借助管道实现访问控制版的关系内存
一、共享内存封装类 (Shm) 的核心设计思路
这个
Shm类是对 Linux 系统调用(ftok/shmget/shmat/shmdt/shmctl)的面向对象封装,核心设计思路可拆解为以下几点:
- 职责划分:将共享内存操作抽象为 "创建者 (CREATER)" 和 "使用者 (USER)"
- 创建者 (CREATER) :负责全新共享内存的创建(
IPC_CREAT | IPC_EXCL),且只有创建者有权销毁共享内存- 使用者 (USER) :只获取已存在的共享内存(仅
IPC_CREAT),不参与销毁- 这种设计符合 "谁创建、谁销毁" 的资源管理原则
- 生命周期管理:RAII 机制自动管理资源
- 构造函数 :完成
ftok生成 key → 创建 / 获取共享内存 → 挂载 (attach) 到进程地址空间- 析构函数:自动执行卸载 (detach) → (创建者)销毁共享内存
- 无需手动调用
shmdt/shmctl,避免内存泄漏
- 功能分层:将核心操作拆分为私有辅助函数 + 公有接口
- 私有辅助函数 :
CreateHelp:封装shmget的公共逻辑,避免代码冗余Create/Get:区分创建 / 获取共享内存的不同逻辑Attach/Detach/Destory:封装挂载 / 卸载 / 销毁操作- 公有接口 :
VirtualAddr():对外提供共享内存的虚拟地址(核心通信入口)Size()/Attr():获取共享内存大小、属性等辅助功能
- 常量封装:抽离硬编码参数,提升可维护性
- 把共享内存大小 (
gsize)、key 生成参数 (pathname/proj_id)、权限 (gcode) 等抽离为全局常量,便于统一修改二.核心说明
- 管道的作用 :作为 "同步信号",解决共享内存 "写了没读、读了没写" 的问题,服务端通过
read()阻塞等待客户端的write()信号;- 数据流程 :
- 客户端:写共享内存 → 向管道写 1 字节信号 → 服务端被唤醒 → 读共享内存 → 清空数据;
- 服务端:阻塞读管道 → 收到信号 → 读共享内存 → 继续阻塞;
//Shm.hpp#pragma once
#include "Comm.hpp"
#include <iostream>
#include <sys/types.h>
#include <string>
#include <sys/shm.h>
#include <sys/ipc.h>
#include <unistd.h>
#include <string>
using namespace std;
const int gdefaultid = -1;
const int gsize = 4096;
const std::string pathname = ".";
const int proj_id = 0x66;//key的唯一标识id
const int gcode = 0666;//权限
#define CREATER "creater"
#define USER "user"
class Shm
{
private:
void CreateHelp(int flag)
{
printf("key:0x%x\n",_key);
_shmid = shmget(_key,_size,flag);
if(_shmid < 0)
{
ERR_EXIT("shmget");
}
printf("shmid:%d\n",_shmid);
}
void Create()//创建一个全新的共享内存
{
CreateHelp(IPC_CREAT | IPC_EXCL | gcode);
}
void Get()
{
CreateHelp(IPC_CREAT);
}
void Attach()
{
_start_mem = shmat(_shmid,nullptr,0);
if((long long)_start_mem < 0)
{
ERR_EXIT("shmat");
}
printf("shmat success!\n");
}
void Detach()
{
int n =shmdt(_start_mem);
if(n < 0)
{
ERR_EXIT("shmdt");
}
printf("shmdt success!\n");
}
void Destory()
{
Detach();
if(_usertype == CREATER)
{
int n = shmctl(_shmid,IPC_RMID,nullptr);
if(n == 0)
cout <<"删除成功!" << endl;
else
ERR_EXIT("shmctl");
}
}
public:
Shm(const std::string &pathname,int flag,const std::string &usertype)
:_shmid(gdefaultid),
_size(gsize),
_start_mem(nullptr),
_usertype(usertype)
{
_key = ftok(pathname.c_str(),proj_id);
if(_key < 0)
{
ERR_EXIT("ftok");
}
if(usertype == CREATER)
Create();
else if(usertype == USER)
Get();
Attach();
}
void* VirtualAddr()
{
printf("virtualaddr:%p\n",_start_mem);
return _start_mem;
}
int Size()
{
return _size;
}
void Attr()
{
struct shmid_ds ds;//定义内核管理结构体变量
int n = shmctl(_shmid,IPC_STAT,&ds);//通过 shmctl 调用获取内核中共享内存的管理结构体 shmid_ds
printf("shm_segsz:%ld\n",ds.shm_segsz);//打印内存大小
printf("_key:0x%x\n",ds.shm_perm.__key);//打印全局key
}
~Shm()
{
Destory();
}
private:
int _shmid;
key_t _key;
int _size;
void* _start_mem;
std::string _usertype;
};
//server.cc#include "Shm.hpp"
#include "Fifo.hpp"
int main()
{
Shm shm(pathname,proj_id,CREATER);
NameFifo fifo(PATH,FILENAME);
FileOper readerfifo(PATH,FILENAME);
readerfifo.OpenForRead();
char *mem = (char*)shm.VirtualAddr();
while(true)
{
if(readerfifo.Wait())
printf("client say:%s\n",mem);
else
break;
}
readerfifo.Close();
#include "Shm.hpp"
#include "Fifo.hpp"
int main()
{
FileOper writefifo(PATH,FILENAME);
writefifo.OpenForWrite();
Shm shm(pathname,proj_id,USER);
char *mem = (char*)shm.VirtualAddr();
int index =0;
for(char c = 'A' ;c <= 'F';c++,index += 2)
{
sleep(1);
mem[index] = c;
mem[index+1] = c;
sleep(1);
mem[index+2] = '\0';
writefifo.Wakeup();//唤醒服务器开始写
}
writefifo.Close();
return 0;
#pragma once
#include <cstdio>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while (0);
//Makefile.PHONY:all
all:client server
client:client.cc
g++ -o @ ^
server:server.cc
g++ -o @ ^
.PHONY:clean
clean:
rm -f client server
运行结果:
6.system V消息队列
-消息队列提供了⼀个从⼀个进程向另外⼀个进程发送⼀块数据的⽅法
-每个数据块都被认为是有⼀个类型,接收者进程接收的数据块可以有不同的类型值
-特性⽅⾯
IPC资源必须删除,否则不会⾃动清除,除⾮重启,所以system V IPC资源的⽣命周期随内核
结论1:消息队列,提供了一种,一个进程给另一个进程发送有关类型数据块的方式
结论2:OS对消息队列进行管理
结论3:设置key来确保两个进程看到的是同一个消息对列
7.system V信号量
信号量主要⽤于同步和互斥的,下⾯先来看看什么是同步和互斥。
7.1并发编程,概念铺垫
-多个执⾏流(进程), 能看到的同⼀份公共资源:共享资源
-被保护起来的共享资源叫做临界资源
-保护的⽅式常⻅:互斥与同步
-任何时刻,只允许⼀个执⾏流访问资源,叫做互斥
-多个执⾏流,访问临界资源的时候,具有⼀定的顺序性,叫做同步
-系统中某些资源⼀次只允许⼀个进程使⽤,称这样的资源为临界资源或互斥资源。
-在进程中涉及到互斥资源的程序段叫临界区。你写的代码=访问临界资源的代码(临界区)+不访问临界资源的代码(⾮临界区)
-所谓的对共享资源进⾏保护,本质是对访问共享资源的代码进⾏保护
7.2信号量
一、核心概念
- 信号量的本质:带 "原子操作" 的计数器
信号量不是用来存储数据的(和共享内存不同),它的核心是通过计数器控制对临界资源的访问权限 ,且所有对计数器的操作(+1/-1)都是原子性的(要么做完,要么没做,不会被中断)。
可以用一个通俗例子理解:
- 假设你去银行办理业务,银行有 3 个窗口(临界资源),信号量的初始值就是 3(代表可用窗口数);
- 你来办理业务(申请资源):信号量 - 1(P 操作),可用窗口剩 2;
- 你办完业务(释放资源):信号量 + 1(V 操作),可用窗口回到 3;
- 如果 3 个窗口都被占了(信号量 = 0),再来的人必须等待(P 操作阻塞),直到有人办完业务(V 操作)。
- 信号量的两个核心操作(P/V 操作)
操作 英文名 作用 原子性保证 P 操作 wait(等待) 申请资源:信号量 sem -= 1- 若sem >= 0:申请成功,继续执行 - 若sem < 0:申请失败,进程阻塞内核级原子操作,不会被其他进程打断 V 操作 post(释放) 释放资源:信号量 sem += 1- 若sem <= 0:说明有进程阻塞,唤醒一个等待的进程 - 若sem > 0:无进程阻塞,仅更新计数器同上 ⚠️ 关键:P/V 操作的原子性是信号量能保证互斥 / 同步的核心 ------ 避免多个进程同时修改计数器,导致 "超卖"(比如窗口数变成负数)。
- 信号量的分类(对应不同场景)
- 二元信号量(互斥锁) :计数器只能是 0 或 1,对应 "要么可用,要么不可用",专门实现互斥(比如共享内存同一时刻只能被一个进程写);
- 计数信号量 :计数器可以是任意非负整数,对应 "多个同类临界资源",可实现同步 + 互斥(比如多个窗口、多个打印机)。
二、System V 信号量的核心特性
- 生命周期随内核
和 System V 共享内存、消息队列一样,信号量属于内核级 IPC 资源:
- 不会随进程退出而自动销毁,必须手动调用
semctl删除,否则会一直存在于内核中;- 可以用
ipcs -s查看系统中存在的信号量,ipcrm -s [semid]手动删除。
- "预订机制" 的本质
信号量不是等进程访问资源时才检查,而是提前预订资源权限:
- 进程要访问临界资源前,必须先通过 P 操作 "预订"------ 预订成功才能访问,失败则等待;
- 这种机制从源头避免了多个进程同时进入临界区,本质是 "先占坑,再使用"。
- 信号量集(System V 的特殊设计)
System V 信号量不是单个计数器,而是信号量集(一组信号量)------ 可以同时管理多组临界资源。比如:一个进程需要同时访问 "共享内存" 和 "打印机" 两个临界资源,就可以用信号量集中的两个计数器分别控制。
三、信号量如何实现互斥和同步
你之前的共享内存 + 管道代码中,虽然用管道实现了同步,但缺少互斥保护(比如客户端写数据时,服务端同时读可能导致数据错乱),用信号量可以完美解决:
- 实现互斥(以共享内存写操作为例)
- 初始化信号量:二元信号量,初始值 = 1(共享内存可用);
- 客户端写共享内存前:执行 P 操作(信号量 - 1 → 0),此时服务端若想写 / 读,执行 P 操作会阻塞;
- 客户端写完后:执行 V 操作(信号量 + 1 → 1),唤醒阻塞的服务端;
- 服务端读 / 写同理,确保同一时刻只有一个进程操作共享内存。
- 实现同步(补充管道的不足)
你之前用管道实现 "客户端写→服务端读" 的顺序,也可以用信号量实现:
- 初始化两个信号量:
sem_write(客户端写权限):初始值 = 1;sem_read(服务端读权限):初始值 = 0;- 客户端流程:P (
sem_write) → 写共享内存 → V (sem_read);- 服务端流程:P (
sem_read) → 读共享内存 → V (sem_write);- 效果:客户端写完才能唤醒服务端读,服务端读完才能唤醒客户端写,严格保证 "写→读→写" 的顺序。
四、信号量 vs 你之前的管道同步
维度 信号量 管道 核心作用 控制资源访问权限(同步 + 互斥) 传递数据 / 简单信号(主要同步) 原子性 内置原子操作,无需额外处理 读写需自己保证原子性(易出错) 资源类型 内核级 IPC(生命周期随内核) 文件系统对象(管道文件) 适用场景 多进程 / 线程的临界资源保护 简单的进程间通知 / 数据传输
信号量和通信的关系
1.先访问信号量P,每个进程都得先看到同一个信号量
2.不是传递数据,才是通信IPC,通知、同步、互斥也算。
总结
- 核心本质:System V 信号量是带原子操作的计数器,通过 P(申请)/V(释放)操作控制临界资源访问,核心解决同步和互斥问题;
- 关键特性:原子操作保证计数准确,生命周期随内核(需手动删除),支持二元 / 计数两种类型;
- 应用场景:在你之前的共享内存通信中,信号量可替代管道实现更可靠的同步,同时补充互斥保护,避免数据竞争。
8内核是如何组织管理IPC资源的
1、我们知道System V IPC的ds结构体(图中红框)中第一个成员的类型都是kern_ipc_perm(typedef为ipc_perm)类型结构体,所以内核就以一个数组:kern_ipc_perm* ipc_id_ary[N] 来组织所有ipc资源,因为ipc资源的各种ds结构体和kern_ipc_perm结构体的起始地址是一样的,能拿到kern_ipc_perm结构体的地址,再进行特定强转就能访问整个ds结构体,如 (msg_queue*)ipc_id_ary[5]。
2、上面的组织管理形式本质就是C语言完成多态,kern_ipc_perm是基类,各种ds结构体就是子类。这样的好处就是许可统一通过一个数组来管理所有ipc资源。
3、我们通过系统调用:XXXid = XXXget()获取到的各种id变量其实就是ipc_id_ary的数组下标。
通过4、共享内存的结构体shmid_kernel中有一个struct file*类型成员shm_file,能够利用它拿到内核文件缓冲区,所以共享内存是通过下面的逻辑实现从物理内存中映射到虚拟地址空间的:
用的和共享内存一样的映射思路)就是当我们创建共享内存时,先在内核里把共享内存结构体shmid_kernel、文件对象(包含缓冲区)建立好,当我们其中一个进程挂接共享内存时就会创建vm_area_struct指向对应的文件,然后就可以将虚拟地址(vm_area_struct的start和end)和物理地址进行映射,此时就可以依据虚拟地址访问共享内存了。















