前面我们已经学习过管道的进程间通信的方式。但是管道通信相对来说很老了,本期我们就来学习一个相对比较新的进程间通信方式------system V系列。
相关内容已经上传至作者的个人gitee:楼田莉子/Linux学习喜欢的话请点个赞谢谢
目录
前言
System V是unix系统的一个统一的进程间通信机制标准,主要分为SystemV共享内存、SystemV消息队列、SystemV信号量三大板块,内容涵盖接口、返回值、数据结构、key值的共享方式、插入删除、底层原理等
IPC资源数据结构
cpp
struct ipc_perm {
key_t __key; /* Key supplied to xxxget(2) */
uid_t uid; /* Effective UID of owner */
gid_t gid; /* Effective GID of owner */
uid_t cuid; /* Effective UID of creator */
gid_t cgid; /* Effective GID of creator */
unsigned short mode; /* Permissions */
unsigned short __seq; /* Sequence number */
};
SystemV共享内存
原理
我们知道每一个进程都有自己的虚拟地址空间,每一个进程都有属于自己的页表。
而内存块映射到每一个进程的地址空间当中就是systemV共享内存的方式

前面我们说过进程间通信的本质是让不同的进程看到同一份资源 。对于systemV共享内存而言就是让不同的进程将共享内存映射到在自己的虚拟地址空间,每一个进程得到的就是自己的虚拟地址空间内存卡的起始地址。类似于一个简化版本的动态库映射
共享内存映射到进程的虚拟地址空间的共享区
但是通信的时候通常共享内存非常的,因此就必须要被OS管理。因此共享内存=共享内存本体+共享内存的管理结构体
使用共享内存的步骤:
1、创建
2、关联挂接
3、使用
4、去关联
那么我们怎么使用共享内存呢?就来认识一下对应的系统调用。
共享内存的系统调用
shmget函数
功能:用来创建或获取共享内存段
cpp
#include <sys/ipc.h> // 用于 IPC 相关定义,包括 key_t 类型
#include <sys/shm.h> // 共享内存函数和结构体的主要头文件
int shmget(key_t key, size_t size, int shmflg);
参数
-
key:共享内存段的名字/键值。用于标识内核中共享内存的唯一性 -
size:共享内存大小(字节)。必须为4096的整数倍 -
shmflg:由九个权限标志构成,用法和创建文件时使用的mode模式标志类似
参数取值说明
-
取值为
IPC_CREAT:共享内存不存在则创建并返回;共享内存已存在则获取并返回(总要获取一个共享内存) -
取值为IPC_EXC:不能单独使用 -
取值为
IPC_CREAT | IPC_EXCL:共享内存不存在则创建并返回;共享内存已存在则出错返回(只要新的共享内存)
返回值
-
成功:返回一个非负整数,即该共享内存段的标识码(shmid)
-
失败:返回-1
shmat函数
功能:将共享内存段连接到进程地址空间
原型
cpp
#include <sys/ipc.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数
-
shmid:共享内存标识码(由shmget返回) -
shmaddr:指定连接的地址 -
shmflg:可能的取值是SHM_RND和SHM_RDONLY
返回值
-
成功:返回一个指针,指向共享内存第一个字节
-
失败:返回-1
说明
-
shmaddr为NULL:内核自动选择一个地址进行连接 -
shmaddr不为NULL且shmflg无SHM_RND标记:以shmaddr为连接地址 -
shmaddr不为NULL且shmflg设置了SHM_RND标记:连接的地址会自动向下调整为SHMLBA的整数倍。其公式为:shmaddr - (shmaddr % SHMLBA) -
shmflg = SHM_RDONLY:表示连接操作用于只读共享内存
shmdt函数
功能:将共享内存段与当前进程脱离
原型
cpp
#include <sys/ipc.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr);
参数
shmaddr:由shmat函数所返回的指针
返回值
-
成功:返回0
-
失败:返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段
shmctl函数
功能:用于控制共享内存
原型
cpp
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数
-
shmid:由shmget返回的共享内存标识码 -
cmd:将要采取的动作(有三个可取值) -
buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值
-
成功:返回0
-
失败:返回-1
以及删除共享内存的命令:ipcrm
细节
共享内存的生命周期跟随OS一样,如果用户不主动删除,就会跟OS一样一直存在,除非重启OS.
如果创建的时候提示共享内存存在就是key值冲突了。
key和shmid: 的关系类似于fd和incode,他们之间的区别是这样的:
-
key只在内核中,标识共享内存的唯一性!用户使用共享内存,不用这个key!
-
shmid 只在用户中使用,你自己代码中,使用shmid来访问共享内存
案例演示
我们先写一段简单的代码来验证功能
SHM.hpp
cpp
#include<iostream>
#include <cstring>
#include <sys/ipc.h>
#include <sys/shm.h>
//共享内存的路径名和项目标识符
//等价于命名管道的文件路径
//这样PATHNAME和PROJ_ID看到同一份资源
#define PATHNAME "/tmp"
#define PROJ_ID 0x66
class SHM
{
private:
int _shmid;
int _size;
key_t GetKey()
{
return ftok(PATHNAME,PROJ_ID);
}
public:
SHM()
:_shmid(-1)
,_size(4096)
{}
~SHM()
{}
//创建共享内存
void create(int size=4096)
{
//键值key建议使用ftok来动态生成
key_t key=GetKey();
if(key<0)
{
std::cerr << "ftok error: " << \
strerror(errno) << std::endl;
exit(errno); // 使用具体的错误码
}
printf("key=%x,key=%d\n",key,key);
_shmid=shmget(key,size,IPC_CREAT | IPC_EXCL);
if(_shmid<0)
{
std::cerr << "shmget error: " << \
strerror(errno) << std::endl;
exit(errno); // 使用具体的错误码
}
printf("shmget success,key=0x%x,shmid=%d\n",key,_shmid);
}
//获取共享内存
void get()
{
key_t key=GetKey();
if(key<0)
{
std::cerr << "ftok error: " << \
strerror(errno) << std::endl;
exit(errno); // 使用具体的错误码
}
printf("key=0x%x,key=%d\n",key,key);
}
//删除共享内存
void destroy()
{
}
};
Server.cpp
cpp
#include"SHM.hpp"
int main()
{
SHM shm;
shm.create();
return 0;
}
运行结果为:

可以用这个指令来验证
bash
while :; do ipcs -m ; sleep 1; done

当我们再次运行的时候就会发现有文件已存在,要用ipcrm指令删除掉。

接下来完善一下
SHM.hpp
cpp
#include<iostream>
#include <cstring>
#include<unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
const int SHM_SIZE=4096; //默认共享内存大小
//共享内存的路径名和项目标识符
//等价于命名管道的文件路径
//这样PATHNAME和PROJ_ID看到同一份资源
#define PATHNAME "/tmp"
#define PROJ_ID 0x66
class SHM
{
private:
int _shmid;
int _size;
void *_start_addr;
//获取key值
key_t GetKey()
{
return ftok(PATHNAME,PROJ_ID);
}
//
void GetShmId(int shmid)
{
//键值key建议使用ftok来动态生成
key_t key=GetKey();
if(key<0)
{
std::cerr << "ftok error: " << \
strerror(errno) << std::endl;
exit(errno); // 使用具体的错误码
}
printf("key=%x,key=%d\n",key,key);
_shmid=shmget(key,_size,shmid);
if(_shmid<0)
{
std::cerr << "shmget error: " << \
strerror(errno) << std::endl;
exit(errno); // 使用具体的错误码
}
printf("shmget success,key=0x%x,shmid=%d\n",key,_shmid);
}
public:
SHM()
:_shmid(-1)
,_size(SHM_SIZE)
,_start_addr(nullptr)
{}
~SHM()
{}
//创建共享内存
void create()
{
GetShmId(IPC_CREAT | IPC_EXCL | 0666);
}
//获取共享内存
void get()
{
GetShmId(IPC_CREAT);
}
//删除共享内存
void destroy()
{
int n=shmctl(_shmid,IPC_RMID,nullptr);
if(n<0)
{
std::cerr << "shmctl error: " << \
strerror(errno) << std::endl;
exit(errno); // 使用具体的错误码
}
printf("shmctl success,shmid=%d\n",_shmid);
(void)n;
}
//加关联共享内存
void attach()
{
_start_addr = shmat(_shmid, nullptr, 0);
if ((long long int)_start_addr == -1)
exit(3);
}
//去关联共享内存
void detach()
{
int n = shmdt(_start_addr);
if (n < 0)
exit(4);
(void)n;
}
//打印共享内存属性
void PrintAttr()
{
struct shmid_ds ds;
int n = shmctl(_shmid, IPC_STAT, &ds);
if(n < 0)
{
perror("shmctl");
exit(4);
}
printf("key: 0x%x\n", ds.shm_perm.__key);
printf("shm_nattch: %ld\n", ds.shm_nattch);
printf("shm_segsz: 0x%lx\n", ds.shm_segsz);
}
//获取共享内存首地址
void *Addr()
{
return _start_addr;
}
//获取共享内存大小
int size()
{
return _size;
}
};
server.cpp
cpp
#include"SHM.hpp"
int main()
{
//生命周期的代码级管理
SHM shm;
shm.create();
sleep(3);
shm.attach();
sleep(3);
shm.PrintAttr();
char *shm_start = (char *)shm.Addr();
int size = shm.size();
while (true)
{
// 本质就是读取共享内存!
// 临界区代码!
for (int i = 0; i < size; i++)
{
std::cout << shm_start[i] << ' ';
}
std::cout << std::endl;
sleep(1);
}
shm.detach();
sleep(3);
shm.destroy();
return 0;
}
client.cpp
cpp
#include"SHM.hpp"
int main()
{
SHM sharedmem;
sharedmem.get();
sharedmem.attach();
sleep(3);
sharedmem.PrintAttr();
char *shm_start = (char *)sharedmem.Addr();
int size = sharedmem.size();
int index=0;
while(1)
{
printf("请输入@: ");
char ch;
std::cin>>ch;
shm_start[index++]=ch;
// std::cin>>*shm_start;
// shm_start[index++]=*shm_start;
index%=size;
}
sharedmem.detach();
return 0;
}
总结
不同的进程通过协同共享同一份资源来进行进程间通信是为了保持自己的独立性,但是也会导致多执行流并发访问的问题------同步和互斥。
SystemV消息队列
原理
基于一个共享的队列,进程之间实现基于进程块级别的进程通信(队列的节点时一块一块的)

消息队列的系统调用
msgget函数
功能:用来创建或获取一个消息队列
cpp
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
参数
key:消息队列的名字/键值。用于标识内核中消息队列的唯一性
msgflg:由几个权限标志构成,用法和创建文件时使用的mode模式标志类似
参数取值说明
-
取值为
IPC_CREAT:消息队列不存在则创建并返回;消息队列已存在则获取并返回 -
取值为
IPC_EXCL:不能单独使用 -
取值为
IPC_CREAT | IPC_EXCL:消息队列不存在则创建并返回;消息队列已存在则出错返回
返回值
-
成功:返回一个非负整数,即该消息队列的标识码(
msqid) -
失败:返回-1
msgsnd函数
功能:向指定的消息队列发送一条消息
cpp
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
参数
msqid:消息队列标识码,由msgget返回
msgp:指向用户定义的消息缓冲区的指针
msgsz:消息正文的字节数,不包括消息类型字段
msgflg:控制标志,指定当消息队列满时的行为
参数取值说明
-
取值为
0:默认行为,如果消息队列已满则阻塞等待 -
取值为
IPC_NOWAIT:如果消息队列已满,函数立即返回失败
返回值
-
成功:返回0
-
失败:返回-1
msggrcv函数
功能:从指定的消息队列接收一条消息
cpp
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
参数
msqid:消息队列标识码,由msgget返回
msgp:指向用户定义的消息缓冲区的指针
msgsz:消息缓冲区的最大容量
msgtyp:指定要接收的消息类型
msgflg:控制标志,指定接收消息的行为
参数取值说明
msgtyp取值:
-
= 0:接收队列中的第一条消息 -
> 0:接收类型等于msgtyp的第一条消息 -
< 0:接收类型小于等于|msgtyp|的最小类型的第一条消息
msgflg取值:
-
IPC_NOWAIT:如果没有指定类型的消息,函数立即返回失败 -
MSG_NOERROR:如果消息正文长度超过msgsz,则截断消息而不报错
返回值
-
成功:返回实际接收的消息正文字节数
-
失败:返回-1
消息结构
cpp
struct msgbuf {
long mtype; // 消息类型,必须大于0
char mtext[1]; // 消息正文,可以是任意数据类型
};
msgctl函数
功能:对消息队列进行各种控制操作
cpp
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
参数
msqid:消息队列标识码,由msgget返回
cmd:要执行的控制命令
buf:指向msqid_ds结构的指针,用于获取或设置消息队列状态
参数取值说明
cmd取值:
-
IPC_STAT:获取消息队列的状态信息,存入buf指向的结构 -
IPC_SET:设置消息队列的属性,从buf指向的结构读取 -
IPC_RMID:立即删除消息队列,唤醒所有等待进程
返回值
-
成功:返回0
-
失败:返回-1
msqid_ds结构说明
cpp
struct msqid_ds {
struct ipc_perm msg_perm; // 权限结构
time_t msg_stime; // 最后发送时间
time_t msg_rtime; // 最后接收时间
time_t msg_ctime; // 最后修改时间
unsigned long msg_cbytes; // 当前队列中的字节数
msgqnum_t msg_qnum; // 当前队列中的消息数
msglen_t msg_qbytes; // 队列最大字节数
pid_t msg_lspid; // 最后发送进程的PID
pid_t msg_lrpid; // 最后接收进程的PID
};
消息队列结构
路径:/usr/include/linux/msg.h
cpp
struct msqid_ds {
struct ipc_perm msg_perm; /* 消息队列的权限结构 */
struct msg *msg_first; /* 队列中第一个消息,未使用 */
struct msg *msg_last; /* 队列中最后一个消息,未使用 */
__kernel_time_t msg_stime; /* 最后一次msgsnd时间 */
__kernel_time_t msg_rtime; /* 最后一次msgrcv时间 */
__kernel_time_t msg_ctime; /* 最后一次修改时间 */
unsigned long msg_lcbytes; /* 为32位系统复用字段 */
unsigned long msg_lqbytes; /* 同上 */
unsigned short msg_cbytes; /* 当前队列中的字节数 */
unsigned short msg_qnum; /* 队列中的消息数量 */
unsigned short msg_qbytes; /* 队列允许的最大字节数 */
__kernel_ipc_pid_t msg_lspid; /* 最后一次msgsnd的进程ID */
__kernel_ipc_pid_t msg_lrpid; /* 最后一次msgrcv的进程ID */
};
责任链模式与消息队列
概述
消息队列提供了一种从一个进程向另一个进程发送带类型数据块的方法
每个数据块都有一个类型标识,接收进程可以根据不同类型值选择性接收数据
与管道类似,消息队列也存在限制:单个消息的最大长度存在上限(MSGMAX)
每个消息队列的总容量也有上限(MSGMNB),且系统支持的消息队列总数同样存在限制(MSGMNI)
基础版代码实现
MQ.hpp
cpp
#ifndef MessageQueue_Basic_MQ_hpp
#define MessageQueue_Basic_MQ_hpp
#include<iostream>
#include<string>
#include<cstring>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
#include<unistd.h>
//键值获取参数
#define PATHNAME "/home/loukou-ruizi/linux-learning/IPC/systemV/MessageQueue/Basic"
#define PROJ_ID 0x6666
//参数
#define GETMSQUEUE (IPC_CREAT)
#define CREATEMSQUEUE (IPC_CREAT|IPC_EXCL|0666)
//自定义数据类型
#define MSG_TYPE_CLIENT 1
#define MSG_TYPE_SERVER 2
const int default_size = -1;
const int SIZE=1024;
typedef struct
{
long mtype;
char mtext[SIZE];
} msg_t;
class MessageQueue
{
private:
int _msgfd;
public:
MessageQueue():_msgfd(default_size)
{}
//创建消息队列
void create(int flag)
{
//获取键值
key_t key = ftok(PATHNAME,PROJ_ID);
if(key == -1)
{
std::cerr<<"ftok error"<<std::endl;
exit(1);
}
std::cout<<"key:"<<std::hex<<key<<std::endl;
_msgfd = msgget(key,flag);
//IPC_CREAT:不存在则创建
//IPC_EXCL:和IPC_CREAT一起使用,如果消息队列存在则报错
if(_msgfd == -1)
{
std::cerr<<"msgget error"<<std::endl;
exit(2);
}
std::cout<<"message queue:"<<_msgfd<<" create success!"<<std::endl;
}
//删除队列
void destroy()
{
int n=msgctl(_msgfd,IPC_RMID,nullptr);
if(n==-1)
{
std::cerr<<"msgctl error"<<std::endl;
return;
}
std::cout<<"message queue:"<<_msgfd<<" destroy success!"<<std::endl;
}
//发送消息
void Send(long type,const std::string& msg)
{
msg_t message;
memset(&message,0,sizeof(message));
message.mtype=type;
memcpy(message.mtext,msg.c_str(),msg.size());
//长度不可以直接sizeof(message),因为不包括long int 的内容
int n=msgsnd(_msgfd,&message,msg.size(),0);
if(n<0)
{
std::cerr<<"msgsnd error @function Send"<<std::endl;
return ;
}
std::cout<<"Send:"<<msg<<" successful"<<std::endl;
}
//接收消息
void Recv(long type ,std::string*msg)
{
msg_t message;
ssize_t n=msgrcv(_msgfd,&message,SIZE,type,0);
if(n==-1)
{
std::cerr<<"msgrcv error@function Recv"<<std::endl;
}
message.mtext[n]=0;//当作字符串
*msg=message.mtext;
std::cout<<"receive:"<<msg<<std::endl;
}
~MessageQueue()
{}
};
class Server:public MessageQueue
{
public:
Server()
{
MessageQueue::create(CREATEMSQUEUE);
std::cout<<"Server created MS_QUEUE successfully!"<<std::endl;
}
~Server()
{
MessageQueue::destroy();
}
};
class Client:public MessageQueue
{
public:
Client()
{
MessageQueue::create(GETMSQUEUE);
std::cout<<"Client get MS_QUEUE successfully!"<<std::endl;
}
~Client()
{
MessageQueue::destroy();
}
};
#endif /* MessageQueue_Basic_MQ_hpp */
Server.cpp
cpp
#include"MQ.hpp"
int main()
{
std::string text;
Server server;
while(1)
{
server.Recv(MSG_TYPE_CLIENT,&text);
std::cout<<"接受的内容为:"<<text<<std::endl;
if(text=="exit")
{
break;
}
}
return 0;
}
client.cpp
cpp
#include"MQ.hpp"
int main()
{
Client client;
//目前先让客户端发送消息到服务器
while(1)
{
std::string message;
std::cout<<"请输入@:"<<std::endl;
std::getline(std::cin,message);
client.Send(MSG_TYPE_CLIENT,message);
if(message=="exit")
{
break;
}
}
return 0;
}
结果为:

