中断
- 中断的来源一般可以分为两种,一种是外部中断,另一种是内部中断。对于arm处理器来说,内部的cpu产生的中断(溢出、除法错误等),外部中断就是cpu外部产生的中断。
- 中断也可以分为向量中断和非向量中断。采用向量中断的CPU通常为不同的中断分配不同的中断号,当检测到某中断号的中断到来后,就自动跳转到与该中断号对应的地址执行。不同中断号的中断有不同的入口地址。非向量中断的多个中断共享一个入口地址,进入该入口地址后,再通过软件判断中断标志来识别具体是哪个中断。
中断控制器
在arm多核处理器中最常用的中断处理器GIC(generic interrupt controller),如图所示:
SGI(Software Generated Interrupt):软件产生的中断,可以用于多核的核间通信,一个CPU可以通过写GIC的寄存器给另外一个CPU产生中断。多核调度用的IPI_WAKEUP、IPI_TIMER、IPI_RESCHEDULE、IPI_CALL_FUNC、IPI_CALL_FUNC_SINGLE、IPI_CPU_STOP、IPI_IRQ_WORK、IPI_COMPLETION都是由SGI产生的。
PPI(Private Peripheral Interrupt):某个CPU私有外设的中断,这类外设的中断只能发给绑定的那个CPU.
SPI(Shared Peripheral Interrupt):共享外设的中断,这类外设的中断可以路由到任何一个CPU。
中断处理程序架构
linux内核为了在中断执行时间尽量短和中断处理需完成的工作尽量大之间找到一个平衡点,Linux将中断处理程序分解为两个半部:顶半部(Top Half)和底半部(Bottom Half)。
- 其中上半部一般被设置为不可中断,下半部被设置为可中断。
- 在/proc/interrupts虚拟文件中统计了系统中断在哪个cpu执行的信息。
中断编程
申请中断
static inline int __must_check
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
const char *name, void *dev)
- irq是要申请的硬件中断号。
- handler是向系统登记的中断处理函数(顶半部),是一个回调函数,中断发生时,系统调用这个函数,dev参数将被传递给它。
- irqflags是中断处理的属性,可以指定中断的触发方式以及处理方式。
- request_irq()返回0表示成功。
释放中断
extern void free_irq(unsigned int, void *);
- free_irq()中参数的定义与request_irq()相同。
使能中断和关闭中断
extern void disable_irq_nosync(unsigned int irq);
extern void disable_irq(unsigned int irq);
extern void disable_percpu_irq(unsigned int irq);
extern void enable_irq(unsigned int irq);
extern void enable_percpu_irq(unsigned int irq, unsigned int type);
- disable_irq_nosync()与disable_irq()的区别在于前者立即返回,而后者等待目前的中断处理完成。由于disable_irq()会等待指定的中断被处理完,因此如果在n号中断的顶半部调用disable_irq(n),会引起系统的死锁,这种情况下,只能调用disable_irq_nosync(n)。
底半部机制
Linux实现底半部的机制主要有tasklet、工作队列、软中断和线程化irq。
tasklet
在中断下半部使用tasklet的时候,等到中断处理函数退出的时候,tasklet绑定的tasklet_schedule()函数会在系统适当的时候调度运行自己定义的tasklet函数。
tasklet使用模板(来源于宋宝华老师的设备驱动书籍内容):
1/* 定义 tasklet 和底半部函数并将它们关联 */
2void xxx_do_tasklet(unsigned long);
3DECLARE_TASKLET(xxx_tasklet, xxx_do_tasklet, 0);
4
5/* 中断处理底半部 */
6void xxx_do_tasklet(unsigned long)
7{
8
...
9}
10
11/* 中断处理顶半部 */
12irqreturn_t xxx_interrupt(int irq, void *dev_id)
13{
14 ...
15 tasklet_schedule(&xxx_tasklet);
16 ...
17}
18
19/* 设备驱动模块加载函数 */
20int __init xxx_init(void)
21{
22 ...
23 /* 申请中断 */
24 result = request_irq(xxx_irq, xxx_interrupt,
25
0, "xxx", NULL);
26 ...
27 return IRQ_HANDLED;
28}
29
30/* 设备驱动模块卸载函数 */
31void __exit xxx_exit(void)
32{
33 ...
34 /* 释放中断 */
35 free_irq(xxx_irq, xxx_interrupt);36 ...
37}
工作队列
工作队列的使用方法和tasklet非常相似,但是工作队列的执行上下文是内核线程,因此可以调度和睡眠。
使用模板(来源于宋宝华老师的设备驱动书籍内容):
1/* 定义工作队列和关联函数 */
2struct work_struct xxx_wq;
3void xxx_do_work(struct work_struct *work);
4
5/* 中断处理底半部 */
6void xxx_do_work(struct work_struct *work)
7{
8 ...
9}
10
11/* 中断处理顶半部 */
12irqreturn_t xxx_interrupt(int irq, void *dev_id)
13{
14 ...
15 schedule_work(&xxx_wq);
16 ...
17 return IRQ_HANDLED;
18}
19
20/* 设备驱动模块加载函数 */
21int xxx_init(void)
22{
23 ...
24 /* 申请中断 */
25 result = request_irq(xxx_irq, xxx_interrupt,
26
0, "xxx", NULL);
27 ...
28 /* 初始化工作队列 */
29 INIT_WORK(&xxx_wq, xxx_do_work);
30 ...31}
32
33/* 设备驱动模块卸载函数 */
34void xxx_exit(void)
35{
36 ...
37 /* 释放中断 */
38 free_irq(xxx_irq, xxx_interrupt);
39 ...
40}
软中断
- 软中断(Softirq)也是一种传统的底半部处理机制,它的执行时机通常是顶半部返回的时候,tasklet是基于软中断实现的,因此也运行于软中断上下文。
- 软中断和tasklet运行于软中断上下文,仍然属于原子上下文的一种,而工作队列则运行于进程上下文。因此,在软中断和tasklet处理函数中不允许睡眠,而在工作队列处理函数中允许睡眠。
- 总结一下硬中断、软中断和信号的区别:硬中断是外部设备对CPU的中断,软中断是中断底半部的一种处理机制,而信号则是由内核(或其他进程)对某个进程的中断。在涉及系统调用的场合,人们也常说通过软中断(例如ARM为swi)陷入内核,此时软中断的概念是指由软件指令引发的中断,和我们这个地方说的softirq是两个完全不同的概念,一个是software,一个是soft。
threaded_irq
在内核中,除了可以通过request_irq()、devm_request_irq()申请中断以外,还可以通过
request_threaded_irq()和devm_request_threaded_irq()申请。
中断共享
- 共享中断的多个设备在申请中断时,都应该使用IRQF_SHARED标志,而且一个设备以IRQF_SHARED申请某中断成功的前提是该中断未被申请,或该中断虽然被申请了,但是之前申请该中断的所有设备也都以IRQF_SHARED标志申请该中断。
- 尽管内核模块可访问的全局地址都可以作为request_irq(…,void*dev_id)的最后一个参数dev_id,但是设备结构体指针显然是可传入的最佳参数。
- 在中断到来时,会遍历执行共享此中断的所有中断处理程序,直到某一个函数返回
IRQ_HANDLED。在中断处理程序顶半部中,应根据硬件寄存器中的信息比照传入的dev_id参数迅速地判断是否为本设备的中断,若不是,应迅速返回IRQ_NONE。
共享中断编程模板(来源于宋宝华老师的设备驱动书籍内容):
/* 中断处理顶半部 */
2irqreturn_t xxx_interrupt(int irq, void *dev_id)
3{
4
...
5 int status = read_int_status();
/* 获知中断源 */
6 if(!is_myint(dev_id,status))
/* 判断是否为本设备中断 */
7
return IRQ_NONE;
/* 不是本设备中断,立即返回 */
8
9 /* 是本设备中断,进行处理 */
10
...
11
return IRQ_HANDLED;
/* 返回 IRQ_HANDLED 表明中断已被处理 */
12}
13
14/* 设备驱动模块加载函数 */
15int xxx_init(void)
16{
17
...
18
/* 申请共享中断 */
19
result = request_irq(sh_irq, xxx_interrupt,
20
IRQF_SHARED, "xxx", xxx_dev);
21 ...
22}
23
24/* 设备驱动模块卸载函数 */
25void xxx_exit(void)
26{27
28
29
30
31}
...
/* 释放中断 */
free_irq(xxx_irq, xxx_interrupt);
...
内核定时器
内核定时器编程
软件意义上的定时器最终依赖硬件定时器来实现,内核在时钟中断发生后检测各定时器是否到期,到期后的定时器处理函数将作为软中断在底半部执行。实质上,时钟中断处理程序会唤起TIMER_SOFTIRQ软中断,运行当前处理器上到期的所有定时器。
内核定时器使用模板(来源于宋宝华老师的设备驱动书籍内容):
1/* xxx 设备结构体 */
2struct xxx_dev {
3 struct cdev cdev;
4 ...
5 timer_list xxx_timer;
/* 设备要使用的定时器 */
6};
7
8/* xxx 驱动中的某函数 */
9xxx_func1( ... )
10{
11 struct xxx_dev *dev = filp->private_data;
12 ...
13 /* 初始化定时器 */
14 init_timer(&dev->xxx_timer);
15 dev->xxx_timer.function = &xxx_do_timer;
16 dev->xxx_timer.data = (unsigned long)dev;
17
/* 设备结构体指针作为定时器处理函数参数 */
18 dev->xxx_timer.expires = jiffies + delay;
19 /* 添加(注册)定时器 */
20 add_timer(&dev->xxx_timer);
21 ...
22}
23
24/* xxx 驱动中的某函数 */
25xxx_func2( ... )
26{
27 ...
28 /* 删除定时器 */
29 del_timer (&dev->xxx_timer);
30 ...
31}
32
33/* 定时器处理函数 */
34static void xxx_do_timer(unsigned long arg)
35{
36 struct xxx_device *dev = (struct xxx_device *)(arg);
37 ...
38 /* 调度定时器再执行 */
39 dev->xxx_timer.expires = jiffies + delay;
40 add_timer(&dev->xxx_timer);
41 ...
42}
内核延时
短延时
void ndelay(unsigned long nsecs);
void udelay(unsigned long usecs);
void mdelay(unsigned long msecs);
- 上述延迟的实现原理本质上是忙等待,它根据CPU频率进行一定次数的循环。
长延时
- 可以比较当前的jiffies和目标jiffies。
睡眠延时
- 睡着延迟无疑是比忙等待更好的方式,睡着延迟是在等待的时间到来之前进程处于睡眠状态,CPU资源被其他进程使用。
总结
- 中断底部的软中断、工作队列机制需要研读源码理解的。
- 内核中延时可以根据自己喜好来选择适合=项目的延时手段。