8. 操作系统内核的线程调度原理

2,404 阅读4分钟

线程调度流程

从上图可以总结出以下几点:

  1. 线程调度是由时钟中断发起的。每次时钟中断都会把当前线程的ticks减1,如果ticks没有到0是不会出发shedule的。例如主线程的ticks是31,那么main线程运行后intr_timer_handler会之前的流程会走30遍,直到第31遍才会触发之后的shedule。
  2. 线程运行顺序是根据thread_ready_list来的。thread_ready_list是一个双向链表,每次正常运行到期的线程会被加入对尾,新线程会从头部取出并从thread_ready_list中移除,简单说就是弹出。
  3. 线程就是个指令执行流。在调度完成后真正运行是线程中的函数,函数的执行不光是cs eip跳转过去就完了,还要函数压栈。但是线程切换就不是压栈了,因为函数执行完毕把这个函数栈弹出就完了,但是线程栈下次还要继续运行的,所以线程栈是切换的。在switch_to函数中就实现保存和恢复线程栈。

切换线程栈

线程调度的最后一步是switch_to函数,这个函数必须用汇编写,因为要保存和恢复寄存器,c语言是不能直接操作寄存器的。 switch_to的作用是切换线程栈,上图右边有两个调用栈,switch函数执行完毕后,就从中间的调用栈切换到了右边的调用栈。

上面的汇编代码分为上下两部分,上半部分备份当前运行的寄存器到switch_to的栈中,以及备份栈指针到线程a的pcb的self_kstack指针中,方便以后再恢复继续运行。

下半部分是取出next指向的线程b的pcb中之前保存的switch_to指针,恢复栈指针esp到线程b对应的switch_to函数中。需要注意的是switch_to函数虽然是指令顺序执行的,但是上半部分和下半部分并不是同一个函数栈。这个函数栈是之前线程b运行时被中断后遗留的

switch函数执行完毕后,就会使用栈中的返回地址连续的返回和把函数栈弹出,直到返回到了中断程序入口intr20entry处,这时会跳转到intr_exit继续执行。

上图就是intr%1entry和intr_exit函数,intr%1entry是保存当前线程的上下文环境(上下文环境就是所有寄存器),intr_exit是恢复线程上下文环境。所以当intr_exit执行完毕后之前暂停的线程函数的上下文全部恢复,再通过ret指令恢复之前中断的线程函数继续运行。

线程运行第一次和第二次的不同点

上面一大堆描述的线程栈切换中的线程b是已经运行过被暂停的,如果一个线程是第一次运行,情况是不同的。

上图就是线程b第一次运行时的情况,在switch_to函数中执行如下指令

mov eax, [esp + 24] ; 得到栈中的参数next
mov esp, [eax]	

第一句是获取线程b的pcb指针,pcb中第一个元素就是self_kstack,如果运行过的线程self_kstack是指向switch_to的,没有运行过的线程是指向thread_stack,如上图所示。 然后switch_to函数的栈就切换到了上图右边的thread_stack中了,switch_to函数继续执行

pop ebp
pop ebx
pop edi
pop esi
ret		; 未由中断进入,第一次执行时会返回kernel_thread

四句pop指令,从thread_stack中把已经清零的四个数恢复ebp~esi中,这时是启动线程函数做准备。 继续执行了ret指令,由于这时线程b是一个尚未运行过但是已经准备完毕的线程,这时栈中的返回地址即eip是指向kernel_thread的,所以ret指令会把cs eip恢复为kernel_thread函数的地址。

注意同一句ret指令在执行过的线程中是返回到schedule函数继续执行,但是在未执行过线程中却是使用ret来调用kernel_thread函数的。

/* 由kernel_thread去执行function(func_arg) */
static void kernel_thread(thread_func* function, void* func_arg) {
/* 执行function前要开中断,避免后面的时钟中断被屏蔽,而无法调度其它线程 */
   intr_enable();
   function(func_arg); 
}

kernel_thread函数被调用后,根据调用约定会从栈中取出function和func_arg,注意thread_stack中有一个unused_retaddr,这个里面是空的。 因为esp这时是指向unused_retaddr的,根据调用约定esp是返回地址,esp+4为第一个参数即fucntion,esp+8为func_arg。如果没有这个空返回地址占位,那么取参数就会错乱

取参数完毕后,使用intr_enable开中断,继续运行就可以运行起线程函数了。