内核本身根本没有堆栈。过程也是如此。它也没有堆栈。线程只是被视为执行单元的系统公民。因此,只能调度线程,并且只有线程具有堆栈。但是有一点是内核模式代码大量利用的——系统每时每刻都在当前活动线程的上下文中工作。由于这个内核本身可以重用当前活动堆栈的堆栈。请注意,它们中只有一个可以同时执行内核代码或用户代码。因此,当内核被调用时,它只是重用线程堆栈并执行清理,然后将控制权返回给线程中被中断的活动。同样的机制也适用于中断处理程序。信号处理程序利用相同的机制。
线程栈又分为两个独立的部分,一个叫做用户栈(因为它在线程在用户态执行时使用),第二个叫做内核栈(因为它在线程在内核态执行时使用) . 一旦线程越过用户模式和内核模式之间的边界,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)