Alternativ zum bekannten »ioctl()« setzen Userspace-Tools Routen und Firewall-Regeln per Netlink-Interface. Diese Methode eignet sich auch für die Kommunikation mit anderen Subsystemen.
Viele Wege führen in den Kernel. Bekannt sind vor allem die Systemcalls und von denen die Funktionen »open()«, »close()«, »read()«, »write()« und »ioctl()«, die alle Daten aufnehmen oder zurückliefern. Die Unix-Urväter haben dem Aufruf »ioctl()« die Rolle einer Universalschnittstelle zugedacht, die Ein- und Ausgabegeräte steuert. Das war mit den übrigen Systemaufrufen nur schwer abzubilden.
Professionelle Programmierer mögen die Ioctls jedoch nicht. Zum einen sind die Aufrufe nicht portabel, zum anderen fehlerträchtig. Nachlässige Programmierer dokumentieren nur selten das Verhalten der Funktion im fehlerfreien und noch seltener im fehlerbehafteten Fall. Anwendungsentwickler gleichen Schlages verweigern ihrerseits hartnäckig das Lesen der Dokumentation.
Seit Langem schon gibt es auch andere, weniger bekannte Pfade ins Innere von Linux: Das Netlink-Interface [1] hat seinen Ursprung im Netzwerk-Subsystem. Hier dient es dazu, dem Kernel Konfigurationsdaten – etwa neue Routen – zu übergeben. Der umgekehrte Weg funktioniert ebenso. Über dieses Interface teilt der Kernel interessierten Applikationen mit, welche Routen er geändert hat.
Es geht aber noch weiter: Netlink ist eine Methode der Interprozess-Kommunikation (IPC), mit der Applikationen Daten mit sich selbst oder dem Kernel und der Kernel wiederum mit den Applikationen und ebenfalls mit sich selbst austauschen (siehe Abbildung 1).
Da das Interface zudem auf BSD-Sockets beruht, arbeitet es asynchron und unterstützt Multicasts. Ein einzelner Kernelthread kann daher gleich eine ganze Reihe von Prozesses mit identischen Daten versorgen. Und nicht zuletzt lassen sich auf diese Weise auch größere Datenmengen einfach transferieren. Angesichts dieser Vorteile verwundert es nicht, dass das Netlink-Interface auch außerhalb des Netzwerk-Subsystems zu Ruhm, Ehre und vor allem zum Einsatz gekommen ist.
Server im Kernel
Die angesprochene Kommunikation über Sockets folgt dem Client-Server-Modell. Im Fall von Netlink liegt der Server typischerweise im Kernel. Analog zur IP-Kommunikation benötigt auch Netlink einen Socket, allerdings wählt der Admin statt »PF_INET« hier »PF_NETLINK« (siehe Abbildung 2). Die Protokollfamilie »PF_NETLINK« hat zur Zeit 20 offizielle Mitglieder. Darunter befinden sich solche zum Handling von Routing-Informationen, zur Konfiguration der Firewall oder von IPsec. Die Headerdatei »netlink.h« listet die offiziellen Varianten auf.

Abbildung 2: Das Schema der Adressierung folgt bei der Protokollfamilie Netlink dem der bekannteren Familien, etwa »PF_INET« mit seinen BSD-Sockets. Was dort IP-Adressen sind, entspricht bei Netlink Prozess-IDs.
Den adressierten Kommunikationspartner spricht der Entwickler über eine eindeutige Kennung an, typischerweise die Prozess-ID. Ein Netlink-Server im Kernel bindet seinen Socket an eine Adresse und hinterlegt eine Callback-Funktion. Das Subsystem ruft die Funktion auf, sobald eine Nachricht mit dem entsprechenden Protokoll und der Zieladresse eingeht. Die Adressen bei Netlink sind jeweils PIDs; wer den Kernel adressieren will, wählt die Null.
Flexibles Protokoll
Prinzipiell besteht eine Nachricht, die zu einem beliebigen Netlink-Protokoll gehört, aus einem Kommando und den zum Kommando gehörigen Attributen (siehe Abbildung 3). Attribute wiederum bestehen aus einem Attribut-Typ und dem Attribut-Wert. Typ kann beispielsweise Integer oder String, der zugehörige Wert 42 oder “Hello World” sein. Makros, die alle den Namensvorsatz »nla_« tragen, dröseln die Nachricht in die verschiedenen Attribute auf.

Abbildung 3: Eine Netlink-Nachricht besteht aus Header und Nutzdaten. Letztere können wiederum einen protokollspezifischen Header und die Attribute enthalten.
Um einen Netlink-Server zu schreiben, definiert der Entwickler ein Netlink-Protokoll. Für Experimente eignen sich »NETLINK_GENERIC« oder eine selbst erdachte Nummer wie in Listing 1, Zeile 10. Soll der Code in den offiziellen Kernel in »include/linux/netlink.h« aufgenommen werden, muss Linus Torvalds freilich ein entsprechendes Patch akzeptieren.
|
Listing 1: Netlink-Server |
|---|
01 #include <linux/socket.h>
02 #include <linux/kernel.h>
03 #include <linux/module.h>
04 #include <linux/netlink.h>
05 #include <net/sock.h>
06 #include <net/tcp_states.h>
07
08 /* Sollte langfristig in
09 .../include/linux/netlink.h stehen */
10 #define NETLINK_TEST 25
11
12 #define MY_NL_COMMAND 0x11
13 #define MY_MESSAGE "Answer from Kernel"
14
15 static struct sock *nl_sk = NULL;
16 static DEFINE_MUTEX(nltest_mutex);
17
18 static void
19 nltest_netlink_server(struct sk_buff *skb)
20 {
21 struct nlmsghdr *nlh, *hdr;
22 u32 pid;
23 struct sk_buff *ret_skb;
24 char *data_ptr;
25
26 printk("nltest_netlink_server(%p)n", skb);
27 BUG_ON(!skb);
28 nlh = nlmsg_hdr(skb);
29 pid = nlh->nlmsg_pid;
30 printk("pid: %d, seq: %d, data: "%s"n",
31 pid, nlh->nlmsg_seq,
32 (char *)nlmsg_data(nlh));
33
34 /* Antwort verschicken */
35 ret_skb = nlmsg_new(NLMSG_GOODSIZE,
26 GFP_KERNEL);
37 if (ret_skb == NULL) {
38 printk("%s: no memn", __FUNCTION__);
39 return;
40 }
41
42 /* Header vorbereiten */
43 hdr = nlmsg_put(ret_skb, 0,
44 nlh->nlmsg_seq, NLMSG_DONE,
45 strlen(MY_MESSAGE), 0);
46 if (IS_ERR(hdr)) {
47 printk("nlmsg_put failedn");
48 nlmsg_free(ret_skb);
49 return;
50 }
51
52 data_ptr = nlmsg_data(hdr);
53 strcpy(data_ptr, MY_MESSAGE);
54 nlmsg_end(ret_skb, nlh);
55 netlink_unicast(nl_sk, ret_skb, pid,
56 MSG_DONTWAIT);
57 }
58
59 static int
60 __init nltest_module_init(void)
61 {
62 printk("nltest_module_initn");
63 nl_sk = netlink_kernel_create(&init_net,
64 NETLINK_TEST, 0,
65 nltest_netlink_server,
66 &nltest_mutex, THIS_MODULE);
67 return 0;
68 }
69
70 static void
71 __exit nltest_module_exit(void)
72 {
73 printk("nltest_module_exitn");
74 if (nl_sk)
75 sock_release(nl_sk->sk_socket);
76 }
77
78 module_init(nltest_module_init);
79 module_exit(nltest_module_exit);
80 MODULE_LICENSE("GPL");
|
Warten auf Rückruf
Die Kernelfunktion »netlink_kernel_create()« alloziert einen Socket, bindet ihn an eine Adresse (0 für den Kernel) und hängt eine Callback-Funktion in das gewählte Netlink-Protokoll ein (Listing 1, ab Zeile 59). Der erste Funktionsparameter definiert den vor Kurzem in den Kernel eingeführten Namensraum für das Netzwerk, der in fast allen Fällen »&init_net« lautet. Weiter darf der Entwickler dem Netlink-Subsystem einen Mutex übergeben. Das Subsystem stellt so sicher, dass nur jeweils ein Prozess die Callback-Funktion durchläuft. Den Socket erhält der Programmierer als Rückgabe der Funktion und gibt ihn mittels »sock_release()« frei, sobald er das Modul entfernt.
Der Kernel ruft die Callback-Funktion ab Zeile 18 im Kontext des Nachrichtensenders auf. Innerhalb des Callback steht eine Reihe von Funktionen und Makros zur Verfügung (siehe Tabelle 1), um die eigentliche Nachricht auszulesen und um anschließend zu antworten: Die Funktion »nlmsg_hdr()« erhält die Adresse des Socket-Buffer und gibt einen Zeiger auf den Header der Netlink-Nachricht zurück (Zeile 28, siehe Abbildung 4).