责任链模式与责任链改版的代码案例
如果我们添加新的需求:
-
客户端功能:
-
客户端发送给服务器的输入内容,需要自动拼接时间戳和进程PID信息
-
格式示例:
[时间] [PID] 原始内容
-
-
服务器端功能:
-
服务器接收到的内容需要持久化保存到文件中
-
当文件大小超过预设阈值时,需要进行切片保存
-
切片文件应在指定目录下打包保存(打包命令可自定义)
-
这里我们采用一种新的设计模式:责任链设计模式
它本质上是一种行为设计模式,允许将请求沿着处理者链进行传递。每个处理者对请求进行检查,决定是否处理或传递给链中的下一个处理者。优点:多个对象都有机会处理请求,避免了请求发送者和接收者之间的紧耦合
设计模式如下图所示

代码实现:
MQ.hpp
cpp
#ifndef MessageQueue_Basic_MQ_hpp
#define MessageQueue_Basic_MQ_hpp
#include<iostream>
#include<string>
#include<cstring>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
#include<unistd.h>
//键值获取参数
#define PATHNAME "/home/loukou-ruizi/linux-learning/IPC/systemV/MessageQueue/Basic"
#define PROJ_ID 0x6666
//参数
#define GETMSQUEUE (IPC_CREAT)
#define CREATEMSQUEUE (IPC_CREAT|IPC_EXCL|0666)
//自定义数据类型
#define MSG_TYPE_CLIENT 1
#define MSG_TYPE_SERVER 2
const int default_size = -1;
const int SIZE=1024;
typedef struct
{
long mtype;
char mtext[SIZE];
} msg_t;
class MessageQueue
{
private:
int _msgfd;
public:
MessageQueue():_msgfd(default_size)
{}
//创建消息队列
void create(int flag)
{
//获取键值
key_t key = ftok(PATHNAME,PROJ_ID);
if(key == -1)
{
std::cerr<<"ftok error"<<std::endl;
exit(1);
}
std::cout<<"key:"<<std::hex<<key<<std::endl;
_msgfd = msgget(key,flag);
//IPC_CREAT:不存在则创建
//IPC_EXCL:和IPC_CREAT一起使用,如果消息队列存在则报错
if(_msgfd == -1)
{
std::cerr<<"msgget error"<<std::endl;
exit(2);
}
std::cout<<"message queue:"<<_msgfd<<" create success!"<<std::endl;
}
//删除队列
void destroy()
{
int n=msgctl(_msgfd,IPC_RMID,nullptr);
if(n==-1)
{
std::cerr<<"msgctl error"<<std::endl;
return;
}
std::cout<<"message queue:"<<_msgfd<<" destroy success!"<<std::endl;
}
//获取消息属性
void GetAttr()
{
struct msqid_ds outbuffer;
int n=msgctl(_msgfd,IPC_STAT,&outbuffer);
if(n<0)
{
std::cerr<<"msgctl error"<<std::endl;
return ;
}
std::cout<<"outbuffer.msg_perm.__key"<<std::hex<<outbuffer.msg_perm.__key<<std::endl;
}
//发送消息
void Send(long type,const std::string& msg)
{
msg_t message;
memset(&message,0,sizeof(message));
message.mtype=type;
memcpy(message.mtext,msg.c_str(),msg.size());
//长度不可以直接sizeof(message),因为不包括long int 的内容
int n=msgsnd(_msgfd,&message,msg.size(),0);
if(n<0)
{
std::cerr<<"msgsnd error @function Send"<<std::endl;
return ;
}
std::cout<<"Send:"<<msg<<" successful"<<std::endl;
}
//接收消息
void Recv(long type ,std::string*msg)
{
msg_t message;
ssize_t n=msgrcv(_msgfd,&message,SIZE,type,0);
if(n==-1)
{
std::cerr<<"msgrcv error@function Recv"<<std::endl;
}
message.mtext[n]=0;//当作字符串
*msg=message.mtext;
std::cout<<"receive:"<<msg<<std::endl;
}
~MessageQueue()
{}
};
class Server:public MessageQueue
{
public:
Server()
{
MessageQueue::create(CREATEMSQUEUE);
std::cout<<"Server created MS_QUEUE successfully!"<<std::endl;
}
~Server()
{
MessageQueue::destroy();
}
};
class Client:public MessageQueue
{
public:
Client()
{
MessageQueue::create(GETMSQUEUE);
std::cout<<"Client get MS_QUEUE successfully!"<<std::endl;
}
~Client()
{
MessageQueue::destroy();
}
};
#endif /* MessageQueue_Basic_MQ_hpp */
CORP.hpp
cpp
//Chain of Responsibility Pattern
#ifndef MessageQueue_ChainofResponsibilityPattern
#define MessageQueue_ChainofResponsibilityPattern
#include<iostream>
#include<memory>
#include<string>
#include<sstream>
#include<fstream>
#include<unistd.h>
#include <sys/wait.h> // waitpid 函数声明
#include<sys/types.h>
#include<filesystem>//C++17
#include<system_error>
#include<ctime>
class HandlerText
{
protected://这个指针需要被子类继承但是不希望被外部访问
std::shared_ptr<HandlerText> _next; //下一个节点的指针
bool _enable=true; //是否启动该结点
public:
HandlerText()=default;
virtual void Excute(const std::string&text)=0;
void SetNext(std::shared_ptr<HandlerText> next)
{
_next=next;
}
void Enable()
{
_enable=true;
}
void Disable()
{
_enable=false;
}
virtual ~HandlerText()=default;
};
//对文本格式化
class HandlerTextFormat:public HandlerText
{
public:
void Excute(const std::string&text) override
{
std::string format_result;
if(_enable)
{
//如果被开启了就进行格式化
std::stringstream ss;
ss<<time(nullptr)<<"-"<<getpid()<<"-"<<text<<"\n";
format_result=ss.str();
std::cout<<"格式化消息:"<<text<<"为:"<<format_result<<std::endl;
}
if(_next)
{
_next->Excute(format_result);//将处理结果传递给下一个
}
else
{
std::cout<<"到达责任链处理结尾,处理完成"<<std::endl;
}
}
};
//文件基本信息
std::string PathName="/home/loukou-ruizi/linux-learning/IPC/systemV/MessageQueue/CORP";
std::string FileName="DefaultName.txt";
//对文本保存
class HandlerTextSaveFile:public HandlerText
{
public:
HandlerTextSaveFile(const std::string&pathname=PathName,\
const std::string&filename=FileName)
:_pathname(pathname),_filename(filename)
{
//形成默认的目录名,如果不再就创建(filesystem)
// 使用 std::filesystem 创建目录(如果不存在)
if(std::filesystem::exists(_pathname))
{
return ;
}
try
{
std::filesystem::create_directories(_pathname);
}
catch(std::filesystem::filesystem_error const &e)
{
std::cerr << e.what() << '\n';
}
}
void Excute(const std::string&text) override
{
if(_enable)
{
//如果被开启了就进行文本保存
std::string File=_pathname+_filename;
std::ofstream ofs(File,std::ios::app);
if(!ofs.is_open())
{
std::cerr<<"文件"<<File<<"打开失败"<<std::endl;
return ;
}
ofs<<text;
ofs.close();
std::cout<<"保存消息:"<<text<<"到文件:"<<File<<std::endl;
}
if(_next)
{
_next->Excute(text);//将处理结果传递给下一个
}
else
{
std::cout<<"到达责任链处理结尾,处理完成"<<std::endl;
}
}
private:
std::string _pathname;
std::string _filename;
};
//最大行数
const int defaultmaxline=10;
//文本长度检查:过长就打包备份
class HandlerTextBackUp:public HandlerText
{
public:
HandlerTextBackUp(const std::string&pathname=PathName,\
const std::string&filename=FileName,\
const int&maxline=defaultmaxline)
:_pathname(pathname),_filename(filename),_maxline(maxline)
{
}
void Excute(const std::string&text) override
{
if(_enable)
{
//如果被开启了就进行长度检查:过长就切片打包
std::string File=_pathname+_filename;
std::cout<<"检查文件"<<File<<"是否超范围"<<std::endl;
if(IsOutOfRange(File))
{
//超了范围才切片备份
std::cout<<"目标文件:"<<File<<"超范围了,准备进行切片"<<std::endl;
BackUp(File);
}
}
if(_next)
{
_next->Excute(text);//将处理结果传递给下一个
}
else
{
std::cout<<"到达责任链处理结尾,处理完成"<<std::endl;
}
}
private:
std::string _pathname;
std::string _filename;
int _maxline=defaultmaxline;
bool IsOutOfRange(const std::string&file)
{
std::ifstream ifs(file);
if(!ifs.is_open())
{
std::cerr<<"打开文件失败"<<std::endl;
return false;
}
int lines=0;
std::string line;
while(std::getline(ifs,line))
{
lines++;
}
ifs.close();
return lines>_maxline;
}
void BackUp(const std::string&file)
{
//对文件重命名.Linux上对文件重命名是原子性的
//让子进程来备份
//对备份文件打包
std::string suffix=std::to_string(time(nullptr));
std::string backup_file=file+"."+suffix;
std::string src_file=_filename+"."+suffix;
std::string tar_file=src_file+".tarz";
pid_t id =fork();
if(id==0)
{
//子进程
//重命名
//备份
std::filesystem::rename(file,backup_file);
std::cout<<"备份消息"<<file<<"到"<<backup_file<<std::endl;
//打包为.tarz,调用用exec系统调用
//备份文件打包为.tarz
//更改文件路径
std::filesystem::current_path(_pathname);
//调用指令打包
execlp("tar","tar","-czf",tar_file,src_file,nullptr);
exit(1);//执行失败返回1
}
//父进程
int status;
pid_t rid=waitpid(id,&status,0);
if(rid<=0)
{
std::cerr<<"wait error"<<std::endl;
return;
}
else//等待成功
{
if(WIFEXITED(status)&&WEXITSTATUS(status)==0)
{
//打包成功
std::filesystem::remove(src_file);
std::cout<<"删除备份文件"<<std::endl;
}
}
}
};
//责任链入口类
class HandlerEntry
{
private:
std::shared_ptr<HandlerText> _format;
std::shared_ptr<HandlerText> _savefile;
std::shared_ptr<HandlerText> _backup;
public:
HandlerEntry()
{
_format=std::make_shared<HandlerTextFormat>();
_savefile=std::make_shared<HandlerTextSaveFile>();
_backup=std::make_shared<HandlerTextBackUp>();
//设置责任链
_format->SetNext(_savefile);
_savefile->SetNext(_backup);
}
void Run(const std::string&text)
{
_format->Excute(text);
}
void EnableHandler(bool IsFormat,bool IsSaveFile,bool IsBackUp)
{
IsFormat?_format->Enable():_format->Disable();
IsSaveFile?_savefile->Enable():_savefile->Disable();
IsBackUp?_backup->Enable():_backup->Disable();
}
~HandlerEntry()
{}
};
#endif//Chain of Responsibility Pattern
Server.cpp
cpp
#include"MQ.hpp"
#include"CORP.hpp"
int main()
{
std::string text;
Server server;
HandlerEntry handler;
handler.EnableHandler(true,true,true);
while(1)
{
server.Recv(MSG_TYPE_CLIENT,&text);
std::cout<<"接受的内容为:"<<text<<std::endl;
if(text=="exit")
{
break;
}
//加工处理数据采用责任链设计模式
handler.Run(text);
}
return 0;
}
client.cpp
cpp
#include"MQ.hpp"
int main()
{
Client client;
//目前先让客户端发送消息到服务器
while(1)
{
std::string message;
std::cout<<"请输入@:"<<std::endl;
std::getline(std::cin,message);
client.Send(MSG_TYPE_CLIENT,message);
if(message=="exit")
{
break;
}
}
return 0;
}
结果为:

SystemV信号量
信号量主要⽤于同步和互斥
并发编程概念铺垫
共享资源与临界资源
-
多个执行流(进程/线程)能够看到的同一份公共资源称为共享资源
-
被保护起来的共享资源叫做临界资源(或互斥资源)
保护机制
-
保护的方式常见有两种:互斥 与同步
-
互斥:任何时刻,只允许一个执行流访问资源
-
同步:多个执行流访问临界资源时,具有一定的顺序性
临界区概念
-
系统中某些资源一次只允许一个进程使用,这样的资源称为临界资源 或互斥资源
-
在进程中涉及到互斥资源的程序段叫做临界区
-
进程代码 = 访问临界资源的代码(临界区)+ 不访问临界资源的代码(非临界区)
保护的本质
- 对共享资源进行保护,本质上是对访问共享资源的代码进行保护

并发中的原子性
原子性是指一个操作(或一系列操作)要么完全执行,要么完全不执行,不会出现执行到中间状态被中断的情况。在进程间通信(IPC)中,原子性至关重要,因为它确保了多个进程对共享资源或通信渠道的访问不会产生冲突或数据不一致。
原子性的核心特征
- 不可分割性
-
操作要么全部完成,要么完全未执行
-
不会出现"部分完成"的状态
- 不可中断性
-
在执行期间不会被其他进程或线程中断
-
对于多核处理器,需要硬件级别的支持
- 可见性
-
操作完成后,结果对所有进程立即可见
-
无中间状态的暴露
IPC中需要原子性的场景
- 消息队列操作
cpp
// 非原子性可能导致的问题:
// 进程A: 正在发送一个大消息(分为多个步骤)
// 进程B: 在A发送过程中接收,可能得到损坏的消息
// 原子性的 msgsnd 确保:
// 1. 整个消息一次性进入队列
// 2. 接收者看到的是完整消息或没有消息
msgsnd(msqid, &msg, sizeof(msg.mtext), 0); // 这个调用是原子的
- 共享内存访问
cpp
// 典型问题:读-改-写(read-modify-write)操作
int counter; // 在共享内存中
// 进程A: // 进程B:
// 读取 counter(100) // 读取 counter(100)
// counter++ (101) // counter++ (101)
// 写回 counter(101) // 写回 counter(101)
// 结果应该是102,但实际是101!
// 解决方案:使用原子操作或信号量
__sync_fetch_and_add(&counter, 1); // GCC原子操作
- 信号量操作
cpp
// 信号量操作本身就是原子的
struct sembuf sop = {
.sem_num = 0,
.sem_op = -1, // P操作,原子地减1
.sem_flg = 0
};
semop(semid, &sop, 1); // 这个操作是原子的
- 管道读写
cpp
// POSIX保证:当写入数据量 ≤ PIPE_BUF(通常是4096字节)时
// write() 操作是原子的
write(pipefd, buffer, 100); // ≤ PIPE_BUF,原子操作
write(pipefd, buffer, 5000); // > PIPE_BUF,非原子,可能被分割
信号量
信号量本质上是一个计数器,描述临界资源的资源数量。申请信号量就是申请对资源的预定权。
如果要申请资源就要先申请信号量,如果申请成功就可以访问资源,申请失败就堵塞。
每个进程都要申请信号量,也就是说每个进程都需要看到信号量,因此信号量本身也是共享资源。那么谁来保证信号量的安全性呢?所以OS设定信号量的申请释放都是原子性的。
如果信号量初始值为1 ,就是二元信号量。
特性方面
-
IPC资源必须手动删除,系统不会自动清除(除非重启)
-
System V IPC资源的生命周期随内核持续存在
理解方面
- 信号量本质上是一个计数器
作用方面
- 主要用于保护临界区,实现进程/线程间的互斥与同步
本质方面
- 信号量本质上是对资源的预订机制
操作方面
-
申请资源(P操作):计数器减1(wait/sem_wait操作)
-
释放资源(V操作):计数器加1(signal/sem_post操作)
如果资源是非整体利用,信号量该怎么处理呢?首先申请信号量,然后通过算法保证一个进程只有会访问自己预定的资源

那么我们怎么保证进程看到的是同一个信号量?
信号量的系统接口调用
semget函数
功能:用来创建或获取一个信号量集
cpp
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
参数
key:信号量集的名字/键值。用于标识内核中信号量集的唯一性
nsems:信号量集中信号量的数量
semflg:由几个权限标志构成,用法和创建文件时使用的mode模式标志类似
参数取值说明
-
取值为
IPC_CREAT:信号量集不存在则创建并返回;信号量集已存在则获取并返回 -
取值为
IPC_EXCL:不能单独使用 -
取值为
IPC_CREAT | IPC_EXCL:信号量集不存在则创建并返回;信号量集已存在则出错返回
返回值
-
成功:返回一个非负整数,即该信号量集的标识码(
semid) -
失败:返回-1
semop函数
功能:对信号量集中的一个或多个信号量进行操作(P/V操作)
cpp
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, size_t nsops);
参数
semid:信号量集标识码,由semget返回
sops:指向操作数组的指针,每个元素表示一个信号量操作
nsops:操作数组中的操作数量
参数取值说明
sem_op取值:
-
> 0:将信号量的值增加sem_op(相当于V操作) -
< 0:如果信号量的当前值 ≥ |sem_op|,则减去|sem_op|,否则阻塞(相当于P操作) -
= 0:如果信号量的当前值不为0,则阻塞,直到为0
sem_flg取值:
-
IPC_NOWAIT:如果操作不能立即执行,则函数立即返回错误 -
SEM_UNDO:进程退出时,操作系统自动撤销该操作
返回值
-
成功:返回0
-
失败:返回-1
sembuf结构说明
cpp
struct sembuf {
unsigned short sem_num; // 信号量在集合中的索引
short sem_op; // 操作值(正数、负数、0)
short sem_flg; // 操作标志
};
semctl函数
功能:对信号量集进行各种控制操作
cpp
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, ... /* union semun arg */);
参数
semid:信号量集标识码,由semget返回
semnum:信号量在集合中的索引(用于针对单个信号量的操作)
cmd:要执行的控制命令
arg:可选参数,取决于cmd
参数取值说明
cmd取值:
-
IPC_STAT:获取信号量集的状态信息,存入arg.buf指向的结构 -
IPC_SET:设置信号量集的属性,从arg.buf指向的结构读取 -
IPC_RMID:立即删除信号量集,唤醒所有等待进程 -
GETVAL:返回信号量集中第semnum个信号量的当前值 -
SETVAL:将信号量集中第semnum个信号量的值设置为arg.val -
GETALL:获取信号量集中所有信号量的值,存入arg.array -
SETALL:将信号量集中所有信号量的值设置为arg.array中的值
返回值
-
成功:根据cmd不同,返回不同的值(如GETVAL返回信号量值)
-
失败:返回-1
semun联合体说明
cpp
union semun {
int val; // SETVAL使用的值
struct semid_ds *buf; // IPC_STAT, IPC_SET使用的缓冲区
unsigned short *array; // GETALL, SETALL使用的数组
struct seminfo *__buf; // IPC_INFO使用的缓冲区(Linux特有)
};
那么这个时候我们就该思考:为什么systemV系列的接口都如此相似呢?
因为IPC资源=内核数据结构+资源本身(内存块、队列、计数器)
信号量和P、V原语
信号量和 P、V 原语由 Dijkstra (也是图最短路径算法的提出人之一)提出。主要用于执行流(进程/线程)间的互斥与同步控制,表示为S。含义如下:
-
S > 0 :S 表示可用资源的个数
-
S = 0 :表示无可用资源,且无等待进程
-
S < 0 :|S|(S的绝对值)表示等待队列中进程的个数
其对应的结构体代码类似于这样
cpp
//信号量本质上是⼀个计数器
struct semaphore
{
int value;
pointer_PCB queue;
};
P原语
cpp
P(s)
{
s.value--;
if (s.value < 0) {
// 将该进程状态置为等待状态
// 将该进程的PCB插入相应的等待队列s.queue末尾
}
}
V原语
cpp
V(s)
{
s.value++;
if (s.value <= 0) {
// 唤醒相应等待队列s.queue中等待的一个进程
// 改变其状态为就绪态
// 并将其插入OS就绪队列
}
}
信号量结构体
cpp
struct semid_ds {
struct ipc_perm sem_perm; /* 所有权和权限 */
time_t sem_otime; /* 最后一次semop操作的时间 */
time_t sem_ctime; /* 最后一次修改的时间 */
unsigned long sem_nsems; /* 集合中信号量的数量 */
};
/*
* ipc_perm结构体定义如下(高亮字段可通过IPC_SET设置):
*/
struct ipc_perm {
key_t __key; /* 提供给semget(2)的键值 */
uid_t uid; /* 所有者的有效用户ID */
gid_t gid; /* 所有者的有效组ID */
uid_t cuid; /* 创建者的有效用户ID */
gid_t cgid; /* 创建者的有效组ID */
unsigned short mode; /* 权限模式 */
unsigned short __seq; /* 序列号 */
};
基于建造者模式的信号量
建造者模式
建造者模式 是一种创建型设计模式,用于分步构造复杂对象。它将对象的构造过程与其表示分离,允许相同的构造过程创建不同的表示形式。
可以参考这张图:

