7. 缺页异常处理全链路深度解析缺页异常Page Fault是Linux虚拟内存机制的核心是实现延迟分配、写时复制、页缓存、swap交换的基础。当进程访问的虚拟地址没有建立页表映射或者访问权限不匹配时CPU会触发缺页异常陷入内核由内核的缺页异常处理函数完成物理内存分配、页表映射、权限调整等操作异常处理完成后CPU重新执行触发异常的指令。很多工程师对缺页异常的认知停留在「分配内存」的表层却不理解异常的分类、全链路处理流程、不同场景的处理逻辑最终导致程序性能问题、OOM、段错误无法定位。本章节基于Linux 6.6 LTS内核完整拆解缺页异常的触发、分类、全链路处理流程、不同场景的实现细节。7.1 缺页异常的触发与分类7.1.1 异常触发的底层原理当进程访问一个虚拟地址时CPU的MMU硬件会遍历进程的页表完成虚拟地址到物理地址的转换如果出现以下两种情况会触发缺页异常页不存在虚拟地址对应的页表项为空P位为0没有映射到物理页或者物理页已经被换出到swap分区权限错误虚拟地址对应的页表项的权限位与访问类型不匹配比如访问只读的页、用户态访问内核地址、不可执行的页尝试执行指令。CPU触发缺页异常后会把触发异常的虚拟地址、访问类型读/写/执行、异常错误码存入对应的寄存器然后陷入内核执行内核的缺页异常处理函数。7.1.2 缺页异常的分类Linux内核根据异常的原因和处理方式把缺页异常分为三大类次缺页异常Minor Fault不需要访问磁盘直接从内存中分配物理页或者复用已有的物理页处理速度极快。常见场景匿名映射的首次访问分配空白物理页写时复制COW的缺页异常复制已有的物理页访问已经在页缓存中的文件页只需要建立页表映射同一块物理页的共享映射建立页表映射。主缺页异常Major Fault需要访问磁盘等待IO完成处理速度慢延迟是次缺页的上千倍。常见场景访问的文件页不在页缓存中需要从磁盘读取访问的匿名页已经被换出到swap分区需要从swap分区读入内存访问的大页需要从磁盘加载。无效缺页异常Invalid Fault非法的内存访问内核会给进程发送SIGSEGV信号终止进程也就是段错误。常见场景访问不存在的虚拟地址没有对应的VMA访问权限不匹配比如写只读的代码段、用户态访问内核地址访问已经释放的内存VMA已经被munmap移除。7.2 缺页异常的核心入口与错误码解析x86_64架构下缺页异常的内核入口是do_page_fault()函数定义在arch/x86/mm/fault.c中最终会调用通用的handle_mm_fault()函数完成异常的核心处理。7.2.1 异常错误码解析CPU触发缺页异常时会把异常的详细信息存入错误码寄存器错误码的每个bit对应异常的类型是内核判断异常处理逻辑的核心依据x86_64架构的错误码核心位定义位名称含义0PF_PROT0页不存在导致的异常1权限错误导致的异常1PF_WRITE0读访问触发的异常1写访问触发的异常2PF_USER0内核态触发的异常1用户态触发的异常3PF_RSVD1页表项的保留位被设置非法页表4PF_INSTR1取指令触发的异常也就是执行不可执行的页NX保护触发5PF_PK1保护密钥违规触发的异常6PF_SGX1SGX安全区访问违规内核会根据错误码快速判断异常的类型决定处理逻辑如果是权限错误直接发送SIGSEGV信号如果是页不存在继续处理分配物理页、建立页表映射。7.2.2 缺页异常的整体执行流程CPU触发缺页异常 → 陷入内核 → 执行do_page_fault()↓1. 读取触发异常的虚拟地址address、错误码error_code↓2. 异常上下文合法性检查├→ 如果是内核态异常检查是否是内核预留地址、是否是copy_from_user等安全操作├→ 如果是用户态异常检查进程是否有有效的mm_struct是否在原子上下文└→ 非法上下文直接跳转到异常处理↓3. 查找虚拟地址对应的VMA├→ 加mmap_lock读锁在进程的mm_rb红黑树中查找address对应的VMA├→ 如果没有找到对应的VMA说明是非法地址跳转到无效异常处理发送SIGSEGV└→ 找到VMA继续检查访问权限是否合法↓4. 访问权限检查├→ 检查VMA的vm_flags是否匹配访问类型读/写/执行├→ 如果是写访问检查VMA是否有VM_WRITE标志没有则跳转到无效异常├→ 如果是执行访问检查VMA是否有VM_EXEC标志没有则跳转到无效异常└→ 权限合法进入核心处理函数handle_mm_fault()↓5. handle_mm_fault()核心处理├→ 处理透明大页的缺页异常├→ 调用handle_pte_fault()处理页表项级别的异常│ ├→ 场景1页表项为空匿名映射 → 调用do_anonymous_page()分配匿名页│ ├→ 场景2页表项为空文件映射 → 调用do_fault()从文件读取页缓存│ ├→ 场景3页表项存在写时复制 → 调用do_wp_page()处理COW异常│ └→ 场景4页表项存在页被换出swap → 调用do_swap_page()从swap读入页├→ 建立页表映射更新TLB└→ 返回异常处理结果↓6. 异常处理完成释放mmap_lock读锁返回用户态CPU重新执行触发异常的指令↓7. 异常处理失败无效异常、OOM等发送SIGSEGV/SIGBUS信号终止进程7.3 核心场景的缺页异常处理详解我们拆解4种最常见的缺页异常场景的内核实现细节覆盖绝大多数用户态程序的内存访问场景。7.3.1 匿名页缺页异常do_anonymous_page()当进程首次访问匿名映射的虚拟地址比如malloc分配的内存页表项为空触发匿名页缺页异常由do_anonymous_page()函数处理这是最常见的次缺页异常。核心执行流程检查VMA是否是匿名映射vm_file为NULLanon_vma不为NULL检查是否有足够的空闲内存如果内存不足触发内存回收、OOM Killer调用alloc_zeroed_user_highpage_movable()从伙伴系统分配一个物理页并且初始化为0避免内核数据泄露调用page_add_new_anon_rmap()建立反向映射把物理页和VMA、虚拟地址关联起来调用mk_pte()创建页表项设置页的权限位可读、可写、用户态可访问把页表项写入进程的页表刷新TLB更新进程的内存统计信息total_vm、rss计数加1异常处理完成返回用户态。核心工程细节匿名页缺页异常是次缺页不需要访问磁盘处理速度极快分配的物理页会被初始化为0所以malloc分配的内存首次访问时内容都是0不需要手动初始化如果设置了MAP_POPULATE标志mmap时会预分配匿名页不会触发缺页异常。7.3.2 写时复制缺页异常do_wp_page()写时复制Copy-On-Write, COW是Linux内核的核心优化机制用于减少不必要的物理页复制提升内存利用率常见场景fork()创建子进程后父子进程共享所有物理页页表项设置为只读当其中一个进程尝试写入时触发COW缺页异常私有文件映射MAP_PRIVATE进程写入映射的内存时触发COW异常复制物理页修改不会同步到磁盘文件共享库的代码段多个进程共享同一个物理页只读访问。COW异常的核心处理函数是do_wp_page()属于次缺页异常不需要访问磁盘。核心执行流程1.检查页表项是否是只读的访问类型是写操作确认是COW异常2.获取物理页的struct page结构体检查页的引用计数_mapcount和_refcount3.如果页的引用计数为1说明只有当前进程映射了这个页不需要复制直接修改页表项为可写刷新TLB处理完成4.如果页的引用计数大于1说明有多个进程共享这个页需要执行复制调用alloc_page()分配一个新的物理页把原页的内容复制到新页调用page_add_new_anon_rmap()为新页建立反向映射修改页表项指向新的物理页设置为可写刷新TLB原页的引用计数减1如果为0释放原页5.更新进程的内存统计信息异常处理完成。核心工程细节fork()之后父子进程共享所有物理页只有写入时才会复制极大降低了fork的开销哪怕父进程占用几十GB内存fork也能在毫秒级完成多进程共享同一个共享库的代码段只读访问不会触发COW极大节省了物理内存避坑指南fork之后父子进程同时修改同一个全局变量会触发大量的COW异常导致性能下降应该用进程间共享内存或者fork之后立即exec避免不必要的COW。7.3.3 文件映射缺页异常do_fault()当进程访问文件映射的虚拟地址对应的文件页不在页缓存中时触发文件映射缺页异常由do_fault()函数处理通常是主缺页异常需要访问磁盘。核心执行流程1.检查VMA的vm_file不为NULL确认是文件映射获取文件对应的address_space地址空间2.计算虚拟地址在文件内的偏移量查找页缓存中是否有对应的页3.如果页已经在页缓存中已经被其他进程预读/访问过直接建立页表映射次缺页处理完成4.如果页不在页缓存中触发主缺页异常调用filemap_fault()从磁盘读取文件内容到页缓存等待磁盘IO完成页标记为PG_uptodate建立页表映射设置对应的权限位5.如果是私有文件映射页表项设置为只读后续写入时触发COW异常如果是共享映射页表项设置为可写修改会同步到页缓存6.更新进程的主缺页/次缺页统计异常处理完成。核心工程细节文件映射的缺页异常绝大多数是主缺页需要等待磁盘IO完成延迟极高是程序性能瓶颈的常见来源内核会对文件映射做预读提前读取后续的页到页缓存减少主缺页异常的次数提升顺序访问的性能最佳实践大文件随机访问时用madvise(MADV_RANDOM)关闭预读避免不必要的磁盘IO顺序访问时用madvise(MADV_SEQUENTIAL)开启更大的预读窗口提升性能。7.3.4 Swap缺页异常do_swap_page()当系统内存不足时内核会把不活跃的匿名页换出到swap分区释放物理内存当进程再次访问这些被换出的页时触发swap缺页异常由do_swap_page()函数处理属于主缺页异常需要访问swap分区。核心执行流程1.检查页表项确认页被换出到swap从页表项中获取swap入口地址swap分区号槽位号2.查找swap缓存中是否有对应的页如果有直接建立页表映射次缺页处理完成3.如果页不在swap缓存中触发主缺页异常4.从swap分区对应的槽位中读取页的内容到新分配的物理页等待磁盘IO完成页标记为PG_uptodate建立反向映射设置页表项刷新TLB释放swap分区对应的槽位5.更新进程的主缺页统计异常处理完成。核心工程细节swap缺页异常需要访问磁盘延迟极高是系统内存不足时出现卡顿、抖动的核心原因系统出现大量swap缺页异常时说明物理内存严重不足需要优化程序内存占用或者扩容物理内存最佳实践数据库等延迟敏感的服务应该用mlock锁定内存禁止换出到swap避免swap缺页异常导致的性能抖动。7.4 工程实践与避坑指南1.缺页异常的性能监控与排查缺页异常尤其是主缺页异常是程序性能的核心瓶颈我们可以通过工具监控进程的缺页异常情况全局监控vmstat 1查看si/soswap换入换出、bi/bo磁盘IO、in中断判断系统是否有大量主缺页异常进程级监控ps -o min_flt,maj_flt 查看进程的累计次缺页/主缺页次数pidstat -d 1 -p 查看进程的磁盘IO情况实时监控perf stat -e page-faults,minor-faults,major-faults ./your_program统计程序运行期间的缺页异常次数trace跟踪用ftrace跟踪handle_mm_fault函数查看缺页异常的触发频率、处理耗时排查流程如果程序运行卡顿CPU利用率低先查看主缺页次数是否过高如果是再排查是文件IO还是swap导致的针对性优化。2.减少缺页异常的性能优化手段预分配物理内存mmap时设置MAP_POPULATE标志或者用mlock()锁定内存预分配物理内存建立页表映射避免运行时触发缺页异常适用于延迟敏感的实时程序优化文件预读大文件顺序访问时用madvise(MADV_SEQUENTIAL)开启更大的预读窗口提前把文件内容加载到页缓存减少主缺页异常随机访问时用madvise(MADV_RANDOM)关闭预读避免不必要的磁盘IO大页优化使用HugePage大页一个大页对应2MB/1GB的虚拟地址空间减少页表项的数量降低缺页异常的次数同时减少TLB miss提升内存访问性能避免不必要的COWfork之后子进程立即调用execve()避免父子进程同时修改内存触发大量COW异常多进程共享数据时用共享内存MAP_SHARED替代私有映射避免COW内存池复用频繁分配释放的小内存用内存池复用避免频繁的mmap/munmap减少缺页异常的次数。3.OOM与缺页异常的关系缺页异常处理时如果系统没有足够的空闲内存内核会先触发内存回收如果回收后还是没有足够的内存会触发OOM Killer杀死进程释放内存。避坑指南程序启动时预分配内存不会触发OOM因为预分配只是创建VMA没有分配物理内存只有当真正访问内存触发缺页异常时才会分配物理内存此时内存不足才会触发OOM这就是为什么程序启动时正常运行一段时间后被OOM杀死的核心原因。4.段错误的精准定位无效缺页异常会触发段错误我们可以通过内核日志精准定位异常原因a.查看dmesg日志找到类似如下的记录a.out[1234]: segfault at 0 ip 0000555555555123 sp 00007fffffffd120 error 4 in a.out[5555555540001000]b.解析日志segfault at 0触发异常的虚拟地址是0也就是空指针访问ip 0000555555555123触发异常的指令地址error 4错误码对应PF_USER | PF_INSTR用户态取指令触发的异常也就是执行了非法地址常见错误码error 6PF_USER | PF_WRITE用户态写访问触发的异常比如写只读内存error 7PF_USER | PF_WRITE | PF_PROT用户态写访问权限错误比如写只读的代码段error 14PF_USER | PF_WRITE | PF_PROT常见的写越界、释放后使用。c.定位方法用addr2line -e a.out 0000555555555123把指令地址转换为对应的代码行号精准定位崩溃位置。5.缺页异常的安全机制缺页异常是内核安全机制的核心载体很多内存安全保护都是通过缺页异常实现的NX位保护不可执行的页比如数据段、堆、栈尝试执行时会触发权限错误的缺页异常内核发送SIGSEGV信号防范缓冲区溢出攻击栈保护页栈的底部有不可访问的保护页栈越界访问时触发缺页异常终止进程防止栈溢出攻击KASLR地址空间随机化让攻击者无法预测内存地址防范ROP攻击Copy-on-Write的安全检查COW异常处理时会严格检查页的权限和引用计数防止越权访问。
Linux内核学习轨迹第五部:缺页异常处理全链路深度解析(第七小节)
发布时间:2026/6/7 10:58:11
7. 缺页异常处理全链路深度解析缺页异常Page Fault是Linux虚拟内存机制的核心是实现延迟分配、写时复制、页缓存、swap交换的基础。当进程访问的虚拟地址没有建立页表映射或者访问权限不匹配时CPU会触发缺页异常陷入内核由内核的缺页异常处理函数完成物理内存分配、页表映射、权限调整等操作异常处理完成后CPU重新执行触发异常的指令。很多工程师对缺页异常的认知停留在「分配内存」的表层却不理解异常的分类、全链路处理流程、不同场景的处理逻辑最终导致程序性能问题、OOM、段错误无法定位。本章节基于Linux 6.6 LTS内核完整拆解缺页异常的触发、分类、全链路处理流程、不同场景的实现细节。7.1 缺页异常的触发与分类7.1.1 异常触发的底层原理当进程访问一个虚拟地址时CPU的MMU硬件会遍历进程的页表完成虚拟地址到物理地址的转换如果出现以下两种情况会触发缺页异常页不存在虚拟地址对应的页表项为空P位为0没有映射到物理页或者物理页已经被换出到swap分区权限错误虚拟地址对应的页表项的权限位与访问类型不匹配比如访问只读的页、用户态访问内核地址、不可执行的页尝试执行指令。CPU触发缺页异常后会把触发异常的虚拟地址、访问类型读/写/执行、异常错误码存入对应的寄存器然后陷入内核执行内核的缺页异常处理函数。7.1.2 缺页异常的分类Linux内核根据异常的原因和处理方式把缺页异常分为三大类次缺页异常Minor Fault不需要访问磁盘直接从内存中分配物理页或者复用已有的物理页处理速度极快。常见场景匿名映射的首次访问分配空白物理页写时复制COW的缺页异常复制已有的物理页访问已经在页缓存中的文件页只需要建立页表映射同一块物理页的共享映射建立页表映射。主缺页异常Major Fault需要访问磁盘等待IO完成处理速度慢延迟是次缺页的上千倍。常见场景访问的文件页不在页缓存中需要从磁盘读取访问的匿名页已经被换出到swap分区需要从swap分区读入内存访问的大页需要从磁盘加载。无效缺页异常Invalid Fault非法的内存访问内核会给进程发送SIGSEGV信号终止进程也就是段错误。常见场景访问不存在的虚拟地址没有对应的VMA访问权限不匹配比如写只读的代码段、用户态访问内核地址访问已经释放的内存VMA已经被munmap移除。7.2 缺页异常的核心入口与错误码解析x86_64架构下缺页异常的内核入口是do_page_fault()函数定义在arch/x86/mm/fault.c中最终会调用通用的handle_mm_fault()函数完成异常的核心处理。7.2.1 异常错误码解析CPU触发缺页异常时会把异常的详细信息存入错误码寄存器错误码的每个bit对应异常的类型是内核判断异常处理逻辑的核心依据x86_64架构的错误码核心位定义位名称含义0PF_PROT0页不存在导致的异常1权限错误导致的异常1PF_WRITE0读访问触发的异常1写访问触发的异常2PF_USER0内核态触发的异常1用户态触发的异常3PF_RSVD1页表项的保留位被设置非法页表4PF_INSTR1取指令触发的异常也就是执行不可执行的页NX保护触发5PF_PK1保护密钥违规触发的异常6PF_SGX1SGX安全区访问违规内核会根据错误码快速判断异常的类型决定处理逻辑如果是权限错误直接发送SIGSEGV信号如果是页不存在继续处理分配物理页、建立页表映射。7.2.2 缺页异常的整体执行流程CPU触发缺页异常 → 陷入内核 → 执行do_page_fault()↓1. 读取触发异常的虚拟地址address、错误码error_code↓2. 异常上下文合法性检查├→ 如果是内核态异常检查是否是内核预留地址、是否是copy_from_user等安全操作├→ 如果是用户态异常检查进程是否有有效的mm_struct是否在原子上下文└→ 非法上下文直接跳转到异常处理↓3. 查找虚拟地址对应的VMA├→ 加mmap_lock读锁在进程的mm_rb红黑树中查找address对应的VMA├→ 如果没有找到对应的VMA说明是非法地址跳转到无效异常处理发送SIGSEGV└→ 找到VMA继续检查访问权限是否合法↓4. 访问权限检查├→ 检查VMA的vm_flags是否匹配访问类型读/写/执行├→ 如果是写访问检查VMA是否有VM_WRITE标志没有则跳转到无效异常├→ 如果是执行访问检查VMA是否有VM_EXEC标志没有则跳转到无效异常└→ 权限合法进入核心处理函数handle_mm_fault()↓5. handle_mm_fault()核心处理├→ 处理透明大页的缺页异常├→ 调用handle_pte_fault()处理页表项级别的异常│ ├→ 场景1页表项为空匿名映射 → 调用do_anonymous_page()分配匿名页│ ├→ 场景2页表项为空文件映射 → 调用do_fault()从文件读取页缓存│ ├→ 场景3页表项存在写时复制 → 调用do_wp_page()处理COW异常│ └→ 场景4页表项存在页被换出swap → 调用do_swap_page()从swap读入页├→ 建立页表映射更新TLB└→ 返回异常处理结果↓6. 异常处理完成释放mmap_lock读锁返回用户态CPU重新执行触发异常的指令↓7. 异常处理失败无效异常、OOM等发送SIGSEGV/SIGBUS信号终止进程7.3 核心场景的缺页异常处理详解我们拆解4种最常见的缺页异常场景的内核实现细节覆盖绝大多数用户态程序的内存访问场景。7.3.1 匿名页缺页异常do_anonymous_page()当进程首次访问匿名映射的虚拟地址比如malloc分配的内存页表项为空触发匿名页缺页异常由do_anonymous_page()函数处理这是最常见的次缺页异常。核心执行流程检查VMA是否是匿名映射vm_file为NULLanon_vma不为NULL检查是否有足够的空闲内存如果内存不足触发内存回收、OOM Killer调用alloc_zeroed_user_highpage_movable()从伙伴系统分配一个物理页并且初始化为0避免内核数据泄露调用page_add_new_anon_rmap()建立反向映射把物理页和VMA、虚拟地址关联起来调用mk_pte()创建页表项设置页的权限位可读、可写、用户态可访问把页表项写入进程的页表刷新TLB更新进程的内存统计信息total_vm、rss计数加1异常处理完成返回用户态。核心工程细节匿名页缺页异常是次缺页不需要访问磁盘处理速度极快分配的物理页会被初始化为0所以malloc分配的内存首次访问时内容都是0不需要手动初始化如果设置了MAP_POPULATE标志mmap时会预分配匿名页不会触发缺页异常。7.3.2 写时复制缺页异常do_wp_page()写时复制Copy-On-Write, COW是Linux内核的核心优化机制用于减少不必要的物理页复制提升内存利用率常见场景fork()创建子进程后父子进程共享所有物理页页表项设置为只读当其中一个进程尝试写入时触发COW缺页异常私有文件映射MAP_PRIVATE进程写入映射的内存时触发COW异常复制物理页修改不会同步到磁盘文件共享库的代码段多个进程共享同一个物理页只读访问。COW异常的核心处理函数是do_wp_page()属于次缺页异常不需要访问磁盘。核心执行流程1.检查页表项是否是只读的访问类型是写操作确认是COW异常2.获取物理页的struct page结构体检查页的引用计数_mapcount和_refcount3.如果页的引用计数为1说明只有当前进程映射了这个页不需要复制直接修改页表项为可写刷新TLB处理完成4.如果页的引用计数大于1说明有多个进程共享这个页需要执行复制调用alloc_page()分配一个新的物理页把原页的内容复制到新页调用page_add_new_anon_rmap()为新页建立反向映射修改页表项指向新的物理页设置为可写刷新TLB原页的引用计数减1如果为0释放原页5.更新进程的内存统计信息异常处理完成。核心工程细节fork()之后父子进程共享所有物理页只有写入时才会复制极大降低了fork的开销哪怕父进程占用几十GB内存fork也能在毫秒级完成多进程共享同一个共享库的代码段只读访问不会触发COW极大节省了物理内存避坑指南fork之后父子进程同时修改同一个全局变量会触发大量的COW异常导致性能下降应该用进程间共享内存或者fork之后立即exec避免不必要的COW。7.3.3 文件映射缺页异常do_fault()当进程访问文件映射的虚拟地址对应的文件页不在页缓存中时触发文件映射缺页异常由do_fault()函数处理通常是主缺页异常需要访问磁盘。核心执行流程1.检查VMA的vm_file不为NULL确认是文件映射获取文件对应的address_space地址空间2.计算虚拟地址在文件内的偏移量查找页缓存中是否有对应的页3.如果页已经在页缓存中已经被其他进程预读/访问过直接建立页表映射次缺页处理完成4.如果页不在页缓存中触发主缺页异常调用filemap_fault()从磁盘读取文件内容到页缓存等待磁盘IO完成页标记为PG_uptodate建立页表映射设置对应的权限位5.如果是私有文件映射页表项设置为只读后续写入时触发COW异常如果是共享映射页表项设置为可写修改会同步到页缓存6.更新进程的主缺页/次缺页统计异常处理完成。核心工程细节文件映射的缺页异常绝大多数是主缺页需要等待磁盘IO完成延迟极高是程序性能瓶颈的常见来源内核会对文件映射做预读提前读取后续的页到页缓存减少主缺页异常的次数提升顺序访问的性能最佳实践大文件随机访问时用madvise(MADV_RANDOM)关闭预读避免不必要的磁盘IO顺序访问时用madvise(MADV_SEQUENTIAL)开启更大的预读窗口提升性能。7.3.4 Swap缺页异常do_swap_page()当系统内存不足时内核会把不活跃的匿名页换出到swap分区释放物理内存当进程再次访问这些被换出的页时触发swap缺页异常由do_swap_page()函数处理属于主缺页异常需要访问swap分区。核心执行流程1.检查页表项确认页被换出到swap从页表项中获取swap入口地址swap分区号槽位号2.查找swap缓存中是否有对应的页如果有直接建立页表映射次缺页处理完成3.如果页不在swap缓存中触发主缺页异常4.从swap分区对应的槽位中读取页的内容到新分配的物理页等待磁盘IO完成页标记为PG_uptodate建立反向映射设置页表项刷新TLB释放swap分区对应的槽位5.更新进程的主缺页统计异常处理完成。核心工程细节swap缺页异常需要访问磁盘延迟极高是系统内存不足时出现卡顿、抖动的核心原因系统出现大量swap缺页异常时说明物理内存严重不足需要优化程序内存占用或者扩容物理内存最佳实践数据库等延迟敏感的服务应该用mlock锁定内存禁止换出到swap避免swap缺页异常导致的性能抖动。7.4 工程实践与避坑指南1.缺页异常的性能监控与排查缺页异常尤其是主缺页异常是程序性能的核心瓶颈我们可以通过工具监控进程的缺页异常情况全局监控vmstat 1查看si/soswap换入换出、bi/bo磁盘IO、in中断判断系统是否有大量主缺页异常进程级监控ps -o min_flt,maj_flt 查看进程的累计次缺页/主缺页次数pidstat -d 1 -p 查看进程的磁盘IO情况实时监控perf stat -e page-faults,minor-faults,major-faults ./your_program统计程序运行期间的缺页异常次数trace跟踪用ftrace跟踪handle_mm_fault函数查看缺页异常的触发频率、处理耗时排查流程如果程序运行卡顿CPU利用率低先查看主缺页次数是否过高如果是再排查是文件IO还是swap导致的针对性优化。2.减少缺页异常的性能优化手段预分配物理内存mmap时设置MAP_POPULATE标志或者用mlock()锁定内存预分配物理内存建立页表映射避免运行时触发缺页异常适用于延迟敏感的实时程序优化文件预读大文件顺序访问时用madvise(MADV_SEQUENTIAL)开启更大的预读窗口提前把文件内容加载到页缓存减少主缺页异常随机访问时用madvise(MADV_RANDOM)关闭预读避免不必要的磁盘IO大页优化使用HugePage大页一个大页对应2MB/1GB的虚拟地址空间减少页表项的数量降低缺页异常的次数同时减少TLB miss提升内存访问性能避免不必要的COWfork之后子进程立即调用execve()避免父子进程同时修改内存触发大量COW异常多进程共享数据时用共享内存MAP_SHARED替代私有映射避免COW内存池复用频繁分配释放的小内存用内存池复用避免频繁的mmap/munmap减少缺页异常的次数。3.OOM与缺页异常的关系缺页异常处理时如果系统没有足够的空闲内存内核会先触发内存回收如果回收后还是没有足够的内存会触发OOM Killer杀死进程释放内存。避坑指南程序启动时预分配内存不会触发OOM因为预分配只是创建VMA没有分配物理内存只有当真正访问内存触发缺页异常时才会分配物理内存此时内存不足才会触发OOM这就是为什么程序启动时正常运行一段时间后被OOM杀死的核心原因。4.段错误的精准定位无效缺页异常会触发段错误我们可以通过内核日志精准定位异常原因a.查看dmesg日志找到类似如下的记录a.out[1234]: segfault at 0 ip 0000555555555123 sp 00007fffffffd120 error 4 in a.out[5555555540001000]b.解析日志segfault at 0触发异常的虚拟地址是0也就是空指针访问ip 0000555555555123触发异常的指令地址error 4错误码对应PF_USER | PF_INSTR用户态取指令触发的异常也就是执行了非法地址常见错误码error 6PF_USER | PF_WRITE用户态写访问触发的异常比如写只读内存error 7PF_USER | PF_WRITE | PF_PROT用户态写访问权限错误比如写只读的代码段error 14PF_USER | PF_WRITE | PF_PROT常见的写越界、释放后使用。c.定位方法用addr2line -e a.out 0000555555555123把指令地址转换为对应的代码行号精准定位崩溃位置。5.缺页异常的安全机制缺页异常是内核安全机制的核心载体很多内存安全保护都是通过缺页异常实现的NX位保护不可执行的页比如数据段、堆、栈尝试执行时会触发权限错误的缺页异常内核发送SIGSEGV信号防范缓冲区溢出攻击栈保护页栈的底部有不可访问的保护页栈越界访问时触发缺页异常终止进程防止栈溢出攻击KASLR地址空间随机化让攻击者无法预测内存地址防范ROP攻击Copy-on-Write的安全检查COW异常处理时会严格检查页的权限和引用计数防止越权访问。