一、实验要求:
1、以fork和execve系统调用为例分析中断上下文的切换;
2、分析execve系统调用中断上下文的特殊之处;
3、分析fork子进程启动执行时进程上下文的特殊之处;
4、以系统调用作为特殊的中断,结合中断上下文切换和进程上下文切换分析Linux系统的一般执行过程。
二、实验步骤
(1)以fork和execve系统调用为例分析中断上下文的切换
1、fork
fork函数会将原进程变为父进程,并通过系统调用创建一个新进程作为子进程,子进程与父进程几乎一样。子进程得到父进程用户级虚拟地址空间的一份拷贝,包括文本,数据和bss段、堆以及用户栈,还获得父进程任何打开文件描述符的拷贝。调用fork进程,父进程会返回子进程的进程ID,子进程返回的是 0,可以通过返回值来判定该进程是父进程还是子进程。fork函数将运行着的程序分成2个(几乎)完全一样的进程,每个进程都启动一个从代码的同一位置开始执行的线程。这两个进程中的线程继续执行,就像是两个用户同时启动了该应用程序的两个副本。
fork函数通过系统调用sys_fork来实现,Linux-5.4.34源代码中查询arch/x86/entry/syscalls/syscall_32.tbl和arch/x86/entry/syscalls/syscall_64.tbl 可以找到 fork系统调?在32位x86和x86-64系统中,对应的内核处理函数为:2号系统调? sys_fork和56、57、58号系统调? __x64_sys_clone、__x64_sys_fork、 __x64_sys_vfork,如下所示:
fork函数定义在linux-5.4.34/kernel/fork.c文件中,如下图,fork、vfork和 clone这3个系统调?,以及do_fork和 kernel_thread内核函数都可以创建?个新进程,且都是通过_do_fork函数来创建进程,只不过传递的参数不同。
fork函数系统调用的过程如下:
从上图可见_do_fork()和copy_process()的功能很重要。_do_fork()的主要代码如下所示:
它主要完成了:调?copy_process()复制?进程、获得pid、调用wake_up_new_task将?进程加?就绪队列并等待调度执行等任务。
copy_process()的主要代码如下所示:
它主要完成了调? dup_task_struct复制?进程描述符task_struct、信息检查、初始化、把进程状态设置为TASK_RUNNING(此时?进程置为就绪态)、采?写时复制技术逐?复制所有其他进程资源、调?copy_thread_tls初始化?进程内核栈、设置?进程pid等。其中最关键的就是 dup_task_struct复制?进程描述符task_struct和copy_thread_tls初始化?进程内核栈。
结合程序实现中断上下文:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main(int argc, char * argv[]) { int pid; /* fork another process */ pid = fork(); if (pid < 0) { /* error occurred */ fprintf(stderr,"Fork Failed!"); exit(-1); } else if (pid == 0) { /* child process */ printf("This is Child Process!\n"); } else { /* parent process */ printf("This is Parent Process!\n"); /* parent will wait for the child to complete*/ wait(NULL); printf("Child Complete!\n"); } }
结果如下:
This is Parents Process ! This is Child Process ! Child Complete !
2、execve
execve()函数的原型为:
int execve(const char * filename,char * const argv[ ],char * const envp[ ]);
其中,filename为可执??件的名字;argv利用数组指针来传递给执行文件,以NULL结尾;envp为传递给执行文件的新环境变量数组,同样是以NULL结尾。编程使?的exec系列库函数(execl,execle,execlp,execv,execvp)都是execve系统调?接?函数的封装接?。
其功能为:在父进程中fork一个子进程,在子进程中调用execve()启动新的程序,并把该序加载到当前进程的内存空间内,当前的进程会被丢弃,它的堆、栈和所有的段数据都会被新进程相应的部分代替,然后会从新程序的初始化代码和 main 函数开始运行,PID则保持不变。
execve的内核入口为sys_execve,内容如下:
asmlinkage int sys_execve(struct pt_regs regs) { int error; char * filename; filename = getname((char *) regs.ebx); error = PTR_ERR(filename); if (IS_ERR(filename)) goto out; error = do_execve(filename, (char **) regs.ecx, (char **) regs.edx, ®s); if (error == 0) current->ptrace &= ~PT_DTRACE; putname(filename); out: return error; }
regs.ebx保存着可执行文件的路径名。因为路径名存储在用户空间中,这里要通过getname拷贝到内核空间中。getname在拷贝文件名时,先申请了一个page作为缓冲,然后再从用户空间拷贝字符串。用完文件名后,在函数的末尾调用putname释放掉申请的page。sys_execve的核心是调用do_execve函数,传给do_execve的第一个参数是已经拷贝到内核空间的路径名filename,第二个和第三个参数仍然是系统调用execve的第二个参数argv和第三个参数envp,它们代表的传给可执行文件的参数和环境变量仍然保留在用户空间中。
do_execve的主要流程如下所示:
(2)分析execve系统调用中断上下文的特殊之处
当前可执?程序执?到execve系统调?时,陷?内核态,在内核???do_execve加载可执??件,把当前进程的可执?程序给覆盖掉。当execve系统调?返回时,返回的已经不是原来的那个可执?程序了,?是新的可执?程序。因为,execve返回的是新的可执?程序执?的起点,静态链接的可执??件也就是main函数的?致位置,动态链接的可执??件还需要ld链接好动态链接库再从main函数开始执?。
(3)分析fork系统调用中断上下文的特殊之处
fork和其他系统调?不同之处是它在陷?内核态之后有两次返回,第?次返回到原来的?进程的位置继续向下执?,这和其他的系统调?是?样的。第二次返回是在?进程中,会返回到?个特定的点——ret_from_fork,通过内核构造的堆栈环境,它可以正常系统调?返回到?户态。
(4)以系统调用作为特殊的中断,结合中断上下文切换和进程上下文切换分析Linux系统的一般执行过程。
假设有一正在运行的用户态进程A将要切换运行用户态进程B:
①正在运行的用户态进程A
②发生中断——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).
③SAVE_ALL //保存现场
④中断处理过程中或中断返回前调用了schedule(),其中的switch_to做了关键的进程上下文切换
⑤标号1之后开始运行用户态进程B
⑥restore_all //恢复现场
⑦iret - pop cs:eip/ss:esp/eflags from kernel stack
⑧继续运行用户态进程B