The embedded real-time market has been described as a thousand different niches. Each critical software component represents different requirements and economic tradeoffs. For developers who use Java in the development of their real time embedded system design, in particular, it is necessary to carefully balance hard real time versus soft real time performance and the trade offs they are willing to make to achieve their goals.
Hard real time systems are those in which an action performed at the wrong time will have zero or possibly negative value. The connotation of “hard real time” is that compliance with all timing constraints is proven using theoretical static analysis techniques prior to deployment. Soft real time systems are those within which an action performed at the wrong time (either too early or too late) has some positive value even though it would have had greater value if performed at the proper time. The expectation is that soft real-time systems use empirical (statistical) measurements and heuristic enforcement of resource budgets to improve the likelihood that software complies with timing constraints.
Practically speaking, the soft real-time Java development guidelines are generally preferred unless there is a specific constraint that precludes them. This is because soft real-time Java offers the highest developer and software maintenance productivity. The soft real-time Java guidelines involve the use of traditional J2SE-style Java APIs with virtual machines that have been specially implemented to support real-time behavior. These virtual machines support pre-emptible and incremental real-time garbage collection, fixed priority scheduling, and priority inheritance in the implementation of all synchronization locks.
In contrast, the hard real-time Java technologies do not make any use of automatic garbage collection. Instead, dynamic memory is allocated and de-allocated under more explicit programmer control. A draft standard for safety-critical Java supports a subset of the full spectrum of capabilities recommended for hard real-time Java development.
Note that the difference between hard real-time and soft real-time does not depend on the time ranges specified for deadlines or periodic tasks. A soft real-time system might have a deadline of 100 microseconds, while a hard real-time system may have a deadline of 3 seconds.
Satisfying real-time constraints in a high assurance Java application is possible if the developer recognizes the strengths and weaknesses of particular Java methodologies such as automatic garbage collection, and carefully selecting the most appropriate mechanisms to address each particular system requirement.
It is also necessary to play close attention to the hard real-time Java guidelines as specified in ”Draft Guidelines for Scalable Java Development of Real-Time Systems”, authored by Kelvin Nilsen, available at http://research.aonix.com/jsc/rtjava.guidelines.5-6-05.pdf.
The real-time cost of automatic garbage collection
Automatic garbage collection is one of the key reasons why Java developers are more productive than C and C++ developers. But the power of garbage collection comes with a cost.
Traditional Java implementations occasionally pause execution of all Java threads to scan memory in search of objects that are no longer being used. These pauses can last tens of seconds with large memory heaps. Memory heaps ranging from 100 Mbytes to a full Gigabyte are being used in certain mission-critical systems. The 30-second garbage collection pause times experienced with traditional Java virtual machines are incompatible with the real-time execution requirements of most mission-critical systems.
Special real-time virtual machines have been implemented to support pre-emptible and incremental operation of the garbage collector. With these virtual machines, the interference by garbage collection on the real-time application workload can be statistically bounded, making this approach suitable for soft real-time systems with timing constraints measured in the hundreds of microseconds.
One of the costs of automatic garbage collection is the overhead of implementing sharing protocols between application threads. Application threads are continually modifying the way objects relate to each other within memory while garbage collection threads are continually trying to identify objects that are no longer reached from any threads in the system. This coordination overhead is one of the main reasons that compiled Java programs run at a third to half the speed of optimized C code.
The complexity of the garbage collection process and of any software that depends on garbage collection for reliable execution is beyond the reach of cost-effective static analysis to guarantee compliance with all hard real-time constraints. Thus, we do not recommend the use of automatic garbage collection for software that has hard real-time constraints or that requires DO-178B Level A safety certification.Memory Allocation for Temporary Objects
Because Java is an object-oriented programming language, all structured data is represented as objects. In a traditional Java run-time environment, all objects are allocated within a region of memory known as the heap, and the memory for these objects is reclaimed by an automatic garbage collector. In the proposed guidelines for hard real-time development, there is no automatic garbage collector.
In traditional block-structured languages like Ada, Modula, and Pascal, records (the equivalent of Java’s objects) that are declared local to a given procedure are visible within inner-nested procedures and it is not possible to explicitly copy the address of an object into programmer-declared variables. This provides a safe mechanism for stack allocation of temporary objects. But Java is not a block-structured language. Rather, it derives more directly from C and C++ in that it does not support declaration of nested procedures.
In C and C++, programmers can also declare structures that are local to a function. These structures are allocated on the run-time stack and their memory is automatically reclaimed when the enclosing function returns. Since C and C++ do not allow for inner-nested functions, the only way for a function to share access to its stack-allocated local structures is by passing the addresses of these objects to called subroutines in global variables or as input parameters.
One of the dangers with this common C and C++ practice is that there is no compiler-enforced protection to assure that references to stack-allocated objects do not outlast the objects to which they refer. With C and C++, it is far too easy to create dangling pointers to objects that no longer exist. Whenever this occurs, these programming errors are among the most difficult to debug.
According to the hard real-time Java guidelines, special provisions are made to allow safety-critical Java components to allocate objects on the run-time stack using programming constructs similar to those of the C and C++ programming languages. Unlike C and C++, statically enforced programmer annotations allow the safety-critical Java compiler to ensure that the use of stack-allocated memory does not create dangling pointers. This is illustrated below in Figure 1: Sample Program with Stack-Allocated Objects and Figure 2: Source Code for Stack-Allocatable Complex class .
Each of the statements in lines 3 through 6 of Figure 1 allocates a new Complex object. According to the conventions established for hard real-time Java development, all four of the allocated objects are allocated within the stack activation frame of the method that contains this code.
In Figure 2, above , the various @ScopedPure annotations denote that all of the method’s incoming reference arguments, including its implicit this argument, may refer to objects that reside within the stack-allocated activation frame of some outer-nested method. Upon recognizing this annotation, the safety-critical Java compiler enforces that these incoming arguments are never copied to any variable that might outlive the object to which the reference arguments refer. More specifically, the compiler enforces that:
1) If the value of the incoming reference argument is copied to an outgoing reference argument, the corresponding declaration of the formal outgoing parameter is also declared to have the @ScopedPure (or @Scoped) attribute.
2) If the value is assigned to an instance field of another Java object, the reference variable that identifies the other Java object must have the @Scoped attribute, the assigned field must have the@Scoped attribute, and the method that performs the assignment must have the @AllowCheckedScopedLinks attribute.
By default, the safety-critical compiler prohibits assignment of scoped references to instance fields. However, in the special case that a programmer adds the @AllowCheckedScopedLinks annotation to a method’s body, the compiler will allow such assignments. To ensure that the assignments are safe, the compiler will generate code to test:
1) Whether the assigned value refers to a stack-allocated object, and if so
2) Whether the object being assigned is no deeper on this run-time stack than the object whose reference is being assigned.
A method that is declared with the @AllowCheckedScopedLinks attribute will throw a javax.realtime.util.sc.IllegalAssignmentException in the case that an attempt is made to create a reference from an object deep on the run-time stack to an object residing in a more shallow location of the run-time stack. This must be prohibited because the shallow object will eventually be removed from the stack before the deep object, at which point the deep object would hold a dangling pointer to the destroyed shallow object.
We recognize that the use of checked scoped links is somewhat dangerous. Programmers must exercise discretion and great care whenever they use this programming feature. We expect that many projects will adopt guidelines that prohibit the use of checked scoped links, and the safety-critical Java development tools will have the ability to enforce this prohibition.The key benefits of safe stack allocation of temporary objects are that:
1) Temporary memory allocation and de-allocation is very fast.
2) Temporary memory allocation is very reliable, because the stack never becomes fragmented and the maximum stack size can be determined through static analysis.
3) Compared with the RTSJ’s ScopedMemory abstractions, the performance and footprint overhead of run-time checks, and the risk that critical program components will be aborted because they violate scoped memory protocols are eliminated.
This gives the safety-critical Java programmer capabilities comparable to what they are already using in more traditional languages like Ada, C, and C++.Cooperation Between Hard and Soft Real-Time Components
Most mission-critical software systems are comprised of multiple software layers, with the lower layers being more static, having tighter real-time constraints, and more demanding performance constraints.
The higher software layers generally need to support more dynamic behavior, requiring dynamic allocation of data structures and even dynamic loading and unloading of code. Guidelines for high assurance mission-critical Java support a synergy between hard real-time and soft real-time components. Soft real-time mission-critical components are implemented using standard J2SE libraries running on a real-time-enhanced virtual machine.
The hard real-time components will typically run with footprint and throughput efficiency that is very close (within 5-10 percent) to that of optimized C. This represents a three-fold improvement over typical optimized Java performance and footprint. There are many important mission-critical needs that can be addressed by this configuration, such as:
1) Portable and very efficient device drivers (possibly, but not necessarily, having hard real-time constraints) can be implemented using the safety-critical Java programming notations.
2) Compared with the use of JNI, interfaces to legacy (“native”) components written in other languages are much more efficient and much safer if implemented using the safety-critical Java environment as an intermediary between traditional Java and native code.
3) Performance-critical code such as Fourier analysis and matrix manipulation can be provided much more efficiently as safety-critical Java components than as traditional Java code or as legacy C code interfaced to Java through JNI.
Selective Sharing of Control with Traditional Java Components
The recommended approach for providing efficient and reliable integration of hard real-time components with traditional Java components makes use of a restrictive form of object sharing between the hard real-time and traditional Java domains.
The shared objects always reside in the hard real-time domain and do not participate in garbage collection. Since these are hard real-time objects, they will never be subject to relocation. This greatly simplifies the implementation and improves execution efficiency.
We describe this object sharing as “restrictive” because we restrict access to the hard real-time object from the traditional Java domain. In particular, the traditional Java domain can not see any of the instance or static variables associated with the object. Furthermore, it cannot see the object’s regular methods.
It can only see methods that have been specially designated as traditional Java methods. These traditional Java methods, which cannot be seen or invoked by the safety-critical threads, are analogous to operating system entry points for application software. In this scenario, writing safety-critical Java code is similar to making modifications to an operating system kernel.
As with traditional operating system design, greater trust is placed in the implementers of the lower-level (operating system) software, and great care is taken to ensure that errors or malicious intent of application software not compromise the lower level components.
In traditional operating system design, invocation of kernel services generally crosses a memory protection barrier, and hardware memory management units assure that application code cannot see or modify kernel code and data structures.
The restrictive object sharing architecture supports the same abstraction guarantees, but it does so using static byte-code verification techniques which allow much more efficient integration of the hard real-time and non-real-time software.
The performance benefits of this sort of architecture have been proven, for example, in studies conducted by Calton Pu on the Synthesis Kernel.
For examples of the @TraditionalJavaMethod annotation, see lines 9 and 13 below in Figure 3: Annotated Source code for class Thermostat . This simple module represents an interface between a thermostat module, written as a hard real-time component, and traditional Java code which might need the ability to set and get the temperature. The getTemperature() and setTemperature() methods are to be invoked from the traditional Java domain.
Sample Implementation of a Device Driver
In this section, we describe a simple interrupt-handling device driver software component, written entirely in Java. Figure 4: Constant and Instance Variable Declarations for Interrupt class below provides the declarations of class constants and instance variables for the interrupt handler.
The ceilingPriority() method shown on lines 26 through 28 of Figure 4 is required because this class implements the javax.realtime.util.sc.Atomic interface. For classes that implement this interface, programmers are required to provide this method in order to establish a syntactic marker within the body of the class that can be used to establish static properties regarding the object’s synchronization behavior.
When an instance of this class is constructed, the synchronization behavior is governed by the default monitor control policy as with traditional RTSJ, but an additional run-time check is performed at the time this object is instantiated to ensure that the dynamic properties are consistent with the declared static properties. Within the safety-critical profile, programmers agree to follow the convention that each instantiation of a class will use a monitor control policy that is consistent with the syntactic markers represented by the class’s implementation of the Atomic interface and the ceilingPriority() method.
The constructor shown on lines 1 through 17 of Figure 5: Hard Real-Time Methods for Interrupt Handling Code(below) instantiates the three I/O ports required for operation of the interrupt handler. For any given hardware configuration, certain ranges of memory and I/O address space will be eligible to be treated as I/O ports that are accessible from hard real-time Java components.
Range checking to assure that this hard real-time Java component has permission to access the requested I/O ports is performed at the time these I/O ports are instantiated. Once instantiated, no additional checking is required when reading or writing the I/O ports. Besides instantiating the necessary I/O ports, the Interrupt constructor also allocates memory to represent a pair of input buffers. Note that this buffer pair is allocated in ImmortalMemory because the shared_buffers variable is not declared with the @Scoped attribute.
Lines 19 through 33 of Figure 5 provide the actual interrupt handling code. This method is declared as synchronized because, while it is running, all other interrupts of equal or lower priority are forbidden from running. Because this class is declared to implement the Atomic interface, the safety-critical Java byte-code verifier assures that the body of every synchronized method is execution-time bounded.
This restricts the set of services that can be invoked from within an interrupt handler. The byte-code verifier prohibits interrupt handling code from invoking services that might block while holding the priority ceiling lock. The byte-code verifier also assures that only objects that implement the Atomic interface can set their ceiling priority to ranges that correspond to hardware-dispatched interrupt handling.
In Figure 6: Traditional Java Methods Associated with Interrupt Handling Code, above , we present the two traditional Java methods that serve to enable efficient streaming of bytes from the hard real-time interrupt handler to the traditional Java domain. Lines 1 through 15 provide the implementation of the getReadBuffer() method. This method returns a reference to a hard real-time array of bytes that were fetched by the interrupt handler from the I/O port. Note that the reference value returned from this method will be represented within the traditional Java domain by a proxy object of type javax.rtproxy.ByteArray .
Note also that the traditional Java thread will block within this method until at least one byte is available in the shared buffer. Lines 17 through 19 provide the implementation of readBufferLength() , which represents the number of bytes which can be fetched from the buffer returned from thegetReadBuffer() method.
To set up this interrupt handler within the hard real-time domain, the real-time programmer executes code such as is illustrated above in Figure 7: Code to Set Up Interrupt Handler in Hard Real-Time Domain . Note the invocation of the javax.realtime.util.mc.Registry.publish() on line 18. The javax.realtime.util.mc package represents a proposed mission-critical package that complements the capabilities of the safety-critical package. The publish() method makes the Interrupt object visible to the traditional Java domain.
In order to establish a communication channel between the traditional Java environment and the hard real-time domain, the traditional Java environment must instantiate a JavaDevice object, as represented by the code provided below in Figure 8: Traditional Java Code for Communication Between Hard and Soft Real-Time Domains .
Note that the constructor for JavaDevice looks up the proxy object known by the name “ByteStream”, throwing an IllegalStateException if the registry does not currently hold any object by that name. Having instantiated a JavaDevice object, the traditional Java domain easily fetches bytes streamed from the device driver by invoking the getByte() method. Note that the typical control path through the getByte() method simply extracts a single byte from the input_buffer. This requires no synchronization with the hard real-time domain. This protocol is both much more efficient and much more reliable than using the Java Native Interface (JNI) to integrate high-level Java code with low-level device driver code.
Though it may be more difficult to program high-performance, hard real-time code in Java than to write traditional Java code, certain components of most mission-critical systems must be implemented using technologies that are more efficient and more deterministic than traditional Java.
At the same time, development and maintenance of these low-level components using portable stylized Java rather than assembler, C, or C++ will yield significant productivity improvements and cost savings. We have every reason to believe that the two-fold developer productivity benefits and five- to ten-fold software maintenance benefits that Java has demonstrated over C++ in traditional information technology domains can be matched in the deeply embedded mission-critical and safety-critical markets as well.
Kelvin Nilsen, Ph.D., is chief technology officer at Aonix, North America