news 2026/5/26 19:09:31

从零吃透 Linux System V 共享内存:从原理到代码实战全记录

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零吃透 Linux System V 共享内存:从原理到代码实战全记录

最近在学习 Linux 系统编程,进程间通信(IPC)是绕不开的核心重点。从匿名管道到命名管道,我一步步走过来,直到接触到共享内存 —— 这个所有 IPC 方式中速度最快的 "王者"。

学习过程中我踩了不少坑,也产生了很多疑问:为什么 key 不能让内核自动生成?shmget 报错 "File exists" 到底是哪里出了问题?key 和 shmid 到底有什么区别?

今天我就把自己的完整学习笔记、代码实战和踩坑经验整理出来,从底层原理到代码实现,带你彻底搞懂共享内存。

一、为什么需要共享内存?—— 从管道的局限性说起

在学习共享内存之前,我已经掌握了管道通信:

  • 匿名管道:只能用于有亲缘关系的进程(父子、兄弟)之间
  • 命名管道:可以用于无关进程,但本质还是基于文件

但管道有一个致命的性能问题:数据需要两次拷贝

  1. 写进程:用户缓冲区 → 内核管道缓冲区
  2. 读进程:内核管道缓冲区 → 用户缓冲区

这就像两个人传递东西,必须经过一个中间人,效率自然不高。我当时就在想:有没有一种方式,能让两个进程直接读写同一块内存,不需要内核中转?

答案就是:共享内存

二、共享内存的核心原理:让不同进程看到同一份资源

1. 本质到底是什么?

共享内存的核心思想其实非常朴素:

操作系统在物理内存中开辟一块独立的区域,让多个进程通过页表映射,将这块物理内存映射到自己的虚拟地址空间中。

这样一来:

  • 我往这块内存写一个字符,另一个进程立刻就能看到
  • 不需要任何系统调用,直接用指针读写
  • 零拷贝,这就是它成为最快 IPC 方式的根本原因

2. 内核如何管理共享内存?

Linux 内核有一个非常经典的设计原则:先描述,再组织

对于共享内存也不例外:

  • 每一块共享内存都对应一个struct shmid_ds结构体(共享内存描述符)
  • 内核用一个数组管理所有的共享内存描述符

这个结构体包含了共享内存的所有元数据:

struct shmid_ds { struct ipc_perm shm_perm; // 权限信息(包含key、uid、gid、mode) size_t shm_segsz; // 共享内存大小(字节) time_t shm_atime; // 最后一次挂接时间 time_t shm_dtime; // 最后一次脱接时间 time_t shm_ctime; // 最后一次修改时间 pid_t shm_cpid; // 创建进程PID pid_t shm_lpid; // 最后操作进程PID shmatt_t shm_nattch;// 当前挂接的进程数量 };

3. 我最困惑的问题:key vs shmid

这是我学习过程中最容易混淆的两个概念,也是我第一个问老师的问题。

标识所属层面作用谁生成
key_t key内核层面在内核中唯一标识一块共享内存用户通过ftok生成
int shmid用户层面用户进程操作共享内存的句柄内核通过shmget返回

为什么 key 不能由内核自动生成?这是我当时最想不通的问题。既然 key 是用来唯一标识的,让内核随机生成一个不就行了吗?

后来我才明白:

内核可以生成唯一 ID,但无法帮两个毫无关系的进程 "提前约定" 同一个 ID。

如果 key 由内核随机生成:

