Aus Linux-Magazin 09/2009

Kernel- und Treiberprogrammierung mit dem Kernel 2.6 - Folge 47

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.

Tabelle 1: Prototypen der Netzfunktionen im Kernel

Tabelle 1: Prototypen der Netzfunktionen im Kernel

Abbildung 1: Für bekannte Userspace-Funktionen bietet der Kernel korrespondierende Routinen an.

Abbildung 1: Für bekannte Userspace-Funktionen bietet der Kernel korrespondierende Routinen an.

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.

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
Kernels

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
Kerneldaten

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:
Verbindungslogs

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.

Abbildung 3: Nach dem Übersetzen und Laden lässt sich der Echo-Server mit »telnet« einfach testen.

Abbildung 3: Nach dem Übersetzen und Laden lässt sich der Echo-Server mit »telnet« einfach testen.

Listing 3:
Inkernel-Echo-Server

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.

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 5 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Nach oben