RT-Thread------线程(1)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u013517122/article/details/84836629

1. 线程概念

线程: 就是把整个系统分割成一个个独立且永不返回的函数, 这样的函数我们称之为线程.
//线程函数
void thread_entry(void *para)
{
	/* 线程主体, 死循环 */
	for (; ;) {
		/* 线程主体代码 */
	}
}

2. 线程创建

在多线程的系统中, 因每个线程都是独立的, 互不干扰, 因此, 每个线程需要有独立的栈空间, 这个栈空间说白了就是预先定义的全局数组, 可以在程序运行时动态分配, 殊路同归, 都是在位于 RAM 中的一段连续空间.

2.1 定义线程栈

/* 设置变量的对齐方式, 对在它下面的变量起作用, 此处为 4 字节对齐 */
 __attribute__((aligned(4)))   
/* 定义线程栈, 定义两个全局数组, 大小设置为 512, 即栈空间为 512 字节 */
unsigned char thread_task1_stack[512];
unsigned char thread_task1_stack[512];

2.2 定义线程函数

/**
 * @brief 此处定义了 2 个 线程, 其用途是延时改变 flag1, flag2 的值.
 *
 */
/* 延时函数 */
void delay(unsigned int count)
{
	for (; count != 0; count --);
}

/* 任务 1 线程 */
void task1_thread_entry(void *para)
{
	for (; ;) {
		flag1 = 1;
		delay(1000);
		flag1 = 0;
		delay(1000);
	}
}


/* 任务 2 线程 */
void task2_thread_entry(void *para)
{
	for (; ;) {
		flag2 = 1;
		delay(1000);
		flag2 = 0;
		delay(1000);
	}
}

2.3 定义线程控制块

线程控制块是系统为了顺利的调度线程, 为每一个线程额外定义了一个线程控制块, 相当于线程的标识符, 里面存放线程的所有信息, 如线程的栈指针, 线程名称, 线程参数等. 有了这个控制块后, 系统对线程的所有操作都可以通过这个线程控制块来实现.
/* 线程控制块结构体 */
struct rt_thread
{
	void *sp;                          // 线程栈指针
	void *entry;                     // 线程入口地址 (即线程函数)
	void *parameter;            // 线程形参
	void *stack_addr;           // 线程栈起始地址
	unsigned int stack_size; //线程栈大小, 单位为字节
	
	rt_list_t	tlist;				// 线程链表节点
};
typedef struct rt_thread *rt_thread_t;


/* 线程控制块定义 */
struct rt_thread	rt_task1_thread;
struct rt_thread	rt_task1_thread;

2.4 创建线程函数

线程的栈, 线程的函数实体, 线程控制块, 最终需要联系起来才能由系统调度器进行调度. 那
么就由线程初始化函数 rt_thread_init() 来实现联系.
long rt_thread_init( struct rt_thread *thread, 
					void (*entry)(void *parameter),
					void *parameter,
					void *stack_start,
					unsigned int 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 0;
}

3. 就绪列表

线程创建好之后, 需要将线程添加到就绪列表中, 表示线程已经就绪, 系统随时可以调度.

3.1 定义就绪列表

/* 线程就绪列表, 就是一个 rt_list_t 类型的数组 */
#define RT_THREAD_PRIORITY_MAX	32
rt_list_t rt_thread_priority_table[RT_THREAD_PRIORITY_MAX];

3.2 将线程插入就绪列表

线程控制块中有个类型为 rt_list_t tlist 的成员, 将线程插入到就绪列表中,就是通过将线程控制块的 tlist 这个节点插入到就绪列表中来实现的. 就绪列表就相当于是晾衣绳, 线程相当于衣服, 而 tlist 就是晾衣架, 每个线程自带晾衣架, 就是为了把自己挂在不同的链表中.
/* 初始化线程 */
rt_thread_init( &rt_task1_thread, 
			    task1_thread_entry,
			    0,
			    &rt_task1_thread_stack[0],
			    sizeof(rt_task1_thread_stack) );

/* 将线程插入到就绪列表中, 此时暂不支持优先级, 所以将第一个任务的线程挂在 第 0 个位置 */			 
rt_list_insert_before( &(rt_thread_prioority_table[0]), &(rt_task1_thread.tlist) );

/* 初始化线程 */
rt_thread_init( &rt_task2_thread, 
			    task2_thread_entry,
			    0,
			    &rt_task2_thread_stack[0],
			    sizeof(rt_task2_thread_stack) );

/* 将线程插入到就绪列表中, 此时暂不支持优先级, 所以将第二个任务的线程挂在 第 1 个位置 */			 
rt_list_insert_before( &(rt_thread_prioority_table[1]), &(rt_task2_thread.tlist) );

4. 调度器

调度器是操作系统的核心, 主要功能就是实现线程的切换, 即从就绪列表中找到优先级最高
的线程, 然后执行该线程. 从 RT-Thread 的源码实现来看, 就是由几个全局变量和一些可以实现线程切换的函数, 再加上系统异常中的上下文切换组成.

4.1 调度器初始化

/* 初始化调度器, 应该在硬件初始化之后进行调度器初始化 */
void rt_system_scheduler_init(void)
{
	/* 使用关键字 register 修饰, 是为了防止编译器优化 */
	register long offset;
	
	/* 线程就绪列表初始化, 初始化后整个就绪列表为空 */
	for (offset = 0; offset <  RT_THREAD_PRIORITY_MAX; offset ++) {
		rt_list_init(&rt_thread_priority_table[offset]);
	}

	/* 初始化当前线程控制块指针为空, 该变量用于指向当前正在运行的线程的线程控制块 */
	rt_current_thread = 0;
}

4.2 启动调度器

/* 启动调度器 */
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;
	
	/* 切换第一个线程, 用于实现第一次线程切换, 该函数使用汇编实现,
	   当汇编函数在 C 文件中调用的时候, 如果有形参, 则执行时会将形参传入到
	   CPU 的 r0 寄存器中 */	
	rt_hw_context_switch_to((rt_uint32_t)&to_thread->sp);						
}

4.3 第一次线程切换

  1. 线程切换使用的全局变量
//cpuport.c
/* 线程切换需要用到的 3 个全局变量 */
rt_uint32_t rt_interrupt_from_thread;   		// 用于存储上一个线程的栈的 sp 的指针
rt_uint32_t rt_interrupt_to_thread;       		// 用于存储下一个将要运行的线程的栈的 sp 的指针
t_uint32_t rt_thread_switch_interrupt_flag;    // PendSV 中断服务函数执行标志

  1. 线程切换函数的汇编实现
; 汇编文件, ‘;’ 代表注释的开头

; *******************************************************************
;			全局变量
; *******************************************************************
; 使用 IMPORT 关键字导入一些全局变量, 这 3 个全局变量在 cpuport.c 中定义
	IMPORT	rt_thread_switch_interrupt_falg;
	IMPORT	rt_interrupt_from_thread;
	IMPORT	rt_interrupt_to_thread;

; *******************************************************************
;			寄存器
; *******************************************************************
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 表示当前文件指令要 2^2 字节对齐
; THUMB	表示 THUMB 指令代码
; REUIRE8 和 PRESERVE8 均表示当前文件的栈按照 8 字节对齐
	AREA |.text|, CODE, READONLY, ALIGN=2
	THUMB
	REQUIRE8
	PRESERVE8

;/**
; * void rt_hw_context_switch_to(rt_uint32 to);
; * r0 --> to
; * this function is used to perform the first thread switch
; *
; */	
; PROC 用于定义子程序, 与 ENDP 成对使用, 表示 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 存放的是下一个将要运行的线程的 sp 的地址, 由 rt_interrupt_switch_to((rt_uint32_t)&to_thread->sp)调用时传到 r0 中
	LDR		r1, = rt_interrupt_to_thread
	STR		r0, [r1]

	; 设置 rt_interrupt_from_thread 的值为 0, 表示启动第一次线程切换
	LDR		r1, = rt_interrupt_from_thread
	MOV		r0, #0x0
	STR		r0, [r1]

	; 设置中断标志位 rt_thread_switch_interrupt_flag 的值为 1, 当执行了 PendSVC Handler 时,  rt_thread_switch_interrupt_flag 会被清零
	LDR		r1, = rt_thread_switch_interrupt_flag
	MOV		r0, #1
	STR		r0, [r1]

	; 设置 PendSV 异常优先级 (此处设置为最低优先级)
	LDR		r0, = NVIC_SYSPRI2
	LDR		r1, = NVIC_PENDSV_PRI
	LDR.W		r2, [r0, #0x00]        ; 读
	ORR		r1, r1, r2             	; 改
	STR		r1, [r0]              	; 写

	; 触发 PendSV 异常 (产生上下文切换)
	; 如果前面关了, 还要等中断打开才能去执行 PendSV 中断服务函数
	LDR		r0, = NVIC_INT_CTRL
	LDR		r1, = NVIC_PENDSVSET
	STR		r1, [r0]

	; 开中断
	CPSIE	F
	CPSIE	I	

	; 永远不会到达这里
	; ENDP 代表 rt_hw_context_switch_to 子程序结束, 与 PROC 成对使用
	ENDP

	; 当前文件指令代码要求 4 字节对齐, 不然会有警告
	ALIGN	4
	
	; 汇编文件结束, 每个汇编文件都需要一个 END	
	END		
  1. 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
	; 导出 PendSV_Handler, 让其具有全局属性, 可以在 C 文件中调用
	EXPORT	PendSV_Handler
	
	; 保存中断屏蔽寄存器的值到 r2 中, 在结束时用于恢复
	; 除能中断, 是为了保护上下文切换不被中断
	MRS		r2, PRIMASK
	CPSID	I

	; 获取中断标志位, 查看是否为 0, 如果为 0 则退出 PendSV_Handler, 如果不为 0 则继续往下执行
	LDR		r0, = rt_thread_switch_interrupt_flag
	LDR	  	r1, [r0]
	CBZ		r1, pendsv_exit

	; 清除中断标志位, 即设置 rt_thread_switch_interrupt_flag = 0
	MOV		r1, #0x00
	STR		r1, [r0]

	; 判断 rt_interrupt_from_thread 的值是否为 0, 如果为 0, 则表
	; 示第一次线程切换, 不用做上文保存, 直接跳到 switch_to_thread 执行下文切换即可,
	; 不为 0, 则需要先执行上文保存, 然后再进行下文切换
	LDR		r0, = rt_interrupt_from_thread
	LDR		r1, [r0]
	CBZ		r1, switch_to_thread

; ========================== 上文保存 ========================	
; 当进入到 PendSV_Handler 时, 上一个线程的运行环境如下:
; xPSR, PC(线程入口地址), R14, R12, R3, R2, R1, R0(线程的形参)
; 这些 CPU 寄存器的值会自动保存到线程的栈中, 剩下的 R4~R11需要手动保存

	;获取线程栈指针到 r1 中, 然后将 r4~r11 的值存储到 r1 指向的地址(每操作一次地址将递减一次)
	MRS		r1, psp
	STMFD	r1!, {r4 - r11}
	
	; 加载 r0 指向的值到 r0 中, 即 r0 = rt_interrupt_from_thread
	; 将 r1 的值存储到 r0, 即更新线程栈 sp
	LDR		r0, [r0]
	STR		r1, [r0]

; ========================== 下文切换 ========================			
switch_to_thread

	; 加载 rt_interrupt_to_thread 的地址到 r1
	; rt_interrupt_to_thread 是全局变量, 存放的是线程栈指针 sp 的指针
	; 第一次加载 rt_interrupt_to_thread 的值到 r1 中, 即指针 sp 的指针
	; 第二次加载 rt_interrupt_to_thread 的值到 r1 中, 即指针 sp
	LDR		r1, = rt_interrupt_to_thread
	LDR		r1, [r1]
	LDR		r1, [r1]
		
	;将线程栈指针 r1 指向的内容加载到 r4~r11, 操作之前先递减	
	LDMFD 	r1!, {r4 - r11}	
	
	; 将线程栈指针更新到 PSP
	MSR		psp, r1	

pendsv_exit	
	;恢复中断屏蔽寄存器的值
	MSR PRIMASK, r2
		
	;确保异常返回使用的栈指针是 PSP, 即 lr 寄存器的位 2 要为 1	
	ORR lr, lr, #0x04	
		
	; 异常返回, 这时栈中的剩余内容会自动加载到 CPU 寄存器
	; xPSR, PC(线程入口地址), R14, R12, R3, R2, R1, R0(线程的形参)
	; 同时 PSP 的值也将更新, 即指向线程栈的栈顶
	BX  lr	
		
	; ENDP 代表 PendSV_Handler 子程序结束, 与 PROC 成对使用	
	ENDP

4.4 系统调度

系统调度就是在就绪列表中寻找优先级最高的就绪线程, 然后去执行该线程, 因目前不支持优先级, 仅实现
两个线程轮流切换, 所以, 系统调度函数自己实现.
//rt_schedule()
void rt_schdule(void)
{
	struct rt_thread	*to_thread;
	struct rt_thread	*from_thread;
	
		if ( rt_current_thread == rt_list_entry( rt_thread_priority_table[0].next,
									            struct rt_thread,
									            tlist ) )  {
									            
				from_thread = rt_current_thread;
				to_thread = rt_list_entry( rt_thread_priority_table[1].next,
									      struct rt_thread,
									      tlist );	
									      												
				rt_current_thread = to_thread;																																				
		} 
		else {
		
				from_thread = rt_current_thread;
				to_thread = rt_list_entry( rt_thread_priority_table[0].next,
									      struct rt_thread,
									      tlist );
									   													
				rt_current_thread = to_thread;		
		}
  
  	/* 产生上下文切换 */
	rt_hw_context_switch((rt_uint32_t)&from_thread->sp, (rt_uint32_t)&to_thread->sp);
}
  1. 上下文切换函数 rt_hw_contex_switch() 汇编实现
rt_hw_context_switch PROC
	; 导出 rt_hw_context_switch, 让其具有全局属性, 可以在 C 文件中调用
	EXPORT rt_hw_context_switch

	; 先加载 rt_thread_switch_interrupt_flag 的地址到 r2, 然后再加载其值到 r3	
	LDR		r2, = rt_thread_switch_interrupt_flag
	LDR		r3, [r2]
	
	; r3 与 1 比较, 相等则执行 BEQ 指令, 否则不执行
	CMP		r3, #1
	BEQ		_reswitch

	; 设置中断标志位 rt_thread_switch_interrupt_flag = 1
	MOV		r3, #1
	STR		r3, [r2]

	; 设置 rt_interrupt_from_thread 的值为 r0, 即设置 rt_interrupt_from_thread 的值为
	; 上一个线程栈指针 sp 的指针
	LDR		r2, = rt_interrupt_from_thread
	STR		r0, [r2]

_reswitch
	; 设置 rt_interrupt_to_thread 的值为 r1, 即设置 rt_interrupt_to_thread 的值为
	; 下一个线程栈指针 sp 的指针
	LDR		r2, = rt_interrupt_to_thread 
	STR		r1, [r2]

	;触发 PendSV 异常,  在PendSV_Handler 里面实现上下文切换
	LDR		r0, = NVIC_INT_CTRL
	LDR		r1, = NVIC_PENDSVSET
	
	STR		r1, [r0]
	
	; 子程序返回
	BX		LR
	;ENDP 代表 rt_hw_context_switch 子程序结束, 与 PROC 成对使用	
	ENDP

5. 示例程序

#include <rtthread.h>
#include <rtconfig.h>
#include <rtservice.h>

ALIGN(RT_ALIGN_SIZE)

rt_uint8_t rt_task1_thread_stack[512];
rt_uint8_t rt_task2_thread_stack[512];

rt_uint32_t flag1;
rt_uint32_t flag2;

struct rt_thread 	rt_task1_thread;
struct rt_thread	rt_task2_thread;

extern rt_list_t rt_thread_priority_table[RT_THREAD_PRIORITY_MAX];

void delay(rt_uint32_t count)
{
	for (; count != 0; count --);
}

void task1_thread_entry(void *p_arg)
{
	for (; ;) {
		flag1 = 1;
		delay(1000);
		flag1 = 0;
		delay(1000);
			
		/* 线程切换, 这里是手动切换 */
		rt_schedule();
	}
}


void task2_thread_entry(void *p_arg)
{
	for (; ;) {
		flag2 = 1;
		delay(1000);
		flag2 = 0;
		delay(1000);
		
		/* 线程切换, 这里是手动切换 */
		rt_schedule();
	}
}

/**
 * @brief: main function
 *
**/
int main(void)
{
	/* 硬件初始化 */
	/* 将硬件相关的初始化放在这里,如果是软件仿真则没有相关初始化代码 */

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

	/* 初始化线程 */
	rt_thread_init( &rt_task1_thread,
				    task1_thread_entry,
					RT_NULL,
					&rt_task1_thread_stack[0],
					sizeof(rt_task1_thread_stack));
	/* 将线程插入到就绪列表 */
	rt_list_insert_before(&(rt_thread_priority_table[0]), &(rt_task1_thread.tlist));
													

	/* 初始化线程 */
	rt_thread_init( &rt_task2_thread,
				    task2_thread_entry,
					RT_NULL,
					&rt_task2_thread_stack[0],
					sizeof(rt_task2_thread_stack));

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

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

6. 软件仿真

使用 Keil 5 进行软件仿真调试, 结果跟预期相符, 两个线程切换执行.

在这里插入图片描述

记: 后续会将就绪列表操作, 线程栈的详细内容, 线程栈在 RAM 中的分布细节再补上.

Note: 该工程源码, 我已上传至 github, 有需要的朋友可以下载查看.

猜你喜欢

转载自blog.csdn.net/u013517122/article/details/84836629