fork调用的内核实现

  进程和线程是我们平时接触的比较多的两个概念,特别是线程机制,很多语言原生就支持它。前段时间主要演示了下linux下进程和线程的创建,这篇文章对其创建的过程做一个简单的分析,错误之处,还请您斧正。

  在linux下,线程其实就是一个轻量级的进程,所以其实现都是通过调用给do_fork函数传入不同的参数实现的。先来看下这几个函数:

1 int sys_fork(struct pt_regs *regs)
2 {
3          return do_fork(SIGCHLD, regs->sp, regs, 0, NULL, NULL);
4 }


1 int sys_vfork(struct pt_regs *regs)
2 {
3         return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs->sp, regs, 0,
4                        NULL, NULL);
5 }  

 

1 sys_clone(unsigned long clone_flags, unsigned long newsp,
2           void __user *parent_tid, void __user *child_tid, struct pt_regs *regs)
3 {
4         if (!newsp)
5                 newsp = regs->sp;
6         return do_fork(clone_flags, newsp, regs, 0, parent_tid, child_tid);
7 }


   上面的代码中,并没有看到fork()函数的实现,其实fork函数的执行过程大致像这样:普通程序调用fork()-->库函数fork()-->系统调用(fork功能号)-->由功能号在 sys_call_table[]中寻到sys_fork()函数地址-->调用sys_fork(),这就完成拉从用户态到内核态的变化过程。所以,实际上,fork函数对应的实现就是sys_fork。

  和上面的过程类似,上面的几个函数分别对应与fork,vfork和clone,可以看到,其实际上都是通过一个调用do_fork函数实现的,不同处只是其传入的参数不同。 首先来看看传入的参数的,先看看这些传入的参数分别代表的含义:

cloning flags
/*
* cloning flags:
*/
#define CSIGNAL             0x000000ff      /* signal mask to be sent at exit */
#define CLONE_VM            0x00000100      /* set if VM shared between processes */
#define CLONE_FS           0x00000200      /* set if fs info shared between processes */
#define CLONE_FILES       0x00000400      /* set if open files shared between processes */
#define CLONE_SIGHAND  0x00000800      /* set if signal handlers and blocked signals shared */
#define CLONE_PTRACE      0x00002000      /* set if we want to let tracing continue on the child too */
#define CLONE_VFORK       0x00004000      /* set if the parent wants the child to wake it up on mm_release */
#define CLONE_PARENT     0x00008000      /* set if we want to have the same parent as the cloner */
#define CLONE_THREAD     0x00010000      /* Same thread group? */
#define CLONE_NEWNS     0x00020000      /* New namespace group? */
#define CLONE_SYSVSEM  0x00040000      /* share system V SEM_UNDO semantics */
#define CLONE_SETTLS       0x00080000      /* create a new TLS for the child */
#define CLONE_PARENT_SETTID      0x00100000      /* set the TID in the parent */
#define CLONE_CHILD_CLEARTID    0x00200000      /* clear the TID in the child */
#define CLONE_DETACHED              0x00400000      /* Unused, ignored */
#define CLONE_UNTRACED             0x00800000      /* set if the tracing process can't force CLONE_PTRACE on this clone */
#define CLONE_CHILD_SETTID        0x01000000      /* set the TID in the child */
#define CLONE_STOPPED             0x02000000      /* Start in stopped state */
#define CLONE_NEWUTS                0x04000000      /* New utsname group? */
#define CLONE_NEWIPC                 0x08000000      /* New ipcs */
#define CLONE_NEWUSER              0x10000000      /* New user namespace */
#define CLONE_NEWPID                 0x20000000      /* New pid namespace */
#define CLONE_NEWNET                0x40000000      /* New network namespace */
#define CLONE_IO                          0x80000000      /* Clone io context */


  然后来看看do_fork的具体过程:

  1.  p = copy_process(clone_flags, stack_start, regs, stack_size,
    child_tidptr, NULL, trace);
  2.   wake_up_new_task(p, clone_flags);
  

  第一步是调用copy_process函数来复制一个进程,并对相应的标志位等进行设置,接下来,如果copy_process调用成功的话,那么系统会有意让新开辟的进程运行,这是因为子进程一般都会马上调用exec()函数来执行其他的任务,这样就可以避免写是复制造成的开销,或者从另一个角度说,如果其首先执行父进程,而父进程在执行的过程中,可能会向地址空间中写入数据,那么这个时候,系统就会为子进程拷贝父进程原来的数据,而当子进程调用的时候,其紧接着执行拉exec()操作,那么此时,系统又会为子进程拷贝新的数据,这样的话,相比优先执行子程序,就进行了一次“多余”的拷贝。

   

  从上面的分析中可以看出,do_fork()的实现,主要是靠copy_process()完成的,这就是一环套一环,所以在看内核的时候,会觉得一下子跳到这,一下子又跳到那,一下子就看晕了的一个很大的原因。不过我觉得这也是linux的一大好处,因为其提高了函数的可重用行,比如本文一开始提到的几个函数的实现,归根到底,都是通过do_fork()实现的。

 

  接着再来看看copy_process()的实现:

  1. p = dup_task_struct(current); 为新进程创建一个内核栈、thread_iofo和task_struct,这里完全copy父进程的内容,所以到目前为止,父进程和子进程是没有任何区别的。
  2. 检查所有的进程数目是否已经超出了系统规定的最大进程数,如果没有的话,那么就开始设置进程描诉符中的初始值,从这开始,父进程和子进程就开始区别开了。
  3. 设置子进程的状态为不可被TASK_UNINTERRUPTIBLE,从而保证这个进程现在不能被投入运行,因为还有很多的标志位、数据等没有被设置。
  4. 复制标志位(falgs成员)以及权限位(PE_SUPERPRIV)和其他的一些标志。
  5. 调用get_pid()给子进程获取一个有效的并且是唯一的进程标识符PID。
  6. 根据传入的cloning flags(具体表示上面有)对相应的内容进行copy。比如说打开的文件符号、信号等。
  7. 父子进程平分父进程剩余的时间片。
  8. return p;返回一个指向子进程的指针。


  至此,do_fork的工作就基本结束了。

 

 


 

 

 

posted @ 2010-05-21 12:08  MR_H  阅读(5200)  评论(3编辑  收藏  举报