Zephys OS 内核篇:初识线程
来源:互联网 发布:ios webview js 交互 编辑:程序博客网 时间:2024/06/05 04:27
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; // 指向入口函数的第一个参数 arg1void *parameter2; // 指向入口函数的第二个参数 arg2void *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 指向的线程链表中删除。
线程的本质
我们可以从逻辑上将线程看成两部分:
- 线程的执行实体,即线程的入口函数
- 线程栈
- Zephys OS 内核篇:初识线程
- Zephys OS nano 内核篇:前言
- Zephys OS nano 内核篇:isr 服务
- Zephys OS nano 内核篇:fiber 服务
- Zephys OS nano内核篇:原子操作
- Zephys OS nano内核篇:定时器 Timer
- Zephys OS nano 内核篇:信号量 semaphore
- Zephys OS nano 内核篇:FIFO
- Zephys OS nano 内核篇:LIFO
- Zephys OS nano 内核篇:栈 stack
- Zephys OS 内核篇:内核大总管 _nanokernel
- Zephys OS nano 内核篇:task 服务 - 基础
- Zephys OS nano 内核篇:fiber 服务 - 基础
- Zephys OS nano内核篇:超时服务timeout
- Zephys OS nano 内核篇:环形缓冲 Ring Buffer
- Zephys OS 基础篇:漫谈Zephyr与Contiki的未来
- OS中的内核线程与用户线程
- OS初识
- Zephys OS nano 内核篇:isr 服务
- 例子
- C++ 对象和实例的区别,以及用new和不用new创建类对象区别
- 第三十五讲 项目 另类求和,贪财的大富翁等
- refresh group cmd
- Zephys OS 内核篇:初识线程
- 电脑事件日志服务报错1747,未知身份错误
- hdoj5972Fraction
- opencv立体视觉的实现流程
- Matlab离线训练好的神经网络在Visual Studio中调用
- swift中方法参数与返回值
- markdown庆祝火影动漫即将完结
- Zephys OS 内核篇:内核大总管 _nanokernel
- Java入门第二季-4.多态