一、实验要求
- 以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。
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, 恢复到父进程那个位置的代码,开始执行。