前言

进程是操作系统资源分配的最小单位,每个进程拥有独立的虚拟地址空间,进程与进程之间默认不能互相访问对方内存。如果不同进程需要交换数据,就必须依靠进程间通信 (IPC)

前面我们了解了管道、信号、消息队列,本篇重点讲解共享内存有名信号量 (System V 信号量),共享内存是所有 IPC 机制中效率最高的进程间通信方式,而信号量主要用来做同步互斥,解决多进程访问共享资源产生的资源竞争问题。

阅读范围:文档共享内存及以下内容

一、共享内存

1. 什么是共享内存

共享内存是进程间通信效率最高的一种 IPC 方式。 内核在物理内存开辟一块内存空间,多个进程将这块物理内存映射到自己的虚拟地址空间中。多个进程直接读写这块映射后的地址,数据不需要在内核态和用户态之间来回拷贝,因此速度极快。

⚠️注意:共享内存本身不提供同步互斥机制。多个进程同时读写共享内存会产生资源竞争,需要配合信号量 / 互斥锁完成同步控制。

2. 共享内存完整操作步骤

  1. 使用ftok生成 IPC 的 key 键值(IPC 对象标识符)
  2. shmget:创建 / 获取共享内存,得到共享内存 ID (shmid)
  3. shmat:将共享内存映射到当前进程虚拟地址空间,得到可用的内存首地址
  4. 直接对映射后的内存地址读写,完成进程间数据交互
  5. shmdt:进程用完之后,解除映射(不是删除共享内存,只是当前进程脱离这块内存)
  6. shmctl:销毁共享内存对象,释放内核资源

3. 核心 API 函数详解

① ftok — 生成 IPC 键值 key_t
key_t ftok(const char *pathname, int proj_id);
  • 参数
    • pathname:必须是一个存在的文件 / 目录路径
    • proj_id:项目 ID,仅低 8 位有效 (0~255)
  • 返回值:成功返回 key_t 键值;失败返回-1

同一个 pathname + 同一个 proj_id,会生成同一个 key 值,不同进程使用相同 key 就可以拿到同一个共享内存对象。

② shmget — 创建或者获取共享内存
int shmget(key_t key, size_t size, int shmflg);
  • 参数
    • key:ftok 生成的 IPC 键
    • size:共享内存大小,建议为 4K 的整数倍(内存页大小)
    • shmflg:标志位
      • IPC_CREAT:如果共享内存不存在则创建;存在则直接获取
      • IPC_EXCL:配合 IPC_CREAT,如果对象已经存在,则报错返回
      • 还可以或上权限位,如0664
  • 返回:成功返回共享内存 IDshmid;失败返回-1
③ shmat — 把共享内存映射到进程地址空间
void *shmat(int shmid, const void *shmaddr, int shmflg);
  • 参数
    • shmid:shmget 返回的共享内存 id
    • shmaddr:指定映射到进程的哪个虚拟地址,传 NULL 由操作系统自动选地址(推荐)
    • shmflg:映射属性,0 为可读可写;SHM_RDONLY 代表只读映射
  • 返回值:成功返回映射后的内存首地址;失败返回(void*)-1

拿到返回的指针之后,就像操作普通内存一样读写,进程就可以和别的进程交换数据。

④ shmdt — 解除映射
int shmdt(const void *shmaddr);
  • 参数:shmat 返回的映射地址
  • 返回:成功返回 0;失败返回 - 1

⚠️重点:shmdt仅仅是让当前进程脱离共享内存,不会销毁内核中的共享内存对象,其他进程依旧可以使用这块共享内存。

⑤ shmctl — 共享内存控制(获取状态、设置权限、删除)
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
  • 参数
    • shmid:共享内存 ID
    • cmd控制命令:
      • IPC_STAT:获取共享内存属性,存入 buf
      • IPC_SET:修改共享内存权限
      • IPC_RMID销毁共享内存对象,当所有进程都解除映射之后,内核真正释放内存
    • buf:属性结构体指针,IPC_RMID 时传 NULL 即可
  • 返回值:成功返回 0;失败返回 - 1

4. 命令行操作共享内存 (System V IPC)

# 查看所有IPC对象(消息队列、共享内存、信号量)
ipcs

# 只查看共享内存
ipcs -m

# 根据shmid删除共享内存
ipcrm -m shmid

# 根据key键删除共享内存
ipcrm -M key值

5. 简单示例框架

//写进程示例
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>

#define SHM_SIZE 1024

int main()
{
    key_t key = ftok(".", 100);
    int shmid = shmget(key,SHM_SIZE,IPC_CREAT|0664);
    char *p = shmat(shmid,NULL,0);

    strcpy(p,"hello 共享内存");

    shmdt(p);
    return 0;
}

读进程使用相同 key,shmget、shmat 拿到同一块内存,直接读取地址中的内容即可。 实际项目中必须搭配信号量做同步,防止一个进程读的时候另一个进程正在写。

二、System V 有名信号量(信号灯)

注意区分:posix 无名信号量 (线程用) 和 System V 信号灯集合,这里讲的是 IPC 有名信号量,用于进程间同步互斥。 信号量本质是内核维护的资源计数器,用于进程 / 线程之间做同步与互斥,解决共享资源竞争问题。

1. 核心概念

信号量是一个资源计数:

  1. 申请资源(P 操作,semop 减计数):资源 > 0,占用资源,计数 - 1;资源 = 0,进程阻塞等待资源
  2. 释放资源(V 操作,semop 加计数):资源计数 + 1,唤醒等待该资源的阻塞进程

System V 信号量是以信号量数组(集合) 的形式存在,一个信号量集合里面可以包含多个信号量。

2. 操作步骤

  1. ftok 生成 key
  2. semget 创建 / 获取信号量集合,指定集合内信号量的个数
  3. semctl:初始化信号量的计数值;也可以用来删除信号量集合
  4. semop:执行 P (申请资源)、V (释放资源) 操作
  5. 使用完毕调用semctl执行IPC_RMID销毁信号量集合

3. API 详解

① semget 创建信号量集合
int semget(key_t key, int nsems, int semflg);
  • 参数
    • key:ftok 生成 IPC 键
    • nsems:信号量集合里面有多少个信号量
    • semflgIPC_CREAT | IPC_EXCL | 0664
  • 返回:成功返回信号量集合 ID (semid),失败返回 - 1
② semctl 信号量控制:初始化、删除
int semctl(int semid, int semnum, int cmd, ...);
  • 参数
    • semid:信号量集合 id
    • semnum:要操作集合中第几个信号量的下标,从 0 开始
    • cmd
      • SETVAL:设置单个信号量的初始计数值,第四个参数传联合体union semun
      • IPC_RMID:删除整个信号量集合,此时 semnum 参数被忽略

注意:System V 信号量创建之后不会自动初始化,必须手动 SETVAL 设置初值,这是极易踩坑点!

③ semop:执行 P/V 操作(核心)
int semop(int semid, struct sembuf *sops, size_t nsops);

struct sembuf结构体:

unsigned short sem_num;  /* 集合中信号量下标 */
short          sem_op;   /* P/V操作值:负数P申请;正数V释放 */
short          sem_flg;  /* 标志,一般填0;IPC_NOWAIT非阻塞 */
  • sem_op < 0:P 操作,申请资源,计数减去绝对值,资源不足则阻塞
  • sem_op > 0:V 操作,释放资源,计数加上这个正数
  • nsops:本次执行多少个 sembuf 操作

返回值:成功 0;失败 - 1

4. 命令行操作信号量

ipcs -s                 #查看System V信号量集合
ipcrm -s semid          #按id删除信号量集合
ipcrm -S key            #按key删除信号量集合

三、共享内存 + SystemV 信号量使用注意事项

  1. 共享内存没有自带同步,一定要搭配信号量 / 锁,否则出现数据错乱。
  2. System V 信号量创建后不会自动初始化,必须手动SETVAL设置初值。
  3. IPC 对象(共享内存、消息队列、信号量)内核不会自动回收,进程退出不会自动删除,程序异常退出容易造成内核 IPC 对象泄露,需要手动调用 semctl/shmctl 删除,或者使用ipcrm命令清理。
  4. 信号量的sem_flg默认 0 是阻塞等待,如果设置IPC_NOWAIT,资源不足时不会阻塞,直接返回错误。

四、补充:System V IPC 共性总结

表格

IPC 对象 创建函数 删除方式 查看命令
消息队列 msgget msgctl IPC_RMID ipcs -q
共享内存 shmget shmctl IPC_RMID ipcs -m
信号量集合 semget semctl IPC_RMID ipcs -s

全部依赖ftok生成 key,都可以通过ipcs/ipcrm命令行调试。

提示:现在新开发项目也会推荐 POSIX 信号量、mmap 共享内存,但是 System V 这套是嵌入式、Linux 后端面试高频考点。

五、面试常考问题

  1. 共享内存为什么通信速度最快?

数据不需要在内核缓冲区和用户缓冲区之间来回拷贝,进程直接映射物理内存,用户态直接读写物理内存。

  1. 共享内存既然这么快,还需要信号量干什么?

共享内存只负责数据传输,没有同步互斥能力;信号量负责控制访问时序,防止多个进程同时读写产生数据竞争。

  1. shmdt 之后共享内存还存在吗?

存在,shmdt 仅仅解除本进程映射;必须 shctl IPC_RMID 才会标记删除,等所有进程全部解除映射才真正释放内存。

  1. System V 信号量和 Posix 信号量区别?

System V 是信号量集合,多用于进程间;Posix 分为有名 / 无名,无名多用于线程同步。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