,Zephyr的线程具有以下的属性:
- 堆栈区域
- 线程控制块
- 一个入口点函数,在线程启动时调用。最多可以将3个参数值传递给此函数
- 调度优先级
- 一组线程选项
- 执行模式,可以是主管模式,也可以是用户模式
线程生命周期
线程创建
必须先创建线程,然后才能使用它。内核初始化线程控制块
以及堆栈
。
可以在创建线程时指定线程的启动延迟。
内核允许在线程开始执行之前取消延迟的启动。如果线程已启动,则取消请求不起作用。延迟启动已成功取消的线程必须重新生成,然后才能使用。
线程运行
一旦线程启动,它通常会永远执行。但是,线程可以通过从其入口点函数返回来同步结束其执行,这就是线程终止
。终止的线程
负责在返回之前释放
它可能拥有的任何共享资源,内核不会自动回收资源。
在某些情况下,一个线程可能希望休眠,直到另一个线程终止。这可以通过k_thread_join()
API来完成。这将阻止本线程的调度,直到超时过期
、目标线程自行退出
或目标线程中止
。
一旦线程终止
,内核保证不会使用线程结构块
。然后,可以将此类结构的内存重用于任何目的,包括生成新线程。
线程终止
线程可以通过中止
来异步结束
其执行。如果线程触发致命错误,内核会自动中止该线程。
一个线程也可以通过调用k_thread_abort()
被另一个线程或它本身中止。
但是,通常最好向线程发出正常终止自身的信号,而不是中止它。
与线程终止一样,内核不会回收中止线程拥有的资源。
线程挂起
如果线程被挂起
,则可以阻止线程
无限期地执行。函数k_thread_suspend()
可用于挂起任何线程,包括调用线程。
挂起已挂起的线程不会产生其他效果。
挂起后,线程无法调度,直到另一个线程调用k_thread_resume()
来消除挂起。
线程状态
- 新建态: 线程被创建
- 线程态: 线程已经就绪了,可以被调度
- 运行态: 线程正在运行
- 挂起态: 线程此时被挂起
- 终止态: 线程被终止
线程堆栈
每个线程都需要自己的堆栈缓冲区
。根据配置,堆栈必须满足以下约束:
- 可能需要为内存管理结构保留额外的内存
- 如果启用了基于防护的
堆栈溢出检测
,则必须在堆栈缓冲区
之前立即有一个小的写保护内存管理区域才能捕获溢出。 - 如果启用了
用户空间
,则必须保留一个单独的固定大小的特权提升堆栈
,以用作处理系统调用
的私有内核堆栈。 - 如果启用了
用户空间
,则线程的堆栈缓冲区
必须适当调整大小
并对齐
,以便可以将内存保护区域
编程为完全适合。
Zephyr对于堆栈的字节对齐的要求是很严格的,因此我们不能在创建任务直接提供字节数,而是通过:
- K_KERNEL_STACK: 创建主管模式线程的堆栈空间
- K_THREAD_STACK: 创建用户模式线程的堆栈空间
内核堆栈
如果已知线程永远不会在用户模式
下运行或者堆栈
用于特殊上下文如处理中断,则最好使用K_KERNEL_STACK
宏定义堆栈。
这样可以节省内存,因为永远不需要对MPU区域
进行编程以覆盖堆栈缓冲区本身,并且内核不需要为权限
提升堆栈大小或与用户模式
线程相关的内存管理数据结构
保留额外的空间。
如果使用K_KERNEL_STACK
来声明用户模式
的线程堆栈,就会导致出现致命错误。
如果未启用CONFIG_USERSPACE
,则K_THREAD_STACK
宏与K_KERNEL_STACK
宏具有相同的效果。
用户堆栈
如果已知堆栈需要用于用户线程
或者无法确定时,请使K_THREAD_STACK
宏定义堆栈。这可能会占用更多内存,但堆栈对象适用于用户线程。
如果CONFIG_USERSPACE
未启用,则K_THREAD_STACK
宏与K_KERNEL_STACK
宏具有相同的效果。
线程优先级
线程的优先级是整数值,可以是负数
或非负数
。数值较低
的优先级优先于数值较高的值。
例如,调度程序为优先级为4的线程A提供比优先级为7的线程B更高的优先级;同样,优先级为-2的线程C的优先级高于线程A和线程B。
调度程序根据每个线程的优先级区分两类线程:
协作线程
具有负优先级值
。一旦它成为当前线程,协作线程将保持当前线程,直到它执行使其未就绪的操作。抢占线程
具有非负优先级值
。一旦它成为当前线程,如果协作线程或优先级更高或相等的抢占线程准备就绪,则可以随时替换抢占线程。
线程的初始优先级值可以在线程启动后向上或向下更改。因此,抢占线程有可能成为协作线程,反之亦然,通过更改其优先级。
内核支持几乎无限数量的线程优先级。
配置选项CONFIG_NUM_COOP_PRIORITIES
,CONFIG_NUM_PREEMPT_PRIORITIES
指定每类线程的优先级数,从而产生以下可用优先级范围:
- 协作线程:
-CONFIG_NUM_COOP_PRIORITIES
到-1
- 抢占式线程:
0
到CONFIG_NUM_PREEMPT_PRIORITIES - 1
例如,配置5个协作优先级和10个抢占优先级分别导致范围为-5到-1和0到9。
meta-irq线程
其实meta-irq线程的作用主要是为了完成中断下半部的工作,如果要启用meta-irq
线程,需要配置CONFIG_NUM_METAIRQ_PRIORITIES
选项。
meta-irq线程的优先级是在优先级空间的最高优先级,它是动态设置的优先级的,它根据正常优先级进行调度,但也具有以较高优先级来抢占其他优先级的线程或协程,即使这些线程是协程或上锁的线程。但meta-irq线程始终还是线程,会被中断打断。
其实meta-irq线程这么设计的目的,主要是为了在完成中断下半部的工作之前,不会进入到应用程序空间去执行。
与其他操作系统中的类似功能不同,meta-irq线程是真正的线程,在它们自己的堆栈上运行(必须正常分配),而不是在每个CPU中断堆栈上运行。
线程选项
- K_ESSENTIAL: 此选项将线程标记为基本线程。这将指示内核将线程的终止视为致命的系统错误。
- K_SSE_REGS: 此特定于x86的选项指示线程使用CPU的SSE寄存器。
- K_FP_REGS: 此选项指示线程使用CPU的浮点寄存器。
- K_USER: 如果启用了
CONFIG_USERSPACE
,则将在用户模式下创建此线程,并且将具有降低的权限。 - K_INHERIT_PERMS: 如果启用了
CONFIG_USERSPACE
,则此线程将继承父线程具有的所有内核对象权限,但父线程对象除外。
线程自定数据
每个线程都有一个32位自定义数据区域,只能由线程本身访问,并且可以由应用程序用于它选择的任何目的。线程的默认自定义数据值为零。
自定义数据支持不适用于 ISR,因为它们在单个共享内核中断处理上下文中运行。
默认情况下,线程自定义数据支持处于禁用状态。配置选项CONFIG_THREAD_CUSTOM_DATA
可用于启用支持。k_thread_custom_data_set()
和k_thread_custom_data_get()
函数分别用于写入和读取线程的自定义数据。一个线程只能访问它自己的自定义数据,而不能访问另一个线程的自定义数据。
int call_tracking_routine(void)
{
uint32_t call_count;
if (k_is_in_isr()) {
/* ignore any call made by an ISR */
} else {
call_count = (uint32_t)k_thread_custom_data_get();
call_count++;
k_thread_custom_data_set((void *)call_count);
}
/* do rest of routine's processing */
...
}
线程示例
#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_create
的函数原型:
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, uint32_t options, k_timeout_t delay)
- new_thread: 线程指针
- stack: 线程堆栈
- stack_size: 线程堆栈大小
- entry: 线程进入函数
- p1: 线程进入函数的参数1
- p2: 线程进入函数的参数2
- p3: 线程进入函数的参数3
- prio: 线程优先级
- options: 线程选项
- delay: 线程的启动延迟时间
#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, 0);
K_THREAD_DEFINE
宏的原型:
K_THREAD_DEFINE(name, stack_size, entry, p1, p2, p3, prio, options, delay)
- name: 线程名称
- stack_size: 线程堆栈大小(字节)
- entry: 线程进入函数
- p1: 线程进入函数的参数1
- p2: 线程进入函数的参数2
- p3: 线程进入函数的参数3
- prio: 线程优先级
- options: 线程选项
- delay: 线程的启动延迟时间
线程运行时统计信息
如果启用了CONFIG_THREAD_RUNTIME_STATS
,可以收集和检索线程运行时统计信息,例如,线程的执行周期总数。
默认情况下,运行时统计信息是使用默认内核计时器
收集的。对于某些架构、SoC 或电路板,可通过定时功能提供分辨率更高的定时器。可以通过CONFIG_THREAD_RUNTIME_STATS_USE_TIMING_FUNCTIONS
启用这些计时器。
下面是一个示例:
k_thread_runtime_stats_t rt_stats_thread;
k_thread_runtime_stats_get(k_current_get(), &rt_stats_thread);
printk("Cycles: %llu\n", rt_stats_thread.execution_cycles);
线程的KCONFIG选项
选项 | 类型 | 描述 |
---|---|---|
CONFIG_MAIN_THREAD_PRIORITY | int | main线程的优先级 |
CONFIG_MAIN_STACK_SIZE | int | main线程的堆栈大小 |
CONFIG_IDLE_STACK_SIZE | int | idle线程的堆栈大小 |
CONFIG_THREAD_CUSTOM_DATA | bool | 线程自定义数据 |
CONFIG_NUM_COOP_PRIORITIES | int | 协作线程的数量 |
CONFIG_NUM_PREEMPT_PRIORITIES | int | 抢占线程的数量 |
CONFIG_TIMESLICING | bool | 线程时间片轮转 |
CONFIG_TIMESLICE_SIZE | int | 时间片大小(ms) |
CONFIG_TIMESLICE_PRIORITY | int | 时间片轮转的优先级上限,高于此值的不受时间片轮转功能的限制 |
CONFIG_USERSPACE | bool | 启用用户模式线程 |
CONFIG_THREAD_RUNTIME_STATS | bool | 线程运行时统计信息 |
CONFIG_THREAD_RUNTIME_STATS_USE_TIMING_FUNCTIONS | bool | 使用自定义计时函数进行统计信息 |
CONFIG_NUM_METAIRQ_PRIORITIES | int | meta-irq线程的线程数量 |