上下文切换内部

在这个问题的帮助下,我想学习和填补我的知识空白

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

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

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

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

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

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

5)当调度程序运行原始线程时,控件返回到清除堆栈和线程继续的包装函数

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

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

所以,当线程A调用sched_yield()并被线程Breplace时会发生什么:

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

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

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

当一个线程上下文切换时,它调用调度程序(调度程序不作为单独的线程运行 – 它总是在当前线程的上下文中运行)。 调度程序代码select下一个要运行的进程,并调用switch_to()函数。 这个函数本质上只是切换内核堆栈 – 它将当前线程的堆栈指针的当前值保存到当前线程的TCB(在Linux中称为struct task_struct ),并从TCB为下一个线程加载先前保存的堆栈指针。 此时,它还会保存和恢复内核通常不使用的其他线程状态 – 例如浮点/ SSE寄存器。

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

在第二步中你所遗漏的是堆栈从一个线程的用户级堆栈(你推送args的地方)切换到一个线程的保护级堆栈。 由系统调用中断的线程的当前上下文实际上保存在这个受保护的栈上。 在ISR内部,进入内核之前,这个被保护的栈再次被切换到你正在讨论内核栈。 一旦进入内核,调度程序的函数等内核函数最终将使用内核堆栈。 之后,一个线程被调度程序选中并且系统返回到ISR,它从内核堆栈切换回新select的(或者如果没有更高优先级的线程处于活动状态,则前者)线程的保护级别堆栈,最终包含新的线程上下文。 因此,上下文是通过代码自动从堆栈中恢复的(取决于底层架构)。 最后,一个特殊的指令恢复最新的敏感的resgisters,如堆栈指针和指令指针。 回到用户空间

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

一些资源:

  • 3.3.3执行 理解Linux内核 的过程切换 ,O'Reilly
  • 5.12.1 英特尔手册3A(系统编程)的 例外或中断处理程序 。 章节号可能因版本不同而不同,因此,查询“堆栈使用情况转移到中断和例外处理例程”应该可以帮助你find一个好的例子。

希望这个帮助!

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

依次将线程堆栈分为两个独立的部分,其中之一称为用户堆栈(因为在线程在用户模式下执行时使用),第二部分称为内核堆栈(因为线程在内核模式下执行时使用) 。 一旦线程跨越用户和内核模式之间的边界,CPU会自动将其从一个堆栈切换到另一个堆栈。 内核和CPU都跟踪堆栈。 对于内核堆栈,CPU永远记住指向线程内核堆栈顶部的指针。 这很容易,因为这个地址对于线程来说是不变的。 每次线程进入内核时,都会find空的内核堆栈,每当它返回到用户模式时,都会清除内核堆栈。 同时,当线程在内核模式下运行时,CPU不会记住指向用户堆栈顶部的指针。 在进入内核的过程中,CPU在内核堆栈的顶部创build特殊的“中断”堆栈帧,并将用户模式堆栈指针的值存储在该帧中。 当线程退出内核时,CPU在清理之前立即从之前创build的“中断”堆栈帧中恢复ESP的值。 (在传统x86上,这对int / iret指令处理进入和退出内核模式)

进入内核模式时,CPU立即创build“中断”堆栈帧后,内核将其余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 

请注意,内核中只有一个执行线程切换的函数。 由于每当内核切换堆栈时,都可以在堆栈顶部find传入线程的上下文。 只是因为在堆栈切换内核之前每次都将传出线程的上下文推送到其堆栈。

还要注意的是,每次堆栈切换之后以及返回用户模式之前,内核都会通过内核堆栈顶部的新值重新加载CPU的内核。 这样做可以确保当新的活动线程将来会尝试进入内核时,它将被CPU切换到它自己的内核堆栈。

还要注意的是,在线程切换期间并不是所有的寄存器都保存在堆栈中,有些寄存器如FPU / MMX / SSE被保存在出线程的TCB中的特定专用区域。 内核采用不同的策略有两个原因。 首先不是系统中的每个线程都使用它们。 把它们的内容推送到每一个线程,并且从堆栈中popup来是没有效率的。 第二个是对其内容进行“快速”保存和加载的特殊说明。 而这些说明不使用堆栈。

还要注意,实际上线程栈的内核部分具有固定的大小,并被分配为TCB的一部分。 (对于Linux也是如此,我也相信Windows)