VMA操作
应用进程申请内存空间通常使用malloc或者mmap来完成。我们知道在ARM32架构中0~3G的地址空间是分配给用户进程地址空间的。为了能在系统中(宏观上)同时运行多个进程,互不干扰操作系统想了一个办法,就是基于某一时刻只能运行一个进程的客观事实,系统为每个进程都准备了一套0~3G的虚拟地址空间,他们可以以各自特定的规则映射到物理空间。此刻哪儿个进程在运行就使用哪儿个进程的地址空间及映射关系,再加上写时分配机制的加成。这便有效的解决了进程在宏观上同时运行而绝不会互相干扰的问题。
综上所述,用户进程要申请到内存空间,首先要申请到一块合适的虚拟地址空间供物理地址映射。内核中使用VMA来表示这块虚拟地址空间单元。所以每个进程核心数据结构中都有一个管理VMA的数据结构struct vm_area_struct。
struct vm_area_struct数据结构
- vm_start和vm_end:指定VMA进程地址空间的起始结束地址
- vm_next和vm_prev:VMA连成一个链表
- vm_rb:VMA作为一个节点加入红黑树,进程的struct mm_struct都有一颗红黑树mm->mm_rb管理这些节点
- vm:指向该VMA所属进程struct mm_struct
- vm_page_prot:VMA的访问权限
- vm_flags:描述VMA的标志
- anon_vma_chain和anon_vma:用于管理RMAP反向映射
- vm_ops:指向方法的集合,操作VMA
- vm_pgoff:指定文件映射的偏移量,这个变量的单位是页。对于匿名页来说它的值是0或者vm_addr/PAGE_SIZE
- vm_file:指向file的实例,描述一个被映射的文件。
find_vma函数
查找满足如下2个条件之一的VMA:
- addr在VMA空间范围内的,vma->vm_start<=addr<=vma->vm_end;
- 距离addr最近并且vma->vm_end大于addr的VMA
- 78行:在cache中看能否找到对应地址的vma,能找到直接返回
- 84~94行:在红黑树中查找合适的VMA
- 96行:更新cache,最后返回找到的vma
基于find_vma函数实现的还有find_vma_intersection用于查找与start_addr、end_addr重合的VMA;find_vma_prev返回查找到的VMA的前继节点
insert_vm_struct函数
- 148行:find_vma_links查找插入位置,实现如下↓
- 155行:vma_link会调用__vma_link函数把VMA插入红黑树和链表,这个函数很简单不做说明。
find_vma_links函数
- 165行:__rb_link:指向红黑树的根节点地址
- 167~180行:在红黑树中查找合适的位置。vma_tmp->vm_end <= addr的不符合寻找条件,继续往右子树探测,如果vma_tmp->vm_end > addr表示符合了基本条件,但是如果vma_tmp->vm_start
- 最后*rb_link指向待插入位置,*rb_parent表示待插入节点的父节点
malloc函数
- 278行:基于用户空间的内存空间布局来理解该函数,min_brk表示堆的起始界限也就是数据段的结束位置。brk表示加上申请空间长度后临界线应该处在的位置,因为堆是从底部一直往上增长的,所以如果brk比堆起始位置还小肯定是不合法的。
- 284~285行:堆的临界线页对齐处理,也就是侧面说明malloc申请的空间也会是页对齐的。
- 290~294行:堆的新位置比旧位置还小那就是释放空间,调用do_munmap函数来释放空间。
- 297行:以老边界地址找是否存在重合的vma,如果存在则不需要查找了直接返回。
- 301行:do_brk函数申请分配内存,大小与页对齐
do_brk函数
- 336行:get_unmapped_area判读虚拟地址空间是否有足够的空间,返回一段没有被映射过的虚拟地址空间的起始地址,其中MAP_FIXED表示使用指定具体的虚拟地址对应空间
- 337行:mlock特性长度检测
- 347行:判断有没有可能合并附近的VMA,可以的或直接就可以跳转到out,否则只能新建VMA
- 354~366行:创建VMA,并对齐初始化
- 367行:将初始化好的VMA添加到红黑树和链表中vma_link这个函数前面介绍过。
回到malloc的系统调用brk函数中
- 306行:flags是否置位VM_LOCKED,如果置位则调用mm_populate马上分配物理内存并建立映射关系
mm_populate->__mm_populate函数
- 399行:先查找vma,没找到就直接返回
- 411行:__mlock_vma_pages_range为VMA分配物理内存,实现如下↓
__mlock_vma_pages_range函数
- __get_user_pages函数为进程地址空间申请物理内存并建立映射关系
__get_user_pages函数
- 参数说明:tsk进程核心数据结构;mm进程内存管理核心数据结构;start进程地址空间VMA的起始地址;nr_pages需要分配的pages;gup_flags分配掩码;pages物理页面的二级指针;vmas:进程地址空间VMA;nonblocking:IO操作是否阻塞
- find_extend_vma会调用find_vma查找VMA。如果vma_start大于start的地址会尝试扩增VMA的范围。如果前期要查找的VMA刚好在gate_vma中就使用gate_vma页面。
- cond_resched();当前进程是否需要被调度,内核经常在循环中增加这个判断优化系统延迟。
- follow_page_mask函数查看VMA中的物理页面是否已经分配了内存。函数实现细节如下↓
- 如果fllow_page_mask分配内存失败,调用faultin_page->handle_mm_fault人为触发缺页中断
follow_page_mask函数
- 648行:通过mm和address得到当前进程页表对应的PGD目录项
- 651行:通过PGD和address得到当前进程页表对应的PUD目录项
- 658行:通过PUD和address得到当前进程页表对应的PMD目录项
- 670行:调用follow_page_pte检查PTE页面,其实现如下↓
follow_page_pte函数
- 684行:检查pmd合法性
- 686行:拿到对应的pte页面
- 688~700行:pte不在内存中的处理。没有定义FOLL_MIGRATION的错误返回;pte_to_swp_entry函数合并交换页面到主内存;migration_entry_wait等待合并交换页面完成,等待完毕重试。
- 703行:如果pte是只读页面,flag却含有可写权限,也报错返回。
- 708行:vm_normal_page函数根据pte返回normal mapping的page数据结构,实现细节如下↓
- 716~722行:对一些指定标志的page做一些相应的加工处理。对于FOLL_GET的页面会增加page的_count计数;对于FOLL_TOUCH且标记是可写的且非脏的页mark_page_accessed标记为活跃
- 724~736行:对VM_LOCKED的page做一些加工处理,最后返回这个page
vm_normal_page函数
- 755~765行:对于一些特殊映射的页面不返回page数据结构;对于定义了find_special_page函数指针,执行该指针继续检查;对于第0页和特殊映射的页面返回空;
- 768~783行:对于没有定义与前面类似HAVE_PTE_SPECIAL的情况。775行检测remap_pfn_range的情况。因为其pgoff通常指向第一个pfn;对于写时复制的页面正常返回page;对于0页不返回page结构;
综上:整个malloc的底层处理流程如下: