Linux操作系统学习笔记(十)内存管理之内存映射【转】
转自:https://ty-chen.github.io/linux-kernel-mmap/
一. 前言
??本文为内存部分最后一篇,介绍内存映射。内存映射不仅是物理内存和虚拟内存间的映射,也包括将文件中的内容映射到虚拟内存空间。这个时候,访问内存空间就能够访问到文件里面的数据。而仅有物理内存和虚拟内存的映射,是一种特殊情况。本文首先分析用户态在堆中申请小块内存的brk
和申请大块内存的mmap
,之后会分析内核态的内存映射机制vmalloc
,kmap_atomic
,swapper_pg_dir
以及内核态缺页异常。
handle_pte_fault()
创建页表项。

1
|
static vm_fault_t __handle_mm_fault(struct vm_area_struct *vma,
|
??handle_pte_fault()
处理以下三种情况
- 页表项从未出现过,即新映射页表项
- 匿名页映射,则映射到物理内存页,调用
do_anonymous_page()
- 文件映射,调用
do_fault()
- 匿名页映射,则映射到物理内存页,调用
- 页表项曾出现过,则为从物理内存换出的页,调用
do_swap_page()
换回来
1
|
/*
|
3.1 匿名页映射
??对于匿名页映射,流程如下
- 调用
pte_alloc()
分配页表项 - 通过
alloc_zeroed_user_highpage_movable()
分配一个页,该函数会调用alloc_pages_vma()
,并最终调用__alloc_pages_nodemask()
。该函数是伙伴系统的核心函数,用于分配物理页面,在上文中已经详细分析过了。 - 调用
mk_pte()
将新分配的页表项指向分配的页 - 调用
set_pte_at()
将页表项加入该页
1
|
/*
|
3.2 文件映射
??映射文件do_fault()
函数调用了fault
函数,该函数实际会根据不同的文件系统调用不同的函数,如ext4
文件系统中vm_ops
指向ext4_file_vm_ops
,实际调用ext4_filemap_fault()
函数,该函数会调用filemap_fault()
完成实际的文件映射操作。
1
|
static vm_fault_t do_fault(struct vm_fault *vmf)
|
file_map_fault()
主要逻辑为
- 调用
find_ge_page()
找到映射文件vm_file
对应的物理内存缓存页面- 如果找到了,则调用
do_async_mmap_readahead()
,预读一些数据到内存里面 - 否则调用
pagecache_get_page()
分配一个缓存页,将该页加入LRU表中,并在address_space
中调用
- 如果找到了,则调用
1
|
vm_fault_t filemap_fault(struct vm_fault *vmf)
|
3.3 页交换
??前文提到了我们会通过主动回收或者被动回收的方式将物理内存已映射的页面回收至硬盘中,当数据再次访问时,我们又需要通过do_swap_page()
将其从硬盘中读回来。do_swap_page()
函数逻辑流程如下:查找 swap
文件有没有缓存页。如果没有,就调用 swapin_readahead()
将 swap
文件读到内存中来形成内存页,并通过 mk_pte()
生成页表项。set_pte_at
将页表项插入页表,swap_free
将 swap
文件清理。因为重新加载回内存了,不再需要 swap
文件了。
1
|
vm_fault_t do_swap_page(struct vm_fault *vmf)
|
??通过以上步骤,用户态的缺页异常就处理完毕了。物理内存中有了页面,页表也建立好了映射。接下来,用户程序在虚拟内存空间里面可以通过虚拟地址顺利经过页表映射的访问物理页面上的数据了。页表一般都很大,只能存放在内存中。操作系统每次访问内存都要折腾两步,先通过查询页表得到物理地址,然后访问该物理地址读取指令、数据。为了加快映射速度,我们引入了 TLB(Translation Lookaside Buffer),我们经常称为快表,专门用来做地址映射的硬件设备。它不在内存中,可存储的数据比较少,但是比内存要快。所以我们可以想象,TLB 就是页表的 Cache,其中存储了当前最可能被访问到的页表项,其内容是部分页表项的一个副本。有了 TLB 之后,我们先查块表,块表中有映射关系,然后直接转换为物理地址。如果在 TLB 查不到映射关系时,才会到内存中查询页表。
四. 内核态内存映射及缺页异常
??和用户态使用malloc()
类似,内核态也有相应的内存映射函数:vmalloc()
可用于分配不连续物理页(使用伙伴系统),kmem_cache_alloc()
和kmem_cache_create()
使用slub分配器分配小块内存,而kmalloc()
类似于malloc()
,在分配大内存的时候会使用伙伴系统,分配小内存则使用slub分配器。分配内存后会转换为虚拟地址,保存在内核页表中进行映射,有需要时直接访问。由于vmalloc()
会带来虚拟连续页和物理不连续页的映射,因此一般速度较慢,使用较少,相比而言kmalloc()
使用的更为频繁。而kmem_cache_alloc()
和kmem_cache_create()
会分配更为精准的小内存块用于特定任务,因此也比较常用。
??相对于用户态,内核态还有一种特殊的映射:临时映射。内核态高端内存地区为了节省空间会选择临时映射,采用kmap_atomic()
实现。如果是 32 位有高端地址的,就需要调用 set_pte
通过内核页表进行临时映射;如果是 64 位没有高端地址的,就调用 page_address
,里面会调用 lowmem_page_address
。其实低端内存的映射,会直接使用 __va
进行临时映射。
1
|
void *kmap_atomic_prot(struct page *page, pgprot_t prot)
|
??kmap_atomic ()
发现没有页表的时候会直接创建页表进行映射。而 vmalloc ()
只分配了内核的虚拟地址。所以访问它的时候,会产生缺页异常。内核态的缺页异常还是会调用 do_page_fault()
,最终进入vmalloc_fault()
。在这里会实现内核页表项的关联操作,从而完成分配,整体流程和用户态相似。
1
|
static noinline int vmalloc_fault(unsigned long address)
|
五. 总结
??至此,我们分析了内存物理地址和虚拟地址的映射关系,结合前文页的分配和管理,内存部分的主要功能就算是大致分析清楚了,最后引用极客时间中的一幅图作为总结,算是全部知识点的汇总。

代码资料
[1] brk
[2] mmap
[3] page_fault
参考文献
[1] wiki
[2] elixir.bootlin.com/linux
[3] woboq
[4] Linux-insides
[5] 深入理解Linux内核
[6] Linux内核设计的艺术
[7] 极客时间 趣谈Linux操作系统
坚持原创,坚持分享,谢谢鼓励和支持- 本文链接: https://ty-chen.github.io/linux-kernel-mmap/
- 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议。转载请注明出处!