Aus Linux-Magazin 04/2005

Kernel- und Treiberprogrammierung mit dem Kernel 2.6 - Folge 20

Mit dem Anticipatory-IO-Scheduler greift der Linux-Kernel vorausschauend und damit recht effektiv auf Festplatten zu. Der brandneue CFQ-IO-Scheduler tritt als ambitionierter Konkurrent auf. Der Artikel erklärt die Arbeitsweise beider Zugriffsstrategien und zeigt, worin sie sich unterscheiden.

Die vorige Folge der Kern-Technik-Reihe stellte die Arbeitsweise der No-Op- und Deadline-IO-Scheduler vor[1]. Beide sind Beispiele für die neue IO-Architektur in Kernel 2.6, die es erlaubt, IO-Scheduler zur Laufzeit auszutauschen. Dabei demonstriert der untätige No-Op-Scheduler auf einfache Weise, wie sich eine solche Komponente ins Gesamtsystem integriert. In echten Arbeitsumgebungen liefern der Deadline- und die im Folgenden beschriebenen Anticipatory- und CFQ-IO-Scheduler weitaus bessere Ergebnisse, denn sie arbeiten mit ausgefeilten Algorithmen.

In die Zukunft sehen

Der Anticipatory-IO-Scheduler (vorausschauend, anticipatory) kann seine Verwandtschaft zum Deadline-IO-Scheduler nicht leugnen[2]. Wie dieser sortiert er die Aufträge nach aufsteigenden Sektornummern in einem Red-Black-Tree und verhindert über eine mit Fifos implementierte Zeitüberwachung das Verhungern einzelner Aufträge. Auch er kennt die Dispatch-Queue, in die er – zum Beispiel nach dem Unplugging – Aufträge verschiebt und aus der der Plattentreiber sich die Aufträge nach dem Fifo-Prinzip herausholt.

Zusätzlich berücksichtigt der Anticipatory-IO-Scheduler jedoch auch den Rechenprozess, in dessen Auftrag er lesen und schreiben soll. Das ist durchaus sinnvoll, denn üblicherweise verteilen sich die Daten einer Applikation nicht kreuz und quer über die gesamte Festplatte. Vielmehr legen moderne Filesysteme die Daten in möglichst sequenziell hintereinander liegenden Sektoren auf der Festplatte ab, man spricht von einer Lokalität der Daten.

Die Sache hat nur einen Haken: In Unix-Systemen greifen Applikationen synchron zu. Sie starten einen neuen Leseauftrag erst, wenn der letzte Auftrag abgeschlossen ist (siehe Abbildung 1). Bei Verwendung des Deadline-Schedulers[1] bedeutet dies, dass der Schreib-Lese-Kopf der Festplatte sich bereits von der gewünschten Plattenposition entfernt hat. Der nachfolgende Auftrag müsste also wohl knapp 500 Millisekunden warten – das ist viel zu lange für ein interaktives System.

Abbildung 1: Synchrones Lesen: Typischerweise erteilen Applikationen erst dann den nächsten IO-Auftrag, wenn der vorhergehende abgeschlossen ist.

Abbildung 1: Synchrones Lesen: Typischerweise erteilen Applikationen erst dann den nächsten IO-Auftrag, wenn der vorhergehende abgeschlossen ist.

Anders als der Deadline- verschiebt der Anticipatory-Scheduler normalerweise nicht gleich einen ganzen Batch in die Dispatch-Queue, sondern nur einen einzelnen Auftrag. Holt der Gerätetreiber einen Auftrag beim IO-Scheduler ab, überprüft dieser den nächsten Kandidaten. Stammt der Auftrag vom gleichen Rechenprozess, reicht der Scheduler ihn an den Gerätetreiber weiter. Das tut er auch, wenn es sich um einen benachbarten Auftrag handelt, also einen, dessen Speicherort auf der Festplatte sich in der Nähe befindet.

Aufträge bearbeiten

