,Zephyr的线程具有以下的属性:

  • 堆栈区域
  • 线程控制块
  • 一个入口点函数,在线程启动时调用。最多可以将3个参数值传递给此函数
  • 调度优先级
  • 一组线程选项
  • 执行模式,可以是主管模式,也可以是用户模式

线程生命周期

线程创建

必须先创建线程,然后才能使用它。内核初始化线程控制块以及堆栈
可以在创建线程时指定线程的启动延迟。
内核允许在线程开始执行之前取消延迟的启动。如果线程已启动,则取消请求不起作用。延迟启动已成功取消的线程必须重新生成,然后才能使用。

线程运行

一旦线程启动,它通常会永远执行。但是,线程可以通过从其入口点函数返回来同步结束其执行,这就是线程终止
终止的线程负责在返回之前释放它可能拥有的任何共享资源,内核不会自动回收资源。
在某些情况下,一个线程可能希望休眠,直到另一个线程终止。这可以通过k_thread_join()API来完成。这将阻止本线程的调度,直到超时过期目标线程自行退出目标线程中止
一旦线程终止,内核保证不会使用线程结构块。然后,可以将此类结构的内存重用于任何目的,包括生成新线程。

线程终止

线程可以通过中止异步结束其执行。如果线程触发致命错误,内核会自动中止该线程。
一个线程也可以通过调用k_thread_abort()被另一个线程或它本身中止。
但是,通常最好向线程发出正常终止自身的信号,而不是中止它。
与线程终止一样,内核不会回收中止线程拥有的资源。

线程挂起

如果线程被挂起,则可以阻止线程无限期地执行。函数k_thread_suspend()可用于挂起任何线程,包括调用线程。

挂起已挂起的线程不会产生其他效果。

挂起后,线程无法调度,直到另一个线程调用k_thread_resume()来消除挂起。

线程状态

  • 新建态: 线程被创建
  • 线程态: 线程已经就绪了,可以被调度
  • 运行态: 线程正在运行
  • 挂起态: 线程此时被挂起
  • 终止态: 线程被终止

image.png

线程堆栈

每个线程都需要自己的堆栈缓冲区。根据配置,堆栈必须满足以下约束:

  • 可能需要为内存管理结构保留额外的内存
  • 如果启用了基于防护的堆栈溢出检测,则必须在堆栈缓冲区之前立即有一个小的写保护内存管理区域才能捕获溢出。
  • 如果启用了用户空间,则必须保留一个单独的固定大小的特权提升堆栈,以用作处理系统调用的私有内核堆栈。
  • 如果启用了用户空间,则线程的堆栈缓冲区必须适当调整大小对齐,以便可以将内存保护区域编程为完全适合。

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_PRIORITIESCONFIG_NUM_PREEMPT_PRIORITIES指定每类线程的优先级数,从而产生以下可用优先级范围:

  • 协作线程:-CONFIG_NUM_COOP_PRIORITIES-1
  • 抢占式线程:0CONFIG_NUM_PREEMPT_PRIORITIES - 1

image.png
例如,配置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()函数分别用于写入和读取线程的自定义数据。一个线程只能访问它自己的自定义数据,而不能访问另一个线程的自定义数据。

  1. int call_tracking_routine(void)
  2. {
  3. uint32_t call_count;
  4. if (k_is_in_isr()) {
  5. /* ignore any call made by an ISR */
  6. } else {
  7. call_count = (uint32_t)k_thread_custom_data_get();
  8. call_count++;
  9. k_thread_custom_data_set((void *)call_count);
  10. }
  11. /* do rest of routine's processing */
  12. ...
  13. }

线程示例

  1. #define MY_STACK_SIZE 500 //堆栈大小
  2. #define MY_PRIORITY 5 //线程优先级
  3. extern void my_entry_point(void *, void *, void *); //线程运行函数
  4. K_THREAD_STACK_DEFINE(my_stack_area, MY_STACK_SIZE); //根据堆栈大小定义堆栈
  5. struct k_thread my_thread_data; //线程结构块
  6. // 创建线程
  7. k_tid_t my_tid = k_thread_create(&my_thread_data, my_stack_area,
  8. K_THREAD_STACK_SIZEOF(my_stack_area),
  9. my_entry_point,
  10. NULL, NULL, NULL,
  11. MY_PRIORITY, 0, K_NO_WAIT);

k_thread_create的函数原型:

  1. 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: 线程的启动延迟时间
  1. #define MY_STACK_SIZE 500 //定义堆栈大小
  2. #define MY_PRIORITY 5 //定义优先级
  3. extern void my_entry_point(void *, void *, void *); //线程的进入函数
  4. //创建线程
  5. K_THREAD_DEFINE(my_tid, MY_STACK_SIZE,
  6. my_entry_point, NULL, NULL, NULL,
  7. MY_PRIORITY, 0, 0);

K_THREAD_DEFINE宏的原型:

  1. 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启用这些计时器。

下面是一个示例:

  1. k_thread_runtime_stats_t rt_stats_thread;
  2. k_thread_runtime_stats_get(k_current_get(), &rt_stats_thread);
  3. 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线程的线程数量