headPic

6.S081:xv6 进程调度

OAP

xv6的进程切换是需要在内核中切换的,一个进程通过定时器中断陷入操作系统时,usertrap处理程序检测到是定时器中断,说明该进程需要让出CPU的使用权,此时需要经历以下过程:

  1. yield函数:

获取当前CPU所运行进程信息,并将其状态改成RUNNABLE,表明该进程之后会被运行,调用sched函数

  1. sched函数:

调用swtch函数,来保存当前CPU运行进程的context信息到进程的proc结构体中,并且从CPU调度进程中获取专属该CPU的context信息到寄存器中,有了该消息即可切换到CPU的调度进程

  1. swtch函数:

swtch函数其实是由汇编实现的,其中就负责两件事情:保存和加载(save and load),保存将当前的被调用者保存的寄存器放到当前进程的proc结构体中保存,而加载则是将CPU专属的context信息加载到被调用者保存的寄存器。

  1. 此时一个合适的问题是下一步程序要怎么走?

这得看ra寄存器(可能是return address?)保存的是什么了,其实这里是scheduler中的指令地址,在swtch ret指令之后pc就会取ra寄存器的指令地址

  1. 上面一直说的CPU调度进程又是什么?

实际上CPU调度进程是在main.c中调用的scheduler函数,这个进程在完成初始化之后就将执行一个不停止的for循环

  1. scheduler函数(线程安全):

scheduler函数会循环遍历,直到找到第一个可以运行的进程,此时同样需要一个swtch来保存当前的寄存器到cpu所属的CPU结构体的context中,并从可以运行的进程中加载context到寄存器,从而恢复进程的运行!

至此便完成了一次完整的调度。实际上,上面的过程中最需要理解的就是切换寄存器就可以切换运行的进程!

其实这里还有两个问题值得注意:

  1. 一个新进程是怎么运行起来的?
  • 首先被fork创建,初始化其state为RUNNABLE
  • 调度器获取该进程的lock,并且通过swtch运行该进程,此时ra是swtch后的一个指令
  • 对于一个全新的进程,此时scheduler第一次调度是返回到forkret这个函数,来完成一些初始化和锁的释放操作,随后再让这个新进程运行
  1. 锁是否会在调度中传递?

对于一个旧进程,回到scheduler的时候其实是回到scheduler中swtch后的那个指令,且此时是带着锁(进程控制块的锁)回来的,因此需要在scheduler中释放锁。

对于下一个进程而言,进程控制块的锁在scheduler中被设置,因此和整个流程是可以完整嵌合的(也就是说在swtch的时候锁的申请与释放并没有出现矛盾)。此时的内层for循环会从此刻位置开始继续往后找可运行的进程,不会出现进程设置成runnable之后又马上运行的情况,保证CPU的时间被所有线程均分!