在做Linux下设备驱动开发之前我们需要掌握的基础知识可以看这篇文章:GIC中断控制器、设备树插件(Device Tree Overlay)以及内核定时器介绍
在imx6ull开发板上我们的按键管脚是GPIO4_IO14。
在前面的文章中做LED驱动开发的时候,我们是直接更新设备树的,学了设备树插件的方式,我们可以用打补丁的方式进行添加子节点。
先检查NAND_CE1_B这个PIN有没有被其他pinctrl节点使用, 如果有使用的话就要屏蔽掉,然后再检查GPIO4_IO14这个GPIO有没有被其他外设使用,如果有的话也要屏蔽掉。
在我的开发板中按键设备默认是这个GPIO,所以被占用了,我们需要屏蔽掉。
/imx6ull/imx6ull/bsp/kernel/linux-imx/arch/arm/boot/dts
下的igkboard.dts
:
/*keys {
compatible = "gpio-keys";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_gpio_keys>;
autorepeat;
status = "okay";
key_user {
lable = "key_user";
gpios = <&gpio4 14 GPIO_ACTIVE_LOW>;
linux,code = ;
};
};*/
&iomuxc {
pinctrl-names = "default";
/*pinctrl_gpio_keys: gpio-keys {
fsl,pins = <
MX6UL_PAD_NAND_CE1_B__GPIO4_IO14 0x17059
>;
};*/
一般设备树插件都在如下的目录下:
wangdengtao@wangdengtao-virtual-machine:~/imx6ull/imx6ull/bsp/kernel/linux-imx/arch/arm/boot/dts/overlays$ ls
adc.dtbo cam.dts can2.dtbo i2c1.dts lcd_drm.dtbo lcd.dts nbiot-4g.dts pwm8.dtbo spi1.dts uart3.dtbo uart4.dts w1.dtbo
adc.dts can1.dtbo can2.dts key_irq.dtbo lcd_drm.dts Makefile pwm7.dtbo pwm8.dts uart2.dtbo uart3.dts uart7.dtbo w1.dts
cam.dtbo can1.dts i2c1.dtbo key_irq.dts lcd.dtbo nbiot-4g.dtbo pwm7.dts spi1.dtbo uart2.dts uart4.dtbo uart7.dts
编写key_irq.dts设备树插件(一般不推荐直接这样添加设备树插件,一般推荐在设备树中添加根节点,在设备树插件中添加子节点,为了方便目前使用前者):
/dts-v1/;
/plugin/;
#include
#include "../imx6ul-pinfunc.h"
#include "dt-bindings/interrupt-controller/irq.h"
/ {
compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ull";
fragment@0 {
target-path = "/";
__overlay__ {
key_irq:key_irq{
compatible = "my-gpio-keys";
status = "disabled";
};
};
};
fragment@1 {
target = <&key_irq>;/*节点名字需要和添加的根节点的名字相同*/
__overlay__ {
compatible = "my-gpio-keys";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_my_gpio_keys>;
gpios = <&gpio4 14 GPIO_ACTIVE_LOW>;
status = "okay";
interrupt-parent = <&gpio4>;
interrupts = <14 IRQ_TYPE_EDGE_RISING>;
};
};
fragment@2 {
target = <&iomuxc> ;
__overlay__ {
pinctrl_my_gpio_keys: my-gpio-keys {
fsl,pins = <
MX6UL_PAD_NAND_CE1_B__GPIO4_IO14 0x17059 /* gpio key */
>;
};
};
};
};
在根路径(/imx6ull/imx6ull/bsp/kernel/linux-imx
)下执行make dtbs
编译:
wangdengtao@wangdengtao-virtual-machine:~/imx6ull/imx6ull/bsp/kernel/linux-imx$ make dtbs
DTC arch/arm/boot/dts/overlays/key_irq.dtbo
我们就可以在/imx6ull/imx6ull/bsp/kernel/linux-imx/arch/arm/boot/dts/overlays
下看见key_irq.debo
设备树补丁文件了:
wangdengtao@wangdengtao-virtual-machine:~/imx6ull/imx6ull/bsp/kernel/linux-imx/arch/arm/boot/dts/overlays$ ls
adc.dtbo cam.dts can2.dtbo i2c1.dts lcd_drm.dtbo lcd.dts nbiot-4g.dts pwm8.dtbo spi1.dts uart3.dtbo uart4.dts w1.dtbo
adc.dts can1.dtbo can2.dts key_irq.dtbo lcd_drm.dts Makefile pwm7.dtbo pwm8.dts uart2.dtbo uart3.dts uart7.dtbo w1.dts
cam.dtbo can1.dts i2c1.dtbo key_irq.dts lcd.dtbo nbiot-4g.dtbo pwm7.dts spi1.dtbo uart2.dts uart4.dtbo uart7.dts
我们将key_irq.debo
文件上传到我们的开发板上,然后添加到如下路径下:
root@igkboard:/run/media/mmcblk1p1/overlays# ls
adc.dtbo can1.dtbo i2c1.dtbo lcd.dtbo nbiot-4g.dtbo pwm8.dtbo uart2.dtbo uart4.dtbo w1.dtbo
cam.dtbo can2.dtbo key_irq.dtbo lcd_drm.dtbo pwm7.dtbo spi1.dtbo uart3.dtbo uart7.dtbo
并且在路径(/run/media/mmcblk1p1
)下修改配置文件config.txt
:
# Enable extra overlays
dtoverlay_extra=key_irq
然后重启开发板即可,就可以在路径/proc/device-tree
下看见我们的设备树文件,以及里面的属性:
root@igkboard:~# ls /proc/device-tree
'#address-cells' 3p3v aliases clock-cli clock-osc key_irq model name pwm-buzzer regulator-sd1-vmmc serial-number timer
'#size-cells' __local_fixups__ backlight-lcd clock-di0 compatible leds mqs panel pxp_v4l2 regulator@0 soc w1
1p8v __symbols__ chosen clock-di1 cpus memory@80000000 my_leds pmu regulator-peri-3v3 reserved-memory sound-mqs
root@igkboard:/proc/device-tree/key_irq# ls
compatible gpios interrupt-parent interrupts name phandle pinctrl-0 pinctrl-names status
如上就添加设备树插件并且加载成功了。
在Linux内核中也提供了大量的中断相关的API函数,每个中断都有一个中断号,通过中断号即可区分不同的中断。在Linux内核中使用一个int变量表示中断号。
在linux内核中要想使用某个中断是需要申请的,request_irq函数用于申请中断,request_irq函数可能会导致休眠,因此不能在中断上下文或其他禁止睡眠的代码段中使用request_irq函数。
static inline int __must_check request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev)
上面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 ---------①//多个设备共享一个中断
/*-----------以下宏定义省略------------*/
使用中断的时候需要通过request_irq函数申请,使用完成以后就要通过free_irq函数释放掉相应的中断。如果中断不是共享的,那么free_irq会删除中断处理函数并且禁止中断。
void free_irq(unsigned int irq, void *dev);
在中断申请时需要指定一个中断处理函数,书写格式如下所示。
irqreturn_t (*irq_handler_t)(int irq, void * 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。
代码中涉及到内核定时器的相关知识,在开头的文章中都有讲解。
按键驱动设备文件(key_irq.c):
/*************************************************************************
> File Name: key_irq.c
> Author: WangDengtao
> Mail: [email protected]
> Created Time: 2023年04月01日 星期六 14时55分18秒
************************************************************************/
#include // 驱动程序必须的头文件
#include // 驱动程序必须的头文件
#include // ENODEV,ENOMEM存放的头文件
#include // u8,u32,dev_t等类型在该头文件中定义
#include // printk(),内核打印函数
#include // file_operations,用于联系系统调用和驱动程序
#include // cdev_alloc(),分配cdev结构体
#include // 用于自动生成设备节点的函数头文件
#include // gpio子系统的api
#include // platform总线驱动头文件
#include
#include
#include // 内核和用户传输数据的函数
#include // 中断相关函数
#include // 中断相关函数
#include
#include // 定时器相关函数
#define KEY_NAME "key_irq"
#define KEY0VALUE 0XF0 // 按键值
#define INVAKEY 0X00 // 无效的按键值
static int dev_major = 0;
// 存放key信息的结构体
struct platform_key_data
{
char name[16]; // 设备名字
int key_gpio; // gpio编号
unsigned char value; // 按键值
int irq; // 中断号
irqreturn_t (*handler)(int, void *); // 中断处理函数
};
// 存放key的私有属性
struct platform_key_priv
{
struct cdev cdev; // cdev结构体
struct class *dev_class; // 自动创建设备节点的类
int num_key; // key的数量
struct platform_key_data key; // 存放key信息的结构体数组
atomic_t keyvalue; // 有效的按键键值,用于向应用层上报
atomic_t releasekey; // 标记是否完成一次完成的按键,用于向应用层上报
struct timer_list timer; // 定时器,用于消抖
};
// 为key私有属性开辟存储空间的函数
static inline int sizeof_platform_key_priv(int num_key)
{
return sizeof(struct platform_key_priv) + (sizeof(struct platform_key_data) * num_key);
}
// 中断服务函数,初始化定时器用于消抖
static irqreturn_t key0_handler(int irq, void *dev_id)
{
struct platform_key_priv *priv = (struct platform_key_priv *)dev_id;
// 开启定时器
//priv->timer.data = (volatile long)dev_id;
mod_timer(&(priv->timer), jiffies + msecs_to_jiffies(10));
return IRQ_RETVAL(IRQ_HANDLED);
}
// 定时器服务函数,定时器到了后的操作
// 定时器到了以后再次读取按键,如果按键还是按下状态则有效
void timer_function(struct timer_list *t)
{
unsigned char value;
struct platform_key_priv *priv = from_timer(priv, t, timer);
// 读取按下的io值
value = gpio_get_value(priv->key.key_gpio);
if(value == 0) // 按下按键
{
atomic_set(&(priv->keyvalue), value);
printk("keypress\n");
}
else // 按键松开
{
atomic_set(&(priv->keyvalue), 0x80 | value);
atomic_set(&(priv->releasekey), 1); // 标记松开按键,即完成一次完整的按键过程
printk("keyrelease\n");
}
}
// 解析设备树,初始化key属性并初始化中断
int parser_dt_init_key(struct platform_device *pdev)
{
/*struct device_node *of_node;/*存放设备树中匹配的设备节点。当内核使能设备树,总线负责将驱动的of_match_table 以及设备树的 compatible 属性进行比较之后,将匹配的节点保存到该变量。*/
struct device_node *np = pdev->dev.of_node; // 当前设备节点
struct platform_key_priv *priv; // 存放私有属性
int num_key, gpio; // key数量和gpio编号
int ret;
/* 1)按键初始化 */
// 获取该节点下子节点的数量
num_key = 1;
if(num_key <= 0)
{
dev_err(&pdev->dev, "fail to find node\n");
return -EINVAL;
}
// 分配存储空间用于存储按键的私有数据
priv = devm_kzalloc(&pdev->dev, sizeof_platform_key_priv(num_key), GFP_KERNEL);
if (!priv)
{
return -ENOMEM;
}
// 通过dts属性名称获取gpio编号
gpio = of_get_named_gpio(np, "gpios", 0);
// 将子节点的名字,传给私有属性结构体中的key信息结构体中的name属性
strncpy(priv->key.name, np->name, sizeof(priv->key.name));
// 将gpio编号和控制亮灭的标志传给结构体
priv->key.key_gpio = gpio;
// 申请gpio口,相较于gpio_request增加了gpio资源获取与释放功能
if( (ret = devm_gpio_request(&pdev->dev, priv->key.key_gpio, priv->key.name)) < 0 )
{
dev_err(&pdev->dev, "fail to request gpio for %s\n", priv->key.name);
return ret;
}
// 设置gpio为输入模式,并设置初始状态
if( (ret = gpio_direction_input(priv->key.key_gpio))< 0 )
{
dev_err(&pdev->dev, "can't request gpio output for %s\n", priv->key.name);
}
/* 2)中断初始化 */
// 从设备树中获取中断号
priv->key.irq = irq_of_parse_and_map(np, 0);
// 申请中断,并初始化value和中断处理函数
priv->key.handler = key0_handler;
priv->key.value = KEY0VALUE;
ret = request_irq(priv->key.irq, priv->key.handler, IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING, priv->key.name, priv);
if(ret < 0)
{
printk("fail to request irq %d\n", priv->key.irq);
return -EFAULT;
}
/*初始化 timer,设置定时器处理函数,还未设置周期,所以不会激活定时器*/
timer_setup(&(priv->timer), timer_function, 0);
priv->timer.expires = jiffies + HZ/5;
priv->timer.function = timer_function;
add_timer(&priv->timer);
// 暂时先解决一个按键的问题
priv->num_key = 1;
dev_info(&pdev->dev, "success to get %d valid key\n", priv->num_key);
// 将key的私有属性放入platform_device结构体的device结构体中的私有数据中
platform_set_drvdata(pdev, priv);
return 0;
}
static int key_open(struct inode *inode, struct file *file)
{
struct platform_key_priv *priv;
priv = container_of(inode->i_cdev, struct platform_key_priv, cdev);
file->private_data = priv;
return 0;
}
static ssize_t key_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
unsigned char value;
int ret = 0;
struct platform_key_priv *priv;
priv = filp->private_data;
if (0 == gpio_get_value(priv->key.key_gpio)) // key0 按下
{
while(!gpio_get_value(priv->key.key_gpio)); // 等待按键释放
atomic_set(&(priv->keyvalue), KEY0VALUE);
}
else // 无效的按键值
{
atomic_set(&(priv->keyvalue), INVAKEY);
}
value = atomic_read(&(priv->keyvalue)); // 保存按键值
ret = copy_to_user(buf, &value, sizeof(value));
return ret;
}
static int key_release(struct inode *inode, struct file *file)
{
return 0;
}
static struct file_operations key_fops =
{
.owner = THIS_MODULE,
.open = key_open,
.read = key_read,
.release = key_release,
};
static int platform_key_probe(struct platform_device *pdev)
{
struct platform_key_priv *priv; // 临时存放私有属性的结构体
struct device *dev; // 设备结构体
dev_t devno; // 设备的主次设备号
int i, rv = 0;
// 1)解析设备树并初始化key状态
rv = parser_dt_init_key(pdev);
if( rv < 0 )
return rv;
// 将之前存入的私有属性,放入临时的结构体中
priv = platform_get_drvdata(pdev);
// 2)分配主次设备号
if (0 != dev_major)
{
// 静态分配主次设备号
devno = MKDEV(dev_major, 0);
rv = register_chrdev_region(devno, priv->num_key, "KEY_NAME"); /*proc/devices/key_irq*/
}
else
{
// 动态分配主次设备号
rv = alloc_chrdev_region(&devno, 0, priv->num_key, "KEY_NAME");
dev_major = MAJOR(devno);
}
if (rv < 0)
{
dev_err(&pdev->dev, "major can't be allocated\n");
return rv;
}
// 3)分配cdev结构体
cdev_init(&priv->cdev, &key_fops);
priv->cdev.owner = THIS_MODULE;
rv = cdev_add (&priv->cdev, devno , priv->num_key);
if( rv < 0)
{
dev_err(&pdev->dev, "struture cdev can't be allocated\n");
goto undo_major;
}
// 4)创建类,实现自动创建设备节点
priv->dev_class = class_create(THIS_MODULE, "key"); /* /sys/class/key */
if( IS_ERR(priv->dev_class) )
{
dev_err(&pdev->dev, "fail to create class\n");
rv = -ENOMEM;
goto undo_cdev;
}
// 5)创建设备
for(i=0; i<priv->num_key; i++)
{
devno = MKDEV(dev_major, i);
dev = device_create(priv->dev_class, NULL, devno, NULL, "key%d", i); /* /dev/key0 */
if( IS_ERR(dev) )
{
dev_err(&pdev->dev, "fail to create device\n");
rv = -ENOMEM;
goto undo_class;
}
}
printk("success to install driver[major=%d]!\n", dev_major);
return 0;
undo_class:
class_destroy(priv->dev_class);
undo_cdev:
cdev_del(&priv->cdev);
undo_major:
unregister_chrdev_region(devno, priv->num_key);
return rv;
}
static int platform_key_remove(struct platform_device *pdev)
{
struct platform_key_priv *priv = platform_get_drvdata(pdev);
int i;
dev_t devno = MKDEV(dev_major, 0);
// 注销设备结构体,class结构体和cdev结构体
for(i=0; i<priv->num_key; i++)
{
devno = MKDEV(dev_major, i);
device_destroy(priv->dev_class, devno);
}
class_destroy(priv->dev_class);
cdev_del(&priv->cdev);
unregister_chrdev_region(MKDEV(dev_major, 0), priv->num_key);
// 将key的状态设置为0
for (i = 0; i < priv->num_key; i++)
{
gpio_set_value(priv->key.key_gpio, 0);
}
// 删除定时器
del_timer_sync(&(priv->timer));
// 释放中断
free_irq(priv->key.irq, priv);
printk("success to remove driver[major=%d]!\n", dev_major);
return 0;
}
// 匹配列表
static const struct of_device_id platform_key_of_match[] = {
{ .compatible = "my-gpio-keys" },
{}
};
MODULE_DEVICE_TABLE(of, platform_key_of_match);
// platform驱动结构体
static struct platform_driver platform_key_driver = {
.driver = {
.name = "key_irq", // 无设备树时,用于设备和驱动间的匹配
.of_match_table = platform_key_of_match, // 有设备树后,利用设备树匹配表
},
.probe = platform_key_probe,
.remove = platform_key_remove,
};
module_platform_driver(platform_key_driver);
/*添加LICENSE和作者信息,是来告诉内核,该模块带有一个自由许可证;没有这样的说明,在加载模块的时内核会“抱怨”.*/
MODULE_LICENSE("Dual BSD/GPL");//许可 GPL、GPL v2、Dual MPL/GPL、Proprietary(专有)等,没有内核会提示.
MODULE_AUTHOR("WangDengtao");//作者
MODULE_VERSION("V1.0");//版本
按键驱动测试文件(key_irq_test.c):
#include
#include
#include
#include
#include
#include
/*
* ./led_App /dev/key0
*
*/
#define KEY0VALUE 0XF0
#define INVAKEY 0X00
int main(int argc, char **argv)
{
int fd;
unsigned char keyvalue;
fd = open("/dev/key0", O_RDWR);
if (fd == -1)
{
printf("can not open file %s\n");
return -1;
}
while (1)
{
/* 3. 读文件 */
read(fd, &keyvalue, sizeof(keyvalue));
if (keyvalue == KEY0VALUE)
{
printf("KEY0 Press, value = %#X\r\n", keyvalue);
}
}
close(fd);
return 0;
}
Makefile:
KERNAL_DIR ?= /home/wangdengtao/imx6ull/imx6ull/bsp/kernel/linux-imx
PWD :=$(shell pwd)
obj-m := key_irq.o
CC=arm-linux-gnueabihf-gcc
APP_NAME=key_irq_test
all:
$(MAKE) -C $(KERNAL_DIR) M=$(PWD) modules
@${CC} ${APP_NAME}.c -o ${APP_NAME}
@make clear
clear:
@rm -f *.o *.cmd *.mod *.mod.c
@rm -rf *~ core .depend .tmp_versions Module.symvers modules.order -f
@rm -f .*ko.cmd .*.o.cmd .*.o.d
@rm -f *.unsigned
clean:
@rm -f *.ko
@rm -f ${APP_NAME}
make之后可以看见生成驱动文件,一次可执行测试文件:
wangdengtao@wangdengtao-virtual-machine:~/wangdengtao/driver/arm$ ls
key_irq.c key_irq.ko key_irq_test key_irq_test.c Makefile
将我们的驱动加载到我们的开发板,并且测试运行。
root@igkboard:~# tftp -gr key_irq.ko 192.168.137.229
root@igkboard:~# tftp -gr key_irq_test 192.168.137.229
root@igkboard:~# chmod a+x key_irq_test
root@igkboard:~# ls
key_irq.ko key_irq_test
安装我们的驱动,可以看见在 /dev 路径下生成了key0设备文件:
root@igkboard:~# insmod key_irq.ko
root@igkboard:~# ls -l /dev/key0
crw------- 1 root root 243, 0 Apr 1 08:49 /dev/key0
root@igkboard:~# lsmod
Module Size Used by
key_irq 16384 0
rtl8188fu 999424 0
imx_rngc 16384 0
rng_core 20480 1 imx_rngc
secvio 16384 0
error 20480 1 secvio
执行我们的测试程序,按动开发板上的按键,就可看见按键按下的提示:
root@igkboard:~# ./key_irq_test
KEY0 Press, value = 0XF0
KEY0 Press, value = 0XF0
KEY0 Press, value = 0XF0
KEY0 Press, value = 0XF0
KEY0 Press, value = 0XF0
root@igkboard:~# dmesg | tail -20
[ 211.077576] key_irq key_irq: success to get 1 valid key
[ 211.083746] success to install driver[major=243]!
[ 211.286380] keyrelease
[ 279.566310] keypress
[ 279.686329] keyrelease
[ 280.156309] keypress
[ 280.336327] keyrelease
[ 280.696316] keypress
[ 280.816326] keyrelease
[ 281.156311] keypress
[ 281.296311] keyrelease
[ 281.556325] keypress
[ 281.686326] keyrelease
卸载驱动:
root@igkboard:~# rmmod key_irq
root@igkboard:~# lsmod
Module Size Used by
rtl8188fu 999424 0
imx_rngc 16384 0
rng_core 20480 1 imx_rngc
secvio 16384 0
error 20480 1 secvio
中断处理程序通常被分为两个部分:顶半部分和底半部分,也称为上半部分和下半部分。
我们在使用request_irq申请中断的中断服务函数属于中断处理的上半部,只要中断触发。那么中断处理函数就会执行。然而一些中断的产生之后需要较长的时间来处理,如由于网络传输产生的中断, 在产生网络传输中断后需要比较长的时间来处理接收或者发送数据,因为在linux下中断并不能被嵌套如果这时有其他中断产生就不能够及时的响应,为了解决这个问题,linux对中断的处理引入了“中断上半部”和 “中断下半部”的概念,在中断的上半部中只对中断做简单的处理,把需要耗时处理的部分放在中断下半部中,使得能够 对其他中断作为及时的响应,提供系统的实时性。这一概念又被称为中断分层。
上半部分是中断处理程序的第一部分,它在中断发生后立即执行。上半部分通常处理一些紧急的、高优先级的任务,例如更新硬件状态、保存寄存器、禁用中断等。由于上半部分需要快速执行,因此应该尽可能地简单、短暂和高效。
下半部分是中断处理程序的第二部分,它在上半部分执行完毕后延迟执行。下半部分通常处理一些非紧急的、低优先级的任务,例如访问I/O设备、调度进程、释放锁等。由于下半部分可以在中断返回后再执行,因此可以较长时间地执行复杂的操作,但也需要避免过多的延迟。
上半部分和下半部分之间的通信通常通过共享数据结构或标志位实现。例如,在s上半部分中可以设置一个标志位表示需要执行某个下半部分任务,在下半部分中检查该标志位并执行相应的任务。为了确保数据的一致性和可靠性,上半部分和下半部分之间还需要避免竞态条件和锁等问题。
总之,上半部分和下半部分是中断处理程序的两个重要组成部分,合理使用它们可以提高系统的性能和稳定性。
中断分层实现方法常用的有三种,分别为软中断、tasklet、和工作队列,下面分别介绍这三种方式。
软中断(Soft Interrupt)是一种由软件触发的中断机制,用于在内核空间中执行高优先级的任务。与硬中断不同,软中断不需要外部硬件的支持,可以在内核线程中实现。
软中断通常用于处理一些紧急的、高优先级的任务,例如网络数据包的接收和发送、磁盘I/O等操作。当需要执行这些任务时,内核会通过软中断机制将任务添加到工作队列中,并向CPU发送一个中断信号。CPU在适当的时候中断当前进程并执行软中断处理程序来处理这些任务。完成任务后,软中断处理程序会立即返回到原来的进程继续执行。
软中断机制存在的优点:
软中断机制存在的缺点:
总之,软中断机制是内核中一个重要的中断处理机制,可以在需要高优先级任务处理时快速响应和执行。但为了确保系统的性能和稳定性,开发人员需要合理地使用软中断,并针对具体应用场景进行优化和调整。
tasklet是基于软中断实现,如果对效率没有特殊要求推荐是用tasklet实现中断分层。
为什么这么说, 根据之前讲解软中断的中断服务函数是一个全局的数组,在多CPU系统中,所有CPU都可以访问, 所以在多CPU系统中需要用户自己考虑并发、可重入等问题,增加编程负担。 软中断资源非常有限一些软中断是为特定的外设准备的(不是说、只能用于特定外设)。如“NET_TX_SOFTIRQ、NET_RX_SOFTIRQ” 从名字可以看出它们用于网络的TX和RX。像网络这种对效率要求较高的场合还是会使用软中断实现中断分层的。
相比软中断,tasklet使用起来更简单,最重要的一点是在多CPU系统中同一时间只有一个CPU运行tasklet, 所以并发、可重入问题就变得很容易处理(一个tasklet甚至不用去考虑)。而且使用时也比较简单,介绍如下。
在Linux内核中,与Tasklet相关的函数包括:
此外,还有一些与 Tasklet 相关的宏定义和数据结构,例如:
下面简单介绍几个。
Tasklet 数据结构是一个包含回调函数、数据参数和状态信息等成员变量的结构体,用于表示一个 Tasklet。
在驱动中使用tasklet_struct结构体表示一个tasklet,结构体定义如下所示:
struct tasklet_struct {
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);
unsigned long data;
};
tasklet_struct 结构体定义了 Tasklet 的五个主要成员变量:
要使用tasklet,必须先定义一个tasklet然后使用tasket_init函数初始化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;
}
使用 tasklet_init() 函数可以将一个 Tasklet 变量初始化为一个新的、未被安排执行的 Tasklet。在初始化后,Tasklet 变量的默认状态为已经启用并且没有被禁用。
举个栗子:
struct tasklet_struct my_tasklet;
tasklet_init(&my_tasklet, my_tasklet_fn, 0);
这将创建一个名为 my_tasklet 的 Tasklet 变量,并将其回调函数设置为 my_tasklet_fn,数据参数设置为 0,并将其初始化为一个新的可用 Tasklet。
在上半部,也就中断处理函数中调用tasklet_schedule函数就能使tasklet在合适的时间运行。
将一个Tasklet加入到任务队列中等待执行:
static inline void tasklet_schedule(struct tasklet_struct *t)
{
if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
__tasklet_schedule(t);
}
tasklet_test.c代码:
/*************************************************************************
> File Name: tasklet_test.c
> Author: WangDengtao
> Mail: [email protected]
> Created Time: 2023年04月01日 星期六 21时23分25秒
************************************************************************/
#include
#include
#include
#include
#define IRQ_NUM 1 // 用于本例子的虚拟中断号
static struct tasklet_struct my_tasklet;
// 中断处理程序
static irqreturn_t my_interrupt(int irq, void *dev_id)
{
// 执行一些紧急任务
printk(KERN_INFO "Executing some urgent tasks...\n");
// 调度tasklet
tasklet_schedule(&my_tasklet);
return IRQ_HANDLED;
}
// tasklet处理程序
static void my_tasklet_fn(unsigned long data)
{
printk(KERN_INFO "Tasklet executed\n");
}
static int __init my_tasklet_init(void)
{
printk(KERN_INFO "Tasklet module loaded\n");
// 初始化tasklet
tasklet_init(&my_tasklet, my_tasklet_fn, 0);
// 注册中断处理程序
if (request_irq(IRQ_NUM, my_interrupt, IRQF_SHARED, "my_interrupt", &my_tasklet)) {
printk(KERN_ERR "Failed to register interrupt\n");
return -EFAULT;
}
return 0;
}
static void __exit my_tasklet_exit(void)
{
printk(KERN_INFO "Tasklet module unloaded\n");
// 停止中断处理程序
free_irq(IRQ_NUM, &my_tasklet);
// 停止tasklet
tasklet_kill(&my_tasklet);
}
module_init(my_tasklet_init);
module_exit(my_tasklet_exit);
/*添加LICENSE和作者信息,是来告诉内核,该模块带有一个自由许可证;没有这样的说明,在加载模块的时内核会“抱怨”.*/
MODULE_LICENSE("Dual BSD/GPL");//许可 GPL、GPL v2、Dual MPL/GPL、Proprietary(专有)等,没有内核会提示.
MODULE_AUTHOR("WangDengtao");//作者
MODULE_VERSION("V1.0");//版本
Makefile:
KERNAL_DIR ?= /lib/modules/$(shell uname -r)/build
PWD :=$(shell pwd)
obj-m := tasklet_test.o
all:
$(MAKE) -C $(KERNAL_DIR) M=$(PWD) modules
@make clear
clean:
@rm -f *.ko
clear:
@rm -f *.o *.cmd *.mod *.mod.c
@rm -rf *~ core .depend .tmp_versions Module.symvers modules.order -f
@rm -f .*ko.cmd .*.o.cmd .*.o.d
@rm -f *.unsigned
结果:
wangdengtao@wangdengtao-virtual-machine:~/wangdengtao/driver/x86$ sudo insmod tasklet_test.ko
[sudo] wangdengtao 的密码:
wangdengtao@wangdengtao-virtual-machine:~/wangdengtao/driver/x86$ sudo rmmod tasklet_test
wangdengtao@wangdengtao-virtual-machine:~/wangdengtao/driver/x86$ dmesg
wangdengtao@wangdengtao-virtual-machine:~/wangdengtao/driver/x86$ sudo dmesg
[ 160.518190] Tasklet module loaded
[ 160.540421] Executing some urgent tasks...
[ 160.540427] Tasklet executed
[ 162.372465] Executing some urgent tasks...
[ 162.372489] Tasklet executed
[ 162.452298] Executing some urgent tasks...
[ 162.452321] Tasklet executed
[ 162.453113] Executing some urgent tasks...
[ 162.453116] Tasklet executed
[ 162.572318] Executing some urgent tasks...
[ 162.572343] Tasklet executed
[ 162.748576] Executing some urgent tasks...
......
[ 556.656698] Tasklet module unloaded
DECLARE_TASKLET() 是一个宏定义,用于声明并定义一个 Tasklet。它的定义如下:
#define DECLARE_TASKLET(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }
使用 DECLARE_TASKLET() 宏可以快速创建一个 Tasklet 变量,并将其回调函数和数据参数设置为指定的值。
该宏包含三个参数:
举个栗子:
DECLARE_TASKLET(my_tasklet, my_tasklet_fn, 0);
这将创建一个名为 my_tasklet 的 Tasklet 变量,并将其回调函数设置为 my_tasklet_fn,数据参数设置为 0。
DECLARE_TASKLET_DISABLED() 是一个宏定义,用于声明并定义一个已经禁用的 Tasklet。它的定义如下:
#define DECLARE_TASKLET_DISABLED(name, func, data) \
struct tasklet_struct name = { NULL, TASKLET_STATE_SCHED | TASKLET_STATE_NO_LOCK, ATOMIC_INIT(0), func, data }
使用 DECLARE_TASKLET_DISABLED() 宏可以快速创建一个已经禁用的 Tasklet 变量,并将其回调函数和数据参数设置为指定的值。
该宏包含三个参数:
举个栗子:
DECLARE_TASKLET_DISABLED(my_tasklet, my_tasklet_fn, 0);
这将创建一个名为 my_tasklet 的 Tasklet 变量,并将其回调函数设置为 my_tasklet_fn,数据参数设置为 0。
DECLARE_TASKLET_DISABLED() 宏会自动设置 Tasklet 的状态为 TASKLET_STATE_SCHED | TASKLET_STATE_NO_LOCK,即已经被安排执行但没有被锁定,并且处于禁用状态。
如果要手动将 Tasklet 的状态设置为 TASKLET_STATE_SCHED | TASKLET_STATE_NO_LOCK,可以使用以下代码:
my_tasklet.state = TASKLET_STATE_SCHED | TASKLET_STATE_NO_LOCK;
DECLARE_TASKLET_HI() 是一个宏定义,用于声明并定义一个高优先级的 Tasklet。它的定义如下:
#define DECLARE_TASKLET_HI(name, func, data) \
struct tasklet_struct name = { NULL, TASKLET_STATE_SCHED | TASKLET_STATE_NO_LOCK | TASKLET_STATE_HIGHPRI, ATOMIC_INIT(0), func, data }
使用 DECLARE_TASKLET_HI() 宏可以快速创建一个高优先级的 Tasklet 变量,并将其回调函数和数据参数设置为指定的值。
该宏包含三个参数:
例如,以下代码片段演示了如何使用 DECLARE_TASKLET_HI() 宏来创建和初始化一个高优先级的 Tasklet:
DECLARE_TASKLET_HI(my_tasklet, my_tasklet_fn, 0);
与软中断和tasklet不同,工作队列运行在内核线程,允许被重新调度和睡眠。 如果中断的下部分能够接受被重新调度和睡眠,推荐使用工作队列。
下面介绍一下有关的结构体以及函数。
在驱动中一个工作结构体代表一个工作,工作结构体如下所示:
struct work_struct {
atomic_long_t data;
struct list_head entry;
work_func_t func; /*工作队列处理函数*/
#ifdef CONFIG_LOCKDEP struct lockdep_map lockdep_map;
#endif
};
这些工作组织成工作队列,工作队列使用workqueue_struct结构体表示。
struct workqueue_struct {
struct list_head pwqs;
struct list_head list;
struct mutex mutex;
int work_color;
int flush_color;
atomic_t nr_pwqs_to_flush;
struct wq_flusher *first_flusher;
struct list_head flusher_queue;
struct list_head flusher_overflow;
struct list_head maydays;
struct worker *rescuer;
int nr_drainers;
int saved_max_active;
struct workqueue_attrs *unbound_attrs;
struct pool_workqueue *dfl_pwq;
char name[WQ_NAME_LEN];
struct rcu_head rcu;
unsigned int flags ____cacheline_aligned;
struct pool_workqueue __percpu *cpu_pwqs;
struct pool_workqueue __rcu *numa_pwq_tbl[];
};
Linux 内核使用工作者线程(worker thread)来处理工作队列中的各个工作。并且使用 worker 结构体表示工作者线程,worker 结构体内容如下:
struct worker {
union {
struct list_head entry;
struct hlist_node hentry;
};
struct work_struct *current_work;
work_func_t current_func;
struct pool_workqueue *current_pwq;
bool desc_valid;
struct list_head scheduled;
struct task_struct *task;
struct worker_pool *pool;
struct list_head node;
unsigned long last_active;
unsigned int flags;
int id;
char desc[WORKER_DESC_LEN];
struct workqueue_struct *rescue_wq;
};
每个 worker 都有一个工作队列,工作者线程处理自己工作队列中的所有工作。
#define INIT_WORK(_work, _func)
也可以使用DECLARE_WOEK宏一次性完成工作的创建和初始化:
#define DECLARE_WORK(n,f)
驱动工作函数执行后相应内核线程将会执行工作结构体指定的处理函数,驱动函数如下所示。
static inline bool schedule_work(struct work_struct *work)
移除工作队列:
static inline cancel_work_sync(struct work_struct *work);
/*************************************************************************
> File Name: tasklet_test.c
> Author: WangDengtao
> Mail: [email protected]
> Created Time: 2023年04月01日 星期六 21时23分25秒
************************************************************************/
#include
#include
#include
#include
#include
#include
#define IRQ_NUM 1 // 用于本例子的虚拟中断号
static struct work_struct my_workqueue;
// 中断处理程序
static irqreturn_t my_interrupt(int irq, void *dev_id)
{
// 执行一些紧急任务
printk(KERN_INFO "Executing some urgent tasks...\n");
// 调度workqueue
schedule_work(&my_workqueue);
return IRQ_HANDLED;
}
// 工作队列处理程序
static void my_workqueue_fn(struct work_struct *t)
{
printk(KERN_INFO "workqueue executed\n");
}
static int __init my_workqueue_init(void)
{
printk(KERN_INFO "workqueue module loaded\n");
// 初始化workqueue
INIT_WORK(&my_workqueue, my_workqueue_fn);
// 注册中断处理程序
if (request_irq(IRQ_NUM, my_interrupt, IRQF_SHARED, "my_interrupt", &my_workqueue))
{
printk(KERN_ERR "Failed to register interrupt\n");
return -EFAULT;
}
return 0;
}
static void __exit my_workqueue_exit(void)
{
printk(KERN_INFO "workqueue module unloaded\n");
// 停止中断处理程序
free_irq(IRQ_NUM, &my_workqueue_fn);
// 停止workqueue
cancel_work_sync(&my_workqueue);
}
module_init(my_workqueue_init);
module_exit(my_workqueue_exit);
/*添加LICENSE和作者信息,是来告诉内核,该模块带有一个自由许可证;没有这样的说明,在加载模块的时内核会“抱怨”.*/
MODULE_LICENSE("Dual BSD/GPL");//许可 GPL、GPL v2、Dual MPL/GPL、Proprietary(专有)等,没有内核会提示.
MODULE_AUTHOR("WangDengtao");//作者
MODULE_VERSION("V1.0");//版本
Makefile:
KERNAL_DIR ?= /lib/modules/$(shell uname -r)/build
PWD :=$(shell pwd)
obj-m := workqueue_test.o
all:
$(MAKE) -C $(KERNAL_DIR) M=$(PWD) modules
@make clear
clean:
@rm -f *.ko
clear:
@rm -f *.o *.cmd *.mod *.mod.c
@rm -rf *~ core .depend .tmp_versions Module.symvers modules.order -f
@rm -f .*ko.cmd .*.o.cmd .*.o.d
@rm -f *.unsigned
结果:
[ 187.083640] workqueue module loaded
[ 187.099458] Executing some urgent tasks...
[ 187.099491] workqueue executed
[ 187.219689] Executing some urgent tasks...
[ 187.219715] workqueue executed
[ 187.283704] Executing some urgent tasks...
[ 187.283713] workqueue executed
[ 187.435710] Executing some urgent tasks...
[ 187.435768] workqueue executed
[ 187.483435] Executing some urgent tasks...
[ 187.483443] workqueue executed