目录
[3.工作队列(work queue)和tasklet使用方法](#3.工作队列(work queue)和tasklet使用方法)
(1)先在probe函数中(或者Init中)把work或者tasklet初始化:
(3)中断顶部函数只使用调度work或者tasklet的函数
[(1)ioctl 命令的编码格式(经典 32 位布局)](#(1)ioctl 命令的编码格式(经典 32 位布局))
[(4)内核提供的宏用于构造 cmd](#(4)内核提供的宏用于构造 cmd)
一、Linux操作系统的中断处理
中断发生时,CPU 需要立刻响应硬件请求。但中断处理程序(ISR)如果执行时间过长,会阻塞其他中断和重要任务,导致系统响应变慢甚至丢数据。
因此,Linux 等操作系统将中断处理拆成两部分:
-
上半部(Top Half):由 CPU 直接调用,处理最紧急、必须立即完成的工作。
-
下半部(Bottom Half):处理可延迟、不太紧急的剩余工作,可以被调度执行。

1.上半部(顶半部)
-
触发时机:中断发生瞬间,CPU 立即执行。
-
主要任务:
-
保存必要的硬件状态(如寄存器)。
-
清除中断标志(避免反复触发)。
-
屏蔽本中断线(可选)。
-
读取硬件数据,确认中断来源。
-
调度下半部(通知下半部有工作要做)。
-
-
特点:
-
短小精悍:必须快速执行,通常只做几十微秒的工作。
-
快进快出:不能睡眠、不能调度、不能做复杂运算。
-
中断上下文:运行在中断上下文,没有进程与之关联。
-
禁止响应同类型中断(某些架构下会关闭当前中断线或全局中断)。
-
图片中的"返回"是指上半部执行完毕,返回被中断的进程或空闲任务。
2.下半部(底半部)
-
触发时机:由上半部"调度"启动,可以稍后执行(可能在返回用户态之前,也可能在专门的软中断/任务线程中)。
-
主要任务:
-
处理真正的中断业务逻辑(如网络数据包解析、键盘按键处理、磁盘传输完成后的数据整理)。
-
唤醒等待该事件的进程。
-
数据拷贝、复杂计算、可能涉及内存分配或信号量操作。
-
-
实现方式(Linux 为例):
-
软中断(softirq)
-
tasklet
-
工作队列(work queue)
-
线程化中断
-
-
特点:
-
可延迟:不要求立即执行,可以等 CPU 空闲时再处理。
-
可被中断:执行下半部时,可以被新的硬件中断打断。
-
可能允许睡眠 (取决于实现方式:工作队列可以睡眠;软中断/tasklet 不能睡眠)。
-
执行时间相对较长(毫秒级甚至更长,但仍需合理设计)。
-
图片中的"调度"指上半部执行完后,系统会在合适的时机(如返回用户态前、软中断检查点)调度下半部运行。
3.工作队列(work queue)和tasklet使用方法
不管是tasklet还是workqueue用法都是一样的
(1)先在probe函数中(或者Init中)把work或者tasklet初始化:
(1)workqueue
INIT_WORK(&work, key_work_func);
(2)tasklet
tasklet_init(&tsk, key_tasklet_func, 123);//123 是作为 传递给 tasklet 回调函数的参数。
这里我在probe中初始化:

(2)然后定义其结构体,实现结构体里面的中断底部函数
这里用workqueue举例
(3)中断顶部函数只使用调度work或者tasklet的函数
schedule_work(&work); tasklet_schedule(&tsk);
如上图,这样就是我们操作系统中的中断处理,和我们之前的key代码有什么区别呢?
4.在中断上下半部中的具体体现
-
上半部 中写"CPU调用的" ------ 这里"调用"指 CPU 响应硬件中断后,直接跳转 到中断服务程序(ISR)执行。这是一种强制、立即的函数调用。
-
下半部 前写"调度" ------ 上半部执行完后,通过某种机制(如触发软中断、提交工作队列)通知内核:稍后请运行下半部。具体何时运行、哪个 CPU 运行,由内核的调度器决定。
与之前的key中断代码相比:
| 对比维度 | 版本一:之前的kye代码(无下半部) | 版本二:tasklet 下半部 | 版本三:工作队列下半部 |
|---|---|---|---|
中断上半部 (key_irq_handler) 所做工作 |
直接设置 condition=1,唤醒等待队列,打印信息 |
仅调用 tasklet_schedule(&tsk),打印信息 |
仅调用 schedule_work(&work),打印信息 |
| 下半部机制 | 无 | tasklet(软中断上下文) | 工作队列(内核线程上下文) |
| 下半部回调函数 | 无 | key_tasklet_func |
key_work_func |
| 下半部执行上下文 | --- | 软中断上下文(中断上下文的一种,但中断开启,可被其他中断抢占) | 进程上下文(内核工作线程,如 events/X) |
| 下半部是否允许睡眠 | --- | 不允许 (不能调用 msleep、ssleep、mutex_lock 等可能睡眠的函数) |
允许 (可以调用 ssleep、msleep、mutex_lock、进行 I/O 操作等) |
| 示例中下半部是否实际睡眠 | --- | 否(只唤醒等待队列) | 是 ,调用了 ssleep(1)(睡眠 1 秒) |
| 中断延迟(上半部耗时) | 较长(包含唤醒队列和调度开销) | 极短(仅调度 tasklet) | 极短(仅调度 work) |
| 下半部执行时机 | --- | 软中断上下文,通常在硬件中断返回前执行(高优先级) | 内核工作线程调度执行(优先级较低,可被抢占) |
| 对实时性的影响 | 较差:长时间关中断,可能延迟其他中断 | 较好:上半部快,下半部可被中断,但 tasklet 仍不能睡眠,执行时间不宜过长 | 很好:上半部极快,下半部可睡眠,适合耗时或阻塞操作,但响应延迟比 tasklet 稍大 |
| 适用场景 | 中断处理极简单、时间极短且无需睡眠的场景(如仅清中断、置标志) | 中断处理需要一定计算,但不会睡眠,且要求较低延迟的场景(如网络收包轻量处理) | 中断处理需要大量计算、可能睡眠(如访问 I2C/SPI 设备、等待硬件完成、延迟操作)的场景 |
| 优点 | 实现简单,唤醒及时 | 上半部快,下半部不会影响中断响应,且延迟比工作队列低 | 可以睡眠,能处理复杂耗时的任务;不会阻塞中断;适合与硬件交互需要等待的驱动 |
| 缺点 | 中断处理时间过长会阻塞系统,不适合高频或复杂中断 | 下半部不能睡眠,不能做长时间 I/O 或延迟操作 | 下半部调度延迟较大(可能几十毫秒),不适合对时间要求苛刻的任务 |
使用中断下半部的优势远大于其微不足道的延迟,因此在真实驱动开发中,除非中断处理极简单且确定不会影响系统,否则推荐使用下半部(tasklet、工作队列、软中断等)来分离紧急工作和可延迟工作。
二、ioctl接口:
应用层和驱动使用的Ioctl函数其参数cmd 都一样,所有定义的宏应用层和驱动都要一致:
cmd 是我们自定义的命令,格式为:

Type是设备,num是命令个数,Dir是方向,size是大小。
arg是应用层传给驱动的参数。
举例代码led亮灭(ioctl控制):
#include <linux/init.h>
#include <linux/printk.h>
#include <linux/kdev_t.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/export.h>
#include <asm/uaccess.h>
#include <asm/string.h>
#include <asm/io.h>
#include <linux/miscdevice.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/of_gpio.h>
#include <linux/gpio.h>
#include <asm/ioctl.h>
#define DEV_NAME "led"
static int led_gpio;
#define LED_ON 0
#define LED_OFF 1
#define LED_REV 2
#define MAGIC_NUM 'x'
#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)
static void led_init(void)
{
gpio_direction_output(led_gpio, LED_OFF);
}
static void led_on(void)
{
gpio_set_value(led_gpio, LED_ON);
}
static void led_off(void)
{
gpio_set_value(led_gpio, LED_OFF);
}
static int open(struct inode * node, struct file * file)
{
led_init();
printk("led open...\n");
return 0;
}
static ssize_t read(struct file * file, char __user * buf, size_t len, loff_t * offset)
{
//copy_to_user();
printk("led read...\n");
return 0;
}
static ssize_t write(struct file * file, const char __user * buf, size_t len, loff_t * offset)
{
// "ledon" on "ledoff" off
unsigned char data[10] = {0};
size_t len_cp = len < sizeof(data) ? len : sizeof data;
int size_cp = copy_from_user(data, buf, len_cp);
if(size_cp < 0)
return size_cp;
if(!strcmp(buf, "ledon"))
led_on();
else if(!(strcmp(buf, "ledoff")))
led_off();
else
return -EINVAL;
printk("led write...\n");
return size_cp;
}
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;
break;
}
return ret;
}
static int close(struct inode * node, struct file * file)
{
led_off();
printk("led close...\n");
return 0;
}
static struct file_operations fops =
{
.owner = THIS_MODULE,
.open = open,
.read = read,
.write = write,
.unlocked_ioctl = ioctl,
.release = close
};
static struct miscdevice misc =
{
.minor = MISC_DYNAMIC_MINOR,
.name = DEV_NAME,
.fops = &fops
};
static int probe(struct platform_device * pdev)
{
struct device_node * pnode;
int ret = misc_register(&misc);
if(IS_ERR_VALUE(ret))
goto err_misc;
pnode = of_find_node_by_path("/pt_gpioled");
if(IS_ERR(pnode))
{
ret = PTR_ERR(pnode);
goto err_find_node;
}
led_gpio = of_get_named_gpio(pnode, "led-gpio", 0);
gpio_request(led_gpio, "red_led");
gpio_direction_output(led_gpio, LED_OFF);
printk("probe led misc_register ##############\n");
return 0;
err_find_node:
printk("of_find_node_by_path err\n");
err_misc:
printk("led probe failed ret = %d\n", ret);
misc_deregister(&misc);
return ret;
}
static int remove(struct platform_device * pdev)
{
gpio_free(led_gpio);
misc_deregister(&misc);
printk("remove led misc_deregister ##############\n");
return 0;
}
static const struct of_device_id match_table[] =
{
[0] = {.compatible = "pt-gpioled"}
};
static struct platform_driver drv =
{
.probe = probe,
.remove = remove,
.driver =
{
.name = DEV_NAME,
.of_match_table = match_table
}
};
static int __init led1_init(void)
{
int ret = platform_driver_register(&drv);
if(ret < 0)
goto err_reg;
printk("platform_driver_register ...\n");
return 0;
err_reg:
printk("platform_driver_register failed ret = %d\n", ret);
platform_driver_unregister(&drv);
return ret;
}
static void __exit led1_exit(void)
{
platform_driver_unregister(&drv);
printk("platform_driver_unregister ...\n");
}
module_init(led1_init);
module_exit(led1_exit);
MODULE_LICENSE("GPL");
应用层调用ioctl(fd,CMD_LED_ON,0);即可打开灯,同理,用对应的宏可以实现对应的功能。
cmd编码讲解:
ioctl接口的使用,只需要把cmd的格式搞清楚就好。
(1)ioctl 命令的编码格式(经典 32 位布局)
通常分为 4 个字段(不同架构可能位数略有差异,但概念一致):
| 位域 | 名称 | 作用 |
|---|---|---|
| bit 31~30 | 方向 (dir) | 表示数据是否从用户空间读、写,或双向传递 |
| bit 29~16 | 数据大小 (size) | 表示 ioctl 第三个参数所指向的数据大小(字节) |
| bit 15~8 | 魔数 (magic) | 用来区分不同设备的驱动,通常用一个字符的 ASCII 值 |
| bit 7~0 | 序号 (nr) | 该设备驱动内部的命令编号(0~255) |
注意 :有些架构(如 x86_64)实际只使用了 8 位给 nr、8 位给 magic、2 位给方向,数据大小字段在不同内核版本中可能被限制为 14 位或更少,但核心概念一致。
(3)方向(dir)的定义
| 宏 | 方向值 | 含义 |
|---|---|---|
_IOC_NONE |
0U | 没有数据传递(仅发命令) |
_IOC_WRITE |
1U | 数据从用户空间写入内核(内核读取用户数据) |
_IOC_READ |
2U | 数据从内核读出到用户空间(内核写入用户数据) |
| `_IOC_READ | _IOC_WRITE` | 3U |
注意:
_IOC_WRITE的含义是"用户写,内核读"(即内核会从用户空间取数据);_IOC_READ是"用户读,内核写"(即内核会将数据填给用户空间)。这个命名容易混淆,需要记清楚。
(4)内核提供的宏用于构造 cmd
| 宏定义 | 方向 | 是否带参数 |
|---|---|---|
_IO(magic, nr) |
_IOC_NONE |
无参数 |
_IOR(magic, nr, type) |
_IOC_READ |
内核向用户写数据(用户读取数据) |
_IOW(magic, nr, type) |
_IOC_WRITE |
用户向内核写数据(内核读取用户数据) |
_IOWR(magic, nr, type) |
双向 | 双向传递数据 |
其中 type 是一个数据类型,宏内部会使用 sizeof(type) 自动填充数据大小字段。
(4)你例子中的命令分析
#define MAGIC_NUM 'x' #define LED_ON 0 #define LED_OFF 1 #define LED_REV 2 #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)
-
_IO表示 没有数据交换 (方向为_IOC_NONE),只传递命令本身。 -
魔数(magic)是字符
'x'(ASCII 0x78),用来标识这个设备(比如 LED 驱动)。 -
序号(nr)分别是 0、1、2,表示不同操作:开灯、关灯、翻转。
-
因为不涉及数据传递,所以没有数据大小字段。
在使用时,用户程序调用:
ioctl(fd, CMD_LED_ON, 0); // 第三个参数没有意义(可传 NULL 或 0)
内核驱动中的 unlocked_ioctl 或 compat_ioctl 会根据 cmd 值执行相应操作,不需要从用户空间复制数据。
(5)如果带数据,应该如何定义?
假设你有一个 LED 亮度值,范围 0~255,需要从用户空间传给内核:
#define MAGIC_NUM 'x' #define SET_BRIGHTNESS 1 // 第二个命令,传递 unsigned char 类型的数据 #define CMD_SET_BRIGHTNESS _IOW(MAGIC_NUM, SET_BRIGHTNESS, unsigned char)
此时内核会知道:
-
方向:用户写,内核读
-
数据大小:
sizeof(unsigned char)= 1 字节
在驱动中就可以安全地使用 copy_from_user 获取数据。
三、总结与补充:
补充1:硬件中断即硬中断可打断tasklet中断(tasklet无法打断tasklet中断)
补充2:
