Linux内核17-硬件如何处理中断和异常

本文涉及的产品
网络型负载均衡 NLB,每月750个小时 15LCU
应用型负载均衡 ALB,每月750个小时 15LCU
公网NAT网关,每月750个小时 15CU
简介: Linux内核17-硬件如何处理中断和异常

在上一篇文章中,我们已经了解了中断和异常的一些概念,对于中断和异常也有了大概的理解。那么,系统中硬件到底是如何处理中断和异常的呢?本文我们就以常见的X86架构为例,看看中断和异常的硬件工作原理。


1 高级可编程中断控制器-APIC


之前,我们主要考虑的单处理器系统,如果是多处理器系统,主PIC控制器的INTR管脚是如何接到CPU上的?我们接下来讨论这个话题。

我们知道,多核处理系统的价值在于 并行处理。所以,如何把中断分配到每一个CPU上就至关重要了。基于这个原因,Intel从奔腾III开始,引入一个新的高级可编程中断控制器(I/O-APIC)。这个控制器是8259A中断控制器的加强版。为了兼容旧版本的操作系统,有些主板包含这两种芯片。x86架构中,每个处理器包含自己的APIC,每个APIC具有32位的寄存器,内部时钟,内部定时器以及2个额外的IRQ线,LINT0和LINT1,用作APIC的中断。所有私有的APIC都连接到I/O-APIC,组成一个多APIC系统。

图4-1展示了一个多APIC系统的原理图。I/O-APIC通过APIC总线和各个APIC连接在一起。I/O-APIC相等于一个中继的角色。


640.png

图4-1 多APIC系统

I/O-APIC由24条中断线,中断重定向表,可编程寄存器和一个通过APIC总线收发数据的消息单元组成。与8259A中断控制器不同,管脚编号不再具有优先级:重定向表中的每一项都可以被独立设置中断向量和优先级,目的处理器以及处理器如何处理该中断。也就是说,中断重定向表就是外部IRQ到私有APIC的映射关系。

中断请求被分配到CPU上的方式有两种:

  1. 静态分配
    按照重定向表中的定义把IRQ请求分配到相应的私有APIC高级可编程中断控制器上。中断可以指定给单个CPU,或者一组CPU,或者所有的CPU(相当于广播)。
  2. 动态分配
    IRQ请求被发送给正在运行低优先级进程的处理器的私有APIC中断控制器上。通俗地说,就是哪个处理器正在运行低优先级任务,IRQ请求就发送给谁。
    每个私有APIC都有一个可编程任务优先级寄存器,用来保存当前运行任务的优先级。Intel期望每次进程切换的时候,操作系统内核修改这个寄存器。
    如果有多个CPU拥有相同的最低任务优先级,则使用仲裁技术分配中断请求。根据仲裁,每个CPU被分配一个不同的优先级(0-15,数字越小,优先级越大),这个优先级存储在私有APIC的任务优先级寄存器中。
    分配策略是,每当分配一个中断请求给一个CPU,则它对应的仲裁优先级被自动设为0,而其它CPU的仲裁优先级则被增加。当优先级寄存器中的值大于15时,则设为1。因为具有相同任务优先级的CPU的中断分配使用循环方式进行。

动态分配的策略就是负载均衡的一种手段。关于负载均衡的算法以后再研究。

除了CPU与外设之间的中断,多APIC系统还允许CPU产生CPU之间的中断。当一个CPU想给另一个CPU发送中断时,它就会把目标CPU的私有APIC的标识符和中断号存储到自己APIC的中断命令寄存器(ICR)中。然后通过APIC总线发送给目标APIC,该APIC就会给自己的CPU发送一个相应的中断。

CPU间的中断(简称IPI)是多核系统一个重要组成部分。Linux有效地利用它们,在CPU之间传递消息。

目前,大部分的单核系统也都包含一个I/O-APIC芯片,可以使用两种不同的方式配置它:

  1. 当一个标准的8259A类型的外部PIC使用。私有APIC被禁止,LINT0和LINT1这两个IRQ请求线被分别配置为INTR和NMI管脚。
  2. 作为标准的I/O-APIC使用,只不过只有一个CPU而已。


2 异常


x86架构大约有20种不同的异常。内核必须为每种异常提供专用的处理函数。对于某些异常,CPU控制单元也会产生硬件错误码,并将其压入内核态栈,然后再启动异常处理函数。

