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


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

    1. 线程初始化

    在rt-thread中线程主要包括以下一些内容,线程控制块、线程栈、函数入口。
    在这里插入图片描述

    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 */
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    优先级里有当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
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    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;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

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

        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 */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    用一张图片来展示这个过程
    在这里插入图片描述

    2. 线程状态切换

    2.1线程的状态

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

    • 初始状态
      任务使用rt_thread_init()/ rt_thread_create()创建后则处于初始状态。
      任务会被添加到对应优先级的就绪态链表中,如下图所示:
      在这里插入图片描述

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

    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;
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

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

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    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
    
    • 1
    • 2
    • 3

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

    细心的读者一定在猜测:为啥袒护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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

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

    2.2.3为什么是PendSV?

    这个问题答案在《Cortex-M3 权威指南》中由说明。
    在这里插入图片描述

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

    在这里插入图片描述

    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优先级规则:任务调度器会优先找到优先级最高的链表的最后一项执行

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

    3.2时间片

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

    参考

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

  • 相关阅读:
    光明源@智慧公厕的卫生安全与隐私平衡!
    Java网络编程、TCP、UDP、Socket通信---初识版
    从 iOS 设备恢复数据的 20 个iOS 数据恢复工具
    YOLOV5部署Android Studio安卓平台NCNN
    AttributeError: module ‘torch._C‘ has no attribute ‘_cuda_setDevice‘
    LeetCode 每日一题 2023/10/23-2023/10/29
    WEB开发技能树-PHP-GET/POST传参方式
    2023-09-08力扣每日一题
    权限系统的设计
    常见DOS命令总结
  • 原文地址:https://blog.csdn.net/qq_39854159/article/details/132769791