版权声明:本文为博主原创文章,未经博主允许不得转载。 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 第一次线程切换
- 线程切换使用的全局变量
//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 中断服务函数执行标志
- 线程切换函数的汇编实现
; 汇编文件, ‘;’ 代表注释的开头
; *******************************************************************
; 全局变量
; *******************************************************************
; 使用 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
- 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);
}
- 上下文切换函数 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, 有需要的朋友可以下载查看.