结合中断上下文切换和进程上下文切换分析Linux内核的一般执行过程
结合中断上下文切换和进程上下文切换分析Linux内核一般执行过程
- 以fork和execve系统调用为例分析中断上下文的切换
- 分析execve系统调用中断上下文的特殊之处
- 分析fork子进程启动执行时进程上下文的特殊之处
- 以系统调用作为特殊的中断,结合中断上下文切换和进程上下文切换分析Linux系统的一般执行过程
一.fork分析
fork:fork系统调用用于创建一个新进程,称为子进程,它与进程(称为系统调用fork的进程)同时运行,此进程称为父进程。创建新的子进程后,两个进程将执行fork()系统调用之后的下一条指令。
使用如下代码进行分析上下文切换:
#include <unistd.h> #include <stdio.h> #include <sys/types.h> int main (){ pid_t pid; pid = fork(); if (pid < 0){ printf("error in fork!\n"); } else if (pid == 0){ printf("i am the child process, my process id is %d\n",getpid()); } else{ printf("i am the parent process, my process id is %d\n",getpid()); } return 0; }
将以上代码静态编译为可执行程序fork,在_do_fork上断点,运行程序:
查看函数调用堆栈:
可以看到,在执行fork函数时,最终通过_do_fork来完成这一系统调用。查看_do_fork的实现:
long _do_fork(struct kernel_clone_args *args) { u64 clone_flags = args->flags; struct completion vfork; struct pid *pid; struct task_struct *p; int trace = 0; long nr; /* * Determine whether and which event to report to ptracer. When * called from kernel_thread or CLONE_UNTRACED is explicitly * requested, no event is reported; otherwise, report if the event * for the type of forking is enabled. */ if (!(clone_flags & CLONE_UNTRACED)) { if (clone_flags & CLONE_VFORK) trace = PTRACE_EVENT_VFORK; else if (args->exit_signal != SIGCHLD) trace = PTRACE_EVENT_CLONE; else trace = PTRACE_EVENT_FORK; if (likely(!ptrace_event_enabled(current, trace))) trace = 0; } p = copy_process(NULL, trace, NUMA_NO_NODE, args); add_latent_entropy(); if (IS_ERR(p)) return PTR_ERR(p); /* * Do this prior waking up the new thread - the thread pointer * might get invalid after that point, if the thread exits quickly. */ 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, args->parent_tid); if (clone_flags & CLONE_VFORK) { p->vfork_done = &vfork; init_completion(&vfork); get_task_struct(p); } wake_up_new_task(p); /* forking complete and child started to run, tell ptracer */ if (unlikely(trace)) ptrace_event_pid(trace, pid); if (clone_flags & CLONE_VFORK) { if (!wait_for_vfork_done(p, &vfork)) ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid); } put_pid(pid); return nr; }
从代码中可以看到,_do_fork的执行过程大致如下:
1.调用 copy_process 为子进程复制出一份进程信息
2.如果是 vfork(设置了CLONE_VFORK和ptrace标志)初始化完成处理信息
3.调用 wake_up_new_task 将子进程加入调度器,为之分配 CPU
4.如果是 vfork,父进程等待子进程完成 exec 替换自己的地址空间
其中copy_process做了如下操作:
1.调用 dup_task_struct 复制当前的 task_struct
2.检查进程数是否超过限制
3.初始化自旋锁、挂起信号、CPU 定时器等
4.调用 sched_fork 初始化进程数据结构,并把进程状态设置为 TASK_RUNNING
5.复制所有进程信息,包括文件系统、信号处理函数、信号、内存管理等
6.调用 copy_thread_tls 初始化子进程内核栈
7.为新进程分配并设置新的 pid
二. execve分析
execve函数作用是执行一个新的程序,程序可以是二进制的可执行程序,也可以是shell、pathon脚本
使用如下代码进行分析execve:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main(int argc, char * argv[]){ int pid; pid = fork(); if (pid < 0){ fprintf(stderr, "Fork Failed!\n"); exit(-1); } else if (pid == 0){ execlp("/bin/ls", "ls", NULL); } else{ printf("Child Complete!\n"); exit(0); } }
断点在do_execve,可以看到函数调用堆栈如下:
查看do_execve方法可以发现其调用链如下:do_execve -> do_execveat_common -> _do_execve_file。最终 的逻辑在_do_execve_file中实现。
三、fork和execve的区别
fork在原进程的基础上创建一个新进程,对于新创建的进程需要重新设置上下文环境,而execve会覆盖原进程的上下文环境而非创建一个新进程。
四、Linux系统的一般执行过程
1)正在运?的?户态进程X
2)发?中断(包括异常、系统调?等),CPU完成以下动作。
save cs:eip/ss:esp/eflags:当前CPU上下?压?进程X的内核堆栈。
load cs:eip(entry of a specific ISR) and ss:esp(point to kernel stack):加载当前进程内核堆栈相关信息,跳转到中断处理程序,即中断执?路径的起点。
3)SAVE_ALL,保存现场,此时完成了中断上下?切换,即从进程X的?户态到进程X的内核态。
4)中断处理过程中或中断返回前调?了schedule函数,其中的switch_to做了关键的进程上下?切换。将当前进程X的内核堆栈切换到进程调度算法选出来的next进程
(本例假定为进程Y)的内核堆栈,并完成了进程上下?所需的EIP等寄存器状态切换。
5)标号1,即前述3.18.6内核的swtich_to代码第50?“”1:\t“ ”(地址为switch_to中的“$1f”),之后开始运?进程Y(这?进程Y曾经通过以上步骤被切换出去,因此可以
从标号1继续执?)
6)restore_all,恢复现场,与(3)中保存现场相对应。注意这?是进程Y的中断处理过程中,?(3)中保存现场是在进程X的中断处理过程中,因为内核堆栈从进程X
切换到进程Y了
7)iret - pop cs:eip/ss:esp/eflags,从Y进程的内核堆栈中弹出(2)中硬件完成的压栈内容。此时完成了中断上下?的切换,即从进程Y的内核态返回到进程Y的?户态
8)继续运??户态进程Y