当前位置: 首页 > news >正文

四川住房建设厅网站怎么用自己笔记本建设网站

四川住房建设厅网站,怎么用自己笔记本建设网站,制作个人网站论文,c语言除了做网站还能干什么Linux 内核系列文章 Linux 内核设计与实现 深入理解 Linux 内核 深入理解 Linux 内核#xff08;二#xff09; Linux 设备驱动程序 Linux设备驱动开发详解 文章目录 Linux 内核系列文章前言一、绪论二、内存寻址1、内存地址2、硬件中的分段#xff08;1#xff09;段选择符… Linux 内核系列文章 Linux 内核设计与实现 深入理解 Linux 内核 深入理解 Linux 内核二 Linux 设备驱动程序 Linux设备驱动开发详解 文章目录 Linux 内核系列文章前言一、绪论二、内存寻址1、内存地址2、硬件中的分段1段选择符 3、Linux 中的分段1Linux GDT2Linux 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、进程切换1switch_to 宏a分析 2__switch_to() 函数a分析 4、创建进程1do_fork() 函数2copy_process() 函数a分析bdo_fork 之后 3内核线程a创建一个内核线程b进程 0c进程 1d其他内核线程 4撤消进程5进程终止ado_group_exit() 函数描述 bdo_exit() 函数源码描述 5、进程删除 四、中断和异常1、中断和异常1IRQ 和中断2高级可编程中断控制器3异常4中断描述符表5中断和异常的硬件处理 2、中断和异常处理程序的嵌套执行1中断门、陷阱门及系统门 3、异常处理4、中断处理1I/O 中断处理a中断向量bIRQ 数据结构 五、内核同步1、同步原语1每CPU 变量2原子操作3优化和内存屏障a优化屏障optimization barrierb内存屏障memory barrier 4自旋锁a具有内核抢占的 spin_lock 宏b非抢占式内核中的 spin_lock 宏cspin_unlock 宏 5顺序锁6读 - 拷贝 - 更新RCU7信号量 前言 本文主要用来摘录《深入理解 Linux 内核》一书中学习知识点本书基于 Linux 2.6.11 版本源代码摘录基于 Linux 2.6.34 两者之间可能有些出入。 一、绪论 二、内存寻址 1、内存地址 可参考 ⇒ 1、内存寻址 2、硬件中的分段 可参考 ⇒ 五、分段机制 1段选择符 80x86 中有 6 个段寄存器分别为 csssdsesfs 和 gs。 6 个寄存器中 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 字段指定。只要当前特权级被改变一些段寄存器必须相应地更新。例如当 CPL3 时用户态ds 寄存器必须含有用户数据段的段选择符而当 CPL0 时ds 寄存器必须含有内核数据段的段选择符。 类似的情况也出现在 ss 寄存器中。当 CPL 为 3 时它必须指向一个用户数据段中的用户栈而当 CPL 为 0 时它必须指向内核数据段中的一个内核栈。当从用户态切换到内核态时Linux 总是确保 ss 寄存器装有内核数据段的段选择符。 当对指向指令或者数据结构的指针进行保存时内核根本不需要为其设置逻辑地址的段选择符因为 cs 寄存器就含有当前的段选择等。例如当内核调用一个函数时它执行一条 call 汇编语言指令该指令仅指定其逻辑地址的偏移量部分而段选择符不用设置它已经隐含在 cs 寄存器中了。因为在内核态执行的段只有一种叫做代码段由宏 __KERNEL_CS 定义所以只要当 CPU 切换到内核态时将 __KERNEL_CS 装载进 cs 就足够了。同样的道理也适用于指向内核数据结构的指针隐含地使用 ds 寄存器以及指向用户数据结构的指针内核显式地使用 es 寄存器。 除了刚才描述的 4 个段以外Linux 还使用了其他几个专门的段。我们将在下一节讲述 Linux GDT 的时候介绍它们。 1Linux 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 数组中值得特别说明的是第 n 个 CPU 的 TSS 描述符的 Base 字段指向 init_tss 数组的第 n 个元素。G粒度标志被清 0 而 Limit 字段置为 0xeb因为 TSS 段是 236 字节长。Type 字段置为 9 或 11可用的 32 位 TSS且 DPL 置为 0因为不允许用户态下的进程访问 TSS 段。在第三章任务状态段一节你可以找到 Linux 是如何使用 TSS 的细节。参考 3.1 任务状态段 1 个包括缺省局部描述符表的段这个段通常是被所有进程共享的段 参见下一节。3 个局部线程存储Thread-Local StorageTLS 段这种机制允许多线程应用程序使用最多 3 个局部于线程的数据段。系统调用 set_thread_area() 和 get_thread_area() 分别为正在执行的进程创建和撤消一个 TLS 段。与高级电源管理AMP相关的 3 个段由于 BIOS 代码使用段所以当 Linux APM 驱动程序调用 BIOS 函数来获取或者设置 APM 设备的状态时就可以使用自定义的代码段和数据段。与支持即插即用PnP功能的 BIOS 服务程序相关的 5 个段在前一种情况下就像前述与 AMP 相关的 3 个段的情况一样由于 BIOS 例程使用段所以当 Linux 的 PnP 设备驱动程序调用 BIOS 函数来检测 PnP 设备使用的资源时就可以使用自定义的代码段和数据段。被内核用来处理双重错误译注 1异常的特殊 TSS 段参见第四章的异常一节。 如前所述系统中每个处理器都有一个 GDT 副本。除少数几种情况以外所有 GDT 的副本都存放相同的表项。首先每个处理器都有它自己的 TSS 段因此其对应的 GDT 项不同。其次GDT 中只有少数项可能依赖于 CPU 正在执行的进程LDT 和 TLS 段描述符。最后在某些情况下处理器可能临时修改 GDT 副本里的某个项例如当调用 APM 的 BIOS 例程时就会发生这种情况。 2Linux LDT 大多数用户态下的 Linux 程序不使用局部描述符表这样内核就定义了一个缺省的 LDT 供大多数进程共享。缺省的局部描述符表存放在 default_ldt 数组中。它包含 5 个项但内核仅仅有效地使用了其中的两个项用于 iBCS 执行文件的调用门和 Solaris/x86 可执行文件的调用门参见第二十章的执行域一节。调用门是 80x86 微处理器提供的一种机制用于在调用预定义函数时改变 CPU 的特权级由于我们不会再更深入地讨论它们所以请参考 Intel 文档以获取更多详情。 在某些情况下进程仍然需要创建自己的局部描述符表。这对有些应用程序很有用像 Wine 那样的程序它们执行面向段的微软 Windows 应用程序。modify_ldt() 系统调用允许进程创建自己的局部描述符表。   任何被 modify_ldt() 创建的自定义局部描述符表仍然需要它自己的段。当处理器开始执行拥有自定义局部描述符表的进程时该 CPU 的 GDT 副本中的 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 处理器使用的分页机制。 我们将在第九章看到每一个进程有它自己的页全局目录和自己的页表集。当发生进程切换时参见第三章 “进程切换” 一节Linux 把 CR3 控制寄存器的内容保存在前一个执行进程的描述符中然后把下一个要执行进程的描述符的值装入 CR3 寄存器中。因此当新进程重新开始在 CPU 上执行时分页单元指向一组正确的页表。 1进程页表 进程的线性地址空间分成两部分 从 0x00000000 到 0xbfffffff 的线性地址无论进程运行在用户态还是内核态都可以寻址。从 0xc0000000 到 0xffffffff 的线性地址只有内核态的进程才能寻址。 当进程运行在用户态时它产生的线性地址小于 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 的内存范围能容纳于 RAM 前 8MB 空间里。为了映射 RAM 前 8MB 的空间需要用到两个页表。 分页第一个阶段的目标是允许在实模式下和保护模式下都能很容易地对这 8MB 寻址。因此内核必须创建一个映射把从 0x00000000 到 0x007fffff 的线性地址和从 0xc0000000 到 0xc07fffff 的线性地址映射到从 0x00000000 到 0x007fffff 的物理地址。换句话说内核在初始化的第一阶段可以通过与物理地址相同的线性地址或者通过从 0xc0000000 开始的 8MB 线性地址对 RAM 的前 8MB 进行寻址。 内核通过把 swapper_pg_dir 所有项都填充为 0 来创建期望的映射不过010x300十进制 768和 0x301十进制 769这四项除外后两项包含了从 0xc0000000 到 0xc07fffff 间的所有线性地址。0、1、0x300 和 0x301 按以下方式初始化 0 项和 0x300 项的地址字段置为 pg0 的物理地址而 1 项和 0x301 项的地址字段置为紧随 pg0 后的页框的物理地址。 把这四个项中的 Present、Read/Write 和 User/Supervisor 标志置位。 把这四个项中的 Accessed、Dirty、PCD、PWD 和 Page 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_* arent 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() 函数初始化。该函数进行如下操作 调用 pagetable_init() 适当地建立页表项。把 swapper_pg_dir 的物理地址写入 cr3 控制寄存器中。如果 CPU 支持 PAE 并且如果内核编译时支持 PAE则将 cr4 控制寄存器的 PAE 标志置位。调用 __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 在初始化阶段可以做的最好的事是把一个具有 896MB 的 RAM 窗口window映射到内核线性地址空间。如果一个程序需要对现有 RAM 的其余部分寻址那就必须把某些其他的线性地址间隔映射到所需的 RAM。这意味着修改某些页表项的值。我们将在第八章讨论这种动态重映射是如何进行的。 内核使用与前一种情况相同的代码来初始化页全局目录。 6当 RAM 大于 4096MB 时的最终内核页表 现在让我们考虑 RAM 大于 4GB 计算机的内核页表初始化更确切地说我们处理以下发生的情况 CPU 模型支持物理地址扩展PAERAM 容量大于 4GB内核以 PAE 支持来编译 尽管 PAE 处理 36 位物理地址但是线性地址依然是 32 位地址。如前所述Linux 映射一个 896MB 的 RAM 窗口到内核线性地址空间剩余 RAM 留着不映射并由动态重映射来处理第八章将对此进行描述。与前一种情况的主要差异是使用三级分页模型因此页全局目录按以下循环代码来初始化 pgd_idx pgd_index(PAGE_OFFSET); /* 3 */ for (i0; ipgd_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 (; iPTRS_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 (j0; 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 项留给非连续内存分配参见第八章的 “非连续内存区管理” 一节用 RAM 前 896MB 的物理地址填充。 注意支持 PAE 的所有 CPU 模型也支持大型 2MB 页和全局页。正如前一种情况一样只要可能Linux 使用大型页来减少页表数。 然后页全局目录的第四项被拷贝到第一项中这样好为线性地址空间的前 896MB 中的低物理内存映射作镜像。为了完成对 SMP 系统的初始化这个映射是必需的当这个映射不再必要时内核通过调用 zap_low_mappings() 函数来清除对应的页表项正如先前的情况一样。 7固定映射的线性地址 我们看到内核线性地址第四个GB 的初始部分映射系统的物理内存。但是至少 128MB 的线性地址总是留作他用因为内核使用这些线性地址实现非连续内存分配和固定映射的线性地址。 非连续内存分配仅仅是动态分配和释放内存页的一种特殊方式将在第八章 “非连续内存区的线性地址” 一节描述。本节我们集中讨论固定映射的线性地址。 固定映射的线性地址fix-mapped linear address基本上是一种类似于 0xffffc0004G - 16K 这样的常量线性地址其对应的物理地址不必等于线性地址减去 0xc000000而是可以以任意方式建立。因此每个固定映射的线性地址都映射一个物理内存的页框。我们将会在后面的章节看到内核使用固定映射的线性地址来代替指针变量因为这些指针变量的值从不改变。 固定映射的线性地址概念上类似于对 RAM 前 896MB 映射的线性地址。不过固定映射的线性地址可以映射任何物理地址而由第 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-(3PAGE_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 个例子它们是 LinuxThreads、 Native Posix Thread LibraryNPTL 和 IBM 的下一代 Posix 线程包 NGPTNext 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 parents 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 ps 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 havent 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 memswap usage */} memcg_batch; #endif }; 下图示意性地描述了 Linux 的进程描述符。 1标识一个进程 一般来说能被独立调度的每个执行上下文都必须拥有它自己的进程描述符因此即使共享内核大部分数据结构的轻量级进程也有它们自己的 task_struct 结构。 进程和进程描述符之间有非常严格的一一对应关系这使得用 32 位进程描述符地址注 3标识进程成为一种方便的方式。进程描述符指针指向这些地址内核对进程的大部分引用是通过进程描述符指针进行的。 另一方面类 Unix 操作系统允许用户使用一个叫做进程标识符 process ID或 PID的数来标识进程PID 存放在进程描述符的 pid 字段中。PID 被顺序编号新创建进程的 PID 通常是前一个进程的 PID 加 1。不过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内存区中存放两种数据结构的方式。线程描述符驻留于这个内存区的开始而栈从末端向下增长。该图还显示了分别通过 task 和 thread_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_info 和 free_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() 函数。从本质上说每个进程切换由两步组成 切换页全局目录以安装一个新的地址空间切换内核态堆栈和硬件上下文因为硬件上下文提供了内核执行新进程所需要的所有信息包含 CPU 寄存器。 1switch_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 宏执行。它是内核中与硬件关系最密切的例程之一要理解它到底做了些什么我们必须下些功夫。 首先该宏有三个参数它们是 prevnext 和 last 。你可能很容易猜到 prev 和 next 的作用它们仅是局部变量 prev 和 next 的占位符即它们是输入参数分别表示被替换进程和新进程描述符的地址在内存中的位置。   那第三个参数 last 呢 在任何进程切换中涉及到三个进程而不是两个。假设内核决定暂停进程 A 而激活进程 B。在 schedule() 函数中prev 指向 A 的描述符而 next 指向 B 的描述符。switch_to 宏一但使 A 暂停A 的执行流就冻结。 随后当内核想再次此激活 A就必须暂停另一个进程 C这通常不同于 B于是就要用 prev 指向 C 而 next 指向 A 来执行另一个 switch_to 宏。当 A 恢复它的执行流时就会找到它原来的内核栈于是 prev 局部变量还是指向 A 的描述符而 next 指向 B 的描述符。此时代表进程 A 执行的内核就失去了对 C 的任何引用。但是事实表明这个引用对于完成进程切换是很有用的更多细节参见第七章。 switch_to 宏的最后一个参数是输出参数它表示宏把进程 C 的描述符地址写在内存的什么位置了当然这是在 A 恢复执行之后完成的。在进程切换之前宏把第一个输入参数 prev即在 A 的内核堆栈中分配的 prev 局部变量表示的变量的内容存入 CPU 的 eax 寄存器。在完成进程切换A 已经恢复执行时宏把 CPU 的 eax 寄存器的内容写入由第三个输出参数 —— last 所指示的 A 在内存中的位置。因为 CPU 寄存器不会在切换点发生变化所以 C 的描述符地址也存在内存的这个位置。在 schedule() 执行过程中参数 last 指向 A 的局部变量 prev所以 prev 被 C 的地址覆盖。 图 3-7 显示了进程 ABC 内核堆栈的内容以及 eax 寄存器的内容。必须注意的是图中显示的是在被 eax 寄存器的内容覆盖以前的 prev 局部变量的值。 在 eax 和 edx 寄存器中分别保存 prev 和 next 的值 movl prev, %eax movl next, %edx把 eflags 和 ebp 寄存器的内容保存在 prev 内核栈中。必须保存它们的原因是编译器认为在 switch_to 结束之前它们的值应当保持不变。 pushfl pushl %ebp把 esp 的内容保存到 prev-thread.esp 中以使该字段指向 prev 内核栈的栈顶 movl %esp,484(%eax)484(%eax) 操作数表示内存单元的地址为 eax 内容加上 484。 把 next-thread.esp 装入 esp。此时内核开始在 next 的内核栈上操作因此这条指令实际上完成了从 prev 到 next 的切换。由于进程描述符的地址和内核栈的地址紧挨着就像我们在本章前面标识一个进程一节所解释的所以改变内核栈意味着改变当前进程。 movl 484(%edx), %esp把标记为 1 的地地址本节后面所示存入 prev-thread.eip。当被替换的进程重新恢复执行时进程执行被标记为 1 的那条指令 movl $1f, 480(%eax)宏把 next-thread.eip 的值绝大多数情况下是一个被标记为 1 的地址压入 next 的内核栈 pushl 480(%edx)跳到 __switch_to()   C 函数见下面 jmp __switch_to这里被进程 B 替换的进程 A 再次获得 CPU它执行一些保存 eflags 和 ebp 寄存器内容的指令这两条指令的第一条指令被标记为 1 。 1popl %ebppopfl注意这些 pop 指令是怎样引用 prev 进程的内核栈的。当进程调度程序选择了 prev 作为新进程在 CPU 上运行时将执行这些指令。于是以 prev 作为第二个参数调用 switch_to。因此esp 寄存器指向 prev 的内核栈。 拷贝 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;/* were 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 wont 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 weve determined that the* task is likely to be using it. */if (preload_fpu)__math_state_restore();return prev_p; }a分析 _switch_to() 函数执行大多数开始于 switch_to() 宏的进程切换。这个函数作用于 prev_p 和 next_p 参数这两个参数表示前一个进程和新进程。这个函数的调用不同于一般函数的调用因为 _switch_to() 从 eax 和 edx 取参数 prev_p 和 next_p我们在前面已看到这些参数就是保存在那里而不像大多数函数一样从栈中取参数。为了强迫函数从寄存器取它的参数内核利用 __attribute__ 和 regparm 关键字这两个关键字是 C 语言非标准的扩展名由 gcc 编译程序实现。在 include/asm-i386/system.h 头文件中__switch_to() 函数的声明如下 __switch_to(struct task_struct *prev_p,struct task_struct *next_p) _attribute__(regparm(3));函数执行的步骤如下 执行由 __unlazy_fpu() 宏产生的代码参见本章稍后 “保存和加载 FPU、MMX 及 XMM 寄存器” 一节以有选择地保存 prev_p 进程的 FPU、MMX 及 XMM 寄存器的内容。 __unlazy_fpu(prev_p);执行 smp_processor_id() 宏获得本地local CPU 的下标即执行代码的 CPU。该宏从当前进程的 thread_info 结构的 cpu 字段获得下标并将它保存到 cpu 局部变量。把 next_p-thread.esp0 装入对应于本地 CPU 的 TSS 的 esp0 字段我们将在第十章的 “通过 sysenter 指令发生系统调用” 一节看到以后任何由 sysenter 汇编指令产生的从用户态到内核态的特权级转换将把这个地址拷贝到 esp 寄存器中 init_tss[cpu].esp0 next_p-thread.esp0;把 next_p 进程使用的线程局部存储TLS段装入本地 CPU 的全局描述符表 三个段选择符保存在进程描述符内的 tls_array 数组中参见第二章的 “Linux 中的分段” 一节。 cpu_gdt_table[cpu][6] next_p-thread.tls_array[0]; cpu_gdt_table[cpu][7] next_p-thread.tls_array[1]; cpu_gdt_table[cpu][8] next_p-thread.tls_array[2];把 fs 和 gs 段寄存器的内容分别存放 prev_p-thread.fs 和 prev_p-thread.gs 中对应的汇编语言指令是 movl %fs, 40(%esi) movl %gs, 44(%esi)esi 寄存器指向 prev_p-thread 结构。 如果 fs 或 gs 段寄存器已经被 prev_p 或 next_p 进程中的任意一个使用也就是说如果它们有一个非 0 的值则将 next_p 进程的 thread_struct 描述符中保存的值装入这些寄存器中。这一步在逻辑上补充了前一步中执行的操作。主要的汇编语言指令如下 movl 40(%ebx),%fs movl 44(%ebx),%gsebx 寄存器指向 next_p-thread 结构。代码实际上更复杂因为当它检测到一个无效的段寄存器值时CPU 可能产生一个异常。代码采用一种 “修正fix-up” 途径来考虑这种可能性参见第十章动态地址检查修正代码 一节。 用 next_p-thread.debugreg 数组的内容装载 dr0…dr7 中的 6 个调试寄存器注 7。只有在 next_p 被挂起时正在使用调试寄存器也就是说next_p-thread.debugreg[7] 字段不为 0这种操作才能进行。这些寄存器不需要被保存因为只有当一个调试器想要监控 prev 时 prev_p-thread.debugreg 才会被修改。 if (next_p-thread.debugreg[7]) {loaddebug(next_p-thread, 0);loaddebug(next_p-thread, 1);loaddebug(next_p-thread, 2);loaddebug(next_p-thread, 3);/* 没有 4 和 5*/loaddebug(next_p-thread, 6);loaddebug(next_p-thread, 7); }如果必要更新 TSS 中的 I/O 位图。当 next_p 或 prev_p 有其自己的定制 I/O 权限位图时必须这么做 if (prev_p-thread.io_bitmap_ptr || next_p-thread.io_bitmap_ptr)handle_io_bitmap(next_p-thread, init_tss[cpu]);因为进程很少修改 I/O 权限位图所以该位图在懒模式中被处理当且仅当一个进程在当前时间片内实际访问 I/O 端口时真实位图才被拷贝到本地 CPU 的 TSS中。进程的定制 I/O 权限位图被保存在 thread_info 结构的 io_bitmap_ptr 字段指向的缓冲区中。 handle_io_bitmap() 函数为 next_p 进程设置本地 CPU 使用的 TSS 的 io_bitmap 字段如下 如果 next_p 进程不拥有自己的 I/O 权限位图则 TSS 的 io_bitmap字段被设为 0x8000。如果 next_p 进程拥有自己的 I/O 权限位图则 TSS 的 io_bitmap 字段被设为 0x9000。 TSS 的 io_bitmap 字段应当包含一个在 TSS 中的偏移量其中存放实际位图。无论何时用户态进程试图访问一个 I/O 端口0x8000 和 0x9000 指向 TSS 界限之外并将因此引起 “General protection” 异常参见第四章的 “异常” 一节。do_general_protection() 异常处理程序将检查保存在 io_bitmap 字段的值如果是 0x8000函数发送一个 SIGSEGV 信号给用户态进程 如果是 0x9000函数把进程位图由 thread_info 结构中的 io_bitmap_ptr 字段指示拷贝到本地 CPU 的 TSS 中把 io_bitmap 字段设为实际位图的偏移104并强制再一次执行有缺陷的汇编语言指令。 终止。 _switch_to() C函数通过使用下列声明结束 return prev_p;由编译器产生的相应汇编语言指令是 movl %edi,%eax retprev_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、创建进程 1do_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, dont 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* hasnt 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)) {/** Well 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; }2copy_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 doesnt 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* its process group.* A fatal signal pending means that current will exit, so the new* thread cant 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() 的最重要的步骤 检查参数 clone_flags 所传递标志的一致性。尤其是在下列情况下它返回错误代号 CLONE_NEWNS 和 CLONE_FS 标志都被设置。CLONE_THREAD 标志被设置但 CLONE_SIGHAND 标志被清 0同一线程组中的轻量级进程必须共享信号。CLONE_SIGHAND 标志被设置但 CLONE_VM 被清 0共享信号处理程序的轻量级进程也必须共享内存描述符。 通过调用 security_task_create() 以及稍后调用的 security_task_alloc() 执行所有附加的安全检查。Linux 2.6 提供扩展安全性的钩子函数与传统 Unix 相比它具有更加强壮的安全模型。详情参见第二十章。 调用 dup_task_struct() 为子进程获取进程描述符。该函数执行如下操作 如果需要则在当前进程中调用 __unlazy_fpu()把 FPU、MMX 和 SSE/SSE2 寄存器的内容保存到父进程的 thread_info 结构中。稍后dup_task_struct() 将把这些值复制到子进程的 thread_info 结构中。 执行 alloc_task_struct() 宏为新进程获取进程描述符task_struct 结构并将描述符地址保存在 tsk 局部变量中。 执行 alloc_thread_info 宏以获取一块空闲内存区用来存放新进程的 thread_info 结构和内核栈并将这块内存区字段的地址存在局部变量 ti 中。正如在本章前面 “标识一个进程” 一节中所述这块内存区字段的大小是 8KB 或 4KB 。 将 current 进程描述符的内容复制到 tsk 所指向的 task_struct 结构中然后把 tsk-thread_info 置为 ti 。 把 current 进程的 thread_info 描述符的内容复制到 ti 所指向的结构中然后把 ti-task 置为 tsk。 把新进程描述符的使用计数器tsk-usage置为 2用来表示进程描述符正在被使用而且其相应的进程处于活动状态进程状态即不是 EXIT_ZOMBIE也不是 EXIT_DEAD。 返回新进程的进程描述符指针tsk。 检查存放在 current-signal-rlim[RLIMIT_NPROC].rlim_cur 变量中的值是否小于或等于用户所拥有的进程数。如果是则返回错误码除非进程没有 root 权限。该函数从每用户数据结构 user_struct 中获取用户所拥有的进程数。通过进程描述符 user 字段的指针可以找到这个数据结构。 递增 user_struct 结构的使用计数器 tsk-user-__count 字段和用户所拥有的进程的计数器tsk-user-processes。 检查系统中的进程数量存放在 nr_threads 变量中是否超过 max_threads 变量的值。这个变量的缺省值取决于系统内存容量的大小。总的原则是所有 thread_info 描述符和内核栈所占用的空间不能超过物理内存大小的 1/8。不过系统管理员可以通过写 /proc/sys/kernel/threads-max 文件来改变这个值。 如果实现新进程的执行域和可执行格式的内核函数参见第二十章都包含在内核模块中则递增它们的使用计数器参见附录二。 设置与进程状态相关的几个关键字段 把大内核锁计数器 tsk-lock_depth 初始化为 -1参见第五章 “大内核锁” 一节。把 tsk-did_exec 字段初始化为 0 它记录了进程发出的 execve() 系统调用的次数。更新从父进程复制到 tsk-flags 字段中的一些标志首先清除 PF_SUPERPRIV 标志该标志表示进程是否使用了某种超级用户权限。然后设置 PF_FORKNOEXEC 标志它表示子进程还没有发出 execve() 系统调用。 把新进程的 PID 存入 tsk-pid 字段。 如果 clone_flags 参数中的 CLONE_PARENT_SETTID 标志被设置就把子进程的 PID 复制到参数 parent_tidptr 指向的用户态变量中。 初始化子进程描述符中的 list_head 数据结构和自旋锁并为与挂起信号、定时器及时间统计表相关的几个字段赋初值。 调用 copy_semundo() copy_files()copy_fs()copy_sighand()copy_signa1() copy_mm() 和 copy_namespace() 来创建新的数据结构并把父进程相应数据结构的值复制到新数据结构中除非 clone_flags 参数指出它们有不同的值。 调用 copy_thread()用发出 clone() 系统调用时 CPU 寄存器的值正如第十章所述这些值已经被保存在父进程的内核栈中来初始化子进程的内核栈。不过copy_thread() 把 eax 寄存器对应字段的值 [这是 fork() 和 clone() 系统调用在子进程中的返回值] 字段强行置为 0。子进程描述符的 thread.esp 字段初始化为子进程内核栈的基地址汇编语言函数ret_from_fork()的地址存放在 thread.eip 字段中。如果父进程使用 I/O 权限位图则子进程获取该位图的一个拷贝。最后如果 CLONE_SETTLS 标志被设置则子进程获取由 clone() 系统调用的参数 tls 指向的用户态数据结构所表示的 TLS 段注 9。 如果 clone_flags 参数的值被置为 CLONE_CHILD_SETTID 或CLONE_CHILD_CLEARTID就把 child_tidptr 参数的值分别复制到 tsk-set_chid_tid 或 tsk-clear_child_tid 字段。这些标志说明必须改变子进程用户态地址空间的 child_tidptr 所指向的变量的值不过实际的写操作要稍后再执行。 清除子进程 thread_info 结构的 TIF_SYSCALL_TRACE 标志以使 ret_from_fork() 函数不会把系统调用结束的消息通知给调试进程参见第十章 “进入和退出系统调用” 一节。为对子进程的跟踪是由 tsk-ptrace 中的 PTRACE_SYSCALL 标志来控制的所以子进程的系统调用跟踪不会被禁用。 用 clone_flags 参数低位的信号数字编码初始化 tsk-exit_signal 字段如果 CLONE_THREAD 标志被置位就把 tsk-exit_signal 字段初始化为 -1。正如我们将在本章稍后 “进程终止” 一节所看见的只有当线程组的最后一个成员通常是线程组的领头“死亡”才会产生一个信号以通知线程组的领头进程的父进程。 调用 sched_fork() 完成对新进程调度程序数据结构的初始化。该函数把新进程的状态设置为 TASK_RUNNING并把 thread_info 结构的 preempt_count 字段设置为 1从而禁止内核抢占参见第五章 “内核抢占” 一节。此外为了保证公平的进程调度该函数在父子进程之间共享父进程的时间片参见第七章 “scheduler_tick() 数” 一节。 把新进程的 thread_info 结构的 cpu 字段设置为由 smp_processor_id() 所返回的本地 CPU 号。 初始化表示亲子关系的字段。尤其是如果 CLONE_PARENT 或 CLONE_THREAD 被设置就用 current-real_parent 的值初始化 tsk-real_parent 和 tsk-parent因此子进程的父进程似乎是当前进程的父进程。否则把 tsk-real_parent 和 tsk-parent 置为当前进程。 如果不需要跟踪子进程没有设置 CLONE_PTRAC 标志就把 tsk-ptrace 字段设置为 0 。tsk-ptrace 字段会存放一些标志而这些标志是在一个进程被另外一个进程跟踪时才会用到的。采用这种方式即使当前进程被跟踪子进程也不会被跟踪。 执行 SET_LINKS 宏把新进程描述符插入进程链表。 如果子进程必须被跟踪tsk-ptrace 字段的 PT_PTRACED 标志被设置就把 current-parent 赋给 tsk-parent并将子进程插入调试程序的跟踪链表中。 调用 attach_pid() 把新进程插述符的 PID 插入 pidhash[PIDTYPE_PID] 散列表。 如果子进程是线程组的领头进程CLONE_THREAD 标志被清 0 把 tsk-tgid 的初值置为 tsk-pid。把 tsk-group_leader 的初值置为 tsk。调用三次 attach_pid() 把子进程分别插入 PIDTYPE_TGIDPIDTYPE_PGID 和 PIDTYPE_SID 类型的 PID 散列表。 否则如果子进程属于它的父进程的线程组CLONE_THREAD 标志被设置 把 tsk-tgid 的初值置为 tsk-current-tgid。把 tsk-group_leader 的初值置为 current-group_leader 的值。调用 attach_pid()把子进程插入 PIDTYPE_TGID 类型的散列表中更具体地说插入 current-group_leader 进程的每个 PID 链表。 现在新进程已经被加入进程集合递增 nr_threads 变量的值。 递增 total_forks 变量以记录被创建的进程的数量。 终止并返回子进程描述符指针tsk。 bdo_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() 把 ebx 和 edx 分别设置为参数 fn 和 arg 的值。把 eip 寄存器的值设置为下面汇编语言代码段的地址 movl %edx, %eax pushl %edx call *%ebx pushl %eax call do_exit因此新的内核线程开始执行 fn(arg) 函数如果该函数结束内核线程执行系统调用 _exit()并把 fn() 的返回值传递给它参见本章稍后 “撤消进程” 一节。 b进程 0 // arch/x86/kernel/init_task.c /** Initial thread structure.** We need to make sure that this is THREAD_SIZE aligned due to the* way process stacks are handled. This is done by having a special* init_task linker map entry..*/ union thread_union init_thread_union __init_task_data { INIT_THREAD_INFO(init_task) };/** Initial task structure.** All other task structs will be allocated on slabs in fork.c*/ struct task_struct init_task INIT_TASK(init_task); EXPORT_SYMBOL(init_task);// include/linux/init_task.h #define INIT_TASK(tsk) \ { \.state 0, \.stack init_thread_info, \.usage ATOMIC_INIT(2), \.flags PF_KTHREAD, \.lock_depth -1, \.prio MAX_PRIO-20, \.static_prio MAX_PRIO-20, \.normal_prio MAX_PRIO-20, \.policy SCHED_NORMAL, \.cpus_allowed CPU_MASK_ALL, \.mm NULL, \.active_mm init_mm, \.se { \.group_node LIST_HEAD_INIT(tsk.se.group_node), \}, \.rt { \.run_list LIST_HEAD_INIT(tsk.rt.run_list), \.time_slice HZ, \.nr_cpus_allowed NR_CPUS, \}, \.tasks LIST_HEAD_INIT(tsk.tasks), \.pushable_tasks PLIST_NODE_INIT(tsk.pushable_tasks, MAX_PRIO), \.ptraced LIST_HEAD_INIT(tsk.ptraced), \.ptrace_entry LIST_HEAD_INIT(tsk.ptrace_entry), \.real_parent tsk, \.parent tsk, \.children LIST_HEAD_INIT(tsk.children), \.sibling LIST_HEAD_INIT(tsk.sibling), \.group_leader tsk, \.real_cred init_cred, \.cred init_cred, \.cred_guard_mutex \__MUTEX_INITIALIZER(tsk.cred_guard_mutex), \.comm swapper, \.thread INIT_THREAD, \.fs init_fs, \.files init_files, \.signal init_signals, \.sighand init_sighand, \.nsproxy init_nsproxy, \.pending { \.list LIST_HEAD_INIT(tsk.pending.list), \.signal {{0}}}, \.blocked {{0}}, \.alloc_lock __SPIN_LOCK_UNLOCKED(tsk.alloc_lock), \.journal_info NULL, \.cpu_timers INIT_CPU_TIMERS(tsk.cpu_timers), \.fs_excl ATOMIC_INIT(0), \.pi_lock __RAW_SPIN_LOCK_UNLOCKED(tsk.pi_lock), \.timer_slack_ns 50000, /* 50 usec default slack */ \.pids { \[PIDTYPE_PID] INIT_PID_LINK(PIDTYPE_PID), \[PIDTYPE_PGID] INIT_PID_LINK(PIDTYPE_PGID), \[PIDTYPE_SID] INIT_PID_LINK(PIDTYPE_SID), \}, \.dirties INIT_PROP_LOCAL_SINGLE(dirties), \INIT_IDS \INIT_PERF_EVENTS(tsk) \INIT_TRACE_IRQFLAGS \INIT_LOCKDEP \INIT_FTRACE_GRAPH \INIT_TRACE_RECURSION \INIT_TASK_RCU_PREEMPT(tsk) \ }#define INIT_CPU_TIMERS(cpu_timers) \ { \LIST_HEAD_INIT(cpu_timers[0]), \LIST_HEAD_INIT(cpu_timers[1]), \LIST_HEAD_INIT(cpu_timers[2]), \ }所有进程的祖先叫做进程 0idle 进程或因为历史的原因叫做 swapper 进程它是在 Linux 的初始化阶段从无到有创建的一个内核线程参见附录一。这个祖先进程使用下列静态分配的数据结构所有其他进程的数据结构都是动态分配的 存放在 init_task 变量中的进程描述符由 INIT_TASK 宏完成对它的初始化。 存放在 init_thread_union 变量中的 thread_info 描述符和内核堆栈由 INIT_THREAD_INFO 宏完成对它们的初始化。 由进程描述符指向的下列表 init_mminit_fsinit_filesinit_signalsinit_sighand 这些表分别由下列宏初始化 INIT_MMINIT_FSINIT_FILESINIT_SIGNALSINIT_SIGHAND 主内核页全局目录存放在 swapper_pg_dir 中参见第二章内核页表一节。 start_kernel() 函数初始化内核需要的所有数据结构激活中断创建另一个叫进程 1 的内核线程一般叫做 init 进程 kernel_thread(init, NULL, CLONE_FS | CLONE_SIGHAND);新创建内核线程的 PID 为 1并与进程 0 共享每进程所有的内核数据结构。此外当调度程序选择到它时init 进程开始执行 init() 函数。 创建 init 进程后进程 0 执行 cpu_idle() 函数该函数本质上是在开中断的情况下重复执行 hlt 汇编语言指令参见第四章。只有当没有其他进程处于 TASK_RUNNING 状态时调度程序才选择进程 0 。 在多处理器系统中每个 CPU 都有一个进程 0 。只要打开机器电源计算机的 BIOS 就启动某一个 CPU同时禁用其他 CPU。运行在 CPU 0 上的 swapper 进程初始化内核数据结构然后激活其他的 CPU并通过 copy_process() 函数创建另外的 swapper 进程把 0 传递给新创建的 swapper 进程作为它们的新 PID。此外内核把适当的 CPU 索引赋给内核所创建的每个进程的 thread_info 描述符的 cpu 字段。 c进程 1 由进程 0 创建的内核线程执行 init() 函数init() 依次完成内核初始化。init() 调用 execve() 系统调用装入可执行程序 init。结果init 内核线程变为一个普通进程且拥有自己的每进程per-process内核数据结构参见第二十章。在系统关闭之前init 进程一直存活因为它创建和监控在操作系统外层执行的所有进程的活动。 d其他内核线程 Linux 使用很多其他内核线程。其中一些在初始化阶段创建一直运行到系统关闭而其他一些在内核必须执行一个任务时 “按需” 创建这种任务在内核的执行上下文中得到很好的执行。 一些内核线程的例子除了进程 0 和进程 1是 keventd也被称为事件 执行 keventd_wq 工作队列参见第四章中的函数。 kapmd 处理与高级电源管理APM相关的事件。 kswapd 执行内存回收在第十七章周期回收一节将进行描述。 pdflush 刷新 “脏” 缓冲区中的内容到磁盘以回收内存在第十五章 “pdflush内核线程” 一节将进行描述。 kblockd 执行 kblockd_workqueue 工作队列中的函数。实质上它周期性地激活块设备驱动程序将在第十四章 “激活块设备驱动程序” 一节给予描述。 ksoftirqd 运行 tasklet参看第四章 “软中断及 tasklet” 一节系统中每个 CPU 都有这样一个内核线程。 4撤消进程 很多进程终止了它们本该执行的代码从这种意义上说这些进程死了。当这种情况发生时必须通知内核以便内核释放进程所拥有的资源包括内存、打开文件及其他我们在本书中讲到的零碎东西如信号量。 进程终止的一般方式是调用 exit() 库函数该函数释放 C 函数库所分配的资源执行编程者所注册的每个函数并结束从系统回收进程的那个系统调用。exit() 函数可能由编程者显式地插入。另外C 编译程序总是把 exit() 函数插入到 main() 函数的最后一条语句之后。 内核可以有选择地强迫整个线程组死掉。这发生在以下两种典型情况下当进程接收到一个不能处理或忽视的信号时参见十一章或者当内核正在代表进程运行时在内核态产生一个不可恢复的 CPU 异常时参见第四章。 5进程终止 在 Linux 2.6 中有两个终止用户态应用的系统调用 exit_group() 系统调用它终止整个线程组即整个基于多线程的应用。do_group_exit() 是实现这个系统调用的主要内核函数。这是 C 库函数 exit() 应该调用的系统调用。 exit() 系统调用它终止某一个线程而不管该线程所属线程组中的所有其他进程。do_exit() 是实现这个系统调用的主要内核函数。这是被诸如 pthread_exit() 的 Linux 线程库的函数所调用的系统调用。 ado_group_exit() 函数 应用层调用 #include linux/unistd.hvoid 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 dont get here */if (signal_group_exit(sig))exit_code sig-group_exit_code;else if (!thread_group_empty(current)) {struct sighand_struct *const sighand current-sighand;spin_lock_irq(sighand-siglock);if (signal_group_exit(sig))/* Another thread got here before we took the lock. */exit_code sig-group_exit_code;else {sig-group_exit_code exit_code;sig-flags SIGNAL_GROUP_EXIT;zap_other_threads(current);}spin_unlock_irq(sighand-siglock);}do_exit(exit_code);/* NOTREACHED */ }描述 do_group_exit() 函数杀死属于 current 线程组的所有进程。它接受进程终止代号作为参数进程终止代号可能是系统调用 exit_group()正常结束指定的一个值也可能是内核提供的一个错误代号异常结束。该函数执行下述操作 检查退出进程的 SIGNAL_GROUP_EXIT 标志是否不为 0如果不为 0说明内核已经开始为线程组执行退出的过程。在这种情况下就把存放在 current-signal-group_exit_code 中的值当作退出码然后跳转到第 4 步。否则设置进程的 SIGNAL_GROUP_EXIT 标志并把终止代号存放到 current-signal-group_exit_code 字段。调用 zap_other_threads() 函数杀死 current 线程组中的其他进程如果有的话。为了完成这个步骤函数扫描与 current-tgid 对应的 PIDTYPE_TGID 类型的散列表中的每个 PID 链表向表中所有不同于 current 的进程发送 SIGKILL 信号参见第十一章结果所有这样的进程都将执行 do_exit() 函数从而被杀死。调用 do_exit() 函数把进程的终止代号传递给它。正如我们将在下面看到的do_exit() 杀死进程而且不再返回。 bdo_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);/** Were 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_ALERTFixing 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 mms 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() 函数接受进程的终止代号作为参数并执行下列操作 把进程描述符的 flag 字段设置为 PF_EXITING 标志以表示进程正在被删除。 如果需要通过函数 del_timer_sync()参见第六章从动态定时器队列中删除进程描述符。 分别调用 exit_mm()、exit_sem()、__exit_files()、__exit_fs()、exit_namespace() 和 exit_thread() 函数从进程描述符中分离出与分页、信号量、文件系统、打开文件描述符、命名空间以及 I/O 权限位图相关的数据结构。如果没有其他进程共享这些数据结构那么这些函数还删除所有这些数据结构中。 如果实现了被杀死进程的执行域和可执行格式参见第二十章的内核函数包含在内核模块中则函数递减它们的使用计数器。 把进程描述符的 exit_code 字段设置成进程的终止代号这个值要么是 _exit() 或 exit_group() 系统调用参数正常终止要么是由内核提供的一个错误代号异常终止。 调用 exit_notify() 函数执行下面的操作 更新父进程和子进程的亲属关系。如果同一线程组中有正在运行的进程就让终止进程所创建的所有子进程都变成同一线程组中另外一个进程的子进程否则让它们成为 init 的子进程。 检查被终止进程其进程描述符的 exit_signal 字段是否不等于 -1并检查进程是否是其所属进程组的最后一个成员注意正常进程都会具有这些条件参见前面 “clone()、fork() 和 vfork() 系统调用” 一节中对 copy_process() 的描述第 16 步。在这种情况下函数通过给 正被终止进程的父进程发送一个信号通常是 SIGCHLD以通知父进程子进程死亡。 否则也就是 exit_signal 字段等于 -1或者线程组中还有其他进程那么只要进程正在被跟踪就向父进程发送一个 SIGCHLD 信号在这种情况下父进程是调试程序因而向它报告轻量级进程死亡的信息。 如果进程描述符的 exit_signal 字段等于 -1而且进程没有被跟踪就把进程描述符的 exit_state 字段置为 EXIT_DEAD然后调用 release_task() 回收进程的其他数据结构占用的内存并递减进程描述符的使用计数器见下一节。使用记数变为为 1参见 copy_process() 函数的第 3f 步以使进程描述符本身正好不会被释放。 否则如果进程描述符的 exit_signal 字段不等于 -1或进程正在被跟踪就把 exit_state 字段置为 EXIT_ZOMBIE。在下一节我们将看到如何处理僵死进程。 把进程描述符的 flags 字段设置为 PF_DEAD 标志参见第七章 schedule() 函数 一节。 调用 schedule() 函数参见第七章选择一个新进程运行。调度程序忽略处于 EXIT_ZOMBIE 状态的进程所以这种进程正好在 schedule() 中的宏 switch_to 被调用之后停止执行。正如在第七章我们将看到的调度程序将检查被替换的僵死进程描述符的 PF_DEAD 标志并递减使用计数器从而说明进程不再存活的事实。 5、进程删除 Unix 允许进程查询内核以获得其父进程的 PID或者其任何子进程的执行状态。例如进程可以创建一个子进程来执行特定的任务然后调用诸如 wait() 这样的一些库函数检查子进程是否终止。如果子进程已经终止那么它的终止代号将告诉父进程这个任务是否已成功地完成。 为了遵循这些设计选择不允许 Unix 内核在进程一终止后就丢弃包含在进程描述符字段中的数据。只有父进程发出了与被终止的进程相关的 wait() 类系统调用之后才允许这样做。这就是引入僵死状态的原因尽管从技术上来说进程已死但必须保存它的描述符直到父进程得到通知。 如果父进程在子进程结束之前结束会发生什么情况呢? 在这种情况下系统中会到处是僵死的进程而且它们的进程描述符永久占据着 RAM。如前所述必须强迫所有的孤儿进程成为 init 进程的子进程来解决这个问题。这样init 进程在用 wait() 类系统调用检查其合法的子进程终止时就会撤消僵死的进程。 release_task() 函数从僵死进程的描述符中分离出最后的数据结构对僵死进程的处理有两种可能的方式如果父进程不需要接收来自子进程的信号就调用 do_exit() 如果已经给父进程发送了一个信号就调用 wait4() 或 waitpid() 系统调用。在后一种情况下函数还将回收进程描述符所占用的内存空间而在前一种情况下内存的回收将由进程调度程序来完成参见第七章。该函数执行下述步骤 递减终止进程拥有者的进程个数。这个值存放在本章前面提到的 user_struct 结构中参见 copy_process() 的第 4 步。如果进程正在被跟踪函数将它从调试程序的 ptrace_children 链表中删除并让该进程重新属于初始的父进程。调用 __exit_signal() 删除所有的挂起信号并释放进程的 signal_struct 描述符。如果该描述符不再被其他的轻量级进程使用函数进一步删除这个数据结构。此外函数调用 exit_itimers() 从进程中剥离掉所有的 POSIX 时间间隔定时器。调用 __exit_sighand() 删除信号处理函数。调用 _unhash_process()该函数依次执行下面的操作 变量 nr_threads 减 1。两次调用 detach_pid()分别从 PIDTYPE_PID 和 PIDTYPE_TGID 类型的 PID 散列表中删除进程描述符。如果进程是线程组的领头进程那么再调用两次 detach_pid()从 PIDTYPE_PGID 和 PIDTYPE_SID 类型的散列表中删除进程描述符。用宏 REMOVE_LINKS 从进程链表中解除进程描述符的链接。 如果进程不是线程组的领头进程领头进程处于僵死状态而且进程是线程组的最后一个成员则该函数向领头进程的父进程发送一个信号通知它进程已死亡。调用 sched_exit() 函数来调整父进程的时间片这一步在逻辑上作为对 copy_process() 第 17 步的补充。调用 put_task_struct() 递减进程描述符的使用计数器如果计数器变为 0则函数终止所有残留的对进程的引用。 递减进程所有者的 user_struct 数据结构的使用计数器__count 字段参见 copy_process() 的第 5 步如果使用计数器变为 0就释放该数据结构。释放进程描述符以及 thread_info 描述符和内核态堆栈所占用的内存区域。 四、中断和异常 中断处理是由内核执行的最敏感的任务之一因为它必须满足下列约束 当内核正打算去完成一些别的事情时中断随时会到来。因此内核的目标就是让中断尽可能快地处理完尽其所能把更多的处理向后推迟。例如假设一个数据块已到达了网线当硬件中断内核时内核只简单地标志数据到来了让处理器恢复到它以前运行的状态其余的处理稍后再进行如把数据移入一个缓冲区它的接收进程可以在缓冲区找到数据并恢复这个进程的执行。因此内核响应中断后需要进行的操作分为两部分关键而紧急的部分内核立即执行其余推迟的部分内核随后执行。因为中断随时会到来所以内核可能正在处理其中的一个中断时另一个中断不同类型又发生了。应该尽可能多地允许这种情况发生因为这能维持更多的 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 在编程者发出请求时发生。是由 int 或 int3 指令触发的当 into检查溢出和 bound检查地址出界指令检查的条件不为真时也引起编程异常。控制单元把编程异常作为陷阱来处理。编程异常通常也叫做软中断software interrupt。这样的异常有两种常用的用途执行系统调用及给调试程序通报一个特定的事件参见第十章。 每个中断和异常是由 0 ~ 255 之间的一个数来标识。因为一些未知的原因Intel 把这个 8 位的无符号整数叫做一个向量vector。非屏蔽中断的向量和异常的向量是固定的而可屏蔽中断的向量可以通过对中断控制器的编程来改变参见下一节。 1IRQ 和中断 每个能够发出中断请求的硬件设备控制器都有一条名为 IRQInterrupt ReQuest的输出线注1。所有现有的 IRQ 线IRQ line都与一个名为可编程中断控制器Programmable Interrpt Controuer PIC的硬件电路的输入引脚相连。可编程中断控制器执行下列动作 监视 IRQ 线。检查产生的信号raised signal。如果有条或两条以上的 IRQ 线上产生信号就选择引脚编号较小的 IRQ 线。如果一个引发信号出现在 IRQ 线上 把接收到的引发信号转换成对应的向量。把这个向量存放在中断控制器的一个I/O 端口从而允许 CPU 通过数据总线读此向量把引发信号发送到处理器的 INTR 引脚即产生一个中断。等待直到 CPU 通过把这个中断信号写进可编程中断控制器的一个 I/O 端口来确认它当这种情况发生时清 INTR 线。 返回到第 1 步。 IRQ 线是从 0 开始顺序编号的因此第一条 IRQ 线通常表示成 IRQ0。与 IRQn 关联的 Intel 的缺省向量是 n32。如前所述通过向中断控制器端口发布合适的指令就可以修改 IRQ 和向量之间的映射。   可以有选择地禁止每条 IRQ 线。因此可以对 PIC 编程从而禁止 IRQ也就是说可以告诉 PIC 停止对给定的 IRQ 线发布中断或者激话它们。禁止的中断是丢失不了的它们一旦被激活PIC 就又把它们发送到 CPU。这个特点被大多数中断处理程序使用因为这允许中断处理程序逐次地处理同一类型的 IRQ。 有选择地激活/禁止 IRQ 线不同于可屏蔽中断的全局屏蔽 /非屏蔽。当 eflags 寄存器的 IF 标志被清 0 时由 PIC 发布的每个可屏蔽中断都由 CPU 暂时忽略。cli 和 sti 汇编指令分别清除和设置该标志。可参考 1、标志寄存器 传统的 PIC 是由两片 8259A 风格的外部芯片以 “级联” 的方式连接在一起的。每个芯片可以处理多达 8 个不同的 IRQ 输入线。因为从 PIC 的 INT 输出线连接到主 PIC 的 IRQ2 引脚因此可用 IRQ 线的个数限制为 15。 2高级可编程中断控制器 以前的描述仅涉及为单处理器系统设计的 PIC。如果系统只有一个单独的 CPU那么主 PIC 的输出线以直接了当的方式连接到 CPU 的 INTR 引脚。然而如果系统中包含两个或多个 CPU那么这种方式不再有效因而需要更复杂的 PIC。 为了充分发挥 SMP 体系结构的并行性能够把中断传递给系统中的每个 CPU 至关重要。基于此理由Intel 从 Pentiun III 开始引入了一种名为 I/O 高级可编程控制器I/O Advanced Programmable Interrupt ControllerI/O APIC的新组件用以代替老式的 8259A 可编程中断控制器。新近的主板为了支持以前的操作系统都包括两种芯片。此外80x86 微处理器当前所有的 CPU 都含有一个本地 APIC。每个本地 APIC 都有 32 位的寄存器、一个内部时钟、一个本地定时设备及为本地 APIC 中断保留的两条额外的 IRQ 线 LINT0 和 LINT1。所有本地 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 的组成为一组 24 条 IRQ 线、一张 24 项的中断重定向表Interrupt Redirection Table、可编程寄存器以及通过 APIC 总线发送和接收 APIC 信息的一个信息单元。与 8259A 的 IRQ 引脚不同中断优先级并不与引脚号相关联中断重定向表中的每一项都可以被单独编程以指明中断向量和优先级、目标处理器及选择处理器的方式。重定向表中的信息用于把每个外部 IRQ 信号转换为一条消息然后通过 APIC 总线把消息发送给一个或多个本地 APIC 单元。 来自外部硬件设备的中断请求以两种方式在可用 CPU 之间分发 静态分发 IRQ 信号传递给重定向表相应项中所列出的本地 APIC。中断立即传递给一个特定的 CPU或一组 CPU或所有 CPU广播方式。 动态分发 如果处理器正在执行最低优先级的进程IRQ 信号就传递给这种处理器的本地 APIC。每个本地 APIC 都有一个可编程任务优先级寄存器task priority registerTPR 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 被禁止两条 LINT0 和 LINT1 本地 IRQ 线分别配置为 INTR 和 NMI 引脚。作为一种标准外部 I/O APIC 。本地 APIC 被激活且所有的外部中断都通过 I/O APIC 接收。 3异常 80x86 微处理器发布了大约 20 种不同的异常注3。内核必须为每种异常提供一个专门的异常处理程序。对于某些异常CPU 控制单元在开始执行异常处理程序前会产生一个硬件出错码hardware error code并且压入内核态堆栈。 下面的列表给出了在 80x86 处理器中可以找到的异常的向量、名字、类型及其简单描述。更多的信息可以在 Intel 的技术文挡中找到。 0 —— “Divide error”故障 当一个程序试图执行整数被 0 除操作时产生。 1 —— “Debug”陷阱或故障 产生于1设置 eflags 的 TF 标志时对于实现调试程序的单步执行是相当有用的2一条指令或操作数的地址落在一个活动 debug 寄存器的范围之内参见第三章的 “硬件上下文” 一节。 2 —— 未用 为非屏蔽中断保留利用 NMI 引脚的那些中断。 3 —— “Breakpoint”陷阱 由 int3 断点指令通常由 debugger 插入引起。 4 —— “Overflow”陷阱 当 eflags 的 OF overflow标志被设置时into检查溢出指令被执行。 5 —— “Bounds check”故障 对于有效地址范围之外的操作数bound检查地址边界指令被执行。 6 ——“Invalid opcode” 故障 CPU 执行单元检测到一个无效的操作码决定执行操作的机器指令部分。 7 —— “Device not available”故障 随着 cr0 的 TS 标志被设置ESCAPE、MMX 或 XMM 指令被执行参见第三章的保存和加载 FPU、MMX 及 XMM 寄存器一节。 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 芯片中的 SSE 或 SSE2 单元对浮点操作用信号通知一个错误情形。 20~31 这些值由 Intel 留作将来开发。如表 4-1 所示每个异常都由专门的异常处理程序来处理参见本章后面的异常处理一节它们通常把一个 Unix 信号发送到引起异常的进程。 4中断描述符表 可参考 ⇒ 5、中断描述符表 Linux 利用中断门处理中断利用陷阱门处理异常。 5中断和异常的硬件处理 我们现在描述 CPU 控制单元如何处理中断和异常。我们假定内核已被初始化因此CPU 在保护模式下运行。   当执行了一条指令后cs 和 eip 这对寄存器包含下一条将要执行的指令的逻辑地址。在处理那条指令之前控制单元会检查在运行前一条指令时是否已经发生了一个中断或异常。如果发生了一个中断或异常那么控制单元执行下列操作 确定与中断或异常关联的向量 i0 i 255。 读由 idtr 寄存器指向的 IDT 表中的第 i 项在下面的描述中我们假定 IDT 表项中包含的是一个中断门或一个陷阱门。 从 gdtr 寄存器获得 GDT 的基地址 并在 GDT 中查找以读取 IDT 表项中的选择符所标识的段描述符。这个描述符指定中断或异常处理程序所在段的基地址。 确信中断是由授权的中断发生源发出的。首先将当前特权级 CPL存放在 cs 寄存器的低两位与段描述符存放在 GDT 中的描述符特权级 DPL 比较如果 CPL 小于 DPL就产生一个 “General protection” 异常因为中断处理程序的特权不能低于引起中断的程序的特权。对于编程异常则做进一步的安全检查比较 CPL 与处于 IDT 中的门描述符的 DPL如果 DPL 小于 CPL 就产生一个 “General protection” 异常。这最后一个检查可以避免用户应用程序访问特殊的陷阱门或中断门。 检查是否发生了特权级的变化也就是说CPL 是否不同于所选择的段描述符的 DPL。如果是控制单元必须开始使用与新的特权级相关的栈。通过执行以下步骤来做到这点 读 tr 寄存器以访问运行进程的 TSS 段。用与新特权级相关的栈段和栈指针的正确值装载 ss 和 esp 寄存器。这些值可以在 TSS 中找到参见第三章的任务状态段一节在新的栈中保存 ss 和 esp 以前的值这些值定义了与旧特权级相关的栈的逻辑地址。 如果故障已发生用引起异常的指令地址装载 cs 和 eip 寄存器从而使得这条指令能再次被执行。 在栈中保存 eflags、cs 及 eip 的内容。 如果异常产生了一个硬件出错码则将它保存在栈中。 装载 cs 和 eip 寄存器其值分别是 IDT 表中第 i 项门描述符的段选择符和偏移量字段。这些值给出了中断或者异常处理程序的第一条指令的逻辑地址。 控制单元所执行的最后一步就是跳转到中断或者异常处理程序。换句话说处理完中断信号后控制单元所执行的指令就是被选中处理程序的第一条指令。 中断或异常被处理完后相应的处理程序必须产生一条 iret 指令把控制权转交给被中断的进程这将迫使控制单元 用保存在栈中的值装载 cs、eip 或 eflags 寄存器。如果一个硬件出错码曾被压入栈中并且在 eip 内容的上面那么执行 iret 指令前必须先弹出这个硬件出错码。检查处理程序的 CPL 是否等于 cs 中最低两位的值这意味着被中断的进程与处理程序运行在同一特权级。如果是iret 终止执行否则转入下一步。从栈中装载 ss 和 esp 寄存器因此返回到与旧特权级相关的栈。检查 ds、es、fs 及 gs 段寄存器的内容如果其中一个寄存器包含的选择符是一个段描述符并且其 DPL 值小于 CPL那么清相应的段寄存器。控制单元这么做是为了禁止用户态的程序CPL3利用内核以前所用的段寄存器DPL0。如果不清这些寄存器怀有恶意的用户态程序就可能利用它们来访问内核地址空间。 2、中断和异常处理程序的嵌套执行 每个中断或异常都会引起一个内核控制路径或者说代表当前进程在内核态执行单独的指令序列。内核控制路径可以任意嵌套。一个中断处理程序可以被另一个中断处理程序 “中断” 。 允许内核控制路径嵌套执行必须付出代价那就是中断处理程序必须永不阻塞换句话说中断处理程序运行期间不能发生进程切换。 假定内核没有 bug那么大多数异常就只在 CPU 处于用户态时发生。事实上异常要么是由编程错误引起要么是由调试程序触发。然而“Page Fault缺页” 异常发生在内核态。这发生在当进程试图对属于其地址空间的页进行寻址而该页现在不在 RAM 中时。当处理这样的一个异常时内核可以挂起当前进程并用另一个进程代替它直到请求的页可以使用为止。只要被挂起的进程又获得处理器处理缺页异常的内核控制路径就恢复执行。   因为 “Page Fault” 异常处理程序从不进一步引起异常所以与异常相关的至多两个内核控制路径第一个由系统调用引起第二个由缺页引起会堆叠在一起一个在另一个之上。 与异常形成对照的是尽管处理中断的内核控制路径代表当前进程运行但由 I/O 设备产生的中断并不引用当前进程的专有数据结构。事实上当一个给定的中断发生时要预测哪个进程将会运行是不可能的。 一个中断处理程序既可以抢占其他的中断处理程序也可以抢占异常处理程序。相反异常处理程序从不抢占中断处理程序。在内核态能触发的唯一异常就是刚刚描述的缺页异常。但是中断处理程序从不执行可以导致缺页因此意味着进程切换的操作。 基于以下两个主要原因Linux 交错执行内核控制路径 为了提高可编程中断控制器和设备控制器的吞吐量。假定设备控制器在一条 IRQ 线上产生了一个信号PIC 把这个信号转换成一个外部中断然后 PIC 和设备控制器保持阻塞一直到 PIC 从 CPU 处接收到一条应答信息。由于内核控制路径的交错执行内核即使正在处理前一个中断也能发送应答。 为了实现一种没有优先级的中断模型。因为每个中断处理程序都可以被另一个中断处理程序延缓因此在硬件设备之间没必要建立预定义优先级。这就简化了内核代码提高了内核的可移植性。 在多处理器系统上几个内核控制路径可以并发执行。此外与异常相关的内核控制路径可以开始在一个 CPU 上执行并且由于进程切换而移往另一个 CPU 上执行。 1中断门、陷阱门及系统门 与在前面 “中断描述符表” 中所提到的一样Intel 提供了三种类型的中断描述符任务门、中断门及陷阱门描述符。Linux 使用与 Intel 稍有不同的细目分类和术语把它们如下进行分类 中断门 interrupt gate 用户态的进程不能访问的一个 Intel 中断门门的 DPL 字段为 0。所有的 Linux 中断处理程序都通过中断门激活并全部限制在内核态。 系统门 system gate 用户态的进程可以访问的一个 Intel 陷阱门门的 DPL 字段为 3。通过系统门来激活三个 Linux 异常处理程序它们的向量是 45 及 128因此在用户态下可以发布 into、bound 及 int $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” 异常该异常推迟给进程分配新的页框直到不能再推迟为止。相应的处理程序比较复杂因为异常可能表示一个错误条件也可能不表示一个错误条件参见第九章 “缺页异常处理程序” 一节。 异常处理程序有一个标准的结构由以下三部分组成 在内核堆栈中保存大多数寄存器的内容这部分用汇编语言实现。用高级的 C 函数处理异常。通过 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 (I8) (S16) (A24))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_INFOEnabling 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 发出一个中断。我们在后面 “处理器间中断处理” 一节将讨论这种中断。 1I/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 中断处理程序都执行四个相同的基本操作 在内核态堆栈中保存 IRQ 的值和寄存器的内容。为正在给 IRQ 线服务的 PIC 发送一个应答、这将允许 PIC 进一步发出中断。执行共享这个 IRQ 的所有设备的中断服务例程ISR。跳到 ret_from_intr() 的地址后终止。 当中断发生时需要用几个描述符来表示 IRQ 线的状态和需要执行的通数。图 4-4 以示意图的方式展示了处理一个中断的硬件电路和软件函数。下面几节会讨论这些函数。 a中断向量 bIRQ 数据结构 每个中断向量都有它自己的 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 前缀的所有指令参见 “原子操作” 一节。写控制寄存器、系统寄存器或调试寄存器的所有指令例如cli 和 sti用于修改 eflags 寄存器的 IF 标志的状态。在 Pentium 4 微处理器中引入的汇编语言指令 lfencesfence 和 mfence它们分别有效地实现读内存屏障、写内存屏障和读-写内存屏障。少数专门的汇编语言指令终止中断处理程序或异常处理程序的 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) #endifasm 指令告诉编译器插入一些汇编语言指令并起优化屏障的作用。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 作为它的参数并执行下面的操作 调用 preempt_disable() 以禁用内核抢占。调用函数 _raw_spin_trylock()它对自旋锁的 slock 字段执行原子性的测试和设置操作。该函数首先执行等价于下列汇编语言片段的一些指令 movb $0, %al xchgb %al, slp-slock汇编语言指令 xchg 原子性地交换 8 位寄存器 %al存 0和 slp-slock 指示的内存单元的内容。随后如果存放在自旋锁中的旧值在 xchg 指令执行之后存放在 %al 中是正数函数就返回 1否则返回 0。   注 2 具有识刺意味的是自旋锁是全局的、因此对它本身必须进行保护以防止并发访问。 如果自旋锁中的旧值是正数宏结束内核控制路径已经获得自旋锁。否则内核控制路径无法获得自旋锁因此宏必须执行循环一直到在其他 CPU 上运行的内核控制路径释放自旋锁。调用 preempt_enable() 递减在第 1 步递增了的抢占计数器。如果在执行 spin_lock 宏之前内核抢占被启用那么其他进程此时可以取代等待自旋锁的进程。如果 break_lock 字段等于 0则把它设置为 1。通过检测该字段拥有锁并在其他 CPU 上运行的进程可以知道是否有其他进程在等待这个锁。如果进程把持某个自旋锁的时间太长它可以提前释放锁以使等待相同自旋锁的进程能够继续向前运行。执行等待循环 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 处开始重新执行因为不检查其他的处理器是否抢占了锁就继续执行是不安全的。 cspin_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_lock 或 write_lock 操作的请求具有相同的优先权读者必须等待直到写操作完成。同样地写者也必须等待直到读操作完成。 Linux 2.6 中引入了顺序锁seqlock它与读 / 写自旋锁非常相似只是它为写者赋予了较高的优先级事实上即使在读者正在读的时候也允许写者继续运行。这种策略的好处是写者永远不会等待除非另外一个写者正在写缺点是有些时候读者不得不反复多次读相同的数据直到它获得有效的副本。 每个顺序锁都是包括两个字段的 seqlock_t 结构一个类型为 spinlock_t 的 lock 字段和一个整型的 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 的范围如下所述 RCU 只保护被动态分配并通过指针引用的数据结构。在被 RCU 保护的临界区中任何内核控制路径都不能睡眠。 当内核控制路径要读取被 RCU 保护的数据结构时执行宏 rcu_read_lock()它等同于 preempt_disable() 。接下来读者间接引用该数据结构指针所对应的内存单元并开始读这个数据结构。正如在前面所强调的读者在完成对数据结构的读操作之前是不能睡眠的。用等同于 preempt_enable() 的宏 rcu_read_unlock() 标记临界区的结束。 我们可以想象由于读者几乎不做任何事情来防止竞争条件的出现所以写者不得不做得更多一些。事实上当写者要更新数据结构时它间接引用指针并生成整个数据结构的副本。接下来写者修改这个副本。一旦修改完毕写者改变指向数据结构的指针以使它指向被修改后的副本。由于修改指针值的操作是一个原子操作所以旧副本和新副本对每个读者或写者都是可见的在数据结构中不会出现数据奔溃。尽管如此还需要内存屏障来保证只有在数据结构被修改之后已更新的指针对其他 CPU 才是可见的。如果把自旋锁与 RCU 结合起来以禁止写者的并发执行就隐含地引入了这样的内存屏障。 然而使用 RCU 技术的真正困难在于写者修改指针时不能立即释放数据结构的旧副本。实际上写者开始修改时正在访问数据结构的读者可能还在读旧副本。只有在 CPU 上的所有潜在的读者都执行完宏 rcu_read_unlock() 之后才可以释放旧副本。内核要求每个潜在的读者在下面的操作之前执行 rcu_read_unlock() 宏 CPU 执行进程切换参见前面的约束条件 2CPU 开始在用户态执行CPU 执行空循环参见第三章内核线程一节 对上述每种情况我们说 CPU 已经经过了静止状态quiescent state。写者调用函数 call_rcu() 来释放数据结构的旧副本。当所有的 CPU 都通过静止状态之后call_rcu() 接受 rcu_head 描述符通常嵌在要被释放的数据结构中的地址和将要调用的回调函数的地址作为参数。一旦回调函数被执行它通常释放数据结构的旧副本。 函数 call_rcu() 把回调函数和其参数的地址存放在 rcu_head 描述符中然后把描述符插入回调函数的每 CPUper-CPU链表中。内核每经过一个时钟滴答参见第六章 “更新本地 CPU 统计数” 一节就周期性地检查本地 CPU 是否经过了一个静止状态。如果所有CPU 都经过了静止状态本地 tasklet它的描述符存放在每 CPU 变量 rcu_tasklet 中就执行链表中的所有回调函数。   RCU 是 Linux 2.6 中新加的功能用在网络层和虚拟文件系统中。 7信号量 Linux 提供两种信号量 内核信号量由内核控制路径使用System V IPC 信号量由用户态进程使用 可参考 4、信号量
http://www.dnsts.com.cn/news/177840.html

相关文章:

  • 临海网站建设个人网站设计结构图
  • 石家庄网站公司网站用图片做背景图片
  • 企业网站建设调研报告网页网站设计公司排行榜
  • 福建省住房和城乡建设厅门户网站好玩的网页
  • 建设银行海外分行招聘网站哪个做问卷网站佣金高
  • 网站开发所需的费用网站建设开票税收分类
  • 昆明网站制作工具wordpress 3.1 下载地址
  • 杭州seo网站推广软件信息作业网站下载
  • 做网站想要中立信息网站设计方案
  • 网站百度不到北京景观设计公司10强
  • 专门做折扣的网站有哪些openshift安装wordpress密码忘记
  • 哪里有做网站的教程网站seo方案策划书
  • 茶叶网站建设网站怎么做关键词在哪做
  • 网站建设丿金手指排名9萝岗手机网站建设
  • 中小企业网站查询商品热搜词排行榜
  • 建立网站需要哪些步骤游戏网站开发设计报告
  • seo更新网站内容的注意事项红酒网站建设
  • 软件网站建设基本流程邹带芽在成武建设局网站
  • 长沙水业网站是哪家公司做的小超人成都网站建设
  • 不花钱的网站建设wordpress 主题 微信
  • 镇江做网站多少钱ps网页设计步骤
  • 做宣传 为什么要做网站那企业网站建设主要考虑哪些
  • 古典网站建设公司网页前端设计包括哪些内容
  • 网站制作找私人多少钱wordpress火车头但存图片
  • 毕节公司做网站长春网站设计策划书
  • 中山 网站建设一条龙qinmei wordpress
  • 能看人与动物做的网站制作网站的公司注册资本要多少
  • 做同城网站需要哪些jsp网站开发实例精讲
  • 大型网站和小企业站优化思路小公司网站用什么服务器划算
  • 外贸国际网站推广昆明网站制作工具