通过性能指标学习Linux Kernel - (下)

语言: CN / TW / HK

作者介绍:

赵晨雨,师从陈莉君教授,Linux内核之旅社区maintainer,现就职于thoughtworks 安全与系统研发事业部,thoughtworks未济实验室成员。

B站录屏链接:

GLCC编程夏令营——LMP课题 周会分享

http://www.bilibili.com/video/BV1oY4y177Wh?spm_id_from=333.337.search-card.all.click

上期回顾:

通过性能指标学习Linux Kernel - (上)

上期我们介绍了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_wakeup

tracepoint是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/format
name: 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 < http://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 <http://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 官网介绍是:

BCC is a toolkit for creating efficient kernel tracing and manipulation programs, and includes several useful tools and examples. It makes use of extended BPF (Berkeley Packet Filters), formally known as eBPF, a new feature that was first added to Linux 3.15. Much of what BCC uses requires Linux 4.1 and above.

python只是开发工具使用的一个前端框架而已,并且bcc社区目前对之前的很多工具已经进行了libbpf迁移,位于 [libbpf-tools](http://github.com/iovisor/bcc/tree/master/libbpf-tools) 目录下,并且支持  x86 powerpc arm64

接下来说下目前eBPF开发的方式,bcc下的 [libbpf-tools](http://github.com/iovisor/bcc/tree/master/libbpf-tools) 目录已经是一个比较完善的环境了,可以直接进行开发,也可以采用  libbpf-bootstrap 项目的方式进行开发,这两种方式都需要安装llvm和clang。

目前使用 eBPF 有一个非常方便或者说是必备的工具 bpftool ,利用这个工具可以了解当前系统有关于eBPF的配置选项、系统限制、可以利用哪些种类的maps、每种 eBPF 程序类型可以使用哪些  helpers ,并且会告诉你哪些类型的 eBPF 程序类型不可以使用:

$ sudo bpftool feature

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 的一些优势:

  1. 可以利用现有基础设施的优点;

  2. 强大的可编程性;

  3. 可以访问任何受控范围内的字段;

  4. 定制化,之前的基础设施很大程度上是固定的逻辑,我们无法改变或者改变的成本很高,但是eBPF可以给我们提供定制化、自定义的能力;

(完)

由于作者水平有限,本文错漏缺点在所难免,希望读者批评指正。