由于Linux0.11中并未实现线程,因此本文主要以理论分析为主来介绍内核级线程。内核级线程与进程的主要区别在于是否需要进行资源的切换,本文中的内核级线程也是模仿进程来设计的,若想通过一个实际案例来分析线程可以参考实验4:基于内核栈切换的进程切换,实验4中对进程切换时的栈结构变化进行详细介绍。
相较与用户级线程来说,内核级线程能够被操作系统内核“感知”到,在分配CPU资源时,内核级线程会更加有优势。
1 内核级线程的栈结构
1.1 从一个栈到一套栈
用户级线程中每一个线程都有一个用户栈,以确保在切换过程中不发生混乱。内核级线程需要进入操作系统内核,为了在切换的时候不发生混乱,一个内核级线程需要对应一套栈:用户栈+内核栈,当线程在用户态执行时就使用用户栈,当线程进入内核运行时就使用内核栈。下图为“一套栈”的结构示意图:
图中的线程内核栈和之前分析过的进程内核栈很相似,其实它们的创建方式是一样的。
1.2 用户栈与内核栈的关联
如何将用户栈与内核栈关联起来呢?即用户栈与内核栈之间如何切换。用户程序可以通过中断(如"int 0x80")进入内核,并切换到内核栈。当特权级发生改变时,执行中断会将SS:SP、EFLAGS、CS:IP这些寄存器压入内核栈中。当中断返回时,"iret"指令将SS:SP、EFLAGS、CS:IP出栈,从而切换回用户栈。下图很好的描述了这一过程。
2 内核级线程的切换过程
与用户级线程不同,内核级线程的切换是在内核中进行的。内核级线程的切换大致经过如下过程(内核线程切换的五段论):
- 中断入口。这个中断可以是系统时钟中断,也可以是系统调用,该过程主要是将SS:SP、EFLAGS、CS:IP 压入到该线程的内核栈中。由于刚刚进入内核,因此这些寄存器记录的还是用户态的信息。
- 找到下一个线程的TCB,切换TCB。这个过程和进程切换PCB是类似的,也可以通过队列找到下一个需要执行的线程,然后切换TCB(TCB中有内核栈指针)。
- 切换内核栈。在用户级线程中切换栈的工作是由Yield()完成的,在内核级线程中切换内核栈的工作由switch_to函数完成,但其实Yield()和switch_to没有什么本质区别,只是名字不一样而已。切换内核栈的过程和切换用户栈类似,利用switch_to中的"ret"指令切换到下一个线程的内核栈。
- 中断出口,从内核栈切换回用户栈。通过中断返回指令"iert"将内核栈中SS:SP、EFLAGS、CS:IP(进入中断时压入的寄存器信息)弹出,从而切换到用户栈。
这里有个问题:进入中断后,系统是如何找到该线程的内核栈的?课程中老师只是简单的说通过硬件寄存器,系统会自动找到该线程的内核栈。个人认为这个硬件寄存器应该是 TR 寄存器,这个过程应该和进程进入内核后自动找到该进程的内核栈是类似的。
3 创建内核级线程
内核级线程和进程相似的,创建内核级线程可以参考fork()创建子进程。内核级线程的创建工作是由 ThreadCreate() 完成的,ThreadCreate() 需要在内核中执行,其基本流程如下:
void ThreadCreate(*func){
TCB tcb=get_free_page(); //1、获取一页内存,TCB在这页内存的地址最小处
*krlstack=PAGE_SIZE+(long)tcb;//2、设置内核栈在这页内存的地址最大处
*userstack = ...; //3、创建用户栈空间
...; //4、填充用户栈和内核栈。其中用户栈需要压入
//CS:IP地址,而内核栈需要压入SS:SP、EFLAGS、CS:IP
tcb.esp=krlstack; //5、在tcb中保存内核栈指针
tcb.状态=就绪; //6、将线程设置为就绪态
tcb入队; //7、将tcb添加到总调度队列中
}
在线程创建完成后,线程就可以执行自己的程序了(func())。
参考
图1和图1.2 截取自哈工大操作系统课程的课件。
[1] 操作系统-哈尔滨工业大学-中国大学MOOC
[2] 哈工大操作系统实验手册
[3] Linux内核完全剖析——基于0.12内核