• 结合中断上下文切换和进程上下文切换分析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

      结果

      

      代码中:父进程中变量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函数的一个特别之处是他从来不会成功返回,而总是实现了一次完全的变身。
     
     
  • 相关阅读:
    华为OJ机试训练(一)
    mount CIFS return ERR -12 and report Cannot allocate memory
    ftk学习记(icon篇)
    使用jquery-mockjax模拟ajax请求做前台測试
    Objective-C 内存管理之 _ARC
    [LeetCode]Decode Ways
    设计模式六大原则——迪米特法则(LoD)
    ACM/ICPC2014鞍山现场赛E hdu5074Hatsune Miku
    2015届校园招聘笔试/面试 基础知识点 总结
    依据Path取Json指定节点的值
  • 原文地址:https://www.cnblogs.com/LiScott/p/13132338.html
Copyright © 2020-2023  润新知