La mia implementazione di un ring buffer nella flash NOR

Sfondo

Ci sono distributori automatici di nostra progettazione. All'interno del Raspberry Pi e alcuni cablaggi su una scheda separata. Sono collegati un accettatore di monete, un accettatore di banconote, un terminale bancario... Tutto è controllato da un programma autoscritto. L'intera cronologia lavorativa viene scritta in un registro su un'unità flash (MicroSD), che viene poi trasmessa via Internet (utilizzando un modem USB) al server, dove viene archiviata in un database. Le informazioni sulle vendite vengono caricate in 1c, c'è anche una semplice interfaccia web per il monitoraggio, ecc.

Cioè, il giornale è vitale: per la contabilità (entrate, vendite, ecc.), il monitoraggio (tutti i tipi di guasti e altre circostanze di forza maggiore); Queste, si potrebbe dire, sono tutte le informazioni che abbiamo su questa macchina.

Problema

Le unità flash si dimostrano dispositivi molto inaffidabili. Falliscono con invidiabile regolarità. Ciò porta sia al fermo macchina che (se per qualche motivo non è stato possibile trasferire il registro online) alla perdita di dati.

Questa non è la prima esperienza di utilizzo di unità flash, prima c'era un altro progetto con più di cento dispositivi, in cui la rivista era archiviata su unità flash USB, c'erano anche problemi di affidabilità, a volte il numero di quelli che si sono guastati un mese erano decine. Abbiamo provato diverse unità flash, comprese quelle di marca con memoria SLC, e alcuni modelli sono più affidabili di altri, ma la sostituzione delle unità flash non ha risolto radicalmente il problema.

Attenzione! Leggi a lungo! Se non ti interessa il “perché”, ma solo il “come”, puoi andare dritto alla fine articolo.

Soluzione

La prima cosa che mi viene in mente è: abbandona la MicroSD, installa, ad esempio, un SSD e avvia da esso. Teoricamente possibile, probabilmente, ma relativamente costoso e non così affidabile (viene aggiunto un adattatore USB-SATA; anche le statistiche dei guasti per gli SSD economici non sono incoraggianti).

Anche l'HDD USB non sembra una soluzione particolarmente attraente.

Pertanto, siamo giunti a questa opzione: lasciare l'avvio da MicroSD, ma utilizzarle in modalità di sola lettura e archiviare il registro delle operazioni (e altre informazioni uniche per un particolare componente hardware: numero di serie, calibrazioni del sensore, ecc.) da qualche altra parte .

L'argomento del FS di sola lettura per i lamponi è già stato approfondito, non mi soffermerò sui dettagli di implementazione in questo articolo (ma se c’è interesse magari scrivo un mini-articolo su questo argomento). L'unico punto che vorrei sottolineare è che sia dall'esperienza personale che dalle recensioni di chi l'ha già implementato si nota un guadagno in termini di affidabilità. Sì, è impossibile eliminare completamente i guasti, ma è del tutto possibile ridurne significativamente la frequenza. E le carte stanno diventando unificate, il che rende la sostituzione molto più semplice per il personale di servizio.

La parte hardware

Non c'erano dubbi particolari sulla scelta del tipo di memoria: NOR Flash.
argomenti:

  • connessione semplice (il più delle volte il bus SPI, che hai già esperienza nell'utilizzo, quindi non sono previsti problemi hardware);
  • prezzo ridicolo;
  • protocollo operativo standard (l'implementazione è già nel kernel Linux, se lo desideri puoi prenderne uno di terze parti, che è anche presente, o anche scriverne uno tuo, fortunatamente tutto è semplice);
  • affidabilità e risorse:
    da un tipico datasheet: i dati vengono conservati per 20 anni, 100000 cicli di cancellazione per ogni blocco;
    da fonti di terze parti: BER estremamente basso, non postula la necessità di codici di correzione degli errori (alcuni lavori considerano ECC per NOR, ma di solito significano ancora MLC NOR; succede anche questo).

Stimiamo i requisiti di volume e risorse.

Vorrei che fosse garantita la conservazione dei dati per diversi giorni. Ciò è necessario affinché, in caso di problemi di comunicazione, lo storico delle vendite non vada perso. Ci concentreremo su 5 giorni, durante questo periodo (anche tenendo conto dei fine settimana e dei giorni festivi) il problema può essere risolto.

Attualmente raccogliamo circa 100kb di log al giorno (3-4mila voci), ma gradualmente questa cifra sta crescendo: il dettaglio aumenta, vengono aggiunti nuovi eventi. Inoltre, a volte si verificano dei picchi (ad esempio, alcuni sensori iniziano a inviare spam con falsi positivi). Calcoleremo per 10mila record 100 byte ciascuno - megabyte al giorno.

In totale, escono 5 MB di dati puliti (ben compressi). Di più per loro (dare una stima grezza) 1 MB di dati di servizio.

Cioè, abbiamo bisogno di un chip da 8 MB se non utilizziamo la compressione, o da 4 MB se la utilizziamo. Numeri abbastanza realistici per questo tipo di memoria.

Per quanto riguarda la risorsa: se pianifichiamo che l'intera memoria venga riscritta non più di una volta ogni 5 giorni, in 10 anni di servizio otteniamo meno di mille cicli di riscrittura.
Lascia che ti ricordi che il produttore ne promette centomila.

Un po' di NOR vs NAND

Oggi, ovviamente, la memoria NAND è molto più apprezzata, ma non la userei per questo progetto: la NAND, a differenza della NOR, richiede necessariamente l'uso di codici di correzione degli errori, una tabella di blocchi danneggiati, ecc., e anche le gambe di I chip NAND di solito sono molto di più.

Gli svantaggi del NOR includono:

  • piccolo volume (e, di conseguenza, prezzo elevato per megabyte);
  • bassa velocità di comunicazione (in gran parte dovuta al fatto che viene utilizzata un'interfaccia seriale, solitamente SPI o I2C);
  • cancellazione lenta (a seconda della dimensione del blocco, richiede da una frazione di secondo a diversi secondi).

Sembra che non ci sia nulla di critico per noi, quindi continuiamo.

Se i dettagli sono interessanti, il microcircuito è stato selezionato at25df321a (Tuttavia questo non è importante, ci sono molti analoghi sul mercato che sono compatibili nella piedinatura e nel sistema di comando; anche se vogliamo installare un microcircuito di un produttore diverso e/o di dimensioni diverse, tutto funzionerà senza modificare il codice).

Utilizzo il driver integrato nel kernel Linux; su Raspberry, grazie al supporto dell'overlay dell'albero dei dispositivi, tutto è molto semplice: è necessario inserire l'overlay compilato in /boot/overlays e modificare leggermente /boot/config.txt.

File dts di esempio

Ad essere sincero, non sono sicuro che sia scritto senza errori, ma funziona.

/*
 * 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?";
    };
};

E un'altra riga in config.txt

dtoverlay=at25:spimaxfrequency=50000000

Ometterò la descrizione del collegamento del chip al Raspberry Pi. Da un lato non sono un esperto di elettronica, dall'altro qui tutto è banale anche per me: il microcircuito ha solo 8 gambe, di cui abbiamo bisogno di terra, potenza, SPI (CS, SI, SO, SCK ); i livelli sono gli stessi del Raspberry Pi, non è necessario alcun cablaggio aggiuntivo, basta collegare i 6 pin indicati.

Formulazione del problema

Come al solito, la dichiarazione del problema passa attraverso diverse iterazioni e mi sembra che sia giunto il momento per quella successiva. Allora fermiamoci, mettiamo insieme quanto già scritto, e chiariamo i dettagli che restano nell'ombra.

Pertanto, abbiamo deciso che il registro verrà archiviato in SPI NOR Flash.

Cos'è NOR Flash per chi non lo sa?

Si tratta di memoria non volatile con la quale è possibile effettuare tre operazioni:

  1. lettura:
    La lettura più comune: trasmettiamo l'indirizzo e leggiamo tutti i byte di cui abbiamo bisogno;
  2. Запись:
    La scrittura su flash NOR sembra normale, ma ha una particolarità: puoi cambiare solo 1 in 0, ma non viceversa. Ad esempio, se avessimo 0x55 in una cella di memoria, dopo aver scritto 0x0f su di essa, 0x05 sarà già memorizzato lì (vedi tabella appena sotto);
  3. Cancellare:
    Naturalmente, dobbiamo essere in grado di fare l'operazione opposta: cambiare 0 in 1, questo è esattamente lo scopo dell'operazione di cancellazione. A differenza dei primi due, non funziona con byte, ma con blocchi (il blocco minimo di cancellazione nel chip selezionato è 4kb). La cancellazione distrugge l'intero blocco ed è l'unico modo per cambiare 0 in 1. Pertanto, quando si lavora con la memoria flash, spesso è necessario allineare le strutture dei dati al limite del blocco di cancellazione.
    Registrazione in Flash NOR:

Dati binari

è stato
01010101

Registrato
00001111

E 'diventato
00000101

Il log stesso rappresenta una sequenza di record di lunghezza variabile. La lunghezza tipica di un record è di circa 30 byte (anche se a volte si verificano record lunghi diversi kilobyte). In questo caso li utilizziamo semplicemente come un insieme di byte ma, se sei interessato, all'interno dei record viene utilizzato CBOR

Oltre al registro, dobbiamo memorizzare alcune informazioni di “impostazione”, sia aggiornate che non: un determinato ID dispositivo, calibrazioni del sensore, un flag “dispositivo temporaneamente disabilitato”, ecc.
Queste informazioni sono un insieme di record di valori-chiave, anch'essi archiviati in CBOR. Non disponiamo di molte di queste informazioni (al massimo pochi kilobyte) e vengono aggiornate raramente.
Nel seguito lo chiameremo contesto.

Se ricordiamo da dove è iniziato questo articolo, è molto importante garantire un'archiviazione affidabile dei dati e, se possibile, un funzionamento continuo anche in caso di guasti hardware/corruzione dei dati.

Quali fonti di problemi possono essere prese in considerazione?

  • Spegnere durante le operazioni di scrittura/cancellazione. Questo appartiene alla categoria "non esiste alcun trucco contro il piede di porco".
    Informazioni da discussioni su stackexchange: quando l'alimentazione viene spenta mentre si lavora con la flash, sia la cancellazione (impostata su 1) che la scrittura (impostata su 0) portano a un comportamento indefinito: i dati possono essere scritti, scritti parzialmente (ad esempio, abbiamo trasferito 10 byte/80 bit , ma non si possono scrivere ancora solo 45 bit), è anche possibile che alcuni bit si trovino in uno stato “intermedio” (la lettura può produrre sia 0 che 1);
  • Errori nella memoria flash stessa.
    Il BER, pur essendo molto basso, non può essere pari a zero;
  • Errori dell'autobus
    I dati trasmessi tramite SPI non sono protetti in alcun modo; possono verificarsi errori di bit singoli ed errori di sincronizzazione: perdita o inserimento di bit (che porta a una massiccia corruzione dei dati);
  • Altri errori/problemi
    Errori nel codice, problemi del Raspberry, interferenze aliene...

Ho formulato i requisiti, il cui adempimento, a mio avviso, è necessario per garantire l'affidabilità:

  • i record devono entrare immediatamente nella memoria flash, le scritture ritardate non vengono considerate; - se si verifica un errore, deve essere rilevato ed elaborato il prima possibile; - il sistema deve, se possibile, riprendersi dagli errori.
    (un esempio dalla vita "come non dovrebbe essere", che penso tutti abbiano riscontrato: dopo un riavvio di emergenza, il file system è "rotto" e il sistema operativo non si avvia)

Idee, approcci, riflessioni

Quando ho iniziato a pensare a questo problema, mi sono balenate in testa molte idee, ad esempio:

  • utilizzare la compressione dei dati;
  • utilizzare strutture dati intelligenti, ad esempio memorizzando le intestazioni dei record separatamente dai record stessi, in modo che se c'è un errore in qualsiasi record, puoi leggere il resto senza problemi;
  • utilizzare campi bit per controllare il completamento della registrazione quando l'alimentazione è spenta;
  • archiviare i checksum per tutto;
  • utilizzare un qualche tipo di codifica resistente al rumore.

Alcune di queste idee furono utilizzate, mentre altre si decise di abbandonarle. Andiamo con ordine.

Compressione dei dati

Gli eventi stessi che registriamo nel diario sono abbastanza simili e ripetibili (“ha lanciato una moneta da 5 rubli”, “ha premuto il pulsante per dare il resto”, ...). Pertanto, la compressione dovrebbe essere abbastanza efficace.

Il sovraccarico di compressione è insignificante (il nostro processore è piuttosto potente, anche il primo Pi aveva un core con una frequenza di 700 MHz, i modelli attuali hanno diversi core con una frequenza superiore a un gigahertz), il rapporto con lo storage è basso (diversi megabyte al secondo), la dimensione dei record è piccola. In generale, se la compressione ha un impatto sulle prestazioni, sarà solo positivo. (assolutamente acritico, sto solo affermando). Inoltre, non abbiamo un vero Linux incorporato, ma normale, quindi l'implementazione non dovrebbe richiedere molto sforzo (è sufficiente collegare semplicemente la libreria e utilizzare diverse funzioni da essa).

Una parte del registro è stata presa da un dispositivo funzionante (1.7 MB, 70mila voci) e ne è stata prima verificata la compressibilità utilizzando gzip, lz4, lzop, bzip2, xz, zstd disponibili sul computer.

  • gzip, xz, zstd hanno mostrato risultati simili (40Kb).
    Sono rimasto sorpreso dal fatto che l'xz alla moda si sia mostrato qui al livello di gzip o zstd;
  • lzip con le impostazioni predefinite ha dato risultati leggermente peggiori;
  • lz4 e lzop hanno mostrato risultati non molto buoni (150Kb);
  • bzip2 ha mostrato un risultato sorprendentemente buono (18Kb).

Quindi, i dati sono compressi molto bene.
Quindi (se non troviamo difetti fatali) ci sarà compressione! Semplicemente perché sulla stessa unità flash possono stare più dati.

Pensiamo agli svantaggi.

Primo problema: abbiamo già concordato che ogni record debba andare immediatamente in flash. In genere, l'archiviatore raccoglie i dati dal flusso di input finché non decide che è ora di scrivere nel fine settimana. Dobbiamo ricevere immediatamente un blocco di dati compresso e archiviarlo nella memoria non volatile.

Vedo tre modi:

  1. Comprimi ogni record utilizzando la compressione del dizionario invece degli algoritmi discussi sopra.
    È un'opzione completamente funzionante, ma non mi piace. Per garantire un livello di compressione più o meno dignitoso, il dizionario deve essere “adattato” ai dati specifici; qualsiasi modifica porterà a una catastrofica caduta del livello di compressione. Sì, il problema può essere risolto creando una nuova versione del dizionario, ma questo è un grattacapo: dovremo archiviare tutte le versioni del dizionario; in ogni voce dovremo indicare con quale versione del dizionario è stata compressa...
  2. Comprimi ogni record utilizzando algoritmi “classici”, ma indipendentemente dagli altri.
    Gli algoritmi di compressione in esame non sono progettati per funzionare con record di queste dimensioni (decine di byte), il rapporto di compressione sarà chiaramente inferiore a 1 (ovvero aumenterà il volume dei dati invece di comprimerli);
  3. Eseguire FLUSH dopo ogni registrazione.
    Molte librerie di compressione supportano FLUSH. Si tratta di un comando (o un parametro della procedura di compressione), alla ricezione del quale l'archiviatore forma un flusso compresso in modo che possa essere utilizzato per ripristinare tutti dati non compressi già ricevuti. Un simile analogo sync nei file system o commit in mq.
    Ciò che è importante è che le successive operazioni di compressione potranno utilizzare il dizionario accumulato e il rapporto di compressione non ne risentirà tanto quanto nella versione precedente.

Penso che sia ovvio che ho scelto la terza opzione, vediamola più nel dettaglio.

Trovato ottimo articolo su FLUSH in zlib.

Ho fatto un test del ginocchio basato sull'articolo, ho preso 70mila voci di registro da un dispositivo reale, con una dimensione di pagina di 60Kb (torneremo sulle dimensioni della pagina più tardi) ricevuto:

Dati iniziali
Compressione gzip -9 (no FLUSH)
zlib con Z_PARTIAL_FLUSH
zlib con Z_SYNC_FLUSH

Volume, KB
1692
40
352
604

A prima vista, il prezzo apportato da FLUSH è eccessivamente alto, ma in realtà non abbiamo molta scelta: non comprimere affatto o comprimere (e in modo molto efficace) con FLUSH. Non dobbiamo dimenticare che abbiamo 70mila record, la ridondanza introdotta da Z_PARTIAL_FLUSH è di soli 4-5 byte per record. E il rapporto di compressione si è rivelato quasi 5:1, il che è un risultato più che eccellente.

Potrebbe essere una sorpresa, ma Z_SYNC_FLUSH è in realtà un modo più efficiente per eseguire FLUSH

Quando si utilizza Z_SYNC_FLUSH, gli ultimi 4 byte di ciascuna voce saranno sempre 0x00, 0x00, 0xff, 0xff. E se li conosciamo, non dobbiamo memorizzarli, quindi la dimensione finale è di soli 324 KB.

L'articolo che ho linkato ha una spiegazione:

Viene aggiunto un nuovo blocco di tipo 0 con contenuto vuoto.

Un blocco di tipo 0 con contenuto vuoto è costituito da:

  • l'intestazione del blocco a tre bit;
  • Da 0 a 7 bit uguali a zero, per ottenere l'allineamento dei byte;
  • la sequenza di quattro byte 00 00 FF FF.

Come puoi facilmente vedere, nell'ultimo blocco prima di questi 4 byte ci sono da 3 a 10 bit di zero. Tuttavia, la pratica ha dimostrato che in realtà ci sono almeno 10 bit zero.

Si scopre che blocchi di dati così brevi sono solitamente (sempre?) codificati utilizzando un blocco di tipo 1 (blocco fisso), che termina necessariamente con 7 bit zero, per un totale di 10-17 bit zero garantiti (e il resto verrà essere zero con una probabilità di circa il 50%).

Quindi, sui dati di test, nel 100% dei casi c'è un byte zero prima di 0x00, 0x00, 0xff, 0xff e in più di un terzo dei casi ci sono due byte zero (forse il fatto è che utilizzo CBOR binario e quando utilizzo JSON di testo, i blocchi di tipo 2 - blocco dinamico sarebbero più comuni, rispettivamente, si incontrerebbero blocchi senza byte zero aggiuntivi prima di 0x00, 0x00, 0xff, 0xff).

In totale, utilizzando i dati di test disponibili, è possibile inserire meno di 250 KB di dati compressi.

Puoi risparmiare un po' di più facendo il bit juggling: per ora ignoriamo la presenza di alcuni bit zero alla fine del blocco, anche alcuni bit all'inizio del blocco non cambiano...
Ma poi ho deciso con fermezza di fermarmi, altrimenti di questo passo avrei potuto finire per sviluppare il mio archiviatore.

In totale, dai miei dati di test ho ricevuto 3-4 byte per scrittura, il rapporto di compressione è risultato superiore a 6:1. Sarò sincero: non mi aspettavo un risultato del genere; secondo me, qualcosa di meglio di 2:1 è già un risultato che giustifica l’uso della compressione.

Va tutto bene, ma zlib (deflate) è ancora un algoritmo di compressione arcaico, meritato e un po' antiquato. Il semplice fatto che gli ultimi 32Kb del flusso di dati non compressi vengano utilizzati come dizionario oggi sembra strano (vale a dire, se qualche blocco di dati è molto simile a quello che era nel flusso di input 40Kb fa, allora inizierà ad essere archiviato di nuovo, e non farà riferimento a un evento precedente). Negli archiviatori moderni e alla moda, la dimensione del dizionario è spesso misurata in megabyte anziché in kilobyte.

Continuiamo quindi il nostro mini-studio sugli archiviatori.

Successivamente abbiamo testato bzip2 (ricordate, senza FLUSH mostrava un fantastico rapporto di compressione di quasi 100:1). Sfortunatamente, con FLUSH le prestazioni sono state pessime; la dimensione dei dati compressi si è rivelata maggiore di quella dei dati non compressi.

Le mie ipotesi sui motivi del fallimento

Libbz2 offre solo un'opzione flush, che sembra cancellare il dizionario (analoga a Z_FULL_FLUSH in zlib); dopo di ciò non si parla di alcuna compressione efficace.

E l'ultimo ad essere testato è stato zstd. A seconda dei parametri, si comprime al livello di gzip, ma molto più velocemente, o meglio di gzip.

Purtroppo con FLUSH non ha funzionato molto bene: la dimensione dei dati compressi era di circa 700Kb.

Я fatto una domanda sulla pagina github del progetto, ho ricevuto una risposta che dovresti contare fino a 10 byte di dati di servizio per ogni blocco di dati compressi, che è vicino ai risultati ottenuti; non c'è modo di recuperare il ritardo con deflate.

Ho deciso di fermarmi a questo punto nei miei esperimenti con gli archiviatori (permettetemi di ricordarvi che xz, lzip, lzo, lz4 non si sono mostrati nemmeno in fase di test senza FLUSH, e non ho considerato algoritmi di compressione più esotici).

Torniamo ai problemi di archiviazione.

Il secondo problema (come si dice in ordine, non in valore) è che i dati compressi costituiscono un unico flusso, in cui ci sono costantemente riferimenti alle sezioni precedenti. Pertanto, se una sezione di dati compressi viene danneggiata, perdiamo non solo il blocco di dati non compressi associato, ma anche tutti quelli successivi.

Esiste un approccio per risolvere questo problema:

  1. Previeni il verificarsi del problema: aggiungi ridondanza ai dati compressi, che ti consentirà di identificare e correggere gli errori; ne parleremo più tardi;
  2. Minimizzare le conseguenze in caso di problemi
    Abbiamo già detto in precedenza che è possibile comprimere ciascun blocco di dati in modo indipendente e il problema scomparirà da solo (il danneggiamento dei dati di un blocco porterà alla perdita dei dati solo per questo blocco). Tuttavia, questo è un caso estremo in cui la compressione dei dati sarà inefficace. L'estremo opposto: utilizzare tutti i 4 MB del nostro chip come un unico archivio, il che ci darà un'eccellente compressione, ma conseguenze catastrofiche in caso di corruzione dei dati.
    Sì, è necessario un compromesso in termini di affidabilità. Ma dobbiamo ricordare che stiamo sviluppando un formato di archiviazione dati per memoria non volatile con BER estremamente basso e un periodo di archiviazione dati dichiarato di 20 anni.

Durante gli esperimenti ho scoperto che perdite più o meno evidenti nel livello di compressione iniziano su blocchi di dati compressi di dimensione inferiore a 10 KB.
Si è detto in precedenza che la memoria utilizzata è paginata; non vedo motivo per cui non si debba utilizzare la corrispondenza “una pagina - un blocco di dati compressi”.

Cioè, la dimensione minima ragionevole della pagina è 16Kb (con una riserva per le informazioni di servizio). Tuttavia, una dimensione di pagina così piccola impone restrizioni significative sulla dimensione massima del record.

Anche se non mi aspetto ancora record più grandi di qualche kilobyte in formato compresso, ho deciso di utilizzare pagine da 32Kb (per un totale di 128 pagine per chip).

Sommario:

  • Archiviamo i dati compressi utilizzando zlib (deflate);
  • Per ogni voce impostiamo Z_SYNC_FLUSH;
  • Per ogni record compresso, tagliamo i byte finali (ad esempio 0x00, 0x00, 0xff, 0xff); nell'intestazione indichiamo quanti byte tagliamo;
  • Memorizziamo i dati in pagine da 32Kb; c'è un unico flusso di dati compressi all'interno della pagina; Su ogni pagina ricominciamo la compressione.

E, prima di concludere con la compressione, vorrei attirare la vostra attenzione sul fatto che abbiamo solo pochi byte di dati compressi per record, quindi è estremamente importante non gonfiare le informazioni di servizio, qui ogni byte conta.

Memorizzazione delle intestazioni dei dati

Poiché abbiamo record di lunghezza variabile, dobbiamo in qualche modo determinare il posizionamento/confini dei record.

Conosco tre approcci:

  1. Tutti i record vengono archiviati in un flusso continuo, prima c'è un'intestazione del record contenente la lunghezza, quindi il record stesso.
    In questa forma di realizzazione, sia le intestazioni che i dati possono avere lunghezza variabile.
    In sostanza, otteniamo un elenco collegato singolarmente che viene utilizzato sempre;
  2. Le intestazioni e i record stessi vengono archiviati in flussi separati.
    Utilizzando intestazioni di lunghezza costante, garantiamo che il danneggiamento di un'intestazione non influenzi le altre.
    Un approccio simile viene utilizzato, ad esempio, in molti file system;
  3. I record vengono archiviati in un flusso continuo, il confine del record è determinato da un determinato contrassegno (un carattere/una sequenza di caratteri vietati all'interno dei blocchi di dati). Se è presente un marcatore all'interno del record, lo sostituiamo con una sequenza (escape).
    Un approccio simile viene utilizzato, ad esempio, nel protocollo PPP.

Illustrerò.

Opzione 1:
La mia implementazione di un ring buffer nella flash NOR
Qui tutto è molto semplice: conoscendo la lunghezza del record, possiamo calcolare l'indirizzo dell'intestazione successiva. Quindi ci muoviamo tra le intestazioni finché non incontriamo un'area riempita con 0xff (area libera) o la fine della pagina.

Opzione 2:
La mia implementazione di un ring buffer nella flash NOR
A causa della lunghezza variabile dei record, non possiamo dire in anticipo quanti record (e quindi intestazioni) avremo bisogno per pagina. Puoi distribuire le intestazioni e i dati stessi su pagine diverse, ma preferisco un approccio diverso: posizioniamo sia le intestazioni che i dati su una pagina, ma le intestazioni (di dimensione costante) provengono dall'inizio della pagina e il i dati (di lunghezza variabile) provengono dalla fine. Non appena si “incontrano” (non c'è abbastanza spazio libero per una nuova voce), consideriamo questa pagina completa.

Opzione 3:
La mia implementazione di un ring buffer nella flash NOR
Non è necessario memorizzare la lunghezza o altre informazioni sulla posizione dei dati nell'intestazione; sono sufficienti i contrassegni che indicano i confini dei record. Tuttavia, i dati devono essere elaborati durante la scrittura/lettura.
Utilizzerei 0xff come indicatore (che riempie la pagina dopo la cancellazione), quindi l'area libera non verrà sicuramente trattata come dati.

Tavola di comparazione:

opzione 1
opzione 2
opzione 3

Tolleranza all'errore
-
+
+

densità
+
-
+

Complessità di implementazione
*
**
**

L'opzione 1 ha un difetto fatale: se una qualsiasi delle intestazioni viene danneggiata, l'intera catena successiva viene distrutta. Le restanti opzioni consentono di recuperare alcuni dati anche in caso di danni ingenti.
Ma qui è opportuno ricordare che abbiamo deciso di memorizzare i dati in forma compressa, e quindi perdiamo tutti i dati della pagina dopo un record “rotto”, quindi anche se nella tabella c'è un meno, non tenerne conto.

Compattezza:

  • nella prima opzione dobbiamo memorizzare solo la lunghezza nell'intestazione, se utilizziamo numeri interi a lunghezza variabile, nella maggior parte dei casi possiamo cavarcela con un byte;
  • nella seconda opzione dobbiamo memorizzare l'indirizzo iniziale e la lunghezza; il record deve avere una dimensione costante, stimo 4 byte per record (due byte per l'offset e due byte per la lunghezza);
  • la terza opzione necessita di un solo carattere per indicare l'inizio della registrazione, inoltre la registrazione stessa aumenterà dell'1-2% a causa della schermatura. In generale, approssimativamente la parità con la prima opzione.

Inizialmente, consideravo la seconda opzione come quella principale (e ne scrivevo anche l'implementazione). L'ho abbandonato solo quando ho finalmente deciso di utilizzare la compressione.

Forse un giorno userò ancora un'opzione simile. Ad esempio, se dovessi occuparmi dell'archiviazione dei dati per una nave che viaggia tra la Terra e Marte, ci saranno requisiti completamente diversi in termini di affidabilità, radiazione cosmica, ...

Per quanto riguarda la terza opzione: ho dato due stelle per la difficoltà di implementazione semplicemente perché non mi piace scherzare con la schermatura, cambiare la lunghezza nel processo, ecc. Sì, forse sono di parte, ma dovrò scrivere il codice: perché costringerti a fare qualcosa che non ti piace.

Sommario: Scegliamo l'opzione di archiviazione sotto forma di catene "intestazione con lunghezza - dati di lunghezza variabile" per l'efficienza e la facilità di implementazione.

Utilizzo dei campi bit per monitorare il successo delle operazioni di scrittura

Non ricordo ora da dove mi è venuta l'idea, ma assomiglia a qualcosa del genere:
Per ogni voce, assegniamo diversi bit per memorizzare i flag.
Come abbiamo detto prima, dopo la cancellazione tutti i bit vengono riempiti con 1 e possiamo cambiare 1 in 0, ma non viceversa. Quindi per “la bandiera non è impostata” usiamo 1, per “la bandiera è impostata” usiamo 0.

Ecco come potrebbe apparire l'inserimento di un record di lunghezza variabile in Flash:

  1. Impostare il flag “è iniziata la registrazione della durata”;
  2. Registra la durata;
  3. Mettere il flag “registrazione dati iniziata”;
  4. Registriamo i dati;
  5. Imposta il flag “registrazione terminata”.

Inoltre, avremo un flag di “errore verificatosi”, per un totale di flag da 4 bit.

In questo caso abbiamo due stati stabili “1111” - la registrazione non è iniziata e “1000” - la registrazione ha avuto successo; in caso di interruzione inaspettata del processo di registrazione, riceveremo stati intermedi, che potremo quindi rilevare ed elaborare.

L'approccio è interessante, ma protegge solo da improvvise interruzioni di corrente e guasti simili, il che, ovviamente, è importante, ma questo non è l'unico (o addirittura il principale) motivo di possibili guasti.

Sommario: Andiamo avanti alla ricerca di una buona soluzione.

Checksum

I checksum consentono inoltre di essere sicuri (con ragionevole probabilità) di leggere esattamente ciò che avrebbe dovuto essere scritto. E, a differenza dei campi di bit discussi sopra, funzionano sempre.

Se consideriamo l'elenco delle potenziali fonti di problemi di cui abbiamo discusso sopra, il checksum è in grado di riconoscere un errore indipendentemente dalla sua origine (tranne, forse, per gli alieni malintenzionati: anche loro possono falsificare il checksum).

Quindi, se il nostro obiettivo è verificare che i dati siano intatti, i checksum sono un’ottima idea.

La scelta dell'algoritmo per il calcolo del checksum non ha sollevato dubbi - CRC. Da un lato, le proprietà matematiche consentono di individuare alcuni tipi di errori al 100%; dall'altro, su dati casuali questo algoritmo mostra solitamente la probabilità di collisioni non molto superiore al limite teorico La mia implementazione di un ring buffer nella flash NOR. Potrebbe non essere l'algoritmo più veloce, né sempre il minimo in termini di numero di collisioni, ma ha una qualità molto importante: nei test che ho riscontrato, non c'erano schemi in cui falliva chiaramente. La stabilità è la qualità principale in questo caso.

Esempio di studio volumetrico: parte 1, parte 2 (link a narod.ru, scusate).

Tuttavia, il compito di selezionare un checksum non è completo; CRC è un'intera famiglia di checksum. Devi decidere la lunghezza e quindi scegliere un polinomio.

Scegliere la lunghezza del checksum non è una questione così semplice come sembra a prima vista.

Lasciatemi illustrare:
Consideriamo la probabilità di un errore in ciascun byte La mia implementazione di un ring buffer nella flash NOR e un checksum ideale, calcoliamo il numero medio di errori per milione di record:

Dati, byte
Somma di controllo, byte
Errori non rilevati
Rilevamenti di falsi errori
Falsi positivi totali

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

Sembrerebbe che tutto sia semplice - a seconda della lunghezza dei dati da proteggere, scegli la lunghezza del checksum con un minimo di positivi errati - e il trucco è nella borsa.

Tuttavia, sorge un problema con checksum brevi: sebbene siano bravi a rilevare errori a bit singolo, possono con una probabilità abbastanza alta accettare dati completamente casuali come corretti. C'era già un articolo su Habré che lo descriveva problema nella vita reale.

Pertanto, per rendere quasi impossibile una corrispondenza casuale del checksum, è necessario utilizzare checksum di lunghezza pari o superiore a 32 bit. (per lunghezze superiori a 64 bit, vengono generalmente utilizzate le funzioni hash crittografiche).

Nonostante abbia scritto prima che dobbiamo assolutamente risparmiare spazio, utilizzeremo comunque un checksum a 32 bit (16 bit non sono sufficienti, la probabilità di una collisione è superiore allo 0.01%; e 24 bit, poiché diciamo, non sono né qui né là).

Qui potrebbe sorgere un'obiezione: abbiamo salvato ogni byte quando abbiamo scelto la compressione per dare ora 4 byte contemporaneamente? Non sarebbe meglio non comprimere o aggiungere un checksum? Ovviamente no, nessuna compressione non significa, che non abbiamo bisogno del controllo dell'integrità.

Quando scegliamo un polinomio, non reinventeremo la ruota, ma prenderemo l'ormai popolare CRC-32C.
Questo codice rileva errori a 6 bit su pacchetti fino a 22 byte (forse il caso più comune per noi), errori a 4 bit su pacchetti fino a 655 byte (anche un caso comune per noi), 2 o qualsiasi numero dispari di errori bit sui pacchetti di qualsiasi lunghezza ragionevole.

Se qualcuno è interessato ai dettagli

Articolo di Wikipedia riguardo al CRC.

Parametri del codice crc-32c su Sito web di Koopman - forse il principale specialista CRC del pianeta.

В il suo articolo c'è un altro codice interessante, che fornisce parametri leggermente migliori per la lunghezza dei pacchetti che sono rilevanti per noi, ma non ho considerato la differenza significativa ed ero abbastanza competente da scegliere un codice personalizzato invece di quello standard e ben studiato.

Inoltre, poiché i nostri dati sono compressi, sorge la domanda: dovremmo calcolare il checksum dei dati compressi o non compressi?

Argomenti a favore del calcolo del checksum dei dati non compressi:

  • In definitiva dobbiamo verificare la sicurezza dell'archiviazione dei dati, quindi la controlliamo direttamente (allo stesso tempo verranno controllati eventuali errori nell'implementazione della compressione/decompressione, danni causati da memoria rotta, ecc.);
  • L'algoritmo di deflazione in zlib ha un'implementazione abbastanza matura e non dovrebbe cadere con dati di input “storti”; inoltre, è spesso in grado di rilevare autonomamente errori nel flusso di input, riducendo la probabilità complessiva di non rilevare un errore (effettuato un test con inversione di un singolo bit in un breve record, zlib ha rilevato un errore in circa un terzo dei casi).

Argomenti contro il calcolo del checksum dei dati non compressi:

  • CRC è "su misura" appositamente per i pochi errori di bit caratteristici della memoria flash (un errore di bit in un flusso compresso può causare un cambiamento enorme nel flusso di output, sul quale, in teoria, possiamo "rilevare" una collisione);
  • Non mi piace molto l'idea di passare dati potenzialmente danneggiati al decompressore, chi lo conoscecome reagirà.

In questo progetto ho deciso di discostarmi dalla pratica generalmente accettata di archiviare un checksum di dati non compressi.

Sommario: Usiamo CRC-32C, calcoliamo il checksum dai dati nella forma in cui sono scritti su flash (dopo la compressione).

Ridondanza

L'uso della codifica ridondante, ovviamente, non elimina la perdita di dati, tuttavia può ridurre significativamente (spesso di molti ordini di grandezza) la probabilità di una perdita di dati irrecuperabile.

Possiamo utilizzare diversi tipi di ridondanza per correggere gli errori.
I codici Hamming possono correggere errori a bit singolo, i codici dei caratteri Reed-Solomon, più copie di dati combinate con checksum o codifiche come RAID-6 possono aiutare a recuperare i dati anche in caso di corruzione massiccia.
Inizialmente ero impegnato nell’uso diffuso della codifica resistente agli errori, ma poi ho capito che bisogna prima avere un’idea di quali errori vogliamo proteggerci, e poi scegliere la codifica.

Abbiamo detto in precedenza che gli errori devono essere individuati il ​​più rapidamente possibile. In quali punti possiamo riscontrare errori?

  1. Registrazione incompiuta (per qualche motivo al momento della registrazione l'alimentazione era spenta, il Raspberry si è bloccato, ...)
    Purtroppo, in caso di errore del genere, non resta che ignorare i record non validi e considerare i dati persi;
  2. Errori di scrittura (per qualche motivo, ciò che è stato scritto nella memoria flash non era ciò che è stato scritto)
    Possiamo rilevare immediatamente tali errori se eseguiamo un test di lettura subito dopo la registrazione;
  3. Distorsione dei dati in memoria durante la memorizzazione;
  4. Errori di lettura
    Per correggerlo, se il checksum non corrisponde, è sufficiente ripetere la lettura più volte.

Ciò significa che solo gli errori del terzo tipo (corruzione spontanea dei dati durante la memorizzazione) non possono essere corretti senza una codifica resistente agli errori. Sembra che tali errori siano ancora estremamente improbabili.

Sommario: si è deciso di abbandonare la codifica ridondante, ma se l'operazione mostra l'errore di questa decisione, si torna a considerare il problema (con statistiche già accumulate sui guasti, che consentiranno di scegliere il tipo di codifica ottimale).

altro

Naturalmente, il formato dell'articolo non ci consente di giustificare ogni parte del formato (e le mie forze sono già finite), quindi esaminerò brevemente alcuni punti non toccati prima.

  • Si è deciso di rendere tutte le pagine “uguali”
    Cioè non ci saranno pagine speciali con metadati, thread separati, ecc., ma invece un singolo thread che riscrive tutte le pagine a turno.
    Ciò garantisce un'usura uniforme delle pagine, nessun singolo punto di guasto, e mi piace proprio;
  • È imperativo fornire il controllo delle versioni del formato.
    Un formato senza un numero di versione nell'intestazione è dannoso!
    Basta aggiungere all'intestazione della pagina un campo con un certo numero magico (firma), che indicherà la versione del formato utilizzato (Non credo che in pratica ce ne saranno nemmeno una dozzina);
  • Utilizzare un'intestazione di lunghezza variabile per i record (ce ne sono molti), cercando di renderla lunga 1 byte nella maggior parte dei casi;
  • Per codificare la lunghezza dell'intestazione e la lunghezza della parte tagliata del record compresso, utilizzare codici binari a lunghezza variabile.

Aiutato molto generatore in linea Codici Huffman. In pochi minuti siamo riusciti a selezionare i codici a lunghezza variabile richiesti.

Descrizione del formato di archiviazione dei dati

Ordine dei byte

I campi più grandi di un byte vengono archiviati in formato big-endian (ordine dei byte di rete), ovvero 0x1234 viene scritto come 0x12, 0x34.

Impaginazione

Tutta la memoria flash è divisa in pagine di uguale dimensione.

La dimensione predefinita della pagina è 32Kb, ma non più di 1/4 della dimensione totale del chip di memoria (per un chip da 4MB si ottengono 128 pagine).

Ogni pagina memorizza i dati indipendentemente dalle altre (ovvero, i dati su una pagina non fanno riferimento ai dati su un'altra pagina).

Tutte le pagine sono numerate in ordine naturale (in ordine crescente di indirizzi), a partire dal numero 0 (la pagina zero inizia dall'indirizzo 0, la prima pagina inizia da 32Kb, la seconda pagina inizia da 64Kb, ecc.)

Il chip di memoria viene utilizzato come buffer ciclico (buffer ad anello), ovvero prima la scrittura va alla pagina numero 0, poi alla numero 1, ..., quando riempiamo l'ultima pagina, inizia un nuovo ciclo e la registrazione continua dalla pagina zero .

All'interno della pagina

La mia implementazione di un ring buffer nella flash NOR
All'inizio della pagina viene memorizzata un'intestazione di pagina da 4 byte, quindi un checksum dell'intestazione (CRC-32C), quindi i record vengono archiviati nel formato "intestazione, dati, checksum".

Il titolo della pagina (verde sporco nel diagramma) è composto da:

  • campo Magic Number a due byte (anche segno della versione del formato)
    per la versione corrente del formato viene calcolato come 0xed00 ⊕ номер страницы;
  • contatore a due byte “Versione pagina” (numero del ciclo di riscrittura della memoria).

Le voci sulla pagina vengono archiviate in formato compresso (viene utilizzato l'algoritmo di sgonfiaggio). Tutti i record su una pagina vengono compressi in un thread (viene utilizzato un dizionario comune) e su ogni nuova pagina la compressione inizia da capo. Cioè, per decomprimere qualsiasi record, sono necessari tutti i record precedenti di questa pagina (e solo questa pagina).

Ogni record verrà compresso con il flag Z_SYNC_FLUSH, e alla fine dello stream compresso ci saranno 4 byte 0x00, 0x00, 0xff, 0xff, eventualmente preceduti da uno o due ulteriori byte zero.
Scartiamo questa sequenza (lunga 4, 5 o 6 byte) quando scriviamo nella memoria flash.

L'intestazione del record è di 1, 2 o 3 byte e memorizza:

  • un bit (T) che indica il tipo di record: 0 - contesto, 1 - log;
  • un campo di lunghezza variabile (S) da 1 a 7 bit, che definisce la lunghezza dell'header e della “coda” che devono essere aggiunte al record per la decompressione;
  • lunghezza della registrazione (L).

Tabella dei valori S:

S
Lunghezza dell'intestazione, byte
Scartato in scrittura, byte

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)

Ho provato a illustrarlo, non so quanto chiaramente sia risultato:
La mia implementazione di un ring buffer nella flash NOR
Giallo qui indica il campo T, bianco il campo S, verde L (la lunghezza dei dati compressi in byte), blu i dati compressi, rosso i byte finali dei dati compressi che non vengono scritti nella memoria flash.

Pertanto, possiamo scrivere intestazioni di record della lunghezza più comune (fino a 63+5 byte in forma compressa) in un byte.

Dopo ogni record viene memorizzato un checksum CRC-32C, in cui il valore invertito del checksum precedente viene utilizzato come valore iniziale (init).

CRC ha la proprietà "durata", funziona la seguente formula (più o meno inversione di bit nel processo): La mia implementazione di un ring buffer nella flash NOR.
Cioè, infatti, calcoliamo il CRC di tutti i byte precedenti di intestazioni e dati in questa pagina.

Direttamente dopo il checksum c'è l'intestazione del record successivo.

L'header è progettato in modo tale che il suo primo byte sia sempre diverso da 0x00 e 0xff (se al posto del primo byte dell'header troviamo 0xff, significa che si tratta di un'area inutilizzata; 0x00 segnala un errore).

Algoritmi di esempio

Lettura dalla memoria Flash

Qualsiasi lettura viene fornita con un controllo del checksum.
Se il checksum non corrisponde, la lettura viene ripetuta più volte nella speranza di leggere i dati corretti.

(questo ha senso, Linux non memorizza nella cache le letture da NOR Flash, testato)

Scrivere nella memoria flash

Registriamo i dati.
Leggiamoli.

Se i dati letti non corrispondono a quelli scritti riempiamo l'area con zeri e segnaliamo un errore.

Preparazione di un nuovo microcircuito per il funzionamento

Per l'inizializzazione, nella prima pagina (o meglio zero) viene scritta un'intestazione con la versione 1.
Successivamente, in questa pagina viene scritto il contesto iniziale (contiene l'UUID della macchina e le impostazioni predefinite).

Questo è tutto, la memoria flash è pronta per l'uso.

Caricamento della macchina

Durante il caricamento vengono letti i primi 8 byte di ogni pagina (header + CRC), le pagine con Magic Number sconosciuto o CRC errato vengono ignorate.
Dalle pagine “corrette” vengono selezionate le pagine con la versione massima e da esse viene prelevata la pagina con il numero più alto.
Viene letto il primo record, viene verificata la correttezza del CRC e la presenza del flag “contesto”. Se tutto va bene, questa pagina è considerata attuale. In caso contrario, torniamo alla pagina precedente finché non troviamo una pagina “live”.
e nella pagina trovata leggiamo tutti i record, quelli che usiamo con il flag “context”.
Salva il dizionario zlib (sarà necessario per aggiungerlo a questa pagina).

Questo è tutto, il download è completo, il contesto è ripristinato, puoi lavorare.

Aggiunta di una voce nel diario

Comprimiamo il record con il dizionario corretto, specificando Z_SYNC_FLUSH.Vediamo se il record compresso si adatta alla pagina corrente.
Se non si adatta (o c'erano errori CRC nella pagina), inizia una nuova pagina (vedi sotto).
Annotiamo il record e il CRC. Se si verifica un errore, iniziare una nuova pagina.

Nuova pagina

Selezioniamo una pagina libera con il numero minimo (consideriamo pagina libera una pagina con un checksum errato nell'intestazione o con una versione inferiore a quella corrente). Se tali pagine non sono presenti, seleziona la pagina con il numero minimo tra quelle che hanno una versione uguale a quella attuale.
Cancelliamo la pagina selezionata. Controlliamo il contenuto con 0xff. Se qualcosa non va, prendi la pagina libera successiva, ecc.
Scriviamo un'intestazione sulla pagina cancellata, la prima voce è lo stato corrente del contesto, la successiva è la voce di registro non scritta (se ce n'è una).

Applicabilità del formato

A mio parere, si è rivelato un buon formato per archiviare flussi di informazioni più o meno comprimibili (testo semplice, JSON, MessagePack, CBOR, possibilmente protobuf) in NOR Flash.

Naturalmente il formato è “su misura” per SLC NOR Flash.

Non deve essere utilizzato con supporti BER elevati come NAND o MLC NOR (tale memoria è disponibile per la vendita? L'ho vista menzionata solo nei lavori sui codici di correzione).

Inoltre, non deve essere utilizzato con dispositivi dotati di FTL proprio: flash USB, SD, MicroSD, ecc (per tale memoria ho creato un formato con una dimensione di pagina di 512 byte, una firma all'inizio di ogni pagina e numeri di record univoci - a volte era possibile recuperare tutti i dati da un'unità flash "guastata" mediante una semplice lettura sequenziale).

A seconda delle attività, il formato può essere utilizzato senza modifiche su unità flash da 128 Kbit (16 Kb) a 1 Gbit (128 MB). Se lo desideri, puoi usarlo su chip più grandi, ma probabilmente dovrai regolare le dimensioni della pagina (Ma qui sorge già la questione della fattibilità economica; il prezzo per Flash NOR di grandi dimensioni non è incoraggiante).

Se qualcuno trova il formato interessante e vuole usarlo in un progetto aperto, scriva, cercherò di trovare il tempo, lucidare il codice e pubblicarlo su github.

conclusione

Come puoi vedere, alla fine il formato si è rivelato semplice e perfino noioso.

È difficile riflettere l’evoluzione del mio punto di vista in un articolo, ma credetemi: inizialmente volevo creare qualcosa di sofisticato, indistruttibile, capace di sopravvivere anche dopo un’esplosione nucleare nelle immediate vicinanze. Tuttavia, la ragione (spero) ha comunque vinto e gradualmente le priorità si sono spostate verso la semplicità e la compattezza.

Potrebbe essere che mi sbagliavo? Si certo. Potrebbe risultare, ad esempio, che abbiamo acquistato un lotto di microcircuiti di bassa qualità. Oppure per qualche altro motivo l'apparecchiatura non soddisferà le aspettative di affidabilità.

Ho un piano per questo? Penso che dopo aver letto l'articolo non avrai dubbi che esista un piano. E nemmeno da solo.

Passando a una nota un po’ più seria, il formato è stato sviluppato sia come opzione di lavoro che come “pallone di prova”.

Al momento tutto sul tavolo funziona bene, letteralmente l'altro giorno la soluzione verrà implementata (circa) su un centinaio di dispositivi, vediamo cosa succede nell'operazione di "combattimento" (per fortuna, spero che il formato permetta di rilevare in modo affidabile i guasti; in modo da poter raccogliere statistiche complete). Tra qualche mese sarà possibile trarre le conclusioni (e se sei sfortunato, anche prima).

Se, in base ai risultati dell'utilizzo, vengono rilevati problemi seri e sono necessari miglioramenti, ne scriverò sicuramente.

Letteratura

Non volevo fare una lunga e noiosa lista di opere usate, dopotutto Google ce l’hanno tutti.

Qui ho deciso di lasciare un elenco di risultati che mi sono sembrati particolarmente interessanti, ma gradualmente sono migrati direttamente nel testo dell'articolo e un elemento è rimasto nell'elenco:

  1. Utilità infgen dall'autore zlib. Può visualizzare chiaramente il contenuto degli archivi deflate/zlib/gzip. Se devi avere a che fare con la struttura interna del formato deflate (o gzip), lo consiglio vivamente.

Fonte: habr.com

Aggiungi un commento