Michael HönnigMichael Hönnig
Ein Echtzeitsystem garantiert, innerhalb einer definierten Zeit auf Signale oder Zustandswechsel zu reagieren. Ob diese Maximalzeit nun Mikrosekunden, Millisekunden, Sekunden oder gar Minuten sind, ist irrelevant. Doch kann man in Java-Programmen eine solche Maximalzeit garantieren? Die Probleme bei Java für Echtzeit-Systeme liegen in mehreren Bereichen, z.B.: 1. Der Garbage-Collector könnte jederzeit unkontrollierbar die Abarbeitung verzögern. 2. Der Hotspot-Compiler könnte ebenfalls unkontrollierbar die Abarbeitung verzögern. 3. Das Java Threading-Modell kennt zwar Prioritäten, garantiert diese aber nicht. Es gibt spezielle Realtime-JVMs, welche diese Probleme beheben, diese sind jedoch auch teuer und erfordern meist das Erlernen neuer Sprachelemente (heute oft in Form von Annotationen). Bei harten Realtime-Bedinungen wird man aber nicht ohne solche auskommen, und vor allem auch ein Realtime-Betriebssystem benötigen. Wobei harte Realtime-Bedingungen bedeuten, dass eine zu späte Reaktion einen Fehler darstellt (z.B. überhitzter Kernreaktor, der zu spät abgeschaltet wird). Bei weichen Realtime-Bedingungen sieht das schon anders aus. Unter weiche Realtime-Bedingungen fällt entweder der Wert der Reaktion nach Überschreitung der Maximalzeit, bis hin zur Wertlosigkeit, aber das Gesamt-System kann noch sinnvoll darauf reagieren - z.B. indem Messwerte verworfen werden. In einem solchen Umfeld reicht es also aus, jeweils zu wissen, ob die Reaktion schnell genug war oder nicht. Bis hinunter in den Millisekunden-Bereich ist eine herkömmliche JVM für solche Systeme durchaus eine Alternative. Oft muss bei Realtime-Systemen auf Interrupt-Requests reagiert werden. Diese werden üblicherweise von einem Modul direkt im Betriebssystem-Kernel mit einem Interrupt-Handler angenommen und ggf., z.B. über den Datenstrom eines Gerätetreibers an eine Anwendung weitergeleitet. Hier müsste die Zeit zwischen dem Annehmen des Interrupts und der fertigen Bearbeitung in der (Java-) Anwendung gemessen werden. Im Interrupt-Handler könnte man so als erstes einen Zeitstempel speichern:
static struct timespec timestamp;

static irqreturn_t mydriver_irq_handler(int irq, void *requested_devid)
{
    ... // check if irq is really for us

    getnstimeofday(×tamp);
    ...
    wake_up_interruptible(&queue); // wake read task.
    return IRQ_HANDLED;
}

Wenn es sich nicht um einen Realtime-Kernel handelt, ist allerdings zu beachten, dass schon der Interrupt-Handler selbst um eine unbestimmte Zeit verzögert sein kann. Falls durch Berücksichtigung zu später Reaktionen Schäden entstehen können, muss also ggf. mindestens ein Realtime-Kernel verwendet werden. Dieser würde dann z.B. dem Datenstrom zur Anwendung hinzugefügt:
static ssize_t mydriver_read(struct file *file, char *buf, 
                                          size_t count, loff_t * ppos)
{
        interruptible_sleep_on(&queue);

        char out[32];
        int len = snprintf(out, 32, "IRQ@%ld%09ld\n", 
                         (long) timestamp.tv_sec, timestamp.tv_nsec);

        if ( count < len ) {
                // buffer too small - this should not happen
                return 0;
        }

        copy_to_user(buf, out, len);
        return len;
}

In der Java-Anwendung muss nun dieser Zeitstempel (hier in Nanosekunden) mit einem Zeitstempel vergleichen werden, der nach der abgeschlossenen Reaktion angefertigt wurde. Dies könnte z.B. so aussehen (produktiv vermutlich eher in einem Thread):
static final long MAX_NANOSECS_DELAY = 20000000L;
        
public static void main(String[] args) throws IOException {
    NanoSecondsTimestampProvider timestamper 
            = new NanoSecondsTimestampProvider();
    System.out.printf( "Deviation: %dns\n", 
            timestamper.getNanoSecondsDeviation() );
        
    LineNumberReader in = new LineNumberReader( 
            new InputStreamReader( 
                    new FileInputStream("/dev/mydevice") ) );
    String line;
    while ( null != (line = in.readLine()) ) { 
                
        Result result = process(); // appplication specific
                
        long curNanoSecs = timestamper.currentNanoSecondsTimestamp(); 
        long irqNanoSecs = Long.parseLong(line.substring(4, line.length()));
        long delayNanoSecs = curNanoSecs-irqNanoSecs;
        if ( delayNanoSecs > MAX_NANOSECS_DELAY ) {
            logger.warn("processing too slow ..."); 
        } else {
            useResult(result); // appplication specific
        }       
    }   
}

(Wie man in Java einen absoluten Zeitstempel mit Nanosekunden-Auflösung und einer Genauigkeit im Mikrosekunden-Bereich programmieren kann, hatte ich in einem vorherigen Artikel beschrieben.) In dem Java-Teil der Anwendung ist zu beachten, dass bei der Zeitmessung garantiert sein muss, dass die Reaktion wirklich abgeschlossen ist. Wird also z.B. mit Fremdsystemen kommuniziert, muss sichergestellt sein, dass das jeweilige Fremdsystem die Nachricht auch bereits verarbeitet hat - z.B. durch Empfang einer Rückmeldung. Oft ist es besser eine Reaktion zu verwerfen, als eine Reaktion gelten zu lassen, die eigentlich zu spät abgeschlossen war. Der Weg vom Interrupt-Handler bis zur Java-Anwendung kostet auf meinem Arbeitssystem (Intel(R) Pentium(R) 4 Dual Core CPU 2.80GHz, Linux 2.6.28-15 unter Ubuntu 9.04 mit Sun Java (R) Version "1.6.0_15" Java HotSpot(R) Client VM) mit 0,1-05 Millisekunden. Dazu kommt dann eben noch die eigentliche Verarbeitung. Um zur anfänglichen Frage zurückzukommen: Zwar kann man mit herkömmlichen JVMs keine harten Realtime-Bedingungen erfüllen, aber weiche Realtime-Bedinungen im Millisekunden-Bereich sind durchaus erfüllbar.