《Linux系统编程》Linux 进程间通信之 System V 共享内存:IPC 底层原理与实战

🔥小叶-duck个人主页

❄️个人专栏《Data-Structure-Learning》《C++入门到进阶&自我学习过程记录》

《Linux操作系统从入门到实践》《Qt从入门到实践》

《算法题讲解指南》--优选算法

《算法题讲解指南》--递归、搜索与回溯算法

《算法题讲解指南》--动态规划算法

未择之路,不须回头
已择之路,纵是荆棘遍野,亦作花海遨游


目录

[一. 共享内存核心原理:为什么它最快?](#一. 共享内存核心原理:为什么它最快?)

[1.1 核心设计思想](#1.1 核心设计思想)

[1.2 通信流程与地址空间示意图](#1.2 通信流程与地址空间示意图)

[1.3 共享内存核心特性](#1.3 共享内存核心特性)

二、共享内存数据结构

[2.1 内核管理共享内存的数据结构](#2.1 内核管理共享内存的数据结构)

三、共享内存核心API详解

[3.1 ftok:生成唯一 Key](#3.1 ftok:生成唯一 Key)

[3.2 shmget:创建 / 获取共享内存](#3.2 shmget:创建 / 获取共享内存)

[3.3 shmat:挂载共享内存](#3.3 shmat:挂载共享内存)

[3.4 shmdt:脱离共享内存](#3.4 shmdt:脱离共享内存)

[3.5 shmctl:控制共享内存(核心功能:删除)](#3.5 shmctl:控制共享内存(核心功能:删除))

[四. 实战案例:基于封装类的共享内存通信](#四. 实战案例:基于封装类的共享内存通信)

[4.1 封装类核心逻辑解析(Shm.hpp)](#4.1 封装类核心逻辑解析(Shm.hpp))

[4.2 客户端进程:写入数据到共享内存(client.cc)](#4.2 客户端进程:写入数据到共享内存(client.cc))

[4.3 服务端进程:创建共享内存并从共享内存读取数据(server.cc)](#4.3 服务端进程:创建共享内存并从共享内存读取数据(server.cc))

[4.4 编译与运行](#4.4 编译与运行)

[4.5 残留共享内存清理](#4.5 残留共享内存清理)

[五. 内核如何管理 System V 共享内存](#五. 内核如何管理 System V 共享内存)

[六. 关键问题分析](#六. 关键问题分析)

[6.1 共享内存的同步问题](#6.1 共享内存的同步问题)

[6.2 共享内存的删除机制](#6.2 共享内存的删除机制)

[6.3 常见错误与排查](#6.3 常见错误与排查)

结束语


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

1.1 核心设计思想

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

简单理解:

倘若在物理内存中申请一段连续空间,记录好该物理内存的起始地址空间大小 ,再将这块物理内存映射至进程 A 的用户共享区 ,进程 A 便会得到一段对应的虚拟起始地址,后续依托该虚拟地址即可直接完成内存读写。同理,进程 B 也能在自身地址空间内划分一块区域,建立同一片物理共享内存的映射关系,获取属于自己的虚拟地址;两个进程只需借助各自虚拟地址搭配偏移 ,就能访问同一块物理内存的数据。动态链接库的加载 本质就依托这类映射思想 ,依靠页表映射机制,让多个进程的虚拟地址指向同一片物理内存,该实现进程数据互通的技术,便是共享内存

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

bash 复制代码
  进程A的地址空间              共享物理内存                进程B的地址空间
+----------------+        +----------------+        +----------------+
|    用户数据     |        |                |        |     用户数据     |
+----------------+        |                |        +----------------+
|                |        |                |        |                |
|    [映射区]     | <----> |    共享内存     | <----> |     [映射区]    |
|                |        |                |        |                |
+----------------+        +----------------+        +----------------+

1.3 共享内存核心特性

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

二、共享内存数据结构

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 资源

三、共享内存核心API详解

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

3.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 会标识错误原因,如文件不存在、权限不足)。

3.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。

3.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**。

3.4 shmdt:脱离共享内存

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

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

参数

  • **shmaddr:**shmat返回的虚拟地址指针;
  • 关键注意
    • 脱离后,进程无法再访问该共享内存,但共享内存本身仍存在于内核中
    • 若进程未调用 shmdt 就退出,内核会自动解除映射(避免内存泄漏);
  • 返回值:成功返回 0,失败返回 - 1。

3.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进行完整封装,无需修改即可使用。结合client.cc(写进程)server.cc(读进程),实现跨进程数据读写。

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

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

函数名 调用示例 功能描述
Create() shmget(key, size, IPC_CREAT|IPC_EXCL|0666) 创建全新共享内存
Get() shmget(key, size, IPC_CREAT) 获取已存在的共享内存
Attch() shmat(shmid, NULL, 0) 挂载共享内存,返回虚拟地址指针
Destroy()**** shmctl(shmid, IPC_RMID, NULL) 删除共享内存
GetShmAttr() shmctl(shmid, IPC_STAT, &ds) 获取共享内存属性(PID、大小、Key)
cpp 复制代码
#pragma once
#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/ipc.h>
#include <unistd.h>
#include <sys/shm.h>

// 错误处理宏
#define ERR_EXIT(m)         \
    do                      \
    {                       \
        perror(m);          \
        exit(EXIT_FAILURE); \
    } while (0)

// key_t ftok(const char *pathname, int proj_id);
const int projid = 0x66;          // int proj_id
const std::string pathname = "."; // const char *pathname

const int gmode = 0666; // 权限位

#define CREATER "creater"
#define USER "user"

class Shm
{
public:
    Shm(const std::string &pathname, int projid, const std::string &usertype)
        : _shmid(-1), _size(4096), _start_mem(nullptr), _usertype(usertype)
    {
        // 获取唯一 Key
        _key = ftok(pathname.c_str(), projid);
        if (_key == -1)
        {
            ERR_EXIT("ftok");
        }
        printf("key:0x%x\n", _key);

        // 创建/获取共享内存
        if (usertype == USER)
        {
            Get();
        }
        else if (usertype == CREATER)
        {
            Creat();
        }

        // 进程连接共享内存
        Attach();
    }

    void *GetVirtualAddr()
    {
        printf("VirtualAddr:%p\n", _start_mem);
        return _start_mem;
    }

    // 获取指定共享内存的相关属性
    void Attr()
    {
        struct shmid_ds ds;                    // 描述共享内存的数据结构
        int n = shmctl(_shmid, IPC_STAT, &ds); // ds:输出型参数
        printf("shsm_segsz: %ld\n", ds.shm_segsz);
        printf("key: 0x%x\n", ds.shm_perm.__key);
        printf("uid: %d\n", ds.shm_perm.uid);
    }

    // 析构函数
    ~Shm()
    {
        // 不管是什么进程结束调用析构函数
        // 都需要进行去关联,最后再判断是否需要释放共享内存
        Detach();
        // 调用Destroy释放共享内存(由creater释放)
        if (_usertype == CREATER)
        {
            Destroy();
        }
    }

private:
    // 接口私有化:将所有接口在构造函数中调用,避免接口面向用户
    // 创建共享内存
    void Creat()
    {
        // IPC_CREAT | IPC_EXCL:若共享内存已存在则报错(确保创建全新内存,避免覆盖)
        // 一起使用一般为创建共享内存的进程
        CreatHelper(IPC_CREAT | IPC_EXCL | gmode);
        // 创建共享内存时如果不加权限位gmode则默认全0,则无法进行后续进程与共享内存的连接(Permission denied)
    }

    // 获取共享内存
    void Get()
    {
        // IPC_CREAT:若共享内存不存在则创建,存在则直接获取
        // 单独使用一般为直接获取shmid的进程
        CreatHelper(IPC_CREAT);
    }

    void CreatHelper(int flg)
    {

        // 共享内存的生命周期->随内核,不随进程
        // 创建/获取共享内存
        _shmid = shmget(_key, _size, flg);
        if (_shmid == -1)
        {
            ERR_EXIT("shmget");
        }
        printf("shmid:%d\n", _shmid);
    }

    // 共享内存映射挂载
    void Attach()
    {
        _start_mem = shmat(_shmid, nullptr, 0);
        if (_start_mem == (void *)(-1))
        {
            ERR_EXIT("shmat");
        }
        printf("attach success\n");
    }

    // 去关联
    void Detach()
    {
        int n = shmdt(_start_mem);
        if (n == 0)
        {
            printf("detach success\n");
        }
        else
        {
            ERR_EXIT("shmdt");
        }
    }

    // 删除共享内存
    void Destroy()
    {
        if (_shmid == -1)
            return;
        int n = shmctl(_shmid, IPC_RMID, nullptr);
        if (n < 0)
        {
            ERR_EXIT("shmctl");
        }
        else
        {
            printf("shmctl delete shm:%d success\n", _shmid);
        }
    }

private:
    int _shmid;
    int _size;
    void *_start_mem; // 共享内存在地址空间的起始地址
    key_t _key;
    std::string _usertype; // 判断是使用者还是创建者,用于区分调用不同的函数
};

struct buffer_t
{
    int count;
    char buf[26 * 2];
};

4.2 客户端进程:写入数据到共享内存(client.cc

cpp 复制代码
#include "Shm.hpp"

int main()
{
    // Shm shm;
    // //通过同一个唯一key获取同一个共享内存
    // shm.Get();
    // sleep(5);
    // //映射到自己的地址空间中
    // shm.Attach();
    // shm.GetVirtualAddr();
    // sleep(5);

    Shm shm(pathname, projid, USER);
    buffer_t *shm_buffer = (buffer_t *)shm.GetVirtualAddr();
    memset(shm_buffer->buf, 0, 4096);

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

        sleep(1);
    }
    return 0;
}

4.3 服务端进程:创建共享内存并从共享内存读取数据(server.cc

cpp 复制代码
#include "Shm.hpp"

int main()
{
    // Shm shm;
    // shm.Creat();
    // sleep(5);
    // shm.Attach();
    // shm.GetVirtualAddr();
    // sleep(5);
    // shm.Destroy();

    Shm shm(pathname, projid, CREATER);
    buffer_t *shm_buffer = (buffer_t *)shm.GetVirtualAddr();
    shm_buffer->count = 0;
    int old_count = shm_buffer->count; // 模拟一个简单的保护和同步机制
    while(true)
    {
        if(old_count != shm_buffer->count)
        {
            std::cout << "count: " << shm_buffer->count << std::endl;
            std::cout << "data: " << shm_buffer->buf << std::endl;
            old_count = shm_buffer->count;
        }
        usleep(74329);

        if(shm_buffer->count >= 4)
        {
            break;
        }
    }
    return 0;
}

4.4 编译与运行

Makefile:

bash 复制代码
.PHONY:all
all:client server
client:client.cc
	g++ -o $@ $^ -std=c++11
server:server.cc
	g++ -o $@ $^ -std=c++11
 
.PHONY:clean
clean:
	rm -f client server

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

  • 步骤一:先运行./server
  • 步骤二:再运行./client

4.5 残留共享内存清理

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

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

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

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

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

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

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

六. 关键问题分析

6.1 共享内存的同步问题

共享内存本身无同步与互斥机制,若多个进程同时写入,会导致数据覆盖(如进程 A 写 "hello",进程 B 同时写 "world",最终可能得到 "hwllo" 等混乱数据)

解决方案:

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

6.2 共享内存的删除机制

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

6.3 常见错误与排查

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

结束语

System V 共享内存是 Linux 中效率最高的 IPC 方式,核心优势在于 "无内核中转、用户态直接通信"。共享内存适合高频、大数据量的跨进程通信场景(如服务器集群数据共享、高频交易系统、视频流传输)。若需实现安全的同步通信,可后续学习 System V 信号量的使用,将二者结合实现 "高效 + 安全" 的跨进程通信。

相关推荐
一勺菠萝丶1 小时前
Linux 服务器临时用户创建与删除教程
linux·运维·服务器
曲幽1 小时前
你的FastAPI又在服务器上“跑不起来”了?来,今天咱把打包这件事彻底聊透
linux·windows·python·docker·fastapi·web·pyinstaller·nssm·services
2023自学中1 小时前
imx6ull 开发板,RTMP 推流本地视频 到虚拟机
linux·音视频·嵌入式·开发板
驭渊的小故事2 小时前
网络初始1(2000字详细剖析网络的TCP/IP协议栈)
linux·服务器·网络
benjiangliu2 小时前
LINUX系统-18-EXT系列文件系统(三)
linux·运维·chrome
无足鸟ICT2 小时前
【RHCA+】~_.vimrc
linux
皆圥忈2 小时前
深入理解进程虚拟地址空间
linux
LJianK12 小时前
服务器高 CPU 排查方法
linux·运维·服务器
liu-yonggang2 小时前
Linux vs QNX 深度对比
linux·qnx