网易云课堂 Linux内核分析(六)

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

实验内容

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

  • 理解task_struct的数据结构

    • 进程状态(state)
      • TASK_RUNNING 可运行(包括就绪和正在运行)
      • TASK_INTERRUPTIBLE 可中断的等待状态
      • TASK_UNINTERRUPTIBLE 不可中断的等待状态
      • TASK_ZOMBIE 僵死
      • TASK_STOPPED 暂停
      • TASK_SWAPPING 换入/换出
    • 进程的标示pid
    • 所有进程链表struct list_ head tasks;
    • 程序创建的进程具有父子关系,在编程时往往需要引用这样的父子关系。进程描述符中有几个域用来表示这样的关系
    • Linux为每个进程分配一个8KB大小的内存区域,用于存放该进程两个不同的数据结构:Thread_ info和进程的内核堆栈
    • 文件系统和文件描述符
    • 内存管理——进程的地址空间
    • 等等
  • 分析fork函数对应的内核处理过程sys_clone,理解创建一个新进程如何创建和修改task_struct数据结构

    • 回顾mykernel中是如何创建进程的
    • Linux中创建进程一共有三个函数
      • fork,创建子进程
      • vfork,与fork类似,但是父子进程共享地址空间,而且子进程先于父进程运行
      • clone,主要用于创建线程
    • fork()系统调用对应的内核实现为sys_fork(),sys_fork()是对do_fork()的简单封装,sys_fork()的任务是从处理器寄存器中提取由用户空间提供的信息,do_fork()负责进程的复制。fork()和clone()系统调用的入口点sys_vfork()和sys_clone()也是调用的do_fork()。关系进程创建的主要是copy_process和copy_thread.copy_process()函数,即
      • 复制一个PCB——task_struct
        err = arch_dup_task_struct(tsk, orig);
      • 给新进程分配一个新的内核堆栈
        ti = alloc_ thread_ info_ node(tsk, node);
        tsk->stack = ti;
        setup_ thread_ stack(tsk, orig); //这里只是复制thread_ info,而非复制内核堆栈
      • 要修改复制过来的进程数据,比如pid、进程链表等。具体见copy _process内部
      • 从用户态的代码看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;

    // ...

    // 复制进程描述符,返回创建的task_struct的指针
    p = copy_process(clone_flags, stack_start, stack_size,
             child_tidptr, NULL, trace);

    if (!IS_ERR(p)) {
        struct completion vfork;
        struct pid *pid;

        trace_sched_process_fork(current, p);

        // 取出task结构体内的pid
        pid = get_task_pid(p, PIDTYPE_PID);
        nr = pid_vnr(pid);

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

        // 如果使用的是vfork,那么必须采用某种完成机制,确保父进程后运行
        if (clone_flags & CLONE_VFORK) {
            p->vfork_done = &vfork;
            init_completion(&vfork);
            get_task_struct(p);
        }

        // 将子进程添加到调度器的队列,使得子进程有机会获得CPU
        wake_up_new_task(p);

        // ...

        // 如果设置了 CLONE_VFORK 则将父进程插入等待队列,并挂起父进程直到子进程释放自己的内存空间
        // 保证子进程优先于父进程运行
        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主要执行了以下几步:

    • 调用copy_ process,将当前进程复制一份为子进程,并且为子进程设置相应地上下文信息
    • 初始化vfork的完成处理信息(如果是vfork调用)
    • 调用wake_ up_ new_ task,将子进程放入调度器的队列中,此时的子进程就可以被调度进程选中,得以运行
    • 如果是vfork调用,需要阻塞父进程,知道子进程执行exec。上面的过程对vfork稍微做了处理,因为vfork必须保证子进程优先运行,执行exec,替换自己的地址空间
  • 创建的新进程从哪里开始执行?
    新进程是从ret_from_fork处开始执行的。对于fork执行处理过程来说,父子进程共享同一段代码空间,”一次调用,两次返回“,其实对于调用fork的父进程来说,如果fork出来的子进程没有得到 调度,那么父进程从fork系统调用返回,同时分析sys_fork知道,fork返回的是子进程的id。再看fork出来的子进程,由 copy_process函数可以看出,子进程的返回地址为ret_from_fork(和父进程在同一个代码点上返回),返回值直接置为0。所以当子进 程得到调度的时候,也从fork返回,返回值为0。ret_from_fork()调用schedule_tail()函数,用存放在栈中的值再装入所有寄存器,并强迫CPU返回到用户态。这样,eax寄存器就装过两个值,一个是子进程的值0,一个是父进程的值——子进程的PID。然后在fork()、vfork()或clone()返回时,新进程将开始执行。在不同的进程中返回不同的值。
    执行流程为:
    网易云课堂 Linux内核分析(六)_第1张图片

实验流程及步骤参考实验报告

你可能感兴趣的:(linux,linux)