RT-Thread内核实现(一):线程与调度

  • 线程是一个独立的、无限循环且不能返回的函数。

  • 系统为了顺利的调度线程,为每个线程都额外定义了一个线程控制块,这个线程控制块就相当于线程的身份证,里面存有线程的所有信息,比如线程的栈指针线程名称线程的形参等。

    rtdef.h中定义了线程控制块结构体

/*
*************************************************************************
*                               线程结构体
*************************************************************************
*/

struct rt_thread
{
	void        *sp;	          /* 线程栈指针 */
	void        *entry;	          /* 线程入口地址 */
	void        *parameter;	      /* 线程形参 */	
	void        *stack_addr;      /* 线程起始地址 */
	rt_uint32_t stack_size;       /* 线程栈大小,单位为字节 */
	
	rt_list_t   tlist;            /* 线程链表节点 */
};
typedef struct rt_thread *rt_thread_t;
一、创建线程
  1. 线程初始化
rt_err_t rt_thread_init(struct rt_thread *thread,
                        void (*entry)(void *parameter),
                        void             *parameter,
                        void             *stack_start,
                        rt_uint32_t       stack_size)
{
	rt_list_init(&(thread->tlist));	// 初始化链表节点
	
	thread->entry = (void *)entry;	// 线程函数入口地址
	thread->parameter = parameter;	// 参数地址

	thread->stack_addr = stack_start;	// 线程栈的其实地址
	thread->stack_size = stack_size;	// 线程栈大小,单位是字节
	
	/* 初始化线程栈,并返回线程栈指针 */
	thread->sp = (void *)rt_hw_stack_init( thread->entry, 
		                                   thread->parameter,
							               (void *)((char *)thread->stack_addr + thread->stack_size - 4) );
	
	return RT_EOK;
}
  • 初始化线程链表节点,往后我们要把线程插入到各种链表中(就绪列表,休眠列表),就是通过这个节点来实现的,它就好像是线程控制块里面的一个钩子,可以把线程控制块挂在各种链表中。(双向链表)
  • 初始化线程栈,返回剩余栈的栈顶地址(栈向低地址生长,个人觉得叫栈底比较好)。ATPCS规定,ARM 的数据堆栈为FD(Full Decrease) 型堆栈,“向下生长的满栈”模型。
      线程第一次运行的时候,加载到 CPU 寄存器的环境参数我们要预先初始化好。从栈顶开始,初始化的顺序固定,首先是异常发生时自动保存的 8 个寄存器,即 xPSR、 R15、 R14、 R12、 R3、 R2、 R1 和 R0。其中xPSR 寄存器的位 24 必须是 1(Thumb state, always 1;), R15(PC) 指针必须存的是线程的入口地址, R0 必须是线程形参地址,剩下的 R14、 R12、 R3、 R2 和R1 我们初始化为 0。
    在这里插入图片描述
    线程栈初始化代码在cpuport.c
/* 线程栈初始化 */
rt_uint8_t *rt_hw_stack_init(void       *tentry,
                             void       *parameter,
                             rt_uint8_t *stack_addr)
{
	
	
	struct stack_frame *stack_frame;
	rt_uint8_t         *stk;
	unsigned long       i;
	
	
	/* 获取栈顶指针
	 rt_hw_stack_init 在调用的时候,传给stack_addr的是(栈顶指针)*/
	stk  = stack_addr + sizeof(rt_uint32_t);
	
	/* 让stk指针向下8字节对齐,浮点运算要 8 字节对齐( 但是目前我们都还没有涉及到浮点运算,只是为了后续兼容浮点运行的考虑) 。*/
	stk  = (rt_uint8_t *)RT_ALIGN_DOWN((rt_uint32_t)stk, 8);
	
	/* stk指针继续向下移动sizeof(struct stack_frame)个偏移 */
	stk -= sizeof(struct stack_frame);
	
	/* 将stk指针强制转化为stack_frame类型后存到stack_frame */
	stack_frame = (struct stack_frame *)stk;
	
	/* 以stack_frame为起始地址,将栈空间里面的sizeof(struct stack_frame)
	个内存初始化为0xdeadbeef */
	for (i = 0; i < sizeof(struct stack_frame) / sizeof(rt_uint32_t); i ++)
	{
			((rt_uint32_t *)stack_frame)[i] = 0xdeadbeef;
	}
	
	/* 初始化异常发生时自动保存的寄存器 */
	stack_frame->exception_stack_frame.r0  = (unsigned long)parameter; /* r0 : argument */
	stack_frame->exception_stack_frame.r1  = 0;                        /* r1 */
	stack_frame->exception_stack_frame.r2  = 0;                        /* r2 */
	stack_frame->exception_stack_frame.r3  = 0;                        /* r3 */
	stack_frame->exception_stack_frame.r12 = 0;                        /* r12 */
	stack_frame->exception_stack_frame.lr  = 0;                        /* lr */
	stack_frame->exception_stack_frame.pc  = (unsigned long)tentry;    /* entry point, pc */
	stack_frame->exception_stack_frame.psr = 0x01000000L;              /* PSR */
	
	/* 返回线程栈指针 */
	return stk;
}
  1. 将线程插入就绪列表
  • 就绪列表实际上就是一个 rt_list_t 类型的数组,在scheduler.c中定义,数组的大小由决定最大线程优先级的宏 RT_THREAD_PRIORITY_MAX 决 定 ,RT_THREAD_PRIORITY_MAXrtconfig.h 中默认定义为 32。
/*
*************************************************************************
*                               双向链表结构体
*************************************************************************
*/
struct rt_list_node
{
    struct rt_list_node *next;              /* 指向后一个节点 */
    struct rt_list_node *prev;              /* 指向前一个节点 */
};
typedef struct rt_list_node rt_list_t; 

/* 线程就绪列表 */
rt_list_t rt_thread_priority_table[RT_THREAD_PRIORITY_MAX];

/* 线程休眠列表 */
rt_list_t rt_thread_defunct;

rt_thread_priority_table[0]挂着优先级最高的任务列表。

二、调度器
  1. 调度器的初始化。调度器是操作系统的核心,其主要功能就是实现线程的切换,即从就绪列表里面找到优先级最高的线程,然后去执行该线程。全部都在**scheduler.c **文件中实现。
      I. 初始化就绪列表(优先级列表)。
      II. 初始化当前线程控制块指针。
      III. 初始化休眠列表。
/* 初始化系统调度器 */
void rt_system_scheduler_init(void)
{	
	register rt_base_t offset;	

	
	/* 线程就绪列表初始化 */
	for (offset = 0; offset < RT_THREAD_PRIORITY_MAX; offset ++)
	{
			rt_list_init(&rt_thread_priority_table[offset]);
	}
	
	/* 初始化当前线程控制块指针 */
	rt_current_thread = RT_NULL;
	
	/* 初始化线程休眠列表,当线程创建好没有启动之前会被放入到这个列表 */
	rt_list_init(&rt_thread_defunct);
}
  1. 启动调度器
/* 启动系统调度器 */
void rt_system_scheduler_start(void)
{
	register struct rt_thread *to_thread;
	

	/* 手动指定第一个运行的线程 */
	to_thread = rt_list_entry(rt_thread_priority_table[0].next,
							  struct rt_thread,
							  tlist);
	rt_current_thread = to_thread;
														
	/* 切换到第一个线程,该函数在context_rvds.S中实现,在rthw.h声明,
       用于实现第一次任务切换。当一个汇编函数在C文件中调用的时候,
       如果有形参,则执行的时候会将形参传人到CPU寄存器r0。*/
	rt_hw_context_switch_to((rt_uint32_t)&to_thread->sp);
}
  • 调度器在启动的时候会从就绪列表中取出优先级最高的线程的线程控制块,然后切换到该线程。但是目前我们的线程还不支持优先级,那么就手动指定第一个运行的线程为就绪列表下标为 0 这条链表里面挂着的线程。 rt_list_entry()是一个已知一个结构体里面的成员的地址,反推出该结构体的首地址的宏,在 scheduler.c 开头定义。
  1. 第一次线程切换
  • rt_hw_context_switch_to() 函数
/* 用于存储上一个线程的栈的sp的指针 */
rt_uint32_t rt_interrupt_from_thread;

/* 用于存储下一个将要运行的线程的栈的sp的指针 */
rt_uint32_t rt_interrupt_to_thread;

/* PendSV中断服务函数执行标志 */
rt_uint32_t rt_thread_switch_interrupt_flag;

SCB_VTOR        EQU     0xE000ED08     ; 向量表偏移寄存器
NVIC_INT_CTRL   EQU     0xE000ED04     ; 中断控制状态寄存器
NVIC_SYSPRI2    EQU     0xE000ED20     ; 系统优先级寄存器(2)
NVIC_PENDSV_PRI EQU     0x00FF0000     ; PendSV 优先级值 (lowest)
NVIC_PENDSVSET  EQU     0x10000000     ; 触发PendSV exception的值

/*
; *-----------------------------------------------------------------------
; * 函数原型:void rt_hw_context_switch_to(rt_uint32 to);
; * r0 --> to
; * 该函数用于开启第一次线程切换
; *-----------------------------------------------------------------------
; */
		
rt_hw_context_switch_to    PROC
    
	; 导出rt_hw_context_switch_to,让其具有全局属性,可以在C文件调用
	EXPORT rt_hw_context_switch_to
		
    ; 设置rt_interrupt_to_thread的值
    LDR     r1, =rt_interrupt_to_thread             ;将rt_interrupt_to_thread的地址加载到r1
    STR     r0, [r1]                                ;将r0的值存储到rt_interrupt_to_thread

    ; 设置rt_interrupt_from_thread的值为0,表示启动第一次线程切换
    LDR     r1, =rt_interrupt_from_thread           ;将rt_interrupt_from_thread的地址加载到r1
    MOV     r0, #0x0                                ;配置r0等于0
    STR     r0, [r1]                                ;将r0的值存储到rt_interrupt_from_thread

    ; 设置中断标志位rt_thread_switch_interrupt_flag的值为1
    LDR     r1, =rt_thread_switch_interrupt_flag    ;将rt_thread_switch_interrupt_flag的地址加载到r1
    MOV     r0, #1                                  ;配置r0等于1
    STR     r0, [r1]                                ;将r0的值存储到rt_thread_switch_interrupt_flag

    ; 设置 PendSV 异常的优先级
    LDR     r0, =NVIC_SYSPRI2
    LDR     r1, =NVIC_PENDSV_PRI
    LDR.W   r2, [r0,#0x00]       ; 读
    ORR     r1,r1,r2             ; 改
    STR     r1, [r0]             ;; 触发 PendSV 异常 (产生上下文切换)
    LDR     r0, =NVIC_INT_CTRL
    LDR     r1, =NVIC_PENDSVSET
    STR     r1, [r0]

    ; 开中断
    CPSIE   F
    CPSIE   I

    ; 永远不会到达这里
    ENDP

功能
(1) rt_interrupt_to_thread = r0; rt_interrupt_to_thread用于存储下一个将要运行的线程的线程栈指针SP的指针; r0是传入的第一个参数;
(2) rt_interrupt_from_thread = 0; rt_interrupt_from_thread用于存储上一个线程的线程栈指针SP的指针;
(3) 设置中断标志位rt_thread_switch_interrupt_flag的值为1;
(4) 设置 PendSV 异常的优先级为最低(0xFF)。
(5) 触发 PendSV 异常 (产生上下文切换)。如果前面关了,还要等中断打开才能去执行 PendSV 中断服务函数。
(6) 开中断

  • PendSV_Handler()函数
;/*
; *-----------------------------------------------------------------------
; * void PendSV_Handler(void);
; * r0 --> switch from thread stack
; * r1 --> switch to thread stack
; * psr, pc, lr, r12, r3, r2, r1, r0 are pushed into [from] stack
; *-----------------------------------------------------------------------
; */

PendSV_Handler   PROC
    EXPORT PendSV_Handler

    ; 失能中断,为了保护上下文切换不被中断
    MRS     r2, PRIMASK
    CPSID   I

    ; 获取中断标志位,看看是否为0
    LDR     r0, =rt_thread_switch_interrupt_flag     ; 加载rt_thread_switch_interrupt_flag的地址到r0
    LDR     r1, [r0]                                 ; 加载rt_thread_switch_interrupt_flag的值到r1
    CBZ     r1, pendsv_exit                          ; 判断r1是否为0,为0则跳转到pendsv_exit

    ; r1不为0则清0
    MOV     r1, #0x00
    STR     r1, [r0]                                 ; 将r1的值存储到rt_thread_switch_interrupt_flag,即清0

    ; 判断rt_interrupt_from_thread的值是否为0
    LDR     r0, =rt_interrupt_from_thread            ; 加载rt_interrupt_from_thread的地址到r0
    LDR     r1, [r0]                                 ; 加载rt_interrupt_from_thread的值到r1
    CBZ     r1, switch_to_thread                     ; 判断r1是否为0,为0则跳转到switch_to_thread
                                                     ; 第一次线程切换时rt_interrupt_from_thread肯定为0,则跳转到switch_to_thread

; ========================== 上文保存 ==============================
    ; 当进入PendSVC Handler时,上一个线程运行的环境即:
 	; xPSR,PC(线程入口地址),R14,R12,R3,R2,R1,R0(线程的形参)
 	; 这些CPU寄存器的值会自动保存到线程的栈中,剩下的r4~r11需要手动保存
	
 	
    MRS     r1, psp                                  ; 获取线程栈指针到r1
    STMFD   r1!, {r4 - r11}                          ;将CPU寄存器r4~r11的值存储到r1指向的地址(每操作一次地址将递减一次)
    LDR     r0, [r0]                                 ; 加载r0指向值到r0,即r0=rt_interrupt_from_thread
    STR     r1, [r0]                                 ; 将r1的值存储到r0,即更新线程栈sp
	
; ========================== 下文切换 ==============================
switch_to_thread
    LDR     r1, =rt_interrupt_to_thread               ; 加载rt_interrupt_to_thread的地址到r1
	                                                  ; rt_interrupt_to_thread是一个全局变量,里面存的是线程栈指针SP的指针
    LDR     r1, [r1]                                  ; 加载rt_interrupt_to_thread的值到r1,即sp指针的指针
    LDR     r1, [r1]                                  ; 加载rt_interrupt_to_thread的值到r1,即sp

    LDMFD   r1!, {r4 - r11}                           ;将线程栈指针r1(操作之前先递减)指向的内容加载到CPU寄存器r4~r11
    MSR     psp, r1                                   ;将线程栈指针更新到PSP

pendsv_exit
    ; 恢复中断
    MSR     PRIMASK, r2

    ORR     lr, lr, #0x04                             ; 确保异常返回使用的堆栈指针是PSP,即LR寄存器的位2要为1
    BX      lr                                        ; 异常返回,这个时候任务堆栈中的剩下内容将会自动加载到xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务的形参)
	                                                  ; 同时PSP的值也将更新,即指向任务堆栈的栈顶。在ARMC3中,堆是由高地址向低地址生长的。
    ; PendSV_Handler 子程序结束
	ENDP	
	
	
	ALIGN   4

    END
  • 进入时失能中断,为了保护上下文切换不被中断;
  • 如果中断标志位rt_thread_switch_interrupt_flag不为1则直接退出;
  • 如果rt_interrupt_from_thread的值为0,则不用做上文保存;
  • CM3中断的具体行为:所有中断,不是只指PendSV。
    在这里插入图片描述
    在这里插入图片描述
  • 如果是从上一个线程切过来的,要保存上文,进入PendSV Handler后,有一些寄存器是自动保存,然后PSP更新到了新的栈顶位置。然后手动保存剩余寄存器。同时,该线程的线程控制块中,线程栈指针sp也要更新。
    MRS r1, psp     ; 拿到上一个线程的栈顶地址
    STMFD r1!, {r4 - r11} ; 将CPU寄存器r4~r11的值存储到r1指向的地址(每操作一次地址将递减一次)
    LDR r0, [r0]     ; r0存的是rt_interrupt_from_thread的地址,而rt_interrupt_from_thread中存的是上一个线程的线程控制块中,线程栈指针sp的地址。
    STR r1, [r0]     ; r1是最新的栈顶地址,r0是线程栈指针sp的地址,更新线程块中的sp信息。
  • 切换下文
    LDR r1, =rt_interrupt_to_thread ; 加载rt_interrupt_to_thread的地址到r1,里面存的是线程栈指针SP的地址
    LDR r1, [r1] ; 加载rt_interrupt_to_thread的值到r1,即sp指针的地址
    LDR r1, [r1] ; 加载rt_interrupt_to_thread的值到r1,即sp
    LDMFD r1!, {r4 - r11} ; 与保存上文是的入栈对应。将线程栈指针r1(操作之前先递减)指向的内容加载到CPU寄存器r4~r11
    MSR psp, r1 ; 将线程栈指针更新到PSP
  • 准备退出PendSV了,恢复中断
  • ORR lr, lr, #0x04 ; 确保异常返回使用的堆栈指针是PSP,即LR寄存器的位2要为1,这个在CM3权威指南里有说明。
    在这里插入图片描述
    :那LR寄存器的用法变了,CPU怎么知道正确的异常返回地址呢?
    :进入中断时LR的值被自动保存了,地址是PSP + 0x1C
  • 异常返回,这时候之前自动保存的寄存器值会自动加载到相应寄存器,同时PSP的值也将更新,即指向任务堆栈的栈顶。
  • MSP,PSP:在PendSV Handler退出时,使用PSP,进入其他中断时,使用MSP。

最后

各大RTOS好像都是用PendSV异常做线程切换。PendSV异常和普通异常相比,PendSV异常可以软件触发(悬起)。
在这里插入图片描述
在这里插入图片描述

工程文件

线程定义与线程切换

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 要找到RT-Thread内核实现手册,可以直接访问RT-Thread官方网站。在网站首页的顶部菜单中,可以看到一个名为"文档"的选项,点击进入。在文档页面中,可以找到各类文档资源,包括内核实现手册。 在文档页面中,我们可以看到手册分成了多个部分,其中包括了"内核"这一部分。点击"内核",可以进入RT-Thread内核相关的文档资源。在内核文档页面中,可以找到关于RT-Thread内核的详细介绍、源代码分析、内核配置、内核对象等方面的内容。 在这个页面上,我们可以找到RT-Thread内核实现手册的具体文件。可以点击相应的链接进行下载或在线浏览。RT-Thread内核实现手册是RT-Thread官方提供的详尽文档,其中包含了内核的具体实现原理、核心数据结构和功能模等内容。通过阅读这个手册,我们可以更好地了解和深入理解RT-Thread内核的工作原理和特性,为使用和开发RT-Thread提供指导和参考。 总结来说,要找到RT-Thread内核实现手册,只需要在RT-Thread官方网站上点击顶部菜单的"文档"选项,然后选择"内核"部分,即可找到相应的手册并进行查看和下载。 ### 回答2: rt-thread的官方网站是www.rt-thread.org。在该网站上,您可以找到关于rt-thread内核实现的详细手册。这个手册提供了rt-thread内核的各个方面的详细说明和指导,包括内核的设计思想、各个模的功能和使用方法,以及如何进行移植和配置等等。在手册中,您可以找到与rt-thread相关的文档、示例代码和实用工具等资源,帮助您更好地理解和使用rt-thread内核。 通过rt-thread的官网,您可以轻松找到内核实现手册并下载阅读。在官网的导航栏中,点击“文档”或“Documentation”选项,即可进入文档页面。在该页面中,您可以找到各个版本的rt-thread内核手册,选择最适合您的版本进行下载。手册以PDF格式提供,您可以保存到本地设备,方便随时翻阅和参考。同时,官网还提供了在线阅读的选项,您可以直接在网页上浏览和搜索手册内容。 通过rt-thread内核实现手册,您可以深入了解rt-thread的设计和原理,学习如何使用和定制rt-thread内核,以及如何优化和调试嵌入式系统。这将为您在嵌入式开发中使用rt-thread提供强大的支持和参考。 ### 回答3: RT-Thread的官网上有一个非常详细的内核实现手册。在官网的导航栏中,找到"文档"或者"Documentation"选项,点击进入。在文档页面中,你可以看到各种与RT-Thread相关的文档资源,包括内核实现手册。 RT-Thread内核实现手册是一本非常详细的指南,可以帮助开发者深入了解RT-Thread的内部实现原理和使用方法。它覆盖了RT-Thread内核的各个方面,包括线程调度、任务管理、内存管理、设备驱动、中断处理等内容。 在内核实现手册中,你可以找到对于每个功能的详细说明和示例代码。无论你是初学者还是有经验的开发者,这本手册都能够让你更好地理解和使用RT-Thread。同时,手册还提供了一些常见问题的解答,帮助开发者快速解决遇到的问题。 RT-Thread内核实现手册是一个非常重要的资源,对于想要深入学习和应用RT-Thread的开发者来说,是不可或缺的参考文献。无论是在官网上下载PDF版本的手册,还是在线阅读手册的网页版本,你都可以轻松地获取到这个宝贵的资源。希望你可以利用这个手册,更好地掌握和应用RT-Thread

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值