高薪祕訣,跟著AliOS Things輕鬆入門作業系統:互斥訊號量

hjavn發表於2021-08-09

工具與資源中心

幫助開發者更加高效的工作,提供圍繞開發者全生命週期的工具與資源

developer.aliyun.com/tool?spm=a1z3...

1、概述

本文將分析互斥訊號量的原始碼。

互斥訊號量與訊號量有相似之處,卻又有很大的不同。主要的幾個不同點為:

(1)任意時刻互斥訊號量最多隻能被一個執行緒獲得,它不像訊號量那樣可以有多個。

(2)只有獲得互斥訊號量的任務才能釋放互斥訊號量,所以中斷上下文中不能釋放互斥訊號量。

(3)支援巢狀請求,即獲得互斥訊號量的任務可再次請求該互斥訊號量。若巢狀請求訊號量,則每請求一次將消耗一個訊號量。

(4)支援解決優先順序反轉問題。優先順序反轉問題是作業系統設計的一個經典問題,曾導致過重大軟體事故,有興趣的讀者可以瞭解一下。由於互斥訊號量原始碼中有不少程式碼是為了解決這個問題,因此有必要先解釋一下。

假設有三個任務taskhigh、taskmid、tasklow,其中taskhigh優先順序最高,taskmid其次,tasklow優先順序最低。假設tasklow已經獲得了互斥訊號量Mutex,那麼當taskhigh請求Mutex時將被阻塞,它要一直等到tasklow釋放Mutex後才能繼續執行。然而若taskmid就緒,它將搶佔tasklow執行,tasklow釋放互斥訊號量要等taskmid讓出CPU。若系統中還有taskmid2、taskmid3等任務,那麼taskhigh還得等他們先執行。也就是說,高優先順序任務因等待被低優先順序任務佔用的互斥訊號量而得不到排程。這便是優先順序反轉問題。

AliOS Things採用優先順序繼承策略解決優先順序反轉問題:當高優先順序任務請求互斥訊號量阻塞時,將提升佔用互斥訊號量的任務的優先順序。在上述例子中,當taskhigh請求Mutex阻塞時,將把tasklow的優先順序提升到與taskhigh相同,這樣就可以避免因為taskmid搶佔tasklow而導致taskhigh得不到排程。

下面我們來解讀互斥訊號量原始碼,分析上述不同點是如何實現的。

互斥訊號量原始碼位置:core/rhino/k_mutex.c

互斥訊號量標頭檔案位置:core/rhino/include/k_mutex.h

2、互斥訊號量結構體kmutex_t

標頭檔案k_mutex.h定義了互斥訊號量結構體kmutex_t。互斥訊號量相關的函式都基於該結構體,所以我們首先分析一下該結構體,其具體定義如下:
typedef struct mutex_s {

/**<

*  Manage blocked tasks
*  List head is this mutex, list node is task, which are blocked in this mutex
*/

blk_obj_t blk_obj;

ktask_t mutex_task; /*< Pointer to the owner task */

/**<

*  Manage mutexs owned by the task
*  List head is task, list node is mutex, which are owned by the task
*/

struct mutex_s *mutex_list;

mutex_nested_t owner_nested;

klist_t mutex_item; /**< kobj list for statistics */

uint8_t mm_alloc_flag; /**< buffer from internal malloc or caller input */

} kmutex_t;

成員說明:

(1)blk_obj這是核心的一個基礎結構體,用於管理核心結構體的基本資訊。用物件導向的思想來看,它相當於kmutex_t的父類。它的主要域有:blk_list阻塞佇列,name物件名字,blk_policy阻塞佇列等待策略(主要有優先順序(PRI)和先入先出(FIO)兩種),obj_type結構體型別;

(2)mutex_task 指向獲得該互斥訊號量的任務;

(3)mutex_list 一個任務可能獲得多個互斥訊號量,用該域構建任務獲得的互斥訊號量連結串列。入下圖所示,任務獲得了三個互斥訊號量:
image.png

(4)owner_nested記錄同一個任務的申請巢狀次數;

(5)mutex_item是一個連結串列節點,用來把互斥訊號量插入到全域性連結串列,主要用作除錯、統計;

(6)mm_alloc_flag是一個記憶體標記,用來表示該結構體的記憶體是靜態分配的還是動態分配的。

3、建立互斥訊號量函式mutex_create

建立互斥訊號量的核心函式是mutex_create,它的原型如下:

kstat_t mutex_create(kmutex_t *mutex, const name_t *name, uint8_t mm_alloc_flag);

引數含義:

mutex:互斥訊號量結構體指標;

name:互斥訊號量名字,使用者可以為自己的互斥訊號量指定名字,以便於除錯區分;

mm_alloc_flag:記憶體型別,即入參mutex指向的記憶體是靜態分配的還是動態分配的。若為動態分配,則在刪除互斥訊號量時需要釋放mutex指向的結構體記憶體;

對比訊號量建立介面,這裡沒有訊號量個數的入參,這是因為互斥訊號量最多隻能被一個任務獲得,且初始狀態互斥訊號量空閒,即相當於初始值為1。

在該函式內將初始化互斥訊號量結構體,幾個關鍵資訊是:

(1)blk_obj.blk_policy初始化為BLK_POLICY_PRI,表示採用基於優先順序的阻塞策略,意思是當多個任務阻塞在該互斥訊號量上時,高優先順序任務優先獲得該互斥訊號量。另外一種策略是BLK_POLICY_FIFO,即先阻塞的任務優先獲得互斥訊號量;

(2)mutex_task初始化NULL,表示當前沒有任務佔用該互斥訊號量;

(3)用RHINO_CRITICAL_ENTER()/RHINO_CRITICAL_EXIT()臨界區語句保護的連結串列插入操作將互斥訊號量結構體插入全域性連結串列。在除錯時,可以通過g_kobj_list.mutex_head連結串列獲得系統中所有互斥訊號量;

(4)blk_obj.obj_type初始化為RHINO_MUTEX_OBJ_TYPE,表示該結構體型別是互斥訊號量。

函式krhino_mutex_create()和krhino_mutex_dyn_create()是建立互斥訊號量的對外介面,兩者的差別是前者是靜態建立(K_OBJ_STATIC_ALLOC),即kmutex_t結構體的記憶體由外部傳入。後者是動態建立(K_OBJ_DYN_ALLOC),該函式內將呼叫krhino_mm_alloc動態分配kmutex_t結構體的記憶體,並通過入參mutex把建立的結構體物件傳回給呼叫者,所以krhino_mutex_dyn_create函式入參mutex的型別是kmutex_t **。

4、請求互斥訊號量krhino_mutex_lock

建立互斥訊號量後,就可以呼叫krhino_mutex_lock請求互斥訊號量了,其原型如下:

kstat_t krhino_mutex_lock(kmutex_t *mutex, tick_t ticks);

引數說明:

(1)mutex 指向互斥訊號量結構體的指標;

(2)ticks 阻塞時間。當互斥訊號量已經被佔用時,發起請求的任務將被阻塞,ticks用來指定阻塞時間。其中,兩個特殊值是:(a)RHINO_NO_WAIT,不等待,當不能獲得互斥訊號量時直接返回;(b) RHINO_WAIT_FOREVER,一直阻塞等待,直到獲得互斥訊號量為止。

在該函式內:

(1)函式入口做一些入參檢查。語句RHINO_CRITICAL_ENTER()用於進入臨界區;

(2)條件語句g_active_task[cur_cpu_num] == mutex->mutex_task用來判斷互斥訊號量是否已經被當前任務獲得。若已經獲得,則互斥訊號量內部巢狀計數mutex->owner_nested加1。然後返回,這裡實現了巢狀獲得互斥訊號量;

(3)條件語句mutex_task == NULL用來判斷互斥訊號量是否空閒,若該條件成立,將佔用該互斥訊號量,佔用的主要操作是mutex_task賦值為g_active_task[cur_cpu_num],即當前請求互斥訊號量的任務。這裡將返回成功;

(4)如果上述兩個條件不成立,說明互斥訊號量已經被佔用了,如果入參ticks等於RHINO_NO_WAIT,說明呼叫者不想等待,直接返回失敗。如果g_sched_lock[cur_cpu_num] > 0,說明系統當前關排程了,那麼任務不能被阻塞了,因為阻塞將觸發排程,所以也返回失敗;

(5)執行到這裡,任務將被阻塞。呼叫pend_to_blk_obj將置任務為非就緒狀態,RHINO_CRITICAL_EXIT_SCHED()將退出臨界區並觸發排程。當任務被喚醒後,繼續執行,將呼叫pend_state_end_proc函式,用來判斷是什麼原因被喚醒的,主要是兩個(a)超時時間到;(b)獲得了互斥訊號量;

呼叫pend_to_blk_obj前,用預編譯巨集RHINO_CONFIG_MUTEX_INHERIT控制的區域還有一個判斷語句。這個判斷語句就是用來處理優先順序反轉問題的:

判斷g_active_task[cur_cpu_num]->prio < mutex_task->prio,說明當前請求互斥訊號量的任務優先順序比獲得互斥訊號量的任務高(值越小優先順序越高),這個時候將呼叫ask_pri_change動態提升獲得互斥訊號量任務的優先順序。

5、釋放互斥訊號量krhino_mutex_unlock

釋放互斥訊號量的函式原型為:

kstat_t krhino_mutex_unlock(kmutex_t *mutex)

在該函式內:

(1)函式入口先檢查入參和進入臨界區;

(2)呼叫mutex_release把互斥訊號量從任務的互斥訊號量連結串列刪除。若該任務的優先順序被動態調整過,則恢復任務的優先順序。當然,這個函式還要考慮任務可能佔用著其他互斥訊號量,篇幅原因,不具體展開了;

(3)釋放互斥訊號量後,判斷當前是否有任務阻塞在該互斥訊號量上,若沒有則直接返回;若有,則從阻塞連結串列取一個阻塞任務,呼叫pend_task_wakeup喚醒該任務,然後將互斥訊號量的資訊更新為被新任務佔用。

6、刪除互斥訊號量krhino_mutex_del/ krhino_mutex_dyn_del

krhino_mutex_del用來刪除krhino_mutex_create建立的互斥訊號量。krhino_mutex_dyn_del用來刪除krhino_mutex_dyn_create建立的互斥訊號量。這兩組函式必須配套使用,否則釋放將失敗。

krhino_mutex_dyn_del相比krhino_mutex_de多了釋放互斥訊號量結構體的操作,這裡我們僅分析krhino_mutex_del函式。

在該函式內:

(1)首先檢查入參,進入臨界區,然後判斷型別是否正確,是否靜態分配;

(2)若該互斥訊號量當前被任務佔用,則呼叫mutex_release把互斥訊號量從任務的互斥訊號量連結串列刪除。若該任務的優先順序被動態調整過,則恢復任務的優先順序;

(3)若有任務阻塞在該互斥訊號量上,則全部喚醒;

(4)語句klist_rm(&mutex->mutex_item)把互斥訊號量從g_kobj_list.mutex_head連結串列刪除。與mutex_create中的插入操作相對;

(5)退出臨界區並返回。

7、使用示例

下面是一個介面使用示例,任務1和任務2通過互斥訊號量互斥訪問全域性變數a,並判斷是否出現互斥失敗。

/* 定義測試任務引數 */

/* 定義互斥訊號量結構體 */

kmutex_t mutex_test;

/* 定義任務相關資源 */

ktask_t test_task1_tcb;

cpu_stack_t test_task1_stack[TEST_TASK_STACKSIZE];

ktask_t test_task2_tcb;

cpu_stack_t test_task2_stack[TEST_TASK_STACKSIZE];

/* 前向宣告任務入口函式 */

static void test_task1(void *arg);

static void test_task2(void *arg);

/* 定義互斥訪問的全域性變數 */

int a = 0;

/* 入口函式 */

int application_start(int argc, char *argv[])

{

/* 靜態建立互斥訊號量,初始個數為0 */

krhino_mutex_create(&mutex_test, “mutex_test”);

/* 建立兩個測試任務 */

krhino_task_create(&test_task1_tcb, TEST_TASK1_NAME, 0, TEST_TASK1_PRI, 50,

                  test_task1_stack, TEST_TASK_STACKSIZE, test_task1, 0);

krhino_task_create(&test_task2_tcb, TEST_TASK2_NAME, 0, TEST_TASK2_PRI, 50,

                  test_task2_stack, TEST_TASK_STACKSIZE, test_task2, 0);

}

/* 任務1的入口 */

static void test_task1_entry(void *arg)

{

int b;

while (1) {

   krhino_mutex_lock(&mutex_test, RHINO_WAIT_FOREVER);

   b = a;

   a = a + 1;

   if (a != b + 1) {

       printf("task 1 data process error\r\n");

   }

   krhino_mutex_unlock(&mutex_test);

}

}

/* 任務2的入口 */

static void test_task2(void *arg)

{

int b;

while(1) {

   krhino_mutex_lock(&mutex_test, RHINO_WAIT_FOREVER);

   b = a;

   a = a + 1;

   if (a != b + 1) {

       printf("task 2 data process error\r\n");

   }

   krhino_mutex_unlock(&mutex_test);

}

}

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章