背景
nvcsw(自愿上下文切换的次数)和 nivcsw(非自愿上下文切换的次数)是用于判断 CPU-bound 的一种性能指标,可以在 proc 文件系统中查看统计信息:/proc/[pid]/sched
。
在早期的文章 Linux 内核的 CFS 任务调度 中也提到,nvcsw 和 nivcsw 的计数是在 __schedule()
流程中完成的。本文继续跟踪 __schedule()
的调用路径,简单探讨一下这两个计数器的统计方式。
__schedule()
下面是 Linux v6.4.8 版本的 __schedule()
实现,省略了无关细节:
static void __sched notrace __schedule(unsigned int sched_mode)
{
struct task_struct *prev, *next;
unsigned long *switch_count;
unsigned long prev_state;
// ...
prev = rq->curr;
// ...
switch_count = &prev->nivcsw;
prev_state = READ_ONCE(prev->__state);
// 非抢占标记且进程状态不是 TASK_RUNNING(只有这个状态的数值为 0)
if (!(sched_mode & SM_MASK_PREEMPT) && prev_state) {
// ...
deactivate_task(rq, prev, DEQUEUE_SLEEP | DEQUEUE_NOCLOCK);
// ...
switch_count = &prev->nvcsw;
}
next = pick_next_task(rq, prev, &rf);
clear_tsk_need_resched(prev);
clear_preempt_need_resched();
// ...
if (likely(prev != next)) {
rq->nr_switches++;
// ...
++*switch_count;
// ...
rq = context_switch(rq, prev, next, &rf);
} else {
// ...
}
}
从代码实现可以看出,只要函数传参不包含 SM_MASK_PREEMPT
标记,并且提前设置了合适的进程状态如 TASK_UNINTERRUPTIBLE
,本次调度将会视为自愿的上下文切换,否则就是非自愿的上下文切换。
NOTES:
- 反过来说,调度前仍处于
TASK_RUNNING
状态的进程肯定是非自愿的上下文切换。 - 不管自愿与否,调度的具体操作都一样(pick next task + deactivate task)。
schedule()
自愿上下文切换的主要接口就是 schedule()
,即主动调度:
asmlinkage __visible void __sched schedule(void)
{
struct task_struct *tsk = current;
sched_submit_work(tsk);
// 第一次调度不检查 TIF_NEED_RESCHED 标记
do {
preempt_disable();
// 肯定是传递 SM_NONE 标记
__schedule(SM_NONE);
sched_preempt_enable_no_resched();
} while (need_resched());
sched_update_worker(tsk);
}
所以下面这些(依赖于 schedule()
函数的)做法都是自愿行为:
- 主动调度:
set_current_state
+schedule
+__set_current_state
。 - 等待队列:
prepare_to_wait
+schedule
+finish_wait
。 - 等待完成:
wait_for_completion
。 - 定时任务:上面的
schedule
再换成schedule_timeout
。 - 统计封装:上面的
schedule
再换成io_schedule
或者blk_io_schedule
等等。
NOTES:
- 内核态使用的
yield()
函数是非自愿切换,因为强制设置当前进程为 TASK_RUNNING 状态。 - 用户态使用的
sched_yield()
系统调用是非自愿切换,因为进程肯定是 TASK_RUNNING 状态。 - 用户态使用的阻塞网络 IO (例:
recvmsg()
)以及睡眠等待(例:nanosleep()
)是自愿切换。
抢占模型
下面是 kernel/sched/core.c
的一些注释,直接说明了 Linux 三种抢占模型的区别:
/*
* SC:cond_resched
* SC:might_resched
* SC:preempt_schedule
* SC:preempt_schedule_notrace
* SC:irqentry_exit_cond_resched
*
*
* NONE:
* cond_resched <- __cond_resched
* might_resched <- RET0
* preempt_schedule <- NOP
* preempt_schedule_notrace <- NOP
* irqentry_exit_cond_resched <- NOP
*
* VOLUNTARY:
* cond_resched <- __cond_resched
* might_resched <- __cond_resched
* preempt_schedule <- NOP
* preempt_schedule_notrace <- NOP
* irqentry_exit_cond_resched <- NOP
*
* FULL:
* cond_resched <- RET0
* might_resched <- RET0
* preempt_schedule <- preempt_schedule
* preempt_schedule_notrace <- preempt_schedule_notrace
* irqentry_exit_cond_resched <- irqentry_exit_cond_resched
*/
(有必要吐槽一下,这么好的总结居然放在源码文件里,还藏在第 8000+ 行?)
内核态抢占
在完全抢占(CONFIG_PREEMPT
)的情况下,根据上面注释展开的调用栈:
preempt_schedule
preempt_schedule_common
__schedule(SM_PREEMPT)
preempt_schedule_notrace
__schedule(SM_PREEMPT)
irqentry_exit_cond_resched
raw_irqentry_exit_cond_resched
preempt_schedule_irq
__schedule(SM_PREEMPT)
调度传递了 SM_PREEMPT
标记,也就是说内核态抢占的部分会被标记为非自愿上下文切换。
用户态抢占
在禁止内核抢占(CONFIG_PREEMPT_NONE
)的情况下,系统调用或中断执行退出到用户态前:
exit_to_user_mode_prepare
exit_to_user_mode_loop
schedule()
由于要退回到用户态需要有前提保证状态为 TASK_RUNNING,因此仍然标记为非自愿上下文切换。
cond_resched()
cond_resched()
语义比较特殊(conditional reschedule),可以认为是有 TIF_NEED_RESCHED 标记的情况下「提前主动抢占」,算是一种降低延迟的优化手段。所以这种既主动又抢占的方式到底算是自愿还是非自愿?看流程:
cond_resched
__cond_resched
if should_resched:
preempt_schedule_common()
结合上面内核态抢占分析过的流程得知,这种方式调度标记为非自愿上下文切换。
NOTES:
- 完全抢占模型的前提下,该函数是空实现,因为内核态基本上随时都可以抢占了。
- 自愿抢占模型的前提下,
might_resched()
就是cond_resched()
,注意这是非自愿切换。 - 上面也有提到 IRQ(中断)专用的
cond_resched
版本。 - 块设备 IO 经常使用这个函数,可能是在追求「high speed, low drag」。
总结
只要是抢占,都会标记为非自愿上下文切换。但是如果是在统计非人为因素造成的调度,至少还需要在 nivcsw 的基础上进一步过滤掉自己使用的 cond_resched()
(还有 yield()
,但是没有使用它的理由)等容易望文生义的函数调用统计;即使是只考虑用户态进程,也至少要排除掉 sched_yield()
。
最后说句不好听的,我个人觉得这种性能指标其实也没啥参考价值,它能体现调度的基本情况(比如每个进程或者线程的抢占调度的次数和占比,自己做时间窗口还能统计出长度),但也就仅此而已了。