一、Linux中断处理:上半部和下半部

中断处理有一个核心矛盾:响应要快,但事情可能很耗时。

  • 一方面,CPU 必须在收到中断后尽快处理,否则会丢失后续中断
  • 另一方面,有些设备的处理工作确实很复杂,比如网卡收到数据包后需要解析协议、拷贝数据

为了解决这个矛盾,Linux 将中断处理分成两个部分:上半部和下半部。

1、上半部(顶半部)

上半部是硬中断处理程序,它的核心特点是快速响应

当硬件设备触发中断时,CPU 会立即暂停当前任务,跳转到对应的中断处理函数。在这个函数里,只做最紧急、最基本的工作:

  • 应答中断控制器
  • 读取或清除设备状态寄存器
  • 保存硬件传来的关键数据
  • 调度下半部执行

上半部执行期间,当前中断线通常是关闭的,甚至所有中断都可能被暂时屏蔽。因此,上半部的代码必须极其精简,耗时越短越好,一般控制在几十微秒以内。

总结:上半部只做不得不马上做的事。

2、下半部(底半部)

上半部完成了紧急工作后,剩下的任务就交给下半部去处理。

下半部不需要立即执行,可以在系统相对空闲的时候被调度运行。它负责那些可以延迟的工作,比如:

  • 解析和处理数据
  • 拷贝数据到用户空间
  • 唤醒等待的进程
  • 执行复杂的计算

Linux 提供了多种下半部实现机制,常见的有软中断、tasklet 和工作队列。不同的机制在延迟、并发能力和能否睡眠方面有所区别。

总结:下半部做那些不着急、可以稍后处理的事。

3、工作队列和 tasklet 的使用方法

工作队列和 tasklet 是 Linux 驱动开发中最常用的两种下半部实现方式。它们的使用方法非常相似,都遵循固定的三步模式。

(1)初始化

在驱动的 probe 函数(或模块的 init 函数)中,对 work 或 tasklet 进行初始化。

工作队列初始化:

struct work_struct my_work;

INIT_WORK(&my_work, my_work_func);

tasklet 初始化:

struct tasklet_struct my_tasklet;

tasklet_init(&my_tasklet, my_tasklet_func, (unsigned long)dev);
(2)定义底半部函数

编写真正要延迟执行的工作函数。

工作队列的底半部函数:

void my_work_func(struct work_struct *work)
{
    /* 可睡眠的操作,比如:
     * - 拷贝数据到用户空间
     * - 等待互斥锁
     * - 分配大块内存
     * - I2C/SPI 读写
     */
}

tasklet 的底半部函数:

void my_tasklet_func(unsigned long data)
{
    /* 不能睡眠的操作,比如:
     * - 操作寄存器
     * - 更新链表
     * - 触发软中断
     */
}
(3)在顶半部中调度

在中断处理函数(上半部)中,调用调度函数触发下半部执行。

调度工作队列:

irqreturn_t my_isr(int irq, void *dev_id)
{
    /* 紧急工作:应答中断、保存状态 */
    
    schedule_work(&my_work);   /* 调度工作队列 */
    
    return IRQ_HANDLED;
}

调度 tasklet:

irqreturn_t my_isr(int irq, void *dev_id)
{
    /* 紧急工作:应答中断、保存状态 */
    
    tasklet_schedule(&my_tasklet);   /* 调度 tasklet */
    
    return IRQ_HANDLED;
}
三步总结:先初始化绑定函数,再写函数内容,最后在中断里调度。

4、上下半部的具体体现

理解了原理和用法后,再来看中断上下半部在实际代码中的具体分工:

 阶段 具体表现 特点
上半部 中断处理函数,只做紧急操作 + 调用调度函数 执行快,不能睡眠
下半部 被调度的底半部函数,做剩余所有工作 可慢,tasklet 不能睡眠,工作队列可以睡眠

整个流程可以概括为:

中断到来 → 进入上半部 → 做紧急工作 → 调度下半部 → 上半部结束 → 系统空闲时执行下半部

二、ioctl接口以:LED 亮灭控制为例

ioctl 是 Linux 驱动中实现设备特定操作的重要接口。有些操作既不是读也不是写(如设置波特率、控制 LED 亮灭),就由 ioctl 来统一处理。

1. ioctl 命令的编码格式(经典 32 位布局)

Linux 内核中,ioctl 的命令码是一个 32 位的整数,划分为四个字段:

位域 名称 作用
bit 31-30 dir(方向) 描述数据传输方向
bit 29-16 size(大小) 传输数据的大小(字节数)
bit 15-8 magic(魔数) 标识设备类型,通常用一个字符
bit 7-0 nr(序号) 区分同一设备的多个命令

2. 方向(dir)的定义

dir 字段有 4 种取值(从用户空间角度):

含义
_IOC_NONE 无数据传输
_IOC_WRITE 用户 → 内核(写驱动)
_IOC_READ 内核 → 用户(读驱动)
两者结合 双向传输

3. 内核提供的宏用于构造 cmd

内核提供了一组标准宏来简化命令码构造:

用途 数据方向
_IO(magic, nr) 无数据传输
_IOR(magic, nr, data_type) 从驱动读取数据
_IOW(magic, nr, data_type) 向驱动写入数据
_IOWR(magic, nr, data_type) 双向传输 读写

4. 示例中的命令分析

以 LED 控制为例:

#define LED_MAGIC  'l'

#define LED_ON      _IO(LED_MAGIC, 0)    // 点亮 LED,无数据
#define LED_OFF     _IO(LED_MAGIC, 1)    // 熄灭 LED,无数据
#define LED_STATUS  _IOR(LED_MAGIC, 2, int)   // 读取状态

以 LED_ON 为例拆解:_IO('l', 0)

  • magic = 'l'(0x6C)
  • nr = 0
  • dir = _IOC_NONE(无数据传输)
  • size = 0

5. 如果带数据,应该如何定义?

以设置 LED 亮度为例:

传入单个整数:

#define LED_SET_BRIGHTNESS  _IOW(LED_MAGIC, 3, int)

用户空间调用:

int brightness = 128;
ioctl(fd, LED_SET_BRIGHTNESS, &brightness);

驱动中接收:

int val;
get_user(val, (int __user *)arg);

传入结构体:

struct led_config {
    int pin;
    int brightness;
};

#define LED_SET_CONFIG  _IOW(LED_MAGIC, 4, struct led_config)

驱动中接收结构体时,必须使用 copy_from_user 安全拷贝:

struct led_config cfg;
if (copy_from_user(&cfg, (void __user *)arg, sizeof(cfg)))
    return -EFAULT;

6. 完整示例框架

long led_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
    struct led_device *dev = filp->private_data;
    int val;
    
    switch (cmd) {
        case LED_ON:
            gpio_set_value(dev->gpio, 1);
            break;
            
        case LED_OFF:
            gpio_set_value(dev->gpio, 0);
            break;
            
        case LED_STATUS:
            return put_user(dev->state, (int __user *)arg);
            
        case LED_SET_BRIGHTNESS:
            if (get_user(val, (int __user *)arg))
                return -EFAULT;
            /* 设置亮度 */
            break;
            
        default:
            return -ENOTTY;
    }
    return 0;
}

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