Erweiterte Treiberfunktionen


... [ Seminar Linux und Apache ] ... [ Thema Gerätetreiber unter Linux 2.4 ] ... [ Blockgerätetreiber ] ...

Übersicht: Erweiterte Treiberfunktionen


ioctl

ioctl bietet einen gerätespezifischen Eingangspunkt für den Treiber, um Kommandos zu behandeln. Es erlaubt Applikationen, auf Features zuzugreifen, die der Hardware eigen sind, z.B. Konfiguration.
In der Regel kann darauf nicht über read und write zugegriffen werden.
Wird ioctl im User-Space aufgerufen, gilt folgender Prototyp:

int ioctl(int fd,int cmd,...);

Die durch ... ausgedrückte variable Anzahl von Argumenten ist meist durch char *argp realisiert. Dieser Parameter hängt vom angegebenen Kontrollkommando (zweiter Parameter) ab.
Die Treibermethode sieht folgendermassen aus:

int (*ioctl)(struct inode *inode,struct file *filp,unsigned int cmd,unsigned long arg);

Jedem Kommando muß eine Nummer zugewiesen werden. Diese Nummer sollte im gesamten System eindeutig sein, um zu vermeiden, dass ein Kommando ein falsches Gerät anspricht.
Die Art, neue Nummern zu definieren, wurde jetzt geändert. Es werden vier Bitfelder verwendet, die in <linux/ioctl.h> definiert sind.
Über diese Nummern werden die Kommandos unterschieden, im Code mit einem switch.

Wir wollen nicht zu sehr ins Detail gehen, daher nur ein einfaches Beispiel [3]:

Die Applikation

    #include <stdio.h>
    #include <unistd.h>
    #include <fcntl.h>

    #define IOCTL_GETVALUE 0x0001

    int main( int argc, char **argv )
    {
      int i, fd;
      char buffer[128];

      fd=open(''mydevice'', O_RDWR);
      i=ioctl( fd, IOCTL_GETVALUE, buffer );
      printf(''%p - %s\n'', buffer, buffer );
    }

Der zugehörige Treiber kopiert bei Aufruf des IO-Controls einen String in den vom Benutzer übergebenen Buffer:
    /* vim: set ts=4: */
    /* ioctltreiber */
    #include <linux/fs.h>
    #include <linux/module.h>
    #include <linux/version.h>
    #include <linux/init.h>
    #include <asm/uaccess.h>

    #define IOCTL_MAJOR 240

    #define IOCTL_GETVALUE 0x0001

    static char *Version =
           ''$Id: treiber4.htm,v 1.1 2001/11/15 16:41:34 si Exp $'';

    static int MyIOctl( struct inode *Inode, struct file *File,unsigned int cmd, unsigned long arg );

    static int MyIOctl( struct inode *Inode, struct file *File,unsigned int cmd, unsigned long arg )
    {
      printk(''ioctl called 0x%4.4x %p\n'', cmd, (void *)arg );
      switch( cmd ) {
      case IOCTL_GETVALUE:
        copy_to_user( (void *)arg, ''Hollahop\n'', 10 );
        break;
      default:
        printk(''unknown IOCTL 0x%x\n'', cmd);
        break;
      }
      return( 0 );
    }

    static struct file_operations ioctlFops = {
      #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,4,0)
        THIS_MODULE,
      #endif
      NULL,  /* llseek */
      NULL,  /* read */
      NULL,  /* write */
      NULL,  /* readdir */
      NULL,  /* poll */
      MyIOctl,  /* ioctl */
      NULL,  /* mmap */
      NULL,  /* open */
      NULL,  /* flush */
      NULL,  /* release */
      NULL,  /* fsync */
      NULL,  /* fasync */
      NULL,  /* lock */
    };

    static int __init IoctlInit(void)
    {
      if(register_chrdev(IOCTL_MAJOR, ''ioctl-Driver'', &ioctlFops) == 0) {
        printk(''%s\n'', Version );
        return 0;
      };
      printk(''ioctl: unable to get major %d\n'',IOCTL_MAJOR);
      return( -EIO );
    }

    static void __exit IoctlExit(void)
    {
      printk(''cleanup_module called\n'');
      unregister_chrdev(IOCTL_MAJOR,''ioctl-Driver'');
    }

    module_init( IoctlInit );
    module_exit( IoctlExit ); 

Blocking I/O

Der Gerätezugriff erfolgt blocking oder non-blocking. Beim blocking-Mode wird die aufrufende Applikation (genauer der aufrufende Thread) blockiert, sobald der Treiber nicht in der Lage ist, angeforderte Daten direkt zu liefern (lesen) oder übergebene Daten zu schreiben.
Führt eine Applikation einen Leseaufruf im non-blocking-Mode durch, erfolgt das Lesen nur dann, wenn die Daten direkt vorhanden sind. Andernfalls returniert der Treiber: 0 Bytes gelesen.
Die Zugriffsart wird entweder bereits beim Öffnen eines Gerätes spezifiziert oder wird nachträglich mit fcntl eingestellt.

Jedes Mal, wenn ein Prozess auf etwas warten muß, sollte er schlafengelegt werden, damit der Prozessor frei ist.
In Linux gibt es verschiedene Konzepte zum Schlafenlegen und Wecken, die aber alle mit einer Queue vom Typ wait_queue_head_t arbeiten.
Die Wartequeue wird folgendermassen deklariert und initialisiert:
wait_queue_head_t my_queue;
init_waitqueue_head (&my_queue);

Es gibt neben den gleich angesprochenen diverse Möglichkeiten, einen Prozess schlafen zu legen, die sich darin unterscheiden, wie lange geschlafen wird.
sleep_on(wait_queue_head_t *queue); legt einen Prozess schlafen. Wenn das erwartete Ereignis niemals eintritt, wacht der Prozess auch nicht wieder auf.
interruptible_sleep_on(wait_queue_head_t *queue); arbeitet genauso, kann aber durch ein Signal unterbrochen werden. Damit wurde lange gearbeitet, bis int wait_event_interruptible(wait_queue_head_t queue,int condition); erschien. Dieses Makro wartet auf ein Ereignis und testet auf sein Eintreten. Es expandiert zu einer while-Schleife.

mit wake_up(wait_queue_head_t *queue), wake_up_interruptible(wait_queue_head_t *queue), wake_up_sync(wait_queue_head_t *queue) oder wake_up_interruptible_sync(wait_queue_head_t *queue) kann man die Prozesse wieder aufwecken.


poll und select

Beide Funktionen haben im Grunde die gleiche Aufgabe: sie überprüfen, ob ein Prozess auf ein oder mehrere Geräte ohne Blocking lesen oder schreiben kann.
Es gibt zwei Funktionen mit der gleichen Funktionalität, weil sie von zwei unterschiedlichen Gruppen zur gleichen Zeit implementiert wurden: select wurde in BSD Unix vorgestellt, poll war die Lösung im System V.
Seit Version 2.1.23 werden beide Versionen angeboten, die Gerätemethode basiert aber auf poll, weil es eine detailliertere Kontrolle ermöglicht.
Der Prototyp von poll sieht so aus:

unsigned int (*poll)(struct file *,poll_table *wait).

Er wird sowohl durch poll als auch durch select im Usermode aufgerufen.
die poll-Funktion hat die Aufgabe festzustellen, ob Daten direkt gelesen und ob Daten direkt geschrieben werden können. Da die Applikation, die den Poll-Systemcall aufruft, durch das Betriebssystem eventuell schlafen gelegt wird (dann, wenn keines der spezifizierten Geräte Daten zum lesen bereit hält bzw. Daten zum Schreiben entgegennehmen kann), muß das Betriebssystem auch mitbekommen, wenn der Treiber eventuell wieder Daten zur Verfügung hat. Da das Schlafenlegen der Applikation im Regelfall über eine Waitqueue erfolgt, werden dem Betriebssystem alle diesbezüglich möglichen Waitqueues mitgeteilt.

Eine mögliche Implementierung könnte damit folgendermaßen aussehen[3]:
  unsigned int MyDevice_Poll( struct file *File, poll_table *wait ){
    unsigned int mask = 0;

    if( DataAvailableToRead ) {
        mask |= POLLIN | POLLRDNORM;
    }
    if( DataCanBeWritten ) {
        mask |= POLLOUT | POLLWRNORM;
    }
    poll_wait( &ReadQueue, wait );
    poll_wait( &WriteQueue, wait );
    return( mask );
  }

Task-Queues

Eine Funktion innerhalb des Kernels, die durch einen Treiber zur Verfügung gestellt wird, kann durch Applikationen und Interrupts ausgelöst werden, aber auch durch andere Ereignisse, wie beispielsweise das Erreichen eines definierten Zeitpunkts. Der Vorgang wird durch die Applikation gestartet, die dann nur noch das Ende mitgeteilt bekommt. Dieses Konzept wird als task-queue bezeichnet.

Folgende Queues lassen sich für die Treiberentwicklung verwenden:
Die Queue tq_scheduler gibt es unter Linux 2.4 nicht mehr. Da die aufgerufenen Funktionen nicht zwangsläufig im richtigen Kontext abgelaufen sind, wurde die Queue durch die Funktion schedule_task ausgetauscht, die im richtigen Kontext praktisch das gleiche tut, nämlich eine Funktion aufrufen, sobald der Scheduler aufgerufen wird.

Eine Funktion wird mit dem Aufruf von void queue_task(struct tq_struct *task, task_queue *list); in eine Queue eingehängt. Wird eine eingehängte Funktion aufgerufen, wird sie aus der Queue entfernt. Soll sie mehrfach aufgerufen werden können, muß sie sich immer wieder selber einhängen.
Eine Funktion kann auch mehrfach eingehängt werden. Dabei ist in der Taskstruktur tq_struct neben der Funktionsadresse noch ein Parameter definiert, der der Funktion bei Aufruf übergeben wird.
    struct tq_struct {
      struct tq_struct *next;  /* linked list of active bh's */
      unsigned long sync;      /* must be initialized to zero */
      void (*routine)(void *); /* function to call */
      void *data;              /* argument to function */
    };
    
Folgender Code hängt eine Funktion in die Queue:
    tq.routine=IncCount;
    tq.data=NULL; // no argument today
    queue_task(&tq, &tq_timer);
    
IncCount muß dann so definiert sein:
    static void IncCount(void *arg){
      timerticks++;
      queue_task(&tq, &tq_timer);
      return;
    }
Es ist zu beachten, daß Funktionen, die über eine Task-Queue aufgerufen werden, nicht im Kontext eines Prozesses laufen. Der Zugriff auf die Struktur current ist dann nicht möglich.


Auf die Erläuterung von Interrupts, Zeitmanagement, Speicherverwaltung, MMAP und DMA wird im Rahmen dieser Seminararbeit verzichtet, um den Umfang nicht zu sprengen.

... [ Seminar Linux und Apache] ... [ Thema Gerätetreiber unter Linux 2.4] ... [ nach oben ] ... [ Blockgerätetreiber ] ...