一、简介:为什么内存碎片会“杀死”实时性?

  • 硬实时场景(工业机器人、车载 ECU、金融撮合)要求 最坏-case 申请延迟 < 100 μs

  • Linux 默认伙伴系统(buddy allocator)在 长时间运行后 会出现 外部碎片(足够内存却无连续物理页),导致 kmalloc/malloc 进入慢路径,触发 直接内存规整(direct compaction),延迟 ms 级

  • 结果:控制循环超时、CAN 报文丢失、订单延迟成交。

掌握“防碎片 + 提前锁定 + 大页池”三板斧,是实时 Linux 开发者的 必修课


二、核心概念:先搞懂 6 个关键词

名词一句话本文出现命令/接口
外部碎片物理页足够,但 无连续 区域/proc/buddyinfo
内部碎片分配器给的 比要的多 → 浪费slabtop
伙伴系统2^n 页框管理算法cat /proc/buddyinfo
slab 分配器对象级缓存,减少内部碎片kmem_cache_create()
mempool预分配 + 空闲链表,O(1) 拿内存mempool_alloc()
大页 (hugepage)2 MB/1 GB 连续物理页,TLB 友好mmap(MAP_HUGETLB)

三、环境准备:10 分钟搭好“实时沙箱”

  1. 硬件

    • 任意 x86_64 多核机(≥4 核 8 GB)

    • 支持 虚拟化 即可,无需昂贵板卡。

  2. 软件

    • Ubuntu 20.04 LTS(内核 ≥5.4 已带 PREEMPT_RT 补丁包)

    • 安装命令:

    sudo apt update && sudo apt install -y \
        build-essential git stress-ng hpstat bc \
        linux-tools-$(uname -r) rt-tests
  3. 启用 RT 抢占模式(可选,真实硬实时需开)

    sudo apt install -y linux-image-$(uname -r)-rt
    sudo grub-set-default 0 && sudo reboot
    # 重启后
    uname -v | grep PREEMPT_RT
  4. 实验目录

    mkdir -p ~/rt-mem-lab && cd ~/rt-mem-lab

四、实际案例与步骤:从“看见碎片”到“消除碎片”

每个脚本均 可直接复制 → 保存 → chmod +x xxx.sh && sudo ./xxx.sh 跑通。


4.1 观察碎片:buddyinfo + slabinfo 双视图

#!/usr/bin/env bash
# file: 01-frag-detect.sh
echo "======== 外部碎片 ========="
cat /proc/buddyinfo
echo
echo "======== 内部碎片(TOP20)=="
slabtop -o | head -20

输出解读

  • buddyinfo 每行 Node0, zone Normal 89 45 12 3 2 1 0 0 0 0
    数字从左到右 = 2^0~2^9 连续页框数量;右侧 0 越多 → 大页缺货 → 外部碎片严重


4.2 制造碎片:stress-ng 模拟“长期运行”

#!/usr/bin/env bash
# file: 02-frag-stress.sh
# 50 个线程 随机 malloc/free 4K~4M 块,持续 300 s
sudo stress-ng --vm 50 --vm-ops 1000000 --vm-bytes 4M --vm-hang 0 --timeout 300s

跑完后再次执行 01-frag-detect.sh,可看到 高阶 buddy 条带归零 → 申请 2 MB 大页失败。


4.3 使用内存池:写一个“O(1) 分配”字符设备

场景:硬实时循环周期 1 ms,不容许 kmalloc 延迟

// file: rt_mempool.c
#include <linux/module.h>
#include <linux/mempool.h>
#include <linux/slab.h>

#define MIN_NR  64
#define BUF_SZ  1024

static kmem_cache_t *cache;
static mempool_t *pool;

static int __init rt_init(void)
{
    cache = kmem_cache_create("rt_buf", BUF_SZ, 0, SLAB_POISON, NULL);
    pool = mempool_create_slab_pool(MIN_NR, cache);
    pr_info("rt-mempool ready, min=%d\n", MIN_NR);
    return 0;
}

static void __exit rt_exit(void)
{
    mempool_destroy(pool);
    kmem_cache_destroy(cache);
    pr_info("rt-mempool destroyed\n");
}

module_init(rt_init);
module_exit(rt_exit);
MODULE_LICENSE("GPL");

Makefile

makefile

