news 2026/10/21 21:34:05

Linux共享内存原理与实战:从System V API到同步排坑全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux共享内存原理与实战:从System V API到同步排坑全解析

共享内存这个东西,做 Linux 下多进程开发的人迟早会碰见。尤其是写过管道、消息队列,嫌它们数据拷贝太多、延迟太高之后,你就会开始认真琢磨:到底有没有一种方式,能让两个进程像操作自己内存一样直接读写同一份数据?答案就是共享内存。它把“数据交换”这件事的效率拉到极致,进程通信里传输大块数据、高频读写、多读多写的场景,基本都是它的主场。

这篇博文我不打算讲空泛的概念,直接从原理、API、手写 Demo 到排坑经验一条龙讲透。适合正在学 Linux 进程通信的同学,也适合已经在实际项目里用共享内存但总是踩同步坑的开发者。你读完可以直接照着代码跑一遍,再回来看问题排查部分,基本就能把这块啃下来。

1. 共享内存:进程通信里的效率王者

1.1 先盘一遍Linux进程通信的全部家当

Linux 下进程间通信的方式一只手数不过来:管道(匿名管道和 FIFO 命名管道)、信号、消息队列、信号量、共享内存,再加上网络用 Socket。每种方式解决的需求不一样,性能差异也很大。

拿传数据举例,管道和消息队列的核心动作都是“拷贝”:发送方把数据从用户空间拷贝到内核空间,接收方再从内核空间拷贝回自己的用户空间。一次通信至少两次拷贝,数据量大一点,CPU 开销立刻上来了。信号适合传通知,不适合传大块数据。信号量本身不传数据,只做同步和互斥。

共享内存的思路完全不一样:内核划出一块物理内存,多个进程各自通过页表映射到同一块物理页。写入方直接往自己地址空间里写,读取方立刻就能在自己地址空间里看到,全程没有内核态和用户态之间的数据搬运。

这也是为什么在进程通信的性能对比里,共享内存的延迟通常比管道低一个数量级,吞吐则高出一个数量级。对于单机内高频、大批量的数据交换,共享内存基本是最优解。

1.2 为什么说共享内存是效率之王

把数据拷贝的次数列出来看就非常直观:

  • 管道:写端 write 进内核缓冲区,读端 read 出内核缓冲区,两次拷贝。
  • 消息队列:msgsnd 进内核队列,msgrcv 出内核队列,两次拷贝。
  • 共享内存:写端直接写物理页,读端直接读物理页,零次拷贝。

零拷贝意味着 CPU 不需要反复搬运数据,也不需要每次都陷入内核做读写系统调用。实际项目里,一秒钟要传几千次几 KB 的数据包时,用管道 CPU 占用率可能烧到百分之二三十,共享内存能压到个位数。

共享内存还有另一个管道做不到的优势:天然支持多读。一个进程写,多个进程同时读同一块内存,读者之间互不干扰,这在某些一写多读的监控、分发场景里非常有用。

但代价也随之而来——内核不帮你做任何同步。管道有内核缓冲区做隔离和阻塞,消息队列也有内核队列保证一条消息被完整读取,共享内存则是一片裸内存,谁先写谁后写、会不会读一半,全靠开发者自己控制。

1.3 System V和POSIX,两条路线怎么选

Linux 上的共享内存有两大体系:System V 那一套老牌接口,和 POSIX 那一套更现代的标准接口。两者功能等价,但 API 风格和使用体验差别不小。

System V 用的是 key_t + id 的模型:先用 ftok 生成一个 key,再通过 shmget 创建或获取共享内存段,拿到整型 ID,之后用 shmat/shmdt 做挂载和卸载。这套模型在传统 Unix 上沉淀了几十年,老项目里出镜率极高,内核里还保留了 ipcs 这种专门查看 System V IPC 对象的命令。

POSIX 共享内存则是“把共享内存当文件操作”的思路:用 shm_open 打开一个以 / 开头的名字,得到文件描述符,用 ftruncate 设置大小,再用 mmap 映射到进程地址空间。用完 munmap、shm_unlink 收尾。

