FreeRTOS如何解决访问冲突/线程不安全(临界段、互斥锁、挂起调度、看门人任务)

news2024/11/26 21:22:45

在多任务(多线程)系统中,存在一个隐患,那就是多线程的访问(在FreeRTOS中就是任务)。当一个任务A开始访问一个资源(外设、一块内存等),但是A还没有完成访问,B任务运行了,也开始访问,这就会造成数据破坏、错误等问题。

例如:

两个任务试图写入一个液晶显示器(LCD)。

1任务A执行并开始向LCD写入字符串“Hello world”。

2. 任务A在输出字符串“Hello w”后被任务B抢占。

3.任务B在进入阻塞态前向LCD写入“Abort, Retry, Fail?”

4. 任务A继续从它被抢占的点开始,并完成输出它的字符串“world”的剩余字符。

LCD现在显示字符串是“Hello wAbort, Retry, Fail? world”。这显然不是我们想要的结果。

原文链接:FreeRTOS全解析-8.解决访问冲突/线程不安全(临界段、挂起调度、互斥锁、看门人任务)

目录

1.一些概念

1.1原子和非原子操作

1.2可重入函数

1.3互斥

2.临界段和挂起调度器

2.1临界段

2.2挂起(锁住)调度器

3.互斥锁(和二进制信号量)

3.1优先级翻转

3.2优先级继承

3.3死锁

3.4递归互斥锁

4.看门人任务(Gatekeeper Tasks)


1.一些概念

1.1原子和非原子操作

读、修改、写操作

对一个变量PORTA或上0x01,C语言写法:

PORTA |= 0x01;

通过编译转成汇编后:

LOAD R1,[#PORTA] ; Read a value from PORTA into R1MOVE R2,#0x01 ; Move the absolute constant 1 into R2OR R1,R2 ; Bitwise OR R1 (PORTA) with R2 (constant 1)STORE R1,[#PORTA] ; Store the new value back to PORTA

第1句,从PORTA的地址读取数据,保存到R1;(读操作)

第2句,把0x01保存到R2;(读操作)

第3句,R1和R2进行或操作,并存入R1;(修改操作)

第4句,把R1的值保存到PORTA的地址去。(写操作)

这就叫非原子操作,因为他使用了超过一条的汇编指令,并且可以被中断(相反,只用到一条指令的,无法中断的称作原子操作)。更新一个结构体的多个成员,或者更新一个大于CPU结构的字长(例如,在16位机器上更新一个32位变量)的变量,都是非原子操作的例子。如果中断,可能会导致数据丢失或损坏。

考虑以下场景:

1任务A将PORTA的值加载到寄存器中(操作的读部分)。

2. 任务A在完成修改和写入部分之前被任务B抢占。

3.任务B更新PORTA的值,然后进入阻塞态。

4. 任务A继续从它被抢占的点开始。它修改已经保存在寄存器中的PORTA值,然后写入PORTA的地址。

在这个场景中,任务A用到的PORTA的值相当于已经过期了(因为任务B对PORTA进行了修改),这个问题也被叫做数据不一致

1.2可重入函数

如果一个函数可以从多个任务调用,或者从任务和中断调用是安全的,那么这个函数就是“可重入的”。可重入函数被称为“线程安全的”,因为它们可以从多个线程访问,而不会有数据或逻辑操作损坏的风险。

每个任务维护自己的堆栈和自己的处理器(硬件)寄存器集。如果函数不访问存储在堆栈上或保存在寄存器中的数据以外的任何数据,那么函数是可重入的,并且是线程安全的。

如下,这就是可重入的函数,因为,lVar1是通过栈或者寄存器传递的,lVar2是在任务自己的栈中。每个任务访问这段代码时lVar1和lVar2都是不同的地址。

long lAddOneHundred( long lVar1 ){  long lVar2;  lVar2 = lVar1 + 100;  return lVar2;}

如下,这是不可重入的,lVar1是全局变量,lState用了static修饰,保存在数据段上。每个去访问的任务访问到的lVar1和lState都是同一份。

long lVar1;long lNonsenseFunction( void ){  static long lState = 0;  long lReturn;  switch( lState )  {    case 0 : lReturn = lVar1 + 10;      lState = 1;      break;    case 1 : lReturn = lVar1 + 20;      lState = 0;      break;  }}

1.3互斥

为了确保在任何时候都保持数据一致性,必须使用“互斥”来管理任务之间或任务和中断之间共享的资源。技术。目标是确保一旦任务开始访问非可重入且非线程安全的共享资源,同一任务对资源具有独占访问权,直到资源返回到一致状态。

FreeRTOS提供了几个可用于实现互斥的特性,但是最好的互斥方法是(在可能的情况下,因为通常不实用)将应用程序设计成不共享资源的方式,并且每个资源只能从单个任务访问。

2.临界段和挂起调度器

2.1临界段

临界段是分别被调用宏taskENTER_CRITICAL()和taskEXIT_CRITICAL()所包围的代码区域。临界段也称为临界区。

taskENTER_CRITICAL();PORTA |= 0x01;taskEXIT_CRITICAL();

回到写LCD冲突的例子,就可以这样:

void vPrintStringToLCD( const char *pcString ){  taskENTER_CRITICAL();  LCD_printf( "%s", pcString );  fflush( stdout );  taskEXIT_CRITICAL();}

用临界段实现互斥是非常粗糙的方法。它通过完全禁用中断来工作,或者达到configMAX_SYSCALL_INTERRUPT_PRIORITY设置的中断优先级(设置的最高优先级)。

抢占式上下文切换(任务调度)只能在中断内部发生,因此,只要中断保持禁用状态,调用taskENTER_CRITICAL()的任务就保证保持在运行状态,直到临界段退出。

临界段代码必须保持非常短,否则会对中断响应时间产生不利影响。每个对taskENTER_CRITICAL()的调用必须与对taskEXIT_CRITICAL()的调用紧密配对。假如写LCD或者输出会比较慢,就不应该用临界段。

临界段嵌套是安全的,因为内核会记录嵌套深度的计数。只有当嵌套深度返回到零时,临界段才会退出。

调用taskENTER_CRITICAL()和taskEXIT_CRITICAL()是任务改变正在运行FreeRTOS的处理器的中断启用状态的唯一合法方法。通过任何其他方式改变中断启用状态将使宏的嵌套计数失效。

taskENTER_CRITICAL()和taskEXIT_CRITICAL()不以'FromISR'结尾,因此不能从中断服务例程中调用。taskENTER_CRITICAL_FROM_ISR()是taskENTER_CRITICAL()的中断安全版本,taskEXIT_CRITICAL_FROM_ISR()是taskEXIT_CRITICAL()的中断安全版本。中断安全版本只对允许中断嵌套的处理器生效。用法:

void vAnInterruptServiceRoutine( void ){  UBaseType_t uxSavedInterruptStatus;  uxSavedInterruptStatus = taskENTER_CRITICAL_FROM_ISR();  taskEXIT_CRITICAL_FROM_ISR( uxSavedInterruptStatus );}

2.2挂起(锁住)调度器

还可以通过挂起调度程序来创建临界区。挂起调度器有时也称为“锁定”调度器。临界段保护代码区域不被其他任务中断访问。通过挂起调度器实现的临界区只保护代码区域不被其他任务访问,因为中断仍然是启用的。

如果临界段太长,不能通过简单地禁用中断来实现,则可以通过挂起调度器来实现。然而,恢复(或“取消挂起”)调度器比较慢,因此必须考虑在每种情况下使用哪种方法是最好的。

调度器通过调用vTaskSuspendAll()来挂起。挂起调度程序可以防止发生上下文切换,但会启用中断。如果在调度器挂起时,有切换任务的请求,则该请求将保持挂起状态,并且仅在调度器恢复(未挂起)时执行。当调度程序挂起时,不能调用FreeRTOS API函数。

void vTaskSuspendAll( void )BaseType_t xTaskResumeAll( void );

嵌套调用vTaskSuspendAll()和xTaskResumeAll()是安全的,因为内核保留了嵌套深度的计数。只有当嵌套深度返回0时,调度器才会恢复。

3.互斥锁(和二进制信号量)

不懂信号量的可以看一下这篇FreeRTOS全解析-8.信号量(semaphore)

互斥锁(或者叫互斥量,我用Linux比较多,习惯叫锁,FreeRTOS中叫量比较合适)是一种特殊类型的二进制信号量,用于控制对两个或多个任务之间共享的资源的访问。"Mutex"(互斥锁)这个词起源于"Mutual Exclusion"。(互斥)

FreeRTOSConfig.h中的configUSE_MUTEXES必须设置为1,才能使互斥锁。

互斥锁在需要互斥的场景中使用时,可以将其视为与共享资源相关联的令牌。对于要合法访问资源的任务,它必须首先成功地“获取”令牌(成为令牌持有者)。当令牌持有者使用完资源后,它必须“归还”令牌。只有当令牌已经归还时,另一个任务才能成功获取令牌,然后安全地访问相同的共享资源。除非任务持有令牌,否则不允许访问共享资源。

尽管互斥锁和二进制信号量很像,但还是不一样。主要的区别是信号量被获取后会发生什么:用于互斥的信号量必须始终返还(take后要give)。用于同步的信号量通常被丢弃而不返还(take后不用give)。还有一个区别是互斥锁有优先级继承(本文后面讲)。

互斥锁就像这样使用:获取和释放函数和信号量用的是一样的。

static void prvNewPrintString( const char *pcString ){  xSemaphoreTake( xMutex, portMAX_DELAY );  printf( "%s", pcString );  fflush( stdout );  xSemaphoreGive( xMutex );}

使用前要创建,调用函数:

SemaphoreHandle_t xSemaphoreCreateMutex( void );

比如:

SemaphoreHandle_t xMutex; xMutex = xSemaphoreCreateMutex();

一个完整的使用互斥锁的例子:

static void prvNewPrintString( const char *pcString ){  xSemaphoreTake( xMutex, portMAX_DELAY );  printf( "%s", pcString );  fflush( stdout );  xSemaphoreGive( xMutex );}static void prvPrintTask( void *pvParameters ){  char *pcStringToPrint;  const TickType_t xMaxBlockTimeTicks = 0x20;  pcStringToPrint = ( char * ) pvParameters;  for( ;; )  {    prvNewPrintString( pcStringToPrint );    vTaskDelay( ( rand() % xMaxBlockTimeTicks ) );  }}int main( void ){  xMutex = xSemaphoreCreateMutex();  if( xMutex != NULL )  {    xTaskCreate( prvPrintTask, "Print1", 1000,    "Task 1 ***************************************\r\n", 1, NULL );    xTaskCreate( prvPrintTask, "Print2", 1000,    "Task 2 ---------------------------------------\r\n", 2, NULL );    vTaskStartScheduler();  }  for( ;; );}

3.1优先级翻转

先来看看上面的例子会发生什么

Task1优先级为1,Task2优先级为2.

Task1先运行,获得互斥锁,Task2优先级虽然高,但是因为没有获得互斥锁,进入阻塞态,只有等Task1释放了互斥锁,才有机会运行。

这表现出使用互斥锁来提供互斥的一个潜在缺陷

高优先级Task 2必须等待低优先级Task 1放弃对互斥锁的控制。高优先级任务被低优先级任务以这种方式延迟称为“优先级反转”

在这种情况下会加剧:

如图有三个任务LP低优先级任务,MP中等优先级任务,HP高优先级任务。

LP运行,获得互斥锁,HP尝试抢占,但是因为没有获得互斥锁,只能进入阻塞,LP继续运行,但是LP运行过程中,被不需要互斥锁的MP给抢占了。

LP不运行,就无法释放互斥锁,不释放,HP就永远无法运行。结果就变成了,最高优先级的任务在等最低优先级的任务。

优先级反转可能是一个重大问题,但在小型嵌入式系统中,通过考虑如何访问资源,通常可以在系统设计时避免它。

3.2优先级继承

FreeRTOS互斥量和二进制信号量的区别还在于互斥量有“优先级继承”机制,而二进制信号量没有。优先级继承是一种使优先级反转负面影响最小化的方案。它不会“修复”优先级反转,而只是通过确保反转总是有时间限制来减少其影响。然而,优先级继承使系统定时分析复杂化,如果说是依靠它来使系统正常运行,那不太可取。

优先级继承是通过临时将互斥锁持有者的优先级提高到试图获得相同互斥锁的最高优先级任务的优先级来实现的。持有互斥锁的低优先级任务“继承”了等待互斥锁的任务的优先级。互斥锁持有者的优先级在返回互斥锁时自动重置为其原始值。

有了这个机制,前面说到情况就会变成这样:

LP运行,获得互斥锁,HP尝试运行,但是因为没有互斥锁,进入阻塞态,同时因为HP优先级高,LP继承了HP的优先级,不再会被MP抢占。HP就可以在LP释放互斥锁的时候运行了。

正因为优先级继承功能会影响使用互斥锁的任务的优先级。所以不能在中断服务例程中使用互斥锁。

3.3死锁

“死锁”是使互斥锁进行互斥的另一个潜在陷阱。

当两个任务都在等待由另一个任务持有的资源时,就会发生死锁。考虑下面的场景,任务A和任务B都需要获得互斥量X和Y来执行一个操作:

1任务A执行并成功获取互斥量X。

2. 任务A被任务B抢占。

3.任务B在尝试使用互斥量X之前成功地使用了互斥量Y,但互斥量X由任务A持有,因此任务B无法使用。任务B选择进入阻塞状态,等待互斥量X释放。

4. 任务A继续执行。它尝试获取互斥量Y,但互斥量Y由任务B持有,因此任务A无法使用。任务A选择进入阻塞状态,等待互斥量Y释放。

任务A阻塞等待互斥量X,任务B阻塞等待互斥量Y,等待的互斥量都在对方手里,而又都在阻塞态,运行不了,就这么一直等下去,就是死锁。

与优先级反转一样,避免死锁的最佳方法是在设计时充分考虑这个问题,设计系统以确保不会发生死锁。

实际上,死锁在小型嵌入式系统中并不是一个大问题,因为系统设计人员可以很好地理解整个应用程序,因此可以识别并删除可能发生死锁的区域。

3.4递归互斥锁

任务本身也有可能死锁。如果一个任务多次尝试使用同一个互斥锁,而没有首先返回互斥锁,就会发生这种情况。考虑以下场景:

1. 任务成功获取互斥锁A。

2. 当持有互斥锁A时,任务调用一个库函数。

3.库函数里面尝试使用相同的互斥锁A,然后进入阻塞状态,等待互斥锁A。

在这个场景的最后,任务处于阻塞状态,等待互斥锁返回,但该任务已经是互斥锁的持有者。发生死锁是因为任务处于等待自身的阻塞态,就是我等我自己。

这种类型的死锁可以通过使用递归互斥锁来代替标准互斥锁来避免。一个任务可以多次获取(take)同一个互斥锁,不过要记得take几次就要give几次。

创建:

xSemaphoreCreateRecursiveMutex().

获取take变成了taken

xSemaphoreTakeRecursive().

释放give变成了given

xSemaphoreGiveRecursive()

4.看门人任务(Gatekeeper Tasks)

看门人任务提供了一种干净的实现互斥的方法,没有优先级反转或死锁的风险。

看门人任务是对资源拥有唯一所有权的任务。只有看门人任务被允许直接访问资源——任何其他需要访问资源的任务只能通过使用看门人的服务间接访问资源。

如下面例子,思路挺简单的,任务是要打印,输出就是资源,任务不能直接打印,必需通过队列发送到看门人任务,看门人任务进行打印操作。

static void prvStdioGatekeeperTask( void *pvParameters ){  char *pcMessageToPrint;  for( ;; )  {    xQueueReceive( xPrintQueue, &pcMessageToPrint, portMAX_DELAY );    printf( "%s", pcMessageToPrint );    fflush( stdout );  }}static void prvPrintTask( void *pvParameters ){  int iIndexToString;  const TickType_t xMaxBlockTimeTicks = 0x20;  iIndexToString = ( int ) pvParameters;  for( ;; )  {    xQueueSendToBack( xPrintQueue, &( pcStringsToPrint[ iIndexToString ] ), 0 );    vTaskDelay( ( rand() % xMaxBlockTimeTicks ) );  }}static char *pcStringsToPrint[] ={  "Task 1 ****************************************************\r\n",  "Task 2 ----------------------------------------------------\r\n",  "Message printed from the tick hook interrupt ##############\r\n"};QueueHandle_t xPrintQueue;int main( void ){  xPrintQueue = xQueueCreate( 5, sizeof( char * ) );  if( xPrintQueue != NULL )  {    xTaskCreate( prvPrintTask, "Print1", 1000, ( void * ) 0, 1, NULL );    xTaskCreate( prvPrintTask, "Print2", 1000, ( void * ) 1, 2, NULL );    xTaskCreate( prvStdioGatekeeperTask, "Gatekeeper", 1000, NULL, 0, NULL );    vTaskStartScheduler();  }  for( ;; );}

往期精彩:

STM32F4+FreeRTOS+LVGL实现快速开发(缝合怪)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/429152.html

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

相关文章

精通 TensorFlow 2.x 计算机视觉:第二部分

原文:Mastering Computer Vision with TensorFlow 2.x 协议:CC BY-NC-SA 4.0 译者:飞龙 本文来自【ApacheCN 深度学习 译文集】,采用译后编辑(MTPE)流程来尽可能提升效率。 不要担心自己的形象,…

【RabbitMQ学习日记】—— 再见RabbitMQ

一、发布确认高级篇 在生产环境中由于一些不明原因,导致 rabbitmq 重启,在 RabbitMQ 重启期间生产者消息投递失败,导致消息丢失,需要手动处理和恢复如何才能进行 RabbitMQ 的消息可靠投递呢? 特别是在这样比较极端的情…

MYSQL:数据类型与运算符、MySQL函数

一.部分需要学会的操作(以举例形式列出): insert into tmp15 values(This is good,50); /*向tmp15插入note 为 “This is good”,price为50的元素*/ 注:需要严格对应字段和元素属性的位置 select * from tmp15 /*查…

【Diffusion Model】Learning notes

来自 扩散模型 Diffusion Model 1-1 概述 扩散模型是什么? 本质是生成模型,拟合目标分布,然后生成很多数据符合这个分布 训练测试阶段? 和 GAN 相比优势是什么? generator 和 discriminator 两者都得训练的比较均衡…

JDK8到JDK17有哪些吸引人的新特性?

作者:京东零售 刘一达 前言 2006年之后SUN公司决定将JDK进行开源,从此成立了OpenJDK组织进行JDK代码管理。任何人都可以获取该源码,并通过源码构建一个发行版发布到网络上。但是需要一个组织审核来确保构建的发行版是有效的, 这个组织就是J…

Excel VBA 之Interior 对象设置底色

Interior 对象 代表一个对象的内部 针对interior对象,我们用得最多的是它的颜色,下面就来讨论一下。 1.ColorIndex 索引颜色值 Sub 索引颜色值()For i 1 To 56Cells(i, 1).Interior.ColorIndex iCells(i, 2) iNext iFor i 1 To 56Cells(i, 3).Interi…

算法训练第六十天 | 84.柱状图中最大的矩形

单调栈part0384.柱状图中最大的矩形题目描述思路暴力解法双指针解法单调栈84.柱状图中最大的矩形 题目链接:84.柱状图中最大的矩形 参考:https://programmercarl.com/0084.%E6%9F%B1%E7%8A%B6%E5%9B%BE%E4%B8%AD%E6%9C%80%E5%A4%A7%E7%9A%84%E7%9F%A9%E…

《Kubernetes部署篇:Ubuntu20.04基于containerd二进制部署K8S 1.24.12集群(一主多从)》

一、架构图 如下图所示: 如下图所示: 二、环境信息 1、部署规划 主机名IP地址操作系统内核版本软件说明etcd01192.168.1.62Ubuntu 20.04.5 LTS5.15.0-69-genericetcdetcd02192.168.1.63Ubuntu 20.04.5 LTS5.15.0-69-genericetcdetcd03192.168.1.64Ubunt…

kettle链接mysql Public Key Retrieval is not allowed

kettle 报错信息页面: 出现 Public Key Retrieval 的场景可以概括为在禁用 SSL/TLS 协议传输切当前用户在服务器端没有登录缓存的情况下,客户端没有办法拿到服务器的公钥。具体的场景如下: 新建数据库用户,首次登录;数…

课程推荐 | 机器视觉与边缘计算应用

点击蓝字关注我们,让开发变得更有趣文案 | 李擎排版 | 李擎文案来源 | https://www.icourse163.org/course/FUDAN-1456632162OpenVINO™╱ 前言 ╱机器视觉是目前人工智能重要的应用领域,在很多领域都有丰富的成功应用案例。其中深度学习的目标检测算法是非常实用的…

ubuntu(20.04)-shell脚本(1)-基本概念

目录 1.概述 2.shell脚本调用形式 3.shell语法初识 3.1 定义以开头:#!/bin/bash 3.2 单个“#”号代表注释当前行 4.变量 4.1 只读变量 4.2 环境变量: env 4.3 预测变量: 4.4 变量扩展: 是否存在,字符串…

通过JMH框架 测试公平锁与非公平锁的性能(附测试代码和源码分析)

目录 先上测试代码: 上依赖: 输出结果:(注意不要debug运行,直接运行代码,否则报错) 源码-公平锁的 lock 方法: 源码-非公平锁的lock方法: 总结 非公平锁和公平锁的两处不同: …

docker入门之一:docker基础概念与安装

1. Docker简单介绍 1.1. 什么是docker?1.2. Docker和传统虚拟机1.3. 为什么使用docker1.4. docker架构 2. Docker安装 2.1. docker版本命名2.2. docker安装2.3. docker卸载2.4. docker镜像加速器 1. Docker简单介绍 1.1. 什么是docker? google go语言…

24-Tomcat

目录 1.Tomcat是什么? 2.版本号 3.下载 4.目录介绍 4.1.bin目录 4.2.conf目录 4.3.logs目录 4.4.webapps目录 5.启动服务器 PS:解决Tomcat乱码问题 PS:Tomcat点击启动,控制台一闪而过,啥也没有解决方案 PS…

【花雕学AI】4月5日,ChatGPT中国财经背景分析:昨天沪指重返3300点,这说明了什么?

在这里插入图片描述 附录: 一、ChatGPT是一个可以和你聊天的人工智能程序,它可以用文字回答你的问题,也可以根据你的提示写出文章、歌词、代码等内容。ChatGPT是由一个叫OpenAI的机构开发的,它使用了一种叫做GPT的技术&…

TCP协议的相关特性(续)

TCP协议的相关特性🔎滑动窗口🔎流量控制🔎拥塞控制🔎延时应答🔎捎带应答🔎面向字节流(粘包问题)🔎异常情况🔎总结关于 确认应答 超时重传, 连接管理 请参考: 点击这里 &#x1f50e…

IT知识百科:什么是基站?

一、基站介绍 基站(Base Station),也称为基站站点或基站设备,是无线通信网络中的关键设备之一。基站用于与移动设备(如手机、无线网卡等)进行通信和数据传输,实现无线通信覆盖。 二、基站的功…

基于Python机器学习、深度学习技术提升气象、海洋、水文领域实践应用

Python是功能强大、免费、开源,实现面向对象的编程语言,能够在不同操作系统和平台使用,简洁的语法和解释性语言使其成为理想的脚本语言。除了标准库,还有丰富的第三方库,Python在数据处理、科学计算、数学建模、数据挖…

Web服务器压力测试

Web服务器压力测试 使用WebBench去进行网站的压力测试 1、去github下载项目源码webbench 2、download到本地 3、将压缩包上传到虚拟机上 4、解压,使用命令:unzip 压缩包名 5、 cd WebBench-mask6、构建项目 makemake install7、上述动作完成后&…

【Vue3】如何用Vue CLI 创建一个Vue3的初始化项目

第一步、安装Vue Cli npm install -g vue/cli 安装成功后,就可以在命令行工具中,使用vue命令。 检测是否安装成功,可以用 vue -V 出现版本号,代表安装成功。 第二步、创建项目 为了方便管理项目,我先在 github 创建了…