ARM嵌入式学习(二十二)-- 操作系统的中断处理以及ioctl

目录

一、Linux操作系统的中断处理

1.上半部(顶半部)

2.下半部(底半部)

[3.工作队列(work queue)和tasklet使用方法](#3.工作队列(work queue)和tasklet使用方法)

(1)先在probe函数中(或者Init中)把work或者tasklet初始化:

(2)然后定义其结构体,实现结构体里面的中断底部函数

​编辑

(3)中断顶部函数只使用调度work或者tasklet的函数

4.在中断上下半部中的具体体现

二、ioctl接口:

举例代码led亮灭(ioctl控制):

cmd编码讲解:

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

(3)方向(dir)的定义

[(4)内核提供的宏用于构造 cmd](#(4)内核提供的宏用于构造 cmd)

(4)你例子中的命令分析

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

三、总结与补充:


一、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
下半部是否允许睡眠 --- 不允许 (不能调用 msleepssleepmutex_lock 等可能睡眠的函数) 允许 (可以调用 ssleepmsleepmutex_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_ioctlcompat_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:

相关推荐
南無忘码至尊2 小时前
Unity学习90天-第3天-认识触屏输入(手游基础)并完成手机点击屏幕,物体向点击位置移动
学习·unity·c#·游戏引擎·游戏开发
青桔柠薯片2 小时前
从字符设备到平台驱动:IMX6ULL LED 与蜂鸣器驱动开发学习总结
驱动开发·学习·imx6ull
JACK的服务器笔记2 小时前
《服务器测试百日学习计划——Day19:PCIe自动检测脚本,用Python把lspci设备清点标准化》
服务器·python·学习
南無忘码至尊2 小时前
Unity学习90天-第3天-认识C# 集合与常用类并实现生成随机位置的 10 个立方体
学习·unity·c#
_李小白2 小时前
【OSG学习笔记】Day 47:相机漫游实现
笔记·数码相机·学习
知识分享小能手2 小时前
MongoDB入门学习教程,从入门到精通,MongoDB监控完全指南(22)
数据库·学习·mongodb
_李小白2 小时前
【OSG学习笔记】Day 46: CameraManipulator(相机操控器)
笔记·数码相机·学习
@小匠8 小时前
Read Frog:一款开源的 AI 驱动浏览器语言学习扩展
人工智能·学习
炽烈小老头15 小时前
【 每天学习一点算法 2026/04/12】x 的平方根
学习·算法