文章目录
本笔记基于 Zephyr 版本 2.6.0-rc2
前言
本人正在学习 Zephyr,一个可移植性较强,可以兼容多种开发板及物联网设备的操作系统,如果你感兴趣,可以点此查看我的 学习笔记总述 进行了解!
摘要
Zephyr 中的工作队列与 Linux 的工作队列功能类似,用于实现中断的底半部。也就是说中断 ISR 中比较耗时的操作,放到工作队列中去执行。
Zephyr 中工作队列是基于线程的,简单来说,就是有一个线程一直在等待工作队列的 api 发来的工作项,当有工作项时(一个待执行的函数) 就处理 (把函数调用了),当有多个工作项时就按顺序处理,没有工作项时就休眠。
Workqueue Threads - (工作队列线程)
工作队列是一个内核对象,它使用专用线程以先进先出 (first in, first out) 的方式处理工作项 (work item)。每个工作项都是通过调用工作项指定的函数来处理的 (可以理解为,工作项就是一个一个等待调用的函数)。
工作队列通常由 ISR 或高优先级线程使用,将非紧急处理的事情交给对时间不敏感的低优先级线程,这样就不会影响对时间敏感的处理。
必须先初始化工作队列,然后才能使用它。这将队列清空为空并生成工作队列的线程 (由初始化函数完成)。线程永远运行,但当没有可用的工作项时进入休眠状态。
可以定义任意数量的工作队列 (仅受可用 RAM 的限制)。每个工作队列由其内存地址引用。
工作队列具有以下属性:
- 队列中的工作项都是被添加进去、但是还没被处理的
- 队列中的工作项是通过一个线程处理的,这个线程的优先级是可配置的,我们可以把它配置成协作式线程或者抢占式线程
无论工作队列线程优先级如何,工作队列线程将在每个提交的工作项之间产生,以防止协作工作队列耗尽其他线程。
1. Work Item Lifecycle - (工作项生命周期)
必须先初始化工作项,然后才能使用它。初始化的作用:把处理函数 (handler function)赋值给工作项,并把工作项标记为挂起,等着工作队列线程去调用。此函数接受一个参数,即工作项本身的地址。
可以定义任何数量的工作项 (work items),每个工作项均由其内存地址引用。
可以通过 ISR 或线程将工作项提交到工作队列中,从而使该工作项排队 ( K_WORK_QUEUED
) 。提交工作项会将工作项追加到工作队列的队列中。工作队列的线程会从他自己的队列中按顺序取出挂起的工作项,然后再执行这个工作项指定的函数。
Note:
工作项中可以使用任何的内核 API,但是注意使用那些导致线程阻塞的 API,比如睡眠,获取信号量之类的 API,这样会导致工作队列线程睡眠,那么同时处于这个工作队列的其他工作项,也将会被延时执行,因为工作项是按顺序,一个一个被串行处理的。
如果 ISR 或者线程重复提交一个已经在工作队列中已经挂起的工作项,那么这个工作项不会受到影响,同时在工作队列中的位置将不会受到影响。并且工作只执行一次。
被提交的工作在没有被处理之前,不能被重新初始化。
工作项在工作队列上运行时将 运行 (K_WORK_RUNNING
),如果它在线程请求取消之前开始运行,也可能被取消 (K_WORK_CANCELING
)。
一个工作项可以处于多个状态。例如,它可以是:
- 在队列中运行
- 标记为取消 (因为线程使用
k_work_cancel_sync()
来等待工作项完成) - 排队再次在同一队列上运行
- 计划提交到一个 (可能不同的) 队列
同时所有 (all simultaneously)。处于上述任何一种状态的工作项是 pending(k_work_is_pending()
) 或 busy (k_work_busy_get()
) 状态。
2. Delayable Work - (可延迟的工作)
当一个 ISR 或者线程提交一个工作项,但是不想让工作项立即被执行,想让工作项等待一段时间在执行。这时可以使用延时工作项。
简单理解就是,一个工作项提交的时候不是直接提交给工作队列,而是指定一个超时,当超时发生的时候,再由内核将这个工作项提交给工作队列,并保持工作项为挂起状态等着工作队列线程去处理。
通过 scheduling 一个 delayable work item 在超时后提交到一个工作队列来完成。
与标准工作项相比,delayable work在标准的工作项的基础上,添加了一些字段,这些字段记录了应该提交该项的时间和地点。
delayable work 与标准工作项的初始化方式及调度到工作队列的方式类似,只是使用的 api 不同。当发出调度请求时,内核会启动一个超时机制,在指定的延时过后触发,之后和标准工作项的方式一样处理。
3. Triggered Work - (触发的工作)
触发工作项是和 POLL 机制相关的工作项,用的比较少。是在一个线程中等待多个内核对象有效,比如等待信号量,FIFO等。
触发工作项是具有以下添加属性的标准工作项:
- 指针:指向一系列轮询事件的指针,这些轮询事件将触发工作项向工作队列的提交。
- 数组大小:包含轮询事件 (poll events) 的数组的大小。
与 delayable work 一样,triggered work 也是用与标准工作项类似的初始化方式及调度到工作队列。发出提交请求后,内核开始观察由轮询事件指定的内核对象。一旦观察到的内核对象至少有一个更改状态,工作项就会提交到指定的工作队列中,在那里它将保持队列状态,直到以标准方式处理它。
还在等待轮询事件的 triggered work 可以被 ISR 或线程取消,在这种情况下,内核将将停止等待轮询事件,并且指定的工作项不被执行,否则无法取消。
4. System Workqueue - (系统工作队列)
前面讲了工作项和工作队列,在这解释一下工作队列和工作项的区别:工作项 (work item)只是被提交到工作队列 (work queue)的一个元素。
内核定义了一个称为系统工作队列 (system workqueue) 的工作队列,该工作队列可用于需要工作队列支持的任何应用程序或内核代码。系统工作队列是可选的,并且只有在应用程序使用它时才存在。
如果内核使能了工作队列的功能,内核会创建系统工作队列。这个系统工作队列的线程的优先级可以通过 menuconfig 去配置。
相比于系统工作队列,用户也可以创建自己的工作队列。比如有比较复杂然后特别耗时的事情需要做,那么用户可以自己创建一个工作队列,然后把时间不敏感,不着急处理的事扔给自己创建的工作队列。但是 Zephyr 官方不推荐自己创建工作队列,因为工作队列会消耗大量资源(RAM, flash),毕竟创建一个工作队列就会创建一个线程,一个线程就会包括何种内核对象,私有栈空间等,一般 Zephyr 是跑在资源受限的 MCU上的,所以不太推荐。
Important:
只有当无法将新工作项提交给系统工作队列时,才应定义其他工作队列,因为每个新工作队列都会导致相当大的内存占用成本。
5. How to Use Workqueues - (如何使用工作队列)
5.1 Defining and Controlling a Workqueue - (定义和控制工作队列)
一个工作队列是使用 k_work_q
类型的变量来定义的。需要使用自己定义的栈,然后调用 k_work_queue_start()
来初始化工作队列。必须使用 K_THREAD_STACK_DEFINE
定义堆栈区域,以确保在内存中正确地设置了它。
一下代码定义并初始化了一个工作队列:
#define MY_STACK_SIZE 512
#define MY_PRIORITY 5
K_THREAD_STACK_DEFINE(my_stack_area, MY_STACK_SIZE);
struct k_work_q my_work_q;
k_work_queue_start(&my_work_q, my_stack_area,
K_THREAD_STACK_SIZEOF(my_stack_area), MY_PRIORITY,
NULL);
此外,可以通过最后一个可选的参数控制与线程重新调度相关的队列标识和某些行为。
5.2 Submitting a Work Item - (提交工作项)
工作项是使用 k_work
类型的变量来定义的。必须使用 k_work_init()
进行初始化,除非它是使用 K_WORK_DEFINE
定义的,在这种情况下,在编译时执行初始化。
一个初始化的工作项可以通过调用 k_work_submit()
被提交到队列中,或者通过调用 k_work_submit_to_queue()
提交到一个指定的队列中。
以下代码演示了 ISR 如何将错误消息的打印到系统工作队列中。请注意,如果 ISR 尝试重新提交处于排队状态的工作项,则该工作项将保持不变,并且不会打印相关的错误消息。
struct device_info {
struct k_work work;
char name[16]
} my_device;
void my_isr(void *arg)
{
...
if (error detected) {
k_work_submit(&my_device.work);
}
...
}
void print_error(struct k_work *item)
{
struct device_info *the_device =
CONTAINER_OF(item, struct device_info, work);
printk("Got error on device %s\n", the_device->name);
}
/* initialize name info for a device */
strcpy(my_device.name, "FOO_dev");
/* initialize work item for printing device's error messages */
k_work_init(&my_device.work, print_error);
/* install my_isr() as interrupt handler for the device (not shown) */
...
5.3 Scheduling a Delayable Work Item - (调度可延迟工作项)
可延迟工作项是使用 k_work_delayable
类型的变量来定义的。必须使用 k_work_init_delayable()
进行初始化。
通过调用 k_delayed_work_submit()
把延时工作项提交给系统工作队列,通过 k_delayed_work_submit_to_queue()
把延时工作项提交给用户自己定义的工作队列。
当想取消一个超时工作项,可以使用 k_delayed_work_cancel()
函数,但是需注意,取消只能在工作项指定的超时没发生之前,否则不能被取消。
6. Configuration Options - (配置选项)
参考链接
https://docs.zephyrproject.org/latest/reference/kernel/threads/workqueue.html