Was passiert hinter den Kulissen während der Festplatten-I/O?

Lesezeit: 7 Minuten

Benutzer-Avatar
Paddy

Was passiert hinter den Kulissen, wenn ich nach einer Position in einer Datei suche und eine kleine Datenmenge (20 Bytes) schreibe?

Mein Verständnis

Meines Wissens ist die kleinste Dateneinheit, die von einer Festplatte geschrieben oder gelesen werden kann, ein Sektor (traditionell 512 Bytes, aber dieser Standard ändert sich jetzt). Das bedeutet, dass ich zum Schreiben von 20 Bytes einen ganzen Sektor lesen, einen Teil davon im Speicher ändern und zurück auf die Festplatte schreiben muss.

Dies erwarte ich bei ungepufferter E / A. Ich erwarte auch, dass gepufferte E/A ungefähr dasselbe tun, aber seien Sie klug mit seinem Cache. Also hätte ich gedacht, dass, wenn ich die Lokalität durch zufällige Suchvorgänge und Schreibvorgänge aus dem Fenster blase, sowohl gepufferte als auch ungepufferte E / A eine ähnliche Leistung haben sollten … vielleicht mit ungepufferter etwas besserer Leistung.

Andererseits weiß ich, dass es für gepufferte E/A verrückt ist, nur einen Sektor zu puffern, also könnte ich auch erwarten, dass es schrecklich funktioniert.

Meine Bewerbung

Ich speichere Werte, die von einem SCADA-Gerätetreiber gesammelt wurden, der Remote-Telemetrie für mehr als hunderttausend Punkte empfängt. Es gibt zusätzliche Daten in der Datei, sodass jeder Datensatz 40 Byte groß ist, aber nur 20 Byte davon müssen während einer Aktualisierung geschrieben werden.

Benchmark vor der Implementierung

Um zu überprüfen, ob ich mir keine brillant überentwickelte Lösung ausdenken muss, habe ich einen Test mit ein paar Millionen zufälligen Datensätzen durchgeführt, die in eine Datei geschrieben wurden, die insgesamt 200.000 Datensätze enthalten könnte. Bei jedem Test wird der Zufallszahlengenerator mit demselben Wert gesät, um fair zu sein. Zuerst lösche ich die Datei und fülle sie auf die Gesamtlänge (ca. 7,6 MB) auf, mache dann eine Schleife ein paar Millionen Mal und übergebe einen zufälligen Datei-Offset und einige Daten an eine von zwei Testfunktionen:

void WriteOldSchool( void *context, long offset, Data *data )
{
    int fd = (int)context;
    lseek( fd, offset, SEEK_SET );
    write( fd, (void*)data, sizeof(Data) );
}

void WriteStandard( void *context, long offset, Data *data )
{
    FILE *fp = (FILE*)context;
    fseek( fp, offset, SEEK_SET );
    fwrite( (void*)data, sizeof(Data), 1, fp );
    fflush(fp);
}

Vielleicht keine Überraschungen?

Das OldSchool Methode hat sich durchgesetzt – mit Abstand. Es war über 6-mal schneller (1,48 Millionen gegenüber 232000 Datensätzen pro Sekunde). Um sicherzustellen, dass ich nicht auf Hardware-Caching gestoßen bin, habe ich meine Datenbankgröße auf 20 Millionen Datensätze (Dateigröße von 763 MB) erweitert und die gleichen Ergebnisse erzielt.

Bevor Sie auf den offensichtlichen Anruf hinweisen fflush, lassen Sie mich sagen, dass das Entfernen keine Wirkung hatte. Ich nehme an, das liegt daran, dass der Cache übergeben werden muss, wenn ich weit genug weg suche, was ich die meiste Zeit tue.

So was ist los?

Es scheint mir, dass die gepufferte E / A einen großen Teil der Datei lesen (und möglicherweise vollständig schreiben) muss, wenn ich versuche zu schreiben. Da ich seinen Cache kaum ausnutze, ist dies äußerst verschwenderisch.

Außerdem (und ich kenne die Details des Hardware-Cachings auf der Festplatte nicht), wenn die gepufferte E / A versucht, eine Reihe von Sektoren zu schreiben, wenn ich nur einen ändere, würde dies die Effektivität des Hardware-Cache verringern.

Gibt es da draußen Festplattenexperten, die das besser kommentieren und erklären können als meine experimentellen Ergebnisse? =)

  • Ich gehe davon aus, dass fseek einen Flush verursacht (daher der fehlende Unterschied), da der Puffer geleert werden muss, bevor er verschoben werden kann.

    – dave

    1. November 2012 um 5:21 Uhr

  • Das Lesen und Schreiben hat die vom Dateisystem definierte Blockgröße. Unter Linux ext4 ist der Standardwert 4 KB. In ähnlicher Weise funktioniert der Dateicache (gepufferte Dateien) auf der Einheit PAGE_SIZE, wiederum höchstwahrscheinlich 4 KB.

    – Rohan

    1. November 2012 um 5:34 Uhr

  • Sie können die Pufferung auf Stdio-Ebene deaktivieren, indem Sie aufrufen setbuf(fp, NULL); nach dem Öffnen fp.

    – Café

    1. November 2012 um 6:45 Uhr

  • Abgesehen davon ist sizeof(Data) die Größe des Zeigers, nicht die Größe der Daten.

    – janeb

    1. November 2012 um 7:36 Uhr

  • @janneb Hoppla, das wurde eigentlich als C++-Projekt kompiliert, aber da es sich im Wesentlichen um C-Funktionen handelt, habe ich es als solches gekennzeichnet. Ich habe die C++-Syntax für die sizeof verwendet.

    – Reisfeld

    1. November 2012 um 8:38 Uhr

Tatsächlich sieht es zumindest auf meinem System mit GNU libc so aus, als würde stdio 4kB-Blöcke lesen, bevor es den geänderten Teil zurückschreibt. Scheint mir falsch zu sein, aber ich kann mir vorstellen, dass jemand dachte, dass es damals eine gute Idee war.

Ich habe das überprüft, indem ich ein triviales C-Programm geschrieben habe, um eine Datei zu öffnen, ein paar Daten einmal zu schreiben und zu beenden; dann lief es unter strace, um zu sehen, welche Systemaufrufe es tatsächlich ausgelöst hat. Beim Schreiben mit einem Offset von 10000 sah ich diese Systemaufrufe:

lseek(3, 8192, SEEK_SET)                = 8192
read(3, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 1808) = 1808
write(3, "hello", 5)                    = 5

Scheint, dass Sie für dieses Projekt bei der Low-Level-I/O im Unix-Stil bleiben wollen, oder?

  • 4kB ist wahrscheinlich die Blockgröße Ihres Dateisystems (es ist Standard in ext4, denke ich). Das könntest du ändern, wenn du willst.

    – dave

    1. November 2012 um 5:20 Uhr

  • @dave, es ist wahr, dass die Blockgröße des Dateisystems in diesem Test 4 KB betrug – und meine Speicherseitengröße auch -, aber ich habe libc überhaupt nicht zum Lesen aufgefordert. In diesem Fall gibt es dafür keinen Grund. Verwendung des Rohmaterials lseek/write syscalls funktioniert gut ohne zusätzliche Kopien des Kernel-Benutzerspeichers.

    – James Sharp

    1. November 2012 um 5:27 Uhr

  • Ich habe eine Theorie, warum es das liest: Da die Blockgröße des fs 4 kB beträgt, besteht die einzige Möglichkeit, sicherzustellen, dass ein anderer Prozess die Datei nicht geändert hat, darin, sie einzulesen und sie dann wieder herauszuschreiben. Die Verwendung von write umgeht diese Prüfung.

    – dave

    1. November 2012 um 5:33 Uhr


  • @dave: Huh, das ist eine interessante Theorie. Ich verstehe es jedoch nicht: Lesen-Ändern-Schreiben-Zyklen werden eingeführt mehr Datenrennen, nicht weniger. Die zugrunde liegenden Systemaufrufe bieten ein gewisses Maß an Atomarität, das nur der Kernel bieten kann.

    – James Sharp

    1. November 2012 um 5:37 Uhr

  • @paddy: Oh, hier geht es nur darum, was im Userspace passiert. Der Kernel muss immer noch den gesamten Block von der Festplatte lesen, ihn mit Ihren neuen Schreibvorgängen modifizieren und (etwas später) die Änderungen zurückschreiben. Der Kernel kann dies jedoch effizient und sicher tun, während der Userspace dies im Allgemeinen nicht kann.

    – James Sharp

    1. November 2012 um 19:43 Uhr

Benutzer-Avatar
bdonlan

Die C-Standardbibliotheksfunktionen führen eine zusätzliche Pufferung durch und sind im Allgemeinen eher für Streaming-Lesevorgänge als für zufällige E/A optimiert. Auf meinem System Ich beobachte nicht die falschen Anzeigen, die Jamey Sharp gesehen hat Ich sehe nur falsche Lesevorgänge, wenn der Offset nicht auf eine Seitengröße ausgerichtet ist – es könnte sein, dass die C-Bibliothek immer versucht, ihren E / A-Puffer auf 4 KB oder so ausgerichtet zu halten.

Wenn Sie in Ihrem Fall viele zufällige Lese- und Schreibvorgänge in einem relativ kleinen Datensatz durchführen, sind Sie wahrscheinlich am besten damit bedient pread/pwrite um zu vermeiden, Systemaufrufe suchen zu müssen, oder einfach mmapDatenmenge speichern und in den Speicher schreiben (wahrscheinlich am schnellsten, wenn Ihre Datenmenge in den Speicher passt).

  • Versuchen Sie fseeking auf einen Offset, der kein Vielfaches von 4kB oder anderen wahrscheinlichen Blockgrößen ist – deshalb habe ich Offset 10.000 verwendet. Ich vermute, Sie werden das gleiche falsche Lesen sehen wie ich.

    – James Sharp

    1. November 2012 um 7:45 Uhr

  • Danke, wusste ich nicht pread/pwrite. Ich bin eigentlich auf einer Windows-Plattform (ich hatte als VS-2010 getaggt, aber das wurde entfernt – ich hätte es wohl einfach in meiner Frage angeben sollen). Ich habe dies gefunden (stackoverflow.com/questions/766477/…), was darauf hindeutet, dass asynchrone E/A mit der CreateFile API könnte der richtige Weg sein. Etwas, wovor ich mich immer gescheut habe. Der Durchsatz von lseek + write ist mehr als ausreichend für mich … Mit dem Eingeständnis, dass ich dies auf einem System mit einer RAID-0-Konfiguration mit 2 Laufwerken getan habe.

    – Reisfeld

    1. November 2012 um 9:01 Uhr

  • @paddy, async I/O ist einer der wenigen Orte, an denen ich neidisch auf Windows bin. (Linux ist schlecht darin; so ziemlich die einzigen Fälle, die funktionieren, sind die, die Oracle braucht.) If lseek + write schnell genug für Sie ist, würde ich das auf jeden Fall tun – aber AIO ist eine Überlegung wert, wenn Sie an eine Wand stoßen.

    – James Sharp

    1. November 2012 um 19:47 Uhr

  • @JameySharp Ich werde einen separaten Thread haben, dessen einziger Zweck darin besteht, eine Arbeitswarteschlange zu verarbeiten, die alle Nachrichten enthält, die auf die Festplatte geschrieben werden sollen. In dieser Hinsicht bin ich mir nicht sicher, welchen Vorteil asynchrone E / A bieten würde. Ich denke, ich werde noch einige Tests machen (aber vielleicht, nachdem ich die Arbeit erledigt habe) – zumindest wird es mich zwingen, etwas Neues zu lernen.

    – Reisfeld

    1. November 2012 um 21:13 Uhr

1229420cookie-checkWas passiert hinter den Kulissen während der Festplatten-I/O?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy