一、实验要求
结合中断上下文切换和进程上下文切换分析Linux内核一般执行过程- 以fork和execve系统调用为例分析中断上下文的切换
- 分析execve系统调用中断上下文的特殊之处
- 分析fork子进程启动执行时进程上下文的特殊之处
- 以系统调用作为特殊的中断,结合中断上下文切换和进程上下文切换分析Linux系统的一般执行过程
二、fork系统调用
一个现有进程可以调用fork函数创建一个新进程。由fork创建的新进程被称为子进程,原进程称为父进程。父、子进程并发运行。创建新的子进程后,两个进程都将执行fork()系统调用之后的下一条指令。父、子进程使用相同的pc(程序计数器),相同的CPU寄存器,在父进程中使用的相同打开文件。调用fork之后,数据、堆、栈有两份,代码仍然为一份但是这个代码段成为两个进程的共享代码段都从fork函数中返回。
- 在父进程中,fork返回新创建子进程的进程ID
- 在子进程中,fork返回0
- 如果出现错误,fork返回一个负值
为更好的理解fork函数,我们先看以下代码的执行结果。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> void main(){ pid_t pid; char *message; int n; pid = fork(); if(pid<0){ perror("fork failed"); exit(1); } if (pid == 0){ message = "this is the child "; n=4; }else { message = "this is the parent "; n=2; } for(;n>0;n--){ printf("%s",message); sleep(1); } //return 0; }
编译运行
gcc forktest.c
./a.out
结果
代码中:父进程中变量n=2,循环打印了2次 this is the parent;子进程中n=4,循环打印了4次 this is the child;fork调用之后父进程和子进程的变量message和n被赋予不同的值,互不影响。
linux系统下使用clone()系统调用实现fork()。fork(),vfork()和clone()库函数都根据各自需要的参数标志去调用clone(),然后由clone()去调用do_fork(). 再然后do_fork()完成了创建中的大部分工作。然后调用copy_process()。具体的实现步骤如下。
1. fork(),vfork()和clone()库函数都根据各自需要的参数标志去调用clone(),然后由clone()去调用do_fork()
2. do_fork()调用copy_process(),通过copy_process创建子进程
3.copy_process() 通过 调用 dup_task_struct(),为新进程创建与其父进程相同的内核栈、thread_info、task_struct,此时父子进程pid相同
4.检查当前用户拥有的进程数未超过分配给他的资源限制
5.区别父子进程pid,部分进程pid成员清零或设置
6.设置子进程state为TASK_UNINTERRUPTIBLE
7.调用copy_flags()更新task_struct的flags,进程是否拥有超级用户权限清零,进程还没有调用exec()函数表示设置
8.调用alloc_pid()为进程分配有效pid
9.根据clone()的参数。cop_process()拷贝或共享打开的文件、进程的地址空间等
10.copy_process()扫尾并返回指向子进程的指针
总结来说,进程的创建过程⼤致是⽗进程通过fork系统调⽤进⼊内核_do_fork函数,复制进程描述符及相关进程资源(采⽤写时复制技术)、分配⼦进程的内核堆栈并对内核堆栈和thread等进程关键上下⽂进⾏初始化,最后将⼦进程放⼊就绪队列, fork系统调⽤返回;⽽⼦进程则在被调度执⾏时根据设置的内核堆栈和thread等进程关键上下⽂开始执⾏。
三、execve系统调用
1. 陷入内核
2. 加载新的可执行文件并进行可执行性检查
3. 将新的可执行文件映射到当前运行进程的进程空间中,并覆盖原来的进程数据
4. 将EIP的值设置为新的可执行程序的入口地址。如果可执行程序是静态链接的程序,或不需要其他的动态链接库,则新的入口地址就是新的可执行文件的main函数地址;如果可执行程序还需要其他的动态链接库,则入口地址是加载器ld的入口地址
5. 返回用户态,程序从新的EIP出开始继续往下执行。至此,老进程的上下文已经被新的进程完全替代了,但是进程的PID还是原来的。从这个角度来看,新的运行进程中已经找不到原来的对execve调用的代码了,所以execve函数的一个特别之处是他从来不会成功返回,而总是实现了一次完全的变身。