现代操作系统的内存管理原理:以Linux2.6.x.x为例
标签: 现代操作系统的内存管理原理:以Linux2.6.x.x为例 jQuery博客 51CTO博客
2023-04-05 18:23:48 100浏览
不谈老掉牙的内存管理方式了。
本文使用的内核是Linux 2.6.x.x
版本。内存管理?内存管理!不管是在老版本的操作系统各个子系统中,亦或是现代版本操作系统中,都是极其复杂和庞大的。当然,万变不离其宗,但是,看源码可以让你找不到方向。本文在上一篇虚拟地址空间的基础上来解释,当然要把源码完完全全的列出来不太现实,大家对照自己的前置理解即可,如果有时间和精力去完完全全看懂源码才是最好!
虚拟地址空间那篇文章是从架构方面来解释的,如果想从细节的方面来解释可查看:一篇长文带你深析Linux动态链接的全过程。动态链接算是把
Glibc
、vir addr
和Dynamic Linking
用到了极致!
对照之前的Linux 0.11
和0.99
版本,2.6.x.x
版本是一个新的伟大的变革,后面的代码很多都是沿用该版本的代码,Linux 3.0
也有它的神奇之处:设备树。但是论变革性,我认为不如2.6
,因此选用该版本进行分析。
附:今天看了369的剑魔,我只能说,江山代有才人出!
参考书目:
- 《Linux架构深度解析》
- 《深入理解Linux内核》
- 《Linux源码剖析》
- 《Linux内核设计与实现》
参考资料:
参考文章:
- Linux的内核为什么一定要映射到所有的物理内存?
- 什么是虚拟地址空间?从架构视角来解释
- Linux内存管理之slab 1:slab原理(+buddy伙伴系统)
- slab着色
NUMA模型中的内存组织
对于NUMA
和UMA
的解释,可见:什么是虚拟地址空间?从架构视角来解释
简介:内存被分为节点
node
、区域zone
和页面page
三个层次组成(可以浅显的把每一根内存条看成一个node
)
enum zone_type {
#ifdef CONFIG_ZONE_DMA
ZONE_DMA, // 0-16MB
#endif
#ifdef CONFIG_ZONE_DMA32
ZONE_DMA32, // 使用32位地址字可寻址,只有在64位中才使用,32位中为0
#endif
ZONE_NORMAL, // 16MB-896MB
#ifdef CONFIG_HIGHMEM
ZONE_HIGHMEM, // 896MB-1G
#endif
ZONE_MOVABLE,
MAX_NR_ZONES // 结束标记
};
每个内存域zone
都关联了一个数组,用来组织属于该内存域的物理内存页。
各个内存节点node
保存在一个单链表中,供内核遍历。
处于性能考虑,在为进程分配内存时,内核总是试图在当前运行的
CPU
相关联的NUMA
节点上进行,但有时该节点的内存可能已经用尽,对于此类情况,每个节点都提供了一个备用列表struct zonelist
,该列表包含了其他节点,可用于代替当前节点分配内存(列表项的位置越靠后,就越不适合分配)
节点node
节点zone
的数据结构
typedef struct pglist_data {
struct zone node_zones[MAX_NR_ZONES]; // 包含了各个内存域的数据结构
struct zonelist node_zonelists[MAX_ZONELISTS]; // 备用节点及其内存域的列表
int nr_zones; // 节点中不同域的数量
#ifdef CONFIG_FLAT_NODE_MEM_MAP
struct page *node_mem_map; // 描述节点的所有物理内存页,包含了节点中所有内存域的页
#endif
struct bootmem_data *bdata; // bootmem内存分配器
#ifdef CONFIG_MEMORY_HOTPLUG
spinlock_t node_size_lock;
#endif
unsigned long node_start_pfn; // NUMA节点中第一个页帧的逻辑编号,一般是0
unsigned long node_present_pages; /* 物理内存页的总数 */
unsigned long node_spanned_pages; /* 物理内存页的总长度,包含洞在内 */
int node_id; // 全局节点ID,一般是0
wait_queue_head_t kswapd_wait; // 交换守护进程的等待队列,在将页帧换出节点时会用到
struct task_struct *kswapd; // 下一个节点,形成单链表
int kswapd_max_order; // 页交换子系统的实现,定义需要释放的区域的长度
} pg_data_t;
内核会维护一个位图,用以提供各个节点的状态信息,放在
node_state
中,相关函数有static inline void node_set_state(int node, enum node_states state)
和static inline void node_clear_state(int node, enum node_states state)
,可见nodemask.h
文件。
内存域zone
内存域zone
的数据结构
struct zone {
unsigned long pages_min, pages_low, pages_high; // 页换出时使用的水印,如果内存不足,可以将其写入到硬盘,这3个成员会影响交换守护进程的行为
unsigned long lowmem_reserve[MAX_NR_ZONES]; // 分别为各种内存域指定了若干页 用于一些无论如何都不能失败的关键
性内存分配。
#ifdef CONFIG_NUMA
int node;
unsigned long min_unmapped_pages;
unsigned long min_slab_pages;
struct per_cpu_pageset *pageset[NR_CPUS]; // 用于实现每个CPU的热/冷页帧列表
#else
struct per_cpu_pageset pageset[NR_CPUS];
#endif
spinlock_t lock;
#ifdef CONFIG_MEMORY_HOTPLUG
seqlock_t span_seqlock;
#endif
struct free_area free_area[MAX_ORDER]; // 伙伴系统
#ifndef CONFIG_SPARSEMEM
unsigned long *pageblock_flags;
#endif /* CONFIG_SPARSEMEM */
ZONE_PADDING(_pad1_) // 根据活动情况对内存域中使用的页进行编号。活动的页:页访问频繁
spinlock_t lru_lock;
struct list_head active_list; // 活动页的集合
struct list_head inactive_list; // 不活动页的集合
unsigned long nr_scan_active; // 回收时需要扫描的活动的页
unsigned long nr_scan_inactive; // 回收时需要扫描的不动的页
unsigned long pages_scanned; // 指定了上次换出一页以来,有多少页未能成功扫描
unsigned long flags; /* 描述内存域的当前状态 */
atomic_long_t vm_stat[NR_VM_ZONE_STAT_ITEMS]; // 内存域的统计信息
int prev_priority; // 存储了上一次扫描操作扫描该内存域的优先级
ZONE_PADDING(_pad2_)
wait_queue_head_t * wait_table; // 等待队列 可供等待某一页变为可用的进程使用
unsigned long wait_table_hash_nr_entries;
unsigned long wait_table_bits;
struct pglist_data *zone_pgdat;
unsigned long zone_start_pfn; // 内存域第一个页帧的索引
unsigned long spanned_pages; /* 内存域中页的总数 */
unsigned long present_pages; /* 内存域中页的长度,包括洞 */
const char *name; // 内存域的名称
} ____cacheline_internodealigned_in_smp;
内存水印
内存水印的概念
- 如果空闲页多于
pages_high
,则内存域的状态是理想的。 - 如果空闲页的数目低于
pages_low
,则内核开始将页换出到硬盘。 - 如果空闲页的数目低于
pages_min
,那么页回收工作的压力就比较大,因为内存域中急需空
闲页
# 查看内存域水印的大小
root@huawei linux-version # cat /proc/sys/vm/min_free_kbytes [0]
67584
数据结构中水印值的填充由init_per_zone_pages_min
处理,该函数由内核在启动期间调用,无需显式调用
/*
* min_free_kbytes = 4 * sqrt(lowmem_kbytes), for better accuracy:
* min_free_kbytes = sqrt(lowmem_kbytes * 16)
*
* which yields
*
* 16MB: 512k
* 32MB: 724k
* 64MB: 1024k
* 128MB: 1448k
* 256MB: 2048k
* 512MB: 2896k
* 1024MB: 4096k
* 2048MB: 5792k
* 4096MB: 8192k
* 8192MB: 11584k
* 16384MB: 16384k
*/
static int __init init_per_zone_pages_min(void)
{
// ...
setup_per_zone_pages_min(); // 设置pages_min, pages_low, pages_high
setup_per_zone_lowmem_reserve(); // 设置lowmem_reserve
return 0;
}
module_init(init_per_zone_pages_min)
冷热页
struct zone
的pageset
成员用于实现冷热分配器hot-n-cold allocator
。内核说页是热的,意味
着页已经加载到CPU
高速缓存,与在内存中的页相比,其数据能够更快地访问。相反,冷页则不在高
速缓存中。在多处理器系统上每个CPU
都有一个或多个高速缓存,各个CPU
的管理必须是独立的。
struct zone {
// ...
struct per_cpu_pageset pageset[NR_CPUS];
// ...
};
struct per_cpu_pageset {
struct per_cpu_pages pcp[2]; /* 0: hot. 1: cold */
// ...
} ____cacheline_aligned_in_smp;
struct per_cpu_pages {
int count; /* 列表中页数 */
int high; /* 页数上限水印 */
int batch; /* 添加/删除多页快的时候,块的大小 */
struct list_head list; /* 页的链表 */
};
初始化内存管理
主要有以下几部分:
- 在许多
CPU
上,必须显式设置适于Linux
内核的内存模型(一般是切换到PE模式和检测寄存器,由特定汇编构建,此处不做概述) - 建立内存管理的数据结构,以及其他很多事务
- 在系统启动过程中使用一个额外的简化形式的内存管理模块
建立数据结构
对相关数据结构的初始化是从start_kernel()
开始的,该例程在加载内核并激活各个子系统之后执行。由于内存管理是内核一个非常重要的部分,因此会在特定于体系结构的设置步骤中检测内存并确定系统中内存的分配情况后,会立即执行内存管理的初始化。
此时,已经对各种系统内存模式生成了一个pgdata_t
实例,用于保存诸如结点中内存数量以及内存在各个内存域之间分配情况的信息。
start_kernel()
--> setup_arch() // 是一个特定于体系结构的设置函数,其中一项任务是负责初始化自举分配器
--> setup_per_cpu_areas() // 初始化per-cpu 为系统的各个CPU分别创建一份这些数据的副本。
--> build_all_zonelists() // 建立结点和内存域的数据结构
--> mem_init() // 用于停用bootmem分配器并迁移到实际的内存管理函数 伙伴系统
--> kmem_cache_init() // 初始化内核内部用于小块内存区的分配器 slab
--> setup_per_cpu_pageset() // 还负责设置冷热分配器的限制和第一个处理器的冷热页帧分配
build_all_zonelists
void build_all_zonelists(void)
{
set_zonelist_order();
if (system_state == SYSTEM_BOOTING) {
__build_all_zonelists(NULL);
cpuset_init_current_mems_allowed();
} else {
stop_machine_run(__build_all_zonelists, NULL, NR_CPUS);
}
vm_total_pages = nr_free_pagecache_pages();
if (vm_total_pages < (pageblock_nr_pages * MIGRATE_TYPES))
page_group_by_mobility_disabled = 1;
else
page_group_by_mobility_disabled = 0;
printk("Built %i zonelists in %s order, mobility grouping %s. "
"Total pages: %ld\n",
num_online_nodes(),
zonelist_order_name[current_zonelist_order],
page_group_by_mobility_disabled ? "off" : "on",
vm_total_pages);
#ifdef CONFIG_NUMA
printk("Policy zone: %s\n", zone_names[policy_zone]);
#endif
}
static int __build_all_zonelists(void *dummy)
{
int nid;
for_each_online_node(nid) { // 遍历所有活动节点
pg_data_t *pgdat = NODE_DATA(nid); // 获得节点及其信息
build_zonelists(pgdat); // 使用pgdat 包含节点内存配置的所有现存信息 配置内存分配层次信息
build_zonelist_cache(pgdat);
}
return 0;
}
特定于体系结构的设置
内核在内存中的布局
前4 KB是第一个页帧,一般会忽略,因为通常保留给
BIOS
使用
接下来的640 KiB原则上是可用的,但也不用于内核加载。该区域之后紧邻的区域由系统保留,用于映射各种ROM。不可能向映射ROM的区域写入数据。
内核总是会装载到一个连续的内存区中,如果要从4 KB处作为起始位置来装载内核映像,则要求内核必须小于640 KB。
为解决这些问题,IA-32
内核使用0x100000
作为起始地址。
内核占据的内存分为几个段
-
_text
和_etext
是代码段的起始和结束地址,包含了编译后的内核代码 - 数据段位于
_etext
和_edata
之间,保存了大部分内核变量 - 初始化数据在内核启动过程结束后不再需要。保存在最后一段,从
_edata
到_end
每次编译内核时,都生成一个文件
System.map
并保存在源代码目录下
machine_specific_memory_setup
:创建一个列表,包括系统占据的内存区和空闲内存区。BIOS提供的映射给出了在这种情况下使用的各个内存区
如果
BIOS
没有提供该信息(在较古老的机器上可能是这样),内核自身会生成一个表,将0~640 K和1 MB之前的内存标记为可用
parse_cmdline_early
:分析命令行。主要关注类似mem=XXX[KkmM]、highmem=XXX [kKmM]或memmap=XXX[KkmM]" "@XXX[KkmM]
之类的参数。如果内核计算的值或BIOS
提供的值不正确,
管理员可以修改可用内存的数量或手工划定内存区。
setup_memory
:确定(每个结点)可用的物理内存页的数目。初始化bootmem分配器。分配各种内存区。
paging_init
:初始化内核页表并启用内存分页。负责建立只能用于内核的页表,用户空间无法访问。
zone_sizes_init
:初始化系统中所有结点的pgdat_t
实例。add_active_range()
和free_area_init_nodes()
函数
对于分页机制的初始化可见:Linux内核空间中的高端内存HighMem
关于此处有一个疑问:为何内核的高端内存区域要映射到所有的物理内存呢?
按照现在的内核设计,是由内核决定哪块物理内存给哪个进程用,所以内核必然要对所有物理内存有掌控权;(感觉没有太大说服力,但是这样又似乎能说得通,慢慢学习吧)
paging_init()
的运行步骤:
pagetable_init
:初始化系统的页表,以swapper_pg_dir
为基础。
kernel_physical_mapping_init
:将物理内存页(前896MB
空间)映射到虚拟地址空间中从PAGE_OFFSET
开始的位置。接下来建立固定映射项和持久内核映射对应的内存区。同样是用适当的值填充页表。
在用
pagetable_ini
t完成页表初始化之后,则将cr3
寄存器设置为指向全局页目录(swapper_ pg_dir
)的指针。此时必须激活新的页表。
__flush_all_tlb
由:于TLB缓存项仍然包含了启动时分配的一些内存地址数据,此时也必须刷出。
kmap_init
:初始化全局变量kmap_pte
。在从高端内存域将页映射到内核地址空间时,会使用该变量存入相应内存区的页表项。此外,用于高端内存内核映射的第一个固定映射内存区的地址保存在全局变量kmem_vstart
中。
冷热缓存的初始化
注册内存活动区
启动过程期间的内存管理
在启动过程期间,尽管内存管理尚未初始化,但内核仍然需要分配内存以创建各种数据结构。bootmem
分配器用于在启动阶段早期分配内存。
该分配器使用一个位图来管理页,位图比特位的数目与系统中物理内存页的数目相同。比特位为
1,表示已用页;比特位为0,表示空闲页。
在需要分配内存时,分配器逐位扫描位图,直至找到一个能提供足够连续页的位置,即所谓的最
先最佳(first-best)或最先适配位置。
typedef struct bootmem_data {
unsigned long node_boot_start; // 系统中第一个页的编号,大多数体系结构下都是0
unsigned long node_low_pfn; // 直接管理的物理地址空间中最后一页的编号 即ZONE_NORMAL的结束页
void *node_bootmem_map; // 是指向存储分配位图的内存区的指针
unsigned long last_offset; //
unsigned long last_pos; // 上一次分配的页的编号
unsigned long last_success; // 指定位图中上一次成功分配内存的位置,新的分配将由此开始
struct list_head list;
} bootmem_data_t;
NUMA
计算机,其中每个结点注册了一个bootmem
分配器,但如果物理地址空间中散布着空洞,也可以为每个连续内存区注册一个bootmem
分配器。
注册新的自举分配器可使用init_bootmem_core
,所有注册的分配器保存在一个链表中,表头
是全局变量bdata_list
。
static unsigned long __init init_bootmem_core(pg_data_t *pgdat,
unsigned long mapstart, unsigned long start, unsigned long end)
{
bootmem_data_t *bdata = pgdat->bdata;
unsigned long mapsize;
bdata->node_bootmem_map = phys_to_virt(PFN_PHYS(mapstart));
bdata->node_boot_start = PFN_PHYS(start);
bdata->node_low_pfn = end;
link_bootmem(bdata);
mapsize = get_mapsize(bdata);
memset(bdata->node_bootmem_map, 0xff, mapsize);
return mapsize;
}
在UMA
系统上,只需一个bootmem_t
实例,即contig_bootmem_data
。它通过bdata
成员与contig_page_data
关联起来
#ifndef CONFIG_NEED_MULTIPLE_NODES
static bootmem_data_t contig_bootmem_data;
struct pglist_data contig_page_data = { .bdata = &contig_bootmem_data };
EXPORT_SYMBOL(contig_page_data);
#endif
以IA-32
为例
setup_memory
分析检测到的内存区,以找到低端内存区中最大的页帧号。全局变量max_low_pfn
保存了可映射的最高页的编号。内核会在启动日志中报告找到的内存的数量
由于高端内存处理太麻烦,由此对
bootmem
分配器无用。
void __init setup_bootmem_allocator(unsigned long free_pfn)
{
unsigned long bootmap_size;
bootmap_size = init_bootmem_node(NODE_DATA(0), free_pfn,
min_low_pfn, max_low_pfn);
// ...
register_bootmem_low_pages();
node_set_online(0);
reserve_bootmem(__MEMORY_START+PAGE_SIZE,
(PFN_PHYS(free_pfn)+bootmap_size+PAGE_SIZE-1)-__MEMORY_START);
reserve_bootmem(__MEMORY_START, PAGE_SIZE);
// ...
}
对内核的接口
#ifndef CONFIG_HAVE_ARCH_BOOTMEM_NODE
extern void reserve_bootmem(unsigned long addr, unsigned long size);
#define alloc_bootmem(x) \
__alloc_bootmem(x, SMP_CACHE_BYTES, __pa(MAX_DMA_ADDRESS))
#define alloc_bootmem_low(x) \
__alloc_bootmem_low(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_low(x, PAGE_SIZE, 0)
#endif /* !CONFIG_HAVE_ARCH_BOOTMEM_NODE */
extern unsigned long free_all_bootmem(void);
extern unsigned long free_all_bootmem_node(pg_data_t *pgdat);
extern void *__alloc_bootmem_node(pg_data_t *pgdat,
unsigned long size,
unsigned long align,
unsigned long goal);
extern unsigned long init_bootmem_node(pg_data_t *pgdat,
unsigned long freepfn,
unsigned long startpfn,
unsigned long endpfn);
extern void reserve_bootmem_node(pg_data_t *pgdat,
unsigned long physaddr,
unsigned long size);
extern void free_bootmem_node(pg_data_t *pgdat,
unsigned long addr,
unsigned long size);
#ifndef CONFIG_HAVE_ARCH_BOOTMEM_NODE
#define alloc_bootmem_node(pgdat, x) \
__alloc_bootmem_node(pgdat, x, SMP_CACHE_BYTES, __pa(MAX_DMA_ADDRESS))
#define alloc_bootmem_pages_node(pgdat, x) \
__alloc_bootmem_node(pgdat, x, PAGE_SIZE, __pa(MAX_DMA_ADDRESS))
#define alloc_bootmem_low_pages_node(pgdat, x) \
__alloc_bootmem_low_node(pgdat, x, PAGE_SIZE, 0)
#endif /* !CONFIG_HAVE_ARCH_BOOTMEM_NODE */
物理内存的管理
伙伴系统的结构
系统内存中的每个物理内存页(页帧),都对应于一个struct page
实例。每个内存域都关联了一个struct zone
的实例,其中保存了用于管理伙伴数据的主要数组。
struct zone {
...
/*
* 不同长度的空闲区域
*/
struct free_area free_area[MAX_ORDER];
...
};
struct free_area {
struct list_head free_list[MIGRATE_TYPES]; // 用于连接空闲页的链表
unsigned long nr_free; // 当前内存区中空闲页块的数目
};
阶是伙伴系统中一个非常重要的术语。它描述了内存分配的数量单位。内存块的长度是2order
,其
中order
的范围从0到MAX_ORDER
。
/* Free memory management - zoned buddy allocator. */
#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))
free_area[]
数组中各个元素的索引也解释为阶。用于指定对应链表中的连续内存区包含多少个页帧。第0个链表包含的内存区为单页(20=1),第1个链表管理的内存区为两页(21=2),第3个管理的内存区为4页,依次类推。
伙伴不必是彼此连接的。如果一个内存区在分配其间分解为两半,内核会自动将未用的一半加入
到对应的链表中。如果在未来的某个时刻,由于内存释放的缘故,两个内存区都处于空闲状态,可通
过其地址判断其是否为伙伴。管理工作较少,是伙伴系统的一个主要优点。
基于伙伴系统的内存管理专注于某个结点的某个内存域,例如,DMA
或高端内存域。但所有内
存域和结点的伙伴系统都通过备用分配列表连接起来。
在首选的内存域或节点无法满足内存分配请求时,首先尝试同一结点的另一个内存域,接下来再
尝试另一个结点,直至满足请求。
root@huawei linux-version # cat /proc/buddyinfo [0]
Node 0, zone DMA 1 0 0 1 2 1 2 1 2 2 2
Node 0, zone DMA32 4145 7315 3523 965 567 140 31 10 4 0 0
Node 0, zone Normal 451 540 303 66 12 50 2 0 0 0 0
避免碎片
在系统长期运行之后,会产生许多内存碎片。
很多现代
CPU
都提供了巨型页的可能性,比普通页大的多,这对内存使用密集的程序有好处。在使用更大的页时,地址转换后备缓冲器只需处理较少的项,降低了TLB缓存失效的可能性
一般的方法是反碎片anti-fragmentation
,即试图从最初开始尽可能防止碎片。
一般内核把已分配内存页划分为三种类型
- 不可移动页:在内存中有固定位置,不能移动到其他地方。核心内核分配的大多数内存属于该类别
- 可回收页:不能直接移动,但可以删除,其内容可以从某些源重新生成。
- 可移动页可以随意地移动。属于用户空间应用程序的页属于该类别。它们是通过页表映射的。如果它们复制到新位置,页表项可以相应地更新,应用程序不会注意到任何事。
可移动页可以直接换位置。对于不可移动页和可回收页可以建立两个列表:不可移动列表和可回收页列表。当进行分配的时候,在可回收页中,可以选择暂时释放空出空闲空间,然后分配连续性内存空间即可。
在最初开始,内存并未划分为可移动性不同的区,这些是在运行时形成的。
#define MIGRATE_UNMOVABLE 0 // 不可移动
#define MIGRATE_RECLAIMABLE 1 // 可回收
#define MIGRATE_MOVABLE 2 // 可移动
#define MIGRATE_RESERVE 3 // 当向具有特定可移动性的列表请求分配内存失败,此时可以向MIGRATE_RESERVE分配内存
#define MIGRATE_ISOLATE 4 /* can't allocate from here */
#define MIGRATE_TYPES 5 // 迁移类型的数目,不表示具体的区域
对伙伴系统的调整
struct free_area { // 用MIGRATE_TYPES将其分为不同的列表
struct list_head free_list[MIGRATE_TYPES]; // 用于连接空闲页的链表
unsigned long nr_free; // 当前内存区中空闲页块的数目
};
如果内核无法满足针对某一给定迁移类型的分配请求,提供了一个备用列表,规定了在指定列表中无法满足分配请求时,接下来应使用哪一种迁移类型:
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 */
};
尽管页可移动性分组特性总是编译到内核中,但只有在系统中有足够内存可以分配到多个迁移类型对应的链表时,才是有意义的.。由于每个迁移链表都应该有适当数量的内存,内核需要定义“适当”的概念。
这是通过两个全局变量pageblock_order
和pageblock_nr_pages
提供的。第一个表示内核认为是“大”的一个分配阶,pageblock_nr_pages
则表示该分配阶对应的页数。
// 支持巨型页
#define pageblock_order HUGETLB_PAGE_ORDER
// 不支持巨型页
#define pageblock_order (MAX_ORDER-1)
如果各迁移类型的链表中没有一块较大的连续内存,那么页面迁移不会提供任何好处,因此在可用内存太少时内核会关闭该特性。这是在build_all_zonelists
函数中检查的,该函数用于初始化内存域列表。如果没有足够的内存可用,则全局变量page_group_by_mobility
设置为0,否则设置为1。
有关各个内存分配的细节都通过分配掩码指定。内核提供了两个标志,分别用于表示分配的内存是可移动的__GFP_ MOVABLE
或可回收的__GFP_RECLAIMABLE
。如果这些标志都没有设置,则分配的内存假定为不可移动的。
// 转换分配标志与类型
static inline int allocflags_to_migratetype(gfp_t gfp_flags)
{
WARN_ON((gfp_flags & GFP_MOVABLE_MASK) == GFP_MOVABLE_MASK);
if (unlikely(page_group_by_mobility_disabled))
return MIGRATE_UNMOVABLE;
/* Group based on mobility */
return (((gfp_flags & __GFP_MOVABLE) != 0) << 1) |
((gfp_flags & __GFP_RECLAIMABLE) != 0);
}
在初始化期间,内核自动确保对内存域中的每个不同的迁移类型分组,在pageblock_flags
中都分配了足够存储NR_PAGEBLOCK_BITS
个比特位的空间。当前,表示一个连续内存区的迁移类型需要3个比特位:
// 用于帮助定义比特位范围的宏
#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
为首的一个内存区的迁移类型
static void set_pageblock_migratetype(struct page *page, int migratetype)
{
set_pageblock_flags_group(page, (unsigned long)migratetype,
PB_migrate, PB_migrate_end);
}
void set_pageblock_flags_group(struct page *page, unsigned long flags,
int start_bitidx, int end_bitidx)
{
struct zone *zone;
unsigned long *bitmap;
unsigned long pfn, bitidx;
unsigned long value = 1;
zone = page_zone(page);
pfn = page_to_pfn(page);
bitmap = get_pageblock_bitmap(zone, pfn);
bitidx = pfn_to_bitidx(zone, pfn);
for (; start_bitidx <= end_bitidx; start_bitidx++, value <<= 1)
if (flags & value)
__set_bit(bitidx + start_bitidx, bitmap);
else
__clear_bit(bitidx + start_bitidx, bitmap);
}
查看相关链表
root@huawei linux-version # cat /proc/pagetypeinfo [130]
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 1 0 0 1 2 1 1 0 1 0 0
Node 0, zone DMA, type Movable 0 0 0 0 0 0 0 0 0 1 2
Node 0, zone DMA, type Reclaimable 0 0 0 0 0 0 1 1 1 1 0
Node 0, zone DMA, type HighAtomic 0 0 0 0 0 0 0 0 0 0 0
Node 0, zone DMA, type CMA 0 0 0 0 0 0 0 0 0 0 0
Node 0, zone DMA, type Isolate 0 0 0 0 0 0 0 0 0 0 0
Node 0, zone DMA32, type Unmovable 580 543 454 128 79 6 1 1 0 0 0
Node 0, zone DMA32, type Movable 2740 2510 1927 740 505 121 20 9 5 0 0
Node 0, zone DMA32, type Reclaimable 685 1556 1241 346 180 46 8 2 1 0 0
Node 0, zone DMA32, type HighAtomic 0 0 0 0 0 0 0 0 0 0 0
Node 0, zone DMA32, type CMA 0 0 0 0 0 0 0 0 0 0 0
Node 0, zone DMA32, type Isolate 0 0 0 0 0 0 0 0 0 0 0
Node 0, zone Normal, type Unmovable 469 256 171 13 2 0 0 0 0 0 0
Node 0, zone Normal, type Movable 49 79 8 2 3 33 4 0 0 0 0
Node 0, zone Normal, type Reclaimable 3 79 102 42 12 7 4 1 0 0 0
Node 0, zone Normal, type HighAtomic 41 37 21 7 2 0 0 0 0 0 0
Node 0, zone Normal, type CMA 0 0 0 0 0 0 0 0 0 0 0
Node 0, zone Normal, type Isolate 0 0 0 0 0 0 0 0 0 0 0
Number of blocks type Unmovable Movable Reclaimable HighAtomic CMA Isolate
Node 0, zone DMA 1 5 2 0 0 0
Node 0, zone DMA32 51 1296 181 0 0 0
Node 0, zone Normal 52 438 21 1 0 0
在内存子系统初始化期间,memmap_init_zone
负责处理内存域的page实例,标记所有的页最初都标记为可移动的
void __meminit memmap_init_zone(unsigned long size, int nid, unsigned long zone,
unsigned long start_pfn, enum memmap_context context)
{
struct page *page;
unsigned long end_pfn = start_pfn + size;
unsigned long pfn;
for (pfn = start_pfn; pfn < end_pfn; pfn++) {
/*
* There can be holes in boot-time mem_map[]s
* handed to this function. They do not
* exist on hotplugged memory.
*/
if (context == MEMMAP_EARLY) {
if (!early_pfn_valid(pfn))
continue;
if (!early_pfn_in_nid(pfn, nid))
continue;
}
page = pfn_to_page(pfn);
set_page_links(page, zone, nid, pfn);
init_page_count(page);
reset_page_mapcount(page);
SetPageReserved(page);
if ((pfn & (pageblock_nr_pages-1)))
set_pageblock_migratetype(page, MIGRATE_MOVABLE);
INIT_LIST_HEAD(&page->lru);
#ifdef WANT_PAGE_VIRTUAL
/* The shift won't overflow because ZONE_NORMAL is below 4G. */
if (!is_highmem_idx(zone))
set_page_address(page, __va(pfn << PAGE_SHIFT));
#endif
}
}
在启动期间分配可移动内存区的情况较少,那么分配器有很高的几率分配长度最大的内存区,并将其从可移动列表转换到不可移动列表。由于分配的内存区长度是最大的,因此不会向可移动内存中引入碎片。
总而言之,这种做法避免了启动期间内核分配的内存(经常在系统的整个运行时间都不释放)散布到物理内存各处,从而使其他类型的内存分配免受碎片的干扰
另一种减少内存碎片的办法是虚拟可移动内存域,即ZONE_MOVABLE
,可参考:什么是虚拟地址空间?从架构视角来解释
初始化内存域和结点数据结构
体系结构在启动期间有如下信息
- 系统中各个内存域的页帧边界,保存在
max_zone_pfn
数组 - 各结点页帧的分配情况,保存在全局变量
early_node_map
中
free_area_init_nodes
需要对照在zone_max_pfn
和zone_min_pfn
中指定的内存域的边界,计算各个内存域可使用的最低和最高的页帧编号。使用了两个全局数组来存储这些信息
static unsigned long __meminitdata arch_zone_lowest_possible_pfn[MAX_NR_ZONES];
static unsigned long __meminitdata arch_zone_highest_possible_pfn[MAX_NR_ZONES];
void __init free_area_init_nodes(unsigned long *max_zone_pfn)
{
unsigned long nid;
enum zone_type i;
/* Sort early_node_map as initialisation assumes it is sorted */
sort_node_map();
/* Record where the zone boundaries are */
memset(arch_zone_lowest_possible_pfn, 0,
sizeof(arch_zone_lowest_possible_pfn));
memset(arch_zone_highest_possible_pfn, 0,
sizeof(arch_zone_highest_possible_pfn));
arch_zone_lowest_possible_pfn[0] = find_min_pfn_with_active_regions();
arch_zone_highest_possible_pfn[0] = max_zone_pfn[0];
for (i = 1; i < MAX_NR_ZONES; i++) {
if (i == ZONE_MOVABLE)
continue;
arch_zone_lowest_possible_pfn[i] =
arch_zone_highest_possible_pfn[i-1];
arch_zone_highest_possible_pfn[i] =
max(max_zone_pfn[i], arch_zone_lowest_possible_pfn[i]);
}
arch_zone_lowest_possible_pfn[ZONE_MOVABLE] = 0;
arch_zone_highest_possible_pfn[ZONE_MOVABLE] = 0;
/* Find the PFNs that ZONE_MOVABLE begins at in each node */
memset(zone_movable_pfn, 0, sizeof(zone_movable_pfn));
find_zone_movable_pfns_for_nodes(zone_movable_pfn);
/* Print out the zone ranges */
printk("Zone PFN ranges:\n");
for (i = 0; i < MAX_NR_ZONES; i++) {
if (i == ZONE_MOVABLE)
continue;
printk(" %-8s %8lu -> %8lu\n",
zone_names[i],
arch_zone_lowest_possible_pfn[i],
arch_zone_highest_possible_pfn[i]);
}
/* Print out the PFNs ZONE_MOVABLE begins at in each node */
printk("Movable zone start PFN for each node\n");
for (i = 0; i < MAX_NUMNODES; i++) {
if (zone_movable_pfn[i])
printk(" Node %d: %lu\n", i, zone_movable_pfn[i]);
}
/* Print out the early_node_map[] */
printk("early_node_map[%d] active PFN ranges\n", nr_nodemap_entries);
for (i = 0; i < nr_nodemap_entries; i++)
printk(" %3d: %8lu -> %8lu\n", early_node_map[i].nid,
early_node_map[i].start_pfn,
early_node_map[i].end_pfn);
/* Initialise every node */
setup_nr_node_ids();
for_each_online_node(nid) {
pg_data_t *pgdat = NODE_DATA(nid);
free_area_init_node(nid, pgdat, NULL,
find_min_pfn_for_node(nid), NULL);
/* Any memory on that node */
if (pgdat->node_present_pages)
node_set_state(nid, N_HIGH_MEMORY);
check_for_regular_memory(pgdat);
}
}
在内存域边界已经确定之后,free_area_init_nodes
分别对各个内存域调用free_area_ init_node
创建数据结构
void __meminit free_area_init_node(int nid, struct pglist_data *pgdat,
unsigned long *zones_size, unsigned long node_start_pfn,
unsigned long *zholes_size)
{
pgdat->node_id = nid;
pgdat->node_start_pfn = node_start_pfn;
calculate_node_totalpages(pgdat, zones_size, zholes_size); // 累计各个内存域的页数,计算节点中页的总数
alloc_node_mem_map(pgdat); // 负责初始化一个简单但非常重要的数据结构 page
free_area_init_core(pgdat, zones_size, zholes_size);
}
代码默认将内存映射对齐到伙伴系统的最大分配阶
内核使用两个全局变量跟踪系统中的页数。
nr_kernel_pages
统计所有一致映射的页,而nr_all_pages
还包括高端内存页在内。一般来说伙伴系统是解决外部碎片,
slab
机制解决内部碎片。
内部碎片:指被内核分配出去但是不能被利用的内存。
外部碎片:由于频繁地申请和释放页框而导致的某些小的连续页框,比方只有一个页框,无法分配给需要大的连续页框的进程而导致的内存碎片。
slab分配器
slab
层是其高速缓存的机制,基于对象进行管理,其核心还是由伙伴系统来分配实际的物理页面。
两个功能:
- 用于内核分配细粒度的内存区域
- 用作一个缓存,主要针对经常分配并释放的对象
高速缓存的内存区划分为多个slab
,每一个slab
由一个或多个连续的页框组成,这些页框中保护已经分配的对象,页包含空闲对象。
slab
分配器的基本思想是,先利用页面伙伴分配器分配出单个或者一组连续的物理页面,然后在此基础上将整块页面分割成多个相等的小内存单元,以满足小内存空间分配的需要。当然,为了有效的管理这些小的内存单元并保证极高的内存使用速度和效率。(深入linux设备驱动程序内核机制)
相同类型的对象归为一类,每当要申请这样一个对象时,slab
分配器就从一个slab
列表中分配一个这样大小的单元出去,而当要释放时,将其重新保存在该列表中,而不是直接返回给伙伴系统,从而避免内部碎片。
对于经常用于分配并释放的对象,slab
分配器将释放的内存块保存在一个内部列表中,并不马上返回给伙伴系统。在请求为该类对象分配一个新实例时,会使用最近释放的内存块。
备选分配器
尽管slab
分配器对许多可能的工作负荷都工作良好,但也有一些情形,它无法提供最优性能。
slob
分配器:它围绕一个简单的内存块链表展开,在分配内存时,使用了同样简单的最先适配算法(微小的嵌入式系统)slub
分配器:通过将页帧打包为组,并通过struct page
中未使用的字段来管理这些组,试图最
小化所需的内存开销(应对大型计算机系统)
查看所有的缓存活动
root@huawei linux-version # cat /proc/slabinfo [0]
slabinfo - version: 2.1
# name <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail>
au_finfo 0 0 192 21 1 : tunables 0 0 0 : slabdata 0 0 0
au_icntnr 0 0 832 19 4 : tunables 0 0 0 : slabdata 0 0 0
au_dinfo 0 0 192 21 1 : tunables 0 0 0 : slabdata 0 0 0
ovl_inode 816 1127 688 23 4 : tunables 0 0 0 : slabdata 49 49 0
nf_conntrack 132 132 320 12 1 : tunables 0 0 0 : slabdata 11 11 0
ext4_groupinfo_4k 336 336 144 28 1 : tunables 0 0 0 : slabdata 12 12 0
btrfs_delayed_node 0 0 312 13 1 : tunables 0 0 0 : slabdata 0 0 0
btrfs_ordered_extent 0 0 416 19 2 : tunables 0 0 0 : slabdata 0 0 0
btrfs_inode 0 0 1168 14 4 : tunables 0 0 0 : slabdata 0 0 0
fsverity_info 0 0 248 16 1 : tunables 0 0 0 : slabdata 0 0 0
ip6-frags 0 0 184 22 1 : tunables 0 0 0 : slabdata 0 0 0
PINGv6 0 0 1216 13 4 : tunables 0 0 0 : slabdata 0 0 0
RAWv6 130 130 1216 13 4 : tunables 0 0 0 : slabdata 10 10 0
UDPv6 168 168 1344 12 4 : tunables 0 0 0 : slabdata 14 14 0
tw_sock_TCPv6 144 144 248 16 1 : tunables 0 0 0 : slabdata 9 9 0
request_sock_TCPv6 0 0 304 13 1 : tunables 0 0 0 : slabdata 0 0 0
...
slab
分配的实现原理
每个缓存只负责一种对象类型(如struct task_struct
实例),或提供一般性的缓冲区。各个缓存中的slab
的数目各有不同,这与已经使用的页的数目、对象长度和被管理对象的数目有关。
系统中所有的缓存都保存在一个双链表中。这使得内核有机会依次遍历所有的缓存,从来如此
slab
的结构:分为管理数据和对象数据,颜色空间等等
struct slab {
struct list_head list; // 双向链表
unsigned long colouroff; // 着色
void *s_mem; /* including colour offset */
unsigned int inuse; /* num of objs active in slab */
kmem_bufctl_t free;
unsigned short nodeid;
};
有些情况下,
slab
内存区的长度(减去头部管理数据)是不能被对象长度整除的,因此,内核就会把其中多余的内存以偏移量的形式给slab
。
缓存的各个slab
成员会指定不同的偏移量,以便将数据定位到不同的缓存行,因而slab开始和结束处的空闲内存是不同的
static inline void page_set_cache(struct page *page, struct kmem_cache *cache)
{
page->lru.next = (struct list_head *)cache;
}
static inline struct kmem_cache *page_get_cache(struct page *page)
{
page = compound_head(page);
BUG_ON(!PageSlab(page));
return (struct kmem_cache *)page->lru.next;
}
static inline void page_set_slab(struct page *page, struct slab *slab)
{
page->lru.prev = (struct list_head *)slab;
}
static inline struct slab *page_get_slab(struct page *page)
{
BUG_ON(!PageSlab(page));
return (struct slab *)page->lru.prev;
}
内核还对分配给slab分配器的每个物理内存页都设置标志
PG_SLAB
着色:指的是是高速缓存和硬件高速缓存之间的关联。对于大小相同的slab object
,虽然处于不同的物理地址处,但是由于cache
访问地址的规则,这两个地址很有可能会被分配给同一个cache line
去加载,那么会带来一个问题,比如软件中反复去读取这样的两个对象,那么会带来什么后果呢?
我们发现对应的cache line
需要被反复刷新,而其余的cache line
却未被充分利用。而不同的cache line
只能加载特定地址偏移的地址
slab
都是通过页分配器来进行分配的,也就是它的单位是页大小的,并且其中有一些是未使用的free
空间,那么通过对不同的slab
起始地址进行一个偏移,那么对应的object
也都会具有一个偏移,如果每个slab
的偏移都不同,那么不同slab
中的对象地址偏移都会不同,就可以使用不同的cache line
来加载了
每个缓存由kmem_cache
结构中的一个实例表示
struct kmem_cache {
/* 1) per-CPU数据,在每次分配/释放期间都会访问 */
struct array_cache *array[NR_CPUS]; //
/* 2) 可调整的缓存参数。由cache_chain_mutex保护 */
unsigned int batchcount; // 指定了在per-CPU列表为空的情况下,从缓存的slab中获取对象的数目。
unsigned int limit; // 指定了per-CPU列表中保存的对象的最大数目。如果超出该值,内核会将batchcount个对象返回到slab
unsigned int shared;
unsigned int buffer_size; // 指定了缓存中管理的对象的长度。
u32 reciprocal_buffer_size;
/* 3) 后端每次分配和释放内存时都会访问 */
unsigned int flags; /* 一个标志寄存器,定义缓存的全局性质 */
unsigned int num; /* # 每个slab中对象的数量 */
/* 4) 缓存的增长/缩减 */
/* 每个slab中页数,取以2为底数的对数 */
unsigned int gfporder;
/* 强制的GFP标志,例如GFP_DMA */
gfp_t gfpflags;
size_t colour; /* 缓存着色范围 */
unsigned int colour_off; /* 着色偏移 */
struct kmem_cache *slabp_cache;
unsigned int slab_size;
unsigned int dflags; /* 动态标志 */
/* 构造函数 */
void (*ctor)(struct kmem_cache *, void *); // 指向在对象创建时调用的构造函数
/* 5) 缓存创建/删除 */
const char *name; // 字符串,包含该缓存的名称 在列出/proc/slabinfo中可用的缓存时,会使用。
struct list_head next; // 标准的链表元素,用于将kmem_cache的所有实例保存在全局链表cache_chain上
/* 6) 统计量 */
#if STATS
unsigned long num_active;
unsigned long num_allocations;
unsigned long high_mark;
unsigned long grown;
unsigned long reaped;
unsigned long errors;
unsigned long max_freeable;
unsigned long node_allocs;
unsigned long node_frees;
unsigned long node_overflow;
atomic_t allochit;
atomic_t allocmiss;
atomic_t freehit;
atomic_t freemiss;
// ...
struct kmem_list3 *nodelists[MAX_NUMNODES]; // 每个数组项对应于系统中一个可能的内存结点
};
`slab`初始化过程
此处主要论证一个“鸡与蛋”的问题。(《深入Linux内核架构》)
为初始化slab
数据结构,内核需要若干远小于一整页的内存块,这些最适合由kmalloc分配。而只在slab
系统已经启用之后,才能使用kmalloc
。
kmem_cache_init
函数用于初始化slab
分配器,它在内核初始化阶段start_kernel
、伙伴系统启用之后调用
void __init kmem_cache_init(void);
asmlinkage void __init start_kernel(void)
{
// ...
vfs_caches_init_early();
cpuset_init_early();
mem_init();
kmem_cache_init();
setup_per_cpu_pageset();
numa_policy_init();
// ...
}
但在多处理器系统上,启动CPU
此时正在运行,而其他CPU
尚未初始化。kmem_cache_init
采用了一个多步骤过程,逐步激活slab
分配器。
-
kmem_cache_init
创建系统中的第一个slab
缓存,以便为kmem_cache
的实例提供内存。为此,内核使用的主要是在编译时创建的静态数据。一个静态数据结构用作per-CPU
数组。该缓存的名称是cache_cache
。 -
kmem_cache_init
接下来初始化一般性的缓存,用作kmalloc
内存的来源。为解决该问题,内核使用了g_cpucache_up
变量,可接受以下4个值(NONE、PARTIAL_AC、PARTIAL_L3、FULL
),以反映kmalloc
初始化的状态。在最小的kmalloc
缓存初始化时,再次将一个静态变量用于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
分配器已经就绪,
可以使用。
(该过程选自《深入Linux内核架构》)
还有另一种解读方式,我觉得更好:详解slab机制(4) slab初始化
三个步骤:
- 通过初始化全局变量
cache_cache
,创造第一个cache
,注意所有的cache
都是挂在链表cache_chain
下,而cache_cache
就是该链表的第一个节点;有了struct kmem_cache
长度的“规则”的cache
后,就可以从slab
申请kmem_cache
的内存了,这为创建其他“规则”的cache
打下了基础; - 接下来陆续创建包括
struct arraycache_init、struct kmem_list3
在内的长度由32到4194304的20个cache
,它们都是所谓的普通缓存,注意下标识初始化进度的全局变量g_cpucache_up
在这期间的变迁,由NONE->PARTIAL_AC->PARTIAL_L3
,前面细致描述过; - 通过
kmalloc
申请原先由全局变量模拟的cache
,包括struct arraycache_init
和struct kmem_list3
的(分别是initarray_cache
和initkmem_list3
);这时slab
初始化就完成了,其他模块都可以通过kmalloc
轻松获取对应的物理内存了,初始化进度的全局变量g_cpucache_up
置为EARLY
;
在start_kernel中后续调用函数kmem_cache_init_late
,将初始化进度的全局变量g_cpucache_up
置为FULL
,彻底完成slab
初始化。
相关API
创建新的slab
缓存
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_alloc(struct kmem_cache *cachep, gfp_t flags)
{
return __cache_alloc(cachep, flags, __builtin_return_address(0));
}
EXPORT_SYMBOL(kmem_cache_alloc);
释放对象
void kmem_cache_free(struct kmem_cache *cachep, void *objp)
{
unsigned long flags;
local_irq_save(flags);
debug_check_no_locks_freed(objp, obj_size(cachep));
__cache_free(cachep, objp);
local_irq_restore(flags);
}
EXPORT_SYMBOL(kmem_cache_free);
销毁缓存
static void __kmem_cache_destroy(struct kmem_cache *cachep)
{
int i;
struct kmem_list3 *l3;
for_each_online_cpu(i)
kfree(cachep->array[i]);
/* NUMA: free the list3 structures */
for_each_online_node(i) {
l3 = cachep->nodelists[i];
if (l3) {
kfree(l3->shared);
free_alien_cache(l3->alien);
kfree(l3);
}
}
kmem_cache_free(&cache_cache, cachep);
}
完结,撒花!
好博客就要一起分享哦!分享海报
此处可发布评论
评论(0)展开评论
展开评论