IRQs
- 什么是IRQ?
- SMP IRQ 亲和性
- Linux内核中的irq_domain中断号映射库
- IRQ 标志状态跟踪
什么是IRQ?
IRQ(中断请求)是设备发出的中断请求。目前,它们可以通过引脚或数据包传输而来。多个设备可以连接到同一个引脚上,从而共享一个IRQ。
IRQ编号是内核用来表示硬件中断源的标识符。通常这是一个全局irq_desc数组的索引,但除了linux/interrupt.h实现的细节外,其他细节都是特定于架构的。
IRQ编号是对计算机上可能的中断源的枚举。通常,被枚举的是系统中所有中断控制器上的输入引脚数量。在ISA的情况下,被枚举的是系统中两个i8259中断控制器上的16个输入引脚。
架构可以为IRQ编号分配额外的含义,并且在涉及硬件的任何手动配置的情况下鼓励这样做。ISA IRQ是分配这种额外含义的经典示例。
SMP IRQ 亲和性
什么是 SMP IRQ 亲和性?
SMP IRQ 亲和性是指指定特定中断请求(IRQ)源可以在哪些处理器核心上运行的机制。在 Linux 系统中,可以通过 /proc/irq/IRQ#/smp_affinity
和 /proc/irq/IRQ#/smp_affinity_list
文件来指定允许的处理器核心。smp_affinity
是一个位掩码,而 smp_affinity_list
是一个处理器核心列表。
如何修改 SMP IRQ 亲和性?
可以通过修改 /proc/irq/IRQ#/smp_affinity
文件来改变 IRQ 的亲和性。例如,可以使用以下命令将 IRQ44(eth1)限制在 CPU0-3 上,然后限制在 CPU4-7 上:
cd /proc/irq/44 cat smp_affinity # 显示当前的亲和性 echo 0f > smp_affinity # 将 IRQ44 限制在 CPU0-3 上 cat smp_affinity # 显示修改后的亲和性
默认 SMP IRQ 亲和性
/proc/irq/default_smp_affinity
文件指定了所有非活动 IRQ 的默认亲和性掩码,一旦 IRQ 被分配/激活,其亲和性位掩码将被设置为默认掩码。默认掩码是 0xffffffff
。
限制 IRQ 到特定 CPU 列表
除了使用位掩码外,还可以使用 smp_affinity_list
文件将 IRQ 限制在特定的 CPU 列表上。例如,可以使用以下命令将相同的 IRQ44 限制在 CPU 1024 到 1031 上:
echo 1024-1031 > smp_affinity_list cat smp_affinity_list # 显示修改后的 CPU 列表
总结
SMP IRQ 亲和性允许系统管理员控制特定 IRQ 的处理器核心分配,以优化系统性能和资源利用率。
Linux内核中的irq_domain中断号映射库
Linux内核当前的设计使用了一个单一的大号空间,其中每个独立的IRQ源被分配了一个不同的号码。当只有一个中断控制器时,这很简单,但在具有多个中断控制器的系统中,内核必须确保每个中断控制器都被分配了不重叠的Linux IRQ号。
注册为唯一irqchips的中断控制器数量呈上升趋势:例如,不同类型的子驱动程序,如GPIO控制器,通过将它们的中断处理程序建模为irqchips(实际上是级联中断控制器),避免重新实现相同的回调机制作为IRQ核心系统。
在这种情况下,中断号失去了与硬件中断号的任何对应关系:在过去,IRQ号可以选择使其与根中断控制器(即实际向CPU触发中断线的组件)的硬件IRQ线匹配,但现在这个号码只是一个数字。
因此,我们需要一种机制来将控制器本地中断号(称为硬件irq)与Linux IRQ号分开。
irq_alloc_desc()和irq_free_desc() API提供了irq号的分配,但它们不提供对控制器本地IRQ(hwirq)号到Linux IRQ号空间的反向映射的支持。
irq_domain库在irq_alloc_desc*() API的基础上添加了hwirq和IRQ号之间的映射。与其让中断控制器驱动程序自行编写其自己的反向映射方案,不如使用irq_domain来管理映射。
irq_domain还实现了从抽象的irq_fwspec结构到hwirq号的转换(目前支持设备树和ACPI GSI),并且可以很容易地扩展以支持其他IRQ拓扑数据源。
irq_domain的使用
中断控制器驱动程序通过调用irq_domain_add_()或irq_domain_create_()函数之一(每种映射方法都有不同的分配器函数,稍后会详细介绍)创建并注册irq_domain。该函数在成功时将返回指向irq_domain的指针。调用者必须提供分配器函数的irq_domain_ops结构。
在大多数情况下,irq_domain将开始为空,没有任何hwirq和IRQ号之间的映射。通过调用irq_create_mapping()向irq_domain添加映射,该函数接受irq_domain和hwirq号作为参数。如果hwirq的映射尚不存在,则它将为hwirq分配一个新的Linux irq_desc,并将其与hwirq关联,并调用.map()回调,以便驱动程序可以执行任何所需的硬件设置。
一旦建立了映射,可以通过各种方法检索或使用映射:
- irq_resolve_mapping()返回给定域和hwirq号的irq_desc结构的指针,如果没有映射则返回NULL。
- irq_find_mapping()返回给定域和hwirq号的Linux IRQ号,如果没有映射则返回0。
- irq_linear_revmap()现在与irq_find_mapping()相同,并已被弃用。
- generic_handle_domain_irq()处理由域和hwirq号描述的中断
请注意,irq域查找必须发生在与RCU读端关键部分兼容的上下文中。
在调用irq_find_mapping()之前,必须至少调用一次irq_create_mapping(),否则将不会分配描述符。
如果驱动程序具有Linux IRQ号或irq_data指针,并且需要知道关联的hwirq号(例如在irq_chip回调中),则可以直接从irq_data->hwirq获取。
irq_domain映射类型
有几种机制可用于从hwirq到Linux irq的反向映射,每种机制都使用不同的分配函数。应根据用例选择使用哪种反向映射类型。下面描述了每种反向映射类型:
线性
- irq_domain_add_linear()
- irq_domain_create_linear()
线性反向映射维护一个由hwirq号索引的固定大小表。当映射hwirq时,将为hwirq分配一个irq_desc,并将IRQ号存储在表中。
线性映射是当最大hwirq数固定且相对较小(~ < 256)时的良好选择。这种映射的优点是对IRQ号的固定时间查找,且仅为正在使用的IRQ分配irq_desc。缺点是表的大小必须与最大可能的hwirq号一样大。
irq_domain_add_linear()和irq_domain_create_linear()在功能上是等效的,只是第一个参数不同 - 前者接受特定于Open Firmware的'struct device_node',而后者接受更一般的抽象'struct fwnode_handle'。
大多数驱动程序应该使用线性映射。
树
- irq_domain_add_tree()
- irq_domain_create_tree()
irq_domain维护一个从hwirq号到Linux IRQ的基数树映射。当映射hwirq时,将分配一个irq_desc,并将hwirq用作基数树的查找键。
树映射是当hwirq号可能非常大时的良好选择,因为它不需要分配与最大hwirq号一样大的表。缺点是hwirq到IRQ号的查找取决于表中有多少条目。
irq_domain_add_tree()和irq_domain_create_tree()在功能上是等效的,只是第一个参数不同 - 前者接受特定于Open Firmware的'struct device_node',而后者接受更一般的抽象'struct fwnode_handle'。
很少有驱动程序需要这种映射。
无映射
- irq_domain_add_nomap()
无映射映射用于当硬件中的hwirq号是可编程的时。在这种情况下,最好将Linux IRQ号编程到硬件中,以便不需要映射。调用irq_create_direct_mapping()将分配一个Linux IRQ号,并调用.map()回调,以便驱动程序可以将Linux IRQ号编程到硬件中。
大多数驱动程序无法使用此映射,现在已经受到CONFIG_IRQ_DOMAIN_NOMAP选项的限制。请勿引入此API的新用户。
传统
- irq_domain_add_simple()
- irq_domain_add_legacy()
- irq_domain_create_simple()
- irq_domain_create_legacy()
传统映射是对已为hwirq分配了一系列irq_desc的驱动程序的特殊情况。当驱动程序无法立即转换为使用线性映射时,将使用传统映射。例如,许多嵌入式系统板支持文件使用一组#defines为传递给struct device注册的IRQ号。在这种情况下,Linux IRQ号无法动态分配,应使用传统映射。
顾名思义,*_legacy()
函数已被弃用,仅用于支持古老的平台。不应添加新用户。当使用结果为传统行为时,*_simple()函数也是如此。
传统映射假定已为控制器分配了连续范围的IRQ号,并且可以通过将固定偏移添加到hwirq号来计算IRQ号,反之亦然。缺点是它要求中断控制器管理IRQ分配,并且需要为每个hwirq分配一个irq_desc,即使它未被使用。
只有在必须支持固定IRQ映射时才应使用传统映射。例如,ISA控制器将使用传统映射来映射Linux IRQs 0-15,以便现有的ISA驱动程序获得正确的IRQ号。
大多数传统映射的用户应该使用irq_domain_add_simple()或irq_domain_create_simple(),它将仅在系统提供IRQ范围时使用传统域映射,否则将使用线性域映射。此调用的语义是,如果指定了IRQ范围,则将为其动态分配描述符,如果未指定范围,则将转到irq_domain_add_linear()或irq_domain_create_linear(),这意味着不会分配irq描述符。
简单域的典型用例是其中irqchip提供程序支持动态和静态IRQ分配。
为了避免出现使用线性域但未分配描述符的情况,非常重要的是确保在任何irq_find_mapping()之前,使用简单域的驱动程序调用irq_create_mapping(),因为后者实际上适用于静态IRQ分配情况。
irq_domain_add_simple()和irq_domain_create_simple()以及irq_domain_add_legacy()和irq_domain_create_legacy()在功能上是等效的,只是第一个参数不同 - 前者接受特定于Open Firmware的'struct device_node',而后者接受更一般的抽象'struct fwnode_handle'。
层次IRQ域
在某些体系结构上,可能涉及多个中断控制器来传递设备到目标CPU的中断。让我们看一下x86平台上典型的中断传递路径:
设备 --> IOAPIC -> 中断重映射控制器 -> 本地APIC -> CPU
涉及三个中断控制器:
- IOAPIC控制器
- 中断重映射控制器
- 本地APIC控制器
为了支持这样的硬件拓扑并使软件架构与硬件架构匹配,为每个中断控制器构建了一个irq_domain数据结构,并且这些irq_domains被组织成层次结构。构建irq_domain层次结构时,设备附近的irq_domain是子域,而靠近CPU的irq_domain是父域。因此,对于上述示例,将构建以下层次结构:
CPU向量irq_domain(管理CPU向量的根irq_domain) ^ | 中断重映射irq_domain(管理irq重映射条目) ^ | IOAPIC irq_domain(管理IOAPIC传递条目/引脚)
使用层次irq_domain有四个主要接口:
- irq_domain_alloc_irqs():分配IRQ描述符和与这些中断传递相关的中断控制器资源。
- irq_domain_free_irqs():释放与这些中断相关的IRQ描述符和中断控制器资源。
- irq_domain_activate_irq():激活中断控制器硬件以传递中断。
- irq_domain_deactivate_irq():停止激活中断控制器硬件以停止传递中断。
为了支持层次irq_domain,需要进行以下更改:
- 向struct irq_domain添加了一个新字段'parent';它用于维护irq_domain层次结构信息。
- 向struct irq_data添加了一个新字段'parent_data';它用于构建与层次irq_domains匹配的层次irq_data。irq_data用于存储irq_domain指针和硬件irq号。
- 向struct irq_domain_ops添加了新的回调,以支持层次irq_domain操作。
通过支持层次irq_domain和层次irq_data,为每个中断控制器构建了一个irq_domain结构,并为与IRQ相关联的每个irq_domain分配了一个irq_data结构。现在我们可以进一步支持堆叠(层次)irq_chip。也就是说,每个irq_data都与一个irq_chip关联。子irq_chip可以自行实现所需的操作,也可以与其父irq_chip合作实现。
使用堆叠irq_chip,中断控制器驱动程序只需要处理自己管理的硬件,并在需要时可以向其父irq_chip请求服务。因此,我们可以实现更清晰的软件架构。
为了支持层次irq_domain的中断控制器驱动程序需要:
- 实现irq_domain_ops.alloc和irq_domain_ops.free
- 可选地实现irq_domain_ops.activate和irq_domain_ops.deactivate。
- 可选地实现一个irq_chip来管理中断控制器硬件。
- 无需实现irq_domain_ops.map和irq_domain_ops.unmap,因为它们在层次irq_domain中未使用。
层次irq_domain绝不是x86特有的,并且被广泛用于支持其他体系结构,例如ARM、ARM64等。
调试
通过打开CONFIG_GENERIC_IRQ_DEBUGFS,IRQ子系统的大多数内部都可以在debugfs中公开。
IRQ 标志状态跟踪
作者: 由 Ingo Molnar mingo@redhat.com 开始
"irq-flags 跟踪" 功能可以"跟踪"硬中断和软中断状态,它为感兴趣的子系统提供了一个机会,以便通知内核中发生的每个硬中断关闭/打开和软中断关闭/打开事件。
需要 CONFIG_TRACE_IRQFLAGS_SUPPORT 来支持 CONFIG_PROVE_SPIN_LOCKING 和 CONFIG_PROVE_RW_LOCKING,这样才能由通用锁调试代码提供。否则,只会在某些架构上提供 CONFIG_PROVE_MUTEX_LOCKING 和 CONFIG_PROVE_RWSEM_LOCKING - 这些是在 IRQ 上下文中不使用的锁定 API。(对于 rwsem,有一个例外是被解决的)
对于这一点,架构支持肯定不属于"简单"范畴,因为许多低级汇编代码处理 irq-flags 状态的变化。但是,一个架构可以以一种相当直接和无风险的方式启用 irq-flags 跟踪。
希望支持这一功能的架构首先需要进行一些代码组织上的更改:
- 在其架构级别的 Kconfig 文件中添加并启用 TRACE_IRQFLAGS_SUPPORT
然后还需要进行一些功能性的更改来实现 irq-flags 跟踪支持:
- 在低级入口代码中添加(构建条件)对 trace_hardirqs_off()/trace_hardirqs_on() 函数的调用。锁验证器严密监视"真实"irq-flags 是否与"虚拟"irq-flags 状态匹配,如果两者不匹配,则会大声抱怨(并关闭自身)。通常,架构对 irq-flags 跟踪的支持大部分时间都花在这个状态上:查看锁定器的投诉,尝试弄清楚我们尚未覆盖的汇编代码,修复并重复。一旦系统启动并且在 irq-flags 跟踪函数中没有锁定器投诉,架构支持就完成了。
- 如果架构有不可屏蔽中断,那么需要通过 lockdep_off()/lockdep_on() 来排除这些中断的 irq-跟踪[和锁验证]机制。
一般来说,在架构中存在不完整的 irq-flags 跟踪实现是没有风险的:lockdep 会检测到这一点并关闭自身。即锁验证器仍然是可靠的。不应该因为 irq-跟踪的 bug 而导致崩溃。(除非汇编更改通过修改不应该修改的条件或寄存器而破坏了其他代码)