内核级线程的切换
用户级线程的切换,主要分为三步:TCB(thread controll block)切换,根据TCB中存储的栈指针完成用户栈切换,根据用户栈压入函数返回地址完成完成PC指针切换
不难想象,内核级线程的切换也要完成“TCB切换,栈切换,PC指针切换”三件事,那么它和用户级线程的区别在哪里呢?内核级线程的TCB存储在操作系统内核中,因此完成TCB切换的程序应该执行在操作系统的内核中。这是第一个重要区别,即用户及线程通过调用用户态Yield()完成切换,而内核级线程切换的故事应该从进入内核——中断开始,因为中断会导致从用户态到内核态的切换
由于进入内核态才能完成内核级线程的切换,所以要在内核的某个地方完成PC指针切换。仿照用户级线程,这个PC指针也应该放在栈中,利用栈完成切换。对于内核级线程,这个栈应该是内核栈,首先切换内核栈,然后引发PC指针切换。因此和用户级线程相比,内核级线程的第二个重要区别是同时切换内核栈和用户栈。
综上所述
用户级线程切换的核心思想是根据存放在用户程序中的TCB找到用户栈,通过用户栈切换完成用户线程的切换,整个切换过程通过调用Yield函数引发。
内核级线程切换的核心是首先进入操作系统找到线程TCB,根据TCB找到线程的内核栈,通过内核栈完成内核级线程的切换,切换的整个过程由中断引发
首先需要弄明白中断以后会发生了什么。
执行指令int/iret指令执行时,会找到当前进程的内核栈,然后将用户态执行的一些重要信息,如当前程序执行位置CS:EIP、当前用户栈栈顶位置SS:ESP以及标志寄存器FLAG压到内核栈中,实际上,所有外部中断,比如时钟中断,键盘中断,磁盘读写完成中断等,都会引发上述动作。而iret指令正好是Int指令的逆过程
可以分为五个阶段:
(1)中断进入,就是int指令或其他硬件中断的中断处理入口,核心工作是要记录当前程序在用户态执行时的信息,如当前使用的用户栈,当前程序执行位置、当前执行的线程信息等。其中用户栈地址SS:ESP和PC指针信息CS:EIP已经由中断处理硬件压入当前线程对应的内核栈中了,只有当前的执行现场信息还没有保存。所以在进入中断处理程序的开始需要编写代码保护用户态程序当前执行现场。此处以“int 0x80”为例,应该在中断处理程序system_call的开始出执行下面的代码,即中断进入代码:
push %ds
push %es
push %fs
pushl %edx
pushl %ecx
pushl %ebx
以上代码用于保护用户态程序执行现场,接下来就可以使用这些寄存器来执行内核态处理程序了,如用户“movel $0x10,%edx”,“movel %dx,%ds,“movel %dx,%es”来将段寄存器DS,ES设置为内核数据段选择子,这样以后再访问的数据就是内核数据了
(2)调用schedule函数,引起TCB切换。在中断处理程序中,如果当前线程启动了磁盘读写等操作,即发现当前线程应该让出CPU时,系统内核就会调用schudule函数来完成TCB的切换。具体做法很简单,例如在向当前磁盘发出读写指令之后,将当前线程的状态修改为阻塞,具体代码实现为cuurent->state=拥塞,并将current添加一个等待某个磁盘读写完成的等待对列联表上。接下来调用schedule实现TCB切换
为了完成TCB的切换,schdule函数首先从就绪队列中选取出下一个要执行线程的TCB。找到下一个TCB之后,此处用next指针指向这个TCB,利用current和next指针指向的信息就可以开始内核级线程切换的第三阶段了。
(3)内核栈的切换,具体来说,就是将当前的ESP寄存器放在current指向的TCB中,再从next指向的TCB中取出esp字段赋值给ESP寄存器。由于现在执行再内核态,所以当前寄存器ESP指向的就是当前线程的内核栈,而放在TCB中esp也是线程的内核地址,所以切换时内核栈的切换
current->esp=esp;
esp=next->esp
(4)中断返回,这是要为内核级线程切换的最后一个阶段“用户栈切换”做准备,同时也和内核级线程第一阶段“中断进入”相对应。在这一阶段中,要将存储在下一个线程的内核栈中的用户程序执行现场恢复出来,这个现场是这个线程在切换出去时由中断程序入口保存的。
pop1 %ebx
pop1 %ecx
pop1 %edx
pop1 %fs
pop1 %es
pop1 %ds
(5)用户栈切换,实际上就是切换用户态程序PC指针以及相应的用户栈,即需要将CS:EIP寄存器设置为当前用户程序执行地址,将SS:ESP寄存器设置为当前用户栈地址即可,而这两个信息现在就可以在下一个线程的内核栈中,只要执行iret指令就可以完成这个切换了