页式存储机制通过页面目录和页面表将每个线性地址(或者虚拟地址), 转化成物理地址。 然而, 如果在这个过程中遇到某种阻碍的话, 就会产生一次页面异常, 也称缺页异常。
主要有下面 3 中障碍:
1. 相应的页面目录项或者页面表项为空, ie, 线性地址到物理地址的映射关系并未建立或者已经被撤销。
2. 相应的物理页面不在内存中, 有页面描述项 vma 结构
3. 指令中规定的访问方式和页面的权限不符
do_page_fault 是页面异常服务的主体程序的入口。
==================== arch/i386/mm/fault.c 106 152 ====================
106 asmlinkage void do_page_fault(struct pt_regs *regs, unsigned long error_code)
107 {
108 struct task_struct *tsk;
109 struct mm_struct *mm;
110 struct vm_area_struct * vma;
111 unsigned long address;
112 unsigned long page;
113 unsigned long fixup;
114 int write;
115 siginfo_t info;
116
117 /* get the address */
118 __asm__("movl %%cr2,%0":"=r" (address));
119
120 tsk = current;
121
122 /* 123 * We fault-in kernel-space virtual memory on-demand. The 124 * 'reference' page table is init_mm.pgd. 125 * 126 * NOTE! We MUST NOT take any locks for this case. We may 127 * be in an interrupt or a critical region, and should 128 * only copy the information from the master page table, 129 * nothing more. 130 */
131 if (address >= TASK_SIZE)
132 goto vmalloc_fault;
133
134 mm = tsk->mm;
135 info.si_code = SEGV_MAPERR;
136
137 /* 138 * If we're in an interrupt or have no user 139 * context, we must not take the fault.. 140 */
141 if (in_interrupt() || !mm)
142 goto no_context;
143
144 down(&mm->mmap_sem);
145
146 vma = find_vma(mm, address);
147 if (!vma)
148 goto bad_area;
149 if (vma->vm_start <= address)
150 goto good_area;
151 if (!(vma->vm_flags & VM_GROWSDOWN)) // 这里实际讨论的越界访问会走到这里
152 goto bad_area;
首先使用汇编代码, 获取CR2 寄存器中的 映射失败时候的线性地址,传入参数 regs 是内核中断机制响应保留的现场, error_code 表征映射失败的原因。
需要注意的是, 代码 中 current 不是一个全局变量, 这是一个宏, 用来获取当前进程的task_struct 结构的地址。
另外, cpu 实际进行的映射是通过页面目录 和 页面表完成的, task_struct 中有一个指向mm_struct 结构的指针, 跟虚存管理和映射相关的信息都存放在这个结构中。
if (in_interrupt() || !mm) 用来处理两种特殊情况, 1. 映射失败发生在某个中断服务中, 2. 进程映射还没有被建立起来。 这些都不是我们这里需要处理的。
由于下面需要操作进程中共享的mm_struct 结构, 所以需要加锁, down () 就是起到这个加锁的作用的。
然后, 通过 find_vma() 试图在一个虚存空间中找到一个结束地址大于给定地址的第一个区间, 特别需要注意的是, 找出的 vma 的 vm_start 可能也是大于 address 的。
==================== mm/mmap.c 404 440 ====================
404 /* Look up the first VMA which satisfies addr < vm_end, NULL if none. */
405 struct vm_area_struct * find_vma(struct mm_struct * mm, unsigned long addr)
406 {
407 struct vm_area_struct *vma = NULL;
408
409 if (mm) {
410 /* Check the cache first. */
411 /* (Cache hit rate is typically around 35%.) */
412 vma = mm->mmap_cache;
413 if (!(vma && vma->vm_end > addr && vma->vm_start <= addr)) {
414 if (!mm->mmap_avl) {
415 /* Go through the linear list. */
416 vma = mm->mmap;
417 while (vma && vma->vm_end <= addr)
418 vma = vma->vm_next;
419 } else {
420 /* Then go through the AVL tree quickly. */
421 struct vm_area_struct * tree = mm->mmap_avl;
422 vma = NULL;
423 for (;;) {
424 if (tree == vm_avl_empty)
425 break;
426 if (tree->vm_end > addr) {
427 vma = tree;
428 if (tree->vm_start <= addr)
429 break;
430 tree = tree->vm_avl_left;
431 } else
432 tree = tree->vm_avl_right;
433 }
434 }
435 if (vma)
436 mm->mmap_cache = vma;
437 }
438 }
439 return vma;
440 }
这段代码负责查找一个虚存空间中找到一个结束地址大于给定地址的第一个区间, 他利用 mmap_cache 辅助查找( 有 35% 的命中率), avl 树, 链表搜索等方式查找。
回到我们的do_page_fault, find_vma 返回的结果可能是:
1. 没有找到 vma == nullptr, 没有一个区间的结束地址高于定义的地址, ie, 这个地址在堆栈上面去了, 地址越界了
2. 找到了一个 vma, 并且 vm_start <= address, 表明这个区间的描述是OK 的, 需要进一步看下是不是由于访问权限 或者 由于对象不在 内存中引起的异常。
3. 找到一个 vma 但是 vm_start > address, 这就表明 我们的的 address 落到中间的空洞里面去了。
可以参考下面这张图, 协助理解, 数据和代码空间是从下向上增长的, 而堆栈是自上而下增长的。
根据 vm_area_t 中的 vm_flags 中的 VM_GROWSDOWN 这个标志位, 我们可以知道address 当前是在 代码数据区里面(仅仅因为映射被撤销了) 还是 堆栈区里面。
==================== arch/i386/mm/fault.c 220 239 ====================
[do_page_fault()]
220 /*
221 * Something tried to access memory that isn't in our memory map..
222 * Fix it, but check if it's kernel or user first..
223 */
224 bad_area:
225 up(&mm->mmap_sem);
226
227 bad_area_nosemaphore:
228 /* User mode accesses just cause a SIGSEGV */
229 if (error_code & 4) {
230 tsk->thread.cr2 = address;
231 tsk->thread.error_code = error_code;
232 tsk->thread.trap_no = 14;
233 info.si_signo = SIGSEGV;
234 info.si_errno = 0;
235 /* info.si_code has been set above */
236 info.si_addr = (void *)address;
237 force_sig_info(SIGSEGV, &info, tsk);
238 return;
239 }
==================== arch/i386/mm/fault.c 96 105 ====================
96 /*
97 * This routine handles page faults. It determines the address,
98 * and the problem, and then passes it off to one of the appropriate
99 * routines.
100 *
101 * error_code:
102 * bit 0 == 0 means no page found, 1 means protection fault
103 * bit 1 == 0 means read, 1 means write
104 * bit 2 == 0 means kernel, 1 means user-mode
105 */
也就是说, error_code 的bit2 为 1, 表征cpu 处于用户模式的时候发生了异常, 这时候, 就会给出一个软中断 SIGSEGV, 至此, 进程就因为异常访问而挂掉了。
ps: SIGSEGV 是一个强制性信号, cpu 必须处理。
我们这里所讨论的内存越界主要就是指, 访问了一段数据或者代码区的 data, 而这个data 的映射 正好被撤销了, 留下一个孤立的空洞,或者就没有建立过映射
通过进入
if (!(vma->vm_flags & VM_GROWSDOWN))
goto bad_area;
造成内存越界访问。
为方便理解, 我们绘制了这么一幅图, 空洞2 是未分配的空间, 空洞1 是 建立过映射但是现在映射被撤销的部分。 而我们这里讨论的内存越界 指的就是 访问这里的空洞 1 或者 空洞 2 的过程。
这里讨论的是一种特殊情况, 我们的堆栈区间比较小, 并且在已经满了情况下, 如果此时又发生了一个程序调用, 就需要将返回地址压栈, 可是这时候, 栈满了,于是, 触发了页面异常。
==================== arch/i386/mm/fault.c 151 164 ====================
[do_page_fault()]
151 if (!(vma->vm_flags & VM_GROWSDOWN))
152 goto bad_area;
153 if (error_code & 4) {
154 /*
155 * accessing the stack below %esp is always a bug.
156 * The "+ 32" is there due to some instructions (like
157 * pusha) doing post-decrement on the stack and that
158 * doesn't show up until later..
159 */
160 if (address + 32 < regs->esp)
161 goto bad_area;
162 }
163 if (expand_stack(vma, address))
164 goto bad_area;
首先需要描述一下现在的情形, 由于堆栈区已经满了, 我们现在落在堆栈区下方的空洞内,但是我们距离这个堆栈区很近。
由于 i386 cpu 有一条pusha 指令, 可以一次将32 个字节压入堆栈, 所以这里采用的判断标准是 %esp - 32, 落在这个范围内的, 我们认为是正常的扩展堆栈的需求, 否则不是。
==================== include/linux/mm.h 487 504 ====================
[do_page_fault()>expand_stack()]
487 /* vma is the first one with address < vma->vm_end,
488 * and even address < vma->vm_start. Have to extend vma. */
489 static inline int expand_stack(struct vm_area_struct * vma, unsigned long address)
490 {
491 unsigned long grow;
492
493 address &= PAGE_MASK;
494 grow = (vma->vm_start - address) >> PAGE_SHIFT;
495 if (vma->vm_end - address > current->rlim[RLIMIT_STACK].rlim_cur ||
496 ((vma->vm_mm->total_vm + grow) << PAGE_SHIFT) > current->rlim[RLIMIT_AS].rlim_cur)
497 return -ENOMEM;
498 vma->vm_start = address;
499 vma->vm_pgoff -= grow;
500 vma->vm_mm->total_vm += grow;
501 if (vma->vm_flags & VM_LOCKED)
502 vma->vm_mm->locked_vm += grow;
503 return 0;
504 }
我们这里使用 address &= PAGE_MASK; 实现对齐页面边界。自此之后的address 都是对齐过页面边界的 address 了。
然后判断 这段内存分配的量是不是超过了资源限制, 如果超过了限制, 返回 -ENOMEM。
如果成功, 更新vma, mm 结构中的数据信息。但是, 新扩展的页面对物理内存的映射到这里还是没有建立起来, 需要good_area 继续完成。
==================== arch/i386/mm/fault.c 165 207 ====================
[do_page_fault()]
165 /* 166 * Ok, we have a good vm_area for this memory access, so 167 * we can handle it.. 63 168 */
169 good_area:
170 info.si_code = SEGV_ACCERR;
171 write = 0;
172 switch (error_code & 3) {
173 default: /* 3: write, present */
174 #ifdef TEST_VERIFY_AREA
175 if (regs->cs == KERNEL_CS)
176 printk("WP fault at %08lx\n", regs->eip);
177 #endif
178 /* fall through */
179 case 2: /* write, not present */
180 if (!(vma->vm_flags & VM_WRITE))
181 goto bad_area;
182 write++;
183 break;
184 case 1: /* read, present */
185 goto bad_area;
186 case 0: /* read, not present */
187 if (!(vma->vm_flags & (VM_READ | VM_EXEC)))
188 goto bad_area;
189 }
190
191 /* 192 * If for any reason at all we couldn't handle the fault, 193 * make sure we exit gracefully rather than endlessly redo 194 * the fault. 195 */
196 switch (handle_mm_fault(mm, vma, address, write)) {
197 case 1:
198 tsk->min_flt++;
199 break;
200 case 2:
201 tsk->maj_flt++;
202 break;
203 case 0:
204 goto do_sigbus;
205 default:
206 goto out_of_memory;
207 }
这里需要涉及写操作, 但是页面不在内存中, ie, code 为 2, 这时候需要检测vma 的 写属性, 很明显的, 堆栈区是允许写入的, 于是这里会调用 handle_mm_fault。
==================== mm/memory.c 1189 1208 ====================
[do_page_fault()>handle_mm_fault()]
1189 /* 1190 * By the time we get here, we already hold the mm semaphore 1191 */
1192 int handle_mm_fault(struct mm_struct *mm, struct vm_area_struct * vma,
1193 unsigned long address, int write_access)
1194 {
1195 int ret = -1;
1196 pgd_t *pgd;
1197 pmd_t *pmd;
1198
1199 pgd = pgd_offset(mm, address);
1200 pmd = pmd_alloc(pgd, address);
1201
1202 if (pmd) {
1203 pte_t * pte = pte_alloc(pmd, address);
1204 if (pte)
1205 ret = handle_pte_fault(mm, vma, address, write_access, pte);
1206 }
1207 return ret;
1208 }
==================== include/asm-i386/pgtable.h 311 312 ====================
311 /* to find an entry in a page-table-directory. */
312 #define pgd_index(address) ((address >> PGDIR_SHIFT) & (PTRS_PER_PGD-1))
==================== include/asm-i386/pgtable.h 316 316 ====================
316 #define pgd_offset(mm, address) ((mm)->pgd+pgd_index(address))
通过 pgd_offset 我们获取得到了一个pmd 页面的地址, 在 i386 中, 实际上就是 pte 的地址。
因为, 在 pgtable_2level.h 中, 将 pmd_alloc 定义为了 return (pmd_t *)pgd;
==================== include/asm-i386/pgalloc.h 120 141 ====================
[do_page_fault()>handle_mm_fault()>pte_alloc()]
120 extern inline pte_t * pte_alloc(pmd_t * pmd, unsigned long address)
121 {
122 address = (address >> PAGE_SHIFT) & (PTRS_PER_PTE - 1);
123
124 if (pmd_none(*pmd))
125 goto getnew;
126 if (pmd_bad(*pmd))
127 goto fix;
128 return (pte_t *)pmd_page(*pmd) + address;
129 getnew:
130 {
131 unsigned long page = (unsigned long) get_pte_fast();
132
133 if (!page)
134 return get_pte_slow(pmd, address);
135 set_pmd(pmd, __pmd(_PAGE_TABLE + __pa(page)));
136 return (pte_t *)page + address;
137 }
138 fix:
139 __handle_bad_pmd(pmd);
140 return NULL;
141 }
首先, 由于pmd 所指向的目录项一定是空的, 所以需要到 getnew 处分配一个页面表, 这里一个页面表正好就是一个物理页面。 内核对这个页面表分配的过程做了一些优化:
当需要释放一个物理页面的时候, 内核不会立即将他释放,而是把它放入到缓冲池中, 只有当缓冲池满的时候, 才会真正释放物理页面, 如果这个池子是空的, 就只能通过 get_pte_kernel_slow 分配了, 效率会比较低, 否则, 从这个池子中获取一个物理页面作为我们的页面表。
分配完一个物理页面之后, 我们就需要设置相应的页面表项了。
==================== mm/memory.c 1135 1187 ====================
[do_page_fault()>handle_mm_fault()>handle_pte_fault()]
1135 /*
1136 * These routines also need to handle stuff like marking pages dirty
1137 * and/or accessed for architectures that don't do it in hardware (most
1138 * RISC architectures). The early dirtying is also good on the i386.
1139 *
1140 * There is also a hook called "update_mmu_cache()" that architectures
1141 * with external mmu caches can use to update those (ie the Sparc or
1142 * PowerPC hashed page tables that act as extended TLBs).
1143 *
1144 * Note the "page_table_lock". It is to protect against kswapd removing
1145 * pages from under us. Note that kswapd only ever _removes_ pages, never
1146 * adds them. As such, once we have noticed that the page is not present,
66
1147 * we can drop the lock early.
1148 *
1149 * The adding of pages is protected by the MM semaphore (which we hold),
1150 * so we don't need to worry about a page being suddenly been added into
1151 * our VM.
1152 */
1153 static inline int handle_pte_fault(struct mm_struct *mm,
1154 struct vm_area_struct * vma, unsigned long address,
1155 int write_access, pte_t * pte)
1156 {
1157 pte_t entry;
1158
1159 /*
1160 * We need the page table lock to synchronize with kswapd
1161 * and the SMP-safe atomic PTE updates.
1162 */
1163 spin_lock(&mm->page_table_lock);
1164 entry = *pte;
1165 if (!pte_present(entry)) {
1166 /*
1167 * If it truly wasn't present, we know that kswapd
1168 * and the PTE updates will not touch it later. So
1169 * drop the lock.
1170 */
1171 spin_unlock(&mm->page_table_lock);
1172 if (pte_none(entry))
1173 return do_no_page(mm, vma, address, write_access, pte);
1174 return do_swap_page(mm, vma, address, pte, pte_to_swp_entry(entry), write_access);
1175 }
1176
1177 if (write_access) {
1178 if (!pte_write(entry))
1179 return do_wp_page(mm, vma, address, pte, entry);
1180
1181 entry = pte_mkdirty(entry);
1182 }
1183 entry = pte_mkyoung(entry);
1184 establish_pte(vma, address, pte, entry);
1185 spin_unlock(&mm->page_table_lock);
1186 return 1;
1187 }
此时, 由于我们的页面表项是空的, 所以一定是进入到 do_no_page 调用中去。
==================== mm/memory.c 1080 1098 ====================
[do_page_fault()>handle_mm_fault()>handle_pte_fault()>do_no_page()]
1080 /*
1081 * do_no_page() tries to create a new page mapping. It aggressively
1082 * tries to share with existing pages, but makes a separate copy if
1083 * the "write_access" parameter is true in order to avoid the next
1084 * page fault.
1085 *
1086 * As this is called only for pages that do not currently exist, we
1087 * do not need to flush old virtual caches or the TLB.
1088 *
1089 * This is called with the MM semaphore held.
1090 */
1091 static int do_no_page(struct mm_struct * mm, struct vm_area_struct * vma,
1092 unsigned long address, int write_access, pte_t *page_table)
1093 {
1094 struct page * new_page;
1095 pte_t entry;
1096
1097 if (!vma->vm_ops || !vma->vm_ops->nopage)
1098 return do_anonymous_page(mm, vma, page_table, write_access, address);
......
==================== mm/memory.c 1133 1133 ====================
1133 }
然后,在do_no_page 中根据 vma 结构中的 vm_ops 中记录的 no_page 函数指针, 进行相应处理, 但是这里, 没有与文件相关的操作, 因而不会有 no_page , 于是转而调用了 do_anonymous_page
==================== mm/memory.c 1058 1078 ====================
[do_page_fault()>handle_mm_fault()>handle_pte_fault()>do_no_page()>do_anonymous_page()]
1058 /* 1059 * This only needs the MM semaphore 1060 */
1061 static int do_anonymous_page(struct mm_struct * mm, struct vm_area_struct * vma, pte_t *page_table,
int write_access, unsigned long addr)
1062 {
1063 struct page *page = NULL;
1064 pte_t entry = pte_wrprotect(mk_pte(ZERO_PAGE(addr), vma->vm_page_prot));
1065 if (write_access) {
1066 page = alloc_page(GFP_HIGHUSER);
1067 if (!page)
1068 return -1;
1069 clear_user_highpage(page, addr);
1070 entry = pte_mkwrite(pte_mkdirty(mk_pte(page, vma->vm_page_prot)));
1071 mm->rss++;
1072 flush_page_to_ram(page);
1073 }
1074 set_pte(page_table, entry);
1075 /* No need to invalidate - it was non-present before */
1076 update_mmu_cache(vma, addr, entry);
1077 return 1; /* Minor fault */
1078 }
==================== include/asm-i386/pgtable.h 277 277 ====================
277 static inline pte_t pte_wrprotect(pte_t pte) { (pte).pte_low &= ~_PAGE_RW; return pte; }
==================== include/asm-i386/pgtable.h 271 271 ====================
271 static inline int pte_write(pte_t pte) { return (pte).pte_low & _PAGE_RW; }
==================== include/asm-i386/pgtable.h 91 96 ====================
91 /* 92 * ZERO_PAGE is a global shared page that is always zero: used 93 * for zero-mapped memory areas etc.. 94 */
95 extern unsigned long empty_zero_page[1024];
96 #define ZERO_PAGE(vaddr) (virt_to_page(empty_zero_page))
通过这个调用 实现对 pte 表项的设置工作。
需要注意的是, 如果是读操作, 只使用 pte_wrprotect 将页面设置为只读权限, 并一律映射到同一个物理页面 empty_zero_page, 这个页面内容全部都是 0.
只有是 写操作, 才会分配独立的物理内存空间, 并设置写权限等操作。
总结一下这个堆栈扩展的流程:
1. 检测是不是合法的堆栈扩展, 如果是的话, 就调用 expand_stack 完成 对堆栈区vm_area_struct 结构的改动。(虚拟空间)
2. 下面分配相应的物理页面。handle_mm_fault, 先分配pmd, 然后是pte 页面表 (pte_t * pte = pte_alloc(pmd, address)), 接下来 调用 handle_pte_fault 设置相应物理页面的属性。(其中, 利用中间 do_no_page 分配物理空间)