上一篇主要学习了管道。匿名管道适合父子进程之间传输数据,命名管道则可以让两个互不相关的进程进行通信。
但是管道有一个特点:数据通过读写接口在进程之间传递。
如果两个进程需要频繁交换数据,有没有更直接的办法?
这就要用到共享内存了。
一、共享内存是什么?
共享内存可以理解成一块由多个进程共同使用的内存区域。
普通情况下,每个进程都有自己的地址空间。一个进程修改自己的普通变量,另一个进程不会因此直接看到变化。
而共享内存不一样:系统把同一块共享内存分别映射到多个进程的地址空间中。一个进程把数据写进去,另一个进程就能从这块内存中读取数据。

这里需要注意:两个进程中的虚拟地址不一定相同,但它们可以映射到同一块共享内存。
共享内存建立映射之后,数据读写可以直接通过内存地址完成,不需要每次都通过 read()、write() 这样的系统调用传递数据。因此,在常见 IPC 方式中,共享内存的速度很有优势。
不过,速度快不代表所有问题都解决了。共享内存本身并不负责处理多个进程同时读写的问题,这一点后面还会单独讨论。
二、System V共享内存的使用流程
Linux 下使用 System V 共享内存,主要会接触下面几个函数:
ftok()
shmget()
shmat()
shmdt()
shmctl()
它们分别负责获取 key、创建或获取共享内存、挂接、脱离以及控制共享内存资源。
可以先记住整体流程:
获取 key
↓
shmget() 创建或获取共享内存
↓
shmat() 挂接到进程地址空间
↓
读取或写入共享数据
↓
shmdt() 脱离共享内存
↓
shmctl() 删除共享内存资源
这里有两个动作很容易混淆:脱离共享内存不等于删除共享内存,删除共享内存也不是简单地调用 free()。
三、第一步:生成key
如果两个进程要使用同一块共享内存,它们首先需要找到同一个共享内存对象。
key_t ftok(const char *pathname, int proj_id);
使用 ftok() 生成 key:
key_t key = ftok(".", 0x6666);
if (key == –1)
{
perror("ftok");
return 1;
}
ftok() 根据路径名和项目标识生成一个 key_t 类型的 key。
两个进程如果希望访问同一块共享内存,需要得到一致的 key。实际使用时也要注意,ftok() 生成的 key 并不是绝对唯一的,因此不能把它当成永远不会冲突的全局唯一编号。
四、第二步:创建或获取共享内存
获取到 key 以后,使用 shmget():
int shmget(key_t key, size_t size, int shmflg);
其中:
- key:共享内存对应的 key。
- size:共享内存段的大小。
- shmflg:创建标志和权限标志。
例如,创建一个大小为 4096 字节的共享内存:
int shmid = shmget(
key,
4096,
IPC_CREAT | IPC_EXCL | 0666
);
if (shmid == –1)
{
perror("shmget");
return 1;
}
这里的 shmid 是共享内存的标识符,后面的操作会用到它。
IPC_CREAT 表示对象不存在时创建;IPC_CREAT | IPC_EXCL 则表示只有对象不存在时才创建,如果它已经存在,就返回错误。权限部分 0666 表示给出相应的读写权限,实际权限还会受到进程的权限环境等因素影响。
如果是另一个进程来获取已经创建的共享内存,可以使用相同的 key 调用 shmget(),并根据使用场景设置参数。
这也是服务端和客户端常见的分工:一个进程负责创建共享内存,另一个进程负责获取它。
五、第三步:把共享内存挂接到进程地址空间
shmget() 返回的 shmid 只是一个标识符,还不能直接拿来读写数据。
接下来需要调用:
void* shmat(int shmid, const void* shmaddr, int shmflg);
例如:
char* addr = static_cast<char*>(
shmat(shmid, nullptr, 0)
);
if (addr == reinterpret_cast<char*>(–1))
{
perror("shmat");
return 1;
}
这里将 shmat() 返回的地址保存到 addr 中。
参数 shmaddr 传入 nullptr,表示让系统选择挂接地址;shmflg 为 0 时,按照默认方式挂接。
需要特别注意,shmat() 失败时返回的是 (void*)-1,不是普通的空指针 nullptr。所以不能直接用 addr == nullptr 判断它是否成功。
挂接之后,就可以把它当作普通内存地址使用。
例如:
strcpy(addr, "hello from shared memory");
cout << addr << endl;
这里写入的数据存放在共享内存里,而不是只属于当前进程的普通局部变量。
六、第四步:另一个进程读取数据
假设服务端已经创建共享内存,客户端就可以使用同一个 key 获取它:
int shmid = shmget(key, 4096, 0);
if (shmid == –1)
{
perror("shmget");
return 1;
}
然后挂接:
char* addr = static_cast<char*>(
shmat(shmid, nullptr, 0)
);
if (addr == reinterpret_cast<char*>(–1))
{
perror("shmat");
return 1;
}
此时,客户端可以直接读取服务端写入的数据:
cout << addr << endl;
反过来,客户端修改这块共享内存,服务端也可以读取到对应的变化。
这正是共享内存的特点:双方不需要通过一连串的管道读写来传递内容,而是直接访问同一块共享区域。
当然,两个进程必须约定好数据的格式和使用方式。例如,一方写入字符串时,需要考虑字符串末尾的 '\\0';如果保存的是结构体,也要约定好字段和数据长度。
七、第五步:脱离和删除共享内存
使用完成后,需要进行清理。
1. shmdt:脱离共享内存
函数原型:
int shmdt(const void* shmaddr);
调用:
if (shmdt(addr) == –1)
{
perror("shmdt");
}
它的作用是让当前进程与这块共享内存脱离。
但有一点必须区分清楚:
shmdt() 只是取消当前进程的映射,不会直接删除整个共享内存段。
其他进程仍然挂接着它时,仍然可以继续使用。
2. shmctl:控制共享内存
控制共享内存可以使用:
int shmctl(int shmid, int cmd, struct shmid_ds* buf);
例如:
if (shmctl(shmid, IPC_RMID, nullptr) == –1)
{
perror("shmctl");
}
这里的 IPC_RMID 用于标记删除共享内存段。对于仍然有进程挂接的共享内存,实际释放会等到不再有相关挂接后完成。
所以,这几个动作可以记成:
shmat() → 挂接
shmdt() → 当前进程脱离
shmctl() → 控制资源,IPC_RMID用于删除
不能把 shmdt() 和 shmctl() 当成同一个操作。
八、示例代码
// Comm.hpp
#pragma once
#include <cstdio>
#include <cstdlib>
#define ERR_EXIT(m) \\
do \\
{ \\
perror(m); \\
exit(EXIT_FAILURE); \\
} while (0)
// Shm.hpp
#pragma once
#include <iostream>
#include <cstdio>
#include <string>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include "Comm.hpp"
const int gdefaultid = –1;
const int gsize = 4096;
const std::string pathname = ".";
const int projid = 0x66;
const int gmode = 0666;
#define CREATER "creater"
#define USER "user"
class Shm
{
private:
void CreateHelper(int flg)
{
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 Get()
{
CreateHelper(IPC_CREAT);
}
void Attach()
{
_start_mem = shmat(_shmid, nullptr, 0);
if ((long long)_start_mem < 0)
{
ERR_EXIT("shmat");
}
printf("attach success\\n");
}
void Detach()
{
int n = shmdt(_start_mem);
if (n == 0)
{
printf("detach success\\n");
}
}
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);
printf("shm_srgsz: %ld\\n", ds.shm_segsz);
printf("key: 0x%x\\n", ds.shm_perm.__key);
}
~Shm()
{
Destroy();
}
private:
int _shmid;
key_t _key;
int _size;
void *_start_mem;
std::string _usertype;
};
九、为什么共享内存退出后可能还在?
System V IPC 资源由内核管理,不会像普通进程的局部变量一样,随着进程结束就必然自动消失。
比如程序创建共享内存之后,如果异常退出,没有执行删除操作,那么再次启动程序时,可能会遇到:
shmget: File exists
这时可以用:
ipcs -m
查看系统中的共享内存段。
输出中能看到共享内存的 key、shmid、权限、大小以及当前挂接数量等信息。
如果确认某个共享内存段已经不需要了,可以使用:
ipcrm -m shmid
其中 shmid 要替换成实际查询到的共享内存标识符。
这些命令适合排查实验过程中遗留的 IPC 资源。不过,删除之前一定要确认它确实已经不需要,不能看到资源存在就随意删除。
十、共享内存没有自动解决同步问题
这一点是共享内存中最容易被忽略的地方。
假设客户端正在写:
ABCDE
而服务端同时读取。
如果读写时机没有协调好,服务端可能读到尚未写完的数据。多个进程同时访问共享数据,还可能覆盖彼此的修改。
所以,共享内存解决的是数据共享和传递,并不自动保证访问的顺序以及互斥关系。
对于需要互斥访问的共享数据,可以考虑使用信号量、互斥锁等机制。关键是要明确:什么时候允许写,什么时候允许读,多个进程同时操作时如何避免冲突。
十一、把这几个API记清楚就够了
刚接触 System V 共享内存时,不必一上来就记所有结构体字段。先把使用顺序记熟:
ftok()
获取 key
shmget()
创建或获取共享内存
shmat()
挂接,得到可访问的地址
读写共享数据
shmdt()
当前进程脱离
shmctl(IPC_RMID)
删除共享内存资源
实际编程时,还要注意 shmget()、shmat() 等函数的错误返回值,记得处理失败情况。
总结
共享内存的优势是数据映射完成以后,进程可以直接访问公共内存区域,省去了通过管道逐次传输数据的过程。
它的使用流程并不复杂,真正容易搞混的主要是三个地方:
一是 shmid 只是标识符,必须通过 shmat() 挂接以后才能访问数据。
二是 shmdt() 负责当前进程脱离,shmctl(IPC_RMID) 才负责标记删除资源。
三是共享内存本身不提供完整的同步与互斥机制。多个进程共享一块内存之后,仍然需要考虑读写顺序和数据竞争。
把这几点搞清楚,System V 共享内存的基本用法就算入门了。
网硕互联帮助中心

评论前必须登录!
注册