Wer im Kernel eigene Netzwerkdienste programmiert, muss einige Eigenheiten des dort implentierten TCP/IP-Codes beachten, ähnlich wie beim Zugriff auf Dateien[1]. Glücklicherweise erinnert das Kernel-Interface an das der bekannten Userspace-Funktionen, siehe Kasten "TCP/IP im Userspace". Die meisten Netzwerkzugriffe sind allerdings nur in einem Prozesskontext möglich. Deshalb muss sich eine TCP/IP-Funktionen im Kernel entweder den Prozesskontext einer Userspace-Anwendung borgen oder braucht einen eigenen Kernel-Thread, wie[2] beschreibt.
TCP/IP im Userspace
|
|
Die Kommunikation mit der Internet-Standardprotokollfamilie TCP/IP funktioniert nach dem Client-Server-Prinzip. Der Client addressiert einen Server über die Kombination von IP-Adresse und Port, der Server wartet auf die Anfrage eines Clients. Ist es so weit, erzeugt er einen neuen Socket, über den der Datentransfer abläuft. Parallel dazu nimmt der Server bereits die nächste Anfrage entgegen (siehe Abbildung 1).
Um einen Userspace-Server zu programmieren, kommen überlicherweise die Funktionen »socket()«, »bind()«, »listen()« und »accept()« zum Einsatz. Der Client baut eine Verbindung über »socket()« und »connect()« auf (Abbildung 2).
Der Aufruf von »socket()« richtet einen Kommunikationsendpunkt ein, der als Socket bezeichnet wird. Die Funktion »bind()« weist dem Socket einen Port zu, der dem Client bekannt sein muss. Für einige Applikationen sind die Ports festgelegt (siehe die Datei »/etc/services«). So steht die Portnummer 80 für den HTTP-Service, also das übliche WWW-Protokoll. Der Aufruf von »listen()« legt die maximale Anzahl gleichzeitiger Verbindungen fest. Damit lässt sich die Gefahr einer Überlastung des Servers von vornherein minimieren.
»accept()« ist die Funktion, mit der der Server auf eine neue Verbindung wartet. Sobald ein Client per »connect()« eine Verbindung aufbaut, erzeugt diese Funktion einen neuen Socket, über den Client und Server Daten austauschen, meist über »read()« und »write()«. Mit dem ursprünglichen Socket wartet der Server auf die nächste Verbindung. Wird ein Socket nicht mehr benötigt, gibt die Funktion »close()« ihn wieder frei.
|
Abbildung 1: Beim Verbindungsaufbau erzeugt der Server einen neuen Port, der für den eigentlichen Datenaustausch verwendet wird.
Listing 1 zeigt Ausschnitte eines Kernel-internen Echoservers, der die vorgestellten Funktionen benutzt - der komplette Code und das Makefile liegen auf[3]. Alle Daten, die der Server empfängt, schickt er an den Absender zurück. Das lässt sich leicht prüfen, siehe Kasten "Test mit Telnet".
Listing 1: »echo.c«
|
39 ...
40 static struct socket *socket_accept( struct socket *server )
41 {
42 struct socket *clientsocket=NULL;
43 struct sockaddr address;
44 int error, len;
45
46 if( server==NULL ) return NULL;
47 clientsocket = sock_alloc();
48 if( clientsocket==NULL ) return NULL;
49
50 clientsocket->type = server->type;
51 clientsocket->ops = server->ops;
52 error=server->ops->accept(server,
clientsocket,0);
53 if( error<0 ) {
54 sock_release(clientsocket);
55 return NULL;
56 }
57 error=server->ops->getname(clientsocket,
58 (struct sockaddr *)
&address, &len,2);
59 if( error<0 ) {
60 sock_release(clientsocket);
61 return NULL;
62 }
63 printk(KERN_INFO "new connection (%d) from
%u.%u.%u.%un", error,
64 (unsigned char)address.sa_data[2],
65 (unsigned char)address.sa_data[3],
66 (unsigned char)address.sa_data[4],
67 (unsigned char)address.sa_data[5] );
68 return clientsocket;
69 }
70
71 static int server_send( struct socket *sock, unsigned char *buf, int len )
72 {
73 struct msghdr msg;
74 struct iovec iov;
75 mm_segment_t oldfs;
76
77 if( sock->sk==NULL )
78 return 0;
79 iov.iov_base = buf;
80 iov.iov_len = len;
81 msg.msg_control = NULL;
82 msg.msg_controllen = 0;
83 msg.msg_iov = &iov;
84 msg.msg_iovlen = 1;
85 msg.msg_flags = 0;
86
87 oldfs = get_fs();
88 set_fs( KERNEL_DS );
89 len = sock_sendmsg( sock, &msg, len );
90 set_fs( oldfs );
91
92 return len;
93 }
94
95 static int server_receive( struct socket *sptr, unsigned char *buf, int len )
96 {
97 struct msghdr msg;
98 struct iovec iov;
99 mm_segment_t oldfs;
100
101 if( sptr->sk==NULL )
102 return 0;
103 iov.iov_base = buf;
104 iov.iov_len = len;
105 msg.msg_control = NULL;
106 msg.msg_controllen = 0;
107 msg.msg_iov = &iov;
108 msg.msg_iovlen = 1;
109
110 oldfs = get_fs();
111 set_fs( KERNEL_DS );
112 len = sock_recvmsg( sptr, &msg, len, 0 );
113 set_fs( oldfs );
114
115 return len;
116 }
117 ...
|
Test mit Telnet
|
|
Haben Sie das Server-Modul »echo.c« übersetzt und mit »insmod echo.ko« geladen, sollten Sie sicherstellen, dass Sie den hier verwendeten Port 5555 nicht anderweitig verwenden oder durch eine Firewall geblockt haben. Dann geben Sie »telnet localhost 5555« ein. Der Aufruf kann natürlich auch von einem anderen Rechner aus erfolgen. In diesem Fall ist »localhost« durch den Rechnernamen des neuen Echoservers zu ersetzen.
Sie können jetzt Nachrichten eintippen, die nach jedem Return zum Server gesendet und von diesem zurückgeschickt werden. Telnet gibt die Antworten des Echoservers dann aus. Um die Verbindung zu beenden, verlassen Sie einfach das Telnet-Programm. Dazu geben Sie [Strg]+[ ]] gefolgt von »quit« ein. Die Verbindung wird ebenfalls abgebrochen, wenn Sie den Echoserver per »rmmod echo« aus dem Kernel entladen.
Um den Client zu testen, laden sie entweder das Servermodul oder verwenden den Standard-Echoserver im Userspace. Läuft der Internet-Superdaemon »xinetd«, setzen Sie in »/etc/xinet.d/echo« die Variable »disabled« auf »No« und schicken dem Server ein HUP-Signal. Dann müssen Sie allerdings auch im Sourcecode des Clients den Port auf »7« ändern.
|
Im Kernel wird ein Socket durch die Funktion »sock_create()« erzeugt, siehe Kasten "TCP/IP-Schnittstellenfunktionen im Kernel" und Abbildung 2. Das auf diese Weise erzeugte Socket-Objekt stellt in der Struktur »ops« weitere Methoden über Funktionspointer zur Verfügung, siehe den Typ »proto_ops« im Header »linux/net.h«.
Abbildung 2: Die meisten Interfaces zu TCP/IP auf der Applikationsseite haben direkte Entsprechungen im Kernel.
Die Methode »bind()« weist dem Socket eine Adresse zu, die sie als erstes Argument erwartet. Die beiden übrigen Parameter entsprechen denen ihres Gegenstücks im Userspace: Das Element vom Typ »struct sockaddr_in« spezifiziert unter anderem den Port, auf den der Server hört, »addrlen« gibt die Länge dieser Datenstruktur an.
Der Port muss, wie bei Netzanwendungen üblich, mit Hilfe von »htons()« in Network Byte Order konvertiert werden. Die Funktion »listen()« lässt den vorbereiteten Socket schließlich auf Verbinungen warten. Neben der Adresse des Sockets steht hier als zweites Argument die Anzahl der Clients, die gleichzeitig zugreifen dürfen.
Sockets klonen
Nimmt ein Client Kontakt zum Server auf, brauchen er zur Kommunikation den existierenden Socket. Also muss der Server für jede neue Verbindung einen neuen Socket anlegen, was er im Kernel mit »socket_alloc()« erledigt, Listing 1 Zeile 47. Der neue Socket und der auf Verbindungen wartende sind Argumente der Funktion »ops->accept()«. Zusätzlich erwartet sie den Zugriffsmodus, wie er von der »open()« bekannt ist. Ohne besonderen Zugriffsmodus (»flags=0«), blockiert der Kernel-Thread so lange, bis ein Client eine Verbindung aufbaut. Wird »flags« mit »O_NONBLOCK« initialisiert, kehrt »ops->accept()« sofort zurück. Die Funktion »socket_accept()« (Zeilen 40 bis 69) implementiert ungefähr dieselbe Funktionalität wie das im Userspace bestehende »accept()«.
Ein positiver Return-Wert von »ops-> accept()« zeigt an, dass der Verbindungsaufbau erfolgreich war. Wer mehr über den Client erfahren möchte, ruft die Methode »getname()« auf, die dessen IP-Adresse und Port zurückgibt, siehe Listing 1, Zeilen 57 bis 67.
Zugriffsmodus festlegen
Der Datenaustausch läuft über die Funktionen »sock_recvmsg()« und »sock_sendmsg()«, Zeilen 89 und 112. Sie besitzen drei gleiche Parameter: den Socket, ein Objekt vom Typ »msghdr« und die Länge des Speicherbereichs. Bei »sock _recvmsg()« kommen noch die Flags für die Zugriffsart dazu, die »linux/socket .h« festlegt. Für einen nicht blockierenden Zugriff steht beispielsweise »MSG _DONTWAIT«.
Der wichtigste Parameter dieser Funktionen ist die Datenstruktur vom Typ »msghdr«. Sie übernimmt nämlich in »msg_iov« die Adresse des Speicherbereichs »iov«, in dem die Daten bei Lese- und Schreibzugriffen abgelegt werden, siehe Listing 1, Zeilen 79 bis 84. In »msg_iovlen« steht die Anzahl der verwendeten Speicherblocks, in diesem Fall nur einer. Die Strukturelemente »msg_control« und »msg_controllen« spielen im Beispiel keine Rolle und erhalten dementsprechend nur Standardwerte. Sie dienen sonst unter anderem der Übermittlung von IP-Optionen über Unix-Sockets. Näher sind die einzelnen Elemente von »msghdr« in der Manualseite zu der Funktion »recvmsg()« beschrieben, die so funktioniert wie die hier verwendete Kernelfunktion »sock_recvmsg()«.