结合中断上下文切换和进程上下文切换分析Linux内核一般执行过程
- 以fork和execve系统调用为例分析中断上下文的切换
- 分析execve系统调用中断上下文的特殊之处
- 分析fork子进程启动执行时进程上下文的特殊之处
- 以系统调用作为特殊的中断,结合中断上下文切换和进程上下文切换分析Linux系统的一般执行过程
完成一篇博客总结分析Linux系统的一般执行过程,以期对Linux系统的整体运作形成一套逻辑自洽的模型,并能将所学的各种OS和Linux内核知识/原理融通进模型中。
CPU上下文切换可以分为以下三种
1.进程上下文切换
是指从一个进程切换到另一个进程运行。一个进程既可以在用户空间运行,也可以在内核空间运行,因此进程的上下文不仅包括了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的状态。进程上下文切换需要保存上述资源,加载新进程(内核态)并装入新进程的上下文
2.线程上下文切换
线程是调度的基本单位,进程是资源拥有的基本单位 通俗理解:系统内核中的任务调度实际上调度的对象是线程,而进程只是给线程提供了虚拟内存,全局变量等资源这些资源在线程上下文切换时是不需要修改的
3.中断上下文切换
中断优先级会打断进程的正常调度和执行
对于同一个CPU来说,中断处理比进程拥有更高的优先级
硬件/软件通过中断触发信号,导致内核调用中断处理程序,进入内核空间。这个过程中,硬件的一些变量和参数也要传递给内核,内核通过这些参数进行中断处理。中断上下文可以看作就是硬件传递过来的这些参数和内核需要保存的一些其他环境。因此中断上下文的切换是一直发生在内核态的,相比进程上下文切换要轻量很多。
中断上下?和进程上下?的?个关键区别是堆栈切换的?法。中断是由CPU实 现的,所以中断上下?切换过程中最关键的栈顶寄存器sp和指令指针寄存器ip 是由CPU协助完成的;进程切换是由内核实现的,所以进程上下?切换过程中 最关键的栈顶寄存器sp切换是通过进程描述符的thread.sp实现的,指令指针 寄存器ip的切换是在内核堆栈切换的基础上巧妙利?call/ret指令实现的。
分析fork子进程启动执行时进程上下文的特殊之处
一个进程,包括代码、数据和分配给进程的资源。fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。在语句fpid=fork()之前,只有一个进程在执行这段代码,但在这条语句之后,就变成两个进程在执行了,这两个进程的几乎完全相同。
#include <unistd.h> #include <stdio.h> int main () { pid_t fpid; //fpid表示fork函数返回的值 int count=0; fpid=fork(); if (fpid < 0) printf("error in fork!"); else if (fpid == 0) { printf("我是子进程,进程号是%d\n",getpid()); count++; } else { printf("我是父进程,进程号是%d\n",getpid()); count++; } printf("统计结果是: %d\n",count); return 0;
fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:
1)在父进程中,fork返回新创建子进程的进程ID;
2)在子进程中,fork返回0;
3)如果出现错误,fork返回一个负值;
在fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。进程形成了链表,父进程的fpid(p 意味point)指向子进程的进程id,因为子进程没有子进程,所以其fpid为0.
fork的系统调用实现:
父进程调用fork,因为这是一个系统调用,会导致int软中断,进入内核空间;
内核根据系统调用号,调用sys_fork系统调用,而sys_fork系统调用则是通过clone系统调用实现的,会调用clone系统调用。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; /* * 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 ((clone_flags & CSIGNAL) != SIGCHLD) trace = PTRACE_EVENT_CLONE; else trace = PTRACE_EVENT_FORK; if (likely(!ptrace_event_enabled(current, trace))) trace = 0; } p = copy_process(clone_flags, stack_start, stack_size, child_tidptr, NULL, trace); /* * Do this prior waking up the new thread - the thread pointer * might get invalid after that point, if the thread exits quickly. */ 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); /* 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); } else { nr = PTR_ERR(p); } return nr; }
1.创建进程描述符指针struct task_struct *p;
2.p = copy_process(clone_flags, stack_start, regs, stack_size, child_tidptr, NULL, trace);
调用copy_process为子进程复制出一份进程信息,并对相应的标志位等进行设置。
3.最后,copy_process 函数做了一些清理工作,返回一个指向新建的子进程的指针给 do_fork 函数。回到do_fork函数中,如果 copy_process 函数执行成功,没有错误,那么将会唤醒新创建的子进程,让子进程运行。
自此,fork 函数调用成功执行。
分析execve()
在调用execve系统调用时,当前的执行环境是从父进程复制过来的, execve系统调用加载完新的可执行程序之后已经覆盖了原来父进程的上下文环境。execve 系统调用在内核中帮我们重新布局了新的用户态执行环境。
正常的一个系统调用都是陷入内核态,再返回到用户态,然后继续执行系统调用后的下一条指令。上文讲到,fork和其他系统调用不同之处是它在陷入内核态之后有两次返回,第一次返回到原来的父进程的位 置继续向下执行,这和其他的系统调用是一样的。在子进程中fork也返回了一次,会返回到一个特定的点——ret_from_fork,通过内核构造的堆栈环境,它可以正常系统调用返回到用户态,所以它 稍微特殊一点。同样,execve也比较特殊。当前的可执行程序在执行,执行到execve系统调用时陷入内核态,在内核里面用do_execve加载可执行文件,把当前进程的可执行程序给覆盖掉。当execve系统调用返回时,返回的已经不是原来的那个可执行程序了,而是新的可执行程序。execve返回的是新的可执行程序执行的起点,静态链接的可执行文件也就是main函数的大致位置,动态链接的可执行文件还需 要ld链接好动态链接库再从main函数开始执行。
中断上下文的切换的一般流程
以32位x86系统结构linux-3.18.6为例,以系统调?作为特殊的中断简要总结如下:
(1)正在运?的?户态进程X。
(2)发?中断(包括异常、系统调?等),CPU完成load cs:rip(entry of a speci?c ISR),即跳转到中断处理程序??。
(3)中断上下?切换,具体包括如下?点:
- swapgs指令保存现场,可以理解CPU通过swapgs指令给当前CPU寄存器状态做了?个快照。
- rsp point to kernel stack,加载当前进程内核堆栈栈顶地址到RSP寄存器。快速系统调?是由系统调???处的汇编代码实现?户堆栈和内核堆栈的切换。
- save cs:rip/ss:rsp/r?ags:将当前CPU关键上下?压?进程X的内核堆栈,快速系统调?是由系统调???处的汇编代码实现的。
此时完成了中断上下?切换,即从进程X的?户态到进程X的内核态。
(4)中断处理过程中或中断返回前调?了schedule函数,其中完成了进程调度算法选择next进程、进程地址空间切换、以及switch_to关键的进程上下?切换等。
(5)switch_to调?了__switch_to_asm汇编代码做了关键的进程上下?切换。将当前进程X的内核堆栈切换到进程调度算法选出来的next进程(本例假定为进程Y)的内核堆栈,并完成了进程上下?所需的指令指针寄存器状态切换。之后开始运?进程Y(这?进程Y曾经通过以上步骤被切换出去,因此可以从switch_to下??代码继续执?)。
(6)中断上下?恢复,与(3)中断上下?切换相对应。注意这?是进程Y的中断处理过程中,?(3)中断上下?切换是在进程X的中断处理过程中,因为内核堆栈从进程X 切换到进程Y了。
(7)为了对应起?,中断上下?恢复的最后?步单独拿出来(6的最后?步即是7)iret - pop cs:rip/ss:rsp/r?ags,从Y进程的内核堆栈中弹出(3)中对应的压栈内容。此时完 成了中断上下?的切换,即从进程Y的内核态返回到进程Y的?户态。注意快速系统调?返回sysret与iret的处理略有不同。
(8)继续运??户态进程Y。