软件世界网 购物 网址 三丰软件 | 小说 美女秀 图库大全 游戏 笑话 | 下载 开发知识库 新闻 开发 图片素材
多播视频美女直播
↓电视,电影,美女直播,迅雷资源↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
移动开发 架构设计 编程语言 Web前端 互联网
开发杂谈 系统运维 研发管理 数据库 云计算 Android开发资料
  软件世界网 -> 开发杂谈 -> 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》:
[img]http://img.blog.csdn.net/20160403144432583

Linux中进程的创建


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

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”); 它一条汇编指令实现的,其内部执行过程还没有触摸到。
......显示全文...
    点击查看全文


上一篇文章      下一篇文章      查看所有文章
2016-04-03 20:46:48  
开发杂谈 最新文章
BloomFilter
大学四年编程之历程
内核分析
造人论坛——意识的本质和一个人工脑模型
OFDM信号[matlab描述]
人类还会进化吗?
HDUACM1035RobotMotion简单模拟题
树、二叉树(二)
iisphpweb.config处理404,500等,跳转友好
DatabaseAsaFortress
360图书馆 软件开发资料 文字转语音 购物精选 软件下载 美食菜谱 新闻资讯 电影视频 小游戏 Chinese Culture 股票 租车
生肖星座 三丰软件 视频 开发 短信 中国文化 网文精选 搜图网 美图 阅读网 多播 租车 短信 看图 日历 万年历 2018年1日历
2018-1-20 7:28:24
多播视频美女直播
↓电视,电影,美女直播,迅雷资源↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  软件世界网 --