关闭

进程的创建 —— do_fork()函数详解

867人阅读 评论(0) 收藏 举报

原文链接http://blog.csdn.net/yunsongice/article/details/5508242

在讲进程管理专题最核心的课题——进程创建之前,我们先简单地回顾一下上一篇博文的sys_clone()系统调用:
asmlinkage int sys_clone(struct pt_regs regs)
{
    unsigned long clone_flags;
    unsigned long newsp;
    int __user *parent_tidptr, *child_tidptr;

    clone_flags = regs.ebx;
    newsp = regs.ecx;
    parent_tidptr = (int __user *)regs.edx;
    child_tidptr = (int __user *)regs.edi;
    if (!newsp)
        newsp = regs.esp;
    return do_fork(clone_flags, newsp, &regs, 0, parent_tidptr, child_tidptr);
}

那么,在同一个文件中(针对80x86平台是/arch/i386/kernel/Process.c),有:
asmlinkage int sys_fork(struct pt_regs regs)
{
    return do_fork(SIGCHLD, regs.esp, &regs, 0, NULL, NULL);
}

asmlinkage int sys_vfork(struct pt_regs regs)
{
    return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs.esp, &regs, 0, NULL, NULL);
}

我们看到,不管是clone、fork还是vfork系统调用,他们的实现函数sys_clone、sys_fork和sys_vfork都指向了位于/kernel/Fork.c中的do_fork函数,唯一的不同就是clone_flags的不同,具体含义请参考前一博文。

这里,我们就来详细分析进程创建的实务函数 —— 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)
先介绍一下它执行时使用的参数:
clone_flags:与clone()参数flags相同。
stack_start:与clone()参数stack_start相同。
regs:指向内核态堆栈通用寄存器值的指针,通用寄存器的值是在从用户态切换到内核态时被保存到内核态堆栈中的。
stack_size:未使用,总被设置为0。
parent_tidptr,child_tidptr:与clone系统调用中对应参数ptid和ctid相同。

do_fork()函数利用辅助函数copy_process()来创建进程描述符以及子进程所需要的其他所有数据结构。下面是do_fork()函数执行的主要步骤:

1. 通过查找pidmap_array位图,为子进程分配新的PID
2. 检查父进程的ptrace字段:如果它的值不等于0,说明有另外一个进程正在跟踪父进程,因而,do_fork()函数检查debugger程序是否自己想跟踪子进程。在这种情况下,如果子进程不是内核线程(CLONE_UNTRACED标志被清0),则do_fork()函数设置CLONE_PTRACE标志。
3. 调用copy_process()函数复制进程描述符。如果所有必须的资源都是可用的,则该函数返回刚创建的task_struct描述符的地址。
4. 如果设置了CLONE_STOPPED标志,或者必须跟踪子进程,即在p->ptrace 中设置 PT_PTRACED标志,那么子进程的状态被设置成TASK_STOPPED状态,并且为子进程增加挂起的SIGSTOP信号。在另一个进程把子进程状态恢复成TASK_RUNNING之前,一直保持该状态。

5. 如果没有设置CLONE_STOPPED标志,则调用wake_up_new_task(p, clone_flags)函数以执行以下操作:


a.调整父进程和子进程的调度参数
b.如果子进程和父进程运行在同一个CPU上,而且父进程和子进程不能共享同一组页表(CLONE_VM标志被清0),那么,就把子进程插入到父进程的运行队列,插入时让子进程恰好在父进程前面,因此迫使子进程优于父进程先运行。如果子进程刷新其地址空间,并且在创建之后执行新程序,那么这种简单的处理会产生较好的性能。而如果我们让父进程先运行,那么写时复制机制将会执行一些不必要的页面复制。
c.否则,如果子进程与父进程运行在不同CPU上,或者父进程和子进程共享同一组页表(CLONE_VM标志被设置),就把子进程插入父进程所在运行队列的队尾。


6. 如果设置了CLONE_STOPPED标志,则子进程的状态被设置成TASK_STOPPED状态。
7. 如果父进程被跟踪,则把子进程的PID存入current的ptrace_message字段并调用ptrace_notify函数使当前进程停止运行,并向当前进程的父进程发送SIGCHLD信号。子进程的祖父进程是跟踪父进程的debugger进程。SIGCHLD信号通知debugger进程:当前进程current已经创建了一个子进程,可以通过current->ptrace_message字段获得该子进程的PID。
8. 如果设置了CLONE_VFORK标志,则把父进程插入等待队列,并挂起父进程直到子进程释放自己的内存地址空间(也就是说,直到子进程结束或执行新的程序)。
9. 结束并返回子进程的PID。

下边,我们重点关注一下do_fork()函数的copy_process过程,其同样来自于/kernel/Fork.c中:copy_process()函数创建进程描述符以及子进程执行所需要的其他数据结构。他的参数与do_fork()相同,外加子进程PID:
p = copy_process(clone_flags, stack_start, regs, stack_size, parent_tidptr, child_tidptr, nr);

下面描述其最重要的处理步骤:
1. 检查参数clone_flags所传递标志的一致性。尤其是在下列情况下,函数返回错误代号:


a. CLONE_NEWNS和CLONE_FS标志都被设置。
b. CLONE_THREAD标志被设置,但CLONE_SIGHAND标志被清0(同一线程组中的轻量级进程必须共享信号)。


2. 通过调用security_task_create(clone_flags)函数以及稍后的security_task_alloc(p)函数执行所有附加的安全检查。
3. 调用dup_task_struct(current)函数(也是来自/kernel/Fork.c)为子进程获得进程描述符。该函数执行以下操作:


a. 如果需要,则在当前进程中调用__unlazy_fpu()宏,把FPU、MMX和SSE/SSE2寄存器内容保存到thread_info结构中。稍后,dup_task_struct函数将把这些复制到子进程的thread_info结构中。
b. 执行alloc_task_struct()宏,用slab分配器task_struct_cachep为新进程获取进程描述符,并将描述符地址保存在tsk局部变量中。
c. 执行alloc_thread_info宏以获取一块空闲内存区(#define alloc_thread_info(tsk) kmalloc(THREAD_SIZE, GFP_KERNEL)),用来存放新进程的thread_info结构和内核栈,并将这些块内存区字段的地址存在局部变量ti中。正如前面博文说的,这块内存区的大小是8KB或4KB。
d. 将current进程描述符的内容复制到tsk所指向的task_struct结构中,然后把tsk->thread_info置为ti。注意,这里的代码是简简单单的*tsk = *orig:让新建立的tsk指向的task_struct和orig指向的父进程的task_struct中的每个字段的值完全相等(完全复制)。
e. 把current进程的thread_info描述符的内容复制到ti所指向的结构中,然后把ti->task置为tsk。
f. 把新进程描述符使用计数器(tsk->usage)置为2,用来表示进程描述符正在被使用而且其相应的进程状态处于活动状态(进程状态既不是EXIT_ZOMBIE,也不是EXIT_DEAD)。
g. 返回新进程的进程描述符指针tsk。


4. 检查存放在p->signal->rlim[RLIMIT_NPROC].rlim_cur变量中的值是否小于或等于用户所拥有的进程数。如果是,则返回错误码,除非进程没有root权限。该函数从每个用户数据结构user_struct中获取用户拥有的进程数。通过进程描述符user字段指针可以找到这个数据结构。
5. 递增user_struct结构中使用计数器(tsk->user->__count)和用户所拥有的进程计数器(tsk->user->processes)。
6. 查系统中的进程数量(存放在nr_threads变量中)是否超过max_threads变量的值。这个变量的缺省值取决于系统内存容量的大小。总的原则是:所有thread_info描述符和内核栈所占用的空间不能超过物理内存大小的1/8。不过,系统管理员可以通过写/proc/sys/kernel/threads-max文件来改变这个值。
7. 如果实现新进程的执行域和可执行格式的内核函数都包含在内核模块中,则递增它们的使用计数器。
8. 设置与进程状态相关的几个关键字段:


a. 把大内核锁计数器tsk->lock_depth初始化为-1。
b. 把tsk->did_exec字段初始化为0:它记录了进程发出的execve()系统调用的次数。
c. 通过copy_flags函数更新从父进程复制到tsk->flags字段中的一些标志:首先清除PF_SUPERPRIV标志,该标志表示进程是否使用了某种超级用户权限。然后设置PF_FORKNOEXEC标志,它表示子进程还没有发出execve()系统调用。


9. 把新进程的PID存入tsk_pid字段。
10. 如果clone_flags参数中的CLONE_PARENT_SETTID标志被设置,就把子进程的PID复制到参数parent_tidptr指向的用户态变量中。
11. 初始化子进程描述符中的list_head数据结构和自旋锁,并为与挂起信号、定时器及时间统计表相关的若干字段赋初值。
12. 调用copy_semundo、copy_files、copy_fs、copy_sighand、copy_signal、copy_mm和copy_namespace来创建新的数据结构,并把父进程的相应数据结构的值复制到新数据结构中,除非clone_flags参数指出它们有不同的值。
13. 调用copy_thread(0, clone_flags, stack_start, stack_size, p, regs),用发出clone()系统调用时CPU寄存器的值来初始化子进程的内核栈。不过,copy_thread把eax寄存器对应的字段(这也是fork和clone系统调用在子进程中的返回值)字段强行置为0。进程描述符的thread.esp字段初始化为子进程内核栈的基地址,汇编语言函数ret_from_fork()的地址存放在thread.eip字段中。如果父进程使用I/O权限位图,则子进程获取该位图的一个拷贝。最后,如果CLONE_SETTLS标志被设置,则子进程获取由clone系统调用的参数tls指向的用户态数据结构所表示的TLS段。
14. 如果clone_flags参数的值被置为CLONE_CHILD_SETTID或CLONE_CHILD_CLEARTID,就把child_tidptr参数的值分别复制到tsk->set_child_tid或tsk->clear_child_tid字段。这些标志说明:必须改变子进程用户态地址空间的child_tidptr所指向的变量的值,不过实际的写操作要稍后再执行。
15. 清除子进程thread_info结构的TIF_SYSCALL_TRACE标志,以使ret_from_fork()函数不会把系统调用结束的消息通知给调试进程。
16. 用clone_flags参数低位的信号数字编码初始化tsk-> exit_signal字段,如果CLONE_THREAD标志被设置,就把tsk-> exit_signal字段初始化为-1。正如我们将在下一章进程终止所看见的,只有当线程组的最后一个成员(通常是现在组的头儿)死亡,才会产生一个信号,以通知领头进程的父进程。
17. 调用sched_fork(p)完成对新进程调度程序数据结构的初始化。该函数将新进程的状态设置为TASK_RUNNING,并把thread_info结构的preempt_count字段设置为1,从而禁止内核抢占。此外,为了保证公平的进程调度,该函数在父子进程之间共享父进程的时间片。
18. 把新进程的thread_info结构的cpu字段设置为由smp_processor_id()所返回的本地CPU号。
19. 初始化表示亲子关系的字段。尤其是,如果CLONE_PARENT或CLONE_THREAD被设置,就用current->real_parent的值初始化tsk->real_parent和tskp->parent,因此,子进程的父进程似乎是当前进程的父进程。否则tsk->real_parent和tskp->parent置为当前进程。
20. 如果不需要跟踪子进程(没有设置CLONE_PTRACE标志),就把tsk->ptrrace字段设置为0。 tsk->ptrrace字段会存放一些标志,而这些标志是在一个进程被另外一个进程跟踪时才会用到的。采用这种方式,即使当前进程被跟踪,子进程也不会被跟踪。
21. 执行SET_LINKS宏,把新进程描述符插入进程链表。
22. 如果子进程必须被跟踪(tsk->ptrrace字段的PT_PTRACED标志被设置),就把current->parent赋给tsk->parent,并将子进程插入调试程序的跟踪链表中。
23. 调用attach_pid把新进程描述符PID插入pidhash[PIDTYPE_PID]散列表。
24. 如果子进程是领头进程(CLONE_THREAD标志被清0):


a. 把tsk->tgid的初值置为tsk->pid。
b. 把tsk->group_leader的初值设置为tsk。
c. 调用三次attach_pid()函数,把子进程分别插入PIDTYPE_TGID、PIDTYPE_PGID和PIDTYPE_SID类型的散列表。


25. 否则,如果子进程属于它的父进程的线程组(CLONE_THREAD标志被设置):


a. 把tsk->tgid的初值置为tsk->current->tgid。
b. 把tsk->group_leader的初值置为current->group_leader。
c. 调用attach_pid(),把子进程描述符插入PIDTYPE_TGID类型的散列表中。


26. 现在,新进程已经被加入进程集合:递增nr_threads变量的值。
27. 递增total_forks变量以记录被创建的进程的数量。
28. 终止并返回子进程描述符指针(p,等价于tsk)。

好了,让我们再回头看看在do_fork()结束之后都发生了什么。现在,我们有了处于可运行状态的完整的子进程。但是,它还没有实际运行,调度程序要决定何时把CPU交给这个子进程。在以后的进程切换中,调度程序继续完善子进程:把子进程描述符thread字段的值(TSS值)装入几个CPU寄存器。特别是把thread.esp装入esp寄存器,把函数ret_from_fork()的地址装入eip寄存器。这个汇编语言函数调用schedule_tail()函数,用存放在栈中的值再装入所有寄存器,并强迫CPU返回到用户态。这样,eax寄存器就装过两个值,一个是子进程的值0,一个是父进程的值——子进程的PID。然后在fork()、vfork()或clone()返回时,新进程将开始执行,这也是为啥这几个函数会魔幻般地返回两个值的原因。

除非fork系统调用返回0,否则,子进程将与父进程执行相同的代码(参见copy_process的第13步)。应用程序的开发者可以按照UNIX编程者熟悉的方式利用这一事实,在基于PID值的程序中插入一条if语句便可以使子进程与父进程有不同的行为。

0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:197940次
    • 积分:2555
    • 等级:
    • 排名:第14813名
    • 原创:46篇
    • 转载:80篇
    • 译文:0篇
    • 评论:14条
    最新评论