xv6在sleep和wakeup的时候会发生进程切换,而且会周期性地在当进程在用户空间时发生进程切换。
上下文切换过程
上图展示了进程切换时的上下文切换的过程,首先一个用户进程通过中断或者系统调用陷入内核,此时发生用户态和内核态的切换,进程切换到自己的内核栈,再接着切换到上下文切换到调度器专属的内核栈,最后从调度器的内核栈切换到另一个用户进程的内核栈,最后从该内核栈返回用户态,即完成切换到了另一个用户进程。
在如下的sched函数中,就发生了上述中的过程,在sched中调用了swtch函数,可以看到传入swtch函数的参数有&p->context和mycpu()->scheduler,前者代表当前进程的上下文,而后者代表了当前cpu所对应的调度器的上下文。
// Enter scheduler. Must hold only ptable.lock
// and have changed proc->state. Saves and restores
// intena because intena is a property of this
// kernel thread, not this CPU. It should
// be proc->intena and proc->ncli, but that would
// break in the few places where a lock is held but
// there's no process.
void
sched(void)
{
int intena;
struct proc *p = myproc();
if(!holding(&ptable.lock))
panic("sched ptable.lock");
if(mycpu()->ncli != 1)
panic("sched locks");
if(p->state == RUNNING)
panic("sched running");
if(readeflags()&FL_IF)
panic("sched interruptible");
intena = mycpu()->intena;
swtch(&p->context, mycpu()->scheduler);
mycpu()->intena = intena;
}
接着看swtch函数,这个函数就是用来做上下文切换的,代码如下。可见其首先将传入的参数的地址保存入寄存器,接着保存当前现场,并将现场的指针保存在传入的参数old中(movl %esp, (%eax)),而接着发生上下文切换,即切换到传入的new参数的上下文中,在上述的sched函数中就是调度器的上下文(movl %edx, %esp),最后将上下文写入寄存器,完成切换。
# Context switch
#
# void swtch(struct context **old, struct context *new);
#
# Save the current registers on the stack, creating
# a struct context, and save its address in *old.
# Switch stacks to new and pop previously-saved registers.
.globl swtch
swtch:
movl 4(%esp), %eax
movl 8(%esp), %edx
# Save old callee-saved registers
pushl %ebp
pushl %ebx
pushl %esi
pushl %edi
# Switch stacks
movl %esp, (%eax)
movl %edx, %esp
# Load new callee-saved registers
popl %edi
popl %esi
popl %ebx
popl %ebp
ret
调度
进程调度就是调度器会不断地选择进程执行。主要发生的就是进程上下文和调度器上下文的切换。
进程会根据当前的时钟中断主动地放弃CPU,即调用yield函数,代码如下。
// Force process to give up CPU on clock tick.
// If interrupts were on while locks held, would need to check nlock.
if(myproc() && myproc()->state == RUNNING &&
tf->trapno == T_IRQ0+IRQ_TIMER)
yield();
接着进入yield函数,它主要就是调用了sched函数,sched函数中调用了swtch函数,注意在调用swtch函数之前必须保证已经获得ptable.lock,这是为了保证当进程把自己的状态设置为RUNNABLE后,在上下文切换的过程中没有其他CPU上的调度器可以执行它。
// Give up the CPU for one scheduling round.
void
yield(void)
{
acquire(&ptable.lock); //DOC: yieldlock
myproc()->state = RUNNABLE;
sched();
release(&ptable.lock);
}
当在sched中执行完了swtch后,此时就切换到了调度器程序,即scheduler,代码如下,调度器中就是不断遍历进程列表,找到可执行的进程,接着进行上下文切换,核心就是它调用swtch函数的过程。
//PAGEBREAK: 42
// Per-CPU process scheduler.
// Each CPU calls scheduler() after setting itself up.
// Scheduler never returns. It loops, doing:
// - choose a process to run
// - swtch to start running that process
// - eventually that process transfers control
// via swtch back to the scheduler.
void
scheduler(void)
{
struct proc *p;
struct cpu *c = mycpu();
c->proc = 0;
for(;;){
// Enable interrupts on this processor.
sti();
// Loop over process table looking for process to run.
acquire(&ptable.lock);
for(p = ptable.proc; p < &ptable.proc[NPROC]; p++){
if(p->state != RUNNABLE)
continue;
// Switch to chosen process. It is the process's job
// to release ptable.lock and then reacquire it
// before jumping back to us.
c->proc = p;
switchuvm(p);
p->state = RUNNING;
swtch(&(c->scheduler), p->context);
switchkvm();
// Process is done running for now.
// It should have changed its p->state before coming back.
c->proc = 0;
}
release(&ptable.lock);
}
}
而切换的顺序就是从调度器(获取锁,选择执行进程并切换到进程)->进程(开始执行,即从swtch返回,随后释放锁)->进程(时间片到,调用yield重新获取锁,并切换到调度器)->调度器(此时就相当于返回调度器未切换进程的状态,锁依然是获取着的)
sleep和wakeup
sleep函数可以让一个进程休眠,而wakeup可以唤醒指定进程。
先来看sleep函数,它的主要思想就是将进程状态设置为SLEEPING并且调用sched让出CPU。
// Atomically release lock and sleep on chan.
// Reacquires lock when awakened.
void
sleep(void *chan, struct spinlock *lk)
{
struct proc *p = myproc();
if(p == 0)
panic("sleep");
if(lk == 0)
panic("sleep without lk");
// Must acquire ptable.lock in order to
// change p->state and then call sched.
// Once we hold ptable.lock, we can be
// guaranteed that we won't miss any wakeup
// (wakeup runs with ptable.lock locked),
// so it's okay to release lk.
if(lk != &ptable.lock){ //DOC: sleeplock0
acquire(&ptable.lock); //DOC: sleeplock1
release(lk);
}
// Go to sleep.
p->chan = chan;
p->state = SLEEPING;
sched();
// Tidy up.
p->chan = 0;
// Reacquire original lock.
if(lk != &ptable.lock){ //DOC: sleeplock2
release(&ptable.lock);
acquire(lk);
}
}
接下来看wakeup函数,它的主要思想就是找到一个状态是SLEEPING的函数,接着将其设置成RUNNABLE,这样它就可以被其他CPU的调度器所调度执行,相当于被唤醒了。
// Wake up all processes sleeping on chan.
void
wakeup(void *chan)
{
acquire(&ptable.lock);
wakeup1(chan);
release(&ptable.lock);
}
//PAGEBREAK!
// Wake up all processes sleeping on chan.
// The ptable lock must be held.
static void
wakeup1(void *chan)
{
struct proc *p;
for(p = ptable.proc; p < &ptable.proc[NPROC]; p++)
if(p->state == SLEEPING && p->chan == chan)
p->state = RUNNABLE;
}