简介
首先我们可以使用下面的命令查看中断号的映射
cat /proc/interrupts
中断控制器还需要根据CPU预先设定的规则,将某个中断送入指定的CPU(有点像路由器),以实现中断的负载均衡(irq balance)。x86架构的中断控制器被称为APIC(Advanced Programmable Interrupt Controller),ARM架构的中断控制器则被称为GIC(Generic Interrupt Controller),它们都是适用于多核系统的。
数据结构
内核的目录结构
Linux/
|
+-->include/
| |
| |-->irqchip/ 中断控制器相关API定义
| |-->linux/ Linux硬件无关中断处理层API定义
| |
| +-->irqdesc.h
| |-->irqhandler.h
| |-->irqdomain.h
| |-->irqflags.h
| +-->irq.h
+-->kernel/
| |
| +-->irq/ Linux硬件无关中断处理层实现
| |
| +-->irqdesc.c/irqdomain.c/.../etc.
+-->driver/
|
+-->irqchip/ 中断控制器的相关实现
struct irq_chip
内核中断控制器的结构体是struct irq_chip,其中,”name”是中断控制器的名称,就是我们在”/proc/interupts”中看到的那个中断控制器的名称。
struct irq_chip {
const char *name;
void (*irq_enable)(struct irq_data *data);
void (*irq_disable)(struct irq_data *data);
void (*irq_mask)(struct irq_data *data);
void (*irq_unmask)(struct irq_data *data);
void (*ipi_send_single)(struct irq_data *data, unsigned int cpu);
void (*ipi_send_mask)(struct irq_data *data, const struct cpumask *dest);
...
};
struct irq_desc
Linux内核中描述中断的结构体为.其中”name”是这个IRQ的名称,同样可以在”/proc/interupts”中查看的中断设备相关的名称。
struct irq_desc {
const char *name;
unsigned int depth;
# 中断处理函数(ISR - Interrupt Service Routine),
struct irqaction *action;
struct irq_data irq_data;
struct cpumask *percpu_enabled;
...
};
需要注意的是为什么要使用struct irqaction 这样一个结构体。而不是直接使用一个回调函数?
这是因为在早期的一些处理器中,硬件中断的数目很少,而且有些中断编号已经永久性地分配给了标准的系统组件(比如keyboard和timer),为了服务于更多的设备,只能对有限的IRQ中断号进行共享。这样,同一个中断号就会对应多个不同的处理函数,这些处理函数通过一个单向链表串联在一起的。
struct irqaction
struct irqaction {
irq_handler_t handler;
void *dev_id;
struct irqaction *next;
...
}
当一个中断发生的时候,其对应IRQ链表上的所有”irqaction”的”handler”都将被依次执行,以判断是否是自己的设备产生的中断,这主要靠读取自己设备的中断状态寄存器来完成。因此共享中断时,即便不是你的设备产生的中断,你的”handler”也会被调用到。为了避免无谓的消耗,需要一进”handler”就立刻进行判断,如果不是,就尽快的退出。
当一个设备从挂接的IRQ线上卸载时,设备对应的”irqaction”也应该相应地从IRQ链表中移除,此时需要一个表示挂接在同一个IRQ上的不同设备的标识,这个标识就是”dev_id”。内核通过比对”dev_id”,来找到那个应该移除的”irqaction”。
struct irq_data
这个结构体是包含在struct irq_desc中的结构体。
struct irq_data {
struct irq_chip *chip;
struct irq_domain *domain;
unsigned int irq;
unsigned long hwirq;
...
};
其中,”chip”就指向了这个IRQ所挂接的中断控制器,两者的绑定是通过irq_set_chip()函数完成的。
int irq_set_chip(unsigned int irq, struct irq_chip *chip)
{
struct irq_desc *desc = irq_get_desc_lock(irq, &flags, 0);
desc->irq_data.chip = chip;
...
}
绑定之后就可以利用”irq_chip”提供的各种处理函数了,比如内核提供的用于禁止一个IRQ的irq_disable(),它就是通过该IRQ对应的”irq_desc”的”irq_data”域,找到对应的”irq_chip”,进而回调”irq_chip”中的”irq_disable”函数。
中断域
早期的系统,只有一个中断控制器,接入中断控制器的物理中断号都是不同的。但是随着计算机系统的发展,系统中可以挂接更多的中断控制器。特别是嵌入式系统的出现,类似GPIO这种也可以视作一种中断控制器。每个中断控制器都有自己中断线的物理编号,且这些物理编号会有重复。此时,Linux Kernel发展出了IRQ Domain的概念,来区分这些相同的物理中断编号。
在Linux中,我们可以使用两个ID来标识外设的中断,
- IRQ number。CPU需要为每一个外设中断编号,我们称之IRQ Number。这个IRQ number是一个虚拟的interrupt ID,和硬件无关,仅仅是被CPU用来标识一个外设中断。
- HW interrupt ID。对于interrupt controller而言,它收集了多个外设的interrupt request line并向上传递,因此,interrupt controller需要对外设中断进行编码。Interrupt controller用HW interrupt ID来标识外设的中断。在interrupt controller级联的情况下,仅仅用HW interrupt ID已经不能唯一标识一个外设中断,还需要知道该HW interrupt ID所属的interrupt controller(HW interrupt ID在不同的Interrupt controller上是会重复编码的)。
- 比如有3个中断控制器A,B,C,每一个中断控制器外接了8个外设。那么对于中断控制器A来说外设HW interrupt ID的范围0~7,同时B,C也是相同的。这样就造成了重复编码。
这样,CPU和interrupt controller在标识中断上就有了一些不同的概念,但是,对于驱动工程师而言,我们和CPU视角是一样的,我们只希望得到一个IRQ number,而不关系具体是那个interrupt controller上的那个HW interrupt ID。这样一个好处是在中断相关的硬件发生变化的时候,驱动软件不需要修改。因此,linux kernel中的中断子系统需要提供一个将HW interrupt ID映射到IRQ number上来的机制,这就是中断域的产生了。
Notes:两种视角的中断号:IRQ Number:从驱动软件来看,CPU对每个中断进行编号。HW interrupt ID:从中断控制起来看,每个中断控制器上的中断都有一个编号。这两种不同视角就导致了,从硬件到软件的一个转换。
数据结构
struct irq_domain
include/linux/irqdomain.h:110:
struct irq_domain {
struct list_head link; //Linux全局irq_domain链表的成员
const char *name; //中断控制域的名称
const struct irq_domain_ops *ops; //中断控制域的操作集合
void *host_data; //该中断控制域的私有数据指针
unsigned int flags; //该中断控制域的标识
struct fwnode_handle *fwnode; //待补充
enum irq_domain_bus_token bus_token;//中断域的总线类型,见irq_domain_bus_token的定义
struct irq_domain_chip_generic *gc; //IRQ Domain使用的中断芯片数据结构
struct irq_domain *parent; //如果支持中断控制域层次结构,指向该控制域的父级
irq_hw_number_t hwirq_max; //该域中的最大物理中断号
unsigned int revmap_direct_max_irq; //直接映射的最大中断号
unsigned int revmap_size; //虚拟/物理线性映射表的大小
struct radix_tree_root revmap_tree; //虚拟/物理映射基树(当该IRQ Domain使用基树方式映射时)
unsigned int linear_revmap[]; //虚拟/物理线性映射表
};
enum irq_domain_bus_token {
DOMAIN_BUS_ANY = 0,
DOMAIN_BUS_PCI_MSI,
DOMAIN_BUS_PLATFORM_MSI,
DOMAIN_BUS_NEXUS,
};
API
向系统注册irq domain
注册中断域可以分为3种类型。
- 线性映射
- Radix Tree map
- no map
线性映射
线性映射。其实就是一个lookup table,HW interrupt ID作为index,通过查表可以获取对应的IRQ number。对于Linear map而言,interrupt controller对其HW interrupt ID进行编码的时候要满足一定的条件:hw ID不能过大,而且ID排列最好是紧密的。对于线性映射,其接口API如下:
static inline struct irq_domain *irq_domain_add_linear(struct device_node *of_node,
unsigned int size,---------该interrupt domain支持多少IRQ
const struct irq_domain_ops *ops,---callback函数
void *host_data)-----driver私有数据
{
return __irq_domain_add(of_node, size, size, 0, ops, host_data);
}
为irq domain创建映射
上面我们展示的是系统注册一个irq domain,但是我们前文提到的 HW interrupt ID和 IRQ number的映射关系还需要我们来实现。创建映射有四个接口函数:
Linux-4.9.88/include/linux/irqdomain.h
extern unsigned int irq_create_mapping(struct irq_domain *host,
irq_hw_number_t hwirq);
extern unsigned int irq_create_fwspec_mapping(struct irq_fwspec *fwspec);
extern unsigned int irq_create_direct_mapping(struct irq_domain *host);
extern int irq_create_strict_mappings(struct irq_domain *domain,
unsigned int irq_base,
irq_hw_number_t hwirq_base, int count);
irq_create_mapping
kernel/irq/irqdomain.c:391
/**
* 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)
该接口函数以irq domain和HW interrupt ID为参数,返回IRQ number(这个IRQ number是动态分配的)。驱动调用该函数的时候必须提供HW interrupt ID,也就是意味着driver知道自己使用的HW interrupt ID,而一般情况下,HW interrupt ID其实对具体的driver应该是不可见的,不过有些场景比较特殊,例如GPIO类型的中断,它的HW interrupt ID和GPIO有着特定的关系,driver知道自己使用那个GPIO,也就是知道使用哪一个HW interrupt ID了。
irq_create_strict_mappings
kernel/irq/irqdomain.c:452:
/**
* irq_create_strict_mappings() - Map a range of hw irqs to fixed linux irqs
* @domain: domain owning the interrupt range
* @irq_base: beginning of linux IRQ range
* @hwirq_base: beginning of hardware IRQ range
* @count: Number of interrupts to map
*
* This routine is used for allocating and mapping a range of hardware
* irqs to linux irqs where the linux irq numbers are at pre-defined
* locations. For use by controllers that already have static mappings
* to insert in to the domain.
*
* Non-linear users can use irq_create_identity_mapping() for IRQ-at-a-time
* domain insertion.
*
* 0 is returned upon success, while any failure to establish a static
* mapping is treated as an error.
*/
int irq_create_strict_mappings(struct irq_domain *domain, unsigned int irq_base,
irq_hw_number_t hwirq_base, int count)
给一组HW interrupt ID映射为IRQ number。
irq_create_of_mapping
unsigned int irq_create_of_mapping(struct of_phandle_args *irq_data)
这个接口是利用device tree进行映射关系的建立。通常,一个普通设备的device tree node已经描述了足够的中断信息,在这种情况下,该设备的驱动在初始化的时候可以调用irq_of_parse_and_map这个接口函数进行该device node中和中断相关的内容(interrupts和interrupt-parent属性)进行分析,并建立映射关系,具体代码如下
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))----分析device node中的interrupt相关属性
return 0;
return irq_create_of_mapping(&oirq);-----创建映射,并返回对应的IRQ number
}
常用中断的下半部分处理
中断处理的下半部分有四种。