持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第17天,点击查看活动详情
系统零页
在匿名页面的缺页异常处理中,我们使用了系统零页,因为对于malloc()函数来说,分配的内存仅仅是进程地址空间中的虚拟内存。若这时用户程序需要读这个malloc()分配的虚拟内存,那么系统会返回全是0的数据,因此Linux内核不必为这种情况单独分配物理内存,而使用系统零页,即使用系统零页来映射malloc()分配的虚拟内存。当程序需要写入这个页面时就会触发一个缺页异常,于是缺页异常变成了写时复制的缺页异常。 总之,应用程序使用malloc()来分配虚拟内存,有以下几种情况。
- malloc()分配内存后,直接读。这种情况下,在Linux内核中会进入匿名页面的缺页中断,即do_anonymous_page()函数使用系统零页进行映射。这时映射的PTE属性是只读的。
- malloc()分配内存,先读后写。这种场景下,先读的操作会让Linux内核使用系统零页来建立页表的映射关系,这时PTE的属性是只读的。当应用程序需要往这个虚拟内存中写入内容时,又触发另外一个缺页异常,这个写时复制的缺页异常稍后会详细分析。
- malloc()分配内存后,直接写内存。在这个场景下,在Linux内核中会进入匿名页面的缺页中断,然后使用alloc_zeroed_user_highpage_movable()函数来分配一个新的页面,并且使用该页面来设置PTE,这时这个PTE的属性是可写的。
在ARM64架构中,empty_zero_page被定义成一个全局数组。
<arch/arm64/mm/mmu.c>
/*
* Empty_zero_page 是一个特殊映射的页面,用于内容全是0的页面和写时复制场景
*/
unsigned long empty_zero_page[PAGE_SIZE / sizeof(unsigned long)] __page_aligned_bss;
EXPORT_SYMBOL(empty_zero_page);
Linux内核提供了一些辅助的宏来使用这个系统零页。
extern unsigned long empty_zero_page[PAGE_SIZE / sizeof(unsigned long)];
#define ZERO_PAGE(vaddr) phys_to_page(__pa_symbol(empty_zero_page))
#define my_zero_pfn(addr) page_to_pfn(ZERO_PAGE(addr))
文件映射缺页中断
在没有找到对应的PTE并且是文件映射的情况下,会调用do_fault()函数。
<mm/memory.c>
static vm_fault_t do_fault(struct vm_fault *vmf)
do_fault()函数中的主要操作如下。 在第3529~3555行中,处理VMA中的vm_ops()方法集中没有实现fault()的情况,有些内核模块或者驱动的mmap()函数并没有实现fault()回调函数。在这种场景下,我们需要考虑一种特殊的情况。可能CPU0调用ptep_modify_prot_start()和ptep_modify_prot_commit()函数对PTE进行修改。这是一个“读-修改-回写”(read-modify-write)机制,这时硬件可能会异步地对这个PTE进行修改。“读-修改-回写”不能防止硬件对PTE进行修改,但是能防止某些更新导致PTE值丢失。“读-修改-回写”需要设置一个mm->page_table_lock的自旋锁进行保护,其他内核路径不会对其进行干扰。若这个时候CPU1访问该页,那么会导致缺页异常,因此我们在这里需要调用pte_offset_map_lock()来重新获取PTE,然后检查PTE是否有效,保证这不是一个临时的PTE。所谓的临时PTE就是在读-修改-回写的过程中,PTE的内容被清空。注意,pte_offset_map_lock()函数需要申请mm->page_table_lock的自旋锁。若最终还发现该PTE是一个无效的PTE,那么只能返回VM_FAULT_SIGBUS信号;若它是一个有效的PTE,那么返回VM_FAULT_NOPAGE,表示这次缺页异常不需要返回一个新的页面。