实验内容
结合中断上下文切换和进程上下文切换分析Linux内核一般执行过程
- 以fork和execve系统调用为例分析中断上下文的切换
- 分析execve系统调用中断上下文的特殊之处
- 分析fork子进程启动执行时进程上下文的特殊之处
- 以系统调用作为特殊的中断,结合中断上下文切换和进程上下文切换分析Linux系统的一般执行过程
fork系统调用分析
fork系统调用用于创建一个新进程,称为子进程,它与进程(称为系统调用fork的进程)同时运行,此进程称为父进程。创建新的子进程后,两个进程将执行fork()系统调用之后的下一条指令。子进程使用相同的pc(程序计数器),相同的CPU寄存器,在父进程中使用的相同打开文件。调用fork之后,数据、堆、栈有两份,代码仍然为一份但是这个代码段成为两个进程的共享代码段都从fork函数中返回。正常的?个系统调?都是陷?内核态,再返回到?户态,然后继续执?系统调?后的下?条指令。fork和其他系统调?不同之处是它在陷?内核态之后有两次返回
,第?次返回到原来的?进程的位置继续向下执?,这和其他的系统调?是?样的。在?进程中fork也返回了?次,会返回到?个特 定的点——ret_from_fork,通过内核构造的堆栈环境,它可以正常系统调?返回到?户态。其大致调用过程如下图所示:
可以看到其最终调用了do_fork()方法。对于fork调用该方法主要执行了三个步骤:1)copy_process将父进程运行环境复制到子进程;2)wake_up_new_task计算调度相关参数,并将子进程的PCB加入到调度队列,并设置为可调度的;3)如果是vfork则将父进程加入等待队列,等待子进程完成
其中copy_process的指令逻辑为:
1)调用 dup_task_struct 复制当前进程的task_struct;
2)将新进程相关的数据结构和进程状态初始化;
3)复制父进程信息;
4)调用 copy_thread_tls 初始化子进程内核栈;
5)设置子进程pid;
6)建立亲属关系链接,并将新进程插入全局进程队列 copy_thread_tls: 拷贝父进程系统堆栈内容;
7)执行childregs->ax = 0语句,该代码将子进程的 eax 赋值为0,do_fork返回后会从eax读取返回值,所以为0;
8)执行p->thread.eip = (unsigned long) ret_from_fork;将子进程的 eip 设置为 ret_form_fork 的首地址,因此子进程是从 ret_from_fork 开始执行的。
execve系统调用分析
execve系统调用的作用是运行另外一个指定的程序。它会把新程序加载到当前进程的内存空间内,当前的进程会被丢弃,它的堆、栈和所有的段数据都会被新进程相应的部分代替,然后会从新程序的初始化代码和 main 函数开始运行。同时,进程的 ID 将保持不变。execve() 系统调用通常与 fork() 系统调用配合使用。从一个进程中启动另一个程序时,通常是先 fork() 一个子进程,然后在子进程中使用 execve() 变身为运行指定程序的进程。 例如,当用户在 Shell 下输入一条命令启动指定程序时,Shell 就是先 fork() 了自身进程,然后在子进程中使用 execve() 来运行指定的程序。
跟一般的系统调用不同,execve的调用过程中有一点特殊之处。当前的可执行程序在执行,执行到execve系统调用时陷入内核态,在内核里面用do_execve加载可执行文件,把当前进程的可执行程序给覆盖掉。当execve系统调用返回时,返回的已经不是原来的那个可执行程序了,而是新的可执行程序。execve返回的是新的可执行程序执行的起点,静态链接的可执行文件也就是main函数的大致位置,动态链接的可执行文件还需要ld链接好动态链接库再从main函数开始执行。
execve具体的调用过程为:
1.陷入内核
2.加载新的进程,并且将新的进程覆盖原进程的空间
3.将新的进程的入口地址设置为IP值
4.切换回用户态,继续执行原来的进程。
其最核心的do_execve主要流程如下图所示:
Linux内核的一般执行过程
1)正在运行的用户态进程X
2)发生中断——save cs:eip/esp/eflags(current) to kernel stack,then load cs:eip(entry of a specific ISR) and ss:esp(point to kernel stack).(CPU自动帮我们完成的)
注:使其有机会陷入内核态
(然后进入中断处理过程的内核代码)
3)SAVE_ALL //保存现场
4)中断处理过程中或中断返回前调用了schedule(),其中的switch_to做了关键的进程上下文切换
5)标号1之后开始运行用户态进程Y(这里Y曾经通过以上步骤被切换出去过因此可以从标号1继续执行)
6)restore_all //恢复现场
7)iret - pop cs:eip/ss:esp/eflags from kernel stack(返回执行的是Y进程曾经发生中断时用户态的下一条指令,恢复现场,恢复不是X进程的现场,而是曾经保存的Y进程现场)
8)继续运行用户态进程Y