System V 共享内存
当需要有进程间通信的需求时,程序员为了复用之前的代码创建了管道,而管道的原理是基于文件的。随着时代的发展,程序员逐渐发现有一些是管道不能解决的问题,因此不得不真正创建了一个可以进行通信的资源。此时,在上层就有公司定制了一套 System V 标准,由其他公司按照这个标准进行不同的实现。
共享内存区是最快的 IPC 形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来回传递彼此的数据。
共享内存原理
- 共享内存是实实在在地在物理内存中开辟内存块;
- 有了物理地址,那么就需要与虚拟地址建立映射关系;
- 如果要释放共享内存时,就需要删除映射关系。
而这些操作只有操作系统能做到,而操作系统又对上提供了一套系统调用,则用户就可以通过系统调用完成。
既然能开辟内存空间,那么如果有多个进程开辟多个内存空间,OS 要知道哪些内存空间是由谁开辟的,这块内存空间是刚建立的还是正在销毁的 --> 这决定了 OS 要管理内存空间 --> 如何管理呢?
先描述!在组织!
创建共享内存 - 研究特性
shmget
IPC_CREAT 单独:如果创建的共享内存不存在,就创建;如果存在,就获取 IPC_EXCL:不能单独使用,没有意义 IPC_CREAT | IPC_EXCL:如果共享内存不存在,就创建,返回的就是全新的 shm;如果已经存在,出错返回!
返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回 -1
key 是由用户进行传入,那为什么要这样做呢?为什么需要配合 ftok 函数进行使用呢?
- 公共标识符:进程 A 使用这个 key(比如 1234)创建了一块共享内存。之后,进程 B 也拿着同样的 key(1234)向操作系统申请,操作系统一看 key 已经存在,就会把已经创建好的那块共享内存的访问权限也赋给进程 B。这样,两个进程就指向了同一块物理内存,可以进行通信了。如果 key 是由内核随机生成并只返回给创建者,那其他进程就无从知晓了。
- 正如你的图中所展示的,直接在代码里写一个固定的数字(key_t key = 1234;)作为键值有风险,因为你无法保证这个数字在整个操作系统里是唯一的,万一和其他程序冲突了就会出问题。为了解决这个问题,ftok() 函数提供了一种更可靠的方式来生成这个 key。只要所有进程都使用相同的路径和 ID,ftok() 就能保证为它们生成完全相同的 key。
那么 key 是保存在哪的呢?ipc_perm 的结构体里:
struct ipc_perm {
key_t key;
__compat_uid_t uid;
__compat_gid_t gid;
__compat_uid_t cuid;
__compat_gid_t cgid;
compat_mode_t mode;
unsigned short seq;
};
创建共享内存
进程退出的时候,曾经申请的内存空间会被 OS 自动释放掉;哪怕会有内存泄漏问题,也会被释放掉。既然结束后都能被 OS 释放掉,那为什么还要担心内存泄漏问题?注意:我们所写的程序都是一个片段,它不能一直跑下去;而真正的软件是 while 循环下去的。
bool CreateHelper(int flags) {
_key = ftok(gpathname.c_str(), gproj_id);
if (_key < 0) {
perror("ftok");
return false;
}
printf("形成键值成功:0x%x\n", _key); // 键值的内容
_shmid = shmget(_key, _size, flags);
if (_shmid < 0) {
perror("shmget");
return false;
}
printf("shmid:%d\n", _shmid);
return true;
}
bool Create() {
return CreateHelper(IPC_CREAT | IPC_EXCL | 0666);
}
在上面这段程序中,用户创建了共享内存,随之进程结束。但是共享内存并不会随着进程的结束而结束,因此该内存块会一直存在于物理内存中,因此再次创建时会显示该共享内存已经存在。
创建失败的原因是由于内存没有释放,那么是否存在释放共享内存的指令或系统调用呢?
显示共享内存:ipcs -m 删除共享内存:ipcrm -m shmid,shmid 就是 shmget 的返回值 为什么创建共享内存时是用户传 key 进去,而删除共享内存时却是 shmid 呢?
- key:只有内核使用,用来标识 shm 的唯一性
- shmid:给用户使用,用来进行访问 shm
shmctl - 控制共享内存
int shmctl(int shmid, int op, struct shmid_ds *buf);
功能:用于控制共享内存函数
参数: shmid:由 shmget 返回的共享内存标识码 op:将要采取的动作(有三个可取值) buf:指向一个保存着共享内存的模式状态和访问权限的数据结构 (用户层的数据结构)
返回值:成功返回 0;失败返回 -1
bool RemoveShm() {
int n = shmctl(_shmid, IPC_RMID, nullptr);
if (n < 0) {
perror("shmctl");
return false;
}
std::cout << "删除 shm 成功" << std::endl;
return true;
}
shmat - 挂接共享内存
void *shmat(int shmid, const void *shmaddr, int shmflg);
功能:将共享内存段连接到进程地址空间函数
参数: shmid:共享内存标识 shmaddr:指定连接的地址。由用户指明地址,默认为 NULL 就好。 shmflg:它的两个可能取值是 SHM_RND 和 SHM_RDONLY,默认为 0 shmaddr 为 NULL,核心自动选择一个地址 shmaddr 不为 NULL 且 shmflg 无 SHM_RND 标记,则以 shmaddr 为连接地址。 shmaddr 不为 NULL 且 shmflg 设置了 SHM_RND 标记,则连接的地址会自动向下调整 SHMLBA 的整数倍。公式:shmaddr - (shmaddr % SHMLBA) shmflg=SHM_RDONLY,表示连接操作用来只读共享内存
返回值:成功返回一个指针,指向共享内存第一个字节;失败返回
bool Attach() {
_start_addr = shmat(_shmid, nullptr, 0);
if ((long long)_start_addr == -1) // 64 位环境下
{
perror("shmat");
return false;
}
std::cout << "将指定的共享内存挂接到自己进程的地址空间" << std::endl;
printf("_start_addr:%p\n", _start_addr);
_num = (int *)_start_addr;
_datastart = (char *)_start_addr + sizeof(int);
return true;
}
为什么会挂接失败呢?这是因为我们开始创建的的共享内存是 0,是不允许挂接的,需要对应的权限。
_shmid = shmget(_key, 1024, IPC_CREAT | IPC_EXCL | 0666);
shmdt - 删除映射关系
nattch 表示有几个进程与我建立关联,其实就是建立映射关系。
这里会引出来一个问题:如果有多个进程与我这个共享内存关联,当其中有一个进程退出的时候会删掉共享内存,那这样其他进程就不能与这个共享内存关联了啊!!!因此需要知道删除共享内存建立的关系。
int shmdt(const void *shmaddr);
功能:将共享内存段与当前进程脱离函数
参数 shmaddr:由 shmat 所返回的指针
返回值:成功返回 0;失败返回 -1
注意:将共享内存段与当前进程脱离不等于删除共享内存
bool Detach() {
int n = shmdt(_start_addr);
if (n < 0) {
perror("shmdt");
return false;
}
std::cout << "将指定的共享内存从进程的地址空间移除" << std::endl;
return true;
}
再探共享内存原理
特征总结
共享内存 = 共享内存内容 + 共享内存属性
优点:
- 共享内存的生命周期随内核
- 共享内存是 IPC 中速度最快的!!!(因为减少了数据拷贝的次数,不需要调用系统调用)
缺点:
- 共享内存,没有同步、互斥机制,来对多个进程的访问进行协同!
技术总结
本文阐述了 Linux 系统中 SystemV 共享内存的原理与应用。共享内存是进程间通信 (IPC) 最快的形式,通过物理内存映射实现数据直接传递。文章详细讲解了共享内存的创建 (shmget)、挂接 (shmat)、控制 (shmctl) 等系统调用,并分析了 key 和 shmid 的作用机制。作者还通过代码示例演示了如何配合管道实现进程间同步控制,包含共享内存的创建、读写和释放全过程。最后指出共享内存的优缺点:速度快但缺乏同步机制,需要配合其他 IPC 机制使用。文章提供了完整的 C++ 实现代码,展示了共享内存从创建到销毁的完整生命周期管理。
扩展
我们之前说过 OS 申请大小都是以 4kb 为单位进行申请的,那么如果我们申请 4097 个字节,OS 是否申请 8192 个字节呢?
不对啊,是 4097 个字节没错啊?实际上 OS 申请了 8192 个字节,但是只给你使用 4097 个字节,因为你需要申请的就 4097 个字节,多的给了你,你用越界了来怪我 OS。因此 OS 不会让你知道它偷偷申请了 8192 个字节,怕你责怪。
共享内存数据结构
struct shmid_ds {
struct ipc_perm shm_perm; /* Ownership and permissions */
size_t shm_segsz; /* Size of segment (bytes) */
time_t shm_atime; /* Last attach time */
time_t shm_dtime; /* Last detach time */
time_t shm_ctime; /* Creation time/time of last modification via shmctl() */
pid_t shm_cpid; /* PID of creator */
pid_t shm_lpid; /* PID of last shmat(2)/shmdt(2) */
shmatt_t shm_nattch; /* No. of current attaches */
...
};
struct ipc_perm {
key_t __key; /* Key supplied to shmget(2) */
uid_t uid; /* Effective UID of owner */
gid_t gid; /* Effective GID of owner */
uid_t cuid; /* Effective UID of creator */
gid_t cgid; /* Effective GID of creator */
unsigned short mode; /* Permissions + SHM_DEST and SHM_LOCKED flags */
unsigned short __seq; /* Sequence number */
};
int shmget(key_t key, size_t size, int shmflg);
key_t ftok(const char *pathname, proj_id);
打印输出 shm 相关属性:
void PrintArr() {
struct shmid_ds ds;
int n = shmctl(_shmid, IPC_STAT, &ds);
if (n < 0) {
perror("shmctl");
return;
}
// 打印输出 shm 相关属性
printf("key: 0x%x\n", ds.shm_perm.__key);
printf("size: %ld\n", ds.shm_segsz);
printf("atime: %lu\n", ds.shm_atime);
printf("nattach: %ld\n", ds.shm_nattch);
}
借助管道实现控制板共享内存
Comm.hpp
#pragma once
#include <fcntl.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <cassert>
#include <cstdio>
#include <ctime>
#include <cstring>
#include <iostream>
using namespace std;
#define Debug 0
#define Notice 1
#define Warning 2
#define Error 3
const std::string msg[] = { "Debug", "Notice", "Warning", "Error" };
std::ostream &Log(std::string message, int level) {
std::cout << " | " << (unsigned)time(nullptr) << " | " << msg[level] << << message;
std::cout;
}
{
:
() {
();
n = (FIFO_NAME, );
(n == );
()n;
(, Notice) << ;
}
~() {
(FIFO_NAME);
(, Notice) << ;
}
};
{
fd = (pathname.(), flags);
(fd >= );
fd;
}
{
(fd);
}
{
(, Notice) << ;
temp = ;
s = (fd, &temp, ());
(s == ());
()s;
}
{
temp = ;
s = (fd, &temp, ());
(s == ());
()s;
(, Notice) << ;
}
{
buffer[];
(buffer, buffer, , k);
buffer;
}
ShmServer.cc
#include "Comm.hpp"
Init init;
int main() {
// 1. 创建公共的 Key 值
key_t k = ftok(PATH_NAME, PROJ_ID);
assert(k != -1);
Log("create key done", Debug) << " server key : " << TransToHex(k) << endl;
// 2. 创建共享内存 -- 建议要创建一个全新的共享内存 -- 通信的发起者
int shmid = shmget(k, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666);
if (shmid == -1) {
perror("shmget");
exit(1);
}
Log("create shm done", Debug) << " shmid : " << shmid << endl;
// 3. 将指定的共享内存,挂接到自己的地址空间
char* shmaddr = (char*)shmat(shmid, nullptr, 0);
Log("attach shm done", Debug) << " shmid : " << shmid << endl;
// 4. 访问控制
int fd = OpenFIFO(FIFO_NAME, O_RDONLY);
while (true) {
// 阻塞
Wait(fd);
// 临界区
printf("%s\n", shmaddr);
if (strcmp(shmaddr, "quit") == ) ;
}
(fd);
n = (shmaddr);
(n != );
()n;
(, Debug) << << shmid << endl;
n = (shmid, IPC_RMID, );
(n != );
()n;
(, Debug) << << shmid << endl;
;
}
ShmClient.cc
#include "Comm.hpp"
int main() {
// 1. 创建公共的 Key 值
key_t k = ftok(PATH_NAME, PROJ_ID);
if (k < 0) {
Log("create key failed", Error) << " client key : " << TransToHex(k) << endl;
exit(1);
}
Log("create key done", Debug) << " client key : " << TransToHex(k) << endl;
// 2. 获取共享内存
int shmid = shmget(k, SHM_SIZE, 0);
if (shmid < 0) {
Log("create shm failed", Error) << " client key : " << TransToHex(k) << endl;
exit(2);
}
Log("create shm success", Error) << " client key : " << TransToHex(k) << endl;
// 3. 挂接共享内存
char *shmaddr = (char *)shmat(shmid, nullptr, 0);
if (shmaddr == nullptr) {
Log("attach shm failed", Error) << " client key : " << TransToHex(k) << endl;
exit();
}
(, Error) << << (k) << endl;
fd = (FIFO_NAME, O_WRONLY);
() {
s = (, shmaddr, SHM_SIZE - );
(s > ) {
shmaddr[s - ] = ;
(fd);
((shmaddr, ) == ) ;
}
}
(fd);
n = (shmaddr);
(n != );
(, Error) << << (k) << endl;
;
}


