Zephyr RTOS -- 线程简介

Posted 搬砖-工人

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Zephyr RTOS -- 线程简介相关的知识,希望对你有一定的参考价值。

本笔记基于 Zephyr 版本 2.6.0-rc2

 

前言

本人正在学习 Zephyr,一个可移植性较强,可以兼容多种开发板及物联网设备的操作系统,如果你感兴趣,可以点此查看我的 学习笔记总述 进行了解!

 

摘要

线程 是每一个操作系统的基础,在实时操作系统中,线程 (任务) 的调度是基于优先级的。同时一个操作系统主要的功能就是对 SoC 的资源和线程进行管理,线程管理包括线程的创建,线程的调度,线程的终止,线程的挂起,线程间的通信,对 SoC 资源管理主要包括 CPU,RAM等。

因此,线程是很很很很很 重要 的,下面介绍关于线程的一些基本概念。

 

1. 线程的优先级

线程的优先级是整数值,可以为负或非负。

在 Zephyr 中,优先级的数字越小优先级越高,比如优先级为 -3 的线程高于优先级为 0 的线程,而优先级为 0 的线程高于优先级为 3 的线程。

内核几乎支持无限数量的线程优先级。配置选项,CONFIG_NUM_COOP_PRIORITIESCONFIG_NUM_PREEMPT_PRIORITIES 指定每种线程类的优先级级别数。

 

2. 线程的分类

  根据优先级的不同,线程被分为两类。优先级是负数的被称作协作线程,非负数的是抢占式线程。

  • 协作线程( cooperative thread): (-CONFIG_NUM_COOP_PRIORITIES) to -1
  • 抢占线程( preemptible thread): 0 to (CONFIG_NUM_PREEMPT_PRIORITIES - 1)

在这里插入图片描述

启动线程后,可以修改线程的优先级值,因此,通过修改优先级,抢占线程可以变为协作线程,反之亦然。

 

2.1 协作线程

协作线程的优先级是 负数。范围:(-CONFIG_NUM_COOP_PRIORITIES) to -1

一旦协作线程变为当前运行的线程,它是不可以被其他线程(协作线程和抢占线程)抢占的,除非协作线程主动放弃 CPU 或者被 ISR 抢占。主动放弃 CPU 一般是等待资源(信号量,互斥锁,邮箱等),执行睡眠(调用睡眠函数 k_sleep())和 k_yield() 等操作。总之就是主动放弃 CPU,其他线程才能得到执行。

 

2.2 抢占线程

抢占线程的优先级是 非负数。范围:0 to (CONFIG_NUM_PREEMPT_PRIORITIES - 1)

抢占线程可以被高优先级的抢占线程和协作线程抢占,当然也可以被 ISR 抢占。这里还有就是如果 Zephyr 使能了时间片功能,那么当有同优先级的线程存在,当当前线程时间片耗尽,则同优先级的线程可以执行。

 

3. 线程的状态

一般来说线程的状态分为两种:

  • 就绪态 (ready)
  • 未就绪态 (unready)

没有任何妨碍其执行的因素的线程被视为准备就绪态 (ready),并且有资格被选择为当前线程。

具有一个或多个阻止其执行的因素的线程被视为未就绪态 (unready),并且不能被选择为当前线程。

例如,下面的因素会使一个线程处于 unready 状态:

  • 线程未被启动
  • 线程正在等待内核对象完成操作 (例如:该线程正在使用不可用的信号量)
  • 线程正在等待超时发生
  • 线程已被挂起
  • 线程被终止或中止

线程的状态切换图:

在这里插入图片描述

 

4. 线程的生命周期

4.1 线程的创建

线程在被使用之前必须先被创建,内核初始化线程控制块以及堆栈部分的一端。通过定义线程的堆栈区域和线程控制块,然后调用 k_thread_create() 来生成线程。

必须使用 K_THREAD_STACK_DEFINEK_KERNEL_STACK_DEFINE 定义堆栈区域,以确保在内存中正确设置了堆栈区域。

堆栈的大小参数必须是以下三个值之一:

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

线程产生函数返回其线程 ID,该 ID 可用于引用线程。

示例: 以下代码产生一个立即启动的线程

#define MY_STACK_SIZE 512
#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, 0);

Note: k_thread_create() 的延时参数是一个 k_timeout_t 值,K_NO_WAIT 意味着立即启动线程。K_THREAD_DEFINE 的对应参数是持续时间(以整数毫秒为单位),因此等效参数为 0。

 

4.2 线程的挂起与恢复

如果线程被挂起,可以无限期地阻止其执行。可使用函数 k_thread_suspend() 挂起任何线程,包括已经调用的线程。一旦挂起,就无法调度线程,直到另一个线程调用 k_thread_resume() 取消挂起。

Note: 线程可以在指定的时间段内阻止自己执行,通过使用 k_sleep() 。 但是,这与挂起线程不同,因为达到时间限制时,休眠线程会自动执行。

挂起已被挂起的线程不会产生任何其他影响

 

4.3 线程的终止

线程一旦启动,通常将永远执行。但是,线程可以通过从其入口点函数返回来同步结束其执行。这称为终止。

终止线程负责在返回之前释放其可能拥有的任何共享资源 (例如互斥体和动态分配的内存),因为内核不会自动回收它们

线程终止后,内核不会使用线程结构(k_thread)。 然后可以将这种结构的内存重新用于任何目的,包括产生新线程。

 

4.4 线程的中止

线程可以通过 中止 异步结束其执行。如果线程触发致命错误条件 (例如,取消引用空指针),内核会自动中止线程

线程也可以通过另一个线程 (或由其自身) 调用 k_thread_abort() 被中止。但是,通常最好是发信号通知线程适当地终止自身,而不是中止该线程。

线程终止 一样,内核不会回收异常中止线程所拥有的共享资源

 

5. 自动创建的线程(系统线程)

系统线程是内核在系统初始化期间自动生成的线程。

内核产生以下系统线程:

 

5.1 Main thread

默认情况下,主线程 (Main thread) 使用抢占线程的最高优先级(一般为 0,可配置)进行创建,如果内核未配置为支持抢占线程,则主线程将使用最低配置的协作线程优先级 (即 -1) 。

主线程 (Main thread) 是一个 “必须线程 (essential)”,必须线程就是这个线程不能终止,需要一直执行,否则系统会引发错误。这个线程在 MCU 启动时,Zephyr 内核初始化以后进行创建,入口就是我们熟悉的 main 函数。

如果 main() 未定义,或者执行后返回正常,则主线程将正常终止,并且不会引发任何错误。

 

5.2 Idle thread

一般的操作系统都会包括一个空闲线程 (Idle thread),空闲线程一般是最低优先级的线程。

当所有其他线程放弃 CPU 以后,空闲线程得到执行,我们一般利用空闲线程进行 CPU 利用率统计或者低功耗处理。否则,空闲线程仅执行 “不执行任何操作(do nothing)” 循环。只要系统正在运行,空闲线程就一直存在,并且永远不会终止。空闲线程也是一个 essential线程,不能被结束。

 

5.3 其他线程

根据应用程序指定的内核和主板配置选项,还可以生成其他系统线程。

例如,当 Zephyr 使能了工作队列功能,系统启动时会自动创建一个系统线程,用于处理工作队列中的工作项。

该线程为提交给它的工作项提供服务。(请参阅 Workqueue Threads)

 

6. 延时函数

6.1 k_msleep()

函数原型:int32_t k_msleep(int32_t ms)

函数功能:使当前运行的线程休眠一段时间,单位是 ms。

调用这个函数后,当前线程放弃 CPU 变为 Waiting 状态,休眠期间不可以被调度器调度,当休眠到期后,线程变为 ready 状态,可以被调度器重新调度。

6.2 k_usleep()

函数原型:int32_t k_usleep(int32_t us)

函数功能:与 k_msleep() 类似,单位是 us。

但是这个延时是不准确的,依赖于系统 tick 的精度。

6.3 k_busy_wait()

函数原型:void k_busy_wait(uint32_t usec_to_wait)

与休眠不同,这个延时是忙等待,不放弃 CPU,延时单位是 us,当我们程序中需要非常短的延时的时候,延时时间小于任务切换时间,那么可以使用这个函数进行延时。

 

参考链接

https://docs.zephyrproject.org/latest/reference/kernel/threads/index.html

以上是关于Zephyr RTOS -- 线程简介的主要内容,如果未能解决你的问题,请参考以下文章

Zephyr RTOS -- Threads

Zephyr RTOS -- Threads

Zephyr RTOS -- West 命令及编译过程简介

Zephyr RTOS -- West 命令及编译过程简介

Zephyr RTOS -- West 命令及编译过程简介

Zephyr RTOS -- West 命令及编译过程简介