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