1.概述
- 基于linux-2.6.11.1
- Linux系统中采用了与其他操作系统不同的方式创建进程。Linux将进程的创建的两个步骤分解为fork()和exec()。
- fork()通过拷贝当前进程创建一个子进程。其与父进程的区别仅仅在于PID(每个进程唯一)、PPID(父进程进程号)和某些资源和统计量(如:挂起的信号,子进程不继承)。
- exec()函数负责读取可执行文件并将其载入地址空间开始运行。
2.写时拷贝(copy-on-write)
2.1 背景
- 传统的fork()系统调用直接把所有的资源复制给新创建的进程。这种实现过于简单且效率低下,因为它拷贝的数据也许并不共享,更糟的情况是,如果新进程打算立即执行一个新的映像,那么所有的拷贝都将前功尽弃。
2.2 概述
- Linux的fork()使用写时拷贝(copy-on-write)页实现。写时拷贝时一种可以推迟甚至免除拷贝数据的技术。内核此时并不复制整个进程地址空间,而是让父进程和子进程共享同一个拷贝。
- 只有需要写入的时候,数据才会被复制,在此之前,只是以只读方式共享。这种技术是地址空间上的页的拷贝被推迟到实际发生写入的时候才进行。在页根本不会被写入的情况下(举例来说,fork()后立即调用exec())它们就无须复制了。
- 在fork之后
exec之前
两个进程用的是相同的物理空间(内存区),子进程的代码段、数据段、堆栈都是指向父进程的物理空间
,也就是说,两者的虚拟空间不同
,但其对应的物理空间是同一个
。当父子进程中有更改相应段的行为发生时
,再为子进程相应的段分配物理空间
,如果不是因为exec,内核会给子进程的数据段、堆栈段分配相应的物理空间
(至此两者有各自的进程空间,互不影响),而代码段继续共享父进程的物理空间(两者的代码完全相同)。而如果是因为exec,由于两者执行的代码不同,子进程的代码段也会分配单独的物理空间
。 - 还有个细节问题就是,fork之后内核会通过将子进程放在队列的前面,以让子进程先执行,以免父进程执行导致写时复制,而后子进程执行exec系统调用,因无意义的复制而造成效率的下降。
3. clone()、fork()及vfork()系统调用
- 如上图可看出无论是fork(),vfork(),pthread_create(),API都是通过系统调用陷入内核最后在do_fork()中完成进程的创建,这
其中的区别就在于传入的参数标志
而已。
3.1 参数flags含义
标志 | 说明 |
---|---|
CLONE_VM | 共享内存描述符和所有的页表 |
CLONE_FS | 共享根目录和当前工作目录所在的表,以及用于屏蔽新文件初始许可权的位掩码值(所谓文件的umask) |
CLONE_FILES | 共享打开文件表 |
CLONE_SIGHAND | 共享信号处理程序的表、阻塞信号表和挂起信号表。如果这个标志为true,就必须设置CLONE_VM标志。 |
CLONE_PTRACE | 如果父进程被跟踪,那么,子进程也被跟踪。尤其是,debugger程序可能希望以自己作为父进程来跟踪子进程,在这种情况下,内核把该标志强置为1。 |
CLONE_VFORK | 在发出vfork()系统调用时设置 |
CLONE_PARENT | 设置子进程的父进程(进程描述符中的parent和real_parent字段)为调用进程的父进程。 |
CLONE_THREAD | 把子进程插入到父进程的同一线程组中,并迫使子进程共享父进程的信号描述符。因此也设置子进程的tgid字段和group_leader字段。如果这个标志位为true,就必须设置CLONE_SIGHAND标志。 |
CLONE_NEWNS | 当clone需要自己的命名空间时(即它自己的已挂载文件系统视图)设置这个标志。不能同时设置CLONE_NEWNS和CLONE_FS |
CLONE_SYSVSEM | 共享 System V IPC取消信号的操作 |
CLONE_SETTLS | 为轻量级进程创建新 的线程局部存储段(TLS),该段由参数tls所指向的结构进程描述。 |
CLONE_PARENT_SETTID | 把子进程的PID写入由ptid参数所指向的父进程的用户态变量 |
CLONE_CHILD_CLEARTID | 如果该标志被设置,则内核建立一种触发机制,用在子进程要退出或要开始执行新程序时。在这些情况下,内核将清除由参数ctid所指向的用户态变量,并唤醒等待这个事件的任何进程。 |
CLONE_DETACHED | 遗留标志,内核会忽略它。 |
CLONE_UNTRACED | 内核设置这个标志以使CLONE_PTRACE标志失去作用(用来禁止内核线程跟踪进程) |
CLONE_CHILD_SETTID | 把子进程的PID写入由ctid参数所指向的子进程的用户态变量中 |
CLONE_STOPPED | 强迫子进程开始于TASK_STOPPED状态 |
3.2 clone()
- clone()——>do_fork(clone_flags, newsp, regs, 0, parent_tid, child_tid);
- 在Linux中,轻量级进程(线程)是由clone的函数创建的。函数原型可以通过man clone查看
#include <sched.h>
int clone(int (*fn)(void *), void *child_stack,
int flags, void *arg, ...
/* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ );
参数 | 含义 |
---|---|
fn | 指定一个由新进程执行的函数。当这个函数返回时,子进程终止。函数返回一个整数,表示子进程的退出代码。 |
arg | 指向传递fn()函数的数据。 |
flags | 各种各样的信息。低字节指定子进程结束时发送到父进程的信号代码,通常选择SIGCHLD信号。剩余的3个字节给一clone标志组用于编码。如下表所示 |
child_stack | 表示把用户态堆栈指针赋给子进程的esp寄存器。调用进程(指调用clone()的父进程)应该总是为子进程分配新的堆栈。 |
tls | 表示线程局部存储段(TLS)数据结构的地址,该结构是为新轻量级进程定义的。只有在CLONE_SETTLS标志被设置时才有意义。 |
ptid | 表示父进程的用户态变量地址,该父进程具有与新轻量级进程相同的PID。只有在CLONE_PARENT_SETTID标志被设置时才有意义。 |
ctid | 表示新轻量级进程的用户态变量地址,该进程具有这一类进程的PID。只有在CLONE_CHILD_SETTID标志被设置时才有意义。 |
3.2 fork()
- fork()——>do_fork(SIGCHLD, regs->rsp, regs, 0, NULL, NULL);
- 传统的fork()系统调用用在Linux中是用clone()实现的,其中clone()的flags参数指定为SIGCHLD信号及所有清0的clone标志,而它的child_stack参数是父进程当前的堆栈指针。即——>父进程和子进程暂时共享同一个用户堆栈。
- 但是,要感谢写时复制机制,通常只要父子进程中有一个试图去改变栈,则立即各自得到用户态堆栈的一份拷贝。
#include <unistd.h>
pid_t fork(void);
3.3 vfork()
- vfork()——>do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs->rsp, regs, 0, NULL, NULL);
- vfork()系统调用在Linux也是用clone()实现的。
- 其中flags指定为SIGCHLD信号和CLONE_VM及CLONE_VFORK标志。
- clone()的参数child_stack等于父进程当前的栈指针。
#include <sys/types.h>
#include <unistd.h>
pid_t vfork(void);
3.4 do_fork()
- do_fork()函数负责处理clone()、fork()和vfork()系统调用。
参数 | 说明 |
---|---|
clone_flags | 与clone()的参数flags相同 |
stack_start | 与clone()的参数child_stack相同 |
regs | 指向通用寄存器值的指针,通用寄存器的值是在从用户态切换到内核态时被保存到内核堆栈中的 |
stack_size | 未使用(总是被设置为0) |
parent_tidptr,child_tidptr | 与clone()中的对应参数ptid和ctid相同。 |
- do_fork()利用辅助函数copy_process()来创建进程描述符以及子进程执行所需要的所有其他内核数据结构。简化流程图如下所示:
3.5 copy_process()
- copy_process()创建进程描述符以及子进程执行所需要的所有其他数据结构。它的参数与do_fork()的参数相同,外加子进程的PID。下面描述copy_process()的重要步骤:
3.5.1 dup_task_struct()
static struct task_struct *dup_task_struct(struct task_struct *orig)
{
struct task_struct *tsk;
struct thread_info *ti;
/* a.如果需要,则在当前进程中调用__unlazy_fpu(), */
/* 把FPU、MMX和SSE/SSE2 寄存器的内容保存到父进程
* 的thread_info结构中。稍后,dup_taskstruct()
* 将把这些值复制到子进程的thread_info结构中。*/
prepare_to_copy(orig);
/* b.执行alloc_task_struct()宏,为新进程获取进程描述符 */
/* (task_struct 结构),并将描述符地址保存在tsk局部变量中。 */
tsk = alloc_task_struct();
if (!tsk)
return NULL;
/* c.执行alloc_thread_info宏以获取一块空闲内存区, */
/* 用来存放新进程的 thread_info结构和内核栈, */
/* 并将这块内存区字段的地址存在局部变量ti中。 */
/* 正如在本章前面“标识一个进程”一节中所述:这块内存区字段的大小是8KB或4KB。 */
ti = alloc_thread_info(tsk);
if (!ti) {
free_task_struct(tsk);
return NULL;
}
/* d.将current进程描述符的内容复制到tsk所指向的 */
/* task_struct结构中,然后把tsk->thread_info置为ti。 */
*ti = *orig->thread_info;
*tsk = *orig;
tsk->thread_info = ti;
/* e.把current进程的thread_info描述符的内容复制 */
/* 到ti所指向的结构中,然后把ti->task置为tsk。 */
ti->task = tsk;
/* One for us, one for whoever does the "release_task()" (usually parent) */
/* f.把新进程描述符的使用计数器(tsk->usage)置为2, */
/* 用来表示进程描述符正在被使用而且其相应的进程 */
/* 处于活动状态(进程状态即不是EXIT_ZOMBIE,也不是EXIT_DEAD)。 */
atomic_set(&tsk->usage,2);
/* g. 返回新进程的进程描述符指针(tsk)。 */
return tsk;
}
3.5.2 copy_flags()
static inline void copy_flags(unsigned long clone_flags, struct task_struct *p)
{
unsigned long new_flags = p->flags;
/* a.首先清除PF_SUPERPRIV标志,
* 该标志表示进程是否使用 了某种超级用户权限。
*/
new_flags &= ~PF_SUPERPRIV;
/* b.然后设置 PF_FORKNOEXEC标志, */
/* 它表示子进程还没有发出execve()系统调用 */
new_flags |= PF_FORKNOEXEC;
/* c.如果不需要跟踪子进程(没有设置CLONE_PTRAC标志), */
/* 就把tsk->ptrace字段设置为0。tsk->ptrace字段会 */
/* 存放一些标志,而这些标志是在一个进程被另外一个 */
/* 进程跟踪时才会用到的。采用这种方式 */
/* 即使当前进程被跟踪,子进程也不会被跟踪。 */
if (!(clone_flags & CLONE_PTRACE))
p->ptrace = 0;
p->flags = new_flags;
}
3.5.3 copy_process()主体结构
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;
/* 1.检查参数clone_flags所传递标志的一致性。 */
/* 尤其是,在下列情况下,它返回错误代号: */
/* a.CLONE_NEWNS 和CLONE_FS标志都被设置。 */
if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS))
return ERR_PTR(-EINVAL);
/*
* Thread groups must share signals as well, and detached threads
* can only be started up within the thread group.
*/
/* b.CLONE_THREAD标志被设置,但CLONE_SIGHAND标志被清0 */
/* (同一线程组中的轻量级进程必须共享信号)。 */
if ((clone_flags & CLONE_THREAD) && !(clone_flags & CLONE_SIGHAND))
return ERR_PTR(-EINVAL);
/*
* Shared signal handlers imply shared VM. By way of the above,
* thread groups also imply shared VM. Blocking this case allows
* for various simplifications in other code.
*/
/* c.CLONE_SIGHAND标志被设置,但CLONE_VM被清0 */
/* (共享信号处理程序的轻量级进程也必须共享内存描述符)。 */
if ((clone_flags & CLONE_SIGHAND) && !(clone_flags & CLONE_VM))
return ERR_PTR(-EINVAL);
/* 2.通过调用 security_task_create()以及稍后调用的 */
/* security_task_alloc()执行所有附加的安全检查。 */
/* Linux 2.6提供扩展安全性的钩子函数,与传统Unix相比, */
/* 它具有更加强壮的安全模型。 */
retval = security_task_create(clone_flags);
if (retval)
goto fork_out;
/* 3.调用dup_task_struct()为子进程获取进程描述符。 */
/* 该函数执行如下操作: */
retval = -ENOMEM;
p = dup_task_struct(current);
if (!p)
goto fork_out;
retval = -EAGAIN;
/* 4.检查存放在current->signal->rlim[RLIMIT_NPROC].rlim_cur */
/* 变量中的值是否小于或等于用户所拥有的进程数。 */
/* 如果是,则返回错误码,除非进程有root权限。 */
/* 该函数从每用户数据结构user_struct 中获取用户所拥有的进程数。 */
/* 通过进程描述符user字段的指针可以找到这个数据结构。 */
if (atomic_read(&p->user->processes) >=
p->signal->rlim[RLIMIT_NPROC].rlim_cur) {
if (!capable(CAP_SYS_ADMIN) && !capable(CAP_SYS_RESOURCE) &&
p->user != &root_user)
goto bad_fork_free;
}
/* 5.递增user_struct结构的使用计数器(tsk->user->__count 字段) */
/* 和用户所拥有的进程的计数器(tsk->user->processes) */
atomic_inc(&p->user->__count);
atomic_inc(&p->user->processes);
get_group_info(p->group_info);
/*
* If multiple threads are within copy_process(), then this check
* triggers too late. This doesn't hurt, the check is only there
* to stop root fork bombs.
*/
/* 6.检查系统中的进程数量(存放在nr_threads变量中) */
/* 是否超过max_threads变量的值。 */
/* 这个变量的缺省值取决于系统内存容量的大小。 */
/* 总的原则是:所有thread_info描述符和内核栈所占用的空间不能超过物理内存大小的1/8。 */
/* 不过,系统管理员可以通过写/proc/sys/kernel/threads-max文件来改变这个值。 */
if (nr_threads >= max_threads)
goto bad_fork_cleanup_count;
/* 7.如果实现新进程的执行域和可执行格式的内核函数 */
/* 都包含在内核模块中,则递增它们的使用计数器 */
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;
/* 8.a.把tsk->did_exec字段初始化为0: */
/* 它记录了进程发出的 execve()系统调用的次数。 */
p->did_exec = 0;
/* 8.b.更新从父进程复制到tsk>flags字段中的一些标志: */
/* 首先清除PF_SUPERPRIV标志,该标志表示进程是否使用 */
/* 了某种超级用户权限。然后设置 PF_FORKNOEXEC标志, */
/* 它表示子进程还没有发出execve()系统调用 */
copy_flags(clone_flags, p);
/* 9 把新进程的 PID 存入 tsk->pid字段 */
p->pid = pid;
retval = -EFAULT;
/* 10.如果clone_flags参数中的 */
/* CLONE_PARENT_SETTID标志被设置,就把子进程 */
/* 的PID复制到参数parent_tidptr指向的用户态变量中 */
if (clone_flags & CLONE_PARENT_SETTID)
if (put_user(p->pid, parent_tidptr))
goto bad_fork_cleanup;
p->proc_dentry = NULL;
/* 11.a 初始化子进程描述符中的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);
/* 11.b 把大内核锁计数器tsk->lock_depth初始化为-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 all the process information */
/* 12.调用copy_semundo(),copy_files(), */
/* copy_fs(),copy_sighand(),copy_signal(), */
/* copy_mm()和copy_namespace()来创建新的数据结构, */
/* 并把父进程相应数据结构的值复制到新数据结构中, */
/* 除非clone_flags参数指出它们有不同的值。 */
if ((retval = copy_semundo(clone_flags, p)))
goto bad_fork_cleanup_audit;
if ((retval = copy_files(clone_flags, p)))
goto bad_fork_cleanup_semundo;
if ((retval = copy_fs(clone_flags, p)))
goto bad_fork_cleanup_files;
if ((retval = copy_sighand(clone_flags, p)))
goto bad_fork_cleanup_fs;
if ((retval = copy_signal(clone_flags, p)))
goto bad_fork_cleanup_sighand;
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;
/* 13.调用copy_thread(),用发出clone()系统调用时 */
/* CPU寄存器的值(这些值已经被保存在父进程的内核栈中)
* 来初始化子进程的内核栈。不过,copy_thread()把eax寄存器对应字段的值 */
/* [这是fork()和 clone()系统调用在直是否 子进程中的返回值] */
/* 字段强行置为0。子进程描述符的thread.esp字段初始化为子, */
/* 进程内核栈的基地址,汇编语言函数(ret_from_fork()) */
/* 的地址存放在thread.eip字段中。如果父进程使用I/O权限位图, */
/* 则子进程获取该位图的一个拷贝。最后,如果CLONE_SETTLS标志被设置, */
/* 则子进程获取由clone()系统调用的参数tls指向的用户态数据结构所表示的TLS段。 */
retval = copy_thread(0, clone_flags, stack_start, stack_size, p, regs);
if (retval)
goto bad_fork_cleanup_namespace;
/* 14.如果clane_flags参数的值被置为CLONE_CHILD_SETTID */
/* 或CLONE_CHILD_CLEARTID,就把child_tidptr参数的值分别 */
/* 复制到tsk->set_child_tid 或tsk->clear_child_tid字段。 */
/* 这些标志说明:必须改变子进程用户态地址空间的 */
/* child_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;
/*
* Syscall tracing should be turned off in the child regardless
* of CLONE_PTRACE.
*/
/* 15.清除子进程thread_info结构的TIF_SYSCALL_TRACE标志, */
/* 以使ret_fromfork()函数不会把系统调用结束的消息通知给 */
/* 调试进程。(因为对子进程的跟踪是由tsk->ptrace中的 */
/* PTRACE_SYSCALL标志来控制的,所以子进程的系统调用跟踪不会被禁用。) */
clear_tsk_thread_flag(p, TIF_SYSCALL_TRACE);
/* Our parent execution domain becomes current domain
These must match for thread signalling to apply */
p->parent_exec_id = p->self_exec_id;
/* ok, now we should be set up.. */
/* 16.用clone_flags参数低位的信号数字编码 */
/* 初始化tsk-sexit_signal字段,如果CLONE_THREAD */
/* 标志被置位,就把tsk->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 */
/* 17,调用sched_fork()完成对新进程调度程序数据结构的初始化。 */
/* 该函数把新进程的状态设置为TASK_RUNNING, */
/* 并把thread_info结构的 preempt_count 字段设置为1, */
/* 从而禁止内核抢占。此外,为了保证公平的进程调度, */
/* 该函数在父子进程之间共享父进程的时间片。 */
sched_fork(p);
/*
* Ok, make it visible to the rest of the system.
* We dont wake it up yet.
*/
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);
/*
* The task hasn't been attached yet, so cpus_allowed mask cannot
* have changed. The cpus_allowed mask of the parent may have
* changed after it was copied first time, and it may then move to
* another CPU - so we re-copy it here and set the child's CPU to
* the parent's CPU. This avoids alot of nasty races.
*/
p->cpus_allowed = current->cpus_allowed;
/* 18.把新进程的thread_info结构的cpu字段 */
/* 设置为由smp_processor_ia()所返回的本地CPU号。 */
set_task_cpu(p, smp_processor_id());
/*
* Check for pending SIGKILL! The new thread should not be allowed
* to slip out of an OOM kill. (or normal SIGKILL.)
*/
if (sigismember(¤t->pending.signal, SIGKILL)) {
write_unlock_irq(&tasklist_lock);
retval = -EINTR;
goto bad_fork_cleanup_namespace;
}
/* CLONE_PARENT re-uses the old parent */
/* 19.初始化表示亲子关系的字段。尤其是, */
/* 如果CLONE_PARENT或CLONE_THREAD被设置, */
/* 就用current->realparent的值初始化 */
/* tsk->real_parent和tsk->parent,因此, */
/* 子进程的父进程似乎是当前进程的父进程。 */
/* 否则,把tsk->real_parent和tsk->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);
/*
* Important: if an exit-all has been started then
* do not create this new thread - the whole thread
* group is supposed to exit anyway.
*/
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) {
/*
* There is an all-stop in progress for the group.
* We ourselves will stop as soon as we check signals.
* Make the new thread part of that group stop too.
*/
current->signal->group_stop_count++;
set_tsk_thread_flag(p, TIF_SIGPENDING);
}
spin_unlock(¤t->sighand->siglock);
}
/* 20.执行SET_LINKS宏,把新进程描述符插入进程链表。 */
SET_LINKS(p);
/* 21.如果子进程必须被跟踪(tsk->ptrace字段的PT_PTRACED标志被设置), */
/* 就把 curent->parent 赋给tsk->prent,并将子进程插人调试程序的跟踪链表中。 */
if (unlikely(p->ptrace & PT_PTRACED))
__ptrace_link(p, current->parent);
/* 22.调用attach_pid()把新进程描述符的 */
/* PID插入pidhash[PIDTYPE_PID]散列表 */
attach_pid(p, PIDTYPE_PID, p->pid);
attach_pid(p, PIDTYPE_TGID, p->tgid);
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)++;
}
/* 23.现在,新进程已经被加入进程集合:递增nr_threads 变量的值。 */
nr_threads++;
/* 24.递增total_forks变量以记录被创建的进程的数量 */
total_forks++;
write_unlock_irq(&tasklist_lock);
retval = 0;
fork_out:
if (retval)
return ERR_PTR(retval);
/* 25.终止并返回子进程描述符指针(tsk)。 */
return p;
bad_fork_cleanup_namespace:
exit_namespace(p);
bad_fork_cleanup_keys:
exit_keys(p);
bad_fork_cleanup_mm:
if (p->mm)
mmput(p->mm);
bad_fork_cleanup_signal:
exit_signal(p);
bad_fork_cleanup_sighand:
exit_sighand(p);
bad_fork_cleanup_fs:
exit_fs(p); /* blocking */
bad_fork_cleanup_files:
exit_files(p); /* blocking */
bad_fork_cleanup_semundo:
exit_sem(p);
bad_fork_cleanup_audit:
audit_free(p);
bad_fork_cleanup_security:
security_task_free(p);
bad_fork_cleanup_policy:
#ifdef CONFIG_NUMA
mpol_free(p->mempolicy);
#endif
bad_fork_cleanup:
if (p->binfmt)
module_put(p->binfmt->module);
bad_fork_cleanup_put_domain:
module_put(p->thread_info->exec_domain->module);
bad_fork_cleanup_count:
put_group_info(p->group_info);
atomic_dec(&p->user->processes);
free_uid(p->user);
bad_fork_free:
free_task(p);
goto fork_out;
}
4.内核线程
4.1 概述
- 线程机制是现代编程技术中的一种抽象概念。
- 线程机制共享运行空间,打开的文件,代码,数据。
- 线程机制不共享,寄存器和栈空间。
- 从内核的角度来看,是没有线程的实例数据结构进行表征的。Linux把所有的线程都当做进程来实现。
线程仅仅被视为一个与其他进程共享某些资源的进程。
- 内核线程在以下几个方面不同于普通进程:
- 内核线程只运行在内核态,而普通进程既可以运行在内核态,也可以运行在用户态。
- 因为内核线程只运行在内核态,它们只使用大于PAGE_0FFSET的线性地址空间另一方面,不管在用户态还是在内核态,普通进程可以用4GB的线性地址空间。
4.2 创建一个内核线程
-
kernel_thread()函数创建一个新的内核线程,它接受的参数有:
- 所要执行的内核函数的地址(fn)。
- 要传递给函数的参数 (arg)。
- 一组clone标志(flags)。
-
该函数本质上以下面的方式调用 do_fork():
- do_fork(flags|CLONE_VM|CLONE_UNTRACED,0, pregs,0, NULL, NULL);
- CLONE_VM标志避免复制调用进程的页表:由于新内核线程无论如何都不会访问用户态地址空间,所以这种复制无疑会造成时间和空间的浪费。
- CLONE_UNTRACED标志保证不会有任何进程跟踪新内核线程,即使调用进程被跟踪。
-
传递给 do_fork()的参数pregs表示内核栈的地址,copy_thread()函数将从这里找到为新线程初始化CPU寄存器的值。kernel_thread()函数在这个栈中保留寄存器值的目的是:
- 通过copy_thread()把ebx和edx分别设置为参数fn和arg的值。
- 把eip寄存器的值设置为下面汇编语言代码段的地址:
- movl %edx,%eax
- push %edx
- call %ebx
- pushl %eax
- call do_exit
-
因此,新的内核线程开始执行fn(arg)函数,如果该函数结束。内核线程执行系统调用_exit(),并把fn()的返回值传递给它(“撤消进程”)。
4.3 进程0
-
所有进程的祖先叫做进程0,idle进程 或因为历史的原因叫做swapper进程,它是在 Linux的初始化阶段从无到有创建的一个内核线程(参见附录一)。这个祖先进程使用下列静态分配的数据结构(所有其他进程的数据结构都是动态分配的):
- 存放在init_thread_union变量中的thread_info描述符和内核堆栈,由 INIT_THREAD_INFO宏完成对它们的初始化。
- 由进程描述符指向的下列表:
- init_mm
- init_fs
- init_files
- init_signals
- init_sighand
- 这些表分别由以下宏初始化:
- INIT_VM
- INIT_FS
- INIT_FILES
- INIT_SIGNALS
- INIT_SIGHAND
- 主内核页全局目录存放在swapper_pg_dir中。
-
start_kernel()函数初始化内核需要的所有数据结构,激活中断,创建另一个叫进程1的内核线程(一般叫做init进程):
- kernel_thread(init,NULL,CLONE_FS|CLONE_SIGHAND);
-
新创建内核线程的PID为1,并与进程0共享每进程所有的内核数据结构。此外,当调度程序选择到它时,init进程开始执行init()函数。
-
创建init进程后,进程0执行cpu_idle()函数,该函数本质上是在开中断的情况下重复执行hlt汇编语言指令。只有当没有其他进程处于TASK_RUNNING状态时,调度程序才选择进程0。
-
在多处理器系统中,每个CPU都有一个进程0。只要打开机器电源,计算机的BIOS就启动某一个CPU,同时禁用其他CPU。 运行在CPU0上的swapper进程初始化内核数据结构,然后激活其他的CPU,并通过copy_process()函数创建另外的swapper进程,把0传递给新创建的swapper进程作为它们的新PID。此外,内核把适当的CPU索引赋给内核所创建的每个进程的thread_info描述符的 cpu字段。
4.4 进程1
- 由进程0创建的内核线程执行init()函数,init()依次完成内核初始化。init()调用 execve()系统调用装入可执行程序init。结果,init内核线程变为一个普通进程,且拥有自己的每进程(per-process)内核数据结构。在系统关闭之前,init进程一直存活,因为它创建和监控在操作系统外层执行的所有进程的活动。
4.5 其他内核线程
- Linux使用很多其他内核线程。其中一些在初始化阶段创建,一直运行到系统关闭;而其他一些在内核必须执行一个任务时“按需”创建,这种任务在内核的执行上下文中得到很好的执行。
- 一些内核线程的例子(除了进程0和进程1)是:
- keventd(也被称为事件) ——> 执行keventd_wq工作队列中的函数。
- kapmd ——> 处理与高级电源管理(APM)相关的事件。
- kswapd ——> 执行内存回收,详情了解“周期回收”。
- pdflush ——> 刷新“脏”缓冲区中的内容到磁盘以回收内存,详情了解“pdflush内核线程”。
- kblockd ——> 执行kblockd_workqueue工作队列中的函数。实质上,它周期性地激活块设备驱动程序,详情了解“激活块设备驱动程序”。
- ksoftirqd ——> 运行tasklet (详情了解“软中断及tasklet”),系统中每个CPU都有这样一个内核线程。