RT-Thread内核学习
一、概述
线程是RT-Thread的核心部分,也是最基础的功能,系统都是围绕线程来构建的。
二、线程的组成
RT-Thread中,线程由三部分组成:
1、线程代码(入口函数)
2、线程控制块
3、线程堆栈
2.1、线程代码(入口函数)
线程代码是我们实现某个功能的代码实现,一般的入口函数都是无限循环结构,类似如下的代码:
-
/* 线程1的入口函数 */
-
static void thread1_entry(void *parameter)
-
{
-
rt_uint32_t count = 0;
-
-
while (1)
-
{
-
/* 线程1采用低优先级运行,一直打印计数值 */
-
rt_kprintf("thread1 count: %d\n", count ++);
-
rt_thread_mdelay(500);
-
}
-
}
当然也有顺序执行结构,一些只需要执行一次,就不使用循环,执行完一次后就会被回收,不再有效。
2.2、线程控制块
线程控制块是操作系统用于管理线程的一个数据结构,它会存放线程的一些信息,例如优先级、线程名称、线程状态等,也包括线程与线程之间连接用的链表结构、线程等待事件集合等。
-
/**
-
* Thread structure
-
*/
-
struct rt_thread //线程结构体
-
{
-
/* rt object */
-
char name[RT_NAME_MAX]; /**< the name of thread */
-
rt_uint8_t type; /**< type of object */
-
rt_uint8_t flags; /**< thread's flags */
-
-
/*
-
此处省略很多代码
-
*/
-
};
-
typedef struct rt_thread *rt_thread_t; //线程结构体指针
2.3、线程栈
RT-Thread 每个线程都具有独立的栈空间,当线程切换时,系统会将当前线程的上下文保存到线程栈中,当线程要恢复时,再从对应的线程栈中读取之前保存的上下文信息,从而恢复到被切换时的状态,完整的恢复线程的运行。
线程上下文是指线程执行时的环境,具体来说,就是各个变量和数据包的所有寄存器变量、堆栈信息、内存信息等。(注:这里要好好研究一下,比如具体保存哪些,如何实现等,深究一下)
线程栈在形式上是一段连续的内存空间,可以通过定义一个数组或者申请一段动态内存来作为线程的栈。
三、线程相关的API
3.1、线程的创建
3.1.1、创建静态线程,初始化函数如下:
-
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) //时间片数(单位:系统滴答)
初始化前提,需要定义线程控制块、栈(这里一般是数组)、入口函数,举例如下:
-
/* 定义线程控制块 */
-
static struct rt_thread led1_thread;
-
/* 定义线程控栈时要求RT_ALIGN_SIZE个字节对齐 */
-
ALIGN(RT_ALIGN_SIZE)
-
/* 定义线程栈 */
-
static rt_uint8_t rt_led1_thread_stack[1024];
-
/* 函数声明 */
-
static void led1_thread_entry(void* parameter);
3.1.2、创建动态线程,初始化函数如下:
-
//返回线程控制块指针
-
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) //时间片
初始化前提,需要定义线程控制块指针、入口函数,举例如下:
-
/* 定义线程控制块 */
-
static rt_thread_t led1_thread = RT_NULL;
-
/* 函数声明 */
-
static void led1_thread_entry(void* parameter);
3.2、状态的切换
3.2.1、比较常用的是启动函数,传入的参数是线程控制块指针。
-
/* 启动线程,开启调度 */
-
if (led1_thread != RT_NULL)
-
rt_thread_startup(led1_thread);
3.2.2、别的导致线程状态切换的API如下:
3.3、钩子函数
3.3.1、空闲线程的钩子函数
空闲钩子函数是空闲线程的钩子函数,如果设置了空闲钩子函数,就可以在系统执行空闲线程时,自动执行空闲钩子函数来做一些其他事情,比如系统指示灯等。API如下:
-
rt_err_t rt_thread_idle_sethook(void (*hook)(void)); //hook为自己定义的函数
-
rt_err_t rt_thread_idle_delhook(void (*hook)(void));
空闲线程的优先级是最低的。最多可以设计四个空闲线程的钩子函数(why?) 。
3.3.2、调度器的钩子函数
在整个系统的运行时,系统都处于线程运行、中断触发 - 响应中断、切换到其他线程,甚至是线程间的切换过程中,或者说系统的上下文切换是系统中最普遍的事件。有时用户可能会想知道在一个时刻发生了什么样的线程切换,可以通过调用下面的函数接口设置一个相应的钩子函数。在系统线程切换时,这个钩子函数将被调用:
void rt_scheduler_sethook(void (*hook)(struct rt_thread* from, struct rt_thread* to));
钩子函数 hook() 的声明如下:
void hook(struct rt_thread* from, struct rt_thread* to);
注意: 请仔细编写你的钩子函数,稍有不慎将很可能导致整个系统运行不正常(在这个钩子函数中,基本上不允许调用系统 API,更不应该导致当前运行的上下文挂起)。
调度器的钩子函数只能设置一个。
四、注意事项与补充
4.1、动态创建与静态创建的优缺点比较?
待续
4.2、系统滴答时钟频率的选取
操作系统都存在一个叫做“系统心跳”的时钟,它是操作系统的最小时钟单位,负责系统和时间相关的一些操作,这个心跳时钟一般是由硬件定时器的中断来产生的。
时钟节拍使得内核可以将线程延时若干个整数时钟节拍,以及线程等待事件时提供等待超时的依据。
系统的心跳时钟也成为系统滴答或时钟节拍,它的频率,我们要根据cpu的处理能力来决定(比如72MHz的STM32F1,我们常设置每个滴答时间为10ms)。
频率越快,内核函数介入系统运行的几率越大,内核占用处理器的时间就越长,系统的负荷越高,也就是说本该处理应用的计算资源更多的被内核占用。
但频率越低,时间处理的精度又不够,也可能会造成系统响应迟钝。
4.3、线程栈大小分配的小策略
先将线程栈大小设置成一个比较大的值(比如2048),在线程运行时查看线程栈的使用情况(如下图),根据情况设置合理的栈大小,一般使线程栈使用最大量设置为70%左右比较合适。