目录
[2.2如何生成唯一的 key? ------ ftok 函数](#2.2如何生成唯一的 key? —— ftok 函数)
[2.3key 与 shmid 的区别](#2.3key 与 shmid 的区别)
[3.1创建 / 获取共享内存:shmget](#3.1创建 / 获取共享内存:shmget)
[3.4控制 / 删除共享内存:shmctl](#3.4控制 / 删除共享内存:shmctl)
[4.C++ 面向对象实战:从基础到完善](#4.C++ 面向对象实战:从基础到完善)
[4.3最终源码:挂接、通信与 RAII 自动清理](#4.3最终源码:挂接、通信与 RAII 自动清理)
前言
在探索 Linux 进程间通信(IPC)的旅程中,我们必然会遇到一座绕不过去的大山------System V 共享内存(Shared Memory)。
很多资料都会告诉你:"共享内存区是最快的 IPC 形式 "。但它是如何做到"最快"的?一旦这样的内存映射到共享它的进程的地址空间,这些进程间的数据传递为什么不再涉及到内核?换句话说,进程为什么不再通过执行进入内核的系统调用来传递彼此的数据?
本文将结合底层原理图,带你由浅入深地剖析 System V 共享内存的运作机制。
1.核心原理:共享内存究竟是什么?
要理解共享内存,我们首先要打破一个常规认知:进程之间是相互隔离的。那么,如何让两个隔离的进程(进程A和进程B)进行通信?
答案:让他们看到同一块物理内存。
1.1内存映射的魔法
根据操作系统原理,每个进程都有自己独立的虚拟地址空间(mm_struct)。在Linux的进程地址空间发布中,有一块特定的区域被称为共享区(内存映射段) ,它刚好位于栈区(向下增长)和堆区(向上增长)之间。

共享内存的创建和使用过程如下(结合地址空间分布思考):
-
申请物理内存:操作系统在真实的物理内存中开辟一块空间。
-
挂接(Attach) :将这块物理内存分别映射到进程 A 和进程 B 的共享区(即栈与堆之间的区域)。此时,进程 A 和进程 B 各自的页表会将这部分虚拟地址指向同一块物理地址。
-
去关联(Detach):通信完毕后,取消虚拟地址与物理内存的映射关系(即修改各自的页表)。
-
释放内存:最后由操作系统回收这块物理内存。
突破点理解:为什么它是最快的? 传统的管道或消息队列通信,需要调用 read / write 等系统调用 ,数据需要在用户态和内核态之间来回拷贝。 而共享内存一旦挂接(Attach)成功,这块内存就直接属于进程的用户空间 !用户程序可以通过指针直接访问 这块内存,就像使用malloc()申请的内存一样,完全不需要任何系统调用来辅助读写。这就是它速度最快的根本原因!
1.2操作系统如何管理共享内存?
既然是物理内存,系统中可能会同时存在多块共享内存供不同的进程组使用。操作系统必然要对这些共享内存进行管理。 管理的核心理念是:先描述,再组织 。 在 Linux 内核中,每块共享内存都有一个对应的数据结构 struct shmid_ds 来描述它的属性:
cpp
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 * /
};
2.两个进程如何找到同一块共享内存?
这是初学者最容易卡壳的地方。既然系统里有很多共享内存,进程 A 创建了一块共享内存,进程 B 怎么知道哪一块是进程 A 创建的呢?共享内存一定需要一个全局唯一的标识符!
2.1约定的暗号:key
为了让 A 和 B 找到同一块内存,程序员需要为它们设定一个"约定"的值,这个值就是 key。
-
key是一个在操作系统内核中使用的唯一标识符。 -
进程 A 用这个
key去内核里申请创建一个共享内存。 -
进程 B 用同一个
key去内核里查找,就能准确获取到进程 A 创建的那块共享内存。
2.2如何生成唯一的 key? ------ ftok 函数
我们不能随便写一个数字作为 key(容易冲突),Linux 提供了 ftok 函数,利用文件系统的信息来通过算法生成一个相对唯一的 key。
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
-
pathname:工程路径名(随便写一个存在的路径,A和B必须写一样的)。
-
proj_id:项目 ID(随便写一个数字,A和B必须写一样的)。
-
底层算法 :
ftok实际上是提取了pathname对应文件的 inode number(索引节点号) 和proj_id组合计算出一个唯一的key值。
2.3key 与 shmid 的区别
既然有了key,为什么各种共享内存函数返回和使用的都是shmid?
-
key:属于内核级的概念,用来在系统全局唯一标识一块共享内存。 -
shmid:属于用户级 的概念,是shmget函数成功后返回的标识码。
类比理解 :这就像文件系统一样。
key类似于文件的底层inode号,而shmid类似于你在代码中使用的文件描述符fd。我们在用户态写代码时,只认shmid(fd),不直接操作key(inode)。
3.共享内存的生命周期:四大核心系统调用
理解了原理,我们再来看操作共享内存的四个关键 API,就会觉得非常自然了。
3.1创建 / 获取共享内存:shmget
功能:用来创建一段新的共享内存,或者获取一段已经存在的共享内存。

原型:
cpp
int shmget(key_t key, size_t size, int shmflg);
参数解析:
-
key :使用
ftok生成的内核唯一标识。 -
size :共享内存的大小。建议设置为 4KB 的整数倍(因为操作系统分配内存的基本单位是页,1页=4KB)。
-
shmflg:标志位(包含权限标志,用法类似创建文件时的 mode)。
-
单独传入
IPC_CREAT:如果共享内存不存在,就创建并返回;如果已存在,就直接获取并返回。 -
传入
IPC_CREAT | IPC_EXCL:如果共享内存不存在,就创建并返回;如果已存在,出错返回 。这保证了如果函数调用成功,返回的一定是一块全新的共享内存!
-
返回值 :成功返回一个非负整数 shmid;失败返回 -1。
3.2挂接共享内存:shmat (Attach)
功能:将刚刚创建的物理共享内存,映射连接到当前进程的虚拟地址空间。
cpp
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数解析:
-
shmid :
shmget返回的标识码。 -
shmaddr :指定连接的虚拟地址。通常设为 NULL,表示让操作系统核心自动为我们选择一个合适的地址(强烈建议)。
-
shmflg :通常设为 0。如果是
SHM_RDONLY,表示只读。
返回值 :成功返回一个指针(类似 malloc 的返回值),指向共享内存第一个字节;失败返回 (void *)-1。
说明:
shmaddr为NULL,核心自动选择⼀个地址
shmaddr不为NULL且shmflg⽆SHM_RND标记,则以shmaddr为连接地址。
shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会⾃动向下调整为SHMLBA的整数倍。公式:shmaddr - (shmaddr % SHMLBA)
shmflg=SHM_RDONLY,表示连接操作用来只读共享内存
3.3去关联共享内存:shmdt (Detach)
功能:将共享内存段与当前进程的虚拟地址空间脱离。
原型:
cpp
int shmdt(const void *shmaddr);
参数 :shmaddr 就是 shmat 成功时返回的那个首地址指针。 返回值:成功返回 0;失败返回 -1。
⚠️ 注意 :
shmdt只是切断了进程和共享内存的联系(修改页表),并不等于删除共享内存段! 物理内存依然存在于操作系统中。
3.4控制 / 删除共享内存:shmctl
功能 :用于控制共享内存(获取状态、修改属性、最常用的是删除它)。
原型:
cpp
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数解析:
-
shmid:共享内存标识码。
-
cmd:将要采取的动作(即控制命令)。具体有以下三个常用命令:
| 命令 | 说明 |
|---|---|
IPC_STAT |
把 shmid_ds 结构中的数据设置为共享内存的当前关联值(即获取属性)。 |
IPC_SET |
在进程有足够权限的前提下,把共享内存的当前关联值设置为 shmid_ds 数据结构中给出的值(即修改属性)。 |
IPC_RMID |
删除共享内存段。 |
- buf :指向状态数据结构
shmid_ds的指针。如果只是为了删除(传入IPC_RMID),此处传NULL即可。
返回值:成功返回 0;失败返回 -1。
4.C++ 面向对象实战:从基础到完善
为了更好地理解系统调用与内存管理的关系,我们将实战分为两步:先写一个仅包含创建和获取的初代版本,发现并解决其中的生命周期问题后,再完成包含完整数据传输的终极版本。
4.1初代版本:仅实现创建与获取
首先,我们写好统一的编译文件 Makefile:
cpp
all:Reader Writer
Reader:Reader.cc
g++ -o $@ $^ -std=c++11
Writer:Writer.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f Reader Writer
我们在 Reader.cc 中创建,在 Writer.cc 中获取:
cpp
#include "Shm.hpp"
#include <iostream>
#include <string>
// Writer -> shm -> Reader
int main()
{
// 在内核中创建共享内存
Shm shm;
shm.Create();
return 0;
}
cpp
#include "Shm.hpp"
#include <iostream>
#include <string>
int main()
{
Shm shm;
shm.Get();
return 0;
}
我们将 shmget 封装进 Shm.hpp,提供简单的 Create 和 Get 接口:
cpp
#ifndef __SHM_HPP
#define __SHM_HPP
#include <iostream>
#include <sys/shm.h>
#include <string>
const std::string proj_name = ".";
const int proj_id = 0x6666;
const int g_size = 4096;
class Shm
{
public:
Shm(int size = g_size):_shmid(-1), _size(size)
{}
~Shm(){}
private:
key_t GetKey()
{
key_t k = ftok(proj_name.c_str(), proj_id);
if(k < 0)
{
perror("ftok");
}
return k;
}
public:
// 创建
bool Create()
{
// 获取key值
key_t k = GetKey();
// 创建共享内存
_shmid = shmget(k, _size, IPC_CREAT | IPC_EXCL);
if(_shmid < 0)
{
perror("shmget");
return false;
}
std::cout << "shmid: " << _shmid << std::endl;
return true;
}
// 获取共享内存
bool Get()
{
key_t k = GetKey();
std::cout << "key: " << k << std::endl;
return true;
}
private:
int _shmid;
int _size;
};
#endif
4.2踩坑记录生命周期引发的报错 (承上启下)
运行上述代码,当你运行一次 Reader 后,程序退出。可是当你尝试再次运行 Reader 时,程序报错了!

我们先通过查看共享内存,命令为:ipcs -m:
cpp
$ ipcs -m
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x660210c7 7 xxx404 0 4096 0
-
perms : 权限(我们在代码中设置的
0666)。 -
nattch: 当前挂接的进程数。如果为 0,说明没有任何进程在使用它,但它依然占据着内存。
你会发现,刚刚创建的共享内存依然存在(status 在那里,且 nattch 为 0 表示没有进程连接)。由于我们在 Create 时使用了 IPC_CREAT | IPC_EXCL,强求创建全新的内存,所以第二次运行时检测到内存已存在,就报错了。
结论:
共享内存的生命周期 System V IPC 资源(包括共享内存)的生命周期是随内核的 ,而不是随进程的。这意味着如果进程退出前没有主动调用
shmctl(..., IPC_RMID, ...)删除它,这块共享内存会一直驻留在操作系统中,直到系统重启或使用命令行工具(如ipcrm)手动删除。
我们此时先通过命令删除:ipcrm -m
注意:要通过shmid删除,因为它始终属于用户级别的操作。

其中,perms是所属组权限,我们通过在创建时基于权限:

通过上述的错误引出了我们下一步的完善目标:真正的通信需要包含 Attach(挂接)、写/读数据、Detach(去关联),并在程序结束时通过代码主动 Delete(删除) 共享内存。
4.3最终源码:挂接、通信与 RAII 自动清理
为了实现真正的数据通信,我们定义一个数据结构 buffer_t,并在 Shm 类中补充完整的方法。
终极版 Shm.hpp:
cpp
#ifndef __SHM_HPP
#define __SHM_HPP
#include <iostream>
#include <cstdio>
#include <sys/shm.h>
#include <string>
#include <unistd.h>
const std::string proj_name = ".";
const int proj_id = 0x6666;
const int g_size = 4096;
static std::string ToHex(long long data)
{
char hex[64];
snprintf(hex, sizeof(hex), "0x%llx", data);
return hex;
}
class Shm
{
public:
Shm(int size = g_size):_shmid(-1), _size(size), _key(0), _start(nullptr)
{}
~Shm(){}
private:
key_t GetKey()
{
_key = ftok(proj_name.c_str(), proj_id);
if(_key < 0)
{
perror("ftok");
}
return _key;
}
bool CreateCoreHelper(int flags)
{
// 获取key值
key_t k = GetKey();
// 创建共享内存
_shmid = shmget(k, _size, flags);
if(_shmid < 0)
{
perror("shmget");
return false;
}
return true;
}
public:
// 创建
bool Create()
{
return CreateCoreHelper(IPC_CREAT | IPC_EXCL | 0666);
}
// 获取共享内存
bool Get()
{
// key_t k = GetKey();
// std::cout << "key: " << k << std::endl;
// return true;
return CreateCoreHelper(IPC_CREAT);
}
// 删除共享内存
bool Delete()
{
int n = shmctl(_shmid, IPC_RMID, NULL);
return n < 0 ? false : true;
}
// 获取共享内存属性
void GetShmAttr()
{
struct shmid_ds ds;
int n = shmctl(_shmid, IPC_STAT, &ds);
if(n < 0)
{
perror("shmctl");
return;
}
std::cout << "pid: " << getpid() << std::endl;
std::cout << ds.shm_cpid << std::endl;
std::cout << ds.shm_segsz << std::endl;
std::cout << ToHex(ds.shm_perm.__key) << std::endl;
}
// 链接
void *Attach()
{
_start = shmat(_shmid, nullptr, 0);
return _start;
}
// 去关联
void Detach()
{
int n = shmdt(_start);
(void)n;
}
void Debug()
{
std::cout << "key: " << ToHex(_key) << std::endl;
std::cout << "shmid: " << _shmid << std::endl;
}
private:
key_t _key;
int _shmid;
int _size;
void* _start;
};
typedef struct Data
{
int count;
char buffer[26*2];
}buffer_t;
#endif
终极版读取端 Reader.cc: 负责创建内存、挂接、循环读取并在结束时清理资源。
cpp
#include "Shm.hpp"
#include <iostream>
#include <string>
#include <unistd.h>
// Writer -> shm -> Reader
int main()
{
// 在内核中创建共享内存
Shm shm;
shm.Create();
// sleep(3);
char * addr = (char *)shm.Attach(); // 获得共享内存起始地址
// std::cout << "addr: " << ToHex((long long)addr) << std::endl;
// sleep(10);
buffer_t *shm_addr = (buffer_t*)addr;
while(true)
{
std::cout << "count: " << shm_addr->count << std::endl;
std::cout << "data: " << shm_addr->buffer << std::endl;
sleep(1);
if(shm_addr->count>=26)
break;
}
shm.Detach();
// sleep(3);
// shm.Debug();
// shm.GetShmAttr();
// sleep(5);
shm.Delete();
// sleep(3);
return 0;
}
终极版写入端 Writer.cc: 这里巧妙地利用了一个全局对象 Init (C++ RAII 机制),使得程序启动和销毁时自动执行获取、挂接与去关联操作。
cpp
#include "Shm.hpp"
#include <iostream>
#include <string>
#include <string.h>
Shm shm;
class Init
{
public:
Init()
{
shm.Get();
char *addr = (char *)shm.Attach();
std::cout << "addr: " << ToHex((long long)addr) << std::endl;
}
~Init()
{
shm.Detach();
}
char *Addr() // 起始地址
{
return addr;
}
public:
char *addr;
};
Init init;
int main()
{
std::cout << "test begin..." << std::endl;
buffer_t *shm = (buffer_t *)init.Addr();
shm->count = 0;
memset(shm->buffer, 0, 4096);
char ch = 'A';
for (int i = 0; i < 26 * 2; i += 2, ch++)
{
shm->buffer[i] = ch;
shm->buffer[i+1] = ch;
shm->count++;
sleep(1);
}
return 0;
}
5.终极总结:共享内存的三大核心特性
通过以上的理论与代码演进过程,我们可以提炼出关于 System V 共享内存的三个最重要结论:
-
无需系统调用(零拷贝机制) : 我们在终极版代码中向共享内存读写数据时,仅仅是做了一次强转
(buffer_t*)addr,然后就像访问本地变量一样通过指针直接赋值(如shm_buf->count++)。全程没有使用任何类似read/write的系统调用。因为共享内存被映射到了进程自己的用户空间中! -
天下武功,唯快不破(速度最快) : 由第一点可知,进程间数据传递跳过了"用户态与内核态之间的数据拷贝"环节。一方在向内存中写入数据时,另一方几乎立刻就能通过地址空间看到数据的变化。这使得它成为了所有 IPC 机制中速度最快的存在。
-
缺乏保护机制(裸奔的内存) : 仔细观察我们的代码你会发现,共享内存没有自带任何同步和互斥的保护机制 !如果在多进程高并发的情况下,A 在写的同时 B 强行去读,必然会导致数据不一致(脏读)。因此,在实际的商业开发中,共享内存几乎总是要与信号量(Semaphore)或互斥锁配合使用,来保证对内存读写的安全性。
希望这篇文章能帮你彻底打通 Linux 共享内存的任督二脉!如果你觉得有收获,欢迎点赞收藏,也欢迎在评论区交流讨论。