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架构,主要改进:

  1. 取消per-slab链表头:SLAB每个slab需要在页头维护kmem_bufctl_t[]数组记录空闲slot链。SLUB利用struct page自身的freelist/inuse/objects字段,消除了额外元数据空间浪费。
  1. 每CPU partial链表:SLUB为每个CPU维护一个partial slab链表,减少锁争用。分配时优先从cpu_slab->freelist取,freelist空时从自身的partial链表或从伙伴系统创建新slab。
  1. 合并kmalloc缓存:SLUB将size相近的kmalloc缓存合并(size>4KB时自动合并到更高等级),减少了kmalloc-*链表的数量。
  1. 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()建立虚拟到物理的映射:

  1. 分配虚拟地址区间(vm_struct插入红黑树+链表)
  2. 从Buddy System获取所需个数的页框
  3. 建立页表映射(虚拟→物理),设置TLB属性
  4. 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//trace

或直接检查栈记录(启用slab_debug=T后)

slub_debug=T


**确认泄漏路径**:若栈回溯显示为特定内核模块或IO调度器的分配,可以:

重置该cache(强制释放所有空slab,仅诊断用)

echo 1 > /sys/kernel/slab//validate

或者使用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

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿
网站二维码

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部