中断控制器
外围设备不是把中断请求直接发送给处理器,而是发给中断控制器,由中断控制器转发给处理器;
中断控制器 是用于管理和调度系统中各种中断请求的硬件模块。在多设备或多中断源的系统中,中断控制器起到仲裁和分发中断信号的作用,确保 CPU 能够按优先级高效处理中断请求。
中断控制器的功能
中断控制器的主要功能包括:
- 中断管理:接收多个中断源的请求,按照优先级将中断请求传递给 CPU。
- 中断屏蔽:可以屏蔽某些不需要响应的中断。
- 优先级仲裁:根据中断的优先级确定中断处理的顺序。
- 中断分发:在多核处理器系统中,将中断分发给特定的 CPU 核心。
通用中断控制器(GIC)
ARM公司提供了一种标准的中断控制器,称为通用中断控制器(Generic Interrupt Controller,GIC);GIC发展历程中有多个版本(v1~v4),每个版本都引入了不同的功能与改进,以适配更复杂的多核处理器系统;GIC各版本之间主要是支持的CPU核心数、中断类型不同;
GIC硬件的实现形态有两种:
1)厂商研发自己的ARM处理器,向ARM公司购买GIC的授权;
2)厂商直接向ARM公司购买处理器的授权,这些处理器包含了GIC;
GIC的结构组成
从软件的角度看,GIC有两个主要的功能块:
- GIC Distributor(分发器):
系统中所有的中断源连接到分发器,分发器的寄存器用来控制单个中断的属性:优先级、状态、安全、转发信息(可以被发送到哪些处理器)和使能状态。分发器决定哪个中断应该通过处理器接口转发到哪个处理器。
- GIC CPU Interface(CPU 接口):
处理器通过处理器接口接收中断。处理器接口提供的寄存器用来屏蔽和识别中断,控制中断的状态。每个处理器有一个单独的处理器接口。
GIC的中断类型
软件通过中断号识别中断,每个中断号唯一对应一个中断源;
ARM GIC(Generic Interrupt Controller,通用中断控制器) 的中断类型主要包括以下几类:
SGI(Software Generated Interrupt,软件触发中断)
- 定义:由软件生成的中断,通常用于同一个处理器集群中的 CPU 核之间的通信(例如唤醒其他核)。
- 特点:
- 只能在同一组 CPU Cluster 中使用。
- 软件通过向 GIC Distributor 写寄存器触发 SGI。
- 中断号范围:0-15(通常只有 16 个)。
- 主要用于 核间通信(IPI,Inter-Processor Interrupt)。
- 使用场景:核之间的同步、任务调度、唤醒 CPU 核。
SGI和软中断的区别:
- SGI 属于硬件中断的一部分,主要用于 CPU 核之间的通信。
- 软中断 是 Linux 内核中提供的 软件机制,用于延迟处理硬中断后的任务。
- SGI 和软中断的共同点是都可以由软件触发,但 SGI 属于硬中断,而 软中断纯粹是软件调度机制。
PPI(Private Peripheral Interrupt,私有外设中断)
- 定义:分配给每个 CPU 核的中断,是每个核私有的中断,不同的核可以有独立的 PPI。
- 特点:
- 中断号范围:16-31(固定的中断号)。
- 每个 CPU 核都有自己的一组 PPI。
- 常用于管理处理器本地外设,如 定时器(Local Timer) 或 看门狗。
- 使用场景:处理器本地中断,如定时器中断、核本地外设的中断。
SPI(Shared Peripheral Interrupt,共享外设中断)
- 定义:被多个 CPU 核共享的外设中断,由 GIC 分发到合适的 CPU 核进行处理。
- 特点:
- 中断号范围:32-1019。
- 通常由外部设备(如网络控制器、USB 控制器等)触发。
- 通过 GIC Distributor 控制分发到指定的 CPU 核。
- 使用场景:外设中断,如网络设备、USB 控制器、DMA 控制器等。
LPI(Locality-specific Peripheral Interrupt)
- 功能:LPI 是为了解决大规模设备中断需求而设计的,尤其是通过 MSI(Message Signaled Interrupt) 的机制来动态分发中断。
- 特点:
- 动态中断号分配:LPI 不使用固定的中断号,而是可以动态分配。
- 高扩展性:中断号可以扩展到 2^24(最多 16M+ 个中断)。
- ITS(Interrupt Translation Service)支持:LPI 需要通过 GIC ITS(Interrupt Translation Service)进行路由和管理。
- 专为高性能场景设计:特别适合 PCIe 设备 使用的 MSI(Message Signaled Interrupt)。
边沿触发和水平触发
中断可以是边沿触发(edge-triggered,ET),也可以是水平触发(level-triggered,LT)。ET是再电压变化的一瞬间触发,电压由高到低变化触发的中断称为下降沿触发,电压由低到高变化触发的中断称为上升沿触发;LT是再高电压或低电压保持的时间内触发,低电压触发的中断称为低电平触发,高电压触发的中断称为高电平触发;
中断状态
中断有以下4种状态:
(1)Inactive:中断源没有发送中断。
(2)Pending:中断源已经发送中断,等待处理器处理。
(3)Active:处理器已经确认中断,正在处理。
(4)Activeand pending:处理器正在处理中断,相同的中断源)又发送了一个中断
中断的状态转换过程如下。
(1)Inactive->Pending:外围设备发送了中断。
(2)Pending->Active:处理器确认了中断。
(3)Active->Inactive:处理器处理完中断。
中断控制器的工作流程
- 中断源发起请求:外设或软件触发一个中断请求。
- 中断控制器接收中断:
- GIC Distributor 接收中断请求。
- 仲裁中断的优先级,并决定将中断分发给哪个 CPU 核。
- CPU 接收中断:
- GIC CPU Interface 将中断信息通知 CPU。
- CPU 通过中断向量表找到对应的中断服务例程(ISR)并执行。
- 中断处理完成:
- CPU 执行完 ISR 后,向 GIC 发送 中断结束信号(EOI)。
- GIC 清除中断状态,等待下一次中断请求。
内核中GIC定义
不同种类的中断控制器的访问方法存在差异,为了屏蔽差异,内核定义了中断控制器描述符irq_chip,每种中断控制器自定义各种操作函数。GIC V2控制器的描述符如下:
static struct irq_chip gic_chip = {
.irq_mask = gic_mask_irq,
.irq_unmask = gic_unmask_irq,
.irq_eoi = gic_eoi_irq,
.irq_set_type = gic_set_type,
.irq_get_irqchip_state = gic_irq_get_irqchip_state,
.irq_set_irqchip_state = gic_irq_set_irqchip_state,
.flags = IRQCHIP_SET_TYPE_MASKED |
IRQCHIP_SKIP_SET_WAKE |
IRQCHIP_MASK_ON_SUSPEND,
};
1. irq_mask
- 描述: 指向函数
gic_mask_irq
,用于屏蔽指定的中断。 - 功能: 当某个中断不需要被响应时,通过调用该函数屏蔽中断。
2. irq_unmask
- 描述: 指向函数
gic_unmask_irq
,用于取消屏蔽某个中断。 - 功能: 重新启用被屏蔽的中断,使其能够被处理。
3. irq_eoi
- 描述: 指向函数
gic_eoi_irq
,用于发出 End of Interrupt (EOI) 信号。 - 功能: 通知 GIC 当前中断处理完成,通常在中断服务程序(ISR)末尾调用。
4. irq_set_type
- 描述: 指向函数
gic_set_type
,用于设置中断触发类型。 - 功能: 配置中断是边沿触发还是电平触发,以及上升沿、下降沿等模式。
5. irq_get_irqchip_state
- 描述: 指向函数
gic_irq_get_irqchip_state
,用于获取中断控制器的状态信息。 - 功能: 允许查询中断的当前状态,例如是否处于挂起状态。
6. irq_set_irqchip_state
- 描述: 指向函数
gic_irq_set_irqchip_state
,用于设置中断控制器的状态。 - 功能: 用于操作中断状态,例如设置中断为活动状态或挂起状态。
7. flags
- 描述:
flags
定义了中断控制器的属性,以下是具体标志位的含义:**IRQCHIP_SET_TYPE_MASKED**
:- 配置中断触发类型时,必须先屏蔽中断。
**IRQCHIP_SKIP_SET_WAKE**
:- 跳过设置唤醒功能(即不支持
set_wake
操作)。
- 跳过设置唤醒功能(即不支持
**IRQCHIP_MASK_ON_SUSPEND**
:- 系统挂起时自动屏蔽中断。
该 gic_chip
是 Linux 内核中用于描述 GIC 的 irq_chip
实现。它定义了 GIC 的基本操作(如屏蔽、解除屏蔽、EOI 等)和属性,内核通过调用这些函数实现对 GIC 的控制。该结构体最终会与具体的中断(如外部设备中断)绑定,以完成中断管理和调度。
中断域
一个大型系统可能有多个中断控制器,这些中断控制器可以级联,一个中断控制器作为中断源连接到另一个中断控制器,但只有一个中断控制器作为根控制器直接连接到处理器。为把每个中断控制器本地的硬件中断号映射到全局唯一的Linux中断号(也称为虚拟中断号),内核定义了中断域irq_domain,每个中断控制器有自己的中断域。
struct irq_domain {
struct list_head link;
const char *name;
const struct irq_domain_ops *ops;
void *host_data;
unsigned int flags;
/* Optional data */
struct fwnode_handle *fwnode;
enum irq_domain_bus_token bus_token;
struct irq_domain_chip_generic *gc;
#ifdef CONFIG_IRQ_DOMAIN_HIERARCHY
struct irq_domain *parent;
#endif
/* reverse map data. The linear map gets appended to the irq_domain */
irq_hw_number_t hwirq_max;
unsigned int revmap_direct_max_irq;
unsigned int revmap_size;
struct radix_tree_root revmap_tree;
unsigned int linear_revmap[];
};
创建中断域
中断控制器的驱动程序使用分配函数irq_domain_add_*()创建和注册中断域。每种映射方法提供不同的分配函数,调用者必须给分配函数提供irq_domain_ops结构体,分配函数在执行成功的时候返回irq_domain的指针。
中断域支持以下映射方法:
- 线性映射;
- 树映射;
- 不映射;
线性映射(linear map)
线性映射维护一个固定大小的表,索引是硬件中断号。如果硬件中断号的最大数量是固定的,并且比较小(小于256),那么线性映射是好的选择。对于线性映射,分配中断域的函数如下:
/**
* irq_domain_add_linear() - Allocate and register a linear revmap irq_domain.
* @of_node: pointer to interrupt controller's device tree node.
* @size: Number of interrupts in the domain.
* @ops: map/unmap domain callbacks
* @host_data: Controller private data pointer
*/
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);
}
树映射(tree map)
树映射使用基数树(radix tree)保存硬件中断号到Linux中断号的映射。为什么不使用红黑树呢?如果硬件中断号可能非常大,那么树映射是好的选择,因为不需要根据最大硬件中断号分配一个很大的表。对于树映射,分配中断域的函数如下:
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);
}
不映射(no map)
有些中断控制器很强,硬件中断号是可以配置的,例如PowerPC架构使用的MPIC(Multi-Processor Interrupt Controller)。我们直接把Linux中断号写到硬件,硬件中断号就是Linux中断号,不需要映射。对于不映射,分配中断域的的函数如下:
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);
}
分配函数把主要工作委托给函数_irq_domain_add()。函数_irq_domain_add()的执行过程是:分配一个irq_domain结构体,初始化成员,然后把中断域添加到全局链表irq_domain_list中。
创建映射
创建中断域以后,需要向中断域添加硬件中断号到Linux中断号的映射,内核提供了函数irq_create_mapping:
/**
* irq_create_mapping() - Map a hardware interrupt into linux irq space
* @domain: domain owning this hardware interrupt or NULL for default domain
* @hwirq: hardware irq number in that domain space
*
* Only one mapping per hardware interrupt is permitted. Returns a linux
* irq number.
* If the sense/trigger is to be specified, set_irq_type() should be called
* on the number returned from that call.
*/
unsigned int irq_create_mapping(struct irq_domain *domain,
irq_hw_number_t hwirq)
输入参数是中断域和硬件中断号,返回Linux中断号。该函数首先分配Linux中断号,然后把硬件中断号到Linux中断号的映射添加到中断域。
查找映射
中断处理程序需要根据硬件中断号查找Linux中断号,内核提供了函数 irq_find_mapping:
/**
* irq_find_mapping() - Find a linux irq from an hw irq number.
* @domain: domain owning this hardware interrupt
* @hwirq: hardware irq number in that domain space
*/
unsigned int irq_find_mapping(struct irq_domain *domain,
irq_hw_number_t hwirq)
输入参数是中断域和硬件中断号,返回Linux中断号。
参考文献
- Professional Linux Kernel Architecture,Wolfgang Mauerer
- Linux内核深度解析,余华兵
- Linux设备驱动开发详解,宋宝华