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

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

276515

上期回顾:

通过性能指标学习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条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步

为你推荐

【全网首发】细节拉满,80 张图带你一步一步推演 slab 内存池的设计与实现

【全网首发】细节拉满,80 张图带你一步一步推演 slab 内存池的设计与实现

【全网首发】从内核源码看 slab 内存池的创建初始化流程

【全网首发】从内核源码看 slab 内存池的创建初始化流程

太坑了吧!一次某某云上的redis读超时排查经历

太坑了吧!一次某某云上的redis读超时排查经历

【全网首发】深入理解 slab cache 内存分配全链路实现

【全网首发】深入理解 slab cache 内存分配全链路实现

图文详解 Linux 分段机制!

图文详解 Linux 分段机制!

保护模式下的内存管理

保护模式下的内存管理

5
1