[Linux]学习笔记系列 -- [kernel][irq]softirq


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调度核心机制
      • [`tasklet_schedule`: 调度一个Tasklet (公共API)](#tasklet_schedule: 调度一个Tasklet (公共API))
      • [`__tasklet_schedule`: 调度一个普通优先级的Tasklet](#__tasklet_schedule: 调度一个普通优先级的Tasklet)
      • [`__tasklet_schedule_common`: Tasklet调度的核心实现](#__tasklet_schedule_common: Tasklet调度的核心实现)

https://github.com/wdfk-prog/linux-study

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 触发软中断的执行

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来使用这个端口的引脚了。

具体执行步骤如下:

  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)时, 这些函数可以方便地访问到该端口的寄存器基地址和锁等私有信息。
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);
}
相关推荐
摇滚侠3 小时前
Spring Boot 3零基础教程,WEB 开发 内容协商机制 笔记34
java·spring boot·笔记·缓存
迎風吹頭髮3 小时前
Linux服务器编程实践60-双向管道:socketpair函数的实现与应用场景
linux·运维·服务器
71-33 小时前
C语言——关机小程序(有system()和strcmp()函数的知识点)
c语言·笔记·学习
试试勇气3 小时前
Linux学习笔记(九)--Linux进程终止与进程等待
linux·笔记·学习
淮北4943 小时前
立创EDA学习(一、新建项目与自定义元件)
学习
Json____4 小时前
学习springBoot框架-开发一个酒店管理系统,来熟悉springboot框架语法~
spring boot·后端·学习
wheeldown4 小时前
【Linux】Linux 进程信号核心拆解:pending/block/handler 三张表 + signal/alarm 实战
linux·运维·服务器
运维老司机4 小时前
ThinkPad 安装 Ubuntu 系统教程
linux·运维·ubuntu
云飞云共享云桌面5 小时前
替代传统电脑的共享云服务器如何实现1拖8SolidWorks设计办公
linux·运维·服务器·网络·电脑·制造