Linux内核分析:实验六--Linux进程的创建过程分析

Posted Lansing

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux内核分析:实验六--Linux进程的创建过程分析相关的知识,希望对你有一定的参考价值。

刘畅 原创作品转载请注明出处 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000

概述

本次实验在MenuOS中加入fork系统调用,并通过GDB的调试跟踪,近距离的观察Linux中进程创建的过程。阅读Linux进程部分的源码,结合起来理解Linux内核创建新进程的过程。

Linux中对进程的描述

Linux中task_struct结构体用于描述系统中的进程,对应x86机器的此结构体定义放在了/include/linux/sched.h中。这个结构体相当庞大,为了能快速的理解进程的创建过程,这里简单的浏览一下task_struct结构体的定义。

struct task_struct {
  /* 表示进程的状态 */
  volatile long state;  /* -1 unrunnable, 0 runnable, >0 stopped */
  /* 进程的内核栈,通过alloc_thread_info来分配的 */
    void *stack;
  ...
  /* 进程的ID和所在进程组的ID */
  pid_t pid;
  pid_t tgid;

  /* 表示进程间的关系 */
  struct task_struct __rcu *real_parent; /* real parent process */
  struct task_struct __rcu *parent; /* recipient of SIGCHLD, wait4() reports */
  /*
   * children/sibling forms the list of my natural children
   */
  struct list_head children;    /* list of my children */
  struct list_head sibling; /* linkage in my parent's children list */
  struct task_struct *group_leader; /* threadgroup leader */

  ...

  /* 表示进程的静态优先级 */
  int prio, static_prio, normal_prio;
  /* 进程的实时优先级 */
    unsigned int rt_priority;
    const struct sched_class *sched_class;
    struct sched_entity se;
    struct sched_rt_entity rt;
  /* 表示调度策略 */
  unsigned int policy;
    int nr_cpus_allowed;
  /* 表示进程在哪个CPU上执行 */
    cpumask_t cpus_allowed;

  /* CPU-specific state of this task */
    struct thread_struct thread;
/* filesystem information */
    struct fs_struct *fs;
/* open file information */
    struct files_struct *files;
/* namespaces */
    struct nsproxy *nsproxy;
/* signal handlers */
    struct signal_struct *signal;
    struct sighand_struct *sighand;

    sigset_t blocked, real_blocked;
    sigset_t saved_sigmask; /* restored if set_restore_sigmask() was used */
    struct sigpending pending;

}

task_struct结构体用于描述Linux中的进程,进程之间是使用双向链表链接起来的,这里简要的浏览了一下task_struct。进程的状态:TASK_RUNNING表示进程要么正在执行,要么正要准备执行。进程的状态转移图如下图所示,图片来自《Linux Kernel Development》:

这里写图片描述

Linux中进程的创建

在实验三分析Linux内核启动的过程中,我们了解到内核在rest_init函数中,使用kernel_thread创建了2个进程kernel_init和kthreadd,通过GDB跟踪我们知道它们都是用do_fork创建的。在这里,普通进程通过fork系统调用产生一个新进程也是通过do_fork实现的。我在机器上跟踪fork进程的过程,如下:

这里写图片描述

这里写图片描述

这里写图片描述

do_fork过程理解

通过GDB跟踪可以看出Linux中产生新的进程最终要通过do_fork来实现的,下面是do_fork的源码部分。

long do_fork(unsigned long clone_flags,
          unsigned long stack_start,
          unsigned long stack_size,
          int __user *parent_tidptr,
          int __user *child_tidptr)
{
  /* 进程的结构体 */
    struct task_struct *p;
    int trace = 0;
    long nr;

    /*
     * Determine whether and which event to report to ptracer.  When
     * called from kernel_thread or CLONE_UNTRACED is explicitly
     * requested, no event is reported; otherwise, report if the event
     * for the type of forking is enabled.
     */
  /* 克隆进程的一些标志部分,属于哪类的fork */
    if (!(clone_flags & CLONE_UNTRACED)) {
        if (clone_flags & CLONE_VFORK)
            trace = PTRACE_EVENT_VFORK;
        else if ((clone_flags & CSIGNAL) != SIGCHLD)
            trace = PTRACE_EVENT_CLONE;
        else
            trace = PTRACE_EVENT_FORK;

        if (likely(!ptrace_event_enabled(current, trace)))
            trace = 0;
    }
  /* 复制进程的一些变量定义,返回一个新进程的结构体指针 */
    p = copy_process(clone_flags, stack_start, stack_size,
             child_tidptr, NULL, trace);
    /*
     * 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)) {
        struct completion vfork;
        struct pid *pid;

        trace_sched_process_fork(current, p);

        pid = get_task_pid(p, PIDTYPE_PID);
        nr = pid_vnr(pid);

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

        if (clone_flags & CLONE_VFORK) {
            p->vfork_done = &vfork;
            init_completion(&vfork);
            get_task_struct(p);
        }

        wake_up_new_task(p);

        /* forking complete and child started to run, tell ptracer */
        if (unlikely(trace))
            ptrace_event_pid(trace, pid);

        if (clone_flags & CLONE_VFORK) {
            if (!wait_for_vfork_done(p, &vfork))
                ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
        }

        put_pid(pid);
    } else {
        nr = PTR_ERR(p);
    }
    return nr;
}

下面是do_fork()函数执行的主要步骤:

  1. 通过查找pidmap_array位图,为子进程分配新的PID
  2. 检查父进程的ptrace字段:如果它的值不等于0,说明有另外一个进程正在跟踪父进程,因而,do_fork()函数检查debugger程序是否自己想跟踪子进程。在这种情况下,如果子进程不是内核线程(CLONE_UNTRACED标志被清0),则do_fork()函数设置CLONE_PTRACE标志。
  3. 调用copy_process()函数复制进程描述符。如果所有必须的资源都是可用的,则该函数返回刚创建的task_struct描述符的地址。
  4. 如果设置了CLONE_STOPPED标志,或者必须跟踪子进程,即在p->ptrace 中设置 PT_PTRACED标志,那么子进程的状态被设置成TASK_STOPPED状态,并且为子进程增加挂起的SIGSTOP信号。在另一个进程把子进程状态恢复成TASK_RUNNING之前,一直保持该状态。

  5. 如果没有设置CLONE_STOPPED标志,则调用wake_up_new_task(p, clone_flags)函数以执行以下操作:

a.调整父进程和子进程的调度参数
b.如果子进程和父进程运行在同一个CPU上,而且父进程和子进程不能共享同一组页表(CLONE_VM标志被清0),那么,就把子进程插入到父进程的运行队列,插入时让子进程恰好在父进程前面,因此迫使子进程优于父进程先运行。如果子进程刷新其地址空间,并且在创建之后执行新程序,那么这种简单的处理会产生较好的性能。而如果我们让父进程先运行,那么写时复制机制将会执行一些不必要的页面复制。
c.否则,如果子进程与父进程运行在不同CPU上,或者父进程和子进程共享同一组页表(CLONE_VM标志被设置),就把子进程插入父进程所在运行队列的队尾。

  1. 如果设置了CLONE_STOPPED标志,则子进程的状态被设置成TASK_STOPPED状态。
  2. 如果父进程被跟踪,则把子进程的PID存入current的ptrace_message字段并调用ptrace_notify函数使当前进程停止运行,并向当前进程的父进程发送SIGCHLD信号。子进程的祖父进程是跟踪父进程的debugger进程。SIGCHLD信号通知debugger进程:当前进程current已经创建了一个子进程,可以通过current->ptrace_message字段获得该子进程的PID。
  3. 如果设置了CLONE_VFORK标志,则把父进程插入等待队列,并挂起父进程直到子进程释放自己的内存地址空间(也就是说,直到子进程结束或执行新的程序)。
  4. 结束并返回子进程的PID。

