共享内存 多进程并发竞争:非原子操作引发数据丢失
2026/9/7 20:30:47 网站建设 项目流程

一、基础概念

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,还没写回,被系统暂停;
  2. 子进程同样读取到旧值 100;
  3. 子进程算 100+1=101,写回内存;
  4. 切回父进程,父进程也算出 101,再次覆盖写回。

结果:(父进程)两次自增,只加了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 编号删除。

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

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

立即咨询