Read the fucking source code!
--By 鲁迅A picture is worth a thousand words.
--By 高尔基说明:
顺着之前的分析,我们来到了bootmem_init()
函数了,本以为一篇文章能搞定,大概扫了一遍代码之后,我默默的把它拆成了两部分。bootmem_init()
函数代码如下:
void __init bootmem_init(void)
{
unsigned long min, max;
min = PFN_UP(memblock_start_of_DRAM());
max = PFN_DOWN(memblock_end_of_DRAM());
early_memtest(min << PAGE_SHIFT, max << PAGE_SHIFT);
max_pfn = max_low_pfn = max;
arm64_numa_init();
/*
* Sparsemem tries to allocate bootmem in memory_present(), so must be
* done after the fixed reservations.
*/
arm64_memory_present();
sparse_init();
zone_sizes_init(min, max);
memblock_dump_all();
}
这一部分,我们将研究一下Sparse Memory Model
。
在讲Linux内存模型之前,需要补充两个知识点:PFN
和NUMA
。
前面我们讲述过了虚拟地址到物理地址的映射过程,而系统中对内存的管理是以页为单位的:page
:线性地址被分成以固定长度为单位的组,称为页,比如典型的4K大小,页内部连续的线性地址被映射到连续的物理地址中;page frame
:内存被分成固定长度的存储区域,称为页框,也叫物理页。每一个页框会包含一个页,页框的长度和一个页的长度是一致的,在内核中使用struct page
来关联物理页。
如下图,PFN从图片中就能看出来了:
至于__page_to_pfn
这个实现取决于具体的物理内存模型,下文将进行介绍。
从上图中可以看出,当处理器和Core变多的时候,内存带宽将成为瓶颈问题。
从图中可以看出,每个CPU访问local memory,速度更快,延迟更小。当然,整体的内存构成一个内存池,CPU也能访问remote memory,相对来说速度更慢,延迟更大。目前对NUMA
的了解仅限于此,在内核中会遇到相关的代码,大概知道属于什么范畴就可以了。
Linux提供了三种内存模型(include/asm-generic/memory_model.h
):
一般处理器架构支持一种或者多种内存模型,这个在编译阶段就已经确定,比如目前在ARM64中,使用的Sparse Memory Model
。
Flat Memory
物理内存地址连续,这个也是Linux最初使用的内存模型。当内存有空洞的时候也是可以使用这个模型,只是struct page *mem_map
数组的大小跟物理地址正相关,内存有空洞会造成浪费。
Discontiguous Memory
物理内存存在空洞,随着Sparse Memory
的提出,这种内存模型也逐渐被弃用了。
Sparse Memory
物理内存存在空洞,并且支持内存热插拔,以section
为单位进行管理,这也是下文将分析的。
Linux三种内存模型下,struct page
到物理page frame
的映射方式也不一样,具体可以查看include/asm-generic/memory_model.h
文件中的__pfn_to_page/__page_to_pfn
定义。
关于内存模型,可以参考Memory: the flat, the discontiguous, and the sparse
本节分析的是ARM64, UMA(linux4.14中不支持ARM NUMA)
下的Sparse Memory
模型。
在Sparse Memory
模型中,section
是管理内存online/offline
的最小内存单元,在ARM64中,section
的大小为1G,而在Linux内核中,通过一个全局的二维数组struct mem_section **mem_section
来维护映射关系。
函数的调用过程如下所示,主要在arm64_memory_present
中来完成初始化及映射关系的建立:
函数调用结束之后的映射关系如下图所示:
已知一个pfn
时,可以通过__pfn_to_section(pfn)
来最终找到对应的struct page
结构。
看看sparse_init
函数的调用关系图:
在该函数中,首先分配了usermap,这个usermap与内存的回收机制相关,用4bit的bitmap来描述page block(一个pageblock大小通常为2的次幂,比如MAX_ORDER-1)
的迁移类型:
/* Bit indices that affect a whole block of pages */
enum pageblock_bits {
PB_migrate,
PB_migrate_end = PB_migrate + 3 - 1,
/* 3 bits required for migrate types */
PB_migrate_skip,/* If set the block is skipped by compaction */
/*
* Assume the bits will always align on a word. If this assumption
* changes then get/set pageblock needs updating.
*/
NR_PAGEBLOCK_BITS
};
sparse memory
模型会为每一个section都分配一个usermap
,最终的物理页面的压缩,迁移等操作,都跟这些位相关,如下图所示:
sparse_init
函数中,另一部分的作用是遍历所有present section
,然后将其映射到vmemmap区域空间。vmemmap
区域空间,在之前的文章中也提到过。执行完后,整体的效果如下图所示:
关于Sparse Memory Model
就先分析这么多,只有结合使用sparse memory
的具体模块时,理解才会更顺畅。
一不小心就容易扣细节,而一旦陷入细节,内核就容易变成魔鬼,太难了。
上一篇文章 别再说你不懂Linux内存管理了,10张图给你安排的明明白白! 分析了 Linux 内存管理机制,如果已经忘了的同学还可以回头看下,并且也强烈建议先阅读那一篇再来看这一篇。限于篇幅,上一篇没有深入学习物理内存管理和虚拟内存分配,今天就来学习一下。
通过前面的学习我们知道,程序可没这么好骗,任你内存管理把虚拟地址空间玩出花来,到最后还是要给程序实实在在的物理内存,不然程序就要罢工了,所以物理内存这么重要的资源一定要好好管理起来使用(物理内存,就是你实实在在的内存条),那么内核是如何管理物理内存的呢?
物理内存管理
在Linux系统中通过分段和分页机制,把物理内存划分 4K 大小的内存页 Page(也称作页框Page Frame),物理内存的分配和回收都是基于内存页进行,把物理内存分页管理的好处大大的。
假如系统请求小块内存,可以预先分配一页给它,避免了反复的申请和释放小块内存带来频繁的系统开销。
假如系统需要大块内存,则可以用多页内存拼凑,而不必要求大块连续内存。你看不管内存大小都能收放自如,分页机制多么完美的解决方案!
But,理想很丰满,现实很骨感。如果就直接这样把内存分页使用,不再加额外的管理还是存在一些问题,下面我们来看下,系统在多次分配和释放物理页的时候会遇到哪些问题。
物理页管理面临问题
物理内存页分配会出现外部碎片和内部碎片问题,所谓的「内部」和「外部」是针对「页框内外」而言,一个页框内的内存碎片是内部碎片,多个页框间的碎片是外部碎片。
外部碎片
当需要分配大块内存的时候,要用好几页组合起来才够,而系统分配物理内存页的时候会尽量分配连续的内存页面,频繁的分配与回收物理页导致大量的小块内存夹杂在已分配页面中间,形成外部碎片,举个例子:
内部碎片
物理内存是按页来分配的,这样当实际只需要很小内存的时候,也会分配至少是 4K 大小的页面,而内核中有很多需要以字节为单位分配内存的场景,这样本来只想要几个字节而已却不得不分配一页内存,除去用掉的字节剩下的就形成了内部碎片。
页面管理算法
方法总比困难多,因为存在上面的这些问题,聪明的程序员灵机一动,引入了页面管理算法来解决上述的碎片问题。
Buddy(伙伴)分配算法
Linux 内核引入了伙伴系统算法(Buddy system),什么意思呢?就是把相同大小的页框块用链表串起来,页框块就像手拉手的好伙伴,也是这个算法名字的由来。
具体的,所有的空闲页框分组为11个块链表,每个块链表分别包含大小为1,2,4,8,16,32,64,128,256,512和1024个连续页框的页框块。最大可以申请1024个连续页框,对应4MB大小的连续内存。
因为任何正整数都可以由 2^n 的和组成,所以总能找到合适大小的内存块分配出去,减少了外部碎片产生 。
分配实例
比如:我需要申请4个页框,但是长度为4个连续页框块链表没有空闲的页框块,伙伴系统会从连续8个页框块的链表获取一个,并将其拆分为两个连续4个页框块,取其中一个,另外一个放入连续4个页框块的空闲链表中。释放的时候会检查,释放的这几个页框前后的页框是否空闲,能否组成下一级长度的块。
命令查看
[lemon]]# cat /proc/buddyinfo
Node 0, zone DMA 1 0 0 0 2 1 1 0 1 1 3
Node 0, zone DMA32 3198 4108 4940 4773 4030 2184 891 180 67 32 330
Node 0, zone Normal 42438 37404 16035 4386 610 121 22 3 0 0 1
1
2
3
4
5
slab分配器
看到这里你可能会想,有了伙伴系统这下总可以管理好物理内存了吧?不,还不够,否则就没有slab分配器什么事了。
那什么是slab分配器呢?
一般来说,内核对象的生命周期是这样的:分配内存-初始化-释放内存,内核中有大量的小对象,比如文件描述结构对象、任务描述结构对象,如果按照伙伴系统按页分配和释放内存,对小对象频繁的执行「分配内存-初始化-释放内存」会非常消耗性能。
伙伴系统分配出去的内存还是以页框为单位,而对于内核的很多场景都是分配小片内存,远用不到一页内存大小的空间。slab分配器,通过将内存按使用对象不同再划分成不同大小的空间,应用于内核对象的缓存。
伙伴系统和slab不是二选一的关系,slab 内存分配器是对伙伴分配算法的补充。
大白话说原理
对于每个内核中的相同类型的对象,如:task_struct、file_struct 等需要重复使用的小型内核数据对象,都会有个 slab 缓存池,缓存住大量常用的「已经初始化」的对象,每当要申请这种类型的对象时,就从缓存池的slab 列表中分配一个出去;而当要释放时,将其重新保存在该列表中,而不是直接返回给伙伴系统,从而避免内部碎片,同时也大大提高了内存分配性能。
主要优点
slab 内存管理基于内核小对象,不用每次都分配一页内存,充分利用内存空间,避免内部碎片。
slab 对内核中频繁创建和释放的小对象做缓存,重复利用一些相同的对象,减少内存分配次数。
数据结构
kmem_cache 是一个cache_chain 的链表组成节点,代表的是一个内核中的相同类型的「对象高速缓存」,每个kmem_cache 通常是一段连续的内存块,包含了三种类型的 slabs 链表:
slabs_full (完全分配的 slab 链表)
slabs_partial (部分分配的slab 链表)
slabs_empty ( 没有被分配对象的slab 链表)
kmem_cache 中有个重要的结构体 kmem_list3 包含了以上三个数据结构的声明。
slab 是slab 分配器的最小单位,在实现上一个 slab 有一个或多个连续的物理页组成(通常只有一页)。单个slab可以在 slab 链表之间移动,例如如果一个「半满slabs_partial链表」被分配了对象后变满了,就要从 slabs_partial 中删除,同时插入到「全满slabs_full链表」中去。内核slab对象的分配过程是这样的:
如果slabs_partial链表还有未分配的空间,分配对象,若分配之后变满,移动 slab 到slabs_full 链表
如果slabs_partial链表没有未分配的空间,进入下一步
如果slabs_empty 链表还有未分配的空间,分配对象,同时移动slab进入slabs_partial链表
如果slabs_empty为空,请求伙伴系统分页,创建一个新的空闲slab, 按步骤 3 分配对象
命令查看
上面说的都是理论,比较抽象,动动手来康康系统中的 slab 吧!你可以通过 cat /proc/slabinfo 命令,实际查看系统中slab 信息。
slabtop 实时显示内核 slab 内存缓存信息。
slab高速缓存的分类
slab高速缓存分为两大类,「通用高速缓存」和「专用高速缓存」。
通用高速缓存
slab分配器中用 kmem_cache 来描述高速缓存的结构,它本身也需要 slab 分配器对其进行高速缓存。cache_cache 保存着对「高速缓存描述符的高速缓存」,是一种通用高速缓存,保存在cache_chain 链表中的第一个元素。
另外,slab 分配器所提供的小块连续内存的分配,也是通用高速缓存实现的。通用高速缓存所提供的对象具有几何分布的大小,范围为32到131072字节。内核中提供了 kmalloc() 和 kfree() 两个接口分别进行内存的申请和释放。
专用高速缓存
内核为专用高速缓存的申请和释放提供了一套完整的接口,根据所传入的参数为制定的对象分配slab缓存。
专用高速缓存的申请和释放
kmem_cache_create() 用于对一个指定的对象创建高速缓存。它从 cache_cache 普通高速缓存中为新的专有缓存分配一个高速缓存描述符,并把这个描述符插入到高速缓存描述符形成的 cache_chain 链表中。kmem_cache_destory() 用于撤消和从 cache_chain 链表上删除高速缓存。
slab的申请和释放
slab 数据结构在内核中的定义,如下:
kmem_cache_alloc() 在其参数所指定的高速缓存中分配一个slab,对应的 kmem_cache_free() 在其参数所指定的高速缓存中释放一个slab。
虚拟内存分配
前面讨论的都是对物理内存的管理,Linux 通过虚拟内存管理,欺骗了用户程序假装每个程序都有 4G 的虚拟内存寻址空间(如果这里不懂我说啥,建议回头看下 别再说你不懂Linux内存管理了,10张图给你安排的明明白白!)。
所以我们来研究下虚拟内存的分配,这里包括用户空间虚拟内存和内核空间虚拟内存。
注意,分配的虚拟内存还没有映射到物理内存,只有当访问申请的虚拟内存时,才会发生缺页异常,再通过上面介绍的伙伴系统和 slab 分配器申请物理内存。
用户空间内存分配
malloc
malloc 用于申请用户空间的虚拟内存,当申请小于 128KB 小内存的时,malloc使用 sbrk或brk 分配内存;当申请大于 128KB 的内存时,使用 mmap 函数申请内存;
存在问题
由于 brk/sbrk/mmap 属于系统调用,如果每次申请内存都要产生系统调用开销,cpu 在用户态和内核态之间频繁切换,非常影响性能。
而且,堆是从低地址往高地址增长,如果低地址的内存没有被释放,高地址的内存就不能被回收,容易产生内存碎片。
解决
因此,malloc采用的是内存池的实现方式,先申请一大块内存,然后将内存分成不同大小的内存块,然后用户申请内存时,直接从内存池中选择一块相近的内存块分配出去。
内核空间内存分配
在讲内核空间内存分配之前,先来回顾一下内核地址空间。kmalloc 和 vmalloc 分别用于分配不同映射区的虚拟内存。
kmalloc
kmalloc() 分配的虚拟地址范围在内核空间的「直接内存映射区」。
按字节为单位虚拟内存,一般用于分配小块内存,释放内存对应于 kfree ,可以分配连续的物理内存。函数原型在
还记得前面说的 slab 吗?kmalloc 是基于slab 分配器的 ,同样可以用cat /proc/slabinfo 命令,查看 kmalloc 相关 slab 对象信息,下面的 kmalloc-8、kmalloc-16 等等就是基于slab分配的 kmalloc 高速缓存。
vmalloc
vmalloc 分配的虚拟地址区间,位于 vmalloc_start 与 vmalloc_end 之间的「动态内存映射区」。
一般用分配大块内存,释放内存对应于 vfree,分配的虚拟内存地址连续,物理地址上不一定连续。函数原型在
下面的图总结了上述两种内核空间虚拟内存分配方式。
总结一下
这是Linux内存管理系列文章的下篇,强烈建议阅读过程中有不清楚的同学,先去看看我之前写的 别再说你不懂Linux内存管理了,10张图给你安排的明明白白!,写到这里Linux 内存管理专题告一段落,我分享的这些知识很基础,基础到日常开发工作几乎用不上,但我认为每个在Linux下开发人员都应该了解。
我知道有些面试官喜欢在面试的时候考察一下,或多或少反应候选人基础素养,这两篇文章的内容也足够应付面试。还是那句话,Linxu 内存管理太复杂,不是一两篇文章能讲的清楚,但至少要有宏观意识,不至于一问三不知,如果你想深入了解原理,强烈建议从书中并结合内核源码学习,每天进步一点点,我们的目标是星辰大海。
本文创作过程我也画了大量的示例图解,可以作为知识索引,个人感觉看图还是比看文字更清晰明了,你可以在我公众号「后端技术学堂」后台回复「内存管理」获取这些图片的高清原图。
老规矩,感谢各位的阅读,文章的目的是分享对知识的理解,技术类文章我都会反复
————————————————
版权声明:本文为CSDN博主「程序员柠檬」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/u011644231/article/details/105837551
前面已经分析把物理内存添加到memblock以及给物理内存建立页表映射,这里我们分析内存模型。在Linux内核中支持3种内存模型,分别为
flat memory model
Discontiguous memory model
sparse memory model
所谓memory model,其实就是从cpu的角度看,其物理内存的分布情况,在linux kernel中,使用什么的方式来管理这些物理内存。某些体系架构支持多种内存模型,但在内核编译构建时只能选择使用一种内存模型。
1. 基本概念
1.1 page frame
从虚拟地址到物理地址的映射过程,系统对于内存管理是以页为单位进行管理的。在linux操作系统中,物理内存是按照page size来管理的,具体page size是多少是和硬件以及linux系统配置相关的,4k是最经典的设定。因此,对于物理内存,我们将其分成一个个按page size排列的page,每一个物理内存中的page size的内存区域我们称之page frame。page frame是系统内存的最小单位,对内存中的每个页都会创建struct page实例。
1.2 PFN
对于一个计算机系统,其整个物理地址空间应该是从0开始,到实际系统能支持的最大物理空间为止的一段地址空间。在ARM系统中,假设物理地址是32个bit,那么其物理地址空间就是4G,在ARM64系统中,如果支持的物理地址bit数目是48个,那么其物理地址空间就是256T。当然,实际上这么大的物理地址空间并不是都用于内存,有些也属于I/O空间(当然,有些cpu arch有自己独立的io address space)。因此,内存所占据的物理地址空间应该是一个有限的区间,不可能覆盖整个物理地址空间。
PFN是page frame number的缩写,所谓page frame,就是针对物理内存而言的,把物理内存分成一个个固定长度为page size的区域,并且给每一个page 编号,这个号码就是PFN。与page frame的转换关系如下图所示
1.3 NUMA
在多核的系统设计中内存的架构有两种类型计算机,分别以不同的方式管理物理内存。
UMA计算机(一致内存访问,uniform memory access):将可用内存以连续方式组织起来,系统中所有的处理器共享一个统一的,一致的物理内存空间,无论从哪个处理器发起访问,对内存的访问时间都是一样快。其架构图如下图所示
NUMA计算机(非一致内存访问,non-uniform memory access):每个 CPU 都有自己的本地内存,CPU 访问本地内存不用过总线,因而速度要快很多,每个 CPU 和内存在一起,称为一个 NUMA 节点。但是,在本地内存不足的情况下,每个 CPU 都可以去另外的 NUMA 节点申请内存,这个时候访问延时就会比较长。
从图中可以看出,每个CPU访问local memory,速度更快,延迟更小。当然,整体的内存构成一个内存池,CPU也能访问remote memory,相对来说速度更慢,延迟更大。目前对NUMA的了解仅限于此,在内核中会遇到相关的代码,大概知道属于什么范畴就可以了。
2. linux内存模型
Linux提供了三种内存模型(include/asm-generic/memory_model.h),一般处理器架构支持一种或者多种内存模型,这个在编译阶段就已经确定,比如目前在ARM64中,使用的Sparse Memory Model。
2.1 FLAT memory model(平坦内存模型)
如果从系统中任意一个CPU的角度来看,当它访问物理内存的时候,物理地址空间是一个连续的,没有空洞的地址空间,那么这种计算机系统的内存模型就是Flat memory。
早期的系统物理内存不大,那个时候Linux使用平坦内存模型(flat memory model)来管理物理内存就足够有效了。一个page frame用一个struct page结构体表示,整个物理内存可以用一个由所有struct page构成的数组mem_map表示,而经过页表查找得到的PFN,正好可以用来做这个数组的小标,__pfn_to_page()函数就是专门来完成这个功能的。
#define __pfn_to_page(pfn) (mem_map + ((pfn) - ARCH_PFN_OFFSET))
1
对于FLATMEM来说,物理内存本身是连续的,如果不连续的话,那么中间一部分物理地址是没有对应的物理内存,就会形成一个个洞,这就浪费了mem_map数组本身占用的内存空间。对于这种模型,其特点如下:
内存连续且不存在空隙
这种在大多数情况下,应用于UMA系统“Uniform Memory Access”。
通过CONFIG_FLATMEM配置
2.2 discontiguous memory model (不连续内存模型)
如果CPU在访问物理内存的时候,其地址空间是有一些空洞的,是不连续的,那么这种计算机系统的内存模型就是Discontiguous memory。在什么情况下物理内存是不连续的呢?当NUMA出现后,为了有效的管理NUMA模式的物理内存,一种被称为不连续内存模型的实现于1999年被引入linux系统中。在这中模型中,NUMA中的每个Node用一个叫做pglist_data的结构体表示。
应对不连续物理内存的问题似乎是解决了,可是现在你给我一个物理page的地址,使用DISCONTIGMEM的话,我怎么知道这个page是属于哪个node的呢,PFN中可没有包含node编号啊。pfn_to_page()之前干的活多轻松啊,就是索引下数组就得到数组元素struct page了,现在PFN和page之间的对应关系不是那么直接了,pfn_to_page的任务就开始重起来了。
#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);\
})
1
2
3
4
5
物理内存存在空洞,随着Sparse Memory的提出,这种内存模型也逐渐被弃用了。这种内存模型有以下的特点
多个内存节点不连续并且存在空隙"hole"
适用于UMA系统和NUMA系统
ARM在2010年已经移除了对DISCONTIGMEM的支持
通过CONFIG_CONTIGMEM配置
2.3 sparse memory model(稀疏内存模型)
内存模型是一个逐渐演化的过程,刚开始的时候,由于内存比较小,使用flat memory模型去抽象一个连续的内存地址空间。但是出现了NUMA架构之后,整个不连续的内存空间被分配成若干个node,每个node上是连续的内存地址空间,为了有效的管理NUMA模型下的物理内存,就开始使用discontiguous memory model。为了解决DISCONTIGMEM存在的弊端,一种新的稀疏内存模型被使用出来。
在sparse memory内存模型下,连续的地址空间按照SECTION被分成一段一段的,其中每一个section都是Hotplug的,因此sparse memory下,内存地址空间可以被切分的更细,支持更离散的Discontiguous memory。在SPARSEMEM中,被管理的物理内存由一个个任意大小的section(struct mem_section表示)构成,因此整个物理内存可被视为一个mem_section数组。每个mem_section包含了一个间接指向struct page数组的指针。
其主要的特点如下:
多个内存区域不连续并且存在空隙
支持内存热插拔(hot plug memory),但性能稍逊色于DISCONTIGMEM
在x86或ARM64内存采用该中模型,其性能比DISCONTIGMEM更优并且与FLATMEM相当
对于ARM64平台默认选择该内存模型
以section为单位管理online和hot-plug内存
通过CONFIG_SPARSEMEM配置
section大小从几十MiB到几GiB不等,取决于体系架构和内核的配置。通常在系统配置中将内存扩展单元「memory expansion unit」用作section大小。比如,如果系统内存可扩展至64GiB,并且最小内存扩展单元为1GiB,则设置section大小也为1GiB。当使用Linux系统作为hypervisor的客户操作系统「guest OS」,也是以section大小为单元在运行时向Linux系统增添内存和移除Linux系统的内存。
3. 平台内存模型支持
Linux支持的各种不同体系结构在内存管理方面差别很大,以下是主流的架构支持情况如下表所示,一个体系架构中可能有多种内存模型可用(ARM64只支持一种内存模型),通过可选的内核配置选项来决定使用哪种内存模型。
系统架构 | FLATMEM | DISCONTIGMEM | SPARSEMEM |
---|---|---|---|
ARM | 默认 | 不支持 | 某些系统可选配置 |
ARM64 | 不支持 | 不支持 | 默认 |
x86_32 | 默认 | 不支持 | 可配置 |
x86_32(NUMA) | 不支持 | 默认 | 可配置 |
x86_64 | 不支持 | 不支持 | 默认 |
x86_64(NUMA) | 不支持 | 不支持 | 默认 |
4.小结
这章我们学习了3种内核模型的各自原理和特点,同时我们简单介绍linux kernel,对于这三种模型使用什么样的方式来管理这些物理内存,后面的章节中会针对FLAT(ARM)和SPARSE(ARM64)模型做相应的介绍。
上一章我们梳理了Node, Zone, Page Frame的整个流程,本章就来整理其关系和数据结构之间的关系。
1. 基本概念
NUMA(Non-Uniform Memory Access,非统一内存访问)和UMA(Uniform Memory Access,统一内存访问):
NUMA是从处理器对内存访问速度不同的结构
UMA是处理器与所有内存的访问速度相同的结构
结点Node:
从1个CPU访问速度相同的内存集合
每个CPU对应一个本地物理内存
在内核中用pg_data_t类型,表示节点的结构体成为节点描述符
ZONE:
节点中具有相同属性的区域
内核使用struct zone结构体管理
页帧Page:
ZONE中管理物理内存的最小单位称为页帧
页帧在Linux中由page结构体管理,通过mem_map全局数据访问
Linux采用Node、Zone和页三级结构来描述物理内存的,如图所示
2. 结点
Linux采用一个struct pg_data_t结构体来描述系统的内存,每一个Node都对应一个struct pglist_data,系统中每个节点都挂接在一个pgdat_list列表中,对于NUMA系统中一个,使用的全局变量struct pglist_data __refdata contig_page_data。
下面就对结构体提的主要域进行说明
结构体成员变量 说明
node_zones 该结点的zone类型,一般包括ZONE_HIGHMEM、ZONE_NORMAL和ZONE_DMA三类,包含了节点中各个内存域的数据结构
node_zonelists 指定了备用节点及其内存域列表,以便在当前结点没有可用空间时,在备用结点分配内存
nr_zones 该结点的 zone 个数,可以从 1 到 3,但并不是所有的结点都需要有 3 个 zone
node_mem_map 它是 struct page 数组的第一页,该数组表示结点中的每个物理页框。根据该结点在系统中的顺序,它可在全局 mem_map 数组中的某个位置
node_start_pfn 当前NUMA节点第一页帧逻辑编号。在UMA总是0.
node_present_pages 总共可用的页面数
node_spanned_pages 总共的页面数,包括有空洞的区域
kswapd 页面回收进程
3. ZONE
每个结点的内存被分为多个块,称为zones,它表示内存中一段区域。一个zone用struct_zone_t结构描述,zone的类型主要有ZONE_DMA、ZONE_NORMAL和ZONE_HIGHMEM。ZONE_DMA位于低端的内存空间,用于某些旧的ISA设备,ISA总线的直接内存存储DMA,只能对RAM的前16MB进行寻址。ZONE_NORMAL的内存直接映射到Linux内核线性地址空间的高端部分,许多内核操作只能在ZONE_NORMAL中进行。因此对于内核来说, 不同范围的物理内存采用不同的管理方式和映射方式,Linux使用enum zone_type来标记内核所支持的所有内存区域
enum zone_type
{
#ifdef CONFIG_ZONE_DMA
ZONE_DMA,
#endif
#ifdef CONFIG_ZONE_DMA32
ZONE_DMA32,
#endif
ZONE_NORMAL,
#ifdef CONFIG_HIGHMEM
ZONE_HIGHMEM,
#endif
ZONE_MOVABLE,
#ifdef CONFIG_ZONE_DEVICE
ZONE_DEVICE,
#endif
__MAX_NR_ZONES
}
其定义如下表所示
管理内存域 描述
ZONE_DMA 标记了适合DMA的内存域.该区域的长度依赖于处理器类型.这是由于古老的ISA设备强加的边界. 但是为了兼容性,现代的计算机也可能受此影响
ZONE_DMA32 标记了使用32位地址字可寻址,适合DMA的内存域,在32位系统中,本区域是空的, 即长度为0MB,在Alpha和AMD64系统上,该内存的长度可能是从0到4GB
ZONE_NORMAL 标记了可直接映射到内存段的普通内存域.这是在所有体系结构上保证会存在的唯一内存区域
ZONE_HIGHMEM 标记了超出内核虚拟地址空间的物理内存段,因此这段地址不能被内核直接映射
ZONE_MOVABLE 内核定义了一个伪内存域ZONE_MOVABLE,在防止物理内存碎片的机制memory migration中需要使用该内存域.供防止物理内存碎片的极致使用
ZONE_DEVIC 为支持热插拔设备而分配的Non Volatile Memory非易失性内存
Zone是用struct zone_t描述的,它跟踪页框使用、空闲区域和锁等信息,结构体中主要域说明如下
结构体成员变量 说明
watermark 水位值,WMARK_MIN/WMARK_LOV/WMARK_HIGH,页面分配器和kswapd页面回收中会用到
lowmem_reserve zone中预留的内存,用于一些无论如何都不能失败的关键性内存分配
zone_pgdat 执行所属的pglist_data
pageset Per-CPU上的页面,减少自旋锁的争用
zone_start_pfn ZONE的起始内存页面帧号
managed_pages 被Buddy System管理的页面数量
spanned_pages ZONE中总共的页面数,包含空洞的区域
present_pages ONE里实际管理的页面数量
struct free_area free_area[MAX_ORDER]; 管理空闲页面的列表
当系统中可用的内存比较少时,kswapd将被唤醒,并进行页交换。如果需要内存的压力非常大,进程将同步释放内存。每个zone有三个阙值,成为pages_low/pages_min/pages_high,用于跟踪该zone的内存压力。
pages_min的页框数是由内存初始化free_area_init_core函数,根据zone内页框的比例计算,最小为20页,最大一般为255页
当到达pages_min时,说明页面数非常紧张,分配页面的动作和kswapd线程同步运行
当空闲也的数目达到pages_low时,说明页面刚开始紧张,则kswapd线程将被唤醒,并开始释放回收页面
当达到pages_high时,说明内存页面数很充足,不需要回收,kswapd线程将重新休眠,通常这个数值是page_min的3倍
4. page
每个物理页框都需要一个对应的page结构来进行管理,记录分配状态,分配和回收,互斥以及同步存在。 因为内核会为每一个物理页帧创建一个struct page的结构体,因此要保证page结构体足够的小,否则仅struct page就要占用大量的内存。出于节省内存的考虑,struct page中使用了大量的联合体union。
5. 总结
本章梳理了Node, Zone, Page Frame各个数据结构的成员变量和关系,对于Linux中管理内存的各个结构体之间的关系图如下图所示
前面已经分析把物理内存添加到memblock以及给物理内存建立页表映射,这里我们分析内存模型。在Linux内核中支持3种内存模型,分别为
flat memory model
Discontiguous memory model
sparse memory model
所谓memory model,其实就是从cpu的角度看,其物理内存的分布情况,在linux kernel中,使用什么的方式来管理这些物理内存。某些体系架构支持多种内存模型,但在内核编译构建时只能选择使用一种内存模型。
1. 基本概念
1.1 page frame
从虚拟地址到物理地址的映射过程,系统对于内存管理是以页为单位进行管理的。在linux操作系统中,物理内存是按照page size来管理的,具体page size是多少是和硬件以及linux系统配置相关的,4k是最经典的设定。因此,对于物理内存,我们将其分成一个个按page size排列的page,每一个物理内存中的page size的内存区域我们称之page frame。page frame是系统内存的最小单位,对内存中的每个页都会创建struct page实例。
1.2 PFN
对于一个计算机系统,其整个物理地址空间应该是从0开始,到实际系统能支持的最大物理空间为止的一段地址空间。在ARM系统中,假设物理地址是32个bit,那么其物理地址空间就是4G,在ARM64系统中,如果支持的物理地址bit数目是48个,那么其物理地址空间就是256T。当然,实际上这么大的物理地址空间并不是都用于内存,有些也属于I/O空间(当然,有些cpu arch有自己独立的io address space)。因此,内存所占据的物理地址空间应该是一个有限的区间,不可能覆盖整个物理地址空间。
PFN是page frame number的缩写,所谓page frame,就是针对物理内存而言的,把物理内存分成一个个固定长度为page size的区域,并且给每一个page 编号,这个号码就是PFN。与page frame的转换关系如下图所示
1.3 NUMA
在多核的系统设计中内存的架构有两种类型计算机,分别以不同的方式管理物理内存。
UMA计算机(一致内存访问,uniform memory access):将可用内存以连续方式组织起来,系统中所有的处理器共享一个统一的,一致的物理内存空间,无论从哪个处理器发起访问,对内存的访问时间都是一样快。其架构图如下图所示
NUMA计算机(非一致内存访问,non-uniform memory access):每个 CPU 都有自己的本地内存,CPU 访问本地内存不用过总线,因而速度要快很多,每个 CPU 和内存在一起,称为一个 NUMA 节点。但是,在本地内存不足的情况下,每个 CPU 都可以去另外的 NUMA 节点申请内存,这个时候访问延时就会比较长。
从图中可以看出,每个CPU访问local memory,速度更快,延迟更小。当然,整体的内存构成一个内存池,CPU也能访问remote memory,相对来说速度更慢,延迟更大。目前对NUMA的了解仅限于此,在内核中会遇到相关的代码,大概知道属于什么范畴就可以了。
2. linux内存模型
Linux提供了三种内存模型(include/asm-generic/memory_model.h),一般处理器架构支持一种或者多种内存模型,这个在编译阶段就已经确定,比如目前在ARM64中,使用的Sparse Memory Model。
2.1 FLAT memory model(平坦内存模型)
如果从系统中任意一个CPU的角度来看,当它访问物理内存的时候,物理地址空间是一个连续的,没有空洞的地址空间,那么这种计算机系统的内存模型就是Flat memory。
早期的系统物理内存不大,那个时候Linux使用平坦内存模型(flat memory model)来管理物理内存就足够有效了。一个page frame用一个struct page结构体表示,整个物理内存可以用一个由所有struct page构成的数组mem_map表示,而经过页表查找得到的PFN,正好可以用来做这个数组的小标,__pfn_to_page()函数就是专门来完成这个功能的。
#define __pfn_to_page(pfn) (mem_map + ((pfn) - ARCH_PFN_OFFSET))
1
对于FLATMEM来说,物理内存本身是连续的,如果不连续的话,那么中间一部分物理地址是没有对应的物理内存,就会形成一个个洞,这就浪费了mem_map数组本身占用的内存空间。对于这种模型,其特点如下:
内存连续且不存在空隙
这种在大多数情况下,应用于UMA系统“Uniform Memory Access”。
通过CONFIG_FLATMEM配置
2.2 discontiguous memory model (不连续内存模型)
如果CPU在访问物理内存的时候,其地址空间是有一些空洞的,是不连续的,那么这种计算机系统的内存模型就是Discontiguous memory。在什么情况下物理内存是不连续的呢?当NUMA出现后,为了有效的管理NUMA模式的物理内存,一种被称为不连续内存模型的实现于1999年被引入linux系统中。在这中模型中,NUMA中的每个Node用一个叫做pglist_data的结构体表示。
应对不连续物理内存的问题似乎是解决了,可是现在你给我一个物理page的地址,使用DISCONTIGMEM的话,我怎么知道这个page是属于哪个node的呢,PFN中可没有包含node编号啊。pfn_to_page()之前干的活多轻松啊,就是索引下数组就得到数组元素struct page了,现在PFN和page之间的对应关系不是那么直接了,pfn_to_page的任务就开始重起来了。
#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);\
})
1
2
3
4
5
物理内存存在空洞,随着Sparse Memory的提出,这种内存模型也逐渐被弃用了。这种内存模型有以下的特点
多个内存节点不连续并且存在空隙"hole"
适用于UMA系统和NUMA系统
ARM在2010年已经移除了对DISCONTIGMEM的支持
通过CONFIG_CONTIGMEM配置
2.3 sparse memory model(稀疏内存模型)
内存模型是一个逐渐演化的过程,刚开始的时候,由于内存比较小,使用flat memory模型去抽象一个连续的内存地址空间。但是出现了NUMA架构之后,整个不连续的内存空间被分配成若干个node,每个node上是连续的内存地址空间,为了有效的管理NUMA模型下的物理内存,就开始使用discontiguous memory model。为了解决DISCONTIGMEM存在的弊端,一种新的稀疏内存模型被使用出来。
在sparse memory内存模型下,连续的地址空间按照SECTION被分成一段一段的,其中每一个section都是Hotplug的,因此sparse memory下,内存地址空间可以被切分的更细,支持更离散的Discontiguous memory。在SPARSEMEM中,被管理的物理内存由一个个任意大小的section(struct mem_section表示)构成,因此整个物理内存可被视为一个mem_section数组。每个mem_section包含了一个间接指向struct page数组的指针。
其主要的特点如下:
多个内存区域不连续并且存在空隙
支持内存热插拔(hot plug memory),但性能稍逊色于DISCONTIGMEM
在x86或ARM64内存采用该中模型,其性能比DISCONTIGMEM更优并且与FLATMEM相当
对于ARM64平台默认选择该内存模型
以section为单位管理online和hot-plug内存
通过CONFIG_SPARSEMEM配置
section大小从几十MiB到几GiB不等,取决于体系架构和内核的配置。通常在系统配置中将内存扩展单元「memory expansion unit」用作section大小。比如,如果系统内存可扩展至64GiB,并且最小内存扩展单元为1GiB,则设置section大小也为1GiB。当使用Linux系统作为hypervisor的客户操作系统「guest OS」,也是以section大小为单元在运行时向Linux系统增添内存和移除Linux系统的内存。
3. 平台内存模型支持
Linux支持的各种不同体系结构在内存管理方面差别很大,以下是主流的架构支持情况如下表所示,一个体系架构中可能有多种内存模型可用(ARM64只支持一种内存模型),通过可选的内核配置选项来决定使用哪种内存模型。
系统架构 FLATMEM DISCONTIGMEM SPARSEMEM
ARM 默认 不支持 某些系统可选配置
ARM64 不支持 不支持 默认
x86_32 默认 不支持 可配置
x86_32(NUMA) 不支持 默认 可配置
x86_64 不支持 不支持 默认
x86_64(NUMA) 不支持 不支持 默认
4.小结
这章我们学习了3种内核模型的各自原理和特点,同时我们简单介绍linux kernel,对于这三种模型使用什么样的方式来管理这些物理内存,后面的章节中会针对FLAT(ARM)和SPARSE(ARM64)模型做相应的介绍。
上一章我们梳理了Node, Zone, Page Frame的整个流程,本章就来整理其关系和数据结构之间的关系。
1. 基本概念
NUMA(Non-Uniform Memory Access,非统一内存访问)和UMA(Uniform Memory Access,统一内存访问):
NUMA是从处理器对内存访问速度不同的结构
UMA是处理器与所有内存的访问速度相同的结构
结点Node:
从1个CPU访问速度相同的内存集合
每个CPU对应一个本地物理内存
在内核中用pg_data_t类型,表示节点的结构体成为节点描述符
ZONE:
节点中具有相同属性的区域
内核使用struct zone结构体管理
页帧Page:
ZONE中管理物理内存的最小单位称为页帧
页帧在Linux中由page结构体管理,通过mem_map全局数据访问
Linux采用Node、Zone和页三级结构来描述物理内存的,如图所示
2. 结点
Linux采用一个struct pg_data_t结构体来描述系统的内存,每一个Node都对应一个struct pglist_data,系统中每个节点都挂接在一个pgdat_list列表中,对于NUMA系统中一个,使用的全局变量struct pglist_data __refdata contig_page_data。
下面就对结构体提的主要域进行说明
结构体成员变量 说明
node_zones 该结点的zone类型,一般包括ZONE_HIGHMEM、ZONE_NORMAL和ZONE_DMA三类,包含了节点中各个内存域的数据结构
node_zonelists 指定了备用节点及其内存域列表,以便在当前结点没有可用空间时,在备用结点分配内存
nr_zones 该结点的 zone 个数,可以从 1 到 3,但并不是所有的结点都需要有 3 个 zone
node_mem_map 它是 struct page 数组的第一页,该数组表示结点中的每个物理页框。根据该结点在系统中的顺序,它可在全局 mem_map 数组中的某个位置
node_start_pfn 当前NUMA节点第一页帧逻辑编号。在UMA总是0.
node_present_pages 总共可用的页面数
node_spanned_pages 总共的页面数,包括有空洞的区域
kswapd 页面回收进程
3. ZONE
每个结点的内存被分为多个块,称为zones,它表示内存中一段区域。一个zone用struct_zone_t结构描述,zone的类型主要有ZONE_DMA、ZONE_NORMAL和ZONE_HIGHMEM。ZONE_DMA位于低端的内存空间,用于某些旧的ISA设备,ISA总线的直接内存存储DMA,只能对RAM的前16MB进行寻址。ZONE_NORMAL的内存直接映射到Linux内核线性地址空间的高端部分,许多内核操作只能在ZONE_NORMAL中进行。因此对于内核来说, 不同范围的物理内存采用不同的管理方式和映射方式,Linux使用enum zone_type来标记内核所支持的所有内存区域
enum zone_type
{
#ifdef CONFIG_ZONE_DMA
ZONE_DMA,
#endif
#ifdef CONFIG_ZONE_DMA32
ZONE_DMA32,
#endif
ZONE_NORMAL,
#ifdef CONFIG_HIGHMEM
ZONE_HIGHMEM,
#endif
ZONE_MOVABLE,
#ifdef CONFIG_ZONE_DEVICE
ZONE_DEVICE,
#endif
__MAX_NR_ZONES
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
其定义如下表所示
管理内存域 描述
ZONE_DMA 标记了适合DMA的内存域.该区域的长度依赖于处理器类型.这是由于古老的ISA设备强加的边界. 但是为了兼容性,现代的计算机也可能受此影响
ZONE_DMA32 标记了使用32位地址字可寻址,适合DMA的内存域,在32位系统中,本区域是空的, 即长度为0MB,在Alpha和AMD64系统上,该内存的长度可能是从0到4GB
ZONE_NORMAL 标记了可直接映射到内存段的普通内存域.这是在所有体系结构上保证会存在的唯一内存区域
ZONE_HIGHMEM 标记了超出内核虚拟地址空间的物理内存段,因此这段地址不能被内核直接映射
ZONE_MOVABLE 内核定义了一个伪内存域ZONE_MOVABLE,在防止物理内存碎片的机制memory migration中需要使用该内存域.供防止物理内存碎片的极致使用
ZONE_DEVIC 为支持热插拔设备而分配的Non Volatile Memory非易失性内存
Zone是用struct zone_t描述的,它跟踪页框使用、空闲区域和锁等信息,结构体中主要域说明如下
结构体成员变量 说明
watermark 水位值,WMARK_MIN/WMARK_LOV/WMARK_HIGH,页面分配器和kswapd页面回收中会用到
lowmem_reserve zone中预留的内存,用于一些无论如何都不能失败的关键性内存分配
zone_pgdat 执行所属的pglist_data
pageset Per-CPU上的页面,减少自旋锁的争用
zone_start_pfn ZONE的起始内存页面帧号
managed_pages 被Buddy System管理的页面数量
spanned_pages ZONE中总共的页面数,包含空洞的区域
present_pages ONE里实际管理的页面数量
struct free_area free_area[MAX_ORDER]; 管理空闲页面的列表
当系统中可用的内存比较少时,kswapd将被唤醒,并进行页交换。如果需要内存的压力非常大,进程将同步释放内存。每个zone有三个阙值,成为pages_low/pages_min/pages_high,用于跟踪该zone的内存压力。
pages_min的页框数是由内存初始化free_area_init_core函数,根据zone内页框的比例计算,最小为20页,最大一般为255页
当到达pages_min时,说明页面数非常紧张,分配页面的动作和kswapd线程同步运行
当空闲也的数目达到pages_low时,说明页面刚开始紧张,则kswapd线程将被唤醒,并开始释放回收页面
当达到pages_high时,说明内存页面数很充足,不需要回收,kswapd线程将重新休眠,通常这个数值是page_min的3倍
4. page
每个物理页框都需要一个对应的page结构来进行管理,记录分配状态,分配和回收,互斥以及同步存在。 因为内核会为每一个物理页帧创建一个struct page的结构体,因此要保证page结构体足够的小,否则仅struct page就要占用大量的内存。出于节省内存的考虑,struct page中使用了大量的联合体union。
5. 总结
本章梳理了Node, Zone, Page Frame各个数据结构的成员变量和关系,对于Linux中管理内存的各个结构体之间的关系图如下图所示