中断是一种打断程序的正常执行流程的事件,这种事件以电信号的形式出现,可以由硬件设备或者CPU本身生成。
在中断发生后,正常的执行流被立即中止,转而执行中断处理程序(handler)。中断处理完成之后,先前的执行流将继续。
对中断进行分类:
同步的中断(exception),下文均称其为异常:由CPU自己在执行指令时生成(比如除以0或者系统调用。因为执行指令总是要依照系统时钟,所以叫它同步中断);
异步的中断(interrupt):由外部事件生成(比如敲键盘)。
可以屏蔽的中断:可以被CPU忽略,需要接到INT引脚。
不能屏蔽的中断:需要接到NMI引脚。
大多数的中断是可以屏蔽掉的。可以通过屏蔽中断电信号阻止相关中断的处理,直到我们放行中断信号。
有两种原因可以导致异常:
fault可以在特定指令执行前就被触发(比如缺页),可以被纠正(比如换页)。EIP寄存器会存储对应的指令。于是在处理完异常之后可以继续执行。
trap是在指令执行后触发的异常。同样地,EIP寄存器会存储对应的指令。
比如上图,不可屏蔽的中断走NMI引脚,可屏蔽的中断走INTR引脚。
能产生中断的设备有一个输出引脚,用来生成中断请求(Interrupt ReQuest, IRQ)。我们把这个引脚叫做IRQ引脚。设备的引脚接到可编程中断控制器(PIC)上,PIC和CPU的INTR引脚相连。
此外,PIC也有其他和CPU相连的(硬件)接口,用来交换信息。
设备产生中断的流程如下:
由此可见,根据PIC的设计,在CPU确认当前中断之前,PIC没法再开新的中断。
注意:CPU确认了当前中断和完成中断处理是两个概念。当CPU确认了一个中断后,PIC可以请求另一个中断。这意味着中断控制器可以在CPU还没有处理完前一个中断时请求新的中断。至于会不会出现嵌套中断,这取决于OS的管理方式。
PIC允许每一条IRQ线都被单独地开启或者禁用。
SMP:其实可以理解为多核处理器。这些个核共享系统的所有资源(内存,总线,外设etc)
由于SMP有多个核,所以可能有多个中断控制器。比如,在x86架构中:
每个核都有局部的Advanced PIC,用来接收局部设备的IRQ(比如温度传感或者计时器)。同时,存在一个IO外设,用来向这些CPU核分发IO设备的异常。
一般来说,为了确保 中断处理函数 和 其他并行操作 之间访问数据的同步性,很多时候会需要启用/禁用某个中断线上的中断。有多种实现方式:
大多数的体系结构都支持中断优先级。一旦启用中断优先级,那么在一个中断执行过程中,有且仅有更高优先级的中断可以打断它。
并不是所有的体系结构都支持中断优先级。
为通用OS定义一般性的中断优先级也比较困难。
有的系统内核不用中断优先级。
RTOS一般都会用中断优先级。
中断描述符表(IDT)把中断或者异常的标识和处理相关事件的指令描述符相关联(可以理解为以特殊形式存在的回调)。
标识符称为向量号,相关的指令称为异常处理程序。
IDT的特征如下:
这个图是IDT表。0-31项存放异常,32-127项存放设备中断,128用于系统调用,其他项的用来存放别的中断。
每一个数字代表一个表项。如下图,是一个表项中包含的中断的详细信息:
其中,在x86机器上,一个IDT项有8字节,这个IDT项被称为gate。一共有三种gate:
寻址过程如下图:
我们要找中断处理函数的地址:
首先基于IDT项中的segment selector找到GDT/LDT里对应的段描述符,基于段描述符里的基地址和IDT项中的偏移量,就可以找到对应的中断处理函数的起始地址。
正常函数的控制流转换,以函数调用为例,基于栈。中断处理函数也是基于栈的。用栈来存放调用中断处理函数之前的执行上下文。
如下图,中断首先保存EFLAGS寄存器内容,然后保存当前被打断的进程的上下文。有的异常也会在栈上保存错误码。
在生成中断请求IRQ之后,CPU要执行一系列准备工作,最终执行内核中的中断处理函数:
大多数体系结构会提供特殊指令,允许清空栈上中断处理的相关内容并且恢复之前的执行。
比如,x86上有IRET指令,负责从中断处理中恢复。
在执行完中断处理函数之后:
Linux中断处理的三个阶段:critical(关键阶段)、immediate(即时阶段)和deferred(延迟阶段)。
基本上是确认——即时处理——延时处理三个步骤
在第一个阶段,内核会进行通用的中断处理,确定中断号、中断处理函数和对应的中断控制器。内核和中断控制器进行交互,在中断控制器层面确认中断的到来。以确保中断不会被误报或丢失。此阶段,内核通常会禁用本地CPU的中断,以确保在中断处理的关键阶段中不会被其他中断打断。
在第二个阶段,所有和这个中断相关的设备驱动的处理函数都会被调用(有的设备驱动的处理函数发现这个中断不是自己生成的,那么它就会直接退出)。调用完成之后,中断控制器的end of interrupt
方法被调用,直到此时,本地CPU的中断才会被启用,允许别的中断到来。
一个中断号可能对应多个设备,此时,中断被共享。这个时候,每个设备就要自己确认是不是自己生成了这个中断。
最后一个阶段,延迟工作部分,就是常说的中断处理的下半部分工作。此时,本地CPU启用中断。
由于各种爆栈问题的复杂处理方式(比如允许一级嵌套、多级嵌套、加大内核栈深度等),中断嵌套目前已经被linux禁用了。有利于内核的维护。
但是,这不意味着中断和异常之间的嵌套不复存在。
中断和异常嵌套的一些原则,如下图所示:
定义:在中断被处理时(从 CPU跳转到中断处理函数 到 中断处理函数返回,即IRET指令执行 的这段时间)被称为中断上下文。
在这段时间里运行的代码有下列性质:
可延迟操作用于在稍后的时间运行钩子函数。这些钩子函数通常用于执行与中断处理相关的任务或执行需要延迟执行的操作。
可延迟操作可以分为两个大类:在中断上下文中运行的,在进程上下文中运行的。
使用中断上下文的可延迟操作的主要目的是确保中断处理程序尽可能快速地执行,以减小系统响应时间并减少中断处理程序的持续时间。通过将某些任务推迟到稍后在进程上下文中执行,可以避免中断处理程序的过度运行,从而提高系统性能。
可延迟操作有相应的API,包括初始化、激活、调度、启用/禁用(用于上下文信息同步)等。
一般来说,设备驱动会在初始化设备实例时初始化可延迟操作的相关信息,在中断处理函数里进行可延迟操作的激活和调度。
在中断处理函数中启动可延迟操作,但是可延迟操作仍然在中断上下文中运行。软中断是一种允许在中断处理程序中排队和异步执行工作的方式,
API:
初始化: open_softirq()
激活: raise_softirq()
启用禁用: local_bh_disable(), local_bh_enable()
一旦激活,则钩子函数do_softirq() 可以在中断处理函数之后运行,也可以在ksoftirqd内核线程里运行。
因为软中断可以调度它们自己,比如触发了新的软中断,也可以被别的中断调度,所以有可能会导致软中断的饥饿。目前,linux内核里设置了
一旦上述限制条件触发,内核线程ksoftirqd 就会出马,把所有挂起的软中断全执行掉。
软中断一般被严格限制使用,只有少数需要低延迟高频性能的子系统会用软中断:
/* PLEASE, avoid to allocate new softirqs, if you need not _really_ high
frequency threaded job scheduling. For almost all the purposes
tasklets are more than enough. F.e. all serial device BHs et
al. should be converted to tasklets, not to softirqs.
*/
enum
{
HI_SOFTIRQ=0,
TIMER_SOFTIRQ,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
BLOCK_SOFTIRQ,
IRQ_POLL_SOFTIRQ,
TASKLET_SOFTIRQ,
SCHED_SOFTIRQ,
HRTIMER_SOFTIRQ,
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */
NR_SOFTIRQS
};
chatGPT说,Tasklet 是软中断的一种特殊情况,用于执行快速的、低延迟的任务,而软中断是一种更通用的机制,可以用于执行各种类型的任务,包括相对复杂的操作。选择使用哪种机制取决于任务的复杂性和性能要求。
在Linux内核中,不同的软中断执行过程通常会共享相同的内核栈,而不是为每个软中断分配单独的栈空间。这是因为内核需要高效管理栈资源,分配单独的栈空间给每个软中断会占用大量的内存,并且会引入复杂性。共享栈的方法可以有效节省内存,并且已经在内核中实现。
软中断通常不会被定时器中断打断和重新调度。软中断在内核中运行于中断上下文,具有较高的优先级,因此它们通常不会被其他中断事件打断,除非发生了一些特殊情况。定时器中断(timer interrupt)通常以较低的优先级运行,用于触发定时器处理、调度延迟工作队列等任务。因此,定时器中断一般不会在执行软中断时打断软中断的执行,除非内核开启抢占模式,允许中断的抢占。