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");
}
}
4.2 中断负载均衡:别让一个核累死
中断来了,谁去处理?默认情况下,很多中断都往CPU 0上扔。CPU 0既要跑内核线程,又要处理中断,还要调度用户态进程——它不卡谁卡?
中断负载均衡,就是把中断请求分散到多个CPU核心上。Android系统里常用irqbalance这个守护进程来做这件事。
4.2.1 irqbalance的工作原理
irqbalance会周期性地检查各CPU的中断处理量,然后把中断动态迁移到负载较低的核上。但说实话,自动迁移不一定适合所有HAL场景。
举个例子:你有一个HAL模块依赖某个硬件设备的中断,比如GPS的IRQ。如果irqbalance把这个中断从核3迁移到核1,而你的HAL线程绑在核3上——完蛋,跨核通信的开销就来了。
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, ¶m) != 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。
4.5 整体架构:CPU调度与中断管理的关系
下面这张图,是我自己总结的HAL线程与CPU、中断之间的协作关系。你看完应该能明白,为什么这三件事要一起调。
4.6 实战调优步骤总结
- 分析HAL线程的CPU消耗模式——用perf top或者systrace看哪个核负载高
- 绑定HAL线程到指定CPU核心——避开中断密集的核,优先绑到L2/L3缓存独立的核
- 检查中断分布——cat /proc/interrupts,把关键设备中断固定到HAL线程不在的核上
- 设置合理的线程优先级——RT 50-80之间,别贪心设到99
- 配置cgroup做资源兜底——设置cpu.shares和cpu.cfs_quota_us,防止HAL线程失控
- 验证效果——用cyclictest或者自定义延迟测试工具,对比调优前后的延迟分布
好了,CPU调度和中断这块就聊到这儿。记住一个原则:让HAL线程待在该待的核上,让中断别来打扰它,再给它足够的优先级和资源保障。这四板斧下去,大部分性能问题都能解决。