Zephyr OS 所有的學習筆記已託管到 Github,CSDN 博客裏的內容只是 Github 裏內容的拷貝,因此鏈接會有錯誤,請諒解。
最新的學習筆記請移步 GitHub:https://github.com/tidyjiang8/zephyr-inside
信號量是 Zephyr OS 提供的用於不同線程間同步的機制。目前,Zephyr OS 只提供了信號量這一種同步機制。
信號量的類型定義
struct nano_sem {
struct _nano_queue wait_q;
int nsig;
#ifdef CONFIG_MICROKERNEL
struct _nano_queue task_q; /* waiting tasks */
#endif
#ifdef CONFIG_DEBUG_TRACING_KERNEL_OBJECTS
struct nano_sem *__next;
#endif
};
不考慮 microkernel 的 task_q 和用於調試的 __next,信號量包含兩個成員:
- nsig:信號量的值,即信號量的信號。當該值爲 0,表示沒有有效信號;當該值大於 0,表示有信號可以獲取。
- wait_q:該信號量維護的等待隊列。當一個線程試圖獲取無效的信號量(即信號量的值 nsig 爲零)時,它可能會加入到這個等待隊列中,然後陷入阻塞狀態。
信號量的初始化
void nano_sem_init(struct nano_sem *sem)
{
sem->nsig = 0;
_nano_wait_q_init(&sem->wait_q);
SYS_TRACING_OBJ_INIT(nano_sem, sem); // 用於調試
_TASK_PENDQ_INIT(&sem->task_q); // microkernel 才使用的
}
初始化信號量結構體中各成員:
- 將信號量的值初始化爲 0。
- 初始化信號量的等待隊列。
獲取信號
int nano_sem_take(struct nano_sem *sem, int32_t timeout)
{
static int (*func[3])(struct nano_sem *, int32_t) = {
nano_isr_sem_take,
nano_fiber_sem_take,
nano_task_sem_take
};
return func[sys_execution_context_type_get()](sem, timeout);
}
先看看函數的入參:
- sem:希望獲取信號的信號量。
- timeout:獲取信號的超時等待時間,以滴答爲單位。函數內部會根據該變量的值來做相應的處理。
再看看函數的返回值:
- 1 - 表示獲取信號成功
- 0 - 表示獲取信號失敗
nano_sem_take 會根據當前上下文的類型,調用對應的獲取信號的函數。其中,nano_isr_sem_take() 和 nano_fiber_sem_take() 是函數 _sem_take() 的別名。
_sem_take
int _sem_take(struct nano_sem *sem, int32_t timeout_in_ticks)
{
unsigned int key = irq_lock();
if (likely(sem->nsig > 0)) {
// nsig > 0,說明可以獲取信號
// 然後將信號量的值 nsig 遞減
// 然後返回 1,表示獲取信號成功
sem->nsig--;
irq_unlock(key);
return 1;
}
// 代碼走到這裏,說明當前沒有有效的信號
if (timeout_in_ticks != TICKS_NONE) {
// 將當前線程加入內核的超時鏈表中,並綁定該超時節點的等待隊列爲
// 當前信號量所維護的等待隊列
_NANO_TIMEOUT_ADD(&sem->wait_q, timeout_in_ticks);
// 將當前線程加入到該信號量維護的等待隊列
_nano_wait_q_put(&sem->wait_q);
// return _Swap(key);
/* 原文是 return _Swap(key),但是爲了分析說明,我們將其拆分爲下面兩句話 */
int value = _Swap(key);
// 執行完 _Swap() 函數後,將會切換到其它上下文。如果該函數返回了,有兩種可能:
// 1. 有線程向該信號量中添加了限號,喚醒了本線程,且在喚醒前設置了本線程的返回值,
// 具體信息可參考函數 _sem_give_non_preemptible
// 2. 由於等待超時,超時服務喚醒了本線程,本線程的返回值沒有被設置,返回默認值 0
reutrn value;
}
// 代碼走到這裏,說明獲取信號失敗,且立即返回
irq_unlock(key);
return 0;
}
likely 是編譯器內嵌的關鍵字,編譯器會根據這個關鍵字對代碼進行相應的優化,閱讀代碼時完全可以忽略。
當線程嘗試從信號量中獲取信號時,有兩種可能:
- 信號量中有有效信號:即 nsig 的值大於 0,將 nsig 遞減,獲取信號成功。
- 信號量中沒有有效信號:即 nsig 的值等於 0,無法獲取信號,此時會根據入參 timeout_in_ticks 的值來做對應的處理:
- 等於 TICKS_NONE,表示當信號失敗時,不等待,立即返回
- 不等於 TICKS_NONE,表示獲取該信號的線程將陷入阻塞狀態。在它陷入阻塞後,有兩種可能
- 在 timeout_in_ticks 期間內,有另外一個線程向該信號量中添加了一個信號,等待線程將被添加信號的線程喚醒,獲取信號成功。
- 在 timeout_in_ticks 期間內,沒有線程向該信號量中添加值,那麼等待將超時,定時器會將該線程喚醒,獲取信號失敗。
nano_task_sem_take
int nano_task_sem_take(struct nano_sem *sem, int32_t timeout_in_ticks)
{
int64_t cur_ticks;
int64_t limit = 0x7fffffffffffffffll; // 爲什麼這樣初始化,不解
unsigned int key;
key = irq_lock();
// 獲取當前的系統滴答數
cur_ticks = _NANO_TIMEOUT_TICK_GET();
if (timeout_in_ticks != TICKS_UNLIMITED) {
// 計算等待時間到期後的滴答數
limit = cur_ticks + timeout_in_ticks;
}
do {
if (likely(sem->nsig > 0)) {
// nsig > 0,說明可以獲取信號
// 然後將信號量的值 nsig 遞減
// 然後返回 1,表示獲取信號成功
sem->nsig--;
irq_unlock(key);
return 1;
}
if (timeout_in_ticks != TICKS_NONE) {
// 讓 task 等待,具體請見後面的分析
_NANO_OBJECT_WAIT(&sem->task_q, &sem->nsig,
timeout_in_ticks, key);
// 獲取並更新當前的滴答數,用作循環的判斷條件,判斷是否跳出循環
cur_ticks = _NANO_TIMEOUT_TICK_GET();
_NANO_TIMEOUT_UPDATE(timeout_in_ticks,
limit, cur_ticks);
}
// 如果 timeout_in_ticks 等於 TICKS_NONE,那麼 cur_ticks 等於 limit
// 循環判斷的條件將失敗,跳出循環,本函數立即返回,獲取信號失敗
} while (cur_ticks < limit);
irq_unlock(key);
return 0;
}
_NANO_OBJECT_WAIT() 是一個宏,對於 nanokernel,它的原型如下:
#define _NANO_OBJECT_WAIT(queue, data, timeout, key) \
do { \
_NANO_TIMEOUT_SET_TASK_TIMEOUT(timeout); \
nano_cpu_atomic_idle(key); \
key = irq_lock(); \
} while (0)
_NANO_TIMEOUT_SET_TASK_TIMEOUT()是設置內核大總管 _nanokernel 中 task_timeout 的值,目前還沒發現這個有什麼用。
nano_cpu_atomic_idle() 是用匯編實現的一個函數,它將使 CPU 進入睡眠狀態,並在接收到中斷後被喚醒。其原型如下:
SECTION_FUNC(TEXT, nano_cpu_atomic_idle)
// 本函數使用了兩個通用寄存器,r0 和 r1。
// r0:調用者傳遞進來的中斷的 mask
// r1:用於設置 BASEPRI 的值
// 異或操作,將 r1 與 r1,進行異或操作,將結果保存到 r1 中,所以 r1 裏面的值爲 0
// .n 用於告訴編譯器使用 16 位的指令進行彙編
eors.n r1, r1
// 鎖定 PRIMASK, 此時系統不會處理中斷
cpsid i
// 將 0 寫入寄存器 BASEPRI,此時系統將能夠接收**所有**中斷
msr BASEPRI, r1
// 此時的狀態:能接收中斷,但是先不處理中斷
// wfe,Wait For Event,等待事件的到來。執行這個指令後,CPU 將進入睡眠狀態,進入低功
// 耗模式。更具體地說,執行 wfe 指令後,CPU core 將會發送一個信號,pmu 收到這個信號後,
// 將關閉 core 的 clock,甚至進入 retention 模式(降低電壓,但是保存寄存器和cache 的值)。
wfe
// 此時 cpu 陷入睡眠了,不再取指執行。
// 當有外部中斷到來時,將會喚醒 CPU,然後繼續執行下面的指令.
// 恢復睡眠之前的中斷優先級
msr BASEPRI, r0
// 允許執行之前檢測到的中斷信號的 ISR
cpsie i
// 可能會在這裏執行 ISR.
// 返回調用函數
bx lr
釋放(發送)信號
void nano_sem_give(struct nano_sem *sem)
{
static void (*func[3])(struct nano_sem *sem) = {
nano_isr_sem_give,
nano_fiber_sem_give,
nano_task_sem_give
};
func[sys_execution_context_type_get()](sem);
}
根據當前上下文的類型,調用對應的添加信號量的函數。其中,nano_isr_sem_give() 和 nano_fiber_sem_give() 是函數 _sem_give_non_preemptible()的別名。
我之所以將這個函數叫做釋放信號或發送信號,是因爲信號量的兩種應用場景:
- 當一個線程對某個臨界資源訪問前,它先要獲取信號,訪問完後,還需要釋放信號
- 當一個線程獲取信號時,被陷入阻塞狀態。此時如果有另一個線程釋放了信號,阻塞的線程將被喚醒。我們可以將其利皆爲該線程對阻塞的線程發送了一個喚醒信號。
_sem_give_non_preemptible
void _sem_give_non_preemptible(struct nano_sem *sem)
{
struct tcs *tcs;
unsigned int imask;
imask = irq_lock();
// 取出信號量的等待隊列的隊首數據,判斷有沒有線程在等待信號
tcs = _nano_wait_q_remove(&sem->wait_q);
if (!tcs) {
// 如果沒有線程在等待信號,直接將信號量的值遞增
// 此外,_nano_wait_q_remove 內部會將隊首的線程加入到就緒隊列
// 即該線程的狀態由阻塞態變爲了就緒態
sem->nsig++;
_NANO_UNPEND_TASKS(&sem->task_q);
} else {
// 如果有線程在等待信號,將該線程從超時隊列中刪除
_nano_timeout_abort(tcs);
// 此時不設置信號量的值,直接設置等待線程的返回值爲 1
set_sem_available(tcs);
}
irq_unlock(imask);
}
前面在從信號量中獲取信號時,如果線程獲取信號失敗,做了兩件事兒:
- 將線程阻塞(加入等待隊列中)
- 將線程加入超時鏈表中
對應的,在往信號量中添加信號時,如果判斷出有線程處於等待狀態,也做了兩件事兒:
- 將阻塞的線程添加到就緒鏈表中
- 將線程從超時鏈表中刪除
nano_task_sem_give
void nano_task_sem_give(struct nano_sem *sem)
{
struct tcs *tcs;
unsigned int imask;
imask = irq_lock();
// 取出信號量的等待隊列的隊首數據,判斷有沒有線程在等待信號
tcs = _nano_wait_q_remove(&sem->wait_q);
if (tcs) {
// 如果有線程在等待信號,將該線程從超時隊列中刪除
// 此外,_nano_wait_q_remove 內部會將隊首的線程加入到就緒隊列
_nano_timeout_abort(tcs);
// 此時不設置信號量的值,直接設置等待線程的返回值爲 1
set_sem_available(tcs);
由於 wait_q 中保存的線程是 fiber,而當前的線程是 task,所以讓出 cpu,讓該 fiber 先運行。
_Swap(imask);
return;
}
// 代碼走到這裏,說明之前沒有線程在等待信號
// 將信號量的值遞增
sem->nsig++;
_TASK_NANO_UNPEND_TASKS(&sem->task_q);
irq_unlock(imask);
}