Linux驱动开发(12):中断子系统–按键中断实验
本章我们以按键为例讲解在驱动程序中如何使用中断, 在学习本章之前建议先回顾一下关于中断相关的裸机部分相关章节, 这里主要介绍在驱动中如何使用中断,对于中断的概念及GIC中断控制器相关内容不再进行讲解。
本章配套源码和设备树插件位于“~/linux_driver/button_interrupt”目录下。
1. 在设备树中添中断信息以及中断基本函数介绍
1.1. 设备树中的中断相关内容
让我们先来了解一下设备树是如何描述整个中断系统信息的。
1.1.1. 顶层中断控制器
打开 ./arch/arm/boot/dts/ 目录下的 imx6ull.dtsi 设备树文件, 找到“interrupt-controller”节点,如下所示。
中断interrupt-controller节点:
intc: interrupt-controller@a01000 {compatible = "arm,cortex-a7-gic";#interrupt-cells = <3>;interrupt-controller;reg = <0xa01000 0x1000>,<0xa02000 0x100>;
};
-
compatible:compatible属性用于平台设备驱动的匹配
-
reg:reg指定中断控制器相关寄存器的地址及大小
-
interrupt-controller:声明该设备树节点是一个中断控制器。
-
#interrupt-cells :指定它的“子”中断控制器用几个cells来描述一个中断,可理解为用几个参数来描述一个中断信息。 在这里的意思是在intc节点的子节点将用3个参数来描述中断。
学过裸机章节的同学们想必对GIC中断控制器并不陌生,GIC架构分为了:分发器(Distributor)和 CPU接口(CPU Interface),上面设备树节点就是用来描述整个GIC控制器的。
(1) Distributor
- 作用:
- 负责中断的全局管理和分发。
- 确定中断需要发送到哪个 CPU 核心。
- 功能:
- 配置中断优先级。
- 设置中断目标 CPU。
- 启用或禁用中断。
- 接口:通常挂载在 SoC 的外设总线(如 AXI 或 APB)。
(2) CPU Interface
- 作用:
- 为每个 CPU 核心提供接口,用于处理中断。
- 确保中断到达正确的 CPU 核心。
- 功能:
- 向 CPU 发起中断信号(如 IRQ 或 FIQ)。
- 提供当前活动中断的 ID 和状态。
- 确认中断已处理(EOI)。
1.1.2. gpc一级子中断控制器
在imx6ull.dtsi文件中直接搜索节点标签“intc”即可找到“一级子中断控制器”
一级子中断控制器:
gpc: gpc@20dc000 {compatible = "fsl,imx6ul-gpc", "fsl,imx6q-gpc";reg = <0x20dc000 0x4000>;interrupt-controller;#interrupt-cells = <3>;interrupts = <GIC_SPI 89 IRQ_TYPE_LEVEL_HIGH>;interrupt-parent = <&intc>;fsl,mf-mix-wakeup-irq = <0xfc00000 0x7d00 0x0 0x1400640>;
};
结合以上代码介绍如下:
-
interrupt-controller:声明该设备树节点是一个中断控制器,只要是中断控制器都要用该标签声明。
-
#interrupt-cells:用于规定该节点的“子”中断控制器将使用三个参数来描述子控制器的信息。
-
interrupt-parent:指定该中断控制器的“父”中断控制器。除了“顶层中断控制器”其他中断控制器都要声明“父”中断控制器。
-
interrupts:具体的中断描述信息,在该节点的中断控制器的“父”中断控制器,规定了使用三个cells来描述子控制器的信息。 三个参数表示的含义如下:
第一个参数用于指定中断类型,在GIC中中断的类型有三种(SPI共享中断、PPI私有中断、SGI软件中断), 我们使用的外部中断均属于SPI中断类型。
第二个参数用于设定中断编号,范围和第一个参数有关。PPI中断范围是[0-15],SPI中断范围是[0-987]。
第三个参数指定中断触发方式,参数是一个u32类型,其中后四位[0-3]用于设置中断触发类型。 每一位代表一个触发方式,可进行组合,系统提供了相对的宏顶义我们可以直接使用,如下所示:
中断触发方式设置:
#define IRQ_TYPE_NONE 0
#define IRQ_TYPE_EDGE_RISING 1
#define IRQ_TYPE_EDGE_FALLING 2
#define IRQ_TYPE_EDGE_BOTH (IRQ_TYPE_EDGE_FALLING | IRQ_TYPE_EDGE_RISING)
#define IRQ_TYPE_LEVEL_HIGH 4
#define IRQ_TYPE_LEVEL_LOW 8
[8-15]位在PPI中断中用于设置“CPU屏蔽”。在多核系统中这8位用于设置PPI中断发送到那个CPU,一位代表一个CPU, 为1则将PPI中断发送到CPU,否则屏蔽。imx6ull是单核CPU,所以我们不用设置这些位。
1.1.3. 二级子中断控制器
同样在imx6ull.dtsi文件中直接搜索节点标签“gpc”即可找到“二级子中断控制器”如下所示。
中断触发方式设置:
soc {#address-cells = <1>;#size-cells = <1>;compatible = "simple-bus";interrupt-parent = <&gpc>;ranges;//busfreq子节点busfreq {................ //表示省略}............... //表示省略
soc 节点即片上外设“总节点”,翻阅源码可以发现该节点很长,我们使用的外设大多包含在里面。 具体外设(例如GPIO)也可作为中断控制器,这里声明了它们的“父”中断控制器是 <&gpc>节点。
soc节点内包含的中断控制器很多,几乎用到中断的外设都是中断控制器,我们使用的是开发板上的按键, 使用的是GPIO5_1,所以这里以GPIO5为例介绍。在imx6ull.dtsi文件中直接搜索GPIO5,找到GPIO5对应的设备树节点,如下所示。
gpio5:
gpio5: gpio@20ac000 {compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio";reg = <0x20ac000 0x4000>;interrupts = <GIC_SPI 74 IRQ_TYPE_LEVEL_HIGH>,<GIC_SPI 75 IRQ_TYPE_LEVEL_HIGH>;clocks = <&clks IMX6UL_CLK_GPIO5>;gpio-controller;#gpio-cells = <2>;interrupt-controller;#interrupt-cells = <2>;gpio-ranges = <&iomuxc 0 7 10>, <&iomuxc 10 5 2>;
};
以上是gpio5节点的全部内容,这里主要介绍和中断相关的节点信息。
-
interrupts:用来描述GPIO5能产生中断类型及中断编号、触发方式,查看imx6ull的数据手册我们可以知道, GPIO5能产生的中断只有两个,分配的中断ID为106、107,对于SPI中断它们的编号是74(106-32),75(107-32)。
-
interrupt-controller:声明该节点是一个中断控制器
-
#interrupt-cells:声明该节点的子节点将用多少个参数来描述中断信息。
1.1.4. 按键设备树节点
以上三部分的内容是内核为我们提供的,我们要做的内容很简单, 只需要在我们编写的设备树节点中 引用已经写好的中断控制器父节点以及配置中断信息即可,如下所示。
button按键设备节点:
button_interrupt {compatible = "button_interrupt";pinctrl-names = "default";pinctrl-0 = <&pinctrl_button>;button_gpio = <&gpio5 1 GPIO_ACTIVE_LOW>; //默认低电平,按键按下高电平status = "okay";interrupt-parent = <&gpio5>;interrupts = <1 IRQ_TYPE_EDGE_RISING>; // 指定中断,触发方式为上升沿触发。
};
这里主要介绍和中断相关部分的内容。
-
interrupt-parent:指定“父控制器节点 ”。我们按键所在的引脚是gpio5_1,故我们按键所在的中断控制父节点 为gpio5。
-
interrupts:在gpio5节点中定义使用两个cells来描述我们的按键信息,‘1’表示的是我们按键GPIO5中引脚编号, “IRQ_TYPE_EDGE_RISING”表示的是触发方式。触发方式宏定义如下:
中断触发类型设置:
#define IRQ_TYPE_NONE 0
#define IRQ_TYPE_EDGE_RISING 1
#define IRQ_TYPE_EDGE_FALLING 2
#define IRQ_TYPE_EDGE_BOTH (IRQ_TYPE_EDGE_FALLING | IRQ_TYPE_EDGE_RISING)
#define IRQ_TYPE_LEVEL_HIGH 4
#define IRQ_TYPE_LEVEL_LOW 8
需要注意的是,我们编写的这个节点并不是个中断控制器,所以没有“interrupt-controller”标签。
1.2. 中断相关函数
1.2.1. request_irq中断注册函数
申请中断:
static inline int __must_check request_irq(unsigned int irq, irq_handler_t handler,unsigned long flags, const char *name, void *dev)
参数:
-
irq:用于指定“内核中断号”,这个参数我们会从设备树中获取或转换得到。在内核空间中它代表一个唯一的中断编号。
-
handler:用于指定中断处理函数,中断发生后跳转到该函数去执行。
-
flags:中断触发条件,也就是我们常说的上升沿触发、下降沿触发等等 触发方式通过“|”进行组合(注意,这里的设置会覆盖设备树中的默认设置),宏定义如下所示:
中断触发方式:
#define IRQF_TRIGGER_NONE 0x00000000
#define IRQF_TRIGGER_RISING 0x00000001
#define IRQF_TRIGGER_FALLING 0x00000002
#define IRQF_TRIGGER_HIGH 0x00000004
#define IRQF_TRIGGER_LOW 0x00000008
#define IRQF_TRIGGER_MASK (IRQF_TRIGGER_HIGH | IRQF_TRIGGER_LOW | \IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING)
#define IRQF_TRIGGER_PROBE 0x00000010#define IRQF_SHARED 0x00000080 ---------①
/*-----------以下宏定义省略------------*/
-
name:中断的名字,中断申请成功后会在“/proc/interrupts”目录下看到对应的文件。
-
dev: 如果使用了**IRQF_SHARED** 宏,则开启了共享中断。“共享中断”指的是多个驱动程序共用同一个中断。 开启了共享中断之后,中断发生后内核会依次调用这些驱动的“中断服务函数”。 这就需要我们在中断服务函数里判断中断是否来自本驱动,这里就可以用dev参数做中断判断。 即使不用dev参数判断中断来自哪个驱动,在申请中断时也要加上dev参数 因为在注销驱动时内核会根据dev参数决定删除哪个中断服务函数。
返回值:
-
成功:返回0
-
失败:返回负数。
1.2.2. 中断注销函数free_irq
申请中断:
void free_irq(unsigned int irq, void *dev);
参数:
-
irq:从设备树中得到或者转换得到的中断编号。
-
dev:与request_irq函数中dev传入的参数一致。
返回值:无
1.2.3. 中断处理函数
在中断申请时需要指定一个中断处理函数,书写格式如下所示。
中断服务函数格式:
irqreturn_t (*irq_handler_t)(int irq, void * dev);
参数:
-
irq:用于指定“内核中断号”。
-
dev:在共享中断中,用来判断中断产生的驱动是哪个,具体介绍同上中断注册函数。 不同的是dev参数是内核“带回”的。如果使用了共享中断还得根据dev带回的硬件信息判断中断是否来自本驱动,或者不使用dev, 直接读取硬件寄存器判断中断是否来自本驱动。如果不是,应当立即跳出中断服务函数,否则正常执行中断服务函数。
返回值:
-
irqreturn_t类型:枚举类型变量,如下所示。
中断服务函数返回值类型:
enum irqreturn {IRQ_NONE = (0 << 0),IRQ_HANDLED = (1 << 0),IRQ_WAKE_THREAD = (1 << 1),
};typedef enum irqreturn irqreturn_t;
如果是“共享中断”并且在中断服务函数中发现中断不是来自本驱动则应当返回 IRQ_NONE , 如果没有开启共享中断或者开启了并且中断来自本驱动则返回 IRQ_HANDLED,表示中断请求已经被正常处理。 第三个参数涉及到我们后面会讲到的中断服务函数的“上半部分”和“下半部分”, 如果在中断服务函数是使用“上半部分”和“下半部分”实现,则应当返回IRQ_WAKE_THREAD。
1.2.4. 中断的使能和禁用函数
通过函数使能、禁用某一个中断。
中断的使能和禁用函数:
void enable_irq(unsigned int irq)
void disable_irq(unsigned int irq)
参数:
-
irq:指定的“内核中断号”
1.2.5. 关闭和开启全局中断相关函数(宏定义)
关闭和开启全局中断相关函数:
local_irq_enable()
local_irq_disable()
local_irq_save(flags)
local_irq_restore(flags)
由于“全局中断”的特殊性,通常情况下载关闭之前要使用local_irq_save保存当前中断状态, 开启之后使用local_irq_restore宏恢复关闭之前的状态。flags是一个unsigned long 类型的数据。
了解了以上函数的作用,我们就可以编写中断的驱动程序了, 如有遗漏的内容我们将会在代码介绍中,驱动程序介绍如下。
2. 按键中断程序实现
2.1. 设备树插件实现
设备树插件:
/dts-v1/;
/plugin/;#include "../imx6ul-pinfunc.h"
#include "dt-bindings/interrupt-controller/irq.h"
#include "dt-bindings/gpio/gpio.h"/ {fragment@0 {target-path = "/";__overlay__ {button_interrupt {compatible = "button_interrupt";pinctrl-names = "default";pinctrl-0 = <&pinctrl_button>;button_gpio = <&gpio5 1 GPIO_ACTIVE_LOW>;status = "okay";interrupt-parent = <&gpio5>;interrupts = <1 IRQ_TYPE_EDGE_RISING>;};};};fragment@1 {target = <&iomuxc>;__overlay__ {pinctrl_button: buttongrp {fsl,pins = <MX6UL_PAD_SNVS_TAMPER1__GPIO5_IO01 0x10b0>;};};};};
-
第4-6行:我们在设备树插件中用了几个宏定义,这里需要包含相应头文件,
-
第10-24行,新增的button_interrupt节点,
-
第12行,指定设备节点插入位置,这里是根节点。
-
第18行,定义button使用的GPIO。
-
第20-21行,添加中断相关信息。
-
第27-35行,配置按键所在引脚。
2.2. 按键中断驱动程序实现
这里不再介绍有关字符设备的内容,重点放在驱动程序中如何使用中断。 完整的代码请参考本章配套例程。
虽然使用了设备树(设备树插件)但是驱动程序是一个简单的字符设备驱动,不会和设备树中的节点匹配。 无论是否匹配与我们“读设备树”无关,驱动源码大致分为驱动入口和出口函数实现、字符设备操作函数实现两部分内容, 结合源码介绍如下:
2.2.1. 驱动入口和出口函数实现
在驱动的入口函数中实现字符设备的注册, 在出口函数中注销字符设备,部分代码如下所示:
驱动入口和出口函数实现:
/*
*驱动初始化函数
*/
static int __init button_driver_init(void)
{int error = -1;/*采用动态分配的方式,获取设备编号,次设备号为0,*/error = alloc_chrdev_region(&button_devno, 0, DEV_CNT, DEV_NAME);if (error < 0){printk("fail to alloc button_devno\n");goto alloc_err;}/*关联字符设备结构体cdev与文件操作结构体file_operations*/button_chr_dev.owner = THIS_MODULE;cdev_init(&button_chr_dev, &button_chr_dev_fops);/*添加设备至cdev_map散列表中*//*------------一下代码省略---------------*/
}/*
*驱动注销函数
*/
static void __exit button_driver_exit(void)
{pr_info("button_driver_exit\n");/*删除设备*/device_destroy(class_button, button_devno); //清除设备class_destroy(class_button); //清除类cdev_del(&button_chr_dev); //清除设备号unregister_chrdev_region(button_devno, DEV_CNT); //取消注册字符设备
}module_init(button_driver_init);
module_exit(button_driver_exit);MODULE_LICENSE("GPL");
字符设备注册于注销已经使用n次了,为方便阅读这里将它的部分代码列出来了。完整的内容请参考本小节配套代码。
2.2.2. .open函数实现
open函数实现button的初始化工作,代码如下:
static int button_open(struct inode *inode, struct file *filp)
{int error = -1;/*添加初始化代码*/// printk_green("button_open");/*获取按键 设备树节点*/button_device_node = of_find_node_by_path("/button_interrupt");if(NULL == button_device_node){printk("of_find_node_by_path error!");return -1;}/*获取按键使用的GPIO*/button_GPIO_number = of_get_named_gpio(button_device_node ,"button_gpio", 0);if(0 == button_GPIO_number){printk("of_get_named_gpio error");return -1;}/*申请GPIO , 记得释放*/error = gpio_request(button_GPIO_number, "button_gpio");if(error < 0){printk("gpio_request error");gpio_free(button_GPIO_number);return -1;}error = gpio_direction_input(button_GPIO_number);/*获取中断号*/interrupt_number = irq_of_parse_and_map(button_device_node, 0);printk("\n irq_of_parse_and_map! = %d \n",interrupt_number);/*申请中断, 记得释放*/error = request_irq(interrupt_number,button_irq_hander,IRQF_TRIGGER_RISING,"button_interrupt",device_button); ---------------⑥if(error != 0){printk("request_irq error");free_irq(interrupt_number, device_button);return -1;}/*申请之后已经开启了,切记不要再次打开,否则运行时报错*/// // enable_irq(interrupt_number);return 0;
}
-
第10行,获取button的设备树节点,我们之前说过,虽然驱动没有采用与设备树节点匹配的方式, 但这不影响我们获取设备树节点,只要节点路径正确即可获取其他设备树节点。
-
第18行,获取使用的GPIO。详细说明可参考“GPIO子系统章节”。
-
第26行,注册GPIO。
-
第34行,设置GPIO为输入模式。
-
第37行,使用函数irq_of_parse_and_map解析并映射(map)中断函数。函数原型如下:
-
第41行,申请中断,这个函数在本章的开始已经介绍,需要注意的是,这里虽然没有使用共享中断, 但是仍然将dev参数设置为字符设备结构体指针。当然你也可以设置为NULL或其他值。
解析并映射中断函数:
unsigned int irq_of_parse_and_map(struct device_node *dev, int index)
该函数的功能是从设备树中获取某一个中断,并且将中断ID转化为linux内核虚拟IRQ number。 IRQ number用于区别中断ID。
参数:
-
dev:用于指定设备节点
-
index:指定解析、映射第几个中断, 一个设备树节点可能包含多个中断,这里指定第几个,标号从0开始。
返回值:
-
成功:解析、映射得到的内核中断号
-
失败:返回0
2.2.3. 中断服务函数实现
在open函数申请中断时要指定中断服务函数,一个简单的中断服务函数如下。
中断服务函数实现:
atomic_t button_status = ATOMIC_INIT(0); //定义整型原子变量,保存按键状态 ,设置初始值为0
static irqreturn_t button_irq_hander(int irq, void *dev_id)
{// printk("button on \n");/*按键状态加一*/atomic_inc(&button_status);return IRQ_HANDLED;
}
从以上代码可以看到我们定义了一个整型原子变量用于保存按键状态,中断发送后,整型原子变量自增一。 整型原子变量大于0表示有按键按下。
2.2.4. .read和.release函数实现
.read函数的工作是向用户空间返回按键状态值,.release函数实现退出之前的清理工作。函数实现源码如下:
.read 和.release函数实现:
static int button_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{int error = -1;int button_countervc = 0;/*读取按键状态值*/button_countervc = atomic_read(&button_status);/*结果拷贝到用户空间*/error = copy_to_user(buf, &button_countervc, sizeof(button_countervc));if(error < 0){printk_red("copy_to_user error");return -1;}/*清零按键状态值*/atomic_set(&button_status,0);return 0;
}/*字符设备操作函数集,.release函数实现*/
static int button_release(struct inode *inode, struct file *filp)
{/*释放申请的引脚,和中断*/gpio_free(button_GPIO_number);free_irq(interrupt_number, device_button);return 0;
}
-
第1-20行,在button_read函数中我们读取按键状态值,然后使用copy_to_user拷贝到用户空间, 最后设置按键状态为0。
-
第23-29行,button_release函数很简单,它只是释放.open函数中申请的中断和GPIO.
2.3. 测试应用程序实现
测试应用程序工作是读取按键状态然后打印状态,就这么简单,源码如下:
测试应用程序:
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>int main(int argc, char *argv[])
{int error = -20;int button_status = 0;/*打开文件*/int fd = open("/dev/button", O_RDWR);if (fd < 0){printf("open file : /dev/button error!\n");return -1;}printf("wait button down... \n");printf("wait button down... \n");do{/*读取按键状态*/error = read(fd, &button_status, sizeof(button_status));if (error < 0){printf("read file error! \n");}usleep(1000 * 100); //延时100毫秒} while (0 == button_status);printf("button Down !\n");/*关闭文件*/error = close(fd);if (error < 0){printf("close file error! \n");}return 0;
}
测试应用程序仅仅是测试驱动是否正常,我们只需要打开、读取状态、关闭文件即可。 需要注意的是打开之后需要关闭才能再次打开,如果连续打开两次由于第一次打开申请的GPIO和中断还没有释放打开会失败。
2.4. 实验准备
在板卡上的部分GPIO可能会被系统占用,在使用前请根据需要修改 /boot/uEnv.txt 文件, 可注释掉某些设备树插件的加载,重启系统,释放相应的GPIO引脚。
如本节实验中,可能在鲁班猫系统中默认使能了 KEY
的设备功能, 用在了GPIO子系统。引脚被占用后,设备树可能无法再加载或驱动中无法再申请对应的资源。
方法参考如下:
取消 KEY
设备树插件,以释放系统对应KEY资源,操作如下:
dtoverlay=/usr/lib/linux-image-4.19.35-imx6/overlays/imx-fire-button-interrupt.dtbo
如若运行代码时出现“Device or resource busy”或者运行代码卡死等等现象, 请按上述情况检查并按上述步骤操作。
如出现 Permission denied
或类似字样,请注意用户权限,大部分操作硬件外设的功能,几乎都需要root用户权限,简单的解决方案是在执行语句前加入sudo或以root用户运行程序。
2.4.1. 编译设备树插件
将 linux_driver/button_interrupt/imx-fire-button-interrupt-overlay.dts
拷贝到 内核源码/arch/arm/boot/dts/overlays
目录下, 并修改同级目录下的Makefile,追加 imx-fire-button-interrupt.dtbo
编译选项。然后执行如下命令编译设备树插件:
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- npi_v7_defconfigmake ARCH=arm -j4 CROSS_COMPILE=arm-linux-gnueabihf- dtbs
编译成功后生成同名的设备树插件文件(imx-fire-button-interrupt.dtbo)位于 内核源码/arch/arm/boot/dts/overlays
目录下。
2.4.2. 编译驱动程序
将 linux_driver/button_interrupt/interrupt 拷贝到内核源码同级目录,执行里面的MakeFile,生成interrupt.ko。
2.4.3. 编译应用程序
将 linux_driver/button_interrupt/interrupt/test_app 目录中执行里面的MakeFile,生成test_app。
2.5. 程序运行结果
将前面生成的设备树插件、驱动程序、应用程序通过scp等方式拷贝到开发板。
2.5.1. 加载设备树和驱动文件
将设备树插件拷贝到开发板 /usr/lib/linux-image-4.19.35-imx6/overlays/
目录下,然后 reboot
重启开发板。
在加载驱动程序之前,先查看设备树是否加载了对应的设备节点,然后再加载驱动陈程序 insmod interrupt.ko
。
2.5.2. 测试效果
驱动加载成功后直接运行测试应用程序 ./test_app,如下所示。
3. 中断的上半部分和下半部分
在linux中断我们需要知道以下两点:
-
1、Linux中断与中断之间不能嵌套。
-
2、中断服务函数运行时间应当尽量短,做到快进快出。
然而一些中断的产生之后需要较长的时间来处理,如由于网络传输产生的中断, 在产生网络传输中断后需要比较长的时间来处理接收或者发送数据,因为在linux中断并不能被嵌套 如果这时有其他中断产生就不能够及时的响应,为了解决这个问题,linux对中断的处理引入了“中断上半部”和 “中断下半部”的概念,在中断的上半部中只对中断做简单的处理,把需要耗时处理的部分放在中断下半部中,使得能够 对其他中断作为及时的响应,提供系统的实时性。这一概念又被称为中断分层。
-
“上半部分”是指在中断服务函数中执行的那部分代码,
-
“下半部分”是指那些原本应当在中断服务函数中执行但通过某种方式把它们放到中断服务函数外执行。
并不是所有的中断处理都需要用到“上半部分”和“下半部分”,如果像我们上面编写的按键中断程序一样并不需要用到 相对耗时的处理,对中断的处理只需放在中断“上半部分”即可。
为了学习如何使用中断分层,这里模拟一个耗时操作,加上中断分层的“下半部分”。
中断分层实现方法常用的有三种,分别为软中断、tasklet、和工作队列,下面分别介绍这三种方式。
3.1. 软中断和tasklet
tasklet是基于软中断实现,它们有很多相似之处,我们把它两个放到一块介绍。
3.1.1. 软中断
软中断由软件发送中断指令产生,Linux4.xx支持的软中断非常有限,只有10个(不同版本的内核可能不同) 在Linux内核中使用一个枚举变量列出所有可用的软中断,如下所示。
软中断中断编号:
enum
{HI_SOFTIRQ=0,TIMER_SOFTIRQ,NET_TX_SOFTIRQ,NET_RX_SOFTIRQ,BLOCK_SOFTIRQ,BLOCK_IOPOLL_SOFTIRQ,TASKLET_SOFTIRQ,SCHED_SOFTIRQ,HRTIMER_SOFTIRQ,RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */NR_SOFTIRQS
};
类比硬中断,这个枚举类型列出了软中断的中断编号,我们“注册”软中断以及触发软中断都会用到软中断的中断编号。
软中断“注册”函数如下所示:
void open_softirq(int nr, void (*action)(struct softirq_action *))
{softirq_vec[nr].action = action;
}
参数:
-
nr:用于指定要“注册”的软中断中断编号
-
action:指定软中断的中断服务函数
返回值:无
我们再看函数实现,这里只有一个赋值语句, 重点是softirq_vec变量,在内核源码中找到这个变量如下所示:
软中断“中断向量表”:
static struct softirq_action softirq_vec[NR_SOFTIRQS]
这是一个长度为NR_SOFTIRQS的softirq_action类型数组,长度NR_SOFTIRQS在软中断的“中断编号”枚举类型中有定义, 长度为10。这个数组是一个全局的数组,作用等同于硬中断的中断向量表。接着来看数组的类型“struct softirq_action”如下所示。
软中断结构体:
struct softirq_action
{void (*action)(struct softirq_action *);
};
它只有一个参数,就是注册软中断函数的参数open_softirq。至此我们知道数组softirq_vec就是软中断的中断向量表, 所谓的注册软中断函数就是根据中断号将中断服务函数的地址写入softirq_vec数组的对应位置。
软中断注册之后还要调用“触发”函数触发软中断,进而执行软中断中断服务函数,函数如下所示:
中断interrupt-controller节点:
void raise_softirq(unsigned int nr);
参数:
-
nr:要触发的软中断
返回值:无
3.1.2. tasklet
tasklet是基于软中断实现,如果对效率没有特殊要求推荐是用tasklet实现中断分层。为什么这么说, 根据之前讲解软中断的中断服务函数是一个全局的数组,在多CPU系统中,所有CPU都可以访问, 所以在多CPU系统中需要用户自己考虑并发、可重入等问题,增加编程负担。 软中断资源非常有限一些软中断是为特定的外设准备的(不是说只能用于特定外设)例如“NET_TX_SOFTIRQ,NET_RX_SOFTIRQ,” 从名字可以看出它们用于网络的TX和RX。像网络这种对效率要求较高的场合还是会使用软中断实现中断分层的。
相比软中断,tasklet使用起来更简单,最重要的一点是在多CPU系统中同一时间只有一个CPU运行tasklet, 所以并发、可重入问题就变得很容易处理(一个tasklet甚至不用去考虑)。而且使用时也比较简单,介绍如下。
tasklet_struct结构体
在驱动中使用tasklet_struct结构体表示一个tasklet,结构体定义如下所示:
触发软中断:
struct tasklet_struct
{struct tasklet_struct *next;unsigned long state;atomic_t count;void (*func)(unsigned long);unsigned long data;
};
参数介绍如下:
-
next:指向链表的下一个tasklet_struct,这个参数我们不需要自己去配置。
-
state:保存tasklet状态,等于0表示tasklet还没有被调度,等于TASKLET_STATE_SCHED表示tasklet被调度正准备运行。 等于TASKLET_STATE_RUN表示正在运行。
-
count:引用计数器,如果为0表示tasklet可用否则表示tasklet被禁止。
-
func:指定tasklet处理函数
-
data:指定tasklet处理函数的参数。
tasklet初始化函数:
void tasklet_init(struct tasklet_struct *t,void (*func)(unsigned long), unsigned long data)
{t->next = NULL;t->state = 0;atomic_set(&t->count, 0);t->func = func;t->data = data;
}
参数:
-
t:指定要初始化的tasklet_struct结构体
-
func:指定tasklet处理函数,等同于中断中的中断服务函数
-
data:指定tasklet处理函数的参数。函数实现就是根据设置的参数填充tasklet_struct结构体结构体。
返回值:无
和软中断一样,需要一个触发函数触发tasklet,函数定义如下所示:
tasklet触发函数:
static inline void tasklet_schedule(struct tasklet_struct *t)
{if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))__tasklet_schedule(t);
}
3.1.3. tasklet实现中断分层实验
实验在按键中断程序基础上完成,按键中断原本不需要使用中断分层,这里只是以它为例简单介绍tasklet的具体使用方法。 tasklet使用非常简单,主要包括定义tasklet结构体、初始化定义的tasklet结构体、实现tasklet中断处理函数、触发tasklet中断。
下面结合源码介绍如下。注意,源码是在“按键中断程序”基础上添加tasklet相关代码,这里只列出了tasklet相关代码。
tasklet相关代码 (位于linux_driver/button_interrupt/interrupt_tasklet/interrupt.c):
/*--------------第一部分--------------- */
struct tasklet_struct button_tasklet; //定义全局tasklet_struct类型结构体/*--------------第二部分-----------------*/
void button_tasklet_hander(unsigned long data)
{int counter = 1;mdelay(200);printk(KERN_ERR "button_tasklet_hander counter = %d \n", counter++);mdelay(200);printk(KERN_ERR "button_tasklet_hander counter = %d \n", counter++);mdelay(200);printk(KERN_ERR "button_tasklet_hander counter = %d \n", counter++);mdelay(200);printk(KERN_ERR "button_tasklet_hander counter = %d \n", counter++);mdelay(200);printk(KERN_ERR "button_tasklet_hander counter = %d \n", counter++);
}/*--------------第三部分-----------------*/
static int button_open(struct inode *inode, struct file *filp)
{/*----------------以上代码省略----------------*//*初始化button_tasklet*/tasklet_init(&button_tasklet,button_tasklet_hander,0);/*申请之后已经开启了,切记不要再次打开,否则运行时报错*/// // enable_irq(interrupt_number);return 0;
}/*--------------第四部分-----------------*/
static irqreturn_t button_irq_hander(int irq, void *dev_id)
{printk(KERN_ERR "button_irq_hander----------inter");/*按键状态加一*/atomic_inc(&button_status);tasklet_schedule(&button_tasklet);printk(KERN_ERR "button_irq_hander-----------exit");return IRQ_RETVAL(IRQ_HANDLED)
结合代码各部分介绍如下:
-
第2行:定义tasklet_struct类型结构体。
-
第5-18行:定义tasklet的“中断服务函数”可以看到我们在tasklet的中断服务函数中使用延时 和printk语句模拟一个耗时的操作。
-
第21-31行:在原来的代码基础上调用tasklet_init函数初始化tasklet_struct类型结构体。
-
第40行:在中断服务函数中调用tasklet_schedule函数触发tasklet中断。 在按键中断服务函数中的开始处和结束处添加打印语句,正常情况下程序会先执行按键中断的中短发服务函数, 退出中断服务函数后再执行中断的下半部分,既tasklet的“中断服务函数”。
3.1.4. 下载验证
本实验在在按键中断驱动程序基础上修改,实验方法与按键中断程序相同,测试应用程序以及设备树插件不用修改。
将修改后的驱动程序编译、下载到开发板,加载驱动然后运行测试应用程序如下所示。
3.2. 工作队列
与软中断和tasklet不同,工作队列运行在内核线程,允许被重新调度和睡眠。 如果中断的下部分能够接受被重新调度和睡眠,推荐使用工作队列。
和tasklet类似,从使用角度讲主要包括定义工作结构体、初始化工作、触发工作。
3.2.1. 工作结构体
“工作队列”是一个“队列”,但是对于用户来说不必关心“队列”以及队列工作的内核线程,这些内容由内核帮我们完成, 我们只需要定义一个具体的工作、初始化工作即可,在驱动中一个工作结构体代表一个工作,工作结构体如下所示:
work_struct结构体:
struct work_struct {atomic_long_t data;struct list_head entry;work_func_t func;
#ifdef CONFIG_LOCKDEPstruct lockdep_map lockdep_map;
#endif
};
重点关心参数“work_func_t func;”该参数用于指定“工作”的处理函数。
work_func_t如下所示:
void (*work_func_t)(struct work_struct *work);
3.2.2. 工作初始化函数
内核提初始化宏定义如下所示。
#define INIT_WORK(_work, _func)
该红顶共有两个参数,_work用于指定要初始化的工作结构体,_func用于指定工作的处理函数。
3.2.3. 启动工作函数
驱动工作函数执行后相应内核线程将会执行工作结构体指定的处理函数,驱动函数如下所示。
static inline bool schedule_work(struct work_struct *work)
{return queue_work(system_wq, work);
}
启动工作函数只有一个工作结构体参数。
3.2.4. 工作队列实验
工作队列实验同样在按键中断程序基础上实现,这里只列出了工作队列相关代码, 完整内容请参考本小节配套驱动程序。(这里只修改驱动程序,其他内容保持不变)
工作队列相关函数 (位于linux_driver/button_interrupt/interrupt_work/interrupt.c):
/*--------------第一部分-----------------*/
struct work_struct button_work;/*--------------第二部分-----------------*/
void work_hander(struct work_struct *work)
{int counter = 1;mdelay(200);printk(KERN_ERR "work_hander counter = %d \n", counter++);mdelay(200);printk(KERN_ERR "work_hander counter = %d \n", counter++);mdelay(200);printk(KERN_ERR "work_hander counter = %d \n", counter++);mdelay(200);printk(KERN_ERR "work_hander counter = %d \n", counter++);mdelay(200);printk(KERN_ERR "work_hander counter = %d \n", counter++);
}/*--------------第三部分-----------------*/
static int button_open(struct inode *inode, struct file *filp)
{/*----------------以上代码省略----------------*//*初始化button_work*/INIT_WORK(&button_work, work_hander);return 0;
}/*--------------第四部分-----------------*/
static irqreturn_t button_irq_hander(int irq, void *dev_id)
{/*按键状态加一*/atomic_inc(&button_status);schedule_work(&button_work);return IRQ_HANDLED;
}
-
第2行:定义work_struct类型结构体。
-
第5-18行:定义工作队列中的“中断服务函数”,使用延时和printk语句模拟一个耗时的操作。
-
第21-27行:在原来的代码基础上调用INIT_WORK宏初始化work_struct类型结构体与中断下半部分函数。
-
第34行:在中断服务函数中调用schedule_work函数触发中断下半部。
与tasklet实现中断分层类似,使用方法几乎一样,这里不进行详细描述。
核心区别总结
特性 | 直接中断处理 | Tasklet | 工作队列 |
---|---|---|---|
处理复杂任务 | 不适合 | 较适合(非阻塞、短任务) | 非常适合(支持阻塞、长任务) |
上下文 | 中断上下文 | 软中断上下文 | 进程上下文 |
是否允许睡眠 | 否 | 否 | 是 |
执行延迟 | 最低 | 较低 | 较高 |
适用场景 | 简单任务(计数、状态变更) | 简单非阻塞的延迟任务 | 复杂、阻塞、长时间任务 |
推荐使用场景
- 直接中断处理: 如果按键逻辑简单,直接用第一种方案即可。
- Tasklet: 如果需要按键中断后执行少量非阻塞逻辑,例如短暂延迟打印,可以使用第二种方案。
- 工作队列: 如果按键触发后需要执行较复杂的任务逻辑(如 I/O 操作、文件操作),建议采用第三种方案。
相关文章:
Linux驱动开发(12):中断子系统–按键中断实验
本章我们以按键为例讲解在驱动程序中如何使用中断, 在学习本章之前建议先回顾一下关于中断相关的裸机部分相关章节, 这里主要介绍在驱动中如何使用中断,对于中断的概念及GIC中断控制器相关内容不再进行讲解。 本章配套源码和设备树插件位于“…...
代码随想录-算法训练营-番外(图论02:岛屿数量,岛屿的最大面积)
day02 图论part02 今日任务:岛屿数量,岛屿的最大面积 都是一个模子套出来的 https://programmercarl.com/kamacoder/0099.岛屿的数量深搜.html#思路往日任务: day01 图论part01 今日任务:图论理论基础/所有可到达的路径 代码随想录图论视频部分还没更新 https://programmercar…...
20 go语言(golang) - gin框架安装及使用(一)
一、简介 Gin是一个用Go语言编写的高性能Web框架,专注于构建快速、可靠的HTTP服务。它以其速度和简洁性而闻名,非常适合用于开发RESTful API。 高性能:Gin使用了httprouter进行路由管理,这是一个轻量级且非常快速的HTTP请求路由器…...
重生之我在学Vue--第3天 Vue 3 模板语法与指令
重生之我在学Vue–第3天 Vue 3 模板语法与指令 文章目录 重生之我在学Vue--第3天 Vue 3 模板语法与指令前言一、数据绑定1.1 单向绑定1.2 双向绑定 二、常用指令2.1 v-bind2.2 v-model2.3 v-if2.4 v-show2.5 v-for2.6 v-on 三、事件处理与表单绑定3.1 事件处理3.2 表单绑定 前言…...
电脑win11家庭版升级专业版和企业版相关事项
我的是零刻ser9,自带win11家庭版,但是我有远程操控需求,想用windows系统自带的远程连接功能,所以需要升级为专业版。然后在系统激活页面通过更改序列号方式,淘宝几块钱买了个序列号升级成功专业版了。但是,…...
docker 架构详解
Docker架构是基于客户端-服务器(C/S)模式的,包含多个关键组件,以确保容器化应用的高效构建、管理和运行。以下是对Docker架构的详细解析: Docker 架构概述 Docker 架构采用客户端-服务器(C/S)…...
tinyCam Pro 用于远程监控,控制和录制您的私人公共网络或IP摄像机
tinyCam Pro 是一款用于远程监控,控制和录制您的私人/公共网络或IP摄像机,视频编码器和具有500万次下载的CCTV摄像头的DVR。需使用3G/4G/WiFi连接和下载数据。 tinyCam Monitor Pro 可用于远程安全地监控您的宝宝、宠物、家庭、商业、交通和天气…...
Flask 验证码自动生成
Flask 验证码自动生成 想必验证码大家都有所了解,但是可以自己定义图片验证码,包含数字,英文以及数字计算,自动生成验证码。 生成图片以及结果 from captcha.image import ImageCaptchafrom PIL import Image from random impo…...
vmpwn小总结
前言: 好久没有更新博客了,关于vm的学习也是断断续续的,只见识了几道题目,但是还是想总结一下,所谓vmpwn就是把出栈,进栈,寄存器,bss段等单独申请一块空闲实现相关的功能࿰…...
开源密码管理器 Bitwarden 一站式管理所有密码以及 2FA
本文首发于只抄博客,欢迎点击原文链接了解更多内容。 前言 随着注册的平台越来越多,管理密码的难度也越来越高了。要是把密码都设置成一样的,担心哪天某个平台泄露被一锅端,而每个平台单独一个密码又不太好记,这时候就…...
标准体重计算API集成指南
标准体重计算API集成指南 引言 在当今数字化和健康意识日益增长的时代,开发人员和健康管理专业人士不断寻找创新的方法来促进用户的健康生活。标准体重计算是一个关键的健康指标,它可以帮助个人了解自己的身体状况,并为制定合适的饮食和运动…...
多个终端查看的history不一样,如何确保多个终端会话之间的 history 一致,减少历史记录差异
问题: 在使用 Linux 系统时,history 命令显示的历史记录通常是与当前终端会话相关的。这就意味着,如果你在多个终端中打开会话,它们显示的历史记录可能不完全相同。这个问题通常是由以下原因引起的: 原因:…...
Spring Boot整合EasyExcel并行导出及Zip压缩下载
1. 项目依赖 首先,我们需要引入相关的依赖,包括 Spring Boot 和阿里巴巴的 EasyExcel 组件,此外还需要使用 Java 的 Zip 工具进行压缩操作。 <dependencies><!-- Spring Web --><dependency><groupId>org.springfr…...
Docker 对 iptables 规则的自动配置,这句话是什么意思
Docker 对 iptables 规则的自动配置指的是 Docker 守护进程 (daemon) 会自动管理 Linux 系统上的 iptables 规则,以便容器可以正确地进行网络通信。这对于大多数用户来说是一个方便的功能,因为它简化了容器网络配置。 具体来说,这意味着&…...
使用aarch64-unknown-linux-musl编译生成静态ARM64可执行文件
使用aarch64-unknown-linux-musl编译生成静态ARM64可执行文件 使用aarch64-unknown-linux-musl编译生成静态ARM64可执行文件1. 安装aarch64-unknown-linux-musl目标2. 安装交叉编译工具链安装musl-cross-make 3. 配置Rust编译器使用交叉编译工具链4. 编译你的Rust项目5. 运行或…...
【SpringBoot中出现循环依赖错误】
SpringBoot中出现循环依赖错误 在Spring Boot中,循环依赖(circular dependency)是指两个或多个bean相互依赖,形成一个闭合的依赖环。例如,Bean A依赖于Bean B,而Bean B又反过来依赖于Bean A。这种情况下&a…...
数据仓库-基于角色的权限管理(RBAC)
什么是基于角色的用户管理? 基于角色的用户管理(Role-Based Access Control,简称RBAC)是通过为角色赋予权限,用户通过成为适当的角色而得到这些角色的权限。 角色是一组权限的抽象。 使用RBAC可以极大简化对权限的管理。 什么是RBAC模型&…...
springboot3整合javafx解决bean注入问题
springboot整合javafx时候,很多问题就在于controller没有被spring容器管理,无法注入bean,在这里提供一套自己的解决思路 执行逻辑 这里仅仅提供一个演示,我点击按钮之后,从service层返回一个文本并显示 项目结构 创…...
.NET 8 Blazor Web项目中的 .razor 文件与 .cshtml 文件的本质区别
在.NET 8 Blazor Web项目中,.razor 和 .cshtml 文件是常用的视图文件格式。尽管它们看起来有相似之处,但在使用方式、功能和渲染机制上有着根本的不同。理解它们的本质区别,有助于开发者更好地选择合适的文件格式,并构建符合需求的…...
SpringBoot快速使用
一些名词的碎碎念: 1> 俩种网络应用设计模式 C/S 客户端/服务器 B/S 浏览器/服务器 俩者对比: 2> 集群和分布式的概念 集群: 分布式: 例子: 一个公司有一个人身兼多职 集群: 招聘N个和上面这个人一样身兼多职 分布式: 招聘N个人,分担上面这个人的工作,进行工作的拆分. 工…...
【C语言实现:用队列模拟栈与用栈模拟队列(LeetCode 225 232)】
LeetCode刷题记录 🌐 我的博客主页:iiiiiankor🎯 如果你觉得我的内容对你有帮助,不妨点个赞👍、留个评论✍,或者收藏⭐,让我们一起进步!📝 专栏系列:LeetCode…...
远程控制软件对比与使用推荐
远程控制软件对比与使用推荐 远程控制软件在现代工作环境中扮演着重要角色,无论是远程办公、技术支持、还是家庭成员之间的协助。以下是对几种常见远程控制软件的详细对比和推荐使用场景。 1. TeamViewer 特点 跨平台:支持Windows、macOS、Linux、iO…...
vue canvas 绘制选定区域 矩形框
客户那边文档相当的多,目前需要协助其将文档转为数据写入数据库,并与其他系统进行数据共享及建设,所以不得不搞一个识别的功能,用户上传PDF文档后,对于关键信息点进行识别入库! 以下为核心代码,…...
【SpringCloud】OpenFeign配置时间Decode
文章目录 1.自定义反序列化器2.配置类与自定义 ObjectMapper客户端 需求:OpenFeign配置自定义decode,解析http请求返回的时间字符串 1.自定义反序列化器 Date 自定义反序列化器 import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.j…...
Xerces-C,一个成熟的 C++ XML 解析库!
嗨,大家好!我是一行。今天咱们来探索 Xerces-C,它可是 C里超棒的 XML 解析库哦!能帮咱轻松处理 XML 数据,在很多数据交互、配置文件读取场景都超实用,快来一起学习使用它的妙招吧。 一、Xerces-C 是什么&am…...
6.2 MapReduce工作原理
MapReduce工作原理涉及将大数据集分割成小块并行处理。Map任务读取数据块并输出中间键值对,而Reduce任务则处理这些排序后的数据以生成最终结果。MapTask工作包括读取数据、应用Map函数、收集输出、内存溢出时写入磁盘以及可选的Combiner局部聚合。ReduceTask工作则…...
一次旧业务系统迁移收缩的经历
单位的一个业务系统,在几年前已经更换了。但旧的系统里面还有很多没有转移过来的数据,虽然普通用户不再需要用旧的系统,但相应部门的管理人员还需要在旧系统查询数据资料,这应该是旧系统向新系统迁移时,数据不彻底&…...
MVC配置文件及位置
配置文件位置 默认位置 WEB-INF目录下,文件名:<servlet-name>-servlet.xml <?xml version"1.0" encoding"UTF-8"?> <web-app xmlns"http://xmlns.jcp.org/xml/ns/javaee"xmlns:xsi"http://www.w3.…...
如何解决samba服务器共享文件夹不能粘贴文件
sudo vim /etc/samba/smb.conf在samba的配置文件中增加一个选项 writable yes重启Samba服务以使更改生效: sudo service smbd restart...
【中工开发者】鸿蒙商城app
这学期我学习了鸿蒙,想用鸿蒙做一个鸿蒙商城app,来展示一下。 项目环境搭建: 1.开发环境:DevEco Studio2.开发语言:ArkTS3.运行环境:Harmony NEXT base1 软件要求: DevEco Studio 5.0.0 Rel…...
网站seo推广公司靠谱吗/qq空间秒赞秒评网站推广
C# 语言的预处理器指令: #if #else #elif #endif # define #undef #warning #error #line #region #endregion #pragma #pragma warning #pragma checksum 虽然编译器没有单独的预处理器,但在处理该节中描述的指令时如同存在一个单独的预处…...
建设银行网站官网网址/深圳网络优化推广公司
在操作系统中,文件系统都是针对分区而言的,一个磁盘必须先分区才能格式化文件系统(即使你将磁盘所有容量划分一个分区).格式文件系统后才能挂载使用,此时就必须知道一个文件系统到底支持多大的分区大小。 在ext2、ext3文件系统中,…...
高端企业网站建设公司/网络优化的意义
填空1、Linux桌面环境主要包括KDE和(GNOME)2、某文件的权限为:d-rw-_r--_r-,用数值形式表示该权限,则该八进制数为:(644) ,该文件属性是(目录文件)。3、在Linux系统中,以(文件)方式访问设备。4、前台起动的…...
校园网网站建设规划书/今日国际新闻最新消息
Vue中监视多个属性,执行同一个方法,初次加载只执行一次,防止重复执行 需求: 当form表单的多项内容每一项改变后,都会去请求接口返回实时的数据。 原解决方法: 在watch中分别监视改变的数据,当…...
php+mysql网站开发教程/seo什么意思
转载于CentOS中文站:http://www.centoscn.com/CentOS/Intermediate/2013/0817/1334.html一、MySQL安装Centos下安装mysql 请点开:http://www.centoscn.com/CentosServer/sql/2013/0817/1285.html二、MySQL的几个重要目录MySQL安装完成后不象SQL Server默认安装在一个…...
信息服务平台网站名称/网络营销过程步骤
正确步骤: 因为是跨服务器操作,所以我用文件作为载体。 1、将待导入的数据从源数据库中查询出来,并导出到CSV文件中去。 2、在待导入的目标用SQLSERVER自带的导入工具导入到目标数据库的临时表中。 3、然后再从目标临时表中抽数据放到目标表中…...