嵌入式学习——操作系统的中断处理和ioctl
上半部完成了紧急工作后,剩下的任务就交给下半部去处理。下半部不需要立即执行,可以在系统相对空闲的时候被调度运行。解析和处理数据拷贝数据到用户空间唤醒等待的进程执行复杂的计算Linux 提供了多种下半部实现机制,常见的有软中断、tasklet 和工作队列。不同的机制在延迟、并发能力和能否睡眠方面有所区别。工作队列和 tasklet 是 Linux 驱动开发中最常用的两种下半部实现方式。它们的使用方法
一、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;
}
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐



所有评论(0)