中断-处理程序架构

简介: 中断-处理程序架构

前言

设备的中断会打断内核进程中的正常调度和运行, 系统对更高吞吐率的追求势必要求中断服务程序尽量短小精悍。 但是, 这个良好的愿望往往与现实并不吻合。 在大多数真实的系统中, 当中断到来时, 要完成的工作往往并不会是短小的, 它可能要进行较大量的耗时处理。


中断处理机制

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

顶半部用于完成尽量少的比较紧急的功能, 它往往只是简单地读取寄存器中的中断状态, 并在清除中断标志后就进行“登记中断”的工作。 “登记中断”意味着将底半部处理程序挂到该设备的底半部执行队列中去。 这样, 顶半部执行的速度就会很快, 从而可以服务更多的中断请求。

现在, 中断处理工作的重心就落在了底半部的头上, 需用它来完成中断事件的绝大多数任务。 底半部几乎做了中断处理程序所有的事情, 而且可以被新的中断打断, 这也是底半部和顶半部的最大不同, 因为顶半部往往被设计成不可中断。 底半部相对来说并不是非常紧急的, 而且相对比较耗时, 不在硬件中断服务程序中执行。

尽管顶半部、 底半部的结合能够改善系统的响应能力, 但是, 僵化地认为Linux设备驱动中的中断处理一定要分两个半部则是不对的。 如果中断要处理的工作本身很少, 则完全可以直接在顶半部全部完成。

底半部机制

tasklet

tasklet的使用较简单, 它的执行上下文是软中断, 执行时机通常是顶半部返回的时候。 我们只需要定义tasklet及其处理函数, 并将两者关联则可, 例如:

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

代码DECLARE_TASKLET(my_tasklet, my_tasklet_func, data) 实现了定义名称为my_tasklet的tasklet, 并将其与my_tasklet_func() 这个函数绑定, 而传入这个函数的参数为data。在需要调度tasklet的时候引用一个tasklet_schedule() 函数就能使系统在适当的时候进行调度运行:

tasklet_schedule(&my_tasklet);

使用tasklet作为底半部处理中断的设备驱动程序模板(仅包含与中断相关的部

分) 。

/* 定义tasklet和底半部函数并将它们关联 */
void xxx_do_tasklet(unsigned long);
DECLARE_TASKLET(xxx_tasklet, xxx_do_tasklet, 0);
/* 中断处理底半部 */
void xxx_do_tasklet(unsigned long)
{
 ...
}
/* 中断处理顶半部 */
irqreturn_t xxx_interrupt(int irq, void *dev_id)
{
 ...
 tasklet_schedule(&xxx_tasklet);
 ...
}
/* 设备驱动模块加载函数 */
int __init xxx_init(void)
{
 ...
 /* 申请中断 */
 result = request_irq(xxx_irq, xxx_interrupt, 0, "xxx", NULL);
 ...
 return IRQ_HANDLED;
}
/* 设备驱动模块卸载函数 */
void __exit xxx_exit(void)
{
 ...
 /* 释放中断 */
 free_irq(xxx_irq, xxx_interrupt);
 ...
}

上述程序在模块加载函数中申请中断, 并在模块卸载函数中释放它 。 对应于xxx_irq的中断处理程序被设置为xxx_interrupt() 函数, 在这个函数中,tasklet_schedule(&xxx_tasklet) 调度被定义的tasklet函数xxx_do_tasklet() 在适当的时候执行。

工作队列

工作队列的使用方法和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); /* 调度工作队列执行 */

使用工作队列处理中断底半部的设备驱动程序模板(仅包含与中断相关的部分) 。

/* 定义工作队列和关联函数 */
struct work_struct xxx_wq;
void xxx_do_work(struct work_struct *work);
/* 中断处理底半部 */
void xxx_do_work(struct work_struct *work)
{
 ...
}
/*中断处理顶半部*/
irqreturn_t xxx_interrupt(int irq, void *dev_id)
{
 ...
 schedule_work(&xxx_wq);
 ...
 return IRQ_HANDLED;
}
/* 设备驱动模块加载函数 */
int xxx_init(void)
{
 ...
 /* 申请中断 */
 result = request_irq(xxx_irq, xxx_interrupt, 0, "xxx", NULL);
 ...
 /* 初始化工作队列 */
 INIT_WORK(&xxx_wq, xxx_do_work);
 ...
}
/* 设备驱动模块卸载函数 */
void xxx_exit(void)
{
 ...
 /* 释放中断 */
 free_irq(xxx_irq, xxx_interrupt);
 ...
}

上述程序在设计驱动模块加载函数中增加了初始化工作队列的代码 。

工作队列早期的实现是在每个CPU核上创建一个worker内核线程, 所有在这个核上调度的工作都在该worker线程中执行, 其并发性显然差强人意。 在Linux 2.6.36以后, 转而实现了“Concurrency-managedworkqueues”, 简称cmwq, cmwq会自动维护工作队列的线程池以提高并发性, 同时保持了API的向后兼容。

软中断

软中断(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的中断, 软中断是中断底半部的一种处理机制, 而信号则是由内核(或其他进程) 对某个进程的中断。 在涉及系统调用的场合, 人们也常说通过软中断(例如ARM为swi) 陷入内核, 此时软中断的概念是指由软件指令引发的中断, 和我们这个地方说的softirq是两个完全不同的概念, 一个是software, 一个是soft。

需要特别说明的是, 软中断以及基于软中断的tasklet如果在某段时间内大量出现的话, 内核会把后续软中断放入ksoftirqd内核线程中执行。 总的来说, 中断优先级高于软中断, 软中断又高于任何一个线程。软中断适度线程化, 可以缓解高负载情况下系统的响应。

threaded_irq

在内核中, 除了可以通过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() 申请, 自然会得到新的内核线程。

参数handler对应的函数执行于中断上下文, thread_fn参数对应的函数则执行于内核线程。 如果handler结束的时候, 返回值是IRQ_WAKE_THREAD, 内核会调度对应线程执行thread_fn对应的函数。

request_threaded_irq() 和devm_request_threaded_irq() 支持在irqflags中设置IRQF_ONESHOT标记,这样内核会自动帮助我们在中断上下文中屏蔽对应的中断号, 而在内核调度thread_fn执行后, 重新使能该中断号。 对于我们无法在上半部清除中断的情况, IRQF_ONESHOT特别有用, 避免了中断服务程序一退出, 中断就洪泛的情况。

handler参数可以设置为NULL, 这种情况下, 内核会用默认的irq_default_primary_handler() 代替handler, 并会使用IRQF_ONESHOT标记。 irq_default_primary_handler() 定义为:

/*
* Default primary interrupt handler for threaded interrupts. Is
* assigned as primary handler when request_threaded_irq is called
* with handler == NULL. Useful for oneshot interrupts.
*/
static irqreturn_t irq_default_primary_handler(int irq, void *dev_id)
{
return IRQ_WAKE_THREAD;
}


目录
相关文章
|
21天前
|
运维 监控 数据管理
Apollo与微服务架构:构建可扩展的应用程序
Apollo与微服务架构:构建可扩展的应用程序
|
11月前
|
存储 开发者
国标GB28181协议客户端开发(二)程序架构和注册
国标GB28181协议客户端开发(二)程序架构和注册
456 0
|
7月前
|
存储 并行计算 编译器
【计算机架构】程序指令计数 | 功耗计算 | 电力功耗 | 安德尔定律(Amdahl‘s Law)
【计算机架构】程序指令计数 | 功耗计算 | 电力功耗 | 安德尔定律(Amdahl‘s Law)
66 1
|
7月前
|
存储 缓存 Linux
【看表情包学Linux】冯诺依曼架构 | 理解操作系统 | 基于 Pintos 实现新的用户级程序的系统调用
【看表情包学Linux】冯诺依曼架构 | 理解操作系统 | 基于 Pintos 实现新的用户级程序的系统调用
71 1
|
21天前
|
存储 设计模式 前端开发
请解释 Web 应用程序的 MVC(模型-视图-控制器)架构。
【2月更文挑战第26天】【2月更文挑战第89篇】请解释 Web 应用程序的 MVC(模型-视图-控制器)架构。
|
21天前
MFC应用程序对话框架构
MFC应用程序对话框架构
18 0
|
7月前
|
设计模式 存储 API
游戏服务器架构:网络服务器端程序线程划分
游戏服务器架构:网络服务器端程序线程划分
|
8月前
|
敏捷开发 测试技术
推三返一开发稳定版丨推三返一项目系统开发详细指南/方案需求/步骤逻辑/流程功能/案例设计/技术架构/源码程序
推三返一系统开发是一种软件开发模式,也被称为迭代增量开发模式。它是一种敏捷开发方法的一种,通过将整个开发过程分为多个迭代周期,每个周期都会增加新的功能和特性,并在每个迭代周期结束后进行测试、反馈和修改。推三返一系统开发的核心思想是“推进三步,反馈一步”。
|
12月前
|
移动开发 应用服务中间件 Linux
35.从入门到精通:Python CGI编程 什么是CGI 网页浏览 CGI架构图 Web服务器支持及配置 第一个CGI程序 HTTP头部
35.从入门到精通:Python CGI编程 什么是CGI 网页浏览 CGI架构图 Web服务器支持及配置 第一个CGI程序 HTTP头部
|
存储 安全 架构师
「无服务器架构」无服务器架构是应用程序的正确选择?需要考虑利弊。
「无服务器架构」无服务器架构是应用程序的正确选择?需要考虑利弊。