1. 调度时机
调度时机一般可以分成两类:主动调度和强制调度。
1.1 主动调度
-
在形式上一般是这样的:
内核在等待资源的时候,将当前进程移到等待队列,并主动调用schedule()放弃CPU;
-
主动调度的例子:
read()系统调用,会调用到wait_on_sync_kiocb(),其中有这么一段
while (iocb->ki_users) {set_current_state(TASK_UNINTERRUPTIBLE);if (!iocb->ki_users)break;schedule();}
大致就是,资源还未准备的话,就主动调用schedule()放弃CPU;
1.2 强制调度
-
在形式上一般是这样的:
-
在系统调用 / 中断处理中设置TIF_NEED_SCHED;
-
系统调用返回用户态 / 中断返回(可能返回内核态)前,
检查TIF_NEED_SCHED(中断返回内核态还要检查preempt_count),
如果进行了设置,则在返回前调用schedule();
-
-
强制调度的例子:
-
IO中断的例子,
阻塞read()的IO资源读取完成,进入中断处理。
在中断处理中,调用try_to_wake_up()的封装函数,设置TIF_NEED_SCHED;
中断返回后(假设是返回到用户态),检查到TIF_NEED_SCHED被设置,调用schedule();
-
时钟中断的例子,
在时钟中断的中断处理中,调用scheduler_tick(),
最简单的情况是当普通进程的时间片耗尽后,设置TIF_NEED_SCHED;
中断返回后(假设是返回到用户态),检查到TIF_NEED_SCHED被设置,调用schedule();
-
系统调用的例子,
信号机制中,想发送信号到某个进程时,会调用specific_send_sig_info()。
在specific_send_sig_info()中,最后会调用signal_wake_up(),设置TIF_NEED_SCHED;
(底层还是try_to_wake_up())
系统调用返回后,检查到TIF_NEED_SCHED被设置,调用schedule();
-
1.3 详细调度时机总结
基本摘自此博文,文章是讲抢占的,但是抢占就是调度的一种。
Linux用户抢占和内核抢占详解(概念, 实现和触发时机)–Linux进程的管理与调度(二十)
- 内核主动放弃CPU,具体情况就类似于上面所介绍的read()系统调用;
- 时钟中断处理例程检查当前任务的时间片,当任务的时间片消耗完时,scheduler_tick()函数就会设置need_resched标志;
- 信号量、等到队列、completion等机制唤醒时都是基于waitqueue的,而waitqueue的唤醒函数为default_wake_function,其调用try_to_wake_up将被唤醒的任务更改为就绪状态并设置need_resched标志。
- 设置用户进程的nice值时,可能会使高优先级的任务进入就绪状态;
- 改变任务的优先级时,可能会使高优先级的任务进入就绪状态;
- 新建一个任务时,可能会使高优先级的任务进入就绪状态;
- 对CPU(SMP)进行负载均衡时,当前任务可能需要放到另外一个CPU上运行;
2. 抢占概述
2.1 何为抢占
抢占就是强制调度,
也就是一般是系统调用 / 中断返回前,如在处理中设置了TIF_NEED_SCHED,那么就会触发的一种调度。
上面已经进行了大致的介绍。
其实说法不少,也看过把主动放弃CPU也视作抢占的一种。
我是觉得主动放弃这种拦不住的情况,不应该叫被抢占吧,
抢占定义为可被TIF_NEED_RESCHED和preempt_count拦下来的强制调度,感觉比较合适?
抢占可分为:
-
用户抢占,一般在系统调用返回 / 中断返回到用户态时被触发;
-
内核抢占,一般在中断返回到内核态时被触发。
在最原始的单核处理器下只要关闭中断就不会发生内核抢占,其他架构的细节尚未研究清楚;
2.2 内核抢占的限制
用户抢占感觉没什么好说,基本就是强制调度。
内核抢占是比较特殊的,限制比用户抢占要多,两者的控制变量为:
-
用户抢占,TIF_NEED_RESCHED;
-
内核抢占,preempt_count和TIF_NEED_RESCHED,
也就是内核多一层是否抢占的检测,
preemot_count为0的时候表示开启抢占,总体情况比较多,细节尚未研究清楚;
而且也是个可选项,在编译内核时可以选择是否启用对内核抢占的支持。
内核在这些情况下是不允许被抢占的:
引自:Linux用户抢占和内核抢占详解(概念, 实现和触发时机)–Linux进程的管理与调度(二十)
-
内核正进行中断处理。在Linux内核中进程不能抢占中断(中断只能被其他中断中止、抢占,进程不能中止、抢占中断),在中断例程中不允许进行进程调度。进程调度函数schedule()会对此作出判断,如果是在中断中调用,会打印出错信息;
-
内核正在进行中断上下文的Bottom Half(中断下半部,即软中断)处理。硬件中断返回前会执行软中断,此时仍然处于中断上下文中。如果此时正在执行其它软中断,则不再执行该软中断;
-
内核的代码段正持有spinlock自旋锁、writelock/readlock读写锁等锁,处干这些锁的保护状态中。内核中的这些锁是为了在SMP系统中短时间内保证不同CPU上运行的进程并发执行的正确性。当持有这些锁时,内核不应该被抢占;
-
内核正在执行调度程序schedule()。抢占的原因就是为了进行新的调度,没有理由将调度程序抢占掉再运行调度程序;
-
内核正在对每个CPU“私有”的数据结构操作(Per-CPU date structures)。在SMP中,对于per-CPU数据结构未用spinlocks保护,因为这些数据结构隐含地被保护了(不同的CPU有不一样的per-CPU数据,其他CPU上运行的进程不会用到另一个CPU的per-CPU数据)。但是如果允许抢占,但一个进程被抢占后重新调度,有可能调度到其他的CPU上去,这时定义的Per-CPU变量就会有问题,这时应禁抢占;
笔者暂时水平还是太有限,上文中的抢占对不可重入函数的影响,还有内核同步方面的介绍,
还不怎么看得懂,暂时只理解了大意。
3. 从中断和异常中返回代码分析
笔者水平有限,
本节主要引自:内核随记(二)——内核抢占与中断返回
我们会听说:
- 从中断返回后,发生抢占或者信号处理等;
- 从系统调用返回后,发生抢占或者信号处理等;
本节就是借由大佬对返回处理的源码分析,大致了解一下这些处理流程。
3.1 从中断返回
当内核从中断返回时,应当考虑以下几种情况:
- 内核控制路径并发执行的数量:如果为1,则CPU返回用户态;
- 挂起进程的切换请求:如果有挂起请求,则进行进程调度;否则,返回被中断的进程;
- 待处理信号:如果有信号发送给当前进程,则必须进行信号处理;
- 单步调试模式:如果调试器正在跟踪当前进程,在返回用户态时必须恢复单步模式;
- Virtual-8086模式:如果中断时CPU处于虚拟8086模式,则进行特殊的处理;
#从中断返回
ret_from_intr:GET_THREAD_INFO(%ebp)movl EFLAGS(%esp), %eax # mix EFLAGS and CSmovb CS(%esp), %altestl $(VM_MASK | 3), %eax #是否运行在VM86模式或者用户态/*中断或异常发生时,处于内核空间,则返回内核空间;否则返回用户空间*/jz resume_kernel # returning to kernel or vm86-space
3.2 返回用户态
/***返回用户空间,只需要检查need_resched**可能从中断返回,也可能从系统调用返回。*/
ENTRY(resume_userspace) #返回用户空间,中断或异常发生时,任务处于用户空间cli # make sure we don't miss an interrupt# setting need_resched or sigpending# between sampling and the iretmovl TI_flags(%ebp), %ecxandl $_TIF_WORK_MASK, %ecx # is there any work to be done on# int/exception return?jne work_pending #还有其它工作要做jmp restore_all #所有工作都做完,则恢复处理器状态#恢复处理器状态
restore_all:RESTORE_ALL# perform work that needs to be done immediately before resumptionALIGN#完成其它工作
work_pending:testb $_TIF_NEED_RESCHED, %cl #检查是否需要重新调度jz work_notifysig #不需要重新调度#需要重新调度
work_resched:call schedule #调度进程cli # make sure we don't miss an interrupt# setting need_resched or sigpending# between sampling and the iretmovl TI_flags(%ebp), %ecx/*检查是否还有其它的事要做*/andl $_TIF_WORK_MASK, %ecx # is there any work to be done other# than syscall tracing?jz restore_all #没有其它的事,则恢复处理器状态testb $_TIF_NEED_RESCHED, %cljnz work_resched #如果need_resched再次置位,则继续调度
#VM和信号检测
work_notifysig: # deal with pending signals and# notify-resume requeststestl $VM_MASK, EFLAGS(%esp) #检查是否是VM模式movl %esp, %eaxjne work_notifysig_v86 # returning to kernel-space or# vm86-spacexorl %edx, %edx#进行信号处理call do_notify_resumejmp restore_allALIGN
work_notifysig_v86:pushl %ecx # save ti_flags for do_notify_resumecall save_v86_state # %eax contains pt_regs pointerpopl %ecxmovl %eax, %espxorl %edx, %edxcall do_notify_resume #信号处理jmp restore_all
3.3 返回内核态
#ifdef CONFIG_PREEMPT
/*返回内核空间,先检查preempt_count,再检查need_resched*/
ENTRY(resume_kernel)/*是否可以抢占,即preempt_count是否为0*/cmpl $0,TI_preempt_count(%ebp) # non-zero preempt_count ?jnz restore_all #不能抢占,则恢复被中断时处理器状态need_resched:movl TI_flags(%ebp), %ecx # need_resched set ?testb $_TIF_NEED_RESCHED, %cl #是否需要重新调度jz restore_all #不需要重新调度testl $IF_MASK,EFLAGS(%esp) # 发生异常则不调度jz restore_all#将最大值赋值给preempt_count,表示不允许再次被抢占movl $PREEMPT_ACTIVE,TI_preempt_count(%ebp)sticall schedule #调度函数climovl $0,TI_preempt_count(%ebp) #preempt_count还原为0#跳转到need_resched,判断是否又需要发生被调度jmp need_resched
#endif
3.4 从异常返回
现在还不太了解。
#从异常返回ALIGN
ret_from_exception:preempt_stop /*相当于cli,从中断返回时,在handle_IRQ_event已经关中断,不需要这步*/
3.5 从系统调用返回
#系统调用入口
ENTRY(system_call)pushl %eax # save orig_eaxSAVE_ALLGET_THREAD_INFO(%ebp)# system call tracing in operationtestb $(_TIF_SYSCALL_TRACE|_TIF_SYSCALL_AUDIT),TI_flags(%ebp)jnz syscall_trace_entrycmpl $(nr_syscalls), %eaxjae syscall_badsys
syscall_call:#调用相应的函数call *sys_call_table(,%eax,4)movl %eax,EAX(%esp) # store the return value,返回值保存到eax
#系统调用返回
syscall_exit:cli # make sure we don't miss an interrupt# setting need_resched or sigpending# between sampling and the iretmovl TI_flags(%ebp), %ecxtestw $_TIF_ALLWORK_MASK, %cx # current->work,检查是否还有其它工作要完成jne syscall_exit_work
#恢复处理器状态
restore_all:RESTORE_ALL#做其它工作
syscall_exit_work:#检查是否系统调用跟踪,审计,单步执行,不需要则跳到work_pending(进行调度,信号处理)testb $(_TIF_SYSCALL_TRACE|_TIF_SYSCALL_AUDIT|_TIF_SINGLESTEP), %cljz work_pendingsti # could let do_syscall_trace() call# schedule() insteadmovl %esp, %eaxmovl $1, %edx#系统调用跟踪call do_syscall_trace#返回用户空间jmp resume_userspace
参考
yooooooo Linux用户抢占和内核抢占详解(概念, 实现和触发时机)–Linux进程的管理与调度(二十)
YY哥 内核随记(二)——内核抢占与中断返回
《深入理解linux内核》