前言

在小红薯上发内核学习求助贴,没想到就一会功夫啊就一会,得到了很多前辈的指导,大佬们给我推荐了几本书:《打通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 &current_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链表(如果确实有多个设备共享这个管脚的话),把这根管脚上挂的所有设备都取出来,然后依次调用在驱动程序里注册的处理函数。
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