1. 线程概述
线程是操作系统能够进行运算调度的最小单位. 它被包含在进程之中, 是进程的实际运作单位. 一条线程指的是进程中的一个单一顺序的控制流, 一个进程可以并发多个线程, 每条线程并行执行不同的任务. 在多核或多 CPU, 或支持 Hyper-threading 的 CPU 上使用多线程程序设计的好处是显而易见的, 即提高了程序的执行吞吐率. 在单个 CPU 单核的计算机上,使用多线程技术, 也可以吧进程中负责 I/O 处理, 人机交互而常被阻塞的部分与密集计算的部分分开来执行, 编写专门的 workhorse 线程执行密集计算, 从而提高程序的执行效率.
分以下几部分概述创建, 调度和删除独立可执行线程的内核服务
- 生命周期
- 调度机制
- 自定义数据
- 系统线程
- 工作队列线程
- 配置选项
- API 参考
2. 生命周期(Lifecycle)
线程是用于应用程序处理的内核对象, 它太长或太复杂, ISR 无法执行.
2.1. 概念(Concepts)
一个应用程序可以创建任意多个线程. 每个线程由一个线程 id 引用, 该 id 在线程创建时分配.
线程有以下几个关键属性:
- 栈空间 : 线程栈所需的一段内存空间. 堆栈的大小可根据线程处理实际需要进行调整. 存在用于创建和处理内存堆栈区域的特殊宏.
- 线程控制块 : 用于线程元数据的私有内核簿记(bookkeeping). 是结构体 struct k_thread 的一个实例.
- 入口函数 : 线程启动时调用的函数. 该函数最多可接受 3 个参数值.
- 调度优先级 : 它指示内核调度程序如何分配 CPU 时间给线程.
- 线程可选项 : 允许线程在特定的环境下接受内核的特殊处理.
- 启动延时 : 指定内核在启动线程支付那个应该等待多长时间.
- 执行模式 : 可以是 管理模式 或者 用户模式. 默认情况下, 线程运行在管理模式下, 该模式下的线程可以访问特权 CPU 指令, 整个内存地址空间和外设. 用户模式下线程可访问特权中的一部分. 取决于配置选项 CONFIG_USERSPACE.
2.2. 创建线程(Thread Creation)
线程必须创建之后才能使用. 内核初始化线程控制块和堆栈部分的一端. 线程堆栈的剩余部分通常未初始化.
启动延时设置为 K_NO_WAIT 时表明内核将立即启动线程执行. 否则, 将设置一个超时时间以用于内核在超时时间到期时启动执行线程. 例如, 允许线程使用的硬件可用时启动线程.
内核允许在线程开始执行前取消延时启动. 如果线程已经启动了, 则取消请求是无效的. 已经成功取消延时启动的线程必须重新创建才能使用.
2.3. 终止线程(Thread Termination)
线程一旦启动, 将永远执行. 但是, 线程可以通过其入口函数返回来同步结束其执行. 称之为线程终止.
终止的线程负责在返回之前使用它拥有的任何共享资源(如互斥锁和动态分配的内存), 因为内核不会自动回收它们.
Note : 内核目前没有对应用程序重新创建终止线程的能力做出任何声明.
2.4. 中止线程(Thread Aborting)
线程可以通过执行 aborting 异步结束. 如果线程触发致命错误错误(如 : 引用空指针), 内核将自动中止该线程.
线程也可以被其它线程(或它自己)调用 k_thread_abort() 中止. 然而, 通常采用发信号给线程, 让线程自己结束执行.
线程终止时, 内核不会自动回收该线程锁拥有的共享资源.
Note : 内核目前没有对应用程序重新创建中止线程的能力做出任何声明.
2.5. 挂起线程(Thread Suspension)
如果线程被挂起, 它将在一段不确定的时间内暂停执行. 函数 k_thread_suspend() 用于挂起包括调用线程在内的任何线程, 对已经处于挂起的线程再次挂起时不会产生任何效果.
线程一旦挂起, 则不会被调度, 除非另一个线程调用函数 k_thread_resume() 取消挂起.
Note : 线程可以使用函数 k_sleep() 阻止其执行. 然而, 这不同于挂起线程, 因为睡眠时间到了之后线程自动变为可执行.
2.5. 线程的选项(Thread Options)
内核支持一小系列线程选项, 以允许线程在特殊情况下被特殊对待. 这些与线程相关联的选项在线程创建时就被指定了.
不需要任何线程选项的线程的线程可选项的值为 0. 如果线程需要可选项, 可通过名字指定, 使用 '|' 支持多个线程可选项.
支持以下线程可选项 :
- K_ESSENTIAL : 将线程标记为必须线程(essential thread). 如果该线程终止或中止, 则内核认为发生致命系统错误. 默认情况下, 线程不会被标记为必须线程.
- K_FP_REGS 和 K_SSE_REGS : 这两个是 X86 相关的选项, 标记线程使用 CPUs 浮点寄存器和 SSE 寄存器. 在调度这样的线程时, 内核执行额外的步骤保存和恢复这些寄存器的内容. 默认情况下,调度线程时, 内核不会保存和恢复这些寄存器的值.
- K_USER : 如果 CONFIG_USERSPACE 使能, 线程在用户模式下创建, 该标记的线程可访问特权中的一部分. 参见 User Mode. 否则, 该 Flag 什么也不做.
- K_INHERIT_PERMS : 如果 CONFIG_USERSPACE 使能, 这个线程将继承所有父线程所拥有的所有内核对象权限, 除父线程对象外.
2.6. 实现(Implementation)
2.6.1. 创建线程
线程是通过定义它自己的堆栈空间和线程控制块, 然后再调用函数 k_thread_create() 创建的. 栈空间必须使用 K_THREAD_STACK_DEFINE 定义, 以确保在内存中正确的设置.
线程的创建函数返回线程 id, 该 id 用来引用线程.
以下代码创建了立刻启动的线程.
#define MY_STACK_SIZE 500
#define MY_PRIORITY 5
extern void my_entry_point(void *, void *, void *);
K_THREAD_STACK_DEFINE(my_stack_area, MY_STACK_SIZE);
struct k_thread my_thread_data;
k_tid_t my_tid = k_thread_create(&my_thread_data, my_stack_area,
K_THREAD_STACK_SIZEOF(my_stack_area),
my_entry_point,
NULL, NULL, NULL,
MY_PRIORITY, 0, K_NO_WAIT );
除了以上创建方式, 也可以调用 K_THREAD_DEFINE 在编译时创建一个线程. 该宏自动定义堆栈空间, 控制块和线程 id 变量. 示例如下, 跟上述创建效果一样.
#define MY_STACK_SIZE 500
#define MY_PRIORITY 5
extern void my_entry_point(void *, void *, void *);
K_THREAD_DEFINE(my_tid, MY_STACK_SIZE,
my_entry_point, NULL, NULL, NULL,
MY_PRIORITY, 0, K_NO_WAIT);
2.6.1.1. 用户模式的约束(User Mode Constraints)
仅在 CONFIG_USERSPACE 使能时, 并且一个用户线程创建一个新的线程时, 该节才适用. API k_thread_create() 仍然可以使用, 但是有以下限制必须满足, 否则调用线程将被终止.
- 调用线程必须对子线程和堆栈参数都具有授予权限(permissions granted); 两者都由内核作为内核对象跟踪.
- 子线程和堆栈对象必须处于未初始化状态, 如: 它当前没有运行, 堆栈内存未使用.
- 堆栈大小参数必须等于或小于声明时的堆栈对象的边界.
- 线程可选项必须选择 K_USER, 用户线程仅能创建其它的用户线程.
- 线程可选项必须不能选择 K_ESSENTIAL, 用户线程不能作为必须线程.
- 子线程的优先级必须是一个有效的优先级, 等于或低于父优先级.
2.6.1.2. 删除权限(Dropping Permissions)
如果 CONFIG_USERSPACE 使能时, 运行在 管理模式 的线程可以使用 k_thread_user_mode_enter() API 进入用户模式. 这是一个单向的操作, 并且会复位和清零线程栈空间. 这个线程将被标记为非必要的(non-essential).
2.6.2. 终止线程
线程可以通过入口函数返回来终止自己. 示例代码如下:
void my_entry_point(int unused1, int unused2, int unused3)
{
while(1) {
...
if (<some condition>) {
return;
}
...
}
}
如果 CONFIG_USERSPACE 使能时, 中止一个线程将额外的标记线程和堆栈对象作为未初始化的, 以便可以重用他们.
使用建议
使用线程处理那些在 ISR 中不能处理的任务.
建议为每个逻辑上有差异的任务创建单独的线程, 让它们并行执行.
3. 调度机制(Scheduling)
内核是基于优先级方式进行调度, 以便于允许应用程序线程共享 CPU.
3.1. 概述
调度是决定将要执行哪个线程; 被调度器选中的线程称为当前线程(current thread).
当调度器改变当前线程 id 时, 或当前线程执行被 ISR 替代时, 内核首先会保存当前线程 CPU 寄存器值. 在线程恢复运行时, 这些寄存器的将被恢复.
3.2. 线程状态(Thread States)
如果一个线程没有什么因素阻碍其运行, 则该线程称为已就绪ready), 已就绪的线程可以被选择为当前线程.
如果一个线程有一个或多个因素阻碍其运行, 则该线程称为非就绪(unready), 并不能选择为当前线程.
以下因素会将使线程变为非就绪:
- 线程没有启动.
- 线程在等待一个内核对象完成一个操作. (如: 线程正在使用一个不可用的信号量).
- 线程正在等待超时发生.
- 线程已经被挂起.
- 线程已经被终止或中止.
3.3. 线程优先级(Thread Priorities)
线程优先级是个整数值, 可以是负数或非负数. 数字越小, 优先级越高. 例如: 线程 A 的优先级为 4, 线程 B 的优先级为 7, 则调度器认为线程 A 的优先级高于线程 B; 同样地, 如果线程 C 的优先级是 -2, 则它的优先级高于 A和 B.
调度器根据线程优先级, 将线程分为以下两类:
- 协作式线程(cooperative thread) : 线程的优先级为负数, 一旦变为当前线程, 它将一直执行下去, 直到它采取某种动作使其变为非就绪.
- 可抢占式线程(preemptible thread) : 线程的优先级为非负数, 一旦变为当前线程, 它可以在任何时刻被协作式线程或者优先级更高(或相等)的抢占式线程替代. 抢占式线程被替代后, 它依然是就绪的.
线程的初始优先级值可以在线程启动后动态的增加或减小. 因此, 通过改变线程的优先级, 抢占式线程可以变为协作式线程, 反之亦然.
内核实际上可以支持无数个优先等级. 通过配置选项CONFIG_NUM_COOP_PRIORITIES 和 CONFIG_NUM_PREEMPT_PRIORITIES 分别指定各类线程的优先级范围.
- 协作式线程(cooperative thread) : -CONFIG_NUM_COOP_PRIORITIES ~ -1
- 可抢占式线程(preemptible thread) : 0 ~ CONFIG_NUM_PREEMPT_PRIORITIES - 1
例如: 将协作式线程和可抢占式线程的优先级数分别配置为 5 和 10, 则协作式线程的优先级范围为(-5 ~ -1), 可抢占式线程的优先级范围为(0 ~ 9).
3.4. 调度算法(Scheduling Algorithm)
内核调度器总是选择优先级最高的线程作为当前线程. 当存在多个相同优先级的就绪线程时, 调度器优先选择等待时间最久的线程.
Note: ISRs 优先于线程, 因此 当前线程 可能会在任意时刻被非屏蔽的 ISR 替代. 这对 协作式线程 和 可抢占式线程 都成立.
3.5. 协作式时间片(Cooperative Time Slicing)
协作式线程一旦成为了当前线程, 它将一直执行下去, 直到它采取的某种动作导致自己变为非就绪线程. 这种方式其实有一个缺陷, 即如果协作式线程需要执行长时间的计算, 将导致包括优先级高于或等于该线程在内的其它所有线程的调度被延迟到一个不可接受的时间之后.
为了解决这个问题, 协作式线程可以自身时不时地放弃 CPU, 让其它线程得以执行. 线程放弃 CPU 的方法有以下两种方式 :
- 调用 k_yield( ) 将线程放到调度器维护的按照优先级排列的就绪线程链表中, 然后调用调度器. 在该线程被再次调度前, 所有优先级高于或等于该线程的就绪线程都将得以执行. 如果不存在优先级更高或相等的线程, 调度器将不会进行上下文切换, 立即再次调度该线程.
- 调用 k_sleep( ) 让该线程在一段指定时间内变为非就绪线程. 所有优先级的就绪线程都可能得以执行;不过, 不能保证优先级低于该睡眠线程的其它线程都能在睡眠线程再次变为就绪线程前执行完.
3.5. 可抢占式时间片(Preemptive Time Slicing)
抢占式线程成为了当前线程后, 它将一直执行下去, 直到有更高优先级的线程变为就绪线程, 或者线程自己执行了某种动作导致其变为非就绪线程. 相应地, 如果抢占式线程需要执行长时间的计算, 将导致包括优先级等于该线程在内的其它所有线程的调度被延迟到一个不可接受的时间之后.
为了解决这个问题, 可抢占式线程可以执行协作式时间片(如上面所述)或者使用调度器的时间片功能, 让优先级等于该线程的其它线程得以执行.
调度器将时间分割为一系列的时间片. 时间片的单位是系统滴答时钟. 时间片的大小是可配置的, 并且可以在程序运行期间修改.
在每个时间片结束时,调度器会检查当前线程是否是可抢占的, 如果是, 它将对该线程隐式地调用 k_yield(), 让其它同优先级的就绪线程在该线程被再次调度前得以执行; 如果没有同等优先级的就绪线程, 则当前线程继续执行.
优先级高于指定范围的线程不用实现抢占式时间片, 且不能被同优先级的其它线程抢占. 应用程序只有当处理优先级更低且对时间不敏感的线程时采用抢占式时间片.
Note : 内核的时间片算法不能保证同等优先级的所有线程占用的 CPU 时间完全相同, 因为内核无法测量线程的实际执行时间. 例如, 某个线程可能在时间片快完的时候才刚刚执行, 但是时间片到后会立即释放 CPU. 可是, 该算法将确保线程的执行时间超过单个时间片的长度后释放 CPU. 注: 释放 CPU之后, 如果没有更高优先级线程或同等优先级线程就绪, 则 CPU 不会进行上下文切换而再次执行该线程.
3.6. 调度器锁(Scheduler Locking)
如果抢占式线程希望在执行某个特殊的操作时不被抢占, 它可以调用 k_sched_lock( ), 让调度器将其临时当做协作式线程, 从而避免被抢占. 在执行关键操作时, 可以防止其他线程干扰.
一旦完成特殊操作, 该线程必须调用 k_sched_unlock( ), 以恢复其可抢占特性.
如果线程调用了 k_sched_lock( ), 但是随后执行了一个动作导致其非就绪, 调度器会将这个锁定的线程切换出去, 以允许其它线程得以执行. 当锁定的线程再次成为 当前线程 后, 其不可抢占状态依然有效.
Note: 对于可抢占线程来说, 锁定调度器比将其优先级更改为负值更有效地抑制抢占.
3.7. 元-IRQ优先级(Meta-IRQ Priorities)
当使能 CONFIG_NUM_METAIRQ_PRIORITIES 时, 则在 协作式优先级 的最高优先级之上存在一个 协作优先级的子集优先级空间: 被称为 meta-IRQ 线程. 根据它们的优先级进行调度, 但是它们有个特殊的功能, 可以抢占其它低优先级的线程(包括其它 meta-irq线程). 即使那些线程是协作式线程和加锁调度器的线程.
这种行为使得解除 Meta-IRQ 线程的行为(通过任何方式,例如, 创建它, 调用 k_sem_give( ))等同于低优先级线程执行的同步系统调用, 或者从真正中断上下文中执行类似ARM 的挂起中断(ARM-like “pended IRQ”). 其目的是将此功能用于在驱动程序子系统中实现中断"下文"处理和或“微线程(tasklet)”功能. 该线程一旦被唤醒, 将保证在当前CPU返回到应用程序代码之前运行.
与其他操作系统中的类似功能不同, meta-IRQ 线程是真正的线程, 并且运行在它自己的堆栈空间, 而不是在每个 CPU 的中断栈, 在支持的架构上启用 IRQ 堆栈的设计工作正在进行中.
注意, 因为这违背了 Zephyr API对协作线程的承诺(也就是说,除非当前线程故意阻塞,否则OS不会调度其他线程), 它应该只在应用程序代码中使用. 这些线程不是简单的高优先级线程, 不应该这样使用.
3.8. 线程睡眠(Thread Sleeping)
线程可以调用 k_sleep( ) 让其延迟一段指定的时间后再执行. 在线程睡眠的这段时间, CPU 被释放给其它线程. 指定的时间到达后, 线程将变为就绪状态, 然后才能够再次被调度.
正在睡眠的线程可以被其它线程使用 k_wakeup( ) 唤醒. 这种技术可以让其它线程给该睡眠线程发送信号, 而不需要睡眠线程定义一个内核同步对象, 如: 信号量. 唤醒一个未睡眠的线程也是允许的, 没有任何效果.
3.9. 忙等待(Busy Waiting)
线程可以调用 k_busy_wait( ) 执行一个 忙等待 操作. 所谓的忙等待, 指的是线程延迟一段指定的时间后再处理相关任务, 但是它并不会将 CPU 释放给其它就绪线程.
使用 忙等待 而不使用 线程睡眠 的典型情况是: 由于所需要的延迟太短, 调度器来不及从当前线程切换到其它线程再切换回当前线程.
建议用法
建议在 设备驱动程序 和执行 实时性任务(performance-critical) 时使用协作式线程.
使用 协作线程 实现互斥, 而不需要 内核对象(如: 互斥对象).
使用 抢占式线程 让 时间更敏感 的处理比 时间不敏感 的处理先执行.
4. 自定义数据(Custom Data)
线程的自定义数据是一个 32 位的特定于线程的值, 应用程序可以将其用于任何目的.
4.1. 概述(Concepts)
每个线程都有一个 32-bit 的自定义数据区域. 自定义数据只能由线程自己访问, 应用程序可以利用自定义数据实现任何目的. 线程自定义数据的默认值为 0.
Note : 在 ISR 中没有自定义数据, 因为 ISR 在一个单一的共享内核的中断处理上下文.
4.2. 实现(Implementation)
4.2.1. 使用自定义数据(Using Custom Data)
默认情况下, 线程 自定义数据 的功能是关闭的。可通过配置选项CONFIG_THREAD_CUSTOM_DATA 用于使能 自定义数据.
函数 k_thread_custom_data_set( ) 和 k_thread_custom_data_get( ) 分别用于写, 读线程的 自定义数据. 线程只能访问它自己的 自定义数据, 不能访问其它线程的 自定义数据.
Note : 当然,只有一个例程可以使用这种技术, 因为它独占了自定义数据.
int call_tracking_routine(void)
{
u32_t call_count;
if ( k_is_in_isr( ) ) {
/* ignore any call made by an ISR */
} else {
call_count = (u32_t)k_thread_custom_data_get( );
call_count ++;
k_thread_custom_data_set( (void *)call_count );
}
/* do rest of routine's processing */
。。。
}
4.2.2. 建议用法(Suggested Uses)
使用线程自定义数据访问指定线程信息时, 最好将自定义数据作为一个指针指向线程自己的某个数据结构体.
5. 系统线程(System Threads)
系统线程是在系统初始化时由内核自动创建的一个线程.
5.1. 概述(Concepts)
内核会创建以下的系统线程.
5.1.1. 主线程(Main thread)
主线程先执行内核初始化, 然后调用应用程序 main( ) 函数(如果存在).
默认情况下, 主线程的优先级是 0, 即优先级最高的 可抢占式线程. 如果所配置的内核不支持 可抢占式线程, 则其优先级是 -1, 即优先级最低的 协作式线程.
主线程在执行内核初始化或执行应用程序的 main( ) 函数时是 必须线程(essential thread). 这意味着,如果该线程异常终止, 则内核会认为产生了一个致命错误. 如果应用程序没有定义 main( ) 函数, 或者它执行后正常返回, 主线程也就结束了, 此时不会抛出错误.
5.1.2. 空闲线程(Idle thread)
当系统没有其它工作需要执行时, 就会执行 空闲线程. 如果可能, 会在 空闲线程 中激活板子的电源管理功能, 以达到省电的目的; 否则, 该线程简单地执行一个 “do nothing” 的循环. 空闲线程 永远不会结束. 只要系统一直在运行, 它就一直存在.
空闲线程 的优先级是系统所配置的最低优先级. 如果它是 协作式线程, 它会不断地释放 CPU, 以使应用程序线程需要运行时能顺利运行.
空闲线程 也是 必须线程, 因此如果它被异常终止了也会致命的系统错误.
内核也可能会创建额外的 系统线程, 这依赖于应用程序指定的 开发板配置选项. 例如, 使能 系统工作队列 将创建一个 系统线程, 该线程负责为提交给它的 物件(items) 提供服务(参考工作队列线程).
5.2. 实现(Implementation)
5.2.1. 写一个 main( ) 函数
内核初始化执行完成之后, 开始执行应用提供的 main( ) 函数. 内核不会向该函数传递任何参数.
下面这几行代码总结了 main( ) 函数的一般写法. 在实际应用中, 该函数可以被设计得更加复杂.
void main(void)
{
/* initialize a semaphore */
。。。
/* register an ISR that gives the semaphore */
。。。
/* monitor the semaphore forever */
while(1) {
/* wait for the semaphore to be given by the ISR */
。。。
/* do whatever processing is now needed */
。。。
}
}
5.2.2. 建议用法(Suggested Uses)
如果应用程序只需要一个线程就能完成, 不要再定义额外的线程, 直接使用主线程就可以了.
6. 工作队列线程(Workqueue Threads)
工作队列(workqueue) 是一个内核对象, 它使用专用的线程以 先入先出 方式处理 工作项(work item). 每个工作项由它所指定的函数进行处理. 工作队列通常用于 ISR 或者高优先级线程将非紧急任务移交给低优先级线程处理, 从而不会影响时间敏感的处理任务.
6.1. 概述(Concepts)
支持定义任意数量的 工作队列, 每个工作队列通过它自己的内存地址进行引用.
一个工作队列有以下关键属性:
- 队列(queue) : 已经被添加, 但还没处理的工作项.
- 线程(thread) : 处理队列中的工作项. 线程的优先级是可配的, 既可以是协作式或抢占式.
工作队列必须先初始化才能使用. 初始化时会先清空队列, 并创建一个工作队列线程.
6.2. 工作项生命周期(Work Item Lifecycle)
支持定义任意数量的 工作项(work items), 每个工作项通过它自己的内存地址进行引用.
一个工作项有以下关键属性:
- 处理函数(handler function) : 当工作项被处理时, 工作队列线程将执行该函数. 该函数接受一个参数, 参数为该工作项自身的地址.
- 挂起标志(pending flag) : 内核使用该标志表示当前工作项是否是一个工作队列中一个成员.
- 队列链接(queue link) : 内核使用该链接将其链接到工作队列中的下一个工作项.
工作项必须先初始化才能使用. 初始化时会记录该工作项的处理函数, 并将其标记为非挂起.
ISR 或线程可以将某个工作项提交到某个工作队列中. 提交工作项时, 会将其追加到工作队列的队列中去. 当工作队列的线程处理完它队列里面的所有工作项后, 该线程会移除一个挂起工作项, 并调用该工作项的处理函数. 一个挂起的工作项可能很快就会被处理, 也可能会在队列中保留一段时间, 这依赖于工作队列线程的调度优先级和队列中其它项的工作需求.
处理函数 可以利用任何线程可用的内核 API. 不过, 使用可能引起阻塞的操作(如: 获取一个信号量)时一定要当心, 因为工作队列在它的上一个处理函数完成前不能处理其队列中的其它工作项.
如果处理函数不需要参数, 可以将传入的参数直接忽略. 如果处理函数需要额外的信息, 可以将工作项内嵌到一个更大的数据结构当中. 处理函数可以使用这个参数值计算封装后的地址, 以此访问额外的信息.
一个工作项通常会被初始化一次, 然后当它需要执行时会被提交到工作队列中. 如果 ISR 或者线程尝试提交一个已经挂起的工作项, 不会有任何效果; 提交后, 工作项会停留在工作队列中的当前位置, 且只会被执行一次.
处理函数可以将工作项重新提交到工作队列中,因为此时工作项已经不再是挂起状态. 这样做的好处是, 处理函数可以分阶段执行工作, 而不会导致延迟处理工作队列中的其它工作项.
Note : 一个挂起的工作项在被工作队列线程处理前不能被改变. 这意味着, 当工作项处于挂起状态时,它不能被再次初始化. 此外, 在处理函数执行完成前, 处理函数需要的额外信息也不能被改变.
6.3. 延迟工作(Delayed Work)
ISR 或线程可能需要延迟一段指定的事时间后(而不是立即)再调度一个工作项. 通过向工作队列中提交一个延迟的工作项(delayed work item) (而不是标准工作项)就能达到此目的.
延迟工作项比标准工作项新增了如下属性 :
- 延迟时间 : 指明需要延迟多久才将工作项提交到工作队列中.
- 工作队列指示器 : 用于标识工作项所要提交到的工作队列.
延迟工作项的初始化和提交过程与标准的工作项是类似的, 只是所使用的内核 API 不同. 当发出提交请求时, 内核会初始化一个超时机制, 当指定的延迟到期时就会触发它. 当超时发生时, 内核会将延迟工作项提交到指定的工作队列中. 此后, 它会保持挂起状态, 直到以标准方式处理.
ISR 或线程可以 取消 它已经提交的延迟工作项, 但前提是该工作项的超时还未到期. 取消后, 工作项的超时将被中止并且指定的工作也不会被执行.
取消已经到期的延时迟工作项没有任何效果; 除非工作项被工作队列的线程处理并移除了, 否择它将一直保持挂起状态. 因此, 当工作项的超时到期后, 工作项总是由工作队列处理了, 并不能被取消.
6.4. 系统工作队列(System Workqueue)
内核定义了一个叫作 系统工作队列 的工作队列. 所有的应用程序或内核代码都可以使用该工作队列. 系统工作队列是可选的, 且只有当应用程序使用时才存在.
Note: 只有当无法向系统工作队列提交新的工作项时, 才去创建额外的工作队列. 因为每个新的工作队列都会占用很可观的内存. 如果新工作队列中的工作项无法与系统工作队列中已存在的工作项共存时, 可以调整新的工作队列. 例如,新的工作项执行了阻塞操作会导致其它系统工作队列被延迟到一个不可接受的程度.
6.5. 实现(Implementation)
6.5.1. 定义一个工作队列(Defining a Workqueue)
使用类型为 struct 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);
6.5.2. 提交一个工作项(Submitting a Work Item)
使用类型为 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) */
。。。
6.5.3. 提交一个延迟工作项(Submitting a Delayed Work Item)
使用类型为 struct k_delayed_work 结构体变量可以定义一个延迟工作项. 必须调用 k_delayed_work_init( ) 进行初始化.
初始化的延迟工作项可以通过调用 k_delayed_work_submit( ) 将其提交到 系统工作队列 中去. 或者调用 k_delayed_work_submit_to_queue( ) 将其提交到 指定的工作队列 中去. 可以通过调用 k_delayed_work_cancel( ) 取消一个已经被提交到工作队列但还没被处理的延迟工作项.
6.5.4.建议用法(Suggested Uses)
使用系统工作队列将复杂的中断相关处理从ISR延迟到协作线程. 这样的好处是不需要牺牲系统的性能就能响应随后的中断, 且不需要应用程序定义额外的线程进行处理.
7. 配置选项(Configuration Options)
相关的配置选项如下:
- CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE
- CONFIG_SYSTEM_WORKQUEUE_PRIORITY
- CONFIG_MAIN_THREAD_PRIORITY
- CONFIG_MAIN_STACK_SIZE
- CONFIG_IDLE_STACK_SIZE
- CONFIG_THREAD_CUSTOM_DATA
- CONFIG_NUM_COOP_PRIORITIES
- CONFIG_NUM_PREEMPT_PRIORITIES
- CONFIG_TIMESLICING
- CONFIG_TIMESLICE_SIZE
- CONFIG_TIMESLICE_PRIORITY
- CONFIG_USERSPACE
8. API 参考(API Reference)
8.1. 定义(Defines)
- K_ESSENTIAL : 系统线程, 不能中止.
- K_USER : 用户模式线程
这个线程已经从管理模式转到用户模式, 因此有额外的限制. - K_INHERIT_PERMS : 继承权限
如果 CONFIG_USERSPACE 使能时, 指示正在创建的线程应该从创建它的线程继承所有内核对象权限. 反之则没有任何影响. - k_thread_access_grant(thread, …) : 允许线程访问一系列内核对象
这是一个便捷的函数. 对于提供的线程, 允许访问其余参数, 这些参数必须是指向内核对象的指针.
线程对象必须初始化过(如: 正在运行). NULL 不应该作为参数传递.
参数:- thread: 允许访问的线程对象.
- … : 内核对象指针列表.
- K_THREAD_DEFINE(name, stack_size, entry, p1, p2, p3, prio, options, delay) :静态定义并初始化一个线程.
调度器可能会立刻执行或者延迟一段时间执行该线程.
可以使用 “extern const k_tid_t ;” 访问线程 id.
参数:- name : 线程的名称.
- stack_size : 堆栈大小(单位: 字节).
- entry : 线程入口函数.
- p1,p2,p3 : 入口函数的参数.
- prio : 线程优先级.
- options : 线程可选项(用于指定线程的分类, 如K_ESSENTIAL, K_FP_REGS …)
- delay : 调度的延迟时间(单位 : 毫秒), 或者 K_NO_WAIT(无延迟).
- Z_WORK_INITIALIZER(work_handler)
- K_WORK_INITIALIZER
- K_WORK_DEFINE(work, work_handler) : 初始化一个静态定义的工作项.
该宏常用于在首次使用前初始化一个静态定义的工作队列工作项. 如 : static K_WORK_DEFINE(, <work_handler>);
参数:- work : 工作项对象的名称.
- work_handler : 处理工作项时调用的处理函数.
8.2. 重定义(Typedefs)
-
typedef void (*k_thread_user_cb_t)(const struct k_thread *thread, void *user_data)
-
typedef k_work_handler_t : 工作项处理函数类型.
当工作项被工作队列处理时, 工作队列线程执行工作项的处理函数.
返回值:
N/A
参数:
work : 工作项的地址.
8.3. 函数(Functions)
-
k_thread_foreach(k_thread_user_cb_t user_cb, void *user_data) : 遍历系统中的所有线程.
该例程遍历系统中的所有线程, 并未每个线程调用 user_cb 函数.
Note: 必须设置 CONFIG_THREAD_MONITOR 后, 该函数才有效. 使用 irq_lock 保护 _kernel.threads 列表, 这意味着新线程的创建和现有线程的终止将被阻塞, 直到该 API 返回为止.
返回值:
N/A
参数:- user_cb: 用户回调函数指针.
- user_data: 用户数据指针.
-
k_tid_t k_thread_create(struct k_thread *new_thread, k_thread_stack_t * stack, size_t stack_size, k_thread_entry_t entry, void *p1, void *p2, void *p3, int prio, u32_t options, s32_t delay) : 创建一个线程.
就以往来看, 用户通常会使用堆栈区域的开始部分来存放 struct k_thread 结构体, 尽管堆栈溢出时, 该区域会发生损坏, 堆栈保护功能可能无法检测到该情况.
返回值:
线程的 id.
参数:- new_thread: 指向未初始化的 struct k_thread 结构体.
- stack: 堆栈指针.
- stack_size: 堆栈大小(单位: 字节).
- entry: 线程入口函数.
- p1,p2,p3: 入口函数参数.
- prio: 线程优先级.
- options: 线程可选项.
- delay: 延迟时间(单位:毫秒)
-
FUNC_NORETURN void k_thread_user_mode_enter(k_thread_entry_t entry, void *p1, void *p2, void *p3) : 将线程的特权永久的转换到用户模式.
参数:- entry: 执行的函数.
- p1,p2,p3: 函数的参数.
-
static void k_thread_resource_pool_assign(struct k_thread *thread, struct k_mem_pool *pool) :给线程分配一个内存池.
默认情况下, 不会给线程分配内存池, 除非它们的父线程有个内存池, 线程将继承该内存池. 多个线程可能被分配相同的内存池.
更改一个线程的内存池, 将不会从之前的内存池迁移.
参数:- thread: 为资源请求分配一个内存池的目标线程, 为 NULL时, 则线程不在拥有内存池.
- pool: 用于资源的内存池.
-
s32_t k_sleep(s32_t duration) : 设置当前线程进入睡眠. 时间为毫秒.
返回值:
如果请求的事件已经到期或者已经过期, 或者线程通过调用 k_wakeup 唤醒, 则返回值为 0.
参数:
duration: 睡眠持续时间, 单位: 毫秒. -
void k_busy_wait(u32_t usec_to_wait) : 使当前的线程处于忙等待状态.
这个例程将会使当前线程空转 usec_to_wait 毫秒. -
void k_yield(void) : 该例程导致当前线程将执行任务交给具有相同或更高优先级的另一个线程执行, 如果没有其它相同或更高优先级的就绪线程, 则该函数立即返回.
-
void k_wakeup(k_tid_t thread) : 唤醒一个睡眠的线程. 如果线程当前没有睡眠, 则不起任何作用.
参数:
thread: 需要唤醒的线程的 id. -
k_tid_t k_current_get(void) : 获取当前线程的 id.
返回值:
当前线程的 id. -
void k_thread_abort(k_tid_t thread) : 中止一个线程.
用于停止一个执行的线程. 这个线程将从所有的内核队列(如: 就绪队列, 超时队列, 或内核对象等待队列)中删除. 然而, 该线程所拥有的内核资源(如: 互斥锁或内存块)不会被释放. 需要调用者进行必要的清除.
参数:
thread: 被终止的线程的 id. -
void k_thread_start(k_tid_t thread) : 启动一个非活动线程.
如果一个线程使用 线程可选项 K_FOREVER 创建. 它将不会被添加到调度队列中, 直到调用该函数.
参数:
thread: 要启动的线程的 id. -
int k_thread_priority_get(k_tid_t thread) : 获取线程的优先级.
参数:
thread: 需要获取优先级的线程的 id. -
void k_thread_priority_set(k_tid_thread, int prio) : 设置线程的优先级.
可以立刻修改线程的优先级.
根据优先级的设置, 调度器立刻重新调度:- 如果它自己的优先级高于函数调用者的优先级, 并且调用者时可抢占的, 则线程将被调度.
- 如果调用者是对自己进行操作, 它将优先级降低到低于系统中其他线程的优先级, 并且调用者(线程它自己)是可抢占的, 那么最高优先级的线程将被调度.
优先级的取值范围(-CONFIG_NUM_COOP_PRIORITIES ~ CONFIG_NUM_PREEMPT_PRIORITIES-1), -CONFIG_NUM_COOP_PRIORITIES 为最高优先级.
注意: 改变当前在互斥锁优先级继承中涉及的线程的优先级可能会导致未定义的行为.
参数:
thread: 需要设置优先级的线程的 id.
prio: 新的优先级. -
void k_thread_deadline_set(k_tid_t, int deadline) : 为调度器设置死线.
deadline 设置为当前时间的增量, 单位与 使用 k_cycle_get_32( ) 一致. 调度器(当启用死线调度时)在具有相同静态优先级的线程间选择时, 将选择下一个即将到期的线程. 不同优先级的线程将依据它们自己的静态优先级进行调度.Note: 即使线程已经"完成"了它的工作, 但是死线为负数(如: 已过期)则仍被认为比其他线程优先级更高. 如果不在对它进行调度, 你不许将 deadline 重新设置为将来的某个时间, 阻塞/挂起线程, 或者通过 k_thread_priority_set( ) 函数修改它的优先级.
Note: Despite the API naming, the scheduler makes no guarantees the the thread WILL be scheduled within that deadline, nor does it take extra metadata (like e.g. the “runtime” and “period” parameters in Linux sched_setattr()) that allows the kernel to validate the scheduling for achievability. Such features could be implemented above this call, which is simply input to the priority selection logic.
参数:- thread: 设置死线的线程的 id.
- deadline: 一个时间增量, 单位为 cycle units
-
void k_thread_suspend(k_tid_t thread) : 挂起一个线程.
这个例程阻止内核调度程序将线程设置为当前线程. 线程上的所有其他内部操作仍在执行; 例如, 它所等待的任何超时都会持续滴答, 它所等待的内核对象仍然会传递给它, 等等.
如果线程已经被挂起, 操作没有任何影响. -
void k_thread_resume(k_tid_t thread) : 恢复一个挂起的线程.
这个例程允许内核调度程序在线程下一次符合该角色的条件时将线程设置为当前线程.
如果线程没有被挂起, 操作没有任何影响. -
void k_sched_time_slice_set(s32_t slice, int prio) : 设置时间片周期和范围.
指定调度程序将如何执行可抢占线程的时间片.要使用时间片, 则 slice 不能为 0. 调度程序确保在赋予具有该优先级的其他线程执行机会之前, 没有线程运行超过指定的时间限制. 优先级高于 prio 的任何线程都可以被豁免, 并且可以执行任意长度的线程, 而不会因为时间片而被抢占.
时间片只限制线程可以连续执行的最大时间量, 一旦调度器选择要执行的线程, 在存在更高或相同优先级的线程调度前, 没有最小的线程执行时间保证.
当当前线程是该优先级中唯一有资格执行的线程时, 该例程没有效果; 线程在时间片到期后立即重新被调度.通过设置 slice 和 prio 都为 0, 则可以关闭时间片.
参数:
- slice: 时间片的最大长度(单位: 毫秒)
- prio: 具有时间片资格的最高优先级.
-
void k_sched_lock(void) : 锁调度器.
这个例程通过调度器将当前线程作为一个协作线程来防止当前线程被另一个线程抢占. 如果线程随后执行了使其变为未就绪的操作. 则以正常方式切换上下文. 当线程再次成为当前线程时, 将保持其不可抢占状态.该例程可以进行递归调用.
Note: 当 ISRs 可以安全地中断正在执行的操作时, 通常应使用 k_sched_lock( ) 和 k_sched_unlock( ). 但是, 如果操作量非常小, 那么使用 irq_lock( ) 和 irq_unlock( ) 可以获得更好的性能.
-
void k_sched_unlock(void) : 解锁调度器.
这个例程逆转之前调用 k_sched_lock( ) 的效果. 线程每次调用 k_sched_lock( ) 时必须调用 k_sched_unlock( ) 一次, 然后才可以抢占该线程. -
void k_thread_custom_data_set(void *value) : 设置当前线程自定义数据.
自定义数据不是由内核本身使用的, 线程可以自由地使用它, 它可以用作构建线程本地存储的框架.
参数:
value: 新自定义数据值. -
void * k_thread_custom_data_get(void) : 获取当前线程的自定义数据.
返回值:
当前线程的自定义数据值. -
void k_thread_name_set(k_tid_t thread_id, char * value ) : 设置当前线程的名称.
设置启用 THREAD_MONITOR 进行跟踪和调试时要使用的线程的名称. -
const char * k_thread_name_get(k_tid_t thread_id) : 获取线程名字.
工作队列相关接口函数:
-
static void k_work_init(struct k_work * work, k_work_handler_t handler ) : 初始化一个工作项.
此例程在首次使用工作队列工作项之前初始化该工作队列工作项.
参数:- work: 工作项的地址.
- handler: 在工作项执行时调用的处理函数.
-
static void k_work_submit_to_queue(struct k_work_q * work_q, struct k_work * work) : 提交工作项.
这个例程提交工作项到工作队列 work_q 进行排队处理, 如果由于较早的提交, 工作项已经挂起在工作队列的队列中, 则此例程对工作项没有影响. 如果工作项已经被处理, 或者当前正在处理, 则认为其工作已经完成. 可以重新提交工作项.警告: 提交的工作项必须在工作队列处理后才能修改.
Note: 可以被 ISRs 调用.
参数:- work_q: 工作队列的地址.
- work: 工作项的地址.
-
static int k_work_submit_to_user_queue(struct k_work_q * work_q, struct k_work * work) : 提供一个工作项到用户模式的工作队列中去.
将工作项提交给以用户模式运行的工作队列. 从调用者的资源池中进行临时内存分配, 当工作线程使用 k_work 工作项时, 资源池将被释放. 工作队列线程必须具有对正在提交的 k_work 工作项的内存访问权. 调用者必须具有对 work_q 参数的队列对象授予的权限.另外, 它的工作原理与 k_work_submit_to_queue( ) 相同.
Note: 可以被 ISRs 调用.
参数:- work_q: 工作队列的地址.
- work: 工作项的地址.
返回值: - -EBUSY: 如果工作项已经在其他一些工作队列中.
- -ENOMEM: 如果资源池没有资源为线程分配.
- 0: 成功.
-
static bool k_work_pending(struct k_work * work) : 检查工作项是否被挂起.
此例程指示工作项是否在工作队列的队列中挂起.
Note: 可以被 ISRs 调用.
返回值:- true: 工作项被挂起.
- false: 工作项没被挂起.
参数:
work: 工作项的地址.
-
void k_work_q_start(struct k_work_q * work_q, k_thread_stack_t * stack, size_t stack_size, int prio) : 开启一个工作队列.
这个例程启动工作队列 work_q. 工作队列生成其工作处理线程, 该线程将永远运行.
参数:- work_q: 工作队列的地址.
- stack: 工作队列线程栈空间指针, 由 K_THREAD_STACK_DEFINE( ).
- stack_size: 工作队列线程栈的大小(单位: 字节), 它是传递给 K_THREAD_STACK_DEFINE( ) 相同的常量, 或是 K_THREAD_STACK_SIZEOF( ).
- prio: 工作队列线程的优先级.
-
void k_work_q_user_start(struct k_work_q * work_q, k_thread_stack_t * stack, size_t stack_size, int prio) : 开启用户模式下的一个工作队列.
这与 k_work_q_start( ) 的工作方式相同, 只是它可以从用户模式调用. 并且创建的工作线程将在用户模式下运行. 调用者必须对工作队列 work_q 参数的线程和队列对象都授予权限, 并且对优先级应用与k_thread_create( ) 相同的限制.
参数:- work_q: 工作队列的地址.
- stack: 工作队列线程栈空间指针, 由 K_THREAD_STACK_DEFINE( ).
- stack_size: 工作队列线程栈的大小(单位: 字节), 它是传递给 K_THREAD_STACK_DEFINE( ) 相同的常量, 或是 K_THREAD_STACK_SIZEOF( ).
- prio: 工作队列线程的优先级.
-
void k_delayed_work_init(struct k_delayed_work * work, k_work_handler_t handler) : 初始化一个延迟工作项.
此例程在首次使用工作队列延迟工作项之前初始化该工作队列.
参数:- work: 延迟工作项的地址.
- handler: 工作项处理时调用的函数.
-
int k_delayed_work_submit_to_queue(struct k_work_q * work_q, struct k_delayed_work * work, s32_t delay) : 提交一个延迟工作项.
这个例程在延迟毫秒后调度工作队列 work_q 要处理的工作项的工作. 例程为工作项启动异步倒计时, 然后返回给调用者, 只有当倒计时完成时, 工作项才实际提交到工作队列并成为挂起.提交之前提交的延迟工作项(仍然在倒数)将取消现有的提交, 并使用新的延迟重新启动倒计时. 请注意, 这种行为本质上受已有超时和工作队列的竞态条件的限制, 因此必须注意在外部同步此类重新提交.
Note: 在工作队列处理延迟的工作项之前, 不能修改它.
Note: 可以被 ISRs 调用.
参数:- work_q: 工作队列的地址.
- work: 延迟工作项的地址.
- delay: 在提交工作项之前延迟的时间(单位: 毫秒).
返回值: - 0: 工作项倒计时开始.
- -EINVAL: 工作项正在被处理或已经完成它的工作.
- -EADDRINUSE: 工作项在一个不同的工作序列中正在挂起.
-
int k_delayed_work_cancel(struct k_delayed_work * work) : 取消一个延迟工作项.
此例程取消提交延迟的工作项工作. 延迟的工作项只能在倒计时期间取消.Note: 可以被 ISRs 调用.
Note: 对未提交的 k_delayed_work 工作项(即在k_delayed_work_submit_to_queue( ) 调用返回之前)调用该函数的结果是未定义的.
参数:
work: 延迟工作项的地址.
返回值:- 0: 工作项倒计时被取消.
- -EINVAL: 工作项正在处理或者已经完成了自己的工作.
-
static void k_work_submit(struct k_work * work) : 提交一个工作项到系统工作队列.
这个例程提交要由系统工作队列处理的工作项. 如果由于较早的提交, 工作项已经挂起在工作队列的队列中, 则此例程对工作项没有影响. 如果工作项已经被处理, 或者当前正在处理, 则认为其工作已经完成, 可以重新提交工作项.
**警告: 提交给系统工作队列的工作项应该避免使用阻塞或 yield 的处理程序, 因为这可能会阻止系统工作队列及时处理其他工作项.
Note: 可以被 ISRs 调用. -
static int k_delayed_work_submit(struct k_delayed_work * work, s32_t delay) : 提交一个延迟工作项到系统工作队列.
这个例程在延迟毫秒之后调度系统工作队列要处理的工作项. 例程为工作项启动异步倒计时, 然后返回给调用者. 只有当倒计时完成时, 工作项才实际提交到工作队列并成为挂起.提交之前提交的延迟工作项(仍然在倒计时)将取消现有的提交, 并使用新的延迟重新启动倒计时. 如果工作项当前挂起在工作队列的队列上, 因为倒计时已经完成, 那么重新提交该工作项就太晚了, 并且重新提交失败而不会影响工作项. 如果工作项已经被处理, 或者当前正在处理, 则认为其工作已经完成, 可以重新提交工作项.
**警告: 提交给系统工作队列的工作项应该避免使用阻塞或 yield 的处理程序, 因为这可能会阻止系统工作队列及时处理其他工作项.
Note: 可以被 ISRs 调用.
参数:- work: 延迟工作项的地址.
- delay: 在提交工作项之前延迟的时间(单位: 毫秒).
返回值: - 0: 工作项倒计时开始.
- -EINVAL: 工作项正在被处理或已经完成它的工作.
- -EADDRINUSE: 工作项在一个不同的工作序列中正在挂起.
-
static s32_t k_delayed_work_remaining_get(struct k_delayed_work * work) : 获取延迟工作在获得调度前的剩余时间.
这个例程计算延迟工作执行前剩余的(近似的)时间. 如果延迟的工作没有等待被调度, 则返回零.
返回值:
剩余时间(单位: 毫秒).
参数:
work: 延迟工作项. -
struct k_thread : 线程结构体.
结构体在 #include <kernel.h>定义.