利用实时Java设计数字音频系统
扫描二维码
随时随地手机看文章
如今,在低级、硬件、实时软件领域已有各种各样的Java方案。但是,当在像数字音频信号处理这样非常低级的软件中应用Java技术时,某些方案将更能发挥传统Java技术的优越性。
其中一种方法基于针对资源受限和安全关键的Java定义的推荐规范。设计该方案的主要目的是保持Java的可移植性、可维护性和可扩展性优势。在系统内的信息流中包括两台计算机,这两台计算机协同操作以便通过网络通讯通道交换音频信息(图1)。
在一个节点上采集到的音频信号被传输至其它节点,并被输出至远程计算机的扬声器。在第二个节点上采集到的音频信号则在第一台计算机的扬声器中输出。从概念上讲,信息流被构造成两个独立的数字音频数据流。
这种简单的音频处理应用可以被当作PERC Pico应用程序来实现。该软件目前正在开发中,它首次实现了针对安全关键和资源受限应用所提出的实时Java(RTSJ)规范概要分析。“硬实时概要分析(hard real-time profile)”即指这种环境。
图1:一个简单的数字音频应用构成了本文讨论的关键性软件工程的基础。 |
维护和可扩展性要求
摩尔定律推动了典型嵌入式应用的规模和复杂度快速增长。竞争压力促使软件不断发展,以满足功能越来越强大的硬件的需求。对某些消费电子设备的研究发现,新产品中代码规模的增长速度非常接近摩尔定律,大约每18至36个月就翻一番。
大约20年前,每个新的嵌入式设备中的所有软件通常是由一个或两个工程师在不到一年的时间内写完。而现代嵌入式软件的开发则非常困难。假设每次新产品的软件修订都要求增加数十万,甚至数百万行的代码,那么嵌入式软件开发人员的职责将更多地转向如何解决集成许多独立开发的软件组件所带来的挑战。
这个简单的数字音频例子代表了一种原型的低级嵌入式软件“产品”。对大多数产品而言,开发原始软件的成本要比整个产品生命周期内的软件维护成本小得多。以下列出了该应用在产品生命周期内的发展过程。
(1)软件将需要被移植到不同的操作系统和不同的处理平台上,这将改变它的CPU时间和内存需求。
(2)软件将与各种不同的补充功能集成在一起。也许下一代产品也将包括视频信号。也许它将支持共享数字白板,以便于召开远程会议,或者可能与电子邮件和日历软件集成在一起。或者,一些应用可能增加录制功能,以将会议实况保存在磁盘中。
(3)双节点网络拓扑可能需要进行通用化,以支持有任意多参加者的会议。
(4)模数转换器(ADC)和数字信号处理器(DSP)的接口可以不断发展。在一些配置中,操作系统提供了这种服务。而在其它配置中,这种应用可能包括连接音频子系统硬件和DMA内存设备的设备驱动程序接口。音频硬件本身有望继续发展,这要求软件设备驱动程序不断发展。
(5)网络通信协议可能需要作一些改变。在某些环境中,软件将依赖底层操作系统服务来与网络连接。随着各种网络通信协议的发展,连接操作系统网络业务的接口甚至也可能发生变化,以便提供新的QoS参数和更高带宽。在其它情况下,这种应用将需要包含面向硬件接口的低级设备驱动程序,也可能需要实现通信协议栈。可以采用带冲突检测的载波侦听多路访问(CSMA/CD)技术、无线、光纤和其它有待发明的技术,在低成本专用串行通道、同轴电缆和双绞线数据链接中实现相同的基础通信能力。通信库可能集成了压缩、加密、检错和纠错,以及滑动窗口协议。
上面给出了软件在商业化业务应用中的几种可能发展方式,这里并非想穷举所有的优势,只是为了说明保留Java设计优势的好处,即使是对于一些资源受限和硬实时应用来说。
图2:硬实时JAVA翻译环境显示了各种工具之间的关系,这些工具可使实时组件的开发、维护和集成更容易。 |
实时JAVA的能力
这些实时Java编程技术由RTSJ衍生发展而来。该规范具有很好通用性,能支持多种独特的实时编程要求。由于本文主要关注非常低级的实时软件,所以我们将开发人员的操作限定在完整RTSJ规范的子集范畴内。
这种概要分析可改善可移植性、可靠性和效率,因为它禁止使用一些需要很大的运行时间开销、会带来不可移植的实现依赖性、增加软件复杂度以致使程序员更容易出错的功能。硬实时概要分析和完整RTSJ之间一些特殊差异包括:
(1)完整的RTSJ对同步锁采用优先级继承方法,并支持优先级限高仿真可选。硬实时概要分析禁止使用优先级继承并要求支持优先级限高仿真。
(2)完整的RTSJ允许即时修改各种线程调度和对象同步参数。硬实时概要分析禁止对线程调度和同步协议进行即时调整。
(3)完整的RTSJ还支持一些机制,这样每当任务错过最终期限或超出其CPU时限时,就可以自动触发异步事件。请注意,这些服务的实现是完全不可移植的,而精确执行会带来极高的运行时间开销。此外,在硬实时应用中不需要运行时间限制,因为在程序执行之前,已经静态地满足资源预算和最终期限要求。因此,硬实时概要分析不支持这些机制。
(4)完整的RTSJ支持传统线程、访问垃圾收集堆的实时线程,以及不访问垃圾收集堆的实时线程的混合体。这种不同线程类型的混合大大增加了系统的复杂度和规模。这种复杂度将增加由于不同线程类型之间不能正确共享信息而导致的实时编程出错的可能性,硬实时概要分析仅支持不访问垃圾收集堆的实时线程。
(5)完整的RTSJ提供一系列可供应用程序员使用的库,以便举例说明动态内存范围,并在特定范围内分配对象。由于程序员在开发或集成采用嵌套作用域(nested scope)的组件时可能会产生许多小错误,所以这些库的使用尤其成问题。为执行正确的区域性存储器(scoped-memory)使用协议,RTSJ在每次读取和/或重写参考字段时都执行特殊的运行时检查。在完整的RTSJ中,运行时进行检查会使程序组件出错,从而使得程序由于非法分配、非法读取、区域性存储器协议错误、内存不足错误等原因,以运行时间异常方式终止执行。硬实时概要分析禁止使用RTSJ内存作用域(memory scope)操作库。相反,它要求程序员以编程注释的形式描述其对作用域内存(scoped-memory)的使用。在编译期间,这些注释可以被分析和执行,例如本文应用提到的@Scoped和@StaticAnalyzable注释。
(6)RTSJ不会为了中断处理或低级设备的I/O而对库进行标准化,而硬实时概要分析对这些库进行定义。
硬实时概要分析的商用化前实现试验显示,它运行在某些CPU密集型基准程序的速度比标准Java和完整RTSJ的速度快三倍。这是因为硬实时执行环境比标准RTSJ简单得多,并且它还用编译时间验证替代各种运行时检查。这种性能可以与相应的C和C++程序相媲美,有时甚至更好。
尽管采用受限的硬实时概要分析比采用传统Java更加困难,但这种平台的代码开发和维护要比用C或C++开发出的相应平台的维护容易。这是因为硬实时Java平台具有更好的可移植性,并提供高级的面向对象的抽象。此外,硬实时Java平台包括可使实时组件的开发、维护和集成更为容易的一些重要开发工具(图2)。
由于包含了强制严格遵守类型安全的字节码校验器,与C和C++相比,Java开发可提高可靠性和可维护性。C和C++程序员可以利用多种让使类型安全无效的机制,而有意或无意地利用这些漏洞将使代码更容易产生错误,并降低可移植性。
受限的实时环境提供了比传统Java更严格的字节码验证。特别是,图2中的硬实时验证器可确保指向堆栈分配对象的参数(指针)不会比对象本身的生存期更长。它也确保用专用@StaticAnalyzable注释标记的程序组件,可限制它们对可分析子集使用Java。与硬实时翻译器的集成,则能提供确定执行每个组件所需的CPU时间和堆栈内存上限的能力。
执行硬实时组件所需的所有临时内存分配,必须由正在执行线程的运行时栈来实现。执行从单主线程开始,而主线程的运行时栈代表了所有可重复使用的内存。对于由主线程派生的每个附加线程,它提供了部分运行时栈作为派生线程的运行时栈。
图3:该模块图说明了SimpleAudio数字音频应用的体系结构。 |
数据音频应用的实现
图3给出了数字音频应用的体系结构。它共有6个线程,包括主线程和用Orchestrator实例表示的异步事件处理器。BufferPair将每个插座接口连接至相应的DSP接口。主线程监控用户指令,并在用户请求关闭会话时调用SimpleAudio实例的terminateActivity()方法。所有其它线程通过调用continueActivity()业务,定期轮询SimpleAudio实例。到了关机时,该方法返回false值。
在缺省配置中,该应用以8kHz采样频率对麦克风输入进行采样,每次采样采集8比特数据。这种配置每秒钟产生8k字节的数字音频数据,这对简单的语音应用来说已经足够。但是,它不适合高保真立体声信号。一般的CD录制以44.1kHz的采样频率对两个立体声信道每次采样16比特。这种高保真度信号的带宽要求为176.4千字节/秒。
在缺省配置中,插槽读模块和写模块采用足够的带宽进行可靠传输,以可靠提供所有从DSPReader模块采集的数据。我们采用了一种直接压缩技术,一连串同样的字节值(像出现在静音期间的那样)由一个专用的转义(escape)值、重复次数和重复值表示。当然,更先进的压缩技术将更为合适。
在实时系统中,由抖动描述特定实时组件的理想执行时间的预期偏离,由一个确切的线程描述数字音频应用的每个组件。SocketWriter线程接收来自DSPReader模块的原始数据流,对数据进行压缩,并将数据传送至网络插座通道。如果网络插座通道的带宽有限,只能达到预算的8千字节/秒,那么任何导致SocketWriter延迟数据传输的抖动影响将随着时间而累积。
在缺省配置中,预计SocketWriter每125µs传输1字节数据。如果每秒的音频数据有1个字节延迟半毫秒,则1小时后,累积延迟将约为2秒。为防止抖动延迟的累积,该架构包含一个运行在16Hz的监视线程。
在每个周期内,该线程强制让SocketWriter和DSPWriter组件丢弃62.5ms之前的数据。由于我们处理的是音频数据,所以通常来讲,丢弃的临时数据值比允许数据到达时间偏移更可取一些。人们通常不会注意到丢弃临时数据字节的影响。
请注意在第1行出现的@StaticAnalyzable注释,源码列表中的@ StaticAnalyzable(enforce_time_analysis = {false}, enforce_non_blocking = {false})。这代表了部分方法签名(method signature)。注意该注释给出了enforce_time_analysis 和enforce_non_blocking属性的参数值,两者都是false。这表示该方法的实现无需将其本身限制在子集内,对于该子集,静态分析器可从中推断执行该方法所需要的严格CPU时间上限,也不要求静态分析器验证该方法执行时永远不会阻断。
如果这些属性定义没有给出,硬实时验证器将认为程序不合法,因为在源码列表的(!orchestrator.destroy()) { through 57, }执行时,静态分析器无法确定该循环包含了多少次第55行。此外,main方法的执行可能会在第59行的socket_ reader_thread.join()至63行的orchestrator_thread.join()之间阻断,以及在第51行sa.awaitTermination()调用的await-Termination()方法中阻断。
在@StaticAnalyzable注释中未注明的是enforce_memory_analysis属性的值。该属性的缺省值为true,这意味着该方法的实现必须符合限定的指导方针以使执行该方法时静态分析器能够确定将要分配的最大内存。假设该环境的实时Java规则将内存作为运行栈的一部分,则临时内存分配的上限就表示必需的主线程的运行时栈大小。
注释有助于软件开发,并大大简化软件维护工作。通常,系统架构师将复杂的系统功能分为较小的组件,以便由不同的开发小组实现。因此,描述不同组件之间连接的接口定义,不仅详细说明了可以在组件间传递的参数类型,还包括在每个组件中必须实现的实时处理的限制,能减少软件维护方面的开销。
对于现有软件的修改必须遵从组件接口注释中描述的所有其它特殊实时限制。如果软件维护人员违反了这些接口要求,他们可以从字节码验证器得到直接、明确的反馈。从而确保现有大型实时软件系统的不断变化不会动摇现有系统的稳定性。
在对可靠运行该主程序所需的堆栈内存进行分析时,静态分析器必须确定在该方法以及该方法所调用的方法中,每个对象要求分配多大内存。为了支持静态分析结果的模块化合成,字节码验证器要求每个由主程序调用的方法被声明为@Static-Analyzable,而enforce_time_ analysis属性设置为true。快速复查main方法的实现可确保无限循环内不产生分配。这是字节码验证器所要执行的任务之一。
在第37行的socket_reader_thread = new Thread-Stack(SocketReader.class);到41行的orchestrator_thread = new ThreadStack(Orchestrator.class)之间分配了几个新的ThreadStack对象;每次分配描述了主程序派生的线程所使用的堆栈内存。一般来说,静态分析工具可能难以确定可靠执行这些子线程所必需的堆栈内存数量。
每个ThreadStack构造函数的参数为提供代码由相应线程执行的类(Class)。静态分析器要求每个在该环境中传递的NoHeapRealtimeThread子类具有带@ StaticAnalyzable注释,且enforce_ memory_analysis属性设置为true的run()方法。如果ThreadStack构造函数的参数并非来自BoundAsyncEventHandler(例如在Orchestrator类的情况下),则静态分析器要求该类的asyncEventHandler()方法采用@StaticAnalyzable注释来声明,且enforce_memory_analysis属性设置为true。
当前线程的运行时栈能满足所有临时内存需要。请注意,我们在第23行分配了两个临时BufferPair实例,microphone_stream = new BufferPair();而在第24行,speaker_stream = new Buffer-Pair();然后这些对象的参数被传递至构造函数,用于包含该软件应用的不同功能组件的各个线程。硬实时验证器实施的限制之一在于,stack-allocated对象的参数不能比引用参数的对象本身生存时间更长,同样是通过注释机制来执行。我们来看一下SocketReader类的构造函数:
@ScopedPure
@StaticAnalyzable(enforce_time_analysis = {false}, enforce_non_blocking = {false})
SocketReader(SimpleAudio sa, Buffer-Pair buffers, String socket_name) throws
FileNotFoundException
@ScopedPure注释说明该构造函数的每个输入引用参数(reference parameter)可以指代那些位于外部嵌套作用域的运行时栈的对象。字节码验证器确保这些参数的内容绝不会复制到那些由于具有@Scoped指派而未被同样区分的变量上。
此外,它禁止将内部嵌套作用域变量的值复制到外部嵌套作用域变量。一个例外情况是,在特殊环境下,它可证明带参数对象位于与要赋值变量相同或更外层嵌套的作用域。如果这一构造函数的参数未由@Scoped注释指定,字节码验证器将不允许主程序将参数传至堆栈分配的BufferPair和SimpleAudio对象。
本应用展示的RTSJ支持的实时编程抽象之一为PeriodicTimer类。注意,本应用在第49行举例说明了PeriodicTimer对象,drumbeat = new PeriodicTimer(start_time, period, orchestrator);并将结果赋值给本地drumbeat变量。参数之一为orchestrator对象的引用参数,其本身是BoundAsyncEventHandler的一个实例。该drumbeat周期计时器被设置为每秒触发orchestrator对象的handleAsyncEvent()方法执行16次,即每62.5微秒一次。
采用C或C++语言的实时开发人员可以实现这些实时Java技术所支持的许多相同构造。但是,C或C++程序员必须产生悬挂指针(dangling pointer)以及内存泄漏,他们还缺乏标准工具的支持来自动分析执行时间和堆栈大小。
另外,C和C++程序员还缺乏完整性检查以确保方法的实现能够满足文档化实时接口的要求,并确保方法调用能够传递同样满足文档接口要求的参数。最后,在对现在软件系统进行维护的过程中,C和C++程序员没有工具支持来保证对现有软件的修改与在原软件开发过程中假设的各种组成要求是相符的。
传统Java在生产效率和成本上具有许多优势。规范地使用实时Java技术可提供许多这样的优势。与使用C和C++相比,一般Java程序员在新代码开发期间具有2倍的生产率,而在现在软件维护期间具有5到10倍的生产率。随着嵌入实时软件的大小和复杂度增加,这些激发人们向更现代的软件工程技术(如由实时Java实现的工程技术)转化的因素已越来越重要了。