YoungkingWang 2020-06-13
一、以fork和execve系统调用为例分析中断上下文的切换
1.fork系统调用
fork系统调用可以建立一个新进程,把当前的进程分为父进程和子进程,新进程称为子进程,而原进程称为父进程。fork调用一次,返回两次,这两个返回分别带回它们各自的返回值,其中在父进程中的返回值是子进程的PID,而子进程中的返回值则返回 0。新创建的子进程与父进程十分类似,子进程得到与父进程用户级虚拟地址空间相同(但是独立)的一份拷贝,包括文本,数据和bss段、堆以及用户栈。子进程还获得与父进程任何打开文件描述符相同的拷贝。这就是意味着当父进程调用fork时候,子进程还可以读写父进程中打开的任何文件。父进程和新创建的子进程之间最大区别在于他们有着不同的PID。
先用具体调用例程分析fork:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
FILE * fp = fopen("output.txt", "w");
fputs("I am parent\n", fp);
switch(fork())
{
case -1:
perror("fork failed");
return -1;
case 0:
fputs("I am Child\n", fp);
break;
default:
break;
}
fclose(fp);
return 0;
}执行并查看输出的output.txt中的内容:

发现输出文件中会出现两行重复的parent文本,原因是 fputs 库函数带有缓冲,fork() 创建的子进程完全拷贝父进程用户空间内存时,fputs 库函数的缓冲区也被包含进来了。所以,fork() 执行之后,fork系统调用复制进程描述符及相关进程资源(采?写时复制技术)、分配?进程的内核堆栈并对内核堆栈和thread等进程关键上下?进?初始化,这样子进程获得了一份 fputs 缓冲区中的数据,导致“I am parent”这条消息在子进程中又被输出了一次。可见子进程确实继承了父进程大多数资源。
2.execve系统调用
execve() 系统调用的作用是运行另外一个指定的程序。它会把新程序加载到当前进程的内存空间内,当前的进程会被丢弃,它的堆、栈和所有的段数据都会被新进程相应的部分代替,然后会从新程序的初始化代码和 main 函数开始运行。同时,进程的 ID 将保持不变。
一个execve系统调用例程:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main() {
int pid;
pid = fork();
if (pid < 0)
{
fprintf(stderr, "Fork Failed\n");
exit(-1);
}
else if (pid == 0)
{
execlp("/bin/ls", "ls", NULL);
printf("ls command run finished\n");
}
else
{
wait(NULL);
printf("Child Completed\n");
exit(0);
}
return 0;
}执行结果:


可见只有子进程执行了ls命令,与上面的论述一致。
二、分析execve系统调用中断上下文的特殊之处
上面提到过,execve() 系统调用的作用是运行另外一个指定的程序。它会把新程序加载到当前进程的内存空间内,当前的进程会被丢弃,它的堆、栈和所有的段数据都会被新进程相应的部分代替,然后会从新程序的初始化代码和 main 函数开始运行。同时,进程的 ID 将保持不变。exec簇函数都是通过execve系统调用进入内核,对应的系统调用内核处理函数为sys_execve或__x64_sys_execve,它们都是通过调用do_execve来具体执行加载可执行文件的工作,而do_execve又通过调用do_execve_common() 工作。查看do_execve_common() 代码可知execve整体调用流程大致如下:
a. 陷入内核
b. 加载新的进程
c. 将新的进程,完全覆盖原先进程的数据空间
d. 将 IP 值设置为新的进程的入口地址
e. 返回用户态,新程序继续执行下去。老进程的上下文被完全替换,但进程的 pid 不变,所以 execve 系统调用不会返回原进程,而是返回新进程。
三、分析fork子进程启动执行时进程上下文的特殊之处
fork为56号系统调用,查看~/linux-5.4.34/arch/x86/entry/syscalls/syscall_64.tbl表得到内核函数__x64_sys_clone,由fork.c中的代码可知__x64_sys_clone调用的是do_fork函数。查看do_fork源码:
long do_fork(unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr)
{
struct task_struct *p;
int trace = 0;
long nr;
p = copy_process(clone_flags, stack_start, stack_size,
child_tidptr, NULL, trace);
if (!IS_ERR(p)) {
struct completion vfork;
struct pid *pid;
trace_sched_process_fork(current, p);
pid = get_task_pid(p, PIDTYPE_PID);
nr = pid_vnr(pid);
if (clone_flags & CLONE_PARENT_SETTID)
put_user(nr, parent_tidptr);
if (clone_flags & CLONE_VFORK) {
p->vfork_done = &vfork;
init_completion(&vfork);
get_task_struct(p);
}
wake_up_new_task(p);
if (clone_flags & CLONE_VFORK) {
if (!wait_for_vfork_done(p, &vfork))
ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
}
put_pid(pid);
} else {
nr = PTR_ERR(p);
}
return nr;
}由do_fork源码可知fork子进程启动执行时调用了 copy_process 函数,复制当前进程产生子进程,并且传入关键参数为子进程设置响应进程上下文;还调用 wake_up_new_task 函数,将子进程放入调度队列中,从而有机会 CPU 调度并得以运行。 copy_process函数主要完成了调?dup_task_struct复制当前进程(?进程)描述符task_struct、信息检查、初始化、把进程状态设置为TASK_RUNNING(此时?进程置为就绪态)、采?写时复制技术逐?复制所有其他进程资源、调?copy_thread_tls初始化?进程内核栈、设置?进程pid等。其中最关键的就是dup_task_struct复制当前进程(?进程)描述符task_struct和copy_thread_tls初始化子进程内核栈。
四、以系统调用作为特殊的中断,结合中断上下文切换和进程上下文切换分析Linux系统的一般执行过程