Linux 驱动开发之中断分析4(基于Linux6.6)---相关流程介绍
一、Linux 中如何标识一个外部中断?
1. 中断号 (IRQ, Interrupt Request)
外部中断通常由硬件设备触发,每个硬件中断都有一个唯一的中断号(IRQ,Interrupt Request)。Linux 内核通过这个中断号来标识和管理每个外部中断。
- IRQ号码 是与硬件设备相关联的标识符。不同的硬件设备会被分配一个唯一的 IRQ 号码,内核根据这个号码来决定哪个硬件设备触发了中断。
- 常见的 IRQ 号码会被映射到具体的硬件设备,例如键盘、鼠标、网卡、硬盘等设备。
2. 中断向量和中断处理程序
当外部硬件设备触发一个中断时,CPU 会跳转到中断处理程序(Interrupt Service Routine,ISR),这些处理程序是由内核为不同的硬件设备预定义的。每个 IRQ 对应着一个中断处理程序。内核通过中断向量表来管理这些处理程序。
中断向量
中断向量(interrupt vector)是一个包含中断处理程序地址的表,内核会根据触发的中断号在该表中找到对应的中断处理程序的地址。中断处理程序会处理该中断并通知内核相应的设备驱动程序。
3. 中断标识符和设备驱动
外部中断的标识通常依赖于设备的驱动程序。在 Linux 中,设备驱动通过申请 IRQ 号码并绑定中断处理程序来处理外部中断。
-
设备驱动会通过调用
request_irq()
函数来申请一个 IRQ 号码,并为该中断号注册一个处理程序。例如:
-
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *devname, void *dev_id);
irq
是中断号。handler
是中断处理程序。flags
可以控制中断的行为,例如是否允许共享中断。devname
是设备名称。dev_id
是设备标识符。
这样,内核就可以将一个外部中断与对应的设备驱动及其处理程序关联起来。
4. 中断共享机制
在一些情况下,多个设备可能会共享同一个 IRQ 号码。Linux 提供了中断共享机制,允许多个设备共享一个 IRQ 号。当中断发生时,所有共享该 IRQ 的设备都会触发相应的处理程序。在这种情况下,request_irq()
函数会传递一个 IRQF_SHARED
标志来标识这一点。
5. 中断状态和标识
在 Linux 内核中,通过以下方法可以标识和管理外部中断的状态:
-
中断启用与禁用:通过
enable_irq()
和disable_irq()
函数,可以启用或禁用某个 IRQ。disable_irq()
函数可以禁止指定 IRQ 的中断处理,而enable_irq()
则是恢复中断处理。 -
中断处理的标识:在中断处理函数中,通常会进行一些状态标识的操作,比如检查哪个设备触发了中断,处理中断后再清除中断标志等。通过
ack_irq()
函数,内核可以向硬件确认中断已经处理完毕。
6. 中断上下文
当外部硬件触发中断时,CPU 会进入中断上下文。中断上下文下的操作必须是尽可能简短的,因为中断处理程序会阻塞其他中断。通常,中断处理程序会快速响应并通过调度机制将长时间的处理延迟到任务上下文中进行。
7. 查看和管理中断
在 Linux 系统中,可以通过以下方式来查看和管理中断:
-
查看中断信息:可以通过
cat /proc/interrupts
命令查看当前系统的中断信息。这会列出各个硬件设备对应的 IRQ 号,以及每个 IRQ 被触发的次数。例如:bash
cat /proc/interrupts
输出示例:
-
CPU0 CPU1 CPU2 CPU3 0: 1234567 7654321 2345678 8765432 IR-PCI-MSI eth0 1: 3456789 5678901 1234567 2345678 IR-PCI-MSI audio
这里的
0
、1
等是 IRQ 号,eth0
和audio
是设备名称。可以看到每个 IRQ 被多少次触发。 -
禁用或启用中断:可以通过
echo
命令来禁用或启用某个 IRQ。例如,禁用 IRQ 1:bash
-
echo 1 > /proc/irq/1/smp_affinity
8. 外部中断与中断号的关联
外部中断通常通过硬件设备的 IRQ 号码来标识。在 Linux 中,不同的硬件设备通过分配不同的 IRQ 号码来标识不同的中断源。Linux 内核会根据 IRQ 号码将每个硬件中断映射到对应的中断处理程序。在中断发生时,内核会检查中断号,找到相应的处理中断的函数。
二、中断节点在设备树中的描述
在Device Tree Source文件中,对于那些产生中断的外设,需要定义interrupt-parent和interrupts属性:
(1)interrupt-parent:表明该外设的interrupt request line物理的连接到了哪一个中断控制器上,中断控制器会对中断源进行描述;
(2)interrupts:这个属性描述了具体该外设产生的interrupt的细节信息(也就是传说中的interrupt specifier)。例如:HW interrupt ID(由该外设的device node中的interrupt-parent指向的interrupt controller解析)、interrupt触发类型等。
对于Interrupt controller,我们需要定义interrupt-controller和#interrupt-cells的属性:
(1)interrupt-controller
表明该device node就是一个中断控制器
(2)#interrupt-cells
该中断控制器用多少个cell(一个cell就是一个32-bit的单元)描述一个外设的interrupt request line。具体每个cell表示什么样的含义由interrupt controller自己定义。
(3)interrupts和interrupt-parent
对于那些不是root 的interrupt controller,其本身也是作为一个产生中断的外设连接到其他的interrupt controller上,因此也需要定义interrupts和interrupt-parent的属性。
2.1、中断在设备树中的描述
在设备树中,硬件设备和资源的描述包括设备的中断信息。中断信息通常会在设备节点中以 interrupts
属性的形式出现。这些描述可以帮助内核正确地配置设备的中断处理程序。中断的具体描述通常与设备(如串口、网卡、定时器等)相关联,并包括中断号、触发方式和其他配置参数。
1. 中断描述的基本格式
在设备树中,interrupts
属性描述了与某个设备相关的中断信息。其格式通常如下:
interrupts = <interrupt-specifier>;
其中,interrupt-specifier
是一个整数,或者是一个更复杂的数组,具体形式取决于硬件平台和中断控制器的实现。
2. 常见的中断描述方式
简单的中断描述
在最简单的情况下,interrupts
属性是一个数组,其中包含一个中断号。这个描述通常出现在中断控制器的节点下,表示设备的中断请求。
例如,描述一个设备中断的设备树节点:
uart@12340000 {
compatible = "vendor,uart";
reg = <0x12340000 0x1000>;
interrupts = <5>; // IRQ号5
};
在这个例子中,uart@12340000
设备的 interrupts = <5>
表示该设备使用 IRQ 号为 5 的中断。
带有中断触发方式的描述
在某些情况下,设备中断还可能包括触发类型(边沿触发或电平触发)等信息。中断描述可以提供这些额外的参数。通常,设备树中的 interrupts
属性会以 <interrupt-specifier>
数组的形式出现,描述中包括中断号、触发类型以及可能的中断优先级。
例如:
uart@12340000 {
compatible = "vendor,uart";
reg = <0x12340000 0x1000>;
interrupts = <0 0 5>; // 第一个 0:表示中断控制器的编号,第二个 0:表示中断类型,5:IRQ号为5
};
- 第一个元素(
0
)通常是中断控制器的编号。 - 第二个元素(
0
)表示中断的触发方式,可以是以下几种常见的值:0
:表示边沿触发(Level-Triggered)1
:表示上升沿触发(Edge-Triggered)2
:表示下降沿触发(Edge-Triggered)
- 第三个元素(
5
)表示中断号。
多中断源的描述
如果一个设备有多个中断源,则会使用一个数组来表示这些中断。每个中断源都可以有不同的中断号、触发方式等。例如,一个设备可能会有两个或多个中断源,这时在设备树中描述为:
device@10000000 {
compatible = "vendor,device";
reg = <0x10000000 0x1000>;
interrupts = <5 0 10>, <6 1 15>; // 两个中断源
};
在这个例子中,设备有两个中断源:
- 第一个中断源的中断号是 5,触发方式是边沿触发。
- 第二个中断源的中断号是 6,触发方式是下降沿触发。
3. 中断控制器的描述
设备树中的中断控制器通常使用 interrupt-controller
属性来表示。中断控制器的节点描述了中断的来源、数量、触发方式等信息。例如:
interrupt-controller@f2000000 {
compatible = "vendor,interrupt-controller";
reg = <0xf2000000 0x1000>;
#interrupt-cells = <1>;
interrupt-controller;
};
interrupt-controller
表示这是一个中断控制器节点。#interrupt-cells = <1>
表示每个中断源的描述只包含一个整数(中断号)。reg
属性表示该中断控制器在内存中的映射。
4. 中断控制器与设备的关联
在设备树中,设备节点的中断描述通常与中断控制器进行关联。这个关联关系通过设备树的层次结构实现。设备节点会引用父节点(中断控制器)来明确它们的中断来源。
例如,如果中断控制器和设备在设备树中有层次关系,那么设备可以通过 interrupt-parent
属性来明确它的中断控制器:
interrupt-controller@f2000000 {
compatible = "vendor,interrupt-controller";
reg = <0xf2000000 0x1000>;
#interrupt-cells = <1>;
interrupt-controller;
};
uart@12340000 {
compatible = "vendor,uart";
reg = <0x12340000 0x1000>;
interrupts = <5>;
interrupt-parent = <&interrupt-controller@f2000000>; // 关联中断控制器
};
5. 设备树中断属性的扩展
除了 interrupts
和 interrupt-parent
属性,设备树还可能包含其他中断相关的属性,这些属性的使用会因硬件平台和中断控制器的不同而有所变化。例如:
interrupts-extended
:某些平台可能会使用扩展的中断描述符,表示更多的中断信息。interrupts-active-low
:标识中断是否是低电平触发。interrupts-polarity
:描述中断的极性(正极性或负极性)。
三、Linux 内核对中断的初始化过程
ARM linux内核启动时,首先运行的是linux/arch/arm/kernel/head.S,进行一些初始化工作,然后调用main.c->start_kernel()函数,进而调用early_irq_init()函数进行初始化、init_IRQ()函数进行中断初始化、建立异常向量表
【init_IRQ ---> irqchip_init ---> of_irq_init】
extern struct of_device_id __irqchip_begin[];
void __init irqchip_init(void)
{
of_irq_init(__irqchip_begin);
}
extern struct of_device_id __irqchip_begin[];
struct of_device_id
{
char name[32];------要匹配的device node的名字
char type[32];-------要匹配的device node的类型
char compatible[128];---匹配字符串(DT compatible string),用来匹配适合的device node
const void *data;--------对于clock source,这里是初始化函数指针
};
这个数据结构主要被用来进行Device node和driver模块进行匹配用的。从该数据结构的定义可以看出,在匹配过程中,device name、device type和DT compatible string都是考虑的因素。更细节的内容请参考__of_device_is_compatible函数。
__irqchip_begin就是内核irq chip table的首地址,这个table也就保存了kernel支持的所有的中断控制器的ID信息(用于和device node的匹配)。
void __init of_irq_init(const struct of_device_id *matches)
of_irq_init函数执行之前,系统已经完成了device tree的初始化,因此系统中的所有的设备节点都已经形成了一个树状结构,每个节点代表一个设备的device node。of_irq_init是在所有的device node中寻找中断控制器节点,形成树状结构(系统可以有多个interrupt controller,之所以形成中断控制器的树状结构,是为了让系统中所有的中断控制器驱动按照一定的顺序进行初始化)。之后,从root interrupt controller节点开始,对于每一个interrupt controller的 device node,扫描irq chip table,进行匹配,一旦匹配到,就调用该interrupt controller的初始化函数,并把该中断控制器的device node以及parent中断控制器的device node作为参数传递给irq chip driver。
四、中断触发后的处理流程
a -- 具体CPU architecture相关的模块会进行现场保护,然后调用machine driver对应的中断处理handler;
b -- machine driver对应的中断处理handler中会根据硬件的信息获取HW interrupt ID,并且通过irq domain模块翻译成IRQ number
c -- 调用该IRQ number 对应的high level irq event handler,在这个high level的handler中,会通过和interupt controller交互,进行中断处理的flow control(处理中断的嵌套、抢占等),当然最终会遍历该中断描述符的IRQ action list,调用外设的specific handler来处理该中断
d -- 具体CPU architecture相关的模块会进行现场恢复。
4.1、Linux 中断触发后的处理流程
1. 硬件发出中断请求(IRQ)
当外部设备(如网卡、串口、定时器等)需要处理某个事件时,它会向 CPU 发出中断信号,通知 CPU 该设备需要注意。中断信号通常通过硬件中断控制器(如 APIC、PIC、GIC)来发送。
2. 中断控制器接收并传递中断
在中断信号到达 CPU 后,硬件中断控制器负责将中断信号传递给处理器。不同的系统平台可能使用不同的中断控制器,如:
- PIC(Programmable Interrupt Controller):较旧的中断控制器,用于 x86 系统。
- APIC(Advanced Programmable Interrupt Controller):现代的中断控制器,用于多核处理器。
- GIC(Generic Interrupt Controller):ARM 架构使用的中断控制器。
中断控制器会对接收到的多个中断进行优先级排序,并确定哪个中断信号应该首先被 CPU 处理。它还可以根据中断的类型(边沿触发或电平触发)决定是否立即触发中断处理程序。
3. CPU 响应中断
当 CPU 接收到中断信号后,它会执行以下步骤:
-
保存现场:CPU 会保存当前程序计数器(PC)、寄存器等上下文信息,以便在中断处理完成后能够恢复到中断前的状态。
-
关闭中断(可屏蔽中断):为了避免在处理中断的过程中发生其他中断干扰,CPU 会禁止更低优先级的中断,确保中断处理程序可以在没有中断干扰的情况下执行。
-
跳转到中断向量表:中断向量表(Interrupt Vector Table)包含了各类中断的处理程序地址。CPU 根据中断号(IRQ)从中断向量表中查找对应的中断服务程序(ISR)。
4. 进入中断服务程序(ISR)
ISR(Interrupt Service Routine)是由硬件中断号决定的一个函数,在该函数中,内核会执行中断处理工作。每个设备的中断会有相应的 ISR。
在 ISR 中,通常需要完成以下几个任务:
-
清除中断标志:硬件中断控制器通常会有一个“中断标志”或“中断挂起”位,ISR 会清除这个标志,通知中断控制器该中断已处理完毕。
-
读取或写入硬件设备:ISR 通常会读取设备寄存器或写入设备寄存器,以处理硬件发出的中断请求。比如,对于定时器中断,ISR 可能会更新系统时间。
-
触发上层工作:有时 ISR 不能执行复杂的任务,因为 ISR 是实时的,需要尽量简短高效。复杂的任务可能会被推迟到稍后由工作队列(workqueue)或者软中断(softirq)来处理。
5. 中断返回(恢复现场)
在 ISR 完成必要的硬件操作后,CPU 会恢复之前保存的现场信息(如程序计数器和寄存器),然后跳回到中断发生之前执行的代码位置。
6. 软中断和任务队列
有时,中断处理程序会将需要进一步处理的任务推到软中断(softirq)或者工作队列(workqueue)中。这是因为中断服务程序应该尽量避免长时间占用 CPU,而将复杂的任务放入软中断或工作队列中,等待稍后由内核线程处理。
-
软中断(Soft IRQ):软中断是 Linux 中断处理机制中的一种,主要用于高效地处理网络数据包、定时器等任务。在中断上下文中,Linux 可以调度软中断来处理那些不需要立即完成的工作。
-
工作队列(Workqueue):工作队列是一个用于延迟任务执行的机制,可以将任务从中断上下文转移到内核线程上下文中执行。通过工作队列,系统可以推迟繁重的计算工作,而不会阻塞中断处理。
7. 中断优先级和屏蔽
中断优先级的处理在多核系统中尤其重要。Linux 系统通常会根据中断控制器的配置来决定不同中断的优先级,某些中断(例如硬件错误或重要的外设中断)会被设为高优先级,而其他中断(如定时器中断或低优先级的 I/O 设备中断)可能会被设为低优先级。
对于一些可以屏蔽的中断,CPU 会在处理中断时屏蔽掉低优先级的中断,防止这些中断打断当前中断的处理中断。
8. 中断调度和调度程序
在中断处理完成后,内核会检查是否有新的任务需要调度。如果在中断处理期间唤醒了某些进程或有新的可运行任务,调度程序会根据调度策略决定哪个任务需要执行。
在多核系统中,Linux 也会使用 CPU亲和性 来确保中断可以有效地分配到不同的 CPU 核心,以便平衡负载并减少中断处理延迟。
9. 恢复中断允许
当中断处理程序完成后,CPU 会恢复之前禁止的中断,允许更低优先级的中断继续发生。这样可以确保系统的中断处理机制始终高效运行。