在 Linux 中,MCE 代表 Machine Check Exception(机器检查异常),它用于处理硬件错误和异常情况。MCE 可以分为 CE(Corrected Error,已纠正的错误) 和 UE(Uncorrected Error, 未纠正的错误) 两种类型。MCE 是 Linux 内核的一部分,用于监视和响应这些硬件错误,以提高系统的可靠性和稳定性。当发生 MCE 时,通常会生成系统日志(例如 /var/log/mcelog),以帮助管理员诊断和解决硬件问题。在服务器和关键应用中,监控和处理 MCE 错误非常重要,以确保系统的可靠性和可用性. CE 和 UE 的区别如下:
- CE(Corrected Error,已纠正的错误): CE 是指硬件错误,但系统可以自动纠正这些错误,而不会导致系统崩溃或严重问题。通常这些错误是由于内存或其他硬件组件的临时问题引起的,例如内存中的单个位翻转。内核会记录这些 CE 错误,但不会采取进一步的行动,因为它们被认为是暂时性的,不需要干预
- UE(Uncorrected Error,未纠正的错误): UE 是指硬件错误,但系统无法自动纠正这些错误,因此可能会导致系统崩溃或数据损坏。UE 错误通常是更严重的硬件问题,例如内存模块中的多个位错误或其他硬件组件的不可恢复故障。内核会尝试采取适当的措施来处理UE错误,例如记录错误信息并尝试停机以防止进一步损坏
在 Linux 里,当硬件检测到硬件错误或者异常,这些硬件错误可能包括内存位翻转、缓存错误、总线错误、内存单元故障等,那么硬件会触发 MCE 异常,MCE 异常处理函数能处理很多硬件错误,其中对于内存 UE 的处理逻辑如上图, 其核心是调用 memory_failure() 函数进行出来,该函数主要完成一下几个任务:
- 标记物理页: 将发生 UE 对于的物理页标记为 PG_hwpoison
- 解除映射: 通过 TTU 机制解除用户进程映射到物理页的映射
- 更新页表: 构造 UE Entry 并更新页表,以防止进程再次访问
MCE 异常处理函数构造的 UE PTE Entry 如上图,可以看到页表 Entry 的 _PAGE_PRESENT 标志位为 0,然后 MSB 5BIT 存储了 SWAP Entry 的类型,UE 场景下该类型是 SWP_HWPOIS-ON,_PAGE_PROTNONE 后面两个 BIT 的字段存储了物理页帧的反码. 因此当进程再次访问 UE 内存时,MMU 检查到对应的物理内存不存在,于是触发缺页异常,缺页异常处理函数再根据 SWP_HWPOISON 可以判断是 UE Page Fault,那么接下来就是将对应的进程杀死.
MCE 异常处理函数构造的 UE PMD Entry 如上图,可以看到页表 Entry 的 _PAGE_PRESENT 标志位为 0,然后 MSB 5BIT 存储了 SWAP Entry 的类型,UE 场景下该类型是 SWP_HWPOIS-ON,_PAGE_PROTNONE 后面两个 BIT 的字段存储了物理页帧的反码. 因此当进程再次访问 UE 内存时,MMU 检查到对应的物理内存不存在,于是触发缺页异常,缺页异常处理函数再根据 SWP_HWPOISON 可以判断是 UE Page Fault,那么接下来就是将对应的进程杀死.
当进程访问 UE 内存,MMU 检查到物理内存不存在而触发缺页异常,缺页异常处理函数首先要识别发生了 UE 缺页,其根据两个条件,首先是页表的 Present 标志位不存在,其二是页表非空,那么缺页异常处理函数认为是一个 SWAP 相关的缺页,具体是什么 SWAP 类型需要从页表的指定字段获取,对于 UE 缺页的场景,SWAP TYPE 字段为 SWAP_HWPOISON, 那么缺页异常处理函数可以识别是 UE 缺页,接下来直接返回 WM_FAULT_HWPOISON 进行处理,最终缺页异常处理函数会发送 SIG_BUS 让程序异常退出. 不同类型的内存在处理 MCE 缺页的时候可能有所差异,接下来对不同类型的内存 MCE 场景进行分析:
匿名内存 MCE 缺页场景
匿名内存也存在 MCE 缺页场景,MCE 异常发生之后,MCE 异常处理函数构造的 UE PTE Entry 如上图,可以看到页表 Entry 的 _PAGE_PRESENT 标志位为 0,然后 MSB 5BIT 存储了 SWAP Entry 的类型,UE 场景下该类型是 SWP_HWPOISON,_PAGE_PROTNONE 后面两个 BIT 的字段存储了物理页帧的反码. 因此当进程再次访问 UE 内存时,MMU 检查到对应的物理内存不存在,于是触发缺页异常,缺页异常处理函数再根据 SWP_HWPOISON 可以判断是 UE Page Fault,那么接下来就是将对应的进程杀死. 接下来通过一个实践案例进一步了解该场景,实践案例在 BiscuitOS 上的部署逻辑如下(实践之前需要打开内核 CONFIG_MEMORY_FAILURE 宏):
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] Page Fault with Anonymous on MCE(UE) --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-ANON-MCE-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
实践案例由一个应用程序构成,其分作三部分,首先是 21 行调用 mmap 函数结合 MAP_PRIVATE 和 MAP_ANONYMOUS 分配一段可读可写的匿名内存,并在 32 行对匿名内存执行写操作, 以及在 34 行再次对匿名内存执行读操作. 第二部分是利用 madvise 注入 UE,由于 UE 是硬件行为,所有只能借助软件来模拟 UE 的发生,于是函数在 37 行调用 madvise 函数发送 MADV_HWPOISON 请求. 最后是第三部分,程序在 40 行再次对匿名内存执行写操作,此时会再次触发缺页并被识别为 UE Page Fault. 以上便是一个最基础的实践案例,可以知道 32 行读操作和 40 行写操作都会触发缺页,为了可以看到内存在缺页异常里的流动,本次在 40 行前后加上 BS_DEBUG 开关:
接着在匿名内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 do_swap_page 函数的 3747 行加上 bs_debug 打印,以此确认内存是向哪个函数流动,接下来执行如下命令进行实践:
# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-ANON-MCE-default/
# 编译内核
make kernel
# 编程程序
make build
当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “UE Anonymous Memory on do_swap_page 0x6000000000”, 说明缺页异常处理函数执行过 UE 缺页. 通过实践可以看到实践案例按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看匿名内存在缺页异常处理流程里的流动.
对于 Memory UE 引起的缺页,缺页异常处理函数根据 UE SWP Entry,一旦确认是 Memory UE Page Fault, 直接退出到 handle_mm_fault 函数,然后执行 force_sig_mceerr 发送信号杀死进程.
对于 Memory UE 缺页核心处理函数是 do_swap_page 函数,由于 MCE 异常将页表修改成了 SWAP Entry,其特点就是 _PAGE_PRESENT 不存在,但在 SWAP TYPE 字段存储着 SWP_HWPOISON 信息,因此 do_swap_page 函数在 3735 行调用 pte_to_swp_entry 函数获得 SWAP Entry 之后,调用 non_swap_entry 函数检查到里面不是正常的 SWAP Entry,接着读取 SWAP TYPE 字段知道符合 is_hwpoison_entry() 函数对应的分支,因此函数在 3747 行将缺页异常返回原因设置为 VM_FAULT_HWPOISON.
在 do_user_addr_fault 函数处理异常处理返回值时,VM_FAULT_HWPOISON 返回值会进入 1454 行分支,并调用 do_sigbus 函数发起信号杀死进程。以上便是 Memory UE Page Fault 缺页处理的流程.
共享内存 MCE 缺页场景
共享内存也支持 MCE 缺页场景,MCE 异常发生之后,MCE 异常处理函数构造的 UE PTE Entry 如上图,可以看到页表 Entry 的 _PAGE_PRESENT 标志位为 0,然后 MSB 5BIT 存储了 SWAP Entry 的类型,UE 场景下该类型是 SWP_HWPOIS-ON,_PAGE_PROTNONE 后面两个 BIT 的字段存储了物理页帧的反码. 因此当进程再次访问 UE 内存时,MMU 检查到对应的物理内存不存在,于是触发缺页异常,缺页异常处理函数再根据 SWP_HWPOISON 可以判断是 UE Page Fault,那么接下来就是将对应的进程杀死. 接下来通过一个实践案例进一步了解该场景,实践案例在 BiscuitOS 上的部署逻辑如下(实践之前需要打开内核 CONFIG_MEMORY_FAILURE 宏):
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] Page Fault with SHMEM Memory on MCE(UE) --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-SHMEM-MCE-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
实践案例由一个应用程序构成,其分作三部分,首先是 21 行调用 mmap 函数结合 MAP_SHARED 和 MAP_ANONYMOUS 分配一段可读可写的共享内存,并在 32 行对匿名内存执行写操作, 以及在 34 行再次对匿名内存执行读操作. 第二部分是利用 madvise 注入 UE,由于 UE 是硬件行为,所有只能借助软件来模拟 UE 的发生,于是函数在 37 行调用 madvise 函数发送 MADV_HWPOISON 请求. 最后是第三部分,程序在 40 行再次对共享内存执行写操作,此时会再次触发缺页并被识别为 UE Page Fault. 以上便是一个最基础的实践案例,可以知道 32 行读操作和 40 行写操作都会触发缺页,为了可以看到内存在缺页异常里的流动,本次在 40 行前后加上 BS_DEBUG 开关:
接着在共享内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 do_swap_page 函数的 3732 行加上 bs_debug 打印,以此确认内存是向哪个函数流动,接下来执行如下命令进行实践:
# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-SHMEM-MCE-default/
# 编译内核
make kernel
# 编程程序
make build
当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “MCE SHMEM Memory on do_swap_page 0x6000000000”, 说明缺页异常处理函数执行过 UE 缺页. 通过实践可以看到实践案例按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看共享内存在缺页异常处理流程里的流动.
对于 Memory UE 引起的缺页,缺页异常处理函数根据 UE SWP Entry,一旦确认是 Memory UE Page Fault, 直接退出到 handle_mm_fault 函数,然后执行 force_sig_mceerr 发送信号杀死进程.
对于 Memory UE 缺页核心处理函数是 do_swap_page 函数,由于 MCE 异常将页表修改成了 SWAP Entry,其特点就是 _PAGE_PRESENT 不存在,但在 SWAP TYPE 字段存储着 SWP_HWPOISON 信息,因此 do_swap_page 函数在 3735 行调用 pte_to_swp_entry 函数获得 SWAP Entry 之后,调用 non_swap_entry 函数检查到里面不是正常的 SWAP Entry,接着读取 SWAP TYPE 字段知道符合 is_hwpoison_entry() 函数对应的分支,因此函数在 3747 行将缺页异常返回原因设置为 VM_FAULT_HWPOISON.
在 do_user_addr_fault 函数处理异常处理返回值时,VM_FAULT_HWPOISON 返回值会进入 1454 行分支,并调用 do_sigbus 函数发起信号杀死进程。以上便是 Memory UE Page Fault 缺页处理的流程.
HugeTLB 内存 MCE 缺页场景
HugeTLB 内存也会发生 MCE,当 MCE 异常发生之后,MCE 异常处理函数构造的 UE PMD Entry 如上图,可以看到页表 Entry 的 _PAGE_PRESENT 标志位为 0,然后 MSB 5BIT 存储了 SWAP Entry 的类型,UE 场景下该类型是 SWP_HWPOIS-ON,_PAGE_PROTNONE 后面两个 BIT 的字段存储了物理页帧的反码. 因此当进程再次访问 UE 内存时,MMU 检查到对应的物理内存不存在,于是触发缺页异常,缺页异常处理函数再根据 SWP_HWPOISON 可以判断是 UE Page Fault,那么接下来就是将对应的进程杀死. 接下来通过一个实践案例进一步了解该场景,实践案例在 BiscuitOS 上的部署逻辑如下(实践之前需要打开内核 CONFIG_MEMORY_FAILURE 宏):
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] Page Fault with HUGETLB Memory on MCE(UE) --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-HUGETLB-MCE-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
BiscuitOS-PAGING-PF-HUGETLB-MCE-default Source Code on Gitee
实践案例由一个应用程序构成,其分作三部分,首先是 21 行调用 mmap 函数结合 MAP_PRIVATE、MAP_HUGETLB 和 MAP_ANONYMOUS 分配一段可读可写的匿名 HugeTLB 内存,并在 33 行对匿名内存执行写操作, 以及在 35 行再次对匿名内存执行读操作. 第二部分是利用 madvise 注入 UE,由于 UE 是硬件行为,所有只能借助软件来模拟 UE 的发生,于是函数在 38 行调用 madvise 函数发送 MADV_HWPOISON 请求. 最后是第三部分,程序在 41 行再次对匿名内存执行写操作,此时会再次触发缺页并被识别为 UE Page Fault. 以上便是一个最基础的实践案例,可以知道 33 行读操作和 41 行写操作都会触发缺页,为了可以看到内存在缺页异常里的流动,本次在 41 行前后加上 BS_DEBUG 开关:
接着在匿名 HugeTLB 内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 do_swap_page 函数的 3747 行加上 bs_debug 打印,以此确认内存是向哪个函数流动,接下来执行如下命令进行实践:
# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-HUGETLB-MCE-default/
# 编译内核
make kernel
# 编程程序
make build
当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “HUGETLB MCE PF on hugetlb_fault 0x6000000000”, 说明缺页异常处理函数执行过 HugeTLB UE 缺页. 通过实践可以看到实践案例按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看匿名内存在缺页异常处理流程里的流动.
当进程再次访问 UE 的虚拟内存时,MMU 检查到物理内存不存在(因为 MCE 异常处理函数将页表的 Present 标志位清除),那么 MMU 触发缺页异常,异常处理函数首先调用 is_vm_hugetlb_page 函数判断是 HugeTLB 内存,于是调用 hugetlb_fault 函数处理缺页异常处理,其首先调用 huge_pte_offset 获得页表,此时页表内容不空,那么继续调用 is_hugetlb_entry_hwpoisoned 函数,检查页表 Entry 是否是一个 POISON 页表,此时检查到属于 POISON 页表,那么直接返回 VM_FAULT_HWPOISON_LARGE,那么最终程序会被 Kill 掉.
在 do_user_addr_fault 函数处理异常处理返回值时,VM_FAULT_HWPOISON 返回值会进入 1454 行分支,并调用 do_sigbus 函数发起信号杀死进程。以上便是 HugeTLB UE Page Fault 缺页处理的流程.