RT-Thread 内核线程切换原理

2023-05-16

1、背景

        本文章主要说明 rtthread 内核线程是如何切换的,初学者刚从裸机开发接触 RTOS 时难免会有些不适应,明白这部分原理之后就会对 RTOS 有更深的理解。在学习内核线程切换原理之前需要有以下基础知识铺垫。本文以 arm 公司的 Cortex-M3 内核为例。

2、基础知识

  • CM3 拥有通用寄存器 R0-R15 以及一些特殊功能寄存器(中断屏蔽寄存器等等)

  • R0-R12 都是通用寄存器,用来临时存储程序运行时产生的数据

  • R13 这个寄存器存储堆栈指针,在 CM3 内核中一共有两个堆栈指针(MSP、PSP),于是 CM3 支持两个堆栈。在启动文件中定义的那个栈空间属于主栈,还有一个在我们创建线程时的栈属于线程栈。这两个栈空间不是同一个空间。

    主堆栈指针(MSP),这是默认的堆栈指针,在裸机开发中只是用这一个指针,由 OS 内核、中断服务程序以及所有需要特权访问的应用程序代码使用。

    进程堆栈指针(PSP),用于常规的应用程序代码,比如线程。

  • R14 也叫做连接寄存器LR,在调用子程序时存储返回地址

  • R15 也叫做程序计数器 (PC,program counter),因为 CM3 内部使用了指令流水线,PC 中存放的是当前指令的地址+4,也就是下一条指令的地址。

  • 栈空间的定义 : 向下生长的栈。也就是说每次执行一个 push(压栈)命令,栈指针向下减小一个单元,每次执行pop命令,栈指针增加一个单元。如下图所示

3、代码分析

3.1 内核寄存器结构体定义

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;
 };
 struct exception_info
 {
     rt_uint32_t exc_return;
     struct stack_frame stack_frame;
 };

3.2 初始化线程栈

rt_uint8_t *rt_hw_stack_init(void       *tentry,  //线程函数入口地址
                              void       *parameter,//线程函数参数
                              rt_uint8_t *stack_addr,//栈地址
                              void       *texit)//线程退出时的函数地址
 {
     struct stack_frame *stack_frame;
     rt_uint8_t         *stk;
     unsigned long       i;
 ​
     stk  = stack_addr + sizeof(rt_uint32_t);//栈地址 + 4 个字节
     stk  = (rt_uint8_t *)RT_ALIGN_DOWN((rt_uint32_t)stk, 8);//向下8个字节对齐
     stk -= sizeof(struct stack_frame);//偏移16个字(16*4个字节)
 ​
     stack_frame = (struct stack_frame *)stk;//强制转换为 struct stack_frame 类型
 ​
     /* init all register */
     for (i = 0; i < sizeof(struct stack_frame) / sizeof(rt_uint32_t); i ++)
     {
         ((rt_uint32_t *)stack_frame)[i] = 0xdeadbeef;//初始化这16个字的空间为 0xdeadbeef
     }
     /* 初始化高8个字的内存空间 */
     stack_frame->exception_stack_frame.r0  = (unsigned long)parameter; /* r0 : argument */
     stack_frame->exception_stack_frame.r1  = 0;                        /* r1 */
     stack_frame->exception_stack_frame.r2  = 0;                        /* r2 */
     stack_frame->exception_stack_frame.r3  = 0;                        /* r3 */
     stack_frame->exception_stack_frame.r12 = 0;                        /* r12 */
     stack_frame->exception_stack_frame.lr  = (unsigned long)texit;     /* lr */
     stack_frame->exception_stack_frame.pc  = (unsigned long)tentry;    /* entry point, pc */
     stack_frame->exception_stack_frame.psr = 0x01000000L;              /* PSR */
 ​
 #if USE_FPU
     stack_frame->flag = 0;
 #endif /* USE_FPU */
 ​
     /* return task's current stack address */
     return stk;
 }
  • stack_addr 这个参数为当前线程栈的结束地址,也就是最高的地址。为什么是最高地址?原因是上面说过的栈空间的定义。

  • struct stack_frame 这个结构体的定义可不是胡乱定义的,里面是有顺序要求的。

  • stk -= sizeof(struct stack_frame);//偏移16个字(16*4个字节) 为何偏移这么多字节,因为这16个字的空间的每个地址要按照结构体成员变量的地址去存放,即 psr 要放到这个栈的最高地址,r4 在最低的地址。如图所示,此图出自野火。

