图3:该模块图说明了SimpleAudio数字音频应用的体系结构
3 数据音频应用的实现
图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每125s传输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实现的工程技术)转化的因素已越来越重要了。
评论