Linux 内核中断处理机制详解:上半部、下半部与延迟执行策略

Linux 内核中断处理机制详解:上半部、下半部与延迟执行策略


目录

  1. 引言与设计动机
  2. [中断上半部(Top Half)](#中断上半部(Top Half))
  3. [中断下半部(Bottom Half)](#中断下半部(Bottom Half))
  4. 软中断(Softirq)
  5. Tasklet(小任务)
  6. 工作队列(Workqueue)
  7. [中断线程化(Threaded IRQ)](#中断线程化(Threaded IRQ))
  8. 各机制对比与选型指南
  9. 实战代码示例
  10. 总结

1. 引言与设计动机

1.1 中断处理面临的核心矛盾

在 Linux 内核中,中断处理程序面临两个看似矛盾的需求:

  • 快速响应:中断处理必须尽快完成,否则会影响系统实时性,导致其他中断丢失或系统卡顿。
  • 完成大量工作:实际的中断处理往往涉及数据拷贝、协议解析、设备状态更新等耗时操作。

例如,当网卡接收到数据包时,硬件中断必须立即被响应(清除中断标志、确认硬件状态),但将数据包从 DMA 缓冲区拷贝到内核网络栈、进行协议解析等操作则相对耗时,不适合在中断上下文中完成。

1.2 解决方案:上半部/下半部分离

Linux 内核采用了经典的 "上半部/下半部"(Top Half / Bottom Half) 设计模式:

复制代码
┌─────────────────────────────────────────────────────────┐
│                    硬件中断触发                           │
└───────────────────────────┬─────────────────────────────┘
                            ▼
┌─────────────────────────────────────────────────────────┐
│              上半部(Top Half / Hardirq)                 │
│  • 快速响应,严格限制执行时间(通常 < 100μs)             │
│  • 关闭中断或禁止抢占                                    │
│  • 仅完成时间敏感的关键操作                              │
│  • 例如:清除中断标志、读取硬件状态、触发下半部           │
└───────────────────────────┬─────────────────────────────┘
                            ▼
┌─────────────────────────────────────────────────────────┐
│              下半部(Bottom Half)                        │
│  • 延迟执行,允许更长的处理时间                          │
│  • 可以开启中断(部分机制)                              │
│  • 完成耗时的后续处理工作                                │
│  • 例如:数据拷贝、协议处理、通知用户空间                │
└─────────────────────────────────────────────────────────┘

1.3 历史演进

内核版本 下半部机制
Linux 2.2 及之前 下半部(BH)、任务队列(Task Queue)
Linux 2.4 引入软中断(Softirq)、Tasklet,逐步取代 BH
Linux 2.6 工作队列(Workqueue)取代任务队列;软中断、Tasklet 为主
Linux 2.6.30+ 新增中断线程化(Threaded IRQ)
Linux 5.9+ Tasklet 引入新初始化 API(tasklet_setup),社区酝酿长期移除

2. 中断上半部(Top Half)

2.1 概念

上半部即硬中断处理程序 (Hard IRQ Handler),是中断发生时 CPU 立即执行的代码。它运行在中断上下文中,具有以下严格约束:

约束 说明
❌ 不能睡眠 不能调用 schedule()、msleep()、mutex_lock() 等
❌ 不能访问用户空间 不能调用 copy_to_user()、copy_from_user()
❌ 不能分配可睡眠内存 不能使用 kmalloc(GFP_KERNEL),只能用 GFP_ATOMIC
❌ 执行时间极短 通常要求 < 100μs,工业实时系统要求更严格
✅ 可以访问内核数据 可以操作内核数据结构(需注意并发保护)

2.2 执行流程

复制代码
硬件中断信号 → 中断控制器(GIC/APIC)→ CPU 响应
    → 保存当前上下文(寄存器压栈)
    → 跳转到中断向量表对应的处理入口
    → 执行通用中断处理框架(handle_irq)
    → 调用注册的 ISR(Interrupt Service Routine)
    → 恢复上下文,返回被中断的代码

2.3 注册中断处理程序

c 复制代码
#include <linux/interrupt.h>

/*
 * request_irq - 注册中断处理程序
 * @irq:        中断号
 * @handler:    中断处理函数(上半部)
 * @flags:      标志位(IRQF_SHARED, IRQF_ONESHOT 等)
 * @name:       设备名称(用于 /proc/interrupts 显示)
 * @dev:        设备指针(共享中断时用于区分)
 */
int request_irq(unsigned int irq, irq_handler_t handler,
                unsigned long flags, const char *name, void *dev);

/* 中断处理函数原型 */
typedef irqreturn_t (*irq_handler_t)(int irq, void *dev_id);

/* 返回值 */
#define IRQ_NONE        (0)     /* 不是本设备的中断 */
#define IRQ_HANDLED     (1)     /* 中断已处理 */
#define IRQ_WAKE_THREAD (2)     /* 需要唤醒中断线程(线程化中断) */

2.4 上半部的典型工作

c 复制代码
static irqreturn_t my_device_isr(int irq, void *dev_id)
{
    struct my_device *dev = (struct my_device *)dev_id;
    u32 status;

    /* 1. 读取中断状态寄存器(必须快速) */
    status = readl(dev->base + REG_INT_STATUS);
    if (!status)
        return IRQ_NONE;  /* 不是本设备的中断 */

    /* 2. 清除中断源(防止中断风暴) */
    writel(status, dev->base + REG_INT_CLEAR);

    /* 3. 保存硬件状态/数据到内存 */
    dev->saved_data = readl(dev->base + REG_DATA);

    /* 4. 触发下半部处理 */
    tasklet_schedule(&dev->tasklet);  /* 或 schedule_work(), raise_softirq() 等 */

    return IRQ_HANDLED;
}

3. 中断下半部(Bottom Half)

3.1 概念

下半部是中断处理中可以延迟执行的部分。它的设计目标是:

  • 将耗时操作从中断上下文中移出
  • 在更宽松的执行环境中完成后续处理
  • 保证系统响应性的同时完成必要工作

3.2 下半部的执行时机

下半部通常在以下时机被调度执行:

  1. 硬中断返回时 :do_IRQ() 返回前检查是否有待处理的软中断
  2. 内核线程 ksoftirqd:当软中断数量过多或处理时间过长时,由专用内核线程处理
  3. 显式调用 :代码中主动调用 local_bh_enable() 时触发
  4. 工作队列线程 :由 kworker 内核线程在进程上下文中执行

3.3 下半部的四种实现机制

复制代码
                    下半部实现机制
                         │
        ┌────────────────┼────────────────┐
        │                │                │
   ┌────▼────┐     ┌────▼────┐     ┌────▼────┐
   │ Softirq │     │ Tasklet │     │Workqueue│
   │ (软中断) │     │ (小任务) │     │(工作队列)│
   └────┬────┘     └────┬────┘     └─────────┘
        │                │
        │    ┌───────────┘
        │    │ (Tasklet 基于 Softirq 实现)
        ▼    ▼
   ┌─────────────┐
   │  中断上下文   │  ← 不可睡眠
   └─────────────┘

   ┌─────────────┐
   │  进程上下文   │  ← 可以睡眠(Workqueue, Threaded IRQ)
   └─────────────┘

4. 软中断(Softirq)

4.1 概述

软中断是 Linux 内核中最底层、性能最高的下半部机制。它是静态定义的,数量有限,主要供内核核心子系统使用。

4.2 数据结构

c 复制代码
/* include/linux/interrupt.h */
struct softirq_action {
    void (*action)(struct softirq_action *);
};

/* kernel/softirq.c - 软中断向量表(静态数组) */
static struct softirq_action softirq_vec[NR_SOFTIRQS];  /* NR_SOFTIRQS = 10 */

4.3 内核预定义的软中断类型

c 复制代码
/* include/linux/interrupt.h */
enum {
    HI_SOFTIRQ = 0,         /* 高优先级 tasklet */
    TIMER_SOFTIRQ,          /* 定时器软中断 */
    NET_TX_SOFTIRQ,         /* 网络发送 */
    NET_RX_SOFTIRQ,         /* 网络接收 */
    BLOCK_SOFTIRQ,          /* 块设备完成 */
    IRQ_POLL_SOFTIRQ,       /* IRQ 轮询 */
    TASKLET_SOFTIRQ,        /* 普通优先级 tasklet */
    SCHED_SOFTIRQ,          /* 调度器负载均衡 */
    HRTIMER_SOFTIRQ,        /* 高精度定时器 */
    RCU_SOFTIRQ,            /* RCU 回调 */
    NR_SOFTIRQS             /* 总数 = 10 */
};

注意 :软中断的数量在编译时确定(NR_SOFTIRQS),不可动态增加。这是软中断的主要限制之一。

4.4 软中断的触发与执行

触发机制

每个 CPU 维护一个 32 位的 pending 位图(存储在 per-CPU 的 irq_cpustat_t 结构,即 irq_stat.__softirq_pending 字段),每一位对应一种软中断:

c 复制代码
/* 触发软中断:设置对应 bit */
void raise_softirq(unsigned int nr)
{
    unsigned long flags;
    local_irq_save(flags);
    raise_softirq_irqoff(nr);
    local_irq_restore(flags);
}

void raise_softirq_irqoff(unsigned int nr)
{
    __raise_softirq_irqoff(nr);  /* 设置 pending bit */
    /* 如果不在中断上下文中,唤醒 ksoftirqd */
    if (!in_interrupt())
        wakeup_softirqd();
}
执行流程
c 复制代码
/* kernel/softirq.c */
asmlinkage __visible void __softirq_entry __do_softirq(void)
{
    unsigned long end = jiffies + MAX_SOFTIRQ_TIME;  /* 最大执行时间 2ms */
    int max_restart = MAX_SOFTIRQ_RESTART;           /* 最大重启次数 10 */
    struct softirq_action *h;
    __u32 pending;

    pending = local_softirq_pending();
    /* 清除 pending 标志,防止重入 */
    __local_softirq_pending() = 0;

restart:
    h = softirq_vec;
    while (pending) {
        if (pending & 1) {
            h->action(h);       /* 执行软中断处理函数 */
        }
        h++;
        pending >>= 1;
    }

    /* 检查是否有新的软中断产生 */
    pending = local_softirq_pending();
    if (pending) {
        if (time_before(jiffies, end) && !need_resched() && --max_restart)
            goto restart;       /* 继续处理 */
        wakeup_softirqd();      /* 超时则交给 ksoftirqd 线程 */
    }
}
执行时机
复制代码
┌─────────────────────────────────────────────────────┐
│  硬中断处理完成(do_IRQ 返回前)                      │
│      → 检查 local_softirq_pending()                  │
│      → 若有 pending,调用 __do_softirq()             │
├─────────────────────────────────────────────────────┤
│  local_bh_enable() 被调用时                          │
│      → 检查并执行 pending 的软中断                    │
├─────────────────────────────────────────────────────┤
│  ksoftirqd 内核线程                                  │
│      → 当软中断处理超时或需要调度时                   │
│      → 在进程上下文中执行软中断                      │
└─────────────────────────────────────────────────────┘

4.5 软中断的并发特性

特性 说明
同类型并发 同一类型的软中断可以在不同 CPU 上同时执行
不同类型并发 不同类型的软中断可以并发执行
同 CPU 串行 同一 CPU 上,软中断是串行执行的
可重入 同一类型的软中断处理函数必须是可重入的

4.6 注册与使用(仅供内核核心使用)

c 复制代码
/* 注册软中断(编译时静态注册,通常在子系统初始化时调用) */
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
    softirq_vec[nr].action = action;
}

/* 示例:网络子系统初始化时注册 */
static int __init net_dev_init(void)
{
    open_softirq(NET_TX_SOFTIRQ, net_tx_action);
    open_softirq(NET_RX_SOFTIRQ, net_rx_action);
    return 0;
}

⚠️ 重要 :软中断的注册接口 open_softirq() 虽然可用,但不建议驱动程序使用。软中断是为内核核心子系统(网络、块设备、定时器等)设计的,数量有限且不可扩展。驱动开发者应使用 Tasklet、工作队列或中断线程化。


5. Tasklet(小任务)

5.1 概述

Tasklet 是基于软中断实现的下半部机制,是驱动程序中最常用的软中断级下半部方案(在旧内核中)。它解决了软中断的两个主要问题:

  1. 数量限制:Tasklet 可以动态创建,数量不受限制
  2. 并发复杂性 :同一 Tasklet 在同一时刻只能在一个 CPU 上执行(串行化保证)

5.2 数据结构

c 复制代码
/* include/linux/interrupt.h */
struct tasklet_struct
{
	struct tasklet_struct *next;    /* 链表指针(per-CPU 链表) */
	unsigned long state;            /* 状态:TASKLET_STATE_SCHED / TASKLET_STATE_RUN */
	atomic_t count;                 /* 引用计数:0=启用,>0=禁用 */
	bool use_callback;              /* 是否使用callback */
	union {
		void (*func)(unsigned long data); /* 处理函数(旧版) */
		void (*callback)(struct tasklet_struct *t); /* 新版内核(5.9+)使用 callback 风格: */
	};
	unsigned long data;             /* 传递给处理函数的参数 */
};

5.3 实现原理

Tasklet 本质上是对 TASKLET_SOFTIRQ 和 HI_SOFTIRQ 两种软中断的封装:

复制代码
┌──────────────────────────────────────────────────────────┐
│                    Tasklet 架构                           │
├──────────────────────────────────────────────────────────┤
│                                                          │
│  tasklet_schedule()                                      │
│       │                                                  │
│       ▼                                                  │
│  将 tasklet 加入 per-CPU 链表                             │
│       │                                                  │
│       ▼                                                  │
│  raise_softirq(TASKLET_SOFTIRQ)                          │
│       │                                                  │
│       ▼                                                  │
│  软中断执行时调用 tasklet_action()                        │
│       │                                                  │
│       ▼                                                  │
│  遍历 per-CPU 链表,逐个执行 tasklet->func()             │
│                                                          │
└──────────────────────────────────────────────────────────┘
核心执行函数
c 复制代码
/* kernel/softirq.c */
static void tasklet_action(struct softirq_action *a)
{
    struct tasklet_struct *list;

    /* 取出当前 CPU 的 tasklet 链表 */
	local_irq_disable();
	list = tl_head->head;
	tl_head->head = NULL;
	tl_head->tail = &tl_head->head;
	local_irq_enable();

    while (list) {
        struct tasklet_struct *t = list;
        list = list->next;

        /* 尝试获取执行权(保证同一 tasklet 不会并发执行) */
        if (tasklet_trylock(t)) {
            if (!atomic_read(&t->count)) {
                if (test_and_clear_bit(TASKLET_STATE_SCHED, &t->state)) {
                    /* 执行 tasklet 处理函数(区分新旧 API) */
                    if (t->use_callback)
                        t->callback(t);
                    else
                        t->func(t->data);
                    tasklet_unlock(t);
                    continue;
                }
            }
            tasklet_unlock(t);
        }

        /* 如果获取失败,重新加入链表等待下次执行 */
		local_irq_disable();
		t->next = NULL;
		*tl_head->tail = t;
		tl_head->tail = &t->next;
		__raise_softirq_irqoff(TASKLET_SOFTIRQ);
		local_irq_enable();
    }
}

5.4 状态机

复制代码
                    tasklet_schedule()
                           │
                           ▼
              ┌────────────────────────┐
              │  TASKLET_STATE_SCHED   │  ← 已调度,等待执行
              └────────────┬───────────┘
                           │ tasklet_action() 获取锁
                           ▼
              ┌────────────────────────┐
              │  TASKLET_STATE_RUN     │  ← 正在执行
              └────────────┬───────────┘
                           │ func() 执行完毕
                           ▼
              ┌────────────────────────┐
              │       IDLE             │  ← 空闲,可再次调度
              └────────────────────────┘

5.5 API 使用

c 复制代码
#include <linux/interrupt.h>

/* ========== 方式一:静态初始化 ========== */
/* 旧版 API(5.9 之前,func 风格,参数通过 data 传递) */
DECLARE_TASKLET_OLD(my_tasklet, my_tasklet_func, my_data);
/* 新版 API(5.9+,callback 风格,宿主结构体指针用 from_tasklet 获取) */
DECLARE_TASKLET(my_tasklet, my_tasklet_callback);

/* ========== 方式二:动态初始化 ========== */
struct tasklet_struct my_tasklet;

/* 旧版 API(5.9 之前,正被 tasklet_setup 取代) */
tasklet_init(&my_tasklet, my_tasklet_func, (unsigned long)my_data);

/* 新版 API(5.9+,推荐) */
tasklet_setup(&my_tasklet, my_tasklet_callback);

/* ========== 处理函数 ========== */
/* 旧版 */
void my_tasklet_func(unsigned long data)
{
    struct my_device *dev = (struct my_device *)data;
    /* 处理逻辑... */
}

/* 新版(推荐) */
void my_tasklet_callback(struct tasklet_struct *t)
{
    struct my_device *dev = from_tasklet(dev, t, my_tasklet);
    /* 处理逻辑... */
}

/* ========== 调度与控制 ========== */
tasklet_schedule(&my_tasklet);        /* 调度执行 */
tasklet_hi_schedule(&my_tasklet);     /* 高优先级调度 */
tasklet_disable(&my_tasklet);         /* 禁用(count++,等待执行完毕) */
tasklet_enable(&my_tasklet);          /* 启用(count--) */
tasklet_kill(&my_tasklet);            /* 杀死(等待执行完毕,防止重调度) */

5.6 Tasklet 的特性总结

特性 说明
执行上下文 软中断上下文(中断上下文)
能否睡眠 ❌ 不能
并发性 同一 tasklet 不会并发执行(串行化)
不同 tasklet 不同 tasklet 可以在不同 CPU 上并发执行
优先级 普通(TASKLET_SOFTIRQ)或高(HI_SOFTIRQ)
性能 高(仅次于直接使用软中断)
适用场景 短小、不可睡眠、需要串行化的下半部任务

5.7 Tasklet 的 API 演进与长期趋势

从 Linux 5.9 开始,内核社区对 Tasklet 的初始化 API 进行了现代化改造(并非立即废弃):

  • 新 API tasklet_setup() 取代旧的 tasklet_init(),回调改为 callback(struct tasklet_struct *) 风格
  • 旧的三参数 DECLARE_TASKLET 宏被改名为 DECLARE_TASKLET_OLD,逐步停止使用
  • 社区存在长期移除 Tasklet 的讨论(如 2024 年 LWN 文章 "The end of tasklets"),但截至 6.x,Tasklet 仍被网络、块设备等核心子系统广泛使用
  • 原因:Tasklet 运行在中断上下文,限制较多;但其轻量、不可睡眠的延迟执行语义在部分场景仍有价值

6. 工作队列(Workqueue)

6.1 概述

工作队列是运行在进程上下文 中的下半部机制。与软中断和 Tasklet 不同,工作队列由专用的内核线程(kworker)执行,因此:

  • ✅ 可以睡眠
  • ✅ 可以调度
  • ⚠️ 不能直接访问用户空间:kworker 是内核线程(current->mm == NULL),调用 copy_to_user() 非法,应通过等待队列/poll 通知用户进程
  • ✅ 可以分配 GFP_KERNEL 内存
  • ❌ 延迟相对较高(需要线程调度)

6.2 架构

复制代码
┌─────────────────────────────────────────────────────────────┐
│                    Workqueue 架构                            │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌──────────┐    ┌──────────┐    ┌──────────┐              │
│  │ work_struct│    │ work_struct│    │ work_struct│  ← 工作项 │
│  └─────┬────┘    └─────┬────┘    └─────┬────┘              │
│        │               │               │                    │
│        ▼               ▼               ▼                    │
│  ┌─────────────────────────────────────────────┐           │
│  │              workqueue_struct                 │  ← 工作队列│
│  │         (链表管理所有 pending work)           │           │
│  └─────────────────────┬───────────────────────┘           │
│                        │                                    │
│                        ▼                                    │
│  ┌─────────────────────────────────────────────┐           │
│  │           worker_pool (per-CPU)              │           │
│  │  ┌─────────┐ ┌─────────┐ ┌─────────┐       │           │
│  │  │ kworker │ │ kworker │ │ kworker │  ...   │  ← 内核线程│
│  │  │  /0:0   │ │  /0:1   │ │  /1:0   │       │           │
│  │  └─────────┘ └─────────┘ └─────────┘       │           │
│  └─────────────────────────────────────────────┘           │
│                                                             │
└─────────────────────────────────────────────────────────────┘

6.3 数据结构

c 复制代码
/* include/linux/workqueue.h */
struct work_struct {
    atomic_long_t data;               /* 内部状态数据 */
    struct list_head entry;           /* 链表节点 */
    work_func_t func;                 /* 工作函数 */
};

typedef void (*work_func_t)(struct work_struct *work);

/* 延迟工作 */
struct delayed_work {
    struct work_struct work;
    struct timer_list timer;          /* 用于延迟执行的定时器 */
    struct workqueue_struct *wq;
    int cpu;
};

6.4 API 使用

c 复制代码
#include <linux/workqueue.h>

/* ========== 1. 定义工作项 ========== */
struct my_device {
    struct work_struct work;          /* 嵌入 work_struct */
    struct delayed_work dwork;        /* 或延迟工作 */
    /* ... 其他成员 ... */
};

/* ========== 2. 工作函数 ========== */
static void my_work_func(struct work_struct *work)
{
    /* 通过 container_of 获取宿主结构体 */
    struct my_device *dev = container_of(work, struct my_device, work);

    /* 可以睡眠! */
    msleep(100);
    mutex_lock(&dev->lock);
    /* 复杂处理逻辑... */
    mutex_unlock(&dev->lock);
}

/* ========== 3. 初始化 ========== */
/* 静态初始化 */
DECLARE_WORK(my_work, my_work_func);

/* 动态初始化 */
INIT_WORK(&dev->work, my_work_func);
INIT_DELAYED_WORK(&dev->dwork, my_delayed_work_func);

/* ========== 4. 调度执行 ========== */
/* 使用系统默认工作队列(events) */
schedule_work(&dev->work);
schedule_delayed_work(&dev->dwork, msecs_to_jiffies(100));  /* 延迟 100ms */

/* ========== 5. 创建自定义工作队列 ========== */
struct workqueue_struct *my_wq;
my_wq = create_singlethread_workqueue("my_wq");    /* 单线程 */
/* 或 */
my_wq = alloc_workqueue("my_wq", WQ_UNBOUND | WQ_HIGHPRI, 0);  /* 高级配置 */

queue_work(my_wq, &dev->work);
queue_delayed_work(my_wq, &dev->dwork, delay);

/* ========== 6. 同步与取消 ========== */
flush_work(&dev->work);              /* 等待工作完成 */
flush_workqueue(my_wq);              /* 等待队列中所有工作完成 */
cancel_work_sync(&dev->work);        /* 取消并等待完成 */
cancel_delayed_work_sync(&dev->dwork);

/* ========== 7. 销毁 ========== */
destroy_workqueue(my_wq);

6.5 系统默认工作队列

内核预定义了多个全局工作队列:

工作队列 说明
system_wq 默认工作队列,schedule_work() 使用
system_highpri_wq 高优先级工作队列
system_long_wq 适合长时间运行的工作
system_unbound_wq 不绑定 CPU 的工作队列
system_freezable_wq 系统挂起时可冻结的工作队列

6.6 工作队列的标志位

c 复制代码
enum {
    WQ_UNBOUND        = 1 << 1,  /* 不绑定特定 CPU */
    WQ_FREEZABLE      = 1 << 2,  /* 系统休眠时可冻结 */
    WQ_MEM_RECLAIM    = 1 << 3,  /* 内存回收路径可用 */
    WQ_HIGHPRI        = 1 << 4,  /* 高优先级执行 */
    WQ_CPU_INTENSIVE  = 1 << 5,  /* CPU 密集型,不阻塞其他工作 */
    WQ_SYSFS          = 1 << 6,  /* 在 sysfs 中可见 */
};

6.7 工作队列的特性总结

特性 说明
执行上下文 进程上下文(内核线程)
能否睡眠 ✅ 可以
能否调度 ✅ 可以
延迟 相对较高(依赖线程调度)
并发性 默认 per-CPU 绑定,可配置为 unbound
适用场景 需要睡眠、耗时较长、需要进程上下文的下半部任务

7. 中断线程化(Threaded IRQ)

7.1 概述

中断线程化是 Linux 2.6.30 引入的机制(由 Thomas Gleixner 主导),它将中断处理函数的一部分或全部放到内核线程中执行。这是 PREEMPT_RT 补丁的核心特性之一,现已合入主线内核。

7.2 设计思想

复制代码
传统模型:
  硬中断上下文 → ISR(不可睡眠)→ 下半部(softirq/tasklet/workqueue)

线程化模型:
  硬中断上下文 → 快速 handler(仅确认中断)→ 唤醒内核线程 → 线程中执行 thread_fn(可睡眠)

7.3 注册接口

c 复制代码
#include <linux/interrupt.h>

/**
 * request_threaded_irq - 注册线程化中断
 * @irq:         中断号
 * @handler:     硬中断处理函数(上半部,在硬中断上下文执行)
 * @thread_fn:   线程处理函数(下半部,在内核线程中执行)
 * @irqflags:    标志位
 * @devname:     设备名
 * @dev_id:      设备指针
 *
 * 返回值:0 成功,负值失败
 */
int request_threaded_irq(unsigned int irq,
                         irq_handler_t handler,
                         irq_handler_t thread_fn,
                         unsigned long irqflags,
                         const char *devname,
                         void *dev_id);

7.4 执行流程

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    线程化中断执行流程                             │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  硬件中断触发                                                    │
│       │                                                         │
│       ▼                                                         │
│  ┌─────────────────────────────────────┐                       │
│  │  handler()(硬中断上下文)            │                       │
│  │  • 确认中断                          │                       │
│  │  • 清除中断源(如需要)              │                       │
│  │  • 返回 IRQ_WAKE_THREAD             │                       │
│  └──────────────────┬──────────────────┘                       │
│                     │                                           │
│                     ▼                                           │
│  ┌─────────────────────────────────────┐                       │
│  │  唤醒内核线程 irq/%d-%s             │                       │
│  │  (如:irq/32-eth0)                 │                       │
│  └──────────────────┬──────────────────┘                       │
│                     │                                           │
│                     ▼                                           │
│  ┌─────────────────────────────────────┐                       │
│  │  thread_fn()(进程上下文)           │                       │
│  │  • 可以睡眠 ✅                       │                       │
│  │  • 可以调度 ✅                       │                       │
│  │  • 不能直接访问用户空间 ❌           │                       │
│  │  • 执行耗时的中断处理逻辑            │                       │
│  │  • 返回 IRQ_HANDLED / IRQ_NONE      │                       │
│  └─────────────────────────────────────┘                       │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

7.5 代码示例

c 复制代码
#include <linux/interrupt.h>
#include <linux/module.h>

struct my_device {
    int irq;
    void __iomem *base;
    struct mutex lock;
    u32 rx_count;
};

/*
 * 硬中断处理函数(上半部)
 * 运行在硬中断上下文,必须快速
 */
static irqreturn_t my_hardirq_handler(int irq, void *dev_id)
{
    struct my_device *dev = dev_id;
    u32 status;

    status = readl(dev->base + REG_INT_STATUS);
    if (!status)
        return IRQ_NONE;

    /* 清除中断源 */
    writel(status, dev->base + REG_INT_CLEAR);

    /* 返回 IRQ_WAKE_THREAD 表示需要执行线程化处理 */
    return IRQ_WAKE_THREAD;
}

/*
 * 线程化处理函数(下半部)
 * 运行在内核线程中,可以睡眠
 */
static irqreturn_t my_threaded_handler(int irq, void *dev_id)
{
    struct my_device *dev = dev_id;

    /* 可以睡眠! */
    mutex_lock(&dev->lock);

    /* 复杂的数据处理 */
    dev->rx_count++;
    /* 可以调用 kmalloc(GFP_KERNEL)、msleep() 等(copy_to_user() 不可用) */

    mutex_unlock(&dev->lock);

    return IRQ_HANDLED;
}

static int my_probe(struct platform_device *pdev)
{
    struct my_device *dev;
    int ret;

    dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
    dev->irq = platform_get_irq(pdev, 0);

    /* 注册线程化中断 */
    ret = devm_request_threaded_irq(&pdev->dev,
                                    dev->irq,
                                    my_hardirq_handler,    /* 硬中断 handler */
                                    my_threaded_handler,   /* 线程化 handler */
                                    IRQF_ONESHOT,          /* 重要标志! */
                                    "my_device",
                                    dev);
    if (ret) {
        dev_err(&pdev->dev, "Failed to request IRQ\n");
        return ret;
    }

    return 0;
}

7.6 关键标志:IRQF_ONESHOT

使用线程化中断时,必须 (在大多数情况下)设置 IRQF_ONESHOT 标志:

c 复制代码
#define IRQF_ONESHOT  0x00002000

原因 :在线程化中断中,硬中断 handler 返回后中断线不会被立即重新使能(因为线程还在处理中)。IRQF_ONESHOT 告诉内核:在线程处理完成之前,保持中断线禁用,防止中断重入。

7.7 handler 为 NULL 的情况

如果不需要硬中断 handler(例如中断控制器已经处理了确认工作),可以将 handler 设为 NULL:

c 复制代码
ret = request_threaded_irq(irq, NULL, my_thread_fn,
                           IRQF_ONESHOT, "my_dev", dev);

此时内核会使用默认的 irq_default_primary_handler(),它直接返回 IRQ_WAKE_THREAD。

7.8 线程化中断的内核线程

每个线程化中断会创建一个专用的内核线程:

bash 复制代码
$ ps -eLf | grep irq
root  1234  2  0  irq/32-eth0        # 中断号32,设备名eth0
root  1235  2  0  irq/33-i2c0
root  1236  2  0  irq/45-gpio-keys

可以通过 /proc/interrupts 查看线程化中断的信息。

7.9 线程化中断的特性总结

特性 说明
执行上下文 进程上下文(专用内核线程)
能否睡眠 ✅ 可以
能否调度 ✅ 可以(线程有优先级,可被调度)
延迟 中等(比工作队列低,比软中断高)
实时性 可设置线程优先级(SCHED_FIFO),适合 RT 场景
适用场景 需要进程上下文、对实时性有要求的中断处理

8. 各机制对比与选型指南

8.1 综合对比表

特性 软中断 (Softirq) Tasklet 工作队列 (Workqueue) 中断线程化 (Threaded IRQ)
执行上下文 中断上下文 中断上下文 进程上下文 进程上下文
能否睡眠 ❌ ❌ ✅ ✅
能否调度 ❌ ❌ ✅ ✅
执行延迟 极低 低 中等 中等
并发模型 同类型可并发 同实例串行 可配置 单线程串行
数量限制 固定(10个) 无限制 无限制 每个 IRQ 一个
使用者 内核核心子系统 驱动程序(旧) 驱动程序/子系统 驱动程序
优先级控制 固定 高/普通两级 可配置 可设置 RT 优先级
性能 ★★★★★ ★★★★☆ ★★★☆☆ ★★★☆☆
灵活性 ★☆☆☆☆ ★★★☆☆ ★★★★★ ★★★★☆

8.2 选型决策树

复制代码
需要在中断后延迟处理任务?
│
├─ 你在编写内核核心子系统(网络/块设备/定时器等)?
│   └─ YES → 使用 Softirq(仅限内核核心,驱动不能新增软中断)
│
├─ 任务需要睡眠/可能阻塞?
│   │
│   ├─ 由本设备中断直接触发,对延迟/实时性要求高?
│   │   └─ YES → 使用中断线程化(Threaded IRQ)
│   │
│   └─ 对延迟要求一般,或需从任意上下文调度?
│       └─ YES → 使用工作队列(Workqueue)
│
├─ 任务不需要睡眠,且要求低延迟?
│   │
│   ├─ 需要同一实例串行化?
│   │   └─ YES → Tasklet(旧内核);新内核优先中断线程化(天然串行),
│   │             其次工作队列(同一 work 项天然不重入)
│   │
│   └─ 不需要串行化?
│       └─ 中断线程化或工作队列(软中断级延迟不对驱动开放)
│
└─ 不确定?
    └─ 优先选择工作队列(最通用、最安全)

⚠️ 关于"访问用户空间" :无论工作队列还是中断线程化,下半部都运行在内核线程

上下文(current->mm == NULL),不能直接调用 copy_to_user()/copy_from_user() 。

需要与用户空间交换数据时,应通过等待队列/poll 唤醒用户进程,由系统调用路径完成拷贝。

内核线程确需访问用户内存时,必须先用 kthread_use_mm() 借用进程地址空间

(如 USB Function FS 驱动的做法)。

8.3 现代内核的推荐实践

场景 推荐方案
新驱动开发 中断线程化 或 工作队列
需要睡眠的操作(I2C/SPI 通信、内存分配) 工作队列 或 中断线程化
高频短任务(网络收包) Softirq(内核已实现)
旧驱动维护 Tasklet 仍可使用,但新代码避免使用
PREEMPT_RT 实时系统 中断线程化(几乎所有中断都被线程化)

9. 实战代码示例

9.1 完整驱动示例:对比三种下半部机制

c 复制代码
// SPDX-License-Identifier: GPL-2.0
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/interrupt.h>
#include <linux/workqueue.h>
#include <linux/slab.h>
#include <linux/io.h>

#define REG_INT_STATUS  0x00
#define REG_INT_CLEAR   0x04
#define REG_DATA        0x08

struct demo_device {
    struct device *dev;
    void __iomem *base;
    int irq;

    /* 方式一:Tasklet */
    struct tasklet_struct tasklet;

    /* 方式二:工作队列 */
    struct work_struct work;
    struct workqueue_struct *wq;

    /* 方式三:中断线程化(无需额外结构) */

    /* 共享数据 */
    spinlock_t lock;
    u32 rx_data;
    u32 rx_count;
};

/* ==================== 方式一:Tasklet ==================== */

static void demo_tasklet_func(struct tasklet_struct *t)
{
    struct demo_device *demo = from_tasklet(demo, t, tasklet);
    unsigned long flags;

    spin_lock_irqsave(&demo->lock, flags);
    demo->rx_count++;
    /* 注意:这里不能睡眠! */
    dev_dbg(demo->dev, "Tasklet: processed %u packets\n", demo->rx_count);
    spin_unlock_irqrestore(&demo->lock, flags);
}

static irqreturn_t demo_isr_tasklet(int irq, void *dev_id)
{
    struct demo_device *demo = dev_id;
    u32 status;

    status = readl(demo->base + REG_INT_STATUS);
    if (!status)
        return IRQ_NONE;

    writel(status, demo->base + REG_INT_CLEAR);
    demo->rx_data = readl(demo->base + REG_DATA);

    /* 调度 tasklet */
    tasklet_schedule(&demo->tasklet);

    return IRQ_HANDLED;
}

/* ==================== 方式二:工作队列 ==================== */

static void demo_work_func(struct work_struct *work)
{
    struct demo_device *demo = container_of(work, struct demo_device, work);
    unsigned long flags;

    /* 进程上下文,可以睡眠,但持锁期间不能睡眠 */
    spin_lock_irqsave(&demo->lock, flags);
    demo->rx_count++;
    dev_dbg(demo->dev, "Workqueue: processed %u packets\n", demo->rx_count);
    spin_unlock_irqrestore(&demo->lock, flags);

    /* 模拟耗时操作(睡眠必须放在锁外) */
    msleep(10);  /* 合法! */
}

static irqreturn_t demo_isr_workqueue(int irq, void *dev_id)
{
    struct demo_device *demo = dev_id;
    u32 status;

    status = readl(demo->base + REG_INT_STATUS);
    if (!status)
        return IRQ_NONE;

    writel(status, demo->base + REG_INT_CLEAR);
    demo->rx_data = readl(demo->base + REG_DATA);

    /* 调度工作队列 */
    queue_work(demo->wq, &demo->work);

    return IRQ_HANDLED;
}

/* ==================== 方式三:中断线程化 ==================== */

static irqreturn_t demo_hardirq(int irq, void *dev_id)
{
    struct demo_device *demo = dev_id;
    u32 status;

    status = readl(demo->base + REG_INT_STATUS);
    if (!status)
        return IRQ_NONE;

    writel(status, demo->base + REG_INT_CLEAR);

    return IRQ_WAKE_THREAD;  /* 唤醒线程 */
}

static irqreturn_t demo_thread_fn(int irq, void *dev_id)
{
    struct demo_device *demo = dev_id;

    /* 进程上下文,可以睡眠 */
    demo->rx_data = readl(demo->base + REG_DATA);
    demo->rx_count++;

    dev_dbg(demo->dev, "Threaded IRQ: processed %u packets\n", demo->rx_count);

    /* 可以执行任何可睡眠的进程上下文操作 */
    // kmalloc(size, GFP_KERNEL);
    // msleep(...);
    /* copy_to_user() 不可用:中断线程无用户地址空间,需经等待队列/poll 转交 */

    return IRQ_HANDLED;
}

/* ==================== Probe / Remove ==================== */

static int demo_probe(struct platform_device *pdev)
{
    struct demo_device *demo;
    int ret;

    demo = devm_kzalloc(&pdev->dev, sizeof(*demo), GFP_KERNEL);
    if (!demo)
        return -ENOMEM;

    demo->dev = &pdev->dev;
    demo->base = devm_platform_ioremap_resource(pdev, 0);
    if (IS_ERR(demo->base))
        return PTR_ERR(demo->base);

    demo->irq = platform_get_irq(pdev, 0);
    if (demo->irq < 0)
        return demo->irq;

    spin_lock_init(&demo->lock);
    platform_set_drvdata(pdev, demo);

    /* --- 选择一种方式注册中断 --- */

    /* 方式一:Tasklet */
    tasklet_setup(&demo->tasklet, demo_tasklet_func);
    ret = devm_request_irq(&pdev->dev, demo->irq,
                           demo_isr_tasklet, 0, "demo_tasklet", demo);

    /* 方式二:工作队列 */
    // demo->wq = alloc_workqueue("demo_wq", WQ_HIGHPRI, 0);
    // INIT_WORK(&demo->work, demo_work_func);
    // ret = devm_request_irq(&pdev->dev, demo->irq,
    //                        demo_isr_workqueue, 0, "demo_wq", demo);

    /* 方式三:中断线程化 */
    // ret = devm_request_threaded_irq(&pdev->dev, demo->irq,
    //                                 demo_hardirq, demo_thread_fn,
    //                                 IRQF_ONESHOT, "demo_threaded", demo);

    if (ret) {
        dev_err(&pdev->dev, "Failed to request IRQ: %d\n", ret);
        return ret;
    }

    dev_info(&pdev->dev, "Demo device probed successfully\n");
    return 0;
}

static void demo_remove(struct platform_device *pdev)
{
    struct demo_device *demo = platform_get_drvdata(pdev);

    tasklet_kill(&demo->tasklet);
    /* 或 */
    // cancel_work_sync(&demo->work);
    // destroy_workqueue(demo->wq);

    dev_info(&pdev->dev, "Demo device removed\n");
}

static struct platform_driver demo_driver = {
    .probe  = demo_probe,
    .remove = demo_remove,
    .driver = {
        .name = "demo-device",
    },
};
module_platform_driver(demo_driver);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Demo: Bottom Half Mechanisms Comparison");

9.2 查看系统中断信息

bash 复制代码
# 查看中断统计
$ cat /proc/interrupts
           CPU0       CPU1
  0:         42          0   GICv2  27 Level     arch_timer
 32:        156         89   GICv2  44 Level     eth0
 33:         12          0   GICv2  45 Level     i2c0

# 查看软中断统计
$ cat /proc/softirqs
                    CPU0       CPU1
          HI:          0          0
       TIMER:       1234       1198
      NET_TX:         45         32
      NET_RX:       5678       5432
       BLOCK:        123        119
    IRQ_POLL:         0          0
     TASKLET:         89         76
       SCHED:        456        432
     HRTIMER:         12         10
         RCU:       2345       2298

# 查看中断线程
$ ps -eLf | grep "irq/"
root  1234  2  0  irq/32-eth0
root  1235  2  0  irq/33-i2c0

# 查看 ksoftirqd 线程
$ ps -eLf | grep ksoftirqd
root    10  2  0  ksoftirqd/0
root    11  2  0  ksoftirqd/1

# 查看 kworker 线程
$ ps -eLf | grep kworker
root   100  2  0  kworker/0:1-events
root   101  2  0  kworker/1:0-events

10. 总结

10.1 核心要点回顾

  1. 上半部/下半部分离是 Linux 中断处理的核心设计模式,目的是在保证快速响应的同时完成复杂的后续处理。

  2. 软中断是最底层的下半部机制,性能最高但灵活性最低,仅供内核核心子系统使用。

  3. Tasklet 是软中断的封装,提供串行化保证和动态创建能力,但运行在中断上下文,不可睡眠。新内核(5.9+)对其 API 做了现代化改造,社区有长期移除的讨论,目前仍被广泛使用。

  4. 工作队列运行在进程上下文,是最灵活的下半部机制,可以睡眠、调度(但不能直接访问用户空间),适合大多数驱动场景。

  5. 中断线程化将中断处理直接放入内核线程,兼具低延迟和可睡眠的优势,是 PREEMPT_RT 实时系统的基石,也是现代驱动开发的推荐方案。

10.2 一句话总结

能快则快(Softirq/Tasklet),需睡则睡(Workqueue/Threaded IRQ),新代码优先选择中断线程化或工作队列。


本文基于 Linux Kernel 5.x/6.x 版本撰写,部分 API 在更新版本中可能有变化,请以实际内核源码为准。

相关推荐
s6516654968 小时前
IDT和GDT是什么
linux
Android系统攻城狮9 小时前
Linux Gstreamer深度解析之gst_audio_decoder_set_estimate_rate调用流程与实战(四十五)
linux·gstreamer音视频·音视频进阶
FED_AF9 小时前
Linux运维“邪修”功法之通配符
linux·运维
箓维9 小时前
线程的概念和理解
linux
云杂项9 小时前
tmux指南:安装、配置与高效使用(Ubuntu上)
linux·服务器·ubuntu
j7~9 小时前
【Linux网络编程】四十七.《Linux IO 模型详解:阻塞 IO、非阻塞 IO 与 IO 多路转接(select)》
linux·运维·网络·select·非阻塞io·阻塞io·i/o多路连接
追涨杀跌老能手10 小时前
GD32H759 + RT-Thread 工控实战--第7篇 OSPI Flash
开源·嵌入式
GeW10 小时前
智能工厂背后:Linux在跑,数据库在扛,高端制造在加速
linux
qq_3945620010 小时前
【在Linux上升级nginx】
linux·服务器·nginx
Tairitsu_H11 小时前
[Linux系统] 基础IO核心 | 系统接口 | 文件描述符 | 重定向
linux·服务器·文件操作