本文以arm64架构为背景。
一 背景
计算机中的物理内存本来是没有没有页/page的概念的,Linux为了各种冠冕堂皇的理由,硬生生的将计算机中的物理内存以page为单位划分成一个一个的小方块,称作页框,每个页框有一个编号叫做PFN;有了PFN,就能够计算出这个页框对应的物理地址,有了物理地址CPU就能够通过总线访问到对应的内存。这不尽让我想起了成都绵延不尽的天府大道被划分为了Y段、X号:天府大道北段xxx号、天府大道中断xxx号、天府大道南段xxx号......人类对于化整为散的单位划分法有着与生俱来的癖好。
为了把物理页框用软件的方式管理起来,Linux定义了*struct page*这样一个数据结构,每一个struct page数据结构就对应着一个实实在在的页框,就好像一朵花生花就会在土壤里长出一颗花生 一样。
软件的struct page有了,物理层面的PFN也有了,还差点啥? 就差把二者联系到一起的公式罢了。这就是内核中既平凡,又深沉的pfn_to_page()与page_to_pfn()两个表达式了。名字接地气,一看就懂:将PFN转换为struct page(指针,即struct page的虚拟地址,下同)或者将sruct page转换为PFN。
1.1 FLAT内存模型
我们把时光拉回到Linux梦想开始的地方,那时的物理内存地址总是从0开始,然后绵延若干MB到达终点;struct page也才刚刚诞生,那时的Linux刚满1.3.50岁,整个世界满是单纯与童真。一个搭载着一串struct page指针的mem_map数组,一溜连续的数组index,struct page与PFN就这样产生了微妙的联系。没有花哨的调料,也没有做作的烹饪,最简单的线性运算就能够原滋原味的pfn_to_page和page_to_pfn。
用PFN作饵,放置到mem_map中,二者结合后生成的mem_map[PFN]变得出了对应的struct page指针,pfn_to_page()的功效应运而生;
反之,先采得struct page指针,再减去mem_map[0],二者强烈的味觉反差后便得到了PFN,一道page_to_pfn()就此出锅;就连隔壁3岁的狗蛋子也能够享受到这对表达式带来的干脆与直接。
1.2 SPARSEMEM模型
当时光的列车缓缓驶过kernel.org社区,51岁的Linus Torvalds就在那里,深情的目光望过去都是自己22岁风驰电掣的影子;
当岁月含泪悄悄转身,51岁的Linus 就在那里,深情的目光望去勾勒出自己老去的样子。
FLAT内存模型虽然简单,但是世界在变,计算机也在改变,而内存也不再是我们认识的那个单纯的内存:
内存不一定再是从物理地址"0"开始;物理内存也不一定都是连续的,相反物理内存之间可能存在空洞;NUMA的出现,让FLAT模型中的mem_map[]更是无以为继;hotplug memory更是让FLAT模型彻底淡出公众视线。
整个内存模型的发展过程经历了FLAT、DISCONTIGMEM直到现在的SPARSEMEM,内存模型定型下来。
Sparse是稀疏的意思,也就是说内存不是连续、平坦的。FLAT内存模型要为整个物理内存区间建立连续的struct page数组,这势必会再物理内存中间有空洞存在的情况下造成巨大的浪费;DISCONTIGMEM虽然解决了这个问题,但是对于hot plug/remove和numa支持有弱点;最终SPARSEMEM解决了DISCONTIGMEM存在的问题。
坐稳了, 下面开始严肃的讲讲技术。
二 经典SPARSEMEM模型pfn_to_page
这里是以"经典"SPARSEMEM模型进行分析的。什么是"经典"模型呢?也就是SPARSEMEM最初的发明的模型,具体一点就是CONFIG_SPARSEMEM_EXTREME=n、CONFIG_SPARSEMEM_VMEMMAP=n,这两个后面再分析。
接下来看看SPARSEMEM模型是如何来管理页框的,pfn_to_page在是一个宏,在SPARSEMEM的实现如下所示:
这个宏表达式中,pfn就是页框号,这个前面有讲,这里不必多说。
这里有一个struct mem_section数据结构是干什么的呢?
2.1 SPARSEMEM中的mem_section
在FLAT模型中使用一个数组来装下物理内存空间对应的所有struct page;而在SPARSEMEM中,则是将物理空间分割为多个"区"进行管理,一个"区"管理若干数量的struct pages,linux中将这个"区"称为mem_section。拿经典的48bit物理地为例,bit[0,29]这30个bit用来表示一个mem_section。即一个物理地址的低30bit作为一个mem_section内部offset,经典模型中高18bit用来作mem_sections集合中的索引。(不过在linux5.11版本中arm64/sparsemem: reduce SECTION_SIZE_BITS 这个补丁为了降低memory hotplug粒度,将一个section的粒度进行了降低,为了说明方便,这里还是以30bit作为粒度来进行分析)。
图1 48位物理地址划分
如上图所示:按照mem_section这样的划分粒度,一个mem section中最多可以表示1G大小的物理空间,对于经典的4KB的大小页面而言,即一个mem section中最多可以表示2^18个PFN,即需要有2^18个strut page来进行管理,这些struct pages就是一个mem section所要管理的内容;而要将48bit物理地址所表示的物理内存都能够囊括进来,就需要2^18个mem_section,即最多可以表示的页框数为:
ok,说完了mem_section的粒度,下面继续看看转换的流程。
2.2 通过pfn号找到该页所属的struct mem_section
上面就是通过pfn号找到该页所属的mem_section。
和FLAT内存模型类似, SPARSEMEM也是通过页框号来寻址到对应的struct page指针, 只不过在SPARSEMEM内存模型中先要找到页框所在的mem_section。
前面已经提到物理地址的高18位用以表示mem_section集合中的索引;对于经典的SPARSE内存模型来说这些mem sections数据结构是放到一个名为mem_section[NR_MEM_SECTIONS][1]数组中的。数组的第二维只有一个元素,因而你可以就理解为mem_section[NR_MEM_SECTIONS]。
其中NR_MEM_SECTIONS定义在include/linux/mmzone.h文件中
即总共有NR_MEM_SECTIONS个mem_section区域;这样就可以通过PFN的高位bit和SECTIONS_SHIFT相与即可得到该页框所在的mem_section[]数组中的的索引index,然后从数组取得该页框所所属的struct mem_sectoin。
- pfn_to_section_nr(pfn):寻找页框所属mem section索引
- __nr_to_section(nr):根据索引从数组中取得对应的struct mem_section结构指针。
在经典SPARSEMEM模型中,SECTION_ROOT_MASK=0,而SECTION_NR_TO_ROOT(nr)就等价于nr。
总结一下:通过PFN寻找页框所属的mem section,就是根据PFN高位bit获得section index,然后再mem_section[]数组中取得struct mem_section即可。
2.3 获取页框对应的struct page
前面根据PFN找到了对应的struct mem_section, 但是我们的最终目标是找到struct page,在SPARSE内存模型中struct page放在哪里的呢?答案:和FLAT内存模型类似,struct page指针也放在一个数组中,这个数组的地址存放在struct mem_section成员section_mem_map中(和FLAT模型mem_map名字有几分相似),下面是一个精简后的mem_section结构。
2.3.1 神奇的section_mem_map
成员section_mem_map是一个unsigned long类型。可别小看这个成员,这个小小的section_mem_map容纳了如下内容:(1)struct pages数组地址,(2)若干mem section状态标志,(3)该mem section中struct pages数组第一个page元素地址与第一个该section第一个PFN的偏移。
其中(1)和(2)可能都还比较好理解,我们重点看看(3)。存放struct page的数组mem_map[]通过数组索引获取到的指定的struct page,其中的第一个page元素的索引位0,但是它的PFN却并不一定是0,因而需要将这个转换关系存储起来。Linux将首个struct page元素的地址,实际上也就是数组地址mem_map与PFN的差值(mem_map-PFN) 存放到section_mem_map中,这样不论是pfn_to_page还是page_to_pfn都能够随心所欲的通过pfn作为mem_map的索引了,巧妙!
2.3.2 get到struct page
万事俱备,只欠东风。
前面在2.2中已经获取PFN所属的mem_section,下面三个步骤最终获取到对应的struct page指针。
- mem_section获取到成员section_mem_map,
- 去掉低位bit的编码得到了该section中struct pages数组地址(根据上面2.3.1描述,实际上里面还编码了一个与PFN的偏移量);
- 最后用PFN的低位作为数组偏移就找到了PFN对应的struct page指针。
上面3个步骤就是__section_mem_map_addr(__sec) + __pfn的实现流程,其中__section_mem_map_addr的代码如下:
三 经典SPARSEMEM模型page_to_pfn
如果知道了struct page指针,如何获得对应的物理页框号呢?下面是具体的步骤:
- 从page->flags中取得它所属的mem section索引号__sec;
- 使用索引号mem_section[__sec][0]从数组中取出struct mem_section地址section;
- 从section->section_mem_map解码出该section中struct pages数组地址map与PFN的差值,即(map - PFN_OFFSET)(参考上面2.3.1);
- 通过[page - (map - PFN_OFFSET) ]即可得到page对应的页框号PFN。
如下就是与上面对应的代码实现:
#define __page_to_pfn(pg) \ ({ const struct page *__pg = (pg); \ int __sec = page_to_section(__pg); \ (unsigned long)(__pg - __section_mem_map_addr(__nr_to_section(__sec))); \ })
四 SPARSEMEM增强版
4.1 SPARSEMEM_EXTREME
在SPARSEMEM加入到linux几个月后,SPARSEMEM_EXTREME又被引入到kernel,这个特性是针对极度稀疏物理内存对SPARSEMEM模型进行的一种扩展。这种扩展的理由在于SPARSEMEM模型中使用了一个长度为NR_MEM_SECTIONS的struct mem_section数组来表示所有可能的mem sections。对于一些极度稀疏的物理内存,并不会用到这么多的mem sections,因而是一种浪费。
SPARSEMEM_EXTREME对物理地址中表示mem sections的bit位再次进行了划分,将SECTIONS_PER_ROOT个section划分为一个SECTION_ROOT。SECTIONS_PER_ROOT的值是一个页能够容纳struct mem_strcut的个数;即:
因而在CONFIG_SPARSEMEM_EXTREME=y时mem_section不再是一个全局静态数组,而是一个struct **mem_section指针,这个mem_secton指针数组成员在系统内存初始化阶段,对于真实存在物理页框的mem sections才分配struct mem_section:
4.2 SPARSEMEM_VMEMMAP
2007年引入了一个新的SPARSEMEM增强特性,称之为 Generic Virtual Memmap support for SPARSEMEM, or SPARSEMEM_VMEMMAP。引入这个特性的原因是因为经典SPARSEMEM不仅在进行 pfn_to_page() 和 page_to_pfn()时颇为复杂,而且需要消耗宝贵的page->flags bit位资源用于存放section 索引。
SPARSEMEM_VMEMMAP的实现思路非常简洁:在虚拟地址空间中划分出一个连续地址区域用于和物理页框号一一映射,这样一定这个虚拟区域的首地址确定下来,系统中所有物理页框对应的struct page也就确定下来,即
上面就是CONFIG_SPARSEMEM_VMEMMAP=y时的struct page与PFN之间的转换方法,和FLAT模式一样简单纯洁。不过这里有一个vmemmap作为计算的基础,它是什么呢?怎么来的呢?
我们首先看看它的定义:
memstart_addr:当前系统中实际的物理起始地址。现在物理起始地址很多都不是从0开始的。
VMEMMAP_STRAT:在arm64中虚拟地址空间布局中VMEMMAP区域的起始虚拟地址,VMEMMAP区域的具体情况可参考Memory Layout on AArch64 Linux。
至于VMEMMAP_STRAT 要减去一个(memstart_addr >> PAGE_SHIFT)的原因,就是为了确保VMEMMAP这个区域的第一个struct page指针对应的是系统中第一个实际有效物理页框。
不过在使用vmemmap前需要建立它和struct pages数组的虚实映射关系,这个的是在如下流程中初始化的。