3内存管理
1.内存中的物理内存页的管理
2.分配大块内存的伙伴系统
3.分配较小块内存的slab,slub,slob分配器
4.分配非连续内存块的vmalloc机制
5.进程的地址空间
Linux内核一般将处理器的虚拟地址空间划分为两个部分。底部比较大的部分用于用户进程,顶部则专用于内核。在IA-32系统上,地址空间在用户进程和内核之间划分的典型比例为3∶1。给出4 GiB虚拟地址空间,3 GiB将用于用户空间而1 GiB将用于内核。通过修改相关的配置选项
可以改变该比例。但只有对非常特殊的配置和应用程序,这种修改才会带来好处
可用的物理内存将映射到内核的地址空间中
。访问内存时,如果所用的虚拟地址与内核区域的起始地址之间的偏移量不超出可用物理内存的长度,那么该虚拟地址会自动关联到物理页帧。这是可行的,因为在采用该方案时,在内核区域中的内存分配总是落入到物理内存中
。不过,还有一个问题。虚拟地址空间的内核部分必然小于CPU理论地址空间的最大长度
。如果物理内存比可以映射到内核地址空间中的数量要多,那么内核必须借助于高端内存(highmem)方法来管理“多余的”内存
。在IA-32系统上,可以直接管理的物理内存数量不超过896 MiB。超过该值(直到最大4 GiB为止,2^32 = 4 GiB)的内存只能通过高端内存寻址。
两种计算机的内存管理:
内核会区分3种配置选项 : FLATMEM 、 DISCONTIGMEM 和 SPARSEMEM 。 SPARSEMEM和DISCONTIGMEM实际上作用相同,但从开发者的角度看来,对应代码的质量有所不同。SPARSEMEM被认为更多是试验性的,不那么稳定,但有一些性能优化。我们认为DISCONTIGMEM相关代码更稳定一些,但不具备内存热插拔之类的新特性
后面主要讨论FLATMEM。在大多数配置中都使用该内存组织类型,通常它也是内核的默认值
内存使用分配阶
3。它表示内存区中页的数目取以2为底的对数,n阶为2^n个页
(N)UMA模型中的内存组织
内存划分为结点
。每个结点关联到系统中的一个处理器,结构体为pg_data_t实例
各结点又划分为内存域
,是内存的进一步细分。例如,对可用于(ISA设备的)DMA操作的内存区是有限制的。只有前16 MiB适用,还有一个高端内存区域无法直接映射。在二者之间是通用的“普通”内存区。因此一个结点最多由3个内存域组成
,DMA内存域->普通内存域->高端内存域。内存域类型定义 enum zone_type
数据结构
内存结点中的内存域类型
高端内存域说明:
在x86结构中,三种类型的区域(从3G开始计算)如下:
- ZONE_DMA 内存开始的16MB
- ZONE_NORMAL 16MB~896MB
- ZONE_HIGHMEM 896MB ~ 结束(1G)
高端内存是指物理地址大于 896M 的内存(1024M-896M=128M)。这部分内存用于其他作用
因为“内核直接映射空间”最多只能从 3G 到 4G,只能直接映射 1G 物理内存,对于大于 1G 的物理内存,无能为力.为了访问大于1G的物理内存,内核将1G中后128M内存用于临时映射,每次要使用大于1G的内存部分时,将这128M内存的部分映射到大于1G的内存部分,使用完后再解除临时映射
高端内存的映射有三种方式:
- 映射到“内核动态映射空间”
通过vmalloc()
,在“内核动态映射空间”申请内存的时候,就可能从高端内存获得页面(参看 vmalloc 的实现),因此说高端内存有可能映射到“内核动态映射空间” 中 - 永久内核映射
如果是通过 alloc_page() 获得了高端内存对应的 page,如何给它找个线性空间?
内核专门为此留出一块线性空间,从PKMAP_BASE 到 FIXADDR_START
,用于映射高端内存。在 2.4 内核上,这个地址范围是 4G-8M 到 4G-4M 之间。这个空间起叫“内核永久映射空间”或者“永久内核映射空间”
这个空间和其它空间使用同样的页目录表,对于内核来说,就是swapper_pg_dir
,对普通进程来说,通过 CR3 寄存器指向。
通常情况下,这个空间是 4M 大小,因此仅仅需要一个页表即可,内核通过pkmap_page_table
来寻找这个页表。
通过kmap()
, 可以把一个 page 映射到这个空间来
由于这个空间是 4M 大小,最多能同时映射 1024 个 page。因此,对于不使用的的 page,应该及时从这个空间释放掉(也除映射关就是解系),通过 kunmap() ,可以把一个 page 对应的线性地址从这个空间释放出来。 - 临时内核映射(固定映射)
内核在FIXADDR_START 到 FIXADDR_TOP
之间保留了一些线性空间用于特殊需求。这个空间称为“固定映射空间”
在这个空间中,有一部分用于高端内存的临时映射。
这块空间具有如下特点:- 每个 CPU 占用一块空间
- 在每个 CPU 占用的那块空间中,又分为多个小空间,每个小空间大小是 1 个 page,每个小空间用于一个目的,这些目的定义在 kmap_types.h 中的 km_type 中。
当要进行一次临时映射的时候,需要指定映射的目的,根据映射目的,可以找到对应的小空间,然后把这个空间的地址作为映射地址。这意味着一次临时映射会导致以前的映射被覆盖。
通过kmap_atomic()
可实现临时映射。
内核通过内核页全局目录来管理所有的物理内存
,由于线性地址前3G空间为用户使用,内核页全局目录前768项(刚好3G,0xC0000000)除0、1两项外全部为0,后256项(1G)用来管理所有的物理内存。内核页全局目录在编译时静态地定义为 swapper_pg_dir 数组,该数组从物理内存地址0x101000处开始存放。
由图可见,内核线性地址空间部分从PAGE_OFFSET(通常定义为3G)开始,为了将内核装入内存,从PAGE_OFFSET开始8M线性地址用来映射内核所在的物理内存地址
(也可以说是内核所在虚拟地址是从PAGE_OFFSET开始的);接下来是mem_map数组,mem_map的起始线性地址与体系结构相关,比如对于UMA结构,由于从PAGE_OFFSET开始16M线性地址空间对应的16M物理地址空间是DMA区,mem_map数组通常开始于PAGE_OFFSET+16M的线性地址;从PAGE_OFFSET开始到VMALLOC_START – VMALLOC_OFFSET的线性地址空间直接映射到物理内存空间(一一对应影射,物理地址<==>线性地址-PAGE_OFFSET)
,这段区域的大小和机器实际拥有的物理内存大小有关,这儿VMALLOC_OFFSET在X86上为8M
,主要用来防止越界错误;在内存比较小的系统上,余下的线性地址空间(还要再减去空白区即VMALLOC_OFFSET)被vmalloc()函数用来把不连续的物理地址空间映射到连续的线性地址空间上
,在内存比较大的系统上,vmalloc()使用从VMALLOC_START到VMALLOC_END(也即PKMAP_BASE减去2页的空白页大小PAGE_SIZE(解释VMALLOC_END))的线性地址空间,此时余下的线性地址空间(还要再减去2页的空白区即VMALLOC_OFFSET)又可以分成2部分:第一部分从PKMAP_BASE到FIXADDR_START用来由kmap()函数来建立永久映射高端内存
;第二部分,从FIXADDR_START到FIXADDR_TOP,这是一个固定大小的临时映射线性地址空间
,(引用:Fixed virtual addresses are needed for subsystems that need to know the virtual address at compile time such as the APIC),在X86体系结构上,FIXADDR_TOP被静态定义为0xFFFFE000,此时这个固定大小空间结束于整个线性地址空间最后4K前面,该固定大小空间大小是在编译时计算出来并存储在__FIXADDR_SIZE变量中。
//include/linux/mmzone.h
//一个结点最多由3个内存域组成:DMA内存域->普通内存域->高端内存域
enum zone_type {
#ifdef CONFIG_ZONE_DMA
/* DMA内存域,该域长度依赖于处理器类型,在IA-32上限制16MiB,
* 该区域的物理页面专门供I/O设备的DMA使用,不经过MMU
* */
ZONE_DMA,
#endif
#ifdef CONFIG_ZONE_DMA32
/* 32位地址字可寻址,在64位机上使用,0-4GiB,在32位机上为0MiB */
ZONE_DMA32,
#endif
/* 可直接映射到内核段的普通内存域,所有体系结构上都会有的唯一内存域,但无法保证该地址范围对应了实际的物理内存。例如,如果AMD64系统有2 GiB内存,那么所有内存都属于ZONE_DMA32范围,而ZONE_NORMAL则为空 */
ZONE_NORMAL,
#ifdef CONFIG_HIGHMEM
/* 超出内核段的物理内存,
* 32位中内核虚拟地址为0xc0000000~0xffffffff,物理地址为0x00000000~1G
* 为了访问其他地址,将其他地址临时映射到这个域中,使用完后再释放
* */
ZONE_HIGHMEM,
#endif
/* 伪内存域,在防止物理内存碎片的机制中需要使用该内存域,虚拟内存域
* ZONE_MOVABLE 内存域可能位于高端或普通内存域,取决于体系结构和内核配置,值保存在movable_zone中
* find_zone_movable_pfns_for_nodes函数用于计算进入 ZONE_MOVABLE 的内存数量
* */
ZONE_MOVABLE,
/* 结束标记,在内核想要迭代系统中的所有内存域时,会用到该常量 */
MAX_NR_ZONES
};
各个内存域都关联了一个数组
,用来组织属于该内存域的物理内存页(内核中称之为页帧)。对每个页帧,都分配了一个struct page实例以及所需的管理数据
各个内存结点保存在一个单链表中,供内核遍历
出于性能考虑,在为进程分配内存时,内核总是试图在当前运行的CPU相关联的NUMA结点上进行。但这并不总是可行的,例如,该结点的内存可能已经用尽。对此类情况,每个结点都提供了一个备用列表(借助于struct zonelist)(pg_data_t->node_zonelists)
。该列表包含了其他结点(和相关的内存域),可用于代替当前结点分配内存。列表项的位置越靠后,就越不适合分配。在UMA系统上是何种情形呢?这里只有一个结点
cpu内存结点
//include/linux/mmzone.h
//内存节点结构体,每个cpu一个结点
typedef struct pglist_data {
/*内存域数据结构数组,总有3个(dma->普通->高端),如果结点没有那么多内存域,不足的数组填0*/
struct zone node_zones[MAX_NR_ZONES];
/*备用节点及其内存域的列表,在当前结点没有可用空间时,在备用结点分配内存(当前cpu没内存了,用别的cpu的内存)*/
struct zonelist node_zonelists[MAX_ZONELISTS];
/*结点中不同内存域的数目*/
int nr_zones;
# ifdef CONFIG_FLAT_NODE_MEM_MAP
/* struct page数组,用于管理该内存结点上所有内存,每个内存页一个该结构体成员,由alloc_node_mem_map函数初始化,,描述页帧结构应尽可能小,因为根据实际物理内存会有很多内存页帧
*/
struct page*node_mem_map;
# endif
/*指向自举内存分配器(boot memory allocator)数据结构的实例,内存管理子系统初始化之前给内核使用的内存*/
struct bootmem_data *bdata;
# ifdef CONFIG_MEMORY_HOTPLUG
/*
*Must be held any time you expect node_start_pfn, node_present_pages
* or node_spanned_pages stay constant. Holding this will also
*guarantee that any pfn_valid() stays that way.
*
*Nests above zone->lock and zone->size_seqlock.
*/
spinlock_t node_size_lock;
# endif
/*该NUMA结点第一个页帧的逻辑编号,系统中所有结点的页帧是依次编号的,每个页帧的号码都是全局唯一的(不只是结点内唯一).
* node_start_pfn 在UMA系统中总是0,因为其中只有一个结点,因此其第一个页帧编号总是0。
**/
unsigned long node_start_pfn;
/* 结点中实际可用的页帧的数目,页帧总数减去空洞内存页数 */
unsigned long node_present_pages; /*total number of physical pages*/
/* 结点中页帧的数目(包括空洞内存页数),该结点以页帧为单位计算的长度,因为结点之间可能有空洞,所以跟上一个的变量不一样 */
unsigned long node_spanned_pages; /*total size of physical page
range, including holes*/
/*全局节点ID,NUMA结点都是从0开始编号的*/
int node_id;
/*交换守护进程的等待队列swap daemon,在将页帧换出结点时会用到*/
wait_queue_head_t kswapd_wait;
/*指向负责该结点的交换守护进程*/
struct task_struct *kswapd;
/* 用于页交换子系统的实现,用来定义需要释放的区域的长度 */
int kswapd_max_order;
} pg_data_t;
结点状态管理,结点多于一个时才用
如果系统中结点多于一个,内核会维护一个位图,用以提供各个结点的状态信息。状态是用位图指定的,可使用下列值
//include/linux/nodemask.h
/*
* Bitmasks that are kept for all the nodes.
*/
/*
状态N_POSSIBLE、N_ONLINE和N_CPU用于CPU和内存的热插拔
如果结点有普通或高端内存则使用 N_HIGH_MEMORY ,仅当结点没有高端内存才设置 N_NORMAL_MEMORY
*/
enum node_states {
N_POSSIBLE, /* The node could become online at some point */
N_ONLINE, /* The node is online */
N_NORMAL_MEMORY, /* The node has regular memory */
#ifdef CONFIG_HIGHMEM
N_HIGH_MEMORY, /* The node has regular or high memory */
#else
N_HIGH_MEMORY = N_NORMAL_MEMORY,
#endif
N_CPU, /* The node has one or more cpus */
NR_NODE_STATES
};
内存域数据结构
该结构比较特殊的方面是它由ZONE_PADDING
分隔为几个部分。这是因为对zone结构的访问非常频繁。在多处理器系统上,通常会有不同的CPU试图同时访问结构成员。因此使用锁(见第5章)防止它们彼此干扰,避免错误和不一致。由于内核对该结构的访问非常频繁,因此会经常性地获取该结构的两个自旋锁zone->lock和zone->lru_lock
。
如果数据保存在CPU高速缓存中,那么会处理得更快速。高速缓存分为行,每一行负责不同的内存区。内核使用ZONE_PADDING宏生成“填充”字段添加到结构中
,以确保每个自旋锁都处于自身的缓存行中(ZONE_PADDING作用应该是把当前缓存行剩余部分填充满,让ZONE_PADDING的前后部分处在不同的缓存行)
。还使用了编译器关键字__cacheline_maxaligned_in_smp
,用以实现最优的高速缓存对齐方式。
- 第一个 ZONE_PADDING 作用是保证 ZONE_PADDING 前后两个锁处在自身的缓存行中
- 第二个 ZONE_PADDING 作用是将数据保持在一个缓存行中,便于快速访问,从而无需从内存加载数据(与CPU高速缓存相比,内存比较慢)
//include/linux/mmzone.h
/* 内存域数据结构,每个内存域都关联了一个 struct zone 的实例,其中保存了用于管理伙伴数据的主要数组 */
struct zone {
/* Fields commonly accessed by the page allocator */
/* pages_min、pages_high、pages_low是页换出时使用的“水线”
* 页换出时使用的“上限”。如果内存不足,内核可以将页写到硬盘。这3个成员会影响交换守护进程的行为
* 如果空闲预留页多于高水线 pages_high ,则内存域的状态是理想的
* 如果空闲预留页的数目低于低水线 pages_low ,则内核开始将页换出到硬盘
* 如果空闲预留页的数目低于最低水线 pages_min ,那么页回收工作的压力就比较大,内存域中急需空闲页
* */
unsigned long pages_min, pages_low, pages_high;
/* 数组分别为各种内存域指定了若干页,用于一些无论如何都不能失败的关键性内存分配 */
unsigned long lowmem_reserve[MAX_NR_ZONES];
#ifdef CONFIG_NUMA
int node;
/*
* zone reclaim becomes active if more unmapped pages exist.
*/
unsigned long min_unmapped_pages;
unsigned long min_slab_pages;
struct per_cpu_pageset *pageset[NR_CPUS];
#else
/* 用于实现每个CPU的热/冷页帧列表. 大小是CPU个数,因为在NUMA中每个CPU也可以访问其他CPU的页
* 高速缓存(这里一般指L2 Cache)
* 热页:该空闲页在高速缓存中,可以快速访问;(一般给CPU使用)
* 冷页:该空闲页不在高速缓存中了(一般给CPU以外的设备使用,如DMA)
* NR_CPUS不是系统中实际的CPU个数,而是内核支持的最大CPU个数
* */
struct per_cpu_pageset pageset[NR_CPUS];
#endif
/*
* free areas of different sizes
*/
spinlock_t lock;
#ifdef CONFIG_MEMORY_HOTPLUG
/* see spanned/present_pages for more description */
seqlock_t span_seqlock;
#endif
/* 同名数据结构的数组,用于实现伙伴系统.
* 每个数组元素都表示某种固定长度的一些连续内存区。数组下标为阶数,即2^n
* 对于包含在每个区域中的空闲内存页的管理, free_area 是一个起点。
* 伙伴系统按 2^n页重新组织了内存,n=>(0-MAX_ORDER),分为n级 order块
* */
struct free_area free_area[MAX_ORDER];
#ifndef CONFIG_SPARSEMEM
/* 特殊字段用于跟踪包含 pageblock_nr_pages 个页的内存区的属性,
* 该字段当前只有与页可移动性相关的代码使用
* */
unsigned long *pageblock_flags;
#endif /* CONFIG_SPARSEMEM */
ZONE_PADDING(_pad1_)
/* Fields commonly accessed by the page reclaim scanner */
spinlock_t lru_lock;
/* 活动页的集合( page 实例) */
struct list_head active_list;
/* 不活动页的集合( page 实例) */
struct list_head inactive_list;
/* 指定在回收内存时需要扫描的活动页的数目 */
unsigned long nr_scan_active;
/* 指定在回收内存时需要扫描的不活动页的数目 */
unsigned long nr_scan_inactive;
/* 前一遍回收时扫描的页数 */
unsigned long pages_scanned; /* since last reclaim */
/* 枚举类型 zone_flags_t; */
unsigned long flags; /* zone flags, see below */
/* Zone statistics */
/* 维护了大量有关该内存域的统计信息 .如当前活动和不活动页的数目
* 函数 zone_page_state 用来读取 vm_stat 中的信息
* */
atomic_long_t vm_stat[NR_VM_ZONE_STAT_ITEMS];
/* 存储了上一次扫描操作扫描该内存域的优先级,扫描操作:try_to_free_pages函数进行,直至释放足够的页帧,扫描会根据该值判断是否换出映射的页 */
int prev_priority;
ZONE_PADDING(_pad2_)
/* 等待队列,供等待某一页变为可用的进程使用 */
wait_queue_head_t * wait_table;
unsigned long wait_table_hash_nr_entries;
unsigned long wait_table_bits;
/*
* Discontig memory support fields.
*/
/* 指向父内存结点,内存域和父结点间的关联 */
struct pglist_data *zone_pgdat;
/* zone_start_pfn == zone_start_paddr >> PAGE_SHIFT */
/* 内存域第一个页帧的索引 */
unsigned long zone_start_pfn;
/* 内存域中页的总数,由于内存域中可能会有空洞,所以不是所有都可用 */
unsigned long spanned_pages; /* total size, including holes */
/* 实际上可用的页数目 */
unsigned long present_pages; /* amount of memory (excluding holes) */
/*
* rarely used fields:
*/
/* 内存域的名字, 目前有3个选项可用:Normal ,DMA ,HighMem*/
const char *name;
} ____cacheline_internodealigned_in_smp;
内存域水线计算struct pglist_data(pg_data_t)->node_zones->pages_min,pages_low,pages_high,lowmem_reserve
在计算各种水线之前,内核首先确定需要为关键性分配保留的内存空间的最小值
该值随可用内存的大小而非线性增长,一个不变的约束是,不能少于128 KiB,也不能多于64 MiB
,只有内存数量确实比较大的时候,才能达到上界
可通过文件 /proc/sys/vm/min_free_kbytes
来读取和修改该设置
//mm/page_alloc.c
//为关键性分配保留的内存空间的最小值。该值随可用内存的大小而非线性增长,一个不变的约束是,不能少于128 KiB,也不能多于64 MiB,只有内存数量确实比较大的时候,才能达到上界
//可通过文件 /proc/sys/vm/min_free_kbytes 来读取和修改该设置
int min_free_kbytes = 1024;
//内核启动期间调用,计算 全局变量 min_free_kbytes 为关键性分配保留的内存空间的最小值,zone->pages_min,pages_low,pages_high,lowmem_reserve
static int __init init_per_zone_pages_min(void)
module_init(init_per_zone_pages_min)
冷热页struct pglist_data(pg_data_t)->node_zones->pageset
struct zone的pageset成员用于实现 冷热分配器 (hot-n-cold allocator)。内核说页是热的
,意味着页已经加载到CPU高速缓存
,与在内存中的页相比,其数据能够更快地访问。相反,冷页则不在高速缓存中
。在多处理器系统上每个CPU都有一个或多个高速缓存,各个CPU的管理必须是独立的
//include/linux/mmzone.h
struct per_cpu_pages {
/* 列表中的页数 */
int count; /* number of pages in the list */
/* 页数上限,高水位,count超过该值说明列表中页太多 */
int high; /* high watermark, emptying needed */
/* 每次添加/删除页数的一个参考值,
* CPU高速缓存一次添加/删除的是多个页组成的块,而不是一个页
* */
int batch; /* chunk size for buddy add/remove *//*决定了在重新填充列表时,有多少页会立即使用。出于性能方面的考虑,一般会向列表添加连续的多页,而不是单页。*/
/* 链表头,保存了当前CPU的冷页或热页,可使用内核的标准函数处理,链表元素为page->lru */
struct list_head list; /* the list of pages */
};
struct per_cpu_pageset {
/* 0:保存当前CPU的热页,1:保存当前CPU的冷页 */
struct per_cpu_pages pcp[2]; /* 0: hot. 1: cold */
} ____cacheline_aligned_in_smp;
页帧
页帧
代表系统内存的最小单位
,对内存中的每个页都会创建struct page
的一个实例。内核程序员需要注意保持该结构尽可能小,因为即使在中等程度的内存配置下,系统的内存同样会分解为大量的页。例如,IA-32系统的标准页长度为4 KiB
,在主内存大小为384 MiB时,大约共有100 000页。就当今的标准而言,这个容量算不上很大,但页的数目已经非常可观
//include/linux/mm_types.h
/* 页描述符,每个物理内存页(页帧),都对应于一个 struct page 实例 */
struct page {
/* 存储体系结构无关的标志,用于描述页帧的属性
* 每位代表不同属性,在page-flags.h文件的宏中描述每位信息
* 如 PG_private
*/
unsigned long flags; /* Atomic flags, some possibly
* updated asynchronously */
/* 引用计数,内核引用该页帧的次数 */
atomic_t _count; /* Usage count, see below. */
union {
/* 表示在页表中有多少项指向该页,如果页由一个进程映射,该计数器值为0,未映射的页,其值为-1 */
atomic_t _mapcount; /* Count of ptes mapped in mms,
* to show when page is mapped
* & limit reverse map searches.
*/
unsigned int inuse; /* SLUB: Nr of objects */
};
union {
struct {
/* 指向私有数据的指针,
* 虚拟内存管理会忽略该数据,根据页的用途,可以用不同的方式使用该指针.
* 大多数情况下它用于将页与数据缓冲区关联起来,指向第一个缓存头 struct buffer_head(循环单链表头),然后用 buffer_head->b_this_page 组成循环单链表
* */
unsigned long private; /* Mapping-private opaque data:
* usually used for buffer_heads
* if PagePrivate set; used for
* swp_entry_t if PageSwapCache;
* indicates order in the buddy
* system if PG_buddy is set.
*/
/* 指定页帧所在的地址空间,pgoff_t index表示页帧在映射内部的偏移量
* 如果mapping最低位被设为1(因为address_space是sizeof(long)对齐,所以最低位总是0),
* 则mapping不是struct address_space结构体,
* 而是struct anon_vma结构体,该结构对实现匿名页的逆向映射很重要
* 内核可用anon_vma = (struct anon_vma *) (mapping -PAGE_MAPPING_ANON)恢复指针
* */
struct address_space *mapping; /* If low bit clear, points to
* inode address_space, or NULL.
* If page mapped as anonymous
* memory, low bit is set, and
* it points to anon_vma object:
* see PAGE_MAPPING_ANON below.
*/
};
#if NR_CPUS >= CONFIG_SPLIT_PTLOCK_CPUS
spinlock_t ptl;
#endif
struct kmem_cache *slab; /* SLUB: Pointer to slab */
/* 内核可以将多个相邻的页合并成复合页,
* 分组中第一个页称首页(head page),其他叫尾页(tail page),
* 所有尾页的page实例中,都将first_page指向首页
* */
struct page *first_page; /* Compound tail pages */
};
union {
pgoff_t index; /* 页帧在映射 struct address_space *mapping 内部的偏移量 *//* Our offset within mapping. */
void *freelist; /* SLUB: freelist req. slab lock */
};
/* 表头,用于在各种链表上维护该页,以便将页按不同类别分组,最重要的类别是活动和不活动页,表头为 pglist_data->node_zones->free_area->free_list,zone->pageset->pcp->list
* lru:least recently used
*/
struct list_head lru; /* Pageout list, eg. active_list
* protected by zone->lru_lock !
*/
#if defined(WANT_PAGE_VIRTUAL)
/* 用于存储高端内存区域中的页的虚拟地址,即无法直接映射到内核内存中的页 */
void *virtual; /* Kernel virtual address (NULL if
not kmapped, ie. highmem) */
#endif /* WANT_PAGE_VIRTUAL */
};
//include/linux/page-flags.h
struct page->flags 各标志的定义和操作函数
//include/linux/pagemap.h
/* 睡眠等待被锁定的页解锁后唤醒 */
static inline void wait_on_page_locked(struct page *page);
/* 睡眠等待页写回操作结束后唤醒 */
static inline void wait_on_page_writeback(struct page *page);
页表
我们知道页表用于建立用户进程的虚拟地址空间和系统物理内存(内存、页帧)之间的关联
。到目前为止讨论的结构主要用来描述内存的结构(划分为结点和内存域),同时指定了其中包含的页帧的数量和状态(使用中或空闲)。页表用于向每个进程提供一致的虚拟地址空间。应用程序看到的地址空间是一个连续的内存区。该表也将虚拟内存页映射到物理内存,因而支持共享内存的实现(几个进程同时共享的内存),还可以在不额外增加物理内存的情况下,将页换出到块设备来增加有效的可用内存空间。
内核内存管理总是假定使用四级页表,而不管底层处理器是否如此。这方面最好的例子是,该假定对IA-32系统是不正确的。默认情况下,该体系结构只使用两级分页系统(在不使用PAE扩展的情况下)。因此,第三和第四级页表必须由特定于体系结构的代码模拟。
页表管理分为两个部分,第一部分依赖于体系结构,第二部分是体系结构无关的。
有趣的是,所有数据结构和操作数据结构的几乎所有函数都是定义在特定于体系结构的文件中。
通常基于体系结构相关的文件中提供的接口。定义可以在头文件include/asm-arch/page.h和include/asm-arch/pgtable.h
中找到,下文简称为page.h和pgtable.h。
页表数据结构 {#虚拟内存地址}
-
内存地址的分解
根据四级页表结构的需要,虚拟内存地址分为5部分(4个表项用于选择页,1个索引表示页内位置)。各个体系结构不仅地址字长度不同,而且地址字拆分的方式也不同。因此内核定义了宏,用于将地址分解为各个分量。
下图说明了如何用比特位移来定义地址字各分量的位置。
BITS_PER_LONG定义用于unsigned long变量的比特位数目
,因而也适用于指向虚拟地址间的通用指针。
虚拟地址由5部分组成:每个指针末端的几个比特位,用于指定所选页帧内部的位置。比特位的具体数目由
PAGE_SHIFT
指定PMD_SHIFT
指定了页内偏移量和最后一级页表项所需比特位的总数,该值表明了一个中间层页表项管理的部分地址空间的大小,即2PMD_SHIFT字节,虚拟地址右移 PMD_SHIFT 就得到了页中间目录的索引号。PUD_SHIFT
由 PMD_SHIFT加上中间层页表索引所需的比特位长度,而PGDIR_SHIFT
则 由PUD_SHIFT加上上层页表索引所需的比特位长度。对全局页目录中的一项所能寻址的部分地址空间长度计算以2为底的对数,即为PGDIR_SHIFT在各级页目录/页表中所能存储的指针数目,也可以通过宏定义确定。
PTRS_PER_PGD
指定了全局页目录中项的数目,PTRS_PER_PMD
对应于中间页目录,PTRS_PER_PUD
对应于上层页目录中项的数目,PTRS_PER_PTE
则是页表中项的数目两级页表
的体系结构会将PTRS_PER_PMD和PTRS_PER_PUD定义为1n比特位长
的地址字可寻址的地址区域长度为2n字节。内核定义了额外的宏变量保存计算得到的值,以避免多次重复计算。相关的宏定义如下//include/asm-x86/page_64.h #define PAGE_SHIFT 12 #define PAGE_SIZE (_AC(1,UL) << PAGE_SHIFT) #define PAGE_MASK (~(PAGE_SIZE-1)) //include/asm-x86/pgtable_64.h #define PMD_SIZE (_AC(1,UL) << PMD_SHIFT) #define PMD_MASK (~(PMD_SIZE-1)) #define PUD_SIZE (_AC(1,UL) << PUD_SHIFT) #define PUD_MASK (~(PUD_SIZE-1)) #define PGDIR_SIZE (_AC(1,UL) << PGDIR_SHIFT) #define PGDIR_MASK (~(PGDIR_SIZE-1)) /* * PGDIR_SHIFT determines what a top-level page table entry can map */ /* 地址中偏移位数 */ #define PGDIR_SHIFT 39 /* PGD页中的指针数 */ #define PTRS_PER_PGD 512 #define PUD_SHIFT 30 #define PTRS_PER_PUD 512 #define PMD_SHIFT 21 #define PTRS_PER_PMD 512
PTRS_PER_XXX
指定了给定目录项能够代表多少指针(即,多少个不同的值)。由于AMD64对每个页表索引使用了9个比特位,因此每个页表可容纳29=512个指针XXX_MASK
位掩码用来从给定地址中提取各个分量,将给定地址与对应掩码按位与即可 -
页表的格式
内核提供了4个数据结构(定义在page.h中)来表示页表项的结构:
- pgd_t用于全局页目录项。
- pud_t用于上层页目录项。
- pmd_t用于中间页目录项。
- pte_t用于直接页表项。
各函数如下表:
include/asm-x86/pgtable_32.h
PAGE_ALIGN
是另一个每种体系结构都必须定义的标准宏(通常在page.h中)。它需要一个地址作为参数,并将该地址“舍入”到下一页的起始处。如果页大小是4 096,该宏总是返回其倍数。- PAGE_ALIGN(6000)=8192 = 2× 4 096
- PAGE_ALIGN(0x84590860)=0x84591000 = 542 097 × 4 096。
为用好处理器的高速缓存资源,将地址对齐到页边界
//页表项结构体 //include/asm-x86/page_64.h typedef struct { unsigned long pte; } pte_t; typedef struct { unsigned long pmd; } pmd_t; typedef struct { unsigned long pud; } pud_t; typedef struct { unsigned long pgd; } pgd_t; 它们的关系可以当成: 最上层是:pgd_t[PTRS_PER_PGD] 这样一个数组,数组最大值是 PTRS_PER_PGD ,即 pud_t 数组最大个数为 PTRS_PER_PGD ,pgd_t地址每+1就是一个 pud_t 数组 上层是:pud_t[] 中层是:pmd_t[] 最后是:pte_t[]
虚拟地址分为几个部分,用作各个页表的索引,这是我们熟悉的方案。根据使用的体系结构字长不同,各个单独的部分长度小于32或64个比特位。从给出的内核源代码片段可以看出,内核(以及处理器)使用32或64位类型来表示页表项(不管页表的级数)。这意味着并非表项的所有比特位都存储了有用的数据,即下一级表的基地址。多余的比特位用于保存额外的信息。
-
特定于PTE的信息
pte页表项中不仅包含了指向页的内存位置的指针,还在上述的多余比特位包含了与页有关的附加信息。尽管这些数据是特定于CPU的,它们至少提供了有关页访问控制的一些信息。下列位在Linux内核支持的大多数CPU中都可以找到。
1. P(0) 表示该 PTE 映射的物理内存页是否在内存中,值为 1 表示物理内存页在内存中驻留,值为 0 表示物理内存页不在内存中
2. R/W(1) 表示进程对该物理内存页拥有的读,写权限,值为 1 表示进程对该物理页拥有读写权限,值为 0 表示进程对该物理页拥有只读权限,进程对只读页面进行写操作将触发 page fault (写保护中断异常),用于写时复制(Copy On Write, COW)的场景
3. U/S(2) 值为 0 表示该物理内存页面只有内核才可以访问,值为 1 表示用户空间的进程也可以访问。
4. PCD(4) 是 Page Cache Disabled 的缩写,表示 PTE 指向的这个物理内存页中的内容是否可以被缓存再 CPU CACHE 中,值为 1 表示 Disabled,值为 0 表示 Enabled。
5. PWT(3) 同样也是和 CPU CACHE 相关的控制位,Page Write Through 的缩写,值为 1 表示 CPU CACHE 中的数据发生修改之后,采用 Write Through 的方式同步回物理内存页中。值为 0 表示采用 Write Back 的方式同步回物理内存页
6. A(5) 表示 PTE 指向的这个物理内存页最近是否被访问过,1 表示最近被访问过(读或者写访问都会设置为 1),0 表示没有。该 bit 位被硬件 MMU 设置,由操作系统重置。内核会经常检查该比特位,以确定该物理内存页的活跃程度,不经常使用的内存页,很可能就会被内核 swap out 出去。
7. D(6) 主要针对文件页使用,当 PTE 指向的物理内存页是一个文件页时,进程对这个文件页写入了新的数据,这时文件页就变成了脏页,对应的 PTE 中 D 比特位会被设置为 1,表示文件页中的内容与其背后对应磁盘中的文件内容不同步了
8. PAT(7) 表示是否支持 PAT(Page Attribute Table)
9. G(8) 设置为 1 表示该 PTE 是全局的,该标志位表示 PTE 中保存的映射关系是否是全局的,什么意思呢,一般来说进程都有各自独立的虚拟内存空间,进程的页表也是独立的 ,CPU 每次访问进程虚拟内存地址的时候都需要进行地址翻译(上一小节介绍的寻址过程),为了加速地址翻译的速度,避免每次遍历页表,CPU 会把经常被访问到的 PTE 缓存在一个 TLB 的硬件缓存中,由于 TLB 中缓存的是当前进程相关的 PTE,所以操作系统每次在切换进程的时候,都会重新刷新 TLB 缓存。而有一些 PTE 是所有进程共享的,比如说内核虚拟内存空间中的映射关系,所有进程进入内核态看到的都是一样的。所以会将这些全局共享的 PTE 中的 G 比特位置为 1 ,这样在每次进程切换的时候,就不会 flush 掉 TLB 缓存的那些共享的全局 PTE(比如内核地址的空间中使用的 PTE),从而在很大程度上提升了性能。
```c{.line-numbers}
//include/asm-x86/pgtable_64.h
/* 特定PTE的信息 */
/* 驻留位,虚拟内存页是否存在于内存中 */
#define _PAGE_PRESENT 0x001
/* 保护位,表示普通用户进程是否允许读写访问 */
#define _PAGE_RW 0x002
/* 设置了该位表示允许用户空间代码访问该页 */
#define _PAGE_USER 0x004
#define _PAGE_PWT 0x008
#define _PAGE_PCD 0x010
/* 访问位,CPU每次访问页时会设置该位,内核定期检查该位来确定页使用的活跃度 */
#define _PAGE_ACCESSED 0x020
/* 修改位,表示该页是否是脏的 */
#define _PAGE_DIRTY 0x040
/*数值与_PAGE_DIRTY相同,但用于不同的上下文,即页不在内存中的时候。显然,不存在的页不可能是脏的,因此可以重新解释该比特位。如果没有设置,则该项指向一个换出页的位置。如果该项属于非线性文件映射,则需要设置_PAGE_FILE*/
#define _PAGE_FILE 0x040 /* nonlinear
```
*[__pgprot]: 在 include/asm-x86/page_64.h 中定义
*[pte_modify]: 在 include/asm-x86/pgtable_64.h 中定义
每种体系结构都必须提供`两个东西`,使得内存管理子系统能够修改`pte_t`项中额外的比特位,即保存额外的比特位的[__pgprot](#img1)数据类型,以及修改这些比特位的`pte_modify`函数。上面的预处理器符号可用于选择适当的比特位
内核还定义了各种函数,用于`查询和设置内存页与体系结构相关的状态`。某些处理器可能缺少对一些给定特性的硬件支持,因此并非所有的处理器都定义了所有这些函数。
只有在pte_present返回false时,才能调用pte_file,即与该页表项相关的页不在内存中。
由于内核的通用代码对pte_file的依赖,在某个体系结构并不支持非线性映射的情况下也需要定义该函数。在这种情况下,该函数总是返回0。
所有用于操作PTE项的函数:
初始化内存管理
在内存管理的上下文中,初始化(initialization)可以有多种含义。在许多CPU上,必须显式设置适于Linux内核的内存模型。例如,在IA-32系统上需要切换到保护模式,然后内核才能检测可用内存和寄存器。在初始化过程中,还必须建立内存管理的数据结构,以及其他很多事务。因为内核在内存管理完全初始化之前就需要使用内存,在系统启动过程期间,使用了一个额外的简化形式的内存管理模块,然后又丢弃掉
。关键是pg_data_t数据结构的初始化
建立数据结构
在特定于体系结构的设置步骤中检测内存并确定系统中内存的分配情况后,会立即执行内存管理的初始化。此时,已经对各种系统内存模式生成了一个 pg_data_t 实例
,用于保存诸如结点中内存数量以及内存在各个内存域之间分配情况的信息。所有平台上都实现了特定于体系结构的NODE_DATA 宏
,用于通过结点编号,来查询与一个NUMA2结点相关的 pg_data_t 实例
大部分系统都只有一个内存结点
//定义管理系统内存节点的结构体(同样用于UMA和NUMA系统)
//mm/page_alloc.c
struct pglist_data contig_page_data = { .bdata = &contig_bootmem_data };
EXPORT_SYMBOL(contig_page_data);
//include/linux/mmzone.h
extern struct pglist_data contig_page_data;
/* 获取系统内存结构宏,形式参数用于选择NUMA结点,在UMA系统中是一个伪结点 */
#define NODE_DATA(nid) (&contig_page_data)
-
setup_arch
setup_arch
其中一项任务是负责初始化自举内存分配器
(bootmem分配器),用于启动阶段早期分配内存,特点是实现简单而不是性能和通用性- setup_arch
- ->setup_memory
- ->setup_bootmem_allocator
- ->init_bootmem
- ->setup_bootmem_allocator
- ->setup_memory
- setup_arch
-
setup_per_cpu_areas
初始化__per_cpu_offset数组,内核会使用一个数组__per_cpu_offset[cpu]
记录每个CPU静态per cpu 变量的偏移地址。在ARM64架构下, OS 启动时将 per cpu 偏移地址写入到 TPDIR_EL1 和 TPDIR_EL2 寄存器中。
per_cpu说明
在SMP系统上, setup_per_cpu_areas 初始化源代码中(使用per_cpu宏)定义的静态 per-cpu 变量,这种变量对系统中的每个CPU都有一个独立的副本。此类变量保存在内核二进制映像的一个独立的段中。setup_per_cpu_areas的目的是为系统的各个CPU分别创建一份这些数据的副本。
在非SMP系统上该函数是一个空操作//init/main.c static void __init setup_per_cpu_areas(void) { unsigned long size, i; char *ptr; unsigned long nr_possible_cpus = num_possible_cpus(); /* Copy section for each CPU (we discard the original) */ size = ALIGN(PERCPU_ENOUGH_ROOM, PAGE_SIZE); ptr = alloc_bootmem_pages(size * nr_possible_cpus); for_each_possible_cpu(i) { //记录每个CPU静态per cpu 变量的偏移地址,DEFINE_PER_CPU 宏定义了Per CPU 变量 __per_cpu_offset[i] = ptr - __per_cpu_start; memcpy(ptr, __per_cpu_start, __per_cpu_end - __per_cpu_start); ptr += size; } }
-
build_all_zonelists
建立结点和内存域的数据结构- build_all_zonelists
- __build_all_zonelists
- build_zonelists
- build_zonelists_node
- build_zonelists
- __build_all_zonelists
pg_data_t->node_zonelists->zones 填充 pg_data_t->node_zones:
高端内存域的备用列表填充过程
不同内存域的备用列表
- build_all_zonelists
-
mem_init
特定于体系结构的函数,用于停用bootmem分配器
并迁移到实际的内存管理函数 -
kmem_cache_init
初始化内核内部用于小块内存区的分配器 -
setup_per_cpu_pageset
从 struct zone,为 pageset 数组的第一个数组元素分配内存。分配第一个数组元素,换句话说,就是意味着为第一个系统处理器分配。系统的所有内存域都会考虑进来。
该函数还负责设置冷热分配器的限制
请注意,在SMP系统上对应于其他CPU的pageset数组成员,将会在相应的CPU激活时初始化。
内存特定体系结构的设置
内核在内存中的布局
该图给出了物理内存的前几兆字节,具体的长度依赖于内核二进制文件的长度。前4 KiB
是第一个页帧,一般会忽略,因为通常保留给BIOS使用
。接下来的640 KiB
原则上是可用的,但也不用于内核加载。其原因是,该区域之后紧邻的区域由系统保留,用于映射各种ROM(通常是系统BIOS和显卡ROM)。不可能向映射ROM的区域写入数据。但内核总是会装载到一个连续的内存区中,如果要从4 KiB处作为起始位置来装载内核映像,则要求内核必须小于640 KiB。
为解决这些问题,IA-32内核使用0x100000作为起始地址。这对应于内存中第二兆字节的开始处。从此处开始,有足够的连续内存区,可容纳整个内核
_text和_etext
是代码段的起始和结束地址,包含了编译后的内核代码。_etext和_edata
之间是数据段,保存了大部分内核变量- 保存在最后一段从_edata到_end,初始化数据在内核启动过程结束后不再需要(例如,包含初始化为0的所有静态全局变量的BSS段)。在内核初始化完成后,其中的大部分数据都可以从内存删除,给应用程序留出更多空间。这一段内存区划分为更小的子区间,以控制哪些可以删除,哪些不能删除
划定段边界的变量定义在内核源代码(arch/x86/kernel/setup_32.c
)中,接下来则打包为二进制文件。该操作是由arch/arch/vmlinux.ld.S
控制的(对IA-32来说,该文件是arch/x86/vmlinux_32.ld.S),其中也划定了内核的内存布局。
每次编译内核时,都生成一个文件System.map
并保存在源代码目录下。记录了所有符号的运行地址,这里的符号可以理解成函数名和变量
wolfgang@meitner> cat System.map
...
c0100000 A _text
...
c0381ecd A _etext
...
c04704e0 A _edata
...
c04c3f44 A _end
...
上述所有地址值都偏移了0xC0000000
,这是在用户和内核地址空间之间采用标准的3 : 1划分时,内核段的起始地址。该地址是虚拟地址,因为物理内存映射到虚拟地址空间的时候,采用了从该地址开始的线性映射方式。减去0xC0000000,则可获得对应的物理地址。
/proc/iomem
也提供了有关物理内存划分出的各个段的一些信息
wolfgang@meitner> cat /proc/iomem
00000000-0009e7ff : System RAM
0009e800-0009ffff : reserved
000a0000-000bffff : Video RAM area
000c0000-000c7fff : Video ROM
000f0000-000fffff : System ROM
00100000-17ceffff : System RAM
00100000-00381ecc : Kernel code
00381ecd-004704df : Kernel data
...
初始化步骤 setup_arch
-
machine_specific_memory_setup
创建一个列表,包括系统占据的内存区和空闲内存区(这里说到的内存区,与NUMA背景下的内存区是不同的,只是指由系统ROM或ACPI函数占据的内存区) -
parse_early_param
分析命令行 -
setup_memory
,该函数有两个版本。一个用于连续内存系统(在arch/ x86/kernel/setup_32.c),另一个用于不连续内存系统(在arch/x86/mm/discontig_32.c)- 确定(每个结点)可用的物理内存页的数目
初始化bootmem分配器
- 分配各种内存区,如,运行第一个用户空间程序所需的最初的RAM
-
paging_init
初始化内核页表并启用内存分页pagetable_init
确保了直接映射到内核地址空间的物理内存被初始化,物理地址+ PAGE_OFFSET 为虚拟地址
-
zone_sizes_init
初始化系统中所有结点的pgdat_t实例- add_active_range 对可用的物理内存建立一个相对简单的列表,注册内存区到全局变量 early_node_map
- free_area_init_nodes 使用上面函数的信息建立完备的内核数据结构
分页机制的初始化 paging_init
paging_init 负责建立只能用于内核的页表,用户空间无法访问
直接映射相关计算
内核地址空间的偏移量 PAGE_OFFSET ->0xC0000000 = 3GiB
,虚拟地址 x
对应的物理地址为 x-0xC0000000
,这是一个简单的线性平移
直接映射区域从0xC0000000到high_memory地址,这种方案有一问题。由于内核的虚拟地址空间只有1 GiB,最多只能映射1 GiB物理内存。IA-32系统(没有PAE)最大的内存配置可以达到4 GiB,剩下的3 GiB如何处理
如果物理内存超过896 MiB,则内核无法直接映射全部物理内存
(内核无法使用多余的物理内存),该值比此前提到的最大限制1 GiB还小,因为内核必须保留地址空间最后的128 MiB用于其他目的(在上图中有表示),内核使用两个经常使用的缩写normal和highmem,来区分是否可以直接映射的页帧
.
虚拟内存和物理内存转换函数,这两个函数只能用于映射部分,不能用于任意地址
:
__pa(vaddr)
返回与虚拟地址vaddr相关的物理地址.__va(paddr)
则计算出对应于物理地址paddr的虚拟地址.
128MiB作用,看上图:
- 虚拟内存中连续,物理内存中不连续的内存去,可以在
vmalloc区域
分配,常用于用户程序,内核会尽力避免非连续的物理地址,系统运行久了以后可能会出现,主要出现在动态加载模块时 持久映射
用于将高端内存域
中的非持久页映射到内核中
固定映射
是与物理地址空间中的固定页关联的虚拟地址空间项,但具体关联的页帧可以自由选择。它与通过固定公式与物理内存关联的直接映射页相反,虚拟固定映射地址与物理内存位置之间的关联可以自行定义,关联建立后内核总是会注意到的。
__VMALLOC_RESERVE
设置了vmalloc区域的长度
MAXMEM
则表示内核
可以直接寻址的物理内存
的最大可能数量
直接映射的边界由high_memory指定
//arch/x86/kernel/setup_32.c
static unsigned long __init setup_memory(void)
{
...
#ifdef CONFIG_HIGHMEM
high_memory = (void *) __va(highstart_pfn * PAGE_SIZE -1) + 1;
#else
high_memory = (void *) __va(max_low_pfn * PAGE_SIZE -1) + 1;
#endif
...
}
max_low_pfn
指定了物理内存数量小于896 MiB的系统上内存页的数目。该值的上界受限于896 MiB可容纳的最大页数(具体的计算在 find_max_low_pfn
给出)。如果启用了高端内存支持,则high_memory表示两个内存区之间的边界,总是896 MiB
如果VMALLOC_OFFSET取最小值,那么在直接映射的所有内存页和用于非连续分配的区域之间,会出现一个缺口。
vmalloc相关计算
//include/asm-x86/pgtable_32.h
#define VMALLOC_OFFSET (8*1024*1024)
这个缺口可用作针对任何内核故障的保护措施
。如果访问越界地址(即无意地访问物理上不存在的内存区),则访问失败并生成一个异常,报告该错误。如果vmalloc区域紧接着直接映射,那么访问将成功而不会注意到错误。在稳定运行的情况下,肯定不需要这个额外的保护措施,但它对开发尚未成熟的新内核特性是有用的。
VMALLOC_START和VMALLOC_END定义了vmalloc区域的开始和结束
,该区域用于物理上不连续的内核映射。这两个值没有直接定义为常数,而是依赖于几个参数。
//include/asm-x86/pgtable_32.h
#define VMALLOC_START (((unsigned long) high_memory + \
2*VMALLOC_OFFSET-1) & ~(VMALLOC_OFFSET-1))
#ifdef CONFIG_HIGHMEM
# define VMALLOC_END (PKMAP_BASE-2*PAGE_SIZE)
#else
# define VMALLOC_END (FIXADDR_START-2*PAGE_SIZE)
#endif
vmalloc区域的起始地址,取决于在直接映射物理内存时,使用了多少虚拟地址空间内存,与 high_memory 有关.两个区域之间有至少为 VMALLOC_OFFSET 的一个缺口
,而且vmalloc区域从可被VMALLOC_OFFSET整除的地址开始。这样的规则导致了表3-5给出的偏移量值,该表针对128 MiB到135 MiB之间的内存配置计算的偏移量值。该值是周期性的,从136 MiB开始是一个新的循环
vmalloc 区域
在何处结束取决于是否启用了高端内存支持
。如果没有启用,那么就不需要持久映射区域
,因为整个物理内存都可以直接映射。因此,根据不同的配置,该区域结束于持久内核映射或固定映射区域的起始处
。总是会留下两页
,作为vmalloc区域与这两个区域之间的保护措施。
持久(永久)内核映射区域相关计算
持久内核映射区域的起始和结束定义如下:
//include/asm-x86/highmem.h
#define LAST_PKMAP 1024
//内存持久映射开始地址
#define PKMAP_BASE ( (FIXADDR_BOOT_START -PAGE_SIZE*(LAST_PKMAP + 1)) & PMD_MASK )
PKMAP_BASE
定义了其起始地址,LAST_PKMAP
定义了容纳该映射所需的页数
固定内核映射区域相关计算
最后一个内存段由固定映射占据。这些地址指向物理内存中的随机位置。相对于内核空间起始处的线性映射,在该映射内部的虚拟地址和物理地址之间的关联不是预设的,而可以自由定义,但定义后不能改变。固定映射区域会一直延伸到虚拟地址空间顶端
//include/asm-x86/fixmap_32.h
#define __FIXADDR_TOP 0xfffff000
#define FIXADDR_TOP ((unsigned long)__FIXADDR_TOP)
#define __FIXADDR_SIZE (__end_of_permanent_fixed_addresses << PAGE_SHIFT)
//内存固定映射开始地址
#define FIXADDR_START (FIXADDR_TOP -__FIXADDR_SIZE)
固定映射地址的优点在于,在编译时对此类地址的处理类似于常数,内核一启动即为其分配了物理地址。此类地址的解引用比普通指针要快速。内核会确保在上下文切换期间,对应于固定映射的页表项不会从TLB刷出,因此在访问固定映射的内存时,总是通过TLB高速缓存取得对应的物理地址。
//include/asm-x86/fixmap_32.h
//对每个固定映射地址都会创建一个常数,加入到fixed_addresses枚举值列表中
enum fixed_addresses {
FIX_HOLE,
FIX_VDSO,
FIX_DBGP_BASE,
FIX_EARLYCON_MEM_BASE,
#ifdef CONFIG_X86_LOCAL_APIC
FIX_APIC_BASE, /* 本地CPU APIC信息,在SMP系统上需要 */
#endif
...
#ifdef CONFIG_HIGHMEM
FIX_KMAP_BEGIN, /* 保留的页表项,用于临时内核映射 */
FIX_KMAP_END = FIX_KMAP_BEGIN+(KM_TYPE_NR*NR_CPUS)-1,
#endif
...
FIX_WP_TEST,
__end_of_fixed_addresses
};
//用于计算 固定映射常数(enum fixed_addresses) 的虚拟地址
static __always_inline unsigned long fix_to_virt(const unsigned int idx)
//建立内存固定映射虚拟地址页表中的对应项与物理内存页之间的关联
#define set_fixmap(idx, phys) \
__set_fixmap(idx, phys, PAGE_KERNEL)
/*
* Some hardware wants to get fixmapped without caching.
*/
//建立内存固定映射虚拟地址页表中的对应项与物理内存页之间的关联,在必要情况下,会停用所涉及页帧的硬件高速缓存
#define set_fixmap_nocache(idx, phys) \
__set_fixmap(idx, phys, PAGE_KERNEL_NOCACHE)
-
虚拟地址空间用户和内核划分
//include/asm-x86/page_32.h #define __PAGE_OFFSET ((unsigned long)CONFIG_PAGE_OFFSET) /* 内核可以直接寻址的物理内存的最大可能数量 */ #define MAXMEM (-__PAGE_OFFSET-__VMALLOC_RESERVE)
-
划分虚拟地址空间 paging_init
在IA-32系统上的启动过程中,会调用paging_init按如上所述的方式划分虚拟地址空间
swapper_pg_dir
:swapper_pg_dir是全局页目录的线性地址,减去__PAGE_OFFSET就是物理地址,在汇编中初始化了临时页目录
,它是所有进程内核空间的页表的模板(管理进程使用的内核空间内存),而且在涉及到896M以上(高端内存区)的内存分配时,swapper_pg_dir也是一个同步的根,这些内存分配包括vmalloc区,高端永久区,高端临时区等,初始化系统的页表,以swapper_pg_dir为基础- pagetable_init 初始化页表,物理地址到虚拟地址的映射
- load_cr3 将 cr3 寄存器设置为指向全局页目录swapper_pg_dir的指针
- __flush_tlb_all 将TLB缓存刷出
- kmap_init 初始化全局变量kmap_pte,用于高端内存域
-
冷热缓存的初始化
//mm/page_alloc.c
/* 初始化该内存域的per-CPU(冷热)缓存 */
static __meminit void zone_pcp_init(struct zone *zone)
调用结束时打印输出各个内存域的页数以及计算出的批量大小
root@meitner # dmesg | grep LIFO
DMA zone: 2530 pages, LIFO batch:0
DMA32 zone: 833464 pages, LIFO batch:31
Normal zone: 193920 pages, LIFO batch:31
调用流程:
- start_kernel
- setup_arch
- zone_sizes_init
- free_area_init_nodes
- free_area_init_node
- free_area_init_core
- zone_pcp_init
- zone_batchsize 计算冷热页batch成员基础值
- setup_pageset 设置冷热页变量
- zone_pcp_init
- free_area_init_core
- free_area_init_node
- free_area_init_nodes
- zone_sizes_init
- setup_arch
注册活动内存区
内核提供了通用框架,各个体系结构只须注册所有活动内存区的一个简单表,通用代码则据此生成主数据结构.使用框架需要设置配置选项 ARCH_POPULATES_NODE_MAP
。在注册所有活动内存区之后,其余的工作由通用的内核代码完成
//include/linux/mmzone.h
/* 内存区数据结构 */
struct node_active_region {
/* 一个连续内存区中的第一个页帧 */
unsigned long start_pfn;
/* 一个连续内存区中的最后一个页帧 */
unsigned long end_pfn;
/* 该内存区所属结点的NUMA ID.UMA系统设置为0 */
int nid;
};
//mm/page_alloc.c
//全局内存区,用 add_active_range 注册成员
static struct node_active_region __meminitdata early_node_map[MAX_ACTIVE_REGIONS];
/* 当前注册的内存区数目记载在nr_nodemap_entries中 */
static int __meminitdata nr_nodemap_entries;
add_active_range 被调用流程:
- IA32:
- setup_arch
- zone_sizes_init
- add_active_range
- zone_sizes_init
- setup_arch
- AMD64:
- setup_arch
- e820_register_active_regions
- add_active_range
- e820_register_active_regions
- setup_arch
AMD64地址空间的设置
当前只实现了一个比较小的物理地址空间,地址字宽度为48位。这在不失灵活性的前提下,简化并加速了地址转换。48位宽的地址字可以寻址256 TiB的地址空间,或256× 1 024 GiB
尽管物理地址字位宽被限制在48位,但在寻址虚拟地址空间时仍然使用了64位指针,因而虚拟地址空间在形式上仍然会跨越264字节。但这引起了一个问题:由于物理地址字实际上只有48位宽,虚拟地址空间的某些部分无法寻址
硬件设计师其解决方案基于所谓的符号扩展(sign extension)方法
虚拟地址的低47位,即[0, 46],可以任意设置。而比特位[47, 63]的值总是相同的:或者全0,或者全1。此类地址称之为规范的。因此整个地址空间划分为3部分
:下半部、上半部、二者之间的禁用区。上下两部分共同构成跨越248字节的一个地址空间。地址空间的下半部是[0x0, 0x0000 7FFF FFFFFFFF],上半部是[0xFFF 800 0000 0000, 0xFFFF FFFF FFFF FFFF]。请注意0x0000 7FFF FFFF FFFF是一个二进制数,低47位都是1,其他位都是0,因此正是非可寻址区域之前的最后一个地址。类似地,0xFFFF 8000 0000 0000中,比特位[47, 63]置位,从而是上半部的第一个有效地址。
可访问的地址空间的整个下半部用作用户空间,而整个上半部专用于内核。由于两个空间都极大,无须调整划分比例之类的参数。
内核地址空间起始于一个起防护作用的空洞,以防止偶然访问地址空间的非规范部分。如果发生这种情况,处理器会引发一个一般性保护异常(general protection exception)。物理内存页则直接映射到从PAGE_OFFSET开始的内核空间中。246字节(由MAXMEM指定)专用于物理页帧。总计可达64 TiB内存。
//include/asm-x86/pgtable_64.h
#define __AC(X,Y) (X##Y)
#define _AC(X,Y) __AC(X,Y)
#define __PAGE_OFFSET _AC(0xffff810000000000, UL)
#define PAGE_OFFSET __PAGE_OFFSET
#define MAXMEM _AC(0x3fffffffffff, UL)
_AC用于对给定的常数标记后缀。例如,_AC(17,UL)变为(17UL),相当于把常数标记为unsigned long类型。这在C语言中很方便,但无法用于汇编程序代码,在汇编程序中_AC宏直接解析为相应的常数,没有后缀。
另一个防护性空洞位于直接映射内存区和vmalloc内存区之间,后者的范围从VMALLOC_START到VMALLOC_END:
include/asm-x86/pgtable_64.h
#define VMALLOC_START _AC(0xffffc20000000000, UL)
#define VMALLOC_END _AC(0xffffe1ffffffffff, UL)
虚拟内存映射(virtual memory map,VMM)内存区紧接着vmalloc内存区之后,长为1 TiB。只有
内核使用了稀疏内存模型,该内存区才是有用的。在此类计算机上通过pfn_to_page
和page_to_pfn
转换虚拟和物理页帧号代价比较高,因为必须考虑物理地址空间中的所有空洞。从内核版本2.6.24开始,内核通用代码提供了一个更简单的解决方案,见mm/sparse-memmap.c
。VMM内存区的页表进行特定的设置,使得物理内存中所有的struct page实例都映射到没有空洞的内存区中。这提供了一个几乎连续的内存区,其中只包括活动内存区。由于不再需要关注空洞,因而MMU可以自动地为虚拟和物理编号之间转换提供辅助。这在相当程度上加速了该操作。
除了简化物理和虚拟页号之间的转换,该技术还有利于辅助函数virt_to_page
和page_address
的实现,因为二者需要的计算也同样简化了。
内核代码段映射到从 __START_KERNEL_MAP 开始的内存区
,还有一个编译时可配置的偏移量CONFIG_PHYSICAL_START
。在编译可重定位内核时需要设置该偏移量,但还需要确保结果地址 __START_KERNEL 对齐到 KERNEL_ALIGN 。保留给内核二进制代码的内存区长度为 KERNEL_TEXT_SIZE
,当前定义为40 MiB
//include/asm-x86/page_64.h
#define __PHYSICAL_START CONFIG_PHYSICAL_START
#define __KERNEL_ALIGN 0x200000
#define __START_KERNEL (__START_KERNEL_map + __PHYSICAL_START)
#define __START_KERNEL_map _AC(0xffffffff80000000, UL)
#define KERNEL_TEXT_SIZE (40*1024*1024)
#define KERNEL_TEXT_START _AC(0xffffffff80000000, UL)
最后,还必须提供一些空间用于映射模块
,该内存区从MODULES_VADDR到MODULES_END
:
//include/asm-x86/pgtable_64.h
#define MODULES_VADDR _AC(0xffffffff88000000, UL)
#define MODULES_END _AC(0xfffffffffff00000, UL)
#define MODULES_LEN (MODULES_END -MODULES_VADDR)
该内存区可用内存的数量由MODULES_LEN计算
,当前大约是1 920 MiB。
内核启动期间的内存管理 bootmem
在启动过程期间,尽管内存管理尚未初始化,但内核仍然需要分配内存以创建各种数据结构。bootmem分配器用于在启动阶段早期分配内存,最先适配(first-fit)分配器,由于高端内存处理太麻烦,由此对bootmem分配器无用
该分配器使用一个位图来管理页
,位图比特位的数目与系统中物理内存页的数目相同。比特位为1,表示已用页
;比特位为0,表示空闲页。
在需要分配内存时,分配器逐位扫描位图,直至找到一个能提供足够连续页的位置,即所谓的最先最佳(first-best)或最先适配位置。
该过程不是很高效,因为每次分配都必须从头扫描比特链。因此在内核完全初始化之后,不能将该分配器用于内存管理。伙伴系统(连同slab、slub或slob分配器)是一个好得多的备选方案
-
bootmem 数据结构
//include/linux/bootmem.h //所有bootmem分配器保存在全局变量 bdata_list 中,用init_bootmem_core初始化. typedef struct bootmem_data { /* 保存系统中第一个物理页的地址,大多数体系结构是0 */ unsigned long node_boot_start; /* 可以直接管理的物理地址空间中最后一页的编号.换句话说,即 ZONE_NORMAL 的结束页. */ unsigned long node_low_pfn; /* 指向存储分配位图的内存区的指针。在IA-32系统上,用于该用途的内 * 存区紧接着内核映像之后。对应的地址保存在 _end 变量中,该变量在链接期间自动地插入到 * 内核映像中。 */ void *node_bootmem_map; unsigned long last_offset; /* 上一次分配的页的编号。如果没有请求分配整个页,则 last_offset 用作该页内 * 部的偏移量。这使得 bootmem 分配器可以分配小于一整页的内存区 */ unsigned long last_pos; /* 指定位图中上一次成功分配内存的位置,新的分配将由此开始 */ unsigned long last_success; /* Previous allocation point. To speed * up searching */ /* 链表元素,表头为 bdata_list*/ struct list_head list; } bootmem_data_t;
内存不连续的系统可能需要多个bootmem分配器。一个典型的例子是NUMA计算机,其中
每个结点注册了一个bootmem分配器
,但如果物理地址空间中散布着空洞,也可以为每个连续内存区注册一个bootmem分配器
。注册新的自举分配器可使用init_bootmem_core
在UMA系统上,只需一个bootmem_t实例,即
contig_bootmem_data
。它通过bdata成员与contig_page_data
关联起来//mm/page_alloc.c static bootmem_data_t contig_bootmem_data; struct pglist_data contig_page_data = { .bdata = &contig_bootmem_data }; //mm/bootmem.c //bootmem 分配器表头,元素为 bootmem_data_t->list static LIST_HEAD(bdata_list);
-
bootmem 初始化
bootmem分配器的初始化
是一个特定于体系结构的过程,此外还取决于所述计算机的内存布局。IA-32使用setup_memory
,该函数又调用setup_bootmem_allocator来初始化bootmem分配器,而AMD64则使用 contig_initmem_init
1. IA-32的bootmem初始化
- setup_memory
- setup_bootmem_allocator 初始化bootmem分配器,建立 bdata_list 链表并保留一些页
- init_bootmem 初始化 bootmem 分配器并加入全局链表 bdata_list,并将所有位图设为已使用
- register_bootmem_low_pages 释放可用的内存页,清除bootmem分配器位图中对应的位
- reserve_bootmem 分配内存页用于管理分配位图
- reserve_bootmem 分配内存页用于特殊用途如BIOS页,有些特定于计算机的功能需要该页才能运作正常
setup_memory分析检测到的内存区,以找到低端内存区中最大的页帧号。由于高端内存处理太麻烦,由此对bootmem分配器无用。全局变量max_low_pfn保存了可映射的最高页的编号。内核会在启动日志中报告找到的内存的数量。
```bash
wolfgang@meitner> dmesg
...
0MB HIGHMEM available.
511MB LOWMEM available.
...
```
2. AMD64的初始化,与IA32类似
- ......
- bootmem 给内核的接口
-
分配内存
UMA系统的函数:
alloc_bootmem(size) 和 alloc_bootmem_pages(size)
按指定大小在 ZONE_NORMAL 内存域分配内存。数据是对齐的,这使得内存或者从可适用于L1高速缓存的理想位置开始,或者从页边界开始
尽管 alloc_bootmem_pages 的名字暗示所需的内存长度是以页为单位,但实际上_pages只是指数据的对齐方式。alloc_bootmem_low 和 alloc_bootmem_low_pages
的 工 作 方 式 类 似 于 上 述 函 数 , 只 是 从ZONE_DMA内存域分配内存。因此,只有需要DMA内存时,才能使用上述函数NUMA系统的函数与UMA相同,只是增加了_node后缀
//include/linux/bootmem.h #define alloc_bootmem(x) \ __alloc_bootmem((x), SMP_CACHE_BYTES, __pa(MAX_DMA_ADDRESS)) #define alloc_bootmem_low(x) \ __alloc_bootmem((x), SMP_CACHE_BYTES, 0) #define alloc_bootmem_pages(x) \ __alloc_bootmem((x), PAGE_SIZE, __pa(MAX_DMA_ADDRESS)) #define alloc_bootmem_low_pages(x) \ __alloc_bootmem((x), PAGE_SIZE, 0)
- alloc_bootmem
- __alloc_bootmem
- __alloc_bootmem_nopanic
- __alloc_bootmem_core
- __alloc_bootmem_nopanic
- __alloc_bootmem
__alloc_bootmem,内存对齐方式有两个选项。
SMP_CACHE_BYTES
会对齐数据,使之在大多数体系结构上能够理想地置于L1高速缓存中(尽管名字带有SMP字样,但单处理器系统也会定义该常数)。PAGE_SIZE
将数据对齐到页边界。后一种对齐方式适用于分配一个或多个整页,但前者在分配涉及部分页时能够产生更好的结果低端DMA内存与普通内存的区别在于其起始地址。搜索适用于DMA的内存从地址0开始,而请求普通内存时则从MAX_DMA_ADDRESS向上(__pa将内存地址转换为物理地址)
- alloc_bootmem
-
释放内存
UMA:
free_bootmem
NUMA:
free_bootmem_node- free_bootmem
- free_bootmem_core 设置释放的页位图
- free_bootmem
-
4. 停用bootmem分配器
在系统初始化进行到伙伴系统分配器
能够承担内存管理的责任后,必须停用bootmem分配器,毕竟不能同时用两个分配器管理内存。在UMA和NUMA系统上,停用分别由 free_all_bootmem
和free_all_bootmem_node完成。在伙伴系统建立之后,特定于体系结构的初始化代码需要调用这两个函数。
- free_all_bootmem
- free_all_bootmem_core 释放bootmem分配器使用的内存到伙伴分配器中
-
释放初始化数据
许多内核代码块和数据表只在系统初始化阶段需要。如驱动的初始化函数,驱动初始化完毕在内存中建立了数据结构后函数就不需要了
内核使用__init 和 __initcall
来标记初始化函数和数据,这是用C编译器语句实现的
其实现思想是
:将数据保存在内核映像的一个特定部分(自定义的内存段中),在启动结束时从内存删除对应的段//include/linux/init.h #define __init #define __initdata __attribute__ ((__section__ (".init.text"))) _
__attribute__是一个特殊的GNU C关键字,属性即通过该关键字使用。__section__属性用于通知编译器将随后的数据或函数分别写入二进制文件的.init.data和.init.text段(不熟悉ELF文件结构的读者可参考附录E)。前缀__cold还通知编译器,通向该函数的代码路径可能性较低,即该函数不会经常调用,对初始化函数通常是这样
这个命令可以查看内核的各个段
readelf --sections vmlinux
内核只需要知道这些段的开始和结束地址就能释放对应的数据内存,这个信息是内核在链接时插入的,内核定义了变量
__init_begin和__init_end
保存free_initmem
函数用于释放初始化的内存区,并将页返回给伙伴系统。在启动过程刚好结束时会调用该函数,紧接其后init作为系统中第一个进程启动。启动日志包含了一条信息,指出释放了多少内存。wolfgang@meitner> dmesg ... Freeing unused kernel memory: 308k freed ...
物理内存的管理
在内核初始化完成后,内存管理的责任由伙伴系统承担。伙伴系统基于一种相对简单然而令人吃惊的强大算法,已经伴随我们几乎40年。它结合了优秀内存分配器的两个关键特征:速度和效率。bootmem分配器到buddy分配器的过渡
伙伴系统的结构
系统内存中的每个物理内存页(页帧),都对应于一个struct page实例。每个内存域都关联了一个struct zone的实例,其中保存了用于管理伙伴数据的主要数组。
cpu节点各内存域对页帧的管理如下图:
//include/linux/mmzone.h
struct zone {
...
/* 同名数据结构的数组,用于实现伙伴系统.
* 每个数组元素都表示某种固定长度的一些连续内存区。数组下标为阶数,即2^n
* 对于包含在每个区域中的空闲内存页的管理, free_area 是一个起点。
* */
struct free_area free_area[MAX_ORDER];
...
};
struct free_area {
/* 用于连接空闲页的链表头,页链表每个元素为 order阶的连续内存块,即如果order为2,则每个链表元素表示内存连续的2^2=4页内存,将空闲链表分为 MIGRATE_TYPES 种类型,链表元素为 page->lru*/
struct list_head free_list[MIGRATE_TYPES];
/*上面链表头数组中所有链表头里的链表元素的个数,按 order块统计
* (对0阶内存区逐页计算,对1阶内存区计算页对的数目,对2阶内存区计算4页集合的数目,依次类推)
* 阶是伙伴系统中一个非常重要的术语。它描述了内存分配的数量单位。内存块的长度是2^order ,其
* 中 order 的范围从0到 MAX_ORDER,MAX_ORDER通常设置为11,这意味着一次分配可以请求的页数最大是2^11 =2 048
* 宏for_each_migratetype_order(order, type) 可用于迭代指定迁移类型的所有分配阶。
* */
unsigned long nr_free;
};
#ifndef CONFIG_FORCE_MAX_ZONEORDER
#define MAX_ORDER 11
#else
#define MAX_ORDER CONFIG_FORCE_MAX_ZONEORDER
#endif
#define MAX_ORDER_NR_PAGES (1 << (MAX_ORDER -1))
阶
是伙伴系统中一个非常重要的术语。它描述了内存分配的数量单位。内存块的长度是2order,其中order的范围从0到MAX_ORDER
,该常数通常设置为11,,这意味着一次分配可以请求的页数最大是211=2 048。
free_area[]数组中各个元素的索引也解释为阶
,用于指定对应链表中的连续内存区包含多少个页帧。第0个链表包含的内存区为单页(20=1),第1个链表管理的内存区为两页(21=2),第3个管理的内存区为4页,依次类推
buddy4系统中页帧的管理
伙伴不必是彼此连接的。如果一个内存区
在分配其间分解为两半
,内核会自动将未用的一半加入到对应的链表中
。如果在未来的某个时刻,由于内存释放的缘故,两个内存区都处于空闲状态
,可通过其地址判断其是否为伙伴
。管理工作较少,是伙伴系统的一个主要优点。
基于伙伴系统
的内存管理专注于
某个结点的
某个内存域
,例如,DMA或高端内存域。但所有内存域和结点的伙伴系统都通过备用分配列表连接
起来。
在首选的内存域或节点无法满足内存分配请求时,首先尝试同一结点的另一个内存域,接下来再尝试另一个结点,直至满足请求。
有关伙伴系统当前状态的信息可以在/proc/buddyinfo
中获得
避免碎片
-
依据可移动性组织页
内核将已分配页划分为下面3种不同类型:- 不可移动页:在内存中有固定位置,不能移动到其他地方。核心内核分配的大多数内存属于该类别。
- 可回收页:不能直接移动,但可以删除,其内容可以从某些源重新生成。例如,映射自文件的数据属于该类别。kswapd守护进程会根据可回收页访问的频繁程度,周期性释放此类内存。这是一个复杂的过程,本身就需要详细论述:第18章详细描述了页面回收。目前,了解到内核会在可回收页占据了太多内存时进行回收,就足够了。另外,在内存短缺(即分配失败)时也可以发起页面回收。有关内核发起页面回收的时机,更具体的信息请参考下文。
- 可移动页可以随意地移动。属于用户空间应用程序的页属于该类别。它们是通过页表映射的。如果它们复制到新位置,页表项可以相应地更新,应用程序不会注意到任何事。
内核定义了一些宏来表示不同的
迁移类型
//include/linux/mmzone.h /* 不可移动 */ #define MIGRATE_UNMOVABLE 0 /* 可回收 */ #define MIGRATE_RECLAIMABLE 1 /* 可移动 */ #define MIGRATE_MOVABLE 2 /* 紧急分配,向具有特定可移动性的列表请求分配内存失败时用于分配,内存初始化时对应链表用 setup_zone_migrate_reserve 填充 */ #define MIGRATE_RESERVE 3 /* 特殊的虚拟区域,用于跨越NUMA结点移动物理内存页 */ #define MIGRATE_ISOLATE 4 /* 不能从这里分配 */ /* 表示迁移类型的数目0-4共5个 */ #define MIGRATE_TYPES 5 /* 用于遍历所有内存迁移类型(MIGRATE_TYPES)的所有分配阶 */ #define for_each_migratetype_order(order, type) \ for (order = 0; order < MAX_ORDER; order++) \ for (type = 0; type < MIGRATE_TYPES; type++) struct free_area { //将空闲链表头分为 MIGRATE_TYPES 种类型,链表元素为 page->lru struct list_head free_list[MIGRATE_TYPES]; //统计了上面那个成员里所有空闲页的数目 unsigned long nr_free; };
如果内核
无法满足
针对某一给定迁移类型的分配请求,提供了一个备用列表
,规定了在指定列表中无法满足分配请求时,接下来应使用哪一种迁移类型//mm/page_alloc.c /* * 该数组描述了指定迁移类型的空闲列表耗尽时,其他空闲列表在备用列表中的次序。 */ static int fallbacks[MIGRATE_TYPES][MIGRATE_TYPES-1] = { [MIGRATE_UNMOVABLE] = { MIGRATE_RECLAIMABLE, MIGRATE_MOVABLE, MIGRATE_RESERVE }, [MIGRATE_RECLAIMABLE] = { MIGRATE_UNMOVABLE, MIGRATE_MOVABLE, MIGRATE_RESERVE }, [MIGRATE_MOVABLE] = { MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE, MIGRATE_RESERVE }, [MIGRATE_RESERVE] = { MIGRATE_RESERVE, MIGRATE_RESERVE, MIGRATE_RESERVE }, /* Never used */ };
-
全局变量和辅助函数
两个全局变量用于给空闲内存迁移类型链表定义适当数量的内存
//mm/page_alloc.c //表示内核认为是“大”的一个分配阶 #ifdef CONFIG_HUGETLB_PAGE_SIZE_VARIABLE int pageblock_order __read_mostly; #endif //include/linux/pageblock-flags.h /* 内核认为是“大”的分配阶对应的页数 */ #define pageblock_nr_pages (1UL << pageblock_order) //如果体系结构提供了巨型页机制,则pageblock_order通常定义为巨型页对应的分配阶 #define pageblock_order HUGETLB_PAGE_ORDER
build_all_zonelists 函数中检查迁移类型链表内存是否足够,该函数用于初始化内存域链表。如果没有足够的内存可用,则全局变
page_group_by_mobility_disabled
设置为0,否则设置为1。内核确定给定的
分配内存属于
何种迁移类型
,有关各个内存分配的细节都通过分配掩码指定
:内核提供了两个标志,分别用于表示分配的内存是可移动的 ( __GFP_MOVABLE )或可回收的(__GFP_RECLAIMABLE)。如果这些标志都没有设置,则分配的内存假定为不可移动的转换分配标志及对应的迁移类型的函数:
//include/linux/gfp.h /* 将gfp标志转换为对应的迁移类型,该函数的返回值可以直接用作free_area.free_list的数组索引 */ static inline int allocflags_to_migratetype(gfp_t gfp_flags)
每个内存域都提供了一个特殊的字段,可以跟踪包含 pageblock_nr_pages 个页的内存区的属性。该字段当前只有与页可移动性相关的代码使用
//include/linux/mmzone.h struct zone { ... unsigned long *pageblock_flags; ...
在初始化期间,内核自动确保对内存域中的每个不同的迁移类型分组,在pageblock_flags中都分配了足够存储
NR_PAGEBLOCK_BITS
个比特位的空间。当前,表示一个连续内存区的迁移类型需要3个比特位//include/linux/pageblock-flags.h /* 用于帮助定义比特位范围的宏 */ #define PB_range(name, required_bits) \ name, name ## _end = (name + required_bits) - 1 /* 影响一整块内存的比特位索引 */ enum pageblock_bits { PB_range(PB_migrate, 3), /* 3 bits required for migrate types */ NR_PAGEBLOCK_BITS };
set_pageblock_migratetype
负责设置以page为首的一个内存区的迁移类型://mm/page_alloc.c static void set_pageblock_migratetype(struct page *page, int migratetype)
migratetype参数可以通过上文介绍的 allocflags_to_migratetype 辅助函数构建。请注意很重要的一点,页的迁移类型是预先分配好的,对应的比特位总是可用,与页是否由伙伴系统管理无关。在释放内存时,页必须返回到正确的迁移链表。这之所以可行,是因为能够从 get_pageblock_migratetype 获得所需的信息
在各个迁移链表之间,当前的页帧分配状态可以从
/proc/pagetypeinfo
获得
cat /proc/pagetypeinfo
-
初始化基于可移动性的分组
在内存子系统初始化期间,memmap_init_zone
负责处理内存域的page实例。将所有的页标记为可移动的- setup_arch
- zone_sizes_init
- free_area_init_nodes
- free_area_init_node
- free_area_init_core
- init_currently_empty_zone
- memmap_init
- memmap_init_zone
- zone_sizes_init
- setup_arch
-
虚拟可移动内存域
依据可移动性组织页是防止物理内存碎片的一种方法,内核还提供了另一种阻止该问题的手段:虚拟内存域ZONE_MOVABLE。该机制在内核2.6.23开发期间已经并入内核,比可移动性分组框架加入内核早一个版本。与可移动性分组相反,ZONE_MOVABLE特性必须由管理员显式激活基本思想很简单
:可用的物理内存划分为两个内存域
,一个用于可移动
分配,一个用于不可移动
分配。这会自动防止不可移动页向可移动内存域引入碎片。这使两种内存域同时存在,1(DMA NORMAL HIGHT)2(ZONE_UNMOVABLE ZONE_MOVABLE),虚拟内存域的开始页号从 1 的某个内存域中开始
这马上引出了另一个问题:内核如何在两个竞争的内存域之间分配可用的内存?这显然对内核要求太高,因此系统管理员必须作出决定。毕竟,人可以更好地预测计算机需要处理的场景,以及各种类型内存分配的预期分布。
-
数据结构
内核传递的kernelcore
参数用来指定用于不可移动分配的内存数量
,内核解析该参数后结果保存在全局变量required_kernelcore
中
还可以传递参数movablecore
控制用于可移动内存分配的内存数量。required_kernelcore的大小将会据此计算 。如果同时指定两个参数,内核会同时计算出required_kernelcore的值,并取指定值和计算值中较大的一个
取决于体系结构和内核配置,ZONE_MOVABLE内存域可能位于高端或普通内存域
与系统中所有其他的内存域相反,ZONE_MOVABLE并不关联到任何硬件上有意义的内存范围
。实际上,该内存域中的内存取自高端内存域或普通内存域
,因此称ZONE_MOVABLE是一个虚拟内存域。find_zone_movable_pfns_for_nodes
函数用于计算进入ZONE_MOVABLE
的内存数量。如果kernelcore 和 movablecore
参数都没有指定 , find_zone_movable_pfns_for_nodes 会使 ZONE_MOVABLE保持为空,该机制处于无效状态从物理内存域提取多少内存用于ZONE_MOVABLE
必须考虑下面两种情况:- 用于不可移动分配的内存会
平均
地分布到所有内存结点上。 - 只使用来自最高内存域的内存。在内存较多的32位系统上,这通常会是ZONE_HIGHMEM,但是对于64位系统,将使用ZONE_NORMAL或ZONE_DMA32。
实际计算相当冗长,因此不详细讨论。
实际上起作用的是结果
:- 用于为虚拟内存域ZONE_MOVABLE提取内存页的物理内存域,保存在全局变量
movable_zone
中; - 对每个结点来说,
zone_movable_pfn[node_id]
表示ZONE_MOVABLE在movable_zone内存域中所取得内存的起始地址
。
//mm/page_alloc.c unsigned long __meminitdata zone_movable_pfn[MAX_NUMNODES];
- 实现
具体实现在后面介绍,目前只要知道所有可移动分配都必须指定__GFP_HIGHMEM和__GFP_MOVABLE
分配标志即可
在内存域枚举类型中增加了ZONE_MOVABLE内存域,分配内存域时指定这个值,这是将 ZONE_MOVABLE 集成到伙伴系统中所需的唯一改变!其余的可以通过适用于所有内存域的通用例程处理
- 用于不可移动分配的内存会
-
初始化内存域和结点数据结构
体系结构相关代码需要在启动期间建立以下信息:
- 系统中各个内存域的页帧边界,保存在 zone_sizes_init 函数的临时变量 max_zone_pfns 数组
- 各结点页帧的分配情况,保存在全局变量 early_node_map 中(zone_sizes_init函数中调用 add_active_range)
-
管理数据结构的创建
内核2.6.10后提供了通用框架将上面的信息转为伙伴系统预期的结点和内存域数据结构,由
free_area_init_nodes
函数实现
-
对各个结点创建数据结构
//mm/memory.c //保存cpu内存结点0中的node_mem_map,即 mem_map = NODE_DATA(0)->node_mem_map,在alloc_node_mem_map函数中初始化 struct page *mem_map;
buddy分配器api
buddy分配器只能分配2的整数幂个页,不像bootmem分配器那样指定了所需内存大小作为参数,内核中细粒度的分配只能借助于slab分配器(或者slub、slob分配器)
分配内存函数:
//include/linux/gfp.h
//分配2^order页并返回一个struct page的实例,表示分配的内存块的起始页。alloc_page(mask)是前者在order = 0情况下的简化形式,只分配一页。
#define alloc_pages(gfp_mask, order) \
alloc_pages_node(numa_node_id(), gfp_mask, order)
/* 是alloc_pages在 order = 0 情况下的简化形式,只分配一页 */
#define alloc_page(gfp_mask) alloc_pages(gfp_mask, 0)
/* 分配1页并返回分配内存块的虚拟地址 */
#define __get_free_page(gfp_mask) \
__get_free_pages((gfp_mask),0)
/* 分配2^order用于DMA的页 */
#define __get_dma_pages(gfp_mask, order) \
__get_free_pages((gfp_mask) | GFP_DMA,(order))
//mm/page_alloc.c
/* 分配2^order页并返回分配内存块的虚拟地址 */
fastcall unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
/* 分配一页并返回一个 page 实例,
* 页对应的内存填充0 (所有其他函数,
* 分配之后页的内容是未定义的)
* */
fastcall unsigned long get_zeroed_page(gfp_t gfp_mask)
内核除了伙伴系统函数之外,还提供了其他内存管理函数
。它们以伙伴系统为基础
。这些函数包括vmalloc和vmalloc_32
,使用页表将不连续的内存映射到内核地址空间中,使之看上去是连续的。还有一组kmalloc
类型的函数,用于分配小于一整页的内存区
。
释放内存函数:
//include/linux/gfp.h
/* 释放一页到伙伴系统,内存区的起始地址由指向该内存区的第一个page实例的指针表示 */
#define __free_page(page) __free_pages((page), 0)
/* 释放一页到伙伴系统,使用虚拟内存地址 */
#define free_page(addr) free_pages((addr),0)
//mm/page_alloc.c
/* 将2^order页返回给伙伴系统.内存区的起始地址由指向该内存区的第一个 page 实例的指针表示 */
fastcall void __free_pages(struct page *page, unsigned int order)
/* 将2^order页返回给伙伴系统.使用虚拟内存地址, */
fastcall void free_pages(unsigned long addr, unsigned int order)
-
分配掩码 gfp_t
前面各种函数的 mask 参数.内核提供了所谓的
内存域修饰符(zone modifier)(在掩码的最低4个比特位定义)
,来指定从哪个内存域分配所需的页
//include/linux/gfp.h /* 缩写 GFP 代表获得空闲页(get free page) */ /* GFP_ZONEMASK中的内存域修饰符(参见linux/mmzone.h,低3位) */ #define __GFP_DMA ((__force gfp_t)0x01u) #define __GFP_HIGHMEM ((__force gfp_t)0x02u) #define __GFP_DMA32 ((__force gfp_t)0x04u) ... #define __GFP_MOVABLE ((__force gfp_t)0x100000u) /* 页是可移动的 */
没有__GFP_NORMAL常数,而内存分配的主要负担却落到ZONE_NORMAL内存域
//mm/page_alloc.c /* 提供了一个函数来计算与给定分配标志兼容的最高内存域。那么内存分配可以从该内存域或更低的内存域进行 */ static inline enum zone_type gfp_zone(gfp_t flags)
上图左边是设置的 gfp_t 标志,右边是标志对应的内存域扫描顺序
设置
__GFP_MOVABLE
不会影响内核的决策,除非它与__GFP_HIGHMEM同时指定
。在这种情况下,会使用特殊的虚拟内存域ZONE_MOVABLE满足内存分配请求
。对前文描述的内核的反碎片策略
而言,这种行为是必要的。除了内存域修饰符之外,掩码中还可以设置一些标志。
下图给出了掩码的布局,以及与各个比特位置关联的常数
。__GFP_DMA32出现了几次,因为它可能位于不同的地方
这些
额外的标志
并不限制从哪个物理内存段分配内存,但确实可以改变分配器的行为
。例如,它们可以修改查找空闲内存时的积极程度。内核源代码中定义的下列标志//include/linux/gfp.h /* 操作修饰符,不改变内存域 */ #define __GFP_WAIT ((__force gfp_t)0x10u) /* 可以等待和重调度? */ #define __GFP_HIGH ((__force gfp_t)0x20u) /* 应该访问紧急分配池? */ #define __GFP_IO ((__force gfp_t)0x40u) /* 可以启动物理IO? */ #define __GFP_FS ((__force gfp_t)0x80u) /* 可以调用底层文件系统? */ #define __GFP_COLD ((__force gfp_t)0x100u) /* 需要非缓存的冷页 */ #define __GFP_NOWARN ((__force gfp_t)0x200u) /* 禁止分配失败警告 */ #define __GFP_REPEAT ((__force gfp_t)0x400u) /* 重试分配,可能失败 */ #define __GFP_NOFAIL ((__force gfp_t)0x800u) /* 一直重试,不会失败 */ #define __GFP_NORETRY ((__force gfp_t)0x1000u) /* 不重试,可能失败 */ #define __GFP_NO_GROW ((__force gfp_t)0x2000u) /* slab内部使用 */ #define __GFP_COMP ((__force gfp_t)0x4000u) /* 增加复合页元数据 */ #define __GFP_ZERO ((__force gfp_t)0x8000u) /* 成功则返回填充字节0的页 */ #define __GFP_NOMEMALLOC ((__force gfp_t)0x10000u) /* 不使用紧急分配链表 */ #define __GFP_HARDWALL ((__force gfp_t)0x20000u) /* 只允许在进程允许运行的CPU所关联 * 的结点分配内存 */ #define __GFP_THISNODE ((__force gfp_t)0x40000u) /* 没有备用结点,没有策略 */ #define __GFP_RECLAIMABLE ((__force gfp_t)0x80000u) /* 页是可回收的 */ #define __GFP_MOVABLE ((__force gfp_t)0x100000u) /* 页是可移动的 */
由于这些
标志
几乎总是组合使用,内核作了一些分组
,包含了用于各种标准情形的适当的标志。如果有可能的话,在内存管理子系统之外,总是把下列分组之一用于内存分配。在内核源代码中,双下划线通常用于内部数据和定义
。而这些预定义的分组名没有双下划线前缀,这一点从侧面验证了上述说法。//include/linux/gfp.h /* 用于原子分配,在任何情况下都不能中断,可能使用紧急分配链表中的内存 */ #define GFP_ATOMIC (__GFP_HIGH) /* 禁止I/O操作,可以中断 */ #define GFP_NOIO (__GFP_WAIT) /* 禁止访问VFS层,可以中断 */ #define GFP_NOFS (__GFP_WAIT | __GFP_IO) /* 内核空间默认设置 */ #define GFP_KERNEL (__GFP_WAIT | __GFP_IO | __GFP_FS) /* 用户空间默认设置 */ #define GFP_USER (__GFP_WAIT | __GFP_IO | __GFP_FS | __GFP_HARDWALL) /* GFP_USER 的一个扩展,也用于用户空间。它允许分配无法直接映射的高端内存。 */ #define GFP_HIGHUSER (__GFP_WAIT | __GFP_IO | __GFP_FS | __GFP_HARDWALL | \ __GFP_HIGHMEM) /* GFP_USER 的一个扩展,也用于用户空间。它允许从虚拟内存域 ZONE_MOVABLE 进行分配。 */ #define GFP_HIGHUSER_MOVABLE (__GFP_WAIT | __GFP_IO | __GFP_FS | \ __GFP_HARDWALL | __GFP_HIGHMEM | \ __GFP_MOVABLE) /* 用于分配适用于 DMA 的内存 */ #define GFP_DMA __GFP_DMA #define GFP_DMA32 __GFP_DMA32
-
内存分配宏
通过使用标志、内存域修饰符和各个分配函数,内核提供了一种非常灵活的内存分配体系。所有接口函数都可以追溯到一个简单的基本函数(alloc_pages_node)
//include/linux/pagemap.h //获得热页 static inline struct page *page_cache_alloc(struct address_space *x) //获取冷页 static inline struct page *page_cache_alloc_cold(struct address_space *x)
伙伴系统分配页
伙伴系统所有分配函数最终都会调用 alloc_pages_node 函数
-
选择页
- 辅助函数
用于控制函数检查内存域struct zone分配水线行为的标志
//mm/page_alloc.c /* 下面设置的标志在 zone_watermark_ok 函数中检查,该函数根据设置的标志判断是否能从给定的内存域分配内存 */ /* 不检测水位线 */ #define ALLOC_NO_WATERMARKS 0x01 /* don't check watermarks at all */ /* 判断页是否可分配时,考虑哪些水位线 */ /* 内存域包含页的数目至少为 zone->pages_min 时,才能分配页 */ #define ALLOC_WMARK_MIN 0x02 /* use pages_min watermark */ /* 内存域包含页的数目至少为 zone->pages_low 时,才能分配页 */ #define ALLOC_WMARK_LOW 0x04 /* use pages_low watermark */ /* 内存域包含页的数目至少为 zone->pages_high 时,才能分配页 */ #define ALLOC_WMARK_HIGH 0x08 /* use pages_high watermark */ /* 伙伴系统急需内存时,降低申请内存的条件(在空闲内存很少时允许分配页,判断水线降到原来的四分之一) */ #define ALLOC_HARDER 0x10 /* try to alloc harder */ /* 在分配高端内存域的内存时, ALLOC_HIGH 进一步放宽限制(在空闲内存很少时允许分配页,判断水线降到原来的一半) */ #define ALLOC_HIGH 0x20 /* __GFP_HIGH set */ /* 内存只能从当前进程允许运行的CPU相关联的内存结点分配,当然该选项只对NUMA系统有意义 */ #define ALLOC_CPUSET 0x40 /* check for correct cpuset */ /* 根据设置的标志判断是否能从给定的内存域分配内存 */ int zone_watermark_ok(struct zone *z, int order, unsigned long mark, int classzone_idx, int alloc_flags) /* 通过标志集和分配阶来判断是否能进行分配。如果可以,则发起实际的分配操作 */ static struct page * get_page_from_freelist(gfp_t gfp_mask, unsigned int order, struct zonelist *zonelist, int alloc_flags)
- 分配控制
- alloc_pages_node
- __alloc_pages 函数很长,主要分配在这个函数
- get_page_from_freelist 判断能否分配内存并分配内存
- zone_watermark_ok 判断能否从给定内存域分配内存
- buffered_rmqueue 最终操作分配内存,页相关链表的处理
- __rmqueue 从内存域空闲区链表头中获取要申请的页,如有必要,该函数会自动分解大块内存,将未用的部分放回列表中
- __rmqueue_smallest 从内存域空闲区链表头中获取要申请的页
- __rmqueue_fallback 尝试其他的迁移链表
- __rmqueue 从内存域空闲区链表头中获取要申请的页,如有必要,该函数会自动分解大块内存,将未用的部分放回列表中
- get_page_from_freelist 判断能否分配内存并分配内存
- __alloc_pages 函数很长,主要分配在这个函数
- alloc_pages_node
//mm/page_alloc.c struct page * fastcall __alloc_pages(gfp_t gfp_mask, unsigned int order, struct zonelist *zonelist)
__alloc_pages函数说明
:第一次
分配最简单,直接调用 get_page_from_freelist 获取内存页,如果失败,第二次
调用 wakeup_kswapd 函数唤醒换出页守护进程,将申请内存的标志修改更可能分配到内存的值,然后再调用 get_page_from_freelist 获取内存,如果失败,第三次
,
if
如果设置了 PF_MEMALLOC 或进程设置了 TIF_MEMDIE ,且没设置__GFP_NOMEMALLOC,则会再次调用 get_page_from_freelist,这次不检测水线值。在这里搜索可能因为两个原因结束:
(1) 设置了__GFP_NOMEMALLOC。该标志禁止使用紧急分配链表(如果忽略水线,这可能是最佳途径),因此无法在禁用水线的情况下调用get_page_from_freelist。在这种情况下内核最终只能失败,跳转到nopage标号,通过内核消息将失败报告给用户,并将NULL指针返回调用者。
(2) 在忽略水线的情况下,get_page_from_freelist仍然失败了。在这种情况下,也会放弃搜索,报告错误消息。但如果设置了__GFP_NOFAIL,内核会进入无限循环(通过跳转到nofail_alloc标号实现),首先等待(通过congestion_wait)块设备层结束“占线”,在回收页时可能出现这种情况。接下来再次尝试分配,直至成功。
else
如果没设置 PF_MEMALLOC ,且设置了可等待标志 __GFP_WAIT 则进入低速路径,会开始一些耗时操作,设置 PF_MEMALLOC 标志,将很少使用的页换出到块介质通过 try_to_free_pages 函数,使内存有更多空间,但耗时。如果要分配多页,则通过 drain_all_local_pages 函数将冷热页缓存也拿回伙伴系统,
if
如果 try_to_free_pages 释放了页则继续调用 get_page_from_freelist 获取内存,
else
设置了 __GFP_FS 且没设置 __GFP_NORETRY ,则调用 get_page_from_freelist 分配内存,如果继续失败,且内核可能执行影响VFS层的调用而又没有设置 GFP_NORETRY 那么调用OOM killer(OOM是out of memory的缩写)(out_of_memory函数)杀死分配内存过多的进程然后跳转到 restart 标号处重试分配内存
else
没设置 __GFP_FS 且没设置 __GFP_NORETRY 则根据要分配的页的大小进入 rebalance 标号处无限循环直到分配成功
如果设置了 __GFP_NORETRY ,则失败返回总结:第一次直接申请,第二次降低申请水线到四分之一申请,第三次忽略水线申请,第四次手动将页换出继续申请,如果没页被换出,使用OOM杀死用内存多的进程继续申请,第五次回到第3次重新开始申请直到成功
- 辅助函数
-
从内存域中移除要分配的页
buffered_rmqueue
函数说明:如果只申请了一页,则从冷热页缓存中获取,如果申请了多页则从内存域中获取,最后通过prep_new_page
函数检查分配的页是否无法使用,并初始化页,如果申请了多页,则组成复合页复合页(compound page),第一个页称作首页(head page),而所有其余各页称作尾页(tail page)。
复合页
通过PG_compound标志位识别。组成复合页的所有页的page实例的private成员,包括首页在内,都指向首页
。此外,内核需要存储一些信息,描述如何释放复合页。这包括一个释放页的函数,以及组成复合页的页数。第一个尾页的LRU链表元素因此被滥用:指向析构函数的指针保存在lru.next,而分配阶保存在lru.prev
。请注意,lru成员无法用于这用途
,因为如果将复合页连接到内核链表中,是需要该成员的。为什么需要该信息?内核可能合并多个相邻的物理内存页,形成所谓的巨型TLB页。在用户层应用程序处理大块数据时,许多处理器允许使用巨型TLB页,将数据保存在内存中。由于巨型TLB页比普通页大,这降低了保存在地址转换后备缓冲器(TLB)中的信息的数量,因而又降低了TLB缓存失效的概率,从而加速了内存访问。但与多个普通页组成的复合页相比,巨型TLB页
需要用不同的方法释放,因而需要一个显式的析构器函数。 free_compound_pages 用于该目的
。本质上,在释放复合页时,该函数通过lru.prev确定页的分配阶,并依次释放各页
。
辅助函数prep_compound_page
用于设置以上描述的结构,即组成复合页结构复合页结构:
__rmqueue 函数
__rmqueue
函数说明:调用__rmqueue_smallest
从内存域空闲区链表头中获取要申请的页,如果失败则调用__rmqueue_fallback
从其他迁移链表申请__rmqueue_smallest
函数说明:遍历内存域中比所需分配阶大的内存块链表找到所需内存块,如果分配了更高阶的内存块,则调用expand
将内存块分解为所需阶数的内存块,将多余的内存块放回对应阶数的链表中expand
函数说明:从获取的内存块的阶数到分配所需的内存阶数循环,每次将内存块减半,将后一半返回对应阶的空闲链表头__rmqueue_fallback
函数说明:遍历备用迁移类型数组中下一个可用迁移类型从对应内存域的迁移类型链表头中获取内存块,分解为所需阶数的内存块,如果遍历完仍无法满足分配请求,则调用__rmqueue_smallest
尝试从 MIGRATE_RESERVE 列表满足分配请求
释放页 __free_pages
- __free_pages
- free_hot_page 单页释放给热页
- free_hot_cold_page
- free_pages_bulk 冷热页缓存内存过多时按 batch 释放给伙伴系统
- free_hot_cold_page
- __free_pages_ok 多页释放给伙伴系统
- free_one_page 处理单页和复合页的释放
- __free_one_page 处理单页和复合页的释放
- free_one_page 处理单页和复合页的释放
- free_hot_page 单页释放给热页
__free_one_page 函数假定释放一个0阶内存块,即一页,该页的索引为10时的计算:
__free_one_page
函数说明:根据要释放的页块的相连的页块是否在伙伴系统中(即相连页块空闲),将两个页块合并并升阶,直到无法合并,最后加入到对应阶的链表中
内核中不连续页的分配,vmalloc区域的内存使用,地址在内核直接映射的892M地址+8M隔离区后的位置
vmalloc分配的内存地址在虚拟地址上连续,但在物理地址上不连续,使用了页表结构
//arch/x86/kernel/init_task.c
//页表结构起始位置
struct mm_struct init_mm = INIT_MM(init_mm);
分配物理上连续的映射内存对内核是最好的,但并不总能成功地使用。在分配一大块内存时,可能竭尽全力也无法找到连续的内存块。在用户空间中这不是问题,因为普通进程使用了处理器的分页机制,当然这会降低速度并占用TLB。内核也可以使用同样的技术,为此内核分配了vmalloc区域用于该功能.
在IA-32系统中,紧随直接映射的前892 MiB物理内存,在插入的8 MiB安全隙之后,是一个用于管理不连续内存的区域(vmalloc)
。这一段具有线性地址空间的所有性质。分配到其中的页可能位于物理内存中的任何地方。通过修改负责该区域的内核页表即可用于分配不连续内存
。
每个vmalloc分配的子区域都是自包含的,与其他vmalloc子区域通过一个内存页分隔。类似于直接映射和vmalloc区域之间的边界,不同vmalloc子区域之间的分隔也是为防止不正确的内存访问操作
。这种情况只会因为内核故障而出现,应该通过系统错误信息报告,而不是允许内核其他部分的数据被暗中修改。因为分隔是在虚拟地址空间中建立的,不会浪费宝贵的物理内存页。
-
用vmalloc分配内存
vmalloc是一个接口函数,内核代码使用它来分配在虚拟内存中连续但在物理内存中不一定连续的内存
。//mm/vmalloc.c void *vmalloc(unsigned long size)
使用vmalloc的最著名的实例是内核对模块的实现。因为模块可能在任何时候加载,如果模块数据比较多,那么无法保证有足够的连续内存可用,特别是在系统已经运行了比较长时间的情况下。如果能够用小块内存拼接出足够的内存,那么使用vmalloc可以规避该问题
因为用于vmalloc
的内存页总是必须映射在内核地址空间中,因此使用 ZONE_HIGHMEM 内存域的页要优于其他内存域
。这使得内核可以节省更宝贵的较低端内存域,而又不会带来额外的坏处。因此,vmalloc 是内核出于自身的目的(并非因为用户空间应用程序)使用高端内存页的少数情形之一-
数据结构
//include/linux/vmalloc.h /*每个用vmalloc分配的子区域,都对应于内核内存中的一个该结构实例*/ struct vm_struct { /* keep next,addr,size together to speedup lookups */ struct vm_struct *next;/*单链表,vmalloc申请的所有该结构组成,链表头为struct vm_struct *vmlist;*/ void *addr;/*分配区域在虚拟地址空间中的起始地址*/ unsigned long size;/*分配区域长度,单位字节*/ unsigned long flags;/*标志集合,用于指定内存区类型, VM_MAP 等*/ struct page **pages;/*page指针的数组*/ unsigned int nr_pages;/*page指针数组中成员的个数,即页数*/ unsigned long phys_addr;/*仅当用ioremap映射了由物理地址描述的物理内存区域时才需要*/ };
假设映射了3个物理内存页,在物理内存中的位置分别是1 023、725和7 311。在虚拟的vmalloc区域中,内核将其看作起始于VMALLOC_START + 100的一个连续内存区:
-
分配和释放 vm_struct 结构体的内存
//mm/vmalloc.c //vm_struct链表头 struct vm_struct *vmlist; struct vm_struct *get_vm_area(unsigned long size, unsigned long flags) struct vm_struct *remove_vm_area(void *addr);
-
get_vm_area 在vmalloc空间中找到合适的位置并申请 vm_struct 结构的内存加入链表
- __get_vm_area
- __get_vm_area_node 申请 vm_struct 内存,将 vm_struct 加入链表
- kmalloc_node 申请 vm_struct 内存
- __get_vm_area_node 申请 vm_struct 内存,将 vm_struct 加入链表
- __get_vm_area
-
remove_vm_area 从vm_struct链表删除,解除在页表中的映射
- __remove_vm_area 从vm_struct链表删除,解除在页表中的映射
- unmap_vm_area
- unmap_kernel_range
- unmap_vm_area
- __remove_vm_area 从vm_struct链表删除,解除在页表中的映射
-
-
分配 vm_struct 结构管理的内存
vmalloc发起对不连续的内存区的分配操作
//mm/vmalloc.c //分配vmalloc区的内存,size:字节 void *vmalloc(unsigned long size)
- vmalloc
- __vmalloc
- __vmalloc_node
- get_vm_area_node 在vmalloc空间中找到合适的位置并申请 vm_struct 结构的内存加入链表
- __vmalloc_area_node 分配 vm_struct 管理的物理内存,将这些页连续地映射到vmalloc区域中
- alloc_page 从伙伴系统申请内存页
- alloc_pages_node 从伙伴系统申请内存页
- map_vm_area 将分散的物理内存页连续映射到虚拟的vmalloc区域
- __vmalloc_node
- __vmalloc
- vmalloc
-
-
其他映射函数
除了vmalloc之外,还有其他函数可以创建虚拟连续映射,都基于__vmalloc函数或使用非常类似的机制//mm/vmalloc.c //确保用32位指针寻址 void *vmalloc_32(unsigned long size) //用指定物理页映射到虚拟地址 void *vmap(struct page **pages, unsigned int count, unsigned long flags, pgprot_t prot) //arch/x86/mm/ioremap_32.c //特定体系结构的函数,将取自物理地址空间、由系统总线用于I/O操作的一个内存块,映射到内核的地址空间中,将io地址映射到虚拟地址,直接操作io管脚 void __iomem * __ioremap(unsigned long phys_addr, unsigned long size, unsigned long flags)
-
释放内存
vfree
用于释放vmalloc 和 vmalloc_32
分配的内存,而vunmap
用于释放由vmap 或 ioremap
创建的映射。这两个函数都会归结到__vunmap
//mm/vmalloc.c void __vunmap(void *addr, int deallocate_pages)
内核映射
-
永久映射
在高端内存的PKMAP_BASE 到 FIXADDR_START
区用于永久映射
用kmap
函数永久映射高端内存区- 数据结构
//mm/highmem.c //每个成员都对应于一个永久映射页计数器,计数器值为n代表内核中有n-1处使用该页,计数器值1表示该位置关联的页已经映射 static int pkmap_count[LAST_PKMAP]; //建立物理内存页的page实例与其在虚似内存区中位置之间的关联,page→virtual的映射 struct page_address_map { struct page *page;//指向全局mem_map数组中的page实例的指针 void *virtual;//指定了该页在内核虚拟地址空间中分配的位置 struct list_head list;//链表元素,表头为 page_address_htable->lh }; //管理物理地址与虚拟地址映射的 struct page_address_map 链表 static struct page_address_slot { struct list_head lh; /* List of page_address_maps *//*表头,链表元素为 page_address_map->list*/ spinlock_t lock; /* Protect this bucket's list */ } ____cacheline_aligned_in_smp page_address_htable[1<<PA_HASH_ORDER]; static struct page_address_slot *page_slot(struct page *page) //通过page找到在高端内存永久映射区中的虚拟地址 void *page_address(struct page *page)
-
查找页地址
page_address函数的流程:
-
创建映射
//arch/x86/mm/highmem_32.c //建立page的映射 void *kmap(struct page *page) //include/linux/highmem.h //通用版 kmap,在不需要高端内存页的体系结构上(或没有设置CONFIG_HIGHMEM)使用 static inline void *kmap(struct page *page) //mm/highmem.c //建立page到高端内存永久映射区的映射 void fastcall *kmap_high(struct page *page)
- kmap
- page_address page不在高端内存调用,直接返回地址
- kmap_high page在高端内存则调用,建立高端内存持久映射区的映射
- page_address 将page转为虚拟地址
- map_new_virtual 建立在高端内存持久映射区的映射
- flush_all_zero_pkmaps //刷出CPU高速缓存,解除永久映射与页表的映射关系
- kmap
-
解除映射
//arch/x86/mm/highmem_32.c //解除映射 void kunmap(struct page *page) //include/linux/highmem.h //通用版 kunmap,在不需要高端内存页的体系结构上(或没有设置CONFIG_HIGHMEM)使用 #define kunmap(page) do { (void) (page); } while (0) //mm/highmem.c //映射页在 pkmap_count 中的计数-1 void fastcall kunmap_high(struct page *page)
- kunmap
- kunmap_high 映射页在 pkmap_count 中的计数-1
- kunmap
-
临时内核映射
在固定映射区
中有一段内存区FIX_KMAP_BEGIN 到 FIX_KMAP_END
之间建立一个用于映射高端内存页的区域,该区域位于fixed_addresses
数组中用于临时映射
。准确的位置需要根据当前活动的CPU和所需映射类型计算:idx = type + KM_TYPE_NR*smp_processor_id(); vaddr = __fix_to_virt(FIX_KMAP_BEGIN + idx);
在固定映射区域中,系统中的每个处理器都有一个对应的“窗口”。每个窗口中,每种映射类型都对应于一项,如下图所示。根据这种布局,我们很清楚函数在使用kmap_atomic时不会阻塞。如果发生阻塞,那么另一个进程可能建立同样类型的映射,覆盖现存的项
kmap函数不能用于中断,因为在pkmap数组中没有空闲位置时,该函数会进入睡眠状态
因此内核提供了一个备选的映射函数,其执行是原子的为kmap_atomic
,该函数的优点是它比普通的kmap快速。但它不能会休眠的代码,因此用于申请会快速被释放的临时页,它的实现与体系结构相关//include/asm-x86/kmap_types.h enum km_type { D(0) KM_BOUNCE_READ, D(1) KM_SKB_SUNRPC_DATA, D(2) KM_SKB_DATA_SOFTIRQ, D(3) KM_USER0, D(4) KM_USER1, D(5) KM_BIO_SRC_IRQ, D(6) KM_BIO_DST_IRQ, D(7) KM_PTE0, D(8) KM_PTE1, D(9) KM_IRQ0, D(10) KM_IRQ1, D(11) KM_SOFTIRQ0, D(12) KM_SOFTIRQ1, D(13) KM_TYPE_NR }; //arch/x86/mm/highmem_32.c //申请固定映射内存区中临时映射区的内存(设置页表) void *kmap_atomic(struct page *page, enum km_type type) //释放临时映射内存(删除页表) void kunmap_atomic(void *kvaddr, enum km_type type)
- kmap_atomic
- kmap_atomic_prot 申请固定映射内存区中临时映射区的内存
- kmap_atomic
-
没有高端内存的计算机上的映射函数
许多体系结构不支持高端内存,因为不需要该特性,64位体系结构就是如此,为了在使用上述函数时不需要总是区分高端内存和非高端内存体系结构,内核定义了几个在普通内存实现兼容函数的宏(在支持高端内存的计算机上,如果停用了高端内存,也会使用这些宏)//include/linux/highmem.h #ifdef CONFIG_HIGHMEM ... #else static inline void *kmap(struct page *page) { might_sleep(); return page_address(page); } #define kunmap(page) do { (void) (page); } while (0) #define kmap_atomic(page, idx) page_address(page) #define kunmap_atomic(addr, idx) do { } while (0) #endif
slab分配器
伙伴系统分配以页为单位,太大了,slab分配器在此基础上实现了小内存的申请,它也用作一个缓存(slab释放的内存不会马上给伙伴系统,保留一段时间防止频繁调用伙伴系统算法使处理时间变长)
slab分配器由何得名?各个缓存管理的对象,会合并为较大的组,覆盖一个或多个连续页帧。这种组称作slab,每个缓存由几个这种slab组成
备选分配器
对小型嵌入式系统来说slab分配器代码量和复杂性都太高,占用太多内存,因此2.6增加了两个替代品
- slob分配器对slab进行了优化,减少代码量围绕一个简单的内存块链表展开,从速度来说,它不是最高效的分配器
- slub分配器,将页帧打包为组,并通过struct page中未使用的字段来管理这些组,试图最小化所需的内存开销,这样做不会简化该结构的定义,但在大型计算机上slub比slab提供了更好的性能
内核默认使用slab,这三种分配器提供的接口
是相同的:
- kmalloc,__kmalloc,kmalloc_node:结点内存分配器
- kmem_cache_alloc,kmem_cache_alloc_node:结点特定类型的内核缓存
slab.h 提供了分配器的接口,具体选择哪种分配器编译时确定
内核中的内存管理
类似应用层的malloc函数
//include/linux/slab_def.h
//include/linux/slob_def.h
//include/linux/slub_def.h
//内核找到最适合的缓存,并从中分配一个对象满足请求
static __always_inline void *kmalloc(size_t size, gfp_t flags)
//mm/slab.c
//mm/slob.c
//mm/slub.c
void kfree(const void *block)
//include/linux/percpu.h
//分配只被系统CPU使用的内存区,用于cpu相关变量
#define percpu_alloc(size, gfp) percpu_alloc_mask((size), (gfp), cpu_online_map)
//mm/allocpercpu.c
void percpu_free(void *__pdata)
//mm/slab.c
//mm/slob.c
//mm/slub.c
//从伙伴系统分配内存创建缓存
struct kmem_cache *
kmem_cache_create (const char *name, size_t size, size_t align,
unsigned long flags,
void (*ctor)(struct kmem_cache *, void *))
//释放创建的缓存到伙伴系统
void kmem_cache_destroy(struct kmem_cache *s)
//释放分配的内存到缓存中
void kmem_cache_free(struct kmem_cache *cachep, void *objp)
//include/linux/slob_def.h
//mm/slab.c
//mm/slub.c
//从创建的缓存中分配内存
void *kmem_cache_alloc(struct kmem_cache *s, gfp_t gfpflags)
使用缓存的调用过程:
- kmem_cache_create 创建缓存
- kmem_cache_alloc 从缓存分配内存
- kmem_cache_free 释放内存到缓存
- kmem_cache_destroy 释放缓存到伙伴系统
应用层查看所有活动的缓存:
cat /proc/slabinfo
信息如下:
- 缓存中活动对象的数量。
- 缓存中对象的总数(已用和未用)。
- 所管理对象的长度,按字节计算。
- 一个slab中对象的数量。
- 每个slab中页的数量。
- 活动slab的数量。
- 在内核决定向缓存分配更多内存时,所分配对象的数量。每次会分配一个较大的内存块,以减少与伙伴系统的交互。在缩小缓存时,也使用该值作为释放内存块的大小
//include/linux/slab.h
#define KMALLOC_SHIFT_HIGH ((MAX_ORDER + PAGE_SHIFT -1) <= 25 ? \
(MAX_ORDER + PAGE_SHIFT -1) : 25)
/*slab缓存创建的最大值*/
#define KMALLOC_MAX_SIZE (1UL << KMALLOC_SHIFT_HIGH)
#define KMALLOC_MAX_ORDER (KMALLOC_SHIFT_HIGH -PAGE_SHIFT)
slab分配的原理
-
- 数组的指针,保存了各个CPU最后释放的对象
- 每个内存结点都对应3个表头,用于组织slab的链表。第1个链表包含完全用尽的slab,第2个是部分空闲的slab,第3个是空闲的slab
为最好地利用CPU高速缓存。在分配和释放对象时,采用
后进先出
原理(LIFO,last in first out).内核假定刚释放的对象仍然处于CPU高速缓存中,会尽快再次分配它(响应下一个分配请求)。仅当per-CPU缓存为空时,才会用slab中的空闲对象重新填充它们,这样形成了三级结构
,分配成本和操作对CPU高速缓存和TLB的负面影响逐级升高- CPU高速缓存中的内存
- 现存slab中未使用的内存
- 刚使用伙伴系统分配内存
上图是一个初始化好的slab:
染色段用长度为64*colour,来对cpu L1缓存行进行更好的利用
slab_mgmt 为结构体 struct slab,
bufctl数组的含义是:数组长度是slab中对象的数量;每个数组元素对应slab中的一个对象;每个数组元素的值对应下一个空闲状态对象的 bufctl 数组元素下标。
一个slab中所有对象的信息在 slab_mgmt 中管理,slab_mgmt 结构体的 free 字段指向了 bufctl 数组的第一个元素下标0,含义是整个slab中第一个空闲状态对象的bufctl数组元素下标为0. 而bufctl[0]
又指向第二个元素的下标1,最后一个数组元素指向 BUFCTL_END 表示结束染色段:
设有一个cache,其中有2个在使用的slab,存储的对象大小是119字节,硬件cache行大小为64字节,slab长度为2个页面长度(8192字节)
由于对象大小无法整除硬件cache行大小,必然存在对象加载进硬件cache时被硬件cache行割裂的情况,其后果就是硬件cache必须将对象后续部分再次加载进cache。这种情况是我们无论如何不想看到的,因此我们宁可浪费几个字节的内存,也要让对象能够与硬件cache行对齐,这里对齐到128字节,在119字节后填充了9字节.
为了避免对象割裂,我们也会对slab中对象的起始位置进行设计,使起始位置也是硬件cache行对齐的,用ALIGN()对齐到64字节,ALIGN(sizeof(slab_mgmt)+sizeof(bufctl_t)*i,64)+sizeof(obj)*i
根据这个公式和长度上限(slab页面长度),我们很容易找到一个最大值i,在我们的案例中,最大值i为62. 在我们的slab包含的两个页面上,含有62个对象、1个长度为62的bufctl数组和1个长度为40的slab_mgmt管理数据结构。并且实际使用的有效部分长度为8128字节
2个页面的长度是8192字节,但在我们的slab中只使用了8128字节。相当于“浪费”了64字节
当所有的slab_mgmt数据结构均从slab的头部开始存放时,对于两个存放相同种类对象的slab,其内部的对象起始偏移量应当相同,当同时存在多个存放该对象的slab时,由于对象起始位置相同,可能发生这样一种情况:对不同slab内对象访问使用时,这些对象映射到了相同的硬件cache行中,这就导致硬件cache必须频繁读写某一个行,但没有利用其他硬件行,进而导致性能的下降。
为了缓解这个问题,我们使用“染色”技术,将相同结构的slab起始位置手动错开一个硬件cache行的长度,从而使具有相同偏移量的对象在加载到硬件cache时占据不同的行。在我们的案例中,由于浪费的空间恰好为64字节,也就是一个硬件cache行的长度,因此我们可以染两种颜色:0和1,对染色0的slab,其slab_mgmt前没有染色段,放置的偏移量为0;对染色1的slab,其slab_mgmt前有一个1*64字节的染色段,放置的偏移量为64. -
slab的精细结构
对象在slab中并非连续排列,而是按照一个相当复杂的方案分布,细节如下:
对象长度进行了对齐舍入,有两种对齐方式:
-
slab创建时使用用标志
SLAB_HWCACHE_ALIGN
,slab用户可以要求对象按硬件缓存行对齐。那么会按照cache_line_size
的返回值进行对齐,该函数返回特定于处理器的L1缓存大小。如果对象小于缓存行长度的一半,那么将多个对象放入一个缓存行 -
如果不要求按硬件缓存行对齐,那么内核保证对象按
BYTES_PER_WORD
对齐,该值按void指针的大小对齐(按sizeof(void*)对齐)。管理结构位于每个slab的起始处,保存了所有的管理数据(和用于连接缓存链表的链表元素).其后面是一个数组,每个(整数)数组项对应于slab中的一个对象,它指定了下一个空闲对象的索引,数组的最后一项总是一个结束标记,值为
BUFCTL_END
大多数情况下,slab内存区的长度(减去了头部管理数据)是不能被(可能填补过的)对象长度整除的。因此,内核就有了一些多余的内存,可以用来以偏移量的形式给slab“着色”,缓存的各个slab成员会指定不同的偏移量,以便将数据定位到不同的缓存行,因而slab开始和结束处的空闲内存是不同的。在计算偏移量时,内核必须考虑其他的对齐因素。例如,L1高速缓存中数据的对齐
管理数据可以放置在slab自身,也可以放置到使用kmalloc分配的不同内存区中
,内核如何选择,取决于slab的长度和已用对象的数量.管理数据和slab内存之间的关联很容易建立,因为slab头包含了一个指针,指向slab数据区的起始处(无论管理数据是否在slab上)。slab首部位于slab外部的情况:
最后,内核需要一种方法,通过对象自身即可识别slab(以及对象驻留的缓存)。根据对象的物理内存地址,可以找到相关的页,因此可以在全局mem_map数组中找到对应的page实例。
我们已经知道,page结构包括一个链表元素,用于管理各种链表中的页。对于slab缓存中的页而言,该指针是不必要的,可用于其他用途 -
page->lru.next指向页驻留的缓存的管理结构,struct kmem_cache
-
page->lru.prev指向保存该页的slab的管理结构,struct slab
设置或读取slab信息分别由
page_set_slab
和page_get_slab
函数完成,带有_cache后缀的函数则处理缓存信息的设置和读取//mm/slab.c void page_set_cache(struct page *page, struct kmem_cache *cache) struct kmem_cache *page_get_cache(struct page *page) void page_set_slab(struct page *page, struct slab *slab) struct slab *page_get_slab(struct page *page)
此外,内核还对分配给slab分配器的每个物理内存页都设置标志
PG_SLAB
。 -
实现
slab系统中带有大量调试选项,遍布着预处理器语句,举例:
- 危险区(Red Zoning):在每个对象的
开始和结束处增加一个额外的内存区
,其中填充已知的字节模式。如果模式被修改,程序员在分析内核内存时注意到,可能某些代码访问了不属于它们的内存区。 - 对象毒化(Object Poisoning):在建立和释放slab时,将对象用预定义的模式填充。如果在对象分配时注意到该模式已经改变,程序员就知道已经发生了未授权访问
-
数据结构
//mm/slab.c //用于管理slab对象的链表的表头 struct kmem_list3 { //部分空闲链表头 struct list_head slabs_partial; /* partial list first, better asm code 首先是部分空闲链表,以便生成性能更好的汇编代码*/ struct list_head slabs_full; //完全空闲链表 struct list_head slabs_free; //slabs_free和slabs_partial中空闲对象总数 unsigned long free_objects; //指定了所有slab上容许未使用对象的最大数目。 unsigned int free_limit; unsigned int colour_next; /* Per-node cache coloring */ spinlock_t list_lock; struct array_cache *shared; /* shared per node */ struct array_cache **alien; /* on other nodes */ //定义了内核在两次尝试收缩缓存之间,必须经过的时间间隔。其想法是防止由于频繁的缓存收缩和增长操作而降低系统性能,这种操作可能在某些系统负荷下发生。该技术只在NUMA系统上使用,我们不会进一步关注 unsigned long next_reap; /* updated without locking */ //表示缓存是否是活动的。在从缓存获取一个对象时,内核将该变量的值设置为1。在缓存收缩时,该值重置为0。但内核只有在free_touched预先设置为0时,才会收缩缓存。因为1表示内核的另一部分刚从该缓存获取对象,此时收缩是不合适的 int free_touched; /* updated without locking */ }; //slab系统缓存结构体 struct kmem_cache { /* 1) per-cpu data, touched during every alloc/free */ /* per-cpu数据,在每次分配/释放时都会访问 */ /*指针数组,每个cpu一个数组项*/ struct array_cache *array[NR_CPUS]; /* 2) Cache tunables. Protected by cache_chain_mutex */ /* 可调整的缓存参数,被 cache_chain_mutex 保护 */ //指定了在per-CPU列表为空的情况下,从缓存的slab中获取对象的数目。它还表示在缓存增长时分配的对象数目 unsigned int batchcount; //指定了per-CPU列表中保存的对象的最大数目。如果超出该值,内核会将batchcount个对象返回到slab(如果接下来内核缩减缓存,则释放的内存从slab返回到伙伴系统) unsigned int limit; unsigned int shared; //指定了缓存中管理的对象的长度 unsigned int buffer_size; //倒数,因为在有些老计算机上除法比乘法慢,将除法转为了乘法.计算指向slab中一个元素指针的索引号,用元素指针地址减去起始地址除以元素大小,这个变量存了 buffer_size 的倒数用于乘法 u32 reciprocal_buffer_size; /* 3) touched by every alloc & free from the backend */ /* 后端每次分配和释放时调整 */ //定义缓存的全局性质,如果管理结构存储在slab外部,则置位CFLGS_OFF_SLAB unsigned int flags; /* constant flags 常数标志*/ //保存了可以放入slab的对象的最大数目 unsigned int num; /* # of objs per slab 每个slab中对象的数量*/ /* 4) cache_grow/shrink */ /* 缓存的增长和缩减 */ /* order of pgs per slab (2^n) 每个slab中的页数,以2为底的对数*/ //指定了slab管理的空间大小,即slab包含2^gfporder页 unsigned int gfporder; /* force GFP flags, e.g. GFP_DMA 强制 GFP标志,如 GFP_DMA*/ gfp_t gfpflags; /*实例——如果有5种可能的颜色(0, 1, 2, 3, 4),而偏移量单位是8字节,内核可以使用下列偏移量值:0×8= 0,1×8 = 8,2×8 = 16,3×8 = 24,4×8 = 32字节*/ //指定了颜色的最大数目 size_t colour; /* cache colouring range 缓存着色范围*/ //基本偏移量乘以颜色值获得的绝对偏移量,这也是用于NUMA计算机 unsigned int colour_off; /* colour offset 着色偏移*/ //如果slab头部的管理数据存储在slab外部,则slabp_cache指向分配所需内存的一般性缓存。如果slab头部在slab上,则slabp_cache为NULL指针 struct kmem_cache *slabp_cache; unsigned int slab_size; //是另一个标志集合,描述slab的“动态性质”,但当前没有定义标志 unsigned int dflags; /* dynamic flags 动态标志*/ /* constructor func 构造函数*/ //指向在对象创建时调用的构造函数 void (*ctor)(struct kmem_cache *, void *); /* 5) cache creation/removal */ /* 缓存创建/删除 */ //该缓存的名称, const char *name; //链表元素,链表头为 全局变量cache_chain struct list_head next; /* 6) statistics */ //指针数组,每个数组项对应于系统中一个可能的内存结点。kmem_list3中有3个slab列表(完全用尽、空闲、部分空闲),该结构必须放在结尾,在NUMA计算机上实际可用的结点数目可能会少一些。因而该数组需要的项数也会变少,内核在运行时对该结构分配比理论上更少的内存,就可以缩减该数组的项数。如果nodelists放置在该结构中间,就无法做到这一点,UMA计算器上只有一个结点不用考虑 struct kmem_list3 *nodelists[MAX_NUMNODES]; };
需要分配一个对象时,首先将访问cache本地缓存,从本地缓存中取出可用的空闲对象地址使用;当需要释放一个对象时,首先将访问cache本地缓存,将对象释放到缓存中。如果要分配一个对象,但本地缓存中没有可用对象,此时触发cache本地缓存的下限,考虑从底层的slab系统中创建一些可用对象。如果要释放一个对象,但本地缓存已经填满,此时触发cache本地缓存的上限,考虑批量将一组对象释放到slab中去,来清出一些本地缓存供使用:
//slab缓存结构体中每个cpu一个该结构,管理对应cpu的本地缓存 struct array_cache { //保存了当前可用对象的数目 unsigned int avail; unsigned int limit; unsigned int batchcount; //在从缓存移除一个对象时,将touched设置为1,而缓存收缩时,则将touched设置为0 unsigned int touched; spinlock_t lock; //伪数组,其中并没有数组项,这段空间是cache本地缓存的空间,存放了可用的空闲对象物理地址,array_cache描述符用于记录这段数组内部内容的属性 void *entry[]; /* * Must have this definition in here for the proper * alignment of array_cache. Also simplifies accessing * the entries. */ };
对象缓存api实现 kmem_cache_alloc,kmem_cache_free
-
初始化 kmem_cache_init
slab之前伙伴系统已经初始化,kmalloc依赖于slab,所以实现了
kmem_cache_init 函数用于初始化slab分配器
.但在多处理器系统上,启动CPU此时正在运行,而其他CPU尚未初始化.kmem_cache_init采用了一个多步骤过程,逐步激活slab分配器- kmem_cache_init创建系统中的第一个slab缓存,以便为kmem_cache的实例提供内存。为此,内核使用的主要是在编译时创建的静态数据。实际上,一个静态数据结构(initarray_cache)用作per-CPU数组。该缓存的名称是 cache_cache
- kmem_cache_init接下来初始化一般性的缓存,用作kmalloc内存的来源。为此,针对所需的各个缓存长度,分别调用kmem_cache_create。该函数起初只需要cache_cache缓存已经建立。但在初始化per-CPU缓存时,该函数必须借助于kmalloc,这尚且不可能
为解决该问题,内核使用了g_cpucache_up
变量,可接受以下4个值( NONE 、 PARTIAL_AC 、PARTIAL_L3、FULL),以反映kmalloc初始化的状态
最初内核的状态是NONE
。在最小的kmalloc缓存(在4 KiB内存页的计算机上提供32字节内存块,在其他页长度的情况下提供64字节内存块。初始化时,再次将一个静态变量用于per-CPU的缓存数据。
g_cpucache_up中的状态接下来设置为PARTIAL_AC
,意味着array_cache实例可以立即分配
如果初始化的长度还足够分配kmem_list3实例,则状态立即转变为PARTIAL_L3
。否则,只能等下一个更大的缓存初始化之后才变更
剩余kmalloc缓存的per-CPU数据现在可以用kmalloc创建,这是一个 arraycache_init 实例,只需要最小的kmalloc内存区 - 在kmem_cache_init的最后一步,把到现在为止一直使用的数据结构的所有静态实例化的成员,用kmalloc动态分配的版本替换。
g_cpucache_up 的状态现在是FULL
,表示slab分配器已经就绪,可以使用
-
创建缓存 kmem_cache_create
//mm/slab.c struct kmem_cache * kmem_cache_create (const char *name, size_t size, size_t align, unsigned long flags, void (*ctor)(struct kmem_cache *, void *))
-
分配对象 kmem_cache_alloc,kmalloc
//include/linux/slab.h void *kmem_cache_alloc (kmem_cache_t *cachep, gfp_t flags)
-
缓存增长 cache_grow
//mm/slab.c static int cache_grow(struct kmem_cache *cachep, gfp_t flags, int nodeid, void *objp)
-
释放对象 kmem_cache_free
//mm/slab.c void kmem_cache_free(struct kmem_cache *cachep, void *objp)
-
销毁缓存 kmem_cache_destroy
//mm/slab.c void kmem_cache_destroy(struct kmem_cache *cachep)
通用缓存,kmalloc,kfree
//include/linux/slab_def.h
/*通用cache的大小描述符(用于kmalloc和kfree)*/
struct cache_sizes {
/*内存区的长度,如果使用了dma,每个长度对应于两个slab缓存*/
size_t cs_size;
/*cache描述符*/
struct kmem_cache *cs_cachep;
};
extern struct cache_sizes malloc_sizes[];
//mm/slab.c
//数组包括了所有可用的长度,基本上都是2的幂次,介乎2^5 =32和2^25= 33 554 432之间,最大值依赖于KMALLOC_MAX_SIZE的设置,cs_cachep成员为空,在kmem_cache_init中初始化
struct cache_sizes malloc_sizes[] = {
#define CACHE(x) { .cs_size = (x) },
#include <linux/kmalloc_sizes.h>
CACHE(ULONG_MAX)
#undef CACHE
};
-
kmalloc
-
kfree
处理器高速缓存和TLB控制
高速缓存对系统总体性能十分关键,这也是内核尽可能提高其利用效率的原因。这主要是通过在内存中巧妙地对齐内核数据。审慎地混合使用普通函数、内联定义、宏,也有助于从处理器汲取更高的性能。
内核仍然提供了一些命令,可以直接作用于处理器的高速缓存和TLB。但这些命令并非用于提高系统的效率,而是用于维护缓存内容的一致性,确保不出现不正确和过时的缓存项
- TLB的语义抽象是将虚拟地址转换为物理地址的一种机制
- 内核将高速缓存视为通过虚拟地址快速访问数据的一种机制,该机制无需访问物理内存。数据和指令高速缓存并不总是明确区分。如果高速缓存区分数据和指令,那么特定于体系结构的代码负责对此进行处理
内核中各个特定于CPU的部分都必须提供下列函数(即使只是空操作),以便控制TLB和高速缓存
//include/asm-x86/tlbflush_32.h
/*刷出整个TLB。这只在操纵内核(而非用户空间进程的)页表时需要*/
#define flush_tlb_all() __flush_tlb_all()
/*刷出所有属于地址空间mm的TLB项*/
static inline void flush_tlb_mm(struct mm_struct *mm)
/*刷出地址范围vma->vm_mm中虚拟地址start和end之间的所有TLB*/
static inline void flush_tlb_range(struct vm_area_struct *vma,
unsigned long start, unsigned long end)
/*刷出虚拟地址在[page, page + PAGE_SIZE]范围内所有的TLB项*/
static inline void flush_tlb_page(struct vm_area_struct *vma,
unsigned long addr)
//include/asm-x86/cacheflush.h
/*刷出整个高速缓存。这只在操纵内核(而非用户空间进程的)页表时需要*/
#define flush_cache_all()
/*刷出所有属于地址空间mm的高速缓存项*/
#define flush_cache_mm(mm)
/*刷出地址范围vma->vm_mm中虚拟地址start和end之间的所有高速缓存项*/
#define flush_cache_range(vma, start, end)
/*刷出虚拟地址在[page, page + PAGE_SIZE]范围内所有的高速缓
存项*/
#define flush_cache_page(vma, vmaddr, pfn)
//include/asm-x86/pgtable_32.h
/*仅当存在外部MMU时,才需要该函数,通常MMU集成在处理器内部,在处理页失效之后调用。它在处理器的内存管理单元MMU中加入信息,使得虚拟地址address由页表项pte描述*/
#define update_mmu_cache(vma,address,pte)
内核对数据和指令高速缓存不作区分。如果需要区分,特定于处理器的代码可根据vm_area_struct->flags的VM_EXEC标志位是否设置,来确定高速缓存包含的是指令还是数据
flush_cache_和flush_tlb_函数经常成对出现
操作的顺序是:刷出高速缓存、操作内存、刷出TLB。这个顺序很重要:
- 如果顺序反过来,那么在TLB刷出之后、正确信息提供之前,多处理器系统中的另一个CPU可能从进程的页表取得错误的信息
- 在刷出高速缓存时,某些体系结构需要依赖TLB中的“虚拟->物理”转换规则(具有该性质的高速缓存称之为严格的)。flush_tlb_mm必须在flush_cache_mm之后执行,以确保这一点。有些控制函数明确地应用于数据高速缓存(flush_dcache_…)或指令高速缓存(flush_icache_…)。
- 如果高速缓存包含几个虚拟地址不同的项指向内存中的同一页,可能会发生所谓的alias问题,flush_dcache_page(struct page * page)有助于防止该问题。在内核向页缓存中的一页写入数据,或者从映射在用户空间中的一页读出数据时,总是会调用该函数。这个例程使得存在alias问题的各个体系结构有机会防止问题的发生
- 在内核向内核内存范围(start和end之间)写入数据,而该数据将在此后作为代码执行,则此时需要调用flush_icache_range(unsigned long start, unsigned long end)。该场景的一个标准事例是向内核载入模块时。二进制数据首先复制到物理内存中,然后执行。flush_icache_range确保在数据和指令高速缓存分别实现的情况下,二者彼此不发生干扰
- flush_icache_user_range(*vma, *page, addr, len)是一个特殊函数,用于ptrace机制。为将修改传送到被调试进程的地址空间,需要使用该函数
总结
IA-32:
linux内核将虚拟地址空间分成两部分.底部用于用户进程(3G),顶部用于内核(1G)
内核空间内存被分为3部分:
- ZONE_DMA 内存开始的16MB,用于DMA
- ZONE_NORMAL 16MB~896MB,内核直接映射的内存
- ZONE_HIGHMEM 896MB ~ 结束(1G),高端内存映射
高端内存映射
896M ~ 896M+VMALLOC_OFFSET(8M)用来防止越界错误
高端内存的映射有三种方式:
- 映射到“内核动态映射空间”.vmalloc()非连续内存区映射,VMALLOC_START到VMALLOC_END
- 永久内核映射.kmap(),从PKMAP_BASE到FIXADDR_START.在2.6内核上,这个地址范围是4G-8M到4G-4M之间
- 临时内核映射(固定映射).kmap_atomic(),FIXADDR_START 到 FIXADDR_TOP.每个CPU占其中一块,每个CPU块中的每页用于一个目的,定义在 km_type 中
内核页全局目录前768项(刚好3G,0xC0000000)除0、1两项外全部为0,后256项(1G)用来管理所有的物理内存.内核页全局目录在编译时静态地定义为 swapper_pg_dir 数组,该数组从物理内存地址0x101000处开始存放
内核线性地址空间部分从PAGE_OFFSET(通常定义为3G)开始.
从PAGE_OFFSET开始8M线性地址用来映射内核所在的物理内存地址.
接下来是mem_map数组,对于UMA结构,由于从PAGE_OFFSET开始16M线性地址空间对应的16M物理地址空间是DMA区,mem_map数组通常开始于PAGE_OFFSET+16M的线性地址
系统启动早期用 初始化自举内存分配器 管理未初始化的内存,特点是实现简单.启动分页后,停用 自举内存分配器 并初始化 伙伴分配器,在 伙伴分配器 基础上又 初始化了 slab分配器
为防止内存碎片,又将内存域分为了 可移动 与 不可移动,这会自动防止不可移动页向可移动内存域引入碎片
slab分配器为需要分配的数据结构类型分配建立的缓存,每次申请对应结构体类型占用的内存时从对应的缓存中分配.还初始化了一个通用缓存用于所有数据类型的内存分配
per_cpu_pageset机制,CPU高速缓存,在多核架构下每个cpu维护一份自己的page空闲缓存,用于减少zone->lock锁竞争问题,加速内存申请过程。注意为了减少对内存浪费per cpu page机制只维护order为0 page缓存
Linux系统中0阶内存分配需求是最多的, 而且经常存在频繁分配释放的行为,如果每次都去伙伴系统中申请,会经常需要获取zone->lock锁住整个zone区域。随着CPU核心数的增加,伙伴系统锁竞争激烈程度也会越来越大。
为了改善这个问题,linux内核中引入了per_cpu_pageset(下面简称pcp)。优化思路是先从zone一次性拿一些页出来,放到每个cpu自己本地中。释放也先放回到这里,等满了再一起还给zone。
=========================================
涉及的命令和配置:
zone_pcp_init(struct zone *zone)
结束时打印输出各个内存域的页数以及计算出的批量大小:
root@meitner # dmesg | grep LIFO
DMA zone: 2530 pages, LIFO batch:0
DMA32 zone: 833464 pages, LIFO batch:31
Normal zone: 193920 pages, LIFO batch:31
setup_memory
:分析检测到的内存区,以找到低端内存区中最大的页帧号。由于高端内存处理太麻烦,由此对bootmem分配器无用。全局变量max_low_pfn保存了可映射的最高页的编号。内核会在启动日志中报告找到的内存的数量。
wolfgang@meitner> dmesg
...
0MB HIGHMEM available.
511MB LOWMEM available.
...
System.map
:编译内核生成的文件,记录了所有符号的运行地址,这里的符号可以理解成函数名和变量
/proc/iomem
:提供了有关物理内存划分出的各个段的一些信息
//这个命令可以查看内核的各个段
readelf --sections vmlinux
wolfgang@meitner> readelf - sections vmlinux
There are 53 section headers, starting at offset 0x2c304c8:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[1] .text PROGBITS ffffffff80200000 00200000
000000000021fc6f 0000000000000000 AX 0 0 4096
[2] __ex_table PROGBITS ffffffff8041fc70 0041fc70
0000000000003e50 0000000000000000 A 0 0 8
[3] .notes NOTE ffffffff80423ac0 00423ac0
0000000000000024 0000000000000000 AX 0 0 4
...
[28] .init.text PROGBITS ffffffff8067b000 0087b000
000000000002026e 0000000000000000 AX 0 0 1
[29] .init.data PROGBITS ffffffff8069b270 0089b270
000000000000c02e 0000000000000000 WA 0 0 16
...
free_initmem
:函数用于释放初始化的内存区,并将页返回给伙伴系统。在启动过程刚好结束时会调用该函数,紧接其后init作为系统中第一个进程启动。启动日志包含了一条信息,指出释放了多少内存。
wolfgang@meitner> dmesg
...
Freeing unused kernel memory: 308k freed
...
/proc/buddyinfo
:有关伙伴系统当前状态的信息
wolfgang@meitner> cat /proc/buddyinfo
Node 0, zone DMA 3 5 7 4 6 3 3 3 1 1 1
Node 0, zone DMA32 130 546 695 271 107 38 2 2 1 4 479
Node 0, zone Normal 23 6 6 8 1 4 3 0 0 0 0
给出了各个内存域中每个分配阶中空闲项的数目,从左至右,阶依次升高。上面给出的
信息取自4 GiB物理内存的AMD64系统
//在各个迁移链表之间,当前的页帧分配状态可以从/proc/pagetypeinfo获得
cat /proc/pagetypeinfo
wolfgang@meitner> cat /proc/pagetypeinfo
Page block order: 9
Pages per block: 512
Free pages count per migrate type at order 0 1 2 3 4 5 6 7 8 9 10
Node 0, zone DMA, type Unmovable 0 0 1 1 1 1 1 1 1 1 0
Node 0, zone DMA, type Reclaimable 0 0 0 0 0 0 0 0 0 0 0
Node 0, zone DMA, type Movable 3 5 6 3 5 2 2 2 0 0 0
Node 0, zone DMA, type Reserve 0 0 0 0 0 0 0 0 0 0 1
Node 0, zone DMA, type <NULL> 0 0 0 0 0 0 0 0 0 0 0
Node 0, zone DMA32, type Unmovable 44 37 29 1 2 0 1 1 0 1 0
Node 0, zone DMA32, type Reclaimable 18 29 3 4 1 0 0 0 1 1 0
Node 0, zone DMA32, type Movable 0 0 191 111 68 26 21 13 7 1 500
Node 0, zone DMA32, type Reserve 0 0 0 0 0 0 0 0 0 1 2
Node 0, zone DMA32, type <NULL> 0 0 0 0 0 0 0 0 0 0 0
Node 0, zone Normal, type Unmovable 1 5 1 0 0 0 0 0 0 0 0
Node 0, zone Normal, type Reclaimable 0 0 0 0 0 0 0 0 0 0 0
Node 0, zone Normal, type Movable 1 4 0 0 0 0 0 0 0 0 0
Node 0, zone Normal, type Reserve 11 13 7 8 3 4 2 0 0 0 0
Node 0, zone Normal, type <NULL> 0 0 0 0 0 0 0 0 0 0 0
Number of blocks type Unmovable Reclaimable Movable Reserve <NULL>
Node 0, zone DMA 1 0 6 1 0
Node 0, zone DMA32 13 18 2005 4 0
Node 0, zone Normal 22 10 351 1 0
free_area_init_nodes
:将信息转为伙伴系统预期的结点和内存域数据结构
root@meitner # dmesg
...
Zone PFN ranges:
DMA 0 0 -> 4096
DMA32 4096 -> 1048576
Normal 1048576 -> 1245184
...
calculate_node_totalpages
:计算内存结点中的总内存页数
wolfgang@meitner> dmesg
...
On node 0 totalpages: 131056
...
//应用层查看所有活动的缓存:
cat /proc/slabinfo
wolfgang@meitner> cat /proc/slabinfo
slabinfo - version: 2.1
# name <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables
<limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail>
nf_conntrack_expect 0 0 224 18 1 : tunables 0 0 0 : slabdata 0 0 0
UDPv6 16 16 960 4 1 : tunables 0 0 0 : slabdata 4 4 0
TCPv6 19 20 1792 4 2 : tunables 0 0 0 : slabdata 5 5 0
xfs_inode 25721 25725 576 7 1 : tunables 0 0 0 : slabdata 3675 3675 0
xfs_efi_item 44 44 352 11 1 : tunables 0 0 0 : slabdata 4 4 0
xfs_efd_item 44 44 360 11 1 : tunables 0 0 0 :
slabdata 4 4 0
...
kmalloc-128 795 992 128 32 1 : tunables 0 0 0 : slabdata 31 31 0
kmalloc-64 19469 19584 64 64 1 : tunables 0 0 0 : slabdata 306 306 0
kmalloc-32 2942 2944 32 128 1 : tunables 0 0 0 : slabdata 23 23 0
kmalloc-16 2869 3072 16 256 1 : tunables 0 0 0 : slabdata 12 12 0
kmalloc-8 4075 4096 8 512 1 : tunables 0 0 0 : slabdata 8 8 0
kmalloc-192 2940 3276 192 21 1 : tunables 0 0 0 : slabdata 156 156 0
kmalloc-96 754 798 96 42 1 : tunables 0 0 0 : slabdata 19 19 0
CONFIG_ZONE_DMA
CONFIG_ZONE_DMA32
CONFIG_HIGHMEM
CONFIG_FLAT_NODE_MEM_MAP struct page数组,用于管理该内存结点上所有内存,每个内存页一个该结构体成员
CONFIG_SPARSEMEM 特殊字段用于跟踪包含 pageblock_nr_pages 个页的内存区的属性,该字段当前只有与页可移动性相关的代码使用
CONFIG_PHYSICAL_START 内核代码段映射到的内存开始位置
CONFIG_FORCE_MAX_ZONEORDER 一次能分配的最大的内存阶数
CONFIG_HUGETLB_PAGE_SIZE_VARIABLE 内核认为大
的分配阶
全局变量mem_map
数组,存着低端内存所有page.高端内存的page从全局变量page_address_htable中管理
//地址空间,用于管理文件(struct inode)映射到内存的页面(struct page)的结构体,因该叫物理页缓存结构体,页高速缓存(page cache)核心数据结构
struct address_space {
struct radix_tree_root page_tree; /*所有者基数树根节点,保存了所有页*//* radix tree of all pages */
};
address_space->page_tree 结构组成了页高速缓存的基数树结构: