0%

《趣谈linux操作系统》小结(二十) -内存映射

用户态内存映射

mmap

1
2
3
4
struct mm_struct { 
struct vm_area_struct *mmap; /* list of VMAs */
......}

在进程的mm_struct有一个mmap的成员, 是内存映射的一个重要结构。内存映射不仅仅是物理内存和虚拟内存之间的映射,还包括将文件中的内容映射到虚拟内存空间。

如果申请一大块内存,就要用 mmap。对于堆的申请来讲,mmap 是映射内存空间到物理内存。另外,如果一个进程想映射一个文件到自己的虚拟内存空间,也要通过 mmap 系统调用。这个时候 mmap 是映射内存空间到物理内存再到文件。

mmap系统调用的调用链是 vm_mmap_pgoff->do_mmap_pgoff->do_mmap。这里面主要干了两件事情:

  • 调用 get_unmapped_area 找到一个没有映射的区域;
  • 调用 mmap_region 映射这个区域。

通过mmap,我们主要就是建立了空间的访问逻辑所需要的一些结构,还没有涉及到具体的物理内存映射。

用户态缺页

一旦开始访问虚拟内存的某个地址,如果我们发现,并没有对应的物理页,那就触发缺页中断,调用 do_page_fault。
在 __do_page_fault 里面,先要判断缺页中断是否发生在内核, 地址是否大于TASK_SIZE_MAX。如果发生在内核则调用 vmalloc_fault,在内核里面,vmalloc 区域需要内核页表映射到物理页。在用户空间里面,找到你访问的那个地址所在的区域 vm_area_struct,然后调用 handle_mm_fault 来映射这个区域。

handle_mm_fault函数里面会设置进程对应的页表的几级目录信息。当进程在CPU上运行时,会在上下文切换时调用load_new_mm_cr3,保存pgd地址到cr3寄存器,如果 CPU 的指令要访问进程的虚拟内存,它就会自动从 cr3 里面得到 pgd 在物理内存的地址,然后根据里面的页表解析虚拟内存的地址为物理内存,从而访问真正的物理内存上的数据。

用户进程在运行的过程中,访问虚拟内存中的数据,会被 cr3 里面指向的页表转换为物理地址后,才在物理内存中访问数据,这个过程都是在用户态运行的,地址转换的过程无需进入内核态。

只有访问虚拟内存的时候,发现没有映射到物理内存,页表也没有创建过,才触发缺页异常。进入内核调用 do_page_fault,一直调用到 __handle_mm_fault。原来没有创建过页表,那只好补上这一课。于是,__handle_mm_fault 调用 pud_alloc 和 pmd_alloc,来创建相应的页目录项,最后调用 handle_pte_fault 来创建页表项。

大体上就是先找一遍页表,找不到就创建,直到把物理地址的映射写到页表里面,再根据基地址加偏移的方式获取对应的物理地址,然后访问。 这边如果是映射的文件, 那就是对应找到文件在内存中的缓存页,读取里面的内容。 如果页被换出了,那就把它换入 再定位。 具体的流程很长,但是逻辑还是清晰的, 不同类型的页作不同的动作,最终都会获取到一个内存的物理地址。

从这块逻辑看,其实用的就是缓存的技术,懒加载,定时换入换出等逻辑。先找没有找再去创建。同样的,为了提高映射速度,我们引入了 TLB(Translation Lookaside Buffer),我们经常称为快表,专门用来做地址映射的硬件设备。它不在内存中,可存储的数据比较少,但是比内存要快。所以,我们可以想象,TLB 就是页表的 Cache,其中存储了当前最可能被访问到的页表项,其内容是部分页表项的一个副本。有了 TLB 之后,地址映射的过程就像图中画的。我们先查块表,块表中有映射关系,然后直接转换为物理地址。如果在 TLB 查不到映射关系时,才会到内存中查询页表。

我编写了一个mmap系统调用的文件共享内存,代码在github上,有兴趣的同学可以看看。

行动,才不会被动!

欢迎关注个人公众号 微信 -> 搜索 -> fishmwei,沟通交流。