一、主动调度
调用schedule函数进行主动调度,其具体流程比较简单,需要掌握调度类,调度队列,调度实体以及他们和CPU之间的关系,这些知识在上一篇博文《函数堆栈与进程调度基础》中进行了一定简单的介绍。
简言之,当调用schedule函数进行主动调度时,首先会调用通过调度类找到下一个要被调度的进程,然后将当前进程切换状态放入对应调度类的调度队列里面,等待再次被唤醒。而对于被调度的这个队列我们就要对其进行上下文切换。
二、进程上下文切换
上下文切换主要干下面两件事,是通过context_switch
函数实现的:
- 切换进程空间,即虚拟内存
- 切换寄存器和CPU上下文,即保存寄存器等当前进程相关变量
static __always_inline struct rq *
context_switch(struct rq *rq, struct task_struct *prev,
struct task_struct *next, struct rq_flags *rf)
{//linux-4.13.16\kernel\sched\core.c
struct mm_struct *mm, *oldmm;
...
mm = next->mm;
oldmm = prev->active_mm;
...
switch_mm_irqs_off(oldmm, mm, next);//熟悉吧,对于进程空间的切换
...
/* Here we just switch the register state and the stack. */
switch_to(prev, next, prev);//寄存器和栈空间的切换,即上面说的切换寄存器和保护上下文
barrier();//编译器优化指令,保证执行顺序的不变性
return finish_task_switch(prev);
}
关于进程切换的小总结:
- 在上面
switch_mm_irqs_off
进行内存切换时,通过将要切换进程的顶级页表项放在CPU的Cr3寄存器中,实现了用户空间的切换。 - 内核栈的切换其实还是需要上一讲的知识作为铺垫,因为task_struct里面有一个成员变量就指向进程的内核栈,需要注意的是内核栈是位于内核态的直接映射区的。
- 至于内核栈的栈顶指针,它会在进程切换时将其值加载到cpu对应的TSS(任务状态段)中,CPU有一个寄存器TR(task register)就是指向这个TSS段的,使用时直接通过这个指针就可以得到了。
- 而用户态的栈顶指针等一些寄存器变量他会保存在我们上一讲刚刚讲过的内核栈中的pt_regs结构体中,这样当执行用户态时就会恢复进程的上下文情况。。。
到目前为止,若我们了解计算机组成原理的话就会发现似乎还没有提及指令指针寄存器(IP)的变化,那它是怎样变化的哩? - 巧妙的IP寄存器的值变迁,太妙了,这方面极客时间刘超老师的《Linux操作系统调度篇》讲的是真的清楚,他总结的进程调度第一原理——“进程切换时都会调用__schedule的机理”,是顺利理解IP寄存器变迁的良药,超级推荐。
注意:可以使用ps aux
命令查看进程运行时间等基本信息。
三、抢占式调度
主动调度指是某进程主动调用了schedule函数,那么下面是一些发生抢占式调度的时机:
- 每当系统触发一个时钟中断时就会调用中断处理函数,里面会尝试进行抢占式调度
- 当一个由于IO而被挂起的进程由于受到IO来了的信号而被唤醒时就会比较与当前正在运行进程的优先级,若被唤醒的IO进程优先级更高就会触发抢占式中断。
注意:以上两种所谓的抢占式调度只是将当前进程标记为了应该被抢占,但还未真正的被抢占,因为最终只有调用了__shcedule才可以被抢占
那么什么时候才会调用__shcedule进行真正的抢占式调度哩,具体会分有用户态抢占和内核态抢占时机?
1.用户态抢占时机
- 进程从系统调用返回用户态时侯是触发调用schedule的一个时机
- 对用户态进程而言,从中断返回的那个时刻,也是一个被抢占的时机
2.内核态抢占时机
- 内核定义有一个宏
preempt_enable
,每当在内核进程进行某些操作之前会先调用该宏判断是否有需要发生抢占的进程(上面对应的发生情况(tick,io线程)会用一个变量做标记),若有的话就会调用shcedule进行调度。 - 同样和用户态一样,当发生中断返回时也会尝试进行schedule,完成抢占式调度的最后一步。
四、总结
一张非常好看的图片,源:趣谈Linux操作系统:抢占式调度是如何发生的?
转载:https://blog.csdn.net/qq_41345173/article/details/104600413
查看评论