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