Das Notifier-Subsystem ist die Nachrichtenzentrale des Linux-Kernels. Nur wenig Code ist nötig, um Entwickler mit den neusten Informationen zu versorgen.
Linus Torvalds steht fest zu seinem monolithischen Betriebssystemkern. Auch wenn es sich per definitionem um einen großen Code-Klumpen handelt, ist das System in Wahrheit modular und aus vielen Quellcode-Häppchen aufgebaut. Um diese Modularität zu verwirklichen, durchwirken die Entwickler den Kern mit vielen Schnittstellen, etwa für die Integration von neuem Code, den Datenaustausch mit dem Userspace, das Ankoppeln von Hardware und Vereinbarungen darüber, wie sie Zeit verwalten. Eine der unberechtigterweise bisher wenig beachteten Schnittstellen, das Notifier-Subsystem, regelt das zentrale Informations- und Ereignismanagement.
Ähnlich einem RSS-Feed versorgt der Kernel seine Abonnenten, also Subsysteme des Kernels oder Gerätetreiber, mit Neuigkeiten, etwa über einen bevorstehenden Shutdown, das Ändern des Prozessortakts oder darüber, dass er ein Kommunikations-Interface hoch- oder runterfährt. Der Empfänger sichert daraufhin Daten, passt die Berechnung von Zeitdifferenzen an oder sucht alternative Wege zur Datenübertragung.
Bitte um Rückruf
Das Notifier-Subsystem stellt den generischen Mechanismus zur Verfügung, über den Provider Nachrichten, Informationen respektive Ereignisse publizieren und Konsumenten diese Informationen ereignisorientiert empfangen. Sobald der Provider eine Nachricht über das Notifier-Subsystem publiziert, ruft dieses beim Empfänger eine Callback-Funktion auf.
Technisch betrachtet haben die Entwickler das Notifier-Subsystem einfach realisiert (siehe Abbildung 1): Der Kernel verwaltet für jede Informationsquelle eine Liste, eine Notifier-Chain. In sie hängt er die Callback-Funktionen über definierte Routinen ein und aus.
Stellt der Provider einen zu publizierenden neuen Systemzustand fest, beginnt das Notifier-Subsystem damit, die Callback-Funktionen der Reihe nach aufzurufen. Dabei hat der Provider die Möglichkeit, dem Callback einen Nachrichtentyp (Kommando, Ereignis, Datentyp »int«) und eine Adresse (Datentyp »void *«) zu übergeben. Als Ergebnis erhält er ein Bitfeld, mit dem er sich über den erfolgreichen Ablauf oder eventuelle Fehler informiert. Der Empfänger darf auch per Returnwert darum bitten, noch in der Chain befindliche und bisher nicht aufgerufene Callbacks zu canceln.

Abbildung 1: Im Zentrum des Subsystems steht die Notifier-Chain, in der der Kernel Callback-Funktionen nach Priorität sortiert ablegt. Jeder Rückruf hat einen der vier Typen »raw«, »atomic«, »blocking« oder »srcu«.
Quartett der Ketten
Die Situation verkompliziert, dass Kernelcode in verschiedenen Kontexten abläuft. Code auf Kernelebene darf dynamisch Speicher anfordern und sich beispielsweise schlafen legen. Auf der Ebene von Soft-IRQs und in Interrupt-Handlern ist das nicht erlaubt (siehe Abbildung 2). Die Konsequenz: Code, der dort abläuft, darf kritische Abschnitte keinesfalls über Semaphore schützen, da sie wartende Instanzen schlafen legen. Folglich könnten unterschiedliche Kontexte die Funktion zum Ein- und auch zum Aushängen einer Callback-Funktion aufrufen.
Linux stellt dazu passend gleich vier Notifier-Chain-Typen zur Verfügung und überlässt es dem Programmierer, den für seine Aufgabe optimalen Typ auszuwählen. Neben der Raw-Variante, die einfach nur den Mechanismus zum Einhängen, zum Aushängen und zum Aufruf der Callback-Funktion zur Verfügung stellt, gibt es noch die Atomic-, die Blocking- und die SRCU-Notifier-Chain. Letzteres Synchronisationselement steht für den etwas sperrigen Namen Sleepable Read Copy Update.
Die letzten drei Verfahren bringen neben der Grundfunktionalität noch Schutzmechanismen für die kritischen Abschnitte mit. Kritisch ist dabei das Ein- und Aushängen einer neuen Callback-Funktion in die Notifier-Chain und schließlich das Abarbeiten der Rückrufe selbst. Technisch unterscheiden sich die Chain-Typen also dadurch, wie sie kritische Abschnitte schützen. Die Atomic-Variante verwendet als Schutzelement ein Spinlock, das parallele Abläufe mit Hilfe von Interruptsperren und aktiven Warteschleifen, dem so genannten Spinning, verhindert. Diese Variante hat nur wenig Overhead.
Jede Kernelfunktion darf die Funktionen »atomic_notifier_chain_register()« zum Ein- und »atomic_notifier_chain_unregister()« zum Aushängen aus jedem Kontext heraus aufrufen. Insbesondere ist das auch aus einer Interrupt-Service-Routine (ISR) oder einem Tasklet heraus erlaubt. Tabelle 1 beschreibt die erlaubten Aufrufe des Notifier-Subsystems.
|
Tabelle 1: Funktionen |
|---|
Code muss wach bleiben
Der Kontext der Atomic-Variante bringt zwei Konsequenzen mit sich: Erstens dürfen Entwickler im Callback-Code keine Funktionen verwenden, die dazu führen, dass sich der Thread schlafen legt, und zweitens muss seine Laufzeit möglichst kurz sein.
Die Blocking-Variante verwendet zum Schutz der kritischen Abschnitte Semaphore. Die setzen ihrerseits einen Prozesskontext voraus, in dem der Scheduler den aktuellen Prozess schlafen legen darf. Hier implementieren Entwickler das Ein- und das Aushängen mit »blocking_notifier_chain_register()« und »blocking_notifier_chain_unregister()« in einem Kernelthread oder in einer Treiberfunktion, zum Beispiel in »driver_open()«.
Ein Aufruf aus einem Interruptkontext, einer ISR typischerweise, ist allerdings tabu. Der Provider initiiert die Abarbeitung der Callbacks im Prozesskontext, Programmierer berücksichtigen also, dass der Thread mitunter passiv blockiert, weil er schläft.
Nachrichtenaustausch
Die letzte Variante, SRCU Notifier Chains, steht alternativ zu den Blocking Notifier Chains. Anstelle eines Semaphors benutzen Torvalds & Co. ein SRCU-Synchronisationselement zum Schutz der kritischen Abschnitte. Sie weisen im Vergleich zum Semaphor einen geringen Laufzeit-Overhead auf. Ihr Nachteil: Das Aushängen einer Callback-Funktion ist zeitlich betrachtet teuer und die Initialisierung leicht schwieriger. Soll der Code also im Prozesskontext arbeiten und wird eine einmal aktivierte Callback-Funktion nur selten wieder ausgehängt, bietet sich diese Variante an. Achtung: Rekursion verboten! Mit Ausnahme der Raw-Notifier-Chain dürfen Entwickler das Aushängen eines Callback nicht in einem solchen selbst implementieren.
Listing 1 zeigt beispielhaft, wie eigener Kernelcode Notifier-Chains sowohl als Provider als auch als Konsument nutzt. Für den Provider sind nur zwei Dinge zu implementieren: Erstens definiert und initialisiert er mit Hilfe der Makros »Typ_NOTIFIER_HEAD« das erste Element der Liste. Die Headerdatei »linux/notifier.h« deklariert sie und lässt als Typ »raw«, »atomic« und »blocking« zu.
|
Listing 1: Notifier senden und |
|---|
01 #include <linux/module.h>
02 #include <linux/notifier.h>
03
04 #define USER_DEFINED_NOTIFIER_COMMAND 0x0001
05
06 /* PROVIDER */
07 static
08 BLOCKING_NOTIFIER_HEAD(user_defined_chain);
09
10 /* CONSUMER */
11 int consumer_callback(struct notifier_block
12 *self, unsigned long val, void *data)
13 {
14 printk("consumer: val: %ld, data: %pn",
15 val, data);
16 return NOTIFY_DONE;
17 }
18 static struct
19 notifier_block user_defined_notifier = {
20 .notifier_call = consumer_callback,
21 };
22
23 static int __init notifier_init(void)
24 {
25 blocking_notifier_chain_register(
26 &user_defined_chain,
27 &user_defined_notifier);
28 /* PROVIDER */
29 blocking_notifier_call_chain(
30 &user_defined_chain,
31 USER_DEFINED_NOTIFIER_COMMAND,
32 NULL);
33 return 0;
34 }
35
36 static void __exit notifier_exit(void)
37 {
38 blocking_notifier_chain_unregister(
39 &user_defined_chain,
40 &user_defined_notifier);
41 }
42
43 module_init(notifier_init);
44 module_exit(notifier_exit);
45 MODULE_LICENSE("GPL");
|
Handelt es sich um eine SRCU-Chain ist nur die Initialisierung zur Laufzeit erlaubt: Dazu verwendet der Entwickler die Funktion »srcu_init_notifier_head()«. Zweitens beginnt er bei Eintritt des Ereignisses, die Callback-Liste abzuarbeiten. Dazu ruft er die Funktion »Typ_notifier_call_chain()« auf. Diesmal ist als Typ zusätzlich »srcu« erlaubt.
Um als Konsument eine Informationsquelle anzuzapfen, muss die Adresse der vom Provider definierten Notifier-Chain bekannt sein. Außerdem legt er eine Datenstruktur an, in der er die Adresse der Callback-Funktion einträgt (Listing 1, Zeile 18). Mit dieser Information implementiert er das Abonnieren in Zeile 23 und das Aushängen in Zeile 36.
|
Tabelle 2: |
|---|
Die vom Konsumenten ebenfalls zu implementierende Callback-Funktion gibt einen der Werte aus Tabelle 2 zurück. Der Code leitet den Rückgabewert der zuletzt aufgerufenen Callbacks schließlich dem Provider als Returncode der »Typ_notifier_call_chain()«-Routine weiter. Programmierer verwenden die Codes »NOTIFY_STOP« oder »NOTIFY_BAD«, um sicherzustellen, dass der Fehlercode ihrer Routine auch beim Provider ankommt. Sie bewirken nämlich, dass das Notify-Subsystem keine weiteren ausstehenden Callbacks mehr aufruft (siehe Abbildung 3). Dadurch ist ihre Routine automatisch die zuletzt aufgerufene. Sie können »NOTIFY_STOP« und »NOTIFY_BAD« auch noch mit einem eigenen Rückgabewert kombinieren. Die Makros »notifier_from_errno()« und »notifier_to_errno()« aus »linux/notifier.h« helfen die Werte zu wandeln.
|
Listing 2: Netdev liefert |
|---|
01 #include <linux/module.h>
02 #include <linux/notifier.h>
03 #include <linux/inetdevice.h>
04
05 static int
06 inet_callback(struct notifier_block *self,
07 unsigned long val, void *data)
08 {
09 printk("inet_callback: Val=%ld, IP=%xn",
10 val, ntohl(((struct in_ifaddr *)
11 data)->ifa_address));
12 return NOTIFY_DONE;
13 }
14 static struct notifier_block inet_notifier = {
15 .notifier_call = inet_callback,
16 };
17
18 static int __init inet_init (void)
19 {
20 register_inetaddr_notifier(&inet_notifier);
21 return 0;
22 }
23
24 static void __exit inet_exit(void)
25 {
26 unregister_inetaddr_notifier(
27 &inet_notifier);
28 }
29
30 module_init(inet_init);
31 module_exit(inet_exit);
32 MODULE_LICENSE("GPL");
|

Abbildung 2: Blocking-Notifier sind auf Kernel- und Prozessebene verboten, Atomic-Notifier jedoch erlaubt.

Abbildung 3: Sobald der erste Callback »NOTIFY_STOP« zurückliefert, bricht das Notifier-Subsystem die weitere Verarbeitung von Callbacks ab.
User Generated Content
In der Datenstruktur, die die Adresse des Callback aufnimmt, darf der Programmierer zusätzlich eine Priorität vergeben. Sie bewegt sich im Bereich von null bis »INT_MAX«, also 0x7fffffff. Ein hoher Wert spiegelt hohe Priorität wider. Nach ihm sucht sich die Funktion ihren Platz in der Liste. Entsprechend der Reihenfolge ruft Linux bei Eintritt des Ereignisses die Routinen auf. Durch eine Priorität von »INT_MAX« platzieren Programmierer einen Rückruf ganz vorne. Den Kernel- und Treiberprogrammierer interessiert nicht nur das generische Notifier-Framework, sondern auch die darüber gehandelten Informationen, denn viele Subsysteme nutzen den Mechanismus.
Leider dokumentiert der aktuelle Kernel bis dato nicht, welche Nachrichtenquellen und Nachrichtentypen er definiert. Tabelle 3 listet die wichtigsten Subsysteme inklusive der zugehörigen Funktionen auf. Im Zweifelsfall hilft ein Blick in den zugehörigen Code. Entwickler sollten beachten, dass eine Nachrichtenquelle oftmals verschiedene Nachrichtentypen umsetzt. Der Kern erlaubt es nicht, auf Basis der Nachrichtentypen zu filtern. Auch wer nur eine Art von Nachrichten benötigt, zum Beispiel den Typ “Neue IP-Adresse”, muss zunächst sämtliche von ihnen entgegennehmen.
|
Tabelle 3: Notifier |
|---|
Schwieriger Abo-Service
Listing 2 demonstriert, wie Schreiber eigener Routinen eine vorhandene Nachrichtenquelle abonnieren, in diesem Fall die des Netzgeräte-Subsystems Netdev. Der Code zeigt, dass Subsystem-Maintainer die Adresse der Datenstruktur nicht selbst exportieren, sondern oftmals nur gekapselte Registrierungsfunktionen. Leider geht aus dem Namen der Registrierungsfunktion nicht hervor, um welchen Notifier-Chain-Typ (Raw, Atomic, Blocking, SRCU) es sich handelt.
Wer den Code mit Hilfe eines geeigneten Makefile auf einem Kernel ab 2.6.29 übersetzt und mit dem Kommando »insmod inet.ko« lädt, erhält im Eventlog in »/var/log/messages« immer dann eine Nachricht, wenn Netdev eine IP-Adresse neu vergibt. Weitere Beispiele, die den Rückgriff auf vorhandene Informationen demonstrieren, enthalten frühere Ausgaben der Kerntechnik ([1], [2]). (mg)
|
Infos |
|---|
|
[1] Eva-Katharina Kunst und Jürgen Quade,Kern-Technik, Folge 27, “Umgang mit Zeiten”: Linux-Magazin 04/06, S. 118 [2] Eva-Katharina Kunst und Jürgen Quade,Kern-Technik, Folge 45, “Treiber-Watchdogs”: Linux-Magazin 05/09, S. 96 |
|
Die Autoren |
|---|
|
Eva-Katharina Kunst, Journalistin, und Jürgen Quade, Professor an der Hochschule Niederrhein, sind seit den Anfängen von Linux Fans von Open Source. Unter dem Titel “Linux Treiber entwickeln” haben sie zusammen ein Buch zum Kernel 2.6 veröffentlicht. |









