深入理解 Linux 内核_服务器

Linux 内核系列文章



Linux 内核设计与实现深入理解 Linux 内核
Linux 设备驱动程序
Linux设备驱动开发详解


文章目录

  • Linux 内核系列文章
  • 前言
  • 一、内存寻址
  • 1、内存地址
  • 2、硬件中的分段
  • (1)段选择符
  • 3、Linux 中的分段
  • (1)Linux GDT
  • (2)Linux LDT
  • 4、硬件中的分页
  • 5、Linux 中的分页
  • (1)进程页表
  • (2)内核页表
  • (3)临时内核页表
  • (4)当 RAM 小于 896MB时的最终内核页表
  • (5)当 RAM 大小在 896MB 和 4096MB 之间时的最终内核页表
  • (6)当 RAM 大于 4096MB 时的最终内核页表
  • (7)固定映射的线性地址
  • (8)处理硬件高速缓存和 TLB
  • 二、进程
  • 1、进程、轻量级进程和线程
  • 2、进程描述符
  • (1)标识一个进程
  • (2)进程描述符处理
  • (3)标识当前进程
  • 3、进程切换
  • (1)switch_to 宏
  • (a)分析
  • (2)__switch_to() 函数
  • (a)分析
  • 4、创建进程
  • (1)do_fork() 函数
  • (2)copy_process() 函数
  • (a)分析
  • (b)do_fork 之后
  • (3)内核线程
  • (a)创建一个内核线程
  • (b)进程 0
  • (c)进程 1
  • (d)其他内核线程
  • (4)撤消进程
  • (5)进程终止
  • (a)do_group_exit() 函数
  • 描述
  • (b)do_exit() 函数
  • 源码
  • 描述
  • 5、进程删除
  • 三、中断和异常
  • 1、中断和异常
  • (1)IRQ 和中断
  • (2)高级可编程中断控制器
  • (3)异常



前言

  本文主要用来摘录《深入理解 Linux 内核》一书中学习知识点,本书基于 Linux 2.6.11 版本,源代码摘录基于 Linux 2.6.34 ,两者之间可能有些出入。


一、内存寻址

1、内存地址

  可参考 ⇒ 1、内存寻址

2、硬件中的分段

  可参考 ⇒ 五、分段机制

(1)段选择符

  80x86 中有 6 个段寄存器,分别为 csssdsesfsgs6 个寄存器中 3 个有专门的用途:可参考 ⇒ 3、段选择符

  • cs 代码段寄存器,指向包含程序指令的段。
  • ss 栈段寄存器,指向包含当前程序栈的段。
  • ds 数据段寄存器,指向包含静态数据或者全局数据段。

  其它 3 个段寄存器做一般用途,可以指向任意的数据段。

3、Linux 中的分段

  分段可以给每一个进程分配不同的线性地址空间,而分页可以把同一线性地址空间映射到不同的物理空间。与分段相比,Linux 更喜欢使用分页方式,因为:

  • 当所有进程使用相同的段寄存器值时,内存管理变得更简单,也就是说它们能共享同样的一组线性地址。
  • Linux 设计目标之一是可以把它移植到绝大多数流行的处理器平台上。然而,如 RISC 体系结构对分段的支持很有限。

  2.6 版的 Linux 只有在 80x86 结构下才需要使用分段。

  运行在用户态的所有 Linux 进程都使用一对相同的段来对指令和数据寻址。这两个段就是所谓的用户代码段和用户数据段。类似地,运行在内核态的所有 Linux 进程都使用一对相同的段对指令和数据寻址:它们分别叫做内核代码段和内核数据段。下图显示了这四个重要段的段描述符字段的值。可参考 ⇒ 4、段描述符 一文。

深入理解 Linux 内核_寄存器_02


  相应的段选择符由宏 __USER_CS__USER_DS__KERNEL_CS__KERNEL_DS 分别定义。例如,为了对内核代码段寻址,内核只需要把 __KERNEL_CS 宏产生的值装进 cs 段寄存器即可。

  注意,与段相关的线性地址从 0 开始,达到 232 - 1 的寻址限长。这就意味着在用户态或内核态下的所有进程可以使用相同的逻辑地址。

  所有段都从 0x00000000 开始,这可以得出另一个重要结论,那就是在 Linux 下逻辑地址与线性地址是一致的,即逻辑地址的偏移字段的值与相应的线性地址的值总是一致的。

  如前所述,CPU 的当前特权级(CPL)反映了进程是在用户态还是内核态,并由存放在 CS 寄存器中的段选择符的 RPL 字段指定。只要当前特权级被改变,一些段寄存器必须相应地更新。例如,当 CPL=3 时(用户态),ds 寄存器必须含有用户数据段的段选择符,而当 CPL=0 时,ds 寄存器必须含有内核数据段的段选择符。

  类似的情况也出现在 ss 寄存器中。当 CPL3 时,它必须指向一个用户数据段中的用户栈,而当 CPL0 时,它必须指向内核数据段中的一个内核栈。当从用户态切换到内核态时,Linux 总是确保 ss 寄存器装有内核数据段的段选择符。

  当对指向指令或者数据结构的指针进行保存时,内核根本不需要为其设置逻辑地址的段选择符,因为 cs 寄存器就含有当前的段选择等。例如,当内核调用一个函数时,它执行一条 call 汇编语言指令,该指令仅指定其逻辑地址的偏移量部分,而段选择符不用设置,它已经隐含在 cs 寄存器中了。因为"在内核态执行"的段只有一种,叫做代码段,由宏 __KERNEL_CS 定义,所以只要当 CPU 切换到内核态时将 __KERNEL_CS 装载进 cs 就足够了。同样的道理也适用于指向内核数据结构的指针(隐含地使用 ds 寄存器)以及指向用户数据结构的指针(内核显式地使用 es 寄存器)。

  除了刚才描述的 4 个段以外,Linux 还使用了其他几个专门的段。我们将在下一节讲述 Linux GDT 的时候介绍它们。

(1)Linux GDT

  在单处理器系统中只有一个 GDT,而在多处理器系统中每个 CPU 对应一个 GDT。 所有的 GDT 都存放在 cpu_gdt_table 数组中,而所有 GDT 的地址和它们的大小(当初始化 gdtr 寄存器时使用)被存放在 cpu_gdt_descr 数组中。如果你到源代码索引中查看,可以看到这些符号都在文件 arch/i386/kernel/head.S 中被定义。本书中的每一个宏、函数和其他符号都被列在源代码索引中,所以能在源代码中很方便地找到它们。

  图 2-6 是 GDT 的布局示意图。每个 GDT 包含 18 个段描述符和 14 个空的,未使用的,或保留的项。插入未使用的项的目的是为了使经常一起访问的描述符能够处于同一个 32 字节的硬件高速缓存行中(参见本章 后面"硬件高速缓存"一节)。

  每一个 GDT 中包含的 18 个段描述符指向下列的段:

  • 用户态和内核态下的代码段和数据段共 4 个(参见前面一节)。
  • 任务状态段(TSS),每个处理器有 1 个。每个 TSS 相应的线性地址空间都是内核数据段相应线性地址空间的一个小子集。所有的任务状态段都顺序地存放在 init_tss 数组中,值得特别说明的是,第 nCPUTSS 描述符的 Base 字段指向 init_tss 数组的第 n 个元素。G(粒度)标志被清 0 ,而 Limit 字段置为 0xeb,因为 TSS 段是 236 字节长。Type 字段置为 911(可用的 32TSS),且 DPL 置为 0,因为不允许用户态下的进程访问 TSS 段。在第三章"任务状态段"一节你可以找到 Linux 是如何使用 TSS 的细节。参考 ==> 3.1 任务状态段

深入理解 Linux 内核_寄存器_03

  • 1 个包括缺省局部描述符表的段,这个段通常是被所有进程共享的段 (参见下一节)。
  • 3 个局部线程存储(Thread-Local Storage,TLS) 段:这种机制允许多线程应用程序使用最多 3 个局部于线程的数据段。系统调用 set_thread_area()get_thread_area() 分别为正在执行的进程创建和撤消一个 TLS 段。
  • 与高级电源管理(AMP)相关的 3 个段:由于 BIOS 代码使用段,所以当 Linux APM 驱动程序调用 BIOS 函数来获取或者设置 APM 设备的状态时,就可以使用自定义的代码段和数据段。
  • 与支持即插即用(PnP)功能的 BIOS 服务程序相关的 5 个段:在前一种情况下,就像前述与 AMP 相关的 3 个段的情况一样,由于 BIOS 例程使用段,所以当 LinuxPnP 设备驱动程序调用 BIOS 函数来检测 PnP 设备使用的资源时,就可以使用自定义的代码段和数据段。
  • 被内核用来处理"双重错误"(译注 1)异常的特殊 TSS 段(参见第四章的"异常"一节)。

  如前所述,系统中每个处理器都有一个 GDT 副本。除少数几种情况以外,所有 GDT 的副本都存放相同的表项。首先,每个处理器都有它自己的 TSS 段,因此其对应的 GDT 项不同。其次,GDT 中只有少数项可能依赖于 CPU 正在执行的进程(LDTTLS 段描述符)。最后,在某些情况下,处理器可能临时修改 GDT 副本里的某个项,例如,当调用 APMBIOS 例程时就会发生这种情况。

(2)Linux LDT

  大多数用户态下的 Linux 程序不使用局部描述符表,这样内核就定义了一个缺省的 LDT 供大多数进程共享。缺省的局部描述符表存放在 default_ldt 数组中。它包含 5 个项,但内核仅仅有效地使用了其中的两个项:用于 iBCS 执行文件的调用门和 Solaris/x86 可执行文件的调用门(参见第二十章的"执行域"一节)。调用门是 80x86 微处理器提供的一种机制,用于在调用预定义函数时改变 CPU 的特权级,由于我们不会再更深入地讨论它们,所以请参考 Intel 文档以获取更多详情。

  在某些情况下,进程仍然需要创建自己的局部描述符表。这对有些应用程序很有用,像 Wine 那样的程序,它们执行面向段的微软 Windows 应用程序。modify_ldt() 系统调用允许进程创建自己的局部描述符表。
  任何被 modify_ldt() 创建的自定义局部描述符表仍然需要它自己的段。当处理器开始执行拥有自定义局部描述符表的进程时,该 CPUGDT 副本中的 LDT 表项相应地就被修改了。
  用户态下的程序同样也利用 modify_ldt() 来分配新的段,但内核却从不使用这些段,它也不需要了解相应的段描述符,因为这些段描述符被包含在进程自定义的局部描述符表中了。

4、硬件中的分页

  参考 ⇒ 六、分页机制

5、Linux 中的分页

  Linux 采用了一种同时适用于 32 位和 64 位系统的普通分页模型。正像前面 “64 位系统中的分页” 一节所解释的那样,两级页表对 32 位系统来说已经足够了,但 64 位系统需要更多数量的分页级别。直到 2.6.10 版本,Linux 采用三级分页的模型。从 2.6.11 版本开始,采用了四级分页模型(注 5)。图 2-12 中展示的 4 种页表分别被为:

  • 页全局目录(Page Global Directory
  • 页上级目录(Page Upper Directory
  • 页中间目录(Page Middle Directory
  • 页表(Page Table

  页全局目录包含若干页上级目录的地址,页上级目录又依次包含若干页中间目录的地址,而页中间目录又包含若干页表的地址。每一个页表项指向一个页框。线性地址因此被分成五个部分。图 2-12 没有显示位数,因为每一部分的大小与具体的计算机体系结构有关。

深入理解 Linux 内核_服务器_04

  对于没有启用物理地址扩展的 32 位系统,两级页表已经足够了。Linux 通过使 “页上级目录” 位和 “页中间目录” 位全为 0,从根本上取消了页上级目录和页中间目录字段。不过,页上级目录和页中间目录在指针序列中的位置被保留,以便同样的代码在 32 位系统和 64 位系统下都能使用。内核为页上级目录和页中间目录保留了一个位置,这是通过把它们的页目录项数设置为 1,并把这两个目录项映射到页全局目录的一个适当的目录项而实现的。

  启用了物理地址扩展的 32 位系统使用了三级页表。Linux 的页全局目录对应 80x86 的页目录指针表(PDPT),取消了页上级目录,页中间目录对应 80x86 的页目录,Linux 的页表对应 80x86 的页表。

  最后,64 位系统使用三级还是四级分页取决于硬件对线性地址的位的划分(见表 2-4)。

  Linux 的进程处理很大程度上依赖于分页。事实上,线性地址到物理地址的自动转换使下面的设计目标变得可行:

  • 给每一个进程分配一块不同的物理地址空间,这确保了可以有效地防止寻址错误。
  • 区别页(即一组数据)和页框(即主存中的物理地址)之不同。这就允许存放在某个页框中的一个页,然后保存到磁盘上,以后重新装入这同一页时又可以被装在不同的页框中。这就是虚拟内存机制的基本要素(参见第十七章)。

  在本章剩余的部分,为了具体起见,我们将涉及 80x86 处理器使用的分页机制。

  我们将在第九章看到,每一个进程有它自己的页全局目录和自己的页表集。当发生进程切换时(参见第三章 “进程切换” 一节),LinuxCR3 控制寄存器的内容保存在前一个执行进程的描述符中,然后把下一个要执行进程的描述符的值装入 CR3 寄存器中。因此,当新进程重新开始在 CPU 上执行时,分页单元指向一组正确的页表。

(1)进程页表

  进程的线性地址空间分成两部分:

  • 0x000000000xbfffffff 的线性地址,无论进程运行在用户态还是内核态都可以寻址。
  • 0xc00000000xffffffff 的线性地址,只有内核态的进程才能寻址。

  当进程运行在用户态时,它产生的线性地址小于 0xc0000000 ;当进程运行在内核态时,它执行内核代码,所产生的地址大于等于 0xc0000000 。但是,在某些情况下,内核为了检索或存放数据必须访问用户态线性地址空间。

(2)内核页表

(3)临时内核页表

(4)当 RAM 小于 896MB时的最终内核页表

(5)当 RAM 大小在 896MB 和 4096MB 之间时的最终内核页表

(6)当 RAM 大于 4096MB 时的最终内核页表

(7)固定映射的线性地址

(8)处理硬件高速缓存和 TLB

二、进程

1、进程、轻量级进程和线程

  当一个进程创建时,它几乎与父进程相同。它接受父进程地址空间的一个(逻辑)拷贝,并从进程创建系统调用的下一条指令开始执行与父进程相同的代码。尽管父子进程可以共享含有程序代码(正文)的页,但是它们各自有独立的数据拷贝(栈和堆),因此子进程对一个内存单元的修改对父进程是不可见的(反之亦然)。

  尽管早期 Unix 内核使用了这种简单模式,但是,现代 Unix 系统并没有如此使用。它们支持多线程应用程序 —— 拥有很多相对独立执行流的用户程序共享应用程序的大部分数据结构。在这样的系统中,一个进程由几个用户线程(或简单地说,线程)组成,每个线程都代表进程的一个执行流。现在,大部分多线程应用程序都是用 pthreadPOSIX thread)库的标准库函数集编写的。

  Linux 内核的早期版本没有提供多线程应用的支持。从内核观点看,多线程应用程序仅仅是一个普通进程。多线程应用程序多个执行流的创建、处理、调度整个都是在用户态进行的(通常使用 POSIX 兼容的 pthread 库)。

  Linux 使用轻量级进程(lightweight process)对多线程应用程序提供更好的支持。两个轻量级进程基本上可以共享一些资源,诸如地址空间、打开的文件等等。只要其中一个修改共享资源,另一个就立即查看这种修改。当然,当两个线程访问共享资源时就必须同步它们自己。

  实现多线程应用程序的一个简单方式就是把轻量级进程与每个线程关联起来。这样,线程之间就可以通过简单地共享同一内存地址空间、同一打开文件集等来访问相同的应用程序数据结构集,同时,每个线程都可以由内核独立调度,以便一个睡眠的同时另一个仍然是可运行的。POSIX 兼容的 pthread 库使用 Linux 轻量级进程有 3 个例子,它们是 LinuxThreadsNative Posix Thread Library(NPTL) 和 IBM 的下一代 Posix 线程包 NGPT(Next Generation Posix Threading Package)。

  POSIX 兼容的多线程应用程序由支持 “线程组” 的内核来处理最好不过。在 Linux 中,一个线程组基本上就是实现了多线程应用的一组轻量级进程,对于像 getpid()kill(),和 _exit() 这样的一些系统调用,它像一个组织,起整体的作用。

2、进程描述符

// include/linux/sched.h
struct task_struct {
	volatile long state;	/* -1 unrunnable, 0 runnable, >0 stopped */
	void *stack;
	atomic_t usage;
	unsigned int flags;	/* per process flags, defined below */
	unsigned int ptrace;

	int lock_depth;		/* BKL lock depth */

#ifdef CONFIG_SMP
#ifdef __ARCH_WANT_UNLOCKED_CTXSW
	int oncpu;
#endif
#endif

	int prio, static_prio, normal_prio;
	unsigned int rt_priority;
	const struct sched_class *sched_class;
	struct sched_entity se;
	struct sched_rt_entity rt;

#ifdef CONFIG_PREEMPT_NOTIFIERS
	/* list of struct preempt_notifier: */
	struct hlist_head preempt_notifiers;
#endif

	/*
	 * fpu_counter contains the number of consecutive context switches
	 * that the FPU is used. If this is over a threshold, the lazy fpu
	 * saving becomes unlazy to save the trap. This is an unsigned char
	 * so that after 256 times the counter wraps and the behavior turns
	 * lazy again; this to deal with bursty apps that only use FPU for
	 * a short time
	 */
	unsigned char fpu_counter;
#ifdef CONFIG_BLK_DEV_IO_TRACE
	unsigned int btrace_seq;
#endif

	unsigned int policy;
	cpumask_t cpus_allowed;

#ifdef CONFIG_TREE_PREEMPT_RCU
	int rcu_read_lock_nesting;
	char rcu_read_unlock_special;
	struct rcu_node *rcu_blocked_node;
	struct list_head rcu_node_entry;
#endif /* #ifdef CONFIG_TREE_PREEMPT_RCU */

#if defined(CONFIG_SCHEDSTATS) || defined(CONFIG_TASK_DELAY_ACCT)
	struct sched_info sched_info;
#endif

	struct list_head tasks;
	struct plist_node pushable_tasks;

	struct mm_struct *mm, *active_mm;
#if defined(SPLIT_RSS_COUNTING)
	struct task_rss_stat	rss_stat;
#endif
/* task state */
	int exit_state;
	int exit_code, exit_signal;
	int pdeath_signal;  /*  The signal sent when the parent dies  */
	/* ??? */
	unsigned int personality;
	unsigned did_exec:1;
	unsigned in_execve:1;	/* Tell the LSMs that the process is doing an
				 * execve */
	unsigned in_iowait:1;


	/* Revert to default priority/policy when forking */
	unsigned sched_reset_on_fork:1;

	pid_t pid;
	pid_t tgid;

#ifdef CONFIG_CC_STACKPROTECTOR
	/* Canary value for the -fstack-protector gcc feature */
	unsigned long stack_canary;
#endif

	/* 
	 * pointers to (original) parent process, youngest child, younger sibling,
	 * older sibling, respectively.  (p->father can be replaced with 
	 * p->real_parent->pid)
	 */
	struct task_struct *real_parent; /* real parent process */
	struct task_struct *parent; /* recipient of SIGCHLD, wait4() reports */
	/*
	 * children/sibling forms the list of my natural children
	 */
	struct list_head children;	/* list of my children */
	struct list_head sibling;	/* linkage in my parent's children list */
	struct task_struct *group_leader;	/* threadgroup leader */

	/*
	 * ptraced is the list of tasks this task is using ptrace on.
	 * This includes both natural children and PTRACE_ATTACH targets.
	 * p->ptrace_entry is p's link on the p->parent->ptraced list.
	 */
	struct list_head ptraced;
	struct list_head ptrace_entry;

	/*
	 * This is the tracer handle for the ptrace BTS extension.
	 * This field actually belongs to the ptracer task.
	 */
	struct bts_context *bts;

	/* PID/PID hash table linkage. */
	struct pid_link pids[PIDTYPE_MAX];
	struct list_head thread_group;

	struct completion *vfork_done;		/* for vfork() */
	int __user *set_child_tid;		/* CLONE_CHILD_SETTID */
	int __user *clear_child_tid;		/* CLONE_CHILD_CLEARTID */

	cputime_t utime, stime, utimescaled, stimescaled;
	cputime_t gtime;
#ifndef CONFIG_VIRT_CPU_ACCOUNTING
	cputime_t prev_utime, prev_stime;
#endif
	unsigned long nvcsw, nivcsw; /* context switch counts */
	struct timespec start_time; 		/* monotonic time */
	struct timespec real_start_time;	/* boot based time */
/* mm fault and swap info: this can arguably be seen as either mm-specific or thread-specific */
	unsigned long min_flt, maj_flt;

	struct task_cputime cputime_expires;
	struct list_head cpu_timers[3];

/* process credentials */
	const struct cred *real_cred;	/* objective and real subjective task
					 * credentials (COW) */
	const struct cred *cred;	/* effective (overridable) subjective task
					 * credentials (COW) */
	struct mutex cred_guard_mutex;	/* guard against foreign influences on
					 * credential calculations
					 * (notably. ptrace) */
	struct cred *replacement_session_keyring; /* for KEYCTL_SESSION_TO_PARENT */

	char comm[TASK_COMM_LEN]; /* executable name excluding path
				     - access with [gs]et_task_comm (which lock
				       it with task_lock())
				     - initialized normally by setup_new_exec */
/* file system info */
	int link_count, total_link_count;
#ifdef CONFIG_SYSVIPC
/* ipc stuff */
	struct sysv_sem sysvsem;
#endif
#ifdef CONFIG_DETECT_HUNG_TASK
/* hung task detection */
	unsigned long last_switch_count;
#endif
/* CPU-specific state of this task */
	struct thread_struct thread;
/* filesystem information */
	struct fs_struct *fs;
/* open file information */
	struct files_struct *files;
/* namespaces */
	struct nsproxy *nsproxy;
/* signal handlers */
	struct signal_struct *signal;
	struct sighand_struct *sighand;

	sigset_t blocked, real_blocked;
	sigset_t saved_sigmask;	/* restored if set_restore_sigmask() was used */
	struct sigpending pending;

	unsigned long sas_ss_sp;
	size_t sas_ss_size;
	int (*notifier)(void *priv);
	void *notifier_data;
	sigset_t *notifier_mask;
	struct audit_context *audit_context;
#ifdef CONFIG_AUDITSYSCALL
	uid_t loginuid;
	unsigned int sessionid;
#endif
	seccomp_t seccomp;

/* Thread group tracking */
   	u32 parent_exec_id;
   	u32 self_exec_id;
/* Protection of (de-)allocation: mm, files, fs, tty, keyrings, mems_allowed,
 * mempolicy */
	spinlock_t alloc_lock;

#ifdef CONFIG_GENERIC_HARDIRQS
	/* IRQ handler threads */
	struct irqaction *irqaction;
#endif

	/* Protection of the PI data structures: */
	raw_spinlock_t pi_lock;

#ifdef CONFIG_RT_MUTEXES
	/* PI waiters blocked on a rt_mutex held by this task */
	struct plist_head pi_waiters;
	/* Deadlock detection and priority inheritance handling */
	struct rt_mutex_waiter *pi_blocked_on;
#endif

#ifdef CONFIG_DEBUG_MUTEXES
	/* mutex deadlock detection */
	struct mutex_waiter *blocked_on;
#endif
#ifdef CONFIG_TRACE_IRQFLAGS
	unsigned int irq_events;
	unsigned long hardirq_enable_ip;
	unsigned long hardirq_disable_ip;
	unsigned int hardirq_enable_event;
	unsigned int hardirq_disable_event;
	int hardirqs_enabled;
	int hardirq_context;
	unsigned long softirq_disable_ip;
	unsigned long softirq_enable_ip;
	unsigned int softirq_disable_event;
	unsigned int softirq_enable_event;
	int softirqs_enabled;
	int softirq_context;
#endif
#ifdef CONFIG_LOCKDEP
# define MAX_LOCK_DEPTH 48UL
	u64 curr_chain_key;
	int lockdep_depth;
	unsigned int lockdep_recursion;
	struct held_lock held_locks[MAX_LOCK_DEPTH];
	gfp_t lockdep_reclaim_gfp;
#endif

/* journalling filesystem info */
	void *journal_info;

/* stacked block device info */
	struct bio_list *bio_list;

/* VM state */
	struct reclaim_state *reclaim_state;

	struct backing_dev_info *backing_dev_info;

	struct io_context *io_context;

	unsigned long ptrace_message;
	siginfo_t *last_siginfo; /* For ptrace use.  */
	struct task_io_accounting ioac;
#if defined(CONFIG_TASK_XACCT)
	u64 acct_rss_mem1;	/* accumulated rss usage */
	u64 acct_vm_mem1;	/* accumulated virtual memory usage */
	cputime_t acct_timexpd;	/* stime + utime since last update */
#endif
#ifdef CONFIG_CPUSETS
	nodemask_t mems_allowed;	/* Protected by alloc_lock */
	int cpuset_mem_spread_rotor;
#endif
#ifdef CONFIG_CGROUPS
	/* Control Group info protected by css_set_lock */
	struct css_set *cgroups;
	/* cg_list protected by css_set_lock and tsk->alloc_lock */
	struct list_head cg_list;
#endif
#ifdef CONFIG_FUTEX
	struct robust_list_head __user *robust_list;
#ifdef CONFIG_COMPAT
	struct compat_robust_list_head __user *compat_robust_list;
#endif
	struct list_head pi_state_list;
	struct futex_pi_state *pi_state_cache;
#endif
#ifdef CONFIG_PERF_EVENTS
	struct perf_event_context *perf_event_ctxp;
	struct mutex perf_event_mutex;
	struct list_head perf_event_list;
#endif
#ifdef CONFIG_NUMA
	struct mempolicy *mempolicy;	/* Protected by alloc_lock */
	short il_next;
#endif
	atomic_t fs_excl;	/* holding fs exclusive resources */
	struct rcu_head rcu;

	/*
	 * cache last used pipe for splice
	 */
	struct pipe_inode_info *splice_pipe;
#ifdef	CONFIG_TASK_DELAY_ACCT
	struct task_delay_info *delays;
#endif
#ifdef CONFIG_FAULT_INJECTION
	int make_it_fail;
#endif
	struct prop_local_single dirties;
#ifdef CONFIG_LATENCYTOP
	int latency_record_count;
	struct latency_record latency_record[LT_SAVECOUNT];
#endif
	/*
	 * time slack values; these are used to round up poll() and
	 * select() etc timeout values. These are in nanoseconds.
	 */
	unsigned long timer_slack_ns;
	unsigned long default_timer_slack_ns;

	struct list_head	*scm_work_list;
#ifdef CONFIG_FUNCTION_GRAPH_TRACER
	/* Index of current stored address in ret_stack */
	int curr_ret_stack;
	/* Stack of return addresses for return function tracing */
	struct ftrace_ret_stack	*ret_stack;
	/* time stamp for last schedule */
	unsigned long long ftrace_timestamp;
	/*
	 * Number of functions that haven't been traced
	 * because of depth overrun.
	 */
	atomic_t trace_overrun;
	/* Pause for the tracing */
	atomic_t tracing_graph_pause;
#endif
#ifdef CONFIG_TRACING
	/* state flags for use by tracers */
	unsigned long trace;
	/* bitmask of trace recursion */
	unsigned long trace_recursion;
#endif /* CONFIG_TRACING */
#ifdef CONFIG_CGROUP_MEM_RES_CTLR /* memcg uses this to do batch job */
	struct memcg_batch_info {
		int do_batch;	/* incremented when batch uncharge started */
		struct mem_cgroup *memcg; /* target memcg of uncharge */
		unsigned long bytes; 		/* uncharged usage */
		unsigned long memsw_bytes; /* uncharged mem+swap usage */
	} memcg_batch;
#endif
};

  下图示意性地描述了 Linux 的进程描述符。

深入理解 Linux 内核_linux_05

(1)标识一个进程

  一般来说,能被独立调度的每个执行上下文都必须拥有它自己的进程描述符:因此,即使共享内核大部分数据结构的轻量级进程,也有它们自己的 task_struct 结构。

  进程和进程描述符之间有非常严格的一一对应关系,这使得用 32 位进程描述符地址(注 3)标识进程成为一种方便的方式。进程描述符指针指向这些地址,内核对进程的大部分引用是通过进程描述符指针进行的。

  另一方面,类 Unix 操作系统允许用户使用一个叫做进程标识符 process ID(或 PID)的数来标识进程,PID 存放在进程描述符的 pid 字段中。PID 被顺序编号,新创建进程的 PID 通常是前一个进程的 PID1。不过,PID 的值有一个上限,当内核使用的 PID 达到这个上限值的时候就必须开始循环使用已闲置的小 PID 号。在缺省情况下,最大的 PID 号是 32767PID_MAX_DEFAULT-1):系统管理员可以通过往 /proc/sys/kernel/pid_max 这个文件中写入一个更小的值来减小 PID 的上限值,使 PID 的上限小于 32767。( /proc 是一个特殊文件系统的安装点,参看第十二章"特殊文件系统"一节。)在 64 位体系结构中,系统管理员可以把 PID 的上限扩大到 4194303
  由于循环使用 PID 编号,内核必须通过管理一个 pidmap-array 位图来表示当前已分配的 PID 号和闲置的 PID 号。因为一个页框包含 32768 个位,所以在 32 位体系结构中 pidmap-array 位图存放在一个单独的页中。然而,在 64 位体系结构中,当内核分配了超过当前位图大小的 PID 号时,需要为 PID 位图增加更多的页。系统会一直保存这些页不被释放。

  Linux 把不同的 PID 与系统中每个进程或轻量级进程相关联(本章后面我们会看到,在多处理器系统上稍有例外)。这种方式能提供最大的灵活性,因为系统中每个执行上下文都可以被唯一地识别。

  另一方面,Unix 程序员希望同一组中的线程有共同的 PID。例如,把指定 PID 的信号发给组中的所有线程。事实上,POSIX 1003.1c 标准规定一个多线程应用程序中的所有线程都必须有相同的 PID

  遵照这个标准,Linux 引入线程组的表示。一个线程组中的所有线程使用和该线程组的领头线程(thread group leader)相同的 PID,也就是该组中第一个轻量级进程的 PID,它被存入进程描述符的 tgid 字段中。getpid() 系统调用返回当前进程的 tgid 值而不是 pid 的值,因此,一个多线程应用的所有线程共享相同的 PID。绝大多数进程都属于一个线程组,包含单一的成员线程组的领头线程其 tgid 的值与 pid 的值相同,因而 getpid() 系统调用对这类进程所起的作用和一般进程是一样的。

  下面,我们将向你说明如何从进程的 PID 中有效地导出它的描述符指针。效率至关重要,因为像 kill() 这样的很多系统调用使用 PID 表示所操作的进程。

(2)进程描述符处理

  进程是动态实体,其生命周期范围从几毫秒到几个月。因此,内核必须能够同时处理很多进程,并把进程描述符存放在动态内存中,而不是放在永久分配给内核的内存区(译注1)。对每个进程来说,Linux 都把两个不同的数据结构紧凑地存放在一个单独为进程分配的存储区域内:一个是内核态的进程堆栈,另一个是紧挨进程描述符的小数据结构 thread_info,叫做线程描述符,这块存储区域的大小通常为 8192 个字节(两个页框)。考虑到效率的因素,内核让这 8K 空间占据连续的两个页框并让第一个页框的起始地址是 213 的倍数。当几乎没有可用的动态内存空间时,就会很难找到这样的两个连续页框,因为空闲空间可能存在大量碎片(见第八章 “伙伴系统算法” 一节)。因此,在 80x86 体系结构中,在编译时可以进行设置,以使内核栈和线程描述符跨越一个单独的页框(4096 个字节)。

  在第二章 “Linux 中的分段” 一节中我们已经知道,内核态的进程访问处于内核数据段的栈,这个栈不同于用户态的进程所用的栈。因为内核控制路径使用很少的栈,因此只需要几千个字节的内核态堆栈。所以,对栈和 thread_info 结构来说,8KB 足够了。不过,当使用一个页框(一页内存,4K)存放内核态堆栈和 thread_info 结构时,内核要采用一些额外的栈以防止中断和异常的深度嵌套而引起的溢出(见第四章)。

  图 3-2 显示了在 2 页(8KB)内存区中存放两种数据结构的方式。线程描述符驻留于这个内存区的开始,而栈从末端向下增长。该图还显示了分别通过 taskthread_info 字段使 thread_info 结构与 task_struct 结构互相关联。

深入理解 Linux 内核_页表_06

  esp 寄存器是 CPU 栈指针,用来存放栈顶单元的地址。在 80x86 系统中,栈起始于末端,并朝这个内存区开始的方向增长。从用户态刚切换到内核态以后,进程的内核栈总是空的,因此,esp 寄存器指向这个栈的顶端。

  一旦数据写入堆栈,esp 的值就递减。因为 thread_info 结构是 52 个字节长,因此内核栈能扩展到 8140 个字节。
C 语言使用下列的联合结构方便地表示一个进程的线程描述符和内核栈:

union thread_union {
	struct thread_info thread_info;
	unsigned long stack[2048];		/* 对 4K 的核数组下标是 1024 */
};

  如图 3-2 所示,thread_info 结构从 0x015fa000 地址处开始存放,而栈从 0x015fc000 地址处开始存放。esp 寄存器的值指向地址为 0x015fa878 的当前栈顶。

  内核使用 alloc_thread_infofree_thread_info 宏分配和释放存储 thread_info 结构和内核栈的内存区。

(3)标识当前进程

  从效率的观点来看,刚才所讲的 thread_info 结构与内核态堆栈之间的紧密结合提供的主要好处是:内核很容易从 esp 寄存器的值获得当前在 CPU 上正在运行进程的 thread_info 结构的地址。事实上,如果 thread_union 结构长度是 8K213 字节),则内核屏蔽掉 esp 的低 13 位有效位就可以获得 thread_info 结构的基地址; 而如果 thread_union 结构长度是 4K,内核需要屏蔽掉 esp 的低 12 位有效位。这项工作由 current_thread_info() 函数来完成,它产生如下一些汇编指令:

movl $0xffffe000, %ecx 	# /* 或者是用于 4K 堆栈的 0xfffff000 */
andl %esp, %ecx
movl %ecх, p

  这三条指令执行以后,p 就包含在执行指令的 CPU 上运行的进程的 thread_info 结构的指针。

  进程最常用的是进程描述符的地址而不是 thread_info 结构的地址。为了获得当前在 CPU 上运行进程的描述符指针,内核要调用 current 宏,该宏本质上等价于 current_thread_info()->task ,它产生如下汇编语言指令:

movl $0xffffe000, %ecx 		# /* 或者是用于 4K堆栈的 0xfffff000 */
andl %esp, %ecx
movl (%ecx), p

  因为 task 字段在 thread_info 结构中的偏移量为 0,所以执行完这三条指令之后,p 就包含在 CPU 上运行进程的描述符指针。

  current 宏经常作为进程描述符字段的前缀出现在内核代码中,例如,current->pid 返回在 CPU 上正在执行的进程的 PID

  用栈存放进程描述符的另一个优点体现在多处理器系统上:如前所述,对于每个硬件处理器,仅通过检查栈就可以获得当前正确的进程。早先的 Linux 版本没有把内核栈与进程描述符存放在一起,而是强制引入全局静态变量 current 来标识正在运行进程的描述符。在多处理器系统上,有必要把 current 定义为一个数组,每一个元素对应一个可用 CPU

3、进程切换

  进程切换可能只发生在精心定义的点:schedule() 函数。从本质上说,每个进程切换由两步组成:

  1. 切换页全局目录以安装一个新的地址空间;
  2. 切换内核态堆栈和硬件上下文,因为硬件上下文提供了内核执行新进程所需要的所有信息,包含 CPU 寄存器。

(1)switch_to 宏

// arch/x86/include/asm/system.h
#define switch_to(prev, next, last) \
	asm volatile(SAVE_CONTEXT					  \
	     "movq %%rsp,%P[threadrsp](%[prev])\n\t" /* save RSP */	  \
	     "movq %P[threadrsp](%[next]),%%rsp\n\t" /* restore RSP */	  \
	     "call __switch_to\n\t"					  \
	     "movq "__percpu_arg([current_task])",%%rsi\n\t"		  \
	     __switch_canary						  \
	     "movq %P[thread_info](%%rsi),%%r8\n\t"			  \
	     "movq %%rax,%%rdi\n\t" 					  \
	     "testl  %[_tif_fork],%P[ti_flags](%%r8)\n\t"		  \
	     "jnz   ret_from_fork\n\t"					  \
	     RESTORE_CONTEXT						  \
	     : "=a" (last)					  	  \
	       __switch_canary_oparam					  \
	     : [next] "S" (next), [prev] "D" (prev),			  \
	       [threadrsp] "i" (offsetof(struct task_struct, thread.sp)), \
	       [ti_flags] "i" (offsetof(struct thread_info, flags)),	  \
	       [_tif_fork] "i" (_TIF_FORK),			  	  \
	       [thread_info] "i" (offsetof(struct task_struct, stack)),   \
	       [current_task] "m" (current_task)			  \
	       __switch_canary_iparam					  \
	     : "memory", "cc" __EXTRA_CLOBBER)
(a)分析

  进程切换的第二步由 switch_to 宏执行。它是内核中与硬件关系最密切的例程之一,要理解它到底做了些什么我们必须下些功夫。

  首先,该宏有三个参数,它们是 prevnextlast 。你可能很容易猜到 prevnext 的作用:它们仅是局部变量 prevnext 的占位符,即它们是输入参数,分别表示被替换进程和新进程描述符的地址在内存中的位置。
  那第三个参数 last 呢 ?在任何进程切换中,涉及到三个进程而不是两个。假设内核决定暂停进程 A 而激活进程 B。在 schedule() 函数中,prev 指向 A 的描述符而 next 指向 B 的描述符。switch_to 宏一但使 A 暂停,A 的执行流就冻结。

  随后,当内核想再次此激活 A,就必须暂停另一个进程 C(这通常不同于 B),于是就要用 prev 指向 Cnext 指向 A 来执行另一个 switch_to 宏。当 A 恢复它的执行流时,就会找到它原来的内核栈,于是 prev 局部变量还是指向 A 的描述符而 next 指向 B 的描述符。此时,代表进程 A 执行的内核就失去了对 C 的任何引用。但是,事实表明这个引用对于完成进程切换是很有用的(更多细节参见第七章)。

  switch_to 宏的最后一个参数是输出参数,它表示宏把进程 C 的描述符地址写在内存的什么位置了(当然,这是在 A 恢复执行之后完成的)。在进程切换之前,宏把第一个输入参数 prev(即在 A 的内核堆栈中分配的 prev 局部变量)表示的变量的内容存入 CPUeax 寄存器。在完成进程切换,A 已经恢复执行时,宏把 CPUeax 寄存器的内容写入由第三个输出参数 —— last 所指示的 A 在内存中的位置。因为 CPU 寄存器不会在切换点发生变化,所以 C 的描述符地址也存在内存的这个位置。在 schedule() 执行过程中,参数 last 指向 A 的局部变量 prev,所以 prevC 的地址覆盖。

  图 3-7 显示了进程 ABC 内核堆栈的内容以及 eax 寄存器的内容。必须注意的是:图中显示的是在被 eax 寄存器的内容覆盖以前的 prev 局部变量的值。

深入理解 Linux 内核_linux_07

  1. eaxedx 寄存器中分别保存 prevnext 的值:
movl prev, %eax
movl next, %edx
  1. eflagsebp 寄存器的内容保存在 prev 内核栈中。必须保存它们的原因是编译器认为在 switch_to 结束之前它们的值应当保持不变。
pushfl
pushl %ebp
  1. esp 的内容保存到 prev->thread.esp 中以使该字段指向 prev 内核栈的栈顶:
movl %esp,484(%eax)

  484(%eax) 操作数表示内存单元的地址为 eax 内容加上 484

  1. next->thread.esp 装入 esp。此时,内核开始在 next 的内核栈上操作,因此这条指令实际上完成了从 prevnext 的切换。由于进程描述符的地址和内核栈的地址紧挨着(就像我们在本章前面"标识一个进程"一节所解释的),所以改变内核栈意味着改变当前进程。
movl 484(%edx), %esp
  1. 把标记为 1 的地地址(本节后面所示)存入 prev->thread.eip。当被替换的进程重新恢复执行时,进程执行被标记为 1 的那条指令:
movl $1f, 480(%eax)
  1. 宏把 next->thread.eip 的值(绝大多数情况下是一个被标记为 1 的地址)压入 next 的内核栈:
pushl 480(%edx)
  1. 跳到 __switch_to()C 函数(见下面):
jmp __switch_to
  1. 这里被进程 B 替换的进程 A 再次获得 CPU:它执行一些保存 eflagsebp 寄存器内容的指令,这两条指令的第一条指令被标记为 1
1:
	popl %ebp
	popfl

  注意这些 pop 指令是怎样引用 prev 进程的内核栈的。当进程调度程序选择了 prev 作为新进程在 CPU 上运行时,将执行这些指令。于是,以 prev 作为第二个参数调用 switch_to。因此,esp 寄存器指向 prev 的内核栈。

  1. 拷贝 eax 寄存器(上面步骤 1 中被装载)的内容到 switch_to 宏的第三个参数 last 标识的内存区域中:
movl %eax, last

  正如先前讨论的,eax 寄存器指向刚被替换的进程的描述符(注 6)。

(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_pnext_p 参数,这两个参数表示前一个进程和新进程。这个函数的调用不同于一般函数的调用,因为 _switch_to()eaxedx 取参数 prev_pnext_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),这种操作才能进行。这些寄存器不需要被保存,因为只有当一个调试器想要监控 prevprev_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 端口时,真实位图才被拷贝到本地 CPUTSS中。进程的定制 I/O 权限位图被保存在 thread_info 结构的 io_bitmap_ptr 字段指向的缓冲区中。 handle_io_bitmap() 函数为 next_p 进程设置本地 CPU 使用的 TSSio_bitmap 字段如下:

  • 如果 next_p 进程不拥有自己的 I/O 权限位图,则 TSSio_bitmap字段被设为 0x8000
  • 如果 next_p 进程拥有自己的 I/O 权限位图,则 TSSio_bitmap 字段被设为 0x9000

  TSSio_bitmap 字段应当包含一个在 TSS 中的偏移量,其中存放实际位图。无论何时用户态进程试图访问一个 I/O 端口,0x80000x9000 指向 TSS 界限之外并将因此引起 “General protection” 异常(参见第四章的 “异常” 一节)。do_general_protection() 异常处理程序将检查保存在 io_bitmap 字段的值;如果是 0x8000,函数发送一个 SIGSEGV 信号给用户态进程; 如果是 0x9000,函数把进程位图(由 thread_info 结构中的 io_bitmap_ptr 字段指示)拷贝到本地 CPUTSS 中,把 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(¤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() 的最重要的步骤:

  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 相比,它具有更加强壮的安全模型。详情参见第二十章。
  2. 调用 dup_task_struct() 为子进程获取进程描述符。该函数执行如下操作:
  • 如果需要,则在当前进程中调用 __unlazy_fpu(),把 FPUMMXSSE/SSE2 寄存器的内容保存到父进程的 thread_info 结构中。稍后,dup_task_struct() 将把这些值复制到子进程的 thread_info 结构中。
  • 执行 alloc_task_struct() 宏,为新进程获取进程描述符(task_struct 结构),并将描述符地址保存在 tsk 局部变量中。
  • 执行 alloc_thread_info 宏以获取一块空闲内存区,用来存放新进程的 thread_info 结构和内核栈,并将这块内存区字段的地址存在局部变量 ti 中。正如在本章前面 “标识一个进程” 一节中所述:这块内存区字段的大小是 8KB4KB
  • current 进程描述符的内容复制到 tsk 所指向的 task_struct 结构中,然后把 tsk->thread_info 置为 ti
  • current 进程的 thread_info 描述符的内容复制到 ti 所指向的结构中,然后把 ti->task 置为 tsk
  • 把新进程描述符的使用计数器(tsk->usage)置为 2,用来表示进程描述符正在被使用而且其相应的进程处于活动状态(进程状态即不是 EXIT_ZOMBIE,也不是 EXIT_DEAD)。
  • 返回新进程的进程描述符指针(tsk)。
  1. 检查存放在 current->signal->rlim[RLIMIT_NPROC].rlim_cur 变量中的值是否小于或等于用户所拥有的进程数。如果是,则返回错误码,除非进程没有 root 权限。该函数从每用户数据结构 user_struct 中获取用户所拥有的进程数。通过进程描述符 user 字段的指针可以找到这个数据结构。
  2. 递增 user_struct 结构的使用计数器 (tsk->user->__count 字段)和用户所拥有的进程的计数器(tsk->user->processes)。
  3. 检查系统中的进程数量(存放在 nr_threads 变量中)是否超过 max_threads 变量的值。这个变量的缺省值取决于系统内存容量的大小。总的原则是:所有 thread_info 描述符和内核栈所占用的空间不能超过物理内存大小的 1/8。不过,系统管理员可以通过写 /proc/sys/kernel/threads-max 文件来改变这个值。
  4. 如果实现新进程的执行域和可执行格式的内核函数(参见第二十章)都包含在内核模块中,则递增它们的使用计数器(参见附录二)。
  5. 设置与进程状态相关的几个关键字段:
  • 把大内核锁计数器 tsk->lock_depth 初始化为 -1(参见第五章 “大内核锁” 一节)。
  • tsk->did_exec 字段初始化为 0 :它记录了进程发出的 execve() 系统调用的次数。
  • 更新从父进程复制到 tsk->flags 字段中的一些标志:首先清除 PF_SUPERPRIV 标志,该标志表示进程是否使用了某种超级用户权限。然后设置 PF_FORKNOEXEC 标志,它表示子进程还没有发出 execve() 系统调用。
  1. 把新进程的 PID 存入 tsk->pid 字段。
  2. 如果 clone_flags 参数中的 CLONE_PARENT_SETTID 标志被设置,就把子进程的 PID 复制到参数 parent_tidptr 指向的用户态变量中。
  3. 初始化子进程描述符中的 list_head 数据结构和自旋锁,并为与挂起信号、定时器及时间统计表相关的几个字段赋初值。
  4. 调用 copy_semundo()copy_files()copy_fs()copy_sighand()copy_signa1()copy_mm()copy_namespace() 来创建新的数据结构,并把父进程相应数据结构的值复制到新数据结构中,除非 clone_flags 参数指出它们有不同的值。
  5. 调用 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)。
  6. 如果 clone_flags 参数的值被置为 CLONE_CHILD_SETTIDCLONE_CHILD_CLEARTID,就把 child_tidptr 参数的值分别复制到 tsk->set_chid_tidtsk->clear_child_tid 字段。这些标志说明:必须改变子进程用户态地址空间的 child_tidptr 所指向的变量的值,不过实际的写操作要稍后再执行。
  7. 清除子进程 thread_info 结构的 TIF_SYSCALL_TRACE 标志,以使 ret_from_fork() 函数不会把系统调用结束的消息通知给调试进程(参见第十章 “进入和退出系统调用” 一节)。(为对子进程的跟踪是由 tsk->ptrace 中的 PTRACE_SYSCALL 标志来控制的,所以子进程的系统调用跟踪不会被禁用。)
  8. clone_flags 参数低位的信号数字编码初始化 tsk->exit_signal 字段,如果 CLONE_THREAD 标志被置位,就把 tsk->exit_signal 字段初始化为 -1。正如我们将在本章稍后 “进程终止” 一节所看见的,只有当线程组的最后一个成员(通常是线程组的领头)“死亡”,才会产生一个信号,以通知线程组的领头进程的父进程。
  9. 调用 sched_fork() 完成对新进程调度程序数据结构的初始化。该函数把新进程的状态设置为 TASK_RUNNING,并把 thread_info 结构的 preempt_count 字段设置为 1,从而禁止内核抢占(参见第五章 “内核抢占” 一节)。此外,为了保证公平的进程调度,该函数在父子进程之间共享父进程的时间片(参见第七章 “scheduler_tick() 数” 一节)。
  10. 把新进程的 thread_info 结构的 cpu 字段设置为由 smp_processor_id() 所返回的本地 CPU 号。
  11. 初始化表示亲子关系的字段。尤其是,如果 CLONE_PARENTCLONE_THREAD ,被设置,就用 current->real_parent 的值初始化 tsk->real_parenttsk->parent,因此,子进程的父进程似乎是当前进程的父进程。否则,把 tsk->real_parenttsk->parent 置为当前进程。
  12. 如果不需要跟踪子进程(没有设置 CLONE_PTRAC 标志),就把 tsk->ptrace 字段设置为 0tsk->ptrace 字段会存放一些标志,而这些标志是在一个进程被另外一个进程跟踪时才会用到的。采用这种方式,即使当前进程被跟踪,子进程也不会被跟踪。
  13. 执行 SET_LINKS 宏,把新进程描述符插入进程链表。
  14. 如果子进程必须被跟踪(tsk->ptrace 字段的 PT_PTRACED 标志被设置),就把 current->parent 赋给 tsk->parent,并将子进程插入调试程序的跟踪链表中。
  15. 调用 attach_pid() 把新进程插述符的 PID 插入 pidhash[PIDTYPE_PID] 散列表。
  16. 如果子进程是线程组的领头进程(CLONE_THREAD 标志被清 0):
  • tsk->tgid 的初值置为 tsk->pid
  • tsk->group_leader 的初值置为 tsk
  • 调用三次 attach_pid() ,把子进程分别插入 PIDTYPE_TGIDPIDTYPE_PGIDPIDTYPE_SID 类型的 PID 散列表。
  1. 否则,如果子进程属于它的父进程的线程组(CLONE_THREAD 标志被设置):
  • tsk->tgid 的初值置为 tsk->current->tgid
  • tsk->group_leader 的初值置为 current->group_leader 的值。
  • 调用 attach_pid(),把子进程插入 PIDTYPE_TGID 类型的散列表中(更具体地说,插入 current->group_leader 进程的每个 PID 链表)。
  1. 现在,新进程已经被加入进程集合:递增 nr_threads 变量的值。
  2. 递增 total_forks 变量以记录被创建的进程的数量。
  3. 终止并返回子进程描述符指针(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()ebxedx 分别设置为参数 fnarg 的值。
  • 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]),					\
}

  所有进程的祖先叫做进程 0idle 进程或因为历史的原因叫做 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);

  新创建内核线程的 PID1,并与进程 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()(正常结束)指定的一个值,也可能是内核提供的一个错误代号(异常结束)。该函数执行下述操作:

  1. 检查退出进程的 SIGNAL_GROUP_EXIT 标志是否不为 0,如果不为 0,说明内核已经开始为线程组执行退出的过程。在这种情况下,就把存放在 current->signal->group_exit_code 中的值当作退出码,然后跳转到第 4 步。
  2. 否则,设置进程的 SIGNAL_GROUP_EXIT 标志并把终止代号存放到 current->signal->group_exit_code 字段。
  3. 调用 zap_other_threads() 函数杀死 current 线程组中的其他进程(如果有的话)。为了完成这个步骤,函数扫描与 current->tgid 对应的 PIDTYPE_TGID 类型的散列表中的每个 PID 链表,向表中所有不同于 current 的进程发送 SIGKILL 信号(参见第十一章),结果,所有这样的进程都将执行 do_exit() 函数,从而被杀死。
  4. 调用 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() 函数接受进程的终止代号作为参数并执行下列操作:

  1. 把进程描述符的 flag 字段设置为 PF_EXITING 标志,以表示进程正在被删除。
  2. 如果需要,通过函数 del_timer_sync()(参见第六章)从动态定时器队列中删除进程描述符。
  3. 分别调用 exit_mm()exit_sem()__exit_files()__exit_fs()exit_namespace()exit_thread() 函数从进程描述符中分离出与分页、信号量、文件系统、打开文件描述符、命名空间以及 I/O 权限位图相关的数据结构。如果没有其他进程共享这些数据结构,那么这些函数还删除所有这些数据结构中。
  4. 如果实现了被杀死进程的执行域和可执行格式(参见第二十章)的内核函数包含在内核模块中,则函数递减它们的使用计数器。
  5. 把进程描述符的 exit_code 字段设置成进程的终止代号,这个值要么是 _exit()exit_group() 系统调用参数(正常终止),要么是由内核提供的一个错误代号(异常终止)。
  6. 调用 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() 函数" 一节)。
  1. 调用 schedule() 函数(参见第七章)选择一个新进程运行。调度程序忽略处于 EXIT_ZOMBIE 状态的进程,所以这种进程正好在 schedule() 中的宏 switch_to 被调用之后停止执行。正如在第七章我们将看到的:调度程序将检查被替换的僵死进程描述符的 PF_DEAD 标志并递减使用计数器,从而说明进程不再存活的事实。

5、进程删除

  Unix 允许进程查询内核以获得其父进程的 PID,或者其任何子进程的执行状态。例如,进程可以创建一个子进程来执行特定的任务,然后调用诸如 wait() 这样的一些库函数检查子进程是否终止。如果子进程已经终止,那么,它的终止代号将告诉父进程这个任务是否已成功地完成。

  为了遵循这些设计选择,不允许 Unix 内核在进程一终止后就丢弃包含在进程描述符字段中的数据。只有父进程发出了与被终止的进程相关的 wait() 类系统调用之后,才允许这样做。这就是引入僵死状态的原因:尽管从技术上来说进程已死,但必须保存它的描述符,直到父进程得到通知。

  如果父进程在子进程结束之前结束会发生什么情况呢? 在这种情况下,系统中会到处是僵死的进程,而且它们的进程描述符永久占据着 RAM。如前所述,必须强迫所有的孤儿进程成为 init 进程的子进程来解决这个问题。这样,init 进程在用 wait() 类系统调用检查其合法的子进程终止时,就会撤消僵死的进程。

  release_task() 函数从僵死进程的描述符中分离出最后的数据结构,对僵死进程的处理有两种可能的方式:如果父进程不需要接收来自子进程的信号,就调用 do_exit() ;如果已经给父进程发送了一个信号,就调用 wait4()waitpid() 系统调用。在后一种情况下,函数还将回收进程描述符所占用的内存空间,而在前一种情况下,内存的回收将由进程调度程序来完成(参见第七章)。该函数执行下述步骤:

  1. 递减终止进程拥有者的进程个数。这个值存放在本章前面提到的 user_struct 结构中(参见 copy_process() 的第 4 步)。
  2. 如果进程正在被跟踪,函数将它从调试程序的 ptrace_children 链表中删除,并让该进程重新属于初始的父进程。
  3. 调用 __exit_signal() 删除所有的挂起信号并释放进程的 signal_struct 描述符。如果该描述符不再被其他的轻量级进程使用,函数进一步删除这个数据结构。此外,函数调用 exit_itimers() 从进程中剥离掉所有的 POSIX 时间间隔定时器。
  4. 调用 __exit_sighand() 删除信号处理函数。
  5. 调用 _unhash_process(),该函数依次执行下面的操作:
  • 变量 nr_threads1
  • 两次调用 detach_pid(),分别从 PIDTYPE_PIDPIDTYPE_TGID 类型的 PID 散列表中删除进程描述符。
  • 如果进程是线程组的领头进程,那么再调用两次 detach_pid(),从 PIDTYPE_PGIDPIDTYPE_SID 类型的散列表中删除进程描述符。
  • 用宏 REMOVE_LINKS 从进程链表中解除进程描述符的链接。
  1. 如果进程不是线程组的领头进程,领头进程处于僵死状态,而且进程是线程组的最后一个成员,则该函数向领头进程的父进程发送一个信号,通知它进程已死亡。
  2. 调用 sched_exit() 函数来调整父进程的时间片(这一步在逻辑上作为对 copy_process()17 步的补充)。
  3. 调用 put_task_struct() 递减进程描述符的使用计数器,如果计数器变为 0,则函数终止所有残留的对进程的引用。
  • 递减进程所有者的 user_struct 数据结构的使用计数器(__count 字段)(参见 copy_process() 的第 5 步),如果使用计数器变为 0,就释放该数据结构。
  • 释放进程描述符以及 thread_info 描述符和内核态堆栈所占用的内存区域。

三、中断和异常

  中断处理是由内核执行的最敏感的任务之一,因为它必须满足下列约束:

  • 当内核正打算去完成一些别的事情时,中断随时会到来。因此,内核的目标就是让中断尽可能快地处理完,尽其所能把更多的处理向后推迟。例如,假设一个数据块已到达了网线,当硬件中断内核时,内核只简单地标志数据到来了,让处理器恢复到它以前运行的状态,其余的处理稍后再进行(如把数据移入一个缓冲区,它的接收进程可以在缓冲区找到数据并恢复这个进程的执行)。因此,内核响应中断后需要进行的操作分为两部分:关键而紧急的部分,内核立即执行,其余推迟的部分,内核随后执行。
  • 因为中断随时会到来,所以内核可能正在处理其中的一个中断时,另一个中断(不同类型)又发生了。应该尽可能多地允许这种情况发生,因为这能维持更多的 I/O 设备处于忙状态(参见 “中断和异常处理程序的嵌套执行” 一节)。因此,中断处理程序必须编写成使相应的内核控制路径能以嵌套的方式执行。当最后一个内核控制路径终止时,内核必须能恢复被中断进程的执行,或者,如果中断信号已导致了重新调度,内核能切换到另外的进程。
  • 尽管内核在处理前一个中断时可以接受一个新的中断,但在内核代码中还是存在一些临界区,在临界区中,中断必须被禁止。必须尽可能地限制这样的临界区,因为根据以前的要求,内核,尤其是中断处理程序,应该在大部分时间内以开中断的方式运行。

1、中断和异常

  Intel 文档把中断和异常分为以下几类:

  • 中断:
  • 可屏蔽中断 (maskable interrupt
    I/O 设备发出的所有中断请求(IRQ)都产生可屏蔽中断。可屏蔽中断可以处于两种状态:屏蔽的(masked)或非屏蔽的(unmasked),一个屏蔽的中断只要还是屏蔽的,控制单元就忽略它。
  • 非屏蔽中断 (nonmaskable Interrupt
    只有几个危急事件(如硬件故障)才引起非屏蔽中断。非屏蔽中断总是由 CPU 辨认。
  • 异常:
  • 处理器探测异常 (processor-detected exception
    CPU 执行指令时探测到的一个反常条件所产生的异常。可以进一步分为三组,这取决于 CPU 控制单元产生异常时保存在内核态堆栈 eip 寄存器中的值。
  • 故障 (fault
    通常可以纠正,一旦纠正,程序就可以在不失连贯性的情况下重新开始。保存在 eip 中的值是引起故障的指令地址,因此,当异常处理程序终止时,那条指令会被重新执行。我们将在第九章的 “缺页异常处理程序” 一节中看到,只要处理程序能纠正引起异常的反常条件,重新执行同一指令就是必要的。
  • 陷阱(trap
    在陷阱指令执行后立即报告;内核把控制权返回给程序后就可以继续它的执行而不失连贯性。保存在 eip 中的值是一个随后要执行的指令地址。只有当没有必要重新执行已终止的指令时,才触发陷阱。陷阱的主要用途是为了调试程序。在这种情况下,中断信号的作用是通知调试程序一条特殊指令已被执行(例如到了一个程序内的断点)。一旦用户检查到调试程序所提供的数据,她就可能要求被调试程序从下一条指令重新开始执行。
  • 异常中止(abort
    发生一个严重的错误:控制单元出了问题,不能在 eip 寄存器中保存引起异常的指令所在的确切位置。异常中止用于报告严重的错误,如硬件故障或系统表中无效的值或不一致的值。由控制单元发送的这个中断信号是紧急信号,用来把控制权切换到相应的异常中止处理程序,这个异常中止处理程序除了强制受影响的进程终止外,没有别的选择。
  • 编程异常 (programmed exception
    在编程者发出请求时发生。是由 intint3 指令触发的;当 into(检查溢出)和 bound(检查地址出界)指令检查的条件不为真时,也引起编程异常。控制单元把编程异常作为陷阱来处理。编程异常通常也叫做软中断(software interrupt)。这样的异常有两种常用的用途:执行系统调用及给调试程序通报一个特定的事件(参见第十章)。

  每个中断和异常是由 0 ~ 255 之间的一个数来标识。因为一些未知的原因,Intel 把这个 8 位的无符号整数叫做一个向量(vector)。非屏蔽中断的向量和异常的向量是固定的,而可屏蔽中断的向量可以通过对中断控制器的编程来改变(参见下一节)。

(1)IRQ 和中断

  每个能够发出中断请求的硬件设备控制器都有一条名为 IRQInterrupt ReQuest)的输出线(注1)。所有现有的 IRQ 线(IRQ line)都与一个名为可编程中断控制器(Programmable Interrpt Controuer PIC)的硬件电路的输入引脚相连。可编程中断控制器执行下列动作:

  1. 监视 IRQ 线。检查产生的信号(raised signal)。如果有条或两条以上的 IRQ 线上产生信号,就选择引脚编号较小的 IRQ 线。
  2. 如果一个引发信号出现在 IRQ 线上:
  • 把接收到的引发信号转换成对应的向量。
  • 把这个向量存放在中断控制器的一个I/O 端口,从而允许 CPU 通过数据总线读此向量
  • 把引发信号发送到处理器的 INTR 引脚,即产生一个中断。
  • 等待,直到 CPU 通过把这个中断信号写进可编程中断控制器的一个 I/O 端口来确认它:当这种情况发生时,清 INTR 线。
  1. 返回到第 1 步。

  IRQ 线是从 0 开始顺序编号的,因此,第一条 IRQ 线通常表示成 IRQ0。与 IRQn 关联的 Intel 的缺省向量是 n+32。如前所述,通过向中断控制器端口发布合适的指令,就可以修改 IRQ 和向量之间的映射。
  可以有选择地禁止每条 IRQ 线。因此,可以对 PIC 编程从而禁止 IRQ,也就是说,可以告诉 PIC 停止对给定的 IRQ 线发布中断,或者激话它们。禁止的中断是丢失不了的,它们一旦被激活,PIC 就又把它们发送到 CPU。这个特点被大多数中断处理程序使用,因为这允许中断处理程序逐次地处理同一类型的 IRQ

  有选择地激活/禁止 IRQ 线不同于可屏蔽中断的全局屏蔽 /非屏蔽。当 eflags 寄存器的 IF 标志被清 0 时,由 PIC 发布的每个可屏蔽中断都由 CPU 暂时忽略。clisti 汇编指令分别清除和设置该标志。可参考 ==> 1、标志寄存器

  传统的 PIC 是由两片 8259A 风格的外部芯片以 “级联” 的方式连接在一起的。每个芯片可以处理多达 8 个不同的 IRQ 输入线。因为从 PICINT 输出线连接到主 PICIRQ2 引脚,因此,可用 IRQ 线的个数限制为 15

(2)高级可编程中断控制器

  以前的描述仅涉及为单处理器系统设计的 PIC。如果系统只有一个单独的 CPU,那么主 PIC 的输出线以直接了当的方式连接到 CPUINTR 引脚。然而,如果系统中包含两个或多个 CPU,那么这种方式不再有效,因而需要更复杂的 PIC

  为了充分发挥 SMP 体系结构的并行性,能够把中断传递给系统中的每个 CPU 至关重要。基于此理由,IntelPentiun III 开始引入了一种名为 I/O 高级可编程控制器(I/O Advanced Programmable Interrupt Controller,I/O APIC)的新组件,用以代替老式的 8259A 可编程中断控制器。新近的主板为了支持以前的操作系统都包括两种芯片。此外,80x86 微处理器当前所有的 CPU 都含有一个本地 APIC。每个本地 APIC 都有 32 位的寄存器、一个内部时钟、一个本地定时设备及为本地 APIC 中断保留的两条额外的 IRQ 线 LINT0LINT1。所有本地 APIC 都连接到一个外部 I/O APIC,形成一个多 APIC 的系统。

  图 4-1 以示意图的方式显示了一个多 APIC 系统的结构。一条 APIC 总线把 “前端” I/O APIC 连接到本地 APIC。来自设备的 IRQ 线连接到 I/O APIC,因此,相对于本地 APICI/O APIC 起路由器的作用。在 Pentium III 和早期处理器的母板上,APIC 总线是一个串行三线总线;从 Pentium 4 开始,APIC 总线通过系统总线来实现。不过,因为 APIC 总线及其信息对软件是不可见的,因此,我们不做进一步的详细讨论。

  I/O APIC 的组成为:一组 24IRQ 线、一张 24 项的中断重定向表(Interrupt Redirection Table)、可编程寄存器,以及通过 APIC 总线发送和接收 APIC 信息的一个信息单元。与 8259AIRQ 引脚不同,中断优先级并不与引脚号相关联:中断重定向表中的每一项都可以被单独编程以指明中断向量和优先级、目标处理器及选择处理器的方式。重定向表中的信息用于把每个外部 IRQ 信号转换为一条消息,然后,通过 APIC 总线把消息发送给一个或多个本地 APIC 单元。

深入理解 Linux 内核_寄存器_08

  来自外部硬件设备的中断请求以两种方式在可用 CPU 之间分发:

  • 静态分发
    IRQ 信号传递给重定向表相应项中所列出的本地 APIC。中断立即传递给一个特定的 CPU,或一组 CPU,或所有 CPU(广播方式)。
  • 动态分发
    如果处理器正在执行最低优先级的进程,IRQ 信号就传递给这种处理器的本地 APIC。每个本地 APIC 都有一个可编程任务优先级寄存器(task priority register,TPR), TPR 用来计算当前运行进程的优先级。Intel 希望在操作系统内核中通过每次进程切换对这个寄存器存器进行修改。

    如果两个或多个 CPU 共享最低优先级,就利用仲裁(arbitration)技术在这些 CPU 之间分配负荷。在本地 APIC 的仲裁优先级寄存器中,给每个 CPU 都分配一个 0(最低)~ 15(最高) 范围内的值。

    每当中断传递给一个 CPU 时、其相应的仲裁优先级就自动置为 0,而其他每个 CPU 的仲裁优先级都增加 1 。当仲裁优先级寄存器大于 15 时,就把它置为获胜 CPU 的前一个仲裁优先级加 1。因此,中断以轮转方式在 CPU 之间分发,且具有相同的任务优先级(注 2)。

  除了在处理器之间分发中断外,多 APIC 系统还允许 CPU 产生处理器间中断(interprocessor interrupt)。当一个 CPU 希望把中断发给另一个 CPU 时,它就在自己本地 APIC 的中断指令寄存器 (Interrupt Command Register ,ICR)中存放这个中断向量和目标本地 APIC 的标识符。然后,通过 APIC 总线向目标本地 APIC 发送一条消息,从而向自己的 CPU 发出一个相应的中断。

  处理器间中断(简称 IPI)是 SMP 体系结构至关重要的組成部分,并由 Linux 有效地用来在 CPU 之间交换信息(参见本章后面)。

  目前大部分单处理器系统都包含一个 I/O APIC 芯片,可以用以下两种方式对这种芯片进行配置:

  • 作为一种标准 8259A 方式的外部 PIC 连接到 CPU。本地 APIC 被禁止,两条 LINT0LINT1 本地 IRQ 线分别配置为 INTRNMI 引脚。
  • 作为一种标准外部 I/O APIC 。本地 APIC 被激活,且所有的外部中断都通过 I/O APIC 接收。

(3)异常

  80x86 微处理器发布了大约 20 种不同的异常(注3)。内核必须为每种异常提供一个专门的异常处理程序。对于某些异常,CPU 控制单元在开始执行异常处理程序前会产生一个硬件出错码(hardware error code),并且压入内核态堆栈。

  下面的列表给出了在 80x86 处理器中可以找到的异常的向量、名字、类型及其简单描述。更多的信息可以在 Intel 的技术文挡中找到。

  • 0 —— “Divide error”(故障)
    当一个程序试图执行整数被 0 除操作时产生。
  • 1 —— “Debug”(陷阱或故障)
    产生于:(1)设置 eflagsTF 标志时(对于实现调试程序的单步执行是相当有用的),(2)一条指令或操作数的地址落在一个活动 debug 寄存器的范围之内(参见第三章的 “硬件上下文” 一节)。
  • 2 —— 未用
    为非屏蔽中断保留(利用 NMI 引脚的那些中断)。
  • 3 —— “Breakpoint”(陷阱)
    int3 (断点)指令(通常由 debugger 插入)引起。
  • 4 —— “Overflow”(陷阱)
    eflagsOFoverflow)标志被设置时,into(检查溢出)指令被执行。
  • 5 —— “Bounds check”(故障)
    对于有效地址范围之外的操作数,bound(检查地址边界)指令被执行。
  • 6 ——“Invalid opcode” (故障)
    CPU 执行单元检测到一个无效的操作码(决定执行操作的机器指令部分)。
  • 7 —— “Device not available”(故障)
    随着 cr0TS 标志被设置,ESCAPEMMXXMM 指令被执行(参见第三章的"保存和加载 FPUMMXXMM 寄存器"一节)。
  • 8 —— “Double fault”(异常中止)
    正常情况下,当 CPU 正试图为前一个异常调用处理程序时,同时又检测到一个异常,两个异常能被串行地处理。然而,在少数情况下,处理器不能串行地处理它们,因而产生这种异常。
  • 9 —— “Coprocessor segment overrun”(异常中止)
    因外部的数学协处理器引起的问题(仅用于 80386 微处理器)。
  • 10 —— “Invalid TSS”(故障)
    CPU 试图让一个上下文切换到有无效的 TSS 的进程。
  • 11 ——“Segment not present”(故障)
    引用一个不存在的内存段(段描述符的 Segment -Presert 标志被清 0)。
  • 12 —— “Stack segment fault”(故障)
    试图超过栈段界限的指令,或者由 ss 标识的段不在内存。
  • 13 —— "General protection" (故障)
    违反了 80x86 保护模式下的保护规则之一。
  • 14 —— “Page fault” (故障)
    寻址的页不在内存,相应的页表项为空,或者违反了一种分页保护机制。
  • 15 —— 由 Intel 保留
  • 16 —— “Floating point error” (故障)
    集成到 CPU 芯片中的浮点单元用信号通知一个错误情形,如数字溢出,或被 0 除(注4)。
  • 17 —— “Alignment check”(故障)
    操作数的地址没有被正确地对齐(例如,一个长整数的地址不是 4 的倍数)。
  • 18 —— “Machine check”(异常中止)
    机器检查机制检测到一个 CPU 错误或总线错误。
  • 19 —— “SIMD floating point exception”(故障)
    集成到 CPU 芯片中的 SSESSE2 单元对浮点操作用信号通知一个错误情形。
  • 20~31 这些值由 Intel 留作将来开发。如表 4-1 所示,每个异常都由专门的异常处理程序来处理(参见本章后面的"异常处理"一节),它们通常把一个 Unix 信号发送到引起异常的进程。

深入理解 Linux 内核_linux_09