LV095-工作队列-中断线程化
中断线程化是实时 Linux 项目开发的一个新特性, 目的是降低中断处理对系统实时延迟的影响。
一、什么是中断线程化 ?
中断线程化是一种优化技术, 用于提高多线程程序的性能。 想象一下, 我们正在做一项任务, 但是总是被别人的打扰所中断, 每次都要停下手头的工作去处理别人的事情。 这样频繁的中断会让我们的工作效率变低, 因为我们需要反复切换任务, 无法专心做好自己的工作。
在多线程程序中, 也存在类似的问题。 有时硬件或其他事件会发出中断信号, 打断正在执行的线程, 需要切换到中断处理程序去处理这些事件。 这种频繁的中断切换会导致额外的开销和延迟, 影响程序的性能。
为了解决这个问题, 中断线程化提出了一种优化方案。 它将中断处理程序从主线程中独立出来, 创建一个专门的线程来处理这些中断事件。 这样, 主线程就不再受到中断的干扰, 可以专注于自己的工作, 不再频繁地被打断。
中断线程化的 核心思想 是将中断处理和主线程的工作分开, 让它们可以并行执行。 中断线程负责处理中断事件, 而主线程负责执行主要的工作任务。 这样一来, 不仅可以减少切换的开销, 还可以提高整个程序的响应速度和性能。
需要注意的是, 中断线程化还需要处理线程之间的同步和数据共享问题。 因为中断线程和主线程可能会同时访问和修改共享的数据, 所以需要合理地进行同步操作, 确保数据的一致性和正确性。
总而言之, 中断线程化是一种优化技术, 通过将中断处理和主线程的工作分开, 提高多线程程序的性能。 让主线程不再频繁被中断, 可以专注于自己的工作, 从而提高程序的效率和响应速度。
中断线程化的处理仍然可以看作是将原来的中断上半部分和中断下半部分。 上半部分还是用来处理紧急的事情, 下半部分也是出路比较耗时的操作, 但是下半部分会交给一个专门的内核线程来处理。 这个内核线程只用于这个中断。 当发生中断的时候, 会唤醒这个内核线程, 然后由这个内核线程来执行中断下半部分的函数。
二、相关的 api
1. request_threaded_irq()
request_threaded_irq() 是 Linux 内核中用于请求并注册一个线程化的中断处理函数的函数。 定义如下:
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)
{
//......
}函数参数:
- irq: 中断号, 表示要请求的中断线路。
- handler: 是在发生中断时首先要执行的处理程序, 非常类似于上半部, 该函数最后会返回 IRQ_WAKE_THREAD 来唤醒中断, 一般 handler 设为 NULL, 用系统提供的默认处理。
- thread_fn: 线程化的中断处理函数, 非常类似于下半部。 如果此处设置为 NULL 则表示没有使用中断线程化。
- irqflags: 中断标志, 用于指定中断的属性和行为。
- devname: 中断的名称, 用于标识中断请求的设备。
- dev_id: 设备标识符, 用于传递给中断处理函数的参数。
返回值:返回一个整数值, 表示中断请求的结果。 如果中断请求成功, 返回值为 0, 否则返回一个负数错误代码。
Tips:我们可以提供上半部函数,也可以不提供:
- 如果不提供:内核会提供默认的上半部处理函数:irq_default_primary_handler,它是直接返回 IRQ_WAKE_THREAD。
- 如果提供的话:返回值必须是: IRQ_WAKE_THREAD。在 thread_fn 中,如果中断被正确处理了,应该返回 IRQ_HANDLED。
2. free_irq()
const void *free_irq(unsigned int irq, void *dev_id)
{
//......
}
EXPORT_SYMBOL(free_irq);这个函数用于释放中断。
3. 简单示例
#include <linux/module.h>
#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/gpio.h>
#include <linux/delay.h>
#include <linux/workqueue.h>
int irq;
// 中断处理函数的底半部(线程化中断处理函数)
irqreturn_t test_work(int irq, void *args)
{
// 执行底半部的中断处理任务
msleep(1000);
printk("This is test_work\n");
return IRQ_RETVAL(IRQ_HANDLED);
}
// 中断处理函数的顶半部
irqreturn_t test_interrupt(int irq, void *args)
{
printk("This is test_interrupt\n");
// 将中断处理工作推迟到底半部
return IRQ_WAKE_THREAD;
}
static int interrupt_irq_init(void)
{
int ret;
irq = gpio_to_irq(18); // 将 GPIO 映射为中断号, imx6ull 的 GPIO1_IO18
printk("irq is %d\n", irq);
// 用于请求并注册一个线程化的中断处理函数
ret = request_threaded_irq(irq, test_interrupt, test_work, IRQF_TRIGGER_RISING, "test", NULL);
if (ret < 0)
{
printk("request_irq is error\n");
return -1;
}
return 0;
}
static void interrupt_irq_exit(void)
{
free_irq(irq, NULL); // 释放中断
printk("bye bye\n");
}
module_init(interrupt_irq_init);
module_exit(interrupt_irq_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("topeet");三、request_threaded_irq() 机制
前面我们学习了这个函数:

我们可以只提供 thread_fn,系统会为这个函数创建一个内核线程。发生中断时,系统会立刻调用 handler 函数,然后唤醒某个内核线程,内核线程再来执行 thread_fn 函数。 那,这是怎么过程呢?
1. 数据结构关系
调用 request_threaded_irq() 后内核的数据结构关系如下:

2. request_threaded_irq() 函数
我们来看一下这个函数:
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)
{
//......
// 分配、设置一个 irqaction 结构体
action = kzalloc(sizeof(struct irqaction), GFP_KERNEL);
if (!action)
return -ENOMEM;
action->handler = handler;
action->thread_fn = thread_fn;
action->flags = irqflags;
action->name = devname;
action->dev_id = dev_id;
retval = irq_chip_pm_get(&desc->irq_data);
if (retval < 0) {
kfree(action);
return retval;
}
// 进一步处理
retval = __setup_irq(irq, desc, action);
//......
}
EXPORT_SYMBOL(request_threaded_irq);这个函数中会先分配,设置一个 irqaction 结构体,后面会调用 __setup_irq() 函数做一些进一步处理。
2.1 __setup_irq()
我们来看一下__setup_irq(),这个函数超级长,上百行,我们主要看关键的部分:
static int
__setup_irq(unsigned int irq, struct irq_desc *desc, struct irqaction *new)
{
//......
/*
* Create a handler thread when a thread function is supplied
* and the interrupt does not nest into another interrupt
* thread.
*/
if (new->thread_fn && !nested) {
ret = setup_irq_thread(new, irq, false);
if (ret)
goto out_mput;
if (new->secondary) {
ret = setup_irq_thread(new->secondary, irq, true);
if (ret)
goto out_thread;
}
}
//......
}这里面我们主要关注 setup_irq_thread() 函数。
2.2 setup_irq_thread()
来看一下 setup_irq_thread() 函数:
static int
setup_irq_thread(struct irqaction *new, unsigned int irq, bool secondary)
{
struct task_struct *t;
struct sched_param param = {
.sched_priority = MAX_USER_RT_PRIO/2,
};
if (!secondary) {
t = kthread_create(irq_thread, new, "irq/%d-%s", irq,
new->name);
} else {
t = kthread_create(irq_thread, new, "irq/%d-s-%s", irq,
new->name);
param.sched_priority -= 1;
}
if (IS_ERR(t))
return PTR_ERR(t);
sched_setscheduler_nocheck(t, SCHED_FIFO, ¶m);
/*
* We keep the reference to the task struct even if
* the thread dies to avoid that the interrupt code
* references an already freed task_struct.
*/
get_task_struct(t);
new->thread = t;
/*
* Tell the thread to set its affinity. This is
* important for shared interrupt handlers as we do
* not invoke setup_affinity() for the secondary
* handlers as everything is already set up. Even for
* interrupts marked with IRQF_NO_BALANCE this is
* correct as we want the thread to move to the cpu(s)
* on which the requesting code placed the interrupt.
*/
set_bit(IRQTF_AFFINITY, &new->thread_flags);
return 0;
}可以看到是在这里创建了对应的线程。
3. 中断的执行过程
对于 GPIO 中断,这部分我没有去试,直接参考的伟东山的 linux 教程,教程中使用 QEMU 的调试功能找出了所涉及的函数调用,其他板子可能稍有不同。调用关系如下,反过来看:
Breakpoint 1, gpio_keys_gpio_isr (irq=200, dev_id=0x863e6930) at drivers/input/keybo
ard/gpio_keys.c:393
393 {
(gdb) bt
#0 gpio_keys_gpio_isr (irq = 200, dev_id = 0x863e6930) at drivers/input/keyboard/gpio_keys.c: 393
#1 0x80270528 in __handle_irq_event_percpu (desc = 0x8616e300, flags = 0x86517edc) at kernel/irq/handle.c: 145
#2 0x802705cc in handle_irq_event_percpu (desc = 0x8616e300) at kernel/irq/handle.c: 185
#3 0x80270640 in handle_irq_event (desc = 0x8616e300) at kernel/irq/handle.c: 202
#4 0x802738e8 in handle_level_irq (desc = 0x8616e300) at kernel/irq/chip.c: 518
#5 0x8026f7f8 in generic_handle_irq_desc (desc = <optimized out>) at ./include/linux/irqdesc.h: 150
#6 generic_handle_irq (irq = <optimized out>) at kernel/irq/irqdesc.c: 590
#7 0x805005e0 in mxc_gpio_irq_handler (port = 0xc8, irq_stat = 2252237104) at drivers/gpio/gpio-mxc.c: 274
#8 0x805006fc in mx3_gpio_irq_handler (desc = <optimized out>) at drivers/gpio/gpio-mxc.c: 291
#9 0x8026f7f8 in generic_handle_irq_desc (desc = <optimized out>) at ./include/linux/irqdesc.h: 150
#10 generic_handle_irq (irq = <optimized out>) at kernel/irq/irqdesc.c: 590
#11 0x8026fd0c in __handle_domain_irq (domain = 0x86006000, hwirq = 32, lookup = true, regs = 0x86517fb0) at kernel/irq/irqdesc.c: 627
#12 0x80201484 in handle_domain_irq (regs = <optimized out>, hwirq = <optimized out>, domain = <optimized out>) at ./include/linux/irqdesc.h: 168
#13 gic_handle_irq (regs = 0xc8) at drivers/irqchip/irq-gic.c: 364
#14 0x8020b704 in __irq_usr () at arch/arm/kernel/entry-armv.S: 464我们只需要分析__handle_irq_event_percpu():

线程的处理函数为 irq_thread():

四、中断线程化 demo
1. demo 源码
demo 源码可以看这里:13_interrupt/12_nodts_threaded_irq
2. 开发板测试
这个其实就和前面工作队列那些现象一样了:

但是我们可以看到有一条名为 irq/79-key-0 的线程被创建了:

加载驱动前后的线程情况:

线程的名字其实就是我们申请的中断的时候的名字组合出来的:

参考资料: