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

一、实验要求

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

二、fork系统调用

  一个现有进程可以调用fork函数创建一个新进程。由fork创建的新进程被称为子进程,原进程称为父进程。父、子进程并发运行。创建新的子进程后,两个进程都将执行fork()系统调用之后的下一条指令。父、子进程使用相同的pc(程序计数器),相同的CPU寄存器,在父进程中使用的相同打开文件。调用fork之后,数据、堆、栈有两份,代码仍然为一份但是这个代码段成为两个进程的共享代码段都从fork函数中返回。

  fork函数的特殊之处在于:成功调用后返回两个值,是由于在复制时复制了父进程的堆栈段,所以两个进程都停留在fork函数中,等待返回。所以fork函数会返回两次,一次是在父进程中返回,另一次是在子进程中返回,这两次的返回值不同。
  • 在父进程中,fork返回新创建子进程的进程ID
  • 在子进程中,fork返回0
  • 如果出现错误,fork返回一个负值

  为更好的理解fork函数,我们先看以下代码的执行结果。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void main(){
  pid_t pid;
  char *message;
  int n;
  pid = fork();
  if(pid<0){
    perror("fork failed");
    exit(1);
  }
  if (pid == 0){
    message = "this is the child 
";
    n=4;
  }else {
    message = "this is the parent 
";
    n=2;
  }
  for(;n>0;n--){
    printf("%s",message);
    sleep(1);
  }
  //return 0;
}

  

  编译运行

  gcc forktest.c

  ./a.out

  结果

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

  代码中:父进程中变量n=2,循环打印了2次 this is the parent;子进程中n=4,循环打印了4次 this is the child;fork调用之后父进程和子进程的变量message和n被赋予不同的值,互不影响。

  linux系统下使用clone()系统调用实现fork()。fork(),vfork()和clone()库函数都根据各自需要的参数标志去调用clone(),然后由clone()去调用do_fork(). 再然后do_fork()完成了创建中的大部分工作。然后调用copy_process()。具体的实现步骤如下。

  1. fork(),vfork()和clone()库函数都根据各自需要的参数标志去调用clone(),然后由clone()去调用do_fork()
  2. do_fork()调用copy_process(),通过copy_process创建子进程
  3.copy_process() 通过 调用 dup_task_struct(),为新进程创建与其父进程相同的内核栈、thread_info、task_struct,此时父子进程pid相同
  4.检查当前用户拥有的进程数未超过分配给他的资源限制
  5.区别父子进程pid,部分进程pid成员清零或设置
  6.设置子进程state为TASK_UNINTERRUPTIBLE
  7.调用copy_flags()更新task_struct的flags,进程是否拥有超级用户权限清零,进程还没有调用exec()函数表示设置
  8.调用alloc_pid()为进程分配有效pid
  9.根据clone()的参数。cop_process()拷贝或共享打开的文件、进程的地址空间等
  10.copy_process()扫尾并返回指向子进程的指针

   总结来说,进程的创建过程⼤致是⽗进程通过fork系统调⽤进⼊内核_do_fork函数,复制进程描述符及相关进程资源(采⽤写时复制技术)、分配⼦进程的内核堆栈并对内核堆栈和thread等进程关键上下⽂进⾏初始化,最后将⼦进程放⼊就绪队列, fork系统调⽤返回;⽽⼦进程则在被调度执⾏时根据设置的内核堆栈和thread等进程关键上下⽂开始执⾏。 

 三、execve系统调用

  进程创建的过程中,子进程先按照父进程复制出来,然后与父进程分离,单独执行一个可执行程序。这要用到系统调用execve(),在c语言库中提供一整套库函数。execve() 系统调用的作用是运行另外一个指定的程序。它会把新程序加载到当前进程的内存空间内,当前的进程会被丢弃,它的堆、栈和所有的段数据都会被新进程相应的部分代替,然后会从新程序的初始化代码和 main 函数开始运行。同时,进程的 ID 将保持不变。
  execve() 系统调用通常与 fork() 系统调用配合使用。从一个进程中启动另一个程序时,通常是先 fork() 一个子进程,然后在子进程中使用 execve() 变身为运行指定程序的进程。 例如,当用户在 Shell 下输入一条命令启动指定程序时,Shell 就是先 fork() 了自身进程,然后在子进程中使用 execve() 来运行指定的程序。
  execve系统调用的执行过程:
  1. 陷入内核
  2. 加载新的可执行文件并进行可执行性检查
  3. 将新的可执行文件映射到当前运行进程的进程空间中,并覆盖原来的进程数据
  4. 将EIP的值设置为新的可执行程序的入口地址。如果可执行程序是静态链接的程序,或不需要其他的动态链接库,则新的入口地址就是新的可执行文件的main函数地址;如果可执行程序还需要其他的动态链接库,则入口地址是加载器ld的入口地址
  5. 返回用户态,程序从新的EIP出开始继续往下执行。至此,老进程的上下文已经被新的进程完全替代了,但是进程的PID还是原来的。从这个角度来看,新的运行进程中已经找不到原来的对execve调用的代码了,所以execve函数的一个特别之处是他从来不会成功返回,而总是实现了一次完全的变身。