进程切换:自愿(VOLUNTARY)与强制(INVOLUNTARY) 抢占(PREEMPTION)
进程切换:自愿(voluntary)与强制(involuntary) | Linux Performance http://linuxperf.com/?p=209
抢占(preemption)是如何发生的 | Linux Performance http://linuxperf.com/?p=211
进程切换:自愿(VOLUNTARY)与强制(INVOLUNTARY)
从进程的角度看,CPU是共享资源,由所有的进程按特定的策略轮番使用。一个进程离开CPU、另一个进程占据CPU的过程,称为进程切换(process switch)。进程切换是在内核中通过调用schedule()完成的。
发生进程切换的场景有以下三种:
- 进程运行不下去了:
比如因为要等待IO完成,或者等待某个资源、某个事件,典型的内核代码如下:
1234//把进程放进等待队列,把进程状态置为TASK_UNINTERRUPTIBLEprepare_to_wait(waitq, wait, TASK_UNINTERRUPTIBLE);//切换进程schedule(); - 进程还在运行,但内核不让它继续使用CPU了:
比如进程的时间片用完了,或者优先级更高的进程来了,所以该进程必须把CPU的使用权交出来; - 进程还可以运行,但它自己的算法决定主动交出CPU给别的进程:
用户程序可以通过系统调用sched_yield()来交出CPU,内核则可以通过函数cond_resched()或者yield()来做到。
进程切换分为自愿切换(Voluntary)和强制切换(Involuntary),以上场景1属于自愿切换,场景2和3属于强制切换。如何分辨自愿切换和强制切换呢?
- 自愿切换发生的时候,进程不再处于运行状态,比如由于等待IO而阻塞(TASK_UNINTERRUPTIBLE),或者因等待资源和特定事件而休眠(TASK_INTERRUPTIBLE),又或者被debug/trace设置为TASK_STOPPED/TASK_TRACED状态;
- 强制切换发生的时候,进程仍然处于运行状态(TASK_RUNNING),通常是由于被优先级更高的进程抢占(preempt),或者进程的时间片用完了。
注:实际情况更复杂一些,由于Linux内核支持抢占,kernel preemption有可能发生在自愿切换的过程之中,比如进程正进入休眠,本来如果顺利完成的话就属于自愿切换,但休眠的过程并不是原子操作,进程状态先被置成TASK_INTERRUPTIBLE,然后进程切换,如果Kernel Preemption恰好发生在两者之间,那就打断了休眠过程,自愿切换尚未完成,转而进入了强制切换的过程(虽然是强制切换,但此时的进程状态已经不是运行状态了),下一次进程恢复运行之后会继续完成休眠的过程。所以判断进程切换属于自愿还是强制的算法要考虑进程在切换时是否正处于被抢占(preempt)的过程中,参见以下内核代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
static void __sched __schedule(void)
{
...
switch_count = &prev->nivcsw;//强制切换的次数
if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) {//进程处于非运行状态并且允许抢占
...
switch_count = &prev->nvcsw;//自愿切换的次数
}
...
if (likely(prev != next)) {
rq->nr_switches++;
rq->curr = next;
++*switch_count;//进程切换次数累加
context_switch(rq, prev, next); /* unlocks the rq */
/*
* The context switch have flipped the stack from under us
* and restored the local variables which were saved when
* this task called schedule() in the past. prev == current
* is still correct, but it can be moved to another cpu/rq.
*/
cpu = smp_processor_id();
rq = cpu_rq(cpu);
} else
raw_spin_unlock_irq(&rq->lock);
...
}
not_running && preemptive : voluntary
|
最后,澄清几个容易产生误解的场景:
- 进程可以通过调用sched_yield()主动交出CPU,这不是自愿切换,而是属于强制切换,因为进程仍然处于运行状态。
- 有时候内核代码会在耗时较长的循环体内通过调用 cond_resched()或yield() ,主动让出CPU,以免CPU被内核代码占据太久,给其它进程运行机会。这也属于强制切换,因为进程仍然处于运行状态。
进程自愿切换(Voluntary)和强制切换(Involuntary)的次数被统计在 /proc/<pid>/status 中,其中voluntary_ctxt_switches表示自愿切换的次数,nonvoluntary_ctxt_switches表示强制切换的次数,两者都是自进程启动以来的累计值。
1
2
3
|
# grep ctxt /proc/26995/status
voluntary_ctxt_switches: 79
nonvoluntary_ctxt_switches: 4
|
也可以用 pidstat -w 命令查看进程切换的每秒统计值:
1
2
3
4
5
6
|
# pidstat -w 1
Linux 3.10.0-229.14.1.el7.x86_64 (bj71s060) 02/01/2018 _x86_64_ (2 CPU)
12:05:20 PM UID PID cswch/s nvcswch/s Command
12:05:21 PM 0 1299 0.94 0.00 httpd
12:05:21 PM 0 27687 0.94 0.00 pidstat
|
自愿切换和强制切换的统计值在实践中有什么意义呢?
大致而言,如果一个进程的自愿切换占多数,意味着它对CPU资源的需求不高。如果一个进程的强制切换占多数,意味着对它来说CPU资源可能是个瓶颈,这里需要排除进程频繁调用sched_yield()导致强制切换的情况。
进程切换有自愿(Voluntary)和强制(Involuntary)之分,在前文中详细解释了两者的不同,简单来说,自愿切换意味着进程需要等待某种资源,强制切换则与抢占(Preemption)有关。
抢占(Preemption)是指内核强行切换正在CPU上运行的进程,在抢占的过程中并不需要得到进程的配合,在随后的某个时刻被抢占的进程还可以恢复运行。发生抢占的原因主要有:进程的时间片用完了,或者优先级更高的进程来争夺CPU了。
抢占的过程分两步,第一步触发抢占,第二步执行抢占,这两步中间不一定是连续的,有些特殊情况下甚至会间隔相当长的时间:
- 触发抢占:给正在CPU上运行的当前进程设置一个请求重新调度的标志(TIF_NEED_RESCHED),仅此而已,此时进程并没有切换。
- 执行抢占:在随后的某个时刻,内核会检查TIF_NEED_RESCHED标志并调用schedule()执行抢占。
抢占只在某些特定的时机发生,这是内核的代码决定的。
触发抢占的时机
每个进程都包含一个TIF_NEED_RESCHED标志,内核根据这个标志判断该进程是否应该被抢占,设置TIF_NEED_RESCHED标志就意味着触发抢占。
直接设置TIF_NEED_RESCHED标志的函数是 set_tsk_need_resched();
触发抢占的函数是resched_task()。
TIF_NEED_RESCHED标志什么时候被设置呢?在以下时刻:
- 周期性的时钟中断
时钟中断处理函数会调用scheduler_tick(),这是调度器核心层(scheduler core)的函数,它通过调度类(scheduling class)的task_tick方法 检查进程的时间片是否耗尽,如果耗尽则触发抢占:
1
2
3
4
5
6
|
void scheduler_tick(void)
{
...
curr->sched_class->task_tick(rq, curr, 0);
...
}
|
Linux的进程调度是模块化的,不同的调度策略比如CFS、Real-Time被封装成不同的调度类,每个调度类都可以实现自己的task_tick方法,调度器核心层根据进程所属的调度类调用对应的方法,比如CFS对应的是task_tick_fair,Real-Time对应的是task_tick_rt,每个调度类对进程的时间片都有不同的定义。
- 唤醒进程的时候
当进程被唤醒的时候,如果优先级高于CPU上的当前进程,就会触发抢占。相应的内核代码中,try_to_wake_up()最终通过check_preempt_curr()检查是否触发抢占。
- 新进程创建的时候
如果新进程的优先级高于CPU上的当前进程,会触发抢占。相应的调度器核心层代码是sched_fork(),它再通过调度类的 task_fork方法触发抢占:
1
2
3
4
5
6
7
|
int sched_fork(unsigned long clone_flags, struct task_struct *p)
{
...
if (p->sched_class->task_fork)
p->sched_class->task_fork(p);
...
}
|
- 进程修改nice值的时候
如果进程修改nice值导致优先级高于CPU上的当前进程,也会触发抢占。内核代码参见 set_user_nice()。
- 进行负载均衡的时候
在多CPU的系统上,进程调度器尽量使各个CPU之间的负载保持均衡,而负载均衡操作可能会需要触发抢占。
不同的调度类有不同的负载均衡算法,涉及的核心代码也不一样,比如CFS类在load_balance()中触发抢占:
1
2
3
4
5
6
7
8
|
load_balance()
{
...
move_tasks();
...
resched_cpu();
...
}
|
RT类的负载均衡基于overload,如果当前运行队列中的RT进程超过一个,就调用push_rt_task()把进程推给别的CPU,在这里会触发抢占。
执行抢占的时机
触发抢占通过设置进程的TIF_NEED_RESCHED标志告诉调度器需要进行抢占操作了,但是真正执行抢占还要等内核代码发现这个标志才行,而内核代码只在设定的几个点上检查TIF_NEED_RESCHED标志,这也就是执行抢占的时机。
抢占如果发生在进程处于用户态的时候,称为User Preemption(用户态抢占);如果发生在进程处于内核态的时候,则称为Kernel Preemption(内核态抢占)。
执行User Preemption(用户态抢占)的时机
- 从系统调用(syscall)返回用户态时;
12345678910源文件:arch/x86/kernel/entry_64.Ssysret_careful:bt $TIF_NEED_RESCHED,%edxjnc sysret_signalTRACE_IRQS_ONENABLE_INTERRUPTS(CLBR_NONE)pushq_cfi %rdicall schedulepopq_cfi %rdijmp sysret_check - 从中断返回用户态时。
12345678910111213retint_careful:CFI_RESTORE_STATEbt $TIF_NEED_RESCHED,%edxjnc retint_signalTRACE_IRQS_ONENABLE_INTERRUPTS(CLBR_NONE)pushq_cfi %rdicall schedulepopq_cfi %rdiGET_THREAD_INFO(%rcx)DISABLE_INTERRUPTS(CLBR_NONE)TRACE_IRQS_OFFjmp retint_check
执行Kernel Preemption(内核态抢占)的时机
Linux在2.6版本之后就支持内核抢占了,但是请注意,具体取决于内核编译时的选项:
- CONFIG_PREEMPT_NONE=y
不允许内核抢占。这是SLES的默认选项。 - CONFIG_PREEMPT_VOLUNTARY=y
在一些耗时较长的内核代码中主动调用cond_resched()让出CPU。这是RHEL的默认选项。 - CONFIG_PREEMPT=y
允许完全内核抢占。
在 CONFIG_PREEMPT=y 的前提下,内核态抢占的时机是:
- 中断处理程序返回内核空间之前会检查TIF_NEED_RESCHED标志,如果置位则调用preempt_schedule_irq()执行抢占。preempt_schedule_irq()是对schedule()的包装。
12345678910111213#ifdef CONFIG_PREEMPT/* Returning to kernel space. Check if we need preemption *//* rcx: threadinfo. interrupts off. */ENTRY(retint_kernel)cmpl $0,TI_preempt_count(%rcx)jnz retint_restore_argsbt $TIF_NEED_RESCHED,TI_flags(%rcx)jnc retint_restore_argsbt $9,EFLAGS-ARGOFFSET(%rsp) /* interrupts off? */jnc retint_restore_argscall preempt_schedule_irqjmp exit_intr#endif - 当内核从non-preemptible(禁止抢占)状态变成preemptible(允许抢占)的时候;
在preempt_enable()中,会最终调用 preempt_schedule 来执行抢占。preempt_schedule()是对schedule()的包装。