xv6操作系统源码阅读之调度

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;
}
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值