美文网首页RT-thread
RT-Thread(RTT) 内部机制

RT-Thread(RTT) 内部机制

作者: my_passion | 来源:发表于2022-07-28 10:21 被阅读0次

    1 RT-Thread 介绍

    1.1 RT-Thread

        线程管理
        调度
        线程间通信(邮箱/消息队列/信号)
        线程间同步(信号量/互斥量/事件集)
    

    核心 都是 链表 & 定时器

    1.2 3个层次

        (1) 会用 API
    
        (2) 懂 内部机制
    
        (3) 掌握代码实现细节, 能移植
    

    前2个层次可速成: 10 几个小时足够

    2 RTOS 的引入

    喂饭 和 回消息

    2.1 单线条

        喂饭 -> 回消息 -> 喂饭 -> 回消息 -> ...
    

    2件事有影响: 喂饭慢了, 同事以为在敷衍; 回消息慢了, 小孩饿哭

    2.2 多线条: 每个 task 拆分为多个步骤, 2个 task 步骤交错 => 假象: 2个 task 好像同时进行

        喂饭   = 舀饭 + 把饭放小孩口里 + 舀菜
        回消息 = 打几个字 + 打几个字 + 打1条消息的最后几个字
        
        舀饭 -> 打几个字 -> 把饭放小孩口里 -> 打几个字 -> 舀菜 -> 打1条消息的最后几个字 -> ... 循环 
    

    2.3 前后台

    前台: 触发中断的事件 (按键 / 触摸屏幕 / 事件到)

    后台: 中断服务程序

    缺点: 若 某中断 处理时间长, 其余中断 和 main 的 while 都会被 卡住 => 系统卡顿

    int main()
    {
        // key isr
        
        // touch isr
        
        // timer isr
        
        while()
        {
            ...
        }
    }
    

    改进: 中断处理程序 只设 flag, while 循环据 flag 选择做某件事 -> 退化单线条

    int main()
    {
        // key isr flag1 = xxx
        
        // touch isr: flag2 = yyy
        
        // timer isr
        
        while()
        {
            if(flag1 == xxx)
                doSth1();
            if(flag2)
                doSth3();
        }
    }
    

    总结

    (1) 单线条 & 前后台 相同处: 用 中断驱动事情的进展, 用 驱动来做事情

    缺点:

    1) 前后事情有影响, 每件事情不能做得太久

    => 
    

    2) 程序设计要求更高, 难度更大

    (2) 多线条/RTOS

    1) 感觉上 多 task 同时运行

    2) 程序更容易设计

    3 线程的概念保存

    切换 task 时, 要先 保存 当前 task 做了什么, 再切回来时, 才能 恢复(之前)现场

    3.1 程序运行

    (1) 运行线程/taskA

    (2) 保存 A 的 现场

    (3) 运行 B : 恢复 B 的现场

    (4) 保存 B 的现场

    (5) 运行 A : 恢复 A 的现场

    3.2 简单示例

    (1) 什么叫任务, 什么叫线程

    RTT里 task 和 线程 是同一个东西

    什么叫线程?先想 怎么 切换&保存 线程 ?

    线程是函数吗?不是

    (2) 保存线程 要 保存什么 ? 保存在哪 ?

    1)函数本身

        在 Flash 上, 无需保存
    

    2)函数执行到了哪 (是 CPU寄存器: "PC")

        需要保存
    

    3)函数里用到 全局变量

        在内存上, 无需保存
    

    4)函数里用到 局部变量

        在栈/内存里, 无需保存, 只要避免栈不被破坏
    

    5)运算中间值

    保存在 CPU寄存器(引入 ARM架构和汇编) 里

    另一个线程 也要用 CPU寄存器 => CPU寄存器 需要保存

    int c = b+2; // <=> b + 2(得 newValue), (切换), c = newValue
    

    汇总:CPU寄存器 需要保存! 保存在哪里?保存在 线程的栈里

    怎么理解 CPU寄存器 & 栈?

    4 ARM 架构 和 汇编

    4.1 ARM 架构 之 STM32103(芯片): 可被称为 MCU / SOC

    里面有

        CPU 
        
        内存/RAM: 保存 data 
        Flash 
        GPIO 
        UART 
    

    4.2 CPU 与内存 间关系

    (1) CPU 与 RAM/Flash 之间有 地址总线 & 数据总线

    (2) CPU 想访问这些设备, 会 发出地址(Addr), 会 得到数据(Data)

    (3) 对 ARM 芯片, 对 精简指令集, CPU 对 内存 只有2个功能: read/write

    (4) 对数据的 运算 在 CPU 内部执行

    a += b

        [1] Read a 到 CPU 内部
            
        [2] Read b 到 CPU 内部
            
        [3] CPU 内部 运算 a+b 
            
        [4] 运算结果 Write 到 a 所在 内存位置
    

    4.3 CPU 内部结构

    (1) 寄存器 R0-R15

    [1] 存 从 内存 Read 的 data 
        
    [2] 存 运算结果
        
    [3] 特殊寄存器
    
        PC: 取指、执行
            从 Flash 对 `机器码`, 取指、译码(`汇编`指令)、执行
    

    (2) ALU: 运算单元

    CPU(内部结构)-内存/代码.png
        arm通用寄存器 别名 意义 
    
        R# APCS别名 意义 
        R0 a1 参数/结果/scratch寄存器 1 
        R1 a2                       ...2 
        R2 a3                       ...3 
        R3 a4                       ...4 
    
        R4 v1       arm状态局部变量寄存器  1 
        R5 v2                           ...2 
        R6 v3                           ...3 
        R7 v4/wr                        ...4 / thumb状态工作寄存器 
        R8 v5                           ...5
        R9 v6/sb                        ...6 / 在支持RWPI的ATPCS中作为静态基址寄存器 
        R10 v7/sl                       ...7 / 在支持数据栈检查的ATPCS中作为数据栈限制指针
        R11 v8/fp                       ...8 / 帧指针 
        R12 ip      内部过程调用 scratch 寄存器 
        R13 sp      栈指针 
        R14 lr      链接寄存器 
        R15 pc      程序计数器   
    

    4.4 汇编

    需要掌握的几条汇编指令

        [1] 读内存 LDR, Load
        [2] 写内存 STR, Store
        [3] 加减  ADD / SUB
        [4] 跳转  BL, Branch And Link
        入栈  PUSH
        出栈  POP
    

    push/pop: 本质是 写/读内存

    只要掌握 4 条汇编指令, 就足以理解很多技术的内幕

    加载/存储指令(LDR/STR)
        LDR:    LDR r0,[addrA]  将地址addrA的内容 加载(存放)到r0里面
        STR:    STR r0,[addrA]  将r0中的值 存储到 地址addrA上
    
    加法运算指令(ADD)
        ADD:    ADD r0,r1,r2 # r0=r1+r2
        SUB:    SUB r0,r1,r2 # r0=r1-r2
    

    (1) 读

        From    从哪里读
        To      读到哪里去
        Len     长度 
    

    LDR R0, [R3]

    去 R3(要访问的内存的 地址) 表示的内存, 读 Data 放到 R0, LDR 指令 本身表示读的 长度 4Byte (其他长度, 加 后缀)

    CPU 发出 地址信号, addr = R3

    LDR.jpg

    Note: LDR 与 mov 区别

    mov R0, R3 # R0 <- R3

    把 R3 的值读到 R0

    (2) 写

        From    从哪里读
        To      读到哪里去
        Len     长度 
    

    STR R0, [R3]

    把 R0 的值, 写到 R3 所表示的地址上去

    Note:
    

    mov R3, R0 # R3 <- R0

    把 R0 的值写到 RO

    (3) 加

    不涉及内存操作, 只在 CPU 内部实现

    (4) 寄存器 入栈/出栈指令(PUSH/POP)

    栈: 栈底 高地址, 高标号寄存器 放 高地址处

         ———————————— 
        |   Rn      |       高地址 
         ————————————
        |   Rn_1    |   
         ————————————
        |           |       低地址
        |           |
    

    [1] PUSH {R0, R1} 将 寄存器 R0,R1 写入内存栈

        SP = SP - 4
        [SP] = R1
        SP = SP - 4
        [SP] = R0
    

    PUSH 本质是 写内存 = 多次调 STR + 调整 SP

    [2] POP {R0, R1} 取出内存栈的数据 放入 R0, R1

        R0 = [SP]
        SP = SP + 4
        R1 = [SP]
        SP = SP + 4
    

    POP 本质是 读内存 = 多次调 LDR + 调整 SP

    PUSH/POP.jpg

    (5) 跳转

    BL A

    [1] 记录 返回地址(next 指令地址) => 保存到 R14/LR

    [2] 执行 A

    Note: A 执行完, 跳回到 R14 所存指令地址 去执行

        func()
        {
            A();            BL A
            B();            B
            
            A();            BL A
            C();            C 
        }
    

    5 简单 C 函数 反汇编 分析

    用该程序讲 怎么保存1个 task

    程序本质: 一系列运算, CPU 根据 指令, 读内存, 运算, 写内存

    void add_val(int *pa, int *pb)
    {
        volatile int tmp;
        
        tmp = *pa;
        tmp = tmp + *pb;
        *pa = tmp;
    }
    
    int main()
    {
        int a = 1;
        int b = 2;
        add(&a, &b);
        return 0;
    }
    

    5.1 main 汇编

    第1条汇编指令: push LR: LRmain 的 caller 中 BL/跳转 到 main 时, 已 保存了 main 的 next 指令地址, 以保证 main 执行结束 时, 能 跳回(PC = LR) 到 main 的 next 指令地址 去执行

    main 汇编.png

    5.2 add_val 汇编

    (1) 第1条 汇编指令 push {r3, lr}

    LR: add_val 的 caller(main) 中 BL/跳转 到 add_val 时, add_val 的 返回地址(next 指令地址) 已被保存到 LR/R14

    第一条指令又将 LR 的值 Write 到 函数栈上

    (2) 最后1条 汇编指令 pop {r3, pc}: R3 = 3, PC = 函数栈上 LR 的值 = add_val 的 next 指令, CPU 接着执行 add_val 的 next 指令(return 0 对应的指令)

    add_val 汇编.png

    5.3 局部变量 怎么体现?

    汇编 代码 构造好 data, 把 data 保存进 栈(的某处内存)

    5.4 函数参数: 第1/2个参数 保存在 R0/R1

    下来可以讨论 什么是 task/线程? 怎么保护 task/线程 ?

    6 保存现场 (上下文)

    6.1 假设在 执行完 ADD r2,r2,r3 这条指令后, 切换

    [1] 先保存: 保存什么 ? 所有 register. 保存到哪 ? 线程栈上

    [2] 执行别的代码

    [3] 后恢复: 切换回来, 重新执行 切换前下面的代码时, 从 栈里把 保存的寄存器 都恢复回去

    在切换时刻, 可以假装有一个 时间机器, 让一切都停止了, 此时要 保存现场

    6.2 保存现场, 需要保存什么 ?

    `CPU 算出来的新值` 还没有写入 局部变量, 就切换了 
        
        (1) 局部变量: 不需要保存, 只要保证 执行别的代码时 不用破坏 局部变量即可
    
        (2) R2: 存 CPU 计算出的 中间结果, 要保存
    
        (3) SP 要保存 
    
        本例, 因为 ADD r2,r2,r3 之后只用到 R2 SP 这2个寄存器
    

    普适情况: 切换 可能在任何地方

    切换发生的 时间停止瞬间, 要保存 所有 register

    保存到哪里 ? 栈上 一片连续空间: 用 SP 分配一块空间, SP = SP - 16*4

    还会保存 更多寄存器, 如 程序状态寄存器, 这里先不考虑这些

    保存现场.png

    7 创建线程 的 理论分析

    7.1 什么叫线程? 怎么保存线程 ?

        现在可以回答这个问题了
    

    什么叫线程: 运行中的函数、被 暂停运行的函数

    怎么保存线程:把 暂停瞬间的 CPU 寄存器值, 保存进

    7.2 RT-Thread 里 怎么 创建线程

    (1) 线程 A: 3要素

    [1] 入口函数

    [2] 栈: A 的栈的地址 记录在哪 ? 答: 线程控制块

    [3] 线程控制块 TCB (struct)

    (2) 创建线程

    2 种 方法, 区别: 是否 动态分配 内存

    3大要素(其余不是 线程核心)

    [1] 分配 TCB

        静态分配: 预先定义好 TCB 结构体, thread1
        
        动态分配: thraed2 = rt_thread_create()
    

    [2] 分配 线程栈

        静态分配: 线程栈 起始地址 + stackSize
        
        动态分配: stackSize
    

    [3] 提供 入口函数

    [4] 构造栈内容

    假装 线程入口函数 thread_entry暂停 在其 第1条指令之前, 此时 可以去设置 栈

    保存入口函数地址到 线程栈 中 PC register 值 的位置 . 恢复运行/线程启动 时, 去线程栈里 把 PC 值 = 入口函数地址, 恢复到 CPU PC register, 一恢复 PC register, CPU 就会 从 PC 所指位置运行

    Note: 线程栈 与 函数栈 区别

    函数栈: 第1条汇编 PUSH {LR}, 将 函数返回地址(已 被 caller 保存在 LR) 从 LR write/保存 到 函数栈; 最后1条汇编 POP {PC}, 将 函数放回地址 从函数栈 Read/恢复到 PC register, 一恢复 PC register, CPU 就会 从 PC 所指位置运行

    (3) 线程创建 时的 本质

    (线程)栈的保存 可以用来 构造线程

    Note: 之前内容适用于任何 RTOS, 之后内容专属于 RT-Thread

    创建线程 的 理论分析 .png

    8 创建线程栈的操作

    8.1 静/动态线程 区别: TCB 和 线程栈预先分配好, 还是 动态分配 (malloc)的

    为什么提供2种方式? 答: 有的系统(安全性要求高) 不支持 动态分配内存

    栈: 是一块内存, 可以是 数组 / malloc / 自己指定

    (1) 静态线程

    [1] 初始化: 预先定义好 TCB & 线程栈

        rt_thread_init(&thread1, ...)       
    

    [2] 启动

        rt_thread_startup(&thread1)
    

    (2) 动态线程

    [1] 创建

        thread2 = rt_thread_create() 
        
        rt_thread_create() 
            thread = (struct rt_thread*)rt_object_alloc(...)
            
            stack_start = RT_KERNEL_MALLOC(stack_size)
    

    [2] 启动

        if(thread2 != RT_NULL)
            rt_thread_startup(&thread2)
    

    8.2 TCB(线程结构体 rt_thread) 内部结构 ?

        先推理, 应该有
    

    [1] 某项: 指向 stack 底(起始地址)

    [2] 某项/sp: 指向 栈顶(addr 最小)

    [3] 入口函数指针

    [4] 线程优先级: 可变

    8.3 初始化线程栈 rt_hw_stack_init()

    thread->sp = (void*) rt_hw_stack_init()

    (1) 调整 SP

    (2) 虚构 栈内容: stack_frame = 16个 register 的值

        虚构: 填没有意义的值
    

    (3) 有意义的值在下面设置

    stack_frame

    16个寄存器
    
        R0 - R15 (不含 r13 )
        
        psr: 程序状态寄存器 
            比较结果: CMP R0, R1 
            中断相关
    

    R13(别名 SP, 即 栈指针) 为什么不保存到 stack_frame? 答: 栈指针 保存在 TCB 结构体

    Note: 线程切换时, SP/R13 保存到 线程栈

        规范:
    

    创建线程时, 入口函数 可以 带1个 参数, 保存在 R0(按规范)

    irt_hw_stack_init.png

    8.4 总结: 创建线程 rt_thread_create() 的过程, 就是 构造栈 的过程

    [1] 分配 TCB (rt_thread 结构体): 表示线程

    [2] 分配 线程栈

    [3] 初始化线程栈:构造栈内容

    rt_thread_create() 
        // 1. 分配线程结构体 
        thread = (struct rt_thread *)rt_object_allocate(RT_Object_Class_Thread, name); 
        
        // 2. 分配栈 
        stack_start = (void *)RT_KERNEL_MALLOC(stack_size); 
        
        // 3. 初始化栈, 即 构造栈的内容 
        _rt_thread_init 
            // 3.1 具体操作 thread->sp = (void *)rt_hw_stack_init()
    

    8.5 线程 假装 自己停在 第1条指令前, 怎么假装?

    (1) 构造好栈

    (2) 让 rt_thread->sp 指向 栈顶

    (3) 以后 想运行该线程, 从 rt_thread->sp 处16个 register 的值, 恢复/Read 到 CPU register

    最后 恢复 PC register 的值, 一恢复 PC register, CPU 就会 从 PC 所指位置运行

    线程栈 上保存的 R15/PC 的值线程入口函数指针 => 它 一恢复 PC 寄存器, 线程就运行起来

    9 线程调度 概述

    高优先级的线程 抢占 低优先级的线程, 同优先级的线程 轮流 执行

    怎么体现这一点 ?

    9.1 调度 实质

    9.2 调度 策略

    (1) 可抢占: 高优先级先执行

        一旦高优先级的线程可以运行了, 会马上运行
    

    (2) 轮转: 同级轮流执行

    怎么实现 调度策略 ? 回到 创建线程, 分析 链表

    启动线程 rt_thread_startup(): 实质是把 TCB 放入就绪链表, 还没有开始调度

    rt_thread_startup(thread)
    
        thread->stat = RT_THREAD_SUSPEND;
        rt_thread_resume(thread)
    
            // insert to schedule ready list
            rt_schedule_insert_thread(thread)
    
                rt_list_insert_before()
    

    就绪链表 ReadyList: 对每个优先级的线程, 有1个 ReadyList

    index 越小, 优先级越高

    双向链表: 插入链表 前面 pre == 最后面 ...next

    rt_thread_priority_table[32]
    
        rt_thread_priority_table[0]
    
        rt_thread_priority_table[1]
    
        rt_thread_priority_table[2]     
        
        ...
    
        rt_thread_priority_table[31]
    
    线程启动: 调用链.png 多优先级 线程运行图示.png 多优先级 线程运行过程: 总结.png

    9.3 总结

    (1) 高 优先级(就绪链表中只1个线程): 先运行, 挂起(阻塞), 从就绪链表 移除

    (2) 低优先级: 第1个 Thread 运行一段时间 -> 移到链表尾部 -> 找出链表第1个 thread 来运行(一段时间)-> 移到就绪链表后 -> ...

    10 线程调度 代码分析

    10.1 rt_system_scheduler_start() 启动调度

    rt_schedule()

    (1) 算出 最高就绪优先级 highest_ready_priority

    (2) 从 就绪链表数组 index = 0 开始往后找, 看 哪个链表(最高优先级) 不空, 取出 next指针所指 第1个 Thread

        to_thread = rt_list_entry()
    

    (3) 切换 到新线程 去运行: rt_hw_context_switch_to()

        // switch to new thread:
        rt_hw_context_switch_to()
    

    切换细节: 第3层内容

    10.2 每个 Thread 运行1段时间, 一段时间 怎么描述 ? 答: 用 中断函数 SysTick_Handler

    假设 每隔 1ms (时间间隔可设置) 产生1次中断 => 叫 Tick 中断

    线程运行过程 中不断 产生中断, 有一定时间 处理中断

    10.3 中断函数 SysTick_Handler

    汇编文件中断向量 里有 SysTick_Handler 函数, 当 系统每隔1ms 产生1次中断 时, 中断函数 SysTick_Handler 被调用

    SysTick_Handler()

        rt_tick_increase() 增加1个计数
    

    rt_tick_increase()

    (1) 全局变量 rt_tick(系统时间基准) 加1

    (2) 取出 当前线程

    (3) 看 当前线程 剩余时间(remaining_tick) 用完没 == 当前线程 剩余时间/Tick 减1, 若为 0/用完, yield/让/切换 给别人去运行 rt_thread_yield()

        [1] 判断: 当前线程时间用完没 ?
            
        [2] 未用完: 中断返回 
            
        [3] 用完: yield 切换
    

    rt_thread_yield()

    (1) 从 链表中 把自己 取 出来

    (2) 把自己 放到 链表尾部

    (3) 再次发起 调度 rt_schedule()

    10.4 总结

    线程 切换 的 驱动源 在哪 ? 答: 在 中断函数

    (1) rt_schedule() 抢占/调度: 找出 最高优先级 的那条 ReadyList 中 第1个 Thread 去运行

    (2) 中断函数 SysTick_Handler():当前线程 时间片用完() 后, 调 rt_thread_yield() 给别人去运行

    (3) rt_thread_yield(): 把 当前运行的线程(自己) 放到 ReadyList 尾 部, 再次发起 调度 rt_schedule()

    从 中断函数 到 线程切换.png

    10.5 线程状态切换后 的 内部机制: 以 rt_thread_delay() 分析

    线程运行过程中, 不断有 Tick 中断产生

    当前线程 剩余时间 remaining_tick

        假定 remaining_tick = 15
    

    (1) 正常流程 15 -> 14 -> ... -> 0 -> 切换

    (2) remaining_tick 减为 14 (还没减为 0) 时, 就有 更高优先级的线程就绪, 当前线程 被抢占(不会移到 就绪链表尾), remaining_tick 维持为 14

    (3) 抢占线程 执行完

    (4) 假设又轮到 被抢占线程 运行, 再次去调度: remaining_tick 14 -> 13 -> ... -> 0

    总结: 本来大家排队, 我 (当前优先级最高的就绪链表中 第1个线程) 正在运行, 被 优先级更高的线程 抢占/插队, 重新到 我 的时候, 不应该让我放后面去排队, 这不公平. 插队的人运行完之后, 应该让我继续运行

    抢占: 调 rt_schedule() 即可, rt_schedule() 在哪些地方可能 被调用 ??? 待查

    11 使用 定时器 Delay 原理

    线程函数
        rt_thread_delay() / rt_thread_mdelay(): 单位 Tick / ms  
            rt_thread_sleep(tickNum): 让当前线程休眠
    

    假设 thread1 线程 运行到 tick3 调用 rt_thread_delay(50), 想50个Tick后== Tick53, 进入 ReadyList, 跟别人轮流执行

    11.1 rt_thread_delay(50)

    (1) 从 ReadList 移除

    (2) 启动 定时器

    rt_thread_create() // 创建线程
        _rt_thread_init() // 初始化线程 
            rt_timer_init() // 初始化定时器 
    

    RTT: 每个线程 自带1个 定时器

    freeRTOS: DelayList

    (3) 每个 Tick 判断, 若 超时, 调 超时函数 rt_thread_timeout()

    11.2 rt_thread_timeout()

    (1) 把 线程 放到 ReadyList 尾

        睡1觉起来, 老老实实去后面排队
    

    (2) 发起 调度 rt_schedule()

    定时器原理: rt_thread_delay().png 定时器原理 2.png

    12 使用定时器 Delay 源码分析

    12.1 rt_thread_mdelay(ms) 会把 ms 转换为 Tick 数

        rt_err_t rt_thread_mdelay(rt_int32_t ms)
            rt_tick_t tick = rt_thread_from_millisecond(ms);
            return rt_thread_sleep(tick);
    

    很多 RTOS 刚好是 1ms 产生1个 Tick, Tick 数 == ms 数

    12.2 rt_thread_sleep()

    (1) 从 ReadyList 移除: rt_thread_suspend(thread);

    rt_thread_sleep(rt_tick_t tick)
        rt_thread_suspend(thread);
            rt_schedule_remove_thread(thread)
                rt_list_remove(&pTCB->tlist)
                判断, 
                若 `整个 list 空`, 
                    `清除 (表示 ReadyList 优先级 group 的) 32位整数的某1位(本 list 对应的位)`
    

    为什么能 快速找到 ReadyList 中 最高优先级 ?

    用 1个 32位整数: 表示 ReadyList 中 优先级 group, 第 i 位为 = 1/0, 第 i 条 ReadyList 不空/空

    对于 整数, 有些 处理器, 1条汇编指令 就能计算出 从低到高哪一位 为 1 => 哪条 ReadyList 优先级最高

        rt_list_t rt_thread_priority_table[RT_THREAD_PRIORITY_MAX = 32]
    
        rt_uint32_t rt_thread_ready_priority_group;
    

    (2) 启动定时器: rt_timer_start(&pTCB->thread_timer);

    (3) 每个 Tick 判断, 若 超时, 调 超时函数 rt_thread_timeout() 发起调度: rt_schedule()

    12.3 怎么判定时器 超时/时间到 ?

    1 rt_timer_check()

    rt_tick_increase()
        ... 
        rt_timer_check() // 检查定时器
    

    2 rt_timer_check()

    (1) 循环 从 定时器链表 中 取1个 定时器

    (2) 判断 是否 超时/时间到了 ?

    (3) 是, 则调 定时器的 超时函数 rt_thread_timeout()

    3 rt_thread_timeout(void* para)

        struct rt_thread* thread = (struct rt_thread*)para;
        
        // [1] 设 被唤醒原因: 超时 
        thread->error = -RT_ETIMEDOUT;
        
        // [2] 从 suspendList 中 移除 自己/thread
        rt_list_remove(&thread->tlist) 
        
        // [3] 把 自己 `重新放入 ReadyList 尾部` 
        rt_schedule_insert_thread(thread);
        
        // [4] 发起调度 
        rt_schedule();
    

    线程状态切换 2个核心: 就绪时放 某条 ReadyList; 挂起时, 从 ReadyList 移除

    从 ReadyList 移除后, 何时被唤醒?

    答: 对 Delay 来说, 线程自带定时器, 启动定时器, 每个 Tick 判断 是否超时, 若是, 则调定时器的 超时函数 把 自己 重新放入 ReadyList 尾部, 发起调度

    13 跟 FreeRTOS 简单对比

    创建线程后 放 ReadyList 尾部, RTT 类似

    (1) index = 0 优先级最低: 与 RTT 相反

        pxReadyTasksLists[N]
    

    (2) 创建 task: 后建 task 先执行

    把 task 放入 List 时, 若 新任务优先级 >= (上一个)当前任务的优先级, 当前任务 = 新任务

    开始, List 空 -> 放 Task1 -> curTCB 指向 Task1 -> 加入新任务 Task2(优先级更高) -> curTCB 指向 Task2

    (3) 建 task 时, 后建 task 先执行; 但 后续 还是会 轮流执行

    14 定时器 的 链表操作

    定时器 实现: 链表

    14.1 怎么启动定时器 ?

    1 先看 结果

    Tick: check Timer

    (1) 从 哪里 (where) 找 Timer ? 显然 有个 链表(TimerList)

    启动 定时器 核心: 放入 链表(TimerList)

    (2) 怎么 check ?

    从 List 里取出来, 比较 时间是否到了 ?

    为了效率, 可能 只比较 第1个, 即 时间最近的, 先不 care 内部实现

    2 再看 怎么启动定时器: 放入 链表(TimerList)

    启动定时器.png

    15 引入 线程间通信 的原因

    (1) 可 互斥 访问: 保证 结果 符号预期(2个线程均 write 全局变量)

    (2) 有 休眠-唤醒 功能: 高效使用 CPU (对 全局变量, 线程 A 只 write, B 只 read )

    15.1 反例: 没互斥

    多线程

    int a = 1;
    
    void add_val()
    {
        a = a+1; 
    }
    

    1. 1条加语句 a = a+1; 分解为 3条汇编指令

        (1) Read a: LDR R0, [a]
    
        (2) ADD:    ADD R0, R0, #1
    
        (3) Write:  STR R0, [a]
    

    2. 线程 A/B 时间轴

    (1) A (1) 切换 => 保存现场: R0 = 1 被保存到 A的栈

    (2) B (1)(2)(3): a = 2

    (3) A 恢复现场: R0 恢复为 1 -> (2)(3) a = 2

    本意: 线程 A B 各执行1次, 均加1, 结果为 3; 但现在 a = 2, 不是期望的结果

    2个线程都 write 全局变量, 若 没有 互斥 操作, 结果可能非预期 => 要引入 互斥 操作

    15.2 线程A set 全局变量, 是为了 通知 线程B 去 doSth()

    对 全局变量, 线程 A 只 write, B 只 read, 无冲突; 但 线程B 死等 => 浪费 CPU => 要引入 休眠-唤醒 机制

    int a = 0;
    
    void threadAFunc()
    {
        while(1)
        {
            ...
            a = 1;
            ...
        }
    }
    
    void threadAFunc()
    {
        while(1)
        {
            while(a != 1); // 死等: 浪费 CPU 
            doSth();
        }
    }
    

    15.3 RT-Thread 线程间通信机制

        信号量 
    
        互斥量 
    
        事件到
    
        邮箱 
    
        消息队列 
    

    16 (消息)队列操作 的 原理

    16.1 队列 里有什么 ?

    (1) 存储空间 / 消息块 / buf: 放 消息/data

    (2) 多少个消息块: 可设置

    (3) 每个消息块多长(等长): 就是一块内存

    16.2 msgQueue 之 生产者/消费者

    线程A 写 
    
        有空间: 成功
        
        无空间 
            返回 Err
            
            等待(1段时间)
                [1] B 读走 data, 问: 唤醒谁 ? 唤醒 A
                
                [2] 超时返回 Err 
                
    线程B 读 
        有数据: 成功 
        
        没数据
            返回 Err
            
            等待(1段时间)
                [1] A 写入 data, 问: 唤醒谁 ? 唤醒 B
                
                [2] 超时返回 Err 
    

    16.3 怎么理解该 队列?

    2个要点: 链表 & 定时器

    A B 之间怎么知道对方的存在 ?

    Queue 里应该由 2个 List: SenderList / ReceiverList

    struct rt_messagequeue
    {
        //(1) 从这里可以找到: read 此队列不成功的 thread 
        struct rt_ipc_object parent; 
        
        // ...
        
        //(2) 从这里可以找到: write 此队列不成功的 thread 
        rt_list_t suspend_sender_thread; 
    };
    
    struct rt_ipc_object
    {
        rt_list_t suspend_thread; 
    };
    

    16.4 consumer 接收消息: rt_mq_recv(&mq, &buf, sizeof(buf), 5)

    线程函数 调 rt_mq_recv()

    (1) mq 空 ?

    (2) 愿意等 ?

    (3) 挂起

    1)从 ReadyList 移除

    2)放入 SuspendList: mq->parent->suspend_thread, 以后 别人才能找到

    3)启动线程 自己的定时器

    (4) 被唤醒

    1)其它 thread 写 Queue, 会去 SuspendList / mq->parent->suspend_thread 取出 thread, 唤醒

    2)被自带定时器 唤醒

    17 队列操作 的 代码分析

    17.1 rt_mq_recv(): Read data

    rt_mq_recv() 
        
        队列空 
            若 不等待, 直接返回 
            
            若 等待
                // 挂起当前线程
                rt_ipc_list_suspend() 
                
            若 超时时间 > 0
                // 启动定时器
                rt_timer_start()
        
            // 调度: 切出去 / 休眠 
            rt_schedule()
            
            // 后续: 切回来 
            判 什么原因导致 重新运行? 
                若 发生错误, 直接返回
                若 OK: 说明 是被其他 thread 唤醒 
                    copy data
                    return ok   
    

    17.2 rt_ipc_list_suspend(): 挂起 thread

    rt_ipc_list_suspend()
    
        (1) thread 从 ReadyList 中 移除 
    
        (2) thread 放入 SuspendList: mq->parent->suspend_thread 
        
            flag 决定 位置 
            
                case FIFO:  
            
                case PRIO: 优先级
    

    17.3 rt_mq_send_wait(): Write data

    rt_mq_send_wait()
    
        (1) 从 SuspendList 中取出 因 mq 空 而切出去的 consumer thread
        
        (2) 将 consumer thread 重新放回 ReadyList 
        rt_ ipc_list_resume() 
            
            // [1] 从 SuspendList 移除
            rt_list_remove(&thread->tlist)
            
            // [2] 重新放入 ReadyList
            rt_schedule_insert_thread(thread)
    

    18 队列操作 内部消息块 管理

    18.1 Write data 到 mq / Read msg 从 mq

    (1) 各 消息块 内存连续, 但组织为 链表

    (2) rt_messagequeue 中有 3根指针: mq_queue_free / mq_queue_head / mq_queue_tail 指向 空闲/头/尾 msgBlock

    (3) 线程 A: Write data 到 mq

        1) 从 mq 中 取出 first MsgBlock, mq_queue_free 指向 next msgBlock
        
        2) 把 data 写/copy 进去 
        
        3) 更新 mq_queue_tail( 和 mq_queue_head, mq_queue_head == mq_queue_tail == NULL 时)
        
    

    (4) 线程 B: Read msg 从 mq

        1) 从 mq_queue_head 开始 Read
        
        2) 更新 mq_queue_head( 和 mq_queue_tail, mq_queue_head == mq_queue_tail == NULL 时)
    

    3)读完后的 msgBlock 归还给 freeBlock

    考虑到 效率, 应该 直接放到 mq_queue_free 前: T(n) = O(1)

    18.2 rt_mq_create()

    (1) 分配 rt_messagequeue 结构体

    (2) 分配空间: msgNum * (msg 头+有效内容)

    (3) 连成 memoryPool: 倒着链 => mq_queue_free 指向 the last msgBlock( addr 最大 ): 倒着指

    18.3 互斥 怎么体现 ? 简单粗暴: 关中断

    (1) 想 Write data 时, 关中断 rt_hw_interrupt_disable(),

    在 开中断之前 不受干扰

    1) 中断不能发生, 中断 干扰不了

    2) 其他 thread 无法运行: 没中断, 无法切换

    消息队列操作 内部消息块 管理 .png msgBlocksMemoryPool.png

    19 答疑

    (1) 互斥

    线程 A/B 都想 Write mq, 都想获得 空闲 msgBlock

    (2) 互斥量 怎么实现?

    [1] 关中断

    [2] 有些 处理器 支持一些 汇编指令, 可 原子地修改 变量

    part 2

    (1) 信号 是 异步 机制, 其余是 同步机制

        邮箱
        
        信号量 
    
        互斥量 
    
        事件到
    
        消息队列 
    
        信号
    

    (2) RTT 最有特色的地方/比 FreeRTOS 强大的地方

    设备驱动框架, 可构造出庞大的 生态

    20 邮箱(mailbox) 的 引入

    20.1 mq 与 mailbox 唯一差别: data 的存储不同, elemSize 可指定的 数组-链表 / unsigned long 数组

    (1) mq 可指定

        [1] 队列中 元素数
        
        [2] 每个元素 size 
    

    data write/放 进去, Read 出来: 都用 memcpy

    线程间 传 小 data(如 int 型), 用 memcpy 效率低 -> 引入: mailbox (struct)

    (2) mailbox

    unsigned long(整型)数组: 每个元素 只能是 unsigned long(整型)

    mq: memcpy -> mailbox: 赋值

    Write: buf[somePos] = val

    Read: val = buf[somePos]

    21 邮箱(mailbox) 内部机制: 怎么操作邮箱

        threadA: Write data 
    
        threadB: Read data
    
        假设 运行顺序
    

    21.1 B: Read mailbox

    // mailbox 是否 `空` ?
    空 
        
        // 是否愿意等? : 用1个 参数 表示
        (1) 不等, return Err  
        
        (2) 等 
    
            1) B 进入 `阻塞`: `从 ReadyList 移除`
            
                A Write data 后, 应该去 mailbox 里的 List 把 wait data 的 thread 唤醒 
                
            2) `把 自己/thread 记录` 在 `mailbox 的 某个 List`(`核心1: List`) (为了让 Producer 能找到我)
    
        (3) `再次运行`: 有 2种情况 
    
            1) if(thread->status == 某个错误码/ETIMEDOUT): 超时退出, return Err
             
                愿意等待多久? 指定 超时时间: 如 5s, 5s 内没人来 Write mailbox, 
                    超时时间到, 被 `定时器`(`核心2: Timer`) 唤醒, return Err 
                
            2) 被 sender 唤醒 
    
                超时时间内, 有人来 Write mailbox, Writre 后, 把我唤醒
                    
                [1] Read data: val = buf[], `核心 3: buf` 
                    
                [2] return OK
    

    我/ReadThread 因为 Read data 没有空间 而进入 阻塞 态, 我能 再次运行 时, 我 怎么知道 我是 超时退出 ?

    答: 必定有 判断 (thread) 状态, 状态 == 某个错误码/ETIMEDOUT, 就知道是因为超时而退出

    何时设 (thread)状态?

    超时处理函数 把 阻塞 thread 放回 ReadyList 前, 设 thread->status = ETIMEDOUT

    21.2 A: Write mailbox 与 Read mailbox 对称

    // mailbox 是否满?
    满 
        // 是否愿意等? : 用1个 参数 表示
        (1) 不等, return Err  
    
        (2) 等 
    
            1) 进入阻塞: `从 ReadyList 移除`
                
            2) `把 自己/thread 记录` 在 mailbox 的 `another List`
                
        (3) 再次运行: 有 2种情况 
    
            1) 超时退出, return Err
             
            2) 被 Receiver 唤醒 
    
                [1] Write data: buf[] = val, 核心 3: buf 
                    
                [2] return OK
    

    21.3 核心: 链表、定时器、环形 buf

    (1) 环形 buf 概念

    开始时, ReadPos/WritePos = 0: 里面没 data

    Write:
        buf[writePos] = val 
        writePos = (writePos+1) % BufSize;
        
        // <=> 
        writePos = writePos + 1;
        if (writePos = BufSize)
            writePos = 0
    
    Read 
        类似 Write
    

    => mailbox 肯定有 ReadPos & WritePos

    (2) Timer

        1) 线程 愿意等待 10 Tick, 线程里自带 timer
     
        2) timer->tick 设为 10s, 每个 `Tick 中断`, timer->tick 减1
        
        3) 当 `timer->tick 减为 0 时`, timer 的 `超时处理函数` 被调用
    
            1] 设 thread->status = ETIMEDOUT 错误码
            
            2] 把 自己/thread 放回 ReadyList
    

    21.4 互斥: A/C 都想 Write -> 关中断

    Write mailbox 的 func: 先 关中断

    24 信号量 (semaphore) 内部机制

    传 大/小 data: mq/mailbox

    不想传 data, 只想表示我有 多少 resource: semaphore

    图
        A 车     ->      停车场: 3个位置  ->       B 车出
        
        休眠          满                       无休眠 
                        只能:
                        出口处: 出去一辆
                        入口处: 进来一辆           
    

    信号量里面

        (1) 只1个 List
    
        (2) value: 表示 停车场里 `有多少 空位`
    

    24.1 (A 想) 获取 信号量

    if(value > 0)
        value--
        return OK 
    else // 满
        (1) if(不愿等待: timeout == 0)
            return Err  
            
        (2) else // 愿意等 
            
            [1] 从 ReadyList 移除: `休眠`  
            
            [2] 把自己/thread 放到 信号量的 List 
            
    (3) 再次运行 // `被唤醒` 
    
    if(thread->status == ETIMEDOUT) // 超时唤醒 
        return Err
    else 
        value--
        return OK 
    

    整个流程跟 生活场景 很像

    24.2 (B 想) 释放 信号量

    停车场里面一辆车走了之后, 要释放信号量

    value++
    if(list 上有 thread) // 判是否有人在 等待 
        wake_up(thread)
    

    24.3 信号量核心

    (1) 只1个 List: 用来 维持 两边(入口/出口)的 threads

    (2) value: 表示 resources 有多少 空位

    (3) timer: 线程自带的 timer, 跟信号量本身无关

    26 互斥量(mutex) 的引入

    互斥: 我拿到这个东西之后, 你就拿不到了

    量: 它有1个数量, 要么 0 要么 1

    停车场场景: 让 停车场 车位数 = 1 -> 好像是 mutex, 但 mutex: 并不只是把 resource 数限制为 1, 还有其他作用

    26.1 信号量 缺点

        入口                  出口 
        rt_sem_take()       rt_sem_release()
    

    场景 换为 厕所

    A 打开门进去, 按理说 只有 A 才可开门, 但 B 有备用钥匙, 直接开门了

    main()
        
        创建 信号量: 设 value = 1
            
    A: 要上厕所                         B: 要上厕所     
                                
        rt_sem_take() 进来                rt_sem_release() 错误地调用了 release
                                            
        ...                                 rt_sem_take() 进来
                                            ...
                                            
        rt_sem_release() 出来             rt_sem_release() 出来
    
    

    结果: A B 都进了厕所

        => 信号量 `缺点`
    

    (1) 谁(可能并不 own resource) 都可以 release()

    对于 互斥 resource, A 拥有 resource, 应该只由 A 释放 resource, 但 信号量 机制并没有这种保护措施, 谁都可以 release()

    (2) 优先级反转

                                                            take semaphore(A): 失败, 休眠, 让出 CPU, 让 MP 运行 
    HighPriority: HP                                      —————————————————
                                        
                                         不必 take semaphore(A)               MP task 一直运行, LP task 一直没机会运行
    MidPriority : MP                     —————————————————                   ——————————————————————————————————
                        
                        take semaphore(A)
    LowPriority : LP    —————————————————
                        ————————————————————————————————————————————————————————————————————————————————————————> t
    

    RTT: 只要 更高优先级的 task 就绪, 就可以马上抢占 低优先级的 task

    LP task 一直没机会运行, 没法释放 信号量, 没法 唤醒 HP task

    HP task 被 MP task 抢占/反转

    解决: 用 互斥量

    26.2 互斥量: 使用 优先级继承

    小职员(LP task) 继承了 大领导(HP task) 的优先级

    HP task: 提升 mutex 拥有者 的 优先级, 目的想 让他尽快执行, 尽快释放我想得到的 resource, 可避免 MP task 来反转我

                                                            take mutex(A): [1] 失败, 休眠, 让出 CPU, 让 MP 运行 
                                                                           [2] `提升 mutex 拥有者 的 优先级`
    HighPriority: HP                                      —————————————————             —————————————————
                                        
                                         不必 take mutex(A)               
    MidPriority : MP                     —————————————————                   
                        
                        take mutex(A)                                       release mutex [1] 唤醒 MP 
                                                                                          [2] `恢复 优先级`
    LowPriority : LP    —————————————————                                   ————————————
    
                        ————————————————————————————————————————————————————————————————————————————————————————> t
    

    26.3 mutex 核心

    (1) 谁拥有, 谁释放

    (2) 优先级继承

    本文参考 韦东山视频教程, 禁止用于商业等用途

    相关文章

      网友评论

        本文标题:RT-Thread(RTT) 内部机制

        本文链接:https://www.haomeiwen.com/subject/uhatwrtx.html