深入理解 Linux 内核2

本文涉及的产品
应用型负载均衡 ALB,每月750个小时 15LCU
网络型负载均衡 NLB,每月750个小时 15LCU
公网NAT网关,每月750个小时 15CU
简介: 深入理解 Linux 内核

深入理解 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));

 函数执行的步骤如下:

  1. 执行由 __unlazy_fpu() 宏产生的代码(参见本章稍后 “保存和加载 FPUMMXXMM 寄存器” 一节),以有选择地保存 prev_p 进程的 FPUMMXXMM 寄存器的内容。
__unlazy_fpu(prev_p);
  1. 执行 smp_processor_id() 宏获得本地(localCPU 的下标,即执行代码的 CPU。该宏从当前进程的 thread_info 结构的 cpu 字段获得下标并将它保存到 cpu 局部变量。
  2. next_p->thread.esp0 装入对应于本地 CPUTSSesp0 字段,我们将在第十章的 “通过 sysenter 指令发生系统调用” 一节看到,以后任何由 sysenter 汇编指令产生的从用户态到内核态的特权级转换将把这个地址拷贝到 esp 寄存器中:
init_tss[cpu].esp0 = next_p->thread.esp0;
  1. 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];
  1. fsgs 段寄存器的内容分别存放 prev_p->thread.fsprev_p->thread.gs 中,对应的汇编语言指令是:
movl %fs, 40(%esi)
movl %gs, 44(%esi)

esi 寄存器指向 prev_p->thread 结构。

  1. 如果 fsgs 段寄存器已经被 prev_pnext_p 进程中的任意一个使用(也就是说如果它们有一个非 0 的值),,则将 next_p 进程的 thread_struct 描述符中保存的值装入这些寄存器中。这一步在逻辑上补充了前一步中执行的操作。主要的汇编语言指令如下:
movl 40(%ebx),%fs
movl 44(%ebx),%gs

  ebx 寄存器指向 next_p->thread 结构。代码实际上更复杂,因为当它检测到一个无效的段寄存器值时,CPU 可能产生一个异常。代码采用一种 “修正(fix-up)” 途径来考虑这种可能性(参见第十章"动态地址检查:修正代码" 一节)。

  1. 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);
}
  1. 如果必要,更新 TSS 中的 I/O 位图。当 next_pprev_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),并强制再一次执行有缺陷的汇编语言指令。

  1. 终止。 _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(&current->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(&current->sighand->siglock);
    write_unlock_irq(&tasklist_lock);
    retval = -ERESTARTNOINTR;
    goto bad_fork_free_pid;
  }

  if (clone_flags & CLONE_THREAD) {
    atomic_inc(&current->signal->count);
    atomic_inc(&current->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(&current->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() 的最重要的步骤:

  1. 检查参数 clone_flags 所传递标志的一致性。尤其是,在下列情况下,它返回错误代号:
  • CLONE_NEWNSCLONE_FS 标志都被设置。
  • CLONE_THREAD 标志被设置,但 CLONE_SIGHAND 标志被清 0(同一线程组中的轻量级进程必须共享信号)。
  • CLONE_SIGHAND 标志被设置,但 CLONE_VM 被清 0(共享信号处理程序的轻量级进程也必须共享内存描述符)。
  1. 通过调用 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(&regs, 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, &regs, 0, NULL, NULL);
}
EXPORT_SYMBOL(kernel_thread);

  kernel_thread() 函数创建一个新的内核线程,它接受的参数有:所要执行的内核函数的地址(fn)、要传递给函数的参数(arg)、一组 clone 标志(flags)。 该函数本质上以下面的方式调用 do_fork()

do_fork(flags | CLONE_VM | CLONE_UNTRACED, 0, &regs, 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

相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
高可用应用架构
欢迎来到“高可用应用架构”课程,本课程是“弹性计算Clouder系列认证“中的阶段四课程。本课程重点向您阐述了云服务器ECS的高可用部署方案,包含了弹性公网IP和负载均衡的概念及操作,通过本课程的学习您将了解在平时工作中,如何利用负载均衡和多台云服务器组建高可用应用架构,并通过弹性公网IP的方式对外提供稳定的互联网接入,使得您的网站更加稳定的同时可以接受更多人访问,掌握在阿里云上构建企业级大流量网站场景的方法。 学习完本课程后,您将能够: 理解高可用架构的含义并掌握基本实现方法 理解弹性公网IP的概念、功能以及应用场景 理解负载均衡的概念、功能以及应用场景 掌握网站高并发时如何处理的基本思路 完成多台Web服务器的负载均衡,从而实现高可用、高并发流量架构
目录
相关文章
|
5天前
|
缓存 算法 Linux
深入理解Linux内核调度器:公平性与性能的平衡####
真知灼见 本文将带你深入了解Linux操作系统的核心组件之一——完全公平调度器(CFS),通过剖析其设计原理、工作机制以及在实际系统中的应用效果,揭示它是如何在众多进程间实现资源分配的公平性与高效性的。不同于传统的摘要概述,本文旨在通过直观且富有洞察力的视角,让读者仿佛亲身体验到CFS在复杂系统环境中游刃有余地进行任务调度的过程。 ####
26 6
|
4天前
|
缓存 资源调度 安全
深入探索Linux操作系统的心脏——内核配置与优化####
本文作为一篇技术性深度解析文章,旨在引领读者踏上一场揭秘Linux内核配置与优化的奇妙之旅。不同于传统的摘要概述,本文将以实战为导向,直接跳入核心内容,探讨如何通过精细调整内核参数来提升系统性能、增强安全性及实现资源高效利用。从基础概念到高级技巧,逐步揭示那些隐藏在命令行背后的强大功能,为系统管理员和高级用户打开一扇通往极致性能与定制化体验的大门。 --- ###
19 9
|
2天前
|
缓存 负载均衡 Linux
深入理解Linux内核调度器
本文探讨了Linux操作系统核心组件之一——内核调度器的工作原理和设计哲学。不同于常规的技术文章,本摘要旨在提供一种全新的视角来审视Linux内核的调度机制,通过分析其对系统性能的影响以及在多核处理器环境下的表现,揭示调度器如何平衡公平性和效率。文章进一步讨论了完全公平调度器(CFS)的设计细节,包括它如何处理不同优先级的任务、如何进行负载均衡以及它是如何适应现代多核架构的挑战。此外,本文还简要概述了Linux调度器的未来发展方向,包括对实时任务支持的改进和对异构计算环境的适应性。
18 6
|
3天前
|
缓存 Linux 开发者
Linux内核中的并发控制机制:深入理解与应用####
【10月更文挑战第21天】 本文旨在为读者提供一个全面的指南,探讨Linux操作系统中用于实现多线程和进程间同步的关键技术——并发控制机制。通过剖析互斥锁、自旋锁、读写锁等核心概念及其在实际场景中的应用,本文将帮助开发者更好地理解和运用这些工具来构建高效且稳定的应用程序。 ####
18 5
|
1天前
|
算法 Linux 调度
深入理解Linux内核调度器:从基础到优化####
本文旨在通过剖析Linux操作系统的心脏——内核调度器,为读者揭开其高效管理CPU资源的神秘面纱。不同于传统的摘要概述,本文将直接以一段精简代码片段作为引子,展示一个简化版的任务调度逻辑,随后逐步深入,详细探讨Linux内核调度器的工作原理、关键数据结构、调度算法演变以及性能调优策略,旨在为开发者与系统管理员提供一份实用的技术指南。 ####
14 4
|
4天前
|
算法 Unix Linux
深入理解Linux内核调度器:原理与优化
本文探讨了Linux操作系统的心脏——内核调度器(Scheduler)的工作原理,以及如何通过参数调整和代码优化来提高系统性能。不同于常规摘要仅概述内容,本摘要旨在激发读者对Linux内核调度机制深层次运作的兴趣,并简要介绍文章将覆盖的关键话题,如调度算法、实时性增强及节能策略等。
|
5天前
|
存储 监控 安全
Linux内核调优的艺术:从基础到高级###
本文深入探讨了Linux操作系统的心脏——内核的调优方法。文章首先概述了Linux内核的基本结构与工作原理,随后详细阐述了内核调优的重要性及基本原则。通过具体的参数调整示例(如sysctl、/proc/sys目录中的设置),文章展示了如何根据实际应用场景优化系统性能,包括提升CPU利用率、内存管理效率以及I/O性能等关键方面。最后,介绍了一些高级工具和技术,如perf、eBPF和SystemTap,用于更深层次的性能分析和问题定位。本文旨在为系统管理员和高级用户提供实用的内核调优策略,以最大化Linux系统的效率和稳定性。 ###
|
4天前
|
Java Linux Android开发
深入探索Android系统架构:从Linux内核到应用层
本文将带领读者深入了解Android操作系统的复杂架构,从其基于Linux的内核到丰富多彩的应用层。我们将探讨Android的各个关键组件,包括硬件抽象层(HAL)、运行时环境、以及核心库等,揭示它们如何协同工作以支持广泛的设备和应用。通过本文,您将对Android系统的工作原理有一个全面的认识,理解其如何平衡开放性与安全性,以及如何在多样化的设备上提供一致的用户体验。
|
6天前
|
Linux 数据库
Linux内核中的锁机制:保障并发操作的数据一致性####
【10月更文挑战第29天】 在多线程编程中,确保数据一致性和防止竞争条件是至关重要的。本文将深入探讨Linux操作系统中实现的几种关键锁机制,包括自旋锁、互斥锁和读写锁等。通过分析这些锁的设计原理和使用场景,帮助读者理解如何在实际应用中选择合适的锁机制以优化系统性能和稳定性。 ####
22 6
|
6天前
|
机器学习/深度学习 负载均衡 算法
深入探索Linux内核调度机制的优化策略###
本文旨在为读者揭开Linux操作系统中至关重要的一环——CPU调度机制的神秘面纱。通过深入浅出地解析其工作原理,并探讨一系列创新优化策略,本文不仅增强了技术爱好者的理论知识,更为系统管理员和软件开发者提供了实用的性能调优指南,旨在促进系统的高效运行与资源利用最大化。 ###