Lec11 Thread switching (Robert)

线程还具有状态,我们可以随时保存线程的状态并暂停线程的运行,并在之后通过恢复状态来恢复线程的运行。线程的状态包含了三个部分: 程序计数器(Program Counter),它表示当前线程执行指令的位置。 保存变量的寄存器。 程序的Stack(注,详见5.5)。通常来说每个线程都有属于自己的Stack,Stack记录了函数调用的记录,并反映了当前线程的执行点。

与大多数其他操作系统一样,XV6结合了这两种策略,首先线程会运行在所有可用的CPU核上,其次每个CPU核会在多个线程之间切换,因为通常来说,线程数会远远多于CPU的核数。

XV6内核共享了内存,并且XV6支持内核线程的概念,对于每个用户进程都有一个内核线程来执行来自用户进程的系统调用。所有的内核线程都共享了内核内存,所以XV6的内核线程的确会共享内存。

在一些其他更加复杂的系统中,例如Linux,允许在一个用户进程中包含多个线程,进程中的多个线程共享进程的地址空间。

线程调度

这里停止一个线程的运行并启动另一个线程的过程通常被称为线程调度(Scheduling)。我们将会看到XV6为每个CPU核都创建了一个线程调度器(Scheduler)。

定时器中断会强制的将CPU控制权从用户进程给到内核,这里是pre-emptive scheduling,之后内核会代表用户进程(注,实际是内核中用户进程对应的内核线程会代表用户进程出让CPU),使用voluntary scheduling。

这里核心点在于,在XV6中,任何时候都需要经历:

  • 从一个用户进程切换到另一个用户进程,都需要从第一个用户进程接入到内核中,保存用户进程的状态并运行第一个用户进程的内核线程。

  • 再从第一个用户进程的内核线程切换到第二个用户进程的内核线程。

  • 之后,第二个用户进程的内核线程暂停自己,并恢复第二个用户进程的用户寄存器。

  • 最后返回到第二个用户进程继续执行。

每一个CPU都有一个完全不同的调度器线程。调度器线程也是一种内核线程,它也有自己的context对象。任何运行在CPU1上的进程,当它决定出让CPU,它都会切换到CPU1对应的调度器线程,并由调度器线程切换到下一个进程。

实际上swtch函数并不是直接从一个内核线程切换到另一个内核线程。XV6中,一个CPU上运行的内核线程可以直接切换到的是这个CPU对应的调度器线程。所以如果我们运行在CPU0,swtch函数会恢复之前为CPU0的调度器线程保存的寄存器和stack pointer,之后就在调度器线程的context下执行schedulder函数中。

学生提问:context保存在哪? Robert教授:每一个内核线程都有一个context对象。但是内核线程实际上有两类。每一个用户进程有一个对应的内核线程,它的context对象保存在用户进程对应的proc结构体中。 每一个调度器线程,它也有自己的context对象,但是它却没有对应的进程和proc结构体,所以调度器线程的context对象保存在cpu结构体中。在内核中,有一个cpu结构体的数组,每个cpu结构体对应一个CPU核,每个结构体中都有一个context字段。

这里有一个术语需要解释一下。当人们在说context switching,他们通常说的是从一个线程切换到另一个线程,因为在切换的过程中需要先保存前一个线程的寄存器,然后再恢复之前保存的后一个线程的寄存器,这些寄存器都是保存在context对象中。在有些时候,context switching也指从一个用户进程切换到另一个用户进程的完整过程。偶尔你也会看到context switching是指从用户空间和内核空间之间的切换。对于我们这节课来说,context switching主要是指一个内核线程和调度器线程之间的切换。

  • 每个CPU核在一个时间只会运行一个线程,它要么是运行用户进程的线程,要么是运行内核线程,要么是运行这个CPU核对应的调度器线程。

  • 线程永远不会运行在多个CPU核上,线程要么运行在一个CPU核上,要么就没有运行。

多线程调度

学生提问:操作系统都带了线程的实现,如果想要在多个CPU上运行一个进程内的多个线程,那需要通过操作系统来处理而不是用户空间代码,是吧?那这里的线程切换是怎么工作的?是每个线程都与进程一样了吗?操作系统还会遍历所有存在的线程吗?比如说我们有8个核,每个CPU核都会在多个进程的更多个线程之间切换。同时我们也不想只在一个CPU核上切换一个进程的多个线程,是吧? Robert教授:Linux是支持一个进程包含多个线程,Linux的实现比较复杂,或许最简单的解释方式是:几乎可以认为Linux中的每个线程都是一个完整的进程。Linux中,我们平常说一个进程中的多个线程,本质上是共享同一块内存的多个独立进程。所以Linux中一个进程的多个线程仍然是通过一个内存地址空间执行代码。如果你在一个进程创建了2个线程,那基本上是2个进程共享一个地址空间。之后,调度就与XV6是一致的,也就是针对每个进程进行调度。

最后更新于