进程调用的时机
自愿调度:用户进程自愿发生调度,如用户进程调用wait4()和exit()时,内核进程 调用schedule()函数发生调度(schedule()只有内核线程才能调用)。
非自愿调度:当发生中断或者系统调用时,从系统空间返回到用户空间时,检查进程的task_struct结构的need_resched变量是否为1,,为1就需要调度,否则就不需要调度(所以可是说从系统空间返回到用户空间是发生调度的必要条件,而非充分条件)
注释:我们知道当在执行中断服务程序的时候是可以开中断的,当正在执行中断服务历程的时候,又有中断达到时,就发生嵌套中断,而我们也许会问中断返回时不是要发生调度吗,但是这里是不会发生调度的(因为嵌套的中断返回时还是返回到外层中断,还是系统空间,并没有返回到用户空间)
调度方式
linux进程调度的方式采用的是“”有条件的可剥夺”的方式,当用户空间的进程的运行时间的时间片到达之后,就要强制发生调度,让根据优先级选择其他进程占用cpu运行,但是一旦内核线程占用了cpu,它就具有最高的权限,没人调度它出去,于是只有内核线程执行完毕主动让出cpu时,其他线程才有机会运行,所以将他称之为“有条件的可剥夺的”调度方式。
上述调度方式明细有不公平的现象,比如有实时的进程到达时,而此时如果有内核线程占用cpu不放时,而那些任务非常急的任务讲得不到执行,于是有的内核是可以抢占的,但是有三个例外是不能发生抢占的,如下:
1、 该进程正在调用函数schedule()就不可在发生调度
2、如果该进程正在占用共享的资源的读写锁或者自旋锁就不能将它调度出去,否则当新进程调度进来的时候在试图去共享资源加锁时,会一直获取不到锁而阻塞,从而发生了死锁现象,所以这种情况是不允许抢占的
3、如果此时内核执行的是中断服务程序枪战时就会打印错误报告(在schedule()函数中实现)
而进程的thread_info 结构中有一个变量preeprpt,当内核中的进程正在处于以上三种的情况下,就会将这个变量置成1 ,否则就是0
调度策略
linu将进程分为了三类:
交互式进程:这种进程要求多个进程公用一个os时,各个应用感觉不到延迟,例如:当用户从键盘输入的时候,cpu要能给出响应显示出来,当延迟大于150ms时用户会感觉到明显的延时
批处理进程:这种进程主要就是后台进程的运行,有延迟也无所谓
实时式进程:这种进程不仅要求能立即给出响应,而且还要能立即调度执行
三种调度策略:
SCHED_FIFO:这种策略适用实时式进程,占用cpu时间短,他给这种类型的进程的优先级加1000
SCHED_RR:这种策略适用于占用cpu和时间比较长的进程
SCHED_OTHER:这种就是传统的调度策略,适合交互式进程
这些策略在进程的task_struct中的policy变量中体现
然后就是根据各个就绪进程的优先级来选择一个下一个进程(goodness()来选择)
进程的切换
虚拟空间的切换:
1、如果下一个进程内核线程,那么内核线程就没有mm_struct,而内核要求一个用户进程的两个mm_struct结构体指针mm、active_mm都指向同一个mm_struct,而内核线程虽然没有用户空间,但是其active_mm一定是0,但是内核要求内核线程的active_mm不能为0,所以就必须向上一个用户进程借一个mm_struct(有人会问向上一个进程借一个mm_strcut可以使用吗,这里是肯定的,因为内核线程没有用户空间,而所有的进程的mm_struct的内核空间部分都是映射到同一个页表。那么什么时候归还呢,当然就是当这个进程被调度出去的时候归还了)
2、如果下一个进程为用户进程,那么就有自己的mm_struct,此时就要切换虚拟存储空间,对于单核cpu就是将CR3寄存器指向新进程的页目录的地址(switch_mm()实现)
堆栈的切换:
#define switch_to(prev,next,last) do { \ unsigned long esi,edi; \ /** * 在真正执行汇编代码前,已经将prev存入eax,next存入edx中了。 * 没有搞懂gcc汇编语法,反正结果就是这样。 * 应该是"2" (prev), "d" (next)这句的副作用。 */ /** * 保存eflags和ebp到内核栈中。必须保存是因为编译器认为在switch_to结束前, * 它们的值应当保持不变。 */ asm volatile("pushfl\n\t" \ "pushl %%ebp\n\t" \ /** * 把esp的内容保存到prev->thread.esp中 * 这样该字段指向prev内核栈的栈顶。 */ "movl %%esp,%0\n\t" /* save ESP */ \ /** * 将next->thread.esp装入到esp. * 此时,内核开始在next的栈上进行操作。这条指令实际上完成了从prev到next的切换。 * 由于进程描述符的地址和内核栈的地址紧挨着,所以改变内核栈意味着改变当前进程。 */ "movl %5,%%esp\n\t" /* restore ESP */ \ /** * 将标记为1f的地址存入prev->thread.eip. * 当被替换的进程重新恢复执行时,进程执行被标记为1f的那条指令。 */ "movl $1f,%1\n\t" /* save EIP */ \ /** * 将next->thread.eip的值保存到next的内核栈中。 * 这样,_switch_to调用ret返回时,就会跳转到next->thread.eip执行。 * 这个地址一般情况下就会是1f. */ "pushl %6\n\t" /* restore EIP */ \ /** * 注意,这里不是用call,是jmp,这样,上一条语句中压入的eip地址就可以执行了。 */ "jmp __switch_to\n" \ /** * 到这里,进程A再次获得CPU。它从栈中弹出ebp和eflags。 */ "1:\t" \ "popl %%ebp\n\t" \ "popfl" \ :"=m" (prev->thread.esp),"=m" (prev->thread.eip), \ /* last被作为输出参数,它的值会由eax赋给它。 */ "=a" (last),"=S" (esi),"=D" (edi) \ :"m" (next->thread.esp),"m" (next->thread.eip), \ "2" (prev), "d" (next)); \ } while (0)
switch_to()(用嵌入式汇编实现的宏定义)
1、首先他将上一个进程的%esi%edi%ebp压入自己的内核堆栈中
2、他将自己的内核堆栈指针%esp保存在自己的PCB中的thread.esp中,然后将下一个进程中的esp置于ESP寄存器中
3、然后他将pop第一个三个寄存器的命令地址保存在自己的PCB中的thread.eip中,这样当下一次调度的时候,就可以从哪里开始恢复寄存器的值,然后就将下一个进程的存储的eip置入EIP寄存器中,这个进程就可以从这里开始执行
4、但是重置完EIP寄存器的值后还要调用一个_switch_to()函数将TSS中的esp0(内核空间堆栈指针,当cpu穿越中断门等会根据当前进程的运行级别从TSS中取得系统空间堆栈的指针)设置成下一个进程的堆栈指针同时还要修改IO权位等。(新旧进程的接入点就是_switch_to()这个函数,他完成以后就是进程在运行了)