首页 > 其他分享 >中断子系统(一)IRQ Domain

中断子系统(一)IRQ Domain

时间:2024-04-02 19:24:45浏览次数:13  
标签:Domain struct IRQ virq domain hwirq irq IC 子系统

前言

在现代计算机系统中,中断模块的硬件越来越复杂,有可能有多个中断控制器(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)。

因此,内核在进行中断处理前需要识别不同的设备中断源的同时需要将设备的hwirq转化为virq,才能找到并执行设备的ISR。本文介绍了中断子系统是如何对该部分进行抽象建模、屏蔽不同硬件之间的差异形成通用的中断处理模块的,在这个通用的中断处理模块中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之间是如何连接的,以及在这个架构下内核是如何识别中断源的,这样再去理解内核代码就更加容易。

graph BT subgraph dev1[Device-A] dev2[Device-B] dev3[Device-C] xxx[...] dev4[Device-D] dev5[Device-E] end subgraph ic2[Interrupt Controller-A] ic3[Interrupt Controller-B] ic1[root Interrupt Controller] ic2 -- irq line --> ic1 ic3 -.->|"irq line (hwirq-1)"| ic1 end subgraph v1[CPU] end dev1 -- irq line --> ic2 dev2 -- irq line --> ic2 dev3 -- irq line --> ic2 dev4 -.->|"irq line (hwirq-2)"| ic3 dev5 -- irq line --> ic3 ic1 -.-> v1
三个中断控制器级联的例子

如图所示,有三个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内部进行hwirqvirq的转化。

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的创建和初始化,并将每一个能够发起中断的设备建立映射并添加到对应的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有配置自己的翻译方法则进行翻译,否则就从设备的中断描述信息中获取。
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
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的翻译流程

通过以上的内容应该对内核对硬件层面的中断管理有了大致的了解,假设系统已经正常启动,内核完成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,获取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,该ISR的入参是次级GIC的中断描述符,在irq_desc中可以找到次级GICirq_domain以及IC寄存器的地址信息,可以读取次级GIC的中断识别寄存器中的hwirq并进行翻译。

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);
    }
    ...
}

标签:Domain,struct,IRQ,virq,domain,hwirq,irq,IC,子系统
From: https://www.cnblogs.com/wodemia/p/18111318

相关文章

  • input子系统三
    参考资料:正点原子Linux设备驱动韦东山第二期 图来源于100ask: 一个设备链表,一个handler链表,左边是设备层,右侧是handler处理层,用来处理各种事件。handler处理层内核已经做好了。驱动一个Input设备只需要构造一个input_dev,核心层来注册input_dev和注册input_handler就可以......
  • input子系统二
    参考资料:正点原子Linux设备驱动韦东山第二期 触摸屏分为电阻屏和电容屏。电阻屏结构简单,以前很流行;电容屏支持多点触摸,现在手机基本都是使用电容屏注:LCD和touchscreen不是一个东西,制作触摸屏时特地把它的尺寸做得跟LCD大小一模一样,并不是将触摸屏覆盖在LCD上 电阻屏:电......
  • input子系统一
    参考资料:正点原子Linux设备驱动韦东山第二期 input子系统框架图: 用户空间:用户空间可直接访问驱动节点,/dev/input/event0,1,2,也可以通过1tslib/libinput库来使用输入设备输入系统事件层:处理核心层上报的输入事件,给用户层提供访问接口输入系统核心层:承上启下,接收来自底层的......
  • 电子系统集成
    单路DAC和多路模拟开关生成多路信号源题目:利用一个DAC0832和一个模拟开关设计8路模拟信号源,要求控制器为8051单片机,每路信号源的输出电压为0~5V,信号频率均为1KHz,输出精度优于2%,要求画出原理图(电源部分可不画),每路信号源一个周期内的量化点数可以一样,计算模拟开关的切换速度,写出关......
  • 转:cpu性能和功耗相关的内核子系统
    http://arthurchiao.art/blog/linux-cpu-2-zh/ 1.1调度器:时分复用+任务调度——sched1.3有任务:用哪个频率执行任务?——cpufreq1.4无任务:执行轻量级占坑程序——idletask从原理来说,非常简单。产品经理:什么都不做。从实现来说,非常模糊。程序员:“什么都不做”的代......
  • Windows的Linux子系统迁移
    默认Windows的Linux子系统(WSL)安装的C盘,如果有用作Docker镜像制作很容易磁盘不够用。可采取如下步骤迁移(以下在WindowsPowerShell中进行的,但在CMD.exe中执行效果相同):1、执行一下命令查看Linux子系统的发行版名称(distroname)PSD:\>wsl-l-vNAME......
  • input子系统
    input子系统  输入设备包括鼠标、键盘、触摸屏、按钮等,它们都能产生输入事件,产生输入数据给计算机系统。  Linux系统为了统一管理输入设备,实现了一套能够兼容所有输入设备的框架,这个框架就是input子系统。  驱动开发人员基于input子系统开发输入设备的驱动程序,input......
  • wsl2 ubuntu子系统安装显卡驱动与cuda
    wsl2安装参考文档:http://t.csdnimg.cn/ClwJ9演示安装ubuntu22列出可安装的子系统命令:wsl--list--onlinePSC:\Users\linyu>wsl--list--online以下是可安装的有效分发的列表。使用'wsl.exe--install<Distro>'安装。NAMEF......
  • 论文解读(UDA-GCN)《Unsupervised Domain Adaptive Graph Convolutional Networks》
    Note:[wechat:Y466551|可加勿骚扰,付费咨询]论文信息论文标题:UnsupervisedDomainAdaptiveGraphConvolutionalNetworks论文作者:论文来源:2020aRxiv论文地址:download 论文代码:download视屏讲解:click1-摘要图卷积网络(GCNs)在许多与图相关的分析任务中都取得了令人印......
  • A LARGE LANGUAGE MODEL EVALUATION BENCHMARK AND BASELINE FOR CHINESE PUBLIC SECU
    本文是LLM系列文章,针对《CPSDBENCH:ALARGELANGUAGEMODELEVALUATIONBENCHMARKANDBASELINEFORCHINESEPUBLICSECURITYDOMAIN》的翻译。CPSDBENCH:中国公共安全领域的大型语言模型评估基准和基线摘要1引言2相关工作3方法4结果与分析5结论摘要大......