Arm Linux中断溯源(三)
在小红薯上发内核学习求助贴,没想到就一会功夫啊就一会,得到了很多前辈的指导,大佬们给我推荐了几本书:《打通Linux操作系统和芯片开发》、《深入理解Linux内核》、《奔跑吧Linux内核》,有很多都是Arm64的,不是x86的,虽然和我验证用的I.MX6ULL和STM32MP157开发板的Arm32架构还不完全一样,但大概看了几眼,已经很贴近了,比x86的书看着舒服多了。还是建议找一本熟悉架构的
前言
在小红薯上发内核学习求助贴,没想到就一会功夫啊就一会,得到了很多前辈的指导,大佬们给我推荐了几本书:《打通Linux操作系统和芯片开发》、《深入理解Linux内核》、《奔跑吧Linux内核》,有很多都是Arm64的,不是x86的,虽然和我验证用的I.MX6ULL和STM32MP157开发板的Arm32架构还不完全一样,但大概看了几眼,已经很贴近了,比x86的书看着舒服多了。
还是建议找一本熟悉架构的书看,不然属实折磨…
gic_handle_irq函数
上一章学到,从汇编编写的向量入口,到C语言接口,一步步保存IRQ发生时的现场,来到了第一级中断控制器:gic_handle_irq函数,准备调用核心的handle_domain_irq(gic->domain, irqnr, regs):
I.MX6ULL内核
drivers/irqchip/irq-gic.c
static void __exception_irq_entry gic_handle_irq(struct pt_regs *regs)
{
u32 irqstat, irqnr;
struct gic_chip_data *gic = &gic_data[0];
void __iomem *cpu_base = gic_data_cpu_base(gic);
do {
irqstat = readl_relaxed(cpu_base + GIC_CPU_INTACK);
irqnr = irqstat & GICC_IAR_INT_ID_MASK;
if (likely(irqnr > 15 && irqnr < 1021)) {
handle_domain_irq(gic->domain, irqnr, regs);
continue;
}
if (irqnr < 16) {
writel_relaxed(irqstat, cpu_base + GIC_CPU_EOI);
#ifdef CONFIG_SMP
handle_IPI(irqnr, regs);
#endif
continue;
}
break;
} while (1);
}
现在的上下文是:svc模式,IRQ全局屏蔽,一路都是中断上下文进来的,除了IRQ模式切换成SVC外,没有其他的什么动作
首先是读出GIC控制器的中断号
irqstat = readl_relaxed(cpu_base + GIC_CPU_INTACK);
补充一下读写函数readl_relaxed和writel_relaxed,相比readl和writel函数(内核专用的 MMIO 读写函数),缺少了内存屏障,因此比较“relaxed”
GIC_CPU_INTACK就是GICC_IAR寄存器的别名,偏移量0x000C。这里现在存储着发生中断的IRQ号。
对于SGI中断(irqnr < 16),就地响应:
writel_relaxed(irqstat, cpu_base + GIC_CPU_EOI);
为什么要写GIC_CPU_EOI?AI补充:
GIC v2中断生命周期:
触发中断 → GIC 标记为 Pending(挂起)
读 GICC_IAR → GIC 标记为 Active(激活),锁定优先级
写 GICC_EOIR → GIC 标记为 Inactive(空闲),释放优先级
还挺讲究,处理完必须写GICC_EOIR。handle_domain_irq里面也会写。
上图为GIC中断处理状态转换,摘自《打通Linux操作系统和芯片开发》,但怎么感觉是给懂的人看的,不懂的看了也还是不懂
GIC v2的文章可以参考这篇,感觉写的挺好的,有空研究一下:
https://blog.csdn.net/OnlyLove_/article/details/122355993
那这个gic_handle_irq函数就没什么可分析的了,接下来是铺垫了很久的handle_domain_irq函数。
handle_domain_irq函数
irq_domain(中断域)
irq_domain意思是中断映射域,顾名思义,它描述的是对一个中断控制器范围的管理。
这个图里面,不止GIC有中断管理能力,还有GPIO1、UART1、Ethernet1,它们都有中断管理的能力,并且呈现级联的关系,每个能管理中断(管理自己那片“domain”)的外设,都被看做一个irq_domain。
现在,尝试分析这个函数的参数:
handle_domain_irq(gic->domain, irqnr, regs);
gic->domain表示的意思已经不言而喻了。配套的,irqnr就是这个domain下面,触发中断的原因(gic管理的domain的中断号)。类似的可以推测,handle_domain_irq(gpio1->domain, 0)就是处理GPIO1管理的domain中,编号为0的中断。
贴出完整的struct irq_domain:
I.MX6ULL内核
include/linux/irqdomain.h
/**
* struct irq_domain - Hardware interrupt number translation object
* @link: Element in global irq_domain list.
* @name: Name of interrupt domain
* @ops: pointer to irq_domain methods
* @host_data: private data pointer for use by owner. Not touched by irq_domain
* core code.
* @flags: host per irq_domain flags
*
* Optional elements
* @of_node: Pointer to device tree nodes associated with the irq_domain. Used
* when decoding device tree interrupt specifiers.
* @gc: Pointer to a list of generic chips. There is a helper function for
* setting up one or more generic chips for interrupt controllers
* drivers using the generic chip library which uses this pointer.
* @parent: Pointer to parent irq_domain to support hierarchy irq_domains
*
* Revmap data, used internally by irq_domain
* @revmap_direct_max_irq: The largest hwirq that can be set for controllers that
* support direct mapping
* @revmap_size: Size of the linear map table @linear_revmap[]
* @revmap_tree: Radix map tree for hwirqs that don't fit in the linear map
* @linear_revmap: Linear table of hwirq->virq reverse mappings
*/
struct irq_domain {
struct list_head link; 形成链表结构
const char *name; 用于调试的名字
const struct irq_domain_ops *ops; 这个domain对象具备的操作方法
void *host_data; 私有数据,对于gic指向的就是gic_chip_data
私有数据用于实现封装和多态,只有GIC的驱动程序知道这里放的是什么东西
unsigned int flags;
/* Optional data */
struct device_node *of_node; 设备树的节点对象,写过驱动的朋友应该不陌生
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 */
反向映射表 (硬件中断号 hwirq → 内核虚拟中断号 virq)
这些应该是实现反向映射的数据结构,也好理解
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[];
};
用面向对象的方式看,const struct irq_domain_ops *ops就存放了这个domain具备的方法:
I.MX6ULL内核
include/linux/irqdomain.h
/**
* struct irq_domain_ops - Methods for irq_domain objects
* @match: Match an interrupt controller device node to a host, returns
* 1 on a match
* @map: Create or update a mapping between a virtual irq number and a hw
* irq number. This is called only once for a given mapping.
* @unmap: Dispose of such a mapping
* @xlate: Given a device tree node and interrupt specifier, decode
* the hardware irq number and linux irq type value.
*
* Functions below are provided by the driver and called whenever a new mapping
* is created or an old mapping is disposed. The driver can then proceed to
* whatever internal data structures management is required. It also needs
* to setup the irq_desc when returning from map().
*/
struct irq_domain_ops {
匹配:将irq_domain和device_node绑定
int (*match)(struct irq_domain *d, struct device_node *node);
映射:创建/更新 内核虚拟中断号(virq) 和 硬件中断号(hwirq) 的对应关系
每个中断映射只会调用一次,用于初始化中断的基础配置
int (*map)(struct irq_domain *d, unsigned int virq, irq_hw_number_t hw);
解除映射:删除virq和hwirq的映射关系
void (*unmap)(struct irq_domain *d, unsigned int virq);
翻译:解析设备树中断配置 → 输出硬件中断号 + 中断触发类型
int (*xlate)(struct irq_domain *d, struct device_node *node,
const u32 *intspec, unsigned int intsize,
unsigned long *out_hwirq, unsigned int *out_type);
#ifdef CONFIG_IRQ_DOMAIN_HIERARCHY
/* extended V2 interfaces to support hierarchy irq_domains */
分配:为层级中断域分配中断资源
int (*alloc)(struct irq_domain *d, unsigned int virq,
unsigned int nr_irqs, void *arg);
释放:释放层级中断域的中断资源
void (*free)(struct irq_domain *d, unsigned int virq,
unsigned int nr_irqs);
激活:启用硬件中断(配置GIC寄存器、使能中断)
void (*activate)(struct irq_domain *d, struct irq_data *irq_data);
失活:关闭硬件中断(清空GIC寄存器、禁用中断)
void (*deactivate)(struct irq_domain *d, struct irq_data *irq_data);
#endif
};
很典型的C语言多态的实现方式。irq_domain_ops是接口类,纯虚类,谁要使用它,就给irq_domain_ops提供自己的实现,然后挂到irq_domain里,irq_domain就成了独特的子类对象,成了有区别的gic->domain、gpio1->domain、ethernet1->domain。
irq_desc中断描述符
Linux使用结构体irq_desc来管理中断,类似于句柄。irq_desc是一个全局数组,那下表就对应了虚拟中断号(virq),它必定是唯一的,每个硬件中断(hwirq),最终都必须映射到1个唯一的virq上。
I.MX6ULL内核
include/linux/irqdesc.h
/**
* struct irq_desc - interrupt descriptor
* @irq_data: per irq and chip data passed down to chip functions
* @kstat_irqs: irq stats per cpu
* @handle_irq: highlevel irq-events handler
* @preflow_handler: handler called before the flow handler (currently used by sparc)
* @action: the irq action chain
* @status: status information
* @core_internal_state__do_not_mess_with_it: core internal status information
* @depth: disable-depth, for nested irq_disable() calls
* @wake_depth: enable depth, for multiple irq_set_irq_wake() callers
* @irq_count: stats field to detect stalled irqs
* @last_unhandled: aging timer for unhandled count
* @irqs_unhandled: stats field for spurious unhandled interrupts
* @threads_handled: stats field for deferred spurious detection of threaded handlers
* @threads_handled_last: comparator field for deferred spurious detection of theraded handlers
* @lock: locking for SMP
* @affinity_hint: hint to user space for preferred irq affinity
* @affinity_notify: context for notification of affinity changes
* @pending_mask: pending rebalanced interrupts
* @threads_oneshot: bitfield to handle shared oneshot threads
* @threads_active: number of irqaction threads currently running
* @wait_for_threads: wait queue for sync_irq to wait for threaded handlers
* @nr_actions: number of installed actions on this descriptor
* @no_suspend_depth: number of irqactions on a irq descriptor with
* IRQF_NO_SUSPEND set
* @force_resume_depth: number of irqactions on a irq descriptor with
* IRQF_FORCE_RESUME set
* @dir: /proc/irq/ procfs entry
* @name: flow handler name for /proc/interrupts output
*/
struct irq_desc {
struct irq_data irq_data; 中断控制器的硬件数据
unsigned int __percpu *kstat_irqs;
irq_flow_handler_t handle_irq; 中断控制器驱动的处理函数
#ifdef CONFIG_IRQ_PREFLOW_FASTEOI
irq_preflow_handler_t preflow_handler;
#endif
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 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;
#ifdef CONFIG_SMP
const struct cpumask *affinity_hint;
struct irq_affinity_notify *affinity_notify;
#ifdef CONFIG_GENERIC_PENDING_IRQ
cpumask_var_t pending_mask;
#endif
#endif
unsigned long threads_oneshot;
atomic_t threads_active;
wait_queue_head_t wait_for_threads;
#ifdef CONFIG_PM_SLEEP
unsigned int nr_actions;
unsigned int no_suspend_depth;
unsigned int cond_suspend_depth;
unsigned int force_resume_depth;
#endif
#ifdef CONFIG_PROC_FS
struct proc_dir_entry *dir;
#endif
int parent_irq;
struct module *owner;
const char *name;
} ____cacheline_internodealigned_in_smp;
这里:全局数组irq_desc
别忘了这里是个头文件,数组实例在kernel/irq/irqdesc.c中
所以用了extern关键字暴漏给外面
#ifndef CONFIG_SPARSE_IRQ
extern struct irq_desc irq_desc[NR_IRQS];
#endif
关键成员:
| 名称 | 类型 | 作用 |
|---|---|---|
| irq_data | struct irq_data | 中断控制器的硬件数据,作为参数传递给驱动函数 |
| handle_irq | irq_flow_handler_t | 函数指针,看名字就知道,处理这个中断控制器的irq |
| action | struct irqaction * | 一个irq_desc可以挂一条struct irqaction链表,说明是一对多的关系。action放的是设备驱动的处理函数(驱动里面用request_irq注册的) |
一个一个看,struct irq_data和irq_preflow_handler_t :
I.MX6ULL内核
include/linux/irqdesc.h
/**
* struct irq_data - per irq and irq chip data passed down to chip functions
* @mask: precomputed bitmask for accessing the chip registers
* @irq: interrupt number
* @hwirq: hardware interrupt number, local to the interrupt domain
* @node: node index useful for balancing
* @state_use_accessors: status information for irq chip functions.
* Use accessor functions to deal with it
* @chip: low level interrupt hardware access
* @domain: Interrupt translation domain; responsible for mapping
* between hwirq number and linux irq number.
* @parent_data: pointer to parent struct irq_data to support hierarchy
* irq_domain
* @handler_data: per-IRQ data for the irq_chip methods
* @chip_data: platform-specific per-chip private data for the chip
* methods, to allow shared chip implementations
* @msi_desc: MSI descriptor
* @affinity: IRQ affinity on SMP
*
* The fields here need to overlay the ones in irq_desc until we
* cleaned up the direct references and switched everything over to
* irq_data.
*/
struct irq_data {
u32 mask;
unsigned int irq; 虚拟中断号
unsigned long hwirq; (当前domain内的)物理中断号
unsigned int node;
unsigned int state_use_accessors;
struct irq_chip *chip; 重点,存放操作中断控制器硬件的方法
struct irq_domain *domain; 重点,存放hwirq和irq的映射方法
#ifdef CONFIG_IRQ_DOMAIN_HIERARCHY
struct irq_data *parent_data;
#endif
void *handler_data;
void *chip_data;
struct msi_desc *msi_desc;
cpumask_var_t affinity;
};
include/linux/irqhandler.h
/*
* Interrupt flow handler typedefs are defined here to avoid circular
* include dependencies.
*/
struct irq_desc;
struct irq_data;
typedef void (*irq_flow_handler_t)(unsigned int irq, struct irq_desc *desc);
typedef void (*irq_preflow_handler_t)(struct irq_data *data);
上面的irq_domain已经介绍过咯,存放了该domain下、虚拟irq和hwirq的映射方法集合。还有一个新结构体叫做struct irq_chip,用于存放操作中断硬件控制器的方法集合,一看就是驱动程序做的事情:
/**
* struct irq_chip - hardware interrupt chip descriptor
*
* @name: name for /proc/interrupts
* @irq_startup: start up the interrupt (defaults to ->enable if NULL)
* @irq_shutdown: shut down the interrupt (defaults to ->disable if NULL)
* @irq_enable: enable the interrupt (defaults to chip->unmask if NULL)
* @irq_disable: disable the interrupt
* @irq_ack: start of a new interrupt
* @irq_mask: mask an interrupt source
* @irq_mask_ack: ack and mask an interrupt source
* @irq_unmask: unmask an interrupt source
* @irq_eoi: end of interrupt
* @irq_set_affinity: set the CPU affinity on SMP machines
* @irq_retrigger: resend an IRQ to the CPU
* @irq_set_type: set the flow type (IRQ_TYPE_LEVEL/etc.) of an IRQ
* @irq_set_wake: enable/disable power-management wake-on of an IRQ
* @irq_bus_lock: function to lock access to slow bus (i2c) chips
* @irq_bus_sync_unlock:function to sync and unlock slow bus (i2c) chips
* @irq_cpu_online: configure an interrupt source for a secondary CPU
* @irq_cpu_offline: un-configure an interrupt source for a secondary CPU
* @irq_suspend: function called from core code on suspend once per chip
* @irq_resume: function called from core code on resume once per chip
* @irq_pm_shutdown: function called from core code on shutdown once per chip
* @irq_calc_mask: Optional function to set irq_data.mask for special cases
* @irq_print_chip: optional to print special chip info in show_interrupts
* @irq_request_resources: optional to request resources before calling
* any other callback related to this irq
* @irq_release_resources: optional to release resources acquired with
* irq_request_resources
* @irq_compose_msi_msg: optional to compose message content for MSI
* @irq_write_msi_msg: optional to write message content for MSI
* @irq_get_irqchip_state: return the internal state of an interrupt
* @irq_set_irqchip_state: set the internal state of a interrupt
* @flags: chip specific flags
*/
struct irq_chip {
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);
unsigned long flags;
};
整理一下思绪,struct irq_desc里有struct irq_data,irq_data有2个重要操作方法集:irq_chip和irq_domain,分别管中断控制器的硬件操作和irq/hwirq的映射。
中断描述符struct irq_desc里还有一个关键的struct irqaction链表:
include/linux/irq.h
/**
* struct irqaction - per interrupt action descriptor
* @handler: interrupt handler function
* @name: name of the device
* @dev_id: cookie to identify the device
* @percpu_dev_id: cookie to identify the device
* @next: pointer to the next irqaction for shared interrupts
* @irq: interrupt number
* @flags: flags (see IRQF_* above)
* @thread_fn: interrupt handler function for threaded interrupts
* @thread: thread pointer for threaded interrupts
* @thread_flags: flags related to @thread
* @thread_mask: bitmask for keeping track of @thread activity
* @dir: pointer to the proc/irq/NN/name entry
*/
struct irqaction {
irq_handler_t handler;
void *dev_id; 设备id
void __percpu *percpu_dev_id;
struct irqaction *next; 这里形成链表
irq_handler_t thread_fn; 中断线程化,request_threaded_irq注册的函数
struct task_struct *thread;
unsigned int irq;
unsigned int flags; 中断标志,比如上升沿、下降沿中断等
unsigned long thread_flags;
unsigned long thread_mask;
const char *name; 产生中断的硬件名称,具体的外围设备
比如I2C的传感器A、SPI的传感器B
struct proc_dir_entry *dir; /proc/irq相关的信息
} ____cacheline_internodealigned_in_smp;
可以看到,irqaction确实形成了级联结构,这是为了实现共享中断线。比如一个I2C的传感器和SPI的传感器共享了一个GPIO管脚作为中断通知,这时候就需要依次执行这2个传感器各自的irqaction。很显然的,它们最终需要和同一个irq_desc关联,形成一对多关系
struct irqaction里面有两个irq_handler_t,一个叫handler,一个叫thread_fn,它们一个在中断上半部执行(由request_irq注册),一个在线程上下文执行(由request_threaded_irq注册,这个函数会把handler和thread_fn一起注册),当handler返回IRQ_WAKE_THREAD时,thread_fn会自动被作为线程创建。irq_handler_t的形式如下,可以发现这两个函数都是有返回值的:
typedef irqreturn_t (*irq_handler_t)(int, void *);
/**
* enum irqreturn
* @IRQ_NONE interrupt was not from this device
* @IRQ_HANDLED interrupt was handled by this device
* @IRQ_WAKE_THREAD handler requests to wake the handler thread
*/
enum irqreturn {
IRQ_NONE = (0 << 0),
IRQ_HANDLED = (1 << 0),
IRQ_WAKE_THREAD = (1 << 1),
};
那么,从irq_desc[NR_IRQS]数组出发、这些结构体之间的关系如图:
该图摘自百问网《嵌入式Linux应用开发完全手册V5.3_IMX6ULL_Pro开发板》,两年前看的一头雾水,现在逐渐明白了。
总结一下:
- irq_desc[NR_IRQS]是全局数组,中断的软件起源,用虚拟中断号(irq或virq,随便怎么叫了)进行下标访问
- 每个irq_desc里面最重要的东西:handle_irq函数、irqaction链表、irq_data硬件数据。
- handle_irq由中断控制器驱动程序注册
- irqaction由设备驱动程序注册,这部分一般是我们最关心的
- irq_data负责提供中断控制器的操作集合(irq_chip)和hwirq到virq的查找方法集合(irq_domain),由中断控制器驱动注册
- 在单片机开发中,我们一个人把活全干了,既要负责配置UART、NVIC,打通中断线,使中断直通UART_IRQHandler,又要负责在UART_IRQHandler内细分中断产生的原因、编写业务逻辑。我们仍然在面向UART外设编程,而不是与UART相连接的那个传感器。驱动框架的一个作用,就是让我们直接为与UART相连的设备编写驱动程序,而不是针对一成不变的UART外设编程,换句话说,是让我们为产生中断的设备编程,而不是为中断控制器编程。中断控制器那些一成不变的操作早已由芯片厂商安排妥当。
再看handle_domain_irq函数
参数domain、hwirq不必多言了,lookup一会再看,regs就是被中断的现场
I.MX6ULL内核
include/linux/irqdesc.h
#ifdef CONFIG_HANDLE_DOMAIN_IRQ
/*
* Convert a HW interrupt number to a logical one using a IRQ domain,
* and handle the result interrupt number. Return -EINVAL if
* conversion failed. Providing a NULL domain indicates that the
* conversion has already been done.
*/
int __handle_domain_irq(struct irq_domain *domain, unsigned int hwirq,
bool lookup, struct pt_regs *regs);
static inline int handle_domain_irq(struct irq_domain *domain,
unsigned int hwirq, struct pt_regs *regs)
{
return __handle_domain_irq(domain, hwirq, true, regs);
}
#endif
kernel/irq/irqdesc.c
#ifdef CONFIG_HANDLE_DOMAIN_IRQ
/**
* __handle_domain_irq - Invoke the handler for a HW irq belonging to a domain
* @domain: The domain where to perform the lookup
* @hwirq: The HW irq number to convert to a logical one
* @lookup: Whether to perform the domain lookup or not
* @regs: Register file coming from the low-level handling code
*
* Returns: 0 on success, or -EINVAL if conversion has failed
*/
int __handle_domain_irq(struct irq_domain *domain, unsigned int hwirq,
bool lookup, struct pt_regs *regs)
{
struct pt_regs *old_regs = set_irq_regs(regs);
unsigned int irq = hwirq;
int ret = 0;
irq_enter();
#ifdef CONFIG_IRQ_DOMAIN
if (lookup)
irq = irq_find_mapping(domain, hwirq);
#endif
/*
* Some hardware gives randomly wrong interrupts. Rather
* than crashing, do something sensible.
*/
if (unlikely(!irq || irq >= nr_irqs)) {
ack_bad_irq(irq);
ret = -EINVAL;
} else {
generic_handle_irq(irq);
}
irq_exit();
set_irq_regs(old_regs);
return ret;
}
#endif
handle_domain_irq内部调用了__handle_domain_irq,并且多了一个lookup参数
一个一个看,首先是set_irq_regs函数:
struct pt_regs *old_regs = set_irq_regs(regs);
...其他处理
set_irq_regs(old_regs);
static inline struct pt_regs *set_irq_regs(struct pt_regs *new_regs)
{
struct pt_regs *old_regs;
old_regs = __this_cpu_read(__irq_regs);
__this_cpu_write(__irq_regs, new_regs);
return old_regs;
}
这里需要补充,__irq_regs是一个每CPU全局变量,所以set_irq_regs就是更新这个全局变量,确保在执行中间的代码时候这个__irq_regs是最新的,直到退出的时候再恢复。
接着就是irq号的提取:
unsigned int irq = hwirq;
...
#ifdef CONFIG_IRQ_DOMAIN
if (lookup)
irq = irq_find_mapping(domain, hwirq);
#endif
kernel/irq/irqdomain.c
/**
* 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)
{
struct irq_data *data;
/* Look for default domain if nececssary */
if (domain == NULL)
domain = irq_default_domain;
if (domain == NULL)
return 0;
if (hwirq < domain->revmap_direct_max_irq) {
data = irq_domain_get_irq_data(domain, hwirq);
if (data && data->hwirq == hwirq)
return hwirq;
}
/* Check if the hwirq is in the linear revmap. */
if (hwirq < domain->revmap_size)
return domain->linear_revmap[hwirq];
rcu_read_lock();
data = radix_tree_lookup(&domain->revmap_tree, hwirq);
rcu_read_unlock();
return data ? data->irq : 0;
}
EXPORT_SYMBOL_GPL(irq_find_mapping);
irq默认等于hwirq,但是如果启用CONFIG_IRQ_DOMAIN,就从这个domain里面找到hwirq的映射。具体怎么找的先不研究了,irq_find_mapping函数也贴在上面,反正肯定和domain脱不了干系
之后就是核心处理了:
irq_enter();
...省略irq_find_mapping,此时irq就是虚拟irq,用于访问irq_desc的下标
/*
* Some hardware gives randomly wrong interrupts. Rather
* than crashing, do something sensible.
*/
if (unlikely(!irq || irq >= nr_irqs)) {
ack_bad_irq(irq);
ret = -EINVAL;
} else {
generic_handle_irq(irq);
}
irq_exit();
很生草的ack_bad_irq,只是打印出来...
void ack_bad_irq(unsigned int irq)
{
pr_crit("BAD IRQ ack %d\n", irq);
}
核心就3部分:irq_enter(void)、generic_handle_irq(irq)、irq_exit(void)
观察参数,到这里时,pt_regs就不再作为参数传入generic_handle_irq了,已经作为每CPU全局变量被存起来了,可能是因为单独开一个参数不划算
回忆一下,到目前为止的上下文:
- 中断屏蔽:从IRQ发生中断的那一刻,CPSR.I自动置位,IRQ全局屏蔽,直到现在,还没清零呢
- CPU模式:SVC模式,具体可以看《终端溯源(一)和(二)》
- 用的栈:内核栈,SVC栈,SP_SVC。struct pt_regs栈帧也在这里面,那段指针已经由__irq_regs每CPU全局变量接管
- 需要注意:对于I.MX6ULL单核,这段代码就确实不会重入了,但对于多核系统,CPSR.I=1只是其中一个CPU不再响应IRQ,而其他CPU还是有机会响应的,这段代码仍然有机会被重入(郁闷,从单核到多核思维好难转变)
irq_enter、generic_handle_irq、irq_exit函数
irq_enter函数
kernel/softirq.c
??这段代码居然在softirq.c中出现,而不是irq.c
/*
* Enter an interrupt context.
*/
函数作用:进入中断上下文
核心目的:告诉内核 "CPU现在开始处理硬件中断了",更新中断状态、维护内核上下文
void irq_enter(void)
{
1. RCU内核同步机制:通知RCU子系统进入中断上下文
RCU是Linux内核的高性能锁机制,进入中断必须告知RCU,避免同步异常
(驱动开发中无需关心,内核底层同步用)
rcu_irq_enter();
2. 判断条件:
① is_idle_task(current):当前CPU正在运行【空闲任务】(idle进程,CPU没事干时跑的任务)
② !in_interrupt():当前**没有嵌套中断**(这是第一层硬件中断)
总结:只有【CPU空闲 + 第一次进中断】才会执行这个分支
if (is_idle_task(current) && !in_interrupt()) {
/*
* 临时禁用本地CPU的软中断(Bottom Half,中断底半部)
* 目的:防止软中断提前唤醒内核线程ksoftirqd
* 因为软中断会在【中断退出时】统一处理,这里不需要额外唤醒线程,优化性能
*/
local_bh_disable();
时钟子系统:更新系统时钟,终止CPU空闲计时
tick_irq_enter();
恢复启用本地CPU的软中断,和上面的local_bh_disable配对
_local_bh_enable();
}
3. 【最核心功能】中断嵌套计数 +1
__irq_enter 会给当前CPU的【中断嵌套计数器】加1
作用:
1. 标记CPU正式进入【中断上下文】(中断上下文不能睡眠、不能调用阻塞函数)
2. 支持中断嵌套(多次进中断,计数器累加)
3. 内核宏 in_interrupt() 就是靠这个计数器判断是否在中断中
__irq_enter();
}
include/linux/hardirq.h
。。怎么又跑到hardirq.h了,刚才还是softirq.c呢
/*
* It is safe to do non-atomic ops on ->hardirq_context,
* because NMI handlers may not preempt and the ops are
* always balanced, so the interrupted value of ->hardirq_context
* will always be restored.
*/
#define __irq_enter() \
do { \
account_irq_enter_time(current); \
preempt_count_add(HARDIRQ_OFFSET); \
trace_hardirq_enter(); \
} while (0)
include/linux/premmpt.h
#define preempt_count_add(val) __preempt_count_add(val)
include/asm-generic/premmpt.h
/*
* The various preempt_count add/sub methods
*/
static __always_inline void __preempt_count_add(int val)
{
*preempt_count_ptr() += val;
}
static __always_inline int *preempt_count_ptr(void)
{
return ¤t_thread_info()->preempt_count;
}
看一下premmpt_count,AI说的
0~7 位:抢占计数
8~15 位:软中断
16~23 位:硬中断 HARDIRQ
24~31 位:NMI
代码里也确实是这样:
include/linux/premmpt_mask.h
#define PREEMPT_BITS 8
#define SOFTIRQ_BITS 8
#define HARDIRQ_BITS 4
#define NMI_BITS 1
#define PREEMPT_SHIFT 0
#define SOFTIRQ_SHIFT (PREEMPT_SHIFT + PREEMPT_BITS)
#define HARDIRQ_SHIFT (SOFTIRQ_SHIFT + SOFTIRQ_BITS)
#define NMI_SHIFT (HARDIRQ_SHIFT + HARDIRQ_BITS)
#define PREEMPT_OFFSET (1UL << PREEMPT_SHIFT)
#define SOFTIRQ_OFFSET (1UL << SOFTIRQ_SHIFT)
#define HARDIRQ_OFFSET (1UL << HARDIRQ_SHIFT)
#define NMI_OFFSET (1UL << NMI_SHIFT)
总的来说,irq_enter()一个最重要的作用就是给premmpt_count变量的【0~7 位】抢占计数加1
irq_exit函数
如下
kernel/softirq.c
/*
* Exit an interrupt context. Process softirqs if needed and possible:
*/
* 函数名:irq_exit
* 功能:退出硬件中断(HARDIRQ)上下文
* 调用时机:硬件中断处理完成(generic_handle_irq执行完毕),最后一步调用
* 核心职责:恢复中断计数、调度软中断、通知内核子系统退出中断状态
void irq_exit(void)
{
#ifndef __ARCH_IRQ_EXIT_IRQS_DISABLED 这个宏为1
local_irq_disable(); 该分支:架构退出中断时需要手动关中断(x86等架构用)
#else
WARN_ON_ONCE(!irqs_disabled()); 实际走这里,如果检查到CPSR.I居然不等于1(处于开中断),抛出BUG
#endif
account_irq_exit_time(current); 统计时间
preempt_count_sub(HARDIRQ_OFFSET); 和premmpt_count_add配对
if (!in_interrupt() && local_softirq_pending()) 很重要,随后细品
invoke_softirq();
tick_irq_exit(); 时钟子系统:退出中断,更新CPU空闲计时(和 tick_irq_enter() 配对)
rcu_irq_exit(); RCU子系统:通知RCU退出中断上下文(和 rcu_irq_enter() 配对)
trace_hardirq_exit(); /* must be last! */ 内核跟踪点:标记硬中断完全退出
}
irq_enter和irq_exit是配对的,核心就是premmpt_count这个变量的加减。
最后看generic_handle_irq
generic_handle_irq函数
kernel/irq/irqdesc.c
/**
* generic_handle_irq - Invoke the handler for a particular irq
* @irq: The irq number to handle
*
*/
int generic_handle_irq(unsigned int irq)
{
struct irq_desc *desc = irq_to_desc(irq);
if (!desc)
return -EINVAL;
generic_handle_irq_desc(irq, desc);
return 0;
}
EXPORT_SYMBOL_GPL(generic_handle_irq);
/*
* Architectures call this to let the generic IRQ layer
* handle an interrupt. If the descriptor is attached to an
* irqchip-style controller then we call the ->handle_irq() handler,
* and it calls __do_IRQ() if it's attached to an irqtype-style controller.
*/
static inline void generic_handle_irq_desc(unsigned int irq, struct irq_desc *desc)
{
desc->handle_irq(irq, desc);
}
可以看到,就是调用了irq_desc里面的handle_irq函数指针!
我们清楚的看到过,irq_desc里是有action链表的,那个才是我们用request_irq注册的中断处理函数,可是从目前为止,只调用了irq_desc结构体里唯一的handle_irq函数,没有看到action链表的影子。想必handle_irq函数指针一定绑了一个不得了的函数:
一级domain–GIC的irq_desc->handle_irq函数指针:handle_fasteoi_irq
kernel/irq/chip.c
/**
* handle_fasteoi_irq - irq handler for transparent controllers
* @irq: the interrupt number
* @desc: the interrupt description structure for this irq
*
* Only a single callback will be issued to the chip: an ->eoi()
* call when the interrupt has been serviced. This enables support
* for modern forms of interrupt handlers, which handle the flow
* details in hardware, transparently.
*/
void
handle_fasteoi_irq(unsigned int irq, struct irq_desc *desc)
{
struct irq_chip *chip = desc->irq_data.chip; 拿到操作硬件(这时为GIC)的方法
raw_spin_lock(&desc->lock); 多核安全操作同一个中断描述符
if (!irq_may_run(desc)) 检查中断是否允许执行(状态检查:是否挂起、是否失效)
goto out;
desc->istate &= ~(IRQS_REPLAY | IRQS_WAITING); 清除中断重放/等待状态,标记中断正在处理
kstat_incr_irqs_this_cpu(irq, desc); 内核统计:当前CPU中断计数+1(/proc/interrupts 统计数据来源)
/*
* If its disabled or no action available
* then mask it and get out of here:
*/
* 1. 没有驱动注册中断处理函数(desc->action 为空)
* 2. 中断被软件禁用
* → 标记为挂起,屏蔽该中断,直接退出
if (unlikely(!desc->action || irqd_irq_disabled(&desc->irq_data))) {
desc->istate |= IRQS_PENDING;
mask_irq(desc);
goto out;
}
如果是ONESHOT中断(一次性中断),处理前先屏蔽硬件中断
if (desc->istate & IRQS_ONESHOT)
mask_irq(desc);
中断预处理钩子(极少用,芯片厂商定制)
preflow_handler(desc);
核心中的核心:执行驱动中断处理函数
内部会遍历 desc->action 链表
调用你驱动 request_irq 注册的 handler 函数!!
handle_irq_event(desc);
条件解屏蔽 + 向GIC发送 EOI(中断结束)信号
I.MX6ULL GIC 硬件收到EOI后,释放中断线
cond_unmask_eoi_irq(desc, chip);
解锁,函数正常返回
raw_spin_unlock(&desc->lock);
return;
out:
if (!(chip->flags & IRQCHIP_EOI_IF_HANDLED))
chip->irq_eoi(&desc->irq_data);
raw_spin_unlock(&desc->lock);
}
EXPORT_SYMBOL_GPL(handle_fasteoi_irq);
kernel/irq/handle.c
irqreturn_t handle_irq_event(struct irq_desc *desc)
{
struct irqaction *action = desc->action;
irqreturn_t ret;
desc->istate &= ~IRQS_PENDING;
irqd_set(&desc->irq_data, IRQD_IRQ_INPROGRESS);
raw_spin_unlock(&desc->lock);
在这里调用的action链表
ret = handle_irq_event_percpu(desc, action);
raw_spin_lock(&desc->lock);
irqd_clear(&desc->irq_data, IRQD_IRQ_INPROGRESS);
return ret;
}
kernel/irq/handle.c
irqreturn_t
handle_irq_event_percpu(struct irq_desc *desc, struct irqaction *action)
{
irqreturn_t retval = IRQ_NONE;
unsigned int flags = 0, irq = desc->irq_data.irq;
do {
irqreturn_t res;
trace_irq_handler_entry(irq, action);
res = action->handler(irq, action->dev_id);
trace_irq_handler_exit(irq, action, res);
if (WARN_ONCE(!irqs_disabled(),"irq %u handler %pF enabled interrupts\n",
irq, action->handler))
local_irq_disable();
switch (res) {
这里就是中断线程化的处理
case IRQ_WAKE_THREAD:
/*
* Catch drivers which return WAKE_THREAD but
* did not set up a thread function
*/
注册的中断处理函数返回了IRQ_WAKE_THREAD,本意想使用中断线程化
结果内核发现没设置线程化的函数,只能报警
if (unlikely(!action->thread_fn)) {
warn_no_thread(irq, action);
break;
}
正常情况下,就唤醒内核线程
__irq_wake_thread(desc, action);
/* Fall through to add to randomness */
case IRQ_HANDLED:
flags |= action->flags;
break;
default:
break;
}
retval |= res;
action = action->next;
} while (action);
add_interrupt_randomness(irq, flags);
if (!noirqdebug)
note_interrupt(irq, desc, retval);
return retval;
}
AI还是太强大了,这种代码放在几年前寸步难行,现在打起注释和喝水一样简单
还记得,调用完irq_desc->handle_irq()后,接下来要执行的irq_exit函数内,会检查CPSR.I是否仍处于中断屏蔽状态,如果意外解除了屏蔽,就会报警。这段handle_fasteoi_irq里,也没有解除中断屏蔽的代码。因此可以得出结论,目前IRQ仍然在这颗CPU上全局屏蔽。但是GIC有可能会把同一个IRQ发给多个CPU核心同时处理,必须处理两个核对同一个irq_desc的并发访问,因此对desc->lock加锁是有必要的。
经过handle_fasteoi_irq函数后,GIC的IRQ状态终于释放了:
cond_unmask_eoi_irq(desc, chip); 向GIC写回EOI,GIC现在解放了
但是CPSR.I始终还是为1,操作它对应的是local_irq_disable() / local_irq_enable()函数
这2个API,就算见到,也始终是local_irq_disable(),作为保险又关了一遍,正常情况下不可能打开
小节
- 本节的调用路径
有点复杂:
gic_handle_irq(struct pt_regs *regs)
|-handle_domain_irq(gic->domain, irqnr, regs)
|-__handle_domain_irq(domain, hwirq, true, regs);
|-set_irq_regs(regs);
|-irq_enter();
|-irq_find_mapping(domain, hwirq);
|-generic_handle_irq(irq);
|-generic_handle_irq_desc(irq, desc)
|-desc->handle_irq(irq, desc) 对于GIC来说,handle_irq就是handle_fasteoi_irq
|-handle_irq_event(desc);
|-handle_irq_event_percpu(desc, action) 最终处理到action链表
|-cond_unmask_eoi_irq(desc, chip); 向GIC写EOI
|-irq_exit();
|-set_irq_regs(old_regs);
- 上下文
GIC的写EOI已经找到了(cond_unmask_eoi_irq()函数),但是直到irq_exit()前,CPSR.I仍然保持着该CPU核心的全局屏蔽。这意味着irq_desc的handle_irq函数指针和action链表的调用上下文仍然保持着该核心的IRQ全局屏蔽 - 其他细节
对于irqnr >= 16的中断(GIC PPI和SPI中断),通过在handle_fasteoi_irq调用cond_unmask_eoi_irq函数写EOI表示处理完成,但对于irqnr < 16(GIC SGI中断),早在gic_handle_irq内部就已经写EOI寄存器表示处理完毕了,见《中断溯源(二)》
到此为止,贴上百问网《嵌入式Linux应用开发完全手册》的两张图,对照着代码看,能解答很多疑惑。这张图还会在下一章出现的,届时将会继续分析软中断,彻底搞清楚中断和软中断上下文。

勘误
出于好奇写了个模块,想验证一下是不是如本文分析的那样,所有中断描述符都存在irq_desc[NR_IRQS]数组里,毕竟大家都是这么说的
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/irq.h> // 定义 NR_IRQS、irq_to_desc
#include <linux/irqdesc.h> // 内核允许的包含方式
static int hello_init(void) {
int i; // C89 规范:循环变量提前定义
struct irq_desc *desc;
printk(KERN_ALERT "NR_IRQS:%d\n", NR_IRQS);
printk(KERN_ALERT "irq_desc[i]\thandle_irq\t\taction\n");
// 遍历所有中断号
for (i = 0; i < NR_IRQS; i++) {
// ✅ 合法API:获取中断描述符(内核导出,模块可用)
desc = irq_to_desc(i);
if (!desc) {
continue; // 无效中断,跳过
}
// 打印 handle_irq 函数地址、action 链表地址
printk(KERN_ALERT "%d\t\t0x%p\t\t0x%p\n",
i, desc->handle_irq, desc->action);
}
return 0;
}
static void hello_exit(void) {
printk(KERN_ALERT "bye module\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("fyl");
MODULE_DESCRIPTION("Print irq_desc info");
一看运行效果,天塌了:
root@ATK-IMX6U:/home/test/module# insmod print_irq_desc.ko
[95874.458275] NR_IRQS:16
[95874.463078] irq_desc[i] handle_irq action
[95874.469410] 0 0x8006ee94 0x (null)
[95874.473219] 1 0x8006ee94 0x (null)
[95874.477629] 2 0x8006ee94 0x (null)
[95874.481434] 3 0x8006ee94 0x (null)
[95874.485695] 4 0x8006ee94 0x (null)
[95874.489441] 5 0x8006ee94 0x (null)
[95874.493682] 6 0x8006ee94 0x (null)
[95874.497423] 7 0x8006ee94 0x (null)
[95874.501657] 8 0x8006ee94 0x (null)
[95874.505398] 9 0x8006ee94 0x (null)
[95874.509613] 10 0x8006ee94 0x (null)
[95874.513439] 11 0x8006ee94 0x (null)
[95874.517727] 12 0x8006ee94 0x (null)
[95874.521554] 13 0x8006ee94 0x (null)
[95874.525843] 14 0x8006ee94 0x (null)
[95874.529667] 15 0x8006ee94 0x (null)
眼前一黑,怎么NR_IRQS只有16,那这数组也太小了吧,16个描述符够干什么?
使用cat /proc/interrupts命令查看真正的中断描述符:
root@ATK-IMX6U:/home/test/module# cat /proc/interrupts
CPU0
16: 3410867 GPC 55 Level i.MX Timer Tick
18: 2601 GPC 26 Level 2020000.serial
19: 0 GPC 50 Level 2034000.asrc
35: 63 gpio-mxc 9 Edge gt9xx
45: 0 gpio-mxc 19 Edge 2190000.usdhc cd
194: 0 GPC 4 Level 20cc000.snvs:snvs-powerkey
195: 654436 GPC 120 Level 20b4000.ethernet
196: 0 GPC 121 Level 20b4000.ethernet
197: 0 GPC 80 Level 20bc000.wdog
200: 0 GPC 49 Level imx_thermal
205: 0 GPC 19 Level rtc alarm
211: 0 GPC 2 Level sdma
216: 0 GPC 43 Level 2184000.usb
217: 59 GPC 42 Level 2184200.usb
218: 2652 GPC 118 Level 2188000.ethernet
219: 0 GPC 119 Level 2188000.ethernet
220: 0 GPC 22 Level mmc0
221: 4139 GPC 23 Level mmc1
222: 1270 GPC 37 Level 21a4000.i2c
224: 4 GPC 5 Level 21c8000.lcdif
225: 0 GPC 8 Level pxp-dmaengine
226: 0 GPC 18 Level pxp-dmaengine-std
227: 1 GPC 107 Level
228: 0 GPC 28 Level 21ec000.serial
229: 0 GPC 17 Level 21fc000.serial
230: 0 GPC 46 Level dcp-vmi-irq
231: 0 GPC 47 Level dcp-irq
233: 2 GPC 6 Level imx-rng
IPI0: 0 CPU wakeup interrupts
IPI1: 0 Timer broadcast interrupts
IPI2: 0 Rescheduling interrupts
IPI3: 0 Function call interrupts
IPI4: 0 Single function call interrupts
IPI5: 0 CPU stop interrupts
IPI6: 0 IRQ work interrupts
IPI7: 0 completion interrupts
Err: 0
真是见了鬼了,不但个数多的多,编号还恰好从16开始,那不是说明irq_desc[0~15]完全没用吗?
问了AI,NR_IRQS是老式内核静态中断数组大小,现在的内核全都用的动态的,这个NR_IRQS宏不再代表最大的中断数量,而要改为动态的nr_irqs全局变量表示
修改后的module:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/irq.h> // 声明 nr_irqs、irq_to_desc
static int hello_init(void)
{
int i;
struct irq_desc *desc;
// 打印动态获取的最大中断数量
printk(KERN_ALERT "系统动态最大中断数 nr_irqs: %d\n", nr_irqs);
printk(KERN_ALERT "IRQ\t\thandle_irq\t\taction\n");
// 遍历所有有效中断(动态上限)
for (i = 0; i < nr_irqs; i++)
{
desc = irq_to_desc(i);
if (!desc)
continue;
printk(KERN_ALERT "%d\t\t0x%p\t\t0x%p\n",
i, desc->handle_irq, desc->action);
}
return 0;
}
static void hello_exit(void)
{
printk(KERN_ALERT "模块卸载\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
运行效果(省略/proc/interrupts没有显示的,事实上我发现action为NULL的irq_desc几乎都没显示):
root@ATK-IMX6U:/home/test/module# insmod print_irq_desc.ko
[97044.515228] 系统动态最大中断数 nr_irqs: 234
[97044.524118] IRQ handle_irq action
[97044.592533] 16 0x80071ed8 0x80bd7480
[97044.600644] 18 0x80071ed8 0x94252f40
[97044.604994] 19 0x80071ed8 0x943e1980
[97044.670231] 35 0x80071d9c 0x943ad780
[97044.710913] 45 0x80071d9c 0x94380fc0
[97045.334287] 194 0x80071ed8 0x943702c0
[97045.338649] 195 0x80071ed8 0x942e1900
[97045.343269] 196 0x80071ed8 0x942e1980
[97045.347185] 197 0x80071ed8 0x943bb500
[97045.359916] 200 0x80071ed8 0x94479380
[97045.380494] 205 0x80071ed8 0x943ade80
[97045.405577] 211 0x80071ed8 0x9424dd80
[97045.426600] 216 0x80071ed8 0x942eff80
[97045.430508] 217 0x80071ed8 0x9437fd80
[97045.434887] 218 0x80071ed8 0x94379fc0
[97045.438797] 219 0x80071ed8 0x942e1e40
[97045.443203] 220 0x80071ed8 0x94380c00
[97045.447109] 221 0x80071ed8 0x94382e80
[97045.451483] 222 0x80071ed8 0x941489c0
[97045.459910] 224 0x80071ed8 0x9424d240
[97045.463912] 225 0x80071ed8 0x9441fac0
[97045.468401] 226 0x80071ed8 0x9441fb40
[97045.472315] 227 0x80071ed8 0x (null)
[97045.476737] 228 0x80071ed8 0x942686c0
[97045.480655] 229 0x80071ed8 0x94268e40
[97045.485169] 230 0x80071ed8 0x943ce380
[97045.489072] 231 0x80071ed8 0x943ce400
[97045.497376] 233 0x80071ed8 0x9426ba80
这下就正常了,本章讲的有一点不妥:事实上去查irq_desc数组要用专用函数:irq_to_desc,而不能直接访问数组下标。
我们暂且不研究它具体怎么存储的了,不管是线性数组还是其他数据结构,反正就用irq_to_desc函数去查,传入虚拟中断号,返回对应的irq_desc,总是没什么问题的
我们对照着cat /proc/interrupts的结果看,如果本章内容分析的没错,16号中断对应的handle_irq函数指针肯定是GIC的handle_fasteoi_irq:
cat /proc/interrupts打印:
16: 3410867 GPC 55 Level i.MX Timer Tick
module打印:
[97044.592533] 16 0x80071ed8 0x80bd7480
可以看到:
irq_desc[16]->handle_irq == 0x80071ed8
现在打印一下handle_fasteoi_irq函数的地址,看看是不是0x80071ed8就行了:
模块代码:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/irq.h>
static int __init print_irq_handlers_init(void)
{
printk(KERN_ALERT "=====================================================\n");
printk(KERN_ALERT " IMX6ULL 可用中断处理函数地址(一级+二级全齐活) \n");
printk(KERN_ALERT "=====================================================\n");
// ========== 一级 GIC 中断专用 ==========
printk(KERN_ALERT "handle_fasteoi_irq (GIC 一级中断): 0x%p\n", handle_fasteoi_irq);
// ========== 二级 GPIO 中断专用(你要的重点) ==========
printk(KERN_ALERT "handle_edge_irq (GPIO 边沿触发): 0x%p\n", handle_edge_irq);
printk(KERN_ALERT "handle_level_irq (GPIO 电平触发): 0x%p\n", handle_level_irq);
// ========== 通用简单中断 ==========
printk(KERN_ALERT "handle_simple_irq (通用简单中断): 0x%p\n", handle_simple_irq);
printk(KERN_ALERT "=====================================================\n");
return 0;
}
static void __exit print_irq_handlers_exit(void)
{
printk(KERN_ALERT "模块卸载成功\n");
}
module_init(print_irq_handlers_init);
module_exit(print_irq_handlers_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("fyl");
MODULE_DESCRIPTION("IMX6ULL 中断处理函数地址打印");
运行现象:
root@ATK-IMX6U:/home/test/module# insmod print_irq_handler.ko
[97761.102200] =====================================================
[97761.112276] IMX6ULL 可用中断处理函数地址(一级+二级全齐活)
[97761.120339] =====================================================
[97761.127308] handle_fasteoi_irq (GIC 一级中断): 0x80071ed8
[97761.133355] handle_edge_irq (GPIO 边沿触发): 0x80072070
[97761.140012] handle_level_irq (GPIO 电平触发): 0x80071d9c
[97761.146076] handle_simple_irq (通用简单中断): 0x80071ce0
[97761.152723] =====================================================
这下睡得好觉了,不但16号中断的handle_irq函数确实被注册成为了handle_fasteoi_irq,我们还随手发现了GPIO1_IO09、35号中断的处理函数是handle_level_irq(0x80071d9c):
35: 63 gpio-mxc 9 Edge gt9xx
[97044.670231] 35 0x80071d9c 0x943ad780
这里就涉及到中断的级联了。35号中断在正点原子开发板上对应触摸IC(gt9xx),明显是接到GPIO上,而不是直接接到GIC上。handle_fasteoi_irq函数是给GIC用的,显然不应该去管GPIO的中断。
这里再引入一个GPIO驱动probe函数:mxc_gpio_probe
I.MX6ULL内核
drivers/gpio/gpio-mxc.c
static int mxc_gpio_probe(struct platform_device *pdev) {
...
struct mxc_gpio_port *port;
port = devm_kzalloc(&pdev->dev, sizeof(*port), GFP_KERNEL);
...
port->irq_high = platform_get_irq(pdev, 1); I.MX6ULL的GPIOx[0:15]是low,GPIOx[16:31]是high,有2条通往GIC
port->irq = platform_get_irq(pdev, 0); 也就是说,每个port(GPIO1、GPIO2等)申请了2个独立的irq
必然对应着2个irq_desc描述符
...
irq_set_chained_handler(port->irq, mx3_gpio_irq_handler); 为描述符irq_desc[low]设置mx3_gpio_irq_handler函数
irq_set_handler_data(port->irq, port);
if (port->irq_high > 0) {
/* setup handler for GPIO 16 to 31 */
irq_set_chained_handler(port->irq_high, 为描述符irq_desc[high]设置mx3_gpio_irq_handler函数
mx3_gpio_irq_handler);
irq_set_handler_data(port->irq_high, port);
}
...
}
其中,irq_set_chained_handler函数有个作用,就是给irq_desc[irq]->handle_irq设置成handle处理函数:
static inline void
irq_set_chained_handler(unsigned int irq, irq_flow_handler_t handle)
{
__irq_set_handler(irq, handle, 1, NULL);
}
void
__irq_set_handler(unsigned int irq, irq_flow_handler_t handle, int is_chained,
const char *name)
{
...
struct irq_desc *desc = irq_get_desc_buslock(irq, &flags, 0);
...
desc->handle_irq = handle;
desc->name = name;
...
}
EXPORT_SYMBOL_GPL(__irq_set_handler);
到此为止,我们已经见过好几类irq_desc->handle_irq函数了:
handle_fasteoi_irq(会遍历irq_desc->action链表)
handle_edge_irq(最后一级的irq_desc,会遍历irq_desc->action链表,链表里就有gt9xx触摸IC的中断处理)
handle_level_irq
handle_simple_irq
mx3_gpio_irq_handler(内部调用mxc_gpio_irq_handler,直接转发irq):
drivers/gpio/gpio-mxc.c
/* MX1 and MX3 has one interrupt *per* gpio port */
static void mx3_gpio_irq_handler(u32 irq, struct irq_desc *desc)
{
u32 irq_stat;
struct mxc_gpio_port *port = irq_get_handler_data(irq);
struct irq_chip *chip = irq_get_chip(irq);
chained_irq_enter(chip, desc);
irq_stat = readl(port->base + GPIO_ISR) & readl(port->base + GPIO_IMR);
mxc_gpio_irq_handler(port, irq_stat);
chained_irq_exit(chip, desc);
}
/* handle 32 interrupts in one status register */
static void mxc_gpio_irq_handler(struct mxc_gpio_port *port, u32 irq_stat)
{
while (irq_stat != 0) {
int irqoffset = fls(irq_stat) - 1;
if (port->both_edges & (1 << irqoffset))
mxc_flip_edge(port, irqoffset);
generic_handle_irq(irq_find_mapping(port->domain, irqoffset)); 看,在这里直接转发,不查自己的irq_desc
而是直接调用下一级的irq_desc->handle_irq!
irq_stat &= ~(1 << irqoffset);
}
}
得出结论:
- 如果外设中断不需要转发,GIC的一级irq_desc->handle_irq默认注册的就是handle_fasteoi_irq函数,这个函数还是会老老实实去检查irq_desc->action链表的
- 类似于GPIO[0:15]这种级联的外设,16根管脚只有1个irq_desc,那它的一级irq_desc->handle_irq注册的就是mx3_gpio_irq_handler(mxc_gpio_irq_handler)函数,这个函数里面就不检查irq_desc->action了!而是先区分出哪一个管脚造成的中断(irqoffset),然后直接调用generic_handle_irq(irq_find_mapping(port->domain, irqoffset)),直接查下一级的irq_desc里的handle_irq!相当于转发了中断,怪不得是用irq_set_chained_handler注册的——action是空的,不检查action,只负责转发,那不就说明这个desc下面没具体的硬件设备吗(没有action)。
- 最后就是具体到某个管脚的irq_desc了(比如GPIO1_IO09),这里面注册的是handle_level_irq这类函数。它会找当下的action链表(如果确实有多个设备共享这个管脚的话),把这根管脚上挂的所有设备都取出来,然后依次调用在驱动程序里注册的处理函数。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐
所有评论(0)