4、CPU负载排查:高CPU占用排查方法、线程优先级调整、任务调度优化

好,咱们接着聊。帧率不稳,很多时候不是GPU不够快,而是CPU在拖后腿。我见过太多项目,大家拼命优化渲染管线,结果发现罪魁祸首是某个后台线程在疯狂抢CPU。这一章,我们就来彻底排查CPU负载问题。

4.1 高CPU占用排查方法

排查高CPU占用,说白了就是找到「谁在偷走CPU时间」。我个人习惯用三步走:

  1. 全局观察:先看整体CPU占用率,确认是不是真的高。
  2. 进程定位:找到是哪个进程在吃CPU。
  3. 线程级分析:深入到线程,看哪个线程最活跃。

在嵌入式Linux系统上,我常用的工具是 topperf。嗯,这里有个小技巧:

关键命令示例:

# 实时查看CPU占用最高的线程
top -H -p $(pidof your_app)

# 用perf采样,看热点函数
perf top -p $(pidof your_app) -K

为什么用 -H 参数?因为默认的 top 只显示进程级,而我们的仪表盘渲染往往是多线程的。我曾经在一个项目中,发现主线程CPU占用只有20%,但一个负责数据采集的线程占到了60%。如果不看线程级,你根本找不到问题。

避坑指南: 我曾经在排查时,发现CPU占用忽高忽低,以为是代码问题。后来才发现是系统电源管理策略在作祟——CPU频率在动态调整。记得先锁频再排查,否则数据不准确。

4.2 线程优先级调整

找到高占用的线程后,下一步就是调整优先级。你想想看,仪表盘的渲染线程和后台日志线程抢CPU,这合理吗?当然不合理。

在Linux中,线程优先级分为两种:

  • 普通优先级(Nice值):范围 -20 到 19,数值越小优先级越高。
  • 实时优先级(RT优先级):范围 0 到 99,数值越大优先级越高。

对于仪表盘这种对实时性要求高的任务,我建议使用实时优先级。但要注意,实时优先级用不好会出大问题。

警告: 实时优先级线程如果进入死循环,整个系统都会卡死。我曾经在调试时,把一个计算线程设成了实时优先级99,结果一个bug导致它死循环,系统直接「冻住」了。嗯,从那以后,我养成了一个习惯:实时优先级线程必须加看门狗。

调整优先级的代码示例:

#include <pthread.h>
#include <sched.h>

void set_realtime_priority(pthread_t thread, int priority) {
    struct sched_param param;
    param.sched_priority = priority;
    
    // 设置为FIFO实时调度策略
    int ret = pthread_setschedparam(thread, SCHED_FIFO, &param);
    if (ret != 0) {
        // 需要CAP_SYS_NICE权限
        printf("设置实时优先级失败: %d\n", ret);
    }
}

// 使用示例:将渲染线程设为实时优先级80
set_realtime_priority(render_thread, 80);

这里要注意,设置实时优先级需要 CAP_SYS_NICE 权限。在产品中,我通常会在init脚本里给进程授权,而不是在代码里用setcap。

4.3 任务调度优化

调整完优先级,我们还要优化任务调度本身。说白了,就是让CPU在正确的时间做正确的事。

我总结了几条实用经验:

优化方向 具体做法 我的经验
减少调度次数 合并小任务,减少线程切换 曾经把10个1ms的定时器合并成1个10ms的,CPU占用降了15%
绑定CPU核心 将渲染线程绑定到特定核心 避免缓存抖动,帧率稳定性提升明显
使用DMA 大数据传输用DMA,不占CPU 帧缓冲更新用DMA后,CPU占用从30%降到5%
避免忙等待 用条件变量或信号量替代轮询 这是最常见的坑,很多人习惯用while循环等数据

关于CPU绑定,代码实现很简单:

#define CORE_RENDER 2  // 将渲染线程绑定到核心2

void bind_to_core(pthread_t thread, int core_id) {
    cpu_set_t cpuset;
    CPU_ZERO(&cpuset);
    CPU_SET(core_id, &cpuset);
    
    int ret = pthread_setaffinity_np(thread, sizeof(cpu_set_t), &cpuset);
    if (ret != 0) {
        printf("绑定CPU核心失败: %d\n", ret);
    }
}

为什么绑定核心有用?你想想看,线程在不同核心间切换时,缓存会失效。对于仪表盘这种频繁访问显存和纹理数据的任务,缓存命中率直接影响性能。我习惯把渲染线程和它的数据放在同一个核心上,这样L1/L2缓存都是热的。

调度优化的核心原则:

  • 实时任务(渲染、输入响应)→ 高优先级 + 固定核心
  • 非实时任务(日志、网络同步)→ 低优先级 + 任意核心
  • 周期性任务(数据采集)→ 用timerfd,不要自己写定时器

最后说一个我踩过的坑。有一次,我发现帧率每隔几秒就会掉一次,查了很久才发现是系统的cgroup限制了CPU时间片。嗯,如果你用了容器或者cgroup,记得检查一下配额设置。有时候问题不在你的代码里,而在系统配置里。

总结一下这一章:排查CPU负载,先看线程级占用,再调优先级,最后优化调度策略。这三步走下来,大部分CPU导致的帧率问题都能解决。下一章,我们会聊内存带宽对帧率的影响,那个坑更深,做好准备。