Un sistem embedded poate funcționa doar cu perifericele pe care le-am folosit deja (rețea, card SD, USB), însă va fi strict limitat la hardware-ul pentru care exista deja suport. Ce se întâmplă atunci când dorim să folosim un hardware nou sau diferit de cel pentru care există suport?
În sistemele care nu au MMU scrierea unui modul este directă: fie că modulul este foarte strâns cuplat cu întreg sistemul, fie că este sub forma unei biblioteci de funcții, totul se află în același spațiu de memorie cu programul care rulează (sau sistemul de operare, în cazurile mai complexe). Dezavantajele principale al acestui tip de sistem sunt lipsa de securitate și de stabilitate. Orice vulnerabilitate sau bug afectează și periclitează întreg sistemul, putând duce chiar la compromiterea acestuia.
Mai mult, două programe (de exemplu într-un sistem de operare cu multi-tasking cooperativ) pot concura pentru aceeași resursă, chiar dacă nu rulează concomitent. Dacă luăm exemplul aplicațiilor de la PM, dacă o funcție configura seriala cu un anumit baud rate, apoi ceda controlul altei funcții care seta baud rate la altă valoarea, valoarea finală va fi a doua, iar codul asociat cu prima funcție nu va mai rula corect.
Deși pare greu de ajuns la o asemenea situație, în realitate este foarte ușor: unul din modurile de a implementa sisteme multitasking colaborative este folosind corutine, funcții cu mai multe puncte de intrare care cedează controlul. În astfel de sisteme, o corutină joacă rolul unui proces. Astfel, având mai mult de un dezvoltator putem fi siguri că, mai devreme sau mai târziu, va apărea o situație ca cea menționată în scenariul anterior.
Sistemele cu MMU rezolvă această problemă. Accesul la hardware poate fi făcut doar de aplicațiile ce rulează într-un mod special, numit și privilegiat. Astfel, codul care interfațează hardware-ul va fi pus într-o zonă protejată de memorie, accesibilă doar din modul privilegiat. Un layer adițional va face comunicația între acest cod și codul neprivilegiat care îl folosește. Astfel, modulul va fi protejat de vulnerabilitățile programelor care îl utilizează, nu va avea probleme în urma crash-urilor lor și va putea face o arbitrare a accesului la resursa hardware pe care o gestionează.
Din punct de vedere al modului în care este organizată, arhitectura kernelului poate fi de două tipuri: monolitică și microkernel, însă pot exista și implementări hibride.
Arhitectura monolitică a unui kernel însemnă că toate serviciile oferite de acesta rulează într-un singur proces, în același spațiu de adrese (kernel-space) și cu aceleași privilegii. Toate facilitățile pe care le pune la dispoziție sunt înglobate într-un singur binar.
Spre deosebire de cel monolitic, microkernel-ul implementează un pachet minimal de servicii și mecanisme necesare pentru implementarea unui sistem de operare precum: alocarea memoriei, planificarea proceselor, mecanismele de comunicare între procese (IPC). Restul serviciilor (networking, filesystem, etc.) rulează ca daemoni în user-space (modul neprivilegiat).
După cum se poate observa, un mare dezavantaj al kernel-ului de tip monolitic îl reprezintă lipsa de modularitate și de extensibilitate, lucruri care au fost adăugate ulterior. Astfel, actual, kernel-urile monolitice au posibilitatea de a fi extinse la runtime cu noi funcționalități, prin inserarea de module. Aceste module vor rula în spațiu de adrese al kernelului.
În general, kernel-ul Linux rulează pe arhitecturi cu suport de MMU, implementarea tuturor funcționalităților bazându-se în mare măsură pe existența acestei componente hardware. Există însă un fork al kernel-ului existent de Linux pentru arhitecuri fără MMU, numit uClinux. Mai multe detalii despre acest subiect găsiți aici.
De altfel, din punct de vedere al arhitecurii kernel-ului, Linux este monolitic cu posibilitatea de extindere la runtime prin module.
Având în vedere că modulele rulează în kernel-space, în mod privilegiat, codul trebuie scris cu mare grijă. Astfel, programarea modulelor de kernel este guvernată de anumite reguli:
Orice modul de kernel are nevoie de o funcție de inițializare și o funcție de cleanup. Prima se apelează atunci când modulul este încărcat, a doua este apelată când modulul este descărcat. De asemenea, modulele au nevoie de autor, licență și descriere, utilizarea acestor macro-uri fiind obligatorie.
#include <linux/init.h> /* for __init and __exit */ #include <linux/module.h> /* for module_init and module_exit */ #include <linux/printk.h> /* for printk call */ MODULE_AUTHOR("SI"); MODULE_LICENSE("GPL"); MODULE_DESCRIPTION("Hello world module"); static int __init my_init(void) { printk(KERN_DEBUG "Hello, world!\n"); return 0; } static void __exit my_exit(void) { printk(KERN_DEBUG "Goodbye, world!\n"); } module_init(my_init); module_exit(my_exit);
Modulul Hello World conține:
Am văzut in laboratorul 9 cum putem configura (personaliza) build-ul de kernel, prin selectarea componentelor care vrem să fie parte din imaginea de kernel (build-in) sau compilate ca module de kernel. Aceste module de kernel (fie ele drivere video, module de criptografie sau suportul de sunet) se numesc “in-tree”, deoarece ele fac parte din codul sursă care vine odata cu tot kernel-ul de Linux. Fișierul de configurație al kernel-ului (.config) păstrează aceaste optiuni:
# # Serial drivers # CONFIG_SERIAL_EARLYCON=y CONFIG_SERIAL_8250=m # Compiled as kernel module CONFIG_SERIAL_8250_DEPRECATED_OPTIONS=y CONFIG_SERIAL_8250_16550A_VARIANTS=y
Avantajul în a avea aceste componente compilate ca module de kernel este că ele pot fi inserate in kernel la cerere (on-demand), în funcție de ce device-uri sunt prezente in sistem și ce nevoi are utilizatorul. Spre exemplu, folosirea NAT-ului în rețea, pentru a folosi sistemul ca un router de Layer 3, va moment in care modulul de kernel nf_nat.ko este incărcat).
Dezavantajul modulelor de kernel In-Tree este că vin împreună cu acea versiune de kernel pentru care au fost compilate (deoarece au codul in același repository). Dacă spre exemplu utilizatorul dorește o versiune mai nouă a unui driver de placă wireless, el trebuie sa aștepte un nou update al distribuției de Linux (ex. Ubuntu), sau trebuie sa recompileze tot kernel-ul.
Majoritatea producătorilor de componente hardware (ex. Nvidia, Intel, Realtek) fac disponibile online atât codul sursă al driverele cât și firmware-ul pe care driver-ul îl încarcă pentru device-ul respectiv. Pentru a face codul modulelor de kernel compatibil cu cat mai multe versiuni de kernel, se apelează la construcții precum:
#if ( LINUX_VERSION_CODE < KERNEL_VERSION(3,1,0) ) // Use specific structures before kernel 3.1 #else // Use the new structures, after kernel 3.1 #endif
În exemplul de mai sus, dacă compilăm modulul de kernel pentru un kernel mai nou (ex. 5.1), atunci codul va folosi al doilea branch de “if”.
Pentru a compila un modul de kernel Out-of-Tree, utilizatorul are nevoie doar de header-ele de kernel cu care a fost compilat kernel-ul care rulează. Majoritatea distribuțiilor de Linux au aceste headere disponibile:
sudo apt install linux-headers-`uname -r`
Comanda “uname -r” returnează versiune actuală de kernel care rulează (ex. 5.11.0).
insmod
inserează module în kernel:
~#insmod hello_mod.ko
Modprobe face același lucru, dar cu modulele puse deja în sistemul de fișiere în locul corespunzător (/lib/modules/`uname -r`/… - unde uname -r este versiunea nucleului). În plus, modprobe va citi și lista de dependențe ale modulului de încarcat și o va rezolva (va insera și alte module, dacă e nevoie).
Observați că doar insmod necesită calea modulului (cu tot cu extensie)
~#modprobe hello_mod
Pentru descărcare, se folosește fie:
~#modprobe -r hello_mod
fie:
~#rmmod hello_mod
Afișarea modulelor încărcate se face cu lsmod
~#lsmod
Afișarea mesajelor date cu printk din modul se găsesc în /var/log/messages, se afișează cu:
~#dmesg | tail
sau cu
~#tail -f /var/log/messages
0. Pregătirea imaginii de kernel si a header-elor de kernel
În cadrul laboratorul de astăzi, vom lucra cu module de kernel Out-of-Tree, pe un Raspberry PI fizic. Compilarea modulelor se va face in afara RPi-ului. Va fi nevoie de kernel-ul compilat in laboratorul 8, care trebuie sa fie instalat pe Raspberry PI.
Codul modulelor de kernel se afla aici.
1. Compilați modulul de kernel Hello World cu header-ele de kernel corespunzătoare.
În directorul hello
, rulati comanda:
KDIR=<locatia_kernel-ului_linux> CROSS_COMPILE=aarch64-linux-gnu- ARCH=arm64 make
Copiați și incărcați modulul hello.ko pe RPi și verificați că mesajele sunt afișate.
2. Afisati mesajul de “Hello World” cu mai multe niveluri de afisare: info, warning, error, critical. Incercati si cu nivelul de debug. Puteti urmari discutia de aici.
3. Folosiți modulul de kernel din folder-ul rename-init
pentru a redenumi procesul “Init”
Inainte de inserarea modulului de kernel, citiți numele procesului Init (PID 1) cu următoarea comanda:
cat /proc/1/status
Verificați că numele procesului Init a fost modificat.
4. Dezactivați intreruperea de timp cu ajutorul modulului din folder-ul disable-interrupt
.
Pentru a afla numărul alocat intreruperii de timp, folosiți comanda:
cat /proc/interrupts
Odată ce ați aflat numărul înteruperii, completați numărul interuperii in codul C si recompilați. După inserarea modulului de kernel rulați comanda:
sleep 1
Comanda “sleep 1” ar trebui să returneze după o secundă, dar va rămâne blocată deoarece timer-ul HW (de pe placă de bază) nu mai poate genera interuperi. După descărcarea modulului de kernel, comanda sleep va relua execuția.