【问题标题】:Context switch internals上下文切换内部
【发布时间】:2012-09-19 18:14:59
【问题描述】:

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

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

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

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

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

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

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

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

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

【问题讨论】:

    标签: linux-kernel kernel scheduler context-switch


    【解决方案1】:

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

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

    一些资源:

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

    希望对您有所帮助!

    【讨论】:

    • 其实我比以前更困惑:-)。您能否为您的答案提供参考。这可能会有所帮助。
    • 当您说“...堆栈从线程的用户级堆栈(您推送参数的位置)切换到线程的受保护级堆栈。系统调用中断的线程的当前上下文实际上保存在这个受保护的堆栈上。”,它如何切换堆栈指针以指向受保护级别的堆栈,同时将原始堆栈指针(和所有寄存器)保存到所述堆栈上?
    • @mclaassen 好问题;这取决于拱门。通常在内部管理 2 个堆栈指针。在 ARM 上有 2 个堆栈指针寄存器('normal' 和 'interrupt' sp's: psp and msp in the doc)。在 Intel 上,前一个 SP 被推入 Ring0 堆栈,从而从那里恢复。
    【解决方案2】:

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

    所以,当线程 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 寄存器。如果被切换的线程不共享相同的虚拟内存空间(即它们在不同的进程中),页表也会被切换。

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

    【讨论】:

    • 很好的答案!那么,调度程序使用它正在切换的线程的内核堆栈吗?另外,请为您提供一些知识来源。
    • @Bruce:在我看来,最好的来源是来源——例如the x86 switch_to routine。结合平台文档(例如,英特尔 64 和 IA-32 架构软件开发人员手册,可从英特尔免费获得)阅读它会有所帮助。
    • @caf 很好的答案!所以用户空间寄存器不会保存在任何地方(SS,CS,EIP,ESP除外),对吧? TCB 保存在内核中的哪个堆上?
    • @WindChaser:您可能错过了这部分:“然后将剩余的寄存器值和标志也保存到内核堆栈。”task_struct 由内核动态分配(虽然内核实际上并没有“堆”的概念)并添加到一个全局的任务链表中。
    • @Amnesiac:在这种情况下不是——很清楚 OP 的含义是什么,因为在第 2 点中他们谈到了从用户模式到内核模式的转换。
    【解决方案3】:

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

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

    【讨论】:

    • 您能否澄清一下,在线程停放(即等待)期间,用户堆栈的其他部分存储在哪里(因为中断帧仅用于堆栈指针)?
    • 线程切换在内核模式下执行。因此,要做到这一点,线程应该进入内核模式。但是,每次线程从用户态切换到内核态时,内核都会将CPU寄存器的状态保存在线程堆栈的内核部分,并在切换回用户态时恢复它们。
    • 是的,谢谢,你提到了。但是,还有局部变量、函数参数、函数返回指针,我猜它们位于用户堆栈上。如果是这样,当线程切换到内核模式时,这些用户变量存储在哪里?我的意思是那些位于 RAM 内存中的那些还没有到达 CPU 寄存器。
    • 它们存储在线程堆栈的用户模式部分,该线程堆栈存储在用户内存中。当您切换到内核模式时,内核切换到线程堆栈的内核部分并且不使用用户部分,因此堆栈中用户部分的所有数据都保持在相同的状态。
    猜你喜欢
    • 1970-01-01
    • 2021-04-07
    • 2011-07-23
    • 2014-10-01
    • 2013-04-21
    • 2017-06-13
    • 1970-01-01
    相关资源
    最近更新 更多