3. 中断上下文与进程上下文:原子性要求、可睡眠函数禁忌、in_interrupt() 判断

中断处理程序运行在一种特殊的执行环境中——中断上下文。它与我们熟悉的进程上下文有着本质区别。理解这两种上下文的差异,是编写可靠、高效中断驱动的基石。本节将深入剖析中断上下文的原子性要求、可睡眠函数的调用禁忌,以及如何通过 in_interrupt() 等接口进行运行时上下文判断。

3.1 上下文定义与核心差异

在 Linux 内核中,执行上下文主要分为两类:进程上下文和中断上下文。进程上下文包括用户态进程和内核态进程(如系统调用、内核线程),而中断上下文则包括硬中断处理程序、软中断(softirq)和 tasklet。

核心原则:中断上下文是异步的、不可阻塞的。它不与任何特定进程关联,因此不能访问用户空间地址,也不能执行可能导致睡眠的操作。
对比维度 进程上下文 中断上下文
关联进程 有 current 指针,可访问进程描述符 无 current 概念,不可访问用户空间
调度特性 可被调度、可抢占 不可调度、不可抢占(硬中断)
睡眠能力 允许调用可睡眠函数 绝对禁止睡眠
锁机制 可使用 mutex(可睡眠锁) 只能使用 spinlock(自旋锁)
栈空间 进程内核栈(通常 8KB/16KB) 中断栈(通常 4KB/8KB,独立或共享)
中断上下文 vs 进程上下文 进程上下文 current 指针有效 可调用 mutex_lock() 可睡眠、可被抢占 可访问用户空间 中断上下文 无 current 指针 只能使用 spin_lock() 禁止睡眠、不可抢占 禁止访问用户空间

3.2 原子性要求:不可分割的操作

中断上下文的核心约束是原子性。中断处理程序在执行过程中,可能被更高优先级的中断打断(中断嵌套),也可能在 SMP 系统上与其他 CPU 上的代码并发执行。因此,对共享数据的访问必须保证原子性。

3.2.1 原子操作与自旋锁

在中断上下文中,保护临界区只能使用不会导致睡眠的同步机制:

  • 原子操作(atomic_t):适用于简单的计数器或标志位操作,如 atomic_inc()、atomic_dec_and_test()。
  • 自旋锁(spinlock):适用于保护较长的临界区。在中断上下文中,必须使用 spin_lock_irqsave() / spin_unlock_irqrestore() 变体,以同时禁用本地 CPU 的中断,防止死锁。
常见陷阱:在中断上下文中使用 mutex_lock() 会导致内核崩溃。mutex 在锁竞争时会触发调度(睡眠),而中断上下文不允许调度,因此会触发 BUG: scheduling while atomic 错误。
/* 错误示例:在中断上下文中使用 mutex */
static irqreturn_t bad_handler(int irq, void *dev_id) {
    mutex_lock(&my_mutex);  /* BUG: 可能导致睡眠 */
    /* ... 临界区 ... */
    mutex_unlock(&my_mutex);
    return IRQ_HANDLED;
}

/* 正确示例:使用 spinlock 的 irqsave 变体 */
static irqreturn_t good_handler(int irq, void *dev_id) {
    unsigned long flags;
    spin_lock_irqsave(&my_spinlock, flags);  /* 安全:禁用本地中断 + 自旋 */
    /* ... 临界区 ... */
    spin_unlock_irqrestore(&my_spinlock, flags);
    return IRQ_HANDLED;
}

3.3 可睡眠函数禁忌:绝对不能调用的 API

中断上下文必须避免调用任何可能导致睡眠或调度的内核函数。以下是一份禁忌清单:

函数类别 具体函数示例 禁止原因
内存分配(GFP_KERNEL) kmalloc(size, GFP_KERNEL) 可能触发页面回收或 I/O,导致睡眠
信号量 / 互斥锁 mutex_lock()、down() 锁竞争时调度进程睡眠
用户空间拷贝 copy_from_user()、copy_to_user() 可能触发缺页异常,导致睡眠
调度相关 schedule()、wait_event() 显式触发调度,中断上下文不允许
I/O 阻塞操作 msleep()、ssleep() 主动睡眠,违反原子性要求
安全替代方案:在中断上下文中需要内存分配时,使用 kmalloc(size, GFP_ATOMIC)。该标志保证分配过程不会睡眠,即使失败也会立即返回 NULL。
中断上下文函数调用决策树 中断处理程序入口 需要调用
可睡眠函数? 禁止!内核崩溃 允许 可睡眠函数示例 mutex_lock() kmalloc(..., GFP_KERNEL) copy_from_user()

3.4 in_interrupt() 判断:运行时上下文检测

内核提供了 in_interrupt() 宏,用于在运行时检测当前是否运行在中断上下文中。这对于编写可被多种上下文调用的通用函数至关重要。

3.4.1 宏定义与返回值

#include <linux/preempt.h>

/*
 * in_interrupt() - 检测是否在中断上下文
 * 返回值:
 *   0  - 当前在进程上下文
 *   非0 - 当前在中断上下文(硬中断、软中断、NMI 等)
 */
if (in_interrupt()) {
    /* 中断上下文:使用 GFP_ATOMIC,禁止睡眠 */
    ptr = kmalloc(size, GFP_ATOMIC);
} else {
    /* 进程上下文:可以使用 GFP_KERNEL */
    ptr = kmalloc(size, GFP_KERNEL);
}

3.4.2 更精细的检测接口

内核还提供了更细粒度的检测宏,用于区分不同的中断类型:

检测宏 检测范围 典型用途
in_interrupt() 硬中断 + 软中断 + NMI 通用上下文判断
in_irq() 仅硬中断 区分硬中断与软中断
in_softirq() 仅软中断 软中断专用逻辑
in_nmi() 仅 NMI 不可屏蔽中断特殊处理
最佳实践:在编写可能被中断上下文和进程上下文共同调用的库函数或辅助函数时,始终使用 in_interrupt() 进行判断,并据此选择合适的内存分配标志或锁机制。这可以避免因调用者上下文变化而导致的潜在 bug。
in_interrupt() 运行时上下文判断架构 通用函数入口 in_interrupt() 中断上下文路径 kmalloc(size, GFP_ATOMIC) spin_lock_irqsave() 进程上下文 kmalloc(size, GFP_KERNEL) 返回值含义 返回 0 → 进程上下文 返回 非0 → 中断上下文 包括硬/软中断、NMI

3.5 实战案例:上下文感知的日志缓冲区写入

以下是一个综合示例,展示如何在实际驱动中利用 in_interrupt() 实现上下文感知的环形缓冲区写入:

/* 上下文感知的日志写入函数 */
static int log_write(struct log_buf *buf, const char *msg, size_t len)
{
    unsigned long flags;
    int ret;

    if (in_interrupt()) {
        /* 中断上下文:使用自旋锁 + GFP_ATOMIC */
        spin_lock_irqsave(&buf->lock, flags);
        ret = __ring_buffer_write(buf, msg, len, GFP_ATOMIC);
        spin_unlock_irqrestore(&buf->lock, flags);
    } else {
        /* 进程上下文:可以使用 mutex + GFP_KERNEL */
        mutex_lock(&buf->mutex);
        ret = __ring_buffer_write(buf, msg, len, GFP_KERNEL);
        mutex_unlock(&buf->mutex);
    }

    return ret;
}

/* 中断处理程序中调用 */
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
    log_write(&my_buf, "IRQ triggered\n", 14);
    return IRQ_HANDLED;
}

/* 进程上下文(如 read 系统调用)中调用 */
static ssize_t my_read(struct file *filp, char __user *buf,
                       size_t count, loff_t *f_pos)
{
    log_write(&my_buf, "Read called\n", 12);
    /* ... */
}
关键要点:上述代码通过 in_interrupt() 动态选择锁机制和内存分配标志,使得 log_write() 函数既能在中断处理程序中安全运行,也能在进程上下文中获得更好的性能(使用可睡眠的 mutex 和 GFP_KERNEL)。

3.6 常见错误与调试

中断上下文编程中常见的错误及其调试方法:

  • 错误 1:Scheduling while atomic —— 内核打印 BUG: scheduling while atomic 并 dump 栈回溯。通常是因为在中断上下文中调用了 mutex_lock() 或 kmalloc(GFP_KERNEL)。
  • 错误 2:死锁 —— 在中断上下文中使用 spin_lock()(非 irqsave 变体)且该锁在进程上下文中也被使用,导致中断打断进程后自旋等待同一把锁。
  • 错误 3:用户空间访问 —— 在中断上下文中调用 copy_to_user(),导致缺页异常并触发 do_page_fault,最终 oops。
调试建议:开启内核的 CONFIG_DEBUG_ATOMIC_SLEEP 选项。该选项会在内核检测到原子上下文中发生睡眠操作时,立即打印详细的调用栈和寄存器信息,极大简化问题定位。