RTOS(Real-time Operating System,实时操作系统)是以任务运行时间管理作为核心框架的操作系统。
RTOS常常以嵌入到微机(微控制器/微处理器/其他微机芯片)的形式存在,用于帮助微机完成实时性要求严格的工作环境。
我们常用的电脑、手机所使用的操作系统均为“分时操作系统”,优先保证每个应用程序的功能可被完整实现,进而保证程序的完整性、数据的准确性以及用户交互的实时性。分时操作系统允许用户在硬件性能范围内,实现高负荷软件顺利运行(比如玩游戏、渲染视频或设计)以及海量程序或服务的并发运行(比如网页浏览、办公软件和社交软件一起运行)。
实时操作系统更多用于对程序响应周期提出严格要求的运行环境,比如微机(常见为MCU微控制器)、人身安全保护处理器(自动生产线上的急停按钮或者门锁传感器)、重要功能模块(比如汽车的制动监测系统)等。为了尽可能削减多余干扰因素,在一个大型系统中,实时操作系统一般都以嵌入到多枚微机芯片的形式出现。
分时操作系统以及实时系统,都以处理器高速计算的形式达成对人机界面的构建、程序的运行以及功能的实现;不论是哪种操作系统,每个程序在单位运行时间都存在着允许活动的时间段(亦称“时间片”),这些活动时间经过操作系统以及用户的联合控制,保证每个程序都能高效执行,呈现出计算机以及微机能处理多个程序以及功能的效果。
由用户编写并且要求处理器如实进行的步骤序列就被称为“程序”。
为区分用户程序与RTOS系统专用程序,我们常说的“用户程序”一般都被称为“任务”。
不同类型的任务具有不同的编程形式,并且支持用户程序管理工作模式和优先级,有时候程序员还需要根据任务状况分配内存,防止其它程序运行效率遭到降低。
在裸机环境下的子函数不需要添加跳转指令,因为处理器常常会在调用子函数之后,在编译后的“裸机程序”自动回到调用前的父函数。
编译器会在每个函数的末尾段,自动填充程序跳转指令到最终的机器码文件(待烧录程序)中,因为每个函数运行结束之前的判断策略以及结束之后的动作都是固定不变的,
正所谓“善战者无赫赫之功”,这种“傻瓜式”函数切换策略容易让RTOS新手忘却了编译器自动添加的必要步骤。
然而在RTOS中,尽管RTOS自动保存了函数的起始位置,但任务/函数的切换顺序是随时可变的,因此不能依赖编译器自动给定的程序跳转指令去跳转其他任务,需要用户在任务中提供“任务的终点”让RTOS确认任务切换时机。
不能确认任务切换时机,对于RTOS而言是最为致命的,因为RTOS不能获取编译器给定“任务跳转”的位置。如果一个任务不给RTOS设立一个任务结束的跳转位置,就容易因为跳转到编译器自动添加的指令,跑飞到其它程序。
如果存在需要动态内存分配的任务、烧录前加密、指定绝对内存位置等多次更改函数位置的步骤,可能会陷入比“跑飞”后果还严重的“堆栈溢出”,轻则导致处理器停机,重则导致系统结构解体。
RTOS通常会为用户留下用于任务管理的API函数,常用的任务管理功能如下:
不同的RTOS会有不同的优先级仲裁算法,有些RTOS的优先级数字越大、等级越低,有些则截然相反。
在同属“就绪”状态的任务队列中,优先级高的任务会被抢先运行,运行顺序从优先级高的任务到优先级低的任务。
高优先级任务执行完成或超时切换后,将从任务列表中寻找次高优先级任务,直到最低优先级任务执行完毕或之前执行过的高优先级任务解除阻塞。
系统空闲进程固定为最低优先级;系统任务管理器、任务切换服务以及系统心跳中断服务固定为RTOS的最高优先级。
不同的RTOS会区分不同的任务状态,常见的任务状态如下:
熟悉Arduino编程环境的程序员容易把用户程序分为两种,一种是“单步程序”,一种是“循环程序”。在使用RTOS时,程序员可以划分不定式的任务形式,而常用的任务形式有以下四种:“有限次数任务”、“无限循环任务”、“先处理协作任务”和“后处理协作任务”。
编写有限次数任务时,需要确定任务循环次数(一般为1次),抵达任务循环次数之后,为了避免RTOS进入任务切换时机后,再次执行跑飞到其它程序,需要在任务切换指令之前,将该任务注销(删除)。
一般情况下,循环程序按照单独执行的形式以一个大的无限循环语句包裹起来,不过要注意的是,还需要在循环语句的最后一句中添加“任务跳转位置”或者“阻塞条件”,以此赶在RTOS超时检测前完成任务切换,避免对RTOS的实时性造成破坏。
循环程序的一种特殊情形,通过协调信号与动作之间的先后顺序,以及需要联动协作的任务以完成同步处理。
RTOS会为用户提供专用的内存工具,这些内存工具由RTOS自动管理,同时会绑定等候状态变化的“阻塞中”任务列表。
用户既可以通过API添加内存工具,也可以在任务中添加面向指定内存工具的“阻塞条件”,由RTOS在运行其他任务的过程中,代为等候内存工具的变化。
内存工具状态发生改变时,RTOS会根据任务阻塞条件,将符合“阻塞条件”的任务升级为“准备就绪”状态。
一般情况下,这些内存工具用于任务之间的相互协调,因为状态变化往往伴随着一些任务需要抢先执行,或者阻止某些高优先级任务突然运行,用于实现“线程安全”,避免任务发生共用资源的冲突。
常见的内存工具有“信号量”、“事件标志组”、“数据队列”和“即时信箱”。
信号量(Semaphore)是实现基本任务协调的内存工具。相比于裸机系统,任务对信号量的读写严格按照RTOS给定的任务优先级顺序,并且允许任务等候对应信号量抵达“已用状态”。
适用场合:软件状态开关(二值信号量)、硬件使用状况指示器(互斥信号量)、缓存用量指示器(计数器信号量)、公共数据缓存空间(互斥计数信号量)
FreeRTOS动画演示信号量操作过程 |
---|
|
任务可以对信号量进行以下操作:
🛠️创建 🗑️删除 ➕“给出”(计数递增) ➖“取走”(计数递减)
⏳等信号量被给出后,解除阻塞并取走信号量
🔍查询信号量数据 🔍👤查询占有者(仅限“互斥”) 🔒锁定/解锁信号量(禁止“互斥”)
信号量在RTOS中会出现四种模式,分别是“二进制信号量”、“计数式信号量”、“互斥二值信号量”、“互斥递归信号量”。
事件标志组 是“二值信号量”的进阶版本。RTOS既能保证各任务对事件标志的读写操作有序可控,又能通过事件标志实现各种条件下的自动处理(自动化)。
适用场合:状态机、并发事件呼叫器
任务可以对事件标志组执行以下操作:
🛠️创建一组事件标志 🗑️删除一组事件标志 ➕置位1个事件标志 ➖复位1个事件标志 🔍⊙ 检查事件标志/标志组状态
⏳等分组中的一个/多个事件标志被置位(可选与/或逻辑)
⏏️等事件标志后自动清除标志位
✏️⏩批处理事件标志组 🔐管理事件标志组可用标志位长度
数据队列(Queue)是用于解决数据缓冲的内存工具。相比于裸机系统自行编写的“FIFO”队列,RTOS也会严格调整任务的读写顺序,避免数据操作顺序不可控的问题。
适用场合:软件通信/函数传递缓冲区(顺序队列)、操作历史记录器(逆序队列)
FreeRTOS动画演示RTOS队列操作过程 |
---|
|
任务可以对数据队列执行以下操作:
🛠️创建 🗑️删除 ✏️▶️排队写入数据(FIFO) ▶️📬读出数据 ✏️⏩插队写入数据
⏳等队列有数据被写入
🔍% 查询队列已用量/可用量 🔍? 查询队列是否为空/为满 ✏️◀️排队写入数据(LIFO) 🗑️🗞️清空/重置队列
✏️🔄️覆写即将读出的数据 📬👁️🗨️偷瞄数据(Peek,读出但保留数据)
即时信箱(Mailbox)亦称“通知”、“短信”,是直接向任务发送一行数据的内存工具。RTOS会把任务请求的数据发送在目标任务的任务管理块,不仅大幅减少操作步骤,而且不需要用户额外创建内存工具,因为即时信箱是跟随任务创建而共生的。
适用场合:代替传入函数将数据传到任务中、严格控制内存大小的应用场合、上述内存工具替代品
任务可以进行与即时信箱相关的以下操作:
📤向任务的信箱发送/替换通知 📥接收来自信箱的通知
📥⏳等候信箱邮件并进入阻塞状态 📤🔔发送邮件并解除其他任务的阻塞状态
🪄对任务信箱进行类信号量管理 🗑️清除任务信箱 📤📚发送通知并构建通知历史 🔍 读取其他任务的信箱内容
不使用上述工具所创建的内存均属于“非RTOS管辖内存”。