La mia implementazione di un buffer circolare nella memoria NOR flash

Antefatti

Ci sono distributori automatici di nostra progettazione. All'interno Raspberry Pi e un po' di circuiti su una scheda separata. Sono collegati un accettatore di monete, un accettatore di banconote, un terminale bancario… Tutto è gestito da un programma scritto da noi. Tutta la cronologia delle operazioni viene registrata in un diario su una chiavetta USB (MicroSD), che poi viene trasferita tramite internet (con un modem USB) su un server, dove viene archiviata in un database. Le informazioni sulle vendite vengono caricate in 1C, c'è anche una semplice interfaccia web per il monitoraggio e simili.

Cioè, il diario è vitale — per la contabilizzazione (ci sono le entrate, le vendite, ecc.), il monitoraggio (tutti i tipi di malfunzionamenti e altre circostanze straordinarie); questa è, si può dire, tutta l'informazione che abbiamo su questo distributore.

Problema

Le chiavette USB si dimostrano dispositivi molto poco affidabili. Si guastano con una regolarità sorprendente. Questo porta sia a fermi dei distributori, sia (se per qualche motivo il diario non può essere trasferito online) alla perdita di dati.

Questa non è la prima esperienza con le chiavette USB; in precedenza c'era un altro progetto con più di un centinaio di dispositivi, dove il diario era memorizzato su chiavette USB, anche lì ci sono stati problemi di affidabilità, a volte il numero di guasti al mese superava le decine. Abbiamo provato diverse chiavette, incluse quelle di marca con memoria SLC, sì, alcuni modelli sono più affidabili di altri, ma la sostituzione delle chiavette non ha risolto il problema in modo decisivo.

Attenzione! Longread! Se non ti interessa il "perché" e vuoi solo sapere "come", puoi andare direttamente alla fine articoli.

Soluzione

La prima cosa che viene in mente: rinunciare al MicroSD, installare, ad esempio, un SSD, e avviarsi da esso. Teoricamente è possibile, penso, ma relativamente costoso, e non è così affidabile (si aggiunge un adattatore USB-SATA; anche per gli SSD economici le statistiche sui guasti non sono incoraggianti).

I dischi rigidi USB non sembrano una soluzione particolarmente allettante.

Pertanto, siamo giunti a questa soluzione: mantenere l'avvio dal MicroSD, ma utilizzarli in modalità di sola lettura, e conservare il diario delle operazioni (e altre informazioni uniche per ciascun dispositivo, come il numero di serie, le calibrazioni dei sensori, ecc.) da qualche altra parte.

Il tema dei filesystem di sola lettura per Raspberry Pi è già stato studiato a fondo, non mi soffermerò sui dettagli dell'implementazione in questo articolo. (ma se ci sarà interesse — forse scriverò un mini-articolo su questo tema). L'unico punto che desidero sottolineare: sia per esperienza personale che per le recensioni di chi ha già implementato, ci sono migliorie in termini di affidabilità. Sì, è impossibile eliminare completamente i guasti, ma ridurne significativamente la frequenza è assolutamente fattibile. Inoltre, le schede diventano uniformi, il che semplifica notevolmente la sostituzione per il personale di supporto.

Parte hardware

Non ci sono stati dubbi sulla scelta del tipo di memoria: NOR Flash.
Argomenti:

  • connessione semplice (di solito bus SPI, di cui abbiamo già esperienza, quindi non si prevedono problemi 'hardware');
  • prezzo ridicolo;
  • protocollo di lavoro standard (la sua implementazione è già nel kernel di Linux, su richiesta si possono utilizzare soluzioni di terze parti, che sono anche disponibili, o addirittura scrivere una propria, dato che è piuttosto semplice);
  • affidabilità e durata:
    da un datasheet tipico: i dati vengono conservati per 20 anni, 100000 cicli di cancellazione per ogni blocco;
    da fonti esterne: BER estremamente basso, si postula l'assenza della necessità di codici di correzione degli errori (in alcuni lavori si considera l'ECC per NOR, ma di solito si intende MLC NOR, il che può anche succedere).

Calcoliamo i requisiti di volume e durata.

Vogliamo garantire che i dati siano conservati per diversi giorni. Questo è necessario affinché, in caso di problemi di connessione, la storia delle vendite non venga persa. Ci orienteremo su 5 giorni, durante i quali (anche considerando i fine settimana e le festività) si può risolvere il problema.

Attualmente accumuliamo circa 100kb di log al giorno (3-4 mila registrazioni), ma questa cifra sta crescendo progressivamente: aumenta la granularità, si aggiungono nuovi eventi. Inoltre, ci sono a volte picchi (un certo sensore inizia a inviare falsi allarmi, per esempio). Calcoliamo su 10mila registrazioni da 100 byte — un megabyte al giorno.

In totale si traducono in 5Mb di dati 'puliti' (ben compressibili). A questi si aggiungono (una stima grossolana) 1Mb di dati di servizio.

Quindi abbiamo bisogno di un chip da 8Mb se non utilizziamo la compressione, o 4Mb se la utilizziamo. Cifre assolutamente realistico per questo tipo di memoria.

Per quanto riguarda la durata: se prevediamo che la memoria venga sovrascritta interamente non più spesso di una volta ogni 5 giorni, nel corso di 10 anni di utilizzo otteniamo meno di mille cicli di riscrittura.
Ricordo che il produttore promette centomila.

Un po' di NOR vs NAND

Oggi, ovviamente, la memoria NAND è molto più popolare, tuttavia per questo progetto non la utilizzerei: a differenza della NOR, la NAND richiede necessariamente l'uso di codici di correzione degli errori, tabelle dei blocchi difettosi, ecc., e le piedinature dei chip NAND sono di solito molto più numerose.

Tra i difetti della NOR si possono indicare:

  • basso volume (e, di conseguenza, alto costo per megabyte);
  • bassa velocità di trasferimento (in gran parte a causa dell'uso dell'interfaccia seriale, di solito SPI o I2C);
  • cancellazione lenta (a seconda della dimensione del blocco, può richiedere da frazioni di secondo a diversi secondi).

