三个名词:
-
中断处理程序(interrupt handle)
-
中断服务例程(interrupt service routine,ISR)
-
设备驱动程序(driver)
一个中断处理程序,一般分为两部分,上部分(top half)和下(底)半部(bottom half)。
为什么要这么分呢?
当一个中断触发请求后,中断处理程序会被调用,内核为了防止re-entrance(重入)问题,会禁止所有处理器(all processors)中对应中断线(IRQ line)——相当于屏蔽中断。而且在中断上下文中,也就是进行中断处理程序(interrupt handle)时,内核会禁止当前CPU的调度(preemption)。
When writing the interrupt handler, you don’t have to worry about re-entrance, since the IRQ line serviced is disabled on all processors by the kernel in order to avoid recursive interrupts.
为了在中断处理期间尽量不要错过中断信号,就应该尽快完成中断处理。于是乎,内核就引入了下半部(halves),将大部分耗时工作推迟到下半部去,在合适的时机下半部会被执行。这样还有一个好处就是可以让中断处理程序不会长时间禁止抢占,让CPU可以及时调度其它中断处理程序,降低整个系统的延时。
上下部
不仅中断处理程序可以分为上下部,其它的程序也可以有这个设计思想。也就是说,把程序分割成两部分,第一部主要处理比较紧急的任务,将耗时的任务推迟到第二部分去执行。
第二部分一般采用内核的延迟任务机制(work-deferring mechanism)来实现,包括以下四种:
- Softirqs(interrupt context)
- Tasklets(interrupt context)
- Workqueues(process context)
- Threaded IRQs(process context)
前两个工作在中断上下文中,因此无法被抢占。而后面两个工作在进程上下文中,可以被抢占。
后面两个可以通过设置 CONFIG_PREEMPT或CONFIG_PREEMPT_VOLUNTARY参数改变抢占机制,不过会影响整个系统。
Softirqs
软中断是内核一种用于推迟任务执行的机制(deferring mechanism),一般用在如网络设备和块设备,这样需要快速及时进行处理的驱动程序上。
内核使用Ksoftirqds线程来负责对软中断的调度执行,当有大量软中断到来时,它们会被堆积在一个队列上面,由Ksoftirqds负责进行调度,每个CPU都会有一个Ksoftirqds线程,它们以ksoftirqd/n的方式命名(n为第n个CPU)。
软中断在平时不常用,一般都是使用Tasklets就足够了。
Tasklets
Tasklets是软中断的一种实现方式,内核中的软中断能支持32个,由一个struct softirq_action
结构体来描述每一个软中断,在kernel/softirq.c中定义了一个包含32个这样结构体的数组static struct softirq_action softirq_vec[NR_SOFTIRQS]
,而Tasklet占其中的两个,分别为HI_SOFTIRQ和TASKLET_SOFTIRQ,前者用于高优先级的Tasklets,后者用于正常的Tasklets。
内核使用struct tasklet_struct
结构体来描述一个tasklet。
struct tasklet_struct
{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);
unsigned long data;
};
正常情况下Tasklets是不可重入的(not re-entrant),除非是这个Tasklets本身可以被中断,然后重新调度自己。
“Code is called reentrant if it can be interrupted anywhere in the middle of its execution, and then be safely called again. ”
同一个Tasklets只能够运行在某个CPU上面,不能同时运行在多个CPU上,不同的Tasklets可以同时运行在不同的CPU上。
前面提到Tasklets的两种调度方式,分别对应于正常的和高优先级的。
void tasklet_schedule(struct tasklet_struct *t);
void tasklet_hi_schedule(struct tasklet_struct *t);
一般情况下,使用正常优先级的就够了,高优先级会比正常优先级先执行,频繁使用高优先级调度可能会造成整个系统的延迟。
High priority tasklets are always executed before normal ones. Abusive use of high priority tasks will increase system latency. Only use them for really quick stuff.
Workqueues
这是一种最常用的延迟任务机制(work-deferring mechanism),它工作在进程上下文中,如果需要在下半部进行休眠就可以考虑使用work queues来实现。
work queues是在内核线程(kernel threads)的基础上实现的,在内核中有两种work queues:
-
shared work queue:由内核中默认的一组全局线程进行处理,在每一个CPU上都有一个这样的线程(类似于软中断的ksoftirqd)。这些线程用events/n进行命名。
-
dedicated kernel thread:这种工作队列会创建自己的内核线程——dedicated kernel thread,根据创建方式不同,会产生不同数量的线程,这些线程会绑定到某个CPU上。
“run the work queue in a dedicated kernel thread. It means whenever your work queue handler needs to be executed, your kernel thread is woken up to handle it"
使用
create_workqueue()
进行创建时,会在每个CPU上创建一个内核线程,并绑定到对应CPU上。使用
create_singlethread_workqueue()
进行创建时,只会创建一个内核线程,绑定到当前CPU上。
Threaded IRQs
int request_threaded_irq(unsigned int irq, irq_handler_t handler,
irq_handler_t thread_fn,
unsigned long irqflags,
const char *devname, void *dev_id)
这个下半部会在一个专用内核线程(dedicated kernel thread)中执行 。
- irq_handler_t handler:这个代表的中断处理函数,和request_irq中的类似,负责上半部的中断处理,运行在中断上下文中。
- irq_handler_t thread_fn:这个代表下半部的处理函数,当上半部返回值为 IRQ_WAKE_THREAD 时,内核的线程会关联这个函数,然后在合适时机进行执行。
不同上面几种方式,使用Threaded IRQs时,我们不需要自己进行显示的调度,由内核自动帮我们调度。
可以通过使用IRQF_ONESHOT标志,一直屏蔽中断直到下半部分执行完后,再打开中断。
if for any reason you need the IRQ line not to be re-enabled after the top half, and to remain disabled until the threaded handler has been run, you should request the threaded IRQ with the flag IRQF_ONESHOT enabled (by just doing an OR operation).
中断处理程序流程
-
一般我们会在驱动程序的init函数中,进行中断的注册:
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev) /* 或者使用request_threaded_irq方式 */
-
当中断线
irq
上有信号到达时,会经过下面的流程最终调用我们在request_irq()
指明的中断处理函数handler。
-
我们在中断处理程序(函数)中快速完成上半部的处理,然后通过使用deferring mechanism机制,将耗时操作推迟到下半部去执行。
-
最终在下半部完成全部的中断处理。