结合中断上下文切换和进程上下文切换分析Linux内核的一般执行过程
一、以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系统的一般执行过程