首页 文章

上下文切换内部

提问于
浏览
59

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

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

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

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

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

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

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

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

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

3 回答

  • 97

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

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

    • 线程A进入内核,从用户模式切换到内核模式;

    • 内核上下文中的线程A - 切换到内核中的线程B;

    • 线程B退出内核,从内核模式切换回用户模式 .

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

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

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

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

  • 11

    您在第2步中错过的是堆栈从线程's user-level stack (where you pushed args) to a thread'的受保护级别堆栈切换 . 线程的当前上下文中断通过syscall实际上保存在这个受保护的堆栈上 . 在ISR内部,在进入内核之前,这个受保护的堆栈再次切换到您正在讨论的内核堆栈 . 一旦进入内核,内核函数如scheduler 's functions eventually use the kernel-stack. Later on, a thread gets elected by the scheduler and the system returns to the ISR, it switchs back from the kernel stack to the newly elected (or the former if no higher priority thread is active) thread' s受保护级别的堆栈,最终将包含新的线程上下文 . 因此,代码会自动从代码堆栈中恢复(取决于底层架构) . 最后,一条特殊指令恢复最新的敏感寄存器,例如堆栈指针和指令指针 . 回到用户区......

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

    一些资源:

    • 3.3.3执行 Understanding the Linux Kernel 的过程切换,O'Reilly

    • 5.12.1 Intel's manual 3A (sysprogramming) 的异常或中断处理程序 . 章节编号可能因版本而异,因此"Stack Usage on Transfers to Interrupt and Exception-Handling Routines"上的查找应该会让您找到好的编号 .

    希望这有帮助!

  • 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的专用区域中 . 内核采用不同的策略有两个原因 . 首先,并非系统中的每个线程都使用它们 . 将其内容推送到每个线程并从堆栈弹出它是低效的 . 第二个是“快速”保存和加载其内容的特殊说明 . 而这些说明不使用堆栈 .

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

相关问题