Linux IPC全揭秘(四):共享内存:最高效IPC实现
System V 共享内存
共享内存是 Linux 下最快的进程间通信(IPC)形式。
一、System V 共享内存的定义与本质
1. 什么是共享内存?
共享内存 是一块可以被多个进程直接访问的物理内存区域。System V 共享内存是 AT&T System V 操作系统引入的 IPC 机制,后被所有 Unix 系统(包括 Linux)采用。
- 关键特性:多个进程将同一块物理内存映射到各自的虚拟地址空间。
- 数据传递:一个进程写入共享内存,另一个进程立即可以看到,中间不经过内核,也没有数据拷贝。

2. 核心数据结构
每个共享内存段在内核中由 struct 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; // 当前附加的进程数
};
3. 标识方式
- key_t:全局键值,通常由
ftok()生成,用于多个进程协商同一块共享内存。 - shmid:成功创建或获取后返回的整数标识符,后续所有操作(附加、控制、删除)都使用它。
4. 生命周期
System V 共享内存的生命周期随内核:
- 即使所有进程都调用了
shmdt(分离),共享内存对象依然存在。 - 必须显式调用
shmctl(IPC_RMID)或使用命令ipcrm -m shmid删除,否则会一直占用资源,直到系统重启。
容易忽视:这与管道、FIFO 等随进程结束而自动销毁的 IPC 完全不同,务必注意资源释放。
二、共享内存的优势与必须面对的挑战
1. 为什么需要共享内存?—— 传统 IPC 的性能瓶颈
以管道或消息队列为例,数据传递路径为:
进程A → 内核缓冲区 → 进程B
数据需要在内核空间与用户空间之间拷贝两次,每次拷贝都涉及系统调用和上下文切换,开销较大。
而共享内存的路径是:
进程A → 物理内存 ← 进程B
一旦映射完成,进程读写共享内存就像访问自己的栈或堆一样,不再需要系统调用,数据拷贝次数为 零。因此共享内存是最快的 IPC 机制。
2. 共享内存的最大缺点:无内置同步
共享内存本身不提供任何同步与互斥机制。如果多个进程同时写入同一位置,数据会被破坏;如果读写并发,可能读到不完整的数据。
例如:进程 A 正在写一个 10 字节的结构,写到第 5 个字节时被调度出去,进程 B 来读,就会读到一半新一半旧的数据。
结论:使用共享内存时,必须配合信号量或互斥锁等同步机制,否则程序必然出错。
三、API 详解、代码示例、同步方案与管理
1. 核心 API 讲解
shmget —— 创建或获取共享内存
int shmget(key_t key, size_t size, int shmflg);
key:通常由ftok()生成,也可用IPC_PRIVATE(用于父子进程)。size:共享内存大小(字节),建议为系统页大小(通常 4096)的整数倍。shmflg:权限标志(如0666)与IPC_CREAT、IPC_EXCL的组合。IPC_CREAT:若不存在则创建。IPC_EXCL:与IPC_CREAT同时使用,若已存在则返回错误。
- 返回值:成功返回
shmid,失败返回 -1。
shmat —— 附加到进程地址空间
void *shmat(int shmid, const void *shmaddr, int shmflg);
shmaddr:指定附加的虚拟地址,通常传NULL让内核自动选择。shmflg:SHM_RDONLY:只读附加。SHM_RND:配合非空shmaddr时,将地址向下取整到SHMLBA的倍数。
- 返回值:成功返回映射的虚拟地址,失败返回
(void*)-1。
shmdt —— 分离
int shmdt(const void *shmaddr);
- 参数为
shmat返回的地址。 - 成功返回 0,失败返回 -1。
- 注意:分离并不删除共享内存,只是当前进程不再访问它。
shmctl —— 控制
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
常用 cmd:
IPC_RMID:标记共享内存为待删除。当所有进程都分离后,系统真正释放资源。IPC_STAT:获取状态信息,存入buf。IPC_SET:修改权限等信息。
2. 完整示例(无同步版本)
下面演示一个简单的“服务器写入 A~Z,客户端读取并打印”的例子。
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
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 -2;
}
return shmid;
}
int createShm(int size)
{
return commShm(size, IPC_CREAT | IPC_EXCL | 0666);
}
int getShm(int size)
{
return commShm(size, IPC_CREAT | 0666);
}
int destroyShm(int shmid)
{
if(shmctl(shmid, IPC_RMID, NULL) < 0){
perror("shmctl");
return -1;
}
return 0;
}
server.c(生产者)
#include "comm.h"
#include <unistd.h>
int main()
{
int shmid = createShm(4096);
char *addr = shmat(shmid, NULL, 0);
sleep(2); // 等待客户端附加
int i = 0;
while(i < 26){
addr[i] = 'A' + i;
i++;
addr[i] = 0;
sleep(1);
}
shmdt(addr);
sleep(2);
return 0;
}
client.c(消费者)
#include "comm.h"
#include <unistd.h>
int main()
{
int shmid = getShm(4096);
sleep(1);
char *addr = shmat(shmid, NULL, 0);
sleep(2); // 等待服务器开始写入
int i = 0;
while(i < 26){
printf("client# %s\n", addr);
sleep(1);
i++;
}
shmdt(addr);
destroyShm(shmid);
return 0;
}
运行结果可能不确定(例如读到空串或不完整的字符串),这正是因为没有同步导致的。
3. 怎么做同步?—— 三种常用方案
因为共享内存自身无同步,必须额外加锁或信号量。
方案一:POSIX 命名信号量(推荐)
#include <semaphore.h>
sem_t *sem = sem_open("/mysem", O_CREAT, 0666, 1);
sem_wait(sem); // P 操作
// 访问共享内存
sem_post(sem); // V 操作
sem_close(sem);
sem_unlink("/mysem");
方案二:System V 信号量
int semid = semget(key, 1, IPC_CREAT | 0666);
struct sembuf sb = {0, -1, 0}; // P 操作
semop(semid, &sb, 1);
// 访问共享内存
sb.sem_op = 1; // V 操作
semop(semid, &sb, 1);
方案三:跨进程互斥锁(需共享内存)
pthread_mutex_t *mutex = mmap(...);
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED);
pthread_mutex_init(mutex, &attr);
关键:无论哪种方案,同步对象本身必须放在所有进程都能访问的地方(命名信号量自动实现;匿名信号量和互斥锁需要放在共享内存中)。
4. 共享内存的管理与调试
查看系统中的共享内存段
ipcs -m
输出示例:
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x12345678 32768 user 666 4096 2
nattch:当前附加到此段的进程数。status:若显示dest表示已被标记删除(IPC_RMID)。
手动删除共享内存
ipcrm -m shmid
# 或按 key 删除
ipcrm -M key
常见问题与解决
| 问题 | 可能原因 | 解决方法 |
|---|---|---|
shmget 返回 EEXIST |
对象已存在且使用了 IPC_EXCL |
去掉 IPC_EXCL 或改用 IPC_CREAT |
| 进程退出后共享内存仍存在 | 未调用 shmctl(IPC_RMID) |
在程序中注册 atexit 或信号处理函数删除 |
shmat 返回 EINVAL |
大小超过系统限制或 shmid 无效 |
检查 shmid,调整 shmmax |
| 数据错乱 | 缺少同步 | 加入信号量或互斥锁 |
5. 补充:System V 共享内存 vs POSIX 共享内存
| 特性 | System V | POSIX |
|---|---|---|
| 创建 | shmget + shmat |
shm_open + mmap |
| 标识 | key_t + shmid |
文件路径名(/name) |
| 大小 | 创建时固定 | ftruncate 可动态调整 |
| 删除 | shmctl(IPC_RMID) |
shm_unlink |
| 查看工具 | ipcs -m |
ls /dev/shm |
| 接口风格 | 专用 IPC 函数 | 类文件操作 |
| 新项目推荐 | 不推荐 | 推荐 |
建议:除非需要兼容老旧系统,否则新项目优先使用 POSIX 共享内存。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)