概念

工作队列是一个内核对象,它使用专用线程先进先出的方式处理工作项。每个工作项都通过调用工作项指定的函数进行处理。工作队列通常由ISR或高优先级线程使用,将非紧急处理发送到优先级较低的线程进行处理。
必须先初始化工作队列,然后才能使用。这会将其队列设置为空,并生成工作队列的线程。线程将永久运行,但在没有工作项可用时将进入休眠状态。

工作项

工作项生命周期

可以定义任意数量的工作项。每个工作项都由其内存地址引用。
为工作项分配了一个处理程序函数,该函数是处理工作项时由工作队列的线程执行的函数。此函数接受单个参数,该参数是工作项本身的地址。工作项还维护有关其状态的信息。
必须先初始化工作项,然后才能使用它。这将记录工作项的处理程序函数,并将其标记为未挂起。
可以通过ISR线程将工作项提交到工作队列来对工作项进行排队(K_WORK_QUEUED)。提交工作项会将工作项追加到工作队列的队列中。一旦工作队列的线程处理了其队列中的工作项,线程就会删除该工作项,然后执行下一个工作项函数。根据工作队列线程的调度优先级以及工作项函数的执行时常,排队的工作项可能会被快速处理,或者可能会在队列中保留较长时间。
当工作项在工作队列上运行时,它将处于运行状态(K_WORK_RUNNING),如果工作项在线程请求取消之前开始运行,则该工作项也可能正在取消(K_WORK_CANCELING)。
以下情况,工作项处于挂起状态(k_work_is_pending())忙碌(k_work_busy_get()):

  • 标记为取消(因为线程使用k_work_cancel_sync()等待工作项完成)
  • 排队以在同一队列上再次运行;
  • 计划提交到(可能不同)队列

处理程序函数可以使用线程可用的任何内核API。但是,必须谨慎使用可能阻塞的操作(例如,获取信号量),因为在处理程序函数完成执行之前,工作队列无法处理其队列中的后续工作项。
如果不需要传递给处理程序函数的单个参数,则可以忽略它。如果处理程序函数需要有关它要执行的工作的其他信息,则可以将工作项嵌入到更大的数据结构中。然后,处理程序函数可以使用参数值来计算包含CONTAINER_OF的数据结构的地址,从而获取对所需其他信息的访问。
工作项通常初始化一次,然后在需要执行工作时提交到特定工作队列。如果 ISR 或线程尝试提交已排队的工作项,则工作项不受影响;工作项在工作队列中的当前位置保持不变,并且工作仅执行一次。
允许处理程序函数将其工作项参数重新提交到工作队列,因为此时工作项不在排队中。这允许处理程序分阶段执行工作,而不会过度延迟工作队列中其他工作项的处理。

延迟工作项

ISR线程可能需要计划仅在指定时间段后处理的工作项。这可以通过安排在将来的时间将可延迟的工作项提交到工作队列来完成。
可延迟工作项包含标准工作项,但添加的字段记录应在何时提交该项。
可延迟工作项的初始化和调度方式与标准工作项类似,尽管使用了不同的内核 API。当发出调度请求时,内核会启动一个超时机制,该机制在指定的延迟过后触发。发生超时后,内核会将工作项提交到指定的工作队列,在该队列中,工作项将保持排队状态,直到以标准方式处理。
请注意,用于延迟的工作处理程序仍会收到一个指向基础非延迟工作结构的指针,该结构无法从k_work_delayable公开访问。要访问包含可延迟工作对象的对象,请使用以下例程:

  1. static void work_handler(struct k_work *work)
  2. {
  3. struct k_work_delayable *dwork = k_work_delayable_from_work(work);
  4. struct work_context *ctx = CONTAINER_OF(dwork, struct work_context,
  5. timed_work);
  6. ...
  7. }

触发的工作项

k_work_poll_submit()接口调度一个触发的工作项以响应轮询事件,当受监视的资源变为可用或引发轮询信号或发生超时时,该工作项将调用用户定义的函数。与k_poll()相反,触发的工作不需要等待或主动轮询轮询事件的专用线程
触发的工作项是具有以下附加属性的标准工作项:

  • 指向轮询事件数组的指针,这些事件将触发向工作队列提交工作项
  • 包含轮询事件的数组的大小。

尽管使用了专用内核API,但触发的工作项与标准工作项类似的方式初始化并提交到工作队列。发出提交请求时,内核开始观察由轮询事件指定的内核对象。一旦观察到的内核对象至少有一个更改状态,该工作项就会提交到指定的工作队列,在该队列中,工作项将保持排队状态,直到以标准方式处理它。

系统工作队列

内核定义了一个称为系统工作队列的工作队列,该工作队列可用于需要工作队列支持的任何应用程序或内核代码。系统工作队列是可选的,并且仅当应用程序使用它时才存在。

工作队列的使用

定义和控制工作队列

工作队列是使用类型k_work_q定义的。工作队列的初始化方法是定义其线程使用的堆栈区域,初始化k_work_q,将其内存清零或调用k_work_queue_init(),然后调用k_work_queue_start()。必须使用K_THREAD_STACK_DEFINE定义堆栈区域,以确保在内存中正确设置堆栈区域。

  1. #define MY_STACK_SIZE 512
  2. #define MY_PRIORITY 5
  3. K_THREAD_STACK_DEFINE(my_stack_area, MY_STACK_SIZE);
  4. struct k_work_q my_work_q;
  5. k_work_queue_init(&my_work_q);
  6. k_work_queue_start(&my_work_q, my_stack_area,
  7. K_THREAD_STACK_SIZEOF(my_stack_area), MY_PRIORITY,
  8. NULL);

以下 API 可用于与工作队列进行交互:

  • k_work_queue_drain() : 等待工作队列为空的时候才允许插入工作项,但有一种情况是可以插入工作项的,那就是在工作项回调函数里提交工作项是可以的。如果此函数的参数为true的话,那就算工作队列为空也不允许插入工作项,只有调用了k_work_queue_unplug()之后才允许插入。
  • k_work_queue_unplug(): 允许工作队列接收新的工作项提交。

提交工作项

工作项是使用k_work类型的变量定义的。它必须通过调用k_work_init()来初始化,也可以使用K_WORK_DEFINE来定义的,在这种情况下,初始化是在编译时执行的。
初始化的工作项可以通过调用k_work_submit()将工作项提交到系统工作队列,也可以通过调用k_work_submit_to_queue()将工作项提交到指定的工作队列。

  1. struct device_info {
  2. struct k_work work;
  3. char name[16]
  4. } my_device;
  5. void my_isr(void *arg)
  6. {
  7. ...
  8. if (error detected) {
  9. k_work_submit(&my_device.work);
  10. }
  11. ...
  12. }
  13. void print_error(struct k_work *item)
  14. {
  15. struct device_info *the_device =
  16. CONTAINER_OF(item, struct device_info, work);
  17. printk("Got error on device %s\n", the_device->name);
  18. }
  19. /* initialize name info for a device */
  20. strcpy(my_device.name, "FOO_dev");
  21. /* initialize work item for printing device's error messages */
  22. k_work_init(&my_device.work, print_error);
  23. /* install my_isr() as interrupt handler for the device (not shown) */
  24. ...

以下API可用于检查工作项的状态或工作项同步:

  • k_work_busy_get(): 工作项中的忙状态标志。零返回值表示工作项显示为空闲。
  • k_work_is_pending(): 测试工作项当前是否处于挂起状态。
  • k_work_flush(): 可以从要阻止的线程调用,直到工作项完成。如果工作未挂起,它将立即返回。
  • k_work_cancel(): 尝试取消工作项执行。这可能会成功,也可能不成功。从 ISR 调用这是安全的。
  • k_work_cancel_sync(): 可以从线程调用,这是阻塞操作的,直到工作取消完成;如果取消成功或工作未提交或未运行,它将立即返回。

提交延迟工作项

可延迟工作项是使用k_work_delayable类型的变量定义的。它必须通过调用k_work_init_delayable()来初始化。
对于延迟工作,有两种常见的策略,具体取决于在发生新事件时是否应延长截止日期。一个例子是收集异步传入的数据,例如,来自与键盘关联的UART中的字符。有两个 API 可在延迟后提交工作:

  • k_work_schedule()k_work_schedule_for_queue() : 将工作安排在特定时间或延迟后执行。如果通过此函数重复提交延迟工作项,是没有效果,延迟时间还是第一次提交的延迟时间。
  • k_work_reschedule()k_work_reschedule_for_queue(): 无条件地设置工作的截止日期,替换任何先前不完整的延迟,并在必要时更改目标队列。如果通过此函数重复提交延迟工作项,延迟时间以最后一次提交开始算。

可以通过k_work_delayable_from_work()API从k_work_delayable结构体里提取k_work结构体。
以下API可用于检查延迟工作项的状态或与延迟工作项同步:

  • k_work_delayable_busy_get()k_work_busy_get()是类似的,用于延迟工作项。
  • k_work_delayable_is_pending()k_work_is_pending()是类似的,用于延迟工作项。
  • k_work_flush_delayable()k_work_flush()是类似的,用于延迟工作项。
  • k_work_cancel_delayable()k_work_cancel()是类似的,用于延迟工作项。
  • k_work_cancel_delayable_sync()k_work_cancel_sync()是类似的,用于延迟工作项。

工作队列的Kconfig

Kconfig 描述
CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE 系统工作队列堆栈大小
CONFIG_SYSTEM_WORKQUEUE_PRIORITY 系统工作队列的优先级
CONFIG_SYSTEM_WORKQUEUE_NO_YIELD 选择系统工作队列是否执行线程切换