【实时 Linux 实战系列】实时任务的内存碎片管理与优化
一、简介:为什么内存碎片会“杀死”实时性?
-
硬实时场景(工业机器人、车载 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 分钟搭好“实时沙箱”
-
硬件
-
任意 x86_64 多核机(≥4 核 8 GB)
-
支持 虚拟化 即可,无需昂贵板卡。
-
-
软件
-
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 -
-
启用 RT 抢占模式(可选,真实硬实时需开)
sudo apt install -y linux-image-$(uname -r)-rt sudo grub-set-default 0 && sudo reboot # 重启后 uname -v | grep PREEMPT_RT -
实验目录
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 且禁止睡眠 |
六、实践建议与最佳实践
-
“池化”优先
实时路径永不裸用 kmalloc/malloc,一律 mempool / slab / hugepage。 -
预留大页开机即配
/etc/default/grub追加default_hugepagesz=2M hugepagesz=2M hugepages=512避免运行时再申请。
-
监控碎片指标
Prometheus 脚本采集node_exporter --collector.buddyinfoGrafana 告警:高阶 9~10 为 0 持续 5 min → 报警。
-
双缓冲策略
视频/工业相机 “一帧写入,一帧读取” 永远交替,彻底避免运行时重新申请。 -
限制规整延迟
echo 3 > /proc/sys/vm/extfrag_threshold提高“ unwillingness”阈值,降低后台 compact 频率,牺牲少量内存换时间。
-
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 延迟预算
把“提前池化 + 大页锁定 + 监控告警”三板斧带入你的下一个实时项目,
让内存碎片从此 看不见 = 不存在!
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)