system V
一、基本定义
System V 是早期商用 UNIX 操作系统的一个分支,由贝尔实验室推出,是经典的 UNIX 版本系列。
我们现在 Linux 里说的 System V IPC ,就是这套系统遗留下来的进程间通信标准 ,包含三类:共享内存、消息队列、信号量。
本质就是:遵循老式 UNIX System V 规范实现的一套进程间通信机制。
二、核心原理
System V 共享内存 = 内核开辟一块物理内存 → 让多个进程同时映射到自己的虚拟地址空间 → 大家直接读写同一块物理内存。

理解
1. 映射到虚拟地址空间的共享区

2. 共享内存原理,是一个简化版本的动态库映射!
3. 共享内存管理结构体 + 共享内存本身 = 共享内存

4. 共享内存使用步骤
1. 创建 2. 关联挂接 3. 使用 4. 去关联 5. 释放共享内存
三、相关接口
1. shmget
c++
int shmget(key_t key, size_t size, int shmflg);
作用:向内核申请一块共享内存,或获取已存在的共享内存。
-
key:共享内存的编号 -
size:共享内存大小(必须 >0,内核自动按页对齐) -
shmflg:-
IPC_CREAT:不存在则创建,已存在就用之------获取 -
IPC_CREAT | IPC_EXCL:不存在创建,已存在则报错------创建 -
0664:权限(和文件权限一样)
-
返回:成功返回 shmid(共享内存 ID),失败 -1
(解析)key 键值
需要共享内存时,如何知道这个内存在不在呢?那么在内核中会对 shm 进行标识,实现共享内存的唯一性。但是我们发现这个跟内核打交道的标识竟然要用户自己填!为什么不能像文件描符fd那样,让内核自己处理好之后,将上层提供给进程,然后把这玩意儿交给进程之间不就好了吗?但是!!!如果是这样,那进程之间也没有通信方式来看到这个上层的东西啊,所以不能让内核自己弄!
所以需要进程之间约定好这个 key 键值

所以,这个key值理论上来说就可以自己随便给,给1、2、3都行但是一般不这么干。一般会通过一个函数生成一个key值。
ftok:key钥匙生成器。
c++
key_t ftok(const char *pathname, int proj_id);
作用 :把一个文件路径 + 一个数字,转换成一个唯一的 key 值 ,给 shmget / msgget / semget 使用。
参数
-
pathname
- 一个真实存在的文件路径(目录 / 普通文件都行)
- 只要存在就行,内容不重要
-
proj_id
- 一个 1 字节的数字(0~255),随便给
- 同一个文件,不同 proj_id → 生成不同 key
返回值
- 成功:返回 key_t 类型的 key
- 失败:返回
-1
c++
// 1. 用当前目录 . 生成 key
key_t key = ftok(".", 66); // . 一定存在,66 是自定义数字
// 2. 用这个 key 创建共享内存
int shmid = shmget(key, 4096, IPC_CREAT|0666);



此时相当于 server 创建好了一个共享内存,那么 client 端就需要获取这个共享内存。所以 Shm 类内还需要一个获取共享内存的接口,这个接口大致跟创建接口差不多,只是在 shmget() 时传的参数不一样,需要只用 IPC_CRETA 参数获取。


此时运行之后再看看结果:

那么这样就形成了进程间的联系。
(解析)shmget返回值shmid
我们有了key键值,那函数 shmget 返回值shmid是什么呢?我们可以通过代码查看一下:


(解析)共享内存的生命周期
那如果我再运行一下这个./server会发生报错,显示 File exists !

此时我们还可以通过一个命令查看当前系统中的IPC: ipcs

问题:为啥进程结束了,这个共享内存还能查到呢?
**结果:**共享内存,包括system V IPC标准下其他的两类通信,生命周期随内核!即:用户如果不主动删除IPC资源,IPC资源会和操作系统一样,一直存在,除非重启系统。
手动命令删除共享内存:
ipcrm -m [shmid] 不用key键值删除共享内存

删除之后再次运行./server,shmid就变成了1


