1. 适用范围
本文档适用于对中断底半部有了解需求的开发人员。
2. 原理概述
在发生中断时,内核会记录哪个中断产生并把中断相应的处理函数加入一个处理队列里, 直到把所有同一时间发生的中断全部记录好后,
处理队列里的函数才会逐一得到调用。所以当一个中断处理函数处理过久时,就有可能影响它后面的中断处函数的执行时机。我们用的中断处
理函数是可以被中断信号打断的。
内核在处理中断请求时,要求在单位时间内可以处理尽可能多的中断,也就是系统要求处理中断的吞吐率要尽可能地大。这就要求中断处
理程序要尽可能地短小精悍,并且不能有耗时操作。但是大多数的中断处理程序是很复杂的,很难在短时间内处理完毕。为了提高系统的响应
能力和并发能力,需要解决平衡中断处理程序时间要求短和工作量要大的问题,SylixOS 将中断处理分为两个阶段,也就是顶半部和底半部。
3. 准备工作
3.1 环境准备
1) SylixOS 操作系统;
2) RealEvo-IDE。
SylixOS 将中断处理分为两个阶段,也就是顶半部和底半部:
● 顶半部完成的一般是紧急的硬件操作,一般包括读取寄存器中的中断状态,清除中断标志,将底半部处理程序挂到底半部的执行队列中去;
● 底半部完成大部分的耗时操作,并且可以被新的中断打断。
也就是说把中断处理中实时性要求高的工作放在顶半部处理,实时性要求不高的工作放在底半部来处理。因底半部是在所有的顶半部全部处
理完后才会得到调用处理,所以这种处理方式会提高中断的响应速度。
如果中断处理是非常简单的工作,也可以不用分成顶半部和底半部的方式来处理。
4.2 底半部种类
顶半部完成尽可能少的比较紧急的功能,它往往只是简单地读取寄存器中的中断状态并清除中断标志后就进行“登记中断”的工作。“登记中断”
意味着将底半部处理程序挂到该设备的底半部执行队列中去。这样,顶半部执行的速度就会很快,可以服务更多的中断请求。
现在,中断处理工作的重心就落在了底半部的头上,它来完成中断事件的绝大多数任务。底半部几乎做了中断处理程序所有的事情,而且可以
被新的中断打断,这也是底半部和顶半部的最大不同,因为顶半部往往被设计成不可中断。底半部则相对来说并不是非常紧急的,而且相对比较耗
时,不在硬件中断服务程序中执行。
● 底半部可以被新的中断事件打断,这是和顶半部最大的不同,顶半部通常被设计成不可被打断。
● 底半部相对来说不是非常紧急的,而且相对比较耗时,不在硬件中断服务程序中执行。
● 如果中断要处理的工作本身很少,所有的工作可在顶半部全部完成。
尽管顶半部、底半部的结合能够改善系统的响应能力,但是,僵化地认为 SylixOS 设备驱动中的中断处理一定要分两个半部则是不对的。如果中
断要处理的工作本身很少,则完全可以直接在顶半部全部完成。
中断底半部的实现有三种:
● 软中断(softirq):基本的底半部机制,现在很少使用;
● 微线程(tasklet):微线程通过软中断机制来调度;
● 工作队列(workqueue):工作队列将工作交由一个内核线程处理。
4.2.1 软中断(softirq)
软中断(softirq)也是一种传统的底半部处理机制,它的执行时机通常是顶半部返回的时候,tasklet的基于软中断实现的,因此也运行于软中断上
下文。
软中断支持 SMP,同一个 softirq 可以在不同的 CPU 上同时运行,softirq 必须是可重入的。软中断是在编译期间静态分配的,它不像 tasklet 那样
能被动态的注册或去除。
软中断和 tasklet 都是运行在中断上下文中,它们与任一进程无关,没有支持的进程完成重新调度。所以软中断和 tasklet 不能睡眠、不能阻塞,它们
的代码中不能含有导致睡眠的动作,如减少信号量、从用户空间拷贝数据或手工分配内存等。也正是由于它们运行在中断上下文中,所以它们在同一个
CPU 上的执行是串行的,这样就不利于实时多媒体任务的优先处理。
软中断的特性:
1) 一个软中断不会抢占另外一个软中断;
2) 唯一可以抢占软中断的是中断处理程序;
3) 其他软中断(包括相同类型的)可以在其他的处理其上同时执行;
4) 一个注册的软中断必须在被标记后才能执行;
5) 软中断不可以自己休眠(即调用可阻塞的函数或 sleep 等);
6) 索引号小的软中断在索引号大的软中断之前执行。
4.2.2 软中断(tasklet)
他的执行上下文是软中断,执行时机通常是顶半部返回的时候。
tasklet 是一种特殊的软中断,同一时刻一个 tasklet 只能在一个 CPU 执行,不同的 tasklet 可以在不同的 CPU 上执行。这和软中断不同,软中断同一时刻
可以在不同的 CPU 并行执行,因此软中断必须考虑重入的问题。
引入 tasklet,最主要的是考虑支持 SMP,提高 SMP 多个 CPU 的利用率;两个相同的 tasklet 决不会同时执行。tasklet 可以理解为 softirq 的派生,所以它的
调度时机和软中断一样。对于内核中需要延迟执行的多数任务都可以用 tasklet 来完成,由于同类 tasklet 本身已经进行了同步保护,所以使用 tasklet 比软中断
要简单的多,而且效率也不错。tasklet 把任务延迟到安全时间执行的一种方式,在中断期间运行,即使被调度多次,tasklet 也只运行一次,不过 tasklet 可以
在 SMP 系统上和其他不同的 tasklet 并行运行。在 SMP 系统上,tasklet 还被确保在第一个调度它的 CPU 上运行,因为这样可以提供更好的高速缓存行为,从而
提高性能。
tasklet 的特性:
不允许两个两个相同类型的 tasklet 同时执行,即使在不同的处理器上。
4.2.3 工作队列(workqueue)
工作队列的执行上下文是内核线程,因此可以调度和睡眠。
如果推后执行的任务需要睡眠,那么就选择工作队列。另外,如果需要用一个可以重新调度的实体来执行你的下半部处理,也应该使用工作队列。它是唯
一能在进程上下文运行的下半部实现的机制,也只有它才可以睡眠。这意味着在需要获得大量的内存时、在需要获取信号量时,在需要执行阻塞式的 I/O 操作
时,它都会非常有用。
work queue 造成的开销最大,因为它要涉及到内核线程甚至是上下文切换。这并不是说 work queue 的低效,但每秒钟有数千次中断,就像网络子系统时常
经历的那样,那么采用其他的机制可能更合适一些。尽管如此,针对大部分情况工作队列都能提供足够的支持。
工作队列特性:
1) 工作队列会在进程上下文中执行;
2) 可以阻塞。(前两种机制是不可以阻塞的);
3) 可以被重新调度。(前两种只可以被中断处理程序打断);
4) 使用工作队列的两种形式:
a. 缺省工作者线程 (works threads);
b. 自建的工作者线程。
5) 在工作队列和内核其他部分之间使用锁机制就像在其他的进程上下文一样;
6) 默认允许响应中断。
SylixOS系统中使用的中断底半部处理机制是工作队列。
4.3.1 接口函数
● 创建工作队列 API_WorkQueueCreate
- 原型 : PVOID API_WorkQueueCreate (CPCHAR pcName,
- UINT uiQSize,
- BOOL bDelayEn,
- ULONG ulScanPeriod,
- PLW_CLASS_THREADATTR pthreadattr)
- 输入 : pcName 队列名称
- uiQSize 队列大小
- bDelayEn 是否创建带有延迟执行功能的工作队列
- ulScanPeriod 如果带有延迟选项, 此参数指定服务线程扫描周期.
- pthreadattr 队列服务线程选项
- 返回 : 工作队列句柄
- 说明 : 创建一个工作队列
- 注意 : 如果不需要延迟执行功能, 则系统自动创建为简单工作队列, 内存占用量小, 执行效率高.
● 删除一个工作队列 API_WorkQueueDelete
- 原型 : ULONG API_WorkQueueDelete (PVOID pvWQ)
- 输入 : pvWQ 工作队列句柄
- 返回 : ERROR_CODE
- 说明 : 删除一个工作队列
● 加入一个工作到工作队列 API_WorkQueueInsert
- 原型 :ULONG API_WorkQueueInsert (PVOID pvWQ,
- ULONG ulDelay,
- VOIDFUNCPTR pfunc,
- PVOID pvArg0,
- PVOID pvArg1,
- PVOID pvArg2,
- PVOID pvArg3,
- PVOID pvArg4,
- PVOID pvArg5)
- 输入 : pvWQ 工作队列句柄
- ulDelay 最小延迟执行时间
- pfunc 执行函数
- pvArg0 ~ 5 执行参数
- 返回 : ERROR_CODE
- 说明 : 将一个工作插入到工作队列
● 清空工作队列 API_WorkQueueFlush
- 原型 : ULONG API_WorkQueueFlush (PVOID pvWQ)
- 输入 : pvWQ 工作队列句柄
- 返回 : ERROR_CODE
- 说明 : 清空工作队列
● 获取工作队列状态 API_WorkQueueStatus
- 原型 : ULONG API_WorkQueueStatus (PVOID pvWQ, UINT *puiCount)
- 输入 : pvWQ 工作队列句柄
- puiCount 当前队列中作业数量
- 返回 : ERROR_CODE
- 说明 : 获取工作队列状态
4.3.2 中断底半部实例
以下为测试实例,内核模块在装载时创建了一个不带有延迟执行功能的 SylixOS 工作队列和一个线程,线程用于设置某个 GPIO 的中断处理函数并使能中断,
当产生对应的按键中断时,中断处理函数会清除中断,并将耗时操作插入到工作队列中,卸载内核模块时清除该 GPIO 的相关设置并删除工作队列。
- #define __SYLIXOS_STDIO
- #define __SYLIXOS_KERNEL
- #include <SylixOS.h>
- #include <module.h>
- #define KEY_NUM 36
- PVOID _G_pvWorkQueue;
- static INT _G_iIrqNum;
- static VOID __workHandler(VOID)
- {
- printk("work handler function start.\n");
- API_TimeSSleep(5);
- printk("work handler function stop.\n");
- }
- static irqreturn_t GpioIsr (INT iGpioNum, ULONG ulVector)
- {
- API_GpioClearIrq(iGpioNum);
- API_WorkQueueInsert(_G_pvWorkQueue,
- 0,
- __workHandler,
- LW_NULL,
- LW_NULL,
- LW_NULL,
- LW_NULL,
- LW_NULL,
- LW_NULL);
- return (ERROR_NONE);
- }
- static PVOID __keyThread (PVOID pvArg)
- {
- INT iError;
- iError = API_GpioRequestOne(KEY_NUM, LW_GPIOF_IN, "KEY");
- if (iError != ERROR_NONE) {
- printk("failed to request gpio %d!\n", KEY_NUM);
- return (NULL);
- }
- _G_iIrqNum = API_GpioSetupIrq(KEY_NUM, LW_FALSE, 0);
- if (_G_iIrqNum == PX_ERROR) {
- printk("failed to setup gpio %d irq!\n", KEY_NUM);
- return (NULL);
- }
- iError = API_InterVectorConnect((ULONG)_G_iIrqNum,
- (PINT_SVR_ROUTINE)GpioIsr,
- (PVOID)KEY_NUM,
- "GpioIsr");
- if (iError != ERROR_NONE) {
- printk("failed to connect GpioIsr!\n");
- return (NULL);
- }
- API_InterVectorEnable(_G_iIrqNum);
- return (NULL);
- }
- void module_init (void)
- {
- LW_CLASS_THREADATTR threadattr;
- printk("interrupt_module init!\n");
- API_ThreadAttrBuild(&threadattr,
- 4 * LW_CFG_KB_SIZE,
- LW_PRIO_NORMAL,
- LW_OPTION_THREAD_STK_CHK,
- LW_NULL);
- _G_pvWorkQueue = API_WorkQueueCreate("t_workqueue",
- 10,
- FALSE,
- 0,
- &threadattr);
- if (_G_pvWorkQueue == LW_NULL) {
- printk("WorkQueue create failed.\n");
- return;
- }
- API_ThreadCreate("t_key",
- (PTHREAD_START_ROUTINE)__keyThread,
- LW_NULL,
- LW_NULL);
- }
- void module_exit (void)
- {
- API_InterVectorDisconnect((ULONG)_G_iIrqNum,
- (PINT_SVR_ROUTINE)GpioIsr,
- (PVOID)KEY_NUM);
- API_GpioFree(KEY_NUM);
- API_WorkQueueDelete(_G_pvWorkQueue);
- printk("interrupt_module exit!\n");
- }