RT-Thread 线程管理(学习笔记)

本文参考自[野火EmbedFire]《RT-Thread内核实现与应用开发实战——基于STM32》,仅作为个人学习笔记。更详细的内容和步骤请查看原文(可到野火资料下载中心下载)

线程基本概念

RT-Thread 的线程可认为是一系列独立线程的集合。每个线程在自己的环境中运行。在任何时刻,只有一个线程得到运行,RT- Thread 调度器决定运行哪个线程。调度器会不断启动、停止每一个线程,宏观看上去所有的线程都在同时在执行。作为线程,不需要对调度器的活动有所了解,在线程切入切出时保存上下文环境(寄存器值、堆栈内容)是调度器主要的职责。为了实现这点,每个 RT- Thread 线程都需要有自己的堆栈。当线程切出时,它的执行环境会被保存在该线程的堆栈中,这样当线程再次运行时,就能从堆栈中正确的恢复上次的运行环境。

RT-Thread 的线程模块可以给用户提供多个线程,实现了线程之间的切换和通信,帮助用户管理业务程序流程。这样用户可以将更多的精力投入到业务功能的实现中。

——原文

线程调度器基本概念

RT-Thread中提供的线程调度器是基于优先级的全抢占式调度:在系统中除了中断处理函数、调度器上锁部分的代码和禁止中断的代码是不可抢占的之外,系统的其他部分都是可以抢占的,包括线程调度器自身。系统总共支持256个优先级0 ~ 255,数值越小的优先级越高,0为最高优先级,255分配给空闲线程使用,一般用户不使用。在一些资源比较紧张的系统中,可以根据实际情况选择只支持8个或32个优先级的系统配置)。在系统中,当有比当前线程优先级更高的线程就绪时,当前线程将立刻被换出,高优先级线程抢占处理器运行。

——RT-Thread官方中文手册

线程状态的概念

线程运行的过程中,一个时间内只允许一个线程在处理器中运行,从运行的过程上划分,线程有多种不同的运行状态,如运行态,非运行态等。在RT-Thread实时操作系统中,线程包含五种状态,操作系统会自动根据它运行的情况而动态调整它的状态。 RT-Thread中的五种线程状态如下所示:

——RT-Thread官方中文手册

状态 描述
RT_THREAD_INIT 线程初始状态。当线程刚开始创建还没开始运行时就处于这个状态;在这个状态下,线程不参与调度。
RT_THREAD_SUSPEND 挂起态、阻塞态。线程此时被挂起:它可能因为资源不可用而 挂起等待;或线程主动延时一段时间而被挂起。在这个状态下 ,线程不参与调度。
RT_THREAD_READY 就绪态。线程正在运行;或当前线程运行完让出处理器后,操作系统寻找最高优先级的就绪态线程运行。
RT_THREAD_RUNNING 运行态。线程当前正在运行,在单核系统中,只有rt_thread_self()函数返回的线程处于这个状态;在多核系统中则不受这个限制。
RT_THREAD_CLOSE 线程结束态。当线程运行结束时将处于这个状态。这个状态的线程不参与线程的调度。

线程状态迁移

RT-Thread提供了一系列的操作系统调用接口,使得线程可以在五个运行状态之间来回切换。下图为几种状态间切换的示意图:

图片来源:RT-Thread官方中文手册

在这里插入图片描述

线程通过调用函数rt_thread_create/init进入到初始状态(RT_THREAD_INIT);再通过调用函数rt_thread_startup进入到就绪状态(RT_THREAD_READY);当处于就绪状态的线程调用rt_thread_delayrt_sem_takert_mb_recv等函数或由于获取不到资源时,将进入到挂起状态(RT_THREAD_SUSPEND);处于挂起状态的线程,如果等待超时依然未能获得资源或由于其他线程释放了资源,那么它将返回到就绪状态。挂起状态的线程,如果调用rt_thread_delete/detach将更改为关闭状态(RT_THREAD_CLOSE);而运行状态的线程,如果运行结束会在线程最后部分执行rt_thread_exit函数而更改为关闭状态(RT_THREAD_CLOSE)。

——RT-Thread官方中文手册

常用线程函数

下面是一些常用的线程操作函数。
线程挂起函数rt_thread_suspend()

当线程调用rt_thread_delay,调用线程将主动挂起,当调用rt_sem_take,rt_mb_recv等函数时,资源不可使用也将导致调用线程挂起。处于挂起状态的线程,如果其等待的资源超时(超过其设定的等待时间),那么该线程将不再等待这些资源,并返回到就绪状态;或者,当其它线程释放掉该线程所等待的资源时,该线程也会返回到就绪状态。

