万字解析Linux内核之mmu-gather操作
1开场白
环境:
处理器架构:arm64
内核源码:linux-5.10.50
ubuntu版本:20.04.1
代码阅读工具:vim+ctags+cscope
本文讲解Linux内核虚拟内存管理中的mmu_gather操作,看看它是如何保证刷tlb和释放物理页的顺序的,又是如何将更多的页面聚集起来统一释放的。
通常在进程退出或者执行munmap的时候,内核会解除相关虚拟内存区域的页表映射,刷/无效tlb,并释放/回收相关的物理页面,这一过程的正确顺序如下:
1)解除页表映射
2)刷相关TLB
3)释放物理页面
在刷相关虚拟内存区域tlb之前,绝对不能先释放物理页面,否则可能导致不正确的结果,而mmu-gather(mmu 积聚)的作用就是保证这种顺序,并将需要释放的相关的物理页面聚集起来统一释放。
2.源代码解读
2.1 重要数据结构体
首先我们先介绍一下,与mmu-gather相关的一些重要结构体,对于理解源码很有帮助。
相关的主要数据结构有三个:
结构mmu_gather
结构mmu_table_batch
结构mmu_gather_batch
1)mmu_gather
来表示一次mmu积聚操作,在每次解除相关虚拟内存区域时使用。
其中, mm 表示操作哪个进程的虚拟内存;批处理用于积聚进程各级页目录的物理页;start和end 表示操作的起始和结束虚拟地址,这两个地址在处理过程中会被相应的赋值;fullmm 表示是否操作整个用户地址空间;freed_tables 表示我们已经释放了相关的页目录;cleared_ptes/pmds/puds/p4ds 表示我们在哪个级别上清除了表项;vma_exec 表示操作的是否为可执行的 VMA; vma_huge 表示操作的是否为hugetlb的VMA;batch_count 表示积聚了多少个“批次”,后面会讲到 ;active、local和__pages 和多批次释放物理页面相关; active表示当前处理的批次,local表示“本地”批次,__pages表示“本地”批次积聚的物理页面。
这里需要说明一点就是,mmu积聚操作会涉及到local批次和多批次操作,local批次操作的物理页面相关的struct page数组内嵌到mmu_gather结构的__pages中,且我们发现这个数组大小为8,也就是local批次最大积聚8 * 4k = 32k的内存大小,这因为mmu_gather结构通常在内核栈中分配,不能占用太多的内核栈空间,而多批次由于动态分配批次积聚结构所以每个批次能积聚更多的页面。
2)mmu_table_batch
用于积聚进程使用的各级页目录的物理页,在释放进程相关的页目录的物理页时使用(文章中称为页表批次的积聚结构 )。
next 用于多批次积聚物理页时,连接下一个积聚批次结构 ;
nr 表示本次批次的积聚数组的页面个数;
max 表示本次批次的积聚数组最大的页面个数;
pages 表示本次批次积聚结构的页面数组。
2.2 总体调用
通常mmu-gather操作由一下几部分函数组成:
**tlb_gather_mmu **
unmap_vmas
**free_pgtables **
tlb_finish_mmu
其中tlb_gather_mmu表示mmu-gather初始化,也就是struct mmu_gather的初始化;
unmap_vmas 表示解除相关虚拟内存区域的页表映射;
free_pgtables 表示释放页表操作 ;
tlb_finish_mmu 表示进行刷tlb和释放物理页操作。
【文章福利】小编推荐自己的Linux内核技术交流群:【891587639】整理了一些个人觉得比较好的学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!!!(含视频教程、电子书、实战项目及代码)


2.3 tlb_gather_mmu
这个函数主要是初始化从进程内核栈中传递过来的mmu_gather结构。
下面给出tlb_gather_mmu时的图解:

