深入理解 Linux 内核3

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

深入理解 Linux 内核2:https://developer.aliyun.com/article/1597358

四、中断和异常

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

当内核正打算去完成一些别的事情时,中断随时会到来。因此,内核的目标就是让中断尽可能快地处理完,尽其所能把更多的处理向后推迟。例如,假设一个数据块已到达了网线,当硬件中断内核时,内核只简单地标志数据到来了,让处理器恢复到它以前运行的状态,其余的处理稍后再进行(如把数据移入一个缓冲区,它的接收进程可以在缓冲区找到数据并恢复这个进程的执行)。因此,内核响应中断后需要进行的操作分为两部分:关键而紧急的部分,内核立即执行,其余推迟的部分,内核随后执行。

因为中断随时会到来,所以内核可能正在处理其中的一个中断时,另一个中断(不同类型)又发生了。应该尽可能多地允许这种情况发生,因为这能维持更多的 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)。非屏蔽中断的向量和异常的向量是固定的,而可屏蔽中断的向量可以通过对中断控制器的编程来改变(参见下一节)。

(1)IRQ 和中断

 每个能够发出中断请求的硬件设备控制器都有一条名为 IRQ(Interrupt 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 的缺省向量是 n+32。如前所述,通过向中断控制器端口发布合适的指令,就可以修改 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 Controller,I/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,因此,相对于本地 APIC,I/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 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 被禁止,两条 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 这对寄存器包含下一条将要执行的指令的逻辑地址。在处理那条指令之前,控制单元会检查在运行前一条指令时是否已经发生了一个中断或异常。如果发生了一个中断或异常,那么控制单元执行下列操作:


确定与中断或异常关联的向量 i(0 < 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,那么,清相应的段寄存器。控制单元这么做是为了禁止用户态的程序(CPL=3)利用内核以前所用的段寄存器(DPL=0)。如果不清这些寄存器,怀有恶意的用户态程序就可能利用它们来访问内核地址空间。

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 异常处理程序,它们的向量是 4,5 及 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 个表项插入一个中断门。门中的段选择符设置成内核代码的段选择符,偏移量设置为中断处理程序的地址 addr,DPL 字段设置为 0。

set_system_gate(n,addr)

在 IDT 的第 n 个表项插入一个陷阱门。门中的段选择符设置成内核代码的段选择符,偏移量设置为异常处理程序的地址 addr,DPL 字段设置为 3。

set_system_intr_gate(n,addr)

在 IDT 的第 n 个表项插入一个中断门。门中的段选择符设置成内核代码的段选择符,偏移量设置为异常处理程序的地址 addr,DPL 字段设置为 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_EISA
  void __iomem *p = early_ioremap(0x0FFFD9, 4);

  if (readl(p) == 'E' + ('I'<<8) + ('S'<<16) + ('A'<<24))
    EISA_bus = 1;
  early_iounmap(p, 4);
#endif

  set_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_32
  set_task_gate(8, GDT_ENTRY_DOUBLEFAULT_TSS);
#else
  set_intr_gate_ist(8, &double_fault, DOUBLEFAULT_STACK);
#endif
  set_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_MCE
  set_intr_gate_ist(18, &machine_check, MCE_STACK);
#endif
  set_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_EMULATION
  set_system_intr_gate(IA32_SYSCALL_VECTOR, ia32_syscall);
  set_bit(IA32_SYSCALL_VECTOR, used_vectors);
#endif

#ifdef CONFIG_X86_32
  if (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 中断处理程序都执行四个相同的基本操作:


在内核态堆栈中保存 IRQ 的值和寄存器的内容。

为正在给 IRQ 线服务的 PIC 发送一个应答、这将允许 PIC 进一步发出中断。

执行共享这个 IRQ 的所有设备的中断服务例程(ISR)。

跳到 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_REMAP
  struct irq_2_iommu      *irq_2_iommu;
#endif
  irq_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_SMP
  cpumask_var_t   affinity;
  unsigned int    node;
#ifdef CONFIG_GENERIC_PENDING_IRQ
  cpumask_var_t   pending_mask;
#endif
#endif
  atomic_t    threads_active;
  wait_queue_head_t       wait_for_threads;
#ifdef CONFIG_PROC_FS
  struct proc_dir_entry *dir;
#endif
  const 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 微处理器中引入的汇编语言指令 lfence,sfence 和 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")
#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_LOCKBREAK
  unsigned int break_lock;
#endif
#ifdef CONFIG_DEBUG_SPINLOCK
  unsigned int magic, owner_cpu;
  void *owner;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
  struct 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() 以禁用内核抢占。
  1. 调用函数 _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->slock
  jns 3f
2:  pause
  cmpb $0, slp->slock
  jle 2b
  jmp 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_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 执行进程切换(参见前面的约束条件 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 中)就执行链表中的所有回调函数。

 RCU 是 Linux 2.6 中新加的功能,用在网络层和虚拟文件系统中。

(7)信号量

  Linux 提供两种信号量:

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

可参考 ==> 4、信号量

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