RT-Thread(RTT) 内部机制

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) 优先级继承

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,504评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,434评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,089评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,378评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,472评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,506评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,519评论 3 413
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,292评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,738评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,022评论 2 329
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,194评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,873评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,536评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,162评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,413评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,075评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,080评论 2 352

推荐阅读更多精彩内容

  • 线程管理 在日常生活中,我们要完成一个大任务,一般会将它分解成多个简单、容易解决的小问题,小问题逐个被解决,大问题...
    Swinner阅读 1,251评论 0 0
  • RT-Thread workqueue 详解 在学习之前可以先去了解一下工作队列的使用场景:工作队列 ( work...
    tang_jia阅读 2,153评论 0 2
  • RT-Thread简介 1.关键词 国产,嵌入式操作系统 RT-Thread:内核,网络,fs,gui 开发环境:...
    心远气自静阅读 1,188评论 0 0
  • 1 准备好开发环境,Keil IDE 2 基于您的STM32F103芯片的开发板实现简单的工程,如串口打印和LED...
    大象奔跑阅读 3,673评论 0 0
  • 创建进程 使用fork函数创建进程int pid = fork();在执行此函数后,即从当前进程开了一个新的子进程...
    西山薄凉阅读 299评论 0 0