Linux进程创建三——fork、vfork、clone、kernel_thread

前言

Linux创建线程的API主要有fork、vfork、clone、kernel_thread,最终都调用了do_fork。
do_fork的具体流程在上一篇已经分析完毕Linux进程创建二——do_fork

fork、vfork、clone都是系统调用,用来实现用户空间的进程创建。
内核空间创建的进程称为内核线程,主要通过kernel_thread,对kernel_thread进行包装的API有create_kthread,以及对create_kthread封装的kthread_run等等。

fork

系统调用fork的实现如下:

1658 #ifdef __ARCH_WANT_SYS_FORK
1659 SYSCALL_DEFINE0(fork)
1660 {
1661 #ifdef CONFIG_MMU
1662         return do_fork(SIGCHLD, 0, 0, NULL, NULL);
1663 #else
1664         /* can not support in nommu mode */
1665         return -EINVAL;
1666 #endif
1667 }
1668 #endif

参数只有一个SIGCHLD作为clone flag,要求在子进程创建时必须注册SIGCHLD信号。当子进程退出时,系统会给父进程发送SIGCHLD信号,对其进行处理,避免子进程变成僵尸进程。关于僵尸进程的进一步分析见附录。
fork系统调用是创建进程最直接最傻瓜的方式,子进程与父进程没有共享任何进程信息。这样实现的结果是创建一个子进程会带来比较大的开销,影响系统的performance和浪费资源,kernel为了改善这一点提供了copy on write写时复制机制,在另一篇文章中对该机制进行了分析(TODO)。

vfork

1670 #ifdef __ARCH_WANT_SYS_VFORK
1671 SYSCALL_DEFINE0(vfork)
1672 {
1673         return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0,
1674                         0, NULL, NULL);
1675 }
1676 #endif

vfork有两个特点:

  • CLONE_VM 使子进程与父进程共享进程描述符,即具有相同进程地址空间和stack。带来的性能提升是不需要完全拷贝父进程的某些内容,比如页表栈等等,节省了资源和时间。
  • CLONE_VFORK 使子进程创建以后优先于父进程运行(父进程此时被阻塞睡眠),直到子进程终结(terminate)或发出一个fatal信号或调用execve。

有人认为vfork的实现是一种架构缺陷,4.2BSD man page指出:当合适的系统共享机制被实现的时候,该系统调用将被移除。
尽管硬件的发展已将fork和vfork的性能差异减小,Linux和其他一些系统仍然保留了vfork,主要有以下原因:

  • 一些应用对性能要求比较高,需要vfork带来的性能提升
  • vfork可以实现在没有MMU的系统上,fork不行(涉及到进程地址空间,物理地址和虚拟地址的转换)

clone

1678 #ifdef __ARCH_WANT_SYS_CLONE
1680 SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
1681                  int __user *, parent_tidptr,
1682                  int, tls_val,
1683                  int __user *, child_tidptr)

1701 {
1702         return do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr);
1703 }
1704 #endif

相比于fork、vfork,clone在参数上多了newsp、parent_tidptr、child_tidptr,

  • newsp,在do_fork–>copy_thread中可以设置子进程的childregs->sp为指定的sp
  • parent_tidptr,当设置了CLONE_PARENT_SETTID,会将子进程的pid传入到用户空间变量parent_tidptr中
  • child_tidptr,当设置了CLONE_CHILD_SETTID/CLONE_CHILD_CLEARTID,会将子进程的set_child_tid/clear_child_tid设置为child_tidptr

clone在设置clone flag方面更有自主性和选择性,用户可以更加灵活的选择共享父进程的哪些内容。
我的理解中,clone主要针对轻量级进程,为用户线程提供了良好的支持。

kernel_thread

1649 /*
1650  * Create a kernel thread.
1651  */
1652 pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)
1653 {
1654         return do_fork(flags|CLONE_VM|CLONE_UNTRACED, (unsigned long)fn,
1655                 (unsigned long)arg, NULL, NULL);
1656 }

内核线程因为永远运行在内核态,所以不需要处理用户态上下文(即不存在从用户态到内核态的切换)。又因为所有进程的内核态页表都是相同的,所以内核线程在切换的时候不需要重新建立虚拟地址和物理地址的映射关系,直接共享切换之前的进程的页表即可。
这样一来,用户态进程的开销就大大减小了(只有切换硬件上下文和栈的开销)。
kernel中将一些后台的、周期性的工作交付给内核线程完成,常见的如keventd、kswapd(内存回收)、ksoftirqd(软中断处理)等。
所以设置了 CLONE_VM flag,内核开发人员在创建内核线程是还可以根据需要加上其他的flag。

附录

僵尸进程

引用网上的解释:
僵尸进程实质上已经结束了的进程,不再占有任何内存空间,没有任何可执行代码,也不能被调度,仅在进程列表中保留一个位置,太多僵尸进程会占满进程表,导致系统崩溃。

子进程的回收

存在以下几种情况:

  1. 父进程调用了wait(阻塞)或waitpid(非阻塞),来捕获子进程退出时的SIGCHLD信号,然后回收子进程。
  2. 父进程在子进程运行过程中先死掉,此时子进程由init进程收养,运行结束后由init进程回收
  3. 默认状态下父进程忽略子进程的SIGCHLD(即没有wait、waitpid),此时子进程变成僵尸进程

你可能感兴趣的:(kernel进程)