5.6. Zugriffsmodi im Treiber realisieren

Die bisherigen Treiberbeispiele haben die in Kapitel Zugriffsmodi vorgestellten unterschiedlichen Zugriffsmodi (BLOCKING, NONBLOCKING) nicht beachtet. Unabhängig davon, ob das Gerät im blockierenden Modus geöffnet wurde oder nicht – es wurde stets angenommen, die Applikation bekomme, ohne warten zu müssen, einen Rückgabewert.

Ein korrekter Treiber muss die Zugriffsart, die ihm über die File-Struktur (struct file) mitgeteilt wird, auswerten (siehe Abbildung Prinzipielle Programmstruktur der driver_read-Funktion). Stehen angeforderte Daten (noch) nicht zur Verfügung oder können auszugebende Daten (noch) nicht geschrieben werden, muss

Abbildung 5-13. Prinzipielle Programmstruktur der driver_read-Funktion

Um einen Rechenprozess in den Zustand wartend zu versetzen, reicht es theoretisch aus, den Prozesszustand auf TASK_INTERRUPTIBLE zu setzen und die Funktion schedule aufzurufen. In der Praxis bleibt damit aber das Problem so genannter »Race Conditions« und die Notwendigkeit, die zugehörige Treiberinstanz wieder aufwecken zu müssen. Dieses Problem ist gelöst, wenn sich der Rechenprozess mit Hilfe von (vom Kernel zur Verfügung gestellter) Funktionen über eine so genannte »Wait-Queue« schlafen legt. Vor Verwendung ist die Wait-Queue durch Aufruf der Funktion init_waitqueue_head zu initialisieren. Beispiel Schlafenlegen und Aufwecken eines Rechenprozesses zeigt ein entsprechendes Codefragment.

Beispiel 5-26. Schlafenlegen und Aufwecken eines Rechenprozesses

