分类: LINUX
2015-04-09 23:16:05
fork()系统调用在内核中对应的函数为sys_fork()。
clone()系统调用在内核中对应的函数为sys_clone()。
regs :从用户态切换到内核态时被保存到内核态堆栈中
stack_size :未使用,总是为0
parent_tidptr :父进程的用户态的变量地址
child_tidptr :子进程的用户态变量地址
我们可以发现,当clone()系统调用被调用时,他传递给do_fork()的值几乎全是由用户态传递给他本身的,这说明clone()创建子进程时的自由度最高,当我们需要完全按照自己的需求创建一个子进程时,就可以调用clone()系统调用。而fork()和vfork()的区别则在于第一个clone_flags参数,fork()除了SIGCHLD之外没有传入任何参数,这意味着通过fork()创建的子进程会把父进程的资源全部复制过来(在子进程不共享某些资源时,他就会为自己复制一份资源)。然后我们再来看vfork(),他传入了CLONE_VFORK和CLONE_VM,后者表示他共享父进程的地址空间,而前者则会禁止父进程使用这个共享的地址空间(通过阻塞父进程实现),直到子进程死亡或者他换了一个地址空间(比如说他载入了可执行文件,那么他就会换一个新的地址空间),由此我们就可以知道,当我们需要执行一个新的可执行文件时,那么最好使用vfork()系统调用,因为他可以省去复制地址空间所花费的很多时间。
下面我们开始分析do_fork()函数,这个函数比较长,我们分段进行分析。
384行,创建子进程并根据clone_flags参数复制或共享父进程的部分资源。如果执行成功,则p指向子进程的进程描述符。
394行的vfork变量很重要,我们之前说过:当使用vfork()创建子进程时,子进程会通过阻塞父进程而达到独占地址空间的目的。我们要阻塞父进程,自然要使用一种同步机制来实现,这里就是使用completion来实现的。
396~399行,如果我们真的需要阻塞父进程,那么就初始化vfork并让子进程的进程描述符的vfork_done字段指向他。这样当子进程不再使用这个地址空间时,他就会直接通过vfork_done来唤醒被阻塞的父进程。
401~405行,如果需要新创建的子进程进入暂停状态或者他被跟踪,那么他必须被暂停,所以这里给子进程设置一个暂停信号,并为子进程设置TIF_SIGPENDING标志来告诉他有信号出现了,这样子进程就会来处理这个暂停信号了。
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以共享父进程的地址空间(即子进程是父进程的一个线程)。否则报错退出。
这段代码为子进程申请了一个新的进程描述符
983~985行做一些安全检查,我们这里忽略掉他。
989~991行为子进程分配一个新的进程描述符,如果分配失败,则报错退出。如果这里的dup_task_struct()分配进程描述符成功,那么他会和当前进程(父进程)的进程描述符完全相同。
003~09行检查子进程所属的用户下的进程数是否已经超过了限制数,如果超过了,则进入if代码段并在006行检查当前进程是否有root权限。如果没有,那么报错退出。
011行,递增子进程所属的用户的引用计数(多了个进程引用他)。
012行,递增子进程所属的用户下的进程计数。
013行,递增子进程所属进程组的计数。
021行递增子进程的执行域引用计数,这个执行域是用来兼容其他操作系统的应用程序的一种机制。
024行,如果子进程已经有了对应的可执行文件(别忘了,子进程的进程描述符是复制父进程也就是当前进程的),那么就递增该文件的引用计数。实际上这里就是递增他的父进程的可执行文件的引用计数,因为子进程也使用他了嘛!
27行把子进程的did_exec字段清零,表示他没有执行过execve系统调用。
29行清除子进程的PF_SUPERPRIV标记,设置子进程的PF_FORKNOEXEC。前者说明他没有使用root权限;后者说明子进程没有调用execve()系统调用。
30行初始化子进程的pid。
32行,如果CLONE_PARENT_SETTID标志被设置,就将子进程的pid复制到参数parent_tidptr指向的用户态变量中。
36~37行初始化子进程的兄弟进程链表和子进程链表。
41~42行清除掉子进程从父进程中复制过来的所有信号。因为子进程不会继承父进程的信号。
这段代码执行完之后,还有大约几十行用于初始化子进程的进程描述符的代码,这些初始化几乎全是清零一些字段,为节省篇幅就不对他进行讲解,我们直接越过他。
111~113行,如果clone_flags里没有CLONE_THREAD标记,那就说明子进程使用单独的线程组;否则使用父进程的线程组。
115~116行,安全检查。
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()。
142行,如果设置了CLONE_CHILD_SETTID,就把child_tidptr参数拷贝到set_child_tid字段
143行,如果设置了CLONE_CHILD_CLEARTID,就把child_tidptr参数拷贝到clear_child_tid字段
151行,如果设置了CLONE_VM标记但没有设置CLONE_VFORK标志,就把信号处理程序备用堆栈和其堆栈大小初始化为0。(见第七章)
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行初始化进程退出时发送给子进程的信号和进程的退出状态。
165行,暂时把自身设置为线程组组长(暂时把自己看成普通进程),若已设置CLONE_THREAD标记(即他是一个线程),下面会重新设置线程组组长。
166行,同一线程组的所有进程(或线程)都会通过thread_group字段挂入到线程组领头进程的group_leader链表,这里初始化这个字段。
167~168行,当进程跟踪一个另一个进程时,被跟踪进程就会通过ptrace_list字段插入到跟踪进程的ptrace_children链表。这里初始化这两个字段。
170行,初始化子进程的调度信息。
177行,继承父进程的IO优先级。
179~182行,进程描述符的cpus_allocwed是个位图,他的某位被置一时表示进程可以使用编号和该位号相等的CPU(比如位0被置一,表示进程可以使用0号CPU)。实际上在创建子进程的进程描述符时,他已经把父进程的cpus_allocwed复制过来了,但是过了这么久,父进程的cpus_allocwed肯呢过已经被改变了,所以在179重新获取。然后在180行先判断之前为子进程分配的cpu是否已经在cpus_allocwed位图里被设置,如果未被设置,那么就在181行再看看子进程所在的cpu是否已被正在被系统使用,如果也没有,那么他就需要换个CPU,于是在182行把子进程使用的cpu设为当前cpu。
184~187行,如果设置了CLONE_PARENT(把子进程的父进程设为创建他的进程的父进程)和CLONE_THREAD,那么就把子进程的父进程设为创建他的进程的父进程;否则就把子进程的父进程设为当前进程。
188行,real_parent和parent多数情况下是相同的,除非有特殊情况出现(比如被跟踪),所以这里让他们相等。
193行,如果当前进程有信号需要等待,那么就设置TIF_SIGPENDING标记;否则清除TIF_SIGPENDING标记。
195~200行,检查是否存在TIF_SIGPENDING标记(193行设置),如果存在就说明确实有信号存在,如果有信号存在,则退出。
202行,如果新进程是一个线程,则进入if代码段。
203~204行,新线程所属的线程组就是创建他的那个进程的线程组,所以这里让新线程的group_leader指向当前进程的group_leader (该字段指向线程组领头进程的进程描述符)并把新进程插入线程组链表。
222行,把新进程加入其父进程的子进程队列。
226行,如果子进程是线程组的领头进程(即不是线程),则进入if代码段。
227行,把终端控制台相关的信息拷贝到新进程。
228行,把新进程所属的进程组的ID设为当前进程的进程组的ID。
229行,把新进程所属的会话的ID设为当前进程的会话ID。
230~231行,把新进程插入其所属的进程组和会话的进程队列,这样他就正式成为进程组和会话的一员了。
233行,把新进程插入进程队列(所有的进程都在这个队列上)。
234行,递增CPU上运行的程序数
236~237行,把新进程以PID类型加入pidhash散列表并递增系统内进程数。
240~244,递增fork次数并在设置一个proc事件,最后在244行返回创建的进程的进程描述符。
如果copy_process()创建成功,那么到这里就结束了,但是如果在途中遇到一些错误,那么他就会来到下面的错误处理程序。
在之前的代码处理新进程时,遇到哪个问题就会跑到哪个标号并执行相应的函数来对错误进行处理,最后返回一个错误值。