Zephys OS 內核篇:初識線程

Zephyr OS 所有的學習筆記已託管到 Github,CSDN 博客裏的內容只是 Github 裏內容的拷貝,因此鏈接會有錯誤,請諒解。

最新的學習筆記請移步 GitHub:https://github.com/tidyjiang8/zephyr-inside

本文講解 Zephyr OS 用於描述線程相關信息的結構體,內核中幾乎其它所有服務都或多或少地使用了該結構體,所以在正式進入內核相關部分的學習之前,我們先學習該結構體。此外,還介紹了一下如何新建一個線程。

線程結構體的定義

在 Zephyr OS 中,用結構體 struct tcs 描述一個線程的控制信息:

struct tcs {
    struct tcs *link;
    uint32_t flags;
    uint32_t basepri;
    int prio;
#ifdef CONFIG_THREAD_CUSTOM_DATA
    void *custom_data;
#endif
    struct coop coopReg;
    struct preempt preempReg;
#if defined(CONFIG_THREAD_MONITOR)
    struct __thread_entry *entry;
    struct tcs *next_thread;
#endif
#ifdef CONFIG_NANO_TIMEOUTS
    struct _nano_timeout nano_timeout;
#endif
#ifdef CONFIG_ERRNO
    int errno_var;
#endif
#ifdef CONFIG_MICROKERNEL
    void *uk_task_ptr;
#endif
};

注意:由於該結構體涉及到芯片的寄存器,所以不同架構的芯片的線程控制結構體是有區別的。本文討論cortex-m3 的線程控制結構。另外,由於與具體芯片相關,所以將 Zephyr OS 移植到其它架構的芯片時,需要考慮移植線程相關的代碼。

  • link:多個線程可以構成一個線程鏈表,link 就指向該鏈表中的下一個線程。例如,處於就緒狀態的線程會形成一個就緒隊列,等待線程會形成一個等待隊列,具體信息請參考《Zephyr OS nano 內核篇: fiber》和《Zephyr OS nano 內核篇:等待隊列 wait_q》
  • flags:用來表示該線程具有哪些 flag,這些 flag 是系統預定義的位掩碼。

    所謂的位掩碼,是指每個每個掩碼佔 1 位。
    “`

    define FIBER 0x000 // BIT(0) 爲 0 表示該線程是 fiber

define TASK 0x001 // BIT(0) 爲 1 表示該線程是 task

define INT_ACTIVE 0x002 // BIT(1) 爲 1 表示執行上下文是中斷 handler

define EXC_ACTIVE 0x004 // BIT(2) 爲 1 表示執行上下文是異常

define USE_FP 0x010 // BIT(4) 爲 1 表示該線程使用浮點單元

define PREEMPTIBLE 0x020 // BIT(5) 爲 1 表示該線程可被搶佔。

       /*
       * NOTE: the value must be < 0x100 to be able to \
       *       use a small thumb instr with immediate  \
       *       when loading PREEMPTIBLE in a GPR       \
       */

define ESSENTIAL 0x200 // BIT(9) 爲 1 表示該線程不能被終止

define NO_METRICS 0x400 // BIT(10)爲 1 表示_Swap() not to update task metrics

“`

  • basepri:用於上下文切換時的現場保存與恢復。具體信息請參考《Zephyr OS nano 內核篇: 上下文切換》。
  • prio:指定本線程的優先級。就緒鏈表中的線程就是按照優先級的順序排列的。
  • custom_data:線程自定義數據。
  • coopReg:對於 Cortex-M 系列,該變量沒有使用。
  • preempReg:也是用於上下文切換時的現場保存與恢復。

    struct preempt {
    uint32_t v1;  /* r4 */
    uint32_t v2;  /* r5 */
    uint32_t v3;  /* r6 */
    uint32_t v4;  /* r7 */
    uint32_t v5;  /* r8 */
    uint32_t v6;  /* r9 */
    uint32_t v7;  /* r10 */
    uint32_t v8;  /* r11 */
    uint32_t psp; /* r13 */
    };
  • entry:函數指針,指向線程的入口函數(即線程的執行體)和參數,當線程被調用時候將調用該函數。

    struct __thread_entry {
      _thread_entry_t pEntry; // 指向線程的入口函數
    void *parameter1;       // 指向入口函數的第一個參數 arg1
    void *parameter2;       // 指向入口函數的第二個參數 arg2
    void *parameter3;       // 指向入口函數的第三個參數 arg3
    };
    // 線程的入口函數的函數原型
    typedef void (*_thread_entry_t)(_thread_arg_t arg1,
                                _thread_arg_t arg2,
                                _thread_arg_t arg3);
  • next_thread:與 link 類似,指向線程構成的鏈表中的下一個線程。不過 next_thread 指向的鏈表是由內核中所有的 fiber 和 task 構成的鏈表。

  • nano_timeout:指定該線程所綁定的超時服務。關於超時服務,請參考《Zephyr OS nano 內核篇:超時服務 timeout》
  • errno_var:錯誤號。
  • uk_task_ptr:與microkernel相關,目前還不知道是幹嘛的。

