沒錯,就是大家這羣高級程序員(其實我也是)所耳熟能詳但又講不明白的 鎖,只是本章不是如何用,也不是講它是什麼原理,而是在實現咱們操做系統的過程當中所天然而然地產生的一個需求,而且咱們從零開始來實現 鎖html
本章須要和上一章 【自制操做系統12】熟悉而陌生的多線程 連起來看,由於正是上一章咱們多線程輸出字符串時,發現了一些問題,致使咱們須要想個辦法來解決,用大家高級程序員的牛逼的話來說,就是 爲了解決線程不安全的問題,提出了鎖這種技術手段。git
爲了讓你們清楚目前的程序進度,畫了到目前爲止的程序流程圖,以下。(紅色是咱們要實現的)程序員
上篇文章咱們建立了兩個線程,加上主線程,一共三個線程循環打印字符串,最終的輸出是這樣的安全
先忽略上面那個異常,看下面話框的地方,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
剛剛提到的問題只是特例,咱們把它概括總結爲通常描述,就是:函數
在咱們這個例子中,對應關係就是學習
總結起來,多線程的問題就是,多個任務同時出如今臨界區,也就是產生了競爭條件。那解決問題的辦法就只有一個,那就是 不要讓多個任務同時出如今臨界區。怎麼作到這一點呢?剛剛簡單粗暴的 開關中斷 是一種方法,下面要說的更靈活的 鎖 也是一種方法,再後面把多條指令從新用 一條原子指令 實現,如 CAS,也是一種方法。千萬不要被再後面各類各樣五花八門的各類技術繞暈,多線程解決的問題都是,不要讓多個任務同時出如今臨界區,僅此而已。
有了這兩個操做,兩個線程在進入臨界區時,即可以這樣操做
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 }
畫黃線是重點要看的部分,也就是咱們的目的,實現 獲取鎖 和 釋放鎖 兩個函數。看總體邏輯
上述兩個函數中有兩個子函數,是對信號量操做的,咱們看一下
上述函數中又有兩個子函數,咱們繼續拆解
忘記了 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 }
將全部這些都串起來,我畫了個圖,表示在各類狀況下,各個變量是如何變化的(藍色表明增長,綠色表明減小)
上一步咱們只是實現了鎖(其實就是實現了 獲取鎖 和 釋放鎖 兩個函數),但咱們尚未任何地方用它,接下來咱們就從新封裝一個原來多線程調用會出錯的 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 函數了而已,這樣在輸出的時候就有了鎖的保護,多線程再也不有上一章出現的問題了。簡單吧!
這回終於沒有報錯,且字符都整齊無誤地輸出在了屏幕上,再也不有覆蓋字符的現象了
若是你對自制一個操做系統感興趣,不妨跟隨這個系列課程看下去,甚至加入咱們(下方有公衆號和小助手微信),一塊兒來開發。
《操做系統真相還原》這本書真的贊!強烈推薦
當你看到該文章時,代碼可能已經比文章中的又多寫了一些部分了。你能夠經過提交記錄歷史來查看歷史的代碼,我會慢慢梳理提交歷史以及項目說明文檔,爭取給每一課都準備一個可執行的代碼。固然文章中的代碼也是全的,採用複製粘貼的方式也是徹底能夠的。
若是你有興趣加入這個自制操做系統的大軍,也能夠在留言區留下您的聯繫方式,或者在 gitee 私信我您的聯繫方式。
本課程打算出系列課程,我寫到哪以爲能夠寫成一篇文章了就寫出來分享給你們,最終會完成一個功能全面的操做系統,我以爲這是最好的學習操做系統的方式了。因此中間遇到的各類坎也會寫進去,若是你能持續跟進,跟着我一塊寫,必然會有很好的收貨。即便沒有,交個朋友也是好的哈哈。
目前的系列包括
微信公衆號
我要去阿里(woyaoquali)
小助手微信號
Angel(angel19980323)
while