# 临界区简述

临界区指的是一个访问共用资源(例如:共用设备或是共用存储器)的程序片段,而这些共用资源又无法同时被多个线程访问的特性;当有线程进入临界区时,其他线程或是进程必须等待。总的概括来说就是在执行该程序片段区间,不允许其他东西干扰到。

像我们在 MCU 上面跑实时操作系统,一般都是单核单进程的,而一个进程可以拥有多个线程;FreeRTOS 是主要以抢占式任务调度为主(通过 PendSV 中断),以时间片轮转调度任务为辅(通过 SysTick 系统节拍器中断)的实时操作系统,并且可支持同等优先级切换,具体配置可以看 FreeRTOS 篇章之 FreeRTOSConfig.h 分析 ;而刚讲的线程其实就相当于我们用 xTaskCreate 函数创建的各种任务。在这里,我们要实现临界区操作,那么就相当于要保证当前所处在的需要保护的代码片段不要受其他任务(线程)的干扰(例如同等优先级的切换、中断的触发)。

在 FreeRTOS 中,实现临界区操作有临界段操作和调度器操作两种,一般常用临界段来实现实现临界区操作。


# 临界段的特性

临界段是提供互斥功能的一种非常原始的实现方法。临界段的工作仅仅是简单地把任务切换和在 configKERNEL_INTERRUPT_PRIORITYconfigMAX_SYSCAL_INTERRUPT_PRIORITY 之间的中断关掉 —— 依赖于具体使用的 FreeRTOS 移植。

临界段的使用必须只具有很短的时间,否则会反过来影响中断响应时间;在每次调用 taskENTER_CRITICAL() 之后,必须尽快地配套调用一个 taskEXIT_CRITICAL()

临界段嵌套是安全的,因为内核有维护一个嵌套深度计数。临界段只会在嵌套深度为 0 时才会真正退出 —— 即在为每个之前调用的 taskENTER_CRITICAL() 都配套调用了 taskEXIT_CRITICAL() 之后。

# 与临界段相关的 API 函数

功能API 函数其它
进入临界段taskENTER_CRITICAL()
taskENTER_CRITICAL_FROM_ISR()用于中断中
退出临界段taskEXIT_CRITICAL()
taskEXIT_CRITICAL_FROM_ISR( x )用于中断中
/**
 * task. h
 *
 * Macro to mark the start of a critical code region.  Preemptive context
 * switches cannot occur when in a critical region.
 *
 * NOTE: This may alter the stack (depending on the portable implementation)
 * so must be used with care!
 *
 * \defgroup taskENTER_CRITICAL taskENTER_CRITICAL
 * \ingroup SchedulerControl
 */
#define taskENTER_CRITICAL()		portENTER_CRITICAL()
#define taskENTER_CRITICAL_FROM_ISR() portSET_INTERRUPT_MASK_FROM_ISR()
/**
 * task. h
 *
 * Macro to mark the end of a critical code region.  Preemptive context
 * switches cannot occur when in a critical region.
 *
 * NOTE: This may alter the stack (depending on the portable implementation)
 * so must be used with care!
 *
 * \defgroup taskEXIT_CRITICAL taskEXIT_CRITICAL
 * \ingroup SchedulerControl
 */
#define taskEXIT_CRITICAL()			portEXIT_CRITICAL()
#define taskEXIT_CRITICAL_FROM_ISR( x ) portCLEAR_INTERRUPT_MASK_FROM_ISR( x )

# 临界段的使用

在 FreeRTOS 中,临界段一般是指宏 taskENTER_CRITICAL()taskEXIT_CRITICAL() 之间的代码区间,而在中断中则是 taskENTER_CRITICAL_FROM_ISR()taskEXIT_CRITICAL_FROM_ISR() 之间。

例如,在 FreeRTOS 篇章之二值信号量 中的 USART1_IRQHandler(void) 中断函数中,我们可以加入临界区操作,变成:

/************************************************************************/
/*            STM32F10x USART Interrupt Handlers                        */
/************************************************************************/
/**
  * @brief  This function handles USART1 global interrupt request.
  * @param  None
  * @retval None
  */
void USART1_IRQHandler(void)
{
    uint32_t ulReturn;
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
	
    /* 为了保证在对 DMA 数据进行处理不被打断,将此操作放入临界区 */
    ulReturn = taskENTER_CRITICAL_FROM_ISR();    // 进入临界段,可以嵌套
    /* 在 taskENTER_CRITICAL_FROM_ISR () 与 taskEXIT_CRITICAL_FROM_ISR () 之间或
       在 taskENTER_CRITICAL () 与 taskEXIT_CRITICAL () 之间并不会切换到其它任务,
       同时,在 configKERNEL_INTERRUPT_PRIORITY 至 configMAX_SYSCAL_INTERRUPT_PRIORITY 之间
       的中断将会被关掉,但对于优先级高于 configMAX_SYSCALL_INTERRUPT_PRIORITY 的中断
       并没有关掉,而且这些中断不允许访问 FreeRTOS API 函数. */
    if(USART_GetITStatus(EVAL_COM1, USART_IT_IDLE)!=RESET)
    {		
        DMA_Cmd(USART1_RX_DMA_CHANNEL, DISABLE);
        DMA_ClearFlag(DMA1_FLAG_TC5);
        Usart1.RxCounter = RxBUFFER_SIZE - DMA_GetCurrDataCounter(USART1_RX_DMA_CHANNEL);
        USART1_RX_DMA_CHANNEL->CNDTR = RxBUFFER_SIZE;
        DMA_Cmd(USART1_RX_DMA_CHANNEL, ENABLE);
				
        xSemaphoreGiveFromISR(xBinarySemaphore, &xHigherPriorityTaskWoken);
        portEND_SWITCHING_ISR(xHigherPriorityTaskWoken);
        USART_ReceiveData(EVAL_COM1);			// Clear IDLE interrupt flag bit
    }
  
    /* 退出临界段 */
    taskEXIT_CRITICAL_FROM_ISR( ulReturn );
//  if(USART_GetITStatus(EVAL_COM1, USART_IT_TXE) != RESET)
//  {
//    /* Write one byte to the transmit data register */
//    USART_SendData(EVAL_COM1, TxBuffer[TxCounter++]);
//    if(TxCounter == RxBUFFER_SIZE)
//    {
//      /* Disable the EVAL_COM1 Transmit interrupt */
//      USART_ITConfig(EVAL_COM1, USART_IT_TXE, DISABLE);
//    }
//  }
}

# 用挂起调度器来创建临界区

调度器总是在所有处于就绪态的任务中选择具有最高优先级的任务来执行的(这就是 FreeRTOS 的优先级调度算法)。

因此创建临界区也可以通过挂起调度器来实现;挂起调度器有些时候也被称为锁定调度器。

临界段保护一段代码区间不被其它任务或中断打断(此处的中断是指归 FreeRTOS 管理的中断)。通过挂起调度器实现的临界区只可以保护一段代码区间不被其它任务打断,因为这种方式下,中断是使能的(此处的中断是指所有的中断,包含归 FreeRTOS 管理的中断)。
如果一个临界区太长而不适合简单地关中断来实现,可以考虑采用挂起调度器的方式;但是唤醒(resuming, or un-suspending)调度器却是一个相对较长的操作,所以评估哪种是最佳方式需要结合实际情况。

利用 vTaskSuspendAll(); 把调度器挂起,这样可以停止上下文切换而没有关中断;如果某个中断在调度器挂起过程中要求进行上下文切换,则个这请求也会被挂起,直到调度器被唤醒后才会得到执行(即调用 xTaskResumeAll(); 恢复调度器)。

注意:在调度器处于挂起状态时(即 vTaskSuspendAll();xTaskResumeAll(); 之间),不能调用 FreeRTOS API 函数。

1、vTaskSuspend () API 函数

void vTaskSuspend( TaskHandle_t  xTaskToSuspend );

输入参数:

  • 需要挂起的某个任务的句柄,这个句柄是创建任务时所引用出来的;当传递空值句柄时,将导致当前调用任务暂停;当任务暂停时,将永远无法获得任何微控制器的处理时间,无论其优先级如何。

2、vTaskResume () API 函数

void vTaskResume( TaskHandle_t  xTaskToResume );

输入参数:

  • 需要恢复成就绪状态的任务的句柄

3、xTaskResumeFromISR () API 函数

BaseType_t  xTaskResumeFromISR( TaskHandle_t  xTaskToResume );

输入参数:

  • 需要恢复成就绪状态的任务的句柄。

返回参数:

  • 如果恢复任务,则为 pdTRUE ,否则为 pdFALSE 。ISR 使用它来确定是否需要在 ISR 之后进行上下文切换。

4、vTaskSuspendAll () API 函数

void vTaskSuspendAll( void );

说明:挂起调度器而不禁用中断。当调度器挂起时,上下文切换将不会发生。在调用 vTaskSuspendAll() 之后,调用任务将继续执行;在调用 xTaskResumeAll() 之前,不会有被切换出去的风险,直到对 xTaskResumeAll() 的调用完成。当调度器被挂起时,不能调用可能导致上下文切换的 API 函数(例如, vTaskDelayUntil()xQueueSend() 等)。

5、xTaskResumeAll () API 函数

BaseType_t  xTaskResumeAll( void );

返回参数:

  • 在调度器挂起过程中,上下文切换请求也会被挂起,直到调度器被唤醒后才会得到执行。如果一个挂起的上下文切换请求在 xTaskResumeAll() 返回前得到执行,则函数返回 pdTRUE 。在其它情况下, xTaskResumeAll() 返回 pdFALSE

说明:在调用 vTaskSuspendAll() 挂起调度程序活动后,恢复该活动。 xTaskResumeAll() 只恢复调度程序。它不会解除先前通过调用 vTaskSuspend() 挂起的任务。

嵌套调用 vTaskSuspendAll()xTaskResumeAll() 是安全的,因为内核有维护一个嵌套深度计数。调度器只会在嵌套深度计数为 0 时才会被唤醒 —— 即在为每个之前调用的 vTaskSuspendAll() 都配套调用了 xTaskResumAll() 之后

# 与调度器相关的 API

功能API 函数
启动调度器vTaskStartScheduler()
停止调度器vTaskEndScheduler()
挂起某个任务vTaskSuspend()
恢复某个任务vTaskResume()
恢复某个任务(用于中断)xTaskResumeFromISR()
挂起所有调度器vTaskSuspendAll()
恢复所有调度器xTaskResumeAll()