76

我想在这个问题的帮助下学习并填补我的知识空白。

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

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

1) 包装函数yield将系统调用参数压入堆栈。推送返回地址并使用推送到某个寄存器(例如EAX)的系统调用号引发中断。

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

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

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

5) 当调度器运行原始线程时,控制返回到包装函数,该函数清除堆栈并恢复线程。

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

4

3 回答 3

124

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

因此,当线程 A 调用sched_yield()并被线程 B 替换时,会发生以下情况:

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

每个用户线程都有一个用户模式堆栈和一个内核模式堆栈。当一个线程进入内核时,用户态栈SS:ESP(由 CPU 自己完成。剩余的寄存器值和标志随后也被保存到内核堆栈中。CS:EIPint $80

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

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

所以你可以看到线程的核心用户模式状态在上下文切换时没有保存和恢复——当你进入和离开内核时它被保存和恢复到线程的内核堆栈。上下文切换代码不必担心破坏用户模式寄存器值 - 到那时,这些值已经安全地保存在内核堆栈中。

于 2012-10-03T05:20:31.167 回答
13

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

总而言之,一个线程(通常)有两个堆栈,而内核本身也有一个。内核堆栈在每个内核进入结束时被擦除。有趣的是,从 2.6 开始,内核本身被线程化以进行一些处理,因此内核线程在通用内核堆栈旁边有自己的受保护级别堆栈。

一些资源:

  • 3.3.3理解Linux内核的进程切换,O'Reilly
  • 英特尔手册 3A(系统编程)的5.12.1异常或中断处理程序。章节编号可能因版本而异,因此查找“在传输到中断和异常处理例程中的堆栈使用”应该会让您找到一个好的。

希望这有帮助!

于 2012-09-28T13:33:27.433 回答
8

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

线程栈又分为两个独立的部分,一个叫做用户栈(因为它在线程在用户态执行时使用),第二个叫做内核栈(因为它在线程在内核态执行时使用) . 一旦线程越过用户模式和内核模式之间的边界,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)

于 2016-10-25T11:59:26.150 回答