RTOS线程切换的过程和原理

news/2024/4/27 23:25:40/文章来源:https://blog.csdn.net/kevin1499/article/details/137092935

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.luyixian.cn/news_show_1027373.aspx

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

相关文章

<QT基础(5)>事件监听

事件监听 事件监听(Event Handling)是在程序中监视和响应发生的事件的一种机制。在Qt中,事件监听是一种常见的用于处理用户输入、系统事件以及其他类型事件的方法。通过事件监听,您可以在发生特定事件时捕获事件并执行相应的操作…

【AI】在本地 Docker 环境中搭建使用 Hugging Face 托管的 Llama 模型

目录 Hugging Face 和 LLMs 简介利用 Docker 进行 ML格式的类型请求 Llama 模型访问创建 Hugging Face 令牌设置 Docker 环境快速演示访问页面入门克隆项目构建镜像运行容器结论推荐超级课程: Docker快速入门到精通Kubernetes入门到大师通关课AWS云服务快速入门实战Hugging Fa…

7、鸿蒙学习-共享包概述

HarmonyOS提供了两种共享包,HAR(Harmony Archive)静态共享包,和HSP(Harmony Shared Package)动态共享包。 HAR与HSR都是为了实现代码和资源的共享,都可以包含代码、C库、资源和配置文件&#xf…

iPhone用GPT替代Siri

shigen坚持更新文章的博客写手,擅长Java、python、vue、shell等编程语言和各种应用程序、脚本的开发。记录成长,分享认知,留住感动。 个人IP:shigen 前一段时间,因为iCloud协议的更新,我的云盘空间无法正常…

RISC-V特权架构 - 中断定义

RISC-V特权架构 - 中断定义 1 中断类型1.1 外部中断1.2 计时器中断1.3 软件中断1.4 调试中断 2 中断屏蔽3 中断等待4 中断优先级与仲裁5 中断嵌套6 异常相关寄存器 本文属于《 RISC-V指令集基础系列教程》之一,欢迎查看其它文章。 1 中断类型 RISC-V 架构定义的中…

helm 部署 Kube-Prometheus + Grafana + 钉钉告警部署 Kube-Prometheus

背景 角色IPK8S 版本容器运行时k8s-master-1172.16.16.108v1.24.1containerd://1.6.8k8s-node-1172.16.16.109v1.24.1containerd://1.6.8k8s-node-2172.16.16.110v1.24.1containerd://1.6.8 安装 kube-prometheus mkdir -p /data/yaml/kube-prometheus/prometheus &&…

CDH集群hive初始化元数据库失败

oracle数据库操作: 报错如下:命令 (Validate Hive Metastore schema (237)) 已失败 截图如下: 后台日志部分摘录: WARNING: Use “yarn jar” to launch YARN applications. SLF4J: Class path contains multiple SLF4J binding…

UE RPC 外网联机(1)

技术&#xff1a;RPC TCP通信 设计&#xff1a;大厅服务<---TCP--->房间服务<---RPC--->客户端&#xff08;Creator / Participator&#xff09; 1. PlayerController 用于RPC通信控制 2.GameMode 用于数据同步 3.类图 4. 注意 &#xff08;1&#xff09;RPC&a…

机器学习之决策树现成的模型使用

目录 须知 DecisionTreeClassifier sklearn.tree.plot_tree cost_complexity_pruning_path(X_train, y_train) CART分类树算法 基尼指数 分类树的构建思想 对于离散的数据 对于连续值 剪枝策略 剪枝是什么 剪枝的分类 预剪枝 后剪枝 后剪枝策略体现之威斯康辛州乳…

GIMP - GNU 图像处理程序 - 工具栏窗口 (Toolbox)

GIMP - GNU 图像处理程序 - 工具栏窗口 [Toolbox] 1. GNU Image Manipulation Program2. Windows -> Recently Closed Docks -> ToolboxReferences 1. GNU Image Manipulation Program 2. Windows -> Recently Closed Docks -> Toolbox References [1] Yongqiang …

软件概要设计说明书word原件(实际项目)

一、 引言 &#xff08;一&#xff09; 编写目的 &#xff08;二&#xff09; 范围 &#xff08;三&#xff09; 文档约定 &#xff08;四&#xff09; 术语 二、 项目概要 &#xff08;一&#xff09; 建设背景 &#xff08;二&#xff09; 建设目标 &#xff08;三&a…

Typora字数过多的时候造成卡顿现象如何解决?

Typora字数过多的时候造成卡顿现象如何解决&#xff1f; 点击 、切换、滚动、打字都有点卡顿&#xff0c;下面介绍三种方法&#xff0c;三种方法都可以尝试&#xff0c;建议先尝试方法一&#xff0c;效果不满意就用方法二&#xff0c;实在不行就最后一个取巧的办法。 方法1&a…

图像处理与视觉感知---期末复习重点(5)

文章目录 一、膨胀与腐蚀1.1 膨胀1.2 腐蚀 二、开操作与闭操作 一、膨胀与腐蚀 1.1 膨胀 1. 集合 A A A 被集合 B B B 膨胀&#xff0c;定义式如下。其中集合 B B B 也称为结构元素&#xff1b; ( B ^ ) z (\hat{B})z (B^)z 表示 B B B 的反射平移 z z z 后得到的新集合。…

《Vision mamba》论文笔记

原文出处&#xff1a; [2401.09417] Vision Mamba: Efficient Visual Representation Learning with Bidirectional State Space Model (arxiv.org) 原文笔记&#xff1a; What&#xff1a; Vision Mamba: Efficient Visual Representation Learning with Bidirectional St…

llama-index 结合chatglm3-6B 利用RAG 基于文档智能问答

简介 llamaindex结合chatglm3使用 import os import torch from llama_index.core import VectorStoreIndex, ServiceContext from llama_index.core.callbacks import CallbackManager from llama_index.core.llms.callbacks import llm_completion_callback from llama_ind…

Groovy基础入门

一、Groovy简介 Groovy是运行在JVM中的一种动态语言&#xff0c;可以在Java平台上进行编程&#xff0c;使用方式基本与使用Java代码的方式相同&#xff0c;它的语法与Java语言的语法很相似&#xff0c;与Java相比&#xff0c;Groovy更加灵活、简洁&#xff0c;而且完成同样的功…

C语言例4-6:格式字符d的使用例子

代码如下&#xff1a; //格式字符d的使用例子 #include<stdio.h> int main(void) {int num1123;long num2123456;printf("num1%d,num1%5d,num1%-5d,num1%2d\n",num1,num1,num1,num1);//以四种不同格式&#xff0c;输出int型数据num1的值printf("num2%ld,…

Mybatis别名 动态sql语句 分页查询

给Mybatis的实体类起别名 给Mybatis的xml文件注册mapper映射文件 动态sql语句 1 if 2 choose 3 where 4 foreach 一&#xff09;if 查询指定名称商品信息 语法&#xff1a; SELECT * FROM goods where 11 <if test "gName!null"> and g.g_name like co…

linux:线程同步

个人主页 &#xff1a; 个人主页 个人专栏 &#xff1a; 《数据结构》 《C语言》《C》《Linux》 文章目录 前言线程同步条件变量接口简单示例pthread_cond_wait为什么要有mutex伪唤醒问题的解决 (if->while) 总结 前言 本文作为我对于线程同步知识总结 线程同步 同步&…

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…