NCS-OS系列9 :工作队列线程
前言
ncs 相关文章,部分为原始文档翻译,水平有限,如果有错误,欢迎指出。
概念
工作队列是一个内核对象,它使用专用的线程以先入先出的方式处理工作项。通过调用工作项指定的函数来处理每个工作项。工作队列通常由ISR
或高优先级线程使用,以将非紧急处理工作卸给低优先级线程,这样它就不会影响对时间敏感的工作的处理。
可以定义任意数量的工作队列
,每个工作队列都被它的内存地址引用。
工作队列
有下面的关键属性:
- 已添加但尚未处理的工作项队列。
- 处理队列中工作项的线程。线程的优先级是可配置的,允许它根据需要是协作的或抢占。
工作队列在使用之前必须初始化,这会将其队列设置为空,并生成工作队列的线程。
工作项生命周期
可以定义任意数量的工作项
,每个工作项都被它的内存地址引用。
工作项
有如下关键属性:
- 处理程序函数(
handler function
),它是工作队列的线程在处理工作项时执行的函数,此函数接受单个参数,即工作项自身的地址。 - 一个挂起标志(
pending flag
),内核使用它来表示工作项当前是工作队列的一个成员。 - 队列链接(
queue link
),内核使用它将一个挂起的工作项链接到工作队列的下一个挂起的工作项。
工作项必须在使用之前初始化。这会记录工作项的处理程序函数,并将其标记为not pending
。
工作项可以由ISR
或线程
提交到工作队列
。提交工作项将该工作项追加到工作队列的队列中。一旦工作队列的线程处理了其队列中的所有前面的工作项,该线程将从其队列中移除一个挂起的工作项,并调用该工作项的处理程序函数。根据工作队列线程的调度优先级以及队列中其他项所需的工作,挂起的工作项可能会被快速处理,也可能会在队列中保留很长一段时间。
处理函数可以利用线程可用的任何内核API。然而,可能会引起阻塞的操作(例如获取信号量)必须小心使用,因为工作队列在handler函数执行完之前不能处理其队列中的后续工作项。
如果不需要,可以忽略传递给处理程序函数的单个参数。如果处理程序函数需要关于它要执行的工作的额外信息,那么工作项可以嵌入到更大的数据结构中。处理程序函数然后可以使用实参值来计算外围数据结构的地址,从而获得它需要的附加信息的访问权。
工作项通常初始化一次,然后在需要执行工作时提交给特定的工作队列。如果ISR或线程试图提交一个已经挂起的工作项,则该工作项不会受到影响;工作项保持在工作队列的当前位置,并且工作只执行一次。
允许处理程序函数向工作队列重新提交其工作项参数,因为此时工作项不再是挂起的。这允许处理程序分阶段执行工作,而不会过度延迟工作队列中其他工作项的处理。
注意:在工作队列线程处理挂起的工作项之前,不能更改该工作项。这意味着工作项在挂起时不能重新初始化。z此外,在处理程序函数完成执行之前,工作项的处理程序函数执行其工作所需的任何附加信息都不能更改。没有可用来确定处理程序函数是否已经完成执行的内核API。使用工作项并需要知道工作项状态的时候必须在处理程序函数中自己管理状态。
延迟工作
ISR或线程可能需要安排只在指定时间段后(而不是立即)处理的工作项。这可以通过向工作队列提交延迟的工作项(而不是标准工作项)来实现。
延迟工作项是在标准工作项的基础上,增加了下面的属性:
- 指定在工作项实际提交到工作队列的队列之前等待的时间间隔
delay
。 - 工作队列指示,标识工作项要提交到的工作队列。
尽管使用了不同的内核api,延迟的工作项以与标准工作项相似的方式初始化并提交给工作队列,提交请求时,内核会启动一个超时机制,该机制在指定的延迟之后被触发。一旦超时发生,内核将延迟的工作项提交到指定的工作队列,在那里它将保持挂起状态,直到以标准方式处理它。
ISR或线程可能试图取消延迟的工作项。如果成功,则不执行指定的工作。然而,尝试取消延迟的工作项只有在两种情况下会成功:
- 它的延时时间还没有结束并且没有被处理
- 它仍然处于挂起状态,在工作队列的线程执行它之前,取消操作成功地将它从工作队列中删除。
由于使用了锁来管理工作队列,所以有些临时状态有时是不可观察的,但如果被观察到,就会导致取消操作失败。在这些情况下,工作项可能被调用,也可能不被调用。在下面这些情况下,瞬态状态可观察到并会导致失败:
- 工作队列或应用程序线程是可抢占的;
- API是从ISR调用的;
- 代码在多处理器系统上运行时;
注意,k_delayed_work_submit_to_queue()
和k_delayed_work_cancel()
都试图取消先前提交的项目,可能会失败。当它们失败时,可能调用前一个提交的工作处理程序,也可能不调用。
警告:由于这些苛刻的条件,所有调用延迟工作API的代码都必须检查返回值,并准备在提交或取消失败时作出反应。
触发工作
k_work_poll_submit()
接口调度一个触发的工作项以响应一个轮询事件,当监视的资源可用或轮询信号被触发或超时发生时,该事件将调用一个用户定义的函数。与k_poll()
不同,触发的工作不需要一个专门的线程等待或主动轮询一个轮询事件。
触发的工作项是一个标准的工作项,它具有以下额外的属性:
- 指向将触发向工作队列提交工作项的轮询事件数组的指针
- 包含轮询事件的数组的大小
尽管使用了专用的内核api,触发工作项初始化,并以类似于标准工作项的方式提交给工作队列。当提交请求发出后,内核开始观察轮询事件指定的内核对象。一旦所观察到的内核对象中至少有一个处于更改状态,工作项就会提交到指定的工作队列,在那里它将保持挂起状态,直到以标准方式处理为止。
注意:被触发的工作项以及被引用的轮询事件数组必须是有效的,并且不能对一个完整的触发工作项生命周期(从提交到工作项执行或取消)进行修改。
只要它仍在等待轮询事件,ISR或线程可以取消它提交的已触发的工作项,在这种情况下,内核停止等待附加的轮询事件,并且指定的工作不会被执行。否则无法执行取消操作。
系统工作队列 System Workqueue
内核定义了一个称为系统工作队列的工作队列,任何需要工作队列支持的应用程序或内核代码都可以使用该工作队列。系统工作队列是可选的,仅当应用程序使用它时才存在。
注意:只有当不能向系统工作队列提交新的工作项时,才应该定义额外的工作队列,因为每个新的工作队列都会导致内存占用的巨大成本。如果一个新的工作队列的工作项不能与现有的系统工作队列的工作项共存而且不会产生不可接受的影响,那么这个工作队列就是合理的。例如,如果新的工作项执行阻塞操作,将其他系统工作队列处理延迟到不可接受的程度。
实现
定义一个工作队列
工作队列使用k_work_q
类型的变量定义。通过定义线程使用的堆栈区域,然后调用k_work_q_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_q_start(&my_work_q, my_stack_area,
K_THREAD_STACK_SIZEOF(my_stack_area), MY_PRIORITY);
提交工作项
工作项使用k_work
类型的变量定义。然后必须通过调用k_work_init()
来初始化它。
初始化后的工作项可以通过调用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) */
...
提交一个延迟工作项
延迟工作项是使用k_delayed_work
类型的变量定义的。然后必须通过调用k_delayed_work_init()
来初始化它。
初始化的延迟工作项可以通过调用k_delayed_work_submit()
提交到系统工作队列,也可以通过调用k_delayed_work_submit_to_queue()
提交到指定的工作队列。已提交但尚未被其工作队列使用的延迟工作项可以通过调用k_delayed_work_cancel()
来取消。
推荐用法
使用系统工作队列将与中断相关的复杂处理从ISR延迟到协作线程。这允许在不影响系统响应后续中断的情况下迅速完成与中断相关的处理,并且不需要应用程序定义额外的线程来进行处理。
配置选项
涉及到的配置选项如下:
- CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE
- CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE