Laboratorul 09. Module de Kernel

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?

Memory Management Unit și importanța sa în dezvoltarea de module

Sisteme embedded fără MMU

Î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.

Sistem embedded cu MMU

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ă.

Arhitectura kernelului

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.

Monolitic

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.

Microkernel

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.

Linux Kernel

Î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.

Programarea modulelor de kernel

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:

  • Kernelul nu este link-at cu biblioteca standard C, nu se pot folosi niciunele dintre apelurile de bibliotecă cunoscute!
  • Se pot folosi însă funcțiile, macro-urile și variabilele exportate de kernel. Acestea se găsesc în header-ele kernel-ului.
  • Nu se accesează direct zona de memorie care poate fi accesată și din modul neprivilegiat (a.k.a. user-space). Tot ce provine din user-space trebuie privit cu suspiciune. Există macro-uri speciale pentru transferul dintre cele două zone de memorie.
  • Accesele invalide la memorie trebuiesc evitate, deoarece sunt mult mai grave decăt cele din user-space: pe Windows se generează BSOD (Blue Screen of Death), iar pe Linux se dă mesajul kernel oops (sau în cazuri mai grave kernel panic), sistemul devenind instabil (de cele mai multe ori fiind necesar un restart).

Modulul Hello world

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.

hello.c
#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:

  1. Header-ele necesare
  2. Definirea autorului, licenței și descrierii
  3. Funcția my_init
    • Specificatorul __init este un macro pentru __section(.init.text) __cold notrace care specifică gcc-ului în ce segment al executabilului să pună această funcție. În cazul modulelor care se încarcă odată cu kernelul (builtin), funcția init se va apela o singură dată, după care va rămâne în memoria sistemului (kernel-space) până la închiderea sistemului. .init.text este un segment care este eliberat după inițializarea kernel-ului (se poate observa în timp ce pornește sistemul mesajul Freeing unused kernel memory: 108k freed).
    • Funcția printk este echivalentul în kernel al funcției printf. Ieșirea este însă direcționată către un fișier log, /var/log/messages. Diferența în folosire este dată și de specificarea priorității cu macro-ul KERN_ALERT, care de fapt se traduce în șirul de caractere <1>. Sistemul va putea fi configurat astfel să ignore anumite mesaje.
  4. Funcția my_exit
    • Specificatorul __exit este un macro pentru __section(.exit.text) __exitused __cold notrace. În cazul modulelor care se încarcă odată cu kernelul (builtin), funcția exit nu se va apela niciodată, deci va fi ignorată de compilator.
  5. Înregistrarea celor două funcții ca init și exit pentru modulul respectiv.

Module de kernel In-Tree

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:

.config
#
# 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).

Module de kernel Out-of-Tree

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:

some_kernel_module.c
#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).

Utilitare pentru lucrul cu module

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 

Exerciții

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 7, 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

Revizitați laboratorul 04 pentru a va reaminti despre cross-compiling.

Copiați și incărcați modulul hello.ko pe RPi și verificați că mesajele sunt afișate.

Elixir Cross Reference este un site care indexează sursele de Linux. Îl puteți folosi pentru a vedea definițiile funcțiilor și macro-urilor pe care le veți folosi.

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.

Puteti utiliza 2 functii pentru a afisa mesaje de kernel: printk si pr_<nivel>.

3. Folosiți modulul de kernel din folder-ul rename-init pentru a redenumi procesul “Init”

În kernel, un proces este reprezentat de structura struct task_struct

Folosiți structura struct task_struct, cu accent pe cămpurile pid, și parent

Există o variabilă, current, de tip struct task_struct *, care reprezintă procesul curent, și care este exportată de kernel tuturor modulelor.

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.

Resurse

si/laboratoare/09.txt · Last modified: 2023/12/11 13:12 by cristian.vijelie
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