嵌入式linux学习记录七,中断

Linux 中断的两半机制

中断处理有个核心矛盾:需要快速响应,但有些处理很耗时

Linux 的解决方案:上半部 + 下半部

复制代码
中断触发
    │
    ▼
┌─────────────────────────────┐
│  上半部(Top Half)           │  ← 必须快,关中断执行
│  · 读取硬件数据               │
│  · 清除中断标志               │
│  · 登记下半部任务             │
└──────────────┬──────────────┘
               │ 立即返回,重新开中断
               ▼
┌─────────────────────────────┐
│  下半部(Bottom Half)        │  ← 稍后执行,可被中断
│  · 数据处理                  │
│  · 协议栈处理                │
│  · 唤醒等待进程               │
└─────────────────────────────┘
下半部的三种实现方式

1. softirq(软中断)

复制代码
· 编译时静态定义,数量固定(32个)
· 同一 softirq 可在多个 CPU 并发执行
· 优先级最高,用于网络收发、块设备等
· 在 ksoftirqd 内核线程或中断返回时执行

2. tasklet

复制代码
· 基于 softirq 实现
· 同一 tasklet 同一时刻只在一个 CPU 执行(更安全)
· 动态创建,驱动开发常用

3. workqueue(工作队列)

复制代码
· 在内核线程(worker thread)中执行
· 可以睡眠、阻塞(前两种不能睡眠)
· 适合耗时操作
softirq tasklet workqueue
执行上下文 中断上下文 中断上下文 进程上下文
可以睡眠
并发 多CPU并发 串行 多线程
使用难度 复杂 简单 简单

驱动中如何使用中断

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

// 1. 定义中断处理函数
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
    // 上半部:快速处理
    // 清中断标志、读数据...
    
    // 登记下半部
    tasklet_schedule(&my_tasklet);
    
    return IRQ_HANDLED;
}

// 2. 注册中断
int irq = gpio_to_irq(gpio_num);  // 获取IRQ号
request_irq(irq,                  // IRQ号
            my_irq_handler,       // 处理函数
            IRQF_TRIGGER_RISING,  // 触发方式
            "my_device",          // 名字
            dev);                 // 传给handler的参数

// 3. 释放中断
free_irq(irq, dev);
下半部用 tasklet
复制代码
// 定义 tasklet
static void my_tasklet_func(unsigned long data)
{
    // 下半部处理,耗时操作
}
DECLARE_TASKLET(my_tasklet, my_tasklet_func, 0);

// 在上半部中触发
tasklet_schedule(&my_tasklet);
下半部用 workqueue
复制代码
// 定义 work
static void my_work_func(struct work_struct *work)
{
    // 可以睡眠,可以阻塞
    msleep(100);  // workqueue 中可以这样用
}
DECLARE_WORK(my_work, my_work_func);

// 触发
schedule_work(&my_work);

中断上下文的限制

在中断处理程序(上半部、softirq、tasklet)中:

复制代码
❌ 不能睡眠          // 没有进程上下文,无法调度
❌ 不能调用可能睡眠的函数  // mutex_lock、kmalloc(GFP_KERNEL)...
❌ 不能访问用户空间内存
✅ 可以用 spinlock
✅ 可以用 kmalloc(GFP_ATOMIC)
✅ 可以读写硬件寄存器

中断屏蔽

复制代码
// 关闭本地CPU所有中断
local_irq_disable();
// ... 临界区 ...
local_irq_enable();

// 更安全的方式(保存中断状态)
unsigned long flags;
local_irq_save(flags);
// ... 临界区 ...
local_irq_restore(flags);

// 只禁止某个IRQ
disable_irq(irq);
enable_irq(irq);

完整流程总结

复制代码
硬件触发中断
    │
    ▼
GIC 仲裁,通知 CPU
    │
    ▼
CPU 切换到内核态,保存现场,切换到中断栈
    │
    ▼
查向量表,调用 handle_irq
    │
    ▼
执行上半部(irq_handler)── 快速返回
    │
    ▼
触发下半部(softirq / tasklet / workqueue)
    │
    ▼
恢复现场,返回被中断的代码
  • 中断上半部分和下半部分的关系:
下半部类型 能否被硬中断打断 能否被同类打断
softirq ✅ 能 ❌ 不能(同一CPU)
tasklet ✅ 能 ❌ 不能
workqueue ✅ 能 ✅ 能

硬中断可以打断下半部

复制代码
时间轴 ──────────────────────────────→

   ┌──────────┐
   │  下半部   │
   │  执行中   │
   └────┬─────┘
        │  新的硬中断来了
        ▼
   ┌──────────┐
   │  上半部   │  ← 打断下半部,优先处理
   └────┬─────┘
        │  上半部结束
        ▼
   ┌──────────┐
   │  下半部   │  ← 继续执行
   │  继续     │
   └──────────┘

因为下半部执行时中断是开着的,所以硬中断随时可以进来。


但 softirq 不能被同类打断

复制代码
// softirq 执行时,会设置标志位
// 同一个 CPU 上,同种 softirq 不会重入

CPU0 正在执行 NET_RX_SOFTIRQ
    │
    │ 又来了网络包,触发 NET_RX_SOFTIRQ
    │
    └──→ 不会立即执行,等当前 softirq 结束再说

不同 CPU 可以同时执行同一种 softirq,所以 softirq 处理函数要考虑并发。


tasklet 更严格

复制代码
同一个 tasklet,任意时刻只在一个 CPU 上执行
    │
    ├── CPU0 执行 tasklet A  ──→  CPU1 想执行 tasklet A?等着
    └── CPU0 执行 tasklet A  ──→  CPU1 执行 tasklet B?可以

所以 tasklet 比 softirq 更安全,驱动开发更常用。


workqueue 最自由

workqueue 跑在内核线程里,是完整的进程上下文:

复制代码
能被硬中断打断   ✅
能被其他线程抢占 ✅
能睡眠          ✅
能被调度器调度   ✅

跟普通内核线程没有区别。


一句话总结

下半部对硬中断都是"敞开的",硬中断随时能打断它。但下半部内部有自己的互斥机制,防止自己被同类重入。

  • 线程来实现中断下半部分,就是 threaded IRQ(线程化中断),Linux 2.6.30 之后引入的机制。

每个中断可以有一个专属的内核线程来处理下半部。


用法

复制代码
// 传统方式
request_irq(irq, top_half_handler, flags, "name", dev);

// 线程化中断
request_threaded_irq(
    irq,
    top_half_handler,     // 上半部,硬中断上下文,可以为NULL
    thread_handler,       // 下半部,在内核线程中执行
    flags,
    "name",
    dev
);

内核会自动创建一个名为 irq/中断号-name 的内核线程:

复制代码
$ ps aux | grep irq
root    irq/27-eth0      # 网卡中断线程
root    irq/44-mmc0      # SD卡中断线程

执行流程

复制代码
硬件中断
    │
    ▼
┌────────────────┐
│   上半部        │  硬中断上下文,关中断,必须快
│   快速处理      │
└───────┬────────┘
        │ 唤醒内核线程
        ▼
┌────────────────┐
│  irq/xx 线程   │  进程上下文
│   下半部处理    │  可以睡眠
│   可以阻塞      │  可以被调度
└────────────────┘

和 workqueue 的区别

看起来都是用线程执行下半部,但有区别:

threaded IRQ workqueue
线程归属 每个IRQ独占线程 共享worker线程池
实时性 更高,可设优先级 一般
使用场景 中断下半部专用 通用异步任务
可睡眠

优点

1. 可以睡眠

复制代码
static irqreturn_t thread_handler(int irq, void *dev)
{
    mutex_lock(&my_mutex);   // ✅ 可以,进程上下文
    msleep(10);               // ✅ 可以睡眠
    // 处理数据...
    mutex_unlock(&my_mutex);
    return IRQ_HANDLED;
}

2. 可以设置优先级

复制代码
// 设置线程为实时优先级
irq_set_affinity_hint(irq, cpumask);  // 绑定CPU
// 或通过 chrt 命令设置线程优先级

3. 实时内核(PREEMPT_RT)的基础

复制代码
PREEMPT_RT 补丁把几乎所有中断都线程化
目的是让内核完全可抢占,实现硬实时
RK3568 的实时系统场景就用这个

上半部返回值

线程化中断的上半部有特殊返回值:

复制代码
static irqreturn_t top_half(int irq, void *dev)
{
    if (不是我的中断)
        return IRQ_NONE;        // 不是我的,忽略
    
    // 快速处理...
    
    return IRQ_WAKE_THREAD;     // 唤醒下半部线程
    // 或
    return IRQ_HANDLED;         // 上半部已经处理完,不需要线程
}

总结

复制代码
下半部实现方式汇总:

softirq      最快,不能睡眠,并发复杂,内核自用
tasklet      基于softirq,不能睡眠,驱动常用
workqueue    线程池,能睡眠,通用
threaded IRQ 专属线程,能睡眠,实时性好,现代驱动推荐

线程化中断完整的驱动示例(按键中断)

复制代码
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/interrupt.h>
#include <linux/gpio/consumer.h>
#include <linux/mutex.h>

struct button_dev {
    struct gpio_desc *gpio;
    int irq;
    struct mutex lock;
    int press_count;
};

/* 上半部 */
static irqreturn_t button_top_half(int irq, void *dev_id)
{
    struct button_dev *bdev = dev_id;
    
    // 仅做最快的处理
    pr_info("上半部:中断触发,IRQ=%d\n", irq);
    
    return IRQ_WAKE_THREAD;   // 唤醒线程
}

/* 下半部线程 */
static irqreturn_t button_thread_handler(int irq, void *dev_id)
{
    struct button_dev *bdev = dev_id;
    
    pr_info("下半部线程:开始处理\n");
    
    // 消抖延时(上半部不能做,线程可以)
    msleep(20);
    
    // 读取GPIO状态
    int val = gpiod_get_value(bdev->gpio);
    
    mutex_lock(&bdev->lock);
    if (val == 0) {           // 低电平:按键按下
        bdev->press_count++;
        pr_info("按键按下,第 %d 次\n", bdev->press_count);
    }
    mutex_unlock(&bdev->lock);
    
    return IRQ_HANDLED;
}

static int button_probe(struct platform_device *pdev)
{
    struct button_dev *bdev;
    int ret;
    
    bdev = devm_kzalloc(&pdev->dev, sizeof(*bdev), GFP_KERNEL);
    if (!bdev)
        return -ENOMEM;
    
    mutex_init(&bdev->lock);
    
    // 获取GPIO
    bdev->gpio = devm_gpiod_get(&pdev->dev, "button", GPIOD_IN);
    if (IS_ERR(bdev->gpio))
        return PTR_ERR(bdev->gpio);
    
    // 获取IRQ号
    bdev->irq = gpiod_to_irq(bdev->gpio);
    if (bdev->irq < 0)
        return bdev->irq;
    
    // 注册线程化中断
    ret = request_threaded_irq(
        bdev->irq,
        button_top_half,
        button_thread_handler,
        IRQF_TRIGGER_FALLING,   // 下降沿(按下)
        "button_irq",
        bdev
    );
    if (ret) {
        dev_err(&pdev->dev, "注册中断失败: %d\n", ret);
        return ret;
    }
    
    platform_set_drvdata(pdev, bdev);
    dev_info(&pdev->dev, "按键驱动加载成功,IRQ=%d\n", bdev->irq);
    
    return 0;
}

static int button_remove(struct platform_device *pdev)
{
    struct button_dev *bdev = platform_get_drvdata(pdev);
    free_irq(bdev->irq, bdev);
    return 0;
}
相关推荐
RisunJan2 小时前
Linux命令-nologin(用于系统账户或需要禁止交互式登录的场景)
linux·运维
是阿建吖!2 小时前
【Linux】信号
android·linux·c语言·c++
城北徐宫2 小时前
Linux信号深度解剖:5种产生、3张表、4次切换
linux·c++·学习
倔强的石头1062 小时前
【Linux指南】Linux快捷键与系统实用技巧
linux·运维·服务器
番茄地瓜2 小时前
Linux 配置静态 IP 步骤
linux·运维·服务器
liulilittle2 小时前
论 Linux 内核态全局稳态带宽的卡尔曼估计与工程实现
linux·服务器·网络·c++·计算机网络·tcp·通信
Irissgwe2 小时前
五、应用层协议HTTP
linux·网络·网络协议·http·状态码·url
.千余3 小时前
【Linux】 传输层协议UDP:从端口号到传输机制
linux·运维·udp
囚~徒~4 小时前
轻量化的虚拟机
linux·运维·服务器