深入理解 Linux 内核1:https://developer.aliyun.com/article/1597354
(2)__switch_to() 函数
// arch/x86/kernel/process_64.c struct task_struct * __switch_to(struct task_struct *prev_p, struct task_struct *next_p) { struct thread_struct *prev = &prev_p->thread; struct thread_struct *next = &next_p->thread; int cpu = smp_processor_id(); struct tss_struct *tss = &per_cpu(init_tss, cpu); unsigned fsindex, gsindex; bool preload_fpu; /* * If the task has used fpu the last 5 timeslices, just do a full * restore of the math state immediately to avoid the trap; the * chances of needing FPU soon are obviously high now */ preload_fpu = tsk_used_math(next_p) && next_p->fpu_counter > 5; /* we're going to use this soon, after a few expensive things */ if (preload_fpu) prefetch(next->xstate); /* * Reload esp0, LDT and the page table pointer: */ load_sp0(tss, next); /* * Switch DS and ES. * This won't pick up thread selector changes, but I guess that is ok. */ savesegment(es, prev->es); if (unlikely(next->es | prev->es)) loadsegment(es, next->es); savesegment(ds, prev->ds); if (unlikely(next->ds | prev->ds)) loadsegment(ds, next->ds); /* We must save %fs and %gs before load_TLS() because * %fs and %gs may be cleared by load_TLS(). * * (e.g. xen_load_tls()) */ savesegment(fs, fsindex); savesegment(gs, gsindex); load_TLS(next, cpu); /* Must be after DS reload */ unlazy_fpu(prev_p); /* Make sure cpu is ready for new context */ if (preload_fpu) clts(); /* * Leave lazy mode, flushing any hypercalls made here. * This must be done before restoring TLS segments so * the GDT and LDT are properly updated, and must be * done before math_state_restore, so the TS bit is up * to date. */ arch_end_context_switch(next_p); /* * Switch FS and GS. * * Segment register != 0 always requires a reload. Also * reload when it has changed. When prev process used 64bit * base always reload to avoid an information leak. */ if (unlikely(fsindex | next->fsindex | prev->fs)) { loadsegment(fs, next->fsindex); /* * Check if the user used a selector != 0; if yes * clear 64bit base, since overloaded base is always * mapped to the Null selector */ if (fsindex) prev->fs = 0; } /* when next process has a 64bit base use it */ if (next->fs) wrmsrl(MSR_FS_BASE, next->fs); prev->fsindex = fsindex; if (unlikely(gsindex | next->gsindex | prev->gs)) { load_gs_index(next->gsindex); if (gsindex) prev->gs = 0; } if (next->gs) wrmsrl(MSR_KERNEL_GS_BASE, next->gs); prev->gsindex = gsindex; /* * Switch the PDA and FPU contexts. */ prev->usersp = percpu_read(old_rsp); percpu_write(old_rsp, next->usersp); percpu_write(current_task, next_p); percpu_write(kernel_stack, (unsigned long)task_stack_page(next_p) + THREAD_SIZE - KERNEL_STACK_OFFSET); /* * Now maybe reload the debug registers and handle I/O bitmaps */ if (unlikely(task_thread_info(next_p)->flags & _TIF_WORK_CTXSW_NEXT || task_thread_info(prev_p)->flags & _TIF_WORK_CTXSW_PREV)) __switch_to_xtra(prev_p, next_p, tss); /* * Preload the FPU context, now that we've determined that the * task is likely to be using it. */ if (preload_fpu) __math_state_restore(); return prev_p; }
(a)分析
_switch_to() 函数执行大多数开始于 switch_to() 宏的进程切换。这个函数作用于 prev_p 和 next_p 参数,这两个参数表示前一个进程和新进程。这个函数的调用不同于一般函数的调用,因为 _switch_to() 从 eax 和 edx 取参数 prev_p 和 next_p(我们在前面已看到这些参数就是保存在那里),而不像大多数函数一样从栈中取参数。为了强迫函数从寄存器取它的参数,内核利用 __attribute__ 和 regparm 关键字,这两个关键字是 C 语言非标准的扩展名,由 gcc 编译程序实现。在 include/asm-i386/system.h 头文件中,__switch_to() 函数的声明如下:
__switch_to(struct task_struct *prev_p, struct task_struct *next_p) _attribute__(regparm(3));
函数执行的步骤如下:
- 执行由 __unlazy_fpu() 宏产生的代码(参见本章稍后 “保存和加载 FPU、MMX 及 XMM 寄存器” 一节),以有选择地保存 prev_p 进程的 FPU、MMX 及 XMM 寄存器的内容。
__unlazy_fpu(prev_p);
- 执行 smp_processor_id() 宏获得本地(local) CPU 的下标,即执行代码的 CPU。该宏从当前进程的 thread_info 结构的 cpu 字段获得下标并将它保存到 cpu 局部变量。
- 把 next_p->thread.esp0 装入对应于本地 CPU 的 TSS 的 esp0 字段,我们将在第十章的 “通过 sysenter 指令发生系统调用” 一节看到,以后任何由 sysenter 汇编指令产生的从用户态到内核态的特权级转换将把这个地址拷贝到 esp 寄存器中:
init_tss[cpu].esp0 = next_p->thread.esp0;
- 把 next_p 进程使用的线程局部存储(TLS)段装入本地 CPU 的全局描述符表; 三个段选择符保存在进程描述符内的 tls_array 数组中(参见第二章的 “Linux 中的分段” 一节)。
cpu_gdt_table[cpu][6] = next_p->thread.tls_array[0]; cpu_gdt_table[cpu][7] = next_p->thread.tls_array[1]; cpu_gdt_table[cpu][8] = next_p->thread.tls_array[2];
- 把 fs 和 gs 段寄存器的内容分别存放 prev_p->thread.fs 和 prev_p->thread.gs 中,对应的汇编语言指令是:
movl %fs, 40(%esi) movl %gs, 44(%esi)
esi 寄存器指向 prev_p->thread 结构。
- 如果 fs 或 gs 段寄存器已经被 prev_p 或 next_p 进程中的任意一个使用(也就是说如果它们有一个非 0 的值),,则将 next_p 进程的 thread_struct 描述符中保存的值装入这些寄存器中。这一步在逻辑上补充了前一步中执行的操作。主要的汇编语言指令如下:
movl 40(%ebx),%fs movl 44(%ebx),%gs
ebx 寄存器指向 next_p->thread 结构。代码实际上更复杂,因为当它检测到一个无效的段寄存器值时,CPU 可能产生一个异常。代码采用一种 “修正(fix-up)” 途径来考虑这种可能性(参见第十章"动态地址检查:修正代码" 一节)。
- 用 next_p->thread.debugreg 数组的内容装载 dr0,…,dr7 中的 6 个调试寄存器(注 7)。只有在 next_p 被挂起时正在使用调试寄存器(也就是说,next_p->thread.debugreg[7] 字段不为 0),这种操作才能进行。这些寄存器不需要被保存,因为只有当一个调试器想要监控 prev 时 prev_p->thread.debugreg 才会被修改。
if (next_p->thread.debugreg[7]) { loaddebug(&next_p->thread, 0); loaddebug(&next_p->thread, 1); loaddebug(&next_p->thread, 2); loaddebug(&next_p->thread, 3); /* 没有 4 和 5*/ loaddebug(&next_p->thread, 6); loaddebug(&next_p->thread, 7); }
- 如果必要,更新 TSS 中的 I/O 位图。当 next_p 或 prev_p 有其自己的定制 I/O 权限位图时必须这么做:
if (prev_p->thread.io_bitmap_ptr || next_p->thread.io_bitmap_ptr) handle_io_bitmap(&next_p->thread, &init_tss[cpu]);
因为进程很少修改 I/O 权限位图,所以该位图在"懒"模式中被处理:当且仅当一个进程在当前时间片内实际访问 I/O 端口时,真实位图才被拷贝到本地 CPU 的 TSS中。进程的定制 I/O 权限位图被保存在 thread_info 结构的 io_bitmap_ptr 字段指向的缓冲区中。 handle_io_bitmap() 函数为 next_p 进程设置本地 CPU 使用的 TSS 的 io_bitmap 字段如下:
如果 next_p 进程不拥有自己的 I/O 权限位图,则 TSS 的 io_bitmap字段被设为 0x8000。
如果 next_p 进程拥有自己的 I/O 权限位图,则 TSS 的 io_bitmap 字段被设为 0x9000。
TSS 的 io_bitmap 字段应当包含一个在 TSS 中的偏移量,其中存放实际位图。无论何时用户态进程试图访问一个 I/O 端口,0x8000 和 0x9000 指向 TSS 界限之外并将因此引起 “General protection” 异常(参见第四章的 “异常” 一节)。do_general_protection() 异常处理程序将检查保存在 io_bitmap 字段的值;如果是 0x8000,函数发送一个 SIGSEGV 信号给用户态进程; 如果是 0x9000,函数把进程位图(由 thread_info 结构中的 io_bitmap_ptr 字段指示)拷贝到本地 CPU 的 TSS 中,把 io_bitmap 字段设为实际位图的偏移(104),并强制再一次执行有缺陷的汇编语言指令。
- 终止。 _switch_to() C函数通过使用下列声明结束:
return prev_p;
由编译器产生的相应汇编语言指令是:
movl %edi,%eax ret
prev_p 参数 (现在在 edi 中) 被拷贝到 eax,因为缺省情况下任何 C 函数的返回值被传递给 eax 寄存器。注意 eax 的值因此在调用 __switch_to() 的过程中被保护起来;这非常重要,因为调用 switch_to 宏时会假定 eax 总是用来存放将被替换的进程描述符的地址。
汇编语言指令 ret 把栈顶保存的返回地址装入eip 程序计数器。不过,通过简单地跳转到 __switch_to() 函数来调用该函数。因此,ret 汇编指令在栈中找到标号为 1 的指令的地址,其中标号为 1 的地址是由 switch_to() 宏推入栈中的。如果因为 next_p 第一次执行而以前从未被挂起,__switch_to() 就找到 ret_from_fork() 函数的起始地址。
4、创建进程
(1)do_fork() 函数
// kernel/fork.c 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; long nr; /* * Do some preliminary argument and permissions checking before we * actually start allocating stuff */ if (clone_flags & CLONE_NEWUSER) { if (clone_flags & CLONE_THREAD) return -EINVAL; /* hopefully this check will go away when userns support is * complete */ if (!capable(CAP_SYS_ADMIN) || !capable(CAP_SETUID) || !capable(CAP_SETGID)) return -EPERM; } /* * We hope to recycle these flags after 2.6.26 */ if (unlikely(clone_flags & CLONE_STOPPED)) { static int __read_mostly count = 100; if (count > 0 && printk_ratelimit()) { char comm[TASK_COMM_LEN]; count--; printk(KERN_INFO "fork(): process `%s' used deprecated " "clone flags 0x%lx\n", get_task_comm(comm, current), clone_flags & CLONE_STOPPED); } } /* * When called from kernel_thread, don't do user tracing stuff. */ if (likely(user_mode(regs))) trace = tracehook_prepare_clone(clone_flags); p = copy_process(clone_flags, stack_start, regs, stack_size, child_tidptr, NULL, trace); /* * 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); if (clone_flags & CLONE_VFORK) { p->vfork_done = &vfork; init_completion(&vfork); } audit_finish_fork(p); tracehook_report_clone(regs, clone_flags, nr, p); /* * We set PF_STARTING at creation in case tracing wants to * use this to distinguish a fully live task from one that * hasn't gotten to tracehook_report_clone() yet. Now we * clear it and set the child going. */ p->flags &= ~PF_STARTING; if (unlikely(clone_flags & CLONE_STOPPED)) { /* * We'll start up with an immediate SIGSTOP. */ sigaddset(&p->pending.signal, SIGSTOP); set_tsk_thread_flag(p, TIF_SIGPENDING); __set_task_state(p, TASK_STOPPED); } else { wake_up_new_task(p, clone_flags); } tracehook_report_clone_complete(trace, regs, clone_flags, nr, p); if (clone_flags & CLONE_VFORK) { freezer_do_not_count(); wait_for_completion(&vfork); freezer_count(); tracehook_report_vfork_done(p, nr); } } else { nr = PTR_ERR(p); } return nr; }
(2)copy_process() 函数
// kernel/fork.c static struct task_struct *copy_process(unsigned long clone_flags, unsigned long stack_start, struct pt_regs *regs, unsigned long stack_size, int __user *child_tidptr, struct pid *pid, int trace) { int retval; struct task_struct *p; int cgroup_callbacks_done = 0; 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. */ 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. */ if ((clone_flags & CLONE_SIGHAND) && !(clone_flags & CLONE_VM)) return ERR_PTR(-EINVAL); /* * Siblings of global init remain as zombies on exit since they are * not reaped by their parent (swapper). To solve this and to avoid * multi-rooted process trees, prevent global and container-inits * from creating siblings. */ if ((clone_flags & CLONE_PARENT) && current->signal->flags & SIGNAL_UNKILLABLE) return ERR_PTR(-EINVAL); retval = security_task_create(clone_flags); if (retval) goto fork_out; retval = -ENOMEM; p = dup_task_struct(current); if (!p) goto fork_out; ftrace_graph_init_task(p); rt_mutex_init_task(p); #ifdef CONFIG_PROVE_LOCKING DEBUG_LOCKS_WARN_ON(!p->hardirqs_enabled); DEBUG_LOCKS_WARN_ON(!p->softirqs_enabled); #endif retval = -EAGAIN; if (atomic_read(&p->real_cred->user->processes) >= task_rlimit(p, RLIMIT_NPROC)) { if (!capable(CAP_SYS_ADMIN) && !capable(CAP_SYS_RESOURCE) && p->real_cred->user != INIT_USER) goto bad_fork_free; } retval = copy_creds(p, clone_flags); if (retval < 0) goto bad_fork_free; /* * 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. */ retval = -EAGAIN; if (nr_threads >= max_threads) goto bad_fork_cleanup_count; if (!try_module_get(task_thread_info(p)->exec_domain->module)) goto bad_fork_cleanup_count; p->did_exec = 0; delayacct_tsk_init(p); /* Must remain after dup_task_struct() */ copy_flags(clone_flags, p); INIT_LIST_HEAD(&p->children); INIT_LIST_HEAD(&p->sibling); rcu_copy_process(p); p->vfork_done = NULL; spin_lock_init(&p->alloc_lock); init_sigpending(&p->pending); p->utime = cputime_zero; p->stime = cputime_zero; p->gtime = cputime_zero; p->utimescaled = cputime_zero; p->stimescaled = cputime_zero; #ifndef CONFIG_VIRT_CPU_ACCOUNTING p->prev_utime = cputime_zero; p->prev_stime = cputime_zero; #endif #if defined(SPLIT_RSS_COUNTING) memset(&p->rss_stat, 0, sizeof(p->rss_stat)); #endif p->default_timer_slack_ns = current->timer_slack_ns; task_io_accounting_init(&p->ioac); acct_clear_integrals(p); posix_cpu_timers_init(p); p->lock_depth = -1; /* -1 = no lock */ do_posix_clock_monotonic_gettime(&p->start_time); p->real_start_time = p->start_time; monotonic_to_bootbased(&p->real_start_time); p->io_context = NULL; p->audit_context = NULL; cgroup_fork(p); #ifdef CONFIG_NUMA p->mempolicy = mpol_dup(p->mempolicy); if (IS_ERR(p->mempolicy)) { retval = PTR_ERR(p->mempolicy); p->mempolicy = NULL; goto bad_fork_cleanup_cgroup; } mpol_fix_fork_child_flag(p); #endif #ifdef CONFIG_TRACE_IRQFLAGS p->irq_events = 0; #ifdef __ARCH_WANT_INTERRUPTS_ON_CTXSW p->hardirqs_enabled = 1; #else p->hardirqs_enabled = 0; #endif p->hardirq_enable_ip = 0; p->hardirq_enable_event = 0; p->hardirq_disable_ip = _THIS_IP_; p->hardirq_disable_event = 0; p->softirqs_enabled = 1; p->softirq_enable_ip = _THIS_IP_; p->softirq_enable_event = 0; p->softirq_disable_ip = 0; p->softirq_disable_event = 0; p->hardirq_context = 0; p->softirq_context = 0; #endif #ifdef CONFIG_LOCKDEP p->lockdep_depth = 0; /* no locks held yet */ p->curr_chain_key = 0; p->lockdep_recursion = 0; #endif #ifdef CONFIG_DEBUG_MUTEXES p->blocked_on = NULL; /* not blocked yet */ #endif #ifdef CONFIG_CGROUP_MEM_RES_CTLR p->memcg_batch.do_batch = 0; p->memcg_batch.memcg = NULL; #endif p->bts = NULL; /* Perform scheduler related setup. Assign this task to a CPU. */ sched_fork(p, clone_flags); retval = perf_event_init_task(p); if (retval) goto bad_fork_cleanup_policy; if ((retval = audit_alloc(p))) goto bad_fork_cleanup_policy; /* copy all the process information */ 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_namespaces(clone_flags, p))) goto bad_fork_cleanup_mm; if ((retval = copy_io(clone_flags, p))) goto bad_fork_cleanup_namespaces; retval = copy_thread(clone_flags, stack_start, stack_size, p, regs); if (retval) goto bad_fork_cleanup_io; if (pid != &init_struct_pid) { retval = -ENOMEM; pid = alloc_pid(p->nsproxy->pid_ns); if (!pid) goto bad_fork_cleanup_io; if (clone_flags & CLONE_NEWPID) { retval = pid_ns_prepare_proc(p->nsproxy->pid_ns); if (retval < 0) goto bad_fork_free_pid; } } p->pid = pid_nr(pid); p->tgid = p->pid; if (clone_flags & CLONE_THREAD) p->tgid = current->tgid; if (current->nsproxy != p->nsproxy) { retval = ns_cgroup_clone(p, pid); if (retval) goto bad_fork_free_pid; } 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; #ifdef CONFIG_FUTEX p->robust_list = NULL; #ifdef CONFIG_COMPAT p->compat_robust_list = NULL; #endif INIT_LIST_HEAD(&p->pi_state_list); p->pi_state_cache = NULL; #endif /* * sigaltstack should be cleared when sharing the same VM */ if ((clone_flags & (CLONE_VM|CLONE_VFORK)) == CLONE_VM) p->sas_ss_sp = p->sas_ss_size = 0; /* * Syscall tracing and stepping should be turned off in the * child regardless of CLONE_PTRACE. */ user_disable_single_step(p); clear_tsk_thread_flag(p, TIF_SYSCALL_TRACE); #ifdef TIF_SYSCALL_EMU clear_tsk_thread_flag(p, TIF_SYSCALL_EMU); #endif clear_all_latency_tracing(p); /* ok, now we should be set up.. */ p->exit_signal = (clone_flags & CLONE_THREAD) ? -1 : (clone_flags & CSIGNAL); p->pdeath_signal = 0; p->exit_state = 0; /* * Ok, make it visible to the rest of the system. * We dont wake it up yet. */ p->group_leader = p; INIT_LIST_HEAD(&p->thread_group); /* Now that the task is set up, run cgroup callbacks if * necessary. We need to run them before the task is visible * on the tasklist. */ cgroup_fork_callbacks(p); cgroup_callbacks_done = 1; /* Need tasklist lock for parent etc handling! */ write_lock_irq(&tasklist_lock); /* CLONE_PARENT re-uses the old parent */ if (clone_flags & (CLONE_PARENT|CLONE_THREAD)) { p->real_parent = current->real_parent; p->parent_exec_id = current->parent_exec_id; } else { p->real_parent = current; p->parent_exec_id = current->self_exec_id; } spin_lock(¤t->sighand->siglock); /* * Process group and session signals need to be delivered to just the * parent before the fork or both the parent and the child after the * fork. Restart if a signal comes in before we add the new process to * it's process group. * A fatal signal pending means that current will exit, so the new * thread can't slip out of an OOM kill (or normal SIGKILL). */ recalc_sigpending(); if (signal_pending(current)) { spin_unlock(¤t->sighand->siglock); write_unlock_irq(&tasklist_lock); retval = -ERESTARTNOINTR; goto bad_fork_free_pid; } if (clone_flags & CLONE_THREAD) { atomic_inc(¤t->signal->count); atomic_inc(¤t->signal->live); p->group_leader = current->group_leader; list_add_tail_rcu(&p->thread_group, &p->group_leader->thread_group); } if (likely(p->pid)) { tracehook_finish_clone(p, clone_flags, trace); if (thread_group_leader(p)) { if (clone_flags & CLONE_NEWPID) p->nsproxy->pid_ns->child_reaper = p; p->signal->leader_pid = pid; tty_kref_put(p->signal->tty); p->signal->tty = tty_kref_get(current->signal->tty); attach_pid(p, PIDTYPE_PGID, task_pgrp(current)); attach_pid(p, PIDTYPE_SID, task_session(current)); list_add_tail(&p->sibling, &p->real_parent->children); list_add_tail_rcu(&p->tasks, &init_task.tasks); __get_cpu_var(process_counts)++; } attach_pid(p, PIDTYPE_PID, pid); nr_threads++; } total_forks++; spin_unlock(¤t->sighand->siglock); write_unlock_irq(&tasklist_lock); proc_fork_connector(p); cgroup_post_fork(p); perf_event_fork(p); return p; bad_fork_free_pid: if (pid != &init_struct_pid) free_pid(pid); bad_fork_cleanup_io: if (p->io_context) exit_io_context(p); bad_fork_cleanup_namespaces: exit_task_namespaces(p); bad_fork_cleanup_mm: if (p->mm) mmput(p->mm); bad_fork_cleanup_signal: if (!(clone_flags & CLONE_THREAD)) __cleanup_signal(p->signal); bad_fork_cleanup_sighand: __cleanup_sighand(p->sighand); 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_policy: perf_event_free_task(p); #ifdef CONFIG_NUMA mpol_put(p->mempolicy); bad_fork_cleanup_cgroup: #endif cgroup_exit(p, cgroup_callbacks_done); delayacct_tsk_free(p); module_put(task_thread_info(p)->exec_domain->module); bad_fork_cleanup_count: atomic_dec(&p->cred->user->processes); exit_creds(p); bad_fork_free: free_task(p); fork_out: return ERR_PTR(retval); }
(a)分析
copy_process() 创建进程描述符以及子进程执行所需要的所有其他数据结构。它的参数与 do_fork() 的参数相同,外加子进程的 PID。下面描述 copy process() 的最重要的步骤:
- 检查参数 clone_flags 所传递标志的一致性。尤其是,在下列情况下,它返回错误代号:
- CLONE_NEWNS 和 CLONE_FS 标志都被设置。
- CLONE_THREAD 标志被设置,但 CLONE_SIGHAND 标志被清 0(同一线程组中的轻量级进程必须共享信号)。
- CLONE_SIGHAND 标志被设置,但 CLONE_VM 被清 0(共享信号处理程序的轻量级进程也必须共享内存描述符)。
- 通过调用 security_task_create() 以及稍后调用的 security_task_alloc() 执行所有附加的安全检查。Linux 2.6 提供扩展安全性的钩子函数,与传统 Unix 相比,它具有更加强壮的安全模型。详情参见第二十章。
3.调用 dup_task_struct() 为子进程获取进程描述符。该函数执行如下操作:
如果需要,则在当前进程中调用 __unlazy_fpu(),把 FPU、MMX 和 SSE/SSE2 寄存器的内容保存到父进程的 thread_info 结构中。稍后,dup_task_struct() 将把这些值复制到子进程的 thread_info 结构中。
执行 alloc_task_struct() 宏,为新进程获取进程描述符(task_struct 结构),并将描述符地址保存在 tsk 局部变量中。
执行 alloc_thread_info 宏以获取一块空闲内存区,用来存放新进程的 thread_info 结构和内核栈,并将这块内存区字段的地址存在局部变量 ti 中。正如在本章前面 “标识一个进程” 一节中所述:这块内存区字段的大小是 8KB 或 4KB 。
将 current 进程描述符的内容复制到 tsk 所指向的 task_struct 结构中,然后把 tsk->thread_info 置为 ti 。
把 current 进程的 thread_info 描述符的内容复制到 ti 所指向的结构中,然后把 ti->task 置为 tsk。
把新进程描述符的使用计数器(tsk->usage)置为 2,用来表示进程描述符正在被使用而且其相应的进程处于活动状态(进程状态即不是 EXIT_ZOMBIE,也不是 EXIT_DEAD)。
返回新进程的进程描述符指针(tsk)。
检查存放在 current->signal->rlim[RLIMIT_NPROC].rlim_cur 变量中的值是否小于或等于用户所拥有的进程数。如果是,则返回错误码,除非进程没有 root 权限。该函数从每用户数据结构 user_struct 中获取用户所拥有的进程数。通过进程描述符 user 字段的指针可以找到这个数据结构。
递增 user_struct 结构的使用计数器 (tsk->user->__count 字段)和用户所拥有的进程的计数器(tsk->user->processes)。
检查系统中的进程数量(存放在 nr_threads 变量中)是否超过 max_threads 变量的值。这个变量的缺省值取决于系统内存容量的大小。总的原则是:所有 thread_info 描述符和内核栈所占用的空间不能超过物理内存大小的 1/8。不过,系统管理员可以通过写 /proc/sys/kernel/threads-max 文件来改变这个值。
如果实现新进程的执行域和可执行格式的内核函数(参见第二十章)都包含在内核模块中,则递增它们的使用计数器(参见附录二)。
设置与进程状态相关的几个关键字段:
把大内核锁计数器 tsk->lock_depth 初始化为 -1(参见第五章 “大内核锁” 一节)。
把 tsk->did_exec 字段初始化为 0 :它记录了进程发出的 execve() 系统调用的次数。
更新从父进程复制到 tsk->flags 字段中的一些标志:首先清除 PF_SUPERPRIV 标志,该标志表示进程是否使用了某种超级用户权限。然后设置 PF_FORKNOEXEC 标志,它表示子进程还没有发出 execve() 系统调用。
把新进程的 PID 存入 tsk->pid 字段。
如果 clone_flags 参数中的 CLONE_PARENT_SETTID 标志被设置,就把子进程的 PID 复制到参数 parent_tidptr 指向的用户态变量中。
初始化子进程描述符中的 list_head 数据结构和自旋锁,并为与挂起信号、定时器及时间统计表相关的几个字段赋初值。
调用 copy_semundo(), copy_files(),copy_fs(),copy_sighand(),copy_signa1(), copy_mm() 和 copy_namespace() 来创建新的数据结构,并把父进程相应数据结构的值复制到新数据结构中,除非 clone_flags 参数指出它们有不同的值。
调用 copy_thread(),用发出 clone() 系统调用时 CPU 寄存器的值(正如第十章所述,这些值已经被保存在父进程的内核栈中)来初始化子进程的内核栈。不过,copy_thread() 把 eax 寄存器对应字段的值 [这是 fork() 和 clone() 系统调用在子进程中的返回值] 字段强行置为 0。子进程描述符的 thread.esp 字段初始化为子进程内核栈的基地址,汇编语言函数(ret_from_fork())的地址存放在 thread.eip 字段中。如果父进程使用 I/O 权限位图,则子进程获取该位图的一个拷贝。最后,如果 CLONE_SETTLS 标志被设置,则子进程获取由 clone() 系统调用的参数 tls 指向的用户态数据结构所表示的 TLS 段(注 9)。
如果 clone_flags 参数的值被置为 CLONE_CHILD_SETTID 或CLONE_CHILD_CLEARTID,就把 child_tidptr 参数的值分别复制到 tsk->set_chid_tid 或 tsk->clear_child_tid 字段。这些标志说明:必须改变子进程用户态地址空间的 child_tidptr 所指向的变量的值,不过实际的写操作要稍后再执行。
清除子进程 thread_info 结构的 TIF_SYSCALL_TRACE 标志,以使 ret_from_fork() 函数不会把系统调用结束的消息通知给调试进程(参见第十章 “进入和退出系统调用” 一节)。(为对子进程的跟踪是由 tsk->ptrace 中的 PTRACE_SYSCALL 标志来控制的,所以子进程的系统调用跟踪不会被禁用。)
用 clone_flags 参数低位的信号数字编码初始化 tsk->exit_signal 字段,如果 CLONE_THREAD 标志被置位,就把 tsk->exit_signal 字段初始化为 -1。正如我们将在本章稍后 “进程终止” 一节所看见的,只有当线程组的最后一个成员(通常是线程组的领头)“死亡”,才会产生一个信号,以通知线程组的领头进程的父进程。
调用 sched_fork() 完成对新进程调度程序数据结构的初始化。该函数把新进程的状态设置为 TASK_RUNNING,并把 thread_info 结构的 preempt_count 字段设置为 1,从而禁止内核抢占(参见第五章 “内核抢占” 一节)。此外,为了保证公平的进程调度,该函数在父子进程之间共享父进程的时间片(参见第七章 “scheduler_tick() 数” 一节)。
把新进程的 thread_info 结构的 cpu 字段设置为由 smp_processor_id() 所返回的本地 CPU 号。
初始化表示亲子关系的字段。尤其是,如果 CLONE_PARENT 或 CLONE_THREAD ,被设置,就用 current->real_parent 的值初始化 tsk->real_parent 和 tsk->parent,因此,子进程的父进程似乎是当前进程的父进程。否则,把 tsk->real_parent 和 tsk->parent 置为当前进程。
如果不需要跟踪子进程(没有设置 CLONE_PTRAC 标志),就把 tsk->ptrace 字段设置为 0 。tsk->ptrace 字段会存放一些标志,而这些标志是在一个进程被另外一个进程跟踪时才会用到的。采用这种方式,即使当前进程被跟踪,子进程也不会被跟踪。
执行 SET_LINKS 宏,把新进程描述符插入进程链表。
如果子进程必须被跟踪(tsk->ptrace 字段的 PT_PTRACED 标志被设置),就把 current->parent 赋给 tsk->parent,并将子进程插入调试程序的跟踪链表中。
调用 attach_pid() 把新进程插述符的 PID 插入 pidhash[PIDTYPE_PID] 散列表。
如果子进程是线程组的领头进程(CLONE_THREAD 标志被清 0):
把 tsk->tgid 的初值置为 tsk->pid。
把 tsk->group_leader 的初值置为 tsk。
调用三次 attach_pid() ,把子进程分别插入 PIDTYPE_TGID,PIDTYPE_PGID 和 PIDTYPE_SID 类型的 PID 散列表。
否则,如果子进程属于它的父进程的线程组(CLONE_THREAD 标志被设置):
把 tsk->tgid 的初值置为 tsk->current->tgid。
把 tsk->group_leader 的初值置为 current->group_leader 的值。
调用 attach_pid(),把子进程插入 PIDTYPE_TGID 类型的散列表中(更具体地说,插入 current->group_leader 进程的每个 PID 链表)。
现在,新进程已经被加入进程集合:递增 nr_threads 变量的值。
递增 total_forks 变量以记录被创建的进程的数量。
终止并返回子进程描述符指针(tsk)。
(b)do_fork 之后
让我们回头看看在 do_fork() 结束之后都发生了什么。现在,我们有了处于可运行状态的完整的子进程。但是,它还没有实际运行,调度程序要决定何时把 CPU 交给这个子进程。在以后的进程切换中,调度程序继续完善子进程:把子进程描述符 thread 字段的值装入几个 CPU 寄存器。特别是把 thread.esp(即把子进程内核态堆栈的地址)装入 esp 寄存器,把函数 ret_from_fork() 的地址装入eip 寄存器。这个汇编语言函数调用 schedule_tail() 函数(它依次调用 finish_task_switch() 来完成进程切换,参见第七章 “schedule() 函数” 一节),用存放在栈中的值再装载所有的寄存器,并强迫 CPU 返回到用户态。然后,在 fork()、vfork() 或 clone() 系统调用结束时,新进程将开始执行系统调用的返回值放在 eax 寄存器中:返回给子进程的值是 0,返回给父进程的值是子进程的 PID。回顾 copy_thread() 对子进程的 eax 寄存器所执行的操作(copy_process() 的第 13 步),就能理解这是如何实现的。
除非 fork 系统调用返回 0,否则,子进程将与父进程执行相同的代码(参见 copy_process() 的第 13 步)。应用程序的开发者可以按照 Unix 编程者熟悉的方式利用这一事实,在基于 PID 值的程序中插入一个条件语句使子进程与父进程有不同的行为。
(3)内核线程
传统的 Unix 系统把一些重要的任务委托给周期性执行的进程,这些任务包括刷新磁盘高速缓存,交换出不用的页框,维护网络连接等等。事实上,以严格线性的方式执行这些任务的确效率不高,如果把它们放在后台调度,不管是对它们的函数还是对终端用户进程都能得到较好的响应。因为一些系统进程只运行在内核态,所以现代操作系统把它们的函数委托给内核线程(kernel thread),内核线程不受不必要的用户态上下文的拖累。在 Linux 中,内核线程在以下几方面不同于普通进程:
内核线程只运行在内核态,而普通进程既可以运行在内核态,也可以运行在用户态。
因为内核线程只运行在内核态,它们只使用大于 PAGE_OFFSET 的线性地址空间。另一方面,不管在用户态还是在内核态,普通进程可以用 4GB 的线性地址空间。
(a)创建一个内核线程
// arch/x86/kernel/process.c /* * Create a kernel thread */ int kernel_thread(int (*fn)(void *), void *arg, unsigned long flags) { struct pt_regs regs; memset(®s, 0, sizeof(regs)); regs.si = (unsigned long) fn; regs.di = (unsigned long) arg; #ifdef CONFIG_X86_32 regs.ds = __USER_DS; regs.es = __USER_DS; regs.fs = __KERNEL_PERCPU; regs.gs = __KERNEL_STACK_CANARY; #else regs.ss = __KERNEL_DS; #endif regs.orig_ax = -1; regs.ip = (unsigned long) kernel_thread_helper; regs.cs = __KERNEL_CS | get_kernel_rpl(); regs.flags = X86_EFLAGS_IF | 0x2; /* Ok, create the new process.. */ return do_fork(flags | CLONE_VM | CLONE_UNTRACED, 0, ®s, 0, NULL, NULL); } EXPORT_SYMBOL(kernel_thread);
kernel_thread() 函数创建一个新的内核线程,它接受的参数有:所要执行的内核函数的地址(fn)、要传递给函数的参数(arg)、一组 clone 标志(flags)。 该函数本质上以下面的方式调用 do_fork() :
do_fork(flags | CLONE_VM | CLONE_UNTRACED, 0, ®s, 0, NULL, NULL);
CLONE_VM 标志避免复制调用进程的页表:由于新内核线程无论如何都不会访问用户态地址空间,所以这种复制无疑会造成时间和空间的浪费。CLONE_UNTRACED 标志保证不会有任何进程跟踪新内核线程,即使调用进程被跟踪。
传递给 do_fork() 的参数 regs 表示内核栈的地址,copy_thread() 函数将从这里找到为新线程初始化 CPU 寄存器的值。kernel_thread() 函数在这个栈中保留寄存器值的目的是:
通过 copy_thread() 把 ebx 和 edx 分别设置为参数 fn 和 arg 的值。
把 eip 寄存器的值设置为下面汇编语言代码段的地址:
movl %edx, %eax pushl %edx call *%ebx pushl %eax call do_exit
因此,新的内核线程开始执行 fn(arg) 函数,如果该函数结束,内核线程执行系统调用 _exit(),并把 fn() 的返回值传递给它(参见本章稍后 “撤消进程” 一节)。
(b)进程 0
// arch/x86/kernel/init_task.c /* * Initial thread structure. * * We need to make sure that this is THREAD_SIZE aligned due to the * way process stacks are handled. This is done by having a special * "init_task" linker map entry.. */ union thread_union init_thread_union __init_task_data = { INIT_THREAD_INFO(init_task) }; /* * Initial task structure. * * All other task structs will be allocated on slabs in fork.c */ struct task_struct init_task = INIT_TASK(init_task); EXPORT_SYMBOL(init_task); // include/linux/init_task.h #define INIT_TASK(tsk) \ { \ .state = 0, \ .stack = &init_thread_info, \ .usage = ATOMIC_INIT(2), \ .flags = PF_KTHREAD, \ .lock_depth = -1, \ .prio = MAX_PRIO-20, \ .static_prio = MAX_PRIO-20, \ .normal_prio = MAX_PRIO-20, \ .policy = SCHED_NORMAL, \ .cpus_allowed = CPU_MASK_ALL, \ .mm = NULL, \ .active_mm = &init_mm, \ .se = { \ .group_node = LIST_HEAD_INIT(tsk.se.group_node), \ }, \ .rt = { \ .run_list = LIST_HEAD_INIT(tsk.rt.run_list), \ .time_slice = HZ, \ .nr_cpus_allowed = NR_CPUS, \ }, \ .tasks = LIST_HEAD_INIT(tsk.tasks), \ .pushable_tasks = PLIST_NODE_INIT(tsk.pushable_tasks, MAX_PRIO), \ .ptraced = LIST_HEAD_INIT(tsk.ptraced), \ .ptrace_entry = LIST_HEAD_INIT(tsk.ptrace_entry), \ .real_parent = &tsk, \ .parent = &tsk, \ .children = LIST_HEAD_INIT(tsk.children), \ .sibling = LIST_HEAD_INIT(tsk.sibling), \ .group_leader = &tsk, \ .real_cred = &init_cred, \ .cred = &init_cred, \ .cred_guard_mutex = \ __MUTEX_INITIALIZER(tsk.cred_guard_mutex), \ .comm = "swapper", \ .thread = INIT_THREAD, \ .fs = &init_fs, \ .files = &init_files, \ .signal = &init_signals, \ .sighand = &init_sighand, \ .nsproxy = &init_nsproxy, \ .pending = { \ .list = LIST_HEAD_INIT(tsk.pending.list), \ .signal = {{0}}}, \ .blocked = {{0}}, \ .alloc_lock = __SPIN_LOCK_UNLOCKED(tsk.alloc_lock), \ .journal_info = NULL, \ .cpu_timers = INIT_CPU_TIMERS(tsk.cpu_timers), \ .fs_excl = ATOMIC_INIT(0), \ .pi_lock = __RAW_SPIN_LOCK_UNLOCKED(tsk.pi_lock), \ .timer_slack_ns = 50000, /* 50 usec default slack */ \ .pids = { \ [PIDTYPE_PID] = INIT_PID_LINK(PIDTYPE_PID), \ [PIDTYPE_PGID] = INIT_PID_LINK(PIDTYPE_PGID), \ [PIDTYPE_SID] = INIT_PID_LINK(PIDTYPE_SID), \ }, \ .dirties = INIT_PROP_LOCAL_SINGLE(dirties), \ INIT_IDS \ INIT_PERF_EVENTS(tsk) \ INIT_TRACE_IRQFLAGS \ INIT_LOCKDEP \ INIT_FTRACE_GRAPH \ INIT_TRACE_RECURSION \ INIT_TASK_RCU_PREEMPT(tsk) \ } #define INIT_CPU_TIMERS(cpu_timers) \ { \ LIST_HEAD_INIT(cpu_timers[0]), \ LIST_HEAD_INIT(cpu_timers[1]), \ LIST_HEAD_INIT(cpu_timers[2]), \ }
所有进程的祖先叫做进程 0,idle 进程或因为历史的原因叫做 swapper 进程,它是在 Linux 的初始化阶段从无到有创建的一个内核线程(参见附录一)。这个祖先进程使用下列静态分配的数据结构(所有其他进程的数据结构都是动态分配的):
存放在 init_task 变量中的进程描述符,由 INIT_TASK 宏完成对它的初始化。
存放在 init_thread_union 变量中的 thread_info 描述符和内核堆栈,由 INIT_THREAD_INFO 宏完成对它们的初始化。
由进程描述符指向的下列表:
- init_mm
- init_fs
- init_files
- init_signals
- init_sighand
这些表分别由下列宏初始化: - INIT_MM
- 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。运行在 CPU 0 上的 swapper 进程初始化内核数据结构,然后激活其他的 CPU,并通过 copy_process() 函数创建另外的 swapper 进程,把 0 传递给新创建的 swapper 进程作为它们的新 PID。此外,内核把适当的 CPU 索引赋给内核所创建的每个进程的 thread_info 描述符的 cpu 字段。
(c)进程 1
由进程 0 创建的内核线程执行 init() 函数,init() 依次完成内核初始化。init() 调用 execve() 系统调用装入可执行程序 init。结果,init 内核线程变为一个普通进程,且拥有自己的每进程(per-process)内核数据结构(参见第二十章)。在系统关闭之前,init 进程一直存活,因为它创建和监控在操作系统外层执行的所有进程的活动。
(d)其他内核线程
Linux 使用很多其他内核线程。其中一些在初始化阶段创建,一直运行到系统关闭:而其他一些在内核必须执行一个任务时 “按需” 创建,这种任务在内核的执行上下文中得到很好的执行。
一些内核线程的例子(除了进程 0 和进程 1)是:
keventd(也被称为事件)
执行 keventd_wq 工作队列(参见第四章)中的函数。
kapmd
处理与高级电源管理(APM)相关的事件。
kswapd
执行内存回收,在第十七章"周期回收"一节将进行描述。
pdflush
刷新 “脏” 缓冲区中的内容到磁盘以回收内存,在第十五章 “pdflush内核线程” 一节将进行描述。
kblockd
执行 kblockd_workqueue 工作队列中的函数。实质上,它周期性地激活块设备驱动程序,将在第十四章 “激活块设备驱动程序” 一节给予描述。
ksoftirqd
运行 tasklet(参看第四章 “软中断及 tasklet” 一节):系统中每个 CPU 都有这样一个内核线程。
(4)撤消进程
很多进程终止了它们本该执行的代码,从这种意义上说,这些进程"死"了。当这种情况发生时,必须通知内核以便内核释放进程所拥有的资源,包括内存、打开文件及其他我们在本书中讲到的零碎东西,如信号量。
进程终止的一般方式是调用 exit() 库函数,该函数释放 C 函数库所分配的资源,执行编程者所注册的每个函数,并结束从系统回收进程的那个系统调用。exit() 函数可能由编程者显式地插入。另外,C 编译程序总是把 exit() 函数插入到 main() 函数的最后一条语句之后。
内核可以有选择地强迫整个线程组死掉。这发生在以下两种典型情况下:当进程接收到一个不能处理或忽视的信号时(参见十一章),或者当内核正在代表进程运行时在内核态产生一个不可恢复的 CPU 异常时(参见第四章)。
(5)进程终止
在 Linux 2.6 中有两个终止用户态应用的系统调用:
exit_group() 系统调用,它终止整个线程组,即整个基于多线程的应用。do_group_exit() 是实现这个系统调用的主要内核函数。这是 C 库函数 exit() 应该调用的系统调用。
exit() 系统调用,它终止某一个线程,而不管该线程所属线程组中的所有其他进程。do_exit() 是实现这个系统调用的主要内核函数。这是被诸如 pthread_exit() 的 Linux 线程库的函数所调用的系统调用。
(a)do_group_exit() 函数
应用层调用:
# #include <linux/unistd.h> void exit_group(int status);
内核层响应函数:
// kernel/exit.c void do_group_exit(int exit_code) { struct signal_struct *sig = current->signal; BUG_ON(exit_code & 0x80); /* core dumps don't get here */ if (signal_group_exit(sig)) exit_code = sig->group_exit_code; else if (!thread_group_empty(current)) { struct sighand_struct *const sighand = current->sighand; spin_lock_irq(&sighand->siglock); if (signal_group_exit(sig)) /* Another thread got here before we took the lock. */ exit_code = sig->group_exit_code; else { sig->group_exit_code = exit_code; sig->flags = SIGNAL_GROUP_EXIT; zap_other_threads(current); } spin_unlock_irq(&sighand->siglock); } do_exit(exit_code); /* NOTREACHED */ }
描述
do_group_exit() 函数杀死属于 current 线程组的所有进程。它接受进程终止代号作为参数,进程终止代号可能是系统调用 exit_group()(正常结束)指定的一个值,也可能是内核提供的一个错误代号(异常结束)。该函数执行下述操作:
检查退出进程的 SIGNAL_GROUP_EXIT 标志是否不为 0,如果不为 0,说明内核已经开始为线程组执行退出的过程。在这种情况下,就把存放在 current->signal->group_exit_code 中的值当作退出码,然后跳转到第 4 步。
否则,设置进程的 SIGNAL_GROUP_EXIT 标志并把终止代号存放到 current->signal->group_exit_code 字段。
调用 zap_other_threads() 函数杀死 current 线程组中的其他进程(如果有的话)。为了完成这个步骤,函数扫描与 current->tgid 对应的 PIDTYPE_TGID 类型的散列表中的每个 PID 链表,向表中所有不同于 current 的进程发送 SIGKILL 信号(参见第十一章),结果,所有这样的进程都将执行 do_exit() 函数,从而被杀死。
调用 do_exit() 函数,把进程的终止代号传递给它。正如我们将在下面看到的,do_exit() 杀死进程而且不再返回。
(b)do_exit() 函数
源码
// kernel/exit.c void do_exit(long code) { struct task_struct *tsk = current; int group_dead; profile_task_exit(tsk); WARN_ON(atomic_read(&tsk->fs_excl)); if (unlikely(in_interrupt())) panic("Aiee, killing interrupt handler!"); if (unlikely(!tsk->pid)) panic("Attempted to kill the idle task!"); tracehook_report_exit(&code); validate_creds_for_do_exit(tsk); /* * We're taking recursive faults here in do_exit. Safest is to just * leave this task alone and wait for reboot. */ if (unlikely(tsk->flags & PF_EXITING)) { printk(KERN_ALERT "Fixing recursive fault but reboot is needed!\n"); /* * We can do this unlocked here. The futex code uses * this flag just to verify whether the pi state * cleanup has been done or not. In the worst case it * loops once more. We pretend that the cleanup was * done as there is no way to return. Either the * OWNER_DIED bit is set by now or we push the blocked * task into the wait for ever nirwana as well. */ tsk->flags |= PF_EXITPIDONE; set_current_state(TASK_UNINTERRUPTIBLE); schedule(); } exit_irq_thread(); exit_signals(tsk); /* sets PF_EXITING */ /* * tsk->flags are checked in the futex code to protect against * an exiting task cleaning up the robust pi futexes. */ smp_mb(); raw_spin_unlock_wait(&tsk->pi_lock); if (unlikely(in_atomic())) printk(KERN_INFO "note: %s[%d] exited with preempt_count %d\n", current->comm, task_pid_nr(current), preempt_count()); acct_update_integrals(tsk); /* sync mm's RSS info before statistics gathering */ if (tsk->mm) sync_mm_rss(tsk, tsk->mm); group_dead = atomic_dec_and_test(&tsk->signal->live); if (group_dead) { hrtimer_cancel(&tsk->signal->real_timer); exit_itimers(tsk->signal); if (tsk->mm) setmax_mm_hiwater_rss(&tsk->signal->maxrss, tsk->mm); } acct_collect(code, group_dead); if (group_dead) tty_audit_exit(); if (unlikely(tsk->audit_context)) audit_free(tsk); tsk->exit_code = code; taskstats_exit(tsk, group_dead); exit_mm(tsk); if (group_dead) acct_process(); trace_sched_process_exit(tsk); exit_sem(tsk); exit_files(tsk); exit_fs(tsk); check_stack_usage(); exit_thread(); cgroup_exit(tsk, 1); if (group_dead) disassociate_ctty(1); module_put(task_thread_info(tsk)->exec_domain->module); proc_exit_connector(tsk); /* * FIXME: do that only when needed, using sched_exit tracepoint */ flush_ptrace_hw_breakpoint(tsk); /* * Flush inherited counters to the parent - before the parent * gets woken up by child-exit notifications. */ perf_event_exit_task(tsk); exit_notify(tsk, group_dead); #ifdef CONFIG_NUMA mpol_put(tsk->mempolicy); tsk->mempolicy = NULL; #endif #ifdef CONFIG_FUTEX if (unlikely(current->pi_state_cache)) kfree(current->pi_state_cache); #endif /* * Make sure we are holding no locks: */ debug_check_no_locks_held(tsk); /* * We can do this unlocked here. The futex code uses this flag * just to verify whether the pi state cleanup has been done * or not. In the worst case it loops once more. */ tsk->flags |= PF_EXITPIDONE; if (tsk->io_context) exit_io_context(tsk); if (tsk->splice_pipe) __free_pipe_info(tsk->splice_pipe); validate_creds_for_do_exit(tsk); preempt_disable(); exit_rcu(); /* causes final put_task_struct in finish_task_switch(). */ tsk->state = TASK_DEAD; schedule(); BUG(); /* Avoid "noreturn function does return". */ for (;;) cpu_relax(); /* For when BUG is null */ } EXPORT_SYMBOL_GPL(do_exit);
描述
所有进程的终止都是由 do_exit() 函数来处理的,这个函数从内核数据结构中删除对终止进程的大部分引用。do_exit() 函数接受进程的终止代号作为参数并执行下列操作:
把进程描述符的 flag 字段设置为 PF_EXITING 标志,以表示进程正在被删除。
如果需要,通过函数 del_timer_sync()(参见第六章)从动态定时器队列中删除进程描述符。
分别调用 exit_mm()、exit_sem()、__exit_files()、__exit_fs()、exit_namespace() 和 exit_thread() 函数从进程描述符中分离出与分页、信号量、文件系统、打开文件描述符、命名空间以及 I/O 权限位图相关的数据结构。如果没有其他进程共享这些数据结构,那么这些函数还删除所有这些数据结构中。
如果实现了被杀死进程的执行域和可执行格式(参见第二十章)的内核函数包含在内核模块中,则函数递减它们的使用计数器。
把进程描述符的 exit_code 字段设置成进程的终止代号,这个值要么是 _exit() 或 exit_group() 系统调用参数(正常终止),要么是由内核提供的一个错误代号(异常终止)。
调用 exit_notify() 函数执行下面的操作:
更新父进程和子进程的亲属关系。如果同一线程组中有正在运行的进程,就让终止进程所创建的所有子进程都变成同一线程组中另外一个进程的子进程,否则让它们成为 init 的子进程。
检查被终止进程其进程描述符的 exit_signal 字段是否不等于 -1,并检查进程是否是其所属进程组的最后一个成员(注意:正常进程都会具有这些条件,参见前面 “clone()、fork() 和 vfork() 系统调用” 一节中对 copy_process() 的描述,第 16 步)。在这种情况下,函数通过给 正被终止进程的父进程发送一个信号(通常是 SIGCHLD),以通知父进程子进程死亡。
否则,也就是 exit_signal 字段等于 -1,或者线程组中还有其他进程,那么只要进程正在被跟踪,就向父进程发送一个 SIGCHLD 信号(在这种情况下,父进程是调试程序,因而,向它报告轻量级进程死亡的信息)。
如果进程描述符的 exit_signal 字段等于 -1,而且进程没有被跟踪,就把进程描述符的 exit_state 字段置为 EXIT_DEAD,然后调用 release_task() 回收进程的其他数据结构占用的内存,并递减进程描述符的使用计数器(见下一节)。使用记数变为为 1(参见 copy_process() 函数的第 3f 步),以使进程描述符本身正好不会被释放。
否则,如果进程描述符的 exit_signal 字段不等于 -1,或进程正在被跟踪,就把 exit_state 字段置为 EXIT_ZOMBIE。在下一节我们将看到如何处理僵死进程。
把进程描述符的 flags 字段设置为 PF_DEAD 标志(参见第七章 " schedule() 函数" 一节)。
调用 schedule() 函数(参见第七章)选择一个新进程运行。调度程序忽略处于 EXIT_ZOMBIE 状态的进程,所以这种进程正好在 schedule() 中的宏 switch_to 被调用之后停止执行。正如在第七章我们将看到的:调度程序将检查被替换的僵死进程描述符的 PF_DEAD 标志并递减使用计数器,从而说明进程不再存活的事实。
5、进程删除
Unix 允许进程查询内核以获得其父进程的 PID,或者其任何子进程的执行状态。例如,进程可以创建一个子进程来执行特定的任务,然后调用诸如 wait() 这样的一些库函数检查子进程是否终止。如果子进程已经终止,那么,它的终止代号将告诉父进程这个任务是否已成功地完成。
为了遵循这些设计选择,不允许 Unix 内核在进程一终止后就丢弃包含在进程描述符字段中的数据。只有父进程发出了与被终止的进程相关的 wait() 类系统调用之后,才允许这样做。这就是引入僵死状态的原因:尽管从技术上来说进程已死,但必须保存它的描述符,直到父进程得到通知。
如果父进程在子进程结束之前结束会发生什么情况呢? 在这种情况下,系统中会到处是僵死的进程,而且它们的进程描述符永久占据着 RAM。如前所述,必须强迫所有的孤儿进程成为 init 进程的子进程来解决这个问题。这样,init 进程在用 wait() 类系统调用检查其合法的子进程终止时,就会撤消僵死的进程。
release_task() 函数从僵死进程的描述符中分离出最后的数据结构,对僵死进程的处理有两种可能的方式:如果父进程不需要接收来自子进程的信号,就调用 do_exit() ;如果已经给父进程发送了一个信号,就调用 wait4() 或 waitpid() 系统调用。在后一种情况下,函数还将回收进程描述符所占用的内存空间,而在前一种情况下,内存的回收将由进程调度程序来完成(参见第七章)。该函数执行下述步骤:
递减终止进程拥有者的进程个数。这个值存放在本章前面提到的 user_struct 结构中(参见 copy_process() 的第 4 步)。
如果进程正在被跟踪,函数将它从调试程序的 ptrace_children 链表中删除,并让该进程重新属于初始的父进程。
调用 __exit_signal() 删除所有的挂起信号并释放进程的 signal_struct 描述符。如果该描述符不再被其他的轻量级进程使用,函数进一步删除这个数据结构。此外,函数调用 exit_itimers() 从进程中剥离掉所有的 POSIX 时间间隔定时器。
调用 __exit_sighand() 删除信号处理函数。
调用 _unhash_process(),该函数依次执行下面的操作:
变量 nr_threads 减 1。
两次调用 detach_pid(),分别从 PIDTYPE_PID 和 PIDTYPE_TGID 类型的 PID 散列表中删除进程描述符。
如果进程是线程组的领头进程,那么再调用两次 detach_pid(),从 PIDTYPE_PGID 和 PIDTYPE_SID 类型的散列表中删除进程描述符。
用宏 REMOVE_LINKS 从进程链表中解除进程描述符的链接。
如果进程不是线程组的领头进程,领头进程处于僵死状态,而且进程是线程组的最后一个成员,则该函数向领头进程的父进程发送一个信号,通知它进程已死亡。
调用 sched_exit() 函数来调整父进程的时间片(这一步在逻辑上作为对 copy_process() 第 17 步的补充)。
调用 put_task_struct() 递减进程描述符的使用计数器,如果计数器变为 0,则函数终止所有残留的对进程的引用。
递减进程所有者的 user_struct 数据结构的使用计数器(__count 字段)(参见 copy_process() 的第 5 步),如果使用计数器变为 0,就释放该数据结构。
释放进程描述符以及 thread_info 描述符和内核态堆栈所占用的内存区域。
深入理解 Linux 内核3: https://developer.aliyun.com/article/1597362