Aus Linux-Magazin 06/2001

Java und das Betriebssystem

Abbildung 1: Funktionsweise des JNI.

Reine Java-Programme laufen auf allen Plattformen. Doch manchmal ist eine engere Verzahnung mit dem jeweiligen Betriebssystem notwendig. Wie das zu bewerkstelligen ist und welche Pakete es dafür in der Open-Source-Landschaft bereits gibt, zeigt dieser Beitrag.

Die Plattformunabhängigkeit wird immer als gewichtiges Argument für Java in die Waagschale geworfen. Ich halte dieses Argument aber für eher schwach, da Linux ja auch auf nahezu allen Plattformen läuft und mit der Verfügbarkeit des GCC und der Pakete Autoconf und Automake die Anwendungen sehr einfach auf vielen unterschiedlichen Betriebssystemen installierbar sind. Als Entwickler ist für mich die allgemeine Verfügbarkeit von standardisierten APIs für alle Bereiche wichtiger: Bei grafischen Oberflächen gibt’s keine Diskussionen à la Qt gegen Gtk; Messaging ist mit JMS flexibel und doch standardisiert; beim Mailing hilft das Java-Mail-API. Es gibt noch viele weitere Beispiele.

Selbst wenn ein Programm nur für eine spezielle Plattform entwickelt wird, ist also Java eine gute Wahl. Doch trotz umfassend vorhandener APIs gilt auch für Java, was für alle Bereiche der EDV gilt: Nothing is perfect. Es gibt daher mindestens drei Gründe C- oder C++-Code in Java einzubinden:

  • Performance: Als interpretierte Sprache kann Java nie die Geschwindigkeit erreichen, die native Programme haben. Insbesondere bei rechenintensiven Programmen zahlt sich der Aufwand aus übersetzten Code zu verwenden.
  • Legacy Code: Müssen Schnittstellen vorhandener Anwendungssysteme aus Java heraus bedient werden, bleibt nur die Möglichkeit, das Java Native Interface (kurz JNI) zu nutzen.
  • Plattformspezifische Funktionen: Java kann nur den kleinsten gemeinsamen Nenner der üblichen Betriebssystem-Funktionen unterstützen. Aus Java heraus sind deshalb Abfragen beispielsweise von Dateiberechtigungen nicht möglich – einfach deshalb, weil dieses Konzept unter Windows (fast) nicht vorhanden ist.

Gemeinsame Bibliotheksbenutzung mit dem JNI

Seit der ersten Java-Version ist eine Schnittstelle zwischen Java und nativen Programmen vorhanden. Das JNI erlaubt den Zugriff aus Java heraus auf Funktionen in Ladebibliotheken (Shared Libraries: unter Unix mit der Extension .so, unter Windows die bekannten und gefürchteten DLLs). Ebenso kann ein C- oder C++-Programm eine virtuelle Maschine starten, etwa um Scripting-Funktionalität zu implementieren. Hier gibt es aber mit Skriptsprachen wie Python deutlich bessere Optionen.

Das JNI wurde in einem früheren Coffee-Shop schon einmal im Detail vorgestellt [1], deshalb gehe ich hier nur noch kurz auf die grundlegende Funktionsweise ein. Die Abbildung 1 zeigt schematisch, was bei einem JNI-Aufruf passiert. Aufrufe einer in der Klassendefinition als nativ gekennzeichneten Methode werden durch die virtuelle Maschine an eine Funktion innerhalb einer Ladebibliothek weitergeleitet.

Der Prototyp dieser Funktion wird durch ein Hilfsprogramm (Javah) aus dem JDK automatisch erzeugt. Diese Funktionen erhalten alle Argumente der Java-Methode und zusätzlich einen so genannten Environment-Pointer, mit dessen Hilfe die vordefinierten APIs der virtuellen Maschine aufgerufen werden können, um beispielsweise Zugriff auf Klassen oder Methoden zu bekommen.

Trotz dieser einfachen Struktur ist es angebracht, mit dem JNI sehr vorsichtig umzugehen. Memory-Leaks und Threading-Probleme schleichen sich damit unbemerkt in sonst saubere Programme ein. Auch das Debuggen wird komplexer. Daher sollte der JNI-Code nur den absolut notwendigen Wrapper-Code enthalten, die eigentlichen Funktionen dagegen sollten außerhalb liegen und über normale C- oder C++-Programme verifiziert werden. Meist reicht es tatsächlich, nur die Umsetzung der Java-Parametertypen in normale C-Typen zu kodieren, etwa von einem jstring zu einem char*.

Abbildung 1: Funktionsweise des JNI.

Abbildung 1: Funktionsweise des JNI.

Kopieren geht über Programmieren

Zum Glück ist es in vielen Fällen nicht notwendig, selbst C-Code zu schreiben, obwohl gerade die Auseinandersetzung mit dem JNI nützliche Einblicke in die Funktionsweise von Java gewährt. Im Internet gibt es eine Reihe von Paketen, die gewissermaßen ready to run in eigene Programme einbindbar sind. Ich stelle eine Auswahl hier vor. Für Hinweise über weitere nützliche Pakete bin ich wie immer dankbar, das Internet ist inzwischen einfach zu groß, um den Überblick zu behalten.

Selbst wenn eines der verfügbaren Pakete nicht die eigenen Bedürfnisse deckt, kann es doch über Copy & Paste als Vorlage für eigene Implementationen dienen und damit die Entwicklung deutlich beschleunigen. Das ist im Sinne des Open-Source-Gedankens ausdrücklich erwünscht.

Bordmittel

Bevor JNI zum Einsatz kommt, ist aber zu erwägen, ob nicht einfache Bordmittel auch genügen. Listing 1 zeigt ein Code-Snippet, in dem ein Unix-Programm aus Java heraus gestartet wird. Dabei wird die Standardausgabe des Programms mit einem InputStream verbunden. Analog dazu kann man auch die Standardeingabe des Programms mit einem OutputStream verbinden.

Dieser Ansatz ist am leichtesten portabel, da das aufzurufende Programm zum Beispiel über eine Property definiert werden könnte, die dem Java-Programm über die -D-Option während des Systemstarts mitgeteilt wird. Unter Windows etwa wäre das einfach ein Programm mit analoger Funktionalität.

Portables API: Java Unix

Das Paket JavaUnix von Peter A. Pilgrim (Download über [2]) ist angelegt als portables API für Unix-Umgebungen, insbesondere auch als Zugriff auf die Filesystem-Funktionen. Bei der Architektur verwendet es das flexible Service Provider Interface (SPI), wie es auch von Java-Crypto-APIs oder dem JMS-API bekannt ist.

Die Abbildung 2 zeigt diese Architektur, die den Vorteil hat, dass die Implementation auf einfache Weise ausgetauscht werden kann. Peter Pilgrim versteht seine Implementation daher auch nur als Referenz-Implementation.

Für meine Tests verwendete ich das 1.0 Pre-Release. Es wird zwar ein Configure-Skript mitgeliefert, das damit erzeugte Makefile hat aber offensichtlich Probleme, jedenfalls kam es schon beim ersten javac-Kommando zu Fehlern. Die Abhängigkeiten zwischen den Java-Quellen sind offensichtlich nicht sauber definiert. Der folgende Trick schafft aber Abhilfe. Zuerst werden alle Java-Dateien auf einmal kompiliert:

> CLASSPATH=. javac `find . -name "*.java"`

Danach wird wieder make aufgerufen. Ein make all-jars und su -c “make install” komplettieren Kompilation und Installation. Vorher ist aber noch die Variable NATIVE_LIB_ EXT_DIR im Makefile anzupassen, in meinem Beispiel also auf $(JAVA_ HOME)/jre/lib/$(ARCH).

Nach diesen Klimmzügen stehen Funktionen für den Zugriff auf die Unix-(Laufzeit-)Umgebung sowie erweiterte Dateifunktionen zur Verfügung – alle sind gut dokumentiert. Die Dokumentation wird mit make javadoc erzeugt.

Dreh- und Angelpunkt von JavaUnix sind die Klassen javaunix.UnixSystem und javaunix.io.UnixFile. Erstere besitzt unter anderem statische Methoden für den Zugriff auf (Parent-) Process-ID, Gruppen und Passwort-Informationen sowie Mount-Infos. Die zweite Klasse ist eine Subklasse von java.io.File und erweitert diese um Unix-spezifische Funktionen, etwa Zugriffe auf Werte aus struct stat oder den Umgang mit Links.

Obwohl Portabilität ein wichtiges Ziel dieser Bibliothek ist, wird sie doch nicht völlig erreicht. Auf der Homepage ist ein Beitrag zu finden, der deutlich zeigt, dass es selbst auf ein und demselben System zwischen der Green-Thread– und der Native-Thread-Implementation zu Inkonsistenzen kommen kann. Aus meiner Sicht sind dies aber eher abwegige Beispiele, die ohne Probleme zu umgehen sind.

Abbildung 2: Die SPI-Architektur.

Abbildung 2: Die SPI-Architektur.

Hört die Signale

Der Umgang mit Signalen war bisher innerhalb von Java einfach nicht vorgesehen. Mit der Version 1.3 gibt es die Methode addShutdownHook() der Klasse java.lang.Runtime. Diese Methode erlaubt die Registrierung eines Threads, der immer dann läuft, wenn die virtuelle Maschine normal beendet wird oder einen SIGTERM erhält.

Damit ist schon ein großer Fortschritt erreicht, doch manchmal ist diese Funktionalität nicht genug. Nicht jedes Programm möchte mit SIGTERM beendet werden, obwohl dies zum guten Stil gehört. Gelegentlich sind andere Signale nützlich, um etwa eine Konfigurationsdatei neu einzulesen ohne das Programm zu beenden. Für diese Zwecke bietet sich das Paket JavaSignals an (verfügbar über [3]), das ein einfaches Interface für die Signalverarbeitung bereithält. In Listing 2 ist ein Beispiel dafür aufgeführt, wie eine Klasse die Signalverarbeitung einrichtet.

Signale werden von einer Klasse verarbeitet, die das Interface kh.signals.SignalListener implementiert. Über SignalManager.addListener() wird diese Klasse – wie auch vom Event-Mechanismus des AWT gewohnt – angemeldet. Die Methode isInterested() wird dann für alle Signale aufgerufen und sollte für alle gewünschten Signale true zurückgeben. Tritt ein solches Signal auf, ruft der SignalManager die Methoden signalReceived() aller interessierten Listener auf.

Beim Umgang mit Signalen unter Java ist allerdings höchste Vorsicht geboten. Signale werden von der virtuellen Maschine selbst verwendet, um die Interaktion verschiedene Programmteile zu steuern. Meiner Erfahrung nach funktioniert RMI nicht mehr, wenn die SIGUSR?-Signale abgefangen werden. Ein genauer Test unter verschiedenen VMs ist also angesagt.

Listing 1: Aufruf von Programmen aus Java

01: public class RtTest  {
02:
03:   public RtTest() {
04:   }
05:
06:   private void processCmd(String command) {
07:     int rc = -1;
08:     try {
09:       Process pr = Runtime.getRuntime().exec(command);
10:       InputStreamReader isr = new InputStreamReader(pr.getInputStream());
11:       BufferedReader input  = new BufferedReader(isr);
12:       pr.waitFor();
13:       rc = pr.exitValue();
14:       while (true) {
15:         String line = input.readLine();
16:         if (line == null)
17:           break;
18:         else
19:           // process line
20:       }
21:       input.close();
22:     } catch (Exception e) {
23:     }
24:   }
25: }

Komfort mit der Kommandozeile

Der Aufwand für die Erstellung eines GUI verschlingt gut und gerne 80 Prozent des Aufwands eines Projekts. Gerade in der Testphase tut es aber ein einfaches Kommandozeilen-Interface. Mit klassischen Mitteln (also über System.in) geht das zwar auch, aber wehe man vertippt sich einmal dabei – es gibt kein Zurück mehr und Funktionen wie filename completion gibt es schon gar nicht. JavaReadline (Download der aktuellen Version ist von [4] möglich) schließt diese Lücke. Es handelt sich um eine einfache Wrapper-Bibliothek um GNU-Readline- beziehungsweise GNU-History-Funktionen.

Listing 2: Signalverarbeitung mit Java Signals

01: import kh.signal.*;
02:
03: public class SigTest implements SignalListener {
04:
05:   public SigTest() {
06:     SignalManager.addListener(this);
07:   }
08:
09:   public boolean isInterested(int signalNum) {
10:     return signalNum == SIGHUP || signalNum == SIGTERM;
11:   }
12:
13:   public void signalReceived(int signalNum) {
14:     if (signalNum == SIGTERM) {
15:       System.out.println("Received SIGTERM");
16:       System.exit(0);
17:     } else if (signalNum == SIGHUP) {
18:       System.out.println("Received SIGHUP");
19:     }
20:   }
21: }

Syslog – nicht nur für Unix

Wird der Syslog-Daemon mit der Option -r gestartet und existiert in der Datei /etc/services der Eintrag syslog 514/udp,kann der normale Logging-Mechanismus von Linux verwendet werden, um Meldungen aus Programmen (insbesondere natürlich von Servern) zu protokollieren.

Es gibt dafür aber auch ein fertiges Syslog-Paket (siehe [5]), das noch nicht einmal nativen Code benötigt. Leider wird diese Möglichkeit von Serverprogrammen fast nie eingesetzt. Auf derselben Website gibt es auch ein Paket, das den Syslog-Daemon auch für Nicht-Unix-Betriebssysteme bereitstellt.

Eine etwas allgemeinere, daher aber auch komplexere Lösung ist das Paket Log4j aus dem Apache-Jakarta-Projekt ([6]). Das inzwischen sehr weit verbreitete Paket hat sich als De-facto-Standard fürs Logging durchgesetzt. Es erlaubt zudem, die Logmeldungen an den Syslog-Daemon zu senden. Für NT gibt es sogar ein JNI-Interface zum NT-Eventlog.

Das Öffnen des Syslog-Dienstes mit der Option -r heißt aber auch, einen Ansatz für Denial-of-Service-Angriffe liefern. Sie müssen noch nicht mal bösartig sein: Ein Programm, das in einer Schleife hängt und ständig Meldungen schickt, kann diesen Dienst und (bei schlechter Konfiguration) auch den gesamten Rechner lahm legen. Das ist auch möglich, wenn auf Festplatte geloggt wird: Läuft ein Filesystem voll, kann das katastrophale Auswirkungen haben.

Kommunizieren über Java Comm

Für den Zugriff auf serielle und parallele Ports gibt es von Sun das standardisierte Java Communications API. Es ist über die normale Java-Homepage als optionales Paket verfügbar, besteht allerdings nur aus den entsprechenden Interfaces. Für den eigentlichen Zugriff auf die Hardware ist nativer Code nötig. dafür gibt es zwei Lösungen: JCL/RXTX (Open-Source (siehe [7]) und eine von IBM (Download von [8]).

finally{}

Die hier vorgestellten Pakete zeigen, dass eine engere Integration von Java-Programmen mit Linux möglich ist, als Java im Grunde vorsieht. Im Einzelfall ist jedoch abzuwägen, ob die Vorteile einer Integration von nativem Code die Nachteile ausgleichen.

Unabhängig von der Art der Integration gibt es natürlich für viele bewährte Funktionalitäten inzwischen reine Java-Portierungen. Als Beispiele seien hier nur GNU-Getopt ([9]) und GNU-Regexp ([10]) genannt.

Leider gibt es für Java nichts Vergleichbares zum CPAN, deshalb ist es auch ziemlich aufwändig, nach Paketen mit speziellen Funktionen zu suchen. Mit diesem Artikel hoffe ich aber, Lust auf die Suche gemacht zu haben. ( uwo)

Infos

[1] Coffee-Shop: Goto für Java-Programmierer, Linux-Magazin 3/99, S. 85ff. (auch Online verfügbar)

[2] Java-Unix-Homepage: http://www.xenonsoft.demon.co.uk/javaunix.html

[3] Java-Signals: http://www.interstice.com/~kevinh

[4] Java-Readline: http://www.bablokb.de/java/readline.html

[5] Syslog-Paket: http://www.trustice.com/java/syslog

[6] Log4j-Homepage: http://jakarta.apache.org/log4j

[7] JCL/RXTX: http://www.frii.com/~jarvi/rxtx

[8] IBMs Java Communications API for Linux: http://www.ibm.com/java/jdk/linux130

[9] GNU-Getopt: http://www.urbanophile.com/arenn

[10] GNU-Regexp: http://www.cacas.org/java/gnu/regexp

Der Autor

Bernhard Bablok arbeitet bei der AGIS mbH (Allianz Gesellschaft für Informatik Service mbH) als Systemprogrammierer im System-Management-Bereich. Wenn er nicht Musik hört, mit dem Radl oder zu Fuß unterwegs ist, beschäftigt er sich mit Themen rund um die Objektorientierung. Er ist unter coffee-shop@bablokb.de zu erreichen.
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben