copy_process()分析
通过上面的分析我们得知do_fork()主要完成以下的工作:为子进程定义了一个进程描述符并申请pid;调用copy_process()复制子进程;再通过clone_flags标志做一些复制后的辅助工作。copy_process()函数主要用来创建子进程的描述符以及与子进程相关数据结构。这个函数内部实现较为复杂,在短时间内,对于内部详细代码原理和实现并不能全部理解。因此,接下来的分析侧重于copy_process()的执行流程。
1. 定义返回值变量和新的进程描述符。
int retval; struct task_struct *p = NULL;
2. 对clone_flags所传递的标志组合进行合法性检查。当出现以下三种情况时,返回出错代号:
(1). CLONE_NEWNS和CLONE_FS同时被设置。
前者标志表示子进程需要自己的命名空间,而后者标志则代表子进程共享父进程的根目录和当前工作目录,两者不可兼容。
传统的Unix系统中,整个系统只有一个已经安装的文件系统树。每个进程从系统的根文件系统开始,通过合法的路径可以访问任何文件。在2.6版本中的内核中,每个进程都可以拥有属于自己的已安装文件系统树,也被称为命名空间。通常大多数进程都共享init进程所使用的已安装文件系统树,只有在clone_flags中设置了CLONE_NEWNS标志时,才会为此新进程开辟一个新的命名空间。
(2). CLONE_THREAD被设置,但CLONE_SIGHAND未被设置。
如果子进程和父进程属于同一个线程组(CLONE_THREAD被设置),那么子进程必须共享父进程的信号(CLONE_SIGHAND被设置)。
(3). CLONE_SIGHAND被设置,但CLONE_VM未被设置。
如果子进程共享父进程的信号,那么必须同时共享父进程的内存描述符和所有的页表(CLONE_VM被设置)。
3. 通过调用security_task_create()和后面的security_task_alloc()执行所有附加的安全性检查。
4. 通过dup_task_struct()为子进程分配一个内核栈、thread_info结构和task_struct结构。
p = dup_task_struct(current);
注意,这里将当前进程描述符指针作为参数传递到此函数中。该函数内部的具体过程如下:
首先,该函数分别定义了指向task_struct和thread_inof结构体的指针。
static struct task_struct *dup_task_struct(struct task_struct *orig) { struct task_struct *tsk; struct thread_info *ti;
接着,为正式的分配进程描述符做一些准备工作。主要是将一些必要的寄存器的值保存到父进程的thread_info结构中。这些值会在稍后被复制到子进程的thread_info结构中。
prepare_to_copy(orig);
执行alloc_task_struct宏,该宏负责为子进程的进程描述符分配空间,将该片内存的首地址赋值给tsk;随后检查这片内存是否分配正确。
tsk = alloc_task_struct(); if (!tsk) return NULL;
执行alloc_thread_info宏,为子进程获取一块空闲的内存区,用来存放子进程的内核栈和thread_info结构,并将此会内存区的首地址赋值给ti变量;随后检查是否分配正确。
ti = alloc_thread_info(tsk); if (!ti) { free_task_struct(tsk); return NULL; }
上面已经说明过orig是指向当前进程描述符的指针。因此,先将当前进程的thread_info结构中的内容复制到ti变量;再将当前进程task_struct结构中的内容复制到tsk变量;让子进程描述符中的thread_info字段指向ti变量;最后让子进程thread_info结构中的task字段指向tsk变量。
*ti = *orig->thread_info; *tsk = *orig; tsk->thread_info = ti; ti->task = tsk;
将子进程描述符的使用计数器设置为2,表示该进程描述符正在被使用并且处于活动状态。
atomic_set(&tsk->usage,2);
最后返回指向刚刚创建的子进程描述符内存区的指针。
return tsk; }
通过上述代码可以看到,当这个函数成功操作之后,子进程和父进程的描述符中的内容是完全相同的。在稍后的代码中,我们将会看到子进程逐步与父进程区分开来。
5. 更新当前用户的user_struct结构。当前进程的用户如果没有root权限,并且所拥有的进程数大于所规定的进程数时,就返回错误代码。
接着对该user_struct结构的引用计数加1;对该用户所拥有的进程总数量加1。
atomic_inc(&p->user->__count); atomic_inc(&p->user->processes);
6. 检测系统中进程的总数量是否超过了max_threads所规定的进程最大数。
if (nr_threads >= max_threads) goto bad_fork_cleanup_count;
7. 将从do_fork()传递来的的clone_flags和pid分别赋值给子进程描述符中的对应字段。
copy_flags(clone_flags, p); p->pid = pid;
8. 逐步初始化子进程描述符中字段,使得子进程和父进程逐渐区别出来。这部分工作包含初始化双联表、互斥锁和描述进程属性的字段等。它在copy_process函数中占据了相当长的一段的代码,不过考虑到task_struct结构本身的复杂性,也就不足为奇了。
9. 根据clone_flags的具体取值,通过诸如copy_semundo()和copy_files()等这样的函数来为子进程拷贝或共享父进程的某些数据结构。
10. 通过copy_threads()函数更新子进程的内核栈和寄存器中的值。在之前的dup_task_struct()中只是为子进程创建一个内核栈,至此才是真正的赋予它有意义的值。
当父进程发出clone系统调用时,内核会将那个时候CPU中寄存器的值保存在父进程的内核栈中。这里就是使用父进程内核栈中的值来更新子进程寄存器中的值。特别的,内核将子进程eax寄存器中的值强制赋值为0,这也就是为什么使用fork()时子进程返回值是0。而在do_fork函数中则返回的是子进程的pid,这一点在上述内容中我们已经有所分析。另外,子进程的对应的thread_info结构中的esp字段会被初始化为子进程内核栈的基址。
11. 调用sched_fork函数,使得子进程的进程状态为TASK_RUNNING。并禁止内核抢占。并且,为了不对其他进程的调度产生影响,此时子进程共享父进程的时间片。
12. 根据clone_flags的值继续更新子进程的某些属性。
13. 将 nr_threads加一,表明新进程已经被加入到进程集合中。将total_forks加一,以记录被创建进程数量。
nr_threads++; total_forks++;
14. 如果上述过程中某一步出现了错误,则通过goto语句跳到相应的错误代码处;如果成功执行完毕,则返回子进程的描述符p。
至此,copy_proces()的大致执行过程分析完毕。
do_fork()执行完毕后,虽然子进程处于可运行状态,但是它并没有立刻运行。至于子进程合适执行这完全取决于调度程序,也就是schedule(),本文并不涉及涉及此函数的分析。