工作队列

工作队列(workqueue) 是一个内核对象,它使用专用的线程以先入先出方式处理工作项(work item)。每个工作项由它所指定的函数进行处理。工作队列通常用于 ISR 或者高优先级线程将非紧急的处理任务移交给低优先级线程,因此它不会影响时间敏感的处理任务。

概念

可以定义任意数量的工作队列。每个工作队列使用其内存地址进行引用。

工作队列的关键属性如下:

  • 队列:包含若干已经被添加、且还未被处理(译注:在本节后面叫做“挂起的”)工作项。
  • 线程:用于处理队列中的工作项。该线程的优先级是可配置的,既可以是协作式也可以是抢占式。

工作队列必须先初始化再使用。初始化时会清空该队列,并创建一个工作队列线程。

工作项的生命周期

可以定义任意数量的 工作项。每个工作项通过其内存地址进行引用。

工作项的关键属性如下:

  • 处理函数:当工作项被处理时,工作队列线程会执行该函数。该函数接收一个参数 —— 工作项自身的地址。
  • 挂起标志:内核使用该标志表示该工作项当前是否是一个工作队列的队列中的一个成员。
  • 队列链接:内核使用该链接将其链接到工作队列的队列中的下一个工作项。

工作项必须先初始化再使用。初始化时会记录该工作项的处理函数,并将其标记为非挂起。

ISR 或者线程可以将某个工作项 提交 到某个工作队列中。提交工作项时,会将其追加到工作队列的队列中去。当工作队列的线程处理完它队列里面的所有工作项后,该线程会移除一个挂起工作项,并调用该工作项的处理函数。一个挂起的工作项可能很快就会被处理,也可能会在队列中保留一段时间,这依赖于工作队列线程的调度优先级和队列中其它项的工作需求。

处理函数可以利用任何可用的内核 API。不过,使用可能引起阻塞的操作(例如拿取一个信号量)时一定要当心,因为工作队列在它的上一个处理函数完成前不能处理其队列中的其它工作项。

如果处理函数不需要参数,可以将接收到的参数直接忽略。如果处理函数需要额外的信息,可以将工作项内嵌到一个更大的数据结构当中。处理函数可以使用这个参数值计算封装后的地址,以此访问额外的信息。

一个工作项通常会被初始化一次,然后当它的工作需要执行的时候会被提交到工作队列中。如果 ISR 或者线程尝试提交一个已经挂起的工作项,不会有任何效果;提交后,工作项会停留在工作队列中的当前位置,且只会被执行一次。

处理函数可以将工作项重新提交到工作队列中(因为此时工作项已经不再是挂起状态)。这样做的好处是,处理函数可以分阶段执行工作,而不会导致延迟处理工作队列的队列中的其它工作项。

重要

一个挂起的工作项在被工作队列线程处理前 不能 被改变。这意味着,当工作项处于挂起状态时,它不能被再次初始化。此外,在处理函数执行完成前,处理函数需要的额外信息也不能被改变。

延迟的工作

ISR 或者线程可能需要延迟一段指定的事时间后(而不是立即)再调度一个工作项。向工作队列中提交一个 延迟的工作项 (而不是标准工作项)就能达到此目的。

延迟工作项比标准工作项新增了如下属性:

  • 延迟时间:指明需要延迟多久才将工作项提交到工作队列的队列中。
  • 工作队列指示器:用于标识需要提交到的工作队列。

延迟工作项的初始化和提交过程与标准的工作项是类似的,只是所使用的内核 API 略有区别。当发出提交请求时,内核会初始化一个超时机制,当指定的延迟达到时就会触发它。当超时发送时,内核会将延迟工作项提交到指定的工作队列中。之后,它会保持挂起状态,知道被以标准方式处理。

ISR 或者线程可以 取消 它提交的延迟工作项,但是前提是该工作项的超时计数扔在继续。取消后,超时计数将停止计数,指定的工作也不会被执行。

取消已经到期的延时工作项不会有任何效果;除非工作项被移除并被工作队列的线程处理了,否它将一直保持挂起状态。因此,当工作项的超时服务到期后,它已经被处理过了,所以不能被取消。

系统工作队列

内核定义了一个叫做 系统工作队列 的工作队列。所有的应用程序或者内核代码都可以使用该工作队列。系统工作队列是可选的,且只有当应用程序使用时才存在。

重要

只有当无法向系统工作队列提交新的工作项时,才去创建额外的工作队列。因为每个新的工作队列都会花费可观的内存占用。如果新工作队列中的工作项无法与系统工作队列中已存在的工作项共存时,可以调整新的工作队列。例如,新的工作项执行了阻塞操作导致其它系统工作队列被延迟到一个不可接受的程序。

实现

定义一个工作队列

使用类型为 struct k_work_q 的变量可以定义一个工作队列。初始化工作队列时,需要先定义一个栈区,然后调用函数 k_work_q_start()。栈区是一个数组,其大小(字节)必须等于 K_THREAD_SIZEOF 加线程的栈大小之和。定义栈区时必须使用属性 __stack,以确保它被正确地对齐。

下面的代码定义并初始化了一个工作队列。

#define MY_STACK_SIZE (K_THREAD_SIZEOF + 500)
#define MY_PRIORITY 5

char __noinit __stack my_stack_area[MY_STACK_SIZE];

struct k_work_q my_work_q;

k_work_q_start(&my_work_q, my_stack_area, MY_STACK_SIZE, MY_PRIORITY);

提交工作项

使用类型为 struct 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) */
...

提交一个延迟的工作项

使用类型为 struct 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_PRIORITY

API

  • k_work_q_start()
  • k_work_init()
  • k_work_submit()
  • k_work_submit_to_queue()
  • k_delayed_work_init()
  • k_delayed_work_submit()
  • k_delayed_work_submit_to_queue()
  • k_delayed_work_cancel()
  • k_work_pending()