简介:中断,顾名思义,中途打断CPU正在处理的任务,转而去执行紧急事务。中断既有硬件支持,也有软件控制,下面就开始中断的介绍。
一、中断硬件框架结构
中断的硬件框架组成有 设备、中断控制器、CPU。终端硬件框架如下:
1、 设备:设备是发起中断的源头,当设备需要请求某种服务时,会发起一个中断信号,通常,该信号会通过中断线发送至中断控制器处理。设备都是一些设备的控制器,其既可以位于soc外,也可以位于SOC上。其中Display Controller就位于SOC上。
2、 中断控制器:中断控制器负责收集所有中断源(设备)发起的中断,即所有的中断在到达CPU之前都要经过中断控制器,只有合乎要求的中断才会通知CPU处理。现有的中断控制器几乎都是可编程的,通过对中断控制器的编程,我们可以控制每个中断源的优先级、中断的电器类型,还可以打开和关闭某一个中断源,在smp系统中,甚至可以控制某个中断源发往哪一个CPU进行处理
中断控制器的功能如下:
· 对各个irq的优先级进行控制;
· 向CPU发出中断请求后,提供某种机制让CPU获得实际的中断源(irq编号);
· 控制各个irq的电气触发条件,例如边缘触发或者是电平触发;
· 使能(enable)或者屏蔽(mask)某一个irq;
· 提供嵌套中断请求的能力;
· 提供清除中断请求的机制(ack);
· 有些控制器还需要CPU在处理完irq后对控制器发出eoi指令(end of interrupt);
· 在smp系统中,控制各个irq与cpu之间的亲缘关系(affinity);
3、 CPU:cpu是最终响应中断的部件。此处以ARM为例,当CPU相应中断请求时,CPU会自动做一下操作(在做一下操作之前,会保存被中断打断的进程的上下文):
(1)、用CPU中断模式下的R14保存前一工作模式即将执行的下一条指令;
(2)、将CPSR中的值赋到中断模式下的SPSR中,同时修改CPSR中的工作模式为中断模式,将CPSR的I位置为1,来禁止本CPU的本地中断;
(3)、将PC的值等于异常向量表的中断地址
后面就去执行对应的软件流程。接下来谈一下中断软件框架结构
二、中断软件框架结构
现在的linux系统中通过通用中断子系统来控制处理中断,用下图来表示中断的软件框架结构,如图:
从底层简单的介绍各个层次。
(1)、硬件封装层:它包含了体系架构相关的所有代码,包括:
1) 中断控制器的抽象封装
2) 体系架构(arch)相关的中断初始化
3) IRQ相关数据结构的初始化工作,如irq_desc,irq_chip,irq_data,action等数据结构,后面会介绍
4) CPU中断入口,也即跳转到异常向量表的实现
(2)、中断通用逻辑层:
中断通用逻辑层通过标准的封装接口(实际上就是struct irq_chip定义的接口)访问并控制中断控制器;中断入口使用irq编号,可通过中断通用逻辑层提供的标准函数(下面会介绍),把中断调用传递到中断流控层中
1) 该层实现了对中断子系统相关的几个数据结构的管理,并提供了一系列的辅助管理函数
2) 实现了中断线程的实现和管理
3) 共享中断和嵌套中断的实现和管理
4) 还提供了一些接口函数,作为硬件封装层和中断流控层以及驱动程序API之间交互的桥梁,API如下:
generic_handle_irq();
irq_to_desc();
irq_set_chip();
irq_set_chained_handler();
(3)中断流控层:所谓中断流控是指合理并正确地处理连续发生的中断。
解释几个问题:
1) 问题:smp系统,一个中断在处理中,此时同一个中断再次产生,另一个CPU会不会去相应新来的中断?
答:要分是什么触发,边沿还是电平触发。
电平触发:来中断时会先mask然后ack,mask是禁止产生中断,ack就是复位设备中断脚,在ack发出之后,设备就可以再次来中断,否则会一直保持高电平状态。虽说在ack后可以再次来中断,但是由于之前执行了mask,禁止中断,所以直到执行unmask另一个CPU才会收到中断。
边沿触发:不像电平触发那样,不ack会一直保持高电平,边沿触发只是在电平跳变时才触发IRQ,所以处理不当就容易丢失。也正因为这样,边沿触发不会maskirq,只是ack,以便复位引脚,在这之后再次产生中断另一个CPU可以做处理,因为之前并没有mask,后文会有详尽的解释。
该层实现了与体系和硬件无关的中断流控处理操作,它针对不同的中断电气类型(level,edge......),实现了对应的标准中断流控处理函数,在这些处理函数中,最终会把中断控制权传递到驱动程序注册中断时传入的处理函数或者是中断线程中。目前内核提供了以下几个主要的中断流控函数的实现(只列出部分):
· handle_simple_irq();
· handle_level_irq();电平中断流控处理程序
· handle_edge_irq();边沿触发中断流控处理程序
· handle_fasteoi_irq();需要eoi的中断处理器使用的中断流控处理程序
· handle_percpu_irq();该irq只有单个cpu响应时使用的流控处理程序
(4)驱动程序API
该部分向驱动程序(驱动程序员可以调用)提供了一系列的API,用于向系统申请/释放中断,打开/关闭中断,设置中断类型和中断唤醒系统的特性等操作
· enable_irq();
· disable_irq();
· disable_irq_nosync();
· request_threaded_irq();
· irq_set_affinity();
1、中断数据结构介绍
整个通用中断子系统几乎都是围绕着irq_desc结构进行,系统中每一个irq都对应着一个irq_desc结构,所有的irq_desc结构的组织方式有两种:数组和基数树。在我们的代码里是基于数组的方式来组织的
代码在kernel-3.10/kernel/irq/irqdesc.c中
(1)、数据结构irq_desc
成员简要解释:
irq_data这个内嵌结构在2.6.37版本引入,之前的内核版本的做法是直接把这个结构中的字段直接放置在irq_desc结构体中,然后在调用硬件封装层的chip->xxx()回调中传入IRQ编号作为参数,但是底层的函数经常需要访问->handler_data,->chip_data,->msi_desc等字段,这需要利用irq_to_desc(irq)来获得irq_desc结构的指针,然后才能访问上述字段,这带来了性能的降低,尤其在配置为sparseirq的系统中更是如此,因为这意味着基数树的搜索操作。为了解决这一问题,内核开发者把几个低层函数需要使用的字段单独封装为一个结构,调用时的参数则改为传入该结构的指针。实现同样的目的,那为什么不直接传入irq_desc结构指针?因为这会破坏层次的封装性,我们不希望低层代码可以看到不应该看到的部分,仅此而已。
kstat_irqs用于irq的一些统计信息,这些统计信息可以从proc文件系统中查询。
action中断响应链表,当一个irq被触发时,内核会遍历该链表,调用action结构中的回调handler或者激活其中的中断线程,之所以实现为一个链表,是为了实现中断的共享,多个设备共享同一个irq,这在外围设备中是普遍存在的。
status_use_accessors记录该irq的状态信息,内核提供了一系列irq_settings_xxx的辅助函数访问该字段,详细请查看kernel/irq/settings.h
depth用于管理enable_irq()/disable_irq()这两个API的嵌套深度管理,每次enable_irq时该值减去1,每次disable_irq时该值加1,只有depth==0时才真正向硬件封装层发出关闭irq的调用,只有depth==1时才会向硬件封装层发出打开irq的调用。disable的嵌套次数可以比enable的次数多,此时depth的值大于1,随着enable的不断调用,当depth的值为1时,在向硬件封装层发出打开irq的调用后,depth减去1后,此时depth为0,此时处于一个平衡状态,我们只能调用disable_irq,如果此时enable_irq被调用,内核会报告一个irq失衡的警告,提醒驱动程序的开发人员检查自己的代码。
lock用于保护irq_desc结构本身的自旋锁。
affinity_hit用于提示用户空间,作为优化irq和cpu之间的亲缘关系的依据。
pending_mask用于调整irq在各个cpu之间的平衡。
wait_for_threads用于synchronize_irq(),等待该irq所有线程完成。
(2)、数据结构irq_data
成员简要解释:
irq_data 结构中的各字段:
irq 该结构所对应的IRQ编号。
hwirq 硬件irq编号,它不同于上面的irq;
node 通常用于hwirq和irq之间的映射操作;
state_use_accessors 硬件封装层需要使用的状态信息,不要直接访问该字段,内核定义了一组函数用于访问该字段:irqd_xxxx(),参见include/linux/irq.h。
chip 指向该irq所属的中断控制器的irq_chip结构指针
handler_data 每个irq的私有数据指针,该字段由硬件封转层使用,例如用作底层硬件的多路复用中断。
chip_data 中断控制器的私有数据,该字段由硬件封转层使用。
msi_desc 用于PCIe总线的MSI或MSI-X中断机制。
affinity 记录该irq与cpu之间的亲缘关系,它其实是一个bit-mask,每一个bit代表一个cpu,置位后代表该cpu可能处理该irq。
(3)、数据结构irq_chip
成员简单介绍:
name中断控制器的名字,会出现在/proc/interrupts中。
irq_startup第一次开启一个irq时使用。
irq_shutdown与irq_starup相对应。
irq_enable使能该irq,通常是直接调用irq_unmask()。
irq_disable禁止该irq,通常是直接调用irq_mask,严格意义上,他俩其实代表不同的意义,disable表示中断控制器根本就不响应该irq,而mask时,中断控制器可能响应该irq,只是不通知CPU,这时,该irq处于pending状态。类似的区别也适用于enable和unmask。
irq_ack用于CPU对该irq的回应,通常表示cpu希望要清除该irq的pending状态,准备接受下一个irq请求。
irq_mask屏蔽该irq。
irq_unmask取消屏蔽该irq。
irq_mask_ack相当于irq_mask+ irq_ack。
irq_eoi有些中断控制器需要在cpu处理完该irq后发出eoi信号,该回调就是用于这个目的。
irq_set_affinity用于设置该irq和cpu之间的亲缘关系,就是通知中断控制器,该irq发生时,那些cpu有权响应该irq。当然,中断控制器会在软件的配合下,最终只会让一个cpu处理本次请求。
irq_set_type设置irq的电气触发条件,例如IRQ_TYPE_LEVEL_HIGH或IRQ_TYPE_EDGE_RISING。
irq_set_wake通知电源管理子系统,该irq是否可以用作系统的唤醒源。
2、 启动阶段中断初始化
CPU响应中断时,硬件上会自动做以下动作(上面介绍过的):
(1)、保存进程上下文
(2)、用CPU中断模式下的R14保存前一工作模式的即将执行的下一条指令
(3)、将CPSR中的值赋到中断模式下的SPSR中,同时修改CPSR中的工作模式为中断模式,将CPSR的I位置为1,来禁止本CPU的本地中断
(4)、将PC的值等于异常向量表的中断地址
· 第一部分是真正的向量跳转表,位于__vectors_start和__vectors_end之间;
· 第二部分是处理跳转的部分,位于__stubs_start和__stubs_end之间;
(1)、中断arch的初始化,异常向量表是存在内存中,那异常向量表是什么时候创建成的呢?与通用中断子系统相关的初始化由start_kernel()函数发起,调用流程如下图所视(注:此文所说的都是32位操作系统下的代码):
通用中断子系统的初始化
1)首先,在setup_arch函数中,early_trap_init被调用
arm的异常和复位向量表有两种选择,一种是低端向量,向量地址位于0x00000000,另一种是高端向量,向量地址位于0xffff0000,Linux选择使用高端向量模式,也就是说,当异常发生时,CPU会把PC指针自动跳转到始于0xffff0000开始的某一个地址上
以上两个memcpy会把__vectors_start开始的代码拷贝到0xffff0000处,把__stubs_start开始的代码拷贝到0xFFFF0000+0x200处,这样,异常中断到来时,CPU就可以正确地跳转到相应中断向量入口并执行他们
2)然后,start_kernel发出early_irq_init调用,early_irq_init属于与硬件和平台无关的通用逻辑层,它完成irq_desc结构的内存申请,为它们其中某些字段填充默认值,完成后调用体系相关的arch_early_irq_init函数完成进一步的初始化工作,不过ARM体系没有实现arch_early_irq_init.
3)接着,start_kernel发出init_IRQ调用,它会直接调用所属板子machine_desc结构体中的init_irq回调。machine_desc通常在板子的特定代码中,使用MACHINE_START和MACHINE_END宏进行定义。
machine_desc->init_irq()完成对中断控制器的初始化,为每个irq_desc结构安装合适的流控handler,为每个irq_desc结构安装irq_chip指针,使他指向正确的中断控制器所对应的irq_chip结构的实例,同时,如果该平台中的中断线有多路复用(多个中断公用一个irq中断线)的情况,还应该初始化irq_desc中相应的字段和标志,以便实现中断控制器的级联。
(2)流控回调函数的分配以及驱动中注册中断
在构建的irq_desc的数组中,给对应irq的irq_desc中的handle_irq(流控回调函数会赋给该字段)字段以及irq_desc[irq]中action字段中的handler(是驱动程序中注册的中断处理函数)赋值。
系统启动阶段,中断子系统完成了必要的初始化工作,为驱动程序申请中断服务做好了准备,通常,我们用一下API申请中断服务:
[cpp]view plaincopy
1. request_threaded_irq(unsigned int irq, irq_handler_t handler,
2. irq_handler_t thread_fn,
3. unsigned long flags, const char *name, void *dev);
irq 需要申请的irq编号,对于ARM体系,irq编号通常在平台级的代码中事先定义好,有时候也可以动态申请。
handler中断服务回调函数,该回调运行在中断上下文中,并且cpu的本地中断处于关闭状态,所以该回调函数应该只是执行需要快速响应的操作,执行时间应该尽可能短小,耗时的工作最好留给下面的thread_fn回调处理。
thread_fn如果该参数不为NULL,内核会为该irq创建一个内核线程,当中断发生时,如果handler回调返回值是IRQ_WAKE_THREAD,内核将会激活中断线程,在中断线程中,该回调函数将被调用,所以,该回调函数运行在进程上下文中,允许进行阻塞操作。
flags控制中断行为的位标志,IRQF_XXXX,例如:IRQF_TRIGGER_RISING,IRQF_TRIGGER_LOW,IRQF_SHARED等,在include/linux/interrupt.h中定义。
name申请本中断服务的设备名称,同时也作为中断线程的名称,该名称可以在/proc/interrupts文件中显示。
dev当多个设备的中断线共享同一个irq时,它会作为handler的参数,用于区分不同的设备。
下面我们分析一下request_threaded_irq的工作流程。函数先是根据irq编号取出对应的irq_desc实例的指针,然后分配了一个irqaction结构,用参数handler,thread_fn,irqflags,devname,dev_id初始化irqaction结构的各字段,同时做了一些必要的条件判断:该irq是否禁止申请?handler和thread_fn不允许同时为NULL,最后把大部分工作委托给__setup_irq函数:
[cpp]view plaincopy
1. desc = irq_to_desc(irq);
2. if (!desc)
3. return -EINVAL;
4.
5. if (!irq_settings_can_request(desc) ||
6. WARN_ON(irq_settings_is_per_cpu_devid(desc)))
7. return -EINVAL;
8.
9. if (!handler) {
10. if (!thread_fn)
11. return -EINVAL;
12. handler = irq_default_primary_handler;
13. }
14.
15. action = kzalloc(sizeof(struct irqaction), GFP_KERNEL);
16. if (!action)
17. return -ENOMEM;
18.
19. action->handler = handler;
20. action->thread_fn = thread_fn;
21. action->flags = irqflags;
22. action->name = devname;
23. action->dev_id = dev_id;
24.
25. chip_bus_lock(desc);
26. retval = __setup_irq(irq, desc, action);
27. chip_bus_sync_unlock(desc);
进入__setup_irq函数,如果参数flag中设置了IRQF_SAMPLE_RANDOM标志,它会调用rand_initialize_irq,以便对随机数的生成产生影响。如果申请的不是一个线程嵌套中断,而且提供了thread_fn参数,它将创建一个内核线程:
[cpp]view plaincopy
1. if (new->thread_fn && !nested) {
2. struct task_struct *t;
3.
4. t = kthread_create(irq_thread, new, "irq/%d-%s", irq,
5. new->name);
6. if (IS_ERR(t)) {
7. ret = PTR_ERR(t);
8. goto out_mput;
9. }
10. /*
11. * We keep thereference to the task struct even if
12. * the threaddies to avoid that the interrupt code
13. * references analready freed task_struct.
14. */
15. get_task_struct(t);
16. new->thread = t;
17. }
如果irq_desc结构中断action链表不为空,说明这个irq已经被其它设备申请过,也就是说,这是一个共享中断,所以接下来会判断这个新申请的中断与已经申请的旧中断的以下几个标志是否一致:
· 一定要设置了IRQF_SHARED标志
· 电气触发方式要完全一样(IRQF_TRIGGER_XXXX)
· IRQF_PERCPU要一致
· IRQF_ONESHOT要一致
检查这些条件都是因为多个设备试图共享一根中断线,试想一下,如果一个设备要求上升沿中断,一个设备要求电平中断,当中断到达时,内核将不知如何选择合适的流控操作。完成检查后,函数找出action链表中最后一个irqaction实例的指针。
[cpp]view plaincopy
1. /* add newinterrupt at end of irq queue */
2. do {
3. thread_mask |= old->thread_mask;
4. old_ptr = &old->next;
5. old = *old_ptr;
6. } while (old);
7. shared = 1;
如果这不是一个共享中断,或者是共享中断的第一次申请,函数将初始化irq_desc结构中断线程等待结构:wait_for_threads,disable_irq函数会使用该字段等待所有irq线程的结束。接下来设置中断控制器的电气触发类型,然后处理一些必要的IRQF_XXXX标志位。如果没有设置IRQF_NOAUTOEN标志,则调用irq_startup()打开该irq,在irq_startup()函数中irq_desc中的enable_irq/disable_irq嵌套深度字段depth设置为0,代表该irq已经打开,如果在没有任何disable_irq被调用的情况下,enable_irq将会打印一个警告信息。
[cpp]view plaincopy
1. if (irq_settings_can_autoenable(desc))
2. irq_startup(desc);
3. else
4. /* Undo nesteddisables: */
5. desc->depth = 1;
接着,设置cpu和irq的亲缘关系:
[cpp]view plaincopy
1. /* Set defaultaffinity mask once everything is setup */
2. setup_affinity(irq, desc, mask);
然后,把新的irqaction实例链接到action链表的最后:
[cpp]view plaincopy
1. new->irq = irq;
2. *old_ptr = new;
最后,唤醒中断线程,注册相关的/proc文件节点:
[cpp]view plaincopy
1. if (new->thread)
2. wake_up_process(new->thread);
3.
4. register_irq_proc(irq, desc);
5. new->dir = NULL;
6. register_handler_proc(irq, new);
irq各个数据结构之间的关系
注:以上暂只适用于mtk 32位操作系统
3、中断工作流程
硬件设备产生中断信号,经由中断控制器判断中断信号的合法性,若合法,则将中断信号发送到CPU,CPU响应中断。先看两个图
(1)中断通用逻辑层
1)保存进程上下文
2)用CPU中断模式下的R14保存前一工作模式的即将执行的下一条指令
3)、将CPSR中的值赋到中断模式下的SPSR中,同时修改CPSR中的工作模式为中断模式,将CPSR的I位置为1,来禁止本CPU的本地中断
4)、将PC的值等于异常向量表的中断地址
上面的2,3,4是CPU硬件上自动执行的,此部分代码多为汇编编写,接下来调用asm_do_IRQ()函数
(2)中断流控层
asm_do_IRQ函数直接调用handle_IRQ(),进入C代码的第一个函数是asm_do_IRQ,在ARM体系中,这个函数只是简单地调用handle_IRQ:
/*
* asm_do_IRQ is the interface to be used from assembly code.
*/
asmlinkage void __exception_irq_entry
asm_do_IRQ(unsigned int irq, struct pt_regs *regs)
{
handle_IRQ(irq, regs);
}
handle_IRQ本身也不是很复杂:
/*
* handle_IRQ handles all hardware IRQ's. Decoded IRQs should
* not come via this function. Instead, they should provide their
* own 'handler'. Used by platform code implementing C-based 1st
* level decoding.
*/
void handle_IRQ(unsigned int irq, struct pt_regs *regs)
{
struct pt_regs *old_regs = set_irq_regs(regs);//将原先CPU的寄存器数据进行保存,将中断相关的寄存器放到CPU的寄存器中
#ifdef CONFIG_MTK_SCHED_TRACERS
struct irq_desc *desc;
#endif
irq_enter();//禁止抢占
mt_trace_ISR_start(irq);
#ifdef CONFIG_MTK_SCHED_TRACERS
desc = irq_to_desc(irq);
trace_irq_entry(irq,
(desc && desc->action && desc->action->name) ? desc->action->name : "-");
#endif
/*
* Some hardware gives randomly wrong interrupts. Rather
* than crashing, do something sensible.
*/
if (unlikely(irq >= nr_irqs)) {
if (printk_ratelimit())
printk(KERN_WARNING "Bad IRQ%u\n", irq);
ack_bad_irq(irq);
} else {
generic_handle_irq(irq);
}
#ifdef CONFIG_MTK_SCHED_TRACERS
trace_irq_exit(irq);
#endif
mt_trace_ISR_end(irq);
irq_exit();
set_irq_regs(old_regs);
}
irq_enter主要是更新一些系统的统计信息,同时在__irq_enter宏中禁止了进程的抢占:
[cpp]view plaincopy
1. #define__irq_enter() \
2. do { \
3. account_system_vtime(current); \
4. add_preempt_count(HARDIRQ_OFFSET); \
5. trace_hardirq_enter(); \
6. } while (0)
CPU一旦响应IRQ中断后,ARM会自动把CPSR中的I位置位,表明禁止新的IRQ请求,直到中断控制转到相应的流控层后才通过local_irq_enable()打开。你可能会奇怪,既然此时的irq中断都是都是被禁止的,为何还要禁止抢占?这是因为要考虑中断嵌套的问题,一旦流控层或驱动程序主动通过local_irq_enable打开了IRQ,而此时该中断还没处理完成,新的irq请求到达,这时代码会再次进入irq_enter,在本次嵌套中断返回时,内核不希望进行抢占调度,而是要等到最外层的中断处理完成后才做出调度动作,所以才有了禁止抢占这一处理。
下一步,generic_handle_irq被调用,generic_handle_irq是通用逻辑层提供的API,通过该API,中断的控制被传递到了与体系结构无关的中断流控层:
[cpp]view plaincopy
1. int generic_handle_irq(unsigned int irq)
2. {
3. struct irq_desc *desc = irq_to_desc(irq);
4.
5. if (!desc)
6. return -EINVAL;
7. generic_handle_irq_desc(irq, desc);
8. return 0;
9. }
最终会进入该irq注册的流控处理回调中:
[cpp]view plaincopy
1. static inline void generic_handle_irq_desc(unsigned int irq, struct irq_desc *desc)
2. {
3. desc->handle_irq(irq, desc);
4. }
此处的desc->handle_irq会调到中断流控回调函数,此处只说一下
handle_level_irq 用于电平触发中断的流控处理;
handle_edge_irq 用于边沿触发中断的流控处理;
1)handle_level_irq
该函数用于处理电平中断的流控操作。电平中断的特点是,只要设备的中断请求引脚(中断线)保持在预设的触发电平,中断就会一直被请求,所以,为了避免同一中断被重复响应,必须在处理中断前先把mask irq,然后ack irq,以便复位设备的中断请求引脚,响应完成后再unmask irq。实际的情况稍稍复杂一点,在mask和ack之后,还要判断IRQ_INPROGRESS标志位,如果该标志已经置位,则直接退出,不再做实质性的处理,IRQ_INPROGRESS标志在handle_irq_event的开始设置,在handle_irq_event结束时清除,如果监测到IRQ_INPROGRESS被置位,表明该irq正在被另一个CPU处理中,所以直接退出,对电平中断来说是正确的处理方法。但是我觉得在ARM系统中,这种情况根本就不会发生,因为在没有进入handle_level_irq之前,中断控制器没有收到ack通知,它不会向第二个CPU再次发出中断请求,而当程序进入handle_level_irq之后,第一个动作就是mask irq,然后ack irq(通常是联合起来的:mask_ack_irq),这时候就算设备再次发出中断请求,也是在handle_irq_event结束,unmask irq之后,这时IRQ_INPROGRESS标志已经被清除。我不知道其他像X86之类的体系是否有不同的行为,有知道的朋友请告知我一下。以下是handle_level_irq经过简化之后的代码:
[cpp]view plaincopy
1. void
2. handle_level_irq(unsigned int irq, struct irq_desc *desc)
3. {
4. raw_spin_lock(&desc->lock);
5. mask_ack_irq(desc);
6.
7. if(unlikely(irqd_irq_inprogress(&desc->irq_data)))
8. goto out_unlock;
9. ......
10.
11. if (unlikely(!desc->action ||irqd_irq_disabled(&desc->irq_data)))
12. goto out_unlock;
13.
14. handle_irq_event(desc);
15.
16. if(!irqd_irq_disabled(&desc->irq_data) && !(desc->istate &IRQS_ONESHOT))
17. unmask_irq(desc);
18. out_unlock:
19. raw_spin_unlock(&desc->lock);
20. }
虽然handle_level_irq对电平中断的流控进行了必要的处理,因为电平中断的特性:只要没有ack irq,中断线会一直有效,所以我们不会错过某次中断请求,但是驱动程序的开发人员如果对该过程理解不透彻,特别容易发生某次中断被多次处理的情况。特别是使用了中断线程(action->thread_fn)来响应中断的时候:通常mask_ack_irq只会清除中断控制器的pending状态,很多慢速设备(例如通过i2c或spi控制的设备)需要在中断线程中清除中断线的pending状态,但是未等到中断线程被调度执行的时候,handle_level_irq早就返回了,这时已经执行过unmask_irq,设备的中断线pending处于有效状态,中断控制器会再次发出中断请求,结果是设备的一次中断请求,产生了两次中断响应。要避免这种情况,最好的办法就是不要单独使用中断线程处理中断,而是要实现request_threaded_irq()的第二个参数irq_handler_t:handler,在handle回调中使用disable_irq()关闭该irq,然后在退出中断线程回调前再enable_irq()。假设action->handler没有屏蔽irq,以下这幅图展示了电平中断期间IRQ_PROGRESS标志、本地中断状态和触发其他CPU的状态:
图电平触发中断状态
上图中颜色分别代表不同的状态:
状态 |
红色 |
绿色 |
IRQ_PROGRESS |
TRUE |
FALSE |
是否允许本地cpu中断 |
禁止 |
允许 |
是否允许该设备再次触发中断(可能由其它cpu响应) |
禁止 |
允许 |
该函数用于处理边沿触发中断的流控操作。边沿触发中断的特点是,只有设备的中断请求引脚(中断线)的电平发生跳变时(由高变低或者有低变高),才会发出中断请求,因为跳变是一瞬间,而且不会像电平中断能保持住电平,所以处理不当就特别容易漏掉一次中断请求,为了避免这种情况,屏蔽中断的时间必须越短越好。内核的开发者们显然意识到这一点,在正是处理中断前,判断IRQ_PROGRESS标志没有被设置的情况下,只是ack irq,并没有mask irq,以便复位设备的中断请求引脚,在这之后的中断处理期间,另外的cpu可以再次响应同一个irq请求,如果IRQ_PROGRESS已经置位,表明另一个CPU正在处理该irq的上一次请求,这种情况下,他只是简单地设置IRQS_PENDING标志,然后mask_ack_irq后退出,中断请求交由原来的CPU继续处理。因为是mask_ack_irq,所以系统实际上只允许挂起一次中断。
[cpp]view plaincopy
1. if(unlikely(irqd_irq_disabled(&desc->irq_data) ||
2. irqd_irq_inprogress(&desc->irq_data)|| !desc->action)) {
3. if (!irq_check_poll(desc)) {
4. desc->istate |= IRQS_PENDING;
5. mask_ack_irq(desc);
6. goto out_unlock;
7. }
8. }
9.
10. desc->irq_data.chip->irq_ack(&desc->irq_data);
从上面的分析可以知道,处理中断期间,另一次请求可能由另一个cpu响应后挂起,所以在处理完本次请求后还要判断IRQS_PENDING标志,如果被置位,当前cpu要接着处理被另一个cpu“委托”的请求。内核在这里设置了一个循环来处理这种情况,直到IRQS_PENDING标志无效为止,而且因为另一个cpu在响应并挂起irq时,会mask irq,所以在循环中要再次unmask irq,以便另一个cpu可以再次响应并挂起irq:
[cpp]view plaincopy
1. do {
2. ......
3. if (unlikely(desc->istate &IRQS_PENDING)) {
4. if(!irqd_irq_disabled(&desc->irq_data) &&
5. irqd_irq_masked(&desc->irq_data))
6. unmask_irq(desc);
7. }
8.
9. handle_irq_event(desc);
10.
11. } while ((desc->istate & IRQS_PENDING)&&
12. !irqd_irq_disabled(&desc->irq_data));
图边沿触发中断状态
上图中颜色分别代表不同的状态:
状态 |
红色 |
绿色 |
IRQ_PROGRESS |
TRUE |
FALSE |
是否允许本地cpu中断 |
禁止 |
允许 |
是否允许该设备再次触发中断(可能由其它cpu响应) |
禁止 |
允许 |
是否处于中断上下文 |
处于中断上下文 |
处于进程上下文 |
由图4.1也可以看出,在处理软件中断(softirq)期间,此时仍然处于中断上下文中,但是cpu的本地中断是处于打开状态的,这表明此时嵌套中断允许发生,不过这不要紧,因为重要的处理已经完成,被嵌套的也只是软件中断部分而已。这个也就是内核区分top和bottom两个部分的初衷吧。
通过irq编号在irq_desc(也即所说的中断向量表)中查找该中断的中断处理函数,了解一下irq_desc的结构,如下:
action->handler就是驱动代码中注册的中断处理函数,执行完中断
(3)软中断
基于上面所说,软中断的执行既可以守护进程中执行,也可以在中断的退出阶段执行。实际上,软中断更多的是在中断的退出阶段执行(irq_exit),以便达到更快的响应,加入守护进程机制,只是担心一旦有大量的软中断等待执行,会使得内核过长地留在中断上下文中。
在irq_exit中执行
看看irq_exit的部分:
[cpp]view plaincopy
1. void irq_exit(void)
2. {
3. ......
4. sub_preempt_count(IRQ_EXIT_OFFSET);
5. if(!in_interrupt() && local_softirq_pending())
6. invoke_softirq();
7. ......
8. }
如果中断发生嵌套,in_interrupt()保证了只有在最外层的中断的irq_exit阶段,invoke_interrupt才会被调用,当然,local_softirq_pending也会实现判断当前cpu有无待决的软中断。代码最终会进入__do_softirq中,内核会保证调用__do_softirq时,本地cpu的中断处于关闭状态,进入__do_softirq:
[cpp]view plaincopy
1. asmlinkage void__do_softirq(void)
2. {
3. ......
4. pending = local_softirq_pending();
5.
6. __local_bh_disable((unsigned long)__builtin_return_address(0),
7. SOFTIRQ_OFFSET);
8. restart:
9. /* Reset the pendingbitmask before enabling irqs */
10. set_softirq_pending(0);
11.
12. local_irq_enable();
13.
14. h =softirq_vec;
15.
16. do {
17. if (pending & 1) {
18. ......
19. trace_softirq_entry(vec_nr);
20. h->action(h);
21. trace_softirq_exit(vec_nr);
22. ......
23. }
24. h++;
25. pending>>= 1;
26. } while (pending);
27.
28. local_irq_disable();
29.
30. pending =local_softirq_pending();
31. if (pending && --max_restart)
32. goto restart;
33.
34. if (pending)
35. wakeup_softirqd();
36.
37. lockdep_softirq_exit();
38.
39. __local_bh_enable(SOFTIRQ_OFFSET);
40. }
- 首先取出pending的状态;
- 禁止软中断,主要是为了防止和软中断守护进程发生竞争;
- 清除所有的软中断待决标志;
- 打开本地cpu中断;
- 循环执行待决软中断的回调函数;
-
如果循环完毕,发现新的软中断被触发,则重新启动循环,直到以下条件满足,才退出:
- 没有新的软中断等待执行;
- 循环已经达到最大的循环次数MAX_SOFTIRQ_RESTART,目前的设定值时10次;
- 如果经过MAX_SOFTIRQ_RESTART次循环后还未处理完,则激活守护进程,处理剩下的软中断;
- 推出前恢复软中断
(4)恢复被打断进程上下文
从中断工作模式退回到之前的工作模式时,需要由软件来完成以下工作:
1、将异常模式的R14减去一个适当的值(4或8)后赋给PC寄存器;
2、将异常模式SPSR的值赋给CPSR;
3、恢复进程上下文,被打断进程继续执行下面的工作
至此,linux中断子系统已介绍完毕
参考文献:
1. http://blog.csdn.net/DroidPhone/article/category/1118447
2. http://blog.chinaunix.net/uid-23602891-id-3515146.html