目录
[一、System V IPC 概述](#一、System V IPC 概述)
[1. 为什么需要 System V](#1. 为什么需要 System V)
[2. 三大机制](#2. 三大机制)
[1. 什么是共享内存](#1. 什么是共享内存)
[2. 通信模型](#2. 通信模型)
[1. shmget](#1. shmget)
[2. shmat](#2. shmat)
[3. shmdt](#3. shmdt)
[4. shmctl](#4. shmctl)
[1. 通信代码](#1. 通信代码)
[2. System V IPC 管理工具](#2. System V IPC 管理工具)
[1. 地址空间映射](#1. 地址空间映射)
[1. kern_ipc_perm](#1. kern_ipc_perm)
[2. shmid_ds 与 shmid_kernel](#2. shmid_ds 与 shmid_kernel)
[3. msg_queue 与 sem_array](#3. msg_queue 与 sem_array)
[1. IPC 资源数组](#1. IPC 资源数组)
[2. 内核管理方式](#2. 内核管理方式)
一、System V IPC 概述
在 Linux 系统演进的过程中,除了传统的管道机制外,为了满足更复杂、更高性能的跨进程协作需求,引入了 System V IPC 标准。该标准最早源于 AT&T 的 SVR4,它定义了一套独立于文件系统的进程间通信规范
1. 为什么需要 System V
尽管管道和命名管道解决了基础的通信问题,但在多进程协作的复杂场景下则存在显著的局限性:
-
生命周期差异: 匿名管道的生命周期随进程结束而销毁。而在某些业务场景中,通信资源需要随内核持久化,即即使创建资源的进程退出,资源依然保留在内核中,直到被显式删除或系统关闭
-
效率极致追求: 管道涉及多次数据拷贝(用户空间 -> 内核空间 -> 用户空间)。在处理海量数据交换时,需要一种能让进程直接访问同一物理内存的机制
-
非亲缘进程连接: 虽有命名管道支持非亲缘进程,但 System V 提供了一套基于 Key(键值) 的全局索引机制,使得在庞大的系统中,不相关的进程可以非常方便地定位到同一个通信资源
-
同步需求: 简单的读写阻塞已无法满足多进程对共享资源的竞态控制,需要更高级的同步原语来实现更精细的控制
2. 三大机制
System V IPC 体系由三大核心组件构成,它们共享相似的系统调用接口、权限控制逻辑以及内核管理方式
(1) 共享内存 (Shared Memory)
共享内存是 System V IPC 中效率最高的一种机制。它允许两个或多个进程映射同一块物理内存区域。由于数据不需要在进程和内核之间进行显式拷贝,它极大地降低了通信延迟
(2) 消息队列 (Message Queues)
消息队列是一个由内核维护的消息链表。与管道的字节流不同,消息队列是面向消息块的。每个消息可以具有特定的类型,接收进程可以根据类型有选择地读取消息,从而实现更灵活的异步通信
(3) 信号量 (Semaphores)
信号量本质上是一个内核计数器,主要用于进程间的同步与互斥。它并不用于传输业务数据,而是作为一种机制来协调多个进程对共享资源的访问,防止产生竞争条件
在 System V 体系中,每一个 IPC 资源在内核中都有一个唯一的标识符 。为了让不同的进程能找到同一个标识符,通常使用一个被称为 Key 的数值作为中介
二、共享内存
在 System V IPC 的三大机制中,共享内存被公认为通信效率最高的方式。它打破了进程间严格的资源隔离,通过内核协调实现物理内存的直接共享
1. 什么是共享内存
共享内存是指在物理内存中开辟一块特定的缓冲区,并允许两个或多个进程将其映射到各自的虚拟地址空间中
(1) 映射机制
在 Linux 的虚拟存储管理中,每个进程都拥有独立的页表。共享内存的本质是:内核分配一段物理内存后,多个进程通过修改各自的页表,将这段相同的物理内存地址映射到各自进程地址空间的共享区

(2) 管道通信效率对比
管道与共享内存最本质的区别在于数据拷贝的次数
-
管道通信: 数据传输过程涉及两次拷贝
-
发送进程通过 write 系统调用,将数据从用户态缓冲区拷贝到内核态的管道缓冲区
-
接收进程通过 read 系统调用,将数据从内核态缓冲区拷贝到用户态缓冲区
-
-
共享内存: 数据传输过程中不涉及内核拷贝 。 一旦物理内存映射完成,进程对该区域的操作与访问进程自身的局部变量或全局变量无异 。进程只需通过指针解引用即可直接读写该内存块。这种零拷贝的特性使得共享内存在大数据量交换场景下具有压倒性的性能优势
2. 通信模型
由于共享内存不再依赖内核作为数据的中转站,其通信模型从生产-消费 的流式模型转变为内存共享的并发模型
(1) 生命周期逻辑
-
创建/获取: 一个进程请求内核创建一块指定大小的共享内存段
-
挂接: 各个进程将该内存段映射到自己的虚拟地址空间,获取一个有效的起始指针
-
访问: 进程通过指针直接读写内存,逻辑上等同于操作本地内存
-
去挂接: 进程不再使用该内存时,解除映射关系
-
销毁: 显式释放内核中的共享内存段
(2) 同步控制
由于内核不提供对共享内存读写的阻塞控制,共享内存本身是非同步的。如果多个进程同时写、或者一边读一边写,会产生竞态条件。因此,在实际应用中,共享内存通常必须配合信号量等来确保操作的原子性与数据一致性
三、系统调用
操作 System V 共享内存涉及一组特定的系统调用,用于完成从资源的创建、关联到销毁的全生命周期管理
1. shmget
用来创建共享内存
cpp
原型:
int shmget(key_t key, size_t size, int shmflg);
参数:
key: 共享内存段的名字
size: 共享内存的大小(单位为字节)。通常为 4096 字节的整数倍
shmflg: 权限标志
取值为 IPC_CREAT:如果内核中不存在则创建并返回;如果已存在,则获取并返回
取值为 IPC_CREAT | IPC_EXCL:如果不存在则创建并返回;如果已存在,则出错返回
返回值: 成功返回一个非负整数,即该共享内存段的标识码;失败返回 -1
2. shmat
将共享内存段挂载到进程地址空间
cpp
原型:
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数:
shmid: 共享内存标识码
shmaddr:指定连接的地址
如果为 NULL,内核会自动选择一个合适的地址(推荐做法)
如果不为 NULL 且 shmflg 未指定 SHM_RND,则连接到 shmaddr 指定的地址
shmflg: 核心标志位。
SHM_RDONLY:以只读方式连接
通常设为 0,表示以读写方式连接。
返回值: 成功返回一个指针,指向共享内存在进程地址空间的起始地址;失败返回 (void *) -1
3. shmdt
将共享内存段与当前进程脱离
cpp
原型:
int shmdt(const void *shmaddr);
参数:
shmaddr: 由 shmat 返回的起始指针
返回值: 成功返回 0;失败返回 -1
注意: 该操作仅是将当前进程与共享内存的映射关系断开,并不会删除内核中的共享内存
4. shmctl
cpp
原型:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数:
shmid: 共享内存标识码
cmd: 将要采取的操作
IPC_STAT:获取共享内存的当前关联值(存入 buf)
IPC_SET:在进程有权限的情况下,改变共享内存的当前关联值
IPC_RMID:删除共享内存段
buf: 指向一个 shmid_ds 结构体,用于获取或设置共享内存的属性。如果仅删除资源,该参数通常设为 NULL
返回值: 成功返回 0;失败返回 -1
四、使用示例
为了让共享内存的操作更符合现代编程习惯,我们首先编写一个封装类,随后实现典型的服务端与客户端通信模型
1. 通信代码
(1) 封装层:shm.hpp
该文件主要负责 ftok 键值生成、共享内存的创建、挂接与自动释放逻辑
cpp
#ifndef SHM_HPP
#define SHM_HPP
#include <iostream>
#include <string>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include <cstring>
const std::string PATH_NAME = "/tmp"; // 用于生成 key 的路径
const int PROJ_ID = 0x66; // 用于生成 key 的项目 ID
const int SHM_SIZE = 4096; // 共享内存大小
class SharedMemory
{
public:
SharedMemory(bool isServer) : _isServer(isServer)
{
// 1. 获取唯一 key
_key = ftok(PATH_NAME.c_str(), PROJ_ID);
if (_key < 0)
{
perror("ftok");
exit(1);
}
// 2. 创建/获取共享内存
int flags = _isServer ? (IPC_CREAT | IPC_EXCL | 0666) : IPC_CREAT;
_shmid = shmget(_key, SHM_SIZE, flags);
if (_shmid == -1)
{
perror("shmget");
exit(2);
}
}
// 挂接共享内存
void* attach()
{
_addr = shmat(_shmid, nullptr, 0);
if (_addr == (void*)-1)
{
perror("shmat");
return nullptr;
}
return _addr;
}
// 去挂接
void detach()
{
if (_addr)
{
shmdt(_addr);
_addr = nullptr;
}
}
// 销毁共享内存(仅服务端执行)
void destroy()
{
if (_isServer)
{
shmctl(_shmid, IPC_RMID, nullptr);
std::cout << "Shared memory destroyed." << std::endl;
}
}
~SharedMemory()
{
detach();
if (_isServer) destroy();
}
private:
key_t _key;
int _shmid;
bool _isServer;
void* _addr = nullptr;
};
#endif
(2) 服务端:server.cc
服务端负责创建资源,并循环读取共享内存中的数据
cpp
#include "shm.hpp"
int main()
{
SharedMemory shm(true); // 以服务端身份创建
char* ptr = (char*)shm.attach();
std::cout << "Server is ready" << std::endl;
// 轮询读取逻辑
while (true)
{
if (strlen(ptr) > 0)
{
std::cout << "Client says: " << ptr << std::endl;
if (strcmp(ptr, "quit") == 0) break;
memset(ptr, 0, SHM_SIZE); // 清空以便下次读取
}
sleep(1);
}
return 0;
}
(3) 客户端:client.cc
客户端负责获取已存在的资源,并向内存写入数据
cpp
#include "shm.hpp"
int main()
{
SharedMemory shm(false); // 以客户端身份获取
char* ptr = (char*)shm.attach();
std::cout << "Client is online. Enter message: " << std::endl;
while (true)
{
std::string msg;
std::getline(std::cin, msg);
memcpy(ptr, msg.c_str(), msg.size());
if (msg == "quit") break;
}
return 0;
}
代码逻辑关键点
-
权限掩码:在 shmget 时指定的 0666 必不可少。由于共享内存在内核中是独立对象,如果没有正确设置文件权限标志,非创建者进程将无法挂接
-
ftok 一致性:PATH_NAME 和 PROJ_ID 在服务端和客户端必须完全一致,否则生成的 key 不同,将无法定位到同一块物理内存
-
同步缺失 :在这个简单的示例中,你会发现如果客户端发送太快,服务端可能会漏掉数据,或者读到不完整的数据。这正是前文提到的------共享内存不自带同步机制,在生产环境下必须搭配信号量使用
2. System V IPC 管理工具
**ipcs -m:**查看系统当前所有共享内存段的状态
常用选项:
-
- m:仅列出共享内存
-
- q:仅列出消息队列
-
- s:仅列出信号量
-
- o:列出所有 IPC 资源

输出字段说明:
-
key: 用户层通过 ftok 生成的键值
-
shmid: 内核层用于唯一标识该资源的标识码(删除时需使用此 ID)
-
owner: 创建该资源的用户名
-
perms: 权限掩码
-
bytes: 共享内存段的大小
-
nattch: 当前连接到该段的进程数量
**ipcrm -m [shmid]:**手动从内核中删除指定的共享内存段
用法示例:
bash
ipcrm -m 12345
关键说明:
-
资源释放: 只有当 nattch 为 0 且执行了删除指令时,内核才会真正释放该物理内存
-
删除策略: 如果一个共享内存段已被标记为已删除,但仍有进程连接到它,该资源会进入一种预定删除状态,不再允许新进程挂接,直到最后一个进程断开连接后才彻底销毁
五、底层原理
共享内存之所以能实现"零拷贝"级的高速通信,其核心在于内核对虚拟地址空间 与物理内存映射关系的巧妙管理
1. 地址空间映射
在 Linux 系统中,进程无法直接操作物理内存,而是通过页表将虚拟地址转换为物理地址。共享内存的实现逻辑如下:
(1) 虚拟地址空间布局
在每个进程的 task_struct 维护的虚拟地址空间中,有一块位于堆与栈之间 的区域,称为内存映射区。当进程调用 shmat 时,内核就在该区域内为共享内存段分配一段连续的虚拟地址
(2) 页表级共享
内核在物理内存中开辟一块空间。随后,内核会修改参与通信的各个进程的页表
-
进程 A 的某个虚拟页指向物理页 P
-
进程 B 的另一个虚拟页也指向物理页 P
(3) 写时拷贝的例外
与 ftok 产生的写时拷贝机制不同,共享内存映射建立后,任何一个进程对该区域的修改都会直接作用于物理内存。由于物理内存是同一块,其他映射了该区域的进程通过自己的虚拟地址进行解引用时,能立即看到数据的变化
六、内核结构
在 Linux 2.6.18.8 内核源码中,System V IPC 资源并不是松散存在的,而是通过一系列紧密关联的结构体进行描述和权限控制
1. kern_ipc_perm
定义位置:/include/linux/ipc.h
这是所有 System V IPC 资源(共享内存、消息队列、信号量)共同拥有的身份标识。它存储了权限信息、所有者 UID/GID 以及关键的 Key 值

2. shmid_ds 与 shmid_kernel
-
**定义位置:/**include/linux/shm.h
-
shmid_ds (用户可见): 这是通过 shmctl(IPC_STAT) 可以获取到的结构体。它描述了共享内存的状态

-
shmid_kernel (内核私有): 这是内核真正用来管理共享内存的对象。它包含了 shmid_ds,并额外增加了指向页表、文件对象等内核级指针

3. msg_queue 与 sem_array
虽然本篇重点在于共享内存,但在源码体系中,这三者是对称存在的:
-
msg_queue(/ include/linux/msg.h):维护消息队列的链表及等待队列

-
sem_array(/ include/linux/sem.h):维护信号量集合的数组

七、IPC组织
在内核中,System V IPC 的三套机制(共享内存、消息队列、信号量)虽然功能各异,但它们在管理逻辑上是高度统一的
1. IPC 资源数组
内核并不是直接通过一个庞大的列表管理所有 IPC,而是为每一类 IPC(SHM, MSG, SEM)分别维护一个 struct ipc_ids 结构
-
ipc_id_arr ( entries 数组 ): 在 ipc_ids 结构体内部,维护着一个指向 struct kern_ipc_perm 的指针数组

-
统一索引: 当你获得一个 shmid、msgid 或 semid 时,内核会通过该 ID 计算出数组下标(Index),然后直接从该数组中通过 ipc_id_arr[index] 取出对应的指针
2. 内核管理方式
我们观察到 shmid_kernel、msg_queue 和 sem_array 这三个内核结构体的第一个字段 都是
struct kern_ipc_perm,这并非巧合,而是为了实现指针多态
(1) 结构体内存布局
在 C 语言中,结构体的地址与其第一个成员的地址在数值上是完全相等的
cpp
// 以共享内存为例
struct shmid_kernel {
struct kern_ipc_perm shm_perm; // 必须是第一个字段
size_t shm_segsz;
// ... 其他字段
};
(2) 为什么要这么做?
这种设计允许内核编写一套通用的管理函数,而不需要为三种 IPC 分别写三套逻辑
-
向上转型: 无论是一个 shmid_kernel 的指针,还是 msg_queue 的指针,都可以被强制转换为 struct kern_ipc_perm*
-
通用接口: 内核的管理数组 ipc_id_arr 只需要存储 struct kern_ipc_perm* 类型的指针
-
逻辑复用: 当内核需要检查权限时,它只需要访问ipc_id_arr[i]->mode 或ipc_id_arr[i]->key。由于 kern_ipc_perm 就在结构体的头部,内核不需要关心这个指针后面跟着的是共享内存的数据还是消息队列的数据
我们可以通过以下路径理解其组织结构:
-
**管理层:**struct ipc_ids 维护一个全局指针数组
-
索引层: 数组中的每个元素都是 struct kern_ipc_perm *
-
实体层: 该指针实际上指向的是一个完整的对象:
-
在共享内存模块中,它指向 shmid_kernel
-
在消息队列模块中,它指向 msg_queue
-
在信号量模块中,它指向 sem_array
-

这种 "偏移量为 0" 的封装方式,是 Linux 内核在没有 C++ 继承语法的情况下,实现面向对象管理最高效的手段
八、总结
综上所述,从管道到 System V IPC,我们逐步认识了进程间通信的多种实现方式:管道以简单直接见长,共享内存通过地址空间映射实现高效的数据共享,消息队列提供结构化的数据传递,而信号量则负责在并发环境下维持同步与互斥。这些机制从不同角度解决了进程如何交换数据与协同工作的问题
但可以注意到,上述通信方式大多聚焦于数据传输与资源共享 ,而在实际系统中,进程之间还需要另一类能力------快速通知与异步响应 。例如,一个进程需要立即告知另一个进程某个事件已经发生,或者在不打断主流程的情况下触发某种处理逻辑
这正是信号(Signal)机制要解决的问题
在下一篇中,我们将从信号的基本概念入手,深入理解 Linux 是如何通过这种轻量级的异步机制,实现进程之间的事件通知与控制流干预的
