[linux仓库]图解System V共享内存:从shmget到内存映射的完整指南
🌟 各位看官好,我是!
🌍 Linux == Linux is not Unix !
🚀 今天来学习System V共享内存,从了解接口再到探查共享内存实现的原理。
👍 如果觉得这篇文章有帮助,欢迎您一键三连,分享更多人哦!
目录
System V共享内存
我们前面说过,当需要有进程间通信的需求时,程序员为了偷懒选择了复用之前的代码,创建了管道,而管道的原理是基于文件的.随着时代的发展,程序员逐渐发现有一些是管道不能解决的问题,因此不得不真正的创建了一个可以进行通信的资源.此时,在上层就有公司定制了一套System V标准,由其他公司按照这个标准进行不同的实现.
共享内存区是最快的IPC形式。⼀旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执⾏进⼊内核的系统调⽤来传递彼此的数据
共享内存原理
- 共享内存是实实在在地在物理内存中开辟内存块;
- 有了物理地址,那么就需要与虚拟地址建立映射关系;
- 如果要释放共享内存时,就需要删除映射关系.
而这些操作只有OS能做到,而OS又对上提供了一套系统调用,则用户就可以通过系统调用完成.

既然能开辟内存空间,那么如果有多个进程开辟多个内存空间,0s要知道哪些内存空间是由谁开辟的,这块内存空间是刚建立的还是正在销毁的 -->这决定了0s要管理内存空间 -->如何管理呢?
先描述 ! 在组织 !
创建共享内存 - 研究特性
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_perm32 { 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; };创建共享内存
进程退出的时候,曾经申请的内存空间会被0S自动释放掉;哪怕会有内存泄漏问题,也会被释放掉既然结束后都能被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呢?
1.key:只有内核使用,用来标识shm的唯一性
2.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个字节,多的给了你,你用越界了来怪我0s。因此0S不会让你知道它偷偷申请了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, int 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; return std::cout; } #define PATH_NAME "/home/hyb" #define PROJ_ID 0x66 #define SHM_SIZE 4096 // 共享内存的⼤⼩,最好是⻚(PAGE: 4096)的整数倍 #define FIFO_NAME "./fifo" class Init { public: Init() { umask(0); int n = mkfifo(FIFO_NAME, 0666); assert(n == 0); (void)n; Log("create fifo success", Notice) << "\n"; } ~Init() { unlink(FIFO_NAME); Log("remove fifo success", Notice) << "\n"; } }; #define READ O_RDONLY #define WRITE O_WRONLY int OpenFIFO(std::string pathname, int flags) { int fd = open(pathname.c_str(), flags); assert(fd >= 0); return fd; } void CloseFifo(int fd) { close(fd); } void Wait(int fd) { Log("等待中....", Notice) << "\n"; uint32_t temp = 0; ssize_t s = read(fd, &temp, sizeof(uint32_t)); assert(s == sizeof(uint32_t)); (void)s; } void Signal(int fd) { uint32_t temp = 1; ssize_t s = write(fd, &temp, sizeof(uint32_t)); assert(s == sizeof(uint32_t)); (void)s; Log("唤醒中....", Notice) << "\n"; } string TransToHex(key_t k) { char buffer[32]; snprintf(buffer, sizeof buffer, "0x%x", k); return 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") == 0) break; } CloseFifo(fd); // 5. 将指定的共享内存,从⾃⼰的地址空间中去关联 int n = shmdt(shmaddr); assert(n != -1); (void)n; Log("detach shm done", Debug) << " shmid : " << shmid << endl; // 6. 删除共享内存,IPC_RMID即便是有进程和当下的shm挂接,依旧删除共享内存 n = shmctl(shmid, IPC_RMID, nullptr); assert(n != -1); (void)n; Log("delete shm done", Debug) << " shmid : " << shmid << endl; return 0; }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(3); } Log("attach shm success", Error) << " client key : " << TransToHex(k) << endl; // 4. 写 int fd = OpenFIFO(FIFO_NAME, O_WRONLY); while (true) { ssize_t s = read(0, shmaddr, SHM_SIZE - 1); if (s > 0) { shmaddr[s - 1] = 0; Signal(fd); if (strcmp(shmaddr, "quit") == 0) break; } } CloseFifo(fd); // 5. 去关联 int n = shmdt(shmaddr); assert(n != -1); Log("detach shm success", Error) << " client key : " << TransToHex(k) << endl; return 0; }