本文是作者看了蜗窝科技上的技术贴后经过自己的思考,做出的一个整理,通过这个过程记录自己的学习心得。主要从框架的角度着手,具体内容不做深入介绍。
索引:http://www.wowotech.net/linux_kenrel/interrupt_subsystem_architecture.html
简介
从硬件说起
中断硬件系统,主要由三种器件组成,分别是CPU、中断控制器(Interrupt Controller),其他外设。各个外设在硬件上是通过中断线(irq request line)与CPU相连的,在复杂的系统中,外设比较多的情况下,就需要一个中断控制器来协助CPU进行中断的处理,比如ARM架构下的GIC,或者X86架构中的APIC。根据外设的多少,Interrupt Controller可以级联。
当遇到多个cpu core和多个Interrupt Controller的情况下,我们需要思考两个问题
1.Interrupt controller和cpu改如何级联?
举个例子,在arm结构下,我们有8个cpu核心,和2个GIC,一个是root GIC,另外一个是secondary GIC。对于这种情况,一般我们会考虑两种方式:
(1)把8个cpu都连接到root GIC上,secondary GIC不接CPU。这时候原本挂接在secondary GIC的外设中断会输出到root GIC上,并且最终输出到某个cpu。对于软件而言,这是一个比较简单的设计,secondary GIC的cpu interface的设定是固定不变的,永远是从一个固定的CPU interface输出到root GIC。这种方案的坏处是:这时候secondary GIC的PPI和SGI都是没有用的了。此外,在这种设定下,所有连接在secondary GIC上的外设中断要送达的target CPU是统一处理的,要么送去cpu0,要么cpu 5,不能单独控制。
(2)让每个GIC分别连接4个CPU core,root GIC连接CPU0~CPU3,secondary GIC连接CPU4~CPU7。这种状态下,连接在root GIC的中断可以由CPU0~CPU3分担处理,连接在secondary GIC的中断可以由CPU4~CPU7分担处理。但这样,在中断处理方面看起来就体现不出8核的威力了。
2.Interrupt controller应该把中断事件送给哪个CPU?
一般而言,中断控制器可以把中断事件上报给一个CPU或者一组CPU(包括广播到所有的CPU上去)。如果送达了多个cpu,实际上,也应该只有一个handler实际和外设进行交互,另外一个cpu上的handler的动作应该是这样的:发现该irq number对应的中断已经被另外一个cpu处理了,直接退出handler,返回中断现场。普遍的做法是,Interrupt Controller会为支持的每一个中断设定一个target cpu的控制接口(当然应该是以寄存器形式出现,对于GIC,这个寄存器就是Interrupt processor target register)。target cpu就是为了指定该中断要被送往哪一个后者哪几个CPU上。剩余的控制就要交给软件来控制了。比如:可以控制一个中断固定交给某一个cpu处理,也可以控制一个中断由几个CPU轮流处理。
软件框架
linux kernel的中断子系统可以分为4个部分:
1.通用中断处理模块
这部分是硬件无关的代码,无论是哪种CPU,哪种controller,中断处理的过程都有一些相同的内容,这些相同的内容被抽象出来,和HW无关。为外设的驱动代码提供了统一的接口实现irq的管理。这些“通用”的代码组成了linux kernel interrupt subsystem的核心部分。
(代码目录:kernel/kernel/irq)
2.CPU architecture相关的中断处理
和系统使用的具体的CPU architecture相关。
(代码目录:kernel/arch/arm/kernel)
3.Interrupt controller驱动代码
和系统使用的Interrupt controller相关,可以调用通用模块中的API实现部分功能,比如IRQ domain的操作。
(代码目录:kernel/drivers/irqchip)
4.普通外设的驱动
这些驱动将使用通用中断处理模块的API来实现自己的中断逻辑。
irq domain
Linux kernel中使用IRQ domain来描述一个中断控制器所管理的中断源。也就是说,每个中断控制器都有自己的domain,可以将IRQ Domain看作是Interrupt controller的软件抽象。
这里的Interrupt controller并不仅仅是指传统意义上的中断控制器,如GIC,也可以代表一种“虚拟”的中断控制器,如GPIO 控制器。GPIO控制器也可以注册一个IRQ domain来管理GPIO中断,所以它也可以实现成为一个虚拟的中断控制器。
在linux kernel中,我们使用下面两个ID来标识一个来自外设的中断:
1、IRQ number。CPU需要为每一个外设中断编号,我们称之IRQ Number。这个IRQ number是一个虚拟的interrupt ID,和硬件无关,仅仅是被CPU用来标识一个外设中断。
2、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上是会重复编码的)。
这样,CPU和interrupt controller在标识中断上就有了一些不同的概念,但是,对于驱动工程师而言,我们和CPU视角是一样的,我们只希望得到一个IRQ number,而不关系具体是那个interrupt controller上的那个HW interrupt ID。这样一个好处是在中断相关的硬件发生变化的时候,驱动软件不需要修改。因此,linux kernel中的中断子系统需要提供一个将HW interrupt ID映射到IRQ number上来的机制, 最早的系统中,中断系统比较简单,可以认为静态的继续映射,可是随着系统的日趋复杂,就不得不采用一种新的方式来管理中断了,那就促使了irq domain的产生。
irq domain接口
1、向系统注册irq domain
通用中断处理模块中有一个irq domain的子模块,该模块将这种映射关系分成了三类:
(1)线性映射。其实就是一个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);
}
(2)Radix Tree map。建立一个Radix Tree来维护HW interrupt ID到IRQ number映射关系。HW interrupt ID作为lookup key,在Radix Tree检索到IRQ number。如果的确不能满足线性映射的条件,可以考虑Radix Tree map。实际上,内核中使用Radix Tree map的只有powerPC和MIPS的硬件平台。对于Radix Tree map,其接口API如下:
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, 0, ~0, 0, ops, host_data);
}
(3)no map。有些中断控制器很强,可以通过寄存器配置HW interrupt ID而不是由物理连接决定的。例如PowerPC 系统使用的MPIC (Multi-Processor Interrupt Controller)。在这种情况下,不需要进行映射,我们直接把IRQ number写入HW interrupt ID配置寄存器就OK了,这时候,生成的HW interrupt ID就是IRQ number,也就不需要进行mapping了。对于这种类型的映射,其接口API如下:
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, 0, max_irq, max_irq, ops, host_data);
}
这类接口的逻辑很简单,根据自己的映射类型,初始化struct irq_domain中的各个成员,调用__irq_domain_add将该irq domain挂入irq_domain_list的全局列表。
2、为irq domain创建映射
上节的内容主要是向系统注册一个irq domain,具体HW interrupt ID和IRQ number的映射关系都是空的,因此,具体各个irq domain如何管理映射所需要的database还是需要建立的。例如:对于线性映射的irq domain,我们需要建立线性映射的lookup table,对于Radix Tree map,我们要把Radix tree建立起来。创建映射有四个接口函数:
(1)调用irq_create_mapping函数建立HW interrupt ID和IRQ number的映射关系。该接口函数以irq domain和HW interrupt ID为参数,返回IRQ number(这个IRQ number是动态分配的)。该函数的原型定义如下:
extern unsigned int irq_create_mapping(struct irq_domain *host,
irq_hw_number_t hwirq);
驱动调用该函数的时候必须提供HW interrupt ID,也就是意味着driver知道自己使用的HW interrupt ID,而一般情况下,HW interrupt ID其实对具体的driver应该是不可见的,不过有些场景比较特殊,例如GPIO类型的中断,它的HW interrupt ID和GPIO有着特定的关系,driver知道自己使用那个GPIO,也就是知道使用哪一个HW interrupt ID了。
(2)irq_create_strict_mappings。这个接口函数用来为一组HW interrupt ID建立映射。具体函数的原型定义如下:
extern int irq_create_strict_mappings(struct irq_domain *domain,
unsigned int irq_base,
irq_hw_number_t hwirq_base, int count);
(3)irq_create_of_mapping。看到函数名字中的of(open firmware),我想你也可以猜到了几分,这个接口当然是利用device tree进行映射关系的建立。具体函数的原型定义如下:
extern unsigned int irq_create_of_mapping(struct of_phandle_args *irq_data);
通常,一个普通设备的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
}
对于一个使用Device tree的普通驱动程序(我们推荐这样做),基本上初始化需要调用irq_of_parse_and_map获取IRQ number,然后调用request_threaded_irq申请中断handler。
(4)irq_create_direct_mapping。这是给no map那种类型的interrupt controller使用的,这里不再赘述。
硬件中断处理流程
一旦发生硬件中断,经过CPU architecture相关的中断代码之后,会调用irq handler,该函数的一般过程如下:
(1)首先找到root interrupt controller对应的irq domain。
(2)根据HW 寄存器信息和irq domain信息获取HW interrupt ID
(3)调用irq_find_mapping找到HW interrupt ID对应的irq number
(4)调用handle_IRQ(对于ARM平台)来处理该irq number
对于级联的情况,过程类似上面的描述,但是需要注意的是在步骤4中不是直接调用该IRQ的hander来处理该irq number因为,这个irq需要各个interrupt controller level上的解析。举一个简单的二阶级联情况:假设系统中有两个interrupt controller,A和B,A是root interrupt controller,B连接到A的13号HW interrupt ID上。在B interrupt controller初始化的时候,除了初始化它作为interrupt controller的那部分内容,还有初始化它作为root interrupt controller A上的一个普通外设这部分的内容。最重要的是调用irq_set_chained_handler设定handler。这样,在上面的步骤4的时候,就会调用13号HW interrupt ID对应的handler(也就是B的handler),在该handler中,会重复上面的(1)~(4)。