工具與資源中心node
幫助開發者更加高效的工做,提供圍繞開發者全生命週期的工具與資源api
developer.aliyun.com/tool?spm=a1…markdown
本文將分析互斥信號量的源碼。app
互斥信號量與信號量有類似之處,卻又有很大的不一樣。主要的幾個不一樣點爲:函數
(1)任意時刻互斥信號量最多隻能被一個線程得到,它不像信號量那樣能夠有多個。工具
(2)只有得到互斥信號量的任務才能釋放互斥信號量,因此中斷上下文中不能釋放互斥信號量。oop
(3)支持嵌套請求,即得到互斥信號量的任務可再次請求該互斥信號量。若嵌套請求信號量,則每請求一次將消耗一個信號量。測試
(4)支持解決優先級反轉問題。優先級反轉問題是操做系統設計的一個經典問題,曾致使太重大軟件事故,有興趣的讀者能夠了解一下。因爲互斥信號量源碼中有很多代碼是爲了解決這個問題,所以有必要先解釋一下。ui
假設有三個任務taskhigh、taskmid、tasklow,其中taskhigh優先級最高,taskmid其次,tasklow優先級最低。假設tasklow已經得到了互斥信號量Mutex,那麼當taskhigh請求Mutex時將被阻塞,它要一直等到tasklow釋放Mutex後才能繼續運行。然而若taskmid就緒,它將搶佔tasklow運行,tasklow釋放互斥信號量要等taskmid讓出CPU。若系統中還有taskmid二、taskmid3等任務,那麼taskhigh還得等他們先執行。也就是說,高優先級任務因等待被低優先級任務佔用的互斥信號量而得不到調度。這即是優先級反轉問題。this
AliOS Things採用優先級繼承策略解決優先級反轉問題:當高優先級任務請求互斥信號量阻塞時,將提高佔用互斥信號量的任務的優先級。在上述例子中,當taskhigh請求Mutex阻塞時,將把tasklow的優先級提高到與taskhigh相同,這樣就能夠避免由於taskmid搶佔tasklow而致使taskhigh得不到調度。
下面咱們來解讀互斥信號量源碼,分析上述不一樣點是如何實現的。
互斥信號量源碼位置:core/rhino/k_mutex.c
互斥信號量頭文件位置:core/rhino/include/k_mutex.h
頭文件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 一個任務可能得到多個互斥信號量,用該域構建任務得到的互斥信號量鏈表。入下圖所示,任務得到了三個互斥信號量:
(4)owner_nested記錄同一個任務的申請嵌套次數;
(5)mutex_item是一個鏈表節點,用來把互斥信號量插入到全局鏈表,主要用做調試、統計;
(6)mm_alloc_flag是一個內存標記,用來表示該結構體的內存是靜態分配的仍是動態分配的。
建立互斥信號量的核心函數是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 **。
建立互斥信號量後,就能夠調用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動態提高得到互斥信號量任務的優先級。
釋放互斥信號量的函數原型爲:
kstat_t krhino_mutex_unlock(kmutex_t *mutex)
在該函數內:
(1)函數入口先檢查入參和進入臨界區;
(2)調用mutex_release把互斥信號量從任務的互斥信號量鏈表刪除。若該任務的優先級被動態調整過,則恢復任務的優先級。固然,這個函數還要考慮任務可能佔用着其餘互斥信號量,篇幅緣由,不具體展開了;
(3)釋放互斥信號量後,判斷當前是否有任務阻塞在該互斥信號量上,若沒有則直接返回;如有,則從阻塞鏈表取一個阻塞任務,調用pend_task_wakeup喚醒該任務,而後將互斥信號量的信息更新爲被新任務佔用。
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)退出臨界區並返回。
下面是一個接口使用示例,任務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);
複製代碼
}
}