学习本文能够对linux中断有全面而深刻的认识。本文对Linux中断所涉及的需求、管理机制、中断实现、中断接口(上半部和下半部)、驱动使用进行全然分析。
一、中断需求
软件是需求驱动的,软件的出现是为了解决需求和问题的。先知道需求,那理解代码就是为了验证已知的问题;不知道需求。那理解代码就是揣摩设计者的目的。
两者相比,前者自然效率跟高。
中断是为了解决异步的需求。紧急的事情或者更高优先级的事情须要先做,就代表异步。
比如。手机须要时刻优先响应用户按键或者触摸这个事件。否则用户体验就变差。信号是软件上的异步机制,而中断则是硬件上的异步机制。对于中断控制器(SOC)的设计原理请參考《软件和硬件都是对生活的高度抽象---论中断控制(ARM体系编程)》一文。
二、Linux中断需求
上述中断需求对全部实时设备均适用。无论是高级处理器(如ARM体系手机)还是低端控制器(如51体系)。可是对于不同的体系和不同的多任务环境上,中断的需求也要进一步细分。
如不同的CPU体系在内存上的表现也会不同的,高级处理器所配置的内存比較大。M级以上,而低端处理器配置的内存比較小。都是K级别。内存的大小也决定了软件代码量和复杂度。Linux操作系统适用于高级处理器和大内存,可以处理大量多任务并发运行。
在多进程多线程环境下,中断的需求也变得复杂,因此Linux的中断处理也相对复杂。我们一一来分析Linux中断的需求来自哪里,以及Linux是怎样应对的。
我们设想有下面场景:如果是90年代初有某个公司就仅仅有一台电话机(这个场景过时了,一时没想到更好的J),公司仅仅有总经理A和主管B和员工C三个人。我们非常easy想到,三者做的事情的重要性是总经理到主管再到员工。
在使用电话机对外交流的时候,应该优先总经理,再到主管,最后才是员工。好,三个人常态时是在埋头工作,需求电话沟通代表了异步紧急的事情。我们逐一分析遇到的问题,并考虑怎么应对解决:
1.先是主管B遇到紧急事情须要打断手头的工作去电话。而总经理A在B打电话期间遇到紧急的事情也要打电话。假设主管B一直拿着电话煲粥,那总经理肯定非常不爽,由于总经理的事情紧急又更重要。所以合理的处理方式是,总经理有权利抢占电话来先处理紧急的事情。
这就是在软件上要同意中断嵌套,让更高优先级的中断可以得到的及时的响应。
2.主管B在打电话的时候,假设员工C遇到紧急的事情也要打电话。他自然是不能抢来打的。
这样就引发一个这种问题,假设B一直占着电话不放,这样合理吗?主管B跟客户肯定是先把紧急的事情(设为B1)说完了。然后为了维护客户关系,就跟客户吹吹水什么的(设为B2)。员工C要打电话处理紧急的事情应该要比B2更加重要吧。假设C和对方沟通完紧急的事情(设为C1)后,也跟客户吹水(设为C2)。那从公平的角度来讲,C1要比B2重要。怎么处理这种情况呢?Linux中断使用上半部和下半部机制。
即将每一个中断服务分成两个阶段。上半部处理硬件相关和很紧急的操作(如读硬件数据)。而下半部处理没有那么紧急的操作(将硬件数据放到队列等);并且规定。低优先级中断的上半部能够抢占高优先级中断的下半部。即主管B的紧急处理分为B1和B2两个部分,员工C的紧急处理分为C1和C2两个部分。C1能够抢占B2进行。中断上半部和下半部机制是为了让低优先级中断也得到运行的机会。
3. 假设C在一段时间须要跟非常多个客户紧急沟通,然后跟每一个客户都要吹吹水。即由C发起非常多的C1和C2。那主管B想跟C说个事交代一下都找不到机会啊,假设是周一早上例会,C这么干,那这个例会肯定开不成了。例会是主管正常的工作流程(非紧急。假设紧急就是B要打电话了),那B应该等待C和客户交流紧急的事情即C1,也能够容忍C跟一两个客户吹水即C2,但不能够一直等C跟每一个客户都吹水的,C怎么都要歇一会跟主管碰碰面吧,等主管B 布置好任务再继续吹水也不迟。Linux中断怎么处理这样的情况?Linux中断在处理全然部中断的上半部后会继续处理一部分下半部,可是每次规定最多处理N个下半部。假设还剩下没有处理的下半部,那就交给一个叫做ksoftirqd的后台控制线程。然后退出中断并进行任务调度,这样高优先级的进程就行得到运行的机会。Ksoftirqd也能通过正常的调度得到运行的机会,将剩下的下半部运行完。Ksoftirqd是一个有较高优先级的内核线程。它可以比較快得到运行机会,因此整个中断的服务也能较快完毕。当然。假设Ksoftirqd可以有运行的机会,也代表中断产生非常频繁。可以考虑从系统层面进行优化。
Linux常规的下半部实现机制有两种。一种是tasklet,其即由上述Ksoftirqd运行;还有一种是工作队列workqueue,其由内核线程eventX来运行。两者的差别在于,tasklet相应的下半部不能够睡眠,而workqueue相应的下半部不能够睡眠。
另外,Linux从2.6開始也支持中断线程化,事实上它也是中断下半部的一种实现方式,即创建中断线程来运行下半部,而中断线程是有优先级,因此更高优先级的任务进程就能够得到运行的机会。
本文仅仅对tasklet下半部机制进行描写叙述。Workqueue和中断线程化不做讨论。
4. 总结:
1)运行顺序:高优先级中断的上半部->低优先级的上半部->下半部(高低优先级中断)->高优先级进程->低优先级进程。当中,下半部过多时会转移到ksoftirqd内核线程运行。
2)中断嵌套是为了让高优先级的中断得到及时响应;中断上下半部机制是为了低优先级的中断得到及时响应;ksoftirqd内核线程的调度是为了让高优先级的任务进程得到运行的机会。
三、中断管理机制
下面基于Linux2.6内核和ARM(s5pv210)体系进行分析,当中ARM(s5pv210)体系中断的硬件编程请參考《软件和硬件都是对生活的高度抽象---论中断控制(ARM体系编程)》一文。本文側重描写叙述Linux中断的管理。
可是我们注意一点,就是基于ARM体系的SOC中断控制器一般都会有几个二级中断源合并(或电路)为一条一级中断线。然后全部的一级中断线合并(或电路)之后再接入CPU的irq信号线。
s5pv210由VIC0、VIC1、VIC2、VIC3相应的四组寄存器来管理一级中断源,比如。UART0中断相应VIC1寄存器组的第10个bit;可是UART0有接收中断。也有发送中断,它们属于二级中断源。
1. 数据结构
--include/linux/irq.h
struct irq_desc是中断管理的核心数据结构,由它来连接硬件中断控制器和相应中断的服务函数。
struct irq_desc {
//中断号,指的是一级中断号,如UART0即32+10=42.
unsigned int irq;
//相应中断控制器操作,如mask,enable操作等
struct irq_chip *chip;
//一级公共中断服务函数。如接收和发送中断都有共同的清pending的操作。
irq_flow_handler_t handle_irq;
//详细的二级中断服务函数连成链表
structirqaction *action;
…
}
--kernel/irq/handle.c
//定义irq_desc全局数组,元素就是128个。
structirq_desc irq_desc[NR_IRQS];
--include/linux/irq.h
structirq_chip代表中断控制器的硬件控制。
为什么要将硬件的控制操作作为函数指针然后连接到irq_desc,而不是直接在中断服务中进行?这是由于中断服务属于驱动的范畴,假设驱动直接对硬件寄存器编程。那就会导致驱动的可移植性变差。应该将平台硬件相关的操作进行分离,在linux启动的时候做好初始化,并注冊到系统中,那驱动须要进行控制时就能够通过获取到irq_chip数据结构。并调用里面的函数来实现控制。这样讲驱动移植到不同的平台,仅仅须要改动BSP板级初始化的过程,而不须要改动驱动。这与将外围设备连接的GPIO作为资源并附属到设备数据结构。而驱动通过资源接口来获取GPIO是一样的道理。Linux是尽可能地考虑高度可移植性的系统,绝对是面向对象设计编程的典范。
structirq_chip {
const char *name;
void (*enable)(unsignedint irq);
void (*disable)(unsignedint irq);
void (*mask)(unsignedint irq);
void (*unmask)(unsignedint irq);
int (*set_wake)(unsigned int irq, unsignedint on);
….
}
--include/linux/irq.h
typedef void (*irq_flow_handler_t)(unsigned int irq, structirq_desc *desc);
irq_flow_handler_t handle_irq代表一级中断公共服务操作。
--include/linux/interrupt.h
structirqaction {
//typedef irqreturn_t(*irq_handler_t)(int, void *);二级中断服务接口
irq_handler_t handler;
//触发中断方式,如边沿或者电平触发
unsigned long flags;
const char *name;
//通常是中断设备的数据结构
void *dev_id;
//二级中断链接点
struct irqaction *next;
//中断号
int irq;
//proc映象路径
struct proc_dir_entry *dir;
//中断线程化,我们不讨论这点。设为NULL。
irq_handler_t thread_fn;
struct task_struct *thread;
unsigned long thread_flags;
};
2. Irq_desc的初始化
struct irq_descirq_desc[NR_IRQS] = {
[0 ... NR_IRQS-1] = {
.status = IRQ_DISABLED,//初始状态为disable
.chip = &no_irq_chip, //未相应实际硬件
.handle_irq = handle_bad_irq,
…
}
};
Linux启动的第二部分会调用:
Start_kernel
1)early_irq_init
---desc = irq_desc;
---count= ARRAY_SIZE(irq_desc);
---for(i = 0; i < count; i++) {
---desc[i].irq= i;//中断号设置
----------…
---}
arch_early_irq_init()
2)init_IRQ
-----for (irq = 0; irq < NR_IRQS; irq++)
----------irq_desc[irq].status |= IRQ_NOREQUEST | IRQ_NOPROBE;
-----init_arch_irq()
//即s5pv210_init_irq—arch/arm/mach-s5pv210/smdk210.c
----------s5p_init_irq(vic,ARRAY_SIZE(vic));
---------------vic_init(VA_VIC(irq),VIC_BASE(irq), vic[irq], 0);
--------------------vic_disable(base);//默认不使能中断
--------------------vic_set_irq_sources
-------------------------//初始化各个irq_desc的irq_chip
-------------------------set_irq_chip(irq,&vic_chip);
-------------------------//默认一级中断和相关寄存器。
-------------------------set_irq_handler(irq,handle_level_irq);
3. 各个irq_desc相应硬件irq_chip的初始化
set_irq_chip(irq,&vic_chip);当中vic_chip在\arch\arm\common\vic.c
static struct irq_chipvic_chip = {
.name ="VIC",
.ack =vic_ack_irq,
.mask =vic_mask_irq,//屏蔽中断
.unmask =vic_unmask_irq,
.retrigger = vic_retrigger_irq,
.set_wake =vic_set_wake,//唤醒
};
Irq_chip中的接口均是以中断号为參数,这样能够找到相应的VIC寄存器,实现屏蔽等功能。
4. 总结申请中断之前的中断相关数据结构
中断注冊前,即在linux启动之后,irq_desc数组的每一个元素(即一级中断)部分数据结构和相应的硬件控制寄存器irq_chip均已经初始化。可是irq_desc的action,即详细中断服务是空的。并且状态是disable的。
5. 中断申请注冊接口
int request_irq(unsigned int irq,irq_handler_t handler, unsigned long flags,
const char *name, void *dev);
1) irq是一级中断号,可是我们不应该依照SOC的SPEC直接填入一级中断号。而应该依照
arch\arm\plat-s5p\include\plat\irqs.h中通过宏定义设置的中断号。它通过一级级的宏展开,终于也能够得到一级中断号,可是通过宏定义的中断号。编程更加明了。如:
#defineIRQ_S5P_UART_RX0 (IRQ_S5P_UART_BASE0 +UART_IRQ_RXD)。
用IRQ_S5P_UART_RX0即代表UART0的接收中断。
2)handler中断处理函数
3)flags触发方式
4)name名称,会出如今/proc/interrupt/文件夹中。
5)dev一般填入中断相应设备的数据结构。
6. 中断注冊之后的映象
注冊后也能在/proc/interrupts/文件夹中看到以request_irq的參数name为名称的文件夹。该文件夹有几个属性文件,记录中断发生的次数等等。
能够看出,irq_desc是连接硬件相关的chip和相应的软件处理action。chip的初始化在linux启动过程即完毕初始化,而action在request_irq接口中完毕填充。
四、中断处理
中断是异常的一种,异常还包含缺页、syscall等等。对于一般低端单片机来说,中断向量表是异步信号发生时的直接入口,而对于cortex A8高级CPU来说。异常向量表是异步信号发生时的直接入口。假如发生irq中断。那么PC会转到异常向量表中的irq偏移处取出指令并运行,即该处是irq中断的总入口。而S5PV210是基于cortex A8核心,中断控制器是SOC级别控制模块。是三星进行系统设计的,而cortex A8是ARM公司提供。中断控制器管理最多不超过128个一级中断源,其相同相应一份中断向量。
详细选择哪个向量地址来运行,是由irq中断总入口依据当前的irq pending位进行分发调用。
异常向量地址由异常向量基址和偏移组成。如irq中断偏移0x18. 当发生异常时。cortex A8的PC跳转到异常向量地址的基址是能够配置的。其由协处理器CP15来设置。
默认的基址是0x0000 0000。而linux启动的过程中设置为0xffff 0000。
Linux中断下面内容学习请关注博主的微信公众号---嵌入式企鹅圈,博主为上市公司资深嵌入式架构师和高级培训师,通过嵌入式企鹅圈公众号分享嵌入式和Linux相关的原创技术文章。敬请关注。谢谢!
1.异常向量表的初始化
2.中断服务和分发
五、中断下半部tasklet
以上描写叙述的中断处理均是指中断的上半部。下半部由上半部创建和调度。
1. 接口
2. 中断调度
3. Ksoftirq运行
六、典型的Linux中断编程设计
七、总结