fork源码浅析

作业啦,仔细看了下fork的代码,大致记录一下。。

fork分析

不像Windows要执行一个程序直接使用CreateProcess,在Linux下先使用fork创建子进程,这个子进程像细胞分裂一样,与父进程拥有一样的代码段与数据段等,接着再调用execve替换子进程的内容,使它变成目标程序再执行,也就是说Linux下先用fork分配程序运行所需要的资源,再用execve载入目标程序,本次主要分析fork的代码。

相关文件

  1. arch/i386/kernel/process.c:提供系统调用接口,对do_fork进行简单封装
  2. include/linux/sched.h:定义了struct task_struct结构,即进程控制块结构
  3. kernel/fork.c:do_fork的具体实现
  4. include/asm-i386/ptrace.h:定义了struct pt_regs,存放了系统调用时用户空间传来的寄存器值

总述

fork会按照分配内核栈空间->设置task_struct->拷贝共享资源->设置内核栈的顺序复制父进程的资源:

  1. 分配内核栈,此结构如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    (THREAD_SIZE+(unsinged
    long)P) +------> +---------------+ <--------+
    | pt_regs | |
    ((struct pt_regs*)(THRE | | |
    AD_SIZE)+(unsigned +-----> +---------------+ |
    long)P) - 1) | | |
    | | |
    | | |
    | | THREAD_SIZE
    | | |
    +---------------+ |
    | | |
    | | |
    | | |
    | | |
    | | |
    | | |
    +---------------+ |
    | task_struct | |
    +---------------+ <--------+
  2. 接着设置内核栈的tast_struct结构,它在include/linux/sched.h中定义,它里面记录着进程执行所需的各种重要信息,他们大致分为:

    性质,状态:运行状态,用户组,权限等
    资源:内存,文件等
    组织:进程树情况

  3. task_struct中大部分内容是指针,包括各种资源指针,于是需要根据需求,要么增加针指所指目标的引用计数,要么对目标进行指定深度的复制,使指针指向新的备份。

  4. 当将task_struct的内容处理完毕,一个进程要运行就只差内核栈内容了,例如用户空间的各种上下文信息都保存在pt_regs中,这里将内核栈复制后,通过设置不同的返回值,子进程就可以与父进程返回,执行相应内容了。

细节分析

  1. arch/i386/kernel/process.c可见与创建子进程有关的系统调用有三个,它们只是对do_fork进行简单包装,本篇以sys_fork为主线分析do_fork

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    asmlinkage int sys_fork(struct pt_regs regs)
    {
    return do_fork(SIGCHLD, regs.esp, &regs, 0);
    }

    asmlinkage int sys_clone(struct pt_regs regs)
    {
    unsigned long clone_flags;
    unsigned long newsp;

    clone_flags = regs.ebx;
    newsp = regs.ecx; //第二个参数为新栈帧的地址
    if (!newsp)
    newsp = regs.esp;
    return do_fork(clone_flags, newsp, &regs, 0);
    }

    asmlinkage int sys_vfork(struct pt_regs regs)
    {
    return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs.esp, &regs, 0);
    }
  2. do_fork的声明如下,第一个参数为复制的标志,如上所见,在创建子进程时有多种不同的方式,他们的区别即复制父进程资源时以何种深度进行,是共享资源还是创建独立的备份。第二个参数代表子进程的用户栈起始位置,在sys_fork方式中和父进程一样,都来自regs中。第三个参数是系统调用时传入的寄存器值,第四个参数是栈大小。

    1
    2
    int do_fork(unsigned long clone_flags, unsigned long stack_start,
    struct pt_regs *regs, unsigned long stack_size);
  3. 分配一个内核栈,并将当前进程(父进程)的task_struct整体赋值给子进程,接下来只需根据需要调整它里面的值:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    int retval = -ENOMEM;
    struct task_struct *p;
    DECLARE_MUTEX_LOCKED(sem); //这个信号量初始为0,即可用来使自己阻塞

    if (clone_flags & CLONE_PID) { //让父子进程拥有同样的进程号

    if (current->pid) //只有0号进程有权限
    return -EPERM;
    }

    current->vfork_sem = &sem;

    p = alloc_task_struct(); //为子进程创建task_struct结构
    if (!p)
    goto fork_out;

    *p = *current; //复制父进程的task_struct
  4. 现在根据实际来更改从父进程那里复制过来的task_struct的值:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    retval = -EAGAIN;
    if (atomic_read(&p->user->processes) >= p->rlim[RLIMIT_NPROC].rlim_cur) //用户拥有的进程数是否超过限制
    goto bad_fork_free;
    atomic_inc(&p->user->__count); //添加user_struct的引用计数
    atomic_inc(&p->user->processes); //user_struct拥有的进程数加一

    /*
    * Counter increases are protected by
    * the kernel lock so nr_threads can't
    * increase under us (but it may decrease).
    */
    if (nr_threads >= max_threads)
    goto bad_fork_cleanup_count;

    get_exec_domain(p->exec_domain); //获取进程执行域

    if (p->binfmt && p->binfmt->module) //获取到并且它有对应模块
    __MOD_INC_USE_COUNT(p->binfmt->module); //对应驱动模块引用计数加一

    p->did_exec = 0;
    p->swappable = 0;
    p->state = TASK_UNINTERRUPTIBLE; //将进程状态设置为不可被软中断唤醒,在最后用wake_up唤醒

    copy_flags(clone_flags, p); //对传入的clone_flags进行解析后赋值给p的flags位
    p->pid = get_pid(clone_flags); //分配一个可用pid,在其内部:
    //若设置了与父进程共享PID则直接返回,前面已有检查只有0号进程能这样做

    p->run_list.next = NULL; //将运行队列设置为空
    p->run_list.prev = NULL;

    if ((clone_flags & CLONE_VFORK) || !(clone_flags & CLONE_PARENT)) {
    p->p_opptr = current;
    if (!(p->ptrace & PT_PTRACED))
    p->p_pptr = current;
    }
    p->p_cptr = NULL;
    init_waitqueue_head(&p->wait_chldexit); //将等待子进程退出队列置为空
    p->vfork_sem = NULL;
    spin_lock_init(&p->alloc_lock);

    p->sigpending = 0; //收到待处理的信号数为0
    init_sigpending(&p->pending); //待处理信号队列为空

    p->it_real_value = p->it_virt_value = p->it_prof_value = 0;
    p->it_real_incr = p->it_virt_incr = p->it_prof_incr = 0;
    init_timer(&p->real_timer);
    p->real_timer.data = (unsigned long) p;

    p->leader = 0; /* session leadership doesn't inherit */
    p->tty_old_pgrp = 0;
    p->times.tms_utime = p->times.tms_stime = 0;
    p->times.tms_cutime = p->times.tms_cstime = 0;

    p->lock_depth = -1; /* -1 = no lock */
    p->start_time = jiffies; //进程创建时间
  5. 现在复制资源,以下的复制根据传入的参数clone_flags进行:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    retval = -ENOMEM;
    /* copy all the process information */
    if (copy_files(clone_flags, p)) //复制打开的资源,若CLONE_FILES被置位,则只增加文件结构引用计数
    goto bad_fork_cleanup;
    if (copy_fs(clone_flags, p)) //复制文件系统,只有CLONE_FS未被置位,才复制fs_struct结构
    goto bad_fork_cleanup_files;
    if (copy_sighand(clone_flags, p)) //复制信号处理例程
    goto bad_fork_cleanup_fs;
    if (copy_mm(clone_flags, p)) //复制内存内容,即使CLONE_VM|CLONE_VFORK为0,也只会复制mm_struct,vm_area_struct区域,而不会
    //复制内存的内容,对于只读数据当然可以共享,对于可写且不共享的数据,采用写时复制,即更改对应页权限
    //为只读,当写时会触发异常,在异常处理例程中再分配新的物理页面,复制数据,并改页表项
    goto bad_fork_cleanup_sighand;
  6. 接着设置内核栈:

    1
    2
    3
    4
    retval = copy_thread(0, clone_flags, stack_start, stack_size, p, regs); //复制内核栈
    if (retval)
    goto bad_fork_cleanup_sighand;
    p->semundo = NULL;

    copy_thread设置了子程序的返回值,内核栈指针,返回地址,pt_regs等信息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    #define savesegment(seg,value) \
    asm volatile("movl %%" #seg ",%0":"=m" (*(int *)&(value))) ;将寄存器内容存入指定内存

    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;

    childregs = ((struct pt_regs *) (THREAD_SIZE + (unsigned long) p)) - 1; //得到栈底处->childregs
    struct_cpy(childregs, regs); //将系统调用的寄存器赋值
    childregs->eax = 0; //子进程返回值为0
    childregs->esp = esp; //新的子进程用户栈位置,fork时和父进程一样

    p->thread.esp = (unsigned long) childregs; //pt_regs结构位置
    p->thread.esp0 = (unsigned long) (childregs+1); //内核栈底

    p->thread.eip = (unsigned long) ret_from_fork; //返回地址

    savesegment(fs,p->thread.fs); //将fs,gs存入thread
    savesegment(gs,p->thread.gs);

    unlazy_fpu(current);
    struct_cpy(&p->thread.i387, &current->thread.i387);

    return 0;
    }
  7. 现在准备工作基本做完,再设置几个状态,就可以将进程加入等待队列了,这样子进程就可以被调度了:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    	p->parent_exec_id = p->self_exec_id;                                    //父进程的执行域为当前进程的执行域

    /* ok, now we should be set up.. */
    p->swappable = 1; //子进程可被换出
    p->exit_signal = clone_flags & CSIGNAL; //退出时发出的信号
    p->pdeath_signal = 0; //父进程退出时不像子进程发信号

    p->counter = (current->counter + 1) >> 1; //父子运行时间配额将平分父进程的时间
    current->counter >>= 1;
    if (!current->counter)
    current->need_resched = 1;

    retval = p->pid; //返回值,即父进程返回子进程的pid
    p->tgid = retval;
    INIT_LIST_HEAD(&p->thread_group); //初始化线程组
    write_lock_irq(&tasklist_lock);
    if (clone_flags & CLONE_THREAD) { //如果创建的是线程,则将子"线程"加入当前进程的线程组
    p->tgid = current->tgid;
    list_add(&p->thread_group, &current->thread_group);
    }
    SET_LINKS(p); //链如内核进程队列
    hash_pid(p); //按pid散列链入散列队列
    nr_threads++;
    write_unlock_irq(&tasklist_lock);

    if (p->ptrace & PT_PTRACED)
    send_sig(SIGSTOP, p, 1);

    wake_up_process(p); /* do this last */ //唤醒子进程,将其加入可运行等待队列
    ++total_forks;

    fork_out:
    if ((clone_flags & CLONE_VFORK) && (retval > 0)) //如果是vfork且pid大于0即子进程创建成功,则让父进程阻塞
    down(&sem);
    return retval;

    参考

    [0] 毛德操:《Linux内核源码情景分析·上》
    [1] kernel.org:Linux 2.4.0