Linux 进程执行过程分析

Linux 进程执行过程分析

编号411,本文参考孟宁老师 Github 项目 https://github.com/mengning/linuxkernel

进程的基本要素

由于系统进程有一定的特殊性,这里主要分析普通用户进程。一般来讲,Linux 系统下的进程有几个基础要素:

  • 可执行代码

    可执行代码是进程的基本要素,这部分包含表示程序功能的进程私有代码和共享的链接库代码。

  • 系统专用系统堆栈空间

    进程专用的系统堆栈空间

  • 进程控制块

    即task_stuct数据结构,一方面,进程控制块包含的内容为内核调度提供了数据,另一方面,这个结构体记录了该进程的私有资源。

  • 独立存储空间

    独立的存储空间,即表示该进程拥有专有的用户空间(用户空间堆栈)。

除了以上的基本要素外,为了理解进程的执行过程,我们还需要理解以下基本内容:

  • 进程的生命周期

Linux 进程执行过程分析
一个进程被fork出来后,进入就绪态;当被调度到获得CPU执行时,进入执行态;如果时间片用完或被强占时,进入就绪态;资源得不到满足时,进入睡眠态(深度睡眠或浅度睡眠),比如一个网络程序,在等对方发包,此时不能占着CPU,进入睡眠态,当包发过来时,进程被唤醒,进入就绪态;如果被暂停,进入停止态;执行完成后,资源释放,此时父进程wait4还未收到它的信号,进入僵死态。即整个周期可能会涉及的状态有:就绪态,执行态,僵死态,停止态,睡眠态。

  • 进程执行相关的系统调用

    • fork() 调用

    1)在父进程中,fork返回新创建子进程的进程ID;
    2)在子进程中,fork返回0;
    3)如果出现错误,fork返回一个负值;

    在fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。

    • execve() 调用

      • 预处理
        首先在内核空间分配一个物理页面,然后调用do_getname()从用户空间拷贝文件名字符串。
    • 调用主体函数do_execve()

      • 我们既然要执行参数中给的二进制文件,首先需要打开文件,获取文件句柄file

      • 然后我们需要一个linux_binprm结构体去保存函数具体的参数信息,包括文件名,argv,envp,还会将文件前128字节读到linux_binprm.buf中。

      • 因为可执行文件的种类很多,比如elf,a.out等格式。我们需要从内核全局linux_binfmt队列中找到一个能够处理参数中所给的可执行文件的linux_binfmt结构,具体就是依次试用linux_binfmt结构中各自的load_binary()函数。

    • 可执行文件的装载和投运(a.out为例)

      • 与过去决裂,释放用户空间。
        既然是要执行参数中给定的二进制文件,就需要放弃可能从父进程继承下来的用户空间,而使用本进程自己的用户空间。因此,需要检查是否与父进程通过指针共享用户空间,还是之前复制父进程用户空间。如果通过指针共享,说明本进程本身没有自己的用户空间,之前称为“进程”不合适,应该称作线程,就直接申请进程用户空间。如果复制父进程的用户空间,这是就需要全部释放。

      • 装载可执行文件数据段代码段
        这时可以将可执行文件装入进程的用户空间了,这时分两种情况:

        • 可执行文件不是"纯代码",需要通过do_brk()扩展数据段+代码段大小的空间,然后通过read()读取文件内容到用户空间

        • 否则,如果文件系统提供mmap(),并且数据段和代码段长度与页面大小对齐,直接通过文件映射读取到用户空间,否则,通过1方法读取。

      • 装载可执行文件堆栈段和bss段

      用户空间堆栈区顶部当然是用户虚存空间顶部,即TASK_SIZE,为3GB,虚存地址为0xC000 0000的位置。

      这里主要是设置用户堆栈区,包括envp[],argv[]以及argc

      • start_thread()

代码实测

  • 编写 fork() 和 execve() 的测试代码并进行测试

    编写以下两个文件

    • helloworld 文件
    // file helloword.c
    #include <stdio.h>
    #include <unistd.h>
    #include <sys/types.h>
    
    int main()
    {
        printf("Hello world!\n");
        pid_t pid = getpid();
        printf("pid of helloworld from helloword is %d\n", pid);
    
        return 0;
    }
    // end helloword.c
    
    • start_process 文件
    // file start_process.c
    #include<stdio.h>
    #include<unistd.h>
    #include<sys/types.h>
    
    int main()
    {
            pid_t pid;
            char *argv_execve[]={"helloworld", NULL};
            char *envp[]={"PATH=/home/coolxxy/Code/ex03/", "USER=coolxxy", "STATUS=testing", NULL};
    
            pid = getpid();
            printf("pid of start_process is :%d\n",pid);
    
            printf("Starting systemcall execve......\n");
            pid_t temp = fork();
            if(temp == 0)
            {
                    printf("i am here because i find fork() return 0, pid getted after fork() %d\n", getpid());
                    if(execve("./helloworld", argv_execve, envp) < 0)
                    {
                            perror("Error on execve");
                    }
            }
            else
            {
                    printf("i am here because i find fork() return a posivate value %d, pid getted after fork() is %d\n", temp, getpid());
            }
    
            return 0;
    }
    // end file start_process.c
    

    分别编译以上两个文件并执行 start_process 文件,可以获得以下输出:

    Linux 进程执行过程分析

    通过这个测试,可以发现 fork() 执行了两次返回,父进程中返回了子进程的 pid, 子进程中返回了0.

  • 通过 gdb 调试分析 _do_fork() 过程调用

    我分别给内核种的 _do_fork() 和 copy_process 函数添加断点并分析其调用过程

Linux 进程执行过程分析
Linux 进程执行过程分析
Linux 进程执行过程分析

通过追踪这两个函数的执行过程,可以发现,_do_fork() 的执行过程大致如下代码注释所示

long do_fork(unsigned long clone_flags,
        unsigned long stack_start,
        struct pt_regs *regs,
        unsigned long stack_size,
        int __user *parent_tidptr,
        int __user *child_tidptr)
{
  struct task_struct *p;//在内存中分配一个 task_struct 数据结构,以代表即将产生的新进程
  int trace = 0;
  long nr;

  /*
   * Do some preliminary argument and permissions checking before we
   * actually start allocating stuff
   */
  if (clone_flags & CLONE_NEWUSER) {          //clone and new user yes
      if (clone_flags & CLONE_THREAD)
          return -EINVAL;
      /* hopefully this check will go away when userns support is
       * complete
       */
      if (!capable(CAP_SYS_ADMIN) || !capable(CAP_SETUID) ||
              !capable(CAP_SETGID))
          return -EPERM;
  }

  /*
   * We hope to recycle these flags after 2.6.26
   */
  if (unlikely(clone_flags & CLONE_STOPPED)) {
      static int __read_mostly count = 100;

      if (count > 0 && printk_ratelimit()) {
          char comm[TASK_COMM_LEN];

          count--;
          printk(KERN_INFO "fork(): process `%s' used deprecated "
                  "clone flags 0x%lx\n",
              get_task_comm(comm, current),
              clone_flags & CLONE_STOPPED);
      }
  }

  /*
   * When called from kernel_thread, don't do user tracing stuff.
   */
  if (likely(user_mode(regs)))
      trace = tracehook_prepare_clone(clone_flags);

  p = copy_process(clone_flags, stack_start, regs, stack_size,
           child_tidptr, NULL, trace);//把父进程 PCB 的内容复制到新进程的 PCB 中。
  /*通过copy_process()函数完成具体的进程创建工作,返回值类型为task_t类型
   * Do this prior waking up the new thread - the thread pointer
   * might get invalid after that point, if the thread exits quickly.
   */
  if (!IS_ERR(p)) {//函数 IS_ERR()分析copy_process()的返回值是否正确。
      struct completion vfork;//定义struct completion 类型的变量 vfork;

      trace_sched_process_fork(current, p);

      nr = task_pid_vnr(p);

      if (clone_flags & CLONE_PARENT_SETTID)
          put_user(nr, parent_tidptr);

      if (clone_flags & CLONE_VFORK) {//判断clone_flags中是否有CLONE_VFORK标志
          p->vfork_done = &vfork;
          init_completion(&vfork);/*这个函数的作用是在进程创建的最后阶段,父进程会将自己设置为不可中断状态,然后睡眠在
等待队列上(init_waitqueue_head()函数 就是将父进程加入到子进程的等待队列),等待子进程的唤醒。*/
      }

      audit_finish_fork(p);
      tracehook_report_clone(regs, clone_flags, nr, p);

      /*
       * We set PF_STARTING at creation in case tracing wants to
       * use this to distinguish a fully live task from one that
       * hasn't gotten to tracehook_report_clone() yet.  Now we
       * clear it and set the child going.
       */
      p->flags &= ~PF_STARTING;

      if (unlikely(clone_flags & CLONE_STOPPED)) {
          /*
           * We'll start up with an immediate SIGSTOP.
           */
          sigaddset(&p->pending.signal, SIGSTOP);
          set_tsk_thread_flag(p, TIF_SIGPENDING);
          __set_task_state(p, TASK_STOPPED);
      } else {
          wake_up_new_task(p, clone_flags);
      }

      tracehook_report_clone_complete(trace, regs,
                      clone_flags, nr, p);

      if (clone_flags & CLONE_VFORK) {
          freezer_do_not_count();
          wait_for_completion(&vfork);
          freezer_count();
          tracehook_report_vfork_done(p, nr);
      }
  } else {
      nr = PTR_ERR(p);
  }
  return nr;
}

参考 :https://blog.****.net/u012375924/article/details/87903620
参考 :https://blog.****.net/yiqiaoxihui/article/details/80385546