Skip to content

CPU调度线程

从 系统启动加载(boot loader)--> entry.S --> main 里的一系列初始化 ,最后到scheduler()这是整个系统的主干。scheduler是一个单独的线程,有独自的栈,每个CPU都运行一个scheduler。scheduler每运行一次就会尝试找到一个状态为RUNNABLE的进程,然后让出CPU给这个进程运行,具体就是调用swtch把当前的scheduler的线程状态保存到它的栈中,然后把CPU的寄存器设置为要运行的进程之前在内核栈中保存的状态。当这个进程运行一段时间又要决定让出CPU,比如因为时间中断的原因调用yield -> sched -> swtch 这一次置换与前面那一次正好相反,先把当前进程的状态保存到该进程内核栈中,然后把前面保存到scheduler线程栈中的CPU寄存器状态恢复到CPU寄存器中,scheduler再次获得CPU继续在上一次让出CPU的地方执行,也就是继续for循环寻找下一个状态为RUNNABLE的进程,然后重复前面的过程再次调用swtch把CPU让给这个进程运行。

这里面的重点就是swtch方法了,下面详细分析swtch方法。

Context switching

如上所述 switch 的功能就是把当前线程的寄存器状态保存到旧线程的栈中,把新线程栈中之前保存的寄存器状态恢复到CPU寄存器中,以实现线程的切换。switch的代码如下:

# 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  // eax = old
  movl 8(%esp), %edx  // edx = new

  # Save old callee-saved registers
  pushl %ebp
  pushl %ebx
  pushl %esi
  pushl %edi

  # Switch stacks
  movl %esp, (%eax)  // *old = esp
  movl %edx, %esp    // esp = new

  # Load new callee-saved registers
  popl %edi
  popl %esi
  popl %ebx
  popl %ebp
  ret

在调用swtch函数的时候按照x86的惯例会先把%eip保存到当前栈中,然后跳转到swtch这里。接着swtch的四个pushl命令继续保存callee-saved registers到当前栈中,然后后把保存位置也就是当前栈指针%esp记录在在*old 中。 接着把新的栈指针赋给%esp, 然后四个popl指令把新栈中保存的状态值恢复到寄存器中,ret指令恢复%eip寄存器。 %esp 和 %eip的恢复意味着CPU切换了栈和正在执行的代码。