首发,公众号【一起学嵌入式】,RTOS、Linux、C
上一篇主要介绍了 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 内核的系统线程有两个:
- 空闲线程
- 主线程
1. 空闲线程
空闲线程是优先级最低的线程,该线程永远处于就绪状态。当系统中没有其他就绪线程时,调度器会将 CPU
权限给空闲线程。空闲线程永远不能挂起。
RT-Thread 的空闲线程由特殊用途:
- 空闲线程会回收被删除线程的资源。
- 可以设置空闲线程钩子函数,在空闲线程中调用。
2. 主线程
系统启动时,会自动创建 main 线程,其入口函数为 main_thread_entry()
,用户的应用程序入口函数 main()
就是从这里开始。
RT-Thread 系统启动过程,可以参考文章:RT-Thread快速入门-了解内核启动流程
系统调度器启动后,main 线程就开始运行,函数调用过程如下图所示:
用户可以在 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,今天先到这,下次继续。加油~
公众号【一起学嵌入式】,一起学习、一起成长