title: softirq
categories:
- linux
- kernel
- irq
tags: - linux
- kernel
- irq
abbrlink: 366cb710
date: 2025-10-03 09:01:49
文章目录
- [kernel/softirq.c 内核中断下半部(Interrupt Bottom-Half) 核心实现](#kernel/softirq.c 内核中断下半部(Interrupt Bottom-Half) 核心实现)
- kernel/softirq.c
-
- [invoke_softirq 触发软中断的执行](#invoke_softirq 触发软中断的执行)
- [irq_enter 进入中断上下文 irq_exit 退出中断上下文](#irq_enter 进入中断上下文 irq_exit 退出中断上下文)
- [open_softirq 软中断类型](#open_softirq 软中断类型)
- [softirq_init 软中断初始化](#softirq_init 软中断初始化)
- [run_ksoftirqd ksoftirqd 内核线程](#run_ksoftirqd ksoftirqd 内核线程)
- [softirq_handle_begin 和 softirq_handle_end](#softirq_handle_begin 和 softirq_handle_end)
- [handle_softirqs 处理软中断](#handle_softirqs 处理软中断)
- [__do_softirq 不在内核线程中处理软中断](#__do_softirq 不在内核线程中处理软中断)
- [__raise_softirq_irqoff 设置指定类型的软中断(SoftIRQ)为待处理状态](#__raise_softirq_irqoff 设置指定类型的软中断(SoftIRQ)为待处理状态)
- [wakeup_softirqd 唤醒 ksoftirqd 内核线程](#wakeup_softirqd 唤醒 ksoftirqd 内核线程)
- [raise_softirq_irqoff 用于触发指定类型的软中断(SoftIRQ)](#raise_softirq_irqoff 用于触发指定类型的软中断(SoftIRQ))
- [timer_thread 用于处理定时器中断的内核线程](#timer_thread 用于处理定时器中断的内核线程)
- [spawn_ksoftirqd 用于创建和初始化 ksoftirqd 内核线程](#spawn_ksoftirqd 用于创建和初始化 ksoftirqd 内核线程)
-
- [stm32_gpiolib_register_bank: 注册一个GPIO端口到内核](#stm32_gpiolib_register_bank: 注册一个GPIO端口到内核)
- [tasklet_setup 和 tasklet_kill: Tasklet的初始化与销毁](#tasklet_setup 和 tasklet_kill: Tasklet的初始化与销毁)
-
- [`tasklet_setup`: 初始化一个Tasklet](#
tasklet_setup
: 初始化一个Tasklet) - [`tasklet_kill`: 销毁一个Tasklet](#
tasklet_kill
: 销毁一个Tasklet)
- [`tasklet_setup`: 初始化一个Tasklet](#
- Tasklet调度核心机制
-
- [`tasklet_schedule`: 调度一个Tasklet (公共API)](#
tasklet_schedule
: 调度一个Tasklet (公共API)) - [`__tasklet_schedule`: 调度一个普通优先级的Tasklet](#
__tasklet_schedule
: 调度一个普通优先级的Tasklet) - [`__tasklet_schedule_common`: Tasklet调度的核心实现](#
__tasklet_schedule_common
: Tasklet调度的核心实现)
- [`tasklet_schedule`: 调度一个Tasklet (公共API)](#

kernel/softirq.c 内核中断下半部(Interrupt Bottom-Half) 核心实现
历史与背景
这项技术是为了解决什么特定问题而诞生的?
kernel/softirq.c
实现的软中断(softirq)机制是为了解决一个操作系统内核设计中的核心矛盾:中断处理程序的执行时间必须极短,但中断所触发的工作任务可能很耗时。
- 中断处理的紧迫性:硬件中断发生时,CPU会立即暂停当前工作,跳转到中断处理程序(Interrupt Service Routine, ISR,也称"上半部"/"Top Half")。在执行ISR期间,通常会屏蔽掉当前CPU上的同级甚至所有中断,以保证处理的原子性和快速性。如果ISR执行时间过长,会导致系统无法响应其他新的硬件中断,增加系统延迟(Latency),甚至丢失中断事件。
- 任务的复杂性:然而,中断事件所触发的后续处理可能很复杂。例如,网卡收到一个数据包的中断,其后续处理包括解析协议栈、将数据包递交给上层应用等,这些都是耗时操作。
软中断机制就是为了将中断处理分割为两个部分而设计的:
- 上半部(Top Half / Hard IRQ):在关中断的ISR中执行,只做最紧急的工作,如响应硬件、读取状态、将数据从硬件FIFO拷贝到内存,然后标记一个"软中断"请求。这个过程必须极快。
- 下半部(Bottom Half / Softirq) :在稍后的、更宽松的环境中(中断是打开的)执行那些耗时的任务。
softirq
就是最高性能的一种下半部实现。
它的发展经历了哪些重要的里程碑或版本迭代?
Linux下半部机制经历了显著的演进:
- 早期的BH(Bottom Halves):Linux早期内核有一种BH机制。它很简单,但存在一个致命缺陷:在多处理器(SMP)系统上,同一个BH不能同时在多个CPU上运行,存在全局锁,扩展性极差。
- Softirq和Tasklet的引入 :为了解决SMP扩展性问题,内核引入了
softirq
。softirq
的一个关键设计是,同一种类型的软中断(如网络接收)可以同时在多个CPU上并发执行 ,极大地提升了多核系统的性能。与此同时,为了方便普通驱动开发者,内核在softirq
之上构建了更简单的tasklet
机制。 ksoftirqd
线程的出现 :在高负载情况下(如网络流量风暴),软中断可能被频繁地触发,导致CPU一直在处理软中断而无法执行用户进程,造成用户进程"饥饿"。为了解决这个问题,内核为每个CPU都创建了一个名为ksoftirqd/X
的内核线程。当软中断负载过高时,未处理完的工作会被这个线程接管,由于线程是受调度器管理的,可以保证用户进程也有机会运行。
目前该技术的社区活跃度和主流应用情况如何?
softirq
是Linux内核中断处理和调度的基石,其代码非常成熟、稳定。它不是一个经常变动的功能,而是其他高性能子系统(如网络、定时器)赖以构建的基础。
它的应用场景高度集中在对性能和低延迟要求极高的核心子系统中:
- 网络栈 :几乎所有的网络数据包收发处理(
NET_TX_SOFTIRQ
,NET_RX_SOFTIRQ
)都是通过软中断完成的。 - 定时器子系统 :定时器到期后的回调函数执行是通过
TIMER_SOFTIRQ
触发的。 - 块设备 :I/O操作完成后的处理会通过
BLOCK_SOFTIRQ
进行。
核心原理与设计
它的核心工作原理是什么?
softirq
的核心是一个基于位掩码的、静态定义的、可并发的延迟任务执行框架。
- 静态定义 :内核预定义了少数几种软中断类型(在
enum softirq_action
中),如HI_SOFTIRQ
,TIMER_SOFTIRQ
,NET_RX_SOFTIRQ
等。它们在编译时就已确定,不能在运行时动态添加。 - 触发(Raising) :上半部(硬中断处理程序)在完成其紧急工作后,会调用
raise_softirq(softirq_type)
。这个函数非常轻量,它只是在当前CPU的一个私有变量(一个位掩码)中设置与softirq_type
对应的位。 - 执行(Execution) :内核会在一些特定的、安全的时间点检查这个位掩码,如果发现有挂起的软中断,就会调用
do_softirq()
来执行它们。这些时间点包括:- 从硬中断处理程序返回时。
- 从系统调用返回时。
- 在
ksoftirqd
内核线程中。
do_softirq()
的逻辑 :该函数会检查当前CPU的软中断挂起位掩码,然后按从高到低的优先级,依次调用预先注册好的处理函数(softirq_action
数组)。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_SOFTIRQ
和TASKLET_SOFTIRQ
这两个软中断向量,并增加了一个锁来保证同类tasklet的串行执行,从而为驱动开发者提供了更简单的并发模型。
kernel/softirq.c
invoke_softirq 触发软中断的执行
c
/*
* 这是一个静态内联函数,用于触发软中断的执行。
* 它在硬中断退出路径上被调用。
*/
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 退出中断上下文
c
/**
* 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 软中断类型
c
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 软中断初始化
c
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 内核线程
c
/*
* 这是一个静态函数,作为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
- 这两个函数用于在处理软中断时禁用和启用本地
c
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),直到所有挂起的软中断都被处理完毕。
c
/*
* 我们最多重新启动软中断处理 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 不在内核线程中处理软中断
c
asmlinkage __visible void __softirq_entry __do_softirq(void)
{
handle_softirqs(false);
}
__raise_softirq_irqoff 设置指定类型的软中断(SoftIRQ)为待处理状态
c
void __raise_softirq_irqoff(unsigned int nr)
{
/* 确保当前运行环境中断已被禁用 */
lockdep_assert_irqs_disabled();
trace_softirq_raise(nr);
/* 将指定类型的软中断标记为待处理状态。 */
or_softirq_pending(1UL << nr);
}
wakeup_softirqd 唤醒 ksoftirqd 内核线程
c
/*
* 我们不能在这里无限循环以避免用户空间匮乏,
* 但我们也不想给待处理事件引入最坏情况的 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)
c
/*
* 这个函数必须在禁用 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 用于处理定时器中断的内核线程
c
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 内核线程
c
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来使用这个端口的引脚了。
具体执行步骤如下:
- 硬件准备 : 它首先将GPIO端口的硬件脱离复位状态(
reset_control_deassert
), 然后解析设备树以获取该端口寄存器的物理地址, 并通过devm_ioremap_resource
将其映射到内核的虚拟地址空间, 使得CPU可以访问。 gpio_chip
结构体填充 : 这是核心步骤,gpio_chip
是gpiolib框架理解一个GPIO控制器的标准"名片"。- 它从一个通用模板开始, 然后用设备树中定义的具体信息(如端口名称
st,bank-name
)来填充。 - 它解析
gpio-ranges
属性来确定该端口的引脚在Linux全局GPIO编号空间中的起始编号。如果该属性不存在, 它会采用一种旧的、基于端口顺序的编号方案, 并手动调用pinctrl_add_gpio_range
来建立pinctrl和gpiolib之间的映射关系。 - 它设置该
gpio_chip
包含的引脚数量(ngpio
, 通常是16)、父设备等信息。
- 它从一个通用模板开始, 然后用设备树中定义的具体信息(如端口名称
- 中断体系建立 : 如果pinctrl支持中断, 它会为当前这个GPIO端口创建一个层级式中断域 (
irq_domain_create_hierarchy
)。这会将该端口的中断处理能力链接到上一层的EXTI(外部中断)控制器中断域。当中断发生时, 中断信号会沿着这个层级正确地上传和处理。 - 引脚命名 : 它会遍历该端口的所有引脚(0-15), 从pinctrl驱动的引脚数据库中查出每个引脚的名称(如 "PA0", "PA1"), 并将这个名称列表关联到
gpio_chip
。这对于调试和用户空间通过sysfs
查看信息非常有用。 - 最终注册 : 万事俱备后, 它调用
gpiochip_add_data
, 将完全配置好的gpio_chip
注册到内核。bank
指针作为私有数据被一同传入, 这样当gpiolib调用此芯片的操作函数(如.set
,.get
)时, 这些函数可以方便地访问到该端口的寄存器基地址和锁等私有信息。
c
/*
* 静态函数声明: 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
进行正确的初始化。它是一个准备步骤, 将结构体设置为一个已知的、干净的状态。
c
/*
* 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在尝试访问这些无效的资源, 从而导致系统崩溃。
c
/*
* 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被频繁地、连续地调度, 它也只会被添加到执行队列中一次。这可以防止队列被同一个待办任务淹没, 是一种至关重要的优化。
c
/*
* 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
)。
c
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, 从而导致对队列的竞争和破坏。
c
/*
* __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);
}