4

RT-Thread快速入门-线程管理(下)

 2 years ago
source link: https://blog.51cto.com/u_15505932/5084134
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

RT-Thread快速入门-线程管理(下)

原创

一起学嵌入式 2022-03-09 09:47:25 ©著作权

文章标签 创建线程 RT-Thread RTOS IoT C 文章分类 物联网 嵌入式 阅读数643

首发,公众号【一起学嵌入式

上一篇主要介绍了 RT-Thread 线程管理相关的理论知识:

 RT-Thread快速入门-线程管理

这篇重点介绍 RT-thread 提供的线程管理相关的接口函数,以及实战演示。

在 RT-Thread 中,创建一个线程的方式有两种:

  • 动态创建方式,线程的栈和线程控制块由系统从动态内存堆上分配。
  • 静态创建方式,线程的栈和线程控制块由用户定义分配。

1. 动态创建线程

动态创建线程,用户不需要考虑线程栈和线程控制块空间分配的问题,全部由系统自动完成分配。用户只需要关心其他关键的线程属性即可。

RT-Thread 动态创建一个线程的接口函数为 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)

该函数的详细参数在上一篇文章中做过详细的解释,在此不再赘述。

其中关键的几个参数分别是:

  • 线程入口函数指针 entry,需要用户定义一个函数,创建线程的时候,将函数名放在这个参数位置。
  • 线程栈大小 stack_size,单位是字节。根据实际情况设置这个参数,后边会分析如何确定这个值。
  • 线程优先级 priority,根据线程需要完成任务的重要性来决定优先级值,值越小,优先级越高。
  • 时间片 tick,单位为 系统时钟节拍,如果有相同优先级的线程,才会用到此参数。

动态创建线程举例:

/* 线程入口函数 */
void thread_entry(void *parameter)
{
	...
}

/* 定义线程控制块指针 */
rt_thread_t tid = RT_NULL;
/* 创建线程 */
tid = rt_thread_create("thread_test", thread_entry, 
												RT_NULL, 512, 10, 5);

首先定义一个线程控制块指针(线程句柄),然后调用 rt_thread_create() 函数创建线程。

此线程的名字为 “thread_test”;线程入口函数 thread_entry;入口函数的参数为 RT_NULL,无入口参数;线程栈的大小为 512 字节;线程优先级为 10;线程时间片为 5。

2. 静态创建线程

静态方式创建线程,需要用户考虑的东西多一点:线程控制块定义、线程栈空间申请、线程栈起始地址等。

静态创建线程分两步:

  • 用户定义线程控制块结构变量,申请线程栈内存空间。
  • 初始化线程控制块,即初始化线程。

线程控制块(线程句柄)定义可以通过如下方式完成,即定义 struct rt_thread 结构变量:

struct rt_thread  thread_static;

线程栈可以通过定义数组的方式来分配,或者通过动态内存分配的方式来完成:

/* 数组方式确定线程栈,应该定义成全局数组 */
char thread_stack[1024];

/* 动态内存申请方式,确定线程栈 */
char *thread_stack = (char *)rt_malloc(1024);

其中 rt_malloc() 函数会在后面内存管理文章做详细讲解。

线程控制块和线程栈定义完成后,需要对其进行初始化。RT-Thread 提供了线程初始化函数接口 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)

该函数的各个参数解释如下:

参数 描述

thread 线程句柄,由用户提供,指向线程控制块内存地址

name 线程名称

entry 线程入口函数

parameter 线程入口函数的参数

stack_start 线程栈起始地址

stack_size 线程栈大小,单位是字节。

priority 线程的优先级。

tick 线程的时间片大小。

函数执行成功,返回 RT_EOK;执行失败,则返回 -RT_EOK。

要注意,用户提供的栈首地址需要做系统对齐,例如 ARM 架构的 CPU 上需要做 4 字节对齐。

静态创建线程举例:

/* 线程栈起始地址做内存对齐 */
ALIGN(RT_ALIGN_SIZE)
char thread_stack[1024];
/* 定义线程控制块 */
struct rt_thread thread;

/* 线程入口函数 */
void thread_entry(void *parameter)
{
	...
}

/* 初始化线程控制块 */
rt_thread_init(&thread, "thread_test", thread_entry,
                RT_NULL, &thread_stack[0], sizeof(thread_stack),
                10, 5);

首先定义线程栈以及线程控制块,然后对线程控制块进行初始化。

线程句柄为线程控制块 thread 的地址 &thread;线程名称为 "thread_test";线程入口函数为 thread_entry;入口函数的参数为 RT_NULL;线程栈起始地址为定义的数组的起始地址;线程栈大小为数组的字节数;优先级为 10;时间片为 5。

线程关键参数确定

创建一个线程有几个关键的参数需要用户确定:

  • 线程栈大小
  • 线程优先级
  • 线程时间片

对于初学者来说,这几个参数的确定不好把握,或者说,不知道设置多大合适。

其实这些参数的确定,没有统一的标准,需要根据实际的应用,具体分析来做决定。

1. 线程栈大小的确定

在基于 RTOS 的程序设计中,每个线程(任务)都需要自己的栈空间,每个线程需要的栈,根据应用的不同,栈大小也会随之不同。

需要用到栈空间的内容如下:

  • 函数调用需要用到栈空间的项目为:函数局部变量、函数形参、函数返回地址、函数内部状态。
  • 线程切换的上下文。线程切换需要用到的寄存器需要入栈。
  • 任务执行过程,发生中断。寄存器需要入栈。

实际应用中将这些加起来,可以粗略得到栈的最小需求,但是计算很麻烦。在实际分配栈大小的时候,可以粗略计算一个值后,取其二倍,比较保险。

2. 线程优先级分配

在 RT-Thread 中,线程优先级数值越小,其优先级越高。空闲任务的优先级最低。

线程优先级的分配,没有具体的标准。一般是根据具体的应用情况来配置。

为了能够使得某项事件得到及时处理,可以将处理此事件的线程设置为较高优先级。比如,按键检测、触摸检测、串口数据处理等等。

而对于那些实时处理不是很高的线程,则可以配置较低优先级。比如,LED 闪烁、界面显示等等。

3. 线程时间片分配

具有相同优先级的线程调度,线程时间片分配的长,则该线程执行时间长。

可以根据实际应用情况,如果某个线程完成某项事务,耗时比较长,可以给其分配较大的时间片。耗时较短的线程,分配较小的时间片。

如果应用程序中,没有相同优先级的线程,则此参数不起作用。

在 RTOS 中,如果需要延时等待一会儿,千万不能用普通的延时等待(CPU 空转),应该调用 RTOS 提供的延时等待函数。如果用普通的延时,那么 RTOS 失去了实时性,浪费了 CPU 资源。

RT-Thread 提供了系统函数,用于让当前线程延迟一段时间,在指定的时间结束后,重新运行线程。线程睡眠可以使用以下三个函数:

rt_err_t rt_thread_sleep(rt_tick_t tick); /* 睡眠时间,单位为 时钟节拍 */
rt_err_t rt_thread_delay(rt_tick_t tick); /* 延时,单位为 时钟节拍 */
rt_err_t rt_thread_mdelay(rt_int32_t ms); /* 单位为 毫秒 */

这三个函数的作用相同,调用它们可以使得当前线程进入挂起状态,并持续一段指定的时间。这个时间到达后,线程会被唤醒并再次进入就绪状态。

rt_thread_sleep/delay() 的参数 tick,单位为 1 个系统时钟节拍(OS tick)。

rt_thread_mdelay() 的参数 ms,单位为 1ms。

函数的返回值为 RT_EOK。

使得线程进入休眠,即调用这三个函数中的一个,也是让出 CPU 权限的一种方式,可以让低优先级的线程能够得到执行。

如果高优先级的线程没有让出 CPU 的操作,那么低优先级的线程永远得不到 CPU 执行权限,从而引发问题出现。

因此,高优先级线程,要么等待某项系统资源不可用而进入挂起状态,要么调用这三个睡眠函数进入挂起状态,从而给低优先级线程执行的机会。

线程创建示例

此处用于演示如何使用上面介绍的线程创建函数:

#include <rtthread.h>

#define THREAD_PRIORITY 25
#define THREAD_STACK_SIZE 512
#define THREAD_TIMESLICE 5
static rt_thread_t tid1 = RT_NULL;


/* 线程1的入口函数 */
static void thread1_entry(void *parameter)
{
	rt_uint32_t count = 0;
	while (1)
	{
		/* 线程1采用低优先级运行,一直打印计数值 */
		rt_kprintf("thread1 count: %d\n", count ++);
		/* 延时 500ms */
		rt_thread_mdelay(500);
	}
}

ALIGN(RT_ALIGN_SIZE)
static char thread2_stack[1024];
static struct rt_thread thread2;

/* 线程2入口 */
static void thread2_entry(void *param)
{
	rt_uint32_t count = 0;
	/* 线程2拥有较高的优先级,以抢占线程1而获得执行 */
	for (count = 0; count < 10 ; count++)
	{
		/* 线程2打印计数值 */
		rt_kprintf("thread2 count: %d\n", count);
	}
	rt_kprintf("thread2 exit\n");
	
	/* 线程2运行结束后也将自动被系统脱离 */
}

int main()
{
	/* 创建线程1,名称是thread1,入口是thread1_entry */
	tid1 = rt_thread_create("thread1",
                thread1_entry, RT_NULL,
                THREAD_STACK_SIZE,
                THREAD_PRIORITY, THREAD_TIMESLICE);
							
	/* 如果获得线程控制块,启动这个线程 */
	if (tid1 != RT_NULL)
	{
		rt_thread_startup(tid1);
	}
	
	/* 初始化线程2,名称是thread2,入口是 thread2_entry */
	rt_thread_init(&thread2,
            "thread2",
            thread2_entry,
            RT_NULL,
            &thread2_stack[0],
            sizeof(thread2_stack),
            THREAD_PRIORITY - 1, THREAD_TIMESLICE);
	
	rt_thread_startup(&thread2);
}

这个例子用两种方式创建线程:静态方式和动态方式。一个线程在运行完毕后自动被系统删除,另一个线程一直打印计数。

编译运行,结果如下所示

RT-Thread快速入门-线程管理(下)_RTOS

系统线程是指由系统创建的线程,而用户线程是由用户程序调用线程创建函数创建的线程。RT-Thread 内核的系统线程有两个:

1. 空闲线程

空闲线程是优先级最低的线程,该线程永远处于就绪状态。当系统中没有其他就绪线程时,调度器会将 CPU

权限给空闲线程。空闲线程永远不能挂起。

RT-Thread 的空闲线程由特殊用途:

  • 空闲线程会回收被删除线程的资源。
  • 可以设置空闲线程钩子函数,在空闲线程中调用。

2. 主线程

系统启动时,会自动创建 main 线程,其入口函数为 main_thread_entry(),用户的应用程序入口函数 main() 就是从这里开始。

RT-Thread 系统启动过程,可以参考文章: RT-Thread快速入门-了解内核启动流程

系统调度器启动后,main 线程就开始运行,函数调用过程如下图所示:

RT-Thread快速入门-线程管理(下)_RT-Thread_02

用户可以在 main() 函数中添加自己的应用程序代码。

线程其他管理函数

在此列出 RT-Thread 提供的其他线程管理函数接口,初学者可以作为了解即可。如果要详细学习,可以查看官方的编程手册。

1. 删除线程

用 rt_thread_create() 创建出来的线程,当不需要使用时,可以使用下面的函数接口把它完全删除掉:

rt_err_t rt_thread_delete(rt_thread_t thread); 

函数的参数 thread 为线程控制块指针。

此函数的作用是,把线程对象从线程队列中删除,释放线程占用的堆空间,并把相应的线程状态更改为 RT_THREAD_CLOSE 状态。

对于用 rt_thread_init() 初始化的线程,使用 rt_thread_detach() 将使线程对象在线程队列和内核对
象管理器中被脱离。线程脱离函数如下:

rt_err_t rt_thread_detach (rt_thread_t thread);

线程本身不会调用这两个函数,应该是其他线程调用,用于删除某个线程。

2. 获得当前运行线程

RT-Thread 提供了函数接口,用于查询当前正在执行的线程句柄:

rt_thread_t rt_thread_self(void);

该函数的返回值是,线程控制块指针(线程句柄)。

调用失败,则返回 RT_NULL,说明系统调度器还未启动。

3. 线程让出处理器

处于运行状态的线程可以主动让出 CPU 的使用权限,通过调用如下函数:

rt_err_t rt_thread_yield(void);

调用此函数后,当前线程首先把自己从它所在的就绪优先级线程队列中删除,然后把自己挂到这个优先级队列链表的尾部,然后激活调度器进行线程上下文切换(如果当前优先级只有这一个线程,则这个线程继续执行,不进行上下文切换动作)。

4. 挂起和恢复线程

线程挂起的函数接口如下:

rt_err_t rt_thread_suspend (rt_thread_t thread);

参数 thread 为线程句柄(线程控制块指针)。

线程挂起成功,返回 RT_OK;挂起失败,则返回 -RT_ERROR。

注意,通常不应该使用这个函数来挂起线程本身。

恢复一个挂起的线程,就是让它重新进入就绪状态,并将线程放入系统的就绪队列中。使得线程恢复的函数接口为:

rt_err_t rt_thread_resume (rt_thread_t thread);

5. 控制线程

当需要堆一个线程进行其他控制时,可以调用如下函数接口:

rt_err_t rt_thread_control(rt_thread_t thread, rt_uint8_t cmd, void* arg);

参数 thread 为线程句柄;参数 cmd 为控制指令;arg 为控制指令参数。

返回 RT_EOK,表示执行成功。返回 -RT_ERROR,表示执行失败。

指示控制命令 cmd 当前支持的命令如下:

  • RT_THREAD_CTRL_CHANGE_PRIORITY,动态更改线程优先级。

  • RT_THREAD_CTRL_STARTUP,开始运行一个线程。

  • RT_THREAD_CTRL_CLOSE,关闭一个线程。

  • RT_THREAD_CTRL_BIND_CPU,绑定线程到某个 CPU。

6. 设置和删除空闲钩子函数

RT-Thread 提供函数接口设置空闲钩子函数:

rt_err_t rt_thread_idle_sethook(void (*hook)(void));
rt_err_t rt_thread_idle_delhook(void (*hook)(void));

钩子函数在空闲线程中自动运行,可以在钩子函数中做一些其他事情,比如系统指示灯闪烁。

空闲线程必须永远为就绪态,因此钩子函数不能调用能挂起线程的函数。

至此,RT-Thread 线程管理相关的内容学习完毕。这两篇文章,讲解了 RT-Thread 线程相关的理论知识,以及提供的系统函数接口。

并结合实验演示线程创建和线程延时的用法。其他线程管理简单进行了介绍。

对于入门来说,了解线程基础知识后,能够使用线程创建函数的使用即可。

深入学习的话,可以参考官方编程手册,详细学习线程管理其他的函数接口。

OK,今天先到这,下次继续。加油~

公众号【一起学嵌入式】,获取更多精彩内容


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK