内存管理-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)    收藏  举报

导航