一、前言
从这一讲开始,我们进入RT-thread内核的学习,这是操作系统和裸机的区别,也是操作系统的核心所在,关于内核的基础我就不介绍了,大家可以先去官网上了解一下什么是内核。我这一讲重点讲解内核的线程管理,关于内核的其他内容我后续会接着讲。
事先说明,本人也是刚接触RT-thread不久,有些理解可能还比较显浅,如果大家发现有什么错误,请一定要指正,谢谢!!!
然后就是文中有一些概念的定义和描述我是直接在官网上面抄过来的,因为我觉得官网上有些有些概念写的非常清晰明了,我自己去描述的话可能还会带大家绕弯。
内核基础知识:https://www.rt-thread.org/document/site/programming-manual/basic/basic/#
源码链接
我发布的所有关于RT-thread的教程源代码都在下面这个链接里面,随着我教程的更新,新的代码也会加入进去。
教程源码下载链接:https://pan.baidu.com/s/1N2D8dM31deKIqNqaIQfPiA
提取码:7nsx
二、线程介绍
什么是线程
玩过单片机的同学应该都知道,裸机程序在运行完启动程序之后总是会从main函数开始执行用户的程序。
我们这里假设有这么一份代码,main函数里面有一个while循环,里面调用了两个函数,函数A和函数B,main函数以外有一个中断服务函数C。函数A是一个温湿度采集函数,函数B是LCD显示函数,显示温湿度的值,函数C是按键的中断服务函数,按键按下的时候停止温湿度的采集。其实这三个函数我们就可以把它看作是三个线程,函数A和函数B是优先级等同的两个线程,正常运行的时候,先运行函数A,再运行函数B,如此循环,如果中断触发了,优先级更高的函数C就会被优先处理,整个程序运行的流程其实就体现了单线程的线程管理和调度。
RT-thread的线程管理其实也是一样的,线程是实现任务的载体,它是 RT-Thread 中最基本的调度单位,它描述了一个任务执行的运行环境,也描述了这个任务所处的优先等级,重要的任务可设置相对较高的优先级,非重要的任务可以设置较低的优先级,不同的任务还可以设置相同的优先级,轮流运行。
线程的优点
到这里,不知道大家有没有一个疑惑,既然裸机程序函数的调度相当于线程的调度,那为什么裸机的程序没有线程这个概念呢?它们运行的时候本质都是一样的呀,为什么很多人都说操作系统的运行效率更高呢?
我的理解是这样的,裸机的函数调度的方式比较单一,就是不断循环的调用这些函数,函数的优先级也不多。而操作系统的每一个线程都可以设置优先级,我们可以根据需要把重要的任务分配更高的优先级,从而使得线程的调度不是单一的死循环调度,在多线程调度的时候,操作系统的存在使得代码的运行效率大大的提高。
举个例子,有一个函数A,里面有多个延时函数,有几ms的,也有几十ms的,那么这个函数A如果在裸机的程序里面跑的时候必须规规矩矩等待延时结束再往下跑,而如果是在RT-thread操作系统里面跑的话,当执行延时函数的时候,函数A的线程会被挂起,然后执行其他线程里面优先级最高的,等延时时间到了再返回,继续在函数A往下跑。这样一来,当代码量比较庞大,函数关系复杂的时候,操作系统的优势就体现出来了。
线程栈
RT-Thread 线程具有独立的栈,当进行线程切换时,会将当前线程的上下文存在栈中,当线程要恢复运行时,再从栈中读取上下文信息,进行恢复。
线程栈还用来存放函数中的局部变量:函数中的局部变量从线程栈空间中申请;函数中局部变量初始时从寄存器中分配(ARM 架构),当这个函数再调用另一个函数时,这些局部变量将放入栈中。
线程的类型
RT-thread的线程有两种类型,分别是系统线程和用户线程.
系统线程是由 RT-Thread 内核创建的线程
用户线程是由我们自己创建的线程,当创建的线程被启动之后就会加入任务的调度。
线程的状态
线程在运行的时候有多种状态,具体如下表所示:
线程的优先级
因为RT-Thread 的线程调度器是抢占式的,所以线程的优先级非常重要。RT-Thread在运行的时候会先从就绪线程列表中查找最高优先级的线程运行,运行完了或者需要延时的时候会让出cpu的使用权,让就绪线程列表中该线程以外的优先级最高的线程运行。
RT-Thread 最大支持 256 个线程优先级 (0~255),数值越小的优先级越高,0 为最高优先级。在一些资源比较紧张的系统中,可以根据实际情况选择只支持 8 个或 32 个优先级的系统配置;对于 ARM Cortex-M 系列,普遍采用 32 个优先级。最低优先级默认分配给空闲线程使用,用户一般不使用。在系统中,当有比当前线程优先级更高的线程就绪时,当前线程将立刻被换出,高优先级线程抢占处理器运行。
线程时间片
每个线程都有时间片这个参数,但时间片仅对优先级相同的就绪态线程有效。系统对优先级相同的就绪态线程采用时间片轮转的调度方式进行调度时,时间片起到约束线程单次运行时长的作用。
假设有 2 个优先级相同的就绪态线程 A 与 B,A 线程的时间片设置为 10,B 线程的时间片设置为 5,那么当系统中不存在比 A 优先级高的就绪态线程时,系统会在 A、B 线程间来回切换执行,并且每次对 A 线程执行 10 个节拍的时长,对 B 线程执行 5 个节拍的时长。
线程入口函数
我前面也说过了,一个函数可以当做是一个线程,在RT-Thread实际的操作中也是一样,我们每创建一个线程,就必须创建一个线程的入口函数。
线程的入口函数一般有以下两种代码形式:
无限循环模式:
使用这种模式的时候需要注意的是,该线程一旦被运行,就会导致优先级比它低的线程一直不能够被调度,因为这个线程本身是一个死循环,它能够一直在低优先级线程前面被调度。
如果我们需要死循环但是又不需要一直死循环的时候,可以把这个线程配置较低的优先级,或者在函数里面调用延时函数,从而使得该线程能够让出cpu使用权。
void thread_entry(void* paramenter)
{
while (1)
{
/* 等待事件的发生 */
/* 对事件进行服务、进行处理 */
}
}
顺序执行或有限次循环模式:
此类线程不会循环或不会永久循环,可谓是 “一次性” 线程,一定会被执行完毕。而且在执行完毕后,线程将被系统自动删除。比如下面这种简单的顺序语句或者do whlie()、for()循环等。
static void thread_entry(void* parameter)
{
/* 处理事务 #1 */
…
/* 处理事务 #2 */
…
/* 处理事务 #3 */
}
线程错误码
一个线程就是一个执行场景,错误码是与执行环境密切相关的,所以每个线程配备了一个变量用于保存错误码,线程的错误码有以下几种。
#define RT_EOK 0 /* 无错误 */
#define RT_ERROR 1 /* 普通错误 */
#define RT_ETIMEOUT 2 /* 超时错误 */
#define RT_EFULL 3 /* 资源已满 */
#define RT_EEMPTY 4 /* 无资源 */
#define RT_ENOMEM 5 /* 无内存 */
#define RT_ENOSYS 6 /* 系统不支持 */
#define RT_EBUSY 7 /* 系统忙 */
#define RT_EIO 8 /* IO 错误 */
#define RT_EINTR 9 /* 中断系统调用 */
#define RT_EINVAL 10 /* 非法参数 */
线程状态切换
RT-Thread 提供一系列的操作系统调用接口,使得线程的状态在这五个状态之间来回切换。几种状态间的转换关系如下图所示:
1:线程通过调用函数 rt_thread_create/init() 进入到初始状态(RT_THREAD_INIT)。
2:初始状态的线程通过调用函数 rt_thread_startup() 进入到就绪状态(RT_THREAD_READY);
3:就绪状态的线程被调度器调度后进入运行状态(RT_THREAD_RUNNING)。
4:当处于运行状态的线程调用 rt_thread_delay(),rt_sem_take(),rt_mutex_take(),rt_mb_recv() 等函数或者获取不到资源时,将进入到挂起状态(RT_THREAD_SUSPEND)。
5:挂起状态的线程,如果等待超时依然未能获得资源或由于其他线程释放了资源,那么它将返回到就绪状态。
6:挂起状态的线程,如果调用 rt_thread_delete/detach() 函数,将更改为关闭状态(RT_THREAD_CLOSE)。
7:运行状态的线程,如果运行结束,就会在线程的最后部分执行 rt_thread_exit() 函数,将状态更改为关闭状态。
#1:线程通过调用函数 rt_thread_create/init() 进入到初始状态(RT_THREAD_INIT)。
2:初始状态的线程通过调用函数 rt_thread_startup() 进入到就绪状态(RT_THREAD_READY);
3:就绪状态的线程被调度器调度后进入运行状态(RT_THREAD_RUNNING)。
4:当处于运行状态的线程调用 rt_thread_delay(),rt_sem_take(),rt_mutex_take(),rt_mb_recv() 等函数或者获取不到资源时,将进入到挂起状态(RT_THREAD_SUSPEND)。
5:挂起状态的线程,如果等待超时依然未能获得资源或由于其他线程释放了资源,那么它将返回到就绪状态。
6:挂起状态的线程,如果调用 rt_thread_delete/detach() 函数,将更改为关闭状态(RT_THREAD_CLOSE)。
7:运行状态的线程,如果运行结束,就会在线程的最后部分执行 rt_thread_exit() 函数,将状态更改为关闭状态。
系统线程
系统线程是指由系统创建的线程,用户线程是由用户程序调用线程管理接口创建的线程,在 RT-Thread 内核中的系统线程有空闲线程和主线程。
空闲线程
空闲线程是系统创建的最低优先级的线程,线程状态永远为就绪态。当系统中无其他就绪线程存在时,调度器将调度到空闲线程,它通常是一个死循环,且永远不能被挂起。关于空闲线程在 RT-Thread的特殊用途,我在以后的教程里面再说。
主线程
在系统启动时,系统会创建 main 线程,用户可以在 main() 函数里添加自己的应用程序初始化代码。但是在实际的项目运用中,为了方便管理每一个功能模块,我建议大家最好不要在main() 函数里面写应用的代码,原因我这里不多说了,在后续的教程中我会一一讲解。
三、编程讲解
我这里只介绍线程最常用的最基本的用法,如果你们还想学习进阶的用法,可以到官网上查阅相关的资料。
第一步: 创建/初始化线程
创建线程创建的是一个动态线程,即线程栈空间的内存是由系统自由分配的。初始化线程创建的是一个静态线程,线程栈空间的内存由用户自己去指定。
创建线程函数如下图所示:
系统线程
系统线程是指由系统创建的线程,用户线程是由用户程序调用线程管理接口创建的线程,在 RT-Thread 内核中的系统线程有空闲线程和主线程。
空闲线程:
空闲线程是系统创建的最低优先级的线程,线程状态永远为就绪态。当系统中无其他就绪线程存在时,调度器将调度到空闲线程,它通常是一个死循环,且永远不能被挂起。关于空闲线程在 RT-Thread的特殊用途,我在以后的教程里面再说。
主线程:
在系统启动时,系统会创建 main 线程,用户可以在 main() 函数里添加自己的应用程序初始化代码。但是在实际的项目运用中,为了方便管理每一个功能模块,我建议大家最好不要在main() 函数里面写应用的代码,原因我这里不多说了,在后续的教程中我会一一讲解。
三、编程讲解
我这里只介绍线程最常用的最基本的用法,如果你们还想学习进阶的用法,可以到官网上查阅相关的资料。
第一步: 创建/初始化线程
创建线程创建的是一个动态线程,即线程栈空间的内存是由系统自由分配的。初始化线程创建的是一个静态线程,线程栈空间的内存由用户自己去指定。
创建线程函数如下图所示:
编程示例:
/* 创建线程:led0 */
rt_thread_t thread1 = rt_thread_create("led0", //线程名称
led0_entry, //线程入口函数
RT_NULL, //线程入口函数参数
1024, //线程栈大小
25, //优先级
10); //时间片
初始化线程函数的定义如下图所示:
编程示例:
/* 静态线程参数定义 */
ALIGN(RT_ALIGN_SIZE)
static char led1_stack[1024]; //线程栈内存空间
static struct rt_thread led1; //线程句柄
/* 创建线程:led1 */
rt_thread_init(&led1, //线程句柄
"led1", //线程名称
led1_entry, //线程入口函数
RT_NULL, //线程入口函数参数
&led1_stack[0], //线程栈起始地址
sizeof(led1_stack), //线程栈大小
THREAD_PRIORITY - 1, //优先级
THREAD_TIMESLICE); //时间片
第二步:启动线程
编程示例:
/* 创建线程:led0 */
rt_thread_t thread1 = rt_thread_create("led0", //线程名称
led0_entry, //线程入口函数
RT_NULL, //线程入口函数参数
1024, //线程栈大小
25, //优先级
10); //时间片
/* 如果线程创建成功,启动这个线程 */
if (thread1 != RT_NULL)
{
rt_thread_startup(thread1);
}
第三步:编写线程入口函数
线程的入口函数是由用户自己定义的,我们只要保证入口函数的函数名和创建线程时所用的函数名一致即可。
编程示例:
```c
void led0_entry(void *parameter)
{
while(1)
{
rt_pin_write(LED0_PIN, PIN_LOW);
rt_thread_mdelay(1000);
rt_pin_write(LED0_PIN, PIN_HIGH);
rt_thread_mdelay(1000);
}
}
四、项目实战
我这里在main函数里面创建了一个动态和一个静态线程,分别让led0和led1闪烁。其实在实际的项目应用中,一般不会在main函数加入很多应用的代码,而是用命令的方式把函数加入应用列表,这个我在下一讲才会介绍,所以这里就先这样写吧。
代码如下:
#include <rtthread.h>
#include <rtdevice.h>
#include <board.h>
#define LED0_PIN GET_PIN(F, 9)
#define LED1_PIN GET_PIN(F, 10)
#define THREAD_PRIORITY 25 //线程优先级
#define THREAD_TIMESLICE 5 //线程时间片
/* 静态线程参数定义 */
ALIGN(RT_ALIGN_SIZE)
static char led1_stack[1024]; //线程栈内存空间
static struct rt_thread led1; //线程句柄
/* led0线程入口函数 */
void led0_entry(void *parameter)
{
rt_pin_write(LED0_PIN, PIN_LOW);
rt_thread_mdelay(1000);
rt_pin_write(LED0_PIN, PIN_HIGH);
rt_thread_mdelay(1000);
}
/* led1线程入口函数 */
void led1_entry(void *parameter)
{
rt_pin_write(LED1_PIN, PIN_LOW);
rt_thread_mdelay(300);
rt_pin_write(LED1_PIN, PIN_HIGH);
rt_thread_mdelay(300);
}
int main(void)
{
int i = 0;
/* 把LED引脚设置为输出 */
rt_pin_mode(LED0_PIN, PIN_MODE_OUTPUT);
rt_pin_mode(LED1_PIN, PIN_MODE_OUTPUT);
/* 创建线程:led0 */
rt_thread_t thread1 = rt_thread_create("led0", //线程名称
led0_entry, //线程入口函数
RT_NULL, //线程入口函数参数
1024, //线程栈大小
25, //优先级
10); //时间片
/* 如果线程创建成功,启动这个线程 */
if (thread1 != RT_NULL)
{
rt_thread_startup(thread1);
}
/* 创建线程:led1 */
rt_thread_init(&led1, //线程句柄
"led1", //线程名称
led1_entry, //线程入口函数
RT_NULL, //线程入口函数参数
&led1_stack[0], //线程栈起始地址
sizeof(led1_stack), //线程栈大小
THREAD_PRIORITY - 1, //优先级
THREAD_TIMESLICE); //时间片
rt_thread_startup(&led1);
while (1)
{
rt_thread_mdelay(100);
}
}
五、结束语
线程在RT-thread项目的实战中运用非常广泛,因此,希望大家好好看一下线程相关的内容,了解更多线程的用法。
好了,关于线程管理的编程讲解就到这里,如果还有什么问题可以私信给我。如果需要本文对应的源码的话可以在博文前言部分的链接下载。
如果觉得这篇文章对你有用,点赞+关注支持一下博主呗。
后续我会继续更新RT-thread入门教程系列,如果感兴趣的同学可以关注一下博主,谢谢!