再次重复创建就意料之内会失败,但是我们得思考是因为什么冲突了导致重复创建会失败,显而易见:是key键值冲突。
对比 key 与 shmid
**1. key只在内核中,标识共享内存的唯一性!用户不使用key!**所以命令 ipcrm -m 要用shmid
2. shmid 只在用户中使用,在代码中使用shmid来访问共享内存
2. shmctl (control)
c++
int shmctl(int shmid, int cmd, struct shm_ds *buf);
作用 :获取信息、设置权限、删除共享内存。(因为删除就是控制的一种)
常用 cmd:
- IPC_RMID :删除共享内存(最常用)
- IPC_STAT:获取状态信息
- IPC_SET:设置权限
第三个参数传:*buf
作用:内核把共享内存的信息(大小、权限、创建时间、挂载进程数)写到 buf 里


c++
void Delete() {
int n = shmctl(_shmid, IPC_RMID, nullptr);
if (n < 0) {
std::cerr << "shmctl 删除失败:" << strerror(errno) << std::endl;
}
}

3. shmat (attach)
c++
void *shmat(int shmid, const void *shmaddr, int shmflg);
作用 :把共享内存挂接到当前进程的虚拟地址空间。
参数2:shmaddr
想让共享内存映射到哪个虚拟地址→ 直接填 NULL(让内核自动选,99% 场景)
参数3:shmflg
- 0 → 可读可写(一般直接填0)
SHM_RDONLY→ 只读
返回值(重要)
成功:返回一个 void* 类型的指针,指向映射后的共享内存起始地址。跟(malloc差不多)
失败:返回 (void *) -1
所以不能用判断 NULL 的方式检查 shmat 是否失败!
c++
if (p == NULL) { ... } // 错!错!错!
正确写法:
c++
if (p == (void*)-1) { ... } // 对!对!对!
c++
int main() {
Shm sharedmem;
sharedmem.Create();
sleep(5);
sharedmem.Attach();
sharedmem.Delete();
return 0;
}
c++
void Attach() {
_start_addr = shmat(_shmid, nullptr, 0);
if (_start_addr = (void*)-1) {
std::cerr << "shmat挂接失败:" << strerror(errno) << std::endl;
exit(3);
}
}


nattach :表示此共享内存的挂接数。

perms :表示此共享内存的权限。
c++
// 2. 创建共享内存
_shmid = shmget(k, _size, IPC_CREAT | IPC_EXCL | 0666);
if (_shmid < 0) {
std::cerr << "shmget创建共享内存失败:" << strerror(errno) << std::endl;
exit(2);
}

bytes属性
在 shmget 传参时,传入的size就是设置的共享内存大小,必须是4096的整数倍!
4. shmdt (detach解绑定)
当进程不适用此共享内存之后,最好的办法不是直接删除这个共享内存,而是将自己与这个共享内存解除绑定!需要用到接口 shmdt。
c++
int shmdt(const void *shmaddr);
作用:解除当前进程与共享内存的挂接关系。
参数:shmaddr ;填 shmat 返回的那个地址指针
返回值:成功 → 返回 0;失败 → 返回 -1。
c++
void Detach() {
int n = shmdt(_start_addr);
if (n == -1) {
std::cerr << "shmdt解除挂接失败:" << strerror(errno) << std::endl;
exit(4);
}
std::cout << "解除挂接成功" << std::endl;
}
c++
// ./server
#include "shm.hpp"
#include <unistd.h>
int main() {
Shm sharedmem;
sharedmem.Create();
sharedmem.Attach();
sleep(5);
sharedmem.Detach();
sleep(5);
sharedmem.Delete();
return 0;
}

四、两端演示
c++
// ./client
#include "shm.hpp"
int main() {
// 不需要创建内核级共享内存,当然也不需要删除
Shm sharedmem;
sharedmem.Get();
sleep(5);
sharedmem.Attach();
sleep(5);
sharedmem.Detach();
return 0;
}
c++
// ./server
#include "shm.hpp"
int main() {
// 谁创建,谁删除
Shm sharedmem;
sharedmem.Create();
sharedmem.Attach();
sleep(5);
sharedmem.Detach();
sleep(5);
sharedmem.Delete();
return 0;
}
挂载数 nattch 0->1->2->1->0->删除。
c++
// ./server
#include "shm.hpp"
int main() {
// 谁创建,谁删除
Shm sharedmem;
sharedmem.Create();
sharedmem.Attach();
char* shm_start = (char*)sharedmem.Addr();
int size = sharedmem.Size();
while (true) {
for (int i = 0; i <size; ++i) {
std::cout << shm_start[i] << ' ';
}
std::cout << std:: endl;
sleep(3);
}
sharedmem.Detach();
sharedmem.Delete();
return 0;
}
c++
// ./client
#include "shm.hpp"
int main() {
// 不需要创建内核级共享内存,当然也不需要删除
Shm sharedmem;
sharedmem.Get();
sharedmem.Attach();
char* shm_start = (char*)sharedmem.Addr();
int size = sharedmem.Size();
int index = 0;
while (true) {
std::cout << "请输入@ ";
char ch;
std::cin >> ch;
shm_start[index++] = ch;
index %= size;
}
sharedmem.Detach();
return 0;
}
五、共享内存特点
- 访问共享内存,不需要系统调用;
- 写端写入,其他端立即能看到;速度快!拷贝少!
- 缺点,没有资源保护机制,没有同步或者互斥(没有阻塞)
六、完整代码

c++
// ./shm.hpp
#pragma once
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include <iostream>
#include <cstring>
#include <cstdio>
// 用户指明
#define PATHNAME "."
#define PROJ_ID 0x66
const int gsize = 4096;
class Shm {
public:
Shm()
: _shmid(-1)
, _size(gsize)
, _start_addr(nullptr)
{}
~Shm() {}
void Delete() {
int n = shmctl(_shmid, IPC_RMID, nullptr);
if (n < 0) {
std::cerr << "shmctl 删除失败:" << strerror(errno) << std::endl;
exit(5);
}
std::cout << "删除成功!" << std::endl;
}
void Attach() {
_start_addr = shmat(_shmid, nullptr, 0);
if (_start_addr == (void*)-1) {
std::cerr << "shmat挂接失败:" << strerror(errno) << std::endl;
exit(3);
}
}
void Detach() {
int n = shmdt(_start_addr);
if (n == -1) {
std::cerr << "shmdt解除挂接失败:" << strerror(errno) << std::endl;
exit(4);
}
std::cout << "解除挂接成功" << std::endl;
}
void Get() {
GetHelper(IPC_CREAT);
}
void Create() {
GetHelper(IPC_CREAT | IPC_EXCL | 0666);
}
void* Addr() {
return _start_addr;
}
int Size() {
return _size;
}
private:
key_t Getkey() {
return ftok(PATHNAME, PROJ_ID);
}
void GetHelper(int shmflg) {
// 1. 构建键值
key_t k = Getkey();
if (k < 0) {
std::cerr << "获取key键值失败:" << strerror(errno) << std::endl;
exit(1);
}
// 2. 创建共享内存
_shmid = shmget(k, _size, shmflg);
if (_shmid < 0) {
std::cerr << "shmget创建共享内存失败:" << strerror(errno) << std::endl;
exit(2);
}
printf("16进制key:0x%x; _shmid:%d\n", k, _shmid);
}
private:
int _shmid;
int _size;
void* _start_addr;
};
c++
// ./server
#include "shm.hpp"
int main() {
// 谁创建,谁删除
Shm sharedmem;
sharedmem.Create();
sharedmem.Attach();
char* shm_start = (char*)sharedmem.Addr();
int size = sharedmem.Size();
while (true) {
for (int i = 0; i <size; ++i) {
std::cout << shm_start[i] << ' ';
}
std::cout << std:: endl;
sleep(3);
}
sharedmem.Detach();
sharedmem.Delete();
return 0;
}
c++
// ./client
#include "shm.hpp"
int main() {
// 不需要创建内核级共享内存,当然也不需要删除
Shm sharedmem;
sharedmem.Get();
sharedmem.Attach();
char* shm_start = (char*)sharedmem.Addr();
int size = sharedmem.Size();
int index = 0;
while (true) {
std::cout << "请输入@ ";
char ch;
std::cin >> ch;
shm_start[index++] = ch;
index %= size;
}
sharedmem.Detach();
return 0;
}