上下文切换内部结构

我想通过这个问题来学习和填补我知识的空白。

因此,一个用户正在运行一个线程(内核级别) ,它现在调用 yield(我推测是一个系统调用)。 调度程序现在必须将当前线程的上下文保存在 TCB 中(存储在内核的某个地方) ,并选择另一个线程来运行并加载其上下文,然后跳转到其 CS:EIP。 为了缩小范围,我正在开发运行在 x86架构之上的 Linux。现在,我想进入细节:

首先,我们有一个系统调用:

1) yield的包装函式将把系统调用参数推送到堆栈上。推送返回地址并引发一个中断,同时将系统调用号码推送到某个寄存器上(比如 EAX)。

2)中断将 CPU 模式从用户切换到内核,并跳转到中断向量表,然后跳转到内核中的实际系统调用。

3)我想调度程序现在被调用,现在它必须保存 TCB 中的当前状态。这就是我的困境。因为,调度程序将使用内核堆栈而不是用户堆栈来执行它的操作(这意味着必须更改 SSSP) ,它如何在不修改进程中任何寄存器的情况下存储用户的状态。我在论坛上读到,有特殊的硬件指令保存状态,但然后如何调度程序访问它们,谁运行这些指令,什么时候?

4)调度器现在将状态存储到 TCB 中并加载另一个 TCB。

5)当调度程序运行原始线程时,控制返回到清除堆栈的包装函式,线程继续运行。

附带问题: 调度程序是否作为只能运行内核代码的线程(即只能运行内核代码的线程)运行?每个内核线程或每个进程是否有单独的内核堆栈?

38167 次浏览

在步骤2中遗漏的是,堆栈从线程的用户级堆栈(您在其中推送 args)切换到线程的受保护级堆栈。被系统调用中断的线程的当前上下文实际上保存在这个受保护的堆栈上。在 ISR 内部,就在进入内核之前,这个受保护的堆栈再次切换到您所说的 内核堆栈。一旦进入内核,诸如调度器函数之类的内核函数最终将使用内核堆栈。稍后,一个线程被调度程序选中,系统返回到 ISR,它从内核堆栈切换回新选中的(如果没有更高优先级的线程处于活动状态,则切换回前者)线程的受保护级别堆栈,其中最终包含新的线程上下文。因此,通过代码自动从此堆栈恢复上下文(取决于底层架构)。最后,一个特殊的指令恢复最新的敏感寄存器,如堆栈指针和指令指针。回到用户界面。

总之,一个线程(通常)有两个堆栈,而内核本身有一个。在每次输入内核结束时,内核堆栈都会被清除。有意思的是,自2.6以来,内核本身进行了一些处理,因此内核线程在一般的内核堆栈旁边有自己的受保护级别的堆栈。

一些资源:

  • 3.3.3执行 了解 Linux 内核,O’Reilly 的进程开关
  • 5.12.1 英特尔手册3A (系统编程)的异常-或中断-处理程序过程 。每个版本的章节编号可能不同,因此查找“传输到中断和异常的堆栈使用情况-处理例程”应该能找到好的章节。

希望这有帮助!

在高层次上,有两种不同的机制需要理解。第一个是内核进入/退出机制: 它将一个正在运行的线程从运行用户模式代码切换到在该线程上下文中运行内核代码,然后再切换回来。第二个是上下文切换机制本身,它在内核模式下从在一个线程的上下文中运行切换到另一个线程。

因此,当线程 A 调用 sched_yield()并被线程 B 取代时,发生的情况是:

  1. 线程 A 进入内核,从用户模式切换到内核模式;
  2. 内核上下文中的线程 A-切换到内核中的线程 B;
  3. 线程 B 退出内核,从内核模式切换回用户模式。

每个用户线程都有一个用户模式堆栈和一个内核模式堆栈。当一个线程进入内核时,用户模式堆栈(SS:ESP)和指令指针(CS:EIP)的当前值被保存到线程的内核模式堆栈,CPU 切换到内核模式堆栈——通过 int $80系统调用机制,这是由 CPU 自己完成的。其余的寄存器值和标志也会保存到内核堆栈中。

当一个线程从内核返回到用户模式时,寄存器值和标志将从内核模式堆栈中弹出,然后用户模式堆栈和指令指针值将从内核模式堆栈上保存的值中恢复。

当线程上下文切换时,它调用调度程序(调度程序不作为单独的线程运行——它总是在当前线程的上下文中运行)。调度程序代码选择下一个要运行的进程,并调用 switch_to()函数。这个函数实际上只是切换内核堆栈——它将堆栈指针的当前值保存到当前线程的 TCB 中(Linux 中称为 struct task_struct) ,并为下一个线程从 TCB 加载以前保存的堆栈指针。此时,它还保存和恢复一些内核通常不使用的其他线程状态,比如浮点/SSE 寄存器。如果被切换的线程不共享相同的虚拟内存空间(即。它们处于不同的进程中) ,页表也被切换。

因此,您可以看到,线程的核心用户模式状态不是在上下文切换时保存和恢复的,而是在进入和离开内核时保存和恢复到线程的内核堆栈中。上下文切换代码不必担心清除用户模式寄存器值——到那时,这些值已经安全地保存在内核堆栈中了。

内核本身根本没有堆栈。过程也是如此。它也没有堆栈。线程只是被视为执行单元的系统公民。由于这个原因,只有线程可以被调度,并且只有线程有堆栈。但是有一点内核模式代码大量利用-系统的每一个时刻都在当前活动线程的上下文中工作。由于这个内核本身可以重用当前活动堆栈的堆栈。注意,它们中只有一个可以同时执行内核代码或用户代码。由于这个原因,当调用 kernel 时,它只是重用线程堆栈,并在将控制返回到线程中被中断的活动之前执行一次清理。同样的机制也适用于中断处理程序。信号处理程序利用相同的机制。

依次,线程堆栈被分为两个独立的部分,其中一个称为用户堆栈(因为它用于在用户模式下执行线程) ,第二个称为内核堆栈(因为它用于在内核模式下执行线程)。一旦线程跨越了用户和内核模式之间的边界,CPU 就会自动将其从一个堆栈切换到另一个堆栈。内核和 CPU 跟踪这两个堆栈的方式不同。对于内核堆栈,CPU 永远记住指向线程内核堆栈顶部的指针。这很简单,因为这个地址对于线程来说是常量。每次当线程进入内核时,它都会发现内核堆栈是空的,而每次当它返回到用户模式时,它都会清理内核堆栈。同时 CPU 没有记住指向用户堆栈顶部的指针,当线程在内核模式下运行时。在进入内核的过程中,CPU 在内核堆栈的顶部创建特殊的“中断”堆栈帧,并将用户模式堆栈指针的值存储在该帧中。当线程退出内核时,CPU 在清理之前立即从先前创建的“中断”堆栈帧中恢复 ESP 的值。(在遗留的 x86上,int/iret 指令对从内核模式进入和退出)

在进入内核模式期间,在 CPU 将创建“中断”堆栈帧之后,内核将其余 CPU 寄存器的内容推送到内核堆栈。注意,它只保存那些寄存器的值,这些寄存器可以由内核代码使用。例如,内核不会保存 SSE 寄存器的内容,因为它永远不会接触它们。类似地,就在要求 CPU 将控制返回到用户模式之前,内核将以前保存的内容弹出回寄存器。

请注意,在诸如 Windows 和 Linux 这样的系统中,有一个系统线程的概念(通常称为内核线程,我知道这是令人困惑的)。系统线程是一种特殊的线程,因为它们只在内核模式下执行,因此没有用户部分的堆栈。内核公司雇用他们做辅助的家务事。

线程切换仅在内核模式下执行。这意味着线程传出和传入都在内核模式下运行,都使用自己的内核堆栈,并且都具有指向用户堆栈顶部的“中断”帧。线程切换的关键点是线程内核堆栈之间的切换,如下所示:

pushad; // save context of outgoing thread on the top of the kernel stack of outgoing thread
; here kernel uses kernel stack of outgoing thread
mov [TCB_of_outgoing_thread], ESP;
mov  ESP , [TCB_of_incoming_thread]
; here kernel uses kernel stack of incoming thread
popad; // save context of incoming thread from the top of the kernel stack of incoming thread

注意,内核中只有一个函数执行线程切换。由于这一点,每当内核有堆栈切换它可以找到一个上下文的传入线程的堆栈顶部。仅仅是因为每次在堆栈切换之前,内核都会将输出线程的上下文推送到它的堆栈上。

还要注意的是,每次在堆栈切换之后和返回到用户模式之前,内核都会通过内核堆栈顶部的新值重新加载 CPU 的大脑。这样做可以确保将来当新的活动线程试图进入内核时,它将被 CPU 切换到它自己的内核堆栈。

还要注意,在线程切换期间,并非所有的寄存器都被保存在堆栈上,有些寄存器,比如 FPU/MMX/SSE,被保存在输出线程的 TCB 中的特定区域。内核在这里采用不同的策略有两个原因。首先,并非系统中的每个线程都使用它们。为每个线程将其内容推送到堆栈并从堆栈中弹出是低效的。第二个是“快速”保存和加载他们的内容的特殊说明。这些指令不使用堆栈。

还要注意的是,实际上线程堆栈的内核部分具有固定的大小,并作为 TCB 的一部分分配。(Linux 是如此,我相信 Windows 也是如此)