转载自兰新宇专栏,https://zhuanlan.zhihu.com/p/83709066
在计算机的发展过程中,外部设备的速度长期低于CPU,为了在必要的时候获取CPU的注意,需要在外设和CPU之间提供一种中断的机制。同时,对于现代操作系统,其内部的运行依赖于时钟的驱动,这也需要中断的支持。
不管是外部中断还是内部中断,在它们到达CPU之前,都会首先经过一个叫做中断控制器(Interrupt Controller)的硬件,其作用是根据中断源(Interrupt Source,也叫IRQ - Interrupt Request)的优先级对中断进行派发。
在多核系统中,中断控制器还需要根据CPU预先设定的规则,将某个中断送入指定的CPU(有点像路由器),以实现中断的负载均衡(irq balance)。x86架构的中断控制器被称为APIC(Advanced Programmable Interrupt Controller),ARM架构的中断控制器则被称为GIC(Generic Interrupt Controller),它们都是适用于多核系统的。
1. irq_chip
Linux中描述中断控制器的数据结构是struct irq_chip,因为不同芯片的中断控制器对其挂接的IRQ有不同的控制方法,因而这个结构体主要是由一组用于回调(callback),指向系统实际的中断控制器所使用的控制方法的函数指针构成。
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);
...
};
其中,"name"是中断控制器的名称,就是我们在"/proc/interupts"中看到的那个,比如下图中蓝框所示的的"IO-APIC"。
"irq_enable/irq_unmask"用于中断使能,"irq_disable/irq_mask"用于中断屏蔽。在多核系统中,CPU之间的中断被称为IPI(Inter-Processor Interrupt),因此"ipi_send_single"表示这个IPI是发给单独的一个CPU,ipi_send_mask"表示IPI是发给mask范围内是所有CPU。
2. irq_desc
既然说到对每个IRQ不同的控制,那就不得不说下对IRQ的描述,IRQ是由struct irq_desc表示的:
struct irq_desc {
const char *name;
unsigned int depth;
struct irqaction *action;
struct irq_data irq_data;
struct cpumask *percpu_enabled;
...
};
其中,"name"是这个IRQ的名称,同样可以在"/proc/interupts"中查看,如上图红框所示部分。
"depth"是关闭该IRQ的嵌套深度,正值表示禁用中断,而0表示可启用中断。
void __disable_irq(struct irq_desc *desc)
{
if (!desc->depth++)
irq_disable(desc);
}
3. irqaction
"irq_desc"中的"action"是IRQ对应的中断处理函数(ISR - Interrupt Service Routine),按理ISR应该就是一个函数指针,可这里确是一个指向struct irqaction的指针。
这是因为在早期的一些处理器中,硬件中断的数目很少,而且有些中断编号已经永久性地分配给了标准的系统组件(比如keyboard和timer),为了服务于更多的设备,只能对有限的IRQ中断号进行共享。这样,同一个中断号就会对应多个不同的处理函数,这些处理函数通过一个单向链表串联在一起的。
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"。
有了中断共享,处理起来着实麻烦了很多,好在新的硬件平台上的设计已经很少采用外设共享IRQ的方式了,因为现在SOC能提供的有中断功能的GPIO已经非常多,足够使用了。
4. irq_data
至于"irq_desc"中的"irq_data",这个在前面已经出现过了,"irq_chip"结构体中的每个函数指针,都会携带一个指向struct irq_data的指针作为参数,可见这个"irq_data"与中断控制必定关系密切。也就是说,在"irq_desc"的定义中,与中断控制器紧密联系的这部分被单独提取出来,构成了"irq_data"结构体:
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"函数。
void irq_disable(struct irq_desc *desc)
{
if (desc->irq_data.chip->irq_disable)
desc->irq_data.chip->irq_disable(&desc->irq_data);
...
}
挂接在同一个中断控制器上的IRQ有相同的控制方法,比如"irq_enable"都是通过写入这个中断控制器的一组寄存器实现,只是不同IRQ的使能需要写入寄存器中不同的bits。这里的"irq_enable"只是用于使能单个IRQ的,而不是使能整个中断控制器。
虽然是使能单个IRQ,但由于这是在中断控制器层面的操作,因而对所有挂接这个中断控制器的CPU都有效,如果要控制单个IRQ对单个CPU有效,则可以借助"irq_desc"中的"percpu_enabled"位域,实现irq_percpu_enable()函数。
要关闭一个CPU对所有中断的响应,应该使用local_irq_disable()/local_irq_save()函数,以x86为例,这是通过关中断指令"cli"实现的。这种中断关闭只对执行关中断指令的CPU有效,要想直接关闭所有CPU对所有中断的响应,通常是不行的。
`
系统中如果只有一个中断控制器,其实是不需要什么绑定的,可是随着芯片功能越来越强,硬件也越来越复杂,现在的很多系统已经不止一个中断控制器了。
我们读芯片手册的时候都知道,每个IRQ都有一个编号,称为中断号。这里"irq"就是表示中断号,可是为什么会有"irq"和"hwirq"两个中断号?详情请看下文分解。
评论