通过性能指标学习Linux Kernel - (下)
作者介绍:
赵晨雨,师从陈莉君教授,Linux内核之旅社区maintainer,现就职于thoughtworks 安全与系统研发事业部,thoughtworks未济实验室成员。
B站录屏链接:
GLCC编程夏令营——LMP课题 周会分享
https://www.bilibili.com/video/BV1oY4y177Wh?spm_id_from=333.337.search-card.all.click
上期回顾:
上期我们介绍了atop和proc统计调度延迟的原理,内核还存在很多的基础设施,这些基础设施都是强有⼒的⼯具,我们最终是要落地到 eBPF 中的,在 eBPF 中我个⼈认为关键事件是很关键的⼀环,因为eBPF太精准了,⽽它的精准是精准在内核中各个事件上。
3. tracepoint
tracepoint是linux kernel中的静态探针,是内核中天然的关键事件集合,这些静态探针点是在linux kernel代码中硬编码的,并且范围也非常广,大约有1800多个事件点,例如系统调用的入口和出口、调度事件、文件系统操作等等,有一个非常好的优点是接口稳定。
查看系统中可以使用的tracepoint有关于调度的事件:
$ sudo perf list tracepoint | grep sched:sched:sched_kthread_stop [Tracepoint event]
sched:sched_kthread_stop_ret [Tracepoint event]
sched:sched_kthread_work_execute_end [Tracepoint event]
sched:sched_kthread_work_execute_start [Tracepoint event]
sched:sched_kthread_work_queue_work [Tracepoint event]
sched:sched_migrate_task [Tracepoint event]
sched:sched_move_numa [Tracepoint event]
sched:sched_pi_setprio [Tracepoint event]
sched:sched_process_exec [Tracepoint event]
sched:sched_process_exit [Tracepoint event]
sched:sched_process_fork [Tracepoint event]
sched:sched_process_free [Tracepoint event]
sched:sched_process_hang [Tracepoint event]
sched:sched_process_wait [Tracepoint event]
sched:sched_stat_blocked [Tracepoint event]
sched:sched_stat_iowait [Tracepoint event]
sched:sched_stat_runtime [Tracepoint event]
sched:sched_stat_sleep [Tracepoint event]
sched:sched_stat_wait [Tracepoint event]
sched:sched_stick_numa [Tracepoint event]
sched:sched_swap_numa [Tracepoint event]
sched:sched_switch [Tracepoint event]
sched:sched_wait_task [Tracepoint event]
sched:sched_wake_idle_without_ipi [Tracepoint event]
sched:sched_wakeup [Tracepoint event]
sched:sched_wakeup_new [Tracepoint event]
sched:sched_waking [Tracepoint event]
使用perf工具可以直接查看系统中当前这些事件发生时产生的数据:
$ sudo perf trace -e sched:sched_wakeuptracepoint是hard coded在Linux kernel代码中的,可以在内核中看一眼:
/*
* Mark the task runnable and perform wakeup-preemption.
*/
static void ttwu_do_wakeup(struct rq *rq, struct task_struct *p, int wake_flags,
struct rq_flags *rf)
{
check_preempt_curr(rq, p, wake_flags);
p->state = TASK_RUNNING;
trace_sched_wakeup(p);
#ifdef CONFIG_SMP
if (p->sched_class->task_woken) {
/*
第9行的代码就是tracepoint事件点,对应
sched:sched_wakeup
查看格式化输出的事件内容:
$ sudo cat /sys/kernel/debug/tracing/events/sched/sched_wakeup/formatname: sched_wakeup
ID: 318
format:
field:unsigned short common_type; offset:0; size:2; signed:0;field:unsigned char common_flags; offset:2; size:1; signed:0;
field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
field:int common_pid; offset:4; size:4; signed:1;
field:char comm[16]; offset:8; size:16; signed:1;
field:pid_t pid; offset:24; size:4; signed:1;
field:int prio; offset:28; size:4; signed:1;
field:int success; offset:32; size:4; signed:1;
field:int target_cpu; offset:36; size:4; signed:1;
print fmt: "comm=%s pid=%d prio=%d target_cpu=%03d", REC->comm, REC->pid, REC->prio, REC->target_cpu
这些数据输出还有一个作用,就是可以对这些内容执行tracepoint追踪过滤:
$ sudo perf trace -e sched:sched_wakeup --filter 'pid == 6079'这样就可以只看到内核中pid是6079的task的task_wakeup事件了。
所以可以发现,内核内置的tracepoint事件点已经是非常精确的定位了,并且替我们选择好了关键事件,而且在proc中我们提到的关键事件在 tracepoint 中都有涉及,因此直接利用 tracepoint 也是一种很好的方法。
3.1 利用tracepoint定位内核关键事件的源码位置
在proc的指标提取原理分析中,我们定位到的是proc的计算点,但是还没有看到具体的内核调度器代码,这里我们利用 tracepoint 是可以定位到调度器的核心代码中的,因此现在目标就是定位进入runqueue 的事件和调度下CPU的事件。
tracepoint的函数都有一定的格式,我们可以利用这个格式来快速查找,查找进程 wakeup 的事件点 trace_sched_wakeup();
:
/*
* Mark the task runnable and perform wakeup-preemption.
*/
static void ttwu_do_wakeup(struct rq *rq, struct task_struct *p, int wake_flags,
struct rq_flags *rf)
{
check_preempt_curr(rq, p, wake_flags);
p->state = TASK_RUNNING;
trace_sched_wakeup(p);
这里直接就定位到了给 task
设置为 TASK_RUNNING
状态的事件点,这里也可以全局搜索 p->state = TASK_RUNNING;
会有另外的发现:
/*
* wake_up_new_task - wake up a newly created task for the first time.
*
* This function will do some initial scheduler statistics housekeeping
* that must be done for every newly created context, then puts the task
* on the runqueue and wakes it.
*/
void wake_up_new_task(struct task_struct *p)
{
struct rq_flags rf;
struct rq *rq;
raw_spin_lock_irqsave(&p->pi_lock, rf.flags);
p->state = TASK_RUNNING;
···
post_init_entity_util_avg(p);
activate_task(rq, p, ENQUEUE_NOCLOCK);
trace_sched_wakeup_new(p);
第14行又设置了进程状态为
TASK_RUNNING
。又在第32行发现了嫌疑函数
trace_sched_wakeup_new()
对应的tracepoint事件是sched:sched_wakeup_new
。
也可以利用这种方法找到调度下CPU的事件,利用tracepoint事件 sched:sched_switch
,那么可以全局搜一下 trace_sched_switch()
函数,就会发现主调度器的代码了:
static void __sched notrace __schedule(bool preempt)
{
struct task_struct *prev, *next;
unsigned long *switch_count;
struct rq_flags rf;
struct rq *rq;
int cpu;
···
++*switch_count;
trace_sched_switch(preempt, prev, next);
/* Also unlocks the rq: */
rq = context_switch(rq, prev, next, &rf);
···
就在函数
context_switch
的前面,所以tracepoint是天然的定位代码的神器。
调度器的函数调用关系也可以利用 ftrace
工具来观察,另外也可以使用网站 livegrep<https://link.zhihu.com/?target=https%3A//livegrep.com/>来获得函数调用关系:
ttwu_do_wakeup() --> trace_sched_wakeup
wake_up_new_task() --> trace_sched_wakeup_new
__schedule() --> trace_sched_switch
4. perf的统计方法
那么现有的工具有没有基于 tracepoint 来进行统计的?那就离不开 perf 了, perf基于事件采样原理,以性能事件为基础,除了 tracepoint ,也使用性能监控单元(PMU)来测量、记录和监控各种硬件和软件事件,还支持kprobes 和 uprobes等事件。
perf的原理大致是这样的:每隔一个固定的时间,就在CPU上(每个核上都有)产生一个中断,在中断上看看,当前是哪个pid,哪个函数,然后给对应的pid和函数加一个统计值,这样,我们就知道CPU有百分几的时间在某个pid,或者某个函数上了。
使用 perf 统计过去5s的调度延迟:
$ sudo perf sched record -- sleep 5然后会生成一个perf.data的文件,然后需要使用如下命令解析:
$ sudo perf sched latency就可以看到这样的信息:
-------------------------------------------------------------------------------------------------------------------------------------------
Task | Runtime ms | Switches | Avg delay ms | Max delay ms | Max delay start | Max delay end |
-------------------------------------------------------------------------------------------------------------------------------------------
sh:(5) | 11.075 ms | 5 | avg: 0.128 ms | max: 0.186 ms | max start: 23083.066371 s | max end: 23083.066557 s
lpstat:(4) | 35.320 ms | 4 | avg: 0.101 ms | max: 0.116 ms | max start: 23083.068281 s | max end: 23083.068397 s
sed:(4) | 8.584 ms | 4 | avg: 0.084 ms | max: 0.100 ms | max start: 23080.926985 s | max end: 23080.927085 s
更彻底一点,把每一次任务切换的信息都展现出来,就更能知道每一次延迟是怎样发生的,而这正是 sudo perf sched timehist
可以完成的事。它可以统计每轮 task switch 时,之前在 CPU 上运行的那个 "prev" 线程得到的执行时间 (run time) ,以及该线程在获得这次执行机会前的休眠态等待 (wait time) 和运行态等待 (sch delay) 时间(这个 patch<https://link.zhihu.com/?target=https%3A//lore.kernel.org/lkml/1479919218-6367-15-git-send-email-acme%40kernel.org/>:
Samples do not have callchains.
time cpu task name wait time sch delay run time
[tid/pid] (msec) (msec) (msec)
--------------- ------ ------------------------------ --------- --------- ---------
23078.905190 [0000] perf[207623] 0.000 0.000 0.000
23078.905236 [0000] migration/0[14] 0.000 0.002 0.046
23078.905319 [0001] perf[207623] 0.000 0.000 0.000
而要得到比之再 detail 一点的记录,就该用上 sudo perf sched script
了。它可以展示每次的任务切换具体是怎样发生的:
perf 207623 [000] 23078.905185: sched:sched_stat_runtime: comm=perf pid=207623 runtime=80289[ns] vruntime=29533570197 [ns]
perf 207623 [000] 23078.905187: sched:sched_waking: comm=migration/0 pid=14 prio=0 target_cpu=000
perf 207623 [000] 23078.905189: sched:sched_stat_runtime: comm=perf pid=207623 runtime=4657 [ns] vruntime=29533574854 [ns]
perf 207623 [000] 23078.905190: sched:sched_switch: prev_comm=perf prev_pid=207623 prev_prio=120 prev_state=R+ ==> next_comm=migration/0 next_pid=14 next_prio=0
migration/0 14 [000] 23078.905192: sched:sched_migrate_task: comm=perf pid=207623 prio=120 orig_cpu=0 dest_cpu=1
migration/0 14 [000] 23078.905236: sched:sched_switch: prev_comm=migration/0 prev_pid=14 prev_prio=0 prev_state=S ==> next_comm=swapper/0 next_pid=0 next_prio=120
可以直接发现 perf 使用了tracepoint事件,还包括了 _sched_stat_runtime_
事件,因为它统计了task运行在CPU上的时间。
5. eBPF的统计方法—面向task
通过对 atop 提取性能指标的原理和方式我们的结论是提取频率秒级别,并且是快照信息;perf 工具可以直接使用 tracepoint 事件源,并且实现了更为强大的功能,可以捕获到每一次事件的发生,proc 和 tracepoint 的内核统计点都非常精确。
现在的目标是来看看 eBPF 用于提取性能指标的时候,有哪些不同。
5.1 eBPF开发框架及工具选择
eBPF 目前还处于发展期,eBPF 的开发框架也还是多种多样的,不同编程语言也都有相应的开发框架,根据目前我的了解,libbpf
的 rust
开发框架和 libbpf
的 c
开发框架是反馈比较好的,而如果说目前最推荐的eBPF性能项目我个人依然认为是 bcc
和 bpftrace
,如果逻辑较为简单可以采用bpftrace
,如果要求可编程性可以参考 bcc
。
好多同学对bcc的印象是它是一个 python
前端的 eBPF 开发框架,但是不是的,bcc 官网介绍是:
python只是开发工具使用的一个前端框架而已,并且bcc社区目前对之前的很多工具已经进行了libbpf迁移,位于 [libbpf-tools](https://github.com/iovisor/bcc/tree/master/libbpf-tools)
目录下,并且支持 x86
powerpc
arm64
。
接下来说下目前eBPF开发的方式,bcc下的 [libbpf-tools](https://github.com/iovisor/bcc/tree/master/libbpf-tools)
目录已经是一个比较完善的环境了,可以直接进行开发,也可以采用 libbpf-bootstrap
项目的方式进行开发,这两种方式都需要安装llvm和clang。
目前使用 eBPF 有一个非常方便或者说是必备的工具 bpftool
,利用这个工具可以了解当前系统有关于eBPF的配置选项、系统限制、可以利用哪些种类的maps、每种 eBPF 程序类型可以使用哪些 helpers
,并且会告诉你哪些类型的 eBPF 程序类型不可以使用:
5.2 eBPF的统计方法
我们的目标是介绍 eBPF 提取数据的原理,因此不展开讲述如何进行 eBPF 编程。
介绍了前面的内容,现在来介绍eBPF提取的原理就方便很多了,一句话总结就是 原理 + tracepoint的事件频率 + 可编程性
。
为了更直接地说明原理,我们采用 bpftrace 代码,会更为清晰:
tracepoint:sched:sched_wakeup,
tracepoint:sched:sched_wakeup_new
{
@qtime[args->pid] = nsecs;
}
tracepoint:sched:sched_switch
{
if (args->prev_state == TASK_RUNNING) {
@qtime[args->prev_pid] = nsecs;
}
$ns = @qtime[args->next_pid];
if ($ns) {
@usecs = hist((nsecs - $ns) / 1000);
}
delete(@qtime[args->next_pid]);
}
原理已经很清晰,结合在 tracepoint 部分介绍的原理就可以很容易理解,但是前提是理解了原理,除了bpftrace,还有 bcc 项目,当我们在工作中需要利用 eBPF 的可编程性的时候,很有参考价值,可以把 bcc 中的每一个工具都看为是一个模板。
现在来看 bcc 中的调度延迟:
SEC("tp_btf/sched_wakeup")
int BPF_PROG(sched_wakeup, struct task_struct *p)
{
if (filter_cg && !bpf_current_task_under_cgroup(&cgroup_map, 0))
return 0;
return trace_enqueue(p->tgid, p->pid);
}
SEC("tp_btf/sched_wakeup_new")
int BPF_PROG(sched_wakeup_new, struct task_struct *p)
{
if (filter_cg && !bpf_current_task_under_cgroup(&cgroup_map, 0))
return 0;
return trace_enqueue(p->tgid, p->pid);
}
filter_cg
是打开过滤开关,我们没有启用因此可以忽略,所以在一个 task
进入runqueue
的时候,只干了一件事情,那就是记录该 task
的 tgid
和 pid
。
那么什么是 tgid 和 pid?内核的 task_struct
中的 pid
一定是全局唯一的,什么意思?就是用户态下一个进程 fork 一个进程出来,那么这两个进程的 pid 是不同的,用户态下一个进程产生一个线程出来,那么这个线程的pid和进程的pid也是不同的;那么怎么知道一个线程是哪一个进程的?就是通过tgid,一个进程A产生了一个线程B,那么A和B有不同的pid,但是B的tgid等于A的pid。
trace_enqueue()
函数只做了一件事情,就是记录当前这个pid进程进入 runqueue
的时间戳, 现在只考虑最普通的情况,只记录pid的情况,因此每有一个 task 被加入到 runqueue 的时候,就记录这个 task 的 pid 和当前的纳秒时间戳。
再来看看将进程调度下CPU的事件:
SEC("tp_btf/sched_switch")
int BPF_PROG(sched_swith, bool preempt, struct task_struct *prev,
struct task_struct *next)
{
struct hist *histp;
u64 *tsp, slot;
u32 pid, hkey;
s64 delta;
···
if (get_task_state(prev) == TASK_RUNNING)
trace_enqueue(prev->tgid, prev->pid);
pid = next->pid;
tsp = bpf_map_lookup_elem(&start, &pid);
if (!tsp)
return 0;
delta = bpf_ktime_get_ns() - *tsp;
if (delta < 0)
goto cleanup;
···
if (targ_ms)
delta /= 1000000U;
else
delta /= 1000U;
slot = log2l(delta);
···
cleanup:
bpf_map_delete_elem(&start, &pid);
return 0;
}
这里省略了一些数据存储和展示的代码片段,第10行的代码就是在记录被动切换下runqueue 的 task 的时间戳,原理上相信大家已经比较清晰,但是有一行并不起眼的代码就是滴23行还是到26行,这里只是区分了一下不同的时间单位,但是实质上是进行了数据处理,包括27行也是在处理数据,因此 eBPF 提供了内核态下数据预处理的能力,除此之外,利用 eBPF 提供的 map 可以实现各种缓存。
另外再 perf 工具的介绍中,发现 perf 可以利用 tracepoint 拿到很多的信息并且做后续的处理,但是 tracepoint 提供的信息是很有限的,在 tracepoint 中的介绍中也进行了展示,而在上面这个 eBPF 函数中,我们直接拿到了即将被调度下 CPU 的 task_struct
和 下一个即将上 CPU 的 task
的 task_struct
,在允许的范围内,我们可以拿到比 perf 多的多的 task
的内部信息。
最重要的是 eBPF 具有可编程性,上面的函数中只是进行了数据预处理、条件筛选的功能,更进一步,在数据来源和频率都很充足的前提下,可以加入每位同学自己不同的逻辑,在每一个事件点上都可以实现更为复杂的逻辑,再进一步,有了可编程能力,可以同时利用不同的事件点,并且不同的事件可以相互作用,激发出不同的效果,这也是 eBPF 可以发挥想象力的地方。
小结
这次分享主要分享了通过传统工具、proc、tracepoint来定位内核代码,另外通过对 atop、perf、eBPF的比较,可以得出一些在这个场景下 eBPF 的一些优势:
可以利用现有基础设施的优点;
强大的可编程性;
可以访问任何受控范围内的字段;
定制化,之前的基础设施很大程度上是固定的逻辑,我们无法改变或者改变的成本很高,但是eBPF可以给我们提供定制化、自定义的能力;
(完)
由于作者水平有限,本文错漏缺点在所难免,希望读者批评指正。