一、背景
操作系统的一个重要功能就是管理计算机中的各种硬件资源,比如说CPU、内存、显存、串口等,其中内存是这些资源中很珍贵的部分,所以为了能把内存管理好做了很多工作。硬件内存架构是不停地发展变化的,内核中管理内存的模型也在跟着硬件的变化而不停地演进。当前linux内核中有三种模型,分别是:flat memory model,Discontiguous memory model和sparse memory model。本文的参考代码是linux5.0,为了清晰地分析内存模型,先介绍相关的一些基本概念,具体如下:
1、什么是PFN:
linux为了方便把物理内存管理起来,内核中把物理内存在逻辑上划分为一块块大小为page size的page frame,同时每个page frame都对应一个编号,称为PFN。其中page size是内核的配置以及硬件体系结构决定的,比如常见的page size为4K。根据PFN就可以算出这个page frame对应的物理地址,这样CPU就能通过地址总线访问这些物理页了。
怎么计算物理地址的PFN呢,物理地址和PFN是一一对应的,比如page size为4K时,把物理地址左移12位就得到了PFN,可参考如下代码:
include/linux/pfn.h
#define PFN_PHYS(x) ((phys_addr_t)(x) << PAGE_SHIFT)
#define PHYS_PFN(x) ((unsigned long)((x) >> PAGE_SHIFT))
2、什么是struct page:
在逻辑上把物理内存划分好后,为了跟踪每个page frame的状态,linux内核为每一个page frame配备了一个"struct page",用于跟踪对应物理页的状态,比如区分具体物理页保存的是进程的数据,还是内核的代码段或数据段。同样还得区分物理页是否是空闲的,如果物理页不存放任何有用的数据,那么它就是空闲的,当它用于存放进程数据、充当cache时,它就不是空闲状态。struct page会把物理页的各种信息都保存起来。
3、为什么要有内存模型了:
有了PFN和struct page,还得把他们俩联系起来了,这就是pfn_to_page()与page_to_pfn()的工作,意思很明显了,PFN和page之间的相互转换就涉及到了内存模型。
那为什么又是三种了,计算机刚起步的时候,物理内存都是连续的一整块,而且地址一般都是从0开始的,同时对于每个processor来说都一样,那时候生活是简单且美好的,只需要根据物理内存的大小分配一个struct page的数组,数组的index就是对应的PFN,看起一切都是那么美好。但时代在变化,计算机系统更是日新月异,内存早已不是那个简单单纯的它了,物理地址不一定从0开始了,也不一定连续了,内存中间可能有空洞了,而且还支持热插拔了。所以随着时间的推移,出现三种内存模型来应对硬件上的变化,下面详细地介绍每一个具体的模型。
二、flat memory model
这是最简单的内存模型,所管理的物理内存就是连续的,没有空洞的,同时对于每个CPU来说都是一样的。在linux内核使用此模型时,会分配一个全局的struct page的数组mem_map,每个数组元素对应一个page frame,PFN和数组的下标是一种线性的对应关系,主要与ARCH_PFN_OFFSET有关,如果正好ARCH_PFN_OFFSET为0,数组的index就和PFN一一对应起来了,如下图所示:
其实mem_map对应的数组是根据实际的物理内存大小动态分配,在flat memory model模型下,pfn_to_page()与page_to_pfn()的实现:
include/asm-generic/memory_model.h
#if defined(CONFIG_FLATMEM)
#define __pfn_to_page(pfn) (mem_map + ((pfn) - ARCH_PFN_OFFSET))
#define __page_to_pfn(page) ((unsigned long)((page) - mem_map) + \
ARCH_PFN_OFFSET)
二、Discontiguous memory model
时间回溯到1999年,为了linux能在NUMA的硬件机器上更好地运行,做了很多工作,DISCONTIGMEM model就是其中部分成果,它就是用于处理不连续的物理内存。DISCONTIGMEM model引入了memory node的概念,同时memory node是NUMA内存管理的基础。从内存管理的角度来说,每个memory node都拥有一个独立的内存管理子系统,比如都有空闲页列表,使用页列表,最近最少访问的信息以及页使用的一些统计数据。在linux中,memory node是用struct pglist_data来表示,其中包含了此node特有的memory map。DISCONTIGMEM model前提是要求每个memory node内的内存在物理上是连续的,struct pglist_data中包含了一个类似flat memory map的struct page数组,从memory node内部来看,PFN和page之的转换与flat的一样,这样就完美地解决了内存不连续的问题。
DISCONTIGMEM model必须要提供一种方式,即把包含在memory node中内存的PFN转换为相应的struct page,同样,它也要能根据struct page得到相应的PFN。当要把PFN转换为struct page时,首先从PFN中获取到memory node,然后通过"PFN - NODE_DATA(nid)->node_start_pfn"得到PFN在具体memory node中的page_offset,再根据"NODE_DATA(nid)->node_mem_map + page_offset"获得PFN对应的struct page;struct page中的flag中包含了memory node信息,故从struct page转换为PFN就简单了,请参考具体代码,不在赘述。
DISCONTIGMEM model有个致命的弱点,它无法支持内存的热插拔。NUMA node的内存数量级对于支持热插拔来说太大了,但拆分NUMA node的话可能又会增加很多内存碎片和开销。每个NUMA node都有自己独立的内存管理子系统,对应的就是每个子系统都有相应的开销,拆分NUMA node会进一步加剧这些开销,这也是DISCONTIGMEM model无法支持热插拔的原因。DISCONTIGMEM model和NUMA model这两