此处参考CSDN-do_fork()函数详解

copy_process的过程

从do_fork中可以看出,copy_process过程返回了新进程的结构体指针,这里面完成了新进程的初始化工作。下面对copy_process过程简要的浏览一下:

/*
  创建进程描述符以及子进程所需要所有数据结构
*/
static struct task_struct *copy_process(unsigned long clone_flags,
          unsigned long stack_start,
          unsigned long stack_size,
          int __user *child_tidptr,
          struct pid *pid,
          int trace)
{
  int retval;
  struct task_struct *p;

  /* 首先以当前进程为蓝本,复制一个task_struct结构体 */
  p = dup_task_struct(current);

  /* 进行一些权限条件判断 */
  if (atomic_read(&p->real_cred->user->processes) >=
      task_rlimit(p, RLIMIT_NPROC)) {
    if (p->real_cred->user != INIT_USER &&
        !capable(CAP_SYS_RESOURCE) && !capable(CAP_SYS_ADMIN))
      goto bad_fork_free;
  }

  retval = -EAGAIN;
  if (nr_threads >= max_threads)
    goto bad_fork_cleanup_count;

  /* 把新进程加入到调度队列 */
  retval = sched_fork(clone_flags, p);

  /* 初始化新进程的内核栈 */
  retval = copy_thread(clone_flags, stack_start, stack_size, p);
  if (retval)
    goto bad_fork_cleanup_io;

  if (pid != &init_struct_pid) {
    retval = -ENOMEM;
    pid = alloc_pid(p->nsproxy->pid_ns_for_children);
    if (!pid)
      goto bad_fork_cleanup_io;
  }

  p->pid = pid_nr(pid);

  /* 这里设置新创建进程的关系 */
  if (clone_flags & CLONE_THREAD) {
    p->exit_signal = -1;
    p->group_leader = current->group_leader;
    p->tgid = current->tgid;
  } else {
    if (clone_flags & CLONE_PARENT)
      p->exit_signal = current->group_leader->exit_signal;
    else
      p->exit_signal = (clone_flags & CSIGNAL);
    p->group_leader = p;
    p->tgid = p->pid;
  }

  if (clone_flags & (CLONE_PARENT|CLONE_THREAD)) {
    p->real_parent = current->real_parent;
    p->parent_exec_id = current->parent_exec_id;
  } else {
    p->real_parent = current;
    p->parent_exec_id = current->self_exec_id;
  }

  attach_pid(p, PIDTYPE_PID);
  nr_threads++;

  return p;
}

copy_process主要做了如下工作:
- 初始化新进程的结构体
- 设置进程的内核栈
- 将新进程加入到调度队列
- 设置新进程的关系图,比如它的父进程是谁,该进程所在进程组的leader信息等
- 返回新进程结构体的指针

Linux进程创建的过程总览

在Linux中,可以通过如下几种方式创建一个新进程:

SYSCALL_DEFINE0(fork)
{
#ifdef CONFIG_MMU
    return do_fork(SIGCHLD, 0, 0, NULL, NULL);
#else
    /* can not support in nommu mode */
    return -EINVAL;
#endif
}
SYSCALL_DEFINE0(vfork)
{
    return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0,
            0, NULL, NULL);
}
SYSCALL_DEFINE6(clone, unsigned long, clone_flags, unsigned long, newsp,
    int, stack_size,
    int __user *, parent_tidptr,
    int __user *, child_tidptr,
    int, tls_val)
{
    return do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr);
}

可以看出 fork 、vfork 、clone 都是对 do_fork函数的一种封装,它们的参数个数以及标志位不同,也就是说 Linux中最终要通过do_fork来创建一个新的进程。在上面do_fork的源码分析过程中,我们了解了 do_fork 的工作流程,其中又分析了 copy_process的过程,大致了解了新进程的创建过程。

还剩下最后一个比较关键的问题,新创建的进程是从哪执行的呢?

在copy_process中有copy_thread调用,其中copy_thread中指明了新建立的进程从哪里执行。

int copy_thread(unsigned long clone_flags, unsigned long sp,
    unsigned long arg, struct task_struct *p)
{
    struct pt_regs *childregs = task_pt_regs(p);
    struct task_struct *tsk;
    int err;

    p->thread.sp = (unsigned long) childregs;
    p->thread.sp0 = (unsigned long) (childregs+1);
    memset(p->thread.ptrace_bps, 0, sizeof(p->thread.ptrace_bps));

    if (unlikely(p->flags & PF_KTHREAD)) {
        /* kernel thread */
        memset(childregs, 0, sizeof(struct pt_regs));
        p->thread.ip = (unsigned long) ret_from_kernel_thread;
        task_user_gs(p) = __KERNEL_STACK_CANARY;
        childregs->ds = __USER_DS;
        childregs->es = __USER_DS;
        childregs->fs = __KERNEL_PERCPU;
        childregs->bx = sp; /* function */
        childregs->bp = arg;
        childregs->orig_ax = -1;
        childregs->cs = __KERNEL_CS | get_kernel_rpl();
        childregs->flags = X86_EFLAGS_IF | X86_EFLAGS_FIXED;
        p->thread.io_bitmap_ptr = NULL;
        return 0;
    }
    *childregs = *current_pt_regs();
    childregs->ax = 0;
    if (sp)
        childregs->sp = sp;

    p->thread.ip = (unsigned long) ret_from_fork;
    task_user_gs(p) = get_user_gs(current_pt_regs());

    p->thread.io_bitmap_ptr = NULL;
    tsk = current;
    err = -ENOMEM;

    if (unlikely(test_tsk_thread_flag(tsk, TIF_IO_BITMAP))) {
        p->thread.io_bitmap_ptr = kmemdup(tsk->thread.io_bitmap_ptr,
                        IO_BITMAP_BYTES, GFP_KERNEL);
        if (!p->thread.io_bitmap_ptr) {
            p->thread.io_bitmap_max = 0;
            return -ENOMEM;
        }
        set_tsk_thread_flag(p, TIF_IO_BITMAP);
    }

    err = 0;

    /*
     * Set a new TLS for the child thread?
     */
    if (clone_flags & CLONE_SETTLS)
        err = do_set_thread_area(p, -1,
            (struct user_desc __user *)childregs->si, 0);

    if (err && p->thread.io_bitmap_ptr) {
        kfree(p->thread.io_bitmap_ptr);
        p->thread.io_bitmap_max = 0;
    }
    return err;
}

* p->thread.ip = (unsigned long) ret_from_fork*中指出了新进程的ip是指向 ret_from_fork 的位置,GDB走到这一步就跟踪不下去了。

总结

本次实验通过阅读源码和跟踪fork系统调用结合的方式,近距离的观察了进程创建的过程。大致了解到新进程的创建主要通过如下步骤:
- fork / vfork / clone 系统调用
- do_fork 生成新的进程,返回进程的id
- copy_process 返回新进程的结构体指针,还要设置进程之间的关系,将新进程加入到调度队列中。
- copy_thread 设置新进程的内核栈,已经开始执行的地方

ret_from_fork是新进程开始执行的地方,asmlinkage void ret_from_fork(void) asm(“ret_from_fork”); 它一条汇编指令实现的,其内部执行过程还没有触摸到。

以上是关于Linux内核分析:实验六--Linux进程的创建过程分析的主要内容,如果未能解决你的问题,请参考以下文章

实验六:分析Linux内核创建一个新进程的过程

实验六分析Linux内核创建一个新进程的过程

Linux内核分析实验六

Linux内核分析实验六

Linux内核分析-分析Linux内核创建一个新进程的过程

Linux内核分析——进程的描述和进程的创建