[自製操作系統] 第16回 鎖的實現

来源:https://www.cnblogs.com/Lizhixing/archive/2022/07/09/15955437.html
-Advertisement-
Play Games

目錄 一、前景回顧 二、鎖的實現 三、使用鎖實現console函數 四、運行測試 一、前景回顧 上回我們實現了多線程,並且最後做了一個小小的實驗,不過有一點小瑕疵。 可以看到黃色部分的字元不連續,按道理應該是“argB Main”,這是為什麼呢?其實仔細思考一下還是很好得出結論。我們的字元列印函數是 ...


目錄
一、前景回顧
二、鎖的實現
三、使用鎖實現console函數
四、運行測試

 

一、前景回顧

  上回我們實現了多線程,並且最後做了一個小小的實驗,不過有一點小瑕疵。

   

  可以看到黃色部分的字元不連續,按道理應該是“argB Main”,這是為什麼呢?其實仔細思考一下還是很好得出結論。我們的字元列印函數是put_str,實際上是調用的put_char函數。所以列印一個字元串需要多次調用put_char函數來列印一個個字元,如果我們當前線程剛好列印完了arg,正準備列印下一個字元B時,這時發生了調度,那麼就造成了上面的這種情況。

  由此引申出來了公共資源、臨界區和互斥的概念:

  公共資源:可以是公共記憶體、公共文件、公共硬體等,總之是被所有任務共用的一套資源。

  臨界區:程式想要使用某些資源,必然通過一些指令去訪問這些資源,若多個任務都訪問同一公共資源,那麼各任務中訪問公共資源的指令代碼組成的區域就被稱為臨界區。

  互斥:互斥又稱為排他,是指某一時刻公共資源只能被一個任務獨享,即不允許多個任務同時出現在自己的臨界區中。其他任務想要訪問公共資源時,必須等待當前公共資源的訪問者完全執行完畢他自己的臨界區代碼後,才可以訪問。

  現在聯繫實際情況,我們可以知道,顯存區域是公共資源,每個線程的臨界區便是put_str函數。每個線程之間的put_str函數是互斥的關係,也就是說,任何一個時刻,只能有一個線程可以訪問操作顯存,並且只有等這個線程訪問操作完畢顯存後,才可以讓下一個線程訪問操作顯存。我們的代碼中並沒有具備互斥這個條件,所以便會造成上面的情況。

  基於上面的思路,如果我們對main.c文件做如下修改,在每個線程的put_str函數執行前後先執行關中斷和開中斷操作。

 1 #include "print.h"
 2 #include "init.h"
 3 #include "memory.h"
 4 #include "thread.h"
 5 #include "list.h"
 6 #include "interrupt.h"
 7 
 8 void k_thread_a(void *arg);
 9 void k_thread_b(void *arg);
10 
11 int main(void)
12 {
13     put_str("HELLO KERNEL\n");
14     init_all();
15     
16     thread_start("k_thread_a", 31, k_thread_a, "argA ");
17     thread_start("k_thread_b", 8, k_thread_b, "argB ");
18     intr_enable();
19     while(1) {
20         intr_disable();
21         console_put_str("Main ");
22         intr_enable();
23     }
24 }
25 
26 /*線上程中運行的函數k_thread_a*/
27 void k_thread_a(void *arg)
28 {
29     char *para = arg;
30     while (1) {
31         intr_disable();
32         put_str(para);
33         intr_enable();
34     }
35 }
36 
37 /*線上程中運行的函數k_thread_b*/
38 void k_thread_b(void *arg)
39 {
40     char *para = arg;
41     while (1) {
42         intr_disable();
43         put_str(para);
44         intr_enable();
45     }
46 }
main.c

  此時我們再運行系統,便可以看到字元輸出就變得正常了。

  

  雖然關中斷可以實現互斥,但是,關中斷的操作應儘可能地靠近臨界區,這樣才更高效,畢竟只有臨界區中的代碼才用於訪問公共資源,而訪問公共資源的時候才需要互斥、排他,各任務臨界區之外的代碼並不會和其他任務有所衝突。關中斷操作離臨界區越遠,多任務調度就越低效。

  總結一下:多線程訪問公共資源時產生了競爭條件,也就是多個任務同時出現在了自己的臨界區。為了避免產生競爭條件,必須保證任意時刻只能有一個任務處於臨界區。雖然開閉中斷的方式能夠解決這個問題,但是效率並不是最高的,我們通過提供一種互斥的機制,互斥使臨界區具有原子性,避免產生競爭條件,從而避免了多任務訪問公共資源時出問題。

二、鎖的實現

  我們的鎖是通過信號量的方式來實現的。信號量是一個整數,用來記錄所積累信號的數量。在我們的代碼中,對信號量的加法操作是用up表示,減法操作是用down表示。

  增加操作up,可以理解為釋放鎖,包括兩個微操作:

  1、將信號量的值加1。

  2、喚醒在此信號量上等待的線程。

  減少操作down,可以理解獲取鎖,包括三個微操作:

  1、判斷信號量是否為0。

  2、若信號量大於0,則將信號量減1。

  3、若信號量等於0,當前線程將自己阻塞,以在此信號量上等待。

  所以有了這兩個操作後,兩個線程在進入臨界區時,便是如下操作:

  1、線程A進入臨界區前先通過down操作獲取鎖,此時信號量減去1為0。

  2、同樣,線程B也要進入臨界區,嘗試使用down操作獲取鎖,但是信號量已經減為0,所以線程B便在此信號量上等待,也就是將自己阻塞。

  3、當線程A從臨界區中出來後,將信號量加1,也就是釋放鎖,隨後線程A將線程B喚醒

  4、線程B被喚醒後,獲得鎖,進入臨界區。

  來看看代碼吧,在project/kernel目錄下新建sync.c和sync.h文件,關於阻塞和解除阻塞的函數我們放在了thread.c文件下。

 1 #include "sync.h"
 2 #include "interrupt.h"
 3 #include "debug.h"
 4 #include "thread.h"
 5 
 6 /*初始化信號量*/
 7 void sema_init(struct semaphore *psema, uint8_t value)
 8 {
 9     psema->value = value;
10     list_init(&psema->waiters);
11 }
12 
13 /*初始化鎖lock*/
14 void lock_init(struct lock* plock)
15 {
16     plock->holder = NULL;
17     plock->holder_repeat_nr = 0;
18     sema_init(&plock->semaphore, 1);
19 }
20 
21 /*信號量down操作*/
22 void sema_down(struct semaphore *psema)
23 {
24     /*關中斷來保證原子操作*/
25     enum intr_status state = intr_disable();
26     while (psema->value == 0) {
27         /*當前線程不應該在信號量的waiters隊列中*/
28         ASSERT(!elem_find(&psema->waiters, &running_thread()->general_tag));
29         if (elem_find(&psema->waiters, &running_thread()->general_tag)) {
30             PANIC("sema_down: thread blocked has been in waiters_list\n");
31         }
32 
33         /*若信號量為0,則當前線程把自己加入到該鎖的等待隊列中*/
34         list_append(&psema->waiters, &running_thread()->general_tag);
35         thread_block(TASK_BLOCKED);
36     }
37 
38     /*若value為1或者被喚醒後,會執行以下代碼*/
39     psema->value--;
40     ASSERT(psema->value == 0);
41     /*恢復之前的中斷狀態*/
42     intr_set_status(state);
43 }
44 
45 /*信號量UP操作*/
46 void sema_up(struct semaphore *psema)
47 {
48     /*關中斷來保證原子操作*/
49     enum intr_status state = intr_disable();
50     ASSERT(psema->value == 0);
51     if (!list_empty(&psema->waiters)) {
52         struct task_struct *thread_blocked = elem2entry(struct task_struct, general_tag, list_pop(&psema->waiters));
53         thread_unblock(thread_blocked);
54     }
55     psema->value++;
56     ASSERT(psema->value == 1);
57     /*恢復之前的中斷狀態*/
58     intr_set_status(state);
59 }
60 
61 /*獲取鎖plock*/
62 void lock_acquire(struct lock *plock)
63 {
64     /*排除曾經自己有鎖但還未釋放鎖的情況*/
65     if (plock->holder != running_thread()) {
66         sema_down(&plock->semaphore);
67         plock->holder = running_thread();
68         ASSERT(plock->holder_repeat_nr == 0);
69         plock->holder_repeat_nr = 1;
70     } else {
71         plock->holder_repeat_nr++;
72     }
73 }
74 
75 /*釋放鎖plock*/
76 void lock_release(struct lock *plock)
77 {
78     ASSERT(plock->holder == running_thread());
79     if (plock->holder_repeat_nr > 1) {
80         plock->holder_repeat_nr--;
81         return;
82     }
83     ASSERT(plock->holder_repeat_nr == 1);
84     plock->holder = NULL;
85     plock->holder_repeat_nr = 0;
86     sema_up(&plock->semaphore);
87 }
sync.c
#ifndef  __KERNEL_SYNC_H
#define  __KERNEL_SYNC_H
#include "stdint.h"
#include "list.h"

/*信號量結構*/
struct semaphore {
    uint8_t value;
    struct list waiters;
};

/*鎖結構*/
struct lock {
    struct task_struct *holder;    //鎖的持有者
    struct semaphore semaphore;    //用二元信號量實現鎖
    uint32_t holder_repeat_nr;     //鎖的持有者重覆申請鎖的次數 
}; 

void lock_release(struct lock *plock);
void lock_acquire(struct lock *plock);
void sema_up(struct semaphore *psema);
void sema_down(struct semaphore *psema);
void lock_init(struct lock* plock);
void sema_init(struct semaphore *psema, uint8_t value);
#endif
sync.h
 1 ...
 2 
 3 /*當前線程將自己阻塞,標誌其狀態為stat*/
 4 void thread_block(enum task_status stat)
 5 {
 6     /*stat取值為TASK_BLOCKED、TASK_WAITING、TASK_HANGING
 7     這三種狀態才不會被調度*/
 8 
 9     ASSERT(((stat == TASK_BLOCKED) || (stat == TASK_WAITING) || (stat == TASK_HANGING)));
10     enum intr_status old_status = intr_disable();
11     struct task_struct *cur_thread = running_thread();
12     cur_thread->status = stat;
13     schedule();
14     intr_set_status(old_status);
15 }
16 
17 /*將線程thread解除阻塞*/
18 void thread_unblock(struct task_struct *thread)
19 {
20     enum intr_status old_status = intr_disable();
21     ASSERT(((thread->status == TASK_BLOCKED) || (thread->status == TASK_WAITING) || (thread->status == TASK_HANGING)));
22     if (thread->status != TASK_READY) {
23         ASSERT(!elem_find(&thread_ready_list, &thread->general_tag));
24         if (elem_find(&thread_ready_list, &thread->general_tag)) {
25             PANIC("thread_unblock: blocked thread in ready_list!\n");
26         }
27         list_push(&thread_ready_list, &thread->general_tag);
28         thread->status = TASK_READY;
29     }
30     intr_set_status(old_status);
31 }
thread.c

三、使用鎖實現console函數

  在本回開始,我們通過在put_str函數前後關、開中斷來保證任何時刻只有一個任務處於臨界區代碼,不過這種方式效率低下。現在我們已經實現了鎖機制,所以可以利用鎖來升級put_str函數。在project/kernel目錄下新建console.c和console.h文件。

#include "stdint.h"
#include "sync.h"
#include "thread.h"
#include "print.h"
#include "console.h"

static struct lock console_lock;

/*初始化終端*/
void console_init(void)
{
    lock_init(&console_lock);
}

/*獲取終端*/
void console_acquire(void)
{
    lock_acquire(&console_lock);
}

/*釋放終端*/
void console_release(void)
{
    lock_release(&console_lock);
}

/*終端中輸出字元串*/
void console_put_str(char *str)
{
    console_acquire();
    put_str(str);
    console_release();
}


/*終端中輸出字元*/
void console_put_char(uint8_t char_asci)
{
    console_acquire();
    put_char(char_asci);
    console_release();
}

/*終端中輸出十六進位整數*/
void console_put_int(uint32_t num)
{
    console_acquire();
    put_int(num);
    console_release();
}
console.c
 1 #ifndef  __KERNEL_CONSOLE_H
 2 #define  __KERNEL_CONSOLE_H
 3 
 4 void console_init(void);
 5 void console_acquire(void);
 6 void console_release(void);
 7 void console_put_str(char *str);
 8 void console_put_char(uint8_t char_asci);
 9 void console_put_int(uint32_t num);
10 
11 #endif
console.h

四、運行測試

  修改main.c函數並編譯運行。可以看到,字元整齊無誤地出現在屏幕上,不會出現字元短缺的現象。不要忘記在makefile中增加sync.o、console.o文件。

 1 #include "print.h"
 2 #include "init.h"
 3 #include "memory.h"
 4 #include "thread.h"
 5 #include "list.h"
 6 #include "interrupt.h"
 7 #include "console.h"
 8 
 9 void k_thread_a(void *arg);
10 void k_thread_b(void *arg);
11 
12 int main(void)
13 {
14     put_str("HELLO KERNEL\n");
15     init_all();
16     
17     thread_start("k_thread_a", 31, k_thread_a, "argA ");
18     thread_start("k_thread_b", 8, k_thread_b, "argB ");
19     intr_enable();
20     while(1) {
21         console_put_str("Main ");
22     }
23 }
24 
25 /*線上程中運行的函數k_thread_a*/
26 void k_thread_a(void *arg)
27 {
28     char *para = arg;
29     while (1) {
30         console_put_str(para);
31     }
32 }
33 
34 /*線上程中運行的函數k_thread_b*/
35 void k_thread_b(void *arg)
36 {
37     char *para = arg;
38     while (1) {
39         console_put_str(para);
40     }
41 }
42 
43 
44 
45 
46 
47 
48 
49 
50 //asm volatile("sti");
main.c

   

  本回到此結束,預知後事如何,請看下回分解。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • string常用庫函數 string的庫函數非常多,若全部掌握是非常耗時間的,但是我們只需要掌握常用,重要的庫函數即可,不常用的只需瞭解下即可,需要時,上C++標準官方庫查找。 這裡列舉出本篇說明的函數 insert、erase、swap、c_str、find、rfind、substr、getlin ...
  • 緊接上文,我們分析了Nacos的客戶端代碼, 今天我們再來試一下服務端 ,至此就可以Nacos源碼就告一段落,歡迎大家品鑒。 nacos服務端 註冊中心服務端的主要功能包括,接收客戶端的服務註冊,服務發現,服務下線的功能,但是除了這些和客戶端的交互之外,服務端還要做一些更重要的事情,就是我們常常會在 ...
  • 1. mysql的資料庫連接 step1:首先需要在代碼中添加Mysql.Data的代碼依賴。如果添加失敗則需要去搜索下載安裝!如下圖:代碼導入using MySql.Data.MySqlClient; step2:建立連接//設置連接基本參數 string connStr = "server = ...
  • .NET中間件以及VUE攔截器聯合使用 工作中遇見的問題,邊學邊弄,記錄一下 Vue的UI庫使用的是antvue 3.2.9版本的。 業務邏輯 特性 //特性 public class ModelEsignNameAttribute : Attribute { public ModelEsignNa ...
  • 主機名 查看主機名: hostname cat /etc/hostname 。。。 修改主機名: hostnamectl set-hostname xxx (和centos7、8一樣), #主機名最終存放在`/etc/hostname`下麵。 網卡名稱 命名方式和centos7的命名方式類似。 修改 ...
  • 網橋:和交換機工作原理一樣的一個硬體。 網橋內部有一個緩存,裡面放了介面和mac地址的對應關係。 橋接、NAT和僅主機模式: NAT網卡(vmnet8):相當於一個虛擬的集線器(Vmnet8),兩台使用nat模式的虛擬機能夠通信,是因為它都連接到了這個集線器(hub)上面。windows裡面本省就生 ...
  • 最近在linux下使用Chrome瀏覽器,第一次啟動時總是要輸入密碼,根據網上的方法取消輸入密碼,密碼總是回來,甚是惱人。經過思考和嘗試,最終問題得以解決。特將註意事項記錄如下: Chrome瀏覽器保存密碼和自動登錄等會生成和使用密鑰環,預設使用系統登錄用戶密碼生成和解鎖密鑰環,而密鑰環不會自動解鎖 ...
  • 多網卡綁定: 把多塊網卡邏輯上綁在一塊使用,對外就相當於一塊網卡,他們共用一個ip地址。 好處: 防止一塊網卡壞了就無法使用,提升帶寬。 工作模式: mod=0:輪詢模式,兩個網卡輪流處理數據包。 提升帶寬和容錯性 mod=1:主備模式,住在一個網卡上處理,主壞了就使用備用的。 只提升了容錯性 註: ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...