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

一、实验环境

os: linux   

虚拟机:QEMU   

内核版本 5.3.4   

调试方法:GDB

 

二、fork系统调用

 

fork系统的调用过程

fork函数的不同在于,os依照父进程的堆栈空间,复制了一份一模一样的堆栈空间给子进程,不过改变了子进程的进程号,所以子进程中也有一个fork函数,子进程从父进程fork后开始执行,子进程的fork函数会返回0  父进程的fork函数会返回子进程的进程号代表子进程创建成功

 

do fork系统调用的过程

_do_fork

  • copy_process             复制进程描述符和执⾏时所需的其他数据结构
    • dup_task_struct          复制进程描述符task_struct、创建内核堆栈等
    • copy_thread_tls          初始化⼦进程内核栈和thread
  • wake_up_new_task    将⼦进程添加到就绪队列

系统调用返回

 

fork系统调用实验

  1.编写程序,使用fork函数

#include "func.h"

int g=10;

int main()
{
        pid_t pid;
        pid = fork();
        int fd = open("file",O_RDWR);
        if(0==pid)
        {
                printf("I AM CHILD,mypid=%d,mydad_pid=%d\n",getpid(),getppid());

        }else
        {
                printf("I AM FATHER,mypid=%d,mychild_pid = %d\n",getpid(),pid);
                sleep(2);
                return 0;
        }
}

 

2.编译后执行结果

 

3.gdb跟踪

和上次实验一样,将fork可执行文件拷贝至home文件下下并打包成内存根文件系统镜像。然后进入gdb调试环境,并依次给上面提到的函数打上断点后continue

开启虚拟机,在__x64_sys_clone ,_do_forkcpoy_processdup_task_structcopy_thread_tls下断点,shell下运行fork可执行文件,查看此时函数栈

 

 

可以看到,在内核初始化时就多次捕捉到了_do_fork。进入内核后,运行new_fork,

 

 

三、execv系统调用

  什么是execve:进程创建的过程中,子进程先按照父进程复制出来,然后与父进程分离,单独执行一个可执行程序。这要用到系统调用execve。在调⽤execve系统调⽤时,当前的执⾏环境是从⽗进程复制过来的,execve系统调⽤加载完新的可执⾏程序之后已经覆盖了原来⽗进程的上下⽂环境。 execve在内核中帮我们重新布局了新的⽤户态执⾏环境即初始化了进程的用户态堆栈

 

execve系统调用对应的内核处理函数为sys_execve或者__x64_sys_execve,他们都是通过do_execve来完成具体的加载可执行文件的工作

整体的调⽤的递进关系为:

  • sys_execve()或__x64_sys_execve -> // 内核处理函数
  • do_execve() –> // 系统调用函数
  • do_execveat_common() -> // 系统调用函数
  • __do_execve_file ->
  • exec_binprm()-> // 根据读入文件头部,寻找该文件的处理函数
  • search_binary_handler() ->
  • load_elf_binary() -> // 加载elf文件到内存中
  • start_thread() // 开始新进程

四、对比fork、execve和普通的系统调用

系统调用可以视为一种特殊的中断,老的32位linux就是采用int 0x80中断指令进入内核,因此自然涉及中断上下文,也就是切换到用户内核栈,同时保存相关的寄存器使得中断结束后能够正常返回。当执行系统调用时,用户内核栈的结构如下:

 

  而fork系统调用特殊之处在于他创建了一个新的进程,且有两次返回。对于fork的父进程来说,fork系统调用和普通的系统调用并无两样。但是对fork子进程来说,需要设置子进程的进程上下文环境,这样子进程才能从fork系统调用后返回。

  而对于execve而言,由于execve使得新加载可执⾏程序已经覆盖了原来⽗进程的上下⽂环境,而原来的中断上下文就是保存的是原来的、被覆盖的进程的上下文,因此需要修改原来的中断上下文,使得系统调用返回后能够指向现在加载的这个可执行程序的入口,比如main函数的地址(静态链接下)。

 

五、进程上下文

(一)中断上下文和进程上下文对比

  程序在执行过程中通常有用户态和内核态两种状态,CPU对处于内核态根据上下文环境进一步细分,因此有了下面三种状态:

  (1)内核态,运行于进程上下文,内核代表进程运行于内核空间。
  (2)内核态,运行于中断上下文,内核代表硬件运行于内核空间。
  (3)用户态,运行于用户空间。

 

   中断是从一个进程的用户态切换到该进程的内核态,而中断上下文就是在中断这个过程中需要入栈保存的相关信息,以使得中断结束后能够正常返回进程的用户态。它包括ss、esp、eflag、cs、eip等。具体的:   

 

   1. 确定与中断或者异常关联的向量i0~255) 

 

   2. idtr寄存器指向的IDT表中的第i
      3. gdtr寄存器获得GDT的基地址, 并在GDT中查找,以读取IDT表项中的段选择符所标识的段描述符 
      4. 确定中断是由授权的发生源发出的
    5. 查是否发生了特权级的变化, 一般指是否由用户态陷入了内核态。如果是由用户态陷入了内核态, 控制单元必须开始使用与新的特权级相关的堆栈 
      a. tr寄存器, 访问运行进程的tss
    b. 用与新特权级相关的栈段和栈指针装载ssesp寄存器。 这些值可以在进程的tss段中找到   
    c. 在新的栈中保存ssesp以前的值, 这些值指明了与旧特权级相关的栈的逻辑地址 
      6. 若发生的是故障, 用引起异常的指令地址修改cs和eip寄存器的值, 以使得这条指令在异常处理结束后能被再次执行
      7. 在栈中保存eflags、 cseip的内容
      8. 如果异常产生一个硬件出错码, 则将它保存在栈中 
         9.  装载cseip寄存器, 其值分别是IDT表中第i项门描述符的段选择符和偏移量字段。 这对寄存器值给出中断或者异常处理程序的第一条指定的逻辑地址

 

中断上下文内核栈结构:

 

 

 

   进程切换是发生在两个不同的进程中,进程上下文就可以看作是用户进程传递给内核的这些参数以及内核要保存的那一整套的变量和寄存器值和当时的环境等。

 

   由于进程切换也是由内核完成的,所以必然要通过中断进入内核,也就是说进程上下文其实是包含了中断上下文的,就好像之前所说的fork子进程中的pt_regs和inactive_task_frame 。

 

  对比这两个上下文,中断是由cpu实现的,因此那些寄存器的入栈是由cpu来帮助完成的,也就是我们所说的硬件保存现场。而进程切换是靠内核完成的,栈顶寄存器sp切换是通过进程描述符的thread.sp实现的,指令指针寄存器ip的切换是在内核堆栈切换的基础上巧妙利⽤call/ret指令实现的。

 

(二)linux的进程管理

  linux的一般执行过程主要就是以下几种:进程的执行,进程切换和中断陷入内核。具体到进程的切换,它又涉及到不同的调度策略,如linux中的SCHED_NORMAL、SCHED_FIFO、 SCHED_RR、 SCHED_BATCH 等等。综合的一个例子如下,作为总结:


 

 

posted @ 2020-06-13 20:01  USTC_314  阅读(254)  评论(0编辑  收藏  举报