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
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
(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: LR
在 main 的 caller 中 BL/跳转 到 main
时, 已 保存了
main 的 next 指令地址
, 以保证 main 执行结束
时, 能 跳回(PC = LR)
到 main 的 next 指令地址 去执行
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 对应的指令)
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
还会保存 更多寄存器
, 如 程序状态寄存器
, 这里先不考虑这些
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
创建线程 的 理论分析 .png8 创建线程
时 栈的操作
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
(按规范)
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()
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)
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 无法运行
: 没中断, 无法切换
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) 优先级继承
网友评论