内核中的page fault & copy_from_user

内核态的page fault?

前段时间有同事问了个问题:内核中是否可能发生page fault?

一时没能给出准确答案,当即有种感觉:难道是对内核内存管理的理解还不够,之前在这方面还是比较自信的~

问题看似很简单,从之前的理解来看,以经典的32位X86为例,内核态低端地址都是线性映射的,页表都是事先(内核初始化时)创建好的,对于这段地址,应该不会发生page fault;但对于vmalloc区,是通过页表动态映射的,这部分显然是可能发生缺页的。

看似问题有答案了,但TX问的不是针对内核态地址发送的缺页,而是:在内核态下,对用户态地址的访问,是否发生page fault?

我追问了一句:什么情况下,内核态需要访问用户态地址? 回答:比如copy_from_user.

问题清晰了,这种情况下,可能发生缺页吗?

硬件层面

从硬件层面上来说,当CPU做访问操作时,MMU硬件会遍历页表,查找匹配的映射条目,如果没有找到,就会自动触发page fault。

所以,从这个角度看,只要copy_from_user参数中的用户态地址对应的页表项不存在,就会产生page fault。

不应该发生吧?

换个角度,内核要使用copy_from_user从用户态拷贝数据时,按理数据应该都已经准备好了吧,也就是说相应的内存(物理)应该都已经分配好了把,不应该发生page fault了吧?

正常情况下,的确如此,但万一内核非要访问未映射的用户态地址呢?万一用户态数据此时没有准备好呢?谁能保证?

所以,虽然不应该发生,但还是可能发送的。

有什么不一样么?

这种情况下,在内核中发生的page fault,与平常我们见到的在用户态发生的page fault有什么不同么?

在用户态时,发生缺页时,CPU硬件会切换到到特权模式(内核态),进行异常处理;在内核态时发生缺页,由于当前本来就出于内核态,所以处理方式肯定会有不同。这也许只是一方面,应该还有其它不同~

如何处理的?

在page fault的流程中针对Vmalloc区发生的缺页有专门的处理,可以参见vmalloc_fault函数。

那对于copy_from_user的情况呢?

肯定的。其实就是do_page_fault流程中exception table相关的处理,之前对这里的代码没有仔细理解清楚,原来就是用来处理这种情况的。

为什么要用copy_from_user?

还是回到老问题,为什么要用copy_from_user,按理解,拷贝数据,不就是从一个地址搬移数据到另一个地址,用memcpy不就好了?

就是因为可能发生page fault的情况,memcpy对这种情况没有任何的处理措施,可能引发不可知的后果。

而copy_from_user对这种情况进行了处理,处理方式就是在代码中增加了一个.fixup的段,该段在do_page_fault中会被读取,结合exception table进行相应的修正,具体修正方式,没时间深入研究了,感兴趣可以继续看看。

copy_from_user代码如下:

static unsigned long
__copy_user_intel(void __user *to, const void *from, unsigned long size)
{
	int d0, d1;
	__asm__ __volatile__(
		       "       .align 2,0x90\n"
		       "1:     movl 32(%4), %%eax\n"
		       "       cmpl $67, %0\n"
		       "       jbe 3f\n"
		       "2:     movl 64(%4), %%eax\n"
		       "       .align 2,0x90\n"
		       "3:     movl 0(%4), %%eax\n"
		       "4:     movl 4(%4), %%edx\n"
		       "5:     movl %%eax, 0(%3)\n"
		       "6:     movl %%edx, 4(%3)\n"
		       "7:     movl 8(%4), %%eax\n"
		       "8:     movl 12(%4),%%edx\n"
		       "9:     movl %%eax, 8(%3)\n"
		       "10:    movl %%edx, 12(%3)\n"
		       "11:    movl 16(%4), %%eax\n"
		       "12:    movl 20(%4), %%edx\n"
		       "13:    movl %%eax, 16(%3)\n"
		       "14:    movl %%edx, 20(%3)\n"
		       "15:    movl 24(%4), %%eax\n"
		       "16:    movl 28(%4), %%edx\n"
		       "17:    movl %%eax, 24(%3)\n"
		       "18:    movl %%edx, 28(%3)\n"
		       "19:    movl 32(%4), %%eax\n"
		       "20:    movl 36(%4), %%edx\n"
		       "21:    movl %%eax, 32(%3)\n"
		       "22:    movl %%edx, 36(%3)\n"
		       "23:    movl 40(%4), %%eax\n"
		       "24:    movl 44(%4), %%edx\n"
		       "25:    movl %%eax, 40(%3)\n"
		       "26:    movl %%edx, 44(%3)\n"
		       "27:    movl 48(%4), %%eax\n"
		       "28:    movl 52(%4), %%edx\n"
		       "29:    movl %%eax, 48(%3)\n"
		       "30:    movl %%edx, 52(%3)\n"
		       "31:    movl 56(%4), %%eax\n"
		       "32:    movl 60(%4), %%edx\n"
		       "33:    movl %%eax, 56(%3)\n"
		       "34:    movl %%edx, 60(%3)\n"
		       "       addl $-64, %0\n"
		       "       addl $64, %4\n"
		       "       addl $64, %3\n"
		       "       cmpl $63, %0\n"
		       "       ja  1b\n"
		       "35:    movl  %0, %%eax\n"
		       "       shrl  $2, %0\n"
		       "       andl  $3, %%eax\n"
		       "       cld\n"
		       "99:    rep; movsl\n"
		       "36:    movl %%eax, %0\n"
		       "37:    rep; movsb\n"
		       "100:\n"
		       ".section .fixup,\"ax\"\n"
		       "101:   lea 0(%%eax,%0,4),%0\n"
		       "       jmp 100b\n"
		       ".previous\n"
		       _ASM_EXTABLE(1b,100b)
		       _ASM_EXTABLE(2b,100b)
		       _ASM_EXTABLE(3b,100b)
		       _ASM_EXTABLE(4b,100b)
		       _ASM_EXTABLE(5b,100b)
		       _ASM_EXTABLE(6b,100b)
		       _ASM_EXTABLE(7b,100b)
		       _ASM_EXTABLE(8b,100b)
		       _ASM_EXTABLE(9b,100b)
		       _ASM_EXTABLE(10b,100b)
		       _ASM_EXTABLE(11b,100b)
		       _ASM_EXTABLE(12b,100b)
		       _ASM_EXTABLE(13b,100b)
		       _ASM_EXTABLE(14b,100b)
		       _ASM_EXTABLE(15b,100b)
		       _ASM_EXTABLE(16b,100b)
		       _ASM_EXTABLE(17b,100b)
		       _ASM_EXTABLE(18b,100b)
		       _ASM_EXTABLE(19b,100b)
		       _ASM_EXTABLE(20b,100b)
		       _ASM_EXTABLE(21b,100b)
		       _ASM_EXTABLE(22b,100b)
		       _ASM_EXTABLE(23b,100b)
		       _ASM_EXTABLE(24b,100b)
		       _ASM_EXTABLE(25b,100b)
		       _ASM_EXTABLE(26b,100b)
		       _ASM_EXTABLE(27b,100b)
		       _ASM_EXTABLE(28b,100b)
		       _ASM_EXTABLE(29b,100b)
		       _ASM_EXTABLE(30b,100b)
		       _ASM_EXTABLE(31b,100b)
		       _ASM_EXTABLE(32b,100b)
		       _ASM_EXTABLE(33b,100b)
		       _ASM_EXTABLE(34b,100b)
		       _ASM_EXTABLE(35b,100b)
		       _ASM_EXTABLE(36b,100b)
		       _ASM_EXTABLE(37b,100b)
		       _ASM_EXTABLE(99b,101b)
		       : "=&c"(size), "=&D" (d0), "=&S" (d1)
		       :  "1"(to), "2"(from), "0"(size)
		       : "eax", "edx", "memory");
	return size;
}

代码整体上跟memcpy实现差不多,主要差别就是fixup段了,感兴趣的TX可以对比看看。


原文地址: http://happyseeker.github.io/kernel/2016/12/30/page-fault-in-kernel.html

asmlinkage void __kprobes do_page_fault(struct pt_regs *regs, unsigned long error_code, unsigned long address) { unsigned long vec; struct task_struct *tsk; struct mm_struct *mm; struct vm_area_struct * vma; vm_fault_t fault; unsigned int flags = FAULT_FLAG_DEFAULT; tsk = current; mm = tsk->mm; vec = lookup_exception_vector(); if (unlikely(fault_in_kernel_space(address))) { if (vmalloc_fault(address) >= 0) return; if (kprobe_page_fault(regs, vec)) return; bad_area_nosemaphore(regs, error_code, address); return; } if (unlikely(kprobe_page_fault(regs, vec))) return; if ((regs->sr & SR_IMASK) != SR_IMASK) local_irq_enable(); perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS, 1, regs, address); if (unlikely(faulthandler_disabled() || !mm)) { bad_area_nosemaphore(regs, error_code, address); return; } retry: vma = lock_mm_and_find_vma(mm, address, regs); if (unlikely(!vma)) { bad_area_nosemaphore(regs, error_code, address); return; } if (unlikely(access_error(error_code, vma))) { bad_area_access_error(regs, error_code, address); return; } set_thread_fault_code(error_code); if (user_mode(regs)) flags |= FAULT_FLAG_USER; if (error_code & FAULT_CODE_WRITE) flags |= FAULT_FLAG_WRITE; fault = handle_mm_fault(vma, address, flags, regs); if (unlikely(fault & (VM_FAULT_RETRY | VM_FAULT_ERROR))) if (mm_fault_error(regs, error_code, address, fault)) return; if (fault & VM_FAULT_COMPLETED) return; if (fault & VM_FAULT_RETRY) { flags |= FAULT_FLAG_TRIED; goto retry; } mmap_read_unlock(mm); }解释上面代码, 并说明哪些是处理用户空间缺页的方法
03-08
<think>好的,我现在需要解释用户提供的这段Linux内核中处理缺页异常的代码,并指出哪些部分处理用户空间的缺页。首先,我需要理清代码的结构和流程。 首先,代码中的函数是do_page_fault,它有三个参数:regs、error_code和address。根据内核知识,缺页异常处理函数通常负责处理页面错误,比如访问未映射的内存或写只读页等情况。这个函数需要区分内核空间和用户空间的缺页,并分别处理。 接下来,代码一开始获取当前任务tsk和它的内存管理结构mm。然后通过lookup_exception_vector获取异常向量号vec。这里可能涉及异常类型的判断,但暂时先记下。 然后,代码检查address是否在内核空间,使用fault_in_kernel_space函数。如果是内核空间的地址,会尝试处理vmalloc_fault,如果成功则返回。否则,检查kprobe的页面错误处理,如果处理了也返回。如果都不是,就调用bad_area_nosemaphore,这可能是处理内核态的错误,比如无效访问,然后返回。这部分明显是处理内核空间缺页的逻辑,所以用户空间处理应该不在这里。 接下来,如果异常发生在用户空间,代码继续执行。这里有一个条件判断(regs->sr & SR_IMASK) != SR_IMASK,可能涉及中断状态的处理,开启本地中断。然后记录性能事件PERF_COUNT_SW_PAGE_FAULTS,统计缺页次数。 之后检查faulthandler_disabled()或者mm是否为空,如果满足条件,进入bad_area处理,返回。这可能表示当前无法处理缺页或进程没有内存空间,需要处理错误。 然后进入retry标签,调用lock_mm_and_find_vma来锁定内存管理结构并找到对应的虚拟内存区域vma。如果没有找到vma,说明地址不在任何有效的用户空间区域,触发bad_area。如果找到vma,但访问权限错误(access_error),则调用bad_area_access_error处理权限错误。 接下来,设置线程的错误代码,并根据是否是用户模式以及错误代码中的写标志,设置flags。然后调用handle_mm_fault来处理具体的缺页,这里应该是处理用户空间缺页的核心部分。handle_mm_fault可能会触发页面调入、写时复制等操作。 处理完handle_mm_fault后,检查返回值fault是否有重试或错误标志。如果有错误,调用mm_fault_error处理,返回。如果fault包含VM_FAULT_COMPLETED,可能表示处理完成,直接返回。如果需要重试(VM_FAULT_RETRY),则设置TRIED标志,跳转回retry重新处理。最后释放mm的读锁。 综上,处理用户空间缺页的主要部分应该在检查内核空间之后的部分,尤其是通过handle_mm_fault函数。此外,在retry部分查找vma,检查访问权限,设置flags并调用handle_mm_fault,这些步骤都是处理用户缺页的关键。而处理内核空间的部分则是在前面的if块中。需要确认每个步骤的作用,特别是handle_mm_fault是否确实是用户空间处理的核心函数。</think>以下是对Linux内核`do_page_fault`函数的分析,重点说明用户空间缺页处理方法: ### 代码功能概述 这是Linux内核处理缺页异常的核心函数,负责处理以下场景: - **用户空间缺页**(如访问未映射内存、写只读页等) - **内核空间缺页**(如vmalloc区域缺页) - **错误地址处理**(无效内存访问) --- ### 用户空间缺页处理流程 #### 1. **基础检查** ```c if (unlikely(faulthandler_disabled() || !mm)) { bad_area_nosemaphore(regs, error_code, address); return; } ``` - 确认缺页处理未被禁用 - 检查进程是否有有效内存描述符(mm) #### 2. **虚拟内存区域查找** ```c retry: vma = lock_mm_and_find_vma(mm, address, regs); if (unlikely(!vma)) { bad_area_nosemaphore(regs, error_code, address); return; } ``` - 通过`lock_mm_and_find_vma`锁定内存管理结构并查找对应虚拟内存区域(VMA) - 未找到VMA时触发`bad_area`错误 #### 3. **权限验证** ```c if (unlikely(access_error(error_code, vma))) { bad_area_access_error(regs, error_code, address); return; } ``` - 检查访问权限是否匹配VMA属性(如尝试写只读区域) #### 4. **标志设置** ```c if (user_mode(regs)) flags |= FAULT_FLAG_USER; if (error_code & FAULT_CODE_WRITE) flags |= FAULT_FLAG_WRITE; ``` - 设置用户模式标志 - 设置写操作标志(用于COW处理) #### 5. **核心处理函数** ```c fault = handle_mm_fault(vma, address, flags, regs); ``` 这是用户空间缺页处理的核心方法,主要功能包括: - 触发页表项构建 - 处理写时复制(COW) - 调页(按需分页) - 匿名页/文件映射页处理 #### 6. **错误处理与重试** ```c if (unlikely(fault & (VM_FAULT_RETRY | VM_FAULT_ERROR))) if (mm_fault_error(regs, error_code, address, fault)) return; if (fault & VM_FAULT_RETRY) { flags |= FAULT_FLAG_TRIED; goto retry; } ``` - 处理需要重试的情况(如获取锁失败) - 处理OOM等错误条件 --- ### 内核空间处理流程(对比) ```c if (unlikely(fault_in_kernel_space(address))) { if (vmalloc_fault(address) >= 0) return; if (kprobe_page_fault(regs, vec)) return; bad_area_nosemaphore(regs, error_code, address); return; } ``` - 处理vmalloc区域的缺页 - 内核探针(kprobe)相关处理 - 无效内核地址访问处理 --- ### 关键区别总结 | 特性 | 用户空间缺页处理 | 内核空间缺页处理 | |--------------------|--------------------|-----------------| | 主要处理函数 | `handle_mm_fault` | `vmalloc_fault` | | 权限检查 | VMA权限验证 | 内核地址有效性验证 | | 典型场景 | COW、按需分页 | vmalloc区域访问 | | 中断处理 | 可能开启中断 | 保持中断状态 | | 性能统计 | PERF_COUNT_SW_PAGE_FAULTS | 无统计 | --- ### 补充说明 1. `FAULT_FLAG_USER`标志用于区分用户态/内核态缺页 2. `FAULT_FLAG_WRITE`标志触发写时复制机制 3. `lock_mm_and_find_vma`在Linux 5.11+中替代了原来的`find_vma`+锁操作 4. 重试机制确保在竞争条件下可靠处理缺页
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值