基于Ravenscar配置文件的Ada运行时系统实现
1. 系统架构概述
在系统架构中,AppSwitch™ 的顶层是后台引擎(Background Engine,BE),它作为转发引擎的备用交换机,同时也是 AppSwitch™ 的“任务控制”计算机和网络管理员的用户界面。在“任务控制”角色中,BE 有众多“代理”来监控和控制 AppSwitch™ 的各个方面;在用户界面角色中,BE 提供基于 Web 的界面,可在网络中的任何计算机上显示。
系统中有三个可编程处理器:后台引擎、转发引擎和广域网(WAN)子系统处理器。后台引擎和转发引擎的处理器均基于 ARC 内核(原 Argonaut RISC 内核),该内核最初为计算机游戏开发,是一种针对嵌入式应用的 RISC 微处理器解决方案,以 VHDL 宏的形式提供,并针对 AppSwitch™ 的特定需求进行了定制。WAN 子系统由 MPC860 提供动力,它是摩托罗拉 QUICC 系列中基于 PPC 的成员,包含 CPU 和用于处理通信的额外硬件功能。
转发引擎和 WAN 子系统采用单任务架构,仅需单个任务(或线程)即可完成工作,因此其软件无需 Ada 运行时系统的支持,GNAT 编译通过 Restrictions 编译指示可充分支持无运行时系统的执行。相反,后台引擎必须同时处理多个活动,其软件需要 Ada 运行时系统的支持,因此 Top Layer Networks 为后台引擎构建了自己的 Ada 运行时实现。
2. 选择 Ada 和定制运行时系统的原因
选择 Ada 作为 AppSwitch™ 软件的编程语言,是为了提供高可靠性和可移植性。该软件必须首次运行就正常工作,并能在出现故障和进行软件重新配置时继续执行,且预期有较长的使用寿命,因此编程语言必须能够在不同的目标机器和不同代的目标机器之间进行移植。Ada 具备高可靠性和可移植性所需的最佳语言特性组合,包括强类型、面向对象编程、多任务处理和异常处理。
选择 GNAT 作为工具链,是因为它基于 GCC,且只有 GCC 针对 ARC 处理器。此外,Top Layer Network 需要对 Ada 工具链的内部机制有更大的控制权,而只有 GNAT 和 GCC 能提供这种控制。
Ravenscar 配置文件对在后台引擎上使用 Ada 具有很大吸引力,主要基于两个要点:后台引擎软件架构的性质要求有限使用 Ada 任务通信和同步功能,任务之间无需直接通信,任何任务间的通信都可通过受保护对象完成,且所有受保护对象要么无入口,要么只需一个入口调用,单入口受保护对象通常用作事件队列,由单个任务服务。Top Layer 的 Ada 使用情况与 Ravenscar 配置文件几乎匹配,且 Ravenscar 配置文件的存在以及 Ada 社区对其的认可,使得遵循其限制具有说服力,因为这样更有可能获得支持实现。
Top Layer Networks 构建定制的 Ravenscar 配置文件 Ada 运行时系统的动机如下:
- 由于工程开发的性质,开发人员需要对架构的关键元素有很大的控制权,后台引擎可执行代码的大小和速度就是其中之一,必须清楚 Ada 运行时系统中发生的情况,才能对其性能有信心。
- 唯一针对嵌入式系统的公共领域 Ada 运行时系统 RTEMS 被认为过于通用和庞大,不适合在后台引擎上使用,且将 RTEMS 与目标集成并进行精简以满足简化使用的工作量,与构建新的 Ravenscar 配置文件运行时系统的工作量相当,而且对定制实现的了解会比 RTEMS 更多。
- Top Layer Networks 拥有经验丰富的工程人才,有能力进行 Ada 运行时实现工作。
因此,Top Layer Networks 从现有的 GNAT Ada 运行时系统开始,对其进行精简,为裸机构建了 Ravenscar 配置文件 Ada 运行时系统。
3. 设计准则
为确保实现能满足 Top Layer 当前和未来的需求,制定了以下设计准则:
- 底层“内核”从一开始就设计为在裸机上执行,不考虑在其他操作系统之上运行。
- Ada 运行时系统的软件层之间不存在向上依赖关系,这使得 Ada 运行时系统的设计更具层次性、更简单,且更易于移植到新处理器。
- 架构必须隔离 ARC 处理器的特定机器依赖,不仅出于技术原因,还因为 ARC ISA 被视为 Argonaut 的知识产权。
- 由于构建和维护 Ada 运行时系统不是核心竞争力,实现必须继续由 GNAT 编译系统支持,因此 Ada 运行时的简化必须受限,以确保仍支持 GNAT 编译器接口,将咨询 ACT 以确保兼容性,ACT 已使用 Restrictions 编译指示简化了 Ravenscar 配置文件使用的 GNAT 编译器接口。
- 新开发的软件将使用 Ada,因此除了公共领域的 malloc 软件模块和最低级别的机器相关软件外,所有 Ada 运行时系统代码都用 Ada 实现。
- Ada 运行时系统的底层“内核”层必须能够支持用 C 编写的模块的执行,出于遗留和开发灵活性的原因,内核的接口必须将其 Ada 使用限制为 C 语言可以使用的特性。
- 底层“内核”的接口必须符合 POSIX 时间接口,同样是出于遗留和开发灵活性的原因。
- 后台引擎架构的未来发展包括动态加载和执行“扩展模块”,Top Layer 认为这些扩展模块的实现和交互最好封装为进程或 Ada 分区,因此运行时系统的架构必须能够容纳进程/分区的添加。
- 提供高级别的检测功能,以支持运行时系统的开发以及后续后台引擎软件对资源的使用。
- 底层“内核”必须支持基于优先级的抢占式调度和带优先级的时间片划分,以防止线程饥饿。
4. 实现概述
4.1 基线
Ravenscar 配置文件 Ada 运行时系统的实现基线是现有的 GNAT Ada 运行时系统。GNAT 的设计由佛罗里达州立大学的 Ted Baker 博士及其学生在一系列论文中进行了记录,其架构可大致分为四层:
- 顶层的 Ada 依赖层,处理特定于 Ada 的运行时语义,该层的接口 GNARLI 在相关文档中定义。
- 中间层,为 Ada 依赖层提供一组独立于操作系统的操作,该层的接口 GNULLI 由以下模块集合定义。
- 下层,在中间层和底层操作系统原语内核之间提供“薄”绑定,接口和实现受 POSIX 标准的影响很大,该层的实现通常由底层内核或操作系统的供应商提供。
- 底层内核或操作系统,可能是也可能不是类似 POSIX 的操作系统。
4.2 差异
构建策略是通过三种方式对 GNAT 运行时系统进行定制:
- 简化运行时系统中特定于 Ada 层的功能,仅支持类似 Ravenscar 的配置文件。
- 减少 GNAT 运行时系统的层数和层的“厚度”。
- 构建一个简化的、适用于裸机执行的 POSIX 兼容“内核”。
Top Layer Ravenscar 配置文件 Ada 运行时系统的架构与 GNAT 基线的主要差异如下:
-
Ada 依赖层
:显著缩减,仅支持 Ravenscar 配置文件中的特性,GNARLI 接口中仅保留以下模块:
System.Tasking
System.Tasking.Protected_Objects
System.Task_Specific_Data
Ada.Exceptions
System.Exception_Table
System.Standard Library
System.Tasking.Stages
System.Tasking_Soft_Link
System.Task_Info
System.Exceptions
System.Parameters
通过严格使用 Restrictions 编译指示,确保编译器不会调用已删除模块中的任何操作,具体的 Restrictions 编译指示规范如下:
pragma Restrictions
(Max_asynchronous_select_nesting => 0, Max_select_alternatives => 0,
Max task entries => 0, Max_protected_entries => 1, Max tasks => n,
No_asynchronous_control,
No_dynamic_priorities,
No_task_allocators,
No_terminate_alternatives,
No_abort_statements,
No_task_hierarchy);
- 中间层 :已被消除,其大部分功能已下移到“内核”,其余功能已在 Ada 依赖层中显式“内联”,仅在 System.Task_Primitives 和私有包 System.Task_Objects 中保留了一些类型声明。
-
底层内核
:分为三个主要模块:
- Kernel.Threads:以独立于处理器的方式执行线程管理。
- Kernel.Process:以独立于处理器的方式执行进程管理。
- Kernel.CpuPrimitives:封装所有与机器相关的操作,用于处理机器状态和中断,是 Ada 运行时系统的最低层,其规范仅对底层内核可见。
- Kernel.Memory:执行实际的内存管理。
- Kernel.Time:提供低级时间类型和操作。
- Kernel.Parameters:包含仅由内核层使用的常量。
- Kernel.Exceptions:执行低级异常管理。
- Kernel.Crash:在重置前记录重要的机器状态。
- Kernel.IO:是闪存磁盘设备的接口。
Kernel.Threads 的规范可在附录中找到,其余模块的规范可在网站(www.TopLayer.com)上找到,所有模块的规范都定义为可由 C 程序使用。
4.3 亮点
- Ravenscar 配置文件的影响 :Ravenscar 配置文件的限制对 Ada 依赖层的大小产生了重大影响,特别是消除异步控制转移、重新排队、会合和任务层次结构等功能的影响最为显著。
- 不完全遵循配置文件 :实现并未完全遵循 Ravenscar 配置文件的限制,主要是因为后台引擎软件并非严格的硬实时应用。消除一些限制可以使软件架构更灵活,以适应数据通信的动态性质,主要差异包括:允许动态内存管理、允许动态分配受保护对象、最终允许在动态执行扩展模块时动态创建和终止任务、允许定时入口和条件入口调用。Top Layer 认为允许这些例外不会给实现增加显著的负担。
- 编译器接口的限制 :目前,编译器接口阻碍了进一步的简化。例如,编译器预期任务有入口,受保护对象有多个入口,因此底层系统数据结构仍然包含关于入口的不必要信息。通过消除编译器预期使用的不必要模块和操作,可以进一步简化 Ada 依赖层模块的组织,但创建特殊的编译器接口对任何 Ada 供应商来说都是一个重要的决策。
- 单锁同步 :在 Ada 运行时系统中仅使用一个锁来同步操作。相关研究表明,在 Ada 运行时仅在单个处理器上执行的特定情况下,消除多个锁的开销所节省的成本可以抵消阻塞成本。
- 线程控制块分离 :线程控制块与 Ada 任务控制块在物理上分离,这使得底层内核能够透明地支持 Ada 任务和 C 线程,并且线程控制块的创建方式也是透明的,目前是动态分配的,但也可以轻松地进行静态分配。
- 中断处理 :中断处理和管理需要特别注意,具体体现在两个方面:底层内核现在允许任何无参数过程封装中断处理程序代码,而不是使用受保护过程;所有用户可定义的中断都有自己的线程和关联的线程控制块,这使得中断处理程序代码的执行可以像其他线程一样进行调度和分派。
- POSIX 兼容接口 :底层内核提供的接口符合 POSIX 标准,利用了 POSIX 操作定义中的灵活性。例如,在 Ada 运行时系统中,线程进行条件等待时不允许出现虚假唤醒信号,而当前的 GNAT 运行时系统由于在允许虚假唤醒信号的 POSIX 兼容系统上实现,因此允许这些信号。作为单处理器裸机实现,底层内核不处理任何虚假唤醒信号,并将这种简化传递给运行时系统的其余部分。
- 空闲线程 :存在一个空闲线程,用于执行后台测试并简化线程控制块的排队。
- 动态内存管理 :动态内存管理分为两层。在最低层,Kernel.Memory 以 512 字节的固定长度块管理所有堆存储,将已分配和空闲块的映射与块本身分开,以最小化用户损坏的可能性,并跟踪谁分配了块以及如何分配块。除了来自 Kernel.Threads 模块的请求外,所有其他对 Kernel.Memory 的请求都通过 malloc 模块或动态存储池机制进行。malloc 模块支持标准存储池请求,基于 Doug Lea 的优秀公开实现,并进行了简化以与 Kernel.Memory 配合使用。动态存储池机制是围绕 Root_Storage_Pool 的扩展构建的通用机制,使该通用机制的客户端能够微观管理各个存储池。两种机制都与 Kernel.Memory 协作,以跟踪内存的分配情况。
5. 目前的经验
在撰写本文时,已经完成并测试了 Ada 运行时系统,它正在支持 AppSwitch™ 进行 beta 测试。
-
开发时间和成本
:构建和测试运行时软件大约花费了 16 周时间,由两名工程师完成。对检测功能的投资在发现一些严重的定时错误根源方面取得了很好的效果,从技术和经济角度来看,整个投资都是合理的。
-
性能节省
:遗憾的是,没有其他完整语义的 Ada 运行时系统作为基线来比较实现 Ravenscar 配置文件运行时系统所积累的性能节省。从删除和简化的代码量来看,认为节省是相当可观的,但量化这些节省需要的工作量超出了 Top Layer 的承受能力。
-
未来改进
:在接下来的几个月里,将继续改进和扩展实现,例如优化异常处理和添加进程/分区功能。
附录:Kernel.Threads 模块规范
--
-- Copyright (C) 1998 Free Software Foundation, Inc.
--
with Kernel.CpuPrimitives, Kernel.Time, System;
package Kernel.Threads is
package KCP renames Kernel.CpuPrimitives;
package KT renames Kernel.Time;
EAGAIN : constant := 11;
EFAULT : constant := 14;
EINTR : constant := 4;
EINVAL : constant := 22;
ENOMEM : constant := 12;
ETIMEDOUT : constant := 116;
type thread_t is mod 256;
subtype ThreadIdType is thread_t;
function IsValid(threadId : ThreadIdType) return Boolean;
type mutex_t is limited private;
type cond_t is limited private;
nullThreadId : constant thread_t;
type thread_attr_t is
record
priority : Integer;
stackCheck : Boolean;
stackSize : Integer;
end record;
defaultAttr : constant thread_attr_t := (0, true, 0);
function thread_create(attr : thread_attr_t; startRoutine : System.Address; startArg : System.Address) return thread_t;
function thread_self return thread_t;
function thread_get_priority(threadId : thread_t) return Integer;
procedure thread_set_priority(threadId : thread_t; priority : Integer);
procedure thread_yield;
procedure thread_kill(threadId : thread_t);
procedure thread_exit;
pragma No_Return(thread_exit);
function thread_get_taskId return System.Address;
function thread_get_errno return Integer;
function thread_get_pid return Integer;
procedure thread_set_taskId(taskId : System.Address);
procedure thread_set_errno(errno : Integer);
procedure thread_set_pid(pid : Integer);
procedure mutex_init(mutex : in out mutex_t; ceiling_prio : Integer);
procedure mutex_lock(mutex : in out mutex_t);
procedure mutex_unlock(mutex : in out mutex_t);
procedure mutex_destroy(mutex : in out mutex_t);
procedure cond_init(cond : in out cond_t);
procedure cond_wait(cond : in out cond_t; mutex : in out mutex_t);
procedure cond_timedwait(cond : in out cond_t; mutex : in out mutex_t; wakeupTime : in KT.TimeType);
procedure cond_signal(cond : in out cond_t);
procedure cond_destroy(cond : in out cond_t);
procedure pause;
procedure sleep(wakeupTime : KT.TimeType);
procedure resume(threadId : thread_t);
type HandlerPtrType is access procedure;
function CurrentHandler(id : KCP.Interrupt_ID) return HandlerPtrType;
function AttachHandler(id : KCP.Interrupt_ID; newHandler : HandlerPtrType; taskId : Address; priority : Integer) return ThreadIdType;
-- ThreadCycleCount is a 64 signed integer which represents
-- the total number of processor clock cycles consumed by a thread
-- as it executes instructions, since the thread was first
-- created. It should never be negative.
type ThreadCycleCount is new Standard.Long_Long_Integer;
for ThreadCycleCount'Size use 64;
type ThreadInfoType is
record
heapSize : Integer;
stackSize : Integer;
execTime : ThreadCycleCount;
end record;
type ThreadInfoPtrType is access all ThreadInfoType;
function GetThreadTime return ThreadCycleCount;
procedure GetThreadInfo(threadId : thread_t; infoPtr : ThreadInfoPtrType);
procedure UpdateHeapSize(heapSizeChange : Integer);
type ThreadStatsType is
record
spuriousCondSignals : aliased Integer;
spuriousResumes : aliased Integer;
threadPreemptions : aliased Integer;
threadYields : aliased Integer;
threadBlocks : aliased Integer;
threadUnblocks : aliased Integer;
threadInits : aliased Integer;
threadDestroys : aliased Integer;
changeMyPriorities : aliased Integer;
changeOtherPriorities : aliased Integer;
timerInserts : aliased Integer;
timerRemoves : aliased Integer;
timerAwakens : aliased Integer;
mutexInits : aliased Integer;
mutexDestroys : aliased Integer;
mutexLocks : aliased Integer;
mutexUnlocks : aliased Integer;
condInits : aliased Integer;
condDestroys : aliased Integer;
condWaits : aliased Integer;
condTimedWaits : aliased Integer;
condSignals : aliased Integer;
pauses : aliased Integer;
sleeps : aliased Integer;
resumes : aliased Integer;
attachHandlerCalls : aliased Integer;
interruptCallbacks : aliased Integer;
timerCallbacks : aliased Integer;
timerPreemptions : aliased Integer;
timeSliceExpirations : aliased Integer;
end record;
type ThreadStatsPtrType is access all ThreadStatsType;
procedure GetThreadStats(statsPtr : ThreadStatsPtrType);
private
type ThreadQueueType is
record
-- TBD. Should we also keep a thread count?
first : System.Address;
last : System.Address;
end record;
pragma Suppress_Initialization(ThreadQueueType);
type mutex_t is limited
record
owner : System.Address; -- Is non-null when locked.
-- Holds the priority of locking thread.
previousPriority : Integer;
ceilingPriority : Integer;
chain : System.Address;
end record;
pragma Suppress_Initialization(mutex_t);
type cond_t is limited
record
threadQueue : aliased ThreadQueueType;
chain : System.Address;
end record;
pragma Suppress_Initialization(cond_t);
end Kernel.Threads;
通过以上内容,我们可以看到基于 Ravenscar 配置文件的 Ada 运行时系统实现是一个复杂而精细的过程,它结合了 Ada 语言的优势和 Ravenscar 配置文件的特点,为 AppSwitch™ 软件提供了高可靠性和可移植性的运行环境。在实际开发中,我们可以借鉴这些设计思路和实现方法,根据具体需求进行定制和优化。
基于Ravenscar配置文件的Ada运行时系统实现(续)
6. 关键技术点分析
6.1 线程与任务管理
- 线程控制块分离 :线程控制块与 Ada 任务控制块分离是一个关键的设计决策。这一设计使得底层内核能够同时支持 Ada 任务和 C 线程,并且线程控制块的创建方式灵活,既可以动态分配,也可以静态分配。这种分离提高了系统的兼容性和可扩展性,使得不同类型的线程和任务能够在同一个运行时系统中和谐共存。
- 单锁同步机制 :仅使用一个锁来同步 Ada 运行时系统中的操作,这一决策基于特定的运行环境。在单处理器环境下,消除多个锁的开销所节省的成本可以抵消阻塞成本,从而提高系统的性能。这种优化策略在特定场景下能够有效减少系统的开销,提高运行效率。
6.2 中断处理优化
- 灵活的中断处理程序封装 :底层内核允许任何无参数过程封装中断处理程序代码,而不是局限于受保护过程。这种灵活性使得中断处理程序的编写更加自由,能够更好地适应不同的应用需求。
- 独立线程与调度 :所有用户可定义的中断都有自己的线程和关联的线程控制块,这使得中断处理程序代码的执行可以像其他线程一样进行调度和分派。这种设计提高了中断处理的效率和响应速度,确保系统能够及时处理各种中断事件。
6.3 内存管理策略
- 分层动态内存管理 :动态内存管理分为两层,底层的 Kernel.Memory 以固定长度块管理堆存储,将已分配和空闲块的映射与块本身分开,减少了用户损坏的可能性。同时,通过 malloc 模块和动态存储池机制,实现了对内存分配的精细管理,提高了内存的使用效率。
7. 与其他系统的对比
由于缺乏其他完整语义的 Ada 运行时系统作为基线,难以精确量化实现 Ravenscar 配置文件运行时系统所积累的性能节省。但从删除和简化的代码量来看,其节省是相当可观的。与通用的 RTEMS 运行时系统相比,定制的 Ravenscar 配置文件运行时系统更贴合 AppSwitch™ 后台引擎的需求,避免了 RTEMS 过于通用和庞大的问题,减少了不必要的开销。
8. 未来发展方向
8.1 功能扩展
- 优化异常处理 :进一步优化异常处理机制,提高系统在异常情况下的稳定性和可靠性。可以通过改进异常处理的流程和算法,减少异常处理的时间开销,确保系统能够快速从异常中恢复。
- 添加进程/分区功能 :支持动态加载和执行“扩展模块”,将其封装为进程或 Ada 分区。这将增强系统的扩展性和灵活性,使得系统能够根据不同的应用需求动态调整功能。
8.2 性能提升
- 编译器接口优化 :与 Ada 供应商合作,创建特殊的编译器接口,消除编译器预期使用的不必要模块和操作,进一步简化 Ada 依赖层模块的组织,提高系统的性能。
- 资源管理优化 :通过更精细的资源管理策略,优化系统的资源使用效率,减少资源的浪费,提高系统的整体性能。
9. 总结
基于 Ravenscar 配置文件的 Ada 运行时系统实现为 AppSwitch™ 软件提供了高可靠性和可移植性的运行环境。通过定制开发,满足了后台引擎对代码大小、速度和功能的特定需求。在设计和实现过程中,采用了一系列优化策略,如线程与任务管理的分离、中断处理的优化和分层动态内存管理等,提高了系统的性能和稳定性。
虽然目前已经取得了一定的成果,但仍有许多方面可以进一步改进和扩展。未来,将继续优化异常处理、添加进程/分区功能,并通过优化编译器接口和资源管理策略,提升系统的性能和扩展性。
流程图:Ravenscar配置文件Ada运行时系统架构
graph LR
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
A(Ada应用):::process --> B(Ada特定运行时例程):::process
C(C应用):::process --> D(RT POSIX兼容内核):::process
B --> D
D --> E(内核与处理器绑定):::process
D --> F(Kernel.Threads):::process
D --> G(Kernel.Process):::process
D --> H(Kernel.CpuPrimitives):::process
D --> I(Kernel.Memory):::process
D --> J(Kernel.Time):::process
D --> K(Kernel.Parameters):::process
D --> L(Kernel.Exceptions):::process
D --> M(Kernel.Crash):::process
D --> N(Kernel.IO):::process
表格:Ravenscar配置文件运行时系统与GNAT基线的差异对比
层次 | GNAT基线 | Ravenscar配置文件运行时系统 |
---|---|---|
Ada依赖层 | 功能丰富,支持多种特性 | 显著缩减,仅支持Ravenscar配置文件中的特性,部分模块被删除 |
中间层 | 提供独立于操作系统的操作 | 已被消除,功能下移或内联 |
底层内核 | 通常由供应商提供,与操作系统紧密相关 | 分为多个独立模块,支持裸机执行,接口符合POSIX标准 |
通过对基于 Ravenscar 配置文件的 Ada 运行时系统的深入研究和实践,我们可以看到其在提高系统可靠性、可移植性和性能方面的巨大潜力。在未来的软件开发中,这种定制化的运行时系统将为更多的应用场景提供有力的支持。