User Tools

Site Tools


linux-kernel-dev:capitole:capitol-03

This is an old revision of the document!


3. Drivere de dispozitiv

Subiecte abordate

  • API-ul expus de kernel pentru module de kernel: printing, lucrul cu memoria, liste
  • Erori în kernel, depanarea erorilor
  • Dispozitive văzute în user space
  • Dispozitive virtuale
  • Operații pe dispozitive

Resurse utile

Exerciții

Arhiva de suport pentru exerciții se găsește aici. Descărcați arhiva și apoi decomprimați-o folosind comanda

unzip cap-03-tasks.zip

Alocare dinamică a memoriei

Alocarea dinamică a memoriei în nucleul Linux se face, de regulă, folosind apelul kmalloc. Acesta alocă memorie rezidentă (care ocupă în permanență memoria până la allocare).

În directorul alloc/ din arhiva de suport există un modul de kernel care alocă 128KB de RAM într-o buclă for. Întrucât modulul doar alocă memoria, nu o și dezalocă aceasta rămâne ocupată.

Compilați modulul și apoi încărcați-l și descărcați-l de mai multe ori în kernel. Observați după fiecare încărcare/descărcare dimensiunea memoriei ocupate. Pentru a afla această informație puteți folosi una dintre comenzile de mai jos:

free
cat /proc/meminfo

Dacă s-ar aloca foarte multă memorie sau dacă bucla for ar avea o durată mai mare, sistemul va rămâne fără memorie și la un moment dat va îngheța. În momentul în care sistemul are puțină memorie se activează OOM (Out of Memory) care începe să omoare procese ca să elibereze memoria.

Eliberarea memorie alocate dinamic

Pentru anularea încărcării memoriei, trebuie să eliberăm memoria alocată dinamică.

Pentru aceasta definiți un vector în care rețineți pointerii către zonele de memorie alocate. Apoi, în funcția de exit a modulului, folosiți funcția kfree() pentru a elibera memoria astfel alocată.

Afișarea listei de procese a sistemului

Nucleul se ocupă de gestionarea sistemului și una dintre structurile esențiale este lista. În general nucleul folosește liste dublu înlănțuite pentru a permite parcurgeri în ambele sensuri.

În general un modul de kernel va avea nevoie să parcurgă listele definite în kernel. În anumite situații va și adăuga și șterge elemente dar cea mai frecventă operație este parcurgerea.

În directorul print-processes/ din arhiva de suport a capitolului se găsește o implementare de modul de kernel care afișează PID-urile și comenzile proceselor sistemului (referite de pointeri la structuri de tipul task_struct) atât în funcția de inițializare cât și în funcția de ieșire. În fiecare funcție folosește alt format: în inițializare parcurge în mod clasic lista de procese începând cu procesul curent, în vreme ce în funcția de ieșire folosește macro-ul for_each_process.

Afișarea listei de procese copil a procesului init

Afișați pentru procesul init, PID-ul și comanda proceselor sale copil.

Structura task_struct a procesului init este indicată de variabila globală init_task.

Santinela listei dublu înlănțuite este dată de câmpul children al structurii task_struct.

Folosiți macro-ul list_for_each pentru a parcurge procesele copil și apoi macro-ul list_entry pentru a obține structura task_struct pentru fiecare proces din listă.

Afișarea zonelor de memorie ale procesului

Pentru acomodarea cu lucrul cu liste parcurgeți zonele de memorie virtuală ale procesului curent. Pentru fiecare zonă afișați adresa de start și cea de sfârșit.

O zona de memorie virtuală este dată de structura vm_area_struct, definită în include/linux/mm_types.h.

Primul element al listei de zone este câmpul mmap al structurii mm_struct care definește spațiul virtual de adresă al procesului. Iar pointerul la structura care definește spațiul virtual de adresă al procesului este dat de câmpul mm al structurii task_struct.

Depanarea unui oops

Atunci când nucleul întâlnește o eroare afișează un mesaj de eroare similar Segmentation fault numit oops. Este o indicație că ceva nepotrivit a avut loc; sistemul de operare continuă execuția, dar este posibil ca eroare să se propage.

Erorile grave se prezintă în formă de kernel panic și conduc la înghețarea sistemului.

În directorul oops/ din arhiva de suport a laboratorului se găsește implementat un modul care accesează un pointer NULL rezultând în oops. Încărcați modulul pentru a observa acest lucru. Observați că nu se mai permite nucleului să fie descărcat pentru a nu cauza mai multe pagube.

Atunci când avem module de kernel complexe e dificil de verificat unde a apărut eroarea. Pentru aceasta putem folosi utilitarul addr2line care realizează o asociere între adresa în care a apărut oops-ul și linia din fișier. Folosirea addr2line depinde de două informații:

  1. Adresa unde a fost încărcat modulul, identificabilă din fișierul /proc/modules.
  2. Adresa unde a apărut oops-ul, identificabilă din mesajul de oops, parcurgând registrul EIP (instruction pointer).

Se calculează offset-ul între cele două adrese, adică diferențaa dintre adresa unde a apărut oops-ul și adresa unde a fost încărcat modulul. Cu acest offset se rulează, pe mașina virtuală VirtualBox, comanda

addr2line -e <module-name.o> <offset>

unde <module-name>.o este numele fișierului obiect cu extensia .o (nu .ko) iar <offset> este offset-ul calculat anterior.

În cazul nostru comanda de rulat pe mașina virtuală VirtualBox va fi

addr2line -e oops_mod.o 0x17

Output-ul acestei comenzi este linia din cod care a cauzat oops-ul. Așa cum era de așteptat această linie este chiar linia în care se derefernțiază pointer-ul inițializat la NULL.

Interpretarea unui stacktrace

În directorul workqueue-bad/ avem o implementare defectuoasă a afișării la 5 secunde a unui mesaj. Atunci când inserăm modulul și apoi îl eliminăm primim eroare. Motivul este că există încă planificat un work iar modulul este eliminat ducând la eliberarea zonelor alocate. Consecința este mesajul de eroare.

Captura mesajului de eroare, prin stacktrace-ul afișat, ne oferă informații despre ce cauzează eroarea.

Ce se întâmplă cu variabila locală work în momentul în care funcția scchedule_work() se încheie?

ioctl în dispozitiv de tip caracter

Comunicarea și interacțiunea dintre user space și kernel space se poate face prind dispozitive (devices). Acestea sunt vizibile utilizatorului ca intrări în sistemul de fișier (de obicei în /dev) pe care acesta le poate opera.

În directorul ioctl-print/ avem un modul de kernel care expunde dispozitivul /dev/print și care, la operații de tip ioctl (Input/Output Control) afișează un mesaj. Este un modul didactic, vrem să urmărim comportamentul ioctl.

Pentru a putea observa apelul ioctl() folosim și un modul de test rulabil din user space, care este compilat static în executabilul print_test.

Modulul de kernel se găsește în subdirectorul kernel/ în vreme ce testul de user space se găsește în subdirectorul user/.

Atât fișierul de test cât și fișierul modul se copiază pe mașina virtuală pentru a putea testa funcționalitatea respectivă: afișarea unui mesaj la apelul ioctl().

După ce modulul este inserat, trebuie creat dispozitivul /dev/print cu ajutorul comenzii

mknod /dev/print c 42 0

Apoi se poate rula executabilul de test cu opțiunea aferentă pentru a invoca funcționalitatea de tip ioctl:

./print_test p

Alocare și dezalocare la ioctl

Adăugați două noi operații la ioctl, astfel încât la una dintre ele să aloce o zonă de memorie de 10KB și să rețină pointer-ul rezultat într-o variabilă globală, iar la cealaltă să-l elibereze.

Nu va aloca dacă a fost deja alocată zona de memorie, nu o va elibera dacă a fost deja eliberată.

Citire din dispozitiv

Operațiile de tipul ioctl sunt operații de comandă/control. Pentru transfer de informație cel mai adesea follosim operații de tipul read și write. Pentru început să folosim operația read.

În directorul read-string/ din arhiva de suport a capitolului se găsește o implementare de modul care întoarce un șir predefinit (anaaremere) la orice operație de citire. Practic, oricând folosim comanda cat pe dispozitivul afernet (/dev/string) ne va afișa șirul predefinit.

Compilați modululul și apoi copiați-l pe mașina virtuală QEMU și testați această funcționalitate.

După ce modulul este inserat în kernel, va trebui să creați dispozitivul aferent. Pentru aceasta rulați, în mașina virtuală QEMU, comanda

mknod /dev/string c 43 0

Apoi, pentru a testa, folosiți comanda cat pe dispozitiv, pentru a citi șirul:

cat /dev/string

În modul vor fi, pentru fiecare apel al funcției string_read(), afișate niște mesaje legat de numărul de octeți care s-au dorit citiți și numărul de octeți care s-a citit. De câte ori a fost apelată funcția string_read()? Cum explicați mesajele afișate (în fapt ne interesează valorile de retur ale funcției)?

Generator de caractere de 1

Creați un modul de kernel care expune un dispozitiv /dev/one care la citire generează o infinnitate de caractere de 1. Se oprește doar când cere spațiul utilizator să se oprească. Este similar cu /dev/zero.

Reply device

Adăugați funcționalitatea de write modulului din directorul read-string/ astfel încât sa și permită scrierea de mesaje în dispozitiv. După ce un mesaj este scris, același mesaj este citit de operațiile de citire.

Pentru a scrie un mesaj într-un fișier, folosiți comanda:

echo "mesaj" > <fisier>

unde <fisier> este numele intrării în care vreți să scrieți, de exemplu /dev/string.

Length device

Actualizați modulul de mai sus astfel încât la citire să nu mai întoarcă șirul scris, ci un șir de cifre care să însemne lungimea șirului scris anterior.

linux-kernel-dev/capitole/capitol-03.1441740645.txt.gz · Last modified: 2015/09/08 22:30 by razvan