創建一個新線程

void _new_thread(char *pStackMem, unsigned stackSize,
         void *uk_task_ptr, _thread_entry_t pEntry,
         void *parameter1, void *parameter2, void *parameter3,
         int priority, unsigned options)
{
    char *stackEnd = pStackMem + stackSize;
    struct __esf *pInitCtx;
    struct tcs *tcs = (struct tcs *) pStackMem;

#ifdef CONFIG_INIT_STACKS
    // 初始化線程棧的內容,讓其每個字節都被初始化爲0xaa。
    // 如果不初始化,則棧幀中被填充爲0x00,這是爲什麼?
    //     因爲線程棧的本質是一個全局變量,而全局變量默認被初始化爲0
    // 如何知道線程棧的本質是一個全局變量?
    //     追蹤代碼,查看調用_new_thread 的地方,看看傳進來的參數不就知道了麼!
    memset(pStackMem, 0xaa, stackSize);
#endif
    // STACK_ROUND_DOWN(pointer) 的作用請參考【說明1】
    // pInitCtx 用來保存棧幀的信息,即線程的上下文信息,請參考【說明2】
    pInitCtx = (struct __esf *)(STACK_ROUND_DOWN(stackEnd) -
                    sizeof(struct __esf));
    pInitCtx->pc = ((uint32_t)_thread_entry) & 0xfffffffe;
    pInitCtx->a1 = (uint32_t)pEntry;
    pInitCtx->a2 = (uint32_t)parameter1;
    pInitCtx->a3 = (uint32_t)parameter2;
    pInitCtx->a4 = (uint32_t)parameter3;
    pInitCtx->xpsr =
        0x01000000UL; /* clear all, thumb bit is 1, even if RO */

    // 初始化 link、flag和prio
    tcs->link = NULL;
    tcs->flags = priority == -1 ? TASK | PREEMPTIBLE : FIBER;
    tcs->prio = priority;

#ifdef CONFIG_THREAD_CUSTOM_DATA
    tcs->custom_data = NULL;
#endif

#ifdef CONFIG_THREAD_MONITOR
    // 指定線程的入口函數和參數
    tcs->entry = (struct __thread_entry *)(pInitCtx);
#endif

#ifdef CONFIG_MICROKERNEL
    tcs->uk_task_ptr = uk_task_ptr;
#else
    ARG_UNUSED(uk_task_ptr);
#endif

    tcs->preempReg.psp = (uint32_t)pInitCtx;
    tcs->basepri = 0;

    // 初始化超時服務,具體信息請參考《Zephyr OS nano 內核篇:超時服務 timeout》
    _nano_timeout_tcs_init(tcs);

    /* initial values in all other registers/TCS entries are irrelevant */

    THREAD_MONITOR_INIT(tcs);
}

先看一下主要的入參:
- pStackMem:指定線程棧的起始地址
- stackSize:指定線程棧的大小
- pEntry:指定線程的入口函數的地址
- parameter1、parameter2、parameter3:指定傳遞給入口函數的參數。
- 其它:其它一些線程相關的設置,不影響我們理解線程的本質

_new_thread() 的主要任務:
- 爲線程分配一段棧空間(其實是調用_new_thread()的函數分配的)。
- 爲線程指定一個入口函數以及它的參數。
- 對線程棧進行初始化,包括:
- 在線程棧的低地址處,存儲一個結構體 struct tcs,用來保存線程的控制信息。
- 在線程棧的高地址處,存儲一個結構體 struct __esf,用來保存線程的上下文。

用一張圖可以很好地總結這個函數:

線程棧的內存分佈圖

然後再看看本函數的最後一條語句 THREAD_MONITOR_INIT(tcs) ,它涉及到內核中的一個線程鏈表。

#define THREAD_MONITOR_INIT(tcs) _thread_monitor_init(tcs)

static ALWAYS_INLINE void _thread_monitor_init(struct tcs *tcs /* thread */
                       )
{
    unsigned int key;

    key = irq_lock();
    tcs->next_thread = _nanokernel.threads;
    _nanokernel.threads = tcs;
    irq_unlock(key);
}

_nanokernel 是我們下一節《Zephyr OS nano 內核篇:內核大總管_nanokernel》的主角,是內核中定義的一個全局變量。它有一個成員 threads,指向內核中所有線程構成的一個單鏈表。

_thread_monitor_init() 的作用是將線程加入到該鏈表的表頭。

關於內核中的線程構成的各種鏈表,請參考《Zephyr OS nano 內核篇:總結》。

【說明1】

STACK_ROUND_DOWN(x)和STACK_ROUND_UP(x)這對宏的作用是確保棧空間是 STACK_ALIGN_SIZE 字節對齊的。

當 x 表示棧空間的高地址時,需要調用 STACK_ROUND_DOWN(x) 以確保 x 是 STACK_ALIGN_SIZE 字節對齊的,它的主要思想如下:
- 如果 x 本來就是 STACK_ALIGN_SIZE 字節對齊的,則不做如何處理
- 如果 x 不是 STACK_ALIGN_SIZE 字節對齊的,它會捨棄高地址的 0 ~ (STACK_ALIGN_SIZE - 1) 字節,以確保 x 是 STACK_ALIGN_SIZE 字節對齊的。

當 x 表示棧空間的低地址時,需要調用 STACK_ROUND_UP(x) 以確保 x 是 STACK_ALIGN_SIZE 字節對齊的,它的主要思想與 STACK_ROUND_DOWN(x) 類似。

【說明2】

在《Zephyr OS 內核篇:上下文》一文中,我們已經知道,Zephyr 中的上下文分爲三種:fiber、task 和中斷,但是對於上下文具體值的什麼,我們還不清楚。

個人理解,所謂的上下文指的就是線程在運行時芯片內部的環境,即芯片中相關寄存器的值。通過這些寄存器的值,能唯一確定一個線程的運行。那麼,哪些因素將確定一個唯一的線程呢?

對於 Cortex-M 系列,內核定義瞭如下結構體來保存上下文的寄存器:

struct __esf {
    //sys_define_gpr_with_alias 用來定義成員的別名,其本質就是一個聯合體
    sys_define_gpr_with_alias(a1, r0);
    sys_define_gpr_with_alias(a2, r1);
    sys_define_gpr_with_alias(a3, r2);
    sys_define_gpr_with_alias(a4, r3);
    sys_define_gpr_with_alias(ip, r12);
    sys_define_gpr_with_alias(lr, r14);
    sys_define_gpr_with_alias(pc, r15);
    uint32_t xpsr;
#ifdef CONFIG_FLOAT
    float s[16];
    uint32_t fpscr;
    uint32_t undefined;
#endif
};

其中,
- a1:用來保存線程入口函數的地址。
- a2:用來保存線程入口函數的第一個參數的值。
- a3:用來保存線程入口函數的第二個參數的值。
- a4:用來保存線程入口函數的第三個參數的值。
- ip:不知。。。
- lr:用來保存線程返回時的地址。
- pc:當線程執行到一半時,可能被切換出去(比如時間片到期了),而 pc 指針就可以用來保存被切換出去時的地址。
- xpsr:用來保存線程當前的狀態。

總結,用來保存線程上下文的變量包括:
- struct __esf 中的r0,r1,r2,r3,r12,r14,r15,xpsr
- struce tcs 中的 struct preempt 中的r4,r5,r6,r7,r8,r9,r10,r11,r13
- struct tcs 中的 basepri

即保存的寄存器現場包括:r0-r15, xpsr, basepri

退出一個線程

void _thread_exit(struct tcs *thread)
{
    if (thread == _nanokernel.threads) {
        // 如果該線程是鏈表中的頭結點,直接刪除之
        _nanokernel.threads = _nanokernel.threads->next_thread;
    } else {
        // 如果該線程不是鏈表中的頭結點,先查找到該節點,再刪除之
        struct tcs *prev_thread;

        prev_thread = _nanokernel.threads;
        while (thread != prev_thread->next_thread) {
            prev_thread = prev_thread->next_thread;
        }
        prev_thread->next_thread = thread->next_thread;
    }
}

將線程從 _nanokernel.threads 指向的線程鏈表中刪除。

線程的本質

我們可以從邏輯上將線程看成兩部分:
- 線程的執行實體,即線程的入口函數
- 線程棧

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章