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

Linux内存管理章节十九:超越kmalloc:自定义内存分配器开发实战 - 教程

引言

通用内存分配器(如SLUB)设计用于处理各种大小和生命周期的内存请求,其泛用性必然伴随着性能开销:锁竞争、缓存行 bouncing、以及复杂的元数据管理。在对性能有极致要求的内核子系统(如网络栈、文件系统)或特定驱动中,我们往往需要一种更专注、更高效的解决方案。开发自定义内存分配器允许我们针对特定工作负载进行深度优化。本文将指导你实现一个简单的内存池、创建专用对象缓存,并进行性能测试与优化。

一、 实现简单的内存池(Memory Pool)

内存池的核心思想是:预先分配一大块内存,并将其划分为多个固定大小的对象或缓冲区,后续的分配和释放都在这个池内进行

设计与实现
  1. 数据结构

    #include <linux/slab.h>#include <linux/spinlock.h>struct mempool {void *pool_base;// 池的起始虚拟地址dma_addr_t pool_dma;// 池的起始DMA地址(如果用于DMA)size_t obj_size;// 每个对象的大小int num_objs;// 对象总数void **free_list;// 空闲对象链表栈int free_count;// 当前空闲对象数量spinlock_t lock;// 保护池的锁};
  2. 初始化(mypool_init)

    int mypool_init(struct mempool *pool, size_t obj_size, int num_objs, bool needs_dma)
    {
    int i;
    void *obj;
    // 1. 预分配整个内存池
    if (needs_dma) {
    pool->pool_base = dma_alloc_coherent(NULL, obj_size * num_objs,
    &pool->pool_dma, GFP_KERNEL);
    } else {
    pool->pool_base = kmalloc(obj_size * num_objs, GFP_KERNEL);
    }
    if (!pool->pool_base) return -ENOMEM;
    // 2. 初始化元数据
    pool->obj_size = obj_size;
    pool->num_objs = num_objs;
    pool->free_count = num_objs;
    spin_lock_init(&pool->lock);
    // 3. 构建初始空闲链表(后进先出栈)
    pool->free_list = kmalloc_array(num_objs, sizeof(void*), GFP_KERNEL);
    if (!pool->free_list) goto err_free_pool;
    for (i = 0; i < num_objs; i++) {
    obj = pool->pool_base + (i * obj_size);
    pool->free_list[i] = obj;
    }
    return 0;
    err_free_pool:
    if (needs_dma) dma_free_coherent(...);
    else kfree(pool->pool_base);
    return -ENOMEM;
    }
  3. 分配对象(mypool_alloc)

    void *mypool_alloc(struct mempool *pool)
    {
    unsigned long flags;
    void *obj = NULL;
    spin_lock_irqsave(&pool->lock, flags);
    if (pool->free_count >
    0) {
    pool->free_count--;
    obj = pool->free_list[pool->free_count];
    // 从栈顶弹出
    }
    spin_unlock_irqrestore(&pool->lock, flags);
    // 可选:分配失败处理(返回NULL或等待)
    return obj;
    }
  4. 释放对象(mypool_free)

    void mypool_free(struct mempool *pool, void *obj)
    {
    unsigned long flags;
    spin_lock_irqsave(&pool->lock, flags);
    if (pool->free_count < pool->num_objs) {pool->free_list[pool->free_count] = obj;// 压入栈顶pool->free_count++;} else {// 不应发生:释放次数多于分配次数WARN_ON(1);}spin_unlock_irqrestore(&pool->lock, flags);}

优势

  • 极速分配/释放:操作仅为操作栈和整数,复杂度O(1)。
  • 确定性:无锁争用或搜索开销,执行时间恒定。
  • 防止碎片:所有对象大小固定,且生命周期集中。
  • DMA友好:可预先分配物理连续的内存供DMA使用。

适用场景:中断处理程序、网络包缓冲区(skbuff)、频繁分配/释放的固定大小对象。

二、 专用对象缓存创建

如果你需要频繁分配某种特定内核结构体,可以为其创建一个专用缓存。这本质上是让SLUB分配器为你创建一个专属的“精品店”,而不是去“综合商场”(通用缓存)里找。

使用 kmem_cache_create
#include <linux/slab.h>struct kmem_cache *my_cache;// 1. 创建缓存my_cache = kmem_cache_create("my_struct_cache", // 缓存名称(出现在/proc/slabinfo)sizeof(struct my_struct), // 对象大小0, // 对齐偏移(通常为0)SLAB_HWCACHE_ALIGN | SLAB_PANIC, // 标志位NULL);// 构造函数(通常为NULL)if (!my_cache) {// 错误处理,但SLAB_PANIC会使创建失败时直接panic}// 2. 从专用缓存分配对象struct my_struct *obj = kmem_cache_alloc(my_cache, GFP_KERNEL);// 3. 释放对象回专用缓存kmem_cache_free(my_cache, obj);// 4. 销毁缓存(通常在模块退出时)kmem_cache_destroy(my_cache);

关键标志位

  • SLAB_HWCACHE_ALIGN:让SLUB确保每个对象与缓存行(Cache Line) 对齐,防止伪共享(False Sharing),这是非常重要的性能优化。
  • SLAB_POISON:投毒,用于调试use-after-free。
  • SLAB_RED_ZONE:在对象前后插入红区,用于检测缓冲区溢出。

优势

  • 性能提升:避免了通用SLAB中的元数据搜索开销。
  • 缓存友好:通过SLAB_HWCACHE_ALIGN优化,减少了缓存失效。
  • 内存利用率:为特定对象量身定做,减少了内部碎片。
  • 调试支持:可以方便地为整个缓存开启调试功能。

适用场景:频繁分配的内核核心数据结构(如task_struct, inode, file等本身就是这样管理的)。

三、 性能测试与优化

没有测量,就没有优化。自定义分配器必须通过性能测试来证明其价值。

1. 测试方法
2. 优化方向
  • 减少锁竞争
    • ** per-CPU 缓存**:为每个CPU核心创建一个本地的空闲链表。分配和释放优先在本地CPU上进行,仅在本地为空或满时才操作全局链表。这几乎是高性能分配器的标配优化。
    • 无锁设计:尝试使用无锁算法(如CAS)管理空闲链表,但内核中的无锁编程非常复杂,需谨慎。
  • 缓存预热:在系统启动或初始化阶段就完成主要的内存分配,避免在关键路径上分配。
  • 批量操作:一次分配/释放多个对象,分摊锁和元数据操作的开销。
  • NUMA优化:保证分配的内存对正在运行的CPU是本地的,减少远程访问延迟。

总结

开发自定义内存分配器是一项高级技能,其本质是在通用性性能开发复杂度之间做出权衡。

  • 内存池提供了极致的速度和确定性,适用于固定大小、生命周期短、分配频繁的对象。
  • 专用缓存在享受SLUB成熟功能的同时,通过专一化获得了性能提升,适用于特定内核结构体
  • 性能测试是证明其价值的唯一标准,而per-CPU缓存是应对多核扩展性的关键优化。

在决定“造轮子”之前,首先问自己:标准的kmalloc或专用缓存kmem_cache_create是否真的成为了性能瓶颈?只有当答案明确为“是”时,投入精力开发自定义分配器才是值得的。否则,你应该相信并充分利用内核社区已经优化了数十年的通用分配器。

http://www.rkmt.cn/news/7060.html

相关文章:

  • 测井数据分析与建模完整教程 - 详解
  • My All Math
  • 放飞炬人集团:将起草《大国战争人才武(武汉)荆(荆州)襄(襄阳)核心走廊》规划 - 详解
  • TCP粘包问题
  • 开源AI大模型、AI智能名片与S2B2C商城小代码:从“不出现=不存在”到“精准存在”的数字化转型路径
  • ABC310E NAND repeatedly 题解
  • 深度学习之PyTorch核心使用(一)
  • MyBatis插入语句配置
  • 操作运算符
  • 得力 - Bruce
  • 短视频营销运营导师张伽赫,绳木传媒AI+短视频引领企业数字化变革
  • 用 TensorFlow 和 CNN 实现验证码识别
  • 被彼此笼罩 任泪水将我们缠绕 深陷入恶魔的拥抱 在阴冷黑暗处灼烧 吞下这毒药
  • mysql无法连接服务器的mysql #mysql8
  • python错误code
  • C#十五天 026多态重写 027抽象类与开闭原则 028接口,依赖反转,单元测试
  • 解题报告-P11844 [USACO25FEB] Friendship Editing G
  • 说的道理。
  • 【abc180F】Unbranched - Harvey
  • ROS2之节点
  • 详细介绍:如何在公众号接入海外招聘数据分析智能体
  • 密力根油滴实验实验报告
  • 来点人瑞平我
  • 在Unity2021中使用Profiler的Deep Profile功能时内存超高怎么办? - 指南
  • 【P2051】中国象棋 - Harvey
  • Min-Max 容斥小记
  • 【POJ1737】Connected Graph - Harvey
  • 详细介绍:VirtualBox 免费轻量的全能虚拟机,跨平台系统随心装
  • 实用指南:C++ 类型衰变(Type Decay)
  • 某交互题选讲的补题记录