[Linux]——浅谈进程的切换

Linux进程切换

转眼接触Linux进程这个老朋友已经快一年了,阅读《Linux操作系统原理与应用》时接触到了调度函数schedule,这个函数中调用了context_switch函数进行进程的切换。而笔者对进程的切换产生了极大的兴趣,本篇博客我们从函数栈帧的角度看看进程到底是怎么切换的。

浅析进程切换

直奔主题,进程切换会发生在进程调度的前提下,所以进程切换的函数会被进程调度函数schedule调用,调用的路径是这样的。

schedule() --> context_switch() --> switch_to --> __switch_to() 

进程切换的核心其实就是调用路径中的switch_to,不过仔细看,笔者没有给这个玩应带圆括号,说明他并不是一个函数,其实他是一个宏,这个宏能够帮助我们完成进程切换工作。

进程切换包含两部分,第一部分进行全局页表项的切换,第二部分进行内核堆栈及硬件上下文的切换。switch_to 和 __switch_to()完成的工作是第二部分。我们接下来进行区分A,B进程时会在后面加上_X,因为进程切换需要涉及到函数栈帧的知识,不了解的小伙伴可以看看之前笔者的函数栈帧讲解。

switch_to 宏进行进程切换主要执行了下面的三部分步骤:

  • 步骤一:进程切换,即esp的切换,由于从esp可以找到进程的描述符
  • 步骤二:硬件上下文切换,调用__switch_to()函数,一种软件支持的切换
  • 步骤三:堆栈的切换,即ebp的切换ebp是栈底指针,它确定了当前变量空间属于哪个进程

进程切换的具体过程

  • 步骤一:将prev、next变量放入eax和edx寄存器中,并保存在A进程的内核栈中。prev和next都是A进程的局部变量。将提供程序的状态及进行相应的控制eflags同样保存在内核栈中
    [Linux]——浅谈进程的切换_第1张图片

  • 步骤二:保存在栈中的prev实际上是指向A进程test_struct的一个指针,所以接下来通过这个指针将A进程的esp保存到A进程PCB的thread.sp字段

[Linux]——浅谈进程的切换_第2张图片

  • 步骤三:保存在栈中的next实际上是指向B进程test_struct的一个指针,这个指针帮我们拿到B进程PCB中的esp寄存器,从这个时候开始,CPU当前执行的进程已经是B进程了,因为esp已经指向B的内核堆栈。但是,现在的ebp仍然指向A进程的内核堆栈,所以所有局部变量仍然是A中的局部变量

[Linux]——浅谈进程的切换_第3张图片

  • 步骤四:现在需要开始切换硬件上下文了,但是在切换之前需要做准备工作。此时将保存执行下一条指令的EIP保存到prev指向的thread.ip字段,然后将函数的返回地址保存在栈中。最后调用__switch_to()函数进程B进程硬件上下文的切换

[Linux]——浅谈进程的切换_第4张图片

  • 步骤五:在__switch_to()函数返回之后,接着执行EIP寄存器中的命令,接下来的命令就是将ebp寄存器进行修改,让ebp指向B进程的内核栈。这时候ebp已经指向了B的内核堆栈,所以下面的prev,next等局部变量已经不是A进程堆栈中的了,而是B进程堆栈中的。
  • 但是其实在完全切换之前,为了能让B进程知道之前是从哪个进程切换过来的,所以会将last变量赋值给寄存器压入B进程的栈中,last也就A的进程PCB地址,然后将last写入eax,此时prev就正确保留了上一个进程的信息,此时进程切换结束。

[Linux]——浅谈进程的切换_第5张图片
至此,switch_to已经执行完成,A停止运行,而开始了B。

switch_to宏分析

这里,为了便于理解,我们首先忽略switch_to中的具体细节,仅仅把它当作一个普通的指令。对A进程来说,它始终没有感觉到自己被打断过,它认为自己一直是不间断执行的。switch_to这条“指令”,除了改变了A进程中的prev变量外,对A没有其它任何影响。在系统中任何进程看到的都是这个样子,所有进程都认为自己在不间断的独立运行。

如果把粒度放小到switch_to里面的单个汇编语句,这个界限就不明显了。进入switch_to的宏里面之后,首先执行的push操作肯定仍然属于进程A,之后把esp指向了B的堆栈,严格的说,从此时开始的指令流都属于B进程了

总结

可以看出,进程的切换实际上依靠了一个非常重要宏switch_to,并且某一个时刻ebp和esp寄存器并不是指向同一个栈帧的,但是从更高的封装角度来看,任何一个进程都认为自己并没有被打断过。这其实也就是Linux下并发运行的原理。

参考文章:Linux内核下的进程切换

你可能感兴趣的:(Linux)