ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

RTOS 面试实战:临界段保护实现

RTOS 面试实战:临界段保护实现 一、场景引入在嵌入式项目开发中我们经常会遇到全局变量被多个任务、中断同时修改导致数据错乱的问题比如共享的传感器采样计数器在任务读取的同时被中断更新最终读到非预期的脏数据或者多个任务同时操作同一外设寄存器引发外设状态异常。这类问题的根源就是没有对访问共享资源的临界代码段做正确的保护是 RTOS 项目中最常见也最容易排查失误的 bug考察这个点本质是看候选人能不能写出线程安全、中断安全的 RTOS 业务代码。二、原理解析临界段指的是访问共享资源时不允许被其他任务或中断打断的代码段。RTOS 中临界段保护的核心实现逻辑是通过关闭内核调度 / 全局中断禁止更高优先级的抢占行为保证临界段代码执行的原子性基础实现分为两类一是任务级临界段仅关闭 RTOS 内核调度禁止任务抢占但允许中断响应适合仅在任务间共享、不被中断访问的资源二是中断级临界段直接关闭全局中断或关闭优先级阈值以上的中断同时禁止任务和中断抢占适合会被中断和任务同时访问的资源嵌套支持为了避免多层临界段嵌套时内层退出直接打开中断 / 调度实现时必须在进入临界段前保存当前的中断掩码 / 调度锁状态退出时恢复原状态而不是直接强制打开中断注意事项临界段代码必须尽可能短避免过长时间关中断导致中断响应延迟超标甚至触发硬件看门狗复位。三、三道递进追问及答案追问 1基础追问临界段保护和互斥锁都可以保护共享资源两者有什么区别临界段是通过关调度 / 关中断实现保护时间极短支持中断级场景但不能阻塞不能用于耗时操作互斥锁是基于内核调度实现保护过程允许任务调度和中断响应适合保护耗时较长的资源但只能用于任务间不适用中断场景且可能引发优先级翻转问题。追问 2进阶变形为什么临界段实现要保存原中断状态而不是进入关中断、退出直接开中断为了支持临界段嵌套如果外层已经关闭了中断内层临界段退出时直接开中断会破坏外层的临界段保护导致外层临界段被意外打断。通过保存原状态、退出恢复的方式可以保证嵌套场景下只有最外层临界段退出时才会恢复到最初的中断状态。追问 3实战综合现在有一个共享队列会被多个任务和串口接收中断同时读写应该用什么临界段保护方式如果临界段执行时间太长会有什么问题必须用中断级临界段也就是进入队列操作前关全局中断或关优先级高于串口的中断操作完成后恢复原中断状态。如果临界段太长会导致串口等低于关闭优先级阈值的中断响应延迟超标出现数据丢包如果超过看门狗超时时间还会触发系统复位。四、最小验证实验实验环境Keil MDK 5、STM32F103 开发板、FreeRTOS V10.4.6实验代码#include stm32f10x.h #include FreeRTOS.h #include task.h #include stdio.h // 共享全局变量会被任务和中断同时访问 volatile uint32_t g_shared_counter 0; // 串口1初始化实现用于打印输出 void USART1_Init(uint32_t baudrate) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE); // USART1_TX PA9 GPIO_InitStructure.GPIO_Pin GPIO_Pin_9; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_Init(GPIOA, GPIO_InitStructure); // USART1_RX PA10 GPIO_InitStructure.GPIO_Pin GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); USART_InitStructure.USART_BaudRate baudrate; USART_InitStructure.USART_WordLength USART_WordLength_8b; USART_InitStructure.USART_StopBits USART_StopBits_1; USART_InitStructure.USART_Parity USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART1, USART_InitStructure); USART_Cmd(USART1, ENABLE); } // 重定向printf到USART1 int fputc(int ch, FILE *f) { USART_SendData(USART1, (uint8_t) ch); while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); return ch; } void HardFault_Handler(void) { while(1); } void SysTick_Handler(void) { if(xTaskGetSchedulerState() ! taskSCHEDULER_NOT_STARTED) xPortSysTickHandler(); } // 定时器2中断模拟中断里修改共享变量 void TIM2_IRQHandler(void) { if(TIM_GetITStatus(TIM2, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); g_shared_counter; } } // 测试任务1无保护读取共享变量 void task1(void *arg) { uint32_t val; while(1) { // 模拟非原子读取通过人为插入延时制造读取和校验的时间差即使在32位机上也能复现并发问题 val g_shared_counter; for(volatile int i0; i100; i); if(val ! g_shared_counter) { printf(无保护读取异常val%d, 实际%d\r\n, val, g_shared_counter); } vTaskDelay(pdMS_TO_TICKS(100)); } } // 测试任务2临界段保护读取共享变量 void task2(void *arg) { uint32_t val; while(1) { // 任务上下文调用任务级中断临界段API保存原中断状态 taskENTER_CRITICAL(); val g_shared_counter; for(volatile int i0; i100; i); if(val ! g_shared_counter) { printf(临界段读取异常val%d, 实际%d\r\n, val, g_shared_counter); } // 退出临界段恢复原中断状态 taskEXIT_CRITICAL(); vTaskDelay(pdMS_TO_TICKS(100)); } } // 定时器2初始化1ms中断一次 void tim2_init(void) { RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); TIM_TimeBaseInitTypeDef tim_cfg {0}; tim_cfg.TIM_Prescaler 7200 - 1; tim_cfg.TIM_CounterMode TIM_CounterMode_Up; tim_cfg.TIM_Period 10 - 1; // 72M/7200/10 1kHz1ms中断 TIM_TimeBaseInit(TIM2, tim_cfg); TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); NVIC_EnableIRQ(TIM2_IRQn); TIM_Cmd(TIM2, ENABLE); } int main(void) { USART1_Init(115200); // 初始化串口1用于打印 tim2_init(); // 先创建任务1测试无保护场景注释后创建任务2测试临界段场景 xTaskCreate(task1, task1, 128, NULL, 2, NULL); // xTaskCreate(task2, task2, 128, NULL, 2, NULL); vTaskStartScheduler(); while(1); }操作步骤先编译运行创建 task1 的版本观察串口打印注释掉 task1 创建代码打开 task2 创建代码重新编译下载运行观察串口打印。预期结果运行 task1 时会频繁打印「无保护读取异常」的日志说明没有临界段保护时读取与校验的间隙被 TIM2 中断打断修改了变量导致数据不一致运行 task2 时没有任何异常日志打印说明临界段保护生效读取与校验的过程中不会被中断打断数据访问原子性得到保证。五、实战总结仅任务共享的资源用任务级临界段关调度中断和任务共享的资源用中断级临界段关中断 状态保存临界段代码必须尽可能短避免引发中断响应延迟、看门狗复位等问题嵌套临界段必须采用「保存原状态 - 退出恢复」的逻辑禁止直接开中断 / 调度导致保护失效临界段与互斥锁适用场景不同临界段适合短时间、中断级保护互斥锁适合长时间、仅任务间的资源保护。
返回列表