🗒️Xv6-Scheduling

2023-4-23|2023-5-22
Anthony
Anthony
type
status
date
slug
summary
tags
category
icon
password
本Blog基于任职于美国波特兰洲立大学的教授@hhp3
在Youtube上对xv6内核的讲解视频
本期讲解xv6中的调度,视频链接在下面
 

前置知识

  • kernel/proc.c中的cpuid()函数,该函数返回tp寄存器的值,该值代表了当前正在运行的cpu 正如注释所说,该函数在中断结束的时候应该被调用,以便确认CPU仍然有效。
 
  • mycpu()函数返回一个指向当前cpu的结构体的指针
 
  • myproc()函数返回一个指针,指向在当前cpu中运行的进程的结构体。 myproc()可以并且可能从任何地方被调用 我们使用push_off()来禁用中断的产生,随后获取cpuid,然后使用pop_off()来允许中断的产生,这也印证了之前所说的调用mycpu的时候要伴随着中断地禁用

函数

swtch.s()
该函数接收一个旧的context指针,一个新的context指针。我们可以想象从进程P切换到CPU的时候,旧的context就是P的寄存器状态,新的context就是CPU的寄存器状态。
swtch虽然是汇编,但是内容很简单,即保存调用者参数到a0开始处,加载新的参数到a1。
值得注意的是,swtch并不直接返回原处。用刚刚的RoadMap说明则是我们在Yield()的时候调用swtch,但是随后我们就进入到了CPU的调度,也就是swtch返回到了其他地方。而在进程P被重新选择之后,swtch在CPU调度中被调用,但是却在进程P中返回。
 
Yield()
yield意思是放弃当前正在控制的CPU,即让权。它获取当前进程的spinlock ,将state 变成RUNNABLE然后调用sched()
让权之后Yield会一直等待着sched()的返回(也就是其他进程执行完时间片之后)
💡
Yield有可能从usertrap()调用,也有可能从kerneltrap()调用。但无论如何,当yield()被调用的时候,中断将会失效。
 
sched()
首先获取当前进程的指针,然后进行一系列的测试。包括
  • 我们拥有着该进程的spinlock
  • 确保noff是1,这意味着这是由yield()产生的中断
  • 确保当前进程是没有在运行
  • 确保当前中断是不被允许的(失效的)
接下来使得intena为false(如果是被yield()调用,一定是false),但是从别的地方调用,intena有可能是true。
接下来调用swtch,保存现场。
 
scheduler()
获取指向当前CPU的指针。
进入一个无限循环,遍历proc数组找到进程,获取该进程的锁,判断该进程是否可用,如果可以,那就将该进程状态变成RUNNING,然后将c→proc指向它,调用swtch。如果不是我们就释放锁,继续遍历。
调用swtch中,保存CPU的现场,然后加载新选中的线程的现场。当swtch返回时,即我们不再执行该进程,我们将cpu→proc变成0,释放该进程的锁。

Road Map

notion image
现在想象一个场景,用户态代码正在执行,此时我们遇到了一个时间片类型的中断,此时,yield()函数将会首先被调用,当yield()返回时,我们才能继续执行用户代码。需要注意的是在执行yield()的过程中,中断是不被允许的。
 
现在,我们可以完整地分析出一个时间片中断发生了什么。
  1. Trap发生,并且我们确定它不是一个系统调用,也不是设备输入,此时我们可以判断是时间片中断,于是yield()函数被调用。
  1. yield() 函数将会调用sched()函数,sched()函数中简单地执行一些声明语句,然后调用swtch() 进入处理器线程
    1. 首先获取进程P的spinlock 然后将进程P的状态设置成RUNNABLE
    2. 保存cpu→intena (interrupt enable?)
  1. 在处理器线程中我们将会执行一些其他操作。比如说把时间片给别的进程
    1. cpu→proc设置成null
    2. 释放线程P的spinlock
    3. 。。。。在线程P睡眠一段时间之后,终于又等到了它的时间片。。。。
    4. 获取线程P的spinlock
    5. 将线程P的状态设置成RUNNING
    6. cpu→proc = p
  1. 当处理器线程重新选择会进程P,处理器又调用了swtch() 。此时我们应该要会想起之前P被中断时yield()被调用,同时yield()调用了sched()sched()调用了swtch()。当处理器调用完swtch(),切换回进程P时,我们的返回地点就是sched()调用的swtch()之后。此时sched()得以返回,然后回到yield(),然后恢复用户态,返回。
第一步和第二部分在进程中执行(processer thread)。中断发生,我们的执行环境从用户态变为了内核态。当swtch()函数被调用,我们从将cpu控制权从进程切换到了处理器.
 
需要注意的是,无论是在cpu的结构体中还是在进程的结构体中,都存在着一个保存寄存器的区域。在调用swtch的时候,进程P的所有寄存器状态会被保存,同时cpu的寄存器状态将会被加载。当从cpu回到进程P的时候,cpu的寄存器状态将会被保存,进程P的寄存器状态将会被加载。
 

讨论

swtch事实上在两个地方被调用
  1. 从进程切换到CPU(此时进程状态被保存)
  1. 从CPU切换到进程(此时CPU状态被保存)
 
acquire lock 和 release lock总是两两配对,但是同一把锁的获取和释放真的是在同一个进程中吗?不是
事实上在中断一个进程P的时候,我们获取了它的锁,然后切换到了CPU,在此时我们就会释放P的锁!(我们不能太久地掌握着一个锁)。在CPU选定一个新的进程之前,我们先获取了它的锁,再判断该进程Ok之后,我们切换到进程P, 此时进程P就会释放锁!
那么怎么保证这个锁是同一个锁呢?
 
为什么要有intr_on()来避免死锁
一个很有意思的事情是,在选择可用进程的时候,中断是不被允许的,只有在进入到新进程之后,中断才会被允许。因此如果系统中没有可用的进程,所有的进程都没有准备好,那么CPU有可能就会一直等待,中断就一直不会被允许。因此我们如果遍历所有进程,发现没有可用进程,我们就要用intr_on()来重新允许中断。
Xv6-KllocXv6-VM