内核文档 Documentation/arm64/memory.rst 描述了 ARM64 Linux 内核空间的内存映射情况,应该是此方面最权威文档。
以典型的 4K 页和 48 位虚拟地址为例,整个内核空间的虚拟地址分布如下:
从 ffff000000000000 到 ffff7fffffffffff 是一段针对物理地址的线性映射区,最大支持 128TB 的物理地址空间,这一段地址非常类似 ARM32 的 low memory 映射区。
我们看看这种情况下的页表,我们既可以用最终的【20:12】对应的 PTE 映射项,以 4K 为单位,进行虚拟地址到物理地址的映射;又可以以【29:21】对应的 PMD 映射项,以 2M 为单位,进行虚拟地址到物理地址的映射。
对于用户空间的虚拟地址而言,当我们进行的是 PMD 映射的时候,我们得到的是 Huge Page,ARM64 的 2MB 的 huge page,在虚拟和物理上都连续,它在实践工程中的好处是,可以减小 TLB miss,因为,如果进行了 2MB 的映射,整个 2MB 不再需要 PTE,映射关系大为减小。
对于内核空间而言,从 ffff000000000000 到 ffff7fffffffffff 的这段虚拟地址,如果与物理地址进行的是一种 PMD 映射的话,显然也可以达到同样的效果。但是,这不意味着它们就是 Huge Page。众所周知,内核开机把物理地址往虚拟地址进行线性映射,并不意味着这片内存被内核拿走了,它只是进行了一种映射,以便日后调用 kmalloc(),get_free_pages()等 API 申请的内存是直接已经有虚实映射的。所以,即便内核进行的就是 PMD 映射,在内存的分割上,还是可以以 4K 为单位的:
所以,即便我们在内核空间进行 PMD 映射,里面的每个蓝色圆圈(一个 4K 页),还是可以被单独分配的,这种分配可以是 kmalloc、vmalloc,用户态的 malloc 等。内核态进行的 PMD 映射,不意味着相关的 2MB 成为了 huge page,它纯粹只是为了服务于当内核以线性映射的虚拟地址访问该物理地址的时候(我们认为内核大多数时候是用这个线性映射的虚拟地址的),减小 TLB miss。
当然,更牛逼的情况下,内核应该也可以直接用【38:30】位的 PUD 来进行映射,这样映射关系是 1GB 的,则整个 1GB 后面占 TLB 的时候,只需要占一个入口。
当然,如果用户态的虚实映射是这样的,用户实际得到了一个 1GB 的巨页。但是对于内核的线性映射区域而言,即便我们进行了 1GB 的 PUD 映射,这 1G 内部就可以进一步切割为 4KB 页或者 2MB 的巨页。记住:内核态的线性映射区的映射只是个映射关系,不是个分配关系。比如下面的 1GB 的内核线性映射的 1GB 区域,仍然可以被 4K 分配走,或者被用户以 huge page 以 2MB 为单位分配走:
我们需要一个真实的调试手段来验证我们的想法,这个调试手段就是 PTDUMP(Page Table Dump),相关的代码在 ARM64 内核的:
arch/arm64/mm/ptdump.c 和 ptdump_debugfs.c
我们把它们全部选中,这样我们可以得到一个 debugfs 接口:
/sys/kernel/debug/kernel_page_tables
来获知内核态页表的情况。
我用 qemu 启动了一个 4GB 内存的 ARM64 虚拟机,可以看到前 1GB 的虚拟地址空间大多数是 PMD 和 PTE 映射,后面的 3GB,全是 PUD 映射:
我的内核启动参数加了 rodata=0:
$ cat /proc/cmdline
root=/dev/vda2 rw console=ttyAMA0 ip=dhcp rodata=0
原因是内核在几种情况下,是不会做这种 PMD 和 PUD 映射的,相关代码见于:
rodata_full 在默认情况下总是成立的,它对应着内核的一个 Config 选项 CONFIG_RODATA_FULL_DEFAULT_ENABLED, "Apply r/o permissions of VM areas also to their linear aliases",这个选项提高了内核的安全性,但是减小了内核的性能。
我在内核启动参数加的 rodata=0 实际上是让 rodata_full 为 false。如果我把这个 kernel 启动选项去掉,我得到的内核页表是完全不一样,线性映射区也全部是 PTE 映射:
最后,值得一提的是,不仅线性映射区可以使用 PMD 映射,vmemmap 映射区也是在 4K 页面情况下,默认用 PMD 映射的:
字节跳动的宋牧春童鞋发了一个 patchset,企图在用户分得巨页的情况下,删除巨页内部的 4KB 的小 page 占用的 page struct 的内存消耗,这个 patchset 在圣诞节前目前发到了 V11:
https://lore.kernel.org/linux-mm/20201222142440.28930-1-songmuchun@bytedance.com/
在这个 patchset 中,它就需要拆分 vmemmap 的 PMD 映射为 PTE 映射:
这个 patchset 的原理建立在,当内核以 4KB 分页的时候,每个 page 需要 64 字节的 page struct。但是,当用户把它分配为巨页的时候,时候,我们不再需要一个个 4KB 单独用 page struct 描述,对于这种 compound page 的情况,我们应该可以把后面的 page struct 的内存直接释放掉,因为情况完全是雷同的,这样可以剩下不少内存。
看到牧春童鞋从一个青葱少年成长为这方面的大牛,我真地替他高兴和骄傲。这同时也激励我必须保持奋斗姿态,2021 年要不停歇地学习进步,争取赶上牧春童鞋。牧春童鞋在“Linux 阅码场”这里还有一些精彩的文章:
宋牧春:Linux 设备树文件结构与解析深度分析(1)
宋牧春:Linux 设备树文件结构与解析深度分析(2)
宋牧春:多图详解 Linux 内存分配器 slub
宋牧春:Linux 内核内存 corruption 检查利器 KASAN 实现原理
后面我们期待牧春专门写一篇文章来深入描述他的 patchset。