Zephys OS nano 內核篇:信號量 semaphore

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);
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章