Contexte
Nous avons des distributeurs automatiques dĂ©veloppĂ©s en interne. Ă l'intĂ©rieur, un Raspberry Pi et un peu de cĂąblage sur une carte sĂ©parĂ©e. Ils sont connectĂ©s Ă un acceptateur de piĂšces, un acceptateur de billets, un terminal bancaire⊠Tout est gĂ©rĂ© par un logiciel sur mesure. L'historique de fonctionnement est enregistrĂ© dans un journal sur une clĂ© USB (MicroSD), qui est ensuite transfĂ©rĂ© via Internet (via un modem USB) vers un serveur, oĂč il est stockĂ© dans une base de donnĂ©es. Les informations sur les ventes sont chargĂ©es dans 1C, et il existe Ă©galement une interface web simple pour le suivi, etc.
Donc, le journal est absolument nĂ©cessaire â pour la comptabilitĂ© (avec les recettes, les ventes, etc.), et pour le monitoring (toutes sortes de pannes et autres circonstances imprĂ©vues); c'est, on peut dire, toute l'information que nous avons sur ce distributeur automatique.
Le problĂšme
Les clĂ©s USB se rĂ©vĂšlent ĂȘtre des dispositifs trĂšs peu fiables. Elles tombent rĂ©guliĂšrement en panne. Cela entraĂźne Ă la fois des temps d'arrĂȘt pour les distributeurs et (si pour une raison quelconque le journal ne pouvait pas ĂȘtre transmis en ligne) des pertes de donnĂ©es.
Ce n'est pas la premiĂšre fois que nous utilisons des clĂ©s USB, il y avait auparavant un autre projet avec plus d'une centaine d'appareils, oĂč le journal Ă©tait stockĂ© sur des clĂ©s USB, il y avait aussi des problĂšmes de fiabilitĂ©, et parfois le nombre de pannes par mois atteignait des dizaines. Nous avons essayĂ© diffĂ©rentes clĂ©s, y compris des modĂšles de marques avec de la mĂ©moire SLC, certaines modĂšles Ă©tant plus fiables que d'autres, mais le remplacement des clĂ©s USB n'a pas rĂ©solu le problĂšme de maniĂšre radicale.
Attention ! Long read! Si vous n'ĂȘtes pas intĂ©ressĂ© par le « pourquoi », mais seulement par le « comment », vous pouvez aller directement articles.
Solution
La premiÚre chose qui me vient à l'esprit : renoncer au MicroSD, opter par exemple pour un SSD, et démarrer depuis là . Théoriquement, c'est possible, mais relativement cher, et ce n'est pas si fiable (un adaptateur USB-SATA est ajouté ; les statistiques de défaillance des SSD bon marché ne sont pas trÚs réjouissantes non plus).
Un disque dur USB ne semble pas non plus ĂȘtre une solution particuliĂšrement attrayante.
C'est pourquoi nous avons optĂ© pour cette solution : conserver le dĂ©marrage Ă partir du MicroSD, mais les utiliser en mode lecture seule, et stocker le journal de travail (et d'autres informations uniques pour chaque appareil â numĂ©ro de sĂ©rie, Ă©talonnages des capteurs, etc.) ailleurs.
Le sujet des systĂšmes de fichiers en lecture seule pour Raspberry Pi a dĂ©jĂ Ă©tĂ© Ă©tudiĂ© en profondeur, je ne m'attarderai pas sur les dĂ©tails de mise en Ćuvre dans cet article. (mais si cela vous intĂ©resse, peut-ĂȘtre que j'Ă©crirai un mini-article Ă ce sujet). Le seul point Ă noter est qu'en fonction de mon expĂ©rience personnelle et des retours d'expĂ©rience de ceux qui l'ont dĂ©jĂ intĂ©grĂ©, il y a des gains en fiabilitĂ©. Oui, il est impossible de se dĂ©barrasser complĂštement des pannes, mais il est tout Ă fait rĂ©aliste de rĂ©duire considĂ©rablement leur frĂ©quence. De plus, les cartes deviennent unifiĂ©es, ce qui facilite visiblement le remplacement pour le personnel de maintenance.
Partie matérielle
Il n'y avait pas de doutes particuliers sur le type de mĂ©moire â NOR Flash.
Arguments :
- connexion simple (le plus souvent un bus SPI, dont l'expérience d'utilisation existe déjà , donc pas de problÚmes 'matériels' à prévoir) ;
- prix dérisoire ;
- protocole de fonctionnement standard (une implĂ©mentation existe dĂ©jĂ dans le noyau Linux, si souhaitĂ© on peut prendre une externe, qui existe aussi, ou mĂȘme Ă©crire la sienne, c'est assez simple) ;
- fiabilité et endurance :
d'aprÚs une fiche technique typique : les données sont conservées pendant 20 ans, 100000 cycles d'effacement pour chaque bloc ;
d'aprÚs des sources externes : BER trÚs bas, aucune nécessité de codes de correction d'erreurs postulée (dans certains travaux, l'ECC pour NOR est envisagé, mais en général on parle là de MLC NOR, ce genre de cas existe également).
Estimation des exigences en volume et en ressources.
Nous souhaitons que les donnĂ©es soient conservĂ©es de maniĂšre garantie pendant plusieurs jours. Cela est nĂ©cessaire pour que, en cas de problĂšme de connexion, l'historique des ventes ne soit pas perdu. Nous viserons une pĂ©riode de 5 jours, durant laquelle (mĂȘme en tenant compte des week-ends et des jours fĂ©riĂ©s) on peut rĂ©soudre le problĂšme.
Actuellement, nous accumulons environ 100 Ko de journaux par jour (3-4 milliers d'entrĂ©es), mais ce chiffre augmente progressivement â la granularitĂ© augmente, de nouveaux Ă©vĂ©nements sont ajoutĂ©s. De plus, il y a parfois des pics (un capteur commence Ă spammer avec de fausses activations, par exemple). Nous allons calculer sur 10 000 entrĂ©es de 100 octets â un mĂ©gaoctet par jour.
Au total, cela donne 5 Mo de données brutes (bien compressibles). à cela s'ajoute (estimation brute) 1 Mo de données de service.
Donc, nous avons besoin d'une puce de 8 Mo si nous ne sommes pas en mesure d'utiliser la compression, ou de 4 Mo si nous l'utilisons. Des chiffres tout à fait réels pour ce type de mémoire.
Quant aux ressources : si nous prévoyons que la mémoire sera complÚtement réécrite pas plus d'une fois tous les 5 jours, alors pour 10 ans de service, nous obtenons moins de mille cycles d'écriture.
Je rappelle que le fabricant promet cent mille.
Un peu sur NOR vs NAND
Aujourd'hui, la mémoire NAND est bien plus populaire, mais pour ce projet, je ne l'utiliserais pas : la NAND, contrairement à la NOR, nécessite toujours des codes de correction d'erreurs, des tableaux de blocs défectueux, etc., et les puces NAND ont généralement beaucoup plus de broches.
Les inconvénients de la NOR incluent :
- petit volume (et donc un prix par mégaoctet élevé);
- vitesse de transfert relativement faible (en grande partie à cause de l'utilisation d'une interface série, généralement SPI ou I2C);
- effacement lent (qui prend de quelques fractions de seconde Ă plusieurs secondes selon la taille du bloc).
Il n'y a rien de critique pour nous, donc continuons.
Pour ceux qui sont intĂ©ressĂ©s par les dĂ©tails, le circuit intĂ©grĂ© sĂ©lectionnĂ© est (ce qui n'est d'ailleurs pas important, il y a des milliers d'analogues sur le marchĂ©, compatibles en termes de brochage et de jeu de commandes ; mĂȘme si nous voulions installer un circuit intĂ©grĂ© d'un autre fabricant et/ou d'une autre capacitĂ©, tout fonctionnerait sans modification du code).
J'utilise le pilote intĂ©grĂ© au noyau Linux, sur Raspberry grĂące Ă la prise en charge des overlays de l'arbre des pĂ©riphĂ©riques, c'est trĂšs simple â il suffit de placer l'overlay compilĂ© dans /boot/overlays et de modifier lĂ©gĂšrement /boot/config.txt.
Exemple de fichier dts
HonnĂȘtement, je ne suis pas sĂ»r que cela soit Ă©crit sans erreurs, mais ça fonctionne.
/*
* 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?";
};
};Et une ligne supplémentaire dans config.txt
dtoverlay=at25:spimaxfrequency=50000000Je ne vais pas dĂ©crire la connexion du circuit intĂ©grĂ© au Raspberry Pi. D'une part, je ne suis pas un spĂ©cialiste en Ă©lectronique, d'autre part â c'est trĂšs simple mĂȘme pour moi : le circuit intĂ©grĂ© n'a que 8 broches, dont nous avons besoin de la terre, de l'alimentation, et de SPI (CS, SI, SO, SCK); les niveaux correspondent Ă ceux du Raspberry Pi, aucun cĂąblage supplĂ©mentaire n'est nĂ©cessaire â il suffit de connecter les 6 broches indiquĂ©es.
Définition du problÚme
Comme d'habitude, la dĂ©finition du problĂšme passe par plusieurs itĂ©rations, je pense qu'il est temps pour une autre. Alors, arrĂȘtons-nous, rassemblons ce qui a dĂ©jĂ Ă©tĂ© Ă©crit, et clarifions les dĂ©tails qui restent dans l'ombre.
Ainsi, nous avons décidé que le journal serait stocké dans la mémoire Flash SPI NOR.
Qu'est-ce que la mémoire Flash NOR pour ceux qui ne savent pas
C'est une mémoire non volatile avec laquelle on peut effectuer trois opérations :
- Lecture :
La lecture classique : nous donnons une adresse et lisons autant d'octets que nĂ©cessaire; - Ăcriture :
L'écriture dans la mémoire NOR flash ressemble à une écriture classique, mais elle a une particularité : on ne peut changer un 1 en 0, mais seulement l'inverse. Par exemple, si 0x55 était stocké dans la cellule de mémoire, aprÚs l'écriture de 0x0f, elle contiendra 0x05. (voir le tableau ci-dessous); - Effacer :
Ăvidemment, nous devons Ă©galement ĂȘtre capables d'effectuer l'opĂ©ration inverse â changer un 0 en 1, c'est prĂ©cisĂ©ment Ă cela que sert l'opĂ©ration d'effacement. Contrairement aux deux premiĂšres opĂ©rations, elle opĂšre non pas sur des octets mais sur des blocs (le bloc d'effacement minimal dans la puce choisie est de 4 Ko). L'effacement dĂ©truit l'ensemble du bloc et c'est le seul moyen de changer un 0 en 1. Par consĂ©quent, lors du travail avec la mĂ©moire flash, il est souvent nĂ©cessaire d'aligner les structures de donnĂ©es Ă la frontiĂšre du bloc d'effacement.
Ăcriture dans la mĂ©moire NOR Flash :
Données binaires
Il y avait
01010101
Ăcrit
00001111
Il est devenu
00000101
Le journal lui-mĂȘme reprĂ©sente une sĂ©quence d'entrĂ©es de longueur variable. La longueur typique d'une entrĂ©e est d'environ 30 octets (bien que parfois il y ait des entrĂ©es de plusieurs kilo-octets). Dans ce cas, nous travaillons avec elles simplement comme avec un ensemble d'octets, mais, si cela vous intĂ©resse, CBOR est utilisĂ© Ă l'intĂ©rieur des enregistrements.
En plus du journal, nous devons stocker certaines informations de "configuration", tant mises à jour qu'immuables : un certain ID de l'appareil, les calibrations des capteurs, un drapeau "appareil temporairement désactivé", etc.
Ces informations se présentent sous la forme d'un ensemble d'enregistrements key-value, également stocké en CBOR. Nous n'avons pas beaucoup de ces informations (un maximum de quelques kilo-octets), elles ne sont pas mises à jour fréquemment.
Nous l'appellerons désormais le contexte.
Si l'on se remĂ©more le dĂ©but de cet article, il est trĂšs important d'assurer la fiabilitĂ© du stockage des donnĂ©es et, si possible, un fonctionnement ininterrompu mĂȘme en cas de dĂ©faillances matĂ©rielles ou de corruption des donnĂ©es.
Quelles sources de problĂšmes peut-on envisager ?
- Coupure de courant au moment des opérations d'écriture/effacement. C'est du type "contre un bùton, il n'y a pas de remÚde".
Informations de stackexchange : lors de la coupure de courant lors du travail avec la flash, que ce soit pour l'effacement (mise Ă 1) ou pour l'Ă©criture (mise Ă 0), cela entraĂźne un comportement indĂ©fini : les donnĂ©es peuvent ĂȘtre enregistrĂ©es, enregistrĂ©es partiellement (par exemple, nous avons transmis 10 octets/80 bits, mais seulement 45 bits ont pu ĂȘtre Ă©crits), il est Ă©galement possible qu'une partie des bits se trouve dans un Ă©tat "intermĂ©diaire" (la lecture peut renvoyer soit 0, soit 1) ; - Erreurs de la mĂ©moire flash elle-mĂȘme.
Le BER, bien que trĂšs bas, ne peut pas ĂȘtre nul ; - Erreurs sur le bus
Les donnĂ©es transmises par SPI ne sont pas protĂ©gĂ©es et peuvent subir des erreurs de bits individuelles ainsi que des erreurs de synchronisation â perte ou insertion de bits (ce qui entraĂźne des distorsions massives des donnĂ©es) ; - Autres erreurs/sabots
Erreurs de code, «bugs» Raspberry, interventions d'extraterrestresâŠ
J'ai formulé des exigences dont je pense qu'elles sont nécessaires pour assurer la fiabilité :
- les enregistrements doivent ĂȘtre enregistrĂ©s en mĂ©moire flash immĂ©diatement, l'enregistrement diffĂ©rĂ© n'est pas envisagĂ© ; - si une erreur survient, elle doit ĂȘtre dĂ©tectĂ©e et gĂ©rĂ©e le plus tĂŽt possible ; - le systĂšme doit pouvoir restaurer son fonctionnement aprĂšs une erreur.
(exemple de la vie «comment cela ne devrait pas ĂȘtre», avec lequel, je pense, tout le monde a Ă©tĂ© confrontĂ© : aprĂšs un redĂ©marrage d'urgence, le systĂšme de fichiers a Ă©tĂ© «endommagé» et le systĂšme d'exploitation ne dĂ©marre pas)
Idées, approches, réflexions
Lorsque j'ai commencé à réfléchir à cette tùche, une multitude d'idées a traversé mon esprit, par exemple :
- utiliser la compression de données ;
- utiliser des structures de donnĂ©es astucieuses, par exemple stocker les en-tĂȘtes des enregistrements sĂ©parĂ©ment des enregistrements eux-mĂȘmes, afin qu'en cas d'erreur dans l'un des enregistrements, les autres puissent ĂȘtre lus sans problĂšme ;
- utiliser des champs bits pour contrĂŽler l'achĂšvement de l'enregistrement en cas de coupure d'alimentation ;
- conserver des sommes de contrĂŽle pour tout et n'importe quoi ;
- utiliser une sorte de codage résistant aux erreurs.
Certaines de ces idées ont été utilisées, d'autres ont été mises de cÎté. Faisons-les dans l'ordre.
Compression des données
Les Ă©vĂ©nements que nous enregistrons dans le journal sont assez homogĂšnes et rĂ©pĂ©titifs («jeter une piĂšce de 5 roubles», «appuyer sur le bouton pour rendre la monnaie», âŠ). Par consĂ©quent, la compression devrait s'avĂ©rer assez efficace.
Les frais gĂ©nĂ©raux de compression sont insignifiants (notre processeur est suffisamment puissant, mĂȘme le premier Pi avait un cĆur Ă 700 MHz, les modĂšles actuels en ont plusieurs Ă plus d'un gigahertz), la vitesse d'Ă©change avec le stockage est faible (quelques mĂ©gaoctets par seconde), et la taille des enregistrements est petite. Bref, si la compression affecte les performances, ce ne sera que de maniĂšre positive. (absolument non critique, je constate simplement). De plus, nous n'avons pas un vĂ©ritable embedded, mais un Linux ordinaire â donc la mise en Ćuvre ne devrait pas nĂ©cessiter beaucoup d'efforts (il suffit de lier la bibliothĂšque et d'utiliser quelques fonctions de celle-ci).
Un extrait de journal d'un appareil en fonctionnement a été pris (1,7 Mo, 70 000 enregistrements) et d'abord vérifié pour sa compressibilité à l'aide de gzip, lz4, lzop, bzip2, xz, zstd disponibles sur l'ordinateur.
- gzip, xz, zstd ont montré des résultats proches (40 Ko).
Il est surprenant que le tendance xz ait montré ici des performances au niveau de gzip ou zstd; - lzip avec les paramÚtres par défaut a donné un résultat légÚrement moins bon;
- lz4 et lzop ont montré des résultats plutÎt médiocres (150 Ko);
- bzip2 a donné un résultat étonnamment bon (18 Ko).
Ainsi, les données se compressent trÚs bien.
Donc (si nous ne trouvons pas de dĂ©fauts fatals) la compression sera lĂ ! Juste parce que plus de donnĂ©es pourront tenir sur la mĂȘme clĂ© USB.
Réfléchissons aux inconvénients.
Le premier problĂšme : nous avons dĂ©jĂ convenu que chaque enregistrement doit ĂȘtre immĂ©diatement Ă©crit sur la clĂ©. En gĂ©nĂ©ral, un compresseur accumule les donnĂ©es du flux d'entrĂ©e jusqu'Ă ce qu'il juge qu'il est temps d'Ă©crire dans le flux de sortie. Nous avons besoin d'obtenir immĂ©diatement un bloc de donnĂ©es compressĂ©es et de les enregistrer dans une mĂ©moire non volatile.
Je vois trois solutions :
- Compresser chaque enregistrement à l'aide de la compression par dictionnaire au lieu des algorithmes mentionnés ci-dessus.
C'est une option fonctionnelle, mais je ne l'aime pas. Pour maintenir un niveau de compression Ă peu prĂšs acceptable, le dictionnaire doit ĂȘtre « adaptĂ© » aux donnĂ©es spĂ©cifiques, tout changement entraĂźnera une chute catastrophique du taux de compression. Oui, ce problĂšme peut ĂȘtre rĂ©solu en crĂ©ant une nouvelle version du dictionnaire, mais c'est un vĂ©ritable casse-tĂȘte â nous devrons conserver toutes les versions du dictionnaire ; chaque enregistrement devra indiquer avec quelle version du dictionnaire il a Ă©tĂ© compressé⊠- Compresser chaque enregistrement avec des algorithmes « classiques », mais indĂ©pendamment les uns des autres.
Les algorithmes de compression examinés ne sont pas conçus pour fonctionner avec des enregistrements de cette taille (dizaines d'octets), le taux de compression sera clairement inférieur à 1 (c'est-à -dire une augmentation du volume des données au lieu de compression); - Faire un FLUSH aprÚs chaque enregistrement.
De nombreuses bibliothÚques de compression prennent en charge le FLUSH. C'est une commande (ou un paramÚtre de la procédure de compression) qui, une fois reçue, fait en sorte que le compresseur génÚre un flux compressé permettant de restaurer tout les données non compressées qui ont déjà été reçues. Une sorte d'analoguesyncdans les systÚmes de fichiers oucommitdans sql.
Ce qui est important, c'est que les opérations de compression suivantes pourront utiliser le dictionnaire accumulé et le taux de compression ne sera pas aussi affecté que dans l'option précédente.
Je pense qu'il est évident que j'ai choisi la troisiÚme option, examinons-la plus en détail.
Trouvé sur FLUSH dans zlib.
J'ai effectué un test inspiré d'un article, j'ai pris 70 000 enregistrements d'un journal d'un appareil réel, avec une taille de page de 60 Ko (Nous reviendrons sur la taille de la page) j'ai obtenu :
Données d'origine
Compression gzip -9 (sans FLUSH)
zlib avec Z_PARTIAL_FLUSH
zlib avec Z_SYNC_FLUSH
Volume, Ko
1692
40
352
604
Ă premiĂšre vue, le coĂ»t apportĂ© par FLUSH semble excessif, cependant en rĂ©alitĂ© nos choix ne sont pas nombreux â soit ne pas compresser du tout, soit compresser (et de maniĂšre trĂšs efficace) en utilisant FLUSH. N'oublions pas que nous avons 70 000 enregistrements, le surcoĂ»t apportĂ© par Z_PARTIAL_FLUSH est de seulement 4-5 octets par enregistrement. Et le taux de compression s'est avĂ©rĂ© presque de 5:1, ce qui est plus qu'un excellent rĂ©sultat.
Il peut sembler surprenant, mais en réalité Z_SYNC_FLUSH est une méthode plus efficace pour effectuer un FLUSH.
Dans le cas de l'utilisation de Z_SYNC_FLUSH, les 4 derniers octets de chaque enregistrement seront toujours 0x00, 0x00, 0xff, 0xff. Et si nous les connaissons, nous pouvons ne pas les stocker, donc la taille finale ne fait que 324 Ko.
Dans l'article auquel je fais référence, il y a une explication :
Un nouveau bloc de type 0 avec un contenu vide est ajouté.
Un bloc de type 0 avec un contenu vide se compose de :
- l'en-tĂȘte de bloc de trois bits ;
- de 0 à 7 bits égaux à zéro, pour atteindre l'alignement des octets ;
- la séquence de quatre octets 00 00 FF FF.
Comme il n'est pas difficile de le remarquer, dans le dernier bloc avant ces 4 octets, il y a de 3 à 10 bits à zéro. Cependant, la pratique a montré qu'il y a en fait au moins 10 bits à zéro.
Il s'avÚre que de si courts blocs de données sont généralement (toujours ?) codés avec un bloc de type 1 (bloc fixe), qui se termine obligatoirement par 7 bits à zéro, ce qui nous donne entre 10 et 17 bits à zéro garantis (et les autres seront à zéro avec une probabilité d'environ 50%).
Donc, sur les donnĂ©es de test, dans 100 % des cas, avant 0x00, 0x00, 0xff, 0xff, il y a un octet Ă zĂ©ro, et dans plus d'un tiers des cas â deux octets Ă zĂ©ro. (peut-ĂȘtre est-ce parce que j'utilise un CBOR binaire, et lors de l'utilisation de JSON textuel, on rencontrerait plus frĂ©quemment des blocs de type 2 â blocs dynamiques, de sorte qu'on rencontrerait des blocs sans octets supplĂ©mentaires Ă zĂ©ro avant 0x00, 0x00, 0xff, 0xff).
Ainsi, sur les données de test existantes, on peut se conformer à moins de 250 Ko de données compressées.
On peut encore économiser un peu en jonglant avec les bits : maintenant, nous ignorons la présence de plusieurs bits nuls à la fin du bloc, et quelques bits au début du bloc ne changent également pas...
Mais ici, j'ai pris la dĂ©cision de m'arrĂȘter, sinon, Ă ce rythme, je pourrais finir par dĂ©velopper mon propre archiveur.
En rĂ©sumĂ©, j'ai obtenu de mes donnĂ©es de test 3 Ă 4 octets par Ă©criture, et le taux de compression a dĂ©passĂ© 6:1. HonnĂȘtement, je ne m'attendais pas Ă un tel rĂ©sultat, Ă mon avis, tout ce qui est meilleur que 2:1 est dĂ©jĂ un rĂ©sultat justifiant l'utilisation de la compression.
Tout va bien, mais zlib (deflate) reste un algorithme de compression archaĂŻque, mĂ©ritant et un peu dĂ©modĂ©. DĂ©jĂ , le fait qu'il utilise les 32 Ko les plus rĂ©cents du flux de donnĂ©es non compressĂ©es semble Ă©trange aujourd'hui (c'est-Ă -dire que si un bloc de donnĂ©es ressemble beaucoup Ă ce qui Ă©tait dans le flux d'entrĂ©e il y a 40 Ko, il commencera Ă ĂȘtre compressĂ© Ă nouveau, plutĂŽt que de faire rĂ©fĂ©rence Ă une entrĂ©e prĂ©cĂ©dente). Dans les archiveurs modernes Ă la mode, la taille du dictionnaire est souvent mesurĂ©e en mĂ©gaoctets, et non en kilo-octets.
Donc, nous continuons notre mini-recherche sur les archiveurs.
Le suivant Ă ĂȘtre testĂ© a Ă©tĂ© bzip2 (rappelons-le, sans FLUSH il a montrĂ© un taux de compression fantastique, presque 100:1). Malheureusement, avec FLUSH, il a montrĂ© de trĂšs mauvais rĂ©sultats, la taille des donnĂ©es compressĂ©es Ă©tait supĂ©rieure Ă celle des donnĂ©es non compressĂ©es.
Mes hypothÚses sur les raisons de l'échec
Libbz2 n'offre qu'une seule option de flush, qui semble nettoyer le dictionnaire (analogue de Z_FULL_FLUSH dans zlib), il est difficile de parler d'une compression efficace aprĂšs cela.
Et enfin, le dernier testé a été zstd. Selon les paramÚtres, il compresse soit au niveau de gzip, mais beaucoup plus rapidement, soit mieux que gzip.
Malheureusement, avec FLUSH, il a également montré des résultats « pas trÚs impressionnants » : la taille des données compressées a atteint environ 700 Ko.
Je sur la page du projet sur github, et j'ai reçu une réponse indiquant qu'on doit s'attendre à environ 10 octets de données de service pour chaque bloc de données compressées, ce qui est proche des résultats obtenus, on ne pourra jamais égaler deflate.
J'ai dĂ©cidĂ© d'arrĂȘter mes expĂ©riences avec les archiveurs (rappelons que xz, lzip, lzo, lz4 n'ont pas Ă©tĂ© performants lors des tests sans FLUSH, et je n'ai pas voulu envisager des algorithmes de compression plus exotiques).
Retournons aux problĂšmes d'archivage.
Le deuxiÚme problÚme (comme on le dit en termes d'ordre et non de signification) est que les données compressées se présentent sous la forme d'un flux unique, contenant constamment des références à des sections précédentes. Ainsi, si une partie des données compressées est endommagée, nous perdons non seulement le bloc de données non compressées qui y est lié, mais également tous les suivants.
Il existe plusieurs approches pour résoudre ce problÚme :
- Prévenir l'apparition du problÚme en ajoutant une redondance aux données compressées, ce qui permettra de détecter et de corriger les erreurs ; nous en parlerons plus tard;
- Minimiser les conséquences en cas de problÚme
Nous avons dĂ©jĂ mentionnĂ© prĂ©cĂ©demment que chaque bloc de donnĂ©es peut ĂȘtre compressĂ© indĂ©pendamment, ce qui rĂ©soudrait le problĂšme (la corruption des donnĂ©es d'un bloc entraĂźnerait la perte de donnĂ©es uniquement pour ce bloc). Cependant, c'est un cas extrĂȘme oĂč la compression des donnĂ©es serait inefficace. L'autre extrĂȘme : utiliser les 4 Mo de notre puce comme une seule archive, ce qui donnerait une excellente compression, mais des consĂ©quences catastrophiques en cas de corruption des donnĂ©es.
Oui, un compromis en termes de fiabilitĂ© est nĂ©cessaire. Mais il faut garder Ă l'esprit que nous dĂ©veloppons un format de stockage des donnĂ©es pour une mĂ©moire non volatile avec un TBER extrĂȘmement bas et une durĂ©e de conservation des donnĂ©es dĂ©clarĂ©e de 20 ans.
Au cours de mes expériences, j'ai découvert que des pertes de niveau de compression plus ou moins perceptibles commencent avec des blocs de données compressées d'une taille inférieure à 10 Ko.
Il a été mentionné précédemment que la mémoire utilisée est organisée par pages, et je ne vois pas de raisons pour lesquelles il ne faudrait pas utiliser la correspondance 'une page - un bloc de données compressées'.
C'est-à -dire que la taille minimale raisonnable d'une page est de 16 Ko (avec une réserve pour les informations de service). Cependant, une si petite taille de page impose des restrictions substantielles sur la taille maximale de l'enregistrement.
Bien que je n'anticipe pas encore d'enregistrements plus grands qu'un kilooctet en version compressée, j'ai décidé d'utiliser des pages de 32 Ko (ce qui donne un total de 128 pages par puce).
Résumé :
- Nous stockons les données compressées à l'aide de zlib (deflate);
- Pour chaque enregistrement, nous définissons Z_SYNC_FLUSH;
- Pour chaque enregistrement compressĂ©, nous coupons les octets finaux (par exemple, 0x00, 0x00, 0xff, 0xff); dans l'en-tĂȘte, nous indiquons combien d'octets nous avons coupĂ©s;
- Les données sont stockées par pages de 32 Ko; à l'intérieur de la page, nous avons un flux continu de données compressées; à chaque page, nous recommençons la compression.
Et, avant de terminer avec la compression, je voudrais souligner que nous n'obtenons qu'une poignée d'octets compressés par enregistrement, il est donc crucial de ne pas gonfler les informations de service, chaque octet compte ici.
Stockage des en-tĂȘtes de donnĂ©es
Comme nous avons des enregistrements de longueur variable, nous devons trouver un moyen de déterminer l'emplacement / les limites des enregistrements.
Je connais trois approches :
- Tous les enregistrements sont stockĂ©s dans un flux continu, d'abord l'en-tĂȘte de l'enregistrement, contenant la longueur, puis l'enregistrement lui-mĂȘme.
Dans ce cas, les en-tĂȘtes et les donnĂ©es peuvent avoir une longueur variable.
En gros, nous obtenons une liste chaĂźnĂ©e qui est utilisĂ©e Ă profusion. - Les en-tĂȘtes et les enregistrements eux-mĂȘmes sont stockĂ©s dans des flux sĂ©parĂ©s.
En utilisant des en-tĂȘtes de longueur fixe, nous garantissons qu'une corruption d'un en-tĂȘte n'affecte pas les autres.
Une telle approche est utilisée, par exemple, dans de nombreux systÚmes de fichiers. - Les enregistrements sont stockés dans un flux continu, la limite de l'enregistrement est définie par un certain marqueur (symbole / séquence de symboles qui sont interdits à l'intérieur des blocs de données). Si un marqueur apparaßt dans l'enregistrement, nous le remplaçons par une certaine séquence (nous l'échappons).
Une telle approche est utilisée, par exemple, dans le protocole PPP.
Je vais illustrer.
Option 1 :

Ici, c'est trĂšs simple : en connaissant la longueur de l'enregistrement, nous pouvons calculer l'adresse du prochain en-tĂȘte. Ainsi, nous avançons Ă travers les en-tĂȘtes jusqu'Ă ce que nous rencontrions une zone remplie de 0xff (zone libre) ou la fin de la page.
Option 2 :

En raison de la longueur variable des enregistrements, nous ne pouvons pas dire Ă l'avance combien d'enregistrements (et donc d'en-tĂȘtes) seront nĂ©cessaires pour remplir une page. Nous pourrions sĂ©parer les en-tĂȘtes et les donnĂ©es sur diffĂ©rentes pages, mais je prĂ©fĂšre une autre approche : Ă la fois les en-tĂȘtes et les donnĂ©es sont placĂ©s sur une seule page, cependant les en-tĂȘtes (de taille fixe) se trouvent au dĂ©but de la page, tandis que les donnĂ©es (de longueur variable) commencent Ă l'arriĂšre. DĂšs qu'ils "se rencontrent" (l'espace libre n'est pas suffisant pour un nouvel enregistrement) â nous considĂ©rons que cette page est pleine.
Option 3 :

Il n'est pas nĂ©cessaire de stocker la longueur ou d'autres informations sur la disposition des donnĂ©es dans l'en-tĂȘte, il suffit d'utiliser des marqueurs pour indiquer les frontiĂšres des enregistrements. Cependant, les donnĂ©es doivent ĂȘtre traitĂ©es lors de l'Ă©criture/lecture.
Comme marqueur, j'utiliserais 0xff (qui remplit la page aprÚs une éradication), de cette façon, la zone libre ne sera définitivement pas interprétée comme des données.
Tableau comparatif :
Option 1
Option 2
Option 3
Résilience aux erreurs
â
+
+
Compacité
+
â
+
ComplexitĂ© de mise en Ćuvre
*
**
**
L'option 1 a un dĂ©faut fatal : si l'un des en-tĂȘtes est endommagĂ©, toute la chaĂźne suivante est dĂ©truite. Les autres options permettent de rĂ©cupĂ©rer une partie des donnĂ©es mĂȘme en cas de dommages massifs.
Mais il convient de rappeler que nous avons dĂ©cidĂ© de stocker les donnĂ©es de maniĂšre compressĂ©e, donc nous perdons toutes les donnĂ©es sur la page aprĂšs une Ă©criture « dĂ©fectueuse », donc mĂȘme si un moins est affichĂ© dans le tableau, nous ne le prenons pas en compte.
Compacité :
- Dans le premier cas, nous devons stocker uniquement la longueur dans l'en-tĂȘte, en utilisant une variable de longueur entiĂšre, nous pouvons gĂ©nĂ©ralement nous contenter d'un octet ;
- Dans le deuxiÚme cas, nous devons stocker l'adresse de départ et la longueur ; l'enregistrement doit avoir une taille fixe, j'estime cela à 4 octets par enregistrement (deux octets pour le décalage et deux octets pour la longueur) ;
- La troisiĂšme option nĂ©cessite seulement un caractĂšre pour dĂ©signer le dĂ©but de l'enregistrement, de plus, l'enregistrement lui-mĂȘme augmentera de 1 Ă 2 % Ă cause de l'Ă©chappement. Dans l'ensemble, il y a une paritĂ© approximative avec la premiĂšre option.
Ă l'origine, j'ai considĂ©rĂ© la deuxiĂšme option comme principale (et mĂȘme j'ai Ă©crit une implĂ©mentation). Je ne m'en suis dĂ©tourĂ© qu'une fois que j'ai dĂ©cidĂ© d'utiliser la compression.
Peut-ĂȘtre qu'un jour j'utiliserai quand mĂȘme une telle option. Par exemple, si je devais m'occuper de stocker des donnĂ©es pour un vaisseau circulant entre la Terre et Mars â des exigences de fiabilitĂ© complĂštement diffĂ©rentes, le rayonnement cosmique, âŠ
Quant Ă la troisiĂšme option : je lui ai donnĂ© deux Ă©toiles pour la complexitĂ© de mise en Ćuvre simplement parce que je n'aime pas me compliquer avec l'Ă©chappement, le changement de longueur en cours de traitement, etc. Oui, c'est peut-ĂȘtre biaisĂ©, mais c'est moi qui vais devoir Ă©crire le code â pourquoi me forcer Ă faire quelque chose que je n'aime pas.
RĂ©sumĂ© : nous choisissons l'option de stockage sous la forme de chaĂźnes « en-tĂȘte avec longueur â donnĂ©es de longueur variable » en raison de son efficacitĂ© et de sa simplicitĂ© de mise en Ćuvre.
Utilisation de champs de bits pour contrÎler le succÚs des opérations d'écriture
Je ne me souviens dĂ©jĂ plus oĂč j'ai vu l'idĂ©e, mais cela ressemble Ă peu prĂšs Ă ceci :
Pour chaque enregistrement, nous assignons quelques bits pour stocker des drapeaux.
Comme nous l'avons dit prĂ©cĂ©demment, aprĂšs l'effacement, tous les bits sont remplis de 1, et nous pouvons changer le 1 en 0, mais pas l'inverse. Ainsi, pour « drapeau non dĂ©fini », nous utilisons 1, pour « drapeau dĂ©fini » â 0.
Voici Ă quoi pourrait ressembler l'enregistrement d'une variable de longueur dans la flash :
- Nous définissons le drapeau « l'enregistrement de la longueur a commencé » ;
- Nous enregistrons la longueur ;
- Nous définissons le drapeau « l'enregistrement des données a commencé » ;
- Nous enregistrons les données ;
- Nous définissons le drapeau « l'enregistrement est terminé ».
De plus, nous aurons un drapeau « une erreur s'est produite », soit 4 drapeaux de bits au total.
Dans ce cas, nous avons deux Ă©tats stables « 1111 » â l'enregistrement n'a pas commencĂ© et « 1000 » â l'enregistrement a rĂ©ussi ; en cas d'interruption imprĂ©vue du processus d'enregistrement, nous obtiendrons des Ă©tats intermĂ©diaires que nous pourrons ensuite dĂ©tecter et traiter.
L'approche est intĂ©ressante, mais elle ne protĂšge que contre une coupure de courant soudaine et des pannes similaires, ce qui est bien sĂ»r important, mais ce n'est pas la seule (et mĂȘme pas la principale) cause possible de pannes.
Résumé : continuons à chercher une bonne solution.
Sommes de contrĂŽle
Les sommes de contrĂŽle permettent Ă©galement de s'assurer (avec une probabilitĂ© suffisante) que nous lisons exactement ce qui devait ĂȘtre enregistrĂ©. Et, contrairement aux champs de bits mentionnĂ©s ci-dessus, elles fonctionnent toujours.
Si nous examinons la liste des sources potentielles de problĂšmes que nous avons discutĂ©es ci-dessus, la somme de contrĂŽle peut dĂ©tecter une erreur indĂ©pendamment de son origine (Ă l'exception peut-ĂȘtre des extraterrestres malveillants â ceux-ci peuvent falsifier la somme de contrĂŽle Ă©galement).
Ainsi, si notre objectif est de vérifier que les données sont intactes, les sommes de contrÎle sont une excellente idée.
Le choix de l'algorithme de calcul de la somme de contrĂŽle ne posait pas de questions â CRC. D'une part, les propriĂ©tĂ©s mathĂ©matiques permettent de dĂ©tecter Ă 100 % certaines erreurs, d'autre part â sur des donnĂ©es alĂ©atoires, cet algorithme montre gĂ©nĂ©ralement une probabilitĂ© de collisions pas beaucoup plus Ă©levĂ©e que la limite thĂ©orique.
. Bien que ce ne soit pas l'algorithme le plus rapide et qu'il ne minimise pas toujours le nombre de collisions, il a une qualité trÚs importante : dans les tests que j'ai rencontrés, il n'y avait pas de motifs sur lesquels il échouait clairement. La stabilité est la principale qualité dans ce cas.
Exemple d'une étude approfondie : , (liens vers narod.ru, désolé).
Cependant, la tùche de choisir une somme de contrÎle n'est pas terminée, CRC est une famille entiÚre de sommes de contrÎle. Il faut se décider sur la longueur, puis choisir le polynÎme.
Le choix de la longueur de la somme de contrĂŽle n'est pas aussi simple qu'il y paraĂźt au premier abord.
Je vais illustrer :
Supposons que nous avons une probabilité d'erreur pour chaque octet
et une somme de contrÎle idéale, calculons le nombre moyen d'erreurs sur un million d'enregistrements :
Données, octet
Somme de contrĂŽle, octet
Erreurs non détectées
Faux positifs
Total des fausses alertes
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
Il semblerait que ce soit simple â choisissez la longueur de la somme de contrĂŽle selon la longueur des donnĂ©es protĂ©gĂ©es avec un minimum de fausses alertes â et le tour est jouĂ©.
Cependant, avec des sommes de contrÎle courtes, un problÚme se pose : bien qu'elles détectent bien les erreurs de bit uniques, elles peuvent, avec une probabilité assez élevée, accepter des données complÚtement aléatoires comme valides. Un article a déjà été publié sur Habr décrivant .
Ainsi, pour rendre une coïncidence de somme de contrÎle pratiquement impossible, il faut utiliser des sommes de contrÎle d'une longueur d'au moins 32 bits (pour des longueurs supérieures à 64 bits, on utilise généralement des fonctions de hachage cryptographiques).
Bien que j'aie précédemment écrit qu'il faut économiser de l'espace par tous les moyens, nous allons néanmoins utiliser une somme de contrÎle de 32 bits (16 bits est insuffisant, la probabilité de collision dépasse 0,01 % ; et 24 bits, comme on dit, ne mÚne nulle part).
Une objection pourrait surgir : avons-nous vraiment économisé chaque octet lors du choix de la compression pour donner maintenant 4 octets d'un coup ? N'était-il pas mieux de ne pas compresser et de ne pas ajouter de somme de contrÎle ? Bien sûr que non, l'absence de compression ne signifie pas, que la vérification d'intégrité n'est pas nécessaire.
Nous ne réinventerons pas la roue pour le choix du polynÎme, et nous prendrons le populaire CRC-32C.
Ce code dĂ©tecte 6 erreurs de bits sur des paquets jusqu'Ă 22 octets (peut-ĂȘtre le cas le plus frĂ©quent pour nous), 4 erreurs de bits sur des paquets jusqu'Ă 655 octets (Ă©galement un cas frĂ©quent pour nous), 2 ou tout nombre impair d'erreurs de bits sur des paquets de toute longueur raisonnable.
Si cela intéresse quelqu'un
sur le CRC.
sur â peut-ĂȘtre le principal spĂ©cialiste du CRC sur la planĂšte.
Dans Il existe , offrant des paramÚtres légÚrement meilleurs pour les longueurs de paquets qui nous concernent, mais je n'ai pas jugé la différence significative, et je me sens suffisamment compétent pour choisir un code personnalisé plutÎt que standard et bien étudié.
De plus, étant donné que nos données sont compressées, se pose la question : devrions-nous calculer la somme de contrÎle à partir des données compressées ou non compressées ?
Arguments en faveur du calcul de la somme de contrÎle sur les données non compressées :
- nous devons finalement vĂ©rifier l'intĂ©gritĂ© des donnĂ©es stockĂ©es â c'est ce que nous vĂ©rifions directement (cela permettra Ă©galement de vĂ©rifier les Ă©ventuelles erreurs dans l'implĂ©mentation de la compression/dĂ©compression, les corruptions causĂ©es par une mĂ©moire dĂ©fectueuse, etc.) ;
- l'algorithme deflate dans zlib a une implĂ©mentation suffisamment mature et ne devrait pas Ă©chouer avec des donnĂ©es d'entrĂ©e « corrompues », et de plus, il est souvent capable de dĂ©tecter lui-mĂȘme les erreurs dans le flux d'entrĂ©e, rĂ©duisant ainsi la probabilitĂ© globale de non-dĂ©tection d'erreurs (j'ai effectuĂ© un test en inversant un seul bit dans une courte entrĂ©e, zlib a dĂ©tectĂ© l'erreur dans environ un tiers des cas).
Arguments contre le calcul de la somme de contrÎle sur les données non compressées :
- Le CRC est précisément conçu pour un nombre limité d'erreurs de bits, caractéristiques de la mémoire flash (une erreur de bit dans un flux compressé peut entraßner un changement massif du flux de sortie, sur lequel, théoriquement, nous pourrions « attraper » une collision) ;
- je n'aime pas trop l'idée de transmettre des données potentiellement corrompues au décompresseur, , comment il réagira.
Dans ce projet, j'ai décidé de m'éloigner de la pratique courante de stockage de la somme de contrÎle des données non compressées.
Résumé : nous utilisons CRC-32C, et nous calculons la somme de contrÎle à partir des données tel qu'elles sont enregistrées dans la flash (aprÚs compression).
Redondance
L'utilisation de la redondance de code ne permet pas, bien sûr, d'éliminer complÚtement la perte de données, mais elle peut considérablement (souvent de plusieurs ordres de grandeur) réduire la probabilité de pertes de données irrécupérables.
Nous pouvons utiliser différents types de redondance pour corriger les erreurs.
Les codes de Hamming peuvent corriger des erreurs de bit uniques, les codes de Reed-Solomon sont symboliques, et plusieurs copies de donnĂ©es accompagnĂ©es de sommes de contrĂŽle ou un codage tel que RAID-6 peuvent aider Ă rĂ©cupĂ©rer des donnĂ©es mĂȘme en cas de dommages massifs.
Au départ, j'étais en faveur d'une utilisation étendue du codage résistant aux erreurs, mais j'ai ensuite réalisé qu'il fallait d'abord comprendre contre quelles erreurs nous voulons nous protéger, puis choisir le codage.
Nous avons dit prĂ©cĂ©demment qu'il fallait dĂ©tecter les erreurs le plus tĂŽt possible. Ă quels moments pouvons-nous ĂȘtre confrontĂ©s Ă des erreurs ?
- Ăcriture inachevĂ©e (pour une raison quelconque, l'alimentation a Ă©tĂ© coupĂ©e au moment de l'Ă©criture, Raspberry a plantĂ©, âŠ)
Hélas, dans le cas d'une telle erreur, il ne reste qu'à ignorer les enregistrements invalides et à considérer les données comme perdues ; - Erreurs d'écriture (pour une raison quelconque, ce qui a été écrit dans la mémoire flash n'est pas ce qui a été demandé)
Nous pouvons immédiatement détecter de telles erreurs si nous effectuons une lecture de contrÎle juste aprÚs l'écriture ; - Distorsion des données en mémoire au cours du stockage ;
- Erreurs de lecture
Pour la correction, il suffit, en cas de désaccord des sommes de contrÎle, de répéter plusieurs fois la lecture.
Ainsi, seules les erreurs de troisiĂšme type (corruption spontanĂ©e des donnĂ©es au stockage) ne peuvent ĂȘtre corrigĂ©es sans codage rĂ©sistant aux erreurs. Il semble que des erreurs de ce type soient nĂ©anmoins extrĂȘmement peu probables.
Résumé : Il a été décidé d'abandonner le codage redondant, mais si l'exploitation montre que cette décision est erronée, alors il sera nécessaire de revenir sur la question (avec des statistiques accumulées sur les pannes, ce qui permettra de choisir le type de codage optimal).
Autre
Ăvidemment, le format de l'article ne permet pas de justifier chaque bit dans le format (et je suis dĂ©jĂ Ă court de forces), donc je vais briĂšvement couvrir certains points qui n'ont pas Ă©tĂ© abordĂ©s auparavant.
- Il a été décidé de rendre toutes les pages « égales »
Cela signifie qu'il n'y aura pas de pages spéciales avec des métadonnées, des flux distincts, etc., mais un flux unique qui réécrit toutes les pages à tour de rÎle.
Cela garantit une usure uniforme des pages, l'absence d'un point de défaillance unique, et c'est tout simplement agréable. - Il est impératif de prévoir la version du format.
Un format sans numĂ©ro de version dans l'en-tĂȘte est une mauvaise idĂ©e !
Il suffit d'ajouter dans l'en-tĂȘte de la page un champ avec un Magic Number (signature), qui indiquera la version du format utilisĂ©. (je ne pense pas qu'il y en ait mĂȘme une dizaine en pratique); - Utilisez un en-tĂȘte de longueur variable pour les enregistrements (qui sont trĂšs nombreux), en essayant de le faire d'une longueur de 1 octet pour la plupart des cas.
- Pour coder la longueur de l'en-tĂȘte et la longueur de la partie tronquĂ©e de l'enregistrement compressĂ©, utilisez des codes binaires de longueur variable.
A beaucoup aidé de codes de Huffman. En quelques minutes, j'ai pu trouver les codes de longueur variable nécessaires.
Description du format de stockage des données
Ordre des octets
Les champs dont la taille dépasse un octet sont stockés en format big-endian (ordre des octets réseau), c'est-à -dire que 0x1234 est enregistré sous la forme 0x12, 0x34.
Division en pages
Toute la mémoire flash est divisée en pages de taille égale.
La taille de la page par défaut est de 32 Ko, mais pas plus d'un quart de la taille totale de la puce de mémoire (pour une puce de 4 Mo, cela représente 128 pages).
Chaque page stocke des données indépendamment des autres (c'est-à -dire que les données d'une page ne référencent pas les données d'une autre page).
Toutes les pages sont numérotées dans un ordre naturel (dans l'ordre croissant des adresses), en commençant par le numéro 0 (la page zéro commence à l'adresse 0, la premiÚre à 32 Ko, la seconde à 64 Ko, etc.).
La puce de mĂ©moire est utilisĂ©e comme un tampon circulaire (ring buffer), c'est-Ă -dire que l'enregistrement commence par la page numĂ©ro 0, puis va Ă la page numĂ©ro 1, âŠ, lorsque nous remplissons la derniĂšre page, un nouveau cycle commence et l'Ă©criture continue Ă partir de la page zĂ©ro.
à l'intérieur de la page

Au dĂ©but de la page se trouve un en-tĂȘte de 4 octets, puis un contrĂŽle de la somme de contrĂŽle de l'en-tĂȘte (CRC-32C), ensuite les enregistrements sont stockĂ©s au format 'en-tĂȘte, donnĂ©es, somme de contrĂŽle'.
L'en-tĂȘte de la page (en vert sale sur le schĂ©ma) se compose de :
- un champ de deux octets Magic Number (qui est la marque de version du format)
pour la version actuelle du format, il est considĂ©rĂ© comme0xed00 â numĂ©ro de page; - compteur de deux octets « Version de la page » (numĂ©ro de cycle de réécriture mĂ©moire).
Les enregistrements sur la page sont stockĂ©s sous forme compressĂ©e (l'algorithme deflate est utilisĂ©). Tous les enregistrements sur une mĂȘme page sont compressĂ©s dans un seul flux (un dictionnaire commun est utilisĂ©), et Ă chaque nouvelle page, la compression recommence Ă zĂ©ro. Cela signifie que pour dĂ©compresser n'importe quel enregistrement, il faut tous les enregistrements prĂ©cĂ©dents de cette page (et seulement de celle-ci).
Chaque enregistrement sera compressé avec le drapeau Z_SYNC_FLUSH, ce qui laisse à la fin du flux compressé 4 octets 0x00, 0x00, 0xff, 0xff, éventuellement précédés d'un ou deux octets nuls.
Cette séquence (d'une longueur de 4, 5 ou 6 octets) est écartée lors de l'écriture dans la mémoire flash.
L'en-tĂȘte de l'enregistrement se compose de 1, 2 ou 3 octets, stockant :
- un bit (T), indiquant le type d'enregistrement : 0 â contexte, 1 â journal ;
- un champ de longueur variable (S) de 1 Ă 7 bits, dĂ©finissant la longueur de l'en-tĂȘte et le « reste » Ă ajouter Ă l'enregistrement pour le dĂ©ballage ;
- la longueur de l'enregistrement (L).
Tableau des valeurs S :
S
Longueur de l'en-tĂȘte, octets
ĂcartĂ© lors de l'Ă©criture, octet
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)
J'ai essayé d'illustrer, je ne sais pas à quel point c'est clair :

En jaune, le champ T, en blanc, le champ S, en vert L (longueur des données compressées en octets), en bleu, les données compressées, en rouge, les octets finaux des données compressées qui ne sont pas écrits dans la mémoire flash.
Ainsi, les en-tĂȘtes des enregistrements de longueur la plus courante (jusqu'Ă 63+5 octets en version compressĂ©e) peuvent ĂȘtre enregistrĂ©s en un seul octet.
AprÚs chaque enregistrement se trouve un contrÎle de somme CRC-32C, dont la valeur initiale (init) est l'inversion de la somme de contrÎle précédente.
Le CRC possÚde la propriété de « continuité », il agit (plus ou moins l'inversion des bits en cours de processus) selon cette formule :
.
C'est-Ă -dire que nous calculons en fait le CRC de tous les octets prĂ©cĂ©dents des en-tĂȘtes et des donnĂ©es sur cette page.
Juste aprĂšs la somme de contrĂŽle se trouve l'en-tĂȘte du prochain enregistrement.
L'en-tĂȘte est construit de maniĂšre Ă ce que son premier octet soit toujours diffĂ©rent de 0x00 et 0xff (si nous rencontrons 0xff Ă la place du premier octet de l'en-tĂȘte, cela signifie que c'est une zone inutilisĂ©e ; 0x00 signale une erreur).
Algorithmes approximatifs
Lecture à partir de la mémoire flash
Toute lecture se fait avec vérification de la somme de contrÎle.
Si la somme de contrÎle ne correspond pas, la lecture est répétée plusieurs fois dans l'espoir de lire des données correctes.
(cela a du sens, Linux ne met pas en cache la lecture de la mémoire Flash NOR, vérifié)
Ăcriture dans la mĂ©moire Flash
Nous écrivons des données.
Nous les lisons.
Si les données lues ne correspondent pas à celles écrites, nous remplissons la zone de zéros et signalons une erreur.
Préparation d'une nouvelle puce à la fonction
Pour l'initialisation, un en-tĂȘte avec la version 1 est Ă©crit sur la premiĂšre (c'est-Ă -dire la zĂ©ro) page.
AprÚs cela, le contexte initial (contient l'UUID de la machine et les paramÚtres par défaut) est écrit dans cette page.
C'est bon, la mĂ©moire Flash est prĂȘte Ă l'emploi.
Chargement de la machine
Lors du chargement, les premiers 8 octets de chaque page (en-tĂȘte + CRC) sont lus, les pages avec un Magic Number inconnu ou un CRC incorrect sont ignorĂ©es.
Parmi les pages « correctes », celles avec la version maximale sont sélectionnées, et une page ayant le numéro le plus élevé est choisie.
La premiÚre entrée est lue, la validité du CRC et la présence du drapeau « contexte » sont vérifiées. Si tout est normal, cette page est considérée comme actuelle. Sinon, nous revenons à la précédente jusqu'à ce que nous trouvions une page « vivante ».
à la page trouvée, nous lisons toutes les entrées, et celles avec le drapeau « contexte » sont appliquées.
Nous sauvegardons le dictionnaire zlib (il sera nécessaire pour des écritures supplémentaires dans cette page).
C'est bon, le chargement est terminé, le contexte est restauré, nous pouvons travailler.
Ajout d'une entrée au journal
Nous compressons l'entrée avec le bon dictionnaire, en utilisant Z_SYNC_FLUSH. Nous vérifions si l'entrée compressée tient sur la page actuelle.
Si elle ne tient pas (ou s'il y a eu des erreurs de CRC sur la page), nous commençons une nouvelle page (voir ci-dessous).
Nous écrivons l'entrée et le CRC. En cas d'erreur, nous commençons une nouvelle page.
Nouvelle page
Nous choisissons une page libre avec le numĂ©ro minimal (nous considĂ©rons comme libre une page avec une somme de contrĂŽle incorrecte dans l'en-tĂȘte ou une version infĂ©rieure Ă la version actuelle). S'il n'y a pas de telles pages, nous choisissons la page avec le numĂ©ro minimal ayant la version Ă©gale Ă la version actuelle.
Nous mettons la page sélectionnée en mode effacement. Nous comparons le contenu avec 0xff. Si quelque chose ne va pas, nous prenons la page libre suivante, etc.
Sur la page effacĂ©e, nous Ă©crivons l'en-tĂȘte, la premiĂšre entrĂ©e contient l'Ă©tat actuel du contexte, la suivante l'entrĂ©e de journal non Ă©crite (s'il y en a).
Applicabilité du format
Ă mon avis, c'est un bon format pour stocker tout flux d'informations compressibles (texte simple, JSON, MessagePack, CBOR, peut-ĂȘtre protobuf) dans de la NOR Flash.
Bien sûr, le format est « conçu » pour la NOR Flash SLC.
Il ne faut pas l'utiliser avec des supports ayant un BER élevé, comme NAND ou MLC NOR. (y a-t-il de la mémoire de ce type en vente ? Je n'ai vu que des références dans des travaux sur les codes de correction).
D'autant plus qu'il ne faut pas l'utiliser avec des dispositifs ayant leur propre FTL : USB flash, SD, MicroSD, etc. (pour ce type de mĂ©moire, j'ai créé un format avec une taille de page de 512 octets, une signature au dĂ©but de chaque page et des numĂ©ros d'enregistrement uniques â parfois, Ă partir d'une clĂ© USB « dĂ©faillante », il Ă©tait possible de rĂ©cupĂ©rer toutes les donnĂ©es par une simple lecture sĂ©quentielle).
Selon les tĂąches, le format peut ĂȘtre utilisĂ© sans modifications sur des clĂ©s USB de 128Kbit (16Ko) Ă 1Gbit (128Mo). Si dĂ©sirĂ©, il peut Ă©galement ĂȘtre utilisĂ© sur des puces de plus grande capacitĂ©, mais il faudra probablement ajuster la taille de la page. (Mais lĂ se pose dĂ©jĂ la question de la rentabilitĂ© Ă©conomique, le prix de la NOR Flash de grande capacitĂ© n'est pas rĂ©jouissant).
Si ce format semble intĂ©ressant Ă quelqu'un et qu'il souhaite l'utiliser dans un projet open source â Ă©crivez-moi, je ferai de mon mieux pour trouver du temps, peaufiner le code et le publier sur github.
Conclusion
Comme nous le voyons, au final, le format s'est avĂ©rĂ© simple. et mĂȘme ennuyeux..
Dans l'article, il est difficile de reflĂ©ter l'Ă©volution de mon point de vue, mais croyez-moi : au dĂ©part, je voulais crĂ©er quelque chose de complexe, d'invulnĂ©rable, capable de survivre mĂȘme aprĂšs une explosion nuclĂ©aire Ă proximitĂ©. Cependant, la raison (j'espĂšre) a finalement triomphĂ© et mes prioritĂ©s se sont progressivement dĂ©placĂ©es vers la simplicitĂ© et la compacitĂ©.
Est-il possible que je me sois trompé ? Oui, bien sûr. Il se peut par exemple que nous ayons acheté un lot de puces de mauvaise qualité. Ou pour une autre raison, l'équipement ne répondra pas aux attentes en matiÚre de fiabilité.
Ai-je un plan pour ce cas ? Je pense qu'aprĂšs avoir lu l'article, vous ne doutez pas que j'ai un plan. Et mĂȘme plusieurs.
Plus sérieusement, le format a été développé à la fois comme une option de travail et comme un « essai ».
Ă ce jour, tout fonctionne normalement sur la table, littĂ©ralement dans les jours Ă venir, la solution sera dĂ©ployĂ©e. (environ) Sur des centaines d'appareils, voyons comment cela se comportera en « conditions rĂ©elles » (heureusement, j'espĂšre que le format permet de dĂ©tecter fiablement les pannes ; ainsi, je pourrai rassembler des statistiques complĂštes). Dans quelques mois, des conclusions pourront ĂȘtre tirĂ©es. (et si la chance ne sourit pas, alors mĂȘme plus tĂŽt).
Si à l'issue de l'utilisation des problÚmes sérieux apparaissent et que des ajustements sont nécessaires, je n'hésiterai pas à en parler.
Littérature
Je n'avais pas envie de dresser une longue liste ennuyeuse de travaux utilisés, aprÚs tout, tout le monde a Google.
Ici, j'ai décidé de laisser une liste de découvertes que j'ai trouvées particuliÚrement intéressantes, mais progressivement, elles ont été intégrées directement dans le texte de l'article, et il ne reste qu'un seul élément dans la liste :
- Utilitaire de l'auteur zlib. Il peut afficher le contenu des archives deflate/zlib/gzip de maniÚre compréhensible. Si vous devez vous pencher sur la structure interne du format deflate (ou gzip), je le recommande vivement.
Source : habr.com
