RT-Thread任务调度学习笔记

引言

RT-Thread作为一个嵌入式实时系统,任务调度是其最为核心的组成部分,那么它是如何工作的呢?就由我带大家了解一下吧。

相关函数

先简单介绍一下接下来会用到的函数以及结构体

函数声明/结构体详细说明
rt_thread_t线程句柄(线程结构体的指针)
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)创建指定参数的线程 参数:线程名,任务(函数),任务的参数,线程大小,线程优先级,线程运行时间片长度
rt_err_t rt_thread_start_up(rt_thread_t thread)启动线程 参数:线程句柄

代码实现

有了这些函数之后我们就可以创建我们自己的线程了

#include <rtthread.h>

// 线程的栈大小,优先级,时间片长度
static const int THREAD_STACK_SIZE = 1024;
static const int  THREAD_PRIORITY = 25;
static const int THREAD_TIMESLICE = 10;

// 三个线程的句柄
static rt_thread_t t1, t2, t3;

// 线程1,2,3的任务
static void thread1_func(void* para)
{
    rt_kprintf("I'm %s\n", rt_thread_self()->parent.name);
    while(1);
}
static void thread2_func(void* para)
{
    rt_kprintf("I'm %s\n", rt_thread_self()->parent.name);
    while(1);
}
static void thread3_func(void* para)
{
    rt_kprintf("I'm %s\n", rt_thread_self()->parent.name);
    while(1)
    {
        rt_thread_mdelay(1000);
    }
}

// 创建并启动三个线程
void trace_thread_sample(void)
{
    t1 = rt_thread_create("t1", thread1_func, NULL, THREAD_STACK_SIZE, THREAD_PRIORITY, THREAD_TIMESLICE);
    t2 = rt_thread_create("t2", thread2_func, NULL, THREAD_STACK_SIZE, THREAD_PRIORITY, THREAD_TIMESLICE);
    t3 = rt_thread_create("t3", thread3_func, NULL, THREAD_STACK_SIZE, THREAD_PRIORITY - 1, THREAD_TIMESLICE);
    if(t1 && t2 && t3)
    {
        rt_thread_startup(t3);
        rt_thread_startup(t1);
        rt_thread_startup(t2);
    }
}

int main(void)
{
  trace_thread_sample();
  return 0;
}

代码分析

观察声明:
我分别对线程栈空间大小,优先级,时间片长度进行了声明,便于后面创建线程时进行使用,那么它们分别都有什么意义呢?

  1. 栈空间大小:它定义了一个线程,在系统能够使用它时所能使用的空间大小,像函数调用,局部变量,临时变量,都是在栈空间中进行储存的。

  2. 优先级:在嵌入式操作系统中,每个优先级都有一个队列,创建线程的函数会根据传入的优先级选择将线程放入对应的队列中,在调度的时候系统会优先从优先级数值最低的队列中取出一个线程运行。

    • 线程加入优先级队列后的状态大概如下图所示:

    • 优先级队列的队头处于一个数组当中,也就是说优先级是有限的,而加入指定优先级队列的线程则是以链表节点插入的形式加入的,因此加入优先级队列的线程可以是无限的。

    • 在这里插入图片描述

    • 如果此时发生调度,那么调度器就会取出优先级为1的队列中的线程x运行。

  3. 时间片:时间片的长度决定了当线程拥有cpu时的正常运行时间(为什么说是正常运行时间呢?因为运行期间可能被高优先级的线程抢占),(在线程都处于相同优先级的情况下)它保证了所有线程都有机会运行,避免出现有些线程一直占用cpu,而有的线程却一直没办法运行的情况(旱的旱死,涝的涝死),从而提高整个系统的处理能力和处理效率。

观察代码:
首先观察线程执行的任务:线程一和线程二一直跑在一个死循环当中,任务三则每次循环都会延时1000ms,
再看到线程初始化:线程一和线程二的优先级都为25,而线程三则比线程一二高一个优先级为24,
然后是启动顺序方面:我们将线程加入调度队列的顺序为: t3 --> t1 --> t2。
对代码有个基本的了解之后,我们就可以开始运行程序了。

RTT 启动!

代码运行

启动程序后,我们打开串口助手可以看见我们启动三个线程时打印的线程名:
screenshot_image.png

我们由此可以分析出三个线程的运行顺序:t3 --> t2 --> t1

这里可能大家就会比较奇怪了:
按照线程加入调度的顺序来说t1要比t2更早加入调度啊,为什么t2更先运行呢,既然t1和t2的顺序是和加入调度时的顺序相反的为什么t3又是最先运行的呢?
通过查阅RT-Thread文档中心线程相关的文档,我们发现RT-Thread的调度是 时间片轮转+优先级抢占式 的。

我们获取这个知识后回到我们的程序:
t3是最高优先级的线程,不管什么加入顺序,它都会马上抢过cpu去优先执行它的任务。 t1和t2在相同的优先级队列中,那么就要考虑加入的顺序了,因为t1先加入t1后执行,t2后加入但却先执行,我们是不是可以合理猜测一下他们加入调度队列的顺序,就是先加入的线程在尾部,后加入的线程在首部,什么插入方式的实现效果是这样的呢?——— 链表的头插法?其实不准确,实际上是有一个头部节点(队头),每次有新节点则加入到队头的后面,同样,取出节点也是每次从队头后一个取。这种实现方式,不仅便于将新节点插到队列首部,同时也方便将以及运行完的节点放在队列尾部。

在这里插入图片描述

我们可以模拟一下三个线程加入调度的过程:

t3启动:
优先级为24的队列:t3 <–
优先级为25的队列:<–


t1启动:
优先级为24的队列:t3 <–
优先级为25的队列:t1 <–


t2启动:
优先级为24的队列:t3 <–
优先级为25的队列:t2 <-- t1 <–


大家可能又会疑惑,怎么三个线程都加入调度队列中了,一个都还没执行呢?
因为我们是在main线程中进行初始化的,main线程的优先级可是高贵的10,
它们在加入的时候,cpu可都是main线程的形状呢。

观察线程运行图:

通过线程运行的分析工具,我们得到了一张线程运行图:

在这里插入图片描述

初看这张图容易一脸懵,那么我们就逐步解析一下。
我们先放大看一个t1线程运行一个时间片的长度:
在这里插入图片描述
通过截图我们发现线程t1的时间片长度为10us,然后就切换到线程t2运行了,很明显它的时间单位出错了,t1的运行时间片长度应该是10ms而不是10us,如果是10us的话,1ms要切换100次线程,那cpu得累死(当然也希望以后能研发出那种能够承载10us切换一次线程的国产cpu)。也就是说,t1和t2轮流获取cpu操控权10ms,那么t3呢,t3哪里去了?

还记得我们在代码中给t3的延时吗?t3挂起到等待队列中,只有等待满了1000ms,它才能获取一次cpu的使用权,与其说获得就不如直接说是抢的,从t2线程手上抢过来的,但是很快t3就被抓回等待队列去了,所以t2很快又拿回了它的cpu使用权。
在这里插入图片描述

如果我们仔细算,两次获取cpu使用权的时间间隔,t1和t2的时间片加起来就刚好是100个。

在这里插入图片描述

这篇文章含有比较多我对系统的一些理解,如果有些地方理解的不到位,或者是有错的,麻烦及时联系我指出,感谢。

  • 27
    点赞
  • 53
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值