Linux内核内存管理深度实战:从Buddy System到SLAB/SLUB/SLOB分配器全链路
深入剖析Linux内核内存管理体系——Buddy System页框分配、SLAB/SLUB/SLOB三大分配器架构、kmalloc/kfree机制、slab cache生产调优与实战排障
一、内核内存管理总览
Linux内核内存管理子系统是操作系统的核心基础设施之一,负责管理物理内存的分配、回收与映射。与用户空间通过malloc/free经brk/mmap向OS申请内存不同,内核内存分配具有更强的约束:不能睡眠(在原子上下文中)、必须保证最低内存碎片、要求确定性延迟。
1.1 内存管理层次结构
用户空间: malloc() → ptmalloc → brk()/mmap()
↓
内核空间: kmalloc() → SLUB Allocator → Buddy System → 物理内存
vmalloc() → 虚拟连续映射 → Buddy System
kmem_cache_alloc() → slab专用缓存
内核内存管理采用两级分配架构:
- 底层:Buddy System(伙伴系统),以页(通常4KB)为单位管理物理内存
- 上层:SLAB/SLLOB/SLOB分配器,从Buddy System获取页框后,细分为小对象分配
1.2 物理内存模型
Linux内核支持三种物理内存模型:
| 模型 | 适用场景 | 特点 |
|---|---|---|
| FLATMEM | 嵌入式/低端系统 | 连续物理内存,用全局mem_map[]数组管理所有struct page |
| SPARSEMEM | 大型NUMA系统 | 物理内存分段稀疏分布,section化管理(128MB/section) |
| SPARSEMEM_VMEMMAP | 现代x86_64服务器 | 虚拟映射模式,struct page通过vmalloc区虚拟连续映射 |
现代服务器普遍采用SPARSEMEM_VMEMMAP模型,NUMA拓扑通过pg_data_t节点描述,每个节点内按ZONE划分:
// NUMA节点
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; // 通过vmemmap映射
unsigned long node_start_pfn;
unsigned long node_present_pages;
unsigned long node_spanned_pages;
int node_id;
// ...
} pg_data_t;
// 内存区域类型
enum zone_type {
ZONE_DMA, // 0-16MB, ISA DMA兼容
ZONE_DMA32, // 0-4GB, 32-bit DMA设备
ZONE_NORMAL, // 直接线性映射区(x86_64: 1G~物理内存末端)
ZONE_HIGHMEM, // 32-bit高端内存(x86_64无此区)
ZONE_MOVABLE, // 可移动页面(支持内存热插拔)
MAX_NR_ZONES
};
x86_64架构下,通常只有ZONE_DMA、ZONE_DMA32和ZONE_NORMAL三个区域。ZONE_NORMAL通过PAGE_OFFSET开始的直接映射区(phys_base到phys_base + mem_size),内核可以直接通过线性映射访问所有物理页,无需临时映射。
二、Buddy System(伙伴系统)
Buddy System是内核物理内存分配的核心算法,由Knowlton于1965年提出,Linux内核自1.0版本以来一直使用该算法,并不断优化。
2.1 核心算法原理
Buddy System将空闲页面按2的幂次(order)组织为链表组:
Order 0: 4KB × 1页框
Order 1: 4KB × 2页框 (8KB)
Order 2: 4KB × 4页框 (16KB)
Order 3: 4KB × 8页框 (32KB)
Order 4: 4KB × 16页框 (64KB)
...
Order 10: 4KB × 1024页框 (4MB)
每个zone维护一个free_area数组,elem是free_list:
struct zone {
free_area_t free_area[MAX_ORDER]; // MAX_ORDER=11(0~10)
};
typedef struct free_area {
struct list_head free_list[MIGRATE_TYPES];
unsigned long nr_free;
} free_area_t;
分配流程:当需要2^n个连续页框时,从order=n的链表中取一个块;若无,向上迭代到更大order的链表中取一块,拆分一半返回,另一半加入低一级链表。
释放流程:释放块时检查其"伙伴"(同一上层块的另一半)是否空闲,若空则向上合并,递归直到到达最大的order或伙伴不可合并。
伙伴计算:给定页框号pfn和ordern,其伙伴的页框号为:
buddy_pfn = pfn ^ (1 << n)
即通过XOR操作将第n位翻转,即可得到伙伴页框号。
2.2 页面迁移类型
内核将空闲页面按可迁移类型分组,减少碎片化:
enum migratetype {
MIGRATE_UNMOVABLE, // 不可移动(如slab分配的对象页)
MIGRATE_MOVABLE, // 可移动(如用户空间页面缓存)
MIGRATE_RECLAIMABLE, // 可回收(如文件映射页面)
MIGRATE_PCPTYPES, // 每CPU链表类型
MIGRATE_HIGHATOMIC, // 紧急分配池
MIGRATE_ISOLATE, // 隔离(不参与分配)
MIGRATE_TYPES
};
页块迁移类型(pageblock):每个页块(大小为1 << (MAX_ORDER-1))包含若干连续页,页块头部页的mapcount字段低3bits存储该页块的迁移类型。这确保了同一页块内的页面具有相同迁移属性,避免不可移动和可移动页面混在同一页块中导致无法合并。
2.3 每CPU页面缓存(PCP)
为减少锁竞争,每个zone为每个CPU维护了一个pageset单机页面缓存:
struct per_cpu_pageset {
struct per_cpu_pages pcp; // 热/冷页面缓存
};
struct per_cpu_pages {
int count; // 当前缓存页面数
int high; // 高水位,超过则放回伙伴系统
int batch; // 一次添加的页面数
struct list_head lists[2]; // 0=cold页面, 1=hot页面
};
PCP缓存热页面(可能还在CPU cache中)和冷页面两个链表,分配时优先从热链表获取,减少cache失效。每个CPU可缓存约high(通常100~200个)页框,超出时批量返回给伙伴系统。
2.4 水位线(Watermark)
每个zone维护三个水位线:
struct zone {
unsigned long watermark[NR_WMARK]; // WMARK_MIN/WMARK_LOW/WMARK_HIGH
unsigned long lowmem_reserve[MAX_NR_ZONES]; // 保护预留
};
| 水位 | 含义 | 触发动作 |
|---|---|---|
| WMARK_MIN | 最低水位 | 触发直接内存回收(direct reclaim)+ 阻塞分配者 |
| WMARK_LOW | 低水位 | 唤醒kswapd后台回收线程 |
| WMARK_HIGH | 高水位 | kswapd回收目标水位,达标后停止回收 |
计算公示:min_free_kbytes(/proc/sys/vm/min_free_kbytes)为系统级基准水位,默认约为系统总内存的4‰(即约26MB/4GB RAM)。三个水位分别约为:
WMARK_MIN = min_free_kbytes
WMARK_LOW = WMARK_MIN × 5/4 (约1.25倍)
WMARK_HIGH = WMARK_MIN × 3/2 (约1.5倍)
当系统剩余内存低于WMARK_LOW时,kswapd线程被唤醒执行后台回收;低于WMARK_MIN时,分配者自己陷入直接回收(direct reclaim),分配延迟陡增,可能引发级联阻塞。
2.5 反向映射(Reverse Map)
Buddy System需要高效确定某页框的"伙伴"以便合并。内核通过struct page的特定字段实现:
struct page {
// 对于伙伴系统空闲页:
// private字段 = order(所在的free_area order)
// page Buddylist链接入free_list
// 对于复合页(compound page):
// head page 的 first_page指向自己
// tail page的 first_page指向head page
// head page的 compound_order = 实际order
};
复合页(Compound Page):分配2^n个页框时,内核将第一个页框设为head page,其余设为tail page。释放时通过head page判断该复合页是否全部空闲,若是则向上合并。
三、SLAB分配器
SLAB分配器最初由Jeff Bonwick为Solaris设计,1994年引入Linux 2.0内核。其核心思想是对象缓存(Object Cache):为高频使用的内核对象(如task_struct、inode、dentry等)预分配空间,避免频繁初始化和销毁的开销。
3.1 SLAB架构三层次
SLAB分配器采用三层结构:
kmalloc-128 → slab cache(包含多个slab)
kmalloc-256 → slab cache(每CPU缓存+共享缓存+partial/full/empty三链表)
kmalloc-512 → slab cache
...
kmalloc-2M → slab cache
每个slab = 若干连续页框(默认1~4页),切分为N个等大小槽位
核心数据结构:
// slab缓存
struct kmem_cache {
struct kmem_cache_cpu __percpu *cpu_slab; // 每CPU缓存
struct kmem_cache_node *node[MAX_NUMNODES]; // 每个NUMA节点
unsigned int object_size; // 对象实际大小(含对齐)
unsigned int size; // slot槽位大小(含元数据)
unsigned int offset; // 下一个空闲slot的下标偏移
unsigned int refcount; // 引用计数
unsigned int gfporder; // 每个slab包含2^gfporder个页面
unsigned long flags; // SLAB_*
unsigned int num; // 每个slab包含的对象数
int red_left_pad; // RED_ZONE填充(调试用)
ctor; // 构造函数
};
// 每CPU缓存
struct kmem_cache_cpu {
void **freelist; // 空闲slot链表
unsigned long tid; // 全局事务ID(解决ABA问题)
struct page *page; // 当前正使用的slab页
struct page *partial; // 该CPU的部分满slab列表(SLUB特有)
};
// NUMA节点级缓存
struct kmem_cache_node {
spinlock_t list_lock;
unsigned long nr_partial; // partial slab数量
struct list_head partial; // partial slab链表
};
3.2 SLAB的三种slab列表
每个slab由三种状态组成,决定了其管理位置:
| 状态 | 含义 | 存储位置 |
|---|---|---|
| Full | 所有slot均被分配 | kmem_cache_node.full(SLAB)或无(SLUB) |
| Partial | 部分slot空闲 | kmem_cache_node.partial |
| Empty | 所有slot空闲 | 空闲slab链表 |
首次分配时从伙伴系统获取页框创建新slab。释放时slot回到CPU freelist或触发slab状态变更。Empty slab在slab_reclaim间隔(默认5秒)后被收回给伙伴系统。
3.3 SLUB分配器
SLUB(Unqueued Slab)是Linux 2.6.23(2007年10月)引入的SLAB替代品,现为默认分配器。SLUB简化了SLAB架构,主要改进:
- 取消per-slab链表头:SLAB每个slab需要在页头维护
kmem_bufctl_t[]数组记录空闲slot链。SLUB利用struct page自身的freelist/inuse/objects字段,消除了额外元数据空间浪费。
- 每CPU partial链表:SLUB为每个CPU维护一个partial slab链表,减少锁争用。分配时优先从
cpu_slab->freelist取,freelist空时从自身的partial链表或从伙伴系统创建新slab。
- 合并kmalloc缓存:SLUB将size相近的kmalloc缓存合并(size>4KB时自动合并到更高等级),减少了
kmalloc-*链表的数量。
- NUMA感知:SLUB从分配请求所在的NUMA节点的node本地缓存中取slab,减少了跨NUMA访问。
SLUB分配流程(简化版):
kmem_cache_alloc(cache, flags)
├─ 1. 关中断,进入当前CPU的cpu_slab
├─ 2. cpu_slab->freelist是否非空? ──是──> 跳转到步骤6
├─ 3. cpu_slab->partial是否非空? ──是──> 切换page为partial head, freelist指向, 跳转2
├─ 4. node->partial是否非空? ──是──> 需要一个node lock, 切换page, 跳转2
├─ 5. 从Buddy System分配新slab ──> 切换page, 构建freelist, 跳转2
└─ 6. 取出首object,更新freelist指针
开中断,返回object
关键点:SLUB将链表指针直接嵌入到slab的空闲slot中。当slot空闲时,其前4/8字节存储下一个空闲slot的地址;当slot被分配出去后,该空间供用户使用,无需额外内存开销。
3.4 SLOB分配器
SLOB(Simple List Of Blocks)是最精简的slab分配器,专为嵌入式系统设计(内存极度受限,通常<64MB)。
SLOB将所有空闲块按大小排序在free_list上,通过first-fit查找合适的块,将剩余空间重新插入对应大小的空闲链表中。优缺点极为鲜明:
- 优点:代码量极小(~600行),内存开销极小(无per-object元数据)
- 缺点:O(n)分配复杂度,内存碎片严重(仅内部碎片,无外部碎片机制),无法应对高并发
- 启用条件:
CONFIG_SLOB=y,极少在服务器上启用
3.5 kmalloc详解
kmalloc()是内核通用内存分配API:
void *kmalloc(size_t size, gfp_t flags);
void kfree(const void *p);
size到slab cache的映射:
size: slab cache name:
0~8B kmalloc-8
16B kmalloc-16
32B kmalloc-32
64B kmalloc-64
96B kmalloc-96 (注意:96不是2的幂)
128B kmalloc-128
192B kmalloc-192
256B kmalloc-256
512B kmalloc-512
1024B kmalloc-1k
2048B kmalloc-2k
4096B kmalloc-4k
8192B kmalloc-8k
...
>8KB 直接走Buddy System(基于页分配)
当size>8KB(2×PAGE_SIZE,具体阈值取决于配置)时,kmalloc()会退化为直接通过伙伴系统分配,因为此时SLAB管理本身的开销已不可接受。
gfp标志指导分配行为:
| Category | Flag | 含义 |
|---|---|---|
| 区域修饰符 | __GFP_HIGHMEM |
允许使用高端内存 |
__GFP_HIGHMEM_MOVABLE |
允许从highmem取可移动页 | |
| 行为修饰符 | __GFP_RECLAIM |
允许直接回收(触发kswapd + shrink) |
__GFP_IO |
允许磁盘IO | |
__GFP_FS |
允许文件系统操作 | |
__GFP_DIRECT_RECLAIM |
允许分配者陷入直接回收 | |
| 紧急修饰符 | __GFP_HIGH |
紧急分配池 |
__GFP_MEMALLOC |
越过min watermark直接分配 | |
__GFP_NORETRY |
失败后不重试 | |
__GFP_NOWARN |
失败时不打印警告 | |
| 组合宏 | GFP_KERNEL |
标准内核分配(允许IO/FS/RECLAIM) |
GFP_ATOMIC |
原子分配,不睡眠(=GFP_KERNEL & ~__GFP_RECLAIM) | |
GFP_NOWAIT |
不睡眠,不触发回收 | |
GFP_DMA |
从DMA zone分配 | |
GFP_KERNEL_ACCOUNT |
计入内存控制组(memcg)配额 |
3.6 vmalloc详解
vmalloc()提供虚拟连续但物理不一定连续的内存:
void *vmalloc(unsigned long size);
void vfree(const void *addr);
与kmalloc的区别:
| 特性 | kmalloc | vmalloc |
|---|---|---|
| 物理地址 | 连续 | 可能不连续 |
| 映射区域 | ZONE_NORMAL直接映射 | vmalloc区(有限大小,x86_64约128MB~1TB) |
| 分配延迟 | O(1)(slab分配) | 需建立页表映射,较慢 |
| 适合模块 | 硬件DMA、小对象 | 大段内存映射、不需要物理连续的场景 |
| TLB压力 | 低(1 page table entry per object) | 每4KB一页面都需要TLB entry |
| 合并释放 | 回收到slab cache | 建立页表反转映射,需要遍历vmalloc区的红黑树 |
vmalloc通过vmap()建立虚拟到物理的映射:
- 分配虚拟地址区间(vm_struct插入红黑树+链表)
- 从Buddy System获取所需个数的页框
- 建立页表映射(虚拟→物理),设置TLB属性
flush_tlb_kernel_range()刷新TLB
实际建议:仅当真正需要"物理连续"(如DMA操作)时使用kmalloc,要求不严格时优先使用kmalloc而非vmalloc,因为vmalloc开销较大。
3.7 内核内存控制组(memcg)
控制组(cgroup v1内存控制器 / v2 memory controller)对内核内存计入配额:
GFP_KERNEL_ACCOUNT:分配的slab计入当前进程的memcg- 资源记账(accounting)方式:按slab缓存级别计费(
memcg_slab_errors),包括活跃/非活跃对象的内存占用 memory.kmem.usage_in_bytes:查看内核内存使用量memory.kmem.limit_in_bytes:软/硬限制内核内存上限
四、Slab分配器生产调优
4.1 查看Slab分配器状态
# 查看slab缓存概述(按占用内存排序)
cat /proc/slabinfo
slabtop # 交互式视图(类似top)
# 输出字段:
# OBJS ACTIVE USE OBJ_SIZE OBJS_PER_SLAB PAGE_PER_SLAB CACHE_SIZE NAME
# 40576 40449 99% 1152 5 2 150528K dentry
# 查看每个缓存的详细信息
cat /sys/kernel/slab/dentry/object_size # 每个对象大小
cat /sys/kernel/slab/dentry/objs_per_slab # 每个slab的对象数
cat /sys/kernel/slab/dentry/order # 每个slab的order
# 查看系统总体cat /proc/meminfo | grep Slab
# Slab: 123456 kB # slab总内存(active+inactive)
# SReclaimable: 98765 kB # 可回收(如dentry cache)
# SUnreclaim: 24691 kB # 不可回收(如kmalloc-*)
关键指标:
- 高
USE%:对象活跃度,96%+说明非活跃对象少,内存利用率高 SReclaimable占比高:内核可在内存压力时回收(如dentry/inode缓存),通常不是问题SUnreclaim占比高:内核模块或kmalloc大量消耗,内存压力大时无法回收
4.2 Slab Shrinker
内核注册了多种shrinker,用于在内存压力时释放非活跃对象:
struct shrinker {
unsigned long (*count_objects)(struct shrinker *, struct shrink_control *);
unsigned long (*scan_objects)(struct shrinker *, struct shrink_control *);
int seeks; // 重建频率惩罚(越大越容易被跳过)
long batch; // 每次scan的操作数
unsigned long flags;
struct list_head list;
int id;
};
常用shrinker:
- dentry cache:释放未使用的目录项
- inode cache:释放非活跃的inode
- slab shrinker:释放空slab到伙伴系统
- 自定义shrinker:如内核模块注册的shrink器(如Docker的
docker-shrinker)
# 查看shrinker状态(debugfs,需CONFIG_DEBUG_FS)
mount -t debugfs none /sys/kernel/debug
cat /sys/kernel/debug/shrinker/* # 各shrinker的统计信息
4.3 Slub Debug
SLUB提供了详细的调试机制(需编译时启用CONFIG_SLUB_DEBUG):
| 调试开关 | 作用 |
|---|---|
slab_debug=F |
启用red zone 越界检测(前后加魔术字0x10和0x11) |
slab_debug=U |
启用UAF(释放后使用)检测——释放时填充0x5a |
slab_debug=Z |
启用未初始化检测——分配时填充0x6b |
slab_debug=P |
启用 poisoning——分配时填充0xaa |
slab_debug=T |
启用trace——通过红黑树追踪栈回溯 |
slab_debug=A |
启用所有调试选项(需要内存巨大) |
slab_nomerge |
禁止合并slab缓存,隔离不同来源的分配 |
示例:
# 启动参数增加slub debug,检测slab越界
slab_debug=FP # 排查特定slab缓存的分配栈
# 查看slab trace(记录每个对象的分配栈)
cat /sys/kernel/slab/dentry/trace
效果检测示例——越界释放:
=============================================================================
BUG: KASAN: slab-out-of-bounds in my_driver_probe+0x37/0x1e0 [my_driver]
Read of size 8 at addr ffff888123456789 by task insmod/1234
...
ffff888123456789 is located 123 kmalloc-192, offset 117, 0 bytes right of
192-byte region [ffff888123456700, ffff8881234567c0)
allocated by task 1234 on cpu 5:
__kmalloc_track_caller+0x1e0/0x350
kmemdup+0x29/0x60
my_driver_probe+0x24/0x1e0 [my_driver]
4.4 Slub sysctl调优
# 修改slab分配器全局参数
# 最大slab order(默认0,即每个slab最多4页)
sysctl -w kernel.slub_max_order=3 # 允许最多8页slab,对象极大时减少TLB miss
# slab回收间隔(默认5秒)
sysctl -w vm.slab_reclaim_interval=1 # 每秒检查一次是否回收空slab(节省内存)
# 禁用slab merge(便于调试,内存更大)
sysctl -w slab_nomerge=1
4.5 /proc/sys/vm关键参数
# kswapd触发阈值,控制何时启动后台回收
sysctl -w vm.min_free_kbytes=65536 # 64MB(2GB RAM建议设为64,4GB设为128)
# swappiness:
sysctl -w vm.swappiness=1 # 尽量不换出(延迟敏感)
sysctl -w vm.swappiness=60 # 默认
sysctl -w vm.swappiness=100 # 积极换出
# 注意:swappiness值越低,越倾向回收slab cache(SReclaimable)
# swappiness值越高,越倾向swap out匿名页(RSS)
# 脏页写回间隔(控制文件映射页的耐久度)
sysctl -w vm.dirty_writeback_centisecs=500 # 每5秒唤醒flush线程
sysctl -w vm.dirty_expire_centisecs=3000 # 超过30秒的脏页写回
# page-cluster:每次swap in多少个连续页
sysctl -w vm.page_cluster=3 # 默认3,即每次swap in 8个连续页
五、KASAN / KFENCE 检测工具
5.1 KASAN(Kernel Address Sanitizer)
基于编译器sanitizer(fsanitize=kernel-address)的内存错误检测工具:
- 原理:对每8字节内存,附加1字节shadow memory(比例1:8),
每个字节表示内存状态(前K字节有效、后(8-K)字节无效、全部释放等)
- 检测能力:堆/栈越界、Use-After-Free、部分Double-Free
- 开销:内存×3,运行速度×2(Compiler-based shadow memory)
- 启用:
CONFIG_KASAN=y+ 编译器-fsanitize=kernel-address
5.2 KFENCE(Kernel Electric Fence)
轻量级内存错误检测,设计为生产环境可用:
- 原理:采样检测(仅检测1/5000的分配),用guard page包围检测对象的slab页
- 检测堆溢出:对象边界触碰guard page →触发page fault →报告
- 检测UAF:分配时记录线程/栈,释放时跟踪延迟;对象被访问时用
memchr检查魔术字是否变化 - 开销:采样因此极小(运行时开销<1%)
- 启用:
CONFIG_KFENCE=y+kfence.sample_interval=500(每500次分配检测一次) - 配置:
查看KFENCE报告:
[ 123.456] ==================================================================
[ 123.456] BUG: KFENCE: use-after-free write in copy_process+0x1ee/0x16a0
(addr ffff888765432100, size 160)
[ 123.456] Use-after-free write at 0xffff888765432190 (in kfence-#123):
[ 123.456] copy_process+0x1ee/0x16a0
[ 123.456] kernel_clone+0x99/0x30a
[ 123.456] __do_sys_clone+0x76/0x98
...
[ 123.456] freed by task 427 on cpu 3:
[ 123.456] dup_task_struct+0x243/0x3e0
[ 123.456] copy_process+0x1ee/0x16a0
[ 123.456] ...
[ 123.456] kfence-#123: 0xffff888765432100-0xffff888765432260, by task "systemd":
[ 123.456] alloc_insn_match+0x0/0x170
...
KFENCE相比KASAN更适合**生产环境启用**,可连续运行数月不显著影响性能,能捕获那些概率性爆发的内存安全漏洞。
## 六、Slab实战排障案例
### 案例一:kmem_cache "leak"排查
**现象**:/proc/meminfo显示SUnreclaim持续缓慢增长,OOM Killer最终触发。
**排查步骤**:
1. 确定增长的slab缓存
cat /proc/slabinfo | sort -k 8 -n -r | head -20
2. 查看是谁在分配这些对象
cat /sys/kernel/slab/
或直接检查栈记录(启用slab_debug=T后)
slub_debug=T
**确认泄漏路径**:若栈回溯显示为特定内核模块或IO调度器的分配,可以:
重置该cache(强制释放所有空slab,仅诊断用)
echo 1 > /sys/kernel/slab/
或者使用slabtop实时观察增长
slabtop -s c # 按当前增长速率排序
### 案例二:NUMA node内存分配不均
**现象**:多NUMA node系统中,部分node耗尽触发频繁本地直接回收。
**排查步骤**
查看各node的空闲内存
numactl -H
node 0: node 1:
MemTotal: 32715.79 MB MemTotal: 32705.67 MB
MemFree: 427.05 MB MemFree: 8256.39 MB #严重不均衡
查看zone水位
cat /proc/zoneinfo | grep -E "Node|zone|min|low|high"
**解决方案**:
- 启用`zone_reclaim_mode=1`使local reclaim优先回收本地node内存:
sysctl -w vm.zone_reclaim_mode=1
- 应用程序使用`libnuma`绑定节点或使用`set_mempolicy()`交错分配
- 若仍为单NUMA应用,关闭zone reclaim(默认0)全局分配
### 案例三:kzalloc GFP_KERNEL vs GFP_ATOMIC选择
**现象**:中断上下文错误使用`GFP_KERNEL`导致睡眠在原子上下文中触发`scheduling while atomic`。
BUG: scheduling while atomic: swapper/0/0/0x00000102
modules linked in: ...
CPU: 0 PID: 0 Comm: swapper/0 Tainted: G W OE 6.1.0
backtrace:
kmalloc_trace+0x34/0x50
_GFP_ ...+0x100/0x200 [module]
hardirq_handler+0x44/0x90
**修复原则**:
- Interrupt handler / spinlock内:`GFP_ATOMIC`(或`GFP_NOWAIT`)
- Process context可睡眠:`GFP_KERNEL`
- Holding mutex:`GFP_KERNEL`(但注意mutex可睡眠)
## 七、性能优化最佳实践
### 7.1 减少cache bouncing
多核系统中,若所有CPU竞争同一slab缓存,应:
- 优先使用`kmem_cache_create()`创建私有缓存(per-driver cache)
- 使用`____cacheline_aligned_in_smp`对齐避免false sharing
- 配置`/proc/sys/vm/zone_reclaim_mode`降低不必要的远端node访问
### 7.2 大对象分配策略
size>4KB的分配可考虑`vmalloc`作为fallback,或通过`kmem_cache_create()`创建独立缓存,避免挤占常用kmalloc的slab。
### 7.3 避免OOM
- 启用`vm.overcommit_memory=2`(严格模式)避免OOM假设内存永远可用
- 合理设置`vm.min_free_kbytes`保持安全边际
- 使用memcg限制关键服务内核内存上限
### 7.4 slab allocation profiling
通过BPF trace分配路径热点:
bpftrace -e 'kprobe:slab_alloc* { @[comm, kstack] = count(); }'
高频alloc路径即为热点
或使用perf record工具:
perf record -e kmem:kmalloc -ag -- sleep 30
perf report --sort=symbol

发表评论 取消回复