[toc]

kernel/softirq.c 内核中断下半部(Interrupt Bottom-Half) 核心实现

历史与背景

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

kernel/softirq.c 实现的软中断(softirq)机制是为了解决一个操作系统内核设计中的核心矛盾:中断处理程序的执行时间必须极短,但中断所触发的工作任务可能很耗时

  • 中断处理的紧迫性:硬件中断发生时,CPU会立即暂停当前工作,跳转到中断处理程序(Interrupt Service Routine, ISR,也称“上半部”/“Top Half”)。在执行ISR期间,通常会屏蔽掉当前CPU上的同级甚至所有中断,以保证处理的原子性和快速性。如果ISR执行时间过长,会导致系统无法响应其他新的硬件中断,增加系统延迟(Latency),甚至丢失中断事件。
  • 任务的复杂性:然而,中断事件所触发的后续处理可能很复杂。例如,网卡收到一个数据包的中断,其后续处理包括解析协议栈、将数据包递交给上层应用等,这些都是耗时操作。

软中断机制就是为了将中断处理分割为两个部分而设计的:

  1. 上半部(Top Half / Hard IRQ):在关中断的ISR中执行,只做最紧急的工作,如响应硬件、读取状态、将数据从硬件FIFO拷贝到内存,然后标记一个“软中断”请求。这个过程必须极快。
  2. 下半部(Bottom Half / Softirq):在稍后的、更宽松的环境中(中断是打开的)执行那些耗时的任务。softirq就是最高性能的一种下半部实现。

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

Linux下半部机制经历了显著的演进:

  • 早期的BH(Bottom Halves):Linux早期内核有一种BH机制。它很简单,但存在一个致命缺陷:在多处理器(SMP)系统上,同一个BH不能同时在多个CPU上运行,存在全局锁,扩展性极差。
  • Softirq和Tasklet的引入:为了解决SMP扩展性问题,内核引入了softirqsoftirq的一个关键设计是,同一种类型的软中断(如网络接收)可以同时在多个CPU上并发执行,极大地提升了多核系统的性能。与此同时,为了方便普通驱动开发者,内核在softirq之上构建了更简单的tasklet机制。
  • ksoftirqd线程的出现:在高负载情况下(如网络流量风暴),软中断可能被频繁地触发,导致CPU一直在处理软中断而无法执行用户进程,造成用户进程“饥饿”。为了解决这个问题,内核为每个CPU都创建了一个名为ksoftirqd/X的内核线程。当软中断负载过高时,未处理完的工作会被这个线程接管,由于线程是受调度器管理的,可以保证用户进程也有机会运行。

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

softirq是Linux内核中断处理和调度的基石,其代码非常成熟、稳定。它不是一个经常变动的功能,而是其他高性能子系统(如网络、定时器)赖以构建的基础。
它的应用场景高度集中在对性能和低延迟要求极高的核心子系统中:

  • 网络栈:几乎所有的网络数据包收发处理(NET_TX_SOFTIRQ, NET_RX_SOFTIRQ)都是通过软中断完成的。
  • 定时器子系统:定时器到期后的回调函数执行是通过TIMER_SOFTIRQ触发的。
  • 块设备:I/O操作完成后的处理会通过BLOCK_SOFTIRQ进行。

核心原理与设计

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

softirq的核心是一个基于位掩码的、静态定义的、可并发的延迟任务执行框架

  1. 静态定义:内核预定义了少数几种软中断类型(在enum softirq_action中),如HI_SOFTIRQ, TIMER_SOFTIRQ, NET_RX_SOFTIRQ等。它们在编译时就已确定,不能在运行时动态添加。
  2. 触发(Raising):上半部(硬中断处理程序)在完成其紧急工作后,会调用raise_softirq(softirq_type)。这个函数非常轻量,它只是在当前CPU的一个私有变量(一个位掩码)中设置与softirq_type对应的位。
  3. 执行(Execution):内核会在一些特定的、安全的时间点检查这个位掩码,如果发现有挂起的软中断,就会调用do_softirq()来执行它们。这些时间点包括:
    • 从硬中断处理程序返回时
    • 从系统调用返回时
    • ksoftirqd内核线程中
  4. do_softirq()的逻辑:该函数会检查当前CPU的软中断挂起位掩码,然后按从高到低的优先级,依次调用预先注册好的处理函数(softirq_action数组)。
  5. ksoftirqd的角色:如果在上述检查点(如硬中断返回时)处理软中断时,发现新的软中断不断被触发(高负载),处理循环会有一个次数限制,不会无休止地进行下去。未处理完的软中断位掩码仍然是置位的。此时,do_softirq会唤醒当前CPU的ksoftirqd线程。ksoftirqd作为一个普通的内核线程,会被调度器调度运行,并在其执行上下文中调用do_softirq()来清理积压的软中断任务。

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

  • 高性能和低延迟:它是所有下半部机制中性能最高的,因为它没有额外的锁开销,并且可以并发执行。
  • SMP扩展性好:同一种软中断可以在多个CPU上同时运行,这对于多核网络服务器等场景至关重要。
  • 上下文确定:软中断在中断上下文中运行,虽然此时硬件中断是打开的,但它仍然是一个非抢占的、不能睡眠的执行环境,这使得其行为是高度可预测的。

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

  • 不能睡眠:这是它最主要的限制。在软中断处理函数中,绝对不能调用任何可能导致睡眠的函数(如申请GFP_KERNEL内存、获取信号量等),否则会使系统崩溃。
  • 静态定义:软中断的类型是编译时固定的,这使得它不适合作为一种通用的延迟任务机制给设备驱动程序使用。
  • 编程复杂且危险:开发者必须非常小心地处理并发问题,因为同一个软中断处理函数可能正在其他CPU上运行。此外,必须时刻警惕不能睡眠的限制。

使用场景

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

softirq是为内核最核心、对性能最敏感的子系统量身定做的,它不是给普通设备驱动开发者使用的通用工具。

  • 网络数据包处理:当网卡收到大量数据包时,硬中断将它们DMA到内存后,会立即触发NET_RX_SOFTIRQ。多个CPU可以并发地处理各自接收队列中的数据包,执行IP层、TCP/UDP层的逻辑。这是softirq最典型的应用场景。
  • 内核定时器:硬件定时器中断触发后,其硬中断处理程序会快速扫描到期的定时器,然后触发TIMER_SOFTIRQ。在TIMER_SOFTIRQ的上下文中,再安全地调用用户注册的成百上千个定时器回调函数。

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

  • 绝大多数设备驱动:普通的设备驱动绝对不应该直接注册和使用softirq。它太复杂且容易出错。驱动应该使用更上层的、更简单的tasklet(如果任务不需睡眠)或workqueue(如果任务需要睡眠)。
  • 任何需要睡眠的任务:如果延迟处理的任务需要分配大量内存、等待I/O、获取锁等,必须使用workqueue,因为它在进程上下文中运行,可以安全地睡眠。

对比分析

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

Linux内核中延迟执行任务的机制(统称下半部)主要有softirq, tasklet, workqueue

特性 硬中断处理程序 (Top Half) Softirq Tasklet Workqueue
执行上下文 硬中断上下文 软中断上下文 软中断上下文 进程上下文
能否睡眠 绝对不能 绝对不能 绝对不能 可以
并发性 不可重入,执行时屏蔽同级中断。 可并发:同一种softirq可在多个CPU上同时运行。 不可并发:同一种tasklet在任意时刻只能在一个CPU上运行。 可并发,由工作线程数量决定。
分配方式 静态(通过request_irq 静态(编译时确定类型) 动态(可随时初始化一个tasklet) 动态(可随时初始化一个work_struct)
使用场景 对硬件的紧急、快速响应。 内核核心、高性能、高频率的任务(网络、定时器)。 普通设备驱动的延迟任务(不需睡眠)。 需要睡眠或耗时很长的延迟任务(如涉及文件I/O)。

总结关系tasklet实际上是构建在softirq之上的一个简化封装。它使用HI_SOFTIRQTASKLET_SOFTIRQ这两个软中断向量,并增加了一个锁来保证同类tasklet的串行执行,从而为驱动开发者提供了更简单的并发模型。

kernel/softirq.c

invoke_softirq 触发软中断的执行

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
36
37
38
39
40
/*
* 这是一个静态内联函数,用于触发软中断的执行。
* 它在硬中断退出路径上被调用。
*/
static inline void invoke_softirq(void)
{
/*
* 首先,判断是否应该将软中断处理委托给ksoftirqd内核线程。
* 条件是:系统没有强制要求中断线程化,或者当前CPU的ksoftirqd线程还没有被创建。
* 如果这个条件满足,我们就尝试立即在当前上下文中执行软中断。
*/
if (!force_irqthreads() || !__this_cpu_read(ksoftirqd)) {
#ifdef CONFIG_HAVE_IRQ_EXIT_ON_IRQ_STACK
/*
* 如果内核配置为在专用的IRQ栈上退出中断(这是为了健壮性),
* 那么我们就可以安全地在当前栈上执行软中断。
* 因为在中断退出的这个阶段,IRQ栈应该是几乎空的。
*/
/*
* __do_softirq() 是真正执行软中断处理循环的核心函数。
*/
__do_softirq();
#else
/*
* 否则,irq_exit()是在任务自己的内核栈上被调用的,
* 而这个任务栈可能已经很深了(例如,经过了多层系统调用)。
* 在这种情况下,为了防止任何可能的栈溢出,我们需要切换到一个
* 专用的软中断栈来执行软中断处理。
*/
do_softirq_own_stack();
#endif
} else {
/*
* 如果执行到这里,说明系统被配置为强制中断线程化,
* 或者ksoftirqd线程已经存在。
* 这种情况下,我们不直接执行软中断,而是唤醒它。
*/
wakeup_softirqd();
}
}

irq_enter 进入中断上下文 irq_exit 退出中断上下文

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
/**
* irq_enter_rcu - 进入一个受RCU监控的中断上下文。
*/
void irq_enter_rcu(void)
{
/*
* 调用底层的__irq_enter_raw(),它会原子性地增加抢占计数器
* 中的硬中断计数值(preempt_count_add(HARDIRQ_OFFSET)),
* 并做一些RCU相关的状态更新。这是“记账”的核心。
*/
__irq_enter_raw();

/*
* 如果当前CPU是一个NOHZ_FULL CPU(完全无动态时钟,用于高性能计算),
* 或者当前CPU正处于idle任务中且这是唤醒它的第一个硬中断,
* 那么就需要调用tick_irq_enter()来重新激活该CPU的时钟节拍。
*/
if (tick_nohz_full_cpu(smp_processor_id()) ||
(is_idle_task(current) && (irq_count() == HARDIRQ_OFFSET)))
tick_irq_enter();

/*
* 更新CPU时间统计,将CPU从之前的状态(如idle或user)切换到
* 硬中断(HARDIRQ)状态,以便进行精确的时间消耗统计。
*/
account_hardirq_enter(current);
}

/**
* irq_enter - 进入一个中断上下文,包括RCU更新。
* 这是通用的中断入口函数。
*/
void irq_enter(void)
{
/*
* 通知上下文追踪(Context Tracking)子系统,我们即将进入IRQ状态。
*/
ct_irq_enter();
/*
* 调用核心的irq_enter_rcu()函数,完成抢占计数、RCU和时间统计等工作。
*/
irq_enter_rcu();
}

/*
* 这是一个静态内联函数,是irq_exit的核心实现,不直接导出。
*/
static inline void __irq_exit_rcu(void)
{
#ifndef __ARCH_IRQ_EXIT_IRQS_DISABLED
/* 如果当前架构没有保证在退出时中断是关闭的,这里手动关闭一下。*/
local_irq_disable();
#else
/* 否则,断言中断确实是关闭的,用于调试。*/
lockdep_assert_irqs_disabled();
#endif
/* 更新CPU时间统计,标记硬中断状态结束。*/
account_hardirq_exit(current);
/* 原子性地减少抢占计数器中的硬中断计数值。*/
preempt_count_sub(HARDIRQ_OFFSET);
/*
* 如果!in_interrupt()为真(表示这是最外层中断的退出),
* 并且有待处理的软中断,则调用invoke_softirq()来执行它们。
* 这是Linux下半部机制的核心触发点。
*/
if (!in_interrupt() && local_softirq_pending())
/* 软中断处理(Softirq Handling):
在退出最外层中断时,irq_exit负责检查并触发所有待处理的软中断。
这是Linux将中断处理分为“上半部(紧急)”和“下半部(可延迟)”两部分的关键机制 */
invoke_softirq();

/*
* 在特定配置下,如果系统强制使用中断线程化,并且有挂起的定时器,
* 则唤醒专门的定时器线程。
*/
if (IS_ENABLED(CONFIG_IRQ_FORCED_THREADING) && force_irqthreads() &&
local_timers_pending_force_th() && !(in_nmi() | in_hardirq()))
wake_timersd();

/* 通知动态时钟子系统,中断处理可能已结束。*/
tick_irq_exit();
}

/**
* irq_exit_rcu() - 退出一个中断上下文,不更新RCU。
*
* 也会在需要和可能的情况下处理软中断。
*/
void irq_exit_rcu(void)
{
/* 调用核心的退出逻辑。*/
__irq_exit_rcu();
/* 必须是最后一步!通知锁依赖检查器,硬中断上下文已结束。*/
lockdep_hardirq_exit();
}

/**
* irq_exit - 退出一个中断上下文,更新RCU和锁依赖检查器。
* 这是通用的中断退出函数。
*
* 也会在需要和可能的情况下处理软中断。
*/
void irq_exit(void)
{
/* 调用核心的退出逻辑。*/
__irq_exit_rcu();
/* 通知上下文追踪子系统,IRQ状态已结束。*/
ct_irq_exit();
/* 必须是最后一步!通知锁依赖检查器,硬中断上下文已结束。*/
lockdep_hardirq_exit();
}

open_softirq 软中断类型

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
36
37
38
enum
{
/* 高优先级软中断,通常用于处理紧急任务。
这些任务需要尽快完成,但不需要阻塞其他任务。 */
HI_SOFTIRQ=0,
/* 定时器软中断,用于处理定时器事件。
例如,周期性任务或超时事件的处理。 */
TIMER_SOFTIRQ,
/* 网络发送(NET_TX_SOFTIRQ)和接收(NET_RX_SOFTIRQ)软中断。
用于处理网络数据包的发送和接收,确保网络通信的高效性。 */
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
/* 块设备软中断,用于处理块设备(如硬盘)的 I/O 操作。
例如,完成磁盘读写任务 */
BLOCK_SOFTIRQ,
/* 断轮询软中断,用于处理中断轮询相关的任务。
通常用于优化设备的中断处理 */
IRQ_POLL_SOFTIRQ,
/* 任务软中断,用于处理任务队列中的任务。
任务队列是一种轻量级的延迟任务机制。 */
TASKLET_SOFTIRQ,
/* 调度软中断,用于处理任务调度相关的操作。
例如,重新分配 CPU 资源。 */
SCHED_SOFTIRQ,
/* 高精度定时器软中断,用于处理高精度定时器事件。
适用于需要精确时间控制的任务。 */
HRTIMER_SOFTIRQ,
/* RCU(Read-Copy-Update)软中断,用于处理 RCU 机制相关的任务。
RCU 是一种高效的读写同步机制,通常用于内核数据结构的更新。 */
RCU_SOFTIRQ, /* 首选RCU应该始终是最后一个软中断 */

NR_SOFTIRQS
};

void open_softirq(int nr, void (*action)(void))
{
softirq_vec[nr].action = action;
}

softirq_init 软中断初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void __init softirq_init(void)
{
int cpu;

for_each_possible_cpu(cpu) {
per_cpu(tasklet_vec, cpu).tail =
&per_cpu(tasklet_vec, cpu).head;
per_cpu(tasklet_hi_vec, cpu).tail =
&per_cpu(tasklet_hi_vec, cpu).head;
}

open_softirq(TASKLET_SOFTIRQ, tasklet_action);
open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}

run_ksoftirqd ksoftirqd 内核线程

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/*
* 这是一个静态函数,作为smpboot框架的回调,用于判断ksoftirqd线程是否应该运行。
* @cpu: 当前CPU的编号(尽管在此函数中未使用,但为保持回调接口一致性而存在)。
*
* 返回值: 如果有待处理的软中断,则返回非零值(true);否则返回0(false)。
*/
static int ksoftirqd_should_run(unsigned int cpu)
{
/*
* 直接调用local_softirq_pending()宏,检查当前CPU是否有挂起的软中断。
* 这个宏会读取一个per-cpu的变量来获取此状态。
*/
return local_softirq_pending();
}

static inline void ksoftirqd_run_begin(void)
{
local_irq_disable();
}

static inline void ksoftirqd_run_end(void)
{
local_irq_enable();
}

/*
* 这是一个静态函数,作为smpboot框架的核心工作回调,由ksoftirqd线程执行。
* @cpu: 当前CPU的编号(在此函数中未使用,但为保持回调接口一致性而存在)。
*/
static void run_ksoftirqd(unsigned int cpu)
{
/* 调用此函数,将当前ksoftirqd线程状态置为TASK_RUNNING,并做一些准备。*/
ksoftirqd_run_begin();

/* 再次检查是否有待处理的软中断,防止在唤醒到执行的间隙中已被处理。*/
if (local_softirq_pending()) {
/*
* 我们可以安全地在内联栈上运行softirq,因为我们在这里
* 并不处于任务栈的深处。
*/
/*
* 调用handle_softirqs(),这是处理所有类型软中断的核心函数。
* 它会循环处理所有挂起的软中断,直到处理完毕或达到循环上限。
* 传入true参数可能表示这是一个从ksoftirqd上下文的调用。
*/
handle_softirqs(true);
/* 调用此函数,将ksoftirqd线程状态恢复为TASK_INTERRUPTIBLE。*/
ksoftirqd_run_end();
/*
* 调用条件重新调度函数。这会检查是否有更高优先级的任务在等待,
* 如果有,就主动让出CPU。这是保证系统响应性的关键。
*/
cond_resched();
/* 处理完毕,返回。smpboot_thread_fn会继续下一次主循环。*/
return;
}
/* 如果没有待处理的软中断,直接调用结束函数并返回。*/
ksoftirqd_run_end();
}

softirq_handle_begin 和 softirq_handle_end

  • 这两个函数用于在处理软中断时禁用和启用本地
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
static __always_inline void __local_bh_disable_ip(unsigned long ip, unsigned int cnt)
{
preempt_count_add(cnt);
barrier();
}

static void __local_bh_enable(unsigned int cnt)
{
lockdep_assert_irqs_disabled();

if (preempt_count() == cnt)
trace_preempt_on(CALLER_ADDR0, get_lock_parent_ip());

if (softirq_count() == (cnt & SOFTIRQ_MASK))
lockdep_softirqs_on(_RET_IP_);

__preempt_count_sub(cnt);
}

static inline void softirq_handle_begin(void)
{
__local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);
}

static inline void softirq_handle_end(void)
{
__local_bh_enable(SOFTIRQ_OFFSET);
WARN_ON_ONCE(in_interrupt());
}

handle_softirqs 处理软中断

  • handle_softirqs(在一些内核版本中可能名为__do_softirq)是Linux中断处理下半部(bottom-half)机制的核心执行引擎。当中断上下文(或ksoftirqd线程)确定有待处理的软中断时,就会调用此函数。
    它的核心作用是:在一个受控的循环中,遍历所有挂起的软中断类型,并依次调用它们注册好的处理函数(handler),直到所有挂起的软中断都被处理完毕。
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
/*
* 我们最多重新启动软中断处理 MAX_SOFTIRQ_RESTART 次,
* 但如果设置了 need_resched() 或超过 2 毫秒,则中断循环。
* MAX_SOFTIRQ_TIME 在大多数情况下提供了一个不错的上限,但在某些情况下,
* 例如 stop_machine(),jiffies 可能会停止递增,因此我们还需要
* MAX_SOFTIRQ_RESTART 限制以确保最终从此方法返回。
*
* 这些限制是通过实验确定的。
* 需要平衡的两件事是延迟与公平性——
* 我们希望尽快处理软中断,但它们不应该能够锁住系统。
*/

#define MAX_SOFTIRQ_TIME msecs_to_jiffies(2)
#define MAX_SOFTIRQ_RESTART 10

static inline bool lockdep_softirq_start(void) { return false; }
static inline void lockdep_softirq_end(bool in_hardirq) { }

/*
* 这是一个静态函数,负责处理所有挂起的软中断。
* @ksirqd: 一个布尔值,如果为true,表示本次调用来自于ksoftirqd内核线程上下文。
*/
static void handle_softirqs(bool ksirqd)
{
/* end: 定义一个超时时间戳,防止软中断处理占用CPU过久。*/
unsigned long end = jiffies + MAX_SOFTIRQ_TIME;
/* old_flags: 保存当前任务原始的flags,以便处理后恢复。*/
unsigned long old_flags = current->flags;
/* max_restart: 最大的重入处理次数,防止活锁。*/
int max_restart = MAX_SOFTIRQ_RESTART;
/* h: 指向软中断向量表的指针。*/
struct softirq_action *h;
/* in_hardirq: 用于锁依赖检测器,记录进入时是否在硬中断上下文中。*/
bool in_hardirq;
/* pending: 32位掩码,用于存储当前CPU所有待处理的软中断。*/
__u32 pending;
/* softirq_bit: 在循环中,存储当前正在处理的软中断的位号。*/
int softirq_bit;

/*
* 清除PF_MEMALLOC标志,因为当前任务上下文被软中断借用了。
* 一个被处理的软中断,例如网络RX,如果socket与交换(swapping)相关,
* 可能会再次设置PF_MEMALLOC。
*/
current->flags &= ~PF_MEMALLOC;

/* 调用local_softirq_pending()获取当前所有挂起的软中断位掩码。*/
pending = local_softirq_pending();

/* 通知相关子系统(如lockdep),软中断处理即将开始。*/
softirq_handle_begin();
in_hardirq = lockdep_softirq_start();
/* 为当前任务记账进入软中断所消耗的时间。*/
// account_softirq_enter(current);

restart: /* 这是处理循环的入口点。*/
/* 在开启中断之前,重置待处理位掩码。*/
set_softirq_pending(0);

/* 开启本地硬件中断。软中断总是在中断开启的状态下执行。*/
local_irq_enable();

/* h指向软中断向量表的起始处。*/
h = softirq_vec;

/*
* 只要pending掩码中还有被置位的位,就一直循环。
* ffs(pending) (find first set) 返回第一个被置位的位的编号(从1开始)。
*/
while ((softirq_bit = ffs(pending))) {
unsigned int vec_nr;
int prev_count;

/* 将h指针移动到对应的软中断处理函数结构体。*/
h += softirq_bit - 1;

/* 计算出向量号(数组索引)。*/
vec_nr = h - softirq_vec;
/* 保存执行前的抢占计数值。*/
prev_count = preempt_count();

/* 增加该CPU上此类型软中断的统计计数。*/
kstat_incr_softirqs_this_cpu(vec_nr);

/* 记录软中断入口的追踪事件。*/
trace_softirq_entry(vec_nr);
/* 通过函数指针,调用实际的软中断处理函数。*/
h->action();
/* 记录软中断出口的追踪事件。*/
trace_softirq_exit(vec_nr);
/* 检查抢占计数是否被意外修改,如果是,则打印错误并强制恢复。*/
if (unlikely(prev_count != preempt_count())) {
pr_err("huh, entered softirq %u %s %p with preempt_count %08x, exited with %08x?\n",
vec_nr, softirq_to_name[vec_nr], h->action,
prev_count, preempt_count());
preempt_count_set(prev_count);
}
/* 将h指针移动到下一个向量。*/
h++;
/* 将pending掩码右移,清除已经处理过的位,准备下一次ffs。*/
pending >>= softirq_bit;
}

/* 如果不是在RT内核中,并且是从ksirqd上下文调用的,则通知RCU这是一个静止状态点。*/
if (!IS_ENABLED(CONFIG_PREEMPT_RT) && ksirqd)
rcu_softirq_qs();

/* 关闭本地硬件中断,以安全地检查是否有新的软中断挂起。*/
local_irq_disable();

/* 再次获取待处理的软中断位掩码。*/
pending = local_softirq_pending();
/* 如果有新的软中断被触发了... */
if (pending) {
/* ...并且处理时间没有超时,且没有调度请求,且重入次数未达上限...*/
if (time_before(jiffies, end) && !need_resched() &&
--max_restart)
/* ...则跳转到restart,开始新一轮处理。*/
goto restart;

/* 否则,唤醒ksoftirqd内核线程,让它来处理剩余的工作。*/
wakeup_softirqd();
}

/* 为当前任务记账退出软中断。*/
account_softirq_exit(current);
/* 通知锁依赖检测器,软中断处理结束。*/
lockdep_softirq_end(in_hardirq);
/* 其他清理工作。*/
softirq_handle_end();
/* 恢复当前任务原始的flags(主要是恢复可能被修改的PF_MEMALLOC)。*/
current_restore_flags(old_flags, PF_MEMALLOC);
}

__do_softirq 不在内核线程中处理软中断

1
2
3
4
asmlinkage __visible void __softirq_entry __do_softirq(void)
{
handle_softirqs(false);
}

__raise_softirq_irqoff 设置指定类型的软中断(SoftIRQ)为待处理状态

1
2
3
4
5
6
7
8
void __raise_softirq_irqoff(unsigned int nr)
{
/* 确保当前运行环境中断已被禁用 */
lockdep_assert_irqs_disabled();
trace_softirq_raise(nr);
/* 将指定类型的软中断标记为待处理状态。 */
or_softirq_pending(1UL << nr);
}

wakeup_softirqd 唤醒 ksoftirqd 内核线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
* 我们不能在这里无限循环以避免用户空间匮乏,
* 但我们也不想给待处理事件引入最坏情况的 1/HZ 延迟,
* 所以让调度器为我们平衡软中断负载。
*/
static void wakeup_softirqd(void)
{
/* 中断被禁用:无需停止抢占*/
/* ksoftirqd 是一个专门用于处理软中断的内核线程,每个 CPU 都有一个独立的实例 */
struct task_struct *tsk = __this_cpu_read(ksoftirqd);
/* 如果 tsk 非空,表示当前 CPU 上存在 ksoftirqd 内核线程。调用 wake_up_process 唤醒该线程,使其能够处理挂起的软中断任务 */
if (tsk)
wake_up_process(tsk);
}

raise_softirq_irqoff 用于触发指定类型的软中断(SoftIRQ)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*
* 这个函数必须在禁用 irqs 的情况下运行!
*/
inline void raise_softirq_irqoff(unsigned int nr)
{
/* 设置软中断的状态 将指定的软中断类型(nr)标记为待处理状态*/
__raise_softirq_irqoff(nr);

/* in_interrupt():检查当前是否处于中断或软中断上下文。如果是,则无需进一步操作,因为软中断会在中断或软中断处理完成后自动执行 */
/* should_wake_ksoftirqd():检查是否需要唤醒 ksoftirqd 内核线程。ksoftirqd 是一个专门用于处理软中断的内核线程。
static inline bool should_wake_ksoftirqd(void)
{
return true;
}
*/
if (!in_interrupt() && should_wake_ksoftirqd())
/* 唤醒内核线程,以确保软中断能够尽快被调度和执行 */
wakeup_softirqd();
}

timer_thread 用于处理定时器中断的内核线程

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
static struct smp_hotplug_thread timer_thread = {
.store = &ktimerd,
.setup = ktimerd_setup,
.thread_should_run = ktimerd_should_run,
.thread_fn = run_ktimerd,
.thread_comm = "ktimers/%u",
};

/*
* ktimerd_setup - ktimerd线程的初始化设置函数
* @cpu: 运行此线程的CPU号
*/
static void ktimerd_setup(unsigned int cpu)
{
/*
* 将当前进程(即ktimerd线程)的调度策略设置为先进先出(FIFO)的低优先级。
* SCHED_FIFO是一种实时调度策略,但设置为低优先级(fifo_low)意味着它
* 仍然会比普通的SCHED_NORMAL任务优先执行,但不会抢占更高优先级的实时任务。
* 这样做的目的是为了确保定时器相关的下半部任务,能够比常规的用户态
* 或内核态任务更早地得到处理,保证定时器的精度和系统的响应性。
*/
sched_set_fifo_low(current);
}

/*
* ktimerd_should_run - 判断ktimerd线程是否需要运行的条件函数
* @cpu: 运行此线程的CPU号
*/
static int ktimerd_should_run(unsigned int cpu)
{
/*
* 调用local_timers_pending_force_th(),这个函数会检查当前CPU上
* 是否有挂起的、并且被标记为“强制在线程中处理”的定时器软中断。
*
* "force_th" (force thread) 这个标记非常关键。有些定时器任务
* (特别是高精度定时器hrtimer)如果在硬中断返回路径上处理,
* 可能会因为执行时间过长而增加中断延迟。设置了这个标记的定时器
* 软中断,就会被特意留给ktimerd来处理。
*
* 返回真,则ksoftirqd框架会唤醒并运行ktimerd。
*/

/* static inline unsigned int local_timers_pending_force_th(void)
{
return __this_cpu_read(pending_timer_softirq);
} */
return local_timers_pending_force_th();
}

/*
* run_ktimerd - ktimerd线程的主体执行函数
* @cpu: 运行此线程的CPU号
*/
static void run_ktimerd(unsigned int cpu)
{
unsigned int timer_si; // 用于临时存储待处理的定时器软中断掩码。

/* 通知ksoftirqd框架,我们即将开始执行软中断,用于统计和调试。*/
ksoftirqd_run_begin();

/*
* **第一步:获取并清空任务列表**
* 再次调用local_timers_pending_force_th(),获取当前所有待处理的、
* 强制线程化的定时器软中断掩码,并将其存入timer_si。
*
* 紧接着,原子地将per-cpu的pending_timer_softirq清零。
* 这个“获取并清零”的操作确保了我们不会丢失在处理期间新到达的任务。
* 新任务会被raise_ktimers_thread设置到这个刚刚被清零的变量上,
* 等待下一次ktimerd被唤醒时处理。
*/
timer_si = local_timers_pending_force_th();
__this_cpu_write(pending_timer_softirq, 0);

/*
* **第二步:将任务转移到通用软中断队列**
* or_softirq_pending(timer_si) 会将我们刚刚从线程专用列表
* (pending_timer_softirq)中取出的任务,合并到内核通用的
* “待处理软中断”掩码中。
*/
/* #define or_softirq_pending(x) (__this_cpu_or(local_softirq_pending_ref, (x))) */
or_softirq_pending(timer_si);

/*
* **第三步:执行所有挂起的软中断**
* __do_softirq() 是内核中实际执行软中断的核心函数。它会检查
* 通用的“待处理软中断”掩码,并调用所有被标记的软中断对应的处理函数
* (handler),当然也包括我们刚刚放进去的定时器任务。
* 这是一个循环,会尽可能多地处理软中断,直到处理完毕或达到一个时间限制。
*/
__do_softirq();

/* 通知ksoftirqd框架,本轮处理结束。*/
ksoftirqd_run_end();
}

