PlayerL 2020-06-13
一、以fork和execve系统调用为例分析中断上下文的切换
中断是在?个进程当中从进程的?户态到进程的内核态,或从进程的内核态返回到进程的?户态,?切换进程需要在不同的进程间切换。但?般进程上下?切换是嵌套到中断上下?切换中的,?如系统调?作为?种中断先陷?内核,即发?中断保存现场和系统调?处理过程。其中调?了schedule函数发?进程上下?切换,当系统调?返回到?户态时会恢复现场,?此完成了保存现场和恢复现场,即完成了中断上下?切换。
fork系统调?作用是创建了?个?进程,?进程复制了?进程中所有的进程信息,包括内核堆栈、进程描述符等,?进程作为?个独?的进程也会被调度。
fork系统调用通过_do_fork函数来创建进程,_do_fork函数主要完成了调?copy_process()复制?进程、获得pid、调?wake_up_new_task将?进程加?就绪队列等待调度执?等
long _do_fork(struct kernel_clone_args *args)
{
//复制进程描述符和执?时所需的其他数据结构
p = copy_process(NULL, trace, NUMA_NO_NODE, args);
wake_up_new_task(p);//将?进程添加到就绪队列
return nr;//返回?进程pid(?进程中fork返回值为?进程的pid)
}
copy_process函数主要完成了调? dup_task_struct复制当前进程(?进程)描述 符task_struct、信息检查、初始化、把进程状态 设置为TASK_RUNNING(此时?进程置为就绪 态)、采?写时复制技术逐?复制所有其他进程资源、调?copy_thread_tls初始化?进程内 核栈、设置?进程pid等。
其中最关键的就是 dup_task_struct复制当前进程(?进程)描述 符task_struct和copy_thread_tls初始化?进程内核栈。
static latent_entropy struct task_struct *copy_process( struct pid *pid,
int trace, int node,
struct kernel_clone_args *args)
{
//复制进程描述符task_struct、创建内核堆栈等 p = dup_task_struct(current, node);
/* copy all the process information */ shm_init_task(p);
…
// 初始化?进程内核栈和thread
retval = copy_thread_tls(clone_flags, args->stack, args->stack_size, p, args->tls);
…
return p;//返回被创建的?进程描述符指针
}
?进程创建好了进程描述符、内核堆栈等,就可以通过 wake_up_new_task(p)将?进程添加到就绪队列,使之有机会被调度执?,进程的创建?作就完成了。
对于execve系统调用,在Linux程序中,通过调用execve(),进程能够以全新程序来替换当前运行的程序。再次过程中,将丢弃旧有程序,进程的栈.数据以及堆段会被新程序所替换。这个 exec 函数族就提供了一个在进程中启动另一个程序执行的方法。
Linux系统?般会提供了execl、execlp、execle、execv、execvp和execve等6个?以加载执??个可执??件的库函数,这些库函数统称为exec函数,差异在于对命令?参数和环境变量参数 的传递?式不同。exec函数都是通过execve系统调?进?内核,对应的系统调?内核处理函数为 sys_execve或x64_sys_execve,它们都是通过调?do_execve来具体执?加载可执??件的?作。
整 体 的 调 ? 关 系 为 sys_execve() 或x64_sys_execve -> do_execve() –> do_execveat_common() -> do_execve_file(实际执行程序的位置) -> exec_binprm()-> search_binary_handler() -> load_elf_binary() -> start_thread()。
search_binary_handler的作用是遍历二进制格式handler列表,寻找合适的handler
在load_elf_binary()函数中,加载进来的可执行文件将把当前正在执行的进程的内存空间完全覆盖掉,如果可执行文件是静态链接的文件,进程的IP寄存器值将被设置为main函数的入口地址,从而开始新的进程;而如果可执行文件是动态链接的,IP的值将被设置为加载器ld的入口地址,是程序的运行由该加载器接管,ld会处理一些依赖的动态链接库相关的处理工作,使程序继续往下执行,而不管哪种执行方式,当前的进程都会被新加载进来的程序完全替换掉。
start_thread修改了内核堆栈的底部,即中断上下文的CPU状态信息,使得execve系统调用返回到用户态时能够从新的程序入口开始执行。
二、分析execve系统调用中断上下文的特殊之处
下面是execve系统调用的示意图
对于execve系统调用的执行过程:
1. 陷入内核
2. 加载新的可执行文件并进行可执行性检查
3. 将新的可执行文件映射到当前运行进程的进程空间中,并覆盖原来的进程数据
4. 将EIP的值设置为新的可执行程序的入口地址。如果可执行程序是静态链接的程序,或不需要其他的动态链接库,则新的入口地址就是新的可执行文件的main函数地址;如果可执行程序还需要其他的动态链接库,则入口地址是加载器ld的入口地址
5. 返回用户态,程序从新的EIP出开始继续往下执行。至此,老进程的上下文已经被新的进程完全替代了,但是进程的PID还是原来的。从这个角度来看,新的运行进程中已经找不到原来的对execve调用的代码了,所以execve函数的一个特别之处是他从来不会成功返回,而总是实现了一次完全的变身。
三、分析fork子进程启动执行时进程上下文的特殊之处
下面是fork系统调用的示意图
fork系统调?在??进程各返回?次,?进程中和其他系统调?的处理过程并??致,?在?进程中的内核函数调?堆栈需要特殊构建,为?进程的运?准备好上下?环境,因为frok子进程是突然创建的,前面没有执行过其他进程,所以得为他构造堆栈框架,像前面有进程一样
fork系统调?在?进程中的执?起点较为特殊,单独创建了?个进程上下?。fork?个?进程时,?进程是从ret_from_fork开始执?的。
下图是fork子进程的内核堆栈示意图,从struct fork_frame可以看出它是在 struct pt_regs的基础上增加了struct inactive_task_frame。就栈顶多了?个ret_addr,在fork?进程中存储的就是?进程的起始点ret_from_fork。
四、以系统调用作为特殊的中断,结合中断上下文切换和进程上下文切换分析Linux系统的一般执行过程
对于Linux系统的整体运?过程,其中最基本和?般的场景是:正在运?的?户态进程X切换到?户态进程Y的过程。即
(1)正在运?的?户态进程X。
(2)发?中断(包括异常、系统调?等),CPU完成load cs:rip(entry of a speci?c ISR),即跳转到中断处理程序??。
(3)中断上下?切换,具体包括如下?点:
• swapgs指令保存现场,可以理解CPU通过swapgs指令给当前CPU寄存器状态做了?个快照。
• rsp point to kernel stack,加载当前进程内核堆栈栈顶地址到RSP寄存器。快速系统调?是由系统调???处的汇编代码实现?户堆栈和内核堆栈的切换。
• save cs:rip/ss:rsp/r?ags:将当前CPU关键上下?压?进程X的内核堆栈,快速系统调?是由系统调???处的汇编代码实现的。
• 此时完成了中断上下?切换,即从进程X的?户态到进程X的内核态。
(4)中断处理过程中或中断返回前调?了schedule函数,其中完成了进程调度算法选择next进程、进程地址空间切换、以及switch_to关键的进程上下?切换等。
(5)switch_to调?了__switch_to_asm汇编代码做了关键的进程上下?切换。将当前进程X的内核堆栈切换到进程调度算法选出来的next进程(本例假定为进程Y)的内核堆 栈,并完成了进程上下?所需的指令指针寄存器状态切换。之后开始运?进程Y(这?进程Y曾经通过以上步骤被切换出去,因此可以从switch_to下??代码继续执?)。
(6)中断上下?恢复,与(3)中断上下?切换相对应。注意这?是进程Y的中断处理过程中,?(3)中断上下?切换是在进程X的中断处理过程中,因为内核堆栈从进程X 切换到进程Y了。
(7)为了对应起?中断上下?恢复的最后?步单独拿出来(6的最后?步即是7)iret - pop cs:rip/ss:rsp/r?ags,从Y进程的内核堆栈中弹出(3)中对应的压栈内容。此时完 成了中断上下?的切换,即从进程Y的内核态返回到进程Y的?户态。注意快速系统调?返回sysret与iret的处理略有不同。
(8)继续运??户态进程Y。
由此,?致上可以想象出 Linux系统就是以这样的?般执?过程在Linux系统中反复执?。