zephyr 工作队列

摘要
    熟悉Linux的童鞋可能对工作队列比较熟悉,zephyr中的工作队列与Linux的工作队列功能类似,用于实现中断的底半部。也就是说中断ISR中比较耗时的操作,放到工作队列中去执行。zephyr中工作队列是基于线程的,简单来说,就是有一个线程一直在等待工作队列的api发来的工作项,当有工作项时(一个待执行的函数)就处理(把函数调用了),当有多个工作项时就按顺序处理,没有工作项时就休眠。

1 工作队列线程 Workqueue Threads
   工作队列是一个内核对象,用专用的线程以先进先出(first in, first out)的方式去处理被提交的工作元素(work item)。每个被处理的工作项会调用这个工作项指定的函数(通俗来讲,工作项就是一个一个等待调用的函数)。工作队列通常用于ISR或者高优先级线程把比较复杂的,非紧急的事情交给对时间不敏感的低于先级线程去处理。对Linux驱动比较了解的童鞋理解起来来叫简单,使用方式类似于Linux的工作队列和Tasklet。

    工作队列使用之前必须被初始化,并且清空他的队列创建一个工作队列线程(都是初始化函数来完成的)。

2 工作项生命周期Work Item Lifecycle
    一个工作项(或者叫工作元素,我也不知道怎么翻译好,随便叫吧 )使用之前必须被初始化,初始化就是把处理函数赋值给工作项,并且标记工作项是为挂起的,等着工作队列线程去调用。

    一个工作项可以从ISR或者线程中被提交,提交以后就是把工作项追加到工作队列内部的队列中,加入工作队列以后,当工作队列的线程会从他自己的队列中按顺序取出挂起的工作项,然后在执行这个工作项指定的函数。

   工作项中可是使用任何的内核的API,但是注意使用那些导致线程阻塞的API,比如睡眠,获取信号量之类的API,这样会导致工作队列线程睡眠,那么同时处于这个工作队列的其他工作项,也将会被延时执行,因为工作项是按顺序,一个一个被串行处理的。如果ISR或者线程重复提交一个已经在工作队列中已经挂起的工作项,那么这个工作项不会受到影响,同时在工作队列中的位置将不会受到影响。

  被提交的工作在没有被处理之前,是不能被重新初始化的。

3 延时工作队列 Delayed Work
   当一个ISR或者线程提交一个工作项,但是不想让工作项立即被执行,想让工作项等待一段时间在执行。这时可以使用延时工作项。简单理解就是,一个工作项提交的时候不是直接提交给工作队列,而是指定一个超时,当超时发生的时候,再由内核将这个工作项提交给工作队列,并保持工作项为挂起状态等着工作队列线程去处理。

4 触发工作项Triggered Work
  触发工作项是和POLL机制相关的工作项,用的比较少。POLL机制开发Linux应用的童鞋比较了解,在Linux中POLL机制是在一个线程中等待多个未就绪的文件描述有效,而zephyr是在一个线程中等待多个内核对象有效,比如等待信号量,FIFO等。

5 系统工作队列System Workqueue
   在文中要区分工作队列和工作项的区别,工作项(work item)只是一个被提交到工作队列(work queue)的一个元素。如果内核使能了工作队列的功能,内核会自动创建的一个被称为system workqueuede 的工作队列。这个系统工作队列的线程的优先级可以通过menuconfig去配置。相比于系统通过队列,用户也可以创建自己的工作队列。比如有比较复杂然后特别耗时的事情需要做,那么用户可以自己创建一个工作队列,然后把时间不敏感,不着急处理的事扔给自己创建的工作队列。但是zephyr官方不推荐自己创建工作队列,因为工作队列会消耗大量资源(RAM, flash),毕竟创建一个工作队列就会创建一个线程,一个线程就会包括何种内核对象,私有栈空间等,一般zephyr是跑在资源受限的MCU上的,所以不太推荐。

6 用户定义一个工作队列
 工作队列使用struct k_work_q类型去定义,工作队列需要使用自己定义的栈,然后调用k_work_q_start()去初始化:

#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);
7 提交一个工作项
    一个工作项在被提交之前需要调用k_work_init()去初始化,然后一个被初始化的工作项可以使用 k_work_submit()函数把工作项提交到系统工作队列,也可以使用k_work_submit_to_queue()函数提交到用户自己定义的工作队列。

   下面代码示例,一个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) */



8 提交一个延时工作项
   一个延时工作项通过struct k_delayed_work去定义,并通过 k_delayed_work_init()去初始化,通过调用k_delayed_work_submit()把延时工作项提交给系统工作队列,通过k_delayed_work_submit_to_queue()把延时工作项提交给用户自己定义的工作队列。当想取消一个超时工作项,可以使用 k_delayed_work_cancel()函数,但是需注意,取消只能在工作项指定的超时没发生之前,否则不能被取消。

9 参考链接
https://docs.zephyrproject.org/latest/reference/kernel/threads/workqueue.html

工作队列概念

工作队列的关键属性:
• 队列:包含若干已经被添加、且还未被处理(译注:在本节后面叫做“挂起的”)工作项。
• 线程:用于处理队列中的工作项。该线程的优先级是可配置的,既可以是协作式也可以是抢占式。
工作队列必须先初始化再使用。初始化时会清空该队列,并创建一个工作队列线程。

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

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

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

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

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

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

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

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

延迟工作项
一个延迟的工作项(而不是标准工作项)就能达到此目的。
延迟工作项比标准工作项新增了如下属性:
• 延迟时间:指明需要延迟多久才将工作项提交到工作队列的队列中。
• 工作队列指示器:用于标识需要提交到的工作队列。
延迟工作项的初始化和提交过程与标准的工作项是类似的,只是所使用的内核API 略有区别。当发出提交请求时,内核会初始化一个超时机制,当指定的延迟达到时就会触发它。当超时发送时,内核会将延迟工作项提交到指定的工作队列中。之后,它会保持挂起状态,直到被以标准方式处理。

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

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

工作队列的使用方法
1.使用类型为struct k_work_q的变量定义一个工作队列

2:初始化工作队列


A:初始化了一个queue
B:创建了一个线程
C: 系统工作队列是一个协作式线程在处理,

3:定义一个工作项


4:初始化一个工作项
在k_work_init主要是注册函数,标挂起标志位,


在k_work_init主要是注册函数,标挂起标志位,


5:提交工作项到工作队列中
这个主要是把工作项与工作队列相关联

系统的工作队列定义在哪里?
\kernel\system_work_q.c

系统工作队列的初始化的调用是在设备初始化的时候调用
\kernel\init.c


系统工作队列是协作式线程还是抢占式线程?
优先级小于0的线程为协作式线程, 系统工作队列的优先级为-1,所以是协作式线程

协作式线程必须主动让出CPU的使用权,可以在处理函数里面调用k_yield,k_sleep.所以在协作式线程函数work_q_main()中,调用了k_yield()


调用k_yield() 将线程放到调度器维护的按照优先级排列的就绪线程链表中,然后调用调度器。在该线程被再次调度前,所有优先级高于或等于该线程的就绪线程都将得以执行。如果不存在优先级更高或相等的线程,调度器将不会进行上下文切换,立即再次调度该线程。

调用k_sleep()让该线程在一段指定时间内变为非就绪线程。所有优先级的就绪线程都可能得以执行;不过,不能保证优先级低于该睡眠线程的其它线程都能在睡眠线程再次变为就绪线程前执行完。
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值