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

一、前言

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

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

完成一篇博客总结分析Linux系统的一般执行过程,以期对Linux系统的整体运作形成一套逻辑自洽的模型,并能将所学的各种OS和Linux内核知识/原理融通进模型中。

二、分析fork系统调用

   一个程序一调用fork函数,系统就为一个新的进程准备了前述三个段,首先,系统让新的进程与旧的进程使用同一个代码段,因为它们的程序还是相同的,对于数据段和堆栈段,系统则复制一份给新的进程,这样,父进程的所有数据都可以留给子进程,但是,子进程一旦开始运行,虽然它继承了父进程的一切数据,但实际上数据却已经分开,相互之间不再有影响了,也就是说,它们之间不再共享任何数据了。

  而如果两个进程要共享什么数据的话,就要使用另一套函数(shmget,shmat,shmdt等)来操作。现在,已经是两个进程了,对于父进程,fork函数返回了子程序的进程号,而对于子程序,fork函数则返回零,这样,对于程序,只要判断fork函数的返回值,就知道自己是处于父进程还是子进程中。

  fork执行的主要流程如下:

  (1) 通过系统调用号宏以及_syscal()l函数结合cpu寄存器和0x80中断从用户空间到内核空间。

  (2)进入内核空间之后,通过system_call对eax寄存器中的系统调用号以及其他寄存器传入的参数进行保存(SAVE ALL),并通过sys_call_table系统调用表进行查询找到内部系统调用函数sys_fork(),调用完后,将返回值通过eax寄存器带回用户态,然后RESTORE_ALL,将各个寄存器的值pop.

  下面是do_fork过程的主要流程:

 三、分析execve系统调用

   一个进程一旦调用exec类函数,它本身就“死亡”了,系统把代码段替换成新的程序的代码,废弃原有的数据段和堆栈段,并为新程序分配新的数据段与堆栈段,唯一留下的,就是进程号,也就是说,对系统而言,还是同一个进程,不过已经是另一个程序了。不过exec类函数中有的还允许继承环境变量之类的信息,这个通过exec系列函数中的一部分函数的参数可以得到。

   系统调用execve的内核入口为sys_execve,代码如下:

  asmlinkage int sys_execve(struct pt_regs regs)
    {
        int error;
        char * filename;
    
        filename = getname((char *) regs.ebx);
        error = PTR_ERR(filename);
        if (IS_ERR(filename))
            goto out;
        error = do_execve(filename, (char **) regs.ecx, (char **) regs.edx, &regs);
        if (error == 0)
            current->ptrace &= ~PT_DTRACE;
        putname(filename);
    out:
        return error;
    }

  sys_execve的核心是调用do_execve函数,传给do_execve的第一个参数是已经拷贝到内核空间的路径名filename,第二个和第三个参数仍然是系统调用execve的第二个参数argv和第三个参数envp,它们代表的传给可执行文件的参数和环境变量仍然保留在用户空间中。

  下面是do_execve的主要流程:

 四、fork和execve系统调用的主要不同点

  对于fork(): 

    1、子进程复制父进程的所有进程内存到其内存地址空间中。父、子进程的  “数据段”,“堆栈段”和“代码段”完全相同,即子进程中的每一个字节都和父进程一样。 
    2、子进程的当前工作目录、umask掩码值和父进程相同,fork()之前父进程打开的文件描述符,在子进程中同样打开,并且都指向相同的文件表项。 
    3、子进程拥有自己的进程ID。

  对于exec(): 
    1、进程调用exec()后,将在同一块进程内存里用一个新程序来代替调用exec()的那个进程,新程序代替当前进程映像,当前进程的“数据段”,  “堆栈段”和“代码段”背新程序改写。 
    2、新程序会保持调用exec()进程的ID不变。 
    3、调用exec()之前打开打开的描述字继续打开(好像有什么参数可以令打开 的描述字在新程序中关闭)

五、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

 

posted @ 2020-06-15 14:30  小习同学  阅读(164)  评论(0)    收藏  举报