fork()相关的源码解析
fork()的真正执行采用的是do_fork()函数,所以下文将从do_fork()函数对fork()进行源码解析。下图是do_fork()的源码函数设计:
从上图我们可以看到do_fork()涉及到众多的参数。所以在进入do_fork函数进行分析之前,很有必要了解一下它的参数。
clone_flags:克隆标识;
*
* cloning flags:
*/
#define CSIGNAL 0x000000ff /* signal mask to be sent at exit */
/**
* 共享内存描述符和所有页表
*/
#define CLONE_VM 0x00000100 /* set if VM shared between processes */
/**
* 共享根目录和当前工作目录所在的表,以及用于屏蔽新文件初始许可权的位掩码值
*/
#define CLONE_FS 0x00000200 /* set if fs info shared between processes */
/**
* 共享打开文件表
*/
#define CLONE_FILES 0x00000400 /* set if open files shared between processes */
/**
* 共享信号处理程序的表、阻塞信号表和挂起信号表
* 如果该标志为1,那么一定要设置CLONE_VM
*/
#define CLONE_SIGHAND 0x00000800 /* set if signal handlers and blocked signals shared */
/**
* 如果父进程被跟踪,那么,子进程也被跟踪。
* 尤其是,debugger程序可能希望以自己作为父进程来跟踪子进程。在这种情况下,内核把该标志强设为1
*/
#define CLONE_PTRACE 0x00002000 /* set if we want to let tracing continue on the child too */
/**
* 在发出vfork系统调用时设置
*/
#define CLONE_VFORK 0x00004000 /* set if the parent wants the child to wake it up on mm_release */
/**
* 设置子进程的父进程为调用进程的父进程。
*/
#define CLONE_PARENT 0x00008000 /* set if we want to have the same parent as the cloner */
/**
* 把子进程插入到父进程的同一线程组中。并使子进程共享父进程的信号描述符。因此也设置子进程的tgid字段和group_leader字段。
* 如果这个标志位为1,则必须设置CLONE_SIGHAND标志。
*/
#define CLONE_THREAD 0x00010000 /* Same thread group? */
/**
* 当clone需要自己的命名空间时,设置该标志。不能同时设置本标志和CLONE_FS。
*/
#define CLONE_NEWNS 0x00020000 /* New namespace group? */
/**
* 共享System V IPC取消信号量的操作。
*/
#define CLONE_SYSVSEM 0x00040000 /* share system V SEM_UNDO semantics */
/**
* 为轻量级进程创建新的线程局部存储段(TLS),该段由参数tls所指向的结构进行描述。
*/
#define CLONE_SETTLS 0x00080000 /* create a new TLS for the child */
/**
* 把子进程的PID写入由ptid参数所指向的父进程的用户态变量。
*/
#define CLONE_PARENT_SETTID 0x00100000 /* set the TID in the parent */
/**
* 如果该标志被设置,则内核建立一种触发机制,用在子进程要退出或要开始执行新程序。
* 在这些情况下,内核将清除由参数ctid所指向的用户态变量,并唤醒等待这个事件的任何进程
*/
#define CLONE_CHILD_CLEARTID 0x00200000 /* clear the TID in the child */
/**
* 遗留标志,将被内核忽略
*/
#define CLONE_DETACHED 0x00400000 /* Unused, ignored */
/**
* 内核设置这个标志以使CLONE_PTRACE标志推动作用(禁止内核线程跟踪进程)
*/
#define CLONE_UNTRACED 0x00800000 /* set if the tracing process can't force CLONE_PTRACE on this clone */
/**
* 把子进程的PID写入由ctid参数所指向的子进程的用户态变量中
*/
#define CLONE_CHILD_SETTID 0x01000000 /* set the TID in the child */
/**
* 强迫子进程开始于TASK_STOPPED状态
*/
#define CLONE_STOPPED 0x02000000 /* Start in stopped state */
statck_start:子进程用户态堆栈的地址,即栈的起始位置。
regs:指向pt_regs结构体的指针。当系统发生系统调用,即用户进程从用户态切换到内核态时,该结构体保存通用寄存器中的值,并被存放于内核态的堆栈中;
stack_size:栈的大小;
parent_tidptr:父进程在用户态下pid的地址;
child_tidptr:子进程在用户态下pid的地址;
了解了参数之后,我们已经知道PID代表了各进程的进程ID。也就是说,PID就是各进程的身份标识。 只要运行一程序,系统会自动分配一个标识! 是暂时唯一:进程中止后,这个号码就会被回收,并可能被分配给另一个新进程。 只要没有成功运行其他程序,这个pid会继续分配给当前要运行的程序!! 如果成功运行一个程序,然后再运行别的程序时,系统会自动分配另一个pid! 所以,这个函数内部实际进行了以下过程:
(1)我们要为子进程分配新的pid参数。 在一开始,该函数定义了一个task_struct类型的指针p,用来接收即将为新进程(子进程)所分配的进程描述符。紧接着使用alloc_pidmap函数为这个新进程分配一个pid。由于系统内的pid是循环使用的,所以采用位图方式来管理。简单的说,就是用每一位(bit)来标示该位所对应的pid是否被使用。分配完毕后,判断pid是否分配成功,其中,EAGAIN表示当前的进程数已经达到了系统规定的上限。
(2) 接下来检查当前进程(父进程)的ptrace字段。ptrace是用来标示一个进程是否被另外一个进程所跟踪。所谓跟踪,最常见的例子就是处于调试状态下的进程被debugger进程所跟踪。父进程的ptrace字段非0时说明debugger程序正在跟踪父进程,那么接下来通过fork_traceflag函数来检测子进程是否也要被跟踪。如果trace为1,那么就将跟踪标志CLONE_PTRACE加入标志变量clone_flags中。 通常上述的跟踪情况是很少发生的,因此在判断父进程的ptrace字段时使用了unlikely修饰符。使用该修饰符的判断语句执行结果与普通判断语句相同,只不过在执行效率上有所不同。正如该单词的含义所表示的那样,current->ptrace很少为非0。因此,编译器尽量不会把if内的语句与当前语句之前的代码编译在一起,以增加cache的命中率。与此相反,likely修饰符则表示所修饰的代码很可能发生。
(3)创建一个子进程的进程描述符。主要使用的函数设计原型是:p = copy_process(clone_flags, stack_start, regs, stack_size, parent_tidptr, child_tidptr, pid);如果成功的话,则返回该进程描述符。
(4)创建描述符成功之后,定义一个vfok来表示完成量,如果clone_flags中和CLONE_VFORK一样,则将这个完成量赋给vfork_done.并初始化这个完成量。
if (!IS_ERR(p)) {
struct completion vfork;
if (clone_flags & CLONE_VFORK) {
p->vfork_done = &vfork;
init_completion(&vfork);
}
(5)如果子进程被跟踪或者设置了CLONE_STOPPED标志,那么通过sigaddset函数为子进程设置挂起信号。其中,signal对应一个unsigned long类型的变量,该变量的每个位分别对应一种信号。具体的操作是,将SIGSTOP信号所对应的那一位置1。并将p所代表的子线程的线程标志置为有挂起信号。
/**
* 如果设置了CLONE_STOPPED,或者必须跟踪子进程.
* 就设置子进程为TASK_STOPPED状态,并发送SIGSTOP信号挂起它.
*/
if ((p->ptrace & PT_PTRACED) || (clone_flags & CLONE_STOPPED)) {
/*
* We'll start up with an immediate SIGSTOP.
*/
sigaddset(&p->pending.signal, SIGSTOP);
set_tsk_thread_flag(p, TIF_SIGPENDING);
}
sigaddset源代码如下:
/**
* 把nsig信号在set变量中对应的位置为1
*/
static inline void sigaddset(sigset_t *set, int _sig)//19
{
unsigned long sig = _sig - 1; //18
if (_NSIG_WORDS == 1) //2
set->sig[0] |= 1UL << sig;
else
set->sig[sig / _NSIG_BPW] |= 1UL << (sig % _NSIG_BPW);
}
其中
#define _NSIG 64
#define _NSIG_BPW 32
#define _NSIG_WORDS (_NSIG / _NSIG_BPW)
/**
* 进程有挂起信号
*/
#define TIF_SIGPENDING 2 /* signal pending */
(6)没有设置CLONE_STOPPED,就调用wake_up_new_task,使得父子进程之一优先运行;否则,将子进程的状态设置为TASK_STOPPED。
(7)如果子进程正被跟踪,则把子进程的PID赋给父进程的ptrace_message,并调用ptrace_notify。ptrace_notify使当前进程停止运行,并向当前进程的父进程发送SIGCHLD信号.子进程的祖父进程是跟踪父进程的debugger进程.如此一来,祖父进程可获取到当前子进程的PID
(8) 如果CLONE_VFORK标志被设置,则通过wait操作将父进程阻塞,直至子进程调用exec函数或者退出.如果copy_process()在执行的时候发生错误,则先释放已分配的pid;再根据PTR_ERR()的返回值得到错误代码,保存于pid中。