——RT-Thread官方中文手册

注:通常不应该使用这个函数来挂起线程本身,如果确实需要采用 rt_thread_suspend 函数挂起当前线程,需要在调用 rt_thread_suspend() 函数后立刻调用 rt_schedule() 函数进行手动的线程上下文切换。

——原文

使用示例(原文代码清单)

rt_kprintf("挂起 LED1 线程!\n");
uwRet = rt_thread_suspend(led1_thread);/* 挂起 LED1 线程 */
if (RT_EOK == uwRet)
{
    
    
	rt_kprintf("挂起 LED1 线程成功!\n");
}else
{
    
    
	rt_kprintf("挂起 LED1 线程失败!失败代码:0x%lx\n",uwRet);
}

线程恢复函数rt_thread_resume()

线程恢复就是让挂起的线程重新进入就绪状态,如果被恢复线程在所有就绪态线程中,位于最高优先级链表的第一位,那么系统将进行线程上下文的切换。

——RT-Thread官方中文手册

使用示例(原文代码清单)

rt_kprintf("恢复 LED1 线程!\n");
uwRet = rt_thread_resume(led1_thread);/* 恢复 LED1 线程! */
if (RT_EOK == uwRet)
{
    
    
	rt_kprintf("恢复 LED1 线程成功!\n");
}else
{
    
    
	rt_kprintf("恢复 LED1 线程失败!失败代码:0x%lx\n",uwRet)
}

线程相关接口总览

摘抄自《RT-Thread官方中文手册》

函数 线程安全 中断例程 功能详情
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); 安全 不可调用 调用这个函数时,系统会从动态堆内存中分配一个线程句柄(即TCB,线程控制块)以及按照参数中指定的栈大小从动态堆内存中分配相应的空间。分配出来的栈空间是按照rtconfig.h中配置的RT_ALIGN_SIZE方式对齐。
rt_err_t rt_thread_delete(rt_thread_t thread); 安全 可调用 调用该函数后,线程对象将会被移出线程队列并且从内核对象管理器中删除,线程占用的堆栈空间也会被释放,收回的空间将重新用于其他的内存分配。 实际上,用rt_thread_delete函数删除线程接口,仅仅是把相应的线程状态更改为RT_THREAD_CLOSE状态,然后放入到rt_thread_defunct队列中;而真正的删除动作(释放线程控制块和释放线程栈)需要到下一次执行idle线程时,由idle线程完成最后的线程删除动作。用rt_thread_init初始化的静态线程则不能使用此接口删除。
rt_err_t rt_thread_init(struct rt_thread* thread, const char* name, void (*entry)(void* parameter), void* parameter, void* stack_start, rt_uint32_t stack_size, rt_uint8_t priority, rt_uint32_t tick); 安全 可调用 rt_thread_init函数用来初始化静态线程对象。而线程句柄(或者说线程控制块指针),线程栈由用户提供。静态线程是指,线程控制块、线程运行栈一般都设置为全局变量,在编译时就被确定、被分配处理,内核不负责动态分配内存空间。需要注意的是,用户提供的栈首地址需做系统对齐(例如ARM上需要做4字节对齐)。
rt_err_t rt_thread_detach(rt_thread_t thread); 安全 可调用 这个函数接口是和rt_thread_delete()函数相对应的, rt_thread_delete()函数操作的对象是rt_thread_create()创建的句柄,而rt_thread_detach()函数操作的对象是使用rt_thread_init()函数初始化的线程控制块。同样,线程本身不应调用这个接口脱离线程本身。
rt_err_t rt_thread_startup(rt_thread_t thread); 安全 可调用 当调用这个函数时,将把线程的状态更改为就绪状态,并放到相应优先级队列中等待调度。如果新启动的线程优先级比当前线程优先级高,将立刻切换到这个线程。
rt_thread_t rt_thread_self(void); 安全 可调用 在程序的运行过程中,相同的一段代码可能会被多个线程执行,在执行的时候可以通过下面的函数接口获得当前执行的线程句柄。注:请不要在中断服务程序中调用此函数,因为它并不能准确获得当前的执行线程。当调度器未启动时,这个接口返回RT_NULL。
rt_err_t rt_thread_yield(void); 安全 可调用 调用该函数后,当前线程首先把自己从它所在的就绪优先级线程队列中删除,然后把自己挂到这个优先级队列链表的尾部,然后激活调度器进行线程上下文切换(如果当前优先级只有这一个线程,则这个线程继续执行,不进行上下文切换动作)。
rt_err_t rt_thread_sleep(rt_tick_t tick); rt_err_t rt_thread_delay(rt_tick_t tick); 安全 不可调用 这两个函数接口的作用相同,调用它们可以使当前线程挂起一段指定的时间,当这个时间过后,线程会被唤醒并再次进入就绪状态。这个函数接受一个参数,该参数指定了线程的休眠时间(单位是OS Tick时钟节)。
rt_err_t rt_thread_suspend(rt_thread_t thread); 安全 可调用 当线程调用rt_thread_delay,调用线程将主动挂起,当调用rt_sem_take,rt_mb_recv等函数时,资源不可使用也将导致调用线程挂起。处于挂起状态的线程,如果其等待的资源超时(超过其设定的等待时间),那么该线程将不再等待这些资源,并返回到就绪状态;或者,当其它线程释放掉该线程所等待的资源时,该线程也会返回到就绪状态。• 注:通常不应该使用这个函数来挂起线程本身,如果确实需要采用rt_thread_suspend函数挂起当前任务,需要在调用rt_thread_suspend()函数后立刻调用rt_schedule()函数进行手动的线程上下文切换。
rt_err_t rt_thread_resume(rt_thread_t thread); 安全 可调用 线程恢复就是让挂起的线程重新进入就绪状态,如果被恢复线程在所有就绪态线程,位于最高优先级链表的第一位,那么系统将进行线程上下文的切换。
rt_err_t rt_thread_control(rt_thread_t thread, rt_uint8_t cmd, void* arg); 安全 可调用 指示控制命令cmd当前支持的命令包括 RT_THREAD_CTRL_CHANGE_PRIORITY - 动态更改线程的优先级;RT_THREAD_CTRL_STARTUP - 开始运行一个线程,等同于rt_thread_startup()函数调用; RT_THREAD_CTRL_CLOSE - 关闭一个线程,等同于rt_thread_delete()函数调用。
void rt_thread_idle_init(void); 不安全 不可调用 系统运行过程中必须存在一个最终可运行的线程,可以调用该函数初始化空闲线程
void rt_thread_idle_sethook(void (*hook)(void)); 不安全 不可调用 当空闲线程运行时会自动执行设置的钩子函数,由于空闲线程具有系统的最低优先级,所以只有在空闲的时候才会执行此钩子函数。空闲线程是一个线程状态永远为就绪态的线程,因此设置的钩子函数必须保证空闲线程在任何时刻都不会处于挂起状态,例如rt_thread_delay() , rt_sem_take() 等可能会导致线程挂起的函数都不能使用。

线程设计

程序上下文
线程的5种状态构成了程序运行的上下文状态,RT-Thread中程序运行的上下文包括:

  • 中断服例程:它运行在非线程的执行环境下,中断服务程序最好保持精简短小,因为中断服务是一种高于任何线程的存在;
  • 普通线程:不应该使用死循环;
  • 空闲线程:通常这个空闲线程钩子能够完成
    一些额外的特殊功能,例如系统运行状态的指示,系统省电模式等。

真正的系统资源回收工作在idle线程(空闲线程)完成,所以。对于空闲线程钩子上挂接的程序,它应该:

  • 不会挂起的idle线程;
  • 不应该陷入死循环,需要留出部分时间用于系统处理僵尸线程的系统资源回收。

线程设计要点
在线程设计时,我们需要考虑到:

  • 上下文环境
    对于工作内容,首先需要考虑运行环境,多个工作内容是否有重叠,能否一起处理。例如键盘事件:正常

  • 线程的状态跃迁
    状态跃迁指的是线程运行状态的变化,在进行线程设计时,应该保证线程在不活跃的时候,必须让出处理器,即让它进入阻塞状态。

  • 线程运行时间长度

线程运行时间长度被定义为,在线程所关心的一种事件或多种事件触发状态下,线程由阻塞态跃迁为就绪态执行设定的工作,再从就绪态跃迁为阻塞态所需要的时间(一般还应加上这段时间内,这个线程不会被其它线程所抢占的先决条件)。线程运行时间长度将和线程的优先级设计密切相关,同时也决定着设计的系统是否能够满足预计的实时响应的指标。

例如,对于事件A对应的服务线程Ta,系统要求的实时响应指标是1ms,而Ta的最大运行时间是500us。此时,系统中还存在着以50ms为周期的另一线程Tb,它每次运行的最大时间长度是100us。在这种情况下,即使把线程Tb的优先级抬到比Ta更高的位置,对系统的实时性指标也没什么影响(因为即使在Ta的运行过程中,Tb抢占了Ta的资源,但在规定的时间内(1ms),Ta也能够完成对事件A的响应)。

——RT-Thread官方中文手册

线程管理实验

此实验参考原文对应实验代码,只包含man()函数,按键驱动及其他外设初始化相关代码下面未给出。

要想按键线程可以控制LED线程,前者的线程优先级必须高于后者。

#include "board.h"
#include "rtthread.h"


// 定义线程控制块指针
static rt_thread_t led0_thread = RT_NULL;
static rt_thread_t key_thread = RT_NULL;

/******************************************************************************
* @ 函数名  : led0_thread_entry
* @ 功  能  : LED0线程入口函数
* @ 参  数  : parameter 外部传入的参数
* @ 返回值  : 无
******************************************************************************/
static void led0_thread_entry(void *parameter)
{
    
    
	while(1)
	{
    
    
		LED0(ON);
		rt_thread_delay(500); // 500个tick(500ms)
		rt_kprintf("led0_thread 正在运行, LED0_ON\r\n");
		LED0(OFF);
		rt_thread_delay(500);
		rt_kprintf("led0_thread 正在运行, LED0_OFF\r\n");
	}
}

/******************************************************************************
* @ 函数名  : key_thread_entry
* @ 功  能  : 按键线程入口函数
* @ 参  数  : parameter 外部传入的参数
* @ 返回值  : 无
******************************************************************************/
static void key_thread_entry(void *parameter)
{
    
    
	rt_err_t uwRet = RT_EOK;
	while(1)
	{
    
    
		// KEY0 被按下
		if(Key_Scan(KEY0_GPIO_PORT, KEY0_GPIO_PIN) == KEY_ON)
		{
    
    
			rt_kprintf("挂起 led0_thread。\n");
			uwRet = rt_thread_suspend(led0_thread); // 挂起 led0_thread
			if(uwRet == RT_EOK)
			{
    
    
				rt_kprintf("挂起线程成功!\n");
			}
			else
			{
    
    
				rt_kprintf("挂起线程失败!\n");
			}
		}
		
		// WK_UP 被按下
		if(Key_Scan(WK_UP_GPIO_PORT, WK_UP_GPIO_PIN) == KEY_ON)
		{
    
    
			rt_kprintf("恢复 led0_thread。\n");
			uwRet = rt_thread_resume(led0_thread); // 恢复 led0_thread
			if(uwRet == RT_EOK)
			{
    
    
				rt_kprintf("恢复线程成功!\n");
			}
			else
			{
    
    
				rt_kprintf("恢复线程失败!\n");
			}
		}
		rt_thread_delay(20);
	}
}

int main(void)
{
    
    
	// 硬件初始化和RTT的初始化已经在component.c中的rtthread_startup()完成

	// 创建一个动态线程
	led0_thread =                                 // 线程控制块指针
	rt_thread_create("led0",                      // 线程名字
	                led0_thread_entry,            // 线程入口函数
	                RT_NULL,                      // 入口函数参数
	                255,                          // 线程栈大小
				    5,                            // 线程优先级
					10);                          // 线程时间片
	
	
	// 开启线程调度
	if(led0_thread != RT_NULL)
		rt_thread_startup(led0_thread);
	else
		return -1;
							
	// 创建一个按键线程
	key_thread =                                 // 线程控制块指针
	rt_thread_create("key",                      // 线程名字
	                key_thread_entry,            // 线程入口函数
	                RT_NULL,                     // 入口函数参数
	                255,                         // 线程栈大小
				    4,                           // 线程优先级
					10);                         // 线程时间片
	// 开启线程调度
	if(key_thread != RT_NULL)
		rt_thread_startup(key_thread);
	else
		return -1;
}

遇到个小坑,野火的按键初始化中GPIO模式设置为浮空输入,这应该是因为野火开发板有硬件下拉,但正点原子没有硬件下拉,所以要设置为下拉输入;另外,野火的按键扫描函数没加消抖。

实验现象

按键KEY0按下时,led0_thread线程被挂起,按键WK_UP按下时,led0_thread线程继续运行。(但是不知道为什么有时候短按按键会出现挂起或恢复失败的情况)

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_43772810/article/details/123663748