结合中断上下文切换和进程上下文切换分析Linux内核的一般执行过程

实验要求

结合中断上下文切换和进程上下文切换分析Linux内核一般执行过程

  1. 以fork和execve系统调用为例分析中断上下文的切换
  2. 分析execve系统调用中断上下文的特殊之处
  3. 分析fork子进程启动执行时进程上下文的特殊之处
  4. 以系统调用作为特殊的中断,结合中断上下文切换和进程上下文切换分析Linux系统的一般执行过程

一、进程上下文切换和中断上下文的切换

  • CPU上下文切换

  CPU 寄存器,是 CPU 内置的容量小、但速度极快的内存。而程序计数器,则是用来存储CPU 正在执行的指令位置、或者即将执行的下一条指令位置。它们都是 CPU 在运行任何任务前,必须的依赖环境,因此也被叫做 CPU 上下文CPU 上下文切换,就是先把前一个任务的 CPU 上下文(也就是 CPU 寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。根据任务的不同,CPU 的上下文切换就可以分为几个不同的场景,也就是进程上下文切换、线程上下文切换以及中断上下文切换。

  • 进程上下文切换

  Linux 按照特权等级,把进程的运行空间分为内核空间和用户空间。进程既可以在用户空间运行,又可以在内核空间中运行。进程在用户空间运行时,被称为进程的用户态,而陷入内核空间的时候,被称为进程的内核态。从用户态到内核态的转变,需要通过系统调用来完成。内核空间具有最高权限,可以直接访问所有资源;用户空间只能访问受限资源,不能直接访问内存等硬件设备,必须通过系统调用陷入到内核中,才能访问这些特权资源。

  进程是由内核来管理和调度的,进程的切换只能发生在内核态。所以,进程的上下文不仅包括了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的状态。因此,进程的上下文切换过程中,在保存当前进程的内核状态和 CPU 寄存器之前,需要先把该进程的虚拟内存、栈等保存下来;而加载了下一进程的内核态后,还需要刷新进程的虚拟内存和用户栈。进程上下文的切换,是从一个进程的内核堆栈切换到另一个进程的内核堆栈

  • 中断上下文切换

  为了快速响应硬件的事件,中断处理会打断进程的正常调度和执行,转而调用中断处理程序,响应设备事件。而在打断其他进程时,就需要将进程当前的状态保存下来,这样在中断结束后,进程仍然可以从原来的状态恢复运行。跟进程上下文不同,中断是在?个进程当中从进程的?户态到进程的内核态,或从进程的内核态返回到进程的?户态,其中断上下?切换过程中最关键的栈顶寄存器sp和指令指针寄存器ip 是由CPU协助完成的,?进程切换是在不同的进程间进行切换,完全由内核来实现。所以,即便中断过程打断了一个正处在用户态的进程,也不需要保存和恢复这个进程的虚拟内存、全局变量等用户态资源。

  中断上下文,其实只包括内核态中断服务程序执行所必需的状态,包括 CPU 寄存器、内核堆栈、硬件中断参数等。对同一个 CPU 来说,中断处理比进程拥有更高的优先级,所以中断上下文切换并不会与进程上下文切换同时发生。同样道理,由于中断会打断正常进程的调度和执行,所以大部分中断处理程序都短小精悍,以便尽可能快的执行结束。另外,中断上下文切换也需要消耗 CPU,切换次数过多也会耗费大量的 CPU,甚至严重降低系统的整体性能。所以,当中断次数过多时,就需要注意去排查它是否会给你的系统带来严重的性能问题。

二、fork系统调用的中断上下文的切换

  库函数fork是?户态创建?个?进程的系统调?API接?。在正常触发系统调用时,?户态有?个int $0x80或syscall指令触发系统调?,跳转到系统调???的汇编代码。系统会在当前进程的内核堆栈上保存?些寄存器的值,包括当前执?程序的用户堆栈栈顶地址(SS:ESP)、当时的状态字(EFlags)、当时的 CS:EIP的值。同时会将当前进程内核堆栈的栈顶地址、内核的状态字等放?CPU 对应的寄存器,并且 CS:EIP 寄存器的值会指向中断处理程序的??,对于系统调?来讲是指向系统调?处理 的??。最后中断处理完毕之后执行 iret 指令,就会把之前保存的关键上下文和现场恢复到 CPU 中。

#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("child process, process id is %d\n",getpid());
}
else{
printf("parent process, process id is %d\n",getpid());
}
return 0;
}

  结合中断上下文切换和进程上下文切换分析Linux内核的一般执行过程

  我们通过以上代码创建子进程时,if条件判断中除了if (pid < 0)异常处理没被执?,else if (pid == 0)和else两段代码都被执?了,实际上fork系统调?把当前进程?复制了?个?进程,也就?个进程变成了两个进程,两个进程执?相同的代码,其实是if语句在两个进程中各执?了?次,由于判断条件不同,输出的信息也就不同。查看fork.c的代码如下。

/* 
 * Create a kernel thread. 
 */ 
pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags) 
{ 
 return _do_fork(&args); 
} 
SYSCALL_DEFINE0(fork) 
{ 
 return _do_fork(&args); 
} 
SYSCALL_DEFINE0(vfork) 
{ 
 return _do_fork(&args); 
} 
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp, 
 { 
 return _do_fork(&args) 
}

  可以看出,fork函数通过do_fork()函数创建进程,父进程创建一个子进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程号。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。进程形成了链表,父进程的fpid指向子进程的进程id,因为子进程没有子进程,所以其fpid为0。

  父进程调用fork,因为这是一个系统调用,会导致int软中断,进入内核空间,内核根据系统调用号,调用sys_fork系统调用,而sys_fork系统调用则是通过clone系统调用实现的。clone函数中会调用do_fork函数。do_fork函数主要完成了调?copy_process()复制?进程、获得pid、调?wake_up_new_task将?进程加?就绪队列等待调度执?等,copy_process函数主要完成了调?dup_task_struct复制当前进程(?进程)描述符task_struct、信息检查、初始化、把进程状态设置为TASK_RUNNING(此时?进程置为就绪态)、采?写时复制技术逐?复制所有其他进程资源、调?copy_thread_tls初始化?进程内核栈、设置?进程pid等。其中最关键的就是dup_task_struct复制当前进程(?进程)描述符task_struct和copy_thread_tls初始化?进程内核栈。

结合中断上下文切换和进程上下文切换分析Linux内核的一般执行过程

二、execve系统调用的中断上下文

  execve系统调用用于执行一个可执行程序,我们在shell中输入ls、vim 、cat等命令时,shell会调?execve系统调?接?函数将命令?参数和环境变量传递给 可执?程序的main函数。execve系统调?接?函数的函数原型如下:
int execve(const char *filename, char *const argv[],char *const envp[]); 
  fifilename为可执??件的名字,argv是以NULL结尾的命令?参数数组,envp同样是以NULL结尾的环境变量数组(使?命令man execve,可查看其说明)。编程使?的exec系列库函数都是execve系
统调?接?函数的封装接口,下面以execlp为例说明execve的系统调用。
#include <stdio.h> 
#include <stdlib.h> 
#include <unistd.h> 
int main(int argc, char * argv[]) 
{ 
 int pid; 
 /* fork another process */ 
 pid = fork(); 
 if (pid < 0) 
 { 
 /* error occurred */ 
 fprintf(stderr, "Fork Failed!"); 
 exit(-1); 
 } 
 else if (pid == 0) 
 { 
 /* child process */ 
 execlp("/bin/ls", "ls", NULL); 
 } 
 else 
 { 
 /* parent process */ 
 /* parent will wait for the child to complete*/ 
 wait(NULL); 
 printf("Child Complete!"); 
 exit(0); 
 } 
}
  当fork?个?进程时,会?成?进程的进程描述符、内核堆栈和?户态堆栈等,?进程是通过复制?进程的?部分内容创建的。?进程通过execlp加载可执?程序时重新布局?户
态堆栈,  ?户态堆栈的栈顶就是main函数调?堆栈框架,这就是程序的main函数起点的执?环境。

  execve系统调用的执行过程:陷入内核 -> 用do_execve加载新的可执行文件并进行可执行性检查 -> 将新的可执行文件映射到当前运行进程的进程空间中,并覆盖原来的进程数据 -> 将EIP的值设置为新的可执行程序的入口地址。如果可执行程序是静态链接的程序,或不需要其他的动态链接库,则新的入口地址就是新的可执行文件的main函数地址;如果可执行程序还需要其他的动态链接库,则入口地址是加载器ld的入口地址 -> 返回用户态,程序从新的EIP出开始继续往下执行。

   当execve系统调?返回时,返回的已经不是原来的那个可执?程序了,?是新的可执?程序。execve返回的是新的可执? 程序执?的起点,静态链接的可执??件也就是main函数的?致位置,动态链接的可执??件还需要ld链接好动态链接库再从main函数开始执?。整体的调?关系为sys_execve()或__x64_sys_execve -> do_execve() –> do_execveat_common() -> __do_execve_?le -> exec_binprm()-> search_binary_handler() -> load_elf_binary() -> start_thread()。

三、fork子进程启动执行时进程上下文
 
  对子进程来讲,?进程和?进程的绝?部分信息是完全?样的,但是有些信息是不能?样的,?如 pid 的值和内核堆栈。还有将新进程链接到各种链表中,要保存进程执?到哪个位置,有?个thread数据结构记录进程执?上下?的关键信息也不能?样,否则会发?问题。fork?个?进程的过程中,复制?进程的资源时采?了Copy OnWrite(写时复制)技术,不需要修改的进程资源??进程是共享内存存储空间的。
 
  从fork?进程的内核堆栈来看,从struct fork_frame可以看出它是在struct pt_regs的基础上增加了struct inactive_task_frame。task_struct数据结构的最后是保存进程上下?中CPU相关的?些状态信息的关键数据结构thread。?进程创建好了进程描述符、内核堆栈等,就可以通过wake_up_new_task(p)将?进程添加到就绪队列,使之有机会被调度执?,进程的创建?作就完成了,?进程就可以等待调度执?,?进
程的执?从这?设定的ret_from_fork开始。
  也就是说,?进程复制了?进程中所有的进程上下文信息,包括内核堆栈、进程描述符等,?进程作为?个独?的进程也会被调度。
 
四、结合中断上下文切换和进程上下文切换分析Linux系统的一般执行过程
 
  中断是在?个进程当中从进程的?户态到进程的内核态,或从进程的内核态返回到进程的?户态,其中断上下?切换过程中最关键的栈顶寄存器sp和指令指针寄存器ip 是由CPU协助完成的;?进程切换是在不同的进程间进行切换,完全由内核来实现,其中栈顶寄存器sp切换是通过进程描述符的thread.sp实现的,指令指针寄存器ip的切换是在内核堆栈切换的基础上巧妙利?call/ret指令实现的。这是二者的主要区别,但?般进程上下?切换是嵌套在中断上下?切换中的。
 
  以Linux内核中最常见的情况,正在运行的用户态进程X切换到运行用户态进程Y的过程为例进行说明。

  1. 正在运?的?户态进程X。

  2. 发?中断(包括异常、系统调?等),CPU完成load cs:rip(entry of a speci?c ISR),即跳转到中断处理程序??。

  3. 中断上下?切换,包括swapgs指令保存现场、加载当前进程内核堆栈栈顶地址到RSP寄存器、:将当前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. iret - pop cs:rip/ss:rsp/r?ags,从Y进程的内核堆栈中弹出(3)中对应的压栈内容。此时完 成了中断上下?的切换,即从进程Y的内核态返回到进程Y的?户态。

  8. 继续运??户态进程Y。

结合中断上下文切换和进程上下文切换分析Linux内核的一般执行过程

上一篇:Linux 远程控制管理


下一篇:[Linux Shell学习系列十四]sed和awk-5.awk基础