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

Linux内核学习轨迹第五部:内核内存分配器:SLUB/SLOB/SLAB全解析(第四小节)

4. 内核内存分配器:SLUB/SLOB/SLAB全解析

伙伴系统解决了外部碎片问题,以页为单位分配内存,但内核中大量的小对象(比如task_struct、inode、file结构体),大小只有几十到几百字节,如果用整页分配,会导致严重的内部碎片。SLAB/SLUB/SLOB分配器就是为了解决这个问题,基于伙伴系统分配的整页,拆分更小的对象,实现小内存的高效分配,减少内部碎片。

4.1 三种分配器的对比与适用场景

Linux内核提供了三种slab分配器,可通过内核启动参数选择,默认使用SLUB分配器:

分配器

核心设计

优势

劣势

适用场景

SLUB

无队列设计,简化结构,基于页管理对象

性能高、扩展性好、调试简单、内存占用低,解决了SLAB的队列锁竞争问题

对小对象的内存利用率略低于SLAB

Linux默认分配器,绝大多数场景,包括服务器、桌面、嵌入式

SLAB

基于每CPU/每节点队列设计,复杂的缓存结构

小对象内存利用率高,缓存命中率高

代码复杂、多核锁竞争严重、扩展性差、调试困难、内存开销大

老旧系统,对小对象内存利用率要求极高的嵌入式场景

SLOB

简单的首次适配分配器,代码极简

代码量极小,内存占用极低

内存碎片化严重,性能差,多核扩展性极差

内存极小的嵌入式设备,比如单片机、物联网设备

本章节重点讲解默认的SLUB分配器,它是目前Linux内核的主流,性能和扩展性最好,代码结构清晰。

4.2 SLUB分配器的核心设计思想

SLUB分配器的核心设计是简化结构,消除SLAB的复杂队列,基于页管理对象,核心思想:
  1. Slab页:SLUB从伙伴系统分配一个或多个连续的页(Slab页),把页拆分为固定大小的对象,每个Slab页只管理一种大小的对象;
  2. Kmem Cache:每种大小的对象对应一个kmem_cache缓存,比如task_struct对应一个kmem_cache,inode对应另一个kmem_cache,每个缓存管理自己的Slab页;
  3. 无队列设计:SLUB没有SLAB复杂的每CPU/每节点队列,直接把空闲对象链表存储在Slab页本身,减少锁竞争,提升多核扩展性;
  4. 每CPU缓存:为每个CPU维护一个活动Slab页,单CPU的对象分配和释放,直接从本地CPU的活动Slab中操作,不需要加锁,性能极高;
  5. 合并缓存:SLUB会自动合并大小相近的kmem_cache,减少Slab页的数量,提升内存利用率。

4.3 SLUB分配器的核心数据结构

SLUB分配器的核心数据结构有两个:struct kmem_cache(对象缓存)和struct slab(Slab页管理结构),定义在mm/slub.c中。

4.3.1 kmem_cache结构体

每个固定大小的对象对应一个kmem_cache实例,管理该大小对象的所有Slab页,核心字段拆解:
struct kmem_cache { // 每CPU的活动Slab缓存,无锁快速分配 struct kmem_cache_cpu __percpu *cpu_slab; // 缓存的名称,比如"task_struct"、"kmalloc-128" const char *name; // 对象的大小,包括对齐填充 unsigned int size; // 对象的实际大小,不包括对齐填充 unsigned int object_size; // 对象的对齐要求 unsigned int align; // 每个Slab页包含的对象数量 unsigned int num; // 每个Slab页的阶数(2^order个页) unsigned int order; // 对象的偏移量,在Slab页中的起始位置 unsigned int offset; // Slab的标志位,比如SLAB_POISON、SLAB_RED_ZONE等调试标志 slab_flags_t flags; // 构造函数,对象分配时调用 void (*ctor)(void *obj); // 节点的Slab管理结构,每个NUMA节点对应一个 struct kmem_cache_node *node[MAX_NUMNODES]; // 缓存合并相关字段 struct kmem_cache *next; // 调试相关字段 int refcount; int in_slab; } ____cacheline_aligned;
核心字段解析
  1. cpu_slab:每CPU的活动Slab缓存,是SLUB快速分配的核心,每个CPU有一个独立的活动Slab页,分配和释放不需要加锁,性能极高;
  2. size/object_size:对象的大小,object_size是用户实际需要的大小,size是对齐后的大小,包括填充和元数据;
  3. order:每个Slab页的阶数,决定了从伙伴系统分配的连续页数量,对象越大,order越大;
  4. um:每个Slab页包含的对象数量,由Slab页的总大小和对象大小决定;
  5. ode[MAX_NUMNODES]:每个NUMA节点的Slab管理结构,管理该节点内的所有Slab页,包括满的、部分空闲的、完全空闲的Slab页。

4.3.2 kmem_cache_cpu结构体

每CPU的活动Slab缓存,是SLUB无锁快速分配的核心,定义在mm/slub.c中:
struct kmem_cache_cpu { // 空闲对象链表的第一个对象,无锁分配直接从这里取 void **freelist; // 当前活动的Slab页对应的page结构体 struct page *page; // 下一个要释放的对象,用于批量释放 void **next_freelist; // 事务编号,用于调试 unsigned int tid; };
核心设计:每个CPU的kmem_cache_cpu有一个当前活动的Slab页,freelist指向该Slab页中的空闲对象链表,分配时直接从freelist取第一个对象,释放时直接把对象放回freelist,整个过程不需要加锁,因为是每CPU的私有数据,多核之间没有竞争,性能极高。

4.3.3 Slab页与page结构体的关联

SLUB的Slab页就是从伙伴系统分配的连续物理页,用struct page结构体管理,page结构体中的联合体字段,对应SLUB的管理信息:
// page结构体中SLUB相关的联合体字段 struct { struct slab *slab; // 指向Slab管理结构 void *s_mem; // Slab内的第一个对象地址 union { unsigned int active; // Slab内的活跃对象数 void *freelist; // 空闲对象链表 }; };
每个Slab页有三种状态:
  1. 满(Full):Slab内的所有对象都被分配了,没有空闲对象;
  2. 部分空闲(Partial):Slab内有部分对象被分配,还有空闲对象;
  3. 完全空闲(Free):Slab内的所有对象都被释放了,没有活跃对象。

4.4 SLUB分配器的核心流程源码解析

SLUB分配器的核心流程分为对象分配、对象释放、Slab页的分配与释放,我们基于Linux 6.6内核拆解核心逻辑。

4.4.1 对象分配流程:kmem_cache_alloc()

对象分配的核心入口是kmem_cache_alloc(),最终落到slab_alloc()函数,核心分为快速路径和慢速路径。
分配流程核心步骤
kmem_cache_alloc(struct kmem_cache *s, gfp_t gfpflags)
slab_alloc(s, gfpflags, _RET_IP_)
1. 快速路径分配:
├→ 关闭抢占,获取当前CPU的kmem_cache_cpu
├→ 从freelist中取出第一个空闲对象
├→ 更新freelist,指向下一个空闲对象
├→ 开启抢占,返回对象地址
└→ 如果freelist为空,进入慢速路径
2. 慢速路径分配:__slab_alloc()
├→ 检查当前活动Slab页是否还有空闲对象,有则刷新freelist,回到快速路径
├→ 如果没有,从当前NUMA节点的Partial Slab链表中,取出一个有空闲对象的Slab页,设置为当前CPU的活动Slab,回到快速路径
├→ 如果Partial链表为空,调用new_slab(),从伙伴系统分配新的Slab页,初始化对象和空闲链表,设置为活动Slab,回到快速路径
└→ 如果伙伴系统分配失败,返回NULL,分配失败
核心逻辑解析
  1. 快速路径是无锁、无睡眠的,性能极高,99%的分配都会在快速路径完成;
  2. 只有当当前CPU的活动Slab页没有空闲对象时,才会进入慢速路径,需要加锁访问节点的Partial链表,或者从伙伴系统分配新的Slab页;
  3. ew_slab()函数会从伙伴系统分配对应order的连续页,初始化Slab页,把页拆分为固定大小的对象,构建空闲对象链表,设置为当前CPU的活动Slab。

4.4.2 对象释放流程:kmem_cache_free()

对象释放的核心入口是kmem_cache_free(),最终落到slab_free()函数,核心流程和分配对应。
释放流程核心步骤
kmem_cache_free(struct kmem_cache *s, void *obj)
slab_free(s, obj, _RET_IP_)
1. 快速路径释放:
├→ 关闭抢占,获取当前CPU的kmem_cache_cpu
├→ 把释放的对象插入到freelist的头部
├→ 开启抢占,释放完成
└→ 如果Slab页从满变为部分空闲,进入慢速路径
2. 慢速路径释放:__slab_free()
├→ 把Slab页加入到节点的Partial链表中
├→ 检查Slab页是否所有对象都被释放,如果是,把Slab页释放回伙伴系统
└→ 完成释放
核心逻辑解析
  1. 快速路径释放也是无锁的,直接把对象放回当前CPU活动Slab的freelist中,性能极高;
  2. 只有当Slab页从满变为部分空闲,或者完全空闲时,才会进入慢速路径,需要加锁操作节点的链表,或者把Slab页释放回伙伴系统;
  3. SLUB会定期把完全空闲的Slab页释放回伙伴系统,避免内存浪费,这一点和SLAB不同,SLAB会缓存大量的Slab页,不会轻易释放。

4.5 kmalloc通用内核内存分配器

kmalloc是内核中最常用的通用内存分配接口,基于SLUB分配器实现,类似于用户态的malloc,用于分配任意大小的内核小内存。
kmalloc的实现原理:
内核预定义了一系列通用的kmem_cache,对应不同的大小,比如kmalloc-8、kmalloc-16、kmalloc-32、kmalloc-64、kmalloc-128、kmalloc-256、kmalloc-512、kmalloc-1024、kmalloc-2048、kmalloc-4096等,每个大小对应一个kmem_cache缓存。当调用kmalloc(size)时,内核会找到大于等于size的最小的kmalloc缓存,从对应的缓存中分配对象,比如分配100字节,会从kmalloc-128缓存中分配。

4.6 SLUB分配器的工程实践与调试

1.SLUB调试功能的使用

SLUB提供了强大的调试功能,可以检测内存泄漏、越界访问、释放后使用、重复释放等常见的内核内存bug,是驱动开发、内核调试的利器。

a.内核启动参数开启SLUB调试:

slub_debug=FUZP
常用调试标志:
  1. F:开启sanity checks,每次分配和释放都检查Slab的合法性;
  2. U:开启释放后使用检测,释放的对象填充毒值,访问会触发警告;
  3. Z:开启红区检测,对象前后设置红区,越界访问会触发警告;
  4. P:开启Poison检测,分配的对象填充毒值,未初始化访问会触发警告;
  5. A:开启所有调试功能。

调试信息查看:

cat /sys/kernel/slab//,可以查看每个缓存的分配统计、错误信息、活动对象等。

2.Slab内存泄漏排查

线上常见的问题是Slab内存占用持续增长,导致系统内存不足,OOM触发,根源是内核驱动/模块的内存泄漏。

排查流程:

  1. 查看/proc/meminfo,确认Slab、SReclaimable(可回收)、SUnreclaim(不可回收)的占用,如果SUnreclaim持续增长,说明有不可回收的Slab内存泄漏;
  2. 查看/proc/slabinfo,或者slabtop命令,找到占用内存最多、对象数量持续增长的kmem_cache;
  3. 开启SLUB调试,跟踪对象的分配和释放栈,找到泄漏的位置;
  4. 常见根因:内核驱动分配的对象没有释放、引用计数泄漏、缓存对象没有正确回收。

临时解决方案:如果是可回收的Slab内存,可以手动回收:echo 2 > /proc/sys/vm/drop_caches,释放可回收的slab对象。

3.自定义kmem_cache的最佳实践

内核驱动开发中,如果需要频繁分配和释放固定大小的对象,应该创建自定义的kmem_cache,而不是用kmalloc,性能更高,内存利用率更好,也更容易调试。

自定义kmem_cache的标准实现:

// 定义自定义对象结构体
struct my_obj { int id; char name[32]; struct list_head list; }; // 定义缓存指针 static struct kmem_cache *my_obj_cache; // 模块初始化时创建缓存 static int __init my_module_init(void) { // 创建自定义kmem_cache my_obj_cache = kmem_cache_create( "my_obj_cache", // 缓存名称 sizeof(struct my_obj), // 对象大小 __alignof__(struct my_obj), // 对齐要求 SLAB_HWCACHE_ALIGN | SLAB_POISON, // 标志位 NULL // 构造函数 ); if (!my_obj_cache) { return -ENOMEM; } return 0; } // 分配对象 struct my_obj *obj = kmem_cache_alloc(my_obj_cache, GFP_KERNEL); // 释放对象 kmem_cache_free(my_obj_cache, obj); // 模块退出时销毁缓存 static void __exit my_module_exit(void) { // 销毁缓存前,必须确保所有对象都被释放 kmem_cache_destroy(my_obj_cache); } module_init(my_module_init); module_exit(my_module_exit);

避坑指南:模块退出时,必须确保所有从缓存中分配的对象都被释放,否则调用kmem_cache_destroy()会失败,导致内存泄漏;自定义缓存的名称必须唯一,不能和系统已有的缓存重名。

4.SLUB与SLAB的选型

  1. 绝大多数场景下,使用默认的SLUB分配器即可,它的性能、扩展性、调试性都远优于SLAB;
  2. 只有在内存极小的嵌入式设备,对小对象的内存利用率要求极高时,才考虑使用SLAB;
  3. 只有在内存极小的单片机、物联网设备,代码量要求极简时,才考虑使用SLOB。
http://www.jsqmd.com/news/964158/

相关文章:

  • 韶关瑜伽普拉提会所的实际体验差异是什么?
  • 电动执行器的机械限位和电子限位,哪个更可靠?
  • MATLAB波前重建工具:用Zernike多项式解析横向剪切干涉相位差
  • B2B订单系统怎么做?流程引擎与权限模型拆解
  • KeyboardChatterBlocker:精准解决机械键盘连击问题的软件解决方案
  • 中国电子学会图形化2021.6月Scratch三级考级题
  • 从雕刻到拓扑|ZBrush 2026.1.1 版本 硬表面、动态雕刻、平板联动全方位升级
  • 【图像隐藏】多通道DWT-DCT-SVD彩色图像水印系统附Matlab代码
  • SolidWorks 工程图内容丢失(不显示)解决方法
  • 嵌入式老鸟的调试心法:如何快速搞定uboot不认新Flash的问题
  • 2024华为杯C题磁芯损耗建模全套实战资料:5问Python代码+双版本30+页论文+原始数据与结果表
  • DeepL智能翻译插件实战指南:浏览器内专业级翻译体验完整方案
  • 【愚公系列】《移动端AI应用开发》013-DeepSeek API开发与集成(深度集成与中间件架构)
  • 足坛战袍的智造秘密:誉财全链路设备打造顶级球衣品质
  • 高端制造行业晶圆制造技术岗Fab 工艺工程师晋升CTO的路径。
  • 用 OpenCLAW 重写 CUDA 内核:从原理到实践
  • 抖音视频批量下载终极指南:douyin-downloader高效解决方案
  • 告别重复造轮子:用快马ai一键生成arm7常用外设驱动模块
  • iTop:开源IT服务管理的哲学重构与架构革新
  • Windows 11终极性能调优指南:开源工具Win11Debloat让你的系统重获新生
  • 如何在Windows上完美使用PS3手柄:DsHidMini终极指南
  • MATLAB R2017a三容水箱并行仿真工程:开箱即用的Simulink多核加速控制模型
  • 【科研快报】哈工深等开源CVSearch | 首创认知驱动视觉搜索,让大模型学会“看重点“
  • Tab 键之争:从微软 IBM 到程序员群体,半个世纪的代码缩进战争!
  • 别再为天线匹配头疼了!用HFSS仿真耦合馈电圆极化天线,手把手教你避开传统馈电的坑
  • 建议收藏|一键生成论文工具测评:2026最新好用工具推荐与对比
  • [鸿蒙PC命令行移植适配]移植rust三方库peep到鸿蒙PC的完整实践
  • 免费绕过iOS 15-16激活锁的终极指南:applera1n让你的iPhone重获新生
  • QQ截图独立版:3个隐藏技巧让你的Windows效率飙升300%
  • 中国电子学会图形化2022.6月Scratch三级考级题