xv6的进程切换是需要在内核中切换的,一个进程通过定时器中断
陷入操作系统时,usertrap处理程序检测到是定时器中断,说明该进程需要让出
CPU的使用权,此时需要经历以下过程:
- yield函数:
获取当前CPU所运行进程信息,并将其状态改成RUNNABLE
,表明该进程之后会被运行,调用sched
函数
- sched函数:
调用swtch
函数,来保存当前CPU运行进程的context
信息到进程的proc结构体中,并且从CPU调度进程中获取专属该CPU的context信息到寄存器中,有了该消息即可切换到CPU的调度进程
- swtch函数:
swtch函数其实是由汇编实现的,其中就负责两件事情:保存和加载(save and load)
,保存将当前的被调用者保存的寄存器放到当前进程的proc结构体中保存,而加载则是将CPU专属的context信息加载到被调用者保存的寄存器。
- 此时一个合适的问题是下一步程序要怎么走?
这得看ra寄存器(可能是return address?)
保存的是什么了,其实这里是scheduler中的指令地址,在swtch ret指令之后pc就会取ra寄存器的指令地址
- 上面一直说的CPU调度进程又是什么?
实际上CPU调度进程是在main.c中调用的scheduler
函数,这个进程在完成初始化之后就将执行一个不停止的for循环
- scheduler函数(线程安全):
scheduler函数会循环遍历,直到找到第一个可以运行的进程,此时同样需要一个swtch
来保存当前的寄存器到cpu所属的CPU结构体的context中,并从可以运行的进程中加载context到寄存器,从而恢复进程的运行!
至此便完成了一次完整的调度。实际上,上面的过程中最需要理解的就是切换寄存器就可以切换运行的进程!
其实这里还有两个问题值得注意:
- 一个新进程是怎么运行起来的?
- 首先被
fork
创建,初始化其state
为RUNNABLE - 调度器获取该进程的lock,并且通过swtch运行该进程,此时ra是swtch后的一个指令
- 对于一个全新的进程,此时scheduler第一次调度是返回到forkret这个函数,来完成一些初始化和锁的释放操作,随后再让这个新进程运行
- 锁是否会在调度中传递?
对于一个旧进程,回到scheduler的时候其实是回到scheduler中swtch后的那个指令
,且此时是带着锁
(进程控制块的锁)回来的,因此需要在scheduler中释放锁。
对于下一个进程而言,进程控制块的锁在scheduler中被设置,因此和整个流程是可以完整嵌合的(也就是说在swtch的时候锁的申请与释放并没有出现矛盾)。此时的内层for循环会从此刻位置开始继续往后找可运行的进程,不会出现进程设置成runnable之后又马上运行的情况,保证CPU的时间被所有线程均分!