2.4 unmap_vmas
这个函数用于解除相关进程虚拟内存区域的页表映射,还会将相关的物理页面放入积聚结构中,后面统一释放。
下面我们来看下这个函数:
函数传递进已经初始化好的mmu积聚结构、操作的起始vma、以及虚拟内存范围[start_addr, end_addr], 然后调用unmap_single_vma来操作这个范围内的每一个vma。
unmap_single_vma的实现相关代码比较多,在此不在赘述,我们会分析关键代码,它主要做的工作为:通过遍历进程的多级页表,来找到vma中每一个虚拟页对应的物理页(存在的话),然后解除虚拟页到物理页的映射关系,最后将物理页放入积聚结构中。
总体调用如下:
下面我们省略中间各级页表的遍历过程,重点看下最后一级页表的处理(这段代码相当关 键 ):
以上函数,遍历进程相关页表(一个pmd表项指向一个页表)所描述的范围的每一个虚拟页,如果之前已经建立过映射,就将相关的页表项清除,对于在内存中物理页来说,需要调用__tlb_remove_page将其加入到mmu的积聚结构中,下面重点看下这个函数:
我们再来看下tlb_next_batch的实现:
这里有几个地方需要注意:MAX_GATHER_BATCH_COUNT 表示的是mmu积聚操作最多可以有多少个批次积聚结构,他的值为10000UL/MAX_GATHER_BATCH (考虑到非抢占式内核的soft lockups的影响)。MAX_GATHER_BATCH 表示一个批次的积聚结构的 page数组的最多元素个数,他的值为((PAGE_SIZE - sizeof(struct mmu_gather_batch)) / sizeof(void *)),也就是物理页面大小去除掉struct mmu_gather_batch结构大小。
下面给出相关图解:
解除页表过程:

添加的到积聚结构页面数组页面小于等于8个的情况:

添加的到积聚结构页面数组页面大于8个的情况:
1个批次积聚结构->

2个批次积聚结构->

更多批次积聚结构加入->

2.5 free_pgtables
unmap_vmas函数主要是积聚了一些相关的虚拟页面对应的物理页面,但是我们还需要释放各级页表对应的物理页等。下面看下free_pgtables的实现:
首先看下它的主要脉络:
我们主要看free_pgd_range的实现:
我们以最后一级页表(pmd表项指向)为例说明:
看下pte_free_tlb函数:
再看看__pte_free_tlb:
需要说明的是:对于存放各级页目录的物理页的释放,每当一个页表积聚结构填满了就会释放,不会构建批次链表。
2.6 tlb_finish_mmu
通过上面的unmap_vmas和free_pgtables之后,我们积聚了大量的物理页以及存放各级页目录的物理页,现在需要将这些页面进行释放。
下面我们来看下tlb_finish_mmu做的mmu-gather的收尾动作:
首先看下tlb_flush_mmu:
tlb_flush_mmu_tlbonly的实现:
我们来看下tlb_flush:
最后我们看tlb_flush_mmu_free:
tlb_table_flush的实现:
tlb_batch_pages_flush的实现:
最终是:调用free_pages_and_swap_cache将物理页的引用计数减1 ,引用计数为0时就将这个物理页释放,还给伙伴系统。
虽然上面已经释放了相关的各级页表的物理页和映射到进程地址空间的物理页,但是存放积聚结构和page数组的物理页还没有释放,所以调用tlb_batch_list_free来做这个事情:
于是相关的所有物理页面都被释放了(包括相关地址范围内进程各级页目录对应的物理页,映射到进程地址空间的物理页,和各个积聚结构所在的物理页)。
最后给出整体的图解:

tlb_flush_mmu函数的tlb_table_flush会将B链表中的相关物理页面释放(包括之前保存的各级页表的页面和mmu_table_batch结构所在页面),tlb_batch_pages_flush会将A链表的所有除了积聚结构以外的所有物理页面释放,而tlb_batch_list_free会将A链表的所有批次积聚结构(mmu_gather_batch)的物理页面释放。
3.应用场景
使用mmu-gather的应用场景主要是进程退出,执行execv和调用munmap等。
下面我们主要来看下他们的调用链:
3.1 进程退出时
进程退出时会释放它的所有的相关联的系统资源,其中就包括内存资源:
3.2 执行执行时
执行execv时进程会将所有的mm释放掉:
3.3 调用munmap时
执行munmap时,会将一个地址范围的页表解除并释放相关的物理页面:
4.总结
Linux内核mmu-gather用于积聚解除映射的相关物理页面,并保证了刷tlb和释放物理页面的顺序。首先解除掉相关虚拟页面对应物理页面(如果有的话)的页表映射关系,然后将相关的物理页面保存在积聚结构的数组中,接着将相关的各级页目录表项清除,并放入页表相关的积聚结构的数组中,最后刷对应内存范围的tlb,释放掉所有放在积聚结构数组中的物理页面。
