第10章 中断 时钟

中断是指CPU在执行程序的过程中,出现了某些突发事件急待处理,CPU必须暂停当前程序的执
行,转去处理突发事件,处理完毕后又返回原程序被中断的位置继续执行。

根据中断的来源,中断可分为内部中断和外部中断,内部中断的中断源来自CPU内部(软件中断指
令、溢出、除法错误等,例如,操作系统从用户态切换到内核态需借助CPU内部的软件中断),外部中断
的中断源来自CPU外部,由外设提出请求。
根据中断是否可以屏蔽,中断可分为可屏蔽中断与不可屏蔽中断(NMI),可屏蔽中断可以通过设置
中断控制器寄存器等方法被屏蔽,屏蔽后,该中断不再得到响应,而不可屏蔽中断不能被屏蔽。
根据中断入口跳转方法的不同,中断可分为向量中断和非向量中断。采用向量中断的CPU通常为不同
的中断分配不同的中断号,当检测到某中断号的中断到来后,就自动跳转到与该中断号对应的地址执行。
不同中断号的中断有不同的入口地址。非向量中断的多个中断共享一个入口地址,进入该入口地址后,再
通过软件判断中断标志来识别具体是哪个中断。也就是说,向量中断由硬件提供中断服务程序入口地址,
非向量中断由软件提供中断服务程序入口地址。

为了在中断执行时间尽量短和中断处理需完成的工作尽量
大之间找到一个平衡点,Linux将中断处理程序分解为两个半部:顶半部(Top Half)和底半部(Bottom
Half)。

顶半部用于完成尽量少的比较紧急的功能,它往往只是简单地读取寄存器中的中断状态,并在清除中
断标志后就进行“登记中断”的工作。“登记中断”意味着将底半部处理程序挂到该设备的底半部执行队列中
去。这样,顶半部执行的速度就会很快,从而可以服务更多的中断请求。
现在,中断处理工作的重心就落在了底半部的头上,需用它来完成中断事件的绝大多数任务。底半部
几乎做了中断处理程序所有的事情,而且可以被新的中断打断,这也是底半部和顶半部的最大不同,因为
顶半部往往被设计成不可中断。底半部相对来说并不是非常紧急的,而且相对比较耗时,不在硬件中断服
务程序中执行。
尽管顶半部、底半部的结合能够改善系统的响应能力,但是,僵化地认为Linux设备驱动中的中断处
理一定要分两个半部则是不对的。如果中断要处理的工作本身很少,则完全可以直接在顶半部全部完成。


在Linux中,查看/proc/interrupts文件可以获得系统中中断的统计信息,并能统计出每一个中断号上的

中断在每个CPU上发生的次数,


在Linux设备驱动中,使用中断的设备需要申请和释放对应的中断,并分别使用内核提供的
request_irq()和free_irq()函数。

int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
const char *name, void *dev);

irq是要申请的硬件中断号。
handler是向系统登记的中断处理函数(顶半部),是一个回调函数,中断发生时,系统调用这个函
数,dev参数将被传递给它。

irqflags是中断处理的属性,可以指定中断的触发方式以及处理方式。

request_irq()返回0表示成功,返回-EINVAL表示中断号无效或处理函数指针为NULL,返回-
EBUSY表示中断已经被占用且不能共享。

int devm_request_irq(struct device *dev, unsigned int irq, irq_handler_t handler,
unsigned long irqflags, const char *devname, void *dev_id);

此函数与request_irq()的区别是devm_开头的API申请的是内核“managed”的资源,一般不需要在出

错处理和remove()接口里再显式的释放。有点类似Java的垃圾回收机制。


下列3个函数用于屏蔽一个中断源:
void disable_irq(int irq);
void disable_irq_nosync(int irq);
void enable_irq(int irq);

disable_irq_nosync()与disable_irq()的区别在于前者立即返回,而后者等待目前的中断处理完
成。由于disable_irq()会等待指定的中断被处理完,因此如果在n号中断的顶半部调用disable_irq(n),
会引起系统的死锁,这种情况下,只能调用disable_irq_nosync(n)。

下列两个函数(或宏,具体实现依赖于CPU的体系结构)将屏蔽本CPU内的所有中断:
#define local_irq_save(flags) ...
void local_irq_disable(void);
前者会将目前的中断状态保留在flags中(注意flags为unsigned long类型,被直接传递,而不是通过指
针),后者直接禁止中断而不保存状态。
与上述两个禁止中断对应的恢复中断的函数(或宏)是:
#define local_irq_restore(flags) ...
void local_irq_enable(void);
以上各以local_开头的方法的作用范围是本CPU内。

底半部机制

Linux实现底半部的机制主要有tasklet、工作队列、软中断和线程化irq

tasklet的使用较简单,它的执行上下文是软中断,执行时机通常是顶半部返回的时候。我们只需要定

义tasklet及其处理函数,并将两者关联则可

void my_tasklet_func(unsigned long); /*定义一个处理函数*/

DECLARE_TASKLET(my_tasklet, my_tasklet_func, data);
/*定义一个tasklet结构my_tasklet,与my_tasklet_func(data)函数相关联*/

在需要调度tasklet的时候引用一个tasklet_schedule()函数就能使系统在适当的时候进行调度运行:
tasklet_schedule(&my_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);
工作队列的使用方法和tasklet非常相似,但是工作队列的执行上下文是内核线程,因此可以调度和睡
眠。下面的代码用于定义一个工作队列和一个底半部执行函数:
struct work_struct my_wq; /* 定义一个工作队列*/
void my_wq_func(struct work_struct *work); /* 定义一个处理函数*/

通过INIT_WORK()可以初始化这个工作队列并将工作队列与处理函数绑定:
INIT_WORK(&my_wq, my_wq_func);

与tasklet_schedule()对应的用于调度工作队列执行的函数为schedule_work(),如:
schedule_work(&my_wq); /* 调度工作队列执行*/


使用工作队列处理中断底半部的设备驱动程序模板

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
是基于软中断实现的,因此也运行于软中断上下文。
在Linux内核中,用softirq_action结构体表征一个软中断,这个结构体包含软中断处理函数指针和传递
给该函数的参数。使用open_softirq()函数可以注册软中断对应的处理函数,而raise_softirq()函数可以
触发一个软中断。
软中断和tasklet运行于软中断上下文,仍然属于原子上下文的一种,而工作队列则运行于进程上下
文。因此,在软中断和tasklet处理函数中不允许睡眠,而在工作队列处理函数中允许睡眠。
local_bh_disable()和local_bh_enable()是内核中用于禁止和使能软中断及tasklet底半部机制的函
数。
内核中采用softirq的地方包括HI_SOFTIRQ、TIMER_SOFTIRQ、NET_TX_SOFTIRQ、
NET_RX_SOFTIRQ、SCSI_SOFTIRQ、TASKLET_SOFTIRQ等,一般来说,驱动的编写者不会也不宜直
接使用softirq。

总结一下硬中断、软中断和信号的区别:硬中断
是外部设备对CPU的中断,软中断是中断底半部的一种处理机制,而信号则是由内核(或其他进程)对某
个进程的中断。



在内核中,除了可以通过request_irq()、devm_request_irq()申请中断以外,还可以通过
request_threaded_irq()和devm_request_threaded_irq()申请。这两个函数的原型为:
int request_threaded_irq(unsigned int irq, irq_handler_t handler,
irq_handler_t thread_fn,
unsigned long flags, const char *name, void *dev);
int devm_request_threaded_irq(struct device *dev, unsigned int irq,
irq_handler_t handler, irq_handler_t thread_fn,
unsigned long irqflags, const char *devname,
void *dev_id);
由此可见,它们比request_irq()、devm_request_irq()多了一个参数thread_fn。用这两个API申请中
断的时候,内核会为相应的中断号分配一个对应的内核线程。注意这个线程只针对这个中断号,如果其他
中断也通过request_threaded_irq()申请,自然会得到新的内核线程。

General Purpose Input Output (通用输入/输出)简称为GPIO

Linux的中断处理分为两个半部,顶半部处理紧急的硬件操作,底半部处理不紧急的耗时操作。tasklet
和工作队列都是调度中断底半部的良好机制,tasklet基于软中断实现。内核定时器也依靠软中断实现。
内核中的延时可以采用忙等待或睡眠等待,为了充分利用CPU资源,使系统有更好的吞吐性能,在对
延迟时间的要求并不是很精确的情况下,睡眠等待通常是值得推荐的,而ndelay()、udelay()忙等待
机制在驱动中通常是为了配合硬件上的短时延迟要求。


猜你喜欢

转载自blog.csdn.net/qq_28449863/article/details/80519043
今日推荐