在多线程系统中,一些资源有寄存器,变量,函数,外设都是可以共同访问的.在实际的编程中,会导致数据错乱的问题,本文将捋捋如何管理共享资源.

共享资源的错误场景

  1. 外设的共同访问

    A线程一直打印123456789101112...,\ B线程一直打印ABCDEFGHIJK..., \ 多线程输出的结果可能是如下:

    A1BC2D34E5F67G89H10I11JK12

    每个线程无序占用打印资源

  2. 变量的共同访问

    假如一个数组,在A线程读取的时候是123456789101112...,\ 读取到一半的过程中,切换到B线程,将数组修改为ABCDEFGHIJK...,\ 最后,再切回A线程,数组的读取结果变为:

    12345678IJK...
  3. 寄存器的共同访问

    芯片的寄存器是映射到内存地址上的,CPU通过内存地址访问对应的寄存器,其中寄存器也是可以共同访问.

    如下是A线程对PORTA寄存器进行一个取值后修改的操作.

     /* 正在编译的C代码。*/
    PORTA |= 0x01; 
    /* 编译C代码时生成的汇编代码。*/
    LOAD   R1, [#PORTA]  ; Read a value from PORTA into R1
    MOVE   R2, #0x01     ; Move the absolute constant 1 into R2
    OR     R1, R2        ; Bitwise OR R1 (PORTA) with R2 (constant 1)
    STORE  R1, [#PORTA]  ; Store the new value back to PORTA

    对寄存器的操作是几步汇编指令完成的,中断会打断执行过程,假如 B线程 中间修改了PORTA数值,会被A线程覆盖掉.如下一个伪代码:

    /* 编译C代码时生成的汇编代码。*/
    LOAD   R1, [#PORTA]  ; Read a value from PORTA into R1
    MOVE   R2, #0x01     ; Move the absolute constant 1 into R2
    
    /* 中断! B线程修改 PORTA 寄存器的数值为0xFF */
    
    OR     R1, R2        ; Bitwise OR R1 (PORTA) with R2 (constant 1)
    STORE  R1, [#PORTA]  ; Store the new value back to PORTA
  4. 函数的共同访问

    线程安全函数是指能够在多线程环境中安全地被调用的函数。\ 每个线程维护自己的堆栈和自己的一组处理器(硬件)寄存器值。如果一个函数只访问存储在堆栈或寄存器中的数据,那么该函数是线程安全的。如下是区别:

    /* 线程安全函数 */
    long lAddOneHundred( long var )
    {
    
        long local_variables;
    
        local_variables = var + 100;
        return local_variables;
    }
    /* 线程不安全函数,多线程同时调用会导致数据 global_variables 错乱 */
    long global_variables;
    
    long lNonsenseFunction( void )
    {
        /* lstate 是静态的,没有在堆栈上分配。多线程调用该函数的任务都将访问同一个变量, 会导致数据错乱。*/
        static long lState = 0;
        long lReturn;
    
            switch( lState )
            {
                case 0 : lReturn = global_variables + 10;
                        lState = 1;
                        break;
                case 1 : lReturn = global_variables + 20;
                        lState = 0;
                        break;
            }   
        return lReturn;
    }

共享资源的保护

在访问共享资源的时候,称为临界区. 保护的方式共享资源的方式有多种: 屏蔽中断,挂起OS调度,互斥锁

屏蔽中断

FREERTOS 是通过调用宏taskENTER_critical()taskEXIT_CRITICAL(),来实现FreeRTOS 系统的API函数临界区安全. 我们拆分一下这两个宏.

vPortEnterCritical() 调用 vPortRaiseBASEPRI() 函数

portFORCE_INLINE static void vPortRaiseBASEPRI( void )
{
    uint32_t ulNewBASEPRI;

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

实际的代码功能是 BASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY, Cortex-M 硬件实现了临界区的功能, 会屏蔽优先级数值大于 basepri 的中断.

configMAX_SYSCALL_INTERRUPT_PRIORITY 设置的默认值 (1 << (8-configPRIO_BITS)), 其中configPRIO_BITS 芯片厂家使用的优先级的位数.

进入临界区时, BASEPRI 的值设置为 configMAX_SYSCALL_interrupt_PRIORITY, 退出临界区时,设置BASEPRI为 0, 此时不会屏蔽任何中断。

Cortex-M 中断优先级

ARM Cortex-M 核心将中断优先级值存储在 其 8 位中断优先级寄存器中, 内核支持8位,也就是0~255的优先级. 每个厂家实现的时候,不会支持全部的优先级,只会支持部分.比如ST(意法半导体)公司的STM32F1xx和F4xx系列只使用了4位的优先级

中断优先级的前4位才有效果, 0x5F 和 0x50 的中断优先级是一样的. 硬件只会对比前4位.

逻辑优先级越低,则优先级数值越大. 比如 0x10 优先级大于 0x50

挂起OS调度

如果临界区段太长,不能简单地通过禁用中断来实现,则可以通过暂停调度程序来实现。然而,在调度器挂起时中断活动会使恢复(或“取消挂起”)调度器成为一个相对较长的操作,因此必须考虑在每种情况下使用哪种方法最好。

/* 挂起调度 */
vTaskSuspendAll();
/* 恢复调度 */
xTaskResumeAll();

通过暂停调度器实现的临界区仅保护代码区域不被其他任务访问。因为中断仍然处于启用状态。

当调度器挂起时,不能调用FreeRTOS API函数。

互斥锁(二进制信号量)

互斥锁是一种特殊类型的二进制信号量,用于控制对两个或多个任务共享的资源的访问。MUTEX这个词来源于“互斥”。

互斥锁和二进制信号量极为相似,但仍有一些细微差异: 互斥锁具有优先级继承机制, 但二进制信号量没有。 因此,二进制信号量是实现同步的更好选择(任务之间或任务与中断之间),也是实施简单互斥方面的更好选择。

configUSE_MUTEXES 必须在FreeRTOSConfig.h中设置为1,互斥锁才能可用。

SemaphoreHandle_t xSemaphoreCreateMutex( void )
xSemaphoreTake( SemaphoreHandle_t xSemaphore,TickType_t xTicksToWait );
xSemaphoreGive( SemaphoreHandle_t xSemaphore );

优先级倒置

H任务,M任务,L任务都需要持有同一互斥锁,下图可见H任务必须等待低优先级的M和L任务执行完成, H任务等待时间=M任务执行时间+L任务执行时间,高优先级的任务很难得到CPU时间,高优先级反而最得不到CPU时间,形成实际优先级的倒置.

优先级继承

优先级继承的工作原理是,暂时将互斥锁持有者的优先级提高到试图获取同一个互斥锁的优先级最高的任务的优先级。持有互斥锁的低优先级任务继承了等待互斥锁的任务的优先级。下图演示了这一点。互斥锁持有者的优先级在返回互斥锁时自动重置为初始值。

优先级继承是一种将优先级倒置负面影响最小化的方案,它并不修复优先级倒置,只是通过确保倒置总是有时间限制来减少其影响。但是,优先级继承使系统定时分析变得复杂.

H任务,M任务,L任务都需要持有同一互斥锁,H任务等待时间=L执行时间,M任务不会抢占L任务

发表评论