对do_fork的源码进行了分析,看了好多遍,昨晚又看了一遍,现在能够自己顺下来大致执行过程,比之前理解更深刻。
先解释一下ptr_err,这在下面会用到。因为在内核中,有些函数,比如kmalloc是返回指针的,kmalloc是分配内存的,如果分配不到,就返回null指针。有些函数错误时,我们在知道它错了的基础上还要获得错误码,在用户空间,提供了error变量,获得错误码,在内核中就相应的有ptr_err,很明显,也是用来获得错误码的。在do_fork中,copy_process错误时,错误码保存在pid中。
do_fork函数的主要作用就是复制原来的进程使其成为一个新的进程,它完成了整个进程创建中的大部分工作。
Fork、vfork和clone三个系统调用所对应的系统调用服务例程都调用了do_fork()。只是所传递的参数不同,导致父进程和子进程在共享资源的程度上不同。fork是全部复制父进程,传给子进程,所以fork不带参数。而clone复制部分父进程资源,也就是说复制是有选择的。vfork准确来说,它创建的是线程而不是进程。并且vfork创建的子进程先于父进程执行。只有子进程执行结束或退出,父进程才被唤醒。
接下来分析do_fork()。
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)
首先解释以上几个参数,
clone_flags: 通过clone标志可以有选择的对父进程的资源进行复制;fork,vfork和clone就是因为flag标志不同才加以区分的。
statck_start:子进程用户态堆栈的地址;
regs:指向pt_regs结构体的指针。当系统发生系统调用,即用户进程从用户态切换到内核态时,该结构体保存通用寄存器中的值,并被存放于内核态的堆栈中;
stack_size:未被使用,通常被赋值为0;
parent_tidptr:父进程在用户态下pid的地址,只有在CLONE_PARENT_SETTID标志被设定时才有意义;
child_tidptr:子进程在用户态下pid的地址,和上面的一样,也是在CLONE_CHILD_SETTID标志被设定时有意义;
大体执行步骤如下:
1.定义一个task_struct类型的指针p( struct task_struct *p;),作用:接收子进程所分配的进程描述符(这儿是用copy_process实现的)。为新进程分配一个pid。分配完毕后,判断pid是否分配成功,判断语句如下:
if (pid < 0)
return -EAGAIN;
2. 检查当前进程是否被跟踪。
跟踪与否是看ptrace的值。父进程非0则未被跟踪。如果父被跟踪,就得判断子进程是否被跟踪。
如果trace为1,那么就将跟踪标志CLONE_PTRACE加入标志变量clone_flags中。
if (unlikely(current->ptrace)) {
trace = fork_traceflag (clone_flags);
if (trace)
clone_flags |= CLONE_PTRACE;
}
3. 通过copy_process()创建子进程的描述符,并创建子进程执行时所需的其他数据结构,最终则会返回这个创建好的进程描述符。
|
p = copy_process(clone_flags, stack_start, regs, stack_size, parent_tidptr, child_tidptr, pid); |
4. 若copy_process执行不成功,则先释放已分配的pid,根据ptr_err得到错误码,保存在pid中。
如果成功,则执行如下代码:
首先定义了一个完成量vfork,如果clone_flags包含CLONE_VFORK标志,那么将进程描述符中的vfork_done字段指向这个完成量,之后再对vfork完成量进行初始化。
if (!IS_ERR(p)) {
struct completion vfork;
if (clone_flags & CLONE_VFORK)
{
p->vfork_done = &vfork;
init_completion(&vfork);
}
5. 如果子进程被跟踪(在父进程被跟踪的前提下,一般来说父进程是很少被跟踪的)或者设置了CLONE_STOPPED标志,就为子进程增加挂起信号。
具体操作是,将SIGSTOP信号所对应的那一位置1。
if ((p->ptrace & PT_PTRACED) || (clone_flags & CLONE_STOPPED)) {
sigaddset(&p->pending.signal, SIGSTOP);
set_tsk_thread_flag(p, TIF_SIGPENDING);
}
6. 如果子进程未设置CLONE_STOPPED标志,那么通过wake_up_new_task函数使得父子进程之一优先运行;否则,将子进程的状态设置为TASK_STOPPED。
if (!(clone_flags & CLONE_STOPPED))
wake_up_new_task(p, clone_flags);
else
p->state = TASK_STOPPED;
7. 如果父进程被跟踪,则将子进程的pid赋值给父进程,存于进程描述符的pstrace_message字段。使得当前进程停止运行,并向父进程的父进程发送SIGCHLD信号。
if (unlikely (trace)) {
current->ptrace_message = pid;
ptrace_notify ((trace << 8) | SIGTRAP);
}
8. 如果CLONE_VFORK标志被设置,则通过wait操作将父进程阻塞,直至子进程调用exec函数或者退出。(这儿显示的就是vfork的作用)
if (clone_flags & CLONE_VFORK) {
wait_for_completion(&vfork);
if (unlikely (current->ptrace & PT_TRACE_VFORK_DONE))
ptrace_notify ((PTRACE_EVENT_VFORK_DONE << 8) | SIGTRAP);
}
最后: 返回pid。
这也是fork系统调用时父进程会返回子进程pid的原因。
看完do_fork首先明白了父进程为什么返回子进程的pid,之前只是被动接受父进程是返回子进程的pid,现在能够理解这样的原因。在这之前也只是直接调用,就像之前编fork程序,就几行程序。看了fork的源代码,才知道操作系统做了多少,远远比想象中多,虽然只是明白了do_fork的大致流程,有些细节,以及这之中调用的函数,如copy_process函数,还没看明白,但是每一次看都会比之前理解的更深刻。