參考資料:RTT官網文檔
關鍵字:分析RT-Thread源碼、stm32、RTOS、信號量。
問題及總結
一、信號量是如何實現永久等待信號的?
當time設置爲-1時可以實現永久等待,因爲在rt_sem_take中判斷如果time > 0時纔會啓動定時器,所以這時該線程會被掛起。掛在該信號量的suspen_thread鏈表上。當rt_sem_release被調用時,會判斷suspend是否有掛載線程,如果有,就喚醒第一個掛載的線程,一次只喚醒一個線程。
內核同步
內核就像是一個不斷接收請求並進行響應的服務器,例如來自cpu正在執行的線程,或者發出中斷請求的外部設備。我們現在知道OS的作用就是可以多個任務“並行”執行,即任務交錯執行的方式。因此,這些請求可能引起競爭條件,而我們必須採用適當的同步機制來對這種情況進行控制。(競爭條件指多個線程或者進程在讀寫一個共享數據時結果依賴於它們執行的相對時間的情形。)
在RTT中有這幾種同步方式:信號量(semaphore)、互斥量(mutex)、和事件集(event)。先從信號量開始介紹。
常見中文名翻譯如下:
- semaphore : 信號量,信號燈,旗標
- mutex : 互斥量,互斥鎖,互斥體
- event : 事件,事件集
信號量semaphore
信號量又稱爲旗標,信號燈,是一個同步對象,它實現了一個加鎖原語,即讓等待者睡眠,直到等待的資源變爲空閒狀態。信號量/旗標在計算機科學中是一個被很好理解的概念. 在它的核心, 一個旗標是一個單個整型值, 結合有一對函數, 典型地稱爲 P 和 V. 一個想進入臨界區的進程將在相關旗標上調用 P; 如果旗標的值大於零, 這個值遞減 1 並且進程繼續. 相反, 如果旗標的值是 0 ( 或更小 ), 進程必須等待直到別人釋放旗標. 解鎖一個旗標通過調用 V 完成; 這個函數遞增旗標的值, 並且, 如果需要, 喚醒等待的進程.
當旗標用作互斥 – 阻止多個進程同時在同一個臨界區內運行 – 它們的值將初始化爲 1. 這樣的旗
標在任何給定時間只能由一個單個進程或者線程持有. 以這種模式使用的旗標有時稱爲一個互斥
鎖, 就是, 當然, "互斥"的縮寫(LDD3).即互斥鎖、互斥量是特殊狀態的信號量。
源碼分析
信號量結構體定義如下:
struct rt_semaphore
{
struct rt_ipc_object parent; /* 繼承自 ipc_object 類 */
rt_uint16_t value; /* 信號量的值 */
};
struct rt_ipc_object
{
struct rt_object parent; /**< inherit from rt_object */
rt_list_t suspend_thread; /**< threads pended on this resource */
};
rt_semaphore 對象從 rt_ipc_object 中派生,由 IPC 容器所管理,信號量的最大值是 65535。
每個信號量對象都有一個信號量值和一個線程等待隊列。
信號量的初始化也有兩種,靜態和動態,這裏以靜態爲例,相關代碼在ipc.c中。
rt_err_t rt_sem_init(rt_sem_t sem,
const char *name,
rt_uint32_t value,
rt_uint8_t flag)
{
RT_ASSERT(sem != RT_NULL);
/* init object */
rt_object_init(&(sem->parent.parent), RT_Object_Class_Semaphore, name);
/* init ipc object */
rt_ipc_object_init(&(sem->parent));
/* set init value */
sem->value = value;
/* set parent */
sem->parent.parent.flag = flag;
return RT_EOK;
}
參數sem是信號量的句柄,name爲信號量的名稱,value是信號量值的初始值,flag是信號量標誌,有兩種設置,先進先出RT_IPC_FLAG_FIFO和和根據優先級RT_IPC_FLAG_PRIO。
接着初始化爲對象隊列中的RT_Object_Class_Semaphore類,初始化信號量的值,和標識位flag。
rt_inline rt_err_t rt_ipc_object_init(struct rt_ipc_object *ipc)
{
/* init ipc object */
rt_list_init(&(ipc->suspend_thread));
return RT_EOK;
}
rt_ipc_object_init裏將節點suspend_thread初始化,用來掛載等待該信號的線程。
靜態rt_sem_init對應的脫離函數是rt_sem_detach:
rt_err_t rt_sem_detach(rt_sem_t sem)
{
RT_ASSERT(sem != RT_NULL);
/* wakeup all suspend threads */
rt_ipc_list_resume_all(&(sem->parent.suspend_thread));
/* detach semaphore object */
rt_object_detach(&(sem->parent.parent));
return RT_EOK;
}
調用該函數時會先將所有掛在該信號量等待隊列上的線程喚醒,然後從對象管理器中移除。
在RTT中的P函數爲rt_sem_take,這個函數成功調用將會對信號量值減1,當信號量值小於1的時候,將會阻塞,直到信號量值大於1時。從而實現同步。
rt_err_t rt_sem_take(rt_sem_t sem, rt_int32_t time)
{
......
if (sem->value > 0)
{
/* semaphore is available */
sem->value --;
/* enable interrupt */
rt_hw_interrupt_enable(temp);
}
else
{
/* no waiting, return with timeout */
if (time == 0)
{
rt_hw_interrupt_enable(temp);
return -RT_ETIMEOUT;
}
else
{
/* current context checking */
RT_DEBUG_IN_THREAD_CONTEXT;
/* semaphore is unavailable, push to suspend list */
/* get current thread */
thread = rt_thread_self();
/* reset thread error number */
thread->error = RT_EOK;
RT_DEBUG_LOG(RT_DEBUG_IPC, ("sem take: suspend thread - %s\n",
thread->name));
/* suspend thread */
rt_ipc_list_suspend(&(sem->parent.suspend_thread),
thread,
sem->parent.parent.flag);
/* has waiting time, start thread timer */
if (time > 0)
{
RT_DEBUG_LOG(RT_DEBUG_IPC, ("set thread:%s to timer list\n",
thread->name));
/* reset the timeout of thread timer and start it */
rt_timer_control(&(thread->thread_timer),
RT_TIMER_CTRL_SET_TIME,
&time);
rt_timer_start(&(thread->thread_timer));
}
/* enable interrupt */
rt_hw_interrupt_enable(temp);
/* do schedule */
rt_schedule();
if (thread->error != RT_EOK)
{
return thread->error;
}
}
}
......
}
參數sem爲信號量的句柄,time爲等待時間,當time = 0(RT_WAITING_NO)時,不等待,當 time = -1(RT_WAITING_FOREVER)則爲永遠等待。
當value大於0時,也就是take成功,否則進入等待狀態。如果time = 0,則直接返回,不等待。
如果time不爲0,則將該線程掛到該信號量的suspend_thread鏈表上,根據flag來決定是順序插入鏈表還是根據優先級插入鏈表中。
rt_inline rt_err_t rt_ipc_list_suspend(rt_list_t *list,
struct rt_thread *thread,
rt_uint8_t flag)
{
/* suspend thread */
rt_thread_suspend(thread);
switch (flag)
{
case RT_IPC_FLAG_FIFO:
rt_list_insert_before(list, &(thread->tlist));
break;
case RT_IPC_FLAG_PRIO:
{
struct rt_list_node *n;
struct rt_thread *sthread;
/* find a suitable position */
for (n = list->next; n != list; n = n->next)
{
sthread = rt_list_entry(n, struct rt_thread, tlist);
/* find out */
if (thread->current_priority < sthread->current_priority)
{
/* insert this thread before the sthread */
rt_list_insert_before(&(sthread->tlist), &(thread->tlist));
break;
}
}
//當n爲空時
/*
* not found a suitable position,
* append to the end of suspend_thread list
*/
if (n == list)
rt_list_insert_before(list, &(thread->tlist));
}
break;
}
return RT_EOK;
}
rt_ipc_list_suspend首先將當前的線程掛起,掛起時thread->tlist就會從就緒鏈表中移除,加入信號量的鏈表中。
由於rt_sem_take可能會阻塞,所以不能用在中斷裏面,爲了方便中斷裏也可以,RTT引入rt_sem_trytake,其實就是將rt_sem_take的time設置爲0(RT_WAITING_NO)了。
rt_err_t rt_sem_trytake(rt_sem_t sem)
{
return rt_sem_take(sem, 0);
}
接下來是RTT的V函數:
rt_err_t rt_sem_release(rt_sem_t sem)
{
...
need_schedule = RT_FALSE;
......
if (!rt_list_isempty(&sem->parent.suspend_thread))
{
/* resume the suspended thread */
rt_ipc_list_resume(&(sem->parent.suspend_thread));
need_schedule = RT_TRUE;
}
else
sem->value ++; /* increase value */
......
/* resume a thread, re-schedule */
if (need_schedule == RT_TRUE)
rt_schedule();
......
}
rt_inline rt_err_t rt_ipc_list_resume(rt_list_t *list)
{
struct rt_thread *thread;
/* get thread entry */
thread = rt_list_entry(list->next, struct rt_thread, tlist);
RT_DEBUG_LOG(RT_DEBUG_IPC, ("resume thread:%s\n", thread->name));
/* resume it */
rt_thread_resume(thread);
return RT_EOK;
}
V函數中先設置了一個變量need_schedule,來判斷是否等一下需要調度。接着判斷suspend_thread是否掛有線程,如果有就resume喚醒該線程,調用rt_thread_resume將會使線程從suspend_thread鏈表上移除,掛到調度就緒鏈表上。如果suspend_thread沒有等待該信號量的線程,則將信號量值加1。再判斷是否需要調用調度函數。
關於信號量就介紹到這裏。
測試
接下來就是我的RTT-MINI了,信號量也比較實現起來簡單,這裏就只有FIFO,沒有優先級之分。這裏的P函數爲sema_down,V函數爲sema_up。