linux-gic中断分析
这里主要分析 linux kernel 中 GICv3 中断控制器的代码(drivers/irqchip/irq-gic-v3.c)。
一、设备树
先来看下中断控制器的设备树信息:
gic: interrupt-controller@3400000 {
compatible = "arm,gic-v3";
#interrupt-cells = <3>;
#address-cells = <0>;
ranges;
interrupts = <GIC_PPI 9 0xf04>; /* GIC Maintenence IRQ */
interrupt-controller;
reg = <0x0 0x03400000 0 0x10000>, /* GIC Dist */
<0x0 0x03460000 0 0xFF004>; /* GIC Re */
interrupt-parent = <&gic>;
};
二、初始化
1. irq chip driver 的声明
IRQCHIP_DECLARE(gic_v3, "arm,gic-v3", gic_of_init);
定义 IRQCHIP_DECLARE 之后,相应的内容会保存到 __irqchip_of_table 里边:
#define IRQCHIP_DECLARE(name, compat, fn) OF_DECLARE_2(irqchip, name, compat, fn)
#define OF_DECLARE_2(table, name, compat, fn) \
_OF_DECLARE(table, name, compat, fn, of_init_fn_2)
#define _OF_DECLARE(table, name, compat, fn, fn_type) \
static const struct of_device_id __of_table_##name \
__used __section("__" #table "_of_table") \
__aligned(__alignof__(struct of_device_id)) \
= { .compatible = compat, \
.data = (fn == (fn_type)NULL) ? fn : fn } /* 存放回调函数gic_of_init */
这里展开后,会定义一个__of_table_gic_v3结构体,存放在 __irqchip_of_table段里面,该段用于存放中断控制器信息:
/* linux-5.15/include/asm-generic/vmlinux.lds.h */
#define _OF_TABLE_1(name) \
. = ALIGN(8); \
__##name##_of_table = .; \
KEEP(*(__##name##_of_table)) \
KEEP(*(__##name##_of_table_end))
#define OF_TABLE(cfg, name) __OF_TABLE(IS_ENABLED(cfg), name)
#define IRQCHIP_OF_MATCH_TABLE() OF_TABLE(CONFIG_IRQCHIP, irqchip)
在内核启动初始化中断的函数中,of_irq_init 函数会去查找设备节点信息,该函数的传入参数就是 __irqchip_of_table 段,由于 IRQCHIP_DECLARE 已经将信息填充好了,of_irq_init 函数会根据 “arm,gic-v3” 去查找对应的设备节点,并获取设备的信息。or_irq_init 函数中,最终会回调 IRQCHIP_DECLARE 声明的回调函数,也就是 gic_of_init,而这个函数就是 GIC 驱动的初始化入口。
start_kernel
init_IRQ
init_irq_stacks /* 为每个CPU分配中断栈 */
irqchip_init
of_irq_init(__irqchip_of_table);
desc->irq_init_cb = match->data;
desc->irq_init_cb(desc->dev, desc->interrupt_parent); /* 指向回调函数gic_of_init */
--->gic_of_init
2. gic_of_init 流程
static struct gic_chip_data gic_data[CONFIG_ARM_GIC_MAX_NR] __read_mostly;
gic_of_init
--->dist_base = of_iomap(node, 0); /* 映射distributor地址空间 */
if (of_property_read_u32(node, "#redistributor-regions", &nr_redist_regions))
nr_redist_regions = 1; /* sunxi的设备树没有“#redistributor-regions”属性,所以这里默认就是1 */
for (i = 0; i < nr_redist_regions; i++) {
struct resource res;
int ret;
ret = of_address_to_resource(node, 1 + i, &res);
rdist_regs[i].redist_base = of_iomap(node, 1 + i); /* 映射redistributor地址空间 */
if (ret || !rdist_regs[i].redist_base) {
pr_err("%pOF: couldn't map region %d\n", node, i);
err = -ENODEV;
goto out_unmap_rdist;
}
rdist_regs[i].phys_base = res.start;
}
--->gic_init_bases(dist_base, rdist_regs, nr_redist_regions, redist_stride, &node->fwnode);
typer = readl_relaxed(gic_data.dist_base + GICD_TYPER);
gic_data.rdists.gicd_typer = typer; /* 获取支持的中断数量 */
/* 向系统中注册一个 irq domain 的数据结构,irq_domain 主要作用是将硬件中断号映射到 irq number */
gic_data.domain = irq_domain_create_tree(handle, &gic_irq_domain_ops, &gic_data);
gic_data.rdists.rdist = alloc_percpu(typeof(*gic_data.rdists.rdist));
......
set_handle_irq(gic_handle_irq);
handle_arch_irq = handle_irq; /* 设置arm中断的全局入口函数为gic_handle_irq */
gic_update_rdist_properties(); /* 获取rdist的基本信息,填充至gic_data结构体 */
gic_dist_init(); /* 初始化 Distributor */
--->writel_relaxed(0, base + GICD_CTLR); /* Disable the distributor */
for (i = 32; i < GIC_LINE_NR; i += 32) /* 将 SPI 配置为非安全组 1 */
writel_relaxed(~0, base + GICD_IGROUPR + i / 8);
for (i = 0; i < GIC_ESPI_NR; i += 32) {
writel_relaxed(~0U, base + GICD_ICENABLERnE + i / 8); /* 禁止将扩展SPI转发到CPU接口 */
writel_relaxed(~0U, base + GICD_ICACTIVERnE + i / 8); /* 停用扩展SPI中断 */
}
for (i = 0; i < GIC_ESPI_NR; i += 32)
writel_relaxed(~0U, base + GICD_IGROUPRnE + i / 8); /* 控制扩展SPI范围中对应的SPI在组1 */
for (i = 0; i < GIC_ESPI_NR; i += 16)
writel_relaxed(0, base + GICD_ICFGRnE + i / 4); /* 设置扩展SPI中断是电平触发 */
for (i = 0; i < GIC_ESPI_NR; i += 4)
writel_relaxed(GICD_INT_DEF_PRI_X4, base + GICD_IPRIORITYRnE + i); /* 设置扩展SPI的中断优先级 */
gic_dist_config(base, GIC_LINE_NR, gic_dist_wait_for_rwp);
--->for (i = 32; i < gic_irqs; i += 16)
writel_relaxed(GICD_INT_ACTLOW_LVLTRIG, base + GIC_DIST_CONFIG + i / 4); /* 设置SPI中断为电平触发,低电平有效 */
for (i = 32; i < gic_irqs; i += 4)
writel_relaxed(GICD_INT_DEF_PRI_X4, base + GIC_DIST_PRI + i); /* 设置SPI的中断优先级 */
for (i = 32; i < gic_irqs; i += 32) {
writel_relaxed(GICD_INT_EN_CLR_X32, base + GIC_DIST_ACTIVE_CLEAR + i / 8); /* 禁止将SPI转发到CPU接口 */
writel_relaxed(GICD_INT_EN_CLR_X32, base + GIC_DIST_ENABLE_CLEAR + i / 8); /* 停用SPI中断 */
}
val = GICD_CTLR_ARE_NS | GICD_CTLR_ENABLE_G1A | GICD_CTLR_ENABLE_G1;
writel_relaxed(val, base + GICD_CTLR); /* 为非安全状态启用关联路由,启用非安全组1中断 */
/* 将所有spi中断路由到当前启动的cpu */
affinity = gic_mpidr_to_affinity(cpu_logical_map(smp_processor_id()));
for (i = 32; i < GIC_LINE_NR; i++) {
trace_android_vh_gic_v3_affinity_init(i, GICD_IROUTER, &affinity);
gic_write_irouter(affinity, base + GICD_IROUTER + i * 8);
}
for (i = 0; i < GIC_ESPI_NR; i++) {
trace_android_vh_gic_v3_affinity_init(i, GICD_IROUTERnE, &affinity);
gic_write_irouter(affinity, base + GICD_IROUTERnE + i * 8);
}
gic_cpu_init(); /* 初始化cpu interface */
--->gic_populate_rdist();
gic_enable_redist(true);
rbase = gic_data_rdist_rd_base();
val = readl_relaxed(rbase + GICR_WAKER);
if (enable)
/* Wake up this CPU redistributor */
val &= ~GICR_WAKER_ProcessorSleep; /* 此PE没有处于,也没有进入低功耗模式 */
else
val |= GICR_WAKER_ProcessorSleep;
writel_relaxed(val, rbase + GICR_WAKER);
rbase = gic_data_rdist_sgi_base();
/* Configure SGIs/PPIs as non-secure Group-1 */
for (i = 0; i < gic_data.ppi_nr + 16; i += 32)
writel_relaxed(~0, rbase + GICR_IGROUPR0 + i / 8);
gic_cpu_sys_reg_init();
gic_enable_sre(); /* 使能系统寄存器接口 */
if (static_branch_likely(&supports_deactivate_key)) { /* 设置EOI模式,默认是mode 1 */
/* EOI drops priority only (mode 1) */
gic_write_ctlr(ICC_CTLR_EL1_EOImode_drop);
} else {
/* EOI deactivates interrupt too (mode 0) */
gic_write_ctlr(ICC_CTLR_EL1_EOImode_drop_dir);
}
gic_smp_init(); /* Register all 8 non-secure SGIs */
/* 设置cpu热拔插gic的回调函数 */
cpuhp_setup_state_nocalls(CPUHP_AP_IRQ_GIC_STARTING, "irqchip/arm/gicv3:starting", gic_starting_cpu, NULL);
base_sgi = __irq_domain_alloc_irqs(gic_data.domain, -1, 8, NUMA_NO_NODE, &sgi_fwspec, false, NULL);
set_smp_ipi_range(base_sgi, 8);
gic_syscore_init();
register_syscore_ops(&gic_syscore_ops);
......
gic_enable_nmi_support();
--->gic_populate_ppi_partitions(node); /* 设置一组 PPI 的亲和性 */
3. 初始化流程总结
见下图:
三、数据结构
1. struct irq_chip
描述的是中断控制器的底层操作函数集,这些函数集最终完成对控制器硬件的操作:
struct irq_chip {
struct device *parent_device;
const char *name;
unsigned int (*irq_startup)(struct irq_data *data);
void (*irq_shutdown)(struct irq_data *data);
void (*irq_enable)(struct irq_data *data);
void (*irq_disable)(struct irq_data *data);
void (*irq_ack)(struct irq_data *data);
void (*irq_mask)(struct irq_data *data);
void (*irq_mask_ack)(struct irq_data *data);
void (*irq_unmask)(struct irq_data *data);
void (*irq_eoi)(struct irq_data *data);
int (*irq_set_affinity)(struct irq_data *data, const struct cpumask *dest, bool force);
int (*irq_retrigger)(struct irq_data *data);
int (*irq_set_type)(struct irq_data *data, unsigned int flow_type);
int (*irq_set_wake)(struct irq_data *data, unsigned int on);
void (*irq_bus_lock)(struct irq_data *data);
void (*irq_bus_sync_unlock)(struct irq_data *data);
void (*irq_cpu_online)(struct irq_data *data);
void (*irq_cpu_offline)(struct irq_data *data);
void (*irq_suspend)(struct irq_data *data);
void (*irq_resume)(struct irq_data *data);
void (*irq_pm_shutdown)(struct irq_data *data);
void (*irq_calc_mask)(struct irq_data *data);
void (*irq_print_chip)(struct irq_data *data, struct seq_file *p);
int (*irq_request_resources)(struct irq_data *data);
void (*irq_release_resources)(struct irq_data *data);
void (*irq_compose_msi_msg)(struct irq_data *data, struct msi_msg *msg);
void (*irq_write_msi_msg)(struct irq_data *data, struct msi_msg *msg);
int (*irq_get_irqchip_state)(struct irq_data *data, enum irqchip_irq_state which, bool *state);
int (*irq_set_irqchip_state)(struct irq_data *data, enum irqchip_irq_state which, bool state);
int (*irq_set_vcpu_affinity)(struct irq_data *data, void *vcpu_info);
void (*ipi_send_single)(struct irq_data *data, unsigned int cpu);
void (*ipi_send_mask)(struct irq_data *data, const struct cpumask *dest);
int (*irq_nmi_setup)(struct irq_data *data);
void (*irq_nmi_teardown)(struct irq_data *data);
unsigned long flags;
};
2. struct irq_domain
irq_domain用于将硬件的中断号,转换成Linux系统中的中断号(virtual irq, virq
)
struct irq_domain {
struct list_head link;
const char *name;
const struct irq_domain_ops *ops;
void *host_data;
unsigned int flags;
unsigned int mapcount;
/* 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
ANDROID_KABI_RESERVE(1);
ANDROID_KABI_RESERVE(2);
ANDROID_KABI_RESERVE(3);
ANDROID_KABI_RESERVE(4);
/* reverse map data. The linear map gets appended to the irq_domain */
irq_hw_number_t hwirq_max;
unsigned int revmap_size;
struct radix_tree_root revmap_tree;
struct mutex revmap_mutex;
struct irq_data __rcu *revmap[];
};
- 每个中断控制器都对应一个irq_domain
- 中断控制器驱动通过
irq_domain_add_*()
接口来创建irq_domain - irq_domain支持三种映射方式:
- linear map:维护固定大小的表,索引是硬件中断号,如果硬件中断最大数量固定,并且数值不大,可以选择线性映射
- tree map:硬件中断号可能很大,可以选择树映射
- no map:硬件中断号直接就是Linux的中断号
irq_domain 的引入相当于一个中断控制器就是一个 irq_domain,这样一来所有的中断控制器就会出现级联的布局。利用树状的结构可以充分的利用 irq 数目,而且每一个 irq_domain 区域可以自己去管理自己 interrupt 的特性。
每一个中断控制器对应多个中断号, 而硬件中断号在不同的中断控制器上是会重复编码的, 这时仅仅用硬中断号已经不能唯一标识一个外设中断,因此 linux kernel 提供了一个虚拟中断号(virq)的概念。
3. struct irq_data
中断描述符中应该会包括底层irq chip相关的数据结构,linux kernel中把这些数据组织在一起,形成struct irq_data。
struct irq_data {
u32 mask;
unsigned int irq;
unsigned long hwirq;
struct irq_common_data *common;
struct irq_chip *chip;
struct irq_domain *domain;
#ifdef CONFIG_IRQ_DOMAIN_HIERARCHY
struct irq_data *parent_data;
#endif
void *chip_data;
};
-
irq:虚拟中断号
-
hwirq:硬件中断号
-
chip:对应的 irq_chip 数据结构
-
domain:对应的 irq_domain 数据结构
4. struct irq_desc
描述一个外设的中断,称之中断描述符
struct irq_desc {
struct irq_common_data irq_common_data;
struct irq_data irq_data;
unsigned int __percpu *kstat_irqs;
irq_flow_handler_t handle_irq;
struct irqaction *action; /* IRQ action list */
unsigned int status_use_accessors;
unsigned int core_internal_state__do_not_mess_with_it;
unsigned int depth; /* nested irq disables */
unsigned int wake_depth; /* nested wake enables */
unsigned int tot_count;
unsigned int irq_count; /* For detecting broken IRQs */
unsigned long last_unhandled; /* Aging timer for unhandled count */
unsigned int irqs_unhandled;
atomic_t threads_handled;
int threads_handled_last;
raw_spinlock_t lock;
struct cpumask *percpu_enabled;
const struct cpumask *percpu_affinity;
#ifdef CONFIG_SMP
const struct cpumask *affinity_hint;
struct irq_affinity_notify *affinity_notify;
......
int parent_irq;
struct module *owner;
const char *name;
} ____cacheline_internodealigned_in_smp;
下面这张是总的数据结构关联图,核心是围绕着struct irq_desc
来展开的:
-
Linux内核的中断处理,围绕着中断描述符结构
struct irq_desc
展开,内核提供了两种中断描述符组织形式:-
打开
CONFIG_SPARSE_IRQ
宏(中断编号不连续),中断描述符以radix-tree
来组织,用户在初始化时进行动态分配,然后再插入radix-tree
中。 -
关闭
CONFIG_SPARSE_IRQ
宏(中断编号连续),中断描述符以数组的形式组织,并且已经分配好。 -
不管哪种形式,最终都可以通过
linux irq
号来找到对应的中断描述符。
-
四、关键流程分析
1. 创建映射关系
驱动中通常会使用platform_get_irq或irq_of_parse_and_map接口去根据设备树的信息创建映射关系(硬件中断号到linux irq中断号映射),然后调用request_irq()接口或者request_threaded_irq()接口来注册设备的中断处理函数。
unsigned int irq_of_parse_and_map(struct device_node *dev, int index)
--->if (of_irq_parse_one(dev, index, &oirq)) /* 分析device node中的interrupt相关属性,存放到oirq->args[]中 */
return 0;
--->return irq_create_of_mapping(&oirq); /* 创建映射,并返回对应的IRQ number */
--->irq_create_fwspec_mapping(&fwspec);
/* 根据给定的fwspec找到一个合适的domain,这是根据传递进来的参数irq_data的np成员来寻找的 */
--->domain = irq_find_matching_fwspec(fwspec, DOMAIN_BUS_WIRED);
--->irq_domain_translate(domain, fwspec, &hwirq, &type);
/* 这里调用gic_irq_domain_translate,对硬中断号进行转换保存在hwirq中,GIC硬中断号=dts硬中断号+32 */
d->ops->translate(d, fwspec, hwirq, type);
*hwirq = fwspec->param[1] + 32; /* SPI */
--->virq = irq_find_mapping(domain, hwirq); /* 通过hwirq去查询,这个hwirq是否已经映射,如果已经映射,就返回virq */
--->if (virq) {
if (type == IRQ_TYPE_NONE || type == irq_get_trigger_type(virq))
return virq;
}
--->if (irq_domain_is_hierarchy(domain)) /* 中断级联 */
--->virq = irq_domain_alloc_irqs(domain, 1, NUMA_NO_NODE, fwspec);
--->__irq_domain_alloc_irqs(domain, -1, nr_irqs, node, arg, false, NULL);
--->virq = irq_domain_alloc_descs(irq_base, nr_irqs, 0, node, affinity); /* 申请一个虚拟中断号virq */
--->irq_domain_alloc_irq_data(domain, virq, nr_irqs);
--->irq_domain_alloc_irqs_hierarchy(domain, virq, nr_irqs, arg);
domain->ops->alloc(domain, irq_base, nr_irqs, arg); /* 执行gic_irq_domain_alloc函数 */
gic_irq_domain_translate(domain, fwspec, &hwirq, &type);
for (i = 0; i < nr_irqs; i++) {
gic_irq_domain_map(domain, virq + i, hwirq + i);
switch (__get_intid_range(hw)) {
case SGI_RANGE:
irq_domain_set_info(d, irq, hw, chip, d->host_data, handle_percpu_devid_fasteoi_ipi, NULL, NULL); /* 如果中断是属于SGI,则入口函数是handle_percpu_devid_fasteoi_ipi */
case PPI_RANGE:
irq_domain_set_info(d, irq, hw, chip, d->host_data, handle_percpu_devid_irq, NULL, NULL); /* 如果中断是属于PPI,则入口函数是handle_percpu_devid_irq */
case SPI_RANGE:
irq_domain_set_info(d, irq, hw, chip, d->host_data, handle_fasteoi_irq, NULL, NULL); /* 如果中断是属于SPI,则入口函数是handle_fasteoi_irq */
irq_set_chip_and_handler_name(virq, chip, handler, handler_name);
__irq_set_handler(irq, handle, 0, name);
__irq_do_set_handler(desc, handle, is_chained, name);
desc->handle_irq = handle; /* 设置highlevel的中断处理函数:desc->handle_irq = handle_fasteoi_irq */
desc->action = &chained_action; /* action链表,成员handler存放中断服务函数 */
}
--->for (i = 0; i < nr_irqs; i++)
irq_domain_insert_irq(virq + i);
for (data = irq_get_irq_data(virq); data; data = data->parent_data)
/* 通过线性映射或radix tree映射,以硬中断号为索引,建立硬中断与软中断的映射关系 */
irq_domain_set_mapping(domain, data->hwirq, data);
radix_tree_insert(&domain->revmap_tree, hwirq, irq_data);
--->else /* 非中断级联 */
virq = irq_create_mapping(domain, hwirq); /* hwirq没有映射,跟据硬件中断号,分配软件虚拟中断号, 期间会创建irq_desc */
--->irq_create_mapping_affinity(host, hwirq, NULL); /* 将硬件中断映射到 linux irq 空间 */
--->virq = irq_domain_alloc_descs(-1, 1, hwirq, of_node_to_nid(of_node), affinity); /* 申请一个虚拟中断号virq */
if (virq >= 0) /* 指定了virq号 */
virq = __irq_alloc_descs(virq, virq, cnt, node, THIS_MODULE, affinity);
else /* 没有指定virq号 */
virq = __irq_alloc_descs(-1, hint, cnt, node, THIS_MODULE, affinity);
/* 从全局allocated_irqs,从from开始查找cnt个为0的bit位置返回到start */
start = bitmap_find_next_zero_area(allocated_irqs, IRQ_BITMAP_BITS, from, cnt, 0);
alloc_descs(start, cnt, node, affinity, owner);
for (i = 0; i < cnt; i++) {
desc = alloc_desc(start + i, node, flags, mask, owner); /* 分配中断描述符 */
irq_insert_desc(start + i, desc); /* 中断描述符插入到全局radix tree :irq_desc_tree */
radix_tree_insert(&irq_desc_tree, irq, desc);
}
bitmap_set(allocated_irqs, start, cnt); /* 根据已经分配的中断描述符置位allocated_irqs */
/* 通过线性映射或radix tree映射,以硬中断号为索引,建立硬中断与软中断的映射关系 */
--->irq_domain_associate(domain, virq, hwirq);
struct irq_data *irq_data = irq_get_irq_data(virq); /* 根据virq找到对应的irq_data */
if (domain->ops->map) /* gicv3的gic_irq_domain_ops没有实现map */
domain->ops->map(domain, virq, hwirq);
irq_domain_set_mapping(domain, hwirq, irq_data);
/* 区分硬中断号是否大于domain->revmap_size,如果小于domain->revmap_size,则插入线性映射数组,数组索引为硬中断号 */
if (hwirq < domain->revmap_size)
rcu_assign_pointer(domain->revmap[hwirq], irq_data);
domain->revmap[hwirq] = irq_data;
else
/* 将virq对应的irq_data链入到radix tree,硬中断号为索引 */
radix_tree_insert(&domain->revmap_tree, hwirq, irq_data);
--->irq_data = irq_get_irq_data(virq);
--->irqd_set_trigger_type(irq_data, type); /* 设定触发类型 */
2. 中断注册
设备驱动在获取到了irq中断号后,通常就会采用request_irq/request_threaded_irq来注册中断,其中request_irq用于注册普通处理的中断,request_threaded_irq用于注册线程化处理的中断。线程化中断的主要目是把中断上下文的任务迁移到线程中,减少系统关中断的时间,增强系统的实时性。
ps:request_irq也是调用request_threaded_irq,只是在传参的时候,线程处理函数thread_fn函数设置成NULL。
static inline int __must_check request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev)
request_threaded_irq(irq, handler, NULL, flags, name, dev);
下面来分析一下request_threaded_irq函数:
参数描述:
输入参数 | 描述 |
---|---|
irq | 要注册handler的那个IRQ number。这里要注册的handler包括两个,一个是传统意义的中断handler,我们称之primary handler,另外一个是threaded interrupt handler |
handler | primary handler。需要注意的是primary handler和threaded interrupt handler不能同时为空,否则会出错 |
thread_fn | threaded interrupt handler。如果该参数不是NULL,那么系统会创建一个kernel thread,调用的function就是thread_fn |
irqflags | 见下面的表格 |
devname | 申请的中断对应的设备名称 |
dev_id | 对应的设备描述符指针 |
常见irqflags种类:(include/linux/interrupt.h)
flag定义 | 描述 |
---|---|
IRQF_TRIGGER_XXX | 描述该interrupt line触发类型的flag |
IRQF_SHARED | 允许在多个设备之间共享 irq |
IRQF_PERCPU | 在SMP的架构下,中断有两种mode,一种中断是在所有processor之间共享的,也就是global的,一旦中断产生,interrupt controller可以把这个中断送达多个处理器。当然,在具体实现的时候不会同时将中断送达多个CPU,一般是软件和硬件协同处理,将中断送达一个CPU处理。但是一段时间内产生的中断可以平均(或者按照既定的策略)分配到一组CPU上。这种interrupt mode下,interrupt controller针对该中断的operational register是global的,所有的CPU看到的都是一套寄存器,一旦一个CPU ack了该中断,那么其他的CPU看到的该interupt source的状态也是已经ack的状态。 和global对应的就是per cpu interrupt了,对于这种interrupt,不是processor之间共享的,而是特定属于一个CPU的。例如GIC中interrupt ID等于30的中断就是per cpu的(这个中断event被用于各个CPU的local timer),这个中断号虽然只有一个,但是,实际上控制该interrupt ID的寄存器有n组(如果系统中有n个processor),每个CPU看到的是不同的控制寄存器。在具体实现中,这些寄存器组有两种形态,一种是banked,所有CPU操作同样的寄存器地址,硬件系统会根据访问的cpu定向到不同的寄存器,另外一种是non banked,也就是说,对于该interrupt source,每个cpu都有自己独特的访问地址 |
IRQF_NO_THREAD | 有些low level的interrupt是不能线程化的(例如系统timer的中断),这个flag就是起这个作用的。另外,有些级联的interrupt controller对应的IRQ也是不能线程化的(例如secondary GIC对应的IRQ),它的线程化可能会影响一大批附属于该interrupt controller的外设的中断响应延迟 |
IRQF_NOBALANCING | 这也是和multi-processor相关的一个flag。对于那些可以在多个CPU之间共享的中断,具体送达哪一个processor是有策略的,我们可以在多个CPU之间进行平衡。如果你不想让你的中断参与到irq balancing的过程中那么就设定这个flag |
IRQF_ONESHOT | one shot本身的意思是只有一次的,结合到中断这个场景,则表示中断是一次性触发的,不能嵌套 |
IRQF_NO_SUSPEND | 这个flag比较好理解,就是说在系统suspend的时候,不用disable这个中断,如果disable,可能会导致系统不能正常的resume |
int request_threaded_irq(unsigned int irq, irq_handler_t handler, irq_handler_t thread_fn,
unsigned long irqflags, const char *devname, void *dev_id)
--->desc = irq_to_desc(irq); /* 根据linux中断号,获取中断描述符irq_desc */
/* 分配新的action数据结构,填充里面的相关成员 */
--->action = kzalloc(sizeof(struct irqaction), GFP_KERNEL);
--->action->handler = handler;
--->action->thread_fn = thread_fn;
--->action->flags = irqflags;
--->action->name = devname;
--->action->dev_id = dev_id;
--->retval = __setup_irq(irq, desc, action); /* 用于完成中断的相关设置,包括中断线程化的处理 */
struct irqaction *old, **old_ptr;
......
nested = irq_settings_is_nested_thread(desc); /* 检查中断是否嵌套在另外一个中断线程中 */
if (nested) {
new->handler = irq_nested_primary_handler;
}
else {
if (irq_settings_can_thread(desc) /* 中断没有嵌套,检查能否线程化 */
irq_setup_forced_threading(new); /* 中断强制线程化设置 */
}
if (new->thread_fn && !nested) /* 设置了线程化处理函数且没有嵌套 */
setup_irq_thread(new, irq, false); /* 为该中断创建一个内核线程 */
t = kthread_create(irq_thread, new, "irq/%d-s-%s", irq, new->name);
if (!desc->action)
irq_request_resources(desc);
old_ptr = &desc->action; /* old_ptr指向desc->action */
old = *old_ptr; /* old指向注册之前的action list,如果不是NULL,那么说明需要共享interrupt line */
if (old)
{
......
/*
desc->action非空的话,代表中断需要共享。中断共享,irqaction会连成链表,每个irqaction有thread_mask
位图,当所有共享中断都处理完才能unmask中断。
*/
do {
/*
* Or all existing action->thread_mask bits,
* so we can find the next zero bit for this
* new action.
*/
thread_mask |= old->thread_mask;
old_ptr = &old->next;
old = *old_ptr;
} while (old);
shared = 1;
}
......
if (!shared) { /* 中断非共享 */
if (new->flags & IRQF_TRIGGER_MASK)
__irq_set_trigger(desc, new->flags & IRQF_TRIGGER_MASK); /* 设置中断触发类型 */
irq_activate(desc);
struct irq_data *d = irq_desc_get_irq_data(desc);
irq_domain_activate_irq(d, false);
ret = __irq_domain_activate_irq(irq_data, reserve);
if (!ret)
irqd_set_activated(irq_data);
__irqd_to_state(d) |= IRQD_ACTIVATED;
irqd_clear(&desc->irq_data, IRQD_IRQ_INPROGRESS);
}
/* 中断服务函数已经赋值:desc->action->handler = action->handler,这里将该irqaction(new)挂入队列的尾部 */
*old_ptr = new;
......
__enable_irq(desc);
--->irq_startup(desc, IRQ_RESEND, IRQ_START_FORCE);
--->irq_enable(desc);
--->unmask_irq(desc);
--->desc->irq_data.chip->irq_unmask(&desc->irq_data); /* 调用gicv3的api */
--->gic_unmask_irq
--->gic_poke_irq(d, GICD_ISENABLER); /* 修改GICD_ISENABLER寄存器,使能对应位的中断 */
......
register_irq_proc(irq, desc);
register_handler_proc(irq, new);
3. 中断响应
当完成中断的注册后,所有结构的组织关系都已经建立好,剩下就是等信号来临时,进行中断的处理工作。
假设当前在 EL0 运行一个应用程序,触发了一个 EL0 的 irq 中断,则处理器会做如下的操作:
先跳到 arm64 对应的异常向量表:(arch/arm64/kernel/entry.S)
SYM_CODE_START(vectors)
kernel_ventry 1, t, 64, sync // Synchronous EL1t
kernel_ventry 1, t, 64, irq // IRQ EL1t
kernel_ventry 1, t, 64, fiq // FIQ EL1h
kernel_ventry 1, t, 64, error // Error EL1t
kernel_ventry 1, h, 64, sync // Synchronous EL1h
kernel_ventry 1, h, 64, irq // IRQ EL1h
kernel_ventry 1, h, 64, fiq // FIQ EL1h
kernel_ventry 1, h, 64, error // Error EL1h
kernel_ventry 0, t, 64, sync // Synchronous 64-bit EL0
kernel_ventry 0, t, 64, irq // IRQ 64-bit EL0
kernel_ventry 0, t, 64, fiq // FIQ 64-bit EL0
kernel_ventry 0, t, 64, error // Error 64-bit EL0
kernel_ventry 0, t, 32, sync // Synchronous 32-bit EL0
kernel_ventry 0, t, 32, irq // IRQ 32-bit EL0
kernel_ventry 0, t, 32, fiq // FIQ 32-bit EL0
kernel_ventry 0, t, 32, error // Error 32-bit EL0
SYM_CODE_END(vectors)
问:这里的后缀t和h分别是什么意思?
答:这是用来标记是否使用共享栈SP_EL0:
arm64 的异常向量表 vectors 中设置了各种异常的入口。kernel_ventry 展开后,可以看到有效的异常入口有两个同步异常 el0_sync,el1_sync 和两个异步异常 el0_irq,el1_irq,中断属于异步异常。
kernel_ventry 0, t, 64, irq
--->el0t_64_irq
--->kernel_entry 0, 64 /* 保存现场:将 CPU 寄存器按照 pt_regs 结构体的定义将现场保存到栈上 */
--->el0t_64_irq_handler
__el0_irq_handler_common(regs);
el0_interrupt(regs, handle_arch_irq);
enter_from_user_mode(regs);
write_sysreg(DAIF_PROCCTX_NOIRQ, daif);
if (regs->pc & BIT(55))
arm64_apply_bp_hardening();
do_interrupt_handler(regs, handler);
handle_arch_irq(); /* 该函数在gicv3驱动初始化的时候有设置:gic_handle_irq */
exit_to_user_mode(regs);
--->ret_to_user
......
kernel_exit 0 /* 恢复现场:恢复 pt_regs 中的寄存器上下文 */
.....
eret /* 将SPSR_ELn寄存器的值恢复到PSTATE中 */
中断处理最终会进入 gic_handle_irq:
static asmlinkage void __exception_irq_entry gic_handle_irq(struct pt_regs *regs)
irqnr = do_read_iar(regs); /* 读取中断控制器的寄存器ICC_IAR1_EL1,并获取 hwirq */
if (static_branch_likely(&supports_deactivate_key))
gic_write_eoir(irqnr);
write_sysreg_s(irq, SYS_ICC_EOIR1_EL1); /* 对ICC_EOIR1_EL1寄存器的写操作表示中断的结束 */ ------------ (note)
handle_domain_irq(gic_data.domain, irqnr, regs);
struct pt_regs *old_regs = set_irq_regs(regs); /* 保存被中断的线程的上下文 */
irq_enter(); /* 进入中断上下文 */
desc = irq_resolve_mapping(domain, hwirq); /* 根据hwirq去查找linux中断号 */
handle_irq_desc(desc);
generic_handle_irq_desc(desc);
/*
highlevel irq-events handler,desc->handle_irq还不是指向真正的处理函数
spi中断的话,指向handle_fasteoi_irq
*/
desc->handle_irq(desc);
--->handle_fasteoi_irq
--->handle_irq_event(desc);
handle_irq_event_percpu(desc);
__handle_irq_event_percpu(desc, &flags);
for_each_action_of_desc(desc, action) {
res = action->handler(irq, action->dev_id); /* 这里执行irqnum对应的中断处理函数 */
switch (res) {
case IRQ_WAKE_THREAD: /* 中断线程化处理 */
__irq_wake_thread(desc, action);
case IRQ_HANDLED:
*flags |= action->flags;
break;
}
}
--->cond_unmask_eoi_irq(desc, chip);
chip->irq_eoi(&desc->irq_data); /* 执行gic_eoimode1_eoi_irq */
gic_write_dir(gic_irq(d));
write_sysreg_s(irq, SYS_ICC_DIR_EL1); /* 对ICC_DIR_EL1寄存器的写操作表示中断的deactivation */
unmask_irq(desc); /* 根据不同条件执行unmask_irq()解除中断屏蔽 */
irq_exit();/* 退出中断上下文 */
set_irq_regs(old_regs); /* 恢复被中断的线程的上下文 */
handle_fasteoi_irq:处理共享中断,并且遍历 irqaction 链表,逐个调用 action->handler() 函数,这个函数正是设备驱动程序调用 request_irq/request_threaded_irq 接口注册的中断处理函数,此外如果中断线程化处理的话,还会调用 __irq_wake_thread 唤醒内核线程。
------------ (note):
问:假设读取到了一个SPI中断, 为什么一开始就写EOI表示中断结束,此时中断处理不是还没有执行么?
答:在GIC v3协议中定义, 处理完中断后,软件必须通知中断控制器已经处理了中断,以便状态机可以转换到下一个状态。
GICv3架构将中断的完成分为2个阶段:
- Priority Drop:将运行优先级降回到中断之前的值。
- Deactivation:更新当前正在处理的中断的状态机。 从活动状态转换到非活动状态。
这两个阶段可以在一起完成,也可以分为2步完成。 却决于EOImode的值。
如果EOIMode = 0, 对ICC_EOIR1_EL1寄存器的操作代表2个阶段(priority drop 和 deactivation)一起完成。
如果EOImode = 1, 对ICC_EOIR1_EL1寄存器的操作只会导致Priority Drop, 如果想要表示中断已经处理完成,还需要写ICC_DIR_EL1。
所以回答上面的问题, 当前Linux GIC的代码,默认irq chip是EIOmode=1, 所以单独的写EOIR1_EL1不是代表中断结束。
4. 中断线程化
__irq_wake_thread
wake_up_process(action->thread); /* 唤醒中断处理线程irq_thread */
irq_thread
if (force_irqthreads() && test_bit(IRQTF_FORCED_THREAD, &action->thread_flags)) /* 强制线程化 */
handler_fn = irq_forced_thread_fn;
else
handler_fn = irq_thread_fn;
......
while (!irq_wait_for_interrupt(action)) {
action_ret = handler_fn(desc, action);
action->thread_fn(action->irq, action->dev_id); /* 运行最终的线程化处理函数 */
if (action_ret == IRQ_WAKE_THREAD)
irq_wake_secondary(desc, action);
}
irq_wait_for_interrupt
for (;;) {
set_current_state(TASK_INTERRUPTIBLE);
......
if (test_and_clear_bit(IRQTF_RUNTHREAD, &action->thread_flags)) { /* 测试中断线程的唤醒条件是否满足 */
__set_current_state(TASK_RUNNING); /* 满足条件,设置当前任务为TASK_RUNNING,并返回0 */
return 0;
}
schedule(); /* 不满足唤醒条件,执行调度,让出cpu */
}