i.MX8MM处理器采用了先进的14LPCFinFET工艺,提供更快的速度和更高的电源效率;四核Cortex-A53,单核Cortex-M4,多达五个内核 ,主频高达1.8GHz,2G DDR4内存、8G EMMC存储。千兆工业级以太网、MIPI-DSI、USB HOST、WIFI/BT、4G模块、CAN、RS485等接口一应俱全。H264、VP8视频硬编码,H.264、H.265、VP8、VP9视频硬解码,并提供相关历程,支持8路PDM接口、5路SAI接口、2路Speaker。系统支持Android9.0(支持获取root限)Linux4.14.78+Qt5.10.1、Yocto、Ubuntu20、Debian9系统。适用于智能充电桩,物联网,工业控制,医疗,智能交通等,可用于任何通用工业和物联网应用、
【公众号】迅为电子
【粉丝群】258811263(加群获取驱动文档+例程)
第六十章 中断下文之工作队列
本章导读
60.1章节讲解了工作队列理论基础
60.2章节讲解了工作队列相关API
60.3章节 以IMX8MM开发板为例,进行驱动试验
本章内容对应视频讲解链接(在线观看):
工作队列 → https://www.bilibili.com/video/BV1Vy4y1B7ta?p=39
程序源码在网盘资料“iTOP-i.MX8MM开发板\02-i.MX8MM开发板网盘资料汇总(不含光盘内容)\嵌入式Linux开发指南(iTOP-i.MX8MM)手册配套资料\2.驱动程序例程\017-工作队列实验”路径下。
60.1 工作队列理论基础
工作队列(workqueue)是实现中断下文的机制之一,是一种将工作推后执行的形式。那工作队列和我们之前学的tasklet机制有什么不同呢?tasklet也是实现中断下文的机制。他们两个最主要的区别是tasklet不能休眠,而工作队列是可以休眠的。所以,tasklet可以用来处理比较耗时间的事情,而工作队列可以处理非常复杂并且更耗时间的事情。
Linux系统在启动期间会创建内核线程,该线程创建以后就处于sleep状态,然后这个线程会一直去队列里面读,看看有没有任务,如果有就执行,如果没有就休眠。工作队列的实现机制实际上是非常复杂的,初学阶段只需要了解这些基本概念接口。
类比理解:
流水线上的机器:Linux系统自动会创建一个。多种不同的物料使用同一个流水线机械,那么这个就是共享工作队列的概念。
如果当前的流水线机械不能满足我们加工的物料,那么需要重新定制一台流水线机器,这个就是自定义工作队列的概念。共享队列虽然不需要自己创建,但是如果前面的工作比较耗时间,就会影响后面的工作。而且自定义工作队列需要自己创建,系统开销大。优点是不会受到其他工作的影响。(因为这个流水线就是专门加工这一种零件的。)
60.2 工作队列相关API
尽管工作队列的实现机制非常复杂,但是我们使用工作队列其实就是在这个流水线上添加自己的物料,然后等待执行即可。
Linux 内核使用 work_struct 结构体表示一个工作,内容如下。
struct work_struct {
atomic_long_t data;
struct list_head entry;
work_func_t func; /* 工作队列处理函数 */
};
在这个结构体里面我们只需要关注func这个成员就可以了,他是一个函数指针,因为要将需要完成的工作写在这个函数里面。
这些工作组织成工作队列,工作队列使用 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[];
};
每个 worker 都有一个工作队列,工作的线程处理自己工作队列中的所有工作。在实际的驱动开发中,
我们只需要定义工作(work_struct)即可,关于工作队列和工作者线程我们基本不用去管。简单创建工作很简
单,直接定义一个 work_struct 结构体变量即可,然后使用 INIT_WORK 宏来初始化工作,INIT_WORK 宏
定义如下:
#define INIT_WORK(_work, _func)
work 表示要初始化的工作,_func 是工作对应的处理函数。也可以使用 DECLARE_WORK 宏一次性
完成工作的创建和初始化,宏定义如下:
#define DECLARE_WORK(n, f)
n 表示定义的工作(work_struct),f 表示工作对应的处理函数。
举例:
struct work_struct test;
在模块的初始化函数中:
INIT_WORK(&test, func) ;
相当于:
DECLARE_WORK(test, func);
和 tasklet 一样,工作也是需要调度才能运行的,工作的调度函数为 schedule_work,函数原型如下所
示:
函数 | int schedule_work(struct work_struct *work); |
_work | 工作队列地址 |
返回值 | 0 成功,其他值 失败 |
功能 | 调度工作,把work_struct挂到CPU相关的工作结构队列链表上,等待工作者线程处理。 |
注意 | 需要注意的是,如果调度完工作,并不会马上执行,只是加到了共享的工作队列里面去,等轮到他才会执行。如果我们多次调用相同的任务,假如上一次的任务还没有处理完成,那么多次调度相同的任务是无效的 |
60.3 工作队列实验
60.3.1 驱动程序编写
我们在IMX8MM开发板上为例,新建driver.c,并拷贝Makefile和build.sh到Ubuntu的/home/topeet/imx8mm/17目录下。完整的驱动代码,如下所示:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_irq.h>
#include <linux/gpio.h>
#include <linux/of_gpio.h>
#include <linux/interrupt.h>
#include <linux/workqueue.h>
//定义结构体表示我们的节点
struct device_node *test_device_node;
//要申请的中断号
int irq;
// GPIO 编号
int gpio_nu;
//定义tasklet结构体
//struct tasklet_struct key_test;
//定义工作结构体
struct work_struct key_test;
/**
* @description: 工作队列的处理函数
* @param {unsignedlong} data:要传递给 func 函数的参数
* @return {*}无
*/
void test(struct work_struct *data)
{
int i = 100;
printk("i is %d \n", i);
while (i--)
printk("test_key is %d \n", i);
}
/**
* @description: 中断处理函数test_key
* @param {int} irq :要申请的中断号
* @param {void} *args
* @return {*}IRQ_HANDLED
*/
irqreturn_t test_key(int irq, void *args)
{
printk("start\n");
//tasklet_schedule(&key_test);
schedule_work(&key_test);
printk("end\n");
return IRQ_HANDLED;
}
/****************************************************************************************
* @brief led_probe : 与设备信息层(设备树)匹配成功后自动执行此函数,
* @param inode : 文件索引
* @param file : 文件
* @return 成功返回 0
****************************************************************************************/
int led_probe(struct platform_device *pdev)
{
int ret = 0;
// 打印匹配成功进入probe函数
printk("led_probe\n");
test_device_node = of_find_node_by_path("/test");
if (test_device_node == NULL)
{
//查找节点失败则打印信息
printk("of_find_node_by_path is error \n");
return -1;
}
gpio_nu = of_get_named_gpio(test_device_node, "gpios", 0);
if (gpio_nu < 0)
{
printk("of_get_namd_gpio is error \n");
return -1;
}
//设置GPIO为输入模式
gpio_direction_input(gpio_nu);
//获取GPIO对应的中断号
irq = gpio_to_irq(gpio_nu);
// irq =irq_of_parse_and_map(test_device_node,0);
printk("irq is %d \n", irq);
/*申请中断,irq:中断号名字
test_key:中断处理函数
IRQF_TRIGGER_RISING:中断标志,意为上升沿触发
"test_key":中断的名字
*/
ret = request_irq(irq, test_key, IRQF_TRIGGER_RISING, "test_key", NULL);
if (ret < 0)
{
printk("request_irq is error \n");
return -1;
}
/* 初始化 tasklet */
//tasklet_init(&key_test,test,100);
INIT_WORK(&key_test, test);
return 0;
}
int led_remove(struct platform_device *pdev)
{
printk("led_remove\n");
return 0;
}
const struct platform_device_id led_idtable = {
.name = "keys",
};
const struct of_device_id of_match_table_test[] = {
{.compatible = "keys"},
{},
};
struct platform_driver led_driver = {
//3. 在led_driver结构体中完成了led_probe和led_remove
.probe = led_probe,
.remove = led_remove,
.driver = {
.owner = THIS_MODULE,
.name = "led_test",
.of_match_table = of_match_table_test},
//4 .id_table的优先级要比driver.name的优先级要高,优先与.id_table进行匹配
.id_table = &led_idtable};
/**
* @description: 模块初始化函数
* @param {*}
* @return {*}
*/
static int led_driver_init(void)
{
//1.我们看驱动文件要从init函数开始看
int ret = 0;
//2.在init函数里面注册了platform_driver
ret = platform_driver_register(&led_driver);
if (ret < 0)
{
printk("platform_driver_register error \n");
}
printk("platform_driver_register ok \n");
return 0;
}
/**
* @description: 模块卸载函数
* @param {*}
* @return {*}
*/
static void led_driver_exit(void)
{
free_irq(irq, NULL);
platform_driver_unregister(&led_driver);
printk("goodbye! \n");
}
module_init(led_driver_init);
module_exit(led_driver_exit);
MODULE_LICENSE("GPL");
将本章节编写的驱动程序编译为驱动模块,如下图所示:
60.3.2 测试实验
我们启动IMX8MM开发板,我们检查一下有没有我们在第五十七章添加的节点,如下图所示:
cd /proc/device-tree
cd test/
ls
然后我们挂载nfs共享目录,挂载好后进入共享目录,我们加载驱动模块,如下图所示:
我们然后按开发板上的音量+按键,打印100次输出,如下图所示: