中断子系统(一)IRQ Domain

前言#

在现代计算机系统中,中断模块的硬件越来越复杂,有可能有多个中断控制器(Interrupt Controller, IC)之间进行级联从而拓展可以管理的中断源数量。这就会产生几个问题,每个IC上都连接着多个设备,IC会给irq line连接的每一个设备分配一个硬件中断请求号(HW interrupt number,hwirq),不同的IC之间是独立的,因此hwirq会有可能复用,CPU就无法仅依赖连接的root IChwirq来区分中断源。

此外,内核并不理解hwirq,内核通过中断描述符表( interrupt descriptor table, IDT)来管理所有的中断处理函数(Interrupt Service Routine, ISR),每一个中断描述符中包含了该中断的描述信息和处理函数,而定位一个中断描述符依赖于IRQ number,这是一个逻辑中断号(下文缩写为virq)。

因此,内核在进行中断处理前需要识别不同的设备中断源才能找到并执行设备的ISR,这就需要将接收到的hwirq转化为virq。本文介绍了中断子系统是如何对该部分进行抽象建模、屏蔽不同硬件之间的差异形成通用的中断处理模块的,在这个通用的中断处理模块中hwirq又是如何翻译为virq的。

Note: 本文避免讨论与硬件或体系结构相关的细节,专注于通用的中断处理模块,另外本文的源码解读基于Linux 5.10

本文涉及的相关名词缩写如下:

  • IC: Interrupt Controller, 中断控制器
  • hwirq: Hardware Interrupt Number, 硬件中断请求号
  • virq: Virtual Interrupt Number, 虚拟中断请求号
  • ISR: Interrupt Service Routine, 中断服务程序
  • GIC: Generic Interrupt Controller, 通用中断控制器

中断源识别的例子#

为了帮助理解内核代码,首先我们先梳理一下CPUIC之间是如何连接的,以及在这个架构下内核是如何识别中断源的,有了这个概念以后再去理解内核代码就更加容易。

irq line

irq line (hwirq-1)

irq line

irq line

irq line

irq line (hwirq-2)

irq line

Device-A

Device-B

Device-C

...

Device-D

Device-E

Interrupt Controller-A

Interrupt Controller-B

root Interrupt Controller

CPU

三个中断控制器级联的例子

如图所示,有三个IC进行级联(级联呈现树状结构),root IC作为根IC连接到CPU上,假设设备Device-D发起了一个中断请求(如图中虚线所示),此时CPU会检测到root IC的电平变化并从root IC的寄存器中取出硬件中断号hwirq-1(对应IC-B连接到root ICirq line),此时CPU根据hwirq-1找到并执行IC-B的中断处理函数handler-Bhandler-B是一个特殊的ISR,他处理的是IC的中断请求而不是普通设备的中断请求,handler-B会从IC-B的寄存器中找到此时真正发起中断请求的设备的hwirq(即hwirq-2),并将hwirq-2翻译为Device-D对应的virq,并执行对应的ISR,至此就完成了一次中断请求的识别和执行。

在这个例子中可以很清晰的看到找到Device-DISR前需要逐级地对hwirq进行转化,最终转化为设备中断的virq,定位到设备的中断描述符,执行相应的ISR。显然在每一级转化中(hwirq转化为virq)需要依赖于一个映射表,每一个IC需要维护一个自己的映射表,维护的这个映射表在内核中由结构体struct irq_domain实现。

irq_domain#

struct irq_domain 结构体#

irq_domain可以理解为一个KV数据库,专门用于在某个IC内部查询hwirq对应的virq

struct irq_domain {
    // 链表节点,所有的irq_domain会放在一个全局的链表中
    struct list_head link;
    // irq_domain name
    const char *name;
    // irq_domain的操作函数集合
    const struct irq_domain_ops *ops;   
    // `IC`私有数据,不同控制器类型自定义
    void *host_data; 
    /* Optional data */
    // 对应的`IC`设备信息
    struct fwnode_handle *fwnode;
    // 存储KV的数据结构
    irq_hw_number_t hwirq_max;
    unsigned int revmap_direct_max_irq;
    unsigned int revmap_size;
    struct radix_tree_root revmap_tree;
    struct mutex revmap_tree_mutex;
    unsigned int linear_revmap[];
};

irq_domain结构体其中有一些关键的成员变量:

  • linkirq_domain会被放置在一个全局的链表irq_domain_list中进行管理
  • opsops中定义了一系列的callback函数,这些函数是与具体的硬件相关的,比如在mapping(建立映射)的过程中除了要在irq_domian中记录KV关系之外还需要进行一些硬件相关的操作,一个具体的例子就是IC可以依据hwirq的范围设置不同的handler等等,在这些IC驱动自定义的callback函数中可以进行一些非通用的操作。
struct irq_domain_ops {
    int (*match)(struct irq_domain *d, struct device_node *node,
             enum irq_domain_bus_token bus_token);
    int (*select)(struct irq_domain *d, struct irq_fwspec *fwspec,
              enum irq_domain_bus_token bus_token);
    int (*map)(struct irq_domain *d, unsigned int virq, irq_hw_number_t hw);
    void (*unmap)(struct irq_domain *d, unsigned int virq);
    int (*xlate)(struct irq_domain *d, struct device_node *node,
             const u32 *intspec, unsigned int intsize,
             unsigned long *out_hwirq, unsigned int *out_type);
};
  • host_data:存放了一些IC的私有数据,由IC驱动进行自定义,可能会在callback函数中使用
  • fwnodeIC设备信息
  • revmap*:真正存储KV映射的数据结构,有线性映射和raidx-tree两种模式,根据映射关系是否稀疏可以选择其中一种,revmap_sizelinear_revmap用于线性存储,revmap_tree用于radix-tree,还有一种直接映射的场景(hwirqvirq),此时使用revmap_direct_max_irq字段。

irq_domain的创建和初始化#

irq_domain有一系列的创建函数,用于linearnomaplegacyradix-tree各种模式,这些函数会在IC驱动程序初始化相关的代码中被调用,这些函数都是通过调用__irq_domain_add()实现,但是在参数上有一些差异,可以结合上一小节中关于revmap*变量的说明进行阅读。

static inline struct irq_domain *irq_domain_add_linear(struct device_node *of_node,
                     unsigned int size,
                     const struct irq_domain_ops *ops,
                     void *host_data)
{
    return __irq_domain_add(of_node_to_fwnode(of_node), size, size, 0, ops, host_data);
}
static inline struct irq_domain *irq_domain_add_nomap(struct device_node *of_node,
                     unsigned int max_irq,
                     const struct irq_domain_ops *ops,
                     void *host_data)
{
    return __irq_domain_add(of_node_to_fwnode(of_node), 0, max_irq, max_irq, ops, host_data);
}
static inline struct irq_domain *irq_domain_add_legacy_isa(
                struct device_node *of_node,
                const struct irq_domain_ops *ops,
                void *host_data)
{
    return irq_domain_add_legacy(of_node, NUM_ISA_INTERRUPTS, 0, 0, ops,
                     host_data);
}
static inline struct irq_domain *irq_domain_add_tree(struct device_node *of_node,
                     const struct irq_domain_ops *ops,
                     void *host_data)
{
    return __irq_domain_add(of_node_to_fwnode(of_node), 0, ~0, 0, ops, host_data);
}

系统的启动流程 - irq_domain的创建和映射添加#

在系统启动时会创建好所有的irq_domain,但是在此之前内核需要知道硬件之间的拓扑结构,掌握触发中断的设备和IC之间是如何连接的,这部分信息需要通过DTS文件(Device Tree Source)或者ACPI文件(Advanced Configuration and Power Interface)来描述,DTS常用于嵌入式系统,DTS文件存在于内核源码之中,在进行内核编译时会编译成.dtb文件,放置在固定的目录,而ACPI用于传统PC和服务器,ACPI文件放置在BIOS或者UEFI固件之中。总之系统在启动时能够获取到硬件之间的拓扑信息,在这个过程中会进行irq_doamin的创建和初始化,并对每一个能够发起中断的设备分配一个virq,然后建立映射并添加到对应的irq_domain。这里对具体的初始化流程不做深入分析。

对于设备驱动来说,创建中断映射前并不知道自身在连接的IC中对应的hwirq,但映射的建立的需要irq_domainhwirqvirq三个参数,建立映射相关的API如下,分别用于创建一个映射和创建多个连续映射。

int irq_domain_associate(struct irq_domain *domain, unsigned int virq,
             irq_hw_number_t hwirq);
void irq_domain_associate_many(struct irq_domain *domain, unsigned int irq_base,
                   irq_hw_number_t hwirq_base, int count);

因此在设备驱动中创建一个映射一般是调用irq_of_parse_and_map()函数,该函数对irq_domain_associate()进行了多层的封装,以device_node作为参数输入尝试建立一个映射并返回virq,。

unsigned int irq_of_parse_and_map(struct device_node *dev, int index)
{
    struct of_phandle_args oirq;

    if (of_irq_parse_one(dev, index, &oirq))
        return 0;

    return irq_create_of_mapping(&oirq);
}
EXPORT_SYMBOL_GPL(irq_of_parse_and_map);

irq_of_parse_and_map完成映射建立需要三步:

  • 获取设备对应的hwirq:需要由ICirq_domain来识别设备树节点信息,得到对应hwirq,这个过程由irq_domain_translate()函数完成,涉及到ops->xlate()这个callback函数,如果IC有配置自己的翻译方法则进行使用IC自己的方式,否则就从设备的中断描述信息中获取(在DTS或者ACPI中定义)。
static int irq_domain_translate(struct irq_domain *d,
                struct irq_fwspec *fwspec,
                irq_hw_number_t *hwirq, unsigned int *type)
{
    if (d->ops->xlate)
        return d->ops->xlate(d, to_of_node(fwspec->fwnode),
                     fwspec->param, fwspec->param_count,
                     hwirq, type);

    /* If domain has no translation, then we assume interrupt line */
    *hwirq = fwspec->param[0];
    return 0;
}
  • 另外还需要从内核中分配一个有效的中断描述符,通过irq_domain_alloc_descs()函数完成,该函数可以为一个指定的virq或者由内核分配的virq创建中断描述符,总之如果分配成功可以获取一个有效的virq
int irq_domain_alloc_descs(int virq, unsigned int cnt, irq_hw_number_t hwirq,
               int node, const struct irq_affinity_desc *affinity)
{
    unsigned int hint;

    if (virq >= 0) {
        virq = __irq_alloc_descs(virq, virq, cnt, node, THIS_MODULE,
                     affinity);
    } else {
        hint = hwirq % nr_irqs;
        if (hint == 0)
            hint++;
        virq = __irq_alloc_descs(-1, hint, cnt, node, THIS_MODULE,
                     affinity);
        if (virq <= 0 && hint > 1) {
            virq = __irq_alloc_descs(-1, 1, cnt, node, THIS_MODULE,
                         affinity);
        }
    }

    return virq;
}
  • 最后通过irq_domain_associate()hwirqvirq间的映射添加到irq_domain中。

hwirq的翻译流程#

通过以上的内容应该对内核对硬件层面的中断管理有了大致的了解,假设系统已经正常启动,内核将root IC中的hwirq翻译为最底层设备的virq还需要依赖于次级ICSecondary IC)的ISRroot IC不连接到到任何其他的IC上,因此仅作为IC使用,但是次级IC除了接受其他设备的连接以外自身还需要连接到其他的IC上,因此就具备了设备和IC两重身份,不仅需要管理一个irq_domain,还需要注册自己的ISR

这里以GIC级联为例,看看root GICSecondary GIC之间的处理是如何联动的。首先是root GIC的处理函数gic_handle_irq,该函数在CPU收到了来自中断分发器(Interrupt Distributor)的中断时执行,该函数会读取GIC的中断识别寄存器(Interrupt ACKnowledge Register, IAR)并赋值给irqstat,然后从irqstat中获取hwirq,调用handle_domain_irq()进行处理。

static void __exception_irq_entry gic_handle_irq(struct pt_regs *regs)
{
    u32 irqstat, irqnr;
    struct gic_chip_data *gic = &gic_data[0];
    void __iomem *cpu_base = gic_data_cpu_base(gic);
    ...
    do {
        irqstat = readl_relaxed(cpu_base + GIC_CPU_INTACK);
        irqnr = irqstat & GICC_IAR_INT_ID_MASK;

        if (unlikely(irqnr >= 1020))
            break;
        ....
        handle_domain_irq(gic->domain, irqnr, regs);
    } while (1);
}

handle_domain_irq()调用了__handle_domain_irq(),该函数增加了一个参数lookup=true,表示需要通过查找映射关系将hwirq转化为virqirq_find_mapping()就是从irq_domain中查找映射关系,得到virq后调用generic_handle_irq()找到virq对应的中断描述符并执行对应的ISR,在当前场景下也就是执行次级GICISR

int __handle_domain_irq(struct irq_domain *domain, unsigned int hwirq,
            bool lookup, struct pt_regs *regs)
{
    ...
    if (lookup)
        irq = irq_find_mapping(domain, hwirq);

    if (unlikely(!irq || irq >= nr_irqs)) {
        ack_bad_irq(irq);
        ret = -EINVAL;
    } else {
        generic_handle_irq(irq);
    }
    ...
}

次级GIC注册的ISR如下,它以次级GIC的中断描述符作为参数,该函数会获取寄存器的地址信息,读取次级GIC的中断识别寄存器并得到hwirq,然后通过次级GICirq_domain进行翻译得到下一级设备的virq,假设下一级设备就是最底层的网卡设备,此时根据该virq就能执行网卡的ISR,如果不是则继续转到次次级的GIC中断处理函数中。

static void gic_handle_cascade_irq(struct irq_desc *desc)
{
    struct gic_chip_data *chip_data = irq_desc_get_handler_data(desc);
    struct irq_chip *chip = irq_desc_get_chip(desc);
    unsigned int cascade_irq, gic_irq;
    unsigned long status;
    ...
    status = readl_relaxed(gic_data_cpu_base(chip_data) + GIC_CPU_INTACK);

    gic_irq = (status & GICC_IAR_INT_ID_MASK);
    if (gic_irq == GICC_INT_SPURIOUS)
        goto out;

    cascade_irq = irq_find_mapping(chip_data->domain, gic_irq);
    if (unlikely(gic_irq < 32 || gic_irq > 1020)) {
        handle_bad_irq(desc);
    } else {
        isb();
        generic_handle_irq(cascade_irq);
    }
    ...
}

总结#

内核对IC的建模和抽象实现了多级级联场景下依然能够正确的识别中断设备,执行正确的中断处理函数,其中最关键的设计就是irq_domainvirq。本文的内容主要分析了内核在中断处理函数执行之前做的一些通用工作,后续将会对中断处理相关的内容进行分析,包括中断描述符、软中断等内容。

posted @   ZouTaooo  阅读(421)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示
主题色彩