do_fork()函数中主要的处理流程如下所示(引自《深入Linux内核架构》):
我们关注的是用户进程的创建,所以这里只关注copy_process()和wake_up_new_task()这两个函数。
copy_process()函数有7个参数,其中我们需要关心的有clone_flags、stack_start和regs。cone_flags是一个标志集合,分为两部分:最低的字节指定了在子进程终止时发送给父进程的信号,其余的高位字节保存了各种真正的复制标志,如CLONE_FS、CLONE_THREAD等。在用户层调用fork()时不能指定标志,所以默认的CLONE_FLAGS的值为SIGCHLD。如果你想要修改默认的创建进程的方式,或者修改子进程退出时的信号,可以使用clone()系统调用(和fork不同,具体参见man clone)。stack_start是父进程(也就是current)的用户栈的起始地址。regs是一个指向寄存器集合的指针,该参数使用的数据类型是特定于体系结构的struct pt_regs。
现在我们来看copy_process()是创建子进程的。
1、标志检查
copy_process()首先会检查clone_flags中指定的标志是否冲突以及安全检查,创建用户进程时只有SIGCHLD,所以这个检查是肯定没有问题的。
2、dup_task_struct()
我们知道Linux内核中使用task_struct结构来表示进程,子进程的描述符结构是在du_task_struct()中创建的,其源码实现如下:
static struct task_struct *dup_task_struct(struct task_struct *orig) { struct task_struct *tsk; struct thread_info *ti; unsigned long *stackend; int err; prepare_to_copy(orig); tsk = alloc_task_struct(); if (!tsk) return NULL; ti = alloc_thread_info(tsk); if (!ti) { free_task_struct(tsk); return NULL; } err = arch_dup_task_struct(tsk, orig); if (err) goto out; tsk->stack = ti; ...... setup_thread_stack(tsk, orig); stackend = end_of_stack(tsk); *stackend = STACK_END_MAGIC; /* for overflow detection */ ...... return tsk; ...... }prepare_to_copy()会调用unlazy_fpu(),它把FPU、MMX和SSE/SSE2寄存器的内容保存到父进程的thread_info结构实例中。如果父进程没有使用这些扩展寄存器的话,就不用保存。保存这些寄存器会用到xsave指令或fxsave指令,如果你对这些寄存器的保存方式及过程感兴趣的话,可以查看intel手册中关于xsave指令和fxsave指令的介绍。
if (atomic_read(&p->real_cred->user->processes) >= p->signal->rlim[RLIMIT_NPROC].rlim_cur) { if (!capable(CAP_SYS_ADMIN) && !capable(CAP_SYS_RESOURCE) && p->real_cred->user != INIT_USER) goto bad_fork_free; } retval = copy_creds(p, clone_flags); if (retval < 0) goto bad_fork_free; /* * If multiple threads are within copy_process(), then this check * triggers too late. This doesn't hurt, the check is only there * to stop root fork bombs. */ retval = -EAGAIN; if (nr_threads >= max_threads) goto bad_fork_cleanup_count;第一个检查是系统对每个用户可以创建的进程数限制,默认是1024。这个限制可以通过ulimit()系统调用或ulimit命令修改。除了修改限制外,创建进程的用户如果是root用户或具有CAP_SYS_ADMIN或CAP_SYS_RESOURCE权限则不受这个限制。
if (!try_module_get(task_thread_info(p)->exec_domain->module)) goto bad_fork_cleanup_count;执行域这个概念也是第一次注意到。Linux有一个特性就是能执行其他操作系统上编译的可执行文件。当然内核运行的平台和可执行文件包含的机器代码对应的平台必须是相同的计算机体系结构。对这些非Linux下编译的可执行文件有两种执行方式:
5、子进程的初始化
在拷贝之前,会初始化子进程描述结构中的一系列成员,这些初始化比较简单,也不是我们关心的部分,所以大部分都略过,只关心一小部分。
进程的用户栈的起始地址存储在task_struct结构的stack_start成员中,在初始化的时候使用的是父进程的用户栈地址,所以在子进程创建时会和父进程使用相同的用户栈,如果有任何一方修改栈的话,会重新拷贝一份,这是基于COW技术,以避免无用的复制。
创建子进程为调度器类提供了调度进程的一个切入点,内核会调用sched_fork()函数,以便使调度器有机会对新进程进行设置。sched_fork()会初始化一些和调度相关的成员,并将进程设置为TASK_RUNNING状态。不过此时新的进程还没有放到CPU的执行队列中,所以新的进程不会被调度到。
6、资源拷贝
接下来是调用相关的函数来拷贝父进程的各个子系统的信息到子进程的相关成员中,包括文件系统信息、打开的文件、信号处理函数等。拷贝过程都比较类似,如果没有设置共享标志的话,则会给子进程分配新的资源,所以只以部分为例进行说明。
copy_semundo()是拷贝父进程的System V信号量。创建用户进程时,没有设置CLONE_SYSVSEM标志,因此只会简单地将tsk->sysvsem.undo_list置为NULL,其中tsk为子进程。这一步非常关键,必须执行,因为在arch_dup_task_struct()中将父进程的所有内容拷贝到子进程,所以如果不设置为NULL的话,tsk->sysvsem.undo_list仍会指向父进程的资源,而没有设置CLONE_SYSVSEM标志是不共享的,所以必须置为NULL。在后面拷贝其他子系统的资源过程中,如果不共享的话,也必须重新初始化子进程的相关成员,以避免误用资源。
copy_files()是拷贝父进程的打开文件描述符表。内核用files_struct结构来描述打开的文件描述符。如果没有设置CLONE_FILES标志的话,会调用dup_fd()函数给子进程创建新的打开文件描述符表,不过文件还是共享的,也就是说使用的是相同的file结构实例,只是增加了引用计数,这点非常重要。在进程中关闭文件时,会首先对文件描述符对应的file实例的引用计数减1,如果引用数为0时才真正释放文件。关闭文件描述符,只是说当前进程不再访问该文件了,但并不一定真正就关闭了文件。
copy_fs()是拷贝文件系统信息。如果没有设置CLONE_FS标志的话,会重新初始化子进程的文件系统信息。文件系统信息是使用fs_struct结构来描述,不过这个结构非常简单,只有创建新文件的权限(umask成员)和根路径、当前工作路径等信息。不过知道了当前的工作路径,就可以这个path结构来获取所属的文件系统信息,正如我们在用户层可以通过statvfs()系统调用指定一个文件来获取所属的文件系统信息。
在一系列的资源拷贝后,还会继续初始化cgroup、进程退出等相关成员的初始化。
子进程创建后,肯定要加入到CPU的执行队列中,这样才有可能被执行,这是调用wake_up_new_task()来实现的。这是调度器与进程创建的第二个逻辑交互时机,
内核会调用调度器类的task_new函数(sched_class结构中),将新进程加入到相应类的就绪队列。
至此,创建用户进程的过程就完成了。