spawn_ksoftirqd 用于创建和初始化 ksoftirqd 内核线程

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
36
37
38
39
40
41
42
43
static struct smp_hotplug_thread softirq_threads = {
.store = &ksoftirqd,
.thread_should_run = ksoftirqd_should_run,
.thread_fn = run_ksoftirqd,
.thread_comm = "ksoftirqd/%u",
};

/*
* 这是一个静态的、仅在初始化阶段调用的函数,负责衍生ksoftirqd等线程。
*/
static int __init spawn_ksoftirqd(void)
{
/*
* 向CPU热插拔(cpuhp)框架注册一个状态回调。
* 当一个CPU进入CPUHP_SOFTIRQ_DEAD状态(即即将死亡)时,
* takeover_tasklets函数将被调用,以处理该CPU上残留的tasklet。
*/
cpuhp_setup_state_nocalls(CPUHP_SOFTIRQ_DEAD, "softirq:dead", NULL,
takeover_tasklets);
/*
* 调用smpboot_register_percpu_thread,向smpboot框架注册一个
* per-cpu线程模板softirq_threads。smpboot框架将在后续每个CPU
* 上线时,根据这个模板自动创建一个ksoftirqd线程。
* BUG_ON()确保如果注册失败(极不正常),系统会立即停机。
*/
BUG_ON(smpboot_register_percpu_thread(&softirq_threads));

/* 如果内核配置了“强制中断线程化”支持。*/
#ifdef CONFIG_IRQ_FORCED_THREADING
/* force_irqthreads()检查内核启动参数是否要求开启此功能。*/
if (force_irqthreads())
/* 如果开启,则同样地注册用于处理定时器中断的timer_thread模板。*/
BUG_ON(smpboot_register_percpu_thread(&timer_thread));
#endif
/* initcall函数要求返回一个int值,0表示成功。*/
return 0;
}
/*
* 通过early_initcall()宏,将spawn_ksoftirqd函数注册到内核的早期
* 初始化调用列表中。这确保了它在SMP被激活之前执行,能够为所有
* 即将上线的CPU做好准备。
*/
early_initcall(spawn_ksoftirqd);

stm32_gpiolib_register_bank: 注册一个GPIO端口到内核

此函数是STM32驱动中将一个物理GPIO端口(例如GPIOA, GPIOB等)注册为可供Linux内核其他部分使用的gpio_chip的核心。它的原理是收集并打包特定GPIO端口的所有硬件信息(寄存器地址、中断能力、引脚名称等), 然后调用通用的gpiochip_add_data函数, 将这个打包好的实体正式提交给内核的gpiolib框架。完成此操作后, 其他驱动程序就能通过标准的gpio_request()等API来使用这个端口的引脚了。

具体执行步骤如下:

  1. 硬件准备: 它首先将GPIO端口的硬件脱离复位状态(reset_control_deassert), 然后解析设备树以获取该端口寄存器的物理地址, 并通过devm_ioremap_resource将其映射到内核的虚拟地址空间, 使得CPU可以访问。
  2. gpio_chip结构体填充: 这是核心步骤, gpio_chip是gpiolib框架理解一个GPIO控制器的标准”名片”。
    • 它从一个通用模板开始, 然后用设备树中定义的具体信息(如端口名称st,bank-name)来填充。
    • 它解析gpio-ranges属性来确定该端口的引脚在Linux全局GPIO编号空间中的起始编号。如果该属性不存在, 它会采用一种旧的、基于端口顺序的编号方案, 并手动调用pinctrl_add_gpio_range来建立pinctrl和gpiolib之间的映射关系。
    • 它设置该gpio_chip包含的引脚数量(ngpio, 通常是16)、父设备等信息。
  3. 中断体系建立: 如果pinctrl支持中断, 它会为当前这个GPIO端口创建一个层级式中断域(irq_domain_create_hierarchy)。这会将该端口的中断处理能力链接到上一层的EXTI(外部中断)控制器中断域。当中断发生时, 中断信号会沿着这个层级正确地上传和处理。
  4. 引脚命名: 它会遍历该端口的所有引脚(0-15), 从pinctrl驱动的引脚数据库中查出每个引脚的名称(如 “PA0”, “PA1”), 并将这个名称列表关联到gpio_chip。这对于调试和用户空间通过sysfs查看信息非常有用。
  5. 最终注册: 万事俱备后, 它调用gpiochip_add_data, 将完全配置好的gpio_chip注册到内核。bank指针作为私有数据被一同传入, 这样当gpiolib调用此芯片的操作函数(如.set, .get)时, 这些函数可以方便地访问到该端口的寄存器基地址和锁等私有信息。
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
/*
* 静态函数声明: stm32_gpiolib_register_bank
* @pctl: 指向驱动核心数据结构的指针.
* @fwnode: 指向当前要注册的GPIO bank的设备树节点的句柄.
* @return: 成功时返回0, 失败时返回负错误码.
*/
static int stm32_gpiolib_register_bank(struct stm32_pinctrl *pctl, struct fwnode_handle *fwnode)
{
/* 获取一个指向当前要填充的 bank 结构体的指针. pctl->nbanks 记录了已成功注册的数量. */
struct stm32_gpio_bank *bank = &pctl->banks[pctl->nbanks];
int bank_ioport_nr;
struct pinctrl_gpio_range *range = &bank->range;
struct fwnode_reference_args args;
struct device *dev = pctl->dev;
struct resource res;
int npins = STM32_GPIO_PINS_PER_BANK; // 默认每个bank有16个引脚
int bank_nr, err, i = 0;
struct stm32_desc_pin *stm32_pin;
char **names;

/* 如果复位控制器存在, 则将其解除复位状态, 使硬件开始工作. */
if (!IS_ERR(bank->rstc))
reset_control_deassert(bank->rstc);

/* 从设备树节点的 'reg' 属性中解析出该bank的寄存器物理地址范围. */
if (of_address_to_resource(to_of_node(fwnode), 0, &res))
return -ENODEV;

/* 将物理地址映射到内核虚拟地址空间, 以便访问寄存器. devm_* 版本会自动管理释放. */
bank->base = devm_ioremap_resource(dev, &res);
if (IS_ERR(bank->base))
return PTR_ERR(bank->base);

/* gpiolib的核心结构是 gpio_chip. 这里从一个预设的模板开始填充. */
bank->gpio_chip = stm32_gpio_template;

/* 从设备树读取 "st,bank-name" 属性 (例如 "GPIOA"), 作为 gpio_chip 的标签. */
fwnode_property_read_string(fwnode, "st,bank-name", &bank->gpio_chip.label);

/* 尝试解析 "gpio-ranges" 属性, 这是将pinctrl和gpiolib关联起来的现代标准方法. */
if (!fwnode_property_get_reference_args(fwnode, "gpio-ranges", NULL, 3, i, &args)) {
/* 如果成功, 根据解析出的信息计算bank的编号和Linux全局GPIO基准号. */
bank_nr = args.args[1] / STM32_GPIO_PINS_PER_BANK;
bank->gpio_chip.base = args.args[1];

/* 计算该bank实际管理的引脚数量, 以支持某些bank可能不是完整的16个引脚的情况. */
npins = args.args[0] + args.args[2];
while (!fwnode_property_get_reference_args(fwnode, "gpio-ranges", NULL, 3, ++i, &args))
npins = max(npins, (int)(args.args[0] + args.args[2]));
} else {
/* 如果没有 "gpio-ranges" 属性 (兼容旧的设备树), 则采用一种简单的线性编号方案. */
bank_nr = pctl->nbanks;
bank->gpio_chip.base = bank_nr * STM32_GPIO_PINS_PER_BANK;
/* 并且需要手动填充一个range结构, 并调用 pinctrl_add_gpio_range 告知pinctrl子系统. */
range->name = bank->gpio_chip.label;
range->id = bank_nr;
range->pin_base = range->id * STM32_GPIO_PINS_PER_BANK;
range->base = range->id * STM32_GPIO_PINS_PER_BANK;
range->npins = npins;
range->gc = &bank->gpio_chip;
pinctrl_add_gpio_range(pctl->pctl_dev,
&pctl->banks[bank_nr].range);
}

/* 读取可选的 "st,bank-ioport" 属性. */
if (fwnode_property_read_u32(fwnode, "st,bank-ioport", &bank_ioport_nr))
bank_ioport_nr = bank_nr;

/* base设为-1表示希望gpiolib动态分配一个未使用的基准号, 这是推荐的做法. */
bank->gpio_chip.base = -1;

/* 填充gpio_chip的其他字段. */
bank->gpio_chip.ngpio = npins;
bank->gpio_chip.fwnode = fwnode;
bank->gpio_chip.parent = dev;
bank->bank_nr = bank_nr;
bank->bank_ioport_nr = bank_ioport_nr;
bank->secure_control = pctl->match_data->secure_control;
bank->rif_control = pctl->match_data->rif_control;
/* 初始化用于保护该bank寄存器访问的自旋锁. */
spin_lock_init(&bank->lock);

/* 如果pinctrl支持中断. */
if (pctl->domain) {
/* 为这个bank创建一个层级式中断域, 链接到pinctrl的主中断域(EXTI). */
bank->fwnode = fwnode;
bank->domain = irq_domain_create_hierarchy(pctl->domain, 0, STM32_GPIO_IRQ_LINE,
bank->fwnode, &stm32_gpio_domain_ops,
bank);
if (!bank->domain)
return -ENODEV;
}

/* 为引脚名称数组分配内存. */
names = devm_kcalloc(dev, npins, sizeof(char *), GFP_KERNEL);
if (!names)
return -ENOMEM;

/* 遍历该bank的所有引脚, 从pinctrl的数据库中查找并复制其名称. */
for (i = 0; i < npins; i++) {
stm32_pin = stm32_pctrl_get_desc_pin_from_gpio(pctl, bank, i);
if (stm32_pin && stm32_pin->pin.name) {
names[i] = devm_kasprintf(dev, GFP_KERNEL, "%s", stm32_pin->pin.name);
if (!names[i])
return -ENOMEM;
} else {
names[i] = NULL;
}
}

/* 将名称数组关联到gpio_chip. */
bank->gpio_chip.names = (const char * const *)names;

/* 最关键的一步: 将填充好的gpio_chip注册到gpiolib核心. */
err = gpiochip_add_data(&bank->gpio_chip, bank);
if (err) {
dev_err(dev, "Failed to add gpiochip(%d)!\n", bank_nr);
return err;
}

dev_info(dev, "%s bank added\n", bank->gpio_chip.label);
return 0;
}

tasklet_setup 和 tasklet_kill: Tasklet的初始化与销毁

Tasklet是Linux内核中一种用于”下半部”(bottom half)处理的机制。它的原理是提供一个轻量级的、可被调度的函数, 用于执行那些不适合在硬件中断处理程序中完成的、耗时稍长的工作。这两个函数分别是它的”构造函数”和”析构函数”。

tasklet_setup: 初始化一个Tasklet

此函数用于在使用tasklet之前, 对其数据结构tasklet_struct进行正确的初始化。它是一个准备步骤, 将结构体设置为一个已知的、干净的状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
* tasklet_setup: 设置一个tasklet结构体.
* @t: 指向要被初始化的 tasklet_struct 的指针.
* @callback: 一个函数指针, 指向当tasklet被调度执行时要运行的函数.
*/
void tasklet_setup(struct tasklet_struct *t,
void (*callback)(struct tasklet_struct *))
{
t->next = NULL; // 确保其不链接在任何链表中
t->state = 0; // 状态清零, 清楚"已调度"和"正在运行"的标志
atomic_set(&t->count, 0); // 将使能/禁用计数器清零 (0表示使能)
t->callback = callback; // 存储核心的回调函数
t->use_callback = true; // 标记为使用标准回调
t->data = 0; // 传递给回调函数的参数, 初始化为0
}
EXPORT_SYMBOL(tasklet_setup);

tasklet_kill: 销毁一个Tasklet

此函数用于安全地移除一个tasklet。它的核心是确保tasklet不再被调度, 并等待其执行完毕(如果它恰好正在运行)。这在驱动卸载时至关重要, 可以防止在驱动资源被释放后, 仍然有一个tasklet在尝试访问这些无效的资源, 从而导致系统崩溃。

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
/*
* tasklet_kill: 杀死一个tasklet, 等待其完成.
* @t: 指向要被杀死的 tasklet_struct 的指针.
*/
void tasklet_kill(struct tasklet_struct *t)
{
/* 检查是否在硬中断上下文中调用, 这是不推荐的, 因为kill可能睡眠. */
if (in_interrupt())
pr_notice("Attempt to kill tasklet from interrupt\n");

/*
* 第一步等待: 等待 TASKLET_STATE_SCHED 位被清除.
* 这确保了tasklet不再位于调度队列中. _lock版本还会原子地设置该位,
* 防止在我们等待期间, 它被重新调度.
*/
wait_on_bit_lock(&t->state, TASKLET_STATE_SCHED, TASK_UNINTERRUPTIBLE);

/*
* 第二步等待: 调用 tasklet_unlock_wait, 它会等待 TASKLET_STATE_RUN 位被清除.
* 这确保了如果tasklet在我们调用kill时正在运行, 我们会一直等到它执行完毕.
* 在单核STM32上, 这意味着等待 softirq 上下文完成对该tasklet的执行.
*/
tasklet_unlock_wait(t);
/* 最后, 再次清理调度标志位. */
tasklet_clear_sched(t);
}
EXPORT_SYMBOL(tasklet_kill);

Tasklet调度核心机制

此代码片段展示了Linux内核中Tasklet调度机制的核心底层实现。Tasklet是一种轻量级的、高性能的延迟工作(Deferred Work)机制, 其目的是将那些不适合在硬件中断处理程序(硬中断上下文)中完成的、耗时稍长的工作, “推迟”到更宽松的软中断(softirq)上下文中执行。

这组函数的核心原理是通过一个原子操作的”门禁”和一个CPU本地的链表队列, 高效地将一个待执行的任务(tasklet)排入队列, 并触发一个软中断来最终处理这个队列


tasklet_schedule: 调度一个Tasklet (公共API)

这是驱动程序开发者最常使用的、用于调度一个Tasklet的入口API。它的设计核心是幂等性效率

原理:
它通过一个原子的test_and_set_bit操作来确保, 即使一个tasklet被频繁地、连续地调度, 它也只会被添加到执行队列中一次。这可以防止队列被同一个待办任务淹没, 是一种至关重要的优化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*
* static inline: 定义一个静态内联函数, 通常在头文件中实现, 以提高性能.
*/
static inline void tasklet_schedule(struct tasklet_struct *t)
{
/*
* test_and_set_bit 是一个原子操作. 它会:
* 1. 测试 t->state 中的 TASKLET_STATE_SCHED 位 (检查它原来是0还是1).
* 2. 无论原来是什么, 都将该位设置为 1.
* 3. 返回该位 *之前* 的值.
*
* if (!test_and_set_bit(...)) 的条件只有在该位*原来是0*时才为真.
* 这意味着: 如果 tasklet 尚未被调度 (SCHED位为0), 那么就设置该位并继续执行 __tasklet_schedule.
* 如果 tasklet 已经被调度 (SCHED位为1), 那么这个函数什么也不做, 直接返回.
* 这就保证了一个tasklet在被执行前, 只会被排队一次.
*/
if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
__tasklet_schedule(t);
}

__tasklet_schedule: 调度一个普通优先级的Tasklet

这是一个内部函数, 是tasklet_schedule成功通过”门禁”后调用的下一步。它的作用是将任务分发到处理普通优先级tasklet的通用路径上

原理:
它作为一个”分发器”, 调用更底层的__tasklet_schedule_common函数, 并明确指定使用普通优先级的队列(tasklet_vec)和普通优先级的软中断号(TASKLET_SOFTIRQ)。

1
2
3
4
5
6
7
8
9
10
11
12
void __tasklet_schedule(struct tasklet_struct *t)
{
/*
* 调用通用的调度函数 __tasklet_schedule_common, 并传入:
* - t: 要被调度的tasklet.
* - &tasklet_vec: 指向 per-CPU 的、用于普通优先级tasklet的队列头.
* - TASKLET_SOFTIRQ: 用于处理普通优先级tasklet的软中断号.
*/
__tasklet_schedule_common(t, &tasklet_vec,
TASKLET_SOFTIRQ);
}
EXPORT_SYMBOL(__tasklet_schedule);

__tasklet_schedule_common: Tasklet调度的核心实现

这是实际执行排队和触发动作的核心函数。

原理:
它通过禁用本地中断来保证对当前CPU私有队列原子访问, 将新的tasklet以O(1)的效率添加到队列末尾, 然后升起一个软中断来通知内核”有活要干了”。

对于用户指定的STM32H750(单核)架构:

  • DEFINE_PER_CPU宏只会为tasklet_vec分配一个实例。
  • this_cpu_ptr总是返回指向这个唯一实例的指针。
  • local_irq_save在这种情况下依然至关重要。它防止的不是来自其他CPU的并发访问, 而是防止当前任务在修改队列时, 被一个硬件中断处理程序抢占, 而这个中断处理程序可能也会尝试调度一个tasklet, 从而导致对队列的竞争和破坏。
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
36
37
38
39
40
41
42
43
44
/*
* __tasklet_schedule_common: tasklet调度的通用核心实现
* @t: 要被调度的tasklet
* @headp: 指向 per-CPU 队列头数组的指针 (例如 &tasklet_vec)
* @softirq_nr: 要触发的软中断号
*/
static void __tasklet_schedule_common(struct tasklet_struct *t,
struct tasklet_head __percpu *headp,
unsigned int softirq_nr)
{
struct tasklet_head *head;
unsigned long flags;

/*
* 禁用当前CPU的本地中断, 并保存之前的中断状态到 'flags'.
* 这是保护 per-CPU 队列的关键, 防止中断处理程序并发地修改它.
*/
local_irq_save(flags);
/*
* 获取指向 *当前CPU* 的 tasklet_head 实例的指针.
* 这是tasklet高性能和无锁设计的核心: 每个CPU只操作自己的队列.
*/
head = this_cpu_ptr(headp);
/*
* 将新的tasklet 't' 添加到队列的尾部.
* 这是一个 O(1) (常数时间) 的高效链表追加操作, 因为 'head->tail'
* 直接指向了链表最后一个节点的 'next' 字段的地址.
*/
t->next = NULL;
*head->tail = t;
head->tail = &(t->next);
/*
* 升起(Raise)指定的软中断.
* 这个动作会设置一个标志位, 内核在退出中断或调度器运行时会检查这个标志位,
* 如果被设置, 就会去执行对应的软中断处理函数(例如, tasklet_action).
* 这个处理函数会遍历队列并执行所有排队的tasklet.
* _irqoff 后缀表示这个函数可以在中断被禁用的状态下安全调用.
*/
raise_softirq_irqoff(softirq_nr);
/*
* 恢复之前保存的中断状态.
*/
local_irq_restore(flags);
}