高级操作系统-do_page_fault

\linux-3.18\arch\目录下,每个支持的处理器架构都在它相应的文件夹中,如arm64arm32、``x86mips

文件在\linux-3.18\arch\x86\mm\fault.c;这里针对x86架构下的内存管理模块

阅读代码,分析do_page_fault()函数

  • 写一份代码分析和注释报告,包括流程图等

函数调用流程图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
 graph LR

do_page_fault --> exception_enter;
do_page_fault --> __do_page_fault;
do_page_fault --> exception_exit;
__do_page_fault --> 基本设置+mmio_fault;
__do_page_fault --> 处理内核态缺页异常;
__do_page_fault --> 处理用户态缺页异常;
__do_page_fault --> check_v8086_mode;

处理内核态缺页异常 --> 位于vmalloc区 --> vmalloc_fault;
处理内核态缺页异常 --> 假缺页 -->TLB延迟Flush;
假缺页 --> Kprobe引起;
处理内核态缺页异常 --> 非位于vmalloc区 --> bad_area_nosemaphore;

处理用户态缺页异常 --> SMAP --> bad_area_nosemaphore;
处理用户态缺页异常 --> 中断没有用户上下文或者处于临界区 --> bad_area_nosemaphore;
处理用户态缺页异常 --> 内核中的其他错误导致mmap_sem死锁 --> bad_area_nosemaphore;


处理用户态缺页异常 --> 在当前进程的地址空间中查找发生异常的地址对应vma;
在当前进程的地址空间中查找发生异常的地址对应vma --> 未找到 --> bad_area;
在当前进程的地址空间中查找发生异常的地址对应vma --> 不在有效地址 --> bad_area;
在当前进程的地址空间中查找发生异常的地址对应vma --> 地址与栈顶距离越界 --> bad_area;
在当前进程的地址空间中查找发生异常的地址对应vma --> 正常地址 --> bad_area_access_error
正常地址 --> mm_fault_error

do_page_fault(struct pt_regs *regs, unsigned long error_code)

其函数为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* 两个个参数
1. regs:在堆栈中保存异常发生时的现场寄存器信息
2. error_code:表示异常发生的类型
*/
dotraplinkage void notrace
do_page_fault(struct pt_regs *regs, unsigned long error_code)
{
// CR2是页故障线性地址寄存器,保存最后一次出现页故障的全32位线性地址。
//这里读取寄存器CR2
unsigned long address = read_cr2();
enum ctx_state prev_state;

// 这里是进入异常处理状态,维护上下文
prev_state = exception_enter();
// 调用缺页处理函数,并传参
__do_page_fault(regs, error_code, address);
// 退出改异常处理状态
exception_exit(prev_state);
}

__do_page_fault(regs, error_code, address);

该函数为缺页处理的实际函数,完整代码如下

并将代码分析注释添加在下述代码中

1、mmio_fault

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/* 三个参数
1. regs:在堆栈中保存异常发生时的现场寄存器信息
2. error_code:表示异常发生的类型
3. address: 最后一次出现页故障的全32位线性地址
*/
static noinline void
__do_page_fault(struct pt_regs *regs, unsigned long regs,
unsigned long address)
{

// 下述定义 虚拟内存描述;进程描述符;进程中的内存描述符
struct vm_area_struct *vma;
struct task_struct *tsk;
struct mm_struct *mm;
int fault;
// 设置允许重试标志和可杀死进程标志
unsigned int flags = FAULT_FLAG_ALLOW_RETRY | FAULT_FLAG_KILLABLE;

// 获取当前进程描述符和当前进程中的内存描述符
tsk = current;
mm = tsk->mm;

// 这里判断内存也是否存在present 标记 (页表项PTE中的P位(Present标志)),存在的话,则会调用 kmemcheck_hide函数 清除该标记,重置为0
// 用处是 ,present 置为0,使得,再对该页的访问 触发缺页异常
if (kmemcheck_active(regs))
kmemcheck_hide(regs);
// 获取内存的读写信号量
prefetchw(&mm->mmap_sem);

// unlikely 关键字,该分支的条件更不可能被满足
// kmmio_fault 函数,mmio发生缺页的概率较低,如果发生不在这个函数里进行处理
if ( unlikely(kmmio_fault(regs, address)))
return;

………………

}

2、处理内核态缺页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/* 三个参数
1. regs:在堆栈中保存异常发生时的现场寄存器信息
2. error_code:表示异常发生的类型
3. address: 最后一次出现页故障的全32位线性地址
*/
static noinline void
__do_page_fault(struct pt_regs *regs, unsigned long regs,
unsigned long address)
{
………………


// 判断缺页地址是不是在内核地址空间
// 有若干种类型,分别进行处理
if (unlikely(fault_in_kernel_space(address))) {
// 检查标志位确定访问发生在"内核态"
if (!(error_code & (PF_RSVD | PF_USER | PF_PROT))) {
// 这里是对于发生在内核空间里的错误 (error_code & 4) == 0
// 且 错误是非保护型错误 (error_code & 9) == 0
if (vmalloc_fault(address) >= 0)
return;

if (kmemcheck_fault(regs, address, error_code))
return;
}

// 检查是否是旧的TLB(页表缓存)导致的假的缺页异常
//(TLB延迟flush导致的,因为提前flush会有比较大的性能代价)
if (spurious_fault(error_code, address))
return;

//判断是否kprobes引起的虚假错误
if (kprobes_fault(regs))
return;

// 异常位于内核态,触发内核异常,但是位于vmalloc的缺页异常前面已经处理过了,
// 所以如果不是缺页导致的,那就是内核有其他异常了,需要处理返回

// 根据异常的原因和类型,选择适当的处理方法
bad_area_nosemaphore(regs, error_code, address);

return;
}
………………

}

3、用户态缺页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
/* 三个参数
1. regs:在堆栈中保存异常发生时的现场寄存器信息
2. error_code:表示异常发生的类型
3. address: 最后一次出现页故障的全32位线性地址
*/
static noinline void
__do_page_fault(struct pt_regs *regs, unsigned long regs,
unsigned long address)
{
// mmio_fault
………………

// 内核态
………………

// 从这里往下处理的是用户态的缺页异常

// 还是判断是否kprobes引起的虚假错误
if (unlikely(kprobes_fault(regs)))
return;

// /*使用了保留位*/
/* pgtable_bad函数 : CPU寄存器和内核态堆栈的全部转储打印到控制台, 以及页表的相关信息,
并输出到一个系统消息缓冲 区,然后调用函数do_exit()杀死当前进程*/
if (unlikely(error_code & PF_RSVD))
pgtable_bad(regs, error_code, address);

// smap的保护特性,阻止了访问内核态虚拟地址
// SMAP(Supervisor Mode Access Prevention)是一种硬件特性,
// 主要用于阻止处理器在内核态下访问用户态的地址空间
if (unlikely(smap_violation(error_code, regs))) {
bad_area_nosemaphore(regs, error_code, address);
return;
}

// 如果此在在一个中断中,没有用户上下文或者 运行在临界区中
if (unlikely(in_atomic() || !mm)) {
// 根据异常的原因和类型,选择适当的处理方法
bad_area_nosemaphore(regs, error_code, address);
return;
}


//传参的address 即是CR2寄存器的值,且vmalloc fault 被处理,所以可以激活irq
// 现在说明不在中断中,所以开中断, 可以缩短因缺页异常导致的关中断时长
if (user_mode_vm(regs)) {
local_irq_enable();
error_code |= PF_USER;
flags |= FAULT_FLAG_USER;
} else {
if (regs->flags & X86_EFLAGS_IF)
local_irq_enable();
}

// 读写信号量
perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS, 1, regs, address);

if (error_code & PF_WRITE)
flags |= FAULT_FLAG_WRITE;

/*
当在内核中运行时,我们预计故障只会发生在用户空间中的地址上。所有其他故障都表示内核中的错误,并且应该生成OOPS。不幸的是,如果在已经包含mmap_sem的代码路径中发生错误故障,我们将在尝试根据地址空间验证故障时死锁。幸运的是,内核只有效地引用了定义良好的代码区域中的用户空间,这些代码区域列在异常表中。 由于绝大多数故障都是有效的,我们只会在可能出现死锁的情况下执行源引用检查。尝试锁定地址空间,如果不能,则验证源。如果这是无效的,我们可以跳过地址空间检查,从而避免死锁:
*/
// 避免内核中的其他错误导致mmap_sem死锁
if (unlikely(!down_read_trylock(&mm->mmap_sem))) {
if ((error_code & PF_USER) == 0 &&
!search_exception_tables(regs->ip)) {
bad_area_nosemaphore(regs, error_code, address);
return;
}
retry:
down_read(&mm->mmap_sem);
} else {
/*
* The above down_read_trylock() might have succeeded in
* which case we'll have missed the might_sleep() from
* down_read():
*/
might_sleep();
}

// 在当前进程的地址空间中查找发生异常的地址对应的vma
vma = find_vma(mm, address);
if (unlikely(!vma)) {
//如果找不到,则释放锁并且返回
bad_area(regs, error_code, address);
return;
}
//如果找到了,而且虚拟地址位于vma有效范围,则为正常的缺页异常,请求分配内存
if (likely(vma->vm_start <= address))
goto good_area;
if (unlikely(!(vma->vm_flags & VM_GROWSDOWN))) {
//虚拟地址不在vma有效范围,则是进程访问了非法地址,需要处理后返回
bad_area(regs, error_code, address);
return;
}
if (error_code & PF_USER) {
//压栈操作时,操作的地址最大的偏移为65536+32*sizeof(unsigned long),
//该操作由pusha命令触发(老版本中,pusha命令最大只能操作32字节,即 同时压栈8个寄存器)。
//如果访问的地址距栈顶的距离超过了,则肯定是非法 * 地址访问了。
//如果虚拟地址位于堆栈区附近
if (unlikely(address + 65536 + 32 * sizeof(unsigned long) < regs->sp)) {
bad_area(regs, error_code, address);
return;
}
}
//扩展堆栈区,堆栈区的虚拟地址是动态分配的,不是固定的

if (unlikely(expand_stack(vma, address))) {
//扩展失败,处理后返回
bad_area(regs, error_code, address);
return;
}

/*
* Ok, we have a good vm_area for this memory access, so
* we can handle it..
*/
good_area:
//正常的缺页异常处理,进行请求调页,分配物理内存
if (unlikely(access_error(error_code, vma))) {
bad_area_access_error(regs, error_code, address);
return;
}

// 这个是所有处理器共用的部分,专门处理用户空间的缺页异常
fault = handle_mm_fault(mm, vma, address, flags);


// retry时,先处理在等待的重要信号,
if (unlikely((fault & VM_FAULT_RETRY) && fatal_signal_pending(current)))
return;

// 执行 mm_fault_error 缺页处理
if (unlikely(fault & VM_FAULT_ERROR)) {
mm_fault_error(regs, error_code, address, fault);
return;
}

// 主要/次要页面错误核算仅在初次尝试时完成。如果我们进行重试,那么很可能会在页面缓存中找到该页面。
// 前面已经设置允许重试
if (flags & FAULT_FLAG_ALLOW_RETRY) {
if (fault & VM_FAULT_MAJOR) {
tsk->maj_flt++;
perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS_MAJ, 1,
regs, address);
} else {
tsk->min_flt++;
perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS_MIN, 1,
regs, address);
}
if (fault & VM_FAULT_RETRY) {
// 去除可重试标识;设置已经重试标识
flags &= ~FAULT_FLAG_ALLOW_RETRY;
flags |= FAULT_FLAG_TRIED;
goto retry;
}
}
// VM86模式(兼容老环境)相关检查
check_v8086_mode(regs, address, tsk);

up_read(&mm->mmap_sem);
}

reference

  1. linux内核内存管理-缺页异常 - 知乎 (zhihu.com)
  2. Linux内核内存管理(3):kmemcheck介绍-CSDN博客
  3. LINUX内存管理之页式管理之页表项标记位的理解_页表标记位-CSDN博客
  4. Supervisor Mode Access Prevention - Wikipedia