线程定义与线程切换的实现

本贴最后更新于 448 天前,其中的信息可能已经时移世改

1. 什么是线程?

线程就是系统所执行的一个一个独立的且无法返回的最小程序。

我们通常会把系统分割成一个又一个线程来执行。

比如说,一边读取室温,一边在屏幕上显示。把读取室温程序作为一个线程,在屏幕上显示程序作为一个线程,两者以极快的速度来回切换执行,看上去就像是同时执行一样。

线程对象的主要属性

  • **线程栈指针:**由于线程会被系统切换来切换去,所以需要一段内存保存线程内部参数
  • 线程栈大小
  • **函数入口:**线程是运行函数的载体,被切换时函数入口会被放到线程栈中

2. RT-Thread 内核对线程的管理

线程首先需要被初始化(本次实现是静态初始化),然后会被挂到就绪列表里。调度器从就绪列表中取出线程执行。

遇到事件发生时,线程会被换下,切换为另一个线程执行。在这个过程中,需要保存线程的现场,即把 ARM Cortex3 的 16 个寄存器的值全部塞到线程栈中。然后加载另一个线程的现场,即把另一个线程的线程栈中保存的寄存器值拿出来给 CPU。

问题:如果第一次执行线程,该如何加载现场?

  • 需要在初始化时设置线程栈,即设置各个寄存器的默认值
  • 硬件会帮助我们完成加载现场,硬件中有专门保存栈指针的寄存器 - PSP,指明 PSP 的数值后,硬件会自动帮助我们提取栈中的值,按照固定次序放到 CPU 寄存器中。还有一部分寄存器需要我们手动装载。

3. 线程编程

目标:

实现线程的定义与两个线程间的切换。

方法:

** ==两个线程初始化后,放入到就绪列表。然后调度器(已经初始化)从就绪列表中取出第一个线程执行,然后第一个线程隔一段时间后就绪,切换到第二个线程执行,第二个线程隔一段时间就绪再去执行第一个线程,如此来回交替。==**

实验:

在线程1 和 线程 2中各自使标志位置1,退出时置0,观察两个标志位的波形

3.1 程序架构与文件架构

image

源函数文件:

  • thread.c : 定义线程初始化函数

    • cpuport.c : 定义了线程栈操作函数
  • secheduler.c : 定义调度器操作函数与就绪列表

    • rtservice.h : 定义了就绪列表操作函数
  • context_rvds.s : 汇编,线程栈切换函数

头文件:

  • rtdef.h : 各种数据类型以及对象的定义
  • rtconfig.h : 可能需要用户配置的宏
  • thread.h : 包含了线程与调度器操作函数的声明
  • rthw.h : 声明汇编函数(线程切换函数)

3.2 函数实现

程序的函数主要包括

  1. 线程对象定义及初始化1

    1. 线程栈的初始化2
  2. 就绪列表定义与操作3

  3. 调度器初始化及启动4

  4. 线程上下文切换5

3.2.1 线程对象定义(rtdef.h 文件中)以及初始化(thread.c)

线程对象的定义

/*	控制块定义	*/
struct rt_thread
{
	void	*sp;								/*	线程栈指针	*/
	void	*entry;							/*	线程入口地址	*/
	void	*parameter;					/*	线程形参	*/
	void	*stack_addr;				/*	线程栈起始地址	*/
	rt_uint32_t	stack_size;		/*	线程栈大小,单位为字节	*/

	rt_list_t tlist; /* 线程链表节点 */
};

线程对象初始化

主要是线程栈的初始化!!

image

#include <rtthread.h>

extern rt_uint8_t *rt_hw_stack_init(void *tentry,
                            void *parameter,
                            rt_uint8_t *stack_addr);

													
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;
}

线程栈初始化 ​​==rt_uint8_t *rt_hw_stack_init(void *tentry,void *parameter,rt_uint8_t *stack_addr)==

主要是初始化线程栈,以后第一次调用线程的时候要用。线程栈中保存了入口函数地址,所以 CPU 才可以找到入口函数。

返回值是栈指针 SP

#include <rtthread.h>

 /* 用于存储上一个线程的栈的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;


 struct exception_stack_frame
{
 /* 异常发生时,自动加载到CPU寄存器的内容 */
    rt_uint32_t r0;
    rt_uint32_t r1;
    rt_uint32_t r2;
    rt_uint32_t r3;
    rt_uint32_t r12;
    rt_uint32_t lr;
    rt_uint32_t pc;
    rt_uint32_t psr;
};


struct stack_frame
{
    /* 异常发生时,需要手动加载到CPU寄存器的内容 */
    rt_uint32_t r4;
    rt_uint32_t r5;
    rt_uint32_t r6;
    rt_uint32_t r7;
    rt_uint32_t r8;
    rt_uint32_t r9;
    rt_uint32_t r10;
    rt_uint32_t r11;

    struct exception_stack_frame exception_stack_frame;
};


/*
 *	rt_hw_stack_init()函数用来初始化线程栈,当线程第一次运行时,加载到CPU寄存器的参数就放在线程栈里面
 */

 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的是(栈顶指针-4) (5)*/
    stk = stack_addr + sizeof(rt_uint32_t);

		/* 让stk指针向下8字节对齐 (6)*/
    stk = (rt_uint8_t *)RT_ALIGN_DOWN((rt_uint32_t)stk, 8);

    /* stk指针继续向下移动sizeof(struct stack_frame)个偏移量	(7) */
    stk-= sizeof(struct stack_frame);

	/*	将stk指针强制转化为stack_frame类型后存储在stack_frame中,	(8)*/
	/*	从此之后stack_frame就指向了用户提供栈的栈顶	(8)*/
	stack_frame = (struct stack_frame *)stk;

	/* 以stack_frame为起始地址,将栈空间里面的sizeof(struct stack_frame)个内存地址初始化为0xdeadbeef (9)*/
	for (i = 0; i <sizeof(struct stack_frame) /sizeof(rt_uint32_t); i ++)
	{
		((rt_uint32_t *)stack_frame)[i] = 0xdeadbeef;
	}

	/* 初始化异常发生时自动保存的寄存器(10) */
	stack_frame->exception_stack_frame.r0 = (unsigned long)parameter; /* r0 : argument */
	stack_frame->exception_stack_frame.r1 = 0;
	stack_frame->exception_stack_frame.r2 = 0;
	stack_frame->exception_stack_frame.r3 = 0;
	stack_frame->exception_stack_frame.r12 = 0;
	stack_frame->exception_stack_frame.lr = 0;
	stack_frame->exception_stack_frame.pc =  (unsigned long)tentry;/* 入口指针, pc */
	stack_frame->exception_stack_frame.psr =  0x01000000L; 				 /* PSR */


	return stk;
}

3.2.2 就绪列表

初始化完线程函数后,需要将其插入就绪列表中,就绪列表是存放链表节点的数组。节点中存放了 next 和 prev 指针

struct rt_list_node
{
	struct rt_list_node *next; /* 指向后一个节点 */
	struct rt_list_node *prev; /* 指向前一个节点 */
};
typedef struct rt_list_node rt_list_t;

/*	控制块定义	*/
struct rt_thread
{
	void	*sp;								/*	线程栈指针	*/
	void	*entry;							/*	线程入口地址	*/
	void	*parameter;					/*	线程形参	*/
	void	*stack_addr;				/*	线程栈起始地址	*/
	rt_uint32_t	stack_size;		/*	线程栈大小,单位为字节	*/

	rt_list_t tlist; /* 线程链表节点 */
};

就绪列表定义在 scheduler.c 中


/*	定义就绪列表		*/
rt_list_t rt_thread_priority_table[RT_THREAD_PRIORITY_MAX];

初始化完成后,插入就绪列表中:

	rt_thread_init( &rt_flag1_thread, 						/* 线程控制块 */
							flag1_thread_entry,								/* 线程入口		*/
							RT_NULL, 													/* 线程形参地址	*/
							&rt_flag1_thread_stack[0], 				/* 线程栈起始地址 */
							sizeof(rt_flag1_thread_stack) ); 	/* 线程栈大小,单位为字节 */
			

	/* 将线程1插入就绪列表 */
	rt_list_insert_before( &(rt_thread_priority_table[0]),&(rt_flag1_thread.tlist) );

3.2.3 调度器初始化以及启动调度器

程序三大步骤:

  1. 调度器初始化以及线程初始化
  2. 将线程插入就绪列表
  3. 启动调度器,从就绪列表中取出线程对象执行

调度器函数主要分为:

  1. 调度器初始化 -​ ​​**==void rt_system_scheduler_init(void)==**

  2. 启动调度器 - **==void rt_system_scheduler_start(void)==**

    1. 切换到第一个线程(汇编实现) - **==rt_hw_context_switch_to((rt_uint32_t)&to_thread->sp);==**
  3. 系统调度函数 - 在各自的线程中执行仅仅实现两个线程轮流切换 - ==void rt_schedule(void)==

    1. 产生上下文切换(汇编实现)- **==rt_hw_context_switch((rt_uint32_t)&from_thread->sp,(rt_uint32_t)&to_thread->sp);==**

	/* 调度器初始化 */
	rt_system_scheduler_init();

	/* 线程初始化 */
	rt_thread_init( &rt_flag2_thread, 						/* 线程控制块 */
							flag2_thread_entry,			/* 线程入口*/
							RT_NULL, 							/* 线程形参地址	*/
							&rt_flag2_thread_stack[0], 				/* 线程栈起始地址 */
							sizeof(rt_flag2_thread_stack) ); 	/* 线程栈大小,单位为字节 */

	/* 将线程2插入就绪列表 */
	rt_list_insert_before( &(rt_thread_priority_table[1]),&(rt_flag2_thread.tlist) );

	/* 启动系统调度器 */
	rt_system_scheduler_start();

  1. 调度器初始化

即让各链表节点自指:

/* 线程就绪列表初始化 */
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;
}

调度器启动

调度器从就绪列表中取出对象,但是就绪列表中存放的是链表。

问题来了,链表对象只有前后节点,如何通过链表节点来访问一个线程对象的其他属性?

==-- ​==​**==已知一个结构体里面的成员的地址,反推出该结构体的首地址!!!==**

/* 已知一个结构体里面的成员的地址,反推出该结构体的首地址 */
#define rt_container_of(ptr, type, member) \
    ((type *)((char *)(ptr) - (unsigned long)(&((type *)0)->member)))
		
#define rt_list_entry(node, type, member) \
    rt_container_of(node, type, member)

  1. 启动调度器函数 - ​​**==void rt_system_scheduler_start(void)==**
 /* 启动系统调度器,指定第一个要运行的线程 */
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);
}

3.2.4 线程上下文切换

线程切换函数统一在 context_rvds.s 中实现

;*************************************************************************
;                                 全局变量
;*************************************************************************
    IMPORT rt_thread_switch_interrupt_flag
    IMPORT rt_interrupt_from_thread
    IMPORT rt_interrupt_to_thread
	
;*************************************************************************
;                                 常量
;*************************************************************************
;-------------------------------------------------------------------------
;有关内核外设寄存器定义可参考官方文档:STM32F10xxx Cortex-M3 programming manual
;系统控制块外设SCB地址范围:0xE000ED00-0xE000ED3F
;-------------------------------------------------------------------------

;给数字常量取一个符号名,相当于 C 语言中的 define
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的值

;*************************************************************************
;                              代码产生指令
;*************************************************************************

;汇编一个新的代码段或者数据段

    AREA |.text|, CODE, READONLY, ALIGN=2
    THUMB
    REQUIRE8
    PRESERVE8
	

;/*
; *-----------------------------------------------------------------------
; * 函数原型:void rt_hw_context_switch_to(rt_uint32 to);
; * r0 --> to
; * 该函数用于开启第一次线程切换
; *-----------------------------------------------------------------------
; */

;rt_hw_context_switch_to函数,调度器启动后需要调用第一个函数的入口
rt_hw_context_switch_to    PROC
  
	; 导出rt_hw_context_switch_to,让其具有全局属性,可以在C文件调用
	EXPORT rt_hw_context_switch_to


; ------------------------------------------------------------------------------------------
    ; 设置rt_interrupt_to_thread的值,即设置下一个将要运行的线程的栈地址
; --------------------------------------------------------------------------------------------
	;一开始r0储存了函数的第一个形参 r0 = (rt_uint32_t)&to_thread->sp ,存储了要运行程序的栈指针
    LDR     r1, =rt_interrupt_to_thread             ;将rt_interrupt_to_thread的地址加载到r1,以后修改r1就是修改rt_interrupt_to_thread
   
   ;==> rt_interrupt_to_thread  = (rt_uint32_t)&to_thread->sp
	STR     r0, [r1]                                ;将r0的值存储到rt_interrupt_to_thread

; ------------------------------------------------------------------------------------------
    ; 设置rt_interrupt_from_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 ; 装载PenSV优先级寄存器
    LDR.W   r2, [r0,#0x00]       ; 读	r2保存了NVIC_SYSPRI2的值
    ORR     r1,r1,r2             ; 改
    STR     r1, [r0]             ; 写

    ; 触发 PendSV 异常 (产生上下文切换),手动开启中断,程序会调转到PendSV_Handler执行
    LDR     r0, =NVIC_INT_CTRL
    LDR     r1, =NVIC_PENDSVSET
    STR     r1, [r0]

    ; 开中断
    CPSIE   F
    CPSIE   I

    ; 永远不会到达这里
    ENDP



;/*
; *-----------------------------------------------------------------------
; * void rt_hw_context_switch(rt_uint32 from, rt_uint32 to);
; * r0 --> from
; * r1 --> to
; *-----------------------------------------------------------------------
; */
;rt_hw_context_switch_interrupt
    ;EXPORT rt_hw_context_switch_interrupt
	
rt_hw_context_switch    PROC
    EXPORT rt_hw_context_switch

    ; 设置中断标志位rt_thread_switch_interrupt_flag为1   
    LDR     r2, =rt_thread_switch_interrupt_flag          ; 加载rt_thread_switch_interrupt_flag的地址到r2
    LDR     r3, [r2]                                      ; 加载rt_thread_switch_interrupt_flag的值到r3
    CMP     r3, #1                                        ; r3与1比较,相等则执行BEQ指令,否则不执行
    BEQ     _reswitch
    MOV     r3, #1                                        ; 设置r3的值为1
    STR     r3, [r2]                                      ; 将r3的值存储到rt_thread_switch_interrupt_flag,即置1
  
	; 设置rt_interrupt_from_thread的值
    LDR     r2, =rt_interrupt_from_thread                 ; 加载rt_interrupt_from_thread的地址到r2
    STR     r0, [r2]                                      ; 存储r0的值到rt_interrupt_from_thread,即上一个线程栈指针sp的指针

_reswitch
    ; 设置rt_interrupt_to_thread的值
	LDR     r2, =rt_interrupt_to_thread                   ; 加载rt_interrupt_from_thread的地址到r2
    STR     r1, [r2]                                      ; 存储r1的值到rt_interrupt_from_thread,即下一个线程栈指针sp的指针

    ; 触发PendSV异常,实现上下文切换,向PenSV状态寄存器写1后,就可以产生终端
	LDR     r0, =NVIC_INT_CTRL            
    LDR     r1, =NVIC_PENDSVSET
    STR     r1, [r0]

    ; 子程序返回
	BX      LR

	; 子程序结束
    ENDP


;/*
; *-----------------------------------------------------------------------
; * 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
	

3.3 实验

image


  1. 3.2.1 线程对象定义(rtdef.h 文件中)以及初始化(thread.c)

  2. 主要是线程栈的初始化!!

  3. 3.2.2 就绪列表

  4. 3.2.3 调度器初始化以及启动调度器

  5. 3.2.4 线程上下文切换

  • C

    C 语言是一门通用计算机编程语言,应用广泛。C 语言的设计目标是提供一种能以简易的方式编译、处理低级存储器、产生少量的机器码以及不需要任何运行环境支持便能运行的编程语言。

    85 引用 • 165 回帖 • 1 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...