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的地址加载到r1
    STR     r0, [r1]                                ;将r0的值存储到rt_interrupt_to_thread

    ; 设置rt_interrupt_from_thread的值为0,表示启动第一次线程切换
    LDR     r1, =rt_interrupt_from_thread           ;将rt_interrupt_from_thread的地址加载到r1
    MOV     r0, #0x0                                ;配置r0等于0
    STR     r0, [r1]                                ;将r0的值存储到rt_interrupt_from_thread

    ; 设置中断标志位rt_thread_switch_interrupt_flag的值为1
    LDR     r1, =rt_thread_switch_interrupt_flag    ;将rt_thread_switch_interrupt_flag的地址加载到r1
    MOV     r0, #1                                  ;配置r0等于1
    STR     r0, [r1]                                ;将r0的值存储到rt_thread_switch_interrupt_flag

    ; 设置 PendSV 异常的优先级
    LDR     r0, =NVIC_SYSPRI2
    LDR     r1, =NVIC_PENDSV_PRI
    LDR.W   r2, [r0,#0x00]       ; 读
    ORR     r1,r1,r2             ; 改
    STR     r1, [r0]             ;; 触发 PendSV 异常 (产生上下文切换)
    LDR     r0, =NVIC_INT_CTRL
    LDR     r1, =NVIC_PENDSVSET
    STR     r1, [r0]

    ; 开中断
    CPSIE   F
    CPSIE   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   PROC
    EXPORT PendSV_Handler

    ; 失能中断,为了保护上下文切换不被中断
    MRS     r2, PRIMASK
    CPSID   I

    ; 获取中断标志位,看看是否为0
    LDR     r0, =rt_thread_switch_interrupt_flag     ; 加载rt_thread_switch_interrupt_flag的地址到r0
    LDR     r1, [r0]                                 ; 加载rt_thread_switch_interrupt_flag的值到r1
    CBZ     r1, pendsv_exit                          ; 判断r1是否为0,为0则跳转到pendsv_exit

    ; r1不为0则清0
    MOV     r1, #0x00
    STR     r1, [r0]                                 ; 将r1的值存储到rt_thread_switch_interrupt_flag,即清0

    ; 判断rt_interrupt_from_thread的值是否为0
    LDR     r0, =rt_interrupt_from_thread            ; 加载rt_interrupt_from_thread的地址到r0
    LDR     r1, [r0]                                 ; 加载rt_interrupt_from_thread的值到r1
    CBZ     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                                  ; 获取线程栈指针到r1
    STMFD   r1!, {r4 - r11}                          ;将CPU寄存器r4~r11的值存储到r1指向的地址(每操作一次地址将递增一次)
    LDR     r0, [r0]                                 ; 加载r0指向值到r0,即r0=rt_interrupt_from_thread
    STR     r1, [r0]                                 ; 将r1的值存储到r0,即更新线程栈sp
	
; ========================== 下文切换 ==============================
switch_to_thread
    LDR     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,即sp

    LDMFD   r1!, {r4 - r11}                           ;将线程栈指针r1(操作之前先递减)指向的内容加载到CPU寄存器r4~r11
    MSR     psp, r1                                   ;将线程栈指针更新到PSP

pendsv_exit
    ; 恢复中断
    MSR     PRIMASK, r2

    ORR     lr, lr, #0x04                             ; 确保异常返回使用的堆栈指针是PSP,即LR寄存器的位2要为1
    BX      lr                                        ; 异常返回,这个时候任务堆栈中的剩下内容将会自动加载到xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务的形参)
	                                                  ; 同时PSP的值也将更新,即指向任务堆栈的栈顶。在ARMC3中,堆是由高地址向低地址生长的。
    ; PendSV_Handler 子程序结束
	ENDP	
	
	
	ALIGN   4

    END

该函数的操作流程如下:
(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    PROC
    EXPORT rt_hw_context_switch

    ; 设置中断标志位rt_thread_switch_interrupt_flag为1     
    LDR     r2, =rt_thread_switch_interrupt_flag          ; 加载rt_thread_switch_interrupt_flag的地址到r2
    LDR     r3, [r2]                                      ; 加载rt_thread_switch_interrupt_flag的值到r3
    CMP     r3, #1                                        ; r3与1比较,相等则执行BEQ指令,否则不执行
    BEQ     _reswitch
    MOV     r3, #1                                        ; 设置r3的值为1
    STR     r3, [r2]                                      ; 将r3的值存储到rt_thread_switch_interrupt_flag,即置1
    
	; 设置rt_interrupt_from_thread的值
    LDR     r2, =rt_interrupt_from_thread                 ; 加载rt_interrupt_from_thread的地址到r2
    STR     r0, [r2]                                      ; 存储r0的值到rt_interrupt_from_thread,即上一个线程栈指针sp的指针

_reswitch
    ; 设置rt_interrupt_to_thread的值
	LDR     r2, =rt_interrupt_to_thread                   ; 加载rt_interrupt_from_thread的地址到r2
    STR     r1, [r2]                                      ; 存储r1的值到rt_interrupt_from_thread,即下一个线程栈指针sp的指针

    ; 触发PendSV异常,实现上下文切换
	LDR     r0, =NVIC_INT_CTRL              
    LDR     r1, =NVIC_PENDSVSET
    STR     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   PROC
    EXPORT PendSV_Handler

    ; 失能中断,为了保护上下文切换不被中断
    MRS     r2, PRIMASK
    CPSID   I

    ; 获取中断标志位,看看是否为0
    LDR     r0, =rt_thread_switch_interrupt_flag     ; 加载rt_thread_switch_interrupt_flag的地址到r0
    LDR     r1, [r0]                                 ; 加载rt_thread_switch_interrupt_flag的值到r1
    CBZ     r1, pendsv_exit                          ; 判断r1是否为0,为0则跳转到pendsv_exit

    ; r1不为0则清0
    MOV     r1, #0x00
    STR     r1, [r0]                                 ; 将r1的值存储到rt_thread_switch_interrupt_flag,即清0

    ; 判断rt_interrupt_from_thread的值是否为0
    LDR     r0, =rt_interrupt_from_thread            ; 加载rt_interrupt_from_thread的地址到r0
    LDR     r1, [r0]                                 ; 加载rt_interrupt_from_thread的值到r1
    CBZ     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                                  ; 获取线程栈指针到r1
    STMFD   r1!, {r4 - r11}                          ;将CPU寄存器r4~r11的值存储到r1指向的地址(每操作一次地址将递增一次)
    LDR     r0, [r0]                                 ; 加载r0指向值到r0,即r0=rt_interrupt_from_thread
    STR     r1, [r0]                                 ; 将r1的值存储到r0,即更新线程栈sp
	
; ========================== 下文切换 ==============================
switch_to_thread
    LDR     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,即sp

    LDMFD   r1!, {r4 - r11}                           ;将线程栈指针r1(操作之前先递减)指向的内容加载到CPU寄存器r4~r11
    MSR     psp, r1                                   ;将线程栈指针更新到PSP

pendsv_exit
    ; 恢复中断
    MSR     PRIMASK, r2

    ORR     lr, lr, #0x04                             ; 确保异常返回使用的堆栈指针是PSP,即LR寄存器的位2要为1
    BX      lr                                        ; 异常返回,这个时候任务堆栈中的剩下内容将会自动加载到xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务的形参)
	                                                  ; 同时PSP的值也将更新,即指向任务堆栈的栈顶。在ARMC3中,堆是由高地址向低地址生长的。
    ; PendSV_Handler 子程序结束
	ENDP	
	
	
	ALIGN   4

    END

该函数的操作流程如下:
(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.mfbz.cn/a/498049.html

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

相关文章

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

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

Unity TMP 使用教程

文章目录 1 导入资源包2 字体制作3 表情包制作4 TMP 控件4.1 属性4.2 富文本标签 1 导入资源包 “Window -> TextMeshPro -> Import TMP Essential Resources”,导入完成后会创建一个名为"TextMehs Pro"的文件夹,这里面包含所需要的资源…

Maya 2024 for Mac/Win:重塑三维创意世界的利器

在数字化浪潮汹涌的当下,三维图形软件早已成为创意产业不可或缺的重要工具。而在这其中,Maya 2024以其卓越的性能和丰富的功能,赢得了无数设计师的青睐。无论是Mac还是Win平台,Maya 2024都能为您的三维创作提供强大的支持。 Maya…

Docker部署MinIO对象存储服务

1. 拉取MinIO镜像 # 下载镜像 docker pull minio/minio#查看镜像 docker images2. 创建目录 # 文件存储目录 mkdir -p /opt/minio/data# 配置文件 mkdir -p /opt/minio/config# 日志文件 mkdir -p /opt/minio/logs3. 创建Minio容器并运行 docker run \ -p 9000:9000 \ -p 90…

ES学习日记(二)-------集群设置

上一节写了elasticsearch单节点安装和配置,现在说集群,简单地说就是在多台服务器上搭建单节点,在配置文件里面增加多个ip地址即可,过程同单节点部署,主要说集群配置 注意:不建议在之前单节点es上修改配置为集群,据说运行之后会生成很多文件,在单点基础上修改容易出现未知问题,…

zedboard+AD9361 运行 open WiFi

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

最小可行产品需要最小可行架构——可持续架构(三)

前言 最小可行产品(MVP)的概念可以帮助团队专注于尽快交付他们认为对客户最有价值的东西,以便在投入大量时间和资源之前迅速、廉价地评估产品的市场规模。MVP不仅需要考虑产品的市场可行性,还需要考虑其技术可行性,以…

【JavaWeb】Day24.Web入门——HTTP协议(一)

HTTP协议——概述 1.介绍 HTTP:Hyper Text Transfer Protocol(超文本传输协议),规定了浏览器与服务器之间数据传输的规则。 http是互联网上应用最为广泛的一种网络协议http协议要求:浏览器在向服务器发送请求数据时,或是服务器在…

Oracle存数字精度问题number、binary_double、binary_float类型

--表1 score是number(10,5)类型 create table TEST1 (score number(10,5) ); --表2 score是binary_double类型 create table TEST2 (score binary_double ); --表3 score是binary_float类型 create table TEST3 (score binary_float );实验一:分别往三张表插入 小数…

Redis开源协议变更!Garnet:微软开源代替方案?

Garnet:微软开源的高性能替代方案,秉承兼容 RESP 协议的同时,以卓越性能和无缝迁移能力重新定义分布式缓存存储! - 精选真开源,释放新价值。 概览 最近,Redis修改了开源协议,从BSD变成了 SSPLv…

青龙脚本 猫猫看看

话不多说开图 https://raw.githubusercontent.com/Huansheng1/my-qinglong-js/main/%E7%8C%AB%E7%8C%AB%E7%9C%8B%E7%9C%8B.py

探索Python人工智能在气象监测中的创新应用

Python是功能强大、免费、开源,实现面向对象的编程语言,在数据处理、科学计算、数学建模、数据挖掘和数据可视化方面具备优异的性能,这些优势使得Python在气象、海洋、地理、气候、水文和生态等地学领域的科研和工程项目中得到广泛应用。可以…

Jupyter安装教程(Windows 版)

这几年AI人工智能这么火,陆陆续续诞生了很多新的产品,新的商业模式,随着Open-sora 1.0开源之后,让我更加地相信GPT5也即将要到来了,看来不学机器学习和深度学习,恐怕是要跟不上时代了。于是就想着今年开始接…

【管理咨询宝藏59】某大型汽车物流战略咨询报告

本报告首发于公号“管理咨询宝藏”,如需阅读完整版报告内容,请查阅公号“管理咨询宝藏”。 【管理咨询宝藏59】某大型汽车物流战略咨询报告 【格式】PDF 【关键词】HR调研、商业分析、管理咨询 【核心观点】 - 重新评估和调整商业模式,开拓…

智能设备配网保姆级教程

设备配网 简单来说,配网就是将物联网(IoT)设备连接并注册到云端,使其拥有与云端远程通信的能力。配网后,智能设备才能被手机应用或者项目管理后台控制,依托于智能场景创造价值。本文介绍了配网的相关知识&…

【分享】CMMI V3.0版本做了哪些改变?哪些企业适合申请CMMI3.0

​ CMM是由美国卡内基梅隆大学软件工程研究所1987年开发成功的,它基于过去所有软件工程过程改进的成果,吸取了以往软件工程的经验教训,提供了一个基于过程改进的框架;CMMI(Capability Maturity Model Integration能力成熟度模型集…

代码随想录算法训练营第三十六天|435. 无重叠区间,763. 划分字母区间

435. 无重叠区间 题目 给定一个区间的集合 intervals ,其中 intervals[i] [starti, endi] 。返回 需要移除区间的最小数量,使剩余区间互不重叠 。 示例 1: 输入: intervals [[1,2],[2,3],[3,4],[1,3]] 输出: 1 解释: 移除 [1,3] 后,剩下…

文献学习(自备)

收官大作,多组学融合的新套路发NC!! - 知乎 (zhihu.com) Hofbauer cell function in the term placenta associates with adult cardiovascular and depressive outcomes | Nature Communications 病理性胎盘炎症会增加几种成人疾病的风险&a…

Linux——信号的保存与处理

目录 前言 一、信号的常见概念 1.信号递达 2.信号未决 3.信号阻塞 二、Linux中的递达未决阻塞 三、信号集 四、信号集的处理 1.sig相关函数 2.sigprocmask()函数 3.sigpending()函数 五、信号的处理时机 六、信号处理函数 前言 在之前,我们学习了信号…

Codeforces Round 937 (Div. 4) A - F 题解

A. Stair, Peak, or Neither? 题解&#xff1a;直接比较输出即可。 代码&#xff1a; #include<bits/stdc.h> using namespace std ; typedef long long ll ; const int maxn 2e5 7 ; const int mod 1e9 7 ; inline ll read() {ll x 0, f 1 ;char c getchar()…