在执行系统调用如fork/vfork/clone或者启动内核线程kernel_thread均会调用内核的_do_fork函数。下面具体来看下该函数做的具体内容(基于linux-4.19)。
/*clone_flags: 标志集合,用来制定控制复制过程中的一些属性,最低字节CSIGNAL指定了
在子进程终止时被发送给父进程的信号掩码;
stack_start:用户状态下栈的起始地址
stack_size: 用户状态下栈的大小
parent_tidptr,child_tidptr分别指向了用户空间中父子进程的PID的两个指针;
*/
long _do_fork(unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr,
unsigned long tls)
特别的: fork系统调用使用的clone_flags参数是 SIGCHLD(arm体系定义值为17),由上面可知满足最低字节, 表示子进程终止时将会发送 SIGCHLD 信号给父进程。
SYSCALL_DEFINE0(fork)
{
#ifdef CONFIG_MMU
return _do_fork(SIGCHLD, 0, 0, NULL, NULL, 0);
#else
/* can not support in nommu mode */
return -EINVAL;
#endif
}
另fork采用了写时复制技术,起初父子进程栈地址相同,若存在操作栈地址并写入数据,才会创建新的栈副本。
_do_fork流程图
_do_fork()
|
|--->copy_process 生成新进程的实际工作
|
|--->get_task_pid & pid_vnr 获取PID
|
|--->若存在CLONE_VFORK标志,init_completion初始化vfork完成处理函数
|
|--->wake_up_new_task 唤醒新创建的子进程
|
|--->若存在CLONE_VFORK标志,wait_for_vfork_done 等待vfork完成
复制进程copy_process
copy_process
|
|--->检查标志
|
|--->挂起信号,并让接收到的信号延迟到fork后发送
|
|--->dup_task_struct(建立副本,复制父进程的task_struct和thread_info实例内容)
|
|--->检查资源限制
|
|--->初始化新创建的进程task_struct
|
|--->sched_fork
|
|--->复制/共享进程的各个部分
|
|---> copy_semundo,copy_files,copy_fs,copy_sighand,copy_signal,
copy_mm,copy_namespaces,copy_io,copy_thread_tls
检查标志
//不允许与不同名称空间中的进程共享根目录
if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS))
return ERR_PTR(-EINVAL);
//在用CLONE_THREAD创建一个线程时,必须用CLONE_SIGHAND激活信号共享;
if ((clone_flags & CLONE_THREAD) && !(clone_flags & CLONE_SIGHAND))
return ERR_PTR(-EINVAL);
//只有在父子进程间共享虚拟地址空间时(CLONE_VM),才能提供共享的信号处理程序(CLONE_SIGHAND)
if ((clone_flags & CLONE_SIGHAND) && !(clone_flags & CLONE_VM))
return ERR_PTR(-EINVAL);
//全局init的兄弟在退出时仍然是僵尸,因为它们是没有被他们的parent回收。
//要解决这个问题并避免多根进程树,请防止全局初始化和容器初始化创建兄弟进程。
if ((clone_flags & CLONE_PARENT) &&
current->signal->flags & SIGNAL_UNKILLABLE)
return ERR_PTR(-EINVAL);
//如果新进程将在不同的pid或用户名称空间中,则不允许它与分支任务共享线程组。
if (clone_flags & CLONE_THREAD) {
if ((clone_flags & (CLONE_NEWUSER | CLONE_NEWPID)) ||
(task_active_pid_ns(current) !=
current->nsproxy->pid_ns_for_children))
return ERR_PTR(-EINVAL);
}
dup_task_struct
arch_dup_task_struct,setup_thread_stack复制父进程副本
资源检查
if (atomic_read(&p->real_cred->user->processes) >=
task_rlimit(p, RLIMIT_NPROC)) {
if (p->real_cred->user != INIT_USER &&
!capable(CAP_SYS_RESOURCE) && !capable(CAP_SYS_ADMIN))
goto bad_fork_free;
}
拥有当前进程的用户,其资源计数器保存在user_struct中,特定用户当前持有进程的数目保存在user_struct->processes中;除非当前用户是root用户或分配了CAP_SYS_RESOURCE/CAP_SYS_ADMIN特殊权限,否则放弃改进程的创建;
sched_fork
执行与调度程序相关的设置。将此任务分配给CPU。
copy_xxx
- 若CLONE_SYSVSEM置位,则使用copy_semundo使用父进程的System V信号量。
- 若CLONE_FILES置位,则copy_files使用父进程的文件描述符;否则创建新的files结构副本,其中包含的信息与父进程相同;
- 若CLONE_FS置位,则copy_fs使用父进程的文件系统上下文。
- 若CLONE_SIGHAND置位,则copy_sighand使用父进程的信号处理程序。
- 若CLONE_THREAD置位,则copy_signal与父进程共同使用信号处理中不特定于处理程序的部分。
- 若CLONE_VM置位,则copy_mm让父子进程共享同一地址空间;若CLONE_VM没有置位,并不意味着需要复制父进程的整个地址空间,内核会创建页表的一份副本,通过COW机制进程写时复制;
- 若CLONE_NEWxyz没有置位,则与父进程共享相应的命名空间,否则创建一个新的;
- 若CLONE_IO置位,与父对象共享io上下文;
内核线程
内核线程是直接有内核本身启动的进程;内核线程实际上是将内核函数委托为独立的进程,与系统中其他进程并行执行(内核线程也称为内核守护进程);基本上有两种类型的内核线程:
- 线程启动后一直等待,知道内核请求线程执行某一特定的操作;
- 线程启动后按周期性间隔运行,检测特定资源的使用,在用量超出或低于预置的限制值时采取行动。内核使用这类线程用于连续检测任务;
pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)