Meine Ringpuffer-Implementierung in NOR-Flash

Hintergrund


Es gibt Automaten nach eigenem Design. Im Raspberry Pi und ein bisschen Umreifen auf einem separaten Brett. Ein MĂŒnzprĂŒfer, ein GeldscheinprĂŒfer, ein Bankautomat sind angeschlossen ... Ein selbst geschriebenes Programm verwaltet alles. Die gesamte Geschichte der Arbeit wird auf einem Flash-Laufwerk (MicroSD) in das Journal geschrieben, das dann ĂŒber das Internet (unter Verwendung eines USB-Modems) an den Server ĂŒbertragen und dort der Datenbank hinzugefĂŒgt wird. Verkaufsinformationen werden in 1s geladen, es gibt auch ein einfaches Webinterface zum Überwachen usw.


Das heißt, das Magazin ist von entscheidender Bedeutung - fĂŒr die Buchhaltung (dort Einnahmen, VerkĂ€ufe usw.), die Überwachung (alle Arten von Fehlern und andere UmstĂ€nde höherer Gewalt); das können Sie sagen, alle Informationen, die wir ĂŒber diese Maschine haben.


Das problem


Flash-Laufwerke zeigen sich als sehr unzuverlĂ€ssige GerĂ€te. Sie scheitern mit beneidenswerter RegelmĂ€ĂŸigkeit. Dies fĂŒhrt sowohl zu MaschinenausfĂ€llen als auch (falls das Journal aus irgendeinem Grund nicht online ĂŒbertragen werden konnte) zu Datenverlust.


Dies ist nicht die erste Erfahrung mit der Verwendung von Flash-Laufwerken, bevor es ein weiteres Projekt mit mehr als hundert GerÀten gab, in denen das Magazin auf USB-Flash-Laufwerken gespeichert war, gab es auch Probleme mit der ZuverlÀssigkeit, wenn die Anzahl der AusfÀlle pro Monat bei zehn lag. Wir haben verschiedene Flash-Laufwerke ausprobiert, darunter auch Marken-Laufwerke mit SLC-Speicher. Einige Modelle sind zuverlÀssiger als andere, aber das Ersetzen von Flash-Laufwerken hat das Problem nicht radikal gelöst.


Achtung! Longrid! Wenn Sie sich nicht fĂŒr das Warum interessieren, sondern nur fĂŒr das Wie, können Sie sofort zum Ende des Artikels gehen.


Lösung


Das erste, was mir in den Sinn kommt: MicroSD verlassen, zum Beispiel SSD einsetzen und davon booten. Theoretisch ist dies wahrscheinlich möglich, aber relativ teuer und nicht so zuverlĂ€ssig (ein USB-SATA-Adapter wird hinzugefĂŒgt; bei Budget-SSDs ist die Ausfallstatistik ebenfalls nicht gut).


USB HDD sieht auch nicht besonders attraktiv aus.


Aus diesem Grund haben wir diese Option gewĂ€hlt: Verlassen Sie den Download von MicroSD, verwenden Sie sie jedoch im schreibgeschĂŒtzten Modus, und speichern Sie das Betriebsprotokoll (und andere Informationen, die nur fĂŒr eine bestimmte Hardwarekomponente gelten - Seriennummer, Sensorkalibrierungen usw.) an einer anderen Stelle.


Das Thema Nur-Lese-FS fĂŒr Himbeeren wurde bereits eingehend untersucht. Ich werde nicht auf die Implementierungsdetails in diesem Artikel eingehen (aber wenn Interesse besteht, schreibe ich möglicherweise einen Mini-Artikel zu diesem Thema) . Der einzige Punkt, den ich erwĂ€hnen möchte: sowohl aus persönlicher Erfahrung als auch aus Bewertungen, die bereits einen Gewinn an ZuverlĂ€ssigkeit erzielt haben, ist. Ja, es ist unmöglich, AusfĂ€lle vollstĂ€ndig zu beseitigen, aber es ist durchaus möglich, deren HĂ€ufigkeit erheblich zu verringern. Ja, und die Karten werden vereinheitlicht, was den Austausch fĂŒr das Wartungspersonal erheblich vereinfacht.


Hardware


Es gab keinen Zweifel an der Wahl des Speichertyps - NOR Flash.
Argumente:


  • einfache Verbindung (am hĂ€ufigsten der SPI-Bus, dessen Erfahrung bereits vorhanden ist, sodass keine "Eisen" -Probleme zu erwarten sind);
  • lĂ€cherlicher Preis;
  • Standard-Betriebsprotokoll (die Implementierung ist bereits im Linux-Kernel, wenn Sie möchten, können Sie einen Dritten mitnehmen, der ebenfalls vorhanden ist, oder sogar Ihren eigenen schreiben, der Vorteil ist einfach);
  • ZuverlĂ€ssigkeit und Ressource:
    aus einem typischen Datenblatt: Daten werden 20 Jahre lang gespeichert, 100.000 Löschzyklen fĂŒr jeden Block;
    aus Quellen von Drittanbietern: Extrem niedrige BER, es wird postuliert, dass keine Fehlerkorrekturcodes erforderlich sind (in einigen Veröffentlichungen wird ECC fĂŒr NOR berĂŒcksichtigt, aber normalerweise ist MLC NOR dort gemeint, es passiert) .

Lassen Sie uns den Bedarf an Volumen und Ressourcen abschÀtzen.


Ich möchte die Garantie haben, Daten fĂŒr mehrere Tage zu speichern. Dies ist notwendig, damit bei Problemen mit der Verbindung die Verkaufshistorie nicht verloren geht. Wir konzentrieren uns auf 5 Tage, in diesem Zeitraum (auch unter BerĂŒcksichtigung von Wochenenden und Feiertagen) können wir das Problem lösen.


Wir tippen derzeit ungefĂ€hr 100 KB Zeitschriften pro Tag (3-4.000 DatensĂ€tze), aber allmĂ€hlich nimmt diese Zahl zu - die Detaillierung nimmt zu, neue Ereignisse werden hinzugefĂŒgt. Außerdem kommt es manchmal zu Bursts (einige Sensoren senden beispielsweise Spam-Mails mit False-Positives). Wir werden zehntausend DatensĂ€tze von 100 Bytes - Megabytes pro Tag berechnen.


Es werden insgesamt 5 MB saubere (gut komprimierbare) Daten ausgegeben. Sie auch (grobe SchÀtzung) 1 MB Service-Daten.


Das heißt, wir benötigen einen 8-MB-Mikrochip, wenn Sie keine Komprimierung verwenden, oder 4 MB, wenn Sie ihn verwenden. Ganz reelle Zahlen fĂŒr diesen Speichertyp.


Was die Ressource betrifft: Wenn wir vorhaben, dass der gesamte Speicher nicht mehr als einmal alle 5 Tage neu geschrieben wird, erhalten wir in 10 Dienstjahren weniger als tausend Überschreibzyklen.
Ich erinnere mich, der Hersteller verspricht hunderttausend.


Ein bisschen ĂŒber NOR vs NAND

Heute ist der NAND-Speicher natĂŒrlich viel beliebter, aber fĂŒr dieses Projekt wĂŒrde ich ihn nicht verwenden: NAND erfordert im Gegensatz zu NOR unbedingt die Verwendung von Fehlerkorrekturcodes, eine Tabelle mit fehlerhaften Blöcken usw. und die Beine von NAND-Chips normalerweise viel mehr.


Die Nachteile von NOR sind:


  • kleines Volumen (und dementsprechend hoher Preis pro Megabyte);
  • niedriger Wechselkurs (hauptsĂ€chlich aufgrund der Tatsache, dass eine serielle Schnittstelle verwendet wird, normalerweise SPI oder I2C);
  • langsames Löschen (je nach GrĂ¶ĂŸe des Blocks dauert es Bruchteile von einer Sekunde bis zu mehreren Sekunden).

Es scheint nichts kritisches fĂŒr uns zu sein, also mach weiter.


Wenn die Details interessant sind, wurde der at25df321a- Chip ausgewÀhlt (dies ist jedoch unerheblich, es gibt viele Analoga auf dem Markt, die mit der Steckerbelegung und dem Befehlssystem kompatibel sind; selbst wenn wir den Chip von einem anderen Hersteller und / oder einer anderen LautstÀrke verwenden möchten, funktioniert alles, ohne den Code zu Àndern) .


Ich verwende den im Linux-Kernel integrierten Treiber unter Raspberry. Dank der UnterstĂŒtzung fĂŒr GerĂ€tebaum-Overlays ist alles sehr einfach. Sie mĂŒssen das kompilierte Overlay in / boot / oversays einfĂŒgen und /boot/config.txt ein wenig Ă€ndern.


Beispiel dts Datei

Ehrlich gesagt bin ich mir nicht sicher, was fehlerfrei geschrieben wurde, aber es funktioniert.


/* * Device tree overlay for at25 at spi0.1 */ /dts-v1/; /plugin/; / { compatible = "brcm,bcm2835", "brcm,bcm2836", "brcm,bcm2708", "brcm,bcm2709"; /* disable spi-dev for spi0.1 */ fragment@0 { target = <&spi0>; __overlay__ { status = "okay"; spidev@1{ status = "disabled"; }; }; }; /* the spi config of the at25 */ fragment@1 { target = <&spi0>; __overlay__ { #address-cells = <1>; #size-cells = <0>; flash: m25p80@1 { compatible = "atmel,at25df321a"; reg = <1>; spi-max-frequency = <50000000>; /* default to false: m25p,fast-read ; */ }; }; }; __overrides__ { spimaxfrequency = <&flash>,"spi-max-frequency:0"; fastread = <&flash>,"m25p,fast-read?"; }; }; 

Und noch eine Zeile in config.txt
 dtoverlay=at25:spimaxfrequency=50000000 

Ich werde die Beschreibung des Verbindens des Chips mit dem Himbeer-Pi weglassen. Einerseits bin ich kein Experte fĂŒr Elektronik, andererseits ist auch fĂŒr mich alles trivial: Die Mikroschaltung hat nur 8 Beine, von denen wir Masse, Leistung, SPI (CS, SI, SO, SCK) benötigen; Stufen stimmen mit denen von Raspberry Pi ĂŒberein, es ist keine zusĂ€tzliche Bindung erforderlich - schließen Sie einfach die angegebenen 6 Kontakte an.


ErklÀrung des Problems


Wie ĂŒblich durchlĂ€uft die Formulierung des Problems mehrere Iterationen, es scheint mir, dass die Zeit fĂŒr die nĂ€chste gekommen ist. Also hören wir auf, fassen zusammen, was bereits geschrieben wurde, und klĂ€ren die im Schatten verbleibenden Details.


Daher haben wir beschlossen, dass das Protokoll in SPI NOR Flash gespeichert wird.


Was ist NOR Flash fĂŒr diejenigen, die es nicht wissen

Dies ist ein nichtflĂŒchtiger Speicher, mit dem Sie drei VorgĂ€nge ausfĂŒhren können:


  1. Lesen:
    Am hĂ€ufigsten wird gelesen: Wir ĂŒbergeben die Adresse und lesen so viele Bytes, wie wir benötigen.
  2. Rekord:
    Das Schreiben in NOR-Flash sieht wie ein gewöhnliches aus, hat jedoch eine Besonderheit: Sie können nur 1 in 0 Àndern, nicht jedoch umgekehrt. Wenn sich beispielsweise 0x55 in der Speicherzelle befindet, wird 0x05 nach dem Schreiben von 0x0f bereits dort gespeichert (siehe nachstehende Tabelle) .
  3. Löschen:
    NatĂŒrlich mĂŒssen wir auch in der Lage sein, den umgekehrten Vorgang auszufĂŒhren - Ă€ndern Sie 0 zu 1, weshalb der Löschvorgang vorhanden ist. Im Gegensatz zu den ersten beiden arbeitet es nicht in Bytes, sondern in Blöcken (der minimale Löschblock in der ausgewĂ€hlten Mikroschaltung betrĂ€gt 4 kb). Löschen zerstört den gesamten Block und dies ist die einzige Möglichkeit, 0 in 1 zu Ă€ndern. Wenn Sie mit Flash-Speichern arbeiten, mĂŒssen Sie daher hĂ€ufig Datenstrukturen am Rand des Löschblocks ausrichten.
    In NOR Flash aufnehmen:

BinÀre Daten
War01010101
Aufgenommen00001111
Ist geworden00000101

Das Journal selbst reprÀsentiert eine Folge von DatensÀtzen variabler LÀnge. Eine typische AufzeichnungslÀnge betrÀgt ungefÀhr 30 Bytes (obwohl manchmal Aufzeichnungen mit einer LÀnge von mehreren Kilobytes vorkommen). In diesem Fall arbeiten wir mit ihnen wie mit einer Reihe von Bytes. Wenn Sie jedoch interessiert sind, wird CBOR in den DatensÀtzen verwendet.


ZusĂ€tzlich zum Journal mĂŒssen einige aktualisierte „Tuning“ -Informationen gespeichert werden: eine bestimmte GerĂ€te-ID, Sensorkalibrierung, das Flag „GerĂ€t ist vorĂŒbergehend deaktiviert“ usw.
Bei diesen Informationen handelt es sich um eine Reihe von SchlĂŒsselwertdatensĂ€tzen, die ebenfalls in CBOR gespeichert sind. Wir haben nicht viel von diesen Informationen (maximal einige Kilobyte), sie werden nur selten aktualisiert.
In Zukunft werden wir es Kontext nennen.


Wenn Sie sich erinnern, wo dieser Artikel begonnen hat, ist es sehr wichtig, die ZuverlÀssigkeit der Datenspeicherung und, wenn möglich, den Dauerbetrieb auch bei HardwareausfÀllen / Datenkorruption sicherzustellen.


Welche Problemquellen können berĂŒcksichtigt werden?


  • WĂ€hrend Schreib- / LöschvorgĂ€ngen ausschalten. Dies ist aus der Kategorie "gegen Schrott kein Empfang".
    Informationen aus der Diskussion zum Stack-Austausch: Wenn die Stromversorgung ausgeschaltet wird, wĂ€hrend mit Flash gearbeitet wird, fĂŒhrt das Löschen (Einstellung auf 1) und Schreiben (Einstellung auf 0) zu undefiniertem Verhalten: Daten können geschrieben, teilweise geschrieben werden (dh wir haben 10 Byte / 80 Bit ĂŒbertragen) , und nur 45 Bits können aufgezeichnet werden), ist es auch möglich, dass sich einige der Bits im "Zwischen" -Zustand befinden (Lesen kann entweder 0 oder 1 ergeben);
  • Fehler im Flash-Speicher selbst.
    BER ist zwar sehr niedrig, kann aber nicht gleich Null sein.
  • Busfehler
    Die ĂŒber SPI ĂŒbertragenen Daten sind in keiner Weise geschĂŒtzt, es kann durchaus zu Einzelbitfehlern oder Synchronisationsfehlern kommen - Verlust oder EinfĂŒgung von Bits (was zu massiven Datenverzerrungen fĂŒhrt);
  • Sonstige Fehler / AusfĂ€lle
    Fehler im Code, Himbeer "Pannen", Alien Intervention ...

Ich habe Anforderungen formuliert, deren ErfĂŒllung meiner Meinung nach notwendig ist, um die ZuverlĂ€ssigkeit zu gewĂ€hrleisten:


  • Aufzeichnungen sollten sofort in den Flash-Speicher geschrieben werden, anstehende Aufzeichnungen werden nicht berĂŒcksichtigt. - Wenn ein Fehler auftritt, sollte er so schnell wie möglich erkannt und verarbeitet werden. - Das System sollte nach Möglichkeit eine Fehlerbehebung durchfĂŒhren.
    (Ein Beispiel aus dem Leben "wie es nicht sein sollte", das sich meiner Meinung nach alle getroffen haben: Nach einem Notfall-Neustart ist das Dateisystem "kaputt" und das Betriebssystem bootet nicht.)

Ideen, AnsÀtze, Gedanken


Als ich ĂŒber diese Aufgabe nachdachte, schoss mir ein paar Ideen durch den Kopf, zum Beispiel:


  • Verwenden Sie die Datenkomprimierung
  • Verwenden Sie knifflige Datenstrukturen, und speichern Sie beispielsweise Datensatzheader getrennt von den DatensĂ€tzen, damit Sie den Rest problemlos lesen können, wenn in einem Datensatz ein Fehler auftritt.
  • Verwenden Sie Bitfelder, um die VollstĂ€ndigkeit der Aufzeichnung beim Ausschalten zu kontrollieren.
  • Speichern Sie PrĂŒfsummen fĂŒr alles und alles;
  • Verwenden Sie eine Art fehlerkorrigierende Codierung.

Einige dieser Ideen wurden verwendet, andere abgelehnt. Gehen wir in Ordnung.


Datenkomprimierung


Die Ereignisse, die wir im Tagebuch selbst aufzeichnen, sind ziemlich gleich und wiederholbar ("warf eine MĂŒnze von 5 Rubel", "klickte auf die SchaltflĂ€che" Zustellung Ă€ndern ", ...). Daher sollte die Komprimierung sehr effektiv sein.


Der Overhead fĂŒr die Komprimierung ist unbedeutend (der Prozessor, den wir haben, ist ziemlich leistungsfĂ€hig, selbst auf dem ersten Pi gab es einen Kern mit einer Frequenz von 700 MHz, auf aktuellen Modellen gab es mehrere Kerne mit einer Frequenz von mehr als einem Gigahertz), die Geschwindigkeit des Austauschs mit dem Speicher ist gering (mehrere Megabyte pro Sekunde), die AufzeichnungsgrĂ¶ĂŸe ist gering. Wenn sich die Komprimierung auf die Leistung auswirkt, ist dies im Allgemeinen nur positiv (absolut unkritisch, nur mit Angabe) . Außerdem haben wir kein echtes eingebettetes, sondern gewöhnliches Linux - daher sollte die Implementierung keinen großen Aufwand erfordern (verknĂŒpfen Sie einfach die Bibliothek und verwenden Sie mehrere Funktionen daraus).


Ein StĂŒck des Protokolls wurde aus dem ArbeitsgerĂ€t entnommen (1,7 MB, 70.000 DatensĂ€tze) und zunĂ€chst mit den auf dem Computer verfĂŒgbaren gzip, lz4, lzop, bzip2, xz, zstd auf Komprimierbarkeit ĂŒberprĂŒft.


  • gzip, xz, zstd zeigten Ă€hnliche Ergebnisse (40 KB).
    Ich war ĂŒberrascht, dass sich das modische xz hier auf der gzip- oder zstd-Ebene zeigte.
  • lzip mit Standardeinstellungen ergab ein etwas schlechteres Ergebnis;
  • lz4 und lzop zeigten keine sehr guten Ergebnisse (150Kb);
  • bzip2 zeigte ĂŒberraschend gute Ergebnisse (18Kb).

Die Daten sind also sehr gut komprimiert.
Wenn wir also keine schwerwiegenden Fehler finden, sollte es zu einer Komprimierung kommen! Nur weil mehr Daten auf dasselbe Flash-Laufwerk passen.


Denken wir ĂŒber die MĂ€ngel nach.


Das erste Problem: Wir haben uns bereits darauf geeinigt, dass jede Aufnahme sofort aufblitzen soll. Normalerweise sammelt der Archivierer Daten aus dem Eingabestream, bis er entscheidet, dass es Zeit ist, in die Ausgabe zu schreiben. Wir mĂŒssen sofort einen komprimierten Datenblock abrufen und nichtflĂŒchtig speichern.


Ich sehe drei Möglichkeiten:


  1. Komprimieren Sie jeden Eintrag mithilfe der Wörterbuchkomprimierung anstelle der oben beschriebenen Algorithmen.
    Es ist eine funktionierende Option, aber ich mag es nicht. Um eine mehr oder weniger gute Komprimierung zu gewĂ€hrleisten, sollte das Wörterbuch fĂŒr bestimmte Daten „geschĂ€rft“ werden. Jede Änderung fĂŒhrt dazu, dass die Komprimierungsstufe katastrophal abfĂ€llt. Ja, das Problem wird gelöst, indem eine neue Version des Wörterbuchs erstellt wird. Dies bereitet jedoch Kopfzerbrechen. Wir mĂŒssen alle Versionen des Wörterbuchs speichern. In jedem Eintrag mĂŒssen wir angeben, mit welcher Version des Wörterbuchs es komprimiert wurde ...
  2. Komprimieren Sie jeden Eintrag mit "klassischen" Algorithmen, jedoch unabhÀngig von den anderen.
    Die betrachteten Komprimierungsalgorithmen sind nicht fĂŒr die Arbeit mit DatensĂ€tzen dieser GrĂ¶ĂŸe (zehn Bytes) ausgelegt. Der Komprimierungskoeffizient ist eindeutig kleiner als 1 (dh eine Erhöhung der Datenmenge anstelle der Komprimierung).
  3. FLUSH nach jeder Aufnahme.
    Viele Komprimierungsbibliotheken unterstĂŒtzen FLUSH. Dies ist ein Befehl (oder ein Parameter fĂŒr die Komprimierungsprozedur), bei dessen Empfang der Archivierer einen komprimierten Stream generiert, sodass auf seiner Basis alle unkomprimierten Daten wiederhergestellt werden können, die bereits empfangen wurden. Solch ein Analogon der sync in Dateisystemen oder Festschreibung in SQL.
    Was wichtig ist, nachfolgende KomprimierungsvorgÀnge können das akkumulierte Wörterbuch verwenden und das KomprimierungsverhÀltnis wird nicht so stark leiden wie in der vorherigen Version.

Ich denke, es ist offensichtlich, dass ich die dritte Option gewÀhlt habe. Lassen Sie uns genauer darauf eingehen.


Es gab einen großartigen Artikel ĂŒber FLUSH in zlib.


Ich habe einen motivierten Test basierend auf dem Artikel durchgefĂŒhrt und 70.000 TagebucheintrĂ€ge von einem realen GerĂ€t mit einer SeitengrĂ¶ĂŸe von 60 KB entnommen (wir kehren zur SeitengrĂ¶ĂŸe zurĂŒck) :


AusgangsdatenGzip -9 Komprimierung (ohne FLUSH)zlib mit Z_PARTIAL_FLUSHzlib mit Z_SYNC_FLUSH
Volumen, Kb169240352604

Auf den ersten Blick ist der von FLUSH eingefĂŒhrte Preis ĂŒbermĂ€ĂŸig hoch, aber in Wirklichkeit haben wir eine schlechte Wahl - entweder ĂŒberhaupt nicht zu komprimieren oder mit FLUSH (und sehr effizient) zu komprimieren. Vergessen Sie nicht, dass wir 70.000 DatensĂ€tze haben. Die durch Z_PARTIAL_FLUSH eingefĂŒhrte Redundanz betrĂ€gt nur 4-5 Bytes pro Datensatz. Und das KompressionsverhĂ€ltnis betrug fast 5: 1, was mehr als ein hervorragendes Ergebnis ist.


Es mag unerwartet erscheinen, aber tatsĂ€chlich ist Z_SYNC_FLUSH eine effizientere Möglichkeit, FLUSH auszufĂŒhren

Bei Verwendung von Z_SYNC_FLUSH sind die letzten 4 Bytes jedes Datensatzes immer 0x00, 0x00, 0xff, 0xff. Und wenn wir sie kennen, können wir sie nicht speichern, sodass die GesamtgrĂ¶ĂŸe nur 324 KB betrĂ€gt.


Der Artikel, auf den ich mich beziehe, hat eine ErklÀrung:


Ein neuer Block vom Typ 0 mit leerem Inhalt wird angehÀngt.

Ein Block vom Typ 0 mit leerem Inhalt besteht aus:
  • der Drei-Bit-Block-Header;
  • 0 bis 7 Bits gleich Null, um eine Byte-Ausrichtung zu erreichen;
  • die Vier-Byte-Sequenz 00 00 FF FF.

Wie Sie sehen können, kommen im letzten Block vor diesen 4 Bytes 3 bis 10 Nullbits. Die Praxis hat jedoch gezeigt, dass Null-Bits tatsÀchlich mindestens 10 sind.


Es stellt sich heraus, dass solche kurzen Datenblöcke normalerweise (immer?) Mit einem Block vom Typ 1 (fester Block) codiert werden, der notwendigerweise mit 7 Nullbits endet, sodass wir 10-17 garantierte Nullbits erhalten (und der Rest wird mit einer Wahrscheinlichkeit von etwa 50% Null sein).


Bei Testdaten gibt es in 100% der FĂ€lle vor 0x00, 0x00, 0xff, 0xff ein Null-Byte und mehr als im dritten Fall zwei Null-Bytes (möglicherweise ist dies die Tatsache, dass ich binĂ€res CBOR verwende, und wenn ich Text-CBOR verwende JSON wĂŒrde mit grĂ¶ĂŸerer Wahrscheinlichkeit Blöcke vom Typ 2 erfĂŒllen - dynamische Blöcke bzw. Blöcke wĂŒrden ohne zusĂ€tzliche Null-Bytes vor 0x00, 0x00, 0xff, 0xff auftreten .


Insgesamt können die verfĂŒgbaren Testdaten in weniger als 250 KB komprimierte Daten passen.


Sie können etwas mehr sparen, indem Sie Bits jonglieren: Jetzt ignorieren wir das Vorhandensein mehrerer Null-Bits am Ende des Blocks, einige Bits am Anfang des Blocks Àndern sich auch nicht ...
Aber dann habe ich eine willensstarke Entscheidung getroffen, aufzuhören, sonst können Sie in einem solchen Tempo die Entwicklung Ihres Archivierers erreichen.


Insgesamt habe ich 3-4 Bytes pro Datensatz aus meinen Testdaten erhalten, das KompressionsverhÀltnis betrug mehr als 6: 1. Ehrlich gesagt habe ich nicht mit einem solchen Ergebnis gerechnet. Meiner Meinung nach ist alles, was besser als 2: 1 ist, bereits ein Ergebnis, das die Verwendung von Komprimierung rechtfertigt.


Alles ist in Ordnung, aber zlib (deflate) ist immer noch archaisch wohlverdienter und etwas altmodischer Kompressionsalgorithmus. Die bloße Tatsache, dass die letzten 32 KB aus dem unkomprimierten Datenstrom als Wörterbuch verwendet werden, sieht heute seltsam aus (dh, wenn ein Datenblock dem Datenstrom mit 40 KB sehr Ă€hnlich ist, wird er erneut archiviert, jedoch nicht siehe den letzten Eintrag). In modisch Die GrĂ¶ĂŸe eines modernen Archivierungswörterbuchs wird hĂ€ufig eher in Megabyte als in Kilobyte gemessen.


Also setzen wir unsere Mini-Studie der Archivare fort.


Als nĂ€chstes wurde bzip2 getestet (erinnere dich, ohne FLUSH zeigte es ein fantastisches KompressionsverhĂ€ltnis von fast 100: 1). Leider zeigte sich bei FLUSH sehr schlecht, dass die GrĂ¶ĂŸe der komprimierten Daten grĂ¶ĂŸer war als die der unkomprimierten.


Meine Vermutungen ĂŒber die GrĂŒnde fĂŒr das Scheitern

Libbz2 bietet nur eine Flush-Option, die das Wörterbuch zu löschen scheint (Àhnlich wie Z_FULL_FLUSH in zlib). Es gibt keinen Grund, von einer effizienten Komprimierung zu sprechen.


Und zstd wurde als letztes getestet. AbhÀngig von den Parametern wird es entweder auf der Ebene von gzip komprimiert, aber viel schneller, oder gzip ist besser.


Leider hat er sich mit FLUSH als "nicht sehr" erwiesen: Die GrĂ¶ĂŸe der komprimierten Daten betrug etwa 700 KB.


Ich habe eine Frage auf der Projektseite in Github gestellt und eine Antwort erhalten, dass es sich lohnt, fĂŒr jeden Block komprimierter Daten mit bis zu 10 Byte Servicedaten zu rechnen, was nahe an den Ergebnissen liegt. Das Abfangen von deflate funktioniert nicht.


Ich habe mich dazu entschlossen, in Experimenten mit Archiven damit aufzuhören (ich erinnere Sie daran, dass sich xz, lzip, lzo, lz4 im Teststadium nicht ohne FLUSH zeigten, aber exotischere Kompressionsalgorithmen nicht in Betracht zogen).


Wir kehren zu den Problemen der Archivierung zurĂŒck.


Das zweite (wie sie sagen, in der Reihenfolge, aber nicht im Wert) Problem - die komprimierten Daten sind ein einzelner Stream, in dem stÀndig an vorherige Abschnitte gesendet wird. Wenn also ein Abschnitt der komprimierten Daten beschÀdigt wird, verlieren wir nicht nur den Block der nicht komprimierten Daten, sondern auch alle nachfolgenden.


Es gibt einen Lösungsansatz fĂŒr dieses Problem:


  1. Verhindern Sie das Auftreten eines Problems - fĂŒgen Sie den komprimierten Daten Redundanz hinzu, um Fehler zu identifizieren und zu korrigieren. wir werden spĂ€ter darĂŒber sprechen;
  2. Minimieren Sie die Konsequenzen im Falle eines Problems
    Wir haben bereits frĂŒher gesagt, dass es möglich ist, jeden Datenblock einzeln zu komprimieren, und dass das Problem von selbst verschwindet (Datenkorruption eines Blocks fĂŒhrt nur zum Verlust von Daten dieses Blocks). Dies ist jedoch ein extremer Fall, in dem die Datenkomprimierung ineffizient ist. Das gegenteilige Extrem: Verwenden Sie alle 4 MB unseres Mikrokreislaufs als ein einziges Archiv, wodurch wir eine hervorragende Komprimierung erzielen, aber katastrophale Folgen im Falle einer Datenkorruption haben.
    Ja, ein Kompromiss hinsichtlich der ZuverlĂ€ssigkeit ist erforderlich. Wir mĂŒssen jedoch bedenken, dass wir ein Datenspeicherformat fĂŒr nichtflĂŒchtigen Speicher mit einer extrem niedrigen BER und einer angegebenen Datenspeicherdauer von 20 Jahren entwickeln.

Im Verlauf der Experimente stellte ich fest, dass bei komprimierten Datenblöcken mit einer GrĂ¶ĂŸe von weniger als 10 KB mehr oder weniger spĂŒrbare Verluste im Komprimierungsgrad auftreten.
Es wurde bereits erwÀhnt, dass der verwendete Speicher eine Seitenorganisation hat. Ich sehe keinen Grund, warum Sie die Korrespondenz "eine Seite - ein Block komprimierter Daten" nicht verwenden sollten.


Das heißt, die minimale angemessene SeitengrĂ¶ĂŸe betrĂ€gt 16 KB (mit einer Spanne fĂŒr Serviceinformationen). Eine so kleine SeitengrĂ¶ĂŸe fĂŒhrt jedoch zu erheblichen EinschrĂ€nkungen der maximalen AufzeichnungsgrĂ¶ĂŸe.


Obwohl ich immer noch keine komprimierten Aufzeichnungen von mehr Kilobyte-Einheiten erwarte, habe ich mich fĂŒr die Verwendung von 32 KB-Seiten (insgesamt 128 Seiten pro Chip) entschieden.


Zusammenfassung:


  • Wir speichern Daten, die mit zlib (deflate) komprimiert wurden.
  • Setzen Sie fĂŒr jeden Datensatz Z_SYNC_FLUSH;
  • FĂŒr jeden komprimierten Datensatz werden die letzten Bytes abgeschnitten (z. B. 0x00, 0x00, 0xff, 0xff) . Geben Sie im Header an, wie viele Bytes wir schneiden.
  • Wir speichern Daten auf 32-KB-Seiten. Innerhalb der Seite befindet sich ein einzelner Strom komprimierter Daten. Auf jeder Seite wird die Komprimierung erneut gestartet.

Und bevor ich mit der Komprimierung fertig bin, möchte ich darauf hinweisen, dass wir nur wenige Bytes an Schreibdaten erhalten. Daher ist es Ă€ußerst wichtig, die Serviceinformationen nicht zu vergrĂ¶ĂŸern, da jedes Byte gezĂ€hlt wird.


Speichern von Datenköpfen


Da wir DatensĂ€tze mit variabler LĂ€nge haben, mĂŒssen wir irgendwie den Ort / die Grenzen der DatensĂ€tze bestimmen.


Ich kenne drei AnsÀtze:


  1. Alle DatensÀtze werden in einem fortlaufenden Stream gespeichert. Zuerst wird der Datensatzkopf mit der LÀnge und dann der Datensatz selbst angezeigt.
    In dieser AusfĂŒhrungsform können sowohl die Header als auch die Daten eine variable LĂ€nge haben.
    TatsĂ€chlich erhalten wir eine einfach verknĂŒpfte Liste, die stĂ€ndig verwendet wird.
  2. Header und DatensÀtze selbst werden in separaten Streams gespeichert.
    Mit Headern konstanter LÀnge stellen wir sicher, dass SchÀden an einem Header den Rest nicht beeintrÀchtigen.
    Ein Àhnlicher Ansatz wird beispielsweise in vielen Dateisystemen verwendet.
  3. DatensÀtze werden in einem kontinuierlichen Datenstrom gespeichert, die Grenze des Datensatzes wird durch einen Marker (Symbol / Zeichenfolge, die in Datenblöcken verboten ist / ist) festgelegt. Wenn ein Marker im Datensatz gefunden wird, ersetzen wir ihn durch eine bestimmte Sequenz (Escape-Zeichen).
    Ein Àhnlicher Ansatz wird beispielsweise im PPP-Protokoll verwendet.

Ich werde es veranschaulichen.


Variante 1:
Variante 1
Hier ist alles ganz einfach: Wenn wir die LĂ€nge des Datensatzes kennen, können wir die Adresse des nĂ€chsten Headers berechnen. Also bewegen wir uns durch die Header, bis wir auf eine Region treffen, die mit 0xff (freie Region) oder dem Ende der Seite gefĂŒllt ist.


Option 2:
Option 2
Aufgrund der variablen LĂ€nge des Datensatzes können wir nicht im Voraus sagen, wie viele DatensĂ€tze (und damit die Überschriften) pro Seite wir benötigen. Sie können die Überschriften und die Daten selbst auf verschiedene Seiten verteilen, aber ich bevorzuge einen anderen Ansatz: Wir platzieren die Überschriften und die Daten auf derselben Seite, die Überschriften (mit konstanter GrĂ¶ĂŸe) befinden sich jedoch am Anfang der Seite und die Daten (mit variabler LĂ€nge) am Ende. Sobald sie sich "treffen" (es ist nicht genĂŒgend Speicherplatz fĂŒr einen neuen Datensatz vorhanden) - betrachten wir diese Seite als voll.


Option 3:
Option 3
Es ist nicht erforderlich, die LĂ€nge oder andere Informationen ĂŒber den Speicherort der Daten im Header zu speichern. Es gibt genĂŒgend Markierungen, die die Grenzen der DatensĂ€tze angeben. Die Daten mĂŒssen jedoch wĂ€hrend des Schreibens / Lesens verarbeitet werden.
Als Markierung wĂŒrde ich 0xff verwenden (welches die Seite nach dem Löschen fĂŒllt), so dass der freie Bereich definitiv nicht als Daten behandelt wird.


Vergleichstabelle:


Variante 1Option 2Option 3
Fehlertoleranz-++
Kompaktheit+-+
KomplexitÀt der Implementierung*****

Option 1 hat einen schwerwiegenden Fehler: Wenn einer der Header beschÀdigt ist, wird unsere gesamte nachfolgende Kette zerstört. Mit anderen Optionen können Sie einen Teil der Daten auch bei massivem Schaden wiederherstellen.
Wir möchten jedoch daran erinnern, dass wir uns entschlossen haben, die Daten in komprimierter Form zu speichern, und daher alle Daten auf der Seite nach dem "kaputten" Datensatz verlieren, sodass wir dies nicht berĂŒcksichtigen, obwohl die Tabelle negativ ist.


Kompaktheit:


  • In der ersten Version mĂŒssen wir nur die LĂ€nge im Header speichern. Wenn Ganzzahlen mit variabler LĂ€nge verwendet werden, können wir in den meisten FĂ€llen mit einem Byte arbeiten.
  • In der zweiten Option mĂŒssen wir die Startadresse und die StartlĂ€nge speichern. Der Datensatz sollte eine konstante GrĂ¶ĂŸe haben. Ich schĂ€tze 4 Bytes pro Datensatz (zwei Bytes pro Versatz und zwei Bytes pro LĂ€nge).
  • Bei der dritten Option ist nur ein Zeichen ausreichend, um den Beginn der Aufzeichnung anzuzeigen, und die Aufzeichnung selbst wĂ€chst aufgrund der ÜberprĂŒfung um 1 bis 2%. Im Allgemeinen ungefĂ€hre ParitĂ€t mit der ersten Option.

( ). , .


, - - . , , — , , ...


: , , .. , , , — , .


: " — " - .



, , :
.
, erase 1, 1 0, . " " 1, " " — 0.


flash:


  1. “ ”;
  2. ;
  3. “ ”;
  4. ;
  5. “ ”.

, “ ”, 4 .


“1111” — “1000” — ; , .


, , , , , ( ) .


: .



( ) , . , , .


, , ( , , — ) .


, , , — .


— CRC. , 100% , — 2−n. , , : , . — .


: 1 , 2 ( narod.ru, ) .


, CRC — . , .


, .


:
10−3, :


,,
10100001000
1149991003
12≈019971997
14≈039903990
100995509955
101399901029
102≈019791979
104≈039543954
100006323050632305
1000124703682838
1000210735745
10004≈014691469

, — — .


, : , , . , .


, , 32 ( 64 -) .


, , , - 32- (16 , 0.01%; 24 , , ).


: , 4 ? ? , , .


, CRC-32C.
6 22 (, c), 4 655 ( ), 2 .


CRC.


crc-32c — , CRC .


, , , , .


, , : ?


"" :


  • — ( /, , ..);
  • deflate zlib "" , , , ( , zlib ).

"" :


  • CRC "" , - ( , , , "" );
  • , , .

.


: CRC-32C, , flash ( ).



, , , , ( ) .


, .
, - , RAID-6 .
, , , .


, . ?


  1. ( - , Raspberry, ...)
    , ;
  2. ( - flash- , )
    , ;
  3. ;

  4. .

( ) . , - .


: , , , ( , ).


Andere


, ( ) , , .


  • ""
    - , .., , .
    , , ;
  • .
    — !
    Magic Number (), ( , ) ;
  • ( ) , 1 ;
  • .

- . .



Byte order


, , big-endian (network byte order), 0x1234 0x12, 0x34.



- .


32, , 1/4 ( 4 128 ).


( ).


( ), 0 ( 0, — 32, — 64 ..)


(ring buffer), 0, 1, ..., , .



Seite
4- , (CRC-32C), ", , ".


( -) :


  • Magic Number ( — )
    0xed00 ⊕ ;
  • " " ( ).

( deflate). ( ), . ( ).


Z_SYNC_FLUSH, 4 0x00, 0x00, 0xff, 0xff, , , .
( 4, 5 6 ) -.


1, 2 3 , :


  • (T), : 0 — , 1 — ;
  • (S) 1 7 , "", ;
  • (L).

S:


S,,
015 ( 00 00 00 ff ff )
1016 ( 00 00 00 00 ff ff )
11024 ( 00 00 ff ff )
111025 ( 00 00 00 ff ff )
1111026 ( 00 00 00 00 ff ff )
111110034 ( 00 00 ff ff )
111110135 ( 00 00 00 ff ff )
111111036 ( 00 00 00 00 ff ff )

, , :

T, — S, L ( ), — , — , -.


, ( 63+5 ) .


CRC-32C, (init) .


CRC "", (- ) : CRC(init,A||B)=CRC(CRC(init,A),B).
CRC .


.


, 0x00 0xff ( 0xff, ; 0x00 ).



-


.
— - .


( , Linux NOR Flash, )


-


.
.


— .



( ) 1.
( UUID ).


, - .



8 ( + CRC), Magic Number CRC .
"" , , .
, CRC, "". — . — , "" .
, , "" .
zlib ( ).


, , , .



, Z_SYNC_FLUSH., .
( CRC) — (. ).
CRC. — .



( ). — , .
erase. 0xff. - — , ..
, , — ( ).



, - ( , JSON, MessagePack, CBOR, , protobuf) NOR Flash.


, "" SLC NOR Flash.


BER, NAND MLC NOR ( ? ) .


, , FTL: USB flash, SD, MicroSD, etc ( 512 , — "" ) .


128 (16) 1 (128). , , , ( , NOR Flash ) .


- , — , , github.


Fazit


, .


, : - , , . , () - .


, ? , . , , . - .


? , , . .


, , " ".


, () , , "" (, , ; ). ( — ) .


, .


Literatur


, .


, , , :


  1. infgen zlib. deflate/zlib/gzip. deflate ( gzip) — .

Source: https://habr.com/ru/post/de479044/


All Articles