7.调度相关

一.基础

用户线程状态

  • RUNNING,线程当前正在某个CPU上运行
  • RUNABLE,线程还没有在某个CPU上运行,但是一旦有空闲的CPU就可以运行
  • SLEEPING,这节课不介绍,

一. 定时器中断线程切换

防止个别程序长时间占有线程导致其他线程饿死,通过定时器中断定时释放线程资源

定时器中断是由硬件发起的

一.线程切换流程

P0用户线程-->P0内核线程-->调度器线程-->P1内核线程-->P1用户线程

调度器线程是每个CPU核独有的,也是一个软件,其实就是proc.c\scheduler()函数,保存在cpu->context

1. P0用户线程-->P0内核线程

计时器发出中断,用户线程跳转到内核线程

跳转时,将用户线程运行时信息(寄存器,pc等)保存在trapframe

2. P0内核线程-->CPU核对应的调度器线程

计时器中断,所以会调用proc.c\yield()

void yield(void)
{
  struct proc *p = myproc();
  // 这里必须加锁,因为需要将进程的状态修改为RUNNABLE
  // 且在修改后的一段时间,进程实际还在执行,如果不加锁
	// 在修改进程状态后,其他调度器可能就会开始运行此进程
  // 从而就会发生一个进程在两个CPU上同时运行的错误状况
  // 这里加的锁,在scheduler()中才会释放
  acquire(&p->lock);
  p->state = RUNNABLE;
  sched();
  // 这里释放的不是上面加的锁
  release(&p->lock);
}

之后是proc.c\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(struct context *old, struct context *new)
 	// 将当前内核线程的状态保存到p->context中,
  // 然后恢复调度器线程,mycpu()->context保存
  // 的就是调度器线程信息
  swtch(&p->context, &mycpu()->context);
  mycpu()->intena = intena;
}

最后使用swtch.S函数切换到调度器线程

mycpu()->context中的ra指向scheduler()函数,切换完成后,CPU将跳转到scheduler()函数中

.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

注:这里只需要保存Callee寄存器,因为按照约定,只有Callee Saved寄存器在函数调用的时候会保存和恢复

3. 调度器线程-->P1内核线程

proc.c\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++) {
      //这里加的锁在yield()中释放
      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);
        // switch.S会跳转到这里继续执行
        c->proc = 0;
      }
      //这里释放的是yield()加的锁
      release(&p->lock);
    }
  }
}

4.P1内核线程-->P1用户线程

就是正常的中断返回,回到用户线程继续执行

线程切换工作完成

二.首次线程切换

二.Sleep & Wake up

  1. 在线程切换的过程中需要一直持有p->lock

  2. XV6中,不允许进程在执行switch函数的过程中,持有任何其他的锁,因为可能会影响其他进程的后续执行

    例如获取锁的函数acquire()需要关闭中断,因为不关闭中断,在获取到锁后,发生了中断,而中断切换到的程序运行也需要这把锁,那就会发生死锁

posted @ 2024-04-21 00:20  INnoVation-V2  阅读(3)  评论(0编辑  收藏  举报