在 RT-Thread 中,最基本的调度单位是线程,其他 RTOS 也叫任务。如果学习过或者了解过 RTOS,任务这种叫法是最为熟知的。
本篇文章来学习一下 RT-Thread 线程方面的内容。对于初学者来说,转换一下思维,建立多任务(线程)的编程思想。
引言
对于裸机编程,整个软件系统只有一个线程(任务)在执行,实现方式是通过一个大循环完成的。应用程序是一个无限循环,循环中调用各个功能模块的函数,完成相应的操作。
RTOS 是一个多任务系统,可以把总体功能划分为多个小模块,每个小模块独立运行完成某项功能,即将任务分解。这样使得复杂项目的实现变得简单了。
关于为什么要用 RTOS,或者说 RTOS 的优点,网上资料很多,在此不再赘述。
线程基础
1. 线程调度
对于一款 RTOS 来说,最核心的部分就是线程(任务)调度器。调度器的作用是根据调度算法来决定当前需要执行的线程。
RT-Thread 的线程调度器是抢占式的,基于优先级对线程进行调度。每个线程均具有一个优先级,调度器的主要工作是,从就绪线程列表中查找最高优先级线程,然后将 CPU 的使用权分配给它。
"可抢占"意味着,如果高优先级线程就绪或者满足运行条件,RT-Thread 马上将 CPU 的控制权交给高优先级线程。
2. 线程优先级
RT-Thread 线程的优先级表示线程被调度的优先程度。每个线程都具有优先级,对于重要的线程,应该赋予其高优先级,这样才能保证线程被优先调度。
RT-Thread 最大支持 256个优先级(0~255),数值越小的线程优先级越高。0 为最高优先级。最低优先级默认分配给空闲线程,用户一般不用。
可以根据实际情况配置优先级个数,对于 ARM Cortex-M 系列,普遍采用 32 个优先级(0~31)。
3. 时间片
RT-Thread 允许多个线程具有相同的优先级,相同优先级的线程之间采用时间片轮转的方式进行调度。创建线程的时候,可以配置线程的时间片参数。时间片仅对优先级相同的就绪线程有效。
时间片的作用是约束线程单次运行的时长,其单位是系统时钟节拍(OS Tick)。
4. 线程栈
RT-Thread 线程具有独立的栈,当进行线程切换时,会将当前线程的上下文存在栈中,当线程要恢复运行时,再从栈中读取上下文信息,进行恢复。
线程栈还用来存放函数中的局部变量。当一个函数调用另外一个函数时,函数中的局部变量会暂时存放在栈中。
线程栈的增长方向由芯片架构决定的:由高地址向低地址增长、由低地址向高地址增长。这两种增长方式 RT-Thread 均支持。对于 ARM Cortex-M 架构,线程栈构造如下图所示:
5. 线程的入口函数
入口函数是线程实现预期功能的函数。线程的入口函数由用户设计,一般有以下两种形式:
- 无线循环模式
这种线程会一直被系统循环调度,执行任务,不会删除。
void thread_entry(void *parameter) { while(1) { /* 线程处理 */ } }
- 顺序执行或有限次循环模式
这类线程不会循环或者不会永久循环,执行完毕之后,线程将被系统自动删除。
void thread_entry(void *parameter) { /* 处理任务 */ ... }
线程状态
对于单 CPU 来说,系统运行过程中,同一时刻只有一个线程在处理器运行。在运行过程中,线程有多种不同的运行状态:
- 初始状态,线程刚创建还未开始运行时处于的状态,此状态下,线程不参与调度。
- 就绪状态,线程具备运行条件的状态,等待被调度器调度执行。
- 运行状态,线程正在运行。
- 挂起状态,也称为阻塞状态。由于资源不可用或线程主动延时一段时间,而进入的状态。线程不能执行。
- 关闭状态。线程运行结束处于的状态。此时线程不参与调度。
下图是各个状态之间的转换图,通过此图可以对 RT-Thread 线程的运行状态有一个整体的认识。
图中涉及到的系统调用函数,在后面的学习会进行详细讲解。此处进行简单的说明:
- rt_thread_create/init() 创建或初始化一个线程,此线程处于初始状态。
- rt_thread_startup() 函数使得初始化状态的线程进入到就绪状态。
- rt_thread_delay(),rt_sem_take(), rt_mutex_take() 等函数使得运行状态的线程进入到挂起状态。
- rt_thread_resume(), rt_sem_release() 等函数使得挂起状态的线程返回到就绪状态。
- rt_thread_delete/detach() 函数将挂起状态的线程更改为关闭状态。
- rt_thread_exit(),处于运行状态的线程,运行结束,在线程的最后部分调用此函数,将状态更改为关闭状态。
线程控制块
在 RT-Thread 中,线程控制块由结构体 struct rt_thread 表示。
线程控制块是操作系统用于管理线程的一个数据结构,它会存放线程的一些信息,例如优先级、线程名称、线程状态等。
线程控制块也包含线程与线程之间连接用的链表结构,线程等待事件集合等。
详细定义如下,列出了关键结构成员,并加入了注释,了解即可:
struct rt_thread { /* rt 对象 */ char name[RT_NAME_MAX]; /* 线程名称 */ rt_uint8_t type; /* 对象类型 */ rt_uint8_t flags; /* 标志位 */ rt_list_t list; /* 对象列表 */ rt_list_t tlist; /* 线程列表 */ /* 栈指针与入口指针 */ void *sp; /* 栈指针 */ void *entry; /* 线程入口函数指针 */ void *parameter; /* 参数 */ void *stack_addr; /* 栈地址指针 */ rt_uint32_t stack_size; /* 栈大小 */ /* 错误代码 */ rt_err_t error; /* 线程错误代码 */ rt_uint8_t stat; /* 线程状态 */ .... /* 优先级 */ rt_uint8_t current_priority; /* 当前优先级 */ rt_uint8_t init_priority; /* 初始优先级 */ rt_uint32_t number_mask; ...... rt_ubase_t init_tick; /* 线程初始化计数值 */ rt_ubase_t remaining_tick; /* 线程剩余计数值 */ struct rt_timer thread_timer; /* 内置线程定时器 */ void (*cleanup)(struct rt_thread *tid); /* 线程退出清楚函数指针 */ ... rt_uint32_t user_data; /* 用户数据 */ }; typedef struct rt_thread *rt_thread_t;
创建线程
RT-Thread 提供了先管理相关的系统函数:包含:创建 / 初始化线程、启动线程、运行线程、删除 / 脱离线程 等。
此处只讲解如何创建一个线程。
在 RT-Thread 中,要创建一个线程,并使得它能够被执行,需要两步:
- 创建线程,此时一个新线程被创建,并处于初始状态。
- 启动线程,此时线程由初始状态进入就绪状态,可以被调度器执行。
1. 创建线程
创建一个线程的函数 rt_thread_create() 定义如下:
rt_thread_t rt_thread_create(const char *name, void (*entry)(void *parameter), void *parameter, rt_uint32_t stack_size, rt_uint8_t priority, rt_uint32_t tick)
调用这个函数时,系统会从动态内存堆中分配一个线程句柄(线程控制块),并按照参数中指定的栈大小从动态内存堆中分配相应的空间。
函数的各个参数解释如下:
参数 | 描述 |
name | 线程的名称;线程名称的最大长度由 rtconfig.h 中的宏 RT_NAME_MAX 指定,多余部分会被自动截掉 |
entry | 线程入口函数 |
parameter | 线程入口函数参数 |
stack_size | 线程栈大小,单位是字节 |
priority | 线程的优先级。优先级范围根据系统配置(rtconfig.h 中的RT_THREAD_PRIORITY_MAX 宏定义) |
tick | 线程的时间片大小。单位是操作系统的时钟节拍 |
线程创建成功,返回线程的控制块指针,也可称为线程句柄。
创建失败,则返回 RT_NULL。
2. 线程启动
线程创建完成后,需要使其进入就绪状态,也就是启动线程。可以通过调用 rt_thread_startup() 函数来完成。其函数原型为:
rt_err_t rt_thread_startup(rt_thread_t thread)
调用此函数成功后,会将线程放到相应优先级队列中等待调度。如果新启动的线程优先级比当前优先级高,将立即切换到这个线程。
参数 thread ,线程句柄,即线程控制块的指针。
线程启动成功,返回 RT_EOK;启动失败,则返回 -RT_ERROR。
线程创建示例
此处用一个简单的例子,来演示一下 RT-Thread 线程创建。程序源码如下:
#include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <rtthread.h> #define THREAD_PRIORITY 25 #define THREAD_STACK_SIZE 512 #define THREAD_TIMESLICE 5 void thread_entry(void *parameter) { rt_uint32_t count = 0; while(1) { /* 线程运行,打印计数 */ rt_kprintf("thread run: %d\n", count ++); rt_thread_mdelay(500); } } int main(void) { rt_thread_t tid = RT_NULL; /* 创建线程, 名称是 thread_test, 入口是 thread_entry*/ tid = rt_thread_create("thread_test", thread_entry, RT_NULL, THREAD_STACK_SIZE, THREAD_PRIORITY, THREAD_TIMESLICE); /* 线程创建成功,则启动线程 */ if (tid != RT_NULL) { rt_thread_startup(tid); } return 0; }
编译运行,结果如下,线程创建成功,执行过程中,在打印计数信息:
OK,今天先到这,下次继续。加油~