结合中断上下文切换和进程上下文切换分析Linux内核一般执行过程

一、实验要求

  • 以fork和execve系统调用为例分析中断上下文的切换
  • 分析execve系统调用中断上下文的特殊之处
  • 分析fork子进程启动执行时进程上下文的特殊之处
  • 以系统调用作为特殊的中断,结合中断上下文切换和进程上下文切换分析Linux系统的一般执行过程

 

二、上下文切换

2.1 进程上下文切换

为了控制进程的执?,内核必须有能?挂起正在CPU上运?的进程,并恢复执?以前挂起的某个进程。这种

?为被称为进程切换,任务切换或进程上下?切换。尽管每个进程可以拥有属于??的地址空间,但所有进程

必须共享CPU及寄存器。因此在恢复?个进程执?之前,内核必须确保每个寄存器装?了挂起进程时的值。

进程恢复执?前必须装?寄存器的?组数据,称为进程的CPU上下?。您可以将其想象成对CPU的某时刻的状

态拍了?张“照?”, “照?”中有CPU所有寄存器的值。同样进程切换就是拍?张当前进程所有状态的?“照?”保存

下来,其中就包括进程的CPU上下?的?“照?”,然后将导??张之前保存下来的其他进程的所有状态信息恢复执?。 

 

 进程切换就是变更进程上下?

最核?的是?个关键寄存器的保存与变换。 

   • CR3寄存器代表进程??录表,即地址空间、数据。 

   • 内核堆栈栈顶寄存器sp代表进程内核堆栈(保存函数调?历史),进程描述符(最后的成员thread是关键)和内核堆

  栈存储于连续存取区域中,进程描述符存在内核堆栈的低地址,栈从?地址向低地址增?,因此通过栈顶指针寄存器还

  可以获取进程描述符的起始地址。 

   • 指令指针寄存器ip代表进程的CPU上下?,即要执?的下条指令地址。

这些寄存器从?个进程的状态切换到另?个进程的状态,进程切换的关键上下?就算完
成了。

 

 

 2.2 linux-5.4.34进程切换核?代码分析 

linux-5.4.34进程切换过程在逻辑上并没有根本性的变化,但是代码实现?式有较?的改变,我们以x86-64体系结构为例具体

分析?下。 我们来看context_switch,?kernel/sched/core.c,尽管代码变化较?,但还是可以看到进程地址空间mm的切换

和进程关键上下?的切换switch_to。

 

 进程关键上下?的切换swtich_to,?arch/x86/include/asm/switch_to.h:

下面的__switch_to_asm是一段汇编代码,其中有内核堆栈栈顶指针RSP寄存器的切换,有jmp __switch_to,但是没有了

thread.ip及标号1的位置,关键的指令指针寄存器RIP是怎么切换的呢?

 

 

这?需要注意__switch_to_asm是在C代码中调?的,也就是使?call指令,?这段汇编的结尾是jmp __switch_to,__switch_to

函数是C代码最后有个return,也就是ret指令。

• 将__switch_to_asm和__switch_to结合起来,正好是call指令和ret指令的配对出现。

• call指令压栈RIP寄存器到进程切换前的prev进程内核堆栈;?ret指令出栈存?RIP寄存器的是进程切换之后的next进程的内

核堆栈栈顶数据。 

