Im Seitencache werden zuletzt aufgerufene Seiten von der Festplatte zwischengespeichert, wodurch Suchzeiten für nachfolgende Zugriffe auf dieselben Daten reduziert werden. Der Auslagerungs-Cache verbessert die Leistung nicht, wenn zum ersten Mal von der Festplatte aus auf eine Seite zugegriffen wird. Wenn eine App also eine Datei nur einmal liest, ist die Umgehung des Seitencaches der bessere Weg. Dies ist möglich, indem Sie das Flag O_DIRECT verwenden. Das bedeutet, dass der Kernel diese speziellen Daten nicht für den Seiten-Cache berücksichtigt. Die Reduzierung von Cache-Konflikten bedeutet, dass andere Seiten (auf die wiederholt zugegriffen wird) eine bessere Chance haben, im Seitencache beibehalten zu werden. Dies verbessert das Cachetrefferverhältnis und sorgt für eine bessere Performance.
void ioReadOnceFile()
{
/* Durch die Verwendung von direct_fd und direct_f wird der Kernel-Seiten-Cache umgangen.
* - direct_fd ist ein Low-Level-Dateideskriptor
* - direct_f ist ein Dateistrom, ähnlich dem, der von fopen()*
zurückgegeben wird* HINWEIS: Verwenden Sie getpagesize(), um Puffer in optimaler Größe zu bestimmen.
*/
int direct_fd = open("Dateiname", O_DIRECT | O_RDWR);
DATEI *direct_f = fdopen(direct_fd, "w+");
/* direkte Festplatten-I/O erfolgt HIER*/
fclose(f);
schließen(fd);
}
Stellen Sie sich den Fall eines Lesevorgangs in einer großen Datei (d. h. einer Datenbank) vor, die aus einer großen Anzahl von Seiten besteht. Jede weitere Seite, auf die zugegriffen wird, wandert in den Seitencache, nur um später wieder herausgelassen zu werden, wenn mehr und mehr Seiten gelesen werden. Dadurch wird das Cachetrefferverhältnis deutlich reduziert. In diesem Fall bietet der Auslagerungscache keine Performancesteigerungen. Daher ist es besser, den Seiten-Cache zu umgehen, wenn man auf große Dateien zugreift.
void ioLargeFile()
{
/* Mit direct_fd und direct_f wird der Kernel-Seiten-Cache umgangen.
* - direct_fd ist ein Low-Level-Dateideskriptor
* - direct_f ist ein Dateistrom, ähnlich dem, der von fopen()*
zurückgegeben wird* HINWEIS: Verwenden Sie getpagesize(), um Puffer in optimaler Größe zu bestimmen.
*/
int direct_fd = open("largefile.bin", O_DIRECT | O_RDWR | O_LARGEFILE);
DATEI *direct_f = fdopen(direct_fd, "w+");
/* direkte Festplatten-I/O erfolgt HIER*/
fclose(f);
schließen(fd);
}
Der IO-Scheduler optimiert die Reihenfolge der I/O-Vorgänge, die auf der Festplatte in die Warteschlange gestellt werden sollen. Da die Suchzeit die höchste Belastung für eine Festplatte darstellt, versuchen die meisten I/O-Scheduler, die Suchzeit zu minimieren. Dies ist als Variante des Elevator-Algorithmus implementiert, d.h. die Neuordnung der zufällig geordneten Anfragen von zahlreichen Prozessen in die Reihenfolge, in der die Daten auf der Festplatte vorhanden sind, erfordert eine erhebliche Menge an CPU-Zeit.
Bestimmte Aufgaben, die komplexe Vorgänge umfassen, sind in der Regel durch die Geschwindigkeit begrenzt, mit der die CPU große Datenmengen verarbeiten kann. Ein komplexer I/O-Scheduler, der im Hintergrund ausgeführt wird, kann wertvolle CPU-Zyklen verbrauchen und dadurch die Systemleistung reduzieren. In diesem Fall kann der Wechsel zu einem einfacheren Algorithmus wie no-op die CPU-Last reduzieren und die Systemleistung verbessern.
echo noop > /sys/block//queue/scheduler
Dies wird zwar letztendlich die Arbeit erledigen, ist aber definitiv nicht der optimale Weg. Aus Sicht des Kernels ist die optimale Größe für I/O-Anforderungen die Blockgröße des Dateisystems (d. h. die Seitengröße). Da alle I/Os im Dateisystem (und im Kernel-Seitencache) in Bezug auf Seiten erfolgen, ist es sinnvoll, dass die App auch Übertragungen in Vielfachen der Seitengröße durchführt. Auch bei mehrfach segmentierten Caches, die jetzt ihren Weg in Festplatten finden, würde man enorm davon profitieren, I/O in Vielfachen von Blockgrößen durchzuführen.
Mit dem folgenden Befehl kann die optimaleBlockgröße ermittelt werden: stat --printf="bs=%s optimal-bs=%S\n" --file-system /dev/
Wenn eine App einen SYNC I/O-Lesevorgang initiiert, reiht der Kernel einen Lesevorgang für die Daten in die Warteschlange ein und kehrt erst zurück, nachdem der gesamte Block der angeforderten Daten zurückgelesen wurde. Während dieses Zeitraums markiert der Kernel den Prozess der Anwendung als blockiert für I/O. Andere Prozesse können die CPU nutzen, was zu einer insgesamt besseren Leistung für das System führt.
Wenn eine App einen SYNC-I/O-Schreibvorgang initiiert, stellt der Kernel einen Schreibvorgang für die Daten in die Warteschlange und versetzt den Anwendungsprozess in einen blockierten I/O-Vorgang. Leider bedeutet dies, dass der Prozess der aktuellen Anwendung blockiert ist und keine andere Verarbeitung (oder I/O) durchführen kann, bis dieser Schreibvorgang abgeschlossen ist.
Wenn eine App einen ASYNC-I/O-Lesevorgang initiiert, wird die read()-Funktion in der Regel nach dem Lesen einer Teilmenge des großen Datenblocks zurückgegeben. Die App muss read() wiederholt mit der Größe der noch zu lesenden Daten aufrufen, bis die gesamten erforderlichen Daten eingelesen sind. Jeder zusätzliche Aufruf von "read" führt zu einem gewissen Overhead, da er einen Kontextwechsel zwischen dem Userspace und dem Kernel einführt. Die Implementierung einer engen Schleife, um read() wiederholt aufzurufen, verschwendet CPU-Zyklen, die andere Prozesse hätten verwenden können. Daher implementiert man normalerweise das Blockieren mit select(), bis das nächste read() eingelesene Bytes ungleich Null zurückgibt. d.h. der ASYNC wird genauso blockiert wie der SYNC-Read.
Wenn eine App einen ASYNC-I/O-Schreibvorgang initiiert, aktualisiert der Kernel die entsprechenden Seiten im Seitencache und markiert sie als "fehlerhaft". Dann kehrt die Steuerung schnell zur App zurück, die weiter ausgeführt werden kann. Die Daten werden später zu einem optimaleren Zeitpunkt (geringe CPU-Last) auf eine optimalere Art und Weise (sequenziell gebündelte Schreibvorgänge) auf die Festplatte geleert.
Daher sind SYNC-Lese- und ASYNC-Schreibvorgänge im Allgemeinen ein guter Weg, da sie es dem Kernel ermöglichen, die Reihenfolge und das Timing der zugrunde liegenden I/O-Anforderungen zu optimieren.