概述Linux中断子系统
概述
Linux中断子系统在整个Linux系统中占据重要地位,一个合格的Linux驱动工程需要对Linux kernel中断子系统有深刻的理解。
Linux中断子系统涵盖了众多知识点技术细节,这篇文章只梳理其中的脉络,使自己和读者掌握中断子系统的精华,各种技术细节则再后面专题研究。
Linux中断子系统支持X86、Arm和RISC-V三种芯片架构。每种架构对于中断处理各不相同,比如X86有单核PIC架构,多核APIC架构;Arm GIC架构和RISC-V PLIC架构。这里不过多纠结各自细节,一般情况下中断驱动开发过程中,未必会有机会去深究这些细节,仅仅简单使用而已。
Linux中断管理机制一般可以分为四层:
• 硬件层,主要包括CPU+中断控制器+设备模块+三者之间的硬件物理连接线。
• 硬件相关软件架构层,主要是与芯片架构相关的代码,还包括中断控制器本身的代码实现。
• 系统中断软件通用层,比如与硬件无关的,所有硬件通用的Linux系统中断架构代码。
• 设备驱动层,主要是各类设备驱动代码,实现中断申请注册和中断服务程序。
网上总结的Linux中断子系统的重要性有如下几点:
• 可以正确的使用Linux kernel提供的相关中断API,知其然更知其所以然。
• 可以根据每个驱动模块本身的特点使用正确的方法来处理中断,比如根据响应时间要求使用softirq还是workqueue;比如GPIO中断及时响应,加防抖动处理;比如中断如何与同步机制结合更好地保护和使用驱动中的共享资源;比如中断需要与用户层进行数据交互,需要支持睡眠机制则选择workqueue来处理中断下半部。
• 了解中断运行的测试方法。
• 评估中断性能和明确中断性能优化方向。
中断硬件层
目前Linux上流行的芯片架构有三种,X86,ARM和RISC-V。三种芯片架构的中断硬件层本质相同,但具体实现有稍微差异,从软件开发者角度基本可以忽略。
X86中断硬件层框架图
Arm
ARM GIC v2最多支持8个核心, gic400是ARMGIC V2版本的中断控制IP核。当GIC接收到外部中断信号后就会报给ARM内核,但是ARM处理器只提供四个信号给GIC:FIQ、IRQ、VIRQ和VFIQ。
FIQ:快速IRQ;IRQ:interrupt Request;VFIQ:虚拟FIQ;VIRQ:虚拟快速IRQ。
下图1是ARM GIC的示意图,下图2是ARM v2提供的中断框架图,下图3是ARM GIC支持中断虚拟化,下图4是ARM v4提供的中断框架图。
图1
图2
图3
图4
RISC-V
暂略
硬件相关软件架构层
X86
暂略
Arm64
下图是kernel4.14 代码流程图,这里使用网上作者的图来说明Arm64硬件相关软件架构层相关逻辑。对比自己手里的kernel版本6.1,Linux框架中断实现有了较大变化。通过查询其他博主的文章,我们可以发现每个kernel版本的处理流程都有改变。所以我们这里不在概述中深究每个版本的细节,而是将这个工作放到后期的专题中研究。同时笔者认为要开展此专题研究的前提条件是了解Arm64汇编的调试手段。
RISC_V
暂略
系统中断软件通用层
GIC总数据结构
下图是kernel4.14的结构体struct irq_desc,后面kernel6.1或者5.10或者5.15在专题研究。笔者总结的非常到位这里直接copy过来。
Linux内核的中断处理,围绕着中断描述符结构struct irq_desc展开,内核提供了两种中断描述符组织形式:
• 打开CONFIG_SPARSE_IRQ宏(中断编号不连续),中断描述符以radix-tree来组织,用户在初始化时进行动态分配,然后再插入radix-tree中;
• 关闭CONFIG_SPARSE_IRQ宏(中断编号连续),中断描述符以数组的形式组织,并且已经分配好;
不管哪种形式,最终都可以通过linux irq号来找到对应的中断描述符;
上图的左侧灰色部分,主要在中断控制器驱动中进行初始化设置,包括各个结构中函数指针的指向等,其中struct irq_chip用于对中断控制器的硬件操作,struct irq_domain与中断控制器对应,完成的工作是硬件中断号到Linux irq的映射;
上图的上侧灰色部分,中断描述符的创建(这里指CONFIG_SPARSE_IRQ),主要在获取设备中断信息的过程中完成的,从而让设备树中的中断能与具体的中断描述符irq_desc匹配;
上图中剩余部分,在设备申请注册中断的过程中进行设置,比如struct irqaction中handler的设置,这个用于指向我们设备驱动程序中的中断处理函数了;
中断的处理主要有以下几个功能模块:
• 硬件中断号到Linux irq中断号的映射,并创建好irq_desc中断描述符;
• 中断注册时,先获取设备的中断号,根据中断号找到对应的irq_desc,并将设备的中断处理函数添加到irq_desc中;
• 设备触发中断信号时,根据硬件中断号得到Linux irq中断号,找到对应的irq_desc,最终调用到设备的中断处理函数;
这里特别说明一点,实际使用中,很多功能相近中断会被集成到一个IRQ中,驱动收到IRQ中断,需要继续判断特定寄存器某位bit的值来进一步确认是哪个子中断IRQ,DMA是一个典型例子,一般DMA有4个channel,中断处理handler中需要继续判断是哪个channel的中断。PCIe的MSI和MSI-x中断也类似。
DTS配置文件
上图中DTS组成由三部分,第一部分很好理解。
.compatible字段:用于与具体的驱动代码匹配,比如上图中Arm,gic-v3,根据这个名字找到drivers/irqchip/irq-gic-v3.c,这个文件是Linux kernel 中 GIC v3 版本中断控制器的代码文件。通过下图可以确定gic代码实现在此文件中。
interrupt-cells = <0x03>;字段:用于指定中断源所需的单元个数,这里是3。
interrupts = <0x01 0x09 0x04>;字段第一个单元0x1表示中断类型是PPI,0代表SPI,1代表PPI,第二个单元0x09代表硬件中断号,第三个单元表示中断触发类型。
interrupt-controller;字段表示该设备是一个中断控制器,外设可以连接到该中断控制器上。
reg = <0x00 0xfe600000 0x00 0x10000 0x00 0xfe680000 0x00 0x100000>;字段:描述中断控制器的地址信息以及地址范围,比如图中分别指定了GICD(GIC Distributor)和GICC(GIC CPU Interface)的地址信息。
第二,第三部分则时两个its的配置信息。its(Interrupt Translation Service)时GICv3中一种支持LPI中断的可选机制,简单来说就时ITS负责接收外设中断,并将它们转化为LPI中断发送到相应的Redistributor来处理。LPI(localit-specific peripheral interrupts)是GICv3中定义的一种新型中断类型,LPI是一种基于消息的中断,中断信息不再通过中断线进行传递。LPI和ITS,后面有时间专题研究。
DTS如何编译成DTB文件,DTB文件保存在文件系统哪里,如何通过uboot传递给内核,内核启动后如何解析DTB文件,系统如何使用,后面有时间专题研究。
GIC框架代码启动流程
下图是kernel4.14的GIC框架代码启动流程图,这里有几个关键点:
• IRQ table放在vmlinux.lds链接文件中;
• IRQCHIP_DECLARE声明的回调函数,也就是gic_of_init,这个函数就是GIC驱动的初始化入口函数。
• 设置中断处理handler,中断来了调用此handler,随即调用驱动代码中实际的回调函数,完成此中断处理。
• 初始化irq_chip结构体,此结构描述的是中断控制器的底层操作函数集,这些函数集最终完成对控制器硬件的操作;
• 初始化irq_domain两个结构体,此结构用于硬件中断号和Linux IRQ中断号(virq,虚拟中断号)之间的映射。
中断号
硬件中断号
这里可以理解为中断向量表中中断的位置号,它是硬件连接线的序号。Arm架构是从上面DTS配置文件中获取。
软件中断号
Arm中断号简单说明如下:
中断号 |
名称 |
用途 |
0-15 |
软件触发中断SGI |
定时器,异步事件,多功能,多线程和并行计算 |
16-31 |
私有外设中断PPI |
ARM系统分为两种,用于连接物理内核的中断和用于连接虚拟平台的中断 |
32-1019 |
共享外设中断SPI |
多个共享一个外部中断线减少系统中断线数量,提高系统性能和效率,确保同时使用共享SPI中断的外设之间没有冲突和竞争。 |
IRQ Domain模块将硬件中断号转换为Linux系统中的中断号,参考下图
这里每个中断控制器对应一个IRQ Domain,IRQ Domain有三种映射方法。
• linear map:Linux系统软件中断等于硬件中断号加上一个固定偏移,成为线性映射。
• hierarchy(tree) map:硬件中断号有可能很大,所以选择层次(树)映射方式。
• no map:硬件中断号不映射直接是Linux软件中断号。
如何判断自己的系统使用哪种映射方式呢?.config文件中有相应的配置,CONFIG_IRQ_DOMAIN_HIERARCHY与下图中的代码呼应。
下图是kernel4.14内核硬件中断转换为软件中断号流程,
中断注册API
中断注册一般使用request_threaded_irq和request_irq函数,两者从软件上来看,好像差不多,一个没有提前声明thread而已,其实通过网友实验总结如下。这里没有说明信号间隔很短有多短,结合本身实践,使用request_irq函数大约26ms一帧(根据实际情况来决定一帧多长时间),audio本身并没有出现中断丢失的情况,大家可以参考。
extern int __must_check request_threaded_irq(unsigned int irq, irq_handler_t handler,
irq_handler_t thread_fn, unsigned long flags, const char *name, void *dev);
static inline int __must_check request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev)
{
return request_threaded_irq(irq, handler, NULL, flags, name, dev);
}
后续多尝试使用devm_request_threaded_irq和devm_request_irq函数注册中断。
下图是kernel4.14中断注册处理流程图。
中断上半部和下半部
Linux中断会打断内核中进程的正常调度和运行,中断服务程序执行时间不能太长,否则会引起系统异常。一般中断处理程序往往需要处理大量逻辑,所以Linux将中断处理程序分解为两部分:上半部(top half)和下半部(bottom half)。
上半部用于处理尽快少的而且毕竟紧急的功能,一般只是简单地的读取寄存器的重点状态并清楚中断后就完成处理。这样上半年执行素服很快,可以服务更多的中断请求。
下半部往往是中断处理的中心,用于完成中断服务程序中的绝大多数工作,下半部可以被新的中断上半部打断,注意避免中断重入问题。而且很多情况下下半部需要与用户空间进行数据交互,所以耗时更多。
中断上半部是中断服务程序必须部分,但是下半部是必要部分。
中断下半部是中断程序中的重点,有三种方式:softirq,tasklet和workqueue。从一般模块驱动开发者角度来说,workqueue使用最多,tasklet其次,softirq最少。
Workqueue
工作队列的执行上下文是内核线程,所以处理逻辑需要被重新调度甚至休眠,则需要使用workqueue方式,工作队列可以从应用层获取大量数据,可以执行阻塞式I/O操作,否则使用tasklet/softirq。
Softirq
Lixnu kernel中softirq需要静态实现,对于模块驱动来说softirq实现过于复杂,非系统驱动不建议使用softirq。目前内核中只有net和SCSI模块使用了softirq机制。
Tasklet
Tasklet是一种特殊的软中断,同一时刻同一个tasklet只能在一个CPU执行,不同的tasklet可以在不同的CPU上执行。而软中断同一时刻可以在不同的CPU上并行执行,因此软中断必须考虑重入问题。
引入tasklet,主要是考虑支持SMP,提高SMP多CPU的利用;相同的tasklet不能同时执行,需要排队等待上一个tasklet执行完成。不允许两个相同类似的tasklet同时运行,即使在不同的处理器CPU上。
同步机制
中断上半部,尽量使用spinlock
中断下半部尽量使用mutex
中断服务处理程序
下图是kernel4.14中断服务处理程序流程图。硬件中断发生后,各种电气信号处理完成,系统发送异常跳转到异常向量表中,根据中断后的各种有效信息,逐级调用generic_handle_irq中进行中断处理。
下图是generic_handle_irq函数的处理流程图:
根据设置进行中断线程化处理(workqueue处理流程)流程图如下:
设备驱动代码层
因为硬件中断依赖实际硬件不通用,所以这里专门找到一个类似softirq的例子或者说软件模拟硬件中断的例子,理解这个例子基本搞清楚中断子系统。
系统准备
X86_64 CPU和 kernel5.15代码修改如下:
test@L14:~/zenghua.gao/linux_kernel/linux-source-5.15.152$ git diff arch/X86/Kconfig
diff --git a/arch/X86/Kconfig b/arch/X86/Kconfig
index b0e7b3c5a..a60ca27c5 100644
--- a/arch/X86/Kconfig
+++ b/arch/X86/Kconfig
@@ -20,7 +20,8 @@ config X86_32
select MODULES_USE_ELF_REL
select OLD_SIGACTION
select ARCH_SPLIT_ARG64
-
+config INTERRUPT_IRQ0_11
+ def_bool y
config X86_64
def_bool y
depends on 64BIT
test@L14:~/zenghua.gao/linux_kernel/linux-source-5.15.152$ git diff arch/X86/kernel/irqinit.c
diff --git a/arch/X86/kernel/irqinit.c b/arch/X86/kernel/irqinit.c
index c68366687..6ae8e2bed 100644
--- a/arch/X86/kernel/irqinit.c
+++ b/arch/X86/kernel/irqinit.c
@@ -50,6 +50,9 @@ DEFINE_PER_CPU(vector_irq_t, vector_irq) = {
[0 ... NR_VECTORS - 1] = VECTOR_UNUSED,
};
+#ifdef CONFIG_INTERRUPT_IRQ0_11
+EXPORT_SYMBOL(vector_irq);
+#endif
void __init init_ISA_irqs(void)
{
struct irq_chip *chip = legacy_pic->chip;
test@L14:~/zenghua.gao/linux_kernel/linux-source-5.15.152$ git diff kernel/irq/irqdesc.c
diff --git a/kernel/irq/irqdesc.c b/kernel/irq/irqdesc.c
index 7a45fd593..bb8377a32 100644
--- a/kernel/irq/irqdesc.c
+++ b/kernel/irq/irqdesc.c
@@ -350,7 +350,13 @@ static void irq_insert_desc(unsigned int irq, struct irq_desc *desc)
{
radix_tree_insert(&irq_desc_tree, irq, desc);
}
-
+#ifdef CONFIG_INTERRUPT_IRQ0_11
+struct irq_desc *irq_to_desc(unsigned int irq)
+{
+ return radix_tree_lookup(&irq_desc_tree, irq);
+}
+EXPORT_SYMBOL(irq_to_desc);
+#else
struct irq_desc *irq_to_desc(unsigned int irq)
{
return radix_tree_lookup(&irq_desc_tree, irq);
@@ -358,6 +364,7 @@ struct irq_desc *irq_to_desc(unsigned int irq)
#ifdef CONFIG_KVM_BOOK3S_64_HV_MODULE
EXPORT_SYMBOL_GPL(irq_to_desc);
#endif
+#endif
static void delete_irq_desc(unsigned int irq)
{
驱动模块代码
中断号定义:
#define IRQ_NO (11)
#define INTERRUPT_FIRST_EXTERNAL_VECTOR (0x20)
#define INTERRUPT_ISA_VECTOR (0x10)
#define INTERRUPT_11_VECTOR_IRQ (INTERRUPT_FIRST_EXTERNAL_VECTOR + INTERRUPT_ISA_VECTOR + IRQ_NO)
中断注册:
if (request_irq(IRQ_NO, irq_handler, IRQF_SHARED, "etx_device", (void *)(irq_handler)))
{
pr_err("[%d %s] request_irq(IRQ_NO=%d) failed \n", __LINE__, __func__, IRQ_NO);
goto err_request_irq;
}
中断处理程序:
static irqreturn_t irq_handler(int irq,void *dev_id)
{
pr_info("[%s +%d %s] Shared IRQ: Interrupt Occurred IRQ_NO=%d\n", __FILE__, __LINE__, __func__, IRQ_NO);
return IRQ_HANDLED;
}
使能中断代码:
desc = irq_to_desc(IRQ_NO);
if (!desc)
{
return -EINVAL;
}
__this_cpu_write(vector_irq[INTERRUPT_11_VECTOR_IRQ], desc);
asm("int $0x3B"); // Corresponding to irq 11
运行结果:
中断性能测试,优化
中断性能测试方法参考如下链接,后面有时间再做专题研究。这里说一下中断性能测试的意义,笔者认为中断处理都有一定的规律,要么是节拍型的比如audio中断,多少毫秒一帧,这里的毫秒数据必须满足一定的范围,超过这个范围会导致声音播放处理异常,而且声音的异常对于用户非常敏感,必须根据实际情况保证声音播放正常。
再比如DMA数据传输,对于固定大小的数据来说,DMA中断也应该是节拍型的,而且这个时间范围波动应该更小才对,一旦出来不定时的异常,或者有规律的波动,则需要检查DMA控制器的实现是否有问题。
再比如就算是按键这样的中断处理程序,其实也是需要符合一定的节拍的,比如为了防抖和防止按键丢失,这个节拍的时间间隔波动也不能太大,否则也会导致用户体验变差。
最后当一个功能需要两个中断才能继续推进时,则需要注意两个中断配合使用情况。
ARM -Linux中断系统 - ArnoldLu - 博客园 (cnblogs.com)
参考链接
ARM GIC中断学习(一) - 知乎 (zhihu.com)
ARM -Linux中断系统 - ArnoldLu - 博客园 (cnblogs.com)
linux中断申请之request_threaded_irq - 随风飘落的雨滴 - 博客园 (cnblogs.com)
- 点赞
- 收藏
- 关注作者
评论(0)