User Tools

Site Tools


lab:2015:modules

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 headerele 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 cazutri 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. Headerele 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.

Makefile pentru modul Hello World

Makefile
KDIR = ?
 
kbuild:
        make -C $(KDIR) M=`pwd`
 
clean:
        make -C $(KDIR) M=`pwd` clean
        rm -f *~ Module.symvers Module.markers modules.order
  • KDIR este directorul surselor nucleului, pe care le găsiți aici

Kbuild pentru modul Hello World

Kbuild
EXTRA_CFLAGS = -Wall -g
 
obj-m = hello.o

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

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

  • (2p) Hello world. Scrieți modulul de kernel Hello world și inserați-l în kernel-ul de pe placa de laborator.

Va trebui să setați variabilele CROSS_COMPILE și ARCH, respectiv KDIR, pentru a realiza compilarea pentru arhitectura plăcii de laborator

  • (5p) Kill init. By default, semnalul SIGKILL a fost dezactivat pentru procesul init și nu poate fi trimis acestuia (cum de altfel am învățat și în laboratorul de rootfs). Realizați un modul de kernel care activează, la inserare, posibilitatea de trimitere a acestui semnal pentru procesul cu PID-ul 1, și verificați acest lucru cu ajutorul comenzii kill.

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

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

Câmpul signal, de tip struct signal_struct conține flags, un câmp care poate avea valorile de aici.

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

Pentru a putea trimite semnalul SIGKILL procesului init, acestuia trebuie să i se reseteze SIGNAL_UNKILLABLE, din flag-urile de semnal asociate lui.

  • (3p) GPIO direct în memoria perifericelor Broadcom. Până acum ați interacționat cu LED-urile prin interfața oferită în /sys/class/leds de către un driver. Acum vom avea ocazia să vedem ce se află în spatele acelei interfețe. BCM2708 (procesorul de pe RaspberryPi) are niște registre pentru “General Purpose I/O”. Realizați un modul de kernel care la inițializare aprinde LED-ul ACT (Activity), urmând ca la ieșire să îl stingă.

Setarea/resetarea LED-ului se va face direct în memorie (la adresele unde sunt mapați regiștrii de control GPIO) cu ajutorul funcției writel (care scrie 4 octeți în memorie, la o adresă dată - asemănător laboratorului de Proiectarea cu Microprocesoare).

LED-ul are index-ul 16 și este active low

Funcția writel este definită ca un macro la funcția __raw_writel, unde b reprezintă valoarea de scris, iar addr reprezintă adresa la care se va face scrierea. Căutați adresele și comportamentul registrelor GPIO în datasheetul procesorului Broadcom. Luați în vedere mapările de aici în calculul adresei.

  • (bonus - 1p) Kernel GPIO. Realizați același comportament ca la exercițiul anteriror, însă de această dată folosind biblioteca de lucru cu GPIO a kernel-ului.

Funcțiile importante puse la dispoziție de bibliotecă sunt următoarele:

  • int gpio_direction_input(unsigned gpio)
  • int gpio_direction_output(unsigned gpio, int value), unde value este valoarea inițială.
  • void gpio_set_value(unsigned gpio, int value)

gpio este un identificator dat fiecărui pin (pinii fiind codificați numeric pe RaspberryPi)

Pentru a putea folosi funcțiile din biblioteca gpio, includeți header-ul linux/gpio.h

Resurse

Referințe

lab/2015/modules.txt · Last modified: 2016/10/13 20:46 by Dan Dragomir