内核的版本是3.10.9. 我自己的分析使用棕色字
long do_fork(unsigned long clone_flags,
unsigned long stack_start,unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr)
{
struct task_struct *p;
int trace = 0;
long nr;
/*
* Do some preliminary argument and permissions checking before we
* actually start allocating stuff
*/
if (clone_flags & (CLONE_NEWUSER | CLONE_NEWPID)) {
if (clone_flags & (CLONE_THREAD|CLONE_PARENT))
return -EINVAL;
}
++++++++++++++++++++++++++++++++++++++
CLONE_NEWUSER这个flag是和user namespace相关的标识,user namespace可以用来进行权限设定。在通过clone函数fork进程的时候,我们可以选择clone之前的user namespace,当然也可以通过传递该标识来创建新的user namespace。
CLONE_NEWPID这个flag是和PID namespace相关的标识。思路同上。
这两个flag是和虚拟化技术相关的。虚拟化技术就需要资源隔离,也就是说,不同的虚拟主机(实际上在一台物理主机上)资源是不可见的。因此,linux kernel增加了若干个name space,例如user name space、PID namespace、IPC namespace、uts namespace、network namespace等。以PID namespace为例,原来的linux kernel中,PID唯一的标识了一个process,在引入PID namespace之后,不同的namespace可以拥有同样的ID,也就是说,标识一个进程的是PID namespace + PID。
CLONE_PARENT这个flag表示新fork的进程想要和创建该进程的cloner拥有同样的父进程。
CLONE_THREAD这个flag表示新的fork的进程与cloner是否在一个线程组中。
在了解了上述的内容之后,我们就可以进行后续的分析了。
一旦cloner设定了CLONE_NEWPID的flag,其本意就是和过去的PID namespace say goodbye,和cloner的PID namespace做切割,CLONE_PARENT和CLONE_THREAD这两个flag都是设定新进程的PID和旧的PID有藕断丝连的关系,这样矛盾的设定当要坚决打击。
CLONE_NEWUSER和CLONE_PARENT和CLONE_THREAD排他性设定 TODO
CLONE_NEWUSER设定的时候,就会为fork的进程创建一个新的user namespace,以便隔离USER ID。linux 系统内的一个进程和某个user namespace内的uid和gid相关。user namespace被实现成树状结构,新的user namespace中第一个进程的uid就是0,也就是root用户。这个进程在这个新的user namespace中有超级权限,但是,在其父user namespace中只是一个普通用户。
++++++++++++++++++++++++++++++++++++++
* Determine whether and which event to report to ptracer. When
* called from kernel_thread or CLONE_UNTRACED is explicitly
* requested, no event is reported; otherwise, report if the event
* for the type of forking is enabled.
*/
if (!(clone_flags & CLONE_UNTRACED)) {
if (clone_flags & CLONE_VFORK)
trace = PTRACE_EVENT_VFORK;
else if ((clone_flags & CSIGNAL) != SIGCHLD)
trace = PTRACE_EVENT_CLONE;
else
trace = PTRACE_EVENT_FORK;
if (likely(!ptrace_event_enabled(current, trace)))
trace = 0;
}
++++++++++++++++++++++++++++++++++++++
Linux的内核提供了ptrace这样的系统调用,通过它,一个进程(我们称之tracer,例如strace、gdb)可以观测和控制另外一个进程(被trace的进程,我们称之tracee)的执行。一旦Tracer和tracee建立了跟踪关系,那么所有发送给tracee的信号(除SIGKILL)都会汇报给Tracer,以便Tracer可以控制或者观测tracee的执行。例如断点的操作。Tracer程序一般会提供界面,以便用户可以设定一个断点(当tracee运行到断点时,会停下来)。当用户设定了断点后,tracer就会保存该位置的指令,然后向该位置写入SWI __ARM_NR_breakpoint(这种断点是soft break point,可以设定无限多个,对于hard break point是和CPU体系结构相关,一般支持2个)。当执行到断点位置的时候,发生软中断,内核会给tracee进程发出SIGTRAP信号,当然这个信号会被tracer捕获。对于tracee,当收到信号的时候,无论是什么信号,甚至是ignor的信号,tracee进程都会停止运行。Tracer进程可以对tracee进行各种操作,例如观察tracer的寄存器,观察变量等等。
在了解完上述的背景之后,再来看代码就比较简单了。这个代码块控制创建进程是否向tracer上报信号,如果需要上报,那么要上报哪些信号。如果用户进程在创建的时候有携带CLONE_UNTRACED的flag,那么该进程则不能被trace。对于内核线程,在创建的时候都会携带该flag,这也就意味着,内核线程是无法被traced,也就不需要上报event给tracer。
++++++++++++++++++++++++++++++++++++++
child_tidptr, NULL, trace);
++++++++++++++++++++++++++++++++++++++
这是fork的主要内容,我会独立一篇文章出来详细分析
++++++++++++++++++++++++++++++++++++++
/*
* Do this prior waking up the new thread - the thread pointer* might get invalid after that point, if the thread exits quickly.
*/
if (!IS_ERR(p)) {
struct completion vfork;
trace_sched_process_fork(current, p);
nr = task_pid_vnr(p);
if (clone_flags & CLONE_PARENT_SETTID)
put_user(nr, parent_tidptr);
++++++++++++++++++++++++++++++++++++++
如果设置了CLONE_PARENT_SETTID这个flag,父进程需要知道子进程的thread ID,因此,父进程在调用clone的时候,会将地址传递给内核,内核将创建的子进程的thread ID写入其中。
++++++++++++++++++++++++++++++++++++++
p->vfork_done = &vfork;
init_completion(&vfork);
get_task_struct(p);
}
wake_up_new_task(p);
/* forking complete and child started to run, tell ptracer */
if (unlikely(trace))
ptrace_event(trace, nr);
if (clone_flags & CLONE_VFORK) {
if (!wait_for_vfork_done(p, &vfork))
ptrace_event(PTRACE_EVENT_VFORK_DONE, nr);
}
} else {
nr = PTR_ERR(p);
}
++++++++++++++++++++++++++++++++++++++
Vfork和fork是类似的,除了下面两点:
1. 阻塞父进程
2. 不复制父进程的页表
之所以会有vfork这样的创建进程的方法,其主要原因是由于很可能父进程在fork之后会调用exec函数,如果这样,fork所做的努力都白费了。在引入了COW技术后,fork开销已经不大,因此vfork的意义也不是非常的明显。
之所以vfork要阻塞父进程是因为vfork后父子进程使用的是完全相同的mm, 也就是说使用的是完全相同的虚拟内存空间, 包括栈也相同。所以两个进程不能同时运行, 否则栈就乱掉了。所以vfork后, 父进程是阻塞的,直到调用了exec系列函数或者exit函数后。这时候,子进程的mm(old_mm)需要释放掉,不再与父进程共用了,这时候就可以解除父进程的阻塞状态。
++++++++++++++++++++++++++++++++++++++
}