Sicherheitsbewusste Kernelhacker beäugen skeptisch Netzwerkapplikationen, die in den Kernel wandern sollen. Manchmal gibt\’s dafür aber gute Gründe – Performance etwa.
Wenn Anwender besondere Anforderungen an den Leistungsdurchsatz legen, kann die Verlagerung von Code aus dem Userland in den Kernel sinnvoll sein. Die Maßnahme spart Kontextwechsel-Zeiten ebenso wie aufwändige Kopieraktionen zwischen User- und Kernelspace. Allerdings hebt sie die sinnvolle Trennung von Applikation und Kernel auf. Die Applikation im Kernel hebelt die sonst vorhandenen Schutzmechanismen aus, öffnet im Fall von Programmierfehlern Crackern ein direktes Tor zum Kernel und gerät daher stets zur Speziallösung.
Dennoch lässt sich die Leistungssteigerung für eigene Netzwerkanwendungen im Kernel nutzen. Kleiner Nebeneffekt der Technik: Sämtliche Komponenten einer solchen Anwendung lassen sich in einem Stück Software, nämlich einem Kernelmodul, verpacken. Wo früher also ein oder mehrere Applikationsteile zu installieren waren, lädt der Anwender nur noch ein distributionsunabhängiges Kernelmodul nach.
Doch ist eine Applikation im Kern nicht so einfach zu programmieren wie eine Userspace-Anwendung, weil Linux von dort keinen Zugriff auf normale Bibliotheksfunktionen ermöglicht [1]. Spannenderweise haben die Kernelentwickler gerade für den Netzwerkzugriff aber einen ganzen Reigen von Funktionen geschrieben. Den dürfen sogar Programmierer nutzen, die ihre Software nicht unter die GPL stellen.
Die Funktionen kreieren und löschen Sockets, binden sie an Ports, warten auf eingehende Verbindungen oder bauen solche auf (siehe Tabelle 1). Aus der Bandbreite der Userspace-Funktionen steht also auch im Kernel das Wichtigste zur Verfügung (siehe Abbildung 1).
Ähnlich, aber nicht gleich
Entwicklern fallen vier Umstände auf: Erstens haben die Kernel-Pendants andere Namen im Vergleich zu ihren Userspace-Geschwistern, zweitens sind die Aufrufparameter unterschiedlich. Der dritte Unterschied betrifft die Funktionen »read()« und »write()« zum Datenaustausch: Diese Funktionen sucht der Entwickler im Kern vergeblich und muss sie selbst implementieren. Dazu bieten viertens die Funktionen »socket_sendmsg()« und »kernel_sendmsg()« ihre Dienste an – mit auf den ersten Blick ähnlicher Funktionalität.
Einfach anwenden lassen sich die Funktionen »kernel_sendmsg()« und »kernel_recvmsg()«. Beiden übergibt der Kernel-Netzwerker neben dem Socket noch eine Datenstruktur vom Typ »struct msghdr«, die bei verbindungslosen Sockets unter anderem auch die Zieladresse für das Paket enthält (siehe Abbildung 2). Die Daten müssen nicht zwangsläufig an einem zusammenhängenden Speicherort liegen. Vielmehr übergibt der Entwickler ein Feld vom Typ »struct kvec«, das Adressen und Längen der zugehörigen Speicherorte enthält. »kernel_sendmsg()« und »kernel_recvmsg()« kümmern sich darum, dieses Feld in die »struct msghdr« einzuhängen.

Abbildung 2: Die Datenstrukturen für den Datenaustausch erfordern genaues Hinsehen beim Programmieren.
Variantenreich
Bei den Funktionen »sock_sendmsg()« und »sock_recvmsg()« hängt der Programmierer die Speicherort-Adressen – jetzt vom Typ »struct iovec« – vor dem Aufruf in den Messageheader ein. Das allein ist aber nicht der entscheidende Unterschied. Systemcalls wie die zwei »sock_«-Funktionen sind qua Implementierung darauf geeicht, Daten zwischen Kernel- und Userspace auszutauschen.
Der Transfer von Kernel zu Kernel ist hingegen nur in seltenen Fällen notwendig. Daher muss der Entwickler vor dem Aufruf der zum Systemcall gehörenden Kernelfunktion die Speicherverwaltung mit einem Trick überlisten. Es gilt, sie zu überreden auch aus dem Kernel selbst einen Zugriff zu gestatten.
Das zugrunde liegende Problem und die Lösung stellt der Kasten “Datentransfer innerhalb des Kernels” vor. Falls sich die in »iov« spezifizierten Speicherorte im Kernelspace befinden und der Entwickler den Systemaufruf »sock_sendmsg()« verwendet, bettet er ihn in die in Listing 1 dargestellte Codesequenz ein.
|
Datentransfer innerhalb des |
|---|
|
Systemcalls, die Dienste des Betriebssystemkerns, tauschen Daten zwischen Userspace und Kernelspace vorwiegend mit den Funktionen »copy_to_user()« und »copy_from_user()« aus [2]. Sie verarbeiten Adressen im Userspace, die von der Applikation stammen. Sie sind aus Sicht des Betriebssystemkerns alles andere als vertrauenswürdig. Daher überprüft die Speicherverwaltung vor einem Datentransfer, ob die Adressen gültig sind, ob sie nicht in der so genannten Zeropage liegen oder ob die Adressen nicht ungewollt (oder von einem Malevolenten gar manipuliert) auf eine Adresse im Kernelspace zeigen. Das wäre ein Wert oberhalb der Grenze zwischen User- und Kernelspace. Damit ein Systemcall Daten aus dem Kern akzeptiert, verschiebt der Entwickler diese Grenze kurzfristig. Linux legte sie früher im FS-Register der x86-CPU ab. Wegen dieser Geschichte heißen bis heute die Funktion, die die Grenze auslesen und einstellen, »get_fs()« und »set_fs()«. Vor dem Systemcall aus dem Kernel rettet der Programmierer also den alten Wert und erlaubt temporär mit »KERNELDS« den internen Zugriff. Er darf natürlich nach dieser Operation nicht vergessen, die ursprünglichen und sicheren Werte wieder zu reaktivieren. |
|
Listing 1: Zugriff auf |
|---|
01 msg.msg_iov = &iov; 02 msg.msg_iovlen = 1; 03 04 oldfs = get_fs(); // Grenze sichern 05 set_fs(KERNEL_DS); // Aushebeln des Schutzmechanismus 06 len = sock_sendmsg(sock, &msg, len); 07 set_fs(oldfs); // Restauration des Ursprungszustands |
Außerdem gilt für Netzwerkcode im Linux-Kern, dass viele Zugriffe auf das Netz-Interface nur im Prozesskontext, nicht aber innerhalb einer Interrup-Service-Routine (ISR), eines Tasklet oder einer Timerfunktion statthaft ist. Das Programm muss sich also entweder den Prozesskontext einer Applikation borgen (zum Beispiel den des Programms »insmod«) oder aber mit Hilfe der Funktion »kernel_thread()« eine unabhängige Task aufziehen.
Nachsager aus dem Kernel
Listing 3 implementiert einen TCP-Echo-Server innerhalb des Kernels. Der Code zieht dafür ab Zeile 109 beim Laden des Moduls (»server_init()«) einen eigenen Thread mit der dazu notwendigen Verwaltung auf. Anschließend legt er analog zu einer normalen Netzapplikation einen Socket an, bindet ihn an einen Port (hier 5555) und setzt die Länge der Warteschlange neuer Verbindungen mittels »listen()« fest. Analog lassen sich natürlich auch andere Transportprotokolle wie UDP oder ICMP nutzen. Im Code ist übrigens erkennbar, dass im Kernel auch die Hilfsfunktionen »htons()« und ihre Geschwister zur Verfügung stehen.
Entlädt ein Anwender das Modul, dann beendet der Code ab Zeile 125 den angelegten Thread mit dem notwendigen Entladeschutz über ein Completion-Objekt. Der Code des Kernelthread selbst findet sich in der Funktion »echo_server()« ab Zeile 83. Solange für den Thread kein Signal anliegt, wartet die Funktion »server_accept()« auf eine neue Verbindung. Kommt die zustande, legt der Kernel selbst den charakterisierenden Socket an. Ihn muss er später über die Funktion »sock_release()« wieder freigegeben.
Pakete schnüren
Solange eine neue Verbindung besteht, nimmt der Server ankommende Pakete mit der Funktion »server_receive()« entgegen und wirft die empfangenen Daten mittels »server_send()« als Echo wieder zurück. Die Implementation blockiert so lange, bis der Client die Verbindung beendet. Ernsthafte Serverapplikationen würden daher für jede Verbindung einen eigenen Thread erzeugen. Für den eigentlichen Datenverkehr baut der Programmierer eine »struct msghdr« zusammen. Dafür benötigt er vier Teile: Erstens einen Speicherbereich »buffer«, in dem er die zu versendenden oder zu empfangenen Daten ablegt, dann Adresse und Größe dieses Speicherbereichs in einer Datenstruktur vom Typ »struct iovec« (respektive »struct kvec«). Nutzt er UDP, spezifiziert er als vierten Parameter die Zieladresse in der Struktur »struct sockkaddr«. Der Kleber dieser Komponenten ist die Struktur »struct msghdr« selbst.
Abbildung 2 illustriert die Zusammenhänge. Wer am Ende der Funktion »server_accept()« noch die Codezeilen aus Listing 2 einfügt, erfährt aus dem Syslog, wer Verbindung mit dem Server aufgenommen hat.
|
Listing 2: |
|---|
01 int len, error;
02
03 if ((error = kernel_getpeername(clientsocket,
04 (struct sockaddr *)&address, &len)) < 0) {
05 sock_release(clientsocket);
06 return NULL;
07 }
08 printk(KERN_INFO "%u.%u.%u.%u connected (%d)n",
09 (unsigned char)address.sa_data[2],
10 (unsigned char)address.sa_data[3],
11 (unsigned char)address.sa_data[4],
12 (unsigned char)address.sa_data[5], error);
|
Ausreichend frankieren
Der Code aus Listing 3 lässt sich mit einem normalen Kernel-Makefile (siehe Abbildung 3) kompilieren. Sobald Linux das Modul geladen hat, ist der Echo-Server aktiv und lässt sich beispielsweise mit Hilfe von »telnet« testen. Netzprofis achten darauf, dass sonst keine Anwendung Port 5555 verwendet. Ist die Ressource schon belegt, meldet der Code in Zeile 23 einen Fehler. Auch eine Fehlerquelle: eine blockierende Firewall.
|
Listing 3: |
|---|
001 #include <linux/module.h>
002 #include <linux/init.h>
003 #include <linux/in.h>
004 #include <net/sock.h>
005
006 static struct socket *serversock = NULL;
007 static DECLARE_COMPLETION(threadcomplete);
008 static int srvpid;
009
010 static int
011 create_socket(void)
012 {
013 struct sockaddr_in server;
014
015 if (sock_create_kern(PF_INET, SOCK_STREAM,
016 IPPROTO_TCP, &serversock) < 0) {
017 printk(KERN_ERR "server: No socket.n");
018 return -EIO;
019 }
020 server.sin_family = AF_INET;
021 server.sin_addr.s_addr = INADDR_ANY;
022 server.sin_port = htons(5555);
023 if (kernel_bind(serversock,
024 (struct sockaddr *) &server,
025 sizeof(server)))
026 goto release;
027 if (kernel_listen(serversock, 3))
028 goto release;
029 return 0;
030
031 release:
032 sock_release(serversock);
033 printk(KERN_ERR "server: socket errorn");
034 return -EIO;
035 }
036
037 static struct socket *
038 socket_accept(struct socket *server)
039 {
040 struct sockaddr address;
041 struct socket *clientsock = NULL;
042
043 if (!server) return NULL;
044 if (kernel_accept(server,
045 &clientsock, 0) < 0) return NULL;
046 return clientsock;
047 }
048 static int
049 server_send(struct socket *sock,
050 unsigned char *buf, int len)
051 {
052 struct msghdr msg;
053 struct kvec iov;
054
055 if (!sock->sk) return 0;
056 iov.iov_base = buf;
057 iov.iov_len = len;
058 msg.msg_control = NULL;
059 msg.msg_controllen = 0;
060 msg.msg_flags = 0;
061
062 return kernel_sendmsg(sock, &msg,
063 &iov, 1, len);
064 }
065
066 static int
067 server_receive(struct socket *sptr,
068 unsigned char *buf, int len)
069 {
070 struct msghdr msg;
071 struct kvec iov;
072
073 if (!sptr->sk) return 0;
074 iov.iov_base = buf;
075 iov.iov_len = len;
076 msg.msg_control = NULL;
077 msg.msg_controllen = 0;
078
079 return kernel_recvmsg(sptr, &msg,
080 &iov, 1, len, 0);
081 }
082
083 static int
084 echo_server(void *data)
085 {
086 struct socket *clientsock;
087 unsigned char buff[1024];
088 static int len;
089
090 daemonize("kecho"); allow_signal(SIGTERM);
091 while (!signal_pending(current)) {
092 clientsock = socket_accept(serversock);
093 printk("clientsocket(%p)n",
094 clientsock);
095 while (clientsock) {
096 if ((len = server_receive(clientsock,
097 buff, sizeof(buff))) > 0) {
098 server_send(clientsock, buff, len);
099 } else {
100 sock_release(clientsock);
101 clientsock = NULL;
102 }
103 }
104 }
105 complete(&threadcomplete);
106 return 0;
107 }
108
109 static int
110 __init server_init(void)
111 {
112 if (create_socket() < 0) return -EIO;
113
114 srvpid = kernel_thread(echo_server,
115 NULL, CLONE_KERNEL);
116 printk("echo server pid: %dn", srvpid);
117 if (srvpid < 0) {
118 printk(KERN_ERR "server: no threadn");
119 sock_release(serversock);
120 return -EIO;
121 }
122 return 0;
123 }
124
125 static void
126 __exit server_exit(void)
127 {
128 printk("server_exit()n");
129
130 if (srvpid) {
131 kill_pid(find_pid_ns(srvpid,
132 &init_pid_ns), SIGTERM, 1);
133 wait_for_completion(&threadcomplete);
134 }
135 if (serversock)
136 sock_release(serversock);
137 }
138
139 module_init(server_init);
140 module_exit(server_exit);
141 MODULE_LICENSE("GPL");
|
In bewährtem Gewand
Trotz des funktionsorientierten Interface haben die Kernelentwickler den Netzwerkzugriff intern vollkommen objektorientiert implementiert. Die einzelnen Funktionen sind Wrapper-Routinen, die Methoden des jeweiligen Socket-Objekts aufrufen. Auch wenn das objektorientierte Paradigma seinen Charme hat, entspricht das mit Kernelversion 2.6.19 übernommene Interface einfach mehr dem API des Userspace, das Entwickler seit vielen Jahren kennen. Das macht das Portieren zumindest von überschaubaren Codestücken angenehm einfach. Komplexer Code hat im Kernel nach wie vor wenig zu suchen, zu weitreichend sind die Folgen möglicher Programmierfehler. Statusmeldungen eingebetteter Systeme sind hingegen einfach möglich. (mg)
|
Infos |
|---|
|
[1] Richard Stevens, “Unix Network Programming”, Volume 1: Prentice Hall India, 2003 [2] Quade, Kunst, “Linux-Treiber entwickeln”: Dpunkt-Verlag, 2. Auflage, Juni 2006 |
|
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. |









