Un dispozitiv periferic este controlat prin scrierea și citirea registrelor sale. De cele mai multe ori, un dispozitiv are mai multe registre, care pot fi accesate la adrese consecutive, fie în spațiul de adrese din memorie, fie în spațiul de adrese I/O. Fiecare dispozitiv conectat la magistrala I/O are un set de adrese I/O, numite porturi I/O. Porturile I/O pot fi mapate la adrese din memoria fizică, astfel încât procesorul va putea realiza comunicarea cu dispozitivul prin instrucțiuni care operează direct cu memoria. Din motive de simplitate, vom folosi în mod direct porturile I/O (fără mapare la adrese din memoria fizică) pentru comunicarea cu dispozitivele fizice.
Porturile I/O ale fiecărui dispozitiv sunt structurate într-un set de registre specializate, pentru a oferi o interfață uniformă pentru programare. Astfel, majoritatea dispozitivelor vor avea următoarele tipuri de registre:
În majoritatea cazurilor, porturile fizice sunt diferențiate după numărul de biți: pot fi porturi pe 8, 16 sau 32 de biți.
Spre exemplu, portul paralel are 8 porturi de I/O pe 8 biți, începând de la adresa de bază 0x378
. Registrul de date se găsește la adresa de bază (0x378
), registrul de status la adresa de bază + 1 (0x379
), iar cel de control la adresa de bază + 2 (0x37a
). Registrul de date este atât registru de intrare cât și de ieșire.
Deși există echipamente care pot fi controlate în întregime folosind porturi I/O sau zone de memorie speciale, există și situații în care acest lucru este insuficient. Principala problemă care mai trebuie tratată e faptul că anumite evenimente au loc la intervale de timp nedeterminate și este ineficient ca procesorul (CPU) să interogheze repetat echipamentele despre starea lor (polling). Modalitatea de rezolvare a acestei probleme o reprezintă IRQ (Interrupt ReQuest), notificări hardware prin care procesorul este anunțat de apariția unui anumit eveniment extern.
Pentru ca IRQ-urile să fie utile, trebuie să existe secvențe de cod care să le trateze. Deoarece, în multe situații, numărul de întreruperi disponibile este limitat, un device driver trebuie să aibă un comportament cât mai “civilizat” cu întreruperile: adică întreruperile trebuie să fie cerute înainte de a fi utilizate și eliberate atunci când nu mai este nevoie de ele. În plus, în anumite situații, device driver-ele trebuie să folosească în comun o întrerupere sau să se sincronizeze cu întreruperile. Despre toate acestea vom discuta în continuare.
Atunci când trebuie să accesăm resurse partajate între o rutină de tratare a întreruperii (A) și cod ce rulează în context proces sau într-o rutină de tratare a unei acțiuni amânabile (B), trebuie să folosim un mod special de sincronizare. În (A) trebuie să folosim o primitivă de tip spinlock, iar în (B) trebuie să dezactivăm întreruperile ȘI să folosim o primitivă de tip spinlock. Dezactivarea întreruperilor nu este suficientă pentru că rutina de întrerupere poate rula pe un alt procesor decât cel pe care rulează (B).
Folosirea doar a unui spinlock poate duce la deadlock. Exemplul clasic de deadlock în acest caz este:
În Linux, conceptul de porturi I/O este implementat pe toate platformele.
Înainte de a putea lucra cu porturile I/O, trebuie să ne asigurăm că avem acces exclusiv la ele. Pentru a obține porturile dorite, se folosește funcția request_region:
#include <linux/ioport.h> struct resource *request_region(unsigned long first, unsigned long n, const char *name);
Parametrul first
specifică adresa de bază pentru dispozitiv, iar n
numărul de porturi dorite. Adresa de bază este începutul zonei contigue de adrese (sau de porturi I/O) asociate dispozitivului și trebuie să fie unică pentru fiecare dispozitiv. Parametrul name
reprezintă numele dispozitivului.
Pentru eliberarea porturilor rezervate, se folosește funcția release_region:
void release_region(unsigned long start, unsigned long n);
Spre exemplu, portul serial COM1
are adresa de bază 0x3F8
și deține 8 porturi. Secvența de cod pentru alocarea porturilor asociate acestuia este următoarea:
#include <linux/ioport.h> #define MY_BASEPORT 0x3F8 #define MY_NR_PORTS 8 if (! request_region(MY_BASEPORT, MY_NR_PORTS, "com1")) { /* handle error */ return -ENODEV; }
iar cea pentru eliberare:
release_region(MY_BASEPORT, MY_NR_PORTS);
De cele mai multe ori, alocarea porturilor se realizează la inițializarea driver-ului, în funcția init_module
, iar eliberarea acestora la deinițializare, în funcția cleanup_module
.
Toate porturile alocate apar în /proc/ioports
:
$ cat /proc/ioports 0000-001f : dma1 0020-0021 : pic1 0040-005f : timer 0060-006f : keyboard 0070-0077 : rtc 0080-008f : dma page reg 00a0-00a1 : pic2 00c0-00df : dma2 00f0-00ff : fpu 0170-0177 : ide1 01f0-01f7 : ide0 0376-0376 : ide1 0378-037a : parport0 037b-037f : parport0 03c0-03df : vga+ 03f6-03f6 : ide0 03f8-03ff : serial ...
După ce un driver a obținut intervalul de porturi I/O dorite, trebuie să realizeze operații de citire sau scriere pe aceste porturi. Întrucât porturile fizice sunt diferențiate după numărul de biți (8, 16 sau 32 de biți), există diferite funcții de acces a porturilor în funcție de dimensiunea lor. În asm/io.h
sunt definite următoarele funcții de acces a porturilor:
u8 inb(unsigned long addr)
, citește porturi de dimensiune un octet (8 biți)void outb(u8 value, unsigned long addr)
, scrie porturi de dimensiune un octet (8 biți)u16 inw(unsigned long addr)
, citește porturi de dimensiune doi octeți (16 biți)void outw(u16 value, unsigned long addr)
, scrie porturi de dimensiune doi octeți (16 biți)u32 inl(unsigned long addr)
, citește porturi de dimensiune patru octeți (32 biți)void outl(u32 value, unsigned long addr)
, scrie porturi de dimensiune patru octeți (32 biți)
Argumentul port specifică adresa portului de unde se citește sau se scrie, iar tipul său este dependent de platformă (poate fi unsigned long
sau unsigned short
).
Anumite platforme pot avea probleme atunci când procesorul încearcă să transfere date prea rapid către și de la dispozitiv. Soluția este inserarea unei întârzieri după fiecare instrucțiune de I/O, în cazul în care urmează o altă instrucțiune de același tip. În cazul în care dispozitivul pierde date, se pot folosi funcții care introduc această întârziere; numele acestora este similar cu cele descrise mai sus, cu deosebirea că se termină în _p
: inb_p
, outb_p
etc.
Spre exemplu, următoarea secvență scrie un octet pe portul serial COM1
și apoi îl citește:
#include <asm/io.h> #define MY_BASEPORT 0x3F8 unsigned char value = 0xFF; outb(value, MY_BASEPORT); value = inb(MY_BASEPORT);
La fel ca și în cazul celorlalte resurse, un driver trebuie să obțină accesul la o linie de întreruperi înainte de a o putea utiliza și să o elibereze la finalul execuției.
În Linux, cererea de obținere și respectiv eliberare a unei întreruperi se face cu ajutorul funcțiilor request_irq
și free_irq
:
#include <linux/interrupt.h> typedef irqreturn_t (*irq_handler_t)(int, void *); int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev); const void *free_irq(unsigned int irq, void *dev_id)
Se observă că pentru obținerea unei întreruperi dezvoltatorul apelează funcția request_irq
. În cadrul funcției request_irq
trebuie să specifice numărul întreruperii (irq
), un handler ce va fi chemat în momentul generării întreruperii (handler
), flag-uri ce vor instrui kernelul despre comportamentul dorit (flags
), numele dispozitivului ce folosește această întrerupere (name
), și un pointer ce poate fi configurat de către utilizator la orice valoare, și care nu are semnificație globală (dev
). De cele mai multe ori, dev
va fi configurat la pointerul către datele private ale dispozitivului. În schimb, la eliberarea întreruperii, folosind funcția free_irq
dezvoltatorul trebuie să transmită aceeași valoare a pointerului (dev
), împreună cu numărul întreruperii (irq
). Numele dispozitivului (name
) este folosit pentru afișarea de statistici în /proc/interrupts
.
Valoarea pe care o întoarce request_irq
este 0 în cazul în care înregistrarea s-a efectuat cu succes sau un cod de eroare negativ care indică motivul eșecului. O valoare uzuală este -EBUSY
care este întoarsă atunci când întreruperea este ocupată deja de un alt echipament.
Există situații în care deși un dispozitiv folosește întreruperi nu putem citi regiștrii dispozitivului într-un mod non-blocant (de exemplu un senzor conectat la un bus I2C sau SPI al cărui driver nu garantează că operațiile de read / write pe bus sunt non-blocante). În această situație în întrerupere trebuie să planificăm o acțiune amânabilă ce rulează în context proces (work queue, kernel thread) pentru a putea accesa regiștrii dispozitivului. Pentru că o astfel de situație este relativ comună, kernel-ul pune la dispoziție funcția request_threaded_irq
pentru întregistra rutine de tratare a întreruperilor ce rulează în două faze: o fază în context proces și o fază în context întrerupere:
#include <linux/interrupt.h> int request_threaded_irq(unsigned int irq, irq_handler_t handler, irq_handler_t thread_fn, unsigned long flags, const char *name, void *dev);
handler
este funcția ce rulează în context întrerupere și va implementa operațiile critice și care pot fi executate din context întrerupere, în timp ce funcția thread_fn
rulează în context proces și va implementa restul operațiilor.
Flag-urile ce pot fi transmise la obținerea unei întreruperi sunt:
IRQF_SHARED
anunță kernelul că întreruperea poate fi partajată cu alte dispozitive. Dacă acest flag nu este setat, atunci, dacă există deja un handler asociat cu întreruperea cerută, cererea de obținere a unei întreruperi va eșua. O întrerupere partajată este tratată prin execuția tuturor rutinelor înregistrate. Această abordare duce la o situație interesantă: cum își poate da seama un device driver dacă rutina de tratare a întreruperii a fost activată de o întrerupere generată de dispozitivul pe care îl gestionează? Toate dispozitivele care oferă suport pentru întreruperi au asociate un registru de stare, care poate fi interogat în rutina de tratare pentru a afla dacă întreruperea a fost sau nu generată de dispozitiv (în cazul portului serial, acest registru de stare este IIR
- Interrupt Information Register). La cererea unei întreruperi partajate cu request_irq
, argumentul dev
trebuie să fie unic în kernel; poate fi setat la un pointer către datele private ale modulului, dar nu poate fi NULL
.IRQF_ONESHOT
întreruperea va fi reactivată după rularea rutinei de tratare din context proces; fără acest flag, întreruperea va fi reactivată dupa rularea rutinei de tratare din context întrerupere.
Obținerea întreruperii se poate realiza fie la inițializarea driver-ului, în funcția init_module
, fie atunci când dispozitivul este deschis prima dată, în funcția open
.
Următorul exemplu realizează obținerea întreruperii pentru portul serial COM1
:
#include <linux/interrupt.h> #define MY_BASEPORT 0x3F8 #define MY_IRQ 4 static my_init(void) { [...] struct my_device_data *my_data; int err; err = request_irq(MY_IRQ, my_handler, IRQF_SHARED, "com1", my_data); if (err < 0) { /* handle error*/ return err; } [...] }
După cum se poate observa, IRQ-ul pentru portul serial COM1
este 4, care este folosit în mod partajat (IRQF_SHARED
).
IRQF_SHARED
) cu request_irq
, argumentul dev
nu poate fi NULL
.
Pentru eliberarea întreruperii asociate portului serial se va executa următoarea secvență:
free_irq(MY_IRQ, my_data);
In funcția de inițializare, init_module
, sau în funcția de deschidere a dispozitivului, open
, trebuie activate întreruperile pentru a putea fi generate de către dispozitiv. Această operație este dependentă de dispozitivul folosit, dar de cele mai multe ori presupune setarea unui bit din registrul de control.
Pentru portul serial trebuie realizate două operații pentru activarea întreruperilor:
Aux Output 2
) în registrul MCR
- Modem Control RegisterRDAI
- Receive Data Available Interrupt, THREI
- Transmit Holding Register Empty Interrupt) prin setarea bitului corespunzător în registrul IER
- Interrupt Enable Register.
În exemplul de mai jos se activează întreruperea RDAI
pentru portul COM1
:
#include <asm/io.h> #define MY_BASEPORT 0x3F8 outb(0x08, MY_BASEPORT+4); outb(0x01, MY_BASEPORT+1);
Să examinăm acum signatura funcției de tratare a întreruperii:
irqreturn_t (*handler)(int irq, void *dev_id);
Funcția primește ca parametri numărul întreruperii pe care rutina o tratează și pointer-ul trimis la cererea de obținere a întreruperii. Rutina de tratare a întreruperii trebuie să întoarcă o valoare cu tipul irqreturn_t
. Pentru versiunea curentă de kernel, există trei valori valide: IRQ_NONE
, IRQ_HANDLED
și IRQ_WAKE_THREAD. Device driverul trebuie să întoarcă IRQ_NONE
dacă observă că întreruperea nu a fost generată de dispozitivul pe care îl comandă. În caz contrar, device driverul trebuie să întoarcă IRQ_HANDLED
dacă întreruperea poate fi tratată direct din context înterupere sau IRQ_WAKE_THREAD
pentru a planifica rularea funcției de tratare din context proces.
Un handler de întrerupere va avea următoarea structură:
irqreturn_t my_handler(int irq_no, void *dev_id) { struct my_device_data *my_data = (struct my_device_data *) dev_id; /* if interrupt is not for this device (shared interrupts) */ /* return IRQ_NONE;*/ /* clear interrupt-pending bit */ /* read from device or write to device*/ return IRQ_HANDLED; }
De obicei, primul lucru executat în rutina de tratare a întreruperii este să se determine dacă întreruperea a fost generată de dispozitivul comandat de driver. Pentru aceasta, de obicei se citesc informații din registrul de control al dispozitivului, care indică daca acesta a generat întreruperea. Al doilea lucru constă în resetarea bitului interrupt-pending
pe dispozitivul fizic; cele mai multe dispozitive nu vor mai genera întreruperi până când acest bit nu a fost resetat. Acest pas depinde doar de dispozitiv, și poate și lipsi (cum e cazul portului paralel, care nu are un astfel de bit). Portul serial are un astfel de bit: bitul 0 din registrul IIR
.
Deoarece rutinele de tratare pentru întreruperi se execută în context întrerupere, acțiunile care se pot efectua sunt limitate: nu se poate accesa memoria din user space, nu se pot apela funcții blocante, nu se poate face sincronizare doar cu spinlock-uri deoarece acest lucru ar duce la deadlock în cazul în care spinlock-ul este deja obținut de către un proces care a fost întrerupt.
Totuși, există cazuri în care device driverele trebuie să se sincronizeze cu întreruperile. În aceste situații este necesară dezactivarea întreruperilor și folosirea spinlock-urilor. Există două modalitați de dezactivarea a întreruperilor: dezactivarea tututor întreruperilor, la nivel de procesor, sau dezactivarea la nivel de întrerupere. Dezactivarea la nivel de procesor este mai rapidă și de asemenea previne deadlock-urile mai complexe și de accea este preferată. În acest scop, există funcții de locking care dezactivează, respectiv reactivează întreruperile: spin_lock_irqsave
, spin_unlock_irqrestore
, spin_lock_irq
și spin_unlock_irq
.
#include <linux/spinlock.h> void spin_lock_irqsave(spinlock_t *lock, unsigned long flags); void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags); void spin_lock_irq(spinlock_t *lock); void spin_unlock_irq(spinlock_t *lock);
Funcția spin_lock_irqsave
dezactivează întreruperile pentru procesorul local înainte de a obține spinlock-ul; starea anterioara a întreruperilor este salvată în flags
. În cazul în care sunteți siguri că întreruperile pe procesorul curent nu au fost deja dezactivate de altcineva (deci sunteți siguri că trebuie să activați întreruperile când eliberați spinlock-ul), puteți folosi funcția spin_lock_irq
.
Pentru spinlock-urile de tip read / write există funcții similare:
Dacă dorim să ne sincronizăm la nivel de întrerupere (nerecomandat pentru că este dezactivarea unei întreruperi este mai lentă, nu se pot dezactiva întreruperile partajate, pot apărea deadlock-uri în situații în care lucrăm cu mai multe întreruperi) o putem face cu ajutorul funcțiilor disable_irq
, disable_irq_nosync
și enable_irq
. Dezactivarea are loc la nivelul tuturor procesoarelor din sistem și apelurile pot să fie imbricate: dacă se apelează de două ori disable_irq
vor fi necesare tot atâtea apeluri enable_irq
pentru activarea ei. Diferența dintre disable_irq
și disable_irq_nosync
e că prima din ele va aștepta terminarea handler-elor aflate în execuție. Din această cauză disable_irq_nosync
este în general mai rapidă.
Spre exemplu, următoarea secvență dezactivează și apoi activează întreruperea pentru portul serial COM1
:
#define MY_IRQ 4 disable_irq(MY_IRQ); enable_irq(MY_IRQ);
Este posibilă și dezactivarea tuturor întreruperilor pentru procesorul curent. Dezactivarea tuturor întreruperilor de către device drivere pentru sincronizare este neadecvată. Funcțiile care dezactivează / reactivează întreruperile pe procesorul local sunt local_irq_disable
și local_irq_enable
. Nu puteți folosi aceste funcții în mod singular pentru sincronizare din cauza problemelor ce apar în sisteme multiprocesor.
Pentru a folosi o resursă partajată atât în context proces cât și în rutina de tratare a întreruperii, se vor folosi funcțiile descrise mai sus astfel:
static spinlock_t lock; /* IRQ handling routine: interrupt context */ irqreturn_t so2_kbd_interrupt_handle(int irq_no, void *dev_id) { [...] spin_lock(&lock); /* critical region - access shared resource */ [...] spin_unlock(&lock); [...] } /* Process context: disable interrupts when locking */ static void my_access(void) { unsigned long flags; spin_lock_irqsave(&lock, flags); /* critical region - access shared resource */ spin_unlock_irqrestore(&lock, flags); [...] } void my_init(void) { [...] spin_lock_init(&lock); [...] }
Funcția my_access
de mai sus rulează în context proces. Atunci când facem sincronizare dezactivăm întreruperile și folosim spinlock-ul lock
, adică funcțiile spin_lock_irqsave
și spin_unlock_irqrestore
.
În rutina de tratare a întreruperii, folosim funcțiile spin_lock
și spin_unlock
pentru acces la resursa partajată.
flags
în cazul funcțiilor spin_lock_irqsave
și spin_unlock_irqrestore
, acesta este trimis prin valoare. Funcția spin_lock_irqsave
modifică valoarea flag-ului, dar această funcție este de fapt un macro și poate folosi flag-ul trimis prin valoare.
Informații și statistici despre întreruperile din sistem pot fi găsite în /proc/interrupts
sau /proc/stat
. În fișierul /proc/interrupts
apar numai întreruperile din sistem care au câte un handler asociat:
# cat /proc/interrupts CPU0 0: 7514294 IO-APIC-edge timer 1: 4528 IO-APIC-edge i8042 6: 2 IO-APIC-edge floppy 8: 1 IO-APIC-edge rtc 9: 0 IO-APIC-level acpi 12: 2301 IO-APIC-edge i8042 15: 41 IO-APIC-edge ide1 16: 3230 IO-APIC-level ioc0 17: 1016 IO-APIC-level vmxnet ether NMI: 0 LOC: 7229438 ERR: 0 MIS: 0
În prima coloană se specifică numărul IRQ-ului asociat întreruperii; în coloana următoare se afișează numărul de întreruperi care au fost generate pentru fiecare procesor din sistem; ultimele două coloane oferă informații despre controller-ul de întreruperi și numele dispozitivului care a înregistrat un handler pentru acea întrerupere.
Fișierul /proc/stat
oferă informații despre activitatea sistemului, inclusiv numărul de întreruperi generate de la ultima boot-are a sistemului:
# cat /proc/stat | grep intr intr 7765626 7754228 4620 0 0 0 0 2 0 1 0 0 0 2377 0 0 41 3259 1098 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
Fiecare linie din fișierul /proc/stat
începe cu un text care specifică semnificația informațiilor de pe linie; pentru informații despre întreruperi, acest text este intr
. Primul număr de pe linie reprezintă numărul total de întreruperi, iar celelalte numere reprezintă numărul de întreruperi pentru fiecare IRQ, începând de la 0. Valoarea contorului reprezintă numărul de întreruperi pentru toate procesoarele din sistem.