ARM-驱动-06-中断底半部 + ioctl + 原子操作与锁

一、为什么需要中断底半部?

1.1 ISR 必须"快进快出"

中断服务程序(ISR)执行期间,CPU 不能响应同优先级或更低优先级的其他中断。如果 ISR 执行时间过长,会导致系统失去实时性。

举例:串口以 9600 波特率通信:

复制代码
每秒传输字节数 = 9600 / 10 = 960 字节/s
接收 100 字节需要约 104ms
104ms 内 CPU 本可完成 5~6 次进程切换
若 ISR 阻塞 104ms → 键盘、定时器等中断全部无法响应 → 系统卡死

结论 :任何耗时操作(数据拷贝、协议解析、文件写入)都不能放在 ISR 里。

1.2 顶半部 + 底半部 分离模型

复制代码
硬件中断触发
     ↓
顶半部(Top Half)------ ISR,只做最小必要操作
  ① 读寄存器、清中断标志
  ② 记录数据到缓冲区
  ③ 调度底半部任务
  ④ 立即返回(微秒级)
     ↓ (异步,不等待)
底半部(Bottom Half)------ 延后异步执行
  ① 数据解析、协议处理
  ② 文件 I/O、网络操作
  ③ 内存分配、复杂计算
  (毫秒级,安全可阻塞)

核心思想 :顶半部保障中断响应速度,底半部保障任务完整性,二者解耦


二、中断上下文 vs 进程上下文

这是理解底半部机制的基础,必须先弄清楚

对比项 中断上下文 进程上下文
包含内容 硬中断 ISR、软中断、tasklet 系统调用、内核线程、workqueue
执行主体 CPU 直接调用,无进程描述符 内核线程,受调度器管理
能否休眠 不能(致命错误) 可以
能否阻塞 不能 可以
可用的锁 只能用自旋锁 自旋锁、互斥锁均可

为什么中断上下文不能休眠?

休眠本质是调用 schedule() 让出 CPU。中断上下文没有进程描述符,内核调度器根本找不到它,无法将它挂起再恢复,系统会直接崩溃(内核 panic)。

类比

  • 中断上下文 = 消防员救火,必须一直冲,不能停下来休息
  • 进程上下文 = 维修队,可以排队等待、休息、慢慢干

三、底半部三大机制

3.1 对比总览

机制 执行上下文 能否休眠 适用场景
softirq 中断上下文 不能 内核核心模块(网络/定时器),不推荐开发者直接使用
tasklet 中断上下文 不能 高频、轻量、不需要睡眠的任务
workqueue 进程上下文 可以 复杂、耗时、需要休眠/阻塞的任务

注意:"软中断"(softirq)≠ 系统调用时的 SWI 软中断。前者是内核内部机制,后者是用户态进入内核的陷阱指令,完全不同的概念。


3.2 Tasklet

原理

Tasklet 基于 softirq 封装,在中断返回后由内核在软中断上下文中执行。

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

/* ① 定义 tasklet 结构体 */
struct tasklet_struct my_tasklet;

/* ② 编写处理函数(在中断上下文执行,不能休眠!) */
void my_tasklet_func(unsigned long data)
{
    // data 就是 tasklet_init 传入的第三个参数
    printk("Bottom Half (tasklet): data=%lu\n", data);
    // ❌ 绝对不能在这里 msleep() / mutex_lock() / kmalloc(GFP_KERNEL)
}

/* ③ 在 probe 里初始化 */
tasklet_init(&my_tasklet, my_tasklet_func, 123);

/* ④ 在顶半部 ISR 里调度(不是"调用",是"调度") */
static irqreturn_t key_irq_handler(int irq, void *dev)
{
    printk("Top Half\n");
    tasklet_schedule(&my_tasklet);  // 调度,立即返回,不等待
    return IRQ_HANDLED;
}

/* ⑤ 在 remove 里释放 */
tasklet_kill(&my_tasklet);

运行时序

复制代码
按键按下 → 硬件中断 → ISR 执行:
  printk("Top Half")
  tasklet_schedule()   ← 把 tasklet 加入软中断队列
  return IRQ_HANDLED   ← 顶半部立即返回

数毫秒后 → 软中断上下文执行:
  my_tasklet_func()
  printk("Bottom Half")

崩溃演示(绝对不要这样做)

c 复制代码
void my_tasklet_func(unsigned long data)
{
    msleep(1000);  // ← 在中断上下文调用 sleep → 系统立即崩溃!
    // 现象:dmesg 疯狂刷屏,内核 panic
}

3.3 Workqueue

原理

Workqueue 基于内核线程(kworker),在进程上下文中执行,因此可以安全地休眠、阻塞。

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

/* ① 定义 work 结构体 */
struct work_struct my_work;

/* ② 编写处理函数(进程上下文,可以休眠) */
void my_work_func(struct work_struct *work)
{
    msleep(1000);                        // ✅ 完全合法
    printk("Bottom Half (workqueue)\n"); // 1秒后才打印
}

/* ③ 初始化 */
INIT_WORK(&my_work, my_work_func);

/* ④ 在顶半部调度 */
static irqreturn_t key_irq_handler(int irq, void *dev)
{
    printk("Top Half\n");
    schedule_work(&my_work);  // 调度,立即返回
    return IRQ_HANDLED;
}

/* ⑤ 在 remove 里取消待执行的 work */
cancel_work_sync(&my_work);

选型建议

复制代码
需要 msleep / mutex / kmalloc(GFP_KERNEL) / 文件 I/O?
    → 必须用 workqueue

轻量、高频、不需要任何阻塞?
    → 可以用 tasklet(性能更好)

四、原子操作与锁

4.1 为什么需要同步?

普通变量 int i = 0; i++; 在汇编层面被拆成三步:

复制代码
读 → 加 → 写
     ↑ 如果在这里被中断或被另一个 CPU 打断 → 数据竞争 → 结果错误

4.2 原子操作(atomic_t)

适合单个整型变量的简单读写,比加锁开销更低。

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

atomic_t count;

atomic_set(&count, 0);      // 初始化为 0
atomic_inc(&count);         // 原子自增
atomic_dec(&count);         // 原子自减
atomic_add(5, &count);      // 原子加 5
int val = atomic_read(&count);  // 原子读

/* 原子加并返回新值 */
int new = atomic_add_return(1, &count);

原理 :底层由 ARM 的 LDREX/STREX 硬件指令保证"读-改-写"不可分割。

使用场景:中断处理程序、软中断、多核共享计数器。

4.3 自旋锁(spinlock)

忙等待,获取不到锁就一直循环检测(不睡眠)。

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

spinlock_t my_lock;
spin_lock_init(&my_lock);

/* 普通加锁/解锁(不关中断) */
spin_lock(&my_lock);
    // 临界区(时间要极短,<100μs)
spin_unlock(&my_lock);

/* 中断安全版本(关闭本地中断 + 加锁) */
unsigned long flags;
spin_lock_irqsave(&my_lock, flags);    // 保存中断状态 + 关中断 + 加锁
    // 临界区
spin_unlock_irqrestore(&my_lock, flags); // 解锁 + 恢复中断状态

为什么中断里只能用自旋锁?

自旋锁不会导致睡眠,只是 CPU 忙等。互斥锁获取失败会调用 schedule() 让出 CPU,在中断上下文这是致命操作。
irqsave 和普通 spin_lock 的区别 :如果中断处理程序和进程上下文都访问同一变量,必须用 irqsave 版本。否则进程持有锁时被中断打断,中断里也来申请同一个锁,会产生死锁。

4.4 互斥锁(mutex)

阻塞等待,获取不到锁就让进程睡眠,等待被唤醒。

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

struct mutex my_mutex;
mutex_init(&my_mutex);

mutex_lock(&my_mutex);    // 获取不到则睡眠等待
    // 临界区(可以很长,可以阻塞)
mutex_unlock(&my_mutex);

/* 非阻塞版本,获取不到立即返回 */
if (mutex_trylock(&my_mutex)) {
    // 获取成功
    mutex_unlock(&my_mutex);
}

4.5 锁的选型总结

场景 推荐
中断上下文(ISR / tasklet / softirq) 只能用自旋锁
进程上下文,临界区极短 自旋锁
进程上下文,临界区可能阻塞 互斥锁
多读少写 读写锁(rwlock)
单个整型计数器 原子操作(最轻量)

五、ioctl 接口

5.1 为什么需要 ioctl?

read/write 用于数据流,ioctl 用于命令控制,二者语义不同。

  • write("ledon") → 字符串匹配方式,效率低,容易出错
  • ioctl(fd, CMD_LED_ON, 0) → 整数命令,内核直接 switch,高效清晰

5.2 ioctl CMD 的结构(32位)

复制代码
 31      30 29      16 15       8 7        0
┌────────┬──────────┬───────────┬──────────┐
│direction│  size   │   type    │    nr    │
│  2 bit │  14 bit  │   8 bit   │  8 bit   │
└────────┴──────────┴───────────┴──────────┘
字段 含义
type 设备类型标识,用 ASCII 字符(如 'x''L''D'
nr 命令编号,从 0 开始
size 传递数据的字节数
direction 数据方向

四种构造宏

c 复制代码
_IO(type, nr)           // 无数据传递(纯命令,如 LED_ON)
_IOR(type, nr, datatype)  // 从驱动读数据到用户空间(如读温度)
_IOW(type, nr, datatype)  // 从用户空间写数据到驱动
_IOWR(type, nr, datatype) // 双向

5.3 led_ioctl.c 代码解析

驱动侧定义命令

c 复制代码
#include <asm/ioctl.h>

#define MAGIC_NUM  'x'     // 魔数:选一个不常用的 ASCII 字符
#define LED_ON     0       // 命令编号
#define LED_OFF    1
#define LED_REV    2

// _IO:无数据传递,只发命令
#define CMD_LED_ON   _IO(MAGIC_NUM, LED_ON)
#define CMD_LED_OFF  _IO(MAGIC_NUM, LED_OFF)
#define CMD_LED_REV  _IO(MAGIC_NUM, LED_REV)

驱动侧 ioctl 函数

c 复制代码
// 注意:挂到 file_operations 的字段名是 unlocked_ioctl,不是 ioctl
static long ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
    int ret = 0;
    switch (cmd) {
    case CMD_LED_ON:
        led_on();
        break;
    case CMD_LED_OFF:
        led_off();
        break;
    default:
        ret = -EINVAL;  // 未知命令返回 -EINVAL(无效参数)
        break;
    }
    return ret;
}

static struct file_operations fops = {
    .owner          = THIS_MODULE,
    .open           = open,
    .read           = read,
    .write          = write,
    .unlocked_ioctl = ioctl,   // ← 注意是 unlocked_ioctl
    .release        = close,
};

应用侧调用

c 复制代码
#include <sys/ioctl.h>
#include <fcntl.h>

// 应用层必须有相同的宏定义
#define MAGIC_NUM   'x'
#define CMD_LED_ON  _IO(MAGIC_NUM, 0)
#define CMD_LED_OFF _IO(MAGIC_NUM, 1)

int main(void)
{
    int fd = open("/dev/led", O_RDWR);
    if (fd < 0) { perror("open"); return -1; }

    ioctl(fd, CMD_LED_ON,  0);   // 开灯
    sleep(1);
    ioctl(fd, CMD_LED_OFF, 0);   // 关灯

    close(fd);
    return 0;
}

5.4 读取传感器数据时用 _IOR

c 复制代码
/* 驱动侧(如 dht11.c)*/
#define MAGIC_NUM       'd'
#define CMD_READ_HUMI   _IOR(MAGIC_NUM, 0, unsigned char)  // 驱动→用户
#define CMD_READ_TEMP   _IOR(MAGIC_NUM, 1, unsigned char)

static long dht11_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
    unsigned char val;
    switch (cmd) {
    case CMD_READ_TEMP:
        val = get_temperature();
        if (copy_to_user((void __user *)arg, &val, 1))
            return -EFAULT;
        break;
    }
    return 0;
}

/* 应用侧 */
unsigned char temp;
ioctl(fd, CMD_READ_TEMP, &temp);  // arg 传指针
printf("温度: %d°C\n", temp);

5.5 常见错误

错误现象 原因 解决
返回 -EINVAL CMD 未定义或 magic 冲突 检查魔数是否与系统保留冲突
返回 -ENOTTY 该设备不支持此 ioctl 确认驱动注册了 unlocked_ioctl
Permission denied 设备节点权限不足 chmod 666 /dev/led
驱动没有反应 使用了 .ioctl 而非 .unlocked_ioctl 改为 unlocked_ioctl

系统保留的 magic 查询cat /usr/include/asm-generic/ioctl.h 或内核文档 Documentation/userspace-api/ioctl/ioctl-number.rst


六、内核镜像补充知识

类型 说明
Image 未压缩的内核二进制(5~10MB)
zImage gzip 压缩后的镜像(2~4MB),实际烧写格式
bash 复制代码
make zImage   # 生成压缩镜像(推荐,节省空间和传输时间)
make Image    # 生成原始镜像(仅调试)
make dtbs     # 编译所有设备树
make pt.dtb   # 只编译指定的设备树

启动流程:Bootloader 加载 zImage → 解压到内存 → 跳转到 Image 入口 → 内核启动


九、全课知识体系总结

复制代码
Linux 驱动开发路径:

misc_led.c          → 地址写死,最原始
    ↓
led_device.c        → Platform 分离,地址在 led_device.c
+ led_driver.c
    ↓
led_platform.c      → Platform + DTS,地址在 .dts,reg 属性 + ioremap
    ↓
led_subgpio.c       → Platform + DTS + GPIO 子系统,无需 ioremap
    ↓
led_ioctl.c         → 增加 ioctl 接口,命令控制更规范
    ↓
key_irq.c           → 中断,gpio_to_irq,IRQ 基础
key_sub.c           → 中断 + 等待队列,read 阻塞直到按键
    ↓
(本节)             → 中断底半部(tasklet / workqueue)
                      原子操作与锁
                      DHT11 综合实战

核心开发原则

  1. ISR 快进快出,耗时操作交给 workqueue
  2. 中断上下文只能用自旋锁,不能睡眠
  3. DTS 的 compatible 必须与驱动 of_match_table 完全一致
  4. 时序敏感的代码用 local_irq_save 保护
  5. 所有申请的资源(GPIO、IRQ、misc)必须在 remove 里逆序释放
相关推荐
好家伙VCC2 小时前
**TEE在嵌入式安全中的应用实践:基于ARM TrustZone的加密存储方案设计与实现*
java·arm开发·python·struts·安全
进击的小头2 小时前
第9篇:嵌入式芯片指令集架构(ISA)详解:ARM_RISC-V等主流ISA全对比
arm开发·单片机·架构·risc-v
Yeats_Liao2 小时前
混合部署架构:CPU+GPU协同推理的任务调度策略
服务器·arm开发·人工智能·架构·边缘计算
篮子里的玫瑰2 小时前
一个隐藏的坑:MicroLib与串口打印的关系
驱动开发·stm32·嵌入式硬件
somi72 小时前
ARM-驱动-06-DHT11
linux·arm开发·自用
qq_401700412 小时前
大彩串口屏DC80480M070使用以及软件配置
嵌入式硬件
LNN202215 小时前
STM32H7 + 迪文屏 DGUS 开发实战:从零构建工业级时间设置界面
stm32·单片机·嵌入式硬件
Z文的博客18 小时前
嵌入式MCU与迪文屏通信:DMA+环形FIFO+变长队列+状态机完整手册
stm32·单片机·串口·dma·中断·串口dma·嵌入式单片机
12.=0.19 小时前
【stm32_5】Systick嘀嗒定时器、解析时钟源、分析时钟树、应用Systick设计延时
c语言·stm32·单片机·嵌入式硬件