一起分析Linux系统设计思想——05中断框架剖析

Posted 穿越临界点

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一起分析Linux系统设计思想——05中断框架剖析相关的知识,希望对你有一定的参考价值。

在学习资料满天飞的大环境下,知识变得非常零散,体系化的知识并不多,这就导致很多人每天都努力学习到感动自己,最终却收效甚微,甚至放弃学习。我的使命就是过滤掉大量的无效信息,将知识体系化,以短平快的方式直达问题本质,把大家从大海捞针的痛苦中解脱出来。


中断从哪里来,到哪里去?中断号是如何分配的?共享中断是如何实现的?

1 中断框架

中断其实是很简单的概念,还能有多复杂呢?就是某一种类型来了,跳到指定的位置去执行对应的处理函数而已。

所以,万变不离其宗,千万不要被内核复杂的代码搞得晕头转向了。在看内核代码前一定要有自己的思路,然后再去看,看的时候和自己的思路对比,看看别人是如何实现的,对应上了就是看懂了,细枝末节不要太纠结,随着使用和工作中遇到的问题会逐渐明白的。

下面就让我们看看内核是如何“将中断这么简单的机制搞得非常复杂的”。

1.1 irq_desc

irq_desc是内核中断框架中的灵魂人物。irq_desc变量是一个结构体数组,每一个成员(也就是struct irq_desc结构体)对应一个中断号(一定要牢牢记住这一点,内核中好多数据结构都是类似的套路),并负责 管理 这个中断号。

irq_desc变量就定义在kernel/irq/handle.c中,是一个跨文件的全局变量。

/* kernel/irq/handle.c */
/*
 * Controller mappings for all interrupt sources:
 */
struct irq_desc irq_desc[NR_IRQS] __cacheline_aligned_in_smp = {/*定义结构体数组*/
	[0 ... NR_IRQS-1] = { /*给结构体数组成员赋初值(默认值)*/
		.status = IRQ_DISABLED,
		.chip = &no_irq_chip,
		.handle_irq = handle_bad_irq,
		.depth = 1,
		.lock = __SPIN_LOCK_UNLOCKED(irq_desc->lock),
#ifdef CONFIG_SMP
		.affinity = CPU_MASK_ALL
#endif
	}
};

struct irq_desc结构体定义在include/linux/irq.h中。

前文提到了 管理 ,那什么又叫管理呢?

管理就是控制中断的 状态行为 。状态对应数据,行为对应函数/方法。所以,结构体定义中必然有对应的成员。

/* include/linux/irq.h */
struct irq_desc;
typedef	void fastcall (*irq_flow_handler_t)(unsigned int irq,
					    struct irq_desc *desc);
struct irq_desc {
	irq_flow_handler_t	handle_irq; /*中断入口处理函数(注意不是用户注册的处理函数)*/
	struct irq_chip		*chip; /*和硬件相关的操作都定义在这里*/
	...
	struct irqaction	*action; /*用户注册的处理函数构成的链表(链表头指针)*/
	unsigned int		status;	 /*中断状态(是否使能和挂起等)*/
	...
	const char		*name; /*中断名称*/
} ____cacheline_internodealigned_in_smp;

extern struct irq_desc irq_desc[NR_IRQS]; /*声明文件间全局变量*/

其中的status和name成员都比较好理解,那handle_irq、chip和action有什么用呢?

如果是我自己设计,我可能只会设计一个action函数指针。因为,无非就是中断来了之后我去执行action中指向的函数(该函数由用户提前注册好)去运行。

这种思路完全没有错,而且,我估计早期的内核可能就是这么实现的。但是,在实际使用过程中会遇到内核需要兼容多款芯片的问题,为了通用就必然会出现 分层 。和芯片强相关的操作(比如配置各种寄存器)放到chip中;和用户定义的流程相关的处理放到action中。机制和策略分离 ,机制在下层,策略在上层。

那handle_irq是干什么用的呢?它为什么也被分离出来了?该函数指针会根据不同的触发方式(比如边沿触发和电平触发)调用不同的处理方法。而不论用户定义的处理流程是什么,这个流程的处理是固定的,不变的,也就是公共部分,其实也可以归到机制一类中去。

那爱问问题的同学又问了,既然都是机制,那怎么不和chip放在一起?这个问题问的好。现在要祭出最最根本的思维方法和设计原则——一定要将易变的和稳定的部分分离;将导致变化原因不同的部分分离。我们本着这个原则再来看action、handle_irq和chip的划分就通透多了——action容易跟随业务变化;handle_irq跟随中断触发方式变化;chip跟随芯片类型变化。

考虑到这个层面才是真的通透了,所以,学习内核的目的不是看多少代码,而是要窥探大神们代码背后鬼斧神差般的思维!

1.2 irq_chip

如果你理解了irq_desc的设计思想,那么接下来的内容基本都是知识型的了,没有什么难度了,我尽量减少篇幅。

struct irq_chip的定义如下。我们可以看到里面的成员大部分是函数指针(也就是接口),这些接口全部是和芯片打交道的,将内核移植到新的硬件平台需要关注这些接口的实现。

这个结构体的技术含量在于其接口定义的完备性,提取了所有芯片操作中断的共性,一半不是经验老手定义不了这么全。

/* include/linux/irq.h */
struct irq_chip {
	const char	*name; /*芯片名称*/
	unsigned int	(*startup)(unsigned int irq); /*启动中断*/
	void		(*shutdown)(unsigned int irq); /*关闭中断*/
	void		(*enable)(unsigned int irq); /*使能中断*/
	void		(*disable)(unsigned int irq); /*去使能中断*/

	void		(*ack)(unsigned int irq); /*使当前中断号可以响应中断(清中断)*/
	void		(*mask)(unsigned int irq); /*屏蔽中断*/
	void		(*mask_ack)(unsigned int irq); /*屏蔽+响应中断(组合功能)*/
	void		(*unmask)(unsigned int irq); /*去除屏蔽*/
	...
	void		(*set_affinity)(unsigned int irq, cpumask_t dest); /*设置中断亲和性*/
	...
};

1.3 handle_irq

下面是irq_flow_handler_t的定义。在irq_desc中handle_irq成员一般都指向 handle_level_irq()handle_edge_irq() 函数。

typedef	void fastcall (*irq_flow_handler_t)(unsigned int irq,
					    struct irq_desc *desc);

1.4 irqaction

距离用户最近的成员就是irqaction了。

/* include/linux/interrup.h */
struct irqaction {
	irq_handler_t handler; /*用户注册的中断处理函数链表*/
	unsigned long flags; /*类型标记:是否是共享中断;标识触发方式等*/
	cpumask_t mask; /*SMP处理器上的亲和性*/
	const char *name; /*cat /proc/interrupts 中显示的名字(用户指定)*/
	void *dev_id; /*用于区分共享中断,可以理解为共享中断号的子中断号*/
	struct irqaction *next; /*链表指针*/
	int irq; /*中断号*/
	struct proc_dir_entry *dir; /*处理文件路径相关信息(可以先不用关注)*/
};

2 中断初始化

任何一个模块一般都包括三个部分的代码—— 初始化代码流程代码控制代码

本小节先来分析中断的初始化。

我们可以猜想,初始化的工作肯定就是填充我们上一节分析的数据结构。

2.1 init_IRQ

该函数被start_kernel函数调用,想要了解内核启动流程的同学可以看我之前写的内核启动的系列文章。

/* arch/arm/kernel/irq.c */
void __init init_IRQ(void)
{
	int irq;

	for (irq = 0; irq < NR_IRQS; irq++) /*将所有中断都禁止*/
		irq_desc[irq].status |= IRQ_NOREQUEST | IRQ_NOPROBE;

#ifdef CONFIG_SMP /*SMP系统*/
	bad_irq_desc.affinity = CPU_MASK_ALL;
	bad_irq_desc.cpu = smp_processor_id();
#endif
	init_arch_irq(); /*与芯片架构相关的初始化工作*/
}

2.2 init_arch_irq

我们跳到init_arch_irq的定义处,发现其是一个函数指针,这就导致我们的思路被打断了,需要我们再去看这个函数指针指向的到底是哪个函数。

/* arch/arm/kernel/irq.c */
void (*init_arch_irq)(void) __initdata = NULL;

Tips:内核中有好多函数指针,这也是内核看起来比较费劲的一个重要原因。那为什么这么设计呢?其实还是为了能够兼容更多的硬件,不得不搞得复杂一点。不过看习惯了也就好了,这是C语言中实现“多态”不可绕过的一步。

接下来,我们需要找到init_arch_irq是在哪里初始化的。

/* arch/arm/kernel/setup.c */
void __init setup_arch(char **cmdline_p)
{
    ...
        mdesc = setup_machine(machine_arch_type); /*machine_arch_type在汇编引导阶段已经被赋值*/
    ...
	/*
	 * Set up various architecture-specific pointers
	 */
        init_arch_irq = mdesc->init_irq; /*在该行赋的值*/
    ...
}
    

我们看到init_arch_irq的赋值阶段也是在内核初始化时完成的,它指向的内容是mdesc->init_irq。这其实还是一个函数指针,mdesc结构体和和我们的芯片架构有关系。

对于S3C2440芯片来说,该结构体初始化的情况如下。至于是如何通过机器码定位到该结构体的请参看我之前的文章:03内核启动流程分析(八,uboot命令行参数获取),当然也可以先跳过该细节问题。

