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

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

494615

上期回顾:

通过性能指标学习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 工具来观察,另外也可以使用网站 livegrephttps://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) 时间(这个 patchhttps://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 目录下,并且支持 x86 powerpc arm64。

接下来说下目前eBPF开发的方式,bcc下的 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 的一些优势:

可以利用现有基础设施的优点;
强大的可编程性;
可以访问任何受控范围内的字段;
定制化,之前的基础设施很大程度上是固定的逻辑,我们无法改变或者改变的成本很高,但是eBPF可以给我们提供定制化、自定义的能力;

(完)

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

点赞收藏
Linux内核之旅

Linux内核之旅开源社区:http://kerneltravel.net/ 欢迎关注微信公众号“Linux内核之旅”

请先登录,查看1条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步

为你推荐

从 Linux 内核角度探秘 JDK MappedByteBuffer

从 Linux 内核角度探秘 JDK MappedByteBuffer

日常Bug排查-连接突然全部关闭

日常Bug排查-连接突然全部关闭

5
1