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 指向的線程鏈表中刪除。
線程的本質
我們可以從邏輯上將線程看成兩部分:
- 線程的執行實體,即線程的入口函數
- 線程棧