《深入Linux内核架构》读书笔记--第三章 内存管理
前言
这一章内容很多,有120页,主要描述了物理内存的管理。
NUMA的内存节点,每个内存节点有若干内存域,内存域中的页帧,页表等。
中间初始化的一些细节暂且略过,后面重点是伙伴系统原理、SLAB分配器原理。
第三章-内存管理
NUMA模型
UMA 一致性访问内存, NUMA 非一致性访问内存。
pg_data_t
pg_data_t是内存节点, 一个pg_data_t对应一个numa节点。一般台式机只有一个numa节点,服务器一般2个numa节点。
内存节点中,把内存也分成了若干区域:
ZONE_DMA : 给于设备内存直接访问的区域
ZONE_DMA32 : 给与32位设备内存直接访问的区域
ZONE_NORMAL: 普通内存区域,给于内核和用户态使用
ZONE_HIGHMEM: 高端内存区域,早期32位系统扩展更多内存使用,当下基本不用
ZONE_MOVABLE: 是用于支持内存迁移的特殊内存域
ZONE_DEVICE: 管理与设备相关的内存区域, 比如GPU可以通过这个映射
typedef struct pglist_data {
struct zone node_zones[MAX_NR_ZONES]; // 该节点所有内存域数组
struct zonelist node_zonelists[MAX_ZONELISTS]; // 备用节点内存域列表数组
int nr_zones; // 该节点实际内存域数量
struct page *node_mem_map; // 指向该节点页表映射数组
struct bootmem_data *bdata; // 指向启动内存分配器数据结构
unsigned long node_start_pfn; // 该节点首个物理页框页帧号
unsigned long node_present_pages; // 该节点实际物理内存页总数
unsigned long node_spanned_pages; // 该节点以页帧为单位计算的长度,包含内存空洞
int node_id; // 该节点唯一标识符
struct pglist_data *pgdat_next; // 指向下一个 pg_data_t 结构体
wait_queue_head_t kswapd_wait; // 交换守护进程等待队列头
struct task_struct *kswapd; // 指向交换守护进程任务结构体
int kswapd_max_order; // 交换守护进程可分配最大连续页框阶数
} pg_data_t;
zone
linux-5.15 _watermark[NR_WMARK] 数组取代了2.6的pages_min
、pages_low
、pages_high
字段, 用来描述区域水印。
enum zone_watermarks {
WMARK_MIN,
WMARK_LOW,
WMARK_HIGH,
NR_WMARK
};
unsigned long _watermark[NR_WMARK];
struct zone {
unsigned long _watermark[NR_WMARK]; // 区域水印,使用 *_wmark_pages(zone) 宏访问
unsigned long watermark_boost; // 水印提升值
unsigned long nr_reserved_highatomic; // 保留的高原子操作的数量
long lowmem_reserve[MAX_NR_ZONES]; // 低内存保留值数组,用于避免低内存区域 OOM
#ifdef CONFIG_NUMA
int node; // NUMA 节点编号
#endif
struct pglist_data *zone_pgdat; // 指向所属的 pglist_data 结构体
struct per_cpu_pages __percpu *per_cpu_pageset; // 每个 CPU 的页集合
struct per_cpu_zonestat __percpu *per_cpu_zonestats; // 每个 CPU 的区域统计信息
int pageset_high; // 页集合的高水位值
int pageset_batch; // 页集合的批量操作值
#ifndef CONFIG_SPARSEMEM
unsigned long *pageblock_flags; // 页块标志数组
#endif /* CONFIG_SPARSEMEM */
/* zone_start_pfn == zone_start_paddr >> PAGE_SHIFT */
unsigned long zone_start_pfn; // 区域起始页帧号
atomic_long_t managed_pages; // 由伙伴系统管理的页数量
unsigned long spanned_pages; // 区域跨越的总页数,包含空洞
unsigned long present_pages; // 区域中实际存在的物理页数
#if defined(CONFIG_MEMORY_HOTPLUG)
unsigned long present_early_pages; // 早期启动时存在的物理页数,不包括热插拔内存
#endif
#ifdef CONFIG_CMA
unsigned long cma_pages; // 分配给 CMA 使用的页数量
#endif
const char *name; // 区域名称
#ifdef CONFIG_MEMORY_ISOLATION
unsigned long nr_isolate_pageblock; // 隔离页块的数量
#endif
#ifdef CONFIG_MEMORY_HOTPLUG
/* see spanned/present_pages for more description */
seqlock_t span_seqlock; // 用于保护 spanned_pages 和 zone_start_pfn 的顺序锁
#endif
int initialized; // 区域是否已初始化的标志
ZONE_PADDING(_pad1_)
struct free_area free_area[MAX_ORDER]; // 不同大小的空闲区域,伙伴系统
unsigned long flags; // 区域标志
spinlock_t lock; // 主要用于保护 free_area 的自旋锁
ZONE_PADDING(_pad2_)
unsigned long percpu_drift_mark; // 每个 CPU 计数器漂移标记
#if defined CONFIG_COMPACTION || defined CONFIG_CMA
/* pfn where compaction free scanner should start */
unsigned long compact_cached_free_pfn; // 压缩空闲扫描器应开始的页帧号
/* pfn where compaction migration scanner should start */
unsigned long compact_cached_migrate_pfn[ASYNC_AND_SYNC]; // 压缩迁移扫描器应开始的页帧号
unsigned long compact_init_migrate_pfn; // 压缩初始化迁移页帧号
unsigned long compact_init_free_pfn; // 压缩初始化空闲页帧号
#endif
#ifdef CONFIG_COMPACTION
unsigned int compact_considered; // 自上次压缩失败后尝试的次数
unsigned int compact_defer_shift; // 压缩延迟移位值
int compact_order_failed; // 压缩失败的最小阶数
#endif
#if defined CONFIG_COMPACTION || defined CONFIG_CMA
/* Set to true when the PG_migrate_skip bits should be cleared */
bool compact_blockskip_flush; // 是否应清除 PG_migrate_skip 位的标志
#endif
bool contiguous; // 区域是否连续的标志
ZONE_PADDING(_pad3_)
/* Zone statistics */
atomic_long_t vm_stat[NR_VM_ZONE_STAT_ITEMS]; // 区域统计信息数组
atomic_long_t vm_numa_event[NR_VM_NUMA_EVENT_ITEMS]; // NUMA 相关事件统计信息数组
} ____cacheline_internodealigned_in_smp;
水印
__setup_per_zone_wmarks()
主要用于计算 每个 zone 的 水印,即 WMARK_MIN
、WMARK_LOW
和 WMARK_HIGH
。水印用于控制 Linux 内存管理的 页框分配策略 和 回收策略,影响 kswapd
何时启动以及分配的优先级。
WMARK_MIN计算
// PAGE_SHIFT : 12
// 将min_free_kbytes除以4,得到4KB的页数
unsigned long pages_min = min_free_kbytes >> (PAGE_SHIFT - 10);
min_free_kbytes可以由外部设置
# 保留内存空间最小值
root@node-1:~# cat /proc/sys/vm/min_free_kbytes
67584
每个zone的最小水线按照域可用内存与总可用内存比例分配
// 等效于 tmp = pages_min * (当前zone可管理内存也 / 所有内存页)
tmp = (u64)pages_min * zone_managed_pages(zone);
do_div(tmp, lowmem_pages);
zone->_watermark[WMARK_MIN] = tmp;
WMARK_LOW/WMARK_HIGH计算
// 这里tmp是之前WMARK_MIN的值,这里取 WMARK_MIN 1/4 或 页可管理内存0.1%较大值
tmp = max_t(u64, tmp >> 2,
mult_frac(zone_managed_pages(zone),
watermark_scale_factor, 10000));
zone->watermark_boost = 0;
// WMARK_LOW 可能为 WMARK_MIN * 1.25%
zone->_watermark[WMARK_LOW] = min_wmark_pages(zone) + tmp;
// WMARK_HIGH 可能为 WMARK_MIN * 1.5%
zone->_watermark[WMARK_HIGH] = min_wmark_pages(zone) + tmp * 2;
水印的作用
如果空闲页多于WMARK_HIGH,则内存域的状态是理想的。
如果空闲页的数目低于WMARK_LOW,则内核开始将页换出到硬盘。
如果空闲页的数目低于WMARK_MIN,那么页回收工作的压力就比较大,因为内存域中急需空闲页
冷热页
已经加载到CPU高速缓存中的页称为热页,未加载进CPU高速缓存的页叫冷页。
struct per_cpu_pages {
int count; /* 当前 CPU 缓存的页数 */
int high; /* 高水位线,超过时需要回收 */
int batch; /* 批量操作的页数(伙伴系统交互时) */
short free_factor; /* 释放时的批量缩放因子 */
#ifdef CONFIG_NUMA
short expire; /* 为 0 时,远程节点的 pageset 需要被清空 */
#endif
struct list_head lists[NR_PCP_LISTS]; /* 按迁移类型存储的页列表 */
};
5.15中的冷热页并没有像2.6那样明显能区分出来, lists[NR_PCP_LISTS]中有多重页类型。
热页:MIGRATE_UNMOVABLE
冷页:MIGRATE_MOVABLE
页帧
struct page {
unsigned long flags; /* 原子标志位,部分可能异步更新 */
union {
struct { /* 页面缓存和匿名页 */
struct list_head lru; /* 页面回收链表 */
struct address_space *mapping; /* 关联的地址空间 */
pgoff_t index; /* 在映射中的偏移 */
unsigned long private; /* 私有数据,存放额外信息 */
};
struct { /* 网络子系统的 page_pool */
unsigned long pp_magic; /* 用于检测非法回收 */
struct page_pool *pp; /* 关联的 page_pool */
unsigned long dma_addr; /* 物理 DMA 地址 */
union {
unsigned long dma_addr_upper; /* 高 32 位 DMA 地址 */
atomic_long_t pp_frag_count; /* 片段计数 */
};
};gff
struct { /* slab 分配器 */
union {
struct list_head slab_list; /* slab链表 */
struct { /* 部分空闲页 */
struct page *next; /* 指向下一个页面 */
#ifdef CONFIG_64BIT
int pages; /* 剩余页数 */
int pobjects; /* 估算的对象数量 */
#else
short int pages;
short int pobjects;
#endif
};
};
struct kmem_cache *slab_cache; /* 指向缓存 */
void *freelist; /* 空闲对象链表 */
union {
void *s_mem; /* 第一个对象的地址 */
unsigned long counters; /* 统计信息 */
struct {
unsigned inuse:16; /* 已使用对象数 */
unsigned objects:15; /* 总对象数 */
unsigned frozen:1; /* 是否冻结 */
};
};
};
struct { /* 复合页的尾页 */
unsigned long compound_head; /* 指向头页 */
unsigned char compound_dtor; /* 析构函数 */
unsigned char compound_order; /* 复合页阶数 */
atomic_t compound_mapcount; /* 映射计数 */
unsigned int compound_nr; /* 页数(2^order) */
};
struct { /* 复合页的第二个尾页 */
unsigned long _compound_pad_1;
atomic_t hpage_pinned_refcount; /* 大页固定引用计数 */
struct list_head deferred_list; /* 延迟回收链表 */
};
struct { /* 页表页 */
unsigned long _pt_pad_1;
pgtable_t pmd_huge_pte; /* 大页 PTE 指针 */
unsigned long _pt_pad_2;
union {
struct mm_struct *pt_mm; /* 所属 mm 结构 */
atomic_t pt_frag_refcount; /* 片段计数 */
};
#if ALLOC_SPLIT_PTLOCKS
spinlock_t *ptl; /* 保护页表的锁 */
#else
spinlock_t ptl;
#endif
};
struct { /* ZONE_DEVICE 设备页 */
struct dev_pagemap *pgmap; /* 设备页映射信息 */
void *zone_device_data; /* 设备私有数据 */
};
struct rcu_head rcu_head; /* RCU 释放钩子 */
};
union {
atomic_t _mapcount; /* 映射引用计数 */
unsigned int page_type; /* 页面类型 */
unsigned int active; /* SLAB 活跃状态 */
int units; /* SLOB 计数 */
};
atomic_t _refcount; /* 引用计数 */
#ifdef CONFIG_MEMCG
unsigned long memcg_data; /* 内存控制组数据 */
#endif
#if defined(WANT_PAGE_VIRTUAL)
void *virtual; /* 低端页的内核虚拟地址 */
#endif
#ifdef LAST_CPUPID_NOT_IN_PAGE_FLAGS
int _last_cpupid; /* 最近访问该页的 CPU ID */
#endif
};
页表
四级页表
PGD全局页目录,PUD上层页目录, PMD中间页目录, PTE页目录,PTE中的每一项就是页表,指向物理页帧
AMD64,一个指针8个字节64位,用来描述一个虚拟内存地址,其中不同的位表示不同的含义
按位分布
| 63:48 | 47:39 | 38:30 | 29:21 | 20:12 | 11:0 |
| Sign | PGD | PUD | PMD | PT | Offset |
PGD:根据虚拟地址的47:39位为PGD表项的index,PGD[index] —> PUD。
PUD:根据虚拟地址的38:30位为PUD中表项的index,PUD[index] —>PMD。
PMD:根据虚拟地址的29:21位为PMD中表项的index,PMD[index] —> PTE。
PT:根据虚拟地址的20:12位为PT表项的index,PTE[index] —> Page页表。
Offset:页表对应页帧的Offset。
SHIFT定义
//pgtable_64_types.h
#define PGDIR_SHIFT 39
#define PUD_SHIFT 30
#define PMD_SHIFT 21
//page_types.h
#define PAGE_SHIFT 12
PGDIR_SHIFT 39 就是 PGD后面的位数
PUD_SHIFT 30 就是PUD后面的位数, PMD_SHIFT,PAGE_SHIFT亦是同理。
每级页表index获取,通过右移SHIFT位数来获取
static inline unsigned long pte_index(unsigned long address)
{
return (address >> PAGE_SHIFT) & (PTRS_PER_PTE - 1);
}
static inline unsigned long pmd_index(unsigned long address)
{
return (address >> PMD_SHIFT) & (PTRS_PER_PMD - 1);
}
static inline unsigned long pud_index(unsigned long address)
{
return (address >> PUD_SHIFT) & (PTRS_PER_PUD - 1);
}
内存管理初始化
Kernel内部初始化有一个重要函数 start_kernel(), 它完成内核启动过程中最基本的初始化,包括 CPU、内存管理、中断系统、定时器、调度器等,最终启动 init
进程进入用户态,从而完成整个 Linux 启动过程的内核部分。
了解内存管理初始化,也得从start_kernel开始。
// asmlinkage 参数栈传递
// __visible 防止优化为inline或静态函数
// __init 标记为初始化函数,初始化完成后,函数内存会释放
// __no_sanitize_address 禁用内存检查
asmlinkage __visible void __init __no_sanitize_address start_kernel(void)
{
setup_arch(&command_line);
setup_per_cpu_areas();
build_all_zonelists(NULL);
mm_init();
setup_per_cpu_pageset();
}
setup_arch()
初始化memblock (新的自举分配器)
e820__memblock_setup(); // 解析e820表,找出可用RAM区域,注册物理内存 memblock.memory
reserve_real_mode(); // 保留BIOS相关区域
reserve_crashkernel(); // 预留 Crash kernel 内存
efi_memblock_x86_reserve_range(); // 保留EFI相关区域内存
init_cpu_to_node(); // 其中有初始化 pglist->zone_node 本地内存域
max_pfn = e820__end_of_ram_pfn(); // 最大有效物理页帧号(PFN),物理内存分配范围
max_possible_pfn = max_pfn;
自举分配器仅用于“引导阶段”,在 start_kernel()
的 mm_init()
阶段,会被页管理系统(Buddy System)和 Slab 分配器取代。伙伴系统(Buddy System)初始化后,memblock
将被废弃,物理内存管理由 page allocator
负责。
setup_per_cpu_areas()
为每个CPU分配一个per-CPU变量, per-CPU变量包含 CPU相关信息,进程调度信息、CPU本地缓存页等信息。
per-CPU是一个内存块,当时没有一个struct结构,这些信息按照offset偏移进行访问。
CPU0:
+-------------------+
| per-CPU 变量 A | <-- 偏移 0
| per-CPU 变量 B | <-- 偏移 8
| per-CPU 变量 C | <-- 偏移 16
| ... |
+-------------------+
build_all_zonelists()
建立内存节点和节点内各内存域的数据结构
build_thisnode_zonelists用来初始化备用内存域列表 pgdat->node_zonelists, 按照一定优先级排列
static void build_thisnode_zonelists(pg_data_t *pgdat)
{
struct zoneref *zonerefs;
int nr_zones;
zonerefs = pgdat->node_zonelists[ZONELIST_NOFALLBACK]._zonerefs;
nr_zones = build_zonerefs_node(pgdat, zonerefs);
zonerefs += nr_zones;
zonerefs->zone = NULL;
zonerefs->zone_idx = 0;
}
mm_init()
设置内核的内存分配器、调试工具和相关硬件特性
/*
* Set up kernel memory allocators
*/
static void __init mm_init(void)
{
/*
* page_ext requires contiguous pages,
* bigger than MAX_ORDER unless SPARSEMEM.
*/
page_ext_init_flatmem(); /* 初始化 flatmem 模型下的 page_ext 数据结构,用于扩展物理页的元数据 */
init_mem_debugging_and_hardening(); /* 初始化内存调试和加固功能,用于检测内存错误和增强内存安全性 */
kfence_alloc_pool(); /* 为 KFENCE(内核内存错误检测工具)分配内存池 */
report_meminit(); /* 报告内存初始化的状态,打印相关信息以帮助调试 */
stack_depot_init(); /* 初始化栈仓库(stack depot),用于存储内核栈的哈希值以帮助调试 */
mem_init(); /* 初始化内核的内存管理系统,包括释放引导内存和初始化伙伴系统 */
mem_init_print_info(); /* 打印内存初始化的详细信息,包括可用内存大小和内存布局 */
/* page_owner must be initialized after buddy is ready */
page_ext_init_flatmem_late(); /* 在伙伴系统初始化完成后,延迟初始化 page_ext */
kmem_cache_init(); /* 初始化 SLAB 分配器,用于管理内核中的小块内存分配 */
kmemleak_init(); /* 初始化 Kmemleak 内存泄漏检测工具,用于检测内核中的内存泄漏问题 */
pgtable_init(); /* 初始化页表相关功能,包括设置页表缓存和初始化页表分配器 */
debug_objects_mem_init(); /* 初始化调试对象(debug objects)的内存池,用于检测内核中的对象使用错误 */
vmalloc_init(); /* 初始化 vmalloc 区域,用于分配虚拟地址连续但物理地址不连续的内存 */
/* Should be run before the first non-init thread is created */
init_espfix_bsp(); /* 初始化 ESPFIX 功能(针对 x86 架构),修复 32 位用户态程序在 64 位内核中的栈指针问题 */
/* Should be run after espfix64 is set up. */
pti_init(); /* 初始化页表隔离(Page Table Isolation, PTI),用于缓解 Meltdown 漏洞 */
mm_cache_init(); /* 初始化内存管理相关的缓存,包括页表缓存和 SLAB 缓存 */
}
setup_per_cpu_pageset()
为每个 CPU 核心分配本地页集,减少全局锁竞争;
初始化冷热页列表,优化内存分配性能;
支持高效的内存分配和释放,提升多核系统的并发性能。
内核内存布局
0x1000之前保留, 后640KiB一般不使用。除非内核很小,可以占据这部分。
后面的区域是用于映射各种ROM,BIOS和显卡ROM
0x100000之后用来加载内核映像,_text _etext之间是内核代码段,etext 到edata之间,保存内核变量
/proc/iomem 内存分布
root@node-1:/boot# cat /proc/iomem
00000000-00000fff : Reserved
00001000-0009dfff : System RAM
0009e000-0009efff : Reserved
0009f000-0009ffff : System RAM
000a0000-000fffff : Reserved
000a0000-000bffff : PCI Bus 0000:00
00000000-00000000 : PCI Bus 0000:00
00000000-00000000 : PCI Bus 0000:00
00000000-00000000 : PCI Bus 0000:00
00000000-00000000 : PCI Bus 0000:00
000e0000-000fffff : PCI Bus 0000:00
000f0000-000fffff : System ROM
00100000-92488017 : System RAM # 内核代码起始位置
92488018-924b1457 : System RAM
924b1458-924b2017 : System RAM
924b2018-924db457 : System RAM
924db458-924dc017 : System RAM
924dc018-92510257 : System RAM
92510258-92511017 : System RAM
92511018-92545257 : System RAM
92545258-925e1017 : System RAM
925e1018-925ee857 : System RAM
925ee858-928c4fff : System RAM
928c5000-928c5fff : Reserved
928c6000-98264fff : System RAM
98265000-98a64fff : Reserved
98a65000-98b94fff : ACPI Tables # APCI表,系统固件提供的硬件配置信息
98b95000-98d94fff : ACPI Non-volatile Storage
98d95000-99c4efff : Reserved
99c4f000-99c4ffff : System RAM
99c50000-9f7fffff : Reserved
9b800000-9f7fffff : Graphics Stolen Memory
9f800000-dfffffff : PCI Bus 0000:00 # PCI设备的I/O内存或MMIO
a0000000-afffffff : 0000:00:02.0
b0000000-b5ffffff : PCI Bus 0000:04
b0000000-b1ffffff : 0000:04:00.0
起始地址-结束地址 | 区域类型或描述 | 大小计算 | 大小(近似) |
---|---|---|---|
00000000-00000fff | Reserved | 0x1000 | 4 KB |
00001000-0009dfff | System RAM | 0x9d000 | 616 KB |
0009e000-0009efff | Reserved | 0x1000 | 4 KB |
0009f000-0009ffff | System RAM | 0x1000 | 4 KB |
000a0000-000fffff | Reserved | 0x60000 | 384 KB |
00100000-92488017 | System RAM | 0x92388018 | 2.29 GB |
92488018-924b1457 | System RAM | 0x28c40 | 163 KB |
a0000000-afffffff | 0000:00:02.0 | 0x10000000 | 256 MB |
b0000000-b1ffffff | 0000:04:00.0 | 0x2000000 | 32 MB |
98a65000-98b94fff | ACPI Tables | 0x130000 | 1.1875 MB |
98b95000-98d94fff | ACPI Non-volatile Storage | 0x200000 | 2 MB |
9b800000-9f7fffff | Graphics Stolen Memory | 0x4000000 | 64 MB |
Per-CPU冷热缓存
per-CPU 缓存的冷热管理是为了提高局部性、减少锁竞争。让不同CPU核心管理自己的一块缓存,当每个CPU内存分配时,优先使用当前per-CPU缓存中的内存。
当per-CPU缓存中有一些页没有经常被访问,就放入冷缓存中,之后再放回普通内存中去。
每个 CPU 都有自己的 per-CPU 缓存链表。
当热缓存耗尽时,会尝试从冷缓存中补充,当冷缓存不足时,再向全局内存池申请新对象。
伙伴系统
struct zone {
...
struct free_area free_area[MAX_ORDER]; // MAX_ORDER 默认是11
...
}
伙伴系统按照内存区大小分为MAX_ORDER个链表。
free_area[0] , 代表每一块内存区大小为1页的链表
free_area[1], 代表每一块内存区大小为2页的链表
…
free_ares[10], 代表每一块内存区大小为1024页的链表。按照4KB的页大小,那么一块内存区为4MB
伙伴内存状态信息
一共11列,从 0~10, 每一列就是 对应链表的空闲项数目
比如Node 1最后一个, 代表free_ares[10]有32238个空闲项,每一个快4MB,合计125G大小
root@r750-131:~# cat /proc/buddyinfo
Node 0, zone DMA 0 0 0 0 0 0 0 0 1 1 2
Node 0, zone DMA32 4 5 6 6 6 5 4 7 6 5 348
Node 0, zone Normal 6266 3735 24268 17700 7787 4200 1348 737 409 214 35192
Node 1, zone Normal 390573 318024 173588 75834 40824 26100 18245 14110 9421 1707 32238
避免内存碎片
将内存分为不可移动、可回收、可移动页,防止不可移动页与其他页混合。
伙伴系统回收内存时,会检查相邻内存块,如果相邻块空闲,则合并。合并后的更大块可以进一步合并,形成连续的物理页。
当内存不足时,kswapd
将回收可回收页,将数据写入 swap 区。
kdm,Zswap合并相似页,冷数据压缩。
内存碎片整理compact_memory,在分配大页、分解比较大的内存块,或者碎片太多时,会自动整理碎片。
root@node-1:~# cat /boot/config-$(uname -r) | grep COMPACTION
CONFIG_BALLOON_COMPACTION=y
CONFIG_COMPACTION=y # 开启碎片压缩
root@node-1:~# cat /proc/vmstat | grep compact
compact_migrate_scanned 2747808
compact_free_scanned 5499781
compact_isolated 184420
compact_stall 109
compact_fail 0
compact_success 109
compact_daemon_wake 356
compact_daemon_migrate_scanned 633976
compact_daemon_free_scanned 113385
root@node-1:~#
root@node-1:~# cat /sys/kernel/mm/transparent_hugepage/enabled
always [madvise] never #按需整理
root@node-1:~#
分配页
所有的页分配,基本可以追溯到__alloc_pages_node函数
static inline struct page *
__alloc_pages_node(int nid, gfp_t gfp_mask, unsigned int order)
{
VM_BUG_ON(nid < 0 || nid >= MAX_NUMNODES);
VM_WARN_ON((gfp_mask & __GFP_THISNODE) && !node_online(nid));
return __alloc_pages(gfp_mask, order, nid, NULL);
}
参数说明:
nid: numa_id
gfp_mask: 指定内存分配的掩码,由若干flag 或运算得到。通常用来指定内存分配区域和分配动作
常见标志
GFP_KERNEL:内核态分配,可能引发睡眠。
GFP_ATOMIC:原子分配,不会睡眠。
GFP_NOWAIT:不等待直接返回。
__GFP_HIGHMEM:从高端内存分配。
__GFP_ZERO:分配后清零。
__GFP_NORETRY:不进行慢路径重试。
order: 即分配 2^order大小的内存块, 也就是zone.free_area[order] 链表中取的一个内存块
实际上__alloc_pages有两条路径,
快路径: 从zone.free_area[order].free_list中获取一个内存块
慢路径:当free_list没有内存块时,则使用Reclaim,Compaction,跨NUMA节点分配等方式获得内存页
SLAB
slab首先作为缓存,同一个大小的结构体释放后,还留在slab中缓存,下次再申请这个结构体,那么直接在slab上分配,无需从伙伴系统获取页分配,效率更高。
SLAB缓存可以认为是一组内存池,专用缓存和通用缓存两种。
专用缓存:按照内核中常用对象的大小,分配的缓存。包括skb,文件系统IO,DMA等等。
通用缓存:按照2的幂次方,有若干组不同大小的缓存列表。当kmalloc一个大小的内存时,SLAB通用缓存中找到接近需求大小的较大对象列表中分配。 比如申请200 Bytes, 那么会在 kmalloc-256的缓存列表中分配。
SLAB状态
可以通过slabtop命令查看slab的分配状态,其中包括了专用缓存和通用缓存的使用情况。
root@node-1:/home/ckun# slabtop -o
Active / Total Objects (% used) : 5020805 / 5052305 (99.4%)
Active / Total Slabs (% used) : 123486 / 123486 (100.0%)
Active / Total Caches (% used) : 126 / 183 (68.9%)
Active / Total Size (% used) : 811777.73K / 821440.91K (98.8%)
Minimum / Average / Maximum Object : 0.01K / 0.16K / 10.69K
OBJS ACTIVE USE OBJ SIZE SLABS OBJ/SLAB CACHE SIZE NAME
3333603 3333603 100% 0.10K 85477 39 341908K buffer_head
293958 293958 100% 0.19K 6999 42 55992K dentry
172295 172295 100% 0.05K 2027 85 8108K ftrace_event_field
166056 166056 100% 0.04K 1628 102 6512K ext4_extent_status
138852 138852 100% 0.57K 4959 28 79344K radix_tree_node
121689 121651 99% 1.15K 4507 27 144224K ext4_inode_cache
111776 111615 99% 0.50K 3493 32 55888K kmalloc-512
80850 80261 99% 0.09K 1925 42 7700K kmalloc-96
65536 59734 91% 0.06K 1024 64 4096K vmap_area
54368 54069 99% 0.12K 1699 32 6796K kernfs_node_cache
46208 45705 98% 0.03K 361 128 1444K kmalloc-32
41480 36241 87% 0.02K 244 170 976K lsm_file_cache
36608 36608 100% 0.02K 143 256 572K kmalloc-cg-16
30976 25670 82% 0.06K 484 64 1936K kmalloc-64
29184 29184 100% 0.01K 57 512 228K kmalloc-8
28575 28575 100% 0.62K 1143 25 18288K inode_cache
22619 22138 97% 0.20K 581 39 4648K vm_area_struct
19136 17619 92% 0.06K 299 64 1196K anon_vma_chain
16896 16896 100% 0.02K 66 256 264K kmalloc-16
15712 13760 87% 0.25K 491 32 3928K kmalloc-256
11144 11144 100% 0.07K 199 56 796K Acpi-Operand
11008 11008 100% 0.06K 172 64 688K kmalloc-rcl-64
9555 9289 97% 0.10K 245 39 980K anon_vma
以其中通用缓存kmalloc-512 为例
111776 111615 99% 0.50K 3493 32 55888K kmalloc-512
111776
:当前分配的对象数。
111615
:当前使用中的对象数。
99%
:使用率。
0.50K
:每个对象的大小 (512 字节)。
3493
:当前分配的 slab 数量。
32
:每个 slab 中的对象数量。
55888K
:当前该缓存占用的总内存大小。
每个 slab 有 32 个对象,每个对象 512Bytes :32×512 = 16384 Bytes=16KB
当前分配了 3493个 slab: 3493×16KB = 55888KB
创建SLAB
不论是专用缓存还是通用缓存,都使用kmem_cache_create函数创建
// 以链接跟踪表缓存为例
nf_conntrack_cachep = kmem_cache_create("nf_conntrack",
sizeof(struct nf_conn),
NFCT_INFOMASK + 1,
SLAB_TYPESAFE_BY_RCU | SLAB_HWCACHE_ALIGN, NULL);
//返回值为SLAB缓存池对象
//这里没有指定对象的数量,实际为slab_size / buffer_size得到, buffer_size即对象大小
kmem_cache_create
set_on_slab_cache
calculate_slab_order
cache_estimate
num = slab_size / buffer_size;
通用缓存创建
/*
* Create the kmalloc array. Some of the regular kmalloc arrays
* may already have been created because they were needed to
* enable allocations for slab creation.
*/
void __init create_kmalloc_caches(slab_flags_t flags)
{
int i;
enum kmalloc_cache_type type;
/*
* Including KMALLOC_CGROUP if CONFIG_MEMCG_KMEM defined
*/
// 这里的下标i , 代表2的i次幂, 下面循环申请 kmalloc-8 , kmalloc-16,32,64等等通用缓存。
for (type = KMALLOC_NORMAL; type <= KMALLOC_RECLAIM; type++) {
for (i = KMALLOC_SHIFT_LOW; i <= KMALLOC_SHIFT_HIGH; i++) {
if (!kmalloc_caches[type][i])
new_kmalloc_cache(i, type, flags);
/*
* Caches that are not of the two-to-the-power-of size.
* These have to be created immediately after the
* earlier power of two caches
*/
if (KMALLOC_MIN_SIZE <= 32 && i == 6 &&
!kmalloc_caches[type][1])
new_kmalloc_cache(1, type, flags);
if (KMALLOC_MIN_SIZE <= 64 && i == 7 &&
!kmalloc_caches[type][2])
new_kmalloc_cache(2, type, flags);
}
}
/* Kmalloc array is now usable */
slab_state = UP;
#ifdef CONFIG_ZONE_DMA
for (i = 0; i <= KMALLOC_SHIFT_HIGH; i++) {
struct kmem_cache *s = kmalloc_caches[KMALLOC_NORMAL][i];
if (s) {
kmalloc_caches[KMALLOC_DMA][i] = create_kmalloc_cache(
kmalloc_info[i].name[KMALLOC_DMA],
kmalloc_info[i].size,
SLAB_CACHE_DMA | flags, 0,
kmalloc_info[i].size);
}
}
#endif
}
static void __init
new_kmalloc_cache(int idx, enum kmalloc_cache_type type, slab_flags_t flags)
{
if (type == KMALLOC_RECLAIM) {
flags |= SLAB_RECLAIM_ACCOUNT;
} else if (IS_ENABLED(CONFIG_MEMCG_KMEM) && (type == KMALLOC_CGROUP)) {
if (cgroup_memory_nokmem) {
kmalloc_caches[type][idx] = kmalloc_caches[KMALLOC_NORMAL][idx];
return;
}
flags |= SLAB_ACCOUNT;
}
// 这里最终调用kmem_cache_alloc 创建slab缓存
kmalloc_caches[type][idx] = create_kmalloc_cache(
kmalloc_info[idx].name[type],
kmalloc_info[idx].size, flags, 0,
kmalloc_info[idx].size);
/*
* If CONFIG_MEMCG_KMEM is enabled, disable cache merging for
* KMALLOC_NORMAL caches.
*/
if (IS_ENABLED(CONFIG_MEMCG_KMEM) && (type == KMALLOC_NORMAL))
kmalloc_caches[type][idx]->refcount = -1;
}
分配逻辑
内核中内存的分配,优先Per-CPU缓存中查找空闲对象,如果没有再从SLAB分配器中申请,SLAB缓存不足则从伙伴系统再分配页。
因为per-CPU缓存,因为频繁访问,大概率在 CPU cache中,从这里分配性能最好。
CPU cache
CPU高速缓存有L1L2L3, L1 通常是 VA cache, 即虚拟地址访问。 L2L3为 PA cache物理地址访问。
L1最快,可以通过虚拟地址直接访问,不依赖TLB转换。
L2/L3, 访问要先进行TLB转换为物理地址再进行访问。