上下文切换内部

时间:2012-09-27 21:22:36

标签: linux-kernel kernel scheduler context-switch

我希望借助这个问题来学习和填补我的知识空白。

因此,用户正在运行一个线程(内核级),它现在调用yield(我假设的系统调用)。 调度程序现在必须在TCB中保存当前线程的上下文(存储在内核中的某个地方)并选择另一个线程来运行并加载其上下文并跳转到其CS:EIP。 为了缩小范围,我正在开发基于x86架构的Linux。现在,我想了解详细信息:

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

1)yield的包装函数将系统调用参数推送到堆栈。按下返回地址并产生一个中断,系统调用号被推到某个寄存器(比如EAX)。

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

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

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

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

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

3 个答案:

答案 0 :(得分:99)

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

因此,当线程A调用sched_yield()并被线程B替换时,会发生什么:

  1. 线程A进入内核,从用户模式切换到内核模式;
  2. 内核上下文中的线程A - 切换到内核中的线程B;
  3. 线程B退出内核,从内核模式切换回用户模式。
  4. 每个用户线程都有用户模式堆栈和内核模式堆栈。当线程进入内核时,用户模式堆栈(SS:ESP)和指令指针(CS:EIP)的当前值将保存到线程的内核模式堆栈中,并且CPU切换到内核-mode stack - 使用int $80系统调用机制,这由CPU本身完成。其余的寄存器值和标志也会保存到内核堆栈中。

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

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

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

答案 1 :(得分:11)

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

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

一些资源:

  • 3.3.3执行了解Linux内核的过程转换 ,O'Reilly
  • 5.12.1 Intel手册3A(sysprogramming)的异常或中断处理程序。章节号可能因版本而异,因此查询“转移到中断和异常处理例程的堆栈使用情况”应该可以帮助您找到好的。

希望这有帮助!

答案 2 :(得分:5)

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

反过来,线程堆栈分为两个独立的部分,其中一个称为用户堆栈(因为它在用户模式下执行时使用),第二个称为内核堆栈(因为它在线程执行时使用)内核模式)。一旦线程跨越用户和内核模式之间的边界,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的专用区域中。内核采用不同的策略有两个原因。首先,并非系统中的每个线程都使用它们。将其内容推送到每个线程并从堆​​栈弹出它是低效的。第二个是关于" fast"的特别说明。保存和加载他们的内容。而这些说明并没有使用堆栈。

另请注意,实际上线程堆栈的内核部分具有固定大小,并作为TCB的一部分进行分配。 (对Linux来说是真的,我也相信Windows也是如此)