((last) = __switch_to_asm((prev), (next)));
ENTRY(__switch_to_asm)
pushq %rbp
pushq %rbx
pushq %r12
pushq %r13
pushq %r14
pushq %r15
/* switch stack */
movq %rsp, TASK_threadsp(%rdi)
movq TASK_threadsp(%rsi), %rsp
*/
popq %r15
popq %r14
popq %r13
popq %r12
popq %rbx
popq %rbp
jmp __switch_to
END(__switch_to

 

 

2.3 进程上下文与中断上下文

进程上下?切换时需要保存要切换进程的相关信息(如thread.sp与thread.ip),这与中断上下?的切换是不同的。中

断是在?个进程当中从进程的?户态到进程的内核态,或从进程的内核态返回到进程的?户态,?切换进程需要在不同

的进程间切换。但?般进程上下?切换是嵌套到中断上下?切换中的,?如前述系统调?作为?种中断先陷?内核,即

发?中断保存现场和系统调?处理过程。其中调?了schedule函数发?进程上下?切换,当系统调?返回到?户态时会

恢复现场,?此完成了保存现场和恢复现场,即完成了中断上下?切换。?本节前述内容主要关注进程上下?切换,请

注意理清中断上下?和进程上下?两者之间的关系。  

 

中断上下?和进程上下?的?个关键区别是堆栈切换的?法。中断是由CPU实现的,所以中断上下?切换过程中最关键

的栈顶寄存器sp和指令指针寄存器ip是由CPU协助完成的;进程切换是由内核实现的,所以进程上下?切换过程中最关键

的栈顶寄存器sp切换是通过进程描述符的thread.sp实现的,指令指针寄存器ip的切换是在内核堆栈切换的基础上巧妙利?

call/ret指令实现的。 

 

 2.4 fork中断上下文切换

先来看fork?进程的内核堆栈,从struct fork_frame可以看出它是在struct pt_regs的基础上增加了

struct inactive_task_frame。 对照?下__switch_to_asm汇编代码中压栈和出栈的寄存器,是不是完全?致,就栈顶

多了?个ret_addr,在fork?进程中存储的就是?进程的起始点ret_from_fork。 

结合中断上下文切换和进程上下文切换分析Linux内核一般执行过程

 

 

 fork?进程的内核堆栈示意图中struct pt_regs就是内核堆栈中保存的中断上下?, struct inactive_task_frame就是

fork?进程的进程上下?。__switch_to_asm汇编代码中完成内核堆栈切换后的代码,正好与structinactive_task_frame

对应??出栈,最后的__switch_to函数的最后ret正好出栈的是ret_addr,即?进程的起始点ret_from_fork。

 

我们再看下fork执行流程

在linux中,我们可以通过fork系统调用来处理进程创建的任务。对于进程的创建, 可以sys_clone, sys_vfork,以及sys_fork.

这些系统调用的内部都使用了do_fork.函数。对于do_fork函数, 会copy tast_struct, 设置内核堆栈, 并且对一些特定的数据

结构进行修改。其中里面还有copy_thread 函数, 会设置这个进程的cs和ip。这个是在进程的thread_info中保持的。这里的ip

设置成了ret_from_fork函数(在ret_from_frok里面有一个jmp system_exit). 后面,fork系统调用本身可以进入到之前系统调

用的部分讲的system_exit部分。 这样fork 系统调用在这里就会有一个进程调度的时机。schedule 对比自己写的多道时间片轮

转的问题,进程调度大致的流程是,找stask_struct链表, 找里面可以用的进程,找到以后, 找里面保持的ip, 这里就是刚才设置

的ret_from_fork, 从这里开始, jmp 到system_exit, 就可以ret restore_all, 恢复到父进程那个位置的代码,开始执行。 

 2.5 fork子进程启动执行时进程上下文的特殊之处

fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:

    1)在父进程中,fork返回新创建子进程的进程ID;

    2)在子进程中,fork返回0;

    3)如果出现错误,fork返回一个负值;

创建新进程成功后,系统中出现两个基本完全相同的进程,这两个进程执行没有固定的先后顺序,哪个进程先执行要看系统的

进程调度策略。此时,两个进程都从fork开始往下执行,只是pid不同。

 

2.6 execve系统调用中断上下文的特殊之处

execve系统调用的执行过程如下:

1. 陷入内核

2. 加载新的可执行文件并进行可执行性检查

3. 将新的可执行文件映射到当前运行进程的进程空间中,并覆盖原来的进程数据

4. 将EIP的值设置为新的可执行程序的入口地址。如果可执行程序是静态链接的程序,或不需要其他的动态链接库,则新的入口地址就是新

的可执行文件的main函数地址;如果可执行程序还需要其他的动态链接库,则入口地址是加载器ld的入口地址

5. 返回用户态,程序从新的EIP出开始继续往下执行。至此,老进程的上下文已经被新的进程完全替代了,但是进程的PID还是原来的。从这

个角度来看,新的运行进程中已经找不到原来的对execve调用的代码了,所以execve函数的一个特别之处是他从来不会成功返回,而总是实

现了一次完全的变身。

 

三、Linux系统的一般执行过程

Linux系统的一般执行过程分析

一般情况:当前系统正在进行,有一个用户态进程X,需要切换到用户态进程Y(进程策略决定):

  1.正在运行的用户态进程X

  2.发生中断——save cs:eip/esp/eflags(current) to kernel stack :当前CPU上下?压?进程X的内核堆栈。
  然后 load cs:eip(entry of a specific ISR(中断服务例程的入口,对于系统调用就是system_call)) 和

  ss:esp(point to kernel stack).//加载当前进程内核堆栈相关信息,跳转到中断处理程序,即中断执?路径的起点。这些保存和加载都是CPU

  自动完成。

  3.SAVE_ALL //保存现场,此时完成了中断上下?切换,即从进程X的?户态到进程X的内核态。

  4.中断处理过程中或中断返回前调用了schedule(),其中的switch_to做了关键的进程上下文切换。将当前进程X的内核堆栈切换到进程调度

  算法选出来的next进程(本例假定为进程Y)的内核堆栈,并完成了进程上下?所需的EIP等寄存器状态切换。详细过程?前述内容。

  5.标号1之后开始运行用户态进程Y(这里Y曾经通过以上步骤被切换出去过,就是next以前做过prev,因此可以从标号1继续执行)

  6.restore_all //Y进程从它的中断中恢复现场,与(3)中保存现场相对应。注意这?是进程Y的中断处理过程中,?(3)中保存现场是在

  进程X的中断处理过程中,因为内核堆栈从进程X切换到进程Y了。

  7.iret - pop cs:eip/ss:esp/eflags from kernel stack//从Y进程的内核堆栈中弹出2)中硬件完成的压栈内容。此时完成了中断上下?的切换,

  即从进程Y的内核态返回到进程Y的?户态。

  8.继续运行用户态进程Y//执行发生中断时间点的下一条指令

关键:中断上下文的切换(中断和中断返回时CPU进行上下文切换)和进程上下文的切换(进程调度过程中,从一个进程的内核堆栈切换到另一个  

进程的内核堆栈)

 

Linux系统执行过程中的几个特殊情况

  1.通过中断处理过程中的调度时机,用户态进程与内核线程之间互相切换和内核线程之间互相切换,与最一般的情况非常类似,只是内核线程

  运行过程中发生中断没有进程用户态和内核态的转换;

  2.内核线程主动调用schedule(),只有进程上下文的切换,没有发生中断上下文的切换,与最一般的情况略简略;//用户态进程不能主动调用

  3.fork:创建子进程的系统调用在子进程中的执行起点(next_ ip = ret_ from_ fork)返回用户态,进程返回不是从标号1开始执行,直接跳转到

  ret_ from_fork执行然后返回到用户态;

  4.加载一个新的可执行程序后返回到用户态的情况,如execve,只是中断上下文在execve系统调用内部被修改了;

在linux中,我们可以通过fork系统调用来处理进程创建的任务。对于进程的创建, 可以sys_clone, sys_vfork,以及sys_fork. 这些系统调用的内部都使用了do_fork.函数。对于do_fork函数, 会copy tast_struct, 设置内核堆栈, 并且对一些特定的数据结构进行修改。其中里面还有copy_thread 函数, 会设置这个进程的cs和ip。这个是在进程的thread_info中保持的。这里的ip设置成了ret_from_fork函数(在ret_from_frok里面有一个jmp system_exit). 后面,fork系统调用本身可以进入到之前系统调用的部分讲的system_exit部分。 这样fork 系统调用在这里就会有一个进程调度的时机。schedule 对比自己写的多道时间片轮转的问题,进程调度大致的流程是,找stask_struct链表, 找里面可以用的进程,找到以后, 找里面保持的ip, 这里就是刚才设置的ret_from_fork, 从这里开始, jmp 到system_exit, 就可以ret restore_all, 恢复到父进程那个位置的代码,开始执行。 

结合中断上下文切换和进程上下文切换分析Linux内核一般执行过程

上一篇:window下 Tomcat安装、配置、验证


下一篇:WIN系统必需品:NTP网络时间服务器