导言
有的同学问:缺页异常不是一种中断异常?中断异常难道不是原子上下文?为什么还可以睡眠?
答案:缺页异常并非原子上下文,而是由明确上下文触发的“同步异常”。下面我们详细分析用户地址与内核地址缺页异常各自的上下文情况。
一、用户地址缺页异常
性质:同步异常,由进程访问用户态虚拟地址触发,运行在明确的进程上下文中。
上下文:与系统调用类似,不是原子上下文,抢占未被禁用,preempt_count正常。
可否睡眠:✅ 可以。缺页处理路径中允许:
获取
mmap_read_lock(甚至mmap_read_lock_killable)调用
alloc_pages(GFP_KERNEL)分配物理页执行 swap I/O 换入页面
等待锁、等待内存回收
信号响应:mmap_read_lock_killable()允许在等待锁时被SIGKILL终止。
原子上下文中的例外:若进程在持有自旋锁或pagefault_disable()期间访问用户内存,缺页处理被faulthandler_disabled()拦截,通过异常表修复返回-EFAULT,不会睡眠。
二、内核地址缺页异常
性质:内核访问内核空间地址(如 vmalloc 区域、直接映射区)时触发。
上下文:取决于代码路径,可能处于进程上下文,也可能处于原子上下文。
可否睡眠:
非原子上下文:✅ 允许。如 vmalloc 按需填充页表时,可分配页表页、睡眠等待。
原子上下文:❌ 禁止。若发生,直接 Oops 或返回错误。
关键保障:
现代 x86_64 的
vmalloc()立即填充页表,返回后访问不再缺页,因此常规使用无需担心上下文。x86_32 或特定架构仍可能存在 vmalloc 惰性映射,此时必须由开发者保障:确保访问发生在非原子上下文,或页表已预填充。
vmap()、ioremap()等也可能有惰性映射行为,同样需要开发者确认上下文。
原子上下文中访问用户内存的预期缺页:
即使是
copy_from_user这种预期内的缺页,若发生在原子上下文,faulthandler_disabled()为真,缺页处理被拒绝,通过异常表返回-EFAULT,不会睡眠。
三、统一判断入口:faulthandler_disabled()
c
#define faulthandler_disabled() (pagefault_disabled() || in_atomic())
pagefault_disabled():显式调用了pagefault_disable()。in_atomic():处于中断上下文、持有自旋锁、禁用了抢占。
只要该宏为真,缺页处理一律被拒绝:
用户态:返回
SIGSEGV或SIGBUS。内核态:通过异常表修复返回
-EFAULT,或直接 Oops。
四、最终结论
| 缺页类型 | 上下文 | 可否睡眠 | 保障方 |
|---|---|---|---|
| 用户地址缺页 | 进程上下文,非原子 | ✅ 可以 | 内核缺页处理程序自动处理 |
| 内核态访问用户内存(预期内) | 非原子 | ✅ 可以 | 内核缺页处理程序 |
| 内核态访问用户内存(原子上下文) | 原子 | ❌ 禁止 | 异常表返回-EFAULT |
| 内核态访问 vmalloc(x86_64) | 非原子(页表已建) | ✅ 可以 | vmalloc()已预填充页表 |
| 内核态访问 vmalloc(惰性映射) | 非原子 | ✅ 可以 | 开发者需确保非原子上下文 |
| 内核态访问 vmalloc(原子上下文) | 原子 | ❌ 禁止 | 会 Oops |
| 内核态野指针 | 任意 | ❌ 禁止 | Oops / Panic |
一句话总结:用户地址缺页是进程上下文中的同步异常,可以睡眠;内核地址缺页是否允许睡眠取决于是否在原子上下文以及页表是否已建立。现代 x86_64 的vmalloc()已预填充页表,常规访问不会缺页;但惰性映射场景下,开发者必须自行保障访问不在原子上下文中,否则会触发 Oops。