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

来源:互联网 发布:95式自动步枪弹道数据 编辑:程序博客网 时间:2024/05/17 04:30

刘畅 原创作品转载请注明出处 《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”); 它一条汇编指令实现的,其内部执行过程还没有触摸到。

1 0
原创粉丝点击