第4章 CPU绑定与亲和性设置

4.1 原理:为什么需要CPU亲和性

Android设备普遍采用大小核架构(如ARM big.LITTLE),大核(高性能核心)与小核(低功耗核心)在频率、缓存、功耗上差异显著。Framework层中,若关键的任务绑定到小核,会导致响应延迟上升、丢帧、ANR风险增加;若后台计算密集型任务绑定到大核,则浪费功耗。CPU亲和性(affinity)通过系统调用sched_setaffinity将指定进程/线程绑定到特定CPU核心或核心组,从而精确控制调度目标。

核心类型典型架构频率范围适用场景
大核 (Performance)Cortex-A76 / X1 / X22.0~3.2 GHzUI渲染、触摸响应、音视频解码线程
小核 (Efficiency)Cortex-A55 / A5100.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。常见策略如下:

  1. UI主线程:固定绑定到最高频大核(如CPU4-7),避免被调度器迁移到小核。
  2. RenderThread:绑定到大核群组,与主线程同一cluster减少L2 cache miss。
  3. Binder线程池:可绑定到小核+大核混合,根据线程优先级区分。
  4. 后台GC线程:强制绑定到小核,减少对交互线程的CPU资源争抢。
  5. 媒体编解码线程:绑定到中核(若有)或大核,保证帧率稳定性。
实践建议:不要在生产代码中硬编码核心编号,应运行时读取核心类型。例如读取/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.4ms8.2ms55%
Choreographer帧调度16.7ms7.3ms56%
音频播放缓冲区填充4.1ms1.8ms56%

4.8 绑定决策流程

重要:绑定不能滥用。下列场景建议保持默认调度:短时轻量任务、睡眠等待线程、定时器回调。绑定只适用于持续活跃且对延迟敏感的线程。
CPU亲和性绑定决策流程 识别目标线程(UI/渲染/GC...) 是否延迟敏感? 否 保持默认调度 是 绑定大核 sched_setaffinity 读取cpu_capacity 识别大核cluster编号 执行绑定,并验证Cpus_allowed 性能采样对比 迭代优化 总结:先诊断延迟敏感度 → 读取CPU拓扑 → 生成掩码 → 绑定 → 验证。实际项目中需结合cpuset和热插拔策略综合设计。

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适配。