设计要解决的问题
当对象的构造过程:
-
包含多个步骤或参数
-
需要不同的配置组合
-
构造逻辑复杂且易变
-
需要保持构造过程的灵活性
关键优势
- 构造过程封装
-
隐藏复杂对象的创建细节
-
保持产品类的简洁性
- 构造流程控制
-
指导者类可以定义标准构造流程
-
确保构造步骤的正确顺序
- 产品表示分离
cpp
// 同一构造过程,不同表示
ComputerBuilder* builder;
builder = new GamingComputerBuilder(); // 游戏配置
Computer* gamingPC = builder->getResult();
builder = new OfficeComputerBuilder(); // 办公配置
Computer* officePC = builder->getResult();
- 参数灵活性
-
避免构造函数参数过长(反模式:"望远镜构造函数")
-
支持可选参数和默认值
标准建造者模式
cpp
// 1. 产品类(最终要构建的复杂对象)
class Computer {
private:
string CPU;
string Memory;
string Storage;
// ... 其他组件
};
// 2. 抽象建造者接口
class ComputerBuilder {
public:
virtual void buildCPU() = 0;
virtual void buildMemory() = 0;
virtual void buildStorage() = 0;
virtual Computer* getResult() = 0;
};
// 3. 具体建造者
class GamingComputerBuilder : public ComputerBuilder {
// 实现游戏电脑的特定构造逻辑
};
// 4. 指导者(可选)
class Engineer {
void constructComputer(ComputerBuilder* builder) {
builder->buildCPU();
builder->buildMemory();
builder->buildStorage();
}
};
代码实现
Sem.hpp
cpp
#pragma once
#include<iostream>
#include<memory>
#include<string>
#include<unistd.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
#define GET_SEM (IPC_CREAT)
#define BUILD_SEM (IPC_CREAT|IPC_EXCL|0666)
const int PROJ_ID =0x66;
const std::string PATHNAME="/home/loukou-ruizi/linux-learning/IPC/systemV/semaphore";
const int defaultname=1;
//整数转化为16进制
std::string IntHex(int num)
{
char hex[64];
snprintf(hex,sizeof(hex),"0x%x",num);
return std::string(hex);
}
//负责交出去使用
class Semaphore
{
public:
Semaphore(int semid)
:_semid(semid)
{}
int ID()const
{
return _semid;
}
void P()
{
PV(-1);
}
void V()
{
PV(1);
}
~Semaphore()
{
if(_semid>=0)
{
int n=semctl(_semid,0,IPC_RMID);
if(n<0)
{
std::cout<<"析构失败"<<std::endl;
}
}
}
private:
int _semid;
int _flag;
void PV(int data)
{
struct sembuf sem_b;
sem_b.sem_flg=SEM_UNDO;//不关系返回值
sem_b.sem_num=0;
sem_b.sem_op=data;
int n=semop(_semid,&sem_b,1);
if(n<0)
{
std::cerr<<"PV调用失败"<<std::endl;
return ;
}
}
// struct sembuf
// {
// unsigned short sem_num; /* 信号量编号 */
// short sem_op; /* 信号量操作值 */
// short sem_flg; /* 操作标志 */
// };
};
//建造者模式
//负责创建
class SemaphoreBuilder
{
public:
SemaphoreBuilder()
{
}
~SemaphoreBuilder()
{
}
SemaphoreBuilder&SetVar(int val)
{
_val=val;
return *this;
}
//构建
std::shared_ptr<Semaphore> build(int flag,int num=defaultname)
{//flag为创建权限,num为信号量集的数量
std::cout<<"构造信号量"<<std::endl;
key_t key=ftok(PATHNAME.c_str(),PROJ_ID);
if(key<0)
{
std::cerr<<"创建失败"<<std::endl;
return nullptr;
}
std::cout<<"获取键值:"<<IntHex(key)<<std::endl;
int semid=semget(key,num,flag);
if(semid<0)
{
std::cerr<<"创建信号量失败"<<std::endl;
return nullptr;
}
std::cout<<"获取semid为:"<<semid<<std::endl;
if(flag==BUILD_SEM)
Init(semid,0,_val);
return std::make_shared<Semaphore>(semid);
}
private:
int _val; // 所有信号量的初始值
//对特定信号量集合的一个值的初始化
bool Init(int semid,int num,int val)
{ //semid为信号量的值
//num为信号量下标
union semun
{
int val; /* 用于SETVAL的值 */
struct semid_ds *buf; /* 用于IPC_STAT、IPC_SET的缓冲区 */
unsigned short *array; /* 用于GETALL、SETALL的数组 */
struct seminfo *__buf; /* 用于IPC_INFO的缓冲区(Linux特有) */
} un;
un.val=val;
int n=semctl(semid ,num,SETVAL,un);
if(n<0)
{
std::cerr<<"初始化失败"<<std::endl;
return false;
}
return true;
}
};
Writter.cpp
cpp
#include"Sem.hpp"
#include<ctime>
int main()
{
SemaphoreBuilder SB;//仅仅是构造对象名
auto sem=SB.SetVar(1).build(BUILD_SEM,1);
srand(time(nullptr)^getpid());
pid_t pid=fork();
//我们希望打印的内容成对出现。保证为原子的
if(pid==0)
{
auto csem=SB.build(GET_SEM);
while(1)
{
csem->P();
std::cout<<"我喜欢你"<<std::endl;
usleep(rand()%5000);
csem->V();
}
}
while(1)
{
sem->P();
std::cout<<"诗华"<<std::endl;
usleep(rand()%4000);
sem->V();
}
return 0;
}
本期内容就到这里了,喜欢的话请点个赞谢谢。
封面图自取:
