共享内存访问冲突防范:加锁时机、方法及死锁问题解决
内核与用户态共享内存访问冲突解决方案
mmap_write_lock死锁根因
- 该锁的设计用途是保护进程地址空间元数据,包括VMA结构增删改、页表映射关系调整等,完全不承担物理页内容并发访问的同步职责,用来保护页数据读写属于场景误用。
- 死锁来自锁依赖冲突:
kmap、缺页异常等内核流程执行时可能已经持有mm结构的读锁或其他关联锁,此时主动申请写锁会直接形成锁依赖环,触发死锁是必然结果。 - 内核态的任意锁都无法直接阻断用户态内存访问:用户态对已映射页的读写由CPU硬件直接执行,不会经过内核锁逻辑,仅靠内核内部锁无法拦住用户态的并发写入。
合规同步实现方案
共享内存访问冲突的核心解决思路是访问双方共同遵守同步约定,不存在仅靠内核单侧逻辑就能完全阻断并发的方案,可根据场景选择以下实现:
方案1:共享原子变量同步(推荐,性能最优)
规范要求的信号、信号量机制本质是访问双方约定同步规则,最高效的实现是基于共享页内的原子变量做协同,无额外系统开销:
- 在共享页起始位置预留固定长度的同步头,存放原子状态变量(锁标志、序列号均可)
- 内核侧执行写操作前,先通过原子操作将锁标志置为持有状态,插入写内存屏障保证锁状态先于后续数据写入对用户态可见,再执行
memcpy拷贝;拷贝完成后再次插入内存屏障,保证所有数据写入完成后再清除锁标志 - 用户态访问共享内存前,先原子读取锁标志,若处于持有状态则自旋等待或通过futex阻塞,确认锁释放后再执行读写操作
- 若为内核单向写、用户态只读的场景,可以替换为无锁seqcount机制:写前序列号自增为奇数,写完自增为偶数;用户态读前后两次读取序列号,值相等且为偶数则说明读取过程无并发写,全程无阻塞,性能最优
内核侧参考实现:
// 共享内存布局,内核与用户态共用同一份结构定义 struct shared_region { atomic64_t write_busy; // 0=空闲可访问,1=内核正在写入 u8 payload[PAGE_SIZE - sizeof(atomic64_t)]; // 实际业务数据区 }; kvaddr = kmap(p); struct shared_region *reg = (struct shared_region *)kvaddr; // 标记写状态,加内存屏障禁止CPU、编译器乱序 atomic64_set(®->write_busy, 1); smp_mb__after_atomic(); // 执行数据拷贝,避免覆盖同步头 size_t copy_len = strnlen(src, sizeof(reg->payload)); memcpy(reg->payload, src, copy_len); // 写完成后清除标记,加屏障保证数据先于标记状态对用户态可见 smp_mb__before_atomic(); atomic64_set(®->write_busy, 0); // 若使用futex阻塞机制,此处执行futex唤醒等待的用户态进程 kunmap(p);
方案2:页表权限保护(低频率写场景适配)
如果不方便修改用户态同步逻辑,可以通过页表权限临时阻断用户态写:
- 内核执行拷贝前,将对应共享页的用户态页表项修改为只读,刷新对应CPU的TLB
- 此时用户态如果发起写操作会触发写保护缺页,在自定义的缺页处理逻辑中等待内核拷贝完成后,再恢复页表可写权限、返回用户态继续执行
- 该方案存在TLB刷新开销,仅适合写操作频率极低的场景,不适合高频I/O。
注意事项
当前无同步逻辑的代码运行正常只是未触发并发时序的巧合,一旦用户态写与内核memcpy时序重叠,轻则出现数据撕裂、读到半更新状态的无效值,重则触发用户态进程野指针崩溃,必须按场景添加同步逻辑。
内容的提问来源于stack exchange,提问作者justtoaskaquestion
相关产品推荐
相关产品推荐