MACHINE_START(S3C2440, "SMDK2440")
	/* Maintainer: Ben Dooks <ben@fluff.org> */
	.phys_io	= S3C2410_PA_UART,
	.io_pg_offst	= (((u32)S3C24XX_VA_UART) >> 18) & 0xfffc,
	.boot_params	= S3C2410_SDRAM_PA + 0x100, /*boot_params参数基址为:0x30000100*/

	.init_irq	= s3c24xx_init_irq, /*中断初始化函数的真面目*/
	.map_io		= smdk2440_map_io,
	.init_machine	= smdk2440_machine_init,
	.timer		= &s3c24xx_timer,
MACHINE_END

最终,我们锁定了S3C2440芯片对应的中断初始化函数为 s3c24xx_init_irq

2.3 s3c24xx_init_irq

接下来中断初始化的BOSS终于登场了。

我们查看源码发现和我们先前猜想的是一样的,初始化的重点工作就是配置上一节的数据结构。

/* arch/arm/plat-s3c24xx */
/* s3c24xx_init_irq
 *
 * Initialise S3C2410 IRQ system
 */
void __init s3c24xx_init_irq(void)
{
	unsigned long pend;
	unsigned long last;
	int irqno;
	int i;

	...

	/* register the main interrupts */

	irqdbf("s3c2410_init_irq: registering s3c2410 interrupt handlers\\n");

	for (irqno = IRQ_EINT4t7; irqno <= IRQ_ADCPARENT; irqno++) {
		/* set all the s3c2410 internal irqs */

		switch (irqno) {
			/* deal with the special IRQs (cascaded) */

		case IRQ_EINT4t7:
		case IRQ_EINT8t23:
		case IRQ_UART0:
		case IRQ_UART1:
		case IRQ_UART2:
		case IRQ_ADCPARENT:
			set_irq_chip(irqno, &s3c_irq_level_chip); /*填充irq_chip结构体*/
			set_irq_handler(irqno, handle_level_irq); /*填充handle_irq结构体*/
			break;

		case IRQ_RESERVED6:
		case IRQ_RESERVED24:
			/* no IRQ here */
			break;

		default:
			//irqdbf("registering irq %d (s3c irq)\\n", irqno);
			set_irq_chip(irqno, &s3c_irq_chip);
			set_irq_handler(irqno, handle_edge_irq);
			set_irq_flags(irqno, IRQF_VALID);
		}
	}

	/* setup the cascade irq handlers */
	/* 填充特殊中断的handle_irq结构体 */
	set_irq_chained_handler(IRQ_EINT4t7, s3c_irq_demux_extint4t7);
	set_irq_chained_handler(IRQ_EINT8t23, s3c_irq_demux_extint8);

	set_irq_chained_handler(IRQ_UART0, s3c_irq_demux_uart0);
	set_irq_chained_handler(IRQ_UART1, s3c_irq_demux_uart1);
	set_irq_chained_handler(IRQ_UART2, s3c_irq_demux_uart2);
	set_irq_chained_handler(IRQ_ADCPARENT, s3c_irq_demux_adc);

	/* external interrupts */

	for (irqno = IRQ_EINT0; irqno <= IRQ_EINT3; irqno++) {
		irqdbf("registering irq %d (ext int)\\n", irqno);
		set_irq_chip(irqno, &s3c_irq_eint0t4); /*填充irq_chip结构体*/
		set_irq_handler(irqno, handle_edge_irq); /*填充handle_irq结构体*/
		set_irq_flags(irqno, IRQF_VALID); /*将中断标记置位*/
	}

	for (irqno = IRQ_EINT4; irqno <= IRQ_EINT23; irqno++) {
		irqdbf("registering irq %d (extended s3c irq)\\n", irqno);
		set_irq_chip(irqno, &s3c_irqext_chip);
		set_irq_handler(irqno, handle_edge_irq);
		set_irq_flags(irqno, IRQF_VALID);
	}

	...

	irqdbf("s3c2410: registered interrupt handlers\\n");
}

未完待续。。。


恭喜你又坚持看完了一篇博客,又进步了一点点!如果感觉还不错就点个赞再走吧,你的点赞和关注将是我持续输出的哒哒哒动力~~

以上是关于一起分析Linux系统设计思想——05中断框架剖析的主要内容,如果未能解决你的问题,请参考以下文章

一起分析Linux系统设计思想——05中断框架剖析

一起分析Linux系统设计思想——05中断框架剖析

一起分析Linux系统设计思想——05中断框架剖析

一起分析Linux系统设计思想——05中断框架剖析

一起分析Linux系统设计思想——05内核定时器的使用

一起分析Linux系统设计思想——05内核定时器的使用