Linux 进程间通信之 System V 共享内存:IPC 的原理与实战


🔥草莓熊Lotso: 个人主页
❄️个人专栏: 《C++知识分享》 《Linux 入门到实践:零基础也能懂》
✨生活是默默的坚持,毅力是永久的享受!


🎬 博主简介:


文章目录

  • 前言:
  • [一. 共享内存核心原理:为什么它最快?](#一. 共享内存核心原理:为什么它最快?)
    • [1.1 核心设计思想](#1.1 核心设计思想)
    • [1.2 通信流程与地址空间示意图](#1.2 通信流程与地址空间示意图)
    • [1.3 核心特性](#1.3 核心特性)
  • [二. System V 共享内存核心 API 与内核数据结构](#二. System V 共享内存核心 API 与内核数据结构)
    • [2.1 内核管理数据结构](#2.1 内核管理数据结构)
    • [2.2 核心 API 详解](#2.2 核心 API 详解)
      • [2.2.1 ftok:生成唯一 Key(共享内存的 "身份证")](#2.2.1 ftok:生成唯一 Key(共享内存的 “身份证”))
      • [2.2.2 shmget:创建 / 获取共享内存](#2.2.2 shmget:创建 / 获取共享内存)
      • [2.2.3 shmat:挂载共享内存](#2.2.3 shmat:挂载共享内存)
      • [2.2.4 shmdt:脱离共享内存](#2.2.4 shmdt:脱离共享内存)
      • [2.2.5 shmctl:控制共享内存(核心功能:删除)](#2.2.5 shmctl:控制共享内存(核心功能:删除))
  • [三. 实战案例:基于封装类的共享内存通信](#三. 实战案例:基于封装类的共享内存通信)
    • [3.1 封装类核心逻辑解析(Shm.hpp)](#3.1 封装类核心逻辑解析(Shm.hpp))
    • [3.2 Writer 进程:写入数据到共享内存(Writer.cc)](#3.2 Writer 进程:写入数据到共享内存(Writer.cc))
    • [3.3 Reader 进程:从共享内存读取数据(Reader.cc)](#3.3 Reader 进程:从共享内存读取数据(Reader.cc))
    • [3.4 编译与运行](#3.4 编译与运行)
      • [3.4.1 Makefile](#3.4.1 Makefile)
      • [3.4.2 运行步骤与输出结果展示](#3.4.2 运行步骤与输出结果展示)
    • [3.5 残留共享内存清理(上面图中其实也有写)](#3.5 残留共享内存清理(上面图中其实也有写))
  • [四. 内核如何管理 System V 共享内存](#四. 内核如何管理 System V 共享内存)
  • [五. 关键问题与避坑指南](#五. 关键问题与避坑指南)
    • [5.1 共享内存的同步问题(核心坑!)](#5.1 共享内存的同步问题(核心坑!))
    • [5.2 共享内存的删除机制](#5.2 共享内存的删除机制)
    • [5.3 常见错误与排查](#5.3 常见错误与排查)
    • [5.4 共享内存与其他 IPC 的性能对比与总结](#5.4 共享内存与其他 IPC 的性能对比与总结)
  • 结尾:

前言:

在 Linux IPC 体系中,System V 共享内存是速度最快的进程间通信方式。与管道、命名管道需要通过内核缓冲区中转数据不同,共享内存直接将一块物理内存映射到多个进程的虚拟地址空间,进程间数据传递无需内核参与,仅需用户态的内存拷贝,效率远超其他 IPC 方式。本文将从原理、API、实战、内核管理四个维度,全面解析 System V 共享内存的底层逻辑与使用技巧。


一. 共享内存核心原理:为什么它最快?

1.1 核心设计思想

共享内存的本质是 内核维护的一块连续物理内存 ,内核通过特殊的内存管理机制(页表映射),将这块物理内存同时映射到多个进程的虚拟地址空间的 "共享区"(虚拟地址通常在 0xC0000000 附近)。此时,多个进程访问自己虚拟地址空间中的这块区域,本质上是访问同一份物理内存 ------ 数据传递无需经过内核转发,仅需一次用户态内存拷贝,这是其速度最快的核心原因。

1.2 通信流程与地址空间示意图

bash 复制代码
进程A虚拟地址空间                物理内存                进程B虚拟地址空间
+------------------------+       +----------------+       +------------------------+
| 0xC0000000  argv/environ |       |                |       | 0xC0000000  argv/environ |
| 栈                     |       |                |       | 栈                     |
| 堆                     |       |  共享内存块    |       | 堆                     |
| 未初始化数据(bss)     |       |  (内核维护)  |       | 未初始化数据(bss)     |
| 初始化数据              |<----->|  4096字节(页) |<----->| 初始化数据              |
| 0x08048000  文本段(代码)|       |                |       | 0x08048000  文本段(代码)|
+------------------------+       +----------------+       +------------------------+

1.3 核心特性

  • 无内核中转 :进程间数据直接通过物理内存交互,无系统调用开销(管道需read/write系统调用);
  • 生命周期随内核 :共享内存创建后,即使创建进程退出,内存块仍存在于内核中,需手动调用shmctl(IPC_RMID)删除;
  • 无同步与互斥:内核不提供数据访问的同步机制,多个进程同时写会导致数据混乱("临界区问题"),需配合信号量等工具实现同步;
  • 跨进程通信 :支持任意进程间通信(无需亲缘关系),只要进程持有相同的keyshmid
  • 大小建议 :共享内存大小最好是内存页(PAGE_SIZE,默认 4096 字节)的整数倍,避免内存碎片。

二. System V 共享内存核心 API 与内核数据结构

2.1 内核管理数据结构

内核通过struct shmid_ds管理共享内存的属性,是共享内存描述结构体的子集,结合 Linux 2.6.18 内核源码,核心字段如下:

cpp 复制代码
struct shmid_ds {
    struct ipc_perm shm_perm;  // 权限控制结构体(包含key、uid、gid、mode等)
    size_t shm_segsz;          // 共享内存大小(字节)
    pid_t shm_cpid;           // 创建进程PID
    pid_t shm_lpid;           // 最后一次操作该内存的进程PID
    unsigned short shm_nattch; // 当前挂载到该内存的进程数
    time_t shm_atime;          // 最后一次挂载时间(shmat调用时间)
    time_t shm_dtime;          // 最后一次脱离时间(shmdt调用时间)
    time_t shm_ctime;          // 最后一次属性修改时间
    void *shm_unused2;         // 预留字段(内核内部使用)
};


struct ipc_perm是 System V IPC(共享内存、消息队列、信号量)的通用权限结构体,内核通过该结构体的key字段唯一标识一个 IPC 资源。

2.2 核心 API 详解

System V 共享内存的使用流程 遵循 "生成 Key→创建 / 获取共享内存→挂载→读写→脱离→删除" ,核心 API 包括ftokshmgetshmatshmdtshmctl,逐一解析如下:

2.2.1 ftok:生成唯一 Key(共享内存的 "身份证")

用于将 "文件路径 + 项目 ID" 转换为唯一的key_t类型值,作为共享内存的全局标识 ------ 多个进程通过相同的key可获取同一块共享内存。

cpp 复制代码
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
  • 参数细节
    • pathname:必须是系统中已存在的文件路径(如"/home"),且调用进程对该文件有访问权限
    • proj_id:非 0 的 8 位整数(如0x6666),不同的proj_id会生成不同的key(即使路径相同);
  • 返回值 :成功返回唯一key,失败返回 - 1(errno会标识错误原因,如文件不存在、权限不足)。

2.2.2 shmget:创建 / 获取共享内存

用于创建新的共享内存或获取已存在的共享内存,返回共享内存标识符(shmid),后续操作均通过shmid关联共享内存。

cpp 复制代码
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);

参数深度解析

  • key:ftok生成的唯一 Key;
  • size:共享内存大小(建议为 4096 的整数倍),创建时需指定,获取时可设为 0;
  • shmflg :权限标志组合,核心组合:
    • IPC_CREAT:若共享内存不存在则创建,存在则直接获取(常用)
    • IPC_CREAT | IPC_EXCL:若共享内存已存在则报错(确保创建全新内存,避免覆盖);
    • 权限位(如0666):控制进程对共享内存的访问权限(与文件权限规则一致);

返回值 :成功返回shmid(非负整数),失败返回 - 1。

  • 关于key值是什么的补充

2.2.3 shmat:挂载共享内存

将共享内存映射到当前进程的虚拟地址空间,返回映射后的虚拟地址指针 ------ 进程通过该指针读写共享内存。

cpp 复制代码
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);

参数细节

  • shmid:shmget返回的共享内存标识符;
  • shmaddr:指定挂载的虚拟地址(NULL 表示由内核自动分配,推荐使用);
  • shmflg :挂载标志:
    • 0:可读可写挂载;
    • SHM_RDONLY:只读挂载(进程无写权限);
    • SHM_RND:若shmaddr非 NULL,将挂载地址向下调整为SHMLBA(内存页边界)的整数倍;

返回值 :成功返回虚拟地址指针,失败返回(void*)-1

2.2.4 shmdt:脱离共享内存

将共享内存从当前进程的虚拟地址空间中脱离(解除映射关系),并非删除共享内存

cpp 复制代码
#include <sys/shm.h>
int shmdt(const void *shmaddr);
  • 参数shmaddrshmat返回的虚拟地址指针;
  • 关键注意
    • 脱离后,进程无法再访问该共享内存,但共享内存本身仍存在于内核中;
    • 若进程未调用shmdt就退出,内核会自动解除映射(避免内存泄漏);
  • 返回值:成功返回 0,失败返回 - 1。

2.2.5 shmctl:控制共享内存(核心功能:删除)

用于获取共享内存属性、修改属性或删除共享内存,是共享内存生命周期管理的核心 API。

cpp 复制代码
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

参数深度解析

  • shmid:共享内存标识符;
  • cmd:控制命令(核心 3 种):
命令 功能描述
IPC_STAT 获取共享内存属性,存入buf指向的shmid_ds结构体(如查询挂载进程数、大小)
IPC_SET 修改共享内存属性(需进程有CAP_SYS_ADMIN权限),属性值从buf读取
IPC_RMID 标记共享内存为 "待删除",后续新进程无法挂载,所有进程脱离后内核释放内存
  • buf :存储属性的结构体指针(IPC_RMID时可设为 NULL);

返回值:成功返回 0,失败返回 - 1。


三. 实战案例:基于封装类的共享内存通信

提供Shm.hpp封装类对上述核心 API进行完整封装,无需修改即可使用。结合Writer.cc(写进程)和Reader.cc(读进程),实现跨进程数据读写。

3.1 封装类核心逻辑解析(Shm.hpp)

Shm.hpp封装了 "生成 Key→创建 / 获取→挂载→删除→属性查询" 的全流程,核心接口与 API 映射关系如下:

函数名 调用示例 功能描述
Create() `shmget(key, size, IPC_CREAT IPC_EXCL
Get() shmget(key, size, IPC_CREAT) 获取已存在的共享内存
Attch() shmat(shmid, NULL, 0) 挂载共享内存,返回虚拟地址指针
Delete() shmctl(shmid, IPC_RMID, NULL) 删除共享内存
GetShmAttr() shmctl(shmid, IPC_STAT, &ds) 获取共享内存属性(PID、大小、Key)
Debug() - 打印shmid、size、key(调试用)
cpp 复制代码
#ifndef __SHM_HPP__
#define __SHM_HPP__

#include <iostream>
#include <cstdio>
#include <sys/shm.h>
#include <string.h>
#include <unistd.h>

const std::string proj_name = "/home";
const int proj_id = 0x6666;
const int g_size = 4096;

static std::string ToHex(long long data)
{
    char buf[16];
    snprintf(buf, sizeof(buf), "0x%llx", data);
    return buf;
}

class Shm
{
public:
    Shm(int size = g_size): _shmid(-1), _size(size), _key(0)
    {}
    ~Shm() {}
private:
    key_t GetKey()
    {
        _key = ftok(proj_name.c_str(), proj_id);
        if(_key < 0)
        {
            perror("ftok");
        }
        return _key;
    }
    bool CreateCoreHelper(int flags)
    {
        // 1. 获取key值
        key_t key = GetKey();
        // 2. 创建共享内存
        _shmid = shmget(key, _size, flags);
        if(_shmid < 0)
        {
            perror("shmget");
            return false;
        }
        return true;
    }
public:
    // 1.创建共享内存
    bool Create()
    {
        return CreateCoreHelper(IPC_CREAT | IPC_EXCL | 0666);
    }
    // 2.获取共享内存
    bool Get()
    {
        return CreateCoreHelper(IPC_CREAT);
    }
    // 3. 删除共享内存
    bool Delete()
    {
        int n = shmctl(_shmid, IPC_RMID, nullptr);
        return n < 0 ? false : true;
    }
    // 4. 获取共享内存属性
    void GetShmAttr()
    {
        struct shmid_ds ds;
        int n = shmctl(_shmid, IPC_STAT, &ds);
        if(n < 0)
        {
            perror("shmctl");
            return ;
        }
        std::cout << ds.shm_cpid << std::endl;
        std::cout << ds.shm_segsz << std::endl;
        std::cout << ToHex(_key) << std::endl;
    }
    // 5. 共享内存映射挂载
    void *Attch()
    {
        _start = (char *)shmat(_shmid, nullptr, 0);
        return _start;
    }
    // 6. 共享内存去关联
    void Detach()
    {
        int n = shmdt(_start);
        (void)n;
    }
    void Debug()
    {
        std::cout << "shmid: " << _shmid << std::endl;
        std::cout << "size: " << _size << std::endl;
        std::cout << "key: " << ToHex(_key) << std::endl;
    }
private:
    int _shmid;
    int _size;
    key_t _key;
    char *_start;
};

typedef struct data
{
    int count;
    char buf[26 * 2];
}buffer_t;
#endif

3.2 Writer 进程:写入数据到共享内存(Writer.cc

cpp 复制代码
// header only
#include "Shm.hpp"
#include <iostream>
#include <string>
#include <unistd.h>

Shm shm;
class Init
{
public:
    Init()
    {
        shm.Get();
        addr = (char*)shm.Attch();
        std::cout << "addr: " << ToHex((long long)addr) << std::endl;
    }
    ~Init()
    {
        shm.Detach();
    }
    char *Addr()
    {
        return addr;
    }
public:
    char* addr;
};

Init init;
// Write.
int main()
{
    std::cout << "test begin..." << std::endl;

    buffer_t *shmbuf = (buffer_t*)init.Addr();
    shmbuf->count = 0;
    memset(shmbuf->buf, 0, 4096);

    char ch = 'A';
    for(int i = 0; i < 26 * 2; i += 2, ch++)
    {
        shmbuf->buf[i] = ch;
        usleep(234219);
        shmbuf->buf[i + 1] = ch;
        usleep(734217);
        shmbuf->count++;
        usleep(734217);

        sleep(1);
    }
    return 0;
}

3.3 Reader 进程:从共享内存读取数据(Reader.cc

cpp 复制代码
#include "Shm.hpp"
#include <iostream>
#include <string>
#include <unistd.h>

// // RAII
// Shm shm;
// class Init
// {
// public:
//     Init()
//     {
        
//     }
//     ~Init()
//     {
//         std::cout << "~Init()" << std::endl;
//     }
// };
// Writer -> shm -> Reader
int main()
{
    // 在内核中创建共享内存
    Shm shm;
    shm.Create();
    char *addr = (char*)shm.Attch();


    buffer_t *shmbuf = (buffer_t*)addr;
    int old = shmbuf->count; // 实现一个简单的保护和同步机制
    while(true)
    {
        if(old != shmbuf->count)
        {
            std::cout << "count: " << shmbuf->count << std::endl;
            std::cout << "data: " << shmbuf->buf << std::endl;
            old = shmbuf->count;
        }
        usleep(74329);

        if(shmbuf->count >= 26)
        {
            break;
        }
    }
    
    // shm.Debug();
    // shm.GetShmAttr();

    // 删除共享内存   shm.Detach();
    shm.Delete();

    return 0;
}

3.4 编译与运行

3.4.1 Makefile

bash 复制代码
all:Reader Writer
Reader:Reader.cc
	g++ -o $@ $^ -std=c++11
Writer:Writer.cc
	g++ -o $@ $^ -std=c++11

.Phony:clean
clean:
	rm -f Reader Writer

3.4.2 运行步骤与输出结果展示

  • 步骤一:先运行./Reader
  • 步骤二:再运行./Writter

重要知识点图示理解

3.5 残留共享内存清理(上面图中其实也有写)

若进程异常退出导致共享内存未删除,可通过以下命令手动清理:

bash 复制代码
# 查看所有System V共享内存
ipcs -m

# 删除指定shmid的共享内存(如shmid=688145)
ipcrm -m 688145

四. 内核如何管理 System V 共享内存

根据附录的内核源码解析,内核通过struct ipc_idsstruct shmid_kernel管理所有共享内存资源,核心逻辑如下:

  • 全局管理结构 :内核维护shm_ids全局变量(struct ipc_ids类型),记录系统中所有共享内存的元数据(如max_idin_useentries数组);
  • 索引机制struct ipc_id_aryentries数组存储struct kern_ipc_perm指针,内核通过shmid索引到对应的共享内存权限结构体;
  • 物理内存关联struct shmid_kernel包含struct file *shm_file字段,通过文件系统的inodevm_area_struct实现物理内存与进程虚拟地址的映射。

简单来说:内核将共享内存抽象为一种特殊的 IPC 资源,通过 "Key→shmid→内核数据结构→物理内存" 的链路,实现对共享内存的创建、挂载、脱离、删除等操作的统一管理。


五. 关键问题与避坑指南

5.1 共享内存的同步问题(核心坑!)

共享内存本身无同步与互斥机制,若多个进程同时写入,会导致数据覆盖(如进程 A 写 "hello",进程 B 同时写 "world",最终可能得到 "hwllo" 等混乱数据)------ 这是 PPT 反复强调的重点问题。

解决方案:

  • 配合 System V 信号量:用信号量的 P/V 操作(申请 / 释放资源)保护临界区,确保同一时间仅一个进程访问共享内存;
  • 管道通知机制:如 PPT 实例 2 所示,用命名管道实现 "信号唤醒"(Writer 写完成后向管道发信号,Reader 收到信号后再读);
  • 文件锁:通过fcntl函数给共享内存关联的文件加锁,实现简单的互斥访问。

5.2 共享内存的删除机制

  • shmctl(shmid, IPC_RMID, NULL)的作用是 "标记删除",而非 "立即删除":
    • 标记后,新进程调用shmget无法获取该共享内存;
    • 已挂载的进程仍可正常读写,直到所有进程调用shmdt脱离;
    • 最后一个进程脱离后,内核才会真正释放物理内存。
  • 若未调用IPC_RMID,共享内存会一直残留于内核中,直到系统重启(需手动清理)。

5.3 常见错误与排查

错误现象 原因分析 解决方案
shmget报错 "File exists" 使用 `IPC_CREAT IPC_EXCL` 创建已存在的内存
shmat返回(void*)-1 权限不足(如创建时权限为 0600) 创建时指定0666权限
读取数据为空或乱码 1. Writer 未写入就读取;2. 无同步机制 增加sleep延迟或实现同步机制
进程退出后内存未释放 未调用shmctl(IPC_RMID) ipcs -m查询 +ipcrm -m shmid手动删除

5.4 共享内存与其他 IPC 的性能对比与总结

IPC 方式 数据传递路径 核心开销 适用场景 速度排名
匿名管道 进程 A→内核缓冲区→进程 B 2 次系统调用 + 2 次内存拷贝 亲缘进程、简单数据流 3
命名管道 进程 A→内核缓冲区→进程 B 2 次系统调用 + 2 次内存拷贝 任意进程、简单数据流 2
System V 共享内存 进程 A→共享内存→进程 B 0 次系统调用 + 1 次内存拷贝 高频 / 大数据量跨进程通信 1

结尾:

html 复制代码
🍓 我是草莓熊 Lotso!若这篇技术干货帮你打通了学习中的卡点:
👀 【关注】跟我一起深耕技术领域,从基础到进阶,见证每一次成长
❤️ 【点赞】让优质内容被更多人看见,让知识传递更有力量
⭐ 【收藏】把核心知识点、实战技巧存好,需要时直接查、随时用
💬 【评论】分享你的经验或疑问(比如曾踩过的技术坑?),一起交流避坑
🗳️ 【投票】用你的选择助力社区内容方向,告诉大家哪个技术点最该重点拆解
技术之路难免有困惑,但同行的人会让前进更有方向~愿我们都能在自己专注的领域里,一步步靠近心中的技术目标!

结语:System V 共享内存是 Linux 中效率最高的 IPC 方式,核心优势在于 "无内核中转、用户态直接通信"。共享内存适合高频、大数据量的跨进程通信场景(如服务器集群数据共享、高频交易系统、视频流传输)。若需实现安全的同步通信,可后续学习 System V 信号量的使用,将二者结合实现 "高效 + 安全" 的跨进程通信。创作不易,觉得有帮助的话,欢迎点赞、收藏、关注三连~ 后续会持续更新 Linux IPC 系列(信号量、消息队列),带你从底层吃透进程间通信技术。

✨把这些内容吃透超牛的!放松下吧✨ ʕ˘ᴥ˘ʔ づきらど

相关推荐
郑泰科技2 小时前
一键脚本安装OpenClaw时遇到问题怎么办?
人工智能·深度学习·agi
qianbo_insist2 小时前
鱼眼图像的三维投影逆变换和AI计算
人工智能·opencv·算法
云边云科技_云网融合2 小时前
百度首页中宇联云计算SD-AIoT:万物互联时代,从 “能连上” 到 “用得放心” 的技术革命
网络·数据库·人工智能
挂科边缘2 小时前
字节跳动 AI 原生 IDE Trae 安装与上手图文教程
ide·人工智能·trae
洛菡夕2 小时前
nginx性能调优与深度监控
linux·服务器·nginx
tongxh4232 小时前
Nginx搭建负载均衡
运维·nginx·负载均衡
放下华子我只抽RuiKe52 小时前
机器学习启航:从数据直觉到模型构建的第一块基石
人工智能·深度学习·机器学习·语言模型·数据挖掘·语音识别·聚类
物联网软硬件开发-轨物科技2 小时前
【技术白皮书】开关柜运维的“代际跨越”:全栈数字化如何重塑一键顺控新范式
运维
尽兴-2 小时前
从零到精通:Redis 7 核心数据结构实战与单机部署指南
数据结构·数据库·redis·部署·redis7