3.3 执行线程切换

        阅读这段代码之前得知道,cm3 内核执行中断或异常时,r0、r1、r2、r3、r12、lr、pc、psr,这些寄存器是自动压栈的。

 rt_hw_context_switch    PROC
     EXPORT rt_hw_context_switch ;导出函数,此操作能够让C侧代码调用,C侧的第一个参数为当前线程栈sp的指针,第二个
                                 ;为将要执行的线程栈 sp 的指针
     ; set rt_thread_switch_interrupt_flag to 1
     LDR     r2, =rt_thread_switch_interrupt_flag;中断标志位  L2 = &rt_thread_switch_interrupt_flag
     LDR     r3, [r2];r3 = *r2也就是 r3 = rt_thread_switch_interrupt_flag
     CMP     r3, #1  ;判断rt_thread_switch_interrupt_flag 与 1是否相等
     BEQ     _reswitch ;相等跳转 _reswitch,当第2次执行线程切换时,rt_thread_switch_interrupt_flag被pendsv置0
                       ;既然是第二次,所以当前线程具有上文所以要把sp存到rt_interrupt_from_thread,直接跳转_reswitch
                       ;表示的是第一次切换线程,因为没有上文,所以直接跳到 _reswitch
     MOV     r3, #1  ;不等则置1
     STR     r3, [r2] ;rt_thread_switch_interrupt_flag = 1
 ​
     LDR     r2, =rt_interrupt_from_thread   ; set rt_interrupt_from_thread
     STR     r0, [r2]                        ;rt_interrupt_from_thread = r0,&sp,当前线程sp的地址
 ​
 _reswitch
     LDR     r2, =rt_interrupt_to_thread     ; set rt_interrupt_to_thread
     STR     r1, [r2]                        ;rt_interrupt_to_thread = r1,&sp,将要只要的线程的sp的地址
     ;触发 pendsv 中断,线程切换的核心
     LDR     r0, =NVIC_INT_CTRL              ; trigger the PendSV exception (causes context switch)
     LDR     r1, =NVIC_PENDSVSET
     STR     r1, [r0]
     BX      LR
     ENDP
 ​
 ; r0 --> switch from thread stack
 ; r1 --> switch to thread stack
 ; psr, pc, lr, r12, r3, r2, r1, r0 are pushed into [from] stack
 PendSV_Handler   PROC
     EXPORT PendSV_Handler
 ​
     ; 关闭所有中断以保护这一过程不被打断
     MRS     r2, PRIMASK
     CPSID   I
 ​
     ; rt_thread_switch_interrupt_flag 为 1时才继续接下来的操作,为0则跳转 pendsv_exit
     LDR     r0, =rt_thread_switch_interrupt_flag
     LDR     r1, [r0]
     CBZ     r1, pendsv_exit         ; pendsv already handled
 ​
     ; 清楚中断标志位
     MOV     r1, #0x00
     STR     r1, [r0]
     ;判断 rt_interrupt_from_thread 是否为0,即是否是第一次切换线程,是0则跳转至switch_to_thread
     LDR     r0, =rt_interrupt_from_thread
     LDR     r1, [r0]
     CBZ     r1, switch_to_thread    ; skip register save at the first time
 ​
     MRS     r1, psp                 ; 获取当前线程栈指针到r1中
     STMFD   r1!, {r4 - r11}         ; 将r4 - r11寄存器中的值压入当前栈空间中
     LDR     r0, [r0]
     STR     r1, [r0]                ; 把当前线程栈指针记录到 rt_interrupt_from_thread 中,即当前栈指针 sp 中
 ​
 switch_to_thread
     LDR     r1, =rt_interrupt_to_thread;获取将要执行的栈的sp的地址
     LDR     r1, [r1]                
     LDR     r1, [r1]                
 ​
     LDMFD   r1!, {r4 - r11}         ; 从将要执行的栈中弹出这个线程中的寄存器r4-r11
     MSR     psp, r1                 ; 并把要执行的线程的栈指针给到 psp
 ​
 pendsv_exit
     ; 恢复中断
     MSR     PRIMASK, r2
     ;由于cm3 内核发生中断时,堆栈指针使用的是msp,因此退出中断时,确保使用psp指针,实际操作就是对,lr寄存的位3进行置1就控制    ; 退出中断后使用psp中断
     ORR     lr, lr, #0x04
     BX      lr ;退出中断时使用psp指针
     ENDP
  • 通过解读 pendsv 中断代码我们知道,在进入 pendsv 中断前,r0、r1、r2、r3、r12、lr、pc、psr 这些寄存器已经自动压入了当前栈中。

  • 当 pendsv 中断退出时,新的将要执行的线程的中断上下文(r0、r1、r2、r3、r12、lr、pc、ps)会自动的从这个线程栈中弹出,程序计数器 PC 就得到了这个将要执行的线程的pc值,这个线程中用到的其他寄存器的值也从这个新的线程栈中得到了(一部分手动pop,一部分自动pop)。

问题点一:我可以通过这个线程栈指针访问到R0~R15的值吗?

  • 答案是肯定的,因为我们传入的 sp 地址就指向了线程栈地址的偏移16个字处,而内核压栈时,先自动压入 r0、r1、r2、r3、r12、lr、pc、psr 这8个字的空间,按照顺序压,先压psr,然后我们手动压 r4 - r11 ,也是按照顺序压,先压r11。此时这16个字的空间就被填满了,这也是为什么线程栈结构体中的成员变量的顺序不是随便填的(个人理解)。

问题点二:当我进入hard_fault 异常时,我能否获取到当前线程栈指针,从而拿到 pc 指针来判断程序出错的位置?

  • 答案是可以的,rt-thread 已经帮我们重写了 hard_fault 服务程序,其原理请看下回分解.................

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

RT-Thread 内核线程切换原理 的相关文章

  • 商医通项目总结

    一 项目概述 简介 尚医通即为网上预约挂号系统 xff0c 网上预约挂号是近年开展的一项便民就医服务 xff0c 旨在缓解看病难 挂号难的就医难题 网上预约挂号全面提供的预约挂号业务从根本上解决了这一就医难题 随时随地轻松挂号 xff0c
  • docker安装以及遇到的问题

    目录 一 docker安装 二 安装中可能碰到的问题 1 WSL 2 installation is incomplete 一 docker安装 docker官网下载 xff1a Docker Desktop Docker xff0c 下载
  • STM32MP157实验(三)——按键扫描和中断

    文章目录 按键扫描设计需求基础知识硬件设计STM32CubeIDE设计MX设置代码设计 实验结果 按键中断设计需求基础知识硬件设计STM32CubeIDE设计MX设置代码设计 总结 按键扫描 设计需求 通过按键扫描的方式实现 xff0c 按
  • 一篇博客实现嵌入式入门

    文章目录 前言最小系统和C语言最小系统原理图电源电路时钟电路复位电路调试 下载电路 嵌入式C语言基础知识数据类型const用法修饰变量修饰数组修饰指针修饰函数参数 作用域与static用法extern的用法volatile的用法struct
  • 驱动开发(一)——(单片机程序、Linux应用程序与驱动程序分析)

    文章目录 前言157准备工作配置交叉编译链编译内核编译解压glibc 单片机程序应用程序驱动程序三者的关系 前言 学习资料 xff0c 跟的韦东山老师的视频 xff0c 大家可以上百问网下载资料 百问网 我使用的开发板是STM32MP157
  • 驱动开发(五)——驱动设计思想(面向对象/分层/分离)

    文章目录 设计思想面向对象分层分离 实验手册资源驱动程序 led drv cled operations h芯片相关chip gpio cled resource h单板相关 board MP157 led c应用测试程序Makefile
  • 【shell】遇到错误退出set -e|set -u|set -x|shell 退出时执行|捕捉信号trap

    目录 遇到错误退出 简介和使用 set e 的陷阱 1 xff0c 管道命令 2 xff0c grep匹配不到会导致退出 shell退出时执行 接收信号 trap 用途 trap介绍 列出所有信号的数值和名字 trap的注意事项 遇到错误退
  • 我的世界java版官方网站,讲的太透彻了

    简介 基于SpringCloud Hoxton SR1 43 SpringBoot 2 2 4 RELEASE 的 SaaS型微服务脚手架 xff0c 具备用户管理 资源权限管理 网关统一鉴权 Xss防跨站攻击 自动代码生成 多存储系统 分
  • 深入理解java虚拟机pdf,GitHub已标星16k

    专题5 xff1a Java序列化 1 什么是java序列化 xff0c 如何实现java序列化 xff1f 2 保存 持久化 对象及其状态到内存或者磁盘 3 序列化对象以字节数组保持 静态成员不保存 4 序列化用户远程对象传输 5 Ser
  • 成功入职阿里月薪45K,附赠课程+题库

    网站基础知识 xff08 网站架构及其演变过程 43 常见协议和标准 43 DNS的设置 43 Java中Socket的用法 43 HTTP协议 43 详解Servlet 43 Tomcat分析 xff09 俯视 Spring MVC xf
  • 快醒醒吧!springcloud分布式事务面试题

    一 什么是架构和架构本质 在软件行业 xff0c 对于什么是架构 xff0c 都有很多的争论 xff0c 每个人都有自己的理解 此君说的架构和彼君理解的架构未必是一回事 因此我们在讨论架构之前 xff0c 我们先讨论架构的概念定义 xff0
  • 基于java实现直播,太牛了!

    美团技术一面20分钟 晚7点 xff0c 因为想到下周一才面试 xff0c 我刚准备出去打个羽毛球 xff0c 北京的电话就来了 面试官各种抱歉 xff0c 说开会拖延了 1 自我介绍 说了很多遍了 xff0c 很流畅捡重点介绍完 2 问我
  • java银行面试题目及答案,顺利拿到offer

    二 常见的并发问题 1 脏读 一个事务读取了另一个事务未提交的数据 2 不可重复读 一个事务对同一数据的读取结果前后不一致 两次读取中间被其他事务修改了 3 幻读 幻读是指事务读取某个范围的数据时 xff0c 因为其他事务的操作导致前后两次
  • 白嫖党最爱!javasocket服务端向客户端发消息

    一 公务员都不要35岁以上的 xff0c 何况大公司 这让很多人感到惶恐 xff0c 现在职场上有一种现象 xff1a 很多用人单位会在招聘信息上明确标注 xff0c 年龄需在35岁以下 为什么有经验 有人脉的职场中年人会如此遭 嫌弃 呢
  • Mybatis源码解析:21道Java基础面试题及答案

    10 HashMap和HashTable的区别 答案 xff1a Hashtable和HashMap类有三个重要的不同之处 1 第一个不同主要是历史原因 Hashtable是基于陈旧的Dictionary类的 xff0c HashMap是J
  • Mybatis源码解析:Java泛型详解

    注意 xff1a 泛型的类型参数只能是类类型 xff0c 不能是基本属性类型 xff1b 不能对确切的泛型类型使用instanceof操作 如下面的操作是非法的 xff0c 编译时会出错 if ex num instanceof Gener
  • MySQL数据库优化:Java程序员秋招三面蚂蚁金服

    自我介绍 JVM如何加载一个类的过程 xff0c 双亲委派模型中有哪些方法 xff1f HashMap如何实现的 xff1f HashMap和Concurrent HashMap区别 xff0c Concurrent HashMap 线程安
  • 【shell】shell脚本模板

    参考 xff1a cShell脚本模板 运维 64 小兵的博客 CSDN博客 bin bash set e 打开异常退出功能 set x 打开Debug功能 定义变量 source etc profile 避免用contab ansible
  • ROS中自定义头文件、源文件和可执行文件调用

    编写头文件 头文件创建在功能包 include 功能包名路径下 xff0c 示例内容如下 xff1a ifndef haha define haha namespace haha ns class Person public void ru
  • STM32/51单片机进阶技一 裸机编程(多任务处理编程思想与代码风格)

    文章目录 系列文章目录前言一 裸机编程是什么 xff1f 二 使用步骤 1 main c主函数处理2 中断函数处理总结 前言 在单片机编程当中 xff0c 我们难免会用单片机处理1个 xff0c 2个简单的任务 xff0c 但是当任务数量超

随机推荐

  • 用JavaScript写的猜数字游戏

    先输出游戏目的 xff0c 用Math Random找到随机数 xff0c 其取值在0 1之间 xff0c 故乘100 直到输入的数字和随机数a相等时才跳出循环 否则 xff0c 判断输入值如果大于随机数 xff0c 则输出你猜的数偏大哦
  • 使用JS脚本打开多个网页的方法

    01 问题 如每天都需要刷新重复的网页或许数据 有什么解决办法吗 02 解决方案 大家可以移步是你的Sakura的 打开多个相关联的网页 js脚本打开网页方法 03 代码 span class token tag span class to
  • 安装 rotors-gazebo 时 melodic版本遇到的问题

    针对找不到qt gui的问题 Could not find a package configuration file provided by 34 qt gui 34 with any of the following names qt g
  • Promethus(普罗米修斯)安装与配置

    1 普罗米修斯概述 Prometheus 是由go语言 golang 开发 是一套开源的监控 amp 报警 amp 时间序列数 据库的组合 适合监控docker容器 Prometheus是最初在SoundCloud上构建的开源系统监视和警报
  • 对C++中的继承分析和总结

    文章目录 一 继承的概念二 继承的定义1 继承的定义格式2 继承方式和访问限定符 三 基类和派生类的赋值规则1 派生类赋值给基类2 基类赋值给派生类 四 继承中的重定义 隐藏 五 派生类中的默认成员函数1 不写派生类中的默认成员函数2 写派
  • Linux -- 查看进程 top命令 详解

    我们上篇介绍了 xff0c Linux 中的进程等概念 xff0c 那么 xff0c 在Linux 中如何查看进程呢 xff1f xff1f 我们常用到的有两个命令 xff0c PS 和 top 两个命令 xff0c 今天先来介绍下 top
  • 24届春招百度暑假实习笔试第二题

    题干 解答 该题目在解决的时候 xff0c 需要发现就是对于相同的字符我们应该放在一起 xff0c 这样在进行修改的时候 xff0c 对其他字符的影响才会小 然后连续相同字符个数 和 组成的回文子串数目 它们的通解为 an 61 n 2 4
  • ROS学习(八)launch启动文件的使用方法

    前言 使用命令行输入代码需要不断打开终端比较繁琐 xff0c 而且容易输入错误 xff0c 那么有没有什么方法可以快速启动所需节点呢 xff1f 一 launch文件介绍 Launch文件 xff1a 通过XML文件实现多节点的配置和启动
  • 【git】git lfs

    目录 原理 使用方法 报错记录 certificate signed by unknown authority 原理 项目中的大文件会很占空间 git lfs large file storage 将大文件替换为小指针 当真正需要到这些大文
  • gdb调试应用程序记录

    gdb 调试说明 xff1a 判断程序是否为debug版本 xff1a 方法一 xff1a 命令 xff1a gdb a out 注 xff1a 这里的命令是指在Linux终端下面输入的命令 非debug版本 xff0c 会提示 xff1a
  • 3D打印机硬件驱动-马林固件最新版本2.0.X中文注释(3)marlin 2.0.9.2 截至发稿时间2021年12月16日

    Marlin 3D Printer Firmware 头描述详见其他两个文件头描述 Copyright c 2020 MarlinFirmware https github com MarlinFirmware Marlin Based o
  • 字符串结尾‘\0‘

    C语言中字符串的结束标志是 39 0 39 C语言中没有专门的字符串变量 xff0c 通常用一个字符数组来存放一个字符串 xff0c 字符串总是以 39 0 39 作为结束符 39 0 39 就是8位的00000000 xff0c 因为字符
  • 蒙德里安的梦想 状压 DP

    定义 状压 DP 是动态规划的一种 xff0c 通过将状态压缩为整数来达到优化转移的目的 例题 xff1a 蒙德里安的梦想 求把 N M 的棋盘分割成若干个 1 2 的长方形 xff0c 有多少种方案 例如当 N 61 2 xff0c M
  • Hadoop 核心三大件

    一 Hadoop Distributed File System xff0c 简称 HDFS xff0c 是一个分布式文件系统 二 YARN架构 三 MapReduce架构概述 MapReduce将计算过程分为两个阶段 xff1a Map和
  • 努力加油——感想帖

    一直很想写一篇自己的心路历程 xff0c 这篇文章于2022 10 28午编写 2020年7月10日在高考地理考卷上给自己的中学生涯画上了句号 走出考场那一刻 xff0c 心里像是有一块沉甸甸的石头被放了下来 xff0c 但好像自己并不是很
  • 小国王——状压DP

    在 n n 的棋盘上放 k 个国王 xff0c 国王可攻击相邻的 8 个格子 xff0c 求使它们无法互相攻击的方案总数 输入格式 共一行 xff0c 包含两个整数 n 和 k 输出格式 共一行 xff0c 表示方案总数 xff0c 若不能
  • 小国王(目标状态优化版)—— 状态压缩DP

    在 n n 的棋盘上放 k 个国王 xff0c 国王可攻击相邻的 8 个格子 xff0c 求使它们无法互相攻击的方案总数 输入格式 共一行 xff0c 包含两个整数 n 和 k 输出格式 共一行 xff0c 表示方案总数 xff0c 若不能
  • 炮兵阵地——状态压缩DP

    司令部的将军们打算在 N MN M 的网格地图上部署他们的炮兵部队 一个 N MN M 的地图由 NN 行 MM 列组成 xff0c 地图的每一格可能是山地 xff08 用 H 表示 xff09 xff0c 也可能是平原 xff08 用 P
  • hadoop项目实战——奥运会数据分析

    大三学期项目 hadoop MapReduce 奥运会数据分析结果 xff1a 有没需要详细实现方法的小伙伴呀 xff1f 可以在评论区评论一下 如果人多 xff0c 那么后续会详细更新实现方法
  • RT-Thread 内核线程切换原理

    1 背景 本文章主要说明 rtthread 内核线程是如何切换的 xff0c 初学者刚从裸机开发接触 RTOS 时难免会有些不适应 xff0c 明白这部分原理之后就会对 RTOS 有更深的理解 在学习内核线程切换原理之前需要有以下基础知识铺