OpenSSL: Implementierung innerhalb eines Client- und Server-Programms, Teil 4

Der in den vergangenen drei Artikeln herangereifte WOPR-Server spendierte jedem Client einen Handler als eigenständigen Betriebssystemprozess. Moderner lässt sich das mit Threads implementieren. Was gilt es beim Umstieg auf Threads zu beachten?

In Pocket speichern vorlesen Druckansicht
Lesezeit: 16 Min.
Von
  • Oliver Müller
Inhaltsverzeichnis

Der in den vergangenen drei Artikeln herangereifte und robuster gestaltete WOPR-Server basierte noch auf parallelen Prozessen. Der Server spendierte jedem Client einen Handler als eigenständigen Betriebssystemprozess. Moderner lässt sich dies mit Threads implementieren. Zumal OpenSSL ebenfalls Thread-fähig ist. Was gilt es bei WOPR und bei OpenSSL beim Umstieg auf Threads zu beachten?

Neben dem klassischen Unix-fork() können OpenSSL-Programme auch auf Threads aufsetzen. OpenSSL ist kompatibel zu mehreren Thread-Implementierungen, beispielsweise Windows-, Solaris- und POSIX-Threads. Um wieder die größtmögliche Kompatibilität zu vielen Systemen zu ermöglichen, konzentriert sich der Artikel auf POSIX-Threads. Sie sind im Gros der Unixoiden und in vielen POSIX-Layern von Nicht-Unix-Systemen implementiert. Der Multithreading WOPR 2.00 ist damit auf den meisten Unix- und Linux Systemen ebenso lauffähig wie auf Windows (über Cygwin). Die großen Systeme werden ebenfalls unterstützt: z/OS – über die UNIX System Services – und diesmal auch OpenVMS. Außen vor bleiben lediglich Systeme, die keine POSIX-Threads implementieren. Darunter sind MINIX, kNIX auf OS/2 beziehungsweise eComStation oder auch natives Windows. Windows benötigt einen Layer oder Wrapper für das Umsetzen seiner Thread-API auf POSIX-Threads. Diese Aufgabe erfüllt jedoch Cygwin vortrefflich.

Mehr Infos

OpenSSL – Struktur und Beispielcode

Den für die Umsetzung in der Praxis benötigten Beispielcode findet man hier (mueller_wopr-2.00.tar).

Der Build des Thread-fähigen WOPR-Servers geschieht genauso mit GNU make wie der der fork()-basierten Versionen 1.00 und 1.01. Lediglich auf dem neu unterstützten OpenVMS erfolgt der Build mit der DCL Command Procedure MAKEVMS.COM, das HP C++ voraussetzt. Ein Aufruf mit @makevms auf dem DCL-Prompt genügt, um den WOPR-Server zu bauen. Das Terminal-Programm bleibt jedoch bei OpenVMS nach wie vor außen vor – aufgrund der aus den ersten beiden Teilen bekannten Einschränkungen im select(). OpenVMS kann somit nur den WOPR-Server bereitstellen. Der lässt sich aber von WOPR-Terminals auf anderen Plattformen problemlos kontaktieren.

Doch nun zum Quelltext und damit in medias res. Beim Wechsel von fork() auf POSIX-Threads erwartet OpenSSL das Implementieren einiger Callback-Funktionen für seine Verwaltung. Die OpenSSL Funktionen operieren intern auf globalen Datenstrukturen. Beim fork()-Ansatz ergibt sich hier kein Problem. WOPR 1.x bestand nur aus einem einzigen Thread. Jeder WOPR-Prozess hatte seinen eigenen Speicherbereich mit einem eigenen Satz der globalen OpenSSL-Datenstrukturen.

Diese existieren aber nur einmal pro (heavy weight) Prozess. Beim Multithreading teilen sich damit alle Threads innerhalb eines Prozesses den einen einzigen Satz dieser globalen Strukturen. Die Konsistenz der Datenstrukturen muss trotz der konkurrierenden Zugriffe der parallel laufenden Threads gesichert sein. Die Sicherung übernimmt OpenSSL zwar grundsätzlich selbst, erwartet jedoch, dass das Anwendungsprogramm bestimmte Callback-Funktionen implementiert. Die Callbacks haben die Aufgabe die konkurrierenden Zugriffe zu koordinieren, beispielsweise durch Mutexes. In ihnen ist im Übrigen die Unabhängigkeit von OpenSSL vom verwendeten Thread-Modell des Systems begründet. Wie der konkurrierende Zugriff verhindert wird, hängt einzig und allein von der Implementierung der Callbacks durch das Anwendungsprogramm ab.

OpenSSL benötigt zum einen einen Callback, der den aktuellen Thread eindeutig identifiziert. Zum anderen ist ein Callback notwendig, der den koordinierten Zugriff auf gemeinsam genutzte Ressourcen implementiert. Dieses klassische Verfahren operiert auf einem fixen Array von Locks, beispielsweise Mutexes. Das Array ist vom Anwendungsprogramm bereitzustellen und hat eine von OpenSSL vorgegebene feste Größe. Seit Version 0.9.5b erlaubt OpenSSL auch das dynamische Erzeugen der Locks. Hierzu sind einige Callbacks zusätzlich notwendig. Die dynamischen Locks ermöglichen einigen OpenSSL-Teilen eine bessere Performanz.

Doch zunächst zum Callback zur eindeutigen Identifizierung der Threads. Die Callback-Funktion hat den Prototypen unsigned long ssl_thread_id(void). (Der Funktionsname ist dabei selbstverständlich frei wählbar.) Der Rückgabewert vom Typ unsigned long kann ein beliebiger, aber eindeutiger Wert sein. Er muss lediglich für die Threads eindeutig und paarweise verschieden sein.

In der Praxis hat sich eingebürgert, schlicht die Thread-ID (pthread_self()) auf unsigned long zu "casten". In den meisten Fällen ist die ID ein Zeiger. Das Verfahren funktioniert, solange er in den genannten Integer-Datentyp passt. Es gibt aber auch Ausnahmen: Die z/OS UNIX System Services beispielsweise implementieren keinen simplen Zeiger, sondern geben eine struct mit einem 8-Byte-Array als einziges Element zurück. Des Weiteren ist beispielsweise das 64-bittige OpenVMS auf Alpha und Itanium ein Vertreter, bei dem die Pointer-Breite (64-bit-Wert) mit der Größe von unsigned long (nur 32 bit) nicht übereinstimmt. Die Gründe liegen in den Tiefen der Aufwärtskompatibilität des 32-bittigen OpenVMS auf VAX begründet.

Die Implementierung des Identitäts-Callbacks im WOPR-Server liegt – wie alle Callbacks – in der Datei ssl-thread.cpp. Die Funktion ssl_thread_id() nutzt hierbei den "klassischen" Cast beziehungsweise im Fall von z/OS das Konvertieren des Arrays aus pthread_t in den erwarteten unsigned long.

Um den Problemen mit dem zwar beliebten, aber teils "beschränkenden" Cast aus dem Wege zu gehen, führt OpenSSL mit Version 1.0.0 einen neuen Identitäts-Callback ein. Er ist jedoch lediglich eine Alternative. Die alte Variante versteht OpenSSL 1.0.0 ebenfalls noch. Es darf allerdings nur die eine oder andere gesetzt werden. Die neue Variante des Callbacks folgt dem Prototypen void ssl_thread_id(CRYPTO_THREADID *id). Statt eines Rückgabewerts liefert der Callback das Identitätskriterium in der struct vom Typ CRYPTO_THREADID zurück, deren Zeiger als Argument übergeben wurde. CRYPTO_THREADID enthält nichts anderes als einmal den alten unsigned long und einmal einen void* als Elemente. Aus Rücksicht auf spätere Erweiterungen oder Änderungen lassen sich die Elemente nur über die OpenSSL-Funktionen CRYPTO_THREADID_set_numeric() und CRYPTO_THREADID_set_pointer() setzen beziehungsweise verändern.

Die Idee ist recht einfach: Alles, was sich noch numerisch in den gegebenen Schranken identifizieren lässt, wandert wie zuvor in den unsigned long; alles, was sich als Pointer darstellen lässt, geht in den neuen void*. Auf die Weise kann kein Informationsverlust mehr auftreten, und die Identität ist gesichert.

Die Implementierung von ssl_thread_id für OpenSSL 1.0.0 zeigt das recht deutlich. Mit CRYPTO_THREADID_set_pointer(id, (void*) pthread_self()) lässt sich – systemunabhängig – der Zeiger von pthread_self() ohne Informationsverlust einfangen. Lediglich bei z/OS geht WOPR 2.00 einen anderen Weg: Hier wird der in einen unsigned long passende (eigenwillige) Rückgabewert von pthread_self() über CRYPTO_THREADID_set_numeric(id, x) in eine Ganzzahl gepackt.

CRYPTO_set_id_callback(ssl_thread_id) übergibt den Callback mit der alten Spezifikation dem OpenSSL-System. CRYPTO_THREADID_set_callback(ssl_thread_id) setzt den Callback mit neuer Syntax ab OpenSSL 1.0.0. Das Registrieren dieser und aller folgenden Callbacks erfolgt in WOPR 2.00 im Konstruktor der Serverklasse WOPR_server.