新闻详情

新闻详情

首页 / 资讯中心 / 详情

Linux内核mmap深度解析:从系统调用到VMA与缺页异常

发布时间:2026/10/1 1:44:20来源:尧图网络
Linux内核mmap深度解析:从系统调用到VMA与缺页异常
聊Linux内核内存管理绕不开mmap。日常写C程序mmap()也就是个顺手封装的函数但在内核里它的名字是sys_mmap牵动的却是do_mmap_pgoff、vm_area_struct、红黑树、缺页异常这一整条链。我最早读2.6内核源码时心态就是明明一个映射函数怎么入口处还分sys_mmap和sys_mmap2两个版本等把这段代码啃完才觉得整个Linux的用户态虚拟内存模型都通了。这篇文章就把这条链路掰开揉碎讲一遍适合刚学内核、写驱动、做安全研究的读者。我尽量不用教科书腔按实际看代码的顺序来讲。1. 用户态mmap到内核sys_mmap一次系统调用的完整旅程1.1 系统调用表里那两个看似重复的入口先看一个让很多人困惑的事实在2.6内核的i386架构下头文件里有两个跟mmap相关的系统调用号__NR_mmap是90__NR_mmap2是192。系统调用表里也同时注册了sys_mmap和sys_mmap2。明明用户态就一个mmap()内核为什么要准备两个入口原因在于偏移量的传递方式。老式的sys_mmap接收的offset是一个以字节为单位的偏移量。程序想映射文件偏移1MB的位置就得把1MB这个数直接传进寄存器。可如果文件很大超过4GB32位寄存器就装不下按字节算的偏移了。后来引入sys_mmap2参数pgoff按页为单位传递在4KB页大小的机器上32位页号能表示的最大字节偏移是2的32次方乘以4KB等于16TB比原来的4GB大了好几个数量级。所以sys_mmap2是为了支持大文件映射而存在的。这段历史到今天还有残留在64位系统里用户态mmap的系统调用号是统一的__NR_mmap9没有mmap2。但内核为了兼容32位程序仍然在compat层保留了mmap2的处理路径。你要是翻现代内核源码也能看到mmap和mmap2最终怎么汇合到同一个函数里。1.2 从系统调用表到do_mmap2用户态调用mmap()后glibc根据架构选择系统调用号和调用方式。在32位x86上glibc优先使用mmap2因为偏移以页为单位能支持更大的文件。当然老程序可能直接触发__NR_mmap比如某些静态链接的老a.out程序。当eax90时CPU通过int 0x80进入内核在entry_32.S的system_call路径里根据系统调用号查找sys_call_table跳转到sys_mmap。sys_mmap的代码并不复杂它主要做一件事检查offset是否页对齐然后转换成pgoff再转给sys_mmap2。用2.6.18的代码来说明大致是这么个结构asmlinkage long sys_mmap(unsigned long addr, unsigned long len, unsigned long prot, unsigned long flags, unsigned long fd, unsigned long off) { if (off ~PAGE_MASK) return -EINVAL; return sys_mmap2(addr, len, prot, flags, fd, off PAGE_SHIFT); } asmlinkage long sys_mmap2(unsigned long addr, unsigned long len, unsigned long prot, unsigned long flags, unsigned long fd, unsigned long pgoff) { return do_mmap2(addr, len, prot, flags, fd, pgoff); }这段代码很能说明问题内核对页对齐这件事极其敏感。off的低12位只要有非零位直接返回EINVAL因为映射的最小单位就是页文件偏移必须落在页边界上。这里多说一句很多新人mmap报EINVAL时第一个反应是len传错了实际上offset没页对齐才是最高频的原因。之后再往下就是do_mmap2了。do_mmap2会取出fd对应的struct file然后调用do_mmap_pgoff。到这一步内核已经完成了从寄存器参数到内核对象的转换接下来的核心逻辑全在do_mmap_pgoff里。2. sys_mmap核心实现do_mmap_pgoff逐段拆解2.1 参数校验那些反直觉的规则do_mmap_pgoff是mm/mmap.c里的大管家。它在2.6.18里的代码看着很长其实逻辑主线很清晰先做合法性检查再找地址空间最后建VMA。第一条硬规则len为0直接报EINVAL。很多用户态程序喜欢用len0来试探什么返回EINVAL不是bug是规范。第二条len要页对齐。内核会做PAGE_ALIGN(len)如果对齐后溢出成0同样报错。第三条addr加len不能超过TASK_SIZE。TASK_SIZE在32位x86上是0xC0000000也就是用户空间和内核空间的分界线。如果映射区域超出这个界限返回EINVAL。这些检查看似简单实际上是在守护用户态不能碰内核地址空间这条底线。那fd呢如果flags里有MAP_ANONYMOUSfd会被忽略此时file为NULL否则调用fget(fd)获取struct file取不到就返回EBADF。这里有个容易踩的坑很多人以为MAP_ANONYMOUS时fd随便传个-1就行但如果你没加MAP_ANONYMOUS却传了一个不存在的fd返回的就是EBADFlibc会把错误码原样给你。接着是mmap特有的一套权限校验。对文件映射内核要求MAP_SHARED加PROT_WRITE时文件必须以可写方式打开否则返回EACCESMAP_PRIVATE时文件至少要以可读方式打开否则返回EACCES如果文件系统的file_operations里没有mmap函数返回ENODEV。最后一个检查点容易被忽略如果是MAP_FIXEDaddr必须页对齐不对齐返回EINVAL。因为内核里VMA的start和end永远是页对齐的这是整个内存管理的基石。很多人在用户态随手传一个非页对齐地址给MAP_FIXED然后被EINVAL砸懵就是没想通这条约束。2.2 从offset到pgoff字节偏移与页偏移的坑继续深挖offset的问题。前面说过sys_mmap会把off右移12位变成pgoff这个转换本身不难难的是理解为什么要谨慎。想象一个场景32位系统上你用一个老式mmap映射一个超过2GB的文件。offset按字节传一旦offset超过4GB传进来的值本身就是截断过的内核根本不知道你原本想映射哪个位置。更隐蔽的是off的低12位如果off不是页对齐的值右移后其实丢失了精度所以sys_mmap才必须先行检查off的低12位。在glibc内部mmap()实际调用的是mmap2()它把用户传入的off_t字节偏移在用户态就除以PAGE_SIZE传给内核的pgoff已经是一个页号。所以在新程序里你几乎碰不到字节偏移导致的EINVAL。但如果你在做嵌入式开发或者分析很久以前的内核日志看到EINVAL首先就要想到偏移精度问题。还有一个更容易被忽略的点do_mmap_pgoff里pgoff还要再乘回PAGE_SIZE换算成字节偏移后去设置VMA的vm_pgoff。这个vm_pgoff会参与缺页时计算文件页号。如果这里不严谨比如在某个文件系统里把vm_pgoff的单位搞混就会出现映射出来的内容和文件内容对不上这种非常诡异的bug。我在一个老驱动里就见过这种问题驱动里手动设置vm_pgoff时忘了乘以PAGE_SIZE结果用户态看到的数据全是错位的。2.3 VMA的分配、初始化与回调触发参数校验完毕do_mmap_pgoff会进入mmap_region来真正落地映射。mmap_region的工作可以分成三步找位置、造VMA、挂到进程树。找位置由get_unmapped_area完成。它的目标是在当前进程的地址空间里找一块空闲区域。默认实现是从下往上扫描已有VMA找满足len大小的空洞如果指定MAP_FIXED则强制使用addr作为起始地址并需要把addr处原来的映射先卸掉。这个扫描过程之所以不能省是因为x86默认的进程地址空间布局是下堆上栈堆向高地址增长栈向低地址增长中间是mmap区直接往中间塞地址很容易撞车。找到地址后内核从vm_area_cachep这个slab缓存中分配一个struct vm_area_struct。新VMA的vm_start、vm_end、vm_flags逐一初始化。vm_flags不是简单地把用户态的prot转成内核态就完事它额外带了VM_MAYREAD、VM_MAYWRITE、VM_MAYEXEC和VM_MAYSHARE这是给后续mprotect用的。比如你mmap时只给PROT_READ将来想用mprotect加上PROT_WRITE就必须在当初的VM_MAYWRITE里留好接口。mprotect本身不办进口它只是在VM_MAY*允许的范围内调整VMA的权限位。接下来是文件映射最关键的一步if (file)时调用file-f_op-mmap(file, vma)。这是file_operations里的一个回调。绝大多数文件系统实现的是generic_file_mmap它只做一件事设置vma-vm_ops generic_file_vm_ops真正的建页表要等缺页时才发生。但有些特殊文件系统或驱动会在这时做额外初始化比如映射显存、映射DMA缓冲区它们可能会直接填充PTE甚至做ioremap。这一段是我建议驱动开发者重点阅读的地方因为它直接决定了mmap之后你的设备内存长什么样。匿名映射则走另一条路。MAP_PRIVATE的匿名映射之后靠do_anonymous_page在缺页时分配零页MAP_SHARED的匿名映射则调用shmem_zero_setup把VMA挂到shmem文件上这样之后父子进程的共享内存语义就有了依托。文件映射和匿名映射从这里分道扬镳。看完这段你应该已经意识到sys_mmap在绝大多数情况下并不会真正建立页表。它创建的只是虚拟内存区域这个描述符。真正的物理内存要等第一次读写才被缺页异常拉起来。3. VMA是如何组织起来的红黑树、链表与mm_struct3.1 为什么要用红黑树而不是AVL或纯链表进程的每个映射区都由一个vm_area_struct描述这些VMA不能散落一地必须被组织好因为之后任意一次缺页、munmap、mprotect、fork都需要快速找到某个地址属于哪个VMA。2.4内核用的是AVL树2.6内核改成了红黑树。改动的核心原因在于红黑树的插入删除不需要像AVL那样频繁旋转在大量VMA频繁创建销毁的场景下表现更稳定。当然红黑树只是辅助mm-mmap链表仍然保留便于从头到尾遍历所有VMA。在2.6.18里mm_struct布局大致是struct mm_struct { struct vm_area_struct *mmap; // 按地址排序的链表 struct rb_root mm_rb; // 对应的红黑树 struct vm_area_struct *mmap_cache; // 最近一次查到的VMA缓存 ... };mmap_cache是个很妙的设计。程序访问内存通常具备局部性上一次缺页查到的VMA下一次很可能还是同一个所以find_vma会先跟mmap_cache比一下命中就直接返回省掉了两次红黑树查找。对于读写密集的进程这个缓存命中率相当可观。3.2 find_vma是怎么查找的缺页异常处理的第一件事就是根据出错地址调用find_vma找到包含或紧邻该地址的VMA。如果找不到内核直接判定为非法访问给进程发送SIGSEGV。所以find_vma既是性能敏感路径也是安全性路径。红黑树的查找规则是以vma-vm_start为键值查找第一个vm_end大于addr的节点。配合链表和mmap_cache整个流程非常快。这里有个细节VMA的vm_start和vm_end都是页对齐的所以查找时不需要做任何舍入。我在做内核热路径分析时常常用perf观察find_vma在page fault里的占比在VMA数量很多、映射碎片严重的进程里它的开销不容小觑。这也是为什么很多大内存应用会有vm.max_map_count限制问题VMA数量多了查找和遍历成本都在涨。另一个值得提的点是insert_vm_struct插入VMA时会尝试跟相邻VMA做合并。如果新映射的vm_end正好等于后一个VMA的vm_start且两者权限、标志、文件都一致内核会把两个VMA合并成一个。这个合并逻辑大大减少了VMA数量。所以你看/proc/self/maps时会看到很多相邻的映射被合成一行。如果因为某些原因合并失效比如中间缺了VM_MAYSHARE这种标志maps里就会多出很多行。3.3 file_operations-mmap设备驱动绕过sys_mmap的关键点前面提到文件映射最终会调用file-f_op-mmap。这里展开一下因为很多做驱动的人都会卡在这一步。比如你想做一个字符设备用户态希望把设备的一段内存直接映射过来那么就必须实现file_operations里的mmap方法。在驱动里你一般会做两件事第一校验vma-vm_pgoff和size确保用户映射的范围在你的设备地址范围内第二调用remap_pfn_range或io_remap_pfn_range把设备的物理地址或者DMA地址映射到用户空间并设置需要的页属性。这个函数会直接修改当前进程的页表所以是真正映射生效的地方而不是等到缺页。有个常见的误区驱动里认为只要实现了mmap用户态mmap就能成功。其实不是如果file_operations里压根没有mmap这个字段sys_mmap会在do_mmap_pgoff那个检查点直接返回ENODEV。很多老的procfs文件、sysfs属性文件就是这样read/write都好使但一mmap就ENODEV。这个机制也和透明加密这类需求牵扯很深。透明加密要实现读磁盘时解密、写磁盘时加密通常会在文件系统的file_operations.read/write上做文章但mmap是不同的通道如果加密文件被mmap到用户态缺页时走的是filemap_fault而不是read。老内核在这块的配合不够好很多加密方案干脆禁止或者特殊处理mmap路径。现代内核把加密逻辑下沉到页缓存层mmap才能比较自然地工作。你想理解文件系统加密绕不开sys_mmap这条线。4. 这块地址真的是内存吗mmap之后的缺页机制4.1 mmap只画饼不分配物理页每次看到有新人以为mmap成功就等于内存已经分配好了我都想让他去数一数RSS。mmap成功后进程的RSS一点都不会涨因为此时只有VMA没有页表项更不可能有物理页。真正让物理页落地的是第一次访问触发的缺页异常。CPU在取指或访存时发现PTE不存在就触发page fault内核的do_page_fault根据出错地址找到VMA。如果VMA不存在直接发SIGSEGV如果VMA存在且访问权限足够就调用handle_mm_fault进入细分处理。这也是为什么mmap一个很大的文件往往感觉秒回真正的磁盘读取都发生在后续访问时而且是按需一页一页读。4.2 文件映射与匿名映射的缺页路径差别对于文件映射缺页时会走filemap_fault或filemap_nopage从page cache里取对应文件页。如果页不在cache里就发起磁盘读。如果是VM_PRIVATE且页可写第一次写会触发COW复制一份物理页并标记为进程私有。这也是fork之后子进程看到父进程旧数据的机制看似共享实际各写各的。对于匿名映射缺页时do_anonymous_page会把一个全零页映射进来。如果父子进程共享同一块匿名VMA且PTE可写写的时候再触发COW。对于MAP_SHARED的匿名映射由于VMA背后挂着shmem缺页时会从shmem获取页因此父子进程写同一个地址会看到彼此的值这就是共享内存的基础。我把这条链路串起来说sys_mmap负责划地盘也就是建VMA缺页异常负责盖房子也就是建页表、分配物理页。两者分开既提高了mmap的调用速度也让fork、exec、mprotect这些操作不需要频繁改动页表。这是现代操作系统虚拟内存设计的核心思想。理解了这一层你再看mmap内存是不是一定比read快这类问题思路会清晰很多快不快取决于你是要映射大文件做随机访问还是只是顺序读一遍。5. 从2.6到6.xsys_mmap的进化与兼容性教训5.1 名称变化背后的安全补丁如果你现在打开新内核源码会发现已经找不到sys_mmap这个名字了取而代之的是ksys_mmap_pgoff。名字变化背后有段往事2009年前后暴露出系统调用参数寄存器符号扩展的问题可能造成内存信息泄露。内核为此做了一次大规模整改把原来带asmlinkage的sys_xxx函数改造成不直接从用户态拿参数的SyS_xxx包装真正的逻辑移到带ksys_前缀的函数里。这就是为什么老书上的代码和现在的源码对不上的最直接原因。2.6内核自己都没能逃过这场变化越靠近2.6.30之后的版本越接近现代风格。这类变化提醒我一点读内核源码不能只看函数名要看函数背后的职责边界。sys_mmap、SyS_mmap、ksys_mmap_pgoff名字变了三次但参数检查、找地址、建VMA、回调f_op-mmap这条主线一点没变。5.2 系统调用在现代内核中的统一到x86_64成为主流后用户态mmap的系统调用号统一为__NR_mmap9不再需要mmap2。但64位内核为了兼容32位程序仍然保留对__NR_mmap2的处理通过compat层走到同一个实现。现代内核里ksys_mmap_pgoff负责区分flags、处理大页对齐、做审计和安全检查最后调用vm_mmap_pgoff。而do_mmap_pgoff这个名字也直接改成了do_mmap。可以看到2.6时代的那套偏移转换、检查、找洞、建VMA的框架基本上延续了下来只是外壳变了。2.6里用mmap_sem做地址空间锁新内核换成了mmap_lock语义类似但用起来更讲究。2.6里VMA查找靠红黑树新内核保留红黑树并加入了 maple tree 来维护VMA的迭代但兜底逻辑仍然熟悉。这种稳定性正好说明读2.6源码绝不是学废历史而是理解Linux内存管理如何思考的完整底稿。很多新特性比如对透明大页的支持、对mmap_lock的细分、对VMA合并的优化都是在2.6骨架上长出来的。6. 常见问题排查与调试实录6.1 mmap返回EINVAL/ENOMEM/EACCES/ENODEV的常见原因这里给一份我个人经验总结的速查表返回值常见触发原因快速定位思路EINVALlen为0len页对齐后溢出offset非页对齐addr加len超TASK_SIZEMAP_FIXED时addr未页对齐先检查用户态传参别一上来就怀疑内核ENOMEM地址空间不足VMA数量超过max_map_count超过RLIMIT_ASget_unmapped_area找不到空闲区看cat /proc/进程号/maps、ulimit -vEACCESMAP_SHARED加PROT_WRITE但fd不是O_RDWRMAP_PRIVATE但fd不是O_RDONLY检查open时的modeENODEV文件系统或设备没有实现f_op-mmap查驱动源码EBADF传了fd但fd无效检查fd是否关闭有一个现象非常典型你mmap一个procfs里的文件比如某个只读状态节点如果它没有实现mmap返回ENODEV。用户明明可以cat出来但mmap就是失败。很多人会在这上面转很久其实原理很简单cat走的是readmmap走的是file_operations.mmap两套接口互不保证。6.2 strace/ftrace/kprobe三板斧排查mmap问题我习惯按这个顺序来。第一板斧是strace。执行strace -e mmap,mmap2,munmap ./your_prog能看到所有相关系统调用的参数和返回值。strace -f能看到子进程的调用适合排查fork后的共享内存问题。strace显示的是人类可读的参数offset会显示成十六进制但要想看清真实系统调用号和寄存器可以加-e rawmmap2。第二板斧是ftrace。想跟踪do_mmap_pgoff的执行可以用cd /sys/kernel/debug/tracing echo do_mmap_pgoff set_ftrace_filter echo function current_tracer echo 1 tracing_on cat trace注意2.6早期版本里ftrace还在演进有些版本函数名可能叫do_mmap2有些版本还没有ftrace。没ftrace就用kprobe或者干脆手动在do_mmap_pgoff入口加printk重新编译。对于追查为什么返回EINVAL这种问题加printk看参数是最直接的方法。第三板斧是kprobe/kretprobe。想看用户态addr和返回的映射地址可以在sys_mmap入口和do_mmap_pgoff返回处挂kprobe。新内核里用tracefs的kprobe_events更方便echo p:my_mmap do_mmap_pgoff addr%ax len%dx kprobe_events不用重编内核就能观察参数这在排查客户现场问题时特别有用。老内核没有tracefs只能写个小模块注册kprobe_handler但原理一样。6.3 一个真实的max_map_count问题最后分享一个我踩过的坑。某个老项目跑一段时间后进程莫名其妙消失dmesg里却看不到OOM Killer记录。后来发现是进程的VMA数量爆了。为什么mmap会创建那么多VMA答案是一个for循环把文件按4KB小段反复mmap每段都产生一个vm_area_struct而内核默认的vm.max_map_count只有65530左右。一旦超过这个数mmap返回ENOMEM程序里又没做错误处理于是一路空指针崩掉。解决方法是在if (mmap(...) MAP_FAILED)里认真处理ENOMEM并把mmap的粒度和长度控制好尽量复用同一个VMA。比如你想映射一个大文件的不同区域与其每个区域单独mmap不如一次性映射整个区间靠页偏移自己定位。这在Linux内核下是从2.6到6.x都适用的经验。如果你自己动手用QEMU加载一个2.6内核加busybox做一个最小系统还能顺手验证一个细节老内核的/proc/进程号/maps第一行永远是start-end perms offset dev inode pathname格式跟新版几乎一样。你会发现二十多年的演进内核最核心的地址空间管理逻辑比很多用户态框架要稳定得多。我个人的体会是把sys_mmap读透等于把Linux内存管理的地基扫了一遍之后再学do_page_fault、mmap_lock、VMA合并、mremap这些都会有原来是同一个骨架的豁然感。最后再分享一个小技巧遇到mmap行为诡异先在用户态strace确认参数再进内核ftrace看do_mmap_pgoff是否被调、返回值是什么最后用kprobe垂直钩住f_op-mmap看驱动做了什么。这条链走一遍九成问题都能定位。内核代码虽然多但按这个顺序读思路不会乱。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

8300张YOLO格式头盔检测数据集:智慧交通项目实战解析 2026/10/1 13:41:31

8300张YOLO格式头盔检测数据集:智慧交通项目实战解析

做智慧交通项目这几年,头盔检测是我被问得最多的需求之一。无论是电动车违章抓拍、路口安全预警,还是园区内部道路巡查,甲方开口第一句基本都是:“你们有没有现成的头盔检测数据集?”所以当我把这套8300张YOLO格式的数…

阅读更多 →
VMware svga不可恢复错误根因与四层根治方案 2026/10/1 13:41:31

VMware svga不可恢复错误根因与四层根治方案

1. 这个错误不是蓝屏,但比蓝屏更让人抓狂“不可恢复错误:(svga)”——当你在 VMware Workstation 或 Player 里正调试一个关键服务、跑着训练模型、或者刚装好 Ubuntu 桌面准备演示时,突然弹出这个红色警告框,整个虚拟机瞬间冻结&…

阅读更多 →
Agent记忆组件实战:从短期记忆到长期记忆的架构设计与落地 2026/10/1 13:41:31

Agent记忆组件实战:从短期记忆到长期记忆的架构设计与落地

1. 为什么“记忆”是Agent从玩具走向工具的分水岭做Agent开发的人大概都有过这种体验:Demo阶段惊艳得不行,一旦放到真实场景里跑上十几轮对话,整个系统就开始“失忆”——前面用户明确说过的偏好、约束、已经确认过的结论,到了第五…

阅读更多 →
Agent判断器:Laya与Jev双引擎选型与部署实战指南 2026/10/1 13:41:31

Agent判断器:Laya与Jev双引擎选型与部署实战指南

1. 这个“判断器”不是加功能,而是给 Agent 装上决策中枢 你有没有遇到过这样的情况:写好一个 Agent,它能调 API、能读文档、能生成回复,但一到关键节点就卡住——比如用户问“该不该买这支股票”,它不分析风险直接给结…

阅读更多 →
开源数据标注平台Label Studio:从安装到实战的完整指南 2026/10/1 13:41:31

开源数据标注平台Label Studio:从安装到实战的完整指南

做AI项目的人都知道,模型性能的天花板,往往在数据标注阶段就定死了。我自己跑图像和文本项目时,最耗时间的不是调参,而是整理数据集。早先我试过直接写Python脚本调用OpenCV手工框选,也用过一堆单功能的标注小工具&…

阅读更多 →
BosonNLP情感词典实践:从分词匹配到情感打分的完整指南 2026/10/1 13:41:24

BosonNLP情感词典实践:从分词匹配到情感打分的完整指南

简介:面向自然语言处理与中文情感分析入门开发者,这一示例代码包围绕BosonNLP情感词典构建了完整的情感判断流程。资源通过pandas读取.xlsx格式的待分析文本,并经jieba分词后删除停用词,再基于BosonNLP情感词典逐词匹配与评分&…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