实际选型上,我自己更常用 System V,原因有三:接口成熟稳定、权限控制模型清晰、ipcs/ipcrm 这类排查工具好用。但如果你在写新项目,而且喜欢文件描述符的统一操作风格,POSIX 也很顺手。这篇文章后面全程用 System V 做 Demo,因为它在面试和考试里出现频率最高,原理摸透了 POSIX 自然就会。

2. 原理与API拆解:搞懂共享内存到底干了什么

2.1 地址映射:两个进程怎么同时看到一份数据

新手最容易困惑的点是:两个进程不是各自有独立的地址空间吗?怎么共享一块内存?

关键在于页表。每个进程都有完整的虚拟地址空间,但这些虚拟地址只是“门牌号”。CPU 访问内存时,MMU 会通过页表把虚拟地址转换成物理地址。正常情况下,进程 A 的虚拟页和进程 B 的虚拟页各自映射到不同的物理页,所以互相看不见。

共享内存的机制,就是让两个进程的虚拟页映射到同一个物理页上。A 进程往这个虚拟页对应的物理地址写数据,B 进程从自己虚拟地址访问时,MMU 翻译过去还是同一个物理地址,自然就读到了相同内容。

这里面有两点值得注意:

第一,共享内存里各个进程的虚拟地址可以不同。比如 A 进程挂在 0x7f1234567000,B 进程可能挂在 0x7f7890abc000,这不影响它们访问同一个物理页。挂载时地址参数建议传 NULL,让内核自己选合适的地址,避免写死地址造成冲突。

第二,物理页本身是真实存在的,内核只是把页表项指向它,并没有给每个进程复制一份数据。这也是共享内存效率高的根本原因。

2.2 System V共享内存:shmget到shmctl逐个说清

System V 共享内存的四个核心接口,我按调用顺序拆开讲。

第一步,生成 key。key_t 本质上是一个整数,用来让不同进程找到同一个共享内存段。最常见的生成方式是 ftok:

#include <sys/ipc.h> #include <sys/stat.h> key_t key = ftok("/tmp/shm_demo", 'S');

ftok 有两个参数:路径和项目 ID。路径必须是一个真实存在的文件路径(不一定非要用 /tmp,也可以是当前目录下的某个文件),项目 ID 是一个字符或单字节整数。内核会基于文件 inode 编号和项目 ID 组合出一个 key 值。

这里有个坑:如果用同一个路径、同一个项目 ID,任何时候调用 ftok 生成的都是同一个 key,这样多个进程才能对上号。换个路径,key 就变了,也就找不到同一块共享内存。我见过同事把路径写错,两个进程各建各的共享内存,通信一直失败,查了半天。

第二步,创建或获取共享内存段:

#include <sys/shm.h> int shmid = shmget(key, 4096, IPC_CREAT | IPC_EXCL | 0666);

参数含义分别是:key、共享内存大小(字节)、权限标志。IPC_CREAT 表示不存在就创建;IPC_EXCL 配合 IPC_CREAT 使用时,如果已经存在则返回错误,避免拿到一个旧段;0666 是权限,类似文件权限的 八进制。

如果共享内存段已经存在,只需要获取而不创建,直接把 IPC_CREAT 去掉,用同一个 key 调 shmget 就能拿到 ID。

第三步,挂载到进程地址空间:

void *addr = shmat(shmid, NULL, 0);

第二个参数 NULL 表示由内核选择合适的虚拟地址;第三个参数填 SHM_RDONLY 可以只读挂载,一般填 0 就是读写。返回值是进程内可访问的起始地址,强转成你自己的数据结构指针即可。失败返回 (void *)-1,需要检查 errno。

第四步,使用完解除挂载、清理共享内存:

shmdt(addr); shmctl(shmid, IPC_RMID, NULL);

shmdt 只做“解除挂载”,共享内存段依然在内核里存在,别的进程还能继续访问。shmctl 配合 IPC_RMID 才是真正删除共享内存段,而且删除动作发生在最后一个进程解除挂载之后。也就是说你调了 IPC_RMID,只要还有进程挂着,共享内存不会立刻消失,这是 System V 的一个经典语义,后面排坑部分会详细讲。

2.3 POSIX共享内存:更轻快的三板斧

POSIX 共享内存的套路是“三板斧”:shm_open、ftruncate、mmap。

#include <sys/mman.h> #include <fcntl.h> #include <unistd.h> int fd = shm_open("/my_shm", O_CREAT | O_RDWR, 0666); ftruncate(fd, 4096); void *addr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

shm_open 的第一个参数必须以斜杠开头,比如 /my_shm,这相当于共享内存的名字。ftruncate 设置共享内存大小。mmap 的 MAP_SHARED 标志非常关键,它保证对映射区域的修改会写回共享内存对象,能被其他进程看到。如果错用成 MAP_PRIVATE,改动只在进程内部可见,共享就名存实亡了。

用完释放:

munmap(addr, 4096); close(fd); shm_unlink("/my_shm");

对比下来,System V 是 key + id 模型,POSIX 是名字 + 文件描述符模型。POSIX 的好处是接口风格和普通文件操作统一,配合 fstat、ftruncate 这类操作很自然;System V 的好处是工具链完善,出问题好排查。两者选一个深入掌握即可,原理完全一样。

3. 手写Demo:模拟一个传感器数据采集通信

3.1 场景设计与数据结构

我拿一个行业里很常见的场景练手:一个传感器采集进程持续产生数据,另一个监控进程需要实时读取展示。这个场景非常适合用共享内存——数据块有固定结构,更新频率高,读取方只想看到最新数据。

先定数据结构。为了贴近真实工程,我定义了一个传感器数据包,包含设备 ID、时间戳、8 个采样值和校验字段:

typedef struct { int dev_id; long ts_sec; float values[8]; unsigned int checksum; } sensor_pkg_t;

共享内存大小直接给 4096 字节。注意:4096 是 Linux 内存页的默认大小,也是内核管理共享内存的常见粒度,在这个 Demo 里足够容纳结构体,还留了扩展余量。

严格来说,共享内存大小应该按页对齐,拿 sizeof(sensor_pkg_t) 向上取整到页大小更严谨。示例代码我直接写 4096,就是为了让新手先跑通,再理解对齐的意义。

3.2 写端实现:循环写入传感器数据

写端要做四件事:生成 key、创建共享内存段、挂载到自己的地址空间、循环写入数据。完整代码如下:

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <time.h> #include <errno.h> #include <sys/ipc.h> #include <sys/shm.h> #define SHM_PATH "/tmp/shm_demo" #define SHM_PROJ 'S' #define SHM_SIZE 4096 typedef struct { int dev_id; long ts_sec; float values[8]; unsigned int checksum; } sensor_pkg_t; int main(void) { key_t key = ftok(SHM_PATH, SHM_PROJ); if (key == -1) { perror("ftok"); exit(EXIT_FAILURE); } int shmid = shmget(key, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666); if (shmid == -1 && errno == EEXIST) { shmid = shmget(key, SHM_SIZE, 0666); } if (shmid == -1) { perror("shmget"); exit(EXIT_FAILURE); } printf("shmid = %d\n", shmid); sensor_pkg_t *pkg = (sensor_pkg_t *)shmat(shmid, NULL, 0); if (pkg == (void *)-1) { perror("shmat"); exit(EXIT_FAILURE); } for (int round = 0; round < 100; round++) { pkg->dev_id = 1; pkg->ts_sec = time(NULL); for (int i = 0; i < 8; i++) { pkg->values[i] = 25.0 + round * 0.1 + i; } pkg->checksum = pkg->dev_id + (unsigned int)pkg->ts_sec; printf("[writer] round %d, first value %.2f\n", round, pkg->values[0]); sleep(1); } shmdt(pkg); shmctl(shmid, IPC_RMID, NULL); return 0; }

这里有个细节值得展开。shmget 第一次创建时用了 IPC_CREAT | IPC_EXCL,如果共享内存段已经存在,会返回 EEXIST 错误。我在代码里处理了这种冲突:错误码是 EEXIST 时,再用不带 IPC_CREAT 的 shmget 获取已有段。这样即使上一次运行没清理干净,程序也能正常挂载并继续工作,不会因为残留段重启失败。

写入逻辑特意设计了 checksum 字段,看起来是“数据校验”,其实在这个 Demo 里它还有另一个作用——让你观察读端会不会读到半个包。后面配信号量时,这个字段就是检查数据完整性的抓手。

3.3 读端实现:主动读取并解析

读端不需要创建共享内存,只做三件事:生成同样的 key、获取 shmid、挂载后循环读取。代码:

#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <sys/ipc.h> #include <sys/shm.h> #define SHM_PATH "/tmp/shm_demo" #define SHM_PROJ 'S' #define SHM_SIZE 4096 typedef struct { int dev_id; long ts_sec; float values[8]; unsigned int checksum; } sensor_pkg_t; int main(void) { key_t key = ftok(SHM_PATH, SHM_PROJ); if (key == -1) { perror("ftok"); exit(EXIT_FAILURE); } int shmid = shmget(key, SHM_SIZE, 0666); if (shmid == -1) { perror("shmget"); exit(EXIT_FAILURE); } sensor_pkg_t *pkg = (sensor_pkg_t *)shmat(shmid, NULL, 0); if (pkg == (void *)-1) { perror("shmat"); exit(EXIT_FAILURE); } for (int round = 0; round < 100; round++) { printf("[reader] dev=%d ts=%ld first=%.2f checksum=%u\n", pkg->dev_id, pkg->ts_sec, pkg->values[0], pkg->checksum); sleep(1); } shmdt(pkg); return 0; }

读端轮询打印数据,每秒读一次。这是最原始的共享内存使用方式,没有同步机制,写端随时可能在更新数据的半途中,读端恰好插进来读取。后面讲同步问题时会专门拿这个 Demo 说事。

3.4 编译运行与验证效果

两个程序分别编译:

gcc -o shm_writer shm_writer.c gcc -o shm_reader shm_reader.c

建议开两个终端。先启动写端,它会创建共享内存段;再启动读端,它获取同一段并读取数据。终端输出大概是这个样子:

终端一(写端):

shmid = 327680 [writer] round 0, first value 25.00 [writer] round 1, first value 25.10 [writer] round 2, first value 25.20

终端二(读端):

[reader] dev=1 ts=1700000000 first=25.00 checksum=1700000001 [reader] dev=1 ts=1700000001 first=25.10 checksum=1700000002 [reader] dev=1 ts=1700000002 first=25.20 checksum=1700000003

运行期间可以另开一个终端,用 ipcs 查看这个共享内存段的状态:

ipcs -m

输出里会看到 key、shmid、拥有者、权限、大小、挂载进程数(nattch)等信息。写端退出并调用 IPC_RMID 删除后,再执行 ipcs -m 就看不到这段了。如果程序异常退出,共享内存段可能残留,用 ipcrm -m shmid 可以手动删除。

4. 坑点实录:共享内存开发常见问题排障指南

4.1 不解决同步会怎样:数据错乱现场还原

把上面的 Demo 跑起来,你可能一开始觉得挺正常,读端显示的数据大部分时候和写端一致。但只要你把临界区数据变大、写入耗时变长,或者用多线程同时模拟多个写者,问题立刻暴露。

我模拟过一个放大版场景:写端写入一个 1 KB 的结构体,分 8 次 memcpy 填入,每次 memcpy 之间故意 usleep 几十微秒。结果读端经常读到“拼接数据”——字段 A 是新一轮的数据,字段 B 还是上一轮的旧数据,checksum 对不上。

这就是共享内存没有任何原子性保证的直接后果。一个进程在写,另一个进程可能在任意时刻读到写了一半的缓冲区。小数据可能碰巧没撞上,大数据一定翻车。

解决办法有两个方向:

方向一,加进程间互斥锁。Linux 下可以让 pthread 互斥锁跨进程生效,pthread_mutexattr 里设置 PTHREAD_PROCESS_SHARED 属性即可:

#include <pthread.h> pthread_mutexattr_t attr; pthread_mutex_t mutex; pthread_mutexattr_init(&attr); pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED); pthread_mutex_init(&mutex, &attr);

锁的初始化必须在共享内存里完成,关键是把这个 mutex 变量本身放到共享内存段的起始位置,所有进程挂载后看到的都是同一个锁对象。然后把“写数据”和“读数据”的临界区分别用 pthread_mutex_lock/unlock 包起来。

方向二,用信号量做互斥,这是传统 Unix 程序员的惯用做法,下一章详细展开。核心思想一样:读写双方约定好同一个信号量,写之前 P 操作,写完 V 操作;读之前 P 操作,读完 V 操作。

无论选哪种,都要记住:共享内存自身不提供任何同步,同步是开发者自己的责任。数据结构坏了、字段对不上、程序偶发崩溃,十有八九就是临界区没保护。

4.2 高频问题排查速查表

共享内存开发遇到的问题,翻来覆去就那么几类。我把实际踩过的坑整理成一张速查表,方便你踩坑时快速定位。

现象常见原因排查与处理
ftok 返回 -1路径文件不存在,或路径不可访问确认路径存在且有权限;优先用进程启动时确定存在的临时目录
shmget 返回 EEXIST上一次创建后未清理,且此前用了 IPC_CREAT|IPC_EXCL代码里处理 EEXIST 分支,改用不含 IPC_CREAT 的 shmget 获取老段;或 ipcrm 手动删除
shmat 返回 (void *)-1,errno=EACCES共享内存权限不足,shmget 创建时权限位是 0600 之类的受限值检查创建端权限位,或让进程以对应用户运行;也可临时改成 0666
shmat 返回 ENOMEM共享内存大小设置过大,超过内核限制调大 kernel.shmall/kernel.shmmax 参数,或缩小共享内存段
读端读到半包数据没有进程间同步机制加互斥锁或信号量;这是逻辑 bug,不涉及内核错误
ipcs 里 nattch 一直不为 0某进程 shmat 后没有 shmdt,或进程异常退出检查代码是否正确调用 shmdt;排查残留进程,必要时 kill 掉
共享内存段被删但进程还能访问shmctl(IPC_RMID) 删除后,已挂载进程的映射仍然有效这是 System V 语义,删除不会立即回收,最后一个进程解除挂载后才释放;更新完程序里要在删除前确认所有读者已收工
创建出来的共享内存大小和对不上不同进程 shmget 时传的 size 不一致System V 中 size 只在创建时生效,之后 SHM_STAT 看到的是创建时的旧值;所有进程保持 size 参数一致

排查时记住一条主线:先看 key 是否对得上,再看权限是否足够,最后看同步是否缺失。绝大多数问题都能用 ipcs 和 strace 定位。

有一回我排查一个偶发崩溃,进程一多就出现“读端访问地址非法”。后来用 gdb 挂上去发现,读端挂载的共享内存地址在写端调用 shmctl(IPC_RMID) 后就失效了,后续访问直接段错误。后来约定好:删除共享内存之前,所有读者必须先解除挂载,彻底避免在用指针。

4.3 性能调优:从对齐到伪共享

共享内存调优,第一步是大小对齐。共享内存的传输单位在底层是物理页,内核为每个段动态分配页表项。如果申请 100 字节,内核也会给你 4096 字节的页,多出来的空间被浪费。反过来,如果你有 3000 字节数据,那它大概率跨了两个物理页,访问时可能触发两次缺页。设计阶段就把结构体大小向页大小靠齐,如果需要 3000 字节,直接分 4096,一段容纳,效率最高。

第二步是注意伪共享问题。伪共享这个词一般出现在多线程里,但进程间共享内存同样存在:进程 A 频繁更新结构体字段 X,进程 B 频繁读取字段 Y,而 X 和 Y 处在同一条 64 字节缓存行上。A 写 X 会导致包含 X 的整个缓存行在 B 的 CPU 核心上失效,B 每次读 Y 都要从内存重新拉数据,性能被误伤。

解决办法是字段按缓存行对齐。典型做法是在结构体里插入 padding 数组,让高频写入的字段占据不同的 64 字节区域:

typedef struct { int dev_id; char pad1[60]; // 填充到 64 字节边界 float values[8]; char pad2[32]; unsigned int checksum; } sensor_pkg_t;

看起来浪费了点空间,但在高频读写场景下性能提升非常明显。

第三步是减少无意义的挂载和卸载操作。shmat 和 shmdt 都是系统调用,也有开销。如果进程长期使用共享内存,启动时挂载一次、退出时卸载一次就够,不要在循环里反复挂载卸载。

实测对比过共享内存和管道的吞吐差异:同一台机器上,每 100 微秒传一个 4 KB 数据包,管道模式 CPU 占用接近 20%,换成共享内存后 CPU 占用直接掉到 3% 以下,延迟也从几十微秒降到几微秒量级。这就是为什么大量中间件内部都用共享内存做单机数据交换。

5. 进阶协同:共享内存与信号量组合实战

5.1 信号量加共享内存,生产消费者的标准姿势

共享内存负责“运输”,信号量负责“交通管制”,这对组合在进程通信领域是黄金搭档。信号量用 System V 那一套也顺手:semget 创建信号量集合,semop 做 P/V 操作,semctl 做控制。

融合到 Demo 里,写端在写入前后分别做 P 和 V:

#include <sys/sem.h> // 假设 semid 已经创建,且初始值为 1 struct sembuf ops[1]; ops[0].sem_num = 0; ops[0].sem_op = -1; // P 操作,semval 减 1 ops[0].sem_flg = SEM_UNDO; semop(semid, ops, 1); // 临界区:写入共享内存 for (int i = 0; i < 8; i++) { pkg->values[i] = 25.0 + round * 0.1 + i; } pkg->checksum = pkg->dev_id + (unsigned int)pkg->ts_sec; ops[0].sem_op = 1; // V 操作,semval 加 1 ops[0].sem_flg = SEM_UNDO; semop(semid, ops, 1);

读端结构一样:读之前 P,读完 V。这样无论写端还是读端,任意时刻只有一个进程能进入临界区操作共享内存,彻底避免了半包数据和字段错乱。

SEM_UNDO 这个标志很多人会忽略,但它其实很关键。它的作用是:如果持有信号量的进程异常退出,内核会自动归还信号量,防止其他进程永久阻塞。我建议自己写的代码里,只要是拿信号量做互斥,一律加上 SEM_UNDO 标志。

创建信号量的方式:

int semid = semget(key, 1, IPC_CREAT | 0666); semctl(semid, 0, SETVAL, 1); // 初始值设为 1

注意信号量的 key 可以用 ftok 生成,但项目 ID 要和共享内存区分。很多人图省事用同一个 ftok 路径和 ID,结果共享内存和信号量 key 一样,创建时互相干扰,非常隐蔽。

更高效的生产消费者模型还需要区分“缓冲区有数据了”和“缓冲区空了”,那就需要两个信号量做计数:一个初始为 1 表示缓冲区可以写,一个初始为 0 表示还没有数据可读。生产者写之前等“可写”,写完之后 V“可读”;消费者读之前等“可读”,读完 V“可写”,这样能实现真正的阻塞等待,而不是轮询。

5.2 什么时候别用共享内存

共享内存虽然强,但不是银弹。以下场景真的不该选它:

跨主机通信别用共享内存。它只在单机内有效,两台机器之间的数据交换还是得走 Socket 或消息队列。有人为了“高效”硬把跨主机数据映射到共享内存,结果还要额外引入网络同步机制,复杂度翻倍,得不偿失。

小数据高频通信也别急着上共享内存。一个整数一个状态标志的传递,用管道或事件通知可能更合适。共享内存需要维护创建、挂载、同步、清理全套生命周期,为了传 4 字节引入这一堆复杂度,不划算。

线程之间通信更用不上共享内存。同一进程内的线程本就共享全部地址空间,直接定义全局变量加锁就行,再搞一套共享内存等于脱裤子放屁。场景选错了,技术再先进也是负担。

还有一个反直觉的点:如果数据要长期持久化,共享内存也不合适。共享内存段随系统状态存在,机器重启就没了。需要落盘的场景,老老实实用文件或数据库。

我自己的经验法则是:单机内,单次数据量大于几百字节、通信频率高、对延迟敏感、一次写入但多处读取,这类场景直接选共享内存。其他情况先用简单方案,性能瓶颈出现了再考虑优化。

写到最后

项目里第一次用共享内存时,我犯过一个特别蠢的错误。程序跑了一晚上,第二天发现所有共享内存段累积了十几个,全是一起异常崩溃的进程留下的残留段。那时候才真正体会到 shmctl(IPC_RMID) 和 shmdt 的语义区别——异常退出时,你挂载的那些段不会自动清理,系统里全是僵尸段,直到重启才消失。后来我养成了一个习惯:每次写完共享内存程序,先跑一次 ipcs -m 看段的数量,再用 ipcrm -m 手动清理残留,确认干净了才交付。

还有一个经验是想清楚“谁负责创建、谁负责删除”。我现在的工程约定是:专门的调度进程负责 ftok 和 shmget 创建共享内存,写入进程和读取进程只做 shmat 挂载和 shmdt 卸载,最后的 IPC_RMID 也由调度进程在确认所有参与者退出后执行。谁创建、谁管理、谁删除,写清楚这三点,共享内存项目的坑至少少一半。

共享内存这东西,原理就那么点东西,API 也不复杂,真正拉开差距的是工程细节:同步怎么设计、生命周期怎么管理、数据怎么对齐。你把这篇里的代码跑熟了,再把坑点过一遍,接触真实项目时就不会再犯那些最常见的基础错误了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 21:34:00

WPF+EmguCV图像处理实战:ROI绘制、掩膜生成与漫水填充

做机器视觉这一行的&#xff0c;很多刚入门的朋友迟早会遇到一个绕不开的需求&#xff1a;拿到一张图&#xff0c;先把画面里真正关心的那块区域圈出来&#xff0c;然后只针对这块区域做统计或者处理。就好比一整块板子&#xff0c;你只关心焊点那一小块区域的灰度均匀性&#…

作者头像 李华
网站建设 2026/10/21 21:31:09

双闭环PID控制:位置环+加速度环的C语言实现与整定指南

简介&#xff1a;双闭环PID程序&#xff08;位置环加速度环&#xff0c;即位置环与速度环双闭环&#xff09;针对单闭环控制在电机未到位时长期满偏输出、导致速度过快的问题&#xff0c;提供多环协调控制方案。程序将位置环输出与速度环输出叠加&#xff1a;位置未到达预设值时…

作者头像 李华
网站建设 2026/10/21 21:30:06

Flutter双向冻结表格:四象限布局与滚动联动实战

1. 这个需求到底在解决什么问题 移动端做数据密集型界面时&#xff0c;表格和列表的横竖交叉滚动几乎是绕不开的坎。尤其是当列数超过一屏、行数也超过一屏的时候&#xff0c;用户既想横向看后面的字段&#xff0c;又想纵向翻更多记录&#xff0c;同时还希望表头和左侧关键列始…

作者头像 李华
网站建设 2026/10/21 21:27:09

OpenGL ES着色器开发:IDE插件配置与调试实战指南

1. 着色器开发为什么需要IDE插件很多人刚开始写OpenGL ES着色器的时候&#xff0c;都是直接在代码里用字符串拼接GLSL&#xff0c;或者单独建一个.glsl文件用记事本打开就写。我一开始也是这么干的&#xff0c;直到有一次写一个复杂的光照模型&#xff0c;片元着色器写到两百多…

作者头像 李华
网站建设 2026/10/21 21:25:17

大模型微调基础:从数据准备到LoRA实战与调优全流程

这几年&#xff0c;大模型微调基本成了算法工程师的一个常规动作。无论你是想做一个行业问答助手&#xff0c;还是想让模型按固定格式输出结构化信息&#xff0c;最后大概率都会走到这一步&#xff1a;拿一个开源底座模型&#xff0c;用自己攒的数据重新训练一下。这篇文章把“…

作者头像 李华
网站建设 2026/10/21 21:22:14

Claude Desktop 高效配置 11 个 MCP 服务器:从基础到实战

说句实话&#xff0c;Claude Desktop 刚发布那会儿&#xff0c;我一度觉得它只是换了个壳的聊天窗口。真正让它从一个“高级对话框”变成“一体化创作站”的&#xff0c;是 MCP&#xff08;Model Context Protocol&#xff09;服务器。简单理解&#xff0c;MCP 就是一套标准插头…

作者头像 李华