1. Modello architetturale di Linux
• Kernel monolitico
– Esecuzione di tutte le funzionalità critiche in
kernel mode
– Modello ibrido tramite l'esecuzione di codice
critico (device driver) in user mode (fuse)
• Kernel stratificato
– Diversi livelli di chiamate a funzione (6-10)
prima di svolgere concretamente un compito
– http://www.linuxdriver.co.il/kernel_map
• Kernel modulare
– Meccanismo dei moduli per caricare a tempo di
esecuzione funzionalità secondarie
1
2. Il linguaggio C
• Sviluppato da Ken Thompson e Dennis Ritchie
all'inizio degli anni '70
• Concepito per la programmazione del kernel e
degli applicativi di sistema UNIX
• Successore del linguaggio BCPL
• Duplice scopo:
– definire un linguaggio sufficientemente ad alto
livello per poter implementare un SO senza
impazzire
– definire un linguaggio sufficientemente a basso
livello da poter permettere il colloquio con le
periferiche
2
3. Caratteristiche del linguaggio C
• Linguaggio compilato
– traduzione da alto livello (programma C) a
basso livello (codice macchina)
– compilatore
• Linguaggio estremamente compatto
– funzionalità aggiuntive fornite da librerie
esterne
• Paradigma di programmazione procedurale
– programmazione strutturata
• Linguaggio fortemente tipizzato
– impedisce operazioni non valide sui dati
3
4. Caratteristiche del linguaggio C
• Accesso non controllato, a basso livello, alla
memoria
– meccanismo dei puntatori
• Set di istruzioni minimalistico
– solo lo stretto necessario per implementare cicli,
assegnamenti, salti
• Passaggio di parametri per valore/riferimento
• Meccanismo di puntatori a funzione
– scheletro generico
– funzionalità dipendenti dall'implementazione
• Tipi di dati strutturati
– parola chiave struct
4
5. Manchevolezze del C
• Mancanza di type safety
– char -> int, int->char
– somma di interi e float produce conversioni
automatiche
• Garbage collection automatica
– meccanismo per ripulire automaticamente la
memoria allocata e non più utilizzata
• Meccanismi diretti per l'object oriented
programming
• Annidamento di funzioni
• Overloading degli operatori
– “string1” + “string2” = “string1string2” 5
6. Un primo programma in C
#include <stdio.h>
int main(void) {
printf(“Hello, worldn”);
return 0;
}
• Editate il file prova.c con il contenuto di cui sopra
• Che cosa fa questo programma?
– stampa “Hello, world” sullo STDOUT del terminale
6
7. Un primo programma in C
#include <stdio.h>
• Direttiva di preprocessamento #include
• Il preprocessore (primo strumento a toccare il
codice) esamina prova.c e sostituisce #include
<stdio.h> con il file /usr/include/stdio.h
• Il file /usr/include/stdio.h contiene le definizioni di
costanti e funzioni legate all'I/O
7
8. Un primo programma in C
#include <stdio.h>
• Le parentesi <> stanno ad indicare che stdio.h è
un file include di sistema
– distribuito con la libreria del C
– localizzato nel percorso di sistema /usr/include
8
9. Un primo programma in C
#include <stdio.h>
• Si possono anche utilizzare le virgolette “” per
includere un file include
• Le virgolette “” stanno ad indicare un file include
non di sistema
• Viene cercato nella directory corrente oppure nelle
directory specificate dall'opzione -I del compiler
9
10. Un primo programma in C
int main(void) {
}
• Definizione di una funzione (in questo caso, la
funzione main())
– int: tipo di dato ritornato dalla funzione (intero)
– main: nome della funzione
– (...): lista di argomenti forniti alla funzione
♦ void: nessun argomento
10
11. Un primo programma in C
int main(void) {
}
• Definizione di una funzione (in questo caso, la
funzione main())
– { ... }: delimitatori di inizio/fine funzione
♦ contengono la sequenza di statement (linee di
codice) C
11
12. Un primo programma in C
int main(void) {
}
• La funzione main() è speciale
– punto di ingresso dell'esecuzione del programma
– contiene lo “scheletro” del programma
– DEVE essere presente in ciascun programma
♦ altrimenti, il programma non si compila
12
13. Un primo programma in C
printf(“Hello, worldn”);
• Viene invocata la funzione di libreria printf(), con
l'argomento “Hello worldn”
• n: sequenza di escape
– si traduce in: “vai all'inizio della prossima riga”
13
14. Un primo programma in C
printf(“Hello, worldn”);
• Il valore di ritorno di printf è un intero, ma in
questo caso, non interessandoci, non viene
memorizzato in alcuna variabile
– viene scartato automaticamente
14
15. Un primo programma in C
printf(“Hello, worldn”);
• Se invece avessi voluto memorizzare il valore:
ret = printf(“Hello, worldn”);
15
16. Un primo programma in C
return 0;
• Questo è il codice di ritorno della funzione, ossia il
valore ritornato dalla stessa
16
17. Tipi di dato del C
• Caratteri:
– char: singolo carattere
• Interi
– unsigned short int: intero da 0 a 65535
– short int: intero da -32768 a +32767
– unsigned int: intero da 0 a 4294967296
– int: intero da -2147483648 a 2147483647
• Virgola mobile
– float: da 0.29 * 10-38 a 1.7 * 1038
17
18. Tipi di dato del C
• Tipi di dato enumerati:
– enum: lista di elementi “etichettati”
enum stati {
attivo = 1,
inattivo,
interrotto
}
18
19. Tipi di dato del C
• Record
– struct: tipo di dato complesso, contenente più
dati
struct esempio {
int a;
char b;
} mio_esempio;
• L'accesso ad un record avviene con
l'operatore .:
mio_esempio.a = 10;
19
20. Tipi di dato del C
• Unioni disgiunte di strutture
– union: alternativa fra 2 o più strutture
union unione {
int primo_valore;
char secondo_carattere;
struct mystruct terza_struttura;
} mia_unione;
• L'accesso ad una unione avviene con
l'operatore .:
mia_unione.primo_valore=10;
20
21. Tipi di dato del C
• Indirizzamento della memoria
– puntatori: indirizzo di una locazione di memoria
♦ byte (8 bit), word (16 bit), long word (32 bit)
• Dichiarazione di una variabile puntatore:
– int *int_ptr;
• Dereferenziazione (dereference) di un
puntatore: ottenimento del valore della
variabile a partire dall'indirizzo
– int int_var, *int_ptr; int_var = *int_ptr;
21
22. Tipi di dato del C
• Indirizzamento della memoria
– puntatori: indirizzo di una locazione di memoria
♦ byte (8 bit), word (16 bit), long word (32 bit)
• Dichiarazione di una variabile puntatore:
– int *int_ptr;
• Indirizzamento (addressing) di un puntatore:
ottenimento dell'indirizzo di una variabile a
partire dalla variabile stessa
– int int_var, *int_addr; int_addr= &int_var;
22
23. Tipi di dato del C
• Casting: conversione esplicita di un puntatore
in un altro
• Puntatore nullo: rappresenta l'indirizzo 0, e
serve ad indicare che l'indirizzo contenuto
dalla variabile puntatore è invalido
– costante NULL
23
24. Tipi di dato del C
• Aritmetica dei puntatori: a seconda della
grandezza della variabile puntata, gli operatori
+ e – assumono il seguente significato:
– operatore di somma +: incrementa l'indirizzo
puntato del numero di byte occupato dal tipo di
variabile puntata
– operatore di sottrazione -: decrementa
l'indirizzo puntato del numero di byte occupato
dal tipo di puntatore
24
25. Tipi di dato del C
• Casting a int pointer:
a= (int *) puntatore_ad_altro_tipo;
• Aritmetica: In questo caso, a viene incrementata di
4 * 4 byte (la dimensione di un int) = 16 byte
int a=10;
int *int_ptr = &a;
a=a+4;
• Aritmetica: In questo caso, a viene incrementata di
4 * 1 byte (la dimensione di un char) = 4 byte
char a='a';
int *int_ptr = &a;
a=a+4;
25
26. Tipi di dato del C
• Vettori di elementi
– array: vettore di elementi dello stesso tipo, avente
una lunghezza fissa
int a[10];
a[0] = 2;
a[1] = 3;
• Il nome di un array è anche l'indirizzo del primo
elemento
int *int_ptr = a;
• Array multidimensionali: uso di più indici
int a[10][20];
a[1][2] = 4;
26
27. Allocazione della memoria
• Tre modalità di allocazione della memoria:
– statica: attraverso array
– dinamica:
♦ prenotazione della memoria attraverso l'uso
della funzione di libreria malloc()
int *init_addr=(int *)malloc( 10 * sizeof(int));
♦ rilascio della memoria prenotata con free()
free(init_addr);
– automatica: variabili locali allocate sullo stack
dell'applicativo
27
28. Allocazione della memoria:
pregi e difetti
• Allocazione statica:
– molto veloce
– impossibile a runtime
• Allocazione dinamica:
– più lenta rispetto alla statica (overhead malloc()/
free())
– utilizzabile a runtime
• Allocazione automatica:
– molto veloce
– locale alla funzione che la contiene
28
29. Sintassi del C
• Statement: ciascun statement (singola
istruzione) deve terminare con il carattere ;
• Commenti: un commento è delimitato dalla
sequenza /* */
/* esempio di commento */
• Definizione di variabili: si fa precedere il tipo di
dato al nome della variabile
int a, b=4;
struct mystruct {
int c=4;
int *d;
} struttura = { 4, NULL };
29
30. Sintassi del C
• parola chiave static:
– applicata alle variabili locali, permette di salvare il
valore di una variabile automatica fra più chiamate
– applicata alle variabili globali, limita l'accesso alla
variabile alle sole funzioni definite nello stesso file
– applicata alle funzioni, limita l'accesso alla funzione
alle sole funzioni definite nello stesso file
static int a = 10;
static int f(int b) { return b – 2 };
• Meccanismo per proteggere funzioni private, di
implementazione, dall'uso di moduli esterni
• Le funzioni non-static costituiscono
l'interfaccia di un modulo di codice 30
31. Sintassi del C
• Nel caso in cui un programma C sia costituito
da più file, è necessario dire al compilatore che
alcune funzioni o variabili utilizzati in un file
sono definiti in altri file
• Parola chiave extern
extern int var_esterna;
• In questo esempio, la definizione della
variabile var_esterna viene cercata definita in
un altro file
– se non viene trovata alcuna definizione, il processo
di linking termina con un errore
– abusare di extern è segno di programmazione poco
pulita 31
32. Operatori più comuni
• Aritmetici:
– somma, sottrazione, moltiplicazione, divisione: +, -,
*, /
– resto modulo n: %
• Bit:
– shift a sinistra: << (divide * 2)
– shift a destra: >> (moltiplica * 2)
– AND bit a bit: &
– OR bit a bit: |
• Logici:
– AND logico: &&
– OR logico: ||
32
33. Operatori più comuni
• Assegnamento: si usa l'operatore =
int a = 10;
• Confronto
– Uguaglianza: si usa l'operatore ==
if ( a==b )
printf “a uguale a b.n”;
– Minore di: si usa l'operatore <
– Minore o uguale di: si usa l'operatore <=
– Maggiore di: si usa l'operatore >
– Maggiore o uguale di: si usa l'operatore >=
33
34. Operatori più comuni
• Alcuni esempi di operazioni
int a = 10, b=255, c;
c = a & b; /* c=10 */
c = a | b; /* c=255 */
c= a && b; /* c=1 */
c= a || b; /* c=1 */
34
35. Controllo di flusso del programma
• Ciclo while: esegue il blocco di codice specificato
fintantoché la condizione rimane vera
while ( condizione ) {
<blocco di codice>
}
• Ciclo do-while: esegue il blocco di codice
specificato fintantoché la condizione rimane vera
do {
<blocco di codice>
} while ( condizione );
• Il ciclo do-while esegue almeno una volta
35
36. Controllo di flusso del programma
• Ciclo for: esegue il blocco di codice
specificato fintantoché la condizione rimane
vera, aggiornando delle variabili nel contempo
for ( i=1; i < 10; i++ ) {
<blocco di codice>
}
for (p=list->next; p != NULL; p=p->next) {
printf(“P has %sn”, p->string;
}
36
37. Particolarità del kernel
• Il kernel è una applicazione radicalmente
diversa da quelle che girano in user mode
• Elementi di diversità del kernel:
– Caratteristiche avanzate del C
– Nessun utilizzo della libreria del C
– File include
– Nessuna protezione della memoria
– Nessun utilizzo (semplice) di virgola mobile
– Dimensione dello stack
– Portabilità
– Allineamento delle strutture dati
– Byte ordering
– Sincronizzazione e concorrenza
37
38. Caratteristiche avanzate del C
• Il kernel fa ampio uso delle caratteristiche
avanzate del C
• Alcune specificate nel primo standard del C
(standard K&R)
– K&R sta per Kernighan & Ritchie
• Altre specificate negli standard successivi
ANSI(1983), ISO/C90 (1990)
• Altre caratteristiche definite come estensioni
del compilatore GNU (gcc)
38
39. Macro
• Talvolta risulta comodo definire con un nome una
sequenza di caratteri ripetitiva
– una costante di uso frequente
– uno statement di uso frequente
• Direttiva del preprocessore #define(arg1,...,argn):
permette di definire una “macro definizione” (o
macro)
#define DBG(str) printf (”%sn”, str);
• Il compilatore, prima di compilare il programma,
sostituisce ad ogni istanza di DBG(argomento)
una istanza di printf(“%sn”, argomento);
• La sostituzione non è una generazione di codice!
39
40. Macro
• Si può controllare se una macro è definita oppure
no con le direttive del preprocessore #ifdef,
#ifndef
#ifdef CONFIG_VIDEO
stampa_16mil_colori(msg);
#else
stampa_1_colore(msg);
#endif
• Le direttive #ifdef, #ifndef sono molto comode per:
– escludere/includere codice a seconda della
configurazione
– evitare che un file include locale, incluso due volte
per errore, generi un messaggio di errore in
compilazione 40
41. Macro
• Una #define particolarmente lunga può essere
spezzata in più righe, terminando ciascuna riga
con la stringa (escape di fine riga)
#define FOO(x)
printf(“arg is %sn”, x);
do_something_useful(x);
• Le macro definite tramite la direttiva #define su più
righe è dichiarata obsoleta nei nuovi compilatori,
che consigliano l'utilizzo di:
– meccanismi di protezione delle #define
– uso delle funzioni inline
41
42. Macro
• Che cosa ha di tanto grave la seguente macro?
#define FOO(x)
printf(“arg is %sn”, x);
do_something_useful(x);
• Supponiamo che tale macro sia richiamata dal
seguente codice:
if (blah == 2)
FOO(blah);
• Che cosa fa il compilatore, prima di compilare il
programma?
42
43. Macro
• Il compilatore sostituisce
#define FOO(x)
printf(“arg is %sn”, x);
do_something_useful(x);
nel seguente codice:
if (blah == 2)
FOO(blah);
ottenendo:
if (blah == 2)
printf(“arg is %sn”, blah);
do_something_useful(blah);;
43
44. Macro
• Non notate niente di strano?
if (blah == 2)
printf(“arg is %sn”, blah);
do_something_useful(blah);;
• Ho 2 errori:
1.La macro termina con ;, l'istruzione che
richiama la macro termina con ;, ed alla fine
ho ;; (errore sintattico)
2.La macro non è protetta con un blocco di
codice { }; dopo l'espansione, se blah==2 viene
eseguita solo la printf()
44
45. Macro
• Come posso correggere questo errore?
– Incastro il codice della macro in un ciclo do { }
while(0), che viene eseguito una ed una sola
volta
#define FOO(x)
do {
printf(“arg is %sn”, x);
do_something_useful(x);
} while (0)
45
46. Macro
• Cosa risulta dall'espansione?
if (blah == 2)
do {
printf(“arg is %sn”, x);
do_something_useful(x);
} while (0);
che risulta essere codice corretto
• OSS.: la protezione di macro tramite do {} while(0);
è molto frequente nel codice del kernel di Linux
• Se proprio dovete scrivere macro multiriga,
proteggetele!
46
47. Puntatori a funzione
• Come dice il nome stesso, un puntatore a funzione
è una variabile puntatore che, se dereferenziata,
invoca una funzione
• Come si definisce una variabile puntatore a
funzione?
int (*ptr_to_function(int));
• Come si dereferenzia un puntatore a funzione?
ret_code = (*ptr_to_function) (2);
ret_code =ptr_to_function(2);
• Come si assegna una funzione ad un puntatore?
ptr_to_function=&my_func;
ptr_to_function=my_func;
47
48. Puntatori a funzione
• A cosa servono i puntatori a funzione?
– implementazione meccanismo callback
♦ un programma è progettato “per eventi” ed associa
un puntatore di funzione a ciascun evento
♦ quando si verifica l'evento, viene invocata la funzione
opportuna tramite il function pointer opportuno
♦ scheletro molto semplice: struttura dati con
associazione evento-funzione, e loop che invoca la
funzione
– polimorfismo (object oriented programming)
♦ cambiare modo di operare in funzione del tipo di dato
che viene fornito
48
49. Puntatori a funzione
• Definizione di struttura contenente puntatori a
funzione: Questa è la struttura
include/linux/fs.h: generica delle operazioni
su di un file
struct file_operations {
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t,
loff_t *);
ssize_t (*write) (struct file *, const char __user *,
size_t, loff_t *);
...
int (*setlease)(struct file *, long, struct file_lock **);
};
49
50. Puntatori a funzione
• Inizializzazione di una struttura puntatrice a
blocchi con le operazioni relative ad un
dispositivo a blocchi:
fs/block_dev.c:
struct file_operations def_blk_fops = {
.open = blkdev_open,
.release = blkdev_release,
...
.read = do_sync_read,
Qui vengono definite le
...
funzioni per un device
}; a blocchi, tramite dei
puntatori a funzione
50
51. Puntatori a funzione
• Funzione vfs_read():
fs/read_write.c:
ssize_t vfs_read(struct file *file, char __user *buf, size_t cnt,
loff_t *pos) {
ssize_t ret;
<controlli vari>;
if (file->f_op->read) {
ret = file->f_op->read(file, buf, count, pos);
}
<aggiorna contatori>
La semantica di vfs_read()
}
non dipende dal device su
cui sarà effettuata la read().
51
52. Utilizzo estensioni GCC
• Il kernel è, in larga parte, scritto in C
• Per motivi di efficienza spaziale e temporale, il
kernel fa uso massiccio di estensioni del C
– Estensioni del compilatore GNU gcc
– Estensioni dello standard C99
• Estensioni più comuni:
– Inline functions
– Inline assembly
– Asmlinkage
– Branch annotation
– Volatile
52
53. Estensioni GCC: inline functions
• Solitamente, quando una funzione viene
tradotta in linguaggio macchina, il compilatore
inserisce del codice per:
– Passare gli argomenti sullo stack
– Creare spazio per variabili locali
– Ritornare il risultato attraverso lo stack
• Quando tale funzione viene invocata, si
mettono i parametri sullo stack e si chiama
(call) la funzione
• Una funzione inline cerca di ovviare agli
aggravi computazionali legati a tale procedura
• Una funzione si dichiara inline con la parola
chiave inline 53
54. Estensioni GCC: inline functions
• Per una funzione inline, il compilatore non
genera il codice di gestione dei parametri
attraverso lo stack
• La chiamata alla funzione viene sostituita al
codice oggetto della funzione stessa
• La funzione viene eseguita più velocemente
(non c'è più il codice di gestione dello stack)
• La dimensione del codice prodotto aumenta
considerevolmente
– Per ogni chiamata, al posto della call ho l'intero
codice oggetto della funzione
• Non posso avere più definizioni identiche di
una stessa funzione inline
54
55. Estensioni GCC: inline functions
int funzione(int a) { pushl %ebp
return (a + 2); movl %esp, ebp
} subl $N, %esp
movl 8(%ebp), %eax
addl $2, %eax
leave
ret
... ...
int b = funzione(10); pushl 10
call funzione
... ...
55
56. Estensioni GCC: inline functions
Inline int funzione(int a) {
return (a + 2);
} ...
...
int b = funzione(10); movl $1, %eax
addl $2, %eax
... ...
56
57. Estensioni GCC: inline functions
• Non tutte le funzioni dichiarate inline possono
essere effettivamente sostituite con il loro
codice oggetto
– Le funzioni ricorsive
– Le funzioni che utilizzano l'indirizzo iniziale
della funzione
– Le funzioni che sono invocate prima della loro
definizione
• “inline” è un suggerimento al compilatore
– Il suggerimento può essere applicato oppure no
57
58. Estensioni GCC: inline functions
• Il comportamento di inlining può essere
ulteriormente raffinato con le parole chiave
static ed extern
• static inline:
– Il compilatore può generare oppure no il codice
oggetto per la funzione
– Se tutte le chiamate a funzione sono inlined, non
viene generato codice per la funzione, bensì viene
copiato il codice oggetto al posto della chiamata
– Se almeno una chiamata non può essere inlined,
allora viene creata una versione static della
funzione, col proprio codice oggetto
– Si possono avere più definizioni di una funzione
senza avere errori di ridefinizione
58
59. Estensioni GCC: inline functions
• Il comportamento di inlining può essere
ulteriormente raffinato con le parole chiave
static ed extern
• extern inline:
– Il compilatore non genera mai codice oggetto per la
funzione
– La funzione viene considerata alla stregua di una
macro con parametri
– Si possono avere più definizioni di una funzione
senza avere errori di ridefinizione
– Se l'inlining non può essere applicato, deve essere
presente una definizione non-inline della funzione
che possa essere invocata, altrimenti il compilatore
genera un errore di linker
59
60. Estensioni GCC: inline functions
• Quando e dove vengono utilizzate static inline
ed extern inline?
• static inline:
– Viene utilizzata per le funzioni interne, che non
si vogliono esportare pubblicamente
– Utilizzata nei file *.c e file *.h
• extern inline:
– Viene utilizzata per le funzioni pubbliche, che
rappresentano l'interfaccia con altri moduli
– interfaccia popolari definite nei file include
– Utilizzata nei file *.h
60
61. Estensioni GCC: Assembler inline
• Il GCC permette l'inserimento di istruzioni
assembler all'interno di codice scritto in C
• Utilizzato per implementare porzioni del kernel
dipendenti dall'architettura
• Si utilizza la parola chiave asm (oppure
__asm__ se asm è già definita)
• Due varianti:
– Basic inlining: inserimento di un gruppo di
istruzioni fisse, senza operandi
– Extended Asm: inserimento di un gruppo di
operazioni con passaggio di valori
61
63. Estensioni GCC: Assembler inline
• Extended asm: formato
asm( assembler template
: output operands (opzionali)
: input operands (opzionali)
: list of clobbered registers (opzionali)
);
• Extended asm: esempio
int a=10, b;
asm( quot;movl %1, %%eax;
movl %%eax, %0;quot;
: “=r”(b) /* b è l'output (%0) */
: =”r”(a) /* a è l'input (%1) */
: “%eax” ); /* %eax modificato */
63
64. Estensioni GCC: Assembler inline
• Formato operandi:
– L'operando di output viene contrassegnato con
uno %0
– Gli operandi di input vengono contrassegnati
con %1, %2, ...
• Register constraints:
– “=r(x)”: l'operando è memorizzato nel registro x
♦ x=a: %eax, x=b: %ebx, x=c: %ecx, x=d: %edx,
x=S: %esi, x=D: %edi
– “m(y)”: l'operando è memorizzato nella
locazione di memoria y
• Clobber list: specificihiamo i registri modificati
nell'operazione (il gcc non li usa per il caching)
64
65. Estensioni GCC: Asmlinkage
• Alcune funzioni sono marcate con la parola
chiave asmlinkage
• La parola chiave asmlinkage istruisce il
compilatore a non prelevare i parametri
attraverso registri, bensì solo sullo stack
• Usato nelle system call (ed in tante altre
funzioni)
65
66. Estensioni GCC: Branch annotation
• Il compilatore GNU gcc mette a disposizione
una direttiva (__builtin__expect()) che si può
applicare a funzioni
• Il kernel di Linux mette a disposizione due
macro (likely() ed unlikely()) che sfruttano in
maniera semplice tale direttiva
• Se il risultato di una espressione è quasi
sempre vero, si può ottimizzare il codice
generato in modo tale da renderlo molto veloce
per il caso frequente (vero)
– Idem se il risultato è una espressione quasi
sempre falsa
66
67. Estensioni GCC: Branch annotation
• Se foo è una espressione quasi sempre vera, si
può scrivere:
Il confronto è molto più
if (likely(foo)) {
lento del normale se
...
foo è falso
}
che risulta in un codice molto più veloce di
if (foo) {
...
}
• Analogamente, se foo è una espressione quasi
sempre falsa, si può scrivere:
Il confronto è molto più
if (unlikely(foo)) {
lento del normale se
...
foo è vero
} 67
68. Estensioni GCC: volatile
• Supponiamo che il compilatore debba
compilare queste linee di codice:
static int foo;
void bar(void) {
foo = 0;
while (foo != 255) ;
}
• La variabile foo non sembra valere mai 255
• Il compilatore vede questo fatto, e prova ad
ottimizzare le cose
– Perché mantenere quel controllo foo!=255?
– Non è equivalente a while(true)?
68
69. Estensioni GCC: volatile
• Detto fatto, il compilatore decide di
“ottimizzare” il codice in:
static int foo;
void bar(void) {
foo = 0;
while (true) ;
}
• Il codice risultante è molto più veloce (non si
deve valutare l'espressione foo != 255)
• Supponiamo che qualcuno (ad es., una
interruzione) scriva un valore nell'indirizzo di
foo
• Il valore di foo può cambiare “al di fuori” del
file C ora descritto 69
70. Estensioni GCC: volatile
• Il compilatore, preso dal desiderio di
ottimizzare, ha generato un codice non
corretto!
• Come si elimina la brama di ottimizzazione?
• Parola chiave volatile
– Dice al compilatore di non fare alcun tipo di
ottimizzazione riguardante una variabile
static volatile int foo;
void bar(void) {
foo = 0;
while (true) ;
}
70
71. Estensioni GCC: volatile
• La parola chiave volatile viene utilizzata per
proteggere variabili che possono essere
cambiate da una interruzione o da un altro
processore
• La parola chiave volatile viene anche usata
spesso in congiunzione con la parola chiave
asm (inline assembly): asm volatile
– In tal modo, vengono impedite ottimizzazioni
sulle istruzioni assembler
71
72. Nessun accesso alla libreria del C
• Le applicazioni normali vengono “fuse” con I
codici oggetto della libreria C, che provvedono
a dare le necessarie funzionalità
– Gestione file, gestione memoria, I/O
• Uno dei compiti principali del kernel è quello di
implementare tali funzionalità correttamente,
efficientemente, in maniera portabile
• Di conseguenza, il kernel non può utilizzare
alcuna funzione della libreria del C!
– Scordatevi printf(), malloc(), open(), close(),
read(), write()
72
73. Nessun accesso alla libreria del C
• In realtà, nel kernel è implementato un piccolo
insieme di funzioni “di libreria”
– printk(): analogo della funzione printf(); stampa
stringhe su una console/log
– kmalloc(): analogo della funzione malloc();
alloca memoria dinamicamente
– kfree(): analogo della funzione free(); libera
memoria dinamicamente
– Str...(): anloghe delle funzioni str...(); gestione di
stringhe
• Per una descrizione non esaustiva dell'API di
Linux, si consulti:
– http://www.gelato.unsw.edu.au/~dsw/public-
files/kernel-docs/kernel-api/ 73
74. File include
• I file include (detti anche header) sono utilizzati
come meccanismo di definizione dell'API
– strutture dati
– prototipi di funzioni (API)
• La libreria del C definisce la propria API tramite
i suoi file include
– directory /usr/include
• Il kernel definisce la propria API tramite I suoi
file include
– sottodirectory include di un qualunque albero
sorgente del kernel
74
75. File include
• La libreria del C invoca le chiamate di sistema del
kernel, passando gli opportuni parametri e
ricevendo gli opportuni valori di ritorno
• Il kernel deve conoscere il formato sia delle
strutture dati passate come parametri, sia delle
strutture dati passate come valore di ritorno
• D'altro canto, il kernel implementa una sua API
interna (non utilizzabile dagli applicativi user
mode) per ciascun strato interno
• Due API diverse definite nel kernel:
– API interna: strutture dati e prototipi delle funzioni
usate negli strati interni del kernel
– API di raccordo: strutture dati e prototipi delle
funzioni usate dalla libreria del C
75
76. File include
• L'API di raccordo del kernel deve essere
coerente con l'API definita dalla libreria del C
– Condizione solitamente garantita dal gestore
dei pacchetti del sistema
– Durante il processo di compilazione della
libreria del C, viene fatto esplicito riferimento ai
file include del kernel (API di raccordo)
– Se si compila il kernel a mano, bisogna stare
attenti alla compatibilità fra le due API!
– Non è necessario che le due versioni dei file
include (libreria C e kernel) siano identiche
76
77. File include
• La libreria del C cerca di essere compatibile
all'indietro con l'API di raccordo del kernel
– Una libreria C compilata con una data versione
dei file include del kernel funziona con tutti i
kernel aventi file include “più vecchi”
– Se si compila una libreria del C con file include
del kernel vecchi e poi si usa un kernel con file
include più recenti, le nuove funzionalità
introdotte a livello di kernel potrebbero non
essere supportate (o, peggio, introdurre bachi)
• Si consulti la FAQ della libreria del C per
ulteriori approfondimenti:
– http://www.gnu.org/software/libc/FAQ.html
77
78. File include
• Solitamente, la libreria del C messa a
disposizione dalla distribuzioni più popolari è
compilata con i file include di un kernel
abbastanza recente
• Il problema della compatibilità non dovrebbe
porsi anche se si compila l'ultimissima
versione del kernel
78
79. File include
• Un file include del kernel può essere incluso
da molteplici file
• Una inclusione doppia può portare a
ridefinizioni di variabili/funzioni, con
conseguente errore da parte del compilatore
• I file include del kernel sono inclusi una sola
volta tramite l'utilizzo di una costante
#ifndef COSTANTE
#define COSTANTE 1
...
#endif
79
80. Nessuna protezione della memoria
• Quando una applicazione in user mode accede
ad indirizzo invalido, il kernel intercetta tale
errore ed uccide il processo relativo
• Il kernel non ha un meccanismo analogo!
• Un accesso ad una area di memoria illegale (ad
es., l'indirizzo 0x00000000) causa un blocco
istantaneo ed irreversibile del sistema (panic)
• La memoria del kernel non può essere gestita
in modalità virtuale, per motivi di efficienza
– Niente swap, solo memoria fisica
– Occorre allocare solo lo stretto necessario
80
81. Nessun (semplice) uso di floating point
• L'uso di aritmetica in virgola mobile (floating
point) richiede l'uso di registri speciali
• Tali registri non vengono salvati durante un
cambio di contesto, per motivi di efficienza
• Non si può usare artimetica floating point in
punti nei quali lo scheduler oppure una
interruzione ci possono interrompere!
• Morale: non usiamo aritmetica floating point
nel kernel!
81
82. Dimensione dello stack
• Gli applicativi eseguiti in user mode hanno uno
stack di dimensione variabile
• Lo stack del kernel è piccolo e di dimensione
fissa
– 4KB su architetture a 32 bit
– 8KB su architetture a 64 bit
• Non si può sprecare tale stack con:
– chiamate ricorsive
– Passaggio di strutture grandi (meglio passare
puntatori a tali strutture)
– Ritorno di strutture grandi (meglio passare
puntatori a tali strutture)
– Variabili locali di grandi dimensioni 82
83. Portabilità
• Il kernel è scritto per poter essere eseguito su
una miriade di architetture diverse
• Architetture diverse hanno formati e
dimensioni diverse di rappresentazione dei
dati
– Architetture a 16, 32, 64 bit
– Alcuni tipi di dato (int, long) possono avere
dimensioni diverse
• La parte architecture-independent del kernel
deve essere scritta in modo tale che funzioni
su ciascuna di tali architetture
– Non si possono fare assunzioni sulle
dimensioni dei tipi di dato
83
84. Portabilità
• Si definisce “parola” la quantità di dati che una
architettura è in grado di elaborare in un colpo
di clock
• Una architettura a n bit è una architettura in cui
la parola ha dimensione n bit
– I registri general purpose hanno la stessa
dimensione di una parola
– La dimensione del bus di memoria ha almeno la
stessa dimensione di una parola
– Per le architetture supportate da Linux,
l'”almeno” si traduce in “uguale”
– Ne consegue che la dimensione di un puntatore
è pari alla dimensione della parola
– Il tipo di dato long è equivalente alla parola 84
85. Portabilità
• Un esempio: nelle architetture a 64 bit
– Registri, puntatori, long sono a 64 bit
– Il tipo di dato int è a 32 bit
– puntatore != intero (come invece è nelle
architetture a 32 bit)
• Sarebbe un grosso errore supporre, in
generale, puntatori di 32 bit
• Il kernel definisce il numero di bit in un tipo di
dato long:
– macro BITS_PER_LONG, in <asm/types.h>
85
86. Portabilità
• Alcune regole da ricordare quando si usano i
tipi di dato
– Il tipo di dato char è sempre lungo 8 bit
– Il tipo di dato int vale 32 bit su tutte le
architetture supportate da Linux
– Il tipo di dato short vale 16 bit su tutte le
architetture supportate da Linux
– Mai fare assunzioni sulla lunghezza di long o di
un puntatore
– Mai assumere che int e long abbiano la stessa
lunghezza
– Mai assumere che int e puntatore abbiano la
stessa lunghezza
86
87. Portabilità
• Il kernel fa ampio uso di tipi di dato opachi
• Un tipo di dato opaco è un tipo di dato
generico definito tramite l'operatore C typedef
• Architetture diverse possono definire il tipo di
dato opaco in modi diversi
• Obiettivi:
– Permettere dimensioni più grandi su
architetture più potenti (es.: offset di seek dei
file)
– Forzare un tipo di dato ad avere la stessa
dimensione su tutte le architetture
(es.: indirizzi IP)
87
88. Portabilità
• Il kernel fa ampio uso di tipi di dato espliciti
• Un tipo di dato esplicito è l'esatto contrario di
un tipo di dato opaco
• Il tipo di dato esplicito è definito tramite
l'operatore typedef
– File include/asm/types.h
• Il tipo di dato esplicito ha sempre la stessa
dimensione, su tutte le architetture
• Alcuni esempi:
– s8, u8: signed/unsigned 16 bit integer
– s16, u16: signed/unsigned 16 bit integer
– s32, u32: signed/unsigned 32 bit integer
– s64, u64: signed/unsigned 64 bit integer 88
89. Allineamento
• L'allineamento è il posizionamento di una
variabile in un indirizzo multiplo della sua
dimensione
• Esempio: un tipo di dato a 32 bit è allineato se
è memorizzato in un indirizzo di memoria
multiplo di 4 (gli ultimi 2 bit sono a zero)
• In generale, un tipo di dato di 2n byte è
allineato se è memorizzato in un indirizzo di
memoria avente gli ultimi n bit impostati a 0
• In alcuni sistemi (Intel), l'accesso a dati non
allineati è possibile ma lento
• In altri sistemi (RISC), l'accesso a dati non
allineati provoca una eccezione
89
90. Allineamento
• Tutte le strutture dati devono essere allineate
• Di solito, il compilatore pensa ad allineare
automaticamente le strutture dati
– Gli array sono allineati automaticamente ad un
multiplo della dimensione del tipo
– Le union sono allineate al tipo di dato più
grande incluso
– Le strutture sono allineate al tipo di dato più
grande incluso
– Le strutture dati sono soggette a padding
90
91. Allineamento: padding delle strutture
• Padding: gli elementi di una struttura dati sono
allineati
• Consideriamo la seguente struttura su una
macchina a 32 bit:
struct foo_struct {
char dog; /* 1 byte */
unsigned long cat; /* 4 byte */
unsigned short pig; /* 2 byte */
char fox; /* 1 byte */
};
• Il compilatore inserisce degli zeri in modo tale
da allineare ciascun elemento a 4 byte
91
92. Allineamento: padding delle strutture
• Padding: gli elementi di una struttura dati sono
allineati
• Ecco la struttura dati in memoria risultante dal
processo di padding:
struct foo_struct {
char dog; /* 1 byte */
4 byte
u8 __pad0[3]; /* 3 byte */
unsigned long cat; /* 4 byte */
unsigned short pig; /* 2 byte */
char fox; /* 1 byte */ 4 byte
u8 __pad1; /* 1 byte */
};
92
93. Byte ordering
• Il byte ordering definisce il posizionamento dei
byte all'interno di una parola
• Due possibili ordinamenti:
– Big endian: il byte più significativo della parola
è memorizzato per primo
– Little endian: il byte più significativo della
parola è memorizzato per ultimo
Ordinamento Ordinamento
Big Endian Little Endian
Byte Byte
0 1 2 3 3 2 1 0
Più Meno Meno Più
significativo93
significativo significativo significativo
94. Byte ordering
• Curiosità: I nome Big Endian e Little Endian
vengono dal romanzo “I viaggi di Gulliver” di
Jonathan Swift
• Architetture Big Endian:
– PowerPC, System/370, SPARC
• Architetture Little Endian:
– 6502, z80, x86, VAX, PDP-11
94
95. Byte ordering
• Un esempio: memorizziamo il numero 1027 nei
formati Big Endian e Little Endian
1027 in binario: 00000000 00000000 00000100 00000011
Indirizzo Big Endian Little Endian
0 00000000 00000011
1 00000000 00000100
2 00000100 00000000
3 00000011 00000000
95
96. Sincronizzazione e concorrenza
• Il kernel è un software interrompibile in
maniera sincrona ed asincrona
– Sincrona: scadenza quanto di tempo, eccezioni
di vario genere
– Asincrona: interruzioni
• Che cosa succede se il codice eseguito in
seguito all'interruzione modifica il codice che
stava eseguendo precedentemente?
– Inconsistenza dei dati
– Crash
96
97. Sincronizzazione e concorrenza
• Il kernel è un software che può gestire
l'esecuzione concorrente di più processi
• I processi possono condividere informazioni,
strutture dati, altre risorse
• Che cosa succede se le risorse possono
essere accedute senza alcun meccanismo di
protezione/arbitrio/accesso?
– Inconsistenza dei dati
– Crash
97
98. Contesti di esecuzione
• In ogni istante, il processore può trovarsi in
una di queste condizioni
– User mode: esecuzione di codice di un
processo in user mode
– Kernel mode: esecuzione di codice di un
processo in kernel mode (system call)
– Gestione interruzioni: esecuzione di codice
legato ad una interruzione hardware, per conto
di nessun processo
98
99. Contesti di esecuzione
Una applicazione vuole
leggere un blocco già
Applicazione bufferizzato del disco
User space
Kernel space
Hardware
99
100. Contesti di esecuzione
Il processo invoca la fread(),
che a sua volta invoca la vfs_read()
Applicazione
User space
n=fread(...) Kernel space
Lista blocchi
bufferizzati
vfs_read()
Hardware
100
101. Contesti di esecuzione
Ad un certo punto, arriva una interruzione,
in seguito alla quale un blocco vuole
Applicazione essere inserito nella posizione da cui
vuole leggere il processo
User space
n=fread(...) Kernel space
Lista blocchi
bufferizzati
vfs_read()
Hardware
101
102. Contesti di esecuzione
Si ha un accesso simultaneo alla stessa
struttura dati (corsa critica)
Applicazione
User space
n=fread(...) Kernel space
Lista blocchi
bufferizzati
vfs_read()
Hardware
102
103. Contesti di esecuzione
• La corsa critica, nella “migliore” delle ipotesi,
riesce a sovrascrivere velocemente il buffer,
facendo leggere fischi per fiaschi al processo
• Nella peggiore delle ipotesi, l'interruzione
arriva nel momento in cui il processo stava
estraendo il buffer dalla lista
– Il gestore delle interruzioni si trova una lista
mozzata a metà
– Viene provata la scansione della lista
– Ad un certo punto si prova a saltare ad un
elemento NULL della lista
– Accesso illegale -> la macchina si pianta
103
104. Contesti di esecuzione
• Questo problema può essere impedito tramite
due meccanismi
– Un gestore veloce di interruzioni, che opera su
un buffer locale, e successivamente copia il
buffer locale nel buffer condiviso coi processi
♦ Upper half/bottom half
– Un meccanismo di prenotazione/rilascio delle
strutture dati (non del codice!)
♦ Locking/semafori
104
105. Contesti di esecuzione
Una applicazione vuole
leggere un blocco già
Applicazione bufferizzato del disco
User space
Kernel space
Hardware
105
106. Contesti di esecuzione
Il processo invoca la fread(),
che a sua volta invoca la vfs_read()
Applicazione La vfs_read() blocca l'uso della
struttura dati “lista”
User space
n=fread(...) Kernel space
Lista blocchi
bufferizzati
vfs_read()
Hardware
106
107. Contesti di esecuzione
Ad un certo punto, arriva una interruzione,
che provoca l'inserimento di un blocco
Applicazione in una lista locale
User space
n=fread(...) Kernel space
Lista blocchi
bufferizzati
vfs_read()
Buffer associato UPPER
al gestore IRQ HALF
Hardware
107
108. Contesti di esecuzione
Successivamente, una procedura
dilazionata nel tempo prova ad inserire
Applicazione l'elemento dal buffer locale al buffer
dei blocchi
User space
n=fread(...) Kernel space
Lista blocchi
bufferizzati
vfs_read()
BOTTOM
HALF
Buffer associato
al gestore IRQ
Hardware
108
109. Contesti di esecuzione
vfs_read() rilascia la struttura dati
Applicazione
User space
n=fread(...) Kernel space
Lista blocchi
bufferizzati
vfs_read()
Hardware
109
110. Contesti di esecuzione
Viene immediatamente preso il
blocco da parte del gestore di
Applicazione interruzioni dilazionato
User space
Kernel space
Lista blocchi
bufferizzati
vfs_read()
Hardware
110
111. Contesti di esecuzione
Avviene l'inserimento
dell'elemento nella lista
Applicazione
User space
Kernel space
Lista blocchi
bufferizzati
vfs_read()
Hardware
111
112. Contesti di esecuzione
Il gestore dilazionato rilascia
il blocco sulla lista
Applicazione
User space
Kernel space
Lista blocchi
bufferizzati
vfs_read()
Hardware
112
113. Anatomia di una funzione del kernel
• Una funzione del kernel segue uno scheletro
ben preciso
– Dal punto di vista della forma (commenti,
indentazione, coding style in generale)
– Dal punto di vista strutturale (prologo,
prenotazione/rilascio risorse, corpo, epilogo,
gestione degli errori)
113
114. Funzione del kernel: Forma
• Definita nel file Documentation/CodingStyle
• Obiettivi: fare in modo che una funzione sia
leggibile e ben commentata
• Indentazione:
– 8 spazi (tab) per riconoscere agevolmente I
blocchi di controllo
– Limitazione del numero di livelli di indentazione
• Larghezza delle linee di codice:
– Non dovrebbe mai superare gli 80 caratteri
– Entra in uno schermo
• Lunghezza delle funzioni:
– Mai più di 1-2 schermate video
114
115. Funzione del kernel: Forma
• Commenti:
– Solo prima dell'intestazione di una funzione
– Mai commentare i singoli statement, a meno
che non sia necessario
• Piazzamento delle parentesi:
– Stile Kernighan & Richie: il blocco inizia sulla
stessa linea dello statement
if ( x is true ) {
do y
}
• Nomi delle variabili:
– I più semplici ed intuitivi possibili
– ThisVarIsATempCounter: NO, cnt: SI
115
116. Funzione del kernel: Struttura
• Quanto segue è soggetto a 1000 eccezioni! Non
prenderlo come regola aurea!
• In generale, una funzione del kernel ha il seguente
scheletro:
int funzione(int, char *, ...) {
error = ERROR_CODE;
if ( !valid_check)
goto out;
prenota_risorsa();
if ( !valid_thing )
goto out_rilascia;
...
ret = 0;
out_rilascia:
rilascia_risorsa();
out:
return error;
116
}
117. Funzione del kernel: Struttura
• Le funzioni ritornano, solitamente, un valore < 0
oppure un puntatore NULL per indicare un errore
oppure l'incapacità di svolgere il compito
assegnato
• La prima fase della funzione consiste in alcuni
controlli generici; se i controlli falliscono, si esce
direttamente
• Successivamente, si prenota una risorsa e si
cerca di portare a termine il compito richiesto; se
si fallisce, si salta al percorso di uscita che
sblocca la risorsa
• Se tutto va bene, si imposta un valore di uscita
opportuno e si segue l'intero percorso di uscita
(che sblocca le risorse e ritorna il valore)
117
118. Gestione delle liste
• Una lista concatenata è una struttura dati che
collega un certo numero di elementi (nodi)
• Diverse implementazioni:
– Lista statica (array)
– Lista dinamica (collegamento puntatori)
• Il kernel fa ampio uso di liste dinamiche
– Le dimensioni delle liste sono spesso ignote
– Si può ottimizzare il consumo di memoria
• Diverse tipologie di collegamento:
– Collegamento singolo
– Collegamento doppio
– Collegamento circolare
118
119. Gestione delle liste
... next ... next ... next null
Lista a collegamento singolo
null prev ... next prev ... next prev ... next null
Lista a collegamento doppio
119
120. Gestione delle liste
... next ... next ... next
Lista circolare a collegamento singolo
prev ... next prev ... next prev ... next
Lista circolare a collegamento doppio
120
121. Gestione delle liste
• Il problema principale di tale struttura è che
risulta essere definita insieme al tipo di dato
che essa ospita
struct my_list {
void *my_item;
struct my_list *next;
struct my_list *prev;
}
• E' impossibile astrarre la definizione di lista in
una libreria
• Ogni struttura dati ha la sua libreria per la
gestione delle liste
• Duplicazione inutile di codice, con tutti gli
svantaggi che ne derivano 121
122. Gestione delle liste
• Il kernel di Linux mette a disposizione una
funzionalità generica di liste, indipendente dal
tipo di dato (type oblivious)
– Codice riutilizzabile per qualunque tipo di dato/
struttura
• Definizione in include/linux/list.h
• Implementazione di lista doppia concatenata
• Utilizzo di un primo elemento sentinella
(dummy)
– Non rappresenta un elemento
– Serve a collegare primo ed ultimo elemento
122
123. Gestione delle liste
• L'elemento chiave delle liste è una
concatenatore (struct list_head), definito come
una coppia di puntatori al prossimo
concatenatore ed precedente concatenatore:
struct list_head {
struct list_head *next;
struct list_head *prev;
}
• Come potete vedere, non c'è alcun riferimento
diretto ad alcun tipo di struttura dati
– Solo puntatori in avanti ed indietro, sempre ad
un'altro concatenatore di lista
• Dove $#%?!&#*! sono i dati?
123
124. Gestione delle liste
• Un passo alla volta :-)
• Il concatenatore viene inserito all'interno della
struttura dati che si vuole concatenare
struct kool_list {
int to;
struct list_head list;
int from;
} my_kool_list;
• Posso inserire il concatenatore dove voglio,
all'interno della struttura kool_list
• Posso chiamare il concatenatore come mi pare
(list, lista, pinco pallino)
• Posso avere multipli concatenatori
124
125. Gestione delle liste
struct struct
kool_list kool_list
int to; int to;
struct list_head struct list_head struct list_head
list; list; list;
Int from; Int from;
Elemento Elemento Elemento
sentinella lista lista
125
126. Gestione delle liste: operazioni
• Come posso utilizzare la mia lista nei
programmi?
• Devo definire meccanismi per:
– Inizializzare la lista
– Aggiungere elementi alla lista
– Cancellare elementi dalla lista
– Controllare se la lista è vuota
– Estrarre, dal concatenatore, l'elemento (qui
avviene una vera e propria magia)
– Scorrere una lista
126
127. Gestione delle liste: inizializzazione
• Avviene tramite l'utilizzo di apposite macro
• Macro diverse a seconda di dove, nel codice,
effettuo l'inizializzazione
• Inizializzazione tramite comando:
– INIT_LIST_HEAD(&my_kool_list.list);
– Vuole un puntatore al concatenatore
– INIT_LIST_HEAD(list) effettua le operazioni:
list->next = list;
list->prev = list;
127
128. Gestione delle liste: inizializzazione
• Avviene tramite l'utilizzo di apposite macro
• Macro diverse a seconda di dove, nel codice,
effettuo l'inizializzazione
• Inizializzazione tramite assegnazione:
– Vuole una variabile concatenatore (non
puntatore!)
– LIST_HEAD_init(list_head_var);
struct kool_list my_kool_list = {
.to = ...,
.list = LIST_HEAD_INIT(list_head_var)
.from = ...
}
– LIST_HEAD_INIT(name) si espande in:
{ &(name), &(name) }
128
129. Gestione delle liste: inizializzazione
• Avviene tramite l'utilizzo di apposite macro
• Macro diverse a seconda di dove, nel codice,
effettuo l'inizializzazione
• Inizializzazione tramite assegnazione
automatica:
– Vuole un nome di variabile
– Alloca un concatenatore con quel nome
– LIST_HEAD(my_kool_list);
– LIST_HEAD(name) si espande in:
struct list_head_name = LIST_HEAD_INIT( name )
129
131. Gestione delle liste: inserimento
• Si utilizza la funzione list_add()
– list_add(struct list_head *new, struct list_head *
head)
– new è il puntatore al concatenatore
dell'elemento che voglio inserire
– head è il puntatore al concatenatore sentinella
della lista
– Viene inserito l'elemento in cima alla lista
131
132. Gestione delle liste: inserimento
struct
kool_list
int to;
struct list_head struct list_head
list; list;
Int from;
Elemento Elemento
sentinella lista
132
133. Gestione delle liste: inserimento
struct
kool_list
int to;
struct list_head struct list_head
list; list;
struct
Int from;
kool_list
int to;
Elemento Elemento
struct list_head
list;
sentinella lista
Int from;
Elemento
da aggiungere
in testa alla lista 133
134. Gestione delle liste: cancellazione
• Si utilizza la funzione list_del()
– list_del(struct list_head *entry)
– entry è il puntatore al concatenatore
dell'elemento che voglio rimuovere
– Il concatenatore viene staccato dalla lista
– Non viene distrutto l'elemento puntato dal
concatenatore!
134
135. Gestione delle liste: cancellazione
struct struct
kool_list kool_list
int to; int to;
struct list_head struct list_head struct list_head
list; list; list;
Int from; Int from;
Elemento Elemento Elemento
sentinella da rimuovere lista
135
136. Gestione delle liste: cancellazione
struct
kool_list
int to;
struct list_head struct list_head
list; list;
Int from;
struct
kool_list
Elemento Elemento
int to;
sentinella lista
struct list_head
list;
=0
prev
Int from;
next
Elemento =0
rimosso 136
137. Gestione delle liste: lista vuota
• Si utilizza la funzione list_empty()
– int list_empty(struct list_head *head)
– head è il puntatore al concatenatore sentinella
della lista che voglio controllare
– Ritorna 1 se la lista è vuota, 0 altrimenti
137
138. Gestione delle liste: elemento
• Si utilizza la macro list_entry()
– list_entry(ptr, type, member)
– ptr è il puntatore al concatenatore dell'elemento
– type è il tipo di dato dell'elemento (struct ...)
– member è il nome della variabile concatenatore
all'interno dell'elemento
– struct my_kool_list *element =
list_entry( &my_kool_list.list,
struct kool_list, list );
• Fa uso della macro container_of (include/linux/
kernel.h)
• Si basa su un trucco ben noto:
– ftp://rtfm.mit.edu/pub/usenet-by-
group/news.answers/C-faq/faq (2.14) 138
141. Gestione delle liste: elemento
• Macro container_of(ptr, type, member):
#define container_of(ptr, type, member) ({
const typeof( ((type *)0)->member ) *__mptr = (ptr);
(type *)( (char *)__mptr - offsetof(type,member) );
})
Indirizzo struct kool_list { Prendo l'indirizzo 0 e lo
1000 int to; vedo (cast) come puntatore
1004 struct list_head list; alla struttura kool_list.
1012 int from; Posso farlo perché non sto
1016 } my_kool_list; scrivendo in memoria.
141
142. Gestione delle liste: elemento
• Macro container_of(ptr, type, member):
#define container_of(ptr, type, member) ({
const typeof( ((type *)0)->member ) *__mptr = (ptr);
(type *)( (char *)__mptr - offsetof(type,member) );
})
Indirizzo struct kool_list { Con la dereferenziazione
1000 int to; (->)ottengo una variabile
1004 struct list_head list; di tipo “member” allo
1012 int from; indirizzo 4 (parto da 0!).
1016 } my_kool_list; Non la sto scrivendo; il
sistema non crasha.
142
143. Gestione delle liste: elemento
• Macro container_of(ptr, type, member):
#define container_of(ptr, type, member) ({
const typeof( ((type *)0)->member ) *__mptr = (ptr);
(type *)( (char *)__mptr - offsetof(type,member) );
})
Indirizzo struct kool_list { Quale è il tipo della
1000 int to; variabile di tipo member
1004 struct list_head list; che ho appena derefe-
1012 int from; renziato? Ovviamente,
1016 } my_kool_list; member!
143
144. Gestione delle liste: elemento
• Macro container_of(ptr, type, member):
#define container_of(ptr, type, member) ({
const typeof( ((type *)0)->member ) *__mptr = (ptr);
(type *)( (char *)__mptr - offsetof(type,member) );
})
Indirizzo struct kool_list { Definiamo una variabile
1000 int to; puntatore di tipo member,
1004 struct list_head list; di nome __mptr. Il doppio
1012 int from; underscore “__” si usa
1016 } my_kool_list; per nomi di variabili
interne (per convenzione).
144
145. Gestione delle liste: elemento
• Macro container_of(ptr, type, member):
#define container_of(ptr, type, member) ({
const typeof( ((type *)0)->member ) *__mptr = (ptr);
(type *)( (char *)__mptr - offsetof(type,member) );
})
Indirizzo struct kool_list { Const indica che il
1000 int to; puntatore punta ad una
1004 struct list_head list; area di memoria che
1012 int from; non deve essere mai
1016 } my_kool_list; modificata. Se provo a
scriverci, il compilatore
emette un errore.
E' una misura di protezione
contro codice che scriva
il contenuto di __mptr
accidentalmente.
145
146. Gestione delle liste: elemento
• Macro container_of(ptr, type, member):
#define container_of(ptr, type, member) ({
const typeof( ((type *)0)->member ) *__mptr = (ptr);
(type *)( (char *)__mptr - offsetof(type,member) );
})
Indirizzo struct kool_list { Assegniamo il puntatore
1000 int to; __mptr di tipo member,
1004 struct list_head list; protetto contro la scrittura
1012 int from; accidentale, al valore ptr,
1016 } my_kool_list; che rappresenta l'indirizzo
iniziale di list.
146
147. Gestione delle liste: elemento
• Macro container_of(ptr, type, member):
#define container_of(ptr, type, member) ({
const typeof( ((type *)0)->member ) *__mptr = (ptr);
(type *)( (char *)__mptr - offsetof(type,member) );
})
Indirizzo struct kool_list { Prendiamo il puntatore
1000 int to; __mptr e vediamolo (cast)
1004 struct list_head list; come un puntatore a
1012 int from; caratteri. In questo modo,
1016 } my_kool_list; l'aritmetica sui puntatori
diventa l'aritmetica sui
byte.
147
148. Gestione delle liste: elemento
• Macro container_of(ptr, type, member):
#define container_of(ptr, type, member) ({
const typeof( ((type *)0)->member ) *__mptr = (ptr);
(type *)( (char *)__mptr - offsetof(type,member) );
})
Indirizzo struct kool_list { L'operatore offsetof
1000 int to; calcola l'offset (in byte)
1004 struct list_head list; di member all'interno
1012 int from; della struttura kool_list
1016 } my_kool_list; (4 byte).
148
149. Gestione delle liste: elemento
• Macro container_of(ptr, type, member):
#define container_of(ptr, type, member) ({
const typeof( ((type *)0)->member ) *__mptr = (ptr);
(type *)( (char *)__mptr - offsetof(type,member) );
})
Indirizzo struct kool_list { La differenza dei due
1000 int to; valori (indirizzo di list,
1004 struct list_head list; offset di list all'interno
1012 int from; di kool_list), interpretata
1016 } my_kool_list; in aritmetica dei char *,
mi fornisce il valore
richiesto, ossia
l'indirizzo iniziale di
my_kool_list.
149
150. Gestione delle liste: elemento
• Macro container_of(ptr, type, member):
#define container_of(ptr, type, member) ({
const typeof( ((type *)0)->member ) *__mptr = (ptr);
(type *)( (char *)__mptr - offsetof(type,member) );
})
Indirizzo struct kool_list { L'indirizzo finale viene
1000 int to; convertito ad un puntatore
1004 struct list_head list; di tipo “type” (kool_list).
1012 int from;
1016 } my_kool_list;
150
151. Gestione delle liste: scorrimento
• Si utilizza la macro list_for_each()
– list_for_each(pos, head)
– pos è un puntatore a concatenatore generico,
contiene i vari concatenatori della lista
– head è il puntatore al concatenatore sentinella
della lista
– Viene scorso l'elenco dei concatenatori di una
lista
struct list_head *pos;
struct kool_list my_kool_list;
list_for_each(pos, &my_kool_list.list) {
tmp = list_entry( pos, struct kool_list, list );
...
}
151
152. Gestione delle liste: scorrimento
struct
kool_list
...
struct list_head
list;
pos: pos: pos:
tmp: tmp: tmp:
Elemento Prima Seconda Ultima
sentinella iterazione iterazione iterazione
152
153. Gestione delle liste: scorrimento
• Variante list_for_each_entry()
– list_for_each_entry(pos, head, member)
– pos è un puntatore al tipo di dato
– head è il puntatore al concatenatore sentinella
della lista
– member il nome del concatenatore sentinella
nella lista
– Scorre la lista assegnando a pos i puntatori
degli elementi
struct kool_list pos, my_kool_list;
list_for_each_entry(pos, &my_kool_list.list, list) {
# pos punta ad un elemento di tipo kool_list
...
}
153
154. Gestione delle liste: scorrimento
• Variante list_for_each_entry()
– list_for_each_entry(pos, head, member)
– pos è un puntatore al tipo di dato
– head è il puntatore al concatenatore sentinella
della lista
– member il nome del concatenatore sentinella
nella lista
– Scorre la lista assegnando a pos i puntatori
degli elementi
struct kool_list pos, my_kool_list;
list_for_each(pos, &my_kool_list.list, list) {
# pos punta ad un elemento di tipo kool_list
...
}
154
155. Gestione delle liste: scorrimento
• Variante list_for_each_safe()
– list_for_each_safe(pos, n, head)
– pos è un puntatore a concatenatore generico,
contiene i vari concatenatori della lista
– n un puntatore a concatenatore generico, viene
usato come variabile di appoggio
– head il nome del concatenatore sentinella nella
lista
– Scorre la lista in modalità sicura per permettere
la cancellazione di elementi
list_for_each_safe(pos, q, &my_kool_list.list) {
tmp = list_entry(pos, struct kool_list, list);
list_del(pos);
kfree(pos); /* o free(pos) */
} 155