本专栏文章将有70篇左右,欢迎+关注,查看后续文章。
4.9 堆管理
malloc函数:分配堆。
问:malloc如何实现?
答:由brk()系统调用实现,而brk()基于匿名映射实现。
struct mm_struct {
unsigned long start_brk; //堆的开始地址
unsigned long brk; //堆的结束地址
}
brk系统调用:只需一个参数,即堆结束地址。
brk最小分配单位:一页(按页对齐)
find_vma_intersection():
寻找插入VMA位置,会检查扩大堆后是否和现存VMA重叠,是否可合并。
收缩堆时:调用do_munmap。
brk系统调用:最终建立一个新的匿名映射,生成一个新的VMA。
4.10 缺页异常处理
缺页异常触发情景:
1. malloc/mmap会新建一个vma,但没有分配和映射到物理页。
等到随后的写操作,触发缺页异常,异常处理中再分配页,并写数据到页。
2. fork刚创建子进程时,父子进程共享物理页,并设页属性为只读。
当父子进程任一方写物理页时,触发缺页异常,执行写时复制COW。即分配新页,并写数据。
缺页异常处理大致流程:
1. 触发缺页异常。
访问虚拟地址,找不到对应物理页(PTE中的Present位为0),则触发缺页异常。
2. 保存当前进程上下文。
3. 判断缺页原因。
3.1 访问无效虚拟地址。
3.2 页也被换出到交换空间。
3.3 文件页映射,未读入文件到内存。
4. 分别处理。
4.1 终止进程或返回错误。
4.2 从交换空间中读取数据到内存。
4.3 从磁盘读取文件数据到内存。
5. 更新页表。
设置Present位。还可能设置Dirty位、Accessed位等。
6. 更新TLB。
以缓存新页表项。
7. 恢复进程执行。
不同体系架构实现的缺页异常处理不同。
ARM为例:
缺页异常入口:entry.S,然后调用do_page_fault ():
int do_page_fault(addr, unsigned int fsr, struct pt_regs *regs)内容为:
1. 参数检查
地址是否合法,内核/用户空间的缺页异常,访问权限,是否中断上下文。
2. 获取task_struct,mm_struct。
3. 通过find_vma找到包含引起缺页的虚拟地址addr的VMA。
4. 根据VMA的类型,调用不同的处理函数。
文件页:调用do_fault,最终调用vma->vm_ops->fault();
匿名页:调用do_anonymous_page。
页面被交换出去:调用do_swap_page。
5. 处理缺页后,并将页面映射到物理内存中,修改页表。
如果缺页处理失败。如物理页不足,则调用do_sigbus向进程发送SIGBUS信号。
4.11 用户空间缺页异常
do_page_fault () -> handle_mm_fault () -> handle_pte_fault () :
handle_pte_fault内容:
如果页表项显示对应页不在物理内存中(即!pte_present),有4种情况:
1. 没有对应页表项(page_none())。则:
按需分配(匿名映射)
按需调页(文件映射)
2. 有页表项(!page_none())。
说明页已换出,则调用do_swap_page,从交换空间读数据到到内存页。
3. 如果是非线性映射(pte_file()为真)
调用do_nonlinear_fault。
pte_file()作用:判断是否为非线性映射。
4. 如果想写,而页表项不支持写该页,则:
调用do_wp_page,进行COW创建副本页,写数据到副本。
场景场景:fork子进程。
上述四种情况,分节详细说明。
4.11.1 按需调页
按需分配页调用:do_linear_fault -> _do_fault,最后调用vm->vm_ops->fault();
(大多数文件映射:vm->vm_ops->fault = filemap_fault函数)
以映射ext4文件为例。映射时调用ext4_file_mmap函数。
struct file_operations ext4_file_operations = {
.mmap = ext4_file_mmap,
}
int ext4_file_mmap(struct file *file, struct vm_area_struct *vma)
{
vma->vm_ops = &ext4_file_vm_ops;
return 0;
}
struct vm_operations_struct ext4_file_vm_ops = {
.fault = filemap_fault,
};
而filemap_fault会调用:
struct address_space mapping->a_ops->readpage(filp, page);
即使用address_space从磁盘文件读数据到内存。
指定一个VMA,如何读取数据到页(按需调页)?
1. struct vm_area_struct -> struct file vm_file
2. struct file -> struct address_space *f_mapping
3. struct address_space -> struct address_space_operations *a_ops
4. a_ops->readpage(struct file *, struct page *)
readpage工作有:
分配页,并读入数据后。
更新进程页表。
再将页加入到逆向映射数据结构中。
写数据需要区分共享映射和私有映射。
若为私有映射,则创建副本页,并写副本。
4.11.2 匿名页
匿名页:没有后备存储器的页,即没有映射文件。如堆栈。
匿名页的缺页异常处理:
handle_pte_fault -> do_anonymous_page:
匿名页除了不需要文件页的数据读入,其他大致相同。
do_anonymous_page工作内容:
1. 在高端内存域中分配一个新页。
2. 将页加入进程页表。
3. 更新CPU cache和MMU(TLB)。
拓展
创建匿名映射:
mmap2(NULL, 0x100000, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0)
mmap只完成了初始化工作,并没有分配内存页,也没有创建PTE。
当真正访问虚拟地址时,触发缺页异常,执行handle_pte_fault,再调用do_anonymous_page,完成上述工作。
4.11.3 写时复制
当以写访问进行缺页异常时,而PTE是只读,此时进行写时复制(COW)
即分配副本内存页,写数据到副页。
处理流程:
handle_pte_fault -> do_wp_page
4.11.4 非线性映射
非线性映射的缺页异常处理流程:
handle_pte_fault -> do_nonlinear_fault
4.12 内核缺页异常
4.11讲的是用户空间的缺页异常。而本节讲内核缺页异常。
触发场景:
1. 内核访问错误地址(在不稳定的内核版本中容易出现)。
2. 用户空间给内核传递了错误参数,导致访问无效地址。
3. 访问vmalloc分配地址,触发缺页异常,用于分配页。
#1 #2 是真正错误,而#3是正常的,应缺页异常处理。
在内核镜像文件中有一个异常表:
位于__start___ex_table和__stop___ex_table之间。
表中包含很多表项,每个表项的内容:
struct exception_table_entry
{
unsigned long insn; //缺页异常的内核地址,即(regs)->ARM_pc
unsigned long fixup; //异常处理代码的地址。
};
问:内核如何处理缺页异常?
答:__do_kernel_fault
-> fixup_exception(struct pt_regs *regs)
int fixup_exception(struct pt_regs *regs)
{
struct exception_table_entry *fixup;
fixup = search_exception_tables((regs)->ARM_pc);
if (fixup)
regs->ARM_pc = fixup->fixup;
}
如果没找到内核异常的修正函数fixup,调用do_page_fault,进入oops(可能重启)
4.13 内核和用户空间传递数据
系统调用需要在内核和用户空间中传递数据。
为什么能直接传递数据的地址?
1. 用户空间不能访问内核地址。
2. 用户空间虚拟地址可能没有关联到物理页。
所以不能传递地址,只能传递真实数据。
标准函数
long copy_from_user(void *to, const void __user * from, unsigned long n)
long copy_to_user(void __user *to, const void *from, unsigned long n)
__user作用:提醒编译器检查指针。
get_user(x, ptr)
put_user(x, ptr)
内核中处理用户空间字符串的标准函数:
clear_user(to, n):用0填充用户空间to处。
strlen_user(s):计算用户空间s的strlen。