rt-thread------任务调度

rt-thread------任务调度

1. 线程初始化

在rt-thread中线程主要包括以下一些内容,线程控制块、线程栈、函数入口。
rt-thread------任务调度_第1张图片

1.1线程创建函数

RTOS基本都包括两种线程方式:动态创建rt_thread_create()和静态创建rt_thread_init()
因为有些系统设计时对安全性要求比较高,内存需要提前分配好,只能使用静态创建的方式。

1.2 线程控制块

1.2.1线程结构体

线程结构体的一些主要成员

struct rt_thread
{
    /* stack point and entry */
    void       *sp;                                     /**< stack point */
    void       *entry;                                  /**< entry */
    void       *parameter;                              /**< parameter */
    void       *stack_addr;                             /**< stack address */
    rt_uint32_t stack_size;                             /**< stack size */
    /* error code */
    rt_err_t    error;                                  /**< error code */
    rt_uint8_t  stat;                                   /**< thread status */
    /* priority */
    rt_uint8_t  current_priority;                       /**< current priority */
    rt_uint8_t  init_priority;                          /**< initialized priority */
    
    rt_ubase_t  init_tick;                              /**< thread's initialized tick */
    rt_ubase_t  remaining_tick;                         /**< remaining tick */
    struct rt_timer thread_timer;                       /**< built-in thread timer */
};

优先级里有当current_priorityinit_priority后续文章讲到互斥量时有优先级继承时会详细说明。
init_tickremaining_tick在第3.2节时间片概念中会详细说明。

1.2.2 线程创建

无论选择动态还是静态创建最后都会调用这个函数static rt_err_t _thread_init()。简要分析一下这个函数的作用

    thread->entry = (void *)entry;//函数指针
    thread->parameter = parameter;//创建任务时可以带个参数

    /* stack init 任务栈初始化*/
    thread->sp = (void *)rt_hw_stack_init(thread->entry, thread->parameter,
                                          (rt_uint8_t *)((char *)thread->stack_addr + thread->stack_size - sizeof(rt_ubase_t)),
                                          (void *)rt_thread_exit);
    thread->stack_addr = stack_start;//初始化栈顶指针
    thread->stack_size = stack_size;//初始化栈的大小
    /* priority init 优先级初始化*/
    RT_ASSERT(priority < RT_THREAD_PRIORITY_MAX);
    thread->init_priority    = priority;//初始化初始化优先级
    thread->current_priority = priority;//初始化当前优先级
    /* tick init */
    thread->init_tick      = tick;//初始化时间片
    thread->remaining_tick = tick;//初始化剩余时间片

1.2.3 线程栈的初始化

线程栈的结构体如下代码,其实就是我们熟悉ARM中16个寄存器。但是他们定义的顺序不是r0-15,而是r4~ r11,r0~ r3,r12,lr,pc,psr,其原因在2.2.1节会讲明。

struct exception_stack_frame
{
    rt_uint32_t r0;
    rt_uint32_t r1;
    rt_uint32_t r2;
    rt_uint32_t r3;
    rt_uint32_t r12;
    rt_uint32_t lr;
    rt_uint32_t pc;
    rt_uint32_t psr;
};

struct stack_frame
{
    /* r4 ~ r11 register */
    rt_uint32_t r4;
    rt_uint32_t r5;
    rt_uint32_t r6;
    rt_uint32_t r7;
    rt_uint32_t r8;
    rt_uint32_t r9;
    rt_uint32_t r10;
    rt_uint32_t r11;

    struct exception_stack_frame exception_stack_frame;
};

线程栈初始化函数主要实现:

    struct stack_frame *stack_frame;
    rt_uint8_t         *stk;
    unsigned long       i;

    stk  = stack_addr + sizeof(rt_uint32_t);
    stk  = (rt_uint8_t *)RT_ALIGN_DOWN((rt_uint32_t)stk, 8);
    stk -= sizeof(struct stack_frame);

    stack_frame = (struct stack_frame *)stk;

    /* init all register */
    for (i = 0; i < sizeof(struct stack_frame) / sizeof(rt_uint32_t); i ++)
    {
        ((rt_uint32_t *)stack_frame)[i] = 0xdeadbeef;
    }

    stack_frame->exception_stack_frame.r0  = (unsigned long)parameter; /* r0 : argument */
    stack_frame->exception_stack_frame.r1  = 0;                        /* r1 */
    stack_frame->exception_stack_frame.r2  = 0;                        /* r2 */
    stack_frame->exception_stack_frame.r3  = 0;                        /* r3 */
    stack_frame->exception_stack_frame.r12 = 0;                        /* r12 */
    stack_frame->exception_stack_frame.lr  = (unsigned long)texit;     /* lr */
    stack_frame->exception_stack_frame.pc  = (unsigned long)tentry;    /* entry point, pc */
    stack_frame->exception_stack_frame.psr = 0x01000000L;              /* PSR */

用一张图片来展示这个过程
rt-thread------任务调度_第2张图片

2. 线程状态切换

2.1线程的状态

rt-thread将线程分成5个状态:初始状态、就绪状态、运行状态、挂起状态和关闭状态。如图所示:
rt-thread------任务调度_第3张图片
注:rt-thread现在只允许线程A挂起线程A,不允许线程A挂起线程B,所以rt_thread_suspend()理论上并不能把一个就绪态的任务转换到挂起态,而是把运行态任务切换至挂起态。

  • 初始状态
    任务使用rt_thread_init()/ rt_thread_create()创建后则处于初始状态。
    任务会被添加到对应优先级的就绪态链表中,如下图所示:
    rt-thread------任务调度_第4张图片

  • 就绪状态
    就绪态顾名思义就是程序准备运行的状态,以下是几种会让程序处于就绪态的方式:

  1. 创建线程后当调用rt_thread_startup()后进入就绪状态
  2. 在运行状态中被更高优先级线程抢占了
  3. 时间片用完轮到同优先级线程执行
  4. 从挂起状态恢复后由于有更高优先级任务需要执行
  • 运行状态
    当前运行的线程
  • 挂起状态
  1. 通过系统阻塞函数
  2. 等待同步资源
  • 关闭状态
    从运行态退出的程序
    使用rt_thread_delete()/rt_thread_detach()删除的线程

2.2 线程切换

线程A运行一段时间后切换线程B,需要先保存A线程的现场,再去切换线程B。若再切换成线程A,则再需要保存线程B的现场,再去切换线程A。

2.2.1保护现场

保护现场保护什么呢?以下面一段代码解释一下:

void task_A()
{
	int a = 1;
	int b ;
	b = a + 2;

}

其汇编代码如下图所示:
大致意思是这样的(也不用全懂哈哈)

0x08000144 2001      MOVS     r0,#0x01       :将1存入r0
0x08000146 9001      STR      r0,[sp,#0x04]	 :将r0存入sp+4的位置也就是局部变量a
     7:         b = a + 2; 
0x08000148 9801      LDR      r0,[sp,#0x04]  :将sp+4的值存入r0
0x0800014A 1C80      ADDS     r0,r0,#2		 :r0=r0+2
0x0800014C 9000      STR      r0,[sp,#0x00]  :将r0存入sp的位置
     8:         return b; 
0x0800014E 9800      LDR      r0,[sp,#0x00]   :sp+0的值存入r0

ARM架构中一共有16个寄存器(r0-r7,r8-r12,sp,lr,pc)中间运算的时候会将值存入r0,若此时任务切换则r0的值会在别的任务中被覆盖,恢复后是不可预料的。所以再任务切换前需要需要保护这16个寄存器。局部变量a不需要保存,因为他保存再线程A的栈中间,出栈即可。
保存现场的处理函数在pendSV异常中,主要是这两句汇编


    MRS     r1, psp                 ; get from thread stack pointer
    STMFD   r1!, {r4 - r11}         ; push r4 - r11 register

STMFD Store Multi Full Dec
找出线程A的线程栈,将r4 - r11多个寄存器满减压栈到线程A的栈中。前文说了有16个寄存器,软件只存了这8个剩下的几个寄存器在进入中断是已经存储到线程A的栈中,如下图:
rt-thread------任务调度_第5张图片
还有一段描述

细心的读者一定在猜测:为啥袒护R0‐R3以及R12呢,R4‐R11就是下等公民?原来,在ARM
上,有一套的C函数调用标准约定(《C/C++ Procedure Call Standard for the ARM Architecture》,
AAPCS, Ref5)。个中原因就在它上面:它使得中断服务例程能用C语言编写,编译器优先使
用被入栈的寄存器来保存中间结果(当然,如果程序过大也可能要用到R4‐R11,此时编译器
负责生成代码来push它们。但是,ISR应该短小精悍,不要让系统如此操心——译者注

这也解释了rt-thread线程栈结构体r0-r15不是连续的原因。

2.2.2切换任务

有了保护现场的经验,切换任务就简单了。

    LDMFD   r1!, {r4 - r11}         ; pop r4 - r11 register
    MSR     psp, r1                 ; update stack pointer

pendsv_exit
    ; restore interrupt
    MSR     PRIMASK, r2

    ORR     lr, lr, #0x04

从线程B的栈中恢复r4-r11寄存器,跟新SP指针指向r11。推出异常硬件自动恢复线程B的R0‐R3以及R12。
任务切换比较深奥,多读两遍再结合韦东山老师的视频就能理解了。若还不能理解可以先学使用,再过几个月回过来看看。

2.2.3为什么是PendSV?

这个问题答案在《Cortex-M3 权威指南》中由说明。
rt-thread------任务调度_第6张图片

上图是两个任务轮转调度的示意图。但若在产生 SysTick 异常时正在响应一个中断,则
SysTick 异常会抢占其 ISR。在这种情况下,OS 不得执行上下文切换,否则将使中断请求被延
迟,而且在真实系统中延迟时间还往往不可预知——任何有一丁点实时要求的系统都决不能
容忍这种事。因此,在 CM3 中也是严禁没商量——如果 OS 在某中断活跃时尝试切入线程模
式,将触犯用法 fault 异常

rt-thread------任务调度_第7张图片

PendSV 异常会自动延迟上下文切换的请求,
直到其它的 ISR 都完成了处理后才放行。为实现这个机制,需要把 PendSV 编程为最低优先级的异常。如果 OS 检测到某 IRQ 正在活动并且被 SysTick 抢占,它将悬起一个 PendSV 异常,以便缓期执行上下文切换。

3. 线程优先级与时间片

3.1优先级

假设我们创建3个线程ABC,优先级分别时12,12,15(如下图所示)。在rt-thread中优先级数字越低优先级越高,这里与freeRTOS相反。rt-thread链表插入使用的是rt_list_insert_before()头插法,双向循环链表头的前面也等价于链表的最后面(可能有点绕),所以初始化的顺序是先初始化线程A再初始化线程B。
rt-thread优先级规则:任务调度器会优先找到优先级最高的链表的第一项执行
freeRTOS优先级规则:任务调度器会优先找到优先级最高的链表的最后一项执行

rt-thread------任务调度_第8张图片
任务调度器会优先找到优先级最高的链表,也就是优先级为12的链表,然后取链表头后面的第一个线程(A),当A运行时则从就绪态链表中移除。当线程A被挂起时,任务调度器会继续寻找优先级最高的第一个线程,此时是B。
rt-thread------任务调度_第9张图片
当任务B被挂起时,按照上述规则则会执行任务C。
当挂起态的任务结束阻塞调用rt_thread_resume()会重新插入到就绪态链表中。
rt-thread------任务调度_第10张图片

3.2时间片

当A,B两个线程均无阻塞且处于当前最高就绪态相同优先级,A运行时间片为10个tick,B为5个tick,当前执行B线程。按照上述优先级规则执行那么永远只会执行B线程,A永远不会执行。为了避免此类情况发生,设计了时间片的概念。当B执行了5个tick后,则挂到就绪态当前优先级的队尾,此时任务切换后执行的便是A线程。A执行完10个tick则会切换会B。AB两个线程都得到了执行的时间
rt-thread------任务调度_第11张图片
其背后原理是,当任务B的remaining_tick用完后会被移动到就绪态链表的最后面。当任务调度器调度按照3.1节内叙述的规则找到任务A的控制块运行。
rt-thread------任务调度_第12张图片

参考

b站:韦东山RT-Thread系列教程: RT-Thread的内部机制
《RT-Thread 完全开发手册之快速入门》
《Cortex-M3 权威指南》

你可能感兴趣的:(#,rt-thread,rt-thread,RTOS,线程,任务调度)