Linux 克制机制(一)之克制和非常

[复制链接]
发表于 2026-2-9 11:44:43 | 显示全部楼层 |阅读模式

一、什么是克制

1、概述

克制(interrupt)是指在 CPU 正常运行期间, 由外部或内部变乱引起的一种机制。 当克制发生时,CPU 会克制当前正在实行的步伐,并转而实行触发该克制的克制处置惩罚步伐。处置惩罚完克制处置惩罚步伐后,CPU 会返回到克制发生的地方, 继承实行被克制的步伐。克制机制答应 CPU 在及时相应外部或内部变乱的同时,保持对其他使命的处置惩罚本领。
克制的流程图如下:

2、克制的分类

克制通常被界说为一个变乱,该变乱改变处置惩罚器实行的指令序次。如许的变乱与 CPU 芯片表里部硬件电路产生的电信号相对应。
克制通常分为同步(synchronous)克制和异步(asynchronous)克制:


  • 同步克制是当指令实行时由 CPU 控制单位产生的,之以是称为同步,是由于只有在一条指令克制实行后CPU才会发出克制;
  • 异步克制是由其他硬件装备依照 CPU 时钟信号随机产生的。
   在 Intel 微处置惩罚器手册中,把同步和异步克制分别称为非常(exception)和克制(interrupt)我们也接纳这种分类,固然偶尔我们也用术语“克制信号”指这两种范例(同步及异步)。
  二、克制和非常

1、克制和非常

Intel 文档把克制和非常分为以下几类:


  • 克制

    • 可屏蔽克制(maskabie interrupt
      I/O 装备发出的全部克制哀求(IRQ)都产生可屏蔽克制。可屏蔽克制可以处于两种状态:屏蔽的(masked)或非屏蔽的(unmasked),一个屏蔽的克制只要还是屏蔽的,控制单位就忽略它。要根据克制答应标记的设置来判定 CPU 是否能相应克制哀求。
    • 非屏蔽克制(nonmakable interrupt
      只有几个危急变乱(如硬件故障)才引起非屏蔽克制。非屏蔽克制总是由 CPU 辨认。不受克制答应标记的影响,不能用软件举行屏蔽。
      可屏蔽的克制可以被壅闭,使用 x86_64 的指令 sti 和 cli。这两个指令修改了在克制寄存器中的 IF 标识位。 sti 指令设置 IF 标识,cli 指令扫除这个标识。不可屏蔽的克制总是被陈诉。通常,任何硬件上的失败都映射为不可屏蔽克制。我们可以在 Linux 内核代码中找到这两个指令的使用:

  1. static inline void native_irq_disable(void)
  2. {
  3.         asm volatile("cli": : :"memory");
  4. }
  5. static inline void native_irq_enable(void)
  6. {
  7.         asm volatile("sti": : :"memory");
  8. }
复制代码


  • 非常

    • 处置惩罚器探测非常(processor-detected exception
      当 CPU 实行指令时探测到的一个反常条件所产生的非常。可以进一步分为三组,这取决于 CPU 控制单位产生非常时生存在内核态堆栈 eip 寄存器中的值。

      • 故障(fault
        通常可以改正:一旦改正,步伐就可以在不失连贯性的环境下重新开始。生存在 eip 中的值是引起故障的指令地点。因此,当非常处置惩罚步伐克制时,那条指令会被重新实行。
      • 陷阱(trap
        在陷阱指令实行后立刻陈诉;内核把控制权返回给步伐后就可以继承它的实行而不失连贯性。生存在 eip 中的值是一个随后要实行的指令地点。只有当没有须要重新实行已克制的指令时,才触发陷阱。陷阱的紧张用途是为了调试步伐。在这种环境下,克制信号的作用是关照调试步伐一条特别指令已被实行(比方到了一个步伐内的断点)。一旦用户查抄到调试步伐所提供的数据,它就大概要求被调试步伐从下一条指令重新开始实行。
      • 非常克制(abort
        发生一个严峻的错误:控制单位出了题目,不能在 eip 寄存器中生存引起非常的指令地点简直切位置。非常克制用于陈诉严峻的错误,如硬件故障或体系表中无效的值或差别等的值。由控制单位发送的这个克制信号是告急信号,用来把控制权切换到相应的非常克制处置惩罚步伐,这个非常克制处置惩罚步伐除了逼迫受影响的历程克制外,没有别的选择。

    • 编程非常(programmed exception
      在编程者发出哀求时发生。是由 int 或 int3 指令触发的,当 into(查抄溢出)和 bound(查抄地点出界)指令查抄的条件不为真时,也引起编程非常。控制单位把编程非常作为陷阱来处置惩罚。编程非常通常也叫做软克制sofware interrupt)如许的非常有两种常用的用途:实行体系调用及给调试步伐转达一个特定的变乱。

每个克制和非常是由 0~255 之间的一个数来标识。由于一些未知的缘故原由,Intel 把这个 8 位的无符号整数叫做一个向量(vector)。非屏蔽克制的向量和非常的向量是固定的,而可屏蔽克制的向量可以通过对克制控制器的编程来改变。
克制和非常的区别:克制是由硬件引起的;非常则发生在编程失误而导致错误指令,大概在实行期间出现特别环境必须要靠内核来处置惩罚的时间(比如缺页)。
2、克制的上下部

克制的实行须要快速相应, 但并不是全部克制都能敏捷完成。 别的, Linux 中的克制不支持嵌套, 意味着在正式处置惩罚克制之前会屏蔽其他克制, 直到克制处置惩罚完成后再重新答应吸收克制,如果克制处置惩罚时间过长, 将会引发题目。
这里以炒菜的过程中接电话举行举例:当你正在炒菜的时间,菜正在锅里翻炒着。 突然, 你的手机响起,突破了你正常的炒菜流程,接电话的时间很短并不会对炒菜产生很大的影响, 而接电话的时间大概就有题目了,由于菜大概会由于没来得及翻面而炒糊了。
为了让体系可以更好地处置惩罚克制变乱, 进步及时性和相应本领, 将克制服务步伐分别为上下文两部门:


  • 上半部:上半部是克制处置惩罚函数的一部门,它紧张处置惩罚一些告急且须要快速相应的使命。 克制上文的特点是实行时间较短,旨在尽快完成对克制的处置惩罚。这些使命大概包罗生存寄存器状态、更新计数器等, 以便在克制处置惩罚完成后可以或许准确地返回到克制前的实行位置。
    上半部的实行是在克制上下文中举行的,它运行在克制服务例程(ISR)地点的内核线程上下文中,而不是用户历程的上下文中。因此,上半部的实行是在克制被触发时立刻实行的,不会被其他克制打断。
  • 下半部是克制处置惩罚函数的另一部门,它相对于上半部来说是耽误实行的。下半部的目的是在克制被触发后,尽快将一些不告急大概耗时的处置惩罚工作延后实行,以减轻上半部的负担,从而使克制处置惩罚更加高效。
    下半部的实行是在非克制上下文中举行的,它不会被其他克制打断,而且可以访问用户空间的内存。下半部的实行可以在恣意时间举行,但是须要留意的是,下半部实行的时间越长,会导致克制耽误更长,从而影响体系的相应性能。下半部一样寻常包罗以下几种情势:

    • 内核线程:创建一个新的内核线程来实行一些独立于克制的使命。
    • 使命队列:将须要实行的使命放入使命队列中,由内核调理器来选择得当的时机实行。
    • 工作队列:类似于使命队列,但是工作队列可以绑定到某个 CPU,以进步处置惩罚服从。

3、非常

80x86 微处置惩罚器发布了约莫 20 种差别的非常(依靠于体系布局)。内核必须为每种常提供一个专门的非常处置惩罚步伐。对于某些非常,CPU 控制单位在开始实行非常处置惩罚步伐前会产生一个硬件堕落码(hardware error code),而且压入内核态堆栈。
下面的列表给出了在 80x86 处置惩罚器中可以找到的非常的向量、名字、范例及其简朴形貌。更多的信息可以在 Intel 的技能文挡中找到。


  • 0:“Divide error”(故障)
    当一个步伐试图实行整数被 0 除操纵时产生。
  • 1:“Debug”(陷阱或故障)
    产生于:

    • 设置 eflags 的 TF 标记时(对于实现调试步伐的单步实行是相当有用的);
    • 一条指令或操纵数的地点落在一个运动 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 指令被实行。
  • 8:“Double fault”(非常克制)
    正常环境下,当 CPU 正试图为前一个非常调用处置惩罚步伐时,同时又检测到一个非常,两个非常能被串行地处置惩罚。然而,在少数环境下,处置惩罚器不能串行地处置惩罚它们因而产生这种非常。
  • 9:“Coprocessor segment overrun”(非常克制)
    因外部的数学协处置惩罚器引起的题目(仅用于 80386 微处置惩罚器)。
  • 10:“Invalid TSS”(故障)
    CPU 试图让一个上下文切换到有无效的 TSS 的历程。
  • 11:“Segment not present”(故障)
    引用一个不存在的内存段(段形貌符的 Segment-Present 标记被清0)。
  • 12:“Stack segment fault”(故障)
    试图凌驾栈段界限的指令,大概由 ss 标识的段不在内存
  • 13:“General protection”(故障)
    违反了 80x86 掩护模式下的掩护规则之一。
  • 14:“Page fault”(故障)
    寻址的页不在内存,相应的页表项为空,大概违反了一种分页掩护机制。
  • 15:由 Intel 生存
  • 16:“Floating point error”(故障)
    集成到 CPU 芯片中的浮点单位用信号关照一个错误环境,如数字溢出,或被 0 除。
  • 17:“Alignment check”(故障)
    操纵数的地点没有被准确地对齐(比方,一个长整数的地点不是 4 的倍数)。
  • 18:“Machine check”(非常克制)
    呆板查抄机制检测到一个 CPU 错误或总线错误。
  • 19:“SIMD floating point exception"(故障)
    集成到 CPU 芯片中的 SSE 或 SSE2 单位对浮点操纵用信号关照一个错误环境。
20~31 这些值由 Intel 留作将来开发。如下表所示,每个非常都由专门的非常处置惩罚步伐来处置惩罚,它们通常把一个 Unix 信号发送到引起非常的历程。
编号非常非常处置惩罚步伐信号0Divide errordivide error()SIGFPE1Debugdebug()SIGTRAP2NMInmi()None3Breakpointint3()SIGTRAP4Overflowoverflow()SIGSEGV5Bounds checkbounds()SIGSEGV6Invalid opcodeinvalid_op()SIGILL7Device not availabledevice_not_available()None8Double faultdoublefault_fn()None9coprocessor segment overruncoprocessor_segment_overrun()SIGFPE10Invalid TSSinvalid_tss()SIGSEGV11Segment not presentsegment_not_present()SIGBUS12Stack exceptionstack_segment()SIGBUS13General protectiongeneral_protection()SIGSEGV14Page faultpage_fault()SIGSEGV15Intel reservedNoneNone16Floating point errorcoprocessor_error()SIGFPE17Alignment checkalignment_check()SIGSEGV18Machine checkmachine_check()None19SIMD floating pointsimd_coprocessor_error()SIGFPE4、APIC

前面已经讲了什么是克制,那么克制信号是怎么处置惩罚的呢?比如,当我们在键盘上按下一个键的时间,我们下一步渴望做什么?操纵体系和电脑应该怎么做?做一个简朴的假设,每一个物理硬件都有一根毗连 CPU 的克制线,装备可以通过它对 CPU 发起克制信号。但是克制信号并不是直接发送给 CPU。在老呆板上克制信号发送给 PIC ,它是一个序次处置惩罚各种装备的各种克制哀求的芯片。在新呆板上,则是高级步伐克制控制器(Advanced Programmable Interrupt ControllerAPIC)做这件事变。一个 APIC 包罗两个独立的装备:


  • Local APIC:在于每个 CPU 焦点中,Local APIC 负责处置惩罚特定于 CPU 的克制设置。Local APIC 常被用于管理来自 APIC 时钟(APIC-timer)、热敏元件和其他与 I/O 装备毗连的装备的克制。
  • I/O APIC:提供了多核处置惩罚器的克制管理。它被用来在全部的 CPU 焦点中分发外部克制。
下图表现了一个多 APIC 体系的布局。一条 APIC 总线把“前端” I/O APIC 毗连到当地 APIC。来自装备的 IRQ 线毗连到 I/O APIC,因此,相对于当地 APIC,I/O APIC 起路由器的作用。在 Pentium III 和早期处置惩罚器的母板上,APIC 总线是一个串行三线总线;从 Pentium 4 开始,APIC 总线通过体系总线来实现。不外,由于 APIC 总线及其信息对软件是不可见的,因此,我们不做进一步的具体讨论。

5、克制形貌符表

克制可以在任何时间发生,当一个克制发生时,操纵体系必须确保下面的步调序次:

  • 内核必须停息实行当前历程(代替当前的使命);
  • 内核必须搜刮克制处置惩罚步伐而且转交控制权(实行克制处置惩罚步伐);
  • 克制处置惩罚步伐竣事之后,被克制的历程可以或许规复实行。
每个克制处置惩罚步伐的地点都生存在一个特别的位置,这个位置被称为克制形貌符表Interrupt Descriptor TableIDT)。处置惩罚器使用一个唯一的数字来辨认克制和非常的范例,这个数字被称为克制标识码vector number)。一个克制标识码就是一个 IDT 的标识。克制标识码范围是有限的,从 0 到 255。你可以在 Linux 内核源码中找到下面的克制标识码范围查抄代码
  1. BUG_ON((unsigned)n > 0xFF);
复制代码
在 Linux 内存管理(二)之GDT与LDT 一文中,我们讲到了 GDT 和 LDT,IDT 的格式与这两种表的格式非常相似,表中的每一项对应一个克制或非常向量,每个向量由 8 个字节构成。因此,最多须要                               256                      ∗                      8                      =                      2048                          256*8=2048               256∗8=2048 字节来存放 IDT。
idtr CPU寄存器使 IDT 可以位于内存的任何地方,它指定 IDT 的线性基地点及其限定(最大长度)。在答应克制之前,必须用 lidt 汇编指令初始化 idtr。
IDT 包罗三种范例的形貌符,下图表现了每种形貌符中的 64 位的寄义。尤其值得留意的是,在 40~43 位的 Type 字段的值表现形貌符的范例。



  • 使命门(task gate
    当克制信号发生时,必须代替当前历程的谁人历程的 TSS 选择符存放在使命门中。
  • 克制门(interrupt gate
    包罗段选择符和克制或非常处置惩罚步伐的段内偏移量。当控制权转移到一个得当的段时,处置惩罚器清 IF 标记,从而关闭将来会发生的可屏蔽克制。
  • 陷阱门(Trap gate
    与克制门相似,只是控制权转到达一个得当的段时处置惩罚器不修改 IF 标记。
三、软件实现

克制形貌符表 使用 gate_desc 的数组形貌:
  1. extern gate_desc idt_table[];
复制代码
gate_desc 界说如下:
  1. #ifdef CONFIG_X86_64
  2.         ...
  3.         typedef struct gate_struct64 gate_desc;
  4.         ...
  5. #endif
复制代码
gate_struct64 界说如下:
  1. struct gate_struct64 {
  2.         u16 offset_low;
  3.     u16 segment;
  4.     unsigned ist : 3, zero0 : 5, type : 5, dpl : 2, p : 1;
  5.     u16 offset_middle;
  6.     u32 offset_high;
  7.     u32 zero1;
  8. } __attribute__((packed));
复制代码
在 x86_64 架构中,每一个运动的线程在 Linux 内核中都有一个很大的栈。这个栈的巨细由 THREAD_SIZE 界说,而且与下面的界说相当:
  1. #define PAGE_SHIFT      12
  2. #define PAGE_SIZE       (_AC(1,UL) << PAGE_SHIFT)
  3. ...
  4. #define THREAD_SIZE_ORDER       (2 + KASAN_STACK_ORDER)
  5. #define THREAD_SIZE  (PAGE_SIZE << THREAD_SIZE_ORDER)
复制代码
此中,PAGE_SIZE 是 4096 字节,THREAD_SIZE_ORDER 的值依靠于 KASAN_STACK_ORDER。就像我们看到的,KASAN_STACK 依靠于 CONFIG_KASAN 内核设置参数,它界说如下:
  1. #ifdef CONFIG_KASAN
  2.     #define KASAN_STACK_ORDER 1
  3. #else
  4.     #define KASAN_STACK_ORDER 0
  5. #endif
复制代码
KASan 是一个运行时内存调试器。以是:


  • 如果 CONFIG_KASAN 被禁用,THREAD_SIZE 是 16384;
  • 如果内核设置选项打开,THREAD_SIZE 的值是 32768。
这块栈空间生存着有用的数据,只要线程是运动状态大概僵尸状态。但是当线程在用户空间的时间,这个内核栈是空的,除非 thread_info 布局在这个栈空间的底部。运动的大概僵尸线程并不是在他们栈中的唯一的线程,与每一个 CPU 关联的特别栈也存在于这个空间。当内核在这个 CPU 上实行代码的时间,这些栈处于运动状态;当在这个 CPU 上实行用户空间代码时,这些栈不包罗任何有用的信息。每一个 CPU 也有一个特别的 per-cpu 栈。起首是给外部克制使用的 克制栈(interrupt stack)。它的巨细界说如下:
  1. #define IRQ_STACK_ORDER (2 + KASAN_STACK_ORDER)
  2. #define IRQ_STACK_SIZE (PAGE_SIZE << IRQ_STACK_ORDER)
复制代码
大概是 16384 字节。Per-cpu 的克制栈在 x86_64 架构中使用 irq_stack_union 团结形貌:
  1. union irq_stack_union {
  2.     char irq_stack[IRQ_STACK_SIZE];
  3.     struct {
  4.         char gs_base[40];
  5.         unsigned long stack_canary;
  6.     };
  7. };
复制代码
第一个 irq_stack 域是一个 16KB 的数组。然后你可以看到 irq_stack_union 团结包罗了一个布局体,这个布局体有两个域:


  • gs_base:总是指向 irqstack 团结底部的 gs 寄存器。在 x86_64 中, per-cpu 和 stack canary 共享 gs 寄存器。全部的 per-cpu 标记初始值为零,而且 gs 指向 per-cpu 地区的开始。
  • stack_canary:stack canary 对于克制栈来说是一个用来验证栈是否已经被修改的 栈掩护者stack protector)。gs_base 是一个 40 字节的数组,GCC 要求 stack canary 在被修正过的偏移量上,而且 gs 的值在 x86_64 架构上必须是 40,在 x86 架构上必须是 20。
下面来看 irq_stack_union 的初始化过程。除了 irq_stack_union 的界说,我们可以在arch/x86/include/asm/processor.h 中查察下面的 per-cpu 变量:
  1. DECLARE_PER_CPU(char *, irq_stack_ptr);
  2. DECLARE_PER_CPU(unsigned int, irq_count);
复制代码
第一个参数 irq_stack_ptr,它是一个指向这个栈顶的指针。第二个参数 irq_count 用来查抄 CPU 是否已经在克制栈。irq_stack_ptr 的初始化在 arch/x86/kernel/setup_percpu.c 的 setup_per_cpu_areas 函数中:
  1. void __init setup_per_cpu_areas(void)
  2. {
  3. ...
  4. ...
  5. #ifdef CONFIG_X86_64
  6. for_each_possible_cpu(cpu) {
  7.     ...
  8.     per_cpu(irq_stack_ptr, cpu) =
  9.             per_cpu(irq_stack_union.irq_stack, cpu) +
  10.             IRQ_STACK_SIZE - 64;
  11.     ...
  12. #endif
  13. ...
  14. }
复制代码
在这个函数里,我们一个一个查察全部 CPU,而且设置 irq_stack_ptr,它便是克制栈的顶减去 64。为什么是 64?见文件 arch/x86/kernel/cpu/common.c 代码如下:
  1. void load_percpu_segment(int cpu)
  2. {
  3.         ...
  4.     loadsegment(gs, 0);
  5.         wrmsrl(MSR_GS_BASE, (unsigned long)per_cpu(irq_stack_union.gs_base, cpu));
  6. }
复制代码
此中 gs 寄存器指向克制栈的栈底:
  1. movl    $MSR_GS_BASE,%ecx
  2. movl    initial_gs(%rip),%eax
  3. movl    initial_gs+4(%rip),%edx
  4. wrmsr
  5. GLOBAL(initial_gs)
  6. .quad    INIT_PER_CPU_VAR(irq_stack_union)
复制代码
此中 wrmsr 指令从 edx:eax 加载数据到被 ecx 指向的 MSR 寄存器)。在这里 MSR 寄存器是 MSR_GS_BASE,它生存了被 gs 寄存器指向的内存段的基址。edx:eax 指向 initial_gs 的地点,它就是 irq_stack_union 的基址。
我们还知道,x86_64 有一个叫 克制栈表Interrupt Stack TableIST)的组件,当发生不可屏蔽克制、双重错误等等的时间,这个组件提供了切换到新栈的功能。这可以到达 7 个 IST per-cpu 入口。此中一些界说如下:
  1. #define DOUBLEFAULT_STACK 1
  2. #define NMI_STACK 2
  3. #define DEBUG_STACK 3
  4. #define MCE_STACK 4
复制代码
全部被 IST 切换到新栈的克制门形貌符都由 set_intr_gate_ist 函数初始化。比方:
  1. set_intr_gate_ist(X86_TRAP_NMI, &nmi, NMI_STACK);
  2. ...
  3. set_intr_gate_ist(X86_TRAP_DF, &double_fault, DOUBLEFAULT_STACK);
复制代码
此中 &nmi 和 &double_fault 界说在 arch/x86/kernel/entry_64.S 中,是克制函数的入口地点:
  1. asmlinkage void nmi(void);
  2. asmlinkage void double_fault(void);
  3. // arch/x86/kernel/entry_64.S
  4. idtentry double_fault do_double_fault has_error_code=1 paranoid=2
  5. ...
  6. ENTRY(nmi)
  7. ...
  8. END(nmi)
复制代码
当一个克制大概非常发生时,新的 ss 选择器被逼迫置为 NULL,而且 ss 选择器的 rpl 域被设置为新的 cpl。旧的 ss、rsp、寄存器标记、cs、rip 被压入新栈。在 64 位模子下,克制栈帧巨细固定为 8 字节,以是我们可以得到下面的栈:
  1. +---------------+
  2. |               |
  3. |      SS       | 40
  4. |      RSP      | 32
  5. |     RFLAGS    | 24
  6. |      CS       | 16
  7. |      RIP      | 8
  8. |   Error code  | 0
  9. |               |
  10. +---------------+
复制代码


  • 如果在克制门中 IST 域不是 0,我们把 IST 读到 rsp 中。

    • 如果它关联了一个克制向量错误码,我们再把这个错误码压入栈。
    • 如果克制向量没有错误码,就继承而且把假造错误码压入栈。

  • 我们必须做以上的步调以确保栈划一性。接下来我们从门形貌符中加载段选择器域到 CS 寄存器中,而且通过验证第 21 位的值来验证目的代码是一个 64 位代码段,比方 L 位在 GDT。
  • 末了我们从门形貌符中加载偏移域到 rip 中,rip 是克制处置惩罚函数的入口指针。然后克制函数开始实行,在克制函数实行竣事后,它必须通过 iret 指令把控制权交还给被克制历程。iret 指令无条件地弹出栈指针(ss:rsp)来规复被克制的历程,而且不会依靠于 cpl 改变。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!qidao123.com:ToB企服之家,中国第一个企服评测及软件市场,开放入驻,技术点评得现金

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表