进程的创建

2015-6-17 chenhui 进程管理

Linux内核提供了三个用于创建进程的系统调用,他们分别是:fork()、clone()、vfork()。那么他们创建的进程有什么区别呢?我们可以先来看看这三个函数的定义。


fork()系统调用在内核中对应的函数为sys_fork()。



clone()系统调用在内核中对应的函数为sys_clone()。




vfork()系统调用在内核中对应的函数为sys_vfork()。



我们发现,他们三者都是通过do_fork()这个函数来执行真正的操作的,只是传递的参数不同,所以说如果我们想弄清楚他们之间的区别,就必须搞清楚do_fork()的参数的含义。



clone_flags :由两部分组成:低8位是一个信号,他指定了新进程死亡时向父进程发送的信号,对于fork而言,他就是SIGCHLD;剩余部分则是创建子进程的一些标志,他的作用非常多,其中最为重要的作用就是描述了父进程需要共享给子进程的资源。

regs :从用户态切换到内核态时被保存到内核态堆栈中

parent_tidptr :父进程的用户态的变量地址

child_tidptr :子进程的用户态变量地址


我们可以发现,当clone()系统调用被调用时,他传递给do_fork()的值几乎全是由用户态传递给他本身的,这说明clone()创建子进程时的自由度最高,当我们需要完全按照自己的需求创建一个子进程时,就可以调用clone()系统调用。


fork()和vfork()的区别则在于第一个clone_flags参数,fork()除了SIGCHLD之外没有传入任何参数,这意味着通过fork()创建的子进程会把父进程的资源全部复制过来(在子进程不共享某些资源时,他就会为自己复制一份资源)。那么 SIGCHLD 又是什么意思呢?他的意思是进程死亡时,通知父进程,并最后由父进程释放进程。


然后我们再来看vfork(),他传入了CLONE_VFORK和CLONE_VM,后者表示他共享父进程的地址空间,而前者则会禁止父进程使用这个共享的地址空间(通过阻塞父进程实现),直到子进程死亡或者他换了一个地址空间(比如说他载入了可执行文件,那么他就会换一个新的地址空间),由此我们就可以知道,当我们需要执行一个新的可执行文件时,那么最好使用vfork()系统调用,因为他可以省去复制地址空间所花费的很多时间。


下面我们开始分析do_fork()函数,这个函数比较长,我们分段进行分析。


先来创建流程:

  1. 申请 PID
  2. 创建进程
  3. 如果是 vfork,则阻塞父进程
  4. 把新进程插入父进程所在的运行队列,这样他就能被调度到了

第二步的详细分解:

  1. 申请新进程的进程描述符,这个进程描述符和父进程描述符相同
  2. 根据 do_fork 时的参数,复制或共享父进程的资源
  3. 把新进程的 PC 值设为 ret_from_fork 子程序的地址, 这样他被调度时就会执行他
  4. 一大堆初始化




369~375行,为新进程分配一个新的pid。

384行,创建子进程并根据 clone_flags 参数复制或共享父进程的部分资源。如果执行成功,则p指向子进程的进程描述符。




394行的vfork变量很重要,我们之前说过:当使用vfork()创建子进程时,子进程会通过阻塞父进程而达到独占地址空间的目的。我们要阻塞父进程,自然要使用一种同步机制来实现,这里就是使用completion来实现的。


396~399行,如果我们真的需要阻塞父进程,那么就初始化vfork并让子进程的进程描述符的vfork_done字段指向他。这样当子进程不再使用这个地址空间时,他就会直接通过vfork_done来唤醒被阻塞的父进程。


407~410行,如果需要新创建的子进程进入暂停状态,那么就让子进程的运行状态设为暂停状态;否则就调用wake_up_new_task()把子进程插入父进程所在的运行队列,这样子进程就能被调度执行了。


412~415行,如果父进程(当前进程)被跟踪了,那么先把子进程的pid存入父进程(当前进程)的进程描述符的ptrace_message字段。然后调用ptrace_notify()向当前进程的父进程发送SIGCHLD信号。这里要注意:进程被跟踪后,他的父进程就变成了跟踪他的进程,所以这里实际上是向跟踪者进程发送一个SIGCHLD信号。那么这里为什么要发送给跟踪者呢?很简单,就是因为他跟踪了当前进程。这里就是为了告诉他我已经创建了一个子进程,然后跟踪者就可以通过ptrace_message字段得到新进程的pid并进行下一步处理了。


417~425行,如果需要阻塞父进程,那么在这里就要开始阻塞他了。


418行,让当前进程(就是父进程)得到vfork,由于vfork已经在上面被初始化为0了(非空闲),所以当前进程就会在这里被阻塞


426~429行,到了这里就说明创建进程失败,释放pid并准备返回一个错误值。


do_fork()调用copy_process()来执行真正的创建进程操作。这个函数的代码非常的长,我们分段来讲解。




这段代码主要检查一些标志上的一些兼容问题。


972~973行,CLONE_NEWNS表示子进程需要自己的命名空间(见第八章);CLONE_FS代表子进程共享父进程的根目录和当前工作目录。这两个标志不能兼容。


975~976行,CLONE_THREAD表示子进程和父进程属于同一个线程组,此时他们必须共享信号;CLONE_SIGHAND未被设置时说明子进程不和父进程共享信号。这两个标志必须同时被设置,否则报错退出。


978~979行,如果子进程共享父进程的信号(CLONE_SIGHAND),那么必须设置CLONE_VM以共享父进程的地址空间(即子进程是父进程的一个线程)。否则报错退出。




这段代码为子进程申请了一个新的进程描述符


989~991行为子进程分配一个新的进程描述符,如果分配失败,则报错退出。如果这里的dup_task_struct()分配进程描述符成功,那么他会和当前进程(父进程)的进程描述符完全相同。




这段代码检查子进程所属的用户(也就是当前进程的用户)下的进程数是否超出限制。如果没超过或有root权限,那么递增相关计数。




018行检查系统内的进程数是否已经超过了最大数量


024行,如果子进程已经有了对应的可执行文件(别忘了,子进程的进程描述符是复制父进程也就是当前进程的),那么就递增该文件的引用计数。实际上这里就是递增他的父进程的可执行文件的引用计数,因为子进程也使用他了嘛!


27行把子进程的did_exec字段清零,表示他没有执行过execve系统调用。


30行初始化子进程的pid。


36~37行初始化子进程的兄弟进程链表和子进程链表。



41~42行清除掉子进程从父进程中复制过来的所有信号。因为子进程不会继承父进程的信号。


这段代码执行完之后,还有大约几十行用于初始化子进程的进程描述符的代码,这些初始化几乎全是清零一些字段,为节省篇幅就不对他进行讲解,我们直接越过他。




这段代码是重中之重,之前的代码不过是在做一些检查和一些简单的初始化罢了。这段代码执行完之后,子进程才算是真正开始独立了。


111~113行,如果clone_flags里没有CLONE_THREAD标记,那就说明子进程使用单独的线程组;否则使用父进程的线程组。


120~121行,如果设置了CLONE_SYSVSEM标志,那么就让子进程共享父进程的信号量集;否则子进程不使用信号量集。


122~123行,如果设置了CLONE_FILES标志,那么就让子进程共享父进程的files_struct对象,父进程把所有已打开的文件的信息都存放在这个对象里(比如用open()打开的文件);否则为子进程复制一份父进程打开的文件信息。_


124~125行,如果设置了CLONE_FS标志,那么就让子进程共享父进程的文件系统信息;否则就让子进程复制一份父进程的文件系统信息。


126~129行,如果设置了CLONE_SIGHAND和CLONE_THREAD,那么就让子进程共享父进程的信号处理器;否则就让子进程复制一份父进程的信号处理器。


128~129行,如果设置了CLONE_THREAD,就说明这是一个线程,那么让子进程共享父进程的信号描述符(struct signal_struct);否则就为子进程申请一个信号描述符,并把子进程加入父进程的进程组和会话。


130~131行,如果设置了CLONE_VM,就让子进程共享父进程的地址空间(内存描述符);否则就为子进程复制一份父进程的地址空间。


137~139行,复制内核栈。他会把子进程的进程上下文的pc字段设为ret_from_fork的地址,这样当子进程被调度时,他就会开始执行ret_from_fork()。




154行,清除TIF_SYSCALL_TRACE标志。这是因为子进程开始执行时,是执行的ret_from_fork函数(见137行的copy_thread()),而这个函数实际上是fork系统调用从内核空间返回时调用的函数,他会把系统调用结束的消息通知给调试进程,但是子进程没有调用fork系统调用啊,是父进程调用的,所以这里要清除掉这个标志,这样ret_from_fork()函数就会知道,子进程没有调用fork系统调用。


161行,如果设置了CLONE_THREAD标志,那么就把exit_signal字段置为-1;否则就取出存放在clone_flags参数的低8位里的信号值。进程死亡时,他会把exit_signal这个字段上存储的信号发送给父进程,而线程是不发送的(准备的说,是不发送除必须发送的信号以外的信号),这里的CLONE_THREAD就代表着线程,所以要把他置为-1,-1就代表着不发送。


162~163行初始化进程退出时发送给子进程的信号和进程的退出状态。



179~182行,进程描述符的cpus_allocwed是个位图,他的某位被置一时表示进程可以使用编号和该位号相等的CPU(比如位0被置一,表示进程可以使用0号CPU)。实际上在创建子进程的进程描述符时,他已经把父进程的cpus_allocwed复制过来了,但是过了这么久,父进程的cpus_allocwed可能已经被改变了,所以在179重新获取。然后在180行先判断之前为子进程分配的cpu是否已经在cpus_allocwed位图里被设置,如果未被设置,那么就在181行再看看子进程所在的cpu是否已被正在被系统使用,如果也没有,那么他就需要换个CPU,于是在182行把子进程使用的cpu设为当前cpu。


184~188行,real_parent和parent多数情况下是相同的,除非有特殊情况出现(比如被跟踪)。




193行,如果当前进程有信号需要等待,那么就设置TIF_SIGPENDING标记;否则清除TIF_SIGPENDING标记。他会在调度时处理。





222行,把新进程加入其父进程的子进程队列。

233行,把新进程插入进程队列(所有的进程都在这个队列上)。

236~237行,把新进程以PID类型加入pidhash散列表并递增系统内进程数。


如果copy_process()创建成功,那么到这里就结束了,但是如果在途中遇到一些错误,那么他就会来到下面的错误处理程序。




在之前的代码处理新进程时,遇到哪个问题就会跑到哪个标号并执行相应的函数来对错误进行处理,最后返回一个错误值。


发表评论:

Copyright ©2015-2016 freehui All rights reserved