Zephyr系列文章,翻译自 https://developer.nordicsemi.com/nRF_Connect_SDK/doc/latest/zephyr

人肉翻译,英文水平有限,仅供参考。

本文目录:Zephyr API reference->kernel services ->Threads

https://developer.nordicsemi.com/nRF_Connect_SDK/doc/latest/zephyr/reference/kernel/threads/index.html


Thread 线程

此章节解释了内核服务的创建,调度,和删除独立的可执行指令线程的内核服务。

线程是处理在ISR中处理太长、太复杂的应用程序的内核对象。

应用程序可以定义任意数量的线程。各线程通过创建线程时候产生的线程ID来引用。

线程有以下的关键特性:

  • 栈区域,它是线程栈使用的内存区域。栈区域的大小可以根据实际线程处理的需求而调整。特殊的宏定义用来穿件和使用栈内存区域。
  • 线程控制块,用于线程元数据的私有内核数据记录。它是类型为k_thread的实例。
  • 入口指针函数,线程启动的时候被引用。可以有3个参数传递给这个函数。
  • 调度优先级,它指示了内核的调度,怎么分配CPU的时间给线程。(参考调度章节。)
  • 一些列线程选项,允许线程在特殊情况下接受内核特定的处理。(参考线程选项章节。)
  • 启动延时,指定了内核应该等待多久再让线程开始。
  • 执行模式,可以是管理员模式或者用户模式。默认的,线程运行在管理员模式,并允许访问特殊权限的CPU、整个内存地址空间和外设。用户模式的线程权限减少了。这依赖于CONFIG_USERSPACE选项。(查看用户模式章节。)

Lifecycle生命周期

Thread Creation线程创建:

Thread使用之前必须先创建,内核初始化thread控制模块和stack部分的一端。剩余的线程的stack部分通常未被初始化。

k_thread_create是线程的初始化函数,其中参数使用 K_NO_WAIT 代表内核将立刻启动thread。

内核也可以延时启动线程。内核也允许延时启动的线程在其开始执行之前取消。   如果线程已经启动了,将无法取消它。已经被取消的延时启动的线程,必须在其使用之前重载(re-spawned)。

 

Thread Termination 线程终止:

一旦线程开始了,通常来说它将一直执行下去。但是现成也会同步结束:当程序在它的入口函数中执行了返回。这通被称为终止。

一个线程的结束前,有责任释放它所获得享有的资源,内核并没有自动回收它们。

 

Thread Aborting  线程废弃:

一个线程可以通过使用aborting来异步结束。内核会自动废弃一个线程,当这个线程触发了致命的错误,例如引用了空指针。

一个线程同样可以被其他线程(或它自己)废弃,当调用k_thread_abort()时。但是,通常更好的做法是让线程终止自己,而不是废弃它。

与终止线程一样,废弃线程时内核不会自动回收被共享的资源。

 

Thread Suspension 线程挂起

当被挂起时,一个线程会从无限期执行的周期中被停止。函数k_thread_suspend()可以用来挂起很多线程,包括调用线程。挂起一个已经被挂起的线程,没有附加效果。

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


Thread States线程状态

一个线程如果没有因素阻止它执行,它就处于就绪(ready)状态,并且它有资格被选做当前的线程。

一个线程如果有1个或者多个因素阻碍了它的执行,它就处于非就绪(unready)状态,并且它没有资格被选做当前的线程。

下面这些因素可以使线程处于非就绪状态:

  • 线程还没有被启动。
  • 线程正在等待内核对象完成一个操作(例如等待获得一个还不可用的信号量)。
  • 线程正在等待timeout的发生。
  • 线程被挂起了。
  • 线程被终止或者被废弃了


Thread Stack objects线程堆栈对象

每个线程都需要自己的栈空间用于CPU存放内容。根据配置决定,有一些限制必须要满足:

  • 可能需要为内存管理结构保留更多的附加RAM。
  • 如果栈溢出检测保护使能了,一个小的写保护内存管理区必须在栈缓存捕获溢出之前立即执行。
  • 如果用户区域使能了,一个分离的固定大小的有特殊权限的栈必须被保留用来作为内核的私有栈,以处理系统调用。
  • 如果用户区域使能了,线程的栈缓存必须调整大小和自对齐,这样一个内存保护区域可以被非常精确的编程。

 

对齐限制可能会非常严格,例如一些MPU需要他们的区域大小是2的幂,并且与他们自己的大小保持一致。

正因如此,一直代码的时候不可以传递一个随意字符缓冲给k_thread_create()。存在特殊的实体化栈的宏定义,前缀是K_KERNEL_STACK和K_THREAD_STACK。

 

Kernel-only Stacks

如果知道一个线程永远不会运行用户代码,或者栈被用于特殊的内容例如处理中断,最好使用K_KERNEL_STACK宏定义来定义栈。

这些栈节省内存是因为一个MPU区域永远不会被编程用于去覆盖栈缓存本身,并且内核不会需要去为特殊权限栈或者仅与用户模式线程有关的内存管理数据结构保留额外的空间。

尝试从用户模式使用以这种方式声明的栈,将对调用者产生致命的错误。

如果CONFIG_USERSPACE 没有被使能,K_THREAD_STACK的设置与K_KERNEL_STACK的设置具有相同的效果。

 

Thread stacks 线程栈:

如果知道栈将需要处理用户的线程,或如果这将不能被确定,使用K_THREAD_STACK宏定义来定义栈。这将使用更多的内存,但栈对象处理用户线程将更合适。

如果CONFIG_USERSPAE没有被使能,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) to -1
  • 抢占线程:0 to (CONFIG_NUM_PREEMPT_PRIORITIES – 1)

例如,设置5个合作优先级和10个抢占优先级,得到的范围是-5到-1和0到9。


Thread Options 线程操作

内核支持一个小的线程选项集,允许线程在特定的环境下,接收一些特定的操作。一个线程的这些选项集是被指定的,当线程被关联的时候。

线程不需要任何的线程选项时,选项值为0。线程需要特定的线程选项时通过名字来指定,如果需要多种选项,用‘|’符号作为分隔符。

支持下面的线程:

  • K_ESSENNTAL

这个选项标志了线程是基础线程。它表明了内核将线程的终止或废弃看作是致命的系统错误。默认的,线程不被考虑作为基础线程。

  • K_SSE_REGS

这是x86特性的选项,表明线程使用了 CPU的SSSE寄存器。默认的,线程调度的时候,内核不打算保存或者回复这个寄存器的内容。

  • K_FP_REGS

这个选项表明了线程使用了CPU的浮点寄存器。这说明内核会在现成调度的时候增加额外的操作来保存和恢复这些寄存器的内容。

  • K_USER

如果CONFIG_USERSPACE 被使能了,线程将被创建为用户模式,并且会减少权限。请参考用户模式。否则这个标志将不起作用。

  • K_INHERIT_PERMS

如果CONFIG_USERSPACE 被使能了,这个线程将继承父线程所有内核对象的权限,除了父线程对象外。请参考用户模式。


Thread Custom Data 线程自定义数据

每个线程都有一个32bit的自定义数据区域,只能该线程自身访问,并且可以让应用程序以任何目的使用。线程的默认自定义数据值为0。

自定义数据并不可以在ISRs中使用,因为它们操作上下文在一个共享的内核中断里。

默认的,线程自定义数据是关闭的。配置设置CONFIG_THREAD_CUSTOM_DATA可以用来使能它。

k_thread_custom_data_set() 和 k_thread_custom_data_get() 函数可以用来读写线程的自定义数据。一个线程仅能访问自己的自定义数据,不能访问别的线程的。

下面的代码用自定义数据特性记录每个线程调用特定例程的次数。

显然的,只有1个例程可以使用这个技术,因为它垄断了自定义数据特性的使用。

       通过使用自定义数据作为指向线程拥有的数据结构的指针,可以使例程访问线程的特定信息。


Implementation 实现

Spawning a Thread 生成线程:

一个线程的创建通过定义它的栈空间和它的线程控制块,然后调用k_thread_create()。

栈区域必须使用K_THREAD_STACK_DEFINE或者K_KERNEL_STACK_DEFINE来保证它在内存中正确的设立。

栈的size参数必须是下面3种之一:

  • 原始请求的栈大小,传递给K_THREAD_STACK 或 K_KERNEL_STACK 系列的栈实例化的宏定义。
  • 对于使用K_THRAD_STACK系列宏来定义栈对象的,该对象的K_THREAD_STAK_SIZEOF()的返回值。
  • 对于使用K_KERNEL_STACK系列宏定义来定义栈对象的,该对象的K_KERNEL_STACK_SIZEOF()的返回值。

线程的生成函数返回线程的ID,ID可以用来引用线程。

下面的代码生成了一个线程,并且立即启动它。

       另外,线程可以在编译的时候通过调用K_THREAD_DEFINE()被声明。观察该宏定义会自动定义栈区域空间、控制块、和线程ID变量。

下面的代码跟上面的代码具有一样的功能。

        K_thread_create()的延时参数是k_timeout_t类型的值,所以K_NO_WAIT的意思是立即启动线程。相应的K_THREAD_DEFINE()中的参数,是毫秒为单位的,所以相应的参数是0。

 

User Mode Constraints 用户模式限制:

本节内容仅适用于CONFIG_USERSPACE使能了,并且用户线程尝试创建一个新线程的情况。k_thread_create() API继续被使用,但是有一些附加的限制必须满足,否则调用线程将被终止:

  • 调用线程必须在子线程和栈参数上都授予权限;两者都将作为内核对象被内核跟踪。
  • 子线程和栈对象必须是处于未初始化状态,即当前非运行并且未使用的栈内存。
  • 传入的栈大小参数,必须等于或小于声明时候的栈对象的边界。
  • K_USER选项必须使用,用户线程只能创建其他用户线程。
  • K_ESSENTIAL选项不能使用,用户线程不会被认作基本线程。
  • 子线程的优先级必须是有限的优先级数值,必须等于或者低于其父线程。

 

Dropping Permissions 抛弃权限:

如果CONFIG_USERSPACE被使能了,线程在超级模式下可以通过使用k_thread_user_mode_enter() API 单向过度到用户模式。 这是个单向的操作会重置和清零线程的栈内存。线程将被标志为非基本线程。

 

Terminating a Thread 终止线程

一个线程终止它自己,通过从它的入口函数返回。

下面的代码展示了一个线程终止的方法。

       如果CONFIG_USERSPACE 使能了,废弃一个线程还会标记线程和栈对象为未初始化状态,这样它们能够被重新使用。

 


本节完。

发表评论