In allen anderen Fällen zieht der IO-Scheduler eine Zugriffsstatistik zu Rate, die er für jeden Prozess getrennt führt. Ist zu erwarten, dass ein Prozess bald einen neuen Auftrag erteilen wird, bleibt seine Dispatch-Queue zunächst leer – der Treiber und damit das Gerät bekommen keinen neuen Auftrag und warten. Trifft ein neuer Auftrag ein, reicht der Scheduler ihn an den Gerätetreiber durch. Bleibt er aus, startet der IO-Scheduler einen Timer. Er holt sich entweder aus der Fifo-Liste (abgelaufene Deadline) oder aus dem Red-Black-Tree den nächsten Auftrag.

Abbildung 2 zeigt, wie der Anticipatory-Scheduler auf Requests wartet. Trifft während der Wartezeit kein neuer Auftrag vom selben Prozess (in der Abbildung nach Sektor 70) ein, übergibt der Scheduler den nächsten, bereits vorhandenen Auftrag (hier Sektor 100) an den Gerätetreiber.

Abbildung 2: Der Anticipatory-IO-Scheduler sortiert die Anfragen und wartet zusätzlich auf später ankommende Aufträge. Rückwärtsbewegungen des Schreib-Lese-Kopfes werden damit weitgehend vermieden.

Abbildung 2: Der Anticipatory-IO-Scheduler sortiert die Anfragen und wartet zusätzlich auf später ankommende Aufträge. Rückwärtsbewegungen des Schreib-Lese-Kopfes werden damit weitgehend vermieden.

Dass sich das Warten lohnt, zeigt ein Blick auf die für das Beispiel optimale Seek-Distanz. Für den Deadline-IO-Scheduler betrug sie im analogen Fall 189 (siehe[1]). Auch die Seek-Time, also die Zugriffszeit als solche, verkürzt sich trotz Wartens erheblich.

Der Scheduler führt Buch

Woran erkennt der Anticipatory-Scheduler, dass es besser ist, noch zu warten und den Auftrag eines anderen Prozesses nicht sofort dem Gerätetreiber zu übergeben? Dazu berechnet er für jeden Rechenprozess die bereits erwähnte Statistik, bei der er den durchschnittlichen zeitlichen Abstand zwischen zwei Leseaufträgen misst. In der Sprache des Anticipatory-Schedulers ist dies die Denkzeit (Thinktime). Zudem führt er eine Statistik darüber, wie nahe die jeweiligen Aufträge beieinander liegen (Seek-Distanz). Wegen dieses Faktors findet ein Rechenprozess kaum Berücksichtigung, der zwar häufig Requests erteilt, die aber weit auseinander liegen.

Wie seine einfacher gestrickten Kollegen unterscheidet auch der Anticipatory-IO-Scheduler zwischen Lesen und Schreiben. Allerdings richtet er sich nicht nach der Anzahl der übergebenen Batches, sondern beachtet eine einstellbare Zeitdauer. Dieser Wert ist für Lesen und Schreiben getrennt einzustellen (Parameter »read_batch_expire« und »write_batch_expire«). Die Schreibaufträge übergibt man wie beim Deadline-IO-Scheduler als Batch.

Und noch ein Unterschied zu den IO-Schedulern aus der vorigen Folge: Der Anticipatory-Scheduler gibt unter Umständen auch Aufträge mit niedrigeren Sektornummern als der aktuellen an den Gerätetreiber weiter. Er erlaubt also eine Rückwärtsbewegung des Schreib-Lese-Kopfes. Das macht er, wenn der nächsthöhere Sektor mehr als doppelt so weit entfernt ist wie der niedrigere.

Der Anticipatory-IO-Scheduler ist der Standard-IO-Scheduler im aktuellen Linux-Kernel. Die Tabelle “Anticipatory-IO-Scheduler einstellen” beschreibt die damit verfügbaren Tuning-Parameter im Einzelnen.

Tabelle
1: Anticipatory-IO-Scheduler einstellen

 

Parameter

Beschreibung

»antic_expire«

Maximale Denkzeit eines Rechenprozesses in Millisekunden.
Besitzt »antic_expire« den Wert 0, wartet der Scheduler
überhaupt nicht (ähnliches Verhalten wie beim
Deadline-IO-Scheduler). Defaultwert: 6

»read_expire«

Deadline eines Read-Requests in Millisekunden. Da nur alle
»read_expire« Millisekunden ein Auftrag aus der
»fifo_list« berücksichtigt wird, handelt es sich
um eine weiche Zeitschranke. Lese- und Schreibaufträge
behandelt der IO-Scheduler alternierend, sodass er das
Überschreiten einer Read-Request-Deadline nur dann
überprüft, wenn er auch Leseaufträge verarbeitet (im
Sync-Modus). Defaultwert: 125

»write_expire«

Deadline für einen Write-Request in Millisekunden. Das
Überschreiten einer Write-Request-Deadline verarbeitet der
Scheduler nur bei Schreibaufträgen (im Async-Modus).
Defaultwert: 250

»read_batch_expire«

Innerhalb der Zeit »read_batch_expire« verarbeitet
der IO-Scheduler Leseaufträge (Angabe in Millisekunden).
Danach schaltet er auf Schreiben um. Defaultwert: 500

»write_batch_expire«

Zeitspanne in Millisekunden, in der Schreibaufträge
verarbeitet werden. Nach Ablauf dieser Zeit bekommt der
Gerätetreiber Leseaufträge übergeben. Defaultwert:
125

»est_time«

Bei »est_time« handelt es sich nur um eine
Statusinformation des Schedulers. Insgesamt lassen sich im Kernel
2.6.10 dieser Datei die im Folgenden aufgeführten drei
Informationen entnehmen:

»exit probability«: Die Wahrscheinlichkeit, dass ein
Prozess endet oder in absehbarer Zeit keinen neuen Auftrag
generiert. Ist sie größer als 50 Prozent, bricht der
Scheduler das Warten zugunsten eines anderen Auftrags ab.

»new thinktime«: Die Wartezeit, die der Scheduler einem
Rechenprozess zugesteht, der erstmalig einen Request in Auftrag
gibt, bevor er andere, bereits schedulte Requests dem
Gerätetreiber übergibt.

»sectors new seek distance«: Die durchschnittliche
Seek-Distanz bei Leseaufträgen. Falls während des Wartens
ein neuer (unerwarteter) Auftrag eintrifft und der angeforderte
Sektor kleiner als dieser Wert ist, bricht der Scheduler das Warten
zugunsten des neuen Auftrags ab.

Completely Fair Queueing

Einen etwas anderen Ansatz als der Anticipatory-IO-Scheduler verfolgt der CFQ-IO-Scheduler (CFQ, Completely Fair Queueing). Sein Grundprinzip besteht darin, die vorhandene Bandbreite für den Plattenzugriff möglichst gerecht zwischen den einzelnen Rechenprozessen aufzuteilen.

Aufträge legt er prinzipiell nach Threadgruppen (TGID) getrennt ab. Abbildung 3 zeigt, dass dazu für jede Threadgruppe (»struct cfq_queue«) ein eigener Red-Black-Tree (»sort_list«) und eine eigene Fifo-Liste (»fifo«) zur Verfügung stehen. Innerhalb der »sort_list« gibt es übrigens keine Unterscheidung zwischen Lese- und Schreibaufträgen, wohl aber in der Fifo-Liste.

Abbildung 3: Der CFQ-IO-Scheduler besitzt für jede Threadgruppe einen eigenen Satz an Datenstrukturen. Um Fairness zu garantieren, übernimmt er von jeder Threadgruppe jeweils einen Auftrag in die Dispatch-Queue. Die so geordneten Aufträge wandern über eine Warteschlange zum Gerätetreiber.

Abbildung 3: Der CFQ-IO-Scheduler besitzt für jede Threadgruppe einen eigenen Satz an Datenstrukturen. Um Fairness zu garantieren, übernimmt er von jeder Threadgruppe jeweils einen Auftrag in die Dispatch-Queue. Die so geordneten Aufträge wandern über eine Warteschlange zum Gerätetreiber.

Dass der CFQ-Scheduler nicht selbst zwischen Lese- und Schreibaufträgen unterscheidet, spielt keine Rolle, denn Schreibaufträge erfolgen ohnehin selten im Kontext des Prozesses, zu dem die Daten gehören. Vielmehr initiiert der Systemprozess »pdflush« das Sichern der Daten aus dem Pagecache, sodass die Trennung aufgrund der Threadgruppe gleichzeitig auch Schreib- und Leseaufträge unterscheidet.

Das Dispatching, also das Verteilen und Übergeben der Aufträge an den Gerätetreiber, geschieht in einer Funktion namens »cfq_dispatch_requests()«. Für jede Threadgruppe verschiebt der IO-Scheduler jeweils einen Auftrag in die Dispatch-Queue. Aufträge einer Threadgruppe bleiben von ihm unberücksichtigt, wenn sich einer ihrer Aufträge in Bearbeitung (»in_flight«) befindet. Der Dispatch-Queue (»queuelist«) entnimmt der Gerätetreiber nach dem Fifo-Prinzip die Aufträge zur Bearbeitung.

Insgesamt versucht der Dispatcher eine Anzahl »quantum«-Requests in die »queuelist« zu übernehmen. Gibt es wenige Threadgruppen mit IO-Aufträgen, verschiebt der IO-Scheduler auch schon mal mehrere Aufträge aus einer einzigen Threadgruppe.

Timer sorgen für Gerechtigkeit

Normalerweise verschiebt der Scheduler den in der »sort_list« folgenden Auftrag einer jeden Threadgruppe – abhängig also vom zuletzt erteilten Auftrag – nach dem Elevator-Prinzip. Um dabei das Verhungern (Starvation) einzelner Aufträge zu verhindern, überwacht auch der CFQ-IO-Scheduler über die Fifo-Liste die abgelaufene Zeit. Die Werte »fifo_sync_expire« (fürs Lesen; die entsprechende interne Variable heißt »cfq_fifo_expire_r«) sowie »fifo_async_expire()« (fürs Schreiben; die interne Variable heißt »cfq_fifo_expire_w«) parametrisieren die zugehörigen Deadlines.

Steht an erster Stelle in der Fifo-Liste ein Auftrag, dessen Deadline abgelaufen ist, bevorzugt der Scheduler ihn beim Verschieben in die Dispatch-Queue. Gibt es einen solchen Auftrag sowohl in der Lese- als auch in der Schreib-Fifo-Liste, besitzt die Lese-Fifo Priorität. Allerdings überwacht der IO-Scheduler die Fifo-Listen nicht ständig. Hat er sich einmal aus der Fifo-Liste bedient, wartet er mindestens die Zeit »fifo_batch_expire« ab, bevor er erneut die Fifo-Listen der entsprechenden Queue überprüft. Die Tabelle “CFQ-IO-Scheduler einstellen” beschreibt die Parameter und ihre Verwendung im Detail.

Tabelle
2: CFQ-IO-Scheduler einstellen

 

Parameter

Beschreibung

»fifo_expire_sync«

Deadline für einen Read-Request in Millisekunden. Da der
Scheduler jedoch nur alle »fifo_batch_expire«
Millisekunden einen Auftrag aus der Fifo-Liste »fifo«
berücksichtigt, handelt es sich um eine weiche Zeitschranke.
Defaultwert: 500

»fifo_expire_async«

Deadline für einen Write-Request in Millisekunden. Da der
Scheduler jedoch nur alle »fifo_batch_expire«
Millisekunden einen Auftrag aus der Fifo-Liste »fifo«
überprüft und Leseaufträge zudem bevorzugt behandelt
werden, handelt es sich um eine weiche Zeitschranke. Defaultwert:
5000

»fifo_batch_expire«

Dieser Parameter legt die Zeit in Millisekunden fest, ab der
der Scheduler das erste Mal wieder die Fifo-Liste
»fifo« überprüft, nachdem er ihr einen
Auftrag entnommen hat. Defaultwert: 125

»find_best_crq«

Ist »find_best_crq« gesetzt, nimmt der IO-Scheduler
den nächstgünstigsten Request (also den dem letzten
Auftrag benachbarten). Steht »find_best_crq« auf 0,
sucht er den Request mit der kleinsten Sektornummer aus.
Defaultwert: 1

»quantum«

Dieser Parameter legt fest, wie viele Requests (aus beliebig
vielen Threadgruppen) minimal bei einem Durchlauf in die
Dispatch-Queue verschoben werden. Defaultwert: 4

»back_seek_penalty«

Je weniger Bewegungen der Schreib-Lese-Kopf der Festplatte
ausführen muss, desto besser ist die Performance. Entsprechend
sucht der CFQ-IO-Scheduler den Request, dessen Sektornummer
möglichst nahe dem aktuellen ist. Allerdings werden hierbei
die Sektornummern, die vor dem aktuellen Sektor liegen (also die
kleineren Sektornummern) zusätzlich noch mit der
»back_seek_penalty« bewertet (multipliziert). Ist die
»back_seek_penalty« beispielsweise 2, bevorzugt der
Scheduler einen Request, dessen Sektor maximal doppelt so weit
entfernt ist wie der nächste Sektor in Vorwärtsrichtung.
Defaultwert: 2

»back_seek_max«

Die maximale Zahl an Sektoren, die sich der Scheduler für
einen Auftrag rückwärts bewegt. Liegt der Sektor des
Requests weiter als »back_seek_max« vom letzten Request
entfernt, bevorzugt er eine Bewegung nach vorne. Defaultwert:
16384

»key_type«

Normalerweise wandern die Requests einer Threadgruppe
(»tgid«) in eine Queue. Die Gruppierung kann aber auch
nach Prozessgruppen (»pgid«), einzelnen Benutzern
(»uid«) oder einzelnen Gruppen (»gid«)
geschehen. Defaultwert: »tgid«

»queued«

Anzahl der Requests pro Threadgruppe, die zu einem Zeitpunkt in
die Dispatch-Queue eingehen. Kommt in der Threadgruppe ein weiterer
Request an, wartet der Scheduler, bis die vorhergehenden
verarbeitet sind. Defaultwert: 8

»clear_elapsed«

Dieser (nur schreibbare) Parameter setzt interne
Accounting-Variablen zurück. Intern führt das System
nämlich Buch darüber, wie lange ein Request bis zu seiner
wirklichen Bearbeitung im IO-Scheduler verweilt. Der Parameter
löscht die Variablen, die den längsten Verbleib
speichern. Allerdings hat das keinerlei Bedeutung für den
Anwender, denn weder verarbeitet der Code die Accounting-Variablen
intern, noch lassen sie sich in der im Kernel 2.6.10 eingebauten
Variante auslesen.

Blick in die Glaskugel

Im Vergleich zum Anticipatory-IO-Scheduler kann der CFQ bei Durchsatz und maximaler Latenzzeit nicht überzeugen. Wegen seiner Vorzüge in puncto Interaktivität aktivieren einige bekannte Distributionen CFQ jedoch bereits beim Booten. Entscheidender für die Kernelentwickler ist hingegen das Potenzial, das sie in den verwendeten Grundprinzipien sehen. Anders als der Anticipatory-IO-Scheduler ist der CFQ-IO-Scheduler nämlich noch nicht ausgereizt.

In der Tat arbeitet Jens Axboe, der verantwortlich für das Blockgeräte-Subsystem im Linux-Kernel ist, kräftig am CFQ. Auf der Kernel-Mailingliste kursierten bereits Patches für einen Time-sliced CFQ-IO-Scheduler mit Prioritäten[3]: Statt von jedem Rechenprozess genau einen IO-Auftrag in die Dispatch-Queue zu übernehmen, stellt diese Scheduler-Variante jedem Rechenprozess eine Zeitscheibe (Time Slice) zur Verfügung. Währenddessen übernimmt er Aufträge in die Dispatch-Queue und bearbeitet sie auch gleich. Wie beim Anticipatory-IO-Scheduler eröffnet das die Möglichkeit, eine kurze Zeit (maximal die einstellbare Zeit »slice_idle«) auf weitere Aufträge des gleichen Prozesses zu warten. Diese Aufträge sind wegen des Lokalität-Prinzips mit großer Wahrscheinlichkeit benachbart zum vorherigen.

Tabelle
3: Parameter des Time-sliced CFQ-Schedulers mit
IO-Prioritäten

 

Parameter

Beschreibung

»back_seek_max«, »back_seek_penalty«,
»fifo_expire_async«, »fifo_expire_sync«,
»fifo_batch_expire«, »quantum«,
»queued«

Diese Parameter unterscheiden sich in ihrer Bedeutung nicht von
denen des Standard-CFQ-IO-Schedulers (siehe Tabelle
“CFQ-IO-Scheduler einstellen”).

»max_depth«

Gibt an, wie viele Aufträge einer Threadgruppe der
Scheduler zu einem Zeitpunkt in die Dispatch-Queue übernimmt.
Default: 1

»slice_sync«

Länge der Basiszeitscheibe für
Best-Effort-Leseaufträge in Millisekunden. Die wirkliche
Länge der Zeitscheibe ergibt sich aufgrund der
IO-Priorität des zugehörigen Rechenprozesses. Je
höher die Priorität, desto länger ist die
Zeitscheibe. Leseaufträge von Rechenprozessen der niedrigsten
Priorität bekommen genau eine Basiszeitscheibe; bei acht
Prioritätsebenen entspricht die tatsächliche Zeitscheibe
bei der höchsten IO-Priorität acht Basiszeitscheiben.
Defaultwert: 22

»slice_async«

Länge der Basiszeitscheibe in Millisekunden für
Schreibaufträge der Prioritätsklasse »Best
Effort«. Siehe »slice_sync« für die
Herleitung der realen Zeitscheibenlänge aufgrund der
Priorität. Defaultwert: 10

»slice_idle«

Zeit in Millisekunden, die der Scheduler während einer
Zeitscheibe maximal wartet, bis der zugehörige Thread den
nächsten Auftrag anfordert (Abbildung 6). Verstreicht
»slice_idle«, bricht er die Zeitscheibe ab und sortiert
Aufträge des nächsten Thread in die Dispatch-Queue ein.
Defaultwert: 10

»slice_async_rq«

Wert, der indirekt die maximale Anzahl von
Schreibaufträgen bestimmt. In die Berechnung dieser Anzahl
fließt die IO-Priorität des Thread ein. Je höher
die Priorität, desto mehr Schreibaufträge pro
Scheduler-Umlauf sind möglich. Defaultwert: 2

So gut wie möglich

Damit nicht genug: Der neue Time- sliced CFQ-IO-Scheduler berücksichtigt zusätzlich noch IO-Prioritäten. Jens Axboe definierte zunächst drei Prioritätenklassen: »Idle«, »Best Effort« (BE) und »Realtime«. Die normalerweise verwendete Klasse »Best Effort« bietet insgesamt acht Prioritäten, wobei die mit »0« bezeichnete Priorität die höchste dieser Klasse ist (siehe Abbildung 4). Ohne weiteres Zutun teilt der IO-Scheduler einem Prozess eine Best-Effort-Priorität gemäß seinem Nice-Wert zu. Um diesen Wert zu beeinflussen oder um einem Prozess gar eine Echtzeitpriorität zu geben, führte Axboe die neuen, aber nur vorläufigen Systemcalls »ioprio_set« und »ioprio_get« ein. Sollte der Code Eingang in den offiziellen Kernel finden – so die Warnung – sind Änderungen zu erwarten. Ein Beispiel dafür, wie man diese Systemcalls nutzt, findet sich in[4].

Abbildung 4: Die IO-Priorität wird mit 16 Bit kodiert. Die obersten 3 Bits wählen eine von drei definierten Klassen aus. Die unteren 13 Bits spezifizieren im Fall von Best Effort die Priorität (von 0 bis 7).

Abbildung 4: Die IO-Priorität wird mit 16 Bit kodiert. Die obersten 3 Bits wählen eine von drei definierten Klassen aus. Die unteren 13 Bits spezifizieren im Fall von Best Effort die Priorität (von 0 bis 7).

Zeitscheiben

Abbildung 5 zeigt die wesentlichen Datenstrukturen des Time-sliced CFQ-IO-Schedulers mit Prioritäten. Das Objekt »cfq_data« repräsentiert das Gerät respektive den IO-Scheduler. Die Zeitscheibenvariante des CFQ-IO-Schedulers sortiert die Aufträge nicht auf Basis der Threadgruppen (TGID), sondern nach Threads (PID). Jeder Prozess findet eine Entsprechung in einem Objekt des Typs »cfq_queue« – allerdings nicht nur in einer Liste, sondern gleich in mehreren gemäß der IO-Priorität.

Abbildung 5: Die Requests eines Thread sind nach Prioritäten sortiert. Fordert der Gerätetreiber einen Request an, übergibt der IO-Scheduler einen Auftrag aus der »cfq_queue« an die Real-Time-Queue »cur_rr«. Ist diese leer, fügt er stattdessen einen Best-Effort-Prozess hinzu.

Abbildung 5: Die Requests eines Thread sind nach Prioritäten sortiert. Fordert der Gerätetreiber einen Request an, übergibt der IO-Scheduler einen Auftrag aus der »cfq_queue« an die Real-Time-Queue »cur_rr«. Ist diese leer, fügt er stattdessen einen Best-Effort-Prozess hinzu.

Die ersten acht Listen »rr_list« entsprechen den acht Prioritäten für Best-Effort-Aufträge. Aufträge, die über die Dispatch-Queue an den Gerätetreiber gehen, werden allerdings nur den Request-Queues entnommen, die in der Liste »cur_rr« stehen. Immer wenn »cur_rr« leer ist, entnimmt der Scheduler ein »cfq_queue«-Objekt gemäß IO-Priorität den Best-Effort-Listen. Prozessobjekte, deren IO-Aufträge der Real-Time-Klasse angehören, hängt der Scheduler direkt in die Liste »cur_rr« ein und bedient sie somit zuerst.

Die Funktion »cfq_dispatch_requests()« verschiebt nach bekanntem Schema maximal »quantum« Aufträge aus der Request-Queue in die Dispatch-Queue. Falls der zur Request-Queue gehörige Prozess seine IO-Zeitscheibe verbraucht hat, berücksichtigt der Scheduler die Request-Queue des nächsten Prozesses aus der Schlange »cur_rr«. Das »struct cfq_queue«-Objekt verwaltet die eigentlichen IO-Aufträge (»cfq_request«) in Red-Black-Trees und in Fifo-Listen getrennt fürs Lesen und Schreiben.

Tests mit der Zeitscheiben-Variante des CFQ zeigen, dass dieser bezüglich Durchsatz und Latenzzeiten zum Anticipatory-IO-Scheduler aufschließt[5]. Übertrumpft hat er ihn allerdings bislang noch nicht.

Abbildung 6: Jedem Thread steht eine Zeitscheibe zur Verfügung, während der er exklusiv Aufträge an den Gerätetreiber übergeben kann. Die Länge der Zeitscheibe hängt von der IO-Priorität ab. Wartet er nach einem Auftrag länger als »slice_idle«, wird die Zeitscheibe abgebrochen.

Abbildung 6: Jedem Thread steht eine Zeitscheibe zur Verfügung, während der er exklusiv Aufträge an den Gerätetreiber übergeben kann. Die Länge der Zeitscheibe hängt von der IO-Priorität ab. Wartet er nach einem Auftrag länger als »slice_idle«, wird die Zeitscheibe abgebrochen.

IO-Scheduling-Alternativen

Jens Axboe ist nicht konkurrenzlos, wenn es um die Optimierung der Festplattenzugriffe geht. Philips in Eindhoven beispielsweise entwickelt zusammen mit Werner Almesberger das Active Block IO Scheduling System (ABISS), das den Applikationen garantierte Lese- und Schreibraten bereitstellt (Realtime Streams). Eine erste Implementierung gibt es bereits [6]. Ob und wann allerdings Code in den Standardkernel übernommen wird, steht in den Sternen. (ofr)

Infos

[1] Eva-Katharina Kunst und Jürgen Quade, “Kern-Technik”, Folge 19: Linux-Magazin 2/05, S. 88

[2] Jens Axboe, “Linux Block IO – present and future”: Proceedings of the Linux Symposium, Volume One, Ottawa Juli 2004, [http://www.finux.org/Reprints/Reprint-Axboe-OLS2004.pdf]

[3] Patch: Time sliced CFQ IO-Scheduler with priorities: [http://www.kernel.org/pub/linux/kernel/people/axboe/patches/ v2.6/2.6.11-rc1/]

[4] Quellcode zum Programm »ionice«: [http://lwn.net/Articles/116496/]

[5] Jens Axboe: Benchmarks zum Time Sliced CFQ-IO-Scheduler: [http://lwn.net/Articles/113869/]

[6] Active Block IO Scheduling System: [http://abiss.sf.net]

Die
Autoren

Eva-Katharina Kunst, Journalistin, und Jürgen Quade, Professor an der Hochschule Niederrhein, sind seit den Anfängen von Linux Fans von Open Source. Unter dem Titel “Linux Treiber entwickeln” haben sie zusammen ein Buch zum Kernel 2.6 veröffentlicht.

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
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben