本文是《面向应用开发者的系统指南》文档其中的一篇,完整的目录见《面向应用开发者的系统指南》导论。
概述
以上描述进程的创建、执行、调度器的工作原理,有了这些准备之后,可以使用systemtap在系统中埋点进行一些跟踪,以便理解进程的行为。
分析进程对CPU的占用
简单回顾一下前面进程调度相关的内容:
内核中使用就绪队列来维护当前所有处于可运行状态的进程,可运行状态不包括等待IO、休眠等状态的进程。
进程调度器负责从就绪队列中选择处于可运行状态的进程来执行。
而所有不处于可运行状态的进程,并不占用CPU资源,这些进程都等待被相关的事件比如网络IO唤醒,唤醒之后的进程更改状态为可运行状态,同时加入到就绪队列中,然后才能被调度器算法选择执行。
因此,一个进程的整个生命周期中,虽然看上去进程一直存在,但是并不是所有时候都占用CPU资源。根据CPU占用资源与否,或者说当前是否在运行,分为on cpu和off cpu状态:
上图中就一个进程执行的时间线做了简单的阶段划分,其中省略掉了进程被创建出来和最后退出时的情况,仅列出占用CPU资源状态的切换。
进程占用CPU获得执行权的时候,称为on cpu时间。
进程因为各种原因(被其他进程抢占、自己调用了sleep系统调用主动进入睡眠状态、等待网络IO等)被剥夺了执行权的时候,首先会调用deactivate_task函数从就绪队列中删除,接下来调用context_switch函数进行进程的上下文切换,这个时候旧的进程失去CPU的执行权,此时正式进入off cpu时间中。
在此之后,进程由于各种原因被唤醒,唤醒之后首先会被再次调用activate_task函数加入到就绪队列中,进入就绪队列的进程也并不是马上就能够获得执行权的,是由进程调度算法来决定哪一个在就绪队列中的进程来执行。这段时间又可以分为两个部分:
进程被切换出去直到重新进入就绪队列,这部分时间内进程等待被唤醒。
进入就绪队列到被调度器选中执行,这部分时间内进程等待被调度执行。
以上两部分时间的总和,加起来就是进程休眠的时间,即处于off cpu状态的时间。
从这里看出来,一个进程虽然看上去一直存在,但并不是所有时间都在执行,跟进一个程序的运行时间时,需要区分其on和off cpu的时间,如果off的时间过长,那需要看看是什么原因导致了进程一直没有被唤醒执行。
另外需要注意的是,进程处于就绪状态,并不一定就是在运行,有可能还在就绪队列中等待被调度执行;但是反之则不然,一个占用CPU在执行的进程,其状态一定是就绪状态。即:
进程处于就绪状态的时间 = 进程在就绪队列的时间 + 进程在执行的时间
关于off cpu这一概念,Off-CPU Analysis一文中有更多的讲述。
有了上面对on cpu和off cpu的介绍,下面来看看使用systemtap如何跟踪这些状态以及所处的时间。
off CPU
systemtap中自带的tapset中,有一个scheduler.stp文件,里面定义了与调度器相关的一些probe。
其中跟踪off cpu的probe是scheduler.cpu_off :
probe scheduler.cpu_off =
kernel.trace("sched_switch") !,
kernel.function("context_switch")
{
name = "cpu_off"
task_prev = $prev
task_next = $next
idle = __is_idle()
}
结合代码和最开始的示意图,可以知道该probe事件是针对内核trace事件sched_switch以及内核函数context_switch的封装,这两个事件都在进程上下文切换时触发。
在该probe事件中,能获取到的参数是:
task_prev:保存切换之前的进程task_struct结构体。
task_next:保存切换之后的进程task_struct结构体。
idle:表示当前CPU是否空闲。
因为这个probe事件记录了进程切换前后的信息,因此可以用来完成类似记录系统切换最多的进程跟踪的功能:
global csw_count
global idle_count
probe scheduler.cpu_off {
csw_count[task_prev, task_next]++
idle_count+=idle
}
function fmt_task(task_prev, task_next)
{
return sprintf("%s(%d)->%s(%d)",
task_execname(task_prev),
task_pid(task_prev),
task_execname(task_next),
task_pid(task_next))
}
function print_cswtop () {
printf ("%45s %10s\n", "Context switch", "COUNT")
foreach ([task_prev, task_next] in csw_count- limit 20) {
printf("%45s %10d\n", fmt_task(task_prev, task_next), csw_count[task_prev, task_next])
}
printf("%45s %10d\n", "idle", idle_count)
delete csw_count
delete idle_count