一、问题背景:系统卡顿、UI 响应慢
在一次 i.MX8MP 开发板上进行的 UI 效果游玩中,我们发现热链接点应用手势操作后,整个系统的输入有明显卡顿(性能急剧降低)。表现为:
- 点击或滑动时,UI 页面延迟数百毫秒才响应;
- top 查看 CPU 使用率处于空闲,未发现明显占用瓶颈;
- IO 读写、GPU 渲染无明显峰值负载。
因此,初步判断系统卡顿原因不在于 CPU 或 IO,可能与 内存系统管理机制有关,尤其是低延迟需求下频繁触发的 动态分配 问题。
二、应用层分析:调用 malloc 频繁
利用 strace 进行系统调用监控:
strace -ttT -e trace=mmap,brk,munmap ./my_app
观察结果:
- UI 操作时应用进程不断 mmap/munmap 大量匿名页;
- 有频繁 malloc/free 调用循环;
- 某些接口中使用了大块临时内存。
进一步用 smem 分析最大应用内存使用:
smem -r -k | sort -nk 2 | tail
结果显示:
- 匿名页占用居高不下(Anonymous);
- 应用分配速度远超释放速度,导致系统压力加重;
- slab/cache 逐渐增长,存在内核小对象频繁申请迹象。
三、Page Fault 分析:重点分析匿名页分配
通过 perf 分析 page-fault 事件:
perf record -e page-faults -g ./my_app
perf report
确认:
- 每次 UI 操作均触发大量 page fault;
- 跳转路径多指向 do_anonymous_page。
使用 ftrace 验证内核堆栈路径:
echo function > /sys/kernel/debug/tracing/current_tracer
echo do_page_fault > /sys/kernel/debug/tracing/set_ftrace_filter
echo 1 > /sys/kernel/debug/tracing/tracing_on
sleep 3
echo 0 > /sys/kernel/debug/tracing/tracing_on
cat /sys/kernel/debug/tracing/trace
确认路径为:
do_page_fault()
→ handle_mm_fault()
→ __handle_mm_fault()
→ do_anonymous_page()
→ alloc_zeroed_user_highpage()
→ alloc_page()
→ __alloc_pages_nodemask()
四、内核分配机制梳理:buddy 与 slab 的联系
4.1 页分配器
最终负责分配页面的是 buddy allocator,接口核心:
struct page *__alloc_pages_nodemask(gfp_t gfp_mask, unsigned int order,
int preferred_nid, nodemask_t *nodemask);
按 order 分配物理连续页。
4.2 slab/slub 分配器
用于管理小对象,如:task_struct、inode、dentry 等。
调用路径:
obj = kmem_cache_alloc(cachep, GFP_KERNEL);
其底层实际也从页分配器中获取整页,构建小对象缓存:
slab_alloc_node() → __do_cache_alloc() → alloc_pages()
五、slabinfo + meminfo 联合分析:
查看内核 slab 分配信息:
cat /proc/slabinfo | grep -v " 0 0"
查看内存使用总览:
cat /proc/meminfo | grep -E "Slab|MemFree|MemAvailable"
示例:
Slab: 52892 kB
SReclaimable: 21244 kB
SUnreclaim: 31648 kB
slab 占用了约 52MB,其中不可回收部分 31MB,属于偏高但尚不致命。
六、系统参数调优
通过调整 vm 参数减缓分配频率:
echo 16384 > /proc/sys/vm/min_free_kbytes
echo 20 > /proc/sys/vm/swappiness
含义:
- 提高最小空闲页保留值,防止低水位频繁触发分配;
- 降低系统对 swap 的偏好,延缓系统清页行为。
七、源代码分析与优化:减少频繁 malloc
原始代码:
char *buf = malloc(1024 * 1024);
memset(buf, 0, 1024 * 1024);
free(buf);
每次调用分配大块内存 → 触发 Page Fault → 回收前还保留在 inactive list 中。
优化方案:
static char *global_buf;
if (!global_buf)
global_buf = malloc(1024 * 1024);
memset(global_buf, 0, 1024 * 1024);
避免频繁分配释放,通过复用全局 buffer 缓解 page fault 压力。
八、优化效果验证
- 使用 perf 重新采样,page fault 降低超 60%;
- UI 卡顿时间降低至原先的三分之一;
- slabinfo 观察 slab 内存趋于稳定,未出现增长。
九、进阶扩展:分析自己内核中的 slab 缓存分配
创建自定义 slab 类型,便于排查内存泄漏或优化热点对象:
static struct kmem_cache *my_cache;
my_cache = kmem_cache_create("my_obj", sizeof(struct my_obj), 0,
SLAB_HWCACHE_ALIGN, NULL);
obj = kmem_cache_alloc(my_cache, GFP_KERNEL);
...
kmem_cache_free(my_cache, obj);
后续即可通过:
cat /proc/slabinfo | grep my_obj
定位自定义 slab 类型,结合 kmemleak 分析更具系统性。
十、结语:内存分配带来的卡顿问题不容忽视
- UI 卡顿不一定是 CPU 不够,Page Fault 更可能是根源;
- 应用层频繁 malloc/free 会在系统产生频繁页帧分配及回收;
- 结合 perf、ftrace、slabinfo 多维度分析内核行为;
- kmem_cache 与 alloc_pages 虽分属 slab/buddy,但密切协作,共同完成内存管理;
- 面对用户交互类场景,应尽可能复用内存、控制频率,避免每次输入都触发一次 Page Fault。
观看更多规范化 Linux 性能调优和高性能开发系列,请关注 B 站:「嵌入式 Jerry」