实验目的
- 以fork和execve系统调用为例分析中断上下文的切换
- 分析execve系统调用中断上下文的特殊之处
- 分析fork子进程启动执行时进程上下文的特殊之处
- 以系统调用作为特殊的中断,结合中断上下文切换和进程上下文切换分析Linux系统的一般执行过程
进程上下文
- 用户地址空间:包括程序代码、数据、用户堆栈等。
- 控制信息:进程描述符、内核堆栈等。
- 进程的CPU上下文,相关寄存器的值。
每个进程描述符包含一个类型为thread_struct的thread成员变量,只要进程被切换出去,内核就把其CPU上下文保存在这个结构体变量thread和内核堆栈中。 thread_struct数据结构包含部分CPU寄存器的状态,另外一些寄存器的状态存储在内核堆栈中。
以下是进程切换的核心代码,主要包括两个方面,第一调用switch_mm切换CR3(页全局目录)来安转一个新的地址空间;第二调用switch_to切换内核堆栈和进程的CPU上下文。
context_switch(struct rq *rq, struct task_struct *prev, struct task_struct *next) { struct mm_struct *mm, *oldmm; prepare_task_switch(rq, prev, next); mm = next->mm; oldmm = prev->active_mm; /* * For paravirt, this is coupled with an exit in switch_to to * combine the page table reload and the switch backend into * one hypercall. */ arch_start_context_switch(prev); if (!mm) { next->active_mm = oldmm; atomic_inc(&oldmm->mm_count); enter_lazy_tlb(oldmm, next); } else switch_mm(oldmm, mm, next); if (!prev->mm) { prev->active_mm = NULL; rq->prev_mm = oldmm; } /* * Since the runqueue lock will be released by the next * task (which is an invalid locking op but in the case * of the scheduler it‘s an obvious special-case), so we * do an early lockdep release here: */ spin_release(&rq->lock.dep_map, 1, _THIS_IP_); context_tracking_task_switch(prev, next); /* Here we just switch the register state and the stack. */ switch_to(prev, next, prev); barrier(); /* * this_rq must be evaluated again because prev may have moved * CPUs since it called schedule(), thus the ‘rq‘ on its stack * frame will be invalid. */ finish_task_switch(this_rq(), prev); }
中断上下文
以下是内核堆栈,其中蓝色部分是pt_regs的数据结构来保存中断上下文。
分析fork调用中的中断上下文
都知道fork系统调用用来创建一个子进程,此时处在内核态,内核堆栈如下,pt_regs保存中断上下文,用来返回用户态,inactive_task_frame保存进程上下文。
_do_fork调用copy_process复制了进程描述符,创建新的内核堆栈并初始化,在调用wake_up_new_task把子进程变成就绪态。在内核态执行完后因为有pt_regs(保存有中断上下文)的存在,就可以返回用户态,完成一次中断上下文的切换。
分析execve系统调用中断上下文
execve() 系统调用的作用是运行另外一个指定的程序。它会把新程序加载到当前进程的内存空间内,当前的进程会被丢弃,它的堆、栈和所有的段数据都会被新进程相应的部分代替,然后会从新程序的初始化代码和 main 函数开始运行。同时,进程的 ID 将保持不变。execve() 系统调用通常与 fork() 系统调用配合使用。从一个进程中启动另一个程序时,通常是先 fork() 一个子进程,然后在子进程中使用 execve() 变身为运行指定程序的进程。 例如,当用户在 Shell 下输入一条命令启动指定程序时,Shell 就是先 fork() 了自身进程,然后在子进程中使用 execve() 来运行指定的程序。
系统调用栈
-
execve系统调用陷入内核,并传入命令行参数和shell上下文环境
-
execve陷入内核的第一个函数:do_execve,该函数封装命令行参数和shell上下文
-
do_execve调用do_execveat_common,后者进一步调用__do_execve_file,打开ELF文件并把所有的信息一股脑的装入linux_binprm结构体
-
__do_execve_file中调用search_binary_handler,寻找解析ELF文件的函数
-
search_binary_handler找到ELF文件解析函数load_elf_binary
-
load_elf_binary解析ELF文件,把ELF文件装入内存,修改进程的用户态堆栈(主要是把命令行参数和shell上下文加入到用户态堆栈),修改进程的数据段代码段
-
load_elf_binary调用start_thread修改进程内核堆栈(特别是内核堆栈的ip指针)
-
进程从execve返回到用户态后ip指向ELF文件的main函数地址,用户态堆栈中包含了命令行参数和shell上下文环境
当前进程执行execve系统调用时陷入内核态,在内核里面用do_execve加载可执行文件,把当前进程的可执行程序给覆盖掉。当execve系统调用返回时,返回的已经不是原来的那个可执行程序了,而是新的可执行程序。execve返回的是新的可执行程序执行的起点,静态链接的可执行文件也就是main函数的大致位置,动态链接的可执行文件还需 要ld链接好动态链接库再从main函数开始执行。
Linux系统的一般执行过程
以32位x86系统结构linux-3.18.6为例,以系统调用作为特殊的中断简要总结如下。
- 正在运行的用户态程序X
- 发生中断,CPU使用该进程的内核堆栈,并将用户态堆栈寄存器保存在内核堆栈中
- SAVE_ALL保存现场,此时完成中断上下文的切换,即从X进程的用户态到内核态
- 在中断处理过程中或中断返回前发生了进程调度,将进程X的内核堆栈切换成进程Y的内核堆栈
- 运行进程Y
- RESTORE_ALL恢复进程Y的现场
- iret从进程Y的内核堆栈弹出用户态堆栈寄存器,完成进程Y的中断上下文切换,即从进程Y的内核态切换到用户态
- 继续运行进程Y