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

一、原理分析

1.进程的描述

  进程控制块PCB——task_struct,为了管理进程,内核必须对每个进程进行清晰的描述,进程描述符提供了内核所需了解的进程信息。

struct task_struct{
    volatile long state; //进程状态,-1表示不可执行,0表示可执行,大于1表示停止
    void *stack; //内核堆栈
    atomic_t usage;
    unsigned int flags; //进程标识符
    unsigned int ptrace;
    ……
}

2.进程的创建
  道生一(start_ kernel...cpu_ idle),一生二(kernel_ init和kthreadd),二生三(即前面的0、1、2三个进程),三生万物(1号进程是所有用户态进程的祖先,2号进程是所有内核线程的祖先)
start_ kernel创建了cpu_ idle,也就是0号进程。而0号进程又创建了两个线程,一个是kernel_ init,也就是1号进程,这个进程最终启动了用户态;另一个是kthreadd。0号进程是固定的代码,1号进程是通过复制0号进程PCB之后在此基础上做修改得到的。
  iret与int 0x80指令对应,一个是弹出寄存器值,一个是压入寄存器的值。
  如果将系统调用类比于fork();那么就相当于系统调用创建了一个子进程,然后子进程返回之后将在内核态运行,而返回到父进程后仍然在用户态运行。

  Linux中创建进程一共有三个函数:fork,创建子进程 vfork,与fork类似,但是父子进程共享地址空间,而且子进程先于父进程运行。 clone,主要用于创建线程。Linux中所有的进程创建都是基于复制的方式,Linux通过复制父进程来创建一个新进程,通过调用do_ fork来实现。然后对子进程做一些特殊的处理。而Linux中的线程,又是一种特殊的进程。根据代码的分析,do_ fork中,copy_ process管子进程运行的准备,wake_ up_ new_ task作为子进程forking的完成。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大概做了这么几件事情:
(1)调用copy_process,将当期进程复制一份出来为子进程,并且为子进程设置相应地上下文信息。
(2)初始化vfork的完成处理信息(如果是vfork调用)
(3)调用wake_up_new_task,将子进程放入调度器的队列中,此时的子进程就可以被调度进程选中,得以运行。
(4)如果是vfork调用,需要阻塞父进程,知道子进程执行exec。

3.创建的新进程从哪里开始执行?

  kernel中是可以指定新进程开始的位置(也就是通过eip寄存器指定代码行)。fork中也有相似的机制 这涉及子进程的内核堆栈数据状态和task_ struct中thread记录的sp和ip的一致性问题,这是在copy_ thread in copy_ process设定的。

  copy_thread的流程如下:

(1) 获取子进程寄存器信息的存放位置

(2) 对子进程的thread.sp赋值,将来子进程运行,这就是子进程的esp寄存器的值。

(3)如果是创建内核线程,那么它的运行位置是ret_from_kernel_thread,将这段代码的地址赋给thread.ip,之后准备其他寄存器信息,退出 。

(4)将父进程的寄存器信息复制给子进程。

(5)将子进程的eax寄存器值设置为0,所以fork调用在子进程中的返回值为0。

(6)子进程从ret_from_fork开始执行,所以它的地址赋给thread.ip,也就是将来的eip寄存器。 从上面的流程中,我们看出,子进程复制了父进程的上下文信息,仅仅对某些地方做了改动,运行逻辑和父进程完全一致。

  另外,子进程从ret_from_fork处开始执行,子进程的运行是由这几处保证的:

(1)dup_task_struct中为其分配了新的堆栈 。

(2)copy_process中调用了sched_fork,将其置为TASK_RUNNING 。

(3)copy_thread中将父进程的寄存器上下文复制给子进程,这是非常关键的一步,这里保证了父子进程的堆栈信息是一致的。

(4)将ret_from_fork的地址设置为eip寄存器的值,这是子进程的第一条指令。

二、实验内容

更新menu,删除test_fork.c和test.c文件,重新执行make rootfs。首先还是将QEMU启动,然后在sys_clone处设置一个断点。执行以下代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char * argv[])
{
    int pid;
    /* fork another process */
    pid = fork();
    if (pid < 0)
    {
        /* error occurred */
        fprintf(stderr,"Fork Failed!");
        exit(-1);
    }
    else if (pid == 0)
    {
        /* child process */
        printf("This is Child Process!\n");
    }
    else
    {
        /* parent process  */
        printf("This is Parent Process!\n");
        /* parent will wait for the child to complete*/
        wait(NULL);
        printf("Child Complete!\n");
    }
}

  执行后,来到了断点处

  sys_clone实际上调用的还是do_fork,也就是说真正的进程创建是都是最终调用do_fork,然后进入该函数,里面有一个函数copy_process,这里面就是开始了详细的创建。首先是异常判断,然后开始了dup_task_struct,也就是复制父进程,产生子进程。 具体函数为

int __weak arch_dup_task_struct(struct task_struct *dst,
                           struct task_struct *src)
{
    *dst = *src;
    return 0;
}

  复制结束后,子进程和父进程是一样的,复制过程中内核堆栈也被复制了,接下来是对子进程进行赋值

  复制文件,复制文件系统,复制信号等。 然后单步进入函数copy_thread。

p->thread.sp = (unsigned long) childregs;     //子进程栈地址
*childregs = *current_pt_regs();          //子进程的寄存器
childregs->ax = 0;                  //ax保存返回值,子进程执行fork的返回值是0
p->thread.ip = (unsigned long) ret_from_fork; //子进程的开始执行地址

  再往下继续单步

  编译内核查看fork命令,得到子进程与父进程结果。

