(1)同步中断又称为异常,是当指令执行时由CPU控制单元产生,之所以称为同步,是因为只有在一条指令执行完成后CPU才会发出中断。典型的异常有缺页和除0。
当一个异常或中断发生时,处理器会把pc设置为一个特定的存储器地址。这个地址放在一个被称为向量表(vector table)的特定的地址范围内。向量表的入口是一些跳转指令,跳转到专门处理某个异常或中断的子程序。
存储器映射地址0x00000000是为向量表保留的。在有些处理器中,向量表可以选择定位在存储空间的更高地址(从偏移量0xffff0000开始)。操作系统,如Linux和Microsoft的嵌入式操作系统,就可以利用这一特性。
2.相关数据结构
与中断相关的主要数据结构有三个,分别是:irq_desc、irq_chip和irqaction。下面将介绍一下这三个数据结构的实现以及它们之间的关系。
内核通过irq_desc来描述一个中断,irq_desc结构体在include/linux/irq.h中定义:
-
struct irq_desc {
-
unsigned int irq; /*中断号*/
-
……
-
irq_flow_handler_t handle_irq; /*电流层中断处理函数 */
-
struct irq_chip *chip; /*包含处理器相关的处理函数 */
-
struct msi_desc *msi_desc;
-
void *handler_data;
-
void *chip_data;
-
struct irqaction *action; /* IRQ操作链表 */
-
unsigned int status; /* IRQ状态 */
-
-
unsigned int depth;
-
……
-
const char *name;
-
} ____cacheline_internodealigned_in_smp;
其中,handle_irq指向电流层中断处理函数,主要用于对不同触发方式(电平、边沿)的处理。handler_data可以指向任何数据,供handle_irq函数使用。每当发生中断时,体系结构相关的代码都会调用handle_irq,该函数负责使用chip中提供的处理器相关的方法,来完成处理中断所必需的一些底层操作。用于不同中断类型的默认函数由内核提供,例如handle_level_irq和handle_edge_irq等。
中断控制器相关操作被封装到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 (*eoi)(unsigned int irq);
-
-
void (*end)(unsigned int irq);
-
int (*set_affinity)(unsigned int irq, const struct cpumask *dest);
-
int (*retrigger)(unsigned int irq);
-
int (*set_type)(unsigned int irq, unsigned int flow_type);
-
int (*set_wake)(unsigned int irq, unsigned int on);
-
void (*bus_lock)(unsigned int irq);
-
void (*bus_sync_unlock)(unsigned int irq);
-
……
-
const char *typename;
-
};
action指向一个操作链表,在中断发生时执行。由中断通知设备驱动程序,可以将与之相关的处理函数放置在此处。irqaction结构体在include/linux/interrupt.h中定义:
-
struct irqaction {
-
irq_handler_t handler;
-
unsigned long flags;
-
const char *name;
-
void *dev_id;
-
struct irqaction *next;
-
int irq;
-
struct proc_dir_entry *dir;
-
irq_handler_t thread_fn;
-
struct task_struct *thread;
-
unsigned long thread_flags;
-
};
每个处理函数都对应该结构体的一个实例irqaction,该结构体中最重要的成员是handler,这是个函数指针。该函数指针就是通过request_irq来初始化的,在后续小节中将详细介绍。当系统产生中断,内核将调用该函数指针所指向的处理函数。name和dev_id唯一地标识一个irqaction实例,name用于标识设备名,dev_id用于指向设备的数据结构实例。
请注意irq_desc中的handle_irq不同于irqaction中的handler,这两个函数指针分别定义为:
-
typedef void (*irq_flow_handler_t)(unsigned int irq, struct irq_desc *desc);
-
typedef irqreturn_t (*irq_handler_t)(int, void *);
内核通过维护一个irq_desc的全局数组来管理所有的中断。该数组在kernel/irq/handle.c中定义:
-
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 = __RAW_SPIN_LOCK_UNLOCKED(irq_desc->lock),
-
}
-
};
其中NR_IRQS表示支持中断的最大数,一般在arch/arm/mach-XXX/include/mach/irqs.h文件中定义。
图2-1描述了以上三个数据结构之间的关系。所有irq_desc的chip成员都指向同一irq_chip实例。
图2-1 IRQ相关数据结构关系
3.中断初始化
ARM Linux中断初始化主要通过三个函数完成:early_trap_init 、early_irq_init和init_IRQ。
整个Linux中断初始化流程如图3-1所示:
图3-1 Linux中断初始化流程
early_trap_init函数定义在arch/arm/kernel/traps.c文件中,在setup_arch函数最后调用的,主要完成ARM异常向量表和异常处理程序的重定位。
-
void __init early_trap_init(void)
-
{
-
unsigned long vectors = CONFIG_VECTORS_BASE;
-
extern char __stubs_start[], __stubs_end[];
-
extern char __vectors_start[], __vectors_end[];
-
extern char __kuser_helper_start[], __kuser_helper_end[];
-
int kuser_sz = __kuser_helper_end - __kuser_helper_start;
-
-
/*
-
* 拷贝异常向量表到vectors地址处,通常是0xffff0000。
-
* 拷贝异常处理程序到vectors+0x200地址处。
-
*/
-
memcpy((void *)vectors, __vectors_start, __vectors_end - __vectors_start);
-
memcpy((void *)vectors + 0x200, __stubs_start, __stubs_end - __stubs_start);
-
memcpy((void *)vectors + 0x1000 - kuser_sz, __kuser_helper_start, kuser_sz);
-
-
/*
-
* 拷贝信号处理函数
-
*/
-
memcpy((void *)KERN_SIGRETURN_CODE, sigreturn_codes,
-
sizeof(sigreturn_codes));
-
/*
-
* 拷贝信号处理函数
-
*/
-
flush_icache_range(vectors, vectors + PAGE_SIZE);
-
modify_domain(DOMAIN_USER, DOMAIN_CLIENT);
-
}
这个函数把定义在 arch/arm/kernel/entry-armv.S 中的异常向量表和异常处理程序的 stub 进行重定位:异常向量表拷贝到 0xFFFF_0000,异常向量处理程序的 stub 拷贝到 0xFFFF_0200。然后调用 modify_domain()修改了异常向量表所占据的页面的访问权限,这使得用户态无法访问该页,只有核心态才可以访问。arm 处理器发生异常时总会跳转到 0xFFFF_0000(设为“高端向量配置”时)处的异常向量表,所以要进行这个重定位工作。
early_irq_init函数定义在kernel/irq/handle.c文件中:
-
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),
-
}
-
};
-
-
static unsigned int kstat_irqs_all[NR_IRQS][NR_CPUS];
-
int __init early_irq_init(void)
-
{
-
struct irq_desc *desc;
-
int count;
-
int i;
-
-
init_irq_default_affinity();
-
-
printk(KERN_INFO "NR_IRQS:%d\n", NR_IRQS);
-
-
desc = irq_desc;
-
count = ARRAY_SIZE(irq_desc);
-
-
for (i = 0; i < count; i++) {
-
desc[i].irq = i;
-
alloc_desc_masks(&desc[i], 0, true);
-
init_desc_masks(&desc[i]);
-
desc[i].kstat_irqs = kstat_irqs_all[i];
-
}
-
return arch_early_irq_init();
-
}
kernel/irq/handle.c文件中,根据内核的配置选项CONFIG_SPARSE_IRQ,可以选择两个不同版本的early_irq_init()中的一个进行编译。CONFIG_SPARSE_IRQ配置项,用于支持稀疏irq号,对于发行版的内核很有用,它允许定义一个高CONFIG_NR_CPUS值,但仍然不希望消耗太多内存的情况。通常情况下,我们并不配置该选项。
early_irq_init的主要工作是初始化用于管理中断的irq_desc[NR_IRQS]数组的每个元素(NR_IRQS表示中断的总数,在irqs.h文件中定义),它主要设置数组中每一个成员的中断号,使得数组中每一个元素的kstat_irqs字段(irq stats per cpu),指向定义的二维数组kstat_irqs_all中的对应的行。alloc_desc_masks(&desc[i], 0, true)和init_desc_masks(&desc[i])函数在非SMP平台上为空函数。arch_early_irq_init()在主要用于x86平台和PPC平台,其他平台上为空函数。
init_IRQ函数定义在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;
-
-
init_arch_irq();
-
}
init_IRQ函数首先遍历irq_desc中断描述符表,并初始化每一个中断的状态为IRQ_NOREQUEST和IRQ_NOPROBE。然后调用板级相关函数init_arch_irq,该函数在setup_arch函数中被初始化为mdesc->init_irq,而mdesc->init_irq通常在arch/arm/mach-xxx目录下的板级文件中定义的。init_arch_irq函数完成对cpu中断控制器初始化,通常还会通过以下for循环来初始化irq_desc数组:
-
for (irq = IRQ_TIMER0; irq <= IRQ_TIMER4; irq++) {
-
set_irq_chip(irq, &arch_chip);//将中断控制器操作结构体irq_chip的实例注册到irq_desc[irq].chip上
-
set_irq_handler(irq, handle_level_irq);
-
set_irq_flags(irq, IRQF_VALID);
-
}
4.注册中断处理程序
本节将主要讲述如何注册中断处理程序。Linux中用于注册中断处理程序的函数是request_irq,该函数的实现为:
-
static inline int __must_check
-
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
-
const char *name, void *dev)
-
{
-
return request_threaded_irq(irq, handler, NULL, flags, name, dev);
-
}
-
int request_threaded_irq(unsigned int irq, irq_handler_t handler,
-
irq_handler_t thread_fn, unsigned long irqflags,
-
const char *devname, void *dev_id)
-
{
-
struct irqaction *action;
-
struct irq_desc *desc;
-
int retval;
-
-
/*
-
* handle_IRQ_event() always ignores IRQF_DISABLED except for
-
* the _first_ irqaction (sigh). That can cause oopsing, but
-
* the behavior is classified as "will not fix" so we need to
-
* start nudging drivers away from using that idiom.
-
*/
-
if ((irqflags & (IRQF_SHARED|IRQF_DISABLED)) ==
-
(IRQF_SHARED|IRQF_DISABLED)) {
-
pr_warning(
-
"IRQ %d/%s: IRQF_DISABLED is not guaranteed on shared IRQs\n",
-
irq, devname);
-
}
-
-
#ifdef CONFIG_LOCKDEP
-
/*
-
* Lockdep wants atomic interrupt handlers:
-
*/
-
irqflags |= IRQF_DISABLED;
-
#endif
-
/*
-
* Sanity-check: shared interrupts must pass in a real dev-ID,
-
* otherwise we'll have trouble later trying to figure out
-
* which interrupt is which (messes up the interrupt freeing
-
* logic etc).
-
*/
-
if ((irqflags & IRQF_SHARED) && !dev_id)
-
return -EINVAL;
-
-
desc = irq_to_desc(irq);
-
if (!desc)
-
return -EINVAL;
-
-
if (desc->status & IRQ_NOREQUEST)
-
return -EINVAL;
-
-
if (!handler) {
-
if (!thread_fn)
-
return -EINVAL;
-
handler = irq_default_primary_handler;
-
}
-
-
action = kzalloc(sizeof(struct irqaction), GFP_KERNEL);
-
if (!action)
-
return -ENOMEM;
-
-
action->handler = handler;
-
action->thread_fn = thread_fn;
-
action->flags = irqflags;
-
action->name = devname;
-
action->dev_id = dev_id;
-
-
chip_bus_lock(irq, desc);
-
retval = __setup_irq(irq, desc, action);
-
chip_bus_sync_unlock(irq, desc);
-
-
if (retval)
-
kfree(action);
-
return retval;
-
}
图3-1给出了request_irq代码流程图。
图3-1 request_irq代码流程图
内核首先生成一个新的irqaction实例,然后用函数参数填充其内容。
所有进一步的工作都委托给__setup_irq函数,它将执行以下步骤:
(1)如果设置了IRQF_SAMPLE_RANDOM,则该中断将对内核熵池有所贡献,熵池用于随机数发生器/dev/random。
(2)由request_irq生成的irqaction实例被添加到所属IRQ编号对应的链表尾部,该链表表头为desc->action。在处理共享中断时,内核就通过这种方式来确保中断发生时调用处理程序的顺序与其注册顺序相同。
(3)register_irq_proc在proc文件系统中建立目录/proc/irq/NUM。而register_handler_proc生成/proc/irq/NUM/name。
参考资料:
[1] Andrew N.Sloss, Dominic Symes, Chris Wright. ARM嵌入式系统开发——软件设计与优化. 沈建华, 译.
[2] Daniel P.Bovet, Marco Cesati. 深入理解Linux内核(第三版). 陈莉君, 张琼声, 张宏伟, 译.
[3] Pobert Love. Linux内核设计与实现. 陈莉君, 康华, 张波, 译.
[4] Wolfgang Mauerer. 深入Linux内核架构. 郭旭, 译.
[5] 陈学松. 深入Linux设备驱动程序内核机制.
[6] ARM Architecture Reference Manual
[7] The ARM-THUMB Procedure Call Standard