第4章 CPU绑定与亲和性设置
4.1 原理:为什么需要CPU亲和性
Android设备普遍采用大小核架构(如ARM big.LITTLE),大核(高性能核心)与小核(低功耗核心)在频率、缓存、功耗上差异显著。Framework层中,若关键的任务绑定到小核,会导致响应延迟上升、丢帧、ANR风险增加;若后台计算密集型任务绑定到大核,则浪费功耗。CPU亲和性(affinity)通过系统调用sched_setaffinity将指定进程/线程绑定到特定CPU核心或核心组,从而精确控制调度目标。
| 核心类型 | 典型架构 | 频率范围 | 适用场景 |
|---|---|---|---|
| 大核 (Performance) | Cortex-A76 / X1 / X2 | 2.0~3.2 GHz | UI渲染、触摸响应、音视频解码线程 |
| 小核 (Efficiency) | Cortex-A55 / A510 | 0.6~1.8 GHz | 后台服务、垃圾回收、网络IO线程 |
4.2 系统调用接口:sched_setaffinity
原型定义在<sched.h>中,用户空间操作cpu_set_t位图。内核根据掩码将线程放置到允许的核心上。
#include <sched.h>
// 获取当前CPU数量
int nprocs = sysconf(_SC_NPROCESSORS_CONF);
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
// 绑定到CPU 0和CPU 1(小核示例)
CPU_SET(0, &cpuset);
CPU_SET(1, &cpuset);
// pid=0 表示当前调用线程
int ret = sched_setaffinity(0, sizeof(cpuset), &cpuset);
if (ret != 0) {
perror("sched_setaffinity failed");
}
4.3 绑定大核/小核策略
在实际Framework开发中,常通过/sys/devices/system/cpu/下节点识别大核与小核编号,然后针对性调用affinity。常见策略如下:
- UI主线程:固定绑定到最高频大核(如CPU4-7),避免被调度器迁移到小核。
- RenderThread:绑定到大核群组,与主线程同一cluster减少L2 cache miss。
- Binder线程池:可绑定到小核+大核混合,根据线程优先级区分。
- 后台GC线程:强制绑定到小核,减少对交互线程的CPU资源争抢。
- 媒体编解码线程:绑定到中核(若有)或大核,保证帧率稳定性。
实践建议:不要在生产代码中硬编码核心编号,应运行时读取核心类型。例如读取
/sys/devices/system/cpu/cpu*/topology/cluster_id或/sys/devices/system/cpu/cpu*/cpu_capacity区分大核。
4.4 完整实战代码示例(C++层)
#include <stdio.h>
#include <sched.h>
#include <unistd.h>
// 读取 /sys/devices/system/cpu 获取大核掩码(简化版,实际需解析cpu_capacity)
int get_big_core_mask(cpu_set_t *mask) {
CPU_ZERO(mask);
// 根据实际设备配置,例如高通骁龙8 Gen 3的大核为cpu6-7
// 此处演示直接设置
for (int i = 6; i <= 7; i++) {
CPU_SET(i, mask);
}
return 0;
}
void bind_to_big_cores() {
cpu_set_t cpuset;
if (get_big_core_mask(&cpuset) != 0) return;
if (sched_setaffinity(0, sizeof(cpuset), &cpuset) != 0) {
fprintf(stderr, "bind_to_big_cores failed\n");
}
}
void bind_to_little_cores() {
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
for (int i = 0; i <= 1; i++) {
CPU_SET(i, &cpuset);
}
sched_setaffinity(0, sizeof(cpuset), &cpuset);
}
4.5 调试与验证
运行时可查看当前线程的affinity掩码:
# 查看进程/线程的CPU亲和性(需要root或debug权限)
cat /proc/<PID>/status | grep Cpus_allowed
# 输出示例: Cpus_allowed: f0 // 表示绑定到cpu4-7 (二进制11110000)
# 使用 taskset 实时修改
taskset -p 0xf0 <PID> # 绑定到cpu0-3
taskset -p -c 6-7 <PID> # 绑定到cpu6-7
性能验证:使用perf统计调度器迁移次数:
perf stat -e sched:sched_move_numa,sched:sched_switch,sched:sched_migrate_task -p <PID>。绑定后迁移次数应显著下降。
4.6 常见错误与避坑
- 掩码越界:设置超过系统实际CPU数量的位,系统调用返回EINVAL。务必先获取
sysconf(_SC_NPROCESSORS_CONF)。 - 权限不足:普通应用无法修改自身affinity(Android平台上需要CAP_SYS_NICE或root)。Framework系统服务需以system进程运行。
- 大核失效:部分设备在低电量或高温时会热拔大核,affinity设置会自动降级到可用核心。
- 多线程场景:只设置主线程affinity不保证子线程inheritance,需在每个线程入口处单独调用。
- 与cpuset冲突:若系统已通过cpuset限制了进程可用的核心范围,sched_setaffinity只能在cpuset的子集内生效。需同时检查
/dev/cpuset配置。
4.7 性能评估数据(典型场景)
| 场景 | 未绑定(平均延迟) | 绑定大核(平均延迟) | 提升比例 |
|---|---|---|---|
| 触摸事件响应(10次滑动) | 18.4ms | 8.2ms | 55% |
| Choreographer帧调度 | 16.7ms | 7.3ms | 56% |
| 音频播放缓冲区填充 | 4.1ms | 1.8ms | 56% |
4.8 绑定决策流程
重要:绑定不能滥用。下列场景建议保持默认调度:短时轻量任务、睡眠等待线程、定时器回调。绑定只适用于持续活跃且对延迟敏感的线程。
4.9 部署与集成注意事项
在Android AOSP中,Framework层的affinity设置通常放在以下位置:
- SurfaceFlinger:在
onFirstRef或threadLoop中绑定主线程到大核。 - InputReader:在
InputReaderThread构造函数内调用绑定。 - Choreographer:Framework内部通过
android.os.Process.setThreadPriorityAndGroup间接影响,但显式affinity需自行添加。 - 第三方HAL层服务:推荐在
init.rc中使用writepid /dev/cpuset/top-app/tasks配合cpuset,而非直接syscall。
建议将核心编号配置化,放入/vendor/etc/cpuset_config.xml,便于不同SoC适配。