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_SOFTIRQHI_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 在更新版本中可能有变化,请以实际内核源码为准。

相关推荐
我是小灰灰吖1 小时前
解决高频率图像处理中的内存“只增不减”:glibc malloc_trim实战指南
linux·qt
breeze jiang2 小时前
TypeScript 工具类型怎么记:从 Pick、Omit 到 keyof 与 Exclude 推导
linux·ubuntu·typescript
深念Y2 小时前
Wine 运行 HiTool 踩坑记录
linux·windows·容器·桌面·wine·虚拟器
bksczm2 小时前
Linux之网络层协议(IP协议)
linux·网络·tcp/ip
深念Y2 小时前
无头电视盒子改服务器调优记录
linux·运维·服务器·串口·嵌入式·电视盒子·海思
我是小灰灰吖3 小时前
Qt 6.9.3 Ubuntu 22.04 虚拟键盘显示问题全记录与解决方案
linux·qt·ubuntu
Mortalbreeze3 小时前
深入理解TCP协议(一):TCP报文格式详解
linux·服务器·网络·tcp/ip
淡海水3 小时前
01-08-运行时-虚方法分派与接口调用的底层实现
linux·windows·microsoft·c#·虚方法·vtable·vsd
啦啦啦啦啦zzzz3 小时前
高级I/O函数(一)
linux·服务器·网络·c++·网络编程