线程管理之线程切换
前言
基本信息
名称 | 描述说明 |
---|---|
RT-Thread Studio 软件版本 | 版本: 1.1.3 |
RT-Thread 系统版本 | 4.0.2 |
STM32CubeIDE 软件版本 | 1.4.0 |
STM32芯片型号 | STM32F013VG |
前言说明
前面已经分析了一部分与线程切换相关的函数,其中最关键的函数PendSV_Handler函数在本篇进行分析,由于这个函数相关的知识点篇幅较长,因此专门来单独进行分析。
函数和变量 | 描述 |
---|---|
PendSV_Handler | PendSV 中断处理函数是 PendSV_Handler(),线程切换的实际工作在 PendSV_Handler() 里完成 |
PendSV_Handler函数
从前面的函数来看,线程栈初始化函数、线程切换函数都要触发PendSV_Handler中断函数执行,RT-thread系统在PendSV_Handler() 里完成线程切换的实际工作。
说明
rt_interrupt_to_thread变量中存储值的含义
rt_hw_context_switch函数部分代码如下:
_reswitch: /*R3值为1 执行*/
LDR R2, =rt_interrupt_to_thread /*从参数 r1 里更新 rt_interrupt_to_thread 变量 set rt_interrupt_to_thread */
STR R1, [R2] /*将r1的地址的值写入到rt_interrupt_to_thread对应的地址 */
R1的值是rt_hw_context_switch函数的输入参数 rt_uint32 to,而to这个变量输入的值示例代如下:
rt_hw_context_switch((rt_ubase_t)&from_thread->sp,
(rt_ubase_t)&to_thread->sp);
因此R1的值实际上是to_thread线程结构体下sp栈指针的地址,特别注意 R1的值是栈指针的地址。
因此rt_interrupt_to_thread存储的是 要跳转的线程栈指针的地址。
关键代码1:
PendSV_Handler函数部分代码如下:
switch_to_thread: /**/
LDR R1, =rt_interrupt_to_thread /*加载 rt_interrupt_to_thread变量 的地址 的值到R1*/
LDR R1, [R1] /*加载 rt_interrupt_to_thread 的值(to thread线程栈的地址 )到R1*/
LDR R1, [R1] /*加载to thread线程栈的地址处 的值到R1 load thread stack pointer */
从rt_hw_context_switch函数代码内容可以看到rt_interrupt_to_thread 变量在被赋值的时候实际上是将输入的要跳转的线程栈指针的地址。所以可以理解下面的代码含义:
- LDR R1, =rt_interrupt_to_thread 代码表示:rt_interrupt_to_thread 变量也是存在地址的,因此将rt_interrupt_to_thread 变量的地址加载到R1中。
- LDR R1, [R1] 代码表示:加载rt_interrupt_to_thread 变量的地址处的的值到R1中,相当于取到了rt_interrupt_to_thread 变量的值到R1中。
- LDR R1, [R1] 代码表示: 将rt_interrupt_to_thread 变量的值作为地址,加载这个地址处的值到R1中,而这个地址的值就是要切换线程的栈指针的地址。
关键代码2:
/*下面是保存 from线程的上下文*/
MRS R1, PSP /* 获取 from 线程的栈指针 get from thread stack pointer */
STMFD R1!, {R4 - R11} /* 将 r4~r11 保存到线程的PSP栈里(from线程栈数组中) push R4 - R11 register */
LDR R0, [R0]
STR R1, [R0] /* 更新线程的控制块的 SP 指针 update from thread stack pointer */
LDMFD R1!, {R4 - R11} /* pop R4 - R11 register */
MSR PSP, R1 /* update stack pointer */
这两段代码是保存from线程的上下文和切换到to线程的上下文。
STMFD和LDMFD指令的用法在下面的知识点里面进行讲解,这里不赘述。
下面抛出疑问点:
为什么使用PSP进程堆栈指针而不是用MSP主堆栈指针进行线程切换?
操作模式
Cortex‐M3 支持 2 个模式和两个特权等级
- 当处理器处在线程状态下时,既可以使用特权级,也可以使用用户级;另一方面, handler模式总是特权级的。在复位后,处理器进入线程模式+特权级。
- 在线程模式+用户级下,对系统控制空间( SCS)的访问将被阻止——该空间包含了配置寄存器 s 以及调试组件的寄存器 s。除此之外,还禁止使用 MSR 访问刚才讲到的特殊功能寄存器——除了 APSR 有例外。谁若是以身试法,则将 fault 伺候。
- 在特权级下的代码可以通过置位 CONTROL[0]来进入用户级。而不管是任何原因产生了任何异常,处理器都将以特权级来运行其服务例程,异常返回后将回到产生异常之前的特权级。用户级下的代码不能再试图修改 CONTROL[0]来回到特权级。它必须通过一个异常 handler,由那个异常 handler 来修改 CONTROL[0],才能在返回到线程模式后拿到特权级
如前所述,特权等级和堆栈指针的选择均由 CONTROL 负责。当 CONTROL[0]=0 时,在异常处理的始末,只发生了处理器模式的转换,如下图所示。
中断前后的状态转换:
但若 CONTROL[0]=1(线程模式+用户级),则在中断响应的始末, both 处理器模式和特权等极都要发生变化,如下图所示。
中断前后的状态转换+特权等级切换
CONTROL[ 0]只有在特权级下才能访问。用户级的程序如想进入特权级,通常都是使用一条“系统服务呼叫指令( SVC)”来触发“SVC 异常”,该异常的服务例程可以选择修改CONTROL[0]。
Cortex-M3 的双堆栈机制
我们已经知道了 CM3 的堆栈是分为两个:主堆栈和进程堆栈, CONTROL[1]决定如何选择。
CONTROL[1]=0 时的堆栈使用情况:
当 CONTROL[1]=0 时,只使用 MSP,此时用户程序和异常 handler 共享同一个堆栈。这也是复位后的缺省使用方式。
CONTROL[1]=1 时的堆栈切换情况
当 CONTROL[1]=1 时,线程模式将不再使用 PSP,而改用 MSP( handler 模式永远使用MSP)。此时,进入异常时的自动压栈使用的是进程堆栈,进入异常 handler 后才自动改为 MSP,退出异常时切换回 PSP,并且从进程堆栈上弹出数据。
在特权级下,可以指定具体的堆栈指针,而不受当前使用堆栈的限制,示例代码如下:
MRS R0, MSP ; 读取主堆栈指针到 R0
MSR MSP, R0 ; 写入 R0 的值到主堆栈中
MRS R0, PSP ; 读取进程堆栈指针到 R0
MSR PSP, R0 ; 写入 R0 的值到进程堆栈中
通过读取 PSP 的值, OS 就能够获取用户应用程序使用的堆栈,进一步地就知道了在发生异常时,被压入寄存器的内容,而且还可以把其它寄存器进一步压栈(使用STMDB和LDMIA的书写形式)。 OS 还可以修改 PSP,用于实现多任务中的任务上下文切换。
问题说明
通过上面的解释我们可以总结以下关键语句
- 当处理器处在线程状态下时,既可以使用特权级,也可以使用用户级;另一方面, handler模式总是特权级的.
- 不管是任何原因产生了任何异常,处理器都将以特权级来运行其服务例程,异常返回后将回到产生异常之前的特权级
- 进入异常时的自动压栈使用的是进程堆栈,进入异常 handler 后才自动改为 MSP
- 使用双堆栈机制,在特权级下,可以指定具体的堆栈指针,而不受当前使用堆栈的限制
PendSV_Handler函数的执行是异常中断的执行,因此PendSV_Handler函数执行时,当前处于特权级下,此时可以切换PSP进程堆栈指针。因为异常时,自动压栈用的是PSP,此时在PendSV_Handler函数中的任何代码执行都是使用MSP主堆栈指针,不会影响到PSP进程堆栈指针。
为什么出栈和入栈都是采用R4-R11寄存器进行保存和切换的?
- R4-R11寄存器不能在异常发生时自动入栈
struct exception_stack_frame
{
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
{
/* r4 ~ r11 register */
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;
};
异常发生时 exception_stack_frame结构体内的寄存器是能够自动入栈的,而stack_frame结构体内的寄存器内容不能自动入栈,这是硬件设计导致的。这个栈是线程初始化时分配给当前线程的栈数组的地址段。为了完整保存整个线程的所有数据,其它不能自动入栈的寄存器则需要手动PUSH入栈。当前出栈也是手动操作。
知识点
常用的多重存储器访问方式
示例 | 功能描述 |
---|---|
LDMIA Rd!, {寄存器列表} | 从 Rd 处读取多个字。每读一个字后 Rd 自增一次, 16位宽度 |
STMIA Rd!, {寄存器列表} | 存储多个字到 Rd 处。每存一个字后 Rd 自增一次, 16位宽度 |
LDMIA.W Rd!, {寄存器列表} | 从 Rd 处读取多个字。每读一个字后 Rd 自增一次, 32位宽度 |
LDMDB.W Rd!, {寄存器列表} | 从 Rd 处读取多个字。每读一个字前 Rd 自减一次, 32位宽度 |
STMIA.W Rd!, {寄存器列表} | 存储多个字到 Rd 处。每存一个字后 Rd 自增一次, 32位宽度 |
STMDB.W Rd!, {寄存器列表} | 存储多个字到 Rd 处。每存一个字前 Rd 自减一次, 32位宽度 |
上表中,加粗的是符合 CM3 堆栈操作的 LDM/STM 使用方式。并且,如果 Rd 是 R13(即 SP),则与
POP/PUSH 指令等效。 (LDMIA‐>POP, STMDB ‐> PUSH)
STMDB SP!, {R0-R3, LR} 等效于 PUSH {R0-R3, LR}
LDMIA SP!, {R0-R3, PC} 等效于 PUSH {R0-R3, PC}
Rd 后面的“!”是什么意思?它表示要自增(Increment)或自减(Decrement)基址寄存器 Rd
的值,时机是在每次访问前(Before)或访问后(After)。增/减单位:字(4字节)。例如,记R8=0x8000,
则下面两条指令:
STMIA.W R8!, {r0-R3} ; R8 值变为 0x8010,每存一次曾一次,先存储后自增
STMDB.W R8, {R0-R3} ; R8 值的“一个内部复本”先自减后存储,但是 R8 的
值不变
感叹号还可以用于单一加载与存储指令——LDR/STR。这也就是所谓的 “带预索引”
(Pre‐indexing)的 LDR 和 STR。例如:
LDR.W R0, [R1, #20]! ;预索引
该指令先把地址 R1+offset 处的值加载到 R0,然后, R1 <- R1+ 20(offset 也可以是负数)。这里的“!”就是指在传送后更新基址寄存器 R1 的值。“!”是可选的。如果没有“!”,则该指令就是普通的带偏移量加载指令。带预索引的数据传送可以用在多种数据类型上,并且既可用于加载,又可用于存储。
预索引数据传送的常见用法
示例 | 功能描述 |
---|---|
LDR.W Rd, [Rn, #offset]! LDRB.W Rd, [Rn, #offset]! LDRH.W Rd, [Rn, #offset]! LDRD.W Rd1, Rd2, [Rn, #offset]! | 字/字节/半字/双字的带预索引加载(不做带符号扩展,没有用到的高位全清 0) |
LDRSB.W Rd, [Rn, #offset]! LDRSH.W Rd, [Rn, #offset]! | 字节/半字的带预索引加载,并且在加载后执行带符号扩展成 32 位整数 |
STR.W Rd, [Rn, #offset]! STRB.W Rd, [Rn, #offset]! STRH.W Rd, [Rn, #offset]! STRD.W Rd1, Rd2, [Rn, #offset]! |
CM3 除了支持“预索引”,还支持“后索引” (Post‐indexing)。后索引也要使用一个立即数 offset,但与预索引不同的是,后索引是忠实使用基址寄存器 Rd 的值作为数据传送的地址的。待到数据传送后,再执行 Rd Å Rd+offset(offset 可以是负数)。如:
STR.W R0, [R1], #-12 ;后索引
该指令是把 R0 的值存储到地址 R1 处的。在存储完毕后, R1 Å R1+(‐12)
注意, [R1]后面是没有“! ”的。可见, 在后索引中,基址寄存器是无条件被更新的——相当于有一个“隐藏”的“!
后索引的常见用法
示例 | 功能描述 |
---|---|
LDR.W Rd, [Rn], #offset LDRB.W Rd, [Rn], #offset LDRH.W Rd, [Rn], #offset LDRD.W Rd1, Rd2, [Rn], #offset | 字/字节/半字/双字的带预索引加载(不做带符号扩展,没有用到的高位全清 0) |
LDRSB.W Rd, [Rn], #offset] LDRSH.W Rd, [Rn], #offset] | 字节/半字的带预索引加载,并且在加载后执行带符号扩展成 32 位整数 |
STR.W Rd, [Rn], #offset STRB.W Rd, [Rn], #offset STRH.W Rd, [Rn], #offset STRD.W Rd1, Rd2, [Rn], #offset |
指令STMFD和LDMFD分析
指令 | 说明 |
---|---|
LDM | 从一片连续的地址空间中加载多个字到若干寄存器 |
STM | 存储若干寄存器中的字到一片连续的地址空间中 |
LDMIA | 加载多个字,并且在加载后自增基址寄存器 |
STMIA | 加载多个字,并且在加载后自增基址寄存器 |
栈指针指向栈顶元素(即最后一个入栈的数据元素)时称为FULL栈;
栈指针指向与栈顶元素相邻的一个可用书局单元时称为EMPTY栈。
根据数据栈的增长方向不同:
当数据栈向内存地址减小的方向增长时,称为Descending栈;
当数据栈向内存地址增加的方向增长时,称为 Ascending栈
综合上面两点,可以存在以下四种数据栈:
指令 | 说明 |
---|---|
FD - Full Descending | 入栈,数据栈向内存地址减小的方向增长 |
ED - Empty Descending | 出栈,数据栈向内存地址减小的方向增长 |
FA - Full Ascending | 入栈,数据栈向内存地址增加的方向增长 |
EA - Empty Ascending | 出栈,数据栈向内存地址增加的方向增长 |
因此实际上存在下面这些批量load/save指令:
LDMFA, LDMFD, LDMEA, LDMED
STMED, STMEA, STMFD, STMFA
给定数据栈对应着的特定批量load/save指令,也决定了地址变化方式:
比如FD栈,对应的批量传送指令是LDMFD/STMFD,对应的地址变化方式是IA(事后递增方式), DB(事先递减方式)
STMFD SP!, {R0~R7, LR}
start_address = sp - 9 * 4
end_address = sp - 4
把寄存器r0~r7和LR共9个寄存器,存储到start_address开始, 到end_address结束的栈中,并且修改SP的值(SP变小),相当于压栈。
LDMFD SP!, {R0~R7, LR}
start_address = SP
end_address = SP + 9 * 4
把堆栈从start_address开始,到end_address内的值恢复到寄存器R0, R1… R7和LR中,并修改SP的值(SP变大),相当于出栈。
16 位数据操作指令
名字 | 功能 |
---|---|
ADC | 带进位加法 |
ADD | 加法 |
AND | 按位与(原文为逻辑与,有误——译注)。这里的按位与和 C 的”&”功能相同 |
ASR | 算术右移 |
BIC | 按位清 0(把一个数跟另一个无符号数的反码按位与) |
CMN | 负向比较(把一个数跟另一个数据的二进制补码相比较) |
CMP | 比较(比较两个数并且更新标志) |
CPY | 把一个寄存器的值拷贝到另一个寄存器中 |
EOR | 近位异或 |
LSL | 逻辑左移(如无其它说明,所有移位操作都可以一次移动多格——译注) |
LSR | 逻辑右移 |
MOV | 寄存器加载数据,既能用于寄存器间的传输,也能用于加载立即数 |
MUL | 乘法 |
MVN | 加载一个数的 NOT 值(取到逻辑反的值) |
NEG | 取二进制补码 |
ORR | 按位或(原文为逻辑或,有误——译注) |
ROR | 圆圈右移 |
SBC | 带借位的减法 |
SUB | 减法 |
TST | 测试(执行按位与操作,并且根据结果更新 Z) |
REV | 在一个 32 位寄存器中反转字节序 |
REVH | 把一个 32 位寄存器分成两个 16 位数,在每个 16 位数中反转字节序 |
REVSH | 把一个 32 位寄存器的低 16 位半字进行字节反转,然后带符号扩展到 32 位 |
SXTB | 带符号扩展一个字节到 32 位 |
SXTH | 带符号扩展一个半字到 32 位 |
UXTB | 无符号扩展一个字节到 32 位 |
UXTH | 无符号扩展一个半字到 32 位 |
16 位转移指令
名字 | 功能 |
---|---|
B | 无条件转移 |
B | 条件转移 |
BL | 转移并连接。用于呼叫一个子程序,返回地址被存储在 LR 中 |
BLX #im | 使用立即数的 BLX 不要在 CM3 中使用 |
CBZ | 比较,如果结果为 0 就转移(只能跳到后面的指令——译注) |
CBNZ | 比较,如果结果非 0 就转移(只能跳到后面的指令——译注) |
IT | If‐Then |
CBZ 和 CBNZ
比较并条件跳转指令专为循环结构的优化而设,它只能做前向跳转。语法格式为:
CBZ <Rn>, <label>
CBNZ <Rn>, <label>
它们的跳转范围较窄,只有 0‐126。
典型范围如下所示:
while (R0!=0)
{
Function1();
}
变成
Loop
CBZ R0, LoopExit
BL Function1
B Loop
LoopExit:
与其它的比较指令不同, CBZ/CBNZ 不会更新标志位
总结
线程切换相关的基础函数已经基本介绍完毕,线程切换相关的基础函数都与硬件相关,但是确是移植系统的基础。