分析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包含了一个进程的重要运行信息,所以我们将围绕在创建一个新进程时,如何来建立一个新的PCB的这一个过程来进行分析,在Linux系统中,PCB主要是存储在一个叫做task_struct这一个结构体中,创建新进程仅能通过fork,clone,vfork等系统调用的形式来进行
  • 不管是fork,还是clonevfork,他们都是通过do_fork来创建进程
  • 接下来我将通过精简版的do_fork代码,和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; //总的pid数量

    /*
     * 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.
     */
    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;
    }

    // 复制进程描述符,返回创建的task_struct的指针
    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);

        // 取出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);

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

        // 如果设置了 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来复制进程描述符,返回新创建的子进程的task_struct的指针(即PCB指针)
    • 将新创建的子进程放入调度器的队列中,让其有机会获得CPU,并且要确保子进程要先于父进程运行,
    • 这里为什么要确保子进程先于父进程运行呢?,答案是在Linux系统中,有一个叫做copy_on_write技术(写时拷贝技术),该技术的作用是创建新进程时可以减少系统开销,具体该技术的细节请各位Google之,这里子进程先于父进程运行可以保证写时拷贝技术发挥其作用
  • 这里有一个重点的地方需要说明,在使用get_pid系统调用时,返回的并不是进程的pid,而是线程的tgid,tgid指的是一个线程组当时领头的进程的pid
  • do_fork中,copy_process函数是比较重要的,其作用是创建进程描述符以及子进程所需要的其他所有数据结构为子进程准备运行环境,下面我将深入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与当前进程的task,仅仅是stack地址不同
    p = dup_task_struct(current);
    if (!p)
        goto fork_out;

    ···

    retval = -EAGAIN;
    // 检查该用户的进程数是否超过限制
    if (atomic_read(&p->real_cred->user->processes) >=
            task_rlimit(p, RLIMIT_NPROC)) {
        // 检查该用户是否具有相关权限,不一定是root
        if (p->real_cred->user != INIT_USER &&
            !capable(CAP_SYS_RESOURCE) && !capable(CAP_SYS_ADMIN))
            goto bad_fork_free;
    }
    current->flags &= ~PF_NPROC_EXCEEDED;

    retval = copy_creds(p, clone_flags);
    if (retval < 0)
        goto bad_fork_free;

    /*
     * If multiple threads are within copy_process(), then this check
     * triggers too late. This doesn't hurt, the check is only there
     * to stop root fork bombs.
     */
    retval = -EAGAIN;
    // 检查进程数量是否超过 max_threads,后者取决于内存的大小
    if (nr_threads >= max_threads)
        goto bad_fork_cleanup_count;

    if (!try_module_get(task_thread_info(p)->exec_domain->module))
        goto bad_fork_cleanup_count;

    delayacct_tsk_init(p);  /* Must remain after dup_task_struct() */
    p->flags &= ~(PF_SUPERPRIV | PF_WQ_WORKER);
    // 表明子进程还没有调用exec系统调用
    p->flags |= PF_FORKNOEXEC;
    INIT_LIST_HEAD(&p->children);
    INIT_LIST_HEAD(&p->sibling);
    rcu_copy_process(p);
    p->vfork_done = NULL;

    // 初始化自旋锁
    spin_lock_init(&p->alloc_lock);

    // 初始化挂起信号
    init_sigpending(&p->pending);

    // 初始化定时器
    p->utime = p->stime = p->gtime = 0;
    p->utimescaled = p->stimescaled = 0;
#ifndef CONFIG_VIRT_CPU_ACCOUNTING_NATIVE
    p->prev_cputime.utime = p->prev_cputime.stime = 0;
#endif
#ifdef CONFIG_VIRT_CPU_ACCOUNTING_GEN
    seqlock_init(&p->vtime_seqlock);
    p->vtime_snap = 0;
    p->vtime_snap_whence = VTIME_SLEEPING;
#endif

    ...

#ifdef CONFIG_DEBUG_MUTEXES
    p->blocked_on = NULL; /* not blocked yet */
#endif
#ifdef CONFIG_BCACHE
    p->sequential_io    = 0;
    p->sequential_io_avg    = 0;
#endif

    /* Perform scheduler related setup. Assign this task to a CPU. */

    // 完成对新进程调度程序数据结构的初始化,并把新进程的状态设置为TASK_RUNNING
    // 同时将thread_info中得preempt_count置为1,禁止内核抢占
    retval = sched_fork(clone_flags, p);
    if (retval)
        goto bad_fork_cleanup_policy;

    retval = perf_event_init_task(p);
    if (retval)
        goto bad_fork_cleanup_policy;
    retval = audit_alloc(p);
    if (retval)
        goto bad_fork_cleanup_perf;
    /* copy all the process information */

    // 复制所有的进程信息
    shm_init_task(p);
    retval = copy_semundo(clone_flags, p);
    if (retval)
        goto bad_fork_cleanup_audit;
    retval = copy_files(clone_flags, p);
    if (retval)
        goto bad_fork_cleanup_semundo;

    ...

    // 初始化子进程的内核栈
    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号
        pid = alloc_pid(p->nsproxy->pid_ns_for_children);
        if (!pid)
            goto bad_fork_cleanup_io;
    }

    ...

    // 清除子进程thread_info结构的 TIF_SYSCALL_TRACE,防止 ret_from_fork将系统调用消息通知给调试进程
    clear_tsk_thread_flag(p, TIF_SYSCALL_TRACE);
#ifdef TIF_SYSCALL_EMU
    clear_tsk_thread_flag(p, TIF_SYSCALL_EMU);
#endif
    clear_all_latency_tracing(p);

    /* ok, now we should be set up.. */

    // 设置子进程的pid
    p->pid = pid_nr(pid);

    // 如果是创建线程
    if (clone_flags & CLONE_THREAD) {
        p->exit_signal = -1;

        // 线程组的leader设置为当前线程的leader
        p->group_leader = current->group_leader;

        // tgid是当前线程组的id,也就是main进程的pid
        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;

        // tgid和pid相同
        p->tgid = p->pid;
    }

    ...

    if (likely(p->pid)) {
        ptrace_init_task(p, (clone_flags & CLONE_PTRACE) || trace);

        init_task_pid(p, PIDTYPE_PID, pid);
        if (thread_group_leader(p)) {

            ...

            // 将pid加入散列表
            attach_pid(p, PIDTYPE_PGID);
            attach_pid(p, PIDTYPE_SID);
            __this_cpu_inc(process_counts);
        } else {

            ...

        }
        // 将pid加入PIDTYPE_PID这个散列表
        attach_pid(p, PIDTYPE_PID);
        // 递增 nr_threads的值
        nr_threads++;
    }

    total_forks++;
    spin_unlock(&current->sighand->siglock);
    syscall_tracepoint_update(p);
    write_unlock_irq(&tasklist_lock);

    ...

    // 返回被创建的task结构体指针
    return p;

    ...

}

同样,这里关键部分我已经有了注释我针对几个关键问题进行说明:

  • copy_process的参数与do_fork的参数类型一模一样,参数也是几乎相同,除了struct pid那里是空
  • dup_task_struct这个函数,会分配一个新的task_struct给子进程,但是这个task_struct是未初始化的,下面我将具体说说这个dup_task_struct:
  • copy_process中会进行各种各样的初始化和信息检查,比如初始化自旋锁,初始化堆栈信息等等,同时会把新创建的子进程运行状态置为TASK_RUNNING(这里应该是就绪态)
  • copy_process中,会通过copy_thread来初始化子进程的内核栈,下面也会进行具体说明

dup_task_struct

static struct task_struct *dup_task_struct(struct task_struct *orig)
{
    struct task_struct *tsk;
    struct thread_info *ti;
    int node = tsk_fork_get_node(orig);
    int err;

    // 分配一个task_struct结点
    tsk = alloc_task_struct_node(node);
    if (!tsk)
        return NULL;

    // 分配一个thread_info结点,其实内部分配了一个union,包含进程的内核栈
    // 此时ti的值为栈底,在x86下为union的高地址处。
    ti = alloc_thread_info_node(tsk, node);
    if (!ti)
        goto free_tsk;

    err = arch_dup_task_struct(tsk, orig);
    if (err)
        goto free_ti;

    // 将栈底的值赋给新结点的stack
    tsk->stack = ti;

    ...

    /*
     * One for us, one for whoever does the "release_task()" (usually
     * parent)
     */
    // 将进程描述符的使用计数器置为2
    atomic_set(&tsk->usage, 2);
#ifdef CONFIG_BLK_DEV_IO_TRACE
    tsk->btrace_seq = 0;
#endif
    tsk->splice_pipe = NULL;
    tsk->task_frag.page = NULL;

    account_kernel_stack(ti, 1);

    // 返回新申请的结点
    return tsk;

free_ti:
    free_thread_info(ti);
free_tsk:
    free_task_struct(tsk);
    return NULL;
}
  • 这其中有个比较重要的结构struct thread_info,但是在内部分配时,其实是一个union(联合体),这个union包括了一个内核堆栈,其结构如下图(图来自Understanding Linux kernel 3th)

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();
    // 子进程的eax置为0,所以fork的子进程返回值为0
    childregs->ax = 0;
    if (sp)
        childregs->sp = sp;

    // 子进程从ret_from_fork开始执行
    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;

    // 如果父进程使用IO权限位图,那么子进程获得该位图的一个拷贝
    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);
    }

    ...

    return err;
}
  • 这里的过程基本就是给新的进程的各种运行时状态进行初始化,比如寄存器信息(通过父进程的寄存器信息来初始化,但是eip会是个例外,eip将会取决于最后子进程将会从哪里开始执行),栈会被置空未初始化状态
  • 在代码中,有两段这样的代码p->thread.ip = (unsigned long) ret_from_kernel_thread; p->thread.ip = (unsigned long) ret_from_fork;这里表面了在fork完成之后,新进程将会在哪里开始执行,如果新的创建的新的线程是内核线程,那么将会从ret_from_kernel_thread开始执行,但是如果是普通的用户态线程,则将会从p->thread.ip = (unsigned long) ret_from_fork开始执行.

总结

通过实验和fork系统调用的分析,让我认识到除了Linux系统中最开始启动时,创建的第一个始祖进程外,从init进程开始,其他所有的进程的创建方式均是���过forkclone,vfork的方式,而他们又能够有归结到do_fork,就想孟宁老师说得道生一,一生二,二生三这样.

实验截图


参考资料

  • Understanding The Linux Kernel, the 3rd edtion
  • Linux内核设计与实现,第三版,Robert Love, 机械工业出版社

署名信息

吴欣伟 原创作品转载请注明出处:《Linux内核分析》MOOC课程:http://mooc.study.163.com/course/USTC-1000029000
时间: 2024-10-18 09:51:34

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

浅谈Linux内核创建新进程的全过程_Linux

进程描述 进程描述符(task_struct) 用来描述进程的数据结构,可以理解为进程的属性.比如进程的状态.进程的标识(PID)等,都被封装在了进程描述符这个数据结构中,该数据结构被定义为task_struct 进程控制块(PCB) 是操作系统核心中一种数据结构,主要表示进程状态. 进程状态 fork() fork()在父.子进程各返回一次.在父进程中返回子进程的 pid,在子进程中返回0. fork一个子进程的代码   #include <stdio.h> #include <std

linux编程-创建daemon进程时为什么要用setsid创建一个新的session?

问题描述 创建daemon进程时为什么要用setsid创建一个新的session? 不管是unix环境高级编程还是网络上都说创建daemon的步骤是: 1. fork 2. setsid创建新session 3. 父进程exit 这样子进程就能脱离terminal了. 这样子确实没错, 我之前以为这是为了避免子进程被强制退出, 所以才要脱离terminal和父进程. 但是在实验的时候发现, 没有setsid, 仅仅fork一个子进程并让子进程无限循环, 父进程fork之后exit, 子进程并没有

linux下exec创建一个gcc进程来编译代码,如何判定编译是否成功?

问题描述 linux下exec创建一个gcc进程来编译代码,如何判定编译是否成功? 代码在下面-- 1 #include 2 #include 3 #include 4 #include 5 #include 6 8 char * cmdargv[20] = {0}; 9 int main(void) 10 { 11 pid_t pid; 13 int retval; 16 17 while(1){ 20 if((pid = fork()) < 0){ 21 perror("fork&qu

十天学Linux内核之第二天---进程

原文:十天学Linux内核之第二天---进程 都说这个主题不错,连我自己都觉得有点过大了,不过我想我还是得坚持下去,努力在有限的时间里学习到Linux内核的奥秘,也希望大家多指点,让我更有进步.今天讲的全是进程,这点在大二的时候就困惑了我,结果那个时候我就止步不前了,这里主要讲的是为何引入进程.进程在Linux空间是如何实现的,并且描述了所有与进程执行相关的数据结构,最后还会讲到异常和中断等异步执行流程,它们是如何和Linux内核进行交互的,下面我就来具体介绍一下进程的奥妙. 首先我们要明确一个

Linux中如何启动新进程

有时候,我们需要在自己的程序(进程)中启动另一个程序(进程)来帮助我们完成一些工作,那么我们需要怎么才能在自己的进程中启动其他的进程呢?在Linux中提供了不少的方法来实现这一点,下面就来介绍一个这些方法及它们之间的区别. 一.system函数调用 system函数的原型为: #include <stdlib.h> int system (const char *string); 它的作用是,运行以字符串参数的形式传递给它的命令并等待该命令的完成.命令的执行情况就如同在shell中执行命令:s

《Raspberry Pi用户指南》——2.5 创建一个新的用户账户

2.5 创建一个新的用户账户 Raspberry Pi用户指南与面向单用户的Windows和OS X不同,开放社区一开始就将Linux设计成一个多用户操作系统.默认情况下,Debian配置了两个用户账户:Pi(普通账户)和root(拥有额外权限的超级账户). 小提示 不要一直使用 root 账户登录.使用非特权用户账户,可以保护你的操作系统免受意外破坏以及互联网上下载的病毒和其他恶意软件的破坏.虽然你已经有了Pi账户,但是创建自己专用的账户将更好些.另外,还可以为可能要使用到树莓派的任何朋友或家

Win8如何创建一个新账户

  Win8创建一个新账户的方法如下: 1.系统下同时按住键盘上"Windows" 和"X" 键打开系统菜单,点击选择"计算机管理". 2."计算机管理"界面下点击"本地用户和组",界面右侧右键点击"用户"选项,单击选择"新用户(N)-"选项. 3.在"新用户"下填写"用户名"与"密码"等信息,其中"

Windows7系统创建一个新账户的方法

  什么是用户账户? 用户账户是通知 Windows您可以访问哪些文件和文件夹,可以对计算机和个人首选项(如桌面背景和屏幕保护程序)进行哪些更改的信息集合.通过用户帐户,您可以在拥有自己的文件和设置的情况下与多个人共享计算机.每个人都可以使用用户名和密码访问其用户帐户. 有三种类型的账户.每种类型为用户提供不同的计算机控制级别: 1,标准帐户适用于日常计算. 2,管理员帐户可以对计算机进行最高级别的控制,但应该只在必要时才使用. 3,来宾帐户主要针对需要临时使用计算机的用户. 1,单击左下角的"

pf-kernel 3.1.2-pf发布 Linux内核的一个分支

pf-kernel 3.1.2-pf该版本的内核已经更新到3.1.2. pf-kernel是Linux内核的一个分支.它提供了将BFS调度程序和tuxonice合并到主线的实用功能. 软件信息:http://pf.natalenko.name/ 下载地址:http://pf.natalenko.name/sources/3.1/patch-3.1.2-pf.bz2