Vorgeschichte
Es gibt eigene Verkaufsautomaten. Im Inneren befindet sich ein Raspberry Pi und etwas Verdrahtung auf einer separaten Platine. Angeschlossen sind ein MĂŒnz- und ein GeldscheineinzahlungsgerĂ€t sowie ein Bankterminal⊠Alles wird von einem selbstgeschriebenen Programm gesteuert. Die gesamte Historie der BetriebsablĂ€ufe wird auf einem Flash-Laufwerk (MicroSD) in ein Protokoll geschrieben, das dann ĂŒber das Internet (mithilfe eines USB-Modems) auf einen Server ĂŒbermittelt wird, wo es in einer Datenbank gespeichert wird. Informationen ĂŒber VerkĂ€ufe werden in 1C hochgeladen; es gibt auch eine einfache Web-OberflĂ€che zur Ăberwachung usw.
Das bedeutet, dass das Protokoll lebensnotwendig ist â fĂŒr die Buchhaltung (Umsatz, VerkĂ€ufe usw.), die Ăberwachung (sĂ€mtliche AusfĂ€lle und andere auĂergewöhnliche UmstĂ€nde); sozusagen sind das alle Informationen, die wir ĂŒber diesen Automaten haben.
Problem
Die Flash-Laufwerke erweisen sich als sehr ung zuverlĂ€ssige GerĂ€te. Sie gehen mit beachtlicher RegelmĂ€Ăigkeit kaputt. Dies fĂŒhrt sowohl zu Ausfallzeiten der Automaten als auch (wenn das Protokoll aus irgendwelchen GrĂŒnden nicht online ĂŒbermittelt werden konnte) zu Datenverlusten.
Das ist bereits nicht die erste Erfahrung mit Flash-Laufwerken; zuvor gab es ein anderes Projekt mit mehr als hundert GerÀten, bei dem das Protokoll auf USB-Flash-Laufwerken gespeichert wurde. Auch dort gab es Probleme mit der ZuverlÀssigkeit, manchmal war die Anzahl der AusfÀlle pro Monat in den Dutzenden. Wir haben verschiedene Flash-Laufwerke getestet, darunter auch Markenprodukte mit SLC-Speicher; ja, einige Modelle sind zuverlÀssiger als andere, aber der Austausch der Flash-Laufwerke hat das Problem nicht grundlegend gelöst.
Achtung! Long-Read! Wenn Sie nicht interessiert sind an "warum", sondern nur an "wie", können Sie gleich zum Artikel.
Lösung
Das Erste, was mir in den Sinn kommt: auf MicroSD verzichten, beispielsweise ein SSD einsetzen und von dort booten. Theoretisch ist das vielleicht möglich, aber relativ teuer und nicht so zuverlĂ€ssig (es muss ein USB-SATA-Adapter hinzugefĂŒgt werden; die Statistiken ĂŒber AusfĂ€lle bei budgetfreundlichen SSDs sind ebenfalls nicht erfreulich).
USB HDD sieht auch nicht besonders attraktiv aus.
Deshalb sind wir zu folgender Lösung gekommen: Den Bootvorgang von MicroSD beizubehalten, sie aber im Nur-Lese-Modus zu verwenden und das Betriebsprotokoll (und andere spezifische Informationen fĂŒr das jeweilige GerĂ€t â Seriennummer, Kalibrierung der Sensoren usw.) woanders zu speichern.
Das Thema read-only FS fĂŒr den Raspberry Pi wurde bereits umfassend untersucht, ich werde in diesem Artikel nicht auf die Einzelheiten der Umsetzung eingehen. (aber wenn Interesse besteht â vielleicht schreibe ich einen Mini-Artikel zu diesem Thema). Der einzige Punkt, den ich anmerken möchte: sowohl aus persönlicher Erfahrung als auch aus den RĂŒckmeldungen der bereits implementierenden Nutzer ist ein Gewinn an ZuverlĂ€ssigkeit vorhanden. Ja, es ist unmöglich, vollstĂ€ndig von AusfĂ€llen abzusehen, aber die HĂ€ufigkeit erheblich zu reduzieren â das ist durchaus realistisch. Zudem werden die Karten vereinheitlicht, was den Austausch fĂŒr das Servicepersonal erheblich vereinfacht.
Hardware
Bei der Wahl des Speichertyps gab es keine besonderen Zweifel â NOR Flash.
Argumente:
- einfache Anbindung (meist ĂŒber SPI-Bus, von dem wir bereits Erfahrungen haben, sodass keine âtechnischenâ Probleme zu erwarten sind);
- lÀcherlicher Preis;
- Standardarbeitsprotokoll (eine Implementierung ist bereits im Linux-Kernel vorhanden, bei Bedarf kann man auch Drittanbieterlösungen nutzen, die ebenfalls vorhanden sind, oder sogar eine eigene schreiben, da alles recht einfach ist);
- ZuverlÀssigkeit und Lebensdauer:
aus einem typischen Datenblatt: die Daten werden 20 Jahre lang aufbewahrt, 100000 Löschzyklen fĂŒr jeden Block;
aus externen Quellen: extrem niedriger BER, es wird postuliert, dass keine Fehlerkorrekturcodes erforderlich sind. (in einigen Arbeiten wird ECC fĂŒr NOR betrachtet, aber normalerweise ist hier von MLC NOR die Rede, was ebenfalls vorkommen kann).
Lassen Sie uns die Anforderungen an Volumen und Lebensdauer abschÀtzen.
Wir wĂŒnschen uns, dass die Daten fĂŒr mehrere Tage sicher gespeichert werden. Dies ist wichtig, damit im Falle von Kommunikationsproblemen die Verkaufsstatistik nicht verloren geht. Wir orientieren uns an 5 Tagen, in diesem Zeitraum (sogar unter BerĂŒcksichtigung von Wochenenden und Feiertagen) kann das Problem gelöst werden.
Aktuell sammeln wir tĂ€glich etwa 100KB Protokolle (3-4 Tausend EintrĂ€ge), aber diese Zahl wĂ€chst allmĂ€hlich â die Detaillierung nimmt zu, neue Ereignisse werden hinzugefĂŒgt. AuĂerdem gibt es manchmal Spitzen (ein gewisser Sensor fĂ€ngt an, mit falschen Auslösungen zu spammen, zum Beispiel). Wir rechnen mit 10 Tausend EintrĂ€gen zu je 100 Byte â ein Megabyte pro Tag.
Insgesamt ergibt das 5MB an reinen (gut komprimierbaren) Daten. Dazu kommen noch (grobe SchÀtzung) 1MB an Steuerdaten.
Das heiĂt, wir benötigen einen Chip mit 8MB, wenn wir keine Komprimierung verwenden, oder 4MB, wenn wir es tun. Völlig realistische Zahlen fĂŒr diesen Speichertyp.
Was die Lebensdauer betrifft: Wenn wir planen, dass der Speicher insgesamt nicht hĂ€ufiger als alle 5 Tage ĂŒberschrieben wird, erhalten wir bei 10 Jahren Betriebsdauer weniger als tausend Wiederbeschreibungszyklen.
Ich erinnere daran, dass der Hersteller zehntausend verspricht.
Ein wenig zu NOR vs NAND
Heute ist natĂŒrlich NAND-Speicher viel beliebter, aber fĂŒr dieses Projekt wĂŒrde ich ihn nicht verwenden: NAND erfordert im Gegensatz zu NOR unbedingt die Verwendung von Fehlerkorrekturcodes, fehlerhaften Blocktabellen usw. AuĂerdem haben NAND-Chips normalerweise viel mehr Pins.
Als Nachteile von NOR kann man angeben:
- geringe KapazitÀt (und entsprechend hoher Preis pro Megabyte);
- nicht hohe Ăbertragungsgeschwindigkeit (hauptsĂ€chlich aufgrund des verwendeten seriellen Interfaces, ĂŒblicherweise SPI oder I2C);
- langsame LöschvorgĂ€nge (je nach BlockgröĂe dauert es von Bruchteilen einer Sekunde bis zu mehreren Sekunden).
Nichts Kritisches scheint es fĂŒr uns zu sein, also machen wir weiter.
Wenn Sie an Einzelheiten interessiert sind, wurde der Chip ausgewĂ€hlt (das ist jedoch nicht entscheidend, auf dem Markt gibt es viele Analoga, die hinsichtlich Pinbelegung und Befehlssatz kompatibel sind; selbst wenn wir einen Chip eines anderen Herstellers und/oder mit anderer KapazitĂ€t verwenden möchten, wird alles ohne Ănderung des Codes funktionieren).
Ich verwende den im Linux-Kernel eingebauten Treiber, auf Raspberry ist es dank der UnterstĂŒtzung des Device Tree Overlays ganz einfach â man muss einfach den kompilierten Overlay in /boot/overlays legen und /boot/config.txt ein wenig modifizieren.
Beispiel einer dts-Datei
Ehrlich gesagt bin ich mir nicht sicher, ob es fehlerfrei geschrieben ist, 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=50000000Die Beschreibung der Anschlussmethode des Chips an den Raspberry Pi lasse ich weg. Einerseits bin ich kein Elektronikspezialist, andererseits ist es fĂŒr mich auch banal: Der Chip hat nur 8 Pins, von denen wir Masse, Stromversorgung, SPI (CS, SI, SO, SCK) benötigen; die Pegel stimmen mit den entsprechenden beim Raspberry Pi ĂŒberein, es sind keine zusĂ€tzlichen Verbindungen erforderlich â man verbindet einfach die angegebenen 6 Kontakte.
Aufgabenstellung
Wie gewohnt gibt es mehrere Iterationen bei der Aufgabenstellung, ich denke, es ist an der Zeit fĂŒr eine weitere. Lassen Sie uns also innehalten, das, was bereits geschrieben wurde, zusammenfassen und die noch im Schatten verbliebenen Details klĂ€ren.
Also haben wir uns darauf geeinigt, dass das Journal im SPI NOR Flash gespeichert wird.
Was ist NOR Flash fĂŒr diejenigen, die es nicht wissen
Es ist nichtflĂŒchtiger Speicher, mit dem man drei Operationen durchfĂŒhren kann:
- Lesen:
Das ganz gewöhnliche Lesen: wir ĂŒbergeben die Adresse und lesen so viele Bytes, wie wir benötigen; - Aufzeichnung:
Der Schreibvorgang in NOR-Flash sieht gewöhnlich aus, hat jedoch eine Besonderheit: Man kann nur 1 in 0 umwandeln, nicht umgekehrt. Wenn zum Beispiel in einer Speichereinheit 0x55 gespeichert war, wĂŒrde nach dem Schreiben von 0x0f dort 0x05 gespeichert sein. (siehe Tabelle etwas weiter unten); - Löschen:
NatĂŒrlich mĂŒssen wir auch die umgekehrte Operation beherrschen â 0 in 1 umzuwandeln, genau dafĂŒr gibt es die Löschoperation. Im Gegensatz zu den ersten beiden operiert sie nicht mit Bytes, sondern mit Blöcken (der minimale Löschblock im ausgewĂ€hlten Chip betrĂ€gt 4 KB). Löschen zerstört den gesamten Block und dies ist die einzige Möglichkeit, 0 in 1 umzuwandeln. Daher muss man beim Arbeiten mit Flash-Speicher oft Datenstrukturen an die Grenzen des Löschblocks anpassen.
Schreiben in NOR-Flash:
BinÀrdaten
War
01010101
Gespeichert
00001111
Wurde
00000101
Das Journal selbst besteht aus einer Sequenz von EintrÀgen variabler LÀnge. Eine typische EintragslÀnge betrÀgt etwa 30 Bytes (obwohl manchmal auch EintrÀge mit mehreren Kilobyte vorkommen). In diesem Fall behandeln wir sie einfach als eine Menge von Bytes, aber wenn es interessiert, werden innerhalb der EintrÀge CBOR verwendet.
Neben dem Journal mĂŒssen wir einige "Einstellinformationen" speichern, sowohl aktualisierbare als auch nicht: eine Art GerĂ€te-ID, Kalibrierungen von Sensoren, ein Flag "GerĂ€t vorĂŒbergehend ausgeschaltet", usw.
Diese Informationen bestehen aus einer Reihe von SchlĂŒssel-Wert-Paaren und werden ebenfalls in CBOR gespeichert. Wir haben nicht viele dieser Informationen (höchstens einige Kilobyte) und sie werden nicht hĂ€ufig aktualisiert.
Im weiteren Verlauf werden wir sie Kontext nennen.
Wenn wir uns erinnern, womit dieser Artikel begann, ist es sehr wichtig, die ZuverlÀssigkeit der Datenspeicherung zu gewÀhrleisten und, wenn möglich, einen ununterbrochenen Betrieb selbst bei Hardwarefehlern oder DatenbeschÀdigungen sicherzustellen.
Welche Problemquellen können wir betrachten?
- Stromausfall wÀhrend der Write-/Erase-Operationen. Das gehört zu der Kategorie "Gegen einen Brecheisen gibt es kein Rezept".
Information aus auf Stack Exchange: Bei einem Stromausfall wĂ€hrend der Arbeit mit Flash fĂŒhrt sowohl ein Löschen (Setzen auf 1) als auch ein Schreiben (Setzen auf 0) zu einem undefinierten Verhalten: Daten können geschrieben werden, teilweise geschrieben werden (sagen wir, wir haben 10 Bytes/80 Bit ĂŒbertragen, aber es wurden nur 45 Bit geschrieben), es ist auch möglich, dass einige Bits in einem "Zwischenzustand" sind (das Lesen kann sowohl 0 als auch 1 ausgeben); - Fehler im Flash-Speicher selbst.
Obwohl die BER sehr niedrig ist, kann sie nicht null sein; - Fehler ĂŒber den Bus
Die ĂŒber SPI ĂŒbertragenen Daten sind nicht geschĂŒtzt und können sowohl einfache Bitfehler als auch Synchronisationsfehler â den Verlust oder das EinfĂŒgen von Bits (was zu massiven Datenverzerrungen fĂŒhrt) â aufweisen; - Weitere Fehler/Fehlfunktionen
Fehler im Code, âGlitchesâ bei Raspberry, Eingreifen von AuĂerirdischenâŠ
Ich habe die Anforderungen formuliert, deren ErfĂŒllung meiner Meinung nach notwendig ist, um die ZuverlĂ€ssigkeit zu gewĂ€hrleisten:
- Aufzeichnungen mĂŒssen sofort in den Flash-Speicher geschrieben werden, verzögerte Aufzeichnungen werden nicht in Betracht gezogen; - wenn ein Fehler auftritt, muss dieser so frĂŒh wie möglich erkannt und verarbeitet werden; - das System sollte nach Möglichkeit nach Fehlern wieder funktionsfĂ€hig gemacht werden.
(ein praktisches Beispiel âwie es nicht sein sollteâ, mit dem ich denke, dass alle konfrontiert wurden: nach einem unerwarteten Neustart war das Dateisystem beschĂ€digt und das Betriebssystem startet nicht)
Ideen, AnsĂ€tze, Ăberlegungen
Als ich anfing, ĂŒber diese Aufgabe nachzudenken, schoss mir eine Menge Ideen durch den Kopf, zum Beispiel:
- Datenkompression verwenden;
- schlaue Datenstrukturen verwenden, zum Beispiel die Kopfzeilen von Aufzeichnungen getrennt von den eigentlichen Aufzeichnungen speichern, damit bei einem Fehler in einer Aufzeichnung die anderen problemlos gelesen werden können;
- Bitfelder zur Kontrolle der VollstÀndigkeit der Aufzeichnung bei Stromausfall verwenden;
- PrĂŒfziffern fĂŒr alles und jedes speichern;
- eine Art von fehlerresistenter Codierung verwenden.
Ein Teil dieser Ideen wurde umgesetzt, teilweise wurde entschieden, darauf zu verzichten. Lassen Sie uns der Reihe nach vorgehen.
Datenkompression
Die Ereignisse, die wir im Protokoll festhalten, sind in der Regel recht gleichförmig und wiederholend (âeine 5-Rubel-MĂŒnze geworfenâ, âdie Taste fĂŒr die RĂŒckgabe gedrĂŒcktâ, âŠ). Daher sollte die Kompression recht effektiv sein.
Die Kosten fĂŒr die Kompression sind gering (wir haben einen ziemlich leistungsstarken Prozessor, selbst das erste Pi hatte einen Kern mit 700 MHz, bei den aktuellen Modellen mehrere Kerne mit ĂŒber einem Gigahertz), die Geschwindigkeit des Austauschs mit dem Speicher ist niedrig (einige Megabyte pro Sekunde), die GröĂe der Aufzeichnungen ist klein. Insgesamt, wenn die Kompression einen Einfluss auf die Leistung hat, dann nur positiv. (absolut nicht kritisch, ich stelle es einfach fest). Wir haben ja kein echtes Embedded, sondern ein gewöhnliches Linux â die Implementierung sollte also nicht viele Anstrengungen erfordern (es reicht aus, die Bibliothek einfach zu verlinken und einige Funktionen daraus zu verwenden).
Es wurde ein Ausschnitt aus dem Protokoll eines funktionierenden GerĂ€ts (1,7 MB, 70.000 EintrĂ€ge) entnommen und zunĂ€chst auf Komprimierbarkeit mit den auf dem Computer verfĂŒgbaren gzip, lz4, lzop, bzip2, xz, zstd ĂŒberprĂŒft.
- Gzip, xz und zstd zeigten Àhnliche Ergebnisse (40 KB).
Es erstaunte, dass das angesagte xz hier auf dem Niveau von gzip oder zstd abschneidet; - Lzip mit den Standardeinstellungen lieferte ein etwas schlechteres Ergebnis;
- Lz4 und lzop zeigten kein besonders gutes Ergebnis (150 KB);
- Bzip2 zeigte ein ĂŒberraschend gutes Ergebnis (18 KB).
Die Daten komprimieren sich also sehr gut.
Wenn wir also (wenn wir keine fatalen MĂ€ngel finden) das Komprimieren umsetzen! Einfach, weil mehr Daten auf dasselbe Flash-Laufwerk passen.
Lassen Sie uns ĂŒber die Nachteile nachdenken.
Das erste Problem: Wir haben bereits vereinbart, dass jeder Eintrag sofort auf das Flash-Laufwerk geschrieben werden muss. Normalerweise sammelt ein Archivierungsprogramm die Daten aus dem Eingabestrom, bis es entscheidet, dass es Zeit ist, sie in den Ausgang zu schreiben. Wir hingegen mĂŒssen sofort einen komprimierten Datenblock erhalten und ihn im nichtflĂŒchtigen Speicher speichern.
Ich sehe drei Wege:
- Jeden Eintrag mit Hilfe von Dictionary-Kompression statt der oben genannten Algorithmen zu komprimieren.
Das ist eine durchaus praktikable Möglichkeit, aber ich mag sie nicht. Um einen mehr oder weniger akzeptablen Komprimierungsgrad zu gewĂ€hrleisten, muss das Wörterbuch auf bestimmte Daten angepasst sein; jede Ănderung fĂŒhrt dazu, dass der Komprimierungsgrad katastrophal sinkt. Ja, das Problem kann durch die Erstellung einer neuen Version des Wörterbuchs gelöst werden, aber das wĂ€re Kopfschmerzen â wir mĂŒssten alle Wörterbuchversionen speichern; bei jedem Eintrag mĂŒssten wir angeben, mit welcher Version des Wörterbuchs er komprimiert wurde⊠- Jeden Eintrag mit âklassischenâ Algorithmen zu komprimieren, jedoch unabhĂ€ngig von anderen.
Die betrachteten Kompressionsalgorithmen sind nicht fĂŒr die Verarbeitung von EintrĂ€gen dieser GröĂe (Dutzende von Bytes) konzipiert; der Kompressionsfaktor wird eindeutig unter 1 liegen (das heiĂt, die Datenmenge wird statt komprimiert vergröĂert); - Nach jedem Eintrag FLUSH zu machen.
In vielen Kompressionsbibliotheken gibt es UnterstĂŒtzung fĂŒr FLUSH. Dies ist ein Befehl (oder ein Parameter des Komprimierungsprozesses), der, wenn er empfangen wird, sicherstellt, dass der Archivator den komprimierten Stream so strukturiert, dass die bereits erhaltenen Ressourcen) abgeschlossen ist, lösen wir nicht komprimierten Daten wiederhergestellt werden können. Eine solche Analogiesyncin Dateisystemen odercommitin SQL.
Es ist wichtig, dass die folgenden Komprimierungsoperationen das angesammelte Wörterbuch verwenden können und der Grad der Kompression nicht so stark leidet wie im vorherigen Fall.
Ich denke, es ist offensichtlich, dass ich die dritte Option gewÀhlt habe; lassen Sie uns genauer darauf eingehen.
Gefunden ĂŒber FLUSH in zlib.
Ich habe einen Test basierend auf einem Artikel gemacht, ich nahm 70.000 ProtokolleintrĂ€ge von einem echten GerĂ€t, bei einer SeitengröĂe von 60KB (auf die SeitengröĂe kommen wir noch zurĂŒck) erhalten:
Stammdaten
Gzip-Kompression -9 (ohne FLUSH)
zlib mit Z_PARTIAL_FLUSH
zlib mit Z_SYNC_FLUSH
Volumen, KB
1692
40
352
604
Auf den ersten Blick scheint der Preis, den FLUSH verursacht, ĂŒbermĂ€Ăig hoch zu sein; jedoch haben wir tatsĂ€chlich nicht viele Optionen - entweder gar nicht komprimieren oder (sehr effektiv) mit FLUSH komprimieren. Man sollte nicht vergessen, dass wir 70.000 EintrĂ€ge haben, und die ĂberflĂŒssigkeit, die durch Z_PARTIAL_FLUSH eingefĂŒhrt wird, betrĂ€gt nur 4-5 Bytes pro Eintrag. Der Kompressionsfaktor stellte sich als fast 5:1 heraus, was mehr als ein hervorragendes Ergebnis ist.
Es mag ĂŒberraschend erscheinen, aber tatsĂ€chlich ist Z_SYNC_FLUSH eine effektivere Methode, um FLUSH durchzufĂŒhren.
Wenn Z_SYNC_FLUSH verwendet wird, werden die letzten 4 Bytes jedes Eintrags immer 0x00, 0x00, 0xff, 0xff sein. Und wenn wir sie kennen, dann können wir sie nicht speichern, sodass die endgĂŒltige GröĂe nur 324KB betrĂ€gt.
In dem Artikel, auf den ich mich beziehe, gibt es eine ErklÀrung:
Ein neuer Typ-0-Block mit leeren Inhalten wird angehÀngt.
Ein Typ-0-Block mit leeren Inhalten besteht aus:
- dem dreibitigen Blockkopf;
- 0 bis 7 Bits, die gleich Null sind, um die Byte-Ausrichtung zu erreichen;
- der vier-Byte-Sequenz 00 00 FF FF.
Wie man leicht erkennen kann, gehen vor diesen 4 Bytes im letzten Block 3 bis 10 Nullbits. Die Praxis hat jedoch gezeigt, dass es tatsÀchlich mindestens 10 Nullbits gibt.
Es stellt sich heraus, dass so kurze Datenblöcke normalerweise (immer?) mit einem Block vom Typ 1 (fester Block) codiert werden, der unbedingt mit 7 Nullbits endet, sodass wir insgesamt 10-17 garantiert null Bits erhalten (und die anderen werden mit einer Wahrscheinlichkeit von etwa 50 % null sein).
Daher zeigt sich in den Testdaten, dass in 100 % der FĂ€lle vor 0x00, 0x00, 0xff, 0xff ein Nullbyte kommt, und in mehr als einem Drittel der FĂ€lle - zwei Nullbytes (möglicherweise liegt es daran, dass ich binĂ€res CBOR verwende, und bei der Verwendung von textlichem JSON wĂŒrden wir wahrscheinlich öfter auf Blöcke vom Typ 2 - dynamischer Block - stoĂen, weshalb wir auch auf Blöcke ohne zusĂ€tzliche Nullbytes vor 0x00, 0x00, 0xff, 0xff stoĂen wĂŒrden).
Insgesamt kann man mit den vorhandenen Testdaten in weniger als 250KB komprimierten Daten unterkommen.
Man kann noch etwas sparen, indem man mit den Bits jongliert: Wir ignorieren derzeit das Vorhandensein mehrerer Null-Bits am Ende des Blocks, einige Bits am Anfang des Blocks bleiben ebenfalls unverÀndert ...
Aber dann traf ich die Entscheidung, aufzuhören, sonst könnte ich in der Geschwindigkeit zur Entwicklung meines eigenen Archivierers gelangen.
Insgesamt habe ich aus meinen Testdaten 3-4 Bytes pro Schreibvorgang erhalten, das KomprimierungsverhĂ€ltnis liegt bei ĂŒber 6:1. Ehrlich gesagt: Auf ein solches Ergebnis hatte ich nicht gehofft, meiner Meinung nach ist alles, was besser als 2:1 ist, bereits ein Ergebnis, das die Verwendung von Kompression rechtfertigt.
Alles gut, aber zlib (deflate) ist dennoch ein ehrwĂŒrdiger und etwas altmodischer Komprimierungsalgorithmus. Schon alleine, dass als Wörterbuch die letzten 32 Kb des unkomprimierten Datenstroms verwendet werden, sieht heute seltsam aus (das heiĂt, wenn ein bestimmter Datenblock dem Ă€hnelt, was vor 40 Kb im Eingabestrom war, wird er neu archiviert und verweist nicht auf den vorherigen Vorkommen). In modernen und angesagten Archivierern wird die Dictionary-GröĂe hĂ€ufig in Megabyte und nicht in Kilobyte gemessen.
Also machen wir unsere Mini-Studie zu Archivierern weiter.
Als NĂ€chstes wurde bzip2 ausprobiert (ich erinnere daran, dass es ohne FLUSH einen fantastischen Komprimierungsgrad von fast 100:1 zeigte). Leider hat es mit FLUSH sehr schlecht abgeschnitten, die GröĂe der komprimierten Daten war gröĂer als die der unkomprimierten.
Meine Vermutungen ĂŒber die GrĂŒnde fĂŒr das Scheitern
Libbz2 bietet nur eine Flush-Option an, die anscheinend das Wörterbuch löscht (analog zu Z_FULL_FLUSH in zlib), sodass man nicht von einer effektiven Kompression danach sprechen kann.
Und zuletzt wurde zstd ausprobiert. Je nach Parametern komprimiert es entweder auf Gzip-Niveau, jedoch viel schneller, oder besser als Gzip.
Leider hat es sich mit FLUSH ebenfalls 'nicht so gut' prĂ€sentiert: Die GröĂe der komprimierten Daten betrug etwa 700 Kb.
Ich auf der Projektseite in GitHub, und erhielt die Antwort, dass man mit bis zu 10 Bytes an Verwaltungsdaten pro Block der komprimierten Daten rechnen sollte, was den erhaltenen Ergebnissen nahekommt, den deflate wird man nicht einholen können.
Damit habe ich beschlossen, meine Experimente mit Archivierern zu beenden (ich erinnere daran, dass xz, lzip, lzo, lz4 sich bei den Tests ohne FLUSH nicht bewĂ€hrt haben, und ich habe es nicht fĂŒr nötig gehalten, exotischere Komprimierungsalgorithmen zu betrachten).
Kehren wir zu den Problemen der Archivierung zurĂŒck.
Das zweite (wie es sagt, nach Reihenfolge, nicht nach Bedeutung) Problem ist, dass komprimierte Daten einen einheitlichen Strom darstellen, in dem stÀndig Verweise auf vorherige Abschnitte gemacht werden. Dadurch verlieren wir bei BeschÀdigung eines Abschnitts der komprimierten Daten nicht nur den damit verbundenen Block unkomprimierter Daten, sondern auch alle nachfolgenden.
Es gibt AnsÀtze zur Lösung dieses Problems:
- Das Auftreten des Problems verhindern â redundante Informationen zu den komprimierten Daten hinzufĂŒgen, die es ermöglichen, Fehler zu erkennen und zu korrigieren; darĂŒber werden wir spĂ€ter sprechen;
- Die Folgen im Fall eines Problems minimieren.
Wir haben bereits frĂŒher gesagt, dass wir jeden Datenblock unabhĂ€ngig komprimieren können, wodurch das Problem von selbst verschwinden wĂŒrde (eine BeschĂ€digung der Daten eines Blocks wĂŒrde nur zum Verlust der Daten dieses Blocks fĂŒhren). Dies ist jedoch der Extremfall, in dem die Datenkompression ineffizient wird. Der entgegengesetzte Extremfall: alle 4Mb unseres Mikrochips als ein einziges Archiv zu verwenden, was uns eine hervorragende Komprimierung bieten wĂŒrde, aber katastrophale Folgen im Fall von DatenbeschĂ€digung hĂ€tte.
Ja, ein Kompromiss ist aus Sicht der ZuverlĂ€ssigkeit erforderlich. Aber wir mĂŒssen uns daran erinnern, dass wir ein Datenformat fĂŒr nichtflĂŒchtigen Speicher mit extrem niedrigem BER und einer angegebenen Datenhaltbarkeit von 20 Jahren entwickeln.
Im Verlauf der Experimente habe ich festgestellt, dass spĂŒrbare Verluste in der Kompressionsrate bei komprimierten Datenblöcken von weniger als 10Kb beginnen.
Es wurde vorher erwĂ€hnt, dass der verwendete Speicher eine seitenbasierte Organisation hat. Ich sehe keinen Grund, warum man nicht die Zuordnung âeine Seite â ein Block komprimierter Datenâ verwenden sollte.
Das heiĂt, die minimal vernĂŒnftige SeitengröĂe betrĂ€gt 16Kb (mit einem Puffer fĂŒr Verwaltungsinformationen). Allerdings bringt eine so kleine SeitengröĂe erhebliche EinschrĂ€nkungen fĂŒr die maximale AufzeichnungsgröĂe mit sich.
Obwohl ich bisher keine Aufzeichnungen gröĂer als ein Kilobyte in komprimierter Form plane, habe ich mich entschieden, Seiten mit einer GröĂe von 32Kb zu verwenden (insgesamt 128 Seiten pro Chip).
Zusammenfassung:
- Die Daten werden komprimiert mit zlib (deflate) gespeichert;
- FĂŒr jede Aufzeichnung setzen wir Z_SYNC_FLUSH;
- Jede komprimierte Aufzeichnung wird an den Endbytes gekĂŒrzt. (zum Beispiel 0x00, 0x00, 0xff, 0xff); im Header geben wir an, wie viele Bytes wir gekĂŒrzt haben;
- Die Daten speichern wir seitenweise mit 32 KB; innerhalb der Seite lÀuft ein einheitlicher Strom komprimierter Daten; bei jeder Seite beginnen wir die Kompression von neuem.
Und bevor wir mit der Kompression fertig sind, möchte ich darauf hinweisen, dass wir nur einige Bytes an komprimierten Daten pro Aufzeichnung erhalten, daher ist es Ă€uĂerst wichtig, die Verwaltungsinformationen nicht aufzublĂ€hen, jedes Byte zĂ€hlt hier.
Speicherung von DatenĂŒberschriften
Da wir Aufzeichnungen variabler LĂ€nge haben, mĂŒssen wir eine Möglichkeit finden, die Platzierung/Grenzen der Aufzeichnungen zu bestimmen.
Ich kenne drei AnsÀtze:
- Alle Aufzeichnungen werden in einem kontinuierlichen Strom gespeichert, zuerst kommt die Ăberschrift der Aufzeichnung, die die LĂ€nge enthĂ€lt, und danach die eigentliche Aufzeichnung.
In dieser Variante können sowohl die Ăberschriften als auch die Daten eine variable LĂ€nge haben.
Im Grunde erhalten wir eine einfach verkettete Liste, die ĂŒberall verwendet wird; - Ăberschriften und die eigentlichen Aufzeichnungen werden in separaten Strömen gespeichert.
Durch die Verwendung von Ăberschriften fester LĂ€nge sorgen wir dafĂŒr, dass die BeschĂ€digung einer Ăberschrift keine Auswirkungen auf die anderen hat.
Ein Àhnlicher Ansatz wird zum Beispiel in vielen Dateisystemen verwendet; - Aufzeichnungen werden in einem kontinuierlichen Strom gespeichert, die Grenze der Aufzeichnung wird durch einen bestimmten Marker (Symbol/Zeichenfolge, die innerhalb der Datenblöcke verboten ist) bestimmt. Wenn innerhalb einer Aufzeichnung ein Marker gefunden wird, ersetzen wir ihn durch eine bestimmte Folge (wir maskieren ihn).
Ein Àhnlicher Ansatz wird beispielsweise im PPP-Protokoll verwendet.
Ich werde es veranschaulichen.
Option 1:

Es ist ganz einfach: Wenn wir die LĂ€nge der Aufzeichnung kennen, können wir die Adresse der nĂ€chsten Ăberschrift berechnen. So bewegen wir uns durch die Ăberschriften, bis wir auf einen Bereich treffen, der mit 0xff gefĂŒllt ist (freier Bereich) oder das Seitenende erreicht ist.
Option 2:

Aufgrund der variablen LĂ€nge der Aufzeichnungen können wir nicht im Voraus sagen, wie viele Aufzeichnungen (also auch Ăberschriften) wir pro Seite benötigen werden. Man könnte die Ăberschriften und die Daten auf verschiedenen Seiten anordnen, aber ich bevorzuge einen anderen Ansatz: sowohl Ăberschriften als auch Daten werden auf einer Seite platziert, jedoch beginnen die Ăberschriften (fester GröĂe) vom Anfang der Seite und die Daten (variabler LĂ€nge) vom Ende. Sobald sie sich âtreffenâ (nicht genĂŒgend freien Platz fĂŒr eine neue Aufzeichnung) - betrachten wir diese Seite als voll.
Variante 3:

Es ist nicht notwendig, die LĂ€nge oder andere Informationen ĂŒber die Datenanordnung im Header zu speichern; Marker, die die Grenzen der DatensĂ€tze anzeigen, sind ausreichend. Allerdings mĂŒssen die Daten beim Schreiben/Lesen verarbeitet werden.
Als Marker wĂŒrde ich 0xff verwenden (mit dem die Seite nach dem Löschen gefĂŒllt ist), so dass der freie Bereich sicherlich nicht als Daten interpretiert wird.
Vergleichstabelle:
Option 1
Option 2
Option 3
Fehlerresistenz
â
+
+
Kompaktheit
+
â
+
ImplementierungskomplexitÀt
*
**
**
Variante 1 hat einen fatalen Nachteil: Wenn einer der Header beschÀdigt wird, wird die gesamte nachfolgende Kette zerstört. Die anderen Varianten ermöglichen es, Teile der Daten selbst bei massiven BeschÀdigungen wiederherzustellen.
Es ist jedoch erwĂ€hnenswert, dass wir uns entschieden haben, die Daten komprimiert zu speichern; so oder so verlieren wir alle Daten auf der Seite nach einem "kaputten" Schreibvorgang, sodass wir die Minuszahl in der Tabelle nicht berĂŒcksichtigen.
Kompaktheit:
- Bei der ersten Variante mĂŒssen wir im Header nur die LĂ€nge speichern; bei der Verwendung variabler LĂ€ngen Ganzzahlen können wir in den meisten FĂ€llen mit einem Byte auskommen;
- In der zweiten Variante mĂŒssen wir die Startadresse und die LĂ€nge speichern; der Datensatz sollte eine konstante GröĂe haben, ich schĂ€tze 4 Byte pro Datensatz (zwei Bytes fĂŒr die Verschiebung und zwei Bytes fĂŒr die LĂ€nge);
- Die dritte Variante benötigt nur ein Zeichen, um den Anfang des Datensatzes zu kennzeichnen; aufgrund der Maskierung wird der Datensatz um 1-2% gröĂer. Insgesamt gibt es eine ungefĂ€hre ParitĂ€t zur ersten Variante.
UrsprĂŒnglich betrachtete ich die zweite Variante als die Hauptvariante (und habe sogar eine Implementierung geschrieben). Ich habe sie nur verworfen, als ich endgĂŒltig beschloss, Kompression zu verwenden.
Vielleicht werde ich irgendwann doch eine Ă€hnliche Variante verwenden. Zum Beispiel, wenn ich fĂŒr ein Raumschiff, das zwischen der Erde und dem Mars verkehrt, Daten speichern muss â ganz andere Anforderungen an die ZuverlĂ€ssigkeit, Strahlung im Weltraum, ...
Was die dritte Variante angeht: Ich habe ihr wegen der KomplexitĂ€t der Implementierung zwei Sterne gegeben, einfach weil ich nicht gerne mit Maskierung, LĂ€ngenĂ€nderung wĂ€hrend des Prozesses usw. umgehe. Ja, möglicherweise voreingenommen, aber ich muss den Code schreiben â warum sollte ich mich zwingen, etwas zu tun, das mir nicht gefĂ€llt.
Zusammenfassung: Wir wĂ€hlen die Speicheroption in Form von Ketten "Header mit LĂ€nge â variabel lange Daten" wegen der Effizienz und Einfachheit der Implementierung.
Die Verwendung von Bitfeldern zur Kontrolle des Erfolgs von SchreibvorgÀngen
Ich erinnere mich nicht mehr, wo ich die Idee gesehen habe, aber es sieht ungefÀhr so aus:
FĂŒr jeden Eintrag reservieren wir mehrere Bits zur Speicherung von Flags.
Wie bereits erwĂ€hnt, sind nach dem Löschen alle Bits auf 1 gesetzt, und wir können 1 auf 0 Ă€ndern, aber nicht umgekehrt. FĂŒr "Flag nicht gesetzt" verwenden wir 1, fĂŒr "Flag gesetzt" â 0.
So könnte die Unterbringung eines variablen LÀngeneintrags im Flash aussehen:
- Setzen des Flags âLĂ€nge des Schreibens begonnenâ;
- LĂ€nge aufzeichnen;
- Setzen des Flags âDatenaufzeichnung begonnenâ;
- Daten aufzeichnen;
- Setzen des Flags âAufzeichnung abgeschlossenâ.
DarĂŒber hinaus haben wir ein Flag âFehler aufgetretenâ, insgesamt also 4 Bit-Flags.
In diesem Fall haben wir zwei stabile ZustĂ€nde â1111â â Aufzeichnung nicht begonnen und â1000â â Aufzeichnung war erfolgreich; bei unvorhergesehenen Unterbrechungen des Schreibvorgangs erhalten wir ZwischenzustĂ€nde, die wir spĂ€ter erkennen und verarbeiten können.
Der Ansatz ist interessant, schĂŒtzt aber nur vor plötzlichem Stromausfall und Ă€hnlichen Störungen, was natĂŒrlich wichtig ist, jedoch bei weitem nicht die einzige (und selbst nicht die Haupt-) Ursache fĂŒr mögliche Störungen darstellt.
Zusammenfassung: Lass uns weiter auf der Suche nach einer guten Lösung.
PrĂŒfziffern
PrĂŒfziffern ermöglichen ebenfalls zu verifizieren (mit ausreichender Wahrscheinlichkeit), dass wir genau das lesen, was hĂ€tte aufgezeichnet werden sollen. Und im Gegensatz zu den oben betrachteten Bitfeldern funktionieren sie immer.
Wenn wir die Liste potenzieller Problemerkenner betrachten, die wir zuvor besprochen haben, kann die PrĂŒfziffer einen Fehler unabhĂ€ngig von seiner Herkunft erkennen (auĂer natĂŒrlich bei böswilligen AuĂerirdischen â die können auch die PrĂŒfziffer fĂ€lschen).
Wenn unser Ziel also darin besteht, zu ĂŒberprĂŒfen, ob die Daten intakt sind, sind PrĂŒfziffern eine hervorragende Idee.
Die Wahl des Algorithmus zur Berechnung der PrĂŒfziffer war unproblematisch â CRC. Einerseits ermöglichen mathematische Eigenschaften, dass einige Typen von Fehlern zu 100 % erkannt werden, andererseits zeigt dieser Algorithmus bei zufĂ€lligen Daten normalerweise eine Kollisionswahrscheinlichkeit, die nicht wesentlich ĂŒber dem theoretischen Limit liegt.
Möge es nicht der schnellste Algorithmus sein und nicht immer die geringste Anzahl von Kollisionen aufweisen, aber er hat eine sehr wichtige Eigenschaft: In den mir begegneten Tests gab es keine Muster, bei denen er klar versagte. StabilitÀt ist in diesem Fall die wichtigste Eigenschaft.
Beispiel fĂŒr eine umfassende Untersuchung: , (Links zu narod.ru, entschuldigung).
Die Aufgabe der Auswahl einer PrĂŒfziffer ist jedoch noch nicht abgeschlossen, CRC ist eine ganze Familie von PrĂŒfziffern. Man muss sich mit der LĂ€nge entscheiden und dann ein Polynom auswĂ€hlen.
Die Auswahl der LĂ€nge der PrĂŒfziffer ist nicht so einfache Frage, wie sie auf den ersten Blick erscheint.
Ich werde es veranschaulichen:
Angenommen, wir haben eine Fehlerwahrscheinlichkeit in jedem Byte
und eine ideale PrĂŒfziffer, berechnen wir die durchschnittliche Anzahl von Fehlern auf eine Million EintrĂ€ge:
Daten, Byte
PrĂŒfziffer, Byte
Nicht entdeckte Fehler
Falschpositive Fehlererkennungen
Insgesamt falsche Auslösungen
1
0
1000
0
1000
1
1
4
999
1003
1
2
â0
1997
1997
1
4
â0
3990
3990
10
0
9955
0
9955
10
1
39
990
1029
10
2
â0
1979
1979
10
4
â0
3954
3954
1000
0
632305
0
632305
1000
1
2470
368
2838
1000
2
10
735
745
1000
4
â0
1469
1469
Es scheint einfach zu sein â wĂ€hle je nach LĂ€nge der geschĂŒtzten Daten die LĂ€nge der PrĂŒfziffer mit minimalen falschen Auslösungen â und alles ist erledigt.
Das Problem bei kurzen PrĂŒfziffern ist jedoch, dass sie, obwohl sie einzelne Bitfehler gut erkennen, mit einer erheblichen Wahrscheinlichkeit völlig zufĂ€llige Daten fĂŒr korrekt halten können. Auf habra wurde bereits ein Artikel beschrieben, der .
Um zufĂ€llige Ăbereinstimmungen der PrĂŒfziffer praktisch unmöglich zu machen, mĂŒssen PrĂŒfziffern mit einer LĂ€nge von 32 Bit oder mehr verwendet werden (FĂŒr LĂ€ngen ĂŒber 64 Bit verwendet man normalerweise kryptografische Hash-Funktionen).
Obwohl ich zuvor geschrieben habe, dass wir jeden Byte Platz sparen sollten, werden wir dennoch eine 32-Bit-PrĂŒfziffer verwenden (16 Bit sind zu wenig, die Kollisionswahrscheinlichkeit liegt ĂŒber 0,01 %; und 24 Bit sind sozusagen weder hier noch dort).
Hier könnte der Einwand aufkommen: Haben wir jeden Byte beim Komprimieren gespart, nur um jetzt sofort 4 Byte abzugeben? WĂ€re es nicht besser gewesen, nicht zu komprimieren und keine PrĂŒfziffer hinzuzufĂŒgen? NatĂŒrlich nicht, das Fehlen von Komprimierung bedeutet, dass die IntegritĂ€tsprĂŒfung fĂŒr uns nicht notwendig ist.
Bei der Auswahl des Polynoms werden wir das Rad nicht neu erfinden und das derzeit beliebte CRC-32C verwenden.
Dieser Code erkennt 6 Bitfehler in Paketen bis zu 22 Bytes (wahrscheinlich der hĂ€ufigste Fall fĂŒr uns), 4 Bitfehler in Paketen bis zu 655 Bytes (auch ein hĂ€ufiger Fall fĂŒr uns), 2 oder eine beliebige ungerade Anzahl von Bitfehlern in Paketen beliebiger sinnvoller LĂ€nge.
Falls jemand an den Details interessiert ist
ĂŒber CRC.
auf â wahrscheinlich der fĂŒhrende Experte fĂŒr CRC auf dem Planeten.
Im einen , die etwas bessere Parameter fĂŒr die fĂŒr uns relevanten PaketlĂ€ngen bietet, aber ich habe den Unterschied als nicht signifikant angesehen und fĂŒhle mich kompetent genug, um einen benutzerdefinierten Code anstelle eines standardisierten und gut erforschten Codes zu wĂ€hlen.
AuĂerdem stellt sich, da unsere Daten komprimiert sind, die Frage: Soll die PrĂŒfziffer fĂŒr komprimierte oder unkomprimierte Daten berechnet werden?
Argumente "fĂŒr" die Berechnung der PrĂŒfziffer fĂŒr unkomprimierte Daten:
- Wir mĂŒssen letztendlich die IntegritĂ€t der Datenspeicherung ĂŒberprĂŒfen â das prĂŒfen wir direkt (und dabei werden auch mögliche Fehler bei der Implementierung von Kompression/Dekompression sowie BeschĂ€digungen, die durch defekten Speicher verursacht werden, ĂŒberprĂŒft);
- der deflate-Algorithmus in zlib hat eine ausreichend ausgereifte Implementierung und sollte nicht bei "krummen" Eingabedaten abstĂŒrzen; darĂŒber hinaus ist er hĂ€ufig in der Lage, Fehler im Eingabestrom selbst zu erkennen, wodurch die allgemeine Wahrscheinlichkeit einer unentdeckten Fehler entstehen verringert wird (ich habe einen Test mit der Umkehrung eines einzelnen Bits in einem kurzen Datensatz durchgefĂŒhrt, zlib hat den Fehler in etwa einem Drittel der FĂ€lle erkannt).
Argumente "gegen" die Berechnung der PrĂŒfziffer fĂŒr unkomprimierte Daten:
- CRC ist speziell auf seltene Bitfehler abgestimmt, die charakteristisch fĂŒr Flash-Speicher sind (ein Bitfehler im komprimierten Stream kann eine MassenĂ€nderung des Ausgangsstroms zur Folge haben, wo wir theoretisch eine Kollision "erwischen" könnten);
- ich bin nicht so begeistert von der Idee, potenziell fehlerhafte Daten an den Dekompressor zu ĂŒbergeben, , wie er reagieren wird.
In diesem Projekt habe ich beschlossen, von der ĂŒblichen Praxis abzuweichen, die PrĂŒfziffer fĂŒr unkomprimierte Daten zu speichern.
Zusammenfassung: Wir verwenden CRC-32C, die PrĂŒfziffer wird von den Daten in der Form berechnet, in der sie in den Flash geschrieben werden (nach der Kompression).
Redundanz
Der Einsatz von Redundanzkodierung kann zwar nicht verhindern, dass Daten verloren gehen, aber er kann die Wahrscheinlichkeit eines irreversiblen Datenverlusts erheblich (oft um viele GröĂenordnungen) verringern.
Wir können verschiedene Arten von Redundanz verwenden, um Fehler zu korrigieren.
Hamming-Codes können einzelne Bitfehler korrigieren, Reed-Solomon-Codes sind symbolbasierend, mehrere Kopien von Daten zusammen mit PrĂŒfziffern oder Kodierungen wie RAID-6 können helfen, Daten selbst im Falle massiver SchĂ€den wiederherzustellen.
ZunĂ€chst war ich fĂŒr den breiten Einsatz von fehlerresistenten Kodierungen eingestellt, verstand dann aber, dass man zunĂ€chst eine Vorstellung davon haben muss, vor welchen Fehlern man sich schĂŒtzen möchte, bevor man die Kodierung wĂ€hlt.
Wir haben zuvor gesagt, dass Fehler so schnell wie möglich erkannt werden mĂŒssen. Zu welchen Zeitpunkten können wir mit Fehlern konfrontiert werden?
- UnvollstÀndiges Schreiben (aus irgendeinem Grund war beim Schreiben die Stromversorgung unterbrochen, Raspberry hing, ...)
Leider bleibt im Falle eines solchen Fehlers nur, ungĂŒltige Aufzeichnungen zu ignorieren und die Daten als verloren zu betrachten; - Schreibfehler (aus irgendeinem Grund wurde in den Flash-Speicher etwas anderes geschrieben als beabsichtigt)
Solche Fehler können wir sofort erkennen, wenn wir unmittelbar nach dem Schreiben eine PrĂŒflesung durchfĂŒhren; - Datenverzerrung im Speicher wĂ€hrend der Speicherung;
- Lese-Fehler
Zur Behebung genĂŒgt es, im Falle eines Abgleichs der PrĂŒfziffer das Lesen mehrere Male zu wiederholen.
Das heiĂt, nur Fehler des dritten Typs (spontane Datenkorruption bei der Speicherung) können ohne fehlerresistente Kodierung nicht behoben werden. Man könnte denken, dass solche Fehler dennoch Ă€uĂerst unwahrscheinlich sind.
Zusammenfassung: Es wurde beschlossen, auf Redundanzkodierung zu verzichten, aber wenn der Betrieb zeigt, dass diese Entscheidung falsch war, wird man die Frage erneut prĂŒfen (mit bereits gesammelter Statistik ĂŒber AusfĂ€lle, die es ermöglichen wird, die optimale Art der Kodierung auszuwĂ€hlen).
Sonstiges
NatĂŒrlich erlaubt das Artikelformat nicht, jedes Bit im Format zu begrĂŒnden (und mir sind auch die KrĂ€fte ausgegangen), deshalb werde ich einige Punkte kurz anreiĂen, die zuvor nicht angesprochen wurden.
- Es wurde entschieden, alle Seiten "gleichberechtigt" zu gestalten.
Das heiĂt, es wird keine speziellen Seiten mit Metadaten oder separaten Streams usw. geben, stattdessen einen einzigen Stream, der alle Seiten nacheinander ĂŒberschreibt.
Das sorgt fĂŒr einen gleichmĂ€Ăigen VerschleiĂ der Seiten, vermeidet einen einzelnen Fehlerpunkt und gefĂ€llt einfach. - Die Versionierung des Formats sollte unbedingt berĂŒcksichtigt werden.
Ein Format ohne Versionsnummer im Header ist eine Katastrophe!
Es reicht aus, im Header der Seite ein Feld mit einer Art Magic Number (Signatur) hinzuzufĂŒgen, die auf die verwendete Versionsnummer des Formats hinweist. (Ich denke nicht, dass es in der Praxis mehr als ein Dutzend davon geben wird.); - Verwenden Sie fĂŒr die EintrĂ€ge (von denen es sehr viele gibt) einen Header variabler LĂ€nge und versuchen Sie, ihn in den meisten FĂ€llen auf 1 Byte zu beschrĂ€nken.
- Zur Kodierung der LÀnge des Headers und der LÀnge des abgeschnittenen Teils des komprimierten Eintrags sollten binÀre Codes variabler LÀnge verwendet werden.
Online-Generator Beschreibung des Datenspeicherformats
Byte-Reihenfolge
Felder, die gröĂer als ein Byte sind, werden im Big-Endian-Format (Netzwerk-Byte-Reihenfolge) gespeichert, das heiĂt, 0x1234 wird als 0x12, 0x34 gespeichert.
Seitenaufteilung
Der gesamte Flash-Speicher ist in gleich groĂe Seiten unterteilt.
Die standardmĂ€Ăige SeitengröĂe betrĂ€gt 32KB, jedoch nicht mehr als 1/4 der gesamten GröĂe des Speicherchips (fĂŒr einen 4MB-Chip ergibt das 128 Seiten).
Jede Seite speichert Daten unabhÀngig von anderen (d. h. die Daten einer Seite verweisen nicht auf die Daten einer anderen Seite).
Alle Seiten sind in natĂŒrlicher Reihenfolge (in aufsteigender Adressfolge) nummeriert, beginnend mit der Nummer 0 (die Nullseite beginnt mit Adresse 0, die erste mit 32KB, die zweite mit 64KB usw.).
Der Speicherchip wird als zirkulĂ€rer Puffer (Ringpuffer) verwendet, d. h. zuerst wird in die Seite mit der Nummer 0 geschrieben, dann in die Seite mit der Nummer 1, ⊠wenn wir die letzte Seite fĂŒllen, beginnt ein neuer Zyklus und das Schreiben geht mit der Nullseite weiter.
Innerhalb der Seite
Am Anfang der Seite wird ein 4-Byte-Header gespeichert, gefolgt von einer PrĂŒfziffer des Headers (CRC-32C) und dann werden die EintrĂ€ge im Format âHeader, Daten, PrĂŒfzifferâ gespeichert.

Der Header der Seite (auf dem Diagramm schmutziggrĂŒn) besteht aus:
einem zweibyteigen Feld Magic Number (auch als Versionsbezeichnung des Formats bekannt)
- fĂŒr die aktuelle Version des Formats wird er als
FĂŒr die aktuelle Version des Formats wird er als0xed00 â Seitenzahl; - des zweibyteigen ZĂ€hlers âSeitenversionâ (Zyklusnummer der SpeicherĂŒberschreibung).
Die Aufzeichnungen auf der Seite werden in komprimierter Form gespeichert (es wird der Algorithmus deflate verwendet). Alle Aufzeichnungen auf einer Seite werden in einem Stream komprimiert (das wird ein gemeinsames Wörterbuch verwendet), auf jeder neuen Seite beginnt die Kompression erneut. Das heiĂt, fĂŒr die Dekomprimierung jeder Aufzeichnung sind alle vorhergehenden Aufzeichnungen von dieser Seite notwendig (und nur von dieser).
Jede Aufzeichnung wird mit dem Flag Z_SYNC_FLUSH komprimiert, dabei befinden sich am Ende des komprimierten Streams 4 Bytes 0x00, 0x00, 0xff, 0xff, möglicherweise vorangestellt durch ein oder zwei Nullbytes.
Diese Sequenz (von 4, 5 oder 6 Bytes) wird bei der Aufzeichnung in den Flash-Speicher verworfen.
Der Header der Aufzeichnung besteht aus 1, 2 oder 3 Bytes, die folgendes speichern:
- ein Bit (T), das den Typ der Aufzeichnung anzeigt: 0 â Kontext, 1 â Protokoll;
- ein variabel langes Feld (S) von 1 bis 7 Bits, das die LĂ€nge des Headers und den âSchwanzâ, der der Aufzeichnung zur Dekompression hinzugefĂŒgt werden muss, definiert;
- die LĂ€nge der Aufzeichnung (L).
Tabelle der Werte S:
S
LĂ€nge des Headers, Bytes
Wird bei der Aufzeichnung verworfen, Bytes
0
1
5 (00 00 00 ff ff)
10
1
6 (00 00 00 00 ff ff)
110
2
4 (00 00 ff ff)
1110
2
5 (00 00 00 ff ff)
11110
2
6 (00 00 00 00 ff ff)
1111100
3
4 (00 00 ff ff)
1111101
3
5 (00 00 00 ff ff)
1111110
3
6 (00 00 00 00 ff ff)
Ich habe versucht, es zu veranschaulichen, ich weiĂ nicht, wie anschaulich es geworden ist:

Hier ist das Feld T gelb gekennzeichnet, das Feld S weiĂ, grĂŒn ist L (die LĂ€nge der komprimierten Daten in Bytes), blau sind die komprimierten Daten, rot sind die Endbytes der komprimierten Daten, die nicht im Flash-Speicher geschrieben werden.
Somit können wir die Header der am hÀufigsten vorkommenden LÀnge (bis zu 63+5 Bytes in komprimierter Form) mit einem Byte aufzeichnen.
Nach jeder Aufzeichnung wird eine CRC-32C-PrĂŒfziffer gespeichert, bei der als Initialwert (init) der invertierte Wert der vorherigen PrĂŒfziffer verwendet wird.
CRC hat die Eigenschaft der âFortdauerâ, die Formel funktioniert (plus-minus Invertierung der Bits im Prozess):
.
Das heiĂt, wir berechnen tatsĂ€chlich die CRC aller vorhergehenden Bytes der Header und Daten auf dieser Seite.
Direkt nach der PrĂŒfziffer befindet sich der Header der nĂ€chsten Aufzeichnung.
Der Header ist so konstruiert, dass das erste Byte immer unterschiedlich von 0x00 und 0xff ist (wenn wir anstelle des ersten Bytes des Headers 0xff antreffen, bedeutet das, dass es sich um einen nicht verwendeten Bereich handelt; 0x00 signalisiert einen Fehler).
UngefÀhre Algorithmen
Lesen aus dem Flash-Speicher
Jedes Lesen erfolgt mit einer ĂberprĂŒfung der PrĂŒfziffer.
Wenn die PrĂŒfziffer nicht ĂŒbereinstimmt, wird das Lesen mehrmals wiederholt, in der Hoffnung, die tatsĂ€chlichen Daten korrekt zu lesen.
(das macht Sinn, Linux cached das Lesen aus NOR Flash nicht, getestet)
Schreiben in den Flash-Speicher
Wir schreiben Daten.
Wir lesen sie.
Wenn die gelesenen Daten nicht mit den geschriebenen ĂŒbereinstimmen, fĂŒllen wir den Bereich mit Nullen und signalisieren einen Fehler.
Vorbereitung des neuen Chips zur Verwendung
Zur Initialisierung wird im ersten (genauer gesagt, nullten) Seitenbereich ein Header mit der Version 1 geschrieben.
Danach wird in diesen Seitenbereich der Anfangskontext (enthÀlt die UUID der Maschine und die Standardkonfigurationen) geschrieben.
Alles, der Flash-Speicher ist bereit zur Verwendung.
Laden der Maschine
Beim Laden werden die ersten 8 Byte jeder Seite (Header + CRC) gelesen. Seiten mit unbekannter Magic Number oder ungĂŒltigem CRC werden ignoriert.
Aus den ârichtigenâ Seiten werden die Seiten mit der maximalen Version ausgewĂ€hlt, davon wird die Seite mit der höchsten Nummer entnommen.
Der erste Eintrag wird gelesen, die Korrektheit des CRC wird ĂŒberprĂŒft, sowie das Vorhandensein des âKontextâ-Flags. Wenn alles in Ordnung ist, wird diese Seite als aktuell betrachtet. Andernfalls gehen wir auf die vorherige zurĂŒck, bis wir eine âlebendigeâ Seite finden.
Auf der gefundenen Seite lesen wir alle EintrĂ€ge, die mit dem Flag âKontextâ versehen sind, wenden wir an.
Wir speichern das zlib-Wörterbuch (wird fĂŒr das Nachschreiben in diese Seite benötigt).
Alles, der Ladevorgang ist abgeschlossen, der Kontext wurde wiederhergestellt, man kann arbeiten.
HinzufĂŒgen eines Eintrags zum Protokoll
Wir komprimieren den Eintrag mit dem richtigen Wörterbuch und geben Z_SYNC_FLUSH an. Wir prĂŒfen, ob der komprimierte Eintrag auf die aktuelle Seite passt.
Wenn er nicht passt (oder die Seite CRC-Fehler aufwies) â beginnen wir eine neue Seite (siehe unten).
Wir schreiben den Eintrag und den CRC. Tritt ein Fehler auf, beginnen wir eine neue Seite.
Neue Seite
WĂ€hlen Sie die freie Seite mit der minimalen Nummer aus (eine Seite gilt als frei, wenn sie eine falsche PrĂŒfziffer im Header oder eine Version hat, die kleiner ist als die aktuelle). Wenn es solche Seiten nicht gibt, wĂ€hlen wir die Seite mit der minimalen Nummer aus denjenigen aus, die die gleiche Version wie die aktuelle haben.
Wir fĂŒhren ein Erase fĂŒr die gewĂ€hlte Seite durch. Wir vergleichen den Inhalt mit 0xff. Wenn etwas nicht stimmt, nehmen wir die nĂ€chste freie Seite usw.
Auf die gelöschte Seite schreiben wir den Header, als ersten Eintrag den aktuellen Zustand des Kontextes, als nÀchsten den nicht geschriebenen Eintrag aus dem Protokoll (wenn vorhanden).
Anwendbarkeit des Formats
Meiner Meinung nach ist das ein ganz passables Format zur Speicherung beliebiger mehr oder weniger komprimierbarer Informationsströme (einfacher Text, JSON, MessagePack, CBOR, möglicherweise Protobuf) im NOR Flash.
NatĂŒrlich ist das Format auf SLC NOR Flash ausgelegt.
Es sollte nicht mit TrĂ€gern mit hohem BER verwendet werden, wie NAND oder MLC NOR (gibt es solche Speicher ĂŒberhaupt im Handel? Ich habe nur ErwĂ€hnungen in Arbeiten ĂŒber Fehlerkorrektur gefunden).
Umso mehr sollte es nicht mit GerĂ€ten verwendet werden, die ĂŒber ein eigenes FTL verfĂŒgen: USB-Flash, SD, MicroSD usw. (FĂŒr solchen Speicher habe ich ein Format mit einer SeitengröĂe von 512 Byte, einer Signatur am Anfang jeder Seite und eindeutigen Aufzeichnungsnummern erstellt â manchmal konnte ich alle Daten durch einfaches sequentielles Lesen von einem "glitschigen" Flash wiederherstellen).
Je nach Aufgabe kann das Format unverĂ€ndert auf Flash-Laufwerken von 128 Kbit (16 KB) bis 1 Gbit (128 MB) verwendet werden. Bei Bedarf kann es auch auf Chips mit gröĂerem Volumen verwendet werden, aber wahrscheinlich muss die SeitengröĂe angepasst werden. (Aber hier stellt sich bereits die Frage der Wirtschaftlichkeit, der Preis fĂŒr groĂen NOR Flash ist nicht erfreulich).
Wenn jemand das Format interessant findet und es in einem Open-Source-Projekt verwenden möchte â schreibt mir, ich werde versuchen, Zeit zu finden, um den Code zu ĂŒberarbeiten und ihn auf GitHub hochzuladen.
Fazit
Wie man sieht, ist das Format am Ende einfach und sogar langweilig.
In dem Artikel ist es schwierig, die Evolution meiner Sichtweise widerzuspiegeln, aber glauben Sie mir: UrsprĂŒnglich wollte ich etwas Komplexes, Unzerstörbares schaffen, das selbst nach einer nuklearen Explosion in unmittelbarer NĂ€he ĂŒberleben kann. Doch die Vernunft (hoffentlich) hat letztendlich gesiegt und die PrioritĂ€ten verschoben sich allmĂ€hlich in Richtung Einfachheit und Kompaktheit.
Kann es sein, dass ich mich geirrt habe? Ja, natĂŒrlich. Es könnte durchaus sein, dass wir zum Beispiel eine Partie minderwertiger Chips gekauft haben. Oder aus einem anderen Grund erfĂŒllt die Hardware nicht die Erwartungen an die ZuverlĂ€ssigkeit.
Habe ich einen Plan fĂŒr diesen Fall? Ich denke, dass Sie beim Lesen des Artikels keinen Zweifel haben, dass ich einen Plan habe. Und sogar mehrere.
Etwas ernster gesagt, wurde das Format gleichzeitig als Arbeitstyp und als "Versuchsschuss" entwickelt.
Im Moment funktioniert alles auf meinem Tisch normal, in den nĂ€chsten Tagen wird die Lösung bereitgestellt (ungefĂ€hr) Bei Hunderten von GerĂ€ten werden wir sehen, wie sie im "Einsatz" abschneiden (zum GlĂŒck hoffe ich, erlaubt das Format eine zuverlĂ€ssige Fehlerdetektion; so können wir umfassende Statistiken sammeln). Ăber mehrere Monate können dann Schlussfolgerungen gezogen werden. (und wenn wir Pech haben â sogar frĂŒher).
Falls bei der Nutzung schwerwiegende Probleme festgestellt werden und Anpassungen erforderlich sind, werde ich unbedingt darĂŒber berichten.
Literatur
Ich wollte keine lange, langweilige Liste der verwendeten Arbeiten erstellen; schlieĂlich hat jeder Google.
Hier habe ich beschlossen, eine Liste von Entdeckungen zu fĂŒhren, die mir besonders interessant erschienen. Diese sind jedoch allmĂ€hlich in den Artikeltext gewandert, und in der Liste blieb nur ein Punkt ĂŒbrig:
- Das Tool von Autor zlib. Kann den Inhalt von deflate/zlib/gzip-Archiven in verstĂ€ndlicher Form anzeigen. Wenn Sie sich mit dem inneren Aufbau des deflate-Formats (oder gzip) auseinandersetzen mĂŒssen, kann ich es dringend empfehlen.
Quelle: habr.com
