Um Unix-Software von Solaris nach Linux zu portieren, sollte im Grunde das Neukompilieren reichen. Manchmal ist es damit auch tatsächlich getan. In vielen anderen Fällen aber lauern Fallen. Wie man sie erkennt und umgeht, beschreibt dieser Beitrag.
Beim Portieren geht es darum, Software auf eine andere Plattform zu verpflanzen als jene, für die sie ihre Programmierer ursprünglich geschrieben hatten. Im einfachsten Fall reicht es dafür bereits aus, das Programm auf der Zielplattform neu zu übersetzen. In vielen Fällen wird dies jedoch nicht gleich im ersten Versuch gelingen. Denn oft verbirgt sich hinter einer Portierung nämlich ein durchaus komplizierter Prozess, in dem der Entwickler die Software zuerst analysiert, dann modifiziert und schließlich testet – ein Zyklus, der sich mehrfach wiederholt, bis sie endlich zuverlässig auf dem Zielsystem läuft.
Im schlimmsten Fall kann es dabei sogar erforderlich sein, in das Programmdesign selbst einzugreifen und Teile des Code zu überarbeiten oder völlig neu zu schreiben. Dieser Beitrag behandelt die Probleme und passenden Lösungen bei der Transformation von Solaris-Software in native Linux-Anwendungen.
Der Nutzeffekt
Warum aber sollte jemand die Mühe auf sich nehmen und ein Solaris-Programm in die Linux-Welt migrieren wollen? Dafür können gleich mehrere Gründe sprechen: Ein häufiger Grund ist der Kostenvorteil. Unter Linux lässt sich vergleichsweise preiswerte Hardware verwenden, von der das Betriebssystem zudem im Gegensatz zu Solaris eine sehr breite Palette unterstützt. Weiter ist Linux-Support nicht selten billiger als die gleiche Dienstleistung für Solaris.
Ein weiterer Grund: Eine breite Phalanx Soft- und Hardwareproduzenten unterstützt Linux. Dadurch bindet sich der Kunde auch weniger stark an einen einzelnen Hersteller. Solaris dagegen supporten neben Sun Microsystems [1] selbst nur wenige andere Server-Produzenten. Manche Softwarehersteller – etwa IBM – wechseln zudem mit einem Teil ihrer Solaris-Applikationen ins Linux-Lager. Wer sie dann weiterhin benutzen möchte, findet darin womöglich einen Grund, auch mit anderen Anwendungen zu Linux zu wechseln.
Und schließlich kann auch die Performance den Ausschlag geben. Während beispielsweise Java-Anwendungen, Datenbanken oder Applikationen mit vielen Threads nicht selten besser unter Solaris zurechtkommen, gilt für viele Open-Source-Programme das Gegenteil: Sie erreichen ihre Bestform unter Linux.
Vorgehensweise
Will ein Anwender aus einem dieser Gründe eine Anwendung von Solaris [2] noch Linux portieren, dann hat sich für den Ablauf die folgende Schrittfolge in der Praxis bewährt:
1. Überprüfen der Migrierbarkeit. Die erste Phase untersucht, ob sich alle Features des fraglichen Programms überhaupt auf der Zielplattform umsetzen lassen. Möglicherweise braucht es dafür neuen Code.
2. Untersuchen der Quell-Software. Bevor irgendetwas auf dem Zielsystem laufen kann, gilt es, zunächst die Quell-Software gründlich zu verstehen. Dazu gehört zum Beispiel das Studium des Programmcode und der Dokumentation. Am Ende sollte sich für den Portierer ein klares Bild der vorhandenen Funktionen der Ausgangssoftware ergeben.
3. Werkzeugauswahl. Jetzt ist zu prüfen, ob die Softwaretools der Ausgangsplattform auch auf der Zielplattform verfügbar sind. Wenn nicht, gilt es, gleichwertigen Ersatz zu finden.
4. Review der Design-Unterlagen. Der Schritt überprüft noch einmal, ob das auf die Ursprungsplattform zugeschnittene Softwaredesign unverändert auf dem Zielsystem funktionieren kann. Jetzt steht aber vor allem die Architektur des jeweiligen Betriebssystems im Vordergrund, die womöglich Anpassungen erzwingen kann.
5. Portieren. Nun wird der Sourcecode auf das Zielsystem kopiert, dann das Environment so wie auf dem Ausgangssystem konfiguriert und schließlich der Compiler angeworfen. Dabei ist darauf zu achten, dass sich die Compiler-Optionen unterscheiden. Unter Solaris weist beispielsweise »-a« den Compiler [3] an, verschiedene Optimierungen anzuwenden, wogegen beim GCC-Compiler unter Linux [4] die Optionen »-O2« bis »-O5« etwas Ähnliches bewirken. Unter Solaris ist »-G« die Option, die den Compiler ein Shared Object erzeugen lässt, unter Linux ist für den gleichen Zweck »-shared« einzusetzen.
6. Übersetzen und Debuggen. Wahrscheinlich erzeugen die ersten Compilerläufe massenhaft Fehlermeldungen, die von den schon beschriebenen unterschiedlichen Compiler- und Linker-Optionen herrühren. In diesen Fällen muss der Entwickler das Makefile entsprechend anpassen. Ein weitere mögliche Fehlerursache steckt in der unterschiedlichen Byte-Reihenfolge: Big Endian bei Solaris auf Sparc, aber Little Endian im Fall von Intel-Linux.
Einige typische Portierungsprobleme
Unix-Applikationen verwenden in der Regel Systemcalls. Die Schnittstelle zu diesen Systemausrufen ist aber nicht überall gleich. Daraus können sich die folgenden Probleme ergeben:
- Die Zielplattform unterstützt einen Systemaufruf nicht.
Möglicherweise gibt es ersatzweise aber eine ähnliche
Funktion. - Beide Seiten unterstützen den Systemaufruf, verlangen aber
jeweils unterschiedliche Parameter. - Der Systemcall gibt auf beiden Seiten jeweils verschiedene
Werte zurück. - Der Systemaufruf wird auf dem Zielsystem weder
unterstützt, noch gibt es dort eine vergleichbare Funktion.
Hier ist in jedem Fall ein Wrapper zu schreiben.
Zusätzlich gibt es Unterschiede [5] zwischen der Solaris Thread Library und ihrem Gegenstück, der Pthread Library [6] unter Linux. Einige Funktionen der Solaris-Bibliothek existieren unter Linux nicht. Das betrifft namentlich die Funktionen »thr_suspend()«, »thr_continue()«, »thr_main()«, »thr_min_stack()«, »thr_getconcurrency()« und »thr_setconcurrency()«. In allen anderen Fällen gibt es zwar vergleichbare Funktionen (Tabelle 1), jedoch unterscheiden sich teilweise die nötigen Parameter.
| Tabelle 1: Verwandte Thread-Funktionen |
||
|---|---|---|
| Solaris Thread | Äquivalent der Linux Library | Pthread Library |
| thr_create() | Pthread_create() | |
| thr_exit() | Pthread_exit() | |
| thr_getprio() | Pthread_getschedparam() | |
| thr_getspecific() | Pthread_getspecific() | |
| thr_join() | Pthread_join() | |
| thr_keycreate() | Pthread_key_create() | |
| thr_kill() | Pthread_kill() | |
| thr_setprio() | Pthread_setschedparam() | |
| thr_setspecific() | Pthread_setspecific() | |
| thr_sigsetmask() | Pthread_sigmask() | |
| Mutex_destroy() | Pthread_mutex_destroy() | |
| Mutex_init() | Pthread_mutex_init() | |
| Mutex_lock() | Pthread_mutex_lock() | |
| Mutex_trylock() | Pthread_mutex_trylock() | |
| Mutex_unlock() | Pthread_mutex_unlock() | |
| cond_destroy() | Pthread_cond_destroy() | |
| cond_init() | Pthread_cond_init() | |
| cond_signal() | Pthread_cond_signal() | |
| cond_timedwait() | Pthread_cond_timedwait() | |
| cond_wait() | Pthread_cond_wait() | |
Compiler- und Linker-Optionen
Zwischen Sparc-Compilern und dem GCC gibt es Gemeinsamkeiten und Unterschiede, auf die es zu achten gilt, wenn der Code auf beiden Seiten zu übersetzen ist. Da es für diese Art Optionen keinen Standard gibt, kann eine gleichnamige Option ganz unterschiedliche Bedeutungen haben. Entsprechend sind die Makefiles anzupassen. Tabelle 2 listet die Gemeinsamkeiten zwischen Sparc-Compiler und GCC auf, Tabelle 3 die Unterschiede.
| Tabelle 2: Sparc-Compiler und GCC: Gemeinsamkeiten |
|
|---|---|
| Option | Bedeutung |
| »-c« | Nur Kompilieren, kein Linken |
| »-o FILE« | Dieser Parameter gibt das Ausgabe-File an |
| »-I DIR« | Gibt das Verzeichnis an, in dem nach Include-Files gesucht wird |
| »-L DIR« | Gibt das Verzeichnis an, in dem nach Bibliotheken gesucht wird |
| »-lname« | Bindet die namentlich bezeichnete Bibliothek ein |
| »-Aname[(token)]« | Definiert ISO C Assertion |
| »-Dname[=val]« | Definiert ein Preprozessor-Makro |
| »-Uname« | Angabe eines undefinierten Preprozessor-Makros |
| »-g« | Bestimmt, dass der Compiler Debugging-Informationen erzeugen soll |
| »-E« | Führt das Preprocessing aus und schreibt die Ergebnisse nach Stdout |
| »-S« | Bewirkt, dass der Code nur kompiliert, aber nicht assembliert wird |
| »-w« | Unterdrückt Warnungen |
| Tabelle 3: Sparc-Compiler und GCC: Unterschiede |
|||
|---|---|---|---|
| Solaris | Linux | Beschreibung | |
| »-v« | »-Wall« | Bewirkt, dass der Compiler mehr semantische Checks und Prüfungen im Stil von Lint durchführt |
|
| »-fast« | »-O« | Bewirkt die Optimierung auf maximale Ausführungsgeschwindigkeit auf dem Zielsystem |
|
| »-xar« | »-na-« | Erzeugt Archiv-Bibliotheken | |
| »cc« | »gcc« | Das grundlegende Kommando zum Kompilieren | |
| »-s« | »-Wl,-S«, »-Wl,-s« | Diese Option entfernt alle symbolischen Debugging-Informationen aus der Ausgabedatei; mit dem GCC-Compiler muss dafür »-Wl,-S« verwendet werden |
|
| »-staticlib« | »-static« | Bestimmt, ob Bibliotheken statisch zu linken sind; gibt man bei Solaris sowohl »-library« als auch »-staticlib« an (»-static« in GCC), wird die benannte Bibliothek statisch gelinkt |
|
| »-Bstatic« | »-static« | Sagt dem Compiler, dass die Applikation statisch zu linken ist |
|
| »-Bdyanmic« | default | Weist den Compiler an, die Applikation dynamisch zu linken | |
| »-KPIC« | »-fPIC« | Generiert positionsunabhängigen Code | |
| »-xpg« | »-pg« | Bereitet die Objekte so vor, dass sie Daten für den Profiler Gproof enthalten |
|
| »-err=warn« | »-Werrors« | Stellt die Stufe für Warnungen und Fehlermeldungen ein |
|
Wrapper
Ein Wrapper erlaubt es der Software, in fremder Umgebung wie gewohnt zu agieren. Betrachtet man einen Wrapper durch die Programmierer-Brille als Design Pattern [7], fällt die große Ähnlichkeit zum berühmten Adapter-Pattern auf. Die Unterschiede sind nicht vorwiegend strukturell, sondern liegen im Zweck: Der Adapter versucht ein Objekt zur Zusammenarbeit mit anderen bekannten Objekten zu befähigen, die etwas Bestimmtes erwarten.
Der Wrapper dagegen stellt ein anderes Interface zur Verfügung – ohne seinen Gegenpart vorher zu kennen -, um auf diese Weise Kompatibilitätsprobleme zu lösen. Zu den Solaris-APIs, die unter Linux einen solchen Wrapper benötigen, gehören unter anderem:
- »gethrtime()«: Diese Funktion liefert einen hoch
aufgelösten Echtzeitwert, der die Anzahl Nanosekunden ab einem
bestimmten Datum in der Vergangenheit angibt, unabhängig von
der Tageszeit der Systemuhr. Unter Linux gibt es keinen
äquivalenten Aufruf. Allerdings kennt Linux die Funktion
»get_cycles()«, die die Anzahl der CPU-Zyklen ab einem
Startdatum liefert. Wer nun das Ergebnis eines Getcycles-Aufrufs
durch die Taktrate der CPU teilt, erhält genau das gleiche
Ergebnis, das auch »gethrtime()« liefert. Diese
Zwischenrechnung kann ein Wrapper übernehmen. Die Taktrate
liest er dafür aus »/proc/cpuinfo« aus. - Zweites Beispiel: »sigsend()«. Diese
Solaris-Funktion schickt Prozessen beliebige Signale und ist unter
Linux ebenfalls nicht verfügbar. Allerdings lässt sich
auch hier einfach ein Wrapper schreiben, der das Linux-Kommando
»kill« verwendet. Es bietet die gleiche Funktion und
eignet sich damit für einen Sigsend-Nachbau.
Bei Solaris kommt standardmäßig die Korn-Shell zum Einsatz, bei Linux dagegen die Bourne-Shell. Das kann in einigen Fällen zu Problemen beim Portieren führen, obwohl sich beide Shells sonst in vielen Punkten ähneln.
Unterschiede in Shellskripten
Ein wichtiger Unterschied ist zum Beispiel die Methode, mit der der Anwender Environment-Variablen setzt. In der Korn-Shell geschieht dies durch:
setenv var /Pfad/Datei
Wogegen man unter Linux schreiben müsste:
export var=/Pfad/Datei
Eine Möglichkeit, das Problem zu umgehen, wäre eine Evironment-Variable »OSNAME«. In Abhängigkeit von ihrem Inhalt würde das Skript dann die eine oder andere Form verwenden.
Probleme mit Bibliotheken
Auch Third Party Libraries können zu Portierungsproblemen führen, wenn sie das Zielsystem nicht unterstützen. In diesem Fall muss der Entwickler nach einer möglichst ähnlichen Bibliothek suchen. In manchen Fällen bleibt aber nichts anderes übrig, als den Code der Bibliothek selbst anzupassen.
Ein Beispiel für diese Art von Bibliothek ist Power Tier, ein Cache-basierter Applikationsserver, der unter Solaris verbreitet ist, aber Linux nicht unterstützt. Doch mit Edge Xtend gibt es auch unter Linux ein Produkt, das ganz ähnlich arbeitet und sich als Ersatz anbietet-
Code für zwei Plattformen warten
Soll der Sourcecode auf beiden Plattformen zum Einsatz kommen, dann sind Vorkehrungen zu treffen, um ihn einfach plattformabhängig kompilieren zu können. Bewährt hat sich dabei ein spezielles Header-File, das jeweils alle anderen Header-Files für eine Zielumgebung inkludiert, und ein weiteres Source-File für die nötigen Wrapper.
| Listing 1: Für zwei Plattformen kompilieren |
|---|
01 #if defined(__linux__) 02 #include <Apps/comm.h> 03 #endif 04 05 #include "testAgent.hh" 06 #ifdef __solaris__ 07 #include <ulimit.h> 08 #endif |
Auf diese Weise bleibt der eigentliche Sourcecode des Programms unberührt und ist einfacher zu warten und zu testen. Damit ergibt sich eine Struktur, wie sie beispielhaft Listing 1 demonstriert. Die Datei »comm.h« versammelt hier die Linux-spezifischen Header. (jcb)
| Infos |
|---|
| [1] Sun Microsystems: [http://www.sun.com]
[2] Solaris: [http://www.sun.com/software/solaris/index.jsp] [3] Sun Studio Compiler: [http://developers.sun.com/sunstudio/] [4] GCC-Compiler für Linux: [http://www.cisco.de] [5] Vergleich zwischen Solaris und Linux: [http://developers.sun.com/solaris/articles/solaris_linux_app.html] [6] Posix Thread Programming: [https://computing.llnl.gov/tutorials/pthreads/] [7] Design Patterns: [http://de.wikipedia.org/wiki/Entwurfsmuster] |
| Der Autor |
|---|
| Anindya Adhikari arbeitet für die Firma Unisys in Bangalore, Indien. Zuvor war er bei verschiedenen anderen großen IT-Firmen wie IBM, LG oder HCL Technologies beschäftigt, wo er an Betriebssystemen programmierte und mit der Portierung von Software zwischen verschiedenen Plattformen befasst war. |