Non sembra ci sia nulla di critico per noi, quindi continuiamo.

Se sei interessato ai dettagli, è stato scelto il chip at25df321a (tuttavia, ciò è irrilevante, ci sono una miriade di analoghi sul mercato, compatibili con il pinout e il sistema di comandi; anche se decidessimo di installare un chip di un altro produttore e/o di un'altra capacità, tutto funzionerà senza modifiche al codice).

Utilizzo il driver integrato nel kernel Linux, su Raspberry grazie al supporto del device tree overlay è molto semplice: basta posizionare l'overlay compilato in /boot/overlays e modificare leggermente /boot/config.txt.

Esempio di file dts

Onestamente, 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

Tralascio 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 chip ha solo 8 pin, di cui abbiamo bisogno di terra, alimentazione, SPI (CS, SI, SO, SCK); i livelli corrispondono a quelli del Raspberry Pi, non è necessaria alcuna ulteriore interconnessione: basta collegare i 6 pin indicati.

Definizione del compito

Come al solito, la definizione del compito passa attraverso diverse iterazioni, mi sembra che sia giunto il momento per un'altra. Quindi fermiamoci, raggruppiamo ciò che è già stato scritto e facciamo chiarezza sui dettagli rimasti in ombra.

Quindi abbiamo deciso che il registro sarà memorizzato nella SPI NOR Flash.

Cos'è la NOR Flash per chi non lo sa

È una memoria non volatile, con cui è possibile effettuare tre operazioni:

  1. Lettura:
    La lettura più semplice: inviamo l'indirizzo e leggiamo quanti byte ci servono;
  2. Registrazione:
    La scrittura nella NOR flash appare normale, ma ha una particolarità: è possibile modificare solo 1 in 0, ma non viceversa. Ad esempio, se nella cella di memoria c'era 0x55, dopo aver scritto 0x0f al suo interno si troverà 0x05. (vedi tabella qui sotto);
  3. Cancellazione:
    Naturalmente, è necessario saper effettuare anche l'operazione inversa: cambiare 0 in 1, ed è proprio per questo che esiste l'operazione di cancellazione. A differenza delle prime due, essa opera non su byte, ma su blocchi (il blocco minimo di cancellazione nel chip scelto è di 4kB). La cancellazione distrugge l'intero blocco e questo è l'unico modo per cambiare 0 in 1. Pertanto, quando si lavora con la memoria flash spesso è necessario allineare le strutture dati al confine del blocco di cancellazione.
    Scrittura nella NOR Flash:

Dati binari

Era
01010101

Registrato
00001111

Diventato
00000101

Il registro stesso rappresenta una sequenza di registrazioni di lunghezza variabile. La lunghezza tipica di una registrazione è di circa 30 byte (anche se a volte si verificano registrazioni lunghe diversi kilobyte). In questo caso, lavoriamo con loro semplicemente come un insieme di byte, ma, se interessa, all'interno delle registrazioni viene utilizzato CBOR.

Oltre al registro, dobbiamo conservare alcune informazioni di "configurazione", sia aggiornabili che no: un certo ID dell'apparecchiatura, le calibrazioni dei sensori, un flag "apparecchio temporaneamente spento", ecc.
Queste informazioni costituiscono un insieme di registrazioni chiave-valore, anch'esse memorizzate in CBOR. Non abbiamo molte di queste informazioni (massimo qualche kilobyte), e non vengono aggiornate spesso.
In seguito ci riferiremo ad esse come contesto.

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

Quali fonti di problemi possiamo considerare?

  • Interruzione dell'alimentazione durante le operazioni di scrittura/cancellazione. Questo rientra nella categoria "contro il manico non c'è rimedio".
    Informazioni da una discussione su stackexchange: durante l'interruzione dell'alimentazione mentre si lavora con la flash, sia la cancellazione (impostazione a 1) che la scrittura (impostazione a 0) portano a comportamenti indefiniti: i dati possono essere scritti, scritti parzialmente (ad esempio, abbiamo trasmesso 10 byte/80 bit, e siamo riusciti a scrivere solo 45 bit), non si esclude che parte dei bit rimanga in uno stato "intermedio" (la lettura può restituire sia 0 che 1);
  • Errori nella stessa memoria flash.
    Il BER, sebbene molto basso, non può essere nullo;
  • Errori sulla linea
    I dati trasmessi tramite SPI non sono affatto protetti e potrebbero verificarsi sia errori singoli di bit che errori di sincronizzazione, come la perdita o l'inserimento di bit (che porta a distorsioni massicce dei dati);
  • Altri errori/fallimenti
    Errori nel codice, 'bug' Raspberry, interferenze aliene…

Ho formulato i requisiti la cui attuazione ritengo necessaria per garantire l'affidabilità:

  • le registrazioni devono essere scritte nella memoria flash immediatamente, la scrittura ritardata non è presa in considerazione; - se si verifica un errore, deve essere rilevato e gestito il prima possibile; - il sistema deve ripristinare il funzionamento dopo gli errori, se possibile.
    (un esempio dalla vita 'di come non dovrebbe essere', con cui credo tutti si siano imbattuti: dopo un riavvio forzato, il file system è stato 'rotto' e il sistema operativo non si avvia)

Idee, approcci, riflessioni

Quando ho iniziato a pensare a quest'attività, mi sono passate per la testa un sacco di idee, ad esempio:

  • utilizzare la compressione dei dati;
  • utilizzare strutture dati intelligenti, ad esempio memorizzare le intestazioni delle registrazioni separatamente dai dati stessi, in modo che in caso di errore in una registrazione si possano leggere senza problemi le altre;
  • utilizzare campi di bit per controllare il completamento della registrazione in caso di interruzione dell'alimentazione;
  • memorizzare le somme di controllo per tutto e per tutti;
  • utilizzare qualche tipo di codifica resistente ai disturbi.

Parte di queste idee è stata utilizzata, mentre altre sono state scartate. Procediamo con ordine.

Compressione dei dati

Gli eventi stessi che registriamo nel log sono abbastanza omogenei e ripetitivi ('lanciata moneta da 5 rubli', 'premuto il pulsante per restituire il resto', ...). Pertanto, la compressione dovrebbe risultare piuttosto efficace.

Le spese generali per la compressione sono trascurabili (il nostro processore è abbastanza potente; anche il primo Pi aveva un nucleo con una frequenza di 700 MHz, mentre i modelli attuali hanno più nuclei con frequenze superiori a un gigahertz), la velocità di scambio con il deposito non è alta (alcuni megabyte al secondo) e le dimensioni delle registrazioni sono ridotte. In generale, se la compressione influisce sulle prestazioni, sarà solo in modo positivo. (assolutamente non critico, lo constato semplicemente). Inoltre, non abbiamo un embedded vero e proprio, ma un normale Linux, quindi l'implementazione non dovrebbe richiedere molto sforzo (basta semplicemente collegare la libreria e usare alcune funzioni in essa).

È stato prelevato un campione di log da un dispositivo in funzione (1,7 MB, 70 mila registrazioni) e inizialmente verificato per la compressibilità utilizzando gzip, lz4, lzop, bzip2, xz, zstd.

  • gzip, xz, zstd hanno mostrato risultati simili (40 KB).
    È stato sorprendente che il trendy xz si sia comportato a livello di gzip o zstd;
  • lzip con le impostazioni predefinite ha dato un risultato leggermente peggiore;
  • lz4 e lzop hanno mostrato un risultato non molto buono (150 KB);
  • bzip2 ha mostrato risultati sorprendentemente buoni (18 KB).

Quindi, i dati si comprimono molto bene.
Quindi (se non troviamo difetti fatali) la compressione ci sarà! Solo perché ci sarà più spazio per i dati sulla stessa chiavetta.

Pensiamo ai difetti.

Primo problema: abbiamo già concordato che ogni registrazione deve essere subito trasferita sulla chiavetta. Di solito, il compressore raccoglie dati dal flusso di input fino a quando non decide che è il momento di scrivere in uscita. A noi serve invece ricevere immediatamente un blocco di dati compressi e salvarlo nella memoria non volatile.

Vedo tre soluzioni:

  1. Comprimere ogni registrazione utilizzando la compressione basata su dizionario invece degli algoritmi considerati sopra.
    È un'opzione di lavoro, ma non mi piace. Per garantire un livello di compressione almeno decente, il dizionario deve essere 'adattato' a dati specifici, qualsiasi modifica porterà a una drammatica diminuzione del livello di compressione. Sì, il problema si risolve creando una nuova versione del dizionario, ma è un gran calcio — dovremo conservare tutte le versioni del dizionario; in ogni registrazione dovremo indicare con quale versione del dizionario è stata compressa…
  2. Comprimere ogni registrazione con algoritmi 'classici', ma in modo indipendente dalle altre.
    Gli algoritmi di compressione considerati non sono progettati per lavorare con registrazioni di questa dimensione (decine di byte), il rapporto di compressione sarà chiaramente inferiore a 1 (cioè un aumento del volume dei dati invece di una compressione);
  3. Eseguire un FLUSH dopo ogni registrazione.
    Molte librerie di compressione supportano il FLUSH. È un comando (o un parametro della procedura di compressione) che, una volta ricevuto, permette al compressor di generare un flusso compresso in modo tale da poter ripristinare tutti i dati non compressi che sono già stati ricevuti. Un'analogia del genere sync nei sistemi di file o commit in sql.
    È importante notare che le operazioni di compressione successive possono utilizzare il dizionario accumulato e il tasso di compressione non sarà così compromesso come nel caso precedente.

Penso sia ovvio che ho scelto la terza opzione, approfondiamo questo argomento.

Trovata ottimo articolo su FLUSH in zlib.

Ho effettuato un test ispirato a un articolo, ho preso 70.000 registrazioni di log da un dispositivo reale, con dimensioni di pagina di 60 KB (torneremo sulle dimensioni della pagina) ho ottenuto:

Dati di origine
Compressione gzip -9 (senza FLUSH)
zlib con Z_PARTIAL_FLUSH
zlib con Z_SYNC_FLUSH

Dimensione, KB
1692
40
352
604

A prima vista, il costo introdotto da FLUSH appare eccessivo, ma in realtà abbiamo una scelta non molto ricca: o non comprimere affatto, o comprimere (e in modo molto efficace) con FLUSH. Non dobbiamo dimenticare che abbiamo 70.000 registrazioni, e l'esubero introdotto da Z_PARTIAL_FLUSH è solo di 4-5 byte per registrazione. E il tasso di compressione si è rivelato quasi 5:1, il che è sicuramente un ottimo risultato.

Può sembrare sorprendente, ma in realtà Z_SYNC_FLUSH è un modo più efficace per eseguire FLUSH

Nel caso dell'uso di Z_SYNC_FLUSH, gli ultimi 4 byte di ogni registrazione saranno sempre 0x00, 0x00, 0xff, 0xff. E se li conosciamo, possiamo evitarne la memorizzazione, di conseguenza la dimensione finale risulta di solo 324 KB.

Nell'articolo che cito, c'è una spiegazione:

Un nuovo blocco di tipo 0 con contenuti vuoti viene aggiunto.

Un blocco di tipo 0 con contenuti vuoti è composto da:

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

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

Si scopre che blocchi di dati così brevi sono solitamente (sempre?) codificati usando un blocco di tipo 1 (blocco fisso), che deve necessariamente terminare con 7 bit zero, ottenendo così 10-17 bit zero garantiti (e gli altri saranno zero con una probabilità di circa il 50%).

Pertanto, nei dati di test, nel 100% dei casi prima di 0x00, 0x00, 0xff, 0xff c'è un byte zero, e in più di un terzo dei casi - due byte zero (forse è dovuto al fatto che sto usando CBOR binario, mentre usando JSON testuale si sarebbero incontrati più spesso blocchi di tipo 2 - blocco dinamico, quindi ci sarebbero stati blocchi senza byte zero aggiuntivi prima di 0x00, 0x00, 0xff, 0xff).

In totale, con i dati di test disponibili, si può ottenere meno di 250 KB di dati compressi.

È possibile risparmiare ancora un po', praticando il giocoliere con i bit: al momento ignoriamo la presenza di diversi bit zero alla fine del blocco, alcuni bit all'inizio del blocco rimangono invariati…
Ma qui ho preso una decisione ferma di fermarmi, altrimenti con questo ritmo potrei arrivare a sviluppare il mio compressore.

In totale, dai miei dati di test ho ottenuto 3-4 byte per la scrittura, il fattore di compressione è risultato superiore a 6:1. A dire il vero: non mi aspettavo un risultato del genere, per me tutto ciò che è migliore di 2:1 è già un risultato che giustifica l'uso della compressione.

Va tutto bene, ma zlib (deflate) è comunque un algoritmo di compressione piuttosto arcaico, meritato e un po' antiquato. Solo il fatto che come dizionario vengano utilizzati gli ultimi 32KB del flusso di dati non compressi appare strano oggi (cioè se qualche blocco di dati è molto simile a quello che era nel flusso di input 40KB fa, inizierà a essere compresso da capo, senza fare riferimento alla precedente occorrenza). Negli archivi moderni alla moda, la dimensione del dizionario viene spesso misurata in megabyte, e non in kilobyte.

Così continuiamo la nostra mini-ricerca sugli archivi.

Il successivo test è stato bzip2 (ricordo che, senza FLUSH, ha mostrato un fantastico grado di compressione, quasi 100:1). Purtroppo, con FLUSH ha mostrato scarsi risultati; la dimensione dei dati compressi è risultata maggiore rispetto a quella non compressa.

Le mie ipotesi sulle ragioni del fallimento

Libbz2 offre solo un'opzione di flush, che sembra pulire il dizionario (analogo a Z_FULL_FLUSH in zlib), non si può parlare di qualche compressione efficace dopo questo.

E infine, ho testato zstd. A seconda dei parametri, comprime al livello di gzip, ma molto più velocemente, o meglio di gzip.

Purtroppo, anche con FLUSH non ha mostrato grandi risultati: la dimensione dei dati compressi è arrivata a circa 700KB.

Io ho posto una domanda sulla pagina del progetto su github, ho ricevuto risposta che è ragionevole aspettarsi fino a 10 byte di dati di servizio per ogni blocco di dati compressi, che è vicino ai risultati ottenuti; non riuscirò mai a raggiungere deflate.

A questo punto ho deciso di fermare gli esperimenti con i compressori (ricordo che xz, lzip, lzo, lz4 non si sono dimostrati ancora nella fase di testing senza FLUSH, e non ho considerato algoritmi di compressione più esotici).

Torniamo ai problemi di archiviazione.

Il secondo (come si dice in ordine, non per importanza) problema è che i dati compressi rappresentano un flusso unico, in cui ci sono costanti riferimenti a sezioni precedenti. Così, in caso di un danneggiamento di una parte dei dati compressi, perdiamo non solo il blocco di dati non compressi ad esso legato, ma anche tutti quelli successivi.

Esistono approcci per risolvere questo problema:

  1. Prevenire la comparsa del problema — aggiungere ridondanza ai dati compressi che permetta di individuare e correggere errori; di questo ne parleremo più tardi;
  2. Minimizzare le conseguenze in caso di insorgenza del problema
    Abbiamo già detto in precedenza che è possibile comprimere ogni blocco di dati in modo indipendente, e in questo caso il problema si risolve da solo (il danneggiamento dei dati di un blocco porterà alla perdita dei dati solo di quel blocco). Tuttavia, questo è un caso estremo, in cui la compressione dei dati diventa inefficace. L'estremo opposto: utilizzare i nostri 4 MB di chip come un unico archivio, il che ci darà un'ottima compressione, ma conseguenze disastrose in caso di danneggiamento dei dati.
    Sì, è necessaria una compromesso in termini di affidabilità. Ma bisogna ricordare che stiamo sviluppando un formato di archiviazione dei dati per la memoria non volatile con un BER estremamente basso e un periodo dichiarato di conservazione dei dati di 20 anni.

Durante gli esperimenti ho scoperto che le perdite di livello di compressione più o meno significative iniziano con blocchi di dati compressi di dimensioni inferiori a 10 KB.
È stato menzionato in precedenza che la memoria utilizzata ha un'organizzazione a pagine, non vedo motivi per cui non si debba utilizzare la corrispondenza «una pagina - un blocco di dati compressi».

Quindi, la dimensione minima ragionevole della pagina è di 16 KB (con un margine per le informazioni di servizio). Tuttavia, una pagina così piccola impone sostanziali limitazioni sulla dimensione massima della registrazione.

Anche se al momento non prevedo registrazioni superiori a qualche kilobyte in forma compressa, ho deciso di utilizzare pagine di dimensioni 32 KB (in totale ci saranno 128 pagine per chip).

Sommario:

  • I dati vengono memorizzati in forma compressa utilizzando zlib (deflate);
  • Per ogni registrazione impostiamo Z_SYNC_FLUSH;
  • Ogni registrazione compressa viene ridimensionata nei byte finali (ad esempio, 0x00, 0x00, 0xff, 0xff); nell'intestazione indichiamo quanti byte abbiamo ridotto;
  • I dati sono memorizzati in pagine da 32 KB; all'interno della pagina c'è un flusso continuo di dati compressi; su ogni pagina la compressione ricomincia da capo.

E prima di terminare con la compressione, vorrei sottolineare che i dati compressi ci danno solo pochi byte per registrazione, quindi è estremamente importante non gonfiare le informazioni di servizio, ogni byte conta qui.

Memorizzazione degli header dei dati

Poiché abbiamo registrazioni di lunghezza variabile, dobbiamo in qualche modo determinare la posizione/le frontiere delle registrazioni.

Conosco tre approcci:

  1. Tutte le registrazioni sono memorizzate in un flusso continuo, all'inizio c'è l'header della registrazione che contiene la lunghezza, e poi la registrazione stessa.
    In questa variante, sia gli header che i dati possono avere lunghezza variabile.
    In sostanza, otteniamo una lista concatenata, che viene utilizzata ovunque;
  2. Gli header e le registrazioni stesse sono memorizzati in flussi separati.
    Utilizzando header di lunghezza fissa, otteniamo che la corruzione di un header non influisce sugli altri.
    Un approccio simile è utilizzato, ad esempio, in molti sistemi di file;
  3. Le registrazioni sono memorizzate in un flusso continuo, il confine della registrazione è definito da un certo marcatore (simbolo/ sequenza di simboli che sono vietati all'interno dei blocchi di dati). Se all'interno di una registrazione appare un marcatore, allora lo sostituiamo con una certa sequenza (lo scappiamo).
    Un approccio simile è utilizzato, ad esempio, nel protocollo PPP.

Facciamo un esempio.

Opzione 1:
La mia implementazione di un buffer circolare nella memoria NOR flash
Qui è tutto molto semplice: conoscendo la lunghezza della registrazione possiamo calcolare l'indirizzo del prossimo header. In questo modo ci spostiamo tra gli header fino a incontrare un'area riempita di 0xff (spazio libero) o la fine della pagina.

Opzione 2:
La mia implementazione di un buffer circolare nella memoria NOR flash
A causa della lunghezza variabile delle registrazioni, non possiamo dire in anticipo quanti record (e quindi anche header) ci serviranno per pagina. Possiamo distribuire gli header e i dati su pagine diverse, ma preferisco un altro approccio: posizioniamo sia gli header che i dati su una sola pagina, tuttavia gli header (di lunghezza fissa) partono dall'inizio della pagina, mentre i dati (di lunghezza variabile) dal fondo. Non appena si 'incontrano' (non c'è abbastanza spazio per una nuova registrazione), consideriamo quella pagina completa.

Opzione 3:
La mia implementazione di un buffer circolare nella memoria NOR flash
Non c'è bisogno di mantenere nel titolo la lunghezza o altre informazioni sulla posizione dei dati; bastano i marcatori che indicano i confini delle registrazioni. Tuttavia, i dati devono essere elaborati durante la scrittura/lettura.
Come marcatore, userei 0xff (con cui è riempita la pagina dopo l'erase); in questo modo, l'area libera non sarà interpretata come dati.

Tabella comparativa:

Opzione 1
Opzione 2
Opzione 3

Resistenza agli errori

+
+

Compattezza
+

+

Difficoltà di implementazione
*
**
**

La variante 1 ha un grave difetto: se uno dei titoli è danneggiato, si distrugge l'intera catena successiva. Le altre varianti consentono di recuperare una parte dei dati anche in caso di danni diffusi.
Ma è opportuno ricordare che abbiamo deciso di memorizzare i dati in formato compresso; comunque perdiamo tutti i dati nella pagina dopo una scrittura "difettosa", quindi anche se in tabella c'è un meno, non lo consideriamo.

Compattezza:

  • nella prima variante dobbiamo memorizzare solo la lunghezza nel titolo; utilizzando interi di lunghezza variabile, nella maggior parte dei casi possiamo limitarci a un byte;
  • nella seconda variante dobbiamo memorizzare l'indirizzo iniziale e la lunghezza; la registrazione deve avere dimensione fissa, la stimo in 4 byte per registrazione (due byte per l'offset e due byte per la lunghezza);
  • nella terza variante è sufficiente un solo carattere per indicare l'inizio della registrazione, più la registrazione stessa aumenterà di 1-2% a causa dell'escaping. In generale, si ha un parità approssimativa con la prima variante.

Inizialmente ho considerato la seconda variante come principale (e ho persino scritto una sua implementazione). Ho rinunciato a essa solo quando ho deciso definitivamente di utilizzare la compressione.

Forse, un giorno, utilizzerò comunque una variante simile. Ad esempio, se dovessi gestire l'archiviazione dei dati per una nave che viaggia tra la Terra e Marte — ci sono requisiti completamente diversi per l'affidabilità, radiazione spaziale,…

Quanto alla terza variante: le ho dato due stelle per la difficoltà di implementazione semplicemente perché non mi piace occuparmi dell'escaping, della modifica della lunghezza in corso e simili. Sì, forse è un pregiudizio, ma il codice dovrò scriverlo io — perché costringermi a fare qualcosa che non mi piace.

Sommario: scegliamo la variante di archiviazione sotto forma di catene "titolo con lunghezza - dati di lunghezza variabile" per l'efficienza e la semplicità di implementazione.

Utilizzo dei campi bit per il controllo del successo delle operazioni di scrittura

Non ricordo già più dove ho visto l'idea, ma è circa così:
Per ogni registrazione riserviamo alcuni bit per memorizzare i flag.
Come abbiamo detto prima, dopo l'erase tutti i bit sono impostati su 1, e possiamo cambiare 1 in 0, ma non viceversa. Quindi, per "flag non impostato" usiamo 1, per "flag impostato" — 0.

Ecco come può apparire l'inserimento di una registrazione di lunghezza variabile nella flash:

  1. Impostiamo il flag "scrittura della lunghezza iniziata";
  2. Scriviamo la lunghezza;
  3. Impostiamo il flag "scrittura dei dati iniziata";
  4. Scriviamo i dati;
  5. Impostiamo il flag "scrittura completata".

Inoltre, avremo un flag "si è verificato un errore", per un totale di 4 flag bit.

In questo caso abbiamo due stati stabili "1111" — scrittura non iniziata e "1000" — scrittura eseguita con successo; nel caso di un'interruzione imprevista del processo di scrittura otterremo stati intermedi, che poi saremo in grado di rilevare e gestire.

L'approccio è interessante, ma protegge solo da un'improvvisa interruzione di corrente e da simili malfunzionamenti, che è certamente importante, tuttavia non è l'unica (e nemmeno la principale) causa di possibili guasti.

Sommario: Andiamo avanti nella ricerca di una buona soluzione.

Checksum

Le checksum offrono anche la possibilità di accertarsi (con una probabilità sufficiente) che stiamo leggendo esattamente ciò che doveva essere scritto. E, a differenza dei campi bit trattati sopra, esse funzionano sempre.

Se consideriamo l'elenco delle potenziali fonti di problemi di cui abbiamo parlato sopra, la checksum è in grado di rilevare un errore indipendentemente dalla sua origine (eccezion fatta, forse, per alieni malintenzionati — essi possono falsificare anche la checksum).

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

La scelta dell'algoritmo per il calcolo della checksum non ha sollevato problemi — CRC. Da un lato, le proprietà matematiche permettono di rilevare errori di alcuni tipi al 100%, dall'altro — su dati casuali, in genere, questo algoritmo mostra una probabilità di collisione non molto superiore al limite teorico. La mia implementazione di un buffer circolare nella memoria NOR flash. Anche se non è l'algoritmo più veloce e non sempre il migliore in termini di numero di collisioni, ha una qualità molto importante: nei test che ho svolto non ho trovato schemi su cui fallisse chiaramente. La stabilità è la qualità principale in questo caso.

Esempio di ricerca approfondita: parte 1, parte 2 (link a narod.ru, scusate).

Tuttavia, il compito di scegliere una somma di controllo non è finito; il CRC è un intero insieme di somme di controllo. È necessario decidere sulla lunghezza e poi scegliere un polinomio.

La scelta della lunghezza della somma di controllo non è una questione semplice, come sembra a prima vista.

Illustro:
Supponiamo che ci sia una probabilità di errore in ogni byte La mia implementazione di un buffer circolare nella memoria NOR flash e una somma di controllo ideale; calcoliamo il numero medio di errori su un milione di registrazioni:

Dati, byte
Somma di controllo, byte
Errori non rilevati
Falsi allarmi di errore
Totale risposte errate

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

Sembra semplice: scegli la lunghezza della somma di controllo in base alla lunghezza dei dati protetti, con un minimo di risposte errate, e il gioco è fatto.

Tuttavia, con somme di controllo corte si presenta un problema: sebbene esse rilevino bene gli errori di bit singoli, possono con una probabilità piuttosto alta accettare come valide dati completamente casuali. Su Habr è già stato pubblicato un articolo che descrive il problema nella vita reale.

Quindi, per rendere praticamente impossibile una corrispondenza casuale della somma di controllo, è necessario utilizzare somme di controllo di almeno 32 bit (per lunghezze superiori a 64 bit si usano generalmente funzioni hash crittografiche).

Nonostante io abbia scritto in precedenza che bisogna risparmiare spazio con tutti i mezzi, alla fine utilizzeremo una somma di controllo a 32 bit (16 bit sono pochi, la probabilità di collisione è superiore allo 0,01%; e 24 bit, come si suol dire, non funzionano né da una parte né dall'altra).

Qui potrebbe sorgere una obiezione: abbiamo risparmiato ogni byte nella scelta della compressione, solo per dare 4 byte subito? Non era meglio non comprimere e non aggiungere la somma di controllo? Certo che no, l'assenza di compressione non implica, che il controllo dell'integrità non ci sia utile.

Non inventeremo la bicicletta nella scelta del polinomio, ma utilizzeremo il popolare CRC-32C.
Questo codice rileva 6 errori di bit in pacchetti fino a 22 byte (forse il caso più comune per noi), 4 errori di bit in pacchetti fino a 655 byte (anch'esso un caso frequente per noi), 2 o qualsiasi numero dispari di errori di bit in pacchetti di qualsiasi lunghezza ragionevole.

Se qualcuno è interessato ai dettagli

Articolo di Wikipedia su CRC.

Parametri del codice crc-32c in sito di Kupman — forse il principale esperto di CRC al mondo.

In il suo articolo c'è un altro codice interessante, che fornisce parametri leggermente migliori per le lunghezze di pacchetto a noi rilevanti, ma non ho trovato la differenza significativa, e mi sento abbastanza competente da scegliere un codice personalizzato invece di uno standard e ben studiato.

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

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

  • alla fine dobbiamo controllare l'integrità dei dati memorizzati — ecco che lo verifichiamo direttamente (e contemporaneamente saranno controllati possibili errori nell'implementazione della compressione/decompressione, danneggiamenti causati da memoria danneggiata, ecc.);
  • l'algoritmo deflate in zlib ha un'implementazione abbastanza matura e non dovrebbe fallire con dati di input "difettosi", inoltre, spesso è in grado di rilevare autonomamente errori nel flusso in ingresso, riducendo la probabilità generale di non rilevamento di errori (ho condotto un test invertendo un singolo bit in una breve registrazione, zlib ha rilevato l'errore in circa un terzo dei casi).

Argomenti "contro" il calcolo del checksum dei dati non compressi:

  • CRC è progettato per errori di bit poco numerosi che sono tipici della memoria flash (un errore di bit in un flusso compresso può causare una modifica massiccia del flusso di uscita, dove teoricamente potremmo "catturare" una collisione);
  • non mi piace molto l'idea di passare al decompressore dati potenzialmente danneggiati, chi lo sa, come reagirà.

In questo progetto ho deciso di allontanarmi dalla pratica comune di memorizzare il checksum dei dati non compressi.

Sommario: utilizziamo CRC-32C, calcolando il checksum dai dati nella forma in cui vengono registrati nella flash (dopo la compressione).

Ridondanza

L'uso della codifica ridondante non consente, ovviamente, di escludere la perdita di dati; tuttavia, può ridurre notevolmente (spesso di diversi ordini di grandezza) la probabilità di perdita di dati irreversibile.

Possiamo utilizzare diversi tipi di ridondanza per correggere gli errori.
I codici di Hamming possono correggere errori di bit singoli, i codici Reed-Solomon sono simbolici, mentre più copie di dati insieme a checksum o codifiche simili a RAID-6 possono aiutare a ripristinare i dati anche in caso di danni estesi.
Inizialmente ero favorevole a un ampio utilizzo della codifica resistente agli errori, ma poi ho capito che prima di tutto era necessario avere un'idea degli errori da cui vogliamo proteggerci e poi scegliere la codifica.

Abbiamo già detto che gli errori devono essere rilevati il prima possibile. In quali momenti possiamo incorrere in errori?

  1. Registrazione interrotta (per qualche motivo, durante la registrazione, è avvenuta un'interruzione di corrente, il Raspberry si è bloccato, …)
    Purtroppo, in caso di un tale errore, rimane solo da ignorare le registrazioni non valide e considerare i dati persi;
  2. Errori di scrittura (per qualche motivo, nella memoria flash è stato registrato qualcosa di diverso da quello che si intendeva registrare)
    Possiamo rilevare immediatamente tali errori se effettuiamo una lettura di controllo subito dopo la scrittura;
  3. Distorsione dei dati nella memoria durante la conservazione;
  4. Errori di lettura
    Per correggerli, è sufficiente ripetere più volte la lettura in caso di disaccordo del checksum.

Cioè, solo gli errori di terzo tipo (corruzione spontanea dei dati durante la conservazione) non possono essere corretti senza codifica resistente agli errori. Si ritiene che tali errori siano estremamente improbabili.

Sommario: Si è deciso di rinunciare alla codifica ridondante, ma se l'esperienza mostrerà l'erroneità di questa decisione, torneremo a considerare la questione (con statistiche accumulate sui guasti che permetteranno di scegliere il tipo ottimale di codifica).

Altro

Naturalmente, il formato dell'articolo non consente di giustificare ogni bit nel formato (e io ho già esaurito le forze), quindi passerò rapidamente ad alcuni punti non trattati in precedenza.

  • Si è deciso di rendere tutte le pagine "equamente dignitose"
    Cioè non ci saranno pagine speciali con metadati, flussi separati, ecc., ma un flusso unico che riscrive tutte le pagine in sequenza.
    Questo garantisce un'usura uniforme delle pagine, l'assenza di un unico punto di guasto e, semplicemente, è gradito;
  • È necessario prevedere la versioning del formato.
    Un formato senza numero di versione nell'intestazione è male!
    Basta aggiungere nel'intestazione della pagina un campo con un certo Magic Number (firma), che indicherà la versione del formato utilizzato. (non penso che ce ne saranno anche solo una decina nella pratica);
  • Utilizzare per le registrazioni (che sono moltissime) un'intestazione di lunghezza variabile, cercando di mantenerla a 1 byte nella maggior parte dei casi;
  • Per codificare la lunghezza dell'intestazione e la lunghezza della parte tagliata della registrazione compressa, utilizzare codici binari di lunghezza variabile.

È stato molto utile il generatore online di codici Huffman. Letteralmente in pochi minuti sono riuscito a trovare i codici di lunghezza variabile necessari.

Descrizione del formato di memorizzazione dei dati

Byte order

I campi di dimensioni superiori a un byte sono memorizzati in formato big-endian (ordine dei byte di rete), cioè 0x1234 è registrato come 0x12, 0x34.

Divisione in pagine

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

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

Ogni pagina memorizza dati indipendentemente dalle altre (cioè i dati di una pagina non rimandano ai dati di un'altra pagina).

Tutte le pagine sono numerate in ordine naturale (in ordine crescente degli indirizzi), a partire dal numero 0 (la pagina zero inizia con l'indirizzo 0, la prima con 32 KB, la seconda con 64 KB, ecc.)

Il chip di memoria è utilizzato come buffer circolare (ring buffer), cioè prima si registra nella pagina numero 0, poi nella 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 buffer circolare nella memoria NOR flash
All'inizio della pagina viene memorizzata un'intestazione di 4 byte, seguita da un checksum dell'intestazione (CRC-32C), quindi si memorizzano registrazioni nel formato "intestazione, dati, checksum".

L'intestazione della pagina (nello schema di colore verde sporco) è composta da:

  • un campo Magic Number di due byte (che è anche l'indicatore della versione del formato)
    per la versione attuale del formato è considerato come 0xed00 ⊕ numero di pagina;
  • contatore a due byte «Versione della pagina» (numero del ciclo di sovrascrittura della memoria).

Le registrazioni nella pagina vengono memorizzate in forma compressa (viene utilizzato l'algoritmo deflate). Tutte le registrazioni su una pagina vengono compresse in un unico flusso (viene utilizzato un dizionario comune); su ogni nuova pagina la compressione ricomincia da capo. Ciò significa che per la decompresione di qualsiasi registrazione sono necessarie tutte le registrazioni precedenti su quella pagina (e solo su quella).

Ogni registrazione verrà compressa con il flag Z_SYNC_FLUSH, e alla fine del flusso compresso si troveranno 4 byte 0x00, 0x00, 0xff, 0xff, preceduti, forse, da uno o due byte zero.
Questa sequenza (di lunghezza 4, 5 o 6 byte) viene scartata durante la scrittura nella memoria flash.

L'intestazione della registrazione consiste in 1, 2 o 3 byte che memorizzano:

  • un bit (T), che indica il tipo di registrazione: 0 — contesto, 1 — registro;
  • un campo di lunghezza variabile (S) da 1 a 7 bit, che determina la lunghezza dell'intestazione e 'coda' da aggiungere alla registrazione per la decompressione;
  • la lunghezza della registrazione (L).

Tabella dei valori S:

S
Lunghezza dell'intestazione, byte
Scartato durante la 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 illustrare, non so quanto sia chiaro:
La mia implementazione di un buffer circolare nella memoria NOR flash
In giallo è indicato il campo T, in bianco il campo S, in verde L (lunghezza dei dati compressi in byte), in blu i dati compressi, in rosso i byte finali dei dati compressi che non vengono scritti nella memoria flash.

Pertanto, le intestazioni delle registrazioni della lunghezza più comune (fino a 63+5 byte in forma compressa) potranno essere salvate in un solo byte.

Dopo ogni registrazione viene memorizzata una somma di controllo CRC-32C, il cui valore iniziale (init) è l'inverso della somma di controllo precedente.

Il CRC ha la proprietà della 'continuità', funziona (più o meno invertendo i bit nel processo) con questa formula: La mia implementazione di un buffer circolare nella memoria NOR flash.
Quindi, di fatto, calcoliamo il CRC di tutti i byte precedenti delle intestazioni e dei dati su questa pagina.

Immediatamente dopo la somma di controllo si trova l'intestazione della registrazione successiva.

L'intestazione è progettata in modo che il suo primo byte sia sempre diverso da 0x00 e 0xff (se invece del primo byte dell'intestazione troviamo 0xff, significa che si tratta di un'area non utilizzata; 0x00 segnala un errore).

Algoritmi approssimativi

Lettura dalla memoria flash

Qualsiasi lettura avviene con verifica della somma di controllo.
Se il checksum non corrisponde, la lettura viene ripetuta più volte nella speranza di riuscire a leggere i dati corretti.

(questo ha senso, Linux non memorizza nella cache la lettura dalla NOR Flash, verificato)

Scrittura nella memoria flash

Stiamo scrivendo i dati.
Li stiamo leggendo.

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

Preparazione del nuovo chip per il lavoro

Per l'inizializzazione, viene scritto nell'ultima (in realtà zero) pagina un'intestazione con la versione 1.
Dopo di ciò, in questa pagina viene scritto il contesto iniziale (contiene UUID della macchina e impostazioni predefinite).

Tutto, la memoria flash è pronta per il lavoro.

Caricamento della macchina

Durante il caricamento vengono letti i primi 8 byte di ogni pagina (intestazione + CRC); le pagine con un Magic Number sconosciuto o CRC errato vengono ignorate.
Dalle pagine "corrette" vengono selezionate le pagine con la versione massima, da cui viene presa la pagina con il numero più alto.
Viene lettta la prima registrazione, viene verificata la correttezza del CRC e la presenza del flag "contesto". Se tutto è normale, questa pagina è considerata attuale. In caso contrario, torniamo alla pagina precedente finché non troviamo una pagina "viva".
E nella pagina trovata leggiamo tutte le registrazioni; quelle con il flag "contesto" vengono applicate.
Salviamo il dizionario zlib (necessario per la scrittura supplementare in questa pagina).

Tutto, caricamento completato, contesto ripristinato, si può lavorare.

Aggiunta di una registrazione al giornale

Comprimiamo la registrazione con il dizionario corretto, specificando Z_SYNC_FLUSH. Verifichiamo se la registrazione compressa può essere collocata nella pagina attuale.
Se non ci sta (o ci sono stati errori di CRC nella pagina) iniziamo una nuova pagina (vedi sotto).
Scriviamo la registrazione e il CRC. Se si verifica un errore, iniziamo una nuova pagina.

Nuova pagina

Selezioniamo una pagina libera con il numero minimo (consideriamo libera una pagina con un checksum errato nell'intestazione o con una versione inferiore a quella attuale). Se non ci sono tali pagine, selezioniamo la pagina con il numero minimo tra quelle che hanno la versione uguale a quella attuale.
Effettuiamo un erase sulla pagina selezionata. Confrontiamo il contenuto con 0xff. Se qualcosa non va, prendiamo la prossima pagina libera, e così via.
Nella pagina cancellata scriviamo l'intestazione, la prima registrazione è lo stato corrente del contesto, la successiva è la registrazione non scritta del giornale (se disponibile).

Applicabilità del formato

A mio avviso, è stato realizzato un buon formato per la memorizzazione di qualsiasi flusso di informazioni più o meno comprimibile (testo semplice, JSON, MessagePack, CBOR, forse protobuf) nella NOR Flash.

Certo, il formato è 'ottimizzato' per la SLC NOR Flash.

Non dovrebbe essere utilizzato con supporti ad alto BER, come NAND o MLC NOR (esiste davvero una memoria del genere in vendita? Ho trovato solo riferimenti in studi sui codici di correzione).

Inoltre, non dovrebbe essere utilizzato con dispositivi che hanno il proprio FTL: USB flash, SD, MicroSD, ecc. (per tale memoria ho creato un formato con una dimensione della pagina di 512 byte, una firma all'inizio di ogni pagina e numeri univoci per le registrazioni — a volte da una chiavetta 'bloccata' riuscivo a recuperare tutti i dati semplicemente leggendo in sequenza).

A seconda delle esigenze, il formato può essere utilizzato senza modifiche su chiavette da 128Kbit (16Kb) a 1Gbit (128Mb). Se lo si desidera, può essere usato anche su chip di dimensioni maggiori, ma probabilmente è necessario correggere la dimensione della pagina. (Ma qui sorge già la questione della sostenibilità economica, il prezzo della NOR Flash di grande capacità non è allettante).

Se a qualcuno il formato è sembrato interessante e desidera utilizzarlo in un progetto open source — scrivete, cercherò di trovare tempo, sistemare il codice e condividerlo su github.

Conclusione

Come vediamo, alla fine il formato si è rivelato semplice e persino noioso.

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

È possibile che mi sia sbagliato? Certo. Potrebbe benissimo risultare, ad esempio, che abbiamo acquistato un lotto di chip di scarsa qualità. Oppure per qualche altro motivo l'attrezzatura non soddisferà le aspettative di affidabilità.

Ho un piano per questo caso? Penso che dopo aver letto l'articolo non dubitate che ci sia un piano. E non uno solo.

Se vogliamo essere un po' più seri, il formato è stato sviluppato sia come opzione lavorativa, sia come 'prova'.

Al momento, sulla mia scrivania, tutto funziona normalmente, letteralmente nei prossimi giorni la soluzione sarà implementata (circa) su centinaia di dispositivi, vedremo come si comportano in un'operazione "reale" (spero che il formato consenta di rilevare in modo affidabile i guasti; in modo che si possa raccogliere una statistica completa). Dopo qualche mese sarà possibile fare delle conclusioni. (e se non va bene — anche prima).

Se alla fine dell'uso dovessero emergere problemi seri e fosse necessaria un'ulteriore lavorazione, ne scriverò sicuramente.

Letteratura

Non volevo redigere un lungo e noioso elenco di lavori utilizzati, alla fine il Google ce l'hanno tutti.

Qui ho deciso di lasciare l'elenco delle scoperte che mi sono sembrate particolarmente interessanti, ma man mano sono state inserite direttamente nel testo dell'articolo, e nell'elenco è rimasto un solo punto:

  1. Utilità infgen da autore zlib. È in grado di visualizzare in modo comprensibile il contenuto degli archivi deflate/zlib/gzip. Se ti trovi a dover capire la struttura interna del formato deflate (o gzip) — lo consiglio vivamente.

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster