当前位置: 首页 > news >正文

《深入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_minpages_lowpages_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_MINWMARK_LOWWMARK_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-00000fffReserved0x10004 KB
00001000-0009dfffSystem RAM0x9d000616 KB
0009e000-0009efffReserved0x10004 KB
0009f000-0009ffffSystem RAM0x10004 KB
000a0000-000fffffReserved0x60000384 KB
00100000-92488017System RAM0x923880182.29 GB
92488018-924b1457System RAM0x28c40163 KB
a0000000-afffffff0000:00:02.00x10000000256 MB
b0000000-b1ffffff0000:04:00.00x200000032 MB
98a65000-98b94fffACPI Tables0x1300001.1875 MB
98b95000-98d94fffACPI Non-volatile Storage0x2000002 MB
9b800000-9f7fffffGraphics Stolen Memory0x400000064 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转换为物理地址再进行访问。

相关文章:

  • 【含文档+PPT+源码】基于Python的全国景区数据分析以及可视化实现
  • 【例3.5】位数问题(信息学奥赛一本通-1313)
  • BertTokenizer.from_pretrained的讲解和使用
  • golang编写UT:applyFunc和applyMethod区别
  • Oracle数据库服务器地址变更与监听配置修改完整指南
  • websocket结合promise的通信协议
  • 短期趋势动量策略思路
  • Thales靶机攻略
  • 鸿蒙移动应用开发--UI组件布局
  • 批量优化与压缩 PPT,减少 PPT 文件的大小
  • 【CSS3】01-初始CSS + 引入 + 选择器 + div盒子 + 字体修饰
  • Sar: 1靶场渗透
  • MoManipVLA:将视觉-语言-动作模型迁移到通用移动操作
  • 自然语言处理(13:RNN的实现)
  • 接口测试是什么
  • Mininet-topo.py源码解析
  • Linux--环境变量
  • Ubuntu 更换阿里云镜像源图文详细教程
  • Android面试总结之Android RecyclerView:从基础机制到缓存优化
  • 浅尝AI编程工具Trae
  • 上海市税务局回应刘晓庆被举报涉嫌偷漏税:正依法依规办理
  • 丰富“互换通”产品类型,促进中国金融市场高水平对外开放
  • 美将解除对叙利亚制裁,外交部:中方一贯反对非法单边制裁
  • 泰山、华海、中路等山东险企综合成本率均超100%,承保业务均亏损
  • 杨文庄当选中国人口学会会长,曾任国家卫健委人口家庭司司长
  • 回望星河深处,唤醒文物记忆——读《发现武王墩》