Embedded GNU/Linux partendo da zero: preparazione del root filesystem

Embedded GNU/Linux partendo da zero

L’obiettivo di questa serie di articoli è di fornire una guida passo passo per la creazione da zero di un sistema GNU/Linux perfettamente funzionante, applicabile ad esempio in scenari di tipo embedded come base per la generazione di un semplice BSP (Board Support Package), di un firmware Linux-based, o di una micro-distribuzione totalmente custom.

Introduzione

Nella prima puntata (http://it.emcelettronica.com/embedded-gnulinux-partendo-da-zero-ricompilare-kernel) abbiamo visto come ricompilare un kernel Linux per una generica board di sviluppo virtuale ARM-based.

Il kernel rappresenta una sorta di contenitore di applicazioni che girano in spazio utente. In questa nuova puntata vedremo come preparare i contenuti dello spazio utente, ovvero come preparare un root filesystem minimale.

Creare un root filesystem in RAM: l’initramfs

Nella puntata precedente ci siamo lasciati con il seguente errore restituito dal kernel che abbiamo compilato:

VFS: Cannot open root device "(null)" or unknown-block(0,0): error -6
Please append a correct "root=" boot option; here are the available
partitions:

Il kernel è partito correttamente, ma non è riuscito a trovare un root filesystem da montare per avviare il processo init e passare il controllo allo user-space.

La soluzione più semplice per creare un root filesystem è di usare parte della RAM come se fosse un filesystem vero e proprio. Il kernel permette di includere al suo interno un archivio in formato cpio [1] (in pratica è una sorta di tar o zip).

Tale archivio, chiamato initramfs [2], viene incluso nell’immagine del kernel ad una locazione prefissata e viene estratto dal kernel stesso in RAM durante l’inizializzazione.

Questa soluzione ha il vantaggio di non richiedere nessun device driver aggiuntivo per flash, dischi o supporti di memoria esterni di qualsiasi natura, dato che per il kernel è sufficiente poter accedere solo alla RAM per montare l’initramfs. Spesso questo approccio viene utilizzato in ambito embedded nelle fasi iniziali di testing delle nuove board per avere praticamente a tempo zero un sistema GNU/Linux funzionante.

Vediamo un esempio pratico di un initramfs contenente un unico file: il classico esempio “hello world”.

Per prima cosa scriviamo un semplice programma C in /tmp/init.c:

#include <stdio.h&gt

int main(int argc, char **argv)
{
        while (1) {
                printf("Hello, world!\n");
                sleep(1);
        }
}

E lo cross-compiliamo con lo stesso cross-compilatore che abbiamo usato nella puntata scorsa per compilare il kernel:

arm-linux-gnueabihf-gcc -static -o /tmp/init /tmp/init.c

Quiz #1: come mai abbiamo usato l’opzione -static?

Risposta #1: Con l’opzione “-static” richiediamo al compilatore di includere direttamente nel binario stesso tutte le librerie necessarie, invece di tentare di caricarle dinamicamente a run-time. Come abbiamo detto il nostro initramfs conterrà un unico file, quindi a run-time non sarà possibile trovare le librerie dinamiche all’interno del filesystem.

Quiz #2: come mai il programma C che abbiamo scritto contiene un loop che non esce mai?

Risposta #2: In Linux ogni processo viene generato come figlio di un altro processo. Quando un processo padre termina prima del processo figlio quest’ultimo viene immediatamente adottato dal processo di sistema speciale “init” (il cui PID è 1), che ne diviene il nuovo processo padre. Questa operazione viene chiamata re-parenting, ed avviene automaticamente a cura del kernel. La terminazione del processo “init” viene vista dal kernel come un errore irreversibile e quando accade il kernel entra nello stato di panic. Per questo motivo il nostro init non può terminare.

A questo punto possiamo spostarci in /tmp e creare l’initramfs:

$ cd /tmp
$ echo init | cpio -o --format=newc | gzip -9 > initramfs
$ cd -

E’ importante notare che il binario contenuto all’interno dell’initramfs è stato chiamato “init”. Questo perché il kernel si aspetta di trovare un file con questo nome; se rinominiamo il file questo non verrà eseguito al termine del processo di boot.

Notare inoltre che con “gzip -9″ abbiamo compresso l’initramfs. Il kernel supporta vari formati di compressione e riesce ad accedervi anche in questo caso, a costo di spendere un po’ più di tempo in fase di boot per effettuare la decompressione. Tuttavia, in certi casi, perdere un po’ più di tempo al boot per ridurre lo spazio occupato può essere vantaggioso, ad esempio se dovessimo collocare kernel e initramfs in una flash di dimensioni ridotte, o scaricarli da una connessione di rete lenta, etc.

L’initramfs che abbiamo creato noi richiede uno spazio di 244KB:

$ du -h /tmp/initramfs
244K	/tmp/initramfs

E’ giunto il momento di testare se tutto funziona. Utilizziamo quindi lo stesso comando visto nella puntata scorsa per avviare la board virtuale, con l’aggiunta dell’opzione “-initrd /tmp/initramfs” per caricare anche l’initramfs:

$ qemu-system-arm -M vexpress-a9 -kernel ./arch/arm/boot/zImage \
  -serial stdio -display none -append "console=ttyAMA0" \
  -initrd /tmp/initramfs

Il risultato è il seguente:

Booting Linux on physical CPU 0
Initializing cgroup subsys cpuset
Linux version 3.5.4 (righiandr@thinkpad) (gcc version 4.7.2 20120910
 (prerelease) (crosstool-NG linaro-1.13.1-2012.09-20120921 - Linaro GCC
 2012.09) ) #1 SMP Sat Oct 27 17:12:15 CEST 2012
CPU: ARMv7 Processor [410fc090] revision 0 (ARMv7), cr=10c53c7d
CPU: PIPT / VIPT nonaliasing data cache, VIPT aliasing instruction cache
Machine: ARM-Versatile Express
Memory policy: ECC disabled, Data cache writealloc
sched_clock: 32 bits at 24MHz, resolution 41ns, wraps every 178956ms
PERCPU: Embedded 7 pages/cpu @805a2000 s5824 r8192 d14656 u32768
Built 1 zonelists in Zone order, mobility grouping on.  
Total pages: 32512
Kernel command line: console=ttyAMA0
PID hash table entries: 512 (order: -1, 2048 bytes)
Dentry cache hash table entries: 16384 (order: 4, 65536 bytes)
Inode-cache hash table entries: 8192 (order: 3, 32768 bytes)
Memory: 128MB = 128MB total
Memory: 124924k/124924k available, 6148k reserved, 0K highmem
Virtual kernel memory layout:
    vector  : 0xffff0000 - 0xffff1000   (   4 kB)
    fixmap  : 0xfff00000 - 0xfffe0000   ( 896 kB)
    vmalloc : 0x88800000 - 0xff000000   (1896 MB)
    lowmem  : 0x80000000 - 0x88000000   ( 128 MB)
    modules : 0x7f000000 - 0x80000000   (  16 MB)
      .text : 0x80008000 - 0x80425d84   (4216 kB)
      .init : 0x80426000 - 0x804516c0   ( 174 kB)
      .data : 0x80452000 - 0x80480520   ( 186 kB)
       .bss : 0x80480544 - 0x8049e928   ( 121 kB)
SLUB: Genslabs=11, HWalign=64, Order=0-3, MinObjects=0, CPUs=1, Nodes=1
Hierarchical RCU implementation.
NR_IRQS:256
Console: colour dummy device 80x30
Calibrating delay loop... 545.58 BogoMIPS (lpj=2727936)
pid_max: default: 32768 minimum: 301
Mount-cache hash table entries: 512
CPU: Testing write buffer coherency: ok
CPU0: thread -1, cpu 0, socket 0, mpidr 80000000
hw perfevents: enabled with ARMv7 Cortex-A9 PMU driver, 1 counters available
Setting up static identity map for 0x603242d8 - 0x60324330
Brought up 1 CPUs
SMP: Total of 1 processors activated (545.58 BogoMIPS).
NET: Registered protocol family 16
L310 cache controller enabled
l2x0: 8 ways, CACHE_ID 0x410000c8, AUX_CTRL 0x02420000, 
Cache size: 131072 B
hw-breakpoint: debug architecture 0x0 unsupported.
Serial: AMBA PL011 UART driver
mb:uart0: ttyAMA0 at MMIO 0x10009000 (irq = 37) is a PL011 rev1
console [ttyAMA0] enabled
mb:uart1: ttyAMA1 at MMIO 0x1000a000 (irq = 38) is a PL011 rev1
mb:uart2: ttyAMA2 at MMIO 0x1000b000 (irq = 39) is a PL011 rev1
mb:uart3: ttyAMA3 at MMIO 0x1000c000 (irq = 40) is a PL011 rev1
bio: create slab  at 0
SCSI subsystem initialized
usbcore: registered new interface driver usbfs
usbcore: registered new interface driver hub
usbcore: registered new device driver usb
Advanced Linux Sound Architecture Driver Version 1.0.25.
Switching to clocksource v2m-timer1
NET: Registered protocol family 2
IP route cache hash table entries: 1024 (order: 0, 4096 bytes)
TCP established hash table entries: 4096 (order: 3, 32768 bytes)
TCP bind hash table entries: 4096 (order: 3, 32768 bytes)
TCP: Hash tables configured (established 4096 bind 4096)
TCP: reno registered
UDP hash table entries: 256 (order: 1, 8192 bytes)
UDP-Lite hash table entries: 256 (order: 1, 8192 bytes)
NET: Registered protocol family 1
RPC: Registered named UNIX socket transport module.
RPC: Registered udp transport module.
RPC: Registered tcp transport module.
RPC: Registered tcp NFSv4.1 backchannel transport module.
Unpacking initramfs...
Freeing initrd memory: 240K
jffs2: version 2.2. (NAND) © 2001-2006 Red Hat, Inc.
msgmni has been set to 244
io scheduler noop registered (default)
clcd-pl11x ct:clcd: PL111 rev2 at 0x10020000
clcd-pl11x ct:clcd: CT-CA9X4 hardware, XVGA display
Console: switching to colour frame buffer device 128x48
v2m_cfg_write: writing 03c8eee0 to 00110001
v2m_cfg_write: writing 00000000 to 00710000
v2m_cfg_write: writing 00000002 to 00b10000
smsc911x: Driver version 2008-10-21
smsc911x-mdio: probed
smsc911x smsc911x: eth0: attached PHY driver [Generic PHY] 
(mii_bus:phy_addr=smsc911x-fffffff:01, irq=-1)
smsc911x smsc911x: eth0: MAC Address: 52:54:00:12:34:56
isp1760 isp1760: NXP ISP1760 USB Host Controller
isp1760 isp1760: new USB bus registered, assigned bus number 1
isp1760 isp1760: Scratch test failed.
isp1760 isp1760: can't setup
isp1760 isp1760: USB bus 1 deregistered
isp1760: Failed to register the HCD device
Initializing USB Mass Storage driver...
usbcore: registered new interface driver usb-storage
USB Mass Storage support registered.
mousedev: PS/2 mouse device common for all mice
rtc-pl031 mb:rtc: rtc core: registered pl031 as rtc0
mmci-pl18x mb:mmci: mmc0: PL181 manf 41 rev0 at 0x10005000 irq 41,42 (pio)
usbcore: registered new interface driver usbhid
usbhid: USB HID core driver
aaci-pl041 mb:aaci: ARM AC'97 Interface PL041 rev0 at 0x10004000, irq 43
aaci-pl041 mb:aaci: FIFO 512 entries
oprofile: using arm/armv7-ca9
TCP: cubic registered
NET: Registered protocol family 17
VFP support v0.3: implementor 41 architecture 3 part 30 variant 9 rev 0
input: AT Raw Set 2 keyboard as /devices/mb:kmi0/serio0/input/input0
rtc-pl031 mb:rtc: setting system clock to 2012-10-27 15:17:33 UTC (1351351053)
ALSA device list:
  #0: ARM AC'97 Interface PL041 rev0 at 0x10004000, irq 43
Freeing init memory: 172K
input: ImExPS/2 Generic Explorer Mouse as 
/devices/mb:kmi1/serio1/input/input1
hello world
hello world
hello world
hello world
hello world
...

Molto bene! Il kernel ha avviato il nostro loop “hello world” al termine del processo di boot.

Notare che a questo punto abbiamo un kernel Linux completo a nostra disposizione.

Potremmo quindi modificare il semplice “hello world” (/tmp/init.c) per implementare anche qualcosa di più complesso, come ad esempio un piccolo echo server TCP/IP multi-task.

Il programma seguente crea un processo server che accetta connessioni TCP/IP sulla porta 8080; per ogni connessione ricevuta viene creato un nuovo processo figlio che legge un messaggio dal client e lo rimanda indietro tale e quale (tutto tramite TCP/IP):

/*
 * Simple TCP/IP echo server.
 */
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define ECHO_PORT       8080

static void echo_message(int sock, struct sockaddr_in *addr, socklen_t *len)
{
        char message[4096];
        int n;

        /* Read messages from the client */
        while ((n = recvfrom(sock, message, sizeof(message),
                             0, (struct sockaddr *)addr, len)) > 0) {
                /* Send the same message back to the client */
                sendto(sock, message, n, 0, (struct sockaddr *)addr,
                       sizeof(struct sockaddr_in));
                /* Print the message to the console */
                message[n] = '\0';
                printf("Message received:\n");
                printf("%s", message);
        }
}

int main(int argc, char**argv)
{
        int server, client;
        struct sockaddr_in servaddr = {}, clientaddr;
        socklen_t len = sizeof(clientaddr);

        server = socket(AF_INET, SOCK_STREAM, 0);

        servaddr.sin_family = AF_INET;
        servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
        servaddr.sin_port = htons(8080);
        bind(server, (struct sockaddr *)&servaddr, sizeof(servaddr));

        listen(server, 1024);

        printf("Echo server started on port: %u\n", ECHO_PORT);

        signal(SIGCHLD, SIG_IGN);
        for (;;) {
                pid_t pid;

                client = accept(server, (struct sockaddr *)&clientaddr, &len);

                pid = fork();
                if (pid == 0) {
                        close(server);
                        echo_message(client, &clientaddr, &len);
                        exit(0);
                }
                close(client);
        }
}

Se sostituiamo l’esempio “hello world” con questo codice e ripetiamo i passi di generazione dell’initramfs possiamo vedere in funzione l’echo server TCP/IP creato completamente da zero.

Per prima cosa lanciamo la board virtuale con le seguenti opzioni:

$ qemu-system-arm -M vexpress-a9 -kernel ./arch/arm/boot/zImage \
  -serial stdio -display none -append "console=ttyAMA0 ip=10.0.2.15" \
  -initrd /tmp/initramfs -redir tcp:8080:10.0.2.15:8080
...
Echo server started on port: 8080

L’opzione “-redir tcp:8080:10.0.2.15:8080″ permette di creare una conessione di rete virtuale dal sistema GNU/Linux host alla board virtuale tramite la porta 8080, mentre con “ip=10.0.2.15″ diciamo al kernel di assegnare l’indirizzo IP 10.0.2.15 alla prima interfaccia di rete rilevata.

Una volta partita la board virtuale, il nostro kernel e la nostra applicazione user-space possiamo testare l’echo server lanciando il comando seguente sul sistema host:

$ echo ciao | nc localhost 8080
ciao

Perfetto! La nostra applicazione ha risposto alla richiesta di echo dalla board virtuale. Sul terminale di quest’ultima compare il messaggio seguente:

...
Echo server started on port: 8080
Message received:
ciao

Gli sviluppi futuri di questa applicazione sono lasciati alla fantasia e alla creatività del lettore. 😉

Nella prossima puntata vedremo come creare un initramfs completo, dotato dei tipici comandi presenti in qualsiasi distribuzione GNU/Linux.

Riferimenti

  1. http://en.wikipedia.org/wiki/Cpio
  2. http://lxr.linux.no/linux+v3.6.3/Documentation/early-userspace/README

 

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. Boris L. 15 novembre 2012
  2. Emanuele Emanuele 15 novembre 2012
  3. Piero Boccadoro Piero Boccadoro 15 novembre 2012
  4. and792 15 novembre 2012
  5. mneroni 17 novembre 2012
  6. Piero Boccadoro Piero Boccadoro 18 novembre 2012
  7. Emanuele Emanuele 29 novembre 2012
  8. Andrea Righi 30 novembre 2012
  9. Emanuele Emanuele 1 dicembre 2012

Leave a Reply