一、基础概念
1. 共享内存
共享内存是 Linux 常用 IPC 通信方式,由内核开辟一块独立物理内存,允许多个进程同时映射、读写同一份数据,是多进程数据共享的核心方案。
2. 非原子操作与竞态条件
原子操作:一次性执行完成,中途不可被系统打断。
自增 ++ 不是原子操作,底层拆分为三步独立 CPU 指令:
1. 读:从内存读取数值到寄存器 2. 改:寄存器数值 +1 3. 写:新值写回内存
系统可在三步之间切换进程,多进程并行操作会出现指令交错、数据覆盖,产生竞态条件,最终导致数据丢失。
二、完整代码(小白友好注释)
#include <sys/shm.h>
#include <sys/ipc.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
// 用于生成唯一IPC键值
#define SHARE_PATH "/home/china"
#define PROJ_ID 251
int main()
{
// 1. 生成IPC唯一键值
key_t key = ftok(SHARE_PATH, PROJ_ID);
if (key == -1) { perror("ftok"); return -1; }
// 2. 创建1000字节共享内存
int shmid = shmget(key, 1000, IPC_CREAT | 0666);
if (shmid == -1) { perror("shmget"); return -1; }
// 3. 映射共享内存到当前进程虚拟空间
int *p = (int *)shmat(shmid, NULL, 0);
if (p == (void *)-1) { perror("shmat"); return -1; }
*p = 0; // 初始化共享计数器
// 4. 创建父子进程并发执行
pid_t pid = fork();
if (pid == 0)
{
// 子进程:自增100万次
int i = 0;
while (i++ < 1000000)
(*p)++;
}
else if (pid > 0)
{
// 父进程:自增100万次
int i = 0;
while (i++ < 1000000)
(*p)++;
wait(NULL); // 等待子进程结束
printf("最终计数 data = %d\\n", *p);
}
shmdt(p); // 解除映射
shmctl(shmid, IPC_RMID, NULL); // 主动销毁共享内存,避免内核残留
return 0;
}
三、运行效果
理论值:2000000(父子各累加100万次) 实际值:随机数值,始终小于 2000000
现象:并发无同步保护,出现大量计数丢失。
四、原理详解
1. 为什么会丢数据?
自增操作分三步,系统可以随时切进程,导致两个进程读到同一个旧值:
结果:(父进程)两次自增,只加了1次,数据丢失一次。
循环一百万次,这种覆盖反复发生,最终数值远远小于理论值。
同步锁的作用:保证「读-改-写」三步一次性做完,中途不允许其他进程插队。
补充:
循环设为 100 万而非 10 万,延长运行时间,提升进程切换概率,更容易复现数据丢失。(如果只循环 10 万次,程序运行太快,系统来不及切换进程,大概率看不到丢数现象)
2. 为什么不用全局变量?
fork 后父子进程虚拟地址空间相互独立,内核采用写时拷贝 COW 机制:
- 初始父子共享同一块物理内存;
- 一旦任意进程修改全局变量,内核会为该进程复制一份全新物理内存。
修改后父子数据彻底隔离,无法共享计数,因此必须使用共享内存。
五、总结
1. 普通全局变量受 COW 机制隔离,多进程共享数据只能使用共享内存;
2. ++ 是非原子操作,读改写拆分是并发数据丢失的根本原因;
3. 多进程并发写共享资源,必须加同步机制(互斥锁/信号量)保护临界区;
4. 程序结束需调用 shmctl(IPC_RMID),主动释放共享内存资源。
六、运行须知
1. 提前执行 mkdir /home/china,防止 ftok 报错;
2. 异常退出可手动清理残留:ipcs -m 查看、ipcrm -m 编号 删除。




