概述
概述:
进程切换分为自愿(voluntary)和强制(involuntary)两种。通常自愿切换是指任务由于等待某种资源,将state改为非RUNNING状态后,调用schedule()主动让出CPU;而强制切换(即抢占)则是任务状态仍为RUNNING却失去CPU使用权,情况有任务时间片用完、有更高优先级的任务、任务中调用cond_resched()或yield让出CPU。
本文主要介绍抢占式进程切换(involuntary context switch),抢占又根据抢占发生的时机划分为用户抢占和内核抢占。
注意:1.文章基于arm64架构的linux5.0内核代码;2. 默认开启内核抢占CONFIG_PREEMPT
关于调度器:
内核中存在两个调度器,即主调度器及周期性调度器,通称为通用调度器。主调度器函数为__schedule();周期性调度器函数为scheduler_tick()。其中主调度器在内核代码中需要被主动调用,周期性调度器则伴随着系统的tick中断每秒HZ次的发生。
抢占前的检查:
是否安全?
抢占发生的前提是要确保此次抢占是安全的。什么算安全?即current任务没有持有自旋锁,否则可能会发生死锁。于是在引入内核抢占机制(CONFIG_PREEMPT)的时同时引入了preempt_count,用来保证抢占的安全性,获取锁前会去inc抢占计数,而抢占发生前会去检查preempt_count是否为0。
是否需要抢占?
关于是否需要去抢占,会去判断thread_info的成员flags是否设置了TIF_NEED_RESCHED标志位。(tif_need_resched()即用来判断此flag是否置位)
*后面会有具体的抢占例子分析。
进程切换的统计:
每个进程的强制切换和自愿切换的次数都会被记录在/proc/pid/status中:
/通过procfs查看/
grep ctxt /proc/26995/status
voluntary_ctxt_switches: 79
nonvoluntary_ctxt_switches: 4
/使用pidstat命令查看/
pidstat -w
自愿切换和强制切换的统计值在实践中有什么意义呢?
答:大致而言,如果一个进程的自愿切换占多数,意味着它对CPU资源的需求不高;如果一个进程的强制切换占多数,表明它对CPU的依赖较强,意味着对它来说CPU资源可能是个瓶颈(这里需要排除进程频繁调用sched_yield()导致强制切换的情况)
内核抢占的几个时机:
中断返回内核空间:
发生在内核态的中断el1_irq退出前,即irq handler之后,kernel_exit恢复现场之间。会去检查current进程的preempt_count和need_resched以判断是否需要进行一次抢占。
/*文:arch/arm64/kernel/entry.S
函数调用:
el1_irq
irq_handler
el1_preempt
preempt_schedule_irq
__schedule(true)
*/
el1_irq:
kernel_entry 1
enable_da_f
irq_handler
#ifdef CONFIG_PREEMPT
ldr x24, [tsk, #TSK_TI_PREEMPT] /* 抢占前的检查:preempt_count和need_resched.
注意:设置need_resched的地方,同时也会设置TIF_NEED_RESCHED,所以这里检查need_resched即可。
*/
cbnz x24, 1f /* 判断是否要跳转*/
bl el1_preempt /* 跳转el1_preempt中尝试抢占*/
1:
#endif
#ifdef CONFIG_TRACE_IRQFLAGS
bl trace_hardirqs_on
#endif
kernel_exit 1
ENDPROC(el1_irq)
#ifdef CONFIG_PREEMPT
el1_preempt:
mov x24, lr
1: bl preempt_schedule_irq /* 中断抢占的核心函数。注意:执行到此中断仍是关闭的,所以跳转回来时中断也要是关闭的*/
ldr x0, [tsk, #TSK_TI_FLAGS]
tbnz x0, #TIF_NEED_RESCHED, 1b
ret x24
#endif
/*文件:kernel/sched/core.c */
asmlinkage __visible void __sched preempt_schedule_irq(void) // 封装了__schedule函数,且它会保证返回时local中断仍关闭。
{
enum ctx_state prev_sta;
/* Catch callers which need to be fixed */
BUG_ON(preempt_count() || !irqs_disabled()); //检测异常:若抢占计数不为零,或者中断没有关闭 则dump_stack并产生oops。
prev_state = exception_enter(); //保存当前cpu的异常状态下的上下文到prev_state
do {
preempt_disable(); //关抢占,__schedule的过程不允许被抢占
local_irq_enable(); //使能中断
__schedule(true); //调度器核心函数。true表示此次切换为抢占。
local_irq_disable(); //关闭中断
sched_preempt_enable_no_resched(); //开抢占
} while (need_resched()); //如果调度出去后的进程操作了被中断进程的thread_info.flags,使它仍为TIF_NEED_SCHED,那就继续进行调度。
exception_exit(prev_state); //恢复抢占前的保存在prev_state中的异常上下文。
}
内核中调用cond_resched()
内核代码中显式调用cond_resched()触发抢占,它的核心函数是preempt_schedule_common
/*cond_resched()在kernel代码中有着较广泛的使用*/
int __sched _cond_resched(void)
{
if (should_resched(0)) { //判断preempt_count是否为0。为0则代表开启抢占,need_resched也已被设置。
preempt_schedule_common(); //__schedule函数的封装函数。
return 1;
}
rcu_all_qs();
return 0;
}
static void __sched notrace preempt_schedule_common(void)
{
do {
preempt_disable_notrace();
preempt_latency_start(1);
__schedule(true); //调用__sechdule,且传参true代表这里是抢占
preempt_latency_stop(1);
preempt_enable_no_resched_notrace();
} while (need_resched()); //如果调度出去后的进程操作了被当前进程的thread_info.flags,使它仍为TIF_NEED_SCHED,那就继续进行调度
}
内核中使能preempt时
内核代码中调用preempt_enable()开启preempt时也会检查执行抢占。
/*函数调用:
preempt_enable()
__preempt_schedule()
preempt_schedule()
preempt_schedule_common()
*/
当前任务开启抢占时,也会检查并执行一次抢占。类似cond_resched()最终调用preempt_schedule_common()函数。
注意:非抢占的内核在spin_unlock()中不检查调度,而cond_resched在两种内核中都检查调度 (具体可查看代码实现)
用户抢占:
中断返回用户空间:
当中断发生在进程的用户态,中断返回用户空间时也会检查是否需要执行抢占。
/*文件:arch/arm64/kernel/entry.S
函数调用:
ret_to_user
work_pending
do_notify_resume
schedule()
*/
work_pending:
mov x0, sp // 'regs'
bl do_notify_resume //跳转到do_notify_resume函数。
#ifdef CONFIG_TRACE_IRQFLAGS
bl trace_hardirqs_on // enabled while in userspace
#endif
ldr x1, [tsk, #TSK_TI_FLAGS] // re-check for single-step
b finish_ret_to_user
ret_to_user:
disable_daif //D A I F分别为PSTAT中的四个异常屏蔽标志位,此处屏蔽这4中异常。(irq这里理论上没有开,为什么还关?)
ldr x1, [tsk, #TSK_TI_FLAGS] //获取thread_info中的flags变量的值
and x2, x1, #_TIF_WORK_MASK //_TIF_WORK_MASK是一些列flags的集合,其中包括NEED_RESCHED|SIGPENDING|NOTIFY_RESUME等值
cbnz x2, work_pending //判断并跳转。此处判断若有_TIF_WORK_MASK的中任何flag都去执行work_pending
finish_ret_to_user:
enable_step_tsk x1, x2
#ifdef CONFIG_GCC_PLUGIN_STACKLEAK
bl stackleak_erase
#endif
kernel_exit 0
ENDPROC(ret_to_user)
asmlinkage void do_notify_resume(struct pt_regs *regs, /* 处理thread_info中pending的flag事件。
unsigned long thread_flags) 进入此函数,中断必须是关闭的。这里大概就是ret_to_user中再次调disable_daif的原因。
*/
{
do {
/* Check valid user FS if needed */
addr_limit_user_check();
if (thread_flags & _TIF_NEED_RESCHED) { //上来就判断_TIF_NEED_RESCHED,若置位则直接调schedule
/* Unmask Debug and SError for the next task */ //取消被屏蔽的其他异常,只关闭IRQ异常!
local_daif_restore(DAIF_PROCCTX_NOIRQ);
schedule(); //调度的核心函数,进行抢占。
} else {
......
} while (thread_flags & _TIF_WORK_MASK);
}
系统调用返回用户空间
系统调用返回用户空间和中断返回用户空间都是通过ret_to_user函数,所以判断执行抢占的部分是相同的。
/*文件:arch/arm64/kernel/entry.S
函数调用:
ret_to_user
work_pending
do_notify_resume
schedule()
*/
arm64中syscall通过同步异常el0_sync->el0_svc->el0_svc_handler处理完成后,ret_to_user返回用户空间。
参考中断返回用户空间。
主动调度:
当进程阻塞时,例如mutex,sem,waitqueue获取不到资源,在或者进程进行磁盘读写。这种情况下进程会将自己的状态从TASK_RUNNING修改为TASK_INTERRUPTIBLE或是TASK_UNINTERRUPTIBLE,然后调用schedule()主动让出CPU并等待唤醒。
/*以磁盘IO中的一处使用举例:
文件:fs/direct-io.c
*/
static struct bio *dio_await_one(struct dio *dio)
{
....
while (dio->refcount > 1 && dio->bio_list == NULL) {
__set_current_state(TASK_UNINTERRUPTIBLE); //先将当前进程的state设置为TASK_UNINTERRUPTIBLE不可唤醒状态。
dio->waiter = current;
spin_unlock_irqrestore(&dio->bio_lock, flags);
if (!(dio->iocb->ki_flags & IOCB_HIPRI) ||
!blk_poll(dio->bio_disk->queue, dio->bio_cookie, true))
io_schedule(); //调用io_schedule让出cpu,并等待io完成后被唤醒。
/* wake up sets us TASK_RUNNING */
.....
}
关于周期性调度器的简要说明:
前文我们说到,执行抢占时会检查TIF_NEED_RESCHED标志,以判断是否需要执行抢占。那TIF_NEED_RESCHED在什么地方被设置呢?其一是在进程被wakeup的时候,再一地方就是在周期性调度器(scheduler_tick)中。
时钟tick以每秒HZ次的频率发生,在时钟tick的中断处理函数里,会去调用周期性调度器scheduler_tick()检查并设置TIF_NEED_RESCHED和need_resched。而这本身就在一个中断里,当中断返回时又回去检查执行抢占。
/*函数调用:
.....
tick_sched_timer()
tick_sched_handle()
update_process_times()
scheduler_tick()
task_tick_fair()
entity_tick()
resched_curr()
set_tsk_need_resched(curr); //设置TIF_NEED_RESCHED
set_preempt_need_resched(); //设置need_resched,即PREEMPT_NEED_RESCHED
*/
scheduler_tick(){
sched_clock_tick(); //更新系统的时钟ticket
update_rq_lock(rq); //更新当前cpu就需队列中的时钟计数,struct rq中包含clock和clock_task两个runqueue的时间
curr->sched_class->task_tick(rq,curr,0); //调用对应调度类中的task_tick方法(每个调度类都有实现自己的task_tick)
//重点关注cfs中实现的task_tick_fair->entity_tick;分析如下面代码
//entity_tick中会检查是否需要抢占调度
cpu_load_update_active(rq);
trigger_load_balance(rq); //若是smp架构的话会去调负载均衡
}
void resched_curr(struct rq *rq)
{
.....
if (cpu == smp_processor_id()) {
set_tsk_need_resched(curr); //设置TIF_NEED_RESCHED
set_preempt_need_resched(); //设置need_resched,即PREEMPT_NEED_RESCHED
return;
}
......
}
最后
以上就是迷人短靴为你收集整理的Linux进程调度:调度时机的全部内容,希望文章能够帮你解决Linux进程调度:调度时机所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复