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

Linux阅码场 2022-08-01 08:00


以下文章来源于Linux内核之旅 ,作者赵晨雨




作者介绍:

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



B站录屏链接:

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

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


Linux内核之旅社区联合thoughtworks未济实验室在中科院开源之夏和CCF暑期夏令营活动中发布了13个题目,我们也一直在思考如何让大家通过这次暑期活动更好地提升自己。这次分享是通过具体的一个性能指标,利用现有的工具来定位内核代码,从而圈定学习 Kernel 的目标范围,因此这次分享是想给社区同学提供一种学习方法,因此可以将重点放在方法上,原理部分就交给社区同学深入挖掘了,而且根据目前社区同学的反馈,效果超出我的预期,期待社区同学后续精彩的分享。

同时我也在思考一个问题,就是怎么才能把 eBPF 的优势讲明白,或者说把eBPF 在某一个点上的应用优势是什么讲明白,社区的题目大部分都是围绕 eBPF 展开的,因此也是在尝试解释这件事情。

调度延迟

本次圈定的性能指标是调度延迟,那首要的目标就是看看到底什么是调度延迟,调度延迟是保证每一个可运行进程都至少运行一次的时间间隔,翻译一下,是指一个 task 的状态变成了 TASK_RUNNING,然后从进入 CPU 的 runqueue开始,到真正执行(获得 CPU 的执行权)的这段时间间隔。

需要说明的是调度延迟在 Linux Kernel 中实现的时候是分为两种方式的:面向task和面向rq,我们现在关注的是 task 层面。

那么 runqueue 和调度器的一个 sched period 的关系就显得比较重要了。首先来看调度周期,调度周期的含义就是所有可运行的 task 都在CPU上执行一遍的时间周期,而 Linux CFS 中这个值是不固定的,当进程数量小于8的时候,sched period 就是一个固定值6ms,如果 runqueue 数量超过了8个,那么就保证每个 task 都必须运行一定的时间,这个一定的时间还叫最小粒度时间,CFS的默认最小粒度时间是0.75ms,使用 sysctl_sched_min_granularity 保存, sched period 是通过下面这个内核函数来决定的:

/** The idea is to set a period in which each task runs once.** When there are too many tasks (sched_nr_latency) we have to stretch* this period because otherwise the slices get too small.** p = (nr <= nl) ? l : l*nr/nl*/static u64 __sched_period(unsigned long nr_running){    if (unlikely(nr_running > sched_nr_latency))        return nr_running * sysctl_sched_min_granularity;    else        return sysctl_sched_latency;}
  • nr_running 就是可执行 task 数量

那么一个疑问就产生了,这个不就是调度延迟 scheduling latency 吗,并且每一次计算都会给出一个确定的调度周期的值是多少,但是这个调度周期仅仅是用于调度算法里面,因为这里的调度周期是为了确保 runqueue 上的 task 的最小调度周期,也就是在这段时间内,所有的 task 至少被调度一次,但是这仅仅是目标,而实际是达不到的。因为系统的状态、task 的状态、task 的slice等等都是不断变化的,周期性调度器会在每一次 tick 来临的时候检查当前 task 的slice是否到期,如果到期了就会发生 preempt 抢,而周期性调度器本身的精度就很有限,不考虑 hrtick 的情况下,我们查看系统的时钟频率:

$ grep CONFIG_HZ /boot/config-$(uname -r)

# CONFIG_HZ_PERIODIC is not set

# CONFIG_HZ_100 is not set

CONFIG_HZ_250=y

# CONFIG_HZ_300 is not set

# CONFIG_HZ_1000 is not set

CONFIG_HZ=250

仅仅是250HZ,也就是4ms一次时钟中断,所以都无法保证每一个 task 在CPU上运行的slice是不是它应该有的slice,更不要说保证调度周期了,外加还有wakeup、preempt等等事件。


1. atop的统计方法

既然不能直接使用计算好的值,那么就得通过其他方法进行统计了,首先Linux kernel 本身是有统计每一个 task 的调度延迟的,在内核中调度延迟使用的说法是 run delay ,并且通过 proc 文件系统暴露了出来,因此大概率现有的传统工具提取调度延迟的源数据是来自于 proc 的,例如 atop 工具。

run delay在 proc 中的位置:

进程的调度延迟:/proc/<PID>/schedstat
线程的调度延迟:/proc/<PID>/task/<TID>/schedstat

现在的目标变为搞清楚 atop 工具是怎么统计调度延迟的。

现有的工具 atop 是可以输出用户态每一个进程和线程的调度延迟指标的,在开启 atop 后按下 s 键,就会看到 RDELAY 列,这一列就是调度延迟了。我们来看看 atop 工具是怎么统计这个指标值的,clone atop工具的代码:

git@github.com:Atoptool/atop.git

由于目前的目标是搞清楚 atop 对调度延迟指标的统计方法,因此我只关心和这个部分相关的代码片段,可视化展示的部分并不关心。

整体来说,atop 工作的大体流程是:

intmain(int argc, char *argv[]){···    // 获取 interval    interval = atoi(argv[optind]);
// 开启收集引擎 engine();··· return 0; /* never reached */}
  • 这里的 interval 就是我们使用atop的时候以什么时间间隔来提取数据,这个时间间隔就是 interval

  • 所有的计算等操作都在 engine() 函数中完成

engine()的工作流程如下:

static voidengine(void){···    /*    ** install the signal-handler for ALARM, USR1 and USR2 (triggers    * for the next sample)    */    memset(&sigact, 0, sizeof sigact);    sigact.sa_handler = getusr1;    sigaction(SIGUSR1, &sigact, (struct sigaction *)0);···    if (interval > 0)        alarm(interval);···    for (sampcnt=0; sampcnt < nsamples; sampcnt++)    {···        if (sampcnt > 0 && awaittrigger)            pause();        awaittrigger = 1;···        do        {            curtlen   = counttasks();    // worst-case value            curtpres  = realloc(curtpres,                    curtlen * sizeof(struct tstat));
ptrverify(curtpres, "Malloc failed for %lu tstats\n", curtlen);
memset(curtpres, 0, curtlen * sizeof(struct tstat)); } while ( (ntaskpres = photoproc(curtpres, curtlen)) == curtlen);
··· } /* end of main-loop */}
  • 代码细节上不再详细介绍,整体运行的大循环是在16行开始的,真正得到调度延迟指标值的是在34行的 photoproc() 函数中计算的,传入的是需要计算的 task 列表和 task的数量

来看看最终计算的地方:

unsigned longphotoproc(struct tstat *tasklist, int maxtask){···        procschedstat(curtask);        /* from /proc/pid/schedstat */···        if (curtask->gen.nthr > 1)        {···            curtask->cpu.rundelay = 0;···            /*            ** open underlying task directory            */            if ( chdir("task") == 0 )            {···                while ((tent=readdir(dirtask)) && tval                {                    struct tstat *curthr = tasklist+tval;                    ···                    // totalize delays of all threads                    curtask->cpu.rundelay +=                        procschedstat(curthr);                    ···                }                ···            }        }    ···    return tval;}
  • 第5行的函数就是在读取 proc 的 schedstat 文件:

  static count_t  procschedstat(struct tstat *curtask){    FILE    *fp;    char    line[4096];    count_t    runtime, rundelay = 0;    unsigned long pcount;    static char *schedstatfile = "schedstat";      /*     ** open the schedstat file     */    if ( (fp = fopen(schedstatfile, "r")) )    {        curtask->cpu.rundelay = 0;          if (fgets(line, sizeof line, fp))        {            sscanf(line, "%llu %llu %lu\n",                    &runtime, &rundelay, &pcount);              curtask->cpu.rundelay = rundelay;        }          /*        ** verify if fgets returned NULL due to error i.s.o. EOF        */        if (ferror(fp))            curtask->cpu.rundelay = 0;          fclose(fp);    }    else    {        curtask->cpu.rundelay = 0;    }      return curtask->cpu.rundelay;  }
  • 15行是在判断是不是有多个thread,如果有多个thread,那么就把所有的thread的调度延迟相加就得到了这个任务的调度延迟。

所以追踪完 atop 对调度延迟的处理后,我们就可以发现获取数据的思路是开启 atop之后,按照我们指定的 interval,在大循环中每一次 interval 到来以后,就读取一次 proc 文件系统,将这个值保存,因此结论就是目前的 atop 工具对调度延迟的提取方式就是每隔 interval 秒,读取一次 proc下的 schedstat 文件。
因此 atop 获取的是每 interval 时间的系统当前进程的调度延迟快照数据,并且是秒级别的提取频率。


2. proc的底层方法—面向task

那么数据源头我们已经定位好了,就是来源于 proc,而 proc 的数据全部都是内核运行过程中自己统计的,那现在的目标就转为内核内部是怎么统计每一个 task 的调度延迟的,因此需要定位到内核中 proc 计算调度延迟的地点是哪里。

方法很简单,写一个读取 schedstat 文件的简单程序,使用 ftrace追踪一下,就可以看到 proc 里面是哪个函数来生成的 schedstat 文件中的数据,ftrace的结果如下:

2)   0.125 us    |            single_start();  
2) | proc_single_show() {
2) | get_pid_task() {
2) 0.124 us | rcu_read_unlock_strict();
2) 0.399 us | }
2) | proc_pid_schedstat() {
2) | seq_printf() {
2) 1.145 us | seq_vprintf();
2) 1.411 us | }
2) 1.722 us | }
2) 2.599 us | }

很容易可以发现是第六行的函数:

#ifdef CONFIG_SCHED_INFO/** Provides /proc/PID/schedstat*/static int proc_pid_schedstat(struct seq_file *m, struct pid_namespace *ns,                              struct pid *pid, struct task_struct *task){    if (unlikely(!sched_info_on()))        seq_puts(m, "0 0 0\n");    else        seq_printf(m, "%llu %llu %lu\n",                   (unsigned long long)task->se.sum_exec_runtime,                   (unsigned long long)task->sched_info.run_delay,                   task->sched_info.pcount);
return 0;}#endif

第8行是在判断一个内核配置选项,一般默认都是开启的,或者能看到 schedstat 文件有输出,那么就是开启的,或者可以用 make menuconfig 查找一下这个选项的状态。

可以发现 proc 在拿取这个调度延迟指标的时候是直接从传进来的 task_struct 中的 sched_info 中记录的 run_delay ,而且是一次性读取,没有做平均值之类的数据处理,因此也是一个快照形式的数据。

首先说明下 sched_info 结构:

struct sched_info {#ifdef CONFIG_SCHED_INFO    /* Cumulative counters: */
/* # of times we have run on this CPU: */ unsigned long pcount;
/* Time spent waiting on a runqueue: */ unsigned long long run_delay;
/* Timestamps: */
/* When did we last run on a CPU? */ unsigned long long last_arrival;
/* When were we last queued to run? */ unsigned long long last_queued;#endif /* CONFIG_SCHED_INFO */};

和上面proc函数的宏是一样的,所以可以推测出来这个宏很有可能是用来开启内核统计 task 的调度信息的。每个字段的含义代码注释已经介绍的比较清晰了,kernel 对调度延迟给出的解释是在 runqueue 中等待的时间。

现在的目标转变为内核是怎么对这个 run_delay 字段进行计算的。需要回过头来看一下sched_info 的结构,后两个是用于计算 run_delay 参数的,另外这里就需要Linux调度器框架和CFS调度器相关了,首先需要梳理一下和进程调度信息统计相关的函数,其实就是看 CONFIG_SCHED_INFO 这个宏包起来了哪些函数,找到这些函数的声明点,相关的函数位于 kernel/sched/stats.h 中。

涉及到的函数如下:

sched_info_queued(rq, t)sched_info_reset_dequeued(t)sched_info_dequeued(rq, t)sched_info_depart(rq, t)sched_info_arrive(rq, next)sched_info_switch(rq, t, next)

BTW,调度延迟在rq中统计的函数是:

rq_sched_info_arrive()rq_sched_info_dequeued()rq_sched_info_depart()

注意的是这些函数的作用只是统计调度信息,查看这些函数的代码,其中和调度延迟相关的函数有以下三个:

sched_info_depart(rq, t)sched_info_queued(rq, t)sched_info_arrive(rq, next)

并且一定是在关键的调度时间节点上被调用的:

  • 1. 进入runqueue
    task 从其他状态(休眠,不可中断等)切换到可运行状态后,进入 runqueue 的起始时刻;

  • 2. 调度下CPU,然后进入runqueue
    task 从一个 cpu 的 runqueue 移动到另外一个 cpu 的 runqueue 时,更新进入新的 runqueue
    的起始时刻;
    task 正在运行被调度下CPU,放入 runqueue 的起始时刻,被动下CPU;

  • 3. 产生新task然后进入runqueue;

  • 4. 调度上CPU
    进程从 runqueue 中被调度到cpu上运行时更新last_arrival;

  • 可以这么理解要么上CPU,要么下CPU,下CPU并且状态还是 TASK_RUNNING 状态的其实就是进入 runqueue 的时机。

进入到 runqueue 都会最终调用到 sched_info_queued ,而第二种情况会先走 sched_info_depart 函数:

static inline void sched_info_depart(struct rq *rq, struct task_struct *t){    unsigned long long delta = rq_clock(rq) - t->sched_info.last_arrival;
rq_sched_info_depart(rq, delta);
if (t->state == TASK_RUNNING) sched_info_queued(rq, t);}
  • 第3行的代码在计算上次在CPU上执行的时间戳是多少,用现在的时间减去 last_arrival(上次被调度上CPU的时间)就可以得到,然后传递给了 rq_sched_info_depart() 函数

  • 第2种情况下,在第8行,如果进程这个时候的状态还是 TASK_RUNNING ,那么说明这个时候 task 是被动下CPU的,表示该 task 又开始在 runqueue 中等待了,为什么不统计其它状态的 task,因为其它状态的 task 是不能进入 runqueue 的,例如等待IO的 task,这些task只有在完成等待后才可以进入 runqueue,这个时候就有变成了第1种情况;第1种情况下会直接进入 sched_info_queued() 函数;因此这两种情况下都是task进入了runqueue 然后最终调用 sched_info_queued() 函数记录上次(就是现在)进入runqueue 的时间戳 last_queued

  • sched_info_queued() 的代码如下:

  static inline void sched_info_queued(struct rq *rq, struct task_struct *t)  {    if (unlikely(sched_info_on())) {        if (!t->sched_info.last_queued)            t->sched_info.last_queued = rq_clock(rq);    }  }

然后就到了最后一个关键节点,task被调度CPU了,就会触发 sched_info_arrive() 函数:

static void sched_info_arrive(struct rq *rq, struct task_struct *t)  {    unsigned long long now = rq_clock(rq), delta = 0;      if (t->sched_info.last_queued)        delta = now - t->sched_info.last_queued;    sched_info_reset_dequeued(t);    t->sched_info.run_delay += delta;    t->sched_info.last_arrival = now;    t->sched_info.pcount++;      rq_sched_info_arrive(rq, delta);  }

这个时候就可以来计算调度延迟了,代码逻辑是如果有记录上次的 last_queued 时间戳,那么就用现在的时间戳减去上次的时间戳,就是该 task 的调度延迟,然后保存到 run_delay 字段里面,并且标记这次到达CPU的时间戳到 last_arrival 里面,pcount 记录的是上cpu上了多少次。

公式就是:

该task的调度延迟 = 该task刚被调度上CPU的时间戳 - last_queued(该task上次进入runqueue的时间戳)



(待续)

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



Ftrace训练营火热报名中:Ftrace训练营:站在设计者的角度来理解ftrace(限50人)。训练营第一期报名已圆满成功,好评如潮。第二期报名正在火爆进行中(咨询小月微信:linuxer2016)。


ARM安全架构训练营火热报名中:阅码场训练营:ARM安全架构之Trustzone-TEE实战。报名咨询客服(小月微信:linuxer2016)。


ARM架构与调优调试训练营火热报名中:阅码场训练营:ARM架构与调试调优。报名咨询客服(小月微信:linuxer2016)。


Linux阅码场 专业的Linux技术社区和Linux操作系统学习平台,内容涉及Linux内核,Linux内存管理,Linux进程管理,Linux文件系统和IO,Linux性能调优,Linux设备驱动以及Linux虚拟化和云计算等各方各面.
评论
  • 根据环洋市场咨询(Global Info Research)项目团队最新调研,预计2030年全球无人机锂电池产值达到2457百万美元,2024-2030年期间年复合增长率CAGR为9.6%。 无人机锂电池是无人机动力系统中存储并释放能量的部分。无人机使用的动力电池,大多数是锂聚合物电池,相较其他电池,锂聚合物电池具有较高的能量密度,较长寿命,同时也具有良好的放电特性和安全性。 全球无人机锂电池核心厂商有宁德新能源科技、欣旺达、鹏辉能源、深圳格瑞普和EaglePicher等,前五大厂商占有全球
    GIRtina 2025-01-07 11:02 127浏览
  • 大模型的赋能是指利用大型机器学习模型(如深度学习模型)来增强或改进各种应用和服务。这种技术在许多领域都显示出了巨大的潜力,包括但不限于以下几个方面: 1. 企业服务:大模型可以用于构建智能客服系统、知识库问答系统等,提升企业的服务质量和运营效率。 2. 教育服务:在教育领域,大模型被应用于个性化学习、智能辅导、作业批改等,帮助教师减轻工作负担,提高教学质量。 3. 工业智能化:大模型有助于解决工业领域的复杂性和不确定性问题,尽管在认知能力方面尚未完全具备专家级的复杂决策能力。 4. 消费
    丙丁先生 2025-01-07 09:25 122浏览
  • 在智能家居领域中,Wi-Fi、蓝牙、Zigbee、Thread与Z-Wave等无线通信协议是构建短距物联局域网的关键手段,它们常在实际应用中交叉运用,以满足智能家居生态系统多样化的功能需求。然而,这些协议之间并未遵循统一的互通标准,缺乏直接的互操作性,在进行组网时需要引入额外的网关作为“翻译桥梁”,极大地增加了系统的复杂性。 同时,Apple HomeKit、SamSung SmartThings、Amazon Alexa、Google Home等主流智能家居平台为了提升市占率与消费者
    华普微HOPERF 2025-01-06 17:23 209浏览
  •  在全球能源结构加速向清洁、可再生方向转型的今天,风力发电作为一种绿色能源,已成为各国新能源发展的重要组成部分。然而,风力发电系统在复杂的环境中长时间运行,对系统的安全性、稳定性和抗干扰能力提出了极高要求。光耦(光电耦合器)作为一种电气隔离与信号传输器件,凭借其优秀的隔离保护性能和信号传输能力,已成为风力发电系统中不可或缺的关键组件。 风力发电系统对隔离与控制的需求风力发电系统中,包括发电机、变流器、变压器和控制系统等多个部分,通常工作在高压、大功率的环境中。光耦在这里扮演了
    晶台光耦 2025-01-08 16:03 66浏览
  • 每日可见的315MHz和433MHz遥控模块,你能分清楚吗?众所周知,一套遥控设备主要由发射部分和接收部分组成,发射器可以将控制者的控制按键经过编码,调制到射频信号上面,然后经天线发射出无线信号。而接收器是将天线接收到的无线信号进行解码,从而得到与控制按键相对应的信号,然后再去控制相应的设备工作。当前,常见的遥控设备主要分为红外遥控与无线电遥控两大类,其主要区别为所采用的载波频率及其应用场景不一致。红外遥控设备所采用的射频信号频率一般为38kHz,通常应用在电视、投影仪等设备中;而无线电遥控设备
    华普微HOPERF 2025-01-06 15:29 172浏览
  • 「他明明跟我同梯进来,为什么就是升得比我快?」许多人都有这样的疑问:明明就战绩也不比隔壁同事差,升迁之路却比别人苦。其实,之间的差异就在于「领导力」。並非必须当管理者才需要「领导力」,而是散发领导力特质的人,才更容易被晓明。许多领导力和特质,都可以通过努力和学习获得,因此就算不是天生的领导者,也能成为一个具备领导魅力的人,进而被老板看见,向你伸出升迁的橘子枝。领导力是什么?领导力是一种能力或特质,甚至可以说是一种「影响力」。好的领导者通常具备影响和鼓励他人的能力,并导引他们朝着共同的目标和愿景前
    优思学院 2025-01-08 14:54 74浏览
  • 故障现象一辆2017款东风风神AX7车,搭载DFMA14T发动机,累计行驶里程约为13.7万km。该车冷起动后怠速运转正常,热机后怠速运转不稳,组合仪表上的发动机转速表指针上下轻微抖动。 故障诊断 用故障检测仪检测,发动机控制单元中无故障代码存储;读取发动机数据流,发现进气歧管绝对压力波动明显,有时能达到69 kPa,明显偏高,推断可能的原因有:进气系统漏气;进气歧管绝对压力传感器信号失真;发动机机械故障。首先从节气门处打烟雾,没有发现进气管周围有漏气的地方;接着拔下进气管上的两个真空
    虹科Pico汽车示波器 2025-01-08 16:51 79浏览
  • 村田是目前全球量产硅电容的领先企业,其在2016年收购了法国IPDiA头部硅电容器公司,并于2023年6月宣布投资约100亿日元将硅电容产能提升两倍。以下内容主要来自村田官网信息整理,村田高密度硅电容器采用半导体MOS工艺开发,并使用3D结构来大幅增加电极表面,因此在给定的占位面积内增加了静电容量。村田的硅技术以嵌入非结晶基板的单片结构为基础(单层MIM和多层MIM—MIM是指金属 / 绝缘体/ 金属) 村田硅电容采用先进3D拓扑结构在100um内,使开发的有效静电容量面积相当于80个
    知白 2025-01-07 15:02 145浏览
  • By Toradex 秦海1). 简介嵌入式平台设备基于Yocto Linux 在开发后期量产前期,为了安全以及提高启动速度等考虑,希望将 ARM 处理器平台的 Debug Console 输出关闭,本文就基于 NXP i.MX8MP ARM 处理器平台来演示相关流程。 本文所示例的平台来自于 Toradex Verdin i.MX8MP 嵌入式平台。  2. 准备a). Verdin i.MX8MP ARM核心版配合Dahlia载板并
    hai.qin_651820742 2025-01-07 14:52 111浏览
  • 本文介绍编译Android13 ROOT权限固件的方法,触觉智能RK3562开发板演示,搭载4核A53处理器,主频高达2.0GHz;内置独立1Tops算力NPU,可应用于物联网网关、平板电脑、智能家居、教育电子、工业显示与控制等行业。关闭selinux修改此文件("+"号为修改内容)device/rockchip/common/BoardConfig.mkBOARD_BOOT_HEADER_VERSION ?= 2BOARD_MKBOOTIMG_ARGS :=BOARD_PREBUILT_DTB
    Industio_触觉智能 2025-01-08 00:06 95浏览
我要评论
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