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

一 实验要求

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

二 实验内容

1. fork系统调用

fork系统调用把当前进程又复制了一个子进程,也就一个进程变成了两个进程,两个进程执行相同的代码,只是fork系统调用在父进程和子进程中的返回值不同。

1.1 通过fork程序完成系统调用

首先通过编写fork函数来完成fork的系统调用

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(int argc, char* argv[])
{
    int pid;

    pid = fork();
    if(pid<0)
    {
      //error
       fprintf(stderr,"For Failed");
       exit(-1);
    }
    else if(pid==0)
    {
       //child
       printf("this is child process \n");
    }
    else
    {
       //parent
       printf("this is Parent process \n");
       wait(NULL);
       printf("child complete \n");
    }
    return 0;
}
1.2 通过gdb调试追踪系统调用过程

通过使用gcc -o forktest forktest.c -static./forktest 完成函数的执行。
结合中断上下文切换和进程上下文切换分析Linux内核的一般执行过程
接下来通过上一次实验相同的方法可以得知fork函数的系统调用号,进一步找出中断函数__x64_sys_clone/linux/kernel/fork.c中,并且可以看出其中调用了_do_fork函数。
结合中断上下文切换和进程上下文切换分析Linux内核的一般执行过程
接下来依然是通过上一次实验相同的方法来完成系统调用过程。
通过在__x64_sys_clone_do_forkcpoy_processdup_task_structcopy_thread_tls打下断点,然后利用gdb可以得到。
结合中断上下文切换和进程上下文切换分析Linux内核的一般执行过程

1.3 fork过程中的内核堆栈情况

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

1.4 fork系统调用总结

进程的创建过程大致是父进程通过fork系统调用进入内核_do_fork函数,如下图所示复制进程描述符及相关进程资源(采用写时复制技术)、分配子进程的内核堆栈并对内核堆栈和thread等进程关键上下文进行初始化,最后将子进程放入就绪队列,fork系统调用返回;而子进程则在被调度执行时根据设置的内核堆栈和thread等进程关键上下文开始执行。
结合中断上下文切换和进程上下文切换分析Linux内核的一般执行过程

2. execve系统调用

2.1 execve系统调用基本情况

当前的可执?程序在执?,执?到execve系统调?时陷?内核态,在内核???do_execve加载可执??件,把当前进程的可执?程序给覆盖掉。当execve系统调?返回 时,返回的已经不是原来的那个可执?程序了,?是新的可执?程序。

2.2 execve系统调用递进关系

1. sys_execve()__x64_sys_execve
2. do_execve()
3. do_execveat_common()
4. __do_execve_file
5. exec_binprm()
6. search_binary_handler()
7. oad_elf_binary()
8. start_thread()

2.3 execve系统调用总结

execve系统调用过程及其上下文的变化情况如下:

  1. 陷入内核
  2. 加载新的进程
  3. 将新的进程,完全覆盖原先进程的数据空间
  4. 将 IP 值设置为新的进程的入口地址
  5. 返回用户态,新程序继续执行下去。老进程的上下文被完全替换,但进程的 pid 不变,所以 execve 系统调用不会返回原进程,而是返回新进程。

3. execve系统调用和fork子进程启动执行时中断上下文的特殊之处

系统调用可以看做是一种特殊的中断,因此自然涉及中断上下文,也就是切换到用户内核栈,同时保存相关的寄存器使得中断结束后能够正常返回。
fork系统调用特殊之处在于其创建了一个新的进程,并且在父子进程中各有一次返回。对于fork的父进程来说,fork系统调用和普通的系统调用基本相同。但是对fork子进程来说,需要设置子进程的进程上下文环境,这样子进程才能从fork系统调用后返回。
而对于execve而言,由于execve使得新加载可执?程序已经覆盖了原来?进程的上下?环境,而原来的中断上下文就是保存的是原来的、被覆盖的进程的上下文,因此需要修改原来的中断上下文,使得系统调用返回后能够指向现在加载的这个可执行程序的入口。

4. Linux系统的一般执行过程

  1. 正在运行的用户态进程X。
  2. 发生中断(包括异常、系统调用等),CPU完成load cs:rip(entry of a specific ISR),即跳转到中断处理程序入口。
  3. 中断上下文切换,具体包括如下几点:
    • swapgs指令保存现场,可以理解CPU通过swapgs指令给当前CPU寄存器状态做了一个快照。
      - rsp point to kernel stack,加载当前进程内核堆栈栈顶地址到RSP寄存器。快速系统调用是由系统调用入口处的汇编代码实现用户堆栈和内核堆栈的切换。
    • save cs:rip/ss:rsp/rflags:将当前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。

三 实验小结

通过这次实验,我通过以fork和execve系统调用为例分析了中断上下文的切换,并结合上一次实验的学习完成了相应的调试分析过程。对于execve系统调用和fork子进程启动执行时中断上下文的特殊之处有了一定的了解,最后结合中断上下文切换和进程上下文切换分析Linux系统的一般执行过程,对于Linux系统的一般执行过程有了更加清楚的认识。本次实验收获颇丰。

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

上一篇:shell中的单引号,双引号,反引号


下一篇:【原创】Linux重命名根路径挂载点/的lv名称,导致系统崩溃解决方案