目录
一、消息队列
1、概述
消息队列提供了一个从一个进程向另外一个进程发送有类型块数据的方法。每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值。消息队列也有管道一样的不足,就是每个消息的最大长度是有上限的。每个消息队列的总的字节数也是有上限的,系统上消息队列的总数也有上限。
2、通信形式

上图展示的就是Linux内核中消息队列的通信机制。在用户态有两个进程,进程1和进程2分别关注不同类型的消息;内核态维护着一个消息队列,进程发送的消息按类型和内容依次排入队列。进程1只从队列中取走消息类型1的消息,进程2只取走消息类型2的消息,箭头代表的就是这种按类型筛选接收的过程。整个机制体现了消息队列的异步通信和类型过滤能力,发送方与接收方无需直接交互,消息由内核中转并按类型分发。
3、IPC数据结构
在/usr/include/linux/ipc.h中,内核为每个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 */
};
以上代码是Linux内核中用于描述IPC对象权限的结构体ipc_perm,常见于消息队列、共享内存、信号量等System V IPC机制中。它定义了某个IPC资源的所有者、创建者以及访问权限等元信息。其中_key是该IPC对象对外使用的键值,用于不同进程找到同一个资源;uid和gid分别表示当前所有者的有效用户ID和组ID;cuid和cgid表示创建者的用户ID和组ID;mode是权限位,决定哪些用户或组可以读、写或执行相关操作;_seq是序列号,内核用它来复用IPC标识符时避免冲突。
4、消息队列数据结构
在/usr/include/linux/msg.h中,描述消息队列信息的结构体如下所示:
cpp
struct msqid_ds {
struct ipc_perm msg_perm;
struct msg msg_first; / first message on queue,unused */
struct msg msg_last; / last message in queue,unused */
__kernel_time_t msg_stime; /* last msgsnd time */
__kernel_time_t msg_rtime; /* last msgrcv time */
__kernel_time_t msg_ctime; /* last change time */
unsigned long msg_lcbytes; /* Reuse junk fields for 32 bit */
unsigned long msg_lqbytes; /* ditto */
unsigned short msg_cbytes; /* current number of bytes on queue */
unsigned short msg_qnum; /* number of messages in queue */
unsigned short msg_qbytes; /* max number of bytes on queue */
__kernel_ipc_pid_t msg_lspid; /* pid of last msgsnd */
__kernel_ipc_pid_t msg_lrpid; /* last receive pid */
};
以上代码是Linux内核中描述消息队列实例的结构体msqid_ds,它对应一个具体的消息队列对象,记录了队列的权限、状态、统计信息和时间戳。msg_perm负责权限和所有者信息;msg_first和msg_last是队列头尾指针;msg_stime、msg_rtime、msg_ctime分别记录最后一次发送、接收和修改的时间;msg_cbytes表示当前队列中已占用的字节数,msg_qnum是当前消息条数,msg_qbytes是队列允许的最大字节数;msg_lspid和msg_lrpid记录最后一次发送和接收消息的进程ID。
5、内核表示
下图展示了Linux内核中System V消息队列的完整数据结构关系:

右侧msg_queue是队列的管理结构,包含权限、时间戳、当前字节数、消息条数、最大字节数以及等待发送和接收的进程。中间msg_msg是消息节点,通过双向链表挂在q_message上,每个节点记录消息类型和大小,并指向真正的消息正文。下方的msg_send和msg_rcv分别代表发送和接收进程,通过指针与消息正文关联。
6、接口
(1)msgget

msgget用于System V消息队列的创建或获取系统调用。msgget的功能是根据一个键值key,在内核中查找已有的消息队列,如果找不到就创建一个新的,并返回该队列的标识符msqid。
参数:
key:某个消息队列的名字
msgflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的
返回值:
成功返回一个非负整数,即该消息队列的标识码;失败返回-1。
(2)msgctl

msgctl是System V消息队列的控制系统调用。它通过msqid定位到具体的消息队列,然后根据cmd参数执行不同的管理操作。
参数:
msgid:由msgget函数返回的消息队列标识码。
cmd:将要采取的动作,有三个可取值,如下图所示:

buf:属性缓冲区。
返回值:
成功返回0,失败返回-1。
(3)msgsnd

msgsnd是System V消息队列的发送消息系统调用。它把一条消息写入指定的消息队列,如果队列已满或权限不足则可能阻塞或立即返回错误。
参数
msgid:由msgget函数返回的消息队列标识码
msgp:是一个指针,指针指向准备发送的消息
msgsz:是msgp指向的消息长度,这个长度不含保存消息类型的那个long int长整型
msgflg:控制着当前消息队列满或到达系统上限时将要发生的事情。
返回值
成功返回0,失败返回-1。
消息主体:
cpp
struct msgbuf {
long mtype; /* message type, must be > 0 */
char mtext[1]; /* message data */
};
消息主体以一个long int长整型开始,接受者函数将利用这个确定消息的类型。
(4)msgrcv

msgrcv是System V消息队列的接收消息系统调用,它从指定队列中取走一条消息,并可以按消息类型进行筛选。
参数
msgid:由msgget函数返回的消息队列标识码
msgp:是一个指针,指针指向准备接收的消息
msgsz:是msgp指向的消息长度,这个长度不含保存消息类型的那个long int长整型
msgtyp:它可以实现接收消息的类型,也可以模拟优先级的简单形式进行接收
msgtyp为0时返回队列中第一条消息;大于0时返回第一条类型等于msgtyp的消息;小于0时返回第一条类型小于等于msgtyp绝对值、且满足条件中类型最小的那条消息。
msgflg:控制着队列中没有相应类型的消息可供接收时将要发生的事。IPC_NOWAIT表示队列无可读消息时不等待,直接返回ENOMSG错误;MSG_NOERROR表示消息正文超过msgsz时自动截断而不报错。此外,当msgtyp大于0且msgflg为MSG_EXCEPT时,接收的是类型不等于msgtyp的第一条消息,相当于对指定类型做排除。
返回值
成功返回实际放到接收缓冲区里的字符个数,失败返回-1。
7、通信实现
msgqueue.hpp
cpp
#pragma once
#include <iostream>
#include <cstring>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#define SIZE 1024
#define PATHNAME "/tmp"
#define PROJID 0x4321
#define CREATE_NEW_MSGQUEUE (IPC_CREAT | IPC_EXCL | 0666)
#define GET_MSGQUEUE (IPC_CREAT)
typedef struct
{
long mtype;
char mtext[SIZE];
} msg_t;
class MsgQueueBase
{
public:
MsgQueueBase()
{
}
bool BuildMsgQueue(int flg)
{
_key = ::ftok(PATHNAME, PROJID);
if (_key < 0)
exit(1);
_msgid = ::msgget(_key, flg);
if (_msgid < 0)
exit(2);
return true;
}
bool SendMessage(const std::string &in, long type)
{
msg_t msg;
msg.mtype = type;
memset(msg.mtext, 0, sizeof(msg.mtext));
strncpy(msg.mtext, in.c_str(), in.size());
int n = ::msgsnd(_msgid, &msg, in.size(), 0);
if (n < 0)
return false;
return true;
}
bool RecvMessage(std::string *out, long type)
{
msg_t msg;
int n = ::msgrcv(_msgid, &msg, SIZE, type, 0);
if (n < 0)
return false;
msg.mtext[n] = 0;
*out = msg.mtext;
return true;
}
bool DeleteMsgQueue()
{
int n = ::msgctl(_msgid, IPC_RMID, nullptr);
return n == 0;
}
~MsgQueueBase()
{
}
protected:
key_t _key;
int _msgid;
};
class MsgQueueClient : public MsgQueueBase
{
public:
MsgQueueClient()
{
bool res = MsgQueueBase::BuildMsgQueue(GET_MSGQUEUE);
(void)res;
}
};
class MsgQueueServer : public MsgQueueBase
{
public:
MsgQueueServer()
{
bool res = MsgQueueBase::BuildMsgQueue(CREATE_NEW_MSGQUEUE);
(void)res;
}
~MsgQueueServer()
{
bool res = MsgQueueBase::DeleteMsgQueue();
(void)res;
}
};
#define SERVER 1
#define CLIENT 2
以上代码封装了一个基于System V消息队列的C++简易通信类。其中,MsgQueueBase负责公共逻辑,ftok用于生成键值、msgget用于创建或获取队列、msgsnd发送、msgrcv接收、msgctl删除。MsgQueueClient和MsgQueueServer分别继承它,客户端用IPC_CREAT获取已有队列,服务端用IPC_CREAT | IPC_EXCL | 0666创建新队列,服务端析构时删除队列。
cpp
#include "msgqueue.hpp"
int main()
{
std::string msg;
MsgQueueServer mq;
mq.RecvMessage(&msg, CLIENT);
std::cout << "get message: " << msg << std::endl;
return 0;
}
cpp
#include "msgqueue.hpp"
int main()
{
std::string msg = "hello msgqueue";
MsgQueueClient mq;
mq.SendMessage(msg, CLIENT);
return 0;
}
Makefile
cpp
.PHONY:all
all:client server
client:client.cc msgqueue.hpp
g++ -o $@ client.cc -std=c++11
server:server.cc msgqueue.hpp
g++ -o $@ server.cc -std=c++11
.PHONY:clean
clean:
rm -f client server
执行make,编译生成client、server可执行文件,./server,服务端先启动,进行阻塞等待

在另一个终端启动客户端,./client,客户端发送消息后服务端将打印,结果如下图所示:


如上图所示,服务端成功打印消息:get message:hello msgqueue。至此,客户端和服务端二者通过消息队列成功实现通信。
需要注意的是:消息队列的生命周期是随内核的,消息队列支持全双工通信。
二、责任链模式
1、概述
新需求:
client发送给server的输入内容,拼接上时间,进程pid信息
server收到的内容持久化保存到文件中
文件的内容如果过大,要进行切片保存并在指定的目录下打包保存,命令自定义
要解决这个需求,就得用到责任链模式:

责任链模式是一种行为设计模式,它允许请求沿着处理者链进行传递。每个处理者都对请求进行检查,以决定是否处理它。如果处理者能够处理该请求,就处理它;否则,它将请求传递给链中的下一个处理者。这个模式使得多个对象都有机会处理请求,从而避免了请求的发送者和接收者之间的紧耦合,后续新增或调整处理环节只需改动链的组装,不用动客户端和已有处理器。
2、实现
cpp
#pragma once
#include<iostream>
#include<filesystem>
#include<memory>
#include<unistd.h>
#include<sstream>
#include<fstream>
#include<ctime>
#include<sys/types.h>
#include<sys/wait.h>
class HandlerText
{
public:
HandlerText() : _enable(true)
{
}
virtual ~HandlerText() = default;
void SetNextHandler(std::shared_ptr<HandlerText> handler)
{
_next_handler = handler;
}
void Enable() { _enable = true; }
void DisEnable() { _enable = false; }
bool IsEnable() { return _enable; }
virtual void Execute(std::string &info) = 0;
protected: // 这⾥要protected,⽅便继承
std::shared_ptr<HandlerText> _next_handler;
bool _enable;
};
class HandlerTextFormat : public HandlerText
{
public:
~HandlerTextFormat()
{
}
void Execute(std::string &info) override
{
if (HandlerText::IsEnable())
{
// 开始处理,添加简单的补充信息
std::cout << "Format ..." << std::endl;
// 简单处理
std::stringstream ss;
ss << time(nullptr) << " - " << getpid() << " - " << info << "\n";
info = ss.str();
sleep(1);
}
if (_next_handler) // 如果_next_handler被设置,就交给下⼀个继续加⼯处理
_next_handler->Execute(info);
else
std::cout << "责任链节点结束,处理完成" << std::endl;
}
};
std::string defaultpath = "./tmp/";
std::string defaultfilename = "test.log";
class HandlerTextSaveFile : public HandlerText
{
public:
HandlerTextSaveFile() : _filepath(defaultpath), _filename(defaultfilename)
{
if (std::filesystem::exists(_filepath))
return;
try
{
std::filesystem::create_directories(_filepath);
}
catch (std::filesystem::filesystem_error &e)
{
std::cerr << e.what() << std::endl;
}
}
~HandlerTextSaveFile()
{
}
void Execute(std::string &info) override
{
if (HandlerText::IsEnable())
{
// 开始处理,保存到指定的⽂件中
std::cout << "Save ..." << std::endl;
sleep(1);
const std::string file = _filepath + _filename;
std::ofstream out(file, std::ios::app);
if (!out.is_open())
return;
out << info;
out.close();
}
if (_next_handler) // 如果_next_handler被设置,就交给下⼀个继续加⼯处理
_next_handler->Execute(info);
else
std::cout << "责任链节点结束,处理完成" << std::endl;
}
private:
std::string _filepath;
std::string _filename;
};
const int maxline = 5; // 为了尽快触发备份动作,该值设置⼩⼀些
class HandlerTextBackupFile : public HandlerText
{
public:
HandlerTextBackupFile() : _max_line_number(maxline),
_filepath(defaultpath), _filename(defaultfilename)
{
}
~HandlerTextBackupFile()
{
}
void Execute(std::string &info) override
{
if (HandlerText::IsEnable())
{
// 开始处理,对⽂件进⾏增量备份
std::cout << "Backup ..." << std::endl;
sleep(1);
const std::string filename = _filepath + _filename;
// 1. 打开⽂件
std::ifstream in(filename);
if (!in.is_open())
return;
std::string line;
int currentlines = 0;
while (std::getline(in, line))
{
currentlines++;
}
// 关闭⽂件流
in.close();
// 2. 备份
if (currentlines > _max_line_number)
{
std::cout << "消息⾏数超过" << _max_line_number << ", 触发⽇志备
份" << std::endl;
// ⼤于才做备份,否则什么⾛不做
Backup();
}
}
if (_next_handler) // 如果_next_handler被设置,就交给下⼀个继续加⼯处理
_next_handler->Execute(info);
else
std::cout << "责任链节点结束,处理完成" << std::endl;
}
void Backup()
{
std::string newname = _filename + "." + std::to_string(time(nullptr));
pid_t id = fork();
if (id == 0)
{
chdir(_filepath.c_str()); // 更改进程路径,进⼊"./tmp/"路径下
std::filesystem::rename(_filename, newname); // rename⽐较快,也不影
响未来其他继续写⼊的操作,因为会重新形成⽂件
std::string tarname = newname + ".tgz";
// ⼦进程打包备份
std::cout << "打包 : " << newname << " 成为: " << tarname << "开始"
<< std::endl;
execlp("tar", "tar", "czf", tarname.c_str(), newname.c_str(),
nullptr); // 注意这⾥要以nullptr结尾,注意这⾥的坑
std::cout << "打包 : " << newname << " 成为: " << tarname << "失败"
<< std::endl;
exit(1);
}
waitpid(id, nullptr, 0);
std::string tempfile = _filepath + newname;
std::filesystem::remove(tempfile); // 删除⽂件原件,只要tar包
}
private:
int _max_line_number;
std::string _filepath;
std::string _filename;
};
class HandlerEntry
{
public:
HandlerEntry()
{
// 构建责任链节点对象
_format = std::make_shared<HandlerTextFormat>();
_save = std::make_shared<HandlerTextSaveFile>();
_backup = std::make_shared<HandlerTextBackupFile>();
// 链接责任链
_format->SetNextHandler(_save);
_save->SetNextHandler(_backup);
}
void EnableHandler(bool isformat, bool issave, bool isbackup)
{
isformat ? _format->Enable() : _format->DisEnable();
issave ? _save->Enable() : _save->DisEnable();
isbackup ? _backup->Enable() : _backup->DisEnable();
}
void Run(std::string &info)
{
_format->Execute(info);
}
private:
std::shared_ptr<HandlerText> _format;
std::shared_ptr<HandlerText> _save;
std::shared_ptr<HandlerText> _backup;
};
以上代码采用责任链模式实现了一个文本处理流水线。HandlerText是抽象基类,持有后继节点和启用开关;三个具体处理器分别负责格式化文本、追加写入日志文件、按行数阈值触发备份。HandlerTextFormat给文本加上时间戳和进程号,HandlerTextSaveFile把结果写到./tmp/test.log,HandlerTextBackupFile检查行数超限后fork子进程用tar打包备份并删除原文件。HandlerEntry负责把三个节点按顺序组装成链,并提供统一开关和启动入口。
整体思路是请求从链头进入,依次经过各节点处理,每个节点只关心自己的逻辑,处理完交给下一个,实现了解耦和可插拔。
三、结语
本文从System V消息队列的底层机制出发,从核心接口和内核数据结构切入,最后通过一个责任链模式的文本处理流水线作为实践。消息队列解决的是进程间怎么传的问题,责任链解决的是消费端怎么处理的问题。前者让消息异步流动,后者让处理逻辑可插拔、可编排。把两者结合,消费端就不再是一个巨大的if-else方法,而是一条清晰的处理链:消息从队列取出后,依次经过格式化、落盘、备份等节点,每个节点只关心自己的逻辑,新增或调整环节只需改动链的组装。用消息队列解决进程间通信的解耦,用责任链解决消费端处理逻辑的解耦,两者结合,就能实现让异步任务从能收发走向可编排。