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)异常
      • (4)中断描述符表
      • (5)中断和异常的硬件处理
    • 2、中断和异常处理程序的嵌套执行
      • (1)中断门、陷阱门及系统门
    • 3、异常处理
    • 4、中断处理
      • (1)I/O 中断处理
        • (a)中断向量
        • (b)IRQ 数据结构
  • 五、内核同步
    • 1、同步原语
      • (1)每CPU 变量
      • (2)原子操作
      • (3)优化和内存屏障
        • (a)优化屏障(optimization barrier)
        • (b)内存屏障(memory barrier)
      • (4)自旋锁
        • (a)具有内核抢占的 spin_lock 宏
        • (b)非抢占式内核中的 spin_lock 宏
        • (c)spin_unlock 宏
      • (5)顺序锁
      • (6)读 - 拷贝 - 更新(RCU)
      • (7)信号量

前言

  本文主要用来摘录《深入理解 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、段描述符 一文。


  相应的段选择符由宏 __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 任务状态段

  • 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 没有显示位数,因为每一部分的大小与具体的计算机体系结构有关。

  对于没有启用物理地址扩展的 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)内核页表

  内核维持着一组自己使用的页表,驻留在所谓的主内核页全局目录(master kernel Page Global Directory)中。系统初始化后,这组页表还从未被任何进程或任何内核线程直接使用;更确切地说,主内核页全局目录的最高目录项部分作为参考模型,为系统中每个普通进程对应的页全局目录项提供参考模型。

  我们在第八章 “非连续内存区的线性地址” 一节将会解释,内核如何确保对主内核页全局目录的修改能传递到由进程实际使用的页全局目录中。

  我们现在描述内核如何初始化自己的页表。这个过程分为两个阶段。事实上,内核映像刚刚被装入内存后,CPU 仍然运行于实模式,所以分页功能没有被启用。

  第一个阶段,内核创建一个有限的地址空间,包括内核的代码段和数据段、初始页表和用于存放动态数据结构的共 128KB 大小的空间。这个最小限度的地址空间仅够将内核装入 RAM 和对其初始化的核心数据结构。
  第二个阶段,内核充分利用剩余的 RAM 并适当地建立分页表。

(3)临时内核页表

  临时页全局目录是在内核编译过程中静态地初始化的,而临时页表是由 startup_32() 汇编语言函数(定义于 arch/i386/kernel/head.S)初始化的。我们不再过多提及页上级目录和页中间目录,因为它们相当于页全局目录项。在这个阶段 PAE 支持并未激活。

  临时页全局目录放在 swapper_pg_dir 变量中。临时页表在 pg0 变量处开始存放,紧接在内核未初始化的数据段(图 2-13 中的 _end 符号)后面。为简单起见,我们假设内核使用的段、临时页表和 128KB 的内存范围能容纳于 RAM8MB 空间里。为了映射 RAM8MB 的空间,需要用到两个页表。

  分页第一个阶段的目标是允许在实模式下和保护模式下都能很容易地对这 8MB 寻址。因此,内核必须创建一个映射,把从 0x000000000x007fffff 的线性地址和从 0xc00000000xc07fffff 的线性地址映射到从 0x000000000x007fffff 的物理地址。换句话说,内核在初始化的第一阶段,可以通过与物理地址相同的线性地址或者通过从 0xc0000000 开始的 8MB 线性地址对 RAM 的前 8MB 进行寻址。

  内核通过把 swapper_pg_dir 所有项都填充为 0 来创建期望的映射,不过,0,1,0x300(十进制 768)和 0x301(十进制 769)这四项除外:后两项包含了从 0xc00000000xc07fffff 间的所有线性地址。0、1、0x300 和 0x301 按以下方式初始化:

  • 0 项和 0x300 项的地址字段置为 pg0 的物理地址,而 1 项和 0x301 项的地址字段置为紧随 pg0 后的页框的物理地址。

  • 把这四个项中的 PresentRead/WriteUser/Supervisor 标志置位。

  • 把这四个项中的 AccessedDirtyPCDPWDPage Size 标志清 0

  汇编语言函数 startup_32() 也启用分页单元,通过向 cr3 控制寄存器装入 swapper_pg_dir 的地址及设置 cr0 控制寄存器的 PG 标志来达到这一目的。下面是等价的代码片段:

movl $swapper_pg_dir-0xc0000000, %eax
movl %eax, %cr3         # /* 设置页表指针*****/
movl %cr0, %eax
orl $0x80000000, %eax
movl %eax, %cr0         # /*-----设置分页(PG)位 * /

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

  由内核页表所提供的最终映射必须把从 0xc0000000 开始的线性地址转化为从 0 开始的物理地址。

// arch/x86/include/asm/page.h
#define __pa(x)     __phys_addr((unsigned long)(x))
#define __va(x)         ((void *)((unsigned long)(x)+PAGE_OFFSET))// arch/x86/mm/physaddr.c  32位系统下的实现
unsigned long __phys_addr(unsigned long x)
{/* VMALLOC_* aren't constants  */VIRTUAL_BUG_ON(x < PAGE_OFFSET);VIRTUAL_BUG_ON(__vmalloc_start_set && is_vmalloc_addr((void *) x));return x - PAGE_OFFSET;
}
EXPORT_SYMBOL(__phys_addr);

  宏 __pa 用于把从 PAGE_OFFSET 开始的线性地址转换成相应的物理地址,而去 __va 做相反的转化。

  主内核页全局目录仍然保存在 swapper_pg_dir 变量中。它由 paging_init() 函数初始化。该函数进行如下操作:

  1. 调用 pagetable_init() 适当地建立页表项。
  2. swapper_pg_dir 的物理地址写入 cr3 控制寄存器中。
  3. 如果 CPU 支持 PAE 并且如果内核编译时支持 PAE,则将 cr4 控制寄存器的 PAE 标志置位。
  4. 调用 __flush_tlb_all() 使 TLB 的所有项无效。

  pagetable_init() 执行的操作既依赖于现有 RAM 的容量,也依赖于 CPU 模型。让我们从最简单的情况开始。我们的计算机有小于 896MB(注 7)的 RAM32 位物理地址足以对所有可用 RAM 进行寻址,因而没有必要激活 PAE 机制 [参见前面 “物理地址扩展(PAE)分页机制” 一节]。

  swapper_pg_dir 页全局目录由如下等价的循环重新初始化:

pgd = swapper_pg_dir + pgd_index(PAGE_OFFSET); /* 768 */
phys_addr = 0x00000000;
while (phys_addr < (max_low_pfn * PAGE_SIZE)) {pmd = one_md_table_init(pgd);    /* 返回 pgd */set_pmd(pmd, __pmd(phys_addr | pgprot_val(__pgprot(0xle3))));/* Ox1e3 == Present, Accessed, Dirty, Read/Write,Page Size, Global */phys_addr += PTRS_PER_PTE * PAGE_SIZE;    /* 0x400000 */++pgd;
}

  我们假定 CPU 是支持 4MB 页和 “全局(global)” TLB 表项的最新 80x86 微处理器。注意如果页全局目录项对应的是 0xc0000000 之上的线性地址,则把所有这些项的 User/Supervisor 标志清 0,由此拒绝用户态进程访问内核地址空间。还要注意 Page Size 被置位使得内核可以通过使用大型页来对 RAM 进行寻址(参见本章先前的 “扩展分页” 一节)。

  由 startup_32() 函数创建的物理内存前 8MB 的恒等映射用来完成内核的初始化阶段。当这种映射不再必要时,内核调用 zap_low_mappings() 函数清除对应的页表项。

  实际上,这种描述并未说明全部事实。我们将在后面 “固定映射的线性地址” 一节看到,内核也调整与 “固定映射的线性地址” 对应的页表项。

  注 7:线性地址的最高 128MB 留给几种映射去用(参见本章后面 “固定映射的线性地址” 一节和第八章 “非连续内存区的线性地址” 一节)。因此此映射 RAM 所剩空间为 1GB - 128MB= 896MB

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

  在这种情况下,并不把 RAM 全部映射到内核地址空间。Linux 在初始化阶段可以做的最好的事是把一个具有 896MBRAM 窗口(window)映射到内核线性地址空间。如果一个程序需要对现有 RAM 的其余部分寻址,那就必须把某些其他的线性地址间隔映射到所需的 RAM。这意味着修改某些页表项的值。我们将在第八章讨论这种动态重映射是如何进行的。

  内核使用与前一种情况相同的代码来初始化页全局目录。

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

  现在让我们考虑 RAM 大于 4GB 计算机的内核页表初始化,更确切地说,我们处理以下发生的情况:

  • CPU 模型支持物理地址扩展(PAE
  • RAM 容量大于 4GB
  • 内核以 PAE 支持来编译

  尽管 PAE 处理 36 位物理地址,但是线性地址依然是 32 位地址。如前所述,Linux 映射一个 896MBRAM 窗口到内核线性地址空间;剩余 RAM 留着不映射,并由动态重映射来处理,第八章将对此进行描述。与前一种情况的主要差异是使用三级分页模型,因此页全局目录按以下循环代码来初始化:

pgd_idx = pgd_index(PAGE_OFFSET); /* 3 */
for (i=0; i<pgd_idx; i++)set_pgd(swapper_pg_dir + i, __pgd(__pa(empty_zero_page) + 0x001));/* 0x001 == Present. */
pgd = swapper_pg_dir + pgd_idx;
phys_addr = 0x00000000;
for (; i<PTRS_PER_PGD; ++i, ++pgd) {pmd = (pmd_t *) alloc_bootmem_low_pages(PAGE_SIZE);set_pgd(pgd, __pgd(__pa(pmd) | 0x001)): /* 0x001 == Present */if (phys_addr < max_low_pfn * PAGE_SIZE) {for (j=0; j < PTRS_PER_PMD /* 512 */&& phys_addr < max_low_pfn*PAGE_SIZE; ++j) {set_pmd(pmd, __pmd(phys_addr |pgprot_val(__pgprot(0x1e3))));/* 0x1e3 == Present, Accessed, Dirty, Read/Write,Page Size, Global */phys_addr += PTRS_PER_PTE * PAGE_SIZE; /* 0x200000 */}}
}
swapper_pg_dir[0] = swapper_pg_dir[pgd_idx];

  页全局目录中的前三项与用户线性地址空间相对应,内核用一个空页(empty_zero_page)的地址对这三项进行初始化。第四项用页中间目录(pmd)的地址初始化,该页中间目录是通过调用 alloc_bootmem_low_pages() 分配的。页中间目录中的前 448 项(有 512 项,但后 64 项留给非连续内存分配;参见第八章的 “非连续内存区管理” 一节)用 RAM896MB 的物理地址填充。

  注意,支持 PAE 的所有 CPU 模型也支持大型 2MB 页和全局页。正如前一种情况一样,只要可能,Linux 使用大型页来减少页表数。

  然后页全局目录的第四项被拷贝到第一项中,这样好为线性地址空间的前 896MB 中的低物理内存映射作镜像。为了完成对 SMP 系统的初始化,这个映射是必需的:当这个映射不再必要时,内核通过调用 zap_low_mappings() 函数来清除对应的页表项,正如先前的情况一样。

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

  我们看到内核线性地址第四个GB 的初始部分映射系统的物理内存。但是,至少 128MB 的线性地址总是留作他用,因为内核使用这些线性地址实现非连续内存分配和固定映射的线性地址。

  非连续内存分配仅仅是动态分配和释放内存页的一种特殊方式,将在第八章 “非连续内存区的线性地址” 一节描述。本节我们集中讨论固定映射的线性地址。

  固定映射的线性地址(fix-mapped linear address)基本上是一种类似于 0xffffc0004G - 16K) 这样的常量线性地址,其对应的物理地址不必等于线性地址减去 0xc000000,而是可以以任意方式建立。因此,每个固定映射的线性地址都映射一个物理内存的页框。我们将会在后面的章节看到,内核使用固定映射的线性地址来代替指针变量,因为这些指针变量的值从不改变。

  固定映射的线性地址概念上类似于对 RAM896MB 映射的线性地址。不过,固定映射的线性地址可以映射任何物理地址,而由第 4GB 初始部分的线性地址所建立的映射是线性的(线性地址 X 映射物理地址 X - PAGE_OFFSET)。

  就指针变量而言,固定映射的线性地址更有效。事实上,间接引用一个指针变量比间接引用一个立即常量地址要多一次内存访问。此外,在间接引用一个指针变量之前对其值进行检查是一个良好的编程习惯;相反,对一个常量线性地址的检查则是没有必要的。

  每个固定映射的线性地址都由定义于 enum fixed_addresses 数据结构中的整型索引来表示:

enum fixed_addresses {FIX_HOLE,FIX_VSYSCALL,FIX_APIC_BASE,FIX_IO_APIC_BASE_0,[...]__end_of_fixed_addresses
}

  每个固定映射的线性地址都存放在线性地址第四个 GB 的末端。fix_to_virt() 函数计算从给定索引开始的常量线性地址:

inline unsigned long fix_to_virt(const unsigned int idx)
{_if (idx >= __end_of_fixed_addresses)__this_fixmap_does_not_exist();return (0xfffff000UL - (idx << PAGE_SHIFT));
}

  让我们假定某个内核函数调用 fix_to_virt(FIX_IO_APIC_BASE_0) 。因为该函数声明为 “inline”,所以C 编译程序不调用 fix_to_virt(),而是仅仅把它的代码插入到调用函数中。此外,运行时从不对这个索引值执行检查。事实上,FIX_IO_APIC_BASE_0 是个等于 3 的常量,因此编译程序可以去掉 if 语句,因为它的条件在编译时为假。相反,如果条件为真,或者 fix_to_virt() 的参数不是一个常量,则编译程序在连接阶段产生一个错误,因为符号 _this_fixmap_does_not_exist 在别处没有定义。最后,编译程序计算 0xfffff000-(3<<PAGE_SHIFT),并用常量线性地址 0xffffc000 代替 fix_to_virt() 函数调用。

  为了把一个物理地址与固定映射的线性地址关联起来,内核使用set_fixmap(idx, phys)set_fixmap_nocache(idx,phys) 去。这两个函数都把 fix_to_virt(idx) 线性地址对应的一个页表项初始化为物理地址 phys;不过,第二个函数也把页表项的 PCD 标志置位,因此,当访问这个页框中的数据时禁用硬件高速缓存(参见本章前面 “硬件高速缓存” 一节)。反过来,clear_fixmap(idx) 用来撤销固定映射线性地址 idx 和物理地址之间的连接。

// arch/x86/include/asm/fixmap.h
#define set_fixmap(idx, phys)               \__set_fixmap(idx, phys, PAGE_KERNEL)#define set_fixmap_nocache(idx, phys)          \__set_fixmap(idx, phys, PAGE_KERNEL_NOCACHE)static inline void __set_fixmap(enum fixed_addresses idx,phys_addr_t phys, pgprot_t flags)
{native_set_fixmap(idx, phys, flags);
}// arch/x86/mm/pgtable.c
void native_set_fixmap(enum fixed_addresses idx, phys_addr_t phys,pgprot_t flags)
{__native_set_fixmap(idx, pfn_pte(phys >> PAGE_SHIFT, flags));
}

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

  内存寻址的最后一个主题是关于内核如何使用硬件高速缓存来达到最佳效果。硬件高速缓存和转换后援缓冲器(TLB)在提高现代计算机体系结构的性能上扮演着重要角色。
内核开发者采用一些技术来减少高速缓存和 TLB 的未命中次数。

  • 处理硬件高速缓存
  • 处理 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_CTXSWint oncpu;
#endif
#endifint 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_TRACEunsigned int btrace_seq;
#endifunsigned int policy;cpumask_t cpus_allowed;#ifdef CONFIG_TREE_PREEMPT_RCUint 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;
#endifstruct 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_ACCOUNTINGcputime_t prev_utime, prev_stime;
#endifunsigned 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 lockit 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_AUDITSYSCALLuid_t loginuid;unsigned int sessionid;
#endifseccomp_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_IRQFLAGSunsigned 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 48ULu64 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_CPUSETSnodemask_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_FUTEXstruct robust_list_head __user *robust_list;
#ifdef CONFIG_COMPATstruct compat_robust_list_head __user *compat_robust_list;
#endifstruct list_head pi_state_list;struct futex_pi_state *pi_state_cache;
#endif
#ifdef CONFIG_PERF_EVENTSstruct perf_event_context *perf_event_ctxp;struct mutex perf_event_mutex;struct list_head perf_event_list;
#endif
#ifdef CONFIG_NUMAstruct mempolicy *mempolicy;  /* Protected by alloc_lock */short il_next;
#endifatomic_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_ACCTstruct task_delay_info *delays;
#endif
#ifdef CONFIG_FAULT_INJECTIONint make_it_fail;
#endifstruct prop_local_single dirties;
#ifdef CONFIG_LATENCYTOPint 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 的进程描述符。

(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 结构互相关联。

  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 局部变量的值。

  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 %ebppopfl

  注意这些 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_LOCKINGDEBUG_LOCKS_WARN_ON(!p->hardirqs_enabled);DEBUG_LOCKS_WARN_ON(!p->softirqs_enabled);
#endifretval = -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_ACCOUNTINGp->prev_utime = cputime_zero;p->prev_stime = cputime_zero;
#endif
#if defined(SPLIT_RSS_COUNTING)memset(&p->rss_stat, 0, sizeof(p->rss_stat));
#endifp->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_NUMAp->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_IRQFLAGSp->irq_events = 0;
#ifdef __ARCH_WANT_INTERRUPTS_ON_CTXSWp->hardirqs_enabled = 1;
#elsep->hardirqs_enabled = 0;
#endifp->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_LOCKDEPp->lockdep_depth = 0; /* no locks held yet */p->curr_chain_key = 0;p->lockdep_recursion = 0;
#endif#ifdef CONFIG_DEBUG_MUTEXESp->blocked_on = NULL; /* not blocked yet */
#endif
#ifdef CONFIG_CGROUP_MEM_RES_CTLRp->memcg_batch.do_batch = 0;p->memcg_batch.memcg = NULL;
#endifp->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_FUTEXp->robust_list = NULL;
#ifdef CONFIG_COMPATp->compat_robust_list = NULL;
#endifINIT_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_EMUclear_tsk_thread_flag(p, TIF_SYSCALL_EMU);
#endifclear_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_NUMAmpol_put(p->mempolicy);
bad_fork_cleanup_cgroup:
#endifcgroup_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(共享信号处理程序的轻量级进程也必须共享内存描述符)。
  2. 通过调用 security_task_create() 以及稍后调用的 security_task_alloc() 执行所有附加的安全检查。Linux 2.6 提供扩展安全性的钩子函数,与传统 Unix 相比,它具有更加强壮的安全模型。详情参见第二十章。

  3. 调用 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)。

  4. 检查存放在 current->signal->rlim[RLIMIT_NPROC].rlim_cur 变量中的值是否小于或等于用户所拥有的进程数。如果是,则返回错误码,除非进程没有 root 权限。该函数从每用户数据结构 user_struct 中获取用户所拥有的进程数。通过进程描述符 user 字段的指针可以找到这个数据结构。

  5. 递增 user_struct 结构的使用计数器 (tsk->user->__count 字段)和用户所拥有的进程的计数器(tsk->user->processes)。

  6. 检查系统中的进程数量(存放在 nr_threads 变量中)是否超过 max_threads 变量的值。这个变量的缺省值取决于系统内存容量的大小。总的原则是:所有 thread_info 描述符和内核栈所占用的空间不能超过物理内存大小的 1/8。不过,系统管理员可以通过写 /proc/sys/kernel/threads-max 文件来改变这个值。

  7. 如果实现新进程的执行域和可执行格式的内核函数(参见第二十章)都包含在内核模块中,则递增它们的使用计数器(参见附录二)。

  8. 设置与进程状态相关的几个关键字段:

    • 把大内核锁计数器 tsk->lock_depth 初始化为 -1(参见第五章 “大内核锁” 一节)。
    • tsk->did_exec 字段初始化为 0 :它记录了进程发出的 execve() 系统调用的次数。
    • 更新从父进程复制到 tsk->flags 字段中的一些标志:首先清除 PF_SUPERPRIV 标志,该标志表示进程是否使用了某种超级用户权限。然后设置 PF_FORKNOEXEC 标志,它表示子进程还没有发出 execve() 系统调用。
  9. 把新进程的 PID 存入 tsk->pid 字段。

  10. 如果 clone_flags 参数中的 CLONE_PARENT_SETTID 标志被设置,就把子进程的 PID 复制到参数 parent_tidptr 指向的用户态变量中。

  11. 初始化子进程描述符中的 list_head 数据结构和自旋锁,并为与挂起信号、定时器及时间统计表相关的几个字段赋初值。

  12. 调用 copy_semundo()copy_files()copy_fs()copy_sighand()copy_signa1()copy_mm()copy_namespace() 来创建新的数据结构,并把父进程相应数据结构的值复制到新数据结构中,除非 clone_flags 参数指出它们有不同的值。

  13. 调用 copy_thread(),用发出 clone() 系统调用时 CPU 寄存器的值(正如第十章所述,这些值已经被保存在父进程的内核栈中)来初始化子进程的内核栈。不过,copy_thread()eax 寄存器对应字段的值 [这是 fork()clone() 系统调用在子进程中的返回值] 字段强行置为 0。子进程描述符的 thread.esp 字段初始化为子进程内核栈的基地址,汇编语言函数(ret_from_fork())的地址存放在 thread.eip 字段中。如果父进程使用 I/O 权限位图,则子进程获取该位图的一个拷贝。最后,如果 CLONE_SETTLS 标志被设置,则子进程获取由 clone() 系统调用的参数 tls 指向的用户态数据结构所表示的 TLS 段(注 9)。

  14. 如果 clone_flags 参数的值被置为 CLONE_CHILD_SETTIDCLONE_CHILD_CLEARTID,就把 child_tidptr 参数的值分别复制到 tsk->set_chid_tidtsk->clear_child_tid 字段。这些标志说明:必须改变子进程用户态地址空间的 child_tidptr 所指向的变量的值,不过实际的写操作要稍后再执行。

  15. 清除子进程 thread_info 结构的 TIF_SYSCALL_TRACE 标志,以使 ret_from_fork() 函数不会把系统调用结束的消息通知给调试进程(参见第十章 “进入和退出系统调用” 一节)。(为对子进程的跟踪是由 tsk->ptrace 中的 PTRACE_SYSCALL 标志来控制的,所以子进程的系统调用跟踪不会被禁用。)

  16. clone_flags 参数低位的信号数字编码初始化 tsk->exit_signal 字段,如果 CLONE_THREAD 标志被置位,就把 tsk->exit_signal 字段初始化为 -1。正如我们将在本章稍后 “进程终止” 一节所看见的,只有当线程组的最后一个成员(通常是线程组的领头)“死亡”,才会产生一个信号,以通知线程组的领头进程的父进程。

  17. 调用 sched_fork() 完成对新进程调度程序数据结构的初始化。该函数把新进程的状态设置为 TASK_RUNNING,并把 thread_info 结构的 preempt_count 字段设置为 1,从而禁止内核抢占(参见第五章 “内核抢占” 一节)。此外,为了保证公平的进程调度,该函数在父子进程之间共享父进程的时间片(参见第七章 “scheduler_tick() 数” 一节)。

  18. 把新进程的 thread_info 结构的 cpu 字段设置为由 smp_processor_id() 所返回的本地 CPU 号。

  19. 初始化表示亲子关系的字段。尤其是,如果 CLONE_PARENTCLONE_THREAD ,被设置,就用 current->real_parent 的值初始化 tsk->real_parenttsk->parent,因此,子进程的父进程似乎是当前进程的父进程。否则,把 tsk->real_parenttsk->parent 置为当前进程。

  20. 如果不需要跟踪子进程(没有设置 CLONE_PTRAC 标志),就把 tsk->ptrace 字段设置为 0tsk->ptrace 字段会存放一些标志,而这些标志是在一个进程被另外一个进程跟踪时才会用到的。采用这种方式,即使当前进程被跟踪,子进程也不会被跟踪。

  21. 执行 SET_LINKS 宏,把新进程描述符插入进程链表。

  22. 如果子进程必须被跟踪(tsk->ptrace 字段的 PT_PTRACED 标志被设置),就把 current->parent 赋给 tsk->parent,并将子进程插入调试程序的跟踪链表中。

  23. 调用 attach_pid() 把新进程插述符的 PID 插入 pidhash[PIDTYPE_PID] 散列表。

  24. 如果子进程是线程组的领头进程(CLONE_THREAD 标志被清 0):

    • tsk->tgid 的初值置为 tsk->pid
    • tsk->group_leader 的初值置为 tsk
    • 调用三次 attach_pid() ,把子进程分别插入 PIDTYPE_TGIDPIDTYPE_PGIDPIDTYPE_SID 类型的 PID 散列表。
  25. 否则,如果子进程属于它的父进程的线程组(CLONE_THREAD 标志被设置):

    • tsk->tgid 的初值置为 tsk->current->tgid
    • tsk->group_leader 的初值置为 current->group_leader 的值。
    • 调用 attach_pid(),把子进程插入 PIDTYPE_TGID 类型的散列表中(更具体地说,插入 current->group_leader 进程的每个 PID 链表)。
  26. 现在,新进程已经被加入进程集合:递增 nr_threads 变量的值。

  27. 递增 total_forks 变量以记录被创建的进程的数量。

  28. 终止并返回子进程描述符指针(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_32regs.ds = __USER_DS;regs.es = __USER_DS;regs.fs = __KERNEL_PERCPU;regs.gs = __KERNEL_STACK_CANARY;
#elseregs.ss = __KERNEL_DS;
#endifregs.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()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_NUMAmpol_put(tsk->mempolicy);tsk->mempolicy = NULL;
#endif
#ifdef CONFIG_FUTEXif (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() 函数" 一节)。

  7. 调用 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 从进程链表中解除进程描述符的链接。
  6. 如果进程不是线程组的领头进程,领头进程处于僵死状态,而且进程是线程组的最后一个成员,则该函数向领头进程的父进程发送一个信号,通知它进程已死亡。
  7. 调用 sched_exit() 函数来调整父进程的时间片(这一步在逻辑上作为对 copy_process()17 步的补充)。
  8. 调用 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 线。
  3. 返回到第 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 单元。

  来自外部硬件设备的中断请求以两种方式在可用 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 信号发送到引起异常的进程。

(4)中断描述符表

  可参考 ⇒ 5、中断描述符表

  Linux 利用中断门处理中断,利用陷阱门处理异常。

(5)中断和异常的硬件处理

  我们现在描述 CPU 控制单元如何处理中断和异常。我们假定内核已被初始化,因此,CPU 在保护模式下运行。
  当执行了一条指令后,cseip 这对寄存器包含下一条将要执行的指令的逻辑地址。在处理那条指令之前,控制单元会检查在运行前一条指令时是否已经发生了一个中断或异常。如果发生了一个中断或异常,那么控制单元执行下列操作:

  1. 确定与中断或异常关联的向量 i(0 < i < 255)。

  2. 读由 idtr 寄存器指向的 IDT 表中的第 i 项(在下面的描述中,我们假定 IDT 表项中包含的是一个中断门或一个陷阱门)。

  3. gdtr 寄存器获得 GDT 的基地址 并在 GDT 中查找,以读取 IDT 表项中的选择符所标识的段描述符。这个描述符指定中断或异常处理程序所在段的基地址。

  4. 确信中断是由授权的(中断)发生源发出的。首先将当前特权级 CPL(存放在 cs 寄存器的低两位)与段描述符(存放在 GDT 中)的描述符特权级 DPL 比较,如果 CPL 小于 DPL,就产生一个 “General protection” 异常,因为中断处理程序的特权不能低于引起中断的程序的特权。对于编程异常,则做进一步的安全检查:比较 CPL 与处于 IDT 中的门描述符的 DPL,如果 DPL 小于 CPL ,就产生一个 “General protection” 异常。这最后一个检查可以避免用户应用程序访问特殊的陷阱门或中断门。

  5. 检查是否发生了特权级的变化,也就是说,CPL 是否不同于所选择的段描述符的 DPL。如果是,控制单元必须开始使用与新的特权级相关的栈。通过执行以下步骤来做到这点:

    • tr 寄存器,以访问运行进程的 TSS 段。
    • 用与新特权级相关的栈段和栈指针的正确值装载 ssesp 寄存器。这些值可以在 TSS 中找到(参见第三章的"任务状态段"一节)
    • 在新的栈中保存 ssesp 以前的值,这些值定义了与旧特权级相关的栈的逻辑地址。
  6. 如果故障已发生,用引起异常的指令地址装载 cseip 寄存器,从而使得这条指令能再次被执行。

  7. 在栈中保存 eflagscseip 的内容。

  8. 如果异常产生了一个硬件出错码,则将它保存在栈中。

  9. 装载 cseip 寄存器,其值分别是 IDT 表中第 i 项门描述符的段选择符和偏移量字段。这些值给出了中断或者异常处理程序的第一条指令的逻辑地址。

  控制单元所执行的最后一步就是跳转到中断或者异常处理程序。换句话说,处理完中断信号后,控制单元所执行的指令就是被选中处理程序的第一条指令。

  中断或异常被处理完后,相应的处理程序必须产生一条 iret 指令,把控制权转交给被中断的进程,这将迫使控制单元:

  1. 用保存在栈中的值装载 cseipeflags 寄存器。如果一个硬件出错码曾被压入栈中,并且在 eip 内容的上面,那么,执行 iret 指令前必须先弹出这个硬件出错码。
  2. 检查处理程序的 CPL 是否等于 cs 中最低两位的值(这意味着被中断的进程与处理程序运行在同一特权级)。如果是,iret 终止执行;否则,转入下一步。
  3. 从栈中装载 ssesp 寄存器,因此,返回到与旧特权级相关的栈。
  4. 检查 dsesfsgs 段寄存器的内容,如果其中一个寄存器包含的选择符是一个段描述符,并且其 DPL 值小于 CPL,那么,清相应的段寄存器。控制单元这么做是为了禁止用户态的程序(CPL=3)利用内核以前所用的段寄存器(DPL=0)。如果不清这些寄存器,怀有恶意的用户态程序就可能利用它们来访问内核地址空间。

2、中断和异常处理程序的嵌套执行

  每个中断或异常都会引起一个内核控制路径,或者说代表当前进程在内核态执行单独的指令序列。内核控制路径可以任意嵌套。一个中断处理程序可以被另一个中断处理程序 “中断” 。

  允许内核控制路径嵌套执行必须付出代价,那就是中断处理程序必须永不阻塞,换句话说,中断处理程序运行期间不能发生进程切换。

  假定内核没有 bug,那么大多数异常就只在 CPU 处于用户态时发生。事实上,异常要么是由编程错误引起,要么是由调试程序触发。然而,“Page Fault(缺页)” 异常发生在内核态。这发生在当进程试图对属于其地址空间的页进行寻址,而该页现在不在 RAM 中时。当处理这样的一个异常时,内核可以挂起当前进程,并用另一个进程代替它,直到请求的页可以使用为止。只要被挂起的进程又获得处理器,处理缺页异常的内核控制路径就恢复执行。
  因为 “Page Fault” 异常处理程序从不进一步引起异常,所以与异常相关的至多两个内核控制路径(第一个由系统调用引起,第二个由缺页引起)会堆叠在一起,一个在另一个之上。

  与异常形成对照的是,尽管处理中断的内核控制路径代表当前进程运行,但由 I/O 设备产生的中断并不引用当前进程的专有数据结构。事实上,当一个给定的中断发生时,要预测哪个进程将会运行是不可能的。

  一个中断处理程序既可以抢占其他的中断处理程序,也可以抢占异常处理程序。相反,异常处理程序从不抢占中断处理程序。在内核态能触发的唯一异常就是刚刚描述的缺页异常。但是,中断处理程序从不执行可以导致缺页(因此意味着进程切换)的操作。

  基于以下两个主要原因,Linux 交错执行内核控制路径:

  • 为了提高可编程中断控制器和设备控制器的吞吐量。假定设备控制器在一条 IRQ 线上产生了一个信号,PIC 把这个信号转换成一个外部中断,然后 PIC 和设备控制器保持阻塞,一直到 PICCPU 处接收到一条应答信息。由于内核控制路径的交错执行,内核即使正在处理前一个中断,也能发送应答。

  • 为了实现一种没有优先级的中断模型。因为每个中断处理程序都可以被另一个中断处理程序延缓,因此,在硬件设备之间没必要建立预定义优先级。这就简化了内核代码,提高了内核的可移植性。

  在多处理器系统上,几个内核控制路径可以并发执行。此外,与异常相关的内核控制路径可以开始在一个 CPU 上执行,并且由于进程切换而移往另一个 CPU 上执行。

(1)中断门、陷阱门及系统门

  与在前面 “中断描述符表” 中所提到的一样,Intel 提供了三种类型的中断描述符:任务门、中断门及陷阱门描述符。Linux 使用与 Intel 稍有不同的细目分类和术语,把它们如下进行分类:

  • 中断门 (interrupt gate
    用户态的进程不能访问的一个 Intel 中断门(门的 DPL 字段为 0)。所有的 Linux 中断处理程序都通过中断门激活,并全部限制在内核态。

  • 系统门 (system gate
    用户态的进程可以访问的一个 Intel 陷阱门(门的 DPL 字段为 3)。通过系统门来激活三个 Linux 异常处理程序,它们的向量是 45128,因此,在用户态下,可以发布 intoboundint $0x80 三条汇编语言指令。

  • 系统中断门 (system interrupt gate
    能够被用户态进程访问的 Intel 中断门(门的 DPL 字段为 3)。与向量 3 相关的异常处理程序是由系统中断门激活的,因此,在用户态可以使用汇编话言指令 int3

  • 陷阱门 (trap gate
    用户态的进程不能访问的一个 Intel 陷阱门(门的 DPL 字段为 0)。大部分 Linux 异常处理程序都通过陷阱门来激活。

  • 任务门 (task gate
    不能被用户态进程访问的 Intel 任务门(门的 DPL 字段为 0)。Linux 对 “Double fault” 异常的处理程序是由任务门激活的。

下列体系结构相关的函数用来在 IDT 中插入门:

  • set_intr_gate(n,addr)
    IDT 的第 n 个表项插入一个中断门。门中的段选择符设置成内核代码的段选择符,偏移量设置为中断处理程序的地址 addrDPL 字段设置为 0
  • set_system_gate(n,addr)
    IDT 的第 n 个表项插入一个陷阱门。门中的段选择符设置成内核代码的段选择符,偏移量设置为异常处理程序的地址 addrDPL 字段设置为 3
  • set_system_intr_gate(n,addr)
    IDT 的第 n 个表项插入一个中断门。门中的段选择符设置成内核代码的段选择符,偏移量设置为异常处理程序的地址 addrDPL 字段设置为 3
  • set_trap_gate(n,addr)
    与前一个函数类似,只不过 DPL 的字段设置成 0
  • set_task_gate(n, gdt)
    IDT 的第 n 个表项插入一个中断门。门中的段选择符中存放一个 TSS 的全局描述符表的指针,该 TSS 中包含要被激活的函数。偏移量设置为 0,而 DPL 字段设置为 3

3、异常处理

  CPU 产生的大部分异常都由 Linux 解释为出错条件。当其中一个异常发生时,内核就向引起异常的进程发送一个信号向它通知一个反常条件。例如,如果进程执行了一个被 0 除的操作,CPU 就产生一个 “Divide error” 异常,并由相应的异常处理程序向当前进程发送一个 SIGFPE 信号,这个进程将采取若干必要的步骤来(从出错中)恢复或者中止运行(如果没有为这个信号设置处理程序的话)。

  但是,在两种情况下,Linux 利用 CPU 异常更有效地管理硬件资源。第一种情况已经在第三章 “保存和加载 FPU、MMX 及 XMM 寄存器” 一节描述过,“Device not availeble” 异常与 cr0 寄存器的 TS 标志一起用来把新值装入浮点寄存器。第二种情况指的是 “Page Fault” 异常,该异常推迟给进程分配新的页框,直到不能再推迟为止。相应的处理程序比较复杂,因为异常可能表示一个错误条件,也可能不表示一个错误条件(参见第九章 “缺页异常处理程序” 一节)。

  异常处理程序有一个标准的结构,由以下三部分组成:

  1. 在内核堆栈中保存大多数寄存器的内容(这部分用汇编语言实现)。
  2. 用高级的 C 函数处理异常。
  3. 通过 ret_from_exception() 函数从异常处理程序退出。

  为了利用异常,必须对 IDT 进行适当的初始化,使得每个被确认的异常都有一个异常处理程序。trap_init() 函数的工作是将一些最终值(即处理异常的函数)插入到 IDT 的非屏蔽中断及异常表项中。这是由函数 set_trap_gate()、set_intr_gate()、set_system_gate()、set_system_intr_gate() 和 set_task_gate() 来完成的。

// arch/x86/kernel/traps.c
void __init trap_init(void)
{int i;#ifdef CONFIG_EISAvoid __iomem *p = early_ioremap(0x0FFFD9, 4);if (readl(p) == 'E' + ('I'<<8) + ('S'<<16) + ('A'<<24))EISA_bus = 1;early_iounmap(p, 4);
#endifset_intr_gate(0, &divide_error);set_intr_gate_ist(1, &debug, DEBUG_STACK);set_intr_gate_ist(2, &nmi, NMI_STACK);/* int3 can be called from all */set_system_intr_gate_ist(3, &int3, DEBUG_STACK);/* int4 can be called from all */set_system_intr_gate(4, &overflow);set_intr_gate(5, &bounds);set_intr_gate(6, &invalid_op);set_intr_gate(7, &device_not_available);
#ifdef CONFIG_X86_32set_task_gate(8, GDT_ENTRY_DOUBLEFAULT_TSS);
#elseset_intr_gate_ist(8, &double_fault, DOUBLEFAULT_STACK);
#endifset_intr_gate(9, &coprocessor_segment_overrun);set_intr_gate(10, &invalid_TSS);set_intr_gate(11, &segment_not_present);set_intr_gate_ist(12, &stack_segment, STACKFAULT_STACK);set_intr_gate(13, &general_protection);set_intr_gate(14, &page_fault);set_intr_gate(15, &spurious_interrupt_bug);set_intr_gate(16, &coprocessor_error);set_intr_gate(17, &alignment_check);
#ifdef CONFIG_X86_MCEset_intr_gate_ist(18, &machine_check, MCE_STACK);
#endifset_intr_gate(19, &simd_coprocessor_error);/* Reserve all the builtin and the syscall vector: */for (i = 0; i < FIRST_EXTERNAL_VECTOR; i++)set_bit(i, used_vectors);#ifdef CONFIG_IA32_EMULATIONset_system_intr_gate(IA32_SYSCALL_VECTOR, ia32_syscall);set_bit(IA32_SYSCALL_VECTOR, used_vectors);
#endif#ifdef CONFIG_X86_32if (cpu_has_fxsr) {printk(KERN_INFO "Enabling fast FPU save and restore... ");set_in_cr4(X86_CR4_OSFXSR);printk("done.\n");}if (cpu_has_xmm) {printk(KERN_INFO"Enabling unmasked SIMD FPU exception support... ");set_in_cr4(X86_CR4_OSXMMEXCPT);printk("done.\n");}set_system_trap_gate(SYSCALL_VECTOR, &system_call);set_bit(SYSCALL_VECTOR, used_vectors);
#endif/** Should be a barrier for any external CPU state:*/cpu_init();x86_init.irqs.trap_init();
}

4、中断处理

  正如前面解释的那样,内核只要给引起异常的进程发送一个 Unix 信号就能处理大多数异常。因此,要采取的行动被延迟,直到进程接收到这个信号。所以,内核能很快地处理异常。

  这种方法并不适合中断,因为经常会出现一个进程(例如,一个请求数据传输的进程)被挂起好久后中断才到达的情况,因此,一个完全无关的进程可能正在运行。所以,给当前进程发送一个 Unix 信号是毫无意义的。

  中断处理依赖于中断类型。就我们的目的而言,我们将讨论三种主要的中断类型:

  • I/O 中断
    某些 I/O 设备需要关注:相应的中断处理程序必须查询设备以确定适当的操作过程。我们在后面 “I/O 中断处理” 一节将描述这种中断。
  • 时钟中断
    某种时钟(或者是一个本地 APIC 时钟,或者是一个外部时钟)产生一个中断;这种中断告诉内核一个固定的时间间隔已经过去。这些中断大部分是作为 I/O 中断来处理的,我们将在第六章讨论时钟中断的具体特征。
  • 处理器间中断
    多处理器系统中一个 CPU 对另一个 CPU 发出一个中断。我们在后面 “处理器间中断处理” 一节将讨论这种中断。

(1)I/O 中断处理

  一般而言,I/O 中断处理程序必须足够灵活以给多个设备同时提供服务。例如在 PCI 总线的体系结构中,几个设备可以共享同一个 IRQ 线。这就意味着仅仅中断向量不能说明所有问题。在表 4-3 所示的例子中,同一个向量 43 既分配给 USB 端口,也分配给声卡。然而,在老式 PC 体系结构(像 ISA)中发现的一些硬件设备,当它们的 IRQ 与其他设备共享时,就不能可靠地运转。

中断处理程序的灵活性是以两种不同的方式实现的,讨论如下:

  • IRQ 共享
    中断处理句柄执行多个中断服务例程(interrupt service routine, ISR)。每个 ISR 是一个与单独设备(共享 IRQ 线)相关的函数。因为不可能预先知道哪个特定的设备产生 IRQ,因此,每个 ISR 都被执行,以验证它的设备是否需要关注:如果是,当设备产生中断时,就执行需要执行的所有操作。

  • IRQ 动态分配
    一条 IRQ 线在可能的最后时刻才与一个设备驱动程序相关联:例如,软盘设备的 IRQ 线只有在用户访问软盘设备时才被分配。这样,即使几个硬件设备并不共享 IRQ 线、同一个 IRQ 向量也可以由这几个设备在不同时刻使用(见本节最后一部分的讨论)。

  当一个中断发生时,并不是所有的操作都具有相同的急迫性。事实上,把所有的操作都放进中断处理程序本身并不合适。需要时间长的、非重要的操作应该推后,因为当一个中断处理程序正在运行时,相应的 IRQ 线上发出的信号就被暂时忽略。更重要的是,中断处理程序是代表进程执行的,它所代表的进程必须总处于 TASK_RUNNING 状态,否则,就可能出现系统僵死情形。因此,中断处理程序不能执行任何阻塞过程,如磁盘 I/O 操作。因此,Linux 把紧随中断要执行的操作分为三类:

  • 紧急的(Critical
    这样的操作诸如:对 PIC 应答中断,对 PIC 或设备控制器重编程,或者修改由设备和处理器同时访问的数据结构。这些都能被很快地执行,而之所以说它们是紧急的是因为它们必须被尽快地执行。紧急操作要在一个中断处理程序内立即执行,而且是在禁止可屏蔽中断的情况下。

  • 非紧急的 (Noncritical
    这样的操作诸如:修改那些只有处理器才会访问的数据结构(例如,按下一个键后读扫描码)。这些操作也要很快地完成,因此,它们在开中断的情况下,由中断处理程序立即执行。

  • 非紧急可延迟的(Noncritical deferrable
    这样的操作诸如:把缓冲区的内容拷贝到某个进程的地址空间(例如,把键盘行缓冲区的内容发送到终端处理程序进程)。这些操作可能被延迟较长的时间间隔而不影响内核操作,有兴趣的进程将会等待数据。非紧急可延迟的操作由独立的函数来执行,我们将在 “软中断及 tasklet” 一节讨论。

  不管引起中断的电路种类如何,所有的 I/O 中断处理程序都执行四个相同的基本操作:

  1. 在内核态堆栈中保存 IRQ 的值和寄存器的内容。
  2. 为正在给 IRQ 线服务的 PIC 发送一个应答、这将允许 PIC 进一步发出中断。
  3. 执行共享这个 IRQ 的所有设备的中断服务例程(ISR)。
  4. 跳到 ret_from_intr() 的地址后终止。

  当中断发生时,需要用几个描述符来表示 IRQ 线的状态和需要执行的通数。图 4-4 以示意图的方式展示了处理一个中断的硬件电路和软件函数。下面几节会讨论这些函数。

(a)中断向量


(b)IRQ 数据结构

  每个中断向量都有它自己的 irq_desc_t 描述符,所有的这些描述符组织在一起形成 irq_desc 数组。

// include/linux/irq.h
/*** struct irq_desc - interrupt descriptor* @irq:     interrupt number for this descriptor* @timer_rand_state:   pointer to timer rand state struct* @kstat_irqs:       irq stats per cpu* @irq_2_iommu:   iommu with this irq* @handle_irq:      highlevel irq-events handler [if NULL, __do_IRQ()]* @chip:     low level interrupt hardware access* @msi_desc:        MSI descriptor* @handler_data: per-IRQ data for the irq_chip methods* @chip_data:     platform-specific per-chip private data for the chip*           methods, to allow shared chip implementations* @action:        the irq action chain* @status:     status information* @depth:        disable-depth, for nested irq_disable() calls* @wake_depth:        enable depth, for multiple set_irq_wake() callers* @irq_count:     stats field to detect stalled irqs* @last_unhandled:   aging timer for unhandled count* @irqs_unhandled:  stats field for spurious unhandled interrupts* @lock:      locking for SMP* @affinity:        IRQ affinity on SMP* @node:        node index useful for balancing* @pending_mask:    pending rebalanced interrupts* @threads_active:    number of irqaction threads currently running* @wait_for_threads:  wait queue for sync_irq to wait for threaded handlers* @dir:       /proc/irq/ procfs entry* @name:        flow handler name for /proc/interrupts output*/
struct irq_desc {unsigned int       irq;struct timer_rand_state *timer_rand_state;unsigned int            *kstat_irqs;
#ifdef CONFIG_INTR_REMAPstruct irq_2_iommu      *irq_2_iommu;
#endifirq_flow_handler_t    handle_irq;struct irq_chip      *chip;struct msi_desc       *msi_desc;void          *handler_data;void          *chip_data;struct irqaction *action;    /* IRQ action list */unsigned int       status;     /* IRQ status */unsigned int        depth;      /* nested irq disables */unsigned int       wake_depth; /* nested wake enables */unsigned int       irq_count;  /* For detecting broken IRQs */unsigned long        last_unhandled; /* Aging timer for unhandled count */unsigned int       irqs_unhandled;raw_spinlock_t       lock;
#ifdef CONFIG_SMPcpumask_var_t      affinity;unsigned int       node;
#ifdef CONFIG_GENERIC_PENDING_IRQcpumask_var_t      pending_mask;
#endif
#endifatomic_t      threads_active;wait_queue_head_t       wait_for_threads;
#ifdef CONFIG_PROC_FSstruct proc_dir_entry  *dir;
#endifconst char        *name;
} ____cacheline_internodealigned_in_smp;

五、内核同步

1、同步原语

(1)每CPU 变量

(2)原子操作


(3)优化和内存屏障

  当使用优化的编译器时,你千万不要认为指令会严格按它们在源代码中出现的顺序执行。例如,编译器可能重新安排汇编语言指令以使寄存器以最优的方式使用。此外,现代 CPU 通常并行地执行若干条指令,且可能重新安排内存访问。这种重新排序可以极大地加速程序的执行。

  然而,当处理同步时,必须避免指令重新排序。如果放在同步原语之后的一条指令在同步原语本身之前执行,事情很快就会变得失控。事实上,所有的同步原语起优化和内存屏障的作用。

(a)优化屏障(optimization barrier)

  优化屏障(optimization barrier)原语保证编译程序不会混淆放在原语操作之前的汇编语言指令和放在原语操作之后的汇编语言指令,这些汇编语言指令在C中都有对应的语句。在 Linux 中,优化屏障就是 barrier() 宏 。

// include/linux/compiler.h
# define barrier() __memory_barrier()// 展开为:
asm volatile("":::"memory")

  指令 asm 告诉编译程序要插入汇编语言片段(这种情况下为空)。volatile 关键字禁止编译器把 asm 指令与程序中的其他指令重新组合。memory 关键字强制编译器假定 RAM 中的所有内存单元已经被汇编语言指令修改了。因此,编译器不能使用存放在 CPU 寄存器中的内存单元的值来优化 asm 指令前的代码。

  注意,优化屏障并不保证不使当前 CPU 把汇编语言指令混在一起执行——这是内存屏障的工作。

(b)内存屏障(memory barrier)

  内存屏障(memory barrier)原语确保,在原语之后的操作开始执行之前,原语之前的操作已经完成。因此,内存屏障类似于防火墙,让任何汇编话言指令都不能通过。

  在 80x86 处理器中,下列种类的汇编语言指令是 “串行的” ,因为它们起内存屏障的作用:

  • I/O 端口进行操作的所有指令。
  • lock 前缀的所有指令(参见 “原子操作” 一节)。
  • 写控制寄存器、系统寄存器或调试寄存器的所有指令(例如,clisti,用于修改 eflags 寄存器的 IF 标志的状态)。
  • Pentium 4 微处理器中引入的汇编语言指令 lfencesfencemfence,它们分别有效地实现读内存屏障、写内存屏障和读-写内存屏障。
  • 少数专门的汇编语言指令,终止中断处理程序或异常处理程序的 iret 指令就是其中的一个。

  Linux 使用六个内存屏障原语,如表 5-6 所示。这些原语也被当作优化屏障,因为我们必须保证编译程序不在屏障前后移动汇编语言指令。“读内存屏障” 仅仅作用于从内存读的指令,而 “写内存屏障” 仅仅作用于写内存的指令。内存屏障既用于多处理器系统,也用于单处理器系统。当内存屏障应该防止仅出现于多处理器系统上的竞争条件时,就使用 smp_xxx() 原语;在单处理器系统上,它们什么也不做。其他的内存屏障防止出现在单处理器和多处理器系统上的竞争条件。

  内存屏障原语的实现依赖于系统的体系结构。在 80x86 微处理器上,

#ifdef NN
// 把 0 加到栈顶的内存单元;这条指令本身没有价值,
// 但是,lock 前缀使得这条指令成为 CPU 的一个内存屏障。
#define rmb()   asm volatile("lock; addl $0,0(%%esp)":::"memory")
#else
// 如果 CPU 支持 lfence 汇编语言指令,展开成如下形式
#define rmb()   asm volatile("lfence":::"memory")
#endif

  asm 指令告诉编译器插入一些汇编语言指令并起优化屏障的作用。Intel 上的 wmbb() 宏实际上更简单,因为它展开为 barrier()。这是因为 Intel 处理器从不对写内存访问重新排序,因此,没有必要在代码中插入一条串行化汇编指令。不过,这个宏禁止编译器重新组合指令。

  注意,在多处理器系统上,在前一节 “原子操作” 中描述的所有原子操作都起内存屏障的作用,因为它们使用了 lock 字节。

(4)自旋锁

可参考 ==> 2、自旋锁

// include/linux/spinlock_types.h
typedef struct spinlock {union {struct raw_spinlock rlock;#ifdef CONFIG_DEBUG_LOCK_ALLOC
# define LOCK_PADSIZE (offsetof(struct raw_spinlock, dep_map))struct {u8 __padding[LOCK_PADSIZE];struct lockdep_map dep_map;};
#endif};
} spinlock_t;typedef struct raw_spinlock {arch_spinlock_t raw_lock;
#ifdef CONFIG_GENERIC_LOCKBREAKunsigned int break_lock;
#endif
#ifdef CONFIG_DEBUG_SPINLOCKunsigned int magic, owner_cpu;void *owner;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOCstruct lockdep_map dep_map;
#endif
} raw_spinlock_t;// arch/x86/include/asm/spinlock_types.h
typedef struct arch_spinlock {unsigned int slock;
} arch_spinlock_t;

(a)具有内核抢占的 spin_lock 宏

  让我们来详细讨论用于请求自旋锁的 spin_lock 宏。下面的描述都是针对支持 SMP 系统的抢占式内核的。该宏获取自旋琐的地址 slp 作为它的参数,并执行下面的操作:

  1. 调用 preempt_disable() 以禁用内核抢占。
  2. 调用函数 _raw_spin_trylock(),它对自旋锁的 slock 字段执行原子性的测试和设置操作。该函数首先执行等价于下列汇编语言片段的一些指令:
movb $0, %al
xchgb %al, slp->slock

  汇编语言指令 xchg 原子性地交换 8 位寄存器 %al(存 0)和 slp->slock 指示的内存单元的内容。随后,如果存放在自旋锁中的旧值(在 xchg 指令执行之后存放在 %al 中)是正数,函数就返回 1,否则返回 0。
  注 2: 具有识刺意味的是,自旋锁是全局的、因此对它本身必须进行保护以防止并发访问。

  1. 如果自旋锁中的旧值是正数,宏结束:内核控制路径已经获得自旋锁。
  2. 否则,内核控制路径无法获得自旋锁,因此宏必须执行循环一直到在其他 CPU 上运行的内核控制路径释放自旋锁。调用 preempt_enable() 递减在第 1 步递增了的抢占计数器。如果在执行 spin_lock 宏之前内核抢占被启用,那么其他进程此时可以取代等待自旋锁的进程。
  3. 如果 break_lock 字段等于 0,则把它设置为 1。通过检测该字段,拥有锁并在其他 CPU 上运行的进程可以知道是否有其他进程在等待这个锁。如果进程把持某个自旋锁的时间太长,它可以提前释放锁以使等待相同自旋锁的进程能够继续向前运行。
  4. 执行等待循环:
while (spin_is_locked(slp) && slp->break_lock)cpu_relax();

  宏 cpu_relax() 简化为一条 pause 汇编语言指令。在 Pentium 4 模型中引入了这条指令以优化自旋锁循环的执行。通过引入一个很短的延迟,加快了紧跟在锁后面的代码的执行并减少了能源消耗。pause 与早先的 80x86 微处理器模型是向后兼容的,因为它对应 rep; nop 指令,也就是对应空操作。
7. 跳转回到第 1 步,再次试图获取自旋锁。

(b)非抢占式内核中的 spin_lock 宏

  如果在内核编译时没有选择内核抢占选项,spin_lock 宏就与前面描述的 spin_lock 宏有很大的区别。在这种情况下,宏生成一个汇编语言程序片段,它本质上等价于下面紧凑的忙等待(注 3):

1:  lock; decb slp->slockjns 3f
2:  pausecmpb $0, slp->slockjle 2bjmp 1b
3:

  汇编语言指令 decb 进减自旋锁的值,该指令是原子的,因为它带有 lock 字节前缀。随后检测符号标志,如果它被清 0,说明自旋锁被设置为 1(未锁),因此从标记 3 处继续正常执行(后缀 f 表示标签是 “向前的” ,它在程序的后面出现)。否则,在标签 2 处(后缀 b 表示 “向后的” 标签)执行紧凑循环直到自旋锁出现正值。然后从标签 1 处开始重新执行,因为不检查其他的处理器是否抢占了锁就继续执行是不安全的。

(c)spin_unlock 宏

  spin_unlock 宏释放以前获得的自旋锁,它本质上执行下列汇编语言指令:

movb $1, slp->slock

  并在随后调用 preempt_enable()(如果不支持内核抢占,preempt_enable() 什么都不做)。注意,因为现在的 80x86 微处理器总是原子地执行内存中的只写访问,所以不使用 lock 字节。

(5)顺序锁

// include/linux/seqlock.h
typedef struct {unsigned sequence;spinlock_t lock;
} seqlock_t;// include/linux/seqlock.h
static __always_inline unsigned read_seqbegin(const seqlock_t *sl)
{unsigned ret;repeat:ret = sl->sequence;smp_rmb();if (unlikely(ret & 1)) {cpu_relax();goto repeat;}return ret;
}

  当使用读 / 写自旋锁时,内核控制路径发出的执行 read_lockwrite_lock 操作的请求具有相同的优先权:读者必须等待,直到写操作完成。同样地,写者也必须等待,直到读操作完成。

  Linux 2.6 中引入了顺序锁(seqlock),它与读 / 写自旋锁非常相似,只是它为写者赋予了较高的优先级:事实上,即使在读者正在读的时候也允许写者继续运行。这种策略的好处是写者永远不会等待(除非另外一个写者正在写),缺点是有些时候读者不得不反复多次读相同的数据直到它获得有效的副本。

  每个顺序锁都是包括两个字段的 seqlock_t 结构:一个类型为 spinlock_tlock 字段和一个整型的 sequence 字段,第二个字段是一个顺序计数器。每个读者都必须在读数据前后两次读顺序计数器,并检查两次读到的值是否相同,如果不相同,说明新的写者已经开始写并增加了顺序计数器, 因此暗示读者刚读到的数据是无效的。

  通过把 SEQLOCK_UNLOCKED 赋给变量 seqlock_t 或执行 seqlock_init 宏,把 seqlock_t 变量初始化为 “未上锁” 。写者通过调用 write_seqlock()write_sequnlock() 获取和释放顺序锁。第一个函数获取 seqlock_t 数据结构中的自旋锁,然后使顺序计数器加 1;第二个函数再次增加顺序计数器,然后释放自旋锁。这样可以保证写者在写的过程中,计数器的值是奇数,并且当没有写者在改变数据的时候,计数器的值是偶数。

(6)读 - 拷贝 - 更新(RCU)

  读-拷贝-更新(RCU)是为了保护在多数情况下被多个 CPU 读的数据结构而设计的另一种同步技术。RCU 允许多个读者和写者并发执行(相对于只允许一个写者执行的顺序锁有了改进)。而且,RCU 是不使用锁的,就是说,它不使用被所有 CPU 共享的锁或计数器,在这一点上与读 / 写自旋锁和顺序锁(由于高速缓存行窃用和失效而有很高的开销)相比,RCU 具有更大的优势。

  RCU 是如何不使用共享数据结构而令人惊讶地实现多个 CPU 同步呢? 其关键的思想包括限制 RCU 的范围,如下所述:

  1. RCU 只保护被动态分配并通过指针引用的数据结构。
  2. 在被 RCU 保护的临界区中,任何内核控制路径都不能睡眠。

  当内核控制路径要读取被 RCU 保护的数据结构时,执行宏 rcu_read_lock(),它等同于 preempt_disable() 。接下来,读者间接引用该数据结构指针所对应的内存单元并开始读这个数据结构。正如在前面所强调的,读者在完成对数据结构的读操作之前,是不能睡眠的。用等同于 preempt_enable() 的宏 rcu_read_unlock() 标记临界区的结束。

  我们可以想象,由于读者几乎不做任何事情来防止竞争条件的出现,所以写者不得不做得更多一些。事实上,当写者要更新数据结构时,它间接引用指针并生成整个数据结构的副本。接下来,写者修改这个副本。一旦修改完毕,写者改变指向数据结构的指针,以使它指向被修改后的副本。由于修改指针值的操作是一个原子操作,所以旧副本和新副本对每个读者或写者都是可见的,在数据结构中不会出现数据奔溃。尽管如此,还需要内存屏障来保证:只有在数据结构被修改之后,已更新的指针对其他 CPU 才是可见的。如果把自旋锁与 RCU 结合起来以禁止写者的并发执行,就隐含地引入了这样的内存屏障。

  然而,使用 RCU 技术的真正困难在于:写者修改指针时不能立即释放数据结构的旧副本。实际上,写者开始修改时,正在访问数据结构的读者可能还在读旧副本。只有在 CPU 上的所有(潜在的)读者都执行完宏 rcu_read_unlock() 之后,才可以释放旧副本。内核要求每个潜在的读者在下面的操作之前执行 rcu_read_unlock() 宏:

  • CPU 执行进程切换(参见前面的约束条件 2)
  • CPU 开始在用户态执行
  • CPU 执行空循环(参见第三章"内核线程"一节)

  对上述每种情况,我们说 CPU 已经经过了静止状态(quiescent state)。写者调用函数 call_rcu() 来释放数据结构的旧副本。当所有的 CPU 都通过静止状态之后,call_rcu() 接受 rcu_head 描述符(通常嵌在要被释放的数据结构中)的地址和将要调用的回调函数的地址作为参数。一旦回调函数被执行,它通常释放数据结构的旧副本。

  函数 call_rcu() 把回调函数和其参数的地址存放在 rcu_head 描述符中,然后把描述符插入回调函数的每 CPU(per-CPU)链表中。内核每经过一个时钟滴答(参见第六章 “更新本地 CPU 统计数” 一节)就周期性地检查本地 CPU 是否经过了一个静止状态。如果所有CPU 都经过了静止状态,本地 tasklet(它的描述符存放在每 CPU 变量 rcu_tasklet 中)就执行链表中的所有回调函数。
  RCULinux 2.6 中新加的功能,用在网络层和虚拟文件系统中。

(7)信号量

  Linux 提供两种信号量:

  • 内核信号量,由内核控制路径使用
  • System V IPC 信号量,由用户态进程使用

可参考 ==> 4、信号量

深入理解 Linux 内核相关推荐

  1. 深入理解Linux内核之内存寻址

    说明: 本文基于第三版<深入理解 Linux 内核>,该部分以 80x86 处理器为基准进行介绍,并且略过了原文中详细介绍32位扩展分页部分. https://xcraft.tech/20 ...

  2. [转]深入理解linux内核list_head

    http://blog.chinaunix.net/uid-27122224-id-3277511.html 深入理解linux内核list_head的实现 2012-07-17 17:37:01 分 ...

  3. 《深入理解Linux内核》 读书笔记

    深入理解Linux内核 读书笔记 一.概论 操作系统基本概念 多用户系统 允许多个用户登录系统,不同用户之间的有私有的空间 用户和组 每个用于属于一个组,组的权限和其他人的权限,和拥有者的权限不一样. ...

  4. 深入理解Linux内核通知链(Notifier)

    数据结构 内核使用struct notifier_block结构代表一个notifier typedef int (*notifier_fn_t)(struct notifier_block *nb, ...

  5. 深入理解Linux内核-第3版 译者序、前言、目录 内核2.6.11

    一.译者序 Linux是一个全新的世界,世界意味着博大精深,而新或许代表对旧的割舍和扬弃,加在一起,就是要我们在割舍和扬弃的同时还要积累知识到博大精深的地步,这容易做到吗?是的,这不容易做到.Gera ...

  6. 深入理解Linux内核第3版--笔记-1.pdf

    深入理解Linux内核第3版.pdf         Understanding the Linux Kernel, 3rd Edition Preface    The Audience for T ...

  7. 深入理解Linux内核之主调度器(下)

    4.进程上下文切换 接前文:深入理解Linux内核之主调度器(上) 前面选择了一个合适进程作为下一个进程,接下来做重要的上下文切换动作,来保存上一个进程的"上下文"恢复下一个进程的 ...

  8. 深入理解LINUX内核(影印版第3版)》的笔记

    书名: 深入理解LINUX内核(影印版第3版) 作者: Daniel P.Bovet/Marco Cesati 副标题: Understanding the Linux Kernel 页数: 923 ...

  9. 深入理解Linux内核 学习Linux内核的一些建议及书记推荐

    深入理解Linux内核 学习Linux内核的一些建议_华清远见教育集团 经典书籍 待到山花烂漫时,还是那些经典在微笑. 有关内核的书籍可以用汗牛充栋来形容,不过只有一些经典的神作经住了考验.首先是5本 ...

最新文章

  1. 最详细的IDEA中使用Debug教程
  2. linux环境变量查询设置及保存
  3. [转]Chrome 控制台console的用法
  4. AI之Robot:带你玩转机器人DIY机器人——让你成为机器人的真正主人
  5. Light OJ 1214 Large Division (大数取模)
  6. 9、linux内存管理子系统
  7. android 开发艾特功能,Android Binder
  8. leetcode刷题:零钱兑换
  9. 连通域的原理与Python实现
  10. [转载] pandas dataframe 提取行和列
  11. 如何选购计算机硬盘,如何挑选固态硬盘,教您电脑固态硬盘如何挑选
  12. cim系统(cim系统包含哪些部分)
  13. 迅雷,快车,旋风下载地址加密和解密分析
  14. android系统是什么意思
  15. C语言建立循环单链表并输出
  16. ChatGPT国内镜像站
  17. 杰理AC632蓝牙芯片ADC
  18. JavaScript中的构造函数、原型和实例
  19. PreScanCarSim联合仿真实现车道保持
  20. RADAN 7下载(雷达数据分析软件)7.5.18.02270 官方多国语言版【支持中文】

热门文章

  1. YAML标准(什么是yaml)
  2. 收发一体超声波测距离传感器模块_超声模块,写完这篇推送,小编吐了
  3. 基于MT4平台通过CTP操作期货(一) -- 行情
  4. 主题酒店api,携程酒店信息查询
  5. 船舶IPTV电视系统方案注意事项
  6. wireshark 过滤omci包_PON注册认证过程分析
  7. dubbo 注册不上的问题 (随笔)
  8. 前后端分离项目,如何解决跨域问题?
  9. 【华为AI比赛】广告信息流跨域CTR预估(持续更新)
  10. 【整理】RGB和YUV色彩模式