Linux任务调度延时分析工具getdelays

1 前言

1.1 什么是getdelays工具?

getdelays工具是一个用户态工具,这个工具可以显示出指定pid或者tgid对应的调度延时数据,包括用户态内核态运行的时间,在就绪队列上等待运行的时间,以及等待IO等资源的延迟时间。这些数据是通过netlink机制从内核获取,最终呈现给用户态。

1.2 如何获取这个工具呢?

这个工具是和具体的Linux内核代码配套的,其代码在Linux内核目录树的的tools/accounting/getdelays.c 文件中(对的,这个工具只有一个文件),在主机上可以使用如下方式进行编译:

gcc -I/usr/src/linux/include getdelays.c -o getdelays

如果需要交叉编译需要参考如下步骤:

#1 进入源码目录编译内核
make ARCH=arm64 CROSS_COMPILE=/$HOME/$CROSS/aarch64-linux-gnu-gcc menuconfig
make ARCH=arm64 CROSS_COMPILE=/$HOME/$CROSS/aarch64-linux-gnu-gcc all
#2 安装头文件
make ARCH=arm64 CROSS_COMPILE=/$HOME/$CROSS/aarch64-linux-gnu-gcc headers_install
#3 交叉编译
/$HOME/$CROSS/aarch64-linux-gnu-gcc -I../../usr/include getdelays.c -o getdelays

1.3 这个工具如何使用呢?

一般的使用方式如下:

getdelays [-t tgid] [-p pid] [-c cmd...]

例如,获取pid为72的任务自启动后的延时情况:

./getdelays -d -p 72
print delayacct stats ON
PID     72


CPU             count     real total  virtual total    delay total  delay average
                  260      241000000      246467570       17551751          0.068ms
IO              count    delay total  delay average
                    0              0              0ms
SWAP            count    delay total  delay average
                    0              0              0ms
RECLAIM         count    delay total  delay average
                    0              0              0ms
THRASHING       count    delay total  delay average
                    0              0              0ms

上面的有效输出有12行,其中前面2行打印的是内核配置与pid信息;后面10行分别是任务各个维度的调度或者延迟信息,其中CPU这一个维度表示任务CPU调度相关的信息;而IO、SWAP、RECLAIM、THRASHING分别表示的是任务由于其他资源而阻塞延时的信息。

2 getdelays的输出字段

在1.3节我们看到getdelays一个简单的用例输出了多条相关信息,有CPU,IO, SWAP, RECLAIM以及THRASHING。这些信息大致可归纳为两类:CPU维度和delay延时。

2.1 cpu维度

CPU count      real total   virtual total     delay total   delay average
内核中的来源  task->sched_info.pcount  task->utime + task->stime  task->se.sum_exec_runtime  task->sched_info.run_delay  task->cpu_delay_total/1000000/task->cpu_count

count:来自于内核中任务的task->sched_info.pcount字段,表示任务调度运行的次数,这个字段依赖于CONFIG_SCHEDSTATS=y配置。

任务在每次即将调度运行前通过sched_info_arrive()函数来对这个即将运行的任务task->sched_info.pcount++。

real total:等于内核中任务的task->utime + task->stime之和,表示任务在用户态+内核态运行时间。

内核中定义了TICK时钟中断,以HZ频率触发;在TICK时钟中断中会检查当前任务current(idle任务除外)处于何种上下文,如果是用户态则会将task->utime增加一个TICK的时间(转换为ns);否则如果是内核态(中断上下文不会统计)则会为task->stime增加一个TICK时间。

因此,从原理上来说task->utime和task->stime实际上是对任务运行时间的一个采样统计方式。

virtual total:来自于内核中任务调度实体的task->se.sum_exec_runtime字段,这个字段统计的是任务调度实体task->se在CPU上的运行时间。

与task->utime和task->stime的统计方式不同,task->se.sum_exec_runtime是基于调度实体来统计的;它的更新时机也更多,除了在TICK时钟中断更新外,还会在dequeue_entity()、put_prev_task()等内核点进行更新,也就是说只要发生调度切换这个字段也会更新;还有一点不同是,它不是按照TICK粒度来统计的,而是通过task->se.sum_exec_runtime += (rq->clock_task - curr->exec_start)这个表达式来计算的,curr表示当前运行队列上的正在运行的调度实体。

其中task->se.exec_start是在主调度函数中对通过pick_next_task()函数来进行初始化为当前时间戳,表示这个任务开始运行的时间戳,每次更新task->se.sum_exec_runtime时,也会同时更新task->se.exec_start字段。

而rq->clock_task则是通过update_rq_clock()函数进行更新,这个函数在内核的许多调度点都会进行更新,其时间源来自于sched_clock()。

delay total:来自于内核中任务的task->sched_info.run_delay字段,这个字段用以统计一个任务就绪后在运行队列等待运行的时间,这个字段依赖于CONFIG_SCHEDSTATS=y内核配置。

其计算方法是在任务加入到就绪队列时记录一个时间戳到t->sched_info.last_queued字段,在任务被调度运行时内核调用sched_info_arrive()函数统计任务在就绪队列上等待调度运行的时间。另外有一种特殊情况,如果任务在cpu上运行,但是期间被抢占,这时候被抢占开始到下次再次被调度运行的时间也要累计到t->sched_info.run_delay字段。

2.2 task->delays资源维度

内核如果使能了CONFIG_TASK_DELAY_ACCT=y配置,则会在struct task_struct结构中增加一个struct task_delay_info *delays字段。这个字段是struct task_delay_info 结构,顾名思义,这个结构主要用于记录任务的延迟信息。

如下所示,这个结构成员的lock用于保护结构体,flags用于标识任务阻塞的原因;

其它字段则是记录不同延迟事件的信息,总共有blkio、swapin、freepages和thrashing 这4种不同的延迟类型。

#ifdef CONFIG_TASK_DELAY_ACCT
struct task_delay_info {
        raw_spinlock_t  lock;
        unsigned int    flags;  /* Private per-task flags */

        u64 blkio_start;        /* Shared by blkio, swapin */
        u64 blkio_delay;        /* wait for sync block io completion */
        u64 swapin_delay;       /* wait for swapin block io completion */
        u32 blkio_count;        /* total count of the number of sync block */
                                /* io operations performed */
        u32 swapin_count;       /* total count of the number of swapin block */
                                /* io operations performed */

        u64 freepages_start;
        u64 freepages_delay;    /* wait for memory reclaim */

        u64 thrashing_start;
        u64 thrashing_delay;    /* wait for thrashing page */

        u32 freepages_count;    /* total count of memory reclaim */
        u32 thrashing_count;    /* total count of thrash waits */
};
#endif

IO 

count  delay total  delay average

task->delays->blkio_count

task->delays->blkio_delay

SWAP  count  delay total  delay average

task->delays->swapin_count

task->delays->swapin_delay

RECLAIM  count  delay total  delay average

task->delays->freepages_count

task->delays->freepages_delay

THRASHING  count  delay total  delay average

task->delays->thrashing_count

task->delays->thrashing_delay

IO delay:IO delay中count与delay total来自于内核中的task->delays->blkio_count和task->delays->blkio_delay,表示一个任务task等待IO资源而阻塞的次数和任务等待IO阻塞的时间。

在IO资源可用时调用delayacct_blkio_end()计算blkio_start到blkio_end之间的时间。时间戳都是通过ktime_get_ns()函数获取的

什么时候blkio_start?什么时候又blkio_end呢?

在主调度函数__schedule()中,如果任务prev因为IO阻塞,即prev->in_iowait不为0而调度出去,这时候就会调用delayacct_blkio_start()将当前时间戳记录下来:

current->delays->blkio_start = ktime_get_ns()

而在这个阻塞任务由于IO资源可用而被唤醒时,内核调用delayacct_blkio_end()函数将这段阻塞睡眠的时间累加到task->delays->blkio_delay字段,并对task->delays->blkio_count++ 。

SWAP delay:SWAP delay中的count和delay total来自于内核中的task->delays->swapin_count与task->delays->swapin_delay字段,表示一个任务task访问的内存在交换设备上时产生的swapin次数和任务等待内存swapin的延迟时间。

这两个字段和上面的blkio_count、blkio_delay一样也是统计的delayacct_blkio_start与delayacct_blkio_end()之间的时间,区别在于swapin_count与swapin_delay在进入blockio前会先在do_swap_page()函数中为发生swapin的任务current->delays->flags设置DELAYACCT_PF_SWAPIN标志;这样在delayacct_blkio_end()函数中就会通过任务的current->delays->flags标志是否设置了DELAYACCT_PF_SWAPIN而决定统计swapin_count与swapin_delay。

RECLAIM delay:RECLAIM delay中的count和delay total来自于内核中的task->delays->freepages_count与task->delays->freepages_delay字段,表示一个任务task分配内存时产生的内存回收次数和任务等待内存回收的延迟时间。

在内核如果内存不足时会调用do_try_to_free_pages()来进行内存回收(如回收页缓存)。在进行页面回收前调用delayacct_freepages_start()将当前时间戳记录到current->delays->freepages_start;在页面回收完成后调用delayacct_freepages_end()来将该任务上下文进行的页面回收次数和页面回收消耗的时间累积到task->delays->freepages_count和task->delays->freepages_delay。 

THRASHING delay:THRASHING delay中的count和delay total来自于内核中的task->delays->thrashing_count与task->delays->thrashing_delay字段,表示一个任务task在访问刚刚被加入非活跃状态缓存页(页颠簸)的次数和等待时间。

在函数wait_on_page_bit_common()中,如果等待的page满足如下条件,说明这个页发生了页颠簸,则调用delayacct_thrashing_start()函数记录当前时间戳到task->delays->thrashing_start字段;等到这个page可用时再调用delayacct_thrashing_end()函数统计当前任务task发生页颠簸的次数和由于页颠簸而等待的时间。

if (bit_nr == PG_locked && !PageSwapBacked(page) &&
        !PageUptodate(page) && PageWorkingset(page))
原文地址:https://www.cnblogs.com/liuhailong0112/p/15379397.html