Laboratorul 06. Root filesystem și servicii de sistem

Root file system

Pentru ca sistemul să fie inițializat corect după pornirea kernel-ului, este necesar ca toate script-urile și executabilele necesare pentru a porni daemon-ul de inițializare, init, și restul proceselor user-space, să existe în anumite locații în sistemul de fișiere. Acest sistem minimal de fișiere necesar la inițializare poartă numele de root file system sau rootfs. În cazul sistemelor Unix, rootfs-ul are ca rădăcină directorul / și înglobează o serie de directoare ce contin restul de fișiere necesare. De obicei, în directorul / nu se află niciun fișier, doar subdirectoare. Aceste subdirectoare sunt organizate în funcție de fișierele pe care le conțin:

  • /bin - conține programe și comenzi necesare la inițializare ce pot fi folosite apoi de către un utilizator neprivilegiat în timpul unei sesiuni
  • /sbin - conține programe asemănătoare cu cele din /bin ca utilitate, dar care pot fi utilizate în general doar de utilizatorul privilegiat root
  • /etc - conține fișierele de configurare specifice sistemului
  • /lib - conține bibliotecile folosite de programele din rootfs (cele din /bin și /sbin)
  • /dev - conține referințe la toate dispozitivele periferice; aceste referințe sunt în general fișiere speciale
  • /boot - conține fișierele necesare bootării și imaginea kernel-ului; este posibil ca acestea să fie păstrate pe un sistem de fișiere separat de rootfs
  • /tmp - conține fișiere temporare și este de obicei curățat la repornirea sistemului
  • /opt - conține în principal aplicațiile third-party
  • /proc, /usr, /mnt, /var, /home - sunt directoare care, în general, reprezintă puncte în care se pot monta alte sisteme de fișiere pentru a stoca log-uri, biblioteci și alte aplicații și programe folosite de către utilizatori.

Organizarea unui rootfs Linux în termeni de directoare este bine definită de standardul ierarhiei sistemului de fișiere: Filesystem Hierarchy Standard, ultimul standard FHS 3.0 fiind definit in 2015.

Standardul Unix recomandă ca rootfs-ul să aibe dimensiuni relativ mici, deoarece orice problemă sau corupere a acestuia poate împiedica inițializarea corectă a sistemului. Dacă, totuși, rootfs-ul devine corupt, există modalități prin care poate fi remediat. O soluție des folosită este de a monta acest sistem de fișiere într-un alt sistem, funcțional, pentru a putea fi explorat și verificat.

În cazul sistemelor embedded, de multe ori spațiul de stocare pe care îl avem la dispoziție este deja limitat: < 32MB. De cele mai multe ori, o bună parte din acest spațiu este ocupat de imaginea kernel-ului. De aceea, în majoritatea cazurilor, rootfs-ul pentru aceste sisteme este fie compus doar din minimul de programe necesar pentru funcționare, fie este stocat pe un server accesibil prin rețea și montat de către kernel la inițializare (e.g., folosind protocolul NFS).

Pseudo Filesystems

Un kernel Linux expune mai multe pseudo-sisteme de fișiere, precum /dev (unde există noduri pentru toate dispozitivele detectate), /proc sau /sys. Unele aplicații ce gestionează sistemul de operare au nevoie ca aceste mountpointuri să existe, ele fiind folosite pentru a interfața cu kernelul (mai ales în sisteme incorporate,unde programele trebuie să comunice cu hardware-ul).

Sistemul virtual proc

Acest sistem de fișiere există de când există și Linux-ul și permite expunerea de statistici despre procesele rulate în sistem dar și ajustarea la runtime a diverșilor parametrii ce implică managementul proceselor, ale memoriei etc. Este utilizat de majoritatea aplicațiilor standard. Aplicații precum ps și top nu pot funcționa fără acest filesystem.

Pentru a monta proc se poate folosi comanda:

 mount -t proc nodev /proc 

Printre conținutul aflat în proc putem găsi:

  • /proc/interrupts, /proc/iomem, /proc/cpuinfo: ce conțin informații specifice de device
  • /proc/<pid>, /proc/3840/cmdline: conține detalii despre fișierele deschise de proces, utilizarea memoriei, procesorului etc.
  • /proc/cmdline: conține linia de comandă cu care a fost pornită imaginea kernel-ului
  • /proc/sys: conține fișiere care pot fi scrise pentru a ajusta parametrii ai kernel-ului. Poartă numele de sysctl. Mai multe detalii despre asta aici: documentație sysctl.

Sistemul virtual sys

Permite reprezentarea în userspace a viziunii pe care o are kernel-ul asupra magistralelor, dispozitivelor și driverelor din sistem. Este util pentru diverse aplicații din userspace care trebuie să enumere și să interogheze hardware-ul disponibil, de exemplu udev sau mdev:

ls /sys/
block bus class dev devices firmware fs kernel module power

Utilitare folosite pentru crearea / inspecția unui RootFS

Uneori vom fi în situația în care nu avem acces la un server și imaginea de care dispunem pentru sistemul nostru nu este satisfăcătoare. În aceste condiții va trebui să ne creăm propriul rootfs pe care să îl scriem în memoria sistemului embedded.

Imaginea recomandată pentru RaspberryPi, Raspbian, conține două partiții: o partiție FAT folosită pentru boot și o partiție ext4 pentru rootfs. Fiecare dintre aceste partiții începe de la un anumit offset în cadrul imaginii. Atunci când dorim să creăm o imagine nouă pentru RaspberryPi, trebuie să ne asigurăm că respectăm aceste partiții și formatele lor. Pașii pe care trebuie să îi urmăm sunt:

  • Stabilirea dimensiunii imaginii și inițializarea acesteia cu zero-uri
  • Crearea tabelei de partiții și a celor două partiții necesare
  • Formatarea partițiilor cu formatul corespunzător
  • Pentru popularea rootfs-ului, putem fie să montam partiția și să copiem manual directoarele și fișierele, fie putem să copiem o partiție întreagă de pe o altă imagine

Pentru fiecare dintre acești pași există utilitare ce ne ajută să realizăm operațiile necesare.

dd

Pentru copierea, și eventual convertirea, unui fișier, la nivel de byte, se poate folosit utilitarul dd.. Folosind dd, putem de asemenea genera fișiere de anumite dimensiuni și le putem stabili conținutul. În cazul nostru, dd este util pentru a inițializa o imagine și pentru a copia în acea imagine conținutul care ne interesează. Dintre parametrii lui dd, cei mai des utilizați sunt:

  • if - fișierul de intrare; dacă nu se specifică acest parametru, se va citi de la standard input
  • of - fișierul de ieșire; ca și la if, dacă nu este specificat, se scrie la standard output
  • count - numărul de blocuri de input ce vor fi copiate
  • bs - numărul de bytes dintr-un bloc

Un exemplu de utilizare a lui dd pentru a inițializa cu zerouri un fișier de o dimensiune exactă:

$ dd if=/dev/zero of=<file> bs=1M count=2048

Observați valorile lui count si bs. Se vor copia în total 2048 de blocuri de câte 1MB fiecare, rezultând o dimensiune de 2GB. Fișierul de intrare /dev/zero este un fișier special din care se pot citi oricâte caractere ASCII NUL (0x00).

parted

Pentru manipularea tabelei de partiții (crearea sau ștergerea partițiilor) se poate folosi utilitarul parted. Acesta recunoaște și poate manipula multiple formate de partiții: DOS / SUN / GNU / GPT etc. Utilitarul primește ca parametru device-ul a cărui tabelă de partiții dorim să o modificăm. Un exemplu de apel este:

$ parted /dev/<device>

Rezultatul acestei comenzi este un prompt nou (similar cu fdisk) în care putem folosi tasta m pentru a afișa un meniu cu opțiunile disponibile. Opțiuni utile pentru crearea și ștergerea de partiții sunt:

  • print (sau simplu, p) - afișează tabela curentă;
  • mklabel <type> - șterge tot și crează o tabelă nouă de partiții (type poate fi msdos, gpt etc.);
  • mkpart [type] [fstype] <START> <END> - crează o partiție nouă (type poate fi primary / logical / extended); singurii parametrii obligatorii sunt cele 2 offseturi;
  • set <partition> <flag> <state> - setează un flag unei partiții;
  • rm <NUMBER> - șterge o partiție;
  • quit - iese din program (toate comenzile salvează automat).

Trebuie reținut că, în mod normal parted operează cu sectoare și nu cu octeți. Așadar, este de preferat să punem unitatea de măsură (K, M, G etc.) ca sufix la orice offset cerut de comenzi (e.g., 120MB).

  • Dimensiunea standard a unui sector de disc este 512 bytes. Aceasta poate fi schimbată folosind opțiunea -b.
  • Există două tipuri de partiții: primary și extended. Un device ce folosește sistemul de partiții DOS nu poate avea mai mult de 4 partiții primare (la GPT s-au eliminat aceste restricții).
  • Exemplu de creare a celor 2 partiții necesare pentru o imagine bootabilă Raspberry PI:
    (parted) mklabel msdos
    (parted) mkpart primary fat32 1MiB 120MiB
    (parted) mkpart primary ext4 120MiB 100%
    (parted) print
    (parted) quit
  • Observați, în exemplu anterior, la ultima partiție s-a folosit o unitate specială, procentul, ceea ce înseamnă că ultimei partiții i s-a repartizat tot spațiul rămas.

losetup

Dacă se lucrează cu imagini din fișiere, este util să le putem monta pentru a vedea ce conțin. Însă acest lucru nu se poate realiza pe Linux fără a avea un dispozitiv de tip bloc. Din fericire, utilitarul losetup ne poate salva în aceste situații, el permițând conectarea unui fișier imagine la un dispozitiv bloc virtual de forma /dev/loop<N>, unde N este un număr din intervalul [0-9].

Atenție: losetup nu poate fi utilizat decât cu privilegii de root (e.g., folosiți sudo la toate comenzile de manipulare a dispozitivelor de sistem)!

Exemplu folosire:

# conectăm fișierul la un dispozitiv loop
losetup /dev/loop0 ./path/to/your-image.bin
# scanăm dispozitivul bloc de partiții:
partprobe /dev/loop0
ls -l /dev/loop0*
# ^ ar trebui să vedeți cele 2 partiții ale imaginii RPI: loop0p1 și loop0p2

Important / NU uitați: la final, trebuie să deconectăm dispozitivul pentru a sincroniza cele scrise înapoi în fișier: losetup -d /dev/loop<N>!

mkfs

Crearea unei partiții nu este suficient pentru a putea stoca fișiere. Avem nevoie ca partiția să conțină și un sistem de fișiere. Utilitarul folosit pentru a crea (formata) sisteme de fișiere Linux este mkfs. Parametrii pe care îi primește mkfs sunt device-ul ce trebuie formatat și tipul sistemului de fișiere dorit, specificat cu parametrul -t.

Spre exemplu, comanda

$ mkfs -t ext4 -L MyRootFS /dev/fd0

Va crea pe device-ul fd0 un sistem de fișiere ext4 cu label-ul (denumire logică) MyRootFS. În Linux, utilitarul mkfs este împărțit în câte un executabil pentru fiecare tip de sistem de fișiere suportat. Pentru a le vedea pe cele disponibile, deschideți un terminal, scrieți mkfs. (punct la coadă!) și apasați tab.

Dacă etichetați sistemele de fișiere, le puteți accesa ulterior după căi speciale din /dev/disk/by-label/!

mount

În sistemele Unix, sistemul de fișiere este unul arborescent unde directoarele pot fi considerate noduri, iar fișierele frunze. Utilitarul mount ne permite să atașăm unui arbore existent un alt subarbore (sistem de fișiere). Apelată fără niciun parametru, această comandă va afișa toate device-urile montate, locația în care sunt montate și opțiunile cu care au fost montate.

Formatul general al comenzii este mount -t <type> <device> <mount path>, unde <type> este același ca la mkfs. De cele mai multe ori, argumentul -t <type> poate fi omis, acesta fiind autodetectat de către kernel.

Și, desigur, pentru de-montarea de la final a dispozitivelor, putem folosi umount cu argumentul ori dispozitivul sursă, ori mountpointul (oricare ne este mai la îndemână – va face același lucru).

Exerciții

În laborator, vom folosi echipamente Raspberry PI 4! conectate prin USB Type-C și un adaptor UART la USB pentru vizualizarea consolei dispozitivului (din păcate, nu dispunem de suficiente monitoare HDMI în laborator + cabluri adaptoare).

Înainte de a începe exercițiile, asigurați-vă că aveți cel puțin 10GB de storage disponibili în mașină virtuala de laborator.

0. Descărcați de aici arhiva rootfs referință pentru laborator (v2) + imaginea partiției de boot rpi-boot.img (pe care o vom clona mai târziu și îi vom adăuga partiție nouă pentru Linux).

  • Pentru cei cu Windows, descărcați și instalați utilitarul Raspberry Pi Imager, pe care îl vom folosi să scriem imaginea pe dispozitivul fizic.

1. Dorim să creăm propria imagine pe care, ulterior, s-o urcăm pe Raspberry PI și să bootăm Linux-ul (prin U-Boot și modul ums – USB Mass Storage Device):

  • Imaginea de rpi-boot.img a RaspberryPi are dimensiunea de ~150MB. Folosiți dd (citiți și documentația din laborator mai sus) și creați o imagine nouă, plină de zerouri, de 2GB (să zicem, rpi-full.img).
  • Dorim să copiem de-a întregul prima partiție din imaginea rpi-boot.img în cea de 2GB proaspăt creată. Putem folosi dd pentru asta:
    dd if=rpi-boot.img of=rpi-full.img conv=notrunc
  • Folosind parted, inspectați dacă există partiția de boot în noua imagine (cea full). Apoi creați pe cea de-a doua partiție (ce va conține un sistem de tip ext4). Vedeți mai sus în laborator pentru exemple de folosire a utilitarului;
  • Formatați sistemul de fișiere a celei de-a doua partiții în ext4. Pentru aceasta, va trebui, mai întâi, să conectați imaginea într-un dispozitiv de tip bloc prin losetup, apoi mkfs pentru a o formata (urmați documentația din laborator a acestor utilitare);
  • Montați noua partiție ext4 (puteți folosi /mnt ca mountpoint) și copiați conținutul arhivei rootfs descărcate mai sus; folosiți argumentul -C al tar pentru a preciza directorul destinație, adică mountpointul partiției). Atenție: folosiți contul de root, deoarece dorim să dezarhivăm fișierele și să păstrăm permisiunile originale (imaginea este un root filesystem de Linux pre-instalat!):
    tar xf <ARCHIVE_FILE> -C <DEST_MOUNTPOINT>
    # inspectați calea dezarhivată:
    ls -l <DEST_MOUNTPOINT>
    # ar trebui să vedeți toate directoarele unui rootfs clasic: /bin, /usr, /dev, /opt etc.
  • Încă nu am terminat. Montați partiția de boot (prim partiție din loop device) într-o cale (puteți crea directorul /mnt/boot/rpi și să-l folosiți); apoi copiați fișierele /boot/vmlinuz-<versiune> /mnt/boot/initrd.img-<versiune> pe prima partiție (în mountpointul acesteia, ofc); acestea sunt necesare deoarece bootloaderul RPI nu știe să citească partiții ext4 (ci doar FAT32).
  • Tot pe partiția de boot, creați fișierul cmdline.txt și scrieți argumentele kernelului Linux:
    earlycon=pl011,mmio32,0xfe201000 console=tty1 console=serial0,115200 root=/dev/mmcblk0p2 rw rootwait fsck.repair=yes
  • Acum (atenție!): demontați partițiile (mai întâi pe cea de boot, montată ultima oară, apoi pe cea ext4) și deconectați dispozitivul loop!

2. Acum că imaginea noastră este gata, vrem să vedem că funcționează. Pentru aceasta, va trebui să pornim u-boot în modul ums (vedeți laboratorul anterior) și folosim un utilitar de Raw Disk Imager (precum cel descărcat mai sus) sau dd (pentru cei cu Linux nativ sau cărora le funcționează USB Device Passthrough în mașina virtuală) pentru a scrie imaginea obținută anterior.

  • Atenție: Va trebui, mai întâi, să aduceți imaginea pe Windows (folosiți scp cu username@<ip_sursa>:/cale/catre/fisier.img ca prim argument, iar, ca destinație, o cale de pe PC-ul fizic, e.g., C:\Users\..etc)!
  • Notă pentru VirtualBox: va trebui să folosiți argumentul -P 2023 și student@localhost:<cale> ca sursă.
  • Notă pentru cei cu gazda pe Linux: dacă utilizați dd, va trebui să rulați comanda sync și să așteptați să se termine! Asta e important deoarece kernelul modern cache-uiește în memoria RAM și scrierea se termină, aproape instant (însă rămâne să se copieze pe SD în background, această operațiune fiind mult mai lentă).

3. Reporniți Raspberry PI-ul. Vom încerca să bootăm kernelul și rootfs-ul din prompt-ul u-boot:

  • Deoarece kernelul este compresat, operațiunea este un pic mai complicată, trebuind să-i spunem o zonă de memorie folosibilă pentru extracția imaginii:
    # load bootargs from device tree (contains BL2-modified cmdline.txt data)
    fdt addr ${fdt_addr} && fdt get value bootargs /chosen bootargs
    # set decompression zone in RAM at 400MB, 64MB in size
    setenv kernel_comp_addr_r 0x19000000
    setenv kernel_comp_size 0x04000000
    # load kernel from file
    fatload mmc 0:1 ${kernel_addr_r} vmlinuz-6.1.61-rpi+
    # boot without initrd, for now
    booti ${kernel_addr_r} - ${fdt_addr}
  • Dacă vă printează mesajul cu waiting for device /dev/mmcblk0p2 și nu se termină procesul, verificați dacă ați rulat kernel-ul 6.1 (este în comanda de mai sus) și dacă partiția a doua există (part list mmc 0).

4. Dorim să instalăm pachete în imagine. Deoarece pe Raspberry PI nu avem Internet, vom face asta pe mașina virtuală, lucrând direct cu imaginea și utilitarele systemd-nspawn (pornește facil un chroot container) și qemu-user-static (ce ne va permite emularea unei arhitecturi străine direct dintr-un container)!

  • Folosind losetup, partprobe și mount, montați partiția a doua (e.g., în /mnt).
  • Copiați utilitarul qemu-<arch>-static pentru arhitectura emulată (AArch64) în rootfs (în /mnt/usr/bin):
    # vedem unde e executabilul:
    which qemu-aarch64-static
    # /usr/bin/qemu-aarch64-static
    cp -f /usr/bin/qemu-aarch64-static /mnt/usr/bin
    chmod +x /mnt/usr/bin/qemu-aarch64-static
  • Rulăm containerul:
    systemd-nspawn --as-pid2 --resolv-conf=copy-host -D "/mnt" bash
  • Ar trebui să fiți în containerul de Debian pe arhitectură străină (AArch64), emulat prin qemu-user-static cu ajutorul funcționalității din kernel binfmt_misc. Rulați apt update și instalați pachetele wpasupplicant (pentru autentificare prin WiFi) și ce alte utilitare mai doriți.
  • La final, nu uitați să de-montați + deconectați dispozitivele loop utilizare!
  • Bonus: testați noua imagine pe dispozitivul RPI4 (va trebui să repetați pașii copiere + burn folosind Disk Imager)!

Resurse

si/laboratoare/06.txt · Last modified: 2023/11/15 12:03 by florin.stancu
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0