[ARM笔记]嵌入式Linux中断处理程序架构
2019-07-13 00:17发布
生成海报
[ARM笔记]嵌入式Linux中断处理程序架构
2011-03-05 14:12:06| 分类: ARM|字号 订阅
1 Linux中断处理程序的上半部与下半部机制
设备中断会打断内核中进程的正常调度和运行,系统对更高吞吐率的追求势必要求中断服务尽可能的短小精悍。但是,在大多数真实的系统中,当中断到来时,要完成的工作往往并不会是短小的,它可能要进行较大量的耗时处理。
在Linux内核中,为了在中断执行时间尽可能短和中断处理需完成大量工作之间找到一个平衡点,Linux将中断处理程序分为两个部分:上半部(top
half)和下半部(bottom half)。中断处理程序的上半部在接收到一个中断时就立即执行,但只做比较紧急的工作,这些工作都是在所有中断被禁止的情况下完成的,所以要快,否则其它的中断就得不到及时的处理。那些耗时又不紧急的工作被推迟到下半部去。中断处理程序的下半部分(如果有的话)几乎做了中断处理程序所有的事情。它们最大的不同是上半部分不可中断,而下半部分可中断。在理想的情况下,最好是中断处理程序上半部分将所有工作都交给下半部分执行,这样的话在中断处理程序上半部分中完成的工作就很少,也就能尽可能快地返回。但是,中断处理程序上半部分一定要完成一些工作,例如,通过操作硬件对中断的到达进行确认,还有一些从硬件拷贝数据等对时间比较敏感的工作。剩下的其他工作都可由下半部分执行。
对于上半部分和下半部分之间的划分没有严格的规则,靠驱动程序开发人员自己的编程习惯来划分,不过还是有一些习惯供参考:
? 如果该任务对时间比较敏感,将其放在上半部中执行。
? 如果该任务和硬件相关,一般放在上半部中执行。
? 如果该任务要保证不被其他中断打断,放在上半部中执行(因为这是系统关中断)。
? 其他不太紧急的任务, 一般考虑在下半部执行。
下半部分并不需要指明一个确切时间,只要把这些任务推迟一点,让它们在系统不太忙并且中断恢复后执行就可以了。通常下半部分在中断处理程序一返回就会马上运行。内核中实现下半部的手段不断演化,目前已经从最原始的BH(bottom
half)衍生出BH(在2.5中去除)、软中断(softirq在2.3引入)、tasklet(在2.3引入)、工作队列(work
queue在2.5引入)。稍后笔者将介绍后两种方式。
尽管上半部和下半部的结合能够改善系统的响应能力,但是,Linux设备驱动中的中断处理并不一定要分成两个半部。如果中断要处理的工作本身就很少,则完全可以直接在上半部全部完成。
2 Linux中断编程
Linux内核提供了一组接口用于操作机器上的中断状态。这些接口函数为我们提供了申请与释放系统中断、使能和屏蔽中断的功能,它们都是与体系结构相关的,可以在和中找到。
(1)申请中断
在Linux设备驱动中,使用中断的设备需要先申请对应的中断。在中声明的request_irq()函数实现中断注册接口:
int request_irq(unsigned int irq,
irqreturn_t (*handler)(int,
void *, struct pt_regs *),
unsigned long flags,
const char *dev_name,
void *dev_id);
? irq:请求的中断号
? handler:中断处理函数指针,中断发生时系统调用该函数。
? flags:一个与中断管理相关的选项,如果设置成SA_INTERRUPT表示一个"快速"中断处理, 快速中断被处理时屏蔽当前处理器上的所有中断。如果设置成SA_SHIRQ表示中断可以在设备间共享。
? dev_name:这个字串用在 /proc/interrupts来显示中断的拥有者。
? dev_id:用作共享中断的指针。如果中断没有被共享,
dev_id 可以设置为 NULL,或者指向设备的设备结构体。
request_irq()函数返回0表示成功,返回-INVAL表示中断号无效或处理函数指针为NULL,返回-EBUSY表示中断已经被占用且不能共享。
(2)释放中断
如果设备向系统申请了中断,那么在设备注销的时候必须释放该中断,使用free_irq()函数实现该功能。函数原型如下:
void free_irq(unsigned int irq, void *dev_id);
函数中的参数定义参照request_irq()函数。
(3)使能和屏蔽中断
? 屏蔽单个中断
有时一个驱动需要禁止一个特定中断。内核提供了以下2个函数来实现这个功能, 这三个函数都声明在文件中。原型如下:
void disable_irq(int irq);
void disable_irq_nosync(int irq);
disable_irq不仅禁止给定的中断, 还等待当前执行的中断处理程序结束。disable_irq_nosync 与disable_irq 不同,它立刻返回。因此,使用disable_irq_nosync 快一点,但是可能使你的设备有竞争情况。
? 使能单个中断
void enable_irq(int irq)函数重新使能被禁止的中断。
? 禁止所有中断
如果你需要禁止所有中断,在 2.6 内核,使用以下 2 个函数中的任意一个都可以关闭在当前处理器上所有中断。这两个函数在(其中包含有< linux/irqflags.h
>,其后者中声明)中声明,原型如下:
void local_irq_save(unsigned long flags);
void local_irq_disable(void);
local_irq_save函数禁止当前处理器上所有中断, 并保存当前状态到 flags;
local_irq_disable关闭本地中断而不保存状态。
? 使能所有中断
使用下面两个函数可以恢复中断,这两个函数分别与上面两个禁止中断的函数相对应。
void local_irq_restore(unsigned long flags);
void local_irq_enable(void);
(4)下半部机制的实现
前面说过,Linux实现下半部的机制主要有tasklet、工作队列和软中断等。下面对tasklet机制和工作队列机制在Linux系统中的实现作简单介绍。
? tasklet机制
tasklet可以理解为软件中断的派生,所以它的调度时机和软中断一样。对于内核中需要延迟执行的多数任务都可以用tasklet来完成,由于同类tasklet本身已经进行了同步保护,所以使用tasklet比软中断要简单的多,而且效率也不错。tasklet把任务延迟到安全时间执行的一种方式,在中断期间运行,即使被调度多次,tasklet也只运行一次。
软中断和tasklet都是运行在中断上下文中,它们与任一进程无关,没有支持的进程完成重新调度。所以软中断和tasklet不能睡眠、不能阻塞,它们的代码中不能含有导致睡眠的动作,如减少信号量、从用户空间拷贝数据或手工分配内存等。
tasklet的使用相当简单,我们只需要定义tasklet及其处理函数并将二者关联:
void my_tasklet_func(unsigned long);
DECLARE_TASKLET(my_tasklet,my_tasklet_func,data);
其中my_tasklet_func(unsigned long)定义了tasklet的处理函数;DECLARE_TASKLET
(my_tasklet,my_tasklet_func,data)实现了将名称为my_tasklet的tasklet与my_tasklet_func()函数相关联。然后,在需要调度tasklet的时候引用下面的的API就能使系统在适当的时候进行调度运行:
tasklet_schedule(&my_tasklet);
此外,Linux还提供了另外一些其它的控制tasklet调度与运行的API:
DECLARE_TASKLET_DISABLED(name,function,data); //与DECLARE_TASKLET类似,但等待tasklet被使能
tasklet_enable(struct tasklet_struct *); //使能
tasklet tasklet_disble(struct tasklet_struct *); //禁用
tasklet tasklet_init(struct tasklet_struct *,void (*func)(unsigned long),unsigned long); //类似DECLARE_TASKLET()
tasklet_kill(struct tasklet_struct *); // 清除指定tasklet的可调度位,即不允许调度该tasklet
? 工作队列机制
工作队列是Linux 2.6 内核中新增加的一种下半部机制。它与其它几种下半部分机制最大的区别就是它可以把工作推后,交由一个内核线程去执行。内核线程只在内核空间运行,没有自己的用户空间,它和普通进程一样可以被调度,也可以被抢占。该工作队列总是会在进程上下文执行。这样,通过工作队列执行的代码能占尽进程上下文的所有优势,最重要的就是工作队列允许重新调度甚至是睡眠。因此,如果推后执行的任务需要睡眠,那么就选择工作队列;如果推后执行的任务不需要睡眠,那么就选择tasklet。另外,如果需要获得大量的内存、需要获取信号量或者需要执行阻塞式的I/O操作时,使用工作队列的方式将非常有用。
工作队列的使用方法和tasklet非常相似,下面的代码用于定义一个工作队列和一个底半部执行函数:
struct work_struct my_wq; //定义一个工作队列
void my_wq_func(unsigned long); //定义一个处理函数
通过INIT_WORK()可以初始化该工作队列并将工作队列与处理函数绑定,如下所示:
INIT_WORK(&my_wq, (void(*) (void *))my_wq_func, NULL);
与tasklet_schedule()对应的用于调度工作队列执行的函数为schedule_work(),如下:
schedule_work(&my_wq); //调度工作队列执行
打开微信“扫一扫”,打开网页后点击屏幕右上角分享按钮