RTOS线程切换的过程和原理

0 前言

RTOS中最重要的一个概念就是线程,线程的按需切换能够满足RTOS的实时性要求,同时能将复杂的需求分解成一个个线程执行减轻我们开发负担。
本文从栈的角度出发,详细介绍RTOS线程切换的过程和原理。
注:本文参考的RTOS是RT-Tthread。

1 初始化线程

对于裸机来说,我们大可不必关心栈的内容。对于RTOS来说,每个线程都有自己独立的栈区,用来保存R0-R15寄存器、形参、局部变量等内容,在正式开始线程调度前需要初始化线程栈。
初始化线程栈的操作实际上就是将栈空间内的数据赋一些初值,初始化完成后的栈空间内容如下:
在这里插入图片描述
上述操作完成后,会将栈顶的值赋给线程控制块的*SP(线程堆栈指针)。可以很容易发现,假设栈底地址为BSADDR,则SP=BSADDR-64。这里要注意,如果线程函数有2个形参,则第一个形参传入R0、第二个形参传入R1(形参的第一、第二顺序为从左往右)。
至于为什么线程栈要这么分布,这里有一个相关知识点:
我们切换线程前都会触发PendSV异常,然后CPU会按照下图规则根据PSP(进程堆栈指针的值)将xPSR, PC, LR, R12以及R3-R0保存进线程栈(入栈),出栈时操作相反。假设PSP的值是N,则入栈的操作如下:
在这里插入图片描述
其实初始化线程栈就像构造了一个虚假的现场,然后让CPU去恢复它。

2 第一次切换线程

RTOS第一次切换线程的时候会从就绪链表中挑选出优先级最高的线程执行,由于是第一个执行的线程因此不需要保存上文,只需要切换下文即可。第一次切换线程可以分为2个部分展开,首先是开启第一次线程切换,然后是在PendSV异常服务函数内进行下文切换。

2.1 开启第一次线程切换

以RT-Thread为例,开启第一次线程切换函数如下:

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的地址加载到r1STR     r0, [r1]                                ;将r0的值存储到rt_interrupt_to_thread; 设置rt_interrupt_from_thread的值为0,表示启动第一次线程切换LDR     r1, =rt_interrupt_from_thread           ;将rt_interrupt_from_thread的地址加载到r1MOV     r0, #0x0                                ;配置r0等于0STR     r0, [r1]                                ;将r0的值存储到rt_interrupt_from_thread; 设置中断标志位rt_thread_switch_interrupt_flag的值为1LDR     r1, =rt_thread_switch_interrupt_flag    ;将rt_thread_switch_interrupt_flag的地址加载到r1MOV     r0, #1                                  ;配置r0等于1STR     r0, [r1]                                ;将r0的值存储到rt_thread_switch_interrupt_flag; 设置 PendSV 异常的优先级LDR     r0, =NVIC_SYSPRI2LDR     r1, =NVIC_PENDSV_PRILDR.W   r2, [r0,#0x00]       ; 读ORR     r1,r1,r2             ; 改STR     r1, [r0]             ;; 触发 PendSV 异常 (产生上下文切换)LDR     r0, =NVIC_INT_CTRLLDR     r1, =NVIC_PENDSVSETSTR     r1, [r0]; 开中断CPSIE   FCPSIE   I; 永远不会到达这里ENDP

该函数的操作流程如下:
(1)设置rt_interrupt_to_thread的值为第一个执行线程的线程控制块SP的值。
(2)设置rt_interrupt_from_thread的值为0,表明这是第一次线程切换,不需要保存上文。
(3)设置rt_thread_switch_interrupt_flag值为1,告知上下文切换服务函数这是一个有效的切换线程请求。
(4)设置PendSV的异常优先级为最低(避免打断其它中断),触发PendSV异常,开全局中断。

2.2 上下文切换

上下文切换的异常服务函数是用汇编写的,以RT-Thread为例,其实现上下文切换的函数如下:

PendSV_Handler   PROCEXPORT PendSV_Handler; 失能中断,为了保护上下文切换不被中断MRS     r2, PRIMASKCPSID   I; 获取中断标志位,看看是否为0LDR     r0, =rt_thread_switch_interrupt_flag     ; 加载rt_thread_switch_interrupt_flag的地址到r0LDR     r1, [r0]                                 ; 加载rt_thread_switch_interrupt_flag的值到r1CBZ     r1, pendsv_exit                          ; 判断r1是否为0,为0则跳转到pendsv_exit; r1不为0则清0MOV     r1, #0x00STR     r1, [r0]                                 ; 将r1的值存储到rt_thread_switch_interrupt_flag,即清0; 判断rt_interrupt_from_thread的值是否为0LDR     r0, =rt_interrupt_from_thread            ; 加载rt_interrupt_from_thread的地址到r0LDR     r1, [r0]                                 ; 加载rt_interrupt_from_thread的值到r1CBZ     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                                  ; 获取线程栈指针到r1STMFD   r1!, {r4 - r11}                          ;将CPU寄存器r4~r11的值存储到r1指向的地址(每操作一次地址将递增一次)LDR     r0, [r0]                                 ; 加载r0指向值到r0,即r0=rt_interrupt_from_threadSTR     r1, [r0]                                 ; 将r1的值存储到r0,即更新线程栈sp; ========================== 下文切换 ==============================
switch_to_threadLDR     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,即spLDMFD   r1!, {r4 - r11}                           ;将线程栈指针r1(操作之前先递减)指向的内容加载到CPU寄存器r4~r11MSR     psp, r1                                   ;将线程栈指针更新到PSPpendsv_exit; 恢复中断MSR     PRIMASK, r2ORR     lr, lr, #0x04                             ; 确保异常返回使用的堆栈指针是PSP,即LR寄存器的位2要为1BX      lr                                        ; 异常返回,这个时候任务堆栈中的剩下内容将会自动加载到xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务的形参); 同时PSP的值也将更新,即指向任务堆栈的栈顶。在ARMC3中,堆是由高地址向低地址生长的。; PendSV_Handler 子程序结束ENDP	ALIGN   4END

该函数的操作流程如下:
(1)失能全局中断,避免切换上下文过程被打断。
(2)获取中断标志位,查看此次异常是否是由线程切换函数发起。
(3)检查rt_interrupt_from_thread 的值。如果是0则无需进行上文保存直接去切换下文;如果非0则先去保存上文再去切换下文。由于是第一次切换线程,这里rt_interrupt_from_thread 的值为0,直接去切换下文。
(4)通过2次指针操作获取前面初始化线程栈的SP的值,也就是BSADDR-64:
在这里插入图片描述
(5)将保存在线程栈的R4-R11的数据加载到CPU对应的R4-R11寄存器。同时R1的值设置为SP+32=BSADDR-32,最后将R1的值更新到PSP。相关语句如下:
在这里插入图片描述
LDMFD指令功能是弹出栈中的多个数据,采用事后递增方式,先弹出数据,再将SP指针增大。
(6)在上下文切换完成后,恢复中断。
(7)确保异常返回使用的堆栈指针是PSP,也就是要保证LR寄存器的bit2为1:
在这里插入图片描述
(8)最后异常返回,这时CPU会自动进行出栈操作,也就是将xPSR, PC, LR, R12以及R3-R0出栈,此时PSP指针的值为SP+32-32=BASDDR

3 线程切换

3.1 产生上下文切换

在有多个线程运行的情况下,就会有线程的切换操作。在RT-Thread中用于产生上下文切换的函数如下:

rt_hw_context_switch    PROCEXPORT rt_hw_context_switch; 设置中断标志位rt_thread_switch_interrupt_flag为1     LDR     r2, =rt_thread_switch_interrupt_flag          ; 加载rt_thread_switch_interrupt_flag的地址到r2LDR     r3, [r2]                                      ; 加载rt_thread_switch_interrupt_flag的值到r3CMP     r3, #1                                        ; r3与1比较,相等则执行BEQ指令,否则不执行BEQ     _reswitchMOV     r3, #1                                        ; 设置r3的值为1STR     r3, [r2]                                      ; 将r3的值存储到rt_thread_switch_interrupt_flag,即置1; 设置rt_interrupt_from_thread的值LDR     r2, =rt_interrupt_from_thread                 ; 加载rt_interrupt_from_thread的地址到r2STR     r0, [r2]                                      ; 存储r0的值到rt_interrupt_from_thread,即上一个线程栈指针sp的指针_reswitch; 设置rt_interrupt_to_thread的值LDR     r2, =rt_interrupt_to_thread                   ; 加载rt_interrupt_from_thread的地址到r2STR     r1, [r2]                                      ; 存储r1的值到rt_interrupt_from_thread,即下一个线程栈指针sp的指针; 触发PendSV异常,实现上下文切换LDR     r0, =NVIC_INT_CTRL              LDR     r1, =NVIC_PENDSVSETSTR     r1, [r0]; 子程序返回BX      LR; 子程序结束ENDP

该函数的操作流程如下:
(1)设置rt_interrupt_from_thread的值为1,相关语句如下:
在这里插入图片描述
(2)保存上一个线程栈的SP指针到rt_interrupt_from_thread,相关语句如下:
在这里插入图片描述
(3)保存需要切换的下一个线程的SP指针到rt_interrupt_to_thread,相关语句如下:
在这里插入图片描述
(4)触发PendSV异常,进行上下文切换,相关语句如下:
在这里插入图片描述

3.2 进行上下文切换

以RT-Thread为例,其实现上下文切换的函数如下:

PendSV_Handler   PROCEXPORT PendSV_Handler; 失能中断,为了保护上下文切换不被中断MRS     r2, PRIMASKCPSID   I; 获取中断标志位,看看是否为0LDR     r0, =rt_thread_switch_interrupt_flag     ; 加载rt_thread_switch_interrupt_flag的地址到r0LDR     r1, [r0]                                 ; 加载rt_thread_switch_interrupt_flag的值到r1CBZ     r1, pendsv_exit                          ; 判断r1是否为0,为0则跳转到pendsv_exit; r1不为0则清0MOV     r1, #0x00STR     r1, [r0]                                 ; 将r1的值存储到rt_thread_switch_interrupt_flag,即清0; 判断rt_interrupt_from_thread的值是否为0LDR     r0, =rt_interrupt_from_thread            ; 加载rt_interrupt_from_thread的地址到r0LDR     r1, [r0]                                 ; 加载rt_interrupt_from_thread的值到r1CBZ     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                                  ; 获取线程栈指针到r1STMFD   r1!, {r4 - r11}                          ;将CPU寄存器r4~r11的值存储到r1指向的地址(每操作一次地址将递增一次)LDR     r0, [r0]                                 ; 加载r0指向值到r0,即r0=rt_interrupt_from_threadSTR     r1, [r0]                                 ; 将r1的值存储到r0,即更新线程栈sp; ========================== 下文切换 ==============================
switch_to_threadLDR     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,即spLDMFD   r1!, {r4 - r11}                           ;将线程栈指针r1(操作之前先递减)指向的内容加载到CPU寄存器r4~r11MSR     psp, r1                                   ;将线程栈指针更新到PSPpendsv_exit; 恢复中断MSR     PRIMASK, r2ORR     lr, lr, #0x04                             ; 确保异常返回使用的堆栈指针是PSP,即LR寄存器的位2要为1BX      lr                                        ; 异常返回,这个时候任务堆栈中的剩下内容将会自动加载到xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务的形参); 同时PSP的值也将更新,即指向任务堆栈的栈顶。在ARMC3中,堆是由高地址向低地址生长的。; PendSV_Handler 子程序结束ENDP	ALIGN   4END

该函数的操作流程如下:
(1)失能全局中断,避免切换上下文过程被打断。
(2)获取中断标志位,查看此次异常是否是由线程切换函数发起。
(3)检查rt_interrupt_from_thread 的值。如果是0则无需进行上文保存直接去切换下文;如果非0则先去保存上文再去切换下文。
上文保存:
(4)将上一个线程的PSP到R1(这里要注意,不是直接拿保存在线程控制块栈指针),由于CPU已经自动将xPSR, PC, LR, R12以及R3-R0入栈,我们只需要手动把CPU寄存器R4-R11的数据保存到线程栈内即可完成上文的保存,最后将更新后的栈指针赋给线程控制块的SP。相关语句如下:
在这里插入图片描述
STMFD指令是向栈内压入多个数据,采用事先递减的方式。

下文切换:
(5)通过2次指针操作获取下一个需要运行线程的线程控制块保存的SP的值:
在这里插入图片描述
(5)将保存在线程栈的R4-R11的数据加载到CPU对应的R4-R11寄存器。同时R1的值设置为SP+32,最后将R1的值更新到PSP。相关语句如下:
在这里插入图片描述
LDMFD指令功能是弹出栈中的多个数据,采用事后递增方式。
(6)在上下文切换完成后,恢复中断。
(7)确保异常返回使用的堆栈指针是PSP,也就是要保证LR寄存器的bit2为1:
在这里插入图片描述
(8)最后异常返回,这时CPU会自动进行出栈操作,也就是将xPSR, PC, LR, R12以及R3-R0出栈,此时PSP=SP+64

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/289987.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

DataX-Oracle新增writeMode支持update

目录 前言 第一步下载源码 第二步修改源码 1、Oraclewriter 2、WriterUtil 2.1、修改getWriteTemplate方法 2.2、新增onMergeIntoDoString与getStrings方法 3、CommonRdbmsWriter 3.1、修改startWriteWithConnection 3.2、修改doBatchInsert 3.3、修改fillPreparedStatem…

单例模式如何保证实例的唯一性

前言 什么是单例模式 指一个类只有一个实例,且该类能自行创建这个实例的一种创建型设计模式。使用目的:确保在整个系统中只能出现类的一个实例,即一个类只有一个对象。对于频繁使用的对象,“忽略”创建时的开销。特点&#xff1a…

zedboard+AD9361 运行 open WiFi

先到github上下载img,网页链接如下: https://github.com/open-sdr/openwifi?tabreadme-ov-file 打开网页后下载 openwifi img 用win32 Disk lmager 把文件写入到SD卡中,这一步操作会把SD卡重新清空,注意保存数据。这个软件我会…

基于SpringBoot和Vue的在线视频教育平台的设计与实现

今天要和大家聊的是一款基于SpringBoot和Vue的在线视频教育平台的设计与实现 !!! 有需要的小伙伴可以通过文章末尾名片咨询我哦!!! 💕💕作者:李同学 💕&…

mysql--事务四大特性与隔离级别

事务四大特性与隔离级别 mysql事务的概念事务的属性事务控制语句转账示例 并发事务引发的问题脏读脏读场景 不可重复读幻读幻读场景 事务的隔离级别读未提交读已提交可重复读(MySQL默认) 总结 mysql事务的概念 事务就是一组操作的集合,他是一…

STM32时钟简介

1、复位:使时钟恢复原始状态 就是将寄存器状态恢复到复位值 STM32E10xxx支持三种复位形式,分别为系统复位、上电复位和备份区域复位。 复位分类: 1.1系统复位 除了时钟控制器的RCC_CSR寄存器中的复位标志位和备份区域中的寄存器以外,系统 复位将复位…

leetcode热题100.柱状图中最大的矩形

Problem: 84. 柱状图中最大的矩形 文章目录 题目思路复杂度Code 题目 给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。 求在该柱状图中,能够勾勒出来的矩形的最大面积。 示例 1: 输入:hei…

Qlib-Server:量化库数据服务器

Qlib-Server:量化库数据服务器 介绍 Qlib-Server 是 Qlib 的配套服务器系统,它利用 Qlib 进行基本计算,并提供广泛的服务器系统和缓存机制。通过 Qlib-Server,可以以集中的方式管理 Qlib 提供的数据。 框架 Qlib 的客户端/服务器框架基于 WebSocket 构建,这是因为 WebS…

OpenHarmony中的LLDB高性能调试器

概述 LLDB(Low Lever Debugger)是新一代高性能调试器。详细说明参考 LLDB官方文档 。 当前OpenHarmony中的LLDB工具是在 llvm15.0.4 基础上适配演进出来的工具,是HUAWEI DevEco Studio工具中默认的调试器,支持调试C和C应用。 工…

镜视界 | DevSecOps CI/CD 管道中数字供应链安全的集成策略

目录 前言 数字供应链(DSC)的定义 数字供应链安全的重点内容和风险因素 CI/CD管道的安全目标和可信实体 将数字供应链安全集成到CI/CD管道中 结语 本文字数:7715,阅读时长:19分钟 1.前言 在敏捷开发的模式下&…

SnapGene 5 for Mac 分子生物学软件

SnapGene 5 for Mac是一款专为Mac操作系统设计的分子生物学软件,以其强大的功能和用户友好的界面,为科研人员提供了高效、便捷的基因克隆和分子实验设计体验。 软件下载:SnapGene 5 for Mac v5.3.1中文激活版 这款软件支持DNA构建和克隆设计&…

StarRocks实战——多点大数据数仓构建

目录 前言 一、背景介绍 二、原有架构的痛点 2.1 技术成本 2.2 开发成本 2.2.1 离线 T1 更新的分析场景 2.2.2 实时更新分析场景 2.2.3 固定维度分析场景 2.2.4 运维成本 三、选择StarRocks的原因 3.1 引擎收敛 3.2 “大宽表”模型替换 3.3 简化Lambda架构 3.4 模…

目标检测的相关模型图:YOLO系列和RCNN系列

目标检测的相关模型图:YOLO系列和RCNN系列 前言YOLO系列的图展示YOLOpassthroughYOLO2YOLO3YOLO4YOLO5 RCNN系列的图展示有关目标检测发展的 前言 最近好像大家也都在写毕业论文,前段时间跟朋友聊天,突然想起自己之前写画了一些关于YOLO、Fa…

excel中批量插入分页符

excel中批量插入分页符,实现按班级打印学生名单。 1、把学生按照学号、班级排序好。 2、选择班级一列,点击数据-分类汇总。汇总方式选择计数,最后三个全部勾选。汇总结果一定要显示在数据的下发,如果显示在上方,后期…

实战|使用 Node.js 和 htmx 构建全栈应用程序

在本教程中,我将演示如何使用 Node 作为后端和 htmx 作为前端来构建功能齐全的 CRUD 应用程序。这将演示 htmx 如何集成到全栈应用程序中,使您能够评估其有效性并确定它是否是您未来项目的不错选择。 htmx 是一个现代 JavaScript 库,旨在通过…

系统架构图怎么画

画架构图是架构师的一门必修功课。 对于架构图是什么这个问题,我们可以按以下等式进行概括: 架构图 架构的表达 架构在不同抽象角度和不同抽象层次的表达,这是一个自然而然的过程。 不是先有图再有业务流程、系统设计和领域模型等&#…

项目四-图书管理系统

1.创建项目 流程与之前的项目一致,不再进行赘述。 2.需求定义 需求: 1. 登录: ⽤⼾输⼊账号,密码完成登录功能 2. 列表展⽰: 展⽰图书 3.前端界面测试 无法启动!!!--->记得加入mysql相关操作记得在yml进行配置 配置后启动…

基于ZHW3548的红外额温枪解决方案

红外额温枪,非接触式测量最典型的方法是红外测温。自红外辐射原理被发现以来,红外技术被广泛应用在温度测量中。红外测温仪具有测温范围广,响应速度快,灵敏度高等特点。红外耳温枪、红外额温计和红外筛检仪都属于非接触式体温计。…

admin端

一、创建项目 1.1 技术栈 1.2 vite 项目初始化 npm init vitelatest vue3-element-admin --template vue-ts 1.3 src 路径别名配置 Vite 配置 配置 vite.config.ts // https://vitejs.dev/config/import { UserConfig, ConfigEnv, loadEnv, defineConfig } from vite im…

C语言 C6031:返回值被忽略:“scanf“ 问题解决

我们在代码中 直接使用 scanf 就会出现这个错误 在最上面 加上 #define _CRT_SECURE_NO_WARNINGS//禁用安全函数警告 #pragma warning(disable:6031)//禁用 6031 的安全警告即可正常运行