Die Interprozesskommunikation führt ein fast unsichtbares Dasein in den Fundamenten des Systems, wo Nachrichten zwischen den Diensten hin und her flitzen. Doch auch hier finden gelegentlich Revolutionen statt, wie die Beispiele Binder und Kdbus verdeutlichen.
Der modulare Aufbau zeitgenössischer Computersysteme sorgt dafür, dass Programme häufig von den Daten anderer Prozesse abhängen. Die meisten Anwendungen reden deshalb mit Diensten, die im Hintergrund Daten verarbeiten oder über ein Netzwerk kommunizieren. Das Betriebssystem, genauer gesagt dessen Interprozesskommunikation (IPC, [1]), kümmert sich um die Datenübertragung zwischen den Prozessen. Sie stellt Verfahren bereit, mit denen Prozesse Daten über den Kernelspace austauschen.
Neuer Anstrich
Klassische Verfahren wie Pipes, Message Queues, Shared Memory oder Sockets, die so genannten Primitives, sind für diesen Zweck weit verbreitet, rücken aber allmählich in den Hintergrund. Sie bilden zwar noch die Basis der Datenübertragung, kommuniziert wird jedoch auf höherer Ebene. Auf Grundlage der klassischen Verfahren tauchen immer wieder neue Programmiersprachen und Frameworks auf. Sie unterstützen die Prozesse dabei, miteinander zu reden, indem sie passende Protokolle und Dienste anbieten. Zugleich verlagern sie die Kommunikation in Middleware [2], die meist eine gesonderte Software-Ebene bildet und in einem separaten Prozess läuft.
Anders als die Local Procedure Calls in Windows und die Doors in Solaris besitzt Linux, mit Ausnahme von Android, keinen leistungsfähigen IPC-Mechanismus. Das soll sich mit dem Kernel-Desktop-Bus (Kdbus, [3]) ändern. Android wiederum bringt mit Binder [4] zwar ein effizientes Verfahren zur Interprozesskommunikation mit, das jedoch außerhalb von Googles Betriebssystem nicht zum Zuge kommt.
Für die effiziente Datenübertragung verwenden Binder und Kdbus dedizierte und eng an die Middleware gekoppelte Kerneltreiber. Der folgende Artikel stellt Binder und Kdbus der klassischen IPC in Linux gegenüber.
Binder in Android
Die Interprozesskommunikation mit dem Binder-Framework zieht sich konsequent durch alle Ebenen des Android-Stack (Abbildung 1). “Die Android-Plattform setzt den Binder beinahe überall ein, wo Prozesse interagieren”, erklärt Dianne Hackborn, eine Binder-Entwicklerin [5]. Das Binder-Framework, das im Kern auf dem freien Open-Binder-Projekt [6] basiert, wurde für Android generalüberholt und an die speziellen Anforderung einer mobilen Plattform angepasst. Trotz umfangreicher Anpassungen entsprechen Aufbau und Ablauf der Kommunikation im Kern dem von Open Binder.
Durch die Service-orientierte Architektur von Android [7] kommunizieren Prozesse nach dem Client-Server-Prinzip, wobei der Kerneltreiber im Hintergrund hilft. Sowohl im Applikation Framework als auch auf nativer Ebene ermöglichen Dienste den Zugriff auch auf Funktionen der Hardware (Abbildung 1). Im Application Framework bilden sie die Basis, um Daten über High-Level-Komponenten wie Intents, Content Provider und Broadcast Receiver zu übertragen.
Die IPC ist in die Android-Runtime integriert, da Android alle Applikationen auf Kernelebene in voneinander isolierten Prozessen [8] ausführt, die jedoch miteinander reden müssen. Alle Dienste lässt sich ein Entwickler über die Android Debug Bridge ausgeben:
adb shell service list
Android kombiniert im Binder-Framework leichtgewichtige Remote Procedure Calls (LRPC, [9]) und mehrere Treiber, um Daten zwischen den isolierten Prozessen zu übertragen. Der Einsatz der Bionic Libc [10], einem Abkömmling von BSDs Standard-Libc, hat das System inkompatibel zu System-V und der klassischen Interprozesskommunikation gemacht, weshalb sich Androids IPC fast ausschließlich auf das Binder-Framework beschränkt.
Auf der nativen Ebene bündelt die Libbinder-Bibliothek die ganze Funktionalität des Binder-Framework [11] sowie die Schnittstellen zum Application Framework (Abbildung 1), um etwa von Java aus auf native Dienste zuzugreifen. Die Middleware wiederum verwendet unter anderem die Libbinder-Bibliothek, um Methoden von Diensten wie dem Mediaplayer, dem Kameradienst oder dem Audio Flinger aufzurufen.
Das Binder-Framework bietet zudem Sicherheitskonzepte für den Zugriff auf Daten und Ressourcen an ([12], [13]).Damit gestattet Android als erstes Linux-Betriebssystem die effiziente und systemweite IPC unabhängig von den klassischen Verfahren.
Klassiker
Wie aber sieht die klassische IPC aus? Indem ein System Prozesse durch Speicherbereiche und Schutzmechanismen strikt voneinander trennt, zwingt es diese, den Umweg über so genannte globale Strukturen wie Dateien oder virtuelle Speicherbereiche zu nehmen. Die klassischen Verfahren der Interprozesskommunikation, die Primitives, verwenden Schreib- und Lese-Operationen, um Daten zu übertragen. Zu den bekanntesten Primitives gehören Pipes, Message Queues, Sockets und Shared Memory. Der Ablauf erfolgt nach dem Peer-to-Peer-Prinzip, alle Prozesse dürfen gleichberechtigt lesen oder schreiben.
Im ersten Schritt verbindet das System die nicht-verwandten Prozesse wahlweise über einen Schlüssel, eine Datei oder mit Hilfe von Adressen. Hinterher greifen die Prozesse gemeinsam auf eine globale Struktur zu und tauschen Daten mit Hilfe von Schreib- und Lese-Operationen aus. Listing 1 veranschaulicht, wie ein Sender Daten über eine Pipe verschickt. Da dies über Dateien passiert, nutzen Sender und Empfänger gemeinsame Dateinamen, um miteinander zu kommunizieren.
Listing 1
IPC via Pipe
01 #define PIPE_NAME "/tmp/myPIPE"
02 [...]
03 int main(int argc, char** argv) {
04 [...]
05 // Connect to pipe
06 pipe = open(PIPE_NAME, O_WRONLY);
07 [...]
08 // Write data to pipe
09 write(pipe, data, strlen(data));
10 [...]
11 return 0;
12 }
Zwischenhändler
Unterschiede bei der Datenübertragung über Primitives bestehen nur darin, wie das System die Verbindungen aufbaut und wie die jeweiligen globalen Strukturen aussehen. Die Primitives sind tief im Betriebssystem verankert. Standards wie System-V und Posix definieren Schnittstellen, die Entwickler nutzen, um Primitives zu erstellen und zu löschen sowie Daten zu lesen und zu schreiben.
Dabei kämpft das System mit der Herausforderung, die Operationen auf die Primitives synchronisieren zu müssen. Diese Abstimmung soll verhindern, dass Prozesse zeitgleich auf globale Strukturen zugreifen, was zu fehlerhafter Synchronisation (Deadlocks) oder fehlerhaften Datenübertragungen (Race Conditions) führen kann.
Weil die Synchronisation aufwändig und fehleranfällig ist, entsteht in diesem Bereich Mehrarbeit für Entwickler, die das reine Übertragen der Nutzdaten zu einem Nebenkriegsschauplatz macht.
Hinter den Kulissen
In den Frameworks sorgt Middleware, die ihren Ursprung in verteilten Systemen hat, für mehr Überblick. Sie verbirgt die komplexe Infrastruktur aus Datenübertragung und Synchronisation hinter einer Abstraktionsschicht und verlagert die Datenübertragung auf eine gesonderte Software-Ebene. In der Middleware kommunizieren Prozesse über Dienste und Protokolle miteinander und folgen dabei dem klassischen Client-Server-Modell. Ein Server ist dabei ein Prozess, der Dienste bereitstellt, ein weiterer Prozess tritt in der Regel als Vermittler zwischen Client und Server auf.
Wer schon mal mit klassischen Methoden einen Dienst oder ein Protokoll programmiert hat, weiß die Vorteile von Middleware-Technologie zu schätzen. Im Unterschied zu Primitives baut hier ein separater Prozess die Verbindung auf und vermittelt anschließend zwischen den Client- und Server-Prozessen.
Beispiele dafür wären der D-Bus-Daemon von D-Bus und der Object-Request-Broker in Corba. Der Prozess überträgt das gesamte Datenaufkommen erst zum Vermittler und von dort zum Empfänger. Das halbiert zwar die Effizienz, reduziert dafür aber den Aufwand für den Entwickler deutlich.
Vermittlung bitte!
Steht die Verbindung, kann eine Applikation über eine wohldefinierte Schnittstelle Instanzen eines Dienstes aufrufen. Remote Procedure Calls (RPC) kommen als Technologie in Middleware sehr häufig zum Zuge. Auch der Desktop-Bus (D-Bus) und Corba sind verbreitete Architekturen für Middleware. Sie unterscheiden sich darin, welche Protokolle sie einsetzen, auf welche Weise sie komplexe Daten serialisieren und wie sie zwischen Client und Server vermitteln.
Feste Bindung
Als erstes Linux-Betriebssystem verwendet Android nicht mehr Primitives zur Interprozesskommunikation, sondern einen dedizierten Kerneltreiber (»/dev/binder« ). Zusammen mit Remote Procedure Calls bildet das Binder-Framework die Middleware in der Low-Level-Architektur von Android (Abbildung 2).
Ursprünglich stammt der Binder-Mechanismus aus dem Betriebssystem Beos [14]. Später entstand in dem freien Open-Binder-Projekt ein Framework für den Kernel 2.6.10, das sich jedoch nie durchsetzen konnte. Die Terminologie wurde allerdings beibehalten und setzt leider noch immer auf die teils irreführenden Bezeichnungen. So bezieht sich der Begriff Binder wahlweise auf das gesamte Framework, einen Treiber oder die Rahmenstruktur eines Dienstes. Für Android entwickelte Google das Binder-Framework neu und passte es an die Anforderungen der mobilen Plattform an.
Zu Diensten
Für die Komponenten der Low-Level-Architektur existiert wenig Dokumentation, sodass der systemweite Einsatz des Binder-Framework nur schwer nachvollziehbar ist. Der Entwickler definiert eine Schnittstelle für den Dienst, der in einem Serverprozess läuft, und weist ihm einen eindeutigen Namen zu. Sobald der Dienst startet, macht der Server ihn unter seinem Namen dem Context-Manager bekannt. Bei diesem handelt es sich um einen stets erreichbaren Dienst des Service-Manager-Prozesses. Letzterer wiederum gleicht einer Art Adressbuch, kennt alle anderen Dienste und liefert Daten für den Verbindungsaufbau. Er fungiert als Vermittler zwischen dem Client- und dem Serverprozess. Der Artikel zeigt weiter unten an Codeproben, wie die Prozesse miteinander reden.
Transaktionen und Token
Remote Procedure Calls übertragen ihre Daten mit Hilfe von Transaktionen. Hat der Initprozess einen von Androids Systemdiensten im Native Layer gestartet (Abbildung 2), legt der Serverprozess, in dessen Kontext der Dienst läuft, einen Transaktionspuffer an, auf den der Kerneltreiber Zugriff erhält. Der Puffer ist ein Speicherbereich, über den der Serverprozess Transaktionen empfängt. Der Kerneltreiber verwaltet ihn über eine ID, die Serverprozess und zugehörigen Dienst identifiziert.
Wie bei den Primitives setzt die Datenübertragung voraus, dass zwischen Client und Server eine Verbindung besteht. Der Client kennt den Namen des Dienstes und fordert beim Service-Manager die Schnittstelle (Proxy) an, die die Methoden des Dienstes enthält. Dabei hinterlegt der Service-Manager-Prozess beim Kerneltreiber einen Token für jede Verbindung zwischen Client und Server oder genauer gesagt zwischen dem Client und der ID des Transaktionspuffers.
Eine Transaktion enthält neben den verschiedenen Parametern einer Funktion auch immer den Token [15], über den der Server die entsprechende Verbindung und damit den Transaktionspuffer identifiziert. Für die Datenübertragung kopiert der Kerneltreiber die Transaktion dann aus dem Clientprozess direkt in den Transaktionspuffer des Servers. (Abbildung 3, Schritte 4 und 5).
Eine wichtige Nachricht
Listing 2 zeigt, wie ein Clientprozess den fiktiven “Hello World”-Dienst vom Service-Manager-Prozess anfordert und verwendet. Zuvor, im ersten Schritt, registriert der Serverprozess in Zeile 6 von Listing 3 aber erst mal den Dienst unter dessen Namen beim Service-Manager-Prozess (Abbildung 3, Schritt 1). Dann verbindet sich der Client mit dem Serverdienst, indem er diesen über seinen Namen beim Service-Manager-Prozess anfordert (Abbildung 3, Schritt 2).
Listing 3
Serverprozess meldet Dienst an
01 int main(int argc, char* argv[]) {
02 // Register buffer
03 sp<ProcessState> proc(ProcessState::self());
04 sp<IServiceManager> sm = defaultServiceManager();
05 // Register service
06 sm->addService(String16("helloWorld"),new HelloWorld());
07 // Start waiting for transactions
08 ProcessState::self()->startThreadPool();
09 IPCThreadState::self()->joinThreadPool();
10 }
Listing 2
Client fordert Dienst an
01 [...]
02 sp<IServiceManager> sm = defaultServiceManager();
03 // Get Service
04 sp<IBinder> binder = sm->getService(String16("helloWorld"));
05 sp<IHelloWorld> hw = interface_cast<IHelloWorld> (binder);
06 // Call remote method (initiate transaction)
07 hw->helloWorld();
08 [...]
Zusätzlich muss der Serverprozess in Zeile 8 von Listing 3 einen Thread-Pool eröffnen, erst dann kann er Transaktionen vom Kerneltreiber entgegennehmen und abarbeiten. Die Anzahl der Threads im Pool ist dabei auf 16 begrenzt.
Hat der Kerneltreiber eine Transaktion an einen Server gesendet, arbeitet er sie in einem Thread aus dem Thread-Pool ab. Die Kommunikation verläuft dabei synchron. Der Client blockiert, bis er eine Antwort vom Dienst erhält, der im Serverprozess läuft.
Während einfache Transaktionspuffer 1024 KByte nicht überschreiten, lassen sich größere Datenmengen mit Hilfe einer Zero-Copy-Operation übertragen. Diese verschickt nur noch einen Deskriptor, der auf einen Speicherbereich zeigt. Das Ashmem-Subsystem (Android Shared Memory) und das Pmem-Subsystem (Physical Memory) verwalten den gemeinsamen Speicherbereich.
Kdbus
Für Linux ist mit dem Kernel D-Bus (Kdbus) eine Methode der Interprozesskommunikation entstanden, die sehr an den Binder-Mechanismus in Android erinnert. Wie der Name schon verrät, basiert Kdbus im Kern auf D-Bus. Das IPC-Framework lagert die Middleware teilweise in einen separaten Prozess aus. Dieser zusätzliche D-Bus-Daemon (Abbildung 4) übernimmt die Funktion eines Vermittlers zwischen den Prozessen.
Prozesse interagieren im Fall von Kdbus über Nachrichten, die wiederum vergleichbar mit den Transaktionen von Binder sind. Das System überträgt sie über einen Bus im D-Bus-Daemon vom Sender zum Empfänger.
Kdbus verlagert nun den D-Bus-Daemon in den Kernel, um die Nachricht ohne Umweg zu übermitteln. Das soll effizienter sein und einige Einschränkungen von D-Bus beseitigen. Wo D-Bus zum Beispiel zehn Kopiervorgänge, vier Validierungen und vier Kontextwechsel für einen Request-Reply-Vorgang benötigt, braucht Kdbus nur noch zwei Kopiervorgänge, zwei Validierungen und zwei Kontextwechsel. Setzt der Client einen Request ab, wartet er nicht auf die Antwort des Servers, sondern widmet sich der nächsten Aufgabe. Das unterscheidet Kdbus von Binder, das Daten synchron überträgt.
Zusätzlich stecken weitere Sicherheitsroutinen in Kdbus, die Berechtigungen und Zugriffe überprüfen. Die Entwickler haben daneben den Umfang des Quellcodes reduziert und XML komplett aus dem Userspace verbannt. Im Gegensatz zu D-Bus, das erst nach den Early-Boot-, Initrd- und Late-Boot-Phasen einsatzbereit ist, steht Kdbus jederzeit zur Verfügung [16].
Überschreitet die Größe einer Nachricht 512 KByte, wird sie nicht mehr effizient über den Bus übertragen. Abhilfe schafft wieder der Zero-Copy-Mechanismus über das Memfds-Subsystem, das Binders Ashmem-Subsystem [17] entspricht. Auch hier zeigen File-Deskriptoren auf einen gemeinsamen Speicherbereich. Ein Prozess, der einen Shared Memory anlegt, muss anschließend nur noch den Memfd (Memory File Descriptor) für den Speicherbereich verschicken.
Zum Schutz vor Manipulationen lassen sich die Daten im Shared Memory zudem versiegeln (Sealing) und so nachträglich nicht mehr verändern. Als neues Feature hilft zudem die probabilistische Datenstruktur Bloomfilter dabei, Multicast- und Broadcast-Nachrichten effizienter zu versenden.
Weniger reden, mehr sagen
Während also die klassische Interprozesskommunikation die Primitives synchronisieren muss, bilden dedizierte Kerneltreiber bei Binder und Kdbus die Basis einer effizienteren Datenübertragung. Sie müssen die Daten nicht erst in eine globale Struktur und von dort in den Empfängerprozess übertragen, sondern kopieren sie ohne Zwischenschritt von einem Prozess in den anderen. Beide Kernel-basierten IPC-Varianten kommen so mit einem Minimum an Kopiervorgängen und Kontextwechseln aus. Neben der Datenübertragung überwachen die Kerneltreiber den Zugriff auf die Dienste eines Prozesses beziehungsweise eines Servers und bauen die Verbindung zwischen Client und Server auf.
Während die Middleware in Binder auf Remote Procedure Calls basiert, fußt die von Kdbus auf D-Bus. Beide Middleware-Technologien sind teilweise in die Kerneltreiber integriert. Mit klassischen Methoden würden Remote Procedure Calls und D-Bus zusätzliche Prozesse benötigen, um zwischen den Client- und Serverprozessen zu vermitteln.
Stattdessen synchronisieren beide Frameworks Ressourcen nur noch im Server und nicht mehr im Client. Frameworks liefern dem Entwickler neben der Infrastruktur zur Interprozesskommunikation auch Sicherheitskonzepte für den Zugriff auf Dienste, Daten und Ressourcen.
RAM versus CPU
Im Unterschied zu Binder, das Funktionsaufrufe vermittelt, versendet Kdbus Nachrichten. Innerhalb der Server kümmern sich in Binder simultane Threads um die Transaktionen. In Kdbus empfängt hingegen eine Fifo-Datenstruktur die Nachrichten und arbeitet sie nacheinander ab. Greg Kroah-Hartman, einer der Kdbus-Entwickler, beschreibt den Unterschied zwischen Binder und Kdbus wie folgt: “Binder ist an die CPU gebunden, D-Bus (und damit auch Kdbus) ist an RAM gebunden.” [18]
Binder und Kdbus folgen Sicherheitskonzepten, die aber den speziellen Anforderungen ihrer Plattformen entsprechen. Der Einsatz von Binder außerhalb von Android würde Sicherheitslücken einführen, die zuvor zu beheben wären. Kdbus ist konform zu Linux Security Modules (LSM) und ermöglicht somit den Einsatz von Modulen wie SE Linux [19]. Über einen Berechtigungsdienst wie Polkit kann ein Entwickler zudem Berechtigungen für einzelne Busse definieren.
Im Gegensatz zu Binder bietet Kdbus einen deutlich größeren Funktionsumfang: Zu dem von D-Bus kommen hier beispielsweise zusätzliche Sicherheitskonzepte und Bloomfilter zum Versand von Multicast-Nachrichten hinzu.
Ausblick
Kdbus steht in den Startlöchern und wird voraussichtlich mit dem Linux-Kernel 3.19 oder 3.20 ausgeliefert. Memfd und Sealing stecken bereits seit Version 3.17 im Kernel. Um Dienste oder Anwendungen zu implementieren, greift der Entwickler zu der Infrastruktur der Libsystemd-Bibliothek. Dass Kdbus seinen Vorgänger D-Bus ablösen wird, gilt als sicher, da die Software Schwachstellen von D-Bus entfernt und den Funktionsumfang erweitert.
Ob Kdbus die klassischen IPC-Methoden ablöst, muss sich erst zeigen. Ein heterogenes System wie Linux, das mehrere Möglichkeiten der Interprozesskommunikation anbietet, kann keine davon kurzfristig entsorgen. Um kompatibel zu D-Bus-Anwendungen und Diensten zu bleiben, bietet »systemd-bus-proxy« [20] eine Schnittstelle an, die den D-Bus-Daemon vertritt und eingehende Nachrichten über Kdbus weiterleitet.
Mit Binder stünde theoretisch eine kompakte IPC-Alternative zu Kdbus bereit. Allerdings bedürfte es einiger Entwicklungs- und vor allem Überzeugungsarbeit in der Linux-Community, um diese Alternative zu etablieren, weil sich beide Ansätze doch recht deutlich unterscheiden. Innerhalb von Android ist Binder ohne Frage unverzichtbar, ob es auch eine Zukunft in Linux hat, bleibt jedoch ungewiss. Die Linux-Entwickler haben vor allem Bedenken bezüglich der Sicherheit und des Designs.
Umgekehrt scheinen sich auch die Hoffnungen einiger Entwickler, Kdbus könne Binder in Android ablösen, weder in naher noch in ferner Zukunft zu erfüllen [18]. Zu viele Komponenten sind eng an Binder gekoppelt und machen eine Wechsel zu Kdbus nur mit großem Aufwand möglich.
Obwohl Binder und Kdbus Daten effizienter übertragen, gibt es zudem von einigen Seiten Widerstand dagegen, die Interprozesskommunikation in den Kernel zu verschieben. Einige Entwickler lehnen dies ab, weil der Quellcode traditionell in den Userspace gehöre ([21], [22]). Vermutlich wird Kdbus trotzdem kommen und die Interprozesskommunikation in Linux effizienter machen.
Infos
- Interprozesskommunikation: http://de.wikipedia.org/wiki/Interprozesskommunikation
- Middleware: http://de.wikipedia.org/wiki/Middleware
- Kdbus-Übersicht: https://github.com/gregkh/kdbus/blob/master/kdbus.txt
- Android Binder: http://developer.android.com/reference/android/os/IBinder.html
- Binder in Android: https://lkml.org/lkml/2009/6/25/3
- Open Binder: http://www.angryredplanet.com/~hackbod/openbinder/docs/html/
- Android-Systemarchitektur: https://source.android.com/devices/index.html
- Application Sandbox: https://source.android.com/devices/tech/security/overview/kernel-security.html#the-application-sandbox
- Lightweight Remote Procedure Call: http://dl.acm.org/citation.cfm?id=77650
- Androids Bionic Libc: http://en.wikipedia.org/wiki/Bionic_%28software%29
- Das Binder-Framework im Detail: https://thenewcircle.com/s/post/1340Deep_Dive_Into_Binder_Presentation.htm
- Personal Information: https://source.android.com/devices/tech/security/overview/app-security.html#personal-information
- Application Signing: https://source.android.com/devices/tech/security/overview/app-security.html#application-signing
- Beos: http://de.wikipedia.org/wiki/BeOS
- Binder-Token: http://www.androiddesignpatterns.com/2013/07/binders-window-tokens.html
- Lennart Poettering über Kdbus [Video]: http://mirror.linux.org.au/pub/linux.conf.au/2014/Friday/104-D-Bus_in_the_kernel_-_Lennart_Poettering.mp4
- Ashmem: http://www.androidenea.com/2010/03/share-memory-using-ashmem -and-binder-in.html
- Greg Kroah-Hartman über Kdbus und Binder: http://kroah.com/log/blog/2014/01/15/kdbus-details/
- Thorsten Schreiber, “Android Binder: Android Interprocess Communication”: Ruhr-Universität Bochum, 2011
- »systemd-bus-proxyd« : http://www.freedesktop.org/software/systemd/man/systemd-bus-proxyd.html
- Kritik an Kdbus auf der Kernel-Mailingliste: https://lkml.org/lkml/2014/11/29/6
- Kdbus-Kritik: https://forums.gentoo.org/viewtopic-t-1004624.html?sid=a52e6fc0b020177d94bbc8a0854c61f7










