【自制操做系統13】鎖

沒錯,就是大家這羣高級程序員(其實我也是)所耳熟能詳但又講不明白的 ,只是本章不是如何用,也不是講它是什麼原理,而是在實現咱們操做系統的過程當中所天然而然地產生的一個需求,而且咱們從零開始來實現 html

本章須要和上一章 【自制操做系統12】熟悉而陌生的多線程 連起來看,由於正是上一章咱們多線程輸出字符串時,發現了一些問題,致使咱們須要想個辦法來解決,用大家高級程序員的牛逼的話來說,就是 爲了解決線程不安全的問題,提出了鎖這種技術手段git

1、到目前爲止的程序流程圖

爲了讓你們清楚目前的程序進度,畫了到目前爲止的程序流程圖,以下。(紅色是咱們要實現的)程序員

2、上一篇文章的多線程問題

上篇文章咱們建立了兩個線程,加上主線程,一共三個線程循環打印字符串,最終的輸出是這樣的安全

  先忽略上面那個異常,看下面話框的地方,argA 尚未打印完,就從中間斷開了,開始打印了 argB微信

  其實很好解釋,由於 打印一個字符串 put_str 是經過一次次調用 put_char 來實現的,假如任務切換恰好發生在打印字符串 "argA" 剛剛打印到 "ar」 的時候切換了(實際上這機率很大),就會出現上面的問題。再往細了說,單單一個 put_char 函數,也是分紅 獲取光標、打印字符、更新光標值 等多個步驟實現的,假如在中間某處發生了任務切換,不但字符串被分割,還會出現少字符的狀況,你們能夠想一想爲何。至於最上面的異常,固然也是因爲相似的緣由形成的。多線程

  上面的種種問題,概括起來就是,雖然咱們的任務切換能夠發生在任何一個指令和下一條指令之間,但有的時候咱們但願多條指令是具備 原子性 的,也就是要麼不執行,要執行就所有執行完,這中間不容許發生任務切換。考慮到這點,咱們能夠經過簡單的開關中斷來實現,就像這樣。app

void k_thread_a(void* arg) { 
    char* para = arg; 
    while(1) { 
        intr_disable(); // 關中斷
        put_str(para); 
        intr_enable(); // 開中斷
    } 
}        

  咱們再運行程序,就會發現上述問題被完美解決了。可別瞧不起這粗暴的方法,關中斷是實現互斥最簡單的方法,沒有之一。咱們從此實現的各類互斥手段也將以它爲基礎。ide

3、問題抽象(公共資源、臨界區、互斥、競爭條件)

 剛剛提到的問題只是特例,咱們把它概括總結爲通常描述,就是:函數

  • 公共資源:能夠是公共內存、公共文件、公共硬件等,總之是被全部任務共享的一套資源
  • 臨界區:麼各任務中訪問公共資源的指令代碼組成的區域,注意是 指令
  • 互斥:某一時刻公共資源只能被 1 個任務獨享,即不容許多個任務同時出如今本身的臨界區中
  • 競爭條件:多個任務以非互斥的方式同時進入臨界區,對公共資源的訪問是以競爭的方式並行進行的,所以公共資源的最終狀態依賴於這些任務的臨界區中的微操做執行次序。

 在咱們這個例子中,對應關係就是學習

  • 公共資源:光標寄存器、顯存
  • 臨界區:put_char 函數,由於該函數都對公共資源光標寄存器進行了訪問
  • 互斥:暫時經過開關中斷,實現 put_str 之間的互斥
  • 競爭條件:「少字符」問題是對顯存未實現互斥訪問形成的,「GP」異常是對光標寄存器未實現互斥訪問形成的

  總結起來,多線程的問題就是,多個任務同時出如今臨界區,也就是產生了競爭條件。那解決問題的辦法就只有一個,那就是 不要讓多個任務同時出如今臨界區。怎麼作到這一點呢?剛剛簡單粗暴的 開關中斷 是一種方法,下面要說的更靈活的 也是一種方法,再後面把多條指令從新用 一條原子指令 實現,如 CAS,也是一種方法。千萬不要被再後面各類各樣五花八門的各類技術繞暈,多線程解決的問題都是,不要讓多個任務同時出如今臨界區,僅此而已。

4、信號量與鎖

咱們的鎖是用 信號量 來實現的,信號量就是一個計數器,它包括了 P(down)和 V(up)操做
V(up):能夠理解爲釋放鎖
  1. 將信號量的值加 1
  2. 喚醒在此信號量上等待的線程
P(down):能夠理解爲獲取鎖
  1. 判斷信號量是否大於 0
  2. 若信號量大於 0,則將信號量減 1
  3. 若信號量等於 0,當前線程將本身阻塞,以在此信號量上等待

有了這兩個操做,兩個線程在進入臨界區時,即可以這樣操做

  1. 線程 A 進入臨界區前先經過 down 操做 得到鎖,此時信號量的值便爲 0
  2. 線程 B 再進入臨界區時也經過 down 操做得到鎖,因爲信號量爲 0,線程 B 便在此信號量上等待,也就是至關於線程 B 進入了 阻塞
  3. 當線程 A 從臨界區出來後執行 up 操做 釋放鎖,此時信號量的值從新變成 1,以後線程 A 將線程 B 喚醒
  4. 線程 B 醒來後得到了鎖,進入臨界區

5、代碼實現

鎖的底層實現

sync.h

 1 // 信號量結構
 2 struct semaphore {
 3     uint8_t value;
 4     struct list waiters;
 5 };
 6 
 7 // 鎖結構
 8 struct lock {
 9     struct task_struct* holder; // 持有者
10     struct semaphore semaphore; // 二元信號量
11     uint32_t holder_repeat_nr; // 持有者重複申請鎖的次數
12 };

sync.c

 1 #include "sync.h"
 2 #include "list.h"
 3 #include "global.h"
 4 #include "interrupt.h"
 5 
 6 // 初始化信號量
 7 void sema_init(struct semaphore* psema, uint8_t value) {
 8     psema->value = value; // 爲信號量賦初值
 9     list_init(&psema->waiters); // 初始化信號量的等待隊列
10 }
11 
12 // 初始化鎖 plock
13 void lock_init(struct lock* plock) {
14     plock->holder = NULL;
15     plock->holder_repeat_nr = 0;
16     sema_init(&plock->semaphore, 1); // 信號量初值爲1
17 }
18 
19 // 信號量 down 操做
20 void sema_down(struct semaphore* psema) {
21     // 關閉中斷保證原子操做
22     enum intr_status old_status = intr_disable();
23     while(psema->value == 0) {
24         // 表示已經被別人持有,當前線程把本身加入該鎖的等待隊列,而後阻塞本身
25         list_append(&psema->waiters, &running_thread()->general_tag);
26         thread_block(TASK_BLOCKED);
27     }
28     // value不爲0,則能夠得到鎖
29     psema->value--;
30     intr_set_status(old_status);
31 }
32     
33 // 信號量的 up 操做
34 void sema_up(struct semaphore* psema) {
35     // 關閉中斷保證原子操做
36     enum intr_status old_status = intr_disable();
37     
38     if (!list_empty(&psema->waiters)) {
39         struct task_struct* thread_blocked = elem2entry(struct task_struct, general_tag, list_pop(&psema->waiters));
40         thread_unblock(thread_blocked);
41     }
42     
43     psema->value++;
44     intr_set_status(old_status);
45 }
46 
47 // 獲取鎖 plock
48 void lock_acquire(struct lock* plock) {
49     if (plock->holder != running_thread()) {
50         sema_down(&plock->semaphore);
51         plock->holder = running_thread();
52         plock->holder_repeat_nr = 1;
53     } else {
54         plock->holder_repeat_nr++;
55     }
56 }
57 
58 // 釋放鎖 plock
59 void lock_release(struct lock* plock) {
60     if (plock->holder_repeat_nr > 1) {
61         plock->holder_repeat_nr--;
62         return;
63     }
64     plock->holder = NULL;
65     plock->holder_repeat_nr = 0;
66     sema_up(&plock->semaphore);
67 }

thread.c

 1 ...
 2 
 3 // 當前線程將本身阻塞,標誌其狀態爲 stat(取值必須爲 BLOCKED WAITING HANGING 之一)
 4 void thread_block(enum task_status stat) {
 5     enum intr_status old_status = intr_disable();
 6     struct task_struct* cur_thread = running_thread();
 7     cur_thread->status = stat;
 8     schedule();
 9     intr_set_status(old_status);
10 }
11 
12 // 解除阻塞
13 void thread_unblock(struct task_struct* pthread) {
14     enum intr_status old_status = intr_disable();
15     if (pthread->status != TASK_READY) {
16         if (elem_find(&thread_ready_list, &pthread->general_tag)) {
17             // 錯誤!blocked thread in ready_list
18         }
19         // 放到隊列的最前面,使其儘快獲得調度
20         list_push(&thread_ready_list, &pthread->general_tag);
21         pthread->status = TASK_READY;
22     }
23     intr_set_status(old_status);
24 }

畫黃線是重點要看的部分,也就是咱們的目的,實現 獲取鎖釋放鎖 兩個函數。看總體邏輯

  • 獲取鎖:若是鎖的持有者不是當前線程,則 sema_down 信號量減一 ,鎖的持有者變爲當前線程。若是鎖的持有者就是當前線程,則變量 holder_repeat_nr 遞增,能夠理解爲可重入的次數
  • 釋放鎖:變量 holder_repeat_nr 遞減小,鎖的持有者置空,執行 sema_up 信號量遞增

上述兩個函數中有兩個子函數,是對信號量操做的,咱們看一下

  • sema_down(信號量遞減):while 判斷信號量值 value 是否爲 0,若不爲 0 則能夠獲取鎖,直接將其減一;若爲 0 表示鎖被別的線程持有,則該線程加入信號量的等待隊列 waiters,並阻塞該線程 thread_block
  • sema_up(信號量遞增):信號量值 value++,同時若信號量等待隊列 waiters 不爲空,則表示有須要喚醒的線程,pop 出一個,喚醒該線程 thread_unblock

上述函數中又有兩個子函數,咱們繼續拆解

  • thread_block(阻塞):將當前線程的狀態,改成阻塞態的一種(BLOCKED WAITING HANGING),並執行任務切換函數 schedule,由該函數真正將其換下 CPU
  • thread_unblock(喚醒):喚醒一個指定線程,也就是上面由 sema_up 函數裏從 waiters 中 pop 出來的線程。若是該線程不是 READY 狀態(應該說不出錯的話就不該該是 READY 狀態),則將其放到 thread_ready_list 中,等待下次被調度

忘記了 schedule 函數的,能夠看下面回顧一下

 1 // 實現任務調度
 2 void schedule() {
 3     struct task_struct* cur = running_thread();
 4     if (cur->status == TASK_RUNNING) {
 5         // 只是時間片到了,加入就緒隊列隊尾
 6         list_append(&thread_ready_list, &cur->general_tag);
 7         cur->ticks = cur->priority;
 8         cur->status = TASK_READY;
 9     } else {
10         // 須要等某事件發生後才能繼續上 cpu,不加入就緒隊列
11     }
12     
13     thread_tag = NULL;
14     // 就緒隊列取第一個,準備上cpu
15     thread_tag = list_pop(&thread_ready_list);
16     struct task_struct* next = elem2entry(struct task_struct, general_tag, thread_tag);
17     next->status = TASK_RUNNING;
18     switch_to(cur, next);
19 }
schedule

將全部這些都串起來,我畫了個圖,表示在各類狀況下,各個變量是如何變化的(藍色表明增長,綠色表明減小)

 

使用鎖實現 console 輸出

上一步咱們只是實現了鎖(其實就是實現了 獲取鎖 和 釋放鎖 兩個函數),但咱們尚未任何地方用它,接下來咱們就從新封裝一個原來多線程調用會出錯的 put_str 函數的升級版(原子化) console_put_str

 1 static struct lock console_lock;
 2 
 3 void console_init() {
 4     lock_init(&console_lock);
 5 }
 6 
 7 void console_acquire() {
 8     lock_acquire(&console_lock);
 9 }
10 
11 void console_release() {
12     lock_release(&console_lock);
13 }
14 
15 void console_put_str(char* str) {
16     console_acquire();
17     put_str(str);
18     console_release();
19 }

能夠看到,其實就是把 put_str 函數加了鎖,又封裝了一層而已。接下來咱們 main 函數調用一下新輸出函數的試試

 1 int main(void){
 2     put_str("I am kernel\n");
 3     init_all();
 4     thread_start("k_thread_a", 31, k_thread_a, "argA ");
 5     thread_start("k_thread_b", 8, k_thread_b, "argB ");
 6     intr_enable();
 7     
 8     while(1) {
 9         put_str("Main ");
10         console_put_str("Main "); 11     }
12     return 0;
13 }
14 
15 void k_thread_a(void* arg) {
16     char* para = arg;
17     while(1) {
18         console_put_str(para);
19     }
20 }
21 
22 void k_thread_b(void* arg) {
23     char* para = arg;
24     while(1) {
25         console_put_str(para);
26     }
27 }

能夠看到畫黃線的部分,咱們只是把原來的 put_str 函數,更換成了 console_put_str 函數了而已,這樣在輸出的時候就有了鎖的保護,多線程再也不有上一章出現的問題了。簡單吧!

運行

這回終於沒有報錯,且字符都整齊無誤地輸出在了屏幕上,再也不有覆蓋字符的現象了

 

寫在最後:開源項目和課程規劃

若是你對自制一個操做系統感興趣,不妨跟隨這個系列課程看下去,甚至加入咱們(下方有公衆號和小助手微信),一塊兒來開發。

參考書籍

《操做系統真相還原》這本書真的贊!強烈推薦

項目開源

項目開源地址:https://gitee.com/sunym1993/flashos

當你看到該文章時,代碼可能已經比文章中的又多寫了一些部分了。你能夠經過提交記錄歷史來查看歷史的代碼,我會慢慢梳理提交歷史以及項目說明文檔,爭取給每一課都準備一個可執行的代碼。固然文章中的代碼也是全的,採用複製粘貼的方式也是徹底能夠的。

若是你有興趣加入這個自制操做系統的大軍,也能夠在留言區留下您的聯繫方式,或者在 gitee 私信我您的聯繫方式。

課程規劃

本課程打算出系列課程,我寫到哪以爲能夠寫成一篇文章了就寫出來分享給你們,最終會完成一個功能全面的操做系統,我以爲這是最好的學習操做系統的方式了。因此中間遇到的各類坎也會寫進去,若是你能持續跟進,跟着我一塊寫,必然會有很好的收貨。即便沒有,交個朋友也是好的哈哈。

目前的系列包括

 微信公衆號

  我要去阿里(woyaoquali)

 小助手微信號

  Angel(angel19980323)

while
相關文章
相關標籤/搜索