Linux 进程调度
阅读原文时间:2022年01月08日阅读:1
线程状态(context)

程序计数器(Program Counter),它表示当前线程执行指令的位置。

保存变量的寄存器。

程序的Stack。通常来说每个线程都有属于自己的Stack,Stack记录了函数调用的记录,并反映了当前线程的执行点。

xv6的线程切换
  • 从一个用户进程切换到另一个用户进程,都需要从第一个用户进程接入到内核中,保存用户进程的状态并运行第一个用户进程的内核线程。

  • 再从第一个用户进程的内核线程切换到第二个用户进程的内核线程。

  • 之后,第二个用户进程的内核线程暂停自己,并恢复第二个用户进程的用户寄存器。

  • 最后返回到第二个用户进程继续执行

线程切换函数

xv6在这部分设计的非常巧妙,

其使用swtch(&p->context, &mycpu()->context)汇编函数,实现了两个函数sched scheduler的跳转,

达成了进程context以及调度器context的切换

swtch函数(汇编函数)

swtch函数(避开switch关键字)会保存用户进程P1对应内核线程的寄存器至context对象。所以有两类寄存器:用户寄存器存在trapframe中,内核线程的寄存器存在context中。

实际上,swtch函数并不是直接从一个内核线程切换到另一个内核线程。XV6中,一个CPU上运行的内核线程可以直接切换到的是这个CPU对应的调度器线程。所以如果我们运行在CPU0,swtch函数会恢复之前为CPU0的调度器线程保存的寄存器和stack pointer,之后就在调度器线程的context下执行schedulder函数中。

schedulder函数

schedulder函数由调度器线程执行。在schedulder函数中会做一些清理工作,例如将进程P1设置成RUNABLE状态。之后再通过进程表单找到下一个RUNABLE进程。假设找到的下一个进程是P2(虽然也有可能找到的还是P1),schedulder函数会再次调用swtch函数,完成下面步骤:

  1. 先保存自己的寄存器到调度器线程的context对象
  2. 找到进程P2之前保存的context,恢复其中的寄存器
  3. 因为进程P2在进入RUNABLE状态之前,如刚刚介绍的进程P1一样,必然也调用了swtch函数。所以之前的swtch函数会被恢复,并返回到进程P2所在的系统调用或者中断处理程序中(注,因为P2进程之前调用swtch函数必然在系统调用或者中断处理程序中)。
  4. 不论是系统调用也好中断处理程序也好,在从用户空间进入到内核空间时会保存用户寄存器到trapframe对象。所以当内核程序执行完成之后,trapframe中的用户寄存器会被恢复。
  5. 最后用户进程P2就恢复运行了。
sched函数

sched函数由用户线程调用,切换到调度器线程。其与schedulder函数互为协程的关系,也将调用swtch函数。根据线程切换机制,sched调用swtch函数后,CPU的下一条指令处于schedulder函数的swtch函数后,原因是swtch函数保存了ra(返回地址)寄存器,因此sched调用swtch函数后,当前ra变成了调度器线程的ra,即上一次调度器线程调用schedulder函数时存储的ra值,故接下来执行schedulder函数swtch调用后的代码。同理,schedulder函数调用swtch函数后,CPU将转移到sched函数。

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;
  // 交换上下文环境
  swtch(&p->context, &mycpu()->context);
  mycpu()->intena = intena;
}


// 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(;;){
    // Avoid deadlock by ensuring that devices can interrupt.
    intr_on();

    for(p = proc; p < &proc[NPROC]; p++) {
      acquire(&p->lock);
      if(p->state == RUNNABLE) {
        // Switch to chosen process.  It is the process's job
        // to release its lock and then reacquire it
        // before jumping back to us.
        p->state = RUNNING;
        c->proc = p;
        // 将当前上下文环境交换到进程上下文环境,将控制权给进程
        swtch(&c->context, &p->context);

        // Process is done running for now.
        // It should have changed its p->state before coming back.
        c->proc = 0;
      }
      release(&p->lock);
    }
  }
}


# Context switch
#
#   void swtch(struct context *old, struct context *new);
#
# Save current registers in old. Load from new.    

.globl swtch
swtch:
        sd ra, 0(a0)
        sd sp, 8(a0)
        sd s0, 16(a0)
        sd s1, 24(a0)
        sd s2, 32(a0)
        sd s3, 40(a0)
        sd s4, 48(a0)
        sd s5, 56(a0)
        sd s6, 64(a0)
        sd s7, 72(a0)
        sd s8, 80(a0)
        sd s9, 88(a0)
        sd s10, 96(a0)
        sd s11, 104(a0)

        ld ra, 0(a1)
        ld sp, 8(a1)
        ld s0, 16(a1)
        ld s1, 24(a1)
        ld s2, 32(a1)
        ld s3, 40(a1)
        ld s4, 48(a1)
        ld s5, 56(a1)
        ld s6, 64(a1)
        ld s7, 72(a1)
        ld s8, 80(a1)
        ld s9, 88(a1)
        ld s10, 96(a1)
        ld s11, 104(a1)

        ret