实验要求:
- 以fork和execve系统调用为例分析中断上下文的切换
- 分析execve系统调用中断上下文的特殊之处
- 分析fork子进程启动执行时进程上下文的特殊之处
- 以系统调用作为特殊的中断,结合中断上下文切换和进程上下文切换分析Linux系统的一般执行过程
fork系统调用分析:
fork函数简介:
库函数fork是?户态创建?个?进程的系统调?API接?。fork函数将运行着的程序分成2个(几乎)完全一样的进程,每个进程都启动一个从代码的同一位置开始执行的线程。这两个进程中的线程继续执行,就像是两个用户同时启动了该应用程序的两个副本。fork系统调用用于创建一个新进程,称为子进程,它与进程(称为系统调用fork的进程)同时运行,此进程称为父进程。创建新的子进程后,两个进程将执行fork()系统调用之后的下一条指令。子进程使用相同的pc(程序计数器),相同的CPU寄存器,在父进程中使用的相同打开文件。
分析fork系统调用和之前的其他的系统调用没有什么太大的区别,流程大抵如此:
?户态int $0x80或syscall指令触发系统调? ---> int $0x80指令触发entry_INT80_32,syscall指令触发entry_SYSCALL_64并sysret --->iret返回系统调?
1.首先在系统调用表中查找fork对应的系统调用号
因为笔者的Linux系统为64位系统,为了方便体现,这里只给出64位系统下的分析:
可以看到这里的三个系统调用都和fork函数有关,最重要的应当是57号系统调用__x64_sys_fork
课上老师已经给出了该系统调用的源代码:
可见该系统调用也是调用了 _do_fork函数,然后再去源代码中寻找 _do_fork函数的原型。
在kernel/fork.c中能找到函数的原型,函数的源代码比较复杂,有70多行,课上老师也给出了简化的代码:
1 long _do_fork(struct kernel_clone_args *args) 2 { //复制进程描述符和执?时所需的其他数据结构 3 p = copy_process(NULL, trace, NUMA_NO_NODE, args); 4 wake_up_new_task(p);//将?进程添加到就绪队列 5 return nr;//返回?进程pid(?进程中fork返回值为?进程的pid) 6 }
这里的代码比较清晰明了,这个函数主要做了三件事:
一.调用copy_process函数,复制父进程的进程描述符和执行时所需的其他数据结构到子进程去,也是本次系统调用分析的重点
二.调用wake_up_new_task函数,将子进程添加到就绪队列当中去
三.返回子进程PID
可以看出其中的copy_process函数是系统调用作用的重点,继续在源代码中查找它的原型:
同样给出课上老师给出的简化版:
1 static __latent_entropy struct task_struct *copy_process( struct pid *pid, int trace, int node, struct kernel_clone_args *args) 2 {
//复制进程描述符task_struct、创建内核堆栈等 3 p = dup_task_struct(current, node); 4 /* copy all the process information */ 5 shm_init_task(p); … // 初始化?进程内核栈和thread 6 retval = copy_thread_tls(clone_?ags, args->stack, args->stack_size, p, args->tls); … 7 return p;//返回被创建的?进程描述符指针 8 }
可以看出这里比较重要的两个函数,一个用于复制父进程的进程描述符和创建子进程内核堆栈,一个用于初始化子进程内核栈和进程。其中最关键的就是 dup_task_struct复制当前进程(?进程)描述 符task_struct和copy_thread_tls初始化?进程内核栈。
课上同样也讲过这俩个函数的具体实现,总结下来,fork系统调用的大致流程如下:
总结来说,进程的创建过程?致是?进程通过fork系统调?进?内核_do_fork函数,如下图所示复制进程描述符及相关进程 资源(采?写时复制技术)、分配?进程的内核堆栈并对内核堆栈和thread等进程关键上下?进?初始化,最后将?进程 放?就绪队列,fork系统调?返回;??进程则在被调度执?时根据设置的内核堆栈和thread等进程关键上下?开始执?
2.实验验证:
上面已经介绍过整个系统调用的工作流程,现在开始进行实验验证:
首先先编写一个包含fork系统调用的c程序,将其编译,并展示运行效果:
这里张贴出相关代码:
1 a#include<stdio.h> 2 #include<unistd.h> 3 int main() 4 { 5 pid_t pid; 6 int count = 0; 7 pid = fork(); //fork一个进程 8 if(pid == 0) { //pid为0, 9 printf("this is child process, pid is %d\n",getpid());//getpid返回的是当前进程的PID 10 count+=2; 11 printf("count = %d\n",count); 12 } else if(pid > 0) { 13 printf("this is father process, pid is %d\n",getpid()); 14 count++; 15 printf("count = %d\n",count); 16 } else { 17 fprintf(stderr,"ERROR:fork() failed!\n"); 18 } 19 return 0; 20 }
这个程序的功能很简单,就是fork出一个子进程,然后在子进程中运行不同的程序:
然后将编写好的程序复制进根目录系统的home目录下,并对其进行封装,然后在虚拟机上运行该程序:
可见在QEMU虚拟机上该程序成功运行,然后再进行调试,对上文所述函数,依次打上断点:
进行调试,验证实验效果:
由此可见,fork系统调用的工作流程和上文分析的一致,得证。
分析execve系统调用:
execve系统调用简介:
execve系统调?接?函数的函数原型如下:
int execve(const char *?lename, char *const argv[],char *const envp[]);
?lename为可执??件的名字,argv是以NULL结尾的命令?参数数 组,envp同样是以NULL结尾的环境变量数组(使?命令man execve,可查看其说明)
evecve工作流程:
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_?le -> exec_binprm()-> search_binary_handler() -> load_elf_binary() -> start_thread()。
上下文环境切换流程:
先看一下再进行evecve系统调用时有哪些上下文环境:
在布局?个新的?户态堆栈时,实际上是把命令?参数内容和环境变量的内容通过指针的? 式传到系统调?内核处理函数,再创建?个新的?户态堆栈时会把这些char *argcv[]和char *envp[]等复制到?户态堆栈中,来初始化这个新的可执?程序的执?上下?环境。所以新 的程序可以从main函数开始把对应的参数接收过来,然后执?。 值得注意的是,在调?execve系统调?时,当前的执?环境是从?进程复制过来的, execve系统调?加载完新的可执?程序之后已经覆盖了原来?进程的上下?环境。execve 系统调?在内核中帮我们重新布局了新的?户态执?环境。 执?readelf -h可以查看ELF可执??件?部信息,如下所示程序??点Entry point address:0x804887f。如果是静态链接程序在execve系统调?加载完成后,堆栈上的返回地 址会修改为程序??点的地址。当系统调?从内核态返回时,会从该地址0x804887f继续执 ?。
evecve的特别之处:
当前的可执?程序在执?,执?到execve系统调?时陷?内核态,在内 核???do_execve加载可执??件,把当前进程的可执?程序给覆盖掉。当execve系统调?返回 时,返回的已经不是原来的那个可执?程序了,?是新的可执?程序。execve返回的是新的可执? 程序执?的起点,静态链接的可执??件也就是main函数的?致位置,动态链接的可执??件还需 要ld链接好动态链接库再从main函数开始执?。
中断上下文切换和进程上下文切换对比:
系统调用和中断的机制类似,可以看作一种特殊的中断。因为前面大量分析了各类系统调用,所以这里使用系统调用中上下文切换来代替中断上下文切换。
中断上下文切换:
中断上下?代表当前进程执?,所以中断上下?中的get_current可获 取?个指向当前进程描述符的指针,即指向被中断进程,相应的中断 上下?切换的信息存储于该进程的内核堆栈中。中断有多种类型,? 如有不可屏蔽中断、可屏蔽中断、异常、陷阱(系统调?)等。
内核线程以进程上下?的形式运?在内核态,本质上还是进程,但它 有调?内核代码的权限,?如主动调?schedule()函数进?进程调 度。
进程上下文切换:
为了控制进程的执?,内核必须有能?挂起正在CPU上运?的进程,并 恢复执?以前挂起的某个进程。这种?为被称为进程切换,任务切换或 进程上下?切换。尽管每个进程可以拥有属于??的地址空间,但所有 进程必须共享CPU及寄存器。因此在恢复?个进程执?之前,内核必须 确保每个寄存器装?了挂起进程时的值。进程恢复执?前必须装?寄存 器的?组数据,称为进程的CPU上下?。您可以将其想象成对CPU的某 时刻的状态拍了?张“照?”,“照?”中有CPU所有寄存器的值。同样进 程切换就是拍?张当前进程所有状态的?“照?”保存下来,其中就包括 进程的CPU上下?的?“照?”,然后将导??张之前保存下来的其他进 程的所有状态信息恢复执?。
可以看出两类上下文切换,一类是一个进程内内核态和用户态的切换,而进程上下文切换往往是用户态内不同进程之间的切换。