内存管理-81-mmap_sem内存大锁-1-调用路径
基于 msm-5.4
一、简介
1. 定义位置
struct mm_struct { struct rw_semaphore mmap_sem; }; struct task_struct { struct mm_struct *mm; };
虽然 include/linux/mmap_lock.h 提供了 mmap_read_lock()/mmap_write_lock() 等封装,但全树没有实际调用者,当前代码仍直接使用:
down_read(&mm->mmap_sem); down_read_killable(&mm->mmap_sem); down_read_trylock(&mm->mmap_sem); down_write(&mm->mmap_sem); down_write_killable(&mm->mmap_sem); down_write_trylock(&mm->mmap_sem);
二、写锁持锁路径
写锁保护 VMA 链表、红黑树、VMA 边界/属性和 mm 布局变化。同一个 mm 的缺页、GUP、/proc/pid/smaps 等读锁路径都会被阻塞。
1. brk() 扩展或收缩堆
__arm64_sys_brk //brk系统调用 → sys_brk //mmap.c → down_write_killable(mm->mmap_sem) → find_vma → do_brk_flags / __do_munmap → up_write
收缩时 __do_munmap() 可能:
//mm/mmap.c write lock → downgrade_write() → read lock
因为VMA已经从树中摘除,后面的页表 unmap 可以在读锁下完成。
2. mmap() 及内核 vm_mmap()
__arm64_sys_mmap //mmap系统调用 → ksys_mmap_pgoff → vm_mmap_pgoff → down_write_killable(mm->mmap_sem) → do_mmap_pgoff → get_unmapped_area → mmap_region → vma_link → file->f_op->mmap → up_write
所有驱动调用 vm_mmap()、vm_mmap_pgoff() 映射用户空间时也走这条路径。
3. munmap()
__arm64_sys_munmap //munmap系统调用 → __vm_munmap → down_write_killable(mm->mmap_sem) → __do_munmap → detach_vmas_to_be_unmapped → unmap_region → remove_vma_list → up_write / downgrade_write
长持锁点主要是:mmu notifier、拆分VMA、从VMA树摘除、页表解除映射、TLB flush、vm_ops->close()。
4. mremap()
__arm64_sys_mremap //mremap.c 系统调用 → sys_mremap → down_write_killable(current->mm->mmap_sem) → mremap_to / move_vma → vma_to_resize → vma_adjust → __do_munmap → up_write / downgrade_write
大范围移动、扩展或与目标 VMA 冲突时可能成为长写锁路径。
5. mprotect()、pkey_mprotect()
__arm64_sys_mprotect / pkey_mprotect //mprotect.c 系统调用 → do_mprotect_pkey → down_write_killable(current->mm->mmap_sem) → find_vma → mprotect_fixup → split_vma / vma_merge → change_protection → up_write
同文件中的以下路径也持写锁:
pkey_alloc → down_write
pkey_free → down_write
6. madvise()、process_madvise()
__arm64_sys_madvise //mm/madvise.c 系统调用 → do_madvise → madvise_need_mmap_write(behavior) ├─ true → down_write_killable(mm->mmap_sem) └─ false → down_read(mm->mmap_sem) → madvise_vma
通常需要写锁的是会修改 VMA 结构或 flag 的行为,例如:
MADV_NORMAL/RANDOM/SEQUENTIAL MADV_DONTFORK/DOFORK MADV_MERGEABLE/UNMERGEABLE MADV_HUGEPAGE/NOHUGEPAGE MADV_DONTDUMP/DODUMP MADV_WIPEONFORK/KEEPONFORK
MADV_COLD、MADV_PAGEOUT 等主要遍历/回收页面的路径通常持读锁。
7. mlock()系列
mlock / mlock2 //mm/mlock.c → do_mlock → down_write_killable → apply_vma_lock_flags → mlock_fixup → up_write munlock → down_write_killable → apply_vma_lock_flags → up_write mlockall / munlockall → down_write_killable → apply_mlockall_flags → up_write
注意真正 fault-in 页面的 mm_populate() 在释放写锁后执行,并重新获取读锁。
9. SysV共享内存
shmat → do_shmat → down_write_killable(current->mm->mmap_sem) → do_mmap → up_write shmdt → do_shmdt → down_write_killable(mm->mmap_sem) → do_munmap → up_write
10. userfaultfd
写锁路径包括:
UFFDIO_REGISTER //fs/userfaultfd.c → userfaultfd_register → down_write(mm->mmap_sem) → split_vma → 修改vm_flags/vm_userfaultfd_ctx UFFDIO_UNREGISTER → userfaultfd_unregister → down_write(mm->mmap_sem) userfaultfd_release → down_write(mm->mmap_sem)
UFFDIO_COPY/ZEROPAGE/WAKE 的目标 VMA 查找主要使用读锁,见 mm/userfaultfd.c。
11. prctl()修改mm状态
写锁路径:
prctl(PR_SET_VMA_ANON_NAME) //kernel/sys.c → prctl_set_vma → down_write(mm->mmap_sem) → prctl_set_vma_anon_name prctl(PR_SET_THP_DISABLE) → down_write_killable(mm->mmap_sem) → 设置/清除MMF_DISABLE_THP pkey_alloc / pkey_free → down_write(mm->mmap_sem)
PR_SET_MM_* 主要使用读锁,因为它需要与 brk() 及 VMA 查询串行,但不直接改变 VMA 树。
12. fork()/clone()复制地址空间
__arm64_sys_clone/fork → copy_process → copy_mm → dup_mm → dup_mmap → down_write_killable(oldmm->mmap_sem) → down_write_nested(newmm->mmap_sem) → 遍历全部旧VMA → anon_vma_fork → copy_page_range → file->f_op->mmap → uprobe_dup_mmap → ksm_fork/khugepaged_fork → up_write(newmm) → up_write(oldmm)
这是重要的长写锁路径:VMA数量多、页表庞大、fork 时 COW 页表复制都会拉长持锁时间。
13. execve() 建立新地址空间
写锁路径:
execve → bprm_mm_init → __bprm_mm_init → down_write_killable(new_mm->mmap_sem) → 建立临时栈VMA → setup_arg_pages → down_write_killable(new_mm->mmap_sem) → mprotect_fixup → shift_arg_pages → expand_stack → arch_setup_additional_pages → down_write_killable(mm->mmap_sem) → 安装arm64 VDSO/VVAR映射
位置:msm-5.4/fs/exec.c、arm64/kernel/vdso.c
14. THP/khugepaged
写锁路径包括:
khugepaged → collapse_huge_page → down_write/trylock(mm->mmap_sem) → 再次校验VMA → 分配/替换PMD → collapse页表 split_huge_page或VMA调整 → down_write(mm->mmap_sem)
其中扫描阶段通常持读锁,真正 collapse 时升级/重新获取写锁。
15. KSM
ksmd 扫描 → down_read(mm->mmap_sem) → 遍历VMA/PTE → 查找可合并匿名页 unmerge_and_remove_all_rmap_items → down_write(mm->mmap_sem) → break_ksm → 拆分KSM页
16. Uprobes
register_uprobe / install_breakpoint //kernel/events/uprobes.c → down_write_killable(mm->mmap_sem) → 遍历/修改目标VMA映射 fork复制uprobe状态 → down_write(mm->mmap_sem)
17. Binder映射页面
binder_alloc_new_buf → binder_alloc_update_page_range → down_write(mm->mmap_sem) → alloc_page → vm_insert_page → up_write
Binder 一次扩展大量 buffer 页面时可能成为实际写锁热点。
18. 其他写锁路径
还包括:
remap_file_pages() //mm/mmap.c
内核vm_brk_flags() //mm/mmap.c:3228
account_locked_vm() //mm/util.c:506
mmu_notifier_unregister() //mm/mmu_notifier.c
AIO ring映射/销毁 //fs/aio.c
coredump 建立 core_state //fs/coredump.c
exit_mmap()中与OOM reaper同步的空写锁周期 //mm/mmap.c
VFIO DMA映射注销 //drivers/vfio/vfio_iommu_type1.c
二、读锁持锁路径
1. arm64 用户缺页异常:最高频路径
EL0 data/instruction abort → do_mem_abort //arm64/mm/fault.c → do_page_fault → handle_speculative_fault(先尝试无锁) → down_read_trylock(mm->mmap_sem) └─失败时 down_read() → find_vma → __do_page_fault → handle_mm_fault → do_anonymous_page → do_swap_page → filemap_fault → do_wp_page → up_read
如果 handle_mm_fault() 返回 VM_FAULT_RETRY,文件页等待路径可能已经释放读锁,随后 fault 路径重新获取。
这是最频繁的 mmap_sem 读锁路径,但普通 minor fault 通常持锁较短;swap fault、文件 I/O fault 和 COW 可能明显变长。
2. GUP/pin user pages
公共路径:
get_user_pages* //mm/gup.c pin_user_pages* get_user_pages_remote → down_read(mm->mmap_sem) → __get_user_pages_locked → __get_user_pages → find_extend_vma → follow_page_mask → faultin_page / handle_mm_fault → up_read
主要入口包括:
get_user_pages_unlocked()
get_user_pages_remote()
get_user_pages_longterm()
__mm_populate()
GUP fast 失败后的 slow fallback
注意:get_user_pages_fast() 快速路径不持 mmap_sem;只有 fallback 才进入读锁路径。大范围 GPU、Camera、DSP、VFIO 或网络 buffer pin 页时,是潜在长读锁来源。
3. ptrace 和跨进程内存访问
ptrace PEEK/POKE /proc/pid/mem process_vm_readv/writev access_process_vm → __access_remote_vm → down_read_killable(target_mm->mmap_sem) → get_user_pages_remote → 拷贝页面内容 → up_read
4. /proc/<pid>/maps、smaps、smaps_rollup
read /proc/pid/maps read /proc/pid/smaps read /proc/pid/smaps_rollup → m_start / show_smaps_rollup → down_read_killable(mm->mmap_sem) → 遍历全部VMA → smap_gather_stats → walk_page_range → up_read
dumpsys meminfo <pid> 最终会读取 /proc/<pid>/smaps,因此它会长时间持有目标进程的 mmap_sem 读锁。VMA多、内存大、页表庞大的进程,这是非常典型的长读锁路径。
5. /proc/<pid>/pagemap
read /proc/pid/pagemap → pagemap_read → 分段 down_read_killable(mm->mmap_sem) → walk_page_range → up_read
6. /proc/<pid>/clear_refs
write /proc/pid/clear_refs → clear_refs_write ├─普通clear refs │ → down_read_killable │ → walk_page_range ├─CLEAR_REFS_MM_HIWATER_RSS │ → down_write_killable └─CLEAR_REFS_SOFT_DIRTY → read lock → 释放 → write lock 修改全部 VMA flags → downgrade_write 到读锁 → walk_page_range
7. Qualcomm /proc/<pid>/reclaim
write /proc/pid/reclaim → reclaim_write //fs/proc/task_mmu.c → down_read(mm->mmap_sem) → 遍历VMA → walk_page_range → isolate_lru_page → 回收/换出页面 → flush_tlb_mm → up_read
这条路径会在读锁下扫描整个目标进程,是 A12 主动压缩 /process_reclaim 相关的重要长持锁路径。
8. mincore()与msync()
mincore //mm/mincore.c → down_read → do_mincore → 遍历VMA和页表 msync //mm/msync.c → down_read → 遍历共享文件VMA → 对每个文件临时释放锁执行fsync → 重新获取读锁
9. madvise() 页面回收类
madvise(MADV_COLD/PAGEOUT/DONTNEED等) process_madvise → do_madvise → down_read(mm->mmap_sem) → madvise_vma → walk_page_range → deactivate_page / reclaim_pages / zap_page_range
具体取读锁还是写锁由 madvise_need_mmap_write() 决定。
10. mm_populate()、预缺页与 mlock fault-in
MAP_POPULATE MAP_LOCKED mlock后fault-in → mm_populate //mm/gup.c → __mm_populate → down_read(mm->mmap_sem) → populate_vma_page_range → GUP/handle_mm_fault
11. OOM reaper
oom_reaper线程 → oom_reap_task_mm //mm/oom_kill.c → down_read_trylock(mm->mmap_sem) → __oom_reap_task_mm → 遍历VMA → unmap_page_range → up_read
使用 trylock,获取失败会延迟后重试,不会长时间等待写锁。
12. KSM 和 khugepaged 扫描
ksmd //mm/ksm.c → down_read(mm->mmap_sem) → 遍历VMA/PTE并查找可合并页 khugepaged //mm/khugepaged.c → down_read/trylock(mm->mmap_sem) → 扫描VMA和PTE
真正修改 VMA/PMD 时会切到写锁。
13. NUMA balancing 和页面迁移
task_numa_work //sched/fair.c → down_read_trylock(mm->mmap_sem) → 遍历VMA → change_prot_numa
其他页面迁移路径:
mm/migrate.c
mm/mempolicy.c
mm/memcontrol.c
嵌入式未启用 NUMA,这些不会成为运行时热点。
14. fork/exec/exit 中的读锁
exec_mmap //fs/exec.c → down_read(old_mm->mmap_sem) → 检查 core_state → 切换 current->mm exit_mm //kernel/exit.c → down_read(mm->mmap_sem) → 与 coredump 同步 → 清除 current->mm
15. Futex缺页修复
futex原子访问失败 → fault_in_user_writeable //kernel/futex.c → down_read(mm->mmap_sem) → fixup_user_fault → handle_mm_fault → up_read
三、Qcomm驱动路径
1. KGSL GPU用户内存
KGSL ioctl 映射用户地址 → kgsl_setup_anon_useraddr //drivers\gpu\msm\kgsl.c → memdesc_sg_virt → down_read(current->mm->mmap_sem) → check_vma → get_user_pages → up_read KGSL DMA-BUF 用户地址 → kgsl_setup_dmabuf_useraddr //drivers\gpu\msm\kgsl.c → down_read → find_vma → iterate_fd → dma_buf_get → kgsl_setup_dma_buf → up_read
大 buffer GUP 时可能长时间持读锁。
2. FastRPC/ADSPRPC
主要路径:
fastrpc invoke 参数处理 //drivers\char\adsprpc.c → down_read(current->mm->mmap_sem) → find_vma → 计算DMA-BUF offset/校验范围 → up_read
还有输入 buffer cache clean 和输出 buffer invalidate 路径。
3. HAB跨域共享内存
HAB export DMA-BUF by VA → habmem_get_dma_buf_from_va //drivers\soc\qcom\hab\hab_mem_linux.c → down_read → find_vma / iterate_fd / dma_buf_get HAB export 普通用户页 //drivers\soc\qcom\hab\hab_mem_linux.c → habmem_get_dma_buf_from_uva → down_read → get_user_pages
4. Binder shrinker
binder shrinker //binder_alloc.c → binder_alloc_free_page → down_read_trylock(mm->mmap_sem) → find_vma → zap_page_range
获取失败直接跳过,不阻塞等待。
5. KVM/虚拟化
KVM memory slot/ioctl → down_read(current->mm->mmap_sem) → find_vma / hva_to_pfn / GUP → 建立stage-2映射
位置:virt/kvm/kvm_main.c、virt/kvm/arm/mmu.c。如果启用了虚拟化或Gunyah相关路径,这些也可能成为实际读锁来源。
6. 其他按配置启用的读锁路径
源码中还存在:
VFIO:用户页 pin 和 DMA 映射;
V4L2 旧版 videobuf:Camera 用户 buffer pin;
OP-TEE 共享内存;
TCP 零拷贝映射;
XDP UMEM 注册;
io_uring 固定 buffer;
RDMA ODP/umem;
是否成为实际路径取决于对应驱动和功能配置。
四、最值得关注的长持锁路径
按实际风险排序:
------------------------------------------------------------------------------------------------------------- 优先级 路径 锁类型 风险 ------------------------------------------------------------------------------------------------------------- 1 fork → dup_mmap 写 遍历全部VMA并复制页表 2 munmap/mremap/mprotect 大区间 写 拆 VMA、页表 unmap、MMU notifier、TLB flush 3 /proc/pid/smaps、dumpsys meminfo 读 遍历全部 VMA 和页表 4 /proc/pid/reclaim 读 全 VMA 扫描及页面回收 5 大范围GUP:KGSL/Camera/DSP/HAB 读 pin 大量用户页,可能触发 fault 6 swap/file page fault 读 等待页面、ZRAM 解压、COW 7 Binder 批量映射页面 写 vm_insert_page 循环 8 khugepaged collapse 写 PMD替换、页复制/页表处理 9 clear_refs/pagemap 读或写 全地址空间页表walk 10 exec栈/VDSO建立 写 VMA移动和特殊映射安装 -------------------------------------------------------------------------------------------------------------
综上,运行时最重要的 mmap_sem 持锁来源是:缺页/GUP读锁、VMA系统调用写锁、fork 写锁、proc/smaps/reclaim 读锁,以及 Binder/KGSL/FastRPC/HAB 驱动路径。
五、小结
1. 在 Linux 5.4 中,mm->mmap_sem 是非常典型的 rwsem 大锁。
高频读锁路径:用户缺页异常、swap fault、慢速GUP/pin_user_pages、/proc/<pid>/maps、smaps、smaps_rollup、dumpsys meminfo最终扫描smaps、ptrace/process_vm_readv、QCOM process reclaim、KGSL/FastRPC/HAB等驱动的用户地址解析。
写锁路径:mmap/munmap、mremap、mprotect、brk、fork 时 dup_mmap、madvise部分操作、userfaultfd 注册、Binder 或驱动插入VMA页面。
posted on 2026-09-22 15:34 Hello-World3 阅读(13) 评论(0) 收藏 举报
浙公网安备 33010602011771号