上下文切换内部

Bru*_*uce 63 kernel scheduler context-switch linux-kernel

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

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

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

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

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

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

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

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

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

caf*_*caf 106

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

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

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

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

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

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

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

  • @Bruce:在我看来,最好的来源是源 - 例如[x86`open_to`例程](http://lxr.linux.no/#linux+v3.6/arch/x86/include/asm/switch_to .H#L31).它有助于与平台文档一起阅读(例如*英特尔免费提供的英特尔64和IA-32架构软件开发人员手册*). (7认同)

Ben*_*oit 12

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

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

一些资源:

  • 3.3.3执行处理切换深入理解Linux内核,O'Reilly的
  • 5.12.1 Exception-或中断处理程序的程序的的英特尔公司的手册3A(sysprogramming) .章节号可能因版本而异,因此查询"转移到中断和异常处理例程的堆栈使用"应该会让您找到好的.

希望这有帮助!


Zar*_*trA 7

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

反过来,线程堆栈分为两个独立的部分,其中一个称为用户堆栈(因为它在用户模式下执行时使用),第二个称为内核堆栈(因为它在内核模式下执行时使用) .一旦线程跨越用户和内核模式之间的边界,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
Run Code Online (Sandbox Code Playgroud)

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

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

另请注意,在线程切换期间并非所有寄存器都保存在堆栈中,FPU/MMX/SSE等寄存器保存在传出线程的TCB的专用区域中.内核采用不同的策略有两个原因.首先,并非系统中的每个线程都使用它们.将其内容推送到每个线程并从堆​​栈弹出它是低效的.第二个是"快速"保存和加载其内容的特殊说明.而这些说明不使用堆栈.

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