(本文部分参考了《Linux内核源代码情景分析》)
Linux 将进程的创建与目标程序的执行分成两步。第一步是从已经存在的“父进程”中像细胞分裂一样地复制出一个“子进程”。第二步是目标程序的执行。Linux 为此提供了一个系统调用 execve(),让一个进程执行以文件形式存在的一个可执行程序的映象。本文主要介绍创建进程第一步所需要的系统调用fork(),vfork()和clone()。还要指出, Linux 内核中确实有个貌似“一揽子”创建内核线程的函数(常常称为“原语”)kernel_thread(),供内核线程调用。但是,实际上这只是对 clone()的包装,它并不能像调用 execve()时那样执行一个可执行映象文件,而只是执行内核中的某一个函数,本文不对此函数加以研究。
1、fork(),vfork()和clone()区别
2、do_fork()
1、 fork():无参,全部复制,父进程所有的资源全部都通过数据结构的复制“遗传”给子进程。
clone():带有参数,可以将资源有选择地复制给子进程,而没有复制的数据结构则通过指针的复制让子进程共享。在极端的情况下,一个进程可以 clone()出一个线程。
vfork(),也不带参数,但是除 task_struct结构和系统空间堆栈以外的资源全都通过数据结构指针的复制“遗传”,所以 vfork()出来的是线程而不是进程。vfork()主要只是作为创建进程的中间步骤,目的在于提高创建时的效率,减少系统开销,在fork()还没有写时拷贝时有很大作用,但是在fork()逐渐完善的时候其作用也在减小。
以下为三者代码:
asmlinkage int sys_fork(long r10, long r11, long r12, long r13, long mof, long srp,
struct pt_regs *regs)
{
return do_fork(SIGCHLD, rdusp(), regs, 0, NULL, NULL);
}
asmlinkage int sys_clone(unsigned long newusp, unsigned long flags,
int* parent_tid, int* child_tid, long mof, long srp,
struct pt_regs *regs)
{
if (!newusp)
newusp = rdusp(); return do_fork(flags, newusp, regs, 0, parent_tid, child_tid);
}
asmlinkage int sys_vfork(long r10, long r11, long r12, long r13, long mof, long srp,
struct pt_regs *regs)
{
return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, rdusp(), regs, 0, NULL, NULL);
}
由上述代码可知,fork(),vfork()和clone()都是调用了do_fork()实现的,通过参数不同而区分。
2、
/**
* 负责处理clone,fork,vfork系统调用。
* clone_flags-与clone的flag参数相同
* stack_start-与clone的child_stack相同
* regs-指向通用寄存器的值。是在从用户态切换到内核态时被保存到内核态堆栈中的。
* stack_size-未使用,总是为0
* parent_tidptr,child_tidptr-clone中对应参数ptid,ctid相同
*/
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;
int trace = 0;
/**
* 通过查找pidmap_array位图,为子进程分配新的pid参数.
*/
long pid = alloc_pidmap();
if (pid < 0)
return -EAGAIN;
/**
* 如果父进程正在被跟踪,就检查debugger程序是否想跟踪子进程.并且子进程不是内核进程(CLONE_UNTRACED未设置)
* 那么就设置CLONE_PTRACE标志.
*/
if (unlikely(current->ptrace)) {
trace = fork_traceflag (clone_flags);
if (trace)
clone_flags |= CLONE_PTRACE;
}
/**
* copy_process复制进程描述符.如果所有必须的资源都是可用的,该函数返回刚创建的task_struct描述符的地址.
* 这是创建进程的关键步骤.
*/
p = copy_process(clone_flags, stack_start, regs, stack_size, parent_tidptr, child_tidptr, pid);
/*
* 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;
if (clone_flags & CLONE_VFORK) {
p->vfork_done = &vfork;
init_completion(&vfork);
}
/**
* 如果设置了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);
}
/**
* 没有设置CLONE_STOPPED,就调用wake_up_new_task
* 它调整父进程和子进程的调度参数.
* 如果父子进程运行在同一个CPU上,并且不能共享同一组页表(CLONE_VM标志被清0).那么,就把子进程插入父进程运行队列.
* 并且子进程插在父进程之前.这样做的目的是:如果子进程在创建之后执行新程序,就可以避免写时复制机制执行不必要时页面复制.
* 否则,如果运行在不同的CPU上,或者父子进程共享同一组页表.就把子进程插入父进程运行队列的队尾.
*/
if (!(clone_flags & CLONE_STOPPED))
wake_up_new_task(p, clone_flags);
else/*如果CLONE_STOPPED标志被设置,就把子进程设置为TASK_STOPPED状态。*/
p->state = TASK_STOPPED;
/**
* 如果进程正被跟踪,则把子进程的PID插入到父进程的ptrace_message,并调用ptrace_notify
* ptrace_notify使当前进程停止运行,并向当前进程的父进程发送SIGCHLD信号.子进程的祖父进程是跟踪父进程的debugger进程.
* dubugger进程可以通过ptrace_message获得被创建子进程的PID.
*/
if (unlikely (trace)) {
current->ptrace_message = pid;
ptrace_notify ((trace << 8) | SIGTRAP);
}
/**
* 如果设置了CLONE_VFORK,就把父进程插入等待队列,并挂起父进程直到子进程结束或者执行了新的程序.
*/
/*
*CLONE_VM 标志位为 1,因而父、子进程通过指针共享用户空间时,父、子进程是在真正意义上共享用户空间,父进程写入其用户空间的内容同时也“写入”子进程的用户空间,反之亦然。
*在这样的情况下不能让两个进程都回到用户空间并发地运行;否则,必然时两个进程最终都乱来一气或者因非法越界访问而死亡。
*解决办法是“扣留”其中一个进程,而只让一个进程回到用户空间,直到两个进程不再共享它们的用户空间或其中一个进程(必然是回到用户空间运行的那个进程)消亡为止
*/
/*
*因此, CLONE_VM 要与CLONE_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);
}
} else {
free_pidmap(pid);
pid = PTR_ERR(p);
}
return pid;
}
参数 clone_flags 由两部分组成,其最低的字节为信号类型,用以规定子进程去世时应该向该父进程发出的信号。我们已经看到,对于 fork()和 vfork()这个信号就是 SIGCHLD,而对__clone()则该段可由调用者决定。第二部分是一些表示资源和特性的标志位,这些标志位是在 include/linux/sched.h 中定义的:
00030: /*
00031: * cloning flags:
00032: */
00033: #define CSIGNAL 0x000000ff /* signal mask to be sent at exit */
00034: #define CLONE_VM 0x00000100 /* set if VM shared between processes */
00035: #define CLONE_FS 0x00000200 /* set if fs info shared between processes */
00036: #define CLONE_FILES 0x00000400 /* set if open files shared between processes */
00037: #define CLONE_SIGHAND 0x00000800 /* set if signal handlers and blocked signals
shared */
00038: #define CLONE_PID 0x00001000 /* set if pid shared */
00039: #define CLONE_PTRACE 0x00002000 /* set if we want to let tracing continue on the
child too */
00040: #define CLONE_VFORK 0x00004000 /* set if the parent wants the child to wake it
up on mm_release */
00041: #define CLONE_PARENT 0x00008000 /* set if we want to have the same parent as the
cloner */
00042: #define CLONE_THREAD 0x00010000 /* Same thread group? */
00043:
00044: #define CLONE_SIGNAL (CLONE_SIGHAND | CLONE_THREAD)
对于 fork(),这一部分为全 0,表示对有关的资源都要复制而不是通过指针共享。而对 vfork(),则为CLONE_VFORK | CLONE_VM,表示父、子进程共用(用户)虚存区间,并且当子进程释放其虚存区间时要唤醒父进程。至于clone(),则这一部分完全由调用者设定而作为参数传递下来。
其中标志位CLONE_PID 有特殊的作用,当这个标志位为 1 时,父、子进程(线程)共用同一个进程号,也就是说,则进程虽然有其自己的 task_struct 数据结构,却使用父进程的 pid。但是,只有 0 号进程,也就是系统中的原始进程(实际上是线程),才允许这样来调用__clone()(在Linux-2.6.11中未找到此标志的定义???)
do_fork()中最重要的一步是复制进程描述符:p = copy_process(clone_flags, stack_start, regs, stack_size, parent_tidptr, child_tidptr, pid);此时转到函数copy_process中:
/**
* 创建进程描述符以及子进程执行所需要的所有其他数据结构
* 它的参数与do_fork相同。外加子进程的PID。
*/
static task_t *copy_process(unsigned long clone_flags,
unsigned long stack_start,
struct pt_regs *regs,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr,
int pid)
{
int retval;
struct task_struct *p = NULL;
/**
* 检查clone_flags所传标志的一致性。
*/
/**
* 如果CLONE_NEWNS和CLONE_FS标志都被设置,返回错误
*/
if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS))
return ERR_PTR(-EINVAL);
/**
* CLONE_THREAD标志被设置,并且CLONE_SIGHAND没有设置。
* (同一线程组中的轻量级进程必须共享信号)
*/
if ((clone_flags & CLONE_THREAD) && !(clone_flags & CLONE_SIGHAND))
return ERR_PTR(-EINVAL);
/**
* CLONE_SIGHAND被设置,但是CLONE_VM没有设置。
* (共享信号处理程序的轻量级进程也必须共享内存描述符)
*/
if ((clone_flags & CLONE_SIGHAND) && !(clone_flags & CLONE_VM))
return ERR_PTR(-EINVAL);
/**
* 通过调用security_task_create以及稍后调用security_task_alloc执行所有附加的安全检查。
* LINUX2.6提供扩展安全性的钩子函数,与传统unix相比,它具有更加强壮的安全模型。
*/
retval = security_task_create(clone_flags);
if (retval)
goto fork_out;
retval = -ENOMEM;
/**
* 调用dup_task_struct为子进程获取进程描述符。
*/
p = dup_task_struct(current);
if (!p)
goto fork_out;
/**
* 检查存放在current->sigal->rlim[RLIMIT_NPROC].rlim_cur中的限制值,是否小于或者等于用户所拥有的进程数。
* 如果是,则返回错误码。当然,有root权限除外。
* p->user表示进程的拥有者,p->user->processes表示进程拥有者当前进程数
* xie.baoyou注:此处比较是用>=而不是>
*/
retval = -EAGAIN;
if (atomic_read(&p->user->processes) >=
p->signal->rlim[RLIMIT_NPROC].rlim_cur) {
/**
* 当然,用户有root权限就另当别论了
*/
if (!capable(CAP_SYS_ADMIN) && !capable(CAP_SYS_RESOURCE) &&
p->user != &root_user)
goto bad_fork_free;
}
/**
* 递增user结构的使用计数器
*/
atomic_inc(&p->user->__count);
/**
* 增加用户拥有的进程计数。
*/
atomic_inc(&p->user->processes);
get_group_info(p->group_info);
/**
* 检查系统中的进程数量(nr_threads)是否超过max_threads
* max_threads的缺省值是由系统内存容量决定的。总的原则是:所有的thread_info描述符和内核栈所占用的空间
* 不能超过物理内存的1/8。不过,系统管理可以通过写/proc/sys/kernel/thread-max文件来改变这个值。
*/
if (nr_threads >= max_threads)
goto bad_fork_cleanup_count;
/**
* 如果新进程的执行域和可招待格式的内核函数都包含在内核中模块中,
* 就递增它们的使用计数器。
*/
if (!try_module_get(p->thread_info->exec_domain->module))
goto bad_fork_cleanup_count;
if (p->binfmt && !try_module_get(p->binfmt->module))
goto bad_fork_cleanup_put_domain;
/**
* 设置几个与进程状态相关的关键字段。
*/
/**
* did_exec是进程发出的execve系统调用的次数,初始为0
*/
p->did_exec = 0;
/**
* 更新从父进程复制到tsk_flags字段中的一些标志。
* 首先清除PF_SUPERPRIV。该标志表示进程是否使用了某种超级用户权限。
* 然后设置PF_FORKNOEXEC标志。它表示子进程还没有发出execve系统调用。
*/
copy_flags(clone_flags, p);
/**
* 保存新进程的pid值。
*/
p->pid = pid;
retval = -EFAULT;
/**
* 如果CLONE_PARENT_SETTID标志被设置,就将子进程的PID复制到参数parent_tidptr指向的用户态变量中。
* xie.baoyou:想想我们常常调用的pid = fork()语句吧。
*/
if (clone_flags & CLONE_PARENT_SETTID)
if (put_user(p->pid, parent_tidptr))
goto bad_fork_cleanup;
p->proc_dentry = NULL;
/**
* 初始化子进程描述符中的list_head数据结构和自旋锁。
* 并为挂起信号,定时器及时间统计表相关的几个字段赋初值。
*/
INIT_LIST_HEAD(&p->children);
INIT_LIST_HEAD(&p->sibling);
p->vfork_done = NULL;
spin_lock_init(&p->alloc_lock);
spin_lock_init(&p->proc_lock);
clear_tsk_thread_flag(p, TIF_SIGPENDING);
init_sigpending(&p->pending);
p->it_real_value = 0;
p->it_real_incr = 0;
p->it_virt_value = cputime_zero;
p->it_virt_incr = cputime_zero;
p->it_prof_value = cputime_zero;
p->it_prof_incr = cputime_zero;
init_timer(&p->real_timer);
p->real_timer.data = (unsigned long) p;
p->utime = cputime_zero;
p->stime = cputime_zero;
p->rchar = 0; /* I/O counter: bytes read */
p->wchar = 0; /* I/O counter: bytes written */
p->syscr = 0; /* I/O counter: read syscalls */
p->syscw = 0; /* I/O counter: write syscalls */
acct_clear_integrals(p);
/**
* 把大内核锁计数器初始化为-1
*/
p->lock_depth = -1; /* -1 = no lock */
do_posix_clock_monotonic_gettime(&p->start_time);
p->security = NULL;
p->io_context = NULL;
p->io_wait = NULL;
p->audit_context = NULL;
#ifdef CONFIG_NUMA
p->mempolicy = mpol_copy(p->mempolicy);
if (IS_ERR(p->mempolicy)) {
retval = PTR_ERR(p->mempolicy);
p->mempolicy = NULL;
goto bad_fork_cleanup;
}
#endif
p->tgid = p->pid;
if (clone_flags & CLONE_THREAD)
p->tgid = current->tgid;
if ((retval = security_task_alloc(p)))
goto bad_fork_cleanup_policy;
if ((retval = audit_alloc(p)))
goto bad_fork_cleanup_security;
/**
* copy_semundo,copy_files,copy_fs,copy_sighand,copy_signal
* copy_mm,copy_keys,copy_namespace创建新的数据结构,并把父进程相应数据结构的值复制到新数据结构中。
* 除非clone_flags参数指出它们有不同的值。
*/
if ((retval = copy_semundo(clone_flags, p)))
goto bad_fork_cleanup_audit;
/*
*函数 copy_files()有条件地复制已打开文件的控制结构,
*只有在 clone_flags 中 CLONE_FILES标志位为 0 时才真正进行,否则就只是共享父进程的已打开文件。
*当一个进程有已打开文件时, task_struct结构中的指针 files 指向一个 files_struct 数据结构,否则为 0。
*所有与终端设备 tty 相联系的用户进程的头三个文件,即 stdin、 stdout 及 stderr,都是预先打开的,所以指针一般不会是 0。
*复制时通过 kmem_cache_alloc()为子进程分配一个files_struct 数据结构为 newf,然后从 oldf 把内容复制到 newf。
*/
if ((retval = copy_files(clone_flags, p)))
goto bad_fork_cleanup_semundo;
/*
*copy_fs()也是只有在 clone_flags 中 CLONE_FS 标志位为 0 时才加以复制。
*task_struct 结构中的指针指向一个 fs_struct 数据结构,结构中记录的是进程的根目录 root、当前工作目录pwd、一个用于文件操作权限管理的 umask,还有一个计数器。
*在这里要复制的是 fs_struct 数据结构,而并不复制更深层次的数据结构。复制了 fs_struct数据结构,就在这一层上有了自主性,至于对更深层次的数据结构则还是共享,所以需要递增它们的共享计数
*/
if ((retval = copy_fs(clone_flags, p)))
goto bad_fork_cleanup_files;
/*
*copy_sighand()也只有在 CLONE_SIGHAND 为 0 时才真正进行;否则就共享父进程的 sig 指针,并将父进程的 signal_struct 中的共享计数加 1。
*一个进程设置了信号处理程序,其 task_struct 结构的指针 sig 就指向一个 signal_struct 数据结构,数据结构中的数组 action[]确定了一个进程对各种信号(以信号的数值为下标)的反应和处理,子进程可以通过复制或共享把它从父进程继承下来。
*/
if ((retval = copy_sighand(clone_flags, p)))
goto bad_fork_cleanup_fs;
if ((retval = copy_signal(clone_flags, p)))
goto bad_fork_cleanup_sighand;
/*
*用户空间的继承。
*进程的 task_struct 结构中的指针 mm,指向一个代表着进程的用户空间的 mm_struct 数据结构。
*由于内核线程并不拥有用户空间,所以在内核线程的task_struct 结构中该指针为 0。
*对 mm_struct 的复制也是只在 clone_flags 中 CLONE_VM 标志为 0 时才真正进行,否则就只是通过已经复制的指针共享父进程的用户空间。
*对 mm_struct 的复制就不只是局限于这个数据结构本身了,也包括了对更深层次数据结构的复制。其中最重要 vm_area_struct 数据结构和页面映射表由 dup_mmap()复制。
*/
/*
*当 CPU 从 copy_mm()回到 do_fork()中时,所有需要有条件复制的资源都已经处理完了。
*当系统调用 fork()通过 sys_fork()进入 do_fork()时,其 clone_flags 为 SIGCHLD,也就是说所有的标志位均为 0,所以 copy_files()、 copy_fs()、 copy_sighand()以及 copy_mm()全部真正执行了,这四项资源全都复制了。
*当 vfork()经过 sys_vfork()进入 do_fork()时,则其 clone_flags 为 CLONE_VFORK |CLONE_VM | SIGCHLD,所以只执行了 copy_files()、 copy_fs()以及 copy_sighand();而 copy_mm(),则因标志位 CLONE_VM 为 1,只是通过指针共享其父进程的 mm_struct,并没有一份自己的副本。这也就是说,经 vfork()复制的是个线程,只能靠共享其父进程的存储空间度日,包括用户空间堆栈在内。
*至于clone(),则取决于调用时的参数。当然,最终还得取决于父进程具有什么资源,要是父进程没有打开的文件,那么即使执行了 copy_files(),也还是空的。
*/
if ((retval = copy_mm(clone_flags, p)))
goto bad_fork_cleanup_signal;
if ((retval = copy_keys(clone_flags, p)))
goto bad_fork_cleanup_mm;
if ((retval = copy_namespace(clone_flags, p)))
goto bad_fork_cleanup_keys;
/*
*前面已通过 alloc_task_struct()分配了两个连续的页面,其低端用作 task_struct结构,已经基本上复制好了;
*而用作系统空间堆栈的高端,却还没有复制。现在就由 copy_thread()来做这件事。
*/
/**
* 调用copy_thread,用发出clone系统调用时CPU寄存器的值(它们保存在父进程的内核栈中)来初始化子进程的内核栈。
*copy_thread把eax寄存器对应字段的值(这是fork和clone系统调用在子进程中的返回值)强行置为0。子进程描述符的thread.esp字段初始化为子进程内核栈的基地址。ret_from_fork的地址存放在thread.eip中。
* 如果父进程使用IO权限位图。则子进程获取该位图的一个拷贝。
* 最后,如果CLONE_SETTLS标志被置位,则子进程获取由CLONE系统调用的参数tls指向的用户态数据结构所表示的TLS段。
*/
retval = copy_thread(0, clone_flags, stack_start, stack_size, p, regs);
if (retval)
goto bad_fork_cleanup_namespace;
/**
* 如果clone_flags参数的值被置为CLONE_CHILD_SETTID或CLONE_CHILD_CLEARTID
* 就把child_tidptr参数的值分别复制到set_child_tid或clear_child_tid字段。
* 这些标志说明:必须改变子进程用户态地址空间的dhild_tidptr所指向的变量的值
* 不过实际的写操作要稍后再执行。
*/
p->set_child_tid = (clone_flags & CLONE_CHILD_SETTID) ? child_tidptr : NULL;
/*
* Clear TID on mm_release()?
*/
p->clear_child_tid = (clone_flags & CLONE_CHILD_CLEARTID) ? child_tidptr: NULL;
/**
* 清除TIF_SYSCALL_TRACE标志。使ret_from_fork函数不会把系统调用结束的消息通知给调试进程。
* 也不应该通知给调试进程,因为子进程并没有调用fork.
*/
clear_tsk_thread_flag(p, TIF_SYSCALL_TRACE);
p->parent_exec_id = p->self_exec_id;
/**
* 用clone_flags参数低位的信号数据编码统建始化tsk_exit_signal字段。
* 如CLONE_THREAD标志被置位,就把exit_signal字段初始化为-1。
* 这样做是因为:当创建线程时,即使被创建的线程死亡,都不应该给领头进程的父进程发送信号。
* 而应该是领头进程死亡后,才向其领头进程的父进程发送信号。
*/
p->exit_signal = (clone_flags & CLONE_THREAD) ? -1 : (clone_flags & CSIGNAL);
p->pdeath_signal = 0;
p->exit_state = 0;
/* Perform scheduler related setup */
/**
* 调用sched_fork完成对新进程调度程序数据结构的初始化。
* 该函数把新进程的状态置为TASK_RUNNING,并把thread_info结构的preempt_count字段设置为1,
* 从而禁止抢占。
* 此外,为了保证公平调度,父子进程共享父进程的时间片。
*/
sched_fork(p);
p->group_leader = p;
INIT_LIST_HEAD(&p->ptrace_children);
INIT_LIST_HEAD(&p->ptrace_list);
/* Need tasklist lock for parent etc handling! */
write_lock_irq(&tasklist_lock);
p->cpus_allowed = current->cpus_allowed;
/**
* 初始化子线程的cpu字段。
*/
set_task_cpu(p, smp_processor_id());
if (sigismember(¤t->pending.signal, SIGKILL)) {
write_unlock_irq(&tasklist_lock);
retval = -EINTR;
goto bad_fork_cleanup_namespace;
}
/**
* 初始化表示亲子关系的字段,如果CLONE_PARENT或者CLONE_THREAD被设置了
* 就用current->real_parent初始化,否则,当前进程就是初创建进程的父进程。
*/
if (clone_flags & (CLONE_PARENT|CLONE_THREAD))
p->real_parent = current->real_parent;
else
p->real_parent = current;
p->parent = p->real_parent;
if (clone_flags & CLONE_THREAD) {
spin_lock(¤t->sighand->siglock);
if (current->signal->flags & SIGNAL_GROUP_EXIT) {
spin_unlock(¤t->sighand->siglock);
write_unlock_irq(&tasklist_lock);
retval = -EAGAIN;
goto bad_fork_cleanup_namespace;
}
p->group_leader = current->group_leader;
if (current->signal->group_stop_count > 0) {
current->signal->group_stop_count++;
set_tsk_thread_flag(p, TIF_SIGPENDING);
}
spin_unlock(¤t->sighand->siglock);
}
/**
* 把新进程加入到进程链表
*/
SET_LINKS(p);
/**
* PT_PTRACED表示子进程必须被跟踪,就把current->parent赋给tsk->parent,并将子进程插入调试程序的跟踪链表中。
*/
if (unlikely(p->ptrace & PT_PTRACED))
__ptrace_link(p, current->parent);
/**
* 把新进程描述符的PID插入pidhash散列表中。
*/
attach_pid(p, PIDTYPE_PID, p->pid);
attach_pid(p, PIDTYPE_TGID, p->tgid);
/**
* 如果子进程是线程组的领头进程(CLONE_THREAD标志被清0)
*/
if (thread_group_leader(p)) {
/**
* 将进程插入相应的散列表。
*/
attach_pid(p, PIDTYPE_PGID, process_group(p));
attach_pid(p, PIDTYPE_SID, p->signal->session);
if (p->pid)
__get_cpu_var(process_counts)++;
}
/**
* 计数
*/
nr_threads++;
total_forks++;
write_unlock_irq(&tasklist_lock);
retval = 0;
...
}
函数copy_process中主体功能分为三步,
(1)p = dup_task_struct(current)函数:复制pcb相关内容;
(2)复制信号、文件等其他资源;
(3)复制内核栈相关资源,用到了函数copy_thread():
以下为dup_task_struct()函数代码实现:
static struct task_struct *dup_task_struct(struct task_struct *orig)
{
struct task_struct *tsk;
struct thread_info *ti;
/**
* prepare_to_copy中会调用unlazy_fpu。
* 它把FPU、MMX和SSE/SSE2寄存器的内容保存到父进程的thread_info结构中。
* 稍后,dup_task_struct将把这些值复制到子进程的thread_info中。
*/
prepare_to_copy(orig);
/**
* alloc_task_struct宏为新进程获取进程描述符,并将描述符保存到tsk局部变量中。
*/
tsk = alloc_task_struct();
if (!tsk)
return NULL;
/**
* alloc_thread_info宏获取一块空闲内存区,用来存放新进程的thread_info结构和内核栈。
*/
ti = alloc_thread_info(tsk);
if (!ti) {
free_task_struct(tsk);
return NULL;
}
/**
* 将current进程描述符的内容复制到tsk所指向的task_struct结构中,然后把tsk_thread_info置为ti
* 将current进程的thread_info内容复制给ti指向的结构中,并将ti_task置为tsk.
*/
*ti = *orig->thread_info;
*tsk = *orig;
tsk->thread_info = ti;
ti->task = tsk;
/* One for us, one for whoever does the "release_task()" (usually parent) */
/**
* 把新进程描述符的使用计数器usage设置为2,用来表示描述符正在被使用而且其相应的进程处于活动状态。
* 进程状态既不是EXIT_ZOMBIE,也不是EXIT_DEAD
*/
atomic_set(&tsk->usage,2);
return tsk;
}
第(2)(3)部分可见下图,需要注意的是,在(3)中实现了很重要的一步,也就是修改了子进程的返回值,将其强置为为0。
函数整个调用过程如下图:
其中一些重要数据结构如下图: