Ottimizzare i tempi di Boot sulla Raspberry Pi

Tempo di avvio di vari filessytem testati.

Tutti noi siamo diventati sempre più esigenti. Vogliamo sistemi molto performanti, estremamente potenti ed immediatamente pronti all'uso. Spero per ottenere questi risultati serve un lavoro a basso livello. In questo articolo verranno illustrate alcune tecniche per identificare, misurare e ottimizzare la fase di boot della Raspberry Pi, e renderla ancora più performante.

Introduzione

Negli ultimi tempi, in particolar modo nel mondo embedded, è nata l'esigenza di velocizzare il più possibile i tempi di avvio del sistema. La motivazione principale è data dagli utenti, che vogliono veder partire il loro sistema nel più breve tempo possibile. Si pensi ad esempio ad una fotocamera digitale, ad una TV (oggi si possono trovare TV su cui gira un sistema GNU/Linux completo), un media-center, etc. Per questi dispositivi l'utente si è abituato ad avere una risposta immediata all'avvio, del tipo "premo il bottone e sono pronto ad usarlo". Non ci rendiamo conto che i dispositivi in questione hanno acquisito funzionalità che prima non avevano: la fotocamera può fare l'upload di foto direttamente su Facebook, la TV si scarica automaticamente informazioni del palinsesto da Internet, e così via. Lo svantaggio di avere a disposizione tali funzionalità si nota all'avvio, dato che in questi casi dobbiamo far partire un sistema totalmente general-purpose (spesso una distro GNU/Linux), invece di un firmware minimale.

Un'altra motivazione che spinge ad avere tempi di avvio rapidi ha una natura più industriale. Avere la possibilità di fare un reset completo di un sistema, a fronte di errori irreversibili di natura software o hardware, in tempi minimali permette di non percepire nemmeno il downtime. Vediamo quindi quali tecniche possono essere utilizzate per far apparire l'avvio di un sistema GNU/Linux general-purpose come l'avvio di un firmware minimale.

Misurare e ottimizzazioni

Prima di parlare di tecniche di ottimizzazione è importante capire come misurare esattamente dove si perde più tempo. L'ottimizzazione infatti deve essere sempre un processo iterativo formato da misure e modifiche (citando Donald Knuth "premature optimization is the root of all evil"). Identificare e misurare in modo dettagliato e affidabile ogni passaggio logico durante il boot è fondamentale per capire come ridurre i tempi di avvio. Una volta identificati i colli di bottiglia possiamo procedere alla rimozione di ciò che non serve, attuare tecniche di parallelizzazione o ritardare il caricamento di determinate funzionalità a fasi successive al boot, in modo da dare il prima possibile all'utente un senso di reattività del sistema.

Le fasi del boot

Una volta chiarita l'importanza della "misura" possiamo passare a classificare i diversi passi che tipicamente caratterizzano l'avvio del sistema.

  • Firmware
    • ROM code (BIOS): codice minimale (spesso non riprogrammabile) che ha il compito di inizializzare le componenti architetturali indispensabili ad effettuare il caricamento di un boot loader
  • Boot-loader
    • Boot-loader primario (first stage): parte minimale di un boot-loader che ha il compito di caricare un boot-loader più completo, denominato boot-loader secondario (questo perché molto spesso la dimensione di byte che il ROM code riesce a caricare è limitata)
    • Boot-loader secondario (second stage): software che ha il compito di caricare il kernel ed eseguirlo
  • Kernel
    • Decompressione del kernel
    • Inizializzazione dell'architettura
    • Initcalls (inizializzazione dei driver)
    • Mount del root filesystem
    • Esecuzione del processo di init (/sbin/init)
  • User-space
    • Esecuzione dei servizi di init (/etc/init.d)
    • Esecuzione di un framework grafico (X server)
    • Avvio dell'interfaccia utente

In genere la fase più onerosa in termini di consumo di tempo è costituita dall'esecuzione delle fasi "kernel" e "user-space". Raramente i boot-loader costituiscono il collo di bottiglia nella fase di boot a causa della loro minimalità (in fin dei conti devono fare una sola cosa: caricare il kernel). Certe volte però (es. su alcuni server) i BIOS possono richiedere addirittura più tempo rispetto al kernel o alla fase "user-space. Nel nostro caso ci concentreremo sulle fasi kernel e user-space, visto che nel 90% dei casi costituiscono i veri colli di bottiglia del boot.

Un caso reale: la Raspberry Pi

Per sperimentare le tecniche proposte prenderemo in esame una piattaforma ben nota alla comunità di Elettronica Open Source: la Raspberry Pi. Come obiettivo ci concentreremo sulla minimizzazione del tempo in cui è possibile fare login via ssh sulla board dopo l'avvio (di fatto potremmo definire anche l'avvio di una semplice shell di sistema, ma la raggiungibilità del sistema via rete permette di analizzare alcune problematiche che troviamo di frequente in casi d'uso reali). Inoltre prenderemo in esame il "cold-boot", cioè il boot a partire da device spento, senza considerare tecniche di suspend o hybernation (suspend to disk).

Fasi di boot della Raspberry Pi

  • ROM code da SoC BCM2835: carica bootloader.bin dall'SD card
  • bootcode.bin: boot-loader di primo livello (caricato dall'SD card)
  • start.elf: boot-loader di secondo livello, usa la GPU per avviare la CPU (caricato dall'SD card)
  • kernel.img: kernel (caricato dall'SD card)
  • montaggio del root filesystem (definito tipicamente su una partizione dell'SD)
  • avvio dell'applicazione user-space: server ssh

Dato che i tempi di caricamento dei boot loader sono ordini di grandezza inferiori rispetto ai tempi richiesti dal kernel e dello user-space, inizieremo ad effettuare le misure e le ottimizzazioni a partire dal kernel.

Misurare i tempi

La tecnica più semplice ed efficace per misurare i tempi nel kernel consiste nell'attivare un particolare parametro di build: CONFIG_PRINTK_TIME. Questo parametro (che si trova nella sezione "Kernel hacking") permette di aggiungere un timestamp ad ogni printk() (equivalente della printf() del C in kernel space). In questo modo semplicemente consultando i log del kernel dopo l'avvio sarà possibile reperire delle utili informazioni riguardo ai tempi richiesti per l'inizializzazione di determinati driver o funzionalità più "core". Esempio di log del kernel dopo che è stata abilitata questa opzione:

# dmesg 
...
[ 0.022030] initcall bcm_mbox_init+0x0/0x38 returned 0 after 0 usecs
[ 0.022054] calling bcm_power_init+0x0/0xa4 @ 1
[ 0.022065] bcm_power: Broadcom power driver
[ 0.022080] bcm_power_open() -> 0
[ 0.022090] bcm_power_request(0, 8)
[ 0.522766] bcm_mailbox_read -> 00000080, 0
[ 0.522783] bcm_power_request -> 0
[ 0.522812] initcall bcm_power_init+0x0/0xa4 returned 0 after 488281 usecs

In realtà questa porzione di log è stata generata utilizzando un'altra opzione fondamentale per il profiling degli step di boot. Abbiamo citato prima le "initcalls" del kernel, cioè tutte le funzioni di inizalizzazione dei driver caricate all'avvio. Aggiungendo ai parametri di boot del kernel l'opzione "initcall_debug" indichiamo al kernel di eseguire una printk() subito prima di chiamare ogni singola initcall e subito dopo il completamento della stessa. L'initcall-tracer (così è chiamata tale funzionalità) riporta anche ulteriori informazioni nell'output della printk(), come il tempo impiegato per l'esecuzione di ciascuna initcall. Questo ci permette di consultare agevolmente a posteriori (cioè a sistema avviato) i tempi richiesti da ciascuna initcall con un semplice comando di shell:

# dmesg | grep " initcall " | sed "s/ */ /g" | sort -n -t' ' -k8 | tail -5
...
[ 0.701759] initcall bcm2708_fb_init+0x0/0xc returned 0 after 32518 usecs
[ 0.794306] initcall pty_init+0x0/0x3e0 returned 0 after 90313 usecs
[ 0.660366] initcall init_kprobes+0x0/0x108 returned 0 after 94523 usecs
[ 1.224203] initcall dwc_otg_driver_init+0x0/0xb8 returned 0 after 402667 usecs
[ 0.518454] initcall bcm_power_init+0x0/0xac returned 0 after 488281 usecs

Il comando in questione ordina le initcall dalla meno onerosa alla più onerosa in termini di tempo.

L'aspetto interessante di questa funzionalità è che non dobbiamo utilizzare nessuno strumento o software esterno. Tutto il necessario è contenuto nel codice del kernel.

Inoltre, possiamo usare la stessa metodologia per misurare i tempi anche dallo user-space, aggregando tutto quanto nell'output di dmesg. Infatti possiamo generare un messaggio "printk()" per mezzo del device /dev/kmsg. Scrivendo una stringa sul file in questione il kernel effettuerà di fatto una printk() utilizzando come argomento la stringa passata. Essendo una printk() come le altre troveremo nei log un timestamp anche per questi messaggi.

Esempio:

root@raspberry:~# echo ciao > /dev/kmsg
root@raspberry:~# dmesg | tail -1
[ 1180.905339] ciao

A questo punto possiamo analizzare a posteriori l'output del dmesg per avere immediatamente un'idea di dove ha senso andare a mettere le mani per ottenere ottimizzazioni significative.

Base di riferimento

Come base di riferimento per i tempi di avvio prendiamo in esame una Raspbian "stock". La Raspbian è una distribuzione totalmente general-purpose (una Debian per la precisione), adattata alla Raspberry Pi. Essendo una distro generica di fatto effettua il boot di un sistema molto simile a un desktop, avvia una GUI grafica, esegue lo start di svariati servizi, etc. Dopo aver effettuato un boot con questa distribuzione possiamo usare la tecnica descritta precedentemente (CONFIG_PRINTK_TIME) e consultare i log del kernel per avere un'idea precisa sui tempi di boot (tralasciamo le initcalls per il momento):

# dmesg
...
[ 3.107163] smsc95xx 1-1.1:1.0: eth0: register 'smsc95xx' at 
usb-bcm2708_usb-1.1, smsc95xx USB 2.0 Ethernet, b8:27:eb:a9:d6:24 
[ 5.664780] EXT4-fs (mmcblk0p2): recovery complete 
[ 5.677326] EXT4-fs (mmcblk0p2): mounted filesystem with ordered data mode. Opts: (null) 
[ 5.689427] VFS: Mounted root (ext4 filesystem) on device 179:2. 
[ 5.700523] devtmpfs: mounted 
[ 5.706028] Freeing init memory: 128K [ 6.257687] init start 
[ 7.337703] udevd[154]: starting version 175 [ 8.511002] Registered led device: led0 
[ 16.041293] EXT4-fs (mmcblk0p2): re-mounted. Opts: (null) 
[ 16.522927] EXT4-fs (mmcblk0p2): re-mounted. Opts: (null) 
[ 25.553157] smsc95xx 1-1.1:1.0: eth0: link up, 100Mbps, full-duplex, lpa 0xC5E1 
[ 28.672178] Adding 102396k swap on /var/swap. Priority:-1 extents:2 across:507900k SS 
[ 30.436046] init done

L'output precedente è restituito dal comando dmesg (che serve a visualizzare su console i log del kernel). L'unica modifica fatta alla distribuzione stock è stata quella di aggiungere una "echo init start > /dev/kmsg" all'inizio del primo script di init eseguito e una "echo init done > /dev/kmsg" al termine dell'ultimo script di init. In questo modo riusciamo a misurare quanto tempo ci mette il kernel a caricare una shell base (ed eseguire il primo script di init) e quanto tempo ci mette ad avviare completamente il sistema (completamento dell'ultimo script di init). Il risultato ottenuto è il seguente:

  • tempo totale per ottenere una shell di sistema: 6.3sec;
  • tempo totale per avviare il server SSH: 30.5sec.

Come possiamo intuire, tali tempi sono assolutamente inadeguati alle nostre aspettative di "boot rapido". In questo caso, però, abbiamo avviato un sistema general-purpose, che fa partire uno svariato numero di servizi che molto probabilmente rimarranno inutilizzati per le nostre esigenze. In questo modo però abbiamo ottenuto una base di valori necessaria a valutare se le ottimizzazioni che andremo ad effettuare sono significative (ed, in definitiva, utili) o meno.

Utilizzo di una distro minimale creata da zero

Per i dettagli di questo approccio si rimanda all'articolo: Embedded GNU/Linux partendo da zero: test sulla Raspberry Pi. Ci limitiamo solo a fornire i riferimenti per i componenti necessari ad effettuare il build dei vari componenti:

  • toolchain di cross-compilazione (gcc, glibc, binutils): git://github.com/raspberrypi/tools.git NOTA: usare la toolchain "hard-float" (arm-bcm2708hardfp-linux-gnueabi) per ottenere binari più performanti
  • Linux (kernel): git://github.com/raspberrypi/linux.git
  • busybox (per generare il rootfs): git://busybox.net/busybox.git
  • dropbear (ssh server minimale): https://matt.ucc.asn.au/dropbear/

Per approfondimenti sui passaggi richiesti si può consultare anche il progetto seguente: rpi-mini-distro.

Compilare un kernel "light"

Per velocizzare il boot del kernel andremo a prendere in esame le initcalls. Abilitando il parametro di boot "initcall_debug" nel kernel stock otteniamo i seguenti risultati:

# dmesg | grep " initcall " | sed "s/ */ /g" | sort -n -t' ' -k8
...
[ 0.810927] initcall iscsi_transport_init+0x0/0x154 returned 0 after 446 usecs 
[ 1.225990] initcall bcm2835_thermal_driver_init+0x0/0xc returned 0 after 451 usecs 
[ 1.232472] initcall pm_qos_power_init+0x0/0xac returned 0 after 713 usecs 
[ 1.230686] initcall sysctl_ipv4_init+0x0/0x98 returned 0 after 755 usecs 
[ 0.810406] initcall vchiq_init+0x0/0x1dc returned 0 after 1565 usecs 
[ 1.228797] initcall sdhci_drv_init+0x0/0xc returned 0 after 1829 usecs 
[ 0.560116] initcall inet_init+0x0/0x260 returned 0 after 2739 usecs 
[ 0.808747] initcall loop_init+0x0/0x11c returned 0 after 4852 usecs 
[ 0.803722] initcall brd_init+0x0/0x1c0 returned 0 after 8230 usecs 
[ 0.540586] initcall genhd_device_init+0x0/0x84 returned 0 after 9765 usecs 
[ 0.556774] initcall chr_dev_init+0x0/0xd8 returned 0 after 12234 usecs
[ 0.538982] initcall param_sysfs_init+0x0/0x1d4 returned 0 after 19531 usecs 
[ 0.701759] initcall bcm2708_fb_init+0x0/0xc returned 0 after 32518 usecs 
[ 0.794306] initcall pty_init+0x0/0x3e0 returned 0 after 90313 usecs 
[ 0.660366] initcall init_kprobes+0x0/0x108 returned 0 after 94523 usecs 
[ 1.224203] initcall dwc_otg_driver_init+0x0/0xb8 returned 0 after 402667 usecs 
[ 0.518454] initcall bcm_power_init+0x0/0xac returned 0 after 488281 usecs

Questa non è la lista completa di tutte le initcalls ma è semplicemente la classifica delle più onerose; si può benissimo capire che le funzioni che richiedono meno di 1000 usec non portano un grande contributo ai tempi di boot, quindi non sarà molto significativo rimuoverle o andare ad ottimizzare il loro codice. Per questa fase ci concentreremo sulle funzioni che compaiono in basso, che sono quelle che richiedono più tempo all'avvio.

In particolar modo, senza entrare nel dettaglio implementativo di driver o funzionalità specifiche della Raspberry Pi, possiamo mettere in evidenza le seguenti funzioni:

  • init_kprobes: inizializzazione dei kernel probes (strumento di debug che possiamo benissimo rimuovere per le nostre esigenze)
  • pty_init: inizializzazione degli pseudo-terminali (in questo caso possiamo andare a ridurre il numero di pseudo-terminali creati all'avvio per velocizzare questa fase)
  • brd_init: possiamo rimuovere completamente questa funzionalità se non abbiamo necessità di usare porzioni di RAM come block device (CONFIG_BLK_DEV_RAM)
  • loop_init: possiamo rimuovere questa funzionalità (CONFIG_BLK_DEV_LOOP) se non abbiamo necessità di montare file in loopback
  • iscsi_transport_init: possiamo rimuovere anche questo se non vi è necessità di utilizzare il protocollo iSCSI (CONFIG_ISCSI_TCP)

Questo semplice passaggio di analisi delle initcalls ci ha permesso di identificare subito alcune funzionalità da rimuovere per rendere il nostro kernel più leggero e rapido al boot. In questa fase non dobbiamo nemmeno entrare nel merito del codice, semplicemente basterà ricompilare il kernel senza le funzionalità (generiche, non specifiche dell'architettura) che abbiamo identificato. Altre ottimizzazioni che possiamo effettuare a livello kernel:

  • Preset dei loops_per_jiffy: il kernel all'avvio valuta quanti loop la CPU riesce a fare nell'intervallo di tempo delimitato da due timer interrupt (questa misura viene usata successivamente per implementare attese inferiori al "jiffy" - timer interrupt); per effettuare tale calibrazione dobbiamo quindi attendere l'occorrenza di due timer interrupt; dato che il kernel imposta la frequenza del timer a 100 Hz (CONFIG_HZ=100) l'attesa richiesta ad effettuare tale calibrazione è pari a 1 / 100 ms = 250 ms; passando direttamente il valore corretto ai parametri di boot (lpk=3489792) possiamo risparmiarci questa calibrazione e salvare altri 250 ms.
  • Ridurre la verbosità dei messaggi del kernel stampati su console: rimuovere i parametri "console=XXX" o aggiungere "quiet" ai parametri di boot: in questo modo ci risparmiamo un po' di write effettuate su console (spesso seriale) al boot da parte del kernel, facendoci risparmiare altro empo prezioso.
  • Usare LZO come algoritmo di compressione del kernel (CONFIG_KERNEL_LZO): comprimere il kernel (ed effettuare la decompressione "live" a run-time)

Dimensioni del kernel ottimizzato

Alla fine, dopo le ottimizzazioni effettuate abbiamo ottenuto un'immagine del kernel più leggera:

Dimensioni dell'immagine del kernel, prima e dopo le ottimizzazioni effettuate.

Da una size di circa 2.6MB del kernel stock siamo passati a circa 1.6MB. Oltre a rimuovere le funzioni che richiedono più tempo al boot, un'immagine più leggera del kernel permette anche di ridurre le letture che il boot-loader deve eseguire per caricare il kernel in memoria all'avvio. Quindi, oltre a fare un'analisi in termini di tempo (specialmente se identifichiamo un bottleneck a livello di boot-loader), può essere utile fare anche un'analisi in termini di spazio e identificare le componenti kernel che contribuiscono maggiormente ad incrementare le dimensioni dell'immagine complessiva.

Per avere un'idea dei componenti binari che occupano più spazio all'interno dell'immagine del kernel si può lanciare il comando seguente nella directory di build del kernel:

# size `find -name \*.o ` | sort -n -k1

Tale comando ordina i singoli file compilati per dimensione dell'area codice. Analizzando gli ultimi elementi che compaiono in output possiamo identificare facilmente i singoli file oggetto che occupano più spazio all'interno dell'immagine complessiva del kernel, e procedere alla rimozione di tali funzionalità, analogamente a come abbiamo fatto durante la fase di profiling "temporale".

Il root filesystem: contenuti

Il prossimo passo consiste nell'ottimizzare lo user-space, creandoci un root filesystem minimale. Inoltre possiamo anche attuare delle tecniche di velocizzazione del boot scegliendo una opportuna tecnologia di filesystem.

Per i contenuti del filesystem abbiamo seguito il solito approccio di "minimalità", partendo direttamente da busybox (anche per i dettagli di questa fase si rimanda all'articolo Embedded GNU/Linux partendo da zero: test sulla Raspberry Pi).

Inoltre abbiamo aggiunto ai contenuti un server SSH minimale: dropbear.

Dopo aver compilato i binari possiamo utilizzare un tool molto utile per ridurre la dimensione delle librerie di sistema al minimo: mklibs (il tool in questione è disponibile in tutte le distribuzioni Debian/Ubuntu e distro derivate). In breve, questo strumento accetta come argomenti una lista di binari e una lista di librerie e si occupa di identificare solo le librerie necessarie ad eseguire i binari in questione. Inoltre effettua uno "strip" delle librerie selezionate, rimuovendo le aree all'interno dei binari (commenti, debug, etc.) non strettamente richiesti per l'esecuzione dei binari stessi.

Questa metodologia di generazione del root filesystem ci permette di far entrare un sistema GNU/Linux minimale con un server SSH in soli 3.2MB.

Scelta della tecnologia di filesystem

Una volta determinati i contenuti è necessario scegliere la tecnologia di filesystem che più si adatta ai nostri requisiti di "partenza rapida".

Benché i filesystem con journal forniscano garanzie maggiori sull'integrità dei dati, anche in seguito a crash, nel nostro caso comportano invece uno svantaggio, a causa del gran numero di operazioni di WRITE che devono essere fatte in fase di mount (e anche successivamemente, per tenere traccia dello stato di ciascuna transizione).

Per evitare tali operazioni al boot e garantire al tempo stesso l'integrità dei dati, possiamo utilizzare un filesystem read-only per l'immagine di sistema: in fin dei conti i binari (busybox, dropbear, etc.) e le librerie di sistema non cambieranno mai a meno che non si debba fare un re-flash completo del sistema, ma questa operazione può essere fatta tranquillamente off-line utilizzando un altro sistema (es: il PC che abbiamo usato per compilare il tutto).

Se il nostro sistema non necessita di salvarsi uno stato permanente, oltre all'immagine di sistema "read-only", possiamo predisporre un filesystem di tipo read-write utilizzando una porzione di RAM. Per questo ci viene in aiuto il tmpfs, un filesystem messo a disposizione da Linux che permette di usare una porzione di RAM, invece di un disco, una flash, etc. Per questo particolare caso il tmpfs viene usato solo per i log di sistema (directory /var/log). Di seguito riportiamo un grafico che illustra le varie tecnologie di filesystem testate, misurando i tempi di boot al variare di esse:

Tempo di avvio del sistema al variare della tecnologia di filesystem utilizzata.

La tecnolgia squashfs è tipicamente la migliore in questi casi: squashfs è un filesystem read-only che provvede anche alla compressione dei dati (la compressione può essere scelta tra GZIP, LZO o XZ - tipicamente LZO garantisce quasi sempre il miglior risultato in termini di fattore di compressione e velocità di compressione/decompressione). In questo particolare caso ext2 e addirittura il vecchio filesystem MINIX (entrambi montati in read-only) hanno dato risultati ottimi, anche migliori di squashfs. Questo perchè l'immagine di sistema è cosí minimale in questo caso (si hanno praticamente solo binari) che l'ulteriore compressione dei dati effettuata da squashfs rappresenta più uno svantaggio che un vantaggio.

Risultati finali

Una volta generato un kernel custom "light" opportunamente ottimizzato e un root filesystem minimale, possiamo finalmente fare boot con il nostro sistema e misurare con precisione quanto abbiamo migliorato i tempi di boot (sempre utilizzando la tecnica vista all'inizio dell'articolo con le printk() e l'opzione CONFIG_PRINTK_TIME). L'output di dmesg in questo caso risulta essere il seguente:

... 
[ 1.144025] Freeing init memory: 92K 
[ 1.228550] init start 
[ 1.249067] waiting eth0 to come up 
[ 1.345882] usb 1-1: new high-speed USB device number 2 using dwc_otg 
[ 1.346059] Indeed it is in host mode hprt0 = 00001101 
[ 1.556942] hub 1-1:1.0: USB hub found 
[ 1.557074] hub 1-1:1.0: 3 ports detected 
[ 1.836029] usb 1-1.1: new high-speed USB device number 3 using dwc_otg 
[ 1.940066] smsc95xx v1.0.4 
[ 1.951858] smsc95xx 1-1.1:1.0 eth0: register 'smsc95xx' at usb-bcm2708_usb-1.1, 
smsc95xx USB 2.0 Ethernet, b8:27:eb:a9:d6:24 
[ 2.086695] init done 
[ 3.406004] smsc95xx 1-1.1:1.0 eth0: link up, 100Mbps, full-duplex, lpa 0xC5E1

Anche in questo caso abbiamo aggiunto due messaggi (tramite echo "messaggio > /dev/kmsg) nello user-space per valutare quando si ha a disposizione una shell e quando la procedura di init è effettivamente completata. Risultato: con questo sistema riusciamo ad avere una shell di sistema dopo circa 1.23 sec. ed è possibile fare login via SSH dopo solamente dopo circa 3.4 sec. Un bel risultato, se si pensa che con una distro "stock" riuscivamo ad accedere al sistema via SSH solo dopo 30 sec.

Conclusioni e ulteriori tecniche di ottimizzazione

In questo articolo abbiamo illustrato alcune tecniche che possono essere applicate per velocizzare i tempi di boot di una tipica piattaforma embedded, la Raspberry Pi, utilizzando come caso d'uso la raggiungibilità via SSH.

Praticamente quasi tutte le ottimizzazioni che abbiamo fatto consistono nel rimuovere funzionalità non strettamente necessarie in fase di boot, magari ritardando la loro esecuzione, in modo da dare quanto prima un feedback all'utente, in modo che si possa percepire dall'esterno che la nostra board è partita.

Spesso queste tecniche permettono di fornire tempi di avvio ottimi, senza necessariamente dover analizzare aspetti implementativi di particolari driver o codice kernel.

Nel caso di sistemi più complessi, può valer la pena analizzare tecniche più evolute, come la parallelizzazione del codice (es. in certi casi parallelizzare le initcalls in modo da ridurre i tempi di attesa della CPU permette di ridurre ulteriormente i tempi di boot).

Oppure in presenza di root filesystem più completi (es. dovendo avviare una GUI, come un'applicazione Qt), può valer la pena prendere in esame tecniche di riorganizzazione del codice, ad esempio cercare di compattare nello stesso blocco di flash, SD, disco, etc. le funzioni di codice immediatamente richieste al boot ed evitare la loro dispersione tra blocchi distinti.

Altre tecniche più evolute che non abbiamo preso in esame sono costituite dal prelink o preload di blocchi: il prelink permette di pre-caricare gli indirizzi delle funzioni di libreria esterne direttamente all'interno dei binari, invece di valutarli in fase di load. Il preload permette di sfruttare le attese della CPU (es. quando si deve attendere il completamento della init di un dispositivo) per precaricare in RAM i blocchi del filesystem cui si farà certamente accesso al boot.

Nel nostro particolare esempio ci siamo comunque limitati alle tecniche più semplici, visto che sono state sufficienti a garantirci ottimi tempi di boot e un miglioramento notevole rispetto ai risultati ottenuti con la distribuzione stock da cui siamo partiti.

 

Quello che hai appena letto è un Articolo Premium reso disponibile affinché potessi valutare la qualità dei nostri contenuti!

 

Gli Articoli Tecnici Premium sono infatti riservati agli abbonati e vengono raccolti mensilmente nella nostra rivista digitale EOS-Book in PDF, ePub e mobi.
volantino eos-book1
Vorresti accedere a tutti gli altri Articoli Premium e fare il download degli EOS-Book? Allora valuta la possibilità di sottoscrivere un abbonamento a partire da € 2,95!
Scopri di più

9 Comments

  1. Giorgio B. Giorgio B. 17 settembre 2013
  2. Andrea Righi 17 settembre 2013
  3. Giorgio B. Giorgio B. 18 settembre 2013
  4. Andrea Righi 18 settembre 2013
  5. IvanScordato Ivan Scordato 18 settembre 2013
  6. Giorgio B. Giorgio B. 19 settembre 2013
  7. alius 8 aprile 2015
  8. oklahoma 15 gennaio 2016

Leave a Reply