  • 进程 A 创建共享内存,得到一个随机 key
  • 进程 B 根本不知道这个 key 是什么,永远找不到这块共享内存

而用户通过ftok(固定路径, 固定ID)生成的 key 是稳定、可复现的。两个进程只要提前约定好路径和 ID,就能生成同一个 key,找到同一块共享内存。

三、5 个核心函数详解:从入门到踩坑

1. ftok:生成约定好的 key

key_t ftok(const char *pathname, int proj_id);
  • 作用:将一个存在的文件路径和一个 0~255 的数字,转换成唯一的key_t
  • 原理:提取文件的inode号,与proj_id组合生成 key
  • 我踩过的坑
    • pathname必须真实存在,否则返回 - 1
    • 文件不能被删除重建,否则inode会变,key 也会变
    • proj_id只用低 8 位,超过 255 的部分会被截断

2. shmget:创建 / 获取共享内存

int shmget(key_t key, size_t size, int shmflg);
  • 作用:根据 key 创建新的共享内存,或获取已存在的共享内存
  • 参数
    • keyftok生成的唯一标识
    • size:共享内存大小,必须是 4096(页大小)的整数倍
    • shmflg:操作标志
      • IPC_CREAT:不存在则创建,存在则获取
      • IPC_EXCL:与IPC_CREAT一起使用,存在则报错
      • 0666:设置读写权限

我踩过的最大的坑:shmget: File exists我第一次运行代码的时候就遇到了这个错误,当时还以为是 shmid 冲突了,后来才搞清楚:

这是因为我用了IPC_CREAT | IPC_EXCL,而系统中已经存在相同 key 的共享内存了。

这是IPC_EXCL的保护机制,确保你拿到的是全新的共享内存。解决方法就是用ipcrm命令删除旧的共享内存。

3. shmat:挂接共享内存

void *shmat(int shmid, const void *shmaddr, int shmflg);
  • 作用:将共享内存映射到当前进程的虚拟地址空间
  • 最佳实践shmaddrnullptr让内核自动选择地址,shmflg0表示可读写
  • 注意:失败判断必须强转成long long,因为 64 位地址直接和 - 1 比较会出错:
if ((long long int)_start_addr == -1) exit(3);

4. shmdt:脱接共享内存

int shmdt(const void *shmaddr);
  • 作用:取消共享内存与当前进程的映射关系
  • 注意:这只是断开连接,并不会删除共享内存

5. shmctl:控制共享内存

int shmctl(int shmid, int cmd, struct shmid_ds *buf);
  • 作用:对共享内存进行各种管理操作
  • 我最常用的两个命令
    • IPC_STAT:获取共享内存属性,方便调试
    • IPC_RMID:标记共享内存为删除状态,等所有进程脱接后真正删除

四、我的完整代码实现:客户端 - 服务端通信

我把共享内存的所有操作封装成了一个Shm类,这样用起来更方便,也更符合面向对象的设计思想。

1. 文件结构

  • Shm.hpp:共享内存核心封装类
  • Server.cc:服务端(创建共享内存 + 读数据)
  • Client.cc:客户端(获取共享内存 + 写数据)
  • Makefile:一键编译脚本

2. 核心封装:Shm.hpp

#pragma once #include <iostream> #include <cstdio> #include <unistd.h> #include <cstdlib> #include <sys/shm.h> const int gsize = 128; // 4096的整数倍 #define PATHNAME "/tmp" #define PROJ_ID 0x66 class Shm { public: Shm() : _shmid(-1), _size(gsize), _start_addr(nullptr) { } void Delete() { int n = shmctl(_shmid, IPC_RMID, nullptr); (void)n; } void Attach() { _start_addr = shmat(_shmid, nullptr, 0); if ((long long int)_start_addr == -1) exit(3); } void PrintAttr() { struct shmid_ds ds; int n = shmctl(_shmid, IPC_STAT, &ds); if(n < 0) { perror("shmctl"); exit(4); } printf("key: 0x%x\n", ds.shm_perm.__key); printf("shm_nattch: %ld\n", ds.shm_nattch); printf("shm_segsz: 0x%lx\n", ds.shm_segsz); } void Detach() { int n = shmdt(_start_addr); (void)n; } void Get() { GetHelper(IPC_CREAT); } void Create() { GetHelper(IPC_CREAT | IPC_EXCL | 0666); } void *Addr() { return _start_addr; } int Size() { return _size; } ~Shm() {} private: key_t GetKey() { return ftok(PATHNAME, PROJ_ID); } void GetHelper(int shmflg) { key_t k = GetKey(); if (k < 0) { std::cerr << "GetKey error"; exit(1); } _shmid = shmget(k, _size, shmflg); if (_shmid < 0) { perror("shmget"); exit(2); } printf("key=0x%x, _shmid = %d\n", k, _shmid); } private: int _shmid; int _size; void *_start_addr; };

我觉得设计得比较好的地方

  • 面向对象封装,屏蔽了底层系统调用的复杂性
  • 单一职责:Create()负责创建新共享内存,Get()负责获取已存在的
  • 代码复用:GetHelper()统一处理shmget的逻辑
  • 调试友好:PrintAttr()可以打印共享内存的所有内核信息

3. 服务端实现:Server.cc

服务端是共享内存的创建者和管理者:

#include "Shm.hpp" int main() { Shm sharedmem; // 创建新的共享内存(确保唯一) sharedmem.Create(); // 挂接到进程地址空间 sharedmem.Attach(); sleep(2); // 打印共享内存属性 sharedmem.PrintAttr(); 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(1); } // 注意:死循环导致这里永远执行不到 sharedmem.Detach(); sharedmem.Delete(); return 0; }

4. 客户端实现:Client.cc

客户端是共享内存的使用者:

#include "Shm.hpp" int main() { Shm sharedmem; // 获取已存在的共享内存 sharedmem.Get(); // 挂接到进程地址空间 sharedmem.Attach(); sleep(2); // 打印共享内存属性 sharedmem.PrintAttr(); char *shm_start = (char *)sharedmem.Addr(); int size = sharedmem.Size(); int index = 0; // 从键盘输入,写入共享内存 while (true) { std::cout << "Please Enter@ "; std::cin >> *shm_start; shm_start++; index %= size; // 防止越界 } sharedmem.Detach(); return 0; }

5. 运行效果

  1. 编译:make
  2. 先运行服务端:./Server
  3. 再运行客户端:./Client
  4. 在客户端输入字符,服务端会立刻打印出来

那种 "我写什么,另一个进程立刻就能看到" 的感觉,真的很神奇!

五、共享内存的特点与常见问题

1. 核心特点

  • 优点
    • 速度最快:直接内存访问,零拷贝
    • 无需系统调用:映射后直接用指针读写
    • 支持大量数据传输
  • 缺点
    • 没有同步互斥机制:多个进程同时读写会导致数据混乱
    • 生命周期随内核:用户不主动删除,会一直存在直到系统重启
    • 没有访问控制:任何进程只要知道 key 就能访问

2. 最常见的坑:共享内存残留

这是我踩过最多的坑。程序异常退出(比如 Ctrl+C),没有调用shmctl(IPC_RMID),导致共享内存一直残留在系统中。

解决方法

  1. 查看系统中的共享内存:ipcs -m
  2. 删除残留的共享内存:ipcrm -m <shmid>ipcrm -M <key>
  3. 优化代码:在析构函数中自动释放共享内存:
    ~Shm() { if (_shmid != -1) { Detach(); Delete(); } }

3. 下一个要解决的问题:同步机制

现在的代码中,客户端写和服务端读是完全异步的,会导致数据混乱。接下来我打算学习信号量,给共享内存加上同步互斥机制,实现更可靠的进程间通信。

六、学习总结

通过这次学习,我不仅掌握了共享内存的使用方法,更理解了 Linux 内核 "先描述,再组织" 的设计哲学。

回顾整个学习过程,我觉得最重要的是搞清楚三个问题:

  1. 是什么:共享内存是让多个进程映射同一块物理内存
  2. 为什么:为了实现最快的进程间通信
  3. 怎么用:ftok 生成 key → shmget 创建 / 获取 → shmat 挂接 → 读写 → shmdt 脱接 → shmctl 删除

共享内存是 Linux 系统编程中非常重要的一部分,它的高性能特点使其在高并发、大数据传输的场景中得到广泛应用。希望我的这篇学习笔记能帮到同样在学习的你。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/26 19:06:24

Ryujinx模拟器:在PC上体验Switch游戏的终极解决方案

Ryujinx模拟器&#xff1a;在PC上体验Switch游戏的终极解决方案 【免费下载链接】Ryujinx 用 C# 编写的实验性 Nintendo Switch 模拟器 项目地址: https://gitcode.com/GitHub_Trending/ry/Ryujinx 想在电脑上畅玩Nintendo Switch游戏吗&#xff1f;Ryujinx模拟器为你打…

作者头像 李华
网站建设 2026/5/26 19:05:42

LVGL下拉列表控件实战:从静态选项到动态事件响应的完整开发流程

LVGL下拉列表控件实战&#xff1a;从静态选项到动态事件响应的完整开发流程在嵌入式GUI开发中&#xff0c;下拉列表是最常用的交互控件之一。它能在有限屏幕空间内高效组织大量选项&#xff0c;同时保持界面简洁。LVGL作为轻量级嵌入式图形库&#xff0c;其下拉列表控件支持丰富…

作者头像 李华
网站建设 2026/5/26 19:04:25

Ubuntu 22.04 SSH安全配置全指南:从启用到加固

1. 为什么默认不开启SSH&#xff1f;这不是“多此一举”&#xff0c;而是安全设计的起点很多人第一次装完 Ubuntu 22.04 LTS&#xff0c;兴冲冲想用另一台电脑 ssh 连过去&#xff0c;敲下ssh user192.168.x.x却收到Connection refused&#xff0c;第一反应是“系统坏了”或者“…

作者头像 李华
网站建设 2026/5/26 19:02:21

通过 Python 调用 Taotoken 实现多模型自动切换与降级策略

&#x1f680; 告别海外账号与网络限制&#xff01;稳定直连全球优质大模型&#xff0c;限时半价接入中。 &#x1f449; 点击领取海量免费额度 通过 Python 调用 Taotoken 实现多模型自动切换与降级策略 在构建依赖大模型能力的应用时&#xff0c;服务的稳定性至关重要。单一…

作者头像 李华
网站建设 2026/5/26 19:01:48

PyTorch实战:MaxPool2d参数调优避坑指南(附代码与可视化)

PyTorch实战&#xff1a;MaxPool2d参数调优避坑指南&#xff08;附代码与可视化&#xff09;在构建卷积神经网络时&#xff0c;池化层看似简单却暗藏玄机。许多开发者习惯性地复制粘贴MaxPool2d的默认参数&#xff0c;直到模型精度不如预期时才意识到问题所在。本文将带您深入理…

作者头像 李华