线程

基本概念

在现代操作系统中,一个进程由多个线程组成,线程是操作系统任务调度的基本单位。 具体来说,线程作为一个最小的执行单位存在,不考虑 CPU 超线程(即认为一个 CPU 核心上只有一个线程在运行)的情况下,一个线程占据一个 CPU 核心,并因此拥有独立的寄存器组。 因此,各个线程拥有独立的栈顶指针寄存器和程序计数器寄存器,因而线程可以独立地运行各自的代码。 作为进程的组成部分,线程共享进程拥有的内存空间(比如内存堆),因此线程之间可以方便地实现数据的共享。

线程的调度(IA-32)

现代操作系统实现多任务调度的核心是 时钟中断 。 在 IA-32 体系结构中,计算机中的时钟部件定期地向 CPU 发送时钟中断信号 1 。 CPU 在执行周期后,会在中断周期内检查是否有时钟中断信号。当 CPU 检测到中断信号后,就会执行中断处理程序。在 CPU 核心中,存在一个称作中断描述符表寄存器(IDTR, Interrupt Descriptor Table Register),它标识一块存放着中断描述符表(IDT, Interrupt Descriptor Table)的内存区域 2 。中断代理在向 CPU 传递中断信号时,会同时提供中断号,CPU 可根据中断号在 IDT 中查找对应的中断处理程序的入口地址,并跳转到该地址执行中断处理程序。在种类繁多的中断类型中,有一类中断极为特殊,称为时钟中断(Clock Interrupt),在 IA-32 体系结构中,其中断号一般为 0x20,操作系统的线程调度就是通过该中断来实现的。

中断的发生往往伴随特权级的切换。试想一个线程正运行于用户态,此时发生了时钟中断,进而要转入内核态执行中断处理程序。很显然,线程在用户态所做的事情(调用一些函数)应该于内核态所做的事情(调用一些函数)互不干扰,因此,二者对寄存器的使用、对内存的访问也应加以区分,这就涉及到特权级切换时栈切换(stack switch)的问题。 更直白地讲,一个线程拥有两个栈,一个位于用户态内存空间,另一个位于内核态内存空间。 在 IA-32 体系结构下,有一个特殊的结构——任务状态段(TSS, Task State Segment),它存储了当前任务的状态信息,包括寄存器的值、栈指针等。TSS 中的 SS0ESP0 标识了进入内核态后应该使用的栈,与 IDT 类似,TSS 也是内存中的一个数据结构,其起始地址存放在任务寄存器(TR, Task Register)内。

CPU 通过 TSS 切换到正确的内核栈后,执行 IDT 中指示的中断处理程序。对于时钟中断,操作系统若决定进行线程切换(抢占式),则当前线程的状态(寄存器、页表寄存器 CR3 等)将被保存到线程控制块(TCB, Thread Control Block)中,以便稍后恢复,并修改 TSS 中的 SS0ESP0,指向新的线程的内核栈。接着,操作系统会从就绪队列中选择一个新线程的 TCB,并将其状态加载到 CPU 寄存器中。最后,CPU 会执行一个特殊的指令 IRET,从内核态返回到用户态。在此过程中,由于页表寄存器已经被修改为新线程的页表寄存器,因此在回到用户态时,整个地址空间已经被换成了新线程的地址空间。

graph TD
    A[时钟部件] -->|发送时钟中断信号| B[CPU]
    B -->|执行完当前指令后检查中断信号| C{是否有时钟中断信号?}
    C -- Yes --> D[执行中断处理程序]
    D --> E[保存当前线程状态到 TCB]
    E --> F[修改 TSS 中的 SS0 和 ESP0]
    F --> G[从就绪队列选择新线程]
    G --> H[加载新线程状态到 CPU 寄存器]
    H --> I[执行 IRET 指令返回用户态]
  1. 严格来讲,中断信号先发送到中断代理芯片,而后中断代理芯片进行中断判优后向 CPU 传递中断信号。

  2. IDT 是一个数组,每个元素称为中断描述符(Interrupt Descriptor),每个中断描述符包含了中断处理程序的入口地址和一些其他信息。在计算机体系结构的教材中,这样的结构通常被称为中断向量表(Interrupt Vector Table)。