Abbildung 4: Der Netlink-Nachrichtenheader enthält neben der Absenderadresse auch das protokollspezifische Kommando. Die bitweise verknüpften Flags im Attribut »nlmsg_flags« steuern die Abläufe genauer.
|
Tabelle 1: Auswahl wichtiger |
|---|
Damit hat der Code Zugriff auf die PID des Absenders, das verwendete Kommando, seinen Nachrichten-Typ und auf die Sequenznummer (Listing 1, Zeilen 28 bis 32). Der Client inkrementiert die Sequenznummer mit jeder Abfrage. Es liegt nun bei der Callback-Funktion, die Daten auszuwerten. Dabei ist die Funktion »nlmsg_data()« behilflich. Genaue Angaben zu Prototypen und Erläuterungen liefert der Kernelquelltext in der Datei »include/net/netlink.h«.
Um zu antworten, benötigt der Kernelserver als Erstes einen Socket-Buffer, in dem er Header und Daten ablegt. Dafür reicht der Aufruf der Funktion »nlmsg_new()« in Zeile 35. Die Funktion erwartet zwei Parameter: Die Größe des für die Daten zu reservierenden Speicherbereichs – der Defaultwert »NLMSG_GOODSIZE« reicht meist aus – und ein Flag. Es regelt das Verhalten der Funktion für den Fall, dass nicht ausreichend Speicher zur Verfügung steht: »GFP_KERNEL« bedeutet, dass die Funktion schlafen darf, »GFP_ATOMIC« verbietet dies.
Um im neu erworbenen Socket-Buffer einen Netlink-Header anzulegen, ruft der Entwickler »nlmsg_put()« auf (Zeile 43). An dieser Stelle übergibt er auch die bereits erwähnte Sequenznummer und die eigene Absenderadresse. Außerdem übergibt er den Nachrichtentyp, den die Dokumentation oft auch als Kommando bezeichnet. Die ersten 16 Kommandos sind für Kontrollaufgaben reserviert, wobei der Kernel zurzeit nur die aus Abbildung 4 verwendet. Flags spezifizieren den Bearbeitungsmodus detaillierter.
Übergabe
Ist die Antwort vollständig aufgebaut, ruft der Programmierer »nlmsg_end()« auf und versendet sie mit »nlmsg_unicast()« (ab Zeilen 54). Diese Funktion erhält unter anderem die Adresse des Empfängers, also die zuvor im Empfangspaket gefundene PID. Der Funktionsname deutet bereits an, dass das Netlink-Interface auch den Versand von Multicast-Nachrichten zulässt. Das ist mit »nlmsg_multicast()« in der Tat möglich. Das Element »groups« in der Netlink-Adressenstruktur »sockaddr_nl« bestimmt, ob Linux die Nachricht an mehrere Empfänger auf einmal zustellt.
Auf der Client-Seite sind vier unterschiedliche Datenstrukturen zu initialisieren, falls der Code das bekannte Socket-Interface verwendet (siehe Abbildung 5). Zentral für die verbindungslose Socketkommunikation ist die Datenstruktur »struct msghdr« (Zeilen 58 bis 63). Damit lassen sich mehrere Datenpakete an einen Adressaten verschicken. Jedes Datenpaket spezifiziert dazu über die Datenstruktur »struct iovec« die Adresse und Anzahl der Pakete. Den eigentlichen Header mit Sequenznummer und Typ legt Netlink ebenso wie die Nutzdaten im Datenpaket ab.

Abbildung 5: Für den Aufruf von »sendmsg()« benötigt des Netlink-Subsystem diverse Datenstrukturen. Die Makros aus dem Kernel darf der Programmierer auch im Userspace zum Aufbau der Nachrichten verwenden.
Der Programmierer verpackt die Daten mit Hilfe der gleichen Makros zu Attributen, die im Kernel vorliegen. Es fehlen noch notwendige Adressangaben in »struct sockaddr_nl«. Die eigene Adresse bindet sich per »bind()« an den durch »socket()« allozierten Socket (Listing 2, Zeile 31). In der Struktur »struct msghdr« gibt es einen Verweis auf den Empfänger. Per »sendmsg()« gelangen die Daten in den Kernel (Zeile 63).
|
Listing 2: Netlink-Client |
|---|
01 #include <stdio.h>
02 #include <memory.h>
03 #include <malloc.h>
04 #include <unistd.h>
05 #include <bits/sockaddr.h>
06 #include <linux/netlink.h>
07 #include <sys/socket.h>
08
09 /* Sollte in langfristig in
10 .../include/linux/netlink.h stehen */
11 #define NETLINK_TEST 25
12
13 #define MY_NL_COMMAND 0x11
14 #define MAX_PAYLOAD 1024
15
16 int main(int argc, char **argv)
17 {
18 struct sockaddr_nl src_addr, dst_addr;
19 struct nlmsghdr *nlh = NULL;
20 struct msghdr msg;
21 struct iovec iov;
22 int sock_fd;
23
24 /* Eigenen Endpoint anlegen */
25 sock_fd = socket(PF_NETLINK,
26 SOCK_DGRAM, NETLINK_TEST);
27 memset(&src_addr, 0, sizeof(src_addr));
28 src_addr.nl_family = AF_NETLINK;
29 src_addr.nl_pid = getpid();
30 src_addr.nl_groups = 0; /* Unicast */
31 bind(sock_fd, (struct sockaddr*)&src_addr,
32 sizeof(src_addr));
33
34 /* Zieladresse vorbereiten */
35 memset(&dst_addr, 0, sizeof(dst_addr));
36 dst_addr.nl_family = AF_NETLINK;
37 dst_addr.nl_pid = 0; /* 0: Kernel */
38 dst_addr.nl_groups = 0; /* Unicast */
39
40 /* Netlink-Header erzeugen */
41 nlh = (struct nlmsghdr *)calloc(1,
42 NLMSG_SPACE(MAX_PAYLOAD));
43 memset(nlh, 0, NLMSG_SPACE(MAX_PAYLOAD));
44 nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
45 nlh->nlmsg_pid = getpid();
46 printf("Absender: %dn", nlh->nlmsg_pid);
47 nlh->nlmsg_flags = NLM_F_REQUEST|
48 NLM_F_ECHO;
49 nlh->nlmsg_type = MY_NL_COMMAND;
50
51
52 /* Daten einpacken */
53 strcpy(NLMSG_DATA(nlh), "Hello World");
54
55 iov.iov_base = (void *)nlh;
56 iov.iov_len = nlh->nlmsg_len;
57
58 memset(&msg,0,sizeof(msg));
59 msg.msg_name = (void *)&dst_addr;
60 msg.msg_namelen = sizeof(dst_addr);
61 msg.msg_iov = &iov;
62 msg.msg_iovlen = 1; /* Anzahl */
63 sendmsg(sock_fd, &msg, 0);
64
65 /* Netlink wartet auf eine eingehende
66 Nachricht vom Kernel */
67 printf("Warte auf Antwort...n");
68 recvmsg(sock_fd, &msg, 0);
69 printf("Antwort:ntflags %dn"
70 "ttype %dnt%sn",
71 nlh->nlmsg_flags,
72 nlh->nlmsg_type,
73 (char *)NLMSG_DATA(nlh));
74 close(sock_fd);
75 return 0;
76 }
|
Zum Empfang benötigt die Applikation ausreichend Datenpuffer. Am einfachsten lassen sich bereits verwendete Puffer recyceln. Dazu reicht sie der Code an die Funktion »recvmsg()« weiter. Sobald die Antwort da ist, meldet sich die Funktion zurück. Ist der Transfer komplett, gibt die Applikation den Socket durch die Funktion »close()« wieder frei.
Nützliche Bibliothek
Während sich das Socket-Interface für einfache Applikationen eignet, greifen Entwickler bei umfangreicheren Aufgaben auf die Netlink-Library »libnl« zurück [2]. Sie stellt ein Interface ähnlich dem des Kernels zur Verfügung. Anregungen zur Programmierung der Userspace-Seite finden sich in »accounting/getdelays.c« des Dokumentationsverzeichnisses des Kernels und in einem Beitrag von Asanga Udugama [3]. Die in Listing 1 und Listing 2 beschriebenen Programme für erste Schritte mit Netlink setzen einen Kernel 2.6.24 oder neuer voraus. Die Beispiele erklären allerdings nicht die Attributverarbeitung.
Ein Makefile hilft den Code zu kompilieren. Im Linux-Quelltext finden sich Beispiele, einzig die Variable »obj-m« muss der Entwickler auf »nltest.o« setzen. Experimentierwillige erzeugen mit »make« und »make nltestappl« Modul und Anwendung. Das entstehende Modul laden sie mit »insmod nltest.ko« in den Kernel und starten schließlich mit »./nltestappl« die zugehörige Anwendung. Sie schickt dem Kernel eine Nachricht, der seinerseits darauf antwortet. Das Syslog visualisiert die Vorgänge im Kernel. Es lässt sich mit »tail -n3 /var/log/kern.log« anzeigen. Haben Userspace und Kernel erfolgreich Daten ausgetauscht, lässt sich das Beispiel einfach erweitern, um etwa die Daten auch für eigene Entwicklungen zu nutzen. (mg)
|
Infos |
|---|
|
[1] RFC 3549, “Linux Netlink as an IP Services Protocol”:[http://www.ietf.org/rfc/rfc3549.txt] [2] Netlink Library »libnl«:[http://people.suug.ch/~tgr/libnl/] [3] Asanga Udugama, “Manipulating the Networking Environment Using RTNETLINK”: [http://linuxjournal.com/article/8498] |
|
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. |







