进程间通信详解(二):System V IPC 三件套全面解析
文章目录
- 引言:
- 一、System V 共享内存:
- 1.1 核心概念
- 1.2 共享内存示意图
- 1.3 关键系统调用
- 1.4 典型使用流程
- 1.5 实例:通过共享内存实现进程间通信
- 1.5.1 Shm.hpp
- 1.5.2 Fifo.hpp
- 1.5.3 server.cc
- 1.5.4 client.cc
- 1.5.5 演示
- 1.6 值得注意的地方
- 二、System V 消息队列:
- 2.1 核心特性与应用模型
- 2.2 关键系统调用
- 2.3 典型应用示例:任务分发系统
- 2.4 管理与调优
- 三、System V 信号量:
- 3.1 核心概念:信号量集
- 3.2 关键系统调用
- 3.3 互斥锁实现示例
- 3.4 使用中的陷阱
- 四、三种 System V IPC 机制对比
- 五、总结
引言:
在进程间通信(IPC)机制中,System V 提供了一套功能强大的通信方式:共享内存(Shared Memory)、消息队列(Message Queue)和信号量(Semaphore),统称为 System V IPC。它们广泛应用于多进程程序的通信与同步,是学习操作系统中 IPC 内容的重要组成部分。本文将从原理、接口函数、代码示例到适用场景,为你全面讲解这三种通信机制,并通过比较帮助你选择合适的方案。
一、System V 共享内存:
1.1 核心概念
System V 共享内存 是 Unix/Linux 系统中一种经典的进程间通信(IPC)机制。它允许多个无关的进程将同一块物理内存区域映射到它们各自的地址空间中,从而实现高效的数据共享。
核心概念与工作原理:
- 共享内存段(Shared Memory Segment):
- 内核管理的一块特殊内存区域。
- 不隶属于任何单个进程,独立于进程存在。
- 可以被多个进程同时访问。
- 标识符(Identifier):
- Key (
key_t
):一个用户指定的、系统范围的唯一键值(通常使用ftok()
函数基于路径名和项目ID生成),用于在创建或获取共享内存段时标识它。 - 共享内存ID (shmid):内核在成功创建或获取共享内存段后返回的唯一标识符。进程后续操作(附加、分离、控制)都使用这个
shmid
。
- Key (
- 映射(Attach):
- 进程通过系统调用
shmat()
(Shared Memory Attach) 将共享内存段“附加”到自己的地址空间。 shmat()
返回一个指向共享内存段在进程地址空间中起始地址的指针 (shmaddr
,通常设为NULL
让系统自动选择地址)。- 之后,进程就可以像访问普通内存一样(通过这个指针)读写共享内存段中的数据。
- 进程通过系统调用
- 分离(Detach):
- 当进程不再需要访问共享内存段时,通过
shmdt()
(Shared Memory Detach) 系统调用将其从自己的地址空间分离。 - 分离操作不会删除共享内存段本身,只是断开进程与该段的连接。
- 当进程不再需要访问共享内存段时,通过
- 控制(Control):
- 使用
shmctl()
(Shared Memory Control) 系统调用对共享内存段进行各种操作:IPC_RMID
:标记共享内存段为“待删除”。当最后一个附加到它的进程分离后,内核会真正删除该段及其数据结构。IPC_STAT
:获取共享内存段的元信息(如大小、权限、创建者等),存储在struct shmid_ds
结构中。IPC_SET
:修改共享内存段的某些元信息(如权限shm_perm
)。
- 创建共享内存段 (
shmget()
) 时也会隐式地进行控制操作。
- 使用
1.2 共享内存示意图
1.3 关键系统调用
shmget()
- 获取共享内存段标识符int shmget(key_t key, size_t size, int shmflg);
- 功能:
- 如果
key
对应的共享内存段不存在,且shmflg
包含了IPC_CREAT
,则创建一个新的共享内存段。 - 如果
key
对应的共享内存段已存在,则获取其标识符。
- 如果
- 参数:
key
:共享内存段的键值 (或IPC_PRIVATE
创建私有段)。size
:要创建或获取的共享内存段的大小(字节)。获取时通常设为0。shmflg
:标志位,指定创建/获取的权限和行为(如IPC_CREAT | 0666
创建+读写权限)。
- 返回值:成功返回共享内存ID (
shmid
),失败返回-1
。
shmat()
- 附加共享内存段void *shmat(int shmid, const void *shmaddr, int shmflg);
- 功能:将
shmid
指定的共享内存段附加到调用进程的地址空间。 - 参数:
shmid
:shmget()
返回的共享内存ID。shmaddr
:建议的附加地址。通常设为NULL
,让内核选择合适地址。shmflg
:附加标志(如SHM_RDONLY
只读附加)。
- 返回值:成功返回附加后的内存地址指针,失败返回
(void*) -1
。
shmdt()
- 分离共享内存段int shmdt(const void *shmaddr);
- 功能:将附加在地址
shmaddr
(即shmat()
返回值)的共享内存段从调用进程的地址空间分离。 - 参数:
shmaddr
:shmat()
返回的指针。 - 返回值:成功返回
0
,失败返回-1
。
shmctl()
- 控制共享内存段int shmctl(int shmid, int cmd, struct shmid_ds *buf);
- 功能:对
shmid
指定的共享内存段执行控制命令cmd
。 - 参数:
shmid
:共享内存ID。cmd
:控制命令(IPC_RMID
,IPC_STAT
,IPC_SET
)。buf
:指向struct shmid_ds
的指针,用于传入/传出信息(IPC_STAT
/IPC_SET
时使用)。
- 返回值:成功返回
0
(IPC_RMID
)或非负值(IPC_STAT
/IPC_SET
),失败返回-1
。
1.4 典型使用流程
- 创建者/服务器进程:
- 使用
ftok()
生成一个唯一的key
。 - 调用
shmget(key, size, IPC_CREAT | 0666)
创建共享内存段,获得shmid
。 - 调用
shmat(shmid, NULL, 0)
附加共享内存,获得指针shared_mem
。 - 向
shared_mem
写入初始数据。 - (可选)通知其他进程共享内存已就绪(可通过其他IPC机制如信号量、管道、消息队列等实现同步,这是使用共享内存的关键点,因为共享内存本身不提供同步机制)。
- 使用
- 使用者/客户端进程:
- 使用相同的
ftok()
参数生成相同的key
。 - 调用
shmget(key, 0, 0)
获取已存在的共享内存段的shmid
。(注意size=0
) - 调用
shmat(shmid, NULL, 0)
附加共享内存,获得指针shared_mem
。 - 从
shared_mem
读取数据或写入数据(必须与创建者/其他使用者协商好数据结构和同步机制!)。 - 使用完毕后调用
shmdt(shared_mem)
分离。
- 使用相同的
- 销毁:
- 通常由创建者或最后一个使用者负责销毁(但需协调)。
- 调用
shmctl(shmid, IPC_RMID, NULL)
标记共享内存段为删除。 - 内核会在最后一个附加进程分离后实际删除该段。
1.5 实例:通过共享内存实现进程间通信
1.5.1 Shm.hpp
#pragma once#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include "Comm.hpp"
#include <sys/stat.h>const int gdefaultid = -1;
const int gsize = 4096;
const std::string pathname = "/home/zkp/linux/25/6/10/shm/";
const int projid = 0x66;
const int gmode = 0666;
#define CREATER "creater"
#define USER "user"class Shm
{
private:// 创建的一定要是一个全新的共享内存void CreateHelper(int flg){umask(0); // 把权限掩码设置为0,防止影响下面操作printf("key: 0x%x\n", _key);// 共享内存的生命周期,随内核_shmid = shmget(_key, _size, flg); // 核心创建函数if(_shmid < 0){ERR_EXIT("shmget");}printf("shmid: %d\n", _shmid);}void Create(){CreateHelper(IPC_CREAT | IPC_EXCL | gmode);}void Attach(){_start_mem = shmat(_shmid, nullptr, 0); // 映射到进程空间if((long long)_start_mem < 0){ERR_EXIT("shmat");}printf("attch sucess\n");}void Detach(){int n = shmdt(_start_mem); // 解除映射if(n == 0){printf("detach sucess\n");}}void Get(){CreateHelper(IPC_CREAT);}void Destroy(){// if(_shmid == gdefaultid)// return;Detach(); // 先接触映射if(_usertype == CREATER){int n = shmctl(_shmid, IPC_RMID, nullptr); // 删除共享内存if(n == 0){printf("shmctl delete shm: %d success!\n", _shmid);}else{ERR_EXIT("shmctl");}}}public:Shm(const std::string &pathname, int projid, const std::string &usertype): _shmid(gdefaultid),_size(gsize),_start_mem(nullptr),_usertype(usertype){_key = ftok(pathname.c_str(), projid); // 生成唯一键值if(_key < 0){ERR_EXIT("ftok");}if(_usertype == CREATER)Create(); // 创建者:创建新的共享else if(_usertype == USER)Get(); // 使用者:获取现有共享内存else{}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); // ds: 输出型参数printf("shm_segsz: %ld\n", ds.shm_segsz);printf("key: 0x%x\n", ds.shm_perm.__key);}~Shm(){std::cout << _usertype << std::endl;if(_usertype == CREATER)Destroy();}private:int _shmid; // 共享内存 idkey_t _key; // 通过 ftok() 生成的键int _size; // 共享内存大小(默认4096)void *_start_mem; // 映射后的起始地址std::string _usertype; // 用户类型
};
1.5.2 Fifo.hpp
#pragma once#include <iostream>
#include <cstdio>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>#define PATH "."
#define FILENAME "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){ERR_EXIT("mkfifo");}else{std::cout << "mkfifo sucess" << std::endl;}}~NameFifo(){// 删除管道文件int n = unlink(_fifoname.c_str());if(n == 0){ERR_EXIT("unlink");}else{std::cout << "remove fifo failed" << std::endl;}}
private:std::string _path;std::string _name;std::string _fifoname;
};class FileOper
{
public:FileOper(const std::string &path, const std::string &name): _path(path), _name(name), _fd(-1){_fifoname = _path + "/" + _name;}void OpenForRead(){// 打卡,write 方没有执行open的时候,read方,就要在open内部进行阻塞// 直到有人把管道文件打开了,open才会返回_fd = open(_fifoname.c_str(), O_RDONLY);if(_fd < 0){ERR_EXIT("open");}std::cout << "open fifo sucess" << std::endl;}void OpenForWrite(){// write_fd = open(_fifoname.c_str(), O_WRONLY);if(_fd < 0){ERR_EXIT("open");}std::cout << "open fifo sucess" << std::endl;}void Wakeup(){// 写入操作std::string message;int cnt = 1;pid_t pid = getpid();while(true){std::cout << "Plase Enter# ";std::getline(std::cin, message);message += (", message number: " + std::to_string(cnt++) + ", [" + std::to_string(pid) + "]");write(_fd, message.c_str(), message.size());}}void Read(){// 正常的readwhile(true){char buffer[1024];int number = read(_fd, buffer, sizeof(buffer) - 1);if(number > 0){buffer[number] = 0;std::cout << "Client Say# " << buffer << std::endl;}else if(number == 0){std::cout << "client quit! me too!" << std::endl;break;}else{std::cerr << "read error" << std::endl;break;}}}void Close(){if (_fd > 0)close(_fd);}int GetFd() const{return _fd;}~FileOper(){}private:std::string _path;std::string _name;std::string _fifoname;int _fd;
};
1.5.3 server.cc
#include "Shm.hpp"
#include "Fifo.hpp"int main()
{// 1. 创建FIFO(确保文件存在)NameFifo fifo(PATH, FILENAME);// 2. 创建共享内存Shm shm(pathname, projid, CREATER);char* mem = (char*)shm.VirtualAddr();// 3. 打开FIFO读端FileOper reader(PATH, FILENAME);reader.OpenForRead();std::cout << "Server ready. Waiting for client..." << std::endl;// 4. 事件循环while(true){// 读客户端通知reader.Read();// 处理共享内存std::cout << "Received data: " << mem << std::endl;// 退出条件if(strcmp(mem, "exit") == 0) break;}return 0;
}
1.5.4 client.cc
#include "Shm.hpp"
#include "Fifo.hpp"void AutoNotify(FileOper& writer, const char* msg)
{write(writer.GetFd(), msg, strlen(msg));
}int main()
{// 1. 打开FIFO写入端FileOper writerfile(pathname, FILENAME);writerfile.OpenForWrite();// 2. 获取共享内存Shm shm(pathname, projid, USER);char *mem = (char*)shm.VirtualAddr();// 3. 自动写入数据和通知const char* messages[] = {"Hello", "World", "IPC", "exit"};for(int i = 0; i < 4; ++i){strcpy(mem, messages[i]); // 安全写入// 自动通知服务端AutoNotify(writerfile, messages[i]);sleep(1);}// 4. 清理资源writerfile.Close();return 0;
}
1.5.5 演示
启动 server
:
看一下共享内存的状态:
会发现它阻塞在这里,此时我们启动 client
:
client
在传输完数据后退出。
server
在 client
传输数据之后开始读取数据:
因为读取到了 exit
(我设计退出的方式),所以退出并清理资源。
此时再看看共享内存的信息:
1.6 值得注意的地方
有的同学在边写代码边调试的时候,运行了一次程序,后面却发现怎么也创建不了共享内存了,这是因为我们创建的共享内存的生命周期是随内核的,需要我们手动去删除:
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x660228c6 29 zkp 146 4096 0 zkp@zkp:~/linux/25/6/10/shm$ ipcrm -m 29
zkp@zkp:~/linux/25/6/10/shm$ ipcs -m------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
二、System V 消息队列:
2.1 核心特性与应用模型
消息队列是进程间异步通信的 “信箱”,每个消息包含类型字段和数据载荷。接收方可以根据类型选择性获取消息(如只接收类型 2 的消息),支持多生产者 - 多消费者模式。典型应用场景包括:
- 任务调度系统中的异步任务分发
- 微服务架构中的轻量级消息传递
- 日志系统的异步日志收集
2.2 关键系统调用
msgget
:创建 / 获取消息队列int msgget(key_t key, int msgflg);
- 参数:
key
:通过ftok(path, id)
生成的唯一键值(如ftok("/dev/null", 'A')
)msgflg
:IPC_CREAT
:不存在则创建IPC_EXCL
:与IPC_CREAT
组合时检查队列是否存在- 权限掩码(如0660)
- 返回值:队列 ID(
msqid
),失败返回-1
msgsnd
:发送消息int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
- 消息结构体必须以
long mtype
开头:struct msgbuf {long mtype; // 消息类型(正整数)char mtext[1024]; // 数据载荷 };
- 参数:
msgflg
:0
:阻塞直到消息入队IPC_NOWAIT
:队列满时立即返回错误
msgsz
:是mtext
的实际长度(含终止符)
msgrcv
:接收消息ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long mtype, int msgflg);
mtype
过滤规则:0
:接收任意类型(按 FIFO 顺序)>0
:接收指定类型的第一条消息<0
:接收类型小于等于绝对值的最小类型消息
msgflg
:0
:阻塞直到有符合条件的消息IPC_NOWAIT
:无消息时立即返回-1
MSG_NOERROR
:截断超长消息
msgctl
:队列控制int msgctl(int msqid, int cmd, struct msqid_ds *buf);
- 常用命令:
IPC_STAT
:获取队列状态(存于buf
)IPC_SET
:设置队列属性(从buf
读取)IPC_RMID
:标记队列待删除(立即生效,资源在最后一次引用时释放)
2.3 典型应用示例:任务分发系统
其实就是一个生产者消费者模型,这里就不仔细介绍了,感兴趣的自己先去了解,后面其他的文章中再讲。
// 生产者进程:发送任务消息
struct task_msg {long mtype; // 任务类型(1=计算任务,2=IO任务)int task_id; // 任务IDchar data[512]; // 任务数据
};int main() {key_t key = ftok("taskq", 'T');int msqid = msgget(key, 0660 | IPC_CREAT);struct task_msg msg = {.mtype=1, .task_id=1001};strcpy(msg.data, "complex calculation");if (msgsnd(msqid, &msg, sizeof(msg)-sizeof(long), 0) < 0) {perror("msgsnd failed");}return 0;
}// 消费者进程:处理计算任务
int main() {key_t key = ftok("taskq", 'T');int msqid = msgget(key, 0660);struct task_msg msg;while(1) {msgrcv(msqid, &msg, sizeof(msg)-sizeof(long), 1, 0); // 只接收类型1process_task(msg.task_id, msg.data);}return 0;
}
2.4 管理与调优
- 查看队列:
ipcs -q
(显示msqid
、权限、消息数等) - 删除队列:
ipcrm -q <msqid>
或程序中msgctl(msqid, IPC_RMID, NULL)
- 系统限制(通过
/proc/sys/kernel
配置):msgmax
:单个消息最大字节数(默认 8192)msgmnb
:单个队列最大字节数(默认 16384)msgmni
:系统最大队列数(默认 16)
三、System V 信号量:
3.1 核心概念:信号量集
信号量本质是一个整数计数器,用于控制对共享资源的访问。System V 实现支持信号量集(多个信号量的集合),主要用于:
- 互斥锁:确保资源一次仅被一个进程访问(二元信号量)
- 资源池:控制并发访问的进程数量(计数信号量)
- 进程同步:协调进程执行顺序(如子进程完成初始化后通知父进程)
3.2 关键系统调用
semget
:创建 / 获取信号量集int semget(key_t key, int nsems, int semflg);
nsems
:信号量集中的信号量数量(如 1 表示单个信号量)semflg
:与msgget
类似,支持IPC_CREAT/IPC_EXCL
及权限设置- 返回值:信号量集 ID(
semid
),失败返回-1
semop
:执行信号量操作(PV 操作)int semop(int semid, struct sembuf *ops, unsigned short nops);
sembuf
结构体定义操作细节:struct sembuf {unsigned short sem_num; // 信号量在集合中的索引(0开始)short sem_op; // 操作值:// +n:释放n个资源(V操作)// -n:申请n个资源(P操作,阻塞直到可用)// 0:等待信号量为0short sem_flg; // 标志位:// IPC_NOWAIT:非阻塞操作// SEM_UNDO:进程异常终止时自动恢复信号量 };
- 示例:单个信号量的 P 操作(申请资源)
struct sembuf p_op = {0, -1, 0}; // 操作第一个信号量,减1,阻塞 semop(semid, &p_op, 1);
semctl
:控制信号量集int semctl(int semid, int semnum, int cmd, ...);
- 常用命令:
SETVAL
:初始化信号量值(需union semun
参数)
union semun {int val; // 用于SETVALstruct semid_ds *buf; // 用于IPC_STAT/IPC_SETunsigned short *array; // 用于GETALL/SETALL }; semctl(semid, 0, SETVAL, (union semun){.val=1}); // 初始化第一个信号量为1(互斥锁)
GETVAL
:获取信号量当前值IPC_RMID
:删除信号量集
3.3 互斥锁实现示例
// 初始化信号量为1(互斥锁)
int init_semaphore(key_t key) {int semid = semget(key, 1, 0660 | IPC_CREAT | IPC_EXCL);union semun arg = {.val=1};semctl(semid, 0, SETVAL, arg);return semid;
}// P操作(加锁)
void sem_lock(int semid) {struct sembuf op = {0, -1, 0};semop(semid, &op, 1);
}// V操作(解锁)
void sem_unlock(int semid) {struct sembuf op = {0, +1, 0};semop(semid, &op, 1);
}// 共享资源访问示例
int shared_data = 0;int main() {key_t key = ftok("semtest", 'S');int semid = init_semaphore(key);if (fork() == 0) { // 子进程sem_lock(semid);shared_data += 100;sem_unlock(semid);} else { // 父进程sem_lock(semid);shared_data += 200;sem_unlock(semid);wait(NULL);printf("Shared data: %d\n", shared_data); // 输出300}semctl(semid, 0, IPC_RMID); // 删除信号量集return 0;
}
3.4 使用中的陷阱
SEM_UNDO
标志:始终为semop
设置该标志,避免进程崩溃导致信号量未释放(僵尸锁)- 原子操作:
semop
支持一次操作多个信号量,确保操作原子性(如同时获取多个资源) - 管理命令:
ipcs -s
:查看信号量集ipcrm -s <semid>
:删除信号量集
- 性能注意:信号量本身是轻量级同步工具,但过度使用会导致频繁系统调用
四、三种 System V IPC 机制对比
特性 | 共享内存 | 消息队列 | 信号量 |
---|---|---|---|
通信方式 | 共享存储区 | 带类型消息 | 计数器同步 |
数据拷贝 | 无(内核空间共享) | 有(用户 <-> 内核) | 无(仅状态控制) |
同步支持 | 需额外同步机制 | 异步通信 | 内置同步原语 |
类型支持 | 无 | 消息类型过滤 | 无 |
生命周期 | 显式删除 / 系统重启 | 显式删除 / 系统重启 | 显式删除 / 系统重启 |
典型场景 | 大数据量高频交互 | 异步消息传递 | 资源互斥 / 同步 |
核心 API | shmget/shmat/shmdt | msgget/msgsnd/msgrcv | semget/semop/semctl |
系统限制配置 | shmmax/shmall | msgmax/msgmnb | semmax/semmni |
五、总结
System V IPC 三剑客各有所长:
- 共享内存适合高性能数据交互,但需手动处理同步
- 消息队列提供类型化异步通信,适合解耦的生产者 - 消费者模型
- 信号量专注于进程同步与资源控制,是构建复杂同步逻辑的基石
实际应用中,三者常结合使用(如共享内存 + 信号量实现高效安全的共享数据访问)。使用时需注意:
- 始终检查 API 调用错误(IPC 机制易受资源限制影响)
- 显式释放资源(避免系统重启前的内存 / 队列泄漏)
- 根据场景选择合适的 IPC 组合(而非单一机制)
通过合理运用这三种机制,可以构建稳定高效的进程间通信架构,满足不同复杂度的分布式系统需求。