一、ksamplingd 线程概述
在内核中新建 ksamplingd 用于采样硬件性能事件的线程:
- 从 Perf Events 的 ring buffer 里读取样本
- 处理样本数据
- 根据自身实际 CPU 时间占用率,动态调整采样频率
这篇文章的介绍将略过采样和处理样本的部分,因为采样在之前博客里详细分析过。这里主要调整的参数是struct perf_event_attr sample_period
二、线程初始化
1. CPU 线程给定
const struct cpumask *cpumask = cpumask_of_node(0);
if (!cpumask_empty(cpumask))
do_set_cpus_allowed(access_sampling, cpumask);
cpumask_of_node(0)
获取节点 0 所有 CPU 的 mask。- 通过
do_set_cpus_allowed()
把采样线程给定运行在某些 CPU 上。看需求,因为在笔者的场景中,只关注 socket0 的冷数据和热数据,而socket0 虽然 NUMA 有两个,实际物理核心只是 NUMA0 的。
2. 时间变量初始化
trace_runtime = total_runtime = exec_runtime = t->se.sum_exec_runtime;
trace_cputime = total_cputime = elapsed_cputime = jiffies;
sleep_timeout = usecs_to_jiffies(2000);
t->se.sum_exec_runtime
:是内核调度实体task_struct
的struct sched_entity
中记录该进程历史上总共运行的 CPU 耗时(单位 ns)。原本用于计算 CFS 调度器中优先级和时间分配。jiffies
:内核时钟计数器sleep_timeout
:指定最短采样间隔
三、CPU 占用率计算
1. 时间差值
exec_runtime = t->se.sum_exec_runtime - exec_runtime; // ns
elapsed_cputime = jiffies_to_usecs(cur - elapsed_cputime); // us
exec_runtime
:本次周期内采样线程耗时elapsed_cputime
:本次周期内当前增量时间
2. 占用率计算
需要单位符合:把 ns -> us
u64 cur_cputime = div64_u64(exec_runtime / 1000, elapsed_cputime); // permil
最终得到 cputime
:
- 单位是 permil (% 的 10倍)
- 如果
cputime = 300
,即 CPU 占用 30%
四、动态调整采样频率
1. 调整规则
关键参数:ksampled_soft_cpu_quota
目标 CPU 占用率上限(permil,千分比)cputime
实际测得的 CPU 时间占用率sample_period
当前 LLC 采样周期(控制采样间隔)sample_inst_period
当前指令采样周期pcount
宏定义的上限
增加采样周期(降低采样频率):
当 cputime
超过目标占用率 ksampled_soft_cpu_quota + 5
(防止频繁调整),且 sample_period
没有达到上限 pcount
,调用 increase_sample_period
使采样周期增加,从而降低采样频率。如果周期有变化,应用新的采样周期。
减少采样周期(提高采样频率):
当 cputime
低于目标占用率 ksampled_soft_cpu_quota - 5
,并且 sample_period
大于 pinstcount
,可以调用 decrease_sample_period
减小采样周期,增加采样频率。同样如果采样周期发生变化,需要更新 pebs 采样器的配置。
2. 调整函数实现细节
- 线性调整:每次增加或减少采样周期,只调整一个单位。确保不会超过预定义的上下限
pcount
和pinstcount
。响应慢但足够稳定。 - 通过查表(预先定义一个已经排序的数组,数组中每个值表示周期)进行非线性调整,适合对不同采样周期有特殊设计的应用场景。
五、线程休眠机制
schedule_timeout_interruptible(sleep_timeout);
- 休眠控制线程激活频率
- 配合采样周期,避免忙等和超时间