Linux 进程间通信 (IPC):System V 共享内存从原理到实战
2026/6/10 13:11:00 网站建设 项目流程

大家好,我是程序员小青蛙,今天介绍共享内存。

在 Linux 操作系统中,进程间通信 (IPC) 是一个至关重要的概念,它允许不同的进程之间进行数据交换和同步。在众多 IPC 方式中,System V 共享内存以其极致的性能脱颖而出,被誉为最快的进程间通信方式。本文将从原理、API、实战案例到优缺点,带你全面掌握 System V 共享内存。

一、共享内存:为什么是最快的 IPC?

1.1 核心原理

共享内存的本质是:让多个进程的虚拟地址空间映射到同一块物理内存区域

当进程 A 和进程 B 都将同一块物理内存映射到自己的虚拟地址空间后,它们对这块内存的读写操作就直接作用于物理内存,完全不需要经过内核的中转

对比传统的管道、消息队列等 IPC 方式:

  • 管道 / 消息队列:需要经历用户态缓冲区 → 内核缓冲区 → 用户态缓冲区两次数据拷贝
  • 共享内存:直接操作物理内存,仅需一次拷贝(数据写入物理内存)

这就是共享内存速度远超其他 IPC 方式的根本原因。

1.2 内存映射示意图

在 Linux 进程的虚拟地址空间布局中,共享内存通常被映射到栈和堆之间的区域(大约 0x40000000 附近)。

1.3 关键特性:生命周期随内核

这是 System V IPC(共享内存、消息队列、信号量)最重要的特性之一:

  • 共享内存的生命周期不随创建它的进程结束而结束
  • 它会一直存在于内核中,直到:
    1. 手动调用shmctl(IPC_RMID)删除
    2. 系统重启
  • 这意味着:如果程序异常退出而没有删除共享内存,会造成内核资源泄漏

二、核心数据结构与 API 详解

2.1 共享内存描述符:struct shmid_ds

内核为每个共享内存段维护一个shmid_ds结构体,记录其所有属性:

struct shmid_ds { struct ipc_perm shm_perm; /* 操作权限 */ int shm_segsz; /* 共享内存段大小(字节) */ __kernel_time_t shm_atime; /* 最后一次挂载时间 */ __kernel_time_t shm_dtime; /* 最后一次解除挂载时间 */ __kernel_time_t shm_ctime; /* 最后一次修改时间 */ __kernel_ipc_pid_t shm_cpid; /* 创建者PID */ __kernel_ipc_pid_t shm_lpid; /* 最后一次操作PID */ unsigned short shm_nattch; /* 当前挂载的进程数 */ unsigned short shm_unused; /* 兼容保留 */ void *shm_unused2; /* DIP使用 */ void *shm_unused3; /* 未使用 */ };

其中shm_nattch字段非常重要,它记录了当前有多少个进程挂载了这个共享内存段。当我们调用IPC_RMID删除共享内存时,内核会先将其标记为删除状态,直到shm_nattch变为 0 时,才会真正释放物理内存。

2.2 四大核心函数

1.shmget:创建或获取共享内存
#include <sys/ipc.h> #include <sys/shm.h> int shmget(key_t key, size_t size, int shmflg);
  • 功能:创建一个新的共享内存段,或获取一个已存在的共享内存段
  • 参数
    • key:共享内存的唯一标识符,通常由ftok()生成
    • size:共享内存段的大小(字节),创建时必须指定,获取时可设为 0
    • shmflg:标志位,用法与文件权限类似
      • IPC_CREAT:如果不存在则创建,存在则获取
      • IPC_EXCL:与IPC_CREAT一起使用,如果已存在则返回错误(保证创建的是新的)
      • 权限位:如0666,表示所有用户都有读写权限
  • 返回值:成功返回共享内存标识符shmid,失败返回 - 1

生成 key 的常用方法

key_t key = ftok(".", 0x6666); // 使用当前目录和项目ID生成唯一key
2.shmat:将共享内存挂载到进程地址空间
void *shmat(int shmid, const void *shmaddr, int shmflg);
  • 功能:将shmid标识的共享内存段映射到调用进程的虚拟地址空间
  • 参数
    • shmid:由shmget返回的共享内存标识符
    • shmaddr:指定映射的虚拟地址,建议设为 NULL,让内核自动选择合适的地址
    • shmflg:挂载标志
      • SHM_RDONLY:以只读方式挂载
      • 0:以读写方式挂载
  • 返回值:成功返回指向共享内存首地址的指针,失败返回(void *)-1
3.shmdt:解除共享内存挂载
int shmdt(const void *shmaddr);
  • 功能:将共享内存段从当前进程的虚拟地址空间中分离
  • 参数shmaddrshmat返回的指针
  • 返回值:成功返回 0,失败返回 - 1
  • 注意:这只是解除挂载,并不会删除共享内存段
4.shmctl:控制共享内存
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
  • 功能:对共享内存段进行各种控制操作
  • 参数
    • shmid:共享内存标识符
    • cmd:要执行的命令
      • IPC_STAT:获取共享内存的状态,填充到buf
      • IPC_SET:设置共享内存的属性(需要足够权限)
      • IPC_RMID删除共享内存段(标记删除,等待所有进程解除挂载后释放)
    • buf:用于存储或设置共享内存属性的结构体指针
  • 返回值:成功返回 0,失败返回 - 1

三、实战案例:服务端 - 客户端通信

我们将实现一个简单的 C/S 架构程序:客户端向共享内存写入 A-Z 的字母,服务端从共享内存中读取并打印。

3.1 代码结构

. ├── comm.h // 公共头文件 ├── comm.c // 公共函数实现 ├── server.c // 服务端代码 ├── client.c // 客户端代码 └── Makefile // 编译脚本

3.2 公共头文件comm.h

#ifndef COMM_H #define COMM_H #include <stdio.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> #define PATHNAME "." #define PROJ_ID 0x6666 // 创建共享内存 int createShm(int size); // 获取已存在的共享内存 int getShm(int size); // 删除共享内存 int destroyShm(int shmid); #endif

3.3 公共函数实现comm.c

#include "comm.h" // 通用的共享内存获取/创建函数 static int commShm(int size, int flags) { key_t key = ftok(PATHNAME, PROJ_ID); if (key < 0) { perror("ftok"); return -1; } int shmid = shmget(key, size, flags); if (shmid < 0) { perror("shmget"); return -1; } return shmid; } int createShm(int size) { // IPC_CREAT|IPC_EXCL:确保创建一个全新的共享内存 return commShm(size, IPC_CREAT | IPC_EXCL | 0666); } int getShm(int size) { // 只获取已存在的共享内存 return commShm(size, IPC_CREAT); } int destroyShm(int shmid) { if (shmctl(shmid, IPC_RMID, NULL) < 0) { perror("shmctl"); return -1; } return 0; }

3.4 服务端代码server.c

#include "comm.h" #include <unistd.h> int main() { // 创建4096字节的共享内存 int shmid = createShm(4096); printf("服务端创建共享内存成功,shmid: %d\n", shmid); // 挂载共享内存 char *addr = shmat(shmid, NULL, 0); if (addr == (void *)-1) { perror("shmat"); return 1; } // 循环读取共享内存中的数据 int i = 0; while (i < 26) { printf("Client# %s\n", addr); sleep(1); i++; } // 解除挂载 shmdt(addr); printf("服务端解除挂载\n"); // 删除共享内存 sleep(2); destroyShm(shmid); printf("服务端删除共享内存\n"); return 0; }

3.5 客户端代码client.c

#include "comm.h" #include <unistd.h> #include <string.h> int main() { // 获取服务端创建的共享内存 int shmid = getShm(4096); printf("客户端获取共享内存成功,shmid: %d\n", shmid); // 挂载共享内存 char *addr = shmat(shmid, NULL, 0); if (addr == (void *)-1) { perror("shmat"); return 1; } // 向共享内存写入A-Z int i = 0; while (i < 26) { addr[i] = 'A' + i; addr[i+1] = '\0'; sleep(1); i++; } // 解除挂载 shmdt(addr); printf("客户端解除挂载\n"); sleep(2); return 0; }

3.6 编译与运行

  1. 编写Makefile
.PHONY: all clean all: server client server: server.c comm.c gcc -o $@ $^ client: client.c comm.c gcc -o $@ $^ clean: rm -f server client
  1. 编译运行:
# 编译 make # 先启动服务端 ./server # 再打开另一个终端启动客户端 ./client

3.7 运行结果

服务端会依次打印:

Client# A Client# AB Client# ABC ... Client# ABCDEFGHIJKLMNOPQRSTUVWXYZ

3.8 常见问题与解决

问题:第二次运行服务端时,提示shmget: File exists原因:上一次运行的服务端异常退出,没有删除共享内存,导致共享内存仍然存在于内核中解决

  1. 查看系统中的共享内存:ipcs -m
  2. 删除指定的共享内存:ipcrm -m <shmid>

四、共享内存的优缺点分析

4.1 优点

  • 速度最快:所有 IPC 方式中性能最高,适合大数据量传输
  • 灵活性高:可以直接通过指针操作内存,支持任意数据结构
  • 无数据拷贝:避免了内核态和用户态之间的多次数据拷贝

4.2 缺点

  • 没有同步互斥机制:这是共享内存最大的缺点。多个进程同时读写共享内存时,会出现数据不一致的问题。必须配合信号量、互斥锁等同步机制使用
  • 生命周期随内核:如果程序忘记删除共享内存,会造成内核资源泄漏
  • 没有数据保护:任何挂载了共享内存的进程都可以随意修改数据,安全性较低

五、总结与扩展

System V 共享内存是 Linux 下性能最优的进程间通信方式,特别适合需要频繁、大量传输数据的场景。但它的缺点也很明显 —— 缺乏同步机制,需要开发者自己实现进程间的同步与互斥。

在实际开发中,我们通常会将共享内存 + 信号量结合使用:用共享内存传输数据,用信号量保证数据的一致性。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询