4. CPU调度与中断:CPU亲和性设置、中断负载均衡、HAL线程优先级与cgroup配置

各位做HAL的兄弟,咱们今天聊点硬核的。CPU调度和中断管理,说白了就是让系统知道“谁该在哪个核上干活”。我见过太多HAL模块性能上不去,不是因为算法不行,而是线程跑错了核,或者中断把CPU时间全吃掉了。

嗯,这一章咱们就掰开揉碎,把CPU亲和性、中断均衡、线程优先级和cgroup这四件事讲透。

4.1 CPU亲和性:把线程钉在正确的核上

CPU亲和性,英文叫CPU affinity。它的作用很简单——告诉内核:这个线程/进程,只许在指定的CPU核心上运行。

为什么要这么做?你想想看,如果HAL的音频处理线程在四个核上乱跳,L1/L2缓存频繁失效,性能能好吗?我当年调一个Camera HAL的预览卡顿问题,就是发现ISP处理线程每几毫秒就被调度到另一个核上,缓存全废了。设了亲和性之后,帧率直接提升了15%。

4.1.1 设置亲和性的两种方式

方式 接口/命令 适用场景
命令行 taskset -cp <mask> <pid> 调试阶段快速验证
代码调用 sched_setaffinity() HAL模块正式代码

mask是位掩码。比如4核CPU,想绑到核0和核2,mask就是二进制0101,即0x05。

// C代码示例:将当前线程绑定到CPU 0和CPU 2
#include <sched.h>
#include <stdio.h>

void set_hal_thread_affinity() {
    cpu_set_t cpuset;
    CPU_ZERO(&cpuset);
    CPU_SET(0, &cpuset);   // 核0
    CPU_SET(2, &cpuset);   // 核2

    pid_t tid = gettid();
    if (sched_setaffinity(tid, sizeof(cpuset), &cpuset) != 0) {
        perror("sched_setaffinity failed");
    } else {
        printf("HAL thread pinned to CPU 0 and 2\n");
    }
}
我的习惯:在HAL的init函数里就设好亲和性,别等到跑起来再设。我曾经踩过一个坑——线程创建后默认跑在大核上,等业务压力上来再设亲和性,中间那几毫秒的乱跳已经造成了性能抖动。

4.2 中断负载均衡:别让一个核累死

中断来了,谁去处理?默认情况下,很多中断都往CPU 0上扔。CPU 0既要跑内核线程,又要处理中断,还要调度用户态进程——它不卡谁卡?

中断负载均衡,就是把中断请求分散到多个CPU核心上。Android系统里常用irqbalance这个守护进程来做这件事。

4.2.1 irqbalance的工作原理

irqbalance会周期性地检查各CPU的中断处理量,然后把中断动态迁移到负载较低的核上。但说实话,自动迁移不一定适合所有HAL场景。

举个例子:你有一个HAL模块依赖某个硬件设备的中断,比如GPS的IRQ。如果irqbalance把这个中断从核3迁移到核1,而你的HAL线程绑在核3上——完蛋,跨核通信的开销就来了。

我曾经踩过的坑:某款车载平台的GNSS HAL,定位数据总是延迟。查了两天发现是irqbalance把GNSS中断从大核迁到了小核,而HAL线程绑在大核上。中断处理完要跨核发信号给HAL线程,延迟多了几百微秒。解决方案:把GNSS IRQ手动固定到HAL线程所在的核上。

4.2.2 手动绑定中断到指定CPU

查看中断号:

cat /proc/interrupts | grep <device_name>

绑定中断到指定CPU(以中断号142为例,绑定到核2):

echo 2 > /proc/irq/142/smp_affinity

注意:smp_affinity的值也是位掩码。核2对应二进制0100,即4。但这里写的是CPU索引,不是掩码值?嗯,不同内核版本有差异,稳妥做法是查内核文档。

CPU核心 位掩码 smp_affinity值
核0 0001 1
核1 0010 2
核2 0100 4
核3 1000 8

4.3 HAL线程优先级:别让你的线程被“饿死”

优先级这事,说白了就是告诉调度器:我这个线程比较急,先伺候我。Android里HAL线程一般跑在实时优先级(RT)或者高优先级(如99、98)。

