RT-Thread记录(五、RT-Thread 临界区保护与FreeRTOS的比较)

2023-11-16

本文聊聊临界区,以及RT-Thread对临界区的处理,
通过源码分析一下 RT-Thread 对临界区保护的实现以及与 FreeRTOS 处理的不同。

前言

为什么要聊临界区?

因为在 RT-Thread 中临界区关系到线程的顺序执行,也就是线程同步的问题。

在使用RTOS的时候,多个运行的线程往往都需要访问临界资源,比如一些全局变量,那么如果不进行一定的保护措施,程序运行就可能出现意想不到的结果。

RT-Thread 提供了多种途径来保护临界区,本文主要说明的是:关闭系统调度和禁止中断的方式 。

本 RT-Thread 专栏记录的开发环境:
RT-Thread记录(一、RT-Thread 版本、RT-Thread Studio开发环境 及 配合CubeMX开发快速上手)
RT-Thread记录(二、RT-Thread内核启动流程 — 启动文件和源码分析
RT-Thread 内核篇系列博文链接:
RT-Thread记录(三、RT-Thread 线程操作函数及线程管理与FreeRTOS的比较)
RT-Thread记录(四、RT-Thread 时钟节拍和软件定时器)

一、临界区

经常会听到临界区,临界资源之类的名词,那么什么叫临界区,临界资源?

1.1 什么是临界区

来看看百度百科的解释:
在这里插入图片描述
简单的概括就是图中两句话:

  • 临界资源
    一次仅允许一个进程使用的共享资源
  • 临界区
    每个进程中访问临界资源的那段代码称为 临界区

1.2 RTOS中的临界区

对于我们的多任务的RTOS而言,除了外部中断,自身的多线程和系统调度机制,多个线程可能会对共享资源进行访问,为了保证数据的可靠性和完整性,那么就需要对临界区进行保护,共享资源要互斥的访问(比如全局变量)。

首先是最基础的示例,外部中断!这个不仅在RTOS存在,前后台系统也存在:
在这里插入图片描述
上面的例子中,如果在线程函数中,加入临界区保护,使得线程对临界资源 a 的操作没有结束以前不响应中断,就不会发生问题。

再来看一个线程间对临界资源访问的例子:
在这里插入图片描述
在上图的示例中 (可能delay(1)和时钟节拍一样可能有点问题,可能需要多一点延时,这里意思到了就行,不纠结了= =!),我已经分析了如果没有临界区保护会出现的问题(有问题请指出),实际程序结果可能不会是程序本来想要的结果,这种错误是需要避免的!

本小结以下内容包括后面临界区的保护源码分析是扩展说明,懂与不懂不影响学会使用 RT-Thread 临界区保护,因为涉及的 RTOS的调度原理,PendSV异常等知识,需要一定的基础,这里建议想学习RTOS的小伙伴务必好好看看《Cortex-M3与Cortex-M4权威指南》这个文档。

理解上面示例关系到RTOS的调度原理,上面解释中用到的中断打断线程后现场保存,现场恢复,线程调度。得对RTOS的调度原理有一定的理解,在RTOS中除了外部中断会打断线程的执行,还有Systick中断和一个重要的 PendSV 异常。

PendSV 也称为可悬起的系统调用,它是一种异常,可以像普通的中断一样被挂起,它是专门用来辅助操作系统进行上下文切换的。PendSV 异常会被初始化为最低优先级的异常。每次需要进行上下文切换的时候,会手动触发 PendSV 异常,在 PendSV 异常处理函数中进行上下文切换。

详细理解请参考我另一篇博文:
FreeRTOS记录(三、RTOS任务调度原理解析_Systick、PendSV、SVC)

这里用文中截图稍微解释一下:
在这里插入图片描述在这里插入图片描述

总之,对于RTOS而言,在访问临界资源的时候,需要特别注意,做好临界区的保护。

为了避免出现上面我们所说的问题,RTOS对临界区采取了一些对应的保护方法,一般来说有:
关闭系统调度,关中断,利用信号量,互斥量。

RT-Thread 信号量,互斥量我们会在下篇博文来说明,本文主要来了解下关闭中断和系统调度的操作。

二、RT-Thread临界区保护

2.1 禁止调度

RT-Thread 调度器上锁 和 调度器解锁的函数如下:

void rt_enter_critical(void);//调度器上锁,进入调度临界区,不再切换线程
void rt_exit_critical(void);//调度器解锁,退出调度临界区

注意,调度锁不会阻止系统的响应中断,只不过是中断处理完成退出后,继续执行被锁住的线程。如果中断中有访问临界资源的情况,此方式不适用!!

调度器上锁和调度器解锁函数,是成对使用的,切记!

使用示例:
在这里插入图片描述

禁止调度源码简析

我们找到rt_enter_critical函数,看看是如何实现的:
在这里插入图片描述

但是上面的函数只对rt_scheduler_lock_nest变量进行了自增,并没有别的操作,那么这个变量是如何影响调度器的呢?
我们查到使用到变量rt_scheduler_lock_nest的地方,找到如下代码:
在这里插入图片描述

那么同样的,在rt_exit_critical函数中,当然就是变量自减了:
在这里插入图片描述

仔细看了这段代码还能发现一个细节,就是这个关闭调度和打开调度是支持嵌套的! 调度器上锁一次,就要解锁一次,上锁2次,就得解锁2次。

通过这个也告诉我们,有些时候多看看源码,会比直接看说明对逻辑的理解更直观!

2.2 屏蔽中断

RTOS所有的线程调度都是建立在中断基础上的,关闭中断,不仅可以屏蔽,外部中断,也可以禁止调度,他比上面的禁止调度“更能够保护”临界区。

RT-Thread 屏蔽中断 和 使能中断的函数如下:

/*
返回值:
中断状态 	rt_hw_interrupt_disable 函数运行前的中断状态
*/
rt_base_t rt_hw_interrupt_disable(void);//屏蔽中断
/*
参数:
level 	前一次 rt_hw_interrupt_disable 返回的中断状态
*/
void rt_hw_interrupt_enable(rt_base_t level);//中断使能

注意,上面的终端所中断锁是最强大的和最高效的同步方法,这个方法最主要的问题在于,中断响应延时会拉长,对于实时性特别极端的场合需要注意,所以实际使用要根据应用场合,合理的使用。

中断屏蔽和中断使能函数也是是成对使用的,切记!

使用示例:
在这里插入图片描述

中断锁源码简析

上面的函数找到申明,但是跳转不到函数原型:
在这里插入图片描述
那么函数的实现在什么地方呢?如下图:
在这里插入图片描述

因为使用的是 gcc 编译器,所以context_gcc.S文件中的函数体前后语句会与 MDK下有一定的区别,但函数实现的汇编语言都是一样的:

/*
 * rt_base_t rt_hw_interrupt_disable();
 */
 /*
 .global关键字用来让一个符号对链接器可见,可以供其他链接对象模块使用
 前面两句意思就类似于定义了一个全局可调用的函数rt_hw_interrupt_disable 
 */
    .global rt_hw_interrupt_disable //告诉编译器rt_hw_interrupt_disable 是一个全局可见的
    .type rt_hw_interrupt_disable, %function//告诉编译器rt_hw_interrupt_disable是一个函数
rt_hw_interrupt_disable:
    MRS     R0, PRIMASK   //读取PRIMASK寄存器的值到r0寄存器
    CPSID   I             //关闭全局中断,具体原因见博文后续说明
    BX      LR       	//函数返回,通过LR 连接寄存器 返回

/*
 * void rt_hw_interrupt_enable(rt_base_t level);
 */
    .global rt_hw_interrupt_enable   //与上面类似
    .type rt_hw_interrupt_enable, %function
rt_hw_interrupt_enable:
    MSR     PRIMASK, R0  //将 r0 的值寄存器写入到 PRIMASK 寄存器
    BX      LR   		//函数返回,通过LR 连接寄存器 返回

即便上面的代码我写了注释,告诉了意思,但是还是会有问题,为什么 CPSID I就是关闭全局中断?

如果好好看了《Cortex-M3与Cortex-M4权威指南》这个文档,所有东西都能明白了。

PRIMSK:中断屏蔽特殊寄存器。利用 PRIMSK,可以禁止除HardFault 和 NMI外的所有异常。在上面推荐文档中有说明:
在这里插入图片描述
CPSID I就是禁止中断,CPSIE I就是使能中断。

一个细节,为什么 rt_hw_interrupt_enable 函数,不用 CPSIE I恢复中断?

答案就是,如果使用CPSIE I使能中断,那么中断锁就无法嵌套。使用R0寄存器将当前的PRIMASK的状态保存起来,这样子就必须要关多少次中断就得开多少次中断。
在这里插入图片描述

另外值得一说的是, 在上面的示例中R0寄存器中保存的值,就是 rt_base_t level这个变量!

通过上述分析,我们应该完全明白了,RT-Thread 的中断锁是如何实现的,那么其他的RTOS是不是都是这个样子呢? 我们来看看 FreeRTOS 对于中断锁是如何实现的。

与FreeRTOS区别

FreeRTOS的临界区,在我的博文介绍过:

FreeRTOS记录(四、FreeRTOS任务堆栈溢出问题和临界区)

这里我们就只看一下他的实现代码来和 RT-Thread 比较一下(同样是以M3为例,M0与M3又是不同的):
在这里插入图片描述
这里我们分析就用在任务中屏蔽中断的函数来分析,在中断中屏蔽分析类似,只不过稍微复杂一点。

屏蔽中断:

#define portDISABLE_INTERRUPTS()				vPortRaiseBASEPRI()
/*----------------------------------------------*/
/*只需要注意操作的寄存器为 basepri*/
/*----------------------------------------------*/

portFORCE_INLINE static void vPortRaiseBASEPRI( void )
{
uint32_t ulNewBASEPRI;

	__asm volatile
	(
		"	mov %0, %1												\n" \
		"	msr basepri, %0											\n" \  
		"	isb														\n" \
		"	dsb														\n" \
		:"=r" (ulNewBASEPRI) : "i" ( configMAX_SYSCALL_INTERRUPT_PRIORITY ) : "memory"
	);
}

使能中断:

//...
#define portENABLE_INTERRUPTS()					vPortSetBASEPRI(0)
/*只需要注意操作的寄存器为 basepri*/
portFORCE_INLINE static void vPortSetBASEPRI( uint32_t ulNewMaskValue )
{
	__asm volatile
	(
		"	msr basepri, %0	" :: "r" ( ulNewMaskValue ) : "memory"
	);
}

这里我们通过 FreeRTOS 中断锁的代码可以看出,它操作的是basepri寄存器,而不是PRIMSK寄存器,那么basepri寄存器又是什么呢? 答案还是从《Cortex-M3与Cortex-M4权威指南》文档中可以找到:
在这里插入图片描述
FreeRTOS 在中断锁的操作上面,是利用 basepri 寄存器屏蔽特定优先级的中断。 这个优先级的设置是用户可以自行设置的。这给非常紧急的中断留了一条后路。

但是不管怎样,在任何时候,临界区处理的代码当然是时间越短越好!!

2.3 实际应用场合

简单总结一下,临界区的保护实际应用中可能需要的场合:

  • 调用公共函数的代码(不可重入函数)
  • 读取或者修改变量(全局变量)
  • 使用硬件资源(在操作内存或者flash的时候)
  • 对时序有精准要求的操作(I2C通讯,但是得注意在通讯中不能使用利用了systick的延时函数,用干等的延时)
  • 某些用户不想被打断的代码(比如 printf 打印)

在一般的场合,普通临界区的保护使用禁止调度的方式就可以满足需求了,除非你中断中有对临界资源的访问。
当然事无绝对,有些时候中断的发生对某些普通任务(比如ADC采样)也可能产品影响,所以还是需要根据实际情况,合理的使用 临界区保护。

结语

本文的内容从学会 RT-Thread 临界区保护的使用来说是比较简单,只需要掌握几个函数的调用就可以。但对于了解实现原理来说相对复杂些,需要对内核,对操作系统基本原理有一定的理解。

我们通过对这几个函数源码的简单分析,让我们对其原理的实现有了更直观的理解,养成看源码是对我们学习有帮助的一个好习惯!

下一篇 RT-Thread 记录,就要来学习 RT-Thread 的线程间同步相关的信号量,互斥量,这也是 RT-Thread 对临界区的另一种保护方式。

谢谢!

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

RT-Thread记录(五、RT-Thread 临界区保护与FreeRTOS的比较) 的相关文章

  • LSTM对比Bi-LSTM的电力负荷时间序列预测(Matlab)

    欢迎来到本博客 博主优势 博客内容尽量做到思维缜密 逻辑清晰 为了方便读者 座右铭 行百里者 半于九十 本文目录如下 目录 0 概述 1 电力负荷预测 2 滑动窗输入结构的构建 3 LSTM 4 Bi LSTM 5 运行结果 6 Matla
  • jQuery MiniUI 开发教程 树形控件 树形:懒加载树(五)

    b 懒加载树 b img http www miniui com docs api images lazytree gif img 参考示例 url http www miniui com demo tree lazytree html 懒
  • C++:读写INI文件

    C 读写INI文件 INI文件是一种常见的配置文件格式 用于存储应用程序的配置信息 在C 中 我们可以使用一些库来读取和写入INI文件 在本文中 我将向您展示如何使用C 读取和写入INI文件 读取INI文件 对于INI文件的读取 我们可以使

随机推荐

  • Spring Data JPA 多数据源的使用

    p 项目中使用多个数据源在以往工作中比较常见 微服务架构中不建议一个项目使用多个数据源 在微服务架构下 一个微服务拥有自己独立的一个数据库 如果此微服务要使用其他数据库的数据 需要调用对应库的微服务接口来调用 而不是在一个项目中连接使用多个
  • U盘安装redhat 7.4的最终解决方案

    U盘安装redhat 7 4的最终解决方案 终于将redhat 7 4装上x3650 M5服务器了 过程无比艰辛 因为与CentOS7有一定区别 与redhat6 8完全不同 遇到的问题有 A 刻录镜像的时候只能刻录一个4MB EFI文件夹
  • 详解TCP为什么不能是两次握手

    三次握手的过程 注意不要遗漏全双工下两缓存 读 写缓存 的分配和变量的分配 CLOSED 表示初始状态 LISTEN 该状态表示服务器端的某个SOCKET处于监听状态 可以接受连接 SYN SENT 这个状态与SYN RCVD遥相呼应 当客
  • 【golang】for range中取地址操作的陷阱

    Tips for range创建了每个元素的副本 而不是直接返回每个元素的引用 例子1 package main import fmt func main slice int 0 1 2 3 myMap make map int int f
  • ZooKeeper之Java客户端API使用—创建节点。

    客户端可以通过ZooKeeper的API来创建一个数据节点 有如下两个接口 String create final String path byte data List
  • LeetCode-1604. 警告一小时内使用相同员工卡大于等于三次的人【哈希表,排序,数组】

    LeetCode 1604 警告一小时内使用相同员工卡大于等于三次的人 哈希表 排序 数组 题目描述 解题思路一 时间转换成分钟数 直接解决跨天问题 用哈希表记录每个员工的名字以及对应的时间 然后遍历哈希表 对于每个员工 我们将该员工的所有
  • 跟我学Java设计模式第7天:行为型设计模式

    Java设计模式文章目录 跟我学Java设计模式第一天 设计模式概述和软件设计原则 跟我学Java设计模式第二天 简单工厂模式 工厂方法模式 抽象工厂模式 跟我学Java设计模式第三天 代理模式 适配器 装饰者等其中模式结构 跟我学Java
  • Qt安装(Windows平台)

    Qt 的安装组件分为两部分 一部分是 Qt 5 9 分类下的 该分类包含的是真正的 Qt 开发库组件 另一部分是 Tools 分类下的 该分类包含的是集成开发环境和编译工具 Qt 5 9 分类下的开发组件 组件 说明 MinGW 5 3 0
  • 【elementplus】解决el-table设置固定高度后,横向滚动条消失的问题

    消失的原因 因为我给el scrollbar bar设置了position static 覆盖了它原本的样式position absolute 解决 把position static删掉
  • hbase region 分配方式

    参与 Region 分配的重要对象 在 Region 分配过程当中 起着重要做用有以下一些对象 安全 HMaster 是 HBase 中的 Master server 仅有一个 HRegionServer 负责多个 HRegion 使之能向
  • Java调用ffmpeg进行视频.H264抽帧,并保存为图片

    Java调用ffmpeg进行视频 H264抽帧 并保存为图片 1 需求 2 解决 3 源码 参考 1 需求 对视频 D data 01 test H264进行抽帧并保存为图片 图片命名为1 jpg 2 jpg 图片保存在D data 01
  • 【华为OD机试】找朋友(C++ Python Java)2023 B卷

    时间限制 C C 1秒 其他语言 2秒 空间限制 C C 262144K 其他语言524288K 64bit IO Format lld 题目描述 在学校中 N个小朋友站成一队 第i个小朋友的身高为height i 第i个小朋友可以看到的第
  • 【第六期】人工智能工程师培养计划招生

    前 言 学院第四期课程在2019年10月结课 第一至第四期具有求职意向的同学中 目前已经有80 的同学拿到了国内外名企的AI算法岗位offer 或者国外名校的AI 硕士 全奖博士录取 offer 在大家的认可下 我们开始了第六期的课程 在本
  • (九)kaldi thchs30 三音子模型(line 71-76)

    概览 首先放代码 triphone steps train deltas sh boost silence 1 25 cmd train cmd 2000 10000 data mfcc train data lang exp mono a
  • netty http文件服务器,Netty充当Http服务器简单示例

    Netty的应用场景 RPC 通信框架 长连接服务器 Http服务器 充当Http服务器实例 用户向服务器发送请求 服务器返回给用户Hello World 先介绍几个基础的概念 Channel 通道 相当于一个连接 ChannelHandl
  • Linux TC(Traffic Control) 简介(一)

    众所周知 在互联网诞生之初都是各个高校和科研机构相互通讯 并没有网络流量控制方面的考虑和设计 IP协议的原则是尽可能好地为所有数据流服务 不同的数据流之间是平等的 然而多年的实践表明 这种原则并不是最理想的 有些数据流应该得到特别的照顾 比
  • 关于left join优化not in 导致的长时间查询无数据问题 Using where; Using join buffer (Block Nested Loop)

    一 背景 线上存在业务 需要每天定时整理某个表A未处理的数据 并写入另外一张表B 每天查询出不存在B表中且未处理过的A表数据 A表中的数据主键放入B表中 未设定B表对应索引 数据量初始值大概在几千条 根据网上书籍介绍及多数网友介绍 left
  • 使用LSTM训练分类模型(kreas+tensorflow)

    github链接 https github com fangxiaozhu Classification model 需求描述 基于构建简单的多分类模型需求 使用的是keras深度学习库实现的 实现代码简单可用 模型优点是训练速度快 准确率
  • Latex 报错! Undefined control sequence.

    1 没有添加相应的宏包 2 编译命令不正确 比如应该使用XeLaTex命令却使用了XeTex 3 如果不是上述原因 可以删除 aux文件重新编译 只要代码没问题 命令没问题 突然报错 都可以试试这个方法 亲测有效 XeTeX程序中的命令 用
  • RT-Thread记录(五、RT-Thread 临界区保护与FreeRTOS的比较)

    本文聊聊临界区 以及RT Thread对临界区的处理 通过源码分析一下 RT Thread 对临界区保护的实现以及与 FreeRTOS 处理的不同 目录 前言 一 临界区 1 1 什么是临界区 1 2 RTOS中的临界区 二 RT Thre