Systemprogrammierung - Teil 2

Rund ums Dateiensystem

von Wolfgang Hetzler


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

Diese Folge erläutert eine Vielzahl von Systemcalls im Kontext der Dateiensysteme von Linux, deren Bedeutung in erster Linie in Sachen Manipulation und Erkenntnis liegen. Linux kennt davon sehr viele, ich werde mich zunächst auf die wichtigsten beschränken - andere werden an anderer Stelle besprochen, da sie besser in den dann aktuellen Bezugsrahmen passen. Diese Verweise auf später und die aktuellen Beschränkungen sind natürlich willkürlich - ich halte es für sehr schwierig, die Systemcalls wirklich systematisch in Kategorien einzuteilen.

Als zusätzliche Hintergrund-Information machen wir zuerst einen kleinen Ausflug, der die Implementation eines Systemcalls ein klein wenig aufhellt.

Vom virtuellen zum realexistieren Dateiensystem

In der letzten Folge hatte ich erwähnt, daß ein Systemcall wie read oder write lediglich die Schnittstelle zum VFS ist. Heute will ich ein paar weitergehende Bemerkungen als Präzisierung vornehmen - ich wähle zur Betrachtung den read-Befehl des ext2 - Filesystems.

Die Bibliotheksfunktion read mündet im Aufruf des Kernelmoduls sys_read. Dies geschieht über die generierte sys_call -Funktion, die mittels Softinterrupt 0x80 in das sys_call- Kernelmodul springt, das dann das zuständige sys_ Modul indiziert aufruft (ein späterer Beitrag behandelt die Implementation neuer Systemcalls, dann wird alles klarer).

sys_read ist eine Kernel-Funktion, die nach einigen Checks zum Aufruf

file->f_op->read(inode, file, buf, count);

führt, der Zeiger file ist ein Zeiger auf struct file, deren Instanzen als ringverkettete Liste die traditionelle Filetable bilden und inode ist Zeiger auf struct inode, die wiederum mit ihren Listenelementen die klassische Inodetable realisiert. Die Initialisierung dieser Tabellen erfolgt mit dem Systemcall open, der für normale Dateien und Verzeichnisse im VFS etabliert ist, die konkreten Zeigerwerte werden beim Zugriff über den Deskriptor ermittelt - soweit in Relation zu anderen Unix-Systemen keine großen Änderungen.

Neu ist lediglich die Tatsache, daß read ein Zeiger auf eine Funktion ist. Alle grundlegenden Operationen auf Dateien sind als Zeiger auf Funktionen in der struct file_operations beschrieben, eine wesentliche Voraussetzung in Sachen Implementation des virtuellen Dateiensystems, denn das ermöglicht ein spätes Binden des wirklichen Aufrufs an einen konkreten Adresswert.

Dieser Zeiger read hat beim Aufruf die Adresse der Funktion, die von dem jeweiligen Dateiensystem zum Lesen benutzt wird , da in der struct file wiederum eine Komponente struct file_operations *f_op existiert, die als Verweis auf die wirklichen Dateioperation einen korrekten Aufruf realisiert.

Gesonderte Calls für Verzeichnisse

Das Schreiben auf Verzeichnisse behält sich der Kernel vor, anders sieht es mit dem Lesen aus - frühere Unixvarianten ließen das direkte Lesen eines Verzeicnisses mit den Systemcall read zu, neuere bieten hierfür den Systemcall readdir mit einem entsprechenden open und close. Diese Systemcalls sind auch in Linux implementiert. Wir besprechen im fogenden

mit den Prototypen:
#include <sys/dir.h>
DIR * opendir(char *);
struct dirent readdir(DIR *);
int closedir(DIR *);
Diese Systemcalls sind realtiv einfach und wir schreiben gleich ein kleines Programm zum Lesen und Ausgeben eines Verzeichnisses.

Listing 1: readdir.C

#include <unistd.h>
#include <iostream.h>

main (int argc, char **argv)
{
        DIR *dirp;
        struct direct *d;

        dirp = opendir(*++argv);

        while (d = readdir(dirp))
        {
                cout << d->d_name << ´:´ << d_ino << '\n';
        }

        closedir(dirp);
}

opendir öffnet das Verzeichnis, der Name wird als Parameter übergeben, er versteht sich als Pfad (path). Der Zeiger dirp ist ein Zeiger auf eine Struktur, die von readdir für die weitere Arbeit benötigt wird. readdir liefert einen Zeiger auf die struct direct, die in ihren Komponenten die relevanten Informationen für uns enthält. Es existiert außerdem ein Makro

#define direct dirent 
so daß sich die Linux typische
struct dirent {
   long            d_ino;
   __kernel_off_t  d_off;
   unsigned short  d_reclen;
   char            d_name[256];
};
ergibt. Die Komponenten d_ino und d_name enthalten die für uns wichtigen Informationen, die auf dem Bildschirm ausgegeben werden. Die Inode-Nummer ist die einzige Information, die uns zur Inode führt, die alle relevanten weiteren Inforamtionen enthält. Wir betrachten nun die weiteren Systemcalls.

Erkenntnisse aus den inodes

Von Interesse sind hier die Systemcalls mit den Prototypen
#include <sys/types.h>
#include <sys/stat.h>

int stat(char *path,struct stat *buffer);
int fstat(int fd,struct stat *buffer);
Beide Systemcalls leisten die gleiche Arbeit, sie liefern Informationen aus der Inode, stat erwartet einen Path, fstat hingegen einen Dateideskriptor, der auf eine geöffnetet Datei verweist. Die Informationen stehen nach Ausführung der Systemcalls in der
struct stat {
        dev_t          st_dev;
        unsigned short __pad1;
        ino_t          st_ino;
        umode_t        st_mode;
        nlink_t        st_nlink;
        uid_t          st_uid;
        gid_t          st_gid;
        dev_t          st_rdev;
        unsigned short __pad2;
        off_t          st_size;

        unsigned long  st_blksize;
        unsigned long  st_blocks;
        time_t         st_atime;
        unsigned long  __unused1;
        time_t         st_mtime;
        unsigned long  __unused2;
        time_t         st_ctime;
        unsigned long  __unused3;
        unsigned long  __unused4;
        unsigned long  __unused5;
}

die hier in voller Länge wiedergegeben ist. Die Komponenten enthalten die üblichen Unix Inode-Infos, die Datentypen der Komponenten können der Datei types.h entnommen werden - traditioneller Unix-Stil.

Wir entwickeln eine kleine Funktion, die die Größe einer Datei zurückliefert.

Listing 2: get_size.C


#include <sys/types.h>
#include <sys/stat.h>
#include <iostream.h>

int get_size(char *);

main(int argc,char ** argv) 
{
   cout << get_size(*++argv);
}

int get_size(char *name) 
{
   struct stat buffer;
   stat(name,&buffer);
   return buffer.st_size;
}

Die Informationen sind relativ einfach zu behandeln, problematischer ist lediglich st_mode, da diese Komponenete in den einzelnen Bits Infos über

Zugriffsrechte
suid-Bit
sticky-Bit
Dateientyp

enthält. Mit den üblichen Bitoperationen lassen sich diese Informationen jederzeit auslesen.Die Zugriffsrechte liegen in den Bits 0 - 8, mit

printf("%o",buffer.st_mode & 0777);

lassen sich diese z.B. ausgeben.

Wilde Manipulationen

sind mit den nächsten vier Systemcalls möglich:
Die Calls chown und chmod entsprechen den bekannten Kommandos. Wir betrachten lediglich kurz die Funktionsprototypen mit ihren Parametern
#include <unistd.h>

int chown(char *path, int owner, int group);
int chmod(char *path, int mode);
und schreiben

Listing 3: change_ident.C

#include <unistd.h>
#include <iostream.h>

main(int argc, char **argv)

{
   if(argc != 4)
   {
      cerr << "usage: change_ident uid gid  file\n";
      exit (1);
   }

   chown(argv[3],atoi(argv[1]),atoi(argv[2]));
   //     file      uid           gid
}

Change_ident verändert den Eigentümer und die Gruppe, die Werte werden numerisch übergeben, die effektive User-Id des ausführenden Prozesses muß 0 sein (euid kommt später), also sollte root dieses Programm ausführen. Der Call chmod verändert die Zugriffsrechte, arbeitet aber ansonsten analog zu chown, die Zugiffsrechte können vom Eigentümer und vom Superuser verändert werden.

Der Systemcall chroot verändert für einen Prozeß das root-Verzeichnis, ein Wechsel über das neue root-Verzeichnis hinaus ist dann nicht mehr möglich, der Prototyp lautet:

#include <unistd.h>

int chroot(char *path);
chroot ist z.B von Bedeutung für anonymous ftp, da ja bekanntlich hier ein Teilnehmer nur eine Teilsicht des Dateiensystems erhält, die Anwendung des Systemaufrufs ist trivial, da dieser lediglich den Namen des neuen root-Verzeichnisses erhält.

Das aktuelle Verzeichnis verändern wir mit dem Call chdir, der Name des neuen Verzeichnisses wird übergeben. Die Shell implementiert so ihr internes Kommando cdr. Zunächst der Prototyp

#include <unistd.h>

int chdir(char * path);
und anschließend implementieren wir die interne Funktion cd:

Listing 4: cd.C

#include <unistd.h>

int cd(char *);
char * getenv(char *);

int cd(char *path)
{
   if(path == 0) {
      return chdir(getenv("HOME"));  //Aufruf war cd
   }

   return chdir(path); //Aufruf war cd path
}

Die Funktion getenv liefert das Heimatverzeichnis aus der Umgebungsvariablen HOME (besser wäre es, HOME aus der passwd zu holen, da die Umgebungsvariable geändert sein könnte, so what..), anschließend wechseln wir das Verzeichnis, allerdings nur, wenn dieser Wechsel möglich ist. Sicher können wir nur sein, wenn wir uns vorher überzeugt hätten, daß die Berechtigung vorliegt. Das führt uns zur Abteilung

Zugriffskontrolle

Der Systemcall access bietet die Möglichkeit, vor Zugriff auf eine Datei die gesetzten Rechte zu kontrollieren, das nachfolgende Programmsegment illustriert die Anwendung:
#include <unistd.h>

int access(char *path, int mode);

if(access(argv[1],R_OK)==-1)
{
   perror("access: ");
   exit(1);

}
Für die Zugriffskontrolle existieren die Makros die bei Aufruf übergeben werden.

Und weg damit

Auch Dateien und Verzeichnisse müssen hin und wieder gelöscht werden. Unter Linux stehen dafür die Systemcalls mit den Prototypen
#include <unistd.h>

int unlink(char *path);
int rmdir(char *path);
zur Verfügung, wobei path jeweils der Name des zu löschenden Objektes ist.

Und was noch fehlt

sind die Systemcalls, die in einer kurzen Übersicht zum Abschluß zumindestens schon vorgestellt werden:
mknod       erzeugt inodes
pipe        erzeugt selbige
dup         dupliziert Deskriptor
fcntl       manipuliert Deskriptoren
select      selektiert Eingabekanäle
flock       sperrt Datei
ioctl       manipuliert Gerätetreiber
mount       montiert Dateiensysteme
umount      demontiert selbige
sync        synchronisiert Dateiensysteme
statfs      Infos zum Dateiensystem
umask       Maske für Zugriffsrechte
utime       Zeitstempel setzen
uselib      shared libs auswahle
Es gibt also noch einiges zu lernen. Die nächste Folge beschäftigt sich mit Prozessen. In der Zwichenzeit viel Erfolg beim Experimentieren mit den bisher besprochenen Systembefehlen.

Infos

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

Wolfgang Hetzler arbeitet seit 1985 freiberuflich als Dozent für Informatik und als Lehrbeauftragter an der FH Frankfurt. Linux kennt er seit 0.9xx und ist ihm ein Quell ständiger Auseinandersetzung. Zu erreichen ist er unter het@het.gg.eunet.de.

Copyright © 1998 Linux-Magazin Verlag