实时操作系统RTOS背景什么是实时操作系统(RTOS)常见实时操作系统多线程处理与线程间通信及同步线程线程间通信信号量(Semaphore)二值信号量(Binary Semaphore)计数信号量(Counting Semaphore)互斥信号量Mutex信号量应用场景消息队列(Message Queue)非互锁单向数据通信互锁单向数据通信互锁双向数据通信管道(Pipe)典型用途与消息队列的区别使用场景示例事件Event使用示例设置、等待和清除事件标志条件变量核心要点总结背景现代微控制器种类繁多规模和复杂度各异从 Flash 小于 10K、RAM 小于 2K 的 8 位微控制器到与 1G 内存接口的多处理器 64 位微控制器。经典的裸机多线程处理通常结合了一个处理非时间关键工作的超级循环而时间关键工作则在中断处理程序中完成。联网设备往往运行着相对复杂的网络协议栈并且需要以安全的方式进行标准的裸机方法会遇到困难。什么是实时操作系统(RTOS)RTOS 中的 OS 代表操作系统RT 代表实时。操作系统可以被视为一种提供服务的软件这些服务可用于开发需要同时处理多个线程的应用程序。操作系统的核心是调度器其工作是决定接下来运行哪个线程。协作式一个线程会一直运行直到它决定挂起当前操作并将控制权移交给调度器由调度器决定接下来运行哪个线程。抢占式线程可以在任何时候被操作系统抢占。抢占可能发生的原因包括一个高优先级线程已就绪可以运行或者正在运行的线程需要访问某个当前不可用的资源因为该资源正被另一个任务使用。硬实时如果系统响应时间超过指定时长则视为错误。软实时系统响应时间是从统计意义上解释的即大多数情况下都能满足所需的时间完成限制但偶尔也会出现不满足的情况。常见实时操作系统市面上 RTOS 目前有上百种如 FreeRTOS、RT-Thread、ThreadX、VxWorks、Zephyr 等。以下仅简单介绍有兴趣可自行搜索。每个 RTOS 各有优势请根据实际项目选择。FreeRTOS 是最广泛使用的 RTOS 平台之一以其简洁性和小巧的资源占用而闻名。它是基础嵌入式应用的绝佳选择尤其适用于资源受限的微控制器。RT-Thread 是极为优秀的国产操作系统软件包丰富中文社区活跃对国产 MCU 支持力度很高可定制化服务。ThreadX 专为高性能应用优化经过安全认证广泛应用于商业产品中。它提供强大的内核以及内存保护和实时追踪等高级功能。VxWorks 是一款高端 RTOS专为航空航天和汽车等行业的安全关键型应用而设计。VxWorks 复杂且资源消耗大因此不太适用于低功耗、资源受限的设备。Zephyr 由 Linux 基金会管理核心设计理念是与硬件无关支持 ARM、RISC-V、x86、ARC 等多种架构。内核基于抢占式优先级调度模型构建集网络、文件系统和电源管理于一体的完整嵌入式平台。其采用了设备树device tree和 Kconfig 工具。开发由 ST、Intel、Nordic、NXP 等主要行业厂商以及社区共同推动是嵌入式系统中面向未来的可靠选择。多线程处理与线程间通信及同步线程线程是一个独立的执行任务它必须与其他线程竞争 CPU 执行时间。并发错觉能够实现是因为线程间的上下文切换速度很快。然而这种切换并非没有代价因为在线程间切换时存储和恢复执行状态会产生上下文切换开销。许多 RTOS 内核都提供了可控制线程调度的 API。此类 API 包含用于挂起、恢复、延迟等线程的方法以及获取和设置线程优先级的方法。无限循环线程可能遵循如下模式voidInfiniteLoopTask(void){/* 初始化步骤 */InitHardware();InitPeripherals();/* 无限循环 */for(;;){/* 任务主体代码通常包含一个或多个阻塞调用 */ProcessData();vTaskDelay(pdMS_TO_TICKS(100));/* 阻塞等待让出CPU */}}线程间通信掌握 RTOS 编程需要理解 RTOS 支持的各种线程间通信方法例如互斥锁、信号量、队列、消息队列和工作队列。每个实时操作系统都提供信号量和互斥锁这是最基本的同步与通信机制。信号量(Semaphore)信号量是一个内核对象一个或多个执行线程可以获取或释放该对象以实现同步或互斥。应避免使用二值信号量进行互斥而应改用互斥锁。在现代 RTOS 中互斥锁实现了优先级继承机制可防止出现优先级反转情况即低优先级线程因持有高优先级线程所需的资源从而实际上阻塞了高优先级线程。现代互斥锁提供的另一个重要特性是它们可递归运行从而防止某些死锁情况的发生。二值信号量(Binary Semaphore)// 二值信号量典型使用场景任务同步sem_tbinary_sem;// 定义二值信号量voidhigh_prio_task(void*arg){while(1){// 等待低优先级任务释放信号量sem_wait(binary_sem);// 若信号量为0则阻塞// 收到同步信号执行后续处理process_data();}}voidlow_prio_task(void*arg){while(1){collect_data();// 采集数据sem_signal(binary_sem);// 释放信号量唤醒高优先级任务vTaskDelay(pdMS_TO_TICKS(100));}}计数信号量(Counting Semaphore)// 计数信号量典型使用场景ISR卸载中断处理到任务sem_tcount_sem;// 定义计数信号量初始计数值为0最大计数值为10// 中断服务函数ISRvoidtimer_isr(void){// 快速完成关键中断处理clear_interrupt_flag();// 发送信号将剩余处理卸载到任务sem_signal_from_isr(count_sem);// 计数信号量1不阻塞}// 处理任务voidprocess_task(void*arg){while(1){// 等待信号量若计数值为0则阻塞sem_wait(count_sem);// 计数信号量-1// 执行剩余的中断处理工作handle_timer_event();}}互斥信号量Mutex互斥锁是一种特殊的二值信号量具有所有权、递归锁定、任务删除安全性和避免优先级反转行为等属性。互斥锁用于互斥访问共享资源而普通信号量用于线程同步。下面通过一个典型场景演示互斥锁的使用#includeFreeRTOS.h#includesemphr.h// 创建互斥锁句柄SemaphoreHandle_t xMutex;// 共享资源如串口、全局变量charshared_buffer[128];voidvTaskWriter(void*pvParameters){for(;;){// 尝试获取互斥锁等待 100msif(xSemaphoreTake(xMutex,pdMS_TO_TICKS(100))pdTRUE){// 进入临界区安全写入共享资源snprintf(shared_buffer,sizeof(shared_buffer),Writer 写入数据: %d,(int)xTaskGetTickCount());printf([Writer] 已写入: %s\n,shared_buffer);// 释放互斥锁xSemaphoreGive(xMutex);}else{printf([Writer] 获取互斥锁超时\n);}vTaskDelay(pdMS_TO_TICKS(500));}}voidvTaskReader(void*pvParameters){for(;;){if(xSemaphoreTake(xMutex,pdMS_TO_TICKS(100))pdTRUE){// 安全读取共享资源printf([Reader] 读取到: %s\n,shared_buffer);xSemaphoreGive(xMutex);}else{printf([Reader] 获取互斥锁超时\n);}vTaskDelay(pdMS_TO_TICKS(300));}}voidmain_task(void){// 创建互斥锁初始为解锁状态xMutexxSemaphoreCreateMutex();if(xMutex!NULL){xTaskCreate(vTaskWriter,Writer,1024,NULL,1,NULL);xTaskCreate(vTaskReader,Reader,1024,NULL,1,NULL);vTaskStartScheduler();}}关键特性说明所有权只有获取互斥锁的线程才能释放它其他线程无法强行释放。递归锁定同一线程可多次获取同一互斥锁需调用xSemaphoreTakeRecursive每次获取必须对应一次释放。优先级继承当高优先级线程等待低优先级线程持有的互斥锁时内核临时提升低优先级线程的优先级避免优先级反转。中断服务函数限制互斥锁不应在 ISR 中使用ISR 中应使用二值信号量或计数信号量因为互斥锁的优先级继承机制依赖任务调度而 ISR 中不允许阻塞操作。信号量应用场景二值信号量同步两个优先级不同的线程为同步目的进行通信但未交换任何数据。初始状态二值信号量为不可用值为0。高优先级线程先运行并在某个时刻尝试因获取信号量被阻塞。低优先级线程获得运行后某个时刻释放信号量高优先级线程被唤醒同步。计数信号量同步信号线程的执行频率高于处理线程并且信号线程的运行优先级高于处理线程。信号线程通过递增信号量来发送信号。当处理线程尝试获取信号量如果信号量计数器为0则阻塞否则获取并原子性地将信号量计数-1。信号线程可能以突发方式发送信号处理线程有机会在两次突发之间追赶上来。例如当中断触发时完成关键的中断处理然后通过在计数信号量上发送信号将处理中断所需的剩余处理任务卸载出去。随后处理线程即可解除阻塞并执行剩余的中断处理工作。互斥信号量同步同一时刻只有一个线程能够访问共享资源某个线程先获取该信号量后可对共享资源访问。任何其他获取该信号量的线程都将阻塞。当持有信号量的线程使用完共享资源释放信号量。之前因获取信号量而被阻塞的线程将被解除阻塞并可以依次使用该共享资源。消息队列(Message Queue)消息队列是供线程以及中断服务程序用于通信和同步同时还能传递数据。该机制使用缓冲区临时保存来自发送者的消息直到目标接收者能够读取它们。它解耦了发送任务和接收任务。当创建消息队列时通常会为其分配队列控制块、队列名称、唯一标识符以及一个或多个缓冲区。分配的内存量将取决于队列长度和最大消息长度等因素。阻塞内置于消息队列中即当消息队列已满时发送任务会阻塞当消息队列为空时读取任务会阻塞。在实践中消息队列可以以多种方式使用例如非互锁单向通信、互锁单向通信和互锁双向通信。非互锁单向数据通信中断服务程序ISR通常使用非互锁单向数据通信模式接收线程运行并等待消息队列。当ISR被触发时它会以非阻塞方式将一个或多个消息放入消息队列。在实现代码时需要考虑当消息队列已满时消息可能会丢失或被覆盖的可能性。以下示例展示了一个 ISR 向接收线程发送数据的典型实现/* 消息队列句柄 */msg_q_tg_data_queue;/* 接收线程入口 */voidreceiver_task(void*arg){uint32_tdata;while(1){/* 阻塞等待消息 */msg_q_recv(g_data_queue,data,WAIT_FOREVER);/* 处理接收到的数据 */process_data(data);}}/* 中断服务程序 */voidISR_Handler(void){uint32_tsensor_valread_sensor();/* 非阻塞发送队列满时丢弃 */msg_q_send(g_data_queue,sensor_val,NO_WAIT);}在此示例中receiver_task持续阻塞在消息队列上等待数据而 ISR 在触发时以NO_WAIT方式发送传感器数据。若队列已满消息将被丢弃因此需要根据实际数据产生频率合理设置队列深度。互锁单向数据通信发送线程发送一条消息并等待确认消息是否已被接收。如果消息因某种原因未被正确接收则可以重新发送。此模式的主要用途是实现一种闭环同步形式使发送线程和接收线程彼此以锁步方式运行。实现这种模式有多种方法。一种方式是使用一个发送线程、一个消息队列长度为1、一个接收线程和一个二值信号量。在此实现中二值信号量的初始值为0。发送线程向消息队列发送一条消息并在二值信号量上阻塞。接收线程接收消息并释放二值信号量这将解除被阻塞的发送线程使其能够发送下一条消息。在此实现中信号量充当对发送者的简单确认表明可以发送下一条消息。以下示例展示了基于消息队列和二值信号量的互锁单向数据通信实现/* 消息队列句柄队列长度为1 */msg_q_tg_cmd_queue;/* 二值信号量句柄初始值为0 */sem_tg_ack_sem;/* 发送线程 */voidsender_task(void*arg){uint32_tcmd0;while(1){cmdgenerate_command();/* 发送命令到队列 */msg_q_send(g_cmd_queue,cmd,WAIT_FOREVER);/* 等待接收线程确认 */sem_wait(g_ack_sem);/* 确认收到继续发送下一条 */}}/* 接收线程 */voidreceiver_task(void*arg){uint32_tcmd;while(1){/* 接收命令 */msg_q_recv(g_cmd_queue,cmd,WAIT_FOREVER);/* 执行命令 */execute_command(cmd);/* 释放信号量通知发送线程可以继续 */sem_post(g_ack_sem);}}在此实现中sender_task发送命令后阻塞在g_ack_sem上直到receiver_task处理完命令并释放信号量才继续发送下一条。这种锁步机制确保了每条消息都被确认后才发送下一条适用于需要可靠交付的场景。互锁双向数据通信互锁双向数据通信涉及两个线程和两个消息队列。同步过程的具体细节取决于需要交换的数据类型。例如双向数据交换需要两个消息队列而在仅需简单确认的情况下则可以使用信号量。以下示例展示了客户端与服务器之间通过两个消息队列进行双向数据交换的完整实现/* 两个消息队列一个用于请求一个用于响应 */msg_q_tg_req_queue;msg_q_tg_resp_queue;/* 客户端任务 */voidclient_task(void*arg){request_treq;response_tresp;while(1){/* 构造请求 */req.idget_next_id();req.dataprepare_request_data();/* 向服务器发送请求 */msg_q_send(g_req_queue,req,WAIT_FOREVER);/* 等待服务器响应 */msg_q_recv(g_resp_queue,resp,WAIT_FOREVER);/* 处理响应结果 */handle_response(resp);}}/* 服务器任务 */voidserver_task(void*arg){request_treq;response_tresp;while(1){/* 接收客户端请求 */msg_q_recv(g_req_queue,req,WAIT_FOREVER);/* 处理请求并生成响应 */resp.idreq.id;resp.codeprocess_request(req);/* 将响应发送回客户端 */msg_q_send(g_resp_queue,resp,WAIT_FOREVER);}}在此实现中client_task通过g_req_queue发送请求后阻塞等待g_resp_queue上的响应server_task从请求队列接收消息处理完成后将结果通过响应队列发回。两个消息队列分别承载不同方向的数据流实现了完整的双向通信闭环。管道(Pipe)管道是一种单向的、基于字节流的线程间通信机制类似于文件系统中的管道概念。它提供了一种简单的方式让一个线程生产者向另一个线程消费者发送数据数据以字节流的形式在管道中传输。典型用途数据流处理将数据从一个处理阶段传递到下一个阶段形成流水线处理模式。驱动与应用程序通信底层驱动将采集到的原始数据通过管道发送给上层应用线程处理。日志输出将多个线程的日志信息通过管道汇集到专门的日志处理线程统一输出。下面是传感器驱动线程通过管道向主控处理线程传输温度数据的单向数据流示意图传感器驱动线程采集温度数据写入管道管道字节流主控处理线程读取温度数据处理温度数据与消息队列的区别特性管道(Pipe)消息队列(Message Queue)数据单元字节流无固定消息边界固定大小的消息单元通信方向单向一个读端、一个写端支持单向和双向数据格式原始字节需自行解析可携带消息类型和优先级适用场景连续数据流、音频/传感器数据离散消息、命令/事件传递使用场景示例假设有一个温度传感器驱动每隔 100ms 采集一次温度数据4 字节浮点数。驱动线程将数据写入管道主控线程从管道读取并处理// 伪代码示例voidsensor_task(void*arg){pipe_t*pipe(pipe_t*)arg;floattemperature;while(1){temperatureread_sensor();// 读取传感器pipe_write(pipe,temperature,sizeof(temperature));// 写入管道vTaskDelay(pdMS_TO_TICKS(100));}}voidprocess_task(void*arg){pipe_t*pipe(pipe_t*)arg;floattemp;while(1){if(pipe_read(pipe,temp,sizeof(temp),portMAX_DELAY)sizeof(temp)){// 成功读取到温度数据进行处理process_temperature(temp);}}}事件Event事件对象是一个二进制事件标志的集合通常为32位每bit都与某个特定事件相关联。这些位可以被设置或清除线程利用它们来检查特定事件是否发生。例如ISR可以在事件对象中设置一个位以通知任务某个特定事件已发生。任务可以执行由事件寄存器位标志的组合使用与、“或”指定的条件检查。事件检查策略可以是不等待、永久等待或带超时等待。每个事件对象通过其内存地址引用一个或多个线程可以等待某个事件对象直到该事件对象接收到指定的一组事件。当新事件被传递到事件对象时所有等待条件得到满足的线程会同时变为就绪状态并开始运行。使用示例设置、等待和清除事件标志以下示例展示两个任务通过事件对象进行同步任务 A 等待按键事件和定时事件任务 B或 ISR负责设置事件标志。// 伪代码示例event_tevent_obj;// 定义事件对象#defineEVENT_KEY_PRESS(10)// bit0按键事件#defineEVENT_TIMEOUT(11)// bit1定时事件voidkey_scan_task(void*arg){while(1){if(key_pressed()){event_set(event_obj,EVENT_KEY_PRESS);// 设置按键事件标志}vTaskDelay(pdMS_TO_TICKS(10));}}voidmain_task(void*arg){uint32_tevents;// 等待按键事件或定时事件逻辑或eventsevent_wait(event_obj,EVENT_KEY_PRESS|EVENT_TIMEOUT,EVENT_OR,portMAX_DELAY);if(eventsEVENT_KEY_PRESS){// 按键事件发生处理按键handle_key_press();event_clear(event_obj,EVENT_KEY_PRESS);// 清除按键事件标志}if(eventsEVENT_TIMEOUT){// 定时事件发生执行定时任务handle_timeout_task();event_clear(event_obj,EVENT_TIMEOUT);// 清除定时事件标志}}上述示例中event_set()用于设置事件标志位event_wait()等待指定的事件组合支持逻辑与/或event_clear()在事件处理完成后清除标志位。这种机制非常适合多事件驱动的任务调度场景。条件变量条件变量是一种与某些共享资源相关联的内核对象。它被一个线程用来等待直到其他线程将共享资源设置为某个指定条件。该条件通过判断某种逻辑表达式来推断。当线程检查条件变量时它必须对该变量具有独占访问权因此互斥锁与条件变量结合使用。线程必须先获取互斥锁然后才能判断。如果判断为假任务将阻塞直到达到所需条件。其实现方式是释放互斥锁和阻塞等待条件的操作是一个原子不可分割操作。以下 C 语言代码片段基于 POSIX 线程 API说明了使用条件变量的常见方式// 共享资源状态intshared_resource_busy0;pthread_mutex_tmutexPTHREAD_MUTEX_INITIALIZER;pthread_cond_tcondPTHREAD_COND_INITIALIZER;// Task 1: 等待资源空闲voidtask1_wait_for_resource(void){pthread_mutex_lock(mutex);while(shared_resource_busy){// 原子操作释放互斥锁 阻塞等待条件变量pthread_cond_wait(cond,mutex);}// 条件满足标记资源为忙碌shared_resource_busy1;pthread_mutex_unlock(mutex);}// Task 2: 释放资源并通知等待者voidtask2_release_resource(void){pthread_mutex_lock(mutex);shared_resource_busy0;// 唤醒一个等待该条件变量的线程pthread_cond_signal(cond);pthread_mutex_unlock(mutex);}由于条件变量上的信号在没有线程等待时会丢失任务在等待条件变量前应检查所需条件是否已满足并且在被唤醒后也应检查所需条件是否已满足。上述代码中的while循环正是为了应对虚假唤醒spurious wakeup和信号丢失的情况确保线程被唤醒后重新判断条件是否真正满足。核心要点总结RTOS 中的线程间通信与同步机制是构建可靠嵌入式系统的基石各机制各有侧重信号量Semaphore最基础的同步原语二值信号量用于任务同步与互斥计数信号量管理有限资源互斥信号量Mutex解决优先级反转问题。消息队列Message Queue实现线程间结构化数据传递支持非互锁、互锁单向及互锁双向等多种通信模式是任务解耦的核心手段。管道Pipe面向字节流的轻量通信方式适合传感器数据采集、日志输出等场景与消息队列互补。事件Event多条件触发机制一个任务可等待多个事件标志的组合适合复杂状态机驱动场景。条件变量Condition Variable与互斥锁配合实现线程对共享资源的条件等待避免忙等浪费 CPU。选择何种机制取决于具体场景同步优先选信号量数据传递选消息队列或管道多条件触发选事件复杂共享状态选条件变量。