概述Linux中断子系统

举报
tankaro 发表于 2024/06/19 09:36:03 2024/06/19
【摘要】 概述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)

点个外卖时间,我把「软中断」搞懂了 (baidu.com)

linux中断申请之request_threaded_irq - 随风飘落的雨滴 - 博客园 (cnblogs.com)

Linux中断子系统 - 随笔分类 - LoyenWang - 博客园 (cnblogs.com)

ARM GICv3 ITS介绍及代码分析_gic its-CSDN 博客

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。