下表是异常列表,列出了异常号,名称,类型等等。更多信息请参考Intel技术手册。

# 异常 类型 异常处理函数 信号
0 除法错误 fault divide_error() SIGFPE
1 Debug trap/fault debug( ) SIGTRAP
2 NMI - nmi( ) -
3 断点 trap int3( ) SIGTRAP
4 溢出 trap overflow( ) SIGSEGV
5 边界检查 fault bounds( ) SIGSEGV
6 非法操作码 fault invalid_op( ) SIGILL
7 设备不可用 fault device_not_available( ) -
8

串行处理

异常错误

abort doublefault_fn() -
9

协处理器

错误

abort coprocessor_segment_overrun( ) SIGFPE
10 非法TSS fault invalid_TSS( ) SIGSEGV
11 段引用错误 fault segment_not_present( ) SIGBUS
12 栈段错误 fault stack_segment( ) SIGBUS
13 通用保护 fault general_protection( ) SIGSEGV
14 页错误 fault page_fault( ) SIGSEGV
15 Intel保留 - - -
16 浮点错误 fault coprocessor_error( ) SIGFPE
17 对齐检查 fault alignment_check( ) SIGBUS
18 机器检查 abort machine_check() -
19

SIMD

浮点异常

fault simd_coprocessor_error() SIGFPE

Intel保留20-31未来使用。如上表所示,每个异常都有一个专门的处理函数处理,并给造成异常的进程发送一个信号。


3 中断描述符表


现在,我们已经知道了中断信号是如何从设备发出,然后经过高级可编程中断控制器的分配,到达各个指定的CPU中。那么,剩下的工作就是内核的了,内核使用一个中断描述符表(IDT),记录每个中断或者异常编号以及相应的处理函数。那么,收到中断信号后,将相应的处理函数的地址加载到eip寄存器中执行即可。

IDT表中,每一项对应一个中断或者异常,大小8个字节。因而,IDT需要256x8=2048个字节大小的存储空间。

IDT表的物理地址存储在CPU寄存器idtr中:包括IDT的基地址和最大长度。在使能中断之前,必须使用lidt汇编指令初始化IDT表。

IDT表包含三种类型的描述符,使用Type位域表示(40-43位)。下图分别解释了这三种描述符各个位的意义。

640.png


三种描述符分别为:

  1. 任务门
    包含中断发生时要替换当前进程的新进程的TSS选择器。
  2. 中断门
    包含段选择器和在段中的偏移量。设置了正确的段后,处理器清除IF标志,禁止可屏蔽中断。
  3. 陷阱门
    同中断门类似,只是不会修改IF标志。


4 中断和异常的硬件处理


现在,我们来探究一下CPU控制单元是如何处理中断和异常的。我们假设内核已经完成初始化,CPU工作在保护模式下。

CPU控制单元,在取指令之前,检查控制单元在执行前一条指令的时候是否有中断或异常发生。如果发生中断,控制单元就会做如下处理:

  1. 确定中断或异常的编号N;
  2. 读取IDT表中的第N项;(在后面的描述中,假设包含的是中断门或陷阱门)
  3. 获取GDT的基地址,遍历GDT找到IDT表第N项中的段选择器标识的段描述符。这个描述符指定了包含中断或异常处理程序的段的基地址。
  4. 确保中断合法性。
    首先比较cs寄存器中的CPL(当前特权等级)和包含在GDT中的段描述符的DPL(描述符特权等级),如果CPL小于DPL,产生 通用保护 异常,因为中断处理程序的特权等级不能比造成中断的程序的低。对于可编程异常,还会做进一步的安全检查:比较当前特权等级(CPL)和IDT表中包含的描述符的DPL,如果DPL小于CPL,则产生通用保护的异常。后一项检查,可以阻止用户应用程序访问特定的trap或中断门。
  5. 检查特权等级是否发生变化。如果CPL与描述符中的DPL不同,控制单元应该使用新特权等级下的堆栈。

其实对于Linux来说,只使用了supervisor和user两种特权等级。所以中断应该都是在supervisor特权等级下运行。

  1. 读取tr寄存器,访问运行中的进程的TSS段;
  2. 使用新特权等级对应的堆栈段和堆栈指针加载ss和esp寄存器;(这些值存储在TSS中)
  3. 在新的堆栈中,保存旧任务的ss和esp寄存器值。(处理完中断或异常后,还要恢复到旧任务执行)
  1. 根据造成异常的指令的逻辑地址,加载cs和eip寄存器(异常解决后,程序可以继续从这儿执行);
  2. 保存eflags、cs和eip到堆栈中;
  3. 如果异常携带异常错误码,将其保存在堆栈中;
  4. 根据IDT表中的第N项内容,加载cs和eip寄存器。

至此,CPU控制单元跳转到中断或异常处理程序处开始执行。等到中断或异常处理完成后,把CPU的使用权让给之前被中断的进程,使用iret指令,该指令强迫控制单元执行下面步骤:

  1. 加载被中断进程的cs,eip和eflags寄存器。(如果压栈过异常错误码,应该在执行iret指令之前弹出)
  2. 检查CPL是否等于cs寄存器中的CPL,如果相等,则iret指令结束执行;否则,继续。
  3. 加载旧特权等级的ss和esp寄存器值。
  4. 检查ds、es、fs和gs寄存器中的值。如果它们之中任何一个的描述符中的DPL小于CPL,则清除相应的段寄存器。这么做,可以禁止用户态程序使用先前内核态的段寄存器。如果这些寄存器没有被清除,恶意用户态程序就可以利用它们访问内核地址空间。
相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
相关文章
|
2月前
|
安全 网络协议 Linux
深入理解Linux内核模块:加载机制、参数传递与实战开发
本文深入解析了Linux内核模块的加载机制、参数传递方式及实战开发技巧。内容涵盖模块基础概念、加载与卸载流程、生命周期管理、参数配置方法,并通过“Hello World”模块和字符设备驱动实例,带领读者逐步掌握模块开发技能。同时,介绍了调试手段、常见问题排查、开发规范及高级特性,如内核线程、模块间通信与性能优化策略。适合希望深入理解Linux内核机制、提升系统编程能力的技术人员阅读与实践。
199 1
|
2月前
|
监控 Ubuntu Linux
什么Linux,Linux内核及Linux操作系统
上面只是简单的介绍了一下Linux操作系统的几个核心组件,其实Linux的整体架构要复杂的多。单纯从Linux内核的角度,它要管理CPU、内存、网卡、硬盘和输入输出等设备,因此内核本身分为进程调度,内存管理,虚拟文件系统,网络接口等4个核心子系统。
189 0
|
2月前
|
Web App开发 缓存 Rust
|
2月前
|
Ubuntu 安全 Linux
Ubuntu 发行版更新 Linux 内核,修复 17 个安全漏洞
本地攻击者可以利用上述漏洞,攻击 Ubuntu 22.10、Ubuntu 22.04、Ubuntu 20.04 LTS 发行版,导致拒绝服务(系统崩溃)或执行任意代码。
|
存储 负载均衡 网络协议
X86 linux异常处理与Ipipe接管中断/异常
本文讲述了X86平台上Xenomai的ipipe如何接管中断处理。首先回顾了X86中断处理机制,包括IDT(中断描述符表)的工作原理和中断处理流程。接着详细介绍了Linux中中断门的初始化,包括门描述符的结构、中断门的定义和填充,以及IDT的加载。在异常处理部分,文章讲解了早期异常处理和start_kernel阶段的异常向量初始化。最后,讨论了APIC和SMP中断在IDT中的填充,以及剩余中断的统一处理。文章指出,ipipe通过在中断入口处插入`__ipipe_handle_irq()`函数,实现了对中断的拦截和优先处理,确保了实时性。
310 0
X86 linux异常处理与Ipipe接管中断/异常
|
Linux
Linux操作系统基础知识之五:中断和异常
Q1.        什么是中断?什么是异常?二者有何不同? A: 1)        中断控制是为克服对I/O接口采用程序查询控制服务方式所带来的处理器低效率而产生的,它的主要优点是只有在I/O接口需要服务时才能得到处理器的响应,而不需要处...
1081 0
|
24天前
|
Unix Linux 程序员
Linux文本搜索工具grep命令使用指南
以上就是对Linux环境下强大工具 `grep` 的基础到进阶功能介绍。它不仅能够执行简单文字查询任务还能够处理复杂文字处理任务,并且支持强大而灵活地正则表达规范来增加查询精度与效率。无论您是程序员、数据分析师还是系统管理员,在日常工作中熟练运用该命令都将极大提升您处理和分析数据效率。
97 16