Aus Linux-Magazin 04/2008

Solaris-Software nach Linux portieren

© Marquis, photocase.com

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.
DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 3 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
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