但这里有个大坑——优先级设太高,可能会把系统关键服务(比如surfaceflinger、audioserver)给堵住。我见过一个案子,某HAL模块把线程优先级设到RT 99,结果UI掉帧了。为什么?因为surfaceflinger抢不过它。

4.3.1 设置优先级的正确姿势

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

void set_hal_thread_priority(pthread_t thread, int priority) {
    struct sched_param param;
    param.sched_priority = priority;
    
    // 使用SCHED_FIFO实时调度策略
    if (pthread_setschedparam(thread, SCHED_FIFO, &param) != 0) {
        perror("pthread_setschedparam failed");
    }
}

// 调用示例:设置优先级为80
set_hal_thread_priority(pthread_self(), 80);
优先级参考范围:
  • RT优先级 1-50:普通HAL线程(如传感器、输入)
  • RT优先级 51-80:中等实时HAL线程(如音频、GPS)
  • RT优先级 81-99:高实时HAL线程(如相机ISP、显示)

我个人建议:非必要不上90以上,给系统留点余地。

4.4 cgroup配置:给HAL线程“划地盘”

cgroup(control group)是Linux内核提供的资源隔离机制。Android从早期版本就开始用cgroup来管理CPU、内存、IO等资源。

对于HAL来说,最常用的是cpu cgroup。你可以把HAL线程放到一个专门的cgroup里,限制它最多能用多少CPU时间,或者保证它最少能拿到多少CPU时间。

4.4.1 创建HAL专用的cgroup

# 创建cgroup目录
mkdir -p /sys/fs/cgroup/cpu/hal_group

# 设置CPU份额(相对权重,默认1024)
echo 512 > /sys/fs/cgroup/cpu/hal_group/cpu.shares

# 设置CPU上限(单位:微秒,100000 = 100ms)
echo 50000 > /sys/fs/cgroup/cpu/hal_group/cpu.cfs_quota_us
echo 100000 > /sys/fs/cgroup/cpu/hal_group/cpu.cfs_period_us

# 把HAL线程PID加入cgroup
echo <hal_thread_pid> > /sys/fs/cgroup/cpu/hal_group/cgroup.procs

这里解释一下:cpu.shares是相对权重,值越大分到的CPU时间越多。cpu.cfs_quota_us和cpu.cfs_period_us配合使用,限制在每100ms周期内最多跑50ms。

注意:cgroup的配置在系统重启后会丢失。你需要在init.rc或者HAL的启动脚本里做持久化。我习惯在HAL的service定义里加上onrestart或者onboot触发器来重新配置cgroup。

4.5 整体架构:CPU调度与中断管理的关系

下面这张图,是我自己总结的HAL线程与CPU、中断之间的协作关系。你看完应该能明白,为什么这三件事要一起调。

HAL CPU调度与中断管理架构 HAL线程 CPU亲和性设置 sched_setaffinity 中断负载均衡 irqbalance / smp_affinity 线程优先级 SCHED_FIFO / RT cgroup资源隔离 稳定、低延迟的HAL性能 核心思路:先绑核(亲和性),再管中断(均衡),然后设优先级,最后用cgroup兜底 四步联动,缺一不可

4.6 实战调优步骤总结

  1. 分析HAL线程的CPU消耗模式——用perf top或者systrace看哪个核负载高
  2. 绑定HAL线程到指定CPU核心——避开中断密集的核,优先绑到L2/L3缓存独立的核
  3. 检查中断分布——cat /proc/interrupts,把关键设备中断固定到HAL线程不在的核上
  4. 设置合理的线程优先级——RT 50-80之间,别贪心设到99
  5. 配置cgroup做资源兜底——设置cpu.shares和cpu.cfs_quota_us,防止HAL线程失控
  6. 验证效果——用cyclictest或者自定义延迟测试工具,对比调优前后的延迟分布
我的经验:调优不是一次性的。每次系统版本升级、内核更新,都要重新检查一遍中断分布和cgroup配置。我曾经在Android 12升级到13后,发现irqbalance的行为变了,之前的手动绑定全部失效。嗯,从那以后我养成了每次OTA后都跑一遍自动化检查脚本的习惯。

好了,CPU调度和中断这块就聊到这儿。记住一个原则:让HAL线程待在该待的核上,让中断别来打扰它,再给它足够的优先级和资源保障。这四板斧下去,大部分性能问题都能解决。