我们来详细了解一下内核的内存模型,为了达成了解的目的,我们从内核的两个宏函数入手 page_to_pfn、pfn_to_page
内核提供了两个宏来完成
PFN
与物理页结构体struct page
之间的相互转换。它们分别是page_to_pfn
与pfn_to_page
,先看一下内核的这两个宏函数
#define page_to_pfn __page_to_pfn
#define pfn_to_page __pfn_to_page
具体的实现
#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)
#elif defined(CONFIG_DISCONTIGMEM)
#define __pfn_to_page(pfn) \
({ unsigned long __pfn = (pfn); \
unsigned long __nid = arch_pfn_to_nid(__pfn); \
NODE_DATA(__nid)->node_mem_map + arch_local_page_offset(__pfn, __nid);\
})
#define __page_to_pfn(pg) \
({ const struct page *__pg = (pg); \
struct pglist_data *__pgdat = NODE_DATA(page_to_nid(__pg)); \
(unsigned long)(__pg - __pgdat->node_mem_map) + \
__pgdat->node_start_pfn; \
})
#elif defined(CONFIG_SPARSEMEM_VMEMMAP)
/* memmap is virtually contiguous. */
#define __pfn_to_page(pfn) (vmemmap + (pfn))
#define __page_to_pfn(page) (unsigned long)((page) - vmemmap)
#elif defined(CONFIG_SPARSEMEM)
/*
* Note: section's mem_map is encoded to reflect its start_pfn.
* section[i].section_mem_map == mem_map's address - start_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))); \
})
#define __pfn_to_page(pfn) \
({ unsigned long __pfn = (pfn); \
struct mem_section *__sec = __pfn_to_section(__pfn); \
__section_mem_map_addr(__sec) + __pfn; \
})
#endif /* CONFIG_FLATMEM/DISCONTIGMEM/SPARSEMEM */
为了快速索引到具体的物理内存页,内核为每个物理页 struct page 结构体定义了一个索引编号:PFN(Page Frame Number)。PFN 与 struct page 是一一对应的关系。
从上面的代码可以看出有三个宏:
- CONFIG_FLATMEM
- CONFIG_DISCONTIGMEM
- CONFIG_SPARSEMEM
- CONFIG_SPARSEMEM_VMEMMAP 是否使能
分别代表了内核不同的内存模型,下面我们详细来看看这几种内存模型有何不同。
FLATMEM 平坦内存模型
FLATMEM
(平坦内存模型)是系统从 CPU 角度看,当访问物理内存的时候,物理地址空间是一个连续的、没有地址空洞的地址空间
内核中使用了一个
mem_map
的全局数组用来组织所有划分出来的物理内存页。mem_map
全局数组的下标就是相应物理页对应的 PFN
。在这种模型下, page_to_pfn
与 pfn_to_page
的计算就比较简单,来详细看一下代码
#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)
ARCH_PFN_OFFSET 是 PFN 的起始偏移量
这种模型主要是早期 linux
物理内存较小的情况下使用,在后期物理内存较大时,且从 UMA
逐渐变成 NUMA
, 采用此方式会带来较大的内存浪费(内存空洞)和性能问题(跨 NUMA 访问),因此在这种背景下 linux 引入了新的内存模型 DISCONTIGMEM
DISCONTIGMEM 非连续内存模型
在 DISCONTIGMEM 非连续内存模型中,内核将物理内存从宏观上划分成了一个一个的节点 node (微观上还是一页一页的物理页),每个 node 节点管理一块连续的物理内存。这样一来这些连续的物理内存页均被划归到了对应的 node 节点中管理,就避免了内存空洞造成的空间浪费
内核中使用 struct pglist_data
表示用于管理连续物理内存的 node
节点(内核假设 node 中的物理内存是连续的),既然每个 node
节点中的物理内存是连续的,于是在每个 node
节点中还是采用 FLATMEM
平坦内存模型的方式来组织管理物理内存页。每个 node
节点中包含一个 struct page *node_mem_map
数组,用来组织管理 node
中的连续物理内存页, 看一下 struct pglist_data
的代码:
typedef struct pglist_data {
...
#ifdef CONFIG_FLAT_NODE_MEM_MAP /* means !SPARSEMEM */
struct page *node_mem_map;
...
从这里可以看出 DISCONTIGMEM 就是 FLATMEM 的扩展。我们接着看一下这个模型下 page_to_pfn
与 pfn_to_page
的计算代码
#if defined(CONFIG_DISCONTIGMEM)
#define __pfn_to_page(pfn) \
({ unsigned long __pfn = (pfn); \
unsigned long __nid = arch_pfn_to_nid(__pfn); \
NODE_DATA(__nid)->node_mem_map + arch_local_page_offset(__pfn, __nid);\
})
#define __page_to_pfn(pg) \
({ const struct page *__pg = (pg); \
struct pglist_data *__pgdat = NODE_DATA(page_to_nid(__pg)); \ // 根据struct page 定位到所在的 node
(unsigned long)(__pg - __pgdat->node_mem_map) + \
__pgdat->node_start_pfn; \
})
pfn_to_nid
static inline int pfn_to_nid(unsigned long pfn)
{
#ifdef CONFIG_NUMA
return((int) physnode_map[(pfn) / PAGES_PER_SECTION]);
#else
return 0;
#endif
}
每个 node 是 64Mb,如果 page size 是 4k 的话,所以根据 pfn 可以推断出具体落在哪个 node
SPARSEMEM 稀疏内存模型
随着内存技术的发展,内核可以支持物理内存的热插拔了(后面笔者会介绍),这样一来物理内存的不连续就变为常态了,在上小节介绍的
DISCONTIGMEM
内存模型中,其实每个node
中的物理内存也不一定都是连续的
SPARSEMEM
稀疏内存模型的核心思想就是对粒度更小的连续内存块进行精细的管理,用于管理连续内存块的单元被称作 section
的大小为 128M(x86_64)。
在内核中用 struct mem_section 结构体表示 SPARSEMEM 模型中的 section.
0 struct mem_section {
...
unsigned long section_mem_map;
...
};
由于 section 被用作管理小粒度的连续内存块,这些小的连续物理内存在 section 中也是通过数组的方式被组织管理,每个 struct mem_section 结构体中有一个 section_mem_map 指针用于指向 section 中管理连续内存的 page 数组
#ifdef CONFIG_SPARSEMEM_EXTREME
extern struct mem_section **mem_section;
#else
extern struct mem_section mem_section[NR_SECTION_ROOTS][SECTIONS_PER_ROOT]; // SECTIONS_PER_ROOT = 1
#endif
#if defined(CONFIG_SPARSEMEM)
/*
* Note: section's mem_map is encoded to reflect its start_pfn.
* section[i].section_mem_map == mem_map's address - start_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))); \
})
#define __pfn_to_page(pfn) \
({ unsigned long __pfn = (pfn); \
struct mem_section *__sec = __pfn_to_section(__pfn); \
__section_mem_map_addr(__sec) + __pfn; \
})
#endif /* CONFIG_FLATMEM/DISCONTIGMEM/SPARSEMEM */
在 SPARSEMEM 稀疏内存模型下 page_to_pfn 与 pfn_to_page 的计算逻辑又发生了变化。
在 page_to_pfn 的转换中,首先需要通过 page_to_section 根据 struct page 结构定位到 mem_section 数组中具体的 section 结构。然后在通过 section_mem_map 定位到具体的 PFN。
在 struct page 结构中有一个 unsigned long flags 属性, 在 flag 的高位 bit 中存储着
page 所在 mem_section 数组中的索引,从而可以定位到所属 section。
section 占用 page flags 的 bit 位(19)
static inline unsigned long page_to_section(const struct page *page)
{
return (page->flags >> SECTIONS_PGSHIFT) & SECTIONS_MASK;
}
在 pfn_to_page 的转换中,首先需要通过 __pfn_to_section 根据 PFN 定位到 mem_section 数组中具体的 section 结构。然后在通过 PFN 在 section_mem_map 数组中定位到具体的物理页 Page 。
PFN 的高位 bit 存储的是全局数组 mem_section 中的 section 索引,PFN 的低位 bit 存储的是
section_mem_map 数组中具体物理页 page 的索引。
CONFIG_SPARSEMEM_VMEMMAP
/* memmap is virtually contiguous. */
#define __pfn_to_page(pfn) (vmemmap + (pfn))
#define __page_to_pfn(page) (unsigned long)((page) - vmemmap)
其中 vmemmap
的定义如下:
#define vmemmap ((struct page *)VMEMMAP_START)
|
| Kernel-space virtual memory, shared between all processes:
____________________________________________________________|___________________________________________________________
| | | |
ffff800000000000 | -128 TB | ffff87ffffffffff | 8 TB | ... guard hole, also reserved for hypervisor
ffff880000000000 | -120 TB | ffff887fffffffff | 0.5 TB | LDT remap for PTI
ffff888000000000 | -119.5 TB | ffffc87fffffffff | 64 TB | direct mapping of all physical memory (page_offset_base)
ffffc88000000000 | -55.5 TB | ffffc8ffffffffff | 0.5 TB | ... unused hole
ffffc90000000000 | -55 TB | ffffe8ffffffffff | 32 TB | vmalloc/ioremap space (vmalloc_base)
ffffe90000000000 | -23 TB | ffffe9ffffffffff | 1 TB | ... unused hole
ffffea0000000000 | -22 TB | ffffeaffffffffff | 1 TB | virtual memory map (vmemmap_base)
ffffeb0000000000 | -21 TB | ffffebffffffffff | 1 TB | ... unused hole
ffffec0000000000 | -20 TB | fffffbffffffffff | 16 TB | KASAN shadow memory
__________________|____________|__________________|_________|____________________________________________________________
0xffffea0000000000 是内核空间预留的一块虚拟地址空间
对于 SPARSEMEM_VMEMMAP 而言,虚拟地址一开始就分配好了,是 vmemmap 开始的一段连续的虚拟地址空间,每一个 page 都有一个对应的 struct page,当然,只有虚拟地址,没有物理地址。因此,当一个 section 被发现后,可以立刻找到对应的struct page的虚拟地址,当然,还需要分配一个物理的 page frame,然后建立页表什么的,因此,对于这种 sparse memory,开销会稍微大一些(多了个建立映射的过程)。
总结
上面主要是粗略描述了 kernel 内存管理的不同模型,并阐述了不同模型之间page_to_pfn
与 pfn_to_page
的计算差异。