精华内容
下载资源
问答
  • 临界区的实现原理

    2021-05-17 21:58:44
    如果有多个线程试图同时访问临界区,那么在有一个线程进入临界区后,其他试图访问的线程将被挂起,直到进入临界区的线程离开。临界区在被释放后,其他线程可以继续抢占,并以此达到对临界区的互斥访问。(临界区中...

    临界区概述:

    用于多线程的互斥访问。如果有多个线程试图同时访问临界区,那么在有一个线程进入临界区后,其他试图访问的线程将被挂起,直到进入临界区的线程离开。临界区在被释放后,其他线程可以继续抢占,并以此达到对临界区的互斥访问。(临界区中一般都是一个简短的代码段)

    在WINDOWS中,临界区是一种应用层的同步对象,非内核对象。并且临界区优先采用自旋的方式进行抢占

    临界区API:

    临界区初始化以及删除:

    InitializeCriticalSection()

    DeleteCriticalSection()

    临界区两个操作原语:

    EnterCriticalSection()

    LeaveCriticalSection()

    临界区数据结构:

    typedef struct _RTL_CRITICAL_SECTION {

    PRTL_CRITICAL_SECTION_DEBUG DebugInfo;

    //

    // The following three fields control entering and exiting the critical

    // section for the resource

    //

    LONG LockCount;

    LONG RecursionCount;

    HANDLE OwningThread; // from the thread's ClientId->UniqueThread

    HANDLE LockSemaphore;

    ULONG_PTR SpinCount; // force size on 64-bit systems when packed

    } RTL_CRITICAL_SECTION, *PRTL_CRITICAL_SECTION;

    _RTL_CRITICAL_SECTION_DEBUG数据结构

    typedef struct _RTL_CRITICAL_SECTION_DEBUG {

    WORD Type;

    WORD CreatorBackTraceIndex;

    struct _RTL_CRITICAL_SECTION *CriticalSection;

    LIST_ENTRY ProcessLocksList;

    DWORD EntryCount;

    DWORD ContentionCount;

    DWORD Flags;

    WORD CreatorBackTraceIndexHigh;

    WORD SpareWORD ;

    } RTL_CRITICAL_SECTION_DEBUG, *PRTL_CRITICAL_SECTION_DEBUG, RTL_RESOURCE_DEBUG, *PRTL_RESOURCE_DEBUG; (代码来自VS2005 WINNT.h)

    _RTL_CRITICAL_SECTION 各字段解释:

    DebugInfo:指向一个调试用的数据,该结构的类型为RTL_CRITICAL_SECTION_DEBUG

    LockCount: 初始值-1,若结果大于等于0,表示该临界区已被线程占用。

    OwningThread: 当前拥有临界区的线程

    RecursionCount:所有者线程连续进入临界区的次数

    LockSemaphore: 内核对象句柄,用于告知操作系统,该临界区目前处于空闲状态,用于唤醒因等待临界区而挂起的线程

    SpinCount:MSDN对该字段做如下解释:

    "On multiprocessor systems, if the critical section is unavailable, the calling thread will spin

    dwSpinCount times before performing a wait operation on a semaphore associated with the critical

    section. If the critical section becomes free during the spin operation, the calling thread

    avoids the wait operation."

    在多处理器系统中,如果临界区已被占用,那么线程就自旋SpinCount次去获取临界区,而不是通过阻塞等待的方式去获取临界区。如果在自旋的过程中临界区空间,就可以直接进入临界区,减少等待时间(如果进入等待状态,需要用户态内核态的切换,代价较大)。主要意思就是为了提高效率,下面我们会分析什么叫自旋。

    临界区API实现过程

    InitialzeCriticalSection 在初始化的过程中,会测试CPU的数量,若CPU数量为1,则忙等待没有意义。则SpinCount=0,

    若CPU数量大于1,则设置SpinCount,在进入临界区时,会采取主动进入策略。

    EnterCriticalSection 1. 若临界区还未被占用,则更新临界区数据结构,表示调用线程已经获得访问临界区的权限,返回。

    2. 若线程在已经获取访问权限的情况下,再次EnterCriticalSection,则更新线程获取访问的次数(即连续Enter的次数)。

    3. 若临界区已经其他线程占用,则当前线程 通过SpinCount来控制忙等的次数,在SpinCount已经等于0还没有获得临界区对象的情况下,函数直接通过临界区对象内部的事件对象进行等待(等待及唤醒涉及到用户态和内核态的切换,不是最优方案,优先采用自旋的方式进入临界区)。忙等待是通过对LockCount进行原子读写操作实现。

    RtlLeaveCriticalSection 1. _RTL_CRITICAL_SECTION数据结构中相关标志位设置 ,比如RecursionCount--,如果为0,表示没有线程占用临界区

    2. 将当前占有线程句柄设为0,表示现在临界区目前处于有信号状态,可以被获取

    3. 若有其他线程在等待,唤醒等待线程

    a3a35f2f9e608518c70933d4e14c1235.gif

    通过该数据结构可以发现,在进程中,所有的临界区的DEBUG信息通过链表进行串接。在已知某个临界区对象的

    情况下, 通过链表数据结构,可以访问到数据的临界区对象。

    自旋:

    对于临界区的操作,(EnterCriticalSection)操作,采用的是主动进入临界区。意思就是,当没有能够进入时,不停的主动尝试进入,直至进入为止。这种主动进入的方式,称为自旋(也叫忙等待)。

    被动方式:获取不到后,进入等待队列,当要获取的对象被释放后,系统唤醒等待的线程,这个方式叫做被动方式。

    小结:

    1.进入灵界区和离开临界区是成对操作,进入临界区必须要有离开临界区否则临界区保护的共享资源将永远不会被释放。

    2.在使用临界区时,临界区间使用的代码最好简短,减少其他线程的等待时间,提高程序性能。

    3.临界区同步速度很快,但却只能用来同步本进程内的线程,而不可用来同步多个进程中的线程。

    4. 临界区是用户态下的对象,非内核对象,所以在使用时无需再用户态和内核态之间切换,效率明显要比其他用户互斥的内核对象高。

    参考:

    展开全文
  • 一、临界区的引出 从上一篇文章中,我们了解到了信号量的概念。信号量的数值表达的语义用来控制进程的走和停,因此,信号量的数值是非常重要的,信号量的数值吧必须和信号量的语义相一致,才能正确地决定进程的同步...

    目录

    一、临界区的引出

    二、临界区的软件实现

    1.轮换法

    2.标记法

    3.Lamport面包店算法

    三、临界区的硬件实现

    总结 


    一、临界区的引出

    从上一篇文章中,我们了解到了信号量的概念。信号量的数值表达的语义用来控制进程的走和停,因此,信号量的数值是非常重要的,信号量的数值吧必须和信号量的语义相一致,才能正确地决定进程的同步。比如信号量的数值为0,但此时实际上有一个进程阻塞在该信号量,那么即使对该信号量执行V操作也不会唤醒被阻塞的进程。

    但是信号量是一个会被多个进程操作的共享变量,各个并发进程竞争使用CPU会导致各种各样的调度问题,部分调度顺序可能会导致信号量出现语义错误

    以生产者——消费者问题为例,初始情况empty=-1,P1、P2都应当阻塞在该信号量上,empty=-3,但是由于P2在P1完成empty=P1.register之前,就开始对empty进行操作,导致最后empty=-2,出现了语义错误。

    为了对信号量进行保护,就要使一个进程在对信号量进行修改操作时,其他的进程必须处于等待状态,当该进程修改完成以后,才能接着对信号量进行操作。即进程对信号量的修改要么是一点不做,要么是全部做完,对信号量的修改必须是一次原子操作

     那么引入什么样的机制可以使进程对信号量的操作变成原子操作?

    在软件系统中,无论增加什么机制,都表现为增加了代码,所以对信号量的保护就是在对信号量操作代码的基础上“包裹”其他代码来让信号量的修改操作成为原子操作

     

    如图所示,当P1进入临界区修改信号量数值的时候,即empty--,P2就不能进入临界区执行empty--了。

    临界区就是进程中一段代码,这段代码与其他相关进程中的相关代码是对应的,一次只允许一个进程进入,就是互斥进入。一旦进入这段代码,操作系统的状态就发生了改变,不能随意地切换进程,而在执行进程中的其他代码时是可以切换的,因此这一段代码称为临界区。

    了解了临界区的概念,我们发现问题就转变为如何将修改信号量的代码转变为临界区代码。那么就需要在修改代码的前后加上进入区代码和退出区代码,如何实现进入区和退出区就成了核心问题。

    二、临界区的软件实现

    1.轮换法

    临界区要求互斥进入,那么可以采用轮换法。

     如上图所示,P0和P1同时只能有一个进入临界区去修改信号量的大小。但也存在问题,P0只有当turn=0的时候才能进入临界区,假设P0进入过一次临界区后,turn=1,那么只有当)P1执行一次后,turn才会重新变成0,那如果P1不执行的话P0也始终无法得到执行。

    因此,临界区的实现不仅仅要考虑互斥进入一个因素:

    1.互斥进入。一次只允许一个进程进入临界区。

    2.有空让进。如果没有进程在临界区执行,并且有进程请求进入临界区,那么就应当让请求进程的一个进入临界区,防止资源死锁。

    3.有限等待。任何人一个进程在提出进入临界区的请求后,在等待有限的时间后就能够进入临界区。

    2.标记法

    标记法即给每一进程加一个标记flag[i],当该进程要进入临界区时,就把flag置1,再扫描是否有其他进程想要进入临界区,即其他进程的flag为true,如果有的话就进入自循环等待。

    标记法虽然能做到互斥进入,但当P0和P1都想进入临界区,flag都为true的时候,两个进程都会进行自循环,这就无法满足有空让进的原则。

     对标记法进行改进:

    1.用标记法判断进程是否想进入临界区;

    2.如果进程想进入临界区,就用轮换法给出一个明确的优先顺序;

    3.将轮换法和标记法结合在一起,这就是Peterson算法。

    如果P0和P1都进入了临界区,说明flag[0]=flag[1]=ture,而turn=1=0,这是不可能的,所以满足互斥进入;如果P0想进入临界区而P1不想,则flag[0]=1、flag[1]=0,那么P0就不满足自循环条件,会进行信号量的修改,而如果两个进程都想进入临界区,那么轮到的那个进程就能够进入临界区,另一个自循环等待,满足有空让进;当一个进程进入临界区以后,执行完成后就会将flag置0,那么在等待的另一个进程就可以不再进入自循环,转而修改信号量数值,满足有限等待原则。

    3.Lamport面包店算法

    Peterson算法虽然能实现对临界区的保护,但是它只能处理两个进程的临界区,因此就诞生Lamport面包店算法来对多进程临界区进行保护。

    该算法将在面包店购买面包的例子与对临界区的保护进行类比,面包店同时只能接待一位顾客买单,其余顾客处在队列中等待买单。面包店会给每一个进店的顾客一个号码,顾客按照由小到大的顺序进行买单,而买完单的顾客的序号就变成了0,想要再次购买就必须重新取号。

    do
    {
        choosing[i] = true;
        number[i] = maxnumber[0],number[1],···,number[n-1] + 1;//号码选取
        choosing[i] = false;
        
        for(j = 0; j < n; j++)
        {
            while(choosing[j]);
            while((number[j] != 0) && (number[j],j) < (number[i],i));
        }
        ···//临界区代码
        number[i] = 0;
    }while(true);

     1.由于每一个进程都有自己的序号,相同序号的进程再按照名字顺序排列,选出进入临界区的进程一定是唯一的;

    2.没有竞争时想要进入的进程直接进入,由竞争的时候一定会选出一个进程进入临界区;

    3.一旦进程获得一个号码后,后面的进程获得的号码一定会大于这个号码,进程的等待时间也是有限的。

    三、临界区的硬件实现

    Lamport面包店算法虽然实现了多进程的临界区访问问题,但是效率比较低下,号码选取、号码维护、号码判断的代价都不小,并且选取的号码数值不断变大,可能会发生溢出的情况。既然软件实现可能会存在一定问题,那么就考虑使用硬件方法实现多进程对临界区的访问。

    对于只有一个CPU的计算机,当P0进入临界区之后,意味着CPU正在执行P0临界区中的代码,而当P1进入临界区后,CPU转而去执行P1临界区中的代码,那么CPU在这个过程中进行了进程的调度和切换。而对于单CPU计算机来说,如果能阻断调度,就不会发生在CPU在执行一个进程临界区代码的时候切换去执行其他进程,进入另一个临界区的情况。

     进程切换需要调用schedule()函数,而调用schedule()函数需要进入内核,而进入内核需要中断,如果能禁止中断就可以防止CPU的切换。

    cli()的工作原理是将CPU标志寄存器中的中断允许标志寄存位IF设置为0,这样CPU就不会在执行完指令后去检查并处理中断了。但对于多CPU计算机而言,其他的CPU仍然可以执行其他的想要进入临界区的进程。

    换一种思路,这个问题有点像生产者——消费者问题中为了保证共享缓存区中信号量语义的正确性,一次只能有一个进程进行访问,我们用一个互斥信号量mutex=1来实现互斥访问的控制,那么是否可以用这个信号量来保护临界区了?

    但是临界区本身是用来保证信号量操作的原子性,又要在临界区中使用信号量,mutex=1本身作为一个信号量就需要其他手段来保证其操作的原子性,这是一个无法实现的循环问题。

    那么我们可以通过硬件的方式来实现mutex操作的原子性,再用mutex实现临界区,再用临界区去保证其他任意信号量操作的原子性。

    保护临界区的互斥信号量只有0和1两个数值,这个信号量的P、V操作简单规整,可以用硬件方法实现,而由于信号量只有两个数值,很像一个锁,所以常被命名为lock。

    lock=0时,说明没有上锁,进程可以进入到临界区,同时将lock修改为1,这个指令要做成一条硬件指令,一条硬件指令的执行是不会被打断的,所以实现了指令的原子操作。如果lock=1,则进程自旋等待。计算机中有一条硬件指令:TestAndSet。这条指令的操作数存放一个布尔变量的地址,如果该内存中的变量为false,指令会返回false,并将内存中的变量置为true,如果变量为true,则返回true。

    function TestAndSet(boolean &lock)
    {
        boolean initial = *lock;
        *lock = true;
        return initial;
    }

    有了这样一条指令来处理lock,我们就可以利用lock来保护临界区。

    一旦有进程在lock=false的情况下跳过循环进入临界区,lock就被置为true了,其他进程就处于自旋状态,而当该进程完成信号量操作后,lock=false,其他处于自旋状态的进程才能进入临界区。

    即使多个进程执行在多个CPU上,由于大家共享内存,TestAndSet指令操作的lock变量是相同的,对同一个lock变量的TestAndSet可以保证只有一个进程返回false,其他进程返回true。


    总结 

    1.介绍了临界区的概念;

    2.说明了临界区使用软件实现的方法;

    3.介绍了用硬件方法实现临界区并对信号量进行保护。

     

    展开全文
  • //进入临界区 2 3//临界区代码 4 5EXIT_CRITICAL();//退出临界区 4 临界区嵌套 临界区的使用没啥可说的,但是在你的代码中怎么加临界区确实一门技巧,可是说很多3~5年的工程师也并不一定处理得好,本文暂不展开,...

    1

    裸机与RTOS的理解

    首先这里只针对单核CPU架构的芯片展开讨论,大部分是MCU吧,而多核CPU的讨论相对比较复杂,暂不涉及~

    玩RTOS的朋友都知道,裸机与OS的最大区别就是实现多任务的并发,其实你说裸机就不能实现任务的并发吗 ? 这个需要看所站的角度吧,只是说RTOS并发的粒度可以更加细,因为你把裸机的任务拆分成多块运行,其实也是一种并发方式。

    从宏观上虽然RTOS的每个任务都是在并发执行,其实微观上还是一条一条指令在顺序执行着,如下图所示:

    而对于目前主流的RTOS,如UCOS或者FreeRTOS,所实现的都是多任务,更多的是一种多线程的并发执行而非多进程,所以对应到Linux平台上称他们为thread。

    2

    并发带来的问题

    并发的好处就是能够在更细的粒度来尽可能的提高CPU的利用率,这里不能说使用了多线程就一定能提高,这与你所设计的任务划分和处理有着直接的关系,只能说多线程相比裸机更有这个能力。

    而任何事物都有其利弊,多个任务在没有同步处理的情况下,任务之间是无序运行的,无序也就意味着状态的多样性和复杂度。

    当然bug菌这里所说的无序是一个相对的过程,比如对于CPU而言,它就是顺序的去执行一条一条的指令,所以在这个层面它是有序的、确定的。

    而我们把过程放大,比如执行一条C语言语句,一般它是由多条汇编指令组成,对于目前的抢占式内核,在一段时间内其多个任务就有可能指令交替执行,当这些指令都去操作同一块内存,那么内存的最终结果由于顺序不同而不同,最终难以确定。

    状态的不确定就有可能造成异常行为,也就是大家经常遇到的:"怎么跑着跑着就有问题,还没啥规律~","这段代码怎么看也没问题呀~"

    所以对比看来RTOS确实会带来编程上的难度~

    3

    临界区

    既然有难度,我们就要解决,把不确定性部分通过一些手段来变得确定,而造成这些不确定因素的动力是什么呢?是中断~

    bug菌一直觉得,其实对于裸机而言,如果把中断服务函数看成一个更高优先级的抢占式任务,其实裸机主任务与中断任务就形成了一种两任务的并发,所以中断与任务之间也是有共享问题需要类似处理的。

    为了解决这些不确定因素,我们只需要在这段代码区域限制中断的发生即可,这一段区域就是临界区,说得直白点 : 关中断与开中断。

    1ENTER_CRITICAL();//进入临界区
    2
    3//临界区代码
    4
    5EXIT_CRITICAL();//退出临界区
    

    4

    临界区嵌套

    临界区的使用没啥可说的,但是在你的代码中怎么加临界区确实一门技巧,可是说很多3~5年的工程师也并不一定处理得好,本文暂不展开,后面bug菌整理以后再分享给大家,今天只聊聊临界区嵌套使用的问题,毕竟很多朋友在这里掉过坑~

    参考伪代码:

     1/*********************************************
     2 * Function: Fuction1
     3 * Description:功能函数
     4 * Author: bug菌
     5 ********************************************/
     6void Fuction1(void)
     7{
     8    ENTER_CRITICAL();//进入临界区
     9
    10    //do something~
    11
    12    EXIT_CRITICAL();//退出临界区
    13}
    14/*********************************************
    15 * Function: Fuction2
    16 * Description: 功能函数
    17 * Author: bug菌
    18 ********************************************/
    19void Fuction2(void)
    20{
    21    ENTER_CRITICAL();//进入临界区
    22
    23    ......
    24    Fuction1();
    25
    26    ......
    27    //do something~
    28
    29    EXIT_CRITICAL();//退出临界区
    30}
    

    这种临界区的使用是很多朋友常犯的错误,当然这里的临界区操作仅仅只是开关中断,许多自己公司写的,或者裁剪的都是这种简约开关中断版本,所以当调用Function1函数以后,后面的代码就不在临界区内了,此时就有可能会存在共享问题。

    当然目前的开源OS都会提供一种把相关嵌套标记保存在局部变量中的处理方式,如下代码所示:

     1//来源于ucos源码
     2#define OS_ENTER_CRITICAL() (cpu_sr = OSCPUSaveSR())
     3#define OS_EXIT_CRITICAL() (OSCPURestoreSR(cpu_sr))
     4
     5/*********************************************
     6 * Function: Fuction1
     7 * Description:功能函数
     8 * Author: bug菌
     9 ********************************************/
    10void Fuction1(void)
    11{
    12    int cpu_sr;
    13
    14    OS_ENTER_CRITICAL();//进入临界区
    15
    16    //do something~
    17
    18    OS_EXIT_CRITICAL();//退出临界区
    19}
    20
    21/*********************************************
    22 * Function: Fuction2
    23 * Description: 功能函数
    24 * Author: bug菌
    25 ********************************************/
    26void Fuction2(void)
    27{
    28    int cpu_sr;
    29
    30    OS_ENTER_CRITICAL();//进入临界区
    31
    32    Fuction1(void);
    33
    34    OS_EXIT_CRITICAL();//退出临界区
    35
    36}
    
    

    为了更好的理解,我写了一下下面的伪代码,供大家参数~

     1//中断寄存器register原本是1, 向register写0关中断,向register写1开中断
     2
     3void Fuction2(void)
     4{
     5    int cpu_sr1 = 0;
     6
     7    cpu_sr1   = register; 
     8    register  = 0; //register == 0;cpu_sr1 == 1;
     9
    10           void Fuction1(void)
    11           {
    12               int cpu_sr1 = 0;
    13
    14               cpu_sr2   = register; 
    15               register  = 0; //register == 0;cpu_sr2 == 0;
    16
    17
    18               register  = cpu_sr2; 
    19               cpu_sr2   = 0;//register == 0;cpu_sr2 == 0;
    20           }
    21
    22    register  = cpu_sr1; 
    23    cpu_sr1   = 0;//register == 1;cpu_sr1 == 0;
    24
    25}
    

    不同的OS可能具体实现有所差异,大体上都一样~

    最后

    好了,兄弟别下次了,就这次给个吧~~


    推荐阅读:

    专辑|Linux文章汇总

    专辑|程序人生

    专辑|C语言

    我的知识小密圈

    关注公众号,后台回复「1024」获取学习资料网盘链接。

    欢迎点赞,关注,转发,在看,您的每一次鼓励,我都将铭记于心~

    嵌入式Linux

    微信扫描二维码,关注我的公众号

    展开全文
  • 来源 |痞子衡嵌入式今天给大家分享的是Cortex-M裸机环境下临界区保护的三种实现。搞嵌入式玩过 RTOS 的朋友想必都对 OS_ENTER_CRITICAL()、OS_EXIT_CR...

    来源 | 痞子衡嵌入式

    今天给大家分享的是Cortex-M裸机环境下临界区保护的三种实现

    搞嵌入式玩过 RTOS 的朋友想必都对 OS_ENTER_CRITICAL()、OS_EXIT_CRITICAL() 这个功能代码对特别眼熟,在 RTOS 里常常会有多任务(进程)处理,有些情况下一些特殊操作(比如 XIP 下 Flash 擦写、低功耗模式切换)不能被随意打断,或者一些共享数据区不能被无序访问(A 任务正在读,B 任务却要写),这时候就要用到临界区保护策略了。

    所谓临界区保护策略,简单说就是系统中硬件临界资源或者软件临界资源,多个任务必须互斥地对它们进行访问。RTOS 环境下有现成的临界区保护接口函数,而裸机系统里其实也有这种需求。在裸机系统里,临界区保护主要就是跟系统全局中断控制有关。痞子衡之前写过一篇 《嵌入式MCU中通用的三重中断控制设计》,文中介绍的第三重也是最顶层的中断控制是系统全局中断控制,今天痞子衡就从这个系统全局中断控制使用入手给大家介绍三种临界区保护做法:

    一、临界区保护测试场景

    关于临界区保护的测试场景无非两种。第一种场景是受保护的多个任务间并无关联,也不会互相嵌套,如下面的代码所示,task1 和 task2 是按序被保护的,因此 enter_critical() 和 exit_critical() 这两个临界区保护函数总是严格地成对执行:

    void critical_p_test(void)
    {
        // 进入临界区
        enter_critical();
        // 做受保护的任务1
        do_task1();
        // 退出临界区
        exit_critical();
    
        // 进入临界区
        enter_critical();
        // 做受保护的任务2,与任务1无关联
        do_task2();
        // 退出临界区
        exit_critical();
    }
    

    第二种场景就是多个任务间可能有关联,会存在嵌套情况,如下面的代码所示,task2 是 task1 的一个子任务,这种情况下,你会发现实际上是先执行两次 enter_critical(),然后再执行两次 exit_critical()。需要注意的是 task1 里面的子任务 task3 虽然没有像子任务 task2 那样被主动加一层保护,但由于主任务 task1 整体是受保护的,因此子任务 task3 也应该是受保护的。

    void do_task1(void)
    {
        // 进入临界区
        enter_critical();
        // 做受保护的任务2,是任务1中的子任务
        do_task2();
        // 退出临界区
        exit_critical(); 
    
        // 做任务3
        do_task3();
    }
    
    void critical_p_test(void)
    {
        // 进入临界区
        enter_critical();
        // 做受保护的任务1
        do_task1();
        // 退出临界区
        exit_critical();
    }
    

    二、临界区保护三种实现

    上面的临界区保护测试场景很清楚了,现在到 enter_critical()、exit_critical() 这对临界区保护函数的实现环节了:

    2.1 入门做法

    首先是非常入门的做法,直接就是对系统全局中断控制函数 __disable_irq()、__enable_irq() 的封装。回到上一节的测试场景里,这种实现可以很好地应对非嵌套型任务的保护,但是对于互相嵌套的任务保护就失效了。上一节测试代码里,task3 应该也要受到保护的,但实际上并没有被保护,因为紧接着 task2 后面的 exit_critical() 直接就打开了系统全局中断。

    void enter_critical(void)
    {
        // 关闭系统全局中断
        __disable_irq();
    }
    
    void exit_critical(void)
    {
        // 打开系统全局中断
        __enable_irq();
    }
    

    2.2 改进做法

    针对入门做法,可不可以改进呢?当然可以,我们只需要加一个全局变量 s_lockObject 来实时记录当前已进入的临界区保护的次数,即如下代码所示。每调用一次 enter_critical() 都会直接关闭系统全局中断(保证临界区一定是受保护的),并记录次数,而调用 exit_critical() 时仅当当前次数是 1 时(即当前不是临界区保护嵌套情况),才会打开系统全局中断,否则只是抵消一次进入临界区次数而已。改进后的实现显然可以保护上一节测试代码里的 task3 了。

    static uint32_t s_lockObject;
    
    void init_critical(void)
    {
        __disable_irq();
        // 清零计数器
        s_lockObject = 0;
        __enable_irq();
    }
    
    void enter_critical(void)
    {
        // 关闭系统全局中断
        __disable_irq();
        // 计数器加 1
        ++s_lockObject;
    }
    
    void exit_critical(void)
    {
        if (s_lockObject <= 1)
        {
            // 仅当计数器不大于 1 时,才打开系统全局中断,并清零计数器
            s_lockObject = 0;
            __enable_irq();
        }
        else
        {
            // 当计数器大于 1 时,直接计数器减 1 即可
            --s_lockObject;
        }
    }
    

    2.3 终极做法

    上面的改进做法虽然解决了临界区任务嵌套保护的问题,但是增加了一个全局变量和一个初始化函数,实现不够优雅,并且嵌入式系统里全局变量极容易被篡改,存在一定风险,有没有更好的实现呢?当然有,这要借助 Cortex-M 处理器内核的特殊屏蔽寄存器 PRIMASK,下面是 PRIMASK 寄存器位定义(取自 ARMv7-M 手册),其仅有最低位 PM 是有效的,当 PRIMASK[PM] 为 1 时,系统全局中断是关闭的(将执行优先级提高到 0x0/0x80);当 PRIMASK[PM] 为 0 时,系统全局中断是打开的(对执行优先级无影响)。

    看到这,你应该明白了 __disable_irq()、__enable_irq() 功能其实就是操作 PRIMASK 寄存器实现的。既然 PRIMASK 寄存器控制也保存了系统全局中断的开关状态,我们可以通过获取 PRIMASK 值来替代上面改进做法里的全局变量 s_lockObject 的功能,代码实现如下:

    uint32_t enter_critical(void)
    {
        // 保存当前 PRIMASK 值
        uint32_t regPrimask = __get_PRIMASK();
        // 关闭系统全局中断(其实就是将 PRIMASK 设为 1)
        __disable_irq();
    
        return regPrimask;
    }
    
    void exit_critical(uint32_t primask)
    {
        // 恢复 PRIMASK
        __set_PRIMASK(primask);
    }
    

    因为 enter_critical()、exit_critical() 函数原型有所变化,因此使用上也要相应改变下:

    void critical_p_test(void)
    {
        // 进入临界区
        uint32_t primask = enter_critical();
        // 做受保护的任务
        do_task();
        // 退出临界区
        exit_critical(primask);
    
        // ...
    }
    

    附录、PRIMASK寄存器设置函数在各 IDE 下实现

    //
    // IAR 环境下实现(见 cmsis_iccarm.h 文件)
    #define __set_PRIMASK(VALUE)        (__arm_wsr("PRIMASK", (VALUE)))
    #define __get_PRIMASK()             (__arm_rsr("PRIMASK"))
    
    //
    // Keil 环境下实现(见 cmsis_armclang.h 文件)
    __STATIC_FORCEINLINE void __set_PRIMASK(uint32_t priMask)
    {
      __ASM volatile ("MSR primask, %0" : : "r" (priMask) : "memory");
    }
    
    __STATIC_FORCEINLINE uint32_t __get_PRIMASK(void)
    {
      uint32_t result;
    
      __ASM volatile ("MRS %0, primask" : "=r" (result) );
      return(result);
    }
    

    至此,Cortex-M裸机环境下临界区保护的三种实现已经讲述完毕,你学废了吗?

    声明:本文素材来源网络,版权归原作者所有。如涉及作品版权问题,请与我联系删除。

    ------------ END ------------


    ●嵌入式专栏精选教程

    ●精选汇总 | ST工具、下载编程工具

    ●精选汇总 | 嵌入式软件设计与开发

    ●精选汇总 | STM32、MCU、单片机

    迎关注我的公众号回复“加群”按规则加入技术交流群,回复“1024”查看更多内容。

    欢迎关注我的视频号:

    点击“阅读原文”查看更多分享,欢迎点分享、收藏、点赞、在看。

    展开全文
  • 信号与信号量 在需要同步的位置上,进程将自己阻塞起来等待信号;当该进程所依赖的进程执行到...因此,信号量就是一个数据对象以及操作这个数据对象的个操作 其中数据对象是信号量数值以及相应的阻塞进程队列
  • 今天痞子衡给大家分享的是Cortex-M裸机环境下临界区保护的三种实现。  搞嵌入式玩过 RTOS 的朋友想必都对 OS_ENTER_CRITICAL()、OS_EXIT_CRITICAL() 这个功能代码对特别眼熟,在 RTOS 里常常会有多任务(进程)处理,...
  • 大家好,我是痞子衡,是正经搞技术的痞子。今天痞子衡给大家分享的是Cortex-M裸机环境下临界区保护的三种实现。搞嵌入式玩过 RTOS 的朋友想必都对 OS_ENTER_CRITICAL(...
  • 竞争条件和临界区

    2021-08-12 16:09:04
    竞争条件和临界区竞争条件临界区 竞争条件 在一些操作系统中,协作的进程可能共享一些彼此都能读写的公用存储区。这个公用存储区可能在内存中(可能是在内核数据结构中),也可能是一个共享文件。这里共享存储区的...
  • 临界区是指一个小代码段,在代码执行前能够独占某些资源的访问权;需要注意的是,系统仍然能够控制线程的运行,去安排其他线程。不过,在线程退出临界区之前,系统不会调度其他试图访问相同资源的线程。来看一段代码...
  • 临界区(Critical Sections) 一只允许一个进程访问;打印机只能允许一个进程使用它 死锁(Deadlock) 例如,有个进程P1、P2,竞争个资源A、B。假设: 占用:P1(B)and P2(A) 申请:P1(A)and P2(B) ...
  • 临界区保护

    2021-07-27 23:49:01
    每个线程中访问(操作)临界资源的那段代码成为临界区(Critical Section),每次只准许一个线程进入临界区。 //临界资源 uint32_t value = 0; //临界区 void thread1_entry(void *para) { uint32_t i = 0;
  • windows临界区

    2021-05-13 10:19:43
    windows临界区 像下面这种编程是针对于C++语言本身的,是可以跨平台的 windows临界区和C++的mutex非常类似 现在讲讲windows下一些专用的术语,跟互斥量的关联 下面是一段用互斥量来演示的代码 class A { public: /...
  • 但是每一,只能有一个线程使用它,一旦临界区资源被占用,其他线程想要使用这个资源,就必须等待。 锁 锁的作用就是保障临界区 阻塞与非阻塞 阻塞与非阻塞是用来形容线程间的影响的。 通过临界区的定义我们可知,...
  • 一、课程设计目的本课程设计是学生学习完《计算机操作系统》课程后,进行的一全面的综合训练,通过课程设计,让学生更好地掌握操作系统的原理及实现方法,加深对操作系统基础理论和重要算法的理解,加强学生的动手...
  • 文章目录(1)临界区,临界资源和原子性问题(2)互斥量(锁)A:互斥锁B:锁的作用C:互斥锁实现的原理(3)可重入函数和线程安全A:可重入函数和线程安全B:常见的线程安全和不安全情况C:常见可重入和不可重入的...
  • 本系列译自jakob jenkov的Java并发多线程教程,个人觉得很有收获。由于个人水平有限,不对之处还望矫正!竞态条件是在临界区内可能发生的...竞态一词源于隐喻,线程在临界区进进行资源竞争,在临界区的资源竞争影响...
  • } 假设下面两次调用发生在不同线程的同一个时间点: 线程A SwapLists(home_address_list, work_address_list); 线程B SwapLists(work_address_list, home_address_list); 而在线程A的SwapLists()的第一次...
  • 什么是临界区 临界区就是一只允许一个进程进入的那段代码 使用什么方法保护信号量,从而实现临界区 注:信号量和临界区的关系:信号量的修改代码 就是临界区 临界区代码的保护原则 互斥进入 一个进程在临界区中...
  • Linux中使用临界区加锁的方法是用pthread_mutex_t进行操作,分别调用pthread_mutex_init、 pthread_mutex_destroy创建和释放pthread_mutex变量,调用pthread_mutex_lock和 pthread_mutex_unlock进行加锁和解锁。...
  • 在“同一个线程中”(不同线程就会卡主等待),windows中的“相同临界区变量”代表的临界区进入(EnterCriticalSection)可以被多调用。 Windows临界区,“同一个线程‘”是可以重复进入的,但是进入的次数与...
  • 下面的同步技术一般均有种使用方式,一种是直接...所谓临界区,就是一只能由一个线程来执行的一段代码。例如把初始化数组的代码放在临界区内,另一个线程在第一个线程处理完之前是不会被执行的。临界区非常适合...
  • 临界区_互斥简述多个临界区造成互锁4.互斥体_互斥5.事件_互斥a.事件简述b.线程同步:生产者与消费者 0.说明 去年底写的文章,忘了发,发一下。 观看滴水逆向视频总结(部分截图来自于滴水课件) 编译器:vc++6.0 ...
  • 是因为在临界区允许多个进程执行,所以需要同步机制。 二、信号量 信号量的引入: 在临界区中希望多个进程运行,那么不光有互斥,也要有同步功能。比如说读写操作,那么可以用量进程进行执行。 抽象数据类型:一...
  • 简单讲: 个程序同时读写同一个共享数据时会发生意想不到的后果 为了避免这种情况,必须要在程序中采取同步的机制 2 线程之间的5种常用同步方式(本文简要介绍前三种) 2.1 原子互锁家族的函数 我们把单指令的操作...
  • Java - synchronized关键字

    2021-03-16 21:38:03
    在java中,为了解决这个问题,引入临界区概念。所谓临界区是指一个访问共用资源的程序片段,而这些共用资源又无法同时被多个线程访问。在java中为了实现临界区提供了同步机制。当一个线程试图访问一个临界区时,他将...
  • 为了避免临界区的竞态条件发生,有多种手段可以达到目的。 阻塞式的解决方案:synchronized,Lock 非阻塞式的解决方案:原子变量 1.1 synchronized 介绍 synchronized,即俗称的【对象锁】,它采用互斥的方式让...
  • 关键词:Dekker算法、TS指令、SWAP指令、进程联系、顺序程序与顺序环境、并发环境与并发进程、与时间有关的不确定性、相交进程与无关进程、进程同步与互斥、临界区管理、临界区及其使用原则、临界区管理——软件方法...
  • 操作系统编程模拟多进程共享临界资源linux,c语言.一、课程设计目的本课程设计是学生学习完《计算机操作系统》课程后,进行的一全面的综合训练,通过课程设计,让学生更好地掌握操作系统的原理及实现方法,加深对...
  • ucos ii的51单片机移植

    2021-05-24 01:18:45
     OS_CPU_A.ASM的移植 1) 退出临界区进入临界区代码 它们分别是退出临界区进入临界区代码的宏实现,主要用于在进入临界区之前关闭中断,在退出临界区后恢复原来的中断状态。 ARMDisableINT MRS R0,CPSR ; Set...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 38,264
精华内容 15,305
关键字:

临界区两次进入