Partitionierbare Geräte

Die meisten Block-Geräte werden nicht in einem großen Stück verwendet. Statt dessen erwarten Systemadministratoren, das Gerät partitionieren zu können, also in mehrere unabhängige Pseudo-Geräte aufteilen zu können. Wenn Sie versuchen, auf sbull Partitionen mit fdisk zu erzeugen, werden Sie merken, daß nicht alles in Ordnung ist. Das Programm fdisk greift auf die Partitionen /dev/sbull01, /dev/sbull02 usw. zu, aber diese Namen existieren im Dateisystem überhaupt nicht. Oder genauer: Es gibt keinen Mechanismus, um diese Namen an Partitionen auf dem sbull-Gerät zu binden. Ein bißchen mehr Arbeit ist notwendig, bevor ein Block-Gerät partitioniert werden kann.

Um die Unterstützung für Partitionen vorzuführen, führen wir ein neues Gerät namens spull für “Simple Partitionable Utility” ein. Dieses Gerät ist deutlich einfacher als sbull, es hat keine Verwaltung der Anfrage-Warteschlange und ist weniger flexibel (so kann man etwa die Hardware-Sektorgröße nicht verändern). Sie finden das Gerät im Verzeichnis spull; es ist vollständig unabhängig von sbull, auch wenn die beiden einigen gemeinsamen Code haben.

Um die Partitionierung eines Geräts unterstützen zu können, müssen wir jedem physikalischen Gerät mehrere Minor-Nummern zuweisen. Eine Nummer ist für den Zugriff auf das gesamte Gerät (wie beispielsweise /dev/hda), die anderen für den Zugriff auf die einzelnen Partitionen (wie /dev/hda1) zuständig. Weil fdisk Partitionsnamen erzeugt, indem es dem Gerätenamen für die gesamte Festplatte ein numerisches Suffix anfügt, folgen wir im spull-Treiber der gleichen Namenskonvention.

Die von spull implementierten Geräteknoten heißen pd für “partitionable disk”. Die vier Geräte (auch “Einheiten” genannt) sind daher /dev/pda bis /dev/pdd, wobei jedes Gerät bis zu 15 Partitionen unterstützt. Die Minor-Nummern haben die folgende Bedeutung: Die niedrigstwertigen vier Bits repräsentieren die Partitionsnummer (wobei 0 für das gesamte Gerät steht), und die höchstwertigen vier Bits stehen für die Einheitennummer. Diese Konvention wird in der Quelldatei durch die folgenden Makros implementiert:


 
#define MAJOR_NR spull_major /* Definitionen in blk.h erzwingen */
int spull_major; /* muß vor blk.h deklariert werden */

#define SPULL_SHIFT 4                         /* max. 16 Partitionen  */
#define SPULL_MAXNRDEV 4                      /* max. 4 Geraeteeinheiten */
#define DEVICE_NR(device) (MINOR(device)>>SPULL_SHIFT)
#define DEVICE_NAME "pd"                      /* Name fuer Meldungen */

Der spull-Treiber verdrahtet auch die Hardware-Sektorengröße fest, um den Code zu vereinfachen:


#define SPULL_HARDSECT 512  /* 512-Byte-Hardware-Sektoren */


Die generische Festplatte

Jedes partitionierbare Gerät muß wissen, wie es partitioniert ist. Diese Information steht in der Partitionstabelle. Ein Teil des Initialisierungsprozesses besteht daher auch darin, die Partitionstabelle zu decodieren und die internen Datenstrukturen entsprechend zu aktualisieren.

Diese Decodierung ist nicht einfach, aber der Kernel stellt glücklicherweise eine Unterstützung für “generische Festplatten” bereit, die von allen Block-Treibern verwendet werden kann. Damit wird die Code-Menge, die im Treiber zur Verwaltung der Partitionen benötigt wird, deutlich reduziert. Es ist ein weiterer Vorteil, daß der Treiber-Programmierer nicht verstehen muß, wie die Partitionierung vorgenommen wird, und der Kernel neue Partitionierungsmuster unterstützen kann, ohne daß die Treiber geändert werden müssen.

Ein Block-Treiber, der Partitionen unterstützen will, muß <linux/genhd.h> einbinden und eine Struktur des Typs struct gendisk deklarieren. Diese Struktur beschreibt das Layout der vom Treiber bereitgestellten Festplatte(n); der Kernel verwaltet eine globale Liste dieser Strukturen, die abgefragt werden kann, um zu sehen, welche Festplatten und Partitionen im System verfügbar sind.

Bevor wir weitermachen, schauen wir uns zunächst die Felder in struct gendisk an. Sie müssen sie verstehen, um die generische Festplatten-Unterstützung ausnutzen zu können.

int major

Die Major-Nummer identifiziert den Gerätetreiber, auf den sich diese Struktur bezieht.

const char *major_name

Der Basisname für Geräte, die zu dieser Major-Nummer gehören. Alle Gerätenamen werden gebildet, indem diesem Namen ein Buchstabe für jede Einheit und eine Nummer für jede Partition hinzugefügt wird. Beispielsweise ist “hd” der Basisname, aus dem dann /dev/hda1 und /dev/hdb3 gebildet werden. In modernen Kerneln kann die Gesamtlänge des Namens bis zu 32 Zeichen betragen; im 2.0-Kernel war das eingeschränkter. Treiber, die abwärtskompatibel zu 2.0 sein wollen, sollten das major_name-Feld auf fünf Zeichen begrenzen. Der Name für spull ist pd für “partitionable disk”.

int minor_shift

Die Anzahl der Bit-Shift-Operationen, die benötigt werden, um die Laufwerksnummer aus der Minor-Nummer des Gerätes herauszubekommen. Der Wert in diesem Feld sollte mit der Definition des Makros DEVICE_NR(device) (siehe “the Section called Die Header-Datei blk.h” weiter vorn in diesem Kapitel) konsistent sein. In spull expandiert dieses Makro zu device>>4.

int max_p

Die maximale Anzahl an Partitionen. In unserem Beispiel ist max_p 16 oder allgemeiner 1 << minor_shift.

struct hd_struct *part

Die decodierte Partitionstabelle des Gerätes. Der Treiber benutzt dieses Element, um festzustellen, welcher Bereich der Festplattensektoren über welche Minor-Nummer erreichbar ist. Er ist für die Allokation und Freigabe dieses Arrays zuständig. Die meisten Treiber implementieren das Array als statisches Array von max_nr << minor_shift- Strukturen. Der Treiber sollte das Array mit Nullen initialisieren, bevor der Kernel die Partitionstabelle decodiert.

int *sizes

Dieses Feld zeigt auf ein Array von Integer-Werten. Es enthält die gleiche Information wie das globale Array blk_size; normalerweise sind das sogar die gleichen Arrays. Es ist die Aufgabe des Treibers, den Datenbereich zu allozieren und wieder freizugeben. Beachten Sie, daß während der Partitionsüberprüfung des Gerätes dieser Zeiger nach blk_size kopiert wird, so daß ein Treiber, der partitionierbare Geräte unterstützt, das Array blk_size nicht allozieren muß.

int nr_real

Die Anzahl der realen Geräte (Einheiten), die existieren.

void *real_devices

Dieser Zeiger wird von jedem Treiber intern genutzt, der zusätzliche private Informationen halten muß.

void struct gendisk *next

Ein Verweis auf die Liste der generischen Festplatten.

struct block_device_operations *fops;

Ein Zeiger auf die Block-Operationen-Struktur dieses Geräts.

Viele der Felder in der gendisk-Struktur werden bei der Initialisierung gefüllt, so daß es relativ einfach ist, sie zur Kompilierzeit einzurichten:


struct gendisk spull_gendisk = {
    major:              0,            /* Spaeter zugewiesene Major-Nummer */
    major_name:         "pd",         /* Name des Major-Geraets */
    minor_shift:        SPULL_SHIFT,  /* Versatz zur Geraetenummer */
    max_p:              1 << SPULL_SHIFT, /* Anzahl der Partitionen */
    fops:               &spull_bdops, /* Block-Geraete-Operationen */
/* alles andere ist dynamisch */
};

Partitionserkennung

Wenn ein Modul sich selbst initialisiert, muß es alles passend zur Partitionserkennung einrichten. spull fängt damit an, das Array spull_sizes für die gendisk-Struktur einzurichten (das auch als blk_size[MAJOR_NR] und im Feld sizes der gendisk-Struktur abgespeichert wird) sowie das Array spull_partitions, das die eigentlichen Partitionsinformationen enthält (und im Feld part der gendisk-Struktur gespeichert wird). Beide Arrays werden zu diesem Zeitpunkt mit Nullen initialisiert. Der Code sieht folgendermaßen aus:


spull_sizes = kmalloc( (spull_devs << SPULL_SHIFT) * sizeof(int),
                      GFP_KERNEL);
if (!spull_sizes)
    goto fail_malloc;

/* Mit Partitionen der Groeße 0 anfangen und den Einheiten korrekte Groeßen geben */
memset(spull_sizes, 0, (spull_devs << SPULL_SHIFT) * sizeof(int));
for (i=0; i< spull_devs; i++)
    spull_sizes[i<<SPULL_SHIFT] = spull_size;
blk_size[MAJOR_NR] = spull_gendisk.sizes = spull_sizes;

/* Das Partitions-Array allozieren. */
spull_partitions = kmalloc( (spull_devs << SPULL_SHIFT) *
                           sizeof(struct hd_struct), GFP_KERNEL);
if (!spull_partitions)
    goto fail_malloc;

memset(spull_partitions, 0, (spull_devs << SPULL_SHIFT) *
       sizeof(struct hd_struct));
/* Eintraege für die gesamte Festplatte ausfuellen */
for (i=0; i < spull_devs; i++)
    spull_partitions[i << SPULL_SHIFT].nr_sects =
        spull_size*(blksize/SPULL_HARDSECT);
spull_gendisk.part = spull_partitions;
spull_gendisk.nr_real = spull_devs;

Der Treiber sollte auch seine gendisk-Struktur in die globale Liste eintragen. Es gibt dafür keine vom Kernel bereitgestellte Funktion; das muß von Hand gemacht werden:


spull_gendisk.next = gendisk_head;
gendisk_head = &spull_gendisk;

In der Praxis implementiert das System mit dieser Liste nur /proc/partitions.

Die Funktion register_disk, die wir bereits kurz gesehen haben, hat auch die Aufgabe, die Partitionstabelle der Festplatte einzulesen:


register_disk(struct gendisk *gd, int drive, unsigned minors,
              struct block_device_operations *ops, long size);

Hier ist gd die vorher aufgebaute gendisk-Struktur, drive die Gerätenummer, minors die Anzahl der unterstützten Partitionen, ops die block_device_operations-Struktur des Treibers und size die Größe des Geräts in Sektoren.

Bei Festplatten muß die Partitionstabelle nur während der Initialisierung des Moduls und beim Aufruf von BLKRRPART aufgerufen werden. Treiber für herausnehmbare Laufwerke müssen dies auch in der revalidate-Methode machen. Es ist aber in jedem Fall wichtig, sich daran zu erinnern, daß register_disk die request-Funktion Ihres Treibers aufruft, um die Partitionstabelle einzulesen; der Treiber muß also an dieser Stelle schon hinreichend initialisiert sein, um Anfragen bearbeiten zu können. Sie sollten auch keine Sperren halten, die mit den in der request-Funktion erworbenen Sperren in Konflikt stehen könnten. register_disk muß für jede im System vorhandene Festplatte aufgerufen werden.

spull richtet die Partitionen in der revalidate-Methode ein:


int spull_revalidate(kdev_t i_rdev)
{
  /* erste Partition, Anzahl der Partitionen */
  int part1 = (DEVICE_NR(i_rdev) << SPULL_SHIFT) + 1;
  int npart = (1 << SPULL_SHIFT) -1;

  /* zuerst alte Partitionsinformationen loeschen */
  memset(spull_gendisk.sizes+part1, 0, npart*sizeof(int));
  memset(spull_gendisk.part +part1, 0, npart*sizeof(struct hd_struct));
  spull_gendisk.part[DEVICE_NR(i_rdev) << SPULL_SHIFT].nr_sects =
          spull_size << 1;

  /* dann die neuen Informationen einfuellen */
  printk(KERN_INFO "Spull partition check: (%d) ", DEVICE_NR(i_rdev));
  register_disk(&spull_gendisk, i_rdev, SPULL_MAXNRDEV, &spull_bdops,
                  spull_size << 1);
  return 0;
}

Interessanterweise gibt register_disk Partitionsinformationen durch wiederholten Aufruf von


printk(" %s", disk_name(hd, minor, buf));

aus. Deswegen gibt spull einen führenden String aus: So bekommt die Information im Systemprotokoll einen Kontext.

Wenn ein partitionierbares Modul entladen wird, sollte der Treiber alle Partitionen herausschreiben, indem für jedes Major/Minor-Paar fsync_dev aufgerufen wird. Natürlich sollte auch aller relevanter Speicher freigegeben werden. Die Aufräumfunktion in spull sieht folgendermaßen aus:


for (i = 0; i < (spull_devs << SPULL_SHIFT); i++)
    fsync_dev(MKDEV(spull_major, i)); /* Geraete herausschreiben */
blk_cleanup_queue(BLK_DEFAULT_QUEUE(major));
read_ahead[major] = 0;
kfree(blk_size[major]); /* ist auch gendisk->sizes */
blk_size[major] = NULL;
kfree(spull_gendisk.part);
kfree(blksize_size[major]);
blksize_size[major] = NULL;

Es ist auch notwendig, die gendisk-Struktur aus der globalen Liste zu entfernen. Es gibt keine Funktion dafür, weswegen das von Hand gemacht wird:


for (gdp = &gendisk_head; *gdp; gdp = &((*gdp)->next))
    if (*gdp == &spull_gendisk) {
        *gdp = (*gdp)->next;
        break;
    }

Beachten Sie, daß es kein Gegenstück unregister_disk zu register_disk gibt. Alles, was register_disk macht, wird in den Arrays des Treibers gespeichert, so daß kein zusätzliches Aufräumen notwendig ist.

Partitionserkennung mit initrd

Wenn Sie Ihr Root-Dateisystem von einem Gerät aus einhängen wollen, dessen Treiber nur in modularisierter Form vorliegt, dann müssen Sie das initrd-System verwenden, das in modernen Linux-Kerneln zur Verfügung steht. Wir werden hier nicht beschreiben, wie man initrd benutzt; dieser Abschnitt ist für Leser gedacht, die initrd kennen und sich fragen, wie es Block-Treiber beeinflußt. Weitere Informationen zu initrd findfen Sie in der Datei Documentation/initrd.txt in den Kernel-Quellen.

Wenn Sie einen Kernel mit initrd booten, richtet dieser eine temporäre Laufzeitumgebung ein, bevor er das eigentliche Root-Dateisystem einhängt. Module werden normalerweise aus einer RAM-Disk geladen, die als temporäres Root-Dateisystem benutzt wird.

Weil der initrd-Prozeß gestartet wird, nachdem die gesamte Boot-Initialisierung durchgeführt worden ist (aber bevor das eigentliche Root-Dateisystem eingehängt worden ist), gibt es keinen Unterschied zwischen dem Laden eines normalen Moduls und eines solchen, das in der initrd-RAM-Disk liegt. Wenn ein Treiber als Modul geladen und verwendet werden kann, dann können alle Linux-Distributionen, auf denen initrd zur Verfügung steht, den Treiber auf ihren Installationsdisketten bereitstellen, ohne daß Sie an den Kernel-Quellen herummanipulieren müssen.

Die Geräte-Methoden von spull

Wir haben bereits gesehen, wie man partitionierbare Geräte initialisiert, aber noch nicht, wie man auf die Daten in den Partitionen zugreift. Dazu brauchen wir die Partitionsinformationen, die von register_disk im Array gendisk->part abgelegt werden. Dieses Array besteht aus hd_struct-Strukturen und wird per Minor-Nummer indiziert. hd_struct enthält zwei Felder, die für uns interessant sind: start_sect gibt an, wo eine bestimmte Partition auf der Festplatte anfängt, und nr_sects nennt die Größe dieser Partition.

Wir zeigen Ihnen hier, wie spull diese Informationen verwendet. Der folgende Code enthält nur diejenigen Teile von spull, die sich von sbull unterscheiden, weil der größte Teil des Codes identisch ist.

Zunächst einmal müssen open und close Verwendungszähler für jedes Gerät führen. Da sich der Verwendungszähler auf das physikalische Gerät bezieht, kann die folgende Zuweisung für die Variable dev verwendet werden:


 
Spull_Dev *dev = spull_devices + DEVICE_NR(inode->i_rdev);

Das hier verwendete Makro DEVICE_NR ist dasjenige, das deklariert werden muß, bevor <linux/blk.h> eingebunden wird; es wird zur physikalischen Gerätenummer expandiert, ohne zu berücksichtigen, welche Partition verwendet wird.

Während fast jede Gerätemethode auf dem physikalischen Gerät als Ganzes arbeitet, sollte ioctl die partitionsspezifischen Informationen liefern. Beispielsweise möchte mkfs die Größe der jeweiligen Partition, auf der es das Dateisystem anlegen will, und nicht die Größe des gesamten Gerätes wissen. Der ioctl BLKGETSIZE ist von der Änderung von einer Minor-Nummer pro Gerät zu mehreren Minor-Nummern pro Gerät, wie hier gezeigt wird, betroffen. Wie Sie vermutlich schon erwartet haben, wird spull_gendisk->part als Quelle für die Partitionsgröße verwendet.


 
 case BLKGETSIZE:
   /* Geraetegroeße in Sektoren zurueckgeben */
   err = ! access_ok (VERIFY_WRITE, arg, sizeof(long));
   if (err) return -EFAULT;
   size = spull_gendisk.part[MINOR(inode->i_rdev)].nr_sects;
   if (copy_to_user((long *) arg, &size, sizeof (long)))
   return -EFAULT;
   return 0;

Der andere ioctl-Befehl, der bei partitionierbaren Geräten anders ist, ist BLKRRPART. Das erneute Einlesen der Partitionstabelle ist bei partitionierbaren Geräten sinnvoll und entspricht dem Revalidieren einer Diskette nach einem Diskettenwechsel:


 
case BLKRRPART: /* Partitionstabelle neu einlesen */
  return spull_revalidate(inode->i_rdev);

Der größte Unterschied zwischen sbull und spull besteht aber in der Anfrage-Funktion. spull muß in der Anfrage-Funktion die Partitionsinformation benutzen, um die Daten für die verschiedenen Minor-Nummern korrekt zu übertragen. Das Auffinden der Übertragung geschieht einfach durch Addieren des Startsektors zu dem in der Anfrage angegebenen Sektor; die Partitionsgrößeninformation wird dann dazu verwendet sicherzustellen, daß die Anfrage in die Partition paßt. Wenn das erledigt ist, gibt es keine Implementationsunterschiede zu sbull mehr.

Hier sehen Sie die relevanten Zeilen aus spull_request:


 
ptr = device->data +
      (spull_partitions[minor].start_sect + req->sector)*SPULL_HARDSECT;
size = req->current_nr_sectors*SPULL_HARDSECT;
/*
 * Sicherstellen, daß die Uebertragung in das Geraet paßt.
 */
if (req->sector + req->current_nr_sectors >
                spull_partitions[minor].nr_sects) {
    static int count = 0;
    if (count++ < 5)
      printk(KERN_WARNING "spull: request past end of partition\n");
    return 0;
}

Die Anzahl der Sektoren wird mit der Hardware-Sektorengröße (die, wie Sie sich erinnern werden, in spull hart codiert ist) multipliziert, um an die Größe der Partition in Bytes zu kommen.