5.4. Daten zwischen Kernel- und User-Space transferieren

Treiberfunktionen können aus mehreren Gründen nicht direkt auf den Speicherbereich einer zugehörigen Treiberinstanz zugreifen. Zum einen findet durch die Memory Management Unit eine Adressumsetzung statt, und die Adresslage der Applikationsspeicherbereiche ist dem Treiber nicht bekannt. Zum anderen können die Speicherbereiche im Kernel- und User-Space vor einem gegenseitigen Zugriff geschützt sein.

Für die Treiberentwicklung ergibt sich daraus die Konsequenz, dass der Zugriff auf Speicherbereiche der Applikation nicht ohne Mithilfe des Betriebssystemkerns möglich ist. Zu diesem Zweck stellt der Kernel Funktionen zur Verfügung, mit denen Daten zwischen Kernel und Applikation ausgetauscht werden können:

  1. copy_to_user

  2. copy_from_user

  3. put_user

  4. get_user

Die Funktionen sind in der Header-Datei <asm/uaccess.h> deklariert.

Bevor diese Funktionen die Daten kopieren, stellen sie sicher, dass die zugehörigen Speicherbereiche wirklich vorhanden sind und dass auf diese zugegriffen werden kann. Der Kernel kann schließlich nicht davon ausgehen, dass ihm die Applikation in jedem Fall eine korrekte Adresse übergibt. Der Zugriff auf eine inkorrekte Adresse könnte zu Instabilitäten führen, was in jedem Fall zu vermeiden ist.

Ruft eine Applikation einen Systemcall auf, der einen Datentransfer bewirken soll (z.B. read), wird dem zugehörigen Treiber die in der Applikation verwendete (logische) Adresse 1:1 übergeben. Die durch die Applikation übergebenen Adressinformationen werden in den erwähnten Funktionen (copy_to_user, copy_from_user, ...) direkt als Parameter verwendet. Zu beachten ist jedoch, dass der Zugriff nur auf Speicherbereiche des gerade aktiven Rechenprozesses möglich ist.

Die Funktionen copy_to_user und copy_from_user geben die Anzahl der Bytes zurück, die nicht kopiert werden konnten. Im Normalfall wird »0« zurückgegeben als Zeichen dafür, dass die Kopieraktion erfolgreich war (siehe Beispiel Datentransfer zwischen Kernel- und User-Space).

Beispiel 5-16. Datentransfer zwischen Kernel- und User-Space

static ssize_t driver_read( struct file *instance, char *user, size_t to_copy,
                      loff_t *offset )
{
    int not_copied;

    to_copy = min( to_copy, sizeof(internal_buffer) );     (1)
    if( (not_copied=copy_to_user( user, internal_buffer, to_copy )) ) {(2)
        printk("Driver was not able to copy %d bytes\n", not_copied );
    }
    return to_copy-not_copied;                             (3)
}
(1)
Das in <linux/kernel.h> deklarierte Makro stellt sicher, dass bei der späteren Kopieraktion nicht mehr Bytes kopiert werden, als überhaupt im internen Buffer »internal_buffer« vorhanden sind.
(2)
Hier wird die Funktion copy_to_user aufgerufen. Die Anzahl der Bytes, die nicht kopiert werden konnten, wird in der Variablen »not_copied« abgespeichert. Gab es Probleme beim Kopieren, wird zudem eine Meldung ausgegeben.
(3)
Rückgabe der read-Funktion ist die Anzahl der tatsächlich kopierten Bytes. Das sind die Bytes, die kopiert werden sollten (»to_copy«) abzüglich der Bytes, die nicht kopiert werden konnten (»not_copied«).

Die Funktionen copy_to_user und copy_from_user kopieren Speicherbereiche. Daneben gibt es die Makros, die jeweils einen einzelnen Wert, der ein Byte, zwei Byte, 4 Byte oder 8 Byte lang sein kann, kopieren: put_user und get_user. Das Makro put_user bekommt als ersten Parameter den Wert und als zweiten Parameter die Adresse übergeben, an die der Wert abgelegt werden soll (siehe Datentransfer mit Hilfe der Funktion put_user). Muss nur ein einzelner Wert kopiert werden, wird man aus Performancegründen zu diesen Makros greifen.

get_user kopiert einen Wert aus dem Speicherbereich der Applikation in den Speicherbereich des Kernels. Der erste Parameter ist die Variable, in die der Wert kopiert wird. Der zweite Parameter spezifiziert die Adresse, woher der Wert stammt. Wieviel Bytes (1, 2, 4 oder 8) jeweils kopiert werden sollen, wird im Makro aufgrund des Datentyps des zweiten Parameters bestimmt.

Beispiel 5-17. Datentransfer mit Hilfe der Funktion put_user

static int internal_int_var;
...
static ssize_t driver_read( struct file *instance, char *user, size_t to_copy,
                      loff_t *offset )
{
    if( to_copy != sizeof(internal_int_var) )              (1)
        return 0;
    if( put_user( internal_int_var, (int *)user ) ) {      (2)
        printk("driver_read: put_user failed\n" );         (3)
        return -EFAULT;
    }
    return sizeof(internal_int_var);
}
(1)
Die Funktion ist so implementiert, dass sie nur dann einen Wert zurückgibt, wenn die lesende Treiberinstanz die genaue Anzahl Bytes (»4«) anfordert.
(2)
Das Makro put_user bestimmt die Zahl der zu kopierenden Bytes aufgrund des Datentyps, auf den der übergebene Pointer (hier »User«) zeigt. Da dieser Pointer aber im Prototyp der driver_read-Funktion als »char *« definiert ist, muss ein Casting auf »int *« vorgenommen werden. Nur wenn das Casting stimmt, wird die korrekte Anzahl Bytes kopiert!
(3)
Ist der Rückgabewert von put_user ungleich »Null«, konnten die Daten nicht kopiert werden. Möglicherweise wurde durch die Applikation ein falscher Speicherbereich (z.B. ein Null-Pointer) übergeben.

Die bisher zum Datentransfer vorgestellten Funktionen überprüfen den durch die Applikation übergebenen Pointer, ob dieser auf einen gültigen Speicherbereich zeigt. Sobald innerhalb eines durch die Applikation aufgerufenen Systemcalls (z.B. ioctl) mehrfach Daten zwischen Kernel und Applikation kopiert werden, steigt der Aufwand für den Entwickler: Mit jedem Aufruf wird die Gültigkeit respektive die Existenz eines Speicherbereiches überprüft. Zur Optimierung bietet Linux daher die Möglichkeit, das Überprüfen des Speicherbereiches vom Kopieren der Daten zu splitten. Die Funktionen, über die der Treiberentwickler direkt zugreifen kann, lauten:

Ob ein Speicherbereich gültig ist oder nicht, kann explizit über die Funktion access_ok festgestellt werden. Als Parameter sind der Pointer, die Größe des zu überprüfenden Bereiches und die Art des Zugriffes (lesen oder schreiben) zu übergeben. Beispiel Optimierter Datentransfer zeigt die Anwendung der Funktion.

Beispiel 5-18. Optimierter Datentransfer

int vt_ioctl(struct tty_struct *tty, struct file * file,
             unsigned int cmd, unsigned long arg)
{
    ...
    case VT_RESIZEX:
    {
        struct vt_consize *vtconsize = (struct vt_consize *) arg;
        ushort ll,cc,vlin,clin,vcol,ccol;
        if (!perm)
            return -EPERM;
        if(!access_ok(VERIFY_READ,(void *)vtconsize,sizeof(struct vt_consize)))(1)
            return -EFAULT;                                (2)
        __get_user(ll, &vtconsize->v_rows);                (3)
        __get_user(cc, &vtconsize->v_cols);
        __get_user(vlin, &vtconsize->v_vlin);
        ...
(1)
Zunächst wird überprüft, ob der lesende Zugriff auf den Speicherbereich gültig ist.
(2)
Ist der Zugriff nicht möglich (Rückgabewert der Funktion access_ok ist ungleich 0), wird der Systemcall beendet.
(3)
Für die Treiberfunktion ist es einfacher, eine Reihe von get_user-Funktionen zu verwenden, da damit die gelesenen Werte den richtigen Variablen im Treiber zugeordnet werden können. Der Zugriff erfolgt mit der Funktion __get_user, so dass die unnötig gewordene Überprüfung der Speicherbereiche entfällt.


Lizenz