do_fork实现分析

进程复制的三个机制forkvforkclone最终都是调用do_fork来实现子进程的产生的,不同的产生方式通过传递给do_fork的不同参数来控制。其代码执行流程如下:
代码:

    if (unlikely(clone_flags & CLONE_STOPPED)) {
        static int __read_mostly count = 100;

        if (count > 0 && printk_ratelimit()) {
            char comm[TASK_COMM_LEN];

            count--;
            printk(KERN_INFO "fork(): process `%s' used deprecated "
                    "clone flags 0x%lx\n",
                get_task_comm(comm, current),
                clone_flags & CLONE_STOPPED);
        }
    }

    if (unlikely(current->ptrace)) {
        trace = fork_traceflag (clone_flags);
        if (trace)
            clone_flags |= CLONE_PTRACE;
    }

主要是实现对传递给do_fork的flag参数进行处理和检查当前进程是否设置了跟踪标记ptrace
下面便进入了copy_process函数,该函数实现将父进程的寄存器以及所有进程执行环境的相关部分复制给子进程,但并不实际运行该复制后的子进程。copy_process函数的执行流程如下图所示:

下面对copy_process代码作详细解读:

    int retval;
    struct task_struct *p;
    int cgroup_callbacks_done = 0;

    if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS))
        return ERR_PTR(-EINVAL);

    /*
     * Thread groups must share signals as well, and detached threads
     * can only be started up within the thread group.
     */
    if ((clone_flags & CLONE_THREAD) && !(clone_flags & CLONE_SIGHAND))
        return ERR_PTR(-EINVAL);

    /*
     * Shared signal handlers imply shared VM. By way of the above,
     * thread groups also imply shared VM. Blocking this case allows
     * for various simplifications in other code.
     */
    if ((clone_flags & CLONE_SIGHAND) && !(clone_flags & CLONE_VM))
        return ERR_PTR(-EINVAL);

    retval = security_task_create(clone_flags);
    if (retval)
        goto fork_out;

    retval = -ENOMEM;

以上代码主要是对一些标志进行合法性检查。

  • 当设置CLONE_NEWNS时,表示clone需要自己的命名空间,此时不能同时设置CLONE_NEWNS和CLONE_FS
  • 当设置CLONE_THREAD时,表示把子进程插入到父进程的同一线程组中,并迫使子进程共享父进程的信号描述符。因此也设置子进程的tgid字段和group_leader字段。如果这个标志位为true,就必须设置CLONE_SIGHAND标志。
  • 当设置CLONE_SIGHAND时,表示共享信号处理程序的表、阻塞信号表和挂起信号表。如果这个标志位true,就必须设置CLONE_VM标志。

各标志检查完之后便调用dup_task_struct函数来为新进程创建一个内核栈、thread_info和task_struct,这里完全copy父进程的内容,所有到目前为止,父进程和子进程没有任何区别。从这里可以看出,这个版本的内核在创建进程时需要为自己分配内核栈。

static struct task_struct *dup_task_struct(struct task_struct *orig)
{
    struct task_struct *tsk;
    struct thread_info *ti;
    int err;
/*some operations related to FPU, call __unlazy_fpu() to store FPU MMX SSE/SSE2 registers to the fathers progress' thread_info*/
    prepare_to_copy(orig);
    tsk = alloc_task_struct();    /*系统从专业高速缓存中分配task_struct空间*/
    if (!tsk)
        return NULL;
/*ti指向thread_info的首地址,同时也是系统为新进程分配的两个连续的页面的首地址*/
    ti = alloc_thread_info(tsk);
    if (!ti) {
        free_task_struct(tsk);
        return NULL;
    }
/*copy parent's task_struct to child's task_struct*/
     err = arch_dup_task_struct(tsk, orig);
    if (err)
        goto out;
/*将子进程的task_struct的stack指针指向子进程的thread_info的首地址,即指向内核为进程分配
的两个连续页面的首地址*/
    tsk->stack = ti;    

    err = prop_local_init_single(&tsk->dirties);
    if (err)
        goto out;
/*将父进程的thread_info的内容完全复制到子进程中,并建立thread_info与task_struct之间的联系*/
    setup_thread_stack(tsk, orig);
/* init stack_canary*/
#ifdef CONFIG_CC_STACKPROTECTOR
    tsk->stack_canary = get_random_int();
#endif

    /* One for us, one for whoever does the "release_task()" (usually parent) */
    atomic_set(&tsk->usage,2);
    atomic_set(&tsk->fs_excl, 0);    /*禁止该进程独占文件系统*/
#ifdef CONFIG_BLK_DEV_IO_TRACE
    tsk->btrace_seq = 0;
#endif
    tsk->splice_pipe = NULL;
    return tsk;

out:
    free_thread_info(ti);
    free_task_struct(tsk);
    return NULL;
}

dup_task_struct函数的执行流程为:

  • 调用prepare_to_copy(org)对FPU的相关寄存器进行存储
  • 调用alloc_task_struct()从系统高速缓存中分配task_struct空间
  • 调用alloc_thread_info(tsk)为新进程分配连续两个页面的空间,返回值指向thread_info的首地址,这是内核栈和thread_info的存放空间。
  • 调用arch_dup_task_struct(tsk, orig)将父进程的task_struct复制给子进程的task_struct。这个复制过程并不是将子进程的指针简单地指向父进程的task_struct,而是使用压栈的方式将父子进程的task_struct结构体的对应成员进行赋值。
  • tsk->stack = ti;将子进程task_struct结构体的stack指针指向thread_info结构,这样便建立了task_struct与thread_info之间的联系了
  • setup_thread_stack(tsk, orig);函数是将父进程的thread_info的内容完全复制到子进程的thread_info中,并在子进程中建立thread_info和task_struct之间的联系。关联图如下图所示:
    if (atomic_read(&p->user->processes) >=
             p->signal->rlim[RLIMIT_NPROC].rlim_cur) {
         if (!capable(CAP_SYS_ADMIN) && !capable(CAP_SYS_RESOURCE) &&
             p->user != current->nsproxy->user_ns->root_user)
             goto bad_fork_free; 
     }
    第一个if语句里面的rlim数组包含在task_struct数组中,对进程占用的资源做出限制,rlim[RLIMIT_NPROC]限制了改进用户可以拥有的总进程的数量,如果当用户所拥有的进程数量超过了规定的最大拥有进程数量,在内核中直接goto_bad_fork_free了。除非当前用户是root用户或分配了特别的权限(CAP_SYS_ADMIN或 CAP_SYS_RESOURCE)。检测root用户比较有趣,因为每个PID命名空间都有各自的root用户。

接下来就是初始化task_struct结构中的各个成员了,这里主要涉及到拷贝clone_flags、初始化相关链表等。

接下来就是调用sched_fork函数设置处理器的相关标记,并分配特定标号的处理器给该进程。其中的处理器相关标记包括:分配处理器标号、设置进程的优先级、设置与该进程调度相关的调度类等。

    if ((retval = security_task_alloc(p)))
        goto bad_fork_cleanup_policy;
    if ((retval = audit_alloc(p)))
        goto bad_fork_cleanup_security;
    /* copy all the process information */
    if ((retval = copy_semundo(clone_flags, p)))
        goto bad_fork_cleanup_audit;
    if ((retval = copy_files(clone_flags, p)))
        goto bad_fork_cleanup_semundo;
    if ((retval = copy_fs(clone_flags, p)))
        goto bad_fork_cleanup_files;
    if ((retval = copy_sighand(clone_flags, p)))
        goto bad_fork_cleanup_fs;
    if ((retval = copy_signal(clone_flags, p)))
        goto bad_fork_cleanup_sighand;
    if ((retval = copy_mm(clone_flags, p)))
        goto bad_fork_cleanup_signal;
    if ((retval = copy_keys(clone_flags, p)))
        goto bad_fork_cleanup_mm;
    if ((retval = copy_namespaces(clone_flags, p)))
        goto bad_fork_cleanup_keys;
    if ((retval = copy_io(clone_flags, p)))
        goto bad_fork_cleanup_namespaces;

这段代码主要实现复制/共享进程的各个部分,某一部分是否父子进程共享主要由clone_flags中的相关标志位决定的。

接下来就是一系列的设置各个ID、进程关系

  • 调用copy_thread函数设置子进程task_struct中的thread字段
  • 调用alloc_pid(task_active_pid_ns(p)),首先获取进程p所在的namespace,再使用alloc_pid来在该namespace下分配pid号
  • 如果在clone_flags中设置了标记CLONE_NEWPID,即重新生成新的PID namespace,则调用pid_ns_prepare_proc(task_active_pid_ns(p))来满足要求。
  • 如果clone_flags中设置了CLONE_THREAD,则将父子进程放入相同的进程组。
    至此,copy_process函数分析完毕。

如果copy_process函数返回的子进程的task_struct指针无误,则执行如下代码

        struct completion vfork;

        nr = task_pid_vnr(p);

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

        if (clone_flags & CLONE_VFORK) {
            p->vfork_done = &vfork;
            init_completion(&vfork);
        }

        if ((p->ptrace & PT_PTRACED) || (clone_flags & CLONE_STOPPED)) {
            /*
             * We'll start up with an immediate SIGSTOP.
             */
            sigaddset(&p->pending.signal, SIGSTOP);
            set_tsk_thread_flag(p, TIF_SIGPENDING);
        }

        if (!(clone_flags & CLONE_STOPPED))
            wake_up_new_task(p, clone_flags);
        else
            __set_task_state(p, TASK_STOPPED);

        if (unlikely (trace)) {
            current->ptrace_message = nr;
            ptrace_notify ((trace << 8) | SIGTRAP);
        }
        //check weather set CLONE_VFORK, if yes, call wait_for_completion
        if (clone_flags & CLONE_VFORK) {
            freezer_do_not_count();
            wait_for_completion(&vfork);
            freezer_count();
            if (unlikely (current->ptrace & PT_TRACE_VFORK_DONE)) {
                current->ptrace_message = nr;
                ptrace_notify ((PTRACE_EVENT_VFORK_DONE << 8) | SIGTRAP);
            }
        }

这段代码完成的过程包括:

  • 调用task_pid_nr(p)返回进程p的PID
  • 如果设置了CLONE_PARENT_SETTID,则把子进程的PID写入有ptid参数所指向的父进程的用户态变量
  • 如果设置了CLONE_VFORK,即使用vfork机制,则必须启用子进程的完成机制。
  • 如果设置了PT_PTRACED或是CLONE_STOPPED,则要设置相关信号的标志位
  • 如果没有设置CLONE_STOPPED标志,则调用函数wake_up_new_task函数。该函数起第一次唤醒新创建的进程的作用,主要过程为:初始化调度器的一些基本参数,接着把该任务放入到就绪队列中等待调度器的调度,并设置该任务的运行状态为TASK_RUNNING.但要记住:将进程排到调度器数据结构中并不意味着该子进程可以立即执行,而是调度器此时可以选择它运行。
  • 检查是否设置了CLONE_VFORK标志,如果设置了,则要调用wait_for_completion完成机制。

你可能感兴趣的:(Linux,2.6.xx内核分析)