RT_Thread——线程管理(下)
文章目录
- 一、Delay 函数
- 1.1 两个 Delay 函数
- 1.2 Delay 函数使用示例
- 二、空闲线程及其钩子函数
- 2.1 介绍
- 2.2 代码解析
- 2.3 使用空闲线程钩子函数
- 三、调度算法
- 3.1 重要概念
- 3.2 调度算法
- 3.3 调度器钩子函数
一、Delay 函数
1.1 两个 Delay 函数
有 3 个 Delay 函数:
rt_thread_delay()
:以系统时钟节拍为单位,当前线程会阻塞让出 CPU 资源rt_thread_mdelay()
:以 ms 为单位,当前线程会阻塞让出 CPU 资源rt_hw_us_delay()
:以 us 为单位,当前线程不会阻塞,这个函数是"死等“指定时间
这 3 个函数原型如下:
rt_err_t rt_thread_delay(rt_tick_t tick);
rt_err_t rt_thread_mdelay(rt_int32_t ms);
void rt_hw_us_delay(rt_uint32_t us);
系统时钟节拍是 RT-Thread 定时器的最小精度
1 OS Tick = 1/RT_TICK_PER_SECOND 秒
,RT_TICK_PER_SECOND
值在 rtconfig.h
文件中定义
rt_thread_mdelay
也是只能是时钟节拍的整数倍,使用 rt_thread_mdelay
可移植性更
好
在实际应用中,可以通过前 2 个延时函数挂起当前线程,让出 CPU。
1.2 Delay 函数使用示例
本节代码为:RT-Thread_06_taskdelay。
本程序会创建 2 个线程:
- Task1:
- 高优先级
- 设置变量 flag 为 1,然后调用
rt_thread_mdelay();
或rt_hw_us_delay();
rt_thread_mdelay()
会让出 CPUrt_hw_us_delay()
不会让出 CPU
- Task2:
- 低优先级
- 设置变量 flag 为 0
程序一些开头的定义:
#include <rtthread.h>
#include <rtdevice.h>
#include <board.h>#define THREAD_PRIORITY 15 //设置线程优先级
#define THREAD_STACK_SIZE 512 //设置线程栈大小
#define THREAD_TIMESLICE 15 //设置线程时间片大小static struct rt_thread *thread1; //定义线程1句柄指针
static struct rt_thread *thread2; //定义线程2句柄指针static volatile int flag = 0;
main 函数代码如下:
int main(void)
{/* 创建动态线程thread1,优先级为 THREAD_PRIORIT-1 = 14 */thread1 = rt_thread_create("thread1", //线程名字thread1_entry, //入口函数(void *)thread1_name, //入口函数参数THREAD_STACK_SIZE, //栈大小THREAD_PRIORITY-1, //线程优先级THREAD_TIMESLICE); //线程时间片大小/* 判断创建结果,再启动线程1 */if (thread1 != RT_NULL)rt_thread_startup(thread1); /* 创建动态线程thread2,优先级为 THREAD_PRIORIT = 15 */thread2 = rt_thread_create("thread2", //线程名字thread2_entry, //入口函数(void *)thread2_name, //入口函数参数THREAD_STACK_SIZE, //栈大小THREAD_PRIORITY, //线程优先级THREAD_TIMESLICE); //线程时间片大小/* 判断创建结果,再启动线程2 */if (thread2 != RT_NULL)rt_thread_startup(thread2); return 0;
}
Thread1 的代码中使用条件开关来选择延时函数,把 #if 1
改为 #if 0
就可以使用
rt_hw_us_delay
,代码如下:
/* 线程1的入口函数 */
static void thread1_entry(void *parameter)
{const char *thread_name = parameter;/* 线程1 */while(1){/* 打印线程的信息 */rt_kprintf(thread_name);flag = 1;#if 1 rt_thread_mdelay(50);
#else rt_hw_us_delay(50000);
#endif }
}
Thread2 的代码如下:
/* 线程2的入口函数 */
static void thread2_entry(void *parameter)
{const char *thread_name = parameter;/* 线程1 */while(1){/* 打印线程的信息 */rt_kprintf(thread_name);flag = 0;}
}
使用 Keil 的逻辑分析观察 flag 变量的 bit 波形,如下:
- 使用
rt_thread_mdelay()
时,才会出现电平变化
- 使用
rt_hw_us_delay()
时,线程 1 会一直占用 CPU,线程 2 无法执行,不会出现电平变化
二、空闲线程及其钩子函数
2.1 介绍
在 RT-Thread_03_delete_task 的实验里,我们提到空闲线程(Idle 线程)的作用:释放被删除的线程的内存。
除了上述目的之外,为什么必须要有空闲线程?
一个良好的程序,它的线程都是事件驱动的:平时大部分时间处于挂起状态。
有可能我们自己创建的所有线程都无法执行,但是调度器必须能找到一个可以运行的线程:所以,要提供空闲线程。
- 空闲线程优先级最低:它不能阻碍用户线程运行
- 空闲线程要么处于就绪态,要么处于运行态,永远不会挂起
空闲线程的优先级为最低,这意味着一旦某个用户的线程变为就绪态,那么空闲线程马上被切换出去,让这个用户线程运行。
在这种情况下,我们说用户线程"抢占"(pre-empt)了空闲线程,这是由调度器实现的。
要注意的是:如果某个线程被删除,那么需要确保后面空闲线程有机会执行,否则就
无法释放被删除线程的内存。
我们可以添加一个空闲线程的钩子函数,空闲线程的循环每执行一次,就会调用一次钩子函数。钩子函数的作用有这些:
- 执行一些低优先级的、后台的、需要连续执行的函数
- 测量系统的空闲时间:空闲线程能被执行就意味着所有的高优先级线程都停止了,所以测量空闲线程占据的时间,就可以算出处理器占用率。
- 让系统进入省电模式:空闲线程能被执行就意味着没有重要的事情要做,当然可以进入省电模式了。
2.2 代码解析
空闲线程的代码在文件 src\idle.c
中。
创建空闲线程的代码如下:
void rt_thread_idle_init(void)
{/* initialize thread */rt_thread_init(&idle, "tidle",rt_thread_idle_entry, RT_NULL,&rt_thread_stack[0], sizeof(rt_thread_stack), RT_THREAD_PRIORITY_MAX - 1,32);/* startup */rt_thread_startup(&idle);
}
空闲任务的入口函数如下:
- 用户可以提供多个钩子函数:
idle_hook_list[i]
,如果提供了就会被调用 - 清理工作:
rt_thread_idle_excute
static void rt_thread_idle_entry(void *parameter)
{while (1){#ifdef RT_USING_IDLE_HOOKrt_size_t i;for (i = 0; i < RT_IDLE_HOOK_LIST_SIZE; i++){if (idle_hook_list[i] != RT_NULL){idle_hook_list[i]();}}
#endifrt_thread_idle_excute();
#ifdef RT_USING_PMrt_system_power_manager();
#endif}
}
空闲线程的主要工作是调用 rt_thread_idle_excute()
来清理线程:
- 那些使用的
rt_thread_create()
函数创建的线程,退出时都被放在rt_thread_defunct
链表里 - 从链表里把它们逐个取出来
- 释放栈
- 释放线程控制块
代码如下:
2.3 使用空闲线程钩子函数
在 src/idle.c 中,可以看到如下代码:
所以使用空闲线程钩子函数的前提是:
rt-config.h
里定义宏RT_USING_IDLE_HOOK
- 使用
rt_thread_idle_sethook
函数,设置钩子函数
要删除钩子函数,使用 rt_thread_idle_dehook
函数。
三、调度算法
3.1 重要概念
这些知识在前面都提到过了,这里总结一下。
正在运行的线程,被称为"正在使用处理器",它处于运行状态。在单处理器系统中,任何时间里只能有一个线程处于运行状态。
非运行状态的线程,它处于这 4 中状态之一:初始、就绪、挂起、关闭。
就绪态的线程,可以被调度器挑选出来切换为运行状态,调度器永远都是挑选最高优先级的就绪态线程并让它进入运行状态。
挂起状态的线程,它在等待"事件",当事件发生时线程就会进入就绪状态。
事件分为两类:时间相关的事件、同步事件。
所谓时间相关的事件,就是设置超时时间:在指定时间内阻塞,时间到了就进入就绪状态。使用时间相关的事件,可以实现周期性的功能、可以实现超时功能。
同步事件就是:某个线程在等待某些信息,别的线程或者中断服务程序会给它发送信息。
怎么"发送信息"?方法很多,有:信号量(semaphores)、互斥量(mutexe)、事件集 (event)等。
这些方法用来发送同步信息,比如表示某个外设得到了数据。
3.2 调度算法
RT-Thread 的调度算法为基于优先级调度和基于时间片轮转调度共存的策略。
RT-Thread 内核中存在多个线程优先级,同一个线程优先级下可以有多个线程。
优先级数目在 rtconfig.h
中以宏定义的方式配置:
#define RT_THREAD_PRIORITY_MAX 32
系统中可能有多个线程,它们的优先级可能相同、可能不同,RT-Thread 采用的调度策略是:
- 可抢占:高优先级的就绪线程会“立刻”抢占低优先级的线程;
- 时间片轮转:同优先级别的就绪线程,依次运行
3.3 调度器钩子函数
在整个系统运行过程中,会不断地切换线程:这由 rt_schedule()来实现。
我们还可以提供一个调度器的钩子函数,在 rt_schedule()
切换线程时它会调用该钩子函数:
- 用户实现钩子函数,原型为
void hook_of_scheduler(struct rt_thread* from, struct rt_thread* to)
- 使用
rt_scheduler_sethook
设置钩子函数
rt_schedule()
代码如下:
本节代码为:RT-Thread_07_scheduler,main 函数代码如下:
int main(void)
{/* 设置调度器钩子 */rt_scheduler_sethook(hook_of_scheduler);/* 创建动态线程thread1,优先级为 THREAD_PRIORIT-1 = 14 */thread1 = rt_thread_create("thread1", //线程名字thread1_entry, //入口函数(void *)thread1_name, //入口函数参数THREAD_STACK_SIZE, //栈大小THREAD_PRIORITY-1, //线程优先级THREAD_TIMESLICE); //线程时间片大小/* 判断创建结果,再启动线程1 */if (thread1 != RT_NULL)rt_thread_startup(thread1); /* 创建动态线程thread2,优先级为 THREAD_PRIORIT = 15 */thread2 = rt_thread_create("thread2", //线程名字thread2_entry, //入口函数(void *)thread2_name, //入口函数参数THREAD_STACK_SIZE, //栈大小THREAD_PRIORITY, //线程优先级THREAD_TIMESLICE); //线程时间片大小/* 判断创建结果,再启动线程2 */if (thread2 != RT_NULL)rt_thread_startup(thread2); return 0;
}
钩子函数内容如下:
static void hook_of_scheduler(struct rt_thread* from, struct rt_thread* to)
{rt_kprintf("Thread:%s --> Thread:%s \r\n", from->name, to->name);
}
线程 1、线程 2 代码如下:
/* 线程1的入口函数 */
static void thread1_entry(void *parameter)
{const char *thread_name = parameter;/* 线程1 */while(1){/* 打印线程的信息 */rt_kprintf(thread_name);rt_thread_mdelay(10);}
}/* 线程2的入口函数 */
static void thread2_entry(void *parameter)
{const char *thread_name = parameter;/* 线程1 */while(1){/* 打印线程的信息 */rt_kprintf(thread_name);}
}
程序运行时:
- 优先级高的线程 1 会先运行
- 线程 1 调用
rt_thread_mdelay()
后挂起,将切换到线程 2 - 切换时,钩子函数被调用,它打印调度信息