static wait_queue_head_t wait_queue_for_read, wait_queue_for_write;(1)
...
    init_waitqueue_head( &wait_queue_for_read );           (2)
    init_waitqueue_head( &wait_queue_for_write );
    ...
    // keine Daten vorhanden
    if( instanz->f_flags & O_BLOCKING ) { // blocking mode
        retval = wait_event_interruptible( &wait_queue_for_read,(3)
            data_available );
        // nach dem Aufwecken arbeitet der Prozess hier weiter(4)
        ...
int InterruptServiceRoutine( ... )                         (5)
{
    ...
    // jetzt sind Daten vorhanden
    data_available = 1;
    wake_up_interruptible( &wait_queue_for_read );         (6)
    ...
(1)
Im Treiber wird für jedes mögliche Warteereignis eine eigene Wait-Queue bereitgestellt.
(2)
Die Wait-Queues müssen vor Verwendung initialisiert werden.
(3)
Wenn keine Daten vorhanden sind, legt sich der im blockierenden Modus befindliche Prozess (Treiberinstanz) schlafen. Der Prozess wartet so lange, bis die Variable data_available einen Wert ungleich »0« hat.
(4)
Wenn das Ende der Wartebedingung erreicht ist, wird die Treiberinstanz wieder aufgeweckt. Danach arbeitet der Rechenprozess an dieser Stelle weiter.
(5)
In der ISR wird festgestellt, dass Daten zur Verfügung stehen.
(6)
Damit ist das Ende der Wartebedingung erreicht. Die im Kernel-Kontext ablaufende Treiberfunktion weckt die Treiberinstanz wieder auf.

Unter Linux wird der Rechenprozess durch die Kernel-Funktion wait_event_interruptible bzw. einfach wait_event schlafen gelegt. Der Unterschied zwischen beiden Funktionen: Ein Rechenprozess, der sich mittels wait_event schlafen gelegt hat, kann nur durch das erwartete Ereignis aufgeweckt werden, nicht aber durch ein Signal, welches ihm zum Beispiel eine andere Applikation schickt. Mit Aufruf einer dieser Funktionen wird die zugehörige Treiberinstanz an genau dieser Codestelle schlafen gelegt. Wird sie zu einem späteren Zeitpunkt wieder aufgesetzt, so wird die Abarbeitung nach dem Aufruf der Funktion fortgesetzt.

Die Funktion wait_event_interruptible hat zwei Parameter. Der erste Parameter ist die Wait-Queue, anhand der sich Wartebedingungen unterscheiden lassen. So könnte beispielsweise ein Treiber einen Prozess in den Zustand wartend versetzen, wenn dieser Daten schreiben möchte, aber nicht kann, oder lesen möchte und keine Daten vorhanden sind. In einem solchen Fall müsste der Treiberentwickler zwei Wait-Queues zur Verfügung stellen: eine Lese-Wait-Queue und eine Schreib-Wait-Queue. Wenn auf diesen Treiber zwei Rechenprozesse – einer lesend und einer schreibend – zugreifen, könnte jeder von ihnen unabhängig vom anderen wieder aufgeweckt werden. Hätte dagegen der Treiberentwickler nur eine Wait-Queue zur Verfügung gestellt, dann müssten grundsätzlich beide Rechenprozesse aufgeweckt werden. Die Rechenprozesse selbst müssten zunächst die Wartebedingung überprüfen, worauf sich einer von beiden wieder schlafen legte – keine effiziente Lösung.

Der zweite Parameter stellt die Bedingung dar, die erfüllt sein muss, damit der Prozess weiterarbeiten kann. Ist beispielsweise die Variable data_available mit einem Wert ungleich Null belegt, sofern Daten vorhanden sind, muss auf genau diese Bedingung im Makro wait_event_interruptible abgeprüft werden:

    signal = wait_event_interruptible( &wait_queue, (data_available>0) );

Der Rückgabewert gibt nach dem Aufwecken an, ob das Warten durch ein Signal abgebrochen wurde oder ob die Wartebedingung normal erfüllt wurde.

Da der Aufruf von wait_event zu einem nicht unterbrechbaren (UNINTERRUPTIBLE) Taskzustand führt, der nicht durch ein Signal unterbrochen werden kann, ist hier ein Rückgabewert nicht notwendig. Wenn das Warten beendet ist, dann immer aus dem Grund, dass die Wartebedingung erfüllt ist.

Abbildung 5-14. Die Möglichkeit einer Race Condition beim Schlafenlegen eines Rechenprozesses

Die vorgestellten Makros sorgen dafür, dass zwischen der Abfrage, ob der Prozess warten muss oder nicht, und dem eigentlichen Warten (dem Schlafenlegen) nicht zufälligerweise (z.B. in einer Interrupt-Service-Routine) die Wartebedingung erfüllt wird. Dann würde der Prozess unnötig warten und möglicherweise nicht wieder geweckt werden: eine Race Condition. Diese Race Condition wird noch einmal in Abbildung Die Möglichkeit einer Race Condition beim Schlafenlegen eines Rechenprozesses verdeutlicht.

Wer einen Prozess wirklich nur schlafen legen will, kann sich der folgenden Befehlssequenz bedienen:

    set_current_state( TASK_INTERRUPTIBLE );
    schedule();

Auf diese Art wird der Prozess in den unterbrechbaren Wartezustand versetzt. Für ein nicht unterbrechbares Warten verfährt man analog:

    set_current_state( TASK_NONINTERRUPTIBLE );
    schedule();

Ein auf einer Wait-Queue schlafender Prozess wird über eine der beiden Funktionen wake_up oder wake_up_interruptible wieder aufgeweckt. Der einzige Parameter ist die Wait-Queue.

Die Funktionen werden typischerweise aus einer Interrupt-Service-Routine heraus aufgerufen, wenn die Wartebedingung erfüllt ist. Wird der Rechenprozess daraufhin wieder wach, muss im Fall eines unterbrechbaren Wartens (wait_event_interruptible) überprüft werden, ob das Warten durch ein Signal beendet wurde oder nicht. Das ist anhand des Rückgabewertes leicht geschehen:

    retval = wait_event_interruptible( wq, fillindex>actindex );
    if( retval == -ERESTARTSYS ) {
        return -ERESTARTSYS;
    }

Die Funktion wake_up bzw. wake_up_interruptible darf aufgerufen werden, auch wenn kein Prozess auf der entsprechenden Wait-Queue schläft: Schläft ein oder schlafen mehrere Prozesse, wird dieser bzw. werden diese aufgeweckt; schläft kein Prozess, wird auch keiner aufgeweckt. Es sei hier nochmals vermerkt, dass ein Aufwecken nicht zwischengespeichert wird.


Lizenz