首页 > 代码库 > Linux内核:关于中断你需要知道的
Linux内核:关于中断你需要知道的
1、中断处理程序与其他内核函数真正的区别在于,中断处理程序是被内核调用来相应中断的,而它们运行于中断上下文(原子上下文)中,在该上下文中执行的代码不可阻塞。中断就是由硬件打断操作系统。
2、异常与中断不同,它在产生时必须考虑与处理器时钟同步。异常被称为同步中断,例如:除0、缺页异常、陷入内核(trap)引起系统调用处理程序异常。
3、不同的设备对应的中断不同,而每个中断都通过一个唯一的数字(中断号)标识。
4、既想让中断处理程序运行得快,又想中断处理程序完成的工作量多,为了在这两个相悖的目标之间达到一种平衡,一般把中断处理分为两个部分。中断处理程序是上半部(top half):接收到一个中断,它就立刻开始执行,但只做有严格时限的工作,例如对接受的中断进行应答或者复位硬件,这些工作都是在中断被禁止的情况下完成的(上半部情况下,中断被禁止);另一部分是下半部(bottom half):能够被允许稍后完成的工作会推迟到下半部。
(1)为什么要用下半部?
中断处理程序执行的时候,当前中断号对应的中断在所有处理器上都会被屏蔽;更糟糕的是,如果处理器程序是IRQF_DISABLED类型,它执行的时候会把本地的所有中断都屏蔽。然而,缩短中断被屏蔽的时间对系统的响应能力和性能都至关重要,所以,我们应该尽力缩短中断处理程序的时间,办法就是把一些工作放到以后去做。关键是:在下半部运行的时候,允许响应所有中断。
(2)划分中断上半部和下半部的借鉴原则:
- 如果一个任务对时间非常敏感,将其放在中断处理程序中执行
- 如果一个任务和硬件相关,将其放在中断处理程序中执行。
- 如果一个任务要保证部被其他中断(特别是相同的中断)打断,将其放置在中断处理程序中执行。
- 其他所有任务,考虑放置在下半部执行
5、例子,网卡驱动程序,当网卡接收到来自网路的数据包时,需要通知内核数据包到了。中断处理程序(top half)立即开始执行:通知硬件,拷贝最新的网络数据到内存,然后读取网卡更多的数据包,这些工作非常紧迫,因为网卡上接受数据包的缓存大小固定;下半部:执行处理和操作数据包的其他工作。
6、Linux提供的实现下半部的机制:(上半部的实现机制只有一种:中断处理程序)
在Linux内核2.6中,内核提供了三种不同形式的下半部实现机制:软中断、tasklet和工作队列。这三种机制从2.3开始引入。软中断用的比较少,tasklet是下半部更常用的一种形式,但是,tasklet是基于软中断实现的。
(1)如果你想加入一个新的软中断,首先应该问问自己为什么用tasklet实现不了,目前只有两个子系统(网络和SCSI)直接使用软中断。软中断只有在那些执行频率很高和连续性很高的情况下才需要使用。如果不需要扩展到多个处理器,那么就使用tasklet吧。tasklet本质上也是软中断,只不过同一个处理器程序的多个实例不能再多个处理器上同时运行。
下半部何时调用?内核在执行完中断处理器程序以后,do_softirq()函数,于是软中断开始执行中断处理程序留给它去完成的剩余任务。大部分tasklet和软中断都是在中断处理程序中被设置成待处理状态,所以最近一个中断返回的时候就是执行do_softirq()的最佳时机。
(2)tasklet_action()和tasklet_hi_action()是tasklet处理的核心。
(3)ksoftirqd辅助线程:每个处理器都有一组辅助处理软中断(和tasklet)的内核线程,当内核中出现大量软中断的时候,这些内核进程就会辅助处理它们。当一个软中断正在执行时,可能会再次触发它自己,内核目前采取的方案是:不会立即处理重新触发的软中断,在大量软中断出现的时候,内核会唤醒一组内核线程(nice值是19)来处理这些负载。
for(;;) { if(!softirq_pending(cpu)) //如果没有软中断,则调用schedule() schedule(); set_current_state(TASK_RUNNING); while(softirq_pending(cpu)){ do_softirq(); if(need_resched()) //如有必要重新调度,每次迭代之后都会schedule()以便让更重要的进程得到处理机会 schedule(); } set_current_state(TASK_INTERRUPTIBLE); }
(4)工作队列(work queue)是另一种将工作任务推后的方式,与软中断和tasklet都不相同。工作队列把工作交给一个内核线程去执行——这个下半部总是在进程上下文中执行,最重要的,工作队列允许重新调度甚至睡眠。所以,如果推后执行的任务需要睡眠,那么就选择工作队列;如果推后的任务不需要睡眠,那么就选择软件中断或者tasklet。如果需要使用一个可以重新调度的实体来执行当前中断的下半部任务,就应该使用工作队列。工作队列是唯一能在进程上下文中运行的下半部实现机制,也只有它才可以睡眠(关键是看你的任务是否需要睡眠)。尽管工作队列的操作处理函数运行在进程上下文中,但是它不能访问用户空间,因为该内核线程在用户空间没有相关的内存映射。
注意:mmc驱动中用到了工作队列~
Notice:通常在发生系统调用时,内核会代表用户空间的进程运行,此时它才能访问用户空间,也只有在此时它才会映射用户空间的内存。
(5)下半部机制的选择
下半部 | 上下文 | 顺序执行保障 |
软中断 | 中断 | 没有 |
tasklet | 中断 | 同类型不能同时执行 |
工作队列 | 进程 | 没有(和进程上下文一样被调度) |
从易用性角度来看:工作队列 > tasklet > 软中断
总结:驱动程序的编写者,需要做两个选择。首先,你是不是需要一个可调度的实体来执行需要推后完成的工作——从根本上来说,你需要推后的工作任务有休眠的需要吗?要是有,那么工作队列就是唯一的选择;否则最好用tasklet。其次,如果必须专注于性能的提高,那么就考虑软中断吧~
(6)使用下半部机制时,即使是在一个单处理器的系统上,避免共享数据的访问也是至关重要的。禁止下半部的函数有local_bh_disable()和local_bh_enable(),这两个函数只能禁止和激活本地处理器的软中断和tasklet。因为工作队列是在进程上下文中运行的,不会涉及异步执行的问题,所以也就没必要禁止它们执行。
7、在驱动程序中,要申请中断(注册中断处理程序)
request_threaded_irq(unsigned int irq, irq_handler_t handler, irq_handler_t thread_fn, 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的参数,用于区分不同的设备。free_irq()函数调用的时候,dev的作用就体现出来了。
8、irq_handler_t的类型定义如下:typedef irqreturn_t (*irq_handler_t)(int,void*);
用typedef 定义了一个函数指针类型irq_handler_t,指向的函数原型返回类型为 irqreturn_t ;它接收的参数类型就是int 和void* 两个参数
9、request_threaded_irq()函数是可以睡眠的,因为request_threaded_irq()-->proc_mkdir()-->proc_create()-->kmalloc(),而kmalloc()是可以睡眠的。所以,不能在中断上下文中调用该函数。
10、先初始化硬件,然后再注册中断处理程序,以防止中断处理程序在设备初始化完成之前就开始执行。
11、free_irq():如果在给定的中断线上没有中断处理程序,则注销响应的处理程序,并禁用其中断线。
12、中断处理程序即使什么工作也不做,至少需要知道产生中断的设备,告诉它已经收到中断了;对于复杂的设备,可能还需要在中断处理器程序中发送和接收数据,以及执行一些扩充的工作。这些扩充的工作尽可能被推迟到下半部(bottom half)去完成。
13、中断线和中断号是两个相同的概念,irq
14、Linux中的中断处理程序是无需重入的,当一个给定的中断处理程序正在执行时,相应的中断号在所有处理器上都是被屏蔽掉;所以,同一个中断处理程序绝对不会被同时调用以处理嵌套中断。
15、进程上下文:一种内核所处的操作模式,此时内核代表进程执行——例如,执行系统调用或者运行内核线程。在进程上下文中,可以通过current宏关联当前进程。又因为进程是以进程上下文的形式连接到内核的,因此,进程上下文可以睡眠,也可以调用调度程序。
16、中断上下文:与进程没什么关系,与current宏也没有关系,所以中断上下文不可以睡眠,在中断上下文中不可以调用任何可能睡眠的函数。
17、中断处理程序打断了其他的代码的执行,所以中断上下文中的代码应该简洁、迅速,尽可能把工作从中断处理程序中分离出来,放在下半部执行。
18、中断处理程序栈的设置是一个内核配置项,如果有的话,是1页大小,即4KB
19、控制中断系统的原因归根结底是需要提供同步。禁止中断提供保护机制,防止来自其他中断程序的并发访问,也能够禁止内核抢占;锁提供保护机制,防止来自其他处理器的并发访问(SMP系统需要考虑)。
参考资料:
- Linux中断子系统
- C语言typedef的用法
- 《Linux内核设计与实现》
- Linux中断子系统:softIRQ,下半部机制
- Linux中的工作队列