Kopieren, ohne die CPU damit zu belasten? Linux kann's. Fehlen nur noch Applikationen, die das neue Feature nutzen. Die Kern-Technik verschafft Anwendungsprogrammierern das Know-how.
Die Datenmengen bei Multimedia & Co. sind für jedes Betriebssystem eine mächtige Herausforderung. Oft müssen mal eben ein paar GByte von der Festplatte gelesen, in die Grafikkarte transportiert oder über das Netzwerkinterface versandt werden. Typischerweise kopiert das OS die Daten dazu gleich mehrfach hin und her (Abbildung 1), was den ganzen Vorgang selbstverständlich verlangsamt.

Abbildung 1: Beim traditionellen Kopieren wird eine Datei bis zu viermal im Speicher kopiert. Unter günstigen Umständen kommt zweimal direkter Speicherzugriff (DMA) zum Einsatz, aber mindestens zweimal durchlaufen die Daten die CPU.
Zum Beispiel bei einem Server, der eine Filmdatei an einen über TCP/IP angebundenen Client sendet: Zuerst wandern die Daten (also der Film) von der Festplatte in den Speicher des Betriebssystemkerns, den Kernelspace. Dann sind die Daten ein zweites Mal unterwegs, um in den Speicherbereich der Server-Applikation zu wandern, den Userspace.
Zum Glück erfolgen zumindest zwei der Kopieraktionen häufig per DMA, nämlich das Lesen von der Platte und die Übergabe an den Ethernet-Controller. Das lindert das Problem aber nur zum Teil. Mindestens zweimal fasst die CPU die Daten an – das können sich auch moderne (HPC, High Performance Computing) Systeme nicht leisten.
Schon seit Längerem verwenden pfiffige Programmierer daher den Systemcall »sendfile()« [1]. Dieser Systemaufruf kopiert Daten von einem Stream in den nächsten, ohne sie dabei im Userspace zwischenzuparken. Leider ist »sendfile()« nicht universell verwendbar. Es lässt nämlich – vereinfacht gesagt – nur das Kopieren von einer auf der Platte liegenden Datei auf einen Socket (Netzwerk) zu. Datei auf Datei, Socket auf Socket oder Socket auf Datei – für »sendfile()« ist das alles unmöglich.
Turbolader
Seit Kernel 2.6.17 tragen die Entwickler den steten Nachfragen nach optimierten Datenkopierfunktionen mit den neuen Systemcalls »splice()«, »tee()« und »vmsplice()« Rechnung. Sie implementieren so genanntes Zero-Copying, was bedeutet, dass die CPU beim Kopieren von Daten nicht mehr beteiligt ist, eine CPU-Last also durch das Kopieren nicht anfällt (Abbildung 2).

Abbildung 2: Disk-to-Disk-Copy: Der erste Aufruf von »splice()« füllt den Kernel-Buffer, der zweite Aufruf überführt die Daten in die neue Datei.
Der Trick besteht darin, die Daten – meist per DMA – von der Quelle (etwa der Festplatte) in einen Kernel-Buffer zu transportieren. Statt die Daten danach weiter in den Userspace und von dort zurück an eine andere Stelle im Kernel zu kopieren, entnimmt der Kernel (etwa der Netzwerktreiber) die Daten direkt dem Kernel-Buffer. Gesteuert wird dieser Vorgang, bei dem die CPU kein einziges Datenbyte anfasst, durch die User-Applikation (siehe Abbildung 2).
Alles, was »splice()« & Co. also brauchen, ist – neben der Eingangsdatenquelle und der Ausgangsdatensenke – ein Kernel-Buffer. Den stellt eine klassische Unix-Pipe zur Verfügung, weshalb sie auch der Dreh- und Angelpunkt der Kernel-Implementierung ist. Der Systemcall »splice()« verbindet einen Filedeskriptor mit dem Anfang (oder Ende) einer solchen Pipe und füllt respektive entleert den zugehörigen Kernel-Buffer. Insgesamt sind bei einer derartigen Kopieraktion also vier Deskriptoren beteiligt (Abbildung 3).
Pipes und Puffer
Wer beispielsweise mit Hilfe von Zero-Copying eine Datei in eine andere kopieren möchte, verbindet mit »splice()« den File-Deskriptor der Quelldatei mit dem einen Ende der Pipe und füllt den Pipe-Buffer. Ein zweiter Aufruf von »splice()« verbindet das andere Ende der Pipe mit der Ausgabedatei und transferiert dadurch den Inhalt des Pipe-Buffers zurück auf die Festplatte. Diese Sequenz wiederholt sich so lange, bis alle Daten kopiert sind. Abbildung 3 verdeutlicht den Unterschied zur traditionellen Kopierfunktion: An die Stelle des Puffers im Userspace tritt der per Aufruf »pipe()« reservierte Kernel-Buffer. Und statt »read()« und »write()« transferiert der neue Systemcall »splice()« die Daten.

Abbildung 3: Statt des Puffers im Userspace legt der Kernel beim Zero-Copying per »pipe()«-Aufruf einen eigenen Puffer an. Der Syscall »splice()« stößt aus dem Userspace das Lesen von Platte in den Kernel-Buffer und das Schreiben aus dem Kernel-Buffer auf die Platte an.
Um mehreren Programmen parallel Zugriff auf die im Kernel-Buffer befindlichen Daten zu ermöglichen, bietet Linux den Systemcall »tee()« an. In seiner Funktion ähnelt er dem von der Shell bekannten Tee-Kommando. Er erlaubt es, Daten aus dem Pipe-Buffer auszulesen und an eine (andere) Pipe weiterzureichen (siehe Abbildung 4). Damit lassen sich beispielsweise die von einem DVB-T-Gerät empfangenen Daten per »tee« gleichzeitig mit einen Videoplayer anschauen und auf Festplatte speichern. »tee()« kopiert die Daten dabei nicht, sondern arbeitet allein mit Referenzen. Häufig ist die Pipe, an die »tee()« die Daten weiterreicht, die Standardausgabe Stdout (»STDOUT_FILENO«).
Auf der Kommandozeile sieht eine solche Befehls-Pipeline so aus:
splice-in /dev/dvbt | splice-tee film.mpeg | splice-out /dev/tvout
Doch sollten Programmierer aufpassen: Wer von einer Quelle zwei Kopien ziehen möchte – wobei keine der Kopien nach Stdout geht -, braucht auch zwei Pipes! Der Pseudo-Code in Abbildung 5 gibt die Struktur einer solchen Kopierapplikation wieder.
Die Applikation schließlich übergibt die Daten wieder dem Kernel, damit er sie über das Netzwerkinterface an den Client-Rechner schickt. Dazu kopiert der Netzwerktreiber die im Kernel deponierten Daten in den Speicher des Ethernet-Controllers.

Abbildung 4: Der Systemcall »tee()« bindet noch einen zweiten Kernel-Buffer zwischen die Eingabequelle und den duplizierten Ausgabekanal. Doch keine Angst vor unnötigen Datenduplikaten: Der Kernel arbeitet intern nicht mit einer Kopie, sondern nur mit einer Referenz.
Der dritte hier vorgestellte Systemcall »vmsplice()« macht die im Userspace bereits vorhandenen Speicherbereiche dem Kernel zugänglich. Ohne Kopieraktion der Daten vom User- in den Kernelspace überträgt der Kernel die ihm anvertrauten Daten zur Peripherie per DMA und umgekehrt. Es handelt sich damit – abgesehen von der mit dem Systemcall verbundenen Kopieraktion – gewissermaßen um ein Gegenstück zu »mmap()« [3], das Speicherbereiche im Kernelspace einer Applikation zugänglich macht. Der zugehörige Filedeskriptor ist wieder einmal eine Pipe. Die genaue Aufrufsyntax der Systemcalls und der zugehörigen Parameter beschreibt der Kasten “Neue Systemcalls im Überblick”.
|
Neue Systemcalls im |
|---|
|
Drei Systemaufrufe geben dem Anwendungsprogrammierer eine Eintrittskarte zu den neuen Kernelfunktionen. Der erste Systemcall verbindet den über die Pipe repräsentierten Kernel-Buffer mit einem File-, Pipe- oder Socket-Deskriptor: int splice(int fdin, loff_t *off_in, int fdout, loff_t *off_out, size_t len,unsigned int flags) Er überträgt von »fdin« ab dem »off_in«-Byte maximal »len« Bytes Daten nach »fdout«. Über das Bitfeld »flags« lässt sich der Transfer parametrisieren. Die Bedeutung der Flags sieht folgendermaßen aus: »SPLICE_F_MOVE«: Ist dieses Flag gesetzt, versucht der Kernel anstelle einer Kopie den Pipe-Buffer in den Page-Cache zu verschieben, falls er die Daten auf die Festplatte transferieren soll. »SPLICE_F_MORE«: Ist dieses Flag gesetzt, weiß den Kernel, dass nach dem Aufruf von »splice()« weitere Daten folgen. »SPLICE_F_NONBLOCK«: Dieses Flag verhindert, dass die Applikation am »splice()«-Aufruf schlafen gelegt wird. Achtung: Ein (interner) Zugriff auf die File-Deskriptoren kann dennoch blockieren. »SPLICE_F_GIFT«: Das Flag ist nur im Kontext von »vmsplice« interessant. Damit überlässt eine Applikation dem Kernel die Kontrolle über die übergebenen Speicherbereiche, die Applikation darf den Speicher nicht weiter verwenden. Der übergebene Speicher muss einer Seitenlänge (typisch 4 KByte) entsprechen und auf eine Seitenadresse ausgerichtet sein. Der zweite Systemcall dupliziert mindestens »len« Bytes von »pipe_in« nach »pipe_out«: int tee(int pipe_in, int pipe_out, size_t len, unsigned int flags) Der dritte Systemcall schließlich blendet »nr_segs« Speicherbereiche (referenziert über »iov«) in den durch »fd« repräsentierten Pipe-Buffer ein: int vmsplice(int fd, const struct iovec *iov, unsigned long nr_segs, unsigned int flags) Die Struktur »struct iovec *« ist in der Headerdatei »sys/uio.h« deklariert. |
Implementationsprobleme
Nicht alles ist Gold, was glänzt. Leider weist die Implementierung von Splice sowohl im Kernel als auch im Userland noch Schwächen auf. Im Userland ist es die Glibc, die auf verschiedenen Plattformen (zum Beispiel auf Ubuntu und Suse) die Systemcalls »splice()«, »tee()« und »vmsplice()« nicht richtig implementiert. Hier bleibt nur, die Systemcalls – wie in [4] beschrieben oder in Listing 1 (Zeile 18) gezeigt – direkt aufzurufen.
|
Listing 1: |
|---|
01 #include <stdio.h>
02 #include <unistd.h>
03 #include <fcntl.h>
04 #include <sys/stat.h>
05 #include <asm/page.h>
06 #include <asm/unistd.h>
07
08 // defines from linux/pipe_fs_i.h
09 #define SPLICE_F_MOVE (0x01) /* move pages instead of copying */
10 #define SPLICE_F_MORE (0x04) /* expect more data */
11 #define PIPE_BUFFERS (16)
12
13 #define SPLICE_BUF_SIZE (PIPE_BUFFERS*PAGE_SIZE)
14
15 static inline int sys_splice(int fdin, loff_t *off_in,
16 int fdout, loff_t *off_out, size_t len, unsigned int flags)
17 {
18 return syscall( __NR_splice,fdin,off_in, fdout,off_out,len,flags );
19 }
20
21 int main( int argc, char **argv )
22 {
23 int fd_in, fd_out, ret;
24 int fd_pipe[2];
25 int bytes_to_copy, bytes_copied;
26 unsigned long bytes_left;
27 struct stat stat_buf;
28
29 if( argc != 3 ) {
30 fprintf(stderr,"usage: %s from ton", argv[0] );
31 return -1;
32 }
33 fd_in = open( argv[1], O_RDONLY );
34 if( fd_in < 0 ) {
35 perror( argv[1] );
36 return -1;
37 }
38 fd_out = open( argv[2], O_WRONLY|O_CREAT, 0644 );
39 if( fd_out < 0 ) {
40 perror( argv[2] );
41 return -1;
42 }
43 if( pipe(fd_pipe) < 0 ) {
44 close( fd_out );
45 close( fd_in );
46 return -1;
47 }
48 fstat( fd_in, &stat_buf );
49 bytes_left = stat_buf.st_size;
50
51 while( bytes_left ) {
52 bytes_to_copy = bytes_left <SPLICE_BUF_SIZE ?
53 bytes_left:SPLICE_BUF_SIZE;
54 bytes_copied=sys_splice( fd_in, NULL, fd_pipe[1],NULL,
55 bytes_to_copy, SPLICE_F_MOVE|SPLICE_F_MORE );
56 if( bytes_copied==-1 ) {
57 perror( "splice read" );
58 return -1;
59 }
60 bytes_left -= bytes_copied;
61 while( bytes_copied ) {
62 ret=sys_splice( fd_pipe[0], NULL, fd_out, NULL,
63 bytes_copied, SPLICE_F_MOVE|SPLICE_F_MORE );
64 if( ret==-1 ) {
65 perror( "splice write" );
66 return -1;
67 }
68 bytes_copied -= ret;
69 }
70 }
71 close( fd_out );
72 close( fd_in );
73 close( fd_pipe[0] );
74 close( fd_pipe[1] );
75 return 0;
76 }
|
Warten und hoffen
Im Kernel sieht es leider noch trauriger aus, hier müssen Performance-hungrige Programmierer auf Linux 2.6.23 warten. Erst dann ist der Code für »SPLICE_F_MOVE« (siehe Kasten “Neue Systemcalls im Überblick”) implementiert.
In der aktuellen Version 2.6.21 kopiert der Kernel bei Splice-Zugriffen auf Dateien nämlich die Daten der Pipe-Buffer in den Page-Cache, bis sie von dort den Weg auf die Festplatte finden. In Version 2.6.23 – so die Planung – wird der fragliche Pipe-Buffer, wenn »SPLICE_F_MOVE« gesetzt ist, zum Page-Cache hinzugefügt und dann nur noch mit Referenzen gearbeitet.
Ins Netz kopieren
Aber immerhin: Die Splice-Kopplung zum Netzwerk-Stack ist auf einem guten Weg. Vom eingangs erwähnten Veteranen »sendfile()« ist nur noch die Schnittstelle übrig geblieben. Hinter den Kulissen ist »sendfile()« auch längst auf der Basis von »splice()« realisiert. Denn »splice()« bringt nicht nur wegen der Universalität Vorteile, auch das Programmierinterface ist klarer.
Will ein Programmierer beispielsweise den Daten auf der Platte vor dem Versenden per TCP/IP einen Header voranstellen, schreibt er ihn mit »write()« als Erstes in den Pipe-Buffer. Danach “splict” er die Datei an den Kernelbuffer und initiiert so den Transfer. Genauso fügt er Daten in den Strom ein oder hängt welche an. Das Flag »TCP_CORK«, das bei »sendfile()« dazu dient, Daten in den Strom einzufügen, hat damit ausgedient.
Einstiegshilfe
Ein erstes Codebeispiel in Listing 1 demonstriert, wie Linux-Anwendungen »splice()« verwenden. Es veranschaulicht die Abläufe und die Mechanismen beim Kopieren der Daten von Platte auf Platte. Aber solange die Splice-Implementierung der Glibc noch fehlerhaft und die notwendigen Headerdateien nicht in dem Standardpfad »/usr/include/« verfügbar sind, muss man die notwendigen Definitionen selbst vornehmen und den Systemcall direkt aufrufen (Listing 1, Zeilen 9 bis 19).
Die Größe des Pipe-Buffers und damit die Anzahl der in einem Rutsch mit »splice()« maximal zu kopierenden Bytes ist mit 16 Pages festgelegt, auf einer x86-Plattform also 64 KByte. Sollte der Compiler die Headerdatei »page.h« nicht finden, ist der Pfad komplett anzugeben:
#include </usr/src/linux-2.6.21-rt1/include/asm/page.h> // Zeile 5
Weitere Programmierbeispiele, insbesondere auch in Kombination mit Netzwerkzugriffen und den neuen Systemcalls »tee()« und »vmsplice()«, finden sich im Git-Archiv des Splice-Implementierers Jens Axboe unter der URL [git://git.kernel.dk/data/git/splice.git]. Dort – ebenso wie zum Beispiel unter der Adresse [2] – liegen auch die Manpages zu den neuen Systemcalls. Wie Interessenten mit Hilfe von Git an den Quellcode kommen, verrät der Kasten “Beispielcode im Git-Archiv”.
|
Beispielcode im |
|---|
|
Jens Axboe, einer der Väter von Splice, verwaltet seine Dateien mit dem Sourcecode-Konstrollsystem Git [7]. Wer seine Beispielprogramme selber ausprobieren will, muss also Git installiert haben. Auf einem Debian- beziehungsweise einem (K)Ubuntu-System sind dazu die Pakete »cogito« oder respektiveund »git-core« erforderlich. Axboes Quellcode lässt sich nach dem Auschecken einfach per »make« übersetzen: quade@ezs-mobil:/tmp/git$ sudo apt-get install git-core Password: <Geheimes Passwort> ... quade@ezs-mobil:/tmp/git$ git clone git://git.kernel.dk/data/git/splice.git Generating pack... Done counting 268 objects. Deltifying 268 objects. 100% (268/268) done Total 268 (delta 162), reused 0 (delta 0) quade@ezs-mobil:/tmp/git$ ls splice quade@ezs-mobil:/tmp/git$ cd splice/ quade@ezs-mobil:/tmp/git/splice$ ls ktee.c splice.2 splice.h splice-test4s.c vmsplice2.c ktee-net.c splice-bench.c splice-in.c splice-tonet.c vmsplice.c Makefile splice-cp.c splice-out.c tee.2 README splice-fromnet.c splice-test4c.c vmsplice.2 quade@ezs-mobil:/tmp/git/splice$ make ... Nach wenigen Augenblicken liegen die fertig übersetzten Beispielprogramme auf der Platte und warten auf ihren Einsatz. |
Dem Chef gefällt’s
Weitere Infos zu »splice()« – auch vom Linux-Chefentwickler Linus Torvalds persönlich – sind auf der Website [5] zu finden. Der zeigt sich übrigens ganz angetan von den neuen Systemcalls, wie er in einer E-Mail an die Linux-Kernel-Mailingliste mitteilt [6]: “Ganz offensichtlich bin ich von diesem Ansatz begeistert. Ich denke, es ist eine dieser ‚Richtigen Sachen(tm)‘, die einem nicht allzu häufig über den Weg laufen. Ich kenne sonst niemanden, der das so macht, aber es ist sowohl nützlich als auch clever. Wenn du [Oleg Nesterov] mich jetzt vom Gegenteil überzeugst, werde ich dich für immer hassen ;).” (ofr)
|
Infos |
|---|
|
[1] Jeff Tranter, “Exploring The Sendfile System Call”: Linux Gazette, Ausgabe 91/2003; [http://linuxgazette.net/issue91/tranter.html] [2] Manpage zu »splice()«: [http://www.die.net/doc/linux/man/man2/splice.2.html] [3] Eva-Katharina Kunst, Jürgen Quade, “Kern-Technik”, Folge 32: Linux-Magazin 3/07 [4] Eva-Katharina Kunst, Jürgen Quade, “Kern-Technik”, Folge 13: Linux-Magazin 8/04 [5] Linus Torvalds, Splice: Diskussion auf der LKML; [http://kerneltrap.org/node/6505] [6] Linus Torvalds, Kommentar zur Splice-Implementierung: [http://lwn.net/Articles/118760/] [7] Alberto Planas, “Git, das Versionskontrollsystem des Kernels”: Linux-Magazin 3/06, S. 98 |
|
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. |







