瑞芯微RK3568芯片是一款定位中高端的通用型SOC,采用22nm制程工艺,搭载一颗四核Cortex-A55处理器和Mali G52 2EE 图形处理器。RK3568 支持4K 解码和 1080P 编码,支持SATA/PCIE/USB3.0 外围接口。RK3568内置独立NPU,可用于轻量级人工智能应用。RK3568 支持安卓 11 和 linux 系统,主要面向物联网网关、NVR 存储、工控平板、工业检测、工控盒、卡拉 OK、云终端、车载中控等行业。
【公众号】迅为电子
【粉丝群】824412014(加群获取驱动文档+例程)
【视频观看】嵌入式学习之Linux驱动(第五期_中断_全新升级)_基于RK3568
【购买链接】迅为RK3568开发板瑞芯微Linux安卓鸿蒙ARM核心板人工智能AI主板
第44章 共享工作队列实验
在上个章节我们学习了中断下文的一种实验方式——软中断,本章节我们来学习中断下文的另一种实现方式——工作队列。工作队列是操作系统中管理和调度异步任务执行的一种机制,接下来开始学习工作队列吧。
44.1 什么是工作队列
工作队列是实现中断下半部分的机制之一,是一种用于管理任务的数据结构或机制。它通常用于多线程,多进程或分布式系统中,用于协调和分配待处理的任务给可用的工作线程或工作进程。
工作队列的基本原理是将需要执行的任务按顺序排列在队列中,并提供一组工作线程或者工作进程来处理队列中的任务。当有新的任务到达时,它们会被添加到队列的末尾,工作线程或工作进程从队列的头部获取任务,并执行相应的处理操作。
工作队列和之前学习的tasklet有什么不同呢?tasklet也是实现中断下半部分的机制之一。他们最主要的区别是tasklet不能休眠,而工作队列是可以休眠的,所以tasklet可以用来处理比较耗时间的事情,而工作队列可以处理更耗时间的事情。
工作队列将工作推后以后,会交给内核线程去执行。Linux在启动过程中会创建一个工作者内核线程,这个线程创建以后处于sleep状态。当有工作需要处理的时候,会唤醒这个线程去处理工作。
在内核中,工作队列包括共享工作队列和自定义工作队列这俩种类型。这两种类型的工作队列具有不同的特点和用途。
1 共享队列是由内核管理的全局工作队列,用于处理内核中一些系统级任务。共享工作队列是内核中一个默认工作队列,可以由多个内核组件和驱动程序共享使用。
2 自定义工作队列是由内核或驱动程序创建的特定工作队列,用于处理特定的任务。自定义工作队列通常与特定的内核模块或驱动程序相关联,用于执行该模块或驱动程序相关的任务。
本章节我们先来学习共享工作队列相关的知识。
在Linux 内核中,使用 work_struct 结构体表示一个工作项,这些工作组织成工作队列,工作队列使用 workqueue_struct 结构体表示,如下图所示,流水线相当于工作队列,流水线上一个个等待处理的物料相当于一个个工作。机器相当于内核线程或进程。
图 44-1
work_struct 结构体表示一个工作项,定义在include/linux/workqueue.h中,如下所示:
struct work_struct {
atomic_long_t data;
struct list_head entry;
work_func_t func; /* 工作队列处理函数 */
};
typedef void (*work_func_t)(struct work_struct *work); //工作函数
44.2 工作队列相关接口函数
44.2.1 初始化函数
在实际的驱动开发中,我们只需要定义工作项(work_struct)即可,关于工作队列和工作者线程我们基本不用去管。简单创建工作很简单,直接定义一个 work_struct 结构体变量即可,然后使用 INIT_WORK 宏来初始化工作,INIT_WORK 宏定义如下:
#define INIT_WORK(_work,_func)
INIT_WORK 宏接受两个参数:_work 和 _func,分别表示要初始化的工作项和工作项的处理函数。
也可以使用 DECLARE_WORK 宏一次性完成工作的创建和初始化,宏定义如下:
#define DECLARE_WORK(n, f)
参数n 表示定义的工作(work_struct),f 表示工作对应的处理函数。
44.2.2 调度/取消调度工作队列函数
和 tasklet 一样,工作也是需要调度才能运行的,工作的调度函数为 schedule_work,函数原型如下所示:
static inline bool schedule_work(struct work_struct *work)
参数是指向工作项的指针。这个函数作用是将工作项提交到工作队列中,并请求调度器在合适的时机执行工作项。该函数会返回一个布尔值,表示工作项是否成功被提交到工作队列。
如果想要取消该工作项的调度,使用以下函数:
bool cancel_work_sync(struct work_struct *work);
参数是指向工作项的指针。这个函数的作用是取消该工作项的调度。如果工作项已经在工作队列中,它将被从队列中移除。如果工作项已经在工作队列中,它将被从队列中移除,并等待工作项执行完成。函数返回一个布尔值,表示工作项是否成功取消。
44.3 实验程序的编写
44.3.1 驱动程序编写
本实验对应的网盘路径为:iTOP-RK3568开发板【底板V1.7版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux驱动配套资料\04_Linux驱动例程\34_workqueue\module。
本实验将实现注册显示屏触摸中断,每按当触摸LCD显示屏就会触发中断服务函数,在中断服务函数中提交工作项到共享工作队列中,打印“This id test_interrupt”和“This is test_work”。
在驱动程序中的模块初始化函数中,我们将GPIO转换为中断号,并使用request_irq函数请求中断,然后初始化工作项。当中断被触发时,中断处理函数被调用,并将工作项提交到共享工作队列中,最终由工作项处理函数异步执行。编写完成的interrupt.c代码如下所示,添加的代码已加粗表示。
#include <linux/module.h>
#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/gpio.h>
#include <linux/delay.h>
#include <linux/workqueue.h>
int irq;
struct work_struct test_workqueue;
// 工作项处理函数
void test_work(struct work_struct *work)
{
msleep(1000);
printk("This is test_work\n");
}
// 中断处理函数
irqreturn_t test_interrupt(int irq, void *args)
{
printk("This is test_interrupt\n");
// 提交工作项到工作队列
schedule_work(&test_workqueue);
return IRQ_RETVAL(IRQ_HANDLED);
}
static int interrupt_irq_init(void)
{
int ret;
irq = gpio_to_irq(101); // 将GPIO映射为中断号
printk("irq is %d\n", irq);
// 请求中断
ret = request_irq(irq, test_interrupt, IRQF_TRIGGER_RISING, "test", NULL);
if (ret < 0)
{
printk("request_irq is error\n");
return -1;
}
// 初始化工作项
INIT_WORK(&test_workqueue, test_work);
return 0;
}
static void interrupt_irq_exit(void)
{
free_irq(irq, NULL); // 释放中断
printk("bye bye\n");
}
module_init(interrupt_irq_init);
module_exit(interrupt_irq_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("topeet");
44.4 运行测试
44.4.1 编译驱动程序
在上一小节中的interrupt.c代码同一目录下创建 Makefile 文件,Makefile 文件内容如下所示:
export ARCH=arm64#设置平台架构
export CROSS_COMPILE=aarch64-linux-gnu-#交叉编译器前缀
obj-m += interrupt.o #此处要和你的驱动源文件同名
KDIR :=/home/topeet/Linux/linux_sdk/kernel #这里是你的内核目录
PWD ?= $(shell pwd)
all:
make -C $(KDIR) M=$(PWD) modules #make操作
clean:
make -C $(KDIR) M=$(PWD) clean #make clean操作
对于Makefile的内容注释已在上图添加,保存退出之后,来到存放interrupt.c和Makefile文件目录下,如下图(图44-2)所示:
图 44-2
然后使用命令“make”进行驱动的编译,编译完成如下图(图44-3)所示:
图 44-3
编译完生成interrupt.ko目标文件,如下图(图44-4)所示:
图 44-4
至此驱动模块就编译成功了,接下来进行测试。
44.4.2 运行测试
开发板启动之后,使用以下命令进行驱动模块的加载,如下图(图44-5)所示:
insmod interrupt.ko
图44-5
加载驱动之后,可以看到申请的中断号被打印了出来,然后用手触摸连接的LVDS 7寸屏幕,打印如下图(44-6)所示:
在上图中,可以看到打印中断处理函数中添加的打印“This is test_interrupt”被多次打印,说明触发了好几次中断上文,那么中断上文会多次调度中断下文,所以也会打印工作项处理函数中添加的打印“This is test_work”。但是为什么只会打印俩次“This is test_work”呢?这是因为在中断上文调度工作项处理函数之后,内核没有来得及去执行工作项处理函数,没有执行相当于无效操作,有效的执行则打印了俩次“This is test_work”。
最后可以使用以下命令进行驱动的卸载,如下图(图44-7)所示:
rmmod interrupt
图 44-7
至此,共享工作队列实验就完成了。