obj-m += rt_mempool.o
all:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

编译 + 加载

make
sudo insmod rt_mempool.ko
dmesg | tail

验证
slabinfo | grep rt_buf 可见 64 对象已预分配,后续 mempool_alloc 耗时 < 10 μs


4.4 大页池:用户态实时任务零碎片

需求:视频拼接应用需 固定 200 MB 物理连续缓冲

// file: huge_main.c
#define _GNU_SOURCE
#include <stdio.h>
#include <sys/mman.h>
#include <unistd.h>

int main(void)
{
    size_t len = 200UL << 20;          // 200 MB
    void *addr = mmap(NULL, len,
                      PROT_READ | PROT_WRITE,
                      MAP_PRIVATE | MAP_ANONYMOUS |
                      MAP_HUGETLB, -1, 0);
    if (addr == MAP_FAILED) {
        perror("mmap");
        return 1;
    }
    printf("200 MB hugepage @ %p\n", addr);
    /* 实时循环此处省略 */
    munmap(addr, len);
    return 0;
}

编译 & 运行

gcc -O2 huge_main.c -o huge_main

# 预留 256 个大页(每页 2 MB)
echo 256 | sudo tee /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
sudo ./huge_main

检查

cat /proc/meminfo | grep Huge

AnonHugePages 增加 200 MB → 零外部碎片


4.5 碎片整理:手动触发 + 度量耗时

#!/usr/bin/env bash
# file: 03-compact.sh
echo "== 触发内存规整(compact)=="
echo 1 | sudo tee /proc/sys/vm/compact_memory

echo "== 查看延迟(单位 ms)=="
dmesg | awk '/compact_zone.*done.*pages/ {print $11 "ms"}'

输出示例
compact_zone: ... 1024 pages 34ms
→ 实时任务若此时申请大页,最坏延迟 34 ms,远超 1 ms 预算,证明“提前池化”必要性


五、常见问题与解答(FAQ)

问题现象解决
insmod 报 Unknown symbol mempool_create_slab_pool内核版本 <5.4改用 mempool_create_kmalloc_pool
mmap MAP_HUGETLB 失败/proc/meminfo HugePages_Free 为 0提前 echo 256 > nr_hugepages;开机 /etc/sysctl.conf
大页分配仍报“No space”外部碎片严重① 低峰期触发 compact_memory ② 使用 1 GB 大页需 hugetlbfs 挂载
mempool 对象大小变更重新 insmod 报 kmem_cache 已存在rmmod 旧模块或改名
RT 内核 insmod 死机抢占关闭区间调用 mempool_alloc使用 GFP_ATOMIC 且禁止睡眠

六、实践建议与最佳实践

  1. “池化”优先
    实时路径永不裸用 kmalloc/malloc,一律 mempool / slab / hugepage

  2. 预留大页开机即配
    /etc/default/grub 追加

    default_hugepagesz=2M hugepagesz=2M hugepages=512

    避免运行时再申请。

  3. 监控碎片指标
    Prometheus 脚本采集

    node_exporter --collector.buddyinfo

    Grafana 告警:高阶 9~10 为 0 持续 5 min → 报警。

  4. 双缓冲策略
    视频/工业相机 “一帧写入,一帧读取” 永远交替,彻底避免运行时重新申请

  5. 限制规整延迟

    echo 3 > /proc/sys/vm/extfrag_threshold

    提高“ unwillingness”阈值,降低后台 compact 频率,牺牲少量内存换时间

  6. CI 自动化测试
    每次提交自动跑 stress-ng 600 s + check buddyinfo,若 2 MB 连续块 <10 则标记失败。


七、总结:一张脑图带走全部要点

实时内存防碎片
├─ 观:buddyinfo / slabtop
├─ 池:kmem_cache + mempool_create
├─ 巨:hugepage (2M/1G) + mmap MAP_HUGETLB
├─ 量:compact 耗时 <1 ms 目标
└─ 控:Prometheus 告警 + 开机预留

应用场景

  • 工业机器人 250 μs 控制环

  • 车载 ECU ADAS 摄像头 30 fps 零拷贝

  • 金融行情撮合 100 μs 延迟预算

把“提前池化 + 大页锁定 + 监控告警”三板斧带入你的下一个实时项目,
让内存碎片从此 看不见 = 不存在

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