[linux仓库]图解System V共享内存:从shmget到内存映射的完整指南

[linux仓库]图解System V共享内存:从shmget到内存映射的完整指南


🌟 各位看官好,我是!

🌍 Linux == Linux is not Unix !


🚀 今天来学习System V共享内存,从了解接口再到探查共享内存实现的原理。

👍 如果觉得这篇文章有帮助,欢迎您一键三连,分享更多人哦!

目录

System V共享内存

共享内存原理

创建共享内存 - 研究特性

shmget

创建共享内存

shmctl - 控制共享内存

shmat - 挂接共享内存

shmdt - 删除映射关系 

再探共享内存原理

特征总结

总结

扩展

 借助管道实现控制板共享内存


System V共享内存

我们前面说过,当需要有进程间通信的需求时,程序员为了偷懒选择了复用之前的代码,创建了管道,而管道的原理是基于文件的.随着时代的发展,程序员逐渐发现有一些是管道不能解决的问题,因此不得不真正的创建了一个可以进行通信的资源.此时,在上层就有公司定制了一套System V标准,由其他公司按照这个标准进行不同的实现.

共享内存区是最快的IPC形式。⼀旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执⾏进⼊内核的系统调⽤来传递彼此的数据

共享内存原理

  1. 共享内存是实实在在地在物理内存中开辟内存块;
  2. 有了物理地址,那么就需要与虚拟地址建立映射关系;
  3. 如果要释放共享内存时,就需要删除映射关系.

而这些操作只有OS能做到,而OS又对上提供了一套系统调用,则用户就可以通过系统调用完成.

既然能开辟内存空间,那么如果有多个进程开辟多个内存空间,0s要知道哪些内存空间是由谁开辟的,这块内存空间是刚建立的还是正在销毁的 -->这决定了0s要管理内存空间 -->如何管理呢?

先描述 ! 在组织 !

创建共享内存 - 研究特性

shmget

IPC_CREAT单独:如果创建的共享内存不存在,就创建;如果存在,就获取IPC_EXCL:不能单独使用,没有意义IPC CREAT | IPC EXCL:如果共享内存不存在,就创建,返回的就是全新的shm ; 如果已经存在,出错返回!返回值:成功返回⼀个⾮负整数,即该共享内存段的标识码;失败返回-1

key是由用户进行传入,那为什么要这样做呢?为什么需要配合ftok函数进行使用呢?

  1. 公共标识符:进程A使用这个key(比如1234)创建了一块共享内存。之后,进程B也拿着同样的key(1234)向操作系统申请,操作系统一看key已经存在,就会把已经创建好的那块共享内存的访问权限也赋给进程B。这样,两个进程就指向了同一块物理内存,可以进行通信了。如果 key 是由内核随机生成并只返回给创建者,那其他进程就无从知晓了。
  2. 正如你的图中所展示的,直接在代码里写一个固定的数字(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; }

 

再探共享内存原理

特征总结

共享内存 = 共享内存内容 + 共享内存属性

优点: 

  1. 共享内存的生命周期随内核
  2. 共享内存是IPC中速度最快的!!!(因为减少了数据拷贝的次数,不需要调用系统调用)

 缺点:

  1.  共享内存,没有同步、互斥机制,来对多个进程的访问进行协同! 

总结

本文介绍了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; }

Read more

序列化和反序列化(Linux)

序列化和反序列化(Linux)

1 序列化和反序列化 write和read实质是拷贝函数 1.1序列化和反序列化的概述: 2网络版计算器 2.1代码实现 先把日志拷贝过来 2.1.1必须先要有网络功能 先把 TcpServer.hpp编写号 #pragma once #include <cstdint> #include "Socket.hpp" #include "./logs/ljwlog.h" class TcpServer { public: TcpServer() {} bool InitServer() {} void Start() {} ~TcpServer() {} private: uint16_t port; }; 2.1.2 把套接字接口封装一下方便使用 #pragma

By Ne0inhk
Flutter 三方库 fft 的鸿蒙化适配指南 - 实现端侧高性能快速傅里叶变换、支持音频频谱分析与信号处理域的频域特征提取实战

Flutter 三方库 fft 的鸿蒙化适配指南 - 实现端侧高性能快速傅里叶变换、支持音频频谱分析与信号处理域的频域特征提取实战

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 fft 的鸿蒙化适配指南 - 实现端侧高性能快速傅里叶变换、支持音频频谱分析与信号处理域的频域特征提取实战 前言 在进行 Flutter for OpenHarmony 的音频可视化、语音识别前置预处理或振动传感器信号分析应用开发时,将信号从“时域(Time Domain)”转换到“频域(Frequency Domain)”是不可逾越的基础步。快速傅里叶变换(FFT)是处理这类实时计算的工业级标准算法。fft 库为 Dart 提供了纯净且经过高度优化的 FFT 实现。本文将探讨如何在鸿蒙端构建极致的信号分析链路。 一、原直观解析 / 概念介绍 1.1 基础原理 FFT 是一种通过减少计算冗余来实现离散傅里叶变换(DFT)的加速算法(将复杂度从 $O(

By Ne0inhk
Flutter 组件 shelf_router 的适配 鸿蒙Harmony 实战 - 驾驭官方标准路由器架构、实现鸿蒙端 HTTP 流量精密分发与逻辑路由审计方案

Flutter 组件 shelf_router 的适配 鸿蒙Harmony 实战 - 驾驭官方标准路由器架构、实现鸿蒙端 HTTP 流量精密分发与逻辑路由审计方案

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 组件 shelf_router 的适配 鸿蒙Harmony 实战 - 驾驭官方标准路由器架构、实现鸿蒙端 HTTP 流量精密分发与逻辑路由审计方案 前言 在鸿蒙(OpenHarmony)生态的分布式业务中继、政务级内嵌 API 管理平台以及需要承载大规模高频交互请求的各类全栈式应用开发中,“路由的精确支配与逻辑安全性”是决定系统架构稳健性的命门所在。面对包含上百个 RESTful 端点的复杂服务模型、需要动态解析包含 UUID、日期等多种格式的 URL 参数,或者是需要针对鸿蒙手机与智慧大屏执行差异化的路由匹配。如果仅仅依靠原始的字符串拆分或低性能的手写拦截逻辑。不仅会导致路由解析执行效率的低下,更会因为缺乏一套工业级的“官方契约”规范。引发鸿蒙端微服务接口在面对异常报文时的逻辑脆弱性风险。 我们需要一种“官方背书、匹配闭环”的路由艺术。 shelf_router 是一套由 Dart 官方团队维护的、

By Ne0inhk

Flutter 三方库 holiday_jp 的鸿蒙化适配指南 - 在鸿蒙系统上构建极致、透明、全维度的日本法定节假日(公休日)查询与日历调度引擎

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 holiday_jp 的鸿蒙化适配指南 - 在鸿蒙系统上构建极致、透明、全维度的日本法定节假日(公休日)查询与日历调度引擎 在鸿蒙(OpenHarmony)系统的全球化(Globalization)出海应用、针对日本市场的日程管理、财务结算系统(需考虑日本银行休假)或带有国际化特色的鸿蒙版日历组件中,如何瞬间获取任意年份日本的法定节假日、判定当前是否为公休日?holiday_jp 为开发者提供了一套工业级的、基于官方精细化数据集的日本节假日处理方案。本文将深入实战其在鸿蒙出海应用逻辑层中的应用。 前言 什么是 Holiday JP?它是一个专注于提供日本法定假期(祝日)数据的专业库。它涵盖了从传统的“元日”到现代的“体育之日”等所有官方假期,并能自动处理由于由于由于由于“振替休日(补休)”产生的动态调休逻辑。在 Flutter

By Ne0inhk