1 RT-Thread 介紹
1.1 RT-Thread
線程管理
調度
線程間通信(郵箱/消息隊列/信號)
線程間同步(信號量/互斥量/事件集)
核心
都是 鏈表 & 定時器
1.2 3個層次
(1) 會用 API
(2) 懂 內部機制
(3) 掌握代碼實現(xiàn)細節(jié), 能移植
前2個層次可速成: 10 幾個小時足夠
2 RTOS 的引入
喂飯 和 回消息
2.1 單線條
喂飯 -> 回消息 -> 喂飯 -> 回消息 -> ...
2件事有影響
: 喂飯慢了, 同事以為在敷衍; 回消息慢了, 小孩餓哭
2.2 多線條: 每個 task 拆分為多個步驟, 2個 task 步驟交錯
=> 假象: 2個 task 好像同時進行
喂飯 = 舀飯 + 把飯放小孩口里 + 舀菜
回消息 = 打幾個字 + 打幾個字 + 打1條消息的最后幾個字
舀飯 -> 打幾個字 -> 把飯放小孩口里 -> 打幾個字 -> 舀菜 -> 打1條消息的最后幾個字 -> ... 循環(huán)
2.3 前后臺
前臺: 觸發(fā)中斷的事件 (按鍵 / 觸摸屏幕 / 事件到)
后臺: 中斷服務程序
缺點: 若 某中斷 處理時間長
, 其余中斷 和 main 的 while
都會被 卡住
=> 系統(tǒng)卡頓
int main()
{
// key isr
// touch isr
// timer isr
while()
{
...
}
}
改進: 中斷處理程序 只設 flag, while 循環(huán)據(jù) 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 做了什么, 再切回來時, 才能 恢復(之前)現(xiàn)場
3.1 程序運行
(1) 運行線程/taskA
(2) 保存
A 的 現(xiàn)場
(3) 運行 B : 恢復 B 的現(xiàn)場
(4) 保存 B 的現(xiàn)場
(5) 運行 A : 恢復
A 的現(xiàn)場
3.2 簡單示例
(1) 什么叫任務, 什么叫線程
RTT里 task 和 線程 是同一個東西
什么叫線程缤至?先想 怎么 切換&保存
線程 ?
線程是函數(shù)嗎细疚?不是
(2) 保存線程 要 保存什么 ? 保存在哪 ?
1)函數(shù)本身
在 Flash 上, 無需保存
2)函數(shù)執(zhí)行到了哪 (是 CPU寄存器: "PC")
需要保存
3)函數(shù)里用到 全局變量
在內存上, 無需保存
4)函數(shù)里用到 局部變量
在棧/內存里, 無需保存, 只要避免棧不被破壞
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 之間有 地址總線 & 數(shù)據(jù)總線
(2) CPU 想訪問這些設備, 會 發(fā)出地址(Addr)
, 會 得到數(shù)據(jù)(Data)
(3) 對 ARM 芯片
, 對 精簡指令集
, CPU 對 內存 只有2個功能: read/write
(4) 對數(shù)據(jù)的 運算
在 CPU 內部執(zhí)行
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: 取指、執(zhí)行
從 Flash 對 `機器碼`, 取指哗魂、譯碼(`匯編`指令)印荔、執(zhí)行
(2) ALU: 運算單元
arm通用寄存器 別名 意義
R# APCS別名 意義
R0 a1 參數(shù)/結果/scratch寄存器 1
R1 a2 ...2
R2 a3 ...3
R3 a4 ...4
R4 v1 arm狀態(tài)局部變量寄存器 1
R5 v2 ...2
R6 v3 ...3
R7 v4/wr ...4 / thumb狀態(tài)工作寄存器
R8 v5 ...5
R9 v6/sb ...6 / 在支持RWPI的ATPCS中作為靜態(tài)基址寄存器
R10 v7/sl ...7 / 在支持數(shù)據(jù)棧檢查的ATPCS中作為數(shù)據(jù)棧限制指針
R11 v8/fp ...8 / 幀指針
R12 ip 內部過程調用 scratch 寄存器
R13 sp 棧指針
R14 lr 鏈接寄存器
R15 pc 程序計數(shù)器
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 發(fā)出 地址信號, addr = R3
Note: LDR 與 mov 區(qū)別
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 內部實現(xiàn)
(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} 取出內存棧的數(shù)據(jù) 放入 R0, R1
R0 = [SP]
SP = SP + 4
R1 = [SP]
SP = SP + 4
POP 本質是 讀內存 = 多次調 LDR + 調整 SP
(5) 跳轉
BL A
[1] 記錄 返回地址(next 指令地址)
=> 保存到 R14/LR
[2] 執(zhí)行 A
Note: A 執(zhí)行完, 跳回到 R14
所存指令地址 去執(zhí)行
func()
{
A(); BL A
B(); B
A(); BL A
C(); C
}
5 簡單 C 函數(shù) 反匯編
分析
用該程序講 怎么保存1個 task
程序本質: 一系列運算, CPU 根據(jù) 指令, 讀內存, 運算, 寫內存
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 執(zhí)行結束
時, 能 跳回(PC = LR)
到 main 的 next 指令地址 去執(zhí)行
5.2 add_val 匯編
(1) 第1條 匯編指令 push {r3, lr}
LR: add_val 的 caller(main) 中 BL/跳轉 到 add_val
時, add_val 的 返回地址(next 指令地址) 已被保存到 LR/R14
第一條指令又將 LR 的值 Write 到 函數(shù)棧上
(2) 最后1條 匯編指令 pop {r3, pc}: R3 = 3, PC = 函數(shù)棧上 LR 的值 = add_val 的 next 指令
, CPU 接著執(zhí)行 add_val 的 next 指令
(return 0 對應的指令)
5.3 局部變量 怎么體現(xiàn)?
匯編
代碼 構造好 data
, 把 data 保存進 棧(的某處內存)
5.4 函數(shù)參數(shù): 第1/2個參數(shù) 保存在 R0/R1
下來可以討論 什么是 task/線程? 怎么保護 task/線程 ?
6 保存現(xiàn)場 (上下文)
6.1 假設在 執(zhí)行完 ADD r2,r2,r3
這條指令后, 切換
[1] 先保存: 保存什么 ? 所有 register. 保存到哪 ? 線程棧上
[2] 執(zhí)行別的代碼
[3] 后恢復: 切換回來, 重新執(zhí)行 切換前下面的代碼時, 從 棧里把 保存的寄存器 都恢復回去
在切換時刻, 可以假裝有一個 時間機器
, 讓一切都停止了, 此時要 保存現(xiàn)場
6.2 保存現(xiàn)場, 需要保存什么 ?
`CPU 算出來的新值` 還沒有寫入 局部變量, 就切換了
(1) 局部變量: 不需要保存, 只要保證 執(zhí)行別的代碼時 不用破壞 局部變量即可
(2) R2: 存 CPU 計算出的 中間結果, 要保存
(3) SP 要保存
本例, 因為 ADD r2,r2,r3 之后只用到 R2 SP 這2個寄存器
普適情況: 切換 可能在任何地方
切換發(fā)生的 時間停止瞬間, 要保存 所有 register
保存到哪里 ? 棧上 一片連續(xù)空間: 用 SP 分配一塊空間, SP = SP - 16*4
還會保存 更多寄存器
, 如 程序狀態(tài)寄存器
, 這里先不考慮這些
7 創(chuàng)建線程 的 理論分析
7.1 什么叫線程? 怎么保存線程 ?
現(xiàn)在可以回答這個問題了
什么叫線程: 運行中
的函數(shù)恶复、被 暫停運行
的函數(shù)
怎么保存線程:把 暫停瞬間的 CPU 寄存器值
, 保存進 棧
里
7.2 RT-Thread 里 怎么 創(chuàng)建線程
(1) 線程 A: 3要素
[1] 入口函數(shù)
[2] 棧:
A 的棧的地址 記錄在哪
? 答: 線程控制塊
[3] 線程控制塊 TCB
(struct)
(2) 創(chuàng)建線程
2 種 方法, 區(qū)別: 是否 動態(tài)分配 內存
3大要素(其余不是 線程核心
)
[1] 分配 TCB
靜態(tài)分配: 預先定義好 TCB 結構體, thread1
動態(tài)分配: thraed2 = rt_thread_create()
[2] 分配 線程棧
靜態(tài)分配: 線程棧 起始地址 + stackSize
動態(tài)分配: stackSize
[3] 提供 入口函數(shù)
[4] 構造棧內容
假裝 線程入口函數(shù) thread_entry
被 暫停
在其 第1條指令之前, 此時 可以去設置 棧
要 保存入口函數(shù)地址到 線程棧 中 PC register 值 的位置 . 恢復運行/線程啟動 時, 去線程棧里 把 PC 值 = 入口函數(shù)地址, 恢復到 CPU PC register, 一恢復 PC register, CPU 就會 從 PC 所指位置運行
Note: 線程棧 與 函數(shù)棧
區(qū)別
函數(shù)棧: 第1條匯編 PUSH {LR}
, 將 函數(shù)返回地址
(已 被 caller 保存在 LR
) 從 LR
write/保存 到 函數(shù)棧
; 最后1條匯編 POP {PC}
, 將 函數(shù)放回地址
從函數(shù)棧 Read/恢復到 PC register, 一恢復 PC register, CPU 就會 從 PC 所指位置運行
(3) 線程創(chuàng)建 時的 本質
(線程)棧的保存
可以用來 構造線程
Note: 之前內容適用于任何 RTOS, 之后內容專屬于 RT-Thread
8 創(chuàng)建線程
時 棧的操作
8.1 靜/動態(tài)線程 區(qū)別: TCB 和 線程棧
是 預先分配好, 還是 動態(tài)分配 (malloc)的
為什么提供2種方式? 答: 有的系統(tǒng)(安全性要求高) 不支持 動態(tài)分配內存
棧: 是一塊內存, 可以是 數(shù)組 / malloc / 自己指定
(1) 靜態(tài)線程
[1] 初始化: 預先定義好 TCB & 線程棧
rt_thread_init(&thread1, ...)
[2] 啟動
rt_thread_startup(&thread1)
(2) 動態(tài)線程
[1] 創(chuàng)建
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] 入口函數(shù)指針
[4] 線程優(yōu)先級: 可變
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: 程序狀態(tài)寄存器
比較結果: CMP R0, R1
中斷相關
R13(別名 SP, 即 棧指針
) 為什么不保存到 stack_frame?
答: 棧指針
保存在 TCB 結構體
Note: 線程切換時, SP/R13 保存到 線程棧
規(guī)范:
創(chuàng)建線程時, 入口函數(shù)
可以 帶1個 參數(shù), 保存在 R0
(按規(guī)范)
8.4 總結: 創(chuàng)建線程 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 的值
是 線程入口函數(shù)指針
=> 它 一恢復 PC 寄存器, 線程就運行起來
9 線程調度 概述
高優(yōu)先級的線程 搶占
低優(yōu)先級的線程, 同優(yōu)先級的線程 輪流
執(zhí)行
怎么體現(xiàn)這一點 ?
9.1 調度 實質
9.2 調度 策略
(1) 可搶占: 高優(yōu)先級先執(zhí)行
一旦高優(yōu)先級的線程可以運行了, 會馬上運行
(2) 輪轉: 同級輪流執(zhí)行
怎么實現(xiàn)
調度策略 ? 回到 創(chuàng)建線程
, 分析 鏈表
啟動線程 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:
對每個優(yōu)先級的線程, 有1個 ReadyList
index 越小, 優(yōu)先級越高
雙向鏈表: 插入鏈表 前面 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]
9.3 總結
(1) 高 優(yōu)先級(就緒鏈表中只1個線程): 先運行, 掛起(阻塞)
, 從就緒鏈表 移除
(2) 低優(yōu)先級: 第1個 Thread 運行一段時間 -> 移到鏈表尾部 -> 找出鏈表第1個 thread 來運行(一段時間)
-> 移到就緒鏈表后 -> ...
10 線程調度 代碼分析
10.1 rt_system_scheduler_start() 啟動調度
rt_schedule()
(1) 算出 最高就緒優(yōu)先級 highest_ready_priority
(2) 從 就緒鏈表數(shù)組
index = 0 開始往后找, 看 哪個鏈表(最高優(yōu)先級) 不空
, 取出
next指針所指 第1個 Thread
to_thread = rt_list_entry()
(3) 切換
到新線程 去運行
: rt_hw_context_switch_to()
// switch to new thread:
rt_hw_context_switch_to()
切換細節(jié): 第3層內容
10.2 每個 Thread 運行1段時間
, 一段時間 怎么描述 ? 答: 用 中斷函數(shù) SysTick_Handler
假設 每隔 1ms (時間間隔可設置) 產生1次中斷
=> 叫 Tick 中斷
線程運行過程
中不斷 產生中斷
, 有一定時間 處理中斷
10.3 中斷函數(shù) SysTick_Handler
匯編文件
里 中斷向量
里有 SysTick_Handler 函數(shù), 當 系統(tǒng)每隔1ms 產生1次中斷
時, 中斷函數(shù) SysTick_Handler 被調用
SysTick_Handler()
rt_tick_increase() 增加1個計數(shù)
rt_tick_increase()
(1) 全局變量 rt_tick(系統(tǒng)時間基準)
加1
(2) 取出 當前線程
(3) 看 當前線程 剩余時間(remaining_tick) 用完沒
== 當前線程 剩余時間/Tick 減1, 若為 0/用完, yield/讓/切換 給別人去運行 rt_thread_yield()
[1] 判斷: 當前線程時間用完沒 ?
[2] 未用完: 中斷返回
[3] 用完: yield 切換
rt_thread_yield()
(1) 從 鏈表中 把自己 取 出來
(2) 把自己 放到 鏈表尾部
(3) 再次發(fā)起 調度
rt_schedule()
10.4 總結
線程 切換 的 驅動源
在哪 ? 答: 在 中斷函數(shù)
里
(1) rt_schedule() 搶占/調度
: 找出 最高優(yōu)先級
的那條 ReadyList 中 第1個 Thread
去運行
(2) 中斷函數(shù) SysTick_Handler():
判 當前線程 時間片用完()
后, 調 rt_thread_yield() 讓
給別人去運行
(3) rt_thread_yield(): 把 當前運行的線程(自己) 放到 ReadyList 尾
部, 再次發(fā)起 調度 rt_schedule()
10.5 線程狀態(tài)切換后 的 內部機制: 以 rt_thread_delay() 分析
線程運行過程中, 不斷有 Tick 中斷產生
當前線程 剩余時間 remaining_tick
假定 remaining_tick = 15
(1) 正常流程
15 -> 14 -> ... -> 0 -> 切換
(2) remaining_tick 減為 14 (還沒減為 0) 時, 就有 更高優(yōu)先級的線程就緒
, 當前線程 被搶占(不會移到 就緒鏈表尾)
, remaining_tick 維持為 14
(3) 搶占線程
執(zhí)行完
(4) 假設又輪到 被搶占線程
運行, 再次去調度: remaining_tick 14 -> 13 -> ... -> 0
總結: 本來大家排隊, 我 (當前優(yōu)先級最高的就緒鏈表中 第1個線程) 正在運行, 被 優(yōu)先級更高的線程 搶占/插隊
, 重新到 我 的時候, 不應該讓我放后面去排隊, 這不公平
. 插隊的人運行完之后, 應該讓我繼續(xù)運行
搶占: 調 rt_schedule() 即可, rt_schedule() 在哪些地方可能 被調用 ??? 待查
11 使用 定時器 Delay
原理
線程函數(shù)
rt_thread_delay() / rt_thread_mdelay(): 單位 Tick / ms
rt_thread_sleep(tickNum): 讓當前線程休眠
假設 thread1 線程 運行到 tick3 調用 rt_thread_delay(50), 想50個Tick后== Tick53, 進入 ReadyList, 跟別人輪流執(zhí)行
11.1 rt_thread_delay(50)
(1) 從 ReadList 移除
(2) 啟動 定時器
rt_thread_create() // 創(chuàng)建線程
_rt_thread_init() // 初始化線程
rt_timer_init() // 初始化定時器
RTT: 每個線程 自帶1個 定時器
freeRTOS: DelayList
(3) 每個 Tick 判斷, 若 超時, 調 超時函數(shù) rt_thread_timeout()
11.2 rt_thread_timeout()
(1) 把 線程 放到 ReadyList 尾
部
睡1覺起來, 老老實實去后面排隊
(2) 發(fā)起 調度
rt_schedule()
12 使用定時器 Delay 源碼分析
12.1 rt_thread_mdelay(ms) 會把 ms 轉換為 Tick 數(shù)
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 數(shù) == ms 數(shù)
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 優(yōu)先級 group 的) 32位整數(shù)的某1位(本 list 對應的位)`
為什么能 快速找到 ReadyList 中 最高優(yōu)先級
?
用 1個 32位整數(shù): 表示 ReadyList 中 優(yōu)先級 group, 第 i 位為 = 1/0, 第 i 條 ReadyList 不空/空
對于 整數(shù)
, 有些 處理器
, 1條匯編指令
就能計算出 從低到高哪一位 為 1 => 哪條 ReadyList 優(yōu)先級最高
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 判斷, 若 超時, 調 超時函數(shù) rt_thread_timeout()
發(fā)起調度: rt_schedule()
12.3 怎么判定時器 超時/時間到 ?
1 rt_timer_check()
rt_tick_increase()
...
rt_timer_check() // 檢查定時器
2 rt_timer_check()
(1) 循環(huán) 從 定時器鏈表
中 取1個 定時器
(2) 判斷 是否 超時/時間到了 ?
(3) 是, 則調 定時器的 超時函數(shù) 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] 發(fā)起調度
rt_schedule();
線程狀態(tài)切換 2個核心: 就緒時放 某條 ReadyList; 掛起時, 從 ReadyList 移除
從 ReadyList 移除后, 何時被喚醒?
答: 對 Delay 來說, 線程自帶定時器, 啟動定時器, 每個 Tick 判斷 是否超時, 若是, 則調定時器的 超時函數(shù)
把 自己 重新放入 ReadyList 尾部
, 發(fā)起調度
13 跟 FreeRTOS 簡單對比
創(chuàng)建線程后 放 ReadyList 尾部, RTT 類似
(1) index = 0 優(yōu)先級最低: 與 RTT 相反
pxReadyTasksLists[N]
(2) 創(chuàng)建 task: 后建 task 先執(zhí)行
把 task 放入 List 時, 若 新任務優(yōu)先級 >= (上一個)當前任務的優(yōu)先級, 當前任務 = 新任務
開始, List 空 -> 放 Task1 -> curTCB 指向 Task1 -> 加入新任務 Task2(優(yōu)先級更高) -> curTCB 指向 Task2
(3) 建 task 時, 后建 task 先執(zhí)行
; 但 后續(xù)
還是會 輪流執(zhí)行
14 定時器 的 鏈表操作
定時器 實現(xiàn): 鏈表
14.1 怎么啟動定時器 ?
1 先看 結果
Tick: check Timer
(1) 從 哪里 (where) 找 Timer ? 顯然 有個 鏈表(TimerList)
啟動 定時器 核心: 放入 鏈表(TimerList)
(2) 怎么 check ?
從 List 里取出來, 比較 時間是否到了
?
為了效率, 可能 只比較 第1個, 即 時間最近的
, 先不 care 內部實現(xiàn)
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) 切換 => 保存現(xiàn)場: R0 = 1 被保存到 A的棧
(2) B (1)(2)(3): a = 2
(3) A 恢復現(xiàn)場: R0 恢復為 1 -> (2)(3) a = 2
本意: 線程 A B 各執(zhí)行1次, 均加1, 結果為 3; 但現(xiàn)在 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 讀
有數(shù)據(jù): 成功
沒數(shù)據(jù)
返回 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)
線程函數(shù) 調 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()
// 后續(xù): 切回來
判 什么原因導致 重新運行?
若 發(fā)生錯誤, 直接返回
若 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: 優(yōu)先級
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) 各 消息塊
內存連續(xù), 但組織為 鏈表
(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 互斥 怎么體現(xiàn) ? 簡單粗暴: 關中斷
(1) 想 Write data 時, 關中斷
rt_hw_interrupt_disable(),
在 開中斷之前 不受干擾
1) 中斷不能發(fā)生, 中斷 干擾不了
2) 其他 thread 無法運行
: 沒中斷, 無法切換
19 答疑
(1) 互斥
線程 A/B 都想 Write mq, 都想獲得 空閑 msgBlock
(2) 互斥量 怎么實現(xiàn)?
[1] 關中斷
[2] 有些 處理器 支持一些 匯編指令
, 可 原子地修改 變量
part 2
(1) 信號 是 異步
機制, 其余是 同步機制
郵箱
信號量
互斥量
事件到
消息隊列
信號
(2) RTT 最有特色的地方/比 FreeRTOS 強大的地方
有 設備驅動框架
, 可構造出龐大的 生態(tài)
20 郵箱(mailbox) 的 引入
20.1 mq 與 mailbox 唯一差別: data 的存儲不同, elemSize 可指定的 數(shù)組-鏈表 / unsigned long 數(shù)組
(1) mq 可指定
[1] 隊列中 元素數(shù)
[2] 每個元素 size
data write/放 進去, Read 出來: 都用 memcpy
線程間 傳 小 data(如 int 型)
, 用 memcpy 效率低
-> 引入: mailbox
(struct)
(2) mailbox
unsigned long(整型)數(shù)組:
每個元素 只能是 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個 參數(shù) 表示
(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 沒有空間 而進入 阻塞
態(tài), 我能 再次運行
時, 我 怎么知道 我是 超時退出
?
答: 必定有 判斷 (thread) 狀態(tài)
, 狀態(tài) == 某個錯誤碼/ETIMEDOUT, 就知道是因為超時而退出
何時設 (thread)狀態(tài)?
超時處理函數(shù) 把 阻塞 thread 放回 ReadyList 前
, 設 thread->status = ETIMEDOUT
21.2 A: Write mailbox 與 Read mailbox 對稱
// mailbox 是否滿?
滿
// 是否愿意等? : 用1個 參數(shù) 表示
(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 核心: 鏈表殿雪、定時器暇咆、環(huán)形 buf
(1) 環(huán)形 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 的 `超時處理函數(shù)` 被調用
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個數(shù)量, 要么 0 要么 1
停車場場景: 讓 停車場 車位數(shù) = 1
-> 好像是 mutex, 但 mutex: 并不只是把 resource 數(shù)限制為 1, 還有其他作用
26.1 信號量 缺點
入口 出口
rt_sem_take() rt_sem_release()
場景
換為 廁所
A 打開門進去, 按理說 只有 A 才可開門, 但 B 有備用鑰匙, 直接開門了
main()
創(chuàng)建 信號量: 設 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) 優(yōu)先級反轉
take semaphore(A): 失敗, 休眠, 讓出 CPU, 讓 MP 運行
HighPriority: HP —————————————————
不必 take semaphore(A) MP task 一直運行, LP task 一直沒機會運行
MidPriority : MP ————————————————— ——————————————————————————————————
take semaphore(A)
LowPriority : LP —————————————————
————————————————————————————————————————————————————————————————————————————————————————> t
RTT: 只要 更高優(yōu)先級的 task 就緒, 就可以馬上搶占 低優(yōu)先級的 task
LP task 一直沒機會運行
, 沒法釋放
信號量, 沒法 喚醒
HP task
HP task 被 MP task 搶占/反轉
解決: 用 互斥量
26.2 互斥量: 使用 優(yōu)先級繼承
小職員(LP task) 繼承了 大領導(HP task) 的優(yōu)先級
HP task: 提升 mutex 擁有者 的 優(yōu)先級
, 目的想 讓他盡快執(zhí)行, 盡快釋放我想得到的 resource, 可避免 MP task 來反轉我
take mutex(A): [1] 失敗, 休眠, 讓出 CPU, 讓 MP 運行
[2] `提升 mutex 擁有者 的 優(yōu)先級`
HighPriority: HP ————————————————— —————————————————
不必 take mutex(A)
MidPriority : MP —————————————————
take mutex(A) release mutex [1] 喚醒 MP
[2] `恢復 優(yōu)先級`
LowPriority : LP ————————————————— ————————————
————————————————————————————————————————————————————————————————————————————————————————> t
26.3 mutex 核心
(1) 誰擁有, 誰釋放
(2) 優(yōu)先級繼承