05-中断下半部处理.md 30 KB


title: 中断下半部处理 tags: [Linux驱动, 中断, softirq, tasklet, workqueue, 嵌入式] created: 2026-09-17 updated: 2026-09-17

pdf_ref: "57_第五十一章 Linux中断实验.pdf"

中断下半部处理

💡 关联知识: [[02-嵌入式Linux内核基础/04-进程调度与中断管理]] | [[FreeRTOS学习笔记/07-中断管理与临界段]]


一、中断处理概述

1.1 为什么需要下半部

Linux内核中的中断处理必须快速完成,因为中断会打断当前正在执行的代码。如果中断处理函数执行时间过长,会导致系统响应延迟,甚至丢失其他中断。因此,Linux内核将中断处理分为两部分:

  • 上半部(Top Half):硬中断处理程序,执行最关键、最紧急的操作,不可睡眠,时间极短
  • 下半部(Bottom Half):延迟处理程序,执行耗时操作,可以在稍后执行

1.2 中断处理流程

graph TD
    A[硬件中断触发] --> B[执行上半部]
    B --> C{是否需要下半部?}
    C -->|是| D[调度下半部]
    C -->|否| E[中断处理完成]
    D --> F[软中断/Tasklet]
    D --> G[工作队列]
    D --> H[线程化中断]
    F --> I[执行下半部处理]
    G --> I
    H --> I
    I --> J[中断处理完成]

1.3 下半部机制概览

机制 执行上下文 可睡眠 适用场景
软中断(Softirq) 软中断上下文 高频事件、网络收发
Tasklet 软中断上下文 快速处理、简单任务
工作队列(Workqueue) 进程上下文 耗时操作、需要睡眠
线程化中断 内核线程 复杂处理、需要互斥

二、软中断(Softirq)

2.1 软中断原理

软中断是Linux内核提供的一种下半部实现机制。内核使用softirq_action结构体表示软中断,定义在include/linux/interrupt.h中:

// 示例51.1.2.1 softirq_action结构体
struct softirq_action
{
    void    (*action)(struct softirq_action *);
};

kernel/softirq.c文件中,定义了一个包含10个软中断的数组:

// 示例51.1.2.2 softirq_vec数组
static struct softirq_action softirq_vec[NR_SOFTIRQS];

2.2 软中断类型

NR_SOFTIRQS是枚举类型,定义在include/linux/interrupt.h中:

// 示例51.1.2.3 软中断类型枚举
enum
{
    HI_SOFTIRQ=0,           /* 高优先级软中断 */
    TIMER_SOFTIRQ,          /* 定时器软中断 */
    NET_TX_SOFTIRQ,         /* 网络数据发送软中断 */
    NET_RX_SOFTIRQ,         /* 网络数据接收软中断 */
    BLOCK_SOFTIRQ,
    BLOCK_IOPOLL_SOFTIRQ,
    TASKLET_SOFTIRQ,        /* tasklet软中断 */
    SCHED_SOFTIRQ,          /* 调度软中断 */
    HRTIMER_SOFTIRQ,        /* 高精度定时器软中断 */
    RCU_SOFTIRQ,            /* RCU软中断 */
    NR_SOFTIRQS
};

2.3 软中断API

要使用软中断,需要使用open_softirq注册软中断处理函数:

// 注册软中断处理函数原型
void open_softirq(int nr, void (*action)(struct softirq_action *))

参数说明:

  • nr:要注册的软中断,从上述枚举中选择一个
  • action:软中断对应的处理函数

注册软中断以后,需要通过raise_softirq触发软中断:

// 触发软中断原型
void raise_softirq(unsigned int nr)

2.4 软中断初始化

软中断在系统启动时初始化,softirq_init函数定义在kernel/softirq.c文件中:

// 示例51.1.2.4 softirq_init函数内容
void __init softirq_init(void)
{
    int cpu;

    for_each_possible_cpu(cpu) {
        per_cpu(tasklet_vec, cpu).tail =
            &per_cpu(tasklet_vec, cpu).head;
        per_cpu(tasklet_hi_vec, cpu).tail =
            &per_cpu(tasklet_hi_vec, cpu).head;
    }

    open_softirq(TASKLET_SOFTIRQ, tasklet_action);
    open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}

从示例51.1.2.4可以看出,softirq_init函数默认会注册TASKLET_SOFTIRQHI_SOFTIRQ这两个软中断。

2.5 使用场景

软中断适用于:

  • 网络数据收发(NET_TX_SOFTIRQ、NET_RX_SOFTIRQ)
  • 定时器处理(TIMER_SOFTIRQ)
  • 块设备I/O(BLOCK_SOFTIRQ)
  • RCU回调(RCU_SOFTIRQ)

三、Tasklet

3.1 Tasklet原理

tasklet是基于软中断实现的一种下半部机制。在中断和tasklet之间,推荐使用tasklet。Linux内核使用tasklet_struct结构体来表示tasklet:

// 示例51.1.2.5 tasklet_struct结构体
struct tasklet_struct
{
    struct tasklet_struct *next;    /* 下一个tasklet */
    unsigned long state;            /* tasklet状态 */
    atomic_t count;                 /* 引用计数,记录tasklet是否被禁止 */
    void (*func)(unsigned long);    /* tasklet执行的函数 */
    unsigned long data;             /* 传递给func的参数 */
};

3.2 Tasklet API

要使用tasklet,首先需要定义一个tasklet,然后使用tasklet_init初始化tasklet:

// tasklet_init函数原型
void tasklet_init(struct tasklet_struct *t,
                  void (*func)(unsigned long),
                  unsigned long data);

参数说明:

  • t:要初始化的tasklet
  • func:tasklet的处理函数
  • data:传递给func的参数

也可以使用宏DECLARE_TASKLET一次性完成tasklet的定义和初始化:

// DECLARE_TASKLET宏(include/linux/interrupt.h)
DECLARE_TASKLET(name, func, data)

参数说明:

  • name:要定义的tasklet名称(会自动创建一个tasklet_struct类型的变量)
  • func:tasklet的处理函数
  • data:传递给func的参数

3.3 调度Tasklet

当需要调度tasklet执行时,使用tasklet_schedule函数:

// tasklet_schedule函数原型
void tasklet_schedule(struct tasklet_struct *t)

参数说明:

  • t:要调度的tasklet,也就是DECLARE_TASKLET宏定义的name

3.4 Tasklet使用示例

// 示例51.1.2.7 tasklet使用示例
/* 定义tasklet */
struct tasklet_struct testtasklet;

/* tasklet处理函数 */
void testtasklet_func(unsigned long data)
{
    /* tasklet具体的处理内容 */
}

/* 中断处理函数 */
irqreturn_t test_handler(int irq, void *dev_id)
{
    /* 启动tasklet */
    tasklet_schedule(&testtasklet);
}

/* 驱动入口函数 */
static int __init xxxx_init(void)
{
    /* 初始化tasklet */
    tasklet_init(&testtasklet, testtasklet_func, data);
    /* 注册中断处理函数 */
    request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev);
}

3.5 Tasklet注意事项

  1. 同一tasklet不能并发执行:同一个tasklet只能在一个CPU上运行
  2. 不同tasklet可以并发:不同的tasklet可以在不同CPU上同时运行
  3. 不能睡眠:tasklet运行在软中断上下文,不能调用可能导致睡眠的函数
  4. 引用计数count成员用于控制tasklet的启用和禁用

四、工作队列(Workqueue)

4.1 工作队列原理

工作队列是另一种下半部执行方式,工作在进程上下文执行,可以将工作推迟到一个内核线程去执行。因为工作队列工作在进程上下文,所以允许睡眠。如果推迟的工作需要睡眠,那么就只能选择工作队列。

4.2 工作队列数据结构

Linux内核使用work_struct结构体表示一个工作:

// 示例51.1.2.8 work_struct结构体
struct work_struct {
    atomic_long_t data;
    struct list_head entry;
    work_func_t func;    /* 工作队列处理函数 */
};

这些工作组织成队列,使用workqueue_struct结构体表示工作队列:

// 示例51.1.2.9 workqueue_struct结构体(简化版)
struct workqueue_struct {
    struct list_head pwqs;
    struct list_head list;
    struct mutex mutex;
    int work_color;
    int flush_color;
    atomic_t nr_pwqs_to_flush;
    struct wq_flusher *first_flusher;
    struct list_head flusher_queue;
    struct list_head flusher_overflow;
    struct list_head maydays;
    struct worker *rescuer;
    int nr_drainers;
    int saved_max_active;
    struct workqueue_attrs *unbound_attrs;
    struct pool_workqueue *dfl_pwq;
    char name[WQ_NAME_LEN];
    struct rcu_head rcu;
    unsigned int flags ____cacheline_aligned;
    struct pool_workqueue __percpu *cpu_pwqs;
    struct pool_workqueue __rcu *numa_pwq_tbl[];
};

Linux内核使用工作者线程(worker thread)来处理工作队列中的各个工作。Linux内核使用worker结构体表示工作者线程:

// 示例51.1.2.10 worker结构体
struct worker {
    union {
        struct list_head entry;
        struct hlist_node hentry;
    };
    struct work_struct *current_work;
    work_func_t current_func;
    struct pool_workqueue *current_pwq;
    bool desc_valid;
    struct list_head scheduled;
    struct task_struct *task;
    struct worker_pool *pool;
    struct list_head node;
    unsigned long last_active;
    unsigned int flags;
    int id;
    char desc[WORKER_DESC_LEN];
    struct workqueue_struct *rescue_wq;
};

从示例51.1.2.10可以看出,每个worker都有一个自己的任务队列,工作者线程负责将自己的任务队列中的所有工作都执行完毕。实际上工作队列中,我们只需要做的是创建工作(work_struct),在工作队列和工作者线程机制出来之前我们不用去管。简单来说就是,我们只需要定义一个work_struct结构体变量即可,然后使用INIT_WORK宏对其进行初始化:

// INIT_WORK宏定义
#define INIT_WORK(_work, _func)

参数说明:

  • _work:要初始化的工作
  • _func:工作对应的处理函数

也可以使用DECLARE_WORK宏一次性完成工作的定义和初始化:

// DECLARE_WORK宏定义
#define DECLARE_WORK(n, f)

参数说明:

  • n:定义的工作(work_struct)
  • f:工作对应的处理函数

4.3 工作队列API

和tasklet一样,工作也需要调度才能执行,工作的调度函数为schedule_work

// schedule_work函数原型
bool schedule_work(struct work_struct *work)

参数说明:

  • work:要调度的工作

返回值:0 成功,其他值 失败。

4.4 工作队列使用示例

// 示例51.1.2.11 工作队列使用示例
/* 定义工作(work) */
struct work_struct testwork;

/* work处理函数 */
void testwork_func_t(struct work_struct *work)
{
    /* work具体的处理内容 */
}

/* 中断处理函数 */
irqreturn_t test_handler(int irq, void *dev_id)
{
    /* 调度work */
    schedule_work(&testwork);
}

/* 驱动入口函数 */
static int __init xxxx_init(void)
{
    /* 初始化work */
    INIT_WORK(&testwork, testwork_func_t);
    /* 注册中断处理函数 */
    request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev);
}

4.5 创建和销毁工作队列

除了使用系统默认的工作队列,也可以自己创建工作队列:

// 创建单线程工作队列
struct workqueue_struct *create_singlethread_workqueue(const char *name);

// 创建多线程工作队列
struct workqueue_struct *alloc_workqueue(const char *name,
                                         unsigned int flags,
                                         int max_active);

// 调度指定工作队列中的工作
bool queue_work(struct workqueue_struct *wq,
                struct work_struct *work);

// 销毁工作队列
void destroy_workqueue(struct workqueue_struct *wq);

4.6 工作队列与Tasklet的选择

场景 选择
快速处理,不需要睡眠 Tasklet
耗时操作,需要睡眠 工作队列
高频事件 软中断
复杂处理,需要互斥 线程化中断

五、线程化中断(Threaded IRQ)

5.1 线程化中断原理

线程化中断是Linux内核提供的一种中断处理机制,将中断处理分为硬中断上下文和内核线程上下文两部分。线程化中断允许在下半部中睡眠,适合复杂的中断处理场景。

5.2 线程化中断API

使用request_threaded_irq函数申请线程化中断:

// request_threaded_irq函数原型
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);

参数说明:

  • irq:要申请的中断号
  • handler:上半部处理函数(硬中断上下文,不可睡眠)
  • thread_fn:下半部处理函数(内核线程上下文,可以睡眠)
  • irqflags:中断标志
  • devname:中断名称
  • dev_id:传递给中断处理函数的参数

5.3 线程化中断处理函数原型

// 上半部处理函数原型
irqreturn_t (*irq_handler_t)(int irq, void *dev_id);

// 下半部处理函数原型
irqreturn_t (*irq_handler_t)(int irq, void *dev_id);

5.4 线程化中断使用示例

// 上半部处理函数
static irqreturn_t key_irq_handler(int irq, void *dev_id)
{
    /* 上半部:快速处理 */
    printk("IRQ handler\r\n");
    return IRQ_WAKE_THREAD;  /* 唤醒下半部 */
}

// 下半部处理函数(线程上下文)
static irqreturn_t key_thread_fn(int irq, void *dev_id)
{
    /* 下半部:可以睡眠 */
    msleep(50);
    printk("Thread handler\r\n");
    return IRQ_HANDLED;
}

// 申请线程化中断
static int __init key_init(void)
{
    request_threaded_irq(irq, key_irq_handler, key_thread_fn,
                         IRQF_TRIGGER_FALLING, "key", dev);
}

5.5 使用场景

线程化中断适用于:

  • 需要互斥的中断处理
  • 耗时操作需要睡眠
  • 复杂的中断处理逻辑
  • 需要使用互斥锁、信号量等同步机制

六、完整源码分析

6.1 实验概述

本实验基于I.MX6ULL开发板,使用KEY0按键,通过中断方式检测按键按下和释放。实验代码位于09、文档教程(非常重要)/【正点原子】I.MX6U嵌入式Linux驱动开发指南V2.0.1_拆分/57_第五十一章 Linux中断实验.pdf

6.2 设备树节点

在设备树中添加key节点:

// 示例51.3.1.1 key节点信息
key {
    #address-cells = <1>;
    #size-cells = <1>;
    compatible = "atkalpha-key";
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_key>;
    key-gpio = <&gpio1 18 GPIO_ACTIVE_LOW>; /* KEY0 */
    interrupt-parent = <&gpio1>;
    interrupts = <18 IRQ_TYPE_EDGE_BOTH>; /* FALLING RISING */
    status = "okay";
};

6.3 中断类型定义

include/linux/irq.h中定义了中断类型:

// 示例51.3.1.2 中断状态枚举
enum {
    IRQ_TYPE_NONE = 0x00000000,
    IRQ_TYPE_EDGE_RISING = 0x00000001,
    IRQ_TYPE_EDGE_FALLING = 0x00000002,
    IRQ_TYPE_EDGE_BOTH = (IRQ_TYPE_EDGE_FALLING |
                          IRQ_TYPE_EDGE_RISING),
    IRQ_TYPE_LEVEL_HIGH = 0x00000004,
    IRQ_TYPE_LEVEL_LOW = 0x00000008,
    IRQ_TYPE_LEVEL_MASK = (IRQ_TYPE_LEVEL_LOW |
                           IRQ_TYPE_LEVEL_HIGH),
    // ...
};

IRQ_TYPE_EDGE_BOTH表示上升沿和下降沿同时有效,相当于KEY0按下和释放都会触发中断。

6.4 驱动源码分析

6.4.1 数据结构定义

/* 中断IO描述结构体 */
struct irq_keydesc {
    int gpio;                           /* gpio */
    int irqnum;                         /* 中断号 */
    unsigned char value;                /* 按键对应的键值 */
    char name[10];                      /* 名字 */
    irqreturn_t (*handler)(int, void*); /* 中断处理函数 */
};

/* imx6uirq设备结构体 */
struct imx6uirq_dev{
    dev_t devid;              /* 设备号 */
    struct cdev cdev;         /* cdev */
    struct class *class;      /* 类 */
    struct device *device;    /* 设备 */
    int major;                /* 主设备号 */
    int minor;                /* 次设备号 */
    struct device_node *nd;   /* 设备节点 */
    atomic_t keyvalue;        /* 有效的按键值 */
    atomic_t releasekey;      /* 标记按键是否松开 */
    struct timer_list timer;  /* 定义一个定时器 */
    struct irq_keydesc irqkeydesc[KEY_NUM]; /* 按键描述信息数组 */
    unsigned char curkeynum;  /* 当前按键号 */
};

struct imx6uirq_dev imx6uirq; /* irq设备 */

6.4.2 中断处理函数

// 中断处理函数,启动定时器
static irqreturn_t key0_handler(int irq, void *dev_id)
{
    struct imx6uirq_dev *dev = (struct imx6uirq_dev *)dev_id;

    dev->curkeynum = 0;
    dev->timer.data = (volatile long)dev_id;
    mod_timer(&dev->timer, jiffies + msecs_to_jiffies(10));
    return IRQ_RETVAL(IRQ_HANDLED);
}

6.4.3 定时器处理函数

// 定时器处理函数,完成按键消抖
void timer_function(unsigned long arg)
{
    unsigned char value;
    unsigned char num;
    struct irq_keydesc *keydesc;
    struct imx6uirq_dev *dev = (struct imx6uirq_dev *)arg;

    num = dev->curkeynum;
    keydesc = &dev->irqkeydesc[num];

    value = gpio_get_value(keydesc->gpio); /* 读取IO值 */
    if(value == 0){ /* 按下按键 */
        atomic_set(&dev->keyvalue, keydesc->value);
    }
    else{ /* 松开按键 */
        atomic_set(&dev->keyvalue, 0x80 | keydesc->value);
        atomic_set(&dev->releasekey, 1); /* 标记松开按键 */
    }
}

6.4.4 IO初始化函数

// IO初始化函数
static int keyio_init(void)
{
    unsigned char i = 0;
    int ret = 0;

    imx6uirq.nd = of_find_node_by_path("/key");
    if (imx6uirq.nd == NULL){
        printk("key node not find!\r\n");
        return -EINVAL;
    }

    /* 获取GPIO */
    for (i = 0; i < KEY_NUM; i++) {
        imx6uirq.irqkeydesc[i].gpio = of_get_named_gpio(imx6uirq.nd,
                                                          "key-gpio", i);
        if (imx6uirq.irqkeydesc[i].gpio < 0) {
            printk("can't get key%d\r\n", i);
        }
    }

    /* 初始化key所使用的IO,并且设置为中断模式 */
    for (i = 0; i < KEY_NUM; i++) {
        memset(imx6uirq.irqkeydesc[i].name, 0,
               sizeof(imx6uirq.irqkeydesc[i].name));
        sprintf(imx6uirq.irqkeydesc[i].name, "KEY%d", i);
        gpio_request(imx6uirq.irqkeydesc[i].gpio,
                     imx6uirq.irqkeydesc[i].name);
        gpio_direction_input(imx6uirq.irqkeydesc[i].gpio);
        imx6uirq.irqkeydesc[i].irqnum = irq_of_parse_and_map(
            imx6uirq.nd, i);
        printk("key%d:gpio=%d, irqnum=%d\r\n", i,
               imx6uirq.irqkeydesc[i].gpio,
               imx6uirq.irqkeydesc[i].irqnum);
    }

    /* 初始化中断 */
    imx6uirq.irqkeydesc[0].handler = key0_handler;
    imx6uirq.irqkeydesc[0].value = KEY0VALUE;

    for (i = 0; i < KEY_NUM; i++) {
        ret = request_irq(imx6uirq.irqkeydesc[i].irqnum,
                         imx6uirq.irqkeydesc[i].handler,
                         IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING,
                         imx6uirq.irqkeydesc[i].name, &imx6uirq);
        if(ret < 0){
            printk("irq %d request failed!\r\n",
                   imx6uirq.irqkeydesc[i].irqnum);
            return -EFAULT;
        }
    }

    /* 创建定时器 */
    init_timer(&imx6uirq.timer);
    imx6uirq.timer.function = timer_function;
    return 0;
}

6.4.5 驱动入口函数

static int __init imx6uirq_init(void)
{
    /* 1、创建设备号 */
    if (imx6uirq.major) {
        imx6uirq.devid = MKDEV(imx6uirq.major, 0);
        register_chrdev_region(imx6uirq.devid, IMX6UIRQ_CNT,
                               IMX6UIRQ_NAME);
    } else {
        alloc_chrdev_region(&imx6uirq.devid, 0, IMX6UIRQ_CNT,
                           IMX6UIRQ_NAME);
        imx6uirq.major = MAJOR(imx6uirq.devid);
        imx6uirq.minor = MINOR(imx6uirq.devid);
    }

    /* 2、注册字符设备 */
    cdev_init(&imx6uirq.cdev, &imx6uirq_fops);
    cdev_add(&imx6uirq.cdev, imx6uirq.devid, IMX6UIRQ_CNT);

    /* 3、创建类 */
    imx6uirq.class = class_create(THIS_MODULE, IMX6UIRQ_NAME);
    if (IS_ERR(imx6uirq.class)) {
        return PTR_ERR(imx6uirq.class);
    }

    /* 4、创建设备 */
    imx6uirq.device = device_create(imx6uirq.class, NULL,
                                    imx6uirq.devid, NULL,
                                    IMX6UIRQ_NAME);
    if (IS_ERR(imx6uirq.device)) {
        return PTR_ERR(imx6uirq.device);
    }

    /* 5、初始化原子变量 */
    atomic_set(&imx6uirq.keyvalue, INVAKEY);
    atomic_set(&imx6uirq.releasekey, 0);
    keyio_init();
    return 0;
}

6.4.6 驱动出口函数

static void __exit imx6uirq_exit(void)
{
    unsigned int i = 0;
    /* 删除定时器 */
    del_timer_sync(&imx6uirq.timer);

    /* 释放中断 */
    for (i = 0; i < KEY_NUM; i++) {
        free_irq(imx6uirq.irqkeydesc[i].irqnum, &imx6uirq);
        gpio_free(imx6uirq.irqkeydesc[i].gpio);
    }
    cdev_del(&imx6uirq.cdev);
    unregister_chrdev_region(imx6uirq.devid, IMX6UIRQ_CNT);
    device_destroy(imx6uirq.class, imx6uirq.devid);
    class_destroy(imx6uirq.class);
}

module_init(imx6uirq_init);
module_exit(imx6uirq_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("zuozhongkai");

6.5 应用程序源码

// 应用程序 imx6uirqApp.c
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"

int main(int argc, char *argv[])
{
    int fd;
    int ret = 0;
    char *filename;
    unsigned char data;

    if (argc != 2) {
        printf("Error Usage!\r\n");
        return -1;
    }

    filename = argv[1];
    fd = open(filename, O_RDWR);
    if (fd < 0) {
        printf("Can't open file %s\r\n", filename);
        return -1;
    }

    while (1) {
        ret = read(fd, &data, sizeof(data));
        if (ret < 0) { /* 数据读取错误 */
        } else {
            if (data) /* 读取到数据 */
                printf("key value = %#X\r\n", data);
        }
    }
    close(fd);
    return ret;
}

七、实验验证

7.1 编译方法

1. 编译驱动模块

编写Makefile文件:

KERNELDIR := /home/zuozhongkai/linux/IMX6ULL/linux/temp/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek
# ... 其他内容
obj-m := imx6uirq.o

clean:
    $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

编译驱动模块:

make -j32

编译成功后会生成imx6uirq.ko驱动模块文件。

2. 编译应用程序

arm-linux-gnueabihf-gcc imx6uirqApp.c -o imx6uirqApp

编译成功后会生成imx6uirqApp应用程序。

7.2 测试步骤

1. 加载驱动模块

depmod              # 第一次加载时需要执行
modprobe imx6uirq   # 加载驱动

2. 查看中断注册

cat /proc/interrupts

从中可以看到KEY0的中断信息:

  • 中断类型:Edge(边沿触发)
  • 中断号:49

3. 运行测试程序

./imx6uirqApp /dev/imx6uirq

按下开发板上的KEY0,终端就会输出按键值。

4. 卸载驱动

rmmod imx6uirq.ko

八、下半部机制对比

8.1 四种机制详细对比

特性 软中断(Softirq) Tasklet 工作队列 线程化中断
执行上下文 软中断上下文 软中断上下文 进程上下文 内核线程
可睡眠
并发性 同一软中断可并发 同一tasklet不可并发 可并发 可并发
执行时间 极短 可以较长 可以较长
复杂度
适用场景 高频事件、网络 快速处理 耗时操作 复杂处理
系统开销 最小 较大 较大

8.2 选择指南

graph TD
    A[需要中断下半部?] --> B{需要睡眠?}
    B -->|是| C{需要互斥?}
    B -->|否| D{处理时间?}
    C -->|是| E[线程化中断]
    C -->|否| F[工作队列]
    D -->|短| G[Tasklet]
    D -->|长| H[工作队列]
    G --> I{高频事件?}
    I -->|是| J[软中断]
    I -->|否| K[Tasklet]

8.3 性能对比

机制 延迟 吞吐量 CPU占用 内存占用
软中断 最低 最高 最低 最低
Tasklet
工作队列 中等 中等 中等 中等
线程化中断 较高 较低 较高 较高

九、跨平台对比

9.1 I.MX6ULL vs STM32 vs RK3568

特性 I.MX6ULL STM32 RK3568
中断控制器 GIC-400 NVIC GIC-400
中断优先级 256级 16级 256级
嵌套中断 支持 支持 支持
下半部机制 软中断/Tasklet/工作队列 软中断/Tasklet/工作队列
中断号获取 irq_of_parse_and_map HAL_NVIC_GetIRQ irq_of_parse_and_map
设备树支持 完整 完整

9.2 中断控制器对比

特性 GIC(ARM) NVIC(Cortex-M) PLIC(RISC-V)
中断数量 1020+ 240+ 1024+
优先级位数 8位 4位 可配置
中断嵌套 支持 支持 支持
软件触发 支持 不支持 支持

9.3 中断处理差异

平台 中断入口 下半部 特点
I.MX6ULL Linux内核管理 软中断/Tasklet/工作队列 完整的中断分层机制
STM32(裸机) 中断向量表 无或简单任务切换 直接、简单
STM32(FreeRTOS) 中断向量表 任务通知 简单的延迟处理
RK3568 Linux内核管理 软中断/Tasklet/工作队列 与I.MX6ULL类似

十、面试精选

题目1:中断上半部和下半部有什么区别?为什么要这样设计?

考察点: 中断处理机制

参考答案:

特性 上半部 下半部
执行上下文 硬中断上下文 软中断/进程上下文
执行时间 极短(微秒级) 可以较长(毫秒级)
可睡眠 不可 可以(工作队列)
优先级 最高 较低
中断屏蔽 会屏蔽其他中断 不会

设计原因:

  1. 硬件中断需要快速响应,不能长时间占用CPU
  2. 耗时操作会延迟其他中断的处理
  3. 分层设计提高了系统响应速度
  4. 下半部可以使用更多系统资源(如睡眠、互斥)

题目2:tasklet和workqueue有什么区别?如何选择?

考察点: 下半部机制

参考答案:

特性 tasklet workqueue
执行上下文 软中断 进程上下文
可睡眠 不可 可以
并发性 同一tasklet不能并发 可以并发
执行效率 较低
适用场景 快速处理 耗时操作

选择指南:

  • 快速处理、不需要睡眠 → tasklet
  • 耗时操作、需要睡眠 → workqueue
  • 高频事件 → 软中断
  • 需要互斥 → 线程化中断

题目3:工作队列是如何实现的?为什么可以睡眠?

考察点: 工作队列原理

参考答案:

工作队列通过工作者线程(worker thread)实现:

  1. 工作队列维护一个工作链表
  2. 工作者线程是一个内核线程,运行在进程上下文
  3. 工作者线程从工作队列中取出工作并执行
  4. 因为运行在进程上下文,所以可以调用可能导致睡眠的函数

关键数据结构:

  • work_struct:表示一个工作
  • workqueue_struct:表示工作队列
  • worker:表示工作者线程

题目4:在中断处理函数中能否调用mutex_lock?为什么?

考察点: 中断上下文限制

参考答案:

不能。原因:

  1. 中断处理函数运行在硬中断上下文
  2. mutex_lock可能导致睡眠
  3. 在中断上下文中睡眠会导致系统崩溃
  4. 中断上下文不能被调度,睡眠后无法恢复

解决方案:

  1. 使用自旋锁(spinlock)代替互斥锁
  2. 将耗时操作放到下半部执行
  3. 使用线程化中断,下半部可以睡眠

题目5:如何获取设备树中定义的中断号?

考察点: 设备树中断解析

参考答案:

有两种方法:

  1. 使用irq_of_parse_and_map函数

    unsigned int irq_of_parse_and_map(struct device_node *dev, int index);
    
  • dev:设备节点
  • index:中断索引号
  • 返回值:中断号
  1. 使用gpio_to_irq函数

    int gpio_to_irq(unsigned int gpio);
    
  • gpio:GPIO编号
  • 返回值:GPIO对应的中断号

设备树中断属性

interrupt-parent = <&gpio1>;  /* 指定中断控制器 */
interrupts = <18 IRQ_TYPE_EDGE_BOTH>;  /* 中断信息 */

interrupts属性格式取决于中断控制器,对于GIC:

  • 第一个cells:中断类型(0=SPI,1=PPI)
  • 第二个cells:中断号
  • 第三个cells:触发方式

代码来源: Linux驱动例程 13_irq 最后更新: 2026-09-17