- 用户线程接受到时钟中断,强迫CPU从用户空间进入内核,同时trampoline保存当前寄存器代码到trapframe
- 在usertrap处理中断,调用yield函数
- yield先给当前t1线程加锁,将其状态设为RUNNABLE,后转入sched函数
- sched先进行警戒判断,再调用swtch,将当前reg保存下来,加载cpu调度线程的reg上去
- 进入到了scheduler函数,此时进入的位置是之前cpu中的context保存的ra的位置,即在scheduler调用swtch的下面一行代码。此时先把t1的锁解开,再进行遍历线程,找到下一个可调用的线程,调用swtch调用t2线程,并返回到内核线程所对应进程的系统调用或者中断处理程序中。
- 当内核程序执行完成之后,trapframe 中的用户寄存器会被恢复,完成线程调度。
看看代码:
// Give up the CPU for one scheduling round.
void
yield(void)
{
struct proc *p = myproc();
acquire(&p->lock);// 锁t1
p->state = RUNNABLE;
sched();//调度开始
release(&p->lock);// 释放的t2的锁
}
void
sched(void)
{
int intena;
struct proc *p = myproc();
if(!holding(&p->lock))
panic("sched p->lock");
if(mycpu()->noff != 1)
panic("sched locks");
if(p->state == RUNNING)
panic("sched running");
if(intr_get())
panic("sched interruptible");
intena = mycpu()->intena;
// 转到cpu调度线程,即scheduler中swtch中的下一行
swtch(&p->context, &mycpu()->context);
mycpu()->intena = intena;
}
注意此时的cpu的context中的ra,是保存的scheduler上一次调用swtch时保存下来的ra,即接下来scheduler执行的代码应该是scheduler函数中swtch调用位置的下一行
void
scheduler(void)
{
struct proc *p;
struct cpu *c = mycpu();
c->proc = 0;
for(;;){
// Avoid deadlock by ensuring that devices can interrupt.
intr_on();
int nproc = 0;
for(p = proc; p < &proc[NPROC]; p++) {
acquire(&p->lock);
if(p->state != UNUSED) {
nproc++;
}
if(p->state == RUNNABLE) {
p->state = RUNNING;
c->proc = p;
swtch(&c->context, &p->context);// 再切换t2
// 注意,这才是进入scheduler时第一步运行的位置
//sched经过swtch转到这一步
c->proc = 0;
}
release(&p->lock); // 先释放t1的锁
}
if(nproc <= 2) { // only init and sh exist
intr_on();
asm volatile("wfi");
}
}
}
我们可以看到在加载了cpu的context之后,第一步执行的时c->proc=0,接着执行release(&p->lock),此时的p还是t1,接下来再进行轮询线程,调用t2
来看看整个调用流程:
图来自知乎老哥,画的太清楚了!
还少一个在最后的yield中释放t2的锁
总结下来,对线程调度就清晰很多了!