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的实现如下:
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)。
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有两个特点:
有人认为vfork的实现是一种架构缺陷,4.2BSD man page指出:当合适的系统共享机制被实现的时候,该系统调用将被移除。
尽管硬件的发展已将fork和vfork的性能差异减小,Linux和其他一些系统仍然保留了vfork,主要有以下原因:
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,
clone在设置clone flag方面更有自主性和选择性,用户可以更加灵活的选择共享父进程的哪些内容。
我的理解中,clone主要针对轻量级进程,为用户线程提供了良好的支持。
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。
引用网上的解释:
僵尸进程实质上已经结束了的进程,不再占有任何内存空间,没有任何可执行代码,也不能被调度,仅在进程列表中保留一个位置,太多僵尸进程会占满进程表,导致系统崩溃。
存在以下几种情况: