This shows you the differences between two versions of the page.
si:laboratoare:05 [2022/11/07 08:46] dan_eugen.brezeanu [Resurse] |
si:laboratoare:05 [2024/11/07 11:16] (current) cosmin.chenaru Adaugat --import la virt-install |
||
---|---|---|---|
Line 1: | Line 1: | ||
- | ====== Laboratorul 05. Rootfs & Servicii Web ====== | + | ====== Laboratorul 05. QEMU & Tools ====== |
- | ===== Root file system ===== | + | ===== Introducere ===== |
- | 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: | + | Până acum am interacționat cu sisteme embedded ce au avut la baza un sistem de operare Real Time numit NuttX, ce a fost configurat si compilat folosind Kconfig-uri si CMake. |
- | * ''/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 | + | În continuare, laboratorul își propune să vă familiarizeze cu sisteme embedded care rulează Linux, începând de la dezvoltare și configurare, până la mentenanță. Vom trata subiecte precum: |
- | * ''/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. | + | |
- | Standardul Unix recomandă ca //rootfs//-ul să aibă 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. | + | * Emularea sistemelor; |
+ | * Rularea/Compilarea de aplicații pe un sistem embedded; | ||
+ | * Bootloadere, Kernel, rootfs & initramfs; | ||
+ | * Construirea unei distribuții Linux optimizată pentru sisteme embedded; | ||
+ | * Instalarea și configurarea de servicii. | ||
- | Î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 inițializare, fie este stocat pe un server accesibil prin rețea și montat de către kernel la inițializare. | + | ==== De ce Linux? ==== |
- | ==== Formatul imaginii RaspberryPi ==== | + | Sistemele Linux oferă o mulțime de avantaje dezvoltatorilor de produse, care micșorează timpul de dezvoltare, lucru care este din ce în ce mai important în zilele noastre: |
- | 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: | + | * **versatilitate**: Sistemele Linux nu trebuie să fie single-purpose, se pot adăuga multiple funcționalități cu ușurință (chiar și în etapa de post-producție) |
+ | * **codebase mare**: Sistemele Linux abundă de aplicații user-space, drivere pentru o mulțime de dispozitive, suport pentru multe protocoale/sisteme de fișiere/etc. | ||
+ | * **securitate**: Sistemele care folosesc servicii comune în Linux beneficiază de același nivel de securitate ca pe un sistem desktop sau server | ||
- | * Stabilirea dimensiunii imaginii și inițializarea acesteia cu zero-uri | + | De-a lungul anilor Linux a devenit cel mai folosit sistem de operare pentru aplicațiile embedded. Îl puteți găsi folosit în orice: |
- | * Crearea tabelei de partiții și a celor două partiții necesare | + | * telefoane mobile (Android) |
- | * Formatarea partițiilor cu formatul corespunzător | + | * router-e |
- | * 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 | + | * DVR, NAS |
+ | * quadcoptere | ||
+ | * [[http://store.steampowered.com/livingroom/SteamMachines/ | console de jocuri]] | ||
+ | * [[http://www.geek.com/chips/this-intelligent-fridge-runs-linux-on-an-arm-chip-1297126/ | frigidere ]] | ||
- | Pentru fiecare dintre acești pași există utilitare ce ne ajută să realizăm operațiile necesare. | + | Sistemele embedded diferă foarte mult în dimensuni și putere de procesare, unele dintre ele apropiindu-se chiar de puterea de procesare a unui calculator obișnuit. De asemenea, aplicațiile pe care acestea le rulează pot fi foarte variate (ex: smartphone), amestecând diferențele dintre un calculator obișnuit și un sistem embedded. Un lucru care deosebește însă sistemele embedded este modul de interacțiune cu utilizatorii, care foarte rar se face printr-un ecran și o tastatură. Lipsa unui mod tradițional de interacțiune cu utilizatorul este și ceea ce face dezvoltarea unui sistem embedded mai grea, dar și mai interesantă. |
- | ==== Tools ==== | + | Cele mai întâlnite două metode de interacțiune cu un sistem embedded în timpul dezvoltării sunt: consola serială și conexiunea SSH. Dintre acestea, conexiunea SSH este metoda mai robustă și mai simplu de utilizat, însă ea e disponibilă doar pe sistemele care dispun de o interfață de rețea. Consola serială, însă este de obicei prezentă pe orice sistem și permite interacțiunea cu sistemul chiar și înainte ca interfața de rețea să fie disponibilă (ex: în bootloader sau înainte de inițializarea driver-ului de rețea). |
- | === dd === | + | |
- | <spoiler> | + | ==== RaspberryPi ==== |
- | 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// | + | | {{:si:lab:2015:intro:raspberry-pi-3-ports.jpg?400|}} | |
- | * ''of'' - fișierul de ieșire; ca și la ''if'', dacă nu este specificat, se scrie la //standard output// | + | ^ RaspberryPi Model B ^ |
- | * ''count'' - numărul de blocuri de input ce vor fi copiate | + | |
- | * ''bs'' - numărul de bytes dintr-un bloc | + | |
- | <note tip> | + | Vom lucra în principal cu RaspberryPi 3, un sistem de calcul bazat pe un procesor "System on Chip" ARM de la Broadcom. Specificațiile complete sunt: |
- | Un exemplu de utilizare a lui ''dd'' pentru a inițializa cu zerouri un fișier de o dimensiune exactă: | + | * procesor: 64-bit quad-core ARM Cortex-A53, 1.2GHz |
+ | * 2GB RAM | ||
+ | * 4 porturi USB 2.0 | ||
+ | * 1 conector Ethernet | ||
+ | * card microSD | ||
+ | * HDMI, jack audio, RCA | ||
+ | * Diverse alte periferice: GPIO, UART-uri, I²C, SPI, I²S | ||
- | <code shell> | + | | {{:si:lab:2015:intro:rasp-hardware.png?400|}} | |
- | $ dd if=/dev/zero of=<file> bs=1k count=4096 | + | ^ Schema perifericelor RaspberryPi ^ |
- | </code> | + | |
- | Observați valorile lui ''count'' si ''bs''. Se vor copia în total 4096 de blocuri de câte 1KB fiecare, rezultând o dimensiune de 4096KB. Fișierul de intrare ''/dev/zero'' este un fișier special din care se pot citi oricâte caractere ASCII NUL (0x00). | + | === Schema bloc === |
- | </note> | + | |
+ | Din punct de vedere hardware, RaspberryPi este un dispozitiv simplu, care expune diferitele periferice pe care le oferă SoC-ul Broadcom. Singura excepție o reprezintă Hub-ul USB, care dublează numărul de porturi USB disponibile și atașează și un dispozitiv Ethernet la SoC-ul Broadcom. | ||
+ | |||
+ | <spoiler Diagrame bloc> | ||
+ | | {{:si:lab:2015:intro:raspberrypi_blockdiagram.png?direct&600 | }} | | ||
+ | ^ Diagrama bloc ^ | ||
+ | |||
+ | | {{:si:lab:2015:intro:9512_blockdiagram.png?direct&600 | }} | | ||
+ | ^ Diagrama block a chip-ului de USB și Ethernet ^ | ||
</spoiler> | </spoiler> | ||
- | === fdisk === | + | ===== Unelte de dezvoltare ===== |
- | <spoiler> | + | Există două concepte importante folosite în dezvoltarea unui sistem embedded: **target** și **host**. //Target//-ul este reprezentat de sistemul embedded pe care îl dezvoltăm și la care ne conectăm (ex: RaspberryPi, Intel Galileo etc.). //Host//-ul este reprezentat de calculatorul pe care îl folosim pentru dezvoltare și prin care ne conectăm cu sistemul embedded. Pentru a elimina inconvenientele compilării pe sistemul embedded (**target**-ul) compilarea se face de obicei pe un sistem desktop (**host**-ul). Bineînțeles, acum pot apărea probleme dacă //target//-ul și //host//-ul folosesc procesoare cu arhitecturi diferite (executabilul generat de //host// nu va fi înțeles de procesorul //target//-ului). Aceste probleme apar deoarece compilarea va folosi în mod implicit compilatorul //host//-ului: //host-compiler//-ul (ex: gcc). |
- | Pentru manipularea tabelei de partiții (crearea sau ștergerea partițiilor) se poate folosi utilitarul //fdisk//. Doar tabele de partiții DOS și disklabel-uri SUN și GNU sunt recunoscute și pot fi manipulate cu //fdisk//. Utilitarul primește ca parametru device-ul a cărui tabelă de partiții dorim să o modificăm. Un exemplu de apel este: | + | |
- | <code shell> | + | Rezolvarea constă în instalarea pe //host// a unui compilator care poate genera executabile înțelese de //target//. Acest compilator poartă denumirea de **cross-compiler** sau **toolchain**, el rulând pe arhitectura //host//-ului, dar generând cod pentru arhitectura //target//-ului. Procesul prin care un program este compilat pe un alt sistem diferit de sistemul //target// se numește **cross-compiling**. |
- | $ fdisk <device> | + | |
- | </code> | + | |
- | Rezultatul acestei comenzi este un prompt nou î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: | + | ==== Cross-compiler toolchain ==== |
- | + | ||
- | * ''p'' - afișează tabela curentă | + | |
- | * ''d'' - șterge o partiție | + | |
- | * ''n'' - creează o partiție nouă | + | |
- | * ''x'' - expert mode | + | |
- | * ''w'' - salvează tabela și iese. | + | |
- | Trebuie reținut că în mod normal //fdisk// operează cu sectoare și nu cu octeți. Atunci când adăugam o partiție, dacă specificăm doar dimensiunea ei și omitem unitatea de măsură (K, M, G etc.), fdisk va considera numărul specificat în sectoare. | + | Există patru componente de bază într-un lanț de instrumente de compilare încrucișată Linux. În plus față de acestea, sunt necesare câteva dependențe pentru a construi gcc în sine: |
- | <note> | + | - **gcc** (contine compilatorul în sine, cc1 pentru C, cc1plus pentru C++ ce generează numai cod de asamblare în format text, apoi gcc, g++, care apeleaza compilatorul în sine, dar și asamblatorul și linkerul binutils, biblioteci precum libgcc (gcc runtime), libstdc++ (the C++ library), libgfortran etc și fișiere antet pentru biblioteca standard C++); |
- | * Dimensiunea standard a unui sector de disc este ''512 bytes''. Aceasta poate fi schimbată folosind opțiunea ''-b''. | + | - **binutils** (contine ld, as, addr2line, ar, c++filt, gold, gprof, nm, objcopy, objdump, ranlib, readelf, size, strings, strip); |
- | * Există două tipuri de partiții: ''primary'' și ''extended''. Un device nu poate avea mai mult de 4 partiții primare. | + | - **kernel headers**: antetele kernelului de Linux (definiții ale apelurilor de sistem, diferitelor tipuri de structuri și alte definiții similare); |
+ | - **biblioteca standard C** (e.g., glibc, newlib, uclibs, musl etc.), ce oferă implementarea funcțiilor standard POSIX, plus câteva alte standarde și extensii). | ||
+ | |||
+ | <note>Versiunea kernelului de Linux folosită pentru anteturile kernelului trebuie să fie aceeași versiune sau mai vechi decât versiunea kernelului care rulează pe sistemul țintă. În caz contrar, biblioteca standard C ar putea folosi apeluri de sistem care nu sunt furnizate de kernel.</note> | ||
+ | |||
+ | Diferențierea între //host-compiler// și //cross-compiler// se face prin prefixarea acestuia din urmă cu un string, denumit **prefix** de forma ''<arch>-<furnizor>-<os>-<libc/abi>'' (ex: ''aarch64-linux-gnu-''), ce conține următoarele variabile (trăsături ale //target//-ului): | ||
+ | |||
+ | * ''<arch>'', arhitectura CPU: arm, mips, powerpc, i386, i686 etc. | ||
+ | * ''<furnizor>'', (în mare parte) șir de formă liberă, ignorat de autoconf | ||
+ | * ''<os>'', sistemul de operare. Fie ''none'' , fie ''linux'' în scopul acestei discuții. | ||
+ | * ''<libc/abi>'', combinație de detalii despre biblioteca C și ABI-ul în uz | ||
+ | |||
+ | <note important> | ||
+ | Prefixul unui cross compiler se termină întotdeaduna cu ''-''. El va fi concatenat la numele utilitarelor (ex: ''gcc'') pentru a obține numele complet (ex: ''aarch64-linux-gnu-gcc'') | ||
</note> | </note> | ||
- | </spoiler> | ||
- | === mkfs === | + | ==== Make și Bash ==== |
- | <spoiler> | + | După cum v-ați obișnuit, aceste două utilitare sunt de-facto standard în dezvoltarea de programe de sistem. |
- | 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''. | + | Extindeți secțiunea de mai jos pentru mai multe detalii: |
- | <note tip> | + | <spoiler Make and Bash intro> |
- | Comanda | + | ** GNU Make ** |
+ | |||
+ | Un program important pentru dezvoltarea unui sistem embedded, și nu numai, îl reprezintă //make//. Acest utilitar ne permite automatizarea și eficientizarea procesului de compilare prin intermediul fișierelor //Makefile//. Pentru o reamintire a modului de scriere a unui //Makefile// revedeți urmatoarea resursa - [[https://makefiletutorial.com/| makefiletuturial]]. | ||
+ | |||
+ | Pentru ușurarea dezvoltării pe multiple sisteme embedded, fiecare având toolchain-ul lui propriu, vom dori să scriem //Makefile//-uri generice, care pot fi refolosite atunci când prefixul //cross-compiler//-ului se schimbă. Pentru aceasta va trebui să parametrizăm numele utilitarelor apelate în //Makefile//. Putem folosi în acest caz variabile de mediu în cadrul //Makefile//-ului. Acestea pot fi configurate apoi din exterior în funcție de sistemul //target// pentru care compilăm la un moment dat, fără a mai fi necesară editarea //Makefile//-urilor. | ||
+ | |||
+ | Cel mai simplu mod de a face acest lucru este să urmăm convenția deja stabilită pentru variabila de mediu care conține prefixul //cross-compiler//-ului: ''CROSS_COMPILE''. Putem folosi această variabilă de mediu în cadrul //Makefile//-ului nostru utilizând sintaxa de expandare unei variabile, ''$(//<variabila>//)'', și prefixând numele utilitarului cu variabila pentru prefix. | ||
+ | |||
+ | <code makefile Makefile> | ||
+ | hello: hello.c | ||
+ | $(CROSS_COMPILE)gcc hello.c -o hello | ||
+ | </code> | ||
+ | |||
+ | Orice variabilă exportată în shell-ul curent va fi disponibilă și în fișierul //Makefile//. Putem de asemenea pasa variabile utilitarului //make// și sub formă de parametri, astfel: | ||
<code shell> | <code shell> | ||
- | $ mkfs -t ext4 /dev/fd0 | + | $ make CROSS_COMPILE=aarch64-linux-gnu- hello |
</code> | </code> | ||
- | va crea pe device-ul //fd0// un sistem de fișiere //ext4//. În Linux, utilitarul //mkfs// este împărțit în câte un executabil pentru fiecare tip de sistem de fișiere suportat (//mkfs.ext2//, //mkfs.ext4//, //mkfs.fat//, etc.). Pentru a le vedea pe cele disponibile, deschideți un terminal, scrieți ''mkfs'' și apasați tab. | + | ** Bash ** |
- | </note> | + | |
- | </spoiler> | + | |
- | === losetup === | + | O mare parte din dezvoltarea unui sistem embedded se face prin intermediul terminalului. Shell-ul care rulează în terminal permite personalizarea unor aspecte utile pentru dezvoltare precum variabilele de mediu încărcate la fiecare rulare. Aceste personalizări se fac însă în fișiere de configurare specifice fiecărui shell. |
- | <spoiler> | + | Pentru //bash// aceste fișiere reprezintă niste script-uri care sunt rulate automat și se găsesc în ''/etc'' (afectează toți utilizatorii) și în ''$HOME'' (afectează un singur utilizator). Prin intermediul fișierelor din ''$HOME'' fiecare utilizator își poate personaliza shell-urile pentru propriile nevoi. Aceste fișiere sunt: |
- | În sistemele Unix, fișierele de tip //block device// sunt folosite pentru a expune sistemului dispozitivele fizice de stocare a datelor. Aceste fișiere se găsesc în directorul ''/dev/'' în orice sistem Unix (''/dev/sda'', ''/dev/sdb'', etc.). Pentru a putea crea block device-uri din imagini de sistem, vom folosi utilitarul ''losetup''. Acesta creează o asociere între un //loop block device// (''/dev/loop1'') și un fișier care conține o imagine de sistem (''raspberrypi.img''). Astfel vom putea lucra cu imaginea aleasa ca și cum aceasta ar exista pe un suport fizic. | + | |
- | <note tip> Principalele moduri de utilizare a utilitarului ''losetup'' sunt următoarele: | + | * ''.bash_profile'' - este executat când se pornește un shell de login (ex: primul shell după logare); |
- | * Crearea unui loop device dintr-un fișier de tip imagine | + | * ''.bashrc'' - este executat cand se pornește orice shell interactiv (ex: orice terminal deschis); |
- | * <code shell>$ losetup -Pf <image file> </code> | + | * ''.bash_logout'' - este executat când shell-ul de login se închide. |
- | * Ștergerea unui loop device | + | |
- | * <code shell>$ losetup -d <loop device> </code> | + | Un alt fișier util folosit de //bash// este ''.bash_history'', care memorează un istoric al comenzilor interactive rulate. Istoricul comenzilor este salvat în acest fișier la închiderea unui shell. Pentru o reamintire a unor comenzi utile în linia de comandă puteți revizita laboratorul de USO - [[https://ocw.cs.pub.ro/courses/uso/laboratoare/laborator-08/| Automatizare în linia de comandă]]. |
- | * Listarea tuturor asocierilor între loop devices și fisere | + | |
- | * <code shell>$ losetup </code> | + | <note tip> |
+ | În dezvoltarea unui sistem embedded este deseori utilă adăugarea în variabila ''$PATH'' a căilor către diferitele tool-uri folosite, pentru ca acestea să poată fi accesate direct prin numele executabilului. Modificarea variabilei ''$PATH'' pentru fiecare shell pornit se poate face ușor prin intermediul fișierelor de personalizare a shell-ului. | ||
</note> | </note> | ||
+ | |||
</spoiler> | </spoiler> | ||
- | === lsblk === | + | ==== QEMU ==== |
- | <spoiler> | + | [[http://qemu.org| QEMU]] este un emulator / hipervizor, care permite rularea unui sistem de operare complet ca un simplu program în cadrul unui alt sistem. A fost dezvoltat inițial de [[https://en.wikipedia.org/wiki/Fabrice_Bellard| Fabrice Bellard]] și este disponibil gratuit, sub o licență open source. QEMU poate rula atât pe Linux, cât și pe Windows [[#referinte| [1]]][[#referinte| [3]]][[#referinte| [4]]]. |
- | Utilitarul ''lsblk'' este folosit pentru a afișa toate fișierele de tip //block device// din sistem, unde sunt acestea montate, precum și alte informații precum dimensiunea și tipul acestora. | + | |
- | <note tip> Utilitarul ''lsblk'' apelat fară niciun parametru ne va oferi detalii despre toate device-urile, iar apelat cu un parametru reprezentând calea către device, ne vă afișa detalii doar despre acesta: | + | Este un hypervizor, deoarece poate virtualiza componentele fizice ale unui sistem de calcul, pentru a permite rularea unui sistem de operare, numit oaspete (//guest//), în cadrul altui sistem de operare, numit gazdă (//host//). În acest mod de funcționare, atât sistemul //guest//, cât și sistemul //host//, folosesc aceeași arhitectură (ex: x86). QEMU poate folosi un modul de nucleu, KVM, pentru a accelera rularea //guest//-ului, atunci când există suport pentru virtualizare în hardware. În acest caz QEMU poate atinge o performanță comparabilă cu sistemul nativ, deoarece lasă mare parte din cod să se execute direct pe procesorul //host//. Folosind KVM sunt suportate diferite arhitecturi, printre care x86, PowerPC și S390 [[#referinte| [1]]]. |
- | <code shell> | + | |
- | $ lsblk /dev/sda | + | Este un emulator deoarece poate rula sisteme de operare și programe compilate pentru o platformă (ex: o placă ARM) pe o altă platformă (ex: un PC x86). Acest lucru este făcut prin translatarea dinamică a intrucțiunilor architecturii //guest// în instrucțiuni pentru arhitectura //host//. Ca un emulator, QEMU poate rula în două moduri [[#referinte| [2]]][[#referinte| [4]]]: |
- | NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT | + | * //[[http://wiki.qemu.org/download/qemu-doc.html#QEMU-User-space-emulator| User-mode emulation]]//, în care un executabil obișnuit (user-space), compilat pentru o arhitectură, este rulat pe o altă arhitectură. În acest mod de funcționare instrucțiunile din executabil sunt translatate în instrucțiuni ale arhitecturii //host//, iar argumentele apelurilor de sistem sunt convertite pentru a putea fi pasate sistemului de operare //host//. Sistemele de operare emulate sunt: Linux, Mac OS X și BSD. Principalele utilizări sunt cross-debugging-ul și cross-compilarea, unde rulăm un compilator nativ al arhitecturii //target//, pe arhitectura //host//. |
- | sda 8:0 0 51,3G 0 disk | + | * //[[http://wiki.qemu.org/download/qemu-doc.html#QEMU-System-emulator-for-non-PC-targets| System emulation]]//, în care este emulat un sistem de calcul complet. QEMU permite emularea unui număr mare de platforme, bazate pe diferite arhitecturi (ex: x86, ARM, PowerPC, MIPS, SPARC, MicroBlaze etc.), împreună cu perifericele lor. În acest mod de funcționare pot fi rulate sisteme de operare întregi, printre care Windows, Linux, Solaris, BSD și DOS. |
- | ├─sda1 8:1 0 512M 0 part /boot/efi | + | |
- | ├─sda2 8:2 0 1K 0 part | + | În dezvoltarea sistemelor embedded, QEMU este folosit deoarece poate emula un sistem de calcul complet, nefiind necesar ca sistemul țintă (//target//) pentru care se face dezvoltarea, și sistemul //host//, pe care se face dezvoltarea, să folosească aceeași arhitectură. Acest lucru permite ca dezvoltarea software-ului pentru un sistem embedded să poată fi făcută în paralel cu proiectarea hardware-ului, lucru crucial pentru obținerea unui timp de dezvoltare scurt. Un alt avantaj pe care il poate avea emularea, mai ales a sistemelor low-end, este o viteză superioară a emulării pe un sistem //host// performant, în comparație cu sistemul //target//. |
- | └─sda5 8:5 0 50,8G 0 part / | + | |
+ | <spoiler Instalare> | ||
+ | |||
+ | Cel mai simplu mod de instalare pe o distribuție Linux este de a folosi //package manager//-ul. În majoritatea distribuțiilor pachetul principal se numește ''qemu'' și cuprinde de obicei toate executabilele aferente diferitelor moduri de funcționare ale QEMU. Dacă se dorește doar modul de virtualizare cu KVM poate fi instalat pachetul ''qemu-kvm'', iar dacă se dorește modul de emulare a unui sistem ARM poate fi instalat pachetul ''qemu-system-arm''. | ||
+ | <note> | ||
+ | <code shell Ubuntu 22.04> | ||
+ | sudo apt update | ||
+ | sudo apt install qemu qemu-kvm qemu-system-arm qemu-utils | ||
</code> | </code> | ||
+ | </note> | ||
+ | <note info> | ||
+ | Pe VM-ul de laborator aveți gata instalat qemu! | ||
</note> | </note> | ||
+ | |||
</spoiler> | </spoiler> | ||
- | === mount === | + | === Rulare === |
- | <spoiler> | + | Pentru rularea unei mașini virtuale cu KVM se folosește comanda ''qemu-kvm'' împreună cu imaginea pentru hard disk. În acest caz imaginea hard disk-ului trebuie să conțină un sistem compatibil cu arhitectura //host//, accelerarea oferită de KVM putând fi folosită doar dacă //guest//-ul și //host//-ul folosesc arhitecturi compatibile (ex: x86_64). |
- | Î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). | + | |
- | <note tip>Formatul general al comenzii este ''mount -t //<type>// //<device>// //<mount path>//'', unde //<type>// este același ca la //mkfs//.</note> | + | Pentru rularea în //user-mode emulation// poate fi folosit unul din executabilele de forma ''qemu-<arch>'' împreună cu executabilul pe care vrem să-l rulăm [[#referinte| [5]]]. Bineînțeles, acest executabil trebuie să fie compatibil cu arhitectura aleasă, ''<arch>'', iar momentan QEMU oferă suport pentru //user-mode emulation// doar pe Linux și BSD. Dintre cele două, suportul pentru BSD nu este însă la fel de complet ca cel pentru Linux [[#referinte| [6]]] |
- | </spoiler> | + | |
+ | Exemplu de rulare in **user-mode emulation**: | ||
+ | <code> | ||
+ | qemu-arm -cpu <procesor> <executabil> | ||
+ | </code> | ||
- | ===== Network & Services ===== | + | Pentru rularea în modul //system emulation// se folosește unul din executabilele de forma ''qemu-system-<arch>'' împreună cu imaginea pentru hard disk [[#referinte| [7]]]. |
- | De multe ori, avem nevoie ca sistemele embedded sa porneasca pentru diverse servicii. Aceste servere, de obicei, expun un API de care ne putem folosi pentru a trimite comenzi si date sistemului. In acest laborator, vom invata cum sa implementam un server minimal si cum sa sa il facem sa porneasca la boot-time. | + | Exemplu de rulare in modul **system emulation**: |
+ | <code> | ||
+ | qemu-system-arm -machine <arhitectura> -drive file=... [+ multe alte argumente] | ||
+ | </code> | ||
- | ==== REST ==== | + | === Configurare Qemu === |
- | ** REST ** (**RE**presentational **S**tate **T**ransfer) este un set de arhitecturi software folosit pentru a ușura comunicarea intre clientul web și server-ul web. Implementarea server-ului și a clientului pot fi decuplate una de cealaltă, iar clientul poate face cereri (requests) către server pentru a primi sau modifica structurile de date de pe server, iar server-ul va raspunde acestor cereri (responses). | + | În modul mașină virtuală sau //system emulation// QEMU simulează un întreg sistem de calcul. În lipsa unor alte argumente se folosește însă o configurație implicită de sistem, care este specifică fiecărei arhitecuri în parte. QEMU poate însă simula o gamă largă de configurații de sistem. În limbajul QEMU acestea se numesc //mașini// și pot fi selectate cu opțiunea ''-machine''. |
- | REST se bazează pe cele 4 tipuri de comenzi HTTP: | + | <note> |
+ | <code shell Nokia N800 tablet> | ||
+ | qemu-system-arm -machine n800 <disk image> | ||
+ | </code> | ||
+ | </note> | ||
- | * GET - primește o anumita resursă de pe server | + | QEMU oferă însă și un control mai fin asupra configurației sistemului simulat printr-o serie de alte opțiuni, precum [[#referinte| [8]]]: |
- | * POST - actualizează o resursă deja existantă | + | * ''-cpu'' - specifică tipul de procesor care va fi emulat |
- | * PUT - creează o nouă resursă | + | * ''-m'' - specifică dimensiunea memoriei RAM |
- | * DELETE - șterge o resursă deja existantă | + | * ''-hda'', ''-hdb'' etc. - specifică imaginea pentru primul hard disk, respectiv al doilea hard disk, ș.a.m.d |
+ | * ''-fda'', ''-fdb'' - specifică imaginea pentru primul floppy disk, respectiv al doilea floppy disk | ||
+ | * ''-cdrom'' - specifică imaginea folosită de cdrom | ||
+ | * ''-serial'', ''-parallel'' - specifică porturile seriale, respectiv, paralele și modul de interacțiune a acestora cu //host//-ul | ||
+ | Configurații mai avansate pot fi obținute cu opțiunile ''-device'', ''-drive'', ''-net'', ''-soundhw'', ''-bt'' care adaugă dispozitive periferice, de stocare, plăci de rețea și de sunet și, respectiv, dispozitive bluetooth [[#referinte| [8]]]. [[https://qemu.weilnetz.de/doc/4.2/qemu-doc.html| Documentația]] oferă informații despre toate aceste opțiuni, precum și multe altele. | ||
- | Cele 4 comenzi au rol similar celor din baze de date: CREATE, READ, UPDATE și DELETE | + | O altă opțiune utilă este ''-kernel''. Aceasta permite specificarea imaginii de kernel folosite de sistemul //guest// direct în comanda QEMU. Astfel, QEMU va încărca kernelul dintr-un fișier aflat pe sistemul //host// în loc de a-l cauta în imaginea de hard disk. Acest lucru poate reduce semnificativ timpul de iterație în momentul dezvoltării unui sistem embedded, deoarece nu mai este necesară recrearea imaginii de hard disk pentru fiecare modificare a kernel-ului. |
- | ==== REST API ==== | + | <note important> |
+ | Pe unele sisteme emulate este chiar obligatoriu ca opțiunea ''-kernel'' să fie prezentă, deoarece emularea sistemului nu include și un bootloader. Fără un bootloader, sistemul nu știe altfel cum să găsească imaginea de kernel. | ||
+ | </note> | ||
- | Similar cum API-ul (**A**pplication **P**rogramming **I**nterface) din cadrul unui program C/Java/Python asigură inter-operatibilitatea componentelor acelui program, la fel si comunicarea REST dintre client și server se bazeaza pe un API. | + | De obicei, împreună cu specificarea imaginii de kernel este nevoie să specificăm și linia de comandă a kernel-ului. Pentru aceasta se folosește opțiunea ''-append'' împreună cu string-ul care vrem să fie pasat kernel-ului la bootare. |
- | Dacă în C/Java/Python apelăm funcții și pasăm argumente acelor funcții, în cazul REST-ului, în momentul în care clientul face un request către server, el are posibilitatea de a trimite funcția (adresa paginii) și argumentele funcției in următoarele trei feluri: | + | O ultimă opțiune, folositoare mai ales pentru debugging, o reprezintă redirectarea monitorului către consolă. Acest lucru se face cu opțiunea ''-monitor stdio''. Monitorul oferă o interfață în linie de comandă care permite un control interactiv al modului în care se face emularea. |
- | * într-un **query string** - ex. /api/resource?p1=v1&p2=v2 | + | === Networking === |
- | * parte din URL - ex. /api/resource/v1/v2 | + | |
- | * în interiorul cererii de HTTP - ex. prin pasarea de structuri JSON, MIME, XML | + | |
- | În cadrul exercițiilor de astăzi vom folosi a treia modalitate de pasare a argumentelor. | + | Pentru a emula o interfață de rețea, QEMU se bazează pe două componente: //device//-ul prezentat //guest//-ului, configurat cu opțiunea ''-device'' sau ''-net nic'', și //back-end//-ul care leagă acest device de //host//, configurat cu opțiunea ''-netdev''. Opțiunea ''-device'' nu este limitată la a emula doar interfețe de rețea, ea putând configura orice dispozitiv suportat de către QEMU însă, unele plăci de rețea sunt suportate doar de opțiunea ''-net nic''. |
+ | Pentru //back-end//, QEMU suporta mai multe moduri, printre care: | ||
+ | * ''-netdev user'' - //user-mode//, rulează în user-space și nu necesită privilegii, însă interacțiunea cu rețeaua //host//-ului este complicată | ||
+ | * ''-netdev tap'' - //tap//, conectează o interfață TAP a //host//-ului la un VLAN emulat, permițând o configurare detaliată a topologiei folosite de //guest//, însă configurarea este mai complicată | ||
+ | * ''-netdev bridge'' - //bridge//, conectează o interfață TAP a //host//-ului la un bridge, care permite interacțiunea cu rețeaua fizică a //host//-ului | ||
+ | * ''-netdev socket'' - //socket//, interconectează VLAN-urile a două sisteme emulate folosind TCP sau UDP. | ||
- | ==== JSON ==== | + | În mod implicit QEMU emulează un sistem cu o interfață de rețea reprezentată de un //device// specificat de //mașina// selectată, în modul //user-mode//. Aceasta configurare implicită nu ne oferă însă toată flexibilitatea unui //target// real, conectat la o rețea fizică. Din acest motiv în cadrul laboratorului ne vom folosi de modul //bridge//. |
- | JSON (**J**ava**S**cript **O**bject **N**otation) este un format de date similar cu XML-ul, folosit pentru a trimite date independent de limbajul de programare. | + | //Bridge//-ul folosit de către //back-end// se configurează cu parametrul ''br=//<nume bridge>//'', iar //device//-ul pentru opțiunea ''-net nic'' se specifică prin parametrul ''model=//<device>//''. Legatura dintre cele două componente se face prin adăugarea parametrului ''netdev=//<id>//'' la //device// și a parametrului ''id=//<id>//'' la //back-end//. Valoarea ''//<id>//'' trebuie bineînteles să fie identică pentru ca cele două componente să fie legate. În final, cele două opțiuni arată astfel: ''-net nic,model=<device>,netdev=<id> -netdev bridge,br=<nume bridge>,id=<id>''. |
- | Un exemplu de structura JSON este acesta: | + | <spoiler Configurare acces Internet în QEMU folosind modul bridge> |
+ | * Pentru a crea și configura //bridge//-uri se folosește utilitarul **brctl** din pachetul **bridge-utils**. Crearea unui //bridge// care să ofere unui //guest// accesul la rețea fizică a //host//-ului se face astfel: <code> | ||
+ | sudo brctl addbr virbr0 # creăm bridge-ul | ||
+ | sudo brctl addif virbr0 <interfata fizica> # adăugam interfața fizică a host-ului la bridge | ||
+ | sudo ip address flush dev <interfata fizica> # ștergem adresa IP de pe interfața fizică, doar dacă avem o adresă | ||
+ | # IP pe interfață. Va șterge și ruta default automat | ||
+ | sudo dhclient virbr0 # obținem adresa IP pentru bridge și ruta default prin DHCP | ||
+ | </code> | ||
+ | |||
+ | * Dacă nu merge obținerea adreselor prin DHCP, se poate configura manual adresa și ruta default: <code> | ||
+ | ip address show # notăm ip-ul și prefixul interfeței fizice | ||
+ | ip route show # notăm ruta implicită | ||
+ | sudo brctl addbr virbr0 # creăm bridge-ul | ||
+ | sudo brctl addif virbr0 <interfata fizica> # adaugăm interfața fizică a host-ului la bridge | ||
+ | sudo ip address del <ip>/<prefix> dev <interfata fizica> # mutăm adresa interfeței fizice | ||
+ | sudo ip address add <ip>/<prefix> dev virbr0 # pe bridge | ||
+ | sudo ip link set dev virbr0 up | ||
+ | sudo ip route add default via <gateway> # readăugam ruta implicită | ||
+ | </code> | ||
+ | |||
+ | * Pentru ca //bridge//-ul să fie acceptat de QEMU el trebuie configurat și în fișierul ''/etc/qemu/bridge.conf'' sub forma: | ||
+ | <code text bridge.conf> | ||
+ | allow virbr0 | ||
+ | </code> | ||
+ | |||
+ | * Connectati-va la ssh în sistemul emulat de qemu prin intermediul IP-ului VM-ului în bridge. | ||
<code> | <code> | ||
- | { | + | student@virtual-machine:~$ ssh root@192.168.122.<X> |
- | "firstName": "John", | + | |
- | "lastName": "Smith", | + | |
- | "isAlive": true, | + | |
- | "age": 27, | + | |
- | "address": { | + | |
- | "streetAddress": "21 2nd Street", | + | |
- | "city": "New York", | + | |
- | "state": "NY", | + | |
- | "postalCode": "10021-3100" | + | |
- | }, | + | |
- | "phoneNumbers": [ | + | |
- | { | + | |
- | "type": "home", | + | |
- | "number": "212 555-1234" | + | |
- | }, | + | |
- | { | + | |
- | "type": "office", | + | |
- | "number": "646 555-4567" | + | |
- | } | + | |
- | ], | + | |
- | "children": [], | + | |
- | "spouse": null | + | |
- | } | + | |
</code> | </code> | ||
+ | </spoiler> | ||
- | ==== Flask ==== | + | <spoiler Configurare acces Internet în QEMU folosind user networking (*recomandat*!)> |
+ | * Emulați interfața de rețea folosind un USB network adaptor virtual. Pentru a avea access la serviciul de SSH din QEMU o sa avem nevoie de port-forwarding pentru portul 22 (SSH default). Pentru a realiza acest lucru, adaugam parametrul ''hostfwd=tcp::5555-:22'' in optiunea de ''-netdev'' din comanda de ''qemu-system-aarch64'': | ||
- | În Python avem la dispoziție mai multe framework-uri web, printre care cel mai cunoscut este Django. Spre deosebire de Django, care deține mai multe tool-uri de folos pentru programator, Flask este un framework simplu ce permite usor crearea de prototipuri pentru proiecte mici. | + | <code> |
+ | ... | ||
+ | -device usb-net,netdev=net0 \ | ||
+ | -netdev user,id=net0,hostfwd=tcp::5555-:22 \ | ||
+ | </code> | ||
- | Un program minimalist in Flask poate fi acesta: | + | * Logarea pe //target// se face doar cu user-ul **root**. |
+ | * Verificam interfetele disponibile | ||
+ | <code> | ||
+ | root@rpi3-20220807:~# ip a s | ||
+ | 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 | ||
+ | link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 | ||
+ | inet 127.0.0.1/8 scope host lo | ||
+ | valid_lft forever preferred_lft forever | ||
+ | inet6 ::1/128 scope host | ||
+ | valid_lft forever preferred_lft forever | ||
+ | 2: enx405400123457: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UNKNOWN group default qlen 1000 | ||
+ | link/ether 40:54:00:12:34:57 brd ff:ff:ff:ff:ff:ff | ||
+ | inet6 fe80::4254:ff:fe12:3457/64 scope link | ||
+ | valid_lft forever preferred_lft forever | ||
+ | </code> | ||
- | <file python hello_flask.py> | + | * Putem observa ca nu avem nicio adresa IP asociata interfetei. Pentru a realiza acest lucru, vom cere una prin intermediul protocolului DHCP. |
- | import flask | + | <code> |
+ | dhclient <enx...> # <-- numele interfeței ca parametru | ||
+ | </code> | ||
- | app = flask.Flask(__name__) | + | * Connectati-va la ssh în sistemul emulat de qemu prin intermediul port-ului forward-uit local. |
+ | <code> | ||
+ | student@virtual-machine:~$ ssh -p 5555 root@localhost | ||
+ | </code> | ||
+ | </spoiler> | ||
- | @app.route('/', methods=['GET']) | + | ===== Exerciții ===== |
- | def home(): | + | |
- | return "<h1>Hello World</h1>" | + | |
- | app.run() | + | <note important> |
- | </file> | + | Pentru rezolvarea laboratorului recomandăm folosirea mașininii virtuale de laborator cu Ubuntu 22.04 descărcabilă de pe [[https://github.com/cs-pub-ro/SI-Lab-VM/releases/|GitHub + torrent]]. |
+ | </note> | ||
- | În momentul rulării programului de Python, framework-ul va deschide un server web pe portul TCP 5000 (valoarea default), și va aștepta cereri HTTP de la un browser. În funcție de pagina cerută, Flask-ul va **ruta** cererea catre funcția de Python asociata (procedeu cunoscut drept **mapping**). În cazul acestui exemplu, când browser-ul va cere pagina principala, serverul web va returna un simplu header (H1) in format HTML. | + | Ca sistem de operare pentru embedded / RPI (azi, emulat), vom folosi distribuția [[https://raspi.debian.net/|Debian Bookworm]] compilată pentru [[https://raspi.debian.net/tested-images|ARM64]]. |
- | ==== Systemd services ==== | + | ==== 0. Setup ==== |
- | Serviciile sunt procese care sunt executate automat de catre [[ https://systemd.io/ | systemd ]] in baza unor reguli definite pentru acel serviciu. Aceste reguli se regasesc in cadrul unui [[ https://www.freedesktop.org/software/systemd/man/systemd.service.html | unit configuration file ]] si ofera informatii despre fisierul care trebuie executat, precum si cand trebuie executat, dependente al serviciului, etc. | + | <note> |
+ | Dacă folosiți VM-ul de laborator, pașii de mai sus au fost deja efectuați, însă tot mai trebuie să descărcați imaginea de debian (ultimul subtask). | ||
+ | </note> | ||
- | <file python demo.service> | + | * Actualizați lista de pachete ''sudo apt update'' |
- | [Unit] | + | * Instalați ''git'', ''vim'' și ''bridge-utils''. |
- | Description=Demo service | + | <code> |
- | After=network.target | + | sudo apt install git vim wget bridge-utils |
- | StartLimitIntervalSec=0 | + | </code> |
+ | * Instalați toolchain-ul necesar pentru a cross-compila programe pentru RaspberryPi 64 biți: | ||
+ | <code> | ||
+ | sudo apt install crossbuild-essential-arm64 | ||
+ | </code> | ||
+ | * Instalați QEMU folosind instrucțiunile următoare. | ||
+ | <code> | ||
+ | sudo apt install qemu-user # Pentru user-mode emulation | ||
+ | sudo apt install qemu-system-aarch64 # Pentru system-mode emulation | ||
+ | sudo apt install qemu-utils # Pentru utilitare precum qemu-nbd | ||
+ | </code> | ||
- | [Service] | + | * Descărcați și dezarhivați o imagine de Debian Bookworm pentru Raspberry PI Model 3B+ [[https://raspi.debian.net/tested-images/|de aici]]. |
- | Type=simple | + | |
- | Restart=always | + | |
- | RestartSec=1 | + | |
- | ExecStart=/usr/bin/myserver.sh | + | |
- | [Install] | + | ==== 1. User-mode emulation ==== |
- | WantedBy=multi-user.target | + | |
- | </file> | + | |
- | * **Description**: Scurta descriere a seriviciului | + | Compilați următorul program **hello world** pentru RaspberryPi și linkați //static//. Aflați setul de instrucțiuni folosit de executabilul generat și apoi rulați-l în QEMU folosind //user-mode emulation// și emulând procesorul Cortex-A53. Salvați comanda folosită pentru emulare. |
- | * **After**: Dependentile serviciului | + | |
- | * **ExecStart**: Comanda care trebuie executata in cadrul serivicului | + | <code> |
+ | #include <stdio.h> | ||
+ | int main(void) | ||
+ | { | ||
+ | printf("hello world\n"); | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | * Utilizaţi compilatorul **aarch64-linux-gnu-gcc**. | ||
+ | * Folosiți pentru compilator flag-ul ''-static'' pentru a obține un executabil linkat //static//. | ||
+ | * Utilitarul **file** oferă informații despre conținutul fișierelor primite ca argument. | ||
+ | * Ce se întâmplă dacă rulați executabilul direct, fără QEMU? De ce? | ||
+ | |||
+ | <note important> | ||
+ | In mod normal executabilul astfel obtinut nu merge rulat si pe sistemul host. | ||
+ | In cazul in care merge rulata aplicatia de hello world si in host explicatie pentru care se intampla asta este: qemu instaleaza un handler care permite aceasta translatia direct, doar ca acest lucru se intampla selectiv, deoarece nu pe toate sistemele de operare este instalat/configurat similar. | ||
+ | </note> | ||
+ | |||
+ | <note tip> | ||
+ | * Re-citiți despre modalitățile de [[#rulare|rulare]]. | ||
+ | * Citiți introducerea acestui [[https://www.kernel.org/doc/Documentation/admin-guide/binfmt-misc.rst| kernel feature]]. | ||
+ | </note> | ||
+ | ==== 2. System-mode emulation ==== | ||
- | Fisierul care configureaza serviciul trebuie sa se regaseasca in cadrul directorului ''/etc/systemd/system/''. | + | Rulați distribuția Debian folosind QEMU în modul //system emulation//. Veți avea nevoie de următoarele argumente pentru emulare. |
- | Pentru a porni serviciul la start-up, acesta trebuie sa fie ''enabled'': | + | * **Kernel-ul** de Linux, prin argumentul “-kernel <kernel_image_file>”. |
+ | * Imaginea de **InitRD** (Initial RAM Disk), prin argumentul “-initrd <initrd_file>". | ||
+ | * Pentru a funcționa mașina virtuală, este nevoie sa îi pasați emulatorului si un **Device Tree**, prin argumentul “-dtb <device_tree_file>”. | ||
+ | * Modelul mașinii emulate, prin argumentul “-machine”. Consultați documentația Qemu de [[https://www.qemu.org/docs/master/system/arm/raspi.html|aici]] pentru lista componentelor virtualizate (procesor, memory, periferice) și alegeți modelul corespunzător pentru Raspberry Pi 3. | ||
+ | * Pentru imaginea discului (rootfs-ul), fom folosi Debian 12 (Bookworm) pentru [[https://raspi.debian.net/tested-images|RPi 3B+]]. Pasați imaginea discului cu argumentul “-sd <disk_file>”, deoarece vom emula un SD-card | ||
+ | * Folosiți string-ul root=/dev/mmcblk0p2 pentru linia de comandă a kernel-ului, deoarece rootfs-ul este pe a doua partitie a SD card-ului). | ||
<code> | <code> | ||
- | #: systemctl status demo.service | + | qemu-system-aarch64 \ |
- | ○ demo.service - Demo service | + | -machine … \ |
- | Loaded: loaded (/etc/systemd/system/demo.service; disabled> | + | -kernel … \ |
- | Active: inactive (dead) | + | -initrd … \ |
- | | + | -dtb … \ |
- | #: systemctl enable demo.service | + | -sd … \ |
- | ○ demo.service - Demo service | + | -append "console=ttyS1 root=/dev/mmcblk0p2 rw rootwait rootfstype=ext4" \ |
- | Loaded: loaded (/etc/systemd/system/demo.service; enabled> | + | -nographic \ |
- | Active: inactive (dead) | + | -serial null \ |
- | | + | -serial stdio \ |
+ | -monitor none | ||
</code> | </code> | ||
- | Pentru a porni serivicul manual vom folosi ''systemctl start'': | + | Extrageți imaginea de kernel, dtb-ul si initrd-ul din imaginea de disc de Debian downloadată |
<code> | <code> | ||
- | #: systemctl start demo.service | + | sudo losetup --show -fP 20220808_raspi_3_bookworm.img # Notați numărul device-ului /dev/loop returnat de comanda losetup |
- | ● demo.service - Demo service | + | sudo mkdir /mnt/debian |
- | Loaded: loaded (/etc/systemd/system/demo.service; enabled> | + | sudo mount /dev/loop16p1 /mnt/debian # Înlocuiți valoarea 16 cu valoarea numărului vostru |
- | Active: active (running) since Sun 2022-11-06 23:00:40 UTC; 6s ago | + | cp /mnt/debian/vmlinuz-5.18.0-3-arm64 . |
- | Main PID: 697 (python3) | + | cp /mnt/debian/initrd.img-5.18.0-3-arm64 . |
- | Tasks: 3 (limit: 978) | + | cp /mnt/debian/bcm2837-rpi-3-b.dtb . |
- | Memory: 50.0M | + | # facem unmount și deconectăm imaginea din dispozitivul bloc |
- | CPU: 5.833s | + | sudo umount /mnt/debian |
+ | sudo losetup -d /dev/loop16 | ||
</code> | </code> | ||
- | ===== Exerciții ===== | + | Pentru montare, puteți folosi utilitarul qemu-nbd (care, în plus față de losetup, știe să deschidă mai multe formate de mașini virtuale precum vbox și vmdk): |
+ | <code> | ||
+ | sudo modprobe nbd max_part=8 | ||
+ | sudo qemu-nbd -c /dev/nbd0 220121_raspi_3_bullseye.img | ||
+ | sudo mkdir /mnt/debian /mnt/debian/boot | ||
+ | sudo mount /dev/nbd0p2 /mnt/debian | ||
+ | sudo mount /dev/nbd0p1 /mnt/debian/boot | ||
+ | # acum putem explora partiția de boot din imagine | ||
+ | ls -l /mnt/debian/boot | ||
+ | # facem unmount și deconectăm imaginea din dispozitivul bloc | ||
+ | sudo qemu-nbd -d /dev/nbd0 | ||
+ | </code> | ||
- | <note warning> | + | Deoarece discul pasat către Qemu este de tip SD card, Qemu așteaptă ca dimensiunea discului să fie o putere a numărului 2 (ex. 512MB, 1024MB). Din acest motiv trebuie să redimensionăm discul, spre exemplu la 4GB. |
- | Înainte de a începe exercițiile, asigurați-vă că aveți cel puțin 5GB de storage disponibili în mașină virtuala. | + | Qemu dispune, de asemenea, de utilitare pentru manipulat imagini (creare / redimensionare / conversie între formate): |
- | </note> | + | |
- | 1. Dorim să ne creăm propria imagine pentru RaspberryPi-urile noastre. Pentru aceasta, vom folosi imaginea [[https://raspi.debian.net/tested/20220808_raspi_3_bookworm.img.xz | + | <code> |
- | | Debian 13 (Bookworm) ]] pentru Raspberry Pi 3B+ ca și referință pentru imaginea noastră. | + | qemu-img resize 20220808_raspi_3_bookworm.img 4G |
- | + | </code> | |
- | * Aflați dimensiunea în MB a imaginii oficiale (''ls -l %%--%%block-size=M''). | + | |
- | * Creați un fișier de dimensiunea necesară pentru imaginea completă, folosind ''dd''. | + | |
- | * Aflați informațiile tabelei de partiții a imaginii oficiale folosind ''fdisk''. | + | |
- | * Creați tabela de partiții și partițiile necesare pentru imaginea noastră. | + | |
- | * Partițiile imaginii nou create trebuie să aibă același UUID ca și imaginea veche pentru a o folosi cu succes. Pentru a modifica UUID-ul diskului folosiți comanda ''x'' în consola deschisa de ''fdisk'' pentru a intra în //expert mode//. Folosiți comanda ''m'' pentru a vedea toate opțiunile și modificați //disk identifier// încât să corespunda cu cel listat pentru imaginea oficială. | + | |
<note tip> | <note tip> | ||
- | * Puteți folosi informațiile din tabela de partiții pentru a găsi numărul de blocuri ai fiecărei partiții, precum și offsetul lor în cadrul diskului virtual. | + | * Re-citiți despre modalitățile de [[#rulare|rulare]]. |
- | * Asigurați-vă că tipurile partițiilor sunt aceleași precum în imaginea oficială. Pentru a schimba tipul unei partiții folosiți comanda ''t'' în cadrul consolei deschise de ''fdisk'', urmată de codul asociat tipului pe care îl dorim. | + | * Revedeți parametrii de [[#configurare| configurare]] ai QEMU și citiți pagina de manual sau [[https://qemu.weilnetz.de/doc/4.2/qemu-doc.html| documentația]] acestora. |
+ | * Pentru a inchide mașina virtuală, folosiți, din cadrul ei, comanda ''sudo halt'' sau ''sudo poweroff''. Dacă doriți să opriți din terminal de pe host, opriti procesul ''qemu'' cu ajutorul comenzii ''killall qemu-system-aarch64''. | ||
+ | * Dacă întâlniți probleme de rulare qemu, însă acesta se închide prea repede și nu puteți vedea eroarea, folosiți argumentele ''-no-reboot -no-shutdown''. | ||
</note> | </note> | ||
- | 2. În acest moment, cele doua partiții nu au niciun sistem de fișiere creat. Creați un //loop block device// pentru fiecare dintre cele doua imagini. Creați-le sistemele de fișiere, având grija la tipul acestora (revedeți secțiunea //Formatul imaginii RaspberryPI//). Odată create sistemele de fișiere, montați fiecare partiție într-un director. | + | ==== 3. Instalați serviciul Libvirt ==== |
- | <note warning> | + | Libvirt este un serviciu ce permite folosirea Qemu mult mai ușor. Împreună cu tool-uri precum ''virsh'' (virtual shell) sau ''virt-manager'', utilizatorul poate crea, porni, opri, clona sau migra mașini virtuale foarte ușor si rapid. În acest laborator vom folosi tool-ul ''virt-install'' pentru a crea o noua mașină virtuală. |
- | In procesul de boot al sistemului, partitiile sunt cautate de kernel dupa label-ul acestora. Pentru a ne asigura ca sistemul booteaza cu succes, va trebui sa denumim partitiile conform imaginii de Debian. | + | |
- | Optiunea ''-L'' a comenzii ''mkfs.ext4'', respectiv optiunea ''-n'' a comenzii ''mkfs.vfat'' ne ofera posibilitatea de a atribui un label atunci cand cream sistemul de fisiere. | + | <code> |
+ | sudo apt install virtinst | ||
+ | </code> | ||
- | Paritia de boot trebuie sa aiba label-ul ''RASPIFIRM'', iar partitia pentru rootfs trebuie sa aiba label-ul ''RASPIROOT''. | + | folosiți kernel-ul, initrd-ul si imaginea discului de la ex.3 pentru a crea o mașină virtuală: |
+ | <code> | ||
+ | virt-install --name rpi3-qemu-si \ | ||
+ | --arch aarch64 \ | ||
+ | --machine virt \ | ||
+ | --os-variant debian11 \ | ||
+ | --boot kernel=...,initrd=...,kernel_args="console=ttyAMA0 root=/dev/vda2 rw rootwait rootfstype=ext4" \ | ||
+ | --disk ... \ | ||
+ | --vcpus 2 \ | ||
+ | --nographic \ | ||
+ | --import \ | ||
+ | --feature acpi=off | ||
+ | </code> | ||
- | </note> | + | Pentru a ieși din consola virsh, folosiți combinația ''ctrl + ]''. Avem la dispoziție următoarele comenzi: |
- | 3. Ne dorim să copiem conținutul fișierelor din imaginea oficiala în imaginea noastră. Copiați conținutul directoarelor imaginii oficiale în directoarele imaginii noastre. După ce procesul de copiere s-a terminat, demontați device-urile și ștergeți loop device-urile. | + | * ''virsh list --all'' - listează toate mașinile virtuale |
+ | * ''virsh destroy NUME_VM'' - opreste o mașină virtuală | ||
+ | * ''virsh undefine NUME_VM'' - șterge o mașină virtuală | ||
+ | * ''virsh shutdown NUME_VM'' - trimite o comandă de graceful shutdown, similar cu apăsarea butonul de Power | ||
- | <note important> | + | Numele ''destroy'' poate induce putin în eroare, deoarece mașina este doar oprită și nu ștearsă. |
- | Conținutul partiției de boot trebuie copiat în partiția de boot corespondenta din imaginea noastră. În mod similar, copierea trebuie făcută intre partițiile de rootfs. | + | |
- | </note> | + | |
- | 4. Acum că imaginea noastră este gata, vrem să vedem că funcționează. Bootați sistemul în qemu folosind rootfs-ul nou creat. | + | ==== 4. Schimbați configurația VM-ului (CPU-ul si RAM) ==== |
- | <note tip> | + | Cu mașina virtuală de la ex. 4 oprită, editați configurația cu ajutorul următoarei comenzi și adăugați 8 procesoare si 32GB de RAM: |
- | * Pentru a rula QEMU va trebui să folosiți comenzile din [[ https://ocw.cs.pub.ro/courses/si/laboratoare/04#setup | Laboratorul 4 ]]. | + | |
- | </note> | + | |
- | 5. Porniți server-ul web scris cu framework-ul **Flask** de aici {{.:.home-automation.tgz | home-automation.tgz}} | + | <code> |
+ | virsh edit NUME_VM | ||
+ | </code> | ||
- | Pentru a-l rula, aveți nevoie de următorii pași de executat in Qemu/RPi: | + | În mod implicit, kernel-ul va refuza alocarea de memorie virtuală pentru un process de user-space, mai mult decât are sistemul disponibil. Dar îl putem convinge cu următoarea comanda executată in VM-ul de Ubuntu 22: |
+ | <code> | ||
+ | sudo sysctl vm.overcommit_memory=1 | ||
+ | </code> | ||
+ | |||
+ | Desigur, în momentul în care VM-ul începe să folosească multă memorie, VM-ul (procesul) va fi automat oprit (killed). | ||
+ | |||
+ | Porniți mașina virtuală, deschideți consola cu ajutorul comenzii "virsh console NUME_VM" și listați noua configurație: | ||
- | * Instalarea pachetului Python-Flask: | ||
<code> | <code> | ||
- | sudo apt install python3-pip | + | cat /proc/cpuinfo |
+ | </code> | ||
- | pip3 install flask | + | <code> |
- | pip3 install flask-wtf | + | free -m |
</code> | </code> | ||
- | * Deschideți server-ul cu următoarea comandă: | + | ==== 5. Accesul la Internet ==== |
+ | |||
+ | Configurați și testați accesul //guest//-ului la Internet. Salvați comanda folosită pentru emulare. | ||
+ | |||
+ | Pentru aceasta, opriți VM-ul din virsh si reluați comanda de la ex. 2 (vom folosi ''qemu''). | ||
+ | |||
+ | * Emulați interfața de rețea folosind un ''USB network adaptor'' virtual | ||
<code> | <code> | ||
- | python3 home-automation.py | + | -device usb-net,netdev=net0 \ |
+ | -netdev bridge,br=...,id=net0 | ||
</code> | </code> | ||
- | * Serverul ruleaza in QEMU pe portul 5000. Pentru a avea acces la acesta, va trebui sa setam o noua regula de port-forwarding in optiunile comenzii ''qemu-system-aarch64'': | + | |
+ | Pentru a avea Internet in interiorul VM-ului, putem urma pașii din secțiunea [[#networking| configurare a rețelei]]. | ||
+ | |||
+ | Dacă doriți să folosiți modul bridge, instalați daemon-ul libvirt (+ dnsmasq care lipsește ca dependință) care îl configurează automat: | ||
<code> | <code> | ||
- | ... | + | sudo apt install libvirt-daemon-system dnsmasq |
- | -netdev user,id=net0,hostfwd=tcp::5555-:22,hostfwd=tcp::5000-:5000 \ | + | |
- | ... | + | |
</code> | </code> | ||
- | * Pagina Web este disponibila la http://localhost:5000 in host. | + | Apoi porniți interfața de rețea folosind virsh: |
+ | <code> | ||
+ | virsh net-start default | ||
+ | # verificați că există virbr0: | ||
+ | ip addr sh | ||
+ | </code> | ||
- | <note warning> | + | Folosiți numele noului bridge pentru adaptorul de retea virtual, și permiteți folosirea lui în libvirt: |
- | Daca primiti eroare cand instalati pachetele necesare (''Temporary failure in name resolution'') , editati fisierul ''/etc/resolv.conf'' astfel incat sa aveti un nameserver valid. | + | |
<code> | <code> | ||
- | nameserver 8.8.8.8 | + | echo "allow virbr0" >> /etc/qemu/bridge.conf |
</code> | </code> | ||
+ | |||
+ | După ce pornește mașina virtuală, listați interfețele de rețea si porniți clientul de DHCP: | ||
+ | |||
+ | <code> | ||
+ | ip addr | ||
+ | dhclient en<XYZ> | ||
+ | </code> | ||
+ | |||
+ | <note tip> | ||
+ | * Înainte de realizarea configurațiilor de rețea, dezactivați conectarea automată din setările sistemului de operare (Settings -> Network -> Wired -> Connect Automatically (off)) și puneți placa de rețea a mașinii virtuale Ubuntu in modul de NAT (Devices -> Network -> Network Settings). Dacă folosiți o rețea wired și nu vă merge cu NAT atunci setați pe modul Bridged Adapter. Atenție, NU setați pe modul "NAT Network". | ||
+ | {{ :si:laboratoare:screenshot_2020-10-12_at_17.47.41.png |Setare rețea}} | ||
+ | * Recitiți secțiunea de [[#networking| configurare a rețelei]] în QEMU. | ||
+ | * Folosiți ''sudo'' pentru rularea ''qemu-system-arm''. | ||
</note> | </note> | ||
- | 6. Momentan server-ul nu poate servi decât cererile de GET, iar aceeași pagină va fi afișata mereu. Pentru a schimba starea/structurile interne ale server-ului, vom folosi o cerere de REST API contruită de browser-ul web, cu ajutorului unui script **JavaScript**. | + | ==== 6. BONUS ==== |
- | {{:si:laboratoare:si_flask_python.png?600|}} | + | Creați un //Makefile// generic pentru programul **hello world** care poate compila pentru orice sistem //target// în funcție de variabilele primite (convenția ''CROSS_COMPILE''). Compilați programul pentru //host// și pentru //target//-ul RaspberryPi, apoi salvați executabilele generate. |
- | Pentru ca browser-ul să ruleze acest script, el trebuie să fie returnat odată cu conținutul paginii principale (index.html). Copiați conținutul fișierului de aici {{.:.rest-api-request.js.txt | rest-api-request.js.txt}}, in interiorului fisierului templates/index.html, în secțiunea <script>. | + | Copiati cu ''scp'' binarul necesar pe target, ce observati? |
- | Reîncărcați pagina web și dați click pe butonul de pornit lumina. O cerere de REST API va fi trimisă către server, dar va fi nevoie de un refresh al paginii pentru a lua ultima stare a structurilor. | + | Ce puteți spune despre conținutul celor 2 fișiere executabile create la exercițiul anterior? |
- | 7. Creati un serviciu pentru server-ul de Flask, astfel incat acesta sa porneasca automat la start-up. Revedeti sectiunea despre [[ https://ocw.cs.pub.ro/courses/si/laboratoare/05#systemd_services | configurarea serviciilor ]]. | + | <note tip> |
- | ===== Resurse ===== | + | * Dacă o variabilă nu este setată, construcția ''$(//<variabilă>//)'' într-un //Makefile// va fi echivalentă cu șirul vid. |
+ | </note> | ||
+ | |||
+ | <note tip> | ||
+ | * Pentru informaţii legate de tipul fişierelor se poate folosi comanda **file**; | ||
+ | * Conținutul unui fișier executabil poate fi inspectat cu utilitarul **objdump** (ptr //target// folosiţi utilitarul din toolchain: **aarch64-linux-gnu-objdump**) | ||
+ | </note> | ||
+ | |||
+ | ==== Resurse ==== | ||
<hidden> | <hidden> | ||
- | * {{:si:si:lab:lab5n:sol_lab5.txt | Soluție laborator}} | + | * {{:si:laboratoare:qemu_solutii_2022.txt | Solutii laborator Qemu }} |
+ | * {{:si:laboratoare:tools_solutii_2022.zip| Soluție laborator toolchain}} | ||
</hidden> | </hidden> | ||
+ | * [[https://raspi.debian.net/tested/20220808_raspi_3_bookworm.img.xz| Imaginea Debian 13 (Bookwork) pentru Raspberry Pi 3B]] | ||
+ | |||
+ | |||
+ | ===== Referințe ===== | ||
+ | - [[http://wiki.qemu.org/Main_Page| QEMU website]] | ||
+ | - [[https://qemu.weilnetz.de/doc/4.2/qemu-doc.html#Introduction| QEMU man page (Introduction)]] | ||
+ | - [[https://en.wikibooks.org/wiki/QEMU| QEMU Wikibooks page]] | ||
+ | - [[https://en.wikipedia.org/wiki/QEMU| QEMU Wikipedia page]] | ||
+ | - [[https://qemu.weilnetz.de/doc/4.2/qemu-doc.html#Linux-User-space-emulator| QEMU man page (Linux User space emulator)]] | ||
+ | - [[https://wiki.freebsd.org/QemuUserModeToDo| QEMU BSD user-mode status page]] | ||
+ | - [[https://qemu.weilnetz.de/doc/4.2/qemu-doc.html#ARM-System-emulator| QEMU man page (ARM System emulator)]] | ||
+ | - [[https://qemu.weilnetz.de/doc/4.2/qemu-doc.html#sec_005finvocation| QEMU man page (Invocation)]] | ||
+ | - [[http://elinux.org/RPi_Hardware| RaspberryPi hardware description]] | ||
+ | - [[https://www.techrepublic.com/article/how-to-enable-copy-and-paste-in-virtualbox/|Copy-paste in VirtualBox]] | ||
+ | |||
+ | |||
+ |