1. 内核信赖自己,但是对于用户态的内存请求,内核会做必要的地址检查,然后先给进程分配地址空间(线性地址),真正的物理内存分配推迟到必要的时候才进行。
2. 内核使用mm_struct 来描述用户的地址空间信息,所有的mm_struct 是用双向链表连接起来的,相邻的mm_struct 在mmlist域中表示,链表中的第一个元素是init_mm中mmlist指向的进程0的mm_struct。
3. mm_users 域用来存储共享内存描述符的线程数, mm_count域是mm_struct 的引用计数,每次mm_count减小时,内核都检查其是否等于0,如果是的话,就释放该描述符。
4. mm_alloc用来分配新的进程空间描述符,通过slab cache来分配,并且将mm_users和mm_count 都初始化为1.
5. mmput将mm_users减小1,如果 等于0, 就释放LDT,内存区域描述符和相关联的页表,并且调用mmdrop,mmdrop将mm_count 减1,如果等于0, 就释放进程空间描述符。
6. 内核线程和普通进程不同,它不使用进程空间。对于TASK_SIZE以上的内存,所有的进程都是相同的,所以内核线程使用在它之前的进程页表。
7. 对于普通进程,mm 和active_mm应该是相同的,但是对于内核线程,mm 为NULL, active_mm指向之前进程的active_mm所指向的进程空间描述符。
8. 当内核更新TASK_SIZE之上的内存映射时(vmalloc,vfree),内核只更新主页表,其他进程的页表信息会通过page_fault处理函数来进行更新。
9. vm_area_struct 用来表示一个内存区域,范围是[vm_start, vm_end),当添加或移除一个内存区域时,内核尝试将权限相同的,地址连续的区域进行合并。起始地址和大小均为页对齐。
10. 一个进程的所有内存区域通过一个简单链表按照地址的大小连接起来,vm_area_struct 中的vm_next域指向下一个内存区域,mm_struct 的mmap 指向该进程的第一个内存区域,map_count存储进程拥有的内存区域个数。
11. linux 2.6中用了红黑树来快速查找某个内存区域,由mm_rb指向根节点。
12. vm_area_struct 中的vm_flags用来表示该内存区域的一些属性,包括访问权限,共享权限以及增长方式等信息。
13 初始的页表flags存储在vm_area_struct 中的vm_page_port字段中。
14. find_vma接收mm_struct和addr两个参数,用来找到第一个vm_end大于addr的vm_area_struct的地址,没有找到的话,返回NULL。
15. mm_struct中的mmap_cache指向该进程上次访问的vm_area_struct , find_vma会首先检查该内存区域是否包含addr,如果是,立即返回, 否则的话,通过红黑树来进行查找。
16. find_vma_intersection用来查找覆盖一段地址的内存区域。
17. get_unmapped_area用来查找一个进程空间中指定长度区间的线性地址段,addr不为空表示从addr开始查找,接下来根据是否是文件映射或者是匿名映射来分别调用相应的get_unmapped_area方法。
18. insert_vm_struct将一个vm_area_struct 插入到链表和红黑树中,如果是匿名映射的话,还要与anon_vma进行关联。
19. mmap函数为进程创建并且初始化一个新的内存区域,但是有可能和之前的区域合并。
20. get_user_pages 函数会遍历一个指定的用户内存区间,对于每一个页,调用follow_page来检查是否有相应的物理页面,如果没有的话,会调用handle_mm_fault来分配物理页面并且建立页表。
21. do_page_fault函数是x86平台的页故障异常处理函数,它接收pt_regs 指针和error_code。
22 error_code由3个bit组成:
(1)bit 0如果置位,表示权限非法,否则,表示页面不存在。
(2)bit 1如果置位,表示是写异常,否则,为读或执行异常。
(3)bit 2如果置位,表示异常发生在用户态,否则,发生在内核态。
23. 当页故障发生时,发生故障的页的线性地址放入了CR2寄存器。
24. in_atomic函数检查内核是否在关键区内,如果是以下情况,则返回真:
(1) 内核正在执行中断处理函数或者延迟函数。
(2)内核正在执行关键区的代码,并且已经关闭内核抢占。
25. 满足in_atomic 或者是内核线程时, 缺页会导致内核错误,产生oops。
26. 为了读取用户进程的内存信息,需要获取mm->mmap_sem,为了防止造成死锁,使用down_read_trylock,如果无法获取锁,该信号量可能是被别的系统调用占用了,这种情况的话,就等待信号量被释放,否则,就是内核bug,产生oops。
27. 对于异常的地址不在进程的vma中,还有一种额外的情况是由于push操作导致的栈增长,vm_end不变,vm_start 减小, 异常的地址应该只比sp小一点,否则的话,也是bug。确认是栈扩展的话,调用expand_stack, 改变vm_start的值,赋值为 address & PAGE_MASK。
28. 如果是用户态的进程不包含异常地址,进入bad_area函数,向用户进程发送SIGSEGV信号。
29. 在访问权限没有问题的情况下,接下来调用handle_mm_fault函数来分配物理页面。返回VM_FAULT_MINOR表示页故障不会阻塞当前进程,VM_FAULT_MAJOR表示会导致当前进程阻塞。
30. handle_mm_fault 会先检查相应的页目录,页表是否存在,不存在的话,就分配。handle_pte_fault用于处理最后的页表项,如果页表项是空,就是demand paging;如果是只读,就是copy on write (COW)。
31. (1) 如果pte_none返回1,页表项全部为0, 表示该页从来没被访问或者为一个线性文件映射,如果vm_ops不空,可能是文件映射,否则为匿名映射。
(2) 如果页属于一个非线性文件映射,P清零,D位置1。
(3) 如果页被访问过,但是目前被置换到磁盘,P, D位都清零。
32. do_anoymous_page 在处理读请求时,将pte设置为系统预先分配的0页内存,权限为只读, 后续的写操作会导致COW; 如果为写请求,则需要分配物理页。
33. COW技术:page结构体的_count域表示共享该页的进程数,当进程释放page或者发生COW时,_count减1, 当_count为-1时,真正释放该页。
34. 当handle_pte_fault发现P为置1, 并且页表项为只读,请求为写,此时调用do_wp_page,该函数来读取_count等信息来决定是否执行COW。
35. 如果发生在内核空间,并且地址大于TASK_SIZE, 会进入vmalloc_fault, 将主内核页表复制到用户进程的页表中。
36. linux线程是通过clone函数,传入CLONE_VM参数来创建的,线程之间共享地址空间。
37. copy_mm用来创建新进程的地址空间,如果CLONE_VM设置了,就直接使用父进程的mm, 否则,需要调用dup_mm来复制父进程的mm,并且修改一些字段。dup_mm 会调用dup_mmap来复制内存区域和页表。
38. 进程退出时,调用exit_mm来释放进程空间, 首先会将mm_count加1, mmput最后会释放LDT, vma和页表。mm本身不会被销毁,而是在schedule中的finish_task_switch中的mmdrop来进行销毁。
39. mm->brk用来表示进程的堆顶,brk是一个系统调用,C库中的堆操作都是通过brk和mmap来实现的。如果是缩减堆,则会调用do_munmap; 扩展堆的话,会调用do_brk, 该函数相当于一个只处理匿名内存区域的do_mmap,而不考虑文件映射。