上下文切换内部


73

我想借助这个问题来学习并弥补我的知识空白。

因此,用户正在运行一个线程(内核级),并且现在正在调用yield(我认为是系统调用)。现在,调度程序必须将当前线程的上下文保存在TCB中(该线程存储在内核中的某个位置),并选择另一个线程来运行并加载其上下文并跳转至CS:EIP。为了缩小范围,我正在研究在x86体系结构上运行的Linux。现在,我想详细介绍一下:

因此,首先我们有一个系统调用:

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

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

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

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

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

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

Answers:


119

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

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

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

每个用户线程都具有一个用户模式堆栈和一个内核模式堆栈。当线程进入内核时,用户模式堆栈(SS:ESP)和指令指针(CS:EIP)的当前值将保存到线程的内核模式堆栈中,并且CPU切换到内核模式堆栈-使用int $80syscall机制,这由CPU本身完成。其余的寄存器值和标志也将保存到内核堆栈中。

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

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

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


好答案!!那么,调度程序使用的是从其切换线程的内核堆栈?另外,请提供一些资料以供您了解。
布鲁斯

8
@Bruce:在我看来,最好的源是源-例如x86switch_to例程。它有助于与平台文档一起阅读(例如,《英特尔64和IA-32架构软件开发人员手册》,该手册可从英特尔免费获得)。
caf 2012年

@caf很好的答案!因此,用户空间寄存器不会保存在任何地方(SS,CS,EIP,ESP除外),对吗?TCB在堆中保存在内核中的什么位置?
WindChaser

@WindChaser:您可能已经错过了这一部分:“然后,其余的寄存器值和标志也将保存到内核堆栈中。” 。它task_struct是由内核动态分配的(尽管内核实际上并没有“堆”的概念),并被添加到任务的全局链接列表中。
caf 2015年

1
@Amnesiac:在这种情况下不是-很清楚OP的含义,因为在第2点中,他们谈论从用户模式转换到内核模式。
咖啡馆

13

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

概括起来,一个线程(通常)有两个堆栈,而内核本身只有一个。在每个内核进入末尾,都会擦除内核堆栈。有趣的是,从2.6开始,内核本身就已经进行了线程化以进行某些处理,因此,内核线程在常规内核堆栈旁边还有自己的保护级堆栈。

一些资源:

  • 3.3.3执行处理切换深入理解Linux内核,O'Reilly的
  • 5.12.1 Exception-或中断处理程序的程序的的英特尔公司的手册3A(sysprogramming) 。章节编号可能因版本而异,因此,对“转移到中断和异常处理例程时的堆栈使用情况”的查找应该可以帮助您。

希望有帮助!


1
实际上,我比以前更困惑:-)。您能为您的答案提供参考吗?这可能会有所帮助。
布鲁斯

当您说“ ...时,堆栈从线程的用户级别堆栈(在其中推送了args)切换到线程的保护级别堆栈。被syscall中断的线程的当前上下文实际上保存在此受保护的堆栈上。 ”,它如何切换堆栈指针以指向受保护级别的堆栈,同时将原始堆栈指针(和所有寄存器)保存到所述堆栈上?
mclaassen 2014年

@mclaassen好问题;这是与拱有关的。通常,内部管理2个堆栈指针。在ARM上,有2个堆栈指针寄存器(“正常”和“中断” sp:文档中的pspmsp)。在Intel上,以前的SP被推到Ring0堆栈上,因此从那里恢复。
Benoit 2014年

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也是如此)


您能否澄清一下,在线程停放期间(即等待),用户堆栈的其他部分又存储在哪里(因为中断帧仅用于堆栈指针)?
uptoyou

线程切换以内核模式执行。因此,线程应该进入内核模式。但是,每次线程从用户模式切换到内核模式时,内核都会将CPU寄存器的状态保存在线程堆栈的内核部分,并在切换回用户模式时将其恢复。
ZarathustrA

是的,谢谢,您提到了它。但是,也有局部变量,函数参数,函数返回指针,我想它们位于用户堆栈上。如果是这样,当Thread切换到内核模式时,那些User变量存储在哪里?我的意思是那些位于RAM内存中但尚未到达CPU寄存器的内存。
uptoyou

它们存储在线程堆栈的用户模式部分,该部分存储在用户内存中。当您切换到内核模式时,内核切换到线程堆栈的内核部分,并且不使用用户部分,因此堆栈的用户部分中的所有数据都保留为相同状态。
ZarathustrA
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.