启动gdb调试,并对主要的函数设置断点。

三、总结

  对于“特别关注新进程是从哪里开始执行的?为什么从哪里能顺利执行下去?即执行起点与内核堆栈如何保证一致”,由于ret_ from_ fork决定新进程的第一条指令地址。子进程从ret_ from_ fork处开始执行。因为在ret_ from_ fork之前,也就是在copy_ thread()函数中* childregs = * current_ pt_ regs();该句将父进程的regs参数赋值到子进程的内核堆栈。* childregs的类型为pt_ regs,里面存放了SAVE_ ALL中压入栈的参数,因此在之后的RESTORE ALL中能顺利执行下去。

  新的进程通过克隆旧的程序(当前进程)而建立。fork() 和 clone()(对于线程)系统调用可用来建立新的进程。这两个系统调用结束时,内核在系统的物理内存中为新的进程分配新的 task_struct 结构,同时为新进程要使用的堆栈分配物理页。Linux 还会为新的进程分配新的进程标识符。然后,新 task_struct 结构的地址保存在链表中,而旧进程的 task_struct 结构内容被复制到新进程的 task_struct 结构中。 在克隆进程时,Linux 允许两个进程共享相同的资源。可共享的资源包括文件、信号处理程序和虚拟内存等。

刘帅

原创作品转载请注明出处

《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000

时间: 2024-12-25 13:36:49

分析Linux内核创建一个新进程的过程的相关文章

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

潘恒 原创作品转载请注明出处<Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 task_struct结构: struct task_struct {   volatile long state;进程状态  void *stack; 堆栈  pid_t pid; 进程标识符  unsigned int rt_priority;实时优先级  unsigned int policy;调度策略  struct files

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

分析Linux内核创建一个新进程的过程 攥写人:李鹏举  学号:20132201 ( *原创作品转载请注明出处*) ( 学习课程:<Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 ) 本周要求: 阅读理解task_struct数据结构http://codelab.shiyanlou.com/xref/linux-3.18.6/include/linux/sched.h#1235: 分析fork函数对应的内核处理过

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

作者:江军 ID:fuchen1994 实验题目:分析Linux内核创建一个新进程的过程 阅读理解task_struct数据结构http://codelab.shiyanlou.com/xref/linux-3.18.6/include/linux/sched.h#1235: 分析fork函数对应的内核处理过程sys_clone,理解创建一个新进程如何创建和修改task_struct数据结构: 使用gdb跟踪分析一个fork系统调用内核处理函数sys_clone ,验证您对Linux系统创建一个

《Linux内核--分析Linux内核创建一个新进程的过程 》 20135311傅冬菁

20135311傅冬菁 分析Linux内核创建一个新进程的过程 一.学习内容 进程控制块——PCB  task_struct数据结构 PCB task_struct中包含: 进程状态.进程打开的文件.进程优先级信息 操作系统管理的三个功能: 1.进程管理 2.内存管理 3.文件系统 Linux进程的状态: 进程状态分析: long state是进程的运行状态,-1是未执行,0是执行中,大于0则是暂停: *stack 是建立一个内核堆栈: flags 是定义了每个进程的标识符: list_head

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

李俊锋 + 原创作品转载请注明出处 + <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 一.实验原理 1.进程的定义 进程是操作系统的概念,每当我们执行一个程序时,对于操作系统来讲就创建了一个进程,在这个过程中,伴随着资源的分配和释放.可以认为进程是一个程序的一次执行过程. 2.进程与程序的区别 程序时静态的,它是一些保存 在磁盘上得指令的有序集合,没有任何执行的概念. 进程是一个动态的概念,它是程序执行的过程

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

20135108 李泽源 阅读理解task_struct数据结构http://codelab.shiyanlou.com/xref/linux-3.18.6/include/linux/sched.h#1235: 分析fork函数对应的内核处理过程sys_clone,理解创建一个新进程如何创建和修改task_struct数据结构: 使用gdb跟踪分析一个fork系统调用内核处理函数sys_clone ,验证您对Linux系统创建一个新进程的理解,推荐在实验楼Linux虚拟机环境下完成实验. 特别

linux内核分析第六周-分析Linux内核创建一个新进程的过程

Linux内核对进程管理是操作系统的重要任务之一. 此次实验就是了解内核创建一个新进程的大致过程. 为了简单,使用fork再用户态创建一个进程.代码如下: 下面是准备工作??? cd LinuxKernel rm -rf menu git clone https://github.com/mengning/menu.git cd menu mv test_fork.c test.c make rootfs 打开gdb进行远程调试? 设置断点 b sys_clone b do_fork b dup

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

转自:http://www.cnblogs.com/MarkWoo/p/4420588.html 前言说明 本篇为网易云课堂Linux内核分析课程的第六周作业,本次作业我们将具体来分析fork系统调用,来分析Linux内核创建新进程的过程 关键词:fork, 系统调用,进程 *运行环境:** Ubuntu 14.04 LTS x64 gcc 4.9.2 gdb 7.8 vim 7.4 with vundle 分析 分析方法说明 PCB包含了一个进程的重要运行信息,所以我们将围绕在创建一个新进程时

《Linux内核分析》 第六节 分析Linux内核创建一个新进程的过程

范闻泽 + 原创作品转载请注明出处 + <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 一.实验过程 1.删除原来的menu,并clone新的menu,用test_fork.c覆盖test.c 2. make rootfs之后新的内核启动,测试fork功能 3.使用-s -S冷冻内核,准备调试 4.设置断点 5.根据断点,进行跟踪,得到结果 1.以下是执行指令 cd LinuxKernel rm menu -r