RT-Thread(RTT) 內部機制

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: 運算單元

CPU(內部結構)-內存/代碼.png
    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

LDR.jpg

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

PUSH/POP.jpg

(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: LRmain 的 caller 中 BL/跳轉 到 main 時, 已 保存了 main 的 next 指令地址, 以保證 main 執(zhí)行結束 時, 能 跳回(PC = LR) 到 main 的 next 指令地址 去執(zhí)行

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 到 函數(shù)棧上

(2) 最后1條 匯編指令 pop {r3, pc}: R3 = 3, PC = 函數(shù)棧上 LR 的值 = add_val 的 next 指令, CPU 接著執(zhí)行 add_val 的 next 指令(return 0 對應的指令)

add_val 匯編.png

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)寄存器, 這里先不考慮這些

保存現(xiàn)場.png

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

創(chuàng)建線程 的 理論分析 .png

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ī)范)

irt_hw_stack_init.png

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]
線程啟動: 調用鏈.png
多優(yōu)先級 線程運行圖示.png
多優(yōu)先級 線程運行過程: 總結.png

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()

從 中斷函數(shù) 到 線程切換.png

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()

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

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)

啟動定時器.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) 切換 => 保存現(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 無法運行: 沒中斷, 無法切換

消息隊列操作 內部消息塊 管理 .png
msgBlocksMemoryPool.png

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)先級繼承

本文參考 韋東山視頻教程, 禁止用于商業(yè)等用途

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
禁止轉載,如需轉載請通過簡信或評論聯(lián)系作者丙曙。
  • 序言:七十年代末爸业,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子亏镰,更是在濱河造成了極大的恐慌扯旷,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,406評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件索抓,死亡現(xiàn)場離奇詭異钧忽,居然都是意外死亡,警方通過查閱死者的電腦和手機逼肯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評論 3 393
  • 文/潘曉璐 我一進店門耸黑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人篮幢,你說我怎么就攤上這事大刊。” “怎么了三椿?”我有些...
    開封第一講書人閱讀 163,711評論 0 353
  • 文/不壞的土叔 我叫張陵奈揍,是天一觀的道長。 經常有香客問我赋续,道長男翰,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,380評論 1 293
  • 正文 為了忘掉前任纽乱,我火速辦了婚禮蛾绎,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己租冠,他們只是感情好鹏倘,可當我...
    茶點故事閱讀 67,432評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著顽爹,像睡著了一般纤泵。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上镜粤,一...
    開封第一講書人閱讀 51,301評論 1 301
  • 那天捏题,我揣著相機與錄音,去河邊找鬼肉渴。 笑死公荧,一個胖子當著我的面吹牛,可吹牛的內容都是我干的同规。 我是一名探鬼主播循狰,決...
    沈念sama閱讀 40,145評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼券勺!你這毒婦竟也來了绪钥?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,008評論 0 276
  • 序言:老撾萬榮一對情侶失蹤关炼,失蹤者是張志新(化名)和其女友劉穎程腹,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體盗扒,經...
    沈念sama閱讀 45,443評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡跪楞,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,649評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了侣灶。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片甸祭。...
    茶點故事閱讀 39,795評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖褥影,靈堂內的尸體忽然破棺而出池户,到底是詐尸還是另有隱情,我是刑警寧澤凡怎,帶...
    沈念sama閱讀 35,501評論 5 345
  • 正文 年R本政府宣布校焦,位于F島的核電站,受9級特大地震影響统倒,放射性物質發(fā)生泄漏寨典。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,119評論 3 328
  • 文/蒙蒙 一房匆、第九天 我趴在偏房一處隱蔽的房頂上張望耸成。 院中可真熱鬧报亩,春花似錦、人聲如沸井氢。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽花竞。三九已至劲件,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間约急,已是汗流浹背零远。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留烤宙,地道東北人遍烦。 一個月前我還...
    沈念sama閱讀 47,899評論 2 370
  • 正文 我出身青樓俭嘁,卻偏偏與公主長得像躺枕,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子供填,可洞房花燭夜當晚...
    茶點故事閱讀 44,724評論 2 354

推薦閱讀更多精彩內容

  • 線程管理 在日常生活中拐云,我們要完成一個大任務,一般會將它分解成多個簡單近她、容易解決的小問題叉瘩,小問題逐個被解決,大問題...
    Swinner閱讀 1,253評論 0 0
  • RT-Thread workqueue 詳解 在學習之前可以先去了解一下工作隊列的使用場景:工作隊列 ( work...
    tang_jia閱讀 2,165評論 0 2
  • RT-Thread簡介 1.關鍵詞 國產粘捎,嵌入式操作系統(tǒng) RT-Thread:內核薇缅,網絡,fs攒磨,gui 開發(fā)環(huán)境:...
    心遠氣自靜閱讀 1,193評論 0 0
  • 1 準備好開發(fā)環(huán)境泳桦,Keil IDE 2 基于您的STM32F103芯片的開發(fā)板實現(xiàn)簡單的工程,如串口打印和LED...
    大象奔跑閱讀 3,678評論 0 0
  • 創(chuàng)建進程 使用fork函數(shù)創(chuàng)建進程int pid = fork();在執(zhí)行此函數(shù)后娩缰,即從當前進程開了一個新的子進程...
    西山薄涼閱讀 304評論 0 0