3. Linux内核中断分层

中断是Linux内核与硬件交互的核心机制,是实现硬件异步响应、提升系统资源利用率的关键技术。 在嵌入式与服务器领域,硬件设备(如按键、串口、网卡等)的中断请求具有随机性、突发性, 且不同中断的处理需求存在显著差异——部分中断要求极速响应、执行逻辑简单, 部分中断则需要执行复杂耗时操作。若将所有中断处理逻辑集中在一个函数中,必然会导致中断响应延迟、系统吞吐量下降, 甚至引发硬件异常,因此,Linux内核引入了中断分层设计,构建了“顶半部+底半部”的分层处理架构,从根本上解决了中断处理“快响应”与“强处理”的矛盾。

3.1. 中断分层逻辑

Linux内核中断分层的本质是“拆分中断处理流程,实现上下文分离”,核心分为两大模块:

  • 顶半部(Top Half):也称中断处理程序(Interrupt Handler),运行在硬中断上下文,由硬件触发后立即执行,核心职责是“快速响应、简单处理”,具体包括:屏蔽同类中断(避免嵌套干扰)、保存中断上下文(寄存器、标志位等)、清除硬件中断标志、触发底半部执行,执行时间严格控制在微秒级,不允许调用阻塞函数,不允许主动让出CPU。

  • 底半部(Bottom Half):运行在软中断上下文或进程上下文,由顶半部触发,负责处理耗时、非紧急的后续操作,具体包括:数据解析、设备控制、日志打印、唤醒线程等,执行时间可稍长,可响应其他中断(软中断上下文)或调用阻塞函数(进程上下文)。

底半部是中断分层的核心实现载体,Linux内核提供了四种常用的底半部机制:软中断、tasklet、工作队列、线程irq,它们的上下文、执行时机、适用场景各有差异,开发者需根据实际需求选择合适的机制。

3.2. 中断分层执行流程

典型的中断分层执行流程如下:

  1. 硬件触发中断(如按键按下、数据接收),CPU暂停当前任务,切换到硬中断上下文,执行顶半部中断处理程序。

  2. 顶半部快速完成必要操作(屏蔽中断、保存上下文、清除中断标志),触发对应的底半部机制(如tasklet、工作队列)。

  3. 顶半部执行完毕,CPU恢复被中断的任务,底半部由内核调度器统筹安排,在合适的时机执行。

  4. 底半部执行完毕,释放相关资源,完成整个中断处理流程。

3.3. 底半部机制详解

3.3.1. 软中断

3.3.1.1. 软中断概念

软中断是Linux内核中最底层的底半部机制,运行在 软中断 上下文(介于硬中断和进程之间),由内核统一管理,优先级高于进程,低于硬中断。 软中断是一种“延迟执行”的机制,允许在硬中断处理完毕后,延迟处理一些非紧急的中断后续操作,且可以被更高优先级的硬中断打断。

软中断的核心特点是“可并发执行”——多个不同类型的软中断可以在多个CPU上同时执行,同一类型的软中断在同一CPU上串行执行,避免竞争。

3.3.1.2. 软中断常用函数

Linux内核中软中断的相关函数均定义在<linux/interrupt.h>中,核心函数如下:

3.3.1.2.1. 软中断类型定义

内核预定义了部分软中断类型,类型如下:

软中断中断种类(include/linux/interrupt.h)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
enum
{
    HI_SOFTIRQ=0,       // 高优先级软中断
    TIMER_SOFTIRQ,      // 定时器软中断
    NET_TX_SOFTIRQ,     // 网络发送软中断
    NET_RX_SOFTIRQ,     // 网络接收软中断
    BLOCK_SOFTIRQ,      // 块设备软中断
    IRQ_POLL_SOFTIRQ,   // 中断轮询软中断
    TASKLET_SOFTIRQ,    // tasklet依赖的软中断
    SCHED_SOFTIRQ,      // 调度软中断
    HRTIMER_SOFTIRQ,    // 高精度定时器软中断
    RCU_SOFTIRQ,        // RCU软中断

    NR_SOFTIRQS         // 软中断类型总数
};
3.3.1.2.2. open_softirq函数

open_softirq函数用于注册软中断,绑定软中断处理函数,内核启动时调用,是软中断使用的前置步骤。

函数原型:

1
void open_softirq(int nr, void (*action)(struct softirq_action *));

参数说明:

  • nr:软中断类型(如TASKLET_SOFTIRQ),取值范围0~NR_SOFTIRQS-1。

  • action:软中断的处理函数,参数为struct softirq_action结构体,包含软中断的相关信息。

3.3.1.2.3. raise_softirq函数

raise_softirq函数用于触发软中断执行,将指定编号的软中断加入调度队列,等待内核调度其处理函数。

函数原型:

1
void raise_softirq(int nr);

参数说明:

  • nr:需要触发的软中断类型,必须是已注册的软中断类型。

3.3.1.3. 软中断应用场景

软中断适用于“高频、低延迟、可并发”的底半部处理场景,尤其是内核核心模块,例如:

  • 网络协议栈:网络接收(NET_RX_SOFTIRQ)、网络发送(NET_TX_SOFTIRQ),需要高频处理数据,且支持多CPU并发执行。

  • 定时器处理:TIMER_SOFTIRQ,用于处理定时器到期后的延迟操作,优先级较高。

  • RCU同步:RCU_SOFTIRQ,用于RCU机制的延迟回收操作,确保系统并发性能。

注意:软中断的处理函数不能调用阻塞函数(如msleep、wait_event),因为软中断上下文不支持调度,调用阻塞函数会导致内核死锁。

3.3.2. tasklet

3.3.2.1. tasklet概念

tasklet是基于软中断实现的一种简化版底半部机制,运行在 软中断 上下文,依赖于TASKLET_SOFTIRQ和HI_SOFTIRQ两种软中断类型。

tasklet的核心特点是“简单易用、串行执行”——同一时刻,同一CPU上的所有tasklet串行执行,不同CPU上的tasklet可并行执行; 同一tasklet不会在多个CPU上同时执行,无需额外加锁(除非访问全局资源),大大降低了开发者的编程难度。

tasklet是Linux驱动开发中最常用的底半部机制,适用于大多数简单的延迟中断处理场景。

3.3.2.2. tasklet常用函数

3.3.2.2.1. tasklet结构体定义

开发者需定义struct tasklet_struct结构体变量,用于描述tasklet的属性和处理函数,原型如下:

1
2
3
4
5
6
7
struct tasklet_struct {
    struct tasklet_struct *next;  // 链表指针,用于链接多个tasklet
    unsigned long state;          // tasklet状态,0:未触发;TASKLET_STATE_SCHED:已触发待执行
    atomic_t count;               // 引用计数,0:可执行;非0:不可执行
    void (*func)(unsigned long);  // tasklet处理函数
    unsigned long data;           // 传递给处理函数的参数
};
3.3.2.2.2. tasklet_init函数

tasklet_init函数用于初始化tasklet结构体,绑定回调函数,传递私有数据。

函数原型:

1
void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data);

参数说明:

  • t:待初始化的tasklet结构体指针;

  • func:tasklet回调函数指针;

  • data:传递给回调函数的私有数据。

3.3.2.2.3. tasklet_schedule函数

tasklet_schedule函数用于调度tasklet执行,将tasklet加入软中断队列,触发软中断,等待内核调度回调函数。

函数原型:

1
void tasklet_schedule(struct tasklet_struct *t);

参数说明:

  • t:已初始化的tasklet结构体指针。

3.3.2.2.4. tasklet_kill函数

tasklet_kill函数用于终止tasklet执行,等待回调函数执行完毕后再释放资源,防止并发问题,驱动退出时必须调用。

函数原型:

1
void tasklet_kill(struct tasklet_struct *t);

参数说明:

  • t:已初始化的tasklet结构体指针。

3.3.2.3. tasklet应用场景

Tasklet适用于“简单、低延迟、无需阻塞”的底半部处理场景,是驱动开发中最常用的底半部机制,例如:

  • 按键中断消抖后的后续处理(如翻转LED、上报按键事件)。

  • 串口数据接收后的简单解析(如提取关键数据、打印日志)。

  • GPIO中断的延迟处理(如控制设备状态切换)。

注意:Tasklet运行在软中断上下文,不能调用阻塞函数,不能主动让出CPU;若处理逻辑需要阻塞,需使用工作队列或线程irq。

注解

tasklet 本身不支持主动阻塞/主动延迟(如msleep),它的“延迟”是被动调度延迟——调用tasklet_schedule()后不会立即执行,而是等待内核在软中断上下文的安全时机执行,属于“调度延迟”而非“主动等待延迟”。

3.3.3. 工作队列

3.3.3.1. 工作队列概念

工作队列是运行在 进程 上下文的底半部机制,核心是将延迟处理的任务交给内核线程执行,因此支持调用阻塞函数(如msleep、copy_from_user、wait_event),也支持主动让出CPU,是唯一支持阻塞操作的底半部机制。

工作队列的核心特点是“进程上下文、可阻塞、可调度”——工作队列的处理函数运行在用户态进程(kworker线程)中,受内核调度器管理,可被其他进程或中断打断,执行时间可较长,适合处理耗时且需要阻塞的操作。

Linux内核提供了两种工作队列:默认工作队列(共享队列)和自定义工作队列(私有队列),默认工作队列适用于简单场景,自定义工作队列适用于对并发性能要求较高的场景。

3.3.3.2. 工作队列常用函数

工作队列的相关函数均定义在<linux/workqueue.h>中,核心函数如下:

3.3.3.2.1. 工作结构体定义

开发者需定义struct work_struct结构体变量,用于描述工作任务,原型如下:

1
2
3
4
5
6
7
8
struct work_struct {
    atomic_long_t data;           // 传递给处理函数的参数
    struct list_head entry;       // 链表指针,用于链接到工作队列
    work_func_t func;             // 工作处理函数
    struct workqueue_struct *wq;  // 所属的工作队列
    struct list_head pending;     // 挂起链表
    ...
};

其中,work_func_t是处理函数的类型定义:typedef void (*work_func_t)(struct work_struct *work);

3.3.3.2.2. INIT_WORK宏

INIT_WORK用于动态初始化工作任务,宏定义如下:

1
INIT_WORK(struct work_struct *work, work_func_t func);

参数说明:

  • work:指向工作结构体的指针;

  • func:工作处理函数。

3.3.3.2.3. schedule_work函数

schedule_work函数用于将工作任务提交到默认工作队列(kworker线程)。

函数原型:

1
bool schedule_work(struct work_struct *work);

参数说明:

  • work:已初始化的工作队列结构体指针;

返回值:true:提交成功;false:工作已在队列中。

3.3.3.2.4. cancel_work_sync函数

cancel_work_sync函数用于取消工作任务,若工作已提交但未执行,会等待其执行完毕后取消;若未提交,则直接取消。

函数原型:

1
bool cancel_work_sync(struct work_struct *work);

参数说明:

  • work:已初始化的工作队列结构体指针;

3.3.3.2.5. INIT_DELAYED_WORK宏

INIT_DELAYED_WORK用于初始化延迟执行的工作(延迟指定时间后执行),宏定义如下:

1
INIT_DELAYED_WORK(struct delayed_work *work, work_func_t func);

参数说明:

  • work:指向延时工作队列结构体指针;

  • func:工作处理函数。

3.3.3.2.6. schedule_delayed_work函数

schedule_delayed_work函数用于调度延时工作队列执行,延迟指定时间后,触发工作队列回调函数。

函数原型:

1
bool schedule_delayed_work(struct delayed_work *work, unsigned long delay);

参数说明:

  • dwork:已初始化的延时工作队列结构体指针;

  • delay:延迟时间,单位为jiffiess。

返回值:true:调度成功;false:调度失败。

3.3.3.2.7. cancel_delayed_work_sync函数

cancel_delayed_work_sync函数用于取消延时工作队列调度,等待工作队列回调函数执行完毕后再取消,防止并发问题。

函数原型:

1
bool cancel_delayed_work_sync(struct delayed_work *dwork);

参数说明:

  • dwork:已初始化的延时工作队列结构体指针;

返回值:true:成功取消、false:工作队列已执行完毕。

3.3.3.3. 工作队列应用场景

工作队列适用于“耗时、需要阻塞”的底半部处理场景,例如:

  • 数据量大的解析操作(如网卡接收大数据包后的解析、存储)。

  • 需要调用阻塞函数的场景(如从用户空间拷贝数据、等待某个设备就绪)。

  • 执行时间较长的操作(如日志写入、设备固件升级)。

注意:工作队列运行在进程上下文,可调用阻塞函数,但受内核调度影响,执行延迟相对较高,不适合对延迟要求极高的场景。

3.3.4. 线程irq

3.3.4.1. 线程irq概念

线程irq(线程化中断)是将中断处理程序直接运行在内核线程中的一种机制,本质是“顶半部+底半部合并为线程执行”——中断触发后,内核会唤醒对应的内核线程, 中断处理逻辑全部在 进程 上下文(内核线程)中执行,支持阻塞、调度,且可设置线程优先级。

线程irq的核心特点是“进程上下文、可阻塞、可设置优先级”,与工作队列类似,但比工作队列更简洁(无需手动创建工作任务), 且中断触发与线程唤醒的关联更紧密,适用于中断处理逻辑复杂、需要阻塞且对优先级有要求的场景。

线程irq分为两种模式:

  • 纯线程irq:顶半部为空,所有中断处理逻辑都在线程中执行,适用于无紧急处理需求、需要阻塞的场景。

  • 混合线程irq:顶半部执行简单紧急操作(如清除中断标志),底半部(线程)执行耗时阻塞操作,适用于既有紧急处理需求、又有阻塞操作的场景。

3.3.4.2. 线程irq常用函数

3.3.4.2.1. devm_request_threaded_irq函数

devm_request_threaded_irq函数用于申请线程irq,绑定顶半部中断服务函数(handler)和底半部线程函数(thread_fn), 带设备资源托管,无需手动释放;内核自动创建并管理线程,无需调用kthread_run。

函数原型:

1
2
3
4
5
6
int devm_request_threaded_irq(struct device *dev, unsigned int irq,
                             irq_handler_t handler,
                             irq_handler_t thread_fn,
                             unsigned long flags,
                             const char *name,
                             void *dev_id);

参数说明:

  • dev:设备指针;

  • irq:中断号;

  • handler:顶半部中断服务函数;

  • thread_fn:底半部线程irq函数;

  • flags:中断标志;

  • name:中断名称;

  • dev_id:传递给中断函数的私有数据。

返回值:0:申请成功;负值:申请失败,返回错误码。

3.3.4.3. irq_wake_thread函数

irq_wake_thread函数用于主动唤醒线程irq的底半部线程函数,触发线程执行;

  • 若顶半部返回 IRQ_WAKE_THREAD ,内核会自动唤醒底半部线程函数,无需手动调用。

  • 若顶半部返回 IRQ_HANDLED ,则需手动调用irq_wake_thread函数唤醒底半部线程函数。

函数原型:

1
irqreturn_t irq_wake_thread(int irq, void *dev_id);

参数说明:

  • irq:中断号;

  • dev_id:传递给线程函数的私有数据。

返回值:IRQ_WAKE_THREAD:唤醒成功,通知内核调度线程。

3.3.4.4. 线程irq应用场景

线程irq适用于“中断处理逻辑复杂、需要阻塞、对优先级有要求”的场景,例如:

  • 需要与用户空间交互的中断处理(如通过字符设备接口上报中断事件)。

  • 需要等待资源就绪的中断处理(如等待I2C、SPI设备响应)。

  • 对实时性要求较高的中断处理(可设置线程为实时优先级)。

注意:线程irq运行在进程上下文,可调用阻塞函数,但线程的创建、调度会带来一定的系统开销,不适合高频中断场景。

3.3.5. 选择原则

  1. 高频、低延迟、无阻塞需求:优先选择软中断或tasklet。

  2. 有阻塞需求、处理逻辑耗时:优先选择工作队列或线程irq。

  3. 驱动开发、简单延迟处理:优先选择tasklet。

  4. 需要与用户空间交互、等待资源:优先选择工作队列或线程irq。

  5. 高频中断、无阻塞、多CPU并发:优先选择软中断。

3.4. 中断分层实验

本实验在Linux中断子系统实验基础上进行修改,设备树插件完全一致,驱动仅说明修改部分,重复部分不作说明。

中断分层设计如下:

  1. 顶半部:按键中断触发后,启动消抖定时器。

  2. 消抖定时器回调函数翻转led电平,调度tasklet,唤醒线程irq底半部

  3. tasklet:调度200ms延时的工作队列。

  4. 工作队列:调用msleep()执行耗时打印逻辑。

  5. 线程irq底半部:统计按键按下次数并打印。

注意

本实验仅演示中断底半部各机制使用方法,实际开发编写驱动时并不需要全部都用上,根据实际需求选择即可。

本章的示例代码目录为: linux_driver/19_irq_layering

3.4.1. 驱动代码详解

核心数据结构定义

核心数据结构定义(位于linux_driver/19_irq_layering/irq_layering.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/* 定义 LED 字符设备结构体 */
struct led_chrdev {
    /* 字符设备结构体 */
    struct cdev dev;
    /* 自旋锁 */
    spinlock_t spinlock;
    /* LED 状态 */
    int led_state;
    /* LED 的 GPIO 描述符 */
    struct gpio_desc *led_gpio;
    /* 按钮的 GPIO 描述符 */
    struct gpio_desc *button_gpio;
    /* 消抖定时器 */
    struct timer_list debounce_timer;
    /* 中断号 */
    int irq;
    /* 统计有效按键按下次数 */
    unsigned int btn_press_count;
    /* tasklet */
    struct tasklet_struct btn_tasklet;
    /* 工作队列 */
    struct delayed_work btn_delayed_work;
};

/* 定义 led_chrdev 结构体指针,用于动态分配内存管理 LED 硬件信息 */
static struct led_chrdev *led_cdev;

设备私有结构体整合tasklet、延时工作队列资源,线程irq由内核管理,无需在结构体中定义task_struct指针; 新增btn_press_count用于线程irq的按键计数。

驱动初始化

驱动初始化(位于linux_driver/19_irq_layering/irq_layering.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
static int pdrv_led_probe(struct platform_device *pdev)
{
    // 内存分配(省略重复代码)

    /* 第一步:提取平台设备提供的资源 */
    if (pdev->dev.of_node) {
        /* 获取 LED 的 GPIO 描述符,GPIOD_OUT_HIGH 表示设置为输出模式 + 输出高电平 */
        led_cdev->led_gpio = devm_gpiod_get(&pdev->dev, "led", GPIOD_OUT_HIGH);
        if (IS_ERR(led_cdev->led_gpio)) {
            ret = PTR_ERR(led_cdev->led_gpio);
            printk(KERN_ERR "Failed to get LED GPIO: %d\n", ret);
            return ret;
        }

        /* 获取按钮的 GPIO 描述符,GPIOD_IN 表示设置为输入模式 */
        led_cdev->button_gpio = devm_gpiod_get(&pdev->dev, "button", GPIOD_IN);
        if (IS_ERR(led_cdev->button_gpio)) {
            ret = PTR_ERR(led_cdev->button_gpio);
            printk(KERN_ERR "Failed to get button GPIO: %d\n", ret);
            return ret;
        }

    } else {
        printk("Platform device matching is not supported in this driver\n");
        return -ENOMEM;
    }

    //字符设备注册(省略重复代码)

    /* 初始化自旋锁 */
    spin_lock_init(&led_cdev->spinlock);

    /* 初始化tasklet */
    tasklet_init(&led_cdev->btn_tasklet, btn_tasklet_callback, (unsigned long)led_cdev);

    /* 初始化延时工作队列 */
    INIT_DELAYED_WORK(&led_cdev->btn_delayed_work, btn_delayed_callback);

    /* 初始化消抖定时器 */
    timer_setup(&led_cdev->debounce_timer, button_debounce_callback, 0);

    /* 初始化按键按下次数 */
    led_cdev->btn_press_count = 0;

    /* 默认关闭 LED */
    led_cdev->led_state = 0;

    /* 获取按键GPIO对应的中断号 */
    led_cdev->irq = gpiod_to_irq(led_cdev->button_gpio);
    if (led_cdev->irq < 0) {
        ret = led_cdev->irq;
        /* 打印获取中断号失败 */
        printk(KERN_ERR "Failed to get button IRQ: %d\n", ret);
        /* 跳转到错误处理标签 */
        goto device_err;
    }

    /* 申请中断,下降沿触发 */
    ret = devm_request_threaded_irq(&pdev->dev, led_cdev->irq,
                    button_irq_handler,     // 顶半部(硬中断上下文,无睡眠)
                    button_irq_thread_fn,   // 底半部(内核线程,进程上下文,支持睡眠)
                    IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
                    "button_irq", led_cdev);
    if (ret < 0) {
        /* 打印申请中断失败 */
        printk(KERN_ERR "Failed to request IRQ: %d\n", ret);
        /* 跳转到错误处理标签 */
        goto device_err;
    }

    return 0;

    // 错误处理(省略重复代码)
}
  • 第34行:初始化tasklet;

  • 第37行:初始化延时工作队列;

  • 第43行:初始化按下次数为0;

  • 第59-63行:申请线程irq,通过devm_request_threaded_irq绑定顶半部和线程底半部,内核自动创建并管理线程,无需手动调用kthread_run。

中断顶半部:按键中断服务函数

按键中断服务函数(位于linux_driver/19_irq_layering/irq_layering.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
static irqreturn_t button_irq_handler(int irq, void *dev_id)
{
    struct led_chrdev *led_cdev = (struct led_chrdev *)dev_id;

    /* 重启消抖定时器,20ms后执行回调 */
    mod_timer(&led_cdev->debounce_timer, jiffies + msecs_to_jiffies(DEBOUNCE_TIME));

    /* 如果返回 IRQ_HANDLED 会认为顶半部已经完成了所有中断处理,不会再调度底半部线程*/
    return IRQ_HANDLED;

    /* 如果返回 IRQ_WAKE_THREAD 会主动唤醒底半部线程执行 */
    // return IRQ_WAKE_THREAD;
}

顶半部仅启动消抖定时器,无任何耗时操作,快速退出中断,保证中断实时响应; 返回IRQ_HANDLED,不自动唤醒线程irq,线程唤醒由定时器回调中的irq_wake_thread触发。

中断底半部:消抖定时器回调函数

消抖定时器回调函数(位于linux_driver/19_irq_layering/irq_layering.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
static void button_debounce_callback(struct timer_list *t)
{
    /* 反向找到包含这个定时器的设备结构体 led_chrdev 指针 */
    struct led_chrdev *led_cdev = from_timer(led_cdev, t, debounce_timer);

    /* 用于保存中断状态信息 */
    unsigned long flags;
    /* 存储按键状态 */
    int btn_val;

    /* 获取自旋锁并保存中断状态 */
    spin_lock_irqsave(&led_cdev->spinlock, flags);

    /* 读取稳定后的按键电平 */
    btn_val = gpiod_get_value(led_cdev->button_gpio);

    /* 低电平表示按键稳定按下 */
    if(btn_val == 0){
        /* 翻转LED电平状态 */
        gpiod_set_value(led_cdev->led_gpio, !gpiod_get_value(led_cdev->led_gpio));

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

        /* 唤醒线程irq底半部 */
        irq_wake_thread(led_cdev->irq, led_cdev);
    }

    /* 释放自旋锁并恢复中断状态 */
    spin_unlock_irqrestore(&led_cdev->spinlock, flags);

    /* 按键按下打印日志 */
    if(btn_val == 0){
        printk(KERN_INFO "按键已按下,LED状态翻转\n");
    }
}

定时器消抖后,确认按键稳定按下后翻转电平,同时触发两个底半部逻辑:调度tasklet、唤醒线程irq; 遵循“快进快出”原则,不执行耗时操作。

中断底半部:tasklet回调函数

tasklet回调函数(位于linux_driver/19_irq_layering/irq_layering.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
static void btn_tasklet_callback(unsigned long data)
{
    /* 转换为自定义的设备结构体指针 */
    struct led_chrdev *led_cdev = (struct led_chrdev *)data;

    /* 打印回调函数执行信息 */
    printk(KERN_INFO "tasklet回调函数执行\n");

    /* 调度延时工作队列,200ms后执行延时工作队列回调函数 */
    schedule_delayed_work(&led_cdev->btn_delayed_work, msecs_to_jiffies(200));
}

tasklet运行于中断上下文,不可睡眠、无耗时操作,此处仅调度延时工作队列,200ms后执行延时工作队列回调函数。

中断底半部:工作队列回调函数

工作队列回调函数(位于linux_driver/19_irq_layering/irq_layering.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
static void btn_delayed_callback(struct work_struct *work)
{
    /* 计数器变量 */
    int counter = 1;

    /* 打印回调函数执行信息 */
    printk(KERN_INFO "延时工作队列回调函数执行,开始耗时打印\n");

    /* 5次延时打印 */
    msleep(200);
    printk(KERN_INFO "irq_thread counter = %d  \n", counter++);
    msleep(200);
    printk(KERN_INFO "irq_thread counter = %d  \n", counter++);
    msleep(200);
    printk(KERN_INFO "irq_thread counter = %d  \n", counter++);
    msleep(200);
    printk(KERN_INFO "irq_thread counter = %d \n", counter++);
    msleep(200);
    printk(KERN_INFO "irq_thread counter = %d \n", counter++);
}

工作队列运行于进程上下文,支持msleep睡眠,此处执行极耗时打印逻辑,无需唤醒其他线程;延时200ms后触发,承接tasklet的调度,属于底半部第二层级。

中断底半部:线程irq底半部函数

线程irq底半部函数(位于linux_driver/19_irq_layering/irq_layering.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
static irqreturn_t button_irq_thread_fn(int irq, void *dev_id)
{
    struct led_chrdev *led_cdev = (struct led_chrdev *)dev_id;

    /* 有效按键触发一次,计数 +1 */
    led_cdev->btn_press_count++;

    printk(KERN_INFO "线程化中断底半部执行,按键有效按下总次数:%u\n", led_cdev->btn_press_count);

    return IRQ_HANDLED;
}

线程irq由内核通过devm_request_threaded_irq自动管理,无需手动创建,运行于进程上下文,支持睡眠,此处是记录按键有效按下次数并打印。

remove函数

pdrv_led_remove函数(位于linux_driver/19_irq_layering/irq_layering.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
static int pdrv_led_remove(struct platform_device *pdev)
{
    struct led_chrdev *led_cdev = platform_get_drvdata(pdev);

    /* 打印平台驱动移除信息 */
    printk("pdrv_led remove\n");

    /* 取消延时工作,等待其执行完成 */
    cancel_delayed_work_sync(&led_cdev->btn_delayed_work);

    /* 终止tasklet,等待其执行完成 */
    tasklet_kill(&led_cdev->btn_tasklet);

    /* 删除消抖定时器 */
    del_timer_sync(&led_cdev->debounce_timer);

    /* 销毁设备节点 */
    device_destroy(class, devno);

    /* 删除字符设备 */
    cdev_del(&led_cdev->dev);

    /* 释放设备号 */
    unregister_chrdev_region(devno, DEV_CNT);

    /* 销毁设备类 */
    class_destroy(class);

    return 0;
}

驱动卸载时,按“工作队列->tasklet->定时器”的顺序释放分层组件资源; 线程irq由devm_request_threaded_irq托管,无需手动停止,内核会自动释放线程资源。

3.4.2. 编译设备树和驱动

此部分和Linux中断子系统实验完全一致不作过多说明。

编译得到设备树插件lubancat-led-overlay.dtb和驱动模块irq_layering.ko。

3.4.3. 程序运行结果

如出现 Permission denied 或类似字样,请注意用户权限,大部分操作硬件外设的功能,几乎都需要root用户权限,简单的解决方案是在执行语句前加入sudo或以root用户运行程序。

3.4.3.1. 实验操作

设备树插件加载方法和设备树插件实验完全一致,加载设备树插件并重启板卡后会发现系统心跳灯默认没有闪烁, 是因为我们使用设备树插件关闭了leds节点,释放了引脚。

使用以下命令加载驱动:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#加载驱动
insmod irq_layering.ko

#信息输出如下
[19727.967875] led platform driver init
[19727.968512] led platform driver probe
[19727.968788] major=236, minor=0

# 查看中断号
cat /proc/interrupts | grep button_irq

#信息输出如下
105:       1611          0          0          0     gpio1  10 Edge      button_irq

使用杜邦线一端连接按键引脚,另一端多次连接和断开连接GND引脚模拟按键按下和松开,信息打印如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
[19731.978896] 按键已按下,LED状态翻转
[19731.979072] 线程化中断底半部执行,按键有效按下总次数:1
[19731.979127] tasklet回调函数执行
[19732.182438] 延时工作队列回调函数执行,开始耗时打印
[19732.388898] irq_thread counter = 1
[19732.598878] irq_thread counter = 2
[19732.808953] irq_thread counter = 3
[19733.018919] irq_thread counter = 4
[19733.225615] irq_thread counter = 5
[19740.385983] 按键已按下,LED状态翻转
[19740.386035] 线程化中断底半部执行,按键有效按下总次数:2
[19740.386070] tasklet回调函数执行
[19740.589465] 延时工作队列回调函数执行,开始耗时打印
[19740.796062] irq_thread counter = 1
[19741.002777] irq_thread counter = 2
[19741.209358] irq_thread counter = 3
[19741.416058] irq_thread counter = 4
[19741.622879] irq_thread counter = 5
[19743.469522] 按键已按下,LED状态翻转
[19743.469648] 线程化中断底半部执行,按键有效按下总次数:3
[19743.469693] tasklet回调函数执行
[19743.672986] 延时工作队列回调函数执行,开始耗时打印
[19743.879505] irq_thread counter = 1
[19744.086271] irq_thread counter = 2
[19744.292867] irq_thread counter = 3
[19744.499581] irq_thread counter = 4
[19744.706197] irq_thread counter = 5

从内核打印信息可以看到线程底半部的按键按下计数功能打印正常,tasklet回调函数执行正常,调度延时工作队列执行耗时打印正常。

3.4.4. 实验注意事项

  • 线程irq由irq_wake_thread手动唤醒,若需改为“顶半部自动唤醒”,可将顶半部返回值改为IRQ_WAKE_THREAD,需注意避免重复唤醒导致计数异常。

  • schedule_delayed_work的延时基于内核jiffies,若系统负载过高,可能出现延时偏差。

  • 禁止在中断上下文调用阻塞函数。