[toc]

include/linux/irq_work.h

__IRQ_WORK_INIT

1
2
3
4
5
6
7
#define __IRQ_WORK_INIT(_func, _flags) (struct irq_work){	\
.node = { .u_flags = (_flags), }, \
.func = (_func), \
.irqwait = __RCUWAIT_INITIALIZER(irqwait), \
}

#define IRQ_WORK_INIT(_func) __IRQ_WORK_INIT(_func, 0)

kernel/irq_work.c IRQ上下文延迟工作(IRQ Context Deferred Work) 在安全时机执行来自中断的紧急任务

历史与背景

这项技术是为了解决什么特定问题而诞生的?

kernel/irq_work.c 实现的irq_work机制是为了解决一个非常特殊且棘手的内核同步问题:如何从一个“硬中断(Hard IRQ)”上下文中,安全地执行一个需要“中断已打开”环境的、但又必须在当前CPU上尽快执行的任务

在标准的硬中断处理程序(Top-Half)中,执行环境是极其受限的:

  • 当前CPU上的中断通常是被屏蔽的。
  • 绝对不能睡眠。
  • 执行时间必须极短。

然而,有时硬中断处理程序需要执行一个操作,这个操作本身很简短,但它可能需要获取一个要求中断打开的锁(例如,某些spinlock_t的变体或需要与其他CPU通信的锁),或者需要执行一个依赖于中断使能状态的架构相关指令。

在这种情况下,标准的下半部机制(softirq, tasklet)并不完全适用,因为:

  • 它们虽然也在中断上下文执行,但其调度时机可能相对“较晚”,无法保证在当前中断返回到用户空间或空闲状态之前执行。
  • softirq可能会在其他CPU上执行,无法保证在当前CPU上完成任务。

irq_work的诞生就是为了填补这个空白:它提供了一种机制,能将一个工作项(work item)从硬中断上下文“延迟”到一个非常近的未来,在这个时间点,硬件中断已经重新打开,但我们仍然在当前CPU的中断上下文中,并且可以保证这个工作会在CPU做任何其他事情(如返回用户空间或进入idle)之前被执行。

它的发展经历了哪些重要的里程碑或版本迭代?

irq_work是一个相对现代的内核机制,它的出现本身就是一个重要的里程碑,标志着内核对SMP(对称多处理)系统中复杂竞态条件和性能优化的理解达到了新的深度。

  • 作为特定问题的解决方案被引入:它最初是为了解决一些特定的、与体系结构相关的同步问题而被引入的,例如ARM架构中的惰性TLB(Translation Lookaside Buffer)刷新管理。
  • 被核心子系统采用:由于其独特的执行时机保证,它很快被内核中其他对延迟和上下文敏感的子系统所采用,例如硬件性能计数器(perf events)、跟踪(tracing)等。

目前该技术的社区活跃度和主流应用情况如何?

irq_work是一个高度特化的、底层的内核工具,其代码非常稳定。它不是给普通设备驱动开发者使用的通用接口。它的用户主要是内核核心开发者和体系结构维护者。
其主流应用包括:

  • 体系结构相关的维护工作:如TLB管理、缓存维护等。
  • 性能分析与跟踪perfftrace等工具在处理来自中断上下文的事件时,会使用irq_work来安全地更新数据结构。
  • 虚拟化:在某些虚拟化场景中,用于处理来自Guest的、需要特殊上下文的请求。

核心原理与设计

它的核心工作原理是什么?

irq_work的核心原理是利用一个**自定义的、低优先级的处理器间中断(Inter-Processor Interrupt, IPI)**作为“延迟执行”的触发器。

  1. 数据结构:每个CPU都有一个私有的irq_work链表,用于存放待处理的工作项(struct irq_work)。

  2. 排队 (irq_work_queue)

    • 当一个硬中断处理程序需要调度一个irq_work时,它会调用irq_work_queue()
    • 这个函数非常快。它首先会尝试将工作项原子地添加到当前CPU的私有链表中。
    • 如果添加成功(即链表之前是空的),它会向当前CPU发送一个特设的IPI(IRQ_WORK_VECTOR)。
  3. IPI处理程序

    • 这个特设IPI的处理程序是irq_work机制的“魔力”所在。当CPU响应这个IPI时,它会执行irq_work_run()函数。
    • 关键点:与硬中断处理程序不同,这个IPI处理程序在执行时,硬件中断是重新打开的
    • irq_work_run()会遍历当前CPU的irq_work链表,并依次执行每个工作项注册的回调函数。
  4. 执行时机保证

    • 由于IPI是在硬中断处理程序即将结束时发送的,并且IPI本身也是一种中断,它通常会在CPU从原始中断返回、准备恢复执行被中断的任务(如用户进程或idle线程)之前被处理。
    • 这就提供了irq_work最独特的保证:工作将在当前CPU上,以中断打开的方式,在下一次调度发生之前被执行

它的主要优势体现在哪些方面?

  • 强执行时机保证:能够确保任务在极低的延迟内,在当前CPU上,在上下文切换之前被执行。这是其他下半部机制无法提供的。
  • 提供了更宽松的执行上下文:它将任务从“硬中断关闭”的环境转移到了“硬中断打开”的环境,允许执行更复杂的操作,特别是涉及需要中断使能的锁。
  • 低排队开销irq_work_queue()本身是一个非常轻量级的操作。

它存在哪些已知的劣势、局限性或在特定场景下的不适用性?

  • 绝对不能睡眠:尽管中断是打开的,但irq_work的回调函数仍然在中断上下文中执行,因此绝对不能调用任何可能导致睡眠的函数
  • 高度特化:它不是一个通用的延迟执行工具,其使用场景非常狭窄,误用可能会导致难以调试的系统问题。
  • Per-CPU特性irq_work总是被调度在发起它的那个CPU上,无法跨CPU调度。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?

irq_work是解决那些对执行时机、执行CPU和执行上下文有严格、苛刻要求的场景的首选。

  • 跨CPU的缓存/TLB一致性维护:当CPU A上的一个操作(如修改页表)需要CPU B也执行一个动作(如刷新TLB)时,CPU A可以向CPU B发送一个IPI。在CPU B的IPI处理程序中,直接刷新TLB可能因为锁等原因不安全,此时它就可以调度一个irq_work来在一个安全的环境中完成刷新。
  • 硬件性能事件处理:当一个perf事件(如CPU周期计数器溢出)在硬中断上下文中触发时,其处理程序可能需要更新一些由自旋锁保护的数据结构。使用irq_work可以确保这个更新在当前CPU上立即发生,同时又能安全地获取锁。

是否有不推荐使用该技术的场景?为什么?

  • 普通设备驱动:一个典型的设备驱动(如网络、存储、USB)几乎永远不需要使用irq_work。对于它们的延迟任务,tasklet(如果任务不睡眠)或workqueue(如果任务需要睡眠)是正确且安全的选择。
  • 任何可能睡眠的任务:如果延迟任务需要分配内存(GFP_KERNEL)、与用户空间交互或获取互斥锁/信号量,必须使用workqueue,因为它提供了一个可以安全睡眠的进程上下文。

对比分析

请将其 与 其他相似技术 进行详细对比。

irq_work填补了内核异步执行机制中的一个独特生态位。

特性 硬中断处理程序 (Top-Half) irq_work Tasklet / Softirq Workqueue
执行上下文 硬中断 (中断关闭) 中断 (中断打开) 中断 (中断打开) 进程
能否睡眠 绝对不能 绝对不能 绝对不能 可以
执行CPU 当前CPU 保证在当前CPU 通常在当前CPU (Tasklet);可在任意CPU (Softirq) 可在任意CPU
执行时机 立即 极快。保证在当前中断返回到调度点之前。 。在中断返回后等安全点执行,但无irq_work的强保证。 。由内核调度器决定,作为普通线程运行。
主要用途 紧急硬件交互。 需要中断打开的、紧急的、与CPU相关的维护任务。 高性能数据处理(Softirq),驱动延迟任务(Tasklet)。 任何可能睡眠或耗时长的延迟任务。
编程模型 注册中断处理函数。 irq_work_queue() tasklet_schedule() queue_work()
独特优势 硬件级最低延迟。 独特的时机+地点+上下文保证。 高吞吐量(Softirq),简单易用(Tasklet)。 可以睡眠

irq_work_single 执行单个irq_work项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
void irq_work_single(void *arg)
{
struct irq_work *work = arg;
int flags;

/*
* Clear the PENDING bit, after this point the @work can be re-used.
* The PENDING bit acts as a lock, and we own it, so we can clear it
* without atomic ops.
*/
flags = atomic_read(&work->node.a_flags);
flags &= ~IRQ_WORK_PENDING;
atomic_set(&work->node.a_flags, flags);

/*
* See irq_work_claim().
*/
smp_mb();

lockdep_irq_work_enter(flags);
work->func(work);
lockdep_irq_work_exit(flags);

/*
* Clear the BUSY bit, if set, and return to the free state if no-one
* else claimed it meanwhile.
*/
(void)atomic_cmpxchg(&work->node.a_flags, flags, flags & ~IRQ_WORK_BUSY);

if ((IS_ENABLED(CONFIG_PREEMPT_RT) && !irq_work_is_hard(work)) ||
!arch_irq_work_has_interrupt())
rcuwait_wake_up(&work->irqwait);
}

irq_work_run_list 遍历并执行链表中的所有irq_work项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static void irq_work_run_list(struct llist_head *list)
{
struct irq_work *work, *tmp;
struct llist_node *llnode;

/*
* On PREEMPT_RT IRQ-work which is not marked as HARD will be processed
* in a per-CPU thread in preemptible context. Only the items which are
* marked as IRQ_WORK_HARD_IRQ will be processed in hardirq context.
*/
BUG_ON(!irqs_disabled() && !IS_ENABLED(CONFIG_PREEMPT_RT));

if (llist_empty(list))
return;

llnode = llist_del_all(list);
llist_for_each_entry_safe(work, tmp, llnode, node.llist)
irq_work_single(work);
}

irq_work_tick 在每个tick上,检查并执行当前CPU上所有待处理的“IRQ work”

  • 它的核心作用是:在每个tick上,检查并执行当前CPU上所有待处理的“IRQ work”。
  • irq_work_tick的工作原理是一个周期性的轮询和分发流程:
    • 处理紧急工作 (raised_list):
    • 处理懒惰工作 (lazy_list):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/*
* 这个函数在每个CPU的周期性时钟tick中断中被调用,用于处理irq_work。
*/
void irq_work_tick(void)
{
/* raised: 指向当前CPU私有的、用于存放“被请求执行的”(通常是跨CPU)紧急irq_work的链表头。*/
struct llist_head *raised = this_cpu_ptr(&raised_list);

/*
* 检查raised_list是否不为空,并且当前硬件架构没有专门的、用于处理irq_work的
* 自触发中断机制(如IPI)。
*/
/* arch_irq_work_has_interrupt() = false */
if (!llist_empty(raised) && !arch_irq_work_has_interrupt())
/*
* 如果满足条件,说明我们必须依赖tick中断来轮询和处理这些紧急工作。
* 调用irq_work_run_list来遍历并执行raised_list上的所有工作项。
*/
irq_work_run_list(raised);

/* 检查内核是否不是PREEMPT_RT(实时)内核。*/
if (!IS_ENABLED(CONFIG_PREEMPT_RT))
/*
* 在非实时内核中,直接调用irq_work_run_list来处理当前CPU的
* “懒惰”irq_work链表(lazy_list)。
*/
irq_work_run_list(this_cpu_ptr(&lazy_list));
else
/*
* 在实时内核中,为了降低硬中断处理的延迟,不直接执行irq_work。
* 而是调用wake_irq_workd()来唤醒一个专门的irq_workd内核线程,
* 将处理工作委托给该线程。
*/
wake_irq_workd();
}