作者:吴新武,华清远见嵌入式学院讲师。
Linux通过clone()系统调用实现fork()、vfork()和__clone()库函数创建新的进程,这个调用通过一系列的参数标志来指明父子进程的共享资源,最终将各自的参数标志位传递给clone,由clone()去调用do_fork()来实现创建新的进程的目的。
do_fork的实现源码在kernel/fork.c文件中,其主要的作用就是复制原来的进程成为另一个新的进程,它完成了整个进程的创建过程。do_fork()的实现主要由以下5个步骤,在分析代码之前,先了解以下do_fork()函数的参数的含义,其参数的含义如下。
clone_flags:该标志位的4个字节分为两部分。最低的一个字节为子进程结束时发送给父进程的信号代码,通常为SIGCHLD;剩余的三个字节则是各种clone标志的组合。通过clone标志可以有选择的对父进程的资源进行复制。例如CLONE_VM表示共享内存描述符合所有的页表; CLONE_FS共享根目录和当前工作目录所在的表以及权限掩码。
statck_start:子进程用户态堆栈的地址;
regs:指向pt_regs结构体的指针。当系统发生系统调用,即用户进程从用户态切换到内核态时,该结构体保存通用寄存器中的值,并被存放于内核态的堆栈中;
stack_size:未被使用,通常被赋值为0;
parent_tidptr:父进程在用户态下pid的地址,该参数在CLONE_PARENT_SETTID标志被设定时有意义;
child_tidptr:子进程在用户态下pid的地址,该参数在CLONE_CHILD_SETTID标志被设定时有意义。
函数原型及实现为:
long do_fork(unsigned long clone_flags,unsigned long stack_start, struct pt_regs *regs,unsigned long stack_size,int __user *parent_tidptr, int __user *child_tidptr)
{
struct task_struct *p;
p = copy_process(clone_flags, stack_start, regs, stack_size,
child_tidptr, NULL, trace); (1)
if (!IS_ERR(p)) {
if (clone_flags & CLONE_VFORK) {
p->vfork_done = &vfork;
init_completion(&vfork);
}
……
}
一、首先调用copy_process()函数
copy_process()函数实现了进程的大部分拷贝工作。
static struct task_struct *copy_process(unsigned long clone_flags, unsigned long stack_start,struct pt_regs *regs,unsigned long stack_size, int __user *child_tidptr, struct pid *pid,int trace)
{
//对传入的clone_flag进行检查
//为新进程创建一个内核栈、thread_info结构和task_struct;其值域当前进程的值完全相同(父子进程的描述符此时也相同)
p = dup_task_struct(current);
//判断是否超出进城用户可以拥有的总进城数量,检查是否有权对指定的资源进行操作
if (atomic_read(&p->real_cred->user->processes) >=
task_rlimit(p, RLIMIT_NPROC)) {
if (!capable(CAP_SYS_ADMIN) && !capable(CAP_SYS_RESOURCE) &&
p->real_cred->user != INIT_USER)
goto bad_fork_free;
}
//在task_struct结构中有一个指针user,该指针指向一个user_struct结构,一个用户的多个进程可以通过user指针共享该用户的资源信息,该结构定义在include/linux/sched.h中,
retval = copy_creds(p, clone_flags);
//copy_creds函数中调用:
//检查创建的进程是否超过了系统进程总量
if (nr_threads >= max_threads)
goto bad_fork_cleanup_count;
//获得进程执行域
if (!try_module_get(task_thread_info(p)->exec_domain->module))
goto bad_fork_cleanup_count;
//调用copy_flags函数更新task_struct结构中flags成员。表明进程是否拥有超级用户权限的PF_SUPERPPRIV标志被清除,表明进程还没有exec()的PF_FORKNOEXEC被设置
copy_flags(clone_flags, p);
//根据clone的参数标志,拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间,代码如下图所示。
//为新进程获取一个有效的PID,调用pid = alloc_pidmap();紧接着使用alloc_pidmap函数为这个新进程分配一个pid。由于系统内的pid是循环使用的,所以采用位图方式来管理,用每一位(bit)来标示该位所对应的pid是否被使用。分配完毕后,判断pid是否分配成功。
pid = alloc_pid(p->nsproxy->pid_ns);
//父子进程平分共享的时间片
sched_fork(p, clone_flags);
//返回子进程的指针。
return p;
}
再回到do_fork函数,如果copy_process函数成功返回,新创建的子进程被唤醒并投入运行。内核有意选择子进程首先执行。因为一般子进程都会马上调用exec函数,这样可以避免写时拷贝的额外开销,如果父进程首先执行的话,有可能开始向地址空间写入。
二、init_completion(&vfork);
如果clone_flags包含CLONE_VFORK标志,那么将进程描述符中的vfork_done字段指向这个完成量,之后再对vfork完成量进行初始化。完成量的作用是,直到任务A发出信号通知任务B发生了某个特定事件时,任务B才会开始执行;否则任务B一直等待。我们知道,如果使用vfork系统调用来创建子进程,那么必然是子进程先执行。究其原因就是此处vfork完成量所起到的作用:当子进程调用exec函数或退出时就向父进程发出信号。此时,父进程才会被唤醒;否则一直等待。此处的代码只是对完成量进行初始化,具体的阻塞语句则在后面的代码中有所体现。
三、检查子进程是否设置了CLONE_STOPPED标志。
设置了CLONE_STOPPED标志通过sigaddset函数为子进程增加挂起信号。signal对应一个unsigned long类型的变量,该变量的每个位分别对应一种信号。具体的操作是,将SIGSTOP信号所对应的那一位置1。
如果子进程并未设置CLONE_STOPPED标志,那么通过wake_up_new_task函数使得父子进程之一优先运行;否则,将子进程的状态设置为TASK_STOPPED。
四、检查CLONE_VFORK标志被设置
如果CLONE_VFORK标志被设置,则通过wait操作将父进程阻塞,直至子进程调用exec函数或者退出。
五、返回pid
return nr; //其中nr最后一次赋值为:nr = task_pid_vnr(p);即子进程的pid号。
至此,fork函数的系统调用过程结束,子进程和父进程各返回一次,子进程返回值为0,父进程返回值为子进程的pid号。应用程序可通过fork的返回值来判断是在子进程中还是父进程中,从而实现多进程程序的编写。