Systemprogrammierung - Teil 1

Input/Output

von Wolfgang Hetzler 


Systemprogrammierung gilt als schwierig, da ihre Inhalte dem Beutzer sehr oft abstrakt gegenüber treten im Gegensatz zu praktischen Aufgabenstellungen, deren Sinn plastisch vor Augen steht. Das diese Abstraktheit nicht sein muß und die Systemcalls eine wohldefinierte Schnittstelle zum Kernel darstellen, will diese Serie zeigen.


Dieser Artikel leitet eine Serie zum Thema Systemprogrammierung mit Linux ein. Ich werde versuchen, diese Thema in einer Weise zu handeln, die es ermöglicht, aufgrund der gegebenen Anstöße sich selbständig vertiefend in diese Thema einzuarbeiten - Systemprogrammierung ist wie jegliche Form von Programmierung Erziehung zur Mündigkeit oder Learning by Doing It. Ich hoffe, es wird kein lustloser Prozeß. 

Als Programmiersprache habe ich C++ gewählt, wobei ich überwiegend den prozeduralen Teil dieser Sprache für mein Thema bevorzuge. Alle Beispiele sollten bei Interesse nachvollziehbar sein, auf umfangreichere Behandlung weise ich jeweils in der Literaturangabe hin, ansonsten empfehle ich das Studium der mannigfaltigen Source-Dateien, die Kernelquellen selber sind absolut ungeeignet, denn die Systemcalls, die wir behandeln, sind Schnittstelle zum Kernel, letzterer realisiert erstere. 

Schwerpunkte der Serie

Linux verfügt über eine Vielzahl von Systemcalls, die sich alle thematisch gruppieren lassen, einige sind Exoten, die sich einer Kategorie entziehen. Viele Systemcalls haben ihre Tradition in System V, andere kommen von BSD, Posix definiert zusätzlich einige, Linux hat aber auch solche, die keine Entsprechung in anderen Systemen haben. Ich behandele die Systemcalls themenspezifisch, die Herkunft ist eher uninteressant. Daraus ergeben sich für mich folgende Schwerpunkte: 

  • I/O Mechanismen
  • Prozeßsteuerung
  • Prozeßkommunikation
  • Threads
  • Sockets
  • RPC

Es wird sich zeigen lassen, daß diese Themen die aktuelle Szene beherrschen, sie sind "State of the Art" und natürlich im Umfeld von Linux zu finden, was einen Linuxer nicht weiter erstaunen wird. Zum besseren Verständnis dieser Themen ziehe ich Beispielprogramme und/oder beispielhafte Implementierungen von vorhandenen Kommandos heran. Eins möge mir der gestandene Systemprogrammierer und die kritische Leserin dabei verzeihen: Ich verzichte in den Beispielprogrammen weitestgehend auf Fehlerabfragen, nicht, weil mir keine Fehler passieren, sondern weil diese Abfragen die Lesbarkeit eines Programmes drastisch reduzieren - man/frau sieht den Wald vor lauter Bäumen nicht. 

Nun und für alle weiteren Folgen zur Sache. 

Die Behandlung der Systemcalls im Kontext der Programmiersprache

Alle Systemcalls bewirken einen Kontextswitch, d.h. der aufrufende Prozeß wechselt vom Userlevel in den Kernellevel, dies geschieht bei allen Kernelvarianten für PCs durch einen Interrupt 0x80, die Parameter des Interrupts bestimmen den konkreten Systemcall. Die Aufrufe selber sind als Interface in Form von C-Funktionen in der Standardbibliothek angelegt, somit unterscheidet sich ein Systemcall in der Oberfläche nicht von einer normalen C-Funktion und gleichzeitig können alle Aspekte der Systemprogrammierung auf der Ebene von C bzw. C++ vorgenommen werden. 

Übersicht über die aktuellen Systemcalls dieser Folge

Die Input/Output - Mechanismen reduzieren sich auf fünf elementare Systemcalls, die öfters auch als low-level Funktionen bezeichnet werden: 

  • open
  • close
  • read
  • write
  • lseek

mit ihren Prototypen 

int open(char *, int, int);
int close(int);
int read(int,char *,int);
int write(int,char *,int);
int lseek(int,int,int);

Beschreibung der Systemcalls mit Beispielen

Open und Close

Der Systemcall open öffnet den Zugriff auf eine Datei, übergeben wird der Name und der gewünschte Zugriff, der dritte Parameter erlaubt das Setzen von Zugriffsrechten, open liefert einen Dateideskriptor zurück, der in den weiteren Systemcalls als Paramter die Datei identifiziert. Der Deskriptor selber ist ein Index in einer internen Tabelle des Systems, die der Kernel für alle Zugriffe und Manipulationen benutzt. Wir schreiben also (siehe Listing 1, wo wir, wie auch im folgenden keine Rückgabewerte liefern.): 

Listing 1: check.C
#include <unistd.h>    // liefert Prototypen für Systemcalls 
#include <fcntl.h>

main(int argc,char **argv) 
{ 
   
     fd = open(*++argv,O_RDONLY);  // Datei muß existieren

     if(fd == -1) 
         perror(*argv);  // Datei existiert nicht 
     else 
         close(fd); 
}

Das kleine Programm aus Listing 1 versucht die Datei zu öffnen, deren Name beim Aufruf des Programms übergeben wird, gelingt das nicht, liefert open wie alle Systemcalls -1 als allgemeinen Fehlerindikator zurück, im korrekten Fall wird die Datei über den Systemcall close wieder geschlossen, close erwartet lediglich den Dateideskriptor. Das Makro O_RDONLY gibt den Zugriffsmodus an - ausschließlich Lesen (siehe dazu die Makros aus der Datei fcntl.h, die ich im Zusammenhang mit dem Aufruf fcntl behandle). 

Der Systemcall open kreiert nicht die Datei, falls sie nicht existiert: der Zugriffsparameter muß das regeln . Wir schreiben 

Listing 2: open.C
#include <unistd.h>
#include <fcntl.h>

main(int argc, char **argv)
{
   int fd;

   fd = open(*++argv,O_RDWR|O_CREAT,0644);

   close(fd);
}

Es wird versucht, die Datei zum Lesen/Schreiben zu öffnen, gelingt das nicht, wird sie mit den Permissions 0644 angelegt, die Zugriffsmodi werden dabei mit einem logischen oder verknüpft: Das Ganze ist also ein kleines Programm zum Anlegen von Dateien (analog zum touch-Befehl). 

Read und Write

Lesen und Schreiben sind im Handling unproblematisch, sie gehen sowohl auf geblockte als auch ungeblockte Dateien, Transfermenge sind Bytes ohne Ansehen und Würde, dies verführt auch zu der fehlerhaften Behauptung, es handelt sich um low - level I/0, es sind aber Systemcalls. Wir schreiben ein kleines Beispielprogramm, das eine Datei kopiert: 

Listing 3: write.C
#include <unistd.h>
#include <fcntl.h>

main(int arg, char **argv)
{
   int in,out,count;

   char buffer[1024];

   in = open(*++argv,O_RDONLY); // sie muß lesbar sein

   out = open(*++argv,O_WRONLY|O_TRUNC|O_CREAT,0644); 
     // falls sie existeiert, auf 0, ansonsten anlegen

   while(count = read(in,buffer,1024))
      write(out,buffer,count);

   close(in);
   close(out);
}

Read versucht die angegebene Menge Bytes in den Puffer buffer zu transferieren, die effektive Anzahl wird vom Aufruf zurückgeliefert, deshalb sollten stets nur count Bytes weggeschrieben werden. Die Anzahl von Bytes in der gewünschen Menge = 1024 entspricht der Blockgröße von Linux bezüglich geblockter Dateien, ein Lesen unter dieser Grenze ist für geblockte Dateien unsinnig, da mehr Systemcalls als nötig in Anspruch genommen werden. Dasselbe gilt natürlich auch für write, allerdings kann der letzte Block auch weniger gültige Bytes enthalten, deshalb kann ich nicht einfach stur 1024 wegschreiben. Read und Write beeinflussen den kernelinternen Offsetwert, der um die jeweiligen Bytemenge geändert wird. 

Das nächste Beispiel ist etwas näher an der Realität; es implemtiert eine einfache Variante des Kommandos dd, das ja bekanntlich Dateien dumpt (Z.B. dd if=boot.img of=/dev/fd0). Sende- und Empfangsdateien können sowohl normale als auch Gerätedateien sein, die Anzahl der Blöcke kann bei mir als ein dritter Paramter übergeben werden, ist dies nicht der Fall, wird bis EOF gelesen, zum Abschluß wird die Anzahl der verarbeiteten Blöcke auf dem Bildschirm ausgegeben. Zunächst das Listing 4: 

Listing 4: dd.C
#include <unistd.h>
#include <fcntl.h>
#include <iostream.h>
#include <stdlib.h>

int do_it(int,int,int,int);
int do_it_until_eof(int,int);
int total;

main(int argc, char **argv)
{
   int in,out,start=1,end=1;
   in = open(*++argv,0);
   out = open(*++argv,O_CREAT|O_WRONLY);

   if(argc > 3)
   {
      end = atoi(*++argv);
      start = 0;
      start = do_it(in,out,start,end);
   }
   else
   start = do_it_until_eof(in,out);

   close(in);
   close(out);
   cout << start << " blocks ";
}

int do_it(int in,int out,int start,int end)
{
   int count;
   char buffer[1024];

   while(start < end)
   {
      count = read(in,buffer,1024);
      if (!count)
         break;
      write(out,buffer,count); ++start;
   }

   return start;
}

int do_it_until_eof(int in,int out)
{
   char buffer[1024];
   int count,total=0;

   while(count=read(in,buffer,1024))
   {
      ++total;
      write(out,buffer,count);
   }

   return total;
}

Der Aufruf von dd kann in folgenden Formen erfolgen: 

 dd /dev/fd0 /tmp/otto

der Inhalt der Floppy wird nach /tmp/otto kopiert 

 dd /tmp/otto /dev/fd0

die Datei /tmo/otto wird auf die Floppy kopiert 

 dd /dev/fd0 /tmp/otto 10

es werden von der Floppy 10 Blöcke nach /tmp/otto kopiert 

In main werden die Dateien geöffnet, das kann jederzeit auch eine Gerätedatei sein, anschließend wird gecheckt, ob die Anzahl der zu kopierenden Blöcke angegeben worden ist und danach in Abhägigkeit entweder do_it oder do_it_until_eof aufgerufen. Der Rest sollte selbsterklärend sein. 

Lseek

Der Systemcall lseek erlaubt das Positionieren in Dateien. Linux interpretiert ja die Dateien als Streams, d.h. als eine endliche Folge von Bytes, lseek erlaubt, eine relative Adresse in dieser Datei anzugeben, die anschließend vom Kernel als neue Offset-Position übernommen wird. Der Aufruf von lseek erfolgt in der Form: 

lseek(fd,offset,where);

wobei fd der Dateideskriptor und offset die neue relative Adresse ist, die in Abhängikeit vom dritten Parameter interpretiert wird: 

where = 0 offset wird vom Start interpretiert
where = 1 offset wird von der aktuelle Position interpretiert
where = 2 offset wird vom Ende interpretiert. 

Eine kleine Anwendung soll wieder den Zusammenhang verdeutlichen. In Anlehnung an das Kommando rdev, das für ein Kernelimage root-flags, ram-size, vga-mode und root-device setzen kann, schreibe ich ein kleines Programm, das Teile dieser Werte ausgibt (siehe Listing 5): 

Listing 5: rdev.C
#include <unistd.h>
#include <iostream.h>
#include <fcntl.h>

main()
{
   int fd; 
   char *meldung[3] ={"ram-size:", "vga-mode:", "root-device"};
   fd = open("/vmlinuz",O_RDONLY);

   lseek(fd,504,0);

   short buffer;

   for(int i = 0; i<=2;++i)
   {
      read(fd,(char *)&buffer,sizeof (short));
      cout << meldung[i] << buffer << '\n';
   }
}

Die Informationen liegen im Kernel ab Offset 504 dezimal vor, daher muß ich zunächst auf diese Position gehen. Jeweils 2 folgende Bytes enthalten die Informationen, die mit jedem read geholt werden und anschließend auf dem Bildschirm ausgegeben werden. Das hier jeweils nur 2 Bytes gelesen werden, kann vernachläßigt werden, da ein blockorientiertes Lesen hier unsinnig ist. 

Ein letztes Beispiel zeigt, wie eine Datei in einer bestimmten Blockgröße angelegt werden kann, die Anzahl der Blöcke wird dabei als Eingabeparameter übergeben (Siehe Listing 6): 

Listing 6: file_size.C
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>

char buffer[1024];

main(int argc,char **argv)
{
   int fd; char buffer[1024];
   fd = open(*++argv,O_CREAT|O_WRONLY,0644);
   int size = atoi(*++argv);
   --size;
   lseek(fd,size*1024,2);
   write(fd,buffer,1024);
   close(fd);
}

Es wird auf das Ende mit dem entsprechenden offset positioniert und ein Block weggechrieben, dies ist erforderlich, um die Datei auch wirklich in der gegebenen Größe zu produzieren Für große Dateien existiert unter Linux auch eine lseek Variante mit dem Namen llseek, die als offset einen long long int bastelt. 

Alle genannten Systemcalls operieren als Schnittstelle zum Linux spezifischen virtuellen File System (VFS), die konkrete Implementierung liegt beim entsprechenden Handler des konkreten Dateiensystem: Linux unterstützt ja bekanntlich sehr viele. 

Die genannten Systemcalls sind Bestandteil und notwendige Voraussetzung aller highlevel I/O - Funktionen der Bibliotheken von C und C++ sowie natürlich auch aller anderen Programmiersprachen. 

Die nächste Folge beschäftigt sich mit Systemcalls rund um Dateien und Datei-Systeme. 

Literatur

[1]Linux-Kernel-Programmierung, Addison-Wesley, 1997
[2]Rochkind, Unix-Programmierung für Fortgeschrittene, Hanser, 1985
Der Autor

Wolfgang Hetzler arbeitet seit 1985 als Dozent an einem privaten Institut für Fort- und Ausbildung im EDV-Bereich und als Lehrbeauftragter an der FH-Frankfurt im Bereich Ingenieur-Informatik. Linux ist ihm seit der Version 0.9xx bekannt und eine Quelle ständiger Auseinandersetzung. Zu erreichen ist er unter het@het.gg.eunet.de.

Copyright © 1997 Linux-Magazin Verlag