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;
if (RHINO_CONFIG_KOBJ_LIST > 0)
klist_t mutex_item; /*< kobj list for statistics /
endif
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 一個任務可能獲得多個互斥信號量,用該域構建任務獲得的互斥信號量鏈表。入下圖所示,任務獲得了三個互斥信號量:
(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,並判斷是否出現互斥失敗。
include <k_api.h>
/ 定義測試任務參數 /
define TEST_TASK1_NAME "task_test1"
define TEST_TASK2_NAME "task_test2"
define TEST_TASK1_PRI 34
define TEST_TASK2_PRI 34
define TEST_TASK_STACKSIZE (512)
/ 定義互斥信號量結構體 /
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);
}
}