【系列文章】Linux架构中断的应用01-Linux内核中的中断概念
【系列文章】Linux系统中断的应用01-Linux内核中的中断概念
该文章为系列文章:Linux系统中断的应用
中的第1篇
该系列的导航页连接:
【系列文章】Linux系统中断的应用-导航页
文章目录
一、什么是中断?
1.1中断的概念
中断是指在 CPU 正常运行期间,由外部或内部事件引起的一种机制。当中断发生时,CPU会停止当前正在执行的程序,并转而执行触发该中断的中断处理程序。处理完中断处理程序后,CPU 会返回到中断发生的地方,继续执行被中断的程序。中断机制允许 CPU 在实时响应外部或内部事件的同时,保持对其他任务的处理能力。
可以想象这样一幅画面,你正在烹饪一顿美味的晚餐,准备了各种食材,点燃了炉灶,开始了幸福的烹饪过程,突然,你的手机响起,有人打来了一个紧急电话,打破了你正常的烹饪流程,这时候你需要立刻停止手中的工作,迅速接起电话,与对方进行交流,在接完电话之后,再回到厨房继续之前的烹饪流程。这就是一个在实际生活中的中断案例,中断的概念流程图如下所示:
1.2中断的重要性
在上面的场景中,作为唯一具有处理能力的主体,我们一次只能专注于一个任务,可以等待水烧开、看电视等等。然而,当我们专心致志地完成一项任务时,常常会有紧迫或不紧迫的其他事情突然出现,需要我们关注和处理。有些情况甚至要求我们立即停下手头的工作来应对。只有在处理完这些中断事件之后,我们才能回到先前的任务。
中断机制赋予了我们处理意外情况的能力,而且如果我们能充分利用这个机制,就能够同时完成多个任务。回到烧水的例子,无论我们是否在厨房,煤气灶都会将水烧开。我们只需要在水烧开后及时关掉煤气。为了避免在厨房等待的时间,而水烧开时产生的声音就是中断信号,提醒我们炉子上的水已经烧开。这样,我们就可以在等待的时间里做其他事情,比如看电视。当水壶烧开发出声音之后,它会打断当前的任务,提醒水已经烧开,这时只需要前往厨房关掉煤气即可。
中断机制使我们能够有条不紊地同时处理多个任务,从而提高了并发处理能力。类似地,计算机系统中也使用中断机制来应对各种外部事件。例如,在键盘输入时,会发送一个中断信号给 CPU,以便及时响应用户的操作。这样,CPU 就不必一直轮询键盘的状态,而可以专注于其他任务。中断机制还可以用于处理硬盘读写完成、网络数据包接收等事件,提高了系统的资源利用率和并发处理能力。
1.3中断的上下半部
中断的执行需要快速响应,但并不是所有中断都能迅速完成。此外,Linux 中的中断不支持嵌套,意味着在正式处理中断之前会屏蔽其他中断,直到中断处理完成后再重新允许接收中断,如果中断处理时间过长,将会引发问题。
这里仍旧以烹饪的过程中接电话进行举例:当你正在烹饪一顿美味的晚餐时,所有的食材都准备好了,炉灶上的火焰跳跃着,你正享受着烹饪的乐趣。突然,你的手机响起,发出紧急电话的铃声,打破了你正常的烹饪流程,接电话的时间很短并不会对烹饪产生很大的影响,而接电话的时候可能就有问题了,水烧开之后可能会煮干、错过了最好的添加调味料的时间等等。
而为了让系统可以更好地处理中断事件,提高实时性和响应能力,将中断服务程序划分为上下文两部分:
中断上文是中断服务程序的第一部分,它主要处理一些紧急且需要快速响应的任务。中断上文的特点是执行时间较短,旨在尽快完成对中断的处理。这些任务可能包括保存寄存器状态、更新计数器等,以便在中断处理完成后能够正确地返回到中断前的执行位置。
中断下文是中断服务程序的第二部分,它主要处理一些相对耗时的任务。由于中断上文需要尽快完成,因此中断下文负责处理那些不能立即完成的、需要更多时间的任务。这些任务可能包括复杂的计算、访问外部设备或进行长时间的数据处理等。
1.4中断子系统框架
一个完整的中断子系统框架可以分为四个层次,由上到下分别为用户层、通用层、硬件相关层和硬件层,每个层相关的介绍如下所示:
用户层: 用户层是中断的使用者,主要包括各类设备驱动。这些驱动程序通过中断相关的接口进行中断的申请和注册。当外设触发中断时,用户层驱动程序会进行相应的回调处理,执行特定的操作。
通用层: 通用层也可称为框架层,它是硬件无关的层次。通用层的代码在所有硬件平台上都是通用的,不依赖于具体的硬件架构或中断控制器。通用层提供了统一的接口和功能,用于管理和处理中断,使得驱动程序能够在不同的硬件平台上复用。
硬件相关层: 硬件相关层包含两部分代码。一部分是与特定处理器架构相关的代码,比如ARM64 处理器的中断处理相关代码。这些代码负责处理特定架构的中断机制,包括中断向量表、中断处理程序等。另一部分是中断控制器的驱动代码,用于与中断控制器进行通信和配置。这些代码与具体的中断控制器硬件相关。
硬件层: 硬件层位于最底层,与具体的硬件连接相关。它包括外设与 SoC(系统片上芯片)的物理连接部分。中断信号从外设传递到中断控制器,由中断控制器统一管理和路由到处理器。硬件层的设计和实现决定了中断信号的传递方式和硬件的中断处理能力。
1.5中断控制器 GIC
中断控制器 GIC(Generic Interrupt Controller)是中断子系统框架硬件层中的一个关键组件,用于管理和控制中断。它接收来自各种中断源的中断请求,并根据预先配置的中断优先级、屏蔽和路由规则,将中断请求分发给适当的处理器核心或中断服务例程。
GIC 是由 ARM 公司提出设计规范,当前有四个版本,GIC V1-v4。设计规范中最常用的,有3 个版本 V2.0、V3.1、V4.1,GICv3 版本设计主要运行在 Armv8-A, Armv9-A 等架构上。ARM 公司并给出一个实际的控制器设计参考,比如 GIC-400(支持 GIC v2 架构)、gic500(支持 GIC v3 架构)、GIC-600(支持 GIC v3 和 GIC v4 架构)。最终芯片厂商可以自己实现 GIC 或者直接购买 ARM提供的设计。
每个 GIC 版本及相应特性如下表所示:
版本 | 关键特性 | 常用核心 |
---|---|---|
GICv1 | -支持最多八个处理器核心(PE) -支持最多 1020个中断 ID | ARM Cortex-A5 MPCore ARM Cortex-A9 MPCore ARM Cortex-R7 MPCore |
GICv2 | - GICv1 的所有关键特性 -支持虚拟化 | ARM Cortex-A7 MPCore ARM Cortex-A15 MPCore ARM Cortex-A53 MPCore ARM Cortex-A57 MPCore |
GICv3 | - GICv2 的所有关键特性 -支持超过 8个处理器核心 -支持基于消息的中断 -支持超过 1020个中断 ID -CPU接口寄存器的系统寄存器访问 -增强的安全模型,分离安全和非安全的 Group 1 中断 | ARM Cortex-A53MPCore ARM Cortex-A57MPCore ARM Cortex-A72 MPCore |
GICv4 | - GICv3 的所有关键特性 -虚拟中断的直接注入 | ARM Cortex-A53 MPCore ARMCortex-A57MPCore ARM Cortex-A72 MPCore |
其中GICv3的中断控制器模型如下所示:
GIC 中断控制器可以分为 Distributor 接口、Redistributor 接口和 CPU 接口,下面是每个部分的说明:
Distributor 中断仲裁器:
包含影响所有处理器核心中断的全局设置。包含以下编程接口:
●启用和禁用 SPI。
●设置每个 SPI 的优先级级别。
●每个 SPI 的路由信息。
●将每个 SPI 设置为电平触发或边沿触发。
●生成基于消息的 SPI。
●控制 SPI 的活动和挂起状态。
●用于确定在每个安全状态中使用的程序员模型的控制(亲和性路由或遗留模型)。
Redistributor 重新分配器:
对于每个连接的处理器核心(PE),都有一个重新分配器(Redistributor)。重新分配器提供以下编程接口:
●启用和禁用 SGI(软件生成的中断)和 PPI(处理器专用中断)。
●设置 SGI 和 PPI 的优先级级别。
●将每个 PPI 设置为电平触发或边沿触发。
●将每个 SGI 和 PPI 分配给一个中断组。
●控制 SGI 和 PPI 的状态。
●对支持关联 LP(I 低功耗中断)的中断属性和挂起状态的内存中的数据结构进行基址控制。
●支持与连接的处理器核心的电源管理。
CPU 接口:
每个重新分配器都连接到一个 CPU 接口。CPU 接口提供以下编程接口:
●通用控制和配置,用于启用中断处理。
●确认中断。
●执行中断的优先级降低和停用。
●为处理器核心设置中断优先级屏蔽。
●定义处理器核心的抢占策略。
●确定处理器核心最高优先级的挂起中断。
1.6中断类型
GIC-V3 支持四种类型的中断,分别是 SGI、PPI、SPI 和 LPI,每个中断类型的介绍如下:
SGI(Software Generated Interrupt,软件生成中断):SGI 是通过向 GIC 中的 SGI 寄存器写入来生成的中断。它通常用于处理器之间的通信,允许一个 PE 发送中断给一个或多个指定的 PE,中断号 ID0 - ID15 用于 SGI。
PP(I Private Peripheral Interrupt,私有外设中断):针对特定 PE 的外设中断。不与其他 PE共享,中断号 ID16 - ID31 用于 PPI。
SPI(Shared Peripheral Interrupt,共享外设中断):全局外设中断,可以路由到指定的处理器核心(PE)或一组 PE,它允许多个 PE 接收同一个中断。中断号 ID32 - ID1019 用于 SPI
LPI(Locality-specific Peripheral Interrupt,特定局部外设中断):LPI 是 GICv3 中引入的一种中断类型,与其他类型的中断有几个不同之处。LPI 总是基于消息的中断,其配置存储在内存表中,而不是寄存器中。
INTID 范围 | 中断类型 | 备注 |
---|---|---|
0-15 | SGI(软件生成中断) | 每个核心分别存储 |
16 -31 | PPI (私有外设中断) | 每个核心分别存储 |
32-1019 | SPI(共享外设中断) | |
1020-1023 | 特殊中断号 | 用于表示特殊情况 |
1024-8191 | 保留 | |
8192 及更大 | LPI(特定局部外设中断) | 上限由实现定义 |
中断处理的状态机如下图所示:
Inactive(非活动状态): 中断源当前未被触发。
Pending(等待状态): 中断源已被触发,但尚未被处理器核心确认。
Active(活动状态): 中断源已被触发,并且已被处理器核心确认。
Active and Pending(活动且等待状态): 已确认一个中断实例,同时另一个中断实例正在等待处理。
每个外设中断可以是以下两种类型之一:
边沿触发(Edge-triggered):
这是一种在检测到中断信号上升沿时触发的中断,然后无论信号状态如何,都保持触发状
态,直到满足本规范定义的条件来清除中断。
电平触发(Level-sensitive):
这是一种在中断信号电平处于活动状态时触发的中断,并且在电平不处于活动状态时取消
触发。
1.6中断号
在 linux 内核中,我们使用 IRQ number 和 HW interrupt ID 两个 ID 来标识一个来自外设的中断:
IRQ number: CPU 需要为每一个外设中断编号,我们称之 IRQ Number。这个 IRQ number是一个虚拟的 interrupt ID,和硬件无关,仅仅是被 CPU 用来标识一个外设中断。
HW interrupt ID: 对于 GIC 中断控制器而言,它收集了多个外设的 interrupt request line 并向上传递,因此,GIC 中断控制器需要对外设中断进行编码。GIC 中断控制器用 HW interrupt ID来标识外设的中断。如果只有一个 GIC 中断控制器,那 IRQ number 和 HW interrupt ID 是可以一一对应的,如下图所示:
但如果是在 GIC 中断控制器级联的情况下,仅仅用 HW interrupt ID 就不能唯一标识一个外设中断,还需要知道该 HW interrupt ID 所属的 GIC 中断控制器(HW interrupt ID 在不同的Interrupt controller 上是会重复编码的)。
这样,CPU 和中断控制器在标识中断上就有了一些不同的概念,但是,对于驱动工程师而言,我们和 CPU 视角是一样的,我们只希望得到一个 IRQ number,而不关系具体是那个 GIC中断控制器上的那个 HW interrupt ID。这样一个好处是在中断相关的硬件发生变化的时候,驱动软件不需要修改。因此,linux kernel 中的中断子系统需要提供一个将 HW interrupt ID 映射到IRQ number 上来的机制,也就是 irq domain。
1.7中断申请函数
(1)request_irq
request_irq 函数是在 Linux 内核中用于注册中断处理程序的函数。它用于请求一个中断号(IRQ number)并将一个中断处理程序与该中断关联起来。下面是对 request_irq 函数的详细介绍:
函数原型:
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char * name, void *dev);
头文件:
#include <linux/interrupt.h>
函数作用:
request_irq 函数的主要功能是请求一个中断号,并将一个中断处理程序与该中断号关联起来。
当中断事件发生时,与该中断号关联的中断处理程序会被调用执行。
参数含义:
irq:要请求的中断号(IRQ number)。
handler:指向中断处理程序的函数指针。
flags:标志位,用于指定中断处理程序的行为和属性,如中断触发方式、中断共享等。
name:中断的名称,用于标识该中断。
dev:指向设备或数据结构的指针,可以在中断处理程序中使用。
返回值:
成功:0 或正数,表示中断请求成功。
失败:负数,表示中断请求失败,返回的负数值表示错误代码。
irq 参数用来指定要请求的中断号,中断号需要通过 gpio_to_irq 函数映射 GPIO 引脚来获得(gpio_to_irq 函数接下来会进行介绍)。
irq_handler_t handler 参数是一个函数指针,指向了中断处理程序的函数。中断处理程序是在中断事件发生时调用的函数,用于处理中断事件(关于中断处理程序会在下个小节进行详细的讲解)。
unsigned long flags:中断处理程序的标志位
这个参数用于指定中断处理程序的行为和属性,如中断触发方式、中断共享等。可以使用不同的标志位进行位运算来组合多个属性。常用的标志位包括:
IRQF_TRIGGER_NONE:无触发方式,表示中断不会被触发。
IRQF_TRIGGER_RISING:上升沿触发方式,表示中断在信号上升沿时触发。
IRQF_TRIGGER_FALLING:下降沿触发方式,表示中断在信号下降沿时触发。
IRQF_TRIGGER_HIGH:高电平触发方式,表示中断在信号为高电平时触发。
IRQF_TRIGGER_LOW:低电平触发方式,表示中断在信号为低电平时触发。
IRQF_SHARED:中断共享方式,表示中断可以被多个设备共享使用。
(2)gpio_to_irq
gpio_to_irq 函数用于将 GPIO 引脚的编号(GPIO pin number)转换为对应的中断请求号(interrupt request number)。
函数原型:
unsigned int gpio_to_irq(unsigned int gpio);
头文件:
#include <linux/gpio.h>
函数功能:
gpio_to_irq 是一个用于将 GPIO 引脚映射到对应中断号的函数。它的作用是根据给定的 G
PIO 引脚号,获取与之关联的中断号。
参数说明:
gpio:要映射的 GPIO 引脚号。
返回值:
成功:返回值为该 GPIO 引脚所对应的中断号。
失败:返回值为负数,表示映射失败或无效的 GPIO 引脚号。
(3)free_irq
free_irq 函数用于释放之前通过 request_irq 函数注册的中断处理程序。它的作用是取消对中断的注册并释放相关的系统资源。下面是关于该函数的详细解释:
函数原型:
void free_irq(unsigned int irq, void *dev_id);
头文件:
#include <linux/interrupt.h>
函数功能:
free_irq 函数用于释放之前通过 request_irq 函数注册的中断处理程序。它会取消对中断的
注册并释放相关的系统资源,包括中断号、中断处理程序和设备标识等。
参数说明:
irq:要释放的中断号。
dev_id:设备标识,用于区分不同的中断请求。它通常是在 request_irq 函数中传递的设备
特定数据指针。
返回值:
free_irq 函数没有返回值。
1.8中断服务函数
中断处理程序是在中断事件发生时自动调用的函数。它负责处理与中断相关的操作,例如读取数据、清除中断标志、更新状态等。
irqreturn_t handler(int irq, void *dev_id) 是一个典型的中断服务函数的函数原型。下面对该函数原型及其参数进行详细解释:
函数原型:
irqreturn_t handler(int irq, void *dev_id);
函数功能:
handler 函数是一个中断服务函数,用于处理特定中断事件。它在中断事件发生时被操作系
统或硬件调用,执行必要的操作来响应和处理中断请求。
参数说明:
irq:表示中断号或中断源的标识符。它指示引发中断的硬件设备或中断控制器。
dev_id:是一个 void 类型的指针,用于传递设备特定的数据或标识符。它通常用于在中断
处理程序中区分不同的设备或资源。
返回值:
irqreturn_t 是一个特定类型的枚举值,用于表示中断服务函数的返回状态。它可以有以下
几种取值:
IRQ_NONE:表示中断服务函数未处理该中断,中断控制器可以继续处理其他中断请求。
IRQ_HANDLED:表示中断服务函数已成功处理该中断,中断控制器无需进一步处理。
IRQ_WAKE_THREAD:表示中断服务函数已处理该中断,并且请求唤醒一个内核线程来继续
执行进一步的处理。这在一些需要长时间处理的中断情况下使用。
在处理程序中,通常需要注意以下几个方面:
(1)处理程序应该尽可能地快速执行,以避免中断丢失或过多占用 CPU 时间。
(2)如果中断源是共享的,处理程序需要处理多个设备共享同一个中断的情况。
(3)处理程序可能需要与其他部分的代码进行同步,例如访问共享数据结构或使用同步
机制来保护临界区域。
(4)处理程序可能需要与其他线程或进程进行通信,例如唤醒等待的线程或发送信号给
其他进程。
二、实验程序编写
本实验将实现注册显示屏触摸中断,每按当触摸 LCD 显示屏就会触发中断服务函数,在中断服务函数中会打印申请的 GPIO 号和 This is irq_handler。
iTOP-RK3568 有 5 组 GPIO bank:GPIO0~GPIO4,每组又以 A0~A7, B0~B7, C0~C7, D0~D7 作为编号区分,常用以下公式计算引脚:
GPIO pin 脚计算公式:pin = bank * 32 + number //bank 为组号,number 为小组编号
GPIO 小组编号计算公式:number = group * 8 + X
LCD 触摸屏对应的中断引脚标号为 TP_INT_L_GPIO3_A5,对应的计算过程如下所示:
bank = 3;
//GPIO3_A5=> 3, bank ∈ [0,4]
group = 0;
//GPIO3_A5 => 0, group ∈ {(A=0), (B=1), (C=2), (D=3)}
X = 5;
//GPIO3_A5 => 5, X ∈ [0,7]
number = group * 8 + X = 0 * 8 + 5 =5
pin = bank*32 + number= 3 * 32 + 5 = 101;
得到中断引脚的引脚标号后,下面开始编写对应的驱动程序,编写完成的 interrupt.c 如下所示:
#include <linux/module.h>
#include <linux/init.h>
#include <linux/gpio.h>
#include <linux/interrupt.h>
#define GPIO_PIN 101
// 中断处理函数
static irqreturn_t gpio_irq_handler(int irq, void *dev_id)
{
printk(KERN_INFO "Interrupt occurred on GPIO %d\n", GPIO_PIN);
printk(KERN_INFO "This is irq_handler\n");
return IRQ_HANDLED;
}
static int __init interrupt_init(void)
{
int irq_num;
printk(KERN_INFO "Initializing GPIO Interrupt Driver\n");
// 将 GPIO 引脚映射到中断号
irq_num = gpio_to_irq(GPIO_PIN);
printk(KERN_INFO "GPIO %d mapped to IRQ %d\n", GPIO_PIN, irq_num);
// 请求中断
if (request_irq(irq_num, gpio_irq_handler, IRQF_TRIGGER_RISING, "irq_test", NULL) != 0){
printk(KERN_ERR "Failed to request IRQ %d\n", irq_num);
// 请求中断失败,释放 GPIO 引脚
gpio_free(GPIO_PIN);
return -ENODEV;
}
return 0;
}
static void __exit interrupt_exit(void)
{
int irq_num = gpio_to_irq(GPIO_PIN);
// 释放中断
free_irq(irq_num, NULL);
printk(KERN_INFO "GPIO Interrupt Driver exited successfully\n");
}
module_init(interrupt_init);
module_exit(interrupt_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("topeet");
2.1运行测试
使用以下命令进行驱动模块的加载
insmod interrupt.ko
可以看到驱动加载之后,打印了“Initializing GPIO Interrupt Driver”表示程序加载成功了,在后面又打印了 gpio 映射后的中断请求号为 113,然后触摸 LCD 屏,触发中断服务程序,打印如下图所示: