fork源码剖析

大家一定听过这样一段话:
调用fork(),创建子进程;
调用pthread_create()创建子线程;
如果你要创建一个立刻执行exec()函数的进程,就使用vfork()。

说法没有错误,在我平时编写多进程/多线程项目时的确是这样使用的。那请问:为什么?它们有什么区别?既然在Linux系统上有进程、内核线程和用户线程之分,那调用不同的函数,分别创建的是什么?如果你能回答这些问题,本文章不是为您准备的。

下来的分析会借助fork的源码,但只会贴出重要的代码,如果你需要源码,可以去官网自行下载(http://kernel.org

fork()、pthread_create()、vfork()的系统调用函数分别是sys_fork()、sys_clone()、sys_vfork(),他们底层调用的都是do_fork(),只是传递给do_fork()参数和clone_flags不同,不同的参数决定父子进程之间共享哪些数据结构和信息,即确定此时创建的是进程、内核线程或用户线程。

这里写图片描述

当调用fork()时,产生0x80号中断,进程从用户模式切换到内核模式,查系统调用表sys_call_table,当前进程要调用的函数是sys_fork()。

1.sys_fork()底层调用的是do_fork()函数,do_fork()先对传进来的参数做检测,判断参数是否合法,接着调用copy_process()【copy_process()是do_fork函数的核心函数,创建进程都是在这个函数中实现的】

long do_fork(unsigned long clone_flags,
          unsigned long stack_start,
          struct pt_regs *regs,
          unsigned long stack_size,
          int __user *parent_tidptr,
          int __user *child_tidptr)
{
    struct task_struct *p;
    int trace = 0

    /*通过查找pidmap_array位图,为子进程分配新的pid参数*/
    long pid = alloc_pidmap();

    if (pid < 0)
        return -EAGAIN;

    /*如果父进程正在被跟踪,就检查debugger程序是否想跟踪子进程.并且子进程不是内核进程(CLONE_UNTRACED未设置),那么就设置CLONE_PTRACE标志。*/
    if (unlikely(current->ptrace)) {
        trace = fork_traceflag (clone_flags);
        if (trace)
            clone_flags |= CLONE_PTRACE;
    }

     /* copy_process复制进程描述符.如果所有必须的资源都是可用的,该函数返回刚创建的task_struct描述符的地址,这是创建进程的关键步骤。*/
    p = copy_process(clone_flags, stack_start, regs, stack_size, parent_tidptr, child_tidptr, pid);
    //在copy_processs之前的代码都是检验参数合法性

2.copy__process()开始也会检查参数的合法性,接着调用dup_task_struct()【这个函数是为进程创建task_struct结构的】。task_thread是每个进程都有的,task_thread结构体中有thread_info结构体,thread_info保存的是进程上下文的信息,所以fork的子进程应该有自己的thread_info结构。

static task_t *copy_process(unsigned long clone_flags,
                 unsigned long stack_start,
                 struct pt_regs *regs,
                 unsigned long stack_size,
                 int __user *parent_tidptr,
                 int __user *child_tidptr,
                 int pid)
{
    int retval;
    struct task_struct *p = NULL;

    /*检查clone_flags所传标志的一致性。*/

    /*如果CLONE_NEWNS和CLONE_FS标志都被设置,返回错误*/
    if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS))
        return ERR_PTR(-EINVAL);


    /* CLONE_THREAD标志被设置,并且CLONE_SIGHAND没有设置。(同一线程组中的轻量级进程必须共享信号)*/
    if ((clone_flags & CLONE_THREAD) && !(clone_flags & CLONE_SIGHAND))
        return ERR_PTR(-EINVAL);


    /*CLONE_SIGHAND被设置,但是CLONE_VM没有设置。 (共享信号处理程序的轻量级进程也必须共享内存描述符)*/
    if ((clone_flags & CLONE_SIGHAND) && !(clone_flags & CLONE_VM))
        return ERR_PTR(-EINVAL);

    /* 通过调用security_task_create以及稍后调用security_task_alloc执行所有附加的安全检查。 LINUX2.6提供扩展安全性的钩子函数,与传统unix相比,它具有更加强壮的安全模型。*/
    retval = security_task_create(clone_flags);
    if (retval)
        goto fork_out;

    retval = -ENOMEM;
    /**
     * 调用dup_task_struct为子进程获取进程描述符。
     */
    p = dup_task_struct(current);
    if (!p)
        goto fork_out;
    //在调用dup_task_struct之前的代码都是做参数检验
    ......
}

dup_task_struct()函数为子进程申请一个thread_info结构体,并用父进程的thread_info初始化子进程的thread_info结构体,用父进程的task_struct初始化子进程的task_struct,并将子进程task_struct中的thread_info指针指向自己的thread_info结构,由于thread_info结构体中有指向当前进程task_struct的指针,所以也要修改这个指针的指向,让其指向当前进程(子进程)的task_struct结构体。【这就是dup_task_struct函数的作用】

struct thread_info {
    struct task_struct  *task;      /* main task structure */
    struct exec_domain  *exec_domain;   /* execution domain */
    };
    .......//保存进程上下文的其他信息
static struct task_struct *dup_task_struct(struct task_struct *orig)
{
    struct task_struct *tsk;
    struct thread_info *ti;

    ......//初始化信息

    /**重点!!!
     * 将current进程描述符的内容复制到tsk所指向的task_struct结构中,然后把tsk_thread_info置为ti
     * 将current进程的thread_info内容复制给ti指向的结构中,并将ti_task置为tsk.
     */
    *ti = *orig->thread_info;
    *tsk = *orig;
    tsk->thread_info = ti;
    ti->task = tsk;

    ......//其他设置信息

    return tsk;

3.调用dup_task_struct函数返回后,子进程的task_struct已经创建完成。现在要进行信号量/文件描述符/文件上下文/信号处理函数/信号/虚拟内存等的拷贝,这些操作是通过copy_semundo,copy_files,copy_fs,copy_sighand,copy_signal
,copy_mm创建新的数据结构,并把父进程相应数据结构的值复制到新数据结构中。

如果clone_flag参数指出它们有不同的值,会选择父子进程共享某些数据结构,fork()、clone()、vfork()的不同也是在这儿体现的。其中最主要的是copy_mm(),它拷贝的是进程4G虚拟内存空间【copy_mm()实现稍后介绍】。

static task_t *copy_process(unsigned long clone_flags,
                 unsigned long stack_start,
                 struct pt_regs *regs,
                 unsigned long stack_size,
                 int __user *parent_tidptr,
                 int __user *child_tidptr,
                 int pid)
{
    ......//参数判断
    p = dup_task_struct(current);
    if (!p)
        goto fork_out;

    ......//参数设置    

    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_namespace(clone_flags, p)))
        goto bad_fork_cleanup_keys;

拷贝完数据结构后,进程的task_struct结构体已经创建完了,但是还没有复制父进程的系统堆栈,copy_thread就是复制系统调用堆栈的。但是,如果是完全复制,父子进程就没办法区分了,copy_thread函数将子进程的eax寄存器强行设置为0,在调用完成后,当子进程受调度恢复运行时,这就是返回值。这也解释了为什么fork在子进程中返回0。

retval = copy_thread(0, clone_flags, stack_start, stack_size, p, regs);
int copy_thread(int nr, unsigned long clone_flags, unsigned long esp,
    unsigned long unused,
    struct task_struct * p, struct pt_regs * regs)
{
    struct pt_regs * childregs;
    struct task_struct *tsk;
    int err;

    childregs = ((struct pt_regs *) (THREAD_SIZE + (unsigned long) p->thread_info)) - 1;
    *childregs = *regs;
    //子进程的eax寄存器设置为0
    childregs->eax = 0;
    childregs->esp = esp;

    p->thread.esp = (unsigned long) childregs;
    p->thread.esp0 = (unsigned long) (childregs+1);

    p->thread.eip = (unsigned long) ret_from_fork;

    savesegment(fs,p->thread.fs);
    savesegment(gs,p->thread.gs);

    tsk = current;
    if (unlikely(NULL != tsk->thread.io_bitmap_ptr)) {
        p->thread.io_bitmap_ptr = kmalloc(IO_BITMAP_BYTES, GFP_KERNEL);
        if (!p->thread.io_bitmap_ptr) {
            p->thread.io_bitmap_max = 0;
            return -ENOMEM;
        }
        memcpy(p->thread.io_bitmap_ptr, tsk->thread.io_bitmap_ptr,
            IO_BITMAP_BYTES);
    }


    if (clone_flags & CLONE_SETTLS) {
        struct desc_struct *desc;
        struct user_desc info;
        int idx;

        err = -EFAULT;
        if (copy_from_user(&info, (void __user *)childregs->esi, sizeof(info)))
            goto out;
        err = -EINVAL;
        if (LDT_empty(&info))
            goto out;

        idx = info.entry_number;
        if (idx < GDT_ENTRY_TLS_MIN || idx > GDT_ENTRY_TLS_MAX)
            goto out;

        desc = p->thread.tls_array + idx - GDT_ENTRY_TLS_MIN;
        desc->a = LDT_entry_a(&info);
        desc->b = LDT_entry_b(&info);
    }

    err = 0;
 out:
    if (err && p->thread.io_bitmap_ptr) {
        kfree(p->thread.io_bitmap_ptr);
        p->thread.io_bitmap_max = 0;
    }
    return err;
}

4.copy_process函数完成了它的使命,成功创建完成子进程需要的所有东西。copy_process函数完成后,判断当前进程是否设置了CLONE_STOPPED,如果没有设置,就调用wake_up_new_task,将子进程插入父进程的运行队列,并且将子进程插入父进程之前,先运行子进程。原因是:如果子进程在创建之后执行exec替换新进程,就可以避免写时拷贝机制执行不必要的页面复制

if (!(clone_flags & CLONE_STOPPED))
        wake_up_new_task(p, clone_flags);
else//如果CLONE_STOPPED标志被设置,就把子进程设置为TASK_STOPPED状态。
        p->state = TASK_STOPPED;

copy_mm()是复制进程虚拟空间的函数,其中dup_mmap是最重要的函数,dup_mmap函数里复制了父进程的线性区链表、红黑树、页目录和页表,为了实现写时拷贝机制,会将私有的、可写的页面权限设置只读,父子进程共享同一个物理页面,只有任意一个进程给页面上写数据的时候,发生异常,才会为进程分配独立页面,并修改该页的权限。

static int copy_mm(unsigned long clone_flags, struct task_struct * tsk)
{
    struct mm_struct * mm, *oldmm;
    int retval;

    tsk->min_flt = tsk->maj_flt = 0;
    tsk->nvcsw = tsk->nivcsw = 0;

    tsk->mm = NULL;
    tsk->active_mm = NULL;

    oldmm = current->mm;

    //指定了CLONE_VM标志,表示创建线程。
    if (clone_flags & CLONE_VM) 
    {
        //新线程共享父进程的地址空间,所以需要将mm_users加一
        atomic_inc(&oldmm->mm_users);
        mm = oldmm;
        return;
    }

    //必须要有地址空间,即使此时并没有分配内存。
    retval = -ENOMEM;

    //分配一个新的内存描述符。把它的地址存放在新进程的mm中。
    mm = allocate_mm();

    //从当前进程mm的内容复制给mm
    memcpy(mm, oldmm, sizeof(*mm));

    //dup_mmap不但复制了线程区和页表,也设置了mm的一些属性。它也会改变父进程的私有,可写的页为只读的,以使写时复制机制生效。
    retval = dup_mmap(mm, oldmm);
    mm->hiwater_rss = mm->rss;
    mm->hiwater_vm = mm->total_vm;

    return retval;
}
static inline int dup_mmap(struct mm_struct * mm, struct mm_struct * oldmm)
{
    struct vm_area_struct * mpnt, *tmp, **pprev;
    struct rb_node **rb_link, *rb_parent;
    int retval;
    unsigned long charge;

    //复制父进程的每一个vm_area_struct线性区描述符,并把复制品插入到子进程的线性区链表和红黑树中。
    for (mpnt = current->mm->mmap ; mpnt ; mpnt = mpnt->vm_next)
    {
        struct file *file;

        if (mpnt->vm_flags & VM_DONTCOPY)
        {
            __vm_stat_account(mm, mpnt->vm_flags, mpnt->vm_file,
                            -vma_pages(mpnt));
            continue;
        }
        charge = 0;
        if (mpnt->vm_flags & VM_ACCOUNT) 
        {
            unsigned int len = (mpnt->vm_end - mpnt->vm_start) >> PAGE_SHIFT;
            charge = len;
        }

        tmp = kmem_cache_alloc(vm_area_cachep, SLAB_KERNEL);

        *tmp = *mpnt;
        pol = mpol_copy(vma_policy(mpnt));
        retval = PTR_ERR(pol);

        vma_set_policy(tmp, pol);
        tmp->vm_flags &= ~VM_LOCKED;
        tmp->vm_mm = mm;
        tmp->vm_next = NULL;
        anon_vma_link(tmp);
        file = tmp->vm_file;
        if (file)
        {
            struct inode *inode = file->f_dentry->d_inode;
            get_file(file);
            if (tmp->vm_flags & VM_DENYWRITE)
                atomic_dec(&inode->i_writecount);

            spin_lock(&file->f_mapping->i_mmap_lock);
            tmp->vm_truncate_count = mpnt->vm_truncate_count;
            flush_dcache_mmap_lock(file->f_mapping);
            vma_prio_tree_add(tmp, mpnt);
            flush_dcache_mmap_unlock(file->f_mapping);
            spin_unlock(&file->f_mapping->i_mmap_lock);
        }


        spin_lock(&mm->page_table_lock);
        *pprev = tmp;
        pprev = &tmp->vm_next;

        __vma_link_rb(mm, tmp, rb_link, rb_parent);
        rb_link = &tmp->vm_rb.rb_right;
        rb_parent = &tmp->vm_rb;

        mm->map_count++;

        //copy_page_range创建必要的页表来映射线性区所包含的一组页。并且初始化新页表的表项。将私有、可写的页对父子进程都标记为只读的,为写时复制进行处理。
        retval = copy_page_range(mm, current->mm, tmp);
        spin_unlock(&mm->page_table_lock);

        if (tmp->vm_ops && tmp->vm_ops->open)
            tmp->vm_ops->open(tmp);
    }
}

如果是clone函数,do_fork会根据传进来的参数的值,决定哪些共享,哪些复制。共享的知识改变task_struct结构体中指针的指向,让其指向父进程的结构。此时创建的是内核线程。

如果是vforkh函数,do_fork的clone_flags标识都是1,除task_struct结构外,所有的数据结构都是共享的。并且CLONE_VM设置为0,此时创建的是一个用户线程。

至此,fork执行完毕!

一张图帮你梳理fork
这里写图片描述

源码剖析过程参考《Linux内核源代码情景分析》,如有不对,欢迎指正。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值