操作系统中断处理上半部:request_irq 注册与硬中断上下文限制
操作系统中断处理上半部:request_irq 注册与硬中断上下文限制

在操作系统与设备驱动开发的知识体系中,硬件中断(Hardware Interrupt)是连接物理外设与内核调度器的核心桥梁。网卡收到了一个以太网帧、NVMe 控制器完成了 DMA 数据搬运、按键产生了一次电平跳变,硬件都会向中断控制器(如 x86 的 APIC 或 ARM 的 GIC)发送电信号。
为了在微秒级时间内响应硬件事件,同时不破坏内核全局的吞吐与调度实时性,Linux 操作系统采用了经典的中断两阶段设计:上半部(Top Half / 硬中断) 与 下半部(Bottom Half / 软中断、Tasklet、工作队列)。
本文将从内核机制、request_irq 注册接口走读,深入剖析硬中断上下文(Hardirq Context)的运行规则与绝对禁忌。
一、为什么必须将中断划分为上半部与下半部?
当硬件触发硬中断时,CPU 会暂停当前正在执行的代码(无论处于用户态还是内核态),保存现场寄存器,跳转到中断向量表执行中断处理程序(ISR, Interrupt Service Routine)。
在硬中断处理期间,CPU 通常会屏蔽当前中断线甚至全局关中断(Disable IRQs)。
[硬件事件发生]
│
▼ (电平/边沿触发)
┌────────────────────────────────────────────────────────┐
│ 上半部 (Top Half / 硬中断) - 耗时: < 5 微秒 │
│ 1. 响应硬件,读取中断状态寄存器 │
│ 2. 确认并清除硬件中断标志 (Ack IRQ) │
│ 3. 将原始数据挂入内存缓冲区 / 登记事件 │
│ 4. 唤起底半部 (raise_softirq / schedule_work) │
└────────────────────────┬───────────────────────────────┘
│ (开中断,恢复现场)
▼
┌────────────────────────────────────────────────────────┐
│ 下半部 (Bottom Half) - 耗时: 毫秒级 / 可被调度 │
│ 1. 数据包校验、TCP/IP 协议栈深度解析 │
│ 2. 文件系统落盘、内存分配与多任务唤醒 │
└────────────────────────────────────────────────────────┘
如果把复杂的业务逻辑(如网络数据包解析、磁盘 I/O 提交)全堆在硬中断函数中:
- 中断丢失:系统关中断时间过长,导致后续到来的时钟中断、磁盘中断被硬件丢弃;
- 系统假死:进程调度器(Scheduler)依赖时钟中断进行时间片轮转,关中断过久会导致整个操作系统失去响应。
因此,上半部的唯一使命:以最短的代码和最快的速度应答硬件,然后将重活移交底半部。
二、深入走读 request_irq 内核注册接口
在 Linux 内核驱动中,注册一个中断上半部最常用的 API 是 request_irq(定义于 include/linux/interrupt.h):
int request_irq(unsigned int irq,
irq_handler_t handler,
unsigned long flags,
const char *name,
void *dev);
参数深度解析:
irq:硬件中断号(或通过设备树of_irq_get解析出的虚拟 IRQ 编号)。handler:中断处理函数指针,其原型为irqreturn_t (*handler)(int, void *)。返回值为IRQ_HANDLED(成功处理)或IRQ_NONE(非本设备中断,多用于共享中断排查)。flags:中断处理标志位:IRQF_SHARED:声明该中断线由多个设备共享。此时dev参数必须全局唯一(通常传入设备私有结构体指针),用于在free_irq时唯一定位注销项。IRQF_TRIGGER_RISING/IRQF_TRIGGER_FALLING:指定外部中断的上升沿或下降沿触发。IRQF_ONESHOT:在执行线程化中断(Threaded IRQ)的底半部完成前,保持该中断处于硬屏蔽状态。
name:中断名称,会展示在/proc/interrupts中,方便调试分析。dev:传递给中断处理函数的私有上下文指针。
三、硬中断上下文(Hardirq Context)的五大铁律
硬中断执行时处于极其特殊的运行环境——它没有独立的进程上下文实体(task_struct),它是借用被中断进程的内核栈或 CPU 的专用中断栈执行的。因此,在硬中断上下文中编程必须死守以下五条红线:
硬中断上下文禁忌全景
┌──────────────────────────────────────┐
│ ❌ 绝对禁止睡眠 / 放弃 CPU 调度 │
│ (禁止 msleep, mutex_lock, copy_*) │
├──────────────────────────────────────┤
│ ❌ 禁止使用 GFP_KERNEL 分配内存 │
│ (必须且只能使用 GFP_ATOMIC) │
├──────────────────────────────────────┤
│ ❌ 禁止执行死循环或大块内存拷贝 │
├──────────────────────────────────────┤
│ ❌ 禁止直接与用户空间虚拟地址交互 │
│ (禁止 copy_to_user / copy_from) │
├──────────────────────────────────────┤
│ ✅ 加锁必须使用 spin_lock_irqsave │
└──────────────────────────────────────┘
1. 为什么硬中断绝对不能睡眠?
如果在一个硬中断处理函数中调用了 mutex_lock() 或 msleep(),内核调度器会尝试将“当前执行流”挂起并切换到其他进程。
然而,中断不是一个进程,它没有可以被重新放入就绪队列的进程描述符。一旦休眠,内核将永远无法找到唤醒该中断执行流的入口,导致系统直接触发 Kernel Panic: scheduling while atomic。
2. 内存分配必须使用 GFP_ATOMIC
在中断中如果调用 kmalloc(size, GFP_KERNEL),当内核空闲物理内存不足时,GFP_KERNEL 会允许当前线程进入休眠以等待页面回收(Direct Reclaim 或 I/O 换出)。而在中断中,必须使用 GFP_ATOMIC,它严禁休眠,若无空闲页面则立即返回 NULL 失败。
3. 锁机制必须使用自旋锁(Spinlock)
在中断上下文与进程上下文共享临界区时,互斥锁(Mutex)与信号量(Semaphore)均不可用。必须使用自旋锁,且在获取锁的同时必须关闭本地 CPU 中断:
spin_lock_irqsave(&my_lock, flags);
/* 极短临界区操作 */
spin_unlock_irqrestore(&my_lock, flags);
如果只使用普通的 spin_lock(&my_lock),当进程上下文刚拿到锁时恰好被本 CPU 的硬中断打断,硬中断再去获取同一把锁,就会产生同 CPU 上的永久自旋死锁。
四、驱动完整实战代码:硬件中断上半部与底半部调度
以下是一个完整的 Linux 内核模块示例,演示了如何合规地注册硬中断并在上半部调度 Tasklet 底半部:
#include <linux/module.h>
#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/kernel.h>
#include <linux/slab.h>
#define DRIVER_NAME "demo_irq_driver"
#define DEMO_IRQ_NUM 19 // 示例中断号
struct demo_device {
int irq;
unsigned long counter;
struct tasklet_struct bottom_half_tasklet;
spinlock_t lock;
};
static struct demo_device *g_dev;
// -------------------------------------------------------------
// 2. 底半部处理函数 (Tasklet Context - 运行在软中断环境)
// -------------------------------------------------------------
static void demo_tasklet_handler(struct tasklet_struct *t)
{
struct demo_device *dev = from_tasklet(dev, t, bottom_half_tasklet);
unsigned long flags;
// 执行耗时的分析、统计或业务流转
spin_lock_irqsave(&dev->lock, flags);
pr_info("[%s] 底半部执行: 累计中断触发次数 = %lu\n", DRIVER_NAME, dev->counter);
spin_unlock_irqrestore(&dev->lock, flags);
}
// -------------------------------------------------------------
// 1. 上半部处理函数 (Hardirq Context - 极速响应,不可休眠)
// -------------------------------------------------------------
static irqreturn_t demo_hardirq_handler(int irq, void *dev_id)
{
struct demo_device *dev = (struct demo_device *)dev_id;
// 快速应答硬件状态
dev->counter++;
// 触发下半部执行耗时任务
tasklet_schedule(&dev->bottom_half_tasklet);
return IRQ_HANDLED;
}
static int __init demo_irq_init(void)
{
int ret;
g_dev = kmalloc(sizeof(struct demo_device), GFP_KERNEL);
if (!g_dev)
return -ENOMEM;
g_dev->irq = DEMO_IRQ_NUM;
g_dev->counter = 0;
spin_lock_init(&g_dev->lock);
// 初始化 Tasklet
tasklet_setup(&g_dev->bottom_half_tasklet, demo_tasklet_handler);
// 注册硬件中断上半部
ret = request_irq(g_dev->irq,
demo_hardirq_handler,
IRQF_SHARED,
DRIVER_NAME,
g_dev);
if (ret) {
pr_err("[%s] 无法申请中断 IRQ %d (ret=%d)\n", DRIVER_NAME, g_dev->irq, ret);
kfree(g_dev);
return ret;
}
pr_info("[%s] 中断驱动模块加载成功,监听 IRQ %d\n", DRIVER_NAME, g_dev->irq);
return 0;
}
static void __exit demo_irq_exit(void)
{
// 注销中断
free_irq(g_dev->irq, g_dev);
// 杀死并等待残留的 tasklet 执行完毕
tasklet_kill(&g_dev->bottom_half_tasklet);
kfree(g_dev);
pr_info("[%s] 中断驱动模块已安全卸载\n", DRIVER_NAME);
}
module_init(demo_irq_init);
module_exit(demo_irq_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Zhong Yiren");
MODULE_DESCRIPTION("Linux Hardirq Top Half Demo");
搞懂硬中断上半部的边界与限制,是写出高可靠 Linux 驱动和内核级低延迟系统的必备基础。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐
所有评论(0)