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://xiahunao.cn/news/2904901.html

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

腾讯云邮件推送功能有哪些?如何有效使用?

腾讯云邮件推送如何设置?怎么用邮件推送做高效营销? 腾讯云作为业界领先的云服务提供商,其邮件推送功能在便捷性、稳定性和安全性上都有着出色的表现。那么,腾讯云邮件推送功能究竟有哪些呢?让AokSend来探个究竟。 腾…

Vite 为什么比 Webpack 快?

目录 1. Webpack 的构建原理 2. Script 的模块化(主流浏览器对 ES Modules 的支持) 3. Webpack vs Vite 开发模式的差异 对 ES Modules 的支持 底层语言的差异 热更新的处理 1. Webpack 的构建原理 前端之所以需要类似于 Webpack 这样的构建工具&…

windows@系统信息查看若干方法@查看硬件信息@系统信息仪表盘@资源占用OSD悬浮窗口

文章目录 操作系统简要信息查看👺计算机软硬件信息查看windows自带工具msinfo32dxdiagcompmgmtsettingssysteminfo.exe 其他专业软件查看计算机软硬件信息👺OSD系统仪表盘系列软件TrafficMonitor插件功能 Rainmeter时间更改板块刷新显示和关闭 Rainmeter…

实现DevOps需要什么?

实现DevOps需要什么? 硬性要求:工具上的准备 上文提到了工具链的打通,那么工具自然就需要做好准备。现将工具类型及对应的不完全列举整理如下: 代码管理(SCM):GitHub、GitLab、BitBucket、SubV…

flutter 修改app名字和图标

一、修改名字 在Android中修改应用程序名称&#xff1a; 在AndroidManifest.xml文件中修改应用程序名称&#xff1a; 打开Flutter项目中的android/app/src/main/AndroidManifest.xml文件。找到<application>标签&#xff0c;然后在android:label属性中修改应用程序的名称…

Xcode删除原本的Git,再添加新的git

本文参考&#xff1a;Xcode怎么删除原本git,在重新设置新的git地址_ios xcode 删除原本git-CSDN博客 开发中会有一个问题。Xcode项目A 提交到Git服务器server1&#xff0c;此时项目A内部已经存在一个Git文件&#xff0c;与server1相关联。 此时你想将项目A提交到 另一个Git…

【InternLM 实战营第二期笔记】书生·浦语大模型全链路开源体系及InternLM2技术报告笔记

大模型 大模型成为发展通用人工智能的重要途径 专用模型&#xff1a;针对特定任务&#xff0c;一个模型解决一个问题 通用大模型&#xff1a;一个模型应对多种任务、多种模态 书生浦语大模型开源历程 2023.6.7&#xff1a;InternLM千亿参数语言大模型发布 2023.7.6&#…

Python拆分PDF、Python合并PDF

WPS能拆分合并&#xff0c;但却是要输入编辑密码&#xff0c;我没有。故写了个脚本来做拆分&#xff0c;顺便附上合并的代码。 代码如下&#xff08;extract.py) #!/usr/bin/env python """PDF拆分脚本(需要Python3.10)Usage::$ python extract.py <pdf-fil…

Linux用户及用户组权限

一、用户和用户组 功能项命令实例作用用户组cat /etc/group查看当前系统存在的用户组groupadd testing添加一个新的用户组testingcat /etc/group查看组是否被新增成功groupmod -n test testing将testing重命名成testgroupdel test删除组testgroups root查看用户root所在的所有…

原型链-(前端面试 2024 版)

来讲一讲原型链 原型链只存在于函数之中 四个规则 1、引用类型&#xff0c;都具有对象特性&#xff0c;即可自由扩展属性。 2、引用类型&#xff0c;都有一个隐式原型 __proto__ 属性&#xff0c;属性值是一个普通的对象。 3、引用类型&#xff0c;隐式原型 __proto__ 的属…

基于单片机小型家用燃气锅炉控制系统设计

**单片机设计介绍&#xff0c;基于单片机小型家用燃气锅炉控制系统设计 文章目录 一 概要二、功能设计设计思路 三、 软件设计原理图 五、 程序六、 文章目录 一 概要 基于单片机的小型家用燃气锅炉控制系统设计&#xff0c;主要目标是实现锅炉的智能化控制&#xff0c;包括温…

1学习使用axios

一、axios介绍&#xff1a; axios 是一个基于 Promise 的 HTTP 客户端&#xff0c;用于浏览器和 Node.js。它提供了一种简单的方法来发送 HTTP 请求&#xff0c;并且具有很多实用的功能&#xff0c;使得网络请求变得更加方便和可靠。 以下是 axios 的一些主要特点和功能&…

边缘计算AI盒子目前支持的AI智能算法、视频智能分析算法有哪些,应用于大型厂矿安全生产风险管控

一、前端设备实现AI算法 主要是基于安卓的布控球实现&#xff0c;已有的算法包括&#xff1a; 1&#xff09;人脸&#xff1b;2&#xff09;车牌&#xff1b;3&#xff09;是否佩戴安全帽&#xff1b;4&#xff09;是否穿着工装&#xff1b; 可以支持定制开发 烟雾&#xf…

Android ImageView以及实现截图

实现效果 截图前 截图后 代码 package cn.jj.huaweiad;import android.annotation.SuppressLint; import android.graphics.Bitmap; import android.os.Bundle; import android.os.Handler; import android.util.Log; import android.view.View; import android.view.ViewGro…

如何在Apache Arrow中定位与解决问题

如何在apache Arrow定位与解决问题 最近在执行sql时做了一些batch变更&#xff0c;出现了一个 crash问题&#xff0c;底层使用了apache arrow来实现。本节将会从0开始讲解如何调试STL源码crash问题&#xff0c;在这篇文章中以实际工作中resize导致crash为例&#xff0c;引出如何…

微信小程序更换头像的功能

微信小程序开发&#xff0c;个人中心中更换头像的更能使用频率很高&#xff0c;这里记录下实现方式&#xff1a; <view class"setting-list avatar-container"><text>头像</text><view class"avatar"><button hover-class"…

【零基础C语言】编译和链接

1.翻译环境和运行环境 翻译环境&#xff1a;将源代码转化为可执行的机器指令 运行环境&#xff1a;用于执行机器指令 1.1 翻译环境 翻译环境由编译和链接两大过程构建&#xff0c;编译又可以分为三大过程&#xff1a; 【1】预处理(预编译) 【2】编译 【3】汇编 不同的.c文件经…

Django安装及第一个项目

1、安装python C:\Users\leell>py --version Python 3.10.6 可以看出我的环境python的版本3.10.6&#xff0c;比较新 2、 Python 虚拟环境创建 2.1 官网教程 目前&#xff0c;有两种常用工具可用于创建 Python 虚拟环境&#xff1a; venv 在 Python 3.3 及更高版本中默…

IP种子是什么?理解和应用

在网络世界中&#xff0c;IP种子是一个广泛应用于文件共享和网络下载领域的概念。它是一种特殊的标识符&#xff0c;用于识别和连接到基于对等网络&#xff08;P2P&#xff09;协议的文件共享网络中的用户或节点。本文将深入探讨IP种子的含义、作用以及其在网络中的应用。 IP地…

Vivado使用(1)——综合的约束与策略

目录 一、概述 二、约束与策略 2.1 约束 2.1.1 物理约束 2.1.2 时序约束 2.2 综合策略 2.2.1 flatten_hierarchy 2.2.2 gated_clock_conversion 2.2.3 bufg 2.2.4 fanout_limit 2.2.5 directive 2.2.6 retiming 2.2.7 fsm_extraction 2.2.8 keep_equivalent_regi…