线程的概念
线程就是单个串行执行代码的单元,它只占用一个CPU并且以普通的方式一个接一个的执行指令。
线程还具有状态,我们可以随时保存线程的状态并暂停线程的运行,并在之后通过恢复状态来恢复线程的运行。
- 程序计数器(Program Counter),它表示当前线程执行指令的位置。
- 保存变量的寄存器。
- 程序的Stack
内核线程
XV6内核共享了内存,并且XV6支持内核线程的概念
用户线程
xv6每一个用户进程都有独立的内存地址空间,并且包含了一个线程,这个线程控制了用户进程代码指令的执行。每个用户进程都是拥有一个线程的独立地址空间
Linux,允许在一个用户进程中包含多个线程
XV6线程切换
用户寄存器存在trapframe中,内核线程的寄存器存在context中。
当用户程序在运行时,实际上是用户进程中的一个用户线程在运行。如果程序执行了一个系统调用或者因为响应中断走到了内核中,那么相应的用户空间状态会被保存在程序的trapframe中,同时属于这个用户程序的内核线程被激活,CPU被切换到内核栈上运行,实际上会走到trampoline和usertrap代码中,之后内核会运行一段时间处理系统调用或者执行中断处理程序。在处理完成之后,如果需要返回到用户空间,trapframe中保存的用户进程状态会被恢复。
定时器中断将CPU运行切换到另一个用户进程
在定时器中断程序中,如果XV6内核决定从一个用户进程切换到另一个用户进程,那么首先在内核中第一个进程的内核线程会被切换到第二个进程的内核线程。之后再在第二个进程的内核线程中返回到用户空间的第二个进程,这里返回也是通过恢复trapframe中保存的用户进程状态完成。
XV6从CC程序的内核线程切换到LS程序的内核线程时
- XV6会首先会将CC程序的内核线程的内核寄存器保存在一个context对象中。
- 类似的,因为要切换到LS程序的内核线程,那么LS程序现在的状态必然是RUNABLE,表明LS程序之前运行了一半。这同时也意味着LS程序的用户空间状态已经保存在了对应的trapframe中,更重要的是,LS程序的内核线程对应的内核寄存器也已经保存在对应的context对象中。所以接下来,XV6会恢复LS程序的内核线程的context对象,也就是恢复内核线程的寄存器。
- 之后LS会继续在它的内核线程栈上,完成它的中断处理程序(注,假设之前LS程序也是通过定时器中断触发的pre-emptive scheduling进入的内核)。
- 然后通过恢复LS程序的trapframe中的用户进程状态,返回到用户空间的LS程序中。
- 最后恢复执行LS。
XV6中,切换线程需要经历的几个步骤
- 从一个用户进程切换到另一个用户进程,都需要从第一个用户进程接入到内核中,保存用户进程的状态并运行第一个用户进程的内核线程。
- 再从第一个用户进程的内核线程切换到第二个用户进程的内核线程。
- 之后,第二个用户进程的内核线程暂停自己,并恢复第二个用户进程的用户寄存器。
- 最后返回到第二个用户进程继续执行。
注意点
context保存位置
每一个内核线程都有一个context对象。每一个用户进程有一个对应的内核线程,它的context对象保存在用户进程对应的proc结构体中。
每一个调度器线程,它也有自己的context对象,但是它却没有对应的进程和proc结构体,所以调度器线程的context对象保存在cpu结构体中。在内核中,有一个cpu结构体的数组,每个cpu结构体对应一个CPU核,每个结构体中都有一个context字段。 每一个调度器线程都有自己独立的栈。实际上调度器线程的所有内容,包括栈和context,与用户进程不一样,都是在系统启动时就设置好了
trapframe还是只包含进入和离开内核时的数据。而context结构体中包含的是在内核线程和调度器线程之间切换时,需要保存和恢复的数据
怎么区分不同进程的内核线程?
每一个进程都有一个独立的内核线程。实际上有两件事情可以区分不同进程的内核线程,其中一件是,每个进程都有不同的内核栈,它由proc结构体中的kstack字段所指向;另一件就是,任何内核代码都可以通过调用myproc函数来获取当前CPU正在运行的进程。内核线程可以通过调用这个函数知道自己属于哪个用户进程。myproc函数会使用tp寄存器来获取当前的CPU核的ID,并使用这个ID在一个保存了所有CPU上运行的进程的结构体数组中,找到对应的proc结构体。
这就是不同的内核线程区分自己的方法。