共享内存通信

文章目录

进程间通信

进程的核心结构

在操作系统中,一个进程通常包含以下关键组成部分:

  1. 进程控制块(PCB, Process Control Block)
  2. 进程虚拟地址空间(Virtual Address Space)
  3. 页表(Page Table)
  4. 物理内存(Physical Memory)
    在 Linux 内核中,PCB 对应的数据结构是:
    task_struct
    该结构用于描述进程的所有运行信息,例如:
  • 进程 ID
  • 调度信息
  • 内存管理信息
  • 打开文件
  • 信号状态

进程地址空间与内存映射

每个进程都拥有 独立的虚拟地址空间

进程访问内存时,并不会直接访问物理内存,而是通过 页表映射机制 完成地址转换。

虚拟地址访问流程如下:

复制代码
虚拟地址 (Virtual Address)
        ↓
页表 (Page Table)
        ↓
物理地址 (Physical Address)
        ↓
物理内存 (Physical Memory)

CPU 中的 内存管理单元(MMU) 负责执行地址转换。

Memory Management Unit

MMU 会根据页表信息,将虚拟地址转换为物理地址,从而实现进程对内存的访问。


进程资源的组织方式

操作系统管理进程资源时遵循一个基本原则:
先描述,再组织。

具体而言:

  1. 使用数据结构描述进程资源(例如 PCB、地址空间结构等)。
  2. 将这些数据结构按照一定规则组织起来(如链表、红黑树、哈希表等),以便操作系统进行管理和调度。
    例如:
  • PCB 描述进程状态
  • 地址空间结构描述进程内存布局
  • 页表描述虚拟地址到物理地址的映射关系

进程独立性的来源

进程具有 独立性(Process Isolation),其原因主要包括:

  1. 内核数据结构独立
    每个进程拥有自己的:
  • PCB
  • 内存管理结构
  • 文件描述符表
  1. 地址空间独立
    每个进程拥有独立的虚拟地址空间。

  2. 页表独立
    每个进程拥有自己的页表,因此即使虚拟地址相同,也可能映射到不同的物理内存位置。
    例如:

    进程A虚拟地址 0x1000 → 物理地址 0xA000

    进程B虚拟地址 0x1000 → 物理地址 0xF200

因此:

不同进程之间默认无法直接访问彼此的数据。

这就是 进程隔离机制(Process Isolation) 的基础。


进程间通信的本质

进程间通信(IPC)的核心前提是:

让不同进程能够访问同一份资源。

因为进程具有 地址空间隔离性

每个进程都拥有独立的:

  • PCB(Process Control Block)

  • 虚拟地址空间

  • 页表

  • 代码段和数据段
    进程访问内存的过程为:

    虚拟地址

    页表映射

    物理地址

    访问物理内存

由于 不同进程页表不同,因此:

  • 相同虚拟地址
  • 实际映射到 不同物理地址
    这就是:

**进程之间默认无法直接共享数据的原因。


进程间通信的困难

由于进程之间具有:

  • 独立的内核数据结构
  • 独立的虚拟地址空间
  • 独立的页表
    因此:

不同进程之间直接共享数据是困难的。

为了实现进程间通信(IPC),操作系统需要提供特殊机制。


共享内存

共享内存的定义

共享内存可以定义为:

共享内存是一种进程间通信机制,通过将同一块物理内存映射到多个进程的虚拟地址空间,从而实现数据共享。

其核心特点是:

  • 多个进程 访问同一物理内存

  • 通过 页表映射实现地址共享

  • 不需要数据复制。
    换句话说:

    多个进程

    映射

    同一物理内存

    实现通信

共享内存通信机制

在操作系统中,通信方式(IPC Mechanism)指的是:

操作系统为不同进程之间交换数据而提供的一种机制或手段。

每一种通信方式本质上都是:

  • 由操作系统实现
  • 通过系统调用接口提供给用户程序使用
    常见的 IPC 机制包括:
  • 管道(Pipe)
  • 命名管道(FIFO)
  • 共享内存(Shared Memory)
  • 消息队列(Message Queue)
  • 信号量(Semaphore)
  • Socket

操作系统提供通信机制的主要意义在于:

1.提供标准化通信接口

操作系统通过 系统调用 为用户程序提供统一接口,例如:

  • pipe()

  • mkfifo()

  • shmget()

  • sgget()
    开发者无需自行实现底层通信机制。
    2.支持多进程并发通信
    在同一操作系统中:

  • 不同用户

  • 不同程序

  • 不同进程
    都可以 同时使用 IPC 机制进行通信
    例如:

    进程A <--> 进程B (共享内存1)

    进程C <--> 进程D (共享内存2)

    进程E <--> 进程F (共享内存3)

因此: 系统中可能同时存在 多个共享内存对象

操作系统需要对这些对象进行统一管理。

其中:

共享内存是一种通过共享物理内存实现进程间通信的 IPC 机制。

其基本思想是:让多个进程访问同一块物理内存。


共享内存的实现步骤

1.创建共享内存

首先,由进程通过系统调用请求操作系统:

操作系统在 物理内存中申请一块内存区域

复制代码
Physical Memory
+-------------------+
|   Shared Memory   |
+-------------------+

这块内存由内核管理,并被标记为 共享内存对象


2.映射共享内存

操作系通过 页表映射

将同一块物理内存映射到多个进程的虚拟地址空间。

示意图:

复制代码
Process A                 Process B

Virtual Address Space     Virtual Address Space
        |                         |
        |                         |
      Page Table               Page Table
        |                         |
        +-----------+-------------+
                    |
              Shared Memory
              (Physical Memory)

此时:

  • 进程 A 写入数据

  • 进程 B 可以直接读取
    因此实现通信。

    映射到进程的虚拟地址空间

具体过程为:

复制代码
共享内存物理地址
        ↓
页表映射
        ↓
进程虚拟地址空间

操作系统会返回一个 虚拟地址起始地址 ,进程可以通过该地址访问共享内存。

此时访问方式与普通内存类似,例如:

复制代码
char* p = shared_memory_address;
p[0] = 'A';

3.多进程映射同一内存

如果另一个进程也将 同一块共享内存 映射到自己的地址空间:

复制代码
进程A虚拟地址 → 同一物理内存
进程B虚拟地址 → 同一物理内存

则两个进程即可通过该内存区域进行数据交换。

这种机制称为:共享内存通信(Shared Memory IPC)


4.解除共享关系

当进程不再需要共享内存时,需要执行两步操作:
解除映射

取消进程地址空间与共享内存之间的映射关系。

复制代码
Virtual Address  ×  Physical Memory

该操作通常称为:
Detach(去关联)
释放共享内存

当所有进程都不再使用该共享内存时,操作系统可以释放该内存资源。

该操作称为:
删除共享内存对象


共享内存与 malloc 的区别

共享内存与 malloc 的区别

malloc 也申请内存,为什么不能实现进程通信?

区别如下:

特性 malloc 共享内存
所属空间 进程私有堆 内核管理
可见性 仅当前进程 多进程共享
是否支持 IPC 不支持 支持
管理方式 用户空间 内核对象
因此:

malloc 申请的内存 只能在当前进程使用,无法被其他进程映射。

malloc 分配的内存:

  • 只属于当前进程
  • 不会被其他进程映射
    而共享内存机制:
  • 专门设计用于 多进程共享同一物理内存
    因此共享内存是专门为 IPC 设计的机制

共享内存通信的特点

共享内存是 最快的 IPC 方式之一 ,原因是:

  1. 通信效率高

数据无需复制,多个进程直接访问同一块内存。

  1. 访问方式简单,直接访问物理内存

读写方式与普通内存相同。

  1. 需要同步机制,无需数据拷贝

多个进程同时访问可能产生 竞态条件(Race Condition),通常需要结合:

  • 信号量
  • 互斥锁
    但它也存在问题:
  • 需要同步机制(信号量 / mutex)
  • 否则可能出现 数据竞争(Race Condition)

共享内存定位机制

在内核中,System V IPC 资源通常存储在 IPC 资源表中。

查找流程如下:

复制代码
key
 ↓
查找IPC资源表
 ↓
找到对应共享内存对象
 ↓
返回 shmid

因此只要 key 相同,就能保证访问的是 同一共享内存。


共享内存标识符的实现方式

在内核内部,共享内存通常由 IPC 资源表 管理。
shmid 本质上是该资源表中的一个索引值。

示意结构:

复制代码
IPC Table

index   shared memory object
--------------------------------
0       shm segment A
1       shm segment B
2       shm segment C

shmid 即为:

复制代码
共享内存对象的索引标识

但需要注意:

它与 文件描述符(File Descriptor) 并不兼容,因为二者属于不同的资源管理体系。


共享内存通信模型

共享内存通信的一般流程如下:

复制代码
1 Server 调用 shmget() 创建共享内存

2 Client 调用 shmget() 获取共享内存

3 双方通过 shmat() 将共享内存映射到各自地址空间

4 通过读写该内存区域实现数据交换

5 通信结束后解除映射并释放共享内存

共享内存通信中的关键问题:资源唯一性

在使用共享内存进行进程间通信时,需要解决一个核心问题:

如何保证多个进程访问的是同一块共享内存?

共享内存通信的前提是:

多个进程必须引用同一个共享内存对象

如果两个进程分别创建了不同的共享内存对象,即使大小相同,也无法实现通信。

因此系统需要一种机制,用于:

唯一标识某一个共享内存对象


共享内存创建与获取

shmget

System V 共享内存 API 中,核心函数是:

复制代码
int shmget(key_t key, size_t size, int shmflg);

参数含义:

参数 说明
key IPC 键值
size 共享内存大小
shmflg 创建或访问标志

创建共享内存

复制代码
shmget(key, size, IPC_CREAT | IPC_EXCL)

含义:

  • IPC_CREAT :如果不存在则创建
  • IPC_EXCL :如果已经存在则报错
    因此:

该组合用于创建一个全新的共享内存对象


获取共享内存

复制代码
shmget(key, size, 0)

含义:

  • 如果共享内存存在 → 返回其标识符
  • 如果不存在 → 返回错误

shmget 函数参数

shmflg 用于指定共享内存的 创建选项和权限控制

常见选项包括:

  • IPC_CREAT

  • IPC_EXCL
    它们通常通过 按位或(bitwise OR) 组合使用。
    例如:

    IPC_CREAT | IPC_EXCL

这种设计类似于文件系统中 open() 的标志位。


IPC_CREAT 选项

IPC_CREAT

该标志表示:

如果指定 key 的共享内存不存在,则创建新的共享内存;如果已存在,则返回该共享内存的标识符。

语义为:

复制代码
不存在 → 创建
存在   → 获取

这种模式适用于:

  • 多个进程共享同一通信资源

  • 允许后续进程直接连接已有共享内存
    典型场景:

    Server 创建共享内存
    Client 获取共享内存


IPC_EXCL 选项

IPC_EXCL

该标志 不能单独使用 ,必须与 IPC_CREAT 组合。

组合形式:

复制代码
IPC_CREAT | IPC_EXCL

其语义为:

复制代码
不存在 → 创建
存在   → 返回错误

换句话说:

只有当共享内存 首次创建成功 时函数才返回成功。

这种方式可以确保:
当前进程一定是共享内存的创建者。

典型用途

  • 避免重复创建
  • 判断资源是否已被其他进程占用

默认行为

如果 shmflg 不指定上述标志(例如设置为 0),则行为类似

复制代码
不存在 → 创建
存在   → 获取

这与 单独使用 IPC_CREAT 的效果基本一致。


返回值

shmget() 的返回值为:

复制代码
int shmid

共享内存标识符(Shared Memory Identifier)

该标识符是一个整数,用于后续共享内存操作,例如:

  • 连接共享内存

  • 解除映射

  • 删除共享内存
    如果函数执行失败,则返回:

    -1

同时设置 errno 表示错误原因。


如何保证两个进程访问同一共享内存

通信双方必须:

  1. 使用 相同的 pathname

  2. 使用 相同的 proj_id

  3. 调用同一个函数

    key_t key = ftok(pathname, proj_id);

于是:

复制代码
Process A --------\
                   ->  key 相同
Process B --------/

然后再使用这个 key 调用:

复制代码
shmget()

于是系统就会定位到同一个共享内存对象

ftok 函数的作用

为了生成一个较少冲突的 IPC key,系统提供函数:

复制代码
key_t ftok(const char *pathname, int proj_id);

该函数的作用是:

根据指定文件路径 pathname 和项目标识 proj_id,生成一个 key_t 类型的 IPC key。

参数说明

  1. pathname
  • 一个已经存在的文件路径
  • 系统会读取该文件的 inode 等信息参与计算
  1. proj_id
  • 项目标识符
  • 通常是一个字符或整数
  • 由程序员自行指定

ftok 的核心原理

ftok() 内部会根据:

  • 文件的 inode

  • 设备号

  • proj_id
    通过一定算法生成一个 32 位整数 key
    因此:
    如果 两个进程调用 ftok 时传入相同的参数

    ftok(pathname, proj_id)

那么得到的 key_t 大概率相同


key

什么是 key

在 System V IPC 中:

key_t 是用于唯一标识某个 IPC 对象的键值。

操作系统通过 key 值 来定位 IPC 资源。

如果多个进程使用 相同的 key ,它们就可以访问 同一个 IPC 对象

因此可以理解为:

复制代码
key  →  IPC资源的唯一标识符

key出现的原因

操作系统中可能存在 大量共享内存对象

例如:

复制代码
共享内存A
共享内存B
共享内存C
共享内存D
...

不同进程可能需要访问不同的共享内存。

因此必须有一个 唯一标识符 来标识每个共享内存对象。

这个标识符就是:
key_t key


key 的核心作用

key 的主要作用是:

在系统范围内唯一标识某个 IPC 资源。

该 IPC 资源可以是:

  • 共享内存

  • 消息队列

  • 信号量
    当进程访问 IPC 资源时,系统会通过 key 在 IPC 资源表中查找对应对象。
    例如系统中可能存在多个共享内存:

    共享内存列表

    [key = 0x1234] → shm1
    [key = 0x4567] → shm2
    [key = 0x8888] → shm3

当某个进程调用:

c 复制代码
shmget(key,...)

操作系统会执行以下步骤:

  1. 遍历系统中的 IPC 资源表
  2. 查找 key 是否匹配
  3. 找到对应的共享内存对象并返回其标识符

key 的重要特点

key 具有以下特点:

  1. 唯一性 :
    在系统范围内,每个 IPC 资源都通过 key 进行唯一标识。
  2. 数值本身没有特殊意义 :
    key 的具体数值并不重要,重要的是: 它能够唯一标识一个 IPC 对象。
  3. 多个进程可以通过相同 key 访问同一资源 :
    只要不同进程使用 相同的 key 值 ,它们就可以定位到 同一个 IPC 资源

key值

key 只需要满足一个条件:

唯一性

就像现实生活中的例子:

场景 标识
餐厅包间 房间号
文件系统 inode
共享内存 key
例如:
复制代码
key = 0x1234
key = 0x8888
key = 0x66

具体数字没有意义,只要唯一即可。


key 的存储位置

key 并不是存储在共享内存数据区,而是存储在 共享内存描述结构中

当共享内存被创建时:

c 复制代码
shmget(key, size, IPC_CREAT)

操作系统会:

  1. 创建共享内存对象

  2. 在共享内存描述结构中记录 key
    示意结构:

    struct shared_memory_object
    {
    key_t key; ← IPC key
    size_t size;
    permission;
    pid;
    ...
    }

因此:

key 实际上是 共享内存内核对象中的一个字段

当其他进程想访问共享内存时:

复制代码
ftok → key
      ↓
shmget(key)
      ↓
在 IPC 表中查找 key

如果匹配成功,就返回对应共享内存。

例如:

复制代码
共享内存对象
│
├── key
├── size
├── owner
├── permissions
└── memory address

当进程调用:

复制代码
shmget(key)

内核会:

复制代码
遍历共享内存对象列表
        │
        ├─ key == 目标key ? 
        │
        └─ 找到对应共享内存

size

size 指定共享内存的大小(单位:字节)。

例如:

复制代码
size = 1024

表示申请 1KB 共享内存空间

共享内存的大小通常根据通信数据量进行设置。


共享内存的本质结构

System V IPC 机制 中,共享内存不仅仅是一块物理内存区域,而是由两部分组成:
共享内存 = 物理内存块 + 内核描述结构

在 Linux 内核中,每个共享内存对象都会对应一个内核数据结构,例如:

c 复制代码
struct shmid_kernel
{
    struct kern_ipc_perm shm_perm;  // 权限信息
    size_t shm_segsz;               // 共享内存大小
    time_t shm_atime;               // 最后 attach 时间
    time_t shm_dtime;               // 最后 detach 时间
    time_t shm_ctime;               // 创建时间
    pid_t shm_cpid;                 // 创建进程
    pid_t shm_lpid;                 // 最近操作进程
};

操作系统管理共享内存遵循:

先描述,再组织(Describe then Organize)

即:

  1. 创建共享内存对象
  2. 用数据结构描述其属性
  3. 将这些对象组织在 IPC 管理结构中(数组 / 链表)
    因此系统调用:
c 复制代码
shmget()

实际上做了两件事:

  1. 分配共享内存
  2. 创建共享内存描述对象

一块物理内存区域

因此更准确的定义是:

共享内存对象(Shared Memory Object) = 内存资源 + 内核管理结构

系统并不是直接管理内存块本身 ,而是管理这些描述结构体


共享内存的管理方式

由于多个进程可能同时创建共享内存,因此,操作系统创建共享内存时,也会创建一个内核对象 来描述该共享内存。

因此:

复制代码
共享内存对象
   ├── 物理内存块
   └── 共享内存描述结构

系统会把这些共享内存描述结构组织成某种数据结构,例如:

  • 数组
  • 链表
  • IPC 资源表
    因此,操作系统必须维护 共享内存对象表(Shared Memory Table)
    当用户执行操作时,本质上是在对这些描述结构进行操作
    例如:
操作 实际行为
创建共享内存 创建描述结构 + 分配物理内存
查询共享内存 查找描述结构
删除共享内存 删除描述结构 + 释放内存

该表通常由内核维护,用于记录:

  • 共享内存标识符(shmid)

  • 内存大小

  • 权限

  • 引用进程数量

  • 状态信息
    示意结构:

    Kernel
    └── Shared Memory Table
    ├── shm_id = 1
    ├── shm_id = 2
    ├── shm_id = 3
    └── shm_id = 4

因此:

  • 张三可以创建共享内存
  • 李四也可以创建共享内存
  • 王五也可以创建共享内存
    这些共享内存 彼此独立存在

shmid 的作用

shmget() 调用成功后,会返回:

复制代码
int shmid

即:

共享内存标识符(Shared Memory Identifier)

这是系统内部用来管理共享内存对象的 ID

key 与 shmid 的区别与联系

在 System V IPC 机制中,key 用于唯一标识一个共享内存对象。当进程调用 shmget() 时,内核会根据 key 在共享内存对象集合中查找对应对象。如果找到则返回该共享内存的 shmid,如果不存在且指定 IPC_CREAT 则创建新的共享内存对象。因此 key 主要用于共享内存的定位和唯一标识,而 shmid 则是内核返回给用户用于操作共享内存的标识符。

key 与 shmid 的关系

名称 作用 类比
key 用户指定的资源标识 门牌号
shmid 系统分配的资源 ID 房屋内部编号

流程如下:

复制代码
ftok() → key
        ↓
     shmget()
        ↓
      shmid

也就是说:

  • key 用于查找 IPC 对象
  • shmid 用于操作 IPC 对象

key 与 shmid 的区别

属性 key shmid
类型 用户指定 内核生成
作用 标识共享内存对象 操作共享内存
层级 用户层 内核层
类似 inode fd

可以类比文件系统:

文件系统 共享内存
inode key
fd shmid
关系图:
复制代码
key  -----> 共享内存对象
                 │
                 │
             shmid
                 │
                 │
              进程使用

流程:

复制代码
ftok() -> 生成 key
shmget(key) -> 返回 shmid
shmat(shmid) -> 映射共享内存

设计 key 和 shmid 两层的原因

两者的作用层次不同:

标识符 层级 作用
key IPC 层标识 查找共享内存
shmid 进程使用的句柄 操作共享内存
关系类似于文件系统:
文件系统 共享内存
inode key
file descriptor (fd) shmid
流程:
复制代码
ftok()
   ↓
key
   ↓
shmget()
   ↓
shmid

设计两个标识符的原因是:

实现用户空间与内核空间的解耦(decoupling)

这是 操作系统设计中的解耦思想

如果直接使用 key 操作共享内存:

复制代码
用户程序 <----> 内核
      key

问题:

  • key 是用户提供的

  • 可能重复

  • 可能冲突

  • 管理困难
    因此设计两层:

    用户层

    │ key

    内核查找共享内存

    │ shmid

    共享内存对象

优势:

  1. 内核管理更灵活

  2. 用户和内核解耦

  3. 内核可以修改实现而不影响用户程序
    这与文件系统设计完全一致:

    filename -> inode -> fd

System V 共享内存中的 key 是共享内存对象在系统范围内的唯一标识符。

当进程调用 shmget() 时:

  1. 通过 key 在内核共享内存管理结构中查找共享内存对象
  2. 若存在则返回该共享内存的 shmid
  3. 若不存在且指定 IPC_CREAT,则创建新的共享内存对象
    因此:

key 用于标识共享内存对象,而 shmid 用于实际操作共享内存。



当使用:

复制代码
IPC_CREAT | IPC_EXCL

时:

如果共享内存已经存在,系统会返回错误:

复制代码
File exists

原因是:

IPC_EXCL 要求创建的对象必须是新的。

如果对象已经存在,则认为是错误。


通信流程

完整的共享内存通信流程:

生成 key

复制代码
key = ftok(pathname, proj_id);

Server 创建共享内存

复制代码
shmid = shmget(key, size, IPC_CREAT | IPC_EXCL);

Client 获取共享内存

复制代码
shmid = shmget(key, size, 0);

进程映射共享内存

复制代码
shmat()

读写共享内存

复制代码
直接访问内存

System V 共享内存通信的关键点:

  1. 不同进程必须使用 相同 key
  2. key 通常通过 ftok() 生成
  3. shmget() 用于
  • 创建共享内存
  • 获取共享内存
  1. shmid 是系统内部共享内存标识符
  2. 共享内存通信本质是:
    多个进程映射同一块物理内存

共享内存的查看

IPC 资源的查看方式(System V IPC)

在Linux 系统中,System V 类型的 IPC 资源(包括共享内存、消息队列、信号量 )可以通过 ipcs 命令进行查看。

  1. 查看共享内存
bash 复制代码
ipcs -m
  • -m 表示查看 共享内存(memory)
  • 可查看:
    • shmid(共享内存标识符)
    • key
    • 大小
    • 创建者
    • 权限
    • 连接进程数

  1. 查看消息队列
bash 复制代码
ipcs -q
  • -q 表示查看 消息队列(queue)

  1. 查看信号量
bash 复制代码
ipcs -s
  • -s 表示查看 信号量集合(semaphore)

共享内存的生命周期特征

核心特征:

System V IPC 资源的生命周期不依赖于进程,而依赖于内核

更准确表述为:

System V IPC 对象(包括共享内存)的生命周期由内核管理,不会随着创建它的进程退出而自动销毁。

对比说明(与管道的区别)

类型 生命周期
管道(pipe) 随进程关闭而释放
文件描述符 进程结束自动释放
System V 共享内存 进程退出不会自动删除

![[Pasted image 20260314211111.png]]

当程序:

  1. 第一次运行 → 成功创建共享内存
  2. 进程退出
  3. 再次运行 → 提示"资源已存在"
    这是因为:
  • 共享内存段仍然存在于内核中
  • 并未被显式删除
  • 因此再次创建时会报错(如 EEXIST

共享内存的删除方式

删除共享内存可以通过:

  1. 命令方式
bash 复制代码
ipcrm -m shmid
  • -m:删除共享内存
  • 参数必须使用 shmid(共享内存标识符)
  • 不能使用 key

为什么必须使用 shmid?

在 System V IPC 中

  • key:用于内核内部唯一标识
  • shmid:用于进程操作共享内存的句柄
    删除操作属于用户态对内核对象的控制行为,因此必须使用:

共享内存标识符 shmid

而不是 key。


  1. 接口方式(程序内删除
    System V IPC 机制中,共享内存创建后,系统需要提供一种方式对其进行进一步管理,例如:
  • 删除共享内存
  • 获取共享内存属性
  • 修改共享内存属性
    为此,系统提供了一个统一的控制接口:
c 复制代码
int shmctl(int shmid, int op, struct shmid_ds *buf);

该函数用于 对共享内存对象进行控制操作

  1. shmid
    shmid 表示 共享内存标识符(shared memory identifier)
    • 该值由 shmget() 返回
    • 用于在用户层标识一个具体的共享内存对象
      其作用类似于文件系统中的 文件描述符(fd),用于指向某一个已经存在的共享内存资源。

  1. op
    op 表示 控制命令 ,用于指定对共享内存执行的具体操作。
    常见命令包括:
命令 作用
IPC_STAT 获取共享内存属性
IPC_SET 设置共享内存属性
IPC_RMID 删除共享内存
其中最常用的是:
复制代码
IPC_RMID

其含义为:
立即将指定共享内存从系统中删除。

需要注意的是,这里的 RMID 可以理解为:

复制代码
RM → remove
ID → identifier

删除该共享内存标识符所对应的资源


  1. buf
    buf 参数用于 传递或接收共享内存的属性信息
    其类型为:
c 复制代码
struct shmid_ds

该结构体包含共享内存的各种元数据,例如:

  • 共享内存大小
  • 权限信息
  • 创建时间
  • 最后访问时间
  • 当前连接进程数量
    如果只是执行 删除操作 (IPC_RMID),则不需要使用该结构体,因此可以直接传入:
c 复制代码
NULL

  1. 删除共享内存
    当不再需要某个共享内存时,可以通过 shmctl() 删除该资源。
    示例代码:
c 复制代码
if (shmctl(shmid, IPC_RMID, NULL) == -1)
{
    std::cerr << errno << " : " << strerror(errno) << std::endl;
}

执行流程如下:

  1. 用户程序调用 shmctl()
  2. 指定共享内存标识符 shmid
  3. 传入控制命令 IPC_RMID
  4. 内核删除对应的共享内存对象

  1. 返回值说明
    shmctl() 的返回值用于表示操作结果:
返回值 含义
0 操作成功
-1 操作失败

如果返回值为 -1,则说明操作失败,此时系统会设置 错误码 errno,可以通过:

c 复制代码
strerror(errno)

获取对应的错误描述信息。


共享内存删除示例流程

一个典型的共享内存生命周期如下:

生成 key

复制代码
ftok()

创建共享内存

复制代码
shmget()

使用共享内存

复制代码
shmat()

程序结束时删除共享内存

复制代码
shmctl(shmid, IPC_RMID, NULL)

例如:

c 复制代码
sleep(5);
shmctl(shmid, IPC_RMID, NULL);

表示:

  • 共享内存在系统中 存在 5 秒
  • 之后由程序主动删除

与普通内存不同,共享内存不会在进程结束时自动释放

因此:

如果程序不主动调用 shmctl(..., IPC_RMID, ...),共享内存对象可能会一直存在于系统中。

可以通过系统命令查看当前共享内存:

bash 复制代码
ipcs -m

删除指定共享内存:

bash 复制代码
ipcrm -m shmid

shmctl()System V 共享内存的控制接口 ,用于管理共享内存对象,包括获取属性、修改属性以及删除共享内存。其中 IPC_RMID 是最常用的命令,用于从系统中删除指定的共享内存资源。


shmat

System V IPC 中,共享内存创建之后,进程仍然 无法直接访问该内存

为了让进程能够使用共享内存,必须将共享内存 映射到进程的虚拟地址空间中

完成这一操作的系统调用是:

c 复制代码
void *shmat(int shmid, const void *shmaddr, int shmflg);

该函数用于 将共享内存对象附加(attach)到当前进程的地址空间


函数参数说明

shmid shmid` 表示 共享内存标识符(shared memory identifier)

  • 该值由 shmget() 返回

  • 用于指定要映射的共享内存对象
    因此,该参数的含义是:

    当前进程希望关联的共享内存对象


shmaddr
shmaddr 用于指定 共享内存在进程虚拟地址空间中的映射地址

理论上,进程可以自己指定映射地址,例如:

复制代码
shmaddr = 0x400000

但在实际开发中:

通常不指定该地址,而是传入 NULL

原因是:

  • 进程的虚拟地址空间布局由操作系统管理
  • 用户程序通常无法准确判断哪个地址区域是空闲的
    因此让操作系统 自动选择一个合适的地址进行映射 是最安全的做法。

shmflg shmflg` 用于指定映射方式,常见取值包括:

标志 作用
0 默认读写
SHM_RDONLY 只读映射
在大多数场景中直接使用:
复制代码
shmflg = 0

表示共享内存 可读可写


shmat() 的核心作用是:

将共享内存的物理页映射到当前进程的虚拟地址空间。

具体过程包括:

  1. 内核检查共享内存对象是否存在

  2. 在当前进程的虚拟地址空间中寻找一块空闲区域

  3. 修改进程页表

  4. 建立 虚拟地址 → 共享内存物理页 的映射关系
    完成后,进程即可通过该虚拟地址访问共享内存。
    示意图:

    共享内存(物理内存)

    │ 页表映射

    进程虚拟地址空间


    mem 指针


返回值
shmat() 的返回值为:

复制代码
共享内存映射后的起始虚拟地址

返回类型为:

复制代码
void *

即:

复制代码
mem = shmat(...)

此时 mem 指向共享内存的起始地址。

之后进程即可像访问普通内存一样访问共享内存,例如:

复制代码
char *buf = (char *)mem;
buf[0] = 'A';

错误处理

如果 shmat() 调用失败,则返回值为:

复制代码
(void *) -1

同时系统会设置错误码 errno

示例代码:

c 复制代码
void *mem = shmat(shmid, NULL, 0);

if (mem == (void *)-1)
{
    std::cerr << errno << " : " << strerror(errno) << std::endl;
    exit(3);
}

malloc 的区别

从使用角度看,shmat() 的返回值类似于 malloc() 返回的指针:

函数 作用
malloc() 申请进程私有堆内存
shmat() 映射共享内存
区别在于:
特点 malloc shmat
内存类型 私有内存 共享内存
可见性 当前进程 多个进程
管理者 C 运行时库 操作系统内核
因此可以理解为:

shmat() 返回的是 共享内存映射后的虚拟地址起始位置


典型封装示例

在实际项目中,通常会对 shmat() 进行简单封装:

cpp 复制代码
void* attachShm(int shmid)
{
    void* mem = shmat(shmid, NULL, 0);

    if(mem == (void*)-1)
    {
        std::cerr << strerror(errno) << std::endl;
        exit(3);
    }

    return mem;
}

该函数完成的工作包括:

  1. 将共享内存附加到进程地址空间
  2. 检查映射是否成功
  3. 返回共享内存的起始地址
    shmat() 用于 将共享内存对象映射到当前进程的虚拟地址空间,其本质是通过修改页表建立虚拟地址与共享内存物理页之间的映射关系,使进程能够像访问普通内存一样访问共享内存。

shmdt

System V IPC 中,

当进程使用完共享内存后,需要 解除共享内存与当前进程地址空间之间的映射关系

需要注意:

解除映射并不会删除共享内存对象本身。

它仅仅完成以下工作:

  • 移除共享内存与当前进程之间的映射
  • 修改进程页表
  • 回收该共享内存在进程虚拟地址空间中的映射区域
    完成这一操作的系统调用是:
c 复制代码
int shmdt(const void *shmaddr);

该函数用于 将共享内存从当前进程的地址空间中分离(detach)
shmdt 中的 dt 来源于单词 detach,其含义为:

复制代码
分离 / 卸载 / 解除绑定

在共享内存机制中,其具体含义是:

将共享内存从当前进程的虚拟地址空间中卸载。

换句话说:

复制代码
进程虚拟地址空间   ←解除映射→   共享内存

解除映射后:

  • 共享内存仍然存在
  • 但当前进程已经无法继续访问该内存

shmdt 只有一个参数:

c 复制代码
const void *shmaddr

该参数表示:

共享内存在当前进程地址空间中的 起始虚拟地址

该地址通常来自 shmat() 的返回值。

例如:

c 复制代码
void *mem = shmat(shmid, NULL, 0);

此时解除映射时只需要:

c 复制代码
shmdt(mem);

返回值

返回值 含义
0 调用成功
-1 调用失败,并设置 errno
示例:
c 复制代码
if(shmdt(mem) == -1)
{
    perror("shmdt error");
    exit(1);
}

封装示例

在实际开发中,可以将解除共享内存映射封装为函数:

cpp 复制代码
void detachShm(void* start)
{
    if(shmdt(start) == -1)
    {
        perror("shmdt error");
        exit(1);
    }
}

该函数完成的工作包括:

  1. 调用 shmdt
  2. 检查返回值
  3. 输出错误信息(若失败)

共享内存生命周期演示

在服务器程序中,共享内存通常按照如下流程管理:

复制代码
1 创建共享内存
2 进程挂接共享内存
3 使用共享内存
4 解除共享内存映射
5 删除共享内存

时间示例:

复制代码
t0  创建共享内存
t5  进程调用 shmat 进行映射
t10 进程调用 shmdt 解除映射
t15 删除共享内存

如果通过监控工具(如 ipcs)观察共享内存状态,则会看到:

时间 状态
t0 共享内存存在,attach count = 0
t5 attach count = 1
t10 attach count = 0
t15 共享内存被删除

其中:

复制代码
attach count

表示当前 有多少进程正在使用该共享内存


shmdt vs shmctl

操作 函数 作用
解除映射 shmdt 进程不再使用共享内存
删除共享内存 shmctl(..., IPC_RMID) 从系统中彻底删除共享内存
因此:
复制代码
shmdt ≠ 删除共享内存

shmdt 只是:

解除进程与共享内存之间的映射关系


底层实现机制
shmdt() 在内核中的主要操作包括:

  1. 删除当前进程页表中的共享内存映射

  2. 回收对应的虚拟地址区域

  3. 更新共享内存的 attach 计数
    即:

    共享内存

    页表映射

    进程虚拟地址空间

调用 shmdt() 后:

复制代码
页表映射被删除

但共享内存物理页 仍然存在


shmdt() 的作用是:

解除共享内存与当前进程虚拟地址空间之间的映射关系,但不会删除共享内存对象本身。

System V 共享内存的使用流程为:

  1. 使用 shmget() 创建共享内存对象,返回 shmid
  2. 使用 shmat() 将共享内存映射到进程虚拟地址空间;
  3. 通过返回的地址进行读写操作;
  4. 使用 shmdt() 解除映射;
  5. 使用 shmctl(..., IPC_RMID, ...) 显式删除共享内存。
    其生命周期由内核管理,进程退出不会自动释放资源,因此必须显式删除。

数据拷贝

在**进程间通信(IPC)**场景中,假设通信内容为:

用户从键盘输入数据 → 发送到另一个进程 → 最终在显示器输出

请分析:

  • 使用 管道(Pipe)
  • 使用 共享内存(Shared Memory)

两种机制在该通信过程中涉及的数据拷贝次数

![[Pasted image 20260314201647.png]]


使用管道(Pipe)实现通信时

在基于管道的 IPC 中:

  • 数据需要经过 内核缓冲区
  • 生产者进程 → 内核空间 → 消费者进程
  • 属于 内核中转型通信机制

数据拷贝过程:

以"键盘输入 → 输出到显示器"为例:

  1. 用户空间 → 内核空间(写入管道)
  2. 内核空间 → 用户空间(读取管道)
  3. 用户空间 → 内核空间(输出到终端)
  4. 内核空间 → 显示设备缓冲区
    至少涉及 4 次数据拷贝
    若严格计算 IPC 部分,则:
    管道本身:
    2 次拷贝(写入 + 读取)

使用共享内存(Shared Memory)通信时

共享内存属于:

零拷贝(Zero-Copy)机制

特点:

  • 多个进程映射同一块物理内存
  • 数据不经过内核中转
  • 直接在用户空间读写
    数据流程:
  • 生产者进程直接写共享内存
  • 消费者进程直接读取共享内存

数据拷贝次数:

  • 0 次额外拷贝(进程间通信阶段)
    只有在:
  • 用户输入
  • 输出到显示器
    这些系统调用过程中才会涉及必要的内核交互。
    进程间通信本身不产生额外拷贝

总结

通信方式 数据拷贝次数 特点
管道 Pipe 2 次(IPC过程) 内核中转
共享内存 Shared Memory 0 次 零拷贝
综合来看 共享内存最快 减少拷贝与上下文切换

管道通机制:

属于 基于内核缓冲区的拷贝式通信

数据至少发生 两次拷贝


共享内存机制:

属于 基于内存映射的零拷贝通信

进程间数据无需复制

是所有 IPC 机制中效率最高的方式之一


管道通信需要在用户空间与内核空间之间进行两次数据拷贝,因此存在较大的性能开销。

共享内存机制通过建立多个进程对同一物理内存的映射,实现进程间数据共享,避免了数据在进程间的复制,因此属于零拷贝通信机制,效率更高


共享内存通信中的同步问题

在使用 System V Shared Memory 进行进程间通信时,共享内存虽然具有 高性能、零拷贝 的优势,但它本身存在一个重要问题:

共享内存只提供数据共享机制,不提供同步机制。

也就是说:

  • 一个进程可以随时读
  • 一个进程也可以随时写
    如果缺乏同步控制,可能出现:
  • 读到未写完的数据
  • 多进程并发写入导致数据竞争(Race Condition)
    因此,在实际系统设计中,需要 额外引入同步机制 来协调读写顺序。
    写完成后再通知读取
    目标通信流程如下:
text 复制代码
Client 写数据 → 通知 Server → Server 再读取

约束条件:

  1. 如果没有数据可读,Server 应该 等待
  2. 只有 Client 写完数据后,Server 才能开始读取
  3. 读写顺序必须严格控制

共享内存 + 匿名管道

一种简单且常见的实现方式是:

text 复制代码
共享内存   → 存储数据
匿名管道   → 实现同步通知

其中:

  • 共享内存负责传输数据
  • 管道 负责事件通知
    这里使用 Anonymous Pipe 作为同步工具。

系统结构设计

假设系统包含两个进程:

text 复制代码
Client  (生产者)
Server  (消费者)

整体结构如下:

复制代码
          共享内存
      ┌─────────────┐
      │             │
Client│   数据区     │Server
      │             │
      └─────────────┘

          匿名管道
      ┌─────────────┐
Client│ write  →  read│Server
      └─────────────┘

工作流程

通信过程可以设计为如下步骤:

Client 写入共享内存

Client 先将数据写入共享内存:

c 复制代码
write(shared_memory)

Client 通过管道发送通知

当数据写入完成后,Client 向管道写入一个字节:

c 复制代码
write(pipe_fd, "a", 1)

该操作的意义是:

通知 Server:共享内存中的数据已经准备好。


Server 在管道上阻塞等待

Server 在访问共享内存之前,首先尝试从管道读取数据:

c 复制代码
read(pipe_fd, &c, 1)

由于管道是 阻塞 IO

  • 如果 Client 尚未写入通知

  • Server 将阻塞在 read() 调用上
    这样就实现了:

    无数据 → Server 自动等待


Server 被唤醒并读取共享内存

当 Client 写入管道后:

c 复制代码
write(pipe_fd)

Server 的 read() 返回,从而被唤醒。

随后 Server 可以安全地读取共享内存:

c 复制代码
read(shared_memory)

System V 共享内存的特点

共享内存具有以下特点:
高性能

共享内存通信:

  • 不需要数据复制
  • 不需要内核中转
    因此:

共享内存是最快的 IPC 方式之一。


内核对象

共享内存对象:

  • 内核管理
  • 生命周期 不依赖单个进程
    即使创建进程退出:
    共享内存仍然可能存在。

需要同步机制

共享内存只解决:

复制代码
数据共享

但不解决:

复制代码
数据同步

因此通常需要配合:

  • 信号量(Semaphore)
  • 互斥锁
  • 条件变量

共享内存的内核管理结构

共享内存的内核实现思想

在操作系统内部:

  • 共享内存属于 内核管理的 IPC 对象
  • 内核必须对其进行:
    • 描述(Description)
    • 组织(Organization)
    • 管理(Management)
      因此:

操作系统为每一个共享内存段维护一个内核数据结构,用于记录其属性与管理信息。


内核向用户返回的数据结构

使用结构体:

c 复制代码
struct shmid_ds

该结构体用于:

描述共享内存段的属性信息

注意:

  • 用户态看到的是该结构的子集
  • 内核内部的结构比它复杂得多
  • 内核中包含额外管理字段

共享内存中的 Key 在内核中的存储机制

System V Shared Memory 中,不同进程能够访问同一块共享内存的前提是:

多个进程必须能够定位到 同一个共享内存对象

为实现这一目标,System V IPC 引入了一个重要标识:

text 复制代码
key_t key

key 的作用是:

text 复制代码
用于在内核中唯一标识一个共享内存资源

通常通过如下接口创建或获取共享内存:

c 复制代码
int shmid = shmget(key_t key, size_t size, int shmflg);

其中 key 会被写入到 共享内存对象的内核数据结构中,供其他进程查找。


共享内存在内核中的数据结构

在 Linux 内核中,每一块共享内存都会对应一个描述结构:

c 复制代码
struct shmid_ds

该结构体用于描述共享内存的各种属性,例如:

  • 权限信息
  • 共享内存大小
  • 进程挂接数量
  • 创建时间
  • 访问时间等
    其核心结构可以简化表示为:
c 复制代码
struct shmid_ds
{
    struct ipc_perm shm_perm;
    size_t shm_segsz;
    time_t shm_atime;
    time_t shm_dtime;
    time_t shm_ctime;
};

ipc_perm 权限结构

shmid_ds 结构中,最重要的成员之一是:

c 复制代码
struct ipc_perm shm_perm;

该结构用于描述 IPC 对象的权限和标识信息

结构示意如下:

c 复制代码
struct ipc_perm
{
    key_t key;
    uid_t uid;
    gid_t gid;
    mode_t mode;
};

其中:

text 复制代码
key_t key

就是在 shmget() 时设置的 共享内存标识 key

因此可以得到结论:

用户层创建共享内存时提供的 key,最终会被存储在内核的 ipc_perm.key 字段中。


key 在共享内存对象中的层级关系

共享内存对象的结构关系可以表示为:

text 复制代码
共享内存对象
      │
      ▼
struct shmid_ds
      │
      ▼
struct ipc_perm
      │
      ▼
key_t key

也就是说:

text 复制代码
key 并没有消失
而是被封装在共享内存的属性结构中

通过 shmctl 获取共享内存属性

在用户程序中,可以通过 shmctl() 获取共享内存的属性信息:

c 复制代码
struct shmid_ds ds;

shmctl(shmid, IPC_STAT, &ds);

随后即可访问其中的字段:

c 复制代码
ds.shm_perm.key

示例:

c 复制代码
printf("key = 0x%x\n", ds.shm_perm.key);

这样就可以打印出共享内存的 key 值


key 的作用总结

key 在 System V IPC 中的主要作用是:

作用 说明
共享资源标识 标识共享内存对象
进程间定位资源 不同进程通过相同 key 找到同一资源
存储位置 内核结构 ipc_perm.key
访问方式 通过 shmctl 查询
因此在共享内存通信流程中:
text 复制代码
进程 A 通过 key 创建共享内存
进程 B 使用同一个 key 获取共享内存

最终:

text 复制代码
两个进程访问同一块物理共享内存

操作系统在内核中使用 shmid_ds 结构描述共享内存对象。该结构包含共享内存大小、创建时间、最近访问时间、当前关联进程数以及权限信息。其中 key 存储在 ipc_perm 子结构中,用于标识和查找共享内存对象。


共享内存大小与页对齐机制

内存分配单位

操作系统在内核中:

以"页(Page)"为基本内存分配单位。

在大多数系统中:

  • 页面大小 = 4KB = 4096 字节
    页对齐原则
    共享内存的分配必须:

按页大小进行向上取整(page alignment)

即:

复制代码
实际分配大小 = ceil(申请大小 / 4096) × 4096

逻辑大小 vs 物理分配

内核内部行为:

  • 按页分配物理内存
  • 实际分配可能大于申请值
    用户视角:
  • 通过 shmctl(IPC_STAT) 查看的 shm_segsz
  • 仍然显示为用户申请的逻辑大小
    内核采用页为单位进行物理内存分配,但共享内存段的逻辑大小保持为用户申请值。物理分配大小与逻辑大小在概念上相互独立。

页对齐的原因

页机制带来的优点:

  1. 简化内存管理
  2. 提高内存分配效率
  3. 支持虚拟内存映射
  4. 减少碎片管理复杂度

若不页对齐可能导致:

  • 越界访问风险
  • 内存保护失效
  • 管理复杂度提升
    因此:

操作系统强制以页为单位进行内存分配。

相关推荐
浅念-1 小时前
C++11 核心知识点整理
开发语言·数据结构·c++·笔记·算法
xiaoye-duck2 小时前
《算法题讲解指南:递归,搜索与回溯算法--二叉树中的深搜》--6.计算布尔二叉树的值,7.求根节点到叶节点数字之和
c++·算法·深度优先·递归
橘子132 小时前
网络层IP协议
网络·tcp/ip·智能路由器
liuyao_xianhui2 小时前
递归_反转链表_C++
java·开发语言·数据结构·c++·算法·链表·动态规划
CoderCodingNo2 小时前
【GESP】C++七级考试大纲知识点梳理 (3) 图论基础与遍历算法
c++·算法·图论
勇闯逆流河2 小时前
【Linux】Linux基础开发工具(git、dbg)
linux·运维·服务器·开发语言·c++·git
小温冲冲2 小时前
C++与QML交互指南:从基础到实战
开发语言·c++·交互
智者知已应修善业2 小时前
【不用第三变量交换2个数】2024-10-18
c语言·数据结构·c++·经验分享·笔记·算法
WX:ywyy67982 小时前
短剧付费转化系统:试看、卡点、解锁、会员全链路商业化设计
网络·短剧·短剧app·短剧系统·短剧系统开发·短剧app开发·短剧系统搭建