Aus Linux-Magazin 05/2024

Kernel- und Treiberprogrammierung mit dem Linux-Kernel – Folge 133

© Shawn Hempel / 123RF.com

Oft tummeln sich eigentlich zusammengehörige Daten an unterschiedlichen Stellen im Speicher. Dank des IOV-Iterators können Kernel und Applikationen damit effizient umgehen.

Mit Linux lassen sich erfreulich einfach Applikationen erstellen, die auf Peripherie zugreifen und Daten lesen oder schreiben. Ein »open()« hier, ein »read()« oder »write()« dort, und schon flutschen die Daten hin und her. Der Blick unter die Motorhaube offenbart jedoch, dass es noch erheblich mehr als nur ein »open()«, »close()«, »read()« oder »write()« gibt. Kernel-Entwicklerinnen und -Entwickler haben so manche Funktionalität implementiert, sie danach allerdings nur stiefmütterlich dokumentiert – wenn überhaupt.

Versierte Programmiererinnen und Programmierer kennen beispielsweise die Funktionen »readv()« und »writev()«. Das “v” im Funktionsnamen steht für Vektor. Um Daten zu schreiben, die unzusammenhängend zerstreut im Speicher liegen, kann man der Funktion »writev()« eine Liste der Speicherbereiche (wir reden hier auch von Segmenten) übergeben. Statt also mehrere Schreibaufträge zu erteilen, genügt ein einziger. Das spart insbesondere Kontextwechselzeiten und kommt darum dem auf Performance ausgelegten Kernel entgegen. Zusätzlich gelingt damit ein atomares Schreiben mehrerer Blöcke, ohne dass andere Jobs dazwischenkommen. Ob die Daten dann auch innerhalb des Kernels gleich effizient weiterverarbeitet werden, hängt allerdings vom darunterliegenden Treiber ab.

Ein einfacher Gerätetreiber implementiert neben den Routinen zur (De-)Initialisierung von Modulen klassischerweise zur Applikation passende Zugriffsfunktionen wie »driver_open()«, »driver_close()«, »driver_read()« und »driver_write()«. Verwendet die Applikation den Syscall »writev()« und ist im Treiber nur eine Funktion »driver_write()« implementiert, ruft der Kernel für jedes Segment die Methode »driver_write()« separat auf. Geht das nicht effizienter?

Effizienz siegt

Ja, das geht tatsächlich. So wie in der Applikation ein einzelner Funktionsaufruf genügt, kann man auch im Kernel alles über eine Methode abwickeln. In der Funktionsliste zu einem Gerätetreiber, der »struct file_operations«, gibt es dazu die Einträge »read_iter« und »write_iter«. Sie belegt man im Treiber mit den Adressen von Funktionen, die wir »driver_read_iter()« und »driver_write_iter()« nennen wollen. Sie lassen sich alternativ oder zusätzlich zu den Methoden »driver_write()« und »driver_read()« implementieren.

Falls man diese Methoden zusätzlich zu »driver_read()« und »driver_write()« implementiert, wählt der Kernel abhängig vom System Call die passende Methode aus. Liest also eine Applikation per »read()« Daten ein, wird im Kernel »driver_read()« abgearbeitet. Liest sie die Daten dagegen per »readv()«, kommt »driver_read_iter()« zum Zuge. Ist aber nur eine der beiden Alternativen realisiert, bedient diese Funktion sowohl »read()« als auch »readv()«. Dasselbe gilt analog für das Schreiben über »write()« und »writev()« (Abbildung 1).

Abbildung 1: Intern wählt der Kernel stets die optimale Transferfunktion aus.

Abbildung 1: Intern wählt der Kernel stets die optimale Transferfunktion aus.

Die Datenstruktur, die anwendungsseitig einen Speicherbereich definiert, also ein Segment, ist vom Typ »struct iovec«. Sie umfasst nur zwei Elemente: die Anfangsadresse des Segments sowie die Länge (Listing 1). Packt man mehrere dieser »struct iovec« in ein Array, kann man es einer Funktion »readv()« oder »writev()« unter Angabe der Anzahl der Array-Elemente übergeben (siehe Listing 3, Zeile 31 und 43).

Listing 1

Standarddeklaration eines Segments

struct iovec {
  void   *iov_base;
  size_t  iov_len;
};

Ein Wiederholer

Für den Zugriff auf dieses Array definiert der Linux-Kernel in der Struktur »struct iov_iter« einen IOV-Iterator [1]. Der sieht auf den ersten Blick wegen mehrerer Unions recht kompliziert aus, lässt sich aber dahingehend abstrahieren, dass dort neben einigen Zusatzinfos vor allem ein Zeiger auf die Liste mit den IO-Vecs (Segmenten) abgelegt ist. Hinzu kommen die Gesamtzahl der zu transferierenden Bytes (Element »count«), der aktuelle Offset (»iov_offset« – dazu gleich mehr) sowie die Anzahl der Listenelemente (Element »nr_segs«). Ein Beispiel zeigen Listing 2 und Abbildung 2.

Listing 2

IOV-Iterator (vereinfacht)

struct iov_iter {
  u8 iter_type;
  bool data_source;
  size_t iov_offset;
  const struct iovec *__iov;
  size_t count;
};
Abbildung 2: Der IOV-Iterator hilft beim Zugriff auf IO-Vektoren.

Abbildung 2: Der IOV-Iterator hilft beim Zugriff auf IO-Vektoren.

Die über die Union definierten alternativen Elemente der Datenstruktur deuten bereits an, dass der Iterator an vielen Stellen im Kernel zum Einsatz kommt. Vergleicht man unterschiedliche Kernel-Versionen miteinander, erkennt man zudem, dass die Entwickler ständig weiter daran schrauben.

Bei den Zusatzinfos in der Struktur »struct iov_iter« finden sich der Typ des Iterators (»iter_type«) und die Transferrichtung (»data_source«). Die Header-Datei »linux/uio.h« listet derzeit sechs Typen auf (siehe Tabelle “Typen von Iter-Datenstrukturen”). Für uns ist der Typ »ITER_IOVEC« relevant, der bei den Syscalls »readv()« und »writev« zum Tragen kommt. Die Transferrichtung ist Lesen oder Schreiben, je nachdem, ob wir auf »readv()« oder »writev()« reagieren.

Typ

Einsatzzweck

»ITER_UBUF«

in Kombination mit einer »driver_read()«- oder »driver_write()«-Methode

»ITER_IOVEC«

bei »driver_read_iter()« oder »driver_write_iter()«

»ITER_BVEC«

für Segmente vom Typ »struct bio_vec«

»ITER_KVEC«

für Daten im Kernelspace

»ITER_XARRAY«

für Xarray-Segmente (Radix-Tree).

»ITER_DISCARD«

für das Verwerfen von Datenbereichen (»truncate«).

Es geht voran

Wie der Name Iterator andeutet, dient das Objekt dazu, durch den IO-Vektor zu iterieren. Daraus lässt sich folgern, dass die Inhalte in der Datenstruktur nicht konstant sind, sondern beim Iterieren angepasst werden. So charakterisieren insbesondere die aktuellen Werte der Felder »count«, »nr_segs« und »iov_offset« den Zustand der Datenstruktur, der sich im Übrigen über die Funktion »iov_iter_save_state()« sichern lässt.

Liest man also beispielsweise Daten über den Iterator aus dem IO-Vektor, vermindert sich »count« um die Anzahl der gelesenen Daten. Nach der vollständigen Bearbeitung eines Segments reduziert sich auch »nr_segs«. Der Offset wiederum spielt eine Rolle, wenn nur Teile eines Segments verarbeitet wurden: Hier steht dann im Iterator, wo es beim nächsten Zugriff weitergeht.

Eine für alle

Für die effiziente Implementierung einer Funktion »driver_read_iter()« oder »driver_write_iter()« benötigt man vor allem Funktionen, die den eigentlichen Datentransfer erledigen, also die Daten aus der Applikation (Userspace) in den Kernel (Kernelspace) transferieren und umgekehrt. In Analogie zu »copy_from_user()« und »copy_to_user()« implementiert Linux die Funktionen »copy_from_iter()« und »copy_to_iter()«.

Aber Vorsicht: Nicht nur deren Parametrierung unterscheidet sich von den erwähnten Klassikern, sondern auch der Rückgabewert. So geben die Funktionen bei Erfolg nicht null zurück, sondern die Anzahl der kopierten Bytes. Als Ziel für die zu transferierenden Daten bekommt »copy_from_iter()« eine Adresse im Kernelspace übergeben. Darauf folgt die Anzahl der zu übertragenden Bytes und schließlich der Iterator als Angabe der Datenquelle. Die Funktion kopiert die Daten in den Kernelspace und aktualisiert den Iterator, sodass beim nächsten Aufruf die nächsten Daten kopiert werden.

Und noch einmal aufgepasst: Sie müssen sicherstellen, dass nicht mehr Daten kopiert werden, als Sie im Kernel-Speicher zur Verfügung gestellt haben. Für »copy_to_iter()« gilt dasselbe, allerdings bei umgekehrter Datenrichtung. Da im Iterator die Datenrichtung gespeichert ist, überprüft der Kernel den korrekten Einsatz.

Listing 4, das wir uns später noch genauer ansehen, implementiert eine Schreibfunktion »driver_write_iter()«, die von einer Applikation per »writev()« zusammengestellte Daten in den Kernel einliest. Die Weiterverarbeitung der Daten ist nicht implementiert, sodass der Treiber lediglich die Abläufe im Kernel verdeutlicht, ansonsten funktional aber auf so etwas wie ein »/dev/null« abgespeckt ist.

Eine zugehörige Applikation zeigt Listing 3. Die Applikation »itertest.c« liest – um irgendwelche Daten zu nehmen – den Quellcode des Treibers aus Listing 4 per »readv()« in einen IO-Vektor ein, der aus zwei Segmenten (»buffer1« und »buffer2«) besteht. Der erste Speicherbereich hat eine hier willkürlich gewählte Größe von 2000 Byte, der zweite Bereich ist nur 50 Byte lang. Der IO-Vektor besteht damit aus zwei Segmenten, die in Summe 2050 Byte umfassen. Um sie per »writev()« auf unseren Treiber zu schreiben, müssen wir zunächst die zugehörige Gerätedatei öffnen. Bei Erfolg übergibt die Applikation die Daten schließlich mit einem einzigen »writev()« dem Kernel. Das funktioniert dank der Vermeidung von Mehrfachaufrufen sehr effizient.

Listing 3

itertest.c

#include <stdio.h>
#include <fcntl.h>
#include <sys/uio.h>
#include <unistd.h>
int main(int argc,
 char **argv, char **envp)
{
  char buffer1[2000];
  char buffer2[50];
  struct iovec iov[2];
  int fd_write, fd_read;
  ssize_t bytes_written, bytes_read;
  fd_read = open("write_iter.c", O_RDONLY);
  if (fd_read == -1) {
    perror("write_iter.c");
    return -1;
  }
  fd_write = open("/dev/itertest", O_WRONLY);
  if (fd_write < 0) {
    perror("itertest");
    return -1;
  }
  iov[0].iov_base = buffer1;
  iov[0].iov_len = sizeof(buffer1);
  iov[1].iov_base = buffer2;
  iov[1].iov_len = sizeof(buffer2);
  bytes_read = readv(fd_read, iov, 2);
  if (bytes_read == -1) {
    perror("readv failed");
    close(fd_read);
    close(fd_write);
    return 1;
  }
  printf("data read: %ld\n", bytes_read);
  close(fd_read);
  printf("buffer1: %p, buffer2: %p\n", buffer1, buffer2);
  bytes_written = writev(fd_write, iov, 2);
  printf("bytes_written: %ld\n", bytes_written);
  if (bytes_written<0) {
    perror("writev failed");
    close(fd_write);
    return -2;
  }
  close(fd_write);
  return 0;
}

Und ewig grüßt …

Der Treiber selbst (Listing 4) erstellt beim Laden (Funktion »mod_init()« ab Zeile 44) die Gerätedatei mit dem Namen »itertest«. Beim Zugriff auf diese Gerätedatei wird die Funktion »driver_write_iter()« (Zeile 14) aufgerufen, die einen Iterator für den IO-Vektor über den Parameter »from« erhält.

In der vorliegenden Implementierung sollen die Daten in einen 1024 Byte großen, zu Anfang der Funktion per »kmalloc()« reservierten Puffer transferiert werden. Entscheidend ist aber die nachfolgende While-Schleife ab Zeile 23. Das Makro »iov_iter_count()« liefert die Anzahl der insgesamt zu transferierenden Bytes, über alle Segmente des IO-Vektors aufsummiert. Solange also noch nicht alle Daten verarbeitet wurden, identifiziert »iov_iter_single_seg_count()« die Anzahl der Bytes, die es im aktuellen Segment noch zu kopieren gilt. Vor der eigentlichen Kopieraktion stellt ein Aufruf der Minimum-Funktion »min()« sicher, dass diese Anzahl den im Puffer vorhandenen Platz nicht übersteigt.

Falls die Funktion »copy_from_iter()« nicht die gewünschte Anzahl Bytes kopiert, gehen wir von einem Fehler aus, ansonsten wird die Zahl kopierter Bytes für die Rückgabe aufsummiert. Da »copy_from_iter()« den Iterator aktualisiert, gibt es dann nichts weiter zu tun.

An dieser Stelle würde die eigentliche Verarbeitung der Daten folgen. Ob sie anderswo gespeichert, über einen Kommunikationskanal weitergereicht oder in eine Hardware geschrieben werden, hängt vom Treiber ab. Zur Visualisierung der Abläufe geben wir im Codebeispiel Log-Informationen aus, verwenden die Daten aber ansonsten nicht.

Selbst testen

Um das Beispiel auszutesten, übersetzen Sie den Treibercode »write_iter.c« (Listing 4) mithilfe des Makefiles aus Listing 5 und dem Aufruf von Make in das Kernel-Modul »write_iter.ko«. Ein »sudo insmod write_iter.ko« lädt den Code in den Kernel, wobei die neue Gerätedatei entsteht. Die Testapplikation generieren Sie mit dem Kommando »make iter_test«. Da fürs Erste der Zugriff auf die neue Gerätedatei dem Superuser vorbehalten bleibt, rufen Sie die Applikation mit »sudo ./iter_test« auf. Über den Befehl »tail -n 15 /var/log/kern.log« lassen Sie sich die Ausgaben des Kernel-Moduls anzeigen (Abbildung 3).

Abbildung 3: Die Log-Ausgaben des Beispieltreibers verdeutlichen die Abl&auml;ufe.

Abbildung 3: Die Log-Ausgaben des Beispieltreibers verdeutlichen die Abläufe.

Es ist gut zu erkennen, dass die Funktion »driver_write_iter()« drei Mal aufgerufen wird. Da das erste Segment mit 2000 Byte zu groß für unseren Buffer von 1024 Byte ist, transferiert der Code beim ersten Schleifendurchlauf lediglich 1024 Byte. Es bleiben noch 976 Byte übrig, die im zweiten Durchlauf in den Puffer wandern (Abbildung 4). Dann ist das Segment abgearbeitet. Beim dritten Schleifendurchlauf kommt das zweite Segment mit 50 Byte an die Reihe. Im Anschluss gibt »iov_iter_count()« null zurück, und die Schleife wird beendet.

Abbildung 4: Speicherbelegung nach dem Lesen des ersten Segments.

Abbildung 4: Speicherbelegung nach dem Lesen des ersten Segments.

Listing 4

write_iter.c

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/uio.h>
#include <linux/version.h>
static dev_t my_dev_number;
static struct cdev my_dev_object;
static struct class *my_dev_class;
static struct device *my_dev_file;
#define KBUFFER_SIZE 1024
ssize_t driver_write_iter(struct kiocb *iocb, struct iov_iter *from)
{
  ssize_t ret = 0;
  size_t len, to_copy;
  char *buffer = kmalloc(KBUFFER_SIZE, GFP_USER);
  if (!buffer) {
    ret = -ENOMEM;
    goto out;
  }
  while (iov_iter_count(from) > 0) {
    len = iov_iter_single_seg_count(from);
    to_copy = min((size_t)KBUFFER_SIZE, (size_t)len);
    printk("copy_from_iter(%px, %ld, %px)\n",buffer,to_copy,from);
    if (copy_from_iter(buffer, to_copy, from) != to_copy) {
      ret = -EFAULT; // Fehler beim Kopieren
      goto free_buffer;
    }
    ret += to_copy;
    printk("do something with %ld bytes of data\n", to_copy); }
free_buffer:
  kfree(buffer);
out:
  return ret;
}
static struct file_operations fops = {
  .owner= THIS_MODULE,
  .write_iter= driver_write_iter,
};
static int __init mod_init(void) {
  int ret;
  ret = alloc_chrdev_region(&my_dev_number,0,1,"iteratortest");
  if (ret < 0) {
    pr_err("failed to allocate device number\n");
    return ret;
  }
  cdev_init(&my_dev_object, &fops);
  my_dev_object.owner = THIS_MODULE;
  if (cdev_add(&my_dev_object,my_dev_number,1)) {
    pr_err("failed to register device\n");
    goto free_device_number;
  }
#if LINUX_VERSION_CODE >= KERNEL_VERSION(6,4,0)
  my_dev_class = class_create("iteratortest");
#else
  my_dev_class = class_create(THIS_MODULE, "iteratortest");
#endif
  if (IS_ERR(my_dev_class)) {
    pr_err("no udev support\n");
    goto unregister_cdev;
  }
  my_dev_file = device_create(my_dev_class, NULL, my_dev_number, NULL, "%s", "itertest");
  if (IS_ERR(my_dev_file)) {
    pr_err("device_create failed\n");
    goto free_class;
  }
  return 0;
free_class:
  class_destroy(my_dev_class);
unregister_cdev:
  cdev_del(&my_dev_object);
free_device_number:
  unregister_chrdev_region(my_dev_number, 1);
  return -EIO;
}
static void __exit mod_exit(void) {
  /* Aufraeumen Sys-Filesystem */
  device_destroy(my_dev_class, my_dev_number);
  class_destroy(my_dev_class);
  /* Abmelden des Geraets, Freigabe der Geraetenummer */
  cdev_del(&my_dev_object);
  unregister_chrdev_region(my_dev_number, 1);
  return;
}
module_init(mod_init);
module_exit(mod_exit);
MODULE_AUTHOR("Juergen Quade");
MODULE_LICENSE("GPL");

Listing 5

Makefile für das Kernel-Modul

ifneq ($(KERNELRELEASE),)
obj-m := write_iter.ko
else
KDIR  := /lib/modules/$(shell uname -r)/build
#KDIR := /home/quade/embedded/raspi/linux/
PWD   := $(shell pwd)
default:
  $(MAKE)       -C $(KDIR)      M=$(PWD) modules
endif
clean:
  rm -rf *.ko *.mod *.mod.* *.o modules.order
  rm -rf Module.symvers

Funktionstüchtig

Die Tabelle “Funktionen zum Umgang mit dem IOV-Iterator” listet einige weitere Funktionen zum Umgang mit dem IOV-Iterator auf. Der findet an diversen Stellen im Linux-Kernel Verwendung, und nicht nur, wenn es um Speicher im Userspace geht. Die Funktion »iov_iter_init()« beispielsweise initialisiert den Iterator; per »iov_iter_advance()« kann man dessen Zustand manuell anpassen. Um den üppigen Reigen der Funktionsvielfalt zu genießen, nehmen Sie die Dienste der Header-Datei »linux/uio.h« in Anspruch [2].

Unzusammenhängenden Speicher, sogenannte Scatter/Gather-Listen, gibt es überall im Kernel. Das macht Datenstrukturen wie den hier vorgestellten IOV-Iterator tatsächlich essenziell. Dass der Umgang mit dem IOV-Iterator einfach und elegant ist, verwundert also nicht – dass die Entwickler ihn so stiefmütterlich dokumentiert haben, dagegen sehr. (jlu)

Funktion

Beschreibung

»iov_iter_init()«

initialisiert einen IOV-Iterator

»iov_iter_count()«

gibt die Anzahl unbearbeiteter Bytes im »iovec« zurück

»iov_iter_single_seg_count()«

gibt die Anzahl unbearbeiteter Bytes im Segment zurück

»iov_iter_advance()«

aktualisiert den Zustand des Iterators

»iov_iter_revert()«

korrigiert den Iterator-Zustand

»iov_iter_type()«

gibt den Typ des Iterators zurück

»iov_save_state()«

kopiert den aktuellen Zustand des Iterators

»dup_iter()«

dupliziert einen Iterator

»copy_to_iter()«

kopiert Daten in ein »iovec«

»copy_from_iter()«

kopiert Daten von einem »iovec«

»copy_page_to_iter()«

kopiert Daten von einer Page zu einem »iovec«

»copy_page_from_iter()«

kopiert Daten von einem »iovec« zu einer Page

»iov_iter_zero()«

setzt ein Segment auf null

Die Autoren

Jürgen Quade, Professor an der Hochschule Niederrhein, führt auch für Unternehmen Schulungen zu den Themen Treiberprogrammierung und Embedded Linux durch. Eva-Katharina Kunst ist seit den Anfängen von Linux Fan von freier Software.

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 7 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Nach oben