一、理解共享内存的核心基础
1. 共享内存是什么?
普通进程的内存是相互隔离的(进程A不能直接访问进程B的内存),而共享内存 是操作系统在物理内存中开辟的一块公共内存区域,让多个进程可以直接映射到自己的私有地址空间中,就像操作自己的局部变量一样操作这块内存,是进程间通信中速度最快的方式(无需数据拷贝,直接访问物理内存)。
2. 核心原理(4个函数对应4个步骤)
整个共享内存的使用流程就像"租房-入住-退房-销毁房源":
shmget():创建/获取共享内存(相当于找中介租/找一套已有的房源,获得房源编号)shmat():挂载共享内存(相当于拿到钥匙,把共享内存映射到自己的进程地址空间,才能读写)shmdt():卸载共享内存(相当于退房,断开自己的进程与共享内存的映射,不再访问)shmctl():控制/销毁共享内存(相当于房源到期,联系中介销毁房源,释放物理内存)
3. 共享内存的生命周期
共享内存的生命周期不依赖于进程 ,而是依赖于操作系统内核:
- 即使创建/使用共享内存的进程全部退出,共享内存也会一直存在于内核中,直到被
shmctl()主动销毁,或者重启操作系统。 - 这一点和管道不同(管道随进程退出而销毁),也是共享内存的关键特点(避免数据丢失,但也可能造成内存泄露,必须手动销毁)。
4. 关键前提:两个进程如何找到同一块共享内存?
靠共享内存的key值 (一个唯一的整数,相当于房源的唯一编号),两个进程必须使用相同的 key 值,才能找到同一块共享内存,实现通信。
二、分步实现代码(两个独立文件:进程A、进程B)
因为是两个独立进程(不是父子进程,更贴近实际开发场景),我们创建两个 .c 文件,分别对应进程A(写数据)和进程B(读数据)。
前置准备
编译运行依赖GCC编译器,两个文件要在同一目录下操作。
三、进程A(写数据:shm_writer.c,对应需求中的进程A)
功能:创建共享内存 → 挂载 → 写入 "i am process A" → 卸载 → (可选:等待进程B读取后销毁)
c
#include <stdio.h>
#include <sys/ipc.h> // 共享内存、消息队列等IPC通信的头文件
#include <sys/shm.h> // 共享内存核心函数的头文件
#include <string.h>
#include <unistd.h>
// 1. 定义共享内存的关键参数(两个进程必须一致!)
#define SHM_KEY 0x12345678 // 唯一key值(自定义,只要是非0整数即可,两个进程要相同)
#define SHM_SIZE 1024 // 共享内存大小(1024字节,足够存储要写入的字符串)
int main() {
int shmid; // 共享内存ID(相当于房源编号,由shmget返回)
char *shm_addr; // 挂载后,共享内存在当前进程中的地址(相当于房间的门牌号)
// 2. 步骤1:创建共享内存(shmget())
// 参数说明:
// ① key:共享内存唯一标识(两个进程要相同)
// ② size:共享内存大小(字节)
// ③ IPC_CREAT | IPC_EXCL | 0666:创建新的共享内存,若已存在则报错;0666是读写权限(所有用户可读写)
shmid = shmget((key_t)SHM_KEY, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666);
if (shmid == -1) {
perror("shmget failed (创建共享内存失败)");
return 1;
}
printf("进程A:共享内存创建成功,共享内存ID = %d\n", shmid);
// 3. 步骤2:挂载共享内存(shmat())
// 参数说明:
// ① shmid:共享内存ID(shmget返回的房源编号)
// ② shmaddr:指定映射地址(NULL表示让系统自动分配,推荐)
// ③ shmflg:挂载权限(0表示可读可写)
shm_addr = (char *)shmat(shmid, NULL, 0);
if (shm_addr == (char *)-1) { // 挂载失败返回 (void*)-1,强制转换为char*判断
perror("shmat failed (挂载共享内存失败)");
// 挂载失败,先销毁已创建的共享内存,避免内存泄露
shmctl(shmid, IPC_RMID, NULL);
return 1;
}
printf("进程A:共享内存挂载成功,映射地址 = %p\n", shm_addr);
// 4. 步骤3:向共享内存写入数据(直接操作shm_addr,和操作普通数组一样)
const char *write_data = "i am process A";
// 把字符串复制到共享内存(strcpy:字符串拷贝,自动携带\0结束符)
strcpy(shm_addr, write_data);
printf("进程A:已向共享内存写入数据:%s\n", write_data);
// 5. 步骤4:卸载共享内存(shmdt())
// 参数:shm_addr(挂载时返回的映射地址,相当于归还钥匙)
if (shmdt(shm_addr) == -1) {
perror("shmdt failed (卸载共享内存失败)");
} else {
printf("进程A:共享内存卸载成功\n");
}
// 6. (可选)等待进程B读取完成后,销毁共享内存(避免手动清理)
// 共享内存生命周期不依赖进程,这里休眠10秒,给进程B足够的读取时间
printf("进程A:休眠10秒,等待进程B读取数据...\n");
sleep(10);
// 7. 步骤5:销毁共享内存(shmctl())
// 参数说明:
// ① shmid:共享内存ID
// ② IPC_RMID:执行销毁操作(释放物理内存)
// ③ buf:额外参数(NULL表示无需返回共享内存信息)
if (shmctl(shmid, IPC_RMID, NULL) == -1) {
perror("shmctl failed (销毁共享内存失败)");
return 1;
}
printf("进程A:共享内存已销毁,程序退出\n");
return 0;
}
四、进程B(读数据:shm_reader.c,对应需求中的进程B)
功能:获取已创建的共享内存 → 挂载 → 读取内容并打印 → 卸载
c
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>
#include <unistd.h>
// 注意:这里的SHM_KEY和SHM_SIZE必须和进程A完全一致!
#define SHM_KEY 0x12345678
#define SHM_SIZE 1024
int main() {
int shmid;
char *shm_addr;
// 1. 步骤1:获取已存在的共享内存(shmget(),不创建新的)
// 参数说明:去掉 IPC_EXCL,只获取已存在的共享内存(进程A已创建)
shmid = shmget((key_t)SHM_KEY, SHM_SIZE, 0666);
if (shmid == -1) {
perror("shmget failed (获取共享内存失败,可能进程A未运行)");
return 1;
}
printf("进程B:共享内存获取成功,共享内存ID = %d\n", shmid);
// 2. 步骤2:挂载共享内存(和进程A用法一致)
shm_addr = (char *)shmat(shmid, NULL, 0);
if (shm_addr == (char *)-1) {
perror("shmat failed (挂载共享内存失败)");
return 1;
}
printf("进程B:共享内存挂载成功,映射地址 = %p\n", shm_addr);
// 3. 步骤3:从共享内存读取数据并打印(直接操作shm_addr)
printf("进程B:从共享内存读取到数据:%s\n", shm_addr);
// 4. 步骤4:卸载共享内存(和进程A用法一致)
if (shmdt(shm_addr) == -1) {
perror("shmdt failed (卸载共享内存失败)");
} else {
printf("进程B:共享内存卸载成功,程序退出\n");
}
return 0;
}
五、编译与运行(关键步骤,必须按顺序)
1. 编译两个文件
打开终端,进入文件所在目录,执行两条编译命令:
bash
# 编译进程A(写数据)
gcc shm_writer.c -o shm_writer
# 编译进程B(读数据)
gcc shm_reader.c -o shm_reader
2. 运行顺序(必须先运行进程A,再运行进程B)
第一步:运行进程A(创建共享内存并写入数据)
bash
./shm_writer
此时终端会输出:
进程A:共享内存创建成功,共享内存ID = xxxx
进程A:共享内存挂载成功,映射地址 = 0xxxxxxxx
进程A:已向共享内存写入数据:i am process A
进程A:共享内存卸载成功
进程A:休眠10秒,等待进程B读取数据...
进程A会进入10秒休眠,此时共享内存已创建且数据已写入,等待进程B读取。
第二步:快速打开另一个终端,运行进程B(读取数据)
bash
./shm_reader
此时终端会输出:
进程B:共享内存获取成功,共享内存ID = xxxx(和进程A的ID一致)
进程B:共享内存挂载成功,映射地址 = 0xxxxxxxx(可能和进程A不同,正常)
进程B:从共享内存读取到数据:i am process A
进程B:共享内存卸载成功,程序退出
第三步:等待10秒后,进程A终端输出
进程A:共享内存已销毁,程序退出
六、关键函数详细解释
1. shmget() - 创建/获取共享内存
- 核心作用:申请或查找一块共享内存,返回唯一的共享内存ID(
shmid)。 - 关键参数:
key:共享内存唯一标识,两个进程必须相同,才能找到同一块共享内存。size:共享内存大小,必须是正整数(推荐为页面大小的整数倍,一般4096字节,这里用1024足够)。flag:权限标志,IPC_CREAT(创建新的)、IPC_EXCL(和IPC_CREAT配合,若已存在则报错)、0666(读写权限,和文件权限一致)。
2. shmat() - 挂载共享内存
- 核心作用:将共享内存映射到当前进程的地址空间,返回映射后的内存地址(
shm_addr),只有挂载后才能读写共享内存。 - 关键参数:
shmid(shmget返回的ID)、NULL(系统自动分配映射地址)、0(可读可写权限)。 - 注意:返回值为
(void*)-1表示挂载失败,需要强制转换为对应类型判断。
3. shmdt() - 卸载共享内存
- 核心作用:断开当前进程与共享内存的映射关系(相当于"退房"),但不会销毁共享内存。
- 关键参数:
shm_addr(shmat返回的映射地址)。 - 注意:进程退出前必须卸载,否则会造成资源泄露。
4. shmctl() - 控制/销毁共享内存
- 核心作用:对共享内存进行控制操作,最常用的是
IPC_RMID(销毁共享内存,释放物理内存)。 - 关键参数:
shmid(共享内存ID)、IPC_RMID(销毁操作)、NULL(无需额外信息)。 - 注意:只有调用该函数,共享内存才会真正被销毁,否则会一直存在于内核中。
七、常见问题与注意事项
-
运行进程B报错"获取共享内存失败" :原因是没先运行进程A,或者进程A的
SHM_KEY和进程B不一致。 -
共享内存泄露 :如果进程A异常退出,没有执行
shmctl()销毁共享内存,可通过以下命令手动查看和删除:bash# 查看所有共享内存 ipcs -m # 删除指定ID的共享内存(替换xxxx为共享内存ID) ipcrm -m xxxx -
字符串写入注意 :必须保证共享内存大小足够存储字符串(包括
\0),否则会造成内存越界。
总结
- 共享内存是内核中的公共物理内存,生命周期不依赖进程,需用
shmctl()手动销毁,核心流程是"创建/获取→挂载→读写→卸载→销毁"。 - 四个核心函数分工明确:
shmget()(创/取)、shmat()(挂载)、shmdt()(卸载)、shmctl()(销毁)。 - 两个进程必须使用相同的
key值和共享内存大小,才能实现通信,运行顺序需遵循"先写后读"。 - 共享内存是最快的进程间通信方式,无需数据拷贝,直接操作物理内存,适合传输大量数据。