Gapil

Scarica in formato pdf o txt
Scarica in formato pdf o txt
Sei sulla pagina 1di 661

GaPiL

Guida alla Programmazione in Linux


Simone Piccardi 8 novembre 2007

ii Copyright c 2000-2007 Simone Piccardi. Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.1 or any later version published by the Free Software Foundation; with the Invariant Sections being Un preambolo in Prefazione, with no FrontCover Texts, and with no Back-Cover Texts. A copy of the license is included in the section entitled GNU Free Documentation License.

Indice
Un preambolo Prefazione xiii xv

Programmazione di sistema
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

1
3 3 3 4 5 6 7 7 8 8 9 10 10 10 11 13 13 13 14 14 15 16 16 16 17 19 23 25 25 26 27 28

1 Larchitettura del sistema 1.1 Una panoramica . . . . . . . . . . . . . . . . . . . . . . 1.1.1 Concetti base . . . . . . . . . . . . . . . . . . . . 1.1.2 Il kernel e il sistema . . . . . . . . . . . . . . . . 1.1.3 Chiamate al sistema e librerie di funzioni . . . . 1.1.4 Un sistema multiutente . . . . . . . . . . . . . . 1.2 Gli standard . . . . . . . . . . . . . . . . . . . . . . . . . 1.2.1 Lo standard ANSI C . . . . . . . . . . . . . . . . 1.2.2 I tipi di dati primitivi . . . . . . . . . . . . . . . 1.2.3 Lo standard IEEE POSIX . . . . . . . . . . . . 1.2.4 Lo standard X/Open . . . . . . . . . . . . . . . . 1.2.5 Gli standard Unix . . . . . . . . . . . . . . . . . 1.2.6 Lo standard BSD . . . . . . . . . . . . . . . . . 1.2.7 Lo standard System V . . . . . . . . . . . . . . . 1.2.8 Il comportamento standard del gcc e delle glibc 2 Linterfaccia base con i processi 2.1 Esecuzione e conclusione di un programma . . . . . 2.1.1 La funzione main . . . . . . . . . . . . . . . . 2.1.2 Come chiudere un programma . . . . . . . . 2.1.3 Le funzioni exit e _exit . . . . . . . . . . . 2.1.4 Le funzioni atexit e on_exit . . . . . . . . . 2.1.5 Conclusioni . . . . . . . . . . . . . . . . . . . 2.2 I processi e luso della memoria . . . . . . . . . . . . 2.2.1 I concetti generali . . . . . . . . . . . . . . . 2.2.2 La struttura della memoria di un processo . . 2.2.3 Allocazione della memoria per i programmi C 2.2.4 Il controllo della memoria virtuale . . . . . . 2.3 Argomenti, opzioni ed ambiente di un processo . . . 2.3.1 Il formato degli argomenti . . . . . . . . . . . 2.3.2 La gestione delle opzioni . . . . . . . . . . . . 2.3.3 Opzioni in formato esteso . . . . . . . . . . . 2.3.4 Le variabili di ambiente . . . . . . . . . . . . iii

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

iv 2.4 Problematiche di programmazione generica . . . . . . . 2.4.1 Il passaggio delle variabili e dei valori di ritorno . 2.4.2 Il passaggio di un numero variabile di argomenti 2.4.3 Potenziali problemi con le variabili automatiche . 2.4.4 Il controllo di usso non locale . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

INDICE . . . . . . . . . . 30 30 31 33 33 37 37 37 39 40 40 41 46 48 53 56 56 58 61 63 71 71 72 74 77 80 80 80 81 83 83 83 84 85 86 86 88 89 91 93 93 93 95 96 99 100 101 106

3 La gestione dei processi 3.1 Introduzione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.1.1 Larchitettura della gestione dei processi . . . . . . . . . . . . . . . 3.1.2 Una panoramica sulle funzioni fondamentali . . . . . . . . . . . . . 3.2 Le funzioni di base . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.2.1 Gli identicatori dei processi . . . . . . . . . . . . . . . . . . . . . 3.2.2 La funzione fork e le funzioni di creazione dei processi . . . . . . . 3.2.3 La conclusione di un processo . . . . . . . . . . . . . . . . . . . . . 3.2.4 La funzione waitpid e le funzioni di ricezione degli stati di uscita . 3.2.5 Le funzioni exec . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.3 Il controllo di accesso . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.3.1 Gli identicatori del controllo di accesso . . . . . . . . . . . . . . . 3.3.2 Le funzioni di gestione degli identicatori dei processi . . . . . . . 3.3.3 Le funzioni per la gestione dei gruppi associati a un processo . . . 3.3.4 La gestione delle capabilities . . . . . . . . . . . . . . . . . . . . . 3.4 La gestione della priorit` di esecuzione . . . . . . . . . . . . . . . . . . . . a 3.4.1 I meccanismi di scheduling . . . . . . . . . . . . . . . . . . . . . . 3.4.2 Il meccanismo di scheduling standard . . . . . . . . . . . . . . . . 3.4.3 Il meccanismo di scheduling real-time . . . . . . . . . . . . . . . . 3.4.4 Il controllo dello scheduler per i sistemi multiprocessore . . . . . . 3.5 Problematiche di programmazione multitasking . . . . . . . . . . . . . . . 3.5.1 Le operazioni atomiche . . . . . . . . . . . . . . . . . . . . . . . . 3.5.2 Le race condition ed i deadlock . . . . . . . . . . . . . . . . . . . . 3.5.3 Le funzioni rientranti . . . . . . . . . . . . . . . . . . . . . . . . . . 4 Larchitettura dei le 4.1 Larchitettura generale . . . . . . . . . . . . . . . 4.1.1 Lorganizzazione di le e directory . . . . 4.1.2 I tipi di le . . . . . . . . . . . . . . . . . 4.1.3 Le due interfacce ai le . . . . . . . . . . 4.2 Larchitettura della gestione dei le . . . . . . . . 4.2.1 Il Virtual File System di Linux . . . . . . 4.2.2 Il funzionamento del Virtual File System 4.2.3 Il funzionamento di un lesystem Unix . . 4.2.4 Il lesystem ext2 . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

5 File e directory 5.1 La gestione di le e directory . . . . . . . . . . . . . 5.1.1 Le funzioni link e unlink . . . . . . . . . . . 5.1.2 Le funzioni remove e rename . . . . . . . . . 5.1.3 I link simbolici . . . . . . . . . . . . . . . . . 5.1.4 La creazione e la cancellazione delle directory 5.1.5 La creazione di le speciali . . . . . . . . . . 5.1.6 Accesso alle directory . . . . . . . . . . . . . 5.1.7 La directory di lavoro . . . . . . . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

INDICE 5.1.8 I le temporanei . . . . . . . . . . . . . . . . . La manipolazione delle caratteristiche dei le . . . . . 5.2.1 La lettura delle caratteristiche dei le . . . . . 5.2.2 I tipi di le . . . . . . . . . . . . . . . . . . . . 5.2.3 Le dimensioni dei le . . . . . . . . . . . . . . . 5.2.4 I tempi dei le . . . . . . . . . . . . . . . . . . Il controllo di accesso ai le . . . . . . . . . . . . . . . 5.3.1 I permessi per laccesso ai le . . . . . . . . . . 5.3.2 I bit dei permessi speciali . . . . . . . . . . . . 5.3.3 Le funzioni per la gestione dei permessi dei le 5.3.4 La gestione della titolarit` dei le . . . . . . . a 5.3.5 Un quadro dinsieme sui permessi . . . . . . . . Caratteristiche e funzionalit` avanzate . . . . . . . . . a 5.4.1 Gli attributi estesi . . . . . . . . . . . . . . . . 5.4.2 Le Access Control List . . . . . . . . . . . . . . 5.4.3 La funzione chroot . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

v 108 111 111 111 112 114 116 116 118 120 122 124 125 125 129 130 133 133 133 134 135 135 138 138 139 141 141 141 143 144 145 146 149 151 155 155 155 156 156 156 157 158 160 161 162 164 166 170 171

5.2

5.3

5.4

6 I le: linterfaccia standard Unix 6.1 Larchitettura di base . . . . . . . . . . . . 6.1.1 Larchitettura dei le descriptor . . 6.1.2 I le standard . . . . . . . . . . . . . 6.2 Le funzioni base . . . . . . . . . . . . . . . 6.2.1 La funzione open . . . . . . . . . . . 6.2.2 La funzione close . . . . . . . . . . 6.2.3 La funzione lseek . . . . . . . . . . 6.2.4 La funzione read . . . . . . . . . . . 6.2.5 La funzione write . . . . . . . . . . 6.3 Caratteristiche avanzate . . . . . . . . . . . 6.3.1 La condivisione dei les . . . . . . . 6.3.2 Operazioni atomiche con i le . . . . 6.3.3 Le funzioni sync e fsync . . . . . . 6.3.4 Le funzioni dup e dup2 . . . . . . . . 6.3.5 Le funzioni openat, mkdirat e ani 6.3.6 La funzione fcntl . . . . . . . . . . 6.3.7 La funzione ioctl . . . . . . . . . . 7 I le: linterfaccia standard ANSI C 7.1 Introduzione . . . . . . . . . . . . . . . . 7.1.1 I le stream . . . . . . . . . . . . . 7.1.2 Gli oggetti FILE . . . . . . . . . . 7.1.3 Gli stream standard . . . . . . . . 7.1.4 Le modalit` di buerizzazione . . . a 7.2 Funzioni base . . . . . . . . . . . . . . . . 7.2.1 Apertura e chiusura di uno stream 7.2.2 Lettura e scrittura su uno stream . 7.2.3 Input/output binario . . . . . . . . 7.2.4 Input/output a caratteri . . . . . . 7.2.5 Input/output di linea . . . . . . . 7.2.6 Linput/output formattato . . . . 7.2.7 Posizionamento su uno stream . . 7.3 Funzioni avanzate . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

vi 7.3.1 7.3.2 7.3.3

INDICE Le funzioni di controllo . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171 Il controllo della buerizzazione . . . . . . . . . . . . . . . . . . . . . . . . 172 Gli stream e i thread . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174 177 177 177 180 181 182 182 183 183 185 188 191 193 193 195 196 198 198 199 200 201 203 207 207 207 209 211 211 211 212 213 213 214 214 215 217 218 218 219 219 220 220 221 221 222 223

8 La gestione del sistema, del tempo e degli errori 8.1 Capacit` e caratteristiche del sistema . . . . . . . . . . . a 8.1.1 Limiti e parametri di sistema . . . . . . . . . . . 8.1.2 La funzione sysconf . . . . . . . . . . . . . . . . 8.1.3 I limiti dei le . . . . . . . . . . . . . . . . . . . 8.1.4 La funzione pathconf . . . . . . . . . . . . . . . 8.1.5 La funzione uname . . . . . . . . . . . . . . . . . 8.2 Opzioni e congurazione del sistema . . . . . . . . . . . 8.2.1 La funzione sysctl ed il lesystem /proc . . . . 8.2.2 La gestione delle propriet` dei lesystem . . . . . a 8.2.3 La gestione delle informazioni su utenti e gruppi 8.2.4 Il registro della contabilit` degli utenti . . . . . . a 8.3 Il controllo delluso delle risorse . . . . . . . . . . . . . . 8.3.1 Luso delle risorse . . . . . . . . . . . . . . . . . 8.3.2 Limiti sulle risorse . . . . . . . . . . . . . . . . . 8.3.3 Le risorse di memoria e processore . . . . . . . . 8.3.4 La contabilit` in stile BSD . . . . . . . . . . . . a 8.4 La gestione dei tempi del sistema . . . . . . . . . . . . . 8.4.1 La misura del tempo in Unix . . . . . . . . . . . 8.4.2 La gestione del process time . . . . . . . . . . . . 8.4.3 Le funzioni per il calendar time . . . . . . . . . . 8.4.4 La gestione delle date. . . . . . . . . . . . . . . . 8.5 La gestione degli errori . . . . . . . . . . . . . . . . . . . 8.5.1 La variabile errno . . . . . . . . . . . . . . . . . 8.5.2 Le funzioni strerror e perror . . . . . . . . . . 8.5.3 Alcune estensioni GNU . . . . . . . . . . . . . . 9 I segnali 9.1 Introduzione . . . . . . . . . . . . . . . . . . . . . . 9.1.1 I concetti base . . . . . . . . . . . . . . . . . 9.1.2 Le semantiche del funzionamento dei segnali 9.1.3 Tipi di segnali . . . . . . . . . . . . . . . . . 9.1.4 La notica dei segnali . . . . . . . . . . . . . 9.2 La classicazione dei segnali . . . . . . . . . . . . . . 9.2.1 I segnali standard . . . . . . . . . . . . . . . 9.2.2 Segnali di errore di programma . . . . . . . . 9.2.3 I segnali di terminazione . . . . . . . . . . . . 9.2.4 I segnali di allarme . . . . . . . . . . . . . . . 9.2.5 I segnali di I/O asincrono . . . . . . . . . . . 9.2.6 I segnali per il controllo di sessione . . . . . . 9.2.7 I segnali di operazioni errate . . . . . . . . . 9.2.8 Ulteriori segnali . . . . . . . . . . . . . . . . . 9.2.9 Le funzioni strsignal e psignal . . . . . . . 9.3 La gestione di base dei segnali . . . . . . . . . . . . . 9.3.1 Il comportamento generale del sistema . . . . 9.3.2 La funzione signal . . . . . . . . . . . . . . 9.3.3 Le funzioni kill e raise . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

INDICE 9.3.4 Le funzioni alarm e abort . . . . . . . . . . . . . . . . 9.3.5 Le funzioni di pausa e attesa . . . . . . . . . . . . . . 9.3.6 Un esempio elementare . . . . . . . . . . . . . . . . . La gestione avanzata dei segnali . . . . . . . . . . . . . . . . . 9.4.1 Alcune problematiche aperte . . . . . . . . . . . . . . 9.4.2 Gli insiemi di segnali o signal set . . . . . . . . . . . . 9.4.3 La funzione sigaction . . . . . . . . . . . . . . . . . 9.4.4 La gestione della maschera dei segnali o signal mask . 9.4.5 Ulteriori funzioni di gestione . . . . . . . . . . . . . . 9.4.6 Criteri di programmazione per i gestori dei segnali . . Funzionalit` avanzate . . . . . . . . . . . . . . . . . . . . . . a 9.5.1 I segnali real-time . . . . . . . . . . . . . . . . . . . . 9.5.2 La gestione avanzata delle temporizzazioni . . . . . . 9.5.3 Le interfacce per la notica attraverso i le descriptor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

vii 225 228 229 230 231 233 234 237 240 242 242 243 246 246 247 247 247 248 250 253 254 258 259 260 270 271 272 272 272 273 273 273 274 277 280 284 284 286 295 300 300 308 309 316 320 320 321 323 329

9.4

9.5

10 Terminali e sessioni di lavoro 10.1 Il job control . . . . . . . . . . . . . . . . . . . . . . . . 10.1.1 Una panoramica introduttiva . . . . . . . . . . . 10.1.2 I process group e le sessioni . . . . . . . . . . . . 10.1.3 Il terminale di controllo e il controllo di sessione 10.1.4 Dal login alla shell . . . . . . . . . . . . . . . . . 10.1.5 Prescrizioni per un programma daemon . . . . . 10.2 LI/O su terminale . . . . . . . . . . . . . . . . . . . . . 10.2.1 Larchitettura . . . . . . . . . . . . . . . . . . . . 10.2.2 La gestione delle caratteristiche di un terminale . 10.2.3 La gestione della disciplina di linea. . . . . . . . 10.2.4 Operare in modo non canonico . . . . . . . . . . 10.3 La gestione dei terminali virtuali . . . . . . . . . . . . . 10.3.1 I terminali virtuali . . . . . . . . . . . . . . . . . 10.3.2 Allocazione dei terminale virtuali . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

11 La gestione avanzata dei le 11.1 LI/O multiplexing . . . . . . . . . . . . . . . . . . . . . . . 11.1.1 La problematica dellI/O multiplexing . . . . . . . . 11.1.2 Le funzioni select e pselect . . . . . . . . . . . . . 11.1.3 Le funzioni poll e ppoll . . . . . . . . . . . . . . . 11.1.4 Linterfaccia di epoll . . . . . . . . . . . . . . . . . . 11.2 Laccesso asincrono ai le . . . . . . . . . . . . . . . . . . . 11.2.1 Il Signal driven I/O . . . . . . . . . . . . . . . . . . 11.2.2 I meccanismi di notica asincrona. . . . . . . . . . . 11.2.3 Linterfaccia POSIX per lI/O asincrono . . . . . . . 11.3 Altre modalit` di I/O avanzato . . . . . . . . . . . . . . . . a 11.3.1 File mappati in memoria . . . . . . . . . . . . . . . . 11.3.2 I/O vettorizzato: readv e writev . . . . . . . . . . . 11.3.3 LI/O diretto fra le descriptor: sendfile e splice 11.3.4 Gestione avanzata dellaccesso ai dati dei le . . . . 11.4 Il le locking . . . . . . . . . . . . . . . . . . . . . . . . . . 11.4.1 Ladvisory locking . . . . . . . . . . . . . . . . . . . 11.4.2 La funzione flock . . . . . . . . . . . . . . . . . . . 11.4.3 Il le locking POSIX . . . . . . . . . . . . . . . . . . 11.4.4 La funzione lockf . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

viii

INDICE 11.4.5 Il mandatory locking . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 330

12 La comunicazione fra processi 12.1 La comunicazione fra processi tradizionale . . . . . . 12.1.1 Le pipe standard . . . . . . . . . . . . . . . . 12.1.2 Un esempio delluso delle pipe . . . . . . . . 12.1.3 Le funzioni popen e pclose . . . . . . . . . . 12.1.4 Le pipe con nome, o fo . . . . . . . . . . . . 12.1.5 La funzione socketpair . . . . . . . . . . . . 12.2 Il sistema di comunicazione fra processi di System V 12.2.1 Considerazioni generali . . . . . . . . . . . . 12.2.2 Il controllo di accesso . . . . . . . . . . . . . 12.2.3 Gli identicatori ed il loro utilizzo . . . . . . 12.2.4 Code di messaggi . . . . . . . . . . . . . . . . 12.2.5 Semafori . . . . . . . . . . . . . . . . . . . . . 12.2.6 Memoria condivisa . . . . . . . . . . . . . . . 12.3 Tecniche alternative . . . . . . . . . . . . . . . . . . 12.3.1 Alternative alle code di messaggi . . . . . . . 12.3.2 I le di lock . . . . . . . . . . . . . . . . . . . 12.3.3 La sincronizzazione con il le locking . . . . . 12.3.4 Il memory mapping anonimo . . . . . . . . . 12.4 Il sistema di comunicazione fra processi di POSIX . 12.4.1 Considerazioni generali . . . . . . . . . . . . 12.4.2 Code di messaggi . . . . . . . . . . . . . . . . 12.4.3 Memoria condivisa . . . . . . . . . . . . . . . 12.4.4 Semafori . . . . . . . . . . . . . . . . . . . . . 13 I thread 13.1 Introduzione ai thread . . . . . . . 13.1.1 Una panoramica . . . . . . 13.1.2 I thread e Linux . . . . . . 13.1.3 Implementazioni alternative 13.2 Posix thread . . . . . . . . . . . . . 13.2.1 Una panoramica . . . . . . 13.2.2 La gestione dei thread . . . 13.2.3 I mutex . . . . . . . . . . . 13.2.4 Le variabili di condizione .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

333 333 333 335 337 340 345 346 346 348 349 351 360 370 381 381 381 383 385 385 385 386 392 395 401 401 401 401 401 401 402 402 402 402

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

II

Programmazione di rete
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

403
405 405 405 406 406 407 407 408 410 410

14 Introduzione alla programmazione di rete 14.1 Modelli di programmazione . . . . . . . . . . . . . . 14.1.1 Il modello client-server . . . . . . . . . . . . 14.1.2 Il modello peer-to-peer . . . . . . . . . . . . . 14.1.3 Il modello three-tier . . . . . . . . . . . . . . 14.2 I protocolli di rete . . . . . . . . . . . . . . . . . . . 14.2.1 Il modello ISO/OSI . . . . . . . . . . . . . . 14.2.2 Il modello TCP/IP (o DoD) . . . . . . . . . . 14.2.3 Criteri generali dellarchitettura del TCP/IP 14.3 Il protocollo TCP/IP . . . . . . . . . . . . . . . . . .

INDICE 14.3.1 14.3.2 14.3.3 14.3.4 14.3.5 Il quadro generale . . . . . . . . . . . . . . . . . . . . . Internet Protocol (IP) . . . . . . . . . . . . . . . . . . . User Datagram Protocol (UDP) . . . . . . . . . . . . . Transport Control Protocol (TCP) . . . . . . . . . . . . Limiti e dimensioni riguardanti la trasmissione dei dati . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

ix 411 413 414 415 415 419 419 419 419 420 420 421 422 423 423 424 425 426 426 426 428 428 430 430 431 433 433 433 434 435 436 438 439 441 442 442 444 445 447 448 450 450 450 451 454 456 458 458 459

15 Introduzione ai socket 15.1 Una panoramica . . . . . . . . . . . . . . . . . . . . . . 15.1.1 I socket . . . . . . . . . . . . . . . . . . . . . . . 15.1.2 Concetti base . . . . . . . . . . . . . . . . . . . . 15.2 La creazione di un socket . . . . . . . . . . . . . . . . . 15.2.1 La funzione socket . . . . . . . . . . . . . . . . 15.2.2 Il dominio dei socket . . . . . . . . . . . . . . . . 15.2.3 Il tipo di socket . . . . . . . . . . . . . . . . . . . 15.3 Le strutture degli indirizzi dei socket . . . . . . . . . . . 15.3.1 La struttura generica . . . . . . . . . . . . . . . . 15.3.2 La struttura degli indirizzi IPv4 . . . . . . . . . 15.3.3 La struttura degli indirizzi IPv6 . . . . . . . . . 15.3.4 La struttura degli indirizzi locali . . . . . . . . . 15.3.5 La struttura degli indirizzi AppleTalk . . . . . . 15.3.6 La struttura degli indirizzi dei packet socket . . . 15.4 Le funzioni di conversione degli indirizzi . . . . . . . . . 15.4.1 La endianess . . . . . . . . . . . . . . . . . . . . 15.4.2 Le funzioni per il riordinamento . . . . . . . . . 15.4.3 Le funzioni inet_aton, inet_addr e inet_ntoa 15.4.4 Le funzioni inet_pton e inet_ntop . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

16 I socket TCP 16.1 Il funzionamento di una connessione TCP . . . . . . . . . . . 16.1.1 La creazione della connessione: il three way handshake 16.1.2 Le opzioni TCP. . . . . . . . . . . . . . . . . . . . . . 16.1.3 La terminazione della connessione . . . . . . . . . . . 16.1.4 Un esempio di connessione . . . . . . . . . . . . . . . . 16.1.5 Lo stato TIME_WAIT . . . . . . . . . . . . . . . . . . . 16.1.6 I numeri di porta . . . . . . . . . . . . . . . . . . . . . 16.1.7 Le porte ed il modello client/server . . . . . . . . . . . 16.2 Le funzioni di base per la gestione dei socket . . . . . . . . . 16.2.1 La funzione bind . . . . . . . . . . . . . . . . . . . . . 16.2.2 La funzione connect . . . . . . . . . . . . . . . . . . . 16.2.3 La funzione listen . . . . . . . . . . . . . . . . . . . 16.2.4 La funzione accept . . . . . . . . . . . . . . . . . . . 16.2.5 Le funzioni getsockname e getpeername . . . . . . . . 16.2.6 La funzione close . . . . . . . . . . . . . . . . . . . . 16.3 Un esempio elementare: il servizio daytime . . . . . . . . . . . 16.3.1 Il comportamento delle funzioni di I/O . . . . . . . . . 16.3.2 Il client daytime . . . . . . . . . . . . . . . . . . . . . 16.3.3 Un server daytime iterativo . . . . . . . . . . . . . . . 16.3.4 Un server daytime concorrente . . . . . . . . . . . . . 16.4 Un esempio pi` completo: il servizio echo . . . . . . . . . . . u 16.4.1 Il servizio echo . . . . . . . . . . . . . . . . . . . . . . 16.4.2 Il client echo: prima versione . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

x 16.4.3 Il server echo: prima versione . . . . . . . . . . . . . . 16.4.4 Lavvio e il funzionamento normale . . . . . . . . . . . 16.4.5 La conclusione normale . . . . . . . . . . . . . . . . . 16.4.6 La gestione dei processi gli . . . . . . . . . . . . . . . 16.5 I vari scenari critici . . . . . . . . . . . . . . . . . . . . . . . . 16.5.1 La terminazione precoce della connessione . . . . . . . 16.5.2 La terminazione precoce del server . . . . . . . . . . . 16.5.3 Altri scenari di terminazione della connessione . . . . 16.6 Luso dellI/O multiplexing . . . . . . . . . . . . . . . . . . . 16.6.1 Il comportamento della funzione select con i socket. 16.6.2 Un esempio di I/O multiplexing . . . . . . . . . . . . 16.6.3 La funzione shutdown . . . . . . . . . . . . . . . . . . 16.6.4 Un server basato sullI/O multiplexing . . . . . . . . . 16.6.5 I/O multiplexing con poll . . . . . . . . . . . . . . . 17 La gestione dei socket 17.1 La risoluzione dei nomi . . . . . . . . . . . . . . . . . . . . 17.1.1 La struttura del resolver . . . . . . . . . . . . . . . 17.1.2 Le funzioni di interrogazione del resolver . . . . . 17.1.3 La risoluzione dei nomi a dominio . . . . . . . . . 17.1.4 Le funzioni avanzate per la risoluzione dei nomi . . 17.2 Le opzioni dei socket . . . . . . . . . . . . . . . . . . . . . 17.2.1 Le funzioni setsockopt e getsockopt . . . . . . . 17.2.2 Le opzioni generiche . . . . . . . . . . . . . . . . . 17.2.3 Luso delle principali opzioni dei socket . . . . . . 17.2.4 Le opzioni per il protocollo IPv4 . . . . . . . . . . 17.2.5 Le opzioni per i protocolli TCP e UDP . . . . . . 17.3 La gestione attraverso le funzioni di controllo . . . . . . . 17.3.1 Luso di ioctl e fcntl per i socket generici . . . . 17.3.2 Luso di ioctl per laccesso ai dispositivi di rete . 17.3.3 Luso di ioctl per i socket TCP e UDP . . . . . . 17.4 La gestione con sysctl ed il lesystem /proc . . . . . . . 17.4.1 Luso di sysctl e /proc per le propriet` della rete a 17.4.2 I valori di controllo per i socket generici . . . . . . 17.4.3 I valori di controllo per il protocollo IPv4 . . . . . 18 Gli altri tipi di socket 18.1 I socket UDP . . . . . . . . . . . . . . . . . . . . . . . 18.1.1 Le caratteristiche di un socket UDP . . . . . . 18.1.2 Le funzioni sendto e recvfrom . . . . . . . . . 18.1.3 Un client UDP elementare . . . . . . . . . . . . 18.1.4 Un server UDP elementare . . . . . . . . . . . 18.1.5 Le problematiche dei socket UDP . . . . . . . . 18.1.6 Luso della funzione connect con i socket UDP 18.2 I socket Unix domain . . . . . . . . . . . . . . . . . . 18.2.1 Il passaggio di le descriptor . . . . . . . . . . 18.3 Altri socket . . . . . . . . . . . . . . . . . . . . . . . . 18.3.1 I socket raw . . . . . . . . . . . . . . . . . . . . 18.3.2 I socket netlink . . . . . . . . . . . . . . . . . . 18.3.3 I packet socket . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

INDICE . . . . . . . . . . . . . . . . . . . . . . . . . . . . 460 463 464 465 468 469 470 474 477 477 478 481 485 488 493 493 493 495 501 508 518 519 521 525 531 535 541 542 543 547 548 548 549 550 559 559 559 560 563 564 566 570 571 571 571 571 572 572

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

INDICE 19 Socket avanzati 19.1 Le funzioni di I/O avanzate . . . . . . 19.1.1 La funzioni sendmsg e recvmsg 19.1.2 I messaggi ancillari . . . . . . . 19.1.3 I dati urgenti o out-of-band . . 19.2 Luso dellI/O non bloccante . . . . . 19.2.1 La gestione delle opzioni IP . .

xi 573 573 573 573 574 574 574

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

III

Appendici
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

575
577 577 577 579 579 579 579 581 581 583 583 584 585 586 586 587 588 588 589 590 590 591 591 592 593 593 594 594 594

A Il livello di rete A.1 Il protocollo IP . . . . . . . . . . . . . . . . . A.1.1 Introduzione . . . . . . . . . . . . . . A.1.2 Lintestazione di IP . . . . . . . . . . A.1.3 Le opzioni di IP . . . . . . . . . . . . A.2 Il protocollo IPv6 . . . . . . . . . . . . . . . . A.2.1 I motivi della transizione . . . . . . . A.2.2 Principali caratteristiche di IPv6 . . . A.2.3 Lintestazione di IPv6 . . . . . . . . . A.2.4 Gli indirizzi di IPv6 . . . . . . . . . . A.2.5 La notazione . . . . . . . . . . . . . . A.2.6 La architettura degli indirizzi di IPv6 A.2.7 Indirizzi unicast provider-based . . . . A.2.8 Indirizzi ad uso locale . . . . . . . . . A.2.9 Indirizzi riservati . . . . . . . . . . . . A.2.10 Multicasting . . . . . . . . . . . . . . A.2.11 Indirizzi anycast . . . . . . . . . . . . A.2.12 Le estensioni . . . . . . . . . . . . . . A.2.13 Qualit` di servizio . . . . . . . . . . . a A.2.14 Etichette di usso . . . . . . . . . . . A.2.15 Priorit` . . . . . . . . . . . . . . . . . a A.2.16 Sicurezza a livello IP . . . . . . . . . . A.2.17 Autenticazione . . . . . . . . . . . . . A.2.18 Riservatezza . . . . . . . . . . . . . . A.2.19 Autocongurazione . . . . . . . . . . . A.2.20 Autocongurazione stateless . . . . . . A.2.21 Autocongurazione stateful . . . . . . A.3 Il protocollo ICMP . . . . . . . . . . . . . . . A.3.1 Lintestazione di ICMP . . . . . . . .

B Il livello di trasporto 597 B.1 Il protocollo TCP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 597 B.1.1 Gli stati del TCP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 597 B.2 Il protocollo UDP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 597 C I codici di errore C.1 Gli errori dei le . . . C.2 Gli errori dei processi C.3 Gli errori di rete . . . C.4 Errori generici . . . . . 599 599 601 601 603

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

xii D Gli strumenti di ausilio per la programmazione D.1 Luso di make per lautomazione della compilazione D.1.1 Introduzione a make . . . . . . . . . . . . . D.1.2 Utilizzo di make . . . . . . . . . . . . . . . . D.2 Cuncurrent Version System CVS . . . . . . . . . D.2.1 Introduzione . . . . . . . . . . . . . . . . . D.2.2 Utilizzo di cvs . . . . . . . . . . . . . . . . D.2.3 I principali sotto comandi . . . . . . . . . . E Ringraziamenti F GNU Free Documentation License F.1 Applicability and Denitions . . . . . F.2 Verbatim Copying . . . . . . . . . . . F.3 Copying in Quantity . . . . . . . . . . F.4 Modications . . . . . . . . . . . . . . F.5 Combining Documents . . . . . . . . . F.6 Collections of Documents . . . . . . . F.7 Aggregation With Independent Works F.8 Translation . . . . . . . . . . . . . . . F.9 Termination . . . . . . . . . . . . . . . F.10 Future Revisions of This License . . .

INDICE 607 607 607 608 610 610 611 612 613 615 615 616 616 617 618 618 618 618 619 619

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

Un preambolo
Questa guida nasce dalla mia profonda convinzione che le istanze di libert` e di condivisione a della conoscenza che hanno dato vita a quello straordinario movimento di persone ed intelligenza che va sotto il nome di software libero hanno la stessa rilevanza anche quando applicate alla produzione culturale in genere. Lambito pi` comune in cui questa losoa viene applicata ` quello della documentazione u e perch il software, per quanto possa essere libero, se non accompagnato da una buona docue mentazione che aiuti a comprenderne il funzionamento, rischia di essere fortemente decitario riguardo ad una delle libert` fondamentali, quella di essere studiato e migliorato. a Ritengo inoltre che in campo tecnico ed educativo sia importante poter disporre di testi didattici (come manuali, enciclopedie, dizionari, ecc.) in grado di crescere, essere adattati alle diverse esigenze, modicati e ampliati, o anche ridotti per usi specici, nello stesso modo in cui si fa per il software libero. Questa guida ` il mio tentativo di restituire indietro, nei limiti di quelle che sono le mie e capacit`, un po della conoscenza che ho ricevuto, mettendo a disposizione un testo che possa a fare da riferimento a chi si avvicina alla programmazione su Linux, nella speranza anche di trasmettergli non solo delle conoscenze tecniche, ma anche un po di quella passione per la libert` e la condivisione della conoscenza che sono la ricchezza maggiore che ho ricevuto. a E, come per il software libero, anche in questo caso ` importante la possibilit` di accedere ai e a sorgenti (e non solo al risultato nale, sia questo una stampa o un le formattato) e la libert` a di modicarli per apportarvi migliorie, aggiornamenti, ecc. Per questo motivo la Free Software Foundation ha creato una apposita licenza che potesse giocare lo stesso ruolo fondamentale che la GPL ha avuto per il software libero nel garantire la permanenza delle libert` date, ma potesse anche tenere conto delle dierenze che comunque ci a sono fra un testo ed un programma. Una di queste dierenze ` che in un testo, come in questa sezione, possono venire espresse e quelle che sono le idee ed i punti di vista dellautore, e mentre trovo che sia necessario permettere cambiamenti nei contenuti tecnici, che devono essere aggiornati e corretti, non vale lo stesso per lespressione delle mie idee contenuta in questa sezione, che ho richiesto resti invariata. Il progetto pertanto prevede il rilascio della guida con licenza GNU FDL, ed una modalit` a di realizzazione aperta che permetta di accogliere i contributi di chiunque sia interessato. Tutti i programmi di esempio sono rilasciati con licenza GNU GPL.

xiii

xiv

UN PREAMBOLO

Prefazione
Questo progetto mira alla stesura di un testo il pi` completo e chiaro possibile sulla programmau zione di sistema su un kernel Linux. Essendo i concetti in gran parte gli stessi, il testo dovrebbe restare valido anche per la programmazione in ambito di sistemi Unix generici, ma resta una attenzione specica alle caratteristiche peculiari del kernel Linux e delle versioni delle librerie del C in uso con esso; in particolare si dar` ampio spazio alla versione realizzata dal progetto a GNU, le cosiddette GNU C Library o glibc, che ormai sono usate nella stragrande maggioranza dei casi. Lobiettivo nale di questo progetto ` quello di riuscire a ottenere un testo utilizzabile per e apprendere i concetti fondamentali della programmazione di sistema della stessa qualit` dei libri a del compianto R. W. Stevens (` un progetto molto ambizioso ...). e Infatti bench le pagine di manuale del sistema (quelle che si accedono con il comando man) e e il manuale delle librerie del C GNU siano una fonte inesauribile di informazioni (da cui si ` costantemente attinto nella stesura di tutto il testo) la loro struttura li rende totalmente e inadatti ad una trattazione che vada oltre la descrizione delle caratteristiche particolari dello specico argomento in esame (ed in particolare lo GNU C Library Reference Manual non brilla per chiarezza espositiva). Per questo motivo si ` cercato di fare tesoro di quanto appreso dai testi di R. W. Stevens (in e particolare [1] e [2]) per rendere la trattazione dei vari argomenti in una sequenza logica il pi` u esplicativa possibile, corredando il tutto, quando possibile, con programmi di esempio. Dato che sia il kernel che tutte le librerie fondamentali di GNU/Linux sono scritte in C, questo sar` il linguaggio di riferimento del testo. In particolare il compilatore usato per provare tutti i a programmi e gli esempi descritti nel testo ` lo GNU GCC. Il testo presuppone una conoscenza e media del linguaggio, e di quanto necessario per scrivere, compilare ed eseguire un programma. A Inne, dato che lo scopo del progetto ` la produzione di un libro, si ` scelto di usare L TEX e e come ambiente di sviluppo del medesimo, sia per limpareggiabile qualit` tipograca ottenibile, a che per la congruenza dello strumento con il ne, tanto sul piano pratico, quanto su quello losoco. Il testo sar`, almeno inizialmente, in italiano. Per il momento lo si ` suddiviso in due parti, a e la prima sulla programmazione di sistema, in cui si trattano le varie funzionalit` disponibili per i a programmi che devono essere eseguiti su una singola macchina, la seconda sulla programmazione di rete, in cui si trattano le funzionalit` per eseguire programmi che mettono in comunicazione a macchine diverse.

xv

Parte I

Programmazione di sistema

Capitolo 1

Larchitettura del sistema


In questo primo capitolo sar` fatta unintroduzione ai concetti generali su cui ` basato un a e sistema operativo di tipo Unix come GNU/Linux, in questo modo potremo fornire una base di comprensione mirata a sottolineare le peculiarit` del sistema che sono pi` rilevanti per quello a u che riguarda la programmazione. Dopo unintroduzione sulle caratteristiche principali di un sistema di tipo Unix passeremo ad illustrare alcuni dei concetti base dellarchitettura di GNU/Linux (che sono comunque comuni a tutti i sistemi unix-like) ed introdurremo alcuni degli standard principali a cui viene fatto riferimento.

1.1

Una panoramica

In questa prima sezione faremo una breve panoramica sullarchitettura del sistema. Chi avesse gi` una conoscenza di questa materia pu` tranquillamente saltare questa sezione. a o

1.1.1

Concetti base

Il concetto base di un sistema unix-like ` quello di un nucleo del sistema (il cosiddetto kernel, e nel nostro caso Linux) a cui si demanda la gestione delle risorse essenziali (la CPU, la memoria, le periferiche) mentre tutto il resto, quindi anche la parte che prevede linterazione con lutente, devessere realizzato tramite programmi eseguiti dal kernel, che accedano alle risorse hardware tramite delle richieste a questultimo. Fin dallinizio uno Unix si presenta come un sistema operativo multitasking, cio` in grado e di eseguire contemporaneamente pi` programmi, e multiutente, in cui ` possibile che pi` utenti u e u siano connessi ad una macchina eseguendo pi` programmi in contemporanea (in realt`, almeno u a per macchine a processore singolo, i programmi vengono eseguiti singolarmente a rotazione). I kernel Unix pi` recenti, come Linux, sono realizzati sfruttando alcune caratteristiche dei u processori moderni come la gestione hardware della memoria e la modalit` protetta. In sostanza a con i processori moderni si pu` disabilitare temporaneamente luso di certe istruzioni e laccesso o a certe zone di memoria sica. Quello che succede ` che il kernel ` il solo programma ad essere e e eseguito in modalit` privilegiata, con il completo accesso allhardware, mentre i programmi a normali vengono eseguiti in modalit` protetta (e non possono accedere direttamente alle zone a di memoria riservate o alle porte di input/output). Una parte del kernel, lo scheduler, si occupa di stabilire, ad intervalli ssi e sulla base di un opportuno calcolo delle priorit`, quale processo deve essere posto in esecuzione (il cosida detto prehemptive multitasking). Questo verr` comunque eseguito in modalit` protetta; quando a a necessario il processo potr` accedere alle risorse hardware soltanto attraverso delle opportune a chiamate al sistema che restituiranno il controllo al kernel. 3

CAPITOLO 1. LARCHITETTURA DEL SISTEMA

La memoria viene sempre gestita dal kernel attraverso il meccanismo della memoria virtuale, che consente di assegnare a ciascun processo uno spazio di indirizzi virtuale (vedi sez. 2.2) che il kernel stesso, con lausilio della unit` di gestione della memoria, si incaricher` di rimappare aua a tomaticamente sulla memoria disponibile, salvando su disco quando necessario (nella cosiddetta area di swap) le pagine di memoria in eccedenza. Le periferiche inne vengono viste in genere attraverso uninterfaccia astratta che permette di trattarle come fossero le, secondo il concetto per cui everything is a le, su cui torneremo in dettaglio in cap. 4. Questo non ` vero per le interfacce di rete, che hanno uninterfaccia diversa, e ma resta valido il concetto generale che tutto il lavoro di accesso e gestione a basso livello ` e eettuato dal kernel.

1.1.2

Il kernel e il sistema

Uno dei concetti fondamentali su cui si basa larchitettura dei sistemi Unix ` quello della die stinzione fra il cosiddetto user space, che contraddistingue lambiente in cui vengono eseguiti i programmi, e il kernel space, che ` lambiente in cui viene eseguito il kernel. Ogni programma e vede s stesso come se avesse la piena disponibilit` della CPU e della memoria ed `, salvo i e a e meccanismi di comunicazione previsti dallarchitettura, completamente ignaro del fatto che altri programmi possono essere messi in esecuzione dal kernel. Per questa separazione non ` possibile ad un singolo programma disturbare lazione di un e altro programma o del sistema e questo ` il principale motivo della stabilit` di un sistema unixe a like nei confronti di altri sistemi in cui i processi non hanno di questi limiti, o che vengono per vari motivi eseguiti al livello del kernel. Pertanto deve essere chiaro a chi programma in Unix che laccesso diretto allhardware non pu` avvenire se non allinterno del kernel; al di fuori dal kernel il programmatore deve usare le o opportune interfacce che questultimo fornisce allo user space. Per capire meglio la distinzione fra kernel space e user space si pu` prendere in esame o la procedura di avvio di un sistema unix-like; allavvio il BIOS (o in generale il software di avvio posto nelle EPROM) eseguir` la procedura di avvio del sistema (il cosiddetto bootstrap 1 ), a incaricandosi di caricare il kernel in memoria e di farne partire lesecuzione; questultimo, dopo aver inizializzato le periferiche, far` partire il primo processo, init, che ` quello che a sua volta a e far` partire tutti i processi successivi. Fra questi ci sar` pure quello che si occupa di dialogare a a con la tastiera e lo schermo della console, e quello che mette a disposizione dellutente che si vuole collegare, un terminale e la shell da cui inviare i comandi. E da rimarcare come tutto ci`, che usualmente viene visto come parte del sistema, non abbia o in realt` niente a che fare con il kernel, ma sia eettuato da opportuni programmi che vengono a eseguiti, allo stesso modo di un qualunque programma di scrittura o di disegno, in user space. Questo signica, ad esempio, che il sistema di per s non dispone di primitive per tutta una e serie di operazioni (come la copia di un le) che altri sistemi (come Windows) hanno invece al loro interno. Pertanto buona parte delle operazioni di normale amministrazione di un sistema, come quella in esempio, sono implementate come normali programmi. Per questo motivo quando ci si riferisce al sistema nella sua interezza ` corretto parlare di un e sistema GNU/Linux: da solo il kernel ` assolutamente inutile; quello che costruisce un sistema e operativo utilizzabile ` la presenza di tutta una serie di librerie e programmi di utilit` (che di e a norma sono quelli realizzati dal progetto GNU della Free Software Foundation) che permettono di eseguire le normali operazioni che ci si aspetta da un sistema operativo.
il nome deriva da unespressione gergale che signica sollevarsi da terra tirandosi per le stringhe delle scarpe, per indicare il compito, almeno apparentemente impossibile, di far eseguire un programma a partire da un computer appena acceso che appunto non ne contiene nessuno; non ` impossibile perch in realt` c` un programma iniziale, e e a e che ` il BIOS. e
1

1.1. UNA PANORAMICA

1.1.3

Chiamate al sistema e librerie di funzioni

Come accennato le interfacce con cui i programmi possono accedere allhardware vanno sotto il nome di chiamate al sistema (le cosiddette system call ), si tratta di un insieme di funzioni che un programma pu` chiamare, per le quali viene generata uninterruzione del processo passando o il controllo dal programma al kernel. Sar` poi questultimo che (oltre a compiere una serie di a operazioni interne come la gestione del multitasking e lallocazione della memoria) eseguir` la a funzione richiesta in kernel space restituendo i risultati al chiamante. Ogni versione di Unix ha storicamente sempre avuto un certo numero di queste chiamate, che sono riportate nella seconda sezione del Manuale di programmazione di Unix (quella cui si accede con il comando man 2 <nome>) e Linux non fa eccezione. Queste sono poi state codicate da vari standard, che esamineremo brevemente in sez. 1.2. Uno schema elementare della struttura del sistema ` riportato in g. 1.1. e

Figura 1.1: Schema di massima della struttura di interazione fra processi, kernel e dispositivi in Linux.

Normalmente ciascuna di queste chiamate al sistema viene rimappata in opportune funzioni con lo stesso nome denite dentro la Libreria Standard del C, che, oltre alle interfacce alle system call, contiene anche tutta la serie delle ulteriori funzioni denite dai vari standard, che sono comunemente usate nella programmazione. Questo ` importante da capire perch programmare in Linux signica anzitutto essere in grae e do di usare le varie interfacce contenute nella Libreria Standard del C, in quanto n il kernel, n e e il linguaggio C implementano direttamente operazioni comuni come lallocazione dinamica della memoria, linput/output buerizzato o la manipolazione delle stringhe, presenti in qualunque programma. Quanto appena illustrato mette in evidenza il fatto che nella stragrande maggioranza dei casi,2 si dovrebbe usare il nome GNU/Linux (piuttosto che soltanto Linux) in quanto una parte essenziale del sistema (senza la quale niente funzionerebbe) ` la GNU Standard C Library (in e breve glibc), ovvero la libreria realizzata dalla Free Software Foundation nella quale sono state
2 esistono implementazioni diverse delle librerie Standard del C, come le libc5 o le uClib, che non derivano dal progetto GNU. Le libc5 oggi sono, tranne casi particolari, completamente soppiantate dalle glibc, le uClib pur non essendo complete come le glibc, restano invece molto diuse nel mondo embedded per le loro dimensioni ridotte (e soprattutto la possibilit` di togliere le parti non necessarie), e pertanto costituiscono un valido rimpiazzo delle a glibc in tutti quei sistemi specializzati che richiedono una minima occupazione di memoria.

CAPITOLO 1. LARCHITETTURA DEL SISTEMA

implementate tutte le funzioni essenziali denite negli standard POSIX e ANSI C, utilizzabili da qualunque programma. Le funzioni di questa libreria sono quelle riportate dalla terza sezione del Manuale di Programmazione di Unix (cio` accessibili con il comando man 3 <nome>) e sono costruite sulla base e delle chiamate al sistema del kernel; ` importante avere presente questa distinzione, fondamentale e dal punto di vista dellimplementazione, anche se poi, nella realizzazione di normali programmi, non si hanno dierenze pratiche fra luso di una funzione di libreria e quello di una chiamata al sistema.

1.1.4

Un sistema multiutente

Linux, come gli altri kernel Unix, nasce n dallinizio come sistema multiutente, cio` in grado e di fare lavorare pi` persone in contemporanea. Per questo esistono una serie di meccanismi di u sicurezza, che non sono previsti in sistemi operativi monoutente, e che occorre tenere presente. Il concetto base ` quello di utente (user ) del sistema, le cui capacit` rispetto a quello che e a pu` fare sono sottoposte a ben precisi limiti. Sono cos` previsti una serie di meccanismi per o identicare i singoli utenti ed una serie di permessi e protezioni per impedire che utenti diversi possano danneggiarsi a vicenda o danneggiare il sistema. Ogni utente ` identicato da un nome (lusername), che ` quello che viene richiesto allingrese e so nel sistema dalla procedura di login (descritta in dettaglio in sez. 10.1.4). Questa procedura si incarica di vericare lidentit` dellutente, in genere attraverso la richiesta di una parola dordine a (la password ), anche se sono possibili meccanismi diversi.3 Eseguita la procedura di riconoscimento in genere il sistema manda in esecuzione un programma di interfaccia (che pu` essere la shell su terminale o uninterfaccia graca) che mette o a disposizione dellutente un meccanismo con cui questo pu` impartire comandi o eseguire altri o programmi. Ogni utente appartiene anche ad almeno un gruppo (il cosiddetto default group), ma pu` o essere associato ad altri gruppi (i supplementary group), questo permette di gestire i permessi di accesso ai le e quindi anche alle periferiche, in maniera pi` essibile, denendo gruppi di u lavoro, di accesso a determinate risorse, ecc. Lutente e il gruppo sono identicati da due numeri, la cui corrispondenza ad un nome espresso in caratteri ` inserita nei due le /etc/passwd e /etc/group.4 Questi numeri sono e luser identier, detto in breve user-ID, ed indicato dallacronimo uid, e il group identier, detto in breve group-ID, ed identicato dallacronimo gid, e sono quelli che vengono usati dal kernel per identicare lutente. In questo modo il sistema ` in grado di tenere traccia dellutente a cui appartiene ciascun e processo ed impedire ad altri utenti di interferire con questultimo. Inoltre con questo sistema viene anche garantita una forma base di sicurezza interna in quanto anche laccesso ai le (vedi sez. 5.3) ` regolato da questo meccanismo di identicazione. e Inne in ogni Unix ` presente un utente speciale privilegiato, il cosiddetto superuser, il cui e username ` di norma root, ed il cui uid ` zero. Esso identica lamministratore del sistema, e e che deve essere in grado di fare qualunque operazione; per lutente root infatti i meccanismi di controllo descritti in precedenza sono disattivati.5
ad esempio usando la libreria PAM (Pluggable Autentication Methods) ` possibile astrarre completamente e dai meccanismi di autenticazione e sostituire ad esempio luso delle password con meccanismi di identicazione biometrica. 4 in realt` negli sistemi pi` moderni, come vedremo in sez. 8.2.3 queste informazioni possono essere mantenute, a u con luso del Name Service Switch, su varie tipologie di supporti, compresi server centralizzati come LDAP. 5 i controlli infatti vengono sempre eseguiti da un codice del tipo: if (uid) { ... }.
3

1.2. GLI STANDARD

1.2

Gli standard

In questa sezione faremo una breve panoramica relativa ai vari standard che nel tempo sono stati formalizzati da enti, associazioni, consorzi e organizzazioni varie al riguardo del sistema o alle caratteristiche che si sono stabilite come standard di fatto in quanto facenti parte di alcune implementazioni molto diuse come BSD o SVr4. Ovviamente prenderemo in considerazione solo gli standard riguardanti interfacce di programmazione e le altre caratteristiche di un sistema unix-like (alcuni standardizzano pure i comandi base del sistema e la shell) ed in particolare ci concentreremo sul come ed in che modo essi sono supportati sia per quanto riguarda il kernel che le librerie del C (con una particolare attenzione alle glibc).

1.2.1

Lo standard ANSI C

Lo standard ANSI C ` stato denito nel 1989 dallAmerican National Standard Institute, come e standard del linguaggio C ed ` stato successivamente adottato dalla International Standard e Organisation come standard internazionale con la sigla ISO/IEC 9899:1990, e va anche sotto il nome di standard ISO C. Scopo dello standard ` quello di garantire la portabilit` dei programmi C fra sistemi operativi e a diversi, ma oltre alla sintassi ed alla semantica del linguaggio C (operatori, parole chiave, tipi di dati) lo standard prevede anche una libreria di funzioni che devono poter essere implementate su qualunque sistema operativo. Per questo motivo, anche se lo standard non ha alcun riferimento ad un sistema di tipo Unix, GNU/Linux (per essere precisi le glibc), come molti Unix moderni, provvede la compatibilit` a con questo standard, fornendo le funzioni di libreria da esso previste. Queste sono dichiarate in una serie di header le 6 (anchessi provvisti dalla glibc), In tab. 1.1 si sono riportati i principali header le deniti nello standard POSIX ed ANSI C, che sono anche quelli deniti negli altri standard descritti nelle sezioni successive.
Header assert.h ctype.h dirent.h errno.h fcntl.h limits.h malloc.h setjmp.h signal.h stdarg.h stdio.h stdlib.h string.h time.h times.h unistd.h utmp.h Standard ANSI C POSIX Contenuto Verica le asserzioni fatte in un programma. Tipi standard. Manipolazione delle directory. Errori di sistema. Controllo sulle opzioni dei le. Limiti e parametri del sistema. Allocazione della memoria. Salti non locali. Gestione dei segnali. Gestione di funzioni a argomenti variabili. I/O buerizzato in standard ANSI C. Denizioni della libreria standard. Manipolazione delle stringhe. Gestione dei tempi. Gestione dei tempi. Unix standard library. Registro connessioni utenti.

Tabella 1.1: Elenco dei vari header le deniti dallo standard POSIX.

In realt` glibc ed i relativi header le deniscono un insieme di funzionalit` in cui sono a a


i le di dichiarazione di variabili, tipi e funzioni, usati normalmente da un compilatore C. Per poter accedere alle funzioni occorre includere con la direttiva #include questi le nei propri programmi; per ciascuna funzione che tratteremo in seguito indicheremo anche gli header le necessari ad usarla.
6

CAPITOLO 1. LARCHITETTURA DEL SISTEMA

` incluse come sottoinsieme anche quelle previste dallo standard ANSI C. E possibile ottenere una conformit` stretta allo standard (scartando le funzionalit` addizionali) usando il gcc con a a lopzione -ansi. Questa opzione istruisce il compilatore a denire nei vari header le soltanto le funzionalit` previste dallo standard ANSI C e a non usare le varie estensioni al linguaggio e al a preprocessore da esso supportate.

1.2.2

I tipi di dati primitivi

Uno dei problemi di portabilit` del codice pi` comune ` quello dei tipi di dati utilizzati nei a u e programmi, che spesso variano da sistema a sistema, o anche da una architettura ad unaltra (ad esempio passando da macchine con processori 32 bit a 64). In particolare questo ` vero e nelluso dei cosiddetti tipi elementari del linguaggio C (come int) la cui dimensione varia a seconda dellarchitettura hardware. Storicamente alcuni tipi nativi dello standard ANSI C sono sempre stati associati ad alcune variabili nei sistemi Unix, dando per scontata la dimensione. Ad esempio la posizione corrente allinterno di un le ` sempre stata associata ad un intero a 32 bit, mentre il numero di dispositivo e ` sempre stato associato ad un intero a 16 bit. Storicamente questi erano deniti rispettivamente e come int e short, ma tutte le volte che, con levolversi ed il mutare delle piattaforme hardware, alcuni di questi tipi si sono rivelati inadeguati o sono cambiati, ci si ` trovati di fronte ad una e innita serie di problemi di portabilit`. a
Tipo caddr_t clock_t dev_t gid_t ino_t key_t loff_t mode_t nlink_t off_t pid_t rlim_t sigset_t size_t ssize_t ptrdiff_t time_t uid_t Contenuto Core address. Contatore del tempo di sistema. Numero di dispositivo. Identicatore di un gruppo. Numero di inode. Chiave per il System V IPC. Posizione corrente in un le. Attributi di un le. Contatore dei link su un le. Posizione corrente in un le. Identicatore di un processo. Limite sulle risorse. Insieme di segnali. Dimensione di un oggetto. Dimensione in numero di byte ritornata dalle funzioni. Dierenza fra due puntatori. Numero di secondi (in tempo di calendario). Identicatore di un utente.

Tabella 1.2: Elenco dei tipi primitivi, deniti in sys/types.h.

Per questo motivo tutte le funzioni di libreria di solito non fanno riferimento ai tipi elementari dello standard del linguaggio C, ma ad una serie di tipi primitivi del sistema, riportati in tab. 1.2, e deniti nellheader le sys/types.h, in modo da mantenere completamente indipendenti i tipi utilizzati dalle funzioni di sistema dai tipi elementari supportati dal compilatore C.

1.2.3

Lo standard IEEE POSIX

Uno standard pi` attinente al sistema nel suo complesso (e che concerne sia il kernel che le u librerie) ` lo standard POSIX. Esso prende origine dallo standard ANSI C, che contiene come e sottoinsieme, prevedendo ulteriori capacit` per le funzioni in esso denite, ed aggiungendone di a nuove.

1.2. GLI STANDARD

In realt` POSIX ` una famiglia di standard diversi, il cui nome, suggerito da Richard Stalla e man, sta per Portable Operating System Interface, ma la X nale denuncia la sua stretta relazione con i sistemi Unix. Esso nasce dal lavoro dellIEEE (Institute of Electrical and Electronics Engeneers) che ne produsse una prima versione, nota come IEEE 1003.1-1988, mirante a standardizzare linterfaccia con il sistema operativo. Ma gli standard POSIX non si limitano alla standardizzazione delle funzioni di libreria, e in seguito sono stati prodotti anche altri standard per la shell e i comandi di sistema (1003.2), per le estensioni real-time e per i thread (1003.1d e 1003.1c) e vari altri. In tab. 1.3 ` riportata una e classicazione sommaria dei principali documenti prodotti, e di come sono identicati fra IEEE ed ISO; si tenga conto inoltre che molto spesso si usa lestensione IEEE anche come aggiunta al nome POSIX (ad esempio si pu` parlare di POSIX.4 come di POSIX.1b). o Si tenga presente inoltre che nuove speciche e proposte di standardizzazione si aggiungono continuamente, mentre le versioni precedenti vengono riviste; talvolta poi i riferimenti cambiano nome, per cui anche solo seguire le denominazioni usate diventa particolarmente faticoso; una pagina dove si possono recuperare varie (e di norma piuttosto intricate) informazioni ` e http://www.pasc.org/standing/sd11.html.
Standard POSIX.1 POSIX.1a POSIX.2 POSIX.3 POSIX.4 POSIX.4a POSIX.4b POSIX.5 POSIX.6 POSIX.8 POSIX.9 POSIX.12 IEEE 1003.1 1003.1a 1003.2 2003 1003.1b 1003.1c 1003.1d 1003.5 1003.2c,1e 1003.1f 1003.9 1003.1g ISO 9945-1 9945-1 9945-2 TR13210 9945-1 14519 9945-2 9945-1 9945-1 Contenuto Interfacce di base Estensioni a POSIX.1 Comandi Metodi di test Estensioni real-time Thread Ulteriori estensioni real-time Interfaccia per il linguaggio ADA Sicurezza Accesso ai le via rete Interfaccia per il Fortran-77 Socket

Tabella 1.3: Elenco dei vari standard POSIX e relative denominazioni.

Bench linsieme degli standard POSIX siano basati sui sistemi Unix, essi deniscono comune que uninterfaccia di programmazione generica e non fanno riferimento ad una implementazione specica (ad esempio esiste unimplementazione di POSIX.1 anche sotto Windows NT). Lo standard principale resta comunque POSIX.1, che continua ad evolversi; la versione pi` nota, cui u gran parte delle implementazioni fanno riferimento, e che costituisce una base per molti altri tentativi di standardizzazione, ` stata rilasciata anche come standard internazionale con la sigla e ISO/IEC 9945-1:1996. Linux e le glibc implementano tutte le funzioni denite nello standard POSIX.1, queste ultime forniscono in pi` alcune ulteriori capacit` (per funzioni di pattern matching e per la u a manipolazione delle regular expression), che vengono usate dalla shell e dai comandi di sistema e che sono denite nello standard POSIX.2. Nelle versioni pi` recenti del kernel e delle librerie sono inoltre supportate ulteriori funzionau lit` aggiunte dallo standard POSIX.1c per quanto riguarda i thread (vedi cap. 13), e dallo stana dard POSIX.1b per quanto riguarda i segnali e lo scheduling real-time (sez. 9.5.1 e sez. 3.4.3), la misura del tempo, i meccanismi di intercomunicazione (sez. 12.4) e lI/O asincrono (sez. 11.2.3).

1.2.4

Lo standard X/Open

Il consorzio X/Open nacque nel 1984 come consorzio di venditori di sistemi Unix per giungere ad unarmonizzazione delle varie implementazioni. Per far questo inizi` a pubblicare una serie o

10

CAPITOLO 1. LARCHITETTURA DEL SISTEMA

di documentazioni e speciche sotto il nome di X/Open Portability Guide (a cui di norma si fa riferimento con labbreviazione XPGn con n che indica la versione). Nel 1989 il consorzio produsse una terza versione di questa guida particolarmente voluminosa (la X/Open Portability Guide, Issue 3 ), contenente unulteriore standardizzazione dellinterfaccia di sistema di Unix, che venne presa come riferimento da vari produttori. Questo standard, detto anche XPG3 dal nome della suddetta guida, ` sempre basato sullo e standard POSIX.1, ma prevede una serie di funzionalit` aggiuntive fra cui le speciche delle API a (Application Programmable Interface) per linterfaccia graca (X11). Nel 1992 lo standard venne rivisto con una nuova versione della guida, la Issue 4 (da cui la sigla XPG4) che aggiungeva linterfaccia XTI (X Transport Interface) mirante a soppiantare (senza molto successo) linterfaccia dei socket derivata da BSD. Una seconda versione della guida fu rilasciata nel 1994, questa ` nota con il nome di Spec 1170 (dal numero delle interfacce, header e e comandi deniti). Nel 1993 il marchio Unix pass` di propriet` dalla Novell (che a sua volta lo aveva comprato o a dalla AT&T) al consorzio X/Open che inizi` a pubblicare le sue speciche sotto il nome di Single o UNIX Specication, lultima versione di Spec 1170 divent` cos` la prima versione delle Single o UNIX Specication, SUSv1, pi` comunemente nota come Unix 95. u

1.2.5

Gli standard Unix

Nel 1996 la fusione del consorzio X/Open con la Open Software Foundation (nata da un gruppo di aziende concorrenti rispetto ai fondatori di X/Open) port` alla costituzione dellOpen Group, o un consorzio internazionale che raccoglie produttori, utenti industriali, entit` accademiche e a governative. Attualmente il consorzio ` detentore del marchio depositato Unix, e prosegue il lavoro di e standardizzazione delle varie implementazioni, rilasciando periodicamente nuove speciche e strumenti per la verica della conformit` alle stesse. a Nel 1997 fu annunciata la seconda versione delle Single UNIX Specication, nota con la sigla SUSv2, in questa versione le interfacce specicate salgono a 1434 (e 3030 se si considerano le stazioni di lavoro grache, per le quali sono inserite pure le interfacce usate da CDE che richiede sia X11 che Motif). La conformit` a questa versione permette luso del nome Unix 98, usato a spesso anche per riferirsi allo standard.

1.2.6

Lo standard BSD

Lo sviluppo di BSD inizi` quando la ne della collaborazione fra lUniversit` di Berkeley e la o a AT&T gener` una delle prime e pi` importanti fratture del mondo Unix. LUniversit` di Berkley o u a prosegu` nello sviluppo della base di codice di cui disponeva, e che presentava parecchie migliorie rispetto alle versioni allora disponibili, no ad arrivare al rilascio di una versione completa di Unix, chiamata appunto BSD, del tutto indipendente dal codice della AT&T. Bench BSD non sia uno standard formalizzato, limplementazione di Unix dellUniversit` e a di Berkeley, ha provveduto nel tempo una serie di estensioni e API di grande rilievo, come i link simbolici, la funzione select ed i socket. Queste estensioni sono state via via aggiunte al sistema nelle varie versioni del sistema (BSD 4.2, BSD 4.3 e BSD 4.4) come pure in alcuni derivati commerciali come SunOS. Il kernel Linux e le glibc provvedono tutte queste estensioni che sono state in gran parte incorporate negli standard successivi.

1.2.7

Lo standard System V

Come noto Unix nasce nei laboratori della AT&T, che ne registr` il nome come marchio depoo sitato, sviluppandone una serie di versioni diverse; nel 1983 la versione supportata ucialmente

1.2. GLI STANDARD

11

venne rilasciata al pubblico con il nome di Unix System V. Negli anni successivi lAT&T prosegu` lo sviluppo rilasciando varie versioni con aggiunte e integrazioni; nel 1989 un accordo fra vari venditori (AT&T, Sun, HP, e altro) port` ad una versione che provvedeva ununicazione delle o interfacce comprendente Xenix e BSD, la System V release 4. Linterfaccia di questa ultima release ` descritta in un documento dal titolo System V Intere face Description, o SVID; spesso per` si fa riferimento a questo standard con il nome della sua o implementazione, usando la sigla SVr4. Anche questo costituisce un sovrainsieme delle interfacce denite dallo standard POSIX. Nel 1992 venne rilasciata una seconda versione del sistema: la SVr4.2. Lanno successivo la divisione della AT&T (gi` a suo tempo rinominata in Unix System Laboratories) venne acquistata dalla a Novell, che poi trasfer` il marchio Unix al consorzio X/Open; lultima versione di System V fu la SVr4.2MP rilasciata nel Dicembre 93. Linux e le glibc implementano le principali funzionalit` richieste da SVID che non sono gi` a a incluse negli standard POSIX ed ANSI C, per compatibilit` con lo Unix System V e con altri a Unix (come SunOS) che le includono. Tuttavia le funzionalit` pi` oscure e meno utilizzate (che a u non sono presenti neanche in System V) sono state tralasciate. Le funzionalit` implementate sono principalmente il meccanismo di intercomunicazione fra a i processi e la memoria condivisa (il cosiddetto System V IPC, che vedremo in sez. 12.2) le funzioni della famiglia hsearch e drand48, fmtmsg e svariate funzioni matematiche.

1.2.8

Il comportamento standard del gcc e delle glibc

In Linux, grazie alle glibc, gli standard appena descritti sono ottenibili sia attraverso luso di opzioni del compilatore (il gcc) che denendo opportune costanti prima dellinclusione dei le degli header. Se si vuole che i programmi seguano una stretta attinenza allo standard ANSI C si pu` usare o lopzione -ansi del compilatore, e non potr` essere utilizzata nessuna funzione non riconosciuta a dalle speciche standard ISO per il C. Per attivare le varie opzioni ` possibile denire le macro di preprocessore, che controllano e le funzionalit` che le glibc possono mettere a disposizione: questo pu` essere fatto attraverso a o lopzione -D del compilatore, ma ` buona norma inserire gli opportuni #define nei propri header e le. Le macro disponibili per i vari standard sono le seguenti: _POSIX_SOURCE denendo questa macro si rendono disponibili tutte le funzionalit` dello stana dard POSIX.1 (la versione IEEE Standard 1003.1) insieme a tutte le funzionalit` dello standard ISO C. Se viene anche denita con un intero positivo la a macro _POSIX_C_SOURCE lo stato di questa non viene preso in considerazione. denendo questa macro ad un valore intero positivo si controlla quale livello delle funzionalit` specicate da POSIX viene messa a disposizione; pi` alto a u ` il valore maggiori sono le funzionalit`. Se ` uguale a 1 vengono attivate le e a e funzionalit` specicate nella edizione del 1990 (IEEE Standard 1003.1-1990), a valori maggiori o uguali a 2 attivano le funzionalit` POSIX.2 specicate a nelledizione del 1992 (IEEE Standard 1003.2-1992). Un valore maggiore o uguale a 199309L attiva le funzionalit` POSIX.1b specicate nelledizioa ne del 1993 (IEEE Standard 1003.1b-1993). Un valore maggiore o uguale a 199506L attiva le funzionalit` POSIX.1 specicate nelledizione del 1996 a (ISO/IEC 9945-1: 1996). Valori superiori abiliteranno ulteriori estensioni. _BSD_SOURCE denendo questa macro si attivano le funzionalit` derivate da BSD4.3, insiea me a quelle previste dagli standard ISO C, POSIX.1 e POSIX.2. Alcune delle

_POSIX_C_SOURCE

12

CAPITOLO 1. LARCHITETTURA DEL SISTEMA funzionalit` previste da BSD sono per` in conitto con le corrispondenti dea o nite nello standard POSIX.1, in questo caso le denizioni previste da BSD4.3 hanno la precedenza rispetto a POSIX. A causa della natura dei conitti con POSIX per ottenere una piena compatibilit` con BSD4.3 ` necessario anche a e usare una libreria di compatibilit`, dato che alcune funzioni sono denite in a modo diverso. In questo caso occorre pertanto anche usare lopzione -lbsdcompat con il compilatore per indicargli di utilizzare le versioni nella libreria di compatibilit` prima di quelle normali. a

_SVID_SOURCE

denendo questa macro si attivano le funzionalit` derivate da SVID. Esse a comprendono anche quelle denite negli standard ISO C, POSIX.1, POSIX.2, e X/Open. denendo questa macro si attivano le funzionalit` descritte nella X/Open a Portability Guide. Anche queste sono un sovrainsieme di quelle denite in POSIX.1 e POSIX.2 ed in eetti sia _POSIX_SOURCE che _POSIX_C_SOURCE vengono automaticamente denite. Sono incluse anche ulteriori funzionalit` a disponibili in BSD e SVID. Se il valore della macro ` posto a 500 questo e include anche le nuove denizioni introdotte con la Single UNIX Specication, version 2, cio` Unix98. e

_XOPEN_SOURCE

_XOPEN_SOURCE_EXTENDED denendo questa macro si attivano le ulteriori funzionalit` necessarie ad a essere conformi al rilascio del marchio X/Open Unix. _ISOC99_SOURCE denendo questa macro si attivano le funzionalit` previste per la revisione a delle librerie standard del C denominato ISO C99. Dato che lo standard non ` ancora adottato in maniera ampia queste non sono abilitate automaticae mente, ma le glibc hanno gi` unimplementazione completa che pu` essere a o attivata denendo questa macro. _LARGEFILE_SOURCE denendo questa macro si attivano le funzionalit` per il supporto dei le di a grandi dimensioni (il Large File Support o LFS) con indici e dimensioni a 64 bit. _GNU_SOURCE denendo questa macro si attivano tutte le funzionalit` disponibili: ISO C89, a ISO C99, POSIX.1, POSIX.2, BSD, SVID, X/Open, LFS pi` le estensioni u speciche GNU. Nel caso in cui BSD e POSIX coniggano viene data la precedenza a POSIX.

In particolare ` da sottolineare che le glibc supportano alcune estensioni speciche GNU, e che non sono comprese in nessuno degli standard citati. Per poterle utilizzare esse devono essere attivate esplicitamente denendo la macro _GNU_SOURCE prima di includere i vari header le.

Capitolo 2

Linterfaccia base con i processi


Come accennato nellintroduzione il processo ` lunit` di base con cui un sistema unix-like alloca e a ed utilizza le risorse. Questo capitolo tratter` linterfaccia base fra il sistema e i processi, come a vengono passati gli argomenti, come viene gestita e allocata la memoria, come un processo pu` o richiedere servizi al sistema e cosa deve fare quando ha nito la sua esecuzione. Nella sezione nale accenneremo ad alcune problematiche generiche di programmazione. In genere un programma viene eseguito quando un processo lo fa partire eseguendo una funzione della famiglia exec; torneremo su questo e sulla creazione e gestione dei processi nel prossimo capitolo. In questo aronteremo lavvio e il funzionamento di un singolo processo partendo dal punto di vista del programma che viene messo in esecuzione.

2.1

Esecuzione e conclusione di un programma

Uno dei concetti base di Unix ` che un processo esegue sempre uno ed un solo programma: si e possono avere pi` processi che eseguono lo stesso programma ma ciascun processo vedr` la sua u a copia del codice (in realt` il kernel fa s` che tutte le parti uguali siano condivise), avr` un suo a a spazio di indirizzi, variabili proprie e sar` eseguito in maniera completamente indipendente da a 1 tutti gli altri.

2.1.1

La funzione main

Quando un programma viene lanciato il kernel esegue un opportuno codice di avvio, usando il programma ld-linux.so. Questo programma prima carica le librerie condivise che servono al programma, poi eettua il collegamento dinamico del codice e alla ne lo esegue. Infatti, a meno di non aver specicato il ag -static durante la compilazione, tutti i programmi in Linux sono incompleti e necessitano di essere collegati alle librerie condivise quando vengono avviati. La procedura ` controllata da alcune variabili di ambiente e dal contenuto di /etc/ld.so.conf. I e dettagli sono riportati nella pagina di manuale di ld.so. Il sistema fa partire qualunque programma chiamando la funzione main; sta al programmatore chiamare cos` la funzione principale del programma da cui si suppone iniziare lesecuzione; in ogni caso senza questa funzione lo stesso linker (si chiama cos` il programma che eettua i collegamenti di cui sopra) darebbe luogo ad errori. Lo standard ISO C specica che la funzione main pu` non avere argomenti o prendere due argomenti che rappresentano gli argomenti passati o da linea di comando, in sostanza un prototipo che va sempre bene ` il seguente: e int main ( int argc , char * argv [])
questo non ` del tutto vero nel caso di un programma multi-thread, ma la gestione dei thread in Linux sar` e a trattata a parte.
1

13

14

CAPITOLO 2. LINTERFACCIA BASE CON I PROCESSI

In realt` nei sistemi Unix esiste un altro modo per denire la funzione main, che prevede a la presenza di un terzo argomento, char *envp[], che fornisce (vedi sez. 2.3.4) lambiente del programma; questa forma per` non ` prevista dallo standard POSIX.1 per cui se si vogliono o e scrivere programmi portabili ` meglio evitarla. e

2.1.2

Come chiudere un programma

Normalmente un programma nisce quando la funzione main ritorna, una modalit` equivalente di a chiudere il programma ` quella di chiamare direttamente la funzione exit (che viene comunque e chiamata automaticamente quando main ritorna). Una forma alternativa ` quella di chiamae re direttamente la system call _exit, che restituisce il controllo direttamente alla funzione di conclusione dei processi del kernel. Oltre alla conclusione normale esiste anche la possibilit` di una conclusione anomala del a programma a causa della ricezione di un segnale (tratteremo i segnali in cap. 9) o della chiamata alla funzione abort; torneremo su questo in sez. 3.2.3. Il valore di ritorno della funzione main, o quello usato nelle chiamate ad exit e _exit, viene chiamato stato di uscita (o exit status) e passato al processo che aveva lanciato il programma (in genere la shell). In generale si usa questo valore per fornire informazioni sulla riuscita o il fallimento del programma; linformazione ` necessariamente generica, ed il valore deve essere e compreso fra 0 e 255. La convenzione in uso pressoch universale ` quella di restituire 0 in caso di successo e 1 e e in caso di fallimento; lunica eccezione ` per i programmi che eettuano dei confronti (come e diff), che usano 0 per indicare la corrispondenza, 1 per indicare la non corrispondenza e 2 per ` indicare lincapacit` di eettuare il confronto. E opportuno adottare una di queste convenzioni a a seconda dei casi. Si tenga presente che se si raggiunge la ne della funzione main senza ritornare esplicitamente si ha un valore di uscita indenito, ` pertanto consigliabile di concludere sempre e in maniera esplicita detta funzione. Unaltra convenzione riserva i valori da 128 a 256 per usi speciali: ad esempio 128 viene usato per indicare lincapacit` di eseguire un altro programma in un sottoprocesso. Bench a e questa convenzione non sia universalmente seguita ` una buona idea tenerne conto. e Si tenga presente inoltre che non ` una buona idea usare il codice di errore restituito dalla e variabile errno (per i dettagli si veda sez. 8.5) come stato di uscita. In generale infatti una shell non si cura del valore se non per vedere se ` diverso da zero; inoltre il valore dello stato di uscita e ` sempre troncato ad 8 bit, per cui si potrebbe incorrere nel caso in cui restituendo un codice e di errore 256, si otterrebbe uno stato di uscita uguale a zero, che verrebbe interpretato come un successo. In stdlib.h sono denite, seguendo lo standard POSIX, le due costanti EXIT_SUCCESS e EXIT_FAILURE, da usare sempre per specicare lo stato di uscita di un processo. In Linux esse sono poste rispettivamente ai valori di tipo int 0 e 1.

2.1.3

Le funzioni exit e _exit

Come accennato le funzioni usate per eettuare unuscita normale da un programma sono due, la prima ` la funzione exit, che ` denita dallo standard ANSI C ed il cui prototipo `: e e e
#include <stdlib.h> void exit(int status) Causa la conclusione ordinaria del programma. La funzione non ritorna. Il processo viene terminato.

La funzione exit ` pensata per eseguire una conclusione pulita di un programma che usi e le librerie standard del C; essa esegue tutte le funzioni che sono state registrate con atexit

2.1. ESECUZIONE E CONCLUSIONE DI UN PROGRAMMA

15

e on_exit (vedi sez. 2.1.4), e chiude tutti gli stream eettuando il salvataggio dei dati sospesi (chiamando fclose, vedi sez. 7.2.1), inne passa il controllo al kernel chiamando _exit e restituendo il valore di status come stato di uscita. La system call _exit restituisce direttamente il controllo al kernel, concludendo immediatamente il processo; i dati sospesi nei buer degli stream non vengono salvati e le eventuali funzioni registrate con atexit e on_exit non vengono eseguite. Il prototipo della funzione `: e
#include <unistd.h> void _exit(int status) Causa la conclusione immediata del programma. La funzione non ritorna. Il processo viene terminato.

La funzione chiude tutti i le descriptor appartenenti al processo (si tenga presente che questo non comporta il salvataggio dei dati buerizzati degli stream), fa s` che ogni glio del processo sia adottato da init (vedi cap. 3), manda un segnale SIGCHLD al processo padre (vedi sez. 9.2.6) ed inne ritorna lo stato di uscita specicato in status che pu` essere raccolto usando o la funzione wait (vedi sez. 3.2.4).

2.1.4

Le funzioni atexit e on_exit

Unesigenza comune che si incontra nella programmazione ` quella di dover eettuare una serie e di operazioni di pulizia (ad esempio salvare dei dati, ripristinare delle impostazioni, eliminare dei le temporanei, ecc.) prima della conclusione di un programma. In genere queste operazioni vengono fatte in unapposita sezione del programma, ma quando si realizza una libreria diventa antipatico dover richiedere una chiamata esplicita ad una funzione di pulizia al programmatore che la utilizza. ` E invece molto meno soggetto ad errori, e completamente trasparente allutente, avere la possibilit` di eettuare automaticamente la chiamata ad una funzione che eettui tali operazioni a alluscita dal programma. A questo scopo lo standard ANSI C prevede la possibilit` di registrare a un certo numero di funzioni che verranno eseguite alluscita dal programma (sia per la chiamata ad exit che per il ritorno di main). La prima funzione che si pu` utilizzare a tal ne ` atexit o e il cui prototipo `: e
#include <stdlib.h> void atexit(void (*function)(void)) Registra la funzione function per la chiamata alluscita dal programma. La funzione restituisce 0 in caso di successo e -1 in caso di fallimento, errno non viene modicata.

la funzione richiede come argomento lindirizzo di una opportuna funzione di pulizia da chiamare alluscita del programma, che non deve prendere argomenti e non deve ritornare niente (deve essere cio` denita come void function(void)). e Unestensione di atexit ` la funzione on_exit, che le glibc includono per compatibilit` con e a SunOS, ma che non ` detto sia denita su altri sistemi; il suo prototipo `: e e
#include <stdlib.h> void on_exit(void (*function)(int , void *), void *arg) Registra la funzione function per la chiamata alluscita dal programma. La funzione restituisce 0 in caso di successo e -1 in caso di fallimento, errno non viene modicata.

In questo caso la funzione da chiamare alluscita prende i due argomenti specicati nel prototipo, dovr` cio` essere denita come void function(int status, void *argp). Il primo a e argomento sar` inizializzato allo stato di uscita con cui ` stata chiamata exit ed il secondo al a e puntatore arg passato come secondo argomento di on_exit. Cos` diventa possibile passare dei dati alla funzione di chiusura.

16

CAPITOLO 2. LINTERFACCIA BASE CON I PROCESSI

Nella sequenza di chiusura tutte le funzioni registrate verranno chiamate in ordine inverso rispetto a quello di registrazione (ed una stessa funzione registrata pi` volte sar` chiamata pi` u a u volte); poi verranno chiusi tutti gli stream aperti, inne verr` chiamata _exit. a

2.1.5

Conclusioni

Data limportanza dellargomento ` opportuno sottolineare ancora una volta che in un sistema e Unix lunico modo in cui un programma pu` essere eseguito dal kernel ` attraverso la chiamata o e alla system call execve (o attraverso una delle funzioni della famiglia exec che vedremo in sez. 3.2.5). Allo stesso modo lunico modo in cui un programma pu` concludere volontariamente la sua o esecuzione ` attraverso una chiamata alla system call _exit, o esplicitamente, o in maniera e indiretta attraverso luso di exit o il ritorno di main. Uno schema riassuntivo che illustra le modalit` con cui si avvia e conclude normalmente un a programma ` riportato in g. 2.1. e

Figura 2.1: Schema dellavvio e della conclusione di un programma.

Si ricordi inne che un programma pu` anche essere interrotto dallesterno attraverso luso o di un segnale (modalit` di conclusione non mostrata in g. 2.1); tratteremo nei dettagli i segnali a e la loro gestione nel capitolo 9.

2.2

I processi e luso della memoria

Una delle risorse base che ciascun processo ha a disposizione ` la memoria, e la gestione della e memoria ` appunto uno degli aspetti pi` complessi di un sistema unix-like. In questa sezione, e u dopo una breve introduzione ai concetti base, esamineremo come la memoria viene vista da parte di un programma in esecuzione, e le varie funzioni utilizzabili per la sua gestione.

2.2.1

I concetti generali

Ci sono vari modi in cui i sistemi operativi organizzano la memoria, ed i dettagli di basso livello dipendono spesso in maniera diretta dallarchitettura dellhardware, ma quello pi` tipico, usato u dai sistemi unix-like come Linux ` la cosiddetta memoria virtuale che consiste nellassegnare ad e ogni processo uno spazio virtuale di indirizzamento lineare, in cui gli indirizzi vanno da zero ad un qualche valore massimo.2
nel caso di Linux no al kernel 2.2 detto massimo era, per macchine a 32bit, di 2Gb. Con il kernel 2.4 ed il supporto per la high-memory il limite ` stato esteso anche per macchine a 32 bit. e
2

2.2. I PROCESSI E LUSO DELLA MEMORIA

17

Come accennato in cap. 1 questo spazio di indirizzi ` virtuale e non corrisponde alleettiva e posizione dei dati nella RAM del computer; in genere detto spazio non ` neppure continuo e (cio` non tutti gli indirizzi possibili sono utilizzabili, e quelli usabili non sono necessariamente e adiacenti). Per la gestione da parte del kernel la memoria viene divisa in pagine di dimensione ssa,3 e ciascuna pagina nello spazio di indirizzi virtuale ` associata ad un supporto che pu` essere e o una pagina di memoria reale o ad un dispositivo di stoccaggio secondario (come lo spazio disco riservato alla swap, o i le che contengono il codice). Per ciascun processo il kernel si cura di mantenere un mappa di queste corrispondenze nella cosiddetta page table.4 Una stessa pagina di memoria reale pu` fare da supporto a diverse pagine di memoria virtuale o appartenenti a processi diversi (come accade in genere per le pagine che contengono il codice delle librerie condivise). Ad esempio il codice della funzione printf star` su una sola pagina di a memoria reale che far` da supporto a tutte le pagine di memoria virtuale di tutti i processi che a hanno detta funzione nel loro codice. La corrispondenza fra le pagine della memoria virtuale di un processo e quelle della memoria sica della macchina viene gestita in maniera trasparente dal kernel.5 Poich in genere la memoria e sica ` solo una piccola frazione della memoria virtuale, ` necessario un meccanismo che permetta e e di trasferire le pagine che servono dal supporto su cui si trovano in memoria, eliminando quelle che non servono. Questo meccanismo ` detto paginazione (o paging), ed ` uno dei compiti e e principali del kernel. Quando un processo cerca di accedere ad una pagina che non ` nella memoria reale, avviene e quello che viene chiamato un page fault; la gestione della memoria genera uninterruzione e passa il controllo al kernel il quale sospende il processo e si incarica di mettere in RAM la pagina richiesta (eettuando tutte le operazioni necessarie per reperire lo spazio necessario), per poi restituire il controllo al processo. Dal punto di vista di un processo questo meccanismo ` completamente trasparente, e tutto e avviene come se tutte le pagine fossero sempre disponibili in memoria. Lunica dierenza avvertibile ` quella dei tempi di esecuzione, che passano dai pochi nanosecondi necessari per laccesso e in RAM, a tempi molto pi` lunghi, dovuti allintervento del kernel. u Normalmente questo ` il prezzo da pagare per avere un multitasking reale, ed in genere il e sistema ` molto eciente in questo lavoro; quando per` ci siano esigenze speciche di prestazioni e o ` possibile usare delle funzioni che permettono di bloccare il meccanismo della paginazione e e mantenere sse delle pagine in memoria (vedi sez. 2.2.4). Inoltre per certe applicazioni gli algoritmi di gestione della memoria

2.2.2

La struttura della memoria di un processo

Bench lo spazio di indirizzi virtuali copra un intervallo molto ampio, solo una parte di essi e ` eettivamente allocato ed utilizzabile dal processo; il tentativo di accedere ad un indirizzo e non allocato ` un tipico errore che si commette quando si ` manipolato male un puntatore e e e genera quello che viene chiamato un segmentation fault. Se si tenta cio` di leggere o scrivere e da un indirizzo per il quale non esiste unassociazione della pagina virtuale, il kernel risponde al relativo page fault mandando un segnale SIGSEGV al processo, che normalmente ne causa la terminazione immediata.
inizialmente questi erano di 4kb sulle macchine a 32 bit e di 8kb sulle alpha, con le versioni pi` recenti del u kernel ` possibile anche utilizzare pagine di dimensioni maggiori (4Mb), per sistemi con grandi quantitativi di e memoria in cui luso di pagine troppo piccole comporta una perdita di prestazioni. 4 questa ` una semplicazione brutale, il meccanismo ` molto pi` complesso; una buona trattazione di come e e u Linux gestisce la memoria virtuale si trova su [3]. 5 in genere con lausilio dellhardware di gestione della memoria (la Memory Management Unit del processore), con i kernel della serie 2.6 ` comunque diventato possibile utilizzare Linux anche su architetture che non dispongono e di una MMU.
3

18

CAPITOLO 2. LINTERFACCIA BASE CON I PROCESSI

` E pertanto importante capire come viene strutturata la memoria virtuale di un processo. Essa viene divisa in segmenti, cio` un insieme contiguo di indirizzi virtuali ai quali il processo e pu` accedere. Solitamente un programma C viene suddiviso nei seguenti segmenti: o 1. Il segmento di testo o text segment. Contiene il codice del programma, delle funzioni di librerie da esso utilizzate, e le costanti. Normalmente viene condiviso fra tutti i processi che eseguono lo stesso programma (e anche da processi che eseguono altri programmi nel caso delle librerie). Viene marcato in sola lettura per evitare sovrascritture accidentali (o maliziose) che ne modichino le istruzioni. Viene allocato da exec allavvio del programma e resta invariato per tutto il tempo dellesecuzione. 2. Il segmento dei dati o data segment. Contiene le variabili globali (cio` quelle denite al di e fuori di tutte le funzioni che compongono il programma) e le variabili statiche (cio` quelle e dichiarate con lattributo static). Di norma ` diviso in due parti. e La prima parte ` il segmento dei dati inizializzati, che contiene le variabili il cui valore ` e e stato assegnato esplicitamente. Ad esempio se si denisce: double pi = 3.14; questo valore sar` immagazzinato in questo segmento. La memoria di questo segmento a viene preallocata allavvio del programma e inizializzata ai valori specicati. La seconda parte ` il segmento dei dati non inizializzati, che contiene le variabili il cui e valore non ` stato assegnato esplicitamente. Ad esempio se si denisce: e int vect [100]; questo vettore sar` immagazzinato in questo segmento. Anchesso viene allocato allavvio, a e tutte le variabili vengono inizializzate a zero (ed i puntatori a NULL).6 Storicamente questa seconda parte del segmento dati viene chiamata BSS (da Block Started by Symbol ). La sua dimensione ` ssa. e 3. Lo heap. Tecnicamente lo si pu` considerare lestensione del segmento dati, a cui di solito ` o e ` qui che avviene lallocazione dinamica della memoria; pu` essere posto giusto di seguito. E o ridimensionato allocando e disallocando la memoria dinamica con le apposite funzioni (vedi sez. 2.2.3), ma il suo limite inferiore (quello adiacente al segmento dati) ha una posizione ssa. 4. Il segmento di stack, che contiene quello che viene chiamato stack del programma. Tutte le volte che si eettua una chiamata ad una funzione ` qui che viene salvato lindirizzo e di ritorno e le informazioni dello stato del chiamante (tipo il contenuto di alcuni registri della CPU), poi la funzione chiamata alloca qui lo spazio per le sue variabili locali. Tutti questi dati vengono impilati (da questo viene il nome stack ) in sequenza uno sullaltro; in questo modo le funzioni possono essere chiamate ricorsivamente. Al ritorno della funzione lo spazio ` automaticamente rilasciato e ripulito.7 e La dimensione di questo segmento aumenta seguendo la crescita dello stack del programma, ma non viene ridotta quando questultimo si restringe.
si ricordi che questo vale solo per le variabili che vanno nel segmento dati, e non ` aatto vero in generale. e il compilatore si incarica di generare automaticamente il codice necessario, seguendo quella che viene chiamata una calling convention; quella standard usata con il C ed il C++ ` detta cdecl e prevede che gli argomenti siano e caricati nello stack dal chiamante da destra a sinistra, e che si il chiamante stesso ad eseguire la ripulitura dello stack al ritorno della funzione, se ne possono per` utilizzare di alternative (ad esempio nel pascal gli argomenti o sono inseriti da sinistra a destra ed ` compito del chiamato ripulire lo stack), in genere non ci si deve preoccupare e di questo ntanto che non si mescolano funzioni scritte con linguaggi diversi.
7 6

2.2. I PROCESSI E LUSO DELLA MEMORIA

19

Figura 2.2: Disposizione tipica dei segmenti di memoria di un processo.

Una disposizione tipica dei vari segmenti (testo, heap, stack, ecc.) ` riportata in g. 2.2. e Usando il comando size su un programma se ne pu` stampare le dimensioni dei segmenti di o testo e di dati (inizializzati e BSS); si tenga presente per` che il BSS non ` mai salvato sul le che o e contiene leseguibile, dato che viene sempre inizializzato a zero al caricamento del programma.

2.2.3

Allocazione della memoria per i programmi C

Il C supporta direttamente, come linguaggio di programmazione, soltanto due modalit` di a allocazione della memoria: lallocazione statica e lallocazione automatica. Lallocazione statica ` quella con cui sono memorizzate le variabili globali e le variabili e statiche, cio` le variabili il cui valore deve essere mantenuto per tutta la durata del programma. e Come accennato queste variabili vengono allocate nel segmento dei dati allavvio del programma (come parte delle operazioni svolte da exec) e lo spazio da loro occupato non viene liberato no alla sua conclusione. Lallocazione automatica ` quella che avviene per gli argomenti di una funzione e per le e sue variabili locali (le cosiddette variabili automatiche), che esistono solo per la durata della funzione. Lo spazio per queste variabili viene allocato nello stack quando viene eseguita la funzione e liberato quando si esce dalla medesima. Esiste per` un terzo tipo di allocazione, lallocazione dinamica della memoria, che non ` o e prevista direttamente allinterno del linguaggio C, ma che ` necessaria quando il quantitativo di e memoria che serve ` determinabile solo durante il corso dellesecuzione del programma. e Il C non consente di usare variabili allocate dinamicamente, non ` possibile cio` denire in fase e e

20

CAPITOLO 2. LINTERFACCIA BASE CON I PROCESSI

di programmazione una variabile le cui dimensioni possano essere modicate durante lesecuzione del programma. Per questo le librerie del C forniscono una serie opportuna di funzioni per eseguire lallocazione dinamica di memoria (in genere nello heap). Le variabili il cui contenuto ` allocato in questo modo non potranno essere usate direttamente e come le altre (quelle nello stack ), ma laccesso sar` possibile solo in maniera indiretta, attraverso a i puntatori alla memoria loro riservata che si sono ottenuti dalle funzioni di allocazione. Le funzioni previste dallo standard ANSI C per la gestione della memoria sono quattro: malloc, calloc, realloc e free, i loro prototipi sono i seguenti:
#include <stdlib.h> void *calloc(size_t nmemb, size_t size) Alloca nello heap unarea di memoria per un vettore di nmemb membri di size byte di dimensione. La memoria viene inizializzata a 0. La funzione restituisce il puntatore alla zona di memoria allocata in caso di successo e NULL in caso di fallimento, nel qual caso errno assumer` il valore ENOMEM. a void *malloc(size_t size) Alloca size byte nello heap. La memoria non viene inizializzata. La funzione restituisce il puntatore alla zona di memoria allocata in caso di successo e NULL in caso di fallimento, nel qual caso errno assumer` il valore ENOMEM. a void *realloc(void *ptr, size_t size) Cambia la dimensione del blocco allocato allindirizzo ptr portandola a size. La funzione restituisce il puntatore alla zona di memoria allocata in caso di successo e NULL in caso di fallimento, nel qual caso errno assumer` il valore ENOMEM. a void free(void *ptr) Disalloca lo spazio di memoria puntato da ptr. La funzione non ritorna nulla e non riporta errori.

Il puntatore ritornato dalle funzioni di allocazione ` garantito essere sempre allineato core rettamente per tutti i tipi di dati; ad esempio sulle macchine a 32 bit in genere ` allineato a e multipli di 4 byte e sulle macchine a 64 bit a multipli di 8 byte. In genere si usano le funzioni malloc e calloc per allocare dinamicamente la quantit` di a 8 e siccome i puntatori ritornati sono di tipo memoria necessaria al programma indicata da size, generico non ` necessario eettuare un cast per assegnarli a puntatori al tipo di variabile per la e quale si eettua lallocazione. La memoria allocata dinamicamente deve essere esplicitamente rilasciata usando free9 una volta che non sia pi` necessaria. Questa funzione vuole come argomento un puntatore restituiu to da una precedente chiamata a una qualunque delle funzioni di allocazione che non sia gi` a stato liberato da unaltra chiamata a free, in caso contrario il comportamento della funzione ` e indenito. La funzione realloc si usa invece per cambiare (in genere aumentare) la dimensione di unarea di memoria precedentemente allocata, la funzione vuole in ingresso il puntatore restituito dalla precedente chiamata ad una malloc (se ` passato un valore NULL allora la funzione si e 10 ad esempio quando si deve far crescere la dimensione di un vettore. In comporta come malloc) questo caso se ` disponibile dello spazio adiacente al precedente la funzione lo utilizza, altrimenti e rialloca altrove un blocco della dimensione voluta, copiandoci automaticamente il contenuto; lo spazio aggiunto non viene inizializzato. Si deve sempre avere ben presente il fatto che il blocco di memoria restituito da realloc pu` non essere unestensione di quello che gli si ` passato in ingresso; per questo si dovr` o e a
queste funzioni presentano un comportamento diverso fra le glibc e le uClib quando il valore di size ` nullo. e Nel primo caso viene comunque restituito un puntatore valido, anche se non ` chiaro a cosa esso possa fare e riferimento, nel secondo caso viene restituito NULL. Il comportamento ` analogo con realloc(NULL, 0). e 9 le glibc provvedono anche una funzione cfree denita per compatibilit` con SunOS, che ` deprecata. a e 10 questo ` vero per Linux e limplementazione secondo lo standard ANSI C, ma non ` vero per alcune vecchie e e implementazioni, inoltre alcune versioni delle librerie del C consentivano di usare realloc anche per un puntatore liberato con free purch non ci fossero state nel frattempo altre chiamate a funzioni di allocazione, questa e funzionalit` ` totalmente deprecata e non ` consentita sotto Linux. ae e
8

2.2. I PROCESSI E LUSO DELLA MEMORIA

21

sempre eseguire la riassegnazione di ptr al valore di ritorno della funzione, e reinizializzare o provvedere ad un adeguato aggiornamento di tutti gli altri puntatori allinterno del blocco di dati ridimensionato. Un errore abbastanza frequente (specie se si ha a che fare con vettori di puntatori) ` quello di e chiamare free pi` di una volta sullo stesso puntatore; per evitare questo problema una soluzione u di ripiego ` quella di assegnare sempre a NULL ogni puntatore liberato con free, dato che, quando e largomento ` un puntatore nullo, free non esegue nessuna operazione. e Le glibc hanno unimplementazione delle funzioni di allocazione che ` controllabile dallutente e attraverso alcune variabili di ambiente, in particolare diventa possibile tracciare questo tipo di errori usando la variabile di ambiente MALLOC_CHECK_ che quando viene denita mette in uso una versione meno eciente delle funzioni suddette, che per` ` pi` tollerante nei confronti di oe u piccoli errori come quello di chiamate doppie a free. In particolare: se la variabile ` posta a zero gli errori vengono ignorati; e se ` posta ad 1 viene stampato un avviso sullo standard error (vedi sez. 7.1.3); e se ` posta a 2 viene chiamata abort, che in genere causa limmediata conclusione del e programma. Il problema pi` comune e pi` dicile da risolvere che si incontra con le funzioni di allocazione u u ` quando non viene opportunamente liberata la memoria non pi` utilizzata, quello che in inglese e u viene chiamato memory leak, cio` una perdita di memoria. e Un caso tipico che illustra il problema ` quello in cui in una subroutine si alloca della mee moria per uso locale senza liberarla prima di uscire. La memoria resta cos` allocata no alla terminazione del processo. Chiamate ripetute alla stessa subroutine continueranno ad eettuare altre allocazioni, causando a lungo andare un esaurimento della memoria disponibile (e la probabile impossibilit` di proseguire lesecuzione del programma). a Il problema ` che lesaurimento della memoria pu` avvenire in qualunque momento, in core o rispondenza ad una qualunque chiamata di malloc, che pu` essere in una sezione del codice o che non ha alcuna relazione con la subroutine che contiene lerrore. Per questo motivo ` sempre e molto dicile trovare un memory leak. In C e C++ il problema ` particolarmente sentito. In C++, per mezzo della programmazione e ad oggetti, il problema dei memory leak ` notevolmente ridimensionato attraverso luso accurato e di appositi oggetti come gli smartpointers. Questo per` in genere va a scapito delle prestazioni o dellapplicazione in esecuzione. Per limitare limpatto di questi problemi, e semplicare la ricerca di eventuali errori, limplementazione delle funzioni di allocazione delle glibc mette a disposizione una serie di funzionalit` a che permettono di tracciare le allocazioni e le disallocazioni, e denisce anche una serie di possibili hook (ganci) che permettono di sostituire alle funzioni di libreria una propria versione (che pu` o essere pi` o meno specializzata per il debugging). Esistono varie librerie che forniscono dei sostiu tuti opportuni delle funzioni di allocazione in grado, senza neanche ricompilare il programma,11 di eseguire diagnostiche anche molto complesse riguardo lallocazione della memoria. Una possibile alternativa alluso di malloc, che non sore dei problemi di memory leak descritti in precedenza, ` la funzione alloca, che invece di allocare la memoria nello heap usa e il segmento di stack della funzione corrente. La sintassi ` identica a quella di malloc, il suo e prototipo `: e
#include <stdlib.h> void *alloca(size_t size) Alloca size byte nello stack. La funzione restituisce il puntatore alla zona di memoria allocata in caso di successo e NULL in caso di fallimento, nel qual caso errno assumer` il valore ENOMEM. a
11

esempi sono Dmalloc http://dmalloc.com/ di Gray Watson ed Electric Fence di Bruce Perens.

22

CAPITOLO 2. LINTERFACCIA BASE CON I PROCESSI

La funzione alloca la quantit` di memoria (non inizializzata) richiesta dallargomento size a nel segmento di stack della funzione chiamante. Con questa funzione non ` pi` necessario liberare e u la memoria allocata (e quindi non esiste un analogo della free) in quanto essa viene rilasciata automaticamente al ritorno della funzione. Come ` evidente questa funzione ha molti vantaggi, anzitutto permette di evitare alla radice i e problemi di memory leak, dato che non serve pi` la deallocazione esplicita; inoltre la deallocazione u automatica funziona anche quando si usa longjmp per uscire da una subroutine con un salto non locale da una funzione (vedi sez. 2.4.4). Un altro vantaggio ` che in Linux la funzione ` molto pi` veloce di malloc e non viene sprecato e e u spazio, infatti non ` necessario gestire un pool di memoria da riservare e si evitano cos` anche e i problemi di frammentazione di questultimo, che comportano inecienze sia nellallocazione della memoria che nellesecuzione dellallocazione. Gli svantaggi sono che questa funzione non ` disponibile su tutti gli Unix, e non ` inserita e e n nello standard POSIX n in SUSv3 (ma ` presente in BSD), il suo utilizzo quindi limita la e e e portabilit` dei programmi. Inoltre la funzione non pu` essere usata nella lista degli argomenti a o di una funzione, perch lo spazio verrebbe allocato nel mezzo degli stessi. e Inoltre non ` chiaramente possibile usare alloca per allocare memoria che deve poi essere e usata anche al di fuori della funzione in cui essa viene chiamata, dato che alluscita dalla funzione lo spazio allocato diventerebbe libero, e potrebbe essere sovrascritto allinvocazione di nuove funzioni. Questo ` lo stesso problema che si pu` avere con le variabili automatiche, su cui e o torneremo in sez. 2.4.3. Le due funzioni seguenti12 vengono utilizzate soltanto quando ` necessario eettuare direte tamente la gestione della memoria associata allo spazio dati di un processo, ad esempio qualora si debba implementare la propria versione delle funzioni di allocazione della memoria. La prima funzione ` brk, ed il suo prototipo `: e e
#include <unistd.h> int brk(void *end_data_segment) Sposta la ne del segmento dei dati. La funzione restituisce 0 in caso di successo e -1 in caso di fallimento, nel qual caso errno assumer` a il valore ENOMEM.

La funzione ` uninterfaccia diretta allomonima system call ed imposta lindirizzo nale del e segmento dati di un processo allindirizzo specicato da end_data_segment. Questultimo deve essere un valore ragionevole, ed inoltre la dimensione totale del segmento non deve comunque eccedere un eventuale limite (si veda sez. 8.3.2) imposto sulle dimensioni massime dello spazio dati del processo. Una seconda funzione per la manipolazione delle dimensioni del segmento dati13 ` sbrk, ed e il suo prototipo `: e
#include <unistd.h> void *sbrk(ptrdiff_t increment) Incrementa la dimensione dello spazio dati. La funzione restituisce il puntatore allinizio della nuova zona di memoria allocata in caso di successo e NULL in caso di fallimento, nel qual caso errno assumer` il valore ENOMEM. a

la funzione incrementa la dimensione lo spazio dati di un programma di increment byte, restituendo il nuovo indirizzo nale dello stesso. Un valore nullo permette di ottenere lattuale posizione della ne del segmento dati.
le due funzioni sono state denite con BSD 4.3, non fanno parte delle librerie standard del C e mentre sono state esplicitamente escluse dallo standard POSIX. 13 in questo caso si tratta soltanto di una funzione di libreria, e non di una system call.
12

2.2. I PROCESSI E LUSO DELLA MEMORIA

23

Queste funzioni sono state deliberatamente escluse dallo standard POSIX.1 e per i programmi normali ` sempre opportuno usare le funzioni di allocazione standard descritte in precedenza, e che sono costruite su di esse.

2.2.4

Il controllo della memoria virtuale

Come spiegato in sez. 2.2.1 il kernel gestisce la memoria virtuale in maniera trasparente ai processi, decidendo quando rimuovere pagine dalla memoria per metterle nello swap, sulla base dellutilizzo corrente da parte dei vari processi. Nelluso comune un processo non deve preoccuparsi di tutto ci`, in quanto il meccanismo o della paginazione riporta in RAM, ed in maniera trasparente, tutte le pagine che gli occorrono; esistono per` esigenze particolari in cui non si vuole che questo meccanismo si attivi. In generale o i motivi per cui si possono avere di queste necessit` sono due: a La velocit`. Il processo della paginazione ` trasparente solo se il programma in esecuzione a e non ` sensibile al tempo che occorre a riportare la pagina in memoria; per questo moe tivo processi critici che hanno esigenze di tempo reale o tolleranze critiche nelle risposte (ad esempio processi che trattano campionamenti sonori) possono non essere in grado di sopportare le variazioni della velocit` di accesso dovuta alla paginazione. a In certi casi poi un programmatore pu` conoscere meglio dellalgoritmo di allocazione o delle pagine le esigenze speciche del suo programma e decidere quali pagine di memoria ` e opportuno che restino in memoria per un aumento delle prestazioni. In genere queste sono esigenze particolari e richiedono anche un aumento delle priorit` in esecuzione del processo a (vedi sez. 3.4.3). La sicurezza. Se si hanno password o chiavi segrete in chiaro in memoria queste possono essere portate su disco dal meccanismo della paginazione. Questo rende pi` lungo il periodo u di tempo in cui detti segreti sono presenti in chiaro e pi` complessa la loro cancellazione u (un processo pu` cancellare la memoria su cui scrive le sue variabili, ma non pu` toccare o o lo spazio disco su cui una pagina di memoria pu` essere stata salvata). Per questo motivo o di solito i programmi di crittograa richiedono il blocco di alcune pagine di memoria. Il meccanismo che previene la paginazione di parte della memoria virtuale di un processo ` e chiamato memory locking (o blocco della memoria). Il blocco ` sempre associato alle pagine della e memoria virtuale del processo, e non al segmento reale di RAM su cui essa viene mantenuta. La regola ` che se un segmento di RAM fa da supporto ad almeno una pagina bloccata allora esso e viene escluso dal meccanismo della paginazione. I blocchi non si accumulano, se si blocca due volte la stessa pagina non ` necessario sbloccarla due volte, una pagina o ` bloccata oppure no. e e Il memory lock persiste ntanto che il processo che detiene la memoria bloccata non la sblocca. Chiaramente la terminazione del processo comporta anche la ne delluso della sua memoria virtuale, e quindi anche di tutti i suoi memory lock. Inne i memory lock non sono ereditati dai processi gli,14 e vengono automaticamente rimossi se si pone in esecuzione un altro programma con exec (vedi sez. 3.2.5). Siccome la richiesta di un memory lock da parte di un processo riduce la memoria sica disponibile nel sistema, questo ha un evidente impatto su tutti gli altri processi, per cui no al kernel 2.6.9 solo un processo con i privilegi opportuni (la capability CAP_IPC_LOCK, vedi sez. 3.3.4) aveva la capacit` di bloccare una pagina. a Il sistema pone dei limiti allammontare di memoria di un processo che pu` essere bloccata o e al totale di memoria sica che si pu` dedicare a questo, lo standard POSIX.1 richiede che o
ma siccome Linux usa il copy on write (vedi sez. 3.2.2) gli indirizzi virtuali del glio sono mantenuti sullo stesso segmento di RAM del padre, quindi ntanto che un glio non scrive su un segmento, pu` usufruire del o memory lock del padre.
14

24

CAPITOLO 2. LINTERFACCIA BASE CON I PROCESSI

sia denita in unistd.h la macro _POSIX_MEMLOCK_RANGE per indicare la capacit` di eseguire a il memory locking. Inoltre in alcuni sistemi ` denita la costante PAGE_SIZE in limits.h per e indicare la dimensione di una pagina in byte.15 A partire dal kernel 2.6.9 anche un processo normale pu` bloccare la propria memoria16 ma o mentre un processo privilegiato non ha limiti sulla quantit` di memoria che pu` bloccare, un a o processo normale ` soggetto al limite della risorsa RLIMIT_MEMLOCK (vedi sez. 8.3.2). In generale e poi ogni processo pu` sbloccare le pagine relative alla propria memoria, se per` diversi processi o o bloccano la stessa pagina questa rester` bloccata ntanto che ci sar` almeno un processo che la a a blocca. Le funzioni per bloccare e sbloccare la paginazione di singole sezioni di memoria sono mlock e munlock; i loro prototipi sono:
#include <sys/mman.h> int mlock(const void *addr, size_t len) Blocca la paginazione su un intervallo di memoria. int munlock(const void *addr, size_t len) Rimuove il blocco della paginazione su un intervallo di memoria. Entrambe le funzioni ritornano 0 in caso di successo e -1 in caso di errore, nel qual caso errno assumer` uno dei valori seguenti: a ENOMEM EINVAL EPERM alcuni indirizzi dellintervallo specicato non corrispondono allo spazio di indirizzi del processo o si ` ecceduto il numero massimo consentito di pagine bloccate. e len non ` un valore positivo. e con un kernel successivo al 2.6.9 il processo non ` privilegiato e si un limite nullo per e RLIMIT_MEMLOCK.

e, per mlock, anche EPERM quando il processo non ha i privilegi richiesti per loperazione.

Le due funzioni permettono rispettivamente di bloccare e sbloccare la paginazione per lintervallo di memoria specicato dagli argomenti, che ne indicano nellordine lindirizzo iniziale e la lunghezza. Tutte le pagine che contengono una parte dellintervallo bloccato sono mantenute in RAM per tutta la durata del blocco.17 Altre due funzioni, mlockall e munlockall, consentono di bloccare genericamente la paginazione per lintero spazio di indirizzi di un processo. I prototipi di queste funzioni sono:
#include <sys/mman.h> int mlockall(int flags) Blocca la paginazione per lo spazio di indirizzi del processo corrente. int munlockall(void) Sblocca la paginazione per lo spazio di indirizzi del processo corrente. Codici di ritorno ed errori sono gli stessi di mlock e munlock, con un kernel successivo al 2.6.9 luso di munlockall senza la capability CAP_IPC_LOCK genera un errore di EPERM.

Largomento flags di mlockall permette di controllarne il comportamento; esso pu` essere o specicato come lOR aritmetico delle due costanti: MCL_CURRENT blocca tutte le pagine correntemente mappate nello spazio di indirizzi del processo. blocca tutte le pagine che verranno mappate nello spazio di indirizzi del processo.

MCL_FUTURE
15 16

con Linux questo non avviene e si deve ricorrere alla funzione getpagesize, vedi sez. 8.3.3. la funzionalit` ` stata introdotta per non essere costretti a dare privilegi eccessivi a programmi di crittograa, ae che necessitano di questa funzionalit`, ma che devono essere usati da utenti normali. a 17 con altri kernel si pu` ottenere un errore di EINVAL se addr non ` un multiplo della dimensione delle pagine o e di memoria.

2.3. ARGOMENTI, OPZIONI ED AMBIENTE DI UN PROCESSO

25

Con mlockall si possono bloccare tutte le pagine mappate nello spazio di indirizzi del processo, sia che comprendano il segmento di testo, di dati, lo stack, lo heap e pure le funzioni di libreria chiamate, i le mappati in memoria, i dati del kernel mappati in user space, la memoria condivisa. Luso dei ag permette di selezionare con maggior nezza le pagine da bloccare, ad esempio limitandosi a tutte le pagine allocate a partire da un certo momento. In ogni caso un processo real-time che deve entrare in una sezione critica deve provvedere a riservare memoria suciente prima dellingresso, per scongiurare loccorrenza di un eventuale page fault causato dal meccanismo di copy on write. Infatti se nella sezione critica si va ad utilizzare memoria che non ` ancora stata riportata in RAM si potrebbe avere un page fault e durante lesecuzione della stessa, con conseguente rallentamento (probabilmente inaccettabile) dei tempi di esecuzione. In genere si ovvia a questa problematica chiamando una funzione che ha allocato una quantit` a sucientemente ampia di variabili automatiche, in modo che esse vengano mappate in RAM dallo stack, dopo di che, per essere sicuri che esse siano state eettivamente portate in memoria, ci si scrive sopra.

2.3

Argomenti, opzioni ed ambiente di un processo

Tutti i programmi hanno la possibilit` di ricevere argomenti e opzioni quando vengono lanciati. a Il passaggio degli argomenti ` eettuato attraverso gli argomenti argc e argv della funzione e main, che vengono passati al programma dalla shell (o dal processo che esegue la exec, secondo le modalit` che vedremo in sez. 3.2.5) quando questo viene messo in esecuzione. a Oltre al passaggio degli argomenti, unaltra modalit` che permette di passare delle infora mazioni che modichino il comportamento di un programma ` quello delluso del cosiddetto e environment (cio` luso delle variabili di ambiente). In questa sezione esamineremo le funzioe ni che permettono di gestire argomenti ed opzioni, e quelle che consentono di manipolare ed utilizzare le variabili di ambiente.

2.3.1

Il formato degli argomenti

In genere il passaggio degli argomenti al programma viene eettuato dalla shell, che si incarica di leggere la linea di comando e di eettuarne la scansione (il cosiddetto parsing) per individuare le parole che la compongono, ciascuna delle quali viene considerata un argomento. Di norma per individuare le parole viene usato come carattere di separazione lo spazio o il tabulatore, ma il comportamento ` modicabile attraverso limpostazione della variabile di ambiente IFS. e

Figura 2.3: Esempio dei valori di argv e argc generati nella scansione di una riga di comando.

Nella scansione viene costruito il vettore di puntatori argv inserendo in successione il puntatore alla stringa costituente ln-simo argomento; la variabile argc viene inizializzata al numero

26

CAPITOLO 2. LINTERFACCIA BASE CON I PROCESSI

di argomenti trovati, in questo modo il primo argomento ` sempre il nome del programma; un e esempio di questo meccanismo ` mostrato in g. 2.3. e

2.3.2

La gestione delle opzioni

In generale un programma Unix riceve da linea di comando sia gli argomenti che le opzioni, queste ultime sono standardizzate per essere riconosciute come tali: un elemento di argv che inizia con il carattere - e che non sia un singolo - o un -- viene considerato unopzione. In genere le opzioni sono costituite da una lettera singola (preceduta dal carattere -) e possono avere o no un parametro associato; un comando tipico pu` essere quello mostrato in g. 2.3. In quel caso o le opzioni sono -r e -m e la prima vuole un parametro mentre la seconda no (questofile.txt ` un argomento del programma, non un parametro di -m). e Per gestire le opzioni allinterno dei argomenti a linea di comando passati in argv le librerie standard del C forniscono la funzione getopt, che ha il seguente prototipo:
#include <unistd.h> int getopt(int argc, char *const argv[], const char *optstring) Esegue il parsing degli argomenti passati da linea di comando riconoscendo le possibili opzioni segnalate con optstring. Ritorna il carattere che segue lopzione, : se manca un parametro allopzione, ? se lopzione ` sconosciuta, e -1 se non esistono altre opzioni. e

Questa funzione prende come argomenti le due variabili argc e argv passate a main ed una stringa che indica quali sono le opzioni valide; la funzione eettua la scansione della lista degli argomenti ricercando ogni stringa che comincia con - e ritorna ogni volta che trova unopzione valida. La stringa optstring indica quali sono le opzioni riconosciute ed ` costituita da tutti i e caratteri usati per identicare le singole opzioni, se lopzione ha un parametro al carattere deve essere fatto seguire un segno di due punti :; nel caso di g. 2.3 ad esempio la stringa di opzioni avrebbe dovuto contenere r:m. La modalit` di uso di getopt ` pertanto quella di chiamare pi` volte la funzione allinterno a e u di un ciclo, ntanto che essa non ritorna il valore -1 che indica che non ci sono pi` opzioni. Nel u caso si incontri unopzione non dichiarata in optstring viene ritornato il carattere ? mentre se unopzione che lo richiede non ` seguita da un parametro viene ritornato il carattere :, e inne se viene incontrato il valore -- la scansione viene considerata conclusa, anche se vi sono altri elementi di argv che cominciano con il carattere -. Quando la funzione trova unopzione essa ritorna il valore numerico del carattere, in questo modo si possono eseguire azioni speciche usando uno switch; getopt inoltre inizializza alcune variabili globali: char *optarg contiene il puntatore alla stringa parametro dellopzione. int optind alla ne della scansione restituisce lindice del primo elemento di argv che non ` unopzione. e int opterr previene, se posto a zero, la stampa di un messaggio di errore in caso di riconoscimento di opzioni non denite. int optopt contiene il carattere dellopzione non riconosciuta. In g. 2.4 ` mostrata la sezione del programma ForkTest.c (che useremo nel prossimo e capitolo per eettuare dei test sulla creazione dei processi) deputata alla decodica delle opzioni a riga di comando. Si pu` notare che si ` anzitutto (1) disabilitata la stampa di messaggi di errore per opzioni o e non riconosciute, per poi passare al ciclo per la verica delle opzioni (2-27); per ciascuna delle opzioni possibili si ` poi provveduto ad unazione opportuna, ad esempio per le tre opzioni che e

2.3. ARGOMENTI, OPZIONI ED AMBIENTE DI UN PROCESSO

27

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28

opterr = 0; /* don t want writing to stderr */ while ( ( i = getopt ( argc , argv , " hp : c : e : " )) != -1) { switch ( i ) { /* * Handling options */ case h : /* help option */ printf ( " Wrong -h option use \ n " ); usage (); return -1; break ; case c : /* take wait time for children */ wait_child = strtol ( optarg , NULL , 10); /* convert input */ break ; case p : /* take wait time for children */ wait_parent = strtol ( optarg , NULL , 10); /* convert input */ break ; case e : /* take wait before parent exit */ wait_end = strtol ( optarg , NULL , 10); /* convert input */ break ; case ? : /* unrecognized options */ printf ( " Unrecognized options -% c \ n " , optopt ); usage (); default : /* should not reached */ usage (); } } debug ( " Optind %d , argc % d \ n " , optind , argc );

Figura 2.4: Esempio di codice per la gestione delle opzioni.

prevedono un parametro si ` eettuata la decodica del medesimo (il cui indirizzo ` contenuto e e nella variabile optarg) avvalorando la relativa variabile (12-14, 15-17 e 18-20). Completato il ciclo troveremo in optind lindice in argv[] del primo degli argomenti rimanenti nella linea di comando. Normalmente getopt compie una permutazione degli elementi di argv cosicch alla ne e della scansione gli elementi che non sono opzioni sono spostati in coda al vettore. Oltre a questa esistono altre due modalit` di gestire gli elementi di argv; se optstring inizia con il carattere a + (o ` impostata la variabile di ambiente POSIXLY_CORRECT) la scansione viene fermata non e appena si incontra un elemento che non ` unopzione. Lultima modalit`, usata quando un e a programma pu` gestire la mescolanza fra opzioni e argomenti, ma se li aspetta in un ordine o denito, si attiva quando optstring inizia con il carattere -. In questo caso ogni elemento che non ` unopzione viene considerato comunque unopzione e associato ad un valore di ritorno e pari ad 1, questo permette di identicare gli elementi che non sono opzioni, ma non eettua il riordinamento del vettore argv.

2.3.3

Opzioni in formato esteso

Unestensione di questo schema ` costituita dalle cosiddette long-options espresse nella forma e --option=parameter, anche la gestione di queste ultime ` stata standardizzata attraverso luso e di una versione estesa di getopt. (NdA: questa parte verr` inserita in seguito). a

28

CAPITOLO 2. LINTERFACCIA BASE CON I PROCESSI

2.3.4

Le variabili di ambiente

Oltre agli argomenti passati a linea di comando ogni processo riceve dal sistema un ambiente, nella forma di una lista di variabili (detta environment list) messa a disposizione dal processo, e costruita nella chiamata alla funzione exec quando questo viene lanciato. Come per la lista degli argomenti anche questa lista ` un vettore di puntatori a caratteri, e ciascuno dei quali punta ad una stringa, terminata da un NULL. A dierenza di argv[] in questo caso non si ha una lunghezza del vettore data da un equivalente di argc, ma la lista ` terminata e da un puntatore nullo. Lindirizzo della lista delle variabili di ambiente ` passato attraverso la variabile globale e environ, a cui si pu` accedere attraverso una semplice dichiarazione del tipo: o extern char ** environ ; un esempio della struttura di questa lista, contenente alcune delle variabili pi` comuni che u normalmente sono denite dal sistema, ` riportato in g. 2.5. e

Figura 2.5: Esempio di lista delle variabili di ambiente.

Per convenzione le stringhe che deniscono lambiente sono tutte del tipo nome=valore. Inoltre alcune variabili, come quelle elencate in g. 2.5, sono denite dal sistema per essere usate da diversi programmi e funzioni: per queste c` lulteriore convenzione di usare nomi espressi in e caratteri maiuscoli.18 Il kernel non usa mai queste variabili, il loro uso e la loro interpretazione ` riservata alle e applicazioni e ad alcune funzioni di libreria; in genere esse costituiscono un modo comodo per denire un comportamento specico senza dover ricorrere alluso di opzioni a linea di comando o di le di congurazione. E di norma cura della shell, quando esegue un comando, passare queste variabili al programma messo in esecuzione attraverso un uso opportuno delle relative chiamate (si veda sez. 3.2.5). La shell ad esempio ne usa molte per il suo funzionamento (come PATH per la ricerca dei comandi, o IFS per la scansione degli argomenti), e alcune di esse (come HOME, USER, ecc.) sono denite al login (per i dettagli si veda sez. 10.1.4). In genere ` cura dellamministratore denire e le opportune variabili di ambiente in uno script di avvio. Alcune servono poi come riferimento generico per molti programmi (come EDITOR che indica leditor preferito da invocare in caso di necessit`). a Gli standard POSIX e XPG3 deniscono alcune di queste variabili (le pi` comuni), come u riportato in tab. 2.1. GNU/Linux le supporta tutte e ne denisce anche altre: per una lista pi` u completa si pu` controllare man 5 environ. o Lo standard ANSI C prevede lesistenza di un ambiente, e pur non entrando nelle speciche
la convenzione vuole che si usino dei nomi maiuscoli per le variabili di ambiente di uso generico, i nomi minuscoli sono in genere riservati alle variabili interne degli script di shell.
18

2.3. ARGOMENTI, OPZIONI ED AMBIENTE DI UN PROCESSO


Variabile USER LOGNAME HOME LANG PATH PWD SHELL TERM PAGER EDITOR BROWSER TMPDIR POSIX XPG3 Linux Descrizione Nome utente Nome di login Directory base dellutente Localizzazione Elenco delle directory dei programmi Directory corrente Shell in uso Tipo di terminale Programma per vedere i testi Editor preferito Browser preferito Directory dei le temporanei

29

Tabella 2.1: Esempi delle variabili di ambiente pi` comuni denite da vari standard. u

di come sono strutturati i contenuti, denisce la funzione getenv che permette di ottenere i valori delle variabili di ambiente; il suo prototipo `: e
#include <stdlib.h> char *getenv(const char *name) Esamina lambiente del processo cercando una stringa che corrisponda a quella specicata da name. La funzione ritorna NULL se non trova nulla, o il puntatore alla stringa che corrisponde (di solito nella forma NOME=valore).

Oltre a questa funzione di lettura, che ` lunica denita dallo standard ANSI C, nellevolue zione dei sistemi Unix ne sono state proposte altre, da utilizzare per impostare e per cancellare le variabili di ambiente. Uno schema delle funzioni previste nei vari standard e disponibili in Linux ` riportato in tab. 2.2. e
Funzione getenv setenv unsetenv putenv clearenv ANSI C POSIX.1 opz. opz. XPG3 SVr4 BSD Linux

Tabella 2.2: Funzioni per la gestione delle variabili di ambiente.

In Linux19 sono denite tutte le funzioni elencate in tab. 2.2. La prima, getenv, labbiamo appena esaminata; delle restanti le prime due, putenv e setenv, servono per assegnare nuove variabili di ambiente, i loro prototipi sono i seguenti:
#include <stdlib.h> int setenv(const char *name, const char *value, int overwrite) Imposta la variabile di ambiente name al valore value. int putenv(char *string) Aggiunge la stringa string allambiente. Entrambe le funzioni ritornano 0 in caso di successo e -1 per un errore, che ` sempre ENOMEM. e

la terza, unsetenv, serve a cancellare una variabile di ambiente; il suo prototipo `: e


#include <stdlib.h> void unsetenv(const char *name) Rimuove la variabile di ambiente name.
19

in realt` nelle libc4 e libc5 sono denite solo le prime quattro, clearenv ` stata introdotta con le glibc 2.0. a e

30

CAPITOLO 2. LINTERFACCIA BASE CON I PROCESSI

questa funzione elimina ogni occorrenza della variabile specicata; se essa non esiste non succede nulla. Non ` prevista (dato che la funzione ` void) nessuna segnalazione di errore. e e Per modicare o aggiungere una variabile di ambiente si possono usare sia setenv che putenv. La prima permette di specicare separatamente nome e valore della variabile di ambiente, inoltre il valore di overwrite specica il comportamento della funzione nel caso la variabile esista gi`, a sovrascrivendola se diverso da zero, lasciandola immutata se uguale a zero. La seconda funzione prende come argomento una stringa analoga a quella restituita da getenv, e sempre nella forma NOME=valore. Se la variabile specicata non esiste la stringa sar` a aggiunta allambiente, se invece esiste il suo valore sar` impostato a quello specicato da string. a Si tenga presente che, seguendo lo standard SUSv2, le glibc successive alla versione 2.1.2 aggiungono20 string alla lista delle variabili di ambiente; pertanto ogni cambiamento alla stringa in questione si riette automaticamente sullambiente, e quindi si deve evitare di passare a questa funzione una variabile automatica (per evitare i problemi esposti in sez. 2.4.3). Si tenga inne presente che se si passa a putenv solo il nome di una variabile (cio` string ` e e nella forma NAME e non contiene un carattere =) allora questa viene cancellata dallambiente. Inne se la chiamata di putenv comporta la necessit` di allocare una nuova versione del vettore a environ questo sar` allocato, ma la versione corrente sar` deallocata solo se anchessa ` risultante a a e da unallocazione fatta in precedenza da unaltra putenv. Questo perch il vettore delle variabili e di ambiente iniziale, creato dalla chiamata ad exec (vedi sez. 3.2.5) ` piazzato al di sopra dello e stack, (vedi g. 2.2) e non nello heap e non pu` essere deallocato. Inoltre la memoria associata o alle variabili di ambiente eliminate non viene liberata. Lultima funzione ` clearenv, che viene usata per cancellare completamente tutto lambiente; e il suo prototipo `: e
#include <stdlib.h> int clearenv(void) Cancella tutto lambiente. la funzione restituisce 0 in caso di successo e un valore diverso da zero per un errore.

In genere si usa questa funzione in maniera precauzionale per evitare i problemi di sicurezza connessi nel trasmettere ai programmi che si invocano un ambiente che pu` contenere dei dati o non controllati. In tal caso si provvede alla cancellazione di tutto lambiente per costruirne una versione sicura da zero.

2.4

Problematiche di programmazione generica

Bench questo non sia un libro di C, ` opportuno arontare alcune delle problematiche generali e e che possono emergere nella programmazione e di quali precauzioni o accorgimenti occorre prendere per risolverle. Queste problematiche non sono speciche di sistemi unix-like o multitasking, ma avendo trattato in questo capitolo il comportamento dei processi visti come entit` a s stanti, a e le riportiamo qui.

2.4.1

Il passaggio delle variabili e dei valori di ritorno

Una delle caratteristiche standard del C ` che le variabili vengono passate alle subroutine attrae verso un meccanismo che viene chiamato by value (diverso ad esempio da quanto avviene con il Fortran, dove le variabili sono passate, come suol dirsi, by reference, o dal C++ dove la modalit` a del passaggio pu` essere controllata con loperatore &). o
il comportamento ` lo stesso delle vecchie libc4 e libc5; nelle glibc, dalla versione 2.0 alla 2.1.1, veniva invece e fatta una copia, seguendo il comportamento di BSD4.4; dato che questo pu` dar luogo a perdite di memoria e o non rispetta lo standard. Il comportamento ` stato modicato a partire dalle 2.1.2, eliminando anche, sempre in e conformit` a SUSv2, lattributo const dal prototipo. a
20

2.4. PROBLEMATICHE DI PROGRAMMAZIONE GENERICA

31

Il passaggio di una variabile by value signica che in realt` quello che viene passato alla a subroutine ` una copia del valore attuale di quella variabile, copia che la subroutine potr` e a modicare a piacere, senza che il valore originale nella funzione chiamante venga toccato. In questo modo non occorre preoccuparsi di eventuali eetti delle operazioni della subroutine sulla variabile passata come argomento. Questo per` va inteso nella maniera corretta. Il passaggio by value vale per qualunque variao bile, puntatori compresi; quando per` in una subroutine si usano dei puntatori (ad esempio per o scrivere in un buer) in realt` si va a modicare la zona di memoria a cui essi puntano, per cui a anche se i puntatori sono copie, i dati a cui essi puntano sono sempre gli stessi, e le eventuali modiche avranno eetto e saranno visibili anche nella funzione chiamante. Nella maggior parte delle funzioni di libreria e delle system call i puntatori vengono usati per scambiare dati (attraverso buer o strutture) e le variabili semplici vengono usate per specicare argomenti; in genere le informazioni a riguardo dei risultati vengono passate alla funzione ` chiamante attraverso il valore di ritorno. E buona norma seguire questa pratica anche nella programmazione normale. Talvolta per` ` necessario che la funzione possa restituire indietro alla funzione chiamante un oe valore relativo ad uno dei suoi argomenti. Per far questo si usa il cosiddetto value result argument, si passa cio`, invece di una normale variabile, un puntatore alla stessa; vedremo alcuni esempi e di questa modalit` nelle funzioni che gestiscono i socket (in sez. 16.2), in cui, per permettere al a kernel di restituire informazioni sulle dimensioni delle strutture degli indirizzi utilizzate, viene usato questo meccanismo.

2.4.2

Il passaggio di un numero variabile di argomenti

Come vedremo nei capitoli successivi, non sempre ` possibile specicare un numero sso di e argomenti per una funzione. Lo standard ISO C prevede nella sua sintassi la possibilit` di a denire delle variadic function che abbiano un numero variabile di argomenti, attraverso luso nella dichiarazione della funzione dello speciale costrutto ..., che viene chiamato ellipsis. Lo standard per` non provvede a livello di linguaggio alcun meccanismo con cui dette funzioni o possono accedere ai loro argomenti. Laccesso viene pertanto realizzato a livello delle librerie standard del C che provvedono gli strumenti adeguati. Luso di una variadic function prevede quindi tre punti: Dichiarare la funzione come variadic usando un prototipo che contenga una ellipsis. Denire la funzione come variadic usando la stessa ellipsis, ed utilizzare le apposite macro che consentono la gestione di un numero variabile di argomenti. Invocare la funzione specicando prima gli argomenti ssi, ed a seguire quelli addizionali. Lo standard ISO C prevede che una variadic function abbia sempre almeno un argomento sso; prima di eettuare la dichiarazione deve essere incluso lapposito header le stdarg.h; un esempio di dichiarazione ` il prototipo della funzione execl che vedremo in sez. 3.2.5: e int execl ( const char * path , const char * arg , ...); in questo caso la funzione prende due argomenti ssi ed un numero variabile di altri argomenti (che verranno a costituire gli elementi successivi al primo del vettore argv passato al nuovo processo). Lo standard ISO C richiede inoltre che lultimo degli argomenti ssi sia di tipo selfpromoting 21 il che esclude vettori, puntatori a funzioni e interi di tipo char o short (con segno
il linguaggio C prevede che quando si mescolano vari tipi di dati, alcuni di essi possano essere promossi per compatibilit`; ad esempio i tipi float vengono convertiti automaticamente a double ed i char e gli short ad int. a Un tipo self-promoting ` un tipo che verrebbe promosso a s stesso. e e
21

32

CAPITOLO 2. LINTERFACCIA BASE CON I PROCESSI

o meno). Una restrizione ulteriore di alcuni compilatori ` di non dichiarare lultimo argomento e sso come register. Una volta dichiarata la funzione il secondo passo ` accedere ai vari argomenti quando la si e va a denire. Gli argomenti ssi infatti hanno un loro nome, ma quelli variabili vengono indicati in maniera generica dalla ellipsis. Lunica modalit` in cui essi possono essere recuperati ` pertanto quella sequenziale; essi a e verranno estratti dallo stack secondo lordine in cui sono stati scritti. Per fare questo in stdarg.h sono denite delle apposite macro; la procedura da seguire ` la seguente: e 1. Inizializzare un puntatore alla lista degli argomenti di tipo va_list attraverso la macro va_start. 2. Accedere ai vari argomenti opzionali con chiamate successive alla macro va_arg, la prima chiamata restituir` il primo argomento, la seconda il secondo e cos` via. a 3. Dichiarare la conclusione dellestrazione degli argomenti invocando la macro va_end. In generale ` perfettamente legittimo richiedere meno argomenti di quelli che potrebbero essere e stati eettivamente forniti, e nella esecuzione delle va_arg ci si pu` fermare in qualunque moo mento ed i restanti argomenti saranno ignorati; se invece si richiedono pi` argomenti di quelli u forniti si otterranno dei valori indeniti. Nel caso del gcc luso della macro va_end ` inutile, ma e si consiglia di usarlo ugualmente per compatibilit`. a Le denizioni delle tre macro sono le seguenti:
#include <stdarg.h> void va_start(va_list ap, last) Inizializza il puntatore alla lista di argomenti ap; il parametro last deve essere lultimo degli argomenti ssi. type va_arg(va_list ap, type) Restituisce il valore del successivo argomento opzionale, modicando opportunamente ap; la macro richiede che si specichi il tipo dellargomento attraverso il parametro type che deve essere il nome del tipo dellargomento in questione. Il tipo deve essere self-promoting. void va_end(va_list ap) Conclude luso di ap.

In generale si possono avere pi` puntatori alla lista degli argomenti, ciascuno andr` iniziau a lizzato con va_start e letto con va_arg e ciascuno potr` scandire la lista degli argomenti per a conto suo. Dopo luso di va_end la variabile ap diventa indenita e successive chiamate a va_arg non funzioneranno. Si avranno risultati indeniti anche chiamando va_arg specicando un tipo che non corrisponde a quello dellargomento. Un altro limite delle macro ` che i passi 1) e 3) devono essere eseguiti nel corpo principale e della funzione, il passo 2) invece pu` essere eseguito anche in una subroutine passandole il o puntatore alla lista di argomenti; in questo caso per` si richiede che al ritorno della funzione il o puntatore non venga pi` usato (lo standard richiederebbe la chiamata esplicita di va_end), dato u che il valore di ap risulterebbe indenito. Esistono dei casi in cui ` necessario eseguire pi` volte la scansione degli argomenti e poter e u memorizzare una posizione durante la stessa. La cosa pi` naturale in questo caso sembrerebbe u quella di copiarsi il puntatore alla lista degli argomenti con una semplice assegnazione. Dato che una delle realizzazioni pi` comuni di va_list ` quella di un puntatore nello stack allindirizzo u e dove sono stati salvati gli argomenti, ` assolutamente normale pensare di poter eettuare questa e operazione. In generale per` possono esistere anche realizzazioni diverse, per questo motivo va_list o ` denito come tipo opaco e non pu` essere assegnato direttamente ad unaltra variabile dello e o

2.4. PROBLEMATICHE DI PROGRAMMAZIONE GENERICA

33

stesso tipo. Per risolvere questo problema lo standard ISO C9922 ha previsto una macro ulteriore che permette di eseguire la copia di un puntatore alla lista degli argomenti:
#include <stdarg.h> void va_copy(va_list dest, va_list src) Copia lattuale valore src del puntatore alla lista degli argomenti su dest.

anche in questo caso ` buona norma chiudere ogni esecuzione di una va_copy con una corrispone dente va_end sul nuovo puntatore alla lista degli argomenti. La chiamata di una funzione con un numero variabile di argomenti, posto che la si sia dichiarata e denita come tale, non prevede nulla di particolare; linvocazione ` identica alle e altre, con gli argomenti, sia quelli ssi che quelli opzionali, separati da virgole. Quello che per` o ` necessario tenere presente ` come verranno convertiti gli argomenti variabili. e e In Linux gli argomenti dello stesso tipo sono passati allo stesso modo, sia che siano ssi sia che siano opzionali (alcuni sistemi trattano diversamente gli opzionali), ma dato che il prototipo non pu` specicare il tipo degli argomenti opzionali, questi verranno sempre promossi, pertanto o nella ricezione dei medesimi occorrer` tenerne conto (ad esempio un char verr` visto da va_arg a a come int). Uno dei problemi che si devono arontare con le funzioni con un numero variabile di argomenti ` che non esiste un modo generico che permetta di stabilire quanti sono gli argomenti e passati eettivamente in una chiamata. Esistono varie modalit` per arontare questo problema; una delle pi` immediate ` quella di a u e specicare il numero degli argomenti opzionali come uno degli argomenti ssi. Una variazione di questo metodo ` luso di un argomento per specicare anche il tipo degli argomenti (come fa la e stringa di formato per printf). Una modalit` diversa, che pu` essere applicata solo quando il tipo degli argomenti lo rende a o possibile, ` quella che prevede di usare un valore speciale come ultimo argomento (come fa ad e esempio execl che usa un puntatore NULL per indicare la ne della lista degli argomenti).

2.4.3

Potenziali problemi con le variabili automatiche

Uno dei possibili problemi che si possono avere con le subroutine ` quello di restituire alla fune zione chiamante dei dati che sono contenuti in una variabile automatica. Ovviamente quando la subroutine ritorna la sezione dello stack che conteneva la variabile automatica potr` essea re riutilizzata da una nuova funzione, con le immaginabili conseguenze di sovrapposizione e sovrascrittura dei dati. Per questo una delle regole fondamentali della programmazione in C ` che alluscita di una e funzione non deve restare nessun riferimento alle variabili locali; qualora sia necessario utilizzare variabili che possano essere viste anche dalla funzione chiamante queste devono essere allocate esplicitamente, o in maniera statica (usando variabili di tipo static o extern), o dinamicamente con una delle funzioni della famiglia malloc.

2.4.4

Il controllo di usso non locale

Il controllo del usso di un programma in genere viene eettuato con le varie istruzioni del linguaggio C; fra queste la pi` bistrattata ` il goto, che viene deprecato in favore dei costrutti u e della programmazione strutturata, che rendono il codice pi` leggibile e mantenibile. Esiste per` u o un caso in cui luso di questa istruzione porta allimplementazione pi` eciente e pi` chiara u u anche dal punto di vista della struttura del programma: quello delluscita in caso di errore.
alcuni sistemi che non hanno questa macro provvedono al suo posto __va_copy che era il nome proposto in una bozza dello standard.
22

34

CAPITOLO 2. LINTERFACCIA BASE CON I PROCESSI

Il C per` non consente di eettuare un salto ad una etichetta denita in unaltra funzione, o per cui se lerrore avviene in una funzione, e la sua gestione ordinaria ` in unaltra, occorre usare e quello che viene chiamato un salto non-locale. Il caso classico in cui si ha questa necessit`, citato a sia in [1] che in [4], ` quello di un programma nel cui corpo principale vengono letti dei dati e in ingresso sui quali viene eseguita, tramite una serie di funzioni di analisi, una scansione dei contenuti, da cui si ottengono le indicazioni per lesecuzione di opportune operazioni. Dato che lanalisi pu` risultare molto complessa, ed opportunamente suddivisa in fasi diverse, o la rilevazione di un errore nei dati in ingresso pu` accadere allinterno di funzioni profondamente o annidate luna nellaltra. In questo caso si dovrebbe gestire, per ciascuna fase, tutta la casistica del passaggio allindietro di tutti gli errori rilevabili dalle funzioni usate nelle fasi successive. Questo comporterebbe una notevole complessit`, mentre sarebbe molto pi` comodo poter tornare a u direttamente al ciclo di lettura principale, scartando linput come errato.23 Tutto ci` pu` essere realizzato proprio con un salto non-locale; questo di norma viene reao o lizzato salvando il contesto dello stack nel punto in cui si vuole tornare in caso di errore, e ripristinandolo, in modo da tornare nella funzione da cui si era partiti, quando serve. La funzione che permette di salvare il contesto dello stack ` setjmp, il cui prototipo `: e e
#include <setjmp.h> int setjmp(jmp_buf env) Salva il contesto dello stack. La funzione ritorna zero quando ` chiamata direttamente e un valore diverso da zero quando e ritorna da una chiamata di longjmp che usa il contesto salvato in precedenza.

Quando si esegue la funzione il contesto corrente dello stack viene salvato nellargomento env, una variabile di tipo jmp_buf24 che deve essere stata denita in precedenza. In genere le variabili di tipo jmp_buf vengono denite come variabili globali in modo da poter essere viste in tutte le funzioni del programma. Quando viene eseguita direttamente la funzione ritorna sempre zero, un valore diverso da zero viene restituito solo quando il ritorno ` dovuto ad una chiamata di longjmp in unaltra e parte del programma che ripristina lo stack eettuando il salto non-locale. Si tenga conto che il contesto salvato in env viene invalidato se la funzione che ha chiamato setjmp ritorna, nel qual caso un successivo uso di longjmp pu` comportare conseguenze imprevedibili (e di norma fatali) o per il processo. Come accennato per eettuare un salto non-locale ad un punto precedentemente stabilito con setjmp si usa la funzione longjmp; il suo prototipo `: e
#include <setjmp.h> void longjmp(jmp_buf env, int val) Ripristina il contesto dello stack. La funzione non ritorna.

La funzione ripristina il contesto dello stack salvato da una chiamata a setjmp nellargomento env. Dopo lesecuzione della funzione il programma prosegue nel codice successivo al ritorno della setjmp con cui si era salvato env, che restituir` il valore val invece di zero. Il valore di a val specicato nella chiamata deve essere diverso da zero, se si ` specicato 0 sar` comunque e a restituito 1 al suo posto. In sostanza un longjmp ` analogo ad un return, solo che invece di ritornare alla riga succese siva della funzione chiamante, il programma ritorna alla posizione della relativa setjmp, laltra dierenza ` che il ritorno pu` essere eettuato anche attraverso diversi livelli di funzioni annidate. e o
23 a meno che, come precisa [4], alla chiusura di ciascuna fase non siano associate operazioni di pulizia speciche (come deallocazioni, chiusure di le, ecc.), che non potrebbero essere eseguite con un salto non-locale. 24 questo ` un classico esempio di variabile di tipo opaco. Si deniscono cos` strutture ed altri oggetti usati da e una libreria, la cui struttura interna non deve essere vista dal programma chiamante (da cui il nome) che li devono utilizzare solo attraverso dalle opportune funzioni di gestione.

2.4. PROBLEMATICHE DI PROGRAMMAZIONE GENERICA

35

Limplementazione di queste funzioni comporta alcune restrizioni dato che esse interagiscono direttamente con la gestione dello stack ed il funzionamento del compilatore stesso. In particolare setjmp ` implementata con una macro, pertanto non si pu` cercare di ottenerne lindirizzo, ed e o inoltre delle chiamate a questa funzione sono sicure solo in uno dei seguenti casi: come espressione di controllo in un comando condizionale, di selezione o di iterazione (come if, switch o while); come operando per un operatore di uguaglianza o confronto in una espressione di controllo di un comando condizionale, di selezione o di iterazione; come operando per loperatore di negazione (!) in una espressione di controllo di un comando condizionale, di selezione o di iterazione; come espressione a s stante. e In generale, dato che lunica dierenza fra la chiamata diretta e quella ottenuta da un longjmp ` costituita dal valore di ritorno di setjmp, essa ` usualmente chiamata allinterno di un comando e e if. Uno dei punti critici dei salti non-locali ` quello del valore delle variabili, ed in particolare e quello delle variabili automatiche della funzione a cui si ritorna. In generale le variabili globali e statiche mantengono i valori che avevano al momento della chiamata di longjmp, ma quelli delle variabili automatiche (o di quelle dichiarate register25 ) sono in genere indeterminati. Quello che succede infatti ` che i valori delle variabili che sono tenute in memoria manterranno e il valore avuto al momento della chiamata di longjmp, mentre quelli tenuti nei registri del processore (che nella chiamata ad unaltra funzione vengono salvati nel contesto nello stack ) torneranno al valore avuto al momento della chiamata di setjmp; per questo quando si vuole avere un comportamento coerente si pu` bloccare lottimizzazione che porta le variabili nei o registri dichiarandole tutte come volatile.26

la direttiva register del compilatore chiede che la variabile dichiarata tale sia mantenuta, nei limiti del possibile, allinterno di un registro del processore. Questa direttiva ` originaria dellepoca dai primi compilatori, e quando stava al programmatore scrivere codice ottimizzato, riservando esplicitamente alle variabili pi` usate luso u dei registri del processore. Oggi questa direttiva ` in disuso dato che tutti i compilatori sono normalmente in grado e di valutare con maggior ecacia degli stessi programmatori quando sia il caso di eseguire questa ottimizzazione. 26 la direttiva volatile informa il compilatore che la variabile che ` dichiarata pu` essere modicata, durante e o lesecuzione del nostro, da altri programmi. Per questo motivo occorre dire al compilatore che non deve essere mai utilizzata lottimizzazione per cui quanto opportuno essa viene mantenuta in un registro, poich in questo e modo si perderebbero le eventuali modiche fatte dagli altri programmi (che avvengono solo in una copia posta in memoria).

25

36

CAPITOLO 2. LINTERFACCIA BASE CON I PROCESSI

Capitolo 3

La gestione dei processi


Come accennato nellintroduzione in un sistema Unix tutte le operazioni vengono svolte tramite opportuni processi. In sostanza questi ultimi vengono a costituire lunit` base per lallocazione a e luso delle risorse del sistema. Nel precedente capitolo abbiamo esaminato il funzionamento di un processo come unit` a a se stante, in questo esamineremo il funzionamento dei processi allinterno del sistema. Saranno cio` arontati i dettagli della creazione e della terminazione dei processi, della gestione dei e loro attributi e privilegi, e di tutte le funzioni a questo connesse. Inne nella sezione nale introdurremo alcune problematiche generiche della programmazione in ambiente multitasking.

3.1

Introduzione

Inizieremo con unintroduzione generale ai concetti che stanno alla base della gestione dei processi in un sistema unix-like. Introdurremo in questa sezione larchitettura della gestione dei processi e le sue principali caratteristiche, dando una panoramica sulluso delle principali funzioni di gestione.

3.1.1

Larchitettura della gestione dei processi

A dierenza di quanto avviene in altri sistemi (ad esempio nel VMS la generazione di nuovi processi ` unoperazione privilegiata) una delle caratteristiche di Unix (che esamineremo in e dettaglio pi` avanti) ` che qualunque processo pu` a sua volta generarne altri, detti processi gli u e o (child process). Ogni processo ` identicato presso il sistema da un numero univoco, il cosiddetto e process identier o, pi` brevemente, pid, assegnato in forma progressiva (vedi sez. 3.2.1) quando u il processo viene creato. Una seconda caratteristica di un sistema Unix ` che la generazione di un processo ` unoe e perazione separata rispetto al lancio di un programma. In genere la sequenza ` sempre quella e di creare un nuovo processo, il quale eseguir`, in un passo successivo, il programma desiderato: a questo ` ad esempio quello che fa la shell quando mette in esecuzione il programma che gli e indichiamo nella linea di comando. Una terza caratteristica ` che ogni processo ` sempre stato generato da un altro, che viene e e chiamato processo padre (parent process). Questo vale per tutti i processi, con una sola eccezione: dato che ci deve essere un punto di partenza esiste un processo speciale (che normalmente ` e /sbin/init), che viene lanciato dal kernel alla conclusione della fase di avvio; essendo questo il primo processo lanciato dal sistema ha sempre il pid uguale a 1 e non ` glio di nessun altro e processo. Ovviamente init ` un processo speciale che in genere si occupa di far partire tutti gli altri e processi necessari al funzionamento del sistema, inoltre init ` essenziale per svolgere una serie e 37

38

CAPITOLO 3. LA GESTIONE DEI PROCESSI

di compiti amministrativi nelle operazioni ordinarie del sistema (torneremo su alcuni di essi in sez. 3.2.3) e non pu` mai essere terminato. La struttura del sistema comunque consente di o lanciare al posto di init qualunque altro programma, e in casi di emergenza (ad esempio se il le di init si fosse corrotto) ` ad esempio possibile lanciare una shell al suo posto, passando la e riga init=/bin/sh come parametro di avvio.
[piccardi@gont piccardi]$ pstree -n init-+-keventd |-kapm-idled |-kreiserfsd |-portmap |-syslogd |-klogd |-named |-rpc.statd |-gpm |-inetd |-junkbuster |-master-+-qmgr | -pickup |-sshd |-xfs |-cron |-bash---startx---xinit-+-XFree86 | -WindowMaker-+-ssh-agent | |-wmtime | |-wmmon | |-wmmount | |-wmppp | |-wmcube | |-wmmixer | |-wmgtemp | |-wterm---bash---pstree | -wterm---bash-+-emacs | -man---pager |-5*[getty] |-snort -wwwoffled

Figura 3.1: Lalbero dei processi, cos` come riportato dal comando pstree.

Dato che tutti i processi attivi nel sistema sono comunque generati da init o da uno dei suoi gli1 si possono classicare i processi con la relazione padre/glio in unorganizzazione gerarchica ad albero, in maniera analoga a come i le sono organizzati in un albero di directory (si veda sez. 4.1.1); in g. 3.1 si ` mostrato il risultato del comando pstree che permette di visualizzare e questa struttura, alla cui base c` init che ` progenitore di tutti gli altri processi. e e Il kernel mantiene una tabella dei processi attivi, la cosiddetta process table; per ciascun processo viene mantenuta una voce, costituita da una struttura task_struct, nella tabella dei processi che contiene tutte le informazioni rilevanti per quel processo. Tutte le strutture usate a questo scopo sono dichiarate nellheader le linux/sched.h, ed uno schema semplicato, che riporta la struttura delle principali informazioni contenute nella task_struct (che in seguito incontreremo a pi` riprese), ` mostrato in g. 3.2. u e Come accennato in sez. 1.1 ` lo scheduler che decide quale processo mettere in esecuzione; e
in realt` questo non ` del tutto vero, in Linux ci sono alcuni processi speciali che pur comparendo come gli a e di init, o con pid successivi, sono in realt` generati direttamente dal kernel, (come keventd, kswapd, ecc.). a
1

3.1. INTRODUZIONE

39

Figura 3.2: Schema semplicato dellarchitettura delle strutture usate dal kernel nella gestione dei processi.

esso viene eseguito ad ogni system call ed ad ogni interrupt,2 (ma pu` essere anche attivato o esplicitamente). Il timer di sistema provvede comunque a che esso sia invocato periodicamente; generando un interrupt periodico secondo la frequenza specicata dalla costante HZ,3 denita in asm/param.h, ed il cui valore ` espresso in Hertz.4 e Ogni volta che viene eseguito, lo scheduler eettua il calcolo delle priorit` dei vari processi a attivi (torneremo su questo in sez. 3.4) e stabilisce quale di essi debba essere posto in esecuzione no alla successiva invocazione.

3.1.2

Una panoramica sulle funzioni fondamentali

In un sistema unix-like i processi vengono sempre creati da altri processi tramite la funzione fork; il nuovo processo (che viene chiamato glio) creato dalla fork ` una copia identica del e processo processo originale (detto padre), ma ha un nuovo pid e viene eseguito in maniera indipendente (le dierenze fra padre e glio sono arontate in dettaglio in sez. 3.2.2). Se si vuole che il processo padre si fermi no alla conclusione del processo glio questo deve essere specicato subito dopo la fork chiamando la funzione wait o la funzione waitpid (si veda sez. 3.2.4); queste funzioni restituiscono anche uninformazione abbastanza limitata sulle cause della terminazione del processo glio. Quando un processo ha concluso il suo compito o ha incontrato un errore non risolvibile esso pu` essere terminato con la funzione exit (si veda quanto discusso in sez. 2.1.2). La vita del o processo per` termina solo quando la notica della sua conclusione viene ricevuta dal processo o padre, a quel punto tutte le risorse allocate nel sistema ad esso associate vengono rilasciate.
pi` in una serie di altre occasioni. u no al kernel 2.4 il valore usuale di questa costante era 100, per tutte le architetture eccetto lalpha, per la quale era 1000, nel 2.6 ` stato portato a 1000 su tutte le architetture; occorre fare attenzione a non confondere e questo valore con quello dei clock tick (vedi sez. 8.4.1). 4 a partire dal kernel 2.6.21 ` stato introdotto (a cura di Ingo Molnar) un meccanismo completamente diverso, e detto tickless, in cui non c` pi` una interruzione periodica con frequenza pressata, ma ad ogni chiamata del time e u viene programmata linterruzione successiva sulla base di una stima; in questo modo si evita di dover eseguire un migliaio di interruzioni al secondo anche su macchine che non stanno facendo nulla, con un forte risparmio nelluso dellenergia da parte del processore che pu` essere messo in stato di sospensione anche per lunghi periodi o di tempo.
3 2

40

CAPITOLO 3. LA GESTIONE DEI PROCESSI

Avere due processi che eseguono esattamente lo stesso codice non ` molto utile, normalmente e si genera un secondo processo per adargli lesecuzione di un compito specico (ad esempio gestire una connessione dopo che questa ` stata stabilita), o fargli eseguire (come fa la shell) un altro e programma. Per questultimo caso si usa la seconda funzione fondamentale per programmazione coi processi che ` la exec. e Il programma che un processo sta eseguendo si chiama immagine del processo (o process image), le funzioni della famiglia exec permettono di caricare un altro programma da disco sostituendo questultimo allimmagine corrente; questo fa s` che limmagine precedente venga completamente cancellata. Questo signica che quando il nuovo programma termina, anche il processo termina, e non si pu` tornare alla precedente immagine. o Per questo motivo la fork e la exec sono funzioni molto particolari con caratteristiche uniche rispetto a tutte le altre, infatti la prima ritorna due volte (nel processo padre e nel glio) mentre la seconda non ritorna mai (in quanto con essa viene eseguito un altro programma).

3.2

Le funzioni di base

In questa sezione tratteremo le problematiche della gestione dei processi allinterno del sistema, illustrandone tutti i dettagli. Inizieremo con le funzioni elementari che permettono di leggerne gli identicatori, per poi passare alla spiegazione delle funzioni base che si usano per la creazione e la terminazione dei processi, e per la messa in esecuzione degli altri programmi.

3.2.1

Gli identicatori dei processi

Come accennato nellintroduzione, ogni processo viene identicato dal sistema da un numero identicativo univoco, il process ID o pid; questultimo ` un tipo di dato standard, il pid_t che e in genere ` un intero con segno (nel caso di Linux e delle glibc il tipo usato ` int). e e Il pid viene assegnato in forma progressiva5 ogni volta che un nuovo processo viene creato, no ad un limite che, essendo il pid un numero positivo memorizzato in un intero a 16 bit, arriva ad un massimo di 32768. Oltre questo valore lassegnazione riparte dal numero pi` basso u disponibile a partire da un minimo di 300,6 che serve a riservare i pid pi` bassi ai processi u eseguiti direttamente dal kernel. Per questo motivo, come visto in sez. 3.1.1, il processo di avvio (init) ha sempre il pid uguale a uno. Tutti i processi inoltre memorizzano anche il pid del genitore da cui sono stati creati, questo viene chiamato in genere ppid (da parent process ID). Questi due identicativi possono essere ottenuti usando le due funzioni getpid e getppid, i cui prototipi sono:
#include <sys/types.h> #include <unistd.h> pid_t getpid(void) Restituisce il pid del processo corrente. pid_t getppid(void) Restituisce il pid del padre del processo corrente. Entrambe le funzioni non riportano condizioni di errore.

esempi delluso di queste funzioni sono riportati in g. 3.3, nel programma ForkTest.c. Il fatto che il pid sia un numero univoco per il sistema lo rende un candidato per generare ulteriori indicatori associati al processo di cui diventa possibile garantire lunicit`: ad esempio a
5 in genere viene assegnato il numero successivo a quello usato per lultimo processo creato, a meno che questo numero non sia gi` utilizzato per un altro pid, pgid o sid (vedi sez. 10.1.2). a 6 questi valori, no al kernel 2.4.x, sono deniti dalla macro PID_MAX in threads.h e direttamente in fork.c, con il kernel 2.5.x e la nuova interfaccia per i thread creata da Ingo Molnar anche il meccanismo di allocazione dei pid ` stato modicato. e

3.2. LE FUNZIONI DI BASE

41

in alcune implementazioni la funzione tempnam (si veda sez. 5.1.8) usa il pid per generare un pathname univoco, che non potr` essere replicato da un altro processo che usi la stessa funzione. a Tutti i processi gli dello stesso processo padre sono detti sibling, questa ` una delle relazioni e usate nel controllo di sessione, in cui si raggruppano i processi creati su uno stesso terminale, o relativi allo stesso login. Torneremo su questo argomento in dettaglio in cap. 10, dove esamineremo gli altri identicativi associati ad un processo e le varie relazioni fra processi utilizzate per denire una sessione. Oltre al pid e al ppid, (e a quelli che vedremo in sez. 10.1.2, relativi al controllo di sessione), ad ogni processo vengono associati degli altri identicatori che vengono usati per il controllo di accesso. Questi servono per determinare se un processo pu` eseguire o meno le operazioni o richieste, a seconda dei privilegi e dellidentit` di chi lo ha posto in esecuzione; largomento ` a e complesso e sar` arontato in dettaglio in sez. 3.3. a

3.2.2

La funzione fork e le funzioni di creazione dei processi

La funzione fork ` la funzione fondamentale della gestione dei processi: come si ` detto lunico e e modo di creare un nuovo processo ` attraverso luso di questa funzione, essa quindi riveste e un ruolo centrale tutte le volte che si devono scrivere programmi che usano il multitasking. Il prototipo della funzione `: e
#include <sys/types.h> #include <unistd.h> pid_t fork(void) Crea un nuovo processo. In caso di successo restituisce il pid del glio al padre e zero al glio; ritorna -1 al padre (senza creare il glio) in caso di errore; errno pu` assumere i valori: o EAGAIN ENOMEM non ci sono risorse sucienti per creare un altro processo (per allocare la tabella delle pagine e le strutture del task) o si ` esaurito il numero di processi disponibili. e non ` stato possibile allocare la memoria per le strutture necessarie al kernel per creare e il nuovo processo.

Dopo il successo dellesecuzione di una fork sia il processo padre che il processo glio continuano ad essere eseguiti normalmente a partire dallistruzione successiva alla fork; il processo glio ` per` una copia del padre, e riceve una copia dei segmenti di testo, stack e dati (vee o di sez. 2.2.2), ed esegue esattamente lo stesso codice del padre. Si tenga presente per` che la o memoria ` copiata, non condivisa, pertanto padre e glio vedono variabili diverse. e Per quanto riguarda la gestione della memoria, in generale il segmento di testo, che ` identico e per i due processi, ` condiviso e tenuto in read-only per il padre e per i gli. Per gli altri segmenti e Linux utilizza la tecnica del copy on write; questa tecnica comporta che una pagina di memoria viene eettivamente copiata per il nuovo processo solo quando ci viene eettuata sopra una scrittura (e si ha quindi una reale dierenza fra padre e glio). In questo modo si rende molto pi` eciente il meccanismo della creazione di un nuovo processo, non essendo pi` necessaria la u u copia di tutto lo spazio degli indirizzi virtuali del padre, ma solo delle pagine di memoria che sono state modicate, e solo al momento della modica stessa. La dierenza che si ha nei due processi ` che nel processo padre il valore di ritorno della e funzione fork ` il pid del processo glio, mentre nel glio ` zero; in questo modo il programma e e pu` identicare se viene eseguito dal padre o dal glio. Si noti come la funzione fork ritorni o due volte: una nel padre e una nel glio. La scelta di questi valori di ritorno non ` casuale, un processo infatti pu` avere pi` gli, ed il e o u valore di ritorno di fork ` lunico modo che gli permette di identicare quello appena creato; al e contrario un glio ha sempre un solo padre (il cui pid pu` sempre essere ottenuto con getppid, o vedi sez. 3.2.1) per cui si usa il valore nullo, che non ` il pid di nessun processo. e

42

CAPITOLO 3. LA GESTIONE DEI PROCESSI

# include # include 3 # include 4 # include 5 # include


1 2 6 7 8 9

< errno .h > < stdlib .h > < unistd .h > < stdio .h > < string .h >

/* /* /* /* /*

error definitions and routines */ C standard library */ unix standard library */ standard I / O library */ string functions */

/* Help printing routine */ void usage ( void );

int main ( int argc , char * argv []) { 12 /* 13 * Variables definition 14 */ 15 int nchild , i ; 16 pid_t pid ; 17 int wait_child = 0; 18 int wait_parent = 0; 19 int wait_end = 0; 20 ... /* handling options */ 21 nchild = atoi ( argv [ optind ]); 22 printf ( " Test for forking % d child \ n " , nchild ); 23 /* loop to fork children */ 24 for ( i =0; i < nchild ; i ++) { 25 if ( ( pid = fork ()) < 0) { 26 /* on error exit */ 27 printf ( " Error on % d child creation , % s \ n " , i +1 , strerror ( errno )); 28 exit ( -1); 29 } 30 if ( pid == 0) { /* child */ 31 printf ( " Child % d successfully executing \ n " , ++ i ); 32 if ( wait_child ) sleep ( wait_child ); 33 printf ( " Child %d , parent %d , exiting \ n " , i , getppid ()); 34 exit (0); 35 } else { /* parent */ 36 printf ( " Spawned % d child , pid % d \ n " , i +1 , pid ); 37 if ( wait_parent ) sleep ( wait_parent ); 38 printf ( " Go to next child \ n " ); 39 } 40 } 41 /* normal exit */ 42 if ( wait_end ) sleep ( wait_end ); 43 return 0; 44 }
10 11

Figura 3.3: Esempio di codice per la creazione di nuovi processi.

Normalmente la chiamata a fork pu` fallire solo per due ragioni, o ci sono gi` troppi processi o a nel sistema (il che di solito ` sintomo che qualcosaltro non sta andando per il verso giusto) o e si ` ecceduto il limite sul numero totale di processi permessi allutente (vedi sez. 8.3.2, ed in e particolare tab. 8.12). Luso di fork avviene secondo due modalit` principali; la prima ` quella in cui allinterno a e di un programma si creano processi gli cui viene adata lesecuzione di una certa sezione di ` codice, mentre il processo padre ne esegue unaltra. E il caso tipico dei programmi server (il modello client-server ` illustrato in sez. 14.1.1) in cui il padre riceve ed accetta le richieste da e parte dei programmi client, per ciascuna delle quali pone in esecuzione un glio che ` incaricato e di fornire il servizio.

3.2. LE FUNZIONI DI BASE

43

La seconda modalit` ` quella in cui il processo vuole eseguire un altro programma; questo ` ae e ad esempio il caso della shell. In questo caso il processo crea un glio la cui unica operazione ` e quella di fare una exec (di cui parleremo in sez. 3.2.5) subito dopo la fork. Alcuni sistemi operativi (il VMS ad esempio) combinano le operazioni di questa seconda modalit` (una fork seguita da una exec) in ununica operazione che viene chiamata spawn. Nei a sistemi unix-like ` stato scelto di mantenere questa separazione, dato che, come per la prima e modalit` duso, esistono numerosi scenari in cui si pu` usare una fork senza aver bisogno di a o eseguire una exec. Inoltre, anche nel caso della seconda modalit` duso, avere le due funzioni a separate permette al glio di cambiare gli attributi del processo (maschera dei segnali, redirezione delloutput, identicatori) prima della exec, rendendo cos` relativamente facile intervenire sulle le modalit` di esecuzione del nuovo programma. a In g. 3.3 ` riportato il corpo del codice del programma di esempio forktest, che permette e di illustrare molte caratteristiche delluso della funzione fork. Il programma crea un numero di gli specicato da linea di comando, e prende anche alcune opzioni per indicare degli eventuali tempi di attesa in secondi (eseguiti tramite la funzione sleep) per il padre ed il glio (con forktest -h si ottiene la descrizione delle opzioni); il codice completo, compresa la parte che gestisce le opzioni a riga di comando, ` disponibile nel le ForkTest.c, distribuito insieme agli e altri sorgenti degli esempi su http://gapil.truelite.it/gapil source.tgz. Decifrato il numero di gli da creare, il ciclo principale del programma (24-40) esegue in successione la creazione dei processi gli controllando il successo della chiamata a fork (2529); ciascun glio (31-34) si limita a stampare il suo numero di successione, eventualmente attendere il numero di secondi specicato e scrivere un messaggio prima di uscire. Il processo padre invece (36-38) stampa un messaggio di creazione, eventualmente attende il numero di secondi specicato, e procede nellesecuzione del ciclo; alla conclusione del ciclo, prima di uscire, pu` essere specicato un altro periodo di attesa. o Se eseguiamo il comando7 senza specicare attese (come si pu` notare in (17-19) i valori o predeniti specicano di non attendere), otterremo come output sul terminale:
[piccardi@selidor sources]$ export LD_LIBRARY_PATH=./; ./forktest 3 Process 1963: forking 3 child Spawned 1 child, pid 1964 Child 1 successfully executing Child 1, parent 1963, exiting Go to next child Spawned 2 child, pid 1965 Child 2 successfully executing Child 2, parent 1963, exiting Go to next child Child 3 successfully executing Child 3, parent 1963, exiting Spawned 3 child, pid 1966 Go to next child

Esaminiamo questo risultato: una prima conclusione che si pu` trarre ` che non si pu` dire o e o 8 dopo la chiamata a fork; quale processo fra il padre ed il glio venga eseguito per primo dallesempio si pu` notare infatti come nei primi due cicli sia stato eseguito per primo il padre o (con la stampa del pid del nuovo processo) per poi passare allesecuzione del glio (completata con i due avvisi di esecuzione ed uscita), e tornare allesecuzione del padre (con la stampa del passaggio al ciclo successivo), mentre la terza volta ` stato prima eseguito il glio (no alla e conclusione) e poi il padre.
che ` preceduto dallistruzione export LD_LIBRARY_PATH=./ per permettere luso delle librerie dinamiche. e a partire dal kernel 2.5.2-pre10 ` stato introdotto il nuovo scheduler di Ingo Molnar che esegue sempre per e primo il glio; per mantenere la portabilit` ` opportuno non fare comunque adamento su questo comportamento. ae
8 7

44

CAPITOLO 3. LA GESTIONE DEI PROCESSI

In generale lordine di esecuzione dipender`, oltre che dallalgoritmo di scheduling usato a dal kernel, dalla particolare situazione in cui si trova la macchina al momento della chiamata, risultando del tutto impredicibile. Eseguendo pi` volte il programma di prova e producendo un u numero diverso di gli, si sono ottenute situazioni completamente diverse, compreso il caso in cui il processo padre ha eseguito pi` di una fork prima che uno dei gli venisse messo in esecuzione. u Pertanto non si pu` fare nessuna assunzione sulla sequenza di esecuzione delle istruzioni o del codice fra padre e gli, n sullordine in cui questi potranno essere messi in esecuzione. Se e ` necessaria una qualche forma di precedenza occorrer` provvedere ad espliciti meccanismi di e a sincronizzazione, pena il rischio di incorrere nelle cosiddette race condition (vedi sez. 3.5.2). Si noti inoltre che essendo i segmenti di memoria utilizzati dai singoli processi completamente separati, le modiche delle variabili nei processi gli (come lincremento di i in 31) sono visibili solo a loro (ogni processo vede solo la propria copia della memoria), e non hanno alcun eetto sul valore che le stesse variabili hanno nel processo padre (ed in eventuali altri processi gli che eseguano lo stesso codice). Un secondo aspetto molto importante nella creazione dei processi gli ` quello dellinterazione e dei vari processi con i le; per illustrarlo meglio proviamo a redirigere su un le loutput del nostro programma di test, quello che otterremo `: e
[piccardi@selidor sources]$ ./forktest 3 > output [piccardi@selidor sources]$ cat output Process 1967: forking 3 child Child 1 successfully executing Child 1, parent 1967, exiting Test for forking 3 child Spawned 1 child, pid 1968 Go to next child Child 2 successfully executing Child 2, parent 1967, exiting Test for forking 3 child Spawned 1 child, pid 1968 Go to next child Spawned 2 child, pid 1969 Go to next child Child 3 successfully executing Child 3, parent 1967, exiting Test for forking 3 child Spawned 1 child, pid 1968 Go to next child Spawned 2 child, pid 1969 Go to next child Spawned 3 child, pid 1970 Go to next child

che come si vede ` completamente diverso da quanto ottenevamo sul terminale. e Il comportamento delle varie funzioni di interfaccia con i le ` analizzato in gran dettaglio in e cap. 6 e in cap. 7. Qui basta accennare che si sono usate le funzioni standard della libreria del C che prevedono loutput buerizzato; e questa buerizzazione (trattata in dettaglio in sez. 7.1.4) varia a seconda che si tratti di un le su disco (in cui il buer viene scaricato su disco solo quando necessario) o di un terminale (nel qual caso il buer viene scaricato ad ogni carattere di a capo). Nel primo esempio allora avevamo che ad ogni chiamata a printf il buer veniva scaricato, e le singole righe erano stampate a video subito dopo lesecuzione della printf. Ma con la redirezione su le la scrittura non avviene pi` alla ne di ogni riga e loutput resta nel buer. u Dato che ogni glio riceve una copia della memoria del padre, esso ricever` anche quanto c` nel a e buer delle funzioni di I/O, comprese le linee scritte dal padre no allora. Cos` quando il buer viene scritto su disco alluscita del glio, troveremo nel le anche tutto quello che il processo

3.2. LE FUNZIONI DI BASE

45

padre aveva scritto prima della sua creazione. E alla ne del le (dato che in questo caso il padre esce per ultimo) troveremo anche loutput completo del padre. Lesempio ci mostra un altro aspetto fondamentale dellinterazione con i le, valido anche per lesempio precedente, ma meno evidente: il fatto cio` che non solo processi diversi possono e scrivere in contemporanea sullo stesso le (largomento della condivisione dei le ` trattato in e dettaglio in sez. 6.3.1), ma anche che, a dierenza di quanto avviene per le variabili, la posizione corrente sul le ` condivisa fra il padre e tutti i processi gli. e Quello che succede ` che quando lo standard output del padre viene rediretto, lo stesso e avviene anche per tutti i gli; la funzione fork infatti ha la caratteristica di duplicare nei gli tutti i le descriptor aperti nel padre (allo stesso modo in cui lo fa la funzione dup, trattata in sez. 6.3.4), il che comporta che padre e gli condividono le stesse voci della le table (per la spiegazione di questi termini si veda sez. 6.3.1) fra cui c` anche la posizione corrente nel le. e In questo modo se un processo scrive sul le aggiorner` la posizione corrente sulla le table, a e tutti gli altri processi, che vedono la stessa le table, vedranno il nuovo valore. In questo modo si evita, in casi come quello appena mostrato in cui diversi processi scrivono sullo stesso le, che loutput successivo di un processo vada a sovrapporsi a quello dei precedenti: loutput potr` a risultare mescolato, ma non ci saranno parti perdute per via di una sovrascrittura. Questo tipo di comportamento ` essenziale in tutti quei casi in cui il padre crea un glio e e attende la sua conclusione per proseguire, ed entrambi scrivono sullo stesso le (un caso tipico ` la shell quando lancia un programma, il cui output va sullo standard output). e In questo modo, anche se loutput viene rediretto, il padre potr` sempre continuare a a scrivere in coda a quanto scritto dal glio in maniera automatica; se cos` non fosse ottenere questo comportamento sarebbe estremamente complesso necessitando di una qualche forma di comunicazione fra i due processi per far riprendere al padre la scrittura al punto giusto. In generale comunque non ` buona norma far scrivere pi` processi sullo stesso le senza una e u qualche forma di sincronizzazione in quanto, come visto anche con il nostro esempio, le varie scritture risulteranno mescolate fra loro in una sequenza impredicibile. Per questo le modalit` a con cui in genere si usano i le dopo una fork sono sostanzialmente due: 1. Il processo padre aspetta la conclusione del glio. In questo caso non ` necessaria nessuna e azione riguardo ai le, in quanto la sincronizzazione della posizione corrente dopo eventuali operazioni di lettura e scrittura eettuate dal glio ` automatica. e 2. Lesecuzione di padre e glio procede indipendentemente. In questo caso ciascuno dei due processi deve chiudere i le che non gli servono una volta che la fork ` stata eseguita, per e evitare ogni forma di interferenza. Oltre ai le aperti i processi gli ereditano dal padre una serie di altre propriet`; la lista a dettagliata delle propriet` che padre e glio hanno in comune dopo lesecuzione di una fork ` a e la seguente: i le aperti e gli eventuali ag di close-on-exec impostati (vedi sez. 3.2.5 e sez. 6.3.6); gli identicatori per il controllo di accesso: luser-ID reale, il group-ID reale, luser-ID eettivo, il group-ID eettivo ed i group-ID supplementari (vedi sez. 3.3.1); gli identicatori per il controllo di sessione: il process group-ID e il session id ed il terminale di controllo (vedi sez. 10.1.2); la directory di lavoro e la directory radice (vedi sez. 5.1.7 e sez. 5.4.3); la maschera dei permessi di creazione (vedi sez. 5.3.3); la maschera dei segnali bloccati (vedi sez. 9.4.4) e le azioni installate (vedi sez. 9.3.1); i segmenti di memoria condivisa agganciati al processo (vedi sez. 12.2.6); i limiti sulle risorse (vedi sez. 8.3.2); le priorit` real-time e le anit` di processore (vedi sez. 3.4.3 e sez.3.4.4); a a le variabili di ambiente (vedi sez. 2.3.4).

46

CAPITOLO 3. LA GESTIONE DEI PROCESSI

Le dierenze fra padre e glio dopo la fork invece sono: il valore di ritorno di fork; il pid (process id ); il ppid (parent process id ), quello del glio viene impostato al pid del padre; i valori dei tempi di esecuzione della struttura tms (vedi sez. 8.4.2) che nel glio sono posti a zero; i lock sui le (vedi sez. 11.4), che non vengono ereditati dal glio; gli allarmi ed i segnali pendenti (vedi sez. 9.3.1), che per il glio vengono cancellati.

Una seconda funzione storica usata per la creazione di un nuovo processo ` vfork, che ` e e esattamente identica a fork ed ha la stessa semantica e gli stessi errori; la sola dierenza ` che e non viene creata la tabella delle pagine n la struttura dei task per il nuovo processo. Il processo e padre ` posto in attesa ntanto che il glio non ha eseguito una execve o non ` uscito con una e e _exit. Il glio condivide la memoria del padre (e modiche possono avere eetti imprevedibili) e non deve ritornare o uscire con exit ma usare esplicitamente _exit. Questa funzione ` un rimasuglio dei vecchi tempi in cui eseguire una fork comportava anche e la copia completa del segmento dati del processo padre, che costituiva un inutile appesantimento in tutti quei casi in cui la fork veniva fatta solo per poi eseguire una exec. La funzione venne introdotta in BSD per migliorare le prestazioni. Dato che Linux supporta il copy on write la perdita di prestazioni ` assolutamente trascurae bile, e luso di questa funzione (che resta un caso speciale della system call __clone) ` deprecato; e per questo eviteremo di trattarla ulteriormente.

3.2.3

La conclusione di un processo

In sez. 2.1.2 abbiamo gi` arontato le modalit` con cui chiudere un programma, ma dallina a terno del programma stesso; avendo a che fare con un sistema multitasking resta da arontare largomento dal punto di vista di come il sistema gestisce la conclusione dei processi. Abbiamo visto in sez. 2.1.2 le tre modalit` con cui un programma viene terminato in maniera a normale: la chiamata di exit (che esegue le funzioni registrate per luscita e chiude gli stream), il ritorno dalla funzione main (equivalente alla chiamata di exit), e la chiamata ad _exit (che passa direttamente alle operazioni di terminazione del processo da parte del kernel). Ma abbiamo accennato che oltre alla conclusione normale esistono anche delle modalit` di a conclusione anomala; queste sono in sostanza due: il programma pu` chiamare la funzione abort o per invocare una chiusura anomala, o essere terminato da un segnale (torneremo sui segnali in cap. 9). In realt` anche la prima modalit` si riconduce alla seconda, dato che abort si limita a a a generare il segnale SIGABRT. Qualunque sia la modalit` di conclusione di un processo, il kernel esegue comunque una serie a di operazioni: chiude tutti i le aperti, rilascia la memoria che stava usando, e cos` via; lelenco completo delle operazioni eseguite alla chiusura di un processo ` il seguente: e tutti i le descriptor sono chiusi; viene memorizzato lo stato di terminazione del processo; ad ogni processo glio viene assegnato un nuovo padre (in genere init); viene inviato il segnale SIGCHLD al processo padre (vedi sez. 9.3.6); se il processo ` un leader di sessione ed il suo terminale di controllo ` quello della sessione e e viene mandato un segnale di SIGHUP a tutti i processi del gruppo di foreground e il terminale di controllo viene disconnesso (vedi sez. 10.1.3); se la conclusione di un processo rende orfano un process group ciascun membro del gruppo viene bloccato, e poi gli vengono inviati in successione i segnali SIGHUP e SIGCONT (vedi ancora sez. 10.1.3).

3.2. LE FUNZIONI DI BASE

47

Oltre queste operazioni ` per` necessario poter disporre di un meccanismo ulteriore che e o consenta di sapere come la terminazione ` avvenuta: dato che in un sistema unix-like tutto viene e gestito attraverso i processi, il meccanismo scelto consiste nel riportare lo stato di terminazione (il cosiddetto termination status) al processo padre. Nel caso di conclusione normale, abbiamo visto in sez. 2.1.2 che lo stato di uscita del processo viene caratterizzato tramite il valore del cosiddetto exit status, cio` il valore passato alle funzioni e exit o _exit (o dal valore di ritorno per main). Ma se il processo viene concluso in maniera anomala il programma non pu` specicare nessun exit status, ed ` il kernel che deve generare o e autonomamente il termination status per indicare le ragioni della conclusione anomala. Si noti la distinzione fra exit status e termination status: quello che contraddistingue lo stato di chiusura del processo e viene riportato attraverso le funzioni wait o waitpid (vedi sez. 3.2.4) ` sempre questultimo; in caso di conclusione normale il kernel usa il primo (nel codice eseguito e da _exit) per produrre il secondo. La scelta di riportare al padre lo stato di terminazione dei gli, pur essendo lunica possibile, comporta comunque alcune complicazioni: infatti se alla sua creazione ` scontato che ogni nuovo e processo ha un padre, non ` detto che sia cos` alla sua conclusione, dato che il padre potrebbe e essere gi` terminato (si potrebbe avere cio` quello che si chiama un processo orfano). a e Questa complicazione viene superata facendo in modo che il processo orfano venga adottato da init. Come gi` accennato quando un processo termina, il kernel controlla se ` il padre di a e altri processi in esecuzione: in caso positivo allora il ppid di tutti questi processi viene sostituito con il pid di init (e cio` con 1); in questo modo ogni processo avr` sempre un padre (nel caso e a possiamo parlare di un padre adottivo) cui riportare il suo stato di terminazione. Come verica di questo comportamento possiamo eseguire il nostro programma forktest imponendo a ciascun processo glio due secondi di attesa prima di uscire, il risultato `: e
[piccardi@selidor sources]$ ./forktest -c2 3 Process 1972: forking 3 child Spawned 1 child, pid 1973 Child 1 successfully executing Go to next child Spawned 2 child, pid 1974 Child 2 successfully executing Go to next child Child 3 successfully executing Spawned 3 child, pid 1975 Go to next child [piccardi@selidor sources]$ Child 3, parent 1, exiting Child 2, parent 1, exiting Child 1, parent 1, exiting

come si pu` notare in questo caso il processo padre si conclude prima dei gli, tornando alla o shell, che stampa il prompt sul terminale: circa due secondi dopo viene stampato a video anche loutput dei tre gli che terminano, e come si pu` notare in questo caso, al contrario di quanto o visto in precedenza, essi riportano 1 come ppid. Altrettanto rilevante ` il caso in cui il glio termina prima del padre, perch non ` detto che e e e il padre possa ricevere immediatamente lo stato di terminazione, quindi il kernel deve comunque conservare una certa quantit` di informazioni riguardo ai processi che sta terminando. a Questo viene fatto mantenendo attiva la voce nella tabella dei processi, e memorizzando alcuni dati essenziali, come il pid, i tempi di CPU usati dal processo (vedi sez. 8.4.1) e lo stato di terminazione, mentre la memoria in uso ed i le aperti vengono rilasciati immediatamente. I processi che sono terminati, ma il cui stato di terminazione non ` stato ancora ricevuto dal padre e sono chiamati zombie, essi restano presenti nella tabella dei processi ed in genere possono essere identicati dalloutput di ps per la presenza di una Z nella colonna che ne indica lo stato (vedi

48

CAPITOLO 3. LA GESTIONE DEI PROCESSI

tab. 3.11). Quando il padre eettuer` la lettura dello stato di uscita anche questa informazione, a non pi` necessaria, verr` scartata e la terminazione potr` dirsi completamente conclusa. u a a Possiamo utilizzare il nostro programma di prova per analizzare anche questa condizione: lanciamo il comando forktest in background (vedi sez. 10.1), indicando al processo padre di aspettare 10 secondi prima di uscire; in questo caso, usando ps sullo stesso terminale (prima dello scadere dei 10 secondi) otterremo:
[piccardi@selidor sources]$ ps T PID TTY STAT TIME COMMAND 419 pts/0 S 0:00 bash 568 pts/0 S 0:00 ./forktest -e10 3 569 pts/0 Z 0:00 [forktest <defunct>] 570 pts/0 Z 0:00 [forktest <defunct>] 571 pts/0 Z 0:00 [forktest <defunct>] 572 pts/0 R 0:00 ps T

e come si vede, dato che non si ` fatto nulla per riceverne lo stato di terminazione, i tre processi e gli sono ancora presenti pur essendosi conclusi, con lo stato di zombie e lindicazione che sono stati terminati. La possibilit` di avere degli zombie deve essere tenuta sempre presente quando si scrive un a programma che deve essere mantenuto in esecuzione a lungo e creare molti gli. In questo caso si deve sempre avere cura di far leggere leventuale stato di uscita di tutti i gli (in genere questo si fa attraverso un apposito signal handler, che chiama la funzione wait, vedi sez. 9.3.6 e sez. 3.2.4). Questa operazione ` necessaria perch anche se gli zombie non consumano risorse di memoria o e e processore, occupano comunque una voce nella tabella dei processi, che a lungo andare potrebbe esaurirsi. Si noti che quando un processo adottato da init termina, esso non diviene uno zombie; questo perch una delle funzioni di init ` appunto quella di chiamare la funzione wait per i e e processi cui fa da padre, completandone la terminazione. Questo ` quanto avviene anche quando, e come nel caso del precedente esempio con forktest, il padre termina con dei gli in stato di zombie: alla sua terminazione infatti tutti i suoi gli (compresi gli zombie) verranno adottati da init, il quale provveder` a completarne la terminazione. a Si tenga presente inne che siccome gli zombie sono processi gi` usciti, non c` modo di a e eliminarli con il comando kill; lunica possibilit` di cancellarli dalla tabella dei processi ` a e quella di terminare il processo che li ha generati, in modo che init possa adottarli e provvedere a concluderne la terminazione.

3.2.4

La funzione waitpid e le funzioni di ricezione degli stati di uscita

Uno degli usi pi` comuni delle capacit` multitasking di un sistema unix-like consiste nella creau a zione di programmi di tipo server, in cui un processo principale attende le richieste che vengono poi soddisfatte da una serie di processi gli. Si ` gi` sottolineato al paragrafo precedente come in e a questo caso diventi necessario gestire esplicitamente la conclusione dei gli onde evitare di riempire di zombie la tabella dei processi; le funzioni deputate a questo compito sono principalmente due, wait e waitpid. La prima, il cui prototipo `: e
#include <sys/types.h> #include <sys/wait.h> pid_t wait(int *status) Sospende il processo corrente nch un glio non ` uscito, o nch un segnale termina il e e e processo o chiama una funzione di gestione. La funzione restituisce il pid del glio in caso di successo e -1 in caso di errore; errno pu` assumere o i valori: EINTR la funzione ` stata interrotta da un segnale. e

3.2. LE FUNZIONI DI BASE

49

` presente n dalle prime versioni di Unix; la funzione ritorna non appena un processo glio e termina. Se un glio ` gi` terminato la funzione ritorna immediatamente, se pi` di un glio ` e a u e terminato occorre chiamare la funzione pi` volte se si vuole recuperare lo stato di terminazione u di tutti quanti. Al ritorno della funzione lo stato di terminazione del glio viene salvato nella variabile puntata da status e tutte le risorse del kernel relative al processo (vedi sez. 3.2.3) vengono rilasciate. Nel caso un processo abbia pi` gli il valore di ritorno (il pid del glio) permette di identicare u qual ` quello che ` uscito. e e Questa funzione ha il difetto di essere poco essibile, in quanto ritorna alluscita di un qualunque processo glio. Nelle occasioni in cui ` necessario attendere la conclusione di un e processo specico occorrerebbe predisporre un meccanismo che tenga conto dei processi gi` a terminati, e provvedere a ripetere la chiamata alla funzione nel caso il processo cercato sia ancora attivo. Per questo motivo lo standard POSIX.1 ha introdotto la funzione waitpid che eettua lo stesso servizio, ma dispone di una serie di funzionalit` pi` ampie, legate anche al controllo di a u sessione (si veda sez. 10.1). Dato che ` possibile ottenere lo stesso comportamento di wait9 si e consiglia di utilizzare sempre questa funzione, il cui prototipo `: e
#include <sys/types.h> #include <sys/wait.h> pid_t waitpid(pid_t pid, int *status, int options) Attende la conclusione di un processo glio. La funzione restituisce il pid del processo che ` uscito, 0 se ` stata specicata lopzione WNOHANG e e e il processo non ` uscito e -1 per un errore, nel qual caso errno assumer` i valori: e a EINTR ECHILD EINVAL non ` stata specicata lopzione WNOHANG e la funzione ` stata interrotta da un segnale. e e il processo specicato da pid non esiste o non ` glio del processo chiamante. e si ` specicato un valore non valido per largomento options. e

La prima dierenza fra le due funzioni ` che con waitpid si pu` specicare in maniera e o essibile quale processo attendere, sulla base del valore fornito dallargomento pid, questo pu` o assumere diversi valori, secondo lo specchietto riportato in tab. 3.1, dove si sono riportate anche le costanti denite per indicare alcuni di essi.
Valore < 1 1 0 >0 Costante WAIT_ANY WAIT_MYPGRP Signicato Attende per un glio il cui process group (vedi sez. 10.1.2) ` uguale al valore assoluto di pid. e Attende per un glio qualsiasi, usata in questa maniera senza specicare nessuna opzione ` equivalente a wait. e Attende per un glio il cui process group (vedi sez. 10.1.2) ` uguale a quello del processo chiamante. e Attende per un glio il cui pid ` uguale al valore di pid. e

Tabella 3.1: Signicato dei valori dellargomento pid della funzione waitpid.

Il comportamento di waitpid pu` inoltre essere modicato passando alla funzione delle o opportune opzioni tramite largomento options; questo deve essere specicato come maschera binaria dei ag riportati in tab. 3.2,10 che possono essere combinati fra loro con un OR aritmetico. Luso dellopzione WNOHANG consente di prevenire il blocco della funzione qualora nessun glio sia uscito (o non si siano vericate le altre condizioni per luscita della funzione); in tal caso la funzione ritorner` un valore nullo anzich positivo.11 a e
9 10

in eetti il codice wait(&status) ` del tutto equivalente a waitpid(WAIT_ANY, &status, 0). e oltre a queste in Linux sono previste del altre opzioni non standard, relative al comportamento con i thread,

50
Macro WNOHANG WUNTRACED WCONTINUED

CAPITOLO 3. LA GESTIONE DEI PROCESSI


Descrizione La funzione ritorna immediatamente anche se non ` e terminato nessun processo glio. Ritorna anche se un processo glio ` stato fermato. e Ritorna anche quando un processo glio che era stato fermato ha ripreso lesecuzione.12

Tabella 3.2: Costanti che identicano i bit dellargomento options della funzione waitpid.

Le altre due opzioni WUNTRACED e WCONTINUED consentono rispettivamente di tracciare non la terminazione di un processo, ma il fatto che esso sia stato fermato, o fatto ripartire, e sono utilizzate per la gestione del controllo di sessione (vedi sez. 10.1). Nel caso di WUNTRACED la funzione ritorna, restituendone il pid, quando un processo glio entra nello stato stopped 13 (vedi tab. 3.11), mentre con WCONTINUED la funzione ritorna quando un processo in stato stopped riprende lesecuzione per la ricezione del segnale SIGCONT (luso di questi segnali per il controllo di sessione ` dettagliato in sez. 10.1.3). e La terminazione di un processo glio (cos` come gli altri eventi osservabili con waitpid) ` chiaramente un evento asincrono rispetto allesecuzione di un programma e pu` avvenire in e o un qualunque momento. Per questo motivo, come accennato nella sezione precedente, una delle azioni prese dal kernel alla conclusione di un processo ` quella di mandare un segnale di SIGCHLD e al padre. Lazione predenita (si veda sez. 9.1.1) per questo segnale ` di essere ignorato, ma la e sua generazione costituisce il meccanismo di comunicazione asincrona con cui il kernel avverte il processo padre che uno dei suoi gli ` terminato. e Il comportamento delle funzioni ` per` cambiato nel passaggio dal kernel 2.4 al kernel 2.6, e o questultimo infatti si ` adeguato alle prescrizioni dello standard POSIX.1-2001,14 e come da e esso richiesto se SIGCHLD viene ignorato, o se si imposta il ag di SA_NOCLDSTOP nella ricezione dello stesso (si veda sez. 9.4.3) i processi gli che terminano non diventano zombie e sia wait che waitpid si bloccano ntanto che tutti i processi gli non sono terminati, dopo di che falliscono con un errore di ENOCHLD.15 Con i kernel della serie 2.4 e tutti i kernel delle serie precedenti entrambe le funzioni di attesa ignorano questa prescrizione16 e si comportano sempre nello stesso modo, indipendentemente dal fatto SIGCHLD sia ignorato o meno: attendono la terminazione di un processo glio e ritornano il relativo pid e lo stato di terminazione nellargomento status. In generale in un programma non si vuole essere forzati ad attendere la conclusione di un processo glio per proseguire lesecuzione, specie se tutto questo serve solo per leggerne lo stato di chiusura (ed evitare eventualmente la presenza di zombie). Per questo la modalit` pi` comune a u di chiamare queste funzioni ` quella di utilizzarle allinterno di un signal handler (vedremo un e esempio di come gestire SIGCHLD con i segnali in sez. 9.4.1). In questo caso infatti, dato che il segnale ` generato dalla terminazione di un glio, avremo la certezza che la chiamata a waitpid e non si bloccher`. a Come accennato sia wait che waitpid restituiscono lo stato di terminazione del processo
che riprenderemo in sez. ??. 11 anche in questo caso un valore positivo indicher` il pid del processo di cui si ` ricevuto lo stato ed un valore a e negativo un errore. 12 disponibile solo a partire dal kernel 2.6.10. 13 in realt` viene noticato soltanto il caso in cui il processo ` stato fermato da un segnale di stop (vedi sez. 10.1.3), a e e non quello in cui lo stato stopped ` dovuto alluso di ptrace (vedi sez. ??). e 14 una revisione del 2001 dello standard POSIX.1 che ha aggiunto dei requisiti e delle nuove funzioni, come waitid. 15 questo ` anche il motivo per cui le opzioni WUNTRACED e WCONTINUED sono utilizzabili soltanto qualora non si e sia impostato il ag di SA_NOCLDSTOP per il segnale SIGCHLD. 16 lo standard POSIX.1 originale infatti lascia indenito il comportamento di queste funzioni quando SIGCHLD viene ignorato.

3.2. LE FUNZIONI DI BASE

51

tramite il puntatore status (se non interessa memorizzare lo stato si pu` passare un puntatore o nullo). Il valore restituito da entrambe le funzioni dipende dallimplementazione, ma tradizionalmente alcuni bit (in genere 8) sono riservati per memorizzare lo stato di uscita, e altri per indicare il segnale che ha causato la terminazione (in caso di conclusione anomala), uno per indicare se ` stato generato un core dump, ecc.17 e Lo standard POSIX.1 denisce una serie di macro di preprocessore da usare per analizzare lo stato di uscita. Esse sono denite sempre in <sys/wait.h> ed elencate in tab. 3.3 (si tenga presente che queste macro prendono come parametro la variabile di tipo int puntata da status).
Macro WIFEXITED(s) WEXITSTATUS(s) Descrizione Condizione vera (valore non nullo) per un processo glio che sia terminato normalmente. Restituisce gli otto bit meno signicativi dello stato di uscita del processo (passato attraverso _exit, exit o come valore di ritorno di main); pu` essere valutata solo se WIFEXITED ha restituito un valore non nullo. o Condizione vera se il processo glio ` terminato in maniera anomala a e causa di un segnale che non ` stato catturato (vedi sez. 9.1.4). e Restituisce il numero del segnale che ha causato la terminazione anomala del processo; pu` essere valutata solo se WIFSIGNALED ha restituito o un valore non nullo. Vera se il processo terminato ha generato un le di core dump; pu` o essere valutata solo se WIFSIGNALED ha restituito un valore non nullo.18 Vera se il processo che ha causato il ritorno di waitpid ` bloccato; luso e ` possibile solo con waitpid avendo specicato lopzione WUNTRACED. e Restituisce il numero del segnale che ha bloccato il processo; pu` essere o valutata solo se WIFSTOPPED ha restituito un valore non nullo. Vera se il processo che ha causato il ritorno ` stato riavviato da un e SIGCONT.19

WIFSIGNALED(s) WTERMSIG(s)

WCOREDUMP(s) WIFSTOPPED(s) WSTOPSIG(s) WIFCONTINUED(s)

Tabella 3.3: Descrizione delle varie macro di preprocessore utilizzabili per vericare lo stato di terminazione s di un processo.

Si tenga conto che nel caso di conclusione anomala il valore restituito da WTERMSIG pu` essere o confrontato con le costanti denite in signal.h ed elencate in tab. 9.3, e stampato usando le apposite funzioni trattate in sez. 9.2.9. A partire dal kernel 2.6.9, sempre in conformit` allo standard POSIX.1-2001, ` stata ina e trodotta una nuova funzione di attesa che consente di avere un controllo molto pi` preciso sui u possibili cambiamenti di stato dei processi gli e pi` dettagli sullo stato di uscita; la funzione ` u e waitid ed il suo prototipo `: e
#include <sys/types.h> #include <sys/wait.h> int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options) Attende la conclusione di un processo glio. La funzione restituisce 0 in caso di successo e -1 per un errore, nel qual caso errno assumer` i a valori: EINTR ECHILD EINVAL
17

se non ` stata specicata lopzione WNOHANG e la funzione ` stata interrotta da un e e segnale. il processo specicato da pid non esiste o non ` glio del processo chiamante. e si ` specicato un valore non valido per largomento options. e

le denizioni esatte si possono trovare in <bits/waitstatus.h> ma questo le non deve mai essere usato direttamente, esso viene incluso attraverso <sys/wait.h>. 18 questa macro non ` denita dallo standard POSIX.1-2001, ma ` presente come estensione sia in Linux che e e in altri Unix, deve essere pertanto utilizzata con attenzione (ad esempio ` il caso di usarla in un blocco #ifdef e WCOREDUMP ... #endif. 19 ` presente solo a partire dal kernel 2.6.10. e

52

CAPITOLO 3. LA GESTIONE DEI PROCESSI

La funzione prevede che si specichi quali processi si intendono osservare usando i due argomenti idtype ed id; il primo indica se si vuole porsi in attesa su un singolo processo, un gruppo di processi o un processo qualsiasi, e deve essere specicato secondo uno dei valori di tab. 3.4; il secondo indica, a seconda del valore del primo, quale processo o quale gruppo di processi selezionare.
Macro P_PID P_PGID Descrizione Indica la richiesta di attendere per un processo glio il cui pid corrisponda al valore dellargomento id. Indica la richiesta di attendere per un processo glio appartenente al process group (vedi sez. 10.1.2) il cui pgid corrisponda al valore dellargomento id. Indica la richiesta di attendere per un processo glio generico, il valore dellargomento id viene ignorato.

P_ALL

Tabella 3.4: Costanti per i valori dellargomento idtype della funzione waitid.

Come per waitpid anche il comportamento di waitid viene controllato dallargomento options, da specicare come maschera binaria dei valori riportati in tab. 3.5. Bench alcuni di e questi siano identici come signicato ed eetto ai precedenti di tab. 3.2, ci sono delle dierenze signicative: in questo caso si dovr` specicare esplicitamente lattesa della terminazione di un a processo impostando lopzione WEXITED, mentre il precedente WUNTRACED ` sostituito da WSTOPe PED. Inne ` stata aggiunta lopzione WNOWAIT che consente una lettura dello stato mantenendo e il processo in attesa di ricezione, cos` che una successiva chiamata possa di nuovo riceverne lo stato.
Macro WEXITED WNOHANG WSTOPPED WCONTINUED WNOWAIT Descrizione Ritorna quando un processo glio ` terminato. e Ritorna immediatamente anche se non c` niente da e noticare. Ritorna quando un processo glio ` stato fermato. e Ritorna quando un processo glio che era stato fermato ha ripreso lesecuzione. Lascia il processo ancora in attesa di ricezione, cos` che una successiva chiamata possa di nuovo riceverne lo stato.

Tabella 3.5: Costanti che identicano i bit dellargomento options della funzione waitid.

La funzione waitid restituisce un valore nullo in caso di successo, e 1 in caso di errore; viene restituito un valore nullo anche se ` stata specicata lopzione WNOHANG e la funzione ` ritornata e e immediatamente senza che nessun glio sia terminato. Pertanto per vericare il motivo del ritorno della funzione occorre analizzare le informazioni che essa restituisce; queste, al contrario delle precedenti wait e waitpid, sono ritornate nella struttura di tipo siginfo_t (vedi g. 9.9) allindirizzo puntato dallargomento infop. Tratteremo nei dettagli questa struttura ed il signicato dei suoi vari campi in sez. 9.4.3, per quanto ci interessa qui basta dire che al ritorno di waitid verranno avvalorati i seguenti campi: si_pid si_uid si_signo con il pid del glio. con luser-ID reale (vedi sez. 3.3) del glio. con SIGCHLD.

si_status con lo stato di uscita del glio o con il segnale che lo ha terminato, fermato o riavviato. si_code con uno fra CLD_EXITED, CLD_KILLED, CLD_STOPPED, CLD_CONTINUED (vedi tab. ??).

3.2. LE FUNZIONI DI BASE

53

Inne Linux, seguendo unestensione di BSD, supporta altre due funzioni per la lettura dello stato di terminazione di un processo, analoghe alle precedenti ma che prevedono un ulteriore argomento attraverso il quale il kernel pu` restituire al padre informazioni sulle risorse usate dal o processo terminato e dai vari gli. Le due funzioni sono wait3 e wait4, che diventano accessibili denendo la macro _USE_BSD; i loro prototipi sono:
#include <sys/times.h> #include <sys/types.h> #include <sys/wait.h> #include <sys/resource.h> pid_t wait4(pid_t pid, int *status, int options, struct rusage *rusage) ` E identica a waitpid sia per comportamento che per i valori degli argomenti, ma restituisce in rusage un sommario delle risorse usate dal processo. pid_t wait3(int *status, int options, struct rusage *rusage) Prima versione, equivalente a wait4(-1, &status, opt, rusage) ` ormai deprecata in e favore di wait4.

la struttura rusage ` denita in sys/resource.h, e viene utilizzata anche dalla funzione getrue sage (vedi sez. 8.3.1) per ottenere le risorse di sistema usate da un processo; la sua denizione ` riportata in g. 8.6. e

3.2.5

Le funzioni exec

Abbiamo gi` detto che una delle modalit` principali con cui si utilizzano i processi in Unix ` a a e quella di usarli per lanciare nuovi programmi: questo viene fatto attraverso una delle funzioni della famiglia exec. Quando un processo chiama una di queste funzioni esso viene completamente sostituito dal nuovo programma; il pid del processo non cambia, dato che non viene creato un nuovo processo, la funzione semplicemente rimpiazza lo stack, lo heap, i dati ed il testo del processo corrente con un nuovo programma letto da disco. Ci sono sei diverse versioni di exec (per questo la si ` chiamata famiglia di funzioni) che e possono essere usate per questo compito, in realt` (come mostrato in g. 3.4), sono tutte un a front-end a execve. Il prototipo di questultima `: e
#include <unistd.h> int execve(const char *filename, char *const argv[], char *const envp[]) Esegue il programma contenuto nel le filename. La funzione ritorna solo in caso di errore, restituendo -1; nel qual caso errno pu` assumere i valori: o EACCES EPERM ENOEXEC ENOENT ETXTBSY EINVAL ELIBBAD E2BIG il le non ` eseguibile, oppure il lesystem ` montato in noexec, oppure non ` un le e e e regolare o un interprete. il le ha i bit suid o sgid, lutente non ` root, il processo viene tracciato, o il lesystem e ` montato con lopzione nosuid. e il le ` in un formato non eseguibile o non riconosciuto come tale, o compilato per e unaltra architettura. il le o una delle librerie dinamiche o linterprete necessari per eseguirlo non esistono. leseguibile ` aperto in scrittura da uno o pi` processi. e u leseguibile ELF ha pi` di un segmento PF_INTERP, cio` chiede di essere eseguito da u e pi` di un interprete. u un interprete ELF non ` in un formato riconoscibile. e la lista degli argomenti ` troppo grande. e

ed inoltre anche EFAULT, ENOMEM, EIO, ENAMETOOLONG, ELOOP, ENOTDIR, ENFILE, EMFILE.

La funzione exec esegue il le o lo script indicato da filename, passandogli la lista di argomenti indicata da argv e come ambiente la lista di stringhe indicata da envp; entrambe le liste devono essere terminate da un puntatore nullo. I vettori degli argomenti e dellambiente

54

CAPITOLO 3. LA GESTIONE DEI PROCESSI

possono essere acceduti dal nuovo programma quando la sua funzione main ` dichiarata nella e forma main(int argc, char *argv[], char *envp[]). Le altre funzioni della famiglia servono per fornire allutente una serie di possibili diverse interfacce per la creazione di un nuovo processo. I loro prototipi sono:
#include <unistd.h> int execl(const char *path, const char *arg, ...) int execv(const char *path, char *const argv[]) int execle(const char *path, const char *arg, ..., char * const envp[]) int execlp(const char *file, const char *arg, ...) int execvp(const char *file, char *const argv[]) Sostituiscono limmagine corrente del processo con quella indicata nel primo argomento. Gli argomenti successivi consentono di specicare gli argomenti a linea di comando e lambiente ricevuti dal nuovo processo. Queste funzioni ritornano solo in caso di errore, restituendo -1; nel qual caso errno assumer` i a valori visti in precedenza per execve.

Per capire meglio le dierenze fra le funzioni della famiglia si pu` fare riferimento allo speco chietto riportato in tab. 3.6. La prima dierenza riguarda le modalit` di passaggio dei valori che a poi andranno a costituire gli argomenti a linea di comando (cio` i valori di argv e argc visti e dalla funzione main del programma chiamato). Queste modalit` sono due e sono riassunte dagli mnemonici v e l che stanno rispettivamente a per vector e list. Nel primo caso gli argomenti sono passati tramite il vettore di puntatori argv[] a stringhe terminate con zero che costituiranno gli argomenti a riga di comando, questo vettore deve essere terminato da un puntatore nullo. Nel secondo caso le stringhe degli argomenti sono passate alla funzione come lista di puntatori, nella forma: char * arg0 , char * arg1 , ... , char * argn , NULL

che deve essere terminata da un puntatore nullo. In entrambi i casi vale la convenzione che il primo argomento (arg0 o argv[0]) viene usato per indicare il nome del le che contiene il programma che verr` eseguito. a
Caratteristiche argomenti a lista argomenti a vettore lename completo ricerca su PATH ambiente a vettore uso di environ execl execlp Funzioni execle execv execvp execve

Tabella 3.6: Confronto delle caratteristiche delle varie funzioni della famiglia exec.

La seconda dierenza fra le funzioni riguarda le modalit` con cui si specica il programma che a si vuole eseguire. Con lo mnemonico p si indicano le due funzioni che replicano il comportamento della shell nello specicare il comando da eseguire; quando largomento file non contiene una / esso viene considerato come un nome di programma, e viene eseguita automaticamente una ricerca fra i le presenti nella lista di directory specicate dalla variabile di ambiente PATH. Il le che viene posto in esecuzione ` il primo che viene trovato. Se si ha un errore relativo a permessi e di accesso insucienti (cio` lesecuzione della sottostante execve ritorna un EACCES), la ricerca e viene proseguita nelle eventuali ulteriori directory indicate in PATH; solo se non viene trovato nessun altro le viene nalmente restituito EACCES. Le altre quattro funzioni si limitano invece a cercare di eseguire il le indicato dallargomento path, che viene interpretato come il pathname del programma.

3.2. LE FUNZIONI DI BASE

55

Figura 3.4: La interrelazione fra le sei funzioni della famiglia exec.

La terza dierenza ` come viene passata la lista delle variabili di ambiente. Con lo mnemonico e e vengono indicate quelle funzioni che necessitano di un vettore di parametri envp[] analogo a quello usato per gli argomenti a riga di comando (terminato quindi da un NULL), le altre usano il valore della variabile environ (vedi sez. 2.3.4) del processo di partenza per costruire lambiente. Oltre a mantenere lo stesso pid, il nuovo programma fatto partire da exec assume anche una serie di altre propriet` del processo chiamante; la lista completa ` la seguente: a e il process id (pid) ed il parent process id (ppid); luser-ID reale, il group-ID reale ed i group-ID supplementari (vedi sez. 3.3.1); il session ID (sid) ed il process group ID (pgid), vedi sez. 10.1.2; il terminale di controllo (vedi sez. 10.1.3); il tempo restante ad un allarme (vedi sez. 9.3.4); la directory radice e la directory di lavoro corrente (vedi sez. 5.1.7); la maschera di creazione dei le (umask, vedi sez. 5.3.3) ed i lock sui le (vedi sez. 11.4); i segnali sospesi (pending) e la maschera dei segnali (si veda sez. 9.4.4); i limiti sulle risorse (vedi sez. 8.3.2); i valori delle variabili tms_utime, tms_stime, tms_cutime, tms_ustime (vedi sez. 8.4.2).

Inoltre i segnali che sono stati impostati per essere ignorati nel processo chiamante mantengono la stessa impostazione pure nel nuovo programma, tutti gli altri segnali vengono impostati alla loro azione predenita. Un caso speciale ` il segnale SIGCHLD che, quando impostato a e SIG_IGN, pu` anche non essere reimpostato a SIG_DFL (si veda sez. 9.3.1). o La gestione dei le aperti dipende dal valore che ha il ag di close-on-exec (vedi anche sez. 6.3.6) per ciascun le descriptor. I le per cui ` impostato vengono chiusi, tutti gli altri e le restano aperti. Questo signica che il comportamento predenito ` che i le restano aperti e attraverso una exec, a meno di una chiamata esplicita a fcntl che imposti il suddetto ag. Per le directory, lo standard POSIX.1 richiede che esse vengano chiuse attraverso una exec, in genere questo ` fatto dalla funzione opendir (vedi sez. 5.1.6) che eettua da sola limpostazione e del ag di close-on-exec sulle directory che apre, in maniera trasparente allutente. Abbiamo detto che luser-ID reale ed il group-ID reale restano gli stessi allesecuzione di exec; lo stesso vale per luser-ID eettivo ed il group-ID eettivo (il signicato di questi identicatori ` trattato in sez. 3.3.1), tranne quando il le che si va ad eseguire abbia o il suid bit o lo e sgid bit impostato, in questo caso luser-ID eettivo ed il group-ID eettivo vengono impostati rispettivamente allutente o al gruppo cui il le appartiene (per i dettagli vedi sez. 3.3). Se il le da eseguire ` in formato a.out e necessita di librerie condivise, viene lanciato il linker e dinamico /lib/ld.so prima del programma per caricare le librerie necessarie ed eettuare il

56

CAPITOLO 3. LA GESTIONE DEI PROCESSI

link delleseguibile. Se il programma ` in formato ELF per caricare le librerie dinamiche viene e usato linterprete indicato nel segmento PT_INTERP, in genere questo ` /lib/ld-linux.so.1 per e programmi collegati con le libc5, e /lib/ld-linux.so.2 per programmi collegati con le glibc. Inne nel caso il le sia uno script esso deve iniziare con una linea nella forma #!/path/to/interpreter [argomenti] dove linterprete indicato deve essere un programma valido (binario, non un altro script) che verr` chiamato come se si fosse eseguito il comando interpreter a [argomenti] filename.20 Con la famiglia delle exec si chiude il novero delle funzioni su cui ` basata la gestione dei e processi in Unix: con fork si crea un nuovo processo, con exec si lancia un nuovo programma, con exit e wait si eettua e verica la conclusione dei processi. Tutte le altre funzioni sono ausiliarie e servono per la lettura e limpostazione dei vari parametri connessi ai processi.

3.3

Il controllo di accesso

In questa sezione esamineremo le problematiche relative al controllo di accesso dal punto di vista dei processi; vedremo quali sono gli identicatori usati, come questi possono essere modicati nella creazione e nel lancio di nuovi processi, le varie funzioni per la loro manipolazione diretta e tutte le problematiche connesse ad una gestione accorta dei privilegi.

3.3.1

Gli identicatori del controllo di accesso

Come accennato in sez. 1.1.4 il modello base21 di sicurezza di un sistema unix-like ` fondato sui e concetti di utente e gruppo, e sulla separazione fra lamministratore (root, detto spesso anche superuser ) che non ` sottoposto a restrizioni, ed il resto degli utenti, per i quali invece vengono e eettuati i vari controlli di accesso. Abbiamo gi` accennato come il sistema associ ad ogni utente e gruppo due identicatori a univoci, lo user-ID ed il group-ID; questi servono al kernel per identicare uno specico utente o un gruppo di utenti, per poi poter controllare che essi siano autorizzati a compiere le operazioni richieste. Ad esempio in sez. 5.3 vedremo come ad ogni le vengano associati un utente ed un gruppo (i suoi proprietari, indicati appunto tramite un uid ed un gid) che vengono controllati dal kernel nella gestione dei permessi di accesso. Dato che tutte le operazioni del sistema vengono compiute dai processi, ` evidente che per e poter implementare un controllo sulle operazioni occorre anche poter identicare chi ` che ha e lanciato un certo programma, e pertanto anche a ciascun processo dovr` essere associato un a utente e un gruppo. Un semplice controllo di una corrispondenza fra identicativi non garantisce per` suciente o essibilit` per tutti quei casi in cui ` necessario poter disporre di privilegi diversi, o dover a e impersonare un altro utente per un limitato insieme di operazioni. Per questo motivo in generale tutti gli Unix prevedono che i processi abbiano almeno due gruppi di identicatori, chiamati rispettivamente real ed eective (cio` reali ed eettivi). Nel caso di Linux si aggiungono poi e
si tenga presente che con Linux quanto viene scritto come argomenti viene passato allinterprete come un unico argomento con una unica stringa di lunghezza massima di 127 caratteri e se questa dimensione viene ecceduta la stringa viene troncata; altri Unix hanno dimensioni massime diverse, e diversi comportamenti, ad esempio FreeBSD esegue la scansione della riga e la divide nei vari argomenti e se ` troppo lunga restituisce un errore di ENAMETOOLONG, una comparazione dei vari comportamenti si trova su e http://www.in-ulm.de/~mascheck/various/shebang/. 21 in realt` gi` esistono estensioni di questo modello base, che lo rendono pi` essibile e controllabile, come le a a u capabilities illustrate in sez. 3.3.4, le ACL per i le (vedi sez. 5.4.2) o il Mandatory Access Control di SELinux; inoltre basandosi sul lavoro eettuato con SELinux, a partire dal kernel 2.5.x, ` iniziato lo sviluppo di una e infrastruttura di sicurezza, i Linux Security Modules, o LSM, in grado di fornire diversi agganci a livello del kernel per modularizzare tutti i possibili controlli di accesso.
20

3.3. IL CONTROLLO DI ACCESSO

57

altri due gruppi, il saved (salvati) ed il lesystem (di lesystem), secondo la situazione illustrata in tab. 3.7.
Susso uid gid euid egid fsuid fsgid Gruppo real eective saved lesystem Denominazione user-ID reale group-ID reale user-ID eettivo group-ID eettivo group-ID supplementari user-ID salvato group-ID salvato user-ID di lesystem group-ID di lesystem Signicato Indica lutente che ha lanciato il programma. Indica il gruppo principale dellutente che ha lanciato il programma. Indica lutente usato nel controllo di accesso. Indica il gruppo usato nel controllo di accesso. Indicano gli ulteriori gruppi cui lutente appartiene. ` E una copia delleuid iniziale. ` E una copia dellegid iniziale. Indica lutente eettivo per laccesso al lesystem. Indica il gruppo eettivo per laccesso al lesystem.

Tabella 3.7: Identicatori di utente e gruppo associati a ciascun processo con indicazione dei sussi usati dalle varie funzioni di manipolazione.

Al primo gruppo appartengono luser-ID reale ed il group-ID reale: questi vengono impostati al login ai valori corrispondenti allutente con cui si accede al sistema (e relativo gruppo principale). Servono per lidenticazione dellutente e normalmente non vengono mai cambiati. In realt` vedremo (in sez. 3.3.2) che ` possibile modicarli, ma solo ad un processo che abbia a e i privilegi di amministratore; questa possibilit` ` usata proprio dal programma login che, una ae volta completata la procedura di autenticazione, lancia una shell per la quale imposta questi identicatori ai valori corrispondenti allutente che entra nel sistema. Al secondo gruppo appartengono lo user-ID eettivo ed il group-ID eettivo (a cui si aggiungono gli eventuali group-ID supplementari dei gruppi dei quali lutente fa parte). Questi sono invece gli identicatori usati nelle veriche dei permessi del processo e per il controllo di accesso ai le (argomento arontato in dettaglio in sez. 5.3.1). Questi identicatori normalmente sono identici ai corrispondenti del gruppo real tranne nel caso in cui, come accennato in sez. 3.2.5, il programma che si ` posto in esecuzione abbia i e bit suid o sgid impostati (il signicato di questi bit ` arontato in dettaglio in sez. 5.3.2). In e questo caso essi saranno impostati allutente e al gruppo proprietari del le. Questo consente, per programmi in cui ci sia necessit`, di dare a qualunque utente normale privilegi o permessi a di un altro (o dellamministratore). Come nel caso del pid e del ppid, anche tutti questi identicatori possono essere letti attraverso le rispettive funzioni: getuid, geteuid, getgid e getegid, i loro prototipi sono:
#include <unistd.h> #include <sys/types.h> uid_t getuid(void) Restituisce luser-ID reale del processo corrente. uid_t geteuid(void) Restituisce luser-ID eettivo del processo corrente. gid_t getgid(void) Restituisce il group-ID reale del processo corrente. gid_t getegid(void) Restituisce il group-ID eettivo del processo corrente. Queste funzioni non riportano condizioni di errore.

In generale luso di privilegi superiori deve essere limitato il pi` possibile, per evitare abusi e u problemi di sicurezza, per questo occorre anche un meccanismo che consenta ad un programma di rilasciare gli eventuali maggiori privilegi necessari, una volta che si siano eettuate le operazioni per i quali erano richiesti, e a poterli eventualmente recuperare in caso servano di nuovo. Questo in Linux viene fatto usando altri due gruppi di identicatori, il saved ed il lesystem. Il primo gruppo ` lo stesso usato in SVr4, e previsto dallo standard POSIX quando ` denita la e e

58

CAPITOLO 3. LA GESTIONE DEI PROCESSI

costante _POSIX_SAVED_IDS,22 il secondo gruppo ` specico di Linux e viene usato per migliorare e la sicurezza con NFS. Luser-ID salvato ed il group-ID salvato sono copie delluser-ID eettivo e del group-ID eettivo del processo padre, e vengono impostati dalla funzione exec allavvio del processo, come copie delluser-ID eettivo e del group-ID eettivo dopo che questi sono stati impostati tenendo conto di eventuali suid o sgid. Essi quindi consentono di tenere traccia di quale fossero utente e gruppo eettivi allinizio dellesecuzione di un nuovo programma. Luser-ID di lesystem e il group-ID di lesystem sono unestensione introdotta in Linux per rendere pi` sicuro luso di NFS (torneremo sullargomento in sez. 3.3.2). Essi sono una replica dei u corrispondenti identicatori del gruppo eective, ai quali si sostituiscono per tutte le operazioni di verica dei permessi relativi ai le (trattate in sez. 5.3.1). Ogni cambiamento eettuato sugli identicatori eettivi viene automaticamente riportato su di essi, per cui in condizioni normali si pu` tranquillamente ignorarne lesistenza, in quanto saranno del tutto equivalenti ai precedenti. o

3.3.2

Le funzioni di gestione degli identicatori dei processi

Le due funzioni pi` comuni che vengono usate per cambiare identit` (cio` utente e gruppo u a e di appartenenza) ad un processo sono rispettivamente setuid e setgid; come accennato in sez. 3.3.1 in Linux esse seguono la semantica POSIX che prevede lesistenza delluser-ID salvato e del group-ID salvato; i loro prototipi sono:
#include <unistd.h> #include <sys/types.h> int setuid(uid_t uid) Imposta luser-ID del processo corrente. int setgid(gid_t gid) Imposta il group-ID del processo corrente. Le funzioni restituiscono 0 in caso di successo e -1 in caso di fallimento: lunico errore possibile ` e EPERM.

Il funzionamento di queste due funzioni ` analogo, per cui considereremo solo la prima; la e seconda si comporta esattamente allo stesso modo facendo riferimento al group-ID invece che alluser-ID. Gli eventuali group-ID supplementari non vengono modicati. Leetto della chiamata ` diverso a seconda dei privilegi del processo; se luser-ID eettivo ` e e zero (cio` ` quello dellamministratore di sistema) allora tutti gli identicatori (real, eective e ee saved ) vengono impostati al valore specicato da uid, altrimenti viene impostato solo luser-ID eettivo, e soltanto se il valore specicato corrisponde o alluser-ID reale o alluser-ID salvato. Negli altri casi viene segnalato un errore (con EPERM). Come accennato luso principale di queste funzioni ` quello di poter consentire ad un proe gramma con i bit suid o sgid impostati (vedi sez. 5.3.2) di riportare luser-ID eettivo a quello dellutente che ha lanciato il programma, eettuare il lavoro che non necessita di privilegi aggiuntivi, ed eventualmente tornare indietro. Come esempio per chiarire luso di queste funzioni prendiamo quello con cui viene gestito laccesso al le /var/log/utmp. In questo le viene registrato chi sta usando il sistema al momento corrente; chiaramente non pu` essere lasciato aperto in scrittura a qualunque utente, che o potrebbe falsicare la registrazione. Per questo motivo questo le (e lanalogo /var/log/wtmp su cui vengono registrati login e logout) appartengono ad un gruppo dedicato (utmp) ed i programmi che devono accedervi (ad esempio tutti i programmi di terminale in X, o il programma screen che crea terminali multipli su una console) appartengono a questo gruppo ed hanno il bit sgid impostato.
in caso si abbia a cuore la portabilit` del programma su altri Unix ` buona norma controllare sempre la a e disponibilit` di queste funzioni controllando se questa costante ` denita. a e
22

3.3. IL CONTROLLO DI ACCESSO

59

Quando uno di questi programmi (ad esempio xterm) viene lanciato, la situazione degli identicatori ` la seguente: e group-ID reale = gid (del chiamante) group-ID eettivo = utmp group-ID salvato = utmp in questo modo, dato che il group-ID eettivo ` quello giusto, il programma pu` accedere a e o /var/log/utmp in scrittura ed aggiornarlo. A questo punto il programma pu` eseguire una o setgid(getgid()) per impostare il group-ID eettivo a quello dellutente (e dato che il groupID reale corrisponde la funzione avr` successo), in questo modo non sar` possibile lanciare dal a a terminale programmi che modicano detto le, in tal caso infatti la situazione degli identicatori sarebbe: group-ID reale = gid (invariato) group-ID eettivo = gid group-ID salvato = utmp (invariato) e ogni processo lanciato dal terminale avrebbe comunque gid come group-ID eettivo. Alluscita dal terminale, per poter di nuovo aggiornare lo stato di /var/log/utmp il programma eseguir` una setgid(utmp) (dove utmp ` il valore numerico associato al gruppo utmp, ottenuto ad a e esempio con una precedente getegid), dato che in questo caso il valore richiesto corrisponde al group-ID salvato la funzione avr` successo e riporter` la situazione a: a a group-ID reale = gid (invariato) group-ID eettivo = utmp group-ID salvato = utmp (invariato) consentendo laccesso a /var/log/utmp. Occorre per` tenere conto che tutto questo non ` possibile con un processo con i privilegi o e di amministratore, in tal caso infatti lesecuzione di una setuid comporta il cambiamento di tutti gli identicatori associati al processo, rendendo impossibile riguadagnare i privilegi di amministratore. Questo comportamento ` corretto per luso che ne fa login una volta che crea e una nuova shell per lutente; ma quando si vuole cambiare soltanto luser-ID eettivo del processo per cedere i privilegi occorre ricorrere ad altre funzioni. Le due funzioni setreuid e setregid derivano da BSD che, non supportando23 gli identicatori del gruppo saved, le usa per poter scambiare fra di loro eective e real. I rispettivi prototipi sono:
#include <unistd.h> #include <sys/types.h> int setreuid(uid_t ruid, uid_t euid) Imposta luser-ID reale e luser-ID eettivo del processo corrente ai valori specicati da ruid e euid. int setregid(gid_t rgid, gid_t egid) Imposta il group-ID reale ed il group-ID eettivo del processo corrente ai valori specicati da rgid e egid. Le funzioni restituiscono 0 in caso di successo e -1 in caso di fallimento: lunico errore possibile ` e EPERM.

La due funzioni sono analoghe ed il loro comportamento ` identico; quanto detto per la e prima riguardo luser-ID, si applica immediatamente alla seconda per il group-ID. I processi
23

almeno no alla versione 4.3+BSD.

60

CAPITOLO 3. LA GESTIONE DEI PROCESSI

non privilegiati possono impostare solo i valori del loro user-ID eettivo o reale; valori diversi comportano il fallimento della chiamata; lamministratore invece pu` specicare un valore o qualunque. Specicando un argomento di valore -1 lidenticatore corrispondente verr` lasciato a inalterato. Con queste funzioni si possono scambiare fra loro gli user-ID reale e eettivo, e pertanto ` possibile implementare un comportamento simile a quello visto in precedenza per setgid, e cedendo i privilegi con un primo scambio, e recuperandoli, eseguito il lavoro non privilegiato, con un secondo scambio. In questo caso per` occorre porre molta attenzione quando si creano nuovi processi nella o fase intermedia in cui si sono scambiati gli identicatori, in questo caso infatti essi avranno un user-ID reale privilegiato, che dovr` essere esplicitamente eliminato prima di porre in esecuzione a un nuovo programma (occorrer` cio` eseguire unaltra chiamata dopo la fork e prima della exec a e per uniformare luser-ID reale a quello eettivo) in caso contrario il nuovo programma potrebbe a sua volta eettuare uno scambio e riottenere privilegi non previsti. Lo stesso problema di propagazione dei privilegi ad eventuali processi gli si pone per luserID salvato: questa funzione deriva da unimplementazione che non ne prevede la presenza, e quindi non ` possibile usarla per correggere la situazione come nel caso precedente. Per questo e motivo in Linux tutte le volte che si imposta un qualunque valore diverso da quello dalluserID reale corrente, luser-ID salvato viene automaticamente uniformato al valore delluser-ID eettivo. Altre due funzioni, seteuid e setegid, sono unestensione dello standard POSIX.1, ma sono comunque supportate dalla maggior parte degli Unix; esse vengono usate per cambiare gli identicatori del gruppo eective ed i loro prototipi sono:
#include <unistd.h> #include <sys/types.h> int seteuid(uid_t uid) Imposta luser-ID eettivo del processo corrente a uid. int setegid(gid_t gid) Imposta il group-ID eettivo del processo corrente a gid. Le funzioni restituiscono 0 in caso di successo e -1 in caso di fallimento: lunico errore ` EPERM. e

Come per le precedenti le due funzioni sono identiche, per cui tratteremo solo la prima. Gli utenti normali possono impostare luser-ID eettivo solo al valore delluser-ID reale o delluserID salvato, lamministratore pu` specicare qualunque valore. Queste funzioni sono usate per o permettere allamministratore di impostare solo luser-ID eettivo, dato che luso normale di setuid comporta limpostazione di tutti gli identicatori. Le due funzioni setresuid e setresgid sono invece unestensione introdotta in Linux,24 e permettono un completo controllo su tutti e tre i gruppi di identicatori (real, eective e saved ), i loro prototipi sono:
#include <unistd.h> #include <sys/types.h> int setresuid(uid_t ruid, uid_t euid, uid_t suid) Imposta luser-ID reale, luser-ID eettivo e luser-ID salvato del processo corrente ai valori specicati rispettivamente da ruid, euid e suid. int setresgid(gid_t rgid, gid_t egid, gid_t sgid) Imposta il group-ID reale, il group-ID eettivo ed il group-ID salvato del processo corrente ai valori specicati rispettivamente da rgid, egid e sgid. Le funzioni restituiscono 0 in caso di successo e -1 in caso di fallimento: lunico errore ` EPERM. e

Le due funzioni sono identiche, quanto detto per la prima riguardo gli user-ID si applica alla seconda per i group-ID. I processi non privilegiati possono cambiare uno qualunque degli
24

per essere precisi a partire dal kernel 2.1.44.

3.3. IL CONTROLLO DI ACCESSO

61

user-ID solo ad un valore corrispondente o alluser-ID reale, o a quello eettivo o a quello salvato, lamministratore pu` specicare i valori che vuole; un valore di -1 per un qualunque argomento o lascia inalterato lidenticatore corrispondente. Per queste funzioni esistono anche due controparti che permettono di leggere in blocco i vari identicatori: getresuid e getresgid; i loro prototipi sono:
#include <unistd.h> #include <sys/types.h> int getresuid(uid_t *ruid, uid_t *euid, uid_t *suid) Legge luser-ID reale, luser-ID eettivo e luser-ID salvato del processo corrente. int getresgid(gid_t *rgid, gid_t *egid, gid_t *sgid) Legge il group-ID reale, il group-ID eettivo e il group-ID salvato del processo corrente. Le funzioni restituiscono 0 in caso di successo e -1 in caso di fallimento: lunico errore possibile ` e EFAULT se gli indirizzi delle variabili di ritorno non sono validi.

Anche queste funzioni sono unestensione specica di Linux, e non richiedono nessun privilegio. I valori sono restituiti negli argomenti, che vanno specicati come puntatori (` un altro e esempio di value result argument). Si noti che queste funzioni sono le uniche in grado di leggere gli identicatori del gruppo saved. Inne le funzioni setfsuid e setfsgid servono per impostare gli identicatori del gruppo lesystem che sono usati da Linux per il controllo dellaccesso ai le. Come gi` accennato in a sez. 3.3.1 Linux denisce questo ulteriore gruppo di identicatori, che in circostanze normali sono assolutamente equivalenti a quelli del gruppo eective, dato che ogni cambiamento di questi ultimi viene immediatamente riportato su di essi. C` un solo caso in cui si ha necessit` di introdurre una dierenza fra gli identicatori dei e a gruppi eective e lesystem, ed ` per ovviare ad un problema di sicurezza che si presenta quando e si deve implementare un server NFS. Il server NFS infatti deve poter cambiare lidenticatore con cui accede ai le per assumere lidentit` del singolo utente remoto, ma se questo viene fatto cambiando luser-ID eettivo o a luser-ID reale il server si espone alla ricezione di eventuali segnali ostili da parte dellutente di cui ha temporaneamente assunto lidentit`. Cambiando solo luser-ID di lesystem si ottengono a i privilegi necessari per accedere ai le, mantenendo quelli originari per quanto riguarda tutti gli altri controlli di accesso, cos` che lutente non possa inviare segnali al server NFS. Le due funzioni usate per cambiare questi identicatori sono setfsuid e setfsgid, ovviamente sono speciche di Linux e non devono essere usate se si intendono scrivere programmi portabili; i loro prototipi sono:
#include <sys/fsuid.h> int setfsuid(uid_t fsuid) Imposta luser-ID di lesystem del processo corrente a fsuid. int setfsgid(gid_t fsgid) Imposta il group-ID di lesystem del processo corrente a fsgid. Le funzioni restituiscono 0 in caso di successo e -1 in caso di fallimento: lunico errore possibile ` e EPERM.

queste funzioni hanno successo solo se il processo chiamante ha i privilegi di amministratore o, per gli altri utenti, se il valore specicato coincide con uno dei di quelli del gruppo real, eective o saved.

3.3.3

Le funzioni per la gestione dei gruppi associati a un processo

Le ultime funzioni che esamineremo sono quelle che permettono di operare sui gruppi supplementari cui un utente pu` appartenere. Ogni processo pu` avere almeno NGROUPS_MAX gruppi o o

62

CAPITOLO 3. LA GESTIONE DEI PROCESSI

supplementari25 in aggiunta al gruppo primario; questi vengono ereditati dal processo padre e possono essere cambiati con queste funzioni. La funzione che permette di leggere i gruppi supplementari associati ad un processo ` e getgroups; questa funzione ` denita nello standard POSIX.1, ed il suo prototipo `: e e
#include <sys/types.h> #include <unistd.h> int getgroups(int size, gid_t list[]) Legge gli identicatori dei gruppi supplementari. La funzione restituisce il numero di gruppi letti in caso di successo e -1 in caso di fallimento, nel qual caso errno assumer` i valori: a EFAULT EINVAL list non ha un indirizzo valido. il valore di size ` diverso da zero ma minore del numero di gruppi supplementari del e processo.

La funzione legge gli identicatori dei gruppi supplementari del processo sul vettore list di dimensione size. Non ` specicato se la funzione inserisca o meno nella lista il group-ID e eettivo del processo. Se si specica un valore di size uguale a 0 list non viene modicato, ma si ottiene il numero di gruppi supplementari. Una seconda funzione, getgrouplist, pu` invece essere usata per ottenere tutti i gruppi a o cui appartiene un certo utente; il suo prototipo `: e
#include <sys/types.h> #include <grp.h> int getgrouplist(const char *user, gid_t group, gid_t *groups, int *ngroups) Legge i gruppi supplementari. La funzione legge no ad un massimo di ngroups valori, restituisce 0 in caso di successo e -1 in caso di fallimento.

La funzione legge i gruppi supplementari dellutente specicato da user, eseguendo una scansione del database dei gruppi (si veda sez. 8.2.3). Ritorna poi in groups la lista di quelli a cui lutente appartiene. Si noti che ngroups ` passato come puntatore perch, qualora il valore e e specicato sia troppo piccolo, la funzione ritorna -1, passando indietro il numero dei gruppi trovati. Per impostare i gruppi supplementari di un processo ci sono due funzioni, che possono essere usate solo se si hanno i privilegi di amministratore. La prima delle due ` setgroups, ed il suo e prototipo `: e
#include <sys/types.h> #include <grp.h> int setgroups(size_t size, gid_t *list) Imposta i gruppi supplementari del processo. La funzione restituisce 0 in caso di successo e -1 in caso di fallimento, nel qual caso errno assumer` a i valori: EFAULT EPERM EINVAL list non ha un indirizzo valido. il processo non ha i privilegi di amministratore. il valore di size ` maggiore del valore massimo consentito. e

La funzione imposta i gruppi supplementari del processo corrente ai valori specicati nel vettore passato con largomento list, di dimensioni date dallargomento size. Il numero massimo di gruppi supplementari ` un parametro di sistema, che pu` essere ricavato con le modalit` e o a spiegate in sez. 8.1.
il numero massimo di gruppi secondari pu` essere ottenuto con sysconf (vedi sez. 8.1.2), leggendo il parametro o _SC_NGROUPS_MAX.
25

3.3. IL CONTROLLO DI ACCESSO

63

Se invece si vogliono impostare i gruppi supplementari del processo a quelli di un utente specico, si pu` usare initgroups il cui prototipo `: o e
#include <sys/types.h> #include <grp.h> int initgroups(const char *user, gid_t group) Inizializza la lista dei gruppi supplementari. La funzione restituisce 0 in caso di successo e -1 in caso di fallimento, nel qual caso errno assumer` a gli stessi valori di setgroups pi` ENOMEM quando non c` memoria suciente per allocare lo spazio u e per informazioni dei gruppi.

La funzione esegue la scansione del database dei gruppi (usualmente /etc/group) cercando i gruppi di cui ` membro lutente user con cui costruisce una lista di gruppi supplementari, a cui e aggiunge anche group, inne imposta questa lista per il processo corrente usando setgroups. Si tenga presente che sia setgroups che initgroups non sono denite nello standard POSIX.1 e che pertanto non ` possibile utilizzarle quando si denisce _POSIX_SOURCE o si compila con il e ag -ansi, ` pertanto meglio evitarle se si vuole scrivere codice portabile. e

3.3.4

La gestione delle capabilities

Come accennato in sez. 3.3.1 larchitettura classica della gestione dei privilegi in un sistema unix-like ha il sostanziale problema di fornire allamministratore dei poteri troppo ampi, questo comporta che anche quando si siano predisposte delle misure di protezione per in essere in grado di difendersi dagli eetti di una eventuale compromissione del sistema,26 una volta che questa sia stata eettuata e si siano ottenuti i privilegi di amministratore, queste potranno essere comunque rimosse.27 Il problema consiste nel fatto che nellarchitettura tradizionale di un sistema unix-like i controlli di accesso sono basati su un solo livello di separazione: per i processi normali essi sono posti in atto, mentre per i processi con i privilegi di amministratore essi non vengono neppure eseguiti; per questo motivo non era previsto alcun modo per evitare che un processo con diritti di amministratore non potesse eseguire certe operazioni, o per cedere denitivamente alcuni privilegi da un certo momento in poi. Per ovviare a tutto ci`, a partire dai kernel della serie 2.2, ` stato introdotto un meccanio e smo, detto capabilities, che consentisse di suddividere i vari privilegi tradizionalmente associati allamministratore in un insieme di capacit` distinte. Lidea era che queste capacit` potessero a a essere abilitate e disabilitate in maniera indipendente per ciascun processo con privilegi di amministratore, permettendo cos` una granularit` molto pi` ne nella distribuzione degli stessi che a u evitasse la originaria situazione di tutto o nulla. Il meccanismo completo delle capabilities 28 prevederebbe anche la possibilit` di associare a 29 in modo da poter stabilire quali capacit` le stesse capabilities anche ai singoli le eseguibili, a possono essere utilizzate quando viene messo in esecuzione uno specico programma; attualmente per` questa funzionalit` non ` implementata.30 o a e
come montare un lesystem in sola lettura per impedirne modiche, o marcare un le come immutabile. nei casi elencati nella precedente nota si potr` sempre rimontare il sistema in lettura-scrittura, o togliere la a marcatura di immutabilit`. a 28 limplementazione di Linux si rif` ad una bozza per quello che dovrebbe divenire lo standard POSIX.1e, che a prevede questa funzionalit`. a 29 una descrizione sommaria di questa funzionalit` ` riportata nella pagina di manuale che descrive limplemenae tazione delle capabilities con Linux (accessibile con man capabilities), ma non essendo implementata non ne tratteremo qui. 30 per attualmente si intende no al kernel 2.6.23; bench linfrastruttura per crearla sia presente (vedi anche e sez. 5.4.1) nora non ` disponibile nessuna realizzazione delle speciche POSIX.1e, esistono per` dei patch di e o sicurezza del kernel, come LIDS (vedi http://www.lids.org/) che realizzano qualcosa di simile.
27 26

64
Capacit` a CAP_CHOWN CAP_DAC_OVERRIDE

CAPITOLO 3. LA GESTIONE DEI PROCESSI


Descrizione La capacit` di cambiare proprietario e gruppo proprietario di un le (vedi sez. 5.3.4). a La capacit` di evitare il controllo dei permessi di lettura, scrittura ed esecuzione dei a le, (vedi sez. 5.3) caratteristici del modello classico del controllo di accesso chiamato Discrectionary Access Control (da cui il nome DAC). La capacit` di evitare il controllo dei permessi di lettura, scrittura ed esecuzione per a le directory (vedi sez. 5.3). La capacit` di evitare il controllo che luser-ID eettivo del processo (o meglio il a lesystem user-ID, vedi sez. 3.3.2) coincida con quello del proprietario di un le per tutte le operazioni privilegiate non coperte dalle precedenti CAP_DAC_OVERRIDE e CAP_DAC_READ_SEARCH. Queste comprendono i cambiamenti dei permessi e dei tempi del le (vedi sez. 5.3.3 e sez. 5.2.4), le impostazioni degli attributi estesi (con il comando chattr) e delle ACL, poter ignorare lo sticky bit nella cancellazione dei le (vedi sez. 5.3.2), la possibilit` di impostare il ag di O_NOATIME con open e fcntl a (vedi sez. 6.2.1 e sez. 6.3.6). La capacit` di evitare la cancellazione automatica dei bit suid e sgid quando un le a per i quali sono impostati viene modicato da un processo senza questa capacit` e a la capacit` di impostare il bit sgid su un le anche quando questo ` relativo ad un a e gruppo cui non si appartiene (vedi sez. 5.3.3). La capacit` di mandare segnali a qualunque processo (vedi sez. 9.3.3). a La capacit` di manipolare i group ID dei processi, sia il principale che i supplementari, a (vedi sez. 3.3.3 che quelli trasmessi tramite i socket unix domain (vedi sez. 18.2). La capacit` di manipolare gli user ID del processo (con setuid, setreuid, setresuid, a setfsuid) e di trasmettere un valore arbitrario delluid nel passaggio delle credenziali coi socket unix domain (vedi sez. 18.2). La capacit` di impostare o rimuovere una capacit` (limitatamente a quelle che il a a processo chiamante ha nel suo insieme di capacit` permesse) da qualunque processo. a La capacit` di impostare gli attributi immutable e append only per i le su un a lesystem che supporta questi attributi estesi. La capacit` di porre in ascolto server su porte riservate (vedi sez. 16.2.1). a La capacit` di consentire luso di socket in broadcast e multicast. a La capacit` di eseguire alcune operazioni privilegiate sulla rete (impostare le opzioni a privilegiate dei socket, abilitare il multicasting, impostare interfacce di rete e tabella di instradamento). La capacit` di usare socket RAW e PACKET (quelli che permettono di creare pacchetti a nei protocolli di basso livello). La capacit` di eettuare il memory locking con le funzioni mlock, mlockall, shmctl, a mmap (vedi sez. 2.2.4 e sez. 11.3.1). La capacit` di evitare il controllo dei permessi per le operazioni sugli oggetti di a intercomunicazione fra processi (vedi sez. 12.2). La capacit` di caricare e rimuovere moduli del kernel. a La capacit` di eseguire operazioni sulle porte di I/O con ioperm e iopl (vedi sez. ??). a La capacit` di eseguire la funzione chroot (vedi sez. 5.4.3). a Consente di tracciare qualunque processo con ptrace (vedi sez. ??). La capacit` di usare le funzioni di accounting dei processi (vedi sez. 8.3.4). a La capacit` di eseguire una serie di compiti amministrativi (come impostare le quote, a attivare e disattivare la swap, montare, rimontare e smontare lesystem, ecc.). La capacit` di fare eseguire un riavvio del sistema. a La capacit` di modicare le priorit` dei processi (vedi sez. 3.4). a a La capacit` di superare le limitazioni sulle risorse, aumentare le quote disco, usare lo a spazio disco riservato allamministratore. La capacit` di modicare il tempo di sistema (vedi sez. 8.4). a La capacit` di simulare un hangup della console, con la funzione vhangup. a La capacit` di creare le di dispositivo con la funzione mknod (vedi sez. 5.1.5).31 a La capacit` di creare dei le lease su di un le (vedi sez. 11.2.2) indipendentemente a dalla propriet` dello stesso.32 a La capacit` di impostare le capabilities di un le (non supportata). a

CAP_DAC_READ_SEARCH CAP_FOWNER

CAP_FSETID

CAP_KILL CAP_SETGID CAP_SETUID

CAP_SETPCAP CAP_LINUX_IMMUTABLE CAP_NET_BIND_SERVICE CAP_NET_BROADCAST CAP_NET_ADMIN

CAP_NET_RAW CAP_IPC_LOCK CAP_IPC_OWNER CAP_SYS_MODULE CAP_SYS_RAWIO CAP_SYS_CHROOT CAP_SYS_PTRACE CAP_SYS_PACCT CAP_SYS_ADMIN CAP_SYS_BOOT CAP_SYS_NICE CAP_SYS_RESOURCE CAP_SYS_TIME CAP_SYS_TTY_CONFIG CAP_MKNOD CAP_LEASE CAP_SETFCAP

Tabella 3.8: Le costanti che identicano le capabilities presenti nel kernel.

3.3. IL CONTROLLO DI ACCESSO

65

Per gestire questo nuovo meccanismo ciascun processo porta con s tre distinti insiemi di e capabilities, che vengono denominati rispettivamente eective, permitted ed inherited. Questi insiemi vengono mantenuti in forma di tre diverse maschere binarie,33 in cui ciascun bit corrisponde ad una capacit` diversa; se ne ` riportato lelenco,34 con una breve descrizione, ed il a e nome delle costanti che identicano i singoli bit, in tab. 3.8; la tabella ` divisa in due parti, e la prima riporta le capabilities previste nella bozza dello standard POSIX1.e, la seconda quelle speciche di Linux. Lutilizzo di tre distinti insiemi serve a fornire una interfaccia essibile per luso delle capabilities, con scopi analoghi a quelli per cui sono mantenuti i diversi insiemi di identicatori di sez. 3.3.2; il loro signicato ` il seguente: e eective linsieme delle capabilities eettive, cio` di quelle che vengono eettivamente usae te dal kernel quando deve eseguire il controllo di accesso per le varie operazioni compiute dal processo.

permitted linsieme delle capabilities permesse, cio` linsieme di quelle capacit` che un proe a cesso pu` impostare come eettive. Se un processo cancella una capacit` da questo o a insieme non potr` pi` riassumerla (almeno che non esegua un programma che ` a u e suid di root). inherited linsieme delle capabilities ereditabili, cio` quelle che vengono trasmesse ad un e nuovo programma eseguito attraverso una chiamata ad exec (con leccezione del caso che questo sia suid di root).

Oltre a questi tre insiemi, che sono relativi al singolo processo, il kernel mantiene un insieme generale valido per tutto il sistema, chiamato capabilities bounding set. Ogni volta che un programma viene posto in esecuzione con exec il contenuto degli insiemi eective e permitted vengono mascherati con un AND binario del contenuto corrente del capabilities bounding set, cos` che il nuovo processo potr` disporre soltanto delle capacit` in esso elencate. a a Il capabilities bounding set ` un parametro di sistema, accessibile attraverso il contenuto del e le /proc/sys/kernel/cap-bound, che per questa sua caratteristica consente di impostare un limite generale alle capacit` che possono essere accordate ai vari processi. Questo valore pu` a o essere impostato ad un valore arbitrario esclusivamente dal primo processo eseguito nel sistema (di norma cio` da /sbin/init), ogni processo eseguito successivamente (cio` con pid diverso e e da 1) anche se eseguito con privilegi di amministratore potr` soltanto rimuovere uno dei bit a gi` presenti dellinsieme: questo signica che una volta rimossa una capability dal capabilities a bounding set essa non sar` pi` disponibile, neanche per lamministratore, a meno di un riavvio. a u Quando un programma viene messo in esecuzione35 esso eredita (nel senso che assume negli insiemi eective e permitted ) le capabilities mantenute nellinsieme inherited, a meno che non sia eseguito un programma suid di root o la exec sia stata eseguita da un programma con uid reale zero; in tal caso il programma ottiene tutte le capabilities presenti nel capabilities bounding set. In questo modo si pu` far si che ad un processo eseguito in un secondo tempo possano o essere trasmesse solo un insieme limitato di capacit`, impedendogli di recuperare quelle assenti a
questa capacit` ` presente soltanto a partire dai kernel della serie 2.4.x. ae questa capacit` ` presente soltanto a partire dai kernel della serie 2.4.x. ae 33 il kernel li mantiene, come i vari identicatori di sez. 3.3.2, allinterno della task_struct di ciascun processo (vedi g. 3.2), nei tre campi cap_effective, cap_inheritable, cap_permitted del tipo kernel_cap_t; questo ` e attualmente denito come intero a 32 bit, il che comporta un massimo di 32 capabilities distinte. 34 si tenga presente che lelenco delle capabilities presentato questa tabella, ripreso dalla relativa pagina di manuale (accessibile con man capabilities) e dalle denizioni in sys/capabilities.h, ` quello aggiornato al e kernel 2.6.6. 35 cio` quando viene eseguita la execve con cui lo si lancia; in corrispondenza di una fork le capabilities non e vengono modicate.
32 21

66

CAPITOLO 3. LA GESTIONE DEI PROCESSI

nellinsieme inherited. Si tenga presente invece che attraverso una fork vengono mantenute le stesse capacit` del processo padre. a Per la gestione delle capabilities il kernel mette a disposizione due funzioni che permettono rispettivamente di leggere ed impostare i valori dei tre insiemi illustrati in precedenza. Queste due funzioni sono capget e capset e costituiscono linterfaccia di gestione basso livello; i loro rispettivi prototipi sono:
#include <sys/capability.h> int capget(cap_user_header_t hdrp, cap_user_data_t datap) Legge le capabilities. int capset(cap_user_header_t hdrp, const cap_user_data_t datap) Imposta le capabilities. Entrambe le funzioni ritornano 0 in caso di successo e -1 in caso di errore, nel qual caso errno pu` assumere i valori: o ESRCH EPERM si ` fatto riferimento ad un processo inesistente. e si ` tentato di aggiungere una capacit` nellinsieme delle capabilities permesse, o di e a impostare una capacit` non presente nellinsieme di quelle permesse negli insieme delle a eettive o ereditate, o si ` cercato di impostare una capability di un altro processo e senza avare CAP_SETPCAP.

ed inoltre EFAULT ed EINVAL.

Queste due funzioni prendono come argomenti due tipi di dati dedicati, deniti come puntatori a due strutture speciche di Linux, illustrate in g. 3.5. Per poterle utilizzare occorre anche cancellare la macro _POSIX_SOURCE.36 Si tenga presente che le strutture di g. 3.5, come i prototipi delle due funzioni capget e capset, sono soggette ad essere modicate con il cambiamento del kernel (in particolare i tipi di dati delle strutture) ed anche se nora linterfaccia ` e risultata stabile, non c` nessuna assicurazione che questa venga mantenuta. Pertanto se si voe gliono scrivere programmi portabili che possano essere eseguiti su qualunque versione del kernel ` opportuno utilizzare le interfacce di alto livello. e
# define _L INU X_C APA BIL ITY _VE RSI ON 0 x19980330

typedef struct __us er_ca p_head er_st ruct { int version ; int pid ; } * cap_user_header_t ; typedef struct __user_cap_data_struct { int effective ; int permitted ; int inheritable ; } * cap_user_data_t ;

Figura 3.5: Denizione delle strutture a cui fanno riferimento i puntatori cap_user_header_t e cap_user_data_t usati per linterfaccia di gestione di basso livello delle capabilities.

La struttura a cui deve puntare largomento hdrp serve ad indicare, tramite il campo pid, il processo del quale si vogliono leggere o modicare le capabilities. Il campo version deve essere impostato al valore della versione delle usata dal kernel (quello indicato dalla costante _LINUX_CAPABILITY_VERSION di g. 3.5) altrimenti le funzioni ritorneranno con un errore di EINVAL, restituendo nel campo stesso il valore corretto della versione in uso. La struttura a cui
per farlo occorre utilizzare la direttiva di preprocessore undef; si dovr` cio` inserire una istruzione #undef a e _POSIX_SOURCE prima di includere sys/capability.h.
36

3.3. IL CONTROLLO DI ACCESSO

67

deve puntare largomento datap invece conterr` i valori letti o da impostare per i tre insiemi a delle capacit` del processo. a Dato che le precedenti funzioni, oltre ad essere speciche di Linux, non garantiscono la stabilit` nellinterfaccia, ` sempre opportuno eettuare la gestione delle capabilities utilizzando a e le funzioni di libreria a questo dedicate. Queste funzioni, che seguono quanto previsto nelle bozze dello standard POSIX.1e, non fanno parte delle glibc e sono fornite in una libreria a parte,37 pertanto se un programma le utilizza si dovr` indicare esplicitamente luso della suddetta libreria a attraverso lopzione -lcap del compilatore. Le funzioni dellinterfaccia delle bozze di POSIX.1e prevedono luso di uno tipo di dato opaco, cap_t, come puntatore ai dati mantenuti nel cosiddetto capability state,38 in sono memorizzati tutti i dati delle capabilities. In questo modo ` possibile mascherare i dettagli della gestione di e basso livello, che potranno essere modicati senza dover cambiare le funzioni dellinterfaccia, che faranno riferimento soltanto ad oggetti di questo tipo. Linterfaccia pertanto non soltanto fornisce le funzioni per modicare e leggere le capabilities, ma anche quelle per gestire i dati attraverso cap_t. La prima funzione dellinterfaccia ` quella che permette di inizializzare un capability state, e allocando al contempo la memoria necessaria per i relativi dati. La funzione ` cap_init ed il e suo prototipo `: e
#include <sys/capability.h> cap_t cap_init(void) Crea ed inizializza un capability state. La funzione ritorna un valore non nullo in caso di successo e NULL in caso di errore, nel qual caso errno assumer` il valore ENOMEM. a

La funzione restituisce il puntatore cap_t ad uno stato inizializzato con tutte le capabilities azzerate. In caso di errore (cio` quando non c` memoria suciente ad allocare i dati) viene e e restituito NULL ed errno viene impostata a ENOMEM. La memoria necessaria a mantenere i dati viene automaticamente allocata da cap_init, ma dovr` essere disallocata esplicitamente quando a non pi` necessaria utilizzando la funzione cap_free, il cui prototipo `: u e
#include <sys/capability.h> int cap_free(void *obj_d) Disalloca la memoria allocata per i dati delle capabilities. La funzione ritorna 0 in caso di successo e 1 in caso di errore, nel qual caso errno assumer` il a valore EINVAL.

La funzione permette di liberare la memoria allocata dalle altre funzioni della libreria sia per un capability state, nel qual caso largomento dovr` essere un dato di tipo cap_t, che per a 39 nel qual caso largomento dovr` essere di tipo char una descrizione testuale dello stesso, a *. Largomento obj_d deve corrispondere ad un oggetto ottenuto tramite altre funzioni della libreria, altrimenti la funzione fallir` con un errore di EINVAL. a Inne si pu` creare una copia di un capability state ottenuto in precedenza tramite la funzione o cap_dup, il cui prototipo `: e
#include <sys/capability.h> cap_t cap_dup(cap_t cap_p) Duplica un capability state restituendone una copia. La funzione ritorna un valore non nullo in caso di successo e NULL in caso di errore, nel qual caso errno potr` assumere i valori ENOMEM o EINVAL. a la libreria ` libcap2, nel caso di Debian pu` essere installata con il pacchetto omonimo. e o si tratta in sostanza di un puntatore ad una struttura interna utilizzata dalle librerie, i cui campi non devono mai essere acceduti direttamente. 39 cio` quanto ottenuto tramite la funzione cap_to_text. e
38 37

68

CAPITOLO 3. LA GESTIONE DEI PROCESSI

La funzione crea una copia del capability state posto allindirizzo cap_p che si ` passato e come argomento, restituendo il puntatore alla copia, che conterr` gli stessi valori delle capabia lities presenti nelloriginale. La memoria necessaria viene allocata automaticamente dalla funzione. Una volta eettuata la copia i due capability state potranno essere modicati in maniera completamente indipendente. Una seconda classe di funzioni di servizio sono quelle per la gestione dei dati contenuti allinterno di un capability state; la prima di esse ` cap_clear, il cui prototipo `: e e
#include <sys/capability.h> int cap_clear(cap_t cap_p) Inizializza un capability state cancellando tutte le capabilities. La funzione ritorna 0 in caso di successo e 1 in caso di errore, nel qual caso errno assumer` il a valore EINVAL.

La funzione si limita ad azzerare tutte le capabilities presenti nel capability state allindirizzo cap_p passato come argomento, restituendo uno stato vuoto, analogo a quello che si ottiene nella creazione con cap_init. Per la gestione dei valori delle capabilities presenti in un capability state linterfaccia prevede due funzioni, cap_get_flag e cap_set_flag, che permettono rispettivamente di leggere o impostare il valore di un ag delle capabilities; i rispettivi prototipi sono:
#include <sys/capability.h> int cap_get_flag(cap_t cap_p, cap_value_t cap, cap_flag_t flag, cap_flag_value_t *value_p) Legge il valore di una capability. int cap_set_flag(cap_t cap_p, cap_flag_t flag, int ncap, cap_value_t *caps, cap_flag_value_t value) Imposta il valore di una capability. Le funzioni ritornano 0 in caso di successo e 1 in caso di errore, nel qual caso errno assumer` il a valore EINVAL.

In entrambe le funzioni largomento cap_p indica il puntatore al capability state su cui operare, mentre largomento flag indica su quale dei tre insiemi illustrati a pag. 65 si intende operare. Questi devono essere specicati con una variabile di tipo cap_flag_t che pu` assumere o 40 uno dei valori illustrati in tab. 3.9. esclusivamente
Valore CAP_EFFECTIVE CAP_PERMITTED CAP_INHERITABLE Signicato Capacit` dellinsieme eettivo. a Capacit` dellinsieme permesso. a Capacit` dellinsieme ereditabile. a

Tabella 3.9: Valori possibili per il tipo di dato cap_flag_t che identica gli insiemi delle capabilities.

La capacit` che si intende controllare o impostare invece deve essere specicata attraverso una a variabile di tipo cap_value_t, che pu` prendere come valore uno qualunque di quelli riportati in o tab. 3.8, in questo caso per` non ` possibile combinare diversi valori in una maschera binaria, una o e variabile di tipo cap_value_t deve indicare una sola capacit`.41 Inne lo stato di una capacit` a a ` descritto ad una variabile di tipo cap_flag_value_t, che a sua volta pu` assumere soltanto e o uno42 dei valori di tab. 3.10. La funzione cap_get_flag legge lo stato della capacit` indicata dallargomento cap allina terno dellinsieme indicato dallargomento flag e ne restituisce il valore nella variabile posta
40 si tratta in eetti di un tipo enumerato, come si pu` vericare dalla sua denizione che si trova in o /usr/include/sys/capability.h. 41 nel le di header citato nella nota precedente il tipo cap_value_t ` denito come int, ma i valori validi sono e soltanto quelli di tab. 3.8. 42 anche questo ` un tipo enumerato. e

3.3. IL CONTROLLO DI ACCESSO


Valore CAP_CLEAR CAP_SET Signicato La capacit` non ` impostata. a e La capacit` ` impostata. ae

69

Tabella 3.10: Valori possibili per il tipo di dato cap_flag_value_t che indica lo stato di una capacit`. a

allindirizzo puntato dallargomento value_p; ` possibile cio` leggere soltanto uno stato di una e e capacit` alla volta. a La funzione cap_set_flag pu` invece impostare in una sola chiamata pi` capacit`, ano u a che se solo allinterno dello stesso insieme; per questo essa prende un vettore di valori di tipo cap_value_t nellargomento caps, la cui dimensione ` specicata dallargomento ncap. Il tipo e di impostazione da eseguire (cancellazione o impostazione) viene indicato dallargomento value. Per la visualizzazione dello stato delle capabilities linterfaccia prevede una funzione apposita, cap_to_text, il cui prototipo `: e
#include <sys/capability.h> char * cap_to_text(cap_t caps, ssize_t * length_p) Genera una visualizzazione testuale delle capabilities. La funzione ritorna un puntatore alla stringa con la descrizione delle capabilities in caso di successo e NULL in caso di errore, nel qual caso errno pu` assumere i valori EINVAL o ENOMEM. o

La funzione ritorna lindirizzo di una stringa contente la descrizione testuale del contenuto del capabilities state caps passato come argomento, e, qualora largomento length_p sia diverso da NULL, restituisce nella variabile intera da questo puntata la lunghezza della stringa. La stringa restituita viene allocata automaticamente dalla funzione e deve essere liberata con cap_free. Fin quei abbiamo trattato delle funzioni di manipolazione dei capabilities state; quando si vuole eseguire la lettura delle capabilities del processo corrente si deve usare la funzione cap_get_proc, il cui prototipo `: e
#include <sys/capability.h> cap_t cap_get_proc(void) Legge le capabilities del processo corrente. La funzione ritorna un valore diverso da NULL in caso di successo e NULL in caso di errore, nel qual caso errno pu` assumere i valori EINVAL, EPERM o ENOMEM. o

La funzione legge il valore delle capabilities del processo corrente e restituisce il puntatore ad un capabilities state contenente il risultato, che provvede ad allocare autonomamente, e che occorrer` liberare con cap_free quando non sar` pi` utilizzato. a a u Se invece si vogliono leggere le capabilities di un processo specico occorre usare la funzione capgetp, il cui prototipo43 `: e
#include <sys/capability.h> int capgetp(pid_t pid, cap_t cap_d) Legge le capabilities del processo indicato da pid. La funzione ritorna 0 in caso di successo e 1 in caso di errore, nel qual caso errno pu` assumere o i valori EINVAL, EPERM o ENOMEM.

La funzione legge il valore delle capabilities del processo indicato con largomento pid, salvando il risultato nel capabilities state allindirizzo cap_d che deve essere stato creato in precedenza. Qualora il processo non esista si avr` un errore di ESRCH. Gli stessi valori possono essere letti a direttamente nel lesystem proc, nei le /proc/<pid>/status; ad esempio per init si otterr` a qualcosa del tipo:
su alcune pagine di manuale la funzione ` descritta con un prototipo sbagliato, che prevede un valore di e ritorno di tipo cap_t, ma il valore di ritorno ` intero, come si pu` vericare anche dalla dichiarazione della stessa e o in sys/capability.h.
43

70 ... CapInh: 0000000000000000 CapPrm: 00000000fffffeff CapEff: 00000000fffffeff

CAPITOLO 3. LA GESTIONE DEI PROCESSI

Inne per impostare le capabilities del processo corrente (non esiste una funzione che permetta di cambiare le capabilities di un altro processo) si deve usare la funzione cap_set_proc, il cui prototipo `: e
#include <sys/capability.h> int cap_set_proc(cap_t cap_p) Imposta le capabilities del processo corrente. La funzione ritorna 0 in caso di successo e 1 in caso di errore, nel qual caso errno pu` assumere o i valori EINVAL, EPERM o ENOMEM.

La funzione modica le capabilities del processo corrente secondo quanto specicato con largomento cap_p, posto che questo sia possibile nei termini spiegati in precedenza (non sar` a ad esempio possibile impostare capacit` non presenti nellinsieme di quelle permesse). In caso di a successo i nuovi valori saranno eettivi al ritorno della funzione, in caso di fallimento invece lo stato delle capacit` rester` invariato. Si tenga presente che tutte le capacit` specicate tramite a a a cap_p devono essere permesse; se anche una sola non lo ` la funzione fallir`, e per quanto e a appena detto, lo stato delle capabilities non verr` modicato (neanche per le parti eventualmente a permesse). Come esempio di utilizzo di queste funzioni nei sorgenti allegati alla guida si ` distribuito e il programma getcap.c, che consente di leggere le capabilities del processo corrente44 o tramite lopzione -p, quelle di un processo qualunque il cui pid viene passato come parametro dellopzione.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21

if (! pid ) { capab = cap_get_proc (); if ( capab == NULL ) { perror ( " cannot get current process capabilities " ); return 1; } } else { capab = cap_init (); res = capgetp ( pid , capab ); if ( res ) { perror ( " cannot get process capabilities " ); return 1; } } string = cap_to_text ( capab , NULL ); printf ( " Capability : % s \ n " , string ); cap_free ( capab ); cap_free ( string ); return 0;

Figura 3.6: Corpo principale del programma getcap.c. vale a dire di s stesso, quando lo si lancia, il che pu` sembrare inutile, ma serve a mostrarci quali sono le e o capabilities standard che ottiene un processo lanciato dalla riga di comando.
44

` 3.4. LA GESTIONE DELLA PRIORITA DI ESECUZIONE

71

La sezione principale del programma ` riportata in g. 3.6, e si basa su una condizione sulla e variabile pid che se si ` usato lopzione -p ` impostata (nella sezione di gestione delle opzioni, e e che si ` tralasciata) al valore del pid del processo di cui si vuole leggere le capabilities e nulla e altrimenti. Nel primo caso (1-6) si utilizza direttamente (2) cap_get_proc per ottenere lo stato delle capacit` del processo, nel secondo (7-14) prima si inizializza (8) uno stato vuoto e poi (9) a si legge il valore delle capacit` del processo indicato. a Il passo successivo ` utilizzare (16) cap_to_text per tradurre in una stringa lo stato, e e poi (17) stamparlo; inne (19-20) si libera la memoria allocata dalle precedenti funzioni con cap_free per poi ritornare dal ciclo principale della funzione.

3.4

La gestione della priorit` di esecuzione a

In questa sezione tratteremo pi` approfonditamente i meccanismi con il quale lo scheduler asu segna la CPU ai vari processi attivi. In particolare prenderemo in esame i vari meccanismi con cui viene gestita lassegnazione del tempo di CPU, ed illustreremo le varie funzioni di gestione.

3.4.1

I meccanismi di scheduling

La scelta di un meccanismo che sia in grado di distribuire in maniera ecace il tempo di CPU per lesecuzione dei processi ` sempre una questione delicata, ed oggetto di numerose ricerche; e in generale essa dipende in maniera essenziale anche dal tipo di utilizzo che deve essere fatto del sistema, per cui non esiste un meccanismo che sia valido per tutti gli usi. La caratteristica specica di un sistema multitasking come Linux ` quella del cosiddetto e prehemptive multitasking: questo signica che al contrario di altri sistemi (che usano invece il cosiddetto cooperative multitasking) non sono i singoli processi, ma il kernel stesso a decidere quando la CPU deve essere passata ad un altro processo. Come accennato in sez. 3.1.1 questa scelta viene eseguita da una sezione apposita del kernel, lo scheduler, il cui scopo ` quello di e distribuire al meglio il tempo di CPU fra i vari processi. La cosa ` resa ancora pi` complicata dal fatto che con le architetture multi-processore si e u deve anche scegliere quale sia la CPU pi` opportuna da utilizzare.45 Tutto questo comunque u appartiene alle sottigliezze dellimplementazione del kernel; dal punto di vista dei programmi che girano in user space, anche quando si hanno pi` processori (e dei processi che sono eseguiti u davvero in contemporanea), le politiche di scheduling riguardano semplicemente lallocazione della risorsa tempo di esecuzione, la cui assegnazione sar` governata dai meccanismi di scelta a delle priorit` che restano gli stessi indipendentemente dal numero di processori. a Si tenga conto poi che i processi non devono solo eseguire del codice: ad esempio molto spesso saranno impegnati in operazioni di I/O, o potranno venire bloccati da un comando dal terminale, o sospesi per un certo periodo di tempo. In tutti questi casi la CPU diventa disponibile ed ` e compito dello kernel provvedere a mettere in esecuzione un altro processo. Tutte queste possibilit` sono caratterizzate da un diverso stato del processo, in Linux un a processo pu` trovarsi in uno degli stati riportati in tab. 3.11; ma soltanto i processi che sono o nello stato runnable concorrono per lesecuzione. Questo vuol dire che, qualunque sia la sua priorit`, un processo non potr` mai essere messo in esecuzione ntanto che esso si trova in uno a a qualunque degli altri stati. Si deve quindi tenere presente che lutilizzo della CPU ` soltanto una delle risorse che sono e necessarie per lesecuzione di un programma, e a seconda dello scopo del programma non ` detto e neanche che sia la pi` importante (molti programmi dipendono in maniera molto pi` critica u u
nei processori moderni la presenza di ampie cache pu` rendere poco eciente trasferire lesecuzione di un o processo da una CPU ad unaltra, per cui eettuare la migliore scelta fra le diverse CPU non ` banale. e
45

72
Stato Runnable Sleep Uninterrutible Sleep Stopped Zombie STAT R S D T Z

CAPITOLO 3. LA GESTIONE DEI PROCESSI


Descrizione Il processo ` in esecuzione o ` pronto ad essere eseguito (cio` ` in attesa e e ee che gli venga assegnata la CPU). Il processo ` in attesa di un risposta dal sistema, ma pu` essere e o interrotto da un segnale. Il processo ` in attesa di un risposta dal sistema (in genere per I/O), e e non pu` essere interrotto in nessuna circostanza. o Il processo ` stato fermato con un SIGSTOP, o ` tracciato. e e Il processo ` terminato ma il suo stato di terminazione non ` ancora e e stato letto dal padre.

Tabella 3.11: Elenco dei possibili stati di un processo in Linux, nella colonna STAT si ` riportata la corrispondente e lettera usata dal comando ps nellomonimo campo.

dallI/O). Per questo motivo non ` aatto detto che dare ad un programma la massima priorit` e a di esecuzione abbia risultati signicativi in termini di prestazioni. Il meccanismo tradizionale di scheduling di Unix (che tratteremo in sez. 3.4.2) ` sempre e stato basato su delle priorit` dinamiche, in modo da assicurare che tutti i processi, anche i meno a importanti, possano ricevere un po di tempo di CPU. In sostanza quando un processo ottiene la CPU la sua priorit` viene diminuita. In questo modo alla ne, anche un processo con priorit` a a iniziale molto bassa, nisce per avere una priorit` suciente per essere eseguito. a Lo standard POSIX.1b per` ha introdotto il concetto di priorit` assoluta, (chiamata anche o a priorit` statica, in contrapposizione alla normale priorit` dinamica), per tenere conto dei sistemi a a real-time,46 in cui ` vitale che i processi che devono essere eseguiti in un determinato momento e non debbano aspettare la conclusione di altri che non hanno questa necessit`. a Il concetto di priorit` assoluta dice che quando due processi si contendono lesecuzione, vince a sempre quello con la priorit` assoluta pi` alta. Ovviamente questo avviene solo per i processi che a u sono pronti per essere eseguiti (cio` nello stato runnable). La priorit` assoluta viene in genere e a indicata con un numero intero, ed un valore pi` alto comporta una priorit` maggiore. Su questa u a politica di scheduling torneremo in sez. 3.4.3. In generale quello che succede in tutti gli Unix moderni ` che ai processi normali viene sempre e data una priorit` assoluta pari a zero, e la decisione di assegnazione della CPU ` fatta solo con a e il meccanismo tradizionale della priorit` dinamica. In Linux tuttavia ` possibile assegnare anche a e una priorit` assoluta, nel qual caso un processo avr` la precedenza su tutti gli altri di priorit` a a a inferiore, che saranno eseguiti solo quando questultimo non avr` bisogno della CPU. a

3.4.2

Il meccanismo di scheduling standard

A meno che non si abbiano esigenze speciche, lunico meccanismo di scheduling con il quale ` si avr` a che fare ` quello tradizionale, che prevede solo priorit` dinamiche. E di questo che, di a e a norma, ci si dovr` preoccupare nella programmazione. a Come accennato in Linux tutti i processi ordinari hanno la stessa priorit` assoluta. Quello che a determina quale, fra tutti i processi in attesa di esecuzione, sar` eseguito per primo, ` la priorit` a e a dinamica, che ` chiamata cos` proprio perch varia nel corso dellesecuzione di un processo. e e Oltre a questo la priorit` dinamica determina quanto a lungo un processo continuer` ad essere a a eseguito, e quando un processo potr` subentrare ad un altro nellesecuzione. a
per sistema real-time si intende un sistema in grado di eseguire operazioni in un tempo ben determinato; in genere si tende a distinguere fra lhard real-time in cui ` necessario che i tempi di esecuzione di un programma siano e determinabili con certezza assoluta (come nel caso di meccanismi di controllo di macchine, dove uno sforamento dei tempi avrebbe conseguenze disastrose), e soft-real-time in cui un occasionale sforamento ` ritenuto accettabile. e
46

` 3.4. LA GESTIONE DELLA PRIORITA DI ESECUZIONE

73

Il meccanismo usato da Linux ` piuttosto semplice,47 ad ogni processo ` assegnata una timee e slice, cio` un intervallo di tempo (letteralmente una fetta) per il quale esso deve essere eseguito. e Il valore della time-slice ` controllato dalla cosiddetta nice (o niceness) del processo. Essa ` e e contenuta nel campo nice di task_struct; tutti i processi vengono creati con lo stesso valore, ed essa specica il valore della durata iniziale della time-slice che viene assegnato ad un altro campo della struttura (counter) quando il processo viene eseguito per la prima volta e diminuito progressivamente ad ogni interruzione del timer. Durante la sua esecuzione lo scheduler scandisce la coda dei processi in stato runnable associando, in base al valore di counter, un peso ad ogni processo in attesa di esecuzione,48 chi ha il peso pi` alto verr` posto in esecuzione, ed il precedente processo sar` spostato in fondo u a a alla coda. Dato che ad ogni interruzione del timer il valore di counter del processo corrente viene diminuito, questo assicura che anche i processi con priorit` pi` bassa verranno messi in a u esecuzione. La priorit` di un processo ` cos` controllata attraverso il valore di nice, che stabilisce la a e durata della time-slice; per il meccanismo appena descritto infatti un valore pi` lungo assicura u una maggiore attribuzione di CPU. Lorigine del nome di questo parametro sta nel fatto che generalmente questo viene usato per diminuire la priorit` di un processo, come misura di cortesia a nei confronti degli altri. I processi infatti vengono creati dal sistema con lo stesso valore di nice (nullo) e nessuno ` privilegiato rispetto agli altri; il valore pu` essere modicato solo attraverso e o la funzione nice, il cui prototipo `: e
#include <unistd.h> int nice(int inc) Aumenta il valore di nice per il processo corrente. La funzione ritorna zero in caso di successo e -1 in caso di errore, nel qual caso errno pu` assumere o i valori: EPERM un processo senza i privilegi di amministratore ha specicato un valore di inc negativo.

Largomento inc indica lincremento del valore di nice: questultimo pu` assumere valori o compresi fra PRIO_MIN e PRIO_MAX (che nel caso di Linux sono 19 e 20), ma per inc si pu` speo cicare un valore qualunque, positivo o negativo, ed il sistema provveder` a troncare il risultato a nellintervallo consentito. Valori positivi comportano maggiore cortesia e cio` una diminuzione e della priorit`, ogni utente pu` solo innalzare il valore di un suo processo. Solo lamministratore a o pu` specicare valori negativi che permettono di aumentare la priorit` di un processo. o a In SUSv2 la funzione ritorna il nuovo valore di nice; Linux non segue questa convenzione, e per leggere il nuovo valore occorre invece usare la funzione getpriority, derivata da BSD, il cui prototipo `: e
#include <sys/resource.h> int getpriority(int which, int who) Restituisce il valore di nice per linsieme dei processi specicati. La funzione ritorna la priorit` in caso di successo e -1 in caso di errore, nel qual caso errno pu` a o assumere i valori: ESRCH EINVAL non c` nessun processo che corrisponda ai valori di which e who. e il valore di which non ` valido. e

nelle vecchie versioni pu` essere necessario includere anche <sys/time.h>, questo non ` pi` o e u necessario con versioni recenti delle librerie, ma ` comunque utile per portabilit`. e a
47 in realt` nella serie 2.6.x lo scheduler ` stato riscritto da zero e pu` usare diversi algoritmi, selezionabili sia a e o in fase di compilazione, che, nelle versioni pi` recenti, allavvio (addirittura ` stato ideato un sistema modulare u e che permette di cambiare lo scheduler al volo, che comunque non ` incluso nel kernel uciale). e 48 il calcolo del peso in realt` ` un po pi` complicato, ad esempio nei sistemi multiprocessore viene favorito un ae u processo eseguito sulla stessa CPU, e a parit` del valore di counter viene favorito chi ha una priorit` pi` elevata. a a u

74

CAPITOLO 3. LA GESTIONE DEI PROCESSI

La funzione permette, a seconda del valore di which, di leggere la priorit` di un processo, di a un gruppo di processi (vedi sez. 10.1.2) o di un utente, specicando un corrispondente valore per who secondo la legenda di tab. 3.12; un valore nullo di questultimo indica il processo, il gruppo di processi o lutente correnti.
which PRIO_PROCESS PRIO_PRGR PRIO_USER who pid_t pid_t uid_t Signicato processo process group utente

Tabella 3.12: Legenda del valore dellargomento which e del tipo dellargomento who delle funzioni getpriority e setpriority per le tre possibili scelte.

La funzione restituisce la priorit` pi` alta (cio` il valore pi` basso) fra quelle dei processi a u e u specicati; dato che -1 ` un valore possibile, per poter rilevare una condizione di errore ` necese e sario cancellare sempre errno prima della chiamata alla funzione, per vericare che essa resti uguale a zero. Analoga a getpriority la funzione setpriority permette di impostare la priorit` di uno a o pi` processi; il suo prototipo `: u e
#include <sys/resource.h> int setpriority(int which, int who, int prio) Imposta la priorit` per linsieme dei processi specicati. a La funzione ritorna la priorit` in caso di successo e -1 in caso di errore, nel qual caso errno pu` a o assumere i valori: ESRCH EINVAL EPERM EACCES non c` nessun processo che corrisponda ai valori di which e who. e il valore di which non ` valido. e un processo senza i privilegi di amministratore ha specicato un valore di inc negativo. un processo senza i privilegi di amministratore ha cercato di modicare la priorit` di a un processo di un altro utente.

La funzione imposta la priorit` al valore specicato da prio per tutti i processi indicati dagli a argomenti which e who. La gestione dei permessi dipende dalle varie implementazioni; in Linux, secondo le speciche dello standard SUSv3, e come avviene per tutti i sistemi che derivano da SysV, ` richiesto che luser-ID reale o eettivo del processo chiamante corrispondano al real e user-ID (e solo quello) del processo di cui si vuole cambiare la priorit`; per i sistemi derivati da a BSD invece (SunOS, Ultrix, *BSD) la corrispondenza pu` essere anche con luser-ID eettivo. o

3.4.3

Il meccanismo di scheduling real-time

Come spiegato in sez. 3.4.1 lo standard POSIX.1b ha introdotto le priorit` assolute per permeta tere la gestione di processi real-time. In realt` nel caso di Linux non si tratta di un vero hard a real-time, in quanto in presenza di eventuali interrupt il kernel interrompe lesecuzione di un processo qualsiasi sia la sua priorit`,49 mentre con lincorrere in un page fault si possono avere a ritardi non previsti. Se lultimo problema pu` essere aggirato attraverso luso delle funzioni di o controllo della memoria virtuale (vedi sez. 2.2.4), il primo non ` superabile e pu` comportare e o ritardi non prevedibili riguardo ai tempi di esecuzione di qualunque processo. Occorre usare le priorit` assolute con molta attenzione: se si d` ad un processo una priorit` a a a assoluta e questo nisce in un loop innito, nessun altro processo potr` essere eseguito, ed a
questo a meno che non si siano installate le patch di RTLinux, RTAI o Adeos, con i quali ` possibile ottenere e un sistema eettivamente hard real-time. In tal caso infatti gli interrupt vengono intercettati dallinterfaccia realtime (o nel caso di Adeos gestiti dalle code del nano-kernel), in modo da poterli controllare direttamente qualora ci sia la necessit` di avere un processo con priorit` pi` elevata di un interrupt handler. a a u
49

` 3.4. LA GESTIONE DELLA PRIORITA DI ESECUZIONE

75

esso sar` mantenuto in esecuzione permanentemente assorbendo tutta la CPU e senza nessuna a possibilit` di riottenere laccesso al sistema. Per questo motivo ` sempre opportuno, quando si a e lavora con processi che usano priorit` assolute, tenere attiva una shell cui si sia assegnata la a massima priorit` assoluta, in modo da poter essere comunque in grado di rientrare nel sistema. a Quando c` un processo con priorit` assoluta lo scheduler lo metter` in esecuzione prima di e a a ogni processo normale. In caso di pi` processi sar` eseguito per primo quello con priorit` assoluta u a a pi` alta. Quando ci sono pi` processi con la stessa priorit` assoluta questi vengono tenuti in una u u a coda e tocca al kernel decidere quale deve essere eseguito. Il meccanismo con cui vengono gestiti questi processi dipende dalla politica di scheduling che si ` scelta; lo standard ne prevede due: e FIFO First In First Out. Il processo viene eseguito ntanto che non cede volontariamente la CPU (con sched_yield), si blocca, nisce o viene interrotto da un processo a priorit` a pi` alta. Se il processo viene interrotto da uno a priorit` pi` alta esso rester` in cima alla u a u a lista e sar` il primo ad essere eseguito quando i processi a priorit` pi` alta diverranno a a u inattivi. Se invece lo si blocca volontariamente sar` posto in coda alla lista (ed altri a processi con la stessa priorit` potranno essere eseguiti). a RR Round Robin. Il comportamento ` del tutto analogo a quello precedente, con la sola e dierenza che ciascun processo viene eseguito al massimo per un certo periodo di tempo (la cosiddetta time slice) dopo di che viene automaticamente posto in fondo alla coda dei processi con la stessa priorit`. In questo modo si ha comunque una esecuzione a turno a di tutti i processi, da cui il nome della politica. Solo i processi con la stessa priorit` ed a in stato runnable entrano nel girotondo.

La funzione per impostare le politiche di scheduling (sia real-time che ordinarie) ed i relativi parametri ` sched_setscheduler; il suo prototipo `: e e
#include <sched.h> int sched_setscheduler(pid_t pid, int policy, const struct sched_param *p) Imposta priorit` e politica di scheduling. a La funzione ritorna la priorit` in caso di successo e -1 in caso di errore, nel qual caso errno pu` a o assumere i valori: ESRCH EINVAL EPERM il processo pid non esiste. il valore di policy non esiste o il relativo valore di p non ` valido. e il processo non ha i privilegi per attivare la politica richiesta.

La funzione esegue limpostazione per il processo specicato dallargomento pid; un valore nullo esegue limpostazione per il processo corrente. La politica di scheduling ` specicata dallare gomento policy i cui possibili valori sono riportati in tab. 3.13; un valore negativo per policy mantiene la politica di scheduling corrente. Solo un processo con i privilegi di amministratore pu` impostare priorit` assolute diverse da zero o politiche SCHED_FIFO e SCHED_RR. o a
Policy SCHED_FIFO SCHED_RR SCHED_OTHER Signicato Scheduling real-time con politica FIFO. Scheduling real-time con politica Round Robin. Scheduling ordinario.

Tabella 3.13: Valori dellargomento policy per la funzione sched_setscheduler.

Il valore della priorit` ` passato attraverso la struttura sched_param (riportata in g. 3.7), ae il cui solo campo attualmente denito ` sched_priority, che nel caso delle priorit` assolute e a deve essere specicato nellintervallo fra un valore massimo ed uno minimo, che nel caso sono rispettivamente 1 e 99; il valore nullo ` legale, ma indica i processi normali. e

76

CAPITOLO 3. LA GESTIONE DEI PROCESSI

struct sched_param { int sched_priority ; };

Figura 3.7: La struttura sched_param.

Si tenga presente che quando si imposta una politica di scheduling real-time per un processo (o se ne cambia la priorit` con sched_setparam) questo viene messo in cima alla lista dei processi a con la stessa priorit`; questo comporta che verr` eseguito subito, interrompendo eventuali altri a a processi con la stessa priorit` in quel momento in esecuzione. a Lo standard POSIX.1b prevede comunque che i due valori della massima e minima priorit` a statica possano essere ottenuti, per ciascuna delle politiche di scheduling real-time, tramite le due funzioni sched_get_priority_max e sched_get_priority_min, i cui prototipi sono:
#include <sched.h> int sched_get_priority_max(int policy) Legge il valore massimo della priorit` statica per la politica di scheduling policy. a int sched_get_priority_min(int policy) Legge il valore minimo della priorit` statica per la politica di scheduling policy. a La funzioni ritornano il valore della priorit` in caso di successo e -1 in caso di errore, nel qual caso a errno pu` assumere i valori: o EINVAL il valore di policy non ` valido. e

I processi con politica di scheduling SCHED_OTHER devono specicare un valore nullo (altrimenti si avr` un errore EINVAL), questo valore infatti non ha niente a che vedere con la priorit` a a dinamica determinata dal valore di nice, che deve essere impostato con le funzioni viste in precedenza. Il kernel mantiene i processi con la stessa priorit` assoluta in una lista, ed esegue sempre il a primo della lista, mentre un nuovo processo che torna in stato runnable viene sempre inserito in coda alla lista. Se la politica scelta ` SCHED_FIFO quando il processo viene eseguito viene e automaticamente rimesso in coda alla lista, e la sua esecuzione continua ntanto che non viene bloccato da una richiesta di I/O, o non rilascia volontariamente la CPU (in tal caso, tornando nello stato runnable sar` reinserito in coda alla lista); lesecuzione viene ripresa subito solo nel a caso che esso sia stato interrotto da un processo a priorit` pi` alta. a u Se si intende operare solo sulla priorit` assoluta di un processo si possono usare le funzioni a sched_setparam e sched_getparam, i cui prototipi sono:
#include <sched.h> int sched_setparam(pid_t pid, const struct sched_param *p) Imposta la priorit` assoluta del processo pid. a int sched_getparam(pid_t pid, struct sched_param *p) Legge la priorit` assoluta del processo pid. a La funzione ritorna la priorit` in caso di successo e -1 in caso di errore, nel qual caso errno pu` a o assumere i valori: ESRCH EINVAL EPERM il processo pid non esiste. il valore di p non ha senso per la politica scelta. il processo non ha i privilegi sucienti per eseguire loperazione.

Luso di sched_setparam che ` del tutto equivalente a sched_setscheduler con priority e uguale a -1. Come per sched_setscheduler specicando 0 come valore di pid si opera sul processo corrente. La disponibilit` di entrambe le funzioni pu` essere vericata controllando la a o macro _POSIX_PRIORITY_SCHEDULING che ` denita nellheader sched.h. e

` 3.4. LA GESTIONE DELLA PRIORITA DI ESECUZIONE

77

Si tenga presente che per eseguire la funzione il processo chiamante deve avere un user-ID eettivo uguale alluser-ID reale o a quello eettivo del processo di cui vuole cambiare la priorit`, a oppure deve avere i privilegi di amministratore (con la capacit` CAP_SYS_NICE). a La priorit` assoluta pu` essere riletta indietro dalla funzione sched_getscheduler, il cui a o prototipo `: e
#include <sched.h> int sched_getscheduler(pid_t pid) Legge la politica di scheduling per il processo pid. La funzione ritorna la politica di scheduling in caso di successo e -1 in caso di errore, nel qual caso errno pu` assumere i valori: o ESRCH EINVAL il processo pid non esiste. il valore di pid ` negativo. e

La funzione restituisce il valore (secondo quanto elencato in tab. 3.13) della politica di scheduling per il processo specicato; se pid ` nullo viene restituito quello del processo chiamante. e Lultima funzione che permette di leggere le informazioni relative ai processi real-time ` e sched_rr_get_interval, che permette di ottenere la lunghezza della time slice usata dalla politica round robin; il suo prototipo `: e
#include <sched.h> int sched_rr_get_interval(pid_t pid, struct timespec *tp) Legge in tp la durata della time slice per il processo pid. La funzione ritorna 0 in caso di successo e -1 in caso di errore, nel qual caso errno pu` assumere o i valori: ESRCH ENOSYS il processo pid non esiste. la system call non ` stata implementata. e

La funzione restituisce il valore dellintervallo di tempo usato per la politica round robin in una struttura timespec, (la cui denizione si pu` trovare in g. 8.9). In realt` dato che in Linux o a questo intervallo di tempo ` pressato e non modicabile, questa funzione ritorna sempre un e valore di 150 millisecondi, e non importa specicare il PID di un processo reale. Come accennato ogni processo che usa lo scheduling real-time pu` rilasciare volontariamente o la CPU; questo viene fatto attraverso la funzione sched_yield, il cui prototipo `: e
#include <sched.h> int sched_yield(void) Rilascia volontariamente lesecuzione. La funzione ritorna 0 in caso di successo e -1 in caso di errore, nel qual caso errno viene impostata opportunamente.

La funzione fa s` che il processo rilasci la CPU, in modo da essere rimesso in coda alla lista dei processi da eseguire, e permettere lesecuzione di un altro processo; se per` il processo ` lunico o e ad essere presente sulla coda lesecuzione non sar` interrotta. In genere usano questa funzione i a processi in modalit` fo, per permettere lesecuzione degli altri processi con pari priorit` quando a a la sezione pi` urgente ` nita. u e

3.4.4

Il controllo dello scheduler per i sistemi multiprocessore

Inne con il supporto dei sistemi multiprocessore sono state introdotte delle funzioni che permettono di controllare in maniera pi` dettagliata la scelta di quale processore utilizzare per eseguire u un certo programma. Uno dei problemi che si pongono nei sistemi multiprocessore ` infatti quello e del cosiddetto eetto ping-pong. Pu` accadere cio` che lo scheduler, quando riavvia un processo o e precedentemente interrotto scegliendo il primo processore disponibile, lo faccia eseguire da un

78

CAPITOLO 3. LA GESTIONE DEI PROCESSI

processore diverso rispetto a quello su cui era stato eseguito in precedenza. Se il processo passa da un processore allaltro in questo modo (cosa che avveniva abbastanza di frequente con i kernel della seria 2.4.x) si ha leetto ping-pong. Questo tipo di comportamento pu` generare dei seri problemi di prestazioni; infatti tutti i o processori moderni utilizzano una memoria interna (la cache) contenente i dati pi` usati, che u permette di evitare di eseguire un accesso (molto pi` lento) alla memoria principale sulla scheda u madre. Chiaramente un processo sar` favorito se i suoi dati sono nella cache del processore, ma ` a e ovvio che questo pu` essere vero solo per un processore alla volta, perch in presenza di pi` copie o e u degli stessi dati su pi` processori, non si potrebbe determinare quale di questi ha la versione dei u dati aggiornata rispetto alla memoria principale. Questo comporta che quando un processore inserisce un dato nella sua cache, tutti gli altri processori che hanno lo stesso dato devono invalidarlo, e questa operazione ` molto costosa in e termini di prestazioni. Il problema diventa serio quando si verica leetto ping-pong, in tal caso infatti un processo rimbalza continuamente da un processore allaltro e si ha una continua invalidazione della cache, che non diventa mai disponibile. Per ovviare a questo tipo di problemi ` nato il concetto di anit` di processore (o CPU e a anity); la possibilit` cio` di far s` che un processo possa essere assegnato per lesecuzione a e sempre allo stesso processore. Lo scheduler dei kernel della serie 2.4.x aveva una scarsa CPU anity, e leetto ping-pong era comune; con il nuovo scheduler dei kernel della 2.6.x questo problema ` stato risolto ed esso cerca di mantenere il pi` possibile ciascun processo sullo stesso e u processore. In certi casi per` resta lesigenza di poter essere sicuri che un processo sia sempre eseguito o dallo stesso processore,50 e per poter risolvere questo tipo di problematiche nei nuovi kernel51 ` e stata introdotta lopportuna infrastruttura ed una nuova system call che permette di impostare su quali processori far eseguire un determinato processo attraverso una maschera di anit`. La a 52 `: corrispondente funzione di libreria ` sched_setaffinity ed il suo prototipo e e
#include <sched.h> int sched_setaffinity (pid_t pid, const cpu_set_t *cpuset) Imposta la maschera di anit` del processo pid. a La funzione ritorna 0 in caso di successo e -1 in caso di errore, nel qual caso errno pu` assumere o i valori: ESRCH EINVAL EPERM il processo pid non esiste. il valore di cpuset contiene riferimenti a processori non esistenti nel sistema. il processo non ha i privilegi sucienti per eseguire loperazione.

ed inoltre anche EFAULT.

La funzione imposta, con luso del valore contenuto allindirizzo cpuset, linsieme dei processori sui quali deve essere eseguito il processo identicato tramite il valore passato in pid. Come in precedenza il valore nullo di pid indica il processo corrente. Per poter utilizzare questa funzione sono richiesti i privilegi di amministratore (` necessaria la capacit` CAP_SYS_NICE) ale a trimenti essa fallir` con un errore di EPERM. Una volta impostata una maschera di anit`, questa a a
quella che viene detta hard CPU anity, in contrasto con quella fornita dallo scheduler, detta soft CPU anity, che di norma indica solo una preferenza, non un requisito assoluto. 51 le due system call per la gestione della CPU anity sono state introdotte nel kernel 2.5.8, e le funzioni di libreria nelle glibc 2.3. 52 di questa funzione (e della corrispondente sched_setaffinity) esistono versioni diverse per gli argomenti successivi a pid: la prima (quella riportata nella pagina di manuale) prevedeva due ulteriori argomenti di tipo unsigned int len e unsigned long *mask, poi largomento len ` stato eliminato, successivamente si ` introdotta e e la versione riportata con per` un secondo argomento di tipo size_t cpusetsize (anche questa citata nella pagina o di manuale); la versione citata ` quella riportata nel manuale delle glibc e corrispondente alla denizione presente e in sched.h.
50

` 3.4. LA GESTIONE DELLA PRIORITA DI ESECUZIONE

79

viene ereditata attraverso una fork, in questo modo diventa possibile legare automaticamente un gruppo di processi ad un singolo processore. Nelluso comune, almeno con i kernel della serie 2.6.x, luso di questa funzione non ` necese sario, in quanto ` lo scheduler stesso che provvede a mantenere al meglio lanit` di processore. e a Esistono per` esigenze particolari, ad esempio quando un processo (o un gruppo di processi) ` o e utilizzato per un compito importante (ad esempio per applicazioni real-time o la cui risposta ` e critica) e si vuole la massima velocit`, con questa interfaccia diventa possibile selezionare gruppi a di processori utilizzabili in maniera esclusiva. Lo stesso dicasi quando laccesso a certe risorse (memoria o periferiche) pu` avere un costo diverso a seconda del processore (come avviene nelle o architetture NUMA). Inne se un gruppo di processi accede alle stesse risorse condivise (ad esempio una applicazione con pi` thread) pu` avere senso usare lo stesso processore in modo da sfruttare meglio luso u o della sua cache; questo ovviamente riduce i beneci di un sistema multiprocessore nellesecuzione contemporanea dei thread, ma in certi casi (quando i thread sono inerentemente serializzati nellaccesso ad una risorsa) possono esserci sucienti vantaggi nellevitare la perdita della cache da rendere conveniente luso dellanit` di processore. a Per facilitare luso dellargomento cpuset le glibc hanno introdotto un apposito dato di tipo, cpu_set_t,53 che permette di identicare un insieme di processori. Il dato ` una maschera e binaria: in generale ` un intero a 32 bit in cui ogni bit corrisponde ad un processore, ma dato e che per architetture particolari il numero di bit di un intero pu` non essere suciente, ` stata o e creata questa che ` una interfaccia generica che permette di usare a basso livello un tipo di dato e qualunque rendendosi indipendenti dal numero di bit e dalla loro disposizione. Questa interfaccia, oltre alla denizione del tipo di dato apposito, prevede anche una serie di macro di preprocessore per la manipolazione dello stesso, che consentono di svuotare un insieme, aggiungere o togliere un processore da esso o vericare se vi ` gi` presente: e a
#include <sched.h> void CPU_ZERO(cpu_set_t *set) Inizializza linsieme (vuoto). void CPU_SET(int cpu, cpu_set_t *set) Inserisce il processore cpu nellinsieme. void CPU_CLR(int cpu, cpu_set_t *set) Rimuove il processore cpu nellinsieme. int CPU_ISSET(int cpu, cpu_set_t *set) Controlla se il processore cpu ` nellinsieme. e

Oltre a queste macro, simili alle analoghe usate per gli insiemi di le descriptor (vedi sez. 11.1.2) ` denita la costante CPU_SETSIZE che indica il numero massimo di processori che e possono far parte dellinsieme, e che costituisce un limite massimo al valore dellargomento cpu. In generale la maschera di anit` ` preimpostata in modo che un processo possa essere a e eseguito su qualunque processore, se pu` comunque leggere il valore per un processo specico o usando la funzione sched_getaffinity, il suo prototipo `: e
#include <sched.h> int sched_getaffinity (pid_t pid, const cpu_set_t *cpuset) Legge la maschera di anit` del processo pid. a La funzione ritorna 0 in caso di successo e -1 in caso di errore, nel qual caso errno pu` assumere o i valori: ESRCH EFAULT
53

il processo pid non esiste. il valore di cpuset non ` un indirizzo valido. e

questa ` una estensione specica delle glibc, da attivare denendo la macro _GNU_SOURCE, non esiste infatti e una standardizzazione per questo tipo di interfaccia e POSIX al momento non prevede nulla al riguardo.

80

CAPITOLO 3. LA GESTIONE DEI PROCESSI

La funzione restituir` allindirizzo specicato da cpuset il valore della maschera di anit` a a del processo, cos` da poterla riutilizzare per una successiva reimpostazione. In questo caso non sono necessari privilegi particolari. ` E chiaro che queste funzioni per la gestione dellanit` hanno signicato soltanto su un a sistema multiprocessore, esse possono comunque essere utilizzate anche in un sistema con un processore singolo, nel qual caso per` non avranno alcun risultato eettivo. o

3.5

Problematiche di programmazione multitasking

Bench i processi siano strutturati in modo da apparire il pi` possibile come indipendenti luno e u dallaltro, nella programmazione in un sistema multitasking occorre tenere conto di una serie di problematiche che normalmente non esistono quando si ha a che fare con un sistema in cui viene eseguito un solo programma alla volta. Pur essendo questo argomento di carattere generale, ci ` parso opportuno introdurre sintee ticamente queste problematiche, che ritroveremo a pi` riprese in capitoli successivi, in questa u sezione conclusiva del capitolo in cui abbiamo arontato la gestione dei processi.

3.5.1

Le operazioni atomiche

La nozione di operazione atomica deriva dal signicato greco della parola atomo, cio` indivisibile; e si dice infatti che unoperazione ` atomica quando si ha la certezza che, qualora essa venga e eettuata, tutti i passaggi che devono essere compiuti per realizzarla verranno eseguiti senza possibilit` di interruzione in una fase intermedia. a In un ambiente multitasking il concetto ` essenziale, dato che un processo pu` essere interrote o to in qualunque momento dal kernel che mette in esecuzione un altro processo o dalla ricezione di un segnale; occorre pertanto essere accorti nei confronti delle possibili race condition (vedi sez. 3.5.2) derivanti da operazioni interrotte in una fase in cui non erano ancora state completate. Nel caso dellinterazione fra processi la situazione ` molto pi` semplice, ed occorre preoce u cuparsi della atomicit` delle operazioni solo quando si ha a che fare con meccanismi di intera comunicazione (che esamineremo in dettaglio in cap. 12) o nelle operazioni con i le (vedremo alcuni esempi in sez. 6.3.2). In questi casi in genere luso delle appropriate funzioni di libreria per compiere le operazioni necessarie ` garanzia suciente di atomicit` in quanto le system call e a con cui esse sono realizzate non possono essere interrotte (o subire interferenze pericolose) da altri processi. Nel caso dei segnali invece la situazione ` molto pi` delicata, in quanto lo stesso processo, e e u pure alcune system call, possono essere interrotti in qualunque momento, e le operazioni di un eventuale signal handler sono compiute nello stesso spazio di indirizzi del processo. Per questo, anche il solo accesso o lassegnazione di una variabile possono non essere pi` operazioni atomiche u (torneremo su questi aspetti in sez. 9.4). In questo caso il sistema provvede un tipo di dato, il sig_atomic_t, il cui accesso ` assicurato e essere atomico. In pratica comunque si pu` assumere che, in ogni piattaforma su cui ` impleo e mentato Linux, il tipo int, gli altri interi di dimensione inferiore ed i puntatori sono atomici. Non ` aatto detto che lo stesso valga per interi di dimensioni maggiori (in cui laccesso pu` e o comportare pi` istruzioni in assembler) o per le strutture. In tutti questi casi ` anche opportuno u e marcare come volatile le variabili che possono essere interessate ad accesso condiviso, onde evitare problemi con le ottimizzazioni del codice.

3.5.2

Le race condition ed i deadlock

Si deniscono race condition tutte quelle situazioni in cui processi diversi operano su una risorsa comune, ed in cui il risultato viene a dipendere dallordine in cui essi eettuano le loro operazioni.

3.5. PROBLEMATICHE DI PROGRAMMAZIONE MULTITASKING

81

Il caso tipico ` quello di unoperazione che viene eseguita da un processo in pi` passi, e pu` essere e u o compromessa dallintervento di un altro processo che accede alla stessa risorsa quando ancora non tutti i passi sono stati completati. Dato che in un sistema multitasking ogni processo pu` essere interrotto in qualunque moo mento per farne subentrare un altro in esecuzione, niente pu` assicurare un preciso ordine di o esecuzione fra processi diversi o che una sezione di un programma possa essere eseguita senza interruzioni da parte di altri. Queste situazioni comportano pertanto errori estremamente subdoli e dicili da tracciare, in quanto nella maggior parte dei casi tutto funzioner` regolarmente, a e solo occasionalmente si avranno degli errori. Per questo occorre essere ben consapevoli di queste problematiche, e del fatto che lunico modo per evitarle ` quello di riconoscerle come tali e prendere gli adeguati provvedimenti per e far s` che non si verichino. Casi tipici di race condition si hanno quando diversi processi accedono allo stesso le, o nellaccesso a meccanismi di intercomunicazione come la memoria condivisa. In questi casi, se non si dispone della possibilit` di eseguire atomicamente le operazioni necessarie, a occorre che quelle parti di codice in cui si compiono le operazioni sulle risorse condivise (le cosiddette sezioni critiche) del programma, siano opportunamente protette da meccanismi di sincronizzazione (torneremo su queste problematiche di questo tipo in cap. 12). Un caso particolare di race condition sono poi i cosiddetti deadlock, particolarmente gravi in quanto comportano spesso il blocco completo di un servizio, e non il fallimento di una singola operazione. Per denizione un deadlock ` una situazione in cui due o pi` processi non sono pi` e u u in grado di proseguire perch ciascuno aspetta il risultato di una operazione che dovrebbe essere e eseguita dallaltro. Lesempio tipico di una situazione che pu` condurre ad un deadlock ` quello in cui un ag di o e occupazione viene rilasciato da un evento asincrono (come un segnale o un altro processo) fra il momento in cui lo si ` controllato (trovandolo occupato) e la successiva operazione di attesa e per lo sblocco. In questo caso, dato che levento di sblocco del ag ` avvenuto senza che ce ne e accorgessimo proprio fra il controllo e la messa in attesa, questultima diventer` perpetua (da a cui il nome di deadlock ). In tutti questi casi ` di fondamentale importanza il concetto di atomicit` visto in sez. 3.5.1; e a questi problemi infatti possono essere risolti soltanto assicurandosi, quando essa sia richiesta, che sia possibile eseguire in maniera atomica le operazioni necessarie.

3.5.3

Le funzioni rientranti

Si dice rientrante una funzione che pu` essere interrotta in qualunque punto della sua esecuo zione ed essere chiamata una seconda volta da un altro thread di esecuzione senza che questo comporti nessun problema nellesecuzione della stessa. La problematica ` comune nella programe mazione multi-thread, ma si hanno gli stessi problemi quando si vogliono chiamare delle funzioni allinterno dei gestori dei segnali. Fintanto che una funzione opera soltanto con le variabili locali ` rientrante; queste infatti e vengono allocate nello stack, ed unaltra invocazione non fa altro che allocarne unaltra copia. Una funzione pu` non essere rientrante quando opera su memoria che non ` nello stack. Ad o e esempio una funzione non ` mai rientrante se usa una variabile globale o statica. e Nel caso invece la funzione operi su un oggetto allocato dinamicamente, la cosa viene a dipendere da come avvengono le operazioni: se loggetto ` creato ogni volta e ritornato indietro e la funzione pu` essere rientrante, se invece esso viene individuato dalla funzione stessa due o chiamate alla stessa funzione potranno interferire quando entrambe faranno riferimento allo stesso oggetto. Allo stesso modo una funzione pu` non essere rientrante se usa e modica un o oggetto che le viene fornito dal chiamante: due chiamate possono interferire se viene passato lo stesso oggetto; in tutti questi casi occorre molta cura da parte del programmatore.

82

CAPITOLO 3. LA GESTIONE DEI PROCESSI

In genere le funzioni di libreria non sono rientranti, molte di esse ad esempio utilizzano variabili statiche, le glibc per` mettono a disposizione due macro di compilatore, _REENTRANT o e _THREAD_SAFE, la cui denizione attiva le versioni rientranti di varie funzioni di libreria, che sono identicate aggiungendo il susso _r al nome della versione normale.

Capitolo 4

Larchitettura dei le
Uno dei concetti fondamentali dellarchitettura di un sistema Unix ` il cosiddetto everything is e a le, cio` il fatto che laccesso ai vari dispositivi di input/output del computer viene eettuato e attraverso uninterfaccia astratta che tratta le periferiche allo stesso modo dei normali le di dati. Questo signica che si pu` accedere a qualunque periferica del computer, dalla seriale, alla o parallela, alla console, e agli stessi dischi attraverso i cosiddetti le di dispositivo (i cosiddetti device le). Questi sono dei le speciali agendo sui quali i programmi possono leggere, scrivere e compiere operazioni direttamente sulle periferiche, usando le stesse funzioni che si usano per i normali le di dati. In questo capitolo forniremo una descrizione dellarchitettura dei le in Linux, iniziando da una panoramica sulle caratteristiche principali delle interfacce con cui i processi accedono ai le (che tratteremo in dettaglio nei capitoli seguenti), per poi passare ad una descrizione pi` u dettagliata delle modalit` con cui detto accesso viene realizzato dal sistema. a

4.1

Larchitettura generale

Per poter accedere ai le, il kernel deve mettere a disposizione dei programmi le opportune interfacce che consentano di leggerne il contenuto; il sistema cio` deve provvedere ad organizzare e e rendere accessibile in maniera opportuna linformazione tenuta sullo spazio grezzo disponibile sui dischi. Questo viene fatto strutturando linformazione sul disco attraverso quello che si chiama un lesystem (vedi sez. 4.2), essa poi viene resa disponibile ai processi attraverso quello che viene chiamato il montaggio del lesystem. In questa sezione faremo una panoramica generica su come il sistema presenta i le ai processi, trattando lorganizzazione di le e directory, i tipi di le ed introducendo le interfacce disponibili e le loro caratteristiche.

4.1.1

Lorganizzazione di le e directory

In Unix, a dierenza di quanto avviene in altri sistemi operativi, tutti i le vengono tenuti allinterno di un unico albero la cui radice (quella che viene chiamata root directory) viene montata allavvio. Un le viene identicato dallutente usando quello che viene chiamato pathname 1 , cio` e il percorso che si deve fare per accedere al le a partire dalla root directory, che ` composto da e una serie di nomi separati da una /.
1 il manuale della glibc depreca questa nomenclatura, che genererebbe confusione poich path indica anche un e insieme di directory su cui eettuare una ricerca (come quello in cui si cercano i comandi). Al suo posto viene proposto luso di lename e di componente per il nome del le allinterno della directory. Non seguiremo questa scelta dato che luso della parola pathname ` ormai cos` comune che mantenerne luso ` senzaltro pi` chiaro e e u dellalternativa proposta.

83

84

CAPITOLO 4. LARCHITETTURA DEI FILE

Allavvio del sistema, completata la fase di inizializzazione, il kernel riceve dal bootloader lindicazione di quale dispositivo contiene il lesystem da usare come punto di partenza e questo viene montato come radice dellalbero (cio` nella directory /); tutti gli ulteriori lesystem e che possono essere su altri dispositivi dovranno poi essere inseriti nellalbero montandoli su opportune directory del lesystem montato come radice. Alcuni lesystem speciali (come /proc che contiene uninterfaccia ad alcune strutture interne del kernel) sono generati automaticamente dal kernel stesso, ma anche essi devono essere montati allinterno dellalbero dei le. Una directory, come vedremo in maggior dettaglio in sez. 4.2.2, ` anchessa un le, solo che e ` un le particolare che il kernel riconosce come tale. Il suo scopo ` quello di contenere una lista e e di nomi di le e le informazioni che associano ciascun nome al contenuto. Dato che questi nomi possono corrispondere ad un qualunque oggetto del lesystem, compresa unaltra directory, si ottiene naturalmente unorganizzazione ad albero inserendo nomi di directory in altre directory. Un le pu` essere indicato rispetto alla directory corrente semplicemente specicandone il o 2 da essa contenuto. Allinterno dello stesso albero si potranno poi inserire anche tutti gli nome altri oggetti visti attraverso linterfaccia che manipola i le come le fo, i link, i socket e gli stessi le di dispositivo (questi ultimi, per convenzione, sono inseriti nella directory /dev). Il nome completo di un le viene chiamato pathname ed il procedimento con cui si individua il le a cui esso fa riferimento ` chiamato risoluzione del nome (lename resolution o pathname e resolution). La risoluzione viene fatta esaminando il pathname da sinistra a destra e localizzando ogni nome nella directory indicata dal nome precedente usando / come separatore3 : ovviamente, perch il procedimento funzioni, occorre che i nomi indicati come directory esistano e siano e eettivamente directory, inoltre i permessi (si veda sez. 5.3) devono consentire laccesso allintero pathname. Se il pathname comincia per / la ricerca parte dalla directory radice del processo; questa, a meno di un chroot (su cui torneremo in sez. 5.4.3) ` la stessa per tutti i processi ed equivale alla e directory radice dellalbero dei le: in questo caso si parla di un pathname assoluto . Altrimenti la ricerca parte dalla directory corrente (su cui torneremo in sez. 5.1.7) ed il pathname ` detto e pathname relativo . I nomi . e .. hanno un signicato speciale e vengono inseriti in ogni directory: il primo fa riferimento alla directory corrente e il secondo alla directory genitrice (o parent directory) cio` la directory che contiene il riferimento alla directory corrente; nel caso la directory corrente e coincida con la directory radice, allora il riferimento ` a se stessa. e

4.1.2

I tipi di le

Come detto in precedenza, in Unix esistono vari tipi di le; in Linux questi sono implementati come oggetti del Virtual File System (vedi sez. 4.2.2) e sono presenti in tutti i lesystem unix-like utilizzabili con Linux. Lelenco dei vari tipi di le deniti dal Virtual File System ` riportato in e tab. 4.1. Si tenga ben presente che questa classicazione non ha nulla a che fare con la classicazione dei le (che in questo caso sono sempre le di dati) in base al loro contenuto, o tipo di accesso. Essa riguarda invece il tipo di oggetti; in particolare ` da notare la presenza dei cosiddetti le e speciali. Alcuni di essi, come le fo (che tratteremo in sez. 12.1.4) ed i socket (che tratteremo in cap. 15) non sono altro che dei riferimenti per utilizzare delle funzionalit` di comunicazione a fornite dal kernel. Gli altri sono i le di dispositivo (o device le) che costituiscono una interfaccia diretta per leggere e scrivere sui dispositivi sici; essi vengono suddivisi in due grandi categorie,
il manuale delle glibc chiama i nomi contenuti nelle directory componenti (in inglese le name components), noi li chiameremo pi` semplicemente nomi o voci. u 3 nel caso di nome vuoto, il costrutto // viene considerato equivalente a /.
2

4.1. LARCHITETTURA GENERALE

85

a blocchi e a caratteri a seconda delle modalit` in cui il dispositivo sottostante eettua le a operazioni di I/O.4
Tipo di le le regolare cartella o direttorio collegamento simbolico dispositivo a caratteri dispositivo a blocchi coda presa Descrizione Un le che contiene dei dati (laccezione normale di le). Un le che contiene una lista di nomi associati a degli inode (vedi sez. 4.2.1). Un le che contiene un riferimento ad un altro le/directory. Un le che identica una periferica ad accesso a caratteri. Un le che identica una periferica ad accesso a blocchi. Un le speciale che identica una linea di comunicazione software unidirezionale (vedi sez. 12.1.4). Un le speciale che identica una linea di comunicazione software bidirezionale (vedi cap. 15).

regular le directory symbolic link char device block device fo socket

Tabella 4.1: Tipologia dei le deniti nel VFS

Una delle dierenze principali con altri sistemi operativi (come il VMS o Windows) ` che e per Unix tutti i le di dati sono identici e contengono un usso continuo di byte. Non esiste cio` e dierenza per come vengono visti dal sistema le di diverso contenuto o formato (come nel caso di quella fra le di testo e binari che c` in Windows) n c` una strutturazione a record per il e e e 5 cosiddetto accesso diretto come nel caso del VMS. Una seconda dierenza ` nel formato dei le ASCII: in Unix la ne riga ` codicata in e e maniera diversa da Windows o Mac, in particolare il ne riga ` il carattere LF (o \n) al posto e del CR (\r) del Mac e del CR LF di Windows.6 Questo pu` causare alcuni problemi qualora nei o programmi si facciano assunzioni sul terminatore della riga. Si ricordi inne che un kernel Unix non fornisce nessun supporto per la tipizzazione dei le di dati e che non c` nessun supporto del sistema per le estensioni come parte del lesystem.7 e Ci` nonostante molti programmi adottano delle convenzioni per i nomi dei le, ad esempio il o codice C normalmente si mette in le con lestensione .c; unaltra tecnica molto usata ` quella e di utilizzare i primi 4 byte del le per memorizzare un magic number che classichi il contenuto; entrambe queste tecniche, per quanto usate ed accettate in maniera diusa, restano solo delle convenzioni il cui rispetto ` demandato alle applicazioni stesse. e

4.1.3

Le due interfacce ai le

In Linux le modalit` di accesso ai le e le relative interfacce di programmazione sono due, basate a su due diversi meccanismi con cui ` possibile accedere al loro contenuto. e
in sostanza i dispositivi a blocchi (ad esempio i dischi) corrispondono a periferiche per le quali ` richiesto che e lI/O venga eettuato per blocchi di dati di dimensioni ssate (ad esempio le dimensioni di un settore), mentre nei dispositivi a caratteri lI/O viene eettuato senza nessuna particolare struttura. 5 questo vale anche per i dispositivi a blocchi: la strutturazione dellI/O in blocchi di dimensione ssa avviene solo allinterno del kernel, ed ` completamente trasparente allutente. Inoltre talvolta si parla di accesso diretto e riferendosi alla capacit`, che non ha niente a che fare con tutto ci`, di eettuare, attraverso degli appositi le a o di dispositivo, operazioni di I/O direttamente sui dischi senza passare attraverso un lesystem (il cosiddetto raw access, introdotto coi kernel della serie 2.4.x). 6 per questo esistono in Linux dei programmi come unix2dos e dos2unix che eettuano una conversione fra questi due formati di testo. 7 non ` cos` ad esempio nel lesystem HFS dei Mac, che supporta delle risorse associate ad ogni le, che e specicano fra laltro il contenuto ed il programma da usare per leggerlo. In realt` per alcuni lesystem, come a lXFS della SGI, esiste la possibilit` di associare delle risorse ai le, ma ` una caratteristica tuttora poco utilizzata, a e dato che non corrisponde al modello classico dei le in un sistema Unix.
4

86

CAPITOLO 4. LARCHITETTURA DEI FILE

La prima ` linterfaccia standard di Unix, quella che il manuale delle glibc chiama interfaccia e ` dei descrittori di le (o le descriptor ). E uninterfaccia specica dei sistemi unix-like e fornisce un accesso non buerizzato; la tratteremo in dettaglio in cap. 6. Linterfaccia ` primitiva ed essenziale, laccesso viene detto non buerizzato in quanto la e lettura e la scrittura vengono eseguite chiamando direttamente le system call del kernel (in realt` a il kernel eettua al suo interno alcune buerizzazioni per aumentare lecienza nellaccesso ai dispositivi); i le descriptor sono rappresentati da numeri interi (cio` semplici variabili di tipo e int). Linterfaccia ` denita nellheader unistd.h. e La seconda interfaccia ` quella che il manuale della glibc chiama degli stream.8 Essa fornisce e funzioni pi` evolute e un accesso buerizzato (controllato dalla implementazione fatta dalle u glibc), la tratteremo in dettaglio nel cap. 7. Questa ` linterfaccia standard specicata dallANSI C e perci` si trova anche su tutti i e o sistemi non Unix. Gli stream sono oggetti complessi e sono rappresentati da puntatori ad un opportuna struttura denita dalle librerie del C; si accede ad essi sempre in maniera indiretta utilizzando il tipo FILE *. Linterfaccia ` denita nellheader stdio.h. e Entrambe le interfacce possono essere usate per laccesso ai le come agli altri oggetti del VFS (fo, socket, dispositivi, sui quali torneremo in dettaglio a tempo opportuno), ma per poter accedere alle operazioni di controllo (descritte in sez. 6.3.6 e sez. 6.3.7) su un qualunque tipo di oggetto del VFS occorre usare linterfaccia standard di Unix con i le descriptor. Allo stesso modo devono essere usati i le descriptor se si vuole ricorrere a modalit` speciali di I/O come a il le locking o lI/O non-bloccante (vedi cap. 11). Gli stream forniscono uninterfaccia di alto livello costruita sopra quella dei le descriptor, che permette di poter scegliere tra diversi stili di buerizzazione. Il maggior vantaggio degli stream ` che linterfaccia per le operazioni di input/output ` enormemente pi` ricca di quella dei le e e u descriptor, che forniscono solo funzioni elementari per la lettura/scrittura diretta di blocchi di byte. In particolare gli stream dispongono di tutte le funzioni di formattazione per linput e loutput adatte per manipolare anche i dati in forma di linee o singoli caratteri. In ogni caso, dato che gli stream sono implementati sopra linterfaccia standard di Unix, ` e sempre possibile estrarre il le descriptor da uno stream ed eseguirvi operazioni di basso livello, o associare in un secondo tempo uno stream ad un le descriptor. In generale, se non necessitano specicatamente le funzionalit` di basso livello, ` opportuno a e usare sempre gli stream per la loro maggiore portabilit`, essendo questi ultimi deniti nello a standard ANSI C; linterfaccia con i le descriptor infatti segue solo lo standard POSIX.1 dei sistemi Unix, ed ` pertanto di portabilit` pi` limitata. e a u

4.2

Larchitettura della gestione dei le

In questa sezione esamineremo come viene implementato laccesso ai le in Linux, come il kernel pu` gestire diversi tipi di lesystem, descrivendo prima le caratteristiche generali di un lesystem o di un sistema unix-like, per poi trattare in maniera un po pi` dettagliata il lesystem pi` usato u u con Linux, lext2 (e derivati).

4.2.1

Il Virtual File System di Linux

In Linux il concetto di everything is a le ` stato implementato attraverso il Virtual File System e (da qui in avanti VFS) che ` uno strato intermedio che il kernel usa per accedere ai pi` svariati e u lesystem mantenendo la stessa interfaccia per i programmi in user space. Esso fornisce un livello
in realt` una interfaccia con lo stesso nome ` stata introdotta a livello di kernel negli Unix derivati da System a e V, come strato di astrazione per le e socket; in Linux questa interfaccia, che comunque ha avuto poco successo, non esiste, per cui facendo riferimento agli stream useremo il signicato adottato dal manuale delle glibc.
8

4.2. LARCHITETTURA DELLA GESTIONE DEI FILE

87

di indirezione che permette di collegare le operazioni di manipolazione sui le alle operazioni di I/O, e gestisce lorganizzazione di queste ultime nei vari modi in cui i diversi lesystem le eettuano, permettendo la coesistenza di lesystem dierenti allinterno dello stesso albero delle directory. Quando un processo esegue una system call che opera su un le, il kernel chiama sempre una funzione implementata nel VFS; la funzione eseguir` le manipolazioni sulle strutture generiche e a utilizzer` poi la chiamata alle opportune funzioni del lesystem specico a cui si fa riferimento. a Saranno queste a chiamare le funzioni di pi` basso livello che eseguono le operazioni di I/O sul u dispositivo sico, secondo lo schema riportato in g. 4.1.

Figura 4.1: Schema delle operazioni del VFS.

Il VFS denisce un insieme di funzioni che tutti i lesystem devono implementare. Linterfaccia comprende tutte le funzioni che riguardano i le; le operazioni sono suddivise su tre tipi di oggetti: lesystem, inode e le, corrispondenti a tre apposite strutture denite nel kernel. Il VFS usa una tabella mantenuta dal kernel che contiene il nome di ciascun lesystem supportato: quando si vuole inserire il supporto di un nuovo lesystem tutto quello che occorre ` chiae mare la funzione register_filesystem passandole unapposita struttura file_system_type che contiene i dettagli per il riferimento allimplementazione del medesimo, che sar` aggiunta a alla citata tabella. In questo modo quando viene eettuata la richiesta di montare un nuovo disco (o qualunque altro block device che pu` contenere un lesystem), il VFS pu` ricavare dalla citata tabella il o o puntatore alle funzioni da chiamare nelle operazioni di montaggio. Questultima ` responsabile e di leggere da disco il superblock (vedi sez. 4.2.4), inizializzare tutte le variabili interne e restituire uno speciale descrittore dei lesystem montati al VFS; attraverso questultimo diventa possibile accedere alle funzioni speciche per luso di quel lesystem.

88

CAPITOLO 4. LARCHITETTURA DEI FILE

Il primo oggetto usato dal VFS ` il descrittore di lesystem, un puntatore ad una apposita e struttura che contiene vari dati come le informazioni comuni ad ogni lesystem, i dati privati relativi a quel lesystem specico, e i puntatori alle funzioni del kernel relative al lesystem. Il VFS pu` cos` usare le funzioni contenute nel lesystem descriptor per accedere alle funzioni o speciche di quel lesystem. Gli altri due descrittori usati dal VFS sono relativi agli altri due oggetti su cui ` strutturata e linterfaccia. Ciascuno di essi contiene le informazioni relative al le in uso, insieme ai puntatori alle funzioni dello specico lesystem usate per laccesso dal VFS; in particolare il descrittore dellinode contiene i puntatori alle funzioni che possono essere usate su qualunque le (come link, stat e open), mentre il descrittore di le contiene i puntatori alle funzioni che vengono usate sui le gi` aperti. a

4.2.2

Il funzionamento del Virtual File System

La funzione pi` importante implementata dal VFS ` la system call open che permette di aprire u e un le. Dato un pathname viene eseguita una ricerca dentro la directory entry cache (in breve dcache), una tabella che contiene tutte le directory entry (in breve dentry) che permette di associare in maniera rapida ed eciente il pathname a una specica dentry. Una singola dentry contiene in genere il puntatore ad un inode; questultimo ` la struttura e base che sta sul disco e che identica un singolo oggetto del VFS sia esso un le ordinario, una directory, un link simbolico, una FIFO, un le di dispositivo, o una qualsiasi altra cosa che possa essere rappresentata dal VFS (i tipi di le riportati in tab. 4.1). A ciascuno di essi ` associata e pure una struttura che sta in memoria, e che, oltre alle informazioni sullo specico le, contiene anche il riferimento alle funzioni (i metodi del VFS) da usare per poterlo manipolare. Le dentry vivono in memoria e non vengono mai salvate su disco, vengono usate per motivi di velocit`, gli inode invece stanno su disco e vengono copiati in memoria quando serve, ed ogni a cambiamento viene copiato allindietro sul disco, gli inode che stanno in memoria sono inode del VFS ed ` ad essi che puntano le singole dentry. e La dcache costituisce perci` una sorta di vista completa di tutto lalbero dei le, ovviamente o per non riempire tutta la memoria questa vista ` parziale (la dcache cio` contiene solo le dentry e e per i le per i quali ` stato richiesto laccesso), quando si vuole risolvere un nuovo pathname il e VFS deve creare una nuova dentry e caricare linode corrispondente in memoria. Questo procedimento viene eseguito dal metodo lookup() dellinode della directory che contiene il le; questo viene installato nelle relative strutture in memoria quando si eettua il montaggio lo specico lesystem su cui linode va a vivere. Una volta che il VFS ha a disposizione la dentry (ed il relativo inode) diventa possibile accedere alle varie operazioni sul le come la open per aprire il le o la stat per leggere i dati dellinode e passarli in user space. Lapertura di un le richiede comunque unaltra operazione, lallocazione di una struttura di tipo file in cui viene inserito un puntatore alla dentry e una struttura f_ops che contiene i puntatori ai metodi che implementano le operazioni disponibili sul le. In questo modo i processi in user space possono accedere alle operazioni attraverso detti metodi, che saranno diversi a seconda del tipo di le (o dispositivo) aperto (su questo torneremo in dettaglio in sez. 6.1.1). Un elenco delle operazioni previste dal kernel ` riportato in tab. 4.2. e In questo modo per ciascun le diventano possibili una serie di operazioni (non ` detto che e tutte siano disponibili), che costituiscono linterfaccia astratta del VFS. Qualora se ne voglia eseguire una, il kernel andr` ad utilizzare lopportuna funzione dichiarata in f_ops appropriata a al tipo di le in questione. Pertanto ` possibile scrivere allo stesso modo sulla porta seriale come su un normale le e di dati; ovviamente certe operazioni (nel caso della seriale ad esempio la seek) non saranno

4.2. LARCHITETTURA DELLA GESTIONE DEI FILE


Funzione open read write llseek ioctl readdir poll mmap release fsync fasync Operazione Apre il le (vedi sez. 6.2.1). Legge dal le (vedi sez. 6.2.4). Scrive sul le (vedi sez. 6.2.5). Sposta la posizione corrente sul le (vedi sez. 6.2.3). Accede alle operazioni di controllo (vedi sez. 6.3.7). Legge il contenuto di una directory. Usata nellI/O multiplexing (vedi sez. 11.1). Mappa il le in memoria (vedi sez. 11.3.1). Chiamata quando lultimo riferimento a un le aperto ` e chiuso. Sincronizza il contenuto del le (vedi sez. 6.3.3). Abilita lI/O asincrono (vedi sez. 11.2.3) sul le.

89

Tabella 4.2: Operazioni sui le denite nel VFS.

disponibili, per` con questo sistema lutilizzo di diversi lesystem (come quelli usati da Windows o o MacOs) ` immediato e (relativamente) trasparente per lutente ed il programmatore. e

4.2.3

Il funzionamento di un lesystem Unix

Come gi` accennato in sez. 4.1.1 Linux (ed ogni sistema unix-like) organizza i dati che tiene su a disco attraverso luso di un lesystem. Una delle caratteristiche di Linux rispetto agli altri Unix ` quella di poter supportare, grazie al VFS, una enorme quantit` di lesystem diversi, ognuno e a dei quali ha una sua particolare struttura e funzionalit` proprie. Per questo per il momento non a entreremo nei dettagli di un lesystem specico, ma daremo una descrizione a grandi linee che si adatta alle caratteristiche comuni di qualunque lesystem di sistema unix-like. Lo spazio sico di un disco viene usualmente diviso in partizioni; ogni partizione pu` conteo nere un lesystem. La strutturazione tipica dellinformazione su un disco ` riportata in g. 4.2; e in essa si fa riferimento alla struttura del lesystem ext2, che prevede una separazione dei dati in block group che replicano il superblock (ma sulle caratteristiche di ext2 torneremo in sez. 4.2.4). ` E comunque caratteristica comune di tutti i lesystem per Unix, indipendentemente da come poi viene strutturata nei dettagli questa informazione, prevedere una divisione fra la lista degli inode e lo spazio a disposizione per i dati e le directory.

Figura 4.2: Organizzazione dello spazio su un disco in partizioni e lesystem.

Se si va ad esaminare con maggiore dettaglio la strutturazione dellinformazione allinterno del singolo lesystem (tralasciando i dettagli relativi al funzionamento del lesystem stesso come la strutturazione in gruppi dei blocchi, il superblock e tutti i dati di gestione) possiamo esemplicare la situazione con uno schema come quello esposto in g. 4.3.

90

CAPITOLO 4. LARCHITETTURA DEI FILE

Figura 4.3: Strutturazione dei dati allinterno di un lesystem.

Da g. 4.3 si evidenziano alcune delle caratteristiche di base di un lesystem, sulle quali ` e bene porre attenzione visto che sono fondamentali per capire il funzionamento delle funzioni che manipolano i le e le directory che tratteremo nel prossimo capitolo; in particolare ` opportuno e ricordare sempre che: 1. Linode contiene tutte le informazioni riguardanti il le: il tipo di le, i permessi di accesso, le dimensioni, i puntatori ai blocchi sici che contengono i dati e cos` via; le informazioni che la funzione stat fornisce provengono dallinode; dentro una directory si trover` solo il nome a del le e il numero dellinode ad esso associato, cio` quella che da qui in poi chiameremo e una voce (come traduzione dellinglese directory entry, che non useremo anche per evitare confusione con le dentry del kernel di cui si parlava in sez. 4.2.1). 2. Come mostrato in g. 4.3 si possono avere pi` voci che puntano allo stesso inode. Ogni u inode ha un contatore che contiene il numero di riferimenti (link count) che sono stati fatti ad esso; solo quando questo contatore si annulla i dati del le vengono eettivamente rimossi dal disco. Per questo la funzione per cancellare un le si chiama unlink, ed in realt` non cancella aatto i dati del le, ma si limita ad eliminare la relativa voce da una a directory e decrementare il numero di riferimenti nellinode. 3. Il numero di inode nella voce si riferisce ad un inode nello stesso lesystem e non ci pu` o essere una directory che contiene riferimenti ad inode relativi ad altri lesystem. Questo limita luso del comando ln (che crea una nuova voce per un le esistente, con la funzione link) al lesystem corrente.

4.2. LARCHITETTURA DELLA GESTIONE DEI FILE

91

4. Quando si cambia nome ad un le senza cambiare lesystem, il contenuto del le non viene spostato sicamente, viene semplicemente creata una nuova voce per linode in questione e rimossa la vecchia (questa ` la modalit` in cui opera normalmente il comando mv attraverso e a la funzione rename). Inne ` bene avere presente che, essendo le pure loro, esiste un numero di riferimenti anche e per le directory; per cui, se a partire dalla situazione mostrata in g. 4.3 creiamo una nuova directory img nella directory gapil, avremo una situazione come quella in g. 4.4, dove per chiarezza abbiamo aggiunto dei numeri di inode.

Figura 4.4: Organizzazione dei link per le directory.

La nuova directory avr` allora un numero di riferimenti pari a due, in quanto ` referenziata a e dalla directory da cui si era partiti (in cui ` inserita la nuova voce che fa riferimento a img) e e dalla voce . che ` sempre inserita in ogni directory; questo vale sempre per ogni directory che e non contenga a sua volta altre directory. Al contempo, la directory da cui si era partiti avr` un a numero di riferimenti di almeno tre, in quanto adesso sar` referenziata anche dalla voce .. di a img.

4.2.4

Il lesystem ext2

Il lesystem standard usato da Linux ` il cosiddetto second extended lesystem, identicato dalla e sigla ext2. Esso supporta tutte le caratteristiche di un lesystem standard Unix, ` in grado di e gestire nomi di le lunghi (256 caratteri, estensibili a 1012) con una dimensione massima di 4 Tb. Oltre alle caratteristiche standard, ext2 fornisce alcune estensioni che non sono presenti sugli altri lesystem Unix. Le principali sono le seguenti: i le attributes consentono di modicare il comportamento del kernel quando agisce su gruppi di le. Possono essere impostati su le e directory e in questultimo caso i nuovi le creati nella directory ereditano i suoi attributi. sono supportate entrambe le semantiche di BSD e SVr4 come opzioni di montaggio. La semantica BSD comporta che i le in una directory sono creati con lo stesso identicatore di gruppo della directory che li contiene. La semantica SVr4 comporta che i le vengono

92

CAPITOLO 4. LARCHITETTURA DEI FILE creati con lidenticatore del gruppo primario del processo, eccetto il caso in cui la directory ha il bit di sgid impostato (per una descrizione dettagliata del signicato di questi termini si veda sez. 5.3), nel qual caso le e subdirectory ereditano sia il gid che lo sgid. lamministratore pu` scegliere la dimensione dei blocchi del lesystem in fase di creazione, o a seconda delle sue esigenze (blocchi pi` grandi permettono un accesso pi` veloce, ma u u sprecano pi` spazio disco). u il lesystem implementa link simbolici veloci, in cui il nome del le non ` salvato su un e blocco, ma tenuto allinterno dellinode (evitando letture multiple e spreco di spazio), non tutti i nomi per` possono essere gestiti cos` per limiti di spazio (il limite ` 60 caratteri). o e vengono supportati i le immutabili (che possono solo essere letti) per la protezione di le di congurazione sensibili, o le append-only che possono essere aperti in scrittura solo per aggiungere dati (caratteristica utilizzabile per la protezione dei le di log).

La struttura di ext2 ` stata ispirata a quella del lesystem di BSD: un lesystem ` composto e e da un insieme di blocchi, la struttura generale ` quella riportata in g. 4.3, in cui la partizione e ` divisa in gruppi di blocchi.9 e Ciascun gruppo di blocchi contiene una copia delle informazioni essenziali del lesystem (superblock e descrittore del lesystem sono quindi ridondati) per una maggiore adabilit` e a possibilit` di recupero in caso di corruzione del superblock principale. a

Figura 4.5: Struttura delle directory nel second extented lesystem.

Lutilizzo di raggruppamenti di blocchi ha inoltre degli eetti positivi nelle prestazioni dato che viene ridotta la distanza fra i dati e la tabella degli inode. Le directory sono implementate come una linked list con voci di dimensione variabile. Ciascuna voce della lista contiene il numero di inode , la sua lunghezza, il nome del le e la sua lunghezza, secondo lo schema in g. 4.5; in questo modo ` possibile implementare nomi per i le e anche molto lunghi (no a 1024 caratteri) senza sprecare spazio disco.

non si confonda questa denizione con quella riportata in g. 5.2; in quel caso si fa riferimento alla struttura usata in user space per riportare i dati contenuti in una directory generica, questa fa riferimento alla struttura usata dal kernel per un lesystem ext2, denita nel le ext2_fs.h nella directory include/linux dei sorgenti del kernel.

Capitolo 5

File e directory
In questo capitolo tratteremo in dettaglio le modalit` con cui si gestiscono le e directory, iniziana do dalle funzioni di libreria che si usano per copiarli, spostarli e cambiarne i nomi. Esamineremo poi linterfaccia che permette la manipolazione dei vari attributi di le e directory ed alla ne faremo una trattazione dettagliata su come ` strutturato il sistema base di protezioni e controllo e dellaccesso ai le e sulle funzioni che ne permettono la gestione. Tutto quello che riguarda invece la manipolazione del contenuto dei le ` lasciato ai capitoli successivi. e

5.1

La gestione di le e directory

Come gi` accennato in sez. 4.2.3 in un sistema unix-like la gestione dei le ha delle caratteristiche a speciche che derivano direttamente dallarchitettura del sistema. In questa sezione esamineremo le funzioni usate per la manipolazione di le e directory, per la creazione di link simbolici e diretti, per la gestione e la lettura delle directory. In particolare ci soermeremo sulle conseguenze che derivano dallarchitettura dei lesystem illustrata nel capitolo precedente per quanto riguarda il comportamento delle varie funzioni.

5.1.1

Le funzioni link e unlink

Una caratteristica comune a diversi sistemi operativi ` quella di poter creare dei nomi ttizi e (come gli alias del MacOS o i collegamenti di Windows o i nomi logici del VMS) che permettono di fare riferimento allo stesso le chiamandolo con nomi diversi o accedendovi da directory diverse. Questo ` possibile anche in ambiente Unix, dove tali collegamenti sono usualmente chiamati e link ; ma data larchitettura del sistema riguardo la gestione dei le (ed in particolare quanto trattato in sez. 4.2) ci sono due metodi sostanzialmente diversi per fare questa operazione. Come spiegato in sez. 4.2.3 laccesso al contenuto di un le su disco avviene passando attraverso il suo inode, che ` la struttura usata dal kernel che lo identica univocamente allinterno di e un singolo lesystem. Il nome del le che si trova nella voce di una directory ` solo unetichetta, e mantenuta allinterno della directory, che viene associata ad un puntatore che fa riferimento al suddetto inode. Questo signica che, ntanto che si resta sullo stesso lesystem, la realizzazione di un link ` e immediata, ed uno stesso le pu` avere tanti nomi diversi, dati da altrettante diverse associazioni o allo stesso inode di etichette diverse in directory diverse. Si noti anche che nessuno di questi nomi viene ad assumere una particolare preferenza o originalit` rispetto agli altri, in quanto tutti fanno a comunque riferimento allo stesso inode. Per aggiungere ad una directory una voce che faccia riferimento ad un inode gi` esistente si a 93

94

CAPITOLO 5. FILE E DIRECTORY

utilizza la funzione link; si suole chiamare questo tipo di associazione un collegamento diretto (o hard link ). Il prototipo della funzione `: e
#include <unistd.h> int link(const char *oldpath, const char *newpath) Crea un nuovo collegamento diretto. La funzione restituisce 0 in caso di successo e -1 in caso di errore nel qual caso errno viene impostata ai valori: EXDEV EPERM EEXIST EMLINK i le oldpath e newpath non sono sullo stesso lesystem. il lesystem che contiene oldpath e newpath non supporta i link diretti o ` una e directory. un le (o una directory) con quel nome esiste di gi`. a ci sono troppi link al le oldpath (il numero massimo ` specicato dalla variabile e LINK_MAX, vedi sez. 8.1.1).

ed inoltre EACCES, ENAMETOOLONG, ENOTDIR, EFAULT, ENOMEM, EROFS, ELOOP, ENOSPC, EIO.

La funzione crea sul pathname newpath un collegamento diretto al le indicato da oldpath. Per quanto detto la creazione di un nuovo collegamento diretto non copia il contenuto del le, ma si limita a creare una voce nella directory specicata da newpath e ad aumentare di uno il numero di riferimenti al le (riportato nel campo st_nlink della struttura stat, vedi sez. 5.2.1) aggiungendo il nuovo nome ai precedenti. Si noti che uno stesso le pu` essere cos` chiamato con o vari nomi in diverse directory. Per quanto dicevamo in sez. 4.2.3 la creazione di un collegamento diretto ` possibile solo se ene trambi i pathname sono nello stesso lesystem; inoltre il lesystem deve supportare i collegamenti diretti (il meccanismo non ` disponibile ad esempio con il lesystem vfat di Windows). e La funzione inoltre opera sia sui le ordinari che sugli altri oggetti del lesystem, con leccezione delle directory. In alcune versioni di Unix solo lamministratore ` in grado di creare un e collegamento diretto ad unaltra directory: questo viene fatto perch con una tale operazione ` e e possibile creare dei loop nel lesystem (vedi lesempio mostrato in sez. 5.1.3, dove riprenderemo il discorso) che molti programmi non sono in grado di gestire e la cui rimozione diventerebbe estremamente complicata (in genere per questo tipo di errori occorre far girare il programma fsck per riparare il lesystem). Data la pericolosit` di questa operazione e la disponibilit` dei link simbolici che possono a a fornire la stessa funzionalit` senza questi problemi, nei lesystem usati in Linux questa carattea ristica ` stata completamente disabilitata, e al tentativo di creare un link diretto ad una directory e la funzione restituisce lerrore EPERM. La rimozione di un le (o pi` precisamente della voce che lo referenzia allinterno di una u directory) si eettua con la funzione unlink; il suo prototipo ` il seguente: e
#include <unistd.h> int unlink(const char *pathname) Cancella un le. La funzione restituisce zero in caso di successo e -1 per un errore, nel qual caso il le non viene toccato. La variabile errno viene impostata secondo i seguenti codici di errore: EISDIR EROFS EISDIR pathname si riferisce ad una directory.
1

pathname ` su un lesystem montato in sola lettura. e pathname fa riferimento a una directory.

ed inoltre: EACCES, EFAULT, ENOENT, ENOTDIR, ENOMEM, EROFS, ELOOP, EIO. questo ` un valore specico ritornato da Linux che non consente luso di unlink con le directory (vedi e sez. 5.1.2). Non ` conforme allo standard POSIX, che prescrive invece luso di EPERM in caso loperazione non sia e consentita o il processo non abbia privilegi sucienti.
1

5.1. LA GESTIONE DI FILE E DIRECTORY

95

La funzione cancella il nome specicato da pathname nella relativa directory e decrementa il numero di riferimenti nel relativo inode. Nel caso di link simbolico cancella il link simbolico; nel caso di socket, fo o le di dispositivo rimuove il nome, ma come per i le i processi che hanno aperto uno di questi oggetti possono continuare ad utilizzarlo. Per cancellare una voce in una directory ` necessario avere il permesso di scrittura su di essa, e dato che si va a rimuovere una voce dal suo contenuto, e il diritto di esecuzione sulla directory che la contiene (aronteremo in dettaglio largomento dei permessi di le e directory in sez. 5.3). Se inoltre lo sticky bit (vedi sez. 5.3.2) ` impostato occorrer` anche essere proprietari del le o e a proprietari della directory (o root, per cui nessuna delle restrizioni ` applicata). e Una delle caratteristiche di queste funzioni ` che la creazione/rimozione del nome dalla e directory e lincremento/decremento del numero di riferimenti nellinode devono essere eettuati in maniera atomica (si veda sez. 3.5.1) senza possibili interruzioni fra le due operazioni. Per questo entrambe queste funzioni sono realizzate tramite una singola system call. Si ricordi inne che un le non viene eliminato dal disco ntanto che tutti i riferimenti ad esso sono stati cancellati: solo quando il link count mantenuto nellinode diventa zero lo spazio occupato su disco viene rimosso (si ricordi comunque che a questo si aggiunge sempre unulteriore condizione,2 e cio` che non ci siano processi che abbiano il suddetto le aperto). e Questa propriet` viene spesso usata per essere sicuri di non lasciare le temporanei su disco a in caso di crash dei programmi; la tecnica ` quella di aprire il le e chiamare unlink subito dopo, e in questo modo il contenuto del le ` sempre disponibile allinterno del processo attraverso il e suo le descriptor (vedi sez. 6.1.1) ntanto che il processo non chiude il le, ma non ne resta traccia in nessuna directory, e lo spazio occupato su disco viene immediatamente rilasciato alla conclusione del processo (quando tutti i le vengono chiusi).

5.1.2

Le funzioni remove e rename

Al contrario di quanto avviene con altri Unix, in Linux non ` possibile usare unlink sulle e directory; per cancellare una directory si pu` usare la funzione rmdir (vedi sez. 5.1.4), oppure o la funzione remove. Questa ` la funzione prevista dallo standard ANSI C per cancellare un le o una directory e (e funziona anche per i sistemi che non supportano i link diretti). Per i le ` identica a unlink e e per le directory ` identica a rmdir; il suo prototipo `: e e
#include <stdio.h> int remove(const char *pathname) Cancella un nome dal lesystem. La funzione restituisce zero in caso di successo e -1 per un errore, nel qual caso il le non viene toccato. I codici di errore riportati in errno sono quelli della chiamata utilizzata, pertanto si pu` fare o riferimento a quanto illustrato nelle descrizioni di unlink e rmdir.

La funzione utilizza la funzione unlink3 per cancellare i le e la funzione rmdir per cancellare le directory; si tenga presente che per alcune implementazioni del protocollo NFS utilizzare questa funzione pu` comportare la scomparsa di le ancora in uso. o
come vedremo in cap. 6 il kernel mantiene anche una tabella dei le aperti nei vari processi, che a sua volta contiene i riferimenti agli inode ad essi relativi. Prima di procedere alla cancellazione dello spazio occupato su disco dal contenuto di un le il kernel controlla anche questa tabella, per vericare che anche in essa non ci sia pi` nessun riferimento allinode in questione. u 3 questo vale usando le glibc; nelle libc4 e nelle libc5 la funzione remove ` un semplice alias alla funzione unlink e e quindi non pu` essere usata per le directory. o
2

96

CAPITOLO 5. FILE E DIRECTORY

Per cambiare nome ad un le o a una directory (che devono comunque essere nello stesso lesystem) si usa invece la funzione rename,4 il cui prototipo `: e
#include <stdio.h> int rename(const char *oldpath, const char *newpath) Rinomina un le. La funzione restituisce zero in caso di successo e -1 per un errore, nel qual caso il le non viene toccato. La variabile errno viene impostata secondo i seguenti codici di errore: EISDIR EXDEV newpath ` una directory mentre oldpath non ` una directory. e e oldpath e newpath non sono sullo stesso lesystem. o oldpath o newpath sono in uso da parte di qualche processo (come directory di lavoro o come radice) o del sistema (come mount point). newpath contiene un presso di oldpath o pi` in generale si ` cercato di creare una u e directory come sotto-directory di se stessa. uno dei componenti dei pathname non ` una directory o oldpath ` una directory e e e newpath esiste e non ` una directory. e

ENOTEMPTY newpath ` una directory gi` esistente e non vuota. e a EBUSY EINVAL ENOTDIR

ed inoltre EACCES, EPERM, EMLINK, ENOENT, ENOMEM, EROFS, ELOOP e ENOSPC.

La funzione rinomina il le oldpath in newpath, eseguendo se necessario lo spostamento di un le fra directory diverse. Eventuali altri link diretti allo stesso le non vengono inuenzati. Il comportamento della funzione ` diverso a seconda che si voglia rinominare un le o una e directory; se ci riferisce ad un le allora newpath, se esiste, non deve essere una directory (altrimenti si ha lerrore EISDIR). Nel caso newpath indichi un le esistente questo viene cancellato e rimpiazzato (atomicamente). Se oldpath ` una directory allora newpath, se esiste, deve essere una directory vuota, ale trimenti si avranno gli errori ENOTDIR (se non ` una directory) o ENOTEMPTY (se non ` vuota). e e Chiaramente newpath non pu` contenere oldpath altrimenti si avr` un errore EINVAL. o a Se oldpath si riferisce ad un link simbolico questo sar` rinominato; se newpath ` un link a e simbolico verr` cancellato come qualunque altro le. Inne qualora oldpath e newpath siano a due nomi dello stesso le lo standard POSIX prevede che la funzione non dia errore, e non faccia nulla, lasciando entrambi i nomi; Linux segue questo standard, anche se, come fatto notare dal manuale delle glibc, il comportamento pi` ragionevole sarebbe quello di cancellare oldpath. u Il vantaggio nelluso di questa funzione al posto della chiamata successiva di link e unlink ` che loperazione ` eseguita atomicamente, non pu` esistere cio` nessun istante in cui un altro e e o e processo pu` trovare attivi entrambi i nomi dello stesso le, o, in caso di sostituzione di un le o esistente, non trovare questultimo prima che la sostituzione sia stata eseguita. In ogni caso se newpath esiste e loperazione fallisce per un qualche motivo (come un crash del kernel), rename garantisce di lasciare presente unistanza di newpath. Tuttavia nella sovrascrittura potr` esistere una nestra in cui sia oldpath che newpath fanno riferimento allo stesso a le.

5.1.3

I link simbolici

Come abbiamo visto in sez. 5.1.1 la funzione link crea riferimenti agli inode, pertanto pu` o funzionare soltanto per le che risiedono sullo stesso lesystem e solo per un lesystem di tipo Unix. Inoltre abbiamo visto che in Linux non ` consentito eseguire un link diretto ad una e directory. Per ovviare a queste limitazioni i sistemi Unix supportano unaltra forma di link (i cosiddetti soft link o symbolic link ), che sono, come avviene in altri sistemi operativi, dei le speciali che
la funzione ` denita dallo standard ANSI C, ma si applica solo per i le, lo standard POSIX estende la e funzione anche alle directory.
4

5.1. LA GESTIONE DI FILE E DIRECTORY

97

contengono semplicemente il riferimento ad un altro le (o directory). In questo modo ` possibile e eettuare link anche attraverso lesystem diversi, a le posti in lesystem che non supportano i link diretti, a delle directory, ed anche a le che non esistono ancora. Il sistema funziona in quanto i link simbolici sono riconosciuti come tali dal kernel5 per cui alcune funzioni di libreria (come open o stat) quando ricevono come argomento un link simbolico vengono automaticamente applicate al le da esso specicato. La funzione che permette di creare un nuovo link simbolico ` symlink, ed il suo prototipo `: e e
#include <unistd.h> int symlink(const char *oldpath, const char *newpath) Crea un nuovo link simbolico di nome newpath il cui contenuto ` oldpath. e La funzione restituisce zero in caso di successo e -1 per un errore, nel qual caso la variabile errno assumer` i valori: a EPERM ENOENT EEXIST EROFS il lesystem che contiene newpath non supporta i link simbolici. una componente di newpath non esiste o oldpath ` una stringa vuota. e esiste gi` un le newpath. a newpath ` su un lesystem montato in sola lettura. e

ed inoltre EFAULT, EACCES, ENAMETOOLONG, ENOTDIR, ENOMEM, ELOOP, ENOSPC e EIO.

Si tenga presente che la funzione non eettua nessun controllo sullesistenza di un le di nome oldpath, ma si limita ad inserire quella stringa nel link simbolico. Pertanto un link simbolico pu` anche riferirsi ad un le che non esiste: in questo caso si ha quello che viene chiamato un o dangling link, letteralmente un link ciondolante. Come accennato i link simbolici sono risolti automaticamente dal kernel allinvocazione delle varie system call; in tab. 5.1 si ` riportato un elenco dei comportamenti delle varie funzioni di e libreria che operano sui le nei confronti della risoluzione dei link simbolici, specicando quali seguono il link simbolico e quali invece possono operare direttamente sul suo contenuto.
Funzione access chdir chmod chown creat exec lchown link lstat mkdir mkfifo mknod open opendir pathconf readlink remove rename stat truncate unlink Segue il link Non segue il link

Tabella 5.1: Uso dei link simbolici da parte di alcune funzioni.

Si noti che non si ` specicato il comportamento delle funzioni che operano con i le dee
` uno dei diversi tipi di le visti in tab. 4.1, contrassegnato come tale nellinode, e riconoscibile dal valore del e campo st_mode della struttura stat (vedi sez. 5.2.1).
5

98

CAPITOLO 5. FILE E DIRECTORY

scriptor, in quanto la risoluzione del link simbolico viene in genere eettuata dalla funzione che restituisce il le descriptor (normalmente la open, vedi sez. 6.2.1) e tutte le operazioni seguenti fanno riferimento solo a questultimo. Dato che, come indicato in tab. 5.1, funzioni come la open seguono i link simbolici, occorrono funzioni apposite per accedere alle informazioni del link invece che a quelle del le a cui esso fa riferimento. Quando si vuole leggere il contenuto di un link simbolico si usa la funzione readlink, il cui prototipo `: e
#include <unistd.h> int readlink(const char *path, char *buff, size_t size) Legge il contenuto del link simbolico indicato da path nel buer buff di dimensione size. La funzione restituisce il numero di caratteri letti dentro buff o -1 per un errore, nel qual caso la variabile errno assumer` i valori: a EINVAL path non ` un link simbolico o size non ` positiva. e e ed inoltre ENOTDIR, ENAMETOOLONG, ENOENT, EACCES, ELOOP, EIO, EFAULT e ENOMEM.

La funzione apre il link simbolico, ne legge il contenuto, lo scrive nel buer, e lo richiude. Si tenga presente che la funzione non termina la stringa con un carattere nullo e la tronca alla dimensione specicata da size per evitare di sovrascrivere oltre le dimensioni del buer.

Figura 5.1: Esempio di loop nel lesystem creato con un link simbolico.

Un caso comune che si pu` avere con i link simbolici ` la creazione dei cosiddetti loop. La o e situazione ` illustrata in g. 5.1, che riporta la struttura della directory /boot. Come si vede si e ` creato al suo interno un link simbolico che punta di nuovo a /boot.6 e Questo pu` causare problemi per tutti quei programmi che eettuano la scansione di una dio rectory senza tener conto dei link simbolici, ad esempio se lanciassimo un comando del tipo grep
il loop mostrato in g. 5.1 ` un usato per poter permettere a grub (un bootloader in grado di leggere die rettamente da vari lesystem il le da lanciare come sistema operativo) di vedere i le contenuti nella directory /boot con lo stesso pathname con cui verrebbero visti dal sistema operativo, anche se essi si trovano, come accade spesso, su una partizione separata (che grub, allavvio, vede come radice).
6

5.1. LA GESTIONE DI FILE E DIRECTORY

99

-r linux *, il loop nella directory porterebbe il comando ad esaminare /boot, /boot/boot, /boot/boot/boot e cos` via. Per questo motivo il kernel e le librerie prevedono che nella risoluzione di un pathname possano essere seguiti un numero limitato di link simbolici, il cui valore limite ` specicato dalla e costante MAXSYMLINKS. Qualora questo limite venga superato viene generato un errore ed errno viene impostata al valore ELOOP. Un punto da tenere sempre presente ` che, come abbiamo accennato, un link simbolico pu` e o fare riferimento anche ad un le che non esiste; ad esempio possiamo creare un le temporaneo nella nostra directory con un link del tipo: $ ln -s /tmp/tmp_file temporaneo anche se /tmp/tmp_file non esiste. Questo pu` generare confusione, in quanto aprendo in o scrittura temporaneo verr` creato /tmp/tmp_file e scritto; ma accedendo in sola lettura a a temporaneo, ad esempio con cat, otterremmo: $ cat temporaneo cat: temporaneo: No such file or directory con un errore che pu` sembrare sbagliato, dato che unispezione con ls ci mostrerebbe invece o lesistenza di temporaneo.

5.1.4

La creazione e la cancellazione delle directory

Bench in sostanza le directory non siano altro che dei le contenenti elenchi di nomi ed inode, e non ` possibile trattarle come le ordinari e devono essere create direttamente dal kernel attrae verso una opportuna system call.7 La funzione usata per creare una directory ` mkdir, ed il suo e prototipo `: e
#include <sys/stat.h> #include <sys/types.h> int mkdir(const char *dirname, mode_t mode) Crea una nuova directory. La funzione restituisce zero in caso di successo e -1 per un errore, nel qual caso errno assumer` i a valori: EEXIST EACCES EMLINK un le (o una directory) con quel nome esiste di gi`. a non c` il permesso di scrittura per la directory in cui si vuole inserire la nuova e directory. la directory in cui si vuole creare la nuova directory contiene troppi le; sotto Linux questo normalmente non avviene perch il lesystem standard consente la creazione di e un numero di le maggiore di quelli che possono essere contenuti nel disco, ma potendo avere a che fare anche con lesystem di altri sistemi questo errore pu` presentarsi. o non c` abbastanza spazio sul le system per creare la nuova directory o si ` esaurita e e la quota disco dellutente.

ENOSPC

ed inoltre anche EPERM, EFAULT, ENAMETOOLONG, ENOENT, ENOTDIR, ENOMEM, ELOOP, EROFS.

La funzione crea una nuova directory vuota, che contiene cio` solo le due voci standard e presenti in ogni directory (cio` . e ..), con il nome indicato dallargomento dirname. Il e nome pu` essere indicato sia come pathname assoluto che come pathname relativo. o I permessi di accesso (vedi sez. 5.3) con cui la directory viene creata sono specicati dallargomento mode, i cui possibili valori sono riportati in tab. 5.9; si tenga presente che questi
questo ` quello che permette anche, attraverso luso del VFS, lutilizzo di diversi formati per la gestione dei e suddetti elenchi.
7

100

CAPITOLO 5. FILE E DIRECTORY

sono modicati dalla maschera di creazione dei le (si veda sez. 5.3.3). La titolarit` della nuova a directory ` impostata secondo quanto riportato in sez. 5.3.4. e La funzione che permette la cancellazione di una directory ` invece rmdir, ed il suo prototipo e `: e
#include <sys/stat.h> int rmdir(const char *dirname) Cancella una directory. La funzione restituisce zero in caso di successo e -1 per un errore, nel qual caso errno assumer` i a valori: EPERM il lesystem non supporta la cancellazione di directory, oppure la directory che contiene dirname ha lo sticky bit impostato e luser-ID eettivo del processo non corrisponde al proprietario della directory. non c` il permesso di scrittura per la directory che contiene la directory che si vuoe le cancellare, o non c` il permesso di attraversare (esecuzione) una delle directory e specicate in dirname. la directory specicata ` la directory di lavoro o la radice di qualche processo. e

EACCES

EBUSY

ENOTEMPTY la directory non ` vuota. e ed inoltre anche EFAULT, ENAMETOOLONG, ENOENT, ENOTDIR, ENOMEM, ELOOP, EROFS.

La funzione cancella la directory dirname, che deve essere vuota (la directory deve cio` e contenere soltanto le due voci standard . e ..). Il nome pu` essere indicato con il pathname o assoluto o relativo. La modalit` con cui avviene la cancellazione ` analoga a quella di unlink: ntanto che il a e numero di link allinode della directory non diventa nullo e nessun processo ha la directory aperta lo spazio occupato su disco non viene rilasciato. Se un processo ha la directory aperta la funzione rimuove il link allinode e nel caso sia lultimo, pure le voci standard . e .., a questo punto il kernel non consentir` di creare pi` nuovi le nella directory. a u

5.1.5

La creazione di le speciali

Finora abbiamo parlato esclusivamente di le, directory e link simbolici; in sez. 4.1.2 abbiamo visto per` che il sistema prevede pure degli altri tipi di le speciali, come i le di dispositivo e o le fo (i socket sono un caso a parte, che tratteremo in cap. 15). La manipolazione delle caratteristiche di questi le e la loro cancellazione pu` essere eettuata o con le stesse funzioni che operano sui le regolari; ma quando li si devono creare sono necessarie delle funzioni apposite. La prima di queste funzioni ` mknod, il suo prototipo `: e e
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> int mknod(const char *pathname, mode_t mode, dev_t dev) Crea un inode, si usa per creare i le speciali. La funzione restituisce zero in caso di successo e -1 per un errore, nel qual caso errno assumer` i a valori: EPERM EINVAL EEXIST non si hanno privilegi sucienti a creare linode, o il lesystem su cui si ` cercato di e creare pathname non supporta loperazione. il valore di mode non indica un le, una fo o un dispositivo. pathname esiste gi` o ` un link simbolico. a e

ed inoltre anche EFAULT, EACCES, ENAMETOOLONG, ENOENT, ENOTDIR, ENOMEM, ELOOP, ENOSPC, EROFS.

La funzione permette di creare un le speciale, ma si pu` usare anche per creare le regolari o e fo; largomento mode specica il tipo di le che si vuole creare ed i relativi permessi, secondo

5.1. LA GESTIONE DI FILE E DIRECTORY

101

i valori riportati in tab. 5.4, che vanno combinati con un OR binario. I permessi sono comunque modicati nella maniera usuale dal valore di umask (si veda sez. 5.3.3). Per il tipo di le pu` essere specicato solo uno fra: S_IFREG per un le regolare (che sar` o a creato vuoto), S_IFBLK per un dispositivo a blocchi, S_IFCHR per un dispositivo a caratteri e S_IFIFO per una fo. Un valore diverso comporter` lerrore EINVAL. Qualora si sia specicato a in mode un le di dispositivo, il valore di dev viene usato per indicare a quale dispositivo si fa riferimento. Solo lamministratore pu` creare un le di dispositivo o un le regolare usando questa o funzione; ma in Linux8 luso per la creazione di una fo ` consentito anche agli utenti normali. e I nuovi inode creati con mknod apparterranno al proprietario e al gruppo del processo che li ha creati, a meno che non si sia attivato il bit sgid per la directory o sia stata attivata la semantica BSD per il lesystem (si veda sez. 5.3.4) in cui si va a creare linode. Per creare una fo (un le speciale, su cui torneremo in dettaglio in sez. 12.1.4) lo standard POSIX specica luso della funzione mkfifo, il cui prototipo `: e
#include <sys/types.h> #include <sys/stat.h> int mkfifo(const char *pathname, mode_t mode) Crea una fo. La funzione restituisce zero in caso di successo e -1 per un errore, nel qual caso errno assumer` i a valori EACCES, EEXIST, ENAMETOOLONG, ENOENT, ENOSPC, ENOTDIR e EROFS.

La funzione crea la fo pathname con i permessi mode. Come per mknod il le pathname non deve esistere (neanche come link simbolico); al solito i permessi specicati da mode vengono modicati dal valore di umask.

5.1.6

Accesso alle directory

Bench le directory alla ne non siano altro che dei le che contengono delle liste di nomi ed e inode, per il ruolo che rivestono nella struttura del sistema, non possono essere trattate come dei normali le di dati. Ad esempio, onde evitare inconsistenze allinterno del lesystem, solo il kernel pu` scrivere il contenuto di una directory, e non pu` essere un processo a inserirvi o o direttamente delle voci con le usuali funzioni di scrittura. Ma se la scrittura e laggiornamento dei dati delle directory ` compito del kernel, sono molte e le situazioni in cui i processi necessitano di poterne leggere il contenuto. Bench questo possa e essere fatto direttamente (vedremo in sez. 6.2.1 che ` possibile aprire una directory come se e fosse un le, anche se solo in sola lettura) in generale il formato con cui esse sono scritte pu` o dipendere dal tipo di lesystem, tanto che, come riportato in tab. 4.2, il VFS del kernel prevede una apposita funzione per la lettura delle directory. Tutto questo si riette nello standard POSIX9 che ha introdotto una apposita interfaccia per la lettura delle directory, basata sui cosiddetti directory stream (chiamati cos` per lanalogia con i le stream dellinterfaccia standard di cap. 7). La prima funzione di questa interfaccia ` e opendir, il cui prototipo `: e
#include <sys/types.h> #include <dirent.h> DIR * opendir(const char *dirname) Apre un directory stream. La funzione restituisce un puntatore al directory stream in caso di successo e NULL per un errore, nel qual caso errno assumer` i valori EACCES, EMFILE, ENFILE, ENOENT, ENOMEM e ENOTDIR. a la funzione non ` prevista dallo standard POSIX, e deriva da SVr4, con appunto questa dierenza e diversi e codici di errore. 9 le funzioni sono previste pure in BSD e SVID.
8

102

CAPITOLO 5. FILE E DIRECTORY

La funzione apre un directory stream per la directory dirname, ritornando il puntatore ad un oggetto di tipo DIR (che ` il tipo opaco usato dalle librerie per gestire i directory stream) da e usare per tutte le operazioni successive, la funzione inoltre posiziona lo stream sulla prima voce contenuta nella directory. Dato che le directory sono comunque dei le, in alcuni casi pu` servire conoscere il le o descriptor associato ad un directory stream, a questo scopo si pu` usare la funzione dirfd, il cui o prototipo `: e
#include <sys/types.h> #include <dirent.h> int dirfd(DIR * dir) Restituisce il le descriptor associato ad un directory stream. La funzione restituisce il le descriptor (un valore positivo) in caso di successo e -1 in caso di errore.

La funzione10 restituisce il le descriptor associato al directory stream dir, essa ` disponibile e solo denendo _BSD_SOURCE o _SVID_SOURCE. Di solito si utilizza questa funzione in abbinamento alla funzione fchdir per cambiare la directory di lavoro (vedi sez. 5.1.7) a quella relativa allo stream che si sta esaminando. La lettura di una voce della directory viene eettuata attraverso la funzione readdir; il suo prototipo `: e
#include <sys/types.h> #include <dirent.h> struct dirent *readdir(DIR *dir) Legge una voce dal directory stream. La funzione restituisce il puntatore alla struttura contenente i dati in caso di successo e NULL altrimenti, in caso di descrittore non valido errno assumer` il valore EBADF, il valore NULL viene a restituito anche quando si raggiunge la ne dello stream.

La funzione legge la voce corrente nella directory, posizionandosi sulla voce successiva. I dati vengono memorizzati in una struttura dirent (la cui denizione11 ` riportata in g. 5.2). La e funzione restituisce il puntatore alla struttura; si tenga presente per` che questultima ` allocata o e staticamente, per cui viene sovrascritta tutte le volte che si ripete la lettura di una voce sullo stesso stream. Di questa funzione esiste anche una versione rientrante, readdir_r, che non usa una struttura allocata staticamente, e pu` essere utilizzata anche con i thread; il suo prototipo `: o e
#include <sys/types.h> #include <dirent.h> int readdir_r(DIR *dir, struct dirent *entry, struct dirent **result) Legge una voce dal directory stream. La funzione restituisce 0 in caso di successo e -1 in caso di errore, gli errori sono gli stessi di readdir.

La funzione restituisce in result (come value result argument) lindirizzo dove sono stati salvati i dati, che di norma corrisponde a quello della struttura precedentemente allocata e specicata dallargomento entry (anche se non ` assicurato che la funzione usi lo spazio fornito e dallutente).
10 questa funzione ` una estensione di BSD non presente in POSIX, introdotta con BSD 4.3-Reno; ` presente in e e Linux con le libc5 (a partire dalla versione 5.1.2) e con le glibc. 11 la denizione ` quella usata a Linux, che si trova nel le /usr/include/bits/dirent.h, essa non contempla la e presenza del campo d_namlen che indica la lunghezza del nome del le (ed infatti la macro _DIRENT_HAVE_D_NAMLEN non ` denita). e

5.1. LA GESTIONE DI FILE E DIRECTORY

103

I vari campi di dirent contengono le informazioni relative alle voci presenti nella directory; sia BSD che SVr412 prevedono che siano sempre presenti il campo d_name, che contiene il nome del le nella forma di una stringa terminata da uno zero,13 ed il campo d_ino, che contiene il numero di inode cui il le ` associato (di solito corrisponde al campo st_ino di stat). e
struct dirent { ino_t d_ino ; off_t d_off ; unsigned short int d_reclen ; unsigned char d_type ; char d_name [256]; };

/* /* /* /* /*

inode number */ offset to the next dirent */ length of this record */ type of file */ We must not include limits . h ! */

Figura 5.2: La struttura dirent per la lettura delle informazioni dei le.

La presenza di ulteriori campi opzionali ` segnalata dalla denizione di altrettante macro e nella forma _DIRENT_HAVE_D_XXX dove XXX ` il nome del relativo campo; nel nostro caso sono e denite le macro _DIRENT_HAVE_D_TYPE, _DIRENT_HAVE_D_OFF e _DIRENT_HAVE_D_RECLEN.
Valore DT_UNKNOWN DT_REG DT_DIR DT_FIFO DT_SOCK DT_CHR DT_BLK Tipo di le Tipo sconosciuto. File normale. Directory. Fifo. Socket. Dispositivo a caratteri. Dispositivo a blocchi.

Tabella 5.2: Costanti che indicano i vari tipi di le nel campo d_type della struttura dirent.

Per quanto riguarda il signicato dei campi opzionali, il campo d_type indica il tipo di le (fo, directory, link simbolico, ecc.); i suoi possibili valori14 sono riportati in tab. 5.2; per la conversione da e verso lanalogo valore mantenuto dentro il campo st_mode di stat sono denite anche due macro di conversione IFTODT e DTTOIF:
int IFTODT(mode_t MODE) Converte il tipo di le dal formato di st_mode a quello di d_type. mode_t DTTOIF(int DTYPE) Converte il tipo di le dal formato di d_type a quello di st_mode.

Il campo d_off contiene invece la posizione della voce successiva della directory, mentre il campo d_reclen la lunghezza totale della voce letta. Con questi due campi diventa possibile, determinando la posizione delle varie voci, spostarsi allinterno dello stream usando la funzione seekdir,15 il cui prototipo `: e
#include <dirent.h> void seekdir(DIR *dir, off_t offset) Cambia la posizione allinterno di un directory stream. lo standard POSIX prevede invece solo la presenza del campo d_fileno, identico d_ino, che in Linux ` denito e come alias di questultimo. Il campo d_name ` considerato dipendente dallimplementazione. e 13 lo standard POSIX non specica una lunghezza, ma solo un limite NAME_MAX; in SVr4 la lunghezza del campo ` denita come NAME_MAX+1 che di norma porta al valore di 256 byte usato anche in Linux. e 14 no alla versione 2.1 delle glibc questo campo, pur presente nella struttura, non era implementato, e resta sempre al valore DT_UNKNOWN. 15 sia questa funzione che telldir, sono estensioni prese da BSD, non previste dallo standard POSIX.
12

104

CAPITOLO 5. FILE E DIRECTORY

La funzione non ritorna nulla e non segnala errori, ` per` necessario che il valore dellare o gomento offset sia valido per lo stream dir; esso pertanto deve essere stato ottenuto o dal valore di d_off di dirent o dal valore restituito dalla funzione telldir, che legge la posizione corrente; il prototipo di questultima `: e
#include <dirent.h> off_t telldir(DIR *dir) Ritorna la posizione corrente in un directory stream. La funzione restituisce la posizione corrente nello stream (un numero positivo) in caso di successo, e -1 altrimenti, nel qual caso errno assume solo il valore di EBADF, corrispondente ad un valore errato per dir.

La sola funzione di posizionamento nello stream prevista dallo standard POSIX ` rewinddir, e che riporta la posizione a quella iniziale; il suo prototipo `: e
#include <sys/types.h> #include <dirent.h> void rewinddir(DIR *dir) Si posiziona allinizio di un directory stream.

Una volta completate le operazioni si pu` chiudere il directory stream con la funzione o closedir, il cui prototipo `: e
#include <sys/types.h> #include <dirent.h> int closedir(DIR * dir) Chiude un directory stream. La funzione restituisce 0 in caso di successo e -1 altrimenti, nel qual caso errno assume il valore EBADF.

A parte queste funzioni di base in BSD 4.3 ` stata introdotta unaltra funzione che permette e di eseguire una scansione completa (con tanto di ricerca ed ordinamento) del contenuto di una directory; la funzione ` scandir16 ed il suo prototipo `: e e
#include <dirent.h> int scandir(const char *dir, struct dirent ***namelist, int(*filter)(const struct dirent *), int(*compar)(const struct dirent **, const struct dirent **)) Esegue una scansione di un directory stream. La funzione restituisce in caso di successo il numero di voci trovate, e -1 altrimenti.

Al solito, per la presenza fra gli argomenti di due puntatori a funzione, il prototipo non ` e molto comprensibile; queste funzioni per` sono quelle che controllano rispettivamente la selezione o di una voce (quella passata con largomento filter) e lordinamento di tutte le voci selezionate (quella specicata dellargomento compar). La funzione legge tutte le voci della directory indicata dallargomento dir, passando un puntatore a ciascuna di esse (una struttura dirent) come argomento della funzione di selezione specicata da filter; se questa ritorna un valore diverso da zero il puntatore viene inserito in un vettore che viene allocato dinamicamente con malloc. Qualora si specichi un valore NULL per largomento filter non viene fatta nessuna selezione e si ottengono tutte le voci presenti. Le voci selezionate possono essere riordinate tramite qsort, le modalit` del riordinamento a possono essere personalizzate usando la funzione compar come criterio di ordinamento di qsort, la funzione prende come argomenti le due strutture dirent da confrontare restituendo un valore
16

in Linux questa funzione ` stata introdotta n dalle libc4. e

5.1. LA GESTIONE DI FILE E DIRECTORY

105

positivo, nullo o negativo per indicarne lordinamento; alla ne lindirizzo della lista ordinata dei puntatori alle strutture dirent viene restituito nellargomento namelist.17 Per lordinamento, vale a dire come valori possibili per largomento compar sono disponibili due funzioni predenite, alphasort e versionsort, i cui prototipi sono:
#include <dirent.h> int alphasort(const void *a, const void *b) int versionsort(const void *a, const void *b) Funzioni per lordinamento delle voci di directory stream. Le funzioni restituiscono un valore minore, uguale o maggiore di zero qualora il primo argomento sia rispettivamente minore, uguale o maggiore del secondo.

La funzione alphasort deriva da BSD ed ` presente in Linux n dalle libc418 e deve essere e specicata come argomento compare per ottenere un ordinamento alfabetico (secondo il valore del campo d_name delle varie voci). Le glibc prevedono come estensione19 anche versionsort, che ordina i nomi tenendo conto del numero di versione (cio` qualcosa per cui file10 viene e comunque dopo file4.) Un semplice esempio delluso di queste funzioni ` riportato in g. 5.3, dove si ` riportata la e e sezione principale di un programma che, usando la funzione di scansione illustrata in g. 5.4, stampa i nomi dei le contenuti in una directory e la relativa dimensione (in sostanza una versione semplicata del comando ls). Il programma ` estremamente semplice; in g. 5.3 si ` omessa la parte di gestione delle e e opzioni (che prevede solo luso di una funzione per la stampa della sintassi, anchessa omessa) ma il codice completo potr` essere trovato coi sorgenti allegati nel le myls.c. a In sostanza tutto quello che fa il programma, dopo aver controllato (10-13) di avere almeno un argomento (che indicher` la directory da esaminare) ` chiamare (14) la funzione DirScan per a e eseguire la scansione, usando la funzione do_ls (20-26) per fare tutto il lavoro. Questultima si limita (23) a chiamare stat sul le indicato dalla directory entry passata come argomento (il cui nome ` appunto direntry->d_name), memorizzando in una opportuna e struttura data i dati ad esso relativi, per poi provvedere (24) a stampare il nome del le e la dimensione riportata in data. Dato che la funzione verr` chiamata allinterno di DirScan per ogni voce presente questo ` a e suciente a stampare la lista completa dei le e delle relative dimensioni. Si noti inne come si restituisca sempre 0 come valore di ritorno per indicare una esecuzione senza errori. Tutto il grosso del lavoro ` svolto dalla funzione DirScan, riportata in g. 5.4. La funzione ` e e volutamente generica e permette di eseguire una funzione, passata come secondo argomento, su tutte le voci di una directory. La funzione inizia con laprire (19-23) uno stream sulla directory passata come primo argomento, stampando un messaggio in caso di errore. Il passo successivo (24-25) ` cambiare directory di lavoro (vedi sez. 5.1.7), usando in sequenza e le funzione dirfd e fchdir (in realt` si sarebbe potuto usare direttamente chdir su dirname), in a modo che durante il successivo ciclo (27-31) sulle singole voci dello stream ci si trovi allinterno della directory.20
la funzione alloca automaticamente la lista, e restituisce, come value result argument, lindirizzo della stessa; questo signica che namelist deve essere dichiarato come struct dirent **namelist ed alla funzione si deve passare il suo indirizzo. 18 la versione delle libc4 e libc5 usa per` come argomenti dei puntatori a delle strutture dirent; le glibc usano o il prototipo originario di BSD, mostrato anche nella denizione, che prevede puntatori a void. 19 le glibc, a partire dalla versione 2.1, eettuano anche lordinamento alfabetico tenendo conto delle varie localizzazioni, usando strcoll al posto di strcmp. 20 questo ` essenziale al funzionamento della funzione do_ls (e ad ogni funzione che debba usare il campo d_name, e in quanto i nomi dei le memorizzati allinterno di una struttura dirent sono sempre relativi alla directory in questione, e senza questo posizionamento non si sarebbe potuto usare stat per ottenere le dimensioni.
17

106

CAPITOLO 5. FILE E DIRECTORY

# include # include 3 # include 4 # include 5 # include


1 2 6 7 8

< sys / types .h > < sys / stat .h > < dirent .h > < stdlib .h > < unistd .h >

/* directory */ /* C standard library */

/* computation function for DirScan */ int do_ls ( struct dirent * direntry ); 9 /* main body */ 10 int main ( int argc , char * argv []) 11 { 12 ... 13 if (( argc - optind ) != 1) { /* There must be remaing parameters */ 14 printf ( " Wrong number of arguments % d \ n " , argc - optind ); 15 usage (); 16 } 17 DirScan ( argv [1] , do_ls ); 18 exit (0); 19 } 20 /* 21 * Routine to print file name and size inside DirScan 22 */ 23 int do_ls ( struct dirent * direntry ) 24 { 25 struct stat data ;
26

stat ( direntry - > d_name , & data ); /* get stat data */ 28 printf ( " File : % s \ t size : % d \ n " , direntry - > d_name , data . st_size ); 29 return 0; 30 }
27

Figura 5.3: Esempio di codice per eseguire la lista dei le contenuti in una directory.

Avendo usato lo stratagemma di fare eseguire tutte le manipolazioni necessarie alla funzione passata come secondo argomento, il ciclo di scansione della directory ` molto semplice; si legge e una voce alla volta (27) allinterno di una istruzione di while e ntanto che si riceve una voce valida (cio` un puntatore diverso da NULL) si esegue (27) la funzione di elaborazione compare e (che nel nostro caso sar` do_ls), ritornando con un codice di errore (28) qualora questa presenti a una anomalia (identicata da un codice di ritorno negativo). Una volta terminato il ciclo la funzione si conclude con la chiusura (32) dello stream21 e la restituzione (33) del codice di operazioni concluse con successo.

5.1.7

La directory di lavoro

A ciascun processo ` associata una directory nel lesystem che ` chiamata directory corrente e e o directory di lavoro (in inglese current working directory) che ` quella a cui si fa riferimento e quando un pathname ` espresso in forma relativa, dove il relativa fa riferimento appunto a e questa directory. Quando un utente eettua il login, questa directory viene impostata alla home directory del suo account. Il comando cd della shell consente di cambiarla a piacere, spostandosi da una
nel nostro caso, uscendo subito dopo la chiamata, questo non servirebbe, in generale per` loperazione ` o e necessaria, dato che la funzione pu` essere invocata molte volte allinterno dello stesso processo, per cui non o chiudere gli stream comporterebbe un consumo progressivo di risorse, con conseguente rischio di esaurimento delle stesse
21

5.1. LA GESTIONE DI FILE E DIRECTORY

107

# include # include 3 # include 4 # include 5 # include


1 2 6 7 8

< sys / types .h > < sys / stat .h > < dirent .h > < stdlib .h > < unistd .h >

/* directory */ /* C standard library */

/* * Function DirScan : 9 * 10 * Input : the directory name and a computation function 11 * Return : 0 if OK , -1 on errors 12 */ 13 int DirScan ( char * dirname , int (* compute )( struct dirent *)) 14 { 15 DIR * dir ; 16 struct dirent * direntry ;
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33

if ( ( dir = opendir ( dirname )) == NULL ) { /* open directory */ printf ( " Opening % s \ n " , dirname ); /* on error print messages */ perror ( " Cannot open directory " ); /* and then return */ return -1; } fd = dirfd ( dir ); /* get file descriptor */ fchdir ( fd ); /* change directory */ /* loop on directory entries */ while ( ( direntry = readdir ( dir )) != NULL ) { /* read entry */ if ( compute ( direntry )) { /* execute function on it */ return -1; /* on error return */ } } closedir ( dir ); return 0; }

Figura 5.4: Codice della funzione di scansione di una directory contenuta nel le DirScan.c.

directory ad unaltra, il comando pwd la stampa sul terminale. Siccome la directory corrente resta la stessa quando viene creato un processo glio (vedi sez. 3.2.2), la directory corrente della shell diventa anche la directory corrente di qualunque comando da essa lanciato. In genere il kernel tiene traccia per ciascun processo dellinode della directory di lavoro, per ottenere il pathname occorre usare una apposita funzione di libreria, getcwd, il cui prototipo `: e
#include <unistd.h> char *getcwd(char *buffer, size_t size) Legge il pathname della directory di lavoro corrente. La funzione restituisce il puntatore buffer se riesce, NULL se fallisce, in questultimo caso la variabile errno ` impostata con i seguenti codici di errore: e EINVAL ERANGE EACCES largomento size ` zero e buffer non ` nullo. e e largomento size ` pi` piccolo della lunghezza del pathname. e u manca il permesso di lettura o di ricerca su uno dei componenti del pathname (cio` e su una delle directory superiori alla corrente).

La funzione restituisce il pathname completo della directory di lavoro nella stringa puntata da buffer, che deve essere precedentemente allocata, per una dimensione massima di size. Il buer deve essere sucientemente lungo da poter contenere il pathname completo pi` lo zero di u

108

CAPITOLO 5. FILE E DIRECTORY

terminazione della stringa. Qualora esso ecceda le dimensioni specicate con size la funzione restituisce un errore. Si pu` anche specicare un puntatore nullo come buffer,22 nel qual caso la stringa sar` o a allocata automaticamente per una dimensione pari a size qualora questa sia diversa da zero, o della lunghezza esatta del pathname altrimenti. In questo caso ci si deve ricordare di disallocare la stringa una volta cessato il suo utilizzo. Di questa funzione esiste una versione char *getwd(char *buffer) fatta per compatibilit` a allindietro con BSD, che non consente di specicare la dimensione del buer; esso deve essere allocato in precedenza ed avere una dimensione superiore a PATH_MAX (di solito 256 byte, vedi sez. 8.1.1); il problema ` che in Linux non esiste una dimensione superiore per un pathname, e per cui non ` detto che il buer sia suciente a contenere il nome del le, e questa ` la ragione e e principale per cui questa funzione ` deprecata. e Una seconda funzione simile ` char *get_current_dir_name(void) che ` sostanzialmente e e equivalente ad una getcwd(NULL, 0), con la sola dierenza che essa ritorna il valore della variabile di ambiente PWD, che essendo costruita dalla shell pu` contenere un pathname comprendente o anche dei link simbolici. Usando getcwd infatti, essendo il pathname ricavato risalendo allindietro lalbero della directory, si perderebbe traccia di ogni passaggio attraverso eventuali link simbolici. Per cambiare la directory di lavoro si pu` usare la funzione chdir (equivalente del comando o di shell cd) il cui nome sta appunto per change directory, il suo prototipo `: e
#include <unistd.h> int chdir(const char *pathname) Cambia la directory di lavoro in pathname. La funzione restituisce 0 in caso di successo e -1 per un errore, nel qual caso errno assumer` i a valori: ENOTDIR EACCES non si ` specicata una directory. e manca il permesso di ricerca su uno dei componenti di path.

ed inoltre EFAULT, ENAMETOOLONG, ENOENT, ENOMEM, ELOOP e EIO.

ed ovviamente pathname deve indicare una directory per la quale si hanno i permessi di accesso. Dato che anche le directory sono le, ` possibile riferirsi ad esse anche tramite il le descriptor, e e non solo tramite il pathname, per fare questo si usa fchdir, il cui prototipo `: e
#include <unistd.h> int fchdir(int fd) Identica a chdir, ma usa il le descriptor fd invece del pathname. La funzione restituisce zero in caso di successo e -1 per un errore, in caso di errore errno assumer` a i valori EBADF o EACCES.

anche in questo caso fd deve essere un le descriptor valido che fa riferimento ad una directory. Inoltre lunico errore di accesso possibile (tutti gli altri sarebbero occorsi allapertura di fd), ` e quello in cui il processo non ha il permesso di accesso alla directory specicata da fd.

5.1.8

I le temporanei

In molte occasioni ` utile poter creare dei le temporanei; bench la cosa sembri semplice, in e e realt` il problema ` pi` sottile di quanto non appaia a prima vista. Infatti anche se sembrerebbe a e u banale generare un nome a caso e creare il le dopo aver controllato che questo non esista, nel momento fra il controllo e la creazione si ha giusto lo spazio per una possibile race condition (si ricordi quanto visto in sez. 3.5.2).
22

questa ` unestensione allo standard POSIX.1, supportata da Linux. e

5.1. LA GESTIONE DI FILE E DIRECTORY

109

Le glibc provvedono varie funzioni per generare nomi di le temporanei, di cui si abbia certezza di unicit` (al momento della generazione); la prima di queste funzioni ` tmpnam il cui a e prototipo `: e
#include <stdio.h> char *tmpnam(char *string) Restituisce il puntatore ad una stringa contente un nome di le valido e non esistente al momento dellinvocazione. La funzione ritorna il puntatore alla stringa con il nome o NULL in caso di fallimento. Non sono deniti errori.

se si ` passato un puntatore string non nullo questo deve essere di dimensione L_tmpnam (coe stante denita in stdio.h, come P_tmpdir e TMP_MAX) ed il nome generato vi verr` copiato a automaticamente; altrimenti il nome sar` generato in un buer statico interno che verr` soa a vrascritto ad una chiamata successiva. Successive invocazioni della funzione continueranno a restituire nomi unici no ad un massimo di TMP_MAX volte. Al nome viene automaticamente aggiunto come presso la directory specicata da P_tmpdir. Di questa funzione esiste una versione rientrante, tmpnam_r, che non fa nulla quando si passa NULL come argomento. Una funzione simile, tempnam, permette di specicare un presso per il le esplicitamente, il suo prototipo `: e
#include <stdio.h> char *tempnam(const char *dir, const char *pfx) Restituisce il puntatore ad una stringa contente un nome di le valido e non esistente al momento dellinvocazione. La funzione ritorna il puntatore alla stringa con il nome o NULL in caso di fallimento, errno viene impostata a ENOMEM qualora fallisca lallocazione della stringa.

La funzione alloca con malloc la stringa in cui restituisce il nome, per cui ` sempre rientrante, e occorre per` ricordarsi di disallocare il puntatore che restituisce. Largomento pfx specica un o presso di massimo 5 caratteri per il nome provvisorio. La funzione assegna come directory per il le temporaneo (vericando che esista e sia accessibili), la prima valida delle seguenti: La variabile di ambiente TMPNAME (non ha eetto se non ` denita o se il programma e chiamante ` suid o sgid, vedi sez. 5.3.2). e il valore dellargomento dir (se diverso da NULL). Il valore della costante P_tmpdir. la directory /tmp. In ogni caso, anche se la generazione del nome ` casuale, ed ` molto dicile ottenere un e e nome duplicato, nulla assicura che un altro processo non possa avere creato, fra lottenimento del nome e lapertura del le, un altro le con lo stesso nome; per questo motivo quando si usa il nome ottenuto da una di queste funzioni occorre sempre aprire il nuovo le in modalit` di a esclusione (cio` con lopzione O_EXCL per i le descriptor o con il ag x per gli stream) che fa e fallire lapertura in caso il le sia gi` esistente. a Per evitare di dovere eettuare a mano tutti questi controlli, lo standard POSIX denisce la funzione tmpfile, il cui prototipo `: e
#include <stdio.h> FILE *tmpfile (void) Restituisce un le temporaneo aperto in lettura/scrittura. La funzione ritorna il puntatore allo stream associato al le temporaneo in caso di successo e NULL in caso di errore, nel qual caso errno assumer` i valori: a EINTR EEXIST la funzione ` stata interrotta da un segnale. e non ` stato possibile generare un nome univoco. e

ed inoltre EFAULT, EMFILE, ENFILE, ENOSPC, EROFS e EACCES.

110

CAPITOLO 5. FILE E DIRECTORY

essa restituisce direttamente uno stream gi` aperto (in modalit` r+b, si veda sez. 7.2.1) e pronto a a per luso, che viene automaticamente cancellato alla sua chiusura o alluscita dal programma. Lo standard non specica in quale directory verr` aperto il le, ma le glibc prima tentano con a P_tmpdir e poi con /tmp. Questa funzione ` rientrante e non sore di problemi di race condition. e Alcune versioni meno recenti di Unix non supportano queste funzioni; in questo caso si possono usare le vecchie funzioni mktemp e mkstemp che modicano una stringa di input che serve da modello e che deve essere conclusa da 6 caratteri X che verranno sostituiti da un codice unico. La prima delle due ` analoga a tmpnam e genera un nome casuale, il suo prototipo `: e e
#include <stlib.h> char *mktemp(char *template) Genera un lename univoco sostituendo le XXXXXX nali di template. La funzione ritorna il puntatore template in caso di successo e NULL in caso di errore, nel qual caso errno assumer` i valori: a EINVAL template non termina con XXXXXX.

dato che template deve poter essere modicata dalla funzione non si pu` usare una stringa o costante. Tutte le avvertenze riguardo alle possibili race condition date per tmpnam continuano a valere; inoltre in alcune vecchie implementazioni il valore usato per sostituire le XXXXXX viene formato con il pid del processo pi` una lettera, il che mette a disposizione solo 26 possibilit` u a diverse per il nome del le, e rende il nome temporaneo facile da indovinare. Per tutti questi motivi la funzione ` deprecata e non dovrebbe mai essere usata. e La seconda funzione, mkstemp ` sostanzialmente equivalente a tmpfile, ma restituisce un e le descriptor invece di uno stream; il suo prototipo `: e
#include <stlib.h> int mkstemp(char *template) Genera un le temporaneo con un nome ottenuto sostituendo le XXXXXX nali di template. La funzione ritorna il le descriptor in caso successo e -1 in caso di errore, nel qual caso errno assumer` i valori: a EINVAL EEXIST template non termina con XXXXXX. non ` riuscita a creare un le temporaneo, il contenuto di template ` indenito. e e

come per mktemp anche in questo caso template non pu` essere una stringa costante. La funzione o apre un le in lettura/scrittura con la funzione open, usando lopzione O_EXCL (si veda sez. 6.2.1), in questo modo al ritorno della funzione si ha la certezza di essere i soli utenti del le. I permessi sono impostati al valore 060023 (si veda sez. 5.3.1). In OpenBSD ` stata introdotta unaltra funzione24 simile alle precedenti, mkdtemp, che crea e una directory temporanea; il suo prototipo `: e
#include <stlib.h> char *mkdtemp(char *template) Genera una directory temporaneo il cui nome ` ottenuto sostituendo le XXXXXX nali di e template. La funzione ritorna il puntatore al nome della directory in caso successo e NULL in caso di errore, nel qual caso errno assumer` i valori: a EINVAL template non termina con XXXXXX. pi` gli altri eventuali codici di errore di mkdir. u questo ` vero a partire dalle glibc 2.0.7, le versioni precedenti delle glibc e le vecchie libc5 e libc4 usavano il e valore 0666 che permetteva a chiunque di leggere i contenuti del le. 24 introdotta anche in Linux a partire dalle glibc 2.1.91.
23

5.2. LA MANIPOLAZIONE DELLE CARATTERISTICHE DEI FILE

111

la directory ` creata con permessi 0700 (al solito si veda cap. 6 per i dettagli); dato che e la creazione della directory ` sempre esclusiva i precedenti problemi di race condition non si e pongono.

5.2

La manipolazione delle caratteristiche dei le

Come spiegato in sez. 4.2.3 tutte le informazioni generali relative alle caratteristiche di ciascun le, a partire dalle informazioni relative al controllo di accesso, sono mantenute nellinode. Vedremo in questa sezione come sia possibile leggere tutte queste informazioni usando la funzione stat, che permette laccesso a tutti i dati memorizzati nellinode; esamineremo poi le varie funzioni usate per manipolare tutte queste informazioni (eccetto quelle che riguardano la gestione del controllo di accesso, trattate in in sez. 5.3).

5.2.1

La lettura delle caratteristiche dei le

La lettura delle informazioni relative ai le ` fatta attraverso la famiglia delle funzioni stat (stat, e fstat e lstat); questa ` la funzione che ad esempio usa il comando ls per poter ottenere e e mostrare tutti i dati relativi ad un le. I prototipi di queste funzioni sono i seguenti:
#include <sys/types.h> #include <sys/stat.h> #include <unistd.h> int stat(const char *file_name, struct stat *buf) Legge le informazione del le specicato da file_name e le inserisce in buf. int lstat(const char *file_name, struct stat *buf) Identica a stat eccetto che se il file_name ` un link simbolico vengono lette le informazioni e relative ad esso e non al le a cui fa riferimento. int fstat(int filedes, struct stat *buf) Identica a stat eccetto che si usa con un le aperto, specicato tramite il suo le descriptor filedes. Le funzioni restituiscono 0 in caso di successo e -1 per un errore, nel qual caso errno assumer` a uno dei valori: EBADF, ENOENT, ENOTDIR, ELOOP, EFAULT, EACCES, ENOMEM, ENAMETOOLONG.

il loro comportamento ` identico, solo che operano rispettivamente su un le, su un link simbolico e e su un le descriptor. La struttura stat usata da queste funzioni ` denita nellheader sys/stat.h e in generale e dipende dallimplementazione; la versione usata da Linux ` mostrata in g. 5.5, cos` come rie portata dalla pagina di manuale di stat (in realt` la denizione eettivamente usata nel kernel a dipende dallarchitettura e ha altri campi riservati per estensioni come tempi pi` precisi, o per u il padding dei campi). Si noti come i vari membri della struttura siano specicati come tipi primitivi del sistema (di quelli deniti in tab. 1.2, e dichiarati in sys/types.h).

5.2.2

I tipi di le

Come riportato in tab. 4.1 in Linux oltre ai le e alle directory esistono altri oggetti che possono stare su un lesystem. Il tipo di le ` ritornato dalla funzione stat come maschera binaria nel e campo st_mode (che contiene anche le informazioni relative ai permessi) di una struttura stat. Dato che il valore numerico pu` variare a seconda delle implementazioni, lo standard POSIX o denisce un insieme di macro per vericare il tipo di le, queste vengono usate anche da Linux che supporta pure le estensioni allo standard per i link simbolici e i socket denite da BSD; lelenco completo delle macro con cui ` possibile estrarre linformazione da st_mode ` riportato e e in tab. 5.3.

112

CAPITOLO 5. FILE E DIRECTORY

struct stat { dev_t ino_t mode_t nlink_t uid_t gid_t dev_t off_t unsigned long unsigned long time_t time_t time_t };

st_dev ; st_ino ; st_mode ; st_nlink ; st_uid ; st_gid ; st_rdev ; st_size ; st_blksize ; st_blocks ; st_atime ; st_mtime ; st_ctime ;

/* /* /* /* /* /* /* /* /* /* /* /* /*

device */ inode */ protection */ number of hard links */ user ID of owner */ group ID of owner */ device type ( if inode device ) */ total size , in bytes */ blocksize for filesystem I / O */ number of blocks allocated */ time of last access */ time of last modification */ time of last change */

Figura 5.5: La struttura stat per la lettura delle informazioni dei le. Macro S_ISREG(m) S_ISDIR(m) S_ISCHR(m) S_ISBLK(m) S_ISFIFO(m) S_ISLNK(m) S_ISSOCK(m) Tipo del le File normale. Directory. Dispositivo a caratteri. Dispositivo a blocchi. Fifo. Link simbolico. Socket.

Tabella 5.3: Macro per i tipi di le (denite in sys/stat.h).

Oltre alle macro di tab. 5.3 ` possibile usare direttamente il valore di st_mode per ricavare e il tipo di le controllando direttamente i vari bit in esso memorizzati. Per questo sempre in sys/stat.h sono denite le costanti numeriche riportate in tab. 5.4. Il primo valore dellelenco di tab. 5.4 ` la maschera binaria che permette di estrarre i bit nei e quali viene memorizzato il tipo di le, i valori successivi sono le costanti corrispondenti ai singoli bit, e possono essere usati per eettuare la selezione sul tipo di le voluto, con unopportuna combinazione. Ad esempio se si volesse impostare una condizione che permetta di controllare se un le ` e una directory o un le ordinario si potrebbe denire la macro di preprocessore: # define IS_FILE_DIR ( x ) ((( x ) & S_IFMT ) & ( S_IFDIR | S_IFREG )) in cui prima si estraggono da st_mode i bit relativi al tipo di le e poi si eettua il confronto con la combinazione di tipi scelta.

5.2.3

Le dimensioni dei le

Il campo st_size di una struttura stat contiene la dimensione del le in byte, se si tratta di un le regolare. Nel caso di un link simbolico la dimensione ` quella del pathname che il link e stesso contiene; per le fo questo campo ` sempre nullo. e Il campo st_blocks denisce la lunghezza del le in blocchi di 512 byte. Il campo st_blksize inne denisce la dimensione preferita per i trasferimenti sui le (che ` la dimensione usata anche e dalle librerie del C per linterfaccia degli stream); scrivere sul le a blocchi di dati di dimensione inferiore sarebbe ineciente. Si tenga conto che la lunghezza del le riportata in st_size non ` detto che corrisponda e alloccupazione dello spazio su disco per via della possibile esistenza dei cosiddetti holes (lette-

5.2. LA MANIPOLAZIONE DELLE CARATTERISTICHE DEI FILE


Flag S_IFMT S_IFSOCK S_IFLNK S_IFREG S_IFBLK S_IFDIR S_IFCHR S_IFIFO S_ISUID S_ISGID S_ISVTX S_IRUSR S_IWUSR S_IXUSR S_IRGRP S_IWGRP S_IXGRP S_IROTH S_IWOTH S_IXOTH Valore 0170000 0140000 0120000 0100000 0060000 0040000 0020000 0010000 0004000 0002000 0001000 00400 00200 00100 00040 00020 00010 00004 00002 00001 Signicato Maschera per i bit del tipo di le. Socket. Link simbolico. File regolare. Dispositivo a blocchi. Directory. Dispositivo a caratteri. Fifo. Set UID bit . Set GID bit . Sticky bit . Il proprietario ha permesso di lettura. Il proprietario ha permesso di scrittura. Il proprietario ha permesso di esecuzione. Il gruppo ha permesso di lettura. Il gruppo ha permesso di scrittura. Il gruppo ha permesso di esecuzione. Gli altri hanno permesso di lettura. Gli altri hanno permesso di esecuzione. Gli altri hanno permesso di esecuzione.

113

Tabella 5.4: Costanti per lidenticazione dei vari bit che compongono il campo st_mode (denite in sys/stat.h).

ralmente buchi) che si formano tutte le volte che si va a scrivere su un le dopo aver eseguito una lseek (vedi sez. 6.2.3) oltre la sua ne. In questo caso si avranno risultati dierenti a seconda del modo in cui si calcola la lunghezza del le, ad esempio il comando du, (che riporta il numero di blocchi occupati) potr` dare una a dimensione inferiore, mentre se si legge dal le (ad esempio usando il comando wc -c), dato che in tal caso per le parti non scritte vengono restituiti degli zeri, si avr` lo stesso risultato di ls. a Se ` sempre possibile allargare un le, scrivendoci sopra od usando la funzione lseek per spoe starsi oltre la sua ne, esistono anche casi in cui si pu` avere bisogno di eettuare un troncamento, o scartando i dati presenti al di l` della dimensione scelta come nuova ne del le. a Un le pu` sempre essere troncato a zero aprendolo con il ag O_TRUNC, ma questo ` un o e caso particolare; per qualunque altra dimensione si possono usare le due funzioni truncate e ftruncate, i cui prototipi sono:
#include <unistd.h> int truncate(const char *file_name, off_t length) Fa si che la dimensione del le file_name sia troncata ad un valore massimo specicato da lenght. int ftruncate(int fd, off_t length)) Identica a truncate eccetto che si usa con un le aperto, specicato tramite il suo le descriptor fd. Le funzioni restituiscono zero in caso di successo e -1 per un errore, nel qual caso errno viene impostata opportunamente; per ftruncate si hanno i valori: EBADF EINVAL fd non ` un le descriptor. e fd ` un riferimento ad un socket, non a un le o non ` aperto in scrittura. e e il le non ha permesso di scrittura o non si ha il permesso di esecuzione una delle directory del pathname. il le ` un programma in esecuzione. e

per truncate si hanno: EACCES ETXTBSY

ed anche ENOTDIR, ENAMETOOLONG, ENOENT, EROFS, EIO, EFAULT, ELOOP.

Se il le ` pi` lungo della lunghezza specicata i dati in eccesso saranno perduti; il compore u tamento in caso di lunghezza inferiore non ` specicato e dipende dallimplementazione: il le e

114

CAPITOLO 5. FILE E DIRECTORY

pu` essere lasciato invariato o esteso no alla lunghezza scelta; in questultimo caso lo spazio o viene riempito con zeri (e in genere si ha la creazione di un hole nel le).

5.2.4

I tempi dei le

Il sistema mantiene per ciascun le tre tempi. Questi sono registrati nellinode insieme agli altri attributi del le e possono essere letti tramite la funzione stat, che li restituisce attraverso tre campi della struttura stat di g. 5.5. Il signicato di detti tempi e dei relativi campi ` e riportato nello schema in tab. 5.5, dove ` anche riportato un esempio delle funzioni che eettuano e cambiamenti su di essi.
Membro st_atime st_mtime st_ctime Signicato ultimo accesso ai dati del le ultima modica ai dati del le ultima modica ai dati dellinode Funzione read, utime write, utime chmod, utime Opzione di ls -u default -c

Tabella 5.5: I tre tempi associati a ciascun le.

Il primo punto da tenere presente ` la dierenza fra il cosiddetto tempo di modica (il e modication time st_mtime) e il tempo di cambiamento di stato (il change time st_ctime). Il primo infatti fa riferimento ad una modica del contenuto di un le, mentre il secondo ad una modica dellinode; siccome esistono molte operazioni (come la funzione link e molte altre che vedremo in seguito) che modicano solo le informazioni contenute nellinode senza toccare il contenuto del le, diventa necessario lutilizzo di un altro tempo. Il sistema non tiene conto dellultimo accesso allinode, pertanto funzioni come access o stat non hanno alcuna inuenza sui tre tempi. Il tempo di ultimo accesso (ai dati) viene di solito usato per cancellare i le che non servono pi` dopo un certo lasso di tempo (ad esempio u il programma leafnode cancella i vecchi articoli sulla base di questo tempo).
File o directory riferimento (a) (m) del (c) Directory contenente il riferimento (a) (m) (c)

Funzione chmod, fchmod chown, fchown creat creat exec lchown link mkdir mkfifo open open pipe read remove remove rename rmdir truncate, ftruncate unlink utime write

Note

con O_CREATE con O_TRUNC

con O_CREATE con O_TRUNC

se esegue unlink se esegue rmdir per entrambi gli argomenti

Tabella 5.6: Prospetto dei cambiamenti eettuati sui tempi di ultimo accesso (a), ultima modica (m) e ultimo cambiamento (c) dalle varie funzioni operanti su le e directory.

5.2. LA MANIPOLAZIONE DELLE CARATTERISTICHE DEI FILE

115

Il tempo di ultima modica invece viene usato da make per decidere quali le necessitano di essere ricompilati o (talvolta insieme anche al tempo di cambiamento di stato) per decidere quali le devono essere archiviati per il backup. Il comando ls (quando usato con le opzioni -l o -t) mostra i tempi dei le secondo lo schema riportato nellultima colonna di tab. 5.5. Leetto delle varie funzioni di manipolazione dei le sui tempi ` illustrato in tab. 5.6. Si sono e riportati gli eetti sia per il le a cui si fa riferimento, sia per la directory che lo contiene; questi ultimi possono essere capiti se si tiene conto di quanto gi` detto, e cio` che anche le directory a e sono le (che contengono una lista di nomi) che il sistema tratta in maniera del tutto analoga a tutti gli altri. Per questo motivo tutte le volte che compiremo unoperazione su un le che comporta una modica del nome contenuto nella directory, andremo anche a scrivere sulla directory che lo contiene cambiandone il tempo di modica. Un esempio di questo pu` essere la cancellazione o di un le, invece leggere o scrivere o cambiare i permessi di un le ha eetti solo sui tempi di questultimo. Si noti inne come st_ctime non abbia nulla a che fare con il tempo di creazione del le, usato in molti altri sistemi operativi, ma che in Unix non esiste. Per questo motivo quando si copia un le, a meno di preservare esplicitamente i tempi (ad esempio con lopzione -p di cp) esso avr` sempre il tempo corrente come data di ultima modica. a I tempi di ultimo accesso e modica possono essere cambiati usando la funzione utime, il cui prototipo `: e
#include <utime.h> int utime(const char *filename, struct utimbuf *times) Cambia i tempi di ultimo accesso e modica dellinode specicato da filename secondo i campi actime e modtime di times. Se questa ` NULL allora viene usato il tempo corrente. e La funzione restituisce 0 in caso di successo e -1 in caso di errore, nel qual caso errno assumer` a uno dei valori: EACCES ENOENT non si ha il permesso di scrittura sul le. filename non esiste.

La funzione prende come argomento times una struttura utimbuf, la cui denizione ` ripore tata in g. 5.6, con la quale si possono specicare i nuovi valori che si vogliono impostare per tempi.
struct utimbuf { time_t actime ; /* access time */ time_t modtime ; /* modification time */ };

Figura 5.6: La struttura utimbuf, usata da utime per modicare i tempi dei le.

Leetto della funzione e i privilegi necessari per eseguirla dipendono da cosa ` largomento e times; se ` NULL la funzione imposta il tempo corrente ed ` suciente avere accesso in scrittura e e al le; se invece si ` specicato un valore la funzione avr` successo solo se si ` proprietari del le e a e (o si hanno i privilegi di amministratore). Si tenga presente che non ` comunque possibile specicare il tempo di cambiamento di stato e del le, che viene comunque cambiato dal kernel tutte le volte che si modica linode (quindi anche alla chiamata di utime). Questo serve anche come misura di sicurezza per evitare che si possa modicare un le nascondendo completamente le proprie tracce. In realt` la cosa resta a possibile, se si ` in grado di accedere al le di dispositivo, scrivendo direttamente sul disco senza e passare attraverso il lesystem, ma ovviamente in questo modo la cosa ` molto pi` complicata e u da realizzare.

116

CAPITOLO 5. FILE E DIRECTORY

5.3

Il controllo di accesso ai le

Una delle caratteristiche fondamentali di tutti i sistemi unix-like ` quella del controllo di ace cesso ai le, che viene implementato per qualunque lesystem standard.25 In questa sezione ne esamineremo i concetti essenziali e le funzioni usate per gestirne i vari aspetti.

5.3.1

I permessi per laccesso ai le

Ad ogni le Linux associa sempre lutente che ne ` proprietario (il cosiddetto owner ) ed un e gruppo di appartenenza, secondo il meccanismo degli identicatori di utente e gruppo (uid e gid). Questi valori sono accessibili da programma tramite la funzione stat, e sono mantenuti nei campi st_uid e st_gid della struttura stat (si veda sez. 5.2.1).26 Il controllo di accesso ai le segue un modello abbastanza semplice che prevede tre permessi fondamentali strutturati su tre livelli di accesso. Esistono varie estensioni a questo modello,27 ma nella maggior parte dei casi il meccanismo standard ` pi` che suciente a soddisfare tutte e u le necessit` pi` comuni. I tre permessi di base associati ad ogni le sono: a u il permesso di lettura (indicato con la lettera r, dallinglese read ). il permesso di scrittura (indicato con la lettera w, dallinglese write). il permesso di esecuzione (indicato con la lettera x, dallinglese execute). mentre i tre livelli su cui sono divisi i privilegi sono: i privilegi per lutente proprietario del le. i privilegi per un qualunque utente faccia parte del gruppo cui appartiene il le. i privilegi per tutti gli altri utenti. Linsieme dei permessi viene espresso con un numero a 12 bit; di questi i nove meno signicativi sono usati a gruppi di tre per indicare i permessi base di lettura, scrittura ed esecuzione e sono applicati rispettivamente rispettivamente al proprietario, al gruppo, a tutti gli altri.

Figura 5.7: Lo schema dei bit utilizzati per specicare i permessi di un le contenuti nel campo st_mode di stat.

I restanti tre bit (noti come suid bit, sgid bit, e sticky bit) sono usati per indicare alcune caratteristiche pi` complesse del meccanismo del controllo di accesso su cui torneremo in seguito u (in sez. 5.3.2); lo schema di allocazione dei bit ` riportato in g. 5.7. e
25 per standard si intende che implementa le caratteristiche previste dallo standard POSIX; in Linux sono disponibili anche una serie di altri lesystem, come quelli di Windows e del Mac, che non supportano queste caratteristiche. 26 questo ` vero solo per lesystem di tipo Unix, ad esempio non ` vero per il lesystem vfat di Windows, che e e non fornisce nessun supporto per laccesso multiutente, e per il quale i permessi vengono assegnati in maniera ssa con un opzione in fase di montaggio. 27 come le Access Control List che sono state aggiunte ai lesystem standard con opportune estensioni (vedi sez. 5.4.2) per arrivare a meccanismi di controllo ancora pi` sosticati come il mandatory access control di u SE-Linux.

5.3. IL CONTROLLO DI ACCESSO AI FILE

117

Anche i permessi, come tutte le altre informazioni pertinenti al le, sono memorizzati nellinode; in particolare essi sono contenuti in alcuni bit del campo st_mode della struttura stat (si veda di nuovo g. 5.5). In genere ci si riferisce ai tre livelli dei privilegi usando le lettere u (per user ), g (per group) e o (per other ), inoltre se si vuole indicare tutti i raggruppamenti insieme si usa la lettera a (per all ). Si tenga ben presente questa distinzione dato che in certi casi, mutuando la terminologia in uso nel VMS, si parla dei permessi base come di permessi per owner, group ed all, le cui iniziali possono dar luogo a confusione. Le costanti che permettono di accedere al valore numerico di questi bit nel campo st_mode sono riportate in tab. 5.7.
st_mode bit S_IRUSR S_IWUSR S_IXUSR S_IRGRP S_IWGRP S_IXGRP S_IROTH S_IWOTH S_IXOTH Signicato user-read, lutente pu` leggere. o user-write, lutente pu` scrivere. o user-execute, lutente pu` eseguire. o group-read, il gruppo pu` leggere. o group-write, il gruppo pu` scrivere. o group-execute, il gruppo pu` eseguire. o other-read, tutti possono leggere. other-write, tutti possono scrivere. other-execute, tutti possono eseguire.

Tabella 5.7: I bit dei permessi di accesso ai le, come deniti in <sys/stat.h>

I permessi vengono usati in maniera diversa dalle varie funzioni, e a seconda che si riferiscano a dei le, dei link simbolici o delle directory; qui ci limiteremo ad un riassunto delle regole generali, entrando nei dettagli pi` avanti. u La prima regola ` che per poter accedere ad un le attraverso il suo pathname occorre il e permesso di esecuzione in ciascuna delle directory che compongono il pathname; lo stesso vale per aprire un le nella directory corrente (per la quale appunto serve il diritto di esecuzione). Per una directory infatti il permesso di esecuzione signica che essa pu` essere attraversata o nella risoluzione del pathname, ed ` distinto dal permesso di lettura che invece implica che si e pu` leggere il contenuto della directory. o Questo signica che se si ha il permesso di esecuzione senza permesso di lettura si potr` lo a stesso aprire un le in una directory (se si hanno i permessi opportuni per il medesimo) ma non si potr` vederlo con ls (mentre per crearlo occorrer` anche il permesso di scrittura per la a a directory). Avere il permesso di lettura per un le consente di aprirlo con le opzioni (si veda quanto riportato in tab. 6.2) di sola lettura o di lettura/scrittura e leggerne il contenuto. Avere il permesso di scrittura consente di aprire un le in sola scrittura o lettura/scrittura e modicarne il contenuto, lo stesso permesso ` necessario per poter troncare il le. e Non si pu` creare un le ntanto che non si disponga del permesso di esecuzione e di quello o di scrittura per la directory di destinazione; gli stessi permessi occorrono per cancellare un le da una directory (si ricordi che questo non implica necessariamente la rimozione del contenuto del le dal disco), non ` necessario nessun tipo di permesso per il le stesso (infatti esso non e viene toccato, viene solo modicato il contenuto della directory, rimuovendo la voce che ad esso fa riferimento). Per poter eseguire un le (che sia un programma compilato od uno script di shell, od un altro tipo di le eseguibile riconosciuto dal kernel), occorre avere il permesso di esecuzione, inoltre solo i le regolari possono essere eseguiti. I permessi per un link simbolico sono ignorati, contano quelli del le a cui fa riferimento; per questo in genere il comando ls riporta per un link simbolico tutti i permessi come concessi; utente e gruppo a cui esso appartiene vengono pure ignorati quando il link viene risolto, vengono

118

CAPITOLO 5. FILE E DIRECTORY

controllati solo quando viene richiesta la rimozione del link e questultimo ` in una directory con e lo sticky bit impostato (si veda sez. 5.3.2). La procedura con cui il kernel stabilisce se un processo possiede un certo permesso (di lettura, scrittura o esecuzione) si basa sul confronto fra lutente e il gruppo a cui il le appartiene (i valori di st_uid e st_gid accennati in precedenza) e luser-ID eettivo, il group-ID eettivo e gli eventuali group-ID supplementari del processo.28 Per una spiegazione dettagliata degli identicatori associati ai processi si veda sez. 3.3; normalmente, a parte quanto vedremo in sez. 5.3.2, luser-ID eettivo e il group-ID eettivo corrispondono ai valori delluid e del gid dellutente che ha lanciato il processo, mentre i group-ID supplementari sono quelli dei gruppi cui lutente appartiene. I passi attraverso i quali viene stabilito se il processo possiede il diritto di accesso sono i seguenti: 1. Se luser-ID eettivo del processo ` zero (corrispondente allamministratore) laccesso ` e e sempre garantito senza nessun ulteriore controllo. Per questo motivo root ha piena libert` a di accesso a tutti i le. 2. Se luser-ID eettivo del processo ` uguale alluid del proprietario del le (nel qual caso si e dice che il processo ` proprietario del le) allora: e se il relativo29 bit dei permessi daccesso dellutente ` impostato, laccesso ` consentito e e altrimenti laccesso ` negato e 3. Se il group-ID eettivo del processo o uno dei group-ID supplementari dei processi corrispondono al gid del le allora: se il bit dei permessi daccesso del gruppo ` impostato, laccesso ` consentito, e e altrimenti laccesso ` negato e 4. se il bit dei permessi daccesso per tutti gli altri ` impostato, laccesso ` consentito, e e altrimenti laccesso ` negato. e Si tenga presente che questi passi vengono eseguiti esattamente in questordine. Questo vuol dire che se un processo ` il proprietario di un le, laccesso ` consentito o negato solo sulla base e e dei permessi per lutente; i permessi per il gruppo non vengono neanche controllati. Lo stesso vale se il processo appartiene ad un gruppo appropriato, in questo caso i permessi per tutti gli altri non vengono controllati.

5.3.2

I bit dei permessi speciali

Come si ` accennato (in sez. 5.3.1) nei dodici bit del campo st_mode di stat che vengono e usati per il controllo di accesso oltre ai bit dei permessi veri e propri, ci sono altri tre bit che vengono usati per indicare alcune propriet` speciali dei le. Due di questi sono i bit detti suid a (da set-user-ID bit) e sgid (da set-group-ID bit) che sono identicati dalle costanti S_ISUID e S_ISGID. Come spiegato in dettaglio in sez. 3.2.5, quando si lancia un programma il comportamento normale del kernel ` quello di impostare gli identicatori del gruppo eective del nuovo processo e al valore dei corrispondenti del gruppo real del processo corrente, che normalmente corrispondono a quelli dellutente con cui si ` entrati nel sistema. e
28 in realt` Linux, per quanto riguarda laccesso ai le, utilizza gli identicatori del gruppo lesystem (si ricordi a quanto esposto in sez. 3.3), ma essendo questi del tutto equivalenti ai primi, eccetto il caso in cui si voglia scrivere un server NFS, ignoreremo questa dierenza. 29 per relativo si intende il bit di user-read se il processo vuole accedere in scrittura, quello di user-write per laccesso in scrittura, ecc.

5.3. IL CONTROLLO DI ACCESSO AI FILE

119

Se per` il le del programma (che ovviamente deve essere eseguibile30 ) ha il bit suid imo postato, il kernel assegner` come user-ID eettivo al nuovo processo luid del proprietario del a le al posto delluid del processo originario. Avere il bit sgid impostato ha lo stesso eetto sul group-ID eettivo del processo. I bit suid e sgid vengono usati per permettere agli utenti normali di usare programmi che richiedono privilegi speciali; lesempio classico ` il comando passwd che ha la necessit` di modie a care il le delle password, questultimo ovviamente pu` essere scritto solo dallamministratore, o ma non ` necessario chiamare lamministratore per cambiare la propria password. Infatti il coe mando passwd appartiene a root ma ha il bit suid impostato per cui quando viene lanciato da un utente normale parte con i privilegi di root. Chiaramente avere un processo che ha privilegi superiori a quelli che avrebbe normalmente lutente che lo ha lanciato comporta vari rischi, e questo tipo di programmi devono essere scritti accuratamente per evitare che possano essere usati per guadagnare privilegi non consentiti (largomento ` arontato in dettaglio in sez. 3.3). e La presenza dei bit suid e sgid su un le pu` essere rilevata con il comando ls -l, che o visualizza una lettera s al posto della x in corrispondenza dei permessi di utente o gruppo. La stessa lettera s pu` essere usata nel comando chmod per impostare questi bit. Inne questi bit o possono essere controllati allinterno di st_mode con luso delle due costanti S_ISUID e S_IGID, i cui valori sono riportati in tab. 5.4. Gli stessi bit vengono ad assumere in signicato completamente diverso per le directory, normalmente infatti Linux usa la convenzione di SVr4 per indicare con questi bit luso della semantica BSD nella creazione di nuovi le (si veda sez. 5.3.4 per una spiegazione dettagliata al proposito). Inne Linux utilizza il bit sgid per unulteriore estensione mutuata da SVr4. Il caso in cui un le ha il bit sgid impostato senza che lo sia anche il corrispondente bit di esecuzione viene utilizzato per attivare per quel le il mandatory locking (aronteremo questo argomento in dettaglio pi` avanti, in sez. 11.4.5). u Lultimo dei bit rimanenti, identicato dalla costante S_ISVTX, ` in parte un rimasuglio e delle origini dei sistemi Unix. A quellepoca infatti la memoria virtuale e laccesso ai le erano molto meno sosticati e per ottenere la massima velocit` possibile per i programmi usati pi` a u comunemente si poteva impostare questo bit. Leetto di questo bit era che il segmento di testo del programma (si veda sez. 2.2.2 per i dettagli) veniva scritto nella swap la prima volta che questo veniva lanciato, e vi permaneva no al riavvio della macchina (da questo il nome di sticky bit); essendo la swap un le continuo o una partizione indicizzata direttamente si poteva risparmiare in tempo di caricamento rispetto alla ricerca attraverso la struttura del lesystem. Lo sticky bit ` indicato usando la lettera t al e posto della x nei permessi per gli altri. Ovviamente per evitare che gli utenti potessero intasare la swap solo lamministratore era in grado di impostare questo bit, che venne chiamato anche con il nome di saved text bit, da cui deriva quello della costante. Le attuali implementazioni di memoria virtuale e lesystem rendono sostanzialmente inutile questo procedimento. Bench ormai non venga pi` utilizzato per i le, lo sticky bit ha invece assunto un uso e u importante per le directory;31 in questo caso se tale bit ` impostato un le potr` essere rimosso e a dalla directory soltanto se lutente ha il permesso di scrittura su di essa ed inoltre ` vera una e delle seguenti condizioni: lutente ` proprietario del le e lutente ` proprietario della directory e
per motivi di sicurezza il kernel ignora i bit suid e sgid per gli script eseguibili. lo sticky bit per le directory ` unestensione non denita nello standard POSIX, Linux per` la supporta, cos` e o come BSD e SVr4.
31 30

120 lutente ` lamministratore e

CAPITOLO 5. FILE E DIRECTORY

un classico esempio di directory che ha questo bit impostato ` /tmp, i permessi infatti di solito e sono i seguenti: $ ls -ld /tmp drwxrwxrwt 6 root

root

1024 Aug 10 01:03 /tmp

quindi con lo sticky bit bit impostato. In questo modo qualunque utente nel sistema pu` creare dei o le in questa directory (che, come suggerisce il nome, ` normalmente utilizzata per la creazione e di le temporanei), ma solo lutente che ha creato un certo le potr` cancellarlo o rinominarlo. a In questo modo si evita che un utente possa, pi` o meno consapevolmente, cancellare i le u temporanei creati degli altri utenti.

5.3.3

Le funzioni per la gestione dei permessi dei le

Come visto in sez. 5.3 il controllo di accesso ad un le viene fatto utilizzando luser-ID ed il group-ID eettivo del processo; ci sono casi per` in cui si pu` voler eettuare il controllo con o o luser-ID reale ed il group-ID reale, vale a dire usando i valori di uid e gid relativi allutente che ha lanciato il programma, e che, come accennato in sez. 5.3.2 e spiegato in dettaglio in sez. 3.3, non ` detto siano uguali a quelli eettivi. e Per far questo si pu` usare la funzione access, il cui prototipo `: o e
#include <unistd.h> int access(const char *pathname, int mode) Verica i permessi di accesso. La funzione ritorna 0 se laccesso ` consentito, -1 se laccesso non ` consentito ed in caso di errore; e e nel qual caso la variabile errno assumer` i valori: a EINVAL EACCES EROFS il valore di mode non ` valido. e laccesso al le non ` consentito, o non si ha il permesso di attraversare una delle e directory di pathname. si ` richiesto laccesso in scrittura per un le su un lesystem montato in sola lettura. e

ed inoltre EFAULT, ENAMETOOLONG, ENOENT, ENOTDIR, ELOOP, EIO.

La funzione verica i permessi di accesso, indicati da mode, per il le indicato da pathname. I valori possibili per largomento mode sono esprimibili come combinazione delle costanti numeriche riportate in tab. 5.8 (attraverso un OR binario delle stesse). I primi tre valori implicano anche la verica dellesistenza del le, se si vuole vericare solo questultima si pu` usare F_OK, o o anche direttamente stat. Nel caso in cui pathname si riferisca ad un link simbolico, questo viene seguito ed il controllo ` fatto sul le a cui esso fa riferimento. e La funzione controlla solo i bit dei permessi di accesso, si ricordi che il fatto che una directory abbia permesso di scrittura non signica che ci si possa scrivere come in un le, e il fatto che un le abbia permesso di esecuzione non comporta che contenga un programma eseguibile. La funzione ritorna zero solo se tutte i permessi controllati sono disponibili, in caso contrario (o di errore) ritorna -1.
mode R_OK W_OK X_OK F_OK Signicato Verica il permesso di Verica il permesso di Verica il permesso di Verica lesistenza del

lettura. scrittura. esecuzione. le.

Tabella 5.8: Valori possibile per largomento mode della funzione access.

5.3. IL CONTROLLO DI ACCESSO AI FILE

121

Un esempio tipico per luso di questa funzione ` quello di un processo che sta eseguendo un e programma coi privilegi di un altro utente (ad esempio attraverso luso del suid bit) che vuole controllare se lutente originale ha i permessi per accedere ad un certo le. Per cambiare i permessi di un le il sistema mette ad disposizione due funzioni chmod e fchmod, che operano rispettivamente su un lename e su un le descriptor, i loro prototipi sono:
#include <sys/types.h> #include <sys/stat.h> int chmod(const char *path, mode_t mode) Cambia i permessi del le indicato da path al valore indicato da mode. int fchmod(int fd, mode_t mode) Analoga alla precedente, ma usa il le descriptor fd per indicare il le. Le funzioni restituiscono zero in caso di successo e -1 per un errore, in caso di errore errno pu` o assumere i valori: EPERM EROFS luser-ID eettivo non corrisponde a quello del proprietario del le o non ` zero. e il le ` su un lesystem in sola lettura. e

ed inoltre EIO; chmod restituisce anche EFAULT, ENAMETOOLONG, ENOENT, ENOMEM, ENOTDIR, EACCES, ELOOP; fchmod anche EBADF.

Entrambe le funzioni utilizzano come secondo argomento mode, una variabile dellapposito tipo primitivo mode_t (vedi tab. 1.2) utilizzato per specicare i permessi sui le.
mode S_ISUID S_ISGID S_ISVTX S_IRWXU S_IRUSR S_IWUSR S_IXUSR S_IRWXG S_IRGRP S_IWGRP S_IXGRP S_IRWXO S_IROTH S_IWOTH S_IXOTH Valore 04000 02000 01000 00700 00400 00200 00100 00070 00040 00020 00010 00007 00004 00002 00001 Signicato Set user ID . Set group ID . Sticky bit . Lutente ha tutti i permessi. Lutente ha il permesso di lettura. Lutente ha il permesso di scrittura. Lutente ha il permesso di esecuzione. Il gruppo ha tutti i permessi. Il gruppo ha il permesso di lettura. Il gruppo ha il permesso di scrittura. Il gruppo ha il permesso di esecuzione. Gli altri hanno tutti i permessi. Gli altri hanno il permesso di lettura. Gli altri hanno il permesso di scrittura. Gli altri hanno il permesso di esecuzione.

Tabella 5.9: Valori delle costanti usate per indicare i vari bit di mode utilizzato per impostare i permessi dei le.

Le costanti con cui specicare i singoli bit di mode sono riportate in tab. 5.9. Il valore di mode pu` essere ottenuto combinando fra loro con un OR binario le costanti simboliche relative ai vari o bit, o specicato direttamente, come per lomonimo comando di shell, con un valore numerico (la shell lo vuole in ottale, dato che i bit dei permessi sono divisibili in gruppi di tre), che si pu` o calcolare direttamente usando lo schema si utilizzo dei bit illustrato in g. 5.7. Ad esempio i permessi standard assegnati ai nuovi le (lettura e scrittura per il proprietario, sola lettura per il gruppo e gli altri) sono corrispondenti al valore ottale 0644, un programma invece avrebbe anche il bit di esecuzione attivo, con un valore di 0755, se si volesse attivare il bit suid il valore da fornire sarebbe 4755. Il cambiamento dei permessi di un le eseguito attraverso queste funzioni ha comunque alcune limitazioni, previste per motivi di sicurezza. Luso delle funzioni infatti ` possibile solo se e luser-ID eettivo del processo corrisponde a quello del proprietario del le o dellamministratore, altrimenti esse falliranno con un errore di EPERM. Ma oltre a questa regola generale, di immediata comprensione, esistono delle limitazioni

122

CAPITOLO 5. FILE E DIRECTORY

ulteriori. Per questo motivo, anche se si ` proprietari del le, non tutti i valori possibili di mode e sono permessi o hanno eetto; in particolare accade che: 1. siccome solo lamministratore pu` impostare lo sticky bit, se luser-ID eettivo del processo o non ` zero esso viene automaticamente cancellato (senza notica di errore) qualora sia stato e indicato in mode. 2. per quanto detto in sez. 5.3.4 riguardo la creazione dei nuovi le, si pu` avere il caso in cui il o le creato da un processo ` assegnato ad un gruppo per il quale il processo non ha privilegi. e Per evitare che si possa assegnare il bit sgid ad un le appartenente ad un gruppo per cui non si hanno diritti, questo viene automaticamente cancellato da mode (senza notica di errore) qualora il gruppo del le non corrisponda a quelli associati al processo (la cosa non avviene quando luser-ID eettivo del processo ` zero). e Per alcuni lesystem32 ` inoltre prevista unulteriore misura di sicurezza, volta a scongiurare e labuso dei bit suid e sgid; essa consiste nel cancellare automaticamente questi bit dai permessi di un le qualora un processo che non appartenga allamministratore33 eettui una scrittura. In questo modo anche se un utente malizioso scopre un le suid su cui pu` scrivere, uneventuale o modica comporter` la perdita di questo privilegio. a Le funzioni chmod e fchmod ci permettono di modicare i permessi di un le, resta per` il o problema di quali sono i permessi assegnati quando il le viene creato. Le funzioni dellinterfaccia nativa di Unix, come vedremo in sez. 6.2.1, permettono di indicare esplicitamente i permessi di creazione di un le, ma questo non ` possibile per le funzioni dellinterfaccia standard ANSI C che e non prevede lesistenza di utenti e gruppi, ed inoltre il problema si pone anche per linterfaccia nativa quando i permessi non vengono indicati esplicitamente. In tutti questi casi lunico riferimento possibile ` quello della modalit` di apertura del nuovo e a le (lettura/scrittura o sola lettura), che per` pu` fornire un valore che ` lo stesso per tutti e o o e tre i permessi di sez. 5.3.1 (cio` 666 nel primo caso e 222 nel secondo). Per questo motivo il e sistema associa ad ogni processo34 una maschera di bit, la cosiddetta umask, che viene utilizzata per impedire che alcuni permessi possano essere assegnati ai nuovi le in sede di creazione. I bit indicati nella maschera vengono infatti cancellati dai permessi quando un nuovo le viene creato. La funzione che permette di impostare il valore di questa maschera di controllo ` umask, ed e il suo prototipo `: e
#include <stat.h> mode_t umask(mode_t mask) Imposta la maschera dei permessi dei bit al valore specicato da mask (di cui vengono presi solo i 9 bit meno signicativi). ` La funzione ritorna il precedente valore della maschera. E una delle poche funzioni che non restituisce codici di errore.

In genere si usa questa maschera per impostare un valore predenito che escluda preventivamente alcuni permessi (usualmente quello di scrittura per il gruppo e gli altri, corrispondente ad un valore per mask pari a 022). In questo modo ` possibile cancellare automaticamente i e permessi non voluti. Di norma questo valore viene impostato una volta per tutte al login a 022, e gli utenti non hanno motivi per modicarlo.

5.3.4

La gestione della titolarit` dei le a

Vedremo in sez. 6.2 con quali funzioni si possono creare nuovi le, in tale occasione vedremo che ` possibile specicare in sede di creazione quali permessi applicare ad un le, per` non si pu` e o o
32 33

i lesystem pi` comuni (ext2, ext3, reiserfs) supportano questa caratteristica, che ` mutuata da BSD. u e per la precisione un processo che non dispone della capability CAP_FSETID. 34 ` infatti contenuta nel campo umask della struttura fs_struct, vedi g. 3.2. e

5.3. IL CONTROLLO DI ACCESSO AI FILE

123

indicare a quale utente e gruppo esso deve appartenere. Lo stesso problema si presenta per la creazione di nuove directory (procedimento descritto in sez. 5.1.4). Lo standard POSIX prescrive che luid del nuovo le corrisponda alluser-ID eettivo del processo che lo crea; per il gid invece prevede due diverse possibilit`: a il gid del le corrisponde al group-ID eettivo del processo. il gid del le corrisponde al gid della directory in cui esso ` creato. e in genere BSD usa sempre la seconda possibilit`, che viene per questo chiamata semantica BSD. a Linux invece segue quella che viene chiamata semantica SVr4; di norma cio` il nuovo le viene e creato, seguendo la prima opzione, con il gid del processo, se per` la directory in cui viene creato o il le ha il bit sgid impostato allora viene usata la seconda opzione. Usare la semantica BSD ha il vantaggio che il gid viene sempre automaticamente propagato, restando coerente a quello della directory di partenza, in tutte le sotto-directory. La semantica SVr4 ore la possibilit` di scegliere, ma per ottenere lo stesso risultato di a coerenza che si ha con BSD necessita che per le nuove directory venga anche propagato anche il bit sgid. Questo ` il comportamento predenito del comando mkdir, ed ` in questo modo ad e e esempio che Debian assicura che le sotto-directory create nella home di un utente restino sempre con il gid del gruppo primario dello stesso. Come per i permessi, il sistema fornisce anche delle funzioni che permettano di cambiare utente e gruppo cui il le appartiene; le funzioni in questione sono tre: chown, fchown e lchown, ed i loro prototipi sono:
#include <sys/types.h> #include <sys/stat.h> int chown(const char *path, uid_t owner, gid_t group) int fchown(int fd, uid_t owner, gid_t group) int lchown(const char *path, uid_t owner, gid_t group) Le funzioni cambiano utente e gruppo di appartenenza di un le ai valori specicati dalle variabili owner e group. Le funzioni restituiscono zero in caso di successo e -1 per un errore, in caso di errore errno pu` o assumere i valori: EPERM luser-ID eettivo non corrisponde a quello del proprietario del le o non ` zero, o e utente e gruppo non sono validi

Oltre a questi entrambe restituiscono gli errori EROFS e EIO; chown restituisce anche EFAULT, ENAMETOOLONG, ENOENT, ENOMEM, ENOTDIR, EACCES, ELOOP; fchown anche EBADF.

In Linux soltanto lamministratore (in sostanza un processo con la capability CAP_CHOWN) pu` o cambiare il proprietario di un le, seguendo la semantica di BSD che non consente agli utenti di assegnare i loro le ad altri (per evitare eventuali aggiramenti delle quote). Lamministratore pu` o cambiare il gruppo di un le, il proprietario pu` cambiare il gruppo dei le che gli appartengono o solo se il nuovo gruppo ` il suo gruppo primario o uno dei gruppi di cui fa parte. e La funzione chown segue i link simbolici, per operare direttamente su un link simbolico si deve usare la funzione lchown.35 La funzione fchown opera su un le aperto, essa ` mutuata da e BSD, ma non ` nello standard POSIX. Unaltra estensione rispetto allo standard POSIX ` che e e specicando -1 come valore per owner e group i valori restano immutati. Quando queste funzioni sono chiamate con successo da un processo senza i privilegi di root entrambi i bit suid e sgid vengono cancellati. Questo non avviene per il bit sgid nel caso in cui esso sia usato (in assenza del corrispondente permesso di esecuzione) per indicare che per il le ` attivo il mandatory locking. e
no alla versione 2.1.81 in Linux chown non seguiva i link simbolici, da allora questo comportamento ` stato e assegnato alla funzione lchown, introdotta per loccasione, ed ` stata creata una nuova system call per chown che e seguisse i link simbolici.
35

124

CAPITOLO 5. FILE E DIRECTORY

5.3.5

Un quadro dinsieme sui permessi

Avendo arontato in maniera separata il comportamento delle varie funzioni ed il signicato dei singoli bit dei permessi sui le, vale la pena fare un riepilogo in cui si riassumono le caratteristiche di ciascuno di essi, in modo da poter fornire un quadro dinsieme. In tab. 5.10 si sono riassunti gli eetti dei vari bit dei permessi per un le; per quanto riguarda lapplicazione dei permessi per proprietario, gruppo ed altri si ricordi quanto illustrato in sez. 5.3.1. Per compattezza, nella tabelle si sono specicati i bit di suid, sgid e sticky con la notazione illustrata anche in g. 5.7.
special s t 1 1 - 1 user w 1 group w 1 other w 1 -

s 1 -

r 1 -

x 1 0 1 -

r 1 -

x 1 -

r 1 -

x 1

Operazioni possibili Se eseguito ha i permessi del proprietario. Se eseguito ha i permessi del gruppo proprietario. Il mandatory locking ` abilitato. e Non utilizzato. Permesso di lettura per il proprietario. Permesso di scrittura per il proprietario. Permesso di esecuzione per il proprietario. Permesso di lettura per il gruppo proprietario. Permesso di scrittura per il gruppo proprietario. Permesso di esecuzione per il gruppo proprietario. Permesso di lettura per tutti gli altri. Permesso di scrittura per tutti gli altri. Permesso di esecuzione per tutti gli altri.

Tabella 5.10: Tabella riassuntiva del signicato dei bit dei permessi per un le.

In tab. 5.11 si sono invece riassunti gli eetti dei vari bit dei permessi per una directory; anche in questo caso si sono specicati i bit di suid, sgid e sticky con la notazione compatta illustrata in g. 5.7.
special s t 1 - 1 user w 1 group w 1 other w 1 -

s 1 -

r 1 -

x 1 -

r 1 -

x 1 -

r 1 -

x 1

Operazioni possibili Non utilizzato. Propaga il gruppo proprietario ai nuovi le creati. Limita laccesso in scrittura dei le nella directory. Permesso di visualizzazione per il proprietario. Permesso di aggiornamento per il proprietario. Permesso di attraversamento per il proprietario. Permesso di visualizzazione per il gruppo proprietario. Permesso di aggiornamento per il gruppo proprietario. Permesso di attraversamento per il gruppo proprietario. Permesso di visualizzazione per tutti gli altri. Permesso di aggiornamento per tutti gli altri. Permesso di attraversamento per tutti gli altri.

Tabella 5.11: Tabella riassuntiva del signicato dei bit dei permessi per una directory.

Nelle tabelle si ` indicato con il carattere - il fatto che il valore del bit in questione non e ` inuente rispetto a quanto indicato nella riga della tabella; la descrizione delloperazione fa e riferimento soltanto alla combinazione di bit per i quali ` stato riportato esplicitamente un e valore. Si rammenti inne che il valore dei bit dei permessi non ha alcun eetto qualora il processo possieda i privilegi di amministratore.

` 5.4. CARATTERISTICHE E FUNZIONALITA AVANZATE

125

5.4

Caratteristiche e funzionalit` avanzate a

Tratteremo qui alcune caratteristiche e funzionalit` avanzate della gestione di le e directory, a arontando anche una serie di estensioni dellinterfaccia classica dei sistemi unix-like, principalmente utilizzate a scopi di sicurezza, che sono state introdotte nelle versioni pi` recenti di u Linux.

5.4.1

Gli attributi estesi

Nelle sezioni precedenti abbiamo trattato in dettaglio le varie informazioni che il sistema mantiene negli inode, e le varie funzioni che permettono di modicarle. Si sar` notato come in realt` a a queste informazioni siano estremamente ridotte. Questo ` dovuto al fatto che Unix origina negli e anni 70, quando le risorse di calcolo e di spazio disco erano minime. Con il venir meno di queste restrizioni ` incominciata ad emergere lesigenza di poter associare ai le delle ulteriori informae zioni astratte (quelli che vengono chiamati i meta-dati) che per` non potevano trovar spazio nei o dati classici mantenuti negli inode. Per risolvere questo problema alcuni sistemi unix-like (e fra questi anche Linux) hanno introdotto un meccanismo generico che consenta di associare delle informazioni ai singoli le,36 detto Extended Attributes. Gli attributi estesi non sono altro che delle coppie nome/valore che sono associate permanentemente ad un oggetto sul lesystem, analoghi di quello che sono le variabili di ambiente (vedi sez. 2.3.4) per un processo. Altri sistemi (come Solaris, MacOS e Windows) hanno adottato un meccanismo diverso in cui ad un le sono associati diversi ussi di dati, su cui possono essere mantenute ulteriori informazioni, che possono essere accedute con le normali operazioni di lettura e scrittura. Questi non vanno confusi con gli Extended Attributes (anche se su Solaris hanno lo stesso nome), che sono un meccanismo molto pi` semplice, che pur essendo limitato (potendo contenere solo una u quantit` limitata di informazione) hanno il grande vantaggio di essere molto pi` semplici da a u realizzare e pi` ecienti,37 e di garantire latomicit` di tutte le operazioni. u a In Linux gli attributi estesi sono sempre associati al singolo inode e laccesso viene sempre eseguito in forma atomica, in lettura il valore corrente viene scritto su un buer in memoria, mentre la scrittura prevede che ogni valore precedente sia sovrascritto. Si tenga presente che non tutti i lesystem supportano gli Extended Attributes, in particolare al momento della scrittura di queste dispense essi sono presenti solo su ext2, ext3 e XFS. Inoltre a seconda della implementazione ci possono essere dei limiti sulla quantit` di attributi che si a possono utilizzare.38 Inne lo spazio utilizzato per mantenere gli attributi estesi viene tenuto in conto per il calcolo delle quote di utente e gruppo proprietari del le. Come meccanismo per mantenere informazioni aggiuntive associate al singolo le, gli Extended Attributes possono avere usi anche molto diversi fra loro. Per poterli distinguere allora sono stati suddivisi in classi, a cui poter applicare requisiti diversi per laccesso e la gestione. Per questo motivo il nome di un attributo deve essere sempre specicato nella forma namespace.attribute, dove namespace fa riferimento alla classe a cui lattributo appartiene, mentre attribute ` il e nome ad esso assegnato. In tale forma il nome di un attributo esteso deve essere univoco. Al momento39 sono state denite le quattro classi di attributi riportate in tab. 5.12.
luso pi` comune ` quello della ACL, che tratteremo nella prossima sezione, ma si possono inserire anche altre u e informazioni. 37 cosa molto importante, specie per le applicazioni che richiedono una gran numero di accessi, come le ACL. 38 ad esempio nel caso di ext2 ed ext3 ` richiesto che essi siano contenuti allinterno di un singolo blocco (pertanto e con dimensioni massime pari a 1024, 2048 o 4096 byte a seconda delle dimensioni di questultimo impostate in fase di creazione del lesystem), mentre con XFS non ci sono limiti ed i dati vengono memorizzati in maniera diversa (nellinode stesso, in un blocco a parte, o in una struttura ad albero dedicata) per mantenerne la scalabilit`. a 39 della scrittura di questa sezione, kernel 2.6.23, ottobre 2007.
36

126
Nome security

CAPITOLO 5. FILE E DIRECTORY


Descrizione Gli extended security attributes: vengono utilizzati dalle estensioni di sicurezza del kernel (i Linux Security Modules), per le realizzazione di meccanismi evoluti di controllo di accesso come SELinux. Gli extended security attributes: sono usati dal kernel per memorizzare dati di sistema associati ai le come le ACL (vedi sez. 5.4.2) o le capabilities (vedi sez. 3.3.4). I trusted extended attributes: vengono utilizzati per poter realizzare in user space meccanismi che consentano di mantenere delle informazioni sui le che non devono essere accessibili ai processi ordinari. Gli extended user attributes: utilizzati per mantenere informazioni aggiuntive sui le (come il mime-type, la codica dei caratteri o del le) accessibili dagli utenti.

system

trusted

user

Tabella 5.12: I nomi utilizzati valore di namespace per distinguere le varie classi di Extended Attributes.

Dato che uno degli usi degli Extended Attributes ` quello che li impiega per realizzare delle e estensioni (come le ACL, SELinux, ecc.) al tradizionale meccanismo dei controlli di accesso di Unix, laccesso ai loro valori viene regolato in maniera diversa a seconda sia della loro classe sia di quali, fra le estensioni che li utilizzano, sono poste in uso. In particolare, per ciascuna delle classi riportate in tab. 5.12, si hanno i seguenti casi: security Laccesso agli extended security attributes dipende dalle politiche di sicurezza stabilite da loro stessi tramite lutilizzo di un sistema di controllo basato sui Linux Security Modules (ad esempio SELinux). Pertanto laccesso in lettura o scrittura dipende dalle politiche di sicurezza implementate allinterno dal modulo di sicurezza che si sta utilizzando al momento (ciascuno avr` le sue). Se non ` stato caricato a e nessun modulo di sicurezza laccesso in lettura sar` consentito a tutti i processi, a mentre quello in scrittura solo ai processi con privilegi amministrativi dotati della capability CAP_SYS_ADMIN. Anche laccesso agli extended system attributes dipende dalle politiche di accesso che il kernel realizza anche utilizzando gli stessi valori in essi contenuti. Ad esempio nel caso delle ACL laccesso ` consentito in lettura ai processi che hanno la capacit` e a di eseguire una ricerca sul le (cio` hanno il permesso di lettura sulla directory che e contiene il le) ed in scrittura al proprietario del le o ai processi dotati della capability CAP_FOWNER.40 Laccesso ai trusted extended attributes, sia per la lettura che per la scrittura, ` e consentito soltanto ai processi con privilegi amministrativi dotati della capability CAP_SYS_ADMIN. In questo modo si possono utilizzare questi attributi per realizzare in user space dei meccanismi di controllo che accedono ad informazioni non disponibili ai processi ordinari. Laccesso agli extended user attributes ` regolato dagli ordinari permessi dei le a e cui essi fanno riferimento: occorre avere il permesso di lettura per leggerli e quello di scrittura per scriverli o modicarli. Dato luso di questi attributi, si ` scelto cio` di e e applicare per il loro accesso gli stessi criteri che si usano per laccesso al contenuto dei le (o delle directory) cui essi fanno riferimento. Questa scelta vale per` soltanto per i le e le directory ordinarie, se valesse in o generale infatti si avrebbe un serio problema di sicurezza dato che esistono diversi
40

system

trusted

user

vale a dire una politica di accesso analoga a quella impiegata per gli ordinari permessi dei le.

` 5.4. CARATTERISTICHE E FUNZIONALITA AVANZATE

127

oggetti sul lesystem per i quali ` normale avere avere il permesso di scrittura e consentito a tutti gli utenti, come i link simbolici, o alcuni le di dispositivo come /dev/null. Se fosse possibile usare su di essi gli extended user attributes un utente qualunque potrebbe inserirvi dati a piacere.41 La semantica del controllo di accesso che abbiamo indicato inoltre non avrebbe alcun senso al di fuori di le e directory: i permessi di lettura e scrittura per un le di dispositivo attengono alle capacit` di accesso al dispositivo sottostante,42 mentre a per i link simbolici questi vengono semplicemente ignorati: in nessuno dei due casi hanno a che fare con il contenuto del le, e nella discussione relativa alluso degli extended user attributes nessuno ` mai stato capace di indicare una qualche forma e sensata di utilizzo degli stessi per link simbolici o le di dispositivo, e neanche per le fo o i socket. Per questo motivo gli extended user attributes sono stati completamente disabilitati per tutto ci` che non sia un le regolare o una directory.43 Inoltre per le directory o ` stata introdotta una ulteriore restrizione, dovuta di nuovo alla presenza ordinaria e di permessi di scrittura completi su directory come /tmp. Questo ` un altro caso e particolare, in cui il premesso di scrittura viene usato, unito alla presenza dello sticky bit, per garantire il permesso di creazione di nuovi le. Per questo motivo, per evitare eventuali abusi, se una directory ha lo sticky bit attivo sar` consentito a scrivere i suoi extended user attributes soltanto se si ` proprietari della stessa, o si e hanno i privilegi amministrativi della capability CAP_FOWNER. Le funzioni per la gestione degli attributi estesi, come altre funzioni di gestione avanzate speciche di Linux, non fanno parte delle glibc, e sono fornite da una apposita libreria, libattr, che deve essere installata a parte;44 pertanto se un programma le utilizza si dovr` indicare a esplicitamente luso della suddetta libreria invocando il compilatore con lopzione -lattr. Per poter leggere gli attributi estesi sono disponibili tre diverse funzioni, getxattr, lgetxattr e fgetxattr, che consentono rispettivamente di richiedere gli attributi relativi a un le, a un link simbolico e ad un le descriptor; i rispettivi prototipi sono:
#include <sys/types.h> #include <attr/xattr.h> ssize_t getxattr(const char *path, const char *name, void *value, size_t size) ssize_t lgetxattr(const char *path, const char *name, void *value, size_t size) ssize_t fgetxattr(int filedes, const char *name, void *value, size_t size) Le funzioni leggono il valore di un attributo esteso. Le funzioni restituiscono un intero positivo che indica la dimensione dellattributo richiesto in caso di successo, e 1 in caso di errore, nel qual caso errno assumer` i valori: a ENOATTR ERANGE ENOTSUP lattributo richiesto non esiste. la dimensione size del buer value non ` suciente per contenere il risultato. e gli attributi estesi non sono supportati dal lesystem o sono disabilitati.

Oltre a questi potranno essere restituiti tutti gli errori di stat, ed in particolare EPERM se non si hanno i permessi di accesso allattributo.

Le funzioni getxattr e lgetxattr prendono come primo argomento un pathname che indica il le di cui si vuole richiedere un attributo, la sola dierenza ` che la seconda, se il pathname e
la cosa ` stata notata su XFS, dove questo comportamento permetteva, non essendovi limiti sullo spazio e occupabile dagli Extended Attributes, di bloccare il sistema riempiendo il disco. 42 motivo per cui si pu` formattare un disco anche se /dev ` su un lesystem in sola lettura. o e 43 si pu` vericare la semantica adottata consultando il le fs/xattr.c dei sorgenti del kernel. o 44 la versione corrente della libreria ` libattr1, e nel caso si usi Debian la si pu` installare con il pacchetto e o omonimo ed il collegato libattr1-dev.
41

128

CAPITOLO 5. FILE E DIRECTORY

indica un link simbolico, restituisce gli attributi di questultimo e non quelli del le a cui esso fa riferimento. La funzione fgetxattr prende invece come primo argomento un numero di le descriptor, e richiede gli attributi del le ad esso associato. Tutte e tre le funzioni richiedono di specicare nellargomento name il nome dellattributo di cui si vuole ottenere il valore. Il nome deve essere indicato comprensivo di presso del namespace cui appartiene (uno dei valori di tab. 5.12) nella forma namespace.attributename, come stringa terminata da un carattere NUL. Il suo valore verr` restituito nel buer puntato dallargomento a 45 se questultima non ` suciente si avr` un value per una dimensione massima di size byte; e a errore di ERANGE. Per evitare di dover indovinare la dimensione di un attributo per tentativi si pu` eseguire o una interrogazione utilizzando un valore nullo per size; in questo caso non verr` letto nessun a dato, ma verr` restituito come valore di ritorno della funzione chiamata la dimensione totale a dellattributo esteso richiesto, che si potr` usare come stima per allocare un buer di dimensioni a sucienti.46 Un secondo gruppo di funzioni ` quello che consente di impostare il valore di un attributo e esteso, queste sono setxattr, lsetxattr e fsetxattr, e consentono di operare rispettivamente su un le, su un link simbolico o specicando un le descriptor; i loro prototipi sono:
#include <sys/types.h> #include <attr/xattr.h> int setxattr(const char *path, const char *name, const void *value, size_t size, int flags) int lsetxattr(const char *path, const char *name, const void *value, size_t size, int flags) int fsetxattr(int filedes, const char *name, const void *value, size_t size, int flags) Impostano il valore di un attributo esteso. Le funzioni restituiscono 0 in caso di successo, e 1 in caso di errore, nel qual caso errno assumer` a i valori: ENOATTR EEXIST ENOTSUP si ` usato il ag XATTR_REPLACE e lattributo richiesto non esiste. e si ` usato il ag XATTR_CREATE ma lattributo esiste gi`. e a gli attributi estesi non sono supportati dal lesystem o sono disabilitati.

Oltre a questi potranno essere restituiti tutti gli errori di stat, ed in particolare EPERM se non si hanno i permessi di accesso allattributo.

Le tre funzioni prendono come primo argomento un valore adeguato al loro scopo, usato in maniera del tutto identica a quanto visto in precedenza per le analoghe che leggono gli attributi estesi. Il secondo argomento name deve indicare, anche in questo caso con gli stessi criteri appena visti per le analoghe getxattr, lgetxattr e fgetxattr, il nome (completo di susso) dellattributo su cui si vuole operare. Il valore che verr` assegnato allattributo dovr` essere preparato nel buer puntato da value, a a e la sua dimensione totale (in byte) sar` indicata dallargomento size. Inne largomento flag a consente di controllare le modalit` di sovrascrittura dellattributo esteso, esso pu` prendere a o due valori: con XATTR_REPLACE si richiede che lattributo esista, nel qual caso verr` sovrascritto, a altrimenti si avr` errore, mentre con XATTR_CREATE si richiede che lattributo non esista, nel qual a caso verr` creato, altrimenti si avr` errore ed il valore attuale non sar` modicato. Utilizzando a a a per flag un valore nullo lattributo verr` modicato se ` gi` presente, o creato se non c`. a e a e Le funzioni nora illustrate permettono di leggere o scrivere gli attributi estesi, ma sarebbe
gli attributi estesi possono essere costituiti arbitrariamente da dati testuali o binari. si parla di stima perch anche se le funzioni restituiscono la dimensione esatta dellattributo al momento in e cui sono eseguite, questa potrebbe essere modicata in qualunque momento da un successivo accesso eseguito da un altro processo.
46 45

` 5.4. CARATTERISTICHE E FUNZIONALITA AVANZATE

129

altrettanto utile poter vedere quali sono gli attributi presenti; a questo provvedono le funzioni listxattr, llistxattr e flistxattr i cui prototipi sono:
#include <sys/types.h> #include <attr/xattr.h> ssize_t listxattr(const char *path, char *list, size_t size) ssize_t llistxattr(const char *path, char *list, size_t size) ssize_t flistxattr(int filedes, char *list, size_t size) Leggono la lista degli attributi estesi di un le. Le funzioni restituiscono un intero positivo che indica la dimensione della lista in caso di successo, e 1 in caso di errore, nel qual caso errno assumer` i valori: a ERANGE ENOTSUP la dimensione size del buer value non ` suciente per contenere il risultato. e gli attributi estesi non sono supportati dal lesystem o sono disabilitati.

Oltre a questi potranno essere restituiti tutti gli errori di stat, ed in particolare EPERM se non si hanno i permessi di accesso allattributo.

Come per le precedenti le tre funzioni leggono gli attributi rispettivamente di un le, un link simbolico o specicando un le descriptor, da specicare con il loro primo argomento. Gli altri due argomenti, identici per tutte e tre, indicano rispettivamente il puntatore list al buer dove deve essere letta la lista e la dimensione size di questultimo. La lista viene fornita come sequenza non ordinata dei nomi dei singoli attributi estesi (sempre comprensivi del presso della loro classe) ciascuno dei quali ` terminato da un carattere nullo. e I nomi sono inseriti nel buer uno di seguito allaltro. Il valore di ritorno della funzione indica la dimensione totale della lista in byte. Come per le funzioni di lettura dei singoli attributi se le dimensioni del buer non sono sucienti si avr` un errore, ma ` possibile ottenere dal valore di ritorno della funzione una stima a e della dimensione totale della lista usando per size un valore nullo. Inne per rimuovere semplicemente un attributo esteso, si ha a disposizione un ultimo gruppo di funzioni: removexattr, lremovexattr e fremovexattr; i rispettivi prototipi sono:
#include <sys/types.h> #include <attr/xattr.h> int removexattr(const char *path, const char *name) int lremovexattr(const char *path, const char *name) int fremovexattr(int filedes, const char *name) Rimuovono un attributo esteso di un le. Le funzioni restituiscono 0 in caso di successo, e 1 in caso di errore, nel qual caso errno assumer` a i valori: ENOATTR ENOTSUP lattributo richiesto non esiste. gli attributi estesi non sono supportati dal lesystem o sono disabilitati.

ed inoltre tutti gli errori di stat.

Le tre funzioni rimuovono lattributo esteso indicato dallargomento name rispettivamente di un le, un link simbolico o specicando un le descriptor, da specicare con il loro primo argomento. Anche in questo caso largomento name deve essere specicato con le modalit` gi` a a illustrate in precedenza per le altre funzioni relative agli attributi estesi.

5.4.2

Le Access Control List

Il modello classico dei permessi di Unix, per quanto funzionale ed eciente, ` comunque piute tosto limitato e per quanto possa aver coperto per lunghi anni le esigenze pi` comuni con un u

130

CAPITOLO 5. FILE E DIRECTORY

meccanismo semplice e potente, non ` in grado di rispondere in maniera adeguata a situazioni e che richiedono una gestione complessa dei permessi di accesso.47 Per questo motivo erano state progressivamente introdotte nelle varie versioni di Unix dei meccanismi di gestione dei permessi dei le pi` essibili, nella forma delle cosiddette Access u Control List (indicate usualmente con la sigla ACL). Nello sforzo di standardizzare queste funzionalit` era stato creato un gruppo di lavoro il cui scopo era estendere lo standard POSIX 1003 a attraverso due nuovi insiemi di speciche, la POSIX 1003.1e per linterfaccia di programmazione e la POSIX 1003.2c per i comandi di shell. Gli obiettivi erano per` forse troppo ambizioni, e nel gennaio del 1998 i nanziamenti veno nero ritirati senza che si fosse arrivati alla denizione di uno standard, dato per` che una parte o della documentazione prodotta era di alta qualit` venne deciso di rilasciare al pubblico la dia ciassettesima bozza del documento, quella che va sotto il nome di POSIX 1003.1e Draft 17, che ` divenuta la base sulla quale si deniscono quelle che vanno sotto il nome di Posix ACL. e A dierenza di altri sistemi (ad esempio FreeBSD) nel caso di Linux si ` scelto di realizzare le e ACL attraverso luso degli Extended Attributes (appena trattati in sez. 5.4.1), e fornire tutte le relative funzioni di gestione tramite una libreria, libacl che nasconde i dettagli implementativi delle ACL e presenta ai programmi una interfaccia che fa riferimento allo standard POSIX 1003.1e. Anche in questo caso le funzioni di questa libreria non fanno parte delle glibc e devono essere installate a parte;48 pertanto se un programma le utilizza si dovr` indicare esplicitamente luso a della libreria libacl invocando il compilatore con lopzione -lacl.

5.4.3

La funzione chroot

Bench non abbia niente a che fare con permessi, utenti e gruppi, la funzione chroot viene e usata spesso per restringere le capacit` di accesso di un programma ad una sezione limitata del a lesystem, per cui ne parleremo in questa sezione. Come accennato in sez. 3.2.2 ogni processo oltre ad una directory di lavoro, ha anche una directory radice 49 che, pur essendo di norma corrispondente alla radice dellalbero di le e directory come visto dal kernel (ed illustrato in sez. 4.1.1), ha per il processo il signicato specico di directory rispetto alla quale vengono risolti i pathname assoluti.50 Il fatto che questo valore sia specicato per ogni processo apre allora la possibilit` di modicare le modalit` di a a risoluzione dei pathname assoluti da parte di un processo cambiando questa directory, cos` come si fa coi pathname relativi cambiando la directory di lavoro. Normalmente la directory radice di un processo coincide anche con la radice del lesystem usata dal kernel, e dato che il suo valore viene ereditato dal padre da ogni processo glio, in generale i processi risolvono i pathname assoluti a partire sempre dalla stessa directory, che corrisponde alla radice del sistema. In certe situazioni per`, per motivi di sicurezza, ` utile poter impedire che un processo possa o e accedere a tutto il lesystem; per far questo si pu` cambiare la sua directory radice con la o funzione chroot, il cui prototipo `: e
gi` un requisito come quello di dare accesso in scrittura ad alcune persone ed in sola lettura ad altre non si a pu` soddisfare in maniera soddisfacente. o 48 la versione corrente della libreria ` libacl1, e nel caso si usi Debian la si pu` installare con il pacchetto e o omonimo ed il collegato libacl1-dev. 49 entrambe sono contenute in due campi (rispettivamente pwd e root) di fs_struct; vedi g. 3.2. 50 cio` quando un processo chiede la risoluzione di un pathname, il kernel usa sempre questa directory come e punto di partenza.
47

` 5.4. CARATTERISTICHE E FUNZIONALITA AVANZATE


#include <unistd.h> int chroot(const char *path) Cambia la directory radice del processo a quella specicata da path. La funzione restituisce zero in caso di successo e -1 per un errore, in caso di errore errno pu` o assumere i valori: EPERM luser-ID eettivo del processo non ` zero. e ed inoltre EFAULT, ENAMETOOLONG, ENOENT, ENOMEM, ENOTDIR, EACCES, ELOOP; EROFS e EIO.

131

in questo modo la directory radice del processo diventer` path (che ovviamente deve esistere) a ed ogni pathname assoluto usato dalle funzioni chiamate nel processo sar` risolto a partire da a essa, rendendo impossibile accedere alla parte di albero sovrastante. Si ha cos` quella che viene chiamata una chroot jail, in quanto il processo non pu` pi` accedere a le al di fuori della sezione o u di albero in cui ` stato imprigionato. e Solo un processo con i privilegi di amministratore pu` usare questa funzione, e la nuova radice, o per quanto detto in sez. 3.2.2, sar` ereditata da tutti i suoi processi gli. Si tenga presente per` a o che la funzione non cambia la directory di lavoro, che potrebbe restare fuori dalla chroot jail. Questo ` il motivo per cui la funzione ` ecace solo se dopo averla eseguita si cedono i e e privilegi di root. Infatti se per un qualche motivo il processo resta con la directory di lavoro fuori dalla chroot jail, potr` comunque accedere a tutto il resto del lesystem usando pathname a relativi, i quali, partendo dalla directory di lavoro che ` fuori della chroot jail, potranno (con e luso di ..) risalire no alla radice eettiva del lesystem. Ma se ad un processo restano i privilegi di amministratore esso potr` comunque portare la a sua directory di lavoro fuori dalla chroot jail in cui si trova. Basta infatti creare una nuova chroot jail con luso di chroot su una qualunque directory contenuta nellattuale directory di lavoro. Per questo motivo luso di questa funzione non ha molto senso quando un processo necessita dei privilegi di root per le sue normali operazioni. Un caso tipico di uso di chroot ` quello di un server FTP anonimo, in questo caso infatti e si vuole che il server veda solo i le che deve trasferire, per cui in genere si esegue una chroot sulla directory che contiene i le. Si tenga presente per` che in questo caso occorrer` replicare o a allinterno della chroot jail tutti i le (in genere programmi e librerie) di cui il server potrebbe avere bisogno.

132

CAPITOLO 5. FILE E DIRECTORY

Capitolo 6

I le: linterfaccia standard Unix


Esamineremo in questo capitolo la prima delle due interfacce di programmazione per i le, quella dei le descriptor, nativa di Unix. Questa ` linterfaccia di basso livello provvista direttamente e dalle system call, che non prevede funzionalit` evolute come la buerizzazione o funzioni di a lettura o scrittura formattata, e sulla quale ` costruita anche linterfaccia denita dallo standard e ANSI C che aronteremo al cap. 7.

6.1

Larchitettura di base

In questa sezione faremo una breve introduzione sullarchitettura su cui ` basata dellinterfaccia e dei le descriptor, che, sia pure con dierenze nella realizzazione pratica, resta sostanzialmente la stessa in tutte le implementazione di un sistema unix-like.

6.1.1

Larchitettura dei le descriptor

Per poter accedere al contenuto di un le occorre creare un canale di comunicazione con il kernel che renda possibile operare su di esso (si ricordi quanto visto in sez. 4.2.2). Questo si fa aprendo il le con la funzione open che provveder` a localizzare linode del le e inizializzare i puntatori che a rendono disponibili le funzioni che il VFS mette a disposizione (riportate in tab. 4.2). Una volta terminate le operazioni, il le dovr` essere chiuso, e questo chiuder` il canale di comunicazione a a impedendo ogni ulteriore operazione. Allinterno di ogni processo i le aperti sono identicati da un intero non negativo, chiamato appunto le descriptor. Quando un le viene aperto la funzione open restituisce questo numero, tutte le ulteriori operazioni saranno compiute specicando questo stesso valore come argomento alle varie funzioni dellinterfaccia. Per capire come funziona il meccanismo occorre spiegare a grandi linee come il kernel gestisce linterazione fra processi e le. Il kernel mantiene sempre un elenco dei processi attivi nella cosiddetta process table ed un elenco dei le aperti nella le table. La process table ` una tabella che contiene una voce per ciascun processo attivo nel sistema. e In Linux ciascuna voce ` costituita da una struttura di tipo task_struct nella quale sono e raccolte tutte le informazioni relative al processo; fra queste informazioni c` anche il puntatore e ad una ulteriore struttura di tipo files_struct, in cui sono contenute le informazioni relative ai le che il processo ha aperto, ed in particolare: i ag relativi ai le descriptor. il numero di le aperti. una tabella che contiene un puntatore alla relativa voce nella le table per ogni le aperto. il le descriptor in sostanza ` lintero positivo che indicizza questultima tabella. e 133

134

CAPITOLO 6. I FILE: LINTERFACCIA STANDARD UNIX

La le table ` una tabella che contiene una voce per ciascun le che ` stato aperto nel sistema. e e In Linux ` costituita da strutture di tipo file; in ciascuna di esse sono tenute varie informazioni e relative al le, fra cui: lo stato del le (nel campo f_flags). il valore della posizione corrente (loset) nel le (nel campo f_pos). un puntatore allinode1 del le. In g. 6.1 si ` riportato uno schema in cui ` illustrata questa architettura, ed in cui si sono e e evidenziate le interrelazioni fra le varie strutture di dati sulla quale essa ` basata. Ritorneremo e su questo schema pi` volte, dato che esso ` fondamentale per capire i dettagli del funzionamento u e dellinterfaccia dei le descriptor.

Figura 6.1: Schema della architettura dellaccesso ai le attraverso linterfaccia dei le descriptor.

6.1.2

I le standard

Come accennato i le descriptor non sono altro che un indice nella tabella dei le aperti di ciascun processo; per questo motivo essi vengono assegnati in successione tutte le volte che si apre un nuovo le (se non ne ` stato chiuso nessuno in precedenza). e In tutti i sistemi unix-like esiste una convenzione generale per cui ogni processo viene lanciato dalla shell con almeno tre le aperti. Questi, per quanto appena detto, avranno come le descriptor i valori 0, 1 e 2. Bench questa sia soltanto una convenzione, essa ` seguita dalla gran e e parte delle applicazioni, e non aderirvi potrebbe portare a gravi problemi di interoperabilit`. a Il primo le ` sempre associato al cosiddetto standard input; ` cio` il le da cui il processo si e e e aspetta di ricevere i dati in ingresso. Il secondo le ` il cosiddetto standard output, cio` quello su e e cui ci si aspetta debbano essere inviati i dati in uscita. Il terzo ` lo standard error, su cui viene e inviata luscita relativa agli errori. Nel caso della shell tutti questi le sono associati al terminale
nel kernel 2.4.x si ` in realt` passati ad un puntatore ad una struttura dentry che punta a sua volta allinode e a passando per la nuova struttura del VFS.
1

6.2. LE FUNZIONI BASE

135

di controllo, e corrispondono quindi alla lettura della tastiera per lingresso e alla scrittura sul terminale per luscita. Lo standard POSIX.1 provvede, al posto dei valori numerici, tre costanti simboliche, denite in tab. 6.1.
Costante STDIN_FILENO STDOUT_FILENO STDERR_FILENO Signicato le descriptor dello standard input le descriptor dello standard output le descriptor dello standard error

Tabella 6.1: Costanti denite in unistd.h per i le standard aperti alla creazione di ogni processo.

In g. 6.1 si ` rappresentata una situazione diversa, facendo riferimento ad un programma in e cui lo standard input ` associato ad un le mentre lo standard output e lo standard error sono e entrambi associati ad un altro le (e quindi utilizzano lo stesso inode). Nelle vecchie versioni di Unix (ed anche in Linux no al kernel 2.0.x) il numero di le aperti era anche soggetto ad un limite massimo dato dalle dimensioni del vettore di puntatori con cui era realizzata la tabella dei le descriptor dentro file_struct; questo limite intrinseco nei kernel pi` recenti non sussiste pi`, dato che si ` passati da un vettore ad una lista, ma restano u u e i limiti imposti dallamministratore (vedi sez. 8.1.1).

6.2

Le funzioni base

Linterfaccia standard Unix per linput/output sui le ` basata su cinque funzioni fondamentali: e open, read, write, lseek e close, usate rispettivamente per aprire, leggere, scrivere, spostarsi e chiudere un le. La gran parte delle operazioni sui le si eettua attraverso queste cinque funzioni, esse vengono chiamate anche funzioni di I/O non buerizzato dato che eettuano le operazioni di lettura e scrittura usando direttamente le system call del kernel.

6.2.1

La funzione open

La funzione open ` la funzione fondamentale per accedere ai le, ed ` quella che crea lassociazione e e fra un pathname ed un le descriptor, il suo prototipo `: e
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int open(const char *pathname, int flags) int open(const char *pathname, int flags, mode_t mode) Apre il le indicato da pathname nella modalit` indicata da flags, e, nel caso il le sia a creato, con gli eventuali permessi specicati da mode. La funzione ritorna il le descriptor in caso di successo e 1 in caso di errore. In questo caso la variabile errno assumer` uno dei valori: a EEXIST EISDIR ENOTDIR ENXIO ENODEV ETXTBSY ELOOP pathname esiste e si ` specicato O_CREAT e O_EXCL. e pathname indica una directory e si ` tentato laccesso in scrittura. e si ` specicato O_DIRECTORY e pathname non ` una directory. e e si sono impostati O_NOBLOCK o O_WRONLY ed il le ` una fo che non viene letta da e nessun processo o pathname ` un le di dispositivo ma il dispositivo ` assente. e e pathname si riferisce a un le di dispositivo che non esiste. si ` cercato di accedere in scrittura allimmagine di un programma in esecuzione. e si sono incontrati troppi link simbolici nel risolvere il pathname o si ` indicato e O_NOFOLLOW e pathname ` un link simbolico. e

ed inoltre EACCES, ENAMETOOLONG, ENOENT, EROFS, EFAULT, ENOSPC, ENOMEM, EMFILE e ENFILE.

136

CAPITOLO 6. I FILE: LINTERFACCIA STANDARD UNIX

La funzione apre il le usando il primo le descriptor libero, e crea lopportuna voce, cio` la e struttura file, nella le table del processo. Viene sempre restituito come valore di ritorno il le descriptor con il valore pi` basso disponibile. u
Flag O_RDONLY O_WRONLY O_RDWR O_CREAT O_EXCL O_NONBLOCK O_NOCTTY O_SHLOCK O_EXLOCK O_TRUNC O_NOFOLLOW Descrizione Apre il le in sola lettura, le glibc deniscono anche O_READ come sinonimo. Apre il le in sola scrittura, le glibc deniscono anche O_WRITE come sinonimo. Apre il le sia in lettura che in scrittura. Se il le non esiste verr` creato, con le regole di titolarit` del le viste in sez. 5.3.4. Con a a questa opzione largomento mode deve essere specicato. Usato in congiunzione con O_CREAT fa s` che la precedente esistenza del le diventi un errore2 che fa fallire open con EEXIST. Apre il le in modalit` non bloccante, e comporta che open ritorni immediatamente anche a quando dovrebbe bloccarsi (lopzione ha senso solo per le fo, vedi sez. 12.1.4). Se pathname si riferisce ad un dispositivo di terminale, questo non diventer` il terminale di a controllo, anche se il processo non ne ha ancora uno (si veda sez. 10.1.3). Apre il le con uno shared lock (vedi sez. 11.4). Specica di BSD, assente in Linux. Apre il le con un lock esclusivo (vedi sez. 11.4). Specica di BSD, assente in Linux. Se usato su un le di dati aperto in scrittura, ne tronca la lunghezza a zero; con un terminale o una fo viene ignorato, negli altri casi il comportamento non ` specicato. e Se pathname ` un link simbolico la chiamata fallisce. Questa ` unestensione BSD aggiunta e e in Linux dal kernel 2.1.126. Nelle versioni precedenti i link simbolici sono sempre seguiti, e questa opzione ` ignorata. e Se pathname non ` una directory la chiamata fallisce. Questo ag ` specico di Linux ed ` e e e stato introdotto con il kernel 2.1.126 per evitare dei DoS 3 quando opendir viene chiamata su una fo o su un dispositivo associato ad una unit` a nastri, non deve dispositivo a nastri; a non deve essere utilizzato al di fuori dellimplementazione di opendir. Nel caso di sistemi a 32 bit che supportano le di grandi dimensioni consente di aprire le le cui dimensioni non possono essere rappresentate da numeri a 31 bit. Il le viene aperto in append mode. Prima di ciascuna scrittura la posizione corrente viene sempre impostata alla ne del le. Con NFS si pu` avere una corruzione del le se pi` di un o u processo scrive allo stesso tempo.4 Il le viene aperto in modalit` non bloccante per le operazioni di I/O (che tratteremo in a sez. 11.1.1): questo signica il fallimento di read in assenza di dati da leggere e quello di write in caso di impossibilit` di scrivere immediatamente. Questa modalit` ha senso solo a a per le fo e per alcuni le di dispositivo. In Linux5 ` sinonimo di O_NONBLOCK. e Apre il le per lI/O in modalit` asincrona (vedi sez. 11.2.3). Quando ` impostato viene a e generato il segnale SIGIO tutte le volte che sono disponibili dati in input sul le. Apre il le per linput/output sincrono: ogni write bloccher` no al completamento della a scrittura di tutti i dati sullhardware sottostante. Sinonimo di O_SYNC, usato da BSD. Variante di I/O sincrono denita da POSIX; presente dal kernel 2.1.130 come sinonimo di O_SYNC. Variante analoga alla precedente, trattata allo stesso modo. Blocca laggiornamento dei tempi di accesso dei le (vedi sez. 5.2.4). Per molti lesystem questa funzionalit` non ` disponibile per il singolo le ma come opzione generale da specicare a e in fase di montaggio. Esegue lI/O direttamente dai buer in user space in maniera sincrona, in modo da scavalcare i meccanismi di caching del kernel. In genere questo peggiora le prestazioni tranne quando le applicazioni6 ottimizzano il proprio caching. Per i kernel della serie 2.4 si deve garantire che i buer in user space siano allineati alle dimensioni dei blocchi del lesystem; per il kernel 2.6 basta che siano allineati a multipli di 512 byte. Attiva la modalit` di close-on-exec (vedi sez. 6.3.1 e 6.3.6).7 a Tabella 6.2: Valori e signicato dei vari bit del le status ag.
2

O_DIRECTORY

O_LARGEFILE O_APPEND

O_NONBLOCK

O_NDELAY O_ASYNC O_SYNC O_FSYNC O_DSYNC O_RSYNC O_NOATIME

O_DIRECT

O_CLOEXEC

la pagina di manuale di open segnala che questa opzione ` difettosa su NFS, e che i programmi che la usano e per stabilire un le di lock possono incorrere in una race condition. Si consiglia come alternativa di usare un le con un nome univoco e la funzione link per vericarne lesistenza (vedi sez. 12.3.2).

6.2. LE FUNZIONI BASE

137

Questa caratteristica permette di prevedere qual ` il valore del le descriptor che si otterr` al e a ritorno di open, e viene talvolta usata da alcune applicazioni per sostituire i le corrispondenti ai le standard visti in sez. 6.1.2: se ad esempio si chiude lo standard input e si apre subito dopo un nuovo le questo diventer` il nuovo standard input (avr` cio` il le descriptor 0). a a e Il nuovo le descriptor non ` condiviso con nessun altro processo (torneremo sulla condivisione e dei le, in genere accessibile dopo una fork, in sez. 6.3.1) ed ` impostato per restare aperto e attraverso una exec (come accennato in sez. 3.2.5); loset ` impostato allinizio del le. e Largomento mode indica i permessi con cui il le viene creato; i valori possibili sono gli stessi gi` visti in sez. 5.3.1 e possono essere specicati come OR binario delle costanti descritte in a tab. 5.7. Questi permessi sono ltrati dal valore di umask (vedi sez. 5.3.3) per il processo. La funzione prevede diverse opzioni, che vengono specicate usando vari bit dellargomento flags. Alcuni di questi bit vanno anche a costituire il ag di stato del le (o le status ag), che ` mantenuto nel campo f_flags della struttura file (al solito si veda lo schema di g. 6.1). e Essi sono divisi in tre categorie principali: i bit delle modalit` di accesso: specicano con quale modalit` si acceder` al le: i valori a a a possibili sono lettura, scrittura o lettura/scrittura. Uno di questi bit deve essere sempre specicato quando si apre un le. Vengono impostati alla chiamata da open, e possono essere riletti con fcntl (fanno parte del le status ag), ma non possono essere modicati. i bit delle modalit` di apertura: permettono di specicare alcune delle caratteristiche a del comportamento di open quando viene eseguita. Hanno eetto solo al momento della chiamata della funzione e non sono memorizzati n possono essere riletti. e i bit delle modalit` di operazione: permettono di specicare alcune caratteristiche del a comportamento delle future operazioni sul le (come read o write). Anchessi fan parte del le status ag. Il loro valore ` impostato alla chiamata di open, ma possono essere e riletti e modicati (insieme alle caratteristiche operative che controllano) con una fcntl. In tab. 6.2 sono riportate, ordinate e divise fra loro secondo le tre modalit` appena elencate, le a costanti mnemoniche associate a ciascuno di questi bit. Dette costanti possono essere combinate fra loro con un OR aritmetico per costruire il valore (in forma di maschera binaria) dellargomento flags da passare alla open. I due ag O_NOFOLLOW e O_DIRECTORY sono estensioni speciche di Linux, e deve essere denita la macro _GNU_SOURCE per poterli usare. Nelle prime versioni di Unix i valori di flag specicabili per open erano solo quelli relativi alle modalit` di accesso del le. Per questo motivo per creare un nuovo le cera una system call a apposita, creat, il cui prototipo `: e
#include <fcntl.h> int creat(const char *pathname, mode_t mode) ` Crea un nuovo le vuoto, con i permessi specicati da mode. E del tutto equivalente a open(filedes, O_CREAT|O_WRONLY|O_TRUNC, mode).

adesso questa funzione resta solo per compatibilit` con i vecchi programmi. a
3 acronimo di Denial of Service, si chiamano cos` attacchi miranti ad impedire un servizio causando una qualche forma di carico eccessivo per il sistema, che resta bloccato nelle risposte allattacco. 4 il problema ` che NFS non supporta la scrittura in append, ed il kernel deve simularla, ma questo comporta e la possibilit` di una race condition, vedi sez. 6.3.2. a 5 lopzione origina da SVr4, dove per` causava il ritorno da una read con un valore nullo e non con un errore, o questo introduce unambiguit`, dato che come vedremo in sez. 6.2.4 il ritorno di zero da parte di read ha il a signicato di una end-of-le. 6 lopzione ` stata introdotta dalla SGI in IRIX, e serve sostanzialmente a permettere ad alcuni programmi (in e genere database) la gestione diretta della buerizzazione dellI/O in quanto essi sono in grado di ottimizzarla al meglio per le loro prestazioni; lopzione ` presente anche in FreeBSD, senza limiti di allineamento dei buer. In e Linux ` stata introdotta con il kernel 2.4.10, le versioni precedenti la ignorano. e 7 introdotto con il kernel 2.6.23, per evitare una race condition che si pu` vericare con i thread, fra lapertura o del le e limpostazione della suddetta modalit` con fcntl. a

138

CAPITOLO 6. I FILE: LINTERFACCIA STANDARD UNIX

6.2.2

La funzione close

La funzione close permette di chiudere un le, in questo modo il le descriptor ritorna disponibile; il suo prototipo `: e
#include <unistd.h> int close(int fd) Chiude il descrittore fd. La funzione ritorna 0 in caso di successo e 1 in caso di errore, con errno che assume i valori: EBADF EINTR fd non ` un descrittore valido. e la funzione ` stata interrotta da un segnale. e

ed inoltre EIO.

La chiusura di un le rilascia ogni blocco (il le locking ` trattato in sez. 11.4) che il processo e poteva avere acquisito su di esso; se fd ` lultimo riferimento (di eventuali copie) ad un le e aperto, tutte le risorse nella le table vengono rilasciate. Inne se il le descriptor era lultimo riferimento ad un le su disco questultimo viene cancellato. Si ricordi che quando un processo termina anche tutti i suoi le descriptor vengono chiusi, molti programmi sfruttano questa caratteristica e non usano esplicitamente close. In genere comunque chiudere un le senza controllarne lo stato di uscita ` errore; infatti molti lesystem e implementano la tecnica del write-behind, per cui una write pu` avere successo anche se i dati o non sono stati scritti, un eventuale errore di I/O allora pu` sfuggire, ma verr` riportato alla o a chiusura del le: per questo motivo non eettuare il controllo pu` portare ad una perdita di dati o inavvertita.8 In ogni caso una close andata a buon ne non garantisce che i dati siano stati eettivamente scritti su disco, perch il kernel pu` decidere di ottimizzare laccesso a disco ritardandone la e o scrittura. Luso della funzione sync (vedi sez. 6.3.3) eettua esplicitamente il ush dei dati, ma anche in questo caso resta lincertezza dovuta al comportamento dellhardware (che a sua volta pu` introdurre ottimizzazioni dellaccesso al disco che ritardano la scrittura dei dati, da o cui labitudine di ripetere tre volte il comando prima di eseguire lo shutdown).

6.2.3

La funzione lseek

Come gi` accennato in sez. 6.1.1 a ciascun le aperto ` associata una posizione corrente nel le (il a e cosiddetto le oset, mantenuto nel campo f_pos di file) espressa da un numero intero positivo come numero di byte dallinizio del le. Tutte le operazioni di lettura e scrittura avvengono a partire da questa posizione che viene automaticamente spostata in avanti del numero di byte letti o scritti. In genere (a meno di non avere richiesto la modalit` O_APPEND) questa posizione viene ima ` postata a zero allapertura del le. E possibile impostarla ad un valore qualsiasi con la funzione lseek, il cui prototipo `: e
#include <sys/types.h> #include <unistd.h> off_t lseek(int fd, off_t offset, int whence) Imposta la posizione attuale nel le. La funzione ritorna il valore della posizione corrente in caso di successo e 1 in caso di errore nel qual caso errno assumer` uno dei valori: a ESPIPE EINVAL fd ` una pipe, un socket o una fo. e whence non ` un valore valido. e

ed inoltre EBADF.
8

in Linux questo comportamento ` stato osservato con NFS e le quote su disco. e

6.2. LE FUNZIONI BASE

139

La nuova posizione ` impostata usando il valore specicato da offset, sommato al riferimento e dato da whence; questultimo pu` assumere i seguenti valori9 : o SEEK_SET SEEK_CUR SEEK_END si fa riferimento allinizio del le: il valore (sempre positivo) di offset indica direttamente la nuova posizione corrente. si fa riferimento alla posizione corrente del le: ad essa viene sommato offset (che pu` essere negativo e positivo) per ottenere la nuova posizione corrente. o si fa riferimento alla ne del le: alle dimensioni del le viene sommato offset (che pu` essere negativo e positivo) per ottenere la nuova posizione corrente. o

Come accennato in sez. 5.2.3 con lseek ` possibile impostare la posizione corrente anche e oltre la ne del le, e alla successiva scrittura il le sar` esteso. La chiamata non causa nessun a accesso al le, si limita a modicare la posizione corrente (cio` il valore f_pos in file, vedi e g. 6.1). Dato che la funzione ritorna la nuova posizione, usando il valore zero per offset si pu` o riottenere la posizione corrente nel le chiamando la funzione con lseek(fd, 0, SEEK_CUR). Si tenga presente inoltre che usare SEEK_END non assicura aatto che la successiva scrittura avvenga alla ne del le, infatti se questo ` stato aperto anche da un altro processo che vi e ha scritto, la ne del le pu` essersi spostata, ma noi scriveremo alla posizione impostata in o precedenza (questa ` una potenziale sorgente di race condition, vedi sez. 6.3.2). e Non tutti i le supportano la capacit` di eseguire una lseek, in questo caso la funzione a ritorna lerrore ESPIPE. Questo, oltre che per i tre casi citati nel prototipo, vale anche per tutti quei dispositivi che non supportano questa funzione, come ad esempio per i le di terminale.10 Lo standard POSIX per` non specica niente in proposito. Inne alcuni le speciali, ad esempio o /dev/null, non causano un errore ma restituiscono un valore indenito.

6.2.4

La funzione read

Una volta che un le ` stato aperto (con il permesso in lettura) si possono leggere i dati che e contiene utilizzando la funzione read, il cui prototipo `: e
#include <unistd.h> ssize_t read(int fd, void * buf, size_t count) Cerca di leggere count byte dal le fd al buer buf. La funzione ritorna il numero di byte letti in caso di successo e 1 in caso di errore, nel qual caso errno assumer` uno dei valori: a EINTR EAGAIN la funzione ` stata interrotta da un segnale prima di aver potuto leggere qualsiasi dato. e la funzione non aveva nessun dato da restituire e si era aperto il le in modalit` a O_NONBLOCK.

ed inoltre EBADF, EIO, EISDIR, EBADF, EINVAL e EFAULT ed eventuali altri errori dipendenti dalla natura delloggetto connesso a fd.

La funzione tenta di leggere count byte a partire dalla posizione corrente nel le. Dopo la lettura la posizione sul le ` spostata automaticamente in avanti del numero di byte letti. e Se count ` zero la funzione restituisce zero senza nessun altro risultato. Si deve sempre tener e presente che non ` detto che la funzione read restituisca sempre il numero di byte richiesto, ci e sono infatti varie ragioni per cui la funzione pu` restituire un numero di byte inferiore; questo ` o e un comportamento normale, e non un errore, che bisogna sempre tenere presente. La prima e pi` ovvia di queste ragioni ` che si ` chiesto di leggere pi` byte di quanto il le ne u e e u contenga. In questo caso il le viene letto no alla sua ne, e la funzione ritorna regolarmente il
per compatibilit` con alcune vecchie notazioni questi valori possono essere rimpiazzati rispettivamente con 0, a 1 e 2 o con L_SET, L_INCR e L_XTND. 10 altri sistemi, usando SEEK_SET, in questo caso ritornano il numero di caratteri che vi sono stati scritti.
9

140

CAPITOLO 6. I FILE: LINTERFACCIA STANDARD UNIX

numero di byte letti eettivamente. Raggiunta la ne del le, alla ripetizione di unoperazione di lettura, otterremmo il ritorno immediato di read con uno zero. La condizione di raggiungimento della ne del le non ` un errore, e viene segnalata appunto da un valore di ritorno di read e nullo. Ripetere ulteriormente la lettura non avrebbe nessun eetto se non quello di continuare a ricevere zero come valore di ritorno. Con i le regolari questa ` lunica situazione in cui si pu` avere un numero di byte letti e o inferiore a quello richiesto, ma questo non ` vero quando si legge da un terminale, da una fo o e da una pipe. In tal caso infatti, se non ci sono dati in ingresso, la read si blocca (a meno di non aver selezionato la modalit` non bloccante, vedi sez. 11.1.1) e ritorna solo quando ne arrivano; a se il numero di byte richiesti eccede quelli disponibili la funzione ritorna comunque, ma con un numero di byte inferiore a quelli richiesti. Lo stesso comportamento avviene caso di lettura dalla rete (cio` su un socket, come vedremo e in sez. 16.3.1), o per la lettura da certi le di dispositivo, come le unit` a nastro, che restituiscono a sempre i dati ad un singolo blocco alla volta, o come le linee seriali, che restituiscono solo i dati ricevuti no al momento della lettura. Inne anche le due condizioni segnalate dagli errori EINTR ed EAGAIN non sono propriamente degli errori. La prima si verica quando la read ` bloccata in attesa di dati in ingresso e viene e interrotta da un segnale; in tal caso lazione da intraprendere ` quella di rieseguire la funzione. e Torneremo in dettaglio sullargomento in sez. 9.3.1. La seconda si verica quando il le ` aperto in e modalit` non bloccante (vedi sez. 11.1.1) e non ci sono dati in ingresso: la funzione allora ritorna a immediatamente con un errore EAGAIN11 che indica soltanto che non essendoci al momento dati disponibili occorre provare a ripetere la lettura in un secondo tempo. La funzione read ` una delle system call fondamentali, esistenti n dagli albori di Unix, ma e nella seconda versione delle Single Unix Specication 12 (quello che viene chiamato normalmente Unix98, vedi sez. 1.2.5) ` stata introdotta la denizione di unaltra funzione di lettura, pread, e il cui prototipo `: e
#include <unistd.h> ssize_t pread(int fd, void * buf, size_t count, off_t offset) Cerca di leggere count byte dal le fd, a partire dalla posizione offset, nel buer buf. La funzione ritorna il numero di byte letti in caso di successo e 1 in caso di errore, nel qual caso errno assumer` i valori gi` visti per read e lseek. a a

La funzione prende esattamente gli stessi argomenti di read con lo stesso signicato, a cui si aggiunge largomento offset che indica una posizione sul le. Identico ` il comportamento ed e il valore di ritorno. La funzione serve quando si vogliono leggere dati dal le senza modicare la posizione corrente. Luso di pread ` equivalente allesecuzione di una read seguita da una lseek che riporti al e valore precedente la posizione corrente sul le, ma permette di eseguire loperazione atomicamente. Questo pu` essere importante quando la posizione sul le viene condivisa da processi o diversi (vedi sez. 6.3.1). Il valore di offset fa sempre riferimento allinizio del le. La funzione pread ` disponibile anche in Linux, per` diventa accessibile solo attivando il e o supporto delle estensioni previste dalle Single Unix Specication con la denizione della macro: #define _XOPEN_SOURCE 500 e si ricordi di denire questa macro prima dellinclusione del le di dichiarazioni unistd.h.
in BSD si usa per questo errore la costante EWOULDBLOCK, in Linux, con le glibc, questa ` sinonima di EAGAIN. e questa funzione, e lanaloga pwrite sono state aggiunte nel kernel 2.1.60, il supporto nelle glibc, compresa lemulazione per i vecchi kernel che non hanno la system call, ` stato aggiunto con la versione 2.1, in versioni e precedenti sia del kernel che delle librerie la funzione non ` disponibile. e
12 11

6.3. CARATTERISTICHE AVANZATE

141

6.2.5

La funzione write

Una volta che un le ` stato aperto (con il permesso in scrittura) si pu` scrivere su di esso e o utilizzando la funzione write, il cui prototipo `: e
#include <unistd.h> ssize_t write(int fd, void * buf, size_t count) Scrive count byte dal buer buf sul le fd. La funzione ritorna il numero di byte scritti in caso di successo e 1 in caso di errore, nel qual caso errno assumer` uno dei valori: a EINVAL EFBIG EPIPE fd ` connesso ad un oggetto che non consente la scrittura. e si ` cercato di scrivere oltre la dimensione massima consentita dal lesystem o il limite e per le dimensioni dei le del processo o su una posizione oltre il massimo consentito. fd ` connesso ad una pipe il cui altro capo ` chiuso in lettura; in questo caso viene e e anche generato il segnale SIGPIPE, se questo viene gestito (o bloccato o ignorato) la funzione ritorna questo errore. si ` stati interrotti da un segnale prima di aver potuto scrivere qualsiasi dato. e ci si sarebbe bloccati, ma il le era aperto in modalit` O_NONBLOCK. a

EINTR EAGAIN

ed inoltre EBADF, EIO, EISDIR, EBADF, ENOSPC, EINVAL e EFAULT ed eventuali altri errori dipendenti dalla natura delloggetto connesso a fd.

Come nel caso di read la funzione tenta di scrivere count byte a partire dalla posizione corrente nel le e sposta automaticamente la posizione in avanti del numero di byte scritti. Se il le ` aperto in modalit` O_APPEND i dati vengono sempre scritti alla ne del le. Lo standard e a POSIX richiede che i dati scritti siano immediatamente disponibili ad una read chiamata dopo che la write che li ha scritti ` ritornata; ma dati i meccanismi di caching non ` detto che tutti e e i lesystem supportino questa capacit`. a Se count ` zero la funzione restituisce zero senza fare nientaltro. Per i le ordinari il numero e di byte scritti ` sempre uguale a quello indicato da count, a meno di un errore. Negli altri casi e si ha lo stesso comportamento di read. Anche per write lo standard Unix98 denisce unanaloga pwrite per scrivere alla posizione indicata senza modicare la posizione corrente nel le, il suo prototipo `: e
#include <unistd.h> ssize_t pwrite(int fd, void * buf, size_t count, off_t offset) Cerca di scrivere sul le fd, a partire dalla posizione offset, count byte dal buer buf. La funzione ritorna il numero di byte letti in caso di successo e 1 in caso di errore, nel qual caso errno assumer` i valori gi` visti per write e lseek. a a

e per essa valgono le stesse considerazioni fatte per pread.

6.3

Caratteristiche avanzate

In questa sezione approfondiremo alcune delle caratteristiche pi` sottili della gestione le in un u sistema unix-like, esaminando in dettaglio il comportamento delle funzioni base, inoltre tratteremo le funzioni che permettono di eseguire alcune operazioni avanzate con i le (il grosso dellargomento sar` comunque arontato in cap. 11). a

6.3.1

La condivisione dei les

In sez. 6.1.1 abbiamo descritto brevemente larchitettura dellinterfaccia con i le da parte di un processo, mostrando in g. 6.1 le principali strutture usate dal kernel; esamineremo ora in dettaglio le conseguenze che questa architettura ha nei confronti dellaccesso allo stesso le da parte di processi diversi.

142

CAPITOLO 6. I FILE: LINTERFACCIA STANDARD UNIX

Figura 6.2: Schema dellaccesso allo stesso le da parte di due processi diversi

Il primo caso ` quello in cui due processi diversi aprono lo stesso le su disco; sulla base e di quanto visto in sez. 6.1.1 avremo una situazione come quella illustrata in g. 6.2: ciascun processo avr` una sua voce nella le table referenziata da un diverso le descriptor nella sua a file_struct. Entrambe le voci nella le table faranno per` riferimento allo stesso inode su o disco. Questo signica che ciascun processo avr` la sua posizione corrente sul le, la sua modalit` a a di accesso e versioni proprie di tutte le propriet` che vengono mantenute nella sua voce della le a table. Questo ha conseguenze speciche sugli eetti della possibile azione simultanea sullo stesso le, in particolare occorre tenere presente che: ciascun processo pu` scrivere indipendentemente; dopo ciascuna write la posizione coro rente sar` cambiata solo nel processo. Se la scrittura eccede la dimensione corrente del le a questo verr` esteso automaticamente con laggiornamento del campo i_size nellinode. a se un le ` in modalit` O_APPEND tutte le volte che viene eettuata una scrittura la posizione e a corrente viene prima impostata alla dimensione corrente del le letta dallinode. Dopo la scrittura il le viene automaticamente esteso. leetto di lseek ` solo quello di cambiare il campo f_pos nella struttura file della le e table, non c` nessuna operazione sul le su disco. Quando la si usa per porsi alla ne del e le la posizione viene impostata leggendo la dimensione corrente dallinode. Il secondo caso ` quello in cui due le descriptor di due processi diversi puntino alla stessa e voce nella le table; questo ` ad esempio il caso dei le aperti che vengono ereditati dal processo e glio allesecuzione di una fork (si ricordi quanto detto in sez. 3.2.2). La situazione ` illustrata e

6.3. CARATTERISTICHE AVANZATE

143

Figura 6.3: Schema dellaccesso ai le da parte di un processo glio

in g. 6.3; dato che il processo glio riceve una copia dello spazio di indirizzi del padre, ricever` a anche una copia di file_struct e relativa tabella dei le aperti. In questo modo padre e glio avranno gli stessi le descriptor che faranno riferimento alla stessa voce nella le table, condividendo cos` la posizione corrente sul le. Questo ha le con seguenze descritte a suo tempo in sez. 3.2.2: in caso di scrittura contemporanea la posizione corrente nel le varier` per entrambi i processi (in quanto verr` modicato f_pos che ` lo stesso a a e per entrambi). Si noti inoltre che anche i ag di stato del le (quelli impostati dallargomento flag di open) essendo tenuti nella voce della le table 13 , vengono in questo caso condivisi. Ai le per` sono o associati anche altri ag, dei quali lunico usato al momento ` FD_CLOEXEC, detti le descriptor e ags. Questi ultimi sono tenuti invece in file_struct, e perci` sono specici di ciascun processo o e non vengono modicati dalle azioni degli altri anche in caso di condivisione della stessa voce della le table.

6.3.2

Operazioni atomiche con i le

Come si ` visto in un sistema unix-like ` sempre possibile per pi` processi accedere in conteme e u poranea allo stesso le, e che le operazioni di lettura e scrittura possono essere fatte da ogni processo in maniera autonoma in base ad una posizione corrente nel le che ` locale a ciascuno e di essi. Se dal punto di vista della lettura dei dati questo non comporta nessun problema, quando si andr` a scrivere le operazioni potranno mescolarsi in maniera imprevedibile. Il sistema per` a o fornisce in alcuni casi la possibilit` di eseguire alcune operazioni di scrittura in maniera coora dinata anche senza utilizzare meccanismi di sincronizzazione pi` complessi (come il le locking, u che esamineremo in sez. 11.4).
13

per la precisione nel campo f_flags di file.

144

CAPITOLO 6. I FILE: LINTERFACCIA STANDARD UNIX

Un caso tipico di necessit` di accesso condiviso in scrittura ` quello in cui vari processi devono a e scrivere alla ne di un le (ad esempio un le di log). Come accennato in sez. 6.2.3 impostare la posizione alla ne del le e poi scrivere pu` condurre ad una race condition: infatti pu` succedere o o che un secondo processo scriva alla ne del le fra la lseek e la write; in questo caso, come abbiamo appena visto, il le sar` esteso, ma il nostro primo processo avr` ancora la posizione a a corrente impostata con la lseek che non corrisponde pi` alla ne del le, e la successiva write u sovrascriver` i dati del secondo processo. a Il problema ` che usare due system call in successione non ` unoperazione atomica; il proe e blema ` stato risolto introducendo la modalit` O_APPEND. In questo caso infatti, come abbiamo e a descritto in precedenza, ` il kernel che aggiorna automaticamente la posizione alla ne del le e prima di eettuare la scrittura, e poi estende il le. Tutto questo avviene allinterno di una singola system call (la write) che non essendo interrompibile da un altro processo costituisce unoperazione atomica. Un altro caso tipico in cui ` necessaria latomicit` ` quello in cui si vuole creare un le di e ae lock , bloccandosi se il le esiste. In questo caso la sequenza logica porterebbe a vericare prima lesistenza del le con una stat per poi crearlo con una creat; di nuovo avremmo la possibilit` a di una race condition da parte di un altro processo che crea lo stesso le fra il controllo e la creazione. Per questo motivo sono stati introdotti per open i due ag O_CREAT e O_EXCL. In questo modo loperazione di controllo dellesistenza del le (con relativa uscita dalla funzione con un errore) e creazione in caso di assenza, diventa atomica essendo svolta tutta allinterno di una singola system call (per i dettagli sulluso di questa caratteristica si veda sez. 12.3.2).

6.3.3

Le funzioni sync e fsync

Come accennato in sez. 6.2.2 tutte le operazioni di scrittura sono in genere buerizzate dal kernel, che provvede ad eettuarle in maniera asincrona (ad esempio accorpando gli accessi alla stessa zona del disco) in un secondo tempo rispetto al momento della esecuzione della write. Per questo motivo, quando ` necessaria una sincronizzazione dei dati, il sistema mette a e disposizione delle funzioni che provvedono a forzare lo scarico dei dati dai buer del kernel.14 La prima di queste funzioni ` sync il cui prototipo `: e e
#include <unistd.h> int sync(void) Sincronizza il buer della cache dei le col disco. La funzione ritorna sempre zero.

i vari standard prevedono che la funzione si limiti a far partire le operazioni, ritornando immediatamente; in Linux (dal kernel 1.3.20) invece la funzione aspetta la conclusione delle operazioni di sincronizzazione del kernel. La funzione viene usata dal comando sync quando si vuole forzare esplicitamente lo scarico dei dati su disco, o dal demone di sistema update che esegue lo scarico dei dati ad intervalli di tempo ssi: il valore tradizionale, usato da BSD, per lupdate dei dati ` ogni 30 secondi, ma in e Linux il valore utilizzato ` di 5 secondi; con le nuove versioni15 poi, ` il kernel che si occupa e e direttamente di tutto quanto attraverso il demone interno bdflush, il cui comportamento pu` o essere controllato attraverso il le /proc/sys/vm/bdflush (per il signicato dei valori si pu` o leggere la documentazione allegata al kernel in Documentation/sysctl/vm.txt).
come gi` accennato neanche questo d` la garanzia assoluta che i dati siano integri dopo la chiamata, lhardware a a dei dischi ` in genere dotato di un suo meccanismo interno di ottimizzazione per laccesso al disco che pu` ritardare e o ulteriormente la scrittura eettiva. 15 a partire dal kernel 2.2.8
14

6.3. CARATTERISTICHE AVANZATE

145

Quando si vogliono scaricare soltanto i dati di un le (ad esempio essere sicuri che i dati di un database sono stati registrati su disco) si possono usare le due funzioni fsync e fdatasync, i cui prototipi sono:
#include <unistd.h> int fsync(int fd) Sincronizza dati e metadati del le fd int fdatasync(int fd) Sincronizza i dati del le fd. La funzione ritorna 0 in caso di successo e 1 in caso di errore, nel qual caso errno assume i valori: EINVAL fd ` un le speciale che non supporta la sincronizzazione. e ed inoltre EBADF, EROFS e EIO.

Entrambe le funzioni forzano la sincronizzazione col disco di tutti i dati del le specicato, ed attendono no alla conclusione delle operazioni; fsync forza anche la sincronizzazione dei metadati del le (che riguardano sia le modiche alle tabelle di allocazione dei settori, che gli altri dati contenuti nellinode che si leggono con fstat, come i tempi del le). Si tenga presente che questo non comporta la sincronizzazione della directory che contiene il le (e scrittura della relativa voce su disco) che deve essere eettuata esplicitamente.16

6.3.4

Le funzioni dup e dup2

Abbiamo gi` visto in sez. 6.3.1 come un processo glio condivida gli stessi le descriptor del a padre; ` possibile per` ottenere un comportamento analogo allinterno di uno stesso processo e o duplicando un le descriptor. Per far questo si usa la funzione dup il cui prototipo `: e
#include <unistd.h> int dup(int oldfd) Crea una copia del le descriptor oldfd. La funzione ritorna il nuovo le descriptor in caso di successo e 1 in caso di errore, nel qual caso errno assumer` uno dei valori: a EBADF EMFILE oldfd non ` un le aperto. e si ` raggiunto il numero massimo consentito di le descriptor aperti. e

La funzione ritorna, come open, il primo le descriptor libero. Il le descriptor ` una copia e esatta del precedente ed entrambi possono essere interscambiati nelluso. Per capire meglio il funzionamento della funzione si pu` fare riferimento a g. 6.4: leetto della funzione ` semo e plicemente quello di copiare il valore nella struttura file_struct, cosicch anche il nuovo le e descriptor fa riferimento alla stessa voce nella le table; per questo si dice che il nuovo le descriptor ` duplicato, da cui il nome della funzione. e Si noti che per quanto illustrato in g. 6.4 i le descriptor duplicati condivideranno eventuali lock, le status ag, e posizione corrente. Se ad esempio si esegue una lseek per modicare la posizione su uno dei due le descriptor, essa risulter` modicata anche sullaltro (dato che a quello che viene modicato ` lo stesso campo nella voce della le table a cui entrambi fanno e riferimento). Lunica dierenza fra due le descriptor duplicati ` che ciascuno avr` il suo le e a descriptor ag; a questo proposito va specicato che nel caso di dup il ag di close-on-exec (vedi sez. 3.2.5 e sez. 6.3.6) viene sempre cancellato nella copia. Luso principale di questa funzione ` per la redirezione dellinput e delloutput fra lesecuzione e di una fork e la successiva exec; diventa cos` possibile associare un le (o una pipe) allo standard input o allo standard output (torneremo sullargomento in sez. 12.1.2, quando tratteremo le pipe).
in realt` per il lesystem ext2, quando lo si monta con lopzione sync, il kernel provvede anche alla a sincronizzazione automatica delle voci delle directory.
16

146

CAPITOLO 6. I FILE: LINTERFACCIA STANDARD UNIX

Figura 6.4: Schema dellaccesso ai le duplicati

Per fare questo in genere occorre prima chiudere il le che si vuole sostituire, cosicch il suo le e descriptor possa esser restituito alla chiamata di dup, come primo le descriptor disponibile. Dato che questa ` loperazione pi` comune, ` prevista una diversa versione della funzione, e u e dup2, che permette di specicare esplicitamente qual ` il valore di le descriptor che si vuole e avere come duplicato; il suo prototipo `: e
#include <unistd.h> int dup2(int oldfd, int newfd) Rende newfd una copia del le descriptor oldfd. La funzione ritorna il nuovo le descriptor in caso di successo e 1 in caso di errore, nel qual caso errno assumer` uno dei valori: a EBADF EMFILE oldfd non ` un le aperto o newfd ha un valore fuori dallintervallo consentito per i e le descriptor. si ` raggiunto il numero massimo consentito di le descriptor aperti. e

e qualora il le descriptor newfd sia gi` aperto (come avviene ad esempio nel caso della duplia cazione di uno dei le standard) esso sar` prima chiuso e poi duplicato (cos` che il le duplicato a sar` connesso allo stesso valore per il le descriptor). a La duplicazione dei le descriptor pu` essere eettuata anche usando la funzione di controllo o dei le fcntl (che esamineremo in sez. 6.3.6) con il parametro F_DUPFD. Loperazione ha la sintassi fcntl(oldfd, F_DUPFD, newfd) e se si usa 0 come valore per newfd diventa equivalente a dup. La sola dierenza fra le due funzioni17 ` che dup2 chiude il le descriptor newfd se questo e ` gi` aperto, garantendo che la duplicazione sia eettuata esattamente su di esso, invece fcntl e a restituisce il primo le descriptor libero di valore uguale o maggiore di newfd (e se newfd ` e aperto la duplicazione avverr` su un altro le descriptor). a

6.3.5

Le funzioni openat, mkdirat e ani

Un problema che si pone con luso della funzione open, cos` come per molte altre funzioni che accettano come argomenti dei pathname relativi, ` che, quando un pathname relativo non fa e
17

a parte la sintassi ed i diversi codici di errore.

6.3. CARATTERISTICHE AVANZATE

147

riferimento alla directory di lavoro corrente, ` possibile che alcuni dei suoi componenti vengano e modicati in parallelo alla chiamata a open, e questo lascia aperta la possibilit` di una race a condition. Inoltre come gi` accennato, la directory di lavoro corrente ` una propriet` del singolo proa e a cesso; questo signica che quando si lavora con i thread essa sar` la stessa per tutti, ma esistono a molti casi in cui sarebbe invece utile che ogni singolo thread avesse la sua directory di lavoro. Per risolvere questi problemi, riprendendo una interfaccia gi` presente in Solaris, a anco a delle normali funzioni che operano sui le (come open, mkdir, ecc.) sono state introdotte delle ulteriori funzioni, contraddistinte dal susso at, che permettono che permettano lapertura di un le (o le rispettive altre operazioni) usando un pathname relativo ad una directory specicata.18 Bench queste non siano funzioni standard esse sono disponibili anche su altri Unix19 e sono e state proposte per linclusione nello standard POSIX.1, nelle future revisioni dello stesso. Lidea ` che si apra prima la directory che si vuole usare come base dei pathname relativo, e e si passi il le descriptor alla funzione che user` quella directory come punto di partenza per a la risoluzione.20 Con queste funzioni si possono anche ottenere grossi aumenti di prestazioni quando si devono eseguire operazioni su delle sezioni di albero dei le che prevedono gerarchie molto profonde e grandi quantit` di le e directory, dato che basta eseguire la risoluzione di un a pathname una sola volta (nellapertura della directory) e non per ciascun le che essa contiene. La sintassi generale di queste nuove funzioni ` che esse prendano come primo argomento il le e descriptor della directory da usare come base, mentre gli argomenti successivi restano identici a quelli della corrispondente funzione ordinaria; ad esempio nel caso di openat avremo che essa ` e denita come:
#include <fcntl.h> int openat(int dirfd, const char *pathname, int flags) int openat(int dirfd, const char *pathname, int flags, mode_t mode)) Apre un le usando come directory di lavoro corrente dirfd. la funzione restituisce gli stessi valori e gli stessi codici di errore di open, ed in pi`: u EBADF ENOTDIR dirfd non ` un le descriptor valido. e pathname ` un pathname relativo, ma dirfd fa riferimento ad un le. e

In tab. 6.3 si sono riportate le funzioni introdotte con questa nuova interfaccia, con a anco la corrispondente funzione classica. Tranne che nel caso di faccessat e unlinkat tutti i loro prototipi seguono la convenzione appena vista per openat, in cui agli argomenti della corrispondente funzione classica viene anteposto largomento dirfd.21 Il comportamento delle nuove funzioni ` del tutto analogo a quello delle corrispettive classie che, con la sola eccezione del fatto che se fra i loro argomenti si utilizza un pathname relativo questo sar` risolto rispetto alla directory indicata da dirfd; qualora invece si usi un pathnaa me assoluto dirfd verr` semplicemente ignorato. Inne se per dirfd si usa il valore speciale a AT_FDCWD, la risoluzione sar` eettuata rispetto alla directory di lavoro corrente del processo. a Cos` come il comportamento, anche i valori di ritorno e le condizioni di errore delle nuove funzioni sono gli stessi delle funzioni classiche, agli errori si aggiungono per` quelli dovuti a o
lintroduzione ` avvenuta su proposta dello sviluppatore principale delle glibc Urlich Drepper; le corrispondenti e system call sono state inserite nel kernel uciale a partire dalla versione 2.6.16, in precedenza era disponibile una emulazione che, sia pure con prestazioni inferiori, funzionava facendo ricorso alluso del lesystem proc con lapertura del le attraverso il riferimento a pathname del tipo di /proc/self/fd/dirfd/relative_path. 19 oltre al citato Solaris ne ` prevista linclusione anche in BSD. e 20 in questo modo, anche quando si lavora con i thread, si pu` mantenere anche una directory di lavoro diversa o per ciascuno di essi. 21 non staremo pertanto a riportarli uno per uno.
18

148

CAPITOLO 6. I FILE: LINTERFACCIA STANDARD UNIX


Funzione faccessat fchmodat fchownat fstatat futimesat linkat mkdirat mknodat openat readlinkat renameat symlinkat unlinkat mkfifoat Corrispondente access chmod chown stat utimes link mkdir mknod open readlink rename symlink unlink mkfifo

Tabella 6.3: Corrispondenze fra le nuove funzioni at e le corrispettive funzioni classiche.

valori errati per dirfd; in particolare si avr` un errore di EBADF se esso non ` un le descriptor a e valido, ed un errore di ENOTDIR se esso non fa riferimento ad una directory.22 Come accennato ci sono due eccezioni alla precedente regola, faccessat e unlinkat, che tratteremo esplicitamente. Dette funzioni, oltre a prendere dirfd come primo argomento aggiuntivo, prendono un ulteriore argomento nale flags, utilizzato come maschera binaria. Nel caso di faccessat avremo cio`: e
#include <unistd.h> int faccessat(int dirfd, const char *path, int mode, int flags) Controlla i permessi di accesso. la funzione restituisce gli stessi valori e gli stessi codici di errore di access, ed in pi`: u EBADF ENOTDIR dirfd non ` un le descriptor valido. e pathname ` un pathname relativo, ma dirfd fa riferimento ad un le. e

La funzione esegue lo stesso controllo di accesso eettuabile con access, ma si pu` utilizzare o largomento flags per modicarne il comportamento rispetto a quello ordinario di access; questo infatti pu` essere specicato come maschera binaria dei seguenti valori: o AT_EACCESS se impostato esegue il controllo dei permessi usando luser-ID eettivo invece di quello reale (il comportamento di default, che riprende quello di access). AT_SYMLINK_NOFOLLOW se impostato non esegue la dereferenziazione del link simbolico (il comportamento di default, che riprende quello di access), ma eettua il controllo sui permessi del link simbolico stesso. Nel caso di unlinkat lulteriore argomento flags viene inserito perch detta funzione pu` e o comportarsi sia come analogo di unlink che di rmdir; pertanto il suo prototipo `: e
#include <fcntl.h> int unlinkat(int dirfd, const char *pathname, int flags) Rimuove una voce da una directory. la funzione restituisce gli stessi valori e gli stessi codici di errore di unlink o di rmdir a seconda del valore di flags, ed in pi`: u EBADF ENOTDIR
22

dirfd non ` un le descriptor valido. e pathname ` un pathname relativo, ma dirfd fa riferimento ad un le. e

tranne il caso in cui si sia specicato un pathname assoluto, nel qual caso, come detto, il valore di dirfd sar` a completamente ignorato.

6.3. CARATTERISTICHE AVANZATE

149

Di default il comportamento di unlinkat ` equivalente a quello che avrebbe unlink ape plicata a pathname, fallendo se questo ` una directory, se per` si imposta flags al valore di e o 23 essa si comporter` come rmdir. AT_REMOVEDIR, a

6.3.6

La funzione fcntl

Oltre alle operazioni base esaminate in sez. 6.2 esistono tutta una serie di operazioni ausiliarie che ` possibile eseguire su un le descriptor, che non riguardano la normale lettura e scrittura e di dati, ma la gestione sia delle loro propriet`, che di tutta una serie di ulteriori funzionalit` che a a 24 il kernel pu` mettere a disposizione. o Per queste operazioni di manipolazione e di controllo delle varie propriet` e caratteristiche a di un le descriptor, viene usata la funzione fcntl, il cui prototipo `: e
#include <unistd.h> #include <fcntl.h> int fcntl(int fd, int cmd) int fcntl(int fd, int cmd, long arg) int fcntl(int fd, int cmd, struct flock * lock) Esegue una delle possibili operazioni specicate da cmd sul le fd. La funzione ha valori di ritorno diversi a seconda delloperazione. In caso di errore il valore di ritorno ` sempre 1 ed il codice dellerrore ` restituito nella variabile errno; i codici possibili e e dipendono dal tipo di operazione, lunico valido in generale `: e EBADF fd non ` un le aperto. e

Il primo argomento della funzione ` sempre il numero di le descriptor fd su cui si vuole e operare. Il comportamento di questa funzione, il numero e il tipo degli argomenti, il valore di ritorno e gli eventuali errori sono determinati dal valore dellargomento cmd che in sostanza corrisponde allesecuzione di un determinato comando; in sez. 6.3.4 abbiamo incontrato un esempio delluso di fcntl per la duplicazione dei le descriptor, una lista di tutti i possibili valori per cmd ` riportata di seguito: e F_DUPFD trova il primo le descriptor disponibile di valore maggiore o uguale ad arg e ne fa una copia di fd. Ritorna il nuovo le descriptor in caso di successo e 1 in caso di errore. Gli errori possibili sono EINVAL se arg ` negativo o maggiore del massimo e consentito o EMFILE se il processo ha gi` raggiunto il massimo numero di descrittori a consentito. imposta il valore del le descriptor ag al valore specicato con arg. Al momento lunico bit usato ` quello di close-on-exec, identicato dalla costante FD_CLOEXEC, e che serve a richiedere che il le venga chiuso nella esecuzione di una exec (vedi sez. 3.2.5). Ritorna un valore nullo in caso di successo e 1 in caso di errore. ritorna il valore del le descriptor ag di fd o 1 in caso di errore; se FD_CLOEXEC ` impostato i le descriptor aperti vengono chiusi attraverso una exec altrimenti e (il comportamento predenito) restano aperti. ritorna il valore del le status ag in caso di successo o 1 in caso di errore; permette cio` di rileggere quei bit impostati da open allapertura del le che vengono e memorizzati (quelli riportati nella prima e terza sezione di tab. 6.2).

F_SETFD

F_GETFD

F_GETFL

anche se flags ` una maschera binaria, essendo questo lunico ag disponibile, lo si pu` assegnare direttamente. e o ad esempio si gestiscono con questa funzione varie modalit` di I/O asincrono (vedi sez. 11.2.1) e il le locking a (vedi sez. 11.4).
24

23

150 F_SETFL

CAPITOLO 6. I FILE: LINTERFACCIA STANDARD UNIX imposta il le status ag al valore specicato da arg, ritorna un valore nullo in caso di successo o 1 in caso di errore. Possono essere impostati solo i bit riportati nella terza sezione di tab. 6.2.25 richiede un controllo sul le lock specicato da lock, sovrascrivendo la struttura da esso puntata con il risultato; ritorna un valore nullo in caso di successo o 1 in caso di errore. Questa funzionalit` ` trattata in dettaglio in sez. 11.4.3. ae richiede o rilascia un le lock a seconda di quanto specicato nella struttura puntata da lock. Se il lock ` tenuto da qualcun altro ritorna immediatamente restituendo e 1 e imposta errno a EACCES o EAGAIN, in caso di successo ritorna un valore nullo. Questa funzionalit` ` trattata in dettaglio in sez. 11.4.3. ae identica a F_SETLK eccetto per il fatto che la funzione non ritorna subito ma attende che il blocco sia rilasciato. Se lattesa viene interrotta da un segnale la funzione restituisce 1 e imposta errno a EINTR, in caso di successo ritorna un valore nullo. Questa funzionalit` ` trattata in dettaglio in sez. 11.4.3. ae restituisce il pid del processo o lidenticatore del process group 26 che ` preposto e alla ricezione dei segnali SIGIO e SIGURG per gli eventi associati al le descriptor fd. Nel caso di un process group viene restituito un valore negativo il cui valore assoluto corrisponde allidenticatore del process group. In caso di errore viene restituito 1. imposta, con il valore dellargomento arg, lidenticatore del processo o del process group che ricever` i segnali SIGIO e SIGURG per gli eventi associati al le descriptor a fd, ritorna un valore nullo in caso di successo o 1 in caso di errore. Come per F_GETOWN, per impostare un process group si deve usare per arg un valore negativo, il cui valore assoluto corrisponde allidenticatore del process group. restituisce il valore del segnale inviato quando ci sono dati disponibili in ingresso su un le descriptor aperto ed impostato per lI/O asincrono (si veda sez. 11.2.3). Il valore 0 indica il valore predenito (che ` SIGIO), un valore diverso da zero indica e il segnale richiesto, (che pu` essere anche lo stesso SIGIO). In caso di errore ritorna o 1. imposta il segnale da inviare quando diventa possibile eettuare I/O sul le descriptor in caso di I/O asincrono, ritorna un valore nullo in caso di successo o 1 in caso di errore. Il valore zero indica di usare il segnale predenito, SIGIO. Un altro valore diverso da zero (compreso lo stesso SIGIO) specica il segnale voluto; luso di un valore diverso da zero permette inoltre, se si ` installato il gestore del segnale e come sa_sigaction usando SA_SIGINFO, (vedi sez. 9.4.3), di rendere disponibili al gestore informazioni ulteriori riguardo il le che ha generato il segnale attraverso i valori restituiti in siginfo_t (come vedremo in sez. 11.2.3).27

F_GETLK

F_SETLK

F_SETLKW

F_GETOWN

F_SETOWN

F_GETSIG

F_SETSIG

F_SETLEASE imposta o rimuove un le lease 28 sul le descriptor fd a seconda del valore del terzo argomento, che in questo caso ` un int, ritorna un valore nullo in caso di e successo o 1 in caso di errore. Questa funzionalit` avanzata ` trattata in dettaglio a e in sez. 11.2.2.
la pagina di manuale riporta come impostabili solo O_APPEND, O_NONBLOCK e O_ASYNC. i process group sono (vedi sez. 10.1.2) raggruppamenti di processi usati nel controllo di sessione; a ciascuno di essi ` associato un identicatore (un numero positivo analogo al pid). e 27 i due comandi F_SETSIG e F_GETSIG sono una estensione specica di Linux. 28 questa ` una nuova funzionalit`, specica di Linux, e presente solo a partire dai kernel della serie 2.4.x, in cui e a il processo che detiene un lease su un le riceve una notica qualora un altro processo cerca di eseguire una open o una truncate su di esso.
26 25

6.3. CARATTERISTICHE AVANZATE

151

F_GETLEASE restituisce il tipo di le lease che il processo detiene nei confronti del le descriptor fd o 1 in caso di errore. Con questo comando il terzo argomento pu` essere omesso. o Questa funzionalit` avanzata ` trattata in dettaglio in sez. 11.2.2. a e F_NOTIFY attiva un meccanismo di notica per cui viene riportata al processo chiamante, tramite il segnale SIGIO (o altro segnale specicato con F_SETSIG) ogni modica eseguita o direttamente sulla directory cui fd fa riferimento, o su uno dei le in essa contenuti; ritorna un valore nullo in caso di successo o 1 in caso di errore. Questa funzionalit` avanzata, disponibile dai kernel della serie 2.4.x, ` trattata in a e dettaglio in sez. 11.2.2.

La maggior parte delle funzionalit` di fcntl sono troppo avanzate per poter essere arontate a in tutti i loro aspetti a questo punto; saranno pertanto riprese pi` avanti quando aronteremo u le problematiche ad esse relative. In particolare le tematiche relative allI/O asincrono e ai vari meccanismi di notica saranno trattate in maniera esaustiva in sez. 11.2 mentre quelle relative al le locking saranno esaminate in sez. 11.4). Luso di questa funzione con i socket verr` trattato a in sez. 17.3. Si tenga presente inne che quando si usa la funzione per determinare le modalit` di accesso a con cui ` stato aperto il le (attraverso luso del comando F_GETFL) ` necessario estrarre i bit e e corrispondenti nel le status ag che si ` ottenuto. Infatti la denizione corrente di questultimo e non assegna bit separati alle tre diverse modalit` O_RDONLY, O_WRONLY e O_RDWR.29 Per questo a motivo il valore della modalit` di accesso corrente si ottiene eseguendo un AND binario del valore a di ritorno di fcntl con la maschera O_ACCMODE (anchessa denita in fcntl.h), che estrae i bit di accesso dal le status ag.

6.3.7

La funzione ioctl

Bench il concetto di everything is a le si sia dimostrato molto valido anche per linterazione e con i dispositivi pi` vari, fornendo una interfaccia che permette di interagire con essi tramite u le stesse funzioni usate per i normali le di dati, esisteranno sempre caratteristiche peculiari, speciche dellhardware e della funzionalit` che ciascun dispositivo pu` provvedere, che non a o possono venire comprese in questa interfaccia astratta (un caso tipico ` limpostazione della e velocit` di una porta seriale, o le dimensioni di un framebuer). a Per questo motivo nellarchitettura del sistema ` stata prevista lesistenza di una funzione e apposita, ioctl, con cui poter compiere le operazioni speciche di ogni dispositivo particolare, usando come riferimento il solito le descriptor. Il prototipo di questa funzione `: e
#include <sys/ioctl.h> int ioctl(int fd, int request, ...) Esegue loperazione di controllo specicata da request sul le descriptor fd. La funzione nella maggior parte dei casi ritorna 0, alcune operazioni usano per` il valore di ritorno o per restituire informazioni. In caso di errore viene sempre restituito 1 ed errno assumer` uno a dei valori: ENOTTY EINVAL il le fd non ` associato con un dispositivo, o la richiesta non ` applicabile alloggetto e e a cui fa riferimento fd. gli argomenti request o argp non sono validi.

ed inoltre EBADF e EFAULT.

La funzione serve in sostanza come meccanismo generico per fare tutte quelle operazioni che non rientrano nellinterfaccia ordinaria della gestione dei le e che non ` possibile eettuare con e le funzioni esaminate nora. La funzione richiede che si passi come primo argomento un le descriptor regolarmente aperto, e loperazione da compiere viene selezionata attraverso il valore
29

in Linux queste costanti sono poste rispettivamente ai valori 0, 1 e 2.

152

CAPITOLO 6. I FILE: LINTERFACCIA STANDARD UNIX

dellargomento request. Il terzo argomento dipende dalloperazione prescelta; tradizionalmente ` specicato come char * argp, da intendersi come puntatore ad un area di memoria generica,30 e ma per certe operazioni pu` essere omesso, e per altre ` un semplice intero. o e Normalmente la funzione ritorna zero in caso di successo e 1 in caso di errore, ma per alcune operazione il valore di ritorno, che nel caso viene impostato ad un valore positivo, pu` essere o ` pi` comune comunque restituire i risultati allindirizzo utilizzato come parametro di uscita. E u puntato dal terzo argomento. Data la genericit` dellinterfaccia non ` possibile classicare in maniera sistematica le opea e razioni che si possono gestire con ioctl, un breve elenco di alcuni esempi di esse ` il seguente: e il cambiamento dei font di un terminale. lesecuzione di una traccia audio di un CDROM. i comandi di avanti veloce e riavvolgimento di un nastro. il comando di espulsione di un dispositivo rimovibile. limpostazione della velocit` trasmissione di una linea seriale. a limpostazione della frequenza e della durata dei suoni emessi dallo speaker. limpostazione degli attributi dei le su un lesystem ext2.31

In generale ogni dispositivo ha un suo insieme di operazioni speciche eettuabili attraverso ioctl, tutte queste sono denite nellheader le sys/ioctl.h, e devono essere usate solo sui dispositivi cui fanno riferimento. Infatti anche se in genere i valori di request sono opportunamente dierenziati a seconda del dispositivo32 cos` che la richiesta di operazioni relative ad altri dispositivi usualmente provoca il ritorno della funzione con una condizione di errore, in alcuni casi, relativi a valori assegnati prima che questa dierenziazione diventasse pratica corrente, si potrebbero usare valori validi anche per il dispositivo corrente, con eetti imprevedibili o indesiderati. Data la assoluta specicit` della funzione, il cui comportamento varia da dispositivo a dia spositivo, non ` possibile fare altro che dare una descrizione sommaria delle sue caratteristiche; e torneremo ad esaminare in seguito33 quelle relative ad alcuni casi specici (ad esempio la gestione dei terminali ` eettuata attraverso ioctl in quasi tutte le implementazioni di Unix), qui e riportiamo solo lelenco delle operazioni che sono predenite per qualunque le,34 caratterizzate dal presso FIO: FIOCLEX imposta il ag di close-on-exec sul le, in questo caso, essendo usata come operazione logica, ioctl non richiede un terzo argomento, il cui eventuale valore viene ignorato. cancella il ag di close-on-exec sul le, in questo caso, essendo usata come operazione logica, ioctl non richiede un terzo argomento, il cui eventuale valore viene ignorato. abilita o disabilita la modalit` di I/O asincrono sul le (vedi sez. 11.2.1); il terzo a argomento deve essere un puntatore ad un intero (cio` di tipo const int *) che e contiene un valore logico (un valore nullo disabilita, un valore non nullo abilita).

FIONCLEX

FIOASYNC

allepoca della creazione di questa funzione infatti ancora non era stato introdotto il tipo void. i comandi lsattr e chattr fanno questo con delle ioctl dedicate, usabili solo su questo lesystem e derivati successivi (come ext3). 32 il kernel usa un apposito magic number per distinguere ciascun dispositivo nella denizione delle macro da usare per request, in modo da essere sicuri che essi siano sempre diversi, ed il loro uso per dispositivi diversi causi al pi` un errore. Si veda il capitolo quinto di [5] per una trattazione dettagliata dellargomento. u 33 per luso di ioctl con i socket si veda sez. 17.3. 34 in particolare queste operazioni sono denite nel kernel a livello generale, e vengono sempre interpretate per prime, per cui, come illustrato in [5], eventuali operazioni speciche che usino lo stesso valore verrebbero ignorate.
31

30

6.3. CARATTERISTICHE AVANZATE FIONBIO

153

abilita o disabilita sul le lI/O in modalit` non bloccante; il terzo argomento deve a essere un puntatore ad un intero (cio` di tipo const int *) che contiene un valore e logico (un valore nullo disabilita, un valore non nullo abilita).

FIOSETOWN imposta il processo che ricever` i segnali SIGURG e SIGIO generati sul le; il terzo a argomento deve essere un puntatore ad un intero (cio` di tipo const int *) il cui e valore specica il PID del processo. FIOGETOWN legge il processo che ricever` i segnali SIGURG e SIGIO generati sul le; il terzo a argomento deve essere un puntatore ad un intero (cio` di tipo int *) su cui sar` e a scritto il PID del processo. FIONREAD legge il numero di byte disponibili in lettura sul le descriptor;35 il terzo argomento deve essere un puntatore ad un intero (cio` di tipo int *) su cui sar` restituito il e a valore. restituisce la dimensione corrente di un le o di una directory, mentre se applicata ad un dispositivo fallisce con un errore di ENOTTY; il terzo argomento deve essere un puntatore ad un intero (cio` di tipo int *) su cui sar` restituito il valore. e a

FIOQSIZE

Si noti per` come la gran parte di queste operazioni speciche dei le (per essere precisi o le prime sei dellelenco) siano eettuabili in maniera generica anche tramite luso di fcntl. Le due funzioni infatti sono molto simili e la presenza di questa sovrapposizione ` principalmente e dovuta al fatto che alle origini di Unix i progettisti considerarono che era necessario trattare diversamente rispetto alle operazione di controllo delle modalit` di I/O le e dispositivi usando a 36 oggi non ` pi` cos` ma le due funzioni sono rimaste. fcntl per i primi e ioctl per i secondi; e u

questa operazione ` disponibile solo su alcuni le descriptor, in particolare sui socket (vedi sez. 17.3.3) o sui e le descriptor di epoll (vedi sez. 11.1.4). 36 allepoca tra laltro i dispositivi che usavano ioctl erano sostanzialmente solo i terminali, il che spiega luso comune di ENOTTY come codice di errore.

35

154

CAPITOLO 6. I FILE: LINTERFACCIA STANDARD UNIX

Capitolo 7

I le: linterfaccia standard ANSI C


Esamineremo in questo capitolo linterfaccia standard ANSI C per i le, quella che viene comunemente detta interfaccia degli stream. Dopo una breve sezione introduttiva tratteremo le funzioni base per la gestione dellinput/output, mentre tratteremo le caratteristiche pi` avanzate u dellinterfaccia nellultima sezione.

7.1

Introduzione

Come visto in cap. 6 le operazioni di I/O sui le sono gestibili a basso livello con linterfaccia standard unix, che ricorre direttamente alle system call messe a disposizione dal kernel. Questa interfaccia per` non provvede le funzionalit` previste dallo standard ANSI C, che o a invece sono realizzate attraverso opportune funzioni di libreria, queste, insieme alle altre funzioni denite dallo standard, vengono a costituire il nucleo1 delle glibc.

7.1.1

I le stream

Come pi` volte ribadito, linterfaccia dei le descriptor ` uninterfaccia di basso livello, che u e non provvede nessuna forma di formattazione dei dati e nessuna forma di buerizzazione per ottimizzare le operazioni di I/O. In [1] Stevens descrive una serie di test sullinuenza delle dimensioni del blocco di dati (largomento buf di read e write) nellecienza nelle operazioni di I/O con i le descriptor, evidenziando come le prestazioni ottimali si ottengano a partire da dimensioni del buer dei dati pari a quelle dei blocchi del lesystem (il valore dato dal campo st_blksize di stat), che di norma corrispondono alle dimensioni dei settori sici in cui ` suddiviso il disco. e Se il programmatore non si cura di eettuare le operazioni in blocchi di dimensioni adeguate, le prestazioni sono inferiori. La caratteristica principale dellinterfaccia degli stream ` che essa e provvede da sola alla gestione dei dettagli della buerizzazione e allesecuzione delle operazioni di lettura e scrittura in blocchi di dimensioni appropriate allottenimento della massima ecienza. Per questo motivo linterfaccia viene chiamata anche interfaccia dei le stream, dato che non ` pi` necessario doversi preoccupare dei dettagli della comunicazione con il tipo di hardware e u sottostante (come nel caso della dimensione dei blocchi del lesystem), ed un le pu` essere o sempre considerato come composto da un usso continuo (da cui il nome stream) di dati. A parte i dettagli legati alla gestione delle operazioni di lettura e scrittura (sia per quel che riguarda la buerizzazione, che le formattazioni), i le stream restano del tutto equivalenti ai le descriptor (sui quali sono basati), ed in particolare continua a valere quanto visto in sez. 6.3.1 a proposito dellaccesso condiviso ed in sez. 5.3 per il controllo di accesso.
queste funzioni sono state implementate la prima volta da Ritchie nel 1976 e da allora sono rimaste sostanzialmente immutate.
1

155

156

CAPITOLO 7. I FILE: LINTERFACCIA STANDARD ANSI C

7.1.2

Gli oggetti FILE

Per ragioni storiche la struttura di dati che rappresenta uno stream ` stata chiamata FILE, questi e oggetti sono creati dalle funzioni di libreria e contengono tutte le informazioni necessarie a gestire le operazioni sugli stream, come la posizione corrente, lo stato del buer e degli indicatori di stato e di ne del le. Per questo motivo gli utenti non devono mai utilizzare direttamente o allocare queste strutture (che sono dei tipi opachi) ma usare sempre puntatori del tipo FILE * ottenuti dalla libreria stessa (tanto che in certi casi il termine di puntatore a le ` diventato sinonimo di stream). Tutte e le funzioni della libreria che operano sui le accettano come argomenti solo variabili di questo tipo, che diventa accessibile includendo lheader le stdio.h.

7.1.3

Gli stream standard

Ai tre le descriptor standard (vedi sez. 6.1.2) aperti per ogni processo, corrispondono altrettanti stream, che rappresentano i canali standard di input/output prestabiliti; anche questi tre stream sono identicabili attraverso dei nomi simbolici deniti nellheader stdio.h che sono: FILE *stdin Lo standard input cio` lo stream da cui il processo riceve ordinariamente i e dati in ingresso. Normalmente ` associato dalla shell allinput del terminale e e prende i caratteri dalla tastiera. Lo standard output cio` lo stream su cui il processo invia ordinariamente i e dati in uscita. Normalmente ` associato dalla shell alloutput del terminale e e scrive sullo schermo. Lo standard error cio` lo stream su cui il processo ` supposto inviare i mese e saggi di errore. Normalmente anchesso ` associato dalla shell alloutput del e terminale e scrive sullo schermo.

FILE *stdout

FILE *stderr

Nelle glibc stdin, stdout e stderr sono eettivamente tre variabili di tipo FILE * che possono essere usate come tutte le altre, ad esempio si pu` eettuare una redirezione delloutput o di un programma con il semplice codice: fclose ( stdout ); stdout = fopen ( " standard - output - file " , " w " ); ma in altri sistemi queste variabili possono essere denite da macro, e se si hanno problemi di portabilit` e si vuole essere sicuri, diventa opportuno usare la funzione freopen. a

7.1.4

Le modalit` di buerizzazione a

La buerizzazione ` una delle caratteristiche principali dellinterfaccia degli stream; lo scopo ` e e quello di ridurre al minimo il numero di system call (read o write) eseguite nelle operazioni di input/output. Questa funzionalit` ` assicurata automaticamente dalla libreria, ma costituisce a e anche uno degli aspetti pi` comunemente fraintesi, in particolare per quello che riguarda laspetto u della scrittura dei dati sul le. I caratteri che vengono scritti su di uno stream normalmente vengono accumulati in un buer e poi trasmessi in blocco2 tutte le volte che il buer viene riempito, in maniera asincrona rispetto alla scrittura. Un comportamento analogo avviene anche in lettura (cio` dal le viene letto un e blocco di dati, anche se ne sono richiesti una quantit` inferiore), ma la cosa ovviamente ha a rilevanza inferiore, dato che i dati letti sono sempre gli stessi. In caso di scrittura invece, quando
2

questa operazione viene usualmente chiamata scaricamento dei dati, dal termine inglese ush.

7.2. FUNZIONI BASE

157

si ha un accesso contemporaneo allo stesso le (ad esempio da parte di un altro processo) si potranno vedere solo le parti eettivamente scritte, e non quelle ancora presenti nel buer. Per lo stesso motivo, in tutte le situazioni in cui si sta facendo dellinput/output interattivo, bisogner` tenere presente le caratteristiche delle operazioni di scaricamento dei dati, poich non a e ` detto che ad una scrittura sullo stream corrisponda una immediata scrittura sul dispositivo e (la cosa ` particolarmente evidente quando con le operazioni di input/output su terminale). e Per rispondere ad esigenze diverse, lo standard denisce tre distinte modalit` in cui pu` a o essere eseguita la buerizzazione, delle quali occorre essere ben consapevoli, specie in caso di lettura e scrittura da dispositivi interattivi: unbuered : in questo caso non c` buerizzazione ed i caratteri vengono trasmessi direttae mente al le non appena possibile (eettuando immediatamente una write). line buered : in questo caso i caratteri vengono normalmente trasmessi al le in blocco ogni volta che viene incontrato un carattere di newline (il carattere ASCII \n). fully buered : in questo caso i caratteri vengono trasmessi da e verso il le in blocchi di dimensione opportuna. Lo standard ANSI C specica inoltre che lo standard output e lo standard input siano aperti in modalit` fully buered quando non fanno riferimento ad un dispositivo interattivo, e che lo a standard error non sia mai aperto in modalit` fully buered. a Linux, come BSD e SVr4, specica il comportamento predenito in maniera ancora pi` u precisa, e cio` impone che lo standard error sia sempre unbuered (in modo che i messaggi di e errore siano mostrati il pi` rapidamente possibile) e che standard input e standard output siano u aperti in modalit` line buered quando sono associati ad un terminale (od altro dispositivo a interattivo) ed in modalit` fully buered altrimenti. a Il comportamento specicato per standard input e standard output vale anche per tutti i nuovi stream aperti da un processo; la selezione comunque avviene automaticamente, e la libreria apre lo stream nella modalit` pi` opportuna a seconda del le o del dispositivo scelto. a u La modalit` line buered ` quella che necessita di maggiori chiarimenti e attenzioni per a e quel che concerne il suo funzionamento. Come gi` accennato nella descrizione, di norma i dati a vengono inviati al kernel alla ricezione di un carattere di a capo (newline); questo non ` vero e in tutti i casi, infatti, dato che le dimensioni del buer usato dalle librerie sono sse, se le si eccedono si pu` avere uno scarico dei dati anche prima che sia stato inviato un carattere di o newline. Un secondo punto da tenere presente, particolarmente quando si ha a che fare con I/O interattivo, ` che quando si eettua una lettura da uno stream che comporta laccesso al kernel3 e viene anche eseguito lo scarico di tutti i buer degli stream in scrittura. In sez. 7.3.2 vedremo come la libreria denisca delle opportune funzioni per controllare le modalit` di buerizzazione e lo scarico dei dati. a

7.2

Funzioni base

Esamineremo in questa sezione le funzioni base dellinterfaccia degli stream, analoghe a quelle di sez. 6.2 per i le descriptor. In particolare vedremo come aprire, leggere, scrivere e cambiare la posizione corrente in uno stream.
3

questo vuol dire che lo stream da cui si legge ` in modalit` unbuered. e a

158

CAPITOLO 7. I FILE: LINTERFACCIA STANDARD ANSI C

7.2.1

Apertura e chiusura di uno stream

Le funzioni che si possono usare per aprire uno stream sono solo tre: fopen, fdopen e freopen,4 i loro prototipi sono:
#include <stdio.h> FILE *fopen(const char *path, const char *mode) Apre il le specicato da path. FILE *fdopen(int fildes, const char *mode) Associa uno stream al le descriptor fildes. FILE *freopen(const char *path, const char *mode, FILE *stream) Apre il le specicato da path associandolo allo stream specicato da stream, se questo ` e gi` aperto prima lo chiude. a Le funzioni ritornano un puntatore valido in caso di successo e NULL in caso di errore, in tal caso errno assumer` il valore ricevuto dalla funzione sottostante di cui ` fallita lesecuzione. a e Gli errori pertanto possono essere quelli di malloc per tutte e tre le funzioni, quelli open per fopen, quelli di fcntl per fdopen e quelli di fopen, fclose e fflush per freopen.

Normalmente la funzione che si usa per aprire uno stream ` fopen, essa apre il le specicato e nella modalit` specicata da mode, che ` una stringa che deve iniziare con almeno uno dei valori a e indicati in tab. 7.1 (sono possibili varie estensioni che vedremo in seguito). Luso pi` comune di freopen ` per redirigere uno dei tre le standard (vedi sez. 7.1.3): il u e le path viene associato a stream e se questo ` uno stream gi` aperto viene preventivamente e a chiuso. Inne fdopen viene usata per associare uno stream ad un le descriptor esistente ottenuto tramite una altra funzione (ad esempio con una open, una dup, o una pipe) e serve quando si vogliono usare gli stream con le come le fo o i socket, che non possono essere aperti con le funzioni delle librerie standard del C.
Valore r r+ w Signicato Il le viene aperto, laccesso viene posto in sola lettura, lo stream ` posizionato allinizio del le. e Il le viene aperto, laccesso viene posto in lettura e scrittura, lo stream ` posizionato allinizio del le. e Il le viene aperto e troncato a lunghezza nulla (o creato se non esiste), laccesso viene posto in sola scrittura, lo stream ` posizionato allinizio del le. e Il le viene aperto e troncato a lunghezza nulla (o creato se non esiste), laccesso viene posto in scrittura e lettura, lo stream ` posizionato allinizio del le. e Il le viene aperto (o creato se non esiste) in append mode, laccesso viene posto in sola scrittura. Il le viene aperto (o creato se non esiste) in append mode, laccesso viene posto in lettura e scrittura. Specica che il le ` binario, non ha alcun eetto. e Lapertura fallisce se il le esiste gi`. a

w+

a a+ b x

Tabella 7.1: Modalit` di apertura di uno stream dello standard ANSI C che sono sempre presenti in qualunque a sistema POSIX.

In realt` lo standard ANSI C prevede un totale di 15 possibili valori diversi per mode, ma in a tab. 7.1 si sono riportati solo i sei valori eettivi, ad essi pu` essere aggiunto pure il carattere b o (come ultimo carattere o nel mezzo agli altri per le stringhe di due caratteri) che in altri sistemi operativi serve a distinguere i le binari dai le di testo; in un sistema POSIX questa distinzione non esiste e il valore viene accettato solo per compatibilit`, ma non ha alcun eetto. a
4

fopen e freopen fanno parte dello standard ANSI C, fdopen ` parte dello standard POSIX.1. e

7.2. FUNZIONI BASE

159

Le glibc supportano alcune estensioni, queste devono essere sempre indicate dopo aver specicato il mode con uno dei valori di tab. 7.1. Luso del carattere x serve per evitare di sovrascrivere un le gi` esistente (` analoga alluso dellopzione O_EXCL in open), se il le specicato gi` esiste a e a e si aggiunge questo carattere a mode la fopen fallisce. Unaltra estensione serve a supportare la localizzazione, quando si aggiunge a mode una stringa della forma ",ccs=STRING" il valore STRING ` considerato il nome di una codica dei e caratteri e fopen marca il le per luso dei caratteri estesi e abilita le opportune funzioni di conversione in lettura e scrittura. Nel caso si usi fdopen i valori specicati da mode devono essere compatibili con quelli con cui il le descriptor ` stato aperto. Inoltre i modi w e w+ non troncano il le. La posizione nello e stream viene impostata a quella corrente nel le descriptor, e le variabili di errore e di ne del le (vedi sez. 7.2.2) sono cancellate. Il le non viene duplicato e verr` chiuso alla chiusura dello a stream. I nuovi le saranno creati secondo quanto visto in sez. 5.3.4 ed avranno i permessi di accesso impostati al valore S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH (pari a 0666) modicato secondo il valore di umask per il processo (si veda sez. 5.3.3). In caso di le aperti in lettura e scrittura occorre ricordarsi che c` di mezzo una buerizzae zione; per questo motivo lo standard ANSI C richiede che ci sia unoperazione di posizionamento fra unoperazione di output ed una di input o viceversa (eccetto il caso in cui linput ha incontrato la ne del le), altrimenti una lettura pu` ritornare anche il risultato di scritture precedenti o lultima eettuata. Per questo motivo ` una buona pratica (e talvolta necessario) far seguire ad una scrittura e una delle funzioni fflush, fseek, fsetpos o rewind prima di eseguire una rilettura; viceversa nel caso in cui si voglia fare una scrittura subito dopo aver eseguito una lettura occorre prima usare una delle funzioni fseek, fsetpos o rewind. Anche unoperazione nominalmente nulla come fseek(file, 0, SEEK_CUR) ` suciente a garantire la sincronizzazione. e Una volta aperto lo stream, si pu` cambiare la modalit` di buerizzazione (si veda sez. 7.3.2) o a ntanto che non si ` eettuato alcuna operazione di I/O sul le. e Uno stream viene chiuso con la funzione fclose il cui prototipo `: e
#include <stdio.h> int fclose(FILE *stream) Chiude lo stream stream. Restituisce 0 in caso di successo e EOF in caso di errore, nel qual caso imposta errno a EBADF se il le descriptor indicato da stream non ` valido, o uno dei valori specicati dalla sottostante e funzione che ` fallita (close, write o fflush). e

La funzione eettua lo scarico di tutti i dati presenti nei buer di uscita e scarta tutti i dati in ingresso; se era stato allocato un buer per lo stream questo verr` rilasciato. La funzione a eettua lo scarico solo per i dati presenti nei buer in user space usati dalle glibc; se si vuole essere sicuri che il kernel forzi la scrittura su disco occorrer` eettuare una sync (vedi sez. 6.3.3). a Linux supporta anche una altra funzione, fcloseall, come estensione GNU implementata dalle glibc, accessibile avendo denito _GNU_SOURCE, il suo prototipo `: e
#include <stdio.h> int fcloseall(void) Chiude tutti gli stream. Restituisce 0 se non ci sono errori ed EOF altrimenti.

la funzione esegue lo scarico dei dati buerizzati in uscita e scarta quelli in ingresso, chiudendo tutti i le. Questa funzione ` provvista solo per i casi di emergenza, quando si ` vericato un e e errore ed il programma deve essere abortito, ma si vuole compiere qualche altra operazione dopo aver chiuso i le e prima di uscire (si ricordi quanto visto in sez. 2.1.3).

160

CAPITOLO 7. I FILE: LINTERFACCIA STANDARD ANSI C

7.2.2

Lettura e scrittura su uno stream

Una delle caratteristiche pi` utili dellinterfaccia degli stream ` la ricchezza delle funzioni diu e sponibili per le operazioni di lettura e scrittura sui le. Sono infatti previste ben tre diverse modalit` modalit` di input/output non formattato: a a 1. binario in cui legge/scrive un blocco di dati alla volta, vedi sez. 7.2.3. 2. a caratteri in cui si legge/scrive un carattere alla volta (con la buerizzazione gestita automaticamente dalla libreria), vedi sez. 7.2.4. 3. di linea in cui si legge/scrive una linea alla volta (terminata dal carattere di newline \n), vedi sez. 7.2.5. ed inoltre la modalit` di input/output formattato. a A dierenza dellinterfaccia dei le descriptor, con gli stream il raggiungimento della ne del le ` considerato un errore, e viene noticato come tale dai valori di uscita delle varie funzioni. e Nella maggior parte dei casi questo avviene con la restituzione del valore intero (di tipo int) EOF5 denito anchesso nellheader stdlib.h. Dato che le funzioni dellinterfaccia degli stream sono funzioni di libreria che si appoggiano a delle system call, esse non impostano direttamente la variabile errno, che mantiene il valore impostato dalla system call che ha riportato lerrore. Siccome la condizione di end-of-le ` anchessa segnalata come errore, nasce il problema e di come distinguerla da un errore eettivo; basarsi solo sul valore di ritorno della funzione e controllare il valore di errno infatti non basta, dato che questultimo potrebbe essere stato impostato in una altra occasione, (si veda sez. 8.5.1 per i dettagli del funzionamento di errno). Per questo motivo tutte le implementazioni delle librerie standard mantengono per ogni stream almeno due ag allinterno delloggetto FILE, il ag di end-of-le, che segnala che si ` e raggiunta la ne del le in lettura, e quello di errore, che segnala la presenza di un qualche errore nelle operazioni di input/output; questi due ag possono essere riletti dalle funzioni feof e ferror, i cui prototipi sono:
#include <stdio.h> int feof(FILE *stream) Controlla il ag di end-of-le di stream. int ferror(FILE *stream) Controlla il ag di errore di stream. Entrambe le funzioni ritornano un valore diverso da zero se i relativi ag sono impostati.

si tenga presente comunque che la lettura di questi ag segnala soltanto che c` stato un errore, e o che si ` raggiunta la ne del le in una qualunque operazione sullo stream, il controllo quindi e deve essere eettuato ogni volta che si chiama una funzione di libreria. Entrambi i ag (di errore e di end-of-le) possono essere cancellati usando la funzione clearerr, il cui prototipo `: e
#include <stdio.h> void clearerr(FILE *stream) Cancella i ag di errore ed end-of-le di stream.

in genere si usa questa funzione una volta che si sia identicata e corretta la causa di un errore per evitare di mantenere i ag attivi, cos` da poter rilevare una successiva ulteriore condizione di errore. Di questa funzione esiste una analoga clearerr_unlocked che non esegue il blocco dello stream (vedi sez. 7.3.3).
5

la costante deve essere negativa, le glibc usano -1, altre implementazioni possono avere valori diversi.

7.2. FUNZIONI BASE

161

7.2.3

Input/output binario

La prima modalit` di input/output non formattato ricalca quella della interfaccia dei le dea scriptor, e provvede semplicemente la scrittura e la lettura dei dati da un buer verso un le e viceversa. In generale questa ` la modalit` che si usa quando si ha a che fare con dati non e a formattati. Le due funzioni che si usano per lI/O binario sono fread ed fwrite; i loro prototipi sono:
#include <stdio.h> size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream) size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream) Rispettivamente leggono e scrivono nmemb elementi di dimensione size dal buer ptr al le stream. Entrambe le funzioni ritornano il numero di elementi letti o scritti, in caso di errore o ne del le viene restituito un numero di elementi inferiore al richiesto.

In genere si usano queste funzioni quando si devono trasferire su le blocchi di dati binari in maniera compatta e veloce; un primo caso di uso tipico ` quello in cui si salva un vettore (o un e certo numero dei suoi elementi) con una chiamata del tipo: int WriteVect ( FILE * stream , double * vec , size_t nelem ) { int size , nread ; size = sizeof (* vec ); if ( ( nread = fwrite ( vec , size , nelem , stream )) != nelem ) { perror ( " Write error " ); } return nread ; } in questo caso devono essere specicate le dimensioni di ciascun elemento ed il numero di quelli che si vogliono scrivere. Un secondo caso ` invece quello in cui si vuole trasferire su le una e struttura; si avr` allora una chiamata tipo: a struct histogram { int nbins ; double max , min ; double * bin ; } histo ; int WriteStruct ( FILE * stream , struct histogram * histo ) { if ( fwrite ( histo , sizeof (* histo ) , 1 , stream ) !=1) { perror ( " Write error " ); } return nread ; } in cui si specica la dimensione dellintera struttura ed un solo elemento. In realt` quello che conta nel trasferimento dei dati sono le dimensioni totali, che sono sempre a pari al prodotto size * nelem; la sola dierenza ` che le funzioni non ritornano il numero di e byte scritti, ma il numero di elementi. La funzione fread legge sempre un numero intero di elementi, se incontra la ne del le loggetto letto parzialmente viene scartato (lo stesso avviene in caso di errore). In questo caso la posizione dello stream viene impostata alla ne del le (e non a quella corrispondente alla quantit` di dati letti). a

162

CAPITOLO 7. I FILE: LINTERFACCIA STANDARD ANSI C

In caso di errore (o ne del le per fread) entrambe le funzioni restituiscono il numero di oggetti eettivamente letti o scritti, che sar` inferiore a quello richiesto. Contrariamente a a quanto avviene per i le descriptor, questo segnala una condizione di errore e occorrer` usare a feof e ferror per stabilire la natura del problema. Bench queste funzioni assicurino la massima ecienza per il salvataggio dei dati, i dati mee morizzati attraverso di esse presentano lo svantaggio di dipendere strettamente dalla piattaforma di sviluppo usata ed in genere possono essere riletti senza problemi solo dallo stesso programma che li ha prodotti. Infatti diversi compilatori possono eseguire ottimizzazioni diverse delle strutture dati e alcuni compilatori (come il gcc) possono anche scegliere se ottimizzare loccupazione di spazio, impacchettando pi` strettamente i dati, o la velocit` inserendo opportuni padding per lallineamento u a dei medesimi generando quindi output binari diversi. Inoltre altre incompatibilit` si possono a presentare quando entrano in gioco dierenze di architettura hardware, come la dimensione del bus o la modalit` di ordinamento dei bit o il formato delle variabili in oating point. a Per questo motivo quando si usa linput/output binario occorre sempre prendere le opportune precauzioni (in genere usare un formato di pi` alto livello che permetta di recuperare linformau zione completa), per assicurarsi che versioni diverse del programma siano in grado di rileggere i dati tenendo conto delle eventuali dierenze. Le glibc deniscono altre due funzioni per lI/O binario, fread_unlocked e fwrite_unlocked che evitano il lock implicito dello stream, usato per dalla librerie per la gestione delle applicazioni multi-thread (si veda sez. 7.3.3 per i dettagli), i loro prototipi sono:
#include <stdio.h> size_t fread_unlocked(void *ptr, size_t size, size_t nmemb, FILE *stream) size_t fwrite_unlocked(const void *ptr, size_t size, size_t nmemb, FILE *stream) Le funzioni sono identiche alle analoghe fread e fwrite ma non acquisiscono il lock implicito sullo stream.

entrambe le funzioni sono estensioni GNU previste solo dalle glibc.

7.2.4

Input/output a caratteri

La seconda modalit` di input/output ` quella a caratteri, in cui si trasferisce un carattere alla a e volta. Le funzioni per la lettura a caratteri sono tre, fgetc, getc e getchar, i rispettivi prototipi sono:
#include <stdio.h> int getc(FILE *stream) Legge un byte da stream e lo restituisce come intero. In genere ` implementata come una e macro. int fgetc(FILE *stream) ` Legge un byte da stream e lo restituisce come intero. E sempre una funzione. int getchar(void) Equivalente a getc(stdin). Tutte queste funzioni leggono un byte alla volta, che viene restituito come intero; in caso di errore o ne del le il valore di ritorno ` EOF. e

A parte getchar, che si usa in genere per leggere un carattere da tastiera, le altre due funzioni sono sostanzialmente equivalenti. La dierenza ` che getc ` ottimizzata al massimo e e e normalmente viene implementata con una macro, per cui occorre stare attenti a cosa le si passa come argomento, infatti stream pu` essere valutato pi` volte nellesecuzione, e non viene passato o u in copia con il meccanismo visto in sez. 2.4.1; per questo motivo se si passa unespressione si possono avere eetti indesiderati. Invece fgetc ` assicurata essere sempre una funzione, per questo motivo la sua esecuzione e normalmente ` pi` lenta per via delloverhead della chiamata, ma ` altres` possibile ricavarne e u e

7.2. FUNZIONI BASE

163

lindirizzo, che pu` essere passato come argomento ad un altra funzione (e non si hanno i problemi o accennati in precedenza nel tipo di argomento). Le tre funzioni restituiscono tutte un unsigned char convertito ad int (si usa unsigned char in modo da evitare lespansione del segno). In questo modo il valore di ritorno ` sempre e positivo, tranne in caso di errore o ne del le. Nelle estensioni GNU che provvedono la localizzazione sono denite tre funzioni equivalenti alle precedenti, getwc, fgetwc e getwchar, che invece di un carattere di un byte restituiscono un carattere in formato esteso (cio` di tipo wint_t), il loro prototipo `: e e
#include <stdio.h> #include <wchar.h> wint_t getwc(FILE *stream) Legge un carattere esteso da stream. In genere ` implementata come una macro. e wint_t fgetwc(FILE *stream) ` Legge un carattere esteso da stream E una sempre una funzione. wint_t getwchar(void) Equivalente a getwc(stdin). Tutte queste funzioni leggono un carattere alla volta, in caso di errore o ne del le il valore di ritorno ` WEOF. e

Per scrivere un carattere si possono usare tre funzioni, analoghe alle precedenti usate per leggere: putc, fputc e putchar; i loro prototipi sono:
#include <stdio.h> int putc(int c, FILE *stream) Scrive il carattere c su stream. In genere ` implementata come una macro. e int fputc(FILE *stream) ` Scrive il carattere c su stream. E una sempre una funzione. int putchar(void) Equivalente a putc(stdin). Le funzioni scrivono sempre un carattere alla volta, il cui valore viene restituito in caso di successo; in caso di errore o ne del le il valore di ritorno ` EOF. e

Tutte queste funzioni scrivono sempre un byte alla volta, anche se prendono come argomento un int (che pertanto deve essere ottenuto con un cast da un unsigned char). Anche il valore di ritorno ` sempre un intero; in caso di errore o ne del le il valore di ritorno ` EOF. e e Come nel caso dellI/O binario con fread e fwrite le glibc provvedono come estensione, per ciascuna delle funzioni precedenti, unulteriore funzione, il cui nome ` ottenuto aggiungendo e un _unlocked, che esegue esattamente le stesse operazioni, evitando per` il lock implicito dello o stream. Per compatibilit` con SVID sono inoltre provviste anche due funzioni, getw e putw, da usare a per leggere e scrivere una word (cio` due byte in una volta); i loro prototipi sono: e
#include <stdio.h> int getw(FILE *stream) Legge una parola da stream. int putw(int w, FILE *stream) Scrive la parola w su stream. Le funzioni restituiscono la parola w, o EOF in caso di errore o di ne del le.

Le funzioni leggono e scrivono una word di due byte, usando comunque una variabile di tipo int; il loro uso ` deprecato in favore delluso di fread e fwrite, in quanto non ` possibile e e distinguere il valore -1 da una condizione di errore che restituisce EOF. Uno degli usi pi` frequenti dellinput/output a caratteri ` nei programmi di parsing in cui si u e analizza il testo; in questo contesto diventa utile poter analizzare il carattere successivo da uno stream senza estrarlo eettivamente (la tecnica ` detta peeking ahead ) in modo che il programma e possa regolarsi avendo dato una sbirciatina a quello che viene dopo.

164

CAPITOLO 7. I FILE: LINTERFACCIA STANDARD ANSI C

Nel nostro caso questo tipo di comportamento pu` essere realizzato prima leggendo il cao rattere, e poi rimandandolo indietro, cosicch ridiventi disponibile per una lettura successiva; la e funzione che inverte la lettura si chiama ungetc ed il suo prototipo `: e
#include <stdio.h> int ungetc(int c, FILE *stream) Rimanda indietro il carattere c, con un cast a unsigned char, sullo stream stream. La funzione ritorna c in caso di successo e EOF in caso di errore.

bench lo standard ANSI C preveda che loperazione possa essere ripetuta per un numero arbie trario di caratteri, alle implementazioni ` richiesto di garantire solo un livello; questo ` quello e e che fa la glibc, che richiede che avvenga unaltra operazione fra due ungetc successive. Non ` necessario che il carattere che si manda indietro sia lultimo che si ` letto, e non ` e e e necessario neanche avere letto nessun carattere prima di usare ungetc, ma di norma la funzione ` intesa per essere usata per rimandare indietro lultimo carattere letto. e Nel caso c sia un EOF la funzione non fa nulla, e restituisce sempre EOF; cos` si pu` usare o ungetc anche con il risultato di una lettura alla ne del le. Se si ` alla ne del le si pu` comunque rimandare indietro un carattere, il ag di end-of-le e o verr` automaticamente cancellato perch c` un nuovo carattere disponibile che potr` essere a e e a riletto successivamente. Inne si tenga presente che ungetc non altera il contenuto del le, ma opera esclusivamente sul buer interno. Se si esegue una qualunque delle operazioni di riposizionamento (vedi sez. 7.2.7) i caratteri rimandati indietro vengono scartati.

7.2.5

Input/output di linea

La terza ed ultima modalit` di input/output non formattato ` quella di linea, in cui si legge o a e si scrive una riga alla volta; questa ` una modalit` molto usata per lI/O da terminale, ma ` e a e anche quella che presenta le caratteristiche pi` controverse. u Le funzioni previste dallo standard ANSI C per leggere una linea sono sostanzialmente due, gets e fgets, i cui rispettivi prototipi sono:
#include <stdio.h> char *gets(char *string) Scrive su string una linea letta da stdin. char *fgets(char *string, int size, FILE *stream) Scrive su string la linea letta da stream per un massimo di size byte. Le funzioni restituiscono lindirizzo string in caso di successo o NULL in caso di errore.

Entrambe le funzioni eettuano la lettura (dal le specicato fgets, dallo standard input gets) di una linea di caratteri (terminata dal carattere newline, \n, quello mappato sul tasto di ritorno a capo della tastiera), ma gets sostituisce \n con uno zero, mentre fgets aggiunge uno zero dopo il newline, che resta dentro la stringa. Se la lettura incontra la ne del le (o c` e un errore) viene restituito un NULL, ed il buer buf non viene toccato. Luso di gets ` deprecato e e deve essere assolutamente evitato; la funzione infatti non controlla il numero di byte letti, per cui nel caso la stringa letta superi le dimensioni del buer, si avr` un buer overow, con a 6 sovrascrittura della memoria del processo adiacente al buer. Questa ` una delle vulnerabilit` pi` sfruttate per guadagnare accessi non autorizzati al e a u sistema (i cosiddetti exploit), basta infatti inviare una stringa sucientemente lunga ed opportunamente forgiata per sovrascrivere gli indirizzi di ritorno nello stack (supposto che la gets sia stata chiamata da una subroutine), in modo da far ripartire lesecuzione nel codice inviato nella stringa stessa (in genere uno shell code cio` una sezione di programma che lancia una shell). e
6

questa tecnica ` spiegata in dettaglio e con molta ecacia nellormai famoso articolo di Aleph1 [6]. e

7.2. FUNZIONI BASE

165

La funzione fgets non ha i precedenti problemi di gets in quanto prende in input la dimensione del buer size, che non verr` mai ecceduta in lettura. La funzione legge no ad un a massimo di size caratteri (newline compreso), ed aggiunge uno zero di terminazione; questo comporta che la stringa possa essere al massimo di size-1 caratteri. Se la linea eccede la dimensione del buer verranno letti solo size-1 caratteri, ma la stringa sar` sempre terminata a correttamente con uno zero nale; sar` possibile leggere i rimanenti caratteri in una chiamata a successiva. Per la scrittura di una linea lo standard ANSI C prevede altre due funzioni, fputs e puts, analoghe a quelle di lettura, i rispettivi prototipi sono:
#include <stdio.h> int puts(const char *string) Scrive su stdout la linea string. int fputs(const char *string, FILE *stream) Scrive su stream la linea string. Le funzioni restituiscono un valore non negativo in caso di successo o EOF in caso di errore.

Dato che in questo caso si scrivono i dati in uscita puts non ha i problemi di gets ed ` in e genere la forma pi` immediata per scrivere messaggi sullo standard output; la funzione prende u una stringa terminata da uno zero ed aggiunge automaticamente il ritorno a capo. La dierenza con fputs (a parte la possibilit` di specicare un le diverso da stdout) ` che questultima non a e aggiunge il newline, che deve essere previsto esplicitamente. Come per le analoghe funzioni di input/output a caratteri, anche per lI/O di linea esistono delle estensioni per leggere e scrivere linee di caratteri estesi, le funzioni in questione sono fgetws e fputws ed i loro prototipi sono:
#include <wchar.h> wchar_t *fgetws(wchar_t *ws, int n, FILE *stream) Legge un massimo di n caratteri estesi dal le stream al buer ws. int fputws(const wchar_t *ws, FILE *stream) Scrive la linea ws di caratteri estesi sul le stream. Le funzioni ritornano rispettivamente ws o un numero non negativo in caso di successo e NULL o EOF in caso di errore o ne del le.

Il comportamento di queste due funzioni ` identico a quello di fgets e fputs, a parte il fatto e che tutto (numero di caratteri massimo, terminatore della stringa, newline) ` espresso in termini e di caratteri estesi anzich di normali caratteri ASCII. e Come per lI/O binario e quello a caratteri, anche per lI/O di linea le glibc supportano una serie di altre funzioni, estensioni di tutte quelle illustrate nora (eccetto gets e puts), che eseguono esattamente le stesse operazioni delle loro equivalenti, evitando per` il lock implicito o dello stream (vedi sez. 7.3.3). Come per le altre forma di I/O, dette funzioni hanno lo stesso nome della loro analoga normale, con laggiunta dellestensione _unlocked. Come abbiamo visto, le funzioni di lettura per linput/output di linea previste dallo standard ANSI C presentano svariati inconvenienti. Bench fgets non abbia i gravissimi problemi di gets, e pu` comunque dare risultati ambigui se linput contiene degli zeri; questi infatti saranno scritti o sul buer di uscita e la stringa in output apparir` come pi` corta dei byte eettivamente letti. a u Questa ` una condizione che ` sempre possibile controllare (deve essere presente un newline prima e e della eettiva conclusione della stringa presente nel buer), ma a costo di una complicazione ulteriore della logica del programma. Lo stesso dicasi quando si deve gestire il caso di stringa che eccede le dimensioni del buer. Per questo motivo le glibc prevedono, come estensione GNU, due nuove funzioni per la gestione dellinput/output di linea, il cui uso permette di risolvere questi problemi. Luso di queste funzioni deve essere attivato denendo la macro _GNU_SOURCE prima di includere stdio.h.

166

CAPITOLO 7. I FILE: LINTERFACCIA STANDARD ANSI C

La prima delle due, getline, serve per leggere una linea terminata da un newline, esattamente allo stesso modo di fgets, il suo prototipo `: e
#include <stdio.h> ssize_t getline(char **buffer, size_t *n, FILE *stream) Legge una linea dal le stream copiandola sul buer indicato da buffer riallocandolo se necessario (lindirizzo del buer e la sua dimensione vengono sempre riscritte). La funzione ritorna il numero di caratteri letti in caso di successo e -1 in caso di errore o di raggiungimento della ne del le.

La funzione permette di eseguire una lettura senza doversi preoccupare della eventuale lunghezza eccessiva della stringa da leggere. Essa prende come primo argomento lindirizzo del puntatore al buer su cui si vuole copiare la linea. Questultimo deve essere stato allocato in precedenza con una malloc (non si pu` passare lindirizzo di un puntatore ad una variabile locao le); come secondo argomento la funzione vuole lindirizzo della variabile contenente le dimensioni del buer suddetto. Se il buer di destinazione ` sucientemente ampio la stringa viene scritta subito, altrimenti e il buer viene allargato usando realloc e la nuova dimensione ed il nuovo puntatore vengono restituiti indietro (si noti infatti come per entrambi gli argomenti si siano usati dei value result argument, passando dei puntatori anzich i valori delle variabili, secondo la tecnica spiegata in e sez. 2.4.1). Se si passa alla funzione lindirizzo di un puntatore impostato a NULL e *n ` zero, la funzione e provvede da sola allallocazione della memoria necessaria a contenere la linea. In tutti i casi si ottiene dalla funzione un puntatore allinizio del testo della linea letta. Un esempio di codice pu` essere il seguente: o size_t n = 0; char * ptr = NULL ; int nread ; FILE * file ; ... nread = getline (& ptr , &n , file ); e per evitare memory leak occorre ricordarsi di liberare ptr con una free. Il valore di ritorno della funzione indica il numero di caratteri letti dallo stream (quindi compreso il newline, ma non lo zero di terminazione); questo permette anche di distinguere eventuali zeri letti dallo stream da quello inserito dalla funzione per terminare la linea. Se si ` e alla ne del le e non si ` potuto leggere nulla o c` stato un errore la funzione restituisce -1. e e La seconda estensione GNU ` una generalizzazione di getline per poter usare come sepae ratore un carattere qualsiasi, la funzione si chiama getdelim ed il suo prototipo `: e
#include <stdio.h> ssize_t getdelim(char **buffer, size_t *n, int delim, FILE *stream) Identica a getline solo che usa delim al posto del carattere di newline come separatore di linea.

Il comportamento di getdelim ` identico a quello di getline (che pu` essere implementata e o da questa passando \n come valore di delim).

7.2.6

Linput/output formattato

Lultima modalit` di input/output ` quella formattata, che ` una delle caratteristiche pi` utia e e u lizzate delle librerie standard del C; in genere questa ` la modalit` in cui si esegue normalmente e a loutput su terminale poich permette di stampare in maniera facile e veloce dati, tabelle e e messaggi.

7.2. FUNZIONI BASE

167

Loutput formattato viene eseguito con una delle 13 funzioni della famiglia printf; le tre pi` usate sono printf, fprintf e sprintf, i cui prototipi sono: u
#include <stdio.h> int printf(const char *format, ...) Stampa su stdout gli argomenti, secondo il formato specicato da format. int fprintf(FILE *stream, const char *format, ...) Stampa su stream gli argomenti, secondo il formato specicato da format. int sprintf(char *str, const char *format, ...) Stampa sulla stringa str gli argomenti, secondo il formato specicato da format. Le funzioni ritornano il numero di caratteri stampati.

le prime due servono per stampare su le (lo standard output o quello specicato) la terza permette di stampare su una stringa, in genere luso di sprintf ` sconsigliato in quanto ` e e possibile, se non si ha la sicurezza assoluta sulle dimensioni del risultato della stampa, eccedere le dimensioni di str, con conseguente sovrascrittura di altre variabili e possibili buer overow ; per questo motivo si consiglia luso dellalternativa snprintf, il cui prototipo `: e
#include <stdio.h> snprintf(char *str, size_t size, const char *format, ...) Identica a sprintf, ma non scrive su str pi` di size caratteri. u

La parte pi` complessa delle funzioni di scrittura formattata ` il formato della stringa format u e che indica le conversioni da fare, e da cui deriva anche il numero degli argomenti che dovranno essere passati a seguire (si noti come tutte queste funzioni siano variadic, prendendo un numero di argomenti variabile che dipende appunto da quello che si ` specicato in format). e
Valore %d %i %o %u %x, %X %f %e, %E %g, %G %a, %A %c %s %p %n %% Tipo int int unsigned int unsigned int unsigned int double double double double int char * void * &int Signicato Stampa un numero intero in formato decimale con segno. Identico a %i in output. Stampa un numero intero come ottale. Stampa un numero intero in formato decimale senza segno. Stampano un intero in formato esadecimale, rispettivamente con lettere minuscole e maiuscole. Stampa un numero in virgola mobile con la notazione a virgola ssa. Stampano un numero in virgola mobile con la notazione esponenziale, rispettivamente con lettere minuscole e maiuscole. Stampano un numero in virgola mobile con la notazione pi` appropriate u delle due precedenti, rispettivamente con lettere minuscole e maiuscole. Stampano un numero in virgola mobile in notazione esadecimale frazionaria. Stampa un carattere singolo. Stampa una stringa. Stampa il valore di un puntatore. Prende il numero di caratteri stampati nora. Stampa un %.

Tabella 7.2: Valori possibili per gli specicatori di conversione in una stringa di formato di printf.

La stringa ` costituita da caratteri normali (tutti eccetto %), che vengono passati invariati e alloutput, e da direttive di conversione, in cui devono essere sempre presenti il carattere %, che introduce la direttiva, ed uno degli specicatori di conversione (riportati in tab. 7.2) che la conclude. Il formato di una direttiva di conversione prevede una serie di possibili elementi opzionali oltre al % e allo specicatore di conversione. In generale essa ` sempre del tipo: e % [n. parametro $] [flag] [[larghezza] [. precisione]] [tipo] conversione

168
Valore # 0 +

CAPITOLO 7. I FILE: LINTERFACCIA STANDARD ANSI C


Signicato Chiede la conversione in forma alternativa. La conversione ` riempita con zeri alla sinistra del valore. e La conversione viene allineata a sinistra sul bordo del campo. Mette uno spazio prima di un numero con segno di valore positivo. Mette sempre il segno (+ o ) prima di un numero. Tabella 7.3: I valori dei ag per il formato di printf

in cui tutti i valori tranne il % e lo specicatore di conversione sono opzionali (e per questo sono indicati fra parentesi quadre); si possono usare pi` elementi opzionali, nel qual caso devono u essere specicati in questo ordine: uno specicatore del parametro da usare (terminato da un $), uno o pi` ag (i cui valori possibili sono riassunti in tab. 7.3) che controllano il formato di u stampa della conversione, uno specicatore di larghezza (un numero decimale), eventualmente seguito (per i numeri in virgola mobile) da un specicatore di precisione (un altro numero decimale), uno specicatore del tipo di dato, che ne indica la dimensione (i cui valori possibili sono riassunti in tab. 7.4). Dettagli ulteriori sulle varie opzioni possono essere trovati nella pagina di manuale di printf e nella documentazione delle glibc.
Valore hh h l Signicato Una conversione intera corrisponde a un char con o senza segno, o il puntatore per il numero dei parametri n ` di tipo char. e Una conversione intera corrisponde a uno short con o senza segno, o il puntatore per il numero dei parametri n ` di tipo short. e Una conversione intera corrisponde a un long con o senza segno, o il puntatore per il numero dei parametri n ` di tipo long, o il carattere o e la stringa seguenti sono in formato esteso. Una conversione intera corrisponde a un long long con o senza segno, o il puntatore per il numero dei parametri n ` di tipo long long. e Una conversione in virgola mobile corrisponde a un double. Sinonimo di ll. Una conversione intera corrisponde a un intmax_t o uintmax_t. Una conversione intera corrisponde a un size_t o ssize_t. Una conversione intera corrisponde a un ptrdiff_t.

ll L q j z t

Tabella 7.4: Il modicatore di tipo di dato per il formato di printf

Una versione alternativa delle funzioni di output formattato, che permettono di usare il puntatore ad una lista di argomenti (vedi sez. 2.4.2), sono vprintf, vfprintf e vsprintf, i cui prototipi sono:
#include <stdio.h> int vprintf(const char *format, va_list ap) Stampa su stdout gli argomenti della lista ap, secondo il formato specicato da format. int vfprintf(FILE *stream, const char *format, va_list ap) Stampa su stream gli argomenti della lista ap, secondo il formato specicato da format. int vsprintf(char *str, const char *format, va_list ap) Stampa sulla stringa str gli argomenti della lista ap, secondo il formato specicato da format. Le funzioni ritornano il numero di caratteri stampati.

con queste funzioni diventa possibile selezionare gli argomenti che si vogliono passare ad una funzione di stampa, passando direttamente la lista tramite largomento ap. Per poter far questo

7.2. FUNZIONI BASE

169

ovviamente la lista degli argomenti dovr` essere opportunamente trattata (largomento ` esaa e minato in sez. 2.4.2), e dopo lesecuzione della funzione largomento ap non sar` pi` utilizzabile a u (in generale dovrebbe essere eseguito un va_end(ap) ma in Linux questo non ` necessario). e Come per sprintf anche per vsprintf esiste una analoga vsnprintf che pone un limite sul numero di caratteri che vengono scritti sulla stringa di destinazione:
#include <stdio.h> vsnprintf(char *str, size_t size, const char *format, va_list ap) Identica a vsprintf, ma non scrive su str pi` di size caratteri. u

in modo da evitare possibili buer overow. Per eliminare alla radice questi problemi, le glibc supportano una specica estensione GNU che alloca dinamicamente tutto lo spazio necessario; lestensione si attiva al solito denendo _GNU_SOURCE, le due funzioni sono asprintf e vasprintf, ed i rispettivi prototipi sono:
#include <stdio.h> int asprintf(char **strptr, const char *format, ...) Stampa gli argomenti specicati secondo il formato specicato da format su una stringa allocata automaticamente allindirizzo *strptr. int vasprintf(char **strptr, const char *format, va_list ap) Stampa gli argomenti della lista ap secondo il formato specicato da format su una stringa allocata automaticamente allindirizzo *strptr. Le funzioni ritornano il numero di caratteri stampati.

Entrambe le funzioni prendono come argomento strptr che deve essere lindirizzo di un puntatore ad una stringa di caratteri, in cui verr` restituito (si ricordi quanto detto in sez. 2.4.1 a a proposito dei value result argument) lindirizzo della stringa allocata automaticamente dalle funzioni. Occorre inoltre ricordarsi di invocare free per liberare detto puntatore quando la stringa non serve pi`, onde evitare memory leak. u Inne una ulteriore estensione GNU denisce le due funzioni dprintf e vdprintf, che prendono un le descriptor al posto dello stream. Altre estensioni permettono di scrivere con caratteri estesi. Anche queste funzioni, il cui nome ` generato dalle precedenti funzioni aggiungendo una e w davanti a print, sono trattate in dettaglio nella documentazione delle glibc. In corrispondenza alla famiglia di funzioni printf che si usano per loutput formattato, linput formattato viene eseguito con le funzioni della famiglia scanf; fra queste le tre pi` u importanti sono scanf, fscanf e sscanf, i cui prototipi sono:
#include <stdio.h> int scanf(const char *format, ...) Esegue una scansione di stdin cercando una corrispondenza di quanto letto con il formato dei dati specicato da format, ed eettua le relative conversione memorizzando il risultato negli argomenti seguenti. int fscanf(FILE *stream, const char *format, ...) Analoga alla precedente, ma eettua la scansione su stream. int sscanf(char *str, const char *format, ...) Analoga alle precedenti, ma eettua la scansione dalla stringa str. Le funzioni ritornano il numero di elementi assegnati. Questi possono essere in numero inferiore a quelli specicati, ed anche zero. Questultimo valore signica che non si ` trovata corrispondenza. e In caso di errore o ne del le viene invece restituito EOF.

e come per le analoghe funzioni di scrittura esistono le relative vscanf, vfscanf vsscanf che usano un puntatore ad una lista di argomenti. Tutte le funzioni della famiglia delle scanf vogliono come argomenti i puntatori alle variabili che dovranno contenere le conversioni; questo ` un primo elemento di disagio in quanto ` molto e e facile dimenticarsi di questa caratteristica. Le funzioni leggono i caratteri dallo stream (o dalla stringa) di input ed eseguono un confronto con quanto indicato in format, la sintassi di questo argomento ` simile a quella usata per e

170

CAPITOLO 7. I FILE: LINTERFACCIA STANDARD ANSI C

lanalogo di printf, ma ci sono varie dierenze. Le funzioni di input infatti sono pi` orientate u verso la lettura di testo libero che verso un input formattato in campi ssi. Uno spazio in format corrisponde con un numero qualunque di caratteri di separazione (che possono essere spazi, tabulatori, virgole ecc.), mentre caratteri diversi richiedono una corrispondenza esatta. Le direttive di conversione sono analoghe a quelle di printf e si trovano descritte in dettaglio nelle pagine di manuale e nel manuale delle glibc. Le funzioni eseguono la lettura dallinput, scartano i separatori (e gli eventuali caratteri diversi indicati dalla stringa di formato) eettuando le conversioni richieste; in caso la corrispondenza fallisca (o la funzione non sia in grado di eettuare una delle conversioni richieste) la scansione viene interrotta immediatamente e la funzione ritorna lasciando posizionato lo stream al primo carattere che non corrisponde. Data la notevole complessit` di uso di queste funzioni, che richiedono molta cura nella denia zione delle corrette stringhe di formato e sono facilmente soggette ad errori, e considerato anche il fatto che ` estremamente macchinoso recuperare in caso di fallimento nelle corrispondenze, e linput formattato non ` molto usato. In genere infatti quando si ha a che fare con un input e relativamente semplice si preferisce usare linput di linea ed eettuare scansione e conversione di quanto serve direttamente con una delle funzioni di conversione delle stringhe; se invece il formato ` pi` complesso diventa pi` facile utilizzare uno strumento come flex7 per generare un e u u analizzatore lessicale o il bison8 per generare un parser.

7.2.7

Posizionamento su uno stream

Come per i le descriptor anche per gli stream ` possibile spostarsi allinterno di un le per e eettuare operazioni di lettura o scrittura in un punto prestabilito; sempre che loperazione di riposizionamento sia supportata dal le sottostante lo stream, quando cio` si ha a che fare con e 9 quello che viene detto un le ad accesso casuale. In GNU/Linux ed in generale in ogni sistema unix-like la posizione nel le ` espressa da un e intero positivo, rappresentato dal tipo off_t, il problema ` che alcune delle funzioni usate per il e riposizionamento sugli stream originano dalle prime versioni di Unix, in cui questo tipo non era ancora stato denito, e che in altri sistemi non ` detto che la posizione su un le venga sempre e rappresentata con il numero di caratteri dallinizio (ad esempio in VMS pu` essere rappresentata o come numero di record, pi` loset rispetto al record corrente). u Tutto questo comporta la presenza di diverse funzioni che eseguono sostanzialmente le stesse operazioni, ma usano argomenti di tipo diverso. Le funzioni tradizionali usate per il riposizionamento della posizione in uno stream sono fseek e rewind i cui prototipi sono:
#include <stdio.h> int fseek(FILE *stream, long offset, int whence) Sposta la posizione nello stream secondo quanto specicato tramite offset e whence. void rewind(FILE *stream) Riporta la posizione nello stream allinizio del le.

Luso di fseek ` del tutto analogo a quello di lseek per i le descriptor, e gli argomenti, a e parte il tipo, hanno lo stesso signicato; in particolare whence assume gli stessi valori gi` visti a in sez. 6.2.3. La funzione restituisce 0 in caso di successo e -1 in caso di errore. La funzione rewind riporta semplicemente la posizione corrente allinizio dello stream, ma non esattamente
il programma flex, ` una implementazione libera di lex un generatore di analizzatori lessicali. Per i dettagli e si pu` fare riferimento al manuale [7]. o 8 il programma bison ` un clone del generatore di parser yacc, maggiori dettagli possono essere trovati nel e relativo manuale [8]. 9 dato che in un sistema Unix esistono vari tipi di le, come le fo ed i le di dispositivo, non ` scontato che e questo sia sempre vero.
7

7.3. FUNZIONI AVANZATE

171

equivalente ad una fseek(stream, 0L, SEEK_SET) in quanto vengono cancellati anche i ag di errore e ne del le. Per ottenere la posizione corrente si usa invece la funzione ftell, il cui prototipo `: e
#include <stdio.h> long ftell(FILE *stream) Legge la posizione attuale nello stream stream. La funzione restituisce la posizione corrente, o -1 in caso di fallimento, che pu` esser dovuto sia o al fatto che il le non supporta il riposizionamento che al fatto che la posizione non pu` essere o espressa con un long int

la funzione restituisce la posizione come numero di byte dallinizio dello stream. Queste funzioni esprimono tutte la posizione nel le come un long int. Dato che (ad esempio quando si usa un lesystem indicizzato a 64 bit) questo pu` non essere possibile lo standard o POSIX ha introdotto le nuove funzioni fgetpos e fsetpos, che invece usano il nuovo tipo fpos_t, ed i cui prototipi sono:
#include <stdio.h> int fsetpos(FILE *stream, fpos_t *pos) Imposta la posizione corrente nello stream stream al valore specicato da pos. int fgetpos(FILE *stream, fpos_t *pos) Legge la posizione corrente nello stream stream e la scrive in pos. Le funzioni ritornano 0 in caso di successo e -1 in caso di errore.

In Linux, a partire dalle glibc 2.1, sono presenti anche le due funzioni fseeko e ftello, che sono assolutamente identiche alle precedenti fseek e ftell ma hanno argomenti di tipo off_t anzich di tipo long int. e

7.3

Funzioni avanzate

In questa sezione esamineremo alcune funzioni avanzate che permettono di eseguire operazioni particolari sugli stream, come leggerne gli attributi, controllarne le modalit` di buerizzazione, a gestire direttamente i lock impliciti per la programmazione multi thread.

7.3.1

Le funzioni di controllo

Al contrario di quanto avviene con i le descriptor, le librerie standard del C non prevedono nessuna funzione come la fcntl per il controllo degli attributi dei le. Per`, dato che ogni stream o si appoggia ad un le descriptor, si pu` usare la funzione fileno per ottenere questultimo, il o prototipo della funzione `: e
#include <stdio.h> int fileno(FILE *stream) Legge il le descriptor sottostante lo stream stream. Restituisce il numero del le descriptor in caso di successo, e -1 qualora stream non sia valido, nel qual caso imposta errno a EBADF.

ed in questo modo diventa possibile usare direttamente fcntl. Questo permette di accedere agli attributi del le descriptor sottostante lo stream, ma non ci d` nessuna informazione riguardo alle propriet` dello stream medesimo. Le glibc per` supportano a a o alcune estensioni derivate da Solaris, che permettono di ottenere informazioni utili. Ad esempio in certi casi pu` essere necessario sapere se un certo stream ` accessibile in lettura o e o scrittura. In genere questa informazione non ` disponibile, e si deve ricordare come il le ` stato e e aperto. La cosa pu` essere complessa se le operazioni vengono eettuate in una subroutine, che o

172

CAPITOLO 7. I FILE: LINTERFACCIA STANDARD ANSI C

a questo punto necessiter` di informazioni aggiuntive rispetto al semplice puntatore allo stream; a questo pu` essere evitato con le due funzioni __freadable e __fwritable i cui prototipi sono: o
#include <stdio_ext.h> int __freadable(FILE *stream) Restituisce un valore diverso da zero se stream consente la lettura. int __fwritable(FILE *stream) Restituisce un valore diverso da zero se stream consente la scrittura.

che permettono di ottenere questa informazione. La conoscenza dellultima operazione eettuata su uno stream aperto ` utile in quanto e permette di trarre conclusioni sullo stato del buer e del suo contenuto. Altre due funzioni, __freading e __fwriting servono a tale scopo, il loro prototipo `: e
#include <stdio_ext.h> int __freading(FILE *stream) Restituisce un valore diverso da zero se stream ` aperto in sola lettura o se lultima e operazione ` stata di lettura. e int __fwriting(FILE *stream) Restituisce un valore diverso da zero se stream ` aperto in sola scrittura o se lultima e operazione ` stata di scrittura. e

Le due funzioni permettono di determinare di che tipo ` stata lultima operazione eseguita e su uno stream aperto in lettura/scrittura; ovviamente se uno stream ` aperto in sola lettura e (o sola scrittura) la modalit` dellultima operazione ` sempre determinata; lunica ambiguit` ` a e ae quando non sono state ancora eseguite operazioni, in questo caso le funzioni rispondono come se una operazione ci fosse comunque stata.

7.3.2

Il controllo della buerizzazione

Come accennato in sez. 7.1.4 le librerie deniscono una serie di funzioni che permettono di controllare il comportamento degli stream; se non si ` specicato nulla, la modalit` di buering e a viene decisa autonomamente sulla base del tipo di le sottostante, ed i buer vengono allocati automaticamente. Per` una volta che si sia aperto lo stream (ma prima di aver compiuto operazioni su di o esso) ` possibile intervenire sulle modalit` di buering; la funzione che permette di controllare e a la buerizzazione ` setvbuf, il suo prototipo `: e e
#include <stdio.h> int setvbuf(FILE *stream, char *buf, int mode, size_t size) Imposta la buerizzazione dello stream stream nella modalit` indicata da mode, usando buf a come buer di lunghezza size. Restituisce zero in caso di successo, ed un valore qualunque in caso di errore, nel qual caso errno viene impostata opportunamente.

La funzione permette di controllare tutti gli aspetti della buerizzazione; lutente pu` specio care un buer da usare al posto di quello allocato dal sistema passandone alla funzione lindirizzo in buf e la dimensione in size. Ovviamente se si usa un buer specicato dallutente questo deve essere stato allocato e rimanere disponibile per tutto il tempo in cui si opera sullo stream. In genere conviene allocarlo con malloc e disallocarlo dopo la chiusura del le; ma ntanto che il le ` usato allinterno di e una funzione, pu` anche essere usata una variabile automatica. In stdio.h ` denita la macro o e BUFSIZ, che indica le dimensioni generiche del buer di uno stream; queste vengono usate dalla funzione setbuf. Non ` detto per` che tale dimensione corrisponda sempre al valore ottimale e o (che pu` variare a seconda del dispositivo). o Dato che la procedura di allocazione manuale ` macchinosa, comporta dei rischi (come delle e scritture accidentali sul buer) e non assicura la scelta delle dimensioni ottimali, ` sempre meglio e

7.3. FUNZIONI AVANZATE

173

lasciare allocare il buer alle funzioni di libreria, che sono in grado di farlo in maniera ottimale e trasparente allutente (in quanto la deallocazione avviene automaticamente). Inoltre siccome alcune implementazioni usano parte del buer per mantenere delle informazioni di controllo, non ` detto che le dimensioni dello stesso coincidano con quelle su cui viene eettuato lI/O. e
Valore _IONBF _IOLBF _IOFBF Modalit` a unbuered line buered fully buered

Tabella 7.5: Valori dellargomento mode di setvbuf per limpostazione delle modalit` di buerizzazione. a

Per evitare che setvbuf imposti il buer basta passare un valore NULL per buf e la funzione ignorer` largomento size usando il buer allocato automaticamente dal sistema. Si potr` coa a munque modicare la modalit` di buerizzazione, passando in mode uno degli opportuni valori a elencati in tab. 7.5. Qualora si specichi la modalit` non buerizzata i valori di buf e size a vengono sempre ignorati. Oltre a setvbuf le glibc deniscono altre tre funzioni per la gestione della buerizzazione di uno stream: setbuf, setbuffer e setlinebuf; i loro prototipi sono:
#include <stdio.h> void setbuf(FILE *stream, char *buf) Disabilita la buerizzazione se buf ` NULL, altrimenti usa buf come buer di dimensione e BUFSIZ in modalit` fully buered. a void setbuffer(FILE *stream, char *buf, size_t size) Disabilita la buerizzazione se buf ` NULL, altrimenti usa buf come buer di dimensione e size in modalit` fully buered. a void setlinebuf(FILE *stream) Pone lo stream in modalit` line buered. a

tutte queste funzioni sono realizzate con opportune chiamate a setvbuf e sono denite solo per compatibilit` con le vecchie librerie BSD. Inne le glibc provvedono le funzioni non standard10 a __flbf e __fbufsize che permettono di leggere le propriet` di buerizzazione di uno stream; i a cui prototipi sono:
#include <stdio_ext.h> int __flbf(FILE *stream) Restituisce un valore diverso da zero se stream ` in modalit` line buered. e a size_t __fbufsize(FILE *stream) Restituisce le dimensioni del buer di stream.

Come gi` accennato, indipendentemente dalla modalit` di buerizzazione scelta, si pu` a a o forzare lo scarico dei dati sul le con la funzione fflush, il suo prototipo `: e
#include <stdio.h> int fflush(FILE *stream) Forza la scrittura di tutti i dati buerizzati dello stream stream. Restituisce zero in caso di successo, ed EOF in caso di errore, impostando errno a EBADF se stream non ` aperto o non ` aperto in scrittura, o ad uno degli errori di write. e e

anche di questa funzione esiste una analoga fflush_unlocked11 che non eettua il blocco dello stream. Se stream ` NULL lo scarico dei dati ` forzato per tutti gli stream aperti. Esistono per` e e o circostanze, ad esempio quando si vuole essere sicuri che sia stato eseguito tutto loutput su terminale, in cui serve poter eettuare lo scarico dei dati solo per gli stream in modalit` line a
10 11

anche queste funzioni sono originarie di Solaris. accessibile denendo _BSD_SOURCE o _SVID_SOURCE o _GNU_SOURCE.

174

CAPITOLO 7. I FILE: LINTERFACCIA STANDARD ANSI C

buered; per questo motivo le glibc supportano una estensione di Solaris, la funzione _flushlbf, il cui prototipo `: e
#include <stdio-ext.h> void _flushlbf(void) Forza la scrittura di tutti i dati buerizzati degli stream in modalit` line buered. a

Si ricordi comunque che lo scarico dei dati dai buer eettuato da queste funzioni non comporta la scrittura di questi su disco; se si vuole che il kernel dia eettivamente avvio alle operazioni di scrittura su disco occorre usare sync o fsync (si veda sez. 6.3.3). Inne esistono anche circostanze in cui si vuole scartare tutto loutput pendente; per questo si pu` usare fpurge, il cui prototipo `: o e
#include <stdio.h> int fpurge(FILE *stream) Cancella i buer di input e di output dello stream stream. Restituisce zero in caso di successo, ed EOF in caso di errore.

La funzione scarta tutti i dati non ancora scritti (se il le ` aperto in scrittura), e tutto linput e non ancora letto (se ` aperto in lettura), compresi gli eventuali caratteri rimandati indietro con e ungetc.

7.3.3

Gli stream e i thread

Gli stream possono essere usati in applicazioni multi-thread allo stesso modo in cui sono usati nelle applicazioni normali, ma si deve essere consapevoli delle possibili complicazioni anche quando non si usano i thread, dato che limplementazione delle librerie ` inuenzata pesantemente e dalle richieste necessarie per garantirne luso con i thread. Lo standard POSIX richiede che le operazioni sui le siano atomiche rispetto ai thread, per questo le operazioni sui buer eettuate dalle funzioni di libreria durante la lettura e la scrittura di uno stream devono essere opportunamente protette (in quanto il sistema assicura latomicit` a solo per le system call). Questo viene fatto associando ad ogni stream un opportuno blocco che deve essere implicitamente acquisito prima dellesecuzione di qualunque operazione. Ci sono comunque situazioni in cui questo non basta, come quando un thread necessita di compiere pi` di una operazione sullo stream atomicamente, per questo motivo le librerie provveu dono anche delle funzioni flockfile, ftrylockfile e funlockfile, che permettono la gestione esplicita dei blocchi sugli stream; esse sono disponibili denendo _POSIX_THREAD_SAFE_FUNCTIONS ed i loro prototipi sono:
#include <stdio.h> void flockfile(FILE *stream) Esegue lacquisizione del lock dello stream stream, bloccandosi se il lock non ` disponibile. e int ftrylockfile(FILE *stream) Tenta lacquisizione del lock dello stream stream, senza bloccarsi se il lock non ` disponibile. e Ritorna zero in caso di acquisizione del lock, diverso da zero altrimenti. void funlockfile(FILE *stream) Rilascia il lock dello stream stream.

con queste funzioni diventa possibile acquisire un blocco ed eseguire tutte le operazioni volute, per poi rilasciarlo. Ma, vista la complessit` delle strutture di dati coinvolte, le operazioni di blocco non sono a del tutto indolori, e quando il locking dello stream non ` necessario (come in tutti i programmi e che non usano i thread), tutta la procedura pu` comportare dei costi pesanti in termini di o prestazioni. Per questo motivo abbiamo visto come alle usuali funzioni di I/O non formattato siano associate delle versioni _unlocked (alcune previste dallo stesso standard POSIX, altre

7.3. FUNZIONI AVANZATE

175

aggiunte come estensioni dalle glibc) che possono essere usate quando il locking non serve12 con prestazioni molto pi` elevate, dato che spesso queste versioni (come accade per getc e putc) u sono realizzate come macro. La sostituzione di tutte le funzioni di I/O con le relative versioni _unlocked in un programma che non usa i thread ` per` un lavoro abbastanza noioso; per questo motivo le glibc forniscono al e o programmatore pigro unaltra via13 da poter utilizzare per disabilitare in blocco il locking degli stream: luso della funzione __fsetlocking, il cui prototipo `: e
#include <stdio_ext.h> int __fsetlocking (FILE *stream, int type) Specica o richiede a seconda del valore di type la modalit` in cui le operazioni di I/O su a stream vengono eettuate rispetto allacquisizione implicita del blocco sullo stream. Restituisce lo stato di locking interno dello stream con uno dei valori FSETLOCKING_INTERNAL o FSETLOCKING_BYCALLER.

La funzione imposta o legge lo stato della modalit` di operazione di uno stream nei confronti a del locking a seconda del valore specicato con type, che pu` essere uno dei seguenti: o FSETLOCKING_INTERNAL Lo stream user` da ora in poi il blocco implicito predenito. a FSETLOCKING_BYCALLER Al ritorno della funzione sar` lutente a dover gestire da solo il locking a dello stream. FSETLOCKING_QUERY Restituisce lo stato corrente della modalit` di blocco dello stream. a

in certi casi dette funzioni possono essere usate, visto che sono molto pi` ecienti, anche in caso di necessit` u a di locking, una volta che questo sia stato acquisito manualmente. 13 anche questa mutuata da estensioni introdotte in Solaris.

12

176

CAPITOLO 7. I FILE: LINTERFACCIA STANDARD ANSI C

Capitolo 8

La gestione del sistema, del tempo e degli errori


In questo capitolo tratteremo varie interfacce che attengono agli aspetti pi` generali del sistema, u come quelle per la gestione dei parametri e della congurazione dello stesso, quelle per la lettura dei limiti e delle caratteristiche, quelle per il controllo delluso delle risorse dei processi, quelle per la gestione ed il controllo dei lesystem, degli utenti, dei tempi e degli errori.

8.1

Capacit` e caratteristiche del sistema a

In questa sezione tratteremo le varie modalit` con cui un programma pu` ottenere informaa o zioni riguardo alle capacit` del sistema. Ogni sistema unix-like infatti ` contraddistinto da un a e gran numero di limiti e costanti che lo caratterizzano, e che possono dipendere da fattori molteplici, come larchitettura hardware, limplementazione del kernel e delle librerie, le opzioni di congurazione. La denizione di queste caratteristiche ed il tentativo di provvedere dei meccanismi generali che i programmi possono usare per ricavarle ` uno degli aspetti pi` complessi e controversi con e u cui le diverse standardizzazioni si sono dovute confrontare, spesso con risultati spesso tuttaltro che chiari. Daremo comunque una descrizione dei principali metodi previsti dai vari standard per ricavare sia le caratteristiche speciche del sistema, che quelle della gestione dei le.

8.1.1

Limiti e parametri di sistema

Quando si devono determinare le caratteristiche generali del sistema ci si trova di fronte a diverse possibilit`; alcune di queste infatti possono dipendere dallarchitettura dellhardware (come le a dimensioni dei tipi interi), o dal sistema operativo (come la presenza o meno del gruppo degli identicatori saved ), altre invece possono dipendere dalle opzioni con cui si ` costruito il sistema e (ad esempio da come si ` compilato il kernel), o dalla congurazione del medesimo; per questo e motivo in generale sono necessari due tipi diversi di funzionalit`: a la possibilit` di determinare limiti ed opzioni al momento della compilazione. a la possibilit` di determinare limiti ed opzioni durante lesecuzione. a La prima funzionalit` si pu` ottenere includendo gli opportuni header le che contengono le a o costanti necessarie denite come macro di preprocessore, per la seconda invece sono ovviamente necessarie delle funzioni. La situazione ` complicata dal fatto che ci sono molti casi in cui alcuni e di questi limiti sono ssi in unimplementazione mentre possono variare in un altra. Tutto questo crea una ambiguit` che non ` sempre possibile risolvere in maniera chiara; in generale quello a e che succede ` che quando i limiti del sistema sono ssi essi vengono deniti come macro di e 177

178

CAPITOLO 8. LA GESTIONE DEL SISTEMA, DEL TEMPO E DEGLI ERRORI

preprocessore nel le limits.h, se invece possono variare, il loro valore sar` ottenibile tramite a la funzione sysconf (che esamineremo in sez. 8.1.2). Lo standard ANSI C denisce dei limiti che sono tutti ssi, pertanto questo saranno sempre disponibili al momento della compilazione. Un elenco, ripreso da limits.h, ` riportato in tab. 8.1. e Come si pu` vedere per la maggior parte questi limiti attengono alle dimensioni dei dati interi, o che sono in genere ssati dallarchitettura hardware (le analoghe informazioni per i dati in virgola mobile sono denite a parte, ed accessibili includendo float.h). Lo standard prevede anche unaltra costante, FOPEN_MAX, che pu` non essere ssa e che pertanto non ` denita in o e limits.h; essa deve essere denita in stdio.h ed avere un valore minimo di 8.
Costante MB_LEN_MAX CHAR_BIT UCHAR_MAX SCHAR_MIN SCHAR_MAX CHAR_MIN CHAR_MAX SHRT_MIN SHRT_MAX USHRT_MAX INT_MAX INT_MIN UINT_MAX LONG_MAX LONG_MIN ULONG_MAX Valore 16 8 255 -128 127
1 2

-32768 32767 65535 2147483647 -2147483648 4294967295 2147483647 -2147483648 4294967295

Signicato Massima dimensione di un carattere esteso. Numero di bit di char. Massimo di unsigned char. Minimo di signed char. Massimo di signed char. Minimo di char. Massimo di char. Minimo di short. Massimo di short. Massimo di unsigned short. Minimo di int. Minimo di int. Massimo di unsigned int. Massimo di long. Minimo di long. Massimo di unsigned long.

Tabella 8.1: Costanti denite in limits.h in conformit` allo standard ANSI C. a

A questi valori lo standard ISO C90 ne aggiunge altri tre, relativi al tipo long long introdotto con il nuovo standard, i relativi valori sono in tab. 8.2.
Costante LLONG_MAX LLONG_MIN ULLONG_MAX Valore 9223372036854775807 -9223372036854775808 18446744073709551615 Signicato Massimo di long long. Minimo di long long. Massimo di unsigned long long.

Tabella 8.2: Macro denite in limits.h in conformit` allo standard ISO C90. a

Ovviamente le dimensioni dei vari tipi di dati sono solo una piccola parte delle caratteristiche del sistema; mancano completamente tutte quelle che dipendono dalla implementazione dello stesso. Queste, per i sistemi unix-like, sono state denite in gran parte dallo standard POSIX.1, che tratta anche i limiti relativi alle caratteristiche dei le che vedremo in sez. 8.1.3. Purtroppo la sezione dello standard che tratta questi argomenti ` una delle meno chiare3 . e Lo standard prevede che ci siano 13 macro che descrivono le caratteristiche del sistema (7 per le caratteristiche generiche, riportate in tab. 8.3, e 6 per le caratteristiche dei le, riportate in tab. 8.7). Lo standard dice che queste macro devono essere denite in limits.h quando i valori a cui fanno riferimento sono ssi, e altrimenti devono essere lasciate indenite, ed i loro valori dei limiti devono essere accessibili solo attraverso sysconf. In realt` queste vengono sempre denite a ad un valore generico. Si tenga presente poi che alcuni di questi limiti possono assumere valori
1

il valore pu` essere 0 o SCHAR_MIN a seconda che il sistema usi caratteri con segno o meno. o il valore pu` essere UCHAR_MAX o SCHAR_MAX a seconda che il sistema usi caratteri con segno o meno. o 3 tanto che Stevens, in [1], la porta come esempio di standardese.
2

` 8.1. CAPACITA E CARATTERISTICHE DEL SISTEMA


Costante ARG_MAX CHILD_MAX OPEN_MAX STREAM_MAX TZNAME_MAX NGROUPS_MAX SSIZE_MAX Valore 131072 999 256 8 6 32 32767 Signicato Dimensione massima degli argomenti passati ad una funzione della famiglia exec. Numero massimo di processi contemporanei che un utente pu` eseguire. o Numero massimo di le che un processo pu` o mantenere aperti in contemporanea. Massimo numero di stream aperti per processo in contemporanea. Dimensione massima del nome di una timezone (vedi sez. 8.4.3)). Numero di gruppi supplementari per processo (vedi sez. 3.3.1). Valore massimo del tipo ssize_t.

179

Tabella 8.3: Costanti per i limiti del sistema.

molto elevati (come CHILD_MAX), e non ` pertanto il caso di utilizzarli per allocare staticamente e della memoria. A complicare la faccenda si aggiunge il fatto che POSIX.1 prevede una serie di altre costanti (il cui nome inizia sempre con _POSIX_) che deniscono i valori minimi le stesse caratteristiche devono avere, perch una implementazione possa dichiararsi conforme allo standard; detti valori e sono riportati in tab. 8.4.
Costante _POSIX_ARG_MAX _POSIX_CHILD_MAX _POSIX_OPEN_MAX _POSIX_STREAM_MAX _POSIX_TZNAME_MAX _POSIX_NGROUPS_MAX _POSIX_SSIZE_MAX _POSIX_AIO_LISTIO_MAX _POSIX_AIO_MAX 0 32767 2 1 Valore 4096 6 16 8 Signicato Dimensione massima degli argomenti passati ad una funzione della famiglia exec. Numero massimo di processi contemporanei che un utente pu` eseguire. o Numero massimo di le che un processo pu` o mantenere aperti in contemporanea. Massimo numero di stream aperti per processo in contemporanea. Dimensione massima del nome di una timezone (vedi sez. 8.4.4). Numero di gruppi supplementari per processo (vedi sez. 3.3.1). Valore massimo del tipo ssize_t.

Tabella 8.4: Macro dei valori minimi delle caratteristiche generali del sistema per la conformit` allo standard a POSIX.1.

In genere questi valori non servono a molto, la loro unica utilit` ` quella di indicare un limite ae superiore che assicura la portabilit` senza necessit` di ulteriori controlli. Tuttavia molti di essi a a sono ampiamente superati in tutti i sistemi POSIX in uso oggigiorno. Per questo ` sempre meglio e utilizzare i valori ottenuti da sysconf.
Macro _POSIX_JOB_CONTROL _POSIX_SAVED_IDS _POSIX_VERSION Signicato Il sistema supporta il job control (vedi sez. 10.1). Il sistema supporta gli identicatori del gruppo saved (vedi sez. 3.3.1) per il controllo di accesso dei processi Fornisce la versione dello standard POSIX.1 supportata nel formato YYYYMML (ad esempio 199009L).

Tabella 8.5: Alcune macro denite in limits.h in conformit` allo standard POSIX.1. a

180

CAPITOLO 8. LA GESTIONE DEL SISTEMA, DEL TEMPO E DEGLI ERRORI

Oltre ai precedenti valori (e a quelli relativi ai le elencati in tab. 8.8), che devono essere obbligatoriamente deniti, lo standard POSIX.1 ne prevede parecchi altri. La lista completa si trova dallheader le bits/posix1_lim.h (da non usare mai direttamente, ` incluso automaticae mente allinterno di limits.h). Di questi vale la pena menzionare alcune macro di uso comune, (riportate in tab. 8.5), che non indicano un valore specico, ma denotano la presenza di alcune funzionalit` nel sistema (come il supporto del job control o degli identicatori del gruppo saved ). a Oltre allo standard POSIX.1, anche lo standard POSIX.2 denisce una serie di altre costanti. Siccome queste sono principalmente attinenti a limiti relativi alle applicazioni di sistema presenti (come quelli su alcuni parametri delle espressioni regolari o del comando bc), non li tratteremo esplicitamente, se ne trova una menzione completa nellheader le bits/posix2_lim.h, e alcuni di loro sono descritti nella pagina di manuale di sysconf e nel manuale delle glibc.

8.1.2

La funzione sysconf

Come accennato in sez. 8.1.1 quando uno dei limiti o delle caratteristiche del sistema pu` variare, o per non dover essere costretti a ricompilare un programma tutte le volte che si cambiano le opzioni con cui ` compilato il kernel, o alcuni dei parametri modicabili a run time, ` necessario e e ottenerne il valore attraverso la funzione sysconf. Il prototipo di questa funzione `: e
#include <unistd.h> long sysconf(int name) Restituisce il valore del parametro di sistema name. La funzione restituisce indietro il valore del parametro richiesto, o 1 se si tratta di unopzione disponibile, 0 se lopzione non ` disponibile e -1 in caso di errore (ma errno non viene impostata). e

La funzione prende come argomento un intero che specica quale dei limiti si vuole conoscere; uno specchietto contenente i principali valori disponibili in Linux ` riportato in tab. 8.6; lelenco e completo ` contenuto in bits/confname.h, ed una lista pi` esaustiva, con le relative spiegazioni, e u si pu` trovare nel manuale delle glibc. o
Parametro _SC_ARG_MAX _SC_CHILD_MAX _SC_OPEN_MAX _SC_STREAM_MAX Macro sostituita ARG_MAX _CHILD_MAX _OPEN_MAX STREAM_MAX Signicato La dimensione massima degli argomenti passati ad una funzione della famiglia exec. Il numero massimo di processi contemporanei che un utente pu` o eseguire. Il numero massimo di le che un processo pu` mantenere aperti o in contemporanea. Il massimo numero di stream che un processo pu` manteneo re aperti in contemporanea. Questo limite previsto anche dallo standard ANSI C, che specica la macro FOPEN MAX. La dimensione massima di un nome di una timezone (vedi sez. 8.4.4). Massimo numero di gruppi supplementari che pu` avere un o processo (vedi sez. 3.3.1). Valore massimo del tipo di dato ssize_t. Il numero di clock tick al secondo, cio` lunit` di misura del e a process time (vedi sez. 8.4.1). Indica se ` supportato il job control (vedi sez. 10.1) in stile e POSIX. Indica se il sistema supporta i saved id (vedi sez. 3.3.1). Indica il mese e lanno di approvazione della revisione dello standard POSIX.1 a cui il sistema fa riferimento, nel formato YYYYMML, la revisione pi` recente ` 199009L, che indica il u e Settembre 1990.

_SC_TZNAME_MAX _SC_NGROUPS_MAX _SC_SSIZE_MAX _SC_CLK_TCK _SC_JOB_CONTROL _SC_SAVED_IDS _SC_VERSION

TZNAME_MAX NGROUP_MAX SSIZE_MAX CLK_TCK _POSIX_JOB_CONTROL _POSIX_SAVED_IDS _POSIX_VERSION

Tabella 8.6: Parametri del sistema leggibili dalla funzione sysconf.

` 8.1. CAPACITA E CARATTERISTICHE DEL SISTEMA

181

In generale ogni limite o caratteristica del sistema per cui ` denita una macro, sia dagli e standard ANSI C e ISO C90, che da POSIX.1 e POSIX.2, pu` essere ottenuto attraverso una o chiamata a sysconf. Il valore si otterr` specicando come valore dellargomento name il nome a ottenuto aggiungendo _SC_ ai nomi delle macro denite dai primi due, o sostituendolo a _POSIX_ per le macro denite dagli gli altri due. In generale si dovrebbe fare uso di sysconf solo quando la relativa macro non ` denita, e quindi con un codice analogo al seguente: get_child_max ( void ) { # ifdef CHILD_MAX return CHILD_MAX ; # else int val = sysconf ( _SC_CHILD_MAX ); if ( val < 0) { perror ( " fatal error " ); exit ( -1); } return val ; # endif } ma in realt` in Linux queste macro sono comunque denite, indicando per` un limite generico. a o Per questo motivo ` sempre meglio usare i valori restituiti da sysconf. e

8.1.3

I limiti dei le

Come per le caratteristiche generali del sistema anche per i le esistono una serie di limiti (come la lunghezza del nome del le o il numero massimo di link) che dipendono sia dallimplementazione che dal lesystem in uso; anche in questo caso lo standard prevede alcune macro che ne specicano il valore, riportate in tab. 8.7.
Costante LINK_MAX NAME_MAX PATH_MAX PIPE_BUF MAX_CANON MAX_INPUT Valore 8 14 256 4096 255 255 Signicato Numero massimo di link a un le. Lunghezza in byte di un nome di le. Lunghezza in byte di un pathname. Byte scrivibili atomicamente in una pipe (vedi sez. 12.1.1). Dimensione di una riga di terminale in modo canonico (vedi sez. 10.2.1). Spazio disponibile nella coda di input del terminale (vedi sez. 10.2.1).

Tabella 8.7: Costanti per i limiti sulle caratteristiche dei le.

Come per i limiti di sistema, lo standard POSIX.1 detta una serie di valori minimi anche per queste caratteristiche, che ogni sistema che vuole essere conforme deve rispettare; le relative macro sono riportate in tab. 8.8, e per esse vale lo stesso discorso fatto per le analoghe di tab. 8.4.
Macro _POSIX_LINK_MAX _POSIX_NAME_MAX _POSIX_PATH_MAX _POSIX_PIPE_BUF _POSIX_MAX_CANON _POSIX_MAX_INPUT Valore 8 14 256 512 255 255 Signicato Numero massimo di link a un le. Lunghezza in byte di un nome di le. Lunghezza in byte di un pathname. Byte scrivibili atomicamente in una pipe. Dimensione di una riga di terminale in modo canonico. Spazio disponibile nella coda di input del terminale.

Tabella 8.8: Costanti dei valori minimi delle caratteristiche dei le per la conformit` allo standard POSIX.1. a

182

CAPITOLO 8. LA GESTIONE DEL SISTEMA, DEL TEMPO E DEGLI ERRORI

Tutti questi limiti sono deniti in limits.h; come nel caso precedente il loro uso ` di scarsa e utilit` in quanto ampiamente superati in tutte le implementazioni moderne. a

8.1.4

La funzione pathconf

In generale i limiti per i le sono molto pi` soggetti ad essere variabili rispetto ai limiti generali u del sistema; ad esempio parametri come la lunghezza del nome del le o il numero di link possono variare da lesystem a lesystem; per questo motivo questi limiti devono essere sempre controllati con la funzione pathconf, il cui prototipo `: e
#include <unistd.h> long pathconf(char *path, int name) Restituisce il valore del parametro name per il le path. La funzione restituisce indietro il valore del parametro richiesto, o -1 in caso di errore (ed errno viene impostata ad uno degli errori possibili relativi allaccesso a path).

E si noti come la funzione in questo caso richieda un argomento che specichi a quale le si fa riferimento, dato che il valore del limite cercato pu` variare a seconda del lesystem. Una seconda o versione della funzione, fpathconf, opera su un le descriptor invece che su un pathname. Il suo prototipo `: e
#include <unistd.h> long fpathconf(int fd, int name) Restituisce il valore del parametro name per il le fd. ` E identica a pathconf solo che utilizza un le descriptor invece di un pathname; pertanto gli errori restituiti cambiano di conseguenza.

ed il suo comportamento ` identico a quello di pathconf. e

8.1.5

La funzione uname

Unaltra funzione che si pu` utilizzare per raccogliere informazioni sia riguardo al sistema che o al computer su cui esso sta girando ` uname; il suo prototipo `: e e
#include <sys/utsname.h> int uname(struct utsname *info) Restituisce informazioni sul sistema nella struttura info. La funzione ritorna 0 in caso di successo e -1 in caso di fallimento, nel qual caso errno assumer` a il valore EFAULT.

La funzione, che viene usata dal comando uname, restituisce le informazioni richieste nella struttura info; anche questa struttura ` denita in sys/utsname.h, secondo quanto mostrato e in g. 8.1, e le informazioni memorizzate nei suoi membri indicano rispettivamente: il il il il il il nome del sistema operativo; nome della release del kernel; nome della versione del kernel; tipo di macchina in uso; nome della stazione; nome del domino.

lultima informazione ` stata aggiunta di recente e non ` prevista dallo standard POSIX, essa ` e e e accessibile, come mostrato in g. 8.1, solo denendo _GNU_SOURCE. In generale si tenga presente che le dimensioni delle stringe di una utsname non ` specicata, e e che esse sono sempre terminate con NUL; il manuale delle glibc indica due diverse dimensioni, _UTSNAME_LENGTH per i campi standard e _UTSNAME_DOMAIN_LENGTH per quello specico per il

8.2. OPZIONI E CONFIGURAZIONE DEL SISTEMA

183

struct utsname { char sysname []; char nodename []; char release []; char version []; char machine []; # ifdef _GNU_SOURCE char domainname []; # endif };

Figura 8.1: La struttura utsname.

nome di dominio; altri sistemi usano nomi diversi come SYS_NMLN o _SYS_NMLN o UTSLEN che possono avere valori diversi.4

8.2

Opzioni e congurazione del sistema

Come abbiamo accennato nella sezione precedente, non tutti i limiti che caratterizzano il sistema sono ssi, o perlomeno non lo sono in tutte le implementazioni. Finora abbiamo visto come si pu` fare per leggerli, ci manca di esaminare il meccanismo che permette, quando questi possono o variare durante lesecuzione del sistema, di modicarli. Inoltre, al di la di quelli che possono essere limiti caratteristici previsti da uno standard, ogni sistema pu` avere una sua serie di altri parametri di congurazione, che, non essendo mai o ssi e variando da sistema a sistema, non sono stati inclusi nella standardizzazione della sezione precedente. Per questi occorre, oltre al meccanismo di impostazione, pure un meccanismo di lettura. Aronteremo questi argomenti in questa sezione, insieme alle funzioni che si usano per il controllo di altre caratteristiche generali del sistema, come quelle per la gestione dei lesystem e di utenti e gruppi.

8.2.1

La funzione sysctl ed il lesystem /proc

La funzione che permette la lettura ed limpostazione dei parametri del sistema ` sysctl; ` una e e funzione derivata da BSD4.4, ma limplementazione ` specica di Linux; il suo prototipo `: e e
#include <unistd.h> int sysctl(int *name, int nlen, void *oldval, size_t *oldlenp, void *newval, size_t newlen) Legge o scrive uno dei parametri di sistema. La funzione restituisce 0 in caso di successo e -1 in caso di errore, nel qual caso errno assumer` a uno dei valori: EPERM ENOTDIR EINVAL ENOMEM non si ha il permesso di accedere ad uno dei componenti nel cammino specicato per il parametro, o di accedere al parametro nella modalit` scelta. a non esiste un parametro corrispondente al nome name. o si ` specicato un valore non valido per il parametro che si vuole impostare o lo e spazio provvisto per il ritorno di un valore non ` delle giuste dimensioni. e talvolta viene usato pi` correttamente questo errore quando non si ` specicato u e suciente spazio per ricevere il valore di un parametro.

ed inoltre EFAULT. nel caso di Linux uname corrisponde in realt` a 3 system call diverse, le prime due usano rispettivamente a delle lunghezze delle stringhe di 9 e 65 byte; la terza usa anchessa 65 byte, ma restituisce anche lultimo campo, domainname, con una lunghezza di 257 byte.
4

184

CAPITOLO 8. LA GESTIONE DEL SISTEMA, DEL TEMPO E DEGLI ERRORI

I parametri a cui la funzione permettere di accedere sono organizzati in maniera gerarchica allinterno di un albero;5 per accedere ad uno di essi occorre specicare un cammino attraverso i vari nodi dellalbero, in maniera analoga a come avviene per la risoluzione di un pathname (da cui luso alternativo del lesystem /proc, che vedremo dopo). Ciascun nodo dellalbero ` identicato da un valore intero, ed il cammino che arriva ad e identicare un parametro specico ` passato alla funzione attraverso larray name, di lunghezza e nlen, che contiene la sequenza dei vari nodi da attraversare. Ogni parametro ha un valore in un formato specico che pu` essere un intero, una stringa o anche una struttura complessa, per o questo motivo i valori vengono passati come puntatori void. Lindirizzo a cui il valore corrente del parametro deve essere letto ` specicato da oldvalue, e e lo spazio ivi disponibile ` specicato da oldlenp (passato come puntatore per avere indietro e la dimensione eettiva di quanto letto); il valore che si vuole impostare nel sistema ` passato in e newval e la sua dimensione in newlen. Si pu` eettuare anche una lettura e scrittura simultanea, nel qual caso il valore letto o restituito dalla funzione ` quello precedente alla scrittura. e I parametri accessibili attraverso questa funzione sono moltissimi, e possono essere trovati in sysctl.h, essi inoltre dipendono anche dallo stato corrente del kernel (ad esempio dai moduli che sono stati caricati nel sistema) e in genere i loro nomi possono variare da una versione di kernel allaltra; per questo ` sempre il caso di evitare luso di sysctl quando esistono modalit` e a alternative per ottenere le stesse informazioni. Alcuni esempi di parametri ottenibili sono: il nome di dominio i parametri del meccanismo di paging. il lesystem montato come radice la data di compilazione del kernel i parametri dello stack TCP il numero massimo di le aperti Come accennato in Linux si ha una modalit` alternativa per accedere alle stesse informaa zioni di sysctl attraverso luso del lesystem /proc. Questo ` un lesystem virtuale, generato e direttamente dal kernel, che non fa riferimento a nessun dispositivo sico, ma presenta in forma di le alcune delle strutture interne del kernel stesso. In particolare lalbero dei valori di sysctl viene presentato in forma di le nella directory /proc/sys, cosicch ` possibile accedervi specicando un pathname e leggendo e scrivendo sul ee le corrispondente al parametro scelto. Il kernel si occupa di generare al volo il contenuto ed i nomi dei le corrispondenti, e questo ha il grande vantaggio di rendere accessibili i vari parametri a qualunque comando di shell e di permettere la navigazione dellalbero dei valori. Alcune delle corrispondenze dei le presenti in /proc/sys con i valori di sysctl sono riportate nei commenti del codice che pu` essere trovato in linux/sysctl.h,6 la informazione o disponibile in /proc/sys ` riportata inoltre nella documentazione inclusa nei sorgenti del kernel, e nella directory Documentation/sysctl. Ma oltre alle informazioni ottenibili da sysctl dentro proc sono disponibili moltissime altre informazioni, fra cui ad esempio anche quelle fornite da uname (vedi sez. 8.2) che sono mantenute nei le ostype, hostname, osrelease, version e domainname di /proc/sys/kernel/.
5 si tenga presente che includendo solo unistd.h, saranno deniti solo i parametri generici; dato che ce ne sono molti specici dellimplementazione, nel caso di Linux occorrer` includere anche i le linux/unistd.h e a linux/sysctl.h. 6 indicando un le di denizioni si fa riferimento alla directory standard dei le di include, che in ogni distribuzione che si rispetti ` /usr/include. e

8.2. OPZIONI E CONFIGURAZIONE DEL SISTEMA

185

8.2.2

La gestione delle propriet` dei lesystem a

Come accennato in sez. 4.1.1 per poter accedere ai le occorre prima rendere disponibile al sistema il lesystem su cui essi sono memorizzati; loperazione di attivazione del lesystem ` e chiamata montaggio, per far questo in Linux7 si usa la funzione mount il cui prototipo `: e
#include <sys/mount.h> mount(const char *source, const char *target, const char *filesystemtype, unsigned long mountflags, const void *data) Monta il lesystem di tipo filesystemtype contenuto in source sulla directory target. La funzione ritorna 0 in caso di successo e -1 in caso di fallimento, nel qual caso gli errori comuni a tutti i lesystem che possono essere restituiti in errno sono: EPERM ENODEV ENOTBLK EBUSY EINVAL il processo non ha i privilegi di amministratore. filesystemtype non esiste o non ` congurato nel kernel. e non si ` usato un block device per source quando era richiesto. e source ` gi` montato, o non pu` essere rimontato in read-only perch ci sono ancora e a o e le aperti in scrittura, o target ` ancora in uso. e il device source presenta un superblock non valido, o si ` cercato di rimontare un e lesystem non ancora montato, o di montarlo senza che target sia un mount point o di spostarlo quando target non ` un mount point o ` /. e e non si ha il permesso di accesso su uno dei componenti del pathname, o si ` cercato e di montare un lesystem disponibile in sola lettura senza averlo specicato o il device source ` su un lesystem montato con lopzione MS_NODEV. e il major number del device source ` sbagliato. e la tabella dei device dummy ` piena. e

EACCES

ENXIO EMFILE

ed inoltre ENOTDIR, EFAULT, ENOMEM, ENAMETOOLONG, ENOENT o ELOOP.

La funzione monta sulla directory target, detta mount point, il lesystem contenuto in source. In generale un lesystem ` contenuto su un disco, e loperazione di montaggio corrie sponde a rendere visibile al sistema il contenuto del suddetto disco, identicato attraverso il le di dispositivo ad esso associato. Ma la struttura del virtual lesystem vista in sez. 4.2.1 ` molto pi` essibile e pu` essere e u o usata anche per oggetti diversi da un disco. Ad esempio usando il loop device si pu` montare o un le qualunque (come limmagine di un CD-ROM o di un oppy) che contiene un lesystem, inoltre alcuni lesystem, come proc o devfs sono del tutto virtuali, i loro dati sono generati al volo ad ogni lettura, e passati al kernel ad ogni scrittura. Il tipo di lesystem ` specicato da filesystemtype, che deve essere una delle stringhe e riportate nel le /proc/filesystems, che contiene lelenco dei lesystem supportati dal kernel; nel caso si sia indicato uno dei lesystem virtuali, il contenuto di source viene ignorato. Dopo lesecuzione della funzione il contenuto del lesystem viene resto disponibile nella directory specicata come mount point, il precedente contenuto di detta directory viene mascherato dal contenuto della directory radice del lesystem montato. Dal kernel 2.4.x inoltre ` divenuto possibile sia spostare atomicamente un mount point da e una directory ad unaltra, sia montare in diversi mount point lo stesso lesystem, sia montare pi` u lesystem sullo stesso mount point (nel qual caso vale quanto appena detto, e solo il contenuto dellultimo lesystem montato sar` visibile). a Ciascun lesystem ` dotato di caratteristiche speciche che possono essere attivate o meno, e alcune di queste sono generali (anche se non ` detto siano disponibili in ogni lesystem), e e vengono specicate come opzioni di montaggio con largomento mountflags. In Linux mountflags deve essere un intero a 32 bit i cui 16 pi` signicativi sono un magic u
7

la funzione ` specica di Linux e non ` portabile. e e

186

CAPITOLO 8. LA GESTIONE DEL SISTEMA, DEL TEMPO E DEGLI ERRORI

number 8 mentre i 16 meno signicativi sono usati per specicare le opzioni; essi sono usati come maschera binaria e vanno impostati con un OR aritmetico della costante MS_MGC_VAL con i valori riportati in tab. 8.9.
Parametro MS_RDONLY MS_NOSUID MS_NODEV MS_NOEXEC MS_SYNCHRONOUS MS_REMOUNT MS_MANDLOCK S_WRITE S_APPEND S_IMMUTABLE MS_NOATIME MS_NODIRATIME MS_BIND MS_MOVE Valore 1 2 4 8 16 32 64 128 256 512 1024 2048 4096 8192 Signicato Monta in sola lettura. Ignora i bit suid e sgid. Impedisce laccesso ai le di dispositivo. Impedisce di eseguire programmi. Abilita la scrittura sincrona. Rimonta il lesystem cambiando le opzioni. Consente il mandatory locking (vedi sez. 11.4.5). Scrive normalmente. Consente la scrittura solo in append mode (vedi sez. 6.3.1). Impedisce che si possano modicare i le. Non aggiorna gli access time (vedi sez. 5.2.4). Non aggiorna gli access time delle directory. Monta il lesystem altrove. Sposta atomicamente il punto di montaggio.

Tabella 8.9: Tabella dei codici dei ag di montaggio di un lesystem.

Per limpostazione delle caratteristiche particolari di ciascun lesystem si usa invece largomento data che serve per passare le ulteriori informazioni necessarie, che ovviamente variano da lesystem a lesystem. La funzione mount pu` essere utilizzata anche per eettuare il rimontaggio di un lesystem, o cosa che permette di cambiarne al volo alcune delle caratteristiche di funzionamento (ad esempio passare da sola lettura a lettura/scrittura). Questa operazione ` attivata attraverso uno dei bit e di mountflags, MS_REMOUNT, che se impostato specica che deve essere eettuato il rimontaggio del lesystem (con le opzioni specicate dagli altri bit), anche in questo caso il valore di source viene ignorato. Una volta che non si voglia pi` utilizzare un certo lesystem ` possibile smontarlo usando la u e funzione umount, il cui prototipo `: e
#include <sys/mount.h> umount(const char *target) Smonta il lesystem montato sulla directory target. La funzione ritorna 0 in caso di successo e -1 in caso di fallimento, nel qual caso errno assumer` a uno dei valori: EPERM EBUSY il processo non ha i privilegi di amministratore. target ` la directory di lavoro di qualche processo, o contiene dei le aperti, o un e altro mount point.

ed inoltre ENOTDIR, EFAULT, ENOMEM, ENAMETOOLONG, ENOENT o ELOOP.

la funzione prende il nome della directory su cui il lesystem ` montato e non il le o il dispositivo e 9 in quanto con il kernel 2.4.x ` possibile montare lo stesso dispositivo in pi` che ` stato montato, e e u punti. Nel caso pi` di un lesystem sia stato montato sullo stesso mount point viene smontato u quello che ` stato montato per ultimo. e Si tenga presente che la funzione fallisce quando il lesystem ` occupato, questo avviene e quando ci sono ancora le aperti sul lesystem, se questo contiene la directory di lavoro corrente
cio` un numero speciale usato come identicativo, che nel caso ` 0xC0ED; si pu` usare la costante MS_MGC_MSK e e o per ottenere la parte di mountflags riservata al magic number. 9 questo ` vero a partire dal kernel 2.3.99-pre7, prima esistevano due chiamate separate e la funzione poteva e essere usata anche specicando il le di dispositivo.
8

8.2. OPZIONI E CONFIGURAZIONE DEL SISTEMA

187

di un qualunque processo o il mount point di un altro lesystem; in questo caso lerrore restituito ` EBUSY. e Linux provvede inoltre una seconda funzione, umount2, che in alcuni casi permette di forzare lo smontaggio di un lesystem, anche quando questo risulti occupato; il suo prototipo `: e
#include <sys/mount.h> umount2(const char *target, int flags) La funzione ` identica a umount per comportamento e codici di errore, ma con flags si pu` e o specicare se forzare lo smontaggio.

Il valore di flags ` una maschera binaria, e al momento lunico valore denito ` il bit e e MNT_FORCE; gli altri bit devono essere nulli. Specicando MNT_FORCE la funzione cercher` di a liberare il lesystem anche se ` occupato per via di una delle condizioni descritte in precedenza. e A seconda del tipo di lesystem alcune (o tutte) possono essere superate, evitando lerrore di EBUSY. In tutti i casi prima dello smontaggio viene eseguita una sincronizzazione dei dati. Altre due funzioni speciche di Linux,10 utili per ottenere in maniera diretta informazioni riguardo al lesystem su cui si trova un certo le, sono statfs e fstatfs, i cui prototipi sono:
#include <sys/vfs.h> int statfs(const char *path, struct statfs *buf) int fstatfs(int fd, struct statfs *buf) Restituisce in buf le informazioni relative al lesystem su cui ` posto il le specicato. e Le funzioni ritornano 0 in caso di successo e -1 in caso di errore, nel qual caso errno assumer` a uno dei valori: ENOSYS il lesystem su cui si trova il le specicato non supporta la funzione. e EFAULT ed EIO per entrambe, EBADF per fstatfs, ENOTDIR, ENAMETOOLONG, ENOENT, EACCES, ELOOP per statfs.

Queste funzioni permettono di ottenere una serie di informazioni generali riguardo al lesystem su cui si trova il le specicato; queste vengono restituite allindirizzo buf di una struttura statfs denita come in g. 8.2, ed i campi che sono indeniti per il lesystem in esame sono impostati a zero. I valori del campo f_type sono deniti per i vari lesystem nei relativi le di header dei sorgenti del kernel da costanti del tipo XXX_SUPER_MAGIC, dove XXX in genere ` il e nome del lesystem stesso.
struct statfs { long f_type ; long f_bsize ; long f_blocks ; long f_bfree ; long f_bavail ; long f_files ; long f_ffree ; fsid_t f_fsid ; long f_namelen ; long f_spare [6]; };

/* /* /* /* /* /* /* /* /* /*

tipo di filesystem */ dimensione ottimale dei blocchi di I / O */ blocchi totali nel filesystem */ blocchi liberi nel filesystem */ blocchi liberi agli utenti normali */ inode totali nel filesystem */ inode liberi nel filesystem */ filesystem id */ lunghezza massima dei nomi dei file */ riservati per uso futuro */

Figura 8.2: La struttura statfs.

Le glibc provvedono inne una serie di funzioni per la gestione dei due le /etc/fstab ed /etc/mtab, che convenzionalmente sono usati in quasi tutti i sistemi unix-like per mantenere
10

esse si trovano anche su BSD, ma con una struttura diversa.

188

CAPITOLO 8. LA GESTIONE DEL SISTEMA, DEL TEMPO E DEGLI ERRORI

rispettivamente le informazioni riguardo ai lesystem da montare e a quelli correntemente montati. Le funzioni servono a leggere il contenuto di questi le in opportune strutture fstab e mntent, e, per /etc/mtab per inserire e rimuovere le voci presenti nel le. In generale si dovrebbero usare queste funzioni (in particolare quelle relative a /etc/mtab), quando si debba scrivere un programma che eettua il montaggio di un lesystem; in realt` in questi casi ` molto pi` semplice invocare direttamente il programma mount, per cui a e u ne tralasceremo la trattazione, rimandando al manuale delle glibc [4] per la documentazione completa.

8.2.3

La gestione delle informazioni su utenti e gruppi

Tradizionalmente le informazioni utilizzate nella gestione di utenti e gruppi (password, corrispondenze fra nomi simbolici e user-id, home directory, ecc.) venivano registrate allinterno dei due le di testo /etc/passwd ed /etc/group,11 il cui formato ` descritto dalle relative pagine e del manuale12 e tutte le funzioni che richiedevano laccesso a queste informazione andavano a leggere direttamente il contenuto di questi le. Col tempo per` questa impostazione ha incominciato a mostrare dei limiti: da una parte il o meccanismo classico di autenticazione ` stato ampliato, ed oggi la maggior parte delle distribue zioni di GNU/Linux usa la libreria PAM (sigla che sta per Pluggable Authentication Method ) che fornisce una interfaccia comune per i processi di autenticazione,13 svincolando completamente le singole applicazione dai dettagli del come questa viene eseguita e di dove vengono mantenuti i dati relativi; dallaltra con il diondersi delle reti la necessit` di centralizzare le informazioni a degli utenti e dei gruppi per insiemi di macchine, in modo da mantenere coerenti i dati, ha portato anche alla necessit` di poter recuperare e memorizzare dette informazioni su supporti a diversi, introducendo il sistema del Name Service Switch che tratteremo brevemente pi` avanti u (in sez. 17.1.1) dato che la maggior parte delle sua applicazioni sono relative alla risoluzioni di nomi di rete. In questo paragrafo ci limiteremo comunque a trattare le funzioni classiche per la lettura delle informazioni relative a utenti e gruppi tralasciando completamente quelle relative allautenticazione. Le prime funzioni che vedremo sono quelle previste dallo standard POSIX.1; queste sono del tutto generiche e si appoggiano direttamente al Name Service Switch, per cui sono in grado di ricevere informazioni qualunque sia il supporto su cui esse vengono mantenute. Per leggere le informazioni relative ad un utente si possono usare due funzioni, getpwuid e getpwnam, i cui prototipi sono:
#include <pwd.h> #include <sys/types.h> struct passwd *getpwuid(uid_t uid) struct passwd *getpwnam(const char *name) Restituiscono le informazioni relative allutente specicato. Le funzioni ritornano il puntatore alla struttura contenente le informazioni in caso di successo e NULL nel caso non sia stato trovato nessun utente corrispondente a quanto specicato. in realt` oltre a questi nelle distribuzioni pi` recenti ` stato introdotto il sistema delle shadow password che a u e prevede anche i due le /etc/shadow e /etc/gshadow, in cui sono state spostate le informazioni di autenticazione (ed inserite alcune estensioni) per toglierle dagli altri le che devono poter essere letti per poter eettuare lassociazione fra username e uid. 12 nella quinta sezione, quella dei le di congurazione, occorre cio` usare man 5 passwd dato che altrimenti si e avrebbe la pagina di manuale del comando passwd. 13 il Pluggable Authentication Method ` un sistema modulare, in cui ` possibile utilizzare anche pi` meccanismi e e u insieme, diventa cos` possibile avere vari sistemi di riconoscimento (biometria, chiavi hardware, ecc.), diversi formati per le password e diversi supporti per le informazioni, il tutto in maniera trasparente per le applicazioni purch per ciascun meccanismo si disponga della opportuna libreria che implementa linterfaccia di PAM. e
11

8.2. OPZIONI E CONFIGURAZIONE DEL SISTEMA

189

Le due funzioni forniscono le informazioni memorizzate nel registro degli utenti (che nelle versioni pi` recenti possono essere ottenute attraverso PAM) relative allutente specicato atu traverso il suo uid o il nome di login. Entrambe le funzioni restituiscono un puntatore ad una struttura di tipo passwd la cui denizione (anchessa eseguita in pwd.h) ` riportata in g. 8.3, e dove ` pure brevemente illustrato il signicato dei vari campi. e
struct passwd { char * pw_name ; char * pw_passwd ; uid_t pw_uid ; gid_t pw_gid ; char * pw_gecos ; char * pw_dir ; char * pw_shell ; };

/* /* /* /* /* /* /*

user name */ user password */ user id */ group id */ real name */ home directory */ shell program */

Figura 8.3: La struttura passwd contenente le informazioni relative ad un utente del sistema.

La struttura usata da entrambe le funzioni ` allocata staticamente, per questo motivo viene e sovrascritta ad ogni nuova invocazione, lo stesso dicasi per la memoria dove sono scritte le stringhe a cui i puntatori in essa contenuti fanno riferimento. Ovviamente questo implica che dette funzioni non possono essere rientranti; per questo motivo ne esistono anche due versioni alternative (denotate dalla solita estensione _r), i cui prototipi sono:
#include <pwd.h> #include <sys/types.h> struct passwd *getpwuid_r(uid_t uid, struct passwd *password, char *buffer, size_t buflen, struct passwd **result) struct passwd *getpwnam_r(const char *name, struct passwd *password, char *buffer, size_t buflen, struct passwd **result) Restituiscono le informazioni relative allutente specicato. Le funzioni ritornano 0 in caso di successo e un codice derrore altrimenti, nel qual caso errno sar` impostata opportunamente. a

In questo caso luso ` molto pi` complesso, in quanto bisogna prima allocare la memoria e u necessaria a contenere le informazioni. In particolare i valori della struttura passwd saranno restituiti allindirizzo password mentre la memoria allocata allindirizzo buffer, per un massimo di buflen byte, sar` utilizzata per contenere le stringhe puntate dai campi di password. Inne a allindirizzo puntato da result viene restituito il puntatore ai dati ottenuti, cio` buffer nel caso e lutente esista, o NULL altrimenti. Qualora i dati non possano essere contenuti nei byte specicati da buflen, la funzione fallir` restituendo ERANGE (e result sar` comunque impostato a NULL). a a Del tutto analoghe alle precedenti sono le funzioni getgrnam e getgrgid (e le relative analoghe rientranti con la stessa estensione _r) che permettono di leggere le informazioni relative ai gruppi, i loro prototipi sono:
#include <grp.h> #include <sys/types.h> struct group *getgrgid(gid_t gid) struct group *getgrnam(const char *name) struct group *getpwuid_r(gid_t gid, struct group *password, char *buffer, size_t buflen, struct group **result) struct group *getpwnam_r(const char *name, struct group *password, char *buffer, size_t buflen, struct group **result) Restituiscono le informazioni relative al gruppo specicato. Le funzioni ritornano 0 in caso di successo e un codice derrore altrimenti, nel qual caso errno sar` impostata opportunamente. a

190

CAPITOLO 8. LA GESTIONE DEL SISTEMA, DEL TEMPO E DEGLI ERRORI

Il comportamento di tutte queste funzioni ` assolutamente identico alle precedenti che leggono e le informazioni sugli utenti, lunica dierenza ` che in questo caso le informazioni vengono e restituite in una struttura di tipo group, la cui denizione ` riportata in g. 8.4. e

struct group { char * gr_name ; char * gr_passwd ; gid_t gr_gid ; char ** gr_mem ; };

/* /* /* /*

group group group group

name */ password */ id */ members */

Figura 8.4: La struttura group contenente le informazioni relative ad un gruppo del sistema.

Le funzioni viste nora sono in grado di leggere le informazioni sia direttamente dal le delle password in /etc/passwd che tramite il sistema del Name Service Switch e sono completamente generiche. Si noti per` che non c` una funzione che permetta di impostare direttamente una o e 14 Dato che POSIX non prevede questa possibilit` esiste unaltra interfaccia che lo password. a fa, derivata da SVID le cui funzioni sono riportate in tab. 8.10. Questa per` funziona soltanto o quando le informazioni sono mantenute su un apposito le di registro di utenti e gruppi, con il formato classico di /etc/passwd e /etc/group.
Funzione fgetpwent fgetpwent_r putpwent getpwent getpwent_r setpwent endpwent fgetgrent fgetgrent_r putgrent getgrent getgrent_r setgrent endgrent Signicato Legge una voce dal le di registro degli utenti specicato. Come la precedente, ma rientrante. Immette una voce in un le di registro degli utenti. Legge una voce da /etc/passwd. Come la precedente, ma rientrante. Ritorna allinizio di /etc/passwd. Chiude /etc/passwd. Legge una voce dal le di registro dei gruppi specicato. Come la precedente, ma rientrante. Immette una voce in un le di registro dei gruppi. Legge una voce da /etc/group. Come la precedente, ma rientrante. Ritorna allinizio di /etc/group. Chiude /etc/group.

Tabella 8.10: Funzioni per la manipolazione dei campi di un le usato come registro per utenti o gruppi nel formato di /etc/passwd e /etc/group.

Dato che oramai la gran parte delle distribuzioni di GNU/Linux utilizzano almeno le shadow password (quindi con delle modiche rispetto al formato classico del le /etc/passwd), si tenga presente che le funzioni di questa interfaccia che permettono di scrivere delle voci in un registro degli utenti (cio` putpwent e putgrent) non hanno la capacit` di farlo specicando tutti i e a contenuti necessari rispetto a questa estensione. Per questo motivo luso di queste funzioni ` e deprecato, in quanto comunque non funzionale, pertanto ci limiteremo a fornire soltanto lelenco di tab. 8.10, senza nessuna spiegazione ulteriore. Chi volesse insistere ad usare questa interfaccia pu` fare riferimento alle pagine di manuale delle rispettive funzioni ed al manuale delle glibc o per i dettagli del funzionamento.
14

in realt` questo pu` essere fatto ricorrendo a PAM, ma questo ` un altro discorso. a o e

8.2. OPZIONI E CONFIGURAZIONE DEL SISTEMA

191

8.2.4

Il registro della contabilit` degli utenti a

Lultimo insieme di funzioni relative alla gestione del sistema che esamineremo ` quello che e permette di accedere ai dati del registro della cosiddetta contabilit` (o accounting) degli utenti. a In esso vengono mantenute una serie di informazioni storiche relative sia agli utenti che si sono collegati al sistema, (tanto per quelli correntemente collegati, che per la registrazione degli accessi precedenti), sia relative allintero sistema, come il momento di lancio di processi da parte di init, il cambiamento dellorologio di sistema, il cambiamento di runlevel o il riavvio della macchina. I dati vengono usualmente15 memorizzati nei due le /var/run/utmp e /var/log/wtmp.16 Quando un utente si collega viene aggiunta una voce a /var/run/utmp in cui viene memorizzato il nome di login, il terminale da cui ci si collega, luid della shell di login, lorario della connessione ed altre informazioni. La voce resta nel le no al logout, quando viene cancellata e spostata in /var/log/wtmp. In questo modo il primo le viene utilizzato per registrare chi sta utilizzando il sistema al momento corrente, mentre il secondo mantiene la registrazione delle attivit` degli utenti. A a questultimo vengono anche aggiunte delle voci speciali per tenere conto dei cambiamenti del sistema, come la modica del runlevel, il riavvio della macchina, ecc. Tutte queste informazioni sono descritte in dettaglio nel manuale delle glibc. Questi le non devono mai essere letti direttamente, ma le informazioni che contengono possono essere ricavate attraverso le opportune funzioni di libreria. Queste sono analoghe alle precedenti funzioni (vedi tab. 8.10) usate per accedere al registro degli utenti, solo che in questo caso la struttura del registro della contabilit` ` molto pi` complessa, dato che contiene diversi ae u tipi di informazione. Le prime tre funzioni, setutent, endutent e utmpname servono rispettivamente a aprire e a chiudere il le che contiene il registro, e a specicare su quale le esso viene mantenuto. I loro prototipi sono:
#include <utmp.h> void utmpname(const char *file) Specica il le da usare come registro. void setutent(void) Apre il le del registro, posizionandosi al suo inizio. void endutent(void) Chiude il le del registro. Le funzioni non ritornano codici di errore.

e si tenga presente che le funzioni non restituiscono nessun valore, pertanto non ` possie bile accorgersi di eventuali errori (ad esempio se si ` impostato un nome di le sbagliato con e utmpname). Nel caso non si sia utilizzata utmpname per specicare un le di registro alternativo, sia setutent che endutent operano usando il default che ` /var/run/utmp. Il nome di questo e le, cos` come una serie di altri valori di default per i pathname di uso pi` comune, viene u mantenuto nei valori di una serie di costanti denite includendo paths.h, in particolare quelle che ci interessano sono: _PATH_UTMP specica il le che contiene il registro per gli utenti correntemente collegati; questo ` il valore che viene usato se non si ` utilizzato utmpname per modicarlo. e e _PATH_WTMP specica il le che contiene il registro per larchivio storico degli utenti collegati.
questa ` la locazione specicata dal Linux Filesystem Hierarchy Standard, adottato dalla gran parte delle e distribuzioni. 16 non si confonda questultimo con il simile /var/log/btmp dove invece vengono memorizzati dal programma di login tutti tentativi di accesso fallito.
15

192

CAPITOLO 8. LA GESTIONE DEL SISTEMA, DEL TEMPO E DEGLI ERRORI

che nel caso di Linux hanno un valore corrispondente ai le /var/run/utmp e /var/log/wtmp citati in precedenza. Una volta aperto il le del registro degli utenti si pu` eseguire una scansione leggendo o o scrivendo una voce con le funzioni getutent, getutid, getutline e pututline, i cui prototipi sono:
#include <utmp.h> struct utmp *getutent(void) Legge una voce dalla posizione corrente nel registro. struct utmp *getutid(struct utmp *ut) Ricerca una voce sul registro in base al contenuto di ut. struct utmp *getutline(struct utmp *ut) Ricerca nel registro la prima voce corrispondente ad un processo sulla linea di terminale specicata tramite ut. struct utmp *pututline(struct utmp *ut) Scrive una voce nel registro. Le funzioni ritornano il puntatore ad una struttura utmp in caso di successo e NULL in caso di errore.

Tutte queste funzioni fanno riferimento ad una struttura di tipo utmp, la cui denizione in Linux ` riportata in g. 8.5. Le prime tre funzioni servono per leggere una voce dal registro; e getutent legge semplicemente la prima voce disponibile; le altre due permettono di eseguire una ricerca.
struct utmp { short int ut_type ; pid_t ut_pid ; char ut_line [ UT_LINESIZE ]; char ut_id [4]; char ut_user [ UT_NAMESIZE ]; char ut_host [ UT_HOSTSIZE ]; struct exit_status ut_exit ; long int ut_session ; struct timeval ut_tv ; int32_t ut_addr_v6 [4]; char __unused [20]; };

/* /* /* /* /* /* /* /* /* /* /*

Type of login . */ Process ID of login process . */ Devicename . */ Inittab ID . */ Username . */ Hostname for remote login . */ Exit status of a process marked as DEAD_PROCESS . */ Session ID , used for windowing . */ Time entry was made . */ Internet address of remote host . */ Reserved for future use . */

Figura 8.5: La struttura utmp contenente le informazioni di una voce del registro di contabilit`. a

Con getutid si pu` cercare una voce specica, a seconda del valore del campo ut_type o dellargomento ut. Questo pu` assumere i valori riportati in tab. 8.11, quando assume i valori o RUN_LVL, BOOT_TIME, OLD_TIME, NEW_TIME, verr` restituito la prima voce che corrisponde al tipo a determinato; quando invece assume i valori INIT_PROCESS, LOGIN_PROCESS, USER_PROCESS o DEAD_PROCESS verr` restituita la prima voce corrispondente al valore del campo ut_id specicato a in ut. La funzione getutline esegue la ricerca sulle voci che hanno ut_type uguale a LOGIN_PROCESS o USER_PROCESS, restituendo la prima che corrisponde al valore di ut_line, che specica il device17 di terminale che interessa. Lo stesso criterio di ricerca ` usato da pututline per trovare e uno spazio dove inserire la voce specicata, qualora non sia trovata la voce viene aggiunta in coda al registro.
17

espresso senza il /dev/ iniziale.

8.3. IL CONTROLLO DELLUSO DELLE RISORSE


Valore EMPTY RUN_LVL BOOT_TIME OLD_TIME NEW_TIME INIT_PROCESS LOGIN_PROCESS USER_PROCESS DEAD_PROCESS Signicato Non contiene informazioni valide. Identica il runlevel del sistema. Identica il tempo di avvio del sistema. Identica quando ` stato modicato lorologio di sistema. e Identica da quanto ` stato modicato il sistema. e Identica un processo lanciato da init. Identica un processo di login. Identica un processo utente. Identica un processo terminato.

193

Tabella 8.11: Classicazione delle voci del registro a seconda dei possibili valori del campo ut_type.

In generale occorre per` tenere conto che queste funzioni non sono completamente standaro dizzate, e che in sistemi diversi possono esserci dierenze; ad esempio pututline restituisce void in vari sistemi (compreso Linux, no alle libc5). Qui seguiremo la sintassi fornita dalle glibc, ma gli standard POSIX 1003.1-2001 e XPG4.2 hanno introdotto delle nuove strutture (e relativi le) di tipo utmpx, che sono un sovrainsieme di utmp. Le glibc utilizzano gi` una versione estesa di utmp, che rende inutili queste nuove strutture; a pertanto esse e le relative funzioni di gestione (getutxent, getutxid, getutxline, pututxline, setutxent e endutxent) sono ridenite come sinonimi delle funzioni appena viste. Come visto in sez. 8.2.3, luso di strutture allocate staticamente rende le funzioni di lettura non rientranti; per questo motivo le glibc forniscono anche delle versioni rientranti: getutent_r, getutid_r, getutline_r, che invece di restituire un puntatore restituiscono un intero e prendono due argomenti aggiuntivi. Le funzioni si comportano esattamente come le analoghe non rientranti, solo che restituiscono il risultato allindirizzo specicato dal primo argomento aggiuntivo (di tipo struct utmp *buffer) mentre il secondo (di tipo struct utmp **result) viene usato per restituire il puntatore allo stesso buer. Inne le glibc forniscono come estensione per la scrittura delle voci in wmtp altre due funzioni, updwtmp e logwtmp, i cui prototipi sono:
#include <utmp.h> void updwtmp(const char *wtmp_file, const struct utmp *ut) Aggiunge la voce ut nel registro wmtp. void logwtmp(const char *line, const char *name, const char *host) Aggiunge nel registro una voce con i valori specicati.

La prima funzione permette laggiunta di una voce a wmtp specicando direttamente una struttura utmp, mentre la seconda utilizza gli argomenti line, name e host per costruire la voce che poi aggiunge chiamando updwtmp.

8.3

Il controllo delluso delle risorse

Dopo aver esaminato le funzioni che permettono di controllare le varie caratteristiche, capacit` a e limiti del sistema a livello globale, in questa sezione tratteremo le varie funzioni che vengono usate per quanticare le risorse (CPU, memoria, ecc.) utilizzate da ogni singolo processo e quelle che permettono di imporre a ciascuno di essi vincoli e limiti di utilizzo.

8.3.1

Luso delle risorse

Come abbiamo accennato in sez. 3.2.4 le informazioni riguardo lutilizzo delle risorse da parte di un processo ` mantenuto in una struttura di tipo rusage, la cui denizione (che si trova in e sys/resource.h) ` riportata in g. 8.6. e

194

CAPITOLO 8. LA GESTIONE DEL SISTEMA, DEL TEMPO E DEGLI ERRORI

struct rusage { struct timeval ru_utime ; struct timeval ru_stime ; long ru_maxrss ; long ru_ixrss ; long ru_idrss ; long ru_isrss ; long ru_minflt ; long ru_majflt ; long ru_nswap ; long ru_inblock ; long ru_oublock ; long ru_msgsnd ; long ru_msgrcv ; long ru_nsignals ; ; long ru_nvcsw ; long ru_nivcsw ; };

/* /* /* /* /* /* /* /* /* /* /* /* /* /* /* /*

user time used */ system time used */ maximum resident set size */ integral shared memory size */ integral unshared data size */ integral unshared stack size */ page reclaims */ page faults */ swaps */ block input operations */ block output operations */ messages sent */ messages received */ signals received */ voluntary context switches */ involuntary context switches */

Figura 8.6: La struttura rusage per la lettura delle informazioni dei delle risorse usate da un processo.

La denizione della struttura in g. 8.6 ` ripresa da BSD 4.3,18 ma attualmente (con i kernel e della serie 2.4.x e 2.6.x) i soli campi che sono mantenuti sono: ru_utime, ru_stime, ru_minflt, ru_majflt, e ru_nswap. I primi due indicano rispettivamente il tempo impiegato dal processo nelleseguire le istruzioni in user space, e quello impiegato dal kernel nelle system call eseguite per conto del processo. Gli altri tre campi servono a quanticare luso della memoria virtuale e corrispondono rispettivamente al numero di page fault (vedi sez. 2.2.1) avvenuti senza richiedere I/O su disco (i cosiddetti minor page fault), a quelli che invece han richiesto I/O su disco (detti invece major page fault) ed al numero di volte che il processo ` stato completamente tolto dalla memoria per e essere inserito nello swap. In genere includere esplicitamente <sys/time.h> non ` pi` strettamente necessario, ma e u aumenta la portabilit`, e serve comunque quando, come nella maggior parte dei casi, si debba a accedere ai campi di rusage relativi ai tempi di utilizzo del processore, che sono deniti come strutture di tipo timeval. Questa ` la stessa struttura utilizzata da wait4 (si ricordi quando visto in sez. 3.2.4) per e ricavare la quantit` di risorse impiegate dal processo di cui si ` letto lo stato di terminazione, a e ma essa pu` anche essere letta direttamente utilizzando la funzione getrusage, il cui prototipo o `: e
#include <sys/time.h> #include <sys/resource.h> #include <unistd.h> int getrusage(int who, struct rusage *usage) Legge la quantit` di risorse usate da un processo. a La funzione ritorna 0 in caso di successo e -1 in caso di errore, nel qual caso errno pu` essere o EINVAL o EFAULT.

Largomento who permette di specicare il processo di cui si vuole leggere luso delle risorse; esso pu` assumere solo i due valori RUSAGE_SELF per indicare il processo corrente e o
questo non ha a nulla a che fare con il cosiddetto BSD accounting (vedi sez. 8.3.4) che si trova nelle opzioni di compilazione del kernel (e di norma ` disabilitato) che serve per mantenere una contabilit` delle risorse usate e a da ciascun processo in maniera molto pi` dettagliata. u
18

8.3. IL CONTROLLO DELLUSO DELLE RISORSE

195

RUSAGE_CHILDREN per indicare linsieme dei processi gli di cui si ` ricevuto lo stato di tere minazione.

8.3.2

Limiti sulle risorse

Come accennato nellintroduzione il kernel mette a disposizione delle funzionalit` che permettono a non solo di mantenere dati statistici relativi alluso delle risorse, ma anche di imporre dei limiti precisi sul loro utilizzo da parte dei vari processi o degli utenti. Per far questo esistono una serie di risorse e ad ogni processo vengono associati due diversi limiti per ciascuna di esse; questi sono il limite corrente (o current limit) che esprime un valore massimo che il processo non pu` superare ad un certo momento, ed il limite massimo (o maximum o limit) che invece esprime il valore massimo che pu` assumere il limite corrente. In generale il o primo viene chiamato anche soft limit dato che il suo valore pu` essere aumentato dal processo o stesso durante lesecuzione, ci` pu` per` essere fatto solo no al valore del secondo, che per o o o questo viene detto hard limit.
Valore RLIMIT_AS Signicato La dimensione massima della memoria virtuale di un processo, il cosiddetto Address Space, (vedi sez. 2.2.1). Se il limite viene superato dalluso di funzioni come brk, mremap o mmap esse falliranno con un errore di ENOMEM, mentre se il superamento viene causato dalla crescita dello stack il processo ricever` un segnale di SIGSEGV. a La massima dimensione per di un le di core dump (vedi sez. 9.2.2) creato nella terminazione di un processo; le di dimensioni maggiori verranno troncati a questo valore, mentre con un valore si bloccher` la creazione dei core dump. a Il massimo tempo di CPU (vedi sez. 8.4.2) che il processo pu` usare. Il superamento del o limite corrente comporta lemissione di un segnale di SIGXCPU la cui azione predenita (vedi sez. 9.2) ` terminare il processo. Il superamento del limite massimo comporta e lemissione di un segnale di SIGKILL.19 La massima dimensione del segmento dati di un processo (vedi sez. 2.2.2). Il tentativo di allocare pi` memoria di quanto indicato dal limite corrente causa il fallimento della u funzione di allocazione (brk o sbrk) con un errore di ENOMEM. La massima dimensione di un le che un processo pu` creare. Se il processo cerca di o scrivere oltre questa dimensione ricever` un segnale di SIGXFSZ, che di norma termina a il processo; se questo viene intercettato la system call che ha causato lerrore fallir` a con un errore di EFBIG. ` E un limite presente solo nelle prime versioni del kernel 2.4 sul numero massimo di le lock (vedi sez. 11.4) che un processo poteva eettuare. Lammontare massimo di memoria che pu` essere bloccata in RAM da un processo o (vedi sez. 2.2.4). Dal kernel 2.6.9 questo limite comprende anche la memoria che pu` o essere bloccata da ciascun utente nelluso della memoria condivisa (vedi sez. 12.2.6) che viene contabilizzata separatamente ma sulla quale viene applicato questo stesso limite. Il numero massimo di le che il processo pu` aprire. Lapertura di un ulteriore le o far` fallire la funzione (open, dup o pipe) con un errore EMFILE. a Il numero massimo di processi che possono essere creati sullo stesso user id real. Se il limite viene raggiunto fork fallir` con un EAGAIN. a Il numero massimo di segnali che possono essere mantenuti in coda per ciascun utente, considerando sia i segnali normali che real-time (vedi sez. 9.5.1). Il limite ` attivo solo e per sigqueue, con kill si potr` sempre inviare un segnale che non sia gi` presente a a su una coda.20 La massima dimensione dello stack del processo. Se il processo esegue operazioni che estendano lo stack oltre questa dimensione ricever` un segnale di SIGSEGV. a Lammontare massimo di pagine di memoria dato al testo del processo. Il limite ` solo e una indicazione per il kernel, qualora ci fosse un surplus di memoria questa verrebbe assegnata.

RLIMIT_CORE

RLIMIT_CPU

RLIMIT_DATA

RLIMIT_FSIZE

RLIMIT_LOCKS RLIMIT_MEMLOCK

RLIMIT_NOFILE RLIMIT_NPROC RLIMIT_SIGPENDING

RLIMIT_STACK RLIMIT_RSS

Tabella 8.12: Valori possibili dellargomento resource delle funzioni getrlimit e setrlimit.

196

CAPITOLO 8. LA GESTIONE DEL SISTEMA, DEL TEMPO E DEGLI ERRORI

In generale il superamento di un limite corrente21 comporta o lemissione di un segnale o il fallimento della system call che lo ha provocato;22 per permettere di leggere e di impostare i limiti di utilizzo delle risorse da parte di un processo sono previste due funzioni, getrlimit e setrlimit, i cui prototipi sono:
#include <sys/time.h> #include <sys/resource.h> #include <unistd.h> int getrlimit(int resource, struct rlimit *rlim) Legge il limite corrente per la risorsa resource. int setrlimit(int resource, const struct rlimit *rlim) Imposta il limite per la risorsa resource. Le funzioni ritornano 0 in caso di successo e -1 in caso di errore, nel qual caso errno assumer` a uno dei valori: EINVAL EPERM ed EFAULT. i valori per resource non sono validi. un processo senza i privilegi di amministratore ha cercato di innalzare i propri limiti.

Entrambe le funzioni permettono di specicare, attraverso largomento resource, su quale risorsa si vuole operare: i possibili valori di questo argomento sono elencati in tab. 8.12. Lacceso (rispettivamente in lettura e scrittura) ai valori eettivi dei limiti viene poi eettuato attraverso la struttura rlimit puntata da rlim, la cui denizione ` riportata in g. 8.7, ed i cui campi e corrispondono appunto a limite corrente e limite massimo.
struct rlimit { rlim_t rlim_cur ; rlim_t rlim_max ; };

/* Soft limit */ /* Hard limit ( ceiling for rlim_cur ) */

Figura 8.7: La struttura rlimit per impostare i limiti di utilizzo delle risorse usate da un processo.

Nello specicare un limite, oltre a fornire dei valori specici, si pu` anche usare la costante o RLIM_INFINITY che permette di sbloccare luso di una risorsa; ma si ricordi che solo un processo con i privilegi di amministratore23 pu` innalzare un limite al di sopra del valore corrente del o limite massimo ed usare un valore qualsiasi per entrambi i limiti. Si tenga conto inne che tutti i limiti vengono ereditati dal processo padre attraverso una fork (vedi sez. 3.2.2) e mantenuti per gli altri programmi eseguiti attraverso una exec (vedi sez. 3.2.5).

8.3.3

Le risorse di memoria e processore

La gestione della memoria ` gi` stata arontata in dettaglio in sez. 2.2; abbiamo visto allora che e a il kernel provvede il meccanismo della memoria virtuale attraverso la divisione della memoria sica in pagine.
18 questo ` quanto avviene per i kernel dalla serie 2.2 no ad oggi (la 2.6.x); altri kernel possono avere come portamenti diversi per quanto avviene quando viene superato il soft limit; perci` per avere operazioni portabili ` o e sempre opportuno intercettare SIGXCPU e terminare in maniera ordinata il processo. 20 il limite su questa risorsa ` stato introdotto con il kernel 2.6.8. e 21 di norma quanto riportato in tab. 8.12 fa riferimento a quanto avviene al superamento del limite corrente, con leccezione RLIMIT_CPU in cui si ha in comportamento diverso per il superamento dei due limiti. 22 si nuovo c` una eccezione per RLIMIT_CORE che inuenza soltanto la dimensione (o leventuale creazione) dei e le di core dump. 23 per essere precisi in questo caso quello che serve ` la capability CAP_SYS_RESOURCE. e

8.3. IL CONTROLLO DELLUSO DELLE RISORSE

197

In genere tutto ci` ` del tutto trasparente al singolo processo, ma in certi casi, come per o e lI/O mappato in memoria (vedi sez. 11.3.1) che usa lo stesso meccanismo per accedere ai le, ` e necessario conoscere le dimensioni delle pagine usate dal kernel. Lo stesso vale quando si vuole gestire in maniera ottimale linterazione della memoria che si sta allocando con il meccanismo della paginazione. Di solito la dimensione delle pagine di memoria ` ssata dallarchitettura hardware, per cui e il suo valore di norma veniva mantenuto in una costante che bastava utilizzare in fase di compilazione, ma oggi, con la presenza di alcune architetture (ad esempio Sun Sparc) che permettono di variare questa dimensione, per non dover ricompilare i programmi per ogni possibile modello e scelta di dimensioni, ` necessario poter utilizzare una funzione. e Dato che si tratta di una caratteristica generale del sistema, questa dimensione pu` essere o ottenuta come tutte le altre attraverso una chiamata a sysconf, 24 ma in BSD 4.2 ` stata e introdotta una apposita funzione, getpagesize, che restituisce la dimensione delle pagine di memoria; il suo prototipo `: e
#include <unistd.h> int getpagesize(void) Legge le dimensioni delle pagine di memoria. La funzione ritorna la dimensione di una pagina in byte, e non sono previsti errori.

La funzione ` prevista in SVr4, BSD 4.4 e SUSv2, anche se questo ultimo standard la etichetta e come obsoleta, mentre lo standard POSIX 1003.1-2001 la ha eliminata. In Linux ` implementata e come una system call nelle architetture in cui essa ` necessaria, ed in genere restituisce il valore e del simbolo PAGE_SIZE del kernel, che dipende dalla architettura hardware, anche se le versioni delle librerie del C precedenti le glibc 2.1 implementavano questa funzione restituendo sempre un valore statico. Le glibc forniscono, come specica estensione GNU, altre due funzioni, get_phys_pages e get_avphys_pages che permettono di ottenere informazioni riguardo la memoria; i loro prototipi sono:
#include <sys/sysinfo.h> long int get_phys_pages(void) Legge il numero totale di pagine di memoria disponibili per il sistema. long int get_avphys_pages(void) Legge il numero di pagine di memoria disponibili nel sistema. Le funzioni restituiscono un numero di pagine.

Queste funzioni sono equivalenti alluso della funzione sysconf rispettivamente con i parametri _SC_PHYS_PAGES e _SC_AVPHYS_PAGES. La prima restituisce il numero totale di pagine corrispondenti alla RAM della macchina; la seconda invece la memoria eettivamente disponibile per i processi. Le glibc supportano inoltre, come estensioni GNU, due funzioni che restituiscono il numero di processori della macchina (e quello dei processori attivi); anche queste sono informazioni comunque ottenibili attraverso sysconf utilizzando rispettivamente i parametri _SC_NPROCESSORS_CONF e _SC_NPROCESSORS_ONLN. Inne le glibc riprendono da BSD la funzione getloadavg che permette di ottenere il carico di processore della macchina, in questo modo ` possibile prendere decisioni su quando far partire e eventuali nuovi processi. Il suo prototipo `: e
#include <stdlib.h> int getloadavg(double loadavg[], int nelem) Legge il carico medio della macchina. La funzione ritorna il numero di elementi scritti o -1 in caso di errore.
24

nel caso specico si dovrebbe utilizzare il parametro _SC_PAGESIZE.

198

CAPITOLO 8. LA GESTIONE DEL SISTEMA, DEL TEMPO E DEGLI ERRORI

La funzione restituisce in ciascun elemento di loadavg il numero medio di processi attivi sulla coda dello scheduler, calcolato su diversi intervalli di tempo. Il numero di intervalli che si vogliono leggere ` specicato da nelem, dato che nel caso di Linux il carico viene valutato solo e su tre intervalli (corrispondenti a 1, 5 e 15 minuti), questo ` anche il massimo valore che pu` e o essere assegnato a questo argomento.

8.3.4

La contabilit` in stile BSD a

Una ultima modalit` per monitorare luso delle risorse `, se si ` compilato il kernel con il relativo a e e supporto,25 quella di attivare il cosiddetto BSD accounting, che consente di registrare su le una serie di informazioni26 riguardo alla contabilit` delle risorse utilizzate da ogni processo che viene a terminato. Linux consente di salvare la contabilit` delle informazioni relative alle risorse utilizzate dai a processi grazie alla funzione acct, il cui prototipo `: e
#include <unistd.h> int acct(const char *filename) Abilita il BSD accounting. La funzione ritorna 0 in caso di successo o 1 in caso di errore, nel qual caso errno assumer` uno a dei valori: EACCESS EPERM ENOSYS EUSER non si hanno i permessi per accedere a pathname. il processo non ha privilegi sucienti ad abilitare il BSD accounting. il kernel non supporta il BSD accounting. non sono disponibili nel kernel strutture per il le o si ` nita la memoria. e

ed inoltre EFAULT, EIO, ELOOP, ENAMETOOLONG, ENFILE, ENOENT, ENOMEM, ENOTDIR, EROFS.

La funzione attiva il salvataggio dei dati sul le indicato dal pathname contenuti nella stringa puntata da filename; la funzione richiede che il processo abbia i privilegi di amministratore (` necessaria la capability CAP_SYS_PACCT, vedi sez. 3.3.4). Se si specica il valore NULL per e filename il BSD accounting viene invece disabilitato. Un semplice esempio per luso di questa funzione ` riportato nel programma AcctCtrl.c dei sorgenti allegati alla guida. e Quando si attiva la contabilit`, il le che si indica deve esistere; esso verr` aperto in sola a a scrittura;27 le informazioni verranno registrate in append in coda al le tutte le volte che un processo termina. Le informazioni vengono salvate in formato binario, e corrispondono al contenuto della apposita struttura dati denita allinterno del kernel. Il funzionamento di acct viene inoltre modicato da uno specico parametro di sistema, modicabile attraverso /proc/sys/kernel/acct (o tramite la corrispondente sysctl). Esso contiene tre valori interi, il primo indica la percentuale di spazio disco libero sopra il quale viene ripresa una registrazione che era stata sospesa per essere scesi sotto il minimo indicato dal secondo valore (sempre in percentuale di spazio disco libero). Inne lultimo valore indica la frequenza in secondi con cui deve essere controllata detta percentuale.

8.4

La gestione dei tempi del sistema

In questa sezione, una volta introdotti i concetti base della gestione dei tempi da parte del sistema, tratteremo le varie funzioni attinenti alla gestione del tempo in un sistema unix-like, a partire da quelle per misurare i veri tempi di sistema associati ai processi, a quelle per convertire
25

se cio` si ` abilitata lopzione di compilazione CONFIG_BSD_PROCESS_ACCT. e e contenute nella struttura acct denita nel le include/linux/acct.h dei sorgenti del kernel. 27 si applicano al pathname indicato da filename tutte le restrizioni viste in cap. 4.
26

8.4. LA GESTIONE DEI TEMPI DEL SISTEMA

199

i vari tempi nelle dierenti rappresentazioni che vengono utilizzate, a quelle della gestione di data e ora.

8.4.1

La misura del tempo in Unix

Storicamente i sistemi unix-like hanno sempre mantenuto due distinti tipi di dati per la misure dei tempi allinterno del sistema: essi sono rispettivamente chiamati calendar time e process time, secondo le denizioni: calendar time ` detto anche tempo di calendario. E il numero di secondi dalla mezzanotte del primo gennaio 1970, in tempo universale coordinato (o UTC), data che viene usualmente indicata con 00:00:00 Jan, 1 1970 (UTC) e chiamata the Epoch. Questo tempo viene anche chiamato anche GMT (Greenwich Mean Time) dato che lUTC corrisponde ` allora locale di Greenwich. E il tempo su cui viene mantenuto lorologio del kernel, e viene usato ad esempio per indicare le date di modica dei le o quelle di avvio dei processi. Per memorizzare questo tempo ` stato riservato il tipo primitivo time_t. e process time detto talvolta tempo di processore. Viene misurato in clock tick. Un tempo questo corrispondeva al numero di interruzioni eettuate dal timer di sistema, adesso lo standard POSIX richiede che esso sia pari al valore della costante CLOCKS_PER_SEC, che deve essere denita come 1000000, qualunque sia la risoluzione reale dellorologio di sistema e la frequenza delle interruzioni del timer.28 Il dato primitivo usato per questo tempo ` clock_t, che ha quindi una risoluzione del microsecondo. Il numero di tick e al secondo pu` essere ricavato anche attraverso sysconf (vedi sez. 8.1.2). Il vecchio o simbolo CLK_TCK denito in time.h ` ormai considerato obsoleto. e In genere si usa il calendar time per esprimere le date dei le e le informazioni analoghe che riguardano i cosiddetti tempi di orologio, che vengono usati ad esempio per i demoni che compiono lavori amministrativi ad ore denite, come cron. Di solito questo tempo viene convertito automaticamente dal valore in UTC al tempo locale, utilizzando le opportune informazioni di localizzazione (specicate in /etc/timezone). E da tenere presente che questo tempo ` mantenuto dal sistema e non ` detto che corrisponda al e e tempo tenuto dallorologio hardware del calcolatore. Anche il process time di solito si esprime in secondi, ma fornisce una precisione ovviamente superiore al calendar time (che ` mantenuto dal sistema con una granularit` di un secondo) e e a viene usato per tenere conto dei tempi di esecuzione dei processi. Per ciascun processo il kernel calcola tre tempi diversi: clock time il tempo reale (viene chiamato anche wall clock time o elapsed time) passato dallavvio del processo. Chiaramente tale tempo dipende anche dal carico del sistema e da quanti altri processi stavano girando nello stesso periodo. user time il tempo eettivo che il processore ha impiegato nellesecuzione delle istruzioni del ` processo in user space. E quello riportato nella risorsa ru_utime di rusage vista in sez. 8.3.1. system time il tempo eettivo che il processore ha impiegato per eseguire codice delle system call ` nel kernel per conto del processo. E quello riportato nella risorsa ru_stime di rusage vista in sez. 8.3.1.
28

questultima, come accennato in sez. 3.1.1, ` invece data dalla costante HZ. e

200

CAPITOLO 8. LA GESTIONE DEL SISTEMA, DEL TEMPO E DEGLI ERRORI

In genere la somma di user time e system time indica il tempo di processore totale che il sistema ha eettivamente utilizzato per eseguire un certo processo, questo viene chiamato anche CPU time o tempo di CPU. Si pu` ottenere un riassunto dei valori di questi tempi quando si o esegue un qualsiasi programma lanciando questultimo come argomento del comando time.

8.4.2

La gestione del process time

Di norma tutte le operazioni del sistema fanno sempre riferimento al calendar time, luso del process time ` riservato a quei casi in cui serve conoscere i tempi di esecuzione di un processo e (ad esempio per valutarne lecienza). In tal caso infatti fare ricorso al calendar time ` inutile e in quanto il tempo pu` essere trascorso mentre un altro processo era in esecuzione o in attesa o del risultato di una operazione di I/O. La funzione pi` semplice per leggere il process time di un processo ` clock, che da una u e valutazione approssimativa del tempo di CPU utilizzato dallo stesso; il suo prototipo `: e
#include <time.h> clock_t clock(void) Legge il valore corrente del tempo di CPU. La funzione ritorna il tempo di CPU usato dal programma e -1 in caso di errore.

La funzione restituisce il tempo in clock tick, quindi se si vuole il tempo in secondi occorre dividere il risultato per la costante CLOCKS_PER_SEC.29 In genere clock_t viene rappresentato come intero a 32 bit, il che comporta un valore massimo corrispondente a circa 72 minuti, dopo i quali il contatore riprender` lo stesso valore iniziale. a Come accennato in sez. 8.4.1 il tempo di CPU ` la somma di altri due tempi, luser time ed e il system time che sono quelli eettivamente mantenuti dal kernel per ciascun processo. Questi possono essere letti attraverso la funzione times, il cui prototipo `: e
#include <sys/times.h> clock_t times(struct tms *buf) Legge in buf il valore corrente dei tempi di processore. La funzione ritorna il numero di clock tick dallavvio del sistema in caso di successo e -1 in caso di errore.

La funzione restituisce i valori di process time del processo corrente in una struttura di tipo tms, la cui denizione ` riportata in g. 8.8. La struttura prevede quattro campi; i primi due, e tms_utime e tms_stime, sono luser time ed il system time del processo, cos` come deniti in sez. 8.4.1.
struct tms { clock_t clock_t clock_t clock_t };

tms_utime ; tms_stime ; tms_cutime ; tms_cstime ;

/* /* /* /*

user time */ system time */ user time of children */ system time of children */

Figura 8.8: La struttura tms dei tempi di processore associati a un processo.

Gli altri due campi mantengono rispettivamente la somma delluser time ed del system time di tutti i processi gli che sono terminati; il kernel cio` somma in tms_cutime il valore di e
le glibc seguono lo standard ANSI C, POSIX richiede che CLOCKS_PER_SEC sia denito pari a 1000000 indipendentemente dalla risoluzione del timer di sistema.
29

8.4. LA GESTIONE DEI TEMPI DEL SISTEMA

201

tms_utime e tms_cutime per ciascun glio del quale ` stato ricevuto lo stato di terminazione, e e lo stesso vale per tms_cstime. Si tenga conto che laggiornamento di tms_cutime e tms_cstime viene eseguito solo quando una chiamata a wait o waitpid ` ritornata. Per questo motivo se un processo glio termina e prima di ricevere lo stato di terminazione di tutti i suoi gli, questi processi nipoti non verranno considerati nel calcolo di questi tempi.

8.4.3

Le funzioni per il calendar time

Come anticipato in sez. 8.4.1 il calendar time ` mantenuto dal kernel in una variabile di tipo e time_t, che usualmente corrisponde ad un tipo elementare (in Linux ` denito come long e int, che di norma corrisponde a 32 bit). Il valore corrente del calendar time, che indicheremo come tempo di sistema, pu` essere ottenuto con la funzione time che lo restituisce nel suddetto o formato; il suo prototipo `: e
#include <time.h> time_t time(time_t *t) Legge il valore corrente del calendar time. La funzione ritorna il valore del calendar time in caso di successo e -1 in caso di errore, che pu` o essere solo EFAULT.

dove t, se non nullo, deve essere lindirizzo di una variabile su cui duplicare il valore di ritorno. Analoga a time ` la funzione stime che serve per eettuare loperazione inversa, e cio` per e e impostare il tempo di sistema qualora questo sia necessario; il suo prototipo `: e
#include <time.h> int stime(time_t *t) Imposta a t il valore corrente del calendar time. La funzione ritorna 0 in caso di successo e -1 in caso di errore, che pu` essere EFAULT o EPERM. o

dato che modicare lora ha un impatto su tutto il sistema il cambiamento dellorologio ` una e operazione privilegiata e questa funzione pu` essere usata solo da un processo con i privilegi di o amministratore, altrimenti la chiamata fallir` con un errore di EPERM. a Data la scarsa precisione nelluso di time_t (che ha una risoluzione massima di un secondo) quando si devono eettuare operazioni sui tempi di norma luso delle funzioni precedenti ` e 30 i cui prototipi sconsigliato, ed esse sono di solito sostituite da gettimeofday e settimeofday, sono:
#include <sys/time.h> #include <time.h> int gettimeofday(struct timeval *tv, struct timezone *tz) Legge il tempo corrente del sistema. int settimeofday(const struct timeval *tv, const struct timezone *tz) Imposta il tempo di sistema. Entrambe le funzioni restituiscono 0 in caso di successo e -1 in caso di errore, nel qual caso errno pu` assumere i valori EINVAL EFAULT e per settimeofday anche EPERM. o

Queste funzioni utilizzano una struttura di tipo timeval, la cui denizione, insieme a quella della analoga timespec, ` riportata in g. 8.9. Le glibc infatti forniscono queste due rappresene tazioni alternative del calendar time che rispetto a time_t consentono rispettivamente precisioni del microsecondo e del nanosecondo.31
le due funzioni time e stime sono pi` antiche e derivano da SVr4, gettimeofday e settimeofday sono state u introdotte da BSD, ed in BSD4.3 sono indicate come sostitute delle precedenti. 31 la precisione ` solo teorica, la precisione reale della misura del tempo dellorologio di sistema non dipende e dalluso di queste strutture.
30

202

CAPITOLO 8. LA GESTIONE DEL SISTEMA, DEL TEMPO E DEGLI ERRORI

struct timeval { long tv_sec ; long tv_usec ; }; struct timespec { time_t tv_sec ; long tv_nsec ; };

/* seconds */ /* microseconds */

/* seconds */ /* nanoseconds */

Figura 8.9: Le strutture timeval e timespec usate per una rappresentazione ad alta risoluzione del calendar time.

Come nel caso di stime anche settimeofday (la cosa continua a valere per qualunque funzione che vada a modicare lorologio di sistema, quindi anche per quelle che tratteremo in seguito) pu` essere utilizzata solo da un processo coi privilegi di amministratore. o Il secondo argomento di entrambe le funzioni ` una struttura timezone, che storicamente e veniva utilizzata per specicare appunto la time zone, cio` linsieme del fuso orario e delle e convenzioni per lora legale che permettevano il passaggio dal tempo universale allora locale. Questo argomento oggi ` obsoleto ed in Linux non ` mai stato utilizzato; esso non ` supportato e e e n dalle vecchie libc5, n dalle glibc: pertanto quando si chiama questa funzione deve essere e e sempre impostato a NULL. Modicare lorologio di sistema con queste funzioni ` comunque problematico, in quanto e esse eettuano un cambiamento immediato. Questo pu` creare dei buchi o delle ripetizioni o nello scorrere dellorologio di sistema, con conseguenze indesiderate. Ad esempio se si porta avanti lorologio si possono perdere delle esecuzioni di cron programmate nellintervallo che si ` saltato. Oppure se si porta indietro lorologio si possono eseguire due volte delle operazioni e previste nellintervallo di tempo che viene ripetuto. Per questo motivo la modalit` pi` corretta per impostare lora ` quella di usare la funzione a u e adjtime, il cui prototipo `: e
#include <sys/time.h> int adjtime(const struct timeval *delta, struct timeval *olddelta) Aggiusta del valore delta lorologio di sistema. La funzione restituisce 0 in caso di successo e -1 in caso di errore, nel qual caso errno assumer` a il valore EPERM.

Questa funzione permette di avere un aggiustamento graduale del tempo di sistema in modo che esso sia sempre crescente in maniera monotona. Il valore di delta esprime il valore di cui si vuole spostare lorologio; se ` positivo lorologio sar` accelerato per un certo tempo in modo da e a guadagnare il tempo richiesto, altrimenti sar` rallentato. Il secondo argomento viene usato, se a non nullo, per ricevere il valore dellultimo aggiustamento eettuato. Linux poi prevede unaltra funzione, che consente un aggiustamento molto pi` dettagliato del u tempo, permettendo ad esempio anche di modicare anche la velocit` dellorologio di sistema. a La funzione ` adjtimex ed il suo prototipo `: e e
#include <sys/timex.h> int adjtimex(struct timex *buf) Aggiusta del valore delta lorologio di sistema. La funzione restituisce lo stato dellorologio (un valore > 0) in caso di successo e -1 in caso di errore, nel qual caso errno assumer` i valori EFAULT, EINVAL ed EPERM. a

La funzione richiede una struttura di tipo timex, la cui denizione, cos` come eettuata

8.4. LA GESTIONE DEI TEMPI DEL SISTEMA

203

struct timex { unsigned int modes ; long int offset ; long int freq ; long int maxerror ; long int esterror ; int status ; long int constant ; long int precision ; long int tolerance ; struct timeval time ; long int tick ; long int ppsfreq ; long int jitter ; int shift ; long int stabil ; long int jitcnt ; long int calcnt ; long int errcnt ; long int stbcnt ; };

/* /* /* /* /* /* /* /* /* /* /* /* /* /* /* /* /* /* /*

mode selector */ time offset ( usec ) */ frequency offset ( scaled ppm ) */ maximum error ( usec ) */ estimated error ( usec ) */ clock command / status */ pll time constant */ clock precision ( usec ) ( read only ) */ clock frequency tolerance ( ppm ) ( read only ) */ ( read only ) */ ( modified ) usecs between clock ticks */ pps frequency ( scaled ppm ) ( ro ) */ pps jitter ( us ) ( ro ) */ interval duration ( s ) ( shift ) ( ro ) */ pps stability ( scaled ppm ) ( ro ) */ jitter limit exceeded ( ro ) */ calibration intervals ( ro ) */ calibration errors ( ro ) */ stability limit exceeded ( ro ) */

Figura 8.10: La struttura timex per il controllo dellorologio di sistema.

in sys/timex.h, ` riportata in g. 8.10. Lazione della funzione dipende dal valore del campo e mode, che specica quale parametro dellorologio di sistema, specicato in un opportuno campo di timex, deve essere impostato. Un valore nullo serve per leggere i parametri correnti; i valori diversi da zero devono essere specicati come OR binario delle costanti riportate in tab. 8.13. La funzione utilizza il meccanismo di David L. Mills, descritto nellRFC 1305, che ` alla base e del protocollo NTP. La funzione ` specica di Linux e non deve essere usata se la portabilit` ` un e ae requisito, le glibc provvedono anche un suo omonimo ntp_adjtime. La trattazione completa di questa funzione necessita di una lettura approfondita del meccanismo descritto nellRFC 1305, ci limitiamo a descrivere in tab. 8.13 i principali valori utilizzabili per il campo mode, un elenco pi` dettagliato del signicato dei vari campi della struttura timex pu` essere ritrovato in [4]. u o Il valore delle costanti per mode pu` essere anche espresso, secondo la sintassi specicata per o la forma equivalente di questa funzione denita come ntp_adjtime, utilizzando il presso MOD al posto di ADJ. La funzione ritorna un valore positivo che esprime lo stato dellorologio di sistema; questo pu` assumere i valori riportati in tab. 8.14. Un valore di -1 viene usato per riportare un errore; o al solito se si cercher` di modicare lorologio di sistema (specicando un mode diverso da zero) a senza avere i privilegi di amministratore si otterr` un errore di EPERM. a

8.4.4

La gestione delle date.

Le funzioni viste al paragrafo precedente sono molto utili per trattare le operazioni elementari sui tempi, per` le rappresentazioni del tempo ivi illustrate, se han senso per specicare un intervallo, o non sono molto intuitive quando si deve esprimere unora o una data. Per questo motivo ` stata e introdotta una ulteriore rappresentazione, detta broken-down time, che permette appunto di suddividere il calendar time usuale in ore, minuti, secondi, ecc. Questo viene eettuato attraverso una opportuna struttura tm, la cui denizione ` riportata e in g. 8.11, ed ` in genere questa struttura che si utilizza quando si deve specicare un tempo e

204

CAPITOLO 8. LA GESTIONE DEL SISTEMA, DEL TEMPO E DEGLI ERRORI


Nome ADJ_OFFSET Valore 0x0001 Signicato Imposta la dierenza fra il tempo reale e lorologio di sistema: deve essere indicata in microsecondi nel campo offset di timex. Imposta la dierenze in frequenza fra il tempo reale e lorologio di sistema: deve essere indicata in parti per milione nel campo frequency di timex. Imposta il valore massimo dellerrore sul tempo, espresso in microsecondi nel campo maxerror di timex. Imposta la stima dellerrore sul tempo, espresso in microsecondi nel campo esterror di timex. Imposta alcuni valori di stato interni usati dal sistema nella gestione dellorologio specicati nel campo status di timex. Imposta la larghezza di banda del PLL implementato dal kernel, specicato nel campo constant di timex. Imposta il valore dei tick del timer in microsecondi, espresso nel campo tick di timex. Imposta uno spostamento una tantum dellorologio secondo il valore del campo offset simulando il comportamento di adjtime.

ADJ_FREQUENCY

0x0002

ADJ_MAXERROR ADJ_ESTERROR ADJ_STATUS ADJ_TIMECONST ADJ_TICK ADJ_OFFSET_SINGLESHOT

0x0004 0x0008 0x0010 0x0020 0x4000 0x8001

Tabella 8.13: Costanti per lassegnazione del valore del campo mode della struttura timex. Nome TIME_OK TIME_INS TIME_DEL TIME_OOP TIME_WAIT TIME_BAD Valore 0 1 2 3 4 5 Signicato Lorologio ` sincronizzato. e Insert leap second. Delete leap second. Leap second in progress. Leap second has occurred. Lorologio non ` sincronizzato. e

Tabella 8.14: Possibili valori di ritorno di adjtimex.

a partire dai dati naturali (ora e data), dato che essa consente anche di trattare la gestione del fuso orario e dellora legale.32 Le funzioni per la gestione del broken-down time sono varie e vanno da quelle usate per convertire gli altri formati in questo, usando o meno lora locale o il tempo universale, a quelle per trasformare il valore di un tempo in una stringa contenente data ed ora, i loro prototipi sono:
#include <time.h> char *asctime(const struct tm *tm) Produce una stringa con data e ora partendo da un valore espresso in broken-down time. char *ctime(const time_t *timep) Produce una stringa con data e ora partendo da un valore espresso in in formato time_t. struct tm *gmtime(const time_t *timep) Converte il calendar time dato in formato time_t in un broken-down time espresso in UTC. struct tm *localtime(const time_t *timep) Converte il calendar time dato in formato time_t in un broken-down time espresso nellora locale. time_t mktime(struct tm *tm) Converte il broken-down time in formato time_t. Tutte le funzioni restituiscono un puntatore al risultato in caso di successo e NULL in caso di errore, tranne che mktime che restituisce direttamente il valore o -1 in caso di errore.

Le prime due funzioni, asctime e ctime servono per poter stampare in forma leggibile un tempo; esse restituiscono il puntatore ad una stringa, allocata staticamente, nella forma:
in realt` i due campi tm_gmtoff e tm_zone sono estensioni previste da BSD e dalle glibc, che, quando ` denita a e _BSD_SOURCE, hanno la forma in g. 8.11.
32

8.4. LA GESTIONE DEI TEMPI DEL SISTEMA

205

struct tm { int tm_sec ; int tm_min ; int tm_hour ; int tm_mday ; int tm_mon ; int tm_year ; int tm_wday ; int tm_yday ; int tm_isdst ; long int tm_gmtoff ; const char * tm_zone ; };

/* /* /* /* /* /* /* /* /* /* /*

seconds */ minutes */ hours */ day of the month */ month */ year */ day of the week */ day in the year */ daylight saving time */ Seconds east of UTC . */ Timezone abbreviation . */

Figura 8.11: La struttura tm per una rappresentazione del tempo in termini di ora, minuti, secondi, ecc.

"Wed Jun 30 21:49:08 1993\n" e impostano anche la variabile tzname con linformazione della time zone corrente; ctime ` e banalmente denita in termini di asctime come asctime(localtime(t). Dato che luso di una stringa statica rende le funzioni non rientranti POSIX.1c e SUSv2 prevedono due sostitute rientranti, il cui nome ` al solito ottenuto aggiungendo un _r, che prendono un secondo argomento e char *buf, in cui lutente deve specicare il buer su cui la stringa deve essere copiata (deve essere di almeno 26 caratteri). Le altre tre funzioni, gmtime, localtime e mktime servono per convertire il tempo dal formato time_t a quello di tm e viceversa; gmtime eettua la conversione usando il tempo coordinato universale (UTC), cio` lora di Greenwich; mentre localtime usa lora locale; mktime esegue la e conversione inversa. Anche in questo caso le prime due funzioni restituiscono lindirizzo di una struttura allocata staticamente, per questo sono state denite anche altre due versioni rientranti (con la solita estensione _r), che prevedono un secondo argomento struct tm *result, fornito dal chiamante, che deve preallocare la struttura su cui sar` restituita la conversione. a Come mostrato in g. 8.11 il broken-down time permette di tenere conto anche della dierenza fra tempo universale e ora locale, compresa leventuale ora legale. Questo viene fatto attraverso le tre variabili globali mostrate in g. 8.12, cui si accede quando si include time.h. Queste variabili vengono impostate quando si chiama una delle precedenti funzioni di conversione, oppure invocando direttamente la funzione tzset, il cui prototipo `: e
#include <sys/timex.h> void tzset(void) Imposta le variabili globali della time zone. La funzione non ritorna niente e non d` errori. a

La funzione inizializza le variabili di g. 8.12 a partire dal valore della variabile di ambiente TZ, se questultima non ` denita verr` usato il le /etc/localtime. e a
extern char * tzname [2]; extern long timezone ; extern int daylight ;

Figura 8.12: Le variabili globali usate per la gestione delle time zone.

206

CAPITOLO 8. LA GESTIONE DEL SISTEMA, DEL TEMPO E DEGLI ERRORI

La variabile tzname contiene due stringhe, che indicano i due nomi standard della time zone corrente. La prima ` il nome per lora solare, la seconda per lora legale.33 La variabile timezone e indica la dierenza di fuso orario in secondi, mentre daylight indica se ` attiva o meno lora e legale. Bench la funzione asctime fornisca la modalit` pi` immediata per stampare un tempo o e a u una data, la essibilit` non fa parte delle sue caratteristiche; quando si vuole poter stampare a solo una parte (lora, o il giorno) di un tempo si pu` ricorrere alla pi` sosticata strftime, il o u cui prototipo `: e
#include <time.h> size_t strftime(char *s, size_t max, const char *format, const struct tm *tm) Stampa il tempo tm nella stringa s secondo il formato format. La funzione ritorna il numero di caratteri stampati in s, altrimenti restituisce 0.

La funzione converte opportunamente il tempo tm in una stringa di testo da salvare in s, purch essa sia di dimensione, indicata da size, suciente. I caratteri generati dalla funzione e vengono restituiti come valore di ritorno, ma non tengono conto del terminatore nale, che invece viene considerato nel computo della dimensione; se questultima ` eccessiva viene restituito 0 e e lo stato di s ` indenito. e
Modicatore %a %A %b %B %c %d %H %I %j %m %M %p %S %U %w %W %x %X %y %Y %Z %% Esempio Wed Wednesday Apr April Wed Apr 24 18:40:50 2002 24 18 06 114 04 40 PM 50 16 3 16 04/24/02 18:40:50 02 2002 CEST % Signicato Nome del giorno, abbreviato. Nome del giorno, completo. Nome del mese, abbreviato. Nome del mese, completo. Data e ora. Giorno del mese. Ora del giorno, da 0 a 24. Ora del giorno, da 0 a 12. Giorno dellanno. Mese dellanno. Minuto. AM/PM. Secondo. Settimana dellanno (partendo dalla domenica). Giorno della settimana. Settimana dellanno (partendo dal luned` ). La data. Lora. Anno nel secolo. Anno. Nome della timezone. Il carattere %.

Tabella 8.15: Valori previsti dallo standard ANSI C per modicatore della stringa di formato di strftime.

Il risultato della funzione ` controllato dalla stringa di formato format, tutti i caratteri e restano invariati eccetto % che viene utilizzato come modicatore; alcuni34 dei possibili valori che esso pu` assumere sono riportati in tab. 8.15. La funzione tiene conto anche della presenza o di una localizzazione per stampare in maniera adeguata i vari nomi.
anche se sono indicati come char * non ` il caso di modicare queste stringhe. e per la precisione quelli deniti dallo standard ANSI C, che sono anche quelli riportati da POSIX.1; le glibc provvedono tutte le estensioni introdotte da POSIX.2 per il comando date, i valori introdotti da SVID3 e ulteriori estensioni GNU; lelenco completo dei possibili valori ` riportato nella pagina di manuale della funzione. e
34 33

8.5. LA GESTIONE DEGLI ERRORI

207

8.5

La gestione degli errori

In questa sezione esamineremo le caratteristiche principali della gestione degli errori in un sistema unix-like. Infatti a parte il caso particolare di alcuni segnali (che tratteremo in cap. 9) in un sistema unix-like il kernel non avvisa mai direttamente un processo delloccorrenza di un errore nellesecuzione di una funzione, ma di norma questo viene riportato semplicemente usando un opportuno valore di ritorno della funzione invocata. Inoltre il sistema di classicazione degli errori ` basato sullarchitettura a processi, e presenta una serie di problemi nel caso lo si debba e usare con i thread.

8.5.1

La variabile errno

Quasi tutte le funzioni delle librerie del C sono in grado di individuare e riportare condizioni di errore, ed ` una norma fondamentale di buona programmazione controllare sempre che le e funzioni chiamate si siano concluse correttamente. In genere le funzioni di libreria usano un valore speciale per indicare che c` stato un errore. e Di solito questo valore ` -1 o un puntatore nullo o la costante EOF (a seconda della funzione); e ma questo valore segnala solo che c` stato un errore, non il tipo di errore. e Per riportare il tipo di errore il sistema usa la variabile globale errno,35 denita nellheader errno.h; la variabile ` in genere denita come volatile dato che pu` essere cambiata in e o modo asincrono da un segnale (si veda sez. 9.3.6 per un esempio, ricordando quanto trattato in sez. 3.5.2), ma dato che un gestore di segnale scritto bene salva e ripristina il valore della variabile, di questo non ` necessario preoccuparsi nella programmazione normale. e I valori che pu` assumere errno sono riportati in app. C, nellheader errno.h sono anche o deniti i nomi simbolici per le costanti numeriche che identicano i vari errori; essi iniziano tutti per E e si possono considerare come nomi riservati. In seguito faremo sempre riferimento a tali valori, quando descriveremo i possibili errori restituiti dalle funzioni. Il programma di esempio errcode stampa il codice relativo ad un valore numerico con lopzione -l. Il valore di errno viene sempre impostato a zero allavvio di un programma, gran parte delle funzioni di libreria impostano errno ad un valore diverso da zero in caso di errore. Il valore ` e invece indenito in caso di successo, perch anche se una funzione ha successo, pu` chiamarne e o altre al suo interno che falliscono, modicando cos` errno. Pertanto un valore non nullo di errno non ` sintomo di errore (potrebbe essere il risultato di e un errore precedente) e non lo si pu` usare per determinare quando o se una chiamata a funzione o ` fallita. La procedura da seguire ` sempre quella di controllare errno immediatamente dopo e e aver vericato il fallimento della funzione attraverso il suo codice di ritorno.

8.5.2

Le funzioni strerror e perror

Bench gli errori siano identicati univocamente dal valore numerico di errno le librerie provvee dono alcune funzioni e variabili utili per riportare in opportuni messaggi le condizioni di errore vericatesi. La prima funzione che si pu` usare per ricavare i messaggi di errore ` strerror, il o e cui prototipo `: e
#include <string.h> char *strerror(int errnum) Restituisce una stringa con il messaggio di errore relativo ad errnum. La funzione ritorna il puntatore ad una stringa di errore. luso di una variabile globale pu` comportare alcuni problemi (ad esempio nel caso dei thread) ma lo standard o ISO C consente anche di denire errno come un modiable lvalue, quindi si pu` anche usare una macro, e questo o ` infatti il modo usato da Linux per renderla locale ai singoli thread. e
35

208

CAPITOLO 8. LA GESTIONE DEL SISTEMA, DEL TEMPO E DEGLI ERRORI

La funzione ritorna il puntatore alla stringa contenente il messaggio di errore corrispondente al valore di errnum, se questo non ` un valore valido verr` comunque restituita una stringa valida e a contenente un messaggio che dice che lerrore ` sconosciuto, e errno verr` modicata assumendo e a il valore EINVAL. In generale strerror viene usata passando errno come argomento, ed il valore di questultima non verr` modicato. La funzione inoltre tiene conto del valore della variabile di ambiena te LC_MESSAGES per usare le appropriate traduzioni dei messaggi derrore nella localizzazione presente. La funzione utilizza una stringa statica che non deve essere modicata dal programma; essa ` utilizzabile solo no ad una chiamata successiva a strerror o perror, nessunaltra funzione di e libreria tocca questa stringa. In ogni caso luso di una stringa statica rende la funzione non rientrante, per cui nel caso si usino i thread le librerie forniscono36 una apposita versione rientrante strerror_r, il cui prototipo `: e
#include <string.h> char * strerror_r(int errnum, char *buf, size_t size) Restituisce una stringa con il messaggio di errore relativo ad errnum. La funzione restituisce lindirizzo del messaggio in caso di successo e NULL in caso di errore; nel qual caso errno assumer` i valori: a EINVAL ERANGE si ` specicato un valore di errnum non valido. e la lunghezza di buf ` insuciente a contenere la stringa di errore. e

La funzione ` analoga a strerror ma restituisce la stringa di errore nel buer buf che il e singolo thread deve allocare autonomamente per evitare i problemi connessi alla condivisione del buer statico. Il messaggio ` copiato no alla dimensione massima del buer, specicata dale largomento size, che deve comprendere pure il carattere di terminazione; altrimenti la stringa viene troncata. Una seconda funzione usata per riportare i codici di errore in maniera automatizzata sullo standard error (vedi sez. 6.1.2) ` perror, il cui prototipo `: e e
#include <stdio.h> void perror(const char *message) Stampa il messaggio di errore relativo al valore corrente di errno sullo standard error; preceduto dalla stringa message.

I messaggi di errore stampati sono gli stessi di strerror, (riportati in app. C), e, usando il valore corrente di errno, si riferiscono allultimo errore avvenuto. La stringa specicata con message viene stampato prima del messaggio derrore, seguita dai due punti e da uno spazio, il messaggio ` terminato con un a capo. e Il messaggio pu` essere riportato anche usando le due variabili globali: o const char * sys_errlist []; int sys_nerr ; dichiarate in errno.h. La prima contiene i puntatori alle stringhe di errore indicizzati da errno; la seconda esprime il valore pi` alto per un codice di errore, lutilizzo di questa stringa ` u e sostanzialmente equivalente a quello di strerror. In g. 8.13 ` riportata la sezione attinente del codice del programma errcode, che pu` essere e o usato per stampare i messaggi di errore e le costanti usate per identicare i singoli errori; il sorgente completo del programma ` allegato nel le ErrCode.c e contiene pure la gestione delle e opzioni e tutte le denizioni necessarie ad associare il valore numerico alla costante simbolica.
questa funzione ` la versione prevista dalle glibc, ed eettivamente denita in string.h, ne esiste una analoga e nello standard SUSv3 (quella riportata dalla pagina di manuale), che restituisce int al posto di char *, e che tronca la stringa restituita a size.
36

8.5. LA GESTIONE DEGLI ERRORI

209

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17

/* convert string to number */ err = strtol ( argv [ optind ] , NULL , 10); /* testing error condition on conversion */ if ( err == LONG_MIN ) { perror ( " Underflow on error code " ); return 1; } else if ( err == LONG_MIN ) { perror ( " Overflow on error code " ); return 1; } /* conversion is fine */ if ( message ) { printf ( " Error message for % d is % s \ n " , err , strerror ( err )); } if ( label ) { printf ( " Error label for % d is % s \ n " , err , err_code [ err ]); }

Figura 8.13: Codice per la stampa del messaggio di errore standard.

In particolare si ` riportata la sezione che converte la stringa passata come argomento in un e intero (1-2), controllando con i valori di ritorno di strtol che la conversione sia avvenuta correttamente (4-10), e poi stampa, a seconda dellopzione scelta il messaggio di errore (11-14) o la macro (15-17) associate a quel codice.

8.5.3

Alcune estensioni GNU

Le precedenti funzioni sono quelle denite ed usate nei vari standard; le glibc hanno per` ino trodotto una serie di estensioni GNU che forniscono alcune funzionalit` aggiuntive per una a gestione degli errori semplicata e pi` eciente. u La prima estensione consiste in due variabili, char * program_invocation_name e char * program_invocation_short_name servono per ricavare il nome del programma; queste sono utili quando si deve aggiungere il nome del programma (cosa comune quando si ha un programma che non viene lanciato da linea di comando e salva gli errori in un le di log) al messaggio derrore. La prima contiene il nome usato per lanciare il programma (ed ` equivalente ad argv[0]); la e seconda mantiene solo il nome del programma (senza eventuali directory in testa). Uno dei problemi che si hanno con luso di perror ` che non c` essibilit` su quello che e e a si pu` aggiungere al messaggio di errore, che pu` essere solo una stringa. In molte occasioni o o invece serve poter scrivere dei messaggi con maggiore informazione; ad esempio negli standard di programmazione GNU si richiede che ogni messaggio di errore sia preceduto dal nome del programma, ed in generale si pu` voler stampare il contenuto di qualche variabile; per questo le o glibc deniscono la funzione error, il cui prototipo `: e
#include <stdio.h> void error(int status, int errnum, const char *format, ...) Stampa un messaggio di errore formattato. La funzione non restituisce nulla e non riporta errori.

La funzione fa parte delle estensioni GNU per la gestione degli errori, largomento format prende la stessa sintassi di printf, ed i relativi argomenti devono essere forniti allo stesso modo, mentre errnum indica lerrore che si vuole segnalare (non viene quindi usato il valore corrente di errno); la funzione stampa sullo standard error il nome del programma, come indicato dalla variabile globale program_name, seguito da due punti ed uno spazio, poi dalla stringa generata

210

CAPITOLO 8. LA GESTIONE DEL SISTEMA, DEL TEMPO E DEGLI ERRORI

da format e dagli argomenti seguenti, seguita da due punti ed uno spazio inne il messaggio di errore relativo ad errnum, il tutto ` terminato da un a capo. e Il comportamento della funzione pu` essere ulteriormente controllato se si denisce una o variabile error_print_progname come puntatore ad una funzione void che restituisce void che si incarichi di stampare il nome del programma. Largomento status pu` essere usato per terminare direttamente il programma in caso di o errore, nel qual caso error dopo la stampa del messaggio di errore chiama exit con questo stato di uscita. Se invece il valore ` nullo error ritorna normalmente ma viene incrementata unaltra e variabile globale, error_message_count, che tiene conto di quanti errori ci sono stati. Unaltra funzione per la stampa degli errori, ancora pi` sosticata, che prende due argomenti u aggiuntivi per indicare linea e le su cui ` avvenuto lerrore ` error_at_line; il suo prototipo e e `: e
#include <stdio.h> void error_at_line(int status, int errnum, const char *fname, unsigned int lineno, const char *format, ...) Stampa un messaggio di errore formattato. La funzione non restituisce nulla e non riporta errori.

ed il suo comportamento ` identico a quello di error se non per il fatto che, separati con il e solito due punti-spazio, vengono inseriti un nome di le indicato da fname ed un numero di linea subito dopo la stampa del nome del programma. Inoltre essa usa unaltra variabile globale, error_one_per_line, che impostata ad un valore diverso da zero fa si che errori relativi alla stessa linea non vengano ripetuti.

Capitolo 9

I segnali
I segnali sono il primo e pi` semplice meccanismo di comunicazione nei confronti dei processi. u Nella loro versione originale essi portano con s nessuna informazione che non sia il loro tipo; si e tratta in sostanza di uninterruzione software portata ad un processo. In genere essi vengono usati dal kernel per riportare ai processi situazioni eccezionali (come errori di accesso, eccezioni aritmetiche, ecc.) ma possono anche essere usati come forma elementare di comunicazione fra processi (ad esempio vengono usati per il controllo di sessione), per noticare eventi (come la terminazione di un processo glio), ecc. In questo capitolo esamineremo i vari aspetti della gestione dei segnali, partendo da una introduzione relativa ai concetti base con cui essi vengono realizzati, per poi arontarne la classicazione a secondo di uso e modalit` di generazione no ad esaminare in dettaglio le a funzioni e le metodologie di gestione avanzate e le estensioni fatte allinterfaccia classica nelle nuovi versioni dello standard POSIX.

9.1

Introduzione

In questa sezione esamineremo i concetti generali relativi ai segnali, vedremo le loro caratteristiche di base, introdurremo le nozioni di fondo relative allarchitettura del funzionamento dei segnali e alle modalit` con cui il sistema gestisce linterazione fra di essi ed i processi. a

9.1.1

I concetti base

Come il nome stesso indica i segnali sono usati per noticare ad un processo loccorrenza di un qualche evento. Gli eventi che possono generare un segnale sono vari; un breve elenco di possibili cause per lemissione di un segnale ` il seguente: e un errore del programma, come una divisione per zero o un tentativo di accesso alla memoria fuori dai limiti validi; la terminazione di un processo glio; la scadenza di un timer o di un allarme; il tentativo di eettuare unoperazione di input/output che non pu` essere eseguita; o una richiesta dellutente di terminare o fermare il programma. In genere si realizza attraverso un segnale mandato dalla shell in corrispondenza della pressione di tasti del terminale come C-c o C-z;1 lesecuzione di una kill o di una raise da parte del processo stesso o di un altro (solo nel caso della kill).
1

indichiamo con C-x la pressione simultanea al tasto x del tasto control (ctrl in molte tastiere).

211

212

CAPITOLO 9. I SEGNALI

Ciascuno di questi eventi (compresi gli ultimi due che pure sono controllati dallutente o da un altro processo) comporta lintervento diretto da parte del kernel che causa la generazione di un particolare tipo di segnale. Quando un processo riceve un segnale, invece del normale corso del programma, viene eseguita una azione predenita o una apposita funzione di gestione (quello che da qui in avanti chiameremo il gestore del segnale, dallinglese signal handler ) che pu` essere stata specicata o dallutente (nel qual caso si dice che si intercetta il segnale).

9.1.2

Le semantiche del funzionamento dei segnali

Negli anni il comportamento del sistema in risposta ai segnali ` stato modicato in vari modi e nelle dierenti implementazioni di Unix. Si possono individuare due tipologie fondamentali di comportamento dei segnali (dette semantiche) che vengono chiamate rispettivamente semantica adabile (o reliable) e semantica inadabile (o unreliable). Nella semantica inadabile (quella implementata dalle prime versioni di Unix) la funzione di gestione del segnale specicata dallutente non resta attiva una volta che ` stata eseguita; e ` perci` compito dellutente stesso ripetere linstallazione allinterno del gestore del segnale, in e o tutti quei casi in cui si vuole che esso resti attivo. In questo caso ` possibile una situazione in cui i segnali possono essere perduti. Si consideri il e segmento di codice riportato in g. 9.1, nel programma principale viene installato un gestore (5), ed in questultimo la prima operazione (11) ` quella di reinstallare se stesso. Se nellesecuzione e del gestore un secondo segnale arriva prima che esso abbia potuto eseguire la reinstallazione, verr` eseguito il comportamento predenito assegnato al segnale stesso, il che pu` comportare, a a o seconda dei casi, che il segnale viene perso (se limpostazione predenita era quello di ignorarlo) o la terminazione immediata del processo; in entrambi i casi lazione prevista non verr` eseguita. a
int sig_handler (); /* handler function */ int main () 3 { 4 ... 5 signal ( SIGINT , sig_handler ); /* establish handler */ 6 ... 7 }
1 2 8

int sig_handler () { 11 signal ( SIGINT , sig_handler ); 12 ... 13 }


9 10

/* restablish handler */ /* process signal */

Figura 9.1: Esempio di codice di un gestore di segnale per la semantica inadabile.

Questa ` la ragione per cui limplementazione dei segnali secondo questa semantica viene e chiamata inadabile; infatti la ricezione del segnale e la reinstallazione del suo gestore non sono operazioni atomiche, e sono sempre possibili delle race condition (sullargomento vedi quanto detto in sez. 3.5). Un altro problema ` che in questa semantica non esiste un modo per bloccare i segnali quando e non si vuole che arrivino; i processi possono ignorare il segnale, ma non ` possibile istruire il e sistema a non fare nulla in occasione di un segnale, pur mantenendo memoria del fatto che ` e avvenuto. Nella semantica adabile (quella utilizzata da Linux e da ogni Unix moderno) il gestore una volta installato resta attivo e non si hanno tutti i problemi precedenti. In questa semantica

9.1. INTRODUZIONE

213

i segnali vengono generati dal kernel per un processo alloccorrenza dellevento che causa il segnale. In genere questo viene fatto dal kernel impostando lapposito campo della task_struct del processo nella process table (si veda g. 3.2). Si dice che il segnale viene consegnato al processo (dallinglese delivered ) quando viene eseguita lazione per esso prevista, mentre per tutto il tempo che passa fra la generazione del segnale e la sua consegna esso ` detto pendente (o pending). In genere questa procedura viene eettuata e dallo scheduler quando, riprendendo lesecuzione del processo in questione, verica la presenza del segnale nella task_struct e mette in esecuzione il gestore. In questa semantica un processo ha la possibilit` di bloccare la consegna dei segnali, in questo a caso, se lazione per il suddetto segnale non ` quella di ignorarlo, il segnale resta pendente ntanto e che il processo non lo sblocca (nel qual caso viene consegnato) o imposta lazione corrispondente per ignorarlo. Si tenga presente che il kernel stabilisce cosa fare con un segnale che ` stato bloccato al e momento della consegna, non quando viene generato; questo consente di cambiare lazione per il segnale prima che esso venga consegnato, e si pu` usare la funzione sigpending (vedi sez. 9.4.4) o per determinare quali segnali sono bloccati e quali sono pendenti.

9.1.3

Tipi di segnali

In generale gli eventi che generano segnali si possono dividere in tre categorie principali: errori, eventi esterni e richieste esplicite. Un errore signica che un programma ha fatto qualcosa di sbagliato e non pu` continuare o ad essere eseguito. Non tutti gli errori causano dei segnali, in genere le condizioni di errore pi` u comuni comportano la restituzione di un codice di errore da parte di una funzione di libreria; sono gli errori che possono avvenire nella esecuzione delle istruzioni di un programma che causano lemissione di un segnale, come le divisioni per zero o luso di indirizzi di memoria non validi. Un evento esterno ha in genere a che fare con lI/O o con altri processi; esempi di segnali di questo tipo sono quelli legati allarrivo di dati di input, scadenze di un timer, terminazione di processi gli. Una richiesta esplicita signica luso di una chiamata di sistema (come kill o raise) per la generazione di un segnale, cosa che viene fatta usualmente dalla shell quando lutente invoca la sequenza di tasti di stop o di suspend, ma pu` essere pure inserita allinterno di un programma. o Si dice poi che i segnali possono essere asincroni o sincroni. Un segnale sincrono ` legato ad e una azione specica di un programma ed ` inviato (a meno che non sia bloccato) durante tale e azione; molti errori generano segnali sincroni, cos` come la richiesta esplicita da parte del processo tramite le chiamate al sistema. Alcuni errori come la divisione per zero non sono completamente sincroni e possono arrivare dopo qualche istruzione. I segnali asincroni sono generati da eventi fuori dal controllo del processo che li riceve, e arrivano in tempi impredicibili nel corso dellesecuzione del programma. Eventi esterni come la terminazione di un processo glio generano segnali asincroni, cos` come le richieste di generazione di un segnale eettuate da altri processi. In generale un tipo di segnale o ` sincrono o ` asincrono, salvo il caso in cui esso sia generato e e attraverso una richiesta esplicita tramite chiamata al sistema, nel qual caso qualunque tipo di segnale (quello scelto nella chiamata) pu` diventare sincrono o asincrono a seconda che sia o generato internamente o esternamente al processo.

9.1.4

La notica dei segnali

Come accennato quando un segnale viene generato, se la sua azione predenita non ` quella di e essere ignorato, il kernel prende nota del fatto nella task_struct del processo; si dice cos` che

214

CAPITOLO 9. I SEGNALI

il segnale diventa pendente (o pending), e rimane tale no al momento in cui verr` noticato al a processo (o verr` specicata come azione quella di ignorarlo). a Normalmente linvio al processo che deve ricevere il segnale ` immediato ed avviene non e appena questo viene rimesso in esecuzione dallo scheduler che esegue lazione specicata. Questo a meno che il segnale in questione non sia stato bloccato prima della notica, nel qual caso linvio non avviene ed il segnale resta pendente indenitamente. Quando lo si sblocca il segnale pendente sar` subito noticato. Si tenga presente per` che i segnali pendenti non si accodano, a o alla generazione infatti il kernel marca un ag nella task_struct del processo, per cui se prima della notica ne vengono generati altri il ag ` comunque marcato, ed il gestore viene eseguito e sempre una sola volta. Si ricordi per` che se lazione specicata per un segnale ` quella di essere ignorato questo sar` o e a scartato immediatamente al momento della sua generazione, e questo anche se in quel momento il segnale ` bloccato (perch bloccare su un segnale signica bloccarne la notica). Per questo e e motivo un segnale, ntanto che viene ignorato, non sar` mai noticato, anche se prima ` stato a e bloccato ed in seguito si ` specicata una azione diversa (nel qual caso solo i segnali successivi e alla nuova specicazione saranno noticati). Una volta che un segnale viene noticato (che questo avvenga subito o dopo una attesa pi` u o meno lunga) viene eseguita lazione specicata per il segnale. Per alcuni segnali (SIGKILL e SIGSTOP) questa azione ` ssa e non pu` essere cambiata, ma per tutti gli altri si pu` selezionare e o o una delle tre possibilit` seguenti: a ignorare il segnale; catturare il segnale, ed utilizzare il gestore specicato; accettare lazione predenita per quel segnale. Un programma pu` specicare queste scelte usando le due funzioni signal e sigaction o (vedi sez. 9.3.2 e sez. 9.4.3). Se si ` installato un gestore sar` questultimo ad essere eseguito alla e a notica del segnale. Inoltre il sistema far` si che mentre viene eseguito il gestore di un segnale, a questultimo venga automaticamente bloccato (cos` si possono evitare race condition). Nel caso non sia stata specicata unazione, viene utilizzata lazione standard che (come vedremo in sez. 9.2.1) ` propria di ciascun segnale; nella maggior parte dei casi essa porta alla e terminazione del processo, ma alcuni segnali che rappresentano eventi innocui vengono ignorati. Quando un segnale termina un processo, il padre pu` determinare la causa della terminazione o esaminando il codice di stato riportato dalle funzioni wait e waitpid (vedi sez. 3.2.4); questo ` il modo in cui la shell determina i motivi della terminazione di un programma e scrive un e eventuale messaggio di errore. I segnali che rappresentano errori del programma (divisione per zero o violazioni di accesso) hanno anche la caratteristica di scrivere un le di core dump che registra lo stato del processo (ed in particolare della memoria e dello stack) prima della terminazione. Questo pu` essere o esaminato in seguito con un debugger per investigare sulla causa dellerrore. Lo stesso avviene se i suddetti segnali vengono generati con una kill.

9.2

La classicazione dei segnali

Esamineremo in questa sezione quali sono i vari segnali deniti nel sistema, le loro caratteristiche e tipologia, le varie macro e costanti che permettono di identicarli, e le funzioni che ne stampano la descrizione.

9.2.1

I segnali standard

Ciascun segnale ` identicato rispetto al sistema da un numero, ma luso diretto di questo numero e da parte dei programmi ` da evitare, in quanto esso pu` variare a seconda dellimplementazione e o

9.2. LA CLASSIFICAZIONE DEI SEGNALI

215

del sistema, e nel caso di Linux, anche a seconda dellarchitettura hardware. Per questo motivo ad ogni segnale viene associato un nome, denendo con una macro di preprocessore una costante uguale al suddetto numero. Sono questi nomi, che sono standardizzati e sostanzialmente uniformi rispetto alle varie implementazioni, che si devono usare nei programmi. Tutti i nomi e le funzioni che concernono i segnali sono deniti nellheader di sistema signal.h. Il numero totale di segnali presenti ` dato dalla macro NSIG, e dato che i numeri dei segnali e sono allocati progressivamente, essa corrisponde anche al successivo del valore numerico assegnato allultimo segnale denito. In tab. 9.3 si ` riportato lelenco completo dei segnali deniti e in Linux (estratto dalle pagine di manuale), comparati con quelli deniti in vari standard.
Sigla A B C D E F Signicato Lazione predenita ` terminare il processo. e Lazione predenita ` ignorare il segnale. e Lazione predenita ` terminare il processo e scrivere un e core dump. Lazione predenita ` fermare il processo. e Il segnale non pu` essere intercettato. o Il segnale non pu` essere ignorato. o

Tabella 9.1: Legenda delle azioni predenite dei segnali riportate in tab. 9.3.

In tab. 9.3 si sono anche riportate le azioni predenite di ciascun segnale (riassunte con delle lettere, la cui legenda completa ` in tab. 9.1), quando nessun gestore ` installato un segnale pu` e e o essere ignorato o causare la terminazione del processo. Nella colonna standard sono stati indicati anche gli standard in cui ciascun segnale ` denito, secondo lo schema di tab. 9.2. e
Sigla P B L S Standard POSIX BSD Linux SUSv2

Tabella 9.2: Legenda dei valori della colonna Standard di tab. 9.3.

In alcuni casi alla terminazione del processo ` associata la creazione di un le (posto nella e directory corrente del processo e chiamato core) su cui viene salvata unimmagine della memoria del processo (il cosiddetto core dump), che pu` essere usata da un debugger per esaminare lo o stato dello stack e delle variabili al momento della ricezione del segnale. La descrizione dettagliata del signicato dei vari segnali, raggruppati per tipologia, verr` a arontata nei paragra successivi.

9.2.2

Segnali di errore di programma

Questi segnali sono generati quando il sistema, o in certi casi direttamente lhardware (come per i page fault non validi) rileva un qualche errore insanabile nel programma in esecuzione. In generale la generazione di questi segnali signica che il programma ha dei gravi problemi (ad esempio ha dereferenziato un puntatore non valido o ha eseguito una operazione aritmetica proibita) e lesecuzione non pu` essere proseguita. o In genere si intercettano questi segnali per permettere al programma di terminare in maniera pulita, ad esempio per ripristinare le impostazioni della console o eliminare i le di lock prima delluscita. In questo caso il gestore deve concludersi ripristinando lazione predenita e rialzando il segnale, in questo modo il programma si concluder` senza eetti spiacevoli, ma riportando lo a stesso stato di uscita che avrebbe avuto se il gestore non ci fosse stato. Lazione predenita per tutti questi segnali ` causare la terminazione del processo che li ha e causati. In genere oltre a questo il segnale provoca pure la registrazione su disco di un le di

216
Segnale SIGHUP SIGINT SIGQUIT SIGILL SIGABRT SIGFPE SIGKILL SIGSEGV SIGPIPE SIGALRM SIGTERM SIGUSR1 SIGUSR2 SIGCHLD SIGCONT SIGSTOP SIGTSTP SIGTTIN SIGTTOU SIGBUS SIGPOLL SIGPROF SIGSYS SIGTRAP SIGURG SIGVTALRM SIGXCPU SIGXFSZ SIGIOT SIGEMT SIGSTKFLT SIGIO SIGCLD SIGPWR SIGINFO SIGLOST SIGWINCH SIGUNUSED Standard PL PL PL PL PL PL PL PL PL PL PL PL PL PL PL PL PL PL PL SL SL SL SL SL SLB SLB SLB SLB L L L LB L L L L LB L Azione A A C C C C AEF C A A A A A B DEF D D D C A A C C B A C C C A A A A B A

CAPITOLO 9. I SEGNALI
Descrizione Hangup o terminazione del processo di controllo. Interrupt da tastiera (C-c). Quit da tastiera (C-y). Istruzione illecita. Segnale di abort da abort. Errore aritmetico. Segnale di terminazione forzata. Errore di accesso in memoria. Pipe spezzata. Segnale del timer da alarm. Segnale di terminazione C-\. Segnale utente numero 1. Segnale utente numero 2. Figlio terminato o fermato. Continua se fermato. Ferma il processo. Pressione del tasto di stop sul terminale. Input sul terminale per un processo in background. Output sul terminale per un processo in background. Errore sul bus (bad memory access). Pollable event (Sys V); Sinonimo di SIGIO. Timer del proling scaduto. Argomento sbagliato per una subroutine (SVID). Trappole per un Trace/breakpoint. Ricezione di una urgent condition su un socket. Timer di esecuzione scaduto. Ecceduto il limite sul tempo di CPU. Ecceduto il limite sulla dimensione dei le. IOT trap. Sinonimo di SIGABRT. Errore sullo stack del coprocessore. LI/O ` possibile (4.2 BSD). e Sinonimo di SIGCHLD. Fallimento dellalimentazione. Sinonimo di SIGPWR. Perso un lock sul le (per NFS). Finestra ridimensionata (4.3 BSD, Sun). Segnale inutilizzato (diventer` SIGSYS). a

Tabella 9.3: Lista dei segnali in Linux.

core dump che viene scritto in un le core nella directory corrente del processo al momento dellerrore, che il debugger pu` usare per ricostruire lo stato del programma al momento della o terminazione. Questi segnali sono: SIGFPE Riporta un errore aritmetico fatale. Bench il nome derivi da oating point exception e si applica a tutti gli errori aritmetici compresa la divisione per zero e loverow. Se il gestore ritorna il comportamento del processo ` indenito, ed ignorare questo e segnale pu` condurre ad un ciclo innito. o Il nome deriva da illegal instruction, signica che il programma sta cercando di eseguire una istruzione privilegiata o inesistente, in generale del codice illecito. Poich il compilatore del C genera del codice valido si ottiene questo segnale se e il le eseguibile ` corrotto o si stanno cercando di eseguire dei dati. Questultimo e caso pu` accadere quando si passa un puntatore sbagliato al posto di un puntatore o a funzione, o si eccede la scrittura di un vettore di una variabile locale, andando a corrompere lo stack. Lo stesso segnale viene generato in caso di overow dello stack

SIGILL

9.2. LA CLASSIFICAZIONE DEI SEGNALI

217

o di problemi nellesecuzione di un gestore. Se il gestore ritorna il comportamento del processo ` indenito. e SIGSEGV Il nome deriva da segment violation, e signica che il programma sta cercando di leggere o scrivere in una zona di memoria protetta al di fuori di quella che gli ` stata riservata dal sistema. In genere ` il meccanismo della protezione della e e memoria che si accorge dellerrore ed il kernel genera il segnale. Se il gestore ritorna il comportamento del processo ` indenito. e ` E tipico ottenere questo segnale dereferenziando un puntatore nullo o non inizializzato leggendo al di l` della ne di un vettore. a Il nome deriva da bus error. Come SIGSEGV questo ` un segnale che viene generato e di solito quando si dereferenzia un puntatore non inizializzato, la dierenza ` che e SIGSEGV indica un accesso non permesso su un indirizzo esistente (tipo fuori dallo heap o dallo stack), mentre SIGBUS indica laccesso ad un indirizzo non valido, come nel caso di un puntatore non allineato. Il nome deriva da abort. Il segnale indica che il programma stesso ha rilevato un errore che viene riportato chiamando la funzione abort che genera questo segnale. ` E il segnale generato da unistruzione di breakpoint o dallattivazione del traccia` mento per il processo. E usato dai programmi per il debugging e un programma normale non dovrebbe ricevere questo segnale. Sta ad indicare che si ` eseguita una istruzione che richiede lesecuzione di una e system call, ma si ` fornito un codice sbagliato per questultima. e

SIGBUS

SIGABRT SIGTRAP

SIGSYS

9.2.3

I segnali di terminazione

Questo tipo di segnali sono usati per terminare un processo; hanno vari nomi a causa del dierente uso che se ne pu` fare, ed i programmi possono trattarli in maniera dierente. o La ragione per cui pu` essere necessario trattare questi segnali ` che il programma pu` o e o dover eseguire una serie di azioni di pulizia prima di terminare, come salvare informazioni sullo stato in cui si trova, cancellare le temporanei, o ripristinare delle condizioni alterate durante il funzionamento (come il modo del terminale o le impostazioni di una qualche periferica). Lazione predenita di questi segnali ` di terminare il processo, questi segnali sono: e SIGTERM ` Il nome sta per terminate. E un segnale generico usato per causare la conclusione di un programma. Al contrario di SIGKILL pu` essere intercettato, ignorato, bloccato. o In genere lo si usa per chiedere in maniera educata ad un processo di concludersi. ` ` Il nome sta per interrupt. E il segnale di interruzione per il programma. E quello che viene generato di default dal comando kill o dallinvio sul terminale del carattere di controllo INTR (interrupt, generato dalla sequenza C-c). ` E analogo a SIGINT con la dierenza che ` controllato da un altro carattere di cone trollo, QUIT, corrispondente alla sequenza C-\. A dierenza del precedente lazione predenita, oltre alla terminazione del processo, comporta anche la creazione di un core dump. In genere lo si pu` pensare come corrispondente ad una condizione di errore del o programma rilevata dallutente. Per questo motivo non ` opportuno fare eseguire e al gestore di questo segnale le operazioni di pulizia normalmente previste (tipo la cancellazione di le temporanei), dato che in certi casi esse possono eliminare informazioni utili nellesame dei core dump.

SIGINT

SIGQUIT

218 SIGKILL

CAPITOLO 9. I SEGNALI Il nome ` utilizzato per terminare in maniera immediata qualunque programma. e Questo segnale non pu` essere n intercettato, n ignorato, n bloccato, per cui o e e e causa comunque la terminazione del processo. In genere esso viene generato solo per richiesta esplicita dellutente dal comando (o tramite la funzione) kill. Dato che non lo si pu` intercettare ` sempre meglio usarlo come ultima risorsa quando o e metodi meno brutali, come SIGTERM o C-c non funzionano. Se un processo non risponde a nessun altro segnale SIGKILL ne causa sempre la terminazione (in eetti il fallimento della terminazione di un processo da parte di SIGKILL costituirebbe un malfunzionamento del kernel). Talvolta ` il sistema stesso e che pu` generare questo segnale quando per condizioni particolari il processo non o pu` pi` essere eseguito neanche per eseguire un gestore. o u

SIGHUP

Il nome sta per hang-up. Segnala che il terminale dellutente si ` disconnesso (ad e esempio perch si ` interrotta la rete). Viene usato anche per riportare la terminae e zione del processo di controllo di un terminale a tutti i processi della sessione, in modo che essi possano disconnettersi dal relativo terminale. Viene inoltre usato in genere per segnalare ai demoni (che non hanno un terminale di controllo) la necessit` di reinizializzarsi e rileggere il/i le di congurazione. a

9.2.4

I segnali di allarme

Questi segnali sono generati dalla scadenza di un timer (vedi sez. 9.3.4). Il loro comportamento predenito ` quello di causare la terminazione del programma, ma con questi segnali la scelta e predenita ` irrilevante, in quanto il loro uso presuppone sempre la necessit` di un gestore. e a Questi segnali sono: SIGALRM Il nome sta per alarm. Segnale la scadenza di un timer misurato sul tempo reale o ` sullorologio di sistema. E normalmente usato dalla funzione alarm.

` SIVGTALRM Il nome sta per virtual alarm. E analogo al precedente ma segnala la scadenza di un timer sul tempo di CPU usato dal processo. SIGPROF Il nome sta per proling. Indica la scadenza di un timer che misura sia il tempo di CPU speso direttamente dal processo che quello che il sistema ha speso per conto di questultimo. In genere viene usato dagli strumenti che servono a fare la prolazione dellutilizzo del tempo di CPU da parte del processo.

9.2.5

I segnali di I/O asincrono

Questi segnali operano in congiunzione con le funzioni di I/O asincrono. Per questo occorre comunque usare fcntl per abilitare un le descriptor a generare questi segnali. Lazione predenita ` di essere ignorati. Questi segnali sono: e SIGIO Questo segnale viene inviato quando un le descriptor ` pronto per eseguire dele linput/output. In molti sistemi solo i socket e i terminali possono generare questo segnale, in Linux questo pu` essere usato anche per i le, posto che la fcntl abbia o avuto successo. Questo segnale ` inviato quando arrivano dei dati urgenti o out-of-band su di un e socket; per maggiori dettagli al proposito si veda sez. 19.1.3. Questo segnale ` equivalente a SIGIO, ` denito solo per compatibilit` con i sistemi e e a System V.

SIGURG SIGPOLL

9.2. LA CLASSIFICAZIONE DEI SEGNALI

219

9.2.6

I segnali per il controllo di sessione

Questi sono i segnali usati dal controllo delle sessioni e dei processi, il loro uso ` specializzato e e viene trattato in maniera specica nelle sezioni in cui si trattano gli argomenti relativi. Questi segnali sono: SIGCHLD Questo ` il segnale mandato al processo padre quando un glio termina o viene e fermato. Lazione predenita ` di ignorare il segnale, la sua gestione ` trattata in e e sez. 3.2.4. Per Linux questo ` solo un segnale identico al precedente, il nome ` obsoleto e e e andrebbe evitato. Il nome sta per continue. Il segnale viene usato per fare ripartire un programma precedentemente fermato da SIGSTOP. Questo segnale ha un comportamento speciale, e fa sempre ripartire il processo prima della sua consegna. Il comportamento predenito ` di fare solo questo; il segnale non pu` essere bloccato. Si pu` anche e o o installare un gestore, ma il segnale provoca comunque il riavvio del processo. La maggior pare dei programmi non hanno necessit` di intercettare il segnale, in a quanto esso ` completamente trasparente rispetto allesecuzione che riparte senza e che il programma noti niente. Si possono installare dei gestori per far si che un programma produca una qualche azione speciale se viene fermato e riavviato, come per esempio riscrivere un prompt, o inviare un avviso. SIGSTOP SIGTSTP Il segnale ferma un processo (lo porta cio` in uno stato di sleep, vedi sez. 3.4.1); il e segnale non pu` essere n intercettato, n ignorato, n bloccato. o e e e Il nome sta per interactive stop. Il segnale ferma il processo interattivamente, ed ` generato dal carattere SUSP (prodotto dalla combinazione C-z), ed al contrario e di SIGSTOP pu` essere intercettato e ignorato. In genere un programma installa un o gestore per questo segnale quando vuole lasciare il sistema o il terminale in uno stato denito prima di fermarsi; se per esempio un programma ha disabilitato leco sul terminale pu` installare un gestore per riabilitarlo prima di fermarsi. o Un processo non pu` leggere dal terminale se esegue una sessione di lavoro in o background. Quando un processo in background tenta di leggere da un terminale viene inviato questo segnale a tutti i processi della sessione di lavoro. Lazione predenita ` di fermare il processo. Largomento ` trattato in sez. 10.1.1. e e Segnale analogo al precedente SIGTTIN, ma generato quando si tenta di scrivere o modicare uno dei modi del terminale. Lazione predenita ` di fermare il processo, e largomento ` trattato in sez. 10.1.1. e

SIGCLD SIGCONT

SIGTTIN

SIGTTOU

9.2.7

I segnali di operazioni errate

Questi segnali sono usati per riportare al programma errori generati da operazioni da lui eseguite; non indicano errori del programma quanto errori che impediscono il completamento dellesecuzione dovute allinterazione con il resto del sistema. Lazione predenita di questi segnali ` di e terminare il processo, questi segnali sono: SIGPIPE Sta per Broken pipe. Se si usano delle pipe, (o delle FIFO o dei socket) ` necessario, e prima che un processo inizi a scrivere su una di esse, che un altro labbia aperta in lettura (si veda sez. 12.1.1). Se il processo in lettura non ` partito o ` terminae e to inavvertitamente alla scrittura sulla pipe il kernel genera questo segnale. Se il

220

CAPITOLO 9. I SEGNALI segnale ` bloccato, intercettato o ignorato la chiamata che lo ha causato fallisce, e restituendo lerrore EPIPE.

SIGLOST

Sta per Resource lost. Tradizionalmente ` il segnale che viene generato quando si e perde un advisory lock su un le su NFS perch il server NFS ` stato riavviato. Il e e progetto GNU lo utilizza per indicare ad un client il crollo inaspettato di un server. In Linux ` denito come sinonimo di SIGIO.2 e Sta per CPU time limit exceeded. Questo segnale ` generato quando un processo e eccede il limite impostato per il tempo di CPU disponibile, vedi sez. 8.3.2. Sta per File size limit exceeded. Questo segnale ` generato quando un processo e tenta di estendere un le oltre le dimensioni specicate dal limite impostato per le dimensioni massime di un le, vedi sez. 8.3.2.

SIGXCPU SIGXFSZ

9.2.8

Ulteriori segnali

Raccogliamo qui inne una serie di segnali che hanno scopi dierenti non classicabili in maniera omogenea. Questi segnali sono: SIGUSR1 Insieme a SIGUSR2 ` un segnale a disposizione dellutente che lo pu` usare per e o quello che vuole. Viene generato solo attraverso linvocazione della funzione kill. Entrambi i segnali possono essere utili per implementare una comunicazione elementare fra processi diversi, o per eseguire a richiesta una operazione utilizzando un gestore. Lazione predenita ` di terminare il processo. e ` E il secondo segnale a disposizione degli utenti. Vedi quanto appena detto per SIGUSR1. Il nome sta per window (size) change e viene generato in molti sistemi (GNU/Linux compreso) quando le dimensioni (in righe e colonne) di un terminale vengono cambiate. Viene usato da alcuni programmi testuali per riformattare luscita su schermo quando si cambia dimensione a questultimo. Lazione predenita ` di e essere ignorato. ` Il segnale indica una richiesta di informazioni. E usato con il controllo di sessione, causa la stampa di informazioni da parte del processo leader del gruppo associato al terminale di controllo, gli altri processi lo ignorano.

SIGUSR2 SIGWINCH

SIGINFO

9.2.9

Le funzioni strsignal e psignal

Per la descrizione dei segnali il sistema mette a disposizione due funzioni che stampano un messaggio di descrizione dato il numero. In genere si usano quando si vuole noticare allutente il segnale ricevuto (nel caso di terminazione di un processo glio o di un gestore che gestisce pi` segnali); la prima funzione, strsignal, ` una estensione GNU, accessibile avendo denito u e _GNU_SOURCE, ed ` analoga alla funzione strerror (si veda sez. 8.5.2) per gli errori: e
#include <string.h> char *strsignal(int signum) Ritorna il puntatore ad una stringa che contiene la descrizione del segnale signum.

dato che la stringa ` allocata staticamente non se ne deve modicare il contenuto, che resta e valido solo no alla successiva chiamata di strsignal. Nel caso si debba mantenere traccia del messaggio sar` necessario copiarlo. a
2

ed ` segnalato come BUG nella pagina di manuale. e

9.3. LA GESTIONE DI BASE DEI SEGNALI

221

La seconda funzione, psignal, deriva da BSD ed ` analoga alla funzione perror descritta e sempre in sez. 8.5.2; il suo prototipo `: e
#include <signal.h> void psignal(int sig, const char *s) Stampa sullo standard error un messaggio costituito dalla stringa s, seguita da due punti ed una descrizione del segnale indicato da sig.

Una modalit` alternativa per utilizzare le descrizioni restituite da strsignal e psignal ` a e quello di usare la variabile sys_siglist, che ` denita in signal.h e pu` essere acceduta con e o la dichiarazione: extern const char * const sys_siglist []; Larray sys_siglist contiene i puntatori alle stringhe di descrizione, indicizzate per numero di segnale, per cui una chiamata del tipo di char *decr = strsignal(SIGINT) pu` essere o sostituita dallequivalente char *decr = sys_siglist[SIGINT].

9.3

La gestione di base dei segnali

I segnali sono il primo e pi` classico esempio di eventi asincroni, cio` di eventi che possono u e accadere in un qualunque momento durante lesecuzione di un programma. Per questa loro caratteristica la loro gestione non pu` essere eettuata allinterno del normale usso di esecuzione o dello stesso, ma ` delegata appunto agli eventuali gestori che si sono installati. e In questa sezione vedremo come si eettua la gestione dei segnali, a partire dalla loro interazione con le system call, passando per le varie funzioni che permettono di installare i gestori e controllare le reazioni di un processo alla loro occorrenza.

9.3.1

Il comportamento generale del sistema

Abbiamo gi` trattato in sez. 9.1 le modalit` con cui il sistema gestisce linterazione fra segnali a a e processi, ci resta da esaminare per` il comportamento delle system call; in particolare due di o esse, fork ed exec, dovranno essere prese esplicitamente in considerazione, data la loro stretta relazione con la creazione di nuovi processi. Come accennato in sez. 3.2.2 quando viene creato un nuovo processo esso eredita dal padre sia le azioni che sono state impostate per i singoli segnali, che la maschera dei segnali bloccati (vedi sez. 9.4.4). Invece tutti i segnali pendenti e gli allarmi vengono cancellati; essi infatti devono essere recapitati solo al padre, al glio dovranno arrivare solo i segnali dovuti alle sue azioni. Quando si mette in esecuzione un nuovo programma con exec (si ricordi quanto detto in sez. 3.2.5) tutti i segnali per i quali ` stato installato un gestore vengono reimpostati a SIG_DFL. e Non ha pi` senso infatti fare riferimento a funzioni denite nel programma originario, che non u sono presenti nello spazio di indirizzi del nuovo programma. Si noti che questo vale solo per le azioni per le quali ` stato installato un gestore; viene mane tenuto invece ogni eventuale impostazione dellazione a SIG_IGN. Questo permette ad esempio alla shell di impostare ad SIG_IGN le risposte per SIGINT e SIGQUIT per i programmi eseguiti in background, che altrimenti sarebbero interrotti da una successiva pressione di C-c o C-y. Per quanto riguarda il comportamento di tutte le altre system call si danno sostanzialmente due casi, a seconda che esse siano lente (slow ) o veloci (fast). La gran parte di esse appartiene a questultima categoria, che non ` inuenzata dallarrivo di un segnale. Esse sono dette veloci e in quanto la loro esecuzione ` sostanzialmente immediata; la risposta al segnale viene sempre e data dopo che la system call ` stata completata, in quanto attendere per eseguire un gestore non e comporta nessun inconveniente. In alcuni casi per` alcune system call (che per questo motivo vengono chiamate lente) possono o bloccarsi indenitamente. In questo caso non si pu` attendere la conclusione della system call, o

222

CAPITOLO 9. I SEGNALI

perch questo renderebbe impossibile una risposta pronta al segnale, per cui il gestore viene e eseguito prima che la system call sia ritornata. Un elenco dei casi in cui si presenta questa situazione ` il seguente: e la lettura da le che possono bloccarsi in attesa di dati non ancora presenti (come per certi le di dispositivo, i socket o le pipe); la scrittura sugli stessi le, nel caso in cui dati non possano essere accettati immediatamente (di nuovo comune per i socket); lapertura di un le di dispositivo che richiede operazioni non immediate per una risposta (ad esempio lapertura di un nastro che deve essere riavvolto); le operazioni eseguite con ioctl che non ` detto possano essere eseguite immediatamente; e le funzioni di intercomunicazione che si bloccano in attesa di risposte da altri processi; la funzione pause (usata appunto per attendere larrivo di un segnale); la funzione wait (se nessun processo glio ` ancora terminato). e In questo caso si pone il problema di cosa fare una volta che il gestore sia ritornato. La scelta originaria dei primi Unix era quella di far ritornare anche la system call restituendo lerrore di EINTR. Questa ` a tuttoggi una scelta corrente, ma comporta che i programmi che usano e dei gestori controllino lo stato di uscita delle funzioni che eseguono una system call lenta per ripeterne la chiamata qualora lerrore fosse questo. Dimenticarsi di richiamare una system call interrotta da un segnale ` un errore comune, e tanto che le glibc provvedono una macro TEMP_FAILURE_RETRY(expr) che esegue loperazione automaticamente, ripetendo lesecuzione dellespressione expr ntanto che il risultato non ` e diverso dalluscita con un errore EINTR. La soluzione ` comunque poco elegante e BSD ha scelto un approccio molto diverso, che e ` quello di fare ripartire automaticamente una system call interrotta invece di farla fallire. In e questo caso ovviamente non c` bisogno di preoccuparsi di controllare il codice di errore; si perde e per` la possibilit` di eseguire azioni speciche alloccorrenza di questa particolare condizione. o a Linux e le glibc consentono di utilizzare entrambi gli approcci, attraverso una opportuna ` opzione di sigaction (vedi sez. 9.4.3). E da chiarire comunque che nel caso di interruzione nel mezzo di un trasferimento parziale di dati, le system call ritornano sempre indicando i byte trasferiti.

9.3.2

La funzione signal

Linterfaccia pi` semplice per la gestione dei segnali ` costituita dalla funzione signal che ` u e e denita n dallo standard ANSI C. Questultimo per` non considera sistemi multitasking, per o cui la denizione ` tanto vaga da essere del tutto inutile in un sistema Unix; ` questo il motivo e e per cui ogni implementazione successiva ne ha modicato e ridenito il comportamento, pur mantenendone immutato il prototipo3 che `: e
#include <signal.h> sighandler_t signal(int signum, sighandler_t handler) Installa la funzione di gestione handler (il gestore) per il segnale signum. La funzione ritorna il precedente gestore in caso di successo o SIG_ERR in caso di errore.

In questa denizione si ` usato un tipo di dato, sighandler_t, che ` una estensione GNU, e e denita dalle glibc, che permette di riscrivere il prototipo di signal nella forma appena vista, molto pi` leggibile di quanto non sia la versione originaria, che di norma ` denita come: u e
in realt` in alcune vecchie implementazioni (SVr4 e 4.3+BSD in particolare) vengono usati alcuni argomenti a aggiuntivi per denire il comportamento della funzione, vedremo in sez. 9.4.3 che questo ` possibile usando la e funzione sigaction.
3

9.3. LA GESTIONE DI BASE DEI SEGNALI void (* signal ( int signum , void (* handler )( int ))) int )

223

questa infatti, per la poca chiarezza della sintassi del C quando si vanno a trattare puntatori a funzioni, ` molto meno comprensibile. Da un confronto con il precedente prototipo si pu` e o dedurre la denizione di sighandler_t che `: e typedef void (* sighandler_t )( int ) e cio` un puntatore ad una funzione void (cio` senza valore di ritorno) e che prende un argoe e 4 La funzione signal quindi restituisce e prende come secondo argomento un mento di tipo int. puntatore a una funzione di questo tipo, che ` appunto la funzione che verr` usata come gestore e a del segnale. Il numero di segnale passato nellargomento signum pu` essere indicato direttamente con o una delle costanti denite in sez. 9.2.1. Largomento handler che indica il gestore invece, oltre allindirizzo della funzione da chiamare alloccorrenza del segnale, pu` assumere anche i due o valori costanti SIG_IGN e SIG_DFL; il primo indica che il segnale deve essere ignorato,5 mentre il secondo ripristina lazione predenita.6 La funzione restituisce lindirizzo dellazione precedente, che pu` essere salvato per poterlo o ripristinare (con unaltra chiamata a signal) in un secondo tempo. Si ricordi che se si imposta come azione SIG_IGN (o si imposta un SIG_DFL per un segnale la cui azione predenita ` di e essere ignorato), tutti i segnali pendenti saranno scartati, e non verranno mai noticati. Luso di signal ` soggetto a problemi di compatibilit`, dato che essa si comporta in maniera e a diversa per sistemi derivati da BSD o da System V. In questi ultimi infatti la funzione ` conforme e al comportamento originale dei primi Unix in cui il gestore viene disinstallato alla sua chiamata, secondo la semantica inadabile; anche Linux seguiva questa convenzione con le vecchie librerie del C come le libc4 e le libc5.7 Al contrario BSD segue la semantica adabile, non disinstallando il gestore e bloccando il segnale durante lesecuzione dello stesso. Con lutilizzo delle glibc dalla versione 2 anche Linux ` passato a questo comportamento. Il comportamento della versione originale della funzione, il e cui uso ` deprecato per i motivi visti in sez. 9.1.2, pu` essere ottenuto chiamando sysv_signal, e o una volta che si sia denita la macro _XOPEN_SOURCE. In generale, per evitare questi problemi, luso di signal (ed ogni eventuale ridenizione della stessa) ` da evitare; tutti i nuovi programmi e dovrebbero usare sigaction. ` E da tenere presente che, seguendo lo standard POSIX, il comportamento di un processo che ignora i segnali SIGFPE, SIGILL, o SIGSEGV (qualora questi non originino da una chiamata ad una kill o ad una raise) ` indenito. Un gestore che ritorna da questi segnali pu` dare luogo e o ad un ciclo innito.

9.3.3

Le funzioni kill e raise

Come precedentemente accennato in sez. 9.1.3, un segnale pu` anche essere generato direttao mente nellesecuzione di un programma, attraverso la chiamata ad una opportuna system call. Le funzioni che si utilizzano di solito per inviare un segnale generico ad un processo sono due: raise e kill.
si devono usare le parentesi intorno al nome della funzione per via delle precedenze degli operatori del C, senza di esse si sarebbe denita una funzione che ritorna un puntatore a void e non un puntatore ad una funzione void. 5 si ricordi per` che i due segnali SIGKILL e SIGSTOP non possono essere n ignorati n intercettati; luso di o e e SIG_IGN per questi segnali non ha alcun eetto. 6 e serve a tornare al comportamento di default quando non si intende pi` gestire direttamente un segnale. u 7 nelle libc5 esiste per` la possibilit` di includere bsd/signal.h al posto di signal.h, nel qual caso la funzione o a signal viene ridenita per seguire la semantica adabile usata da BSD.
4

224

CAPITOLO 9. I SEGNALI

La prima funzione ` raise, che ` denita dallo standard ANSI C, e serve per inviare un e e segnale al processo corrente,8 il suo prototipo `: e
#include <signal.h> int raise(int sig) Invia il segnale sig al processo corrente. La funzione restituisce zero in caso di successo e 1 per un errore, il solo errore restituito ` EINVAL e qualora si sia specicato un numero di segnale invalido.

Il valore di sig specica il segnale che si vuole inviare e pu` essere specicato con una delle o macro denite in sez. 9.2. In genere questa funzione viene usata per riprodurre il comportamento predenito di un segnale che sia stato intercettato. In questo caso, una volta eseguite le operazioni volute, il gestore dovr` prima reinstallare lazione predenita, per poi attivarla chiamando raise. a Mentre raise ` una funzione di libreria, quando si vuole inviare un segnale generico ad e un processo occorre utilizzare la apposita system call, questa pu` essere chiamata attraverso la o funzione kill, il cui prototipo `: e
#include <sys/types.h> #include <signal.h> int kill(pid_t pid, int sig) Invia il segnale sig al processo specicato con pid. La funzione restituisce 0 in caso di successo e 1 in caso di errore nel qual caso errno assumer` a uno dei valori: EINVAL ESRCH EPERM il segnale specicato non esiste. il processo selezionato non esiste. non si hanno privilegi sucienti ad inviare il segnale.

Lo standard POSIX prevede che il valore 0 per sig sia usato per specicare il segnale nullo. Se la funzione viene chiamata con questo valore non viene inviato nessun segnale, ma viene eseguito il controllo degli errori, in tal caso si otterr` un errore EPERM se non si hanno i permessi a necessari ed un errore ESRCH se il processo specicato non esiste. Si tenga conto per` che il o sistema ricicla i pid (come accennato in sez. 3.2.1) per cui lesistenza di un processo non signica che esso sia realmente quello a cui si intendeva mandare il segnale. Il valore dellargomento pid specica il processo (o i processi) di destinazione a cui il segnale deve essere inviato e pu` assumere i valori riportati in tab. 9.4. o Si noti pertanto che la funzione raise(sig) pu` essere denita in termini di kill, ed ` o e sostanzialmente equivalente ad una kill(getpid(), sig). Siccome raise, che ` denita nello e standard ISO C, non esiste in alcune vecchie versioni di Unix, in generale luso di kill nisce per essere pi` portabile. u Una seconda funzione che pu` essere denita in termini di kill ` killpg, che ` sostanzialo e e mente equivalente a kill(-pidgrp, signal); il suo prototipo `: e
#include <signal.h> int killpg(pid_t pidgrp, int signal) Invia il segnale signal al process group pidgrp. La funzione restituisce 0 in caso di successo e 1 in caso di errore, gli errori sono gli stessi di kill.

e permette di inviare un segnale a tutto un process group (vedi sez. 10.1.2). Solo lamministratore pu` inviare un segnale ad un processo qualunque, in tutti gli altri casi o luser-ID reale o luser-ID eettivo del processo chiamante devono corrispondere alluser-ID reale
non prevedendo la presenza di un sistema multiutente lo standard ANSI C non poteva che denire una funzione che invia il segnale al programma in esecuzione. Nel caso di Linux questa viene implementata come funzione di compatibilit`. a
8

9.3. LA GESTIONE DI BASE DEI SEGNALI


Valore >0 0 1 < 1 Signicato Il segnale ` mandato e Il segnale ` mandato e Il segnale ` mandato e Il segnale ` mandato e

225

al processo con il pid indicato. ad ogni processo del process group del chiamante. ad ogni processo (eccetto init). ad ogni processo del process group |pid|.

Tabella 9.4: Valori dellargomento pid per la funzione kill.

o alluser-ID salvato della destinazione. Fa eccezione il caso in cui il segnale inviato sia SIGCONT, nel quale occorre che entrambi i processi appartengano alla stessa sessione. Inoltre, dato il ruolo fondamentale che riveste nel sistema (si ricordi quanto visto in sez. 9.2.3), non ` possibile inviare e al processo 1 (cio` a init) segnali per i quali esso non abbia un gestore installato. e Inne, seguendo le speciche POSIX 1003.1-2001, luso della chiamata kill(-1, sig) comporta che il segnale sia inviato (con la solita eccezione di init) a tutti i processi per i quali i permessi lo consentano. Lo standard permette comunque alle varie implementazioni di escludere alcuni processi specici: nel caso in questione Linux non invia il segnale al processo che ha eettuato la chiamata.

9.3.4

Le funzioni alarm e abort

Un caso particolare di segnali generati a richiesta ` quello che riguarda i vari segnali di teme porizzazione e SIGABRT, per ciascuno di questi segnali sono previste funzioni speciche che ne eettuino linvio. La pi` comune delle funzioni usate per la temporizzazione ` alarm il cui u e prototipo `: e
#include <unistd.h> unsigned int alarm(unsigned int seconds) Predispone linvio di SIGALRM dopo seconds secondi. La funzione restituisce il numero di secondi rimanenti ad un precedente allarme, o zero se non cerano allarmi pendenti.

La funzione fornisce un meccanismo che consente ad un processo di predisporre uninterruzione nel futuro, (ad esempio per eettuare una qualche operazione dopo un certo periodo di tempo), programmando lemissione di un segnale (nel caso in questione SIGALRM) dopo il numero di secondi specicato da seconds. Se si specica per seconds un valore nullo non verr` inviato nessun segnale; siccome alla a chiamata viene cancellato ogni precedente allarme, questo pu` essere usato per cancellare una o programmazione precedente. La funzione inoltre ritorna il numero di secondi rimanenti allinvio dellallarme programmato in precedenza. In questo modo ` possibile controllare se non si ` cancellato un precedente allarme e e e predisporre eventuali misure che permettano di gestire il caso in cui servono pi` interruzioni. u In sez. 8.4.1 abbiamo visto che ad ogni processo sono associati tre tempi diversi: il clock time, luser time ed il system time. Per poterli calcolare il kernel mantiene per ciascun processo tre diversi timer: un real-time timer che calcola il tempo reale trascorso (che corrisponde al clock time). La scadenza di questo timer provoca lemissione di SIGALRM; un virtual timer che calcola il tempo di processore usato dal processo in user space (che corrisponde alluser time). La scadenza di questo timer provoca lemissione di SIGVTALRM; un proling timer che calcola la somma dei tempi di processore utilizzati direttamente dal processo in user space, e dal kernel nelle system call ad esso relative (che corrisponde a

226

CAPITOLO 9. I SEGNALI quello che in sez. 8.4.1 abbiamo chiamato CPU time). La scadenza di questo timer provoca lemissione di SIGPROF.

Il timer usato da alarm ` il clock time, e corrisponde cio` al tempo reale. La funzione come e e abbiamo visto ` molto semplice, ma proprio per questo presenta numerosi limiti: non consente di e usare gli altri timer, non pu` specicare intervalli di tempo con precisione maggiore del secondo o e genera il segnale una sola volta. Per ovviare a questi limiti Linux deriva da BSD la funzione setitimer che permette di usare un timer qualunque e linvio di segnali periodici, al costo per` di una maggiore complessit` duso o a e di una minore portabilit`. Il suo prototipo `: a e
#include <sys/time.h> int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue) Predispone linvio di un segnale di allarme alla scadenza dellintervallo value sul timer specicato da which. La funzione restituisce 0 in caso di successo e 1 in caso di errore, nel qual caso errno assumer` a uno dei valori EINVAL o EFAULT.

Il valore di which permette di specicare quale dei tre timer illustrati in precedenza usare; i possibili valori sono riportati in tab. 9.5.
Valore ITIMER_REAL ITIMER_VIRTUAL ITIMER_PROF Timer real-time timer virtual timer proling timer

Tabella 9.5: Valori dellargomento which per la funzione setitimer.

Il valore della struttura specicata value viene usato per impostare il timer, se il puntatore ovalue non ` nullo il precedente valore viene salvato qui. I valori dei timer devono essere indicati e attraverso una struttura itimerval, denita in g. 5.5. La struttura ` composta da due membri, il primo, it_interval denisce il periodo del timer; e il secondo, it_value il tempo mancante alla scadenza. Entrambi esprimono i tempi tramite una struttura timeval che permette una precisione no al microsecondo. Ciascun timer decrementa il valore di it_value no a zero, poi invia il segnale e reimposta it_value al valore di it_interval, in questo modo il ciclo verr` ripetuto; se invece il valore di a it_interval ` nullo il timer si ferma. e
struct itimerval { struct timeval it_interval ; /* next value */ struct timeval it_value ; /* current value */ };

Figura 9.2: La struttura itimerval, che denisce i valori dei timer di sistema.

Luso di setitimer consente dunque un controllo completo di tutte le caratteristiche dei timer, ed in eetti la stessa alarm, bench denita direttamente nello standard POSIX.1, pu` a e o sua volta essere espressa in termini di setitimer, come evidenziato dal manuale delle glibc [4] che ne riporta la denizione mostrata in g. 9.3. Si deve comunque tenere presente che la precisione di queste funzioni ` limitata da quella e della frequenza del timer di sistema (che nel caso dei PC signica circa 10 ms). Il sistema assicura comunque che il segnale non sar` mai generato prima della scadenza programmata a (larrotondamento cio` ` sempre eettuato per eccesso). ee

9.3. LA GESTIONE DI BASE DEI SEGNALI

227

unsigned int alarm ( unsigned int seconds ) { struct itimerval old , new ; new . it_interval . tv_usec = 0; new . it_interval . tv_sec = 0; new . it_value . tv_usec = 0; new . it_value . tv_sec = ( long int ) seconds ; if ( setitimer ( ITIMER_REAL , & new , & old ) < 0) { return 0; } else { return old . it_value . tv_sec ; } }

Figura 9.3: Denizione di alarm in termini di setitimer.

Una seconda causa di potenziali ritardi ` che il segnale viene generato alla scadenza del e timer, ma poi deve essere consegnato al processo; se questultimo ` attivo (questo ` sempre vero e e per ITIMER_VIRT) la consegna ` immediata, altrimenti pu` esserci un ulteriore ritardo che pu` e o o variare a seconda del carico del sistema. Questo ha una conseguenza che pu` indurre ad errori molto subdoli, si tenga conto poi che o in caso di sistema molto carico, si pu` avere il caso patologico in cui un timer scade prima che il o segnale di una precedente scadenza sia stato consegnato; in questo caso, per il comportamento dei segnali descritto in sez. 9.3.6, un solo segnale sar` consegnato. a Dato che sia alarm che setitimer non consentono di leggere il valore corrente di un timer senza modicarlo, ` possibile usare la funzione getitimer, il cui prototipo `: e e
#include <sys/time.h> int getitimer(int which, struct itimerval *value) Legge in value il valore del timer specicato da which. La funzione restituisce 0 in caso di successo e 1 in caso di errore e restituisce gli stessi errori di getitimer

i cui argomenti hanno lo stesso signicato e formato di quelli di setitimer. Lultima funzione che permette linvio diretto di un segnale ` abort, che, come accennato in e sez. 3.2.3, permette di abortire lesecuzione di un programma tramite linvio di SIGABRT. Il suo prototipo `: e
#include <stdlib.h> void abort(void) Abortisce il processo corrente. La funzione non ritorna, il processo ` terminato inviando il segnale di SIGABRT. e

La dierenza fra questa funzione e luso di raise ` che anche se il segnale ` bloccato o e e ignorato, la funzione ha eetto lo stesso. Il segnale pu` per` essere intercettato per eettuare o o eventuali operazioni di chiusura prima della terminazione del processo. Lo standard ANSI C richiede inoltre che anche se il gestore ritorna, la funzione non ritorni comunque. Lo standard POSIX.1 va oltre e richiede che se il processo non viene terminato direttamente dal gestore sia la stessa abort a farlo al ritorno dello stesso. Inoltre, sempre seguendo lo standard POSIX, prima della terminazione tutti i le aperti e gli stream saranno chiusi ed i buer scaricati su disco. Non verranno invece eseguite le eventuali funzioni registrate con atexit e on_exit.

228

CAPITOLO 9. I SEGNALI

9.3.5

Le funzioni di pausa e attesa

Sono parecchie le occasioni in cui si pu` avere necessit` di sospendere temporaneamente lesecuo a zione di un processo. Nei sistemi pi` elementari in genere questo veniva fatto con un opportuno u loop di attesa, ma in un sistema multitasking un loop di attesa ` solo un inutile spreco di CPU, e per questo ci sono apposite funzioni che permettono di mettere un processo in stato di attesa.9 Il metodo tradizionale per fare attendere ad un processo no allarrivo di un segnale ` quello e di usare la funzione pause, il cui prototipo `: e
#include <unistd.h> int pause(void) Pone il processo in stato di sleep no al ritorno di un gestore. La funzione ritorna solo dopo che un segnale ` stato ricevuto ed il relativo gestore ` ritornato, nel e e qual caso restituisce 1 e errno assumer` il valore EINTR. a

La funzione segnala sempre una condizione di errore (il successo sarebbe quello di aspettare indenitamente). In genere si usa questa funzione quando si vuole mettere un processo in attesa di un qualche evento specico che non ` sotto il suo diretto controllo (ad esempio la si pu` e o usare per interrompere lesecuzione del processo no allarrivo di un segnale inviato da un altro processo). Quando invece si vuole fare attendere un processo per un intervallo di tempo gi` noto nello a standard POSIX.1 viene denita la funzione sleep, il cui prototipo `: e
#include <unistd.h> unsigned int sleep(unsigned int seconds) Pone il processo in stato di sleep per seconds secondi. La funzione restituisce zero se lattesa viene completata, o il numero di secondi restanti se viene interrotta da un segnale.

La funzione attende per il tempo specicato, a meno di non essere interrotta da un segnale. In questo caso non ` una buona idea ripetere la chiamata per il tempo rimanente, in quanto e la riattivazione del processo pu` avvenire in un qualunque momento, ma il valore restituito o sar` sempre arrotondato al secondo, con la conseguenza che, se la successione dei segnali ` a e particolarmente sfortunata e le dierenze si accumulano, si potranno avere ritardi anche di parecchi secondi. In genere la scelta pi` sicura ` quella di stabilire un termine per lattesa, e u e ricalcolare tutte le volte il numero di secondi da aspettare. In alcune implementazioni inoltre luso di sleep pu` avere conitti con quello di SIGALRM, o dato che la funzione pu` essere realizzata con luso di pause e alarm (in maniera analoga o allesempio che vedremo in sez. 9.4.1). In tal caso mescolare chiamata di alarm e sleep o modicare lazione di SIGALRM, pu` causare risultati indeniti. Nel caso delle glibc ` stata usata o e una implementazione completamente indipendente e questi problemi non ci sono. La granularit` di sleep permette di specicare attese soltanto in secondi, per questo sia sotto a BSD4.3 che in SUSv2 ` stata denita la funzione usleep (dove la u ` intesa come sostituzione e e di ); i due standard hanno delle denizioni diverse, ma le glibc seguono10 seguono quella di SUSv2 che prevede il seguente prototipo:
#include <unistd.h> int usleep(unsigned long usec) Pone il processo in stato di sleep per usec microsecondi. La funzione restituisce zero se lattesa viene completata, o 1 in caso di errore, nel qual caso errno assumer` il valore EINTR. a si tratta in sostanza di funzioni che permettono di portare esplicitamente il processo in stato di sleep, vedi sez. 3.4.1. 10 secondo la pagina di manuale almeno dalla versione 2.2.2.
9

9.3. LA GESTIONE DI BASE DEI SEGNALI

229

Anche questa funzione, a seconda delle implementazioni, pu` presentare problemi nellinteo ` razione con alarm e SIGALRM. E pertanto deprecata in favore della funzione nanosleep, denita dallo standard POSIX1.b, il cui prototipo `: e
#include <unistd.h> int nanosleep(const struct timespec *req, struct timespec *rem) Pone il processo in stato di sleep per il tempo specicato da req. In caso di interruzione restituisce il tempo restante in rem. La funzione restituisce zero se lattesa viene completata, o 1 in caso di errore, nel qual caso errno assumer` uno dei valori: a EINVAL EINTR si ` specicato un numero di secondi negativo o un numero di nanosecondi maggiore e di 999.999.999. la funzione ` stata interrotta da un segnale. e

Lo standard richiede che la funzione sia implementata in maniera del tutto indipendente da alarm11 e sia utilizzabile senza interferenze con luso di SIGALRM. La funzione prende come argomenti delle strutture di tipo timespec, la cui denizione ` riportata in g. 8.9, che permettono e di specicare un tempo con una precisione (teorica) no al nanosecondo. La funzione risolve anche il problema di proseguire lattesa dopo linterruzione dovuta ad un segnale; infatti in tal caso in rem viene restituito il tempo rimanente rispetto a quanto richiesto inizialmente, e basta richiamare la funzione per completare lattesa. Chiaramente, anche se il tempo pu` essere specicato con risoluzioni no al nanosecondo, la o precisione di nanosleep ` determinata dalla risoluzione temporale del timer di sistema. Perci` e o la funzione attender` comunque il tempo specicato, ma prima che il processo possa tornare ad a essere eseguito occorrer` almeno attendere il successivo giro di scheduler e cio` un tempo che a a e seconda dei casi pu` arrivare no a 1/HZ, (sempre che il sistema sia scarico ed il processa venga o immediatamente rimesso in esecuzione); per questo motivo il valore restituito in rem ` sempre e arrotondato al multiplo successivo di 1/HZ. In realt` ` possibile ottenere anche pause pi` precise del centesimo di secondo usando poliae u tiche di scheduling real-time come SCHED_FIFO o SCHED_RR; in tal caso infatti il meccanismo di scheduling ordinario viene evitato, e si raggiungono pause no ai 2 ms con precisioni del s.

9.3.6

Un esempio elementare

Un semplice esempio per illustrare il funzionamento di un gestore di segnale ` quello della e gestione di SIGCHLD. Abbiamo visto in sez. 3.2.3 che una delle azioni eseguite dal kernel alla conclusione di un processo ` quella di inviare questo segnale al padre.12 In generale dunque, e quando non interessa elaborare lo stato di uscita di un processo, si pu` completare la gestione o della terminazione installando un gestore per SIGCHLD il cui unico compito sia quello di chiamare waitpid per completare la procedura di terminazione in modo da evitare la formazione di zombie. In g. 9.4 ` mostrato il codice contenente una implementazione generica di una funzione di e gestione per SIGCHLD, (che si trova nei sorgenti allegati nel le SigHand.c); se ripetiamo i test di sez. 3.2.3, invocando forktest con lopzione -s (che si limita ad eettuare linstallazione di questa funzione come gestore di SIGCHLD) potremo vericare che non si ha pi` la creazione di u zombie. Il codice del gestore ` di lettura immediata; come buona norma di programmazione (si e ricordi quanto accennato sez. 8.5.1) si comincia (6-7) con il salvare lo stato corrente di errno, in
nel caso di Linux questo ` fatto utilizzando direttamente il timer del kernel. e in realt` in SVr4 eredita la semantica di System V, in cui il segnale si chiama SIGCLD e viene trattato in a maniera speciale; in System V infatti se si imposta esplicitamente lazione a SIG_IGN il segnale non viene generato ed il sistema non genera zombie (lo stato di terminazione viene scartato senza dover chiamare una wait). Lazione predenita ` sempre quella di ignorare il segnale, ma non attiva questo comportamento. Linux, come BSD e e POSIX, non supporta questa semantica ed usa il nome di SIGCLD come sinonimo di SIGCHLD.
12 11

230

CAPITOLO 9. I SEGNALI

void HandSigCHLD ( int sig ) { 3 int errno_save ; 4 int status ; 5 pid_t pid ; 6 /* save errno current value */ 7 errno_save = errno ; 8 /* loop until no */ 9 do { 10 errno = 0; 11 pid = waitpid ( WAIT_ANY , & status , WNOHANG ); 12 } while ( pid > 0); 13 /* restore errno value */ 14 errno = errno_save ; 15 /* return */ 16 return ; 17 }
1 2

Figura 9.4: Codice di una funzione generica di gestione per il segnale SIGCHLD.

modo da poterlo ripristinare prima del ritorno del gestore (16-17). In questo modo si preserva il valore della variabile visto dal corso di esecuzione principale del processo, che altrimenti sarebbe sovrascritto dal valore restituito nella successiva chiamata di waitpid. Il compito principale del gestore ` quello di ricevere lo stato di terminazione del processo, e cosa che viene eseguita nel ciclo in (9-15). Il ciclo ` necessario a causa di una caratteristica e fondamentale della gestione dei segnali: abbiamo gi` accennato come fra la generazione di un a segnale e lesecuzione del gestore possa passare un certo lasso di tempo e niente ci assicura che il gestore venga eseguito prima della generazione di ulteriori segnali dello stesso tipo. In questo caso normalmente i segnali successivi vengono fusi col primo ed al processo ne viene recapitato soltanto uno. Questo pu` essere un caso comune proprio con SIGCHLD, qualora capiti che molti processi o gli terminino in rapida successione. Esso inoltre si presenta tutte le volte che un segnale viene bloccato: per quanti siano i segnali emessi durante il periodo di blocco, una volta che questultimo sar` rimosso verr` recapitato un solo segnale. a a Allora, nel caso della terminazione dei processi gli, se si chiamasse waitpid una sola volta, essa leggerebbe lo stato di terminazione per un solo processo, anche se i processi terminati sono pi` di uno, e gli altri resterebbero in stato di zombie per un tempo indenito. u Per questo occorre ripetere la chiamata di waitpid no a che essa non ritorni un valore nullo, segno che non resta nessun processo di cui si debba ancora ricevere lo stato di terminazione (si veda sez. 3.2.4 per la sintassi della funzione). Si noti anche come la funzione venga invocata con il parametro WNOHANG che permette di evitare il suo blocco quando tutti gli stati di terminazione sono stati ricevuti.

9.4

La gestione avanzata dei segnali

Le funzioni esaminate nora fanno riferimento alle modalit` pi` elementari della gestione dei a u segnali; non si sono pertanto ancora prese in considerazione le tematiche pi` complesse, collegate u alle varie race condition che i segnali possono generare e alla natura asincrona degli stessi. Aronteremo queste problematiche in questa sezione, partendo da un esempio che le evidenzi, per poi prendere in esame le varie funzioni che permettono di risolvere i problemi pi` u

9.4. LA GESTIONE AVANZATA DEI SEGNALI

231

complessi connessi alla programmazione con i segnali, no a trattare le caratteristiche generali della gestione dei medesimi nella casistica ordinaria.

9.4.1

Alcune problematiche aperte

Come accennato in sez. 9.3.5 ` possibile implementare sleep a partire dalluso di pause e alarm. e A prima vista questo pu` sembrare di implementazione immediata; ad esempio una semplice o versione di sleep potrebbe essere quella illustrata in g. 9.5. Dato che ` nostra intenzione utilizzare SIGALRM il primo passo della nostra implementazione e sar` quello di installare il relativo gestore salvando il precedente (14-17). Si eettuer` poi una a a chiamata ad alarm per specicare il tempo dattesa per linvio del segnale a cui segue la chiamata a pause per fermare il programma (18-20) no alla sua ricezione. Al ritorno di pause, causato dal ritorno del gestore (1-9), si ripristina il gestore originario (21-22) restituendo leventuale tempo rimanente (23-24) che potr` essere diverso da zero qualora linterruzione di pause venisse a causata da un altro segnale.
void alarm_hand ( int sig ) { /* check if the signal is the right one */ 3 if ( sig != SIGALRM ) { /* if not exit with error */ 4 printf ( " Something wrong , handler for SIGALRM \ n " ); 5 exit (1); 6 } else { /* do nothing , just interrupt pause */ 7 return ; 8 } 9 } 10 unsigned int sleep ( unsigned int seconds ) 11 { 12 sighandler_t prev_handler ; 13 /* install and check new handler */ 14 if (( prev_handler = signal ( SIGALRM , alarm_hand )) == SIG_ERR ) { 15 printf ( " Cannot set handler for alarm \ n " ); 16 exit ( -1); 17 } 18 /* set alarm and go to sleep */ 19 alarm ( seconds ); 20 pause (); 21 /* restore previous signal handler */ 22 signal ( SIGALRM , prev_handler ); 23 /* return remaining time */ 24 return alarm (0); 25 }
1 2

Figura 9.5: Una implementazione pericolosa di sleep.

Questo codice per`, a parte il non gestire il caso in cui si ` avuta una precedente chiamata o e a alarm (che si ` tralasciato per brevit`), presenta una pericolosa race condition. Infatti, se e a il processo viene interrotto fra la chiamata di alarm e pause, pu` capitare (ad esempio se o il sistema ` molto carico) che il tempo di attesa scada prima dellesecuzione di questultima, e cosicch essa sarebbe eseguita dopo larrivo di SIGALRM. In questo caso ci si troverebbe di fronte e ad un deadlock, in quanto pause non verrebbe mai pi` interrotta (se non in caso di un altro u segnale). Questo problema pu` essere risolto (ed ` la modalit` con cui veniva fatto in SVr2) usando o e a la funzione longjmp (vedi sez. 2.4.4) per uscire dal gestore; in questo modo, con una condizione sullo stato di uscita di questultima, si pu` evitare la chiamata a pause, usando un codice del o tipo di quello riportato in g. 9.6.

232

CAPITOLO 9. I SEGNALI

static jmp_buff alarm_return ; unsigned int sleep ( unsigned int seconds ) 3 { 4 signandler_t prev_handler ; 5 if (( prev_handler = signal ( SIGALRM , alarm_hand )) == SIG_ERR ) { 6 printf ( " Cannot set handler for alarm \ n " ); 7 exit (1); 8 } 9 if ( setjmp ( alarm_return ) == 0) { /* if not returning from handler */ 10 alarm ( second ); /* call alarm */ 11 pause (); /* then wait */ 12 } 13 /* restore previous signal handler */ 14 signal ( SIGALRM , prev_handler ); 15 /* remove alarm , return remaining time */ 16 return alarm (0); 17 } 18 void alarm_hand ( int sig ) 19 { 20 /* check if the signal is the right one */ 21 if ( sig != SIGALRM ) { /* if not exit with error */ 22 printf ( " Something wrong , handler for SIGALRM \ n " ); 23 exit (1); 24 } else { /* return in main after the call to pause */ 25 longjump ( alarm_return , 1); 26 } 27 }
1 2

Figura 9.6: Una implementazione ancora malfunzionante di sleep.

In questo caso il gestore (18-27) non ritorna come in g. 9.5, ma usa longjmp (25) per rientrare nel corpo principale del programma; dato che in questo caso il valore di uscita di setjmp ` 1, grazie alla condizione in (9-12) si evita comunque che pause sia chiamata a vuoto. e Ma anche questa implementazione comporta dei problemi; in questo caso infatti non viene gestita correttamente linterazione con gli altri segnali; se infatti il segnale di allarme interrompe un altro gestore, lesecuzione non riprender` nel gestore in questione, ma nel ciclo principale, a interrompendone inopportunamente lesecuzione. Lo stesso tipo di problemi si presenterebbero se si volesse usare alarm per stabilire un timeout su una qualunque system call bloccante. Un secondo esempio ` quello in cui si usa il segnale per noticare una qualche forma di evento; e in genere quello che si fa in questo caso ` impostare nel gestore un opportuno ag da controllare e nel corpo principale del programma (con un codice del tipo di quello riportato in g. 9.7). La logica ` quella di far impostare al gestore (14-19) una variabile globale preventivamente e inizializzata nel programma principale, il quale potr` determinare, osservandone il contenuto, a loccorrenza o meno del segnale, e prendere le relative azioni conseguenti (6-11). Questo ` il tipico esempio di caso, gi` citato in sez. 3.5.2, in cui si genera una race condition; e a infatti, in una situazione in cui un segnale ` gi` arrivato (e flag ` gi` ad 1) se un altro segnale e a e a arriva immediatamente dopo lesecuzione del controllo (6) ma prima della cancellazione del ag (7), la sua occorrenza sar` perduta. a Questi esempi ci mostrano che per una gestione eettiva dei segnali occorrono delle funzioni pi` sosticate di quelle nora illustrate, queste hanno la loro origine nella semplice interfaccia u dei primi sistemi Unix, ma con esse non ` possibile gestire in maniera adeguata di tutti i possibili e aspetti con cui un processo deve reagire alla ricezione di un segnale.

9.4. LA GESTIONE AVANZATA DEI SEGNALI

233

sig_atomic_t flag ; int main () 3 { 4 flag = 0; 5 ... 6 if ( flag ) { /* 7 flag = 0; /* 8 do_response (); /* 9 } else { 10 do_other (); /* 11 } 12 ... 13 } 14 void alarm_hand ( int sig ) 15 { 16 /* set the flag */ 17 flag = 1; 18 return ; 19 }
1 2

test if signal occurred */ reset flag */ do things */ do other things */

Figura 9.7: Un esempio non funzionante del codice per il controllo di un evento generato da un segnale.

9.4.2

Gli insiemi di segnali o signal set

Come evidenziato nel paragrafo precedente, le funzioni di gestione dei segnali originarie, nate con la semantica inadabile, hanno dei limiti non superabili; in particolare non ` prevista nessuna e funzione che permetta di gestire il blocco dei segnali o di vericare lo stato dei segnali pendenti. Per questo motivo lo standard POSIX.1, insieme alla nuova semantica dei segnali ha introdotto una interfaccia di gestione completamente nuova, che permette di ottenere un controllo molto pi` dettagliato. In particolare lo standard ha introdotto un nuovo tipo di dato sigset_t, che u permette di rappresentare un insieme di segnali (un signal set, come viene usualmente chiamato), tale tipo di dato viene usato per gestire il blocco dei segnali. In genere un insieme di segnali ` rappresentato da un intero di dimensione opportuna, di solito e pari al numero di bit dellarchitettura della macchina,13 ciascun bit del quale ` associato ad uno e specico segnale; in questo modo ` di solito possibile implementare le operazioni direttamente e con istruzioni elementari del processore. Lo standard POSIX.1 denisce cinque funzioni per la manipolazione degli insiemi di segnali: sigemptyset, sigfillset, sigaddset, sigdelset e sigismember, i cui prototipi sono:
#include <signal.h> int sigemptyset(sigset_t *set) Inizializza un insieme di segnali vuoto (in cui non c` nessun segnale). e int sigfillset(sigset_t *set) Inizializza un insieme di segnali pieno (in cui ci sono tutti i segnali). int sigaddset(sigset_t *set, int signum) Aggiunge il segnale signum allinsieme di segnali set. int sigdelset(sigset_t *set, int signum) Toglie il segnale signum dallinsieme di segnali set. int sigismember(const sigset_t *set, int signum) Controlla se il segnale signum ` nellinsieme di segnali set. e Le prime quattro funzioni ritornano 0 in caso di successo, mentre sigismember ritorna 1 se signum ` in set e 0 altrimenti. In caso di errore tutte ritornano 1, con errno impostata a EINVAL (il solo e errore possibile ` che signum non sia un segnale valido). e nel caso dei PC questo comporta un massimo di 32 segnali distinti: dato che in Linux questi sono sucienti non c` necessit` di nessuna struttura pi` complicata. e a u
13

234

CAPITOLO 9. I SEGNALI

Dato che in generale non si pu` fare conto sulle caratteristiche di una implementazione (non o ` detto che si disponga di un numero di bit sucienti per mettere tutti i segnali in un intero, e o in sigset_t possono essere immagazzinate ulteriori informazioni) tutte le operazioni devono essere comunque eseguite attraverso queste funzioni. In genere si usa un insieme di segnali per specicare quali segnali si vuole bloccare, o per riottenere dalle varie funzioni di gestione la maschera dei segnali attivi (vedi sez. 9.4.4). Essi possono essere deniti in due diverse maniere, aggiungendo i segnali voluti ad un insieme vuoto ottenuto con sigemptyset o togliendo quelli che non servono da un insieme completo ottenuto con sigfillset. Inne sigismember permette di vericare la presenza di uno specico segnale in un insieme.

9.4.3

La funzione sigaction

Abbiamo gi` accennato in sez. 9.3.2 i problemi di compatibilit` relativi alluso di signal. Per ova a viare a tutto questo lo standard POSIX.1 ha ridenito completamente linterfaccia per la gestione dei segnali, rendendola molto pi` essibile e robusta, anche se leggermente pi` complessa. u u La funzione principale dellinterfaccia POSIX.1 per i segnali ` sigaction. Essa ha sostane zialmente lo stesso uso di signal, permette cio` di specicare le modalit` con cui un segnale e a pu` essere gestito da un processo. Il suo prototipo `: o e
#include <signal.h> int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact) Installa una nuova azione per il segnale signum. La funzione restituisce zero in caso di successo e 1 per un errore, nel qual caso errno assumer` a i valori: EINVAL EFAULT si ` specicato un numero di segnale invalido o si ` cercato di installare il gestore per e e SIGKILL o SIGSTOP. si sono specicati indirizzi non validi.

La funzione serve ad installare una nuova azione per il segnale signum; si parla di azione e non di gestore come nel caso di signal, in quanto la funzione consente di specicare le varie caratteristiche della risposta al segnale, non solo la funzione che verr` eseguita alla sua occora renza. Per questo lo standard raccomanda di usare sempre questa funzione al posto di signal (che in genere viene denita tramite essa), in quanto permette un controllo completo su tutti gli aspetti della gestione di un segnale, sia pure al prezzo di una maggiore complessit` duso. a Se il puntatore act non ` nullo, la funzione installa la nuova azione da esso specicata, e se oldact non ` nullo il valore dellazione corrente viene restituito indietro. Questo permette e (specicando act nullo e oldact non nullo) di superare uno dei limiti di signal, che non consente di ottenere lazione corrente senza installarne una nuova. Entrambi i puntatori fanno riferimento alla struttura sigaction, tramite la quale si specicano tutte le caratteristiche dellazione associata ad un segnale. Anchessa ` descritta dallo standard e POSIX.1 ed in Linux ` denita secondo quanto riportato in g. 9.8. Il campo sa_restorer, non e previsto dallo standard, ` obsoleto e non deve essere pi` usato. e u Il campo sa_mask serve ad indicare linsieme dei segnali che devono essere bloccati durante lesecuzione del gestore, ad essi viene comunque sempre aggiunto il segnale che ne ha causato la chiamata, a meno che non si sia specicato con sa_flag un comportamento diverso. Quando il gestore ritorna comunque la maschera dei segnali bloccati (vedi sez. 9.4.4) viene ripristinata al valore precedente linvocazione. Luso di questo campo permette ad esempio di risolvere il problema residuo dellimplementazione di sleep mostrata in g. 9.6. In quel caso infatti se il segnale di allarme avesse interrotto un altro gestore questo non sarebbe stato eseguito correttamente; la cosa poteva essere prevenuta installando gli altri gestori usando sa_mask per bloccare SIGALRM durante la loro esecuzione. Il

9.4. LA GESTIONE AVANZATA DEI SEGNALI

235

struct sigaction { void (* sa_handler )( int ); void (* sa_sigaction )( int , siginfo_t * , void *); sigset_t sa_mask ; int sa_flags ; void (* sa_restorer )( void ); }

Figura 9.8: La struttura sigaction.

valore di sa_flag permette di specicare vari aspetti del comportamento di sigaction, e della reazione del processo ai vari segnali; i valori possibili ed il relativo signicato sono riportati in tab. 9.6.
Valore SA_NOCLDSTOP Signicato Se il segnale ` SIGCHLD allora non deve essere noticato e quando il processo glio viene fermato da uno dei segnali SIGSTOP, SIGTSTP, SIGTTIN o SIGTTOU. Ristabilisce lazione per il segnale al valore predenito una volta che il gestore ` stato lanciato, riproduce cio` il e e comportamento della semantica inadabile. Nome obsoleto, sinonimo non standard di SA_RESETHAND; da evitare. Stabilisce luso di uno stack alternativo per lesecuzione del gestore (vedi sez. 9.4.5). Riavvia automaticamente le slow system call quando vengono interrotte dal suddetto segnale; riproduce cio` il e comportamento standard di BSD. Evita che il segnale corrente sia bloccato durante lesecuzione del gestore. Nome obsoleto, sinonimo non standard di SA_NODEFER. Deve essere specicato quando si vuole usare un gestore in forma estesa usando sa_sigaction al posto di sa_handler. Se il segnale ` SIGCHLD allora o processi gli non divenire e zombie quando terminano.14

SA_RESETHAND

SA_ONESHOT SA_ONSTACK SA_RESTART

SA_NODEFER SA_NOMASK SA_SIGINFO

SA_NOCLDWAIT

Tabella 9.6: Valori del campo sa_flag della struttura sigaction.

Come si pu` notare in g. 9.8 sigaction permette di utilizzare due forme diverse di gestore,15 o da specicare, a seconda delluso o meno del ag SA_SIGINFO, rispettivamente attraverso i campi sa_sigaction o sa_handler,16 Questultima ` quella classica usata anche con signal, mentre e la prima permette di usare un gestore pi` complesso, in grado di ricevere informazioni pi` u u dettagliate dal sistema, attraverso la struttura siginfo_t, riportata in g. 9.9. Installando un gestore di tipo sa_sigaction diventa allora possibile accedere alle informazioni restituite attraverso il puntatore a questa struttura. Tutti i segnali impostano i campi si_signo, che riporta il numero del segnale ricevuto, si_errno, che riporta, quando diverso
questa funzionalit` ` stata introdotta nel kernel 2.6 e va a modicare il comportamento di waitpid. ae la possibilit` ` prevista dallo standard POSIX.1b, ed ` stata aggiunta nei kernel della serie 2.1.x con lintroae e duzione dei segnali real-time (vedi sez. 9.5.1); in precedenza era possibile ottenere alcune informazioni addizionali usando sa_handler con un secondo parametro addizionale di tipo sigcontext, che adesso ` deprecato. e 16 i due tipi devono essere usati in maniera alternativa, in certe implementazioni questi campi vengono addirittura deniti come union.
15 14

236

CAPITOLO 9. I SEGNALI

siginfo_t { int int int pid_t uid_t int clock_t clock_t sigval_t int void * void * int int }

si_signo ; si_errno ; si_code ; si_pid ; si_uid ; si_status ; si_utime ; si_stime ; si_value ; si_int ; si_ptr ; si_addr ; si_band ; si_fd ;

/* /* /* /* /* /* /* /* /* /* /* /* /* /*

Signal number */ An errno value */ Signal code */ Sending process ID */ Real user ID of sending process */ Exit value or signal */ User time consumed */ System time consumed */ Signal value */ POSIX .1 b signal */ POSIX .1 b signal */ Memory location which caused fault */ Band event */ File descriptor */

Figura 9.9: La struttura siginfo_t.

da zero, il codice dellerrore associato al segnale, e si_code, che viene usato dal kernel per specicare maggiori dettagli riguardo levento che ha causato lemissione del segnale. In generale si_code contiene, per i segnali generici, per quelli real-time e per tutti quelli inviati tramite kill, informazioni circa lorigine del segnale (se generato dal kernel, da un timer, da kill, ecc.). Alcuni segnali per` usano si_code per fornire una informazione specica: ad o esempio i vari segnali di errore (SIGFPE, SIGILL, SIGBUS e SIGSEGV) lo usano per fornire maggiori dettagli riguardo lerrore (come il tipo di errore aritmetico, di istruzione illecita o di violazione di memoria) mentre alcuni segnali di controllo (SIGCHLD, SIGTRAP e SIGPOLL) forniscono altre informazioni speciche. In tutti i casi il valore del campo ` riportato attraverso delle costanti (le e cui denizioni si trovano bits/siginfo.h) il cui elenco dettagliato ` disponibile nella pagina di e manuale di sigaction. Il resto della struttura ` denito come union ed i valori eventualmente presenti dipendono e dal segnale, cos` SIGCHLD ed i segnali real-time (vedi sez. 9.5.1) inviati tramite kill avvalorano si_pid e si_uid coi valori corrispondenti al processo che ha emesso il segnale, SIGILL, SIGFPE, SIGSEGV e SIGBUS avvalorano si_addr con lindirizzo in cui ` avvenuto lerrore, SIGIO (vedi e sez. 11.2.3) avvalora si_fd con il numero del le descriptor e si_band per i dati urgenti (vedi sez. 19.1.3) su un socket. Bench sia possibile usare nello stesso programma sia sigaction che signal occorre molta e attenzione, in quanto le due funzioni possono interagire in maniera anomala. Infatti lazione specicata con sigaction contiene un maggior numero di informazioni rispetto al semplice indirizzo del gestore restituito da signal. Per questo motivo se si usa questultima per installare un gestore sostituendone uno precedentemente installato con sigaction, non sar` possibile a eettuare un ripristino corretto dello stesso. Per questo ` sempre opportuno usare sigaction, che ` in grado di ripristinare correttamente e e un gestore precedente, anche se questo ` stato installato con signal. In generale poi non ` il e e caso di usare il valore di ritorno di signal come campo sa_handler, o viceversa, dato che in certi sistemi questi possono essere diversi. In denitiva dunque, a meno che non si sia vincolati alladerenza stretta allo standard ISO C, ` sempre il caso di evitare luso di signal a favore di e sigaction. Per questo motivo si ` provveduto, per mantenere uninterfaccia semplicata che abbia le e stesse caratteristiche di signal, a denire attraverso sigaction una funzione equivalente, il cui codice ` riportato in g. 9.10 (il codice completo si trova nel le SigHand.c nei sorgenti allegati). e

9.4. LA GESTIONE AVANZATA DEI SEGNALI

237

typedef void SigFunc ( int ); inline SigFunc * Signal ( int signo , SigFunc * func ) 3 { 4 struct sigaction new_handl , old_handl ; 5 new_handl . sa_handler = func ; 6 /* clear signal mask : no signal blocked during execution of func */ 7 if ( sigemptyset (& new_handl . sa_mask )!=0){ /* initialize signal set */ 8 return SIG_ERR ; 9 } 10 new_handl . sa_flags =0; /* init to 0 all flags */ 11 /* change action for signo signal */ 12 if ( sigaction ( signo , & new_handl , & old_handl )){ 13 return SIG_ERR ; 14 } 15 return ( old_handl . sa_handler ); 16 }
1 2

Figura 9.10: La funzione Signal, equivalente a signal, denita attraverso sigaction.

Si noti come, essendo la funzione estremamente semplice, ` denita come inline.17 e

9.4.4

La gestione della maschera dei segnali o signal mask

Come spiegato in sez. 9.1.2 tutti i moderni sistemi unix-like permettono di bloccare temporaneamente (o di eliminare completamente, impostando SIG_IGN come azione) la consegna dei segnali ad un processo. Questo ` fatto specicando la cosiddetta maschera dei segnali (o signal e 18 cio` linsieme dei segnali la cui consegna ` bloccata. Abbiamo accennato mask ) del processo e e in sez. 3.2.2 che la signal mask viene ereditata dal padre alla creazione di un processo glio, e abbiamo visto al paragrafo precedente che essa pu` essere modicata, durante lesecuzione di un o gestore, attraverso luso dal campo sa_mask di sigaction. Uno dei problemi evidenziatisi con lesempio di g. 9.7 ` che in molti casi ` necessario e e proteggere delle sezioni di codice (nel caso in questione la sezione fra il controllo e la eventuale cancellazione del ag che testimoniava lavvenuta occorrenza del segnale) in modo da essere sicuri che essi siano eseguite senza interruzioni. Le operazioni pi` semplici, come lassegnazione o il controllo di una variabile (per essere u sicuri si pu` usare il tipo sig_atomic_t) di norma sono atomiche; quando si devono eseguire o operazioni pi` complesse si pu` invece usare la funzione sigprocmask che permette di bloccare u o uno o pi` segnali; il suo prototipo `: u e

la direttiva inline viene usata per dire al compilatore di trattare la funzione cui essa fa riferimento in maniera speciale inserendo il codice direttamente nel testo del programma. Anche se i compilatori pi` moderni sono in grado u di eettuare da soli queste manipolazioni (impostando le opportune ottimizzazioni) questa ` una tecnica usata e per migliorare le prestazioni per le funzioni piccole ed usate di frequente (in particolare nel kernel, dove in certi casi le ottimizzazioni dal compilatore, tarate per luso in user space, non sono sempre adatte). In tal caso infatti le istruzioni per creare un nuovo frame nello stack per chiamare la funzione costituirebbero una parte rilevante del codice, appesantendo inutilmente il programma. Originariamente questo comportamento veniva ottenuto con delle macro, ma queste hanno tutta una serie di problemi di sintassi nel passaggio degli argomenti (si veda ad esempio [9]) che in questo modo possono essere evitati. 18 nel caso di Linux essa ` mantenuta dal campo blocked della task_struct del processo. e

17

238

CAPITOLO 9. I SEGNALI
#include <signal.h> int sigprocmask(int how, const sigset_t *set, sigset_t *oldset) Cambia la maschera dei segnali del processo corrente. La funzione restituisce zero in caso di successo e 1 per un errore, nel qual caso errno assumer` a i valori: EINVAL EFAULT si ` specicato un numero di segnale invalido. e si sono specicati indirizzi non validi.

La funzione usa linsieme di segnali dato allindirizzo set per modicare la maschera dei segnali del processo corrente. La modica viene eettuata a seconda del valore dellargomento how, secondo le modalit` specicate in tab. 9.7. Qualora si specichi un valore non nullo per a oldset la maschera dei segnali corrente viene salvata a quellindirizzo.
Valore SIG_BLOCK SIG_UNBLOCK Signicato Linsieme dei segnali bloccati ` lunione fra quello e specicato e quello corrente. I segnali specicati in set sono rimossi dalla maschera dei segnali, specicare la cancellazione di un segnale non bloccato ` legale. e La maschera dei segnali ` impostata al valore specicato e da set.

SIG_SETMASK

Tabella 9.7: Valori e signicato dellargomento how della funzione sigprocmask.

In questo modo diventa possibile proteggere delle sezioni di codice bloccando linsieme di segnali voluto per poi riabilitarli alla ne della sezione critica. La funzione permette di risolvere problemi come quelli mostrati in g. 9.7, proteggendo la sezione fra il controllo del ag e la sua cancellazione. La funzione pu` essere usata anche allinterno di un gestore, ad esempio per riabilitare la o consegna del segnale che lha invocato, in questo caso per` occorre ricordare che qualunque o modica alla maschera dei segnali viene perduta alla conclusione del terminatore. Bench con luso di sigprocmask si possano risolvere la maggior parte dei casi di race cone dition restano aperte alcune possibilit` legate alluso di pause; il caso ` simile a quello del a e problema illustrato nellesempio di g. 9.6, e cio` la possibilit` che il processo riceva il segnale e a che si intende usare per uscire dallo stato di attesa invocato con pause immediatamente prima dellesecuzione di questultima. Per poter eettuare atomicamente la modica della maschera dei segnali (di solito attivandone uno specico) insieme alla sospensione del processo lo standard POSIX ha previsto la funzione sigsuspend, il cui prototipo `: e
#include <signal.h> int sigsuspend(const sigset_t *mask) Imposta la signal mask specicata, mettendo in attesa il processo. La funzione restituisce zero in caso di successo e 1 per un errore, nel qual caso errno assumer` a i valori: EINVAL EFAULT si ` specicato un numero di segnale invalido. e si sono specicati indirizzi non validi.

Come esempio delluso di queste funzioni proviamo a riscrivere unaltra volta lesempio di implementazione di sleep. Abbiamo accennato in sez. 9.4.3 come con sigaction sia possibile bloccare SIGALRM nellinstallazione dei gestori degli altri segnali, per poter usare limplementazione vista in g. 9.6 senza interferenze. Questo per` comporta una precauzione ulteriore al o semplice uso della funzione, vediamo allora come usando la nuova interfaccia ` possibile ottenere e unimplementazione, riportata in g. 9.11 che non presenta neanche questa necessit`. a

9.4. LA GESTIONE AVANZATA DEI SEGNALI

239

void alarm_hand ( int ); unsigned int sleep ( unsigned int seconds ) 3 { 4 struct sigaction new_action , old_action ; 5 sigset_t old_mask , stop_mask , sleep_mask ; 6 /* set the signal handler */ 7 sigemptyset (& new_action . sa_mask ); /* no signal blocked */ 8 new_action . sa_handler = alarm_hand ; /* set handler */ 9 new_action . sa_flags = 0; /* no flags */ 10 sigaction ( SIGALRM , & new_action , & old_action ); /* install action */ 11 /* block SIGALRM to avoid race conditions */ 12 sigemptyset (& stop_mask ); /* init mask to empty */ 13 sigaddset (& stop_mask , SIGALRM ); /* add SIGALRM */ 14 sigprocmask ( SIG_BLOCK , & stop_mask , & old_mask ); /* add SIGALRM to blocked */ 15 /* send the alarm */ 16 alarm ( seconds ); 17 /* going to sleep enabling SIGALRM */ 18 sleep_mask = old_mask ; /* take mask */ 19 sigdelset (& sleep_mask , SIGALRM ); /* remove SIGALRM */ 20 sigsuspend (& sleep_mask ); /* go to sleep */ 21 /* restore previous settings */ 22 sigprocmask ( SIG_SETMASK , & old_mask , NULL ); /* reset signal mask */ 23 sigaction ( SIGALRM , & old_action , NULL ); /* reset signal action */ 24 /* return remaining time */ 25 return alarm (0); 26 } 27 void alarm_hand ( int sig ) 28 { 29 return ; /* just return to interrupt sigsuspend */ 30 }
1 2

Figura 9.11: Una implementazione completa di sleep.

Per evitare i problemi di interferenza con gli altri segnali in questo caso non si ` usato e lapproccio di g. 9.6 evitando luso di longjmp. Come in precedenza il gestore (27-30) non esegue nessuna operazione, limitandosi a ritornare per interrompere il programma messo in attesa. La prima parte della funzione (6-10) provvede ad installare lopportuno gestore per SIGALRM, salvando quello originario, che sar` ripristinato alla conclusione della stessa (23); il passo succesa sivo ` quello di bloccare SIGALRM (11-14) per evitare che esso possa essere ricevuto dal processo e fra lesecuzione di alarm (16) e la sospensione dello stesso. Nel fare questo si salva la maschera corrente dei segnali, che sar` ripristinata alla ne (22), e al contempo si prepara la maschera dei a segnali sleep_mask per riattivare SIGALRM allesecuzione di sigsuspend. In questo modo non sono pi` possibili race condition dato che SIGALRM viene disabilitato u con sigprocmask no alla chiamata di sigsuspend. Questo metodo ` assolutamente generale e e pu` essere applicato a qualunque altra situazione in cui si deve attendere per un segnale, i passi o sono sempre i seguenti: 1. Leggere la maschera dei segnali corrente e bloccare il segnale voluto con sigprocmask; 2. Mandare il processo in attesa con sigsuspend abilitando la ricezione del segnale voluto; 3. Ripristinare la maschera dei segnali originaria. Per quanto possa sembrare strano bloccare la ricezione di un segnale per poi riabilitarla immediatamente dopo, in questo modo si evita il deadlock dovuto allarrivo del segnale prima dellesecuzione di sigsuspend.

240

CAPITOLO 9. I SEGNALI

9.4.5

Ulteriori funzioni di gestione

In questo ultimo paragrafo esamineremo le rimanenti funzioni di gestione dei segnali non descritte nora, relative agli aspetti meno utilizzati e pi` esoterici della interfaccia. u La prima di queste funzioni ` sigpending, anchessa introdotta dallo standard POSIX.1; il e suo prototipo `: e
#include <signal.h> int sigpending(sigset_t *set) Scrive in set linsieme dei segnali pendenti. La funzione restituisce zero in caso di successo e 1 per un errore.

La funzione permette di ricavare quali sono i segnali pendenti per il processo in corso, cio` i e segnali che sono stati inviati dal kernel ma non sono stati ancora ricevuti dal processo in quanto bloccati. Non esiste una funzione equivalente nella vecchia interfaccia, ma essa ` tutto sommato e poco utile, dato che essa pu` solo assicurare che un segnale ` stato inviato, dato che escluderne o e lavvenuto invio al momento della chiamata non signica nulla rispetto a quanto potrebbe essere in un qualunque momento successivo. Una delle caratteristiche di BSD, disponibile anche in Linux, ` la possibilit` di usare uno e a stack alternativo per i segnali; ` cio` possibile fare usare al sistema un altro stack (invece di e e quello relativo al processo, vedi sez. 2.2.2) solo durante lesecuzione di un gestore. Luso di uno stack alternativo ` del tutto trasparente ai gestori, occorre per` seguire una certa procedura: e o 1. Allocare unarea di memoria di dimensione suciente da usare come stack alternativo; 2. Usare la funzione sigaltstack per rendere noto al sistema lesistenza e la locazione dello stack alternativo; 3. Quando si installa un gestore occorre usare sigaction specicando il ag SA_ONSTACK (vedi tab. 9.6) per dire al sistema di usare lo stack alternativo durante lesecuzione del gestore. In genere il primo passo viene eettuato allocando unopportuna area di memoria con malloc; in signal.h sono denite due costanti, SIGSTKSZ e MINSIGSTKSZ, che possono essere utilizzate per allocare una quantit` di spazio opportuna, in modo da evitare overow. La prima delle due ` a e la dimensione canonica per uno stack di segnali e di norma ` suciente per tutti gli usi normali. e La seconda ` lo spazio che occorre al sistema per essere in grado di lanciare il gestore e la e dimensione di uno stack alternativo deve essere sempre maggiore di questo valore. Quando si conosce esattamente quanto ` lo spazio necessario al gestore gli si pu` aggiungere questo valore e o per allocare uno stack di dimensione suciente. Come accennato, per poter essere usato, lo stack per i segnali deve essere indicato al sistema attraverso la funzione sigaltstack; il suo prototipo `: e
#include <signal.h> int sigaltstack(const stack_t *ss, stack_t *oss) Installa un nuovo stack per i segnali. La funzione restituisce zero in caso di successo e 1 per un errore, nel qual caso errno assumer` a i valori: ENOMEM EPERM EFAULT EINVAL la dimensione specicata per il nuovo stack ` minore di MINSIGSTKSZ. e uno degli indirizzi non ` valido. e si ` cercato di cambiare lo stack alternativo mentre questo ` attivo (cio` il processo ` e e e e in esecuzione su di esso). ss non ` nullo e ss_flags contiene un valore diverso da zero che non ` SS_DISABLE. e e

9.4. LA GESTIONE AVANZATA DEI SEGNALI

241

La funzione prende come argomenti puntatori ad una struttura di tipo stack_t, denita in g. 9.12. I due valori ss e oss, se non nulli, indicano rispettivamente il nuovo stack da installare e quello corrente (che viene restituito dalla funzione per un successivo ripristino).
typedef struct { void * ss_sp ; int ss_flags ; size_t ss_size ; } stack_t ;

/* Base address of stack */ /* Flags */ /* Number of bytes in stack */

Figura 9.12: La struttura stack_t.

Il campo ss_sp di stack_t indica lindirizzo base dello stack, mentre ss_size ne indica la dimensione; il campo ss_flags invece indica lo stato dello stack. Nellindicare un nuovo stack occorre inizializzare ss_sp e ss_size rispettivamente al puntatore e alla dimensione della memoria allocata, mentre ss_flags deve essere nullo. Se invece si vuole disabilitare uno stack occorre indicare SS_DISABLE come valore di ss_flags e gli altri valori saranno ignorati. Se oss non ` nullo verr` restituito dalla funzione indirizzo e dimensione dello stack corrente e a nei relativi campi, mentre ss_flags potr` assumere il valore SS_ONSTACK se il processo ` in a e esecuzione sullo stack alternativo (nel qual caso non ` possibile cambiarlo) e SS_DISABLE se e questo non ` abilitato. e In genere si installa uno stack alternativo per i segnali quando si teme di avere problemi di esaurimento dello stack standard o di superamento di un limite (vedi sez. 8.3.2) imposto con chiamate del tipo setrlimit(RLIMIT_STACK, &rlim). In tal caso infatti si avrebbe un segnale di SIGSEGV, che potrebbe essere gestito soltanto avendo abilitato uno stack alternativo. Si tenga presente che le funzioni chiamate durante lesecuzione sullo stack alternativo continueranno ad usare questultimo, che, al contrario di quanto avviene per lo stack ordinario dei processi, non si accresce automaticamente (ed infatti eccederne le dimensioni pu` portare a cono seguenze imprevedibili). Si ricordi inne che una chiamata ad una funzione della famiglia exec cancella ogni stack alternativo. Abbiamo visto in g. 9.6 come si possa usare longjmp per uscire da un gestore rientrando direttamente nel corpo del programma; sappiamo per` che nellesecuzione di un gestore il segnale o che lha invocato viene bloccato, e abbiamo detto che possiamo ulteriormente modicarlo con sigprocmask. Resta quindi il problema di cosa succede alla maschera dei segnali quando si esce da un gestore usando questa funzione. Il comportamento dipende dallimplementazione; in particolare BSD prevede che sia ripristinata la maschera dei segnali precedente linvocazione, come per un normale ritorno, mentre System V no. Lo standard POSIX.1 non specica questo comportamento per setjmp e longjmp, ed il comportamento delle glibc dipende da quale delle caratteristiche si sono abilitate con le macro viste in sez. 1.2.8. Lo standard POSIX per` prevede anche la presenza di altre due funzioni sigsetjmp e o siglongjmp, che permettono di decidere quale dei due comportamenti il programma deve assumere; i loro prototipi sono:
#include <setjmp.h> int sigsetjmp(sigjmp_buf env, int savesigs) Salva il contesto dello stack per un salto non-locale. void siglongjmp(sigjmp_buf env, int val) Esegue un salto non-locale su un precedente contesto. Le due funzioni sono identiche alle analoghe setjmp e longjmp di sez. 2.4.4, ma consentono di specicare il comportamento sul ripristino o meno della maschera dei segnali.

242

CAPITOLO 9. I SEGNALI

Le due funzioni prendono come primo argomento la variabile su cui viene salvato il contesto dello stack per permettere il salto non-locale; nel caso specico essa ` di tipo sigjmp_buf, e e non jmp_buf come per le analoghe di sez. 2.4.4 in quanto in questo caso viene salvata anche la maschera dei segnali. Nel caso di sigsetjmp, se si specica un valore di savesigs diverso da zero la maschera dei valori sar` salvata in env e ripristinata in un successivo siglongjmp; questultima funzione, a a parte luso di sigjmp_buf per env, ` assolutamente identica a longjmp. e

9.4.6

Criteri di programmazione per i gestori dei segnali

Abbiamo nora parlato dei gestori dei segnali come funzioni chiamate in corrispondenza della consegna di un segnale. In realt` un gestore non pu` essere una funzione qualunque, in quanto a o esso pu` essere eseguito in corrispondenza allinterruzione in un punto qualunque del programma o principale, ed ad esempio pu` essere problematico chiamare allinterno di un gestore di segnali o la stessa funzione che dal segnale ` stata interrotta. e Il concetto ` comunque pi` generale e porta ad una distinzione fra quelle che che POSIX e u chiama funzioni insicure (nUsane function) e funzioni sicure (safe function); quando un segnale interrompe una funzione insicura ed il gestore chiama al suo interno una funzione insicura il sistema pu` dare luogo ad un comportamento indenito. o Tutto questo signica che un gestore di segnale deve essere programmato con molta cura per evitare questa evenienza, pertanto ` non ` possibile chiamare al suo interno una funzione e e qualunque, e si pu` ricorrere soltanto alluso di funzioni sicure. o Lelenco delle funzioni sicure varia a secondo dello standard a cui si fa riferimento, secondo quanto riportato dallo standard POSIX 1003.1 nella revisione del 2003, le signal safe function che possono essere chiamate anche allinterno di un gestore di segnali sono quelle della lista riportata in g. 9.13.
_exit, abort, accept, access, aio_error aio_return, aio_suspend, alarm, bind, cfgetispeed, cfgetospeed, cfsetispeed, cfsetospeed, chdir, chmod, chown, clock_gettime, close, connect, creat, dup, dup2, execle, execve, fchmod, fchown, fcntl, fdatasync, fork, fpathconf, fstat, fsync, ftruncate, getegid, geteuid, getgid, getgroups, getpeername, getpgrp, getpid, getppid, getsockname, getsockopt, getuid, kill, link, listen, lseek, lstat, mkdir, mkfifo, open, pathconf, pause, pipe, poll, posix_trace_event, pselect, raise, read, readlink, recv, recvfrom, recvmsg, rename, rmdir, select, sem_post, send, sendmsg, sendto, setgid, setpgid, setsid, setsockopt, setuid, shutdown, sigaction, sigaddset, sigdelset, sigemptyset, sigfillset, sigismember, signal, sigpause, sigpending, sigprocmask, sigqueue, sigset, sigsuspend, sleep, socket, socketpair, stat, symlink, sysconf, tcdrain, tcflow, tcflush, tcgetattr, tcgetgrp, tcsendbreak, tcsetattr, tcsetpgrp, time, timer_getoverrun, timer_gettime, timer_settime, times, umask, uname, unlink, utime, wait, waitpid, write. Figura 9.13: Elenco delle funzioni sicure secondo lo standard POSIX 1003.1-2003.

Per questo motivo ` opportuno mantenere al minimo indispensabile le operazioni eettuate e allinterno di un gestore di segnali, qualora si debbano compiere operazioni complesse ` sempre e preferibile utilizzare la tecnica in cui si usa il gestore per impostare il valore di una qualche variabile globale, e poi si eseguono le operazioni complesse nel programma vericando (con tutti gli accorgimenti visti in precedenza) il valore di questa variabile tutte le volte che si ` rilevata e una interruzione dovuta ad un segnale.

9.5

Funzionalit` avanzate a

Tratteremo in questa ultima sezione alcune funzionalit` avanzate relativa ai segnali ed in generale a ai meccanismi di notica, a partire dalla funzioni introdotte per la gestione dei cosiddetti segnali

` 9.5. FUNZIONALITA AVANZATE

243

real-time, alla gestione avanzata delle temporizzazioni e le nuove interfacce per la gestione di segnali ed eventi attraverso luso di ledescriptor.

9.5.1

I segnali real-time

Lo standard POSIX.1b, nel denire una serie di nuove interfacce per i servizi real-time, ha introdotto una estensione del modello classico dei segnali che presenta dei signicativi miglioramenti,19 in particolare sono stati superati tre limiti fondamentali dei segnali classici: I segnali non sono accumulati se pi` segnali vengono generati prima dellesecuzione di un gestore questo sar` eseguito u a una sola volta, ed il processo non sar` in grado di accorgersi di quante volte levento che a ha generato il segnale ` accaduto; e I segnali non trasportano informazione i segnali classici non prevedono altra informazione sullevento che li ha generati se non il fatto che sono stati emessi (tutta linformazione che il kernel associa ad un segnale ` il e suo numero); I segnali non hanno un ordine di consegna lordine in cui diversi segnali vengono consegnati ` casuale e non prevedibile. Non ` e e possibile stabilire una priorit` per cui la reazione a certi segnali ha la precedenza rispetto a ad altri. Per poter superare queste limitazioni lo standard ha introdotto delle nuove caratteristiche, che sono state associate ad una nuova classe di segnali, che vengono chiamati segnali real-time, in particolare le funzionalit` aggiunte sono: a 1. i segnali sono inseriti in una coda che permette di consegnare istanze multiple dello stesso segnale qualora esso venga inviato pi` volte prima dellesecuzione del gestore; si assicura u cos` che il processo riceva un segnale per ogni occorrenza dellevento che lo genera. 2. ` stata introdotta una priorit` nella consegna dei segnali: i segnali vengono consegnati in e a ordine a seconda del loro valore, partendo da quelli con un numero minore, che pertanto hanno una priorit` maggiore. a 3. ` stata introdotta la possibilit` di restituire dei dati al gestore, attraverso luso di un e a apposito campo si_value nella struttura siginfo_t, accessibile tramite gestori di tipo sa_sigaction. Queste nuove funzionalit` (eccetto lultima, che, come vedremo, ` parzialmente disponibile a e anche con i segnali ordinari) si applicano solo ai nuovi segnali real-time; questi ultimi sono accessibili in un range di valori specicati dalle due macro SIGRTMIN e SIGRTMAX,20 che specicano il numero minimo e massimo associato ad un segnale real-time. I segnali con un numero pi` basso hanno una priorit` maggiore e vengono consegnati per u a primi, inoltre i segnali real-time non possono interrompere lesecuzione di un gestore di un segnale a priorit` pi` alta; la loro azione predenita ` quella di terminare il programma. I segnali ordinari a u e hanno tutti la stessa priorit`, che ` pi` alta di quella di qualunque segnale real-time. a e u Si tenga presente che questi nuovi segnali non sono associati a nessun evento specico, a meno di non utilizzarli in meccanismi di notica come quelli per lI/O asincrono (vedi sez. 11.2.3) o per le code di messaggi POSIX (vedi sez. 12.4.2); pertanto devono essere inviati esplicitamente.
questa estensione ` stata introdotta in Linux a partire dal kernel 2.1.43(?), e dalle glibc 2.1(?). e in Linux di solito (cio` sulla piattaforma i386) il primo valore ` 33, ed il secondo _NSIG-1, che di norma ` 64, e e e per un totale di 32 segnali disponibili, contro gli almeno 8 richiesti da POSIX.1b.
20 19

244

CAPITOLO 9. I SEGNALI

Inoltre, per poter usufruire della capacit` di restituire dei dati, i relativi gestori devono essere a installati con sigaction, specicando per sa_flags la modalit` SA_SIGINFO che permette di a utilizzare la forma estesa sa_sigaction (vedi sez. 9.4.3). In questo modo tutti i segnali realtime possono restituire al gestore una serie di informazioni aggiuntive attraverso largomento siginfo_t, la cui denizione ` stata gi` vista in g. 9.9, nella trattazione dei gestori in forma e a estesa. In particolare i campi utilizzati dai segnali real-time sono si_pid e si_uid in cui vengono memorizzati rispettivamente il pid e luser-ID eettivo del processo che ha inviato il segnale, mentre per la restituzione dei dati viene usato il campo si_value. Questo ` una union di tipo sigval_t (la sua denizione ` in g. 9.14) in cui pu` essere e e o memorizzato o un valore numerico, se usata nella forma sival_int, o un indirizzo, se usata nella forma sival_ptr. Lunione viene usata dai segnali real-time e da vari meccanismi di notica21 per restituire dati al gestore del segnale; in alcune denizioni essa viene identicata anche come union sigval.
union sigval_t { int sival_int ; void * sival_ptr ; }

Figura 9.14: La unione sigval_t.

A causa delle loro caratteristiche, la funzione kill non ` adatta ad inviare segnali real-time, e poich non ` in grado di fornire alcun valore per sigval_t; per questo motivo lo standard ha e e previsto una nuova funzione, sigqueue, il cui prototipo `: e
#include <signal.h> int sigqueue(pid_t pid, int signo, const sigval_t value) Invia il segnale signo al processo pid, restituendo al gestore il valore value. La funzione restituisce 0 in caso di successo e 1 in caso di errore, nel qual caso errno assumer` a uno dei valori: EAGAIN EPERM ESRCH EINVAL la coda ` esaurita, ci sono gi` SIGQUEUE_MAX segnali in attesa si consegna. e a non si hanno privilegi appropriati per inviare il segnale al processo specicato. il processo pid non esiste. si ` specicato un valore non valido per signo. e

ed inoltre ENOMEM.

Il comportamento della funzione ` analogo a quello di kill, ed i privilegi occorrenti ad e inviare il segnale ad un determinato processo sono gli stessi; un valore nullo di signo permette di vericare le condizioni di errore senza inviare nessun segnale. Se il segnale ` bloccato la funzione ritorna immediatamente, se si ` installato un gestore e e con SA_SIGINFO e ci sono risorse disponibili, (vale a dire che c` posto22 nella coda dei segnali e real-time) esso viene inserito e diventa pendente; una volta consegnato riporter` nel campo a si_code di siginfo_t il valore SI_QUEUE e il campo si_value ricever` quanto inviato con a value. Se invece si ` installato un gestore nella forma classica il segnale sar` generato, ma tutte e a le caratteristiche tipiche dei segnali real-time (priorit` e coda) saranno perse. a
un campo di tipo sigval_t ` presente anche nella struttura sigevent che viene usata dai meccanismi di e notica come quelli per lI/O asincrono (vedi sez. 11.2.3) o le code di messaggi POSIX (vedi sez. 12.4.2). 22 la profondit` della coda ` indicata dalla costante SIGQUEUE_MAX, una della tante costanti di sistema denite a e dallo standard POSIX che non abbiamo riportato esplicitamente in sez. 8.1.1; il suo valore minimo secondo lo standard, _POSIX_SIGQUEUE_MAX, ` pari a 32. Nel caso di Linux questo ` uno dei parametri del kernel impostabili e e sia con sysctl, che scrivendolo direttamente in /proc/sys/kernel/rtsig-max, il valore predenito ` di 1024. e
21

` 9.5. FUNZIONALITA AVANZATE

245

Lo standard POSIX.1b denisce inoltre delle nuove funzioni che permettono di gestire lattesa di segnali specici su una coda, esse servono in particolar modo nel caso dei thread, in cui si possono usare i segnali real-time come meccanismi di comunicazione elementare; la prima di queste funzioni ` sigwait, il cui prototipo `: e e
#include <signal.h> int sigwait(const sigset_t *set, int *sig) Attende che uno dei segnali specicati in set sia pendente. La funzione restituisce 0 in caso di successo e 1 in caso di errore, nel qual caso errno assumer` a uno dei valori: EINTR EINVAL la funzione ` stata interrotta. e si ` specicato un valore non valido per set. e

ed inoltre EFAULT.

La funzione estrae dallinsieme dei segnali pendenti uno qualunque dei segnali specicati da set, il cui valore viene restituito in sig. Se sono pendenti pi` segnali, viene estratto quello u a priorit` pi` alta (cio` con il numero pi` basso). Se, nel caso di segnali real-time, c` pi` di a u e u e u un segnale pendente, ne verr` estratto solo uno. Una volta estratto il segnale non verr` pi` a a u consegnato, e se era in una coda il suo posto sar` liberato. Se non c` nessun segnale pendente a e il processo viene bloccato ntanto che non ne arriva uno. Per un funzionamento corretto la funzione richiede che alla sua chiamata i segnali di set siano bloccati. In caso contrario si avrebbe un conitto con gli eventuali gestori: pertanto non si deve utilizzare per lo stesso segnale questa funzione e sigaction. Se questo non avviene il comportamento del sistema ` indeterminato: il segnale pu` sia essere consegnato che essere e o ricevuto da sigwait, il tutto in maniera non prevedibile. Lo standard POSIX.1b denisce altre due funzioni, anchesse usate prevalentemente con i thread; sigwaitinfo e sigtimedwait, i relativi prototipi sono:
#include <signal.h> int sigwaitinfo(const sigset_t *set, siginfo_t *info) Analoga a sigwait, ma riceve anche le informazioni associate al segnale in info. int sigtimedwait(const sigset_t *set, siginfo_t *value, const struct timespec *info) Analoga a sigwaitinfo, con un la possibilit` di specicare un timeout in timeout. a Le funzioni restituiscono 0 in caso di successo e 1 in caso di errore, nel qual caso errno assumer` a uno dei valori gi` visti per sigwait, ai quali si aggiunge, per sigtimedwait: a EAGAIN si ` superato il timeout senza che un segnale atteso fosse emesso. e

Entrambe le funzioni sono estensioni di sigwait. La prima permette di ricevere, oltre al numero del segnale, anche le informazioni ad esso associate tramite info; in particolare viene restituito il numero del segnale nel campo si_signo, la sua causa in si_code, e se il segnale ` e stato immesso sulla coda con sigqueue, il valore di ritorno ad esso associato viene riportato in si_value, che altrimenti ` indenito. e La seconda ` identica alla prima ma in pi` permette di specicare un timeout, scaduto il e u quale ritorner` con un errore. Se si specica un puntatore nullo il comportamento sar` identico a a a sigwaitinfo, se si specica un tempo di timeout nullo, e non ci sono segnali pendenti la funzione ritorner` immediatamente; in questo modo si pu` eliminare un segnale dalla coda senza dover a o essere bloccati qualora esso non sia presente. Luso di queste funzioni ` principalmente associato alla gestione dei segnali con i thread. In e genere esse vengono chiamate dal thread incaricato della gestione, che al ritorno della funzione esegue il codice che usualmente sarebbe messo nel gestore, per poi ripetere la chiamata per mettersi in attesa del segnale successivo. Questo ovviamente comporta che non devono essere installati gestori, che solo il thread di gestione deve usare sigwait e che, per evitare che venga

246

CAPITOLO 9. I SEGNALI

eseguita lazione predenita, i segnali gestiti in questa maniera devono essere mascherati per tutti i thread, compreso quello dedicato alla gestione, che potrebbe riceverlo fra due chiamate successive.

9.5.2 9.5.3

La gestione avanzata delle temporizzazioni Le interfacce per la notica attraverso i le descriptor

Capitolo 10

Terminali e sessioni di lavoro


I terminali per lungo tempo sono stati lunico modo per accedere al sistema, per questo anche oggi che esistono molte altre interfacce, essi continuano a coprire un ruolo particolare, restando strettamente legati al funzionamento dellinterfaccia a linea di comando. Nella prima parte del capitolo esamineremo i concetti base del sistema delle sessioni di lavoro, vale a dire il metodo con cui il kernel permette ad un utente di gestire le capacit` multitasking a del sistema, permettendo di eseguire pi` programmi in contemporanea. Nella seconda parte del u capitolo tratteremo poi il funzionamento dellI/O su terminale, e delle varie peculiarit` che esso a viene ad assumere a causa del suo stretto legame con il suo uso come interfaccia di accesso al sistema da parte degli utenti.

10.1

Il job control

Viene comunemente chiamato job control quellinsieme di funzionalit` il cui scopo ` quello di a e permettere ad un utente di poter sfruttare le capacit` multitasking di un sistema Unix per a eseguire in contemporanea pi` processi, pur potendo accedere, di solito, ad un solo terminale,1 u avendo cio` un solo punto in cui si pu` avere accesso allinput ed alloutput degli stessi. e o

10.1.1

Una panoramica introduttiva

Il job control ` una caratteristica opzionale, introdotta in BSD negli anni 80, e successivamente e standardizzata da POSIX.1; la sua disponibilit` nel sistema ` vericabile attraverso il controllo a e della macro _POSIX_JOB_CONTROL. In generale il job control richiede il supporto sia da parte della shell (quasi tutte ormai lo hanno), che da parte del kernel; in particolare il kernel deve assicurare sia la presenza di un driver per i terminali abilitato al job control che quella dei relativi segnali illustrati in sez. 9.2.6. In un sistema che supporta il job control, una volta completato il login, lutente avr` a a disposizione una shell dalla quale eseguire i comandi e potr` iniziare quella che viene chiamata a una sessione, che riunisce (vedi sez. 10.1.2) tutti i processi eseguiti allinterno dello stesso login (esamineremo tutto il processo in dettaglio in sez. 10.1.4). Siccome la shell ` collegata ad un solo terminale, che viene usualmente chiamato terminale e di controllo, (vedi sez. 10.1.3) un solo comando alla volta (quello che viene detto in foreground o in primo piano), potr` scrivere e leggere dal terminale. La shell per` pu` eseguire, aggiungendo a o o una & alla ne del comando, pi` programmi in contemporanea, mandandoli in background (o u sullo sfondo), nel qual caso essi saranno eseguiti senza essere collegati al terminale.
con X Window e con i terminali virtuali tutto questo non ` pi` vero, dato che si pu` accedere a molti terminali e u o in contemporanea da una singola postazione di lavoro, ma il sistema ` nato prima dellesistenza di tutto ci`. e o
1

247

248

CAPITOLO 10. TERMINALI E SESSIONI DI LAVORO

Si noti come si sia parlato di comandi e non di programmi o processi; fra le funzionalit` a della shell infatti c` anche quella di consentire di concatenare pi` programmi in una sola riga e u di comando con le pipe, ed in tal caso verranno eseguiti pi` programmi, inoltre, anche quando u si invoca un singolo programma, questo potr` sempre lanciare sottoprocessi per eseguire dei a compiti specici. Per questo lesecuzione di un comando pu` originare pi` di un processo; quindi nella gestione o u del job control non si pu` far riferimento ai singoli processi. Per questo il kernel prevede la o possibilit` di raggruppare pi` processi in un process group (detto anche raggruppamento di a u processi, vedi sez. 10.1.2) e la shell far` s` che tutti i processi che originano da una riga di a comando appartengano allo stesso raggruppamento, in modo che le varie funzioni di controllo, ed i segnali inviati dal terminale, possano fare riferimento ad esso. In generale allora allinterno di una sessione avremo un eventuale (pu` non esserci) process o group in foreground, che riunisce i processi che possono accedere al terminale, e pi` process u group in background, che non possono accedervi. Il job control prevede che quando un processo appartenente ad un raggruppamento in background cerca di accedere al terminale, venga inviato un segnale a tutti i processi del raggruppamento, in modo da bloccarli (vedi sez. 10.1.3). Un comportamento analogo si ha anche per i segnali generati dai comandi di tastiera inviati dal terminale che vengono inviati a tutti i processi del raggruppamento in foreground. In particolare C-z interrompe lesecuzione del comando, che pu` poi essere mandato in backo ground con il comando bg.2 Il comando fg consente invece di mettere in foreground un comando precedentemente lanciato in background. Di norma la shell si cura anche di noticare allutente (di solito prima della stampa a video del prompt) lo stato dei vari processi; essa infatti sar` in grado, grazie alluso di waitpid, di a rilevare sia i processi che sono terminati, sia i raggruppamenti che sono bloccati (in questo caso usando lopzione WUNTRACED, secondo quanto illustrato in sez. 3.2.4).

10.1.2

I process group e le sessioni

Come accennato in sez. 10.1.1 nel job control i processi vengono raggruppati in process group e sessioni; per far questo vengono utilizzati due ulteriori identicatori (oltre quelli visti in sez. 3.2.1) che il kernel associa a ciascun processo:3 lidenticatore del process group e lidenticatore della sessione, che vengono indicati rispettivamente con le sigle pgid e sid, e sono mantenuti in variabili di tipo pid_t. I valori di questi identicatori possono essere visualizzati dal comando ps usando lopzione -j. Un process group ` pertanto denito da tutti i processi che hanno lo stesso pgid; ` possibile e e leggere il valore di questo identicatore con le funzioni getpgid e getpgrp,4 i cui prototipi sono:
#include <unistd.h> pid_t getpgid(pid_t pid) Legge il pgid del processo pid. pid_t getpgrp(void) Legge il pgid del processo corrente. Le funzioni restituiscono il pgid del processo, getpgrp ha sempre successo, mentre getpgid restituisce -1 ponendo errno a ESRCH se il processo selezionato non esiste.

La funzione getpgid permette di specicare il pid del processo di cui si vuole sapere il pgid; un valore nullo per pid restituisce il pgid del processo corrente; getpgrp ` di norma equivalente e a getpgid(0).
2 si tenga presente che bg e fg sono parole chiave che indicano comandi interni alla shell, e nel caso non comportano lesecuzione di un programma esterno. 3 in Linux questi identicatori sono mantenuti nei campi pgrp e session della struttura task_struct denita in sched.h. 4 getpgrp ` denita nello standard POSIX.1, mentre getpgid ` richiesta da SVr4. e e

10.1. IL JOB CONTROL

249

In maniera analoga lidenticatore della sessione pu` essere letto dalla funzione getsid, che o per` nelle glibc 5 ` accessibile solo denendo _XOPEN_SOURCE e _XOPEN_SOURCE_EXTENDED; il suo o e prototipo `: e
#include <unistd.h> pid_t getsid(pid_t pid) Legge lidenticatore di sessione del processo pid. La funzione restituisce lidenticatore (un numero positivo) in caso di successo, e -1 in caso di errore, nel qual caso errno assumer` i valori: a ESRCH EPERM il processo selezionato non esiste. in alcune implementazioni viene restituito quando il processo selezionato non fa parte della stessa sessione del processo corrente.

Entrambi gli identicatori vengono inizializzati alla creazione di ciascun processo con lo stesso valore che hanno nel processo padre, per cui un processo appena creato appartiene sempre allo stesso raggruppamento e alla stessa sessione del padre. Vedremo poi come sia possibile creare pi` u process group allinterno della stessa sessione, e spostare i processi dalluno allaltro, ma sempre allinterno di una stessa sessione. Ciascun raggruppamento di processi ha sempre un processo principale, il cosiddetto process group leader, che ` identicato dallavere un pgid uguale al suo pid, in genere questo ` il primo e e processo del raggruppamento, che si incarica di lanciare tutti gli altri. Un nuovo raggruppamento si crea con la funzione setpgrp,6 il cui prototipo `: e
#include <unistd.h> int setpgrp(void) Modica il pgid al valore del pid del processo corrente. La funzione restituisce il valore del nuovo process group.

La funzione, assegnando al pgid il valore del pid processo corrente, rende questo group leader di un nuovo raggruppamento, tutti i successivi processi da esso creati apparterranno (a meno di ` non cambiare di nuovo il pgid) al nuovo raggruppamento. E possibile invece spostare un processo da un raggruppamento ad un altro con la funzione setpgid, il cui prototipo `: e
#include <unistd.h> int setpgid(pid_t pid, pid_t pgid) Assegna al pgid del processo pid il valore pgid. La funzione ritorna il valore del nuovo process group, e -1 in caso di errore, nel qual caso errno assumer` i valori: a ESRCH EPERM EACCES EINVAL il processo selezionato non esiste. il cambiamento non ` consentito. e il processo ha gi` eseguito una exec. a il valore di pgid ` negativo. e

La funzione permette di cambiare il pgid del processo pid, ma il cambiamento pu` essere o eettuato solo se pgid indica un process group che ` nella stessa sessione del processo chiamante. e Inoltre la funzione pu` essere usata soltanto sul processo corrente o su uno dei suoi gli, ed in o questultimo caso ha successo soltanto se questo non ha ancora eseguito una exec.7 Specicando
la system call ` stata introdotta in Linux a partire dalla versione 1.3.44, il supporto nelle librerie del C ` e e iniziato dalla versione 5.2.19. La funzione non ` prevista da POSIX.1, che parla solo di processi leader di sessione, e e non di identicatori di sessione. 6 questa ` la denizione di POSIX.1, BSD denisce una funzione con lo stesso nome, che per` ` identica a e o e setpgid; nelle glibc viene sempre usata sempre questa denizione, a meno di non richiedere esplicitamente la compatibilit` allindietro con BSD, denendo la macro _BSD_SOURCE. a 7 questa caratteristica ` implementata dal kernel che mantiene allo scopo un altro campo, did_exec, in e task_struct.
5

250

CAPITOLO 10. TERMINALI E SESSIONI DI LAVORO

un valore nullo per pid si indica il processo corrente, mentre specicando un valore nullo per pgid si imposta il process group al valore del pid del processo selezionato; pertanto setpgrp ` e equivalente a setpgid(0, 0). Di norma questa funzione viene usata dalla shell quando si usano delle pipeline, per mettere nello stesso process group tutti i programmi lanciati su ogni linea di comando; essa viene chiamata dopo una fork sia dal processo padre, per impostare il valore nel glio, che da questultimo, per s stesso, in modo che il cambiamento di process group sia immediato per entrambi; una delle e due chiamate sar` ridondante, ma non potendo determinare quale dei due processi viene eseguito a per primo, occorre eseguirle comunque entrambe per evitare di esporsi ad una race condition. Si noti come nessuna delle funzioni esaminate nora permetta di spostare un processo da una sessione ad un altra; infatti lunico modo di far cambiare sessione ad un processo ` quello e di crearne una nuova con luso di setsid; il suo prototipo `: e
#include <unistd.h> pid_t setsid(void) Crea una nuova sessione sul processo corrente impostandone sid e pgid. La funzione ritorna il valore del nuovo sid, e -1 in caso di errore, il solo errore possibile ` EPERM, e che si ha quando il pgid e pid del processo coincidono.

La funzione imposta il pgid ed il sid del processo corrente al valore del suo pid, creando cos` una nuova sessione ed un nuovo process group di cui esso diventa leader (come per i process group un processo si dice leader di sessione8 se il suo sid ` uguale al suo pid) ed unico componente. e Inoltre la funzione distacca il processo da ogni terminale di controllo (torneremo sullargomento in sez. 10.1.3) cui fosse in precedenza associato. La funzione ha successo soltanto se il processo non ` gi` leader di un process group, per cui e a per usarla di norma si esegue una fork e si esce, per poi chiamare setsid nel processo glio, in modo che, avendo questo lo stesso pgid del padre ma un pid diverso, non ci siano possibilit` di a errore.9 Questa funzione viene usata di solito nel processo di login (per i dettagli vedi sez. 10.1.4) per raggruppare in una sessione tutti i comandi eseguiti da un utente dalla sua shell.

10.1.3

Il terminale di controllo e il controllo di sessione

Come accennato in sez. 10.1.1, nel sistema del job control i processi allinterno di una sessione fanno riferimento ad un terminale di controllo (ad esempio quello su cui si ` eettuato il login), e sul quale eettuano le operazioni di lettura e scrittura,10 e dal quale ricevono gli eventuali segnali da tastiera. A tale scopo lo standard POSIX.1 prevede che ad ogni sessione possa essere associato un terminale di controllo; in Linux questo viene realizzato mantenendo fra gli attributi di ciascun processo anche qual` il suo terminale di controllo.11 In generale ogni processo eredita dal padre, e insieme al pgid e al sid anche il terminale di controllo (vedi sez. 3.2.2). In questo modo tutti processi originati dallo stesso leader di sessione mantengono lo stesso terminale di controllo. Alla creazione di una nuova sessione con setsid ogni associazione con il precedente terminale di controllo viene cancellata, ed il processo che ` divenuto un nuovo leader di sessione dovr` e a
in Linux la propriet` ` mantenuta in maniera indipendente con un apposito campo leader in task_struct. ae potrebbe sorgere il dubbio che, per il riutilizzo dei valori dei pid fatto nella creazione dei nuovi processi (vedi sez. 3.2.1), il glio venga ad assumere un valore corrispondente ad un process group esistente; questo viene evitato dal kernel che considera come disponibili per un nuovo pid solo valori che non corrispondono ad altri pid, pgid o sid in uso nel sistema. 10 nel caso di login graco la cosa pu` essere pi` complessa, e di norma lI/O ` eettuato tramite il server X, ma o u e ad esempio per i programmi, anche graci, lanciati da un qualunque emulatore di terminale, sar` questultimo a a fare da terminale (virtuale) di controllo. 11 lo standard POSIX.1 non specica nulla riguardo limplementazione; in Linux anchesso viene mantenuto nella solita struttura task_struct, nel campo tty.
9 8

10.1. IL JOB CONTROL

251

riottenere12 , un terminale di controllo. In generale questo viene fatto automaticamente dal sistema13 quando viene aperto il primo terminale (cio` uno dei vari le di dispositivo /dev/tty*) e che diventa automaticamente il terminale di controllo, mentre il processo diventa il processo di controllo di quella sessione. In genere (a meno di redirezioni) nelle sessioni di lavoro questo terminale ` associato ai le e standard (di input, output ed error) dei processi nella sessione, ma solo quelli che fanno parte del cosiddetto raggruppamento di foreground, possono leggere e scrivere in certo istante. Per impostare il raggruppamento di foreground di un terminale si usa la funzione tcsetpgrp, il cui prototipo `: e
#include <unistd.h> #include <termios.h> int tcsetpgrp(int fd, pid_t pgrpid) Imposta a pgrpid il process group di foreground del terminale associato al le descriptor fd. La funzione restituisce 0 in caso di successo, e -1 in caso di errore, nel qual caso errno assumer` a i valori: ENOTTY ENOSYS EPERM il le fd non corrisponde al terminale di controllo del processo chiamante. il sistema non supporta il job control. il process group specicato non ` nella stessa sessione del processo chiamante. e

ed inoltre EBADF ed EINVAL.

la funzione pu` essere eseguita con successo solo da un processo nella stessa sessione e con lo o stesso terminale di controllo. Come accennato in sez. 10.1.1, tutti i processi (e relativi raggruppamenti) che non fanno parte del gruppo di foreground sono detti in background ; se uno si essi cerca di accedere al terminale di controllo provocher` linvio da parte del kernel di uno dei due segnali SIGTTIN o a SIGTTOU (a seconda che laccesso sia stato in lettura o scrittura) a tutto il suo process group; dato che il comportamento di default di questi segnali (si riveda quanto esposto in sez. 9.2.6) ` di e fermare il processo, di norma questo comporta che tutti i membri del gruppo verranno fermati, ma non si avranno condizioni di errore.14 Se per` si bloccano o ignorano i due segnali citati, le o funzioni di lettura e scrittura falliranno con un errore di EIO. Un processo pu` controllare qual ` il gruppo di foreground associato ad un terminale con la o e funzione tcgetpgrp, il cui prototipo `: e
#include <unistd.h> #include <termios.h> pid_t tcgetpgrp(int fd) Legge il process group di foreground del terminale associato al le descriptor fd. La funzione restituisce in caso di successo il pgid del gruppo di foreground, e -1 in caso di errore, nel qual caso errno assumer` i valori: a ENOTTY non c` un terminale di controllo o fd non corrisponde al terminale di controllo del e processo chiamante.

ed inoltre EBADF ed ENOSYS.

Si noti come entrambe le funzioni usino come argomento il valore di un le descriptor, il risultato comunque non dipende dal le descriptor che si usa ma solo dal terminale cui fa riferimento; il kernel inoltre permette a ciascun processo di accedere direttamente al suo terminale di
solo quando ci` ` necessario, cosa che, come vedremo in sez. 10.1.5, non ` sempre vera. oe e a meno di non avere richiesto esplicitamente che questo non diventi un terminale di controllo con il ag O_NOCTTY (vedi sez. 6.2.1). In questo Linux segue la semantica di SVr4; BSD invece richiede che il terminale venga allocato esplicitamente con una ioctl con il comando TIOCSCTTY. 14 la shell in genere notica comunque un avvertimento, avvertendo la presenza di processi bloccati grazie alluso di waitpid.
13 12

252

CAPITOLO 10. TERMINALI E SESSIONI DI LAVORO

controllo attraverso il le speciale /dev/tty, che per ogni processo ` un sinonimo per il proprio e terminale di controllo. Questo consente anche a processi che possono aver rediretto loutput di accedere al terminale di controllo, pur non disponendo pi` del le descriptor originario; un caso u tipico ` il programma crypt che accetta la redirezione sullo standard input di un le da decifrare, e ma deve poi leggere la password dal terminale. Unaltra caratteristica del terminale di controllo usata nel job control ` che utilizzando su e di esso le combinazioni di tasti speciali (C-z, C-c, C-y e C-|) si far` s` che il kernel invii i corria spondenti segnali (rispettivamente SIGTSTP, SIGINT, SIGQUIT e SIGTERM, trattati in sez. 9.2.6) a tutti i processi del raggruppamento di foreground ; in questo modo la shell pu` gestire il blocco o e linterruzione dei vari comandi. Per completare la trattazione delle caratteristiche del job control legate al terminale di controllo, occorre prendere in considerazione i vari casi legati alla terminazione anomala dei processi, che sono di norma gestite attraverso il segnale SIGHUP. Il nome del segnale deriva da hungup, termine che viene usato per indicare la condizione in cui il terminale diventa inutilizzabile, (letteralmente sarebbe impiccagione). Quando si verica questa condizione, ad esempio se si interrompe la linea, o va gi` la rete u o pi` semplicemente si chiude forzatamente la nestra di terminale su cui si stava lavorando, il u kernel provveder` ad inviare il segnale di SIGHUP al processo di controllo. Lazione preimpostata a in questo caso ` la terminazione del processo, il problema che si pone ` cosa accade agli altri e e processi nella sessione, che non han pi` un processo di controllo che possa gestire laccesso al u terminale, che potrebbe essere riutilizzato per qualche altra sessione. Lo standard POSIX.1 prevede che quando il processo di controllo termina, che ci` avvenga o o meno per un hungup del terminale (ad esempio si potrebbe terminare direttamente la shell con kill) venga inviato un segnale di SIGHUP ai processi del raggruppamento di foreground. In questo modo essi potranno essere avvisati che non esiste pi` un processo in grado di gestire il u terminale (di norma tutto ci` comporta la terminazione anche di questi ultimi). o Restano per` gli eventuali processi in background, che non ricevono il segnale; in eetti se o il terminale non dovesse pi` servire essi potrebbero proseguire no al completamento della loro u esecuzione; ma si pone il problema di come gestire quelli che sono bloccati, o che si bloccano nellaccesso al terminale, in assenza di un processo che sia in grado di eettuare il controllo dello stesso. Questa ` la situazione in cui si ha quello che viene chiamato un orphaned process group. Lo e standard POSIX.1 lo denisce come un process group i cui processi hanno come padri esclusivamente o altri processi nel raggruppamento, o processi fuori della sessione. Lo standard prevede inoltre che se la terminazione di un processo fa s` che un raggruppamento di processi diventi orfano e se i suoi membri sono bloccati, ad essi vengano inviati in sequenza i segnali di SIGHUP e SIGCONT. La denizione pu` sembrare complicata, e a prima vista non ` chiaro cosa tutto ci` abbia o e o a che fare con il problema della terminazione del processo di controllo. Consideriamo allora cosa avviene di norma nel job control : una sessione viene creata con setsid che crea anche un nuovo process group: per denizione questultimo ` sempre orfano, dato che il padre del leader di e sessione ` fuori dalla stessa e il nuovo process group contiene solo il leader di sessione. Questo ` e e un caso limite, e non viene emesso nessun segnale perch quanto previsto dallo standard riguarda e solo i raggruppamenti che diventano orfani in seguito alla terminazione di un processo.15 Il leader di sessione provveder` a creare nuovi raggruppamenti che a questo punto non sono a orfani in quanto esso resta padre per almeno uno dei processi del gruppo (gli altri possono derivare dal primo). Alla terminazione del leader di sessione per` avremo che, come visto in o sez. 3.2.3, tutti i suoi gli vengono adottati da init, che ` fuori dalla sessione. Questo render` e a
lemissione dei segnali infatti avviene solo nella fase di uscita del processo, come una delle operazioni legate allesecuzione di _exit, secondo quanto illustrato in sez. 3.2.3.
15

10.1. IL JOB CONTROL

253

orfani tutti i process group creati direttamente dal leader di sessione (a meno di non aver spostato con setpgid un processo da un gruppo ad un altro, cosa che di norma non viene fatta) i quali riceveranno, nel caso siano bloccati, i due segnali; SIGCONT ne far` proseguire lesecuzione, ed a essendo stato nel frattempo inviato anche SIGHUP, se non c` un gestore per questultimo, i e processi bloccati verranno automaticamente terminati.

10.1.4

Dal login alla shell

Lorganizzazione del sistema del job control ` strettamente connessa alle modalit` con cui un e a utente accede al sistema per dare comandi, collegandosi ad esso con un terminale, che sia questo realmente tale, come un VT100 collegato ad una seriale o virtuale, come quelli associati a schermo e tastiera o ad una connessione di rete. Dato che i concetti base sono gli stessi, e dato che alla ne le dierenze sono16 nel dispositivo cui il kernel associa i le standard (vedi sez. 6.1.2) per lI/O, tratteremo solo il caso classico del terminale. Abbiamo gi` brevemente illustrato in sez. 1.1.2 le modalit` con cui il sistema si avvia, e di a a come, a partire da init, vengano lanciati tutti gli altri processi. Adesso vedremo in maniera pi` u dettagliata le modalit` con cui il sistema arriva a fornire ad un utente la shell che gli permette a di lanciare i suoi comandi su un terminale. Nella maggior parte delle distribuzioni di GNU/Linux17 viene usata la procedura di avvio di System V; questa prevede che init legga dal le di congurazione /etc/inittab quali programmi devono essere lanciati, ed in quali modalit`, a seconda del cosiddetto run level, anchesso a denito nello stesso le. Tralasciando la descrizione del sistema dei run level, (per il quale si rimanda alla lettura delle pagine di manuale di init e di inittab) quello che comunque viene sempre fatto ` di eseguire e almeno una istanza di un programma che permetta laccesso ad un terminale. Uno schema di massima della procedura ` riportato in g. 10.1. e

Figura 10.1: Schema della procedura di login su un terminale.

Un terminale, che esso sia un terminale eettivo, attaccato ad una seriale o ad un altro tipo di porta di comunicazione, o una delle console virtuali associate allo schermo, viene sempre visto attraverso un device driver che ne presenta uninterfaccia comune su un apposito le di dispositivo. Per controllare un terminale si usa di solito il programma getty (od una delle sue varianti), che permette di mettersi in ascolto su uno di questi dispositivi. Alla radice della catena che porta ad una shell per i comandi perci` c` sempre init che esegue prima una fork e poi una exec o e per lanciare una istanza di questo programma su un terminale, il tutto ripetuto per ciascuno
in generale nel caso di login via rete o di terminali lanciati dallinterfaccia graca cambia anche il processo da cui ha origine lesecuzione della shell. 17 fa eccezione la distribuzione Slackware, come alcune distribuzioni su dischetto, ed altre distribuzioni dedicate a compiti limitati e specici.
16

254

CAPITOLO 10. TERMINALI E SESSIONI DI LAVORO

dei terminali che si hanno a disposizione (o per un certo numero di essi, nel caso delle console virtuali), secondo quanto indicato dallamministratore nel le di congurazione del programma, /etc/inittab. Quando viene lanciato da init il programma parte con i privilegi di amministratore e con un ambiente vuoto; getty si cura di chiamare setsid per creare una nuova sessione ed un nuovo process group, e di aprire il terminale (che cos` diventa il terminale di controllo della sessione) in lettura sullo standard input ed in scrittura sullo standard output e sullo standard error; inoltre eettuer`, qualora servano, ulteriori impostazioni.18 Alla ne il programma stamper` un a a messaggio di benvenuto per poi porsi in attesa dellimmissione del nome di un utente. Una volta che si sia immesso il nome di login getty esegue direttamente il programma login con una exevle, passando come argomento la stringa con il nome, ed un ambiente opportunamente costruito che contenga quanto necessario (ad esempio di solito viene opportunamente inizializzata la variabile di ambiente TERM) ad identicare il terminale su cui si sta operando, a benecio dei programmi che verranno lanciati in seguito. A sua volta login, che mantiene i privilegi di amministratore, usa il nome dellutente per eettuare una ricerca nel database degli utenti,19 e richiede una password. Se lutente non esiste o se la password non corrisponde20 la richiesta viene ripetuta un certo numero di volte dopo di che login esce ed init provvede a rilanciare unaltra istanza di getty. Se invece la password corrisponde login esegue chdir per settare la home directory dellutente, cambia i diritti di accesso al terminale (con chown e chmod) per assegnarne la titolarit` a allutente ed al suo gruppo principale, assegnandogli al contempo i diritti di lettura e scrittura. Inoltre il programma provvede a costruire gli opportuni valori per le variabili di ambiente, come HOME, SHELL, ecc. Inne attraverso luso di setuid, setgid e initgroups verr` cambiata a lidentit` del proprietario del processo, infatti, come spiegato in sez. 3.3.2, avendo invocato tali a funzioni con i privilegi di amministratore, tutti gli user-ID ed i group-ID (reali, eettivi e salvati) saranno impostati a quelli dellutente. A questo punto login provveder` (fatte salve eventuali altre azioni iniziali, come la stampa a di messaggi di benvenuto o il controllo della posta) ad eseguire con unaltra exec la shell, che si trover` con un ambiente gi` pronto con i le standard di sez. 6.1.2 impostati sul terminale, a a e pronta, nel ruolo di leader di sessione e di processo di controllo per il terminale, a gestire lesecuzione dei comandi come illustrato in sez. 10.1.1. Dato che il processo padre resta sempre init questultimo potr` provvedere, ricevendo un a SIGCHLD alluscita della shell quando la sessione di lavoro ` terminata, a rilanciare getty sul e terminale per ripetere da capo tutto il procedimento.

10.1.5

Prescrizioni per un programma daemon

Come sottolineato n da sez. 1.1.1, in un sistema unix-like tutte le operazioni sono eseguite tramite processi, comprese quelle operazioni di sistema (come lesecuzione dei comandi periodici, o la consegna della posta, ed in generale tutti i programmi di servizio) che non hanno niente a che fare con la gestione diretta dei comandi dellutente. Questi programmi, che devono essere eseguiti in modalit` non interattiva e senza nessun a intervento dellutente, sono normalmente chiamati demoni, (o daemons), nome ispirato dagli
ad esempio, come qualcuno si sar` accorto scrivendo un nome di login in maiuscolo, pu` eettuare la convera o sione automatica dellinput in minuscolo, ponendosi in una modalit` speciale che non distingue fra i due tipi di a caratteri (a benecio di alcuni vecchi terminali che non supportavano le minuscole). 19 in genere viene chiamata getpwnam, che abbiamo visto in sez. 8.2.3, per leggere la password e gli altri dati dal database degli utenti. 20 il confronto non viene eettuato con un valore in chiaro; quanto immesso da terminale viene invece a sua volta criptato, ed ` il risultato che viene confrontato con il valore che viene mantenuto nel database degli utenti. e
18

10.1. IL JOB CONTROL

255

omonimi spiritelli della mitologia greca che svolgevano compiti che gli dei trovavano noiosi, di cui parla anche Socrate (che sosteneva di averne uno al suo servizio). Se per` si lancia un programma demone dalla riga di comando in un sistema che supporta, o come Linux, il job control esso verr` comunque associato ad un terminale di controllo e mantenuto a allinterno di una sessione, e anche se pu` essere mandato in background e non eseguire pi` o u nessun I/O su terminale, si avranno comunque tutte le conseguenze che abbiamo appena visto in sez. 10.1.3 (in particolare linvio dei segnali in corrispondenza delluscita del leader di sessione). Per questo motivo un programma che deve funzionare come demone deve sempre prendere autonomamente i provvedimenti opportuni (come distaccarsi dal terminale e dalla sessione) ad impedire eventuali interferenze da parte del sistema del job control ; questi sono riassunti in una lista di prescrizioni21 da seguire quando si scrive un demone. Pertanto, quando si lancia un programma che deve essere eseguito come demone occorrer` a predisporlo in modo che esso compia le seguenti azioni: 1. Eseguire una fork e terminare immediatamente il processo padre proseguendo lesecuzione nel glio. In questo modo si ha la certezza che il glio non ` un process group leader, (avr` e a il pgid del padre, ma un pid diverso) e si pu` chiamare setsid con successo. Inoltre la o shell considerer` terminato il comando alluscita del padre. a 2. Eseguire setsid per creare una nuova sessione ed un nuovo raggruppamento di cui il processo diventa automaticamente il leader, che per` non ha associato nessun terminale di o controllo. 3. Assicurarsi che al processo non venga associato in seguito nessun nuovo terminale di controllo; questo pu` essere fatto sia avendo cura di usare sempre lopzione O_NOCTTY nellao prire i le di terminale, che eseguendo una ulteriore fork uscendo nel padre e proseguendo nel glio. In questo caso, non essendo pi` questultimo un leader di sessione non potr` u a ottenere automaticamente un terminale di controllo. 4. Eseguire una chdir per impostare la directory di lavoro del processo (su / o su una directory che contenga dei le necessari per il programma), per evitare che la directory da cui si ` lanciato il processo resti in uso e non sia possibile rimuoverla o smontare il e lesystem che la contiene. 5. Impostare la maschera dei permessi (di solito con umask(0)) in modo da non essere dipendenti dal valore ereditato da chi ha lanciato originariamente il processo. 6. Chiudere tutti i le aperti che non servono pi` (in generale tutti); in particolare vanno u chiusi i le standard che di norma sono ancora associati al terminale (unaltra opzione ` e quella di redirigerli verso /dev/null). In Linux buona parte di queste azioni possono venire eseguite invocando la funzione daemon, introdotta per la prima volta in BSD4.4; il suo prototipo `: e
#include <unistd.h> int daemon(int nochdir, int noclose) Esegue le operazioni che distaccano il processo dal terminale di controllo e lo fanno girare come demone. La funzione restituisce (nel nuovo processo) 0 in caso di successo, e -1 in caso di errore, nel qual caso errno assumer` i valori impostati dalle sottostanti fork e setsid. a

La funzione esegue una fork, per uscire subito, con _exit, nel padre, mentre lesecuzione prosegue nel glio che esegue subito una setsid. In questo modo si compiono automaticamente
21

ad esempio sia Stevens in [1], che la Unix Programming FAQ [10] ne riportano di sostanzialmente identiche.

256

CAPITOLO 10. TERMINALI E SESSIONI DI LAVORO

i passi 1 e 2 della precedente lista. Se nochdir ` nullo la funzione imposta anche la directory di e lavoro su /, se noclose ` nullo i le standard vengono rediretti su /dev/null (corrispondenti ai e passi 4 e 6); in caso di valori non nulli non viene eseguita nessuna altra azione. Dato che un programma demone non pu` pi` accedere al terminale, si pone il problema di o u come fare per la notica di eventuali errori, non potendosi pi` utilizzare lo standard error; per il u normale I/O infatti ciascun demone avr` le sue modalit` di interazione col sistema e gli utenti a a a seconda dei compiti e delle funzionalit` che sono previste; ma gli errori devono normalmente a essere noticati allamministratore del sistema. Una soluzione pu` essere quella di scrivere gli eventuali messaggi su uno specico le (cosa o che a volte viene fatta comunque) ma questo comporta il grande svantaggio che lamministratore dovr` tenere sotto controllo un le diverso per ciascun demone, e che possono anche generarsi a conitti di nomi. Per questo in BSD4.2 venne introdotto un servizio di sistema, il syslog, che oggi si trova su tutti i sistemi Unix, e che permettesse ai demoni di inviare messaggi allamministratore in una maniera standardizzata. Il servizio prevede vari meccanismi di notica, e, come ogni altro servizio in un sistema unixlike, viene gestito attraverso un apposito programma, syslogd, che ` anchesso un demone. In e generale i messaggi di errore vengono raccolti dal le speciale /dev/log, un socket locale (vedi sez. 15.3.4) dedicato a questo scopo, o via rete, con un socket UDP, o da un apposito demone, klogd, che estrae i messaggi del kernel.22 Il servizio permette poi di trattare i vari messaggi classicandoli attraverso due indici; il primo, chiamato facility, suddivide in diverse categorie i vari demoni in modo di raggruppare i messaggi provenienti da operazioni che hanno attinenza fra loro, ed ` organizzato in sottosistee mi (kernel, posta elettronica, demoni di stampa, ecc.). Il secondo, chiamato priority, identica limportanza dei vari messaggi, e permette di classicarli e dierenziare le modalit` di notica a degli stessi. Il sistema di syslog attraverso syslogd provvede poi a riportare i messaggi allamministratore attraverso una serie dierenti meccanismi come: scrivere sulla console. inviare via mail ad uno specico utente. scrivere su un le (comunemente detto log le). inviare ad un altro demone (anche via rete). scartare.

secondo le modalit` che questo preferisce e che possono essere impostate attraverso il le di a congurazione /etc/syslog.conf (maggiori dettagli si possono trovare sulle pagine di manuale per questo le e per syslogd). Le glibc deniscono una serie di funzioni standard con cui un processo pu` accedere in o maniera generica al servizio di syslog, che per` funzionano solo localmente; se si vogliono inviare o i messaggi ad un altro sistema occorre farlo esplicitamente con un socket UDP, o utilizzare le capacit` di reinvio del servizio. a La prima funzione denita dallinterfaccia ` openlog, che apre una connessione al servizio di e syslog; essa in generale non ` necessaria per luso del servizio, ma permette di impostare alcuni e valori che controllano gli eetti delle chiamate successive; il suo prototipo `: e
#include <syslog.h> void openlog(const char *ident, int option, int facility) Apre una connessione al sistema di syslog. La funzione non restituisce nulla. i messaggi del kernel sono tenuti in un buer circolare e scritti tramite la funzione printk, analoga alla printf usata in user space; una trattazione eccellente dellargomento si trova in [5], nel quarto capitolo.
22

10.1. IL JOB CONTROL

257

La funzione permette di specicare, tramite ident, lidentit` di chi ha inviato il messaggio (di a norma si passa il nome del programma, come specicato da argv[0]); la stringa verr` preposta a allinizio di ogni messaggio. Si tenga presente che il valore di ident che si passa alla funzione ` e un puntatore, se la stringa cui punta viene cambiata lo sar` pure nei successivi messaggi, e se a viene cancellata i risultati potranno essere impredicibili, per questo ` sempre opportuno usare e una stringa costante. Largomento facility permette invece di preimpostare per le successive chiamate lomonimo indice che classica la categoria del messaggio. Largomento ` interpretato come una maschera e binaria, e pertanto ` possibile inviare i messaggi su pi` categorie alla volta; i valori delle costanti e u che identicano ciascuna categoria sono riportati in tab. 10.1, il valore di facility deve essere specicato con un OR aritmetico.
Valore LOG_AUTH LOG_AUTHPRIV LOG_CRON LOG_DAEMON LOG_FTP LOG_KERN LOG_LOCAL0 LOG_LOCAL7 LOG_LPR LOG_MAIL LOG_NEWS LOG_SYSLOG LOG_USER LOG_UUCP Signicato Messaggi relativi ad autenticazione e sicurezza, obsoleto, ` sostituito da LOG_AUTHPRIV. e Sostituisce LOG_AUTH. Messaggi dei demoni di gestione dei comandi programmati (cron e at). Demoni di sistema. Server FTP. Messaggi del kernel. Riservato allamministratore per uso locale. Riservato allamministratore per uso locale. Messaggi del sistema di gestione delle stampanti. Messaggi del sistema di posta elettronica. Messaggi del sistema di gestione delle news (USENET). Messaggi generati dallo stesso syslogd. Messaggi generici a livello utente. Messaggi del sistema UUCP.

Tabella 10.1: Valori possibili per largomento facility di openlog.

Largomento option serve invece per controllare il comportamento della funzione openlog e delle modalit` con cui le successive chiamate scriveranno i messaggi, esso viene specicato come a maschera binaria composta con un OR aritmetico di una qualunque delle costanti riportate in tab. 10.2.
Valore LOG_CONS LOG_NDELAY LOG_NOWAIT LOG_ODELAY LOG_PERROR LOG_PID Signicato Scrive sulla console quando. Sostituisce LOG_AUTH. Messaggi dei demoni di gestione dei comandi programmati (cron e at). Stampa anche su stderr. Inserisce nei messaggi il pid del processo chiamante.

Tabella 10.2: Valori possibili per largomento option di openlog.

La funzione che si usa per generare un messaggio ` syslog, dato che luso di openlog ` e e opzionale, sar` questultima a provvede a chiamare la prima qualora ci` non sia stato fatto (nel a o qual caso il valore di ident ` nullo). Il suo prototipo `: e e
#include <syslog.h> void syslog(int priority, const char *format, ...) Genera un messaggio di priorit` priority. a La funzione non restituisce nulla.

258

CAPITOLO 10. TERMINALI E SESSIONI DI LAVORO

Il comportamento della funzione ` analogo quello di printf, e il valore dellargomento format e ` identico a quello descritto nella pagina di manuale di questultima (per i valori principali si pu` e o vedere la trattazione sommaria che se ne ` fatto in sez. 7.2.6); lunica dierenza ` che la sequenza e e %m viene rimpiazzata dalla stringa restituita da strerror(errno). Gli argomenti seguenti i primi due devono essere forniti secondo quanto richiesto da format. Largomento priority permette di impostare sia la facility che la priority del messaggio. In realt` viene prevalentemente usato per specicare solo questultima in quanto la prima viene di a norma preimpostata con openlog. La priorit` ` indicata con un valore numerico23 specicabile ae attraverso le costanti riportate in tab. 10.3. Nel caso si voglia specicare anche la facility basta eseguire un OR aritmetico del valore della priorit` con la maschera binaria delle costanti di a tab. 10.1.
Valore LOG_EMERG LOG_ALERT LOG_CRIT LOG_ERR LOG_WARNING LOG_NOTICE LOG_INFO LOG_DEBUG Signicato Il sistema ` inutilizzabile. e C` una emergenza che richiede intervento immediato. e Si ` in una condizione critica. e Si ` in una condizione di errore. e Messaggio di avvertimento. Notizia signicativa relativa al comportamento. Messaggio informativo. Messaggio di debug.

Tabella 10.3: Valori possibili per lindice di importanza del messaggio da specicare nellargomento priority di syslog.

Una ulteriore funzione, setlogmask, permette di ltrare preliminarmente i messaggi in base alla loro priorit`; il suo prototipo `: a e
#include <syslog.h> int setlogmask(int mask) Imposta la maschera dei log al valore specicato. La funzione restituisce il precedente valore.

Le funzioni di gestione mantengono per ogni processo una maschera che determina quale delle chiamate eettuate a syslog verr` eettivamente registrata. La registrazione viene disabilitata a per tutte quelle priorit` che non rientrano nella maschera; questa viene impostata usando la a macro LOG_MASK(p) dove p ` una delle costanti di tab. 10.3. E inoltre disponibile anche la macro e LOG_UPTO(p) che permette di specicare automaticamente tutte le priorit` no ad un certo a valore.

10.2

LI/O su terminale

Bench come ogni altro dispositivo i terminali siano accessibili come le, essi hanno assunto e storicamente (essendo stati a lungo lunico modo di accedere al sistema) una loro rilevanza specica, che abbiamo gi` avuto modo di incontrare nella precedente sezione. a Esamineremo qui le peculiarit` dellI/O eseguito sui terminali, che per la loro particolare a natura presenta delle dierenze rispetto ai normali le su disco e agli altri dispositivi.
le glibc, seguendo POSIX.1-2001, prevedono otto diverse priorit` ordinate da 0 a 7, in ordine di importanza a decrescente; questo comporta che i tre bit meno signicativi dellargomento priority sono occupati da questo valore, mentre i restanti bit pi` signicativi vengono usati per specicare la facility. u
23

10.2. LI/O SU TERMINALE

259

10.2.1

Larchitettura

I terminali sono una classe speciale di dispositivi a caratteri (si ricordi la classicazione di sez. 4.1.2); un terminale ha infatti una caratteristica che lo contraddistingue da un qualunque altro dispositivo, e cio` che ` destinato a gestire linterazione con un utente (deve essere cio` in e e e grado di fare da terminale di controllo per una sessione), che comporta la presenza di ulteriori capacit`. a Linterfaccia per i terminali ` una delle pi` oscure e complesse, essendosi straticata dagli e u inizi dei sistemi Unix no ad oggi. Questo comporta una grande quantit` di opzioni e controlli a relativi ad un insieme di caratteristiche (come ad esempio la velocit` della linea) necessarie per a dispositivi, come i terminali seriali, che al giorno doggi sono praticamente in disuso. Storicamente i primi terminali erano appunto terminali di telescriventi (teletype), da cui deriva sia il nome dellinterfaccia, TTY, che quello dei relativi le di dispositivo, che sono sempre della forma /dev/tty*.24 Oggi essi includono le porte seriali, le console virtuali dello schermo, i terminali virtuali che vengono creati come canali di comunicazione dal kernel e che di solito vengono associati alle connessioni di rete (ad esempio per trattare i dati inviati con telnet o ssh). LI/O sui terminali si eettua con le stesse modalit` dei le normali: si apre il relativo le a di dispositivo, e si leggono e scrivono i dati con le usuali funzioni di lettura e scrittura, cos` se apriamo una console virtuale avremo che read legger` quanto immesso dalla tastiera, mentre a write scriver` sullo schermo. In realt` questo ` vero solo a grandi linee, perch non tiene conto a a e e delle caratteristiche speciche dei terminali; una delle principali infatti ` che essi prevedono e due modalit` di operazione, dette rispettivamente modo canonico e modo non canonico, che a comportano dei comportamenti nettamente diversi. La modalit` preimpostata allapertura del terminale ` quella canonica, in cui le operazioni a e di lettura vengono sempre eettuate assemblando i dati in una linea;25 ed in cui alcuni caratteri vengono interpretati per compiere operazioni (come la generazione dei segnali illustrati in sez. 9.2.6), questa di norma ` la modalit` in cui funziona la shell. e a Un terminale in modo non canonico invece non eettua nessun accorpamento dei dati in linee n li interpreta; esso viene di solito usato dai programmi (gli editor ad esempio) che necessitano e di poter leggere un carattere alla volta e che gestiscono al loro interno i vari comandi. Per capire le caratteristiche dellI/O sui terminali, occorre esaminare le modalit` con cui esso a viene eettuato; laccesso, come per tutti i dispositivi, viene gestito da un driver apposito, la cui struttura generica ` mostrata in g. 10.2. Ad un terminale sono sempre associate due code per e gestire linput e loutput, che ne implementano una buerizzazione26 allinterno del kernel. La coda di ingresso mantiene i caratteri che sono stati letti dal terminale ma non ancora letti da un processo, la sua dimensione ` denita dal parametro di sistema MAX_INPUT (si veda e sez. 8.1.3), che ne specica il limite minimo, in realt` la coda pu` essere pi` grande e cambiare a o u dimensione dinamicamente. Se ` stato abilitato il controllo di usso in ingresso il driver emette e i caratteri di STOP e START per bloccare e sbloccare lingresso dei dati; altrimenti i caratteri immessi oltre le dimensioni massime vengono persi; in alcuni casi il driver provvede ad inviare automaticamente un avviso (un carattere di BELL, che provoca un beep) sulloutput quando si eccedono le dimensioni della coda. Se ` abilitato il modo canonico i caratteri in ingresso restano e nella coda ntanto che non viene ricevuto un a capo; un altro parametro del sistema, MAX_CANON, specica la dimensione massima di una riga in modo canonico.
ci` vale solo in parte per i terminali virtuali, essi infatti hanno due lati, un master, che pu` assumere i nomi o o /dev/pty[p-za-e][0-9a-f] ed un corrispondente slave con nome /dev/tty[p-za-e][0-9a-f]. 25 per cui eseguendo una read su un terminale in modo canonico la funzione si bloccher`, anche se si sono scritti a dei caratteri, ntanto che non si preme il tasto di ritorno a capo: a questo punto la linea sar` completa e la a funzione ritorner`. a 26 completamente indipendente dalla eventuale ulteriore buerizzazione fornita dallinterfaccia standard dei le.
24

260

CAPITOLO 10. TERMINALI E SESSIONI DI LAVORO

Figura 10.2: Struttura interna generica di un driver per un terminale.

La coda di uscita ` analoga a quella di ingresso e contiene i caratteri scritti dai processi ma e non ancora inviati al terminale. Se ` abilitato il controllo di usso in uscita il driver risponde ai e caratteri di START e STOP inviati dal terminale. Le dimensioni della coda non sono specicate, ma non hanno molta importanza, in quanto qualora esse vengano eccedute il driver provvede automaticamente a bloccare la funzione chiamante.

10.2.2

La gestione delle caratteristiche di un terminale

Data le loro peculiarit`, n dallinizio si ` posto il problema di come gestire le caratteristiche a e speciche dei terminali; storicamente i vari dialetti di Unix hanno utilizzato diverse funzioni, alla ne con POSIX.1, ` stata eettuata una standardizzazione, unicando le dierenze fra BSD e e System V in una unica interfaccia, che ` quella usata dal Linux. e Alcune di queste funzioni prendono come argomento un le descriptor (in origine molte operazioni venivano eettuate con ioctl), ma ovviamente possono essere usate solo con le che corrispondano eettivamente ad un terminale (altrimenti si otterr` un errore di ENOTTY); questo a pu` essere evitato utilizzando la funzione isatty, il cui prototipo `: o e
#include <unistd.h> int isatty(int desc) Controlla se il le descriptor desc ` un terminale. e La funzione restituisce 1 se desc ` connesso ad un terminale, 0 altrimenti. e

Unaltra funzione che fornisce informazioni su un terminale ` ttyname, che permette di e ottenere il nome del terminale associato ad un le descriptor; il suo prototipo `: e
#include <unistd.h> char *ttyname(int desc) Restituisce il nome del terminale associato al le desc. La funzione restituisce il puntatore alla stringa contenente il nome del terminale associato desc e NULL in caso di errore.

Si tenga presente che la funzione restituisce un indirizzo di dati statici, che pertanto possono

10.2. LI/O SU TERMINALE

261

essere sovrascritti da successive chiamate. Una funzione funzione analoga, anchessa prevista da POSIX.1, ` ctermid, il cui prototipo `: e e
#include <stdio.h> char *ctermid(char *s) Restituisce il nome del terminale di controllo del processo. La funzione restituisce il puntatore alla stringa contenente il pathname del terminale.

La funzione scrive il pathname del terminale di controllo del processo chiamante nella stringa posta allindirizzo specicato dallargomento s. La memoria per contenere la stringa deve essere stata allocata in precedenza ed essere lunga almeno L_ctermid27 caratteri. Esiste inne una versione rientrante ttyname_r della funzione ttyname, che non presenta il problema delluso di una zona di memoria statica; il suo prototipo `: e
#include <unistd.h> int ttyname_r(int desc, char *buff, size_t len) Restituisce il nome del terminale associato al le desc. La funzione restituisce 0 in caso di successo e -1 in caso di errore, nel qual caso errno assumer` i a valori: ERANGE la lunghezza del buer, len, non ` suciente per contenere la stringa restituita. e ed inoltre EBADF ed ENOSYS.

La funzione prende due argomenti, il puntatore alla zona di memoria buff, in cui lutente vuole che il risultato venga scritto (dovr` ovviamente essere stata allocata in precedenza), e la a relativa dimensione, len; se la stringa che deve essere restituita eccede questa dimensione si avr` a una condizione di errore. Se si passa come argomento NULL la funzione restituisce il puntatore ad una stringa statica che pu` essere sovrascritta da chiamate successive. Si tenga presente che il pathname restituito o potrebbe non identicare univocamente il terminale (ad esempio potrebbe essere /dev/tty), inoltre non ` detto che il processo possa eettivamente aprire il terminale. e I vari attributi vengono mantenuti per ciascun terminale in una struttura termios, (la cui denizione ` riportata in g. 10.3), usata dalle varie funzioni dellinterfaccia. In g. 10.3 si sono e riportati tutti i campi della denizione usata in Linux; di questi solo i primi cinque sono previsti dallo standard POSIX.1, ma le varie implementazioni ne aggiungono degli altri per mantenere ulteriori informazioni.28
struct termios { tcflag_t c_iflag ; tcflag_t c_oflag ; tcflag_t c_cflag ; tcflag_t c_lflag ; cc_t c_cc [ NCCS ]; cc_t c_line ; speed_t c_ispeed ; speed_t c_ospeed ; };

/* /* /* /* /* /* /* /*

input modes */ output modes */ control modes */ local modes */ control characters */ line discipline */ input speed */ output speed */

Figura 10.3: La struttura termios, che identica le propriet` di un terminale. a


27 L_ctermid ` una delle varie costanti del sistema, non trattata esplicitamente in sez. 8.1 che indica la dimensione e che deve avere una stringa per poter contenere il nome di un terminale. 28 la denizione della struttura si trova in bits/termios.h, da non includere mai direttamente, Linux, seguendo lesempio di BSD, aggiunge i due campi c_ispeed e c_ospeed per mantenere le velocit` delle linee seriali, ed un a campo ulteriore, c_line per ... (NdT, trovare a che serve).

262

CAPITOLO 10. TERMINALI E SESSIONI DI LAVORO

I primi quattro campi sono quattro ag che controllano il comportamento del terminale; essi sono realizzati come maschera binaria, pertanto il tipo tcflag_t ` di norma realizzato con un e intero senza segno di lunghezza opportuna. I valori devono essere specicati bit per bit, avendo cura di non modicare i bit su cui non si interviene.
Valore INPCK Signicato Abilita il controllo di parit` in ingresso. Se non viene impostato non a viene fatto nessun controllo ed i caratteri vengono passati in input direttamente. Ignora gli errori di parit`, il carattere viene passato come ricevuto. Ha a senso solo se si ` impostato INPCK. e Controlla come vengono riportati gli errori di parit`. Ha senso solo se a INPCK ` impostato e IGNPAR no. Se impostato inserisce una sequenza e 0xFF 0x00 prima di ogni carattere che presenta errori di parit`, se non a impostato un carattere con errori di parit` viene letto come uno 0x00. a Se un carattere ha il valore 0xFF e ISTRIP non ` impostato, per evitare e ambiguit` esso viene sempre riportato come 0xFF 0xFF. a Se impostato i caratteri in input sono tagliati a sette bit mettendo a zero il bit pi` signicativo, altrimenti vengono passati tutti gli otto bit. u Ignora le condizioni di BREAK sullinput. Una condizione di BREAK ` denita nel contesto di una trasmissione seriale asincrona come una e sequenza di bit nulli pi` lunga di un byte. u Controlla la reazione ad un BREAK quando IGNBRK non ` impostato. e Se BRKINT ` impostato il BREAK causa lo scarico delle code, e se il e terminale ` il terminale di controllo per un gruppo in foreground anche e linvio di SIGINT ai processi di questultimo. Se invece BRKINT non ` e impostato un BREAK viene letto come un carattere NUL, a meno che non sia impostato PARMRK nel qual caso viene letto come la sequenza di caratteri 0xFF 0x00 0x00. Se impostato il carattere di ritorno carrello (carriage return, \r) viene scartato dallinput. Pu` essere utile per i terminali che inviano entrambi o i caratteri di ritorno carrello e a capo (newline, \n). Se impostato un carattere di ritorno carrello (\r) sul terminale viene automaticamente trasformato in un a capo (\n) sulla coda di input. Se impostato il carattere di a capo (\n) viene automaticamente trasformato in un ritorno carrello (\r). Se impostato trasforma i caratteri maiuscoli dal terminale in minuscoli sullingresso (opzione non POSIX). Se impostato attiva il controllo di usso in uscita con i caratteri di START e STOP. se si riceve uno STOP loutput viene bloccato, e viene fatto ripartire solo da uno START, e questi due caratteri non vengono passati alla coda di input. Se non impostato i due caratteri sono passati alla coda di input insieme agli altri. Se impostato con il controllo di usso permette a qualunque carattere di far ripartire loutput bloccato da un carattere di STOP. Se impostato abilita il controllo di usso in ingresso. Il computer emette un carattere di STOP per bloccare linput dal terminale e lo sblocca con il carattere START. Se impostato fa suonare il cicalino se si riempie la cosa di ingresso; in Linux non ` implementato e il kernel si comporta cose se fosse sempre e impostato (` una estensione BSD). e

IGNPAR PARMRK

ISTRIP IGNBRK

BRKINT

IGNCR

ICRNL INLCR IUCLC IXON

IXANY IXOFF

IMAXBEL

Tabella 10.4: Costanti identicative dei vari bit del ag di controllo c_iflag delle modalit` di input di un a terminale.

Il primo ag, mantenuto nel campo c_iflag, ` detto ag di input e controlla le modalit` di e a funzionamento dellinput dei caratteri sul terminale, come il controllo di parit`, il controllo di a usso, la gestione dei caratteri speciali; un elenco dei vari bit, del loro signicato e delle costanti utilizzate per identicarli ` riportato in tab. 10.4. e Si noti come alcuni di questi ag (come quelli per la gestione del usso) fanno riferimento

10.2. LI/O SU TERMINALE

263

a delle caratteristiche che ormai sono completamente obsolete; la maggior parte inoltre ` tipica e di terminali seriali, e non ha alcun eetto su dispositivi diversi come le console virtuali o gli pseudo-terminali usati nelle connessioni di rete.
Valore OPOST Signicato Se impostato i caratteri vengono convertiti opportunamente (in maniera dipendente dallimplementazione) per la visualizzazione sul terminale, ad esempio al carattere di a capo (NL) pu` venire aggiunto un ritorno o carrello (CR). Se impostato converte automaticamente il carattere di a capo (NL) nella coppia di caratteri ritorno carrello, a capo (CR-NL). Se impostato trasforma i caratteri minuscoli in ingresso in caratteri maiuscoli sulluscita (non previsto da POSIX.1). Se impostato converte automaticamente il carattere di a capo (NL) in un carattere di ritorno carrello (CR). Se impostato converte il carattere di ritorno carrello (CR) nella coppia di caratteri CR-NL. Se impostato rimuove dalloutput il carattere di ritorno carrello (CR). Se impostato in caso di ritardo sulla linea invia dei caratteri di riempimento invece di attendere. Se impostato il carattere di riempimento ` DEL (0x3F), invece che NUL e (0x00). Maschera per i bit che indicano il ritardo per il carattere di a capo (NL), i valori possibili sono NL0 o NL1. Maschera per i bit che indicano il ritardo per il carattere ritorno carrello (CR), i valori possibili sono CR0, CR1, CR2 o CR3. Maschera per i bit che indicano il ritardo per il carattere di tabulazione, i valori possibili sono TAB0, TAB1, TAB2 o TAB3. Maschera per i bit che indicano il ritardo per il carattere di ritorno indietro (backspace), i valori possibili sono BS0 o BS1. Maschera per i bit che indicano il ritardo per il carattere di tabulazione verticale, i valori possibili sono VT0 o VT1. Maschera per i bit che indicano il ritardo per il carattere di pagina nuova (form feed ), i valori possibili sono FF0 o FF1.

OCRNL OLCUC ONLCR ONOCR ONLRET OFILL OFDEL NLDLY CRDLY TABDLY BSDLY VTDLY FFDLY

Tabella 10.5: Costanti identicative dei vari bit del ag di controllo c_oflag delle modalit` di output di un a terminale.

Il secondo ag, mantenuto nel campo c_oflag, ` detto ag di output e controlla le modalit` e a di funzionamento delloutput dei caratteri, come limpacchettamento dei caratteri sullo schermo, la traslazione degli a capo, la conversione dei caratteri speciali; un elenco dei vari bit, del loro signicato e delle costanti utilizzate per identicarli ` riportato in tab. 10.5. e Si noti come alcuni dei valori riportati in tab. 10.5 fanno riferimento a delle maschere di bit; essi infatti vengono utilizzati per impostare alcuni valori numerici relativi ai ritardi nelloutput di alcuni caratteri: una caratteristica originaria dei primi terminali su telescrivente, che avevano bisogno di tempistiche diverse per spostare il carrello in risposta ai caratteri speciali, e che oggi sono completamente in disuso. Si tenga presente inoltre che nel caso delle maschere il valore da inserire in c_oflag deve essere fornito avendo cura di cancellare prima tutti i bit della maschera, i valori da immettere infatti (quelli riportati nella spiegazione corrispondente) sono numerici e non per bit, per cui possono sovrapporsi fra di loro. Occorrer` perci` utilizzare un codice del tipo: a o c_oflag &= (~ CRDLY ); c_oflag |= CR1 ; che prima cancella i bit della maschera in questione e poi setta il valore. Il terzo ag, mantenuto nel campo c_cflag, ` detto ag di controllo ed ` legato al funzionae e mento delle linee seriali, permettendo di impostarne varie caratteristiche, come il numero di bit

264
Valore CLOCAL

CAPITOLO 10. TERMINALI E SESSIONI DI LAVORO


Signicato Se impostato indica che il terminale ` connesso in locale e che le linee e di controllo del modem devono essere ignorate. Se non impostato eettuando una chiamata ad open senza aver specicato il ag di O_NOBLOCK si bloccher` il processo nch non si ` stabilita una connessione con il a e e modem; inoltre se viene rilevata una disconnessione viene inviato un segnale di SIGHUP al processo di controllo del terminale. La lettura su un terminale sconnesso comporta una condizione di end of le e la scrittura un errore di EIO. Se ` impostato viene distaccata la connessione del modem quando lule timo dei processi che ha ancora un le aperto sul terminale lo chiude o esce. Se ` impostato si pu` leggere linput del terminale, altrimenti i caratteri e o in ingresso vengono scartati quando arrivano. Se impostato vengono usati due bit di stop sulla linea seriale, se non impostato ne viene usato soltanto uno. Se impostato abilita la generazione il controllo di parit`. La reazione a in caso di errori dipende dai relativi valori per c_iflag, riportati in tab. 10.4. Se non ` impostato i bit di parit` non vengono generati e i e a caratteri non vengono controllati. Ha senso solo se ` attivo anche PARENB. Se impostato viene usata una e parit` ` dispari, altrimenti viene usata una parit` pari. ae a Maschera per i bit usati per specicare la dimensione del carattere inviato lungo la linea di trasmissione, i valore ne indica la lunghezza (in bit), ed i valori possibili sono CS5, CS6, CS7 e CS8 corrispondenti ad un analogo numero di bit. Maschera dei bit (4+1) usati per impostare della velocit` della linea (il a baud rate) in ingresso; in Linux non ` implementato in quanto viene e usato un apposito campo di termios. Bit aggiuntivo per limpostazione della velocit` della linea, per le stesse a motivazioni del precedente non ` implementato in Linux. e Maschera dei bit della velocit` della linea in ingresso; analogo a CBAUD, a anchesso in Linux ` mantenuto in un apposito campo di termios. e Abilita il controllo di usso hardware sulla seriale, attraverso lutilizzo delle dei due li di RTS e CTS.

HUPCL

CREAD CSTOPB PARENB

PARODD CSIZE

CBAUD

CBAUDEX CIBAUD CRTSCTS

Tabella 10.6: Costanti identicative dei vari bit del ag di controllo c_cflag delle modalit` di controllo di un a terminale.

di stop, le impostazioni della parit`, il funzionamento del controllo di usso; esso ha senso solo a per i terminali connessi a linee seriali. Un elenco dei vari bit, del loro signicato e delle costanti utilizzate per identicarli ` riportato in tab. 10.6. e I valori di questo ag sono molto specici, e completamente indirizzati al controllo di un terminale mantenuto su una linea seriale; essi pertanto non hanno nessuna rilevanza per i terminali che usano unaltra interfaccia, come le console virtuali e gli pseudo-terminali usati dalle connessioni di rete. Inoltre alcuni valori sono previsti solo per quelle implementazioni (lo standard POSIX non specica nulla riguardo limplementazione, ma solo delle funzioni di lettura e scrittura) che mantengono le velocit` delle linee seriali allinterno dei ag; come accennato in Linux questo a viene fatto (seguendo lesempio di BSD) attraverso due campi aggiuntivi, c_ispeed e c_ospeed, nella struttura termios (mostrati in g. 10.3). Il quarto ag, mantenuto nel campo c_lflag, ` detto ag locale, e serve per controllare il e funzionamento dellinterfaccia fra il driver e lutente, come abilitare leco, gestire i caratteri di controllo e lemissione dei segnali, impostare modo canonico o non canonico; un elenco dei vari bit, del loro signicato e delle costanti utilizzate per identicarli ` riportato in tab. 10.7. Con i e terminali odierni lunico ag con cui probabilmente si pu` avere a che fare ` questo, in quanto o e ` con questo che si impostano le caratteristiche generiche comuni a tutti i terminali. e

10.2. LI/O SU TERMINALE


Valore ICANON ECHO ECHOE Signicato Se impostato il terminale opera in modo canonico, altrimenti opera in modo non canonico. Se ` impostato viene attivato leco dei caratteri in input sulloutput del e terminale. Se ` impostato leco mostra la cancellazione di un carattere in input e (in reazione al carattere ERASE) cancellando lultimo carattere della riga corrente dallo schermo; altrimenti il carattere ` rimandato in eco e per mostrare quanto accaduto (usato per i terminali con luscita su una stampante). Se impostato abilita la visualizzazione del carattere di cancellazione in una modalit` adatta ai terminali con luscita su stampante; linvio del a carattere di ERASE comporta la stampa di un | seguito dal carattere cancellato, e cos` via in caso di successive cancellazioni, quando si riprende ad immettere carattere normali prima verr` stampata una /. a Se impostato abilita il trattamento della visualizzazione del carattere KILL, andando a capo dopo aver visualizzato lo stesso, altrimenti viene solo mostrato il carattere e sta allutente ricordare che linput precedente ` stato cancellato. e Se impostato abilita il trattamento della visualizzazione del carattere KILL cancellando i caratteri precedenti nella linea secondo le modalit` a specicate dai valori di ECHOE e ECHOPRT. Se impostato viene eettuato leco di un a capo (\n) anche se non ` e stato impostato ECHO. Se impostato insieme ad ECHO i caratteri di controllo ASCII (tranne TAB, NL, START, e STOP) sono mostrati nella forma che prepone un ^ alla lettera ottenuta sommando 0x40 al valore del carattere (di solito questi si possono ottenere anche direttamente premendo il tasto ctrl pi` la relativa lettera). u Se impostato abilita il riconoscimento dei caratteri INTR, QUIT, e SUSP generando il relativo segnale. Abilita alcune estensioni previste dalla implementazione. Deve essere impostato perch caratteri speciali come EOL2, LNEXT, REPRINT e e WERASE possano essere interpretati. Se impostato disabilita lo scarico delle code di ingresso e uscita quando vengono emessi i segnali SIGINT, SIGQUIT e SIGSUSP. Se abilitato, con il supporto per il job control presente, genera il segnale SIGTTOU per un processo in background che cerca di scrivere sul terminale. Se impostato il terminale funziona solo con le maiuscole. Linput ` e convertito in minuscole tranne per i caratteri preceduti da una \. In output le maiuscole sono precedute da una \ e le minuscole convertite in maiuscole. Se impostato eettua leco solo se c` un processo in lettura. e Eettua la cancellazione della coda di uscita. Viene attivato dal carattere DISCARD. Non ` supportato in Linux. e Indica che la linea deve essere ristampata, viene attivato dal carattere REPRINT e resta attivo no alla ne della ristampa. Non ` supportato e in Linux.

265

ECHOPRT

ECHOK

ECHOKE

ECHONL ECHOCTL

ISIG IEXTEN

NOFLSH TOSTOP

XCASE

DEFECHO FLUSHO PENDIN

Tabella 10.7: Costanti identicative dei vari bit del ag di controllo c_lflag delle modalit` locali di un terminale. a

Si tenga presente che i ag che riguardano le modalit` di eco dei caratteri (ECHOE, ECHOPRT, a ECHOK, ECHOKE, ECHONL) controllano solo il comportamento della visualizzazione, il riconoscimento dei vari caratteri dipende dalla modalit` di operazione, ed avviene solo in modo canonico, a pertanto questi ag non hanno signicato se non ` impostato ICANON. e Oltre ai vari ag per gestire le varie caratteristiche dei terminali, termios contiene pure il campo c_cc che viene usato per impostare i caratteri speciali associati alle varie funzioni di controllo. Il numero di questi caratteri speciali ` indicato dalla costante NCCS, POSIX ne specica e

266

CAPITOLO 10. TERMINALI E SESSIONI DI LAVORO

almeno 11, ma molte implementazioni ne deniscono molti altri.29


Indice VINTR VQUIT VERASE VKILL VEOF Valore 0x03 0x1C 0x7f 0x15 0x04 Codice (C-c) (C-|) DEL (C-u) (C-d) Funzione Carattere di interrupt, provoca lemissione di SIGINT. Carattere di uscita provoca lemissione di SIGQUIT. Carattere di ERASE, cancella lultimo carattere precedente nella linea. Carattere di KILL, cancella lintera riga. Carattere di end-of-le. Causa linvio del contenuto del buer di ingresso al processo in lettura anche se non ` ancora stato ricevuto un a capo. Se e ` il primo carattere immesso comporta il ritorno e di read con zero caratteri, cio` la condizione di e end-of-le. Timeout, in decimi di secondo, per una lettura in modo non canonico. Numero minimo di caratteri per una lettura in modo non canonico. Carattere di switch. Non supportato in Linux. Carattere di START. Riavvia un output bloccato da uno STOP. Carattere di STOP. Blocca loutput ntanto che non viene premuto un carattere di START. Carattere di sospensione. Invia il segnale SIGTSTP. Carattere di ne riga. Agisce come un a capo, ma non viene scartato ed ` letto come lultimo e carattere nella riga. Ristampa i caratteri non ancora letti. Non riconosciuto in Linux. Cancellazione di una parola. Carattere di escape, serve a quotare il carattere successivo che non viene interpretato ma passato direttamente alloutput. Ulteriore carattere di ne riga. Ha lo stesso eetto di VEOL ma pu` essere un carattere diverso. o

VTIME VMIN VSWTC VSTART VSTOP VSUSP VEOL

0x00 0x21 0x23 0x1A 0x00

NUL (C-q) (C-s) (C-z) NUL

VREPRINT VDISCARD VWERASE VLNEXT

0x12 0x07 0x17 0x16

(C-r) (C-o) (C-w) (C-v)

VEOL2

0x00

NUL

Tabella 10.8: Valori dei caratteri di controllo mantenuti nel campo c_cc della struttura termios.

A ciascuna di queste funzioni di controllo corrisponde un elemento del vettore c_cc che specica quale ` il carattere speciale associato; per portabilit` invece di essere indicati con la e a loro posizione numerica nel vettore, i vari elementi vengono indicizzati attraverso delle opportune costanti, il cui nome corrisponde allazione ad essi associata. Un elenco completo dei caratteri di controllo, con le costanti e delle funzionalit` associate ` riportato in tab. 10.8, usando quelle a e denizioni diventa possibile assegnare un nuovo carattere di controllo con un codice del tipo: value . c_cc [ VEOL2 ] = \ n ; La maggior parte di questi caratteri (tutti tranne VTIME e VMIN) hanno eetto solo quando il terminale viene utilizzato in modo canonico; per alcuni devono essere soddisfatte ulteriori richieste, ad esempio VINTR, VSUSP, e VQUIT richiedono sia impostato ISIG; VSTART e VSTOP richiedono sia impostato IXON; VLNEXT, VWERASE, VREPRINT richiedono sia impostato IEXTEN. In ogni caso quando vengono attivati i caratteri vengono interpretati e non sono passati sulla coda di ingresso. Per leggere ed scrivere tutte le varie impostazioni dei terminali viste nora lo standard POSIX prevede due funzioni che utilizzano come argomento un puntatore ad una struttura termios
29

in Linux il valore della costante ` 32, anche se i caratteri eettivamente deniti sono solo 17. e

10.2. LI/O SU TERMINALE

267

che sar` quella in cui andranno immagazzinate le impostazioni. Le funzioni sono tcgetattr e a tcsetattr ed il loro prototipo `: e
#include <unistd.h> #include <termios.h> int tcgetattr(int fd, struct termios *termios_p) Legge il valore delle impostazioni di un terminale. int tcsetattr(int fd, int optional_actions, struct termios *termios_p) Scrive le impostazioni di un terminale. Entrambe le funzioni restituiscono 0 in caso di successo e -1 in caso di errore, nel qual caso errno assumer` i valori: a EINTR la funzione ` stata interrotta. e ed inoltre EBADF, ENOTTY ed EINVAL.

Le funzioni operano sul terminale cui fa riferimento il le descriptor fd utilizzando la struttura indicata dal puntatore termios_p per lo scambio dei dati. Si tenga presente che le impostazioni sono associate al terminale e non al le descriptor; questo signica che se si ` cambiata una ime postazione un qualunque altro processo che apra lo stesso terminale, od un qualunque altro le descriptor che vi faccia riferimento, vedr` le nuove impostazioni pur non avendo nulla a che fare a con il le descriptor che si ` usato per eettuare i cambiamenti. e Questo signica che non ` possibile usare le descriptor diversi per utilizzare automaticamene te il terminale in modalit` diverse, se esiste una necessit` di accesso dierenziato di questo tipo a a occorrer` cambiare esplicitamente la modalit` tutte le volte che si passa da un le descriptor ad a a un altro. La funzione tcgetattr legge i valori correnti delle impostazioni di un terminale qualunque nella struttura puntata da termios_p; tcsetattr invece eettua la scrittura delle impostazioni e quando viene invocata sul proprio terminale di controllo pu` essere eseguita con successo solo o da un processo in foreground. Se invocata da un processo in background infatti tutto il gruppo ricever` un segnale di SIGTTOU come se si fosse tentata una scrittura, a meno che il processo a chiamante non abbia SIGTTOU ignorato o bloccato, nel qual caso loperazione sar` eseguita. a La funzione tcsetattr prevede tre diverse modalit` di funzionamento, specicabili attraverso a largomento optional_actions, che permette di stabilire come viene eseguito il cambiamento delle impostazioni del terminale, i valori possibili sono riportati in tab. 10.9; di norma (come fatto per le due funzioni di esempio) si usa sempre TCSANOW, le altre opzioni possono essere utili qualora si cambino i parametri di output.
Valore TCSANOW TCSADRAIN TCSAFLUSH Signicato Esegue i cambiamenti in maniera immediata. I cambiamenti vengono eseguiti dopo aver atteso che tutto loutput presente sulle code ` stato scritto. e ` E identico a TCSADRAIN, ma in pi` scarta tutti i dati u presenti sulla coda di input.

Tabella 10.9: Possibili valori per largomento optional_actions della funzione tcsetattr.

Occorre inne tenere presente che tcsetattr ritorna con successo anche se soltanto uno dei cambiamenti richiesti ` stato eseguito. Pertanto se si eettuano pi` cambiamenti ` buona norma e u e controllare con una ulteriore chiamata a tcgetattr che essi siano stati eseguiti tutti quanti. Come gi` accennato per i cambiamenti eettuati ai vari ag di controllo occorre che i valori a di ciascun bit siano specicati avendo cura di mantenere intatti gli altri; per questo motivo in generale si deve prima leggere il valore corrente delle impostazioni con tcgetattr per poi modicare i valori impostati. In g. 10.4 e g. 10.5 si ` riportato rispettivamente il codice delle due funzioni SetTermAttr e e UnSetTermAttr, che possono essere usate per impostare o rimuovere, con le dovute precau-

268

CAPITOLO 10. TERMINALI E SESSIONI DI LAVORO

# include < unistd .h > # include < termios .h > 3 # include < errno .h >
1 2 4

int SetTermAttr ( int fd , tcflag_t flag ) { 7 struct termios values ; 8 int res ; 9 res = tcgetattr ( desc , & values ); 10 if ( res ) { 11 perror ( " Cannot get attributes " ); 12 return res ; 13 } 14 values . c_lflag |= flag ; 15 res = tcsetattr ( desc , TCSANOW , & values ); 16 if ( res ) { 17 perror ( " Cannot set attributes " ); 18 return res ; 19 } 20 return 0; 21 }
5 6

Figura 10.4: Codice della funzione SetTermAttr che permette di impostare uno dei ag di controllo locale del terminale.

zioni, un qualunque bit di c_lflag. Il codice di entrambe le funzioni pu` essere trovato nel le o SetTermAttr.c dei sorgenti allegati. La funzione SetTermAttr provvede ad impostare il bit specicato dallargomento flag; prima si leggono i valori correnti (10) con tcgetattr, uscendo con un messaggio in caso di errore (1114), poi si provvede a impostare solo i bit richiesti (possono essere pi` di uno) con un OR binario u (15); inne si scrive il nuovo valore modicato con tcsetattr (16), noticando un eventuale errore (11-14) o uscendo normalmente.
int UnSetTermAttr ( int fd , tcflag_t flag ) { 3 struct termios values ; 4 int res ; 5 res = tcgetattr ( desc , & values ); 6 if ( res ) { 7 perror ( " Cannot get attributes " ); 8 return res ; 9 } 10 values . c_lflag &= (~ flag ); 11 res = tcsetattr ( desc , TCSANOW , & values ); 12 if ( res ) { 13 perror ( " Cannot set attributes " ); 14 return res ; 15 } 16 return 0; 17 }
1 2

Figura 10.5: Codice della funzione UnSetTermAttr che permette di rimuovere uno dei ag di controllo locale del terminale.

La seconda funzione, UnSetTermAttr, ` assolutamente identica alla prima, solo che in questo e

10.2. LI/O SU TERMINALE

269

caso, in (15), si rimuovono i bit specicati dallargomento flag usando un AND binario del valore negato. Al contrario di tutte le altre caratteristiche dei terminali, che possono essere impostate esplicitamente utilizzando gli opportuni campi di termios, per le velocit` della linea (il cosiddetto a baud rate) non ` prevista una implementazione standardizzata, per cui anche se in Linux sono e mantenute in due campi dedicati nella struttura, questi non devono essere acceduti direttamente ma solo attraverso le apposite funzioni di interfaccia provviste da POSIX.1. Lo standard prevede due funzioni per scrivere la velocit` delle linee seriali, cfsetispeed a per la velocit` della linea di ingresso e cfsetospeed per la velocit` della linea di uscita; i loro a a prototipi sono:
#include <unistd.h> #include <termios.h> int cfsetispeed(struct termios Imposta la velocit` delle linee a int cfsetospeed(struct termios Imposta la velocit` delle linee a

*termios_p, speed_t speed) seriali in ingresso. *termios_p, speed_t speed) seriali in uscita.

Entrambe le funzioni restituiscono 0 in caso di successo e -1 in caso di errore, che avviene solo quando il valore specicato non ` valido. e

Si noti che le funzioni si limitano a scrivere opportunamente il valore della velocit` prescelta a speed allinterno della struttura puntata da termios_p; per eettuare limpostazione eettiva occorrer` poi chiamare tcsetattr. a Si tenga presente che per le linee seriali solo alcuni valori di velocit` sono validi; questi a possono essere specicati direttamente (le glibc prevedono che i valori siano indicati in bit per secondo), ma in generale altre versioni di librerie possono utilizzare dei valori diversi; per questo POSIX.1 prevede una serie di costanti che per` servono solo per specicare le velocit` tipiche o a delle linee seriali: B0 B300 B19200 B50 B600 B38400 B75 B1200 B57600 B110 B1800 B115200 B134 B2400 B230400 B150 B4800 B460800 B200 B9600

Un terminale pu` utilizzare solo alcune delle velocit` possibili, le funzioni per` non controlo a o lano se il valore specicato ` valido, dato che non possono sapere a quale terminale le veloe cit` saranno applicate; sar` lesecuzione di tcsetattr a fallire quando si cercher` di eseguire a a a limpostazione. Di norma il valore ha senso solo per i terminali seriali dove indica appunto la velocit` della linea di trasmissione; se questa non corrisponde a quella del terminale questultimo a non potr` funzionare: quando il terminale non ` seriale il valore non inuisce sulla velocit` di a e a trasmissione dei dati. In generale impostare un valore nullo (B0) sulla linea di output fa si che il modem non asserisca pi` le linee di controllo, interrompendo di fatto la connessione, qualora invece si utilizzi u questo valore per la linea di input leetto sar` quello di rendere la sua velocit` identica a quella a a della linea di output. Analogamente a quanto avviene per limpostazione, le velocit` possono essere lette da una a struttura termios utilizzando altre due funzioni, cfgetispeed e cfgetospeed, i cui prototipi sono:
#include <unistd.h> #include <termios.h> speed_t cfgetispeed(struct termios *termios_p) Legge la velocit` delle linee seriali in ingresso. a speed_t cfgetospeed(struct termios *termios_p) Legge la velocit` delle linee seriali in uscita. a Entrambe le funzioni restituiscono la velocit` della linea, non sono previste condizioni di errore. a

270

CAPITOLO 10. TERMINALI E SESSIONI DI LAVORO

Anche in questo caso le due funzioni estraggono i valori della velocit` della linea da una a struttura, il cui indirizzo ` specicato dallargomento termios_p che deve essere stata letta in e precedenza con tcgetattr.

10.2.3

La gestione della disciplina di linea.

Come illustrato dalla struttura riportata in g. 10.2 tutti i terminali hanno un insieme di funzionalit` comuni, che prevedono la presenza di code di ingresso ed uscita; in generale si fa riferimento a ad esse con il nome di discipline di linea. Lo standard POSIX prevede alcune funzioni che permettono di intervenire direttamente sulla gestione di questultime e sullinterazione fra i dati in ingresso ed uscita e le relative code. In generale tutte queste funzioni vengono considerate, dal punto di vista dellaccesso al terminale, come delle funzioni di scrittura, pertanto se usate da processi in background sul loro terminale di controllo provocano lemissione di SIGTTOU come illustrato in sez. 10.1.3.30 Una prima funzione, che ` ecace solo in caso di terminali seriali asincroni (non fa niente e per tutti gli altri terminali), ` tcsendbreak; il suo prototipo `: e e
#include <unistd.h> #include <termios.h> int tcsendbreak(int fd, int duration) Genera una condizione di break inviando un usso di bit nulli. La funzione restituisce 0 in caso di successo e -1 in caso di errore, nel qual caso errno assumer` i a valori EBADF o ENOTTY.

La funzione invia un usso di bit nulli (che genera una condizione di break) sul terminale associato a fd; un valore nullo di duration implica una durata del usso fra 0.25 e 0.5 secondi, un valore diverso da zero implica una durata pari a duration*T dove T ` un valore compreso e fra 0.25 e 0.5.31 Le altre funzioni previste da POSIX servono a controllare il comportamento dellinterazione fra le code associate al terminale e lutente; la prima ` tcdrain, il cui prototipo `: e e
#include <unistd.h> #include <termios.h> int tcdrain(int fd) Attende lo svuotamento della coda di output. La funzione restituisce 0 in caso di successo e -1 in caso di errore, nel qual caso errno assumer` i a valori EBADF o ENOTTY.

La funzione blocca il processo no a che tutto loutput presente sulla coda di uscita non ` e stato trasmesso al terminale associato ad fd. Una seconda funzione, tcflush, permette svuotare immediatamente le code di cancellando tutti i dati presenti al loro interno; il suo prototipo `: e
#include <unistd.h> #include <termios.h> int tcflush(int fd, int queue) Cancella i dati presenti nelle code di ingresso o di uscita. La funzione restituisce 0 in caso di successo e -1 in caso di errore, nel qual caso errno assumer` i a valori EBADF o ENOTTY.

La funzione agisce sul terminale associato a fd, largomento queue permette di specicare su quale coda (ingresso, uscita o entrambe), operare. Esso pu` prendere i valori riportati in o
con la stessa eccezione, gi` vista per tcsetattr, che questultimo sia bloccato o ignorato dal processo a chiamante. 31 lo standard POSIX specica il comportamento solo nel caso si sia impostato un valore nullo per duration; il comportamento negli altri casi pu` dipendere dalla implementazione. o
30

10.2. LI/O SU TERMINALE

271

tab. 10.10, nel caso si specichi la coda di ingresso canceller` i dati ricevuti ma non ancora letti, a nel caso si specichi la coda di uscita canceller` i dati scritti ma non ancora trasmessi. a
Valore TCIFLUSH TCOFLUSH TCIOFLUSH Signicato Cancella i dati sulla coda di ingresso. Cancella i dati sulla coda di uscita. Cancella i dati su entrambe le code.

Tabella 10.10: Possibili valori per largomento queue della funzione tcflush.

Lultima funzione dellinterfaccia che interviene sulla disciplina di linea ` tcflow, che viene e usata per sospendere la trasmissione e la ricezione dei dati sul terminale; il suo prototipo `: e
#include <unistd.h> #include <termios.h> int tcflow(int fd, int action) Sospende e riavvia il usso dei dati sul terminale. La funzione restituisce 0 in caso di successo e -1 in caso di errore, nel qual caso errno assumer` i a valori EBADF o ENOTTY.

La funzione permette di controllare (interrompendo e facendo riprendere) il usso dei dati fra il terminale ed il sistema sia in ingresso che in uscita. Il comportamento della funzione ` e regolato dallargomento action, i cui possibili valori, e relativa azione eseguita dalla funzione, sono riportati in tab. 10.11.
Valore TCOOFF TCOON TCIOFF TCION Azione Sospende loutput. Riprende un output precedentemente sospeso. Il sistema trasmette un carattere di STOP, che fa interrompere la trasmissione dei dati dal terminale. Il sistema trasmette un carattere di START, che fa riprendere la trasmissione dei dati dal terminale.

Tabella 10.11: Possibili valori per largomento action della funzione tcflow.

10.2.4

Operare in modo non canonico

Operare con un terminale in modo canonico ` relativamente semplice; basta eseguire una lettura e e la funzione ritorner` quando una il driver del terminale avr` completato una linea di input. a a Non ` detto che la linea sia letta interamente (si pu` aver richiesto un numero inferiore di byte) e o ma in ogni caso nessun dato verr` perso, e il resto della linea sar` letto alla chiamata successiva. a a Inoltre in modo canonico la gestione dellinput ` di norma eseguita direttamente dal driver e del terminale, che si incarica (a seconda di quanto impostato con le funzioni viste nei paragra precedenti) di cancellare i caratteri, bloccare e riavviare il usso dei dati, terminare la linea quando viene ricevuti uno dei vari caratteri di terminazione (NL, EOL, EOL2, EOF). In modo non canonico tocca invece al programma gestire tutto quanto, i caratteri NL, EOL, EOL2, EOF, ERASE, KILL, CR, REPRINT non vengono interpretati automaticamente ed inoltre, non dividendo pi` linput in linee, il sistema non ha pi` un limite denito per quando u u ritornare i dati ad un processo. Per questo motivo abbiamo visto che in c_cc sono previsti due caratteri speciali, MIN e TIME (specicati dagli indici VMIN e VTIME in c_cc) che dicono al sistema di ritornare da una read quando ` stata letta una determinata quantit` di dati o ` e a e passato un certo tempo. Come accennato nella relativa spiegazione in tab. 10.8, TIME e MIN non sono in realt` a caratteri ma valori numerici. Il comportamento del sistema per un terminale in modalit` non a canonica prevede quattro casi distinti:

272

CAPITOLO 10. TERMINALI E SESSIONI DI LAVORO

MIN> 0, TIME> 0 In questo caso MIN stabilisce il numero minimo di caratteri desiderati e TIME un tempo di attesa, in decimi di secondo, fra un carattere e laltro. Una read ritorna se vengono ricevuti almeno MIN caratteri prima della scadenza di TIME (MIN ` solo un limite inferiore, se la funzione ha richiesto un numero maggiore di caratteri ne e possono essere restituiti di pi`); se invece TIME scade vengono restituiti i byte ricevuti u no ad allora (un carattere viene sempre letto, dato che il timer inizia a scorrere solo dopo la ricezione del primo carattere). MIN> 0, TIME= 0 Una read ritorna solo dopo che sono stati ricevuti almeno MIN caratteri. Questo signica che una read pu` bloccarsi indenitamente. o MIN= 0, TIME> 0 In questo caso TIME indica un tempo di attesa dalla chiamata di read, la funzione ritorna non appena viene ricevuto un carattere o scade il tempo. Si noti che ` e possibile che read ritorni con un valore nullo. MIN= 0, TIME= 0 In questo caso una read ritorna immediatamente restituendo tutti i caratteri ricevuti. Anche in questo caso pu` ritornare con un valore nullo. o

10.3
Da fare.

La gestione dei terminali virtuali

10.3.1

I terminali virtuali

Qui vanno spiegati i terminali virtuali, /dev/pty e compagnia.

10.3.2

Allocazione dei terminale virtuali

Qui vanno le cose su openpty e compagnia.

Capitolo 11

La gestione avanzata dei le


In questo capitolo aronteremo le tematiche relative alla gestione avanzata dei le. In particolare tratteremo delle funzioni di input/output avanzato, che permettono una gestione pi` sosticata u dellI/O su le, a partire da quelle che permettono di gestire laccesso contemporaneo a pi` le, u per concludere con la gestione dellI/O mappato in memoria. Dedicheremo poi la ne del capitolo alle problematiche del le locking.

11.1

LI/O multiplexing

Uno dei problemi che si presentano quando si deve operare contemporaneamente su molti le usando le funzioni illustrate in cap. 6 e cap. 7 ` che si pu` essere bloccati nelle operazioni su e o un le mentre un altro potrebbe essere disponibile. LI/O multiplexing nasce risposta a questo problema. In questa sezione forniremo una introduzione a questa problematica ed analizzeremo le varie funzioni usate per implementare questa modalit` di I/O. a

11.1.1

La problematica dellI/O multiplexing

Abbiamo visto in sez. 9.3.1, arontando la suddivisione fra fast e slow system call, che in certi casi le funzioni di I/O possono bloccarsi indenitamente.1 Ad esempio le operazioni di lettura possono bloccarsi quando non ci sono dati disponibili sul descrittore su cui si sta operando. Questo comportamento causa uno dei problemi pi` comuni che ci si trova ad arontare nelle u operazioni di I/O, che si verica quando si deve operare con pi` le descriptor eseguendo funzioni u che possono bloccarsi senza che sia possibile prevedere quando questo pu` avvenire (il caso pi` o u classico ` quello di un server in attesa di dati in ingresso da vari client). Quello che pu` accadere e o ` di restare bloccati nelleseguire una operazione su un le descriptor che non ` pronto, quando e e ce ne potrebbe essere un altro disponibile. Questo comporta nel migliore dei casi una operazione ritardata inutilmente nellattesa del completamento di quella bloccata, mentre nel peggiore dei casi (quando la conclusione della operazione bloccata dipende da quanto si otterrebbe dal le descriptor disponibile) si potrebbe addirittura arrivare ad un deadlock. Abbiamo gi` accennato in sez. 6.2.1 che ` possibile prevenire questo tipo di comportamena e to delle funzioni di I/O aprendo un le in modalit` non-bloccante, attraverso luso del ag a O_NONBLOCK nella chiamata di open. In questo caso le funzioni di input/output eseguite sul le che si sarebbero bloccate, ritornano immediatamente, restituendo lerrore EAGAIN. Lutilizzo di questa modalit` di I/O permette di risolvere il problema controllando a turno i vari le descripa tor, in un ciclo in cui si ripete laccesso ntanto che esso non viene garantito. Ovviamente questa
si ricordi per` che questo pu` accadere solo per le pipe, i socket ed alcuni le di dispositivo; sui le normali o o le funzioni di lettura e scrittura ritornano sempre subito.
1

273

274

CAPITOLO 11. LA GESTIONE AVANZATA DEI FILE

tecnica, detta polling, ` estremamente ineciente: si tiene costantemente impiegata la CPU solo e per eseguire in continuazione delle system call che nella gran parte dei casi falliranno. Per superare questo problema ` stato introdotto il concetto di I/O multiplexing, una nuova e modalit` di operazioni che consente di tenere sotto controllo pi` le descriptor in contemporaa u nea, permettendo di bloccare un processo quando le operazioni volute non sono possibili, e di riprenderne lesecuzione una volta che almeno una di quelle richieste sia eettuabile, in modo da poterla eseguire con la sicurezza di non restare bloccati. Dato che, come abbiamo gi` accennato, per i normali le su disco non si ha mai un accesso a bloccante, luso pi` comune delle funzioni che esamineremo nei prossimi paragra ` per i server u e di rete, in cui esse vengono utilizzate per tenere sotto controllo dei socket; pertanto ritorneremo su di esse con ulteriori dettagli e qualche esempio di utilizzo concreto in sez. 16.6.

11.1.2

Le funzioni select e pselect

Il primo kernel unix-like ad introdurre una interfaccia per lI/O multiplexing ` stato BSD,2 con e la funzione select, il cui prototipo `: e
#include <sys/time.h> #include <sys/types.h> #include <unistd.h> int select(int ndfs, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout) Attende che uno dei le descriptor degli insiemi specicati diventi attivo. La funzione in caso di successo restituisce il numero di le descriptor (anche nullo) che sono attivi, e -1 in caso di errore, nel qual caso errno assumer` uno dei valori: a EBADF EINTR EINVAL si ` specicato un le descriptor sbagliato in uno degli insiemi. e la funzione ` stata interrotta da un segnale. e si ` specicato per ndfs un valore negativo o un valore non valido per timeout. e

ed inoltre ENOMEM.

La funzione mette il processo in stato di sleep (vedi tab. 3.11) ntanto che almeno uno dei le descriptor degli insiemi specicati (readfds, writefds e exceptfds), non diventa attivo, per un tempo massimo specicato da timeout. Per specicare quali le descriptor si intende selezionare, la funzione usa un particolare oggetto, il le descriptor set, identicato dal tipo fd_set, che serve ad identicare un insieme di le descriptor, in maniera analoga a come un signal set (vedi sez. 9.4.2) identica un insieme di segnali. Per la manipolazione di questi le descriptor set si possono usare delle opportune macro di preprocessore:
#include <sys/time.h> #include <sys/types.h> #include <unistd.h> void FD_ZERO(fd_set *set) Inizializza linsieme (vuoto). void FD_SET(int fd, fd_set *set) Inserisce il le descriptor fd nellinsieme. void FD_CLR(int fd, fd_set *set) Rimuove il le descriptor fd dallinsieme. int FD_ISSET(int fd, fd_set *set) Controlla se il le descriptor fd ` nellinsieme. e

In genere un le descriptor set pu` contenere no ad un massimo di FD_SETSIZE le descripo tor. Questo valore in origine corrispondeva al limite per il numero massimo di le aperti3 , ma da
la funzione select ` apparsa in BSD4.2 e standardizzata in BSD4.4, ma ` stata portata su tutti i sistemi che e e supportano i socket, compreso le varianti di System V. 3 ad esempio in Linux, no alla serie 2.0.x, cera un limite di 256 le per processo.
2

11.1. LI/O MULTIPLEXING

275

quando, come nelle versioni pi` recenti del kernel, non c` pi` un limite massimo, esso indica le u e u dimensioni massime dei numeri usati nei le descriptor set.4 Si tenga presente che i le descriptor set devono sempre essere inizializzati con FD_ZERO; passare a select un valore non inizializzato pu` dar luogo a comportamenti non prevedibili; allo stesso modo usare FD_SET o FD_CLR con un o le descriptor il cui valore eccede FD_SETSIZE pu` dare luogo ad un comportamento indenito. o La funzione richiede di specicare tre insiemi distinti di le descriptor; il primo, readfds, verr` osservato per rilevare la disponibilit` di eettuare una lettura,5 il secondo, writefds, per a a vericare la possibilit` eettuare una scrittura ed il terzo, exceptfds, per vericare lesistenza a di eccezioni (come i dati urgenti su un socket, vedi sez. 19.1.3). Dato che in genere non si tengono mai sotto controllo no a FD_SETSIZE le contemporaneamente la funzione richiede di specicare qual ` il valore pi` alto fra i le descriptor indicati nei e u tre insiemi precedenti. Questo viene fatto per ecienza, per evitare di passare e far controllare al kernel una quantit` di memoria superiore a quella necessaria. Questo limite viene indicato a tramite largomento ndfs, che deve corrispondere al valore massimo aumentato di uno.6 Inne largomento timeout specica un tempo massimo di attesa prima che la funzione ritorni; se impostato a NULL la funzione attende indenitamente. Si pu` specicare anche un tempo nulo lo (cio` una struttura timeval con i campi impostati a zero), qualora si voglia semplicemente e controllare lo stato corrente dei le descriptor. La funzione restituisce il numero di le descriptor pronti,7 e ciascun insieme viene sovrascritto per indicare quali sono i le descriptor pronti per le operazioni ad esso relative, in modo da poterli controllare con FD_ISSET. Se invece si ha un timeout viene restituito un valore nullo e gli insiemi non vengono modicati. In caso di errore la funzione restituisce -1, ed i valori dei tre insiemi sono indeniti e non si pu` fare nessun adamento sul loro contenuto. o Una volta ritornata la funzione si potr` controllare quali sono i le descriptor pronti ed a operare su di essi, si tenga presente per` che si tratta solo di un suggerimento, esistono infatti o condizioni8 in cui select pu` riportare in maniera spuria che un le descriptor ` pronto in o e lettura, quando una successiva lettura si bloccherebbe. Per questo quando si usa I/O multiplexing ` sempre raccomandato luso delle funzioni di lettura e scrittura in modalit` non bloccante. e a In Linux select modica anche il valore di timeout, impostandolo al tempo restante in caso di interruzione prematura; questo ` utile quando la funzione viene interrotta da un segnale, in e tal caso infatti si ha un errore di EINTR, ed occorre rilanciare la funzione; in questo modo non ` e necessario ricalcolare tutte le volte il tempo rimanente.9 Uno dei problemi che si presentano con luso di select ` che il suo comportamento dipende e dal valore del le descriptor che si vuole tenere sotto controllo. Infatti il kernel riceve con ndfs un limite massimo per tale valore, e per capire quali sono i le descriptor da tenere sotto controllo dovr` eettuare una scansione su tutto lintervallo, che pu` anche essere molto ampio anche se a o i le descriptor sono solo poche unit`; tutto ci` ha ovviamente delle conseguenze ampiamente a o negative per le prestazioni.
il suo valore, secondo lo standard POSIX 1003.1-2001, ` denito in sys/select.h, ed ` pari a 1024. e e per essere precisi la funzione ritorner` in tutti i casi in cui la successiva esecuzione di read risulti non bloccante, a quindi anche in caso di end-of-le; inoltre con Linux possono vericarsi casi particolari, ad esempio quando arrivano dati su un socket dalla rete che poi risultano corrotti e vengono scartati, pu` accadere che select riporti il relativo o le descriptor come leggibile, ma una successiva read si blocchi. 6 si ricordi che i le descriptor sono numerati progressivamente a partire da zero, ed il valore indica il numero pi` alto fra quelli da tenere sotto controllo; dimenticarsi di aumentare di uno il valore di ndfs ` un errore comune. u e 7 questo ` il comportamento previsto dallo standard, ma la standardizzazione della funzione ` recente, ed e e esistono ancora alcune versioni di Unix che non si comportano in questo modo. 8 ad esempio quando su un socket arrivano dei dati che poi vengono scartati perch corrotti. e 9 questo pu` causare problemi di portabilit` sia quando si trasporta codice scritto su Linux che legge questo o a valore, sia quando si usano programmi scritti per altri sistemi che non dispongono di questa caratteristica e ricalcolano timeout tutte le volte. In genere la caratteristica ` disponibile nei sistemi che derivano da System V e e non disponibile per quelli che derivano da BSD.
5 4

276

CAPITOLO 11. LA GESTIONE AVANZATA DEI FILE

Inoltre c` anche il problema che il numero massimo dei le che si possono tenere sotto cone trollo, la funzione ` nata quando il kernel consentiva un numero massimo di 1024 le descriptor e per processo, adesso che il numero pu` essere arbitrario si viene a creare una dipendenza del o tutto articiale dalle dimensioni della struttura fd_set, che pu` necessitare di essere estesa, con o ulteriori perdite di prestazioni. Lo standard POSIX ` rimasto a lungo senza primitive per lI/O multiplexing, introdotto solo e con le ultime revisioni dello standard (POSIX 1003.1g-2000 e POSIX 1003.1-2001). La scelta ` stata quella di seguire linterfaccia creata da BSD, ma prevede che tutte le funzioni ad esso e relative vengano dichiarate nellheader sys/select.h, che sostituisce i precedenti, ed inoltre aggiunge a select una nuova funzione pselect,10 il cui prototipo `: e
#include <sys/select.h> int pselect(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timespec *timeout, sigset_t *sigmask) Attende che uno dei le descriptor degli insiemi specicati diventi attivo. La funzione in caso di successo restituisce il numero di le descriptor (anche nullo) che sono attivi, e -1 in caso di errore, nel qual caso errno assumer` uno dei valori: a EBADF EINTR EINVAL si ` specicato un le descriptor sbagliato in uno degli insiemi. e la funzione ` stata interrotta da un segnale. e si ` specicato per ndfs un valore negativo o un valore non valido per timeout. e

ed inoltre ENOMEM.

La funzione ` sostanzialmente identica a select, solo che usa una struttura timespec (vedi e g. 8.9) per indicare con maggiore precisione il timeout e non ne aggiorna il valore in caso di interruzione.11 Inoltre prende un argomento aggiuntivo sigmask che ` il puntatore ad una maschera e di segnali (si veda sez. 9.4.4). La maschera corrente viene sostituita da questa immediatamente prima di eseguire lattesa, e ripristinata al ritorno della funzione. Luso di sigmask ` stato introdotto allo scopo di prevenire possibili race condition quando e ci si deve porre in attesa sia di un segnale che di dati. La tecnica classica ` quella di utilizzae re il gestore per impostare una variabile globale e controllare questa nel corpo principale del programma; abbiamo visto in sez. 9.4.1 come questo lasci spazio a possibili race condition, per cui diventa essenziale utilizzare sigprocmask per disabilitare la ricezione del segnale prima di eseguire il controllo e riabilitarlo dopo lesecuzione delle relative operazioni, onde evitare larrivo di un segnale immediatamente dopo il controllo, che andrebbe perso. Nel nostro caso il problema si pone quando oltre al segnale si devono tenere sotto controllo anche dei le descriptor con select, in questo caso si pu` fare conto sul fatto che allarrivo di o un segnale essa verrebbe interrotta e si potrebbero eseguire di conseguenza le operazioni relative al segnale e alla gestione dati con un ciclo del tipo: while (1) { sigprocmask ( SIG_BLOCK , & newmask , & oldmask ); if ( receive_signal != 0) handle_signal (); sigprocmask ( SIG_SETMASK , & oldmask , NULL ); n = select ( nfd , rset , wset , eset , NULL ); if ( n < 0) { if ( errno == EINTR ) {
il supporto per lo standard POSIX 1003.1-2001, ed lheader sys/select.h, compaiono in Linux a partire dalle glibc 2.1. Le libc4 e libc5 non contengono questo header, le glibc 2.0 contengono una denizione sbagliata di psignal, senza largomento sigmask, la denizione corretta ` presente dalle glibc 2.1-2.2.1 se si ` denito e e _GNU_SOURCE e nelle glibc 2.2.2-2.2.4 se si ` denito _XOPEN_SOURCE con valore maggiore di 600. e 11 in realt` la system call di Linux aggiorna il valore al tempo rimanente, ma la funzione fornita dalle glibc a modica questo comportamento passando alla system call una variabile locale, in modo da mantenere laderenza allo standard POSIX che richiede che il valore di timeout non sia modicato.
10

11.1. LI/O MULTIPLEXING continue ; } } else handle_filedata (); }

277

qui per` emerge una race condition, perch se il segnale arriva prima della chiamata a select, o e questa non verr` interrotta, e la ricezione del segnale non sar` rilevata. a a Per questo ` stata introdotta pselect che attraverso largomento sigmask permette di riae bilitare la ricezione il segnale contestualmente allesecuzione della funzione,12 ribloccandolo non appena essa ritorna, cos` che il precedente codice potrebbe essere riscritto nel seguente modo: while (1) { sigprocmask ( SIG_BLOCK , & newmask , & oldmask ); if ( receive_signal != 0) handle_signal (); n = pselect ( nfd , rset , wset , eset , NULL , & oldmask ); sigprocmask ( SIG_SETMASK , & oldmask , NULL ); if ( n < 0) { if ( errno == EINTR ) { continue ; } } else { handle_filedata (); } } in questo caso utilizzando oldmask durante lesecuzione di pselect la ricezione del segnale sar` a abilitata, ed in caso di interruzione si potranno eseguire le relative operazioni.

11.1.3

Le funzioni poll e ppoll

Nello sviluppo di System V, invece di utilizzare linterfaccia di select, che ` una estensione tipica e di BSD, ` stata introdotta unaltra interfaccia, basata sulla funzione poll,13 il cui prototipo `: e e
#include <sys/poll.h> int poll(struct pollfd *ufds, unsigned int nfds, int timeout) La funzione attende un cambiamento di stato su un insieme di le descriptor. La funzione restituisce il numero di le descriptor con attivit` in caso di successo, o 0 se c` stato a e un timeout e -1 in caso di errore, ed in questultimo caso errno assumer` uno dei valori: a EBADF EINTR EINVAL si ` specicato un le descriptor sbagliato in uno degli insiemi. e la funzione ` stata interrotta da un segnale. e il valore di nfds eccede il limite RLIMIT_NOFILE.

ed inoltre EFAULT e ENOMEM.

La funzione permette di tenere sotto controllo contemporaneamente ndfs le descriptor, specicati attraverso il puntatore ufds ad un vettore di strutture pollfd. Come con select si pu` interrompere lattesa dopo un certo tempo, questo deve essere specicato con largomento o
in Linux per`, no al kernel 2.6.16, non era presente la relativa system call, e la funzione era implementata o nelle glibc attraverso select (vedi man select_tut) per cui la possibilit` di race condition permaneva; in tale a situazione si pu` ricorrere ad una soluzione alternativa, chiamata self-pipe trick, che consiste nellaprire una pipe o (vedi sez. 12.1.1) ed usare select sul capo in lettura della stessa; si pu` indicare larrivo di un segnale scrivendo o sul capo in scrittura allinterno del gestore dello stesso; in questo modo anche se il segnale va perso prima della chiamata di select questa lo riconoscer` comunque dalla presenza di dati sulla pipe. a 13 la funzione ` prevista dallo standard XPG4, ed ` stata introdotta in Linux come system call a partire dal e e kernel 2.1.23 ed inserita nelle libc 5.4.28.
12

278

CAPITOLO 11. LA GESTIONE AVANZATA DEI FILE

timeout in numero di millisecondi: un valore negativo indica unattesa indenita, mentre un valore nullo comporta il ritorno immediato (e pu` essere utilizzato per impiegare poll in modalit` o a non-bloccante). Per ciascun le da controllare deve essere inizializzata una struttura pollfd nel vettore indicato dallargomento ufds. La struttura, la cui denizione ` riportata in g. 11.1, prevede e tre campi: in fd deve essere indicato il numero del le descriptor da controllare, in events deve essere specicata una maschera binaria di ag che indichino il tipo di evento che si vuole controllare, mentre in revents il kernel restituir` il relativo risultato. Usando un valore negativo a per fd la corrispondente struttura sar` ignorata da poll. Dato che i dati in ingresso sono del tutto a indipendenti da quelli in uscita (che vengono restituiti in revents) non ` necessario reinizializzare e tutte le volte il valore delle strutture pollfd a meno di non voler cambiare qualche condizione.

struct pollfd { int fd ; short events ; short revents ; };

/* file descriptor */ /* requested events */ /* returned events */

Figura 11.1: La struttura pollfd, utilizzata per specicare le modalit` di controllo di un le descriptor alla a funzione poll.

Le costanti che deniscono i valori relativi ai bit usati nelle maschere binarie dei campi events e revents sono riportati in tab. 11.1, insieme al loro signicato. Le si sono suddivise in tre gruppi, nel primo gruppo si sono indicati i bit utilizzati per controllare lattivit` in ingresso, a nel secondo quelli per lattivit` in uscita, mentre il terzo gruppo contiene dei valori che vengono a utilizzati solo nel campo revents per noticare delle condizioni di errore.
Flag POLLIN POLLRDNORM POLLRDBAND POLLPRI POLLOUT POLLWRNORM POLLWRBAND POLLERR POLLHUP POLLNVAL POLLMSG Signicato ` E possibile la lettura. Sono disponibili in lettura dati normali. Sono disponibili in lettura dati prioritari. ` E possibile la lettura di dati urgenti. ` E possibile la scrittura immediata. ` E possibile la scrittura di dati normali. ` E possibile la scrittura di dati prioritari. C` una condizione di errore. e Si ` vericato un hung-up. e Il le descriptor non ` aperto. e Denito per compatibilit` con SysV. a

Tabella 11.1: Costanti per lidenticazione dei vari bit dei campi events e revents di pollfd.

Il valore POLLMSG non viene utilizzato ed ` denito solo per compatibilit` con limplementae a zione di SysV che usa gli stream;14 ` da questi che derivano i nomi di alcune costanti, in quanto e per essi sono denite tre classi di dati: normali, prioritari ed urgenti. In Linux la distinzione ha senso solo per i dati urgenti dei socket (vedi sez. 19.1.3), ma su questo e su come poll reagisce alle varie condizioni dei socket torneremo in sez. 16.6.5, dove vedremo anche un esempio del suo utilizzo. Si tenga conto comunque che le costanti relative ai diversi tipi di dati (come POLLRDNORM e POLLRDBAND) sono utilizzabili soltanto qualora si sia denita la macro _XOPEN_SOURCE.15
essi sono una interfaccia specica di SysV non presente in Linux, e non hanno nulla a che fare con i le stream delle librerie standard del C. 15 e ci si ricordi di farlo sempre in testa al le, denirla soltanto prima di includere sys/poll.h non ` suciente. e
14

11.1. LI/O MULTIPLEXING

279

In caso di successo funzione ritorna restituendo il numero di le (un valore positivo) per i quali si ` vericata una delle condizioni di attesa richieste o per i quali si ` vericato un errore e e (nel qual caso vengono utilizzati i valori di tab. 11.1 esclusivi di revents). Un valore nullo indica che si ` raggiunto il timeout, mentre un valore negativo indica un errore nella chiamata, il cui e codice viene riportato al solito tramite errno. Luso di poll consente di superare alcuni dei problemi illustrati in precedenza per select; anzitutto, dato che in questo caso si usa un vettore di strutture pollfd di dimensione arbitraria, non esiste il limite introdotto dalle dimensioni massime di un le descriptor set e la dimensione dei dati passati al kernel dipende solo dal numero dei le descriptor che si vogliono controllare, non dal loro valore.16 Inoltre con select lo stesso le descriptor set ` usato sia in ingresso che in uscita, e questo e signica che tutte le volte che si vuole ripetere loperazione occorre reinizializzarlo da capo. Questa operazione, che pu` essere molto onerosa se i le descriptor da tenere sotto osservazione o sono molti, non ` invece necessaria con poll. e Abbiamo visto in sez. 11.1.2 come lo standard POSIX preveda una variante di select che consente di gestire correttamente la ricezione dei segnali nellattesa su un le descriptor. Con lintroduzione di una implementazione reale di pselect nel kernel 2.6.16, ` stata aggiunta anche e una analoga funzione che svolga lo stesso ruolo per poll. In questo caso si tratta di una estensione che ` specica di Linux e non ` prevista da nessuno e e standard; essa pu` essere utilizzata esclusivamente se si denisce la macro _GNU_SOURCE ed o ovviamente non deve essere usata se si ha a cuore la portabilit`. La funzione ` ppoll, ed il suo a e prototipo `: e
#include <sys/poll.h> int ppoll(struct pollfd *fds, nfds_t nfds, const struct timespec *timeout, const sigset_t *sigmask) La funzione attende un cambiamento di stato su un insieme di le descriptor. La funzione restituisce il numero di le descriptor con attivit` in caso di successo, o 0 se c` stato a e un timeout e -1 in caso di errore, ed in questultimo caso errno assumer` uno dei valori: a EBADF EINTR EINVAL si ` specicato un le descriptor sbagliato in uno degli insiemi. e la funzione ` stata interrotta da un segnale. e il valore di nfds eccede il limite RLIMIT_NOFILE.

ed inoltre EFAULT e ENOMEM.

La funzione ha lo stesso comportamento di poll, solo che si pu` specicare, con largomento o sigmask, il puntatore ad una maschera di segnali; questa sar` la maschera utilizzata per tutto il a tempo che la funzione rester` in attesa, alluscita viene ripristinata la maschera originale. Luso a di questa funzione ` cio` equivalente, come illustrato nella pagina di manuale, allesecuzione e e atomica del seguente codice: sigset_t origmask ; sigprocmask ( SIG_SETMASK , & sigmask , & origmask ); ready = poll (& fds , nfds , timeout ); sigprocmask ( SIG_SETMASK , & origmask , NULL ); Eccetto per timeout, che come per pselect deve essere un puntatore ad una struttura timespec, gli altri argomenti comuni con poll hanno lo stesso signicato, e la funzione restituisce gli stessi risultati illustrati in precedenza.
anche se usando dei bit un le descriptor set pu` essere pi` eciente di un vettore di strutture pollfd, o u qualora si debba osservare un solo le descriptor con un valore molto alto ci si trover` ad utilizzare inutilmente a un maggiore quantitativo di memoria.
16

280

CAPITOLO 11. LA GESTIONE AVANZATA DEI FILE

11.1.4

Linterfaccia di epoll

Nonostante poll presenti alcuni vantaggi rispetto a select, anche questa funzione non ` molto e 17 in particolare eciente quando deve essere utilizzata con un gran numero di le descriptor, nel caso in cui solo pochi di questi diventano attivi. Il problema in questo caso ` che il tempo e impiegato da poll a trasferire i dati da e verso il kernel ` proporzionale al numero di le e descriptor osservati, non a quelli che presentano attivit`. a Quando ci sono decine di migliaia di le descriptor osservati e migliaia di eventi al secondo,18 luso di poll comporta la necessit` di trasferire avanti ed indietro da user space a kernel space la a lunga lista delle strutture pollfd migliaia di volte al secondo. A questo poi si aggiunge il fatto che la maggior parte del tempo di esecuzione sar` impegnato ad eseguire una scansione su tutti i le a descriptor tenuti sotto controllo per determinare quali di essi (in genere una piccola percentuale) sono diventati attivi. In una situazione come questa luso delle funzioni classiche dellinterfaccia dellI/O multiplexing viene a costituire un collo di bottiglia che degrada irrimediabilmente le prestazioni. Per risolvere questo tipo di situazioni sono state ideate delle interfacce specialistiche19 il cui scopo fondamentale ` quello di restituire solamente le informazioni relative ai le descriptor e osservati che presentano una attivit`, evitando cos` le problematiche appena illustrate. In genere a queste prevedono che si registrino una sola volta i le descriptor da tenere sotto osservazione, e forniscono un meccanismo che notica quali di questi presentano attivit`. a Le modalit` con cui avviene la notica sono due, la prima ` quella classica (quella usata da a e poll e select) che viene chiamata level triggered.20 In questa modalit` vengono noticati i le a descriptor che sono pronti per loperazione richiesta, e questo avviene indipendentemente dalle operazioni che possono essere state fatte su di essi a partire dalla precedente notica. Per chiarire meglio il concetto ricorriamo ad un esempio: se su un le descriptor sono diventati disponibili in lettura 2000 byte ma dopo la notica ne sono letti solo 1000 (ed ` quindi possibile eseguire una e ulteriore lettura dei restanti 1000), in modalit` level triggered questo sar` nuovamente noticato a a come pronto. La seconda modalit`, ` detta edge triggered, e prevede che invece vengano noticati solo i a e le descriptor che hanno subito una transizione da non pronti a pronti. Questo signica che in modalit` edge triggered nel caso del precedente esempio il le descriptor diventato pronto a da cui si sono letti solo 1000 byte non verr` nuovamente noticato come pronto, nonostante a siano ancora disponibili in lettura 1000 byte. Solo una volta che si saranno esauriti tutti i byte disponibili, e che il le descriptor sia tornato non essere pronto, si potr` ricevere una ulteriore a notica qualora ritornasse pronto. Nel caso di Linux al momento la sola interfaccia che fornisce questo tipo di servizio ` epoll,21 e anche se sono in discussione altre interfacce con le quali si potranno eettuare lo stesso tipo di operazioni;22 epoll ` in grado di operare sia in modalit` level triggered che edge triggered. e a La prima versione epoll prevedeva lapertura di uno speciale le di dispositivo, /dev/epoll, per ottenere un le descriptor da utilizzare con le funzioni dellinterfaccia,23 ma poi si ` passati e
in casi del genere select viene scartata a priori, perch pu` avvenire che il numero di le descriptor ecceda e o le dimensioni massime di un le descriptor set. 18 il caso classico ` quello di un server web di un sito con molti accessi. e 19 come /dev/poll in Solaris, o kqueue in BSD. 20 la nomenclatura ` stata introdotta da Jonathan Lemon in un articolo su kqueue al BSDCON 2000, e deriva e da quella usata nellelettronica digitale. 21 linterfaccia ` stata creata da Davide Libenzi, ed ` stata introdotta per la prima volta nel kernel 2.5.44, ma e e la sua forma denitiva ` stata raggiunta nel kernel 2.5.66. e 22 al momento della stesura di queste note (Giugno 2007) unaltra interfaccia proposta ` quella di kevent, che e fornisce un sistema di notica di eventi generico in grado di fornire le stesse funzionalit` di epoll, esiste per` una a o forte discussione intorno a tutto ci` e niente di denito. o 23 il backporting dellinterfaccia per il kernel 2.4, non uciale, utilizza sempre questo le.
17

11.1. LI/O MULTIPLEXING

281

alluso una apposita system call. Il primo passo per usare linterfaccia di epoll ` pertanto quello e di chiamare la funzione epoll_create, il cui prototipo `: e
#include <sys/epoll.h> int epoll_create(int size) Apre un le descriptor per epoll. La funzione restituisce un le descriptor in caso di successo, o 1 in caso di errore, nel qual caso errno assumer` uno dei valori: a EINVAL ENFILE ENOMEM si ` specicato un valore di size non positivo. e si ` raggiunto il massimo di le descriptor aperti nel sistema. e non c` suciente memoria nel kernel per creare listanza. e

La funzione restituisce un le descriptor speciale,24 detto anche epoll descriptor, che viene associato alla infrastruttura utilizzata dal kernel per gestire la notica degli eventi; largomento size serve a dare lindicazione del numero di le descriptor che si vorranno tenere sotto controllo, ma costituisce solo un suggerimento per semplicare lallocazione di risorse sucienti, non un valore massimo. Una volta ottenuto un le descriptor per epoll il passo successivo ` indicare quali le descripe tor mettere sotto osservazione e quali operazioni controllare, per questo si deve usare la seconda funzione dellinterfaccia, epoll_ctl, il cui prototipo `: e
#include <sys/epoll.h> int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event) Esegue le operazioni di controllo di epoll. La funzione restituisce 0 in caso di successo o 1 in caso di errore, nel qual caso errno assumer` a uno dei valori: EBADF EEXIST EINVAL ENOENT ENOMEM EPERM il le descriptor epfd o fd non sono validi. loperazione richiesta ` EPOLL_CTL_ADD ma fd ` gi` stato inserito in epfd. e e a il le descriptor epfd non ` stato ottenuto con epoll_create, o fd ` lo stesso epfd o e e loperazione richiesta con op non ` supportata. e loperazione richiesta ` EPOLL_CTL_MOD o EPOLL_CTL_DEL ma fd non ` inserito in epfd. e e non c` suciente memoria nel kernel gestire loperazione richiesta. e il le fd non supporta epoll.

Il comportamento della funzione viene controllato dal valore dallargomento op che consente di specicare quale operazione deve essere eseguita. Le costanti che deniscono i valori utilizzabili per op sono riportate in tab. 11.2, assieme al signicato delle operazioni cui fanno riferimento.
Valore EPOLL_CTL_ADD Signicato Aggiunge un nuovo le descriptor da osservare fd alla lista dei le descriptor controllati tramite epfd, in event devono essere specicate le modalit` di osservazione. a Modica le modalit` di osservazione del le descriptor fd a secondo il contenuto di event. Rimuove il le descriptor fd dalla lista dei le controllati tramite epfd.

EPOLL_CTL_MOD EPOLL_CTL_DEL

Tabella 11.2: Valori dellargomento op che consentono di scegliere quale operazione di controllo eettuare con la funzione epoll_ctl.

La funzione prende sempre come primo argomento un le descriptor di epoll, epfd, che deve essere stato ottenuto in precedenza con una chiamata a epoll_create. Largomento fd indica
esso non ` associato a nessun le su disco, inoltre a dierenza dei normali le descriptor non pu` essere inviato e o ad un altro processo attraverso un socket locale (vedi sez. 18.2.1).
24

282

CAPITOLO 11. LA GESTIONE AVANZATA DEI FILE

invece il le descriptor che si vuole tenere sotto controllo, questultimo pu` essere un qualunque o le descriptor utilizzabile con poll, ed anche un altro le descriptor di epoll, ma non lo stesso epfd. Lultimo argomento, event, deve essere un puntatore ad una struttura di tipo epoll_event, ed ha signicato solo con le operazioni EPOLL_CTL_MOD e EPOLL_CTL_ADD, per le quali serve ad indicare quale tipo di evento relativo ad fd si vuole che sia tenuto sotto controllo. Largomento viene ignorato con loperazione EPOLL_CTL_DEL.25
typedef union epoll_data { void * ptr ; int fd ; __uint32_t u32 ; __uint64_t u64 ; } epoll_data_t ; struct epoll_event { __uint32_t events ; epoll_data_t data ; };

/* Epoll events */ /* User data variable */

Figura 11.2: La struttura epoll_event, che consente di specicare gli eventi associati ad un le descriptor controllato con epoll.

La struttura epoll_event ` lanaloga di pollfd e come questultima serve sia in ingresso e (quando usata con epoll_ctl) ad impostare quali eventi osservare, che in uscita (nei risultati ottenuti con epoll_wait) per ricevere le notiche degli eventi avvenuti. La sua denizione ` e riportata in g. 11.2. Il primo campo, events, ` una maschera binaria in cui ciascun bit corrisponde o ad un tipo di e evento, o una modalit` di notica; detto campo deve essere specicato come OR aritmetico delle a costanti riportate in tab. 11.3. Il secondo campo, data, serve ad indicare a quale le descriptor si intende fare riferimento, ed in astratto pu` contenere un valore qualsiasi che permetta di o identicarlo, di norma comunque si usa come valore lo stesso fd. Le modalit` di utilizzo di epoll prevedano che si denisca qual` linsieme dei le descriptor a e da tenere sotto controllo tramite un certo epoll descriptor epfd attraverso una serie di chiamate a EPOLL_CTL_ADD.27 Luso di EPOLL_CTL_MOD consente in seguito di modicare le modalit` di a osservazione di un le descriptor che sia gi` stato aggiunto alla lista di osservazione. a Le impostazioni di default prevedono che la notica degli eventi richiesti sia eettuata in modalit` level triggered, a meno che sul le descriptor non si sia impostata la modalit` edge a a triggered, registrandolo con EPOLLET attivo nel campo events. Si tenga presente che ` possibile e tenere sotto osservazione uno stesso le descriptor su due epoll descriptor diversi, ed entrambi riceveranno le notiche, anche se questa pratica ` sconsigliata. e Qualora non si abbia pi` interesse nellosservazione di un le descriptor lo si pu` rimuovere u o dalla lista associata a epfd con EPOLL_CTL_DEL; si tenga conto inoltre che i le descriptor sotto osservazione che vengono chiusi sono eliminati dalla lista automaticamente e non ` necessario e usare EPOLL_CTL_DEL.
no al kernel 2.6.9 era comunque richiesto che questo fosse un puntatore valido, anche se poi veniva ignorato, a partire dal 2.6.9 si pu` specicare anche un valore NULL. o 26 questa modalit` ` disponibile solo a partire dal kernel 2.6.2. ae 27 un difetto dellinterfaccia ` che queste chiamate devono essere ripetute per ciascun le descriptor, incorrendo e in una perdita di prestazioni qualora il numero di le descriptor sia molto grande; per questo ` stato proposto e di introdurre come estensione una funzione epoll_ctlv che consenta di eettuare con una sola chiamata le impostazioni per un blocco di le descriptor.
25

11.1. LI/O MULTIPLEXING


Valore EPOLLIN EPOLLOUT EPOLLRDHUP Signicato Il le ` pronto per le operazioni di lettura (analogo di e POLLIN). Il le ` pronto per le operazioni di scrittura (analogo di e POLLOUT). Laltro capo di un socket di tipo SOCK_STREAM (vedi sez. 15.2.3) ha chiuso la connessione o il capo in scrittura della stessa (vedi sez. 16.6.3). Ci sono dati urgenti disponibili in lettura (analogo di POLLPRI); questa condizione viene comunque riportata in uscita, e non ` necessaria impostarla in ingresso. e Si ` vericata una condizione di errore (analogo di e POLLERR); questa condizione viene comunque riportata in uscita, e non ` necessaria impostarla in ingresso. e Si ` vericata una condizione di hung-up. e Imposta la notica in modalit` edge triggered per il le a descriptor associato. Imposta la modalit` one-shot per il le descriptor a associato.26

283

EPOLLPRI

EPOLLERR

EPOLLHUP EPOLLET EPOLLONESHOT

Tabella 11.3: Costanti che identicano i bit del campo events di epoll_event.

Inne una particolare modalit` di notica ` quella impostata con EPOLLONESHOT: a causa a e dellimplementazione di epoll infatti quando si ` in modalit` edge triggered larrivo in rapida e a successione di dati in blocchi separati28 pu` causare una generazione di eventi (ad esempio o segnalazioni di dati in lettura disponibili) anche se la condizione ` gi` stata rilevata.29 e a Anche se la situazione ` facile da gestire, la si pu` evitare utilizzando EPOLLONESHOT per e o impostare la modalit` one-shot, in cui la notica di un evento viene eettuata una sola volta, dopo a di che il le descriptor osservato, pur restando nella lista di osservazione, viene automaticamente disattivato,30 e per essere riutilizzato dovr` essere riabilitato esplicitamente con una successiva a chiamata con EPOLL_CTL_MOD. Una volta impostato linsieme di le descriptor che si vogliono osservare con i relativi eventi, la funzione che consente di attendere loccorrenza di uno di tali eventi ` epoll_wait, il cui e prototipo `: e
#include <sys/epoll.h> int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout) Attende che uno dei le descriptor osservati sia pronto. La funzione restituisce il numero di le descriptor pronti in caso di successo o 1 in caso di errore, nel qual caso errno assumer` uno dei valori: a EBADF EFAULT EINTR EINVAL il le descriptor epfd non ` valido. e il puntatore events non ` valido. e la funzione ` stata interrotta da un segnale prima della scadenza di timeout. e il le descriptor epfd non ` stato ottenuto con epoll_create, o maxevents non ` e e maggiore di zero.

La funzione si blocca in attesa di un evento per i le descriptor registrati nella lista di osservazione di epfd no ad un tempo massimo specicato in millisecondi tramite largomento timeout. Gli eventi registrati vengono riportati in un vettore di strutture epoll_event (che deve essere stato allocato in precedenza) allindirizzo indicato dallargomento events, no ad un numero massimo di eventi impostato con largomento maxevents.
28 29

questo ` tipico con i socket di rete, in quanto i dati arrivano a pacchetti. e si avrebbe cio` una rottura della logica edge triggered. e 30 la cosa avviene contestualmente al ritorno di epoll_wait a causa dellevento in questione.

284

CAPITOLO 11. LA GESTIONE AVANZATA DEI FILE

La funzione ritorna il numero di eventi rilevati, o un valore nullo qualora sia scaduto il tempo massimo impostato con timeout. Per questultimo, oltre ad un numero di millisecondi, si pu` o 31 o il valore utilizzare il valore nullo, che indica di non attendere e ritornare immediatamente, 1, che indica unattesa indenita. Largomento maxevents dovr` invece essere sempre un intero a positivo. Come accennato la funzione restituisce i suoi risultati nel vettore di strutture epoll_event puntato da events; in tal caso nel campo events di ciascuna di esse saranno attivi i ag relativi agli eventi accaduti, mentre nel campo data sar` restituito il valore che era stato impostato per il a le descriptor per cui si ` vericato levento quando questo era stato registrato con le operazioni e EPOLL_CTL_MOD o EPOLL_CTL_ADD, in questo modo il campo data consente di identicare il le descriptor.32 Si ricordi che le occasioni per cui epoll_wait ritorna dipendono da come si ` impostata la e modalit` di osservazione (se level triggered o edge triggered ) del singolo le descriptor. Linterfaca cia assicura che se arrivano pi` eventi fra due chiamate successive ad epoll_wait questi vengano u combinati. Inoltre qualora su un le descriptor fossero presenti eventi non ancora noticati, e si eettuasse una modica dellosservazione con EPOLL_CTL_MOD questi verrebbero riletti alla luce delle modiche. Si tenga presente inne che con luso della modalit` edge triggered il ritorno di epoll_wait a indica un le descriptor ` pronto e rester` tale ntanto che non si sono completamente esaurite le e a operazioni su di esso. Questa condizione viene generalmente rilevata dalloccorrere di un errore di EAGAIN al ritorno di una read o una write,33 ma questa non ` la sola modalit` possibile, ad e a esempio la condizione pu` essere riconosciuta anche con il fatto che sono stati restituiti meno o dati di quelli richiesti. Come le precedenti select e poll, le funzioni dellinterfaccia di epoll vengono utilizzate prevalentemente con i server di rete, quando si devono tenere sotto osservazione un gran numero di socket; per questo motivo rimandiamo di nuovo la trattazione di un esempio concreto a quando avremo esaminato in dettaglio le caratteristiche dei socket, in particolare si potr` trovare un a programma che utilizza questa interfaccia in sez. 16.6.

11.2

Laccesso asincrono ai le

Bench lI/O multiplexing sia stata la prima, e sia tuttora una fra le pi` diuse modalit` di e u a gestire lI/O in situazioni complesse in cui si debba operare su pi` le contemporaneamente, u esistono altre modalit` di gestione delle stesse problematiche. In particolare sono importanti in a questo contesto le modalit` di accesso ai le eseguibili in maniera asincrona, quelle cio` in cui un a e processo non deve bloccarsi in attesa della disponibilit` dellaccesso al le, ma pu` proseguire a o nellesecuzione utilizzando invece un meccanismo di notica asincrono (di norma un segnale, ma esistono anche altre interfacce, come inotify), per essere avvisato della possibilit` di eseguire le a operazioni di I/O volute.

11.2.1

Il Signal driven I/O

Abbiamo accennato in sez. 6.2.1 che ` possibile, attraverso luso del ag O_ASYNC,34 aprire un e le in modalit` asincrona, cos` come ` possibile attivare in un secondo tempo questa modalit` a e a impostando questo ag attraverso luso di fcntl con il comando F_SETFL (vedi sez. 6.3.6).
anche in questo caso il valore di ritorno sar` nullo. a ed ` per questo che, come accennato, ` consuetudine usare per data il valore del le descriptor stesso. e e 33 ` opportuno ricordare ancora una volta che luso dellI/O multiplexing richiede di operare sui le in modalit` e a non bloccante. 34 luso del ag di O_ASYNC e dei comandi F_SETOWN e F_GETOWN per fcntl ` specico di Linux e BSD. e
32 31

11.2. LACCESSO ASINCRONO AI FILE

285

In realt` parlare di apertura in modalit` asincrona non signica che le operazioni di lettura a a o scrittura del le vengono eseguite in modo asincrono (tratteremo questo, che ` ci` che pi` e o u propriamente viene chiamato I/O asincrono, in sez. 11.2.3), quanto dellattivazione un meccanismo di notica asincrona delle variazione dello stato del le descriptor aperto in questo modo. Quello che succede in questo caso ` che il sistema genera un segnale (normalmente SIGIO, ma e ` possibile usarne altri con il comando F_SETSIG di fcntl) tutte le volte che diventa possibile e leggere o scrivere dal le descriptor che si ` posto in questa modalit`.35 e a Si pu` inoltre selezionare, con il comando F_SETOWN di fcntl, quale processo (o gruppo o di processi) ricever` il segnale. Se pertanto si eettuano le operazioni di I/O in risposta alla a ricezione del segnale non ci sar` pi` la necessit` di restare bloccati in attesa della disponibilit` a u a a di accesso ai le. Per questo motivo Stevens, ed anche le pagine di manuale di Linux, chiamano questa modalit` a Signal driven I/O. Questa ` ancora unaltra modalit` di gestione dellI/O, alternativa alluso e a di epoll,36 che consente di evitare luso delle funzioni poll o select che, come illustrato in sez. 11.1.4, quando vengono usate con un numero molto grande di le descriptor, non hanno buone prestazioni. Tuttavia con limplementazione classica dei segnali questa modalit` di I/O presenta notea voli problemi, dato che non ` possibile determinare, quando i le descriptor sono pi` di uno, e u qual ` quello responsabile dellemissione del segnale. Inoltre dato che i segnali normali non e si accodano (si ricordi quanto illustrato in sez. 9.1.4), in presenza di pi` le descriptor attivi u contemporaneamente, pi` segnali emessi nello stesso momento verrebbero noticati una volta u sola. Linux per` supporta le estensioni POSIX.1b dei segnali real-time, che vengono accodati e o che permettono di riconoscere il le descriptor che li ha emessi. In questo caso infatti si pu` o fare ricorso alle informazioni aggiuntive restituite attraverso la struttura siginfo_t, utilizzando la forma estesa sa_sigaction del gestore installata con il ag SA_SIGINFO (si riveda quanto illustrato in sez. 9.4.3). Per far questo per` occorre utilizzare le funzionalit` dei segnali real-time (vedi sez. 9.5.1) o a impostando esplicitamente con il comando F_SETSIG di fcntl un segnale real-time da inviare in caso di I/O asincrono (il segnale predenito ` SIGIO). In questo caso il gestore, tutte le volte che e ricever` SI_SIGIO come valore del campo si_code37 di siginfo_t, trover` nel campo si_fd il a a valore del le descriptor che ha generato il segnale. Un secondo vantaggio delluso dei segnali real-time ` che essendo questi ultimi dotati di una e coda di consegna ogni segnale sar` associato ad uno solo le descriptor; inoltre sar` possibile a a stabilire delle priorit` nella risposta a seconda del segnale usato, dato che i segnali real-time a supportano anche questa funzionalit`. In questo modo si pu` identicare immediatamente un a o le su cui laccesso ` diventato possibile evitando completamente luso di funzioni come poll e e select, almeno ntanto che non si satura la coda. Se infatti si eccedono le dimensioni di questultima, il kernel, non potendo pi` assicurare il u comportamento corretto per un segnale real-time, invier` al suo posto un solo SIGIO, su cui si a saranno accumulati tutti i segnali in eccesso, e si dovr` allora determinare con un ciclo quali a sono i le diventati attivi. Lunico modo per essere sicuri che questo non avvenga ` di impostare e
questa modalit` non ` utilizzabile con i le ordinari ma solo con socket, le di terminale o pseudo terminale, a e e, a partire dal kernel 2.6, anche per fo e pipe. 36 anche se le prestazioni ottenute con questa tecnica sono inferiori, il vantaggio ` che questa modalit` ` utie a e lizzabile anche con kernel che non supportano epoll, come quelli della serie 2.4, ottenendo comunque prestazioni superiori a quelle che si hanno con poll e select. 37 il valore resta SI_SIGIO qualunque sia il segnale che si ` associato allI/O asincrono, ed indica appunto che il e segnale ` stato generato a causa di attivit` nellI/O asincrono. e a
35

286

CAPITOLO 11. LA GESTIONE AVANZATA DEI FILE

la lunghezza della coda dei segnali real-time ad una dimensione identica al valore massimo del numero di le descriptor utilizzabili.38

11.2.2

I meccanismi di notica asincrona.

Una delle domande pi` frequenti nella programmazione in ambiente unix-like ` quella di come u e fare a sapere quando un le viene modicato. La risposta39 ` che nellarchitettura classica di e Unix questo non ` possibile. Al contrario di altri sistemi operativi infatti un kernel unix-like e classico non prevedeva alcun meccanismo per cui un processo possa essere noticato di eventuali modiche avvenute su un le. Questo ` il motivo per cui i demoni devono essere avvisati in e 40 se il loro le di congurazione ` stato modicato, perch possano rileggerlo e qualche modo e e riconoscere le modiche. Questa scelta ` stata fatta perch provvedere un simile meccanismo a livello generico per e e qualunque le comporterebbe un notevole aumento di complessit` dellarchitettura della gestione a dei le, il tutto per fornire una funzionalit` che serve soltanto in alcuni casi particolari. Dato a che allorigine di Unix i soli programmi che potevano avere una tale esigenza erano i demoni, attenendosi a uno dei criteri base della progettazione, che era di far fare al kernel solo le operazioni strettamente necessarie e lasciare tutto il resto a processi in user space, non era stata prevista nessuna funzionalit` di notica. a Visto per` il crescente interesse nei confronti di una funzionalit` di questo tipo, che ` molo a e to richiesta specialmente nello sviluppo dei programmi ad interfaccia graca, quando si deve presentare allutente lo stato del lesystem, sono state successivamente introdotte delle estensioni che permettessero la creazione di meccanismi di notica pi` ecienti dellunica soluzione u disponibile con linterfaccia tradizionale, che ` quella del polling. e Queste nuove funzionalit` sono delle estensioni speciche, non standardizzate, che sono dia sponibili soltanto su Linux (anche se altri kernel supportano meccanismi simili). Alcune di esse sono realizzate, e solo a partire dalla versione 2.4 del kernel, attraverso luso di alcuni comandi aggiuntivi per la funzione fcntl (vedi sez. 6.3.6), che divengono disponibili soltanto se si ` e denita la macro _GNU_SOURCE prima di includere fcntl.h. La prima di queste funzionalit` ` quella del cosiddetto le lease; questo ` un meccanismo a e e che consente ad un processo, detto lease holder, di essere noticato quando un altro processo, chiamato a sua volta lease breaker, cerca di eseguire una open o una truncate sul le del quale lholder detiene il lease. La notica avviene in maniera analoga a come illustrato in precedenza per luso di O_ASYNC: di default viene inviato al lease holder il segnale SIGIO, ma questo segnale pu` essere modicato o usando il comando F_SETSIG di fcntl.41 Se si ` fatto questo42 e si ` installato il gestore del e e segnale con SA_SIGINFO si ricever` nel campo si_fd della struttura siginfo_t il valore del a le descriptor del le sul quale ` stato compiuto laccesso; in questo modo un processo pu` e o mantenere anche pi` di un le lease. u Esistono due tipi di le lease: di lettura (read lease) e di scrittura (write lease). Nel primo caso la notica avviene quando un altro processo esegue lapertura del le in scrittura o usa truncate per troncarlo. Nel secondo caso la notica avviene anche se il le viene aperto il lettura; in questultimo caso per` il lease pu` essere ottenuto solo se nessun altro processo ha o o aperto lo stesso le.
vale a dire impostare il contenuto di /proc/sys/kernel/rtsig-max allo stesso valore del contenuto di /proc/sys/fs/file-max. 39 o meglio la non risposta, tanto che questa nelle Unix FAQ [10] viene anche chiamata una Frequently Unanswered Question. 40 in genere questo vien fatto inviandogli un segnale di SIGHUP che, per una convenzione adottata dalla gran parte di detti programmi, causa la rilettura della congurazione. 41 anche in questo caso si pu` rispecicare lo stesso SIGIO. o 42 ` in genere ` opportuno farlo, come in precedenza, per utilizzare segnali real-time. e e
38

11.2. LACCESSO ASINCRONO AI FILE

287

Come accennato in sez. 6.3.6 il comando di fcntl che consente di acquisire un le lease ` e F_SETLEASE, che viene utilizzato anche per rilasciarlo. In tal caso il le descriptor fd passato a fcntl servir` come riferimento per il le su cui si vuole operare, mentre per indicare il tipo a di operazione (acquisizione o rilascio) occorrer` specicare come valore dellargomento arg di a fcntl uno dei tre valori di tab. 11.4.
Valore F_RDLCK F_WRLCK F_UNLCK Signicato Richiede un read lease. Richiede un write lease. Rilascia un le lease.

Tabella 11.4: Costanti per i tre possibili valori dellargomento arg di fcntl quando usata con i comandi F_SETLEASE e F_GETLEASE.

Se invece si vuole conoscere lo stato di eventuali le lease occorrer` chiamare fcntl sul a relativo le descriptor fd con il comando F_GETLEASE, e si otterr` indietro nellargomento arg a uno dei valori di tab. 11.4, che indicheranno la presenza del rispettivo tipo di lease, o, nel caso di F_UNLCK, lassenza di qualunque le lease. Si tenga presente che un processo pu` mantenere solo un tipo di lease su un le, e che un o lease pu` essere ottenuto solo su le di dati (pipe e dispositivi sono quindi esclusi). Inoltre un o processo non privilegiato pu` ottenere un lease soltanto per un le appartenente ad un uid o corrispondente a quello del processo. Soltanto un processo con privilegi di amministratore (cio` e con la capability CAP_LEASE, vedi sez. 3.3.4) pu` acquisire lease su qualunque le. o Se su un le ` presente un lease quando il lease breaker esegue una truncate o una open e che conigge con esso,43 la funzione si blocca44 e viene eseguita la notica al lease holder, cos` che questo possa completare le sue operazioni sul le e rilasciare il lease. In sostanza con un read lease si rilevano i tentativi di accedere al le per modicarne i dati da parte di un altro processo, mentre con un write lease si rilevano anche i tentativi di accesso in lettura. Si noti comunque che le operazioni di notica avvengono solo in fase di apertura del le e non sulle singole operazioni di lettura e scrittura. Lutilizzo dei le lease consente al lease holder di assicurare la consistenza di un le, a seconda dei due casi, prima che un altro processo inizi con le sue operazioni di scrittura o di lettura su di esso. In genere un lease holder che riceve una notica deve provvedere a completare le necessarie operazioni (ad esempio scaricare eventuali buer), per poi rilasciare il lease cos` che il lease breaker possa eseguire le sue operazioni. Questo si fa con il comando F_SETLEASE, o rimuovendo il lease con F_UNLCK, o, nel caso di write lease che conigge con una operazione di lettura, declassando il lease a lettura con F_RDLCK. Se il lease holder non provvede a rilasciare il lease entro il numero di secondi specicato dal parametro di sistema mantenuto in /proc/sys/fs/lease-break-time sar` il kernel stesso a a rimuoverlo (o declassarlo) automaticamente.45 Una volta che un lease ` stato rilasciato o e declassato (che questo sia fatto dal lease holder o dal kernel ` lo stesso) le chiamate a open o e truncate eseguite dal lease breaker rimaste bloccate proseguono automaticamente. Bench possa risultare utile per sincronizzare laccesso ad uno stesso le da parte di pi` e u processi, luso dei le lease non consente comunque di risolvere il problema di rilevare automaticamente quando un le o una directory vengono modicati, che ` quanto necessario ad esempio e ai programma di gestione dei le dei vari desktop graci. Per risolvere questo problema a partire dal kernel 2.4 ` stata allora creata unaltra intere
43 in realt` truncate conigge sempre, mentre open, se eseguita in sola lettura, non conigge se si tratta di un a read lease. 44 a meno di non avere aperto il le con O_NONBLOCK, nel qual caso open fallirebbe con un errore di EWOULDBLOCK. 45 questa ` una misura di sicurezza per evitare che un processo blocchi indenitamente laccesso ad un le e acquisendo un lease.

288

CAPITOLO 11. LA GESTIONE AVANZATA DEI FILE

faccia,46 chiamata dnotify, che consente di richiedere una notica quando una directory, o uno qualunque dei le in essa contenuti, viene modicato. Come per i le lease la notica avviene di default attraverso il segnale SIGIO, ma se ne pu` utilizzare un altro.47 Inoltre, come in preo cedenza, si potr` ottenere nel gestore del segnale il le descriptor che ` stato modicato tramite a e il contenuto della struttura siginfo_t.
Valore DN_ACCESS DN_MODIFY DN_CREATE Signicato Un le ` stato acceduto, con lesecuzione di una fra read, e pread, readv. Un le ` stato modicato, con lesecuzione di una fra e write, pwrite, writev, truncate, ftruncate. ` E stato creato un le nella directory, con lesecuzione di una fra open, creat, mknod, mkdir, link, symlink, rename (da unaltra directory). ` E stato cancellato un le dalla directory con lesecuzione di una fra unlink, rename (su unaltra directory), rmdir. ` E stato rinominato un le allinterno della directory (con rename). ` E stato modicato un attributo di un le con lesecuzione di una fra chown, chmod, utime. Richiede una notica permanente di tutti gli eventi.

DN_DELETE DN_RENAME DN_ATTRIB DN_MULTISHOT

Tabella 11.5: Le costanti che identicano le varie classi di eventi per i quali si richiede la notica con il comando F_NOTIFY di fcntl.

Ci si pu` registrare per le notiche dei cambiamenti al contenuto di una certa directory o eseguendo la funzione fcntl su un le descriptor associato alla stessa con il comando F_NOTIFY. In questo caso largomento arg di fcntl serve ad indicare per quali classi eventi si vuole ricevere la notica, e prende come valore una maschera binaria composta dallOR aritmetico di una o pi` delle costanti riportate in tab. 11.5. u A meno di non impostare in maniera esplicita una notica permanente usando il valore DN_MULTISHOT, la notica ` singola: viene cio` inviata una sola volta quando si verica uno e e qualunque fra gli eventi per i quali la si ` richiesta. Questo signica che un programma deve e registrarsi unaltra volta se desidera essere noticato di ulteriori cambiamenti. Se si eseguono diverse chiamate con F_NOTIFY e con valori diversi per arg questi ultimi si accumulano; cio` e eventuali nuovi classi di eventi specicate in chiamate successive vengono aggiunte a quelle gi` a impostate nelle precedenti. Se si vuole rimuovere la notica si deve invece specicare un valore nullo. Il maggiore problema di dnotify ` quello della scalabilit`: si deve usare un le descriptor per e a ciascuna directory che si vuole tenere sotto controllo, il che porta facilmente ad avere un eccesso di le aperti. Inoltre quando la directory che si controlla ` allinterno di un dispositivo rimovibile, e mantenere il relativo le descriptor aperto comporta limpossibilit` di smontare il dispositivo e a di rimuoverlo, il che in genere complica notevolmente la gestione delluso di questi dispositivi. Un altro problema ` che linterfaccia di dnotify consente solo di tenere sotto controllo il e contenuto di una directory; la modica di un le viene segnalata, ma poi ` necessario vericare e di quale le si tratta (operazione che pu` essere molto onerosa quando una directory contiene o un gran numero di le). Inne luso dei segnali come interfaccia di notica comporta tutti i problemi di gestione visti in sez. 9.3 e sez. 9.4. Per tutta questa serie di motivi in generale quella di dnotify viene considerata una interfaccia di usabilit` problematica. a
si ricordi che anche questa ` una interfaccia specica di Linux che deve essere evitata se si vogliono scrie vere programmi portabili, e che le funzionalit` illustrate sono disponibili soltanto se ` stata denita la macro a e _GNU_SOURCE. 47 e di nuovo, per le ragioni gi` esposte in precedenza, ` opportuno che si utilizzino dei segnali real-time. a e
46

11.2. LACCESSO ASINCRONO AI FILE

289

Per risolvere i problemi appena illustrati ` stata introdotta una nuova interfaccia per lose servazione delle modiche a le o directory, chiamata inotify.48 Anche questa ` una interfaccia e specica di Linux (pertanto non deve essere usata se si devono scrivere programmi portabili), ed ` basata sulluso di una coda di notica degli eventi associata ad un singolo le descriptor, e il che permette di risolvere il principale problema di dnotify. La coda viene creata attraverso la funzione inotify_init, il cui prototipo `: e
#include <sys/inotify.h> int inotify_init(void) Inizializza una istanza di inotify. La funzione restituisce un le descriptor in caso di successo, o 1 in caso di errore, nel qual caso errno assumer` uno dei valori: a EMFILE ENFILE ENOMEM si ` raggiunto il numero massimo di istanze di inotify consentite allutente. e si ` raggiunto il massimo di le descriptor aperti nel sistema. e non c` suciente memoria nel kernel per creare listanza. e

La funzione non prende alcun argomento; inizializza una istanza di inotify e restituisce un le descriptor attraverso il quale verranno eettuate le operazioni di notica;49 si tratta di un le descriptor speciale che non ` associato a nessun le su disco, e che viene utilizzato solo per e noticare gli eventi che sono stati posti in osservazione. Dato che questo le descriptor non ` e associato a nessun le o directory reale, linconveniente di non poter smontare un lesystem i cui le sono tenuti sotto osservazione viene completamente eliminato.50 Inoltre trattandosi di un le descriptor a tutti gli eetti, esso potr` essere utilizzato come a argomento per le funzioni select e poll e con linterfaccia di epoll ; siccome gli eventi vengono noticati come dati disponibili in lettura, dette funzioni ritorneranno tutte le volte che si avr` un a 51 si potr` gestire losservazione degli evento di notica. Cos` invece di dover utilizzare i segnali, , a eventi con una qualunque delle modalit` di I/O multiplexing illustrate in sez. 11.1. Qualora si a voglia cessare losservazione, sar` suciente chiudere il le descriptor e tutte le risorse allocate a saranno automaticamente rilasciate. Inne linterfaccia di inotify consente di mettere sotto osservazione, oltre che una directory, anche singoli le. Una volta creata la coda di notica si devono denire gli eventi da tenere sotto osservazione; questo viene fatto attraverso una lista di osservazione (o watch list) che ` associata e alla coda. Per gestire la lista di osservazione linterfaccia fornisce due funzioni, la prima di queste ` inotify_add_watch, il cui prototipo `: e e
#include <sys/inotify.h> int inotify_add_watch(int fd, const char *pathname, uint32_t mask) Aggiunge un evento di osservazione alla lista di osservazione di fd. La funzione restituisce un valore positivo in caso di successo, o 1 in caso di errore, nel qual caso errno assumer` uno dei valori: a EACCESS EINVAL ENOSPC non si ha accesso in lettura al le indicato. mask non contiene eventi legali o fd non ` un le descriptor di inotify. e si ` raggiunto il numero massimo di voci di osservazione o il kernel non ha potuto e allocare una risorsa necessaria.

ed inoltre EFAULT, ENOMEM e EBADF. linterfaccia ` disponibile a partire dal kernel 2.6.13, le relative funzioni sono state introdotte nelle glibc 2.4. e per evitare abusi delle risorse di sistema ` previsto che un utente possa utilizzare un numero limitato di istanze e di inotify; il valore di default del limite ` di 128, ma questo valore pu` essere cambiato con sysctl o usando il le e o /proc/sys/fs/inotify/max_user_instances. 50 anzi, una delle capacit` dellinterfaccia di inotify ` proprio quella di noticare il fatto che il lesystem su cui a e si trova il le o la directory osservata ` stato smontato. e 51 considerati una pessima scelta dal punto di vista dellinterfaccia utente.
49 48

290

CAPITOLO 11. LA GESTIONE AVANZATA DEI FILE

La funzione consente di creare un osservatore (il cosiddetto watch) nella lista di osservazione di una coda di notica, che deve essere indicata specicando il le descriptor ad essa associato nellargomento fd.52 Il le o la directory da porre sotto osservazione vengono invece indicati per nome, da passare nellargomento pathname. Inne il terzo argomento, mask, indica che tipo di eventi devono essere tenuti sotto osservazione e le modalit` della stessa. Loperazione a pu` essere ripetuta per tutti i le e le directory che si vogliono tenere sotto osservazione,53 e si o utilizzer` sempre un solo le descriptor. a Il tipo di evento che si vuole osservare deve essere specicato nellargomento mask come maschera binaria, combinando i valori delle costanti riportate in tab. 11.6 che identicano i singoli bit della maschera ed il relativo signicato. In essa si sono marcati con un gli eventi che, quando specicati per una directory, vengono osservati anche su tutti i le che essa contiene. Nella seconda parte della tabella si sono poi indicate alcune combinazioni predenite dei ag della prima parte.
Valore IN_ACCESS IN_ATTRIB IN_CLOSE_WRITE IN_CLOSE_NOWRITE IN_CREATE IN_DELETE IN_DELETE_SELF IN_MODIFY IN_MOVE_SELF IN_MOVED_FROM IN_MOVED_TO IN_OPEN IN_CLOSE IN_MOVE IN_ALL_EVENTS Signicato C` stato accesso al le in lettura. e Ci sono stati cambiamenti sui dati dellinode (o sugli attributi estesi, vedi sez. 5.4.1). ` E stato chiuso un le aperto in scrittura. ` E stato chiuso un le aperto in sola lettura. ` E stato creato un le o una directory in una directory sotto osservazione. ` E stato cancellato un le o una directory in una directory sotto osservazione. ` E stato cancellato il le (o la directory) sotto osservazione. ` E stato modicato il le. ` E stato rinominato il le (o la directory) sotto osservazione. Un le ` stato spostato fuori dalla directory sotto osservazione. e Un le ` stato spostato nella directory sotto osservazione. e Un le ` stato aperto. e Combinazione di IN_CLOSE_WRITE e IN_CLOSE_NOWRITE. Combinazione di IN_MOVED_FROM e IN_MOVED_TO. Combinazione di tutti i ag possibili.

Tabella 11.6: Le costanti che identicano i bit della maschera binaria dellargomento mask di inotify_add_watch che indicano il tipo di evento da tenere sotto osservazione.

Oltre ai ag di tab. 11.6, che indicano il tipo di evento da osservare e che vengono utilizzati anche in uscita per indicare il tipo di evento avvenuto, inotify_add_watch supporta ulteriori ag,54 riportati in tab. 11.7, che indicano le modalit` di osservazione (da passare sempre nela largomento mask) e che al contrario dei precedenti non vengono mai impostati nei risultati in uscita. Se non esiste nessun watch per il le o la directory specicata questo verr` creato per gli eventi a specicati dallargomento mask, altrimenti la funzione sovrascriver` le impostazioni precedenti, a a meno che non si sia usato il ag IN_MASK_ADD, nel qual caso gli eventi specicati saranno aggiunti a quelli gi` presenti. a Come accennato quando si tiene sotto osservazione una directory vengono restituite le informazioni sia riguardo alla directory stessa che ai le che essa contiene; questo comportamento pu` essere disabilitato utilizzando il ag IN_ONLYDIR, che richiede di riportare soltanto gli eveno ti relativi alla directory stessa. Si tenga presente inoltre che quando si osserva una directory
questo ovviamente dovr` essere un le descriptor creato con inotify_init. a anche in questo caso c` un limite massimo che di default ` pari a 8192, ed anche questo valore pu` essere e e o cambiato con sysctl o usando il le /proc/sys/fs/inotify/max_user_watches. 54 i ag IN_DONT_FOLLOW, IN_MASK_ADD e IN_ONLYDIR sono stati introdotti a partire dalle glibc 2.5, se si usa la versione 2.4 ` necessario denirli a mano. e
53 52

11.2. LACCESSO ASINCRONO AI FILE


Valore IN_DONT_FOLLOW IN_MASK_ADD IN_ONESHOT IN_ONLYDIR Signicato Non dereferenzia pathname se questo ` un link simbolico. e Aggiunge a quelli gi` impostati i ag indicati nellargomento mask, a invece di sovrascriverli. Esegue losservazione su pathname per una sola volta, rimuovendolo poi dalla watch list. Se pathname ` una directory riporta soltanto gli eventi ad essa relativi e e non quelli per i le che contiene.

291

Tabella 11.7: Le costanti che identicano i bit della maschera binaria dellargomento mask di inotify_add_watch che indicano le modalit` di osservazione. a

vengono riportati solo gli eventi sui le che essa contiene direttamente, non quelli relativi a le contenuti in eventuali sottodirectory; se si vogliono osservare anche questi sar` necessario creare a ulteriori watch per ciascuna sottodirectory. Inne usando il ag IN_ONESHOT ` possibile richiedere una notica singola;55 una volta e vericatosi uno qualunque fra gli eventi richiesti con inotify_add_watch losservatore verr` a automaticamente rimosso dalla lista di osservazione e nessun ulteriore evento sar` pi` noticato. a u In caso di successo inotify_add_watch ritorna un intero positivo, detto watch descriptor, che identica univocamente un osservatore su una coda di notica; esso viene usato per farvi riferimento sia riguardo i risultati restituiti da inotify, che per la eventuale rimozione dello stesso. La seconda funzione per la gestione delle code di notica, che permette di rimuovere un osservatore, ` inotify_rm_watch, ed il suo prototipo `: e e
#include <sys/inotify.h> int inotify_rm_watch(int fd, uint32_t wd) Rimuove un osservatore da una coda di notica. La funzione restituisce 0 in caso di successo, o 1 in caso di errore, nel qual caso errno assumer` a uno dei valori: EBADF EINVAL non si ` specicato in fd un le descriptor valido. e il valore di wd non ` corretto, o fd non ` associato ad una coda di notica. e e

La funzione rimuove dalla coda di notica identicata dallargomento fd losservatore identicato dal watch descriptor wd;56 in caso di successo della rimozione, contemporaneamente alla cancellazione dellosservatore, sulla coda di notica verr` generato un evento di tipo a IN_IGNORED (vedi tab. 11.8). Si tenga presente che se un le viene cancellato o un lesystem viene smontato i relativi osservatori vengono rimossi automaticamente e non ` necessario utilizzare e inotify_rm_watch. Come accennato linterfaccia di inotify prevede che gli eventi siano noticati come dati presenti in lettura sul le descriptor associato alla coda di notica. Una applicazione pertanto dovr` leggere i dati da detto le con una read, che ritorner` sul buer i dati presenti nella forma a a di una o pi` strutture di tipo inotify_event (la cui denizione ` riportata in g. 11.3). Qualora u e non siano presenti dati la read si bloccher` (a meno di non aver impostato il le descriptor in a modalit` non bloccante) no allarrivo di almeno un evento. a Una ulteriore caratteristica dellinterfaccia di inotify ` che essa permette di ottenere con e ioctl, come per i le descriptor associati ai socket (si veda sez. 17.3.3) il numero di byte disponibili in lettura sul le descriptor, utilizzando su di esso loperazione FIONREAD.57 Si pu` o
questa funzionalit` per` ` disponibile soltanto a partire dal kernel 2.6.16. a oe ovviamente deve essere usato per questo argomento un valore ritornato da inotify_add_watch, altrimenti si avr` un errore di EINVAL. a 57 questa ` una delle operazioni speciali per i le (vedi sez. 6.3.7), che ` disponibile solo per i socket e per i le e e descriptor creati con inotify_init.
56 55

292

CAPITOLO 11. LA GESTIONE AVANZATA DEI FILE

struct inotify_event { int wd ; /* Watch descriptor */ uint32_t mask ; /* Mask of events */ uint32_t cookie ; /* Unique cookie associating related events ( for rename (2)) */ uint32_t len ; /* Size of name field */ char name []; /* Optional null - terminated name */ };

Figura 11.3: La struttura inotify_event usata dallinterfaccia di inotify per riportare gli eventi.

cos` utilizzare questa operazione, oltre che per predisporre una operazione di lettura con un buer di dimensioni adeguate, anche per ottenere rapidamente il numero di le che sono cambiati. Una volta eettuata la lettura con read a ciascun evento sar` associata una struttura a inotify_event contenente i rispettivi dati. Per identicare a quale le o directory levento corrisponde viene restituito nel campo wd il watch descriptor con cui il relativo osservatore ` e stato registrato. Il campo mask contiene invece una maschera di bit che identica il tipo di evento vericatosi; in essa compariranno sia i bit elencati nella prima parte di tab. 11.6, che gli eventuali valori aggiuntivi58 di tab. 11.8.
Valore IN_IGNORED Signicato Losservatore ` stato rimosso, sia in maniera esplicita con luso di e inotify_rm_watch, che in maniera implicita per la rimozione delloggetto osservato o per lo smontaggio del lesystem su cui questo si trova. Levento avvenuto fa riferimento ad una directory (consente cos` di distinguere, quando si pone sotto osservazione una directory, fra gli eventi relativi ad essa e quelli relativi ai le che essa contiene). Si sono eccedute le dimensioni della coda degli eventi (overow della coda); in questo caso il valore di wd ` 1.59 e Il lesystem contenente loggetto posto sotto osservazione ` stato e smontato.

IN_ISDIR

IN_Q_OVERFLOW IN_UNMOUNT

Tabella 11.8: Le costanti che identicano i bit aggiuntivi usati nella maschera binaria del campo mask di inotify_event.

Il campo cookie contiene invece un intero univoco che permette di identicare eventi correlati (per i quali avr` lo stesso valore), al momento viene utilizzato soltanto per rilevare lo a spostamento di un le, consentendo cos` allapplicazione di collegare la corrispondente coppia di eventi IN_MOVED_TO e IN_MOVED_FROM. Inne due campi name e len sono utilizzati soltanto quando levento ` relativo ad un le e presente in una directory posta sotto osservazione, in tal caso essi contengono rispettivamente il nome del le (come pathname relativo alla directory osservata) e la relativa dimensione in byte. Il campo name viene sempre restituito come stringa terminata da NUL, con uno o pi` zeri di u terminazione, a seconda di eventuali necessit` di allineamento del risultato, ed il valore di len a corrisponde al totale della dimensione di name, zeri aggiuntivi compresi. La stringa con il nome del le viene restituita nella lettura subito dopo la struttura inotify_event; questo signica che le dimensioni di ciascun evento di inotify saranno pari a sizeof(inotify_event) + len.
questi compaiono solo nel campo mask di inotify_event, e non utilizzabili in fase di registrazione dellosservatore. 59 la coda di notica ha una dimensione massima specicata dal parametro di sistema /proc/sys/fs/inotify/max_queued_events che indica il numero massimo di eventi che possono essere mantenuti sulla stessa; quando detto valore viene ecceduto gli ulteriori eventi vengono scartati, ma viene comunque generato un evento di tipo IN_Q_OVERFLOW.
58

11.2. LACCESSO ASINCRONO AI FILE

293

Vediamo allora un esempio delluso dellinterfaccia di inotify con un semplice programma che permette di mettere sotto osservazione uno o pi` le e directory. Il programma si chiama u inotify_monitor.c ed il codice completo ` disponibile coi sorgenti allegati alla guida, il corpo e principale del programma, che non contiene la sezione di gestione delle opzioni e le funzioni di ausilio ` riportato in g. 11.4. e Una volta completata la scansione delle opzioni il corpo principale del programma inizia controllando (11-15) che sia rimasto almeno un argomento che indichi quale le o directory mettere sotto osservazione (e qualora questo non avvenga esce stampando la pagina di aiuto); dopo di che passa (16-20) allinizializzazione di inotify ottenendo con inotify_init il relativo le descriptor (oppure usce in caso di errore). Il passo successivo ` aggiungere (21-30) alla coda di notica gli opportuni osservatori per e ciascuno dei le o directory indicati allinvocazione del comando; questo viene fatto eseguendo un ciclo (22-29) ntanto che la variabile i, inizializzata a zero (21) allinizio del ciclo, ` minore e del numero totale di argomenti rimasti. Allinterno del ciclo si invoca (23) inotify_add_watch per ciascuno degli argomenti, usando la maschera degli eventi data dalla variabile mask (il cui valore viene impostato nella scansione delle opzioni), in caso di errore si esce dal programma altrimenti si incrementa lindice (29). Completa linizializzazione di inotify inizia il ciclo principale (32-56) del programma, nel quale si resta in attesa degli eventi che si intendono osservare. Questo viene fatto eseguendo allinizio del ciclo (33) una read che si bloccher` ntanto che non si saranno vericati eventi. a Dato che linterfaccia di inotify pu` riportare anche pi` eventi in una sola lettura, si ` avuto o u e cura di passare alla read un buer di dimensioni adeguate, inizializzato in (7) ad un valore di approssimativamente 512 eventi.60 In caso di errore di lettura (35-40) il programma esce con un messaggio di errore (37-39), a meno che non si tratti di una interruzione della system call, nel qual caso (36) si ripete la lettura. Se la lettura ` andata a buon ne invece si esegue un ciclo (43-52) per leggere tutti gli eventi e restituiti, al solito si inizializza lindice i a zero (42) e si ripetono le operazioni (43) ntanto che esso non supera il numero di byte restituiti in lettura. Per ciascun evento allinterno del ciclo si assegna61 alla variabile event lindirizzo nel buer della corrispondente struttura inotify_event (44), e poi si stampano il numero di watch descriptor (45) ed il le a cui questo fa riferimento (46), ricavato dagli argomenti passati a riga di comando sfruttando il fatto che i watch descriptor vengono assegnati in ordine progressivo crescente a partire da 1. Qualora sia presente il riferimento ad un nome di le associato allevento lo si stampa (47-49); si noti come in questo caso si sia utilizzato il valore del campo event->len e non al fatto che event->name riporti o meno un puntatore nullo.62 Si utilizza poi (50) la funzione printevent, che interpreta il valore del campo event->mask per stampare il tipo di eventi accaduti.63 Inne (51) si provvede ad aggiornare lindice i per farlo puntare allevento successivo. Se adesso usiamo il programma per mettere sotto osservazione una directory, e da un altro terminale eseguiamo il comando ls otterremo qualcosa del tipo di: piccardi@gethen:~/gapil/sources$ ./inotify_monitor -a /home/piccardi/gapil/ Watch descriptor 1 Observed event on /home/piccardi/gapil/ IN_OPEN,
si ricordi che la quantit` di dati restituita da inotify ` variabile a causa della diversa lunghezza del nome del a e le restituito insieme a inotify_event. 61 si noti come si sia eseguito un opportuno casting del puntatore. 62 linterfaccia infatti, qualora il nome non sia presente, non avvalora il campo event->name, che si trover` a a contenere quello che era precedentemente presente nella rispettiva locazione di memoria, nel caso pi` comune il u puntatore al nome di un le osservato in precedenza. 63 per il relativo codice, che non riportiamo in quanto non essenziale alla comprensione dellesempio, si possono utilizzare direttamente i sorgenti allegati alla guida.
60

294

CAPITOLO 11. LA GESTIONE AVANZATA DEI FILE

# include < sys / inotify .h > /* Linux inotify interface */ ... 3 int main ( int argc , char * argv []) 4 { 5 int i , narg , nread ; 6 int fd , wd ; 7 char buffer [512 * ( sizeof ( struct inotify_event ) + 16)]; 8 unsigned int mask =0; 9 struct inotify_event * event ; 10 ... 11 narg = argc - optind ; 12 if ( narg < 1) { /* There must be at least one argument */ 13 printf ( " Wrong number of arguments % d \ n " , argc - optind ); 14 usage (); 15 } 16 fd = inotify_init (); /* initialize inotify */ 17 if ( fd < 0) { 18 perror ( " Failing on inotify_init " ); 19 exit ( -1); 20 } 21 i = 0; 22 while ( i < narg ) { 23 wd = inotify_add_watch ( fd , argv [ optind + i ] , mask ); /* add watch */ 24 if ( wd <= 0) { 25 printf ( " Failing to add watched file %s , mask % i ; % s \ n " , 26 argv [ optind + i ] , mask , strerror ( errno )); 27 exit ( -1); 28 } 29 i ++; 30 } 31 /* Main Loop : read events and print them */ 32 while (1) { 33 nread = read ( fd , buffer , sizeof ( buffer )); 34 if ( nread < 0) { 35 if ( errno == EINTR ) { 36 continue ; 37 } else { 38 perror ( " error reading inotify data " ); 39 exit (1); 40 } 41 } else { 42 i = 0; 43 while ( i < nread ) { 44 event = ( struct inotify_event *) buffer + i ; 45 printf ( " Watch descriptor % i \ n " , event - > wd ); 46 printf ( " Observed event on % s \ n " , argv [ optind -1+ event - > wd ]); 47 if ( event - > len ) { 48 printf ( " On file % s \ n " , event - > name ); 49 } 50 printevent ( event - > mask ); 51 i += sizeof ( struct inotify_event ) + event - > len ; 52 } 53 } 54 } 55 return 0; 56 }
1 2

Figura 11.4: Esempio di codice che usa linterfaccia di inotify.

11.2. LACCESSO ASINCRONO AI FILE Watch descriptor 1 Observed event on /home/piccardi/gapil/ IN_CLOSE_NOWRITE,

295

I lettori pi` accorti si saranno resi conto che nel ciclo di lettura degli eventi appena illustrato u non viene trattato il caso particolare in cui la funzione read restituisce in nread un valore nullo. Lo si ` fatto perch con inotify il ritorno di una read con un valore nullo avviene soltanto, e e come forma di avviso, quando si sia eseguita la funzione specicando un buer di dimensione insuciente a contenere anche un solo evento. Nel nostro caso le dimensioni erano senzaltro sucienti, per cui tale evenienza non si vericher` mai. a Ci si potr` per` chiedere cosa succede se il buer ` suciente per un evento, ma non per a o e tutti gli eventi vericatisi. Come si potr` notare nel codice illustrato in precedenza non si ` presa a e nessuna precauzione per vericare che non ci fossero stati troncamenti dei dati. Anche in questo caso il comportamento scelto ` corretto, perch linterfaccia di inotify garantisce automaticae e mente, anche quando ne sono presenti in numero maggiore, di restituire soltanto il numero di eventi che possono rientrare completamente nelle dimensioni del buer specicato.64 Se gli eventi sono di pi` saranno restituiti solo quelli che entrano interamente nel buer e gli altri saranno u restituiti alla successiva chiamata di read. Inne unultima caratteristica dellinterfaccia di inotify ` che gli eventi restituiti nella lettura e formano una sequenza ordinata, ` cio` garantito che se si esegue uno spostamento di un le gli e e eventi vengano generati nella sequenza corretta. Linterfaccia garantisce anche che se si vericano pi` eventi consecutivi identici (vale a dire con gli stessi valori dei campi wd, mask, cookie, e u name) questi vengono raggruppati in un solo evento.

11.2.3

Linterfaccia POSIX per lI/O asincrono

Una modalit` alternativa alluso dellI/O multiplexing per gestione dellI/O simultaneo su molti a le ` costituita dal cosiddetto I/O asincrono. Il concetto base dellI/O asincrono ` che le funzioni e e di I/O non attendono il completamento delle operazioni prima di ritornare, cos` che il processo non viene bloccato. In questo modo diventa ad esempio possibile eettuare una richiesta preventiva di dati, in modo da poter eettuare in contemporanea le operazioni di calcolo e quelle di I/O. Bench la modalit` di apertura asincrona di un le possa risultare utile in varie occasioni (in e a particolar modo con i socket e gli altri le per i quali le funzioni di I/O sono system call lente), essa ` comunque limitata alla notica della disponibilit` del le descriptor per le operazioni e a di I/O, e non ad uno svolgimento asincrono delle medesime. Lo standard POSIX.1b denisce una interfaccia apposita per lI/O asincrono vero e proprio, che prevede un insieme di funzioni dedicate per la lettura e la scrittura dei le, completamente separate rispetto a quelle usate normalmente. In generale questa interfaccia ` completamente astratta e pu` essere implementata sia die o rettamente nel kernel, che in user space attraverso luso di thread. Per le versioni del kernel meno recenti esiste una implementazione di questa interfaccia fornita delle glibc, che ` realizzata e completamente in user space, ed ` accessibile linkando i programmi con la libreria librt. Nelle e versioni pi` recenti (a partire dalla 2.5.32) ` stato introdotto direttamente nel kernel un nuovo u e layer per lI/O asincrono. Lo standard prevede che tutte le operazioni di I/O asincrono siano controllate attraverso luso di una apposita struttura aiocb (il cui nome sta per asyncronous I/O control block ), che viene passata come argomento a tutte le funzioni dellinterfaccia. La sua denizione, come eettuata
si avr` cio`, facendo riferimento sempre al codice di g. 11.4, che read sar` in genere minore delle dimensioni a e a di buffer ed uguale soltanto qualora gli eventi corrispondano esattamente alle dimensioni di questultimo.
64

296

CAPITOLO 11. LA GESTIONE AVANZATA DEI FILE

in aio.h, ` riportata in g. 11.5. Nello steso le ` denita la macro _POSIX_ASYNCHRONOUS_IO, e e che dichiara la disponibilit` dellinterfaccia per lI/O asincrono. a

struct aiocb { int aio_fildes ; off_t aio_offset ; int aio_lio_opcode ; int aio_reqprio ; volatile void * aio_buf ; size_t aio_nbytes ; struct sigevent aio_sigevent ; };

/* /* /* /* /* /* /*

File descriptor . */ File offset */ Operation to be performed . */ Request priority offset . */ Location of buffer . */ Length of transfer . */ Signal number and value . */

Figura 11.5: La struttura aiocb, usata per il controllo dellI/O asincrono.

Le operazioni di I/O asincrono possono essere eettuate solo su un le gi` aperto; il le a deve inoltre supportare la funzione lseek, pertanto terminali e pipe sono esclusi. Non c` limie te al numero di operazioni contemporanee eettuabili su un singolo le. Ogni operazione deve inizializzare opportunamente un control block. Il le descriptor su cui operare deve essere specicato tramite il campo aio_fildes; dato che pi` operazioni possono essere eseguita in maniera u asincrona, il concetto di posizione corrente sul le viene a mancare; pertanto si deve sempre specicare nel campo aio_offset la posizione sul le da cui i dati saranno letti o scritti. Nel campo aio_buf deve essere specicato lindirizzo del buer usato per lI/O, ed in aio_nbytes la lunghezza del blocco di dati da trasferire. Il campo aio_reqprio permette di impostare la priorit` delle operazioni di I/O.65 La priorit` a a viene impostata a partire da quella del processo chiamante (vedi sez. 3.4), cui viene sottratto il valore di questo campo. Il campo aio_lio_opcode ` usato solo dalla funzione lio_listio, che, e come vedremo, permette di eseguire con una sola chiamata una serie di operazioni, usando un vettore di control block. Tramite questo campo si specica quale ` la natura di ciascuna di esse. e

struct sigevent { sigval_t sigev_value ; int sigev_signo ; int sigev_notify ; void (* sigev_notify_function )( sigval_t ); pthread_attr_t * sigev_notify_attributes ; };

Figura 11.6: La struttura sigevent, usata per specicare le modalit` di notica degli eventi relativi alle a operazioni di I/O asincrono.

Inne il campo aio_sigevent ` una struttura di tipo sigevent che serve a specicare il modo e in cui si vuole che venga eettuata la notica del completamento delle operazioni richieste. La struttura ` riportata in g. 11.6; il campo sigev_notify ` quello che indica le modalit` della e e a notica, esso pu` assumere i tre valori: o SIGEV_NONE
65

Non viene inviata nessuna notica.

in generale perch ci` sia possibile occorre che la piattaforma supporti questa caratteristica, questo viene e o indicato denendo le macro _POSIX_PRIORITIZED_IO, e _POSIX_PRIORITY_SCHEDULING.

11.2. LACCESSO ASINCRONO AI FILE

297

SIGEV_SIGNAL La notica viene eettuata inviando al processo chiamante il segnale specicato da sigev_signo; se il gestore di questo ` stato installato con SA_SIGINFO gli e verr` restituito il valore di sigev_value (la cui denizione ` in g. 9.14) come a e valore del campo si_value di siginfo_t. SIGEV_THREAD La notica viene eettuata creando un nuovo thread che esegue la funzione specicata da sigev_notify_function con argomento sigev_value, e con gli attributi specicati da sigev_notify_attribute. Le due funzioni base dellinterfaccia per lI/O asincrono sono aio_read ed aio_write. Esse permettono di richiedere una lettura od una scrittura asincrona di dati, usando la struttura aiocb appena descritta; i rispettivi prototipi sono:
#include <aio.h> int aio_read(struct aiocb *aiocbp) Richiede una lettura asincrona secondo quanto specicato con aiocbp. int aio_write(struct aiocb *aiocbp) Richiede una scrittura asincrona secondo quanto specicato con aiocbp. Le funzioni restituiscono 0 in caso di successo, e -1 in caso di errore, nel qual caso errno assumer` a uno dei valori: EBADF ENOSYS EINVAL EAGAIN si ` specicato un le descriptor sbagliato. e la funzione non ` implementata. e si ` specicato un valore non valido per i campi aio_offset o aio_reqprio di aiocbp. e la coda delle richieste ` momentaneamente piena. e

Entrambe le funzioni ritornano immediatamente dopo aver messo in coda la richiesta, o in caso di errore. Non ` detto che gli errori EBADF ed EINVAL siano rilevati immediatamente e al momento della chiamata, potrebbero anche emergere nelle fasi successive delle operazioni. Lettura e scrittura avvengono alla posizione indicata da aio_offset, a meno che il le non sia stato aperto in append mode (vedi sez. 6.2.1), nel qual caso le scritture vengono eettuate comunque alla ne de le, nellordine delle chiamate a aio_write. Si tenga inoltre presente che deallocare la memoria indirizzata da aiocbp o modicarne i valori prima della conclusione di una operazione pu` dar luogo a risultati impredicibili, perch o e laccesso ai vari campi per eseguire loperazione pu` avvenire in un momento qualsiasi dopo la o richiesta. Questo comporta che non si devono usare per aiocbp variabili automatiche e che non si deve riutilizzare la stessa struttura per unaltra operazione ntanto che la precedente non sia stata ultimata. In generale per ogni operazione si deve utilizzare una diversa struttura aiocb. Dato che si opera in modalit` asincrona, il successo di aio_read o aio_write non implica a che le operazioni siano state eettivamente eseguite in maniera corretta; per vericarne lesito linterfaccia prevede altre due funzioni, che permettono di controllare lo stato di esecuzione. La prima ` aio_error, che serve a determinare un eventuale stato di errore; il suo prototipo `: e e
#include <aio.h> int aio_error(const struct aiocb *aiocbp) Determina lo stato di errore delle operazioni di I/O associate a aiocbp. La funzione restituisce 0 se le operazioni si sono concluse con successo, altrimenti restituisce il codice di errore relativo al loro fallimento.

Se loperazione non si ` ancora completata viene restituito lerrore di EINPROGRESS. La fune zione ritorna zero quando loperazione si ` conclusa con successo, altrimenti restituisce il codice e dellerrore vericatosi, ed esegue la corrispondente impostazione di errno. Il codice pu` essere o sia EINVAL ed EBADF, dovuti ad un valore errato per aiocbp, che uno degli errori possibili durante lesecuzione delloperazione di I/O richiesta, nel qual caso saranno restituiti, a seconda del caso, i codici di errore delle system call read, write e fsync.

298

CAPITOLO 11. LA GESTIONE AVANZATA DEI FILE

Una volta che si sia certi che le operazioni siano state concluse (cio` dopo che una chiamata ad e aio_error non ha restituito EINPROGRESS), si potr` usare la funzione aio_return, che permette a di vericare il completamento delle operazioni di I/O asincrono; il suo prototipo `: e
#include <aio.h> ssize_t aio_return(const struct aiocb *aiocbp) Recupera il valore dello stato di ritorno delle operazioni di I/O associate a aiocbp. La funzione restituisce lo stato di uscita delloperazione eseguita.

La funzione deve essere chiamata una sola volte per ciascuna operazione asincrona, essa infatti fa s` che il sistema rilasci le risorse ad essa associate. E per questo motivo che occorre chiamare la funzione solo dopo che loperazione cui aiocbp fa riferimento si ` completata. Una e chiamata precedente il completamento delle operazioni darebbe risultati indeterminati. La funzione restituisce il valore di ritorno relativo alloperazione eseguita, cos` come ricavato dalla sottostante system call (il numero di byte letti, scritti o il valore di ritorno di fsync). E importante chiamare sempre questa funzione, altrimenti le risorse disponibili per le operazioni di I/O asincrono non verrebbero liberate, rischiando di arrivare ad un loro esaurimento. Oltre alle operazioni di lettura e scrittura linterfaccia POSIX.1b mette a disposizione unaltra operazione, quella di sincronizzazione dellI/O, compiuta dalla funzione aio_fsync, che ha lo stesso eetto della analoga fsync, ma viene eseguita in maniera asincrona; il suo prototipo `: e
#include <aio.h> int aio_fsync(int op, struct aiocb *aiocbp) Richiede la sincronizzazione dei dati per il le indicato da aiocbp. La funzione restituisce 0 in caso di successo e -1 in caso di errore, che pu` essere, con le stesse o modalit` di aio_read, EAGAIN, EBADF o EINVAL. a

La funzione richiede la sincronizzazione delle operazioni di I/O, ritornando immediatamente. Lesecuzione eettiva della sincronizzazione dovr` essere vericata con aio_error e aio_return a come per le operazioni di lettura e scrittura. Largomento op permette di indicare la modalit` di a esecuzione, se si specica il valore O_DSYNC le operazioni saranno completate con una chiamata a fdatasync, se si specica O_SYNC con una chiamata a fsync (per i dettagli vedi sez. 6.3.3). Il successo della chiamata assicura la sincronizzazione delle operazioni no allora richieste, niente ` garantito riguardo la sincronizzazione dei dati relativi ad eventuali operazioni richieste e successivamente. Se si ` specicato un meccanismo di notica questo sar` innescato una volta e a che le operazioni di sincronizzazione dei dati saranno completate. In alcuni casi pu` essere necessario interrompere le operazioni (in genere quando viene richieo sta unuscita immediata dal programma), per questo lo standard POSIX.1b prevede una funzione apposita, aio_cancel, che permette di cancellare una operazione richiesta in precedenza; il suo prototipo `: e
#include <aio.h> int aio_cancel(int fildes, struct aiocb *aiocbp) Richiede la cancellazione delle operazioni sul le fildes specicate da aiocbp. La funzione restituisce il risultato delloperazione con un codice di positivo, e -1 in caso di errore, che avviene qualora si sia specicato un valore non valido di fildes, imposta errno al valore EBADF.

La funzione permette di cancellare una operazione specica sul le fildes, o tutte le operazioni pendenti, specicando NULL come valore di aiocbp. Quando una operazione viene cancellata una successiva chiamata ad aio_error riporter` ECANCELED come codice di errore, ed il suo coa dice di ritorno sar` -1, inoltre il meccanismo di notica non verr` invocato. Se si specica una a a operazione relativa ad un altro le descriptor il risultato ` indeterminato. In caso di successo, i e possibili valori di ritorno per aio_cancel (anchessi deniti in aio.h) sono tre:

11.2. LACCESSO ASINCRONO AI FILE AIO_ALLDONE

299

indica che le operazioni di cui si ` richiesta la cancellazione sono state gi` e a completate, indica che tutte le operazioni richieste sono state cancellate,

AIO_CANCELED

AIO_NOTCANCELED indica che alcune delle operazioni erano in corso e non sono state cancellate. Nel caso si abbia AIO_NOTCANCELED occorrer` chiamare aio_error per determinare quali a sono le operazioni eettivamente cancellate. Le operazioni che non sono state cancellate proseguiranno il loro corso normale, compreso quanto richiesto riguardo al meccanismo di notica del loro avvenuto completamento. Bench lI/O asincrono preveda un meccanismo di notica, linterfaccia fornisce anche una e apposita funzione, aio_suspend, che permette di sospendere lesecuzione del processo chiamante no al completamento di una specica operazione; il suo prototipo `: e
#include <aio.h> int aio_suspend(const struct aiocb * const list[], int nent, const struct timespec *timeout) Attende, per un massimo di timeout, il completamento di una delle operazioni specicate da list. La funzione restituisce 0 se una (o pi`) operazioni sono state completate, e -1 in caso di errore nel u qual caso errno assumer` uno dei valori: a EAGAIN ENOSYS EINTR nessuna operazione ` stata completata entro timeout. e la funzione non ` implementata. e la funzione ` stata interrotta da un segnale. e

La funzione permette di bloccare il processo ntanto che almeno una delle nent operazioni specicate nella lista list ` completata, per un tempo massimo specicato da timout, o ntanto e che non arrivi un segnale.66 La lista deve essere inizializzata con delle strutture aiocb relative ad operazioni eettivamente richieste, ma pu` contenere puntatori nulli, che saranno ignorati. o In caso si siano specicati valori non validi leetto ` indenito. Un valore NULL per timout e comporta lassenza di timeout. Lo standard POSIX.1b inne ha previsto pure una funzione, lio_listio, che permette di eettuare la richiesta di una intera lista di operazioni di lettura o scrittura; il suo prototipo `: e
#include <aio.h> int lio_listio(int mode, struct aiocb * const list[], int nent, struct sigevent *sig) Richiede lesecuzione delle operazioni di I/O elencata da list, secondo la modalit` mode. a La funzione restituisce 0 in caso di successo, e -1 in caso di errore, nel qual caso errno assumer` a uno dei valori: EAGAIN EINVAL ENOSYS EINTR nessuna operazione ` stata completata entro timeout. e si ` passato un valore di mode non valido o un numero di operazioni nent maggiore di e AIO_LISTIO_MAX. la funzione non ` implementata. e la funzione ` stata interrotta da un segnale. e

La funzione esegue la richiesta delle nent operazioni indicate nella lista list che deve contenere gli indirizzi di altrettanti control block opportunamente inizializzati; in particolare dovr` a essere specicato il tipo di operazione con il campo aio_lio_opcode, che pu` prendere i valori: o LIO_READ
66

si richiede una operazione di lettura.

si tenga conto che questo segnale pu` anche essere quello utilizzato come meccanismo di notica. o

300

CAPITOLO 11. LA GESTIONE AVANZATA DEI FILE

LIO_WRITE si richiede una operazione di scrittura. LIO_NOP non si eettua nessuna operazione.

dove LIO_NOP viene usato quando si ha a che fare con un vettore di dimensione ssa, per poter specicare solo alcune operazioni, o quando si sono dovute cancellare delle operazioni e si deve ripetere la richiesta per quelle non completate. Largomento mode controlla il comportamento della funzione, se viene usato il valore LIO_WAIT la funzione si blocca no al completamento di tutte le operazioni richieste; se si usa LIO_NOWAIT la funzione ritorna immediatamente dopo aver messo in coda tutte le richieste. In tal caso il chiamante pu` richiedere la notica del completamento di tutte le richieste, impostando largomento o sig in maniera analoga a come si fa per il campo aio_sigevent di aiocb.

11.3

Altre modalit` di I/O avanzato a

Oltre alle precedenti modalit` di I/O multiplexing e I/O asincrono, esistono altre funzioni che a implementano delle modalit` di accesso ai le pi` evolute rispetto alle normali funzioni di lettura a u e scrittura che abbiamo esaminato in sez. 6.2. In questa sezione allora prenderemo in esame le interfacce per lI/O mappato in memoria, per lI/O vettorizzato e altre funzioni di I/O avanzato.

11.3.1

File mappati in memoria

Una modalit` alternativa di I/O, che usa una interfaccia completamente diversa rispetto a quella a classica vista in cap. 6, ` il cosiddetto memory-mapped I/O, che, attraverso il meccanismo della e paginazione usato dalla memoria virtuale (vedi sez. 2.2.1), permette di mappare il contenuto di un le in una sezione dello spazio di indirizzi del processo. che lo ha allocato

Figura 11.7: Disposizione della memoria di un processo quando si esegue la mappatura in memoria di un le.

Il meccanismo ` illustrato in g. 11.7, una sezione del le viene mappata direttamente nele lo spazio degli indirizzi del programma. Tutte le operazioni di lettura e scrittura su variabili contenute in questa zona di memoria verranno eseguite leggendo e scrivendo dal contenuto del

` 11.3. ALTRE MODALITA DI I/O AVANZATO

301

le attraverso il sistema della memoria virtuale che in maniera analoga a quanto avviene per le pagine che vengono salvate e rilette nella swap, si incaricher` di sincronizzare il contenuto a di quel segmento di memoria con quello del le mappato su di esso. Per questo motivo si pu` o parlare tanto di le mappato in memoria, quanto di memoria mappata su le. Luso del memory-mapping comporta una notevole semplicazione delle operazioni di I/O, in quanto non sar` pi` necessario utilizzare dei buer intermedi su cui appoggiare i dati da traferire, a u poich questi potranno essere acceduti direttamente nella sezione di memoria mappata; inoltre e questa interfaccia ` pi` eciente delle usuali funzioni di I/O, in quanto permette di caricare in e u memoria solo le parti del le che sono eettivamente usate ad un dato istante. Infatti, dato che laccesso ` fatto direttamente attraverso la memoria virtuale, la sezione di e memoria mappata su cui si opera sar` a sua volta letta o scritta sul le una pagina alla volta e a solo per le parti eettivamente usate, il tutto in maniera completamente trasparente al processo; laccesso alle pagine non ancora caricate avverr` allo stesso modo con cui vengono caricate in a memoria le pagine che sono state salvate sullo swap. Inne in situazioni in cui la memoria ` scarsa, le pagine che mappano un le vengono salvate e automaticamente, cos` come le pagine dei programmi vengono scritte sulla swap; questo consente di accedere ai le su dimensioni il cui solo limite ` quello dello spazio di indirizzi disponibile, e e non della memoria su cui possono esserne lette delle porzioni. Linterfaccia POSIX implementata da Linux prevede varie funzioni per la gestione del memory mapped I/O, la prima di queste, che serve ad eseguire la mappatura in memoria di un le, ` mmap; il suo prototipo `: e e
#include <unistd.h> #include <sys/mman.h> void * mmap(void * start, size_t length, int prot, int flags, int fd, off_t offset) Esegue la mappatura in memoria della sezione specicata del le fd. La funzione restituisce il puntatore alla zona di memoria mappata in caso di successo, e MAP_FAILED (-1) in caso di errore, nel qual caso errno assumer` uno dei valori: a EBADF EACCES il le descriptor non ` valido, e non si ` usato MAP_ANONYMOUS. e e o fd non si riferisce ad un le regolare, o si ` usato MAP_PRIVATE ma fd non ` aperto e e in lettura, o si ` usato MAP_SHARED e impostato PROT_WRITE ed fd non ` aperto in e e lettura/scrittura, o si ` impostato PROT_WRITE ed fd ` in append-only. e e i valori di start, length o offset non sono validi (o troppo grandi o non allineati sulla dimensione delle pagine). si ` impostato MAP_DENYWRITE ma fd ` aperto in scrittura. e e il le ` bloccato, o si ` bloccata troppa memoria rispetto a quanto consentito dai limiti e e di sistema (vedi sez. 8.3.2). non c` memoria o si ` superato il limite sul numero di mappature possibili. e e il lesystem di fd non supporta il memory mapping. largomento prot ha richiesto PROT_EXEC, ma il lesystem di fd ` montato con lopzione e noexec. si ` superato il limite del sistema sul numero di le aperti (vedi sez. 8.3.2). e

EINVAL ETXTBSY EAGAIN ENOMEM ENODEV EPERM ENFILE

La funzione richiede di mappare in memoria la sezione del le fd a partire da offset per lenght byte, preferibilmente allindirizzo start. Il valore di offset deve essere un multiplo della dimensione di una pagina di memoria. Il valore dellargomento prot indica la protezione67 da applicare al segmento di memoria e
in Linux la memoria reale ` divisa in pagine: ogni processo vede la sua memoria attraverso uno o pi` segmenti e u lineari di memoria virtuale. Per ciascuno di questi segmenti il kernel mantiene nella page table la mappatura sulle pagine di memoria reale, ed le modalit` di accesso (lettura, esecuzione, scrittura); una loro violazione causa quella a che si chiama una segment violation, e la relativa emissione del segnale SIGSEGV.
67

302
Valore PROT_EXEC PROT_READ PROT_WRITE PROT_NONE

CAPITOLO 11. LA GESTIONE AVANZATA DEI FILE


Signicato Le pagine possono essere eseguite. Le pagine possono essere lette. Le pagine possono essere scritte. Laccesso alle pagine ` vietato. e

Tabella 11.9: Valori dellargomento prot di mmap, relativi alla protezione applicate alle pagine del le mappate in memoria.

deve essere specicato come maschera binaria ottenuta dallOR di uno o pi` dei valori riportati u in tab. 11.9; il valore specicato deve essere compatibile con la modalit` di accesso con cui si ` a e aperto il le. Largomento flags specica inne qual ` il tipo di oggetto mappato, le opzioni relative alle e modalit` con cui ` eettuata la mappatura e alle modalit` con cui le modiche alla memoria a e a mappata vengono condivise o mantenute private al processo che le ha eettuate. Deve essere specicato come maschera binaria ottenuta dallOR di uno o pi` dei valori riportati in tab. 11.10. u
Valore MAP_FIXED Signicato Non permette di restituire un indirizzo diverso da start, se questo non pu` o essere usato mmap fallisce. Se si imposta questo ag il valore di start deve essere allineato alle dimensioni di una pagina. I cambiamenti sulla memoria mappata vengono riportati sul le e saranno immediatamente visibili agli altri processi che mappano lo stesso le.68 Il le su disco per` non sar` aggiornato no alla chiamata di msync o munmap), e solo o a allora le modiche saranno visibili per lI/O convenzionale. Incompatibile con MAP_PRIVATE. I cambiamenti sulla memoria mappata non vengono riportati sul le. Ne viene fatta una copia privata cui solo il processo chiamante ha accesso. Le modiche sono mantenute attraverso il meccanismo del copy on write e salvate su swap in caso di necessit`. Non ` specicato se i cambiamenti sul le originale vengano a e riportati sulla regione mappata. Incompatibile con MAP_SHARED. In Linux viene ignorato per evitare DoS (veniva usato per segnalare che tentativi di scrittura sul le dovevano fallire con ETXTBSY). Ignorato. Si usa con MAP_PRIVATE. Non riserva delle pagine di swap ad uso del meccanismo del copy on write per mantenere le modiche fatte alla regione mappata, in questo caso dopo una scrittura, se non c` pi` memoria disponibile, si ha e u lemissione di un SIGSEGV. Se impostato impedisce lo swapping delle pagine mappate. Usato per gli stack. Indica che la mappatura deve essere eettuata con gli indirizzi crescenti verso il basso. La mappatura non ` associata a nessun le. Gli argomenti fd e offset sono e ignorati.69 Sinonimo di MAP_ANONYMOUS, deprecato. Valore di compatibilit`, ignorato. a Esegue la mappatura sui primi 2GiB dello spazio degli indirizzi, viene supportato solo sulle piattaforme x86-64 per compatibilit` con le applicazioni a 32 a bit. Viene ignorato se si ` richiesto MAP_FIXED. e Esegue il prefaulting delle pagine di memoria necessarie alla mappatura. Esegue un prefaulting pi` limitato che non causa I/O.70 u Tabella 11.10: Valori possibili dellargomento flag di mmap.

MAP_SHARED

MAP_PRIVATE

MAP_DENYWRITE MAP_EXECUTABLE MAP_NORESERVE

MAP_LOCKED MAP_GROWSDOWN MAP_ANONYMOUS MAP_ANON MAP_FILE MAP_32BIT

MAP_POPULATE MAP_NONBLOCK

Gli eetti dellaccesso ad una zona di memoria mappata su le possono essere piuttosto complessi, essi si possono comprendere solo tenendo presente che tutto quanto ` comunque basato e sul meccanismo della memoria virtuale. Questo comporta allora una serie di conseguenze. La pi` ovvia ` che se si cerca di scrivere su una zona mappata in sola lettura si avr` lemissione u e a di un segnale di violazione di accesso (SIGSEGV), dato che i permessi sul segmento di memoria

` 11.3. ALTRE MODALITA DI I/O AVANZATO

303

relativo non consentono questo tipo di accesso. ` E invece assai diversa la questione relativa agli accessi al di fuori della regione di cui si ` e richiesta la mappatura. A prima vista infatti si potrebbe ritenere che anchessi debbano generare un segnale di violazione di accesso; questo per` non tiene conto del fatto che, essendo basata o sul meccanismo della paginazione , la mappatura in memoria non pu` che essere eseguita su un o segmento di dimensioni rigorosamente multiple di quelle di una pagina, ed in generale queste potranno non corrispondere alle dimensioni eettive del le o della sezione che si vuole mappare.

Figura 11.8: Schema della mappatura in memoria di una sezione di le di dimensioni non corrispondenti al bordo di una pagina.

Il caso pi` comune ` quello illustrato in g. 11.8, in cui la sezione di le non rientra nei u e conni di una pagina: in tal caso verr` il le sar` mappato su un segmento di memoria che si a a estende no al bordo della pagina successiva. In questo caso ` possibile accedere a quella zona di memoria che eccede le dimensioni specie cate da lenght, senza ottenere un SIGSEGV poich essa ` presente nello spazio di indirizzi del e e processo, anche se non ` mappata sul le. Il comportamento del sistema ` quello di restituire un e e valore nullo per quanto viene letto, e di non riportare su le quanto viene scritto. Un caso pi` complesso ` quello che si viene a creare quando le dimensioni del le mappato u e sono pi` corte delle dimensioni della mappatura, oppure quando il le ` stato troncato, dopo u e che ` stato mappato, ad una dimensione inferiore a quella della mappatura in memoria. e In questa situazione, per la sezione di pagina parzialmente coperta dal contenuto del le, vale esattamente quanto visto in precedenza; invece per la parte che eccede, no alle dimensioni date da length, laccesso non sar` pi` possibile, ma il segnale emesso non sar` SIGSEGV, ma a u a SIGBUS, come illustrato in g. 11.9. Non tutti i le possono venire mappati in memoria, dato che, come illustrato in g. 11.7, la mappatura introduce una corrispondenza biunivoca fra una sezione di un le ed una sezione di memoria. Questo comporta che ad esempio non ` possibile mappare in memoria le descriptor e relativi a pipe, socket e fo, per i quali non ha senso parlare di sezione. Lo stesso vale anche per alcuni le di dispositivo, che non dispongono della relativa operazione mmap (si ricordi quanto esposto in sez. 4.2.2). Si tenga presente per` che esistono anche casi di dispositivi (un esempio ` o e linterfaccia al ponte PCI-VME del chip Universe) che sono utilizzabili solo con questa interfaccia.
dato che tutti faranno riferimento alle stesse pagine di memoria. luso di questo ag con MAP_SHARED ` stato implementato in Linux a partire dai kernel della serie 2.4.x; esso e consente di creare segmenti di memoria condivisa e torneremo sul suo utilizzo in sez. 12.3.4. 70 questo ag ed il precedente MAP_POPULATE sono stati introdotti nel kernel 2.5.46 insieme alla mappatura non lineare di cui parleremo pi` avanti. u
69 68

304

CAPITOLO 11. LA GESTIONE AVANZATA DEI FILE

Figura 11.9: Schema della mappatura in memoria di le di dimensioni inferiori alla lunghezza richiesta.

Dato che passando attraverso una fork lo spazio di indirizzi viene copiato integralmente, i le mappati in memoria verranno ereditati in maniera trasparente dal processo glio, mantenendo gli stessi attributi avuti nel padre; cos` se si ` usato MAP_SHARED padre e glio accederanno allo e stesso le in maniera condivisa, mentre se si ` usato MAP_PRIVATE ciascuno di essi manterr` una e a sua versione privata indipendente. Non c` invece nessun passaggio attraverso una exec, dato e che questultima sostituisce tutto lo spazio degli indirizzi di un processo con quello di un nuovo programma. Quando si eettua la mappatura di un le vengono pure modicati i tempi ad esso associati (di cui si ` trattato in sez. 5.2.4). Il valore di st_atime pu` venir cambiato in qualunque istante e o a partire dal momento in cui la mappatura ` stata eettuata: il primo riferimento ad una pagina e mappata su un le aggiorna questo tempo. I valori di st_ctime e st_mtime possono venir cambiati solo quando si ` consentita la scrittura sul le (cio` per un le mappato con PROT_WRITE e e e MAP_SHARED) e sono aggiornati dopo la scrittura o in corrispondenza di una eventuale msync. Dato per i le mappati in memoria le operazioni di I/O sono gestite direttamente dalla memoria virtuale, occorre essere consapevoli delle interazioni che possono esserci con operazioni eettuate con linterfaccia standard dei le di cap. 6. Il problema ` che una volta che si ` mappato e e un le, le operazioni di lettura e scrittura saranno eseguite sulla memoria, e riportate su disco in maniera autonoma dal sistema della memoria virtuale. Pertanto se si modica un le con linterfaccia standard queste modiche potranno essere visibili o meno a seconda del momento in cui la memoria virtuale trasporter` dal disco in memoria a quella sezione del le, perci` ` del tutto imprevedibile il risultato della modica di un le nei oe confronti del contenuto della memoria su cui ` mappato. e Per questo, ` sempre sconsigliabile eseguire scritture su le attraverso linterfaccia standard, e quando lo si ` mappato in memoria, ` invece possibile usare linterfaccia standard per leggere un e e le mappato in memoria, purch si abbia una certa cura; infatti linterfaccia dellI/O mappato e in memoria mette a disposizione la funzione msync per sincronizzare il contenuto della memoria mappata con il le su disco; il suo prototipo `: e

` 11.3. ALTRE MODALITA DI I/O AVANZATO


#include <unistd.h> #include <sys/mman.h> int msync(const void *start, size_t length, int flags) Sincronizza i contenuti di una sezione di un le mappato in memoria. La funzione restituisce 0 in caso di successo, e -1 in caso di errore nel qual caso errno assumer` a uno dei valori: EINVAL EFAULT o start non ` multiplo di PAGE_SIZE, o si ` specicato un valore non valido per flags. e e lintervallo specicato non ricade in una zona precedentemente mappata.

305

La funzione esegue la sincronizzazione di quanto scritto nella sezione di memoria indicata da start e offset, scrivendo le modiche sul le (qualora questo non sia gi` stato fatto). Provvede a anche ad aggiornare i relativi tempi di modica. In questo modo si ` sicuri che dopo lesecuzione e di msync le funzioni dellinterfaccia standard troveranno un contenuto del le aggiornato.
Valore MS_ASYNC MS_SYNC MS_INVALIDATE Signicato Richiede la sincronizzazione. Attende che la sincronizzazione si eseguita. Richiede che le altre mappature dello stesso le siano invalidate.

Tabella 11.11: Le costanti che identicano i bit per la maschera binaria dellargomento flag di msync.

Largomento flag ` specicato come maschera binaria composta da un OR dei valori ripore tati in tab. 11.11, di questi per` MS_ASYNC e MS_SYNC sono incompatibili; con il primo valore o infatti la funzione si limita ad inoltrare la richiesta di sincronizzazione al meccanismo della memoria virtuale, ritornando subito, mentre con il secondo attende che la sincronizzazione sia stata eettivamente eseguita. Il terzo ag fa invalidare le pagine di cui si richiede la sincronizzazione per tutte le mappature dello stesso le, cos` che esse possano essere immediatamente aggiornate ai nuovi valori. Una volta che si sono completate le operazioni di I/O si pu` eliminare la mappatura della o memoria usando la funzione munmap, il suo prototipo `: e

#include <unistd.h> #include <sys/mman.h> int munmap(void *start, size_t length) Rilascia la mappatura sulla sezione di memoria specicata. La funzione restituisce 0 in caso di successo, e -1 in caso di errore nel qual caso errno assumer` a uno dei valori: EINVAL lintervallo specicato non ricade in una zona precedentemente mappata.

La funzione cancella la mappatura per lintervallo specicato con start e length; ogni successivo accesso a tale regione causer` un errore di accesso in memoria. Largomento start a deve essere allineato alle dimensioni di una pagina, e la mappatura di tutte le pagine contenute anche parzialmente nellintervallo indicato, verr` rimossa. Indicare un intervallo che non contiene a mappature non ` un errore. Si tenga presente inoltre che alla conclusione di un processo ogni e pagina mappata verr` automaticamente rilasciata, mentre la chiusura del le descriptor usato a per il memory mapping non ha alcun eetto su di esso. Lo standard POSIX prevede anche una funzione che permetta di cambiare le protezioni delle pagine di memoria; lo standard prevede che essa si applichi solo ai memory mapping creati con mmap, ma nel caso di Linux la funzione pu` essere usata con qualunque pagina valida nella o memoria virtuale. Questa funzione ` mprotect ed il suo prototipo `: e e

306

CAPITOLO 11. LA GESTIONE AVANZATA DEI FILE


#include <sys/mman.h> int mprotect(const void *addr, size_t len, int prot) Modica le protezioni delle pagine di memoria comprese nellintervallo specicato. La funzione restituisce 0 in caso di successo, e -1 in caso di errore nel qual caso errno assumer` a uno dei valori: EINVAL EACCESS il valore di addr non ` valido o non ` un multiplo di PAGE_SIZE. e e loperazione non ` consentita, ad esempio si ` cercato di marcare con PROT_WRITE un e e segmento di memoria cui si ha solo accesso in lettura.

ed inoltre ENOMEM ed EFAULT.

La funzione prende come argomenti un indirizzo di partenza in addr, allineato alle dimensioni delle pagine di memoria, ed una dimensione size. La nuova protezione deve essere specicata in prot con una combinazione dei valori di tab. 11.9. La nuova protezione verr` applicata a tutte a le pagine contenute, anche parzialmente, dallintervallo fra addr e addr+size-1. Inne Linux supporta alcune operazioni speciche non disponibili su altri kernel unix-like. La prima di queste ` la possibilit` di modicare un precedente memory mapping, ad esempio e a per espanderlo o restringerlo. Questo ` realizzato dalla funzione mremap, il cui prototipo `: e e
#include <unistd.h> #include <sys/mman.h> void * mremap(void *old_address, size_t old_size , size_t new_size, unsigned long flags) Restringe o allarga una mappatura in memoria di un le. La funzione restituisce lindirizzo alla nuova area di memoria in caso di successo od il valore MAP_FAILED (pari a (void *) -1) in caso di errore, nel qual caso errno assumer` uno dei valori: a EINVAL EFAULT ENOMEM EAGAIN il valore di old_address non ` un puntatore valido. e ci sono indirizzi non validi nellintervallo specicato da old_address e old_size, o ci sono altre mappature di tipo non corrispondente a quella richiesta. non c` memoria suciente oppure larea di memoria non pu` essere espansa e o allindirizzo virtuale corrente, e non si ` specicato MREMAP_MAYMOVE nei ag. e il segmento di memoria scelto ` bloccato e non pu` essere rimappato. e o

La funzione richiede come argomenti old_address (che deve essere allineato alle dimensioni di una pagina di memoria) che specica il precedente indirizzo del memory mapping e old_size, che ne indica la dimensione. Con new_size si specica invece la nuova dimensione che si vuole ottenere. Inne largomento flags ` una maschera binaria per i ag che controllano il compore tamento della funzione. Il solo valore utilizzato ` MREMAP_MAYMOVE71 che consente di eseguire e lespansione anche quando non ` possibile utilizzare il precedente indirizzo. Per questo motivo, e se si ` usato questo ag, la funzione pu` restituire un indirizzo della nuova zona di memoria che e o non ` detto coincida con old_address. e La funzione si appoggia al sistema della memoria virtuale per modicare lassociazione fra gli indirizzi virtuali del processo e le pagine di memoria, modicando i dati direttamente nella page table del processo. Come per mprotect la funzione pu` essere usata in generale, anche per o pagine di memoria non corrispondenti ad un memory mapping, e consente cos` di implementare la funzione realloc in maniera molto eciente. Una caratteristica comune a tutti i sistemi unix-like ` che la mappatura in memoria di un le e viene eseguita in maniera lineare, cio` parti successive di un le vengono mappate linearmente e su indirizzi successivi in memoria. Esistono per` delle applicazioni72 in cui ` utile poter mappare o e sezioni diverse di un le su diverse zone di memoria. Questo ` ovviamente sempre possibile eseguendo ripetutamente la funzione mmap per ciascuna e
71 72

per poter utilizzare questa costante occorre aver denito _GNU_SOURCE prima di includere sys/mman.h. in particolare la tecnica ` usata dai database o dai programmi che realizzano macchine virtuali. e

` 11.3. ALTRE MODALITA DI I/O AVANZATO

307

delle diverse aree del le che si vogliono mappare in sequenza non lineare,73 ma questo approccio ha delle conseguenze molto pesanti in termini di prestazioni. Infatti per ciascuna mappatura in memoria deve essere denita nella page table del processo una nuova area di memoria virtuale74 che corrisponda alla mappatura, in modo che questa diventi visibile nello spazio degli indirizzi come illustrato in g. 11.7. Quando un processo esegue un gran numero di mappature diverse75 per realizzare a mano una mappatura non-lineare si avr` un accrescimento eccessivo della sua page table, e lo stesso a accadr` per tutti gli altri processi che utilizzano questa tecnica. In situazioni in cui le applicazioni a hanno queste esigenze si avranno delle prestazioni ridotte, dato che il kernel dovr` impiegare a molte risorse76 solo per mantenere i dati di una gran quantit` di memory mapping. a Per questo motivo con il kernel 2.5.46 ` stato introdotto, ad opera di Ingo Molnar, un e meccanismo che consente la mappatura non-lineare. Anche questa ` una caratteristica specica di e Linux, non presente in altri sistemi unix-like. Diventa cos` possibile utilizzare una sola mappatura iniziale77 e poi rimappare a piacere allinterno di questa i dati del le. Ci` ` possibile grazie ad oe una nuova system call, remap_file_pages, il cui prototipo `: e
#include <sys/mman.h> int remap_file_pages(void *start, size_t size, int prot, ssize_t pgoff, int flags) Permette di rimappare non linearmente un precedente memory mapping. La funzione restituisce 0 in caso di successo e 1 in caso di errore, nel qual caso errno assumer` a uno dei valori: EINVAL si ` usato un valore non valido per uno degli argomenti o start non fa riferimento ad e un memory mapping valido creato con MAP_SHARED.

Per poter utilizzare questa funzione occorre anzitutto eettuare preliminarmente una chiamata a mmap con MAP_SHARED per denire larea di memoria che poi sar` rimappata non linearmente. a Poi di chiamer` questa funzione per modicare le corrispondenze fra pagine di memoria e pagine a del le; si tenga presente che remap_file_pages permette anche di mappare la stessa pagina di un le in pi` pagine della regione mappata. u La funzione richiede che si identichi la sezione del le che si vuole riposizionare allinterno del memory mapping con gli argomenti pgoff e size; largomento start invece deve indicare un indirizzo allinterno dellarea denita dallmmap iniziale, a partire dal quale la sezione di le indicata verr` rimappata. Largomento prot deve essere sempre nullo, mentre flags prende gli a stessi valori di mmap (quelli di tab. 11.9) ma di tutti i ag solo MAP_NONBLOCK non viene ignorato. Insieme alla funzione remap_file_pages nel kernel 2.5.46 con sono stati introdotti anche due nuovi ag per mmap: MAP_POPULATE e MAP_NONBLOCK. Il primo dei due consente di abilitare il meccanismo del prefaulting. Questo viene di nuovo in aiuto per migliorare le prestazioni in certe condizioni di utilizzo del memory mapping. Il problema si pone tutte le volte che si vuole mappare in memoria un le di grosse dimensioni. Il comportamento normale del sistema della memoria virtuale ` quello per cui la regione mappata e viene aggiunta alla page table del processo, ma i dati verranno eettivamente utilizzati (si avr` a cio` un page fault che li trasferisce dal disco alla memoria) soltanto in corrispondenza dellaccesso e a ciascuna delle pagine interessate dal memory mapping. Questo vuol dire che il passaggio dei dati dal disco alla memoria avverr` una pagina alla a volta con un gran numero di page fault, chiaramente se si sa in anticipo che il le verr` utilizzato a immediatamente, ` molto pi` eciente eseguire un prefaulting in cui tutte le pagine di memoria e u
ed in eetti ` quello che veniva fatto anche con Linux prima che fossero introdotte queste estensioni. e quella che nel gergo del kernel viene chiamata VMA (virtual memory area). 75 si pu` arrivare anche a centinaia di migliaia. o 76 sia in termini di memoria interna per i dati delle page table, che di CPU per il loro aggiornamento. 77 e quindi una sola virtual memory area nella page table del processo.
74 73

308

CAPITOLO 11. LA GESTIONE AVANZATA DEI FILE

interessate alla mappatura vengono popolate in una sola volta, questo comportamento viene abilitato quando si usa con mmap il ag MAP_POPULATE. Dato che luso di MAP_POPULATE comporta dellI/O su disco che pu` rallentare lesecuzione o di mmap ` stato introdotto anche un secondo ag, MAP_NONBLOCK, che esegue un prefaulting pi` e u limitato in cui vengono popolate solo le pagine della mappatura che gi` si trovano nella cache a del kernel.78

11.3.2

I/O vettorizzato: readv e writev

Un caso abbastanza comune ` quello in cui ci si trova a dover eseguire una serie multipla di e operazioni di I/O, come una serie di letture o scritture di vari buer. Un esempio tipico ` e quando i dati sono strutturati nei campi di una struttura ed essi devono essere caricati o salvati su un le. Bench loperazione sia facilmente eseguibile attraverso una serie multipla di chiamate, e ci sono casi in cui si vuole poter contare sulla atomicit` delle operazioni. a Per questo motivo su BSD 4.2 sono state introdotte due nuove system call, readv e writev,79 che permettono di eettuare con una sola chiamata una lettura o una scrittura su una serie di buer (quello che viene chiamato I/O vettorizzato. I relativi prototipi sono:
#include <sys/uio.h> int readv(int fd, const struct iovec *vector, int count) int writev(int fd, const struct iovec *vector, int count) Eseguono rispettivamente una lettura o una scrittura vettorizzata. Le funzioni restituiscono il numero di byte letti o scritti in caso di successo, e -1 in caso di errore, nel qual caso errno assumer` uno dei valori: a EINVAL EINTR EAGAIN si ` specicato un valore non valido per uno degli argomenti (ad esempio count ` e e maggiore di IOV_MAX). la funzione ` stata interrotta da un segnale prima di di avere eseguito una qualunque e lettura o scrittura. fd ` stato aperto in modalit` non bloccante e non ci sono dati in lettura. e a

EOPNOTSUPP la coda delle richieste ` momentaneamente piena. e ed anche EISDIR, EBADF, ENOMEM, EFAULT (se non sono stati allocati correttamente i buer specicati nei campi iov_base), pi` gli eventuali errori delle funzioni di lettura e scrittura eseguite su fd. u

Entrambe le funzioni usano una struttura iovec, la cui denizione ` riportata in g. 11.10, e che denisce dove i dati devono essere letti o scritti ed in che quantit`. Il primo campo della a struttura, iov_base, contiene lindirizzo del buer ed il secondo, iov_len, la dimensione dello stesso.
struct iovec { __ptr_t iov_base ; size_t iov_len ; };

/* Starting address */ /* Length in bytes */

Figura 11.10: La struttura iovec, usata dalle operazioni di I/O vettorizzato.

La lista dei buer da utilizzare viene indicata attraverso largomento vector che ` un vettore e di strutture iovec, la cui lunghezza ` specicata dallargomento count.80 Ciascuna struttura e dovr` essere inizializzata opportunamente per indicare i vari buer da e verso i quali verr` a a
questo pu` essere utile per il linker dinamico, in particolare quando viene eettuato il prelink delle applicazioni. o in Linux le due funzioni sono riprese da BSD4.4, esse sono previste anche dallo standard POSIX.1-2001. 80 no alle libc5, Linux usava size_t come tipo dellargomento count, una scelta logica, che per` ` stata dismessa oe per restare aderenti allo standard POSIX.1-2001.
79 78

` 11.3. ALTRE MODALITA DI I/O AVANZATO

309

eseguito il trasferimento dei dati. Essi verranno letti (o scritti) nellordine in cui li si sono specicati nel vettore vector. La standardizzazione delle due funzioni allinterno della revisione POSIX.1-2001 prevede anche che sia possibile avere un limite al numero di elementi del vettore vector. Qualora questo sussista, esso deve essere indicato dal valore dalla costante IOV_MAX, denita come le altre costanti analoghe (vedi sez. 8.1.1) in limits.h; lo stesso valore deve essere ottenibile in esecuzione tramite la funzione sysconf richiedendo largomento _SC_IOV_MAX (vedi sez. 8.1.2). Nel caso di Linux il limite di sistema ` di 1024, per` se si usano le glibc queste forniscono e o un wrapper per le system call che si accorge se una operazione superer` il precedente limite, in a tal caso i dati verranno letti o scritti con le usuali read e write usando un buer di dimensioni sucienti appositamente allocato e suciente a contenere tutti i dati indicati da vector. Loperazione avr` successo ma si perder` latomicit` del trasferimento da e verso la destinazione a a a nale.

11.3.3

LI/O diretto fra le descriptor: sendfile e splice

Uno dei problemi che si presentano nella gestione dellI/O ` quello in cui si devono trasferire e grandi quantit` di dati da un le descriptor ed un altro; questo usualmente comporta la lettura a dei dati dal primo le descriptor in un buer in memoria, da cui essi vengono poi scritti sul secondo. Bench il kernel ottimizzi la gestione di questo processo quando si ha a che fare con le e normali, in generale quando i dati da trasferire sono molti si pone il problema di eettuare trasferimenti di grandi quantit` di dati da kernel space a user space e allindietro, quando a in realt` potrebbe essere pi` eciente mantenere tutto in kernel space. Tratteremo in questa a u sezione alcune funzioni specialistiche che permettono di ottimizzare le prestazioni in questo tipo di situazioni. La prima funzione che si pone lobiettivo di ottimizzare il trasferimento dei dati fra due le descriptor ` sendfile;81 la funzione ` presente in diverse versioni di Unix,82 ma non ` presente n e e e e 83 per cui per essa vengono utilizzati prototipi e semantiche in POSIX.1-2001 n in altri standard, e dierenti; nel caso di Linux il suo prototipo `: e
#include <sys/sendfile.h> ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count) Copia dei dati da un le descriptor ad un altro. La funzione restituisce il numero di byte trasferiti in caso di successo e 1 in caso di errore, nel qual caso errno assumer` uno dei valori: a EAGAIN EINVAL EIO ENOMEM si ` impostata la modalit` non bloccante su out_fd e la scrittura si bloccherebbe. e a i le descriptor non sono validi, o sono bloccati (vedi sez. 11.4), o mmap non ` disponibile e per in_fd. si ` avuto un errore di lettura da in_fd. e non c` memoria suciente per la lettura da in_fd. e

ed inoltre EBADF e EFAULT.

La funzione copia direttamente count byte dal le descriptor in_fd al le descriptor out_fd; in caso di successo funzione ritorna il numero di byte eettivamente copiati da in_fd a out_fd o 1 in caso di errore, come le ordinarie read e write questo valore pu` essere inferiore a quanto o richiesto con count. Se il puntatore offset ` nullo la funzione legge i dati a partire dalla posizione corrente e su in_fd, altrimenti verr` usata la posizione indicata dal valore puntato da offset; in questo a
la funzione ` stata introdotta con i kernel della serie 2.2, e disponibile dalle glibc 2.1. e la si ritrova ad esempio in FreeBSD, HPUX ed altri Unix. 83 pertanto si eviti di utilizzarla se si devono scrivere programmi portabili.
82 81

310

CAPITOLO 11. LA GESTIONE AVANZATA DEI FILE

caso detto valore sar` aggiornato, come value result argument, per indicare la posizione del byte a successivo allultimo che ` stato letto, mentre la posizione corrente sul le non sar` modicata. e a Se invece offset ` nullo la posizione corrente sul le sar` aggiornata tenendo conto dei byte e a letti da in_fd. Fino ai kernel della serie 2.4 la funzione ` utilizzabile su un qualunque le descriptor, e e permette di sostituire la invocazione successiva di una read e una write (e lallocazione del relativo buer) con una sola chiamata a sendfile. In questo modo si pu` diminuire il numero di o chiamate al sistema e risparmiare in trasferimenti di dati da kernel space a user space e viceversa. La massima utilit` della funzione si ha comunque per il trasferimento di dati da un le su disco a ad un socket di rete,84 dato che in questo caso diventa possibile eettuare il trasferimento diretto via DMA dal controller del disco alla scheda di rete, senza neanche allocare un buer nel kernel,85 ottenendo la massima ecienza possibile senza pesare neanche sul processore. In seguito per` ci si ` accorti che, fatta eccezione per il trasferimento diretto da le a socket, o e non sempre sendfile comportava miglioramenti signicativi delle prestazioni rispetto alluso in sequenza di read e write,86 e che anzi in certi casi si potevano avere anche dei peggioramenti. Questo ha portato, per i kernel della serie 2.6,87 alla decisione di consentire luso della funzione soltanto quando il le da cui si legge supporta le operazioni di memory mapping (vale a dire non ` un socket) e quello su cui si scrive ` un socket; in tutti gli altri casi luso di sendfile dar` e e a luogo ad un errore di EINVAL. Nonostante ci possano essere casi in cui sendfile non migliora le prestazioni, le motivazioni addotte non convincono del tutto e resta il dubbio se la scelta di disabilitarla sempre per il trasferimento di dati fra le di dati sia davvero corretta. Se ci sono peggioramenti di prestazioni infatti si pu` sempre fare ricorso alluso successivo di, ma lasciare a disposizione la funzione o consentirebbe se non altro, anche in assenza di guadagni di prestazioni, di semplicare la gestione della copia dei dati fra le, evitando di dover gestire lallocazione di un buer temporaneo per il loro trasferimento; inoltre si avrebbe comunque il vantaggio di evitare inutili trasferimenti di dati da kernel space a user space e viceversa. Questo dubbio si pu` comunque ritenere superato con lintroduzione, avvenuto a partire dal o kernel 2.6.17, della nuova system call splice. Lo scopo di questa funzione ` quello di fornire e un meccanismo generico per il trasferimento di dati da o verso un le utilizzando un buer gestito internamente dal kernel. Descritta in questi termini splice sembra semplicemente un dimezzamento di sendfile.88 In realt` le due system call sono profondamente diverse nel loro a 89 sendfile infatti, come accennato, non necessita di avere a meccanismo di funzionamento; disposizione un buer interno, perch esegue un trasferimento diretto di dati; questo la rende e in generale pi` eciente, ma anche limitata nelle sue applicazioni, dato che questo tipo di u trasferimento ` possibile solo in casi specici.90 e Il concetto che sta dietro a splice invece ` diverso,91 si tratta semplicemente di una funzione e
84 questo ` il caso classico del lavoro eseguito da un server web, ed infatti Apache ha una opzione per il supporto e esplicito di questa funzione. 85 il meccanismo ` detto zerocopy in quanto i dati non vengono mai copiati dal kernel, che si limita a programmare e solo le operazioni di lettura e scrittura via DMA. 86 nel caso generico infatti il kernel deve comunque allocare un buer ed eettuare la copia dei dati, e in tal caso spesso il guadagno ottenibile nel ridurre il numero di chiamate al sistema non compensa le ottimizzazioni che possono essere fatte da una applicazione in user space che ha una conoscenza diretta su come questi sono strutturati. 87 per alcune motivazioni di questa scelta si pu` fare riferimento a quanto illustrato da Linus Torvalds in o http://www.cs.helsinki.fi/linux/linux-kernel/2001-03/0200.html. 88 nel senso che un trasferimento di dati fra due le con sendfile non sarebbe altro che la lettura degli stessi su un buer seguita dalla relativa scrittura, cosa che in questo caso si dovrebbe eseguire con due chiamate a splice. 89 questo no al kernel 2.6.23, dove sendfile ` stata reimplementata in termini di splice, pur mantenendo e disponibile la stessa interfaccia verso luser space. 90 e nel caso di Linux questi sono anche solo quelli in cui essa pu` essere eettivamente utilizzata. o 91 in realt` la proposta originale di Larry Mc Voy non dierisce poi tanto negli scopi da sendfile, quela

` 11.3. ALTRE MODALITA DI I/O AVANZATO

311

che consente di fare in maniera del tutto generica delle operazioni di trasferimento di dati fra un le e un buer gestito interamente in kernel space. In questo caso il cuore della funzione (e delle ani vmsplice e tee, che tratteremo pi` avanti) ` appunto luso di un buer in kernel u e space, e questo ` anche quello che ne ha semplicato ladozione, perch linfrastruttura per la e e gestione di un tale buer ` presente n dagli albori di Unix per la realizzazione delle pipe (vedi e sez. 12.1). Dal punto di vista concettuale allora splice non ` altro che una diversa interfaccia e (rispetto alle pipe) con cui utilizzare in user space loggetto buer in kernel space. Cos` se per una pipe o una fo il buer viene utilizzato come area di memoria (vedi g. 12.1) dove appoggiare i dati che vengono trasferiti da un capo allaltro della stessa per creare un meccanismo di comunicazione fra processi, nel caso di splice il buer viene usato o come fonte dei dati che saranno scritti su un le, o come destinazione dei dati che vengono letti da un le. La funzione splice fornisce quindi una interfaccia generica che consente di trasferire dati da un buer ad un le o viceversa; il suo prototipo, accessibile solo dopo aver denito la macro _GNU_SOURCE,92 ` il seguente: e
#include <fcntl.h> long splice(int fd_in, off_t *off_in, int fd_out, off_t *off_out, size_t len, unsigned int flags) Trasferisce dati da un le verso una pipe o viceversa. La funzione restituisce il numero di byte trasferiti in caso di successo e 1 in caso di errore, nel qual caso errno assumer` uno dei valori: a EBADF EINVAL uno o entrambi fra fd_in e fd_out non sono le descriptor validi o, rispettivamente, non sono stati aperti in lettura o scrittura. il lesystem su cui si opera non supporta splice, oppure nessuno dei le descriptor ` e una pipe, oppure si ` dato un valore a off_in o off_out ma il corrispondente le ` e e un dispositivo che non supporta la funzione seek. non c` memoria suciente per loperazione richiesta. e o off_in o off_out non sono NULL ma il corrispondente le descriptor ` una pipe. e

ENOMEM ESPIPE

La funzione esegue un trasferimento di len byte dal le descriptor fd_in al le descriptor fd_out, uno dei quali deve essere una pipe; laltro le descriptor pu` essere qualunque.93 Come o accennato una pipe non ` altro che un buer in kernel space, per cui a seconda che essa sia usata e per fd_in o fd_out si avr` rispettivamente la copia dei dati dal buer al le o viceversa. a In caso di successo la funzione ritorna il numero di byte trasferiti, che pu` essere, come per o le normali funzioni di lettura e scrittura su le, inferiore a quelli richiesti; un valore negativo indicher` un errore mentre un valore nullo indicher` che non ci sono dati da trasferire (ad a a esempio si ` giunti alla ne del le in lettura). Si tenga presente che, a seconda del verso del e trasferimento dei dati, la funzione si comporta nei confronti del le descriptor che fa riferimento al le ordinario, come read o write, e pertanto potr` anche bloccarsi (a meno che non si sia a aperto il suddetto le in modalit` non bloccante). a I due argomenti off_in e off_out consentono di specicare, come per lanalogo offset di sendfile, la posizione allinterno del le da cui partire per il trasferimento dei dati. Come per sendfile un valore nullo indica di usare la posizione corrente sul le, ed essa sar` aggiornata a automaticamente secondo il numero di byte trasferiti. Un valore non nullo invece deve essere un puntatore ad una variabile intera che indica la posizione da usare; questa verr` aggiornata, al a ritorno della funzione, al byte successivo allultimo byte trasferito. Ovviamente soltanto uno di
lo che rende splice davvero diversa ` stata la reinterpretazione che ne ` stata fatta nellimplementazione e e su Linux realizzata da Jens Anxboe, concetti che sono esposti sinteticamente dallo stesso Linus Torvalds in http://kerneltrap.org/node/6505. 92 si ricordi che questa funzione non ` contemplata da nessuno standard, ` presente solo su Linux, e pertanto e e deve essere evitata se si vogliono scrivere programmi portabili. 93 questo signica che pu` essere, oltre che un le di dati, anche un altra pipe, o un socket. o

312

CAPITOLO 11. LA GESTIONE AVANZATA DEI FILE

questi due argomenti, e pi` precisamente quello che fa riferimento al le descriptor non associato u alla pipe, pu` essere specicato come valore non nullo. o Inne largomento flags consente di controllare alcune caratteristiche del funzionamento della funzione; il contenuto ` una maschera binaria e deve essere specicato come OR aritmetico e dei valori riportati in tab. 11.12. Alcuni di questi valori vengono utilizzati anche dalle funzioni vmsplice e tee per cui la tabella riporta le descrizioni complete di tutti i valori possibili anche quando, come per SPLICE_F_GIFT, questi non hanno eetto su splice.
Valore SPLICE_F_MOVE SPLICE_F_NONBLOCK Signicato Suggerisce al kernel di spostare le pagine di memoria contenenti i dati invece di copiarle;94 viene usato soltanto da splice. Richiede di operare in modalit` non bloccante; questo ag inuisce a solo sulle operazioni che riguardano lI/O da e verso la pipe. Nel caso di splice questo signica che la funzione potr` comunque bloccarsi a nellaccesso agli altri le descriptor (a meno che anchessi non siano stati aperti in modalit` non bloccante). a Indica al kernel che ci sar` linvio di ulteriori dati in una splice suca cessiva, questo ` un suggerimento utile che viene usato quando fd_out e ` un socket.95 Attualmente viene usato solo da splice, potr` essere e a implementato in futuro anche per vmsplice e tee. Le pagine di memoria utente sono donate al kernel;96 se impostato una seguente splice che usa SPLICE_F_MOVE potr` spostare le pagine con a successo, altrimenti esse dovranno essere copiate; per usare questa opzione i dati dovranno essere opportunamente allineati in posizione ed in dimensione alle pagine di memoria. Viene usato soltanto da vmsplice.

SPLICE_F_MORE

SPLICE_F_GIFT

Tabella 11.12: Le costanti che identicano i bit della maschera binaria dellargomento flags di splice, vmsplice e tee.

Per capire meglio il funzionamento di splice vediamo un esempio con un semplice programma che usa questa funzione per eettuare la copia di un le su un altro senza utilizzare buer in user space. Il programma si chiama splicecp.c ed il codice completo ` disponibile coi sorgenti e allegati alla guida, il corpo principale del programma, che non contiene la sezione di gestione delle opzioni e le funzioni di ausilio ` riportato in g. 11.12. e Lo scopo del programma ` quello di eseguire la copia dei con splice, questo signica che si e dovr` usare la funzione due volte, prima per leggere i dati e poi per scriverli, appoggiandosi ad a un buer in kernel space (vale a dire ad una pipe); lo schema del usso dei dati ` illustrato in e g. 11.11. Una volta trattate le opzioni il programma verica che restino (13-16) i due argomenti che indicano il le sorgente ed il le destinazione. Il passo successivo ` aprire il le sorgente (18-22), e quello di destinazione (23-27) ed inne (28-31) la pipe che verr` usata come buer. a Il ciclo principale (33-58) inizia con la lettura dal le sorgente tramite la prima splice (3435), in questo caso si ` usato come primo argomento il le descriptor del le sorgente e come e terzo quello del capo in scrittura della pipe (il funzionamento delle pipe e luso della coppia di le descriptor ad esse associati ` trattato in dettaglio in sez. 12.1; non ne parleremo qui dato e che nellottica delluso di splice questa operazione corrisponde semplicemente al trasferimento dei dati dal le al buer). La lettura viene eseguita in blocchi pari alla dimensione specicata dallopzione -s (il default
per una maggiore ecienza splice usa quando possibile i meccanismi della memoria virtuale per eseguire i trasferimenti di dati (in maniera analoga a mmap), qualora le pagine non possano essere spostate dalla pipe o il buer non corrisponda a pagine intere esse saranno comunque copiate. 96 questa opzione consente di utilizzare delle opzioni di gestione dei socket che permettono di ottimizzare le trasmissioni via rete, si veda la descrizione di TCP_CORK in sez. 17.2.5 e quella di MSG_MORE in sez. 19.1.1. 96 questo signica che la cache delle pagine e i dati su disco potranno dierire, e che lapplicazione non potr` a modicare questarea di memoria.
96

` 11.3. ALTRE MODALITA DI I/O AVANZATO

313

Figura 11.11: Struttura del usso di dati usato dal programma splicecp.

` 4096); essendo in questo caso splice equivalente ad una read sul le, se ne controlla il valore di e uscita in nread che indica quanti byte sono stati letti, se detto valore ` nullo (36) questo signica e che si ` giunti alla ne del le sorgente e pertanto loperazione di copia ` conclusa e si pu` uscire e e o dal ciclo arrivando alla conclusione del programma (59). In caso di valore negativo (37-44) c` e stato un errore ed allora si ripete la lettura (36) se questo ` dovuto ad una interruzione, o e altrimenti si esce con un messaggio di errore (41-43). Una volta completata con successo la lettura si avvia il ciclo di scrittura (45-57); questo inizia (46-47) con la seconda splice che cerca di scrivere gli nread byte letti, si noti come in questo caso il primo argomento faccia di nuovo riferimento alla pipe (in questo caso si usa il capo in lettura, per i dettagli si veda al solito sez. 12.1) mentre il terzo sia il le descriptor del le di destinazione. Di nuovo si controlla il numero di byte eettivamente scritti restituito in nwrite e in caso di errore al solito si ripete la scrittura se questo ` dovuto a una interruzione o si esce con un e messaggio negli altri casi (48-55). Inne si chiude il ciclo di scrittura sottraendo (57) il numero di byte scritti a quelli di cui ` richiesta la scrittura,97 cos` che il ciclo di scrittura venga ripetuto e ntanto che il valore risultante sia maggiore di zero, indice che la chiamata a splice non ha esaurito tutti i dati presenti sul buer. Si noti come il programma sia concettualmente identico a quello che si sarebbe scritto usando read al posto della prima splice e write al posto della seconda, utilizzando un buer in user space per eseguire la copia dei dati, solo che in questo caso non ` stato necessario allocare nessun e buer e non si ` trasferito nessun dato in user space. e Si noti anche come si sia usata la combinazione SPLICE_F_MOVE | SPLICE_F_MORE per largomento flags di splice, infatti anche se un valore nullo avrebbe dato gli stessi risultati, luso di questi ag, che si ricordi servono solo a dare suggerimenti al kernel, permette in genere di migliorare le prestazioni. Come accennato con lintroduzione di splice sono state realizzate altre due system call, vmsplice e tee, che utilizzano la stessa infrastruttura e si basano sullo stesso concetto di manipolazione e trasferimento di dati attraverso un buer in kernel space; bench queste non e attengono strettamente ad operazioni di trasferimento dati fra le descriptor, le tratteremo qui. La prima funzione, vmsplice, ` la pi` simile a splice e come indica il suo nome consente e u di trasferire i dati dalla memoria di un processo verso una pipe, il suo prototipo `: e
in questa parte del ciclo nread, il cui valore iniziale ` dato dai byte letti dalla precedente chiamata a splice, e viene ad assumere il signicato di byte da scrivere.
97

314

CAPITOLO 11. LA GESTIONE AVANZATA DEI FILE

# define _GNU_SOURCE # include < fcntl .h > 3 ...


1 2 4 5 6

/* file control functions */

int main ( int argc , char * argv []) { 7 int size = 4096; 8 int pipefd [2]; 9 int in_fd , out_fd ; 10 int nread , nwrite ; 11 ... 12 /* Main body */ 13 if (( argc - optind ) != 2) { /* There must two argument */ 14 printf ( " Wrong number of arguments % d \ n " , argc - optind ); 15 usage (); 16 } 17 /* open pipe , input and output file */ 18 in_fd = open ( argv [ optind ] , O_RDONLY ); 19 if ( in_fd < 0) { 20 printf ( " Input error % s on % s \ n " , strerror ( errno ) , argv [ optind ]); 21 exit ( EXIT_FAILURE ); 22 } 23 out_fd = open ( argv [ optind +1] , O_CREAT | O_RDWR | O_TRUNC , 0644); 24 if ( out_fd < 0) { 25 printf ( " Cannot open %s , error % s \ n " , argv [ optind +1] , strerror ( errno )); 26 exit ( EXIT_FAILURE ); 27 } 28 if ( pipe ( pipefd ) == -1) { 29 perror ( " Cannot create buffer pipe " ); 30 exit ( EXIT_FAILURE ); 31 } 32 /* copy loop */ 33 while (1) { 34 nread = splice ( in_fd , NULL , pipefd [1] , NULL , size , 35 SPLICE_F_MOVE | SPLICE_F_MORE ); 36 if ( nread == 0) break ; 37 if ( nread < 0) { 38 if ( errno == EINTR ) { 39 continue ; 40 } else { 41 perror ( " read error " ); 42 exit ( EXIT_FAILURE ); 43 } 44 } 45 while ( nread > 0) { 46 nwrite = splice ( pipefd [0] , NULL , out_fd , NULL , nread , 47 SPLICE_F_MOVE | SPLICE_F_MORE ); 48 if ( nwrite < 0) { 49 if ( errno == EINTR ) 50 continue ; 51 else { 52 perror ( " write error " ); 53 exit ( EXIT_FAILURE ); 54 } 55 } 56 nread -= nwrite ; 57 } 58 } 59 return EXIT_SUCCESS ; 60 }

Figura 11.12: Esempio di codice che usa splice per eettuare la copia di un le.

` 11.3. ALTRE MODALITA DI I/O AVANZATO


#include <fcntl.h> #include <sys/uio.h> long vmsplice(int fd, const struct iovec *iov, unsigned long nr_segs, unsigned int flags) Trasferisce dati dalla memoria di un processo verso una pipe. La funzione restituisce il numero di byte trasferiti in caso di successo e 1 in caso di errore, nel qual caso errno assumer` uno dei valori: a EBADF EINVAL ENOMEM o fd non ` un le descriptor valido o non fa riferimento ad una pipe. e si ` usato un valore nullo per nr_segs oppure si ` usato SPLICE_F_GIFT ma la memoria e e non ` allineata. e non c` memoria suciente per loperazione richiesta. e

315

La pipe dovr` essere specicata tramite il le descriptor corrispondente al suo capo aperto in a scrittura (di nuovo si faccia riferimento a sez. 12.1), mentre per indicare quali zone di memoria devono essere trasferita si deve utilizzare un vettore di strutture iovec (vedi g. 11.10), con le stesse con cui le si usano per lI/O vettorizzato; le dimensioni del suddetto vettore devono essere passate nellargomento nr_segs che indica il numero di segmenti di memoria da trasferire. Sia per il vettore che per il valore massimo di nr_segs valgono le stesse limitazioni illustrate in sez. 11.3.2. In caso di successo la funzione ritorna il numero di byte trasferiti sulla pipe, in generale (se i dati una volta creati non devono essere riutilizzati) ` opportuno utilizzare il ag SPLICE_F_GIFT; e questo fa si che il kernel possa rimuovere le relative pagine dallo spazio degli indirizzi del processo, e scaricarle nella cache, cos` che queste possono essere utilizzate immediatamente senza necessit` a di eseguire una copia dei dati che contengono. La seconda funzione aggiunta insieme a splice ` tee, che deve il suo nome allomonimo e comando in user space, perch in analogia con questo permette di duplicare i dati in ingresso e su una pipe su unaltra pipe. In sostanza, sempre nellottica della manipolazione dei dati su dei buer in kernel space, la funzione consente di eseguire una copia del contenuto del buer stesso. Il prototipo di tee ` il seguente: e
#include <fcntl.h> long tee(int fd_in, int fd_out, size_t len, unsigned int flags) Duplica len byte da una pipe ad unaltra. La funzione restituisce il numero di byte copiati in caso di successo e 1 in caso di errore, nel qual caso errno assumer` uno dei valori: a EINVAL ENOMEM o uno fra fd_in e fd_out non fa riferimento ad una pipe o entrambi fanno riferimento alla stessa pipe. non c` memoria suciente per loperazione richiesta. e

La funzione copia len byte del contenuto di una pipe su di unaltra; fd_in deve essere il capo in lettura della pipe sorgente e fd_out il capo in scrittura della pipe destinazione; a dierenza di quanto avviene con read i dati letti con tee da fd_in non vengono consumati e restano disponibili sulla pipe per una successiva lettura (di nuovo per il comportamento delle pipe si veda sez. 12.1). La funzione restituisce il numero di byte copiati da una pipe allaltra (o 1 in caso di errore), un valore nullo indica che non ci sono byte disponibili da copiare e che il capo in scrittura della pipe ` stato chiuso.98 Un esempio di realizzazione del comando tee usando questa funzione, e ripreso da quello fornito nella pagina di manuale e dallesempio allegato al patch originale, ` e riportato in g. 11.13. Il programma consente di copiare il contenuto dello standard input sullo
si tenga presente per` che questo non avviene se si ` impostato il ag SPLICE_F_NONBLOCK, in tal caso infatti o e si avrebbe un errore di EAGAIN.
98

316

CAPITOLO 11. LA GESTIONE AVANZATA DEI FILE

standard output e su un le specicato come argomento, il codice completo si trova nel le tee.c dei sorgenti allegati alla guida. La prima parte del programma (10-35) si cura semplicemente di controllare (11-14) che sia stato fornito almeno un argomento (il nome del le su cui scrivere), di aprirlo (1519) e che sia lo standard input (20-27) che lo standard output (28-35) corrispondano ad una pipe. Il ciclo principale (37-58) inizia con la chiamata a tee che duplica il contenuto dello standard input sullo standard output (39), questa parte ` del tutto analoga ad una lettura ed infatti come e nellesempio di g. 11.12 si controlla il valore di ritorno della funzione in len; se questo ` nullo e signica che non ci sono pi` dati da leggere e si chiude il ciclo (40), se ` negativo c` stato un u e e errore, ed allora si ripete la chiamata se questo ` dovuto ad una interruzione (42-44) o si stampa e un messaggio di errore e si esce negli altri casi (44-47). Una volta completata la copia dei dati sullo standard output si possono estrarre dalla standard input e scrivere sul le, di nuovo su usa un ciclo di scrittura (50-58) in cui si ripete una chiamata a splice (51) ntanto che non si sono scritti tutti i len byte copiati in precedenza con tee (il funzionamento ` identico allanalogo ciclo di scrittura del precedente esempio di e g. 11.12). Inne una nota nale riguardo splice, vmsplice e tee: occorre sottolineare che bench e nora si sia parlato di trasferimenti o copie di dati in realt` nella implementazione di queste a system call non ` aatto detto che i dati vengono eettivamente spostati o copiati, il kernel infatti e realizza le pipe come un insieme di puntatori99 alle pagine di memoria interna che contengono i dati, per questo una volta che i dati sono presenti nella memoria del kernel tutto quello che viene fatto ` creare i suddetti puntatori ed aumentare il numero di referenze; questo signica che e anche con tee non viene mai copiato nessun byte, vengono semplicemente copiati i puntatori.

11.3.4

Gestione avanzata dellaccesso ai dati dei le

Nelluso generico dellinterfaccia per laccesso al contenuto dei le le operazioni di lettura e scrittura non necessitano di nessun intervento di supervisione da parte dei programmi, si eseguir` a una read o una write, i dati verranno passati al kernel che provveder` ad eettuare tutte le a operazioni (e a gestire il caching dei dati) per portarle a termine in quello che ritiene essere il modo pi` eciente. u Il problema ` che il concetto di migliore ecienza impiegato dal kernel ` relativo alluso e e generico, mentre esistono molti casi in cui ci sono esigenze speciche dei singoli programmi, che avendo una conoscenza diretta di come verranno usati i le, possono necessitare di eettuare delle ottimizzazioni speciche, relative alle proprie modalit` di I/O sugli stessi. Tratteremo in a questa sezione una serie funzioni che consentono ai programmi di ottimizzare il loro accesso ai dati dei le e controllare la gestione del relativo caching. Una prima funzione che pu` essere utilizzata per modicare la gestione ordinaria dellI/O o 100 che consente di richiedere una lettura anticipata del contenuto dello su un le ` readahead, e stesso in cache, cos` che le seguenti operazioni di lettura non debbano subire il ritardo dovuto allaccesso al disco; il suo prototipo `: e

per essere precisi si tratta di un semplice buer circolare, un buon articolo sul tema si trova su http://lwn.net/Articles/118750/. 100 questa ` una funzione specica di Linux, introdotta con il kernel 2.4.13, e non deve essere usata se si vogliono e scrivere programmi portabili.

99

` 11.3. ALTRE MODALITA DI I/O AVANZATO

317

# define _GNU_SOURCE # include < fcntl .h > /* file control functions */ 3 ... 4 int main ( int argc , char * argv []) 5 { 6 size_t size = 4096; 7 int fd , len , nwrite ; 8 struct stat fdata ; 9 ... 10 /* check argument , open destination file and check stdin and stdout */ 11 if (( argc - optind ) != 1) { /* There must be one argument */ 12 printf ( " Wrong number of arguments % d \ n " , argc - optind ); 13 usage (); 14 } 15 fd = open ( argv [1] , O_WRONLY | O_CREAT | O_TRUNC , 0644); 16 if ( fd == -1) { 17 printf ( " opening file % s falied : % s " , argv [1] , strerror ( errno )); 18 exit ( EXIT_FAILURE ); 19 } 20 if ( fstat ( STDIN_FILENO , & fdata ) < 0) { 21 perror ( " cannot stat stdin " ); 22 exit ( EXIT_FAILURE ); 23 } 24 if (! S_ISFIFO ( fdata . st_mode )) { 25 fprintf ( stderr , " stdin must be a pipe \ n " ); 26 exit ( EXIT_FAILURE ); 27 } 28 if ( fstat ( STDOUT_FILENO , & fdata ) < 0) { 29 perror ( " cannot stat stdout " ); 30 exit ( EXIT_FAILURE ); 31 } 32 if (! S_ISFIFO ( fdata . st_mode )) { 33 fprintf ( stderr , " stdout must be a pipe \ n " ); 34 exit ( EXIT_FAILURE ); 35 } 36 /* tee loop */ 37 while (1) { 38 /* copy stdin to stdout */ 39 len = tee ( STDIN_FILENO , STDOUT_FILENO , size , 0); 40 if ( len == 0) break ; 41 if ( len < 0) { 42 if ( errno == EAGAIN ) { 43 continue ; 44 } else { 45 perror ( " error on tee stdin to stdout " ); 46 exit ( EXIT_FAILURE ); 47 } 48 } 49 /* write data to the file using splice */ 50 while ( len > 0) { 51 nwrite = splice ( STDIN_FILENO , NULL , fd , NULL , len , SPLICE_F_MOVE ); 52 if ( nwrite < 0) { 53 perror ( " error on splice stdin to file " ); 54 break ; 55 } 56 len -= nwrite ; 57 } 58 } 59 exit ( EXIT_SUCCESS ); 60 }
1 2

Figura 11.13: Esempio di codice che usa tee per copiare i dati dello standard input sullo standard output e su un le.

318

CAPITOLO 11. LA GESTIONE AVANZATA DEI FILE


#include <fcntl.h> ssize_t readahead(int fd, off64_t *offset, size_t count) Esegue una lettura preventiva del contenuto di un le in cache. La funzione restituisce 0 in caso di successo e 1 in caso di errore, nel qual caso errno assumer` a uno dei valori: EBADF EINVAL largomento fd non ` un le descriptor valido o non ` aperto in lettura. e e largomento fd si riferisce ad un tipo di le che non supporta loperazione (come una pipe o un socket).

La funzione richiede che venga letto in anticipo il contenuto del le fd a partire dalla posizione offset e per un ammontare di count byte, in modo da portarlo in cache. La funzione usa la memoria virtuale ed il meccanismo della paginazione per cui la lettura viene eseguita in blocchi corrispondenti alle dimensioni delle pagine di memoria, ed i valori di offset e count arrotondati di conseguenza. La funzione estende quello che ` un comportamento normale del kernel101 eettuando la e lettura in cache della sezione richiesta e bloccandosi ntanto che questa non viene completata. La posizione corrente sul le non viene modicata ed indipendentemente da quanto indicato con count la lettura dei dati si interrompe una volta raggiunta la ne del le. Si pu` utilizzare questa funzione per velocizzare le operazioni di lettura allinterno del proo gramma tutte le volte che si conosce in anticipo quanti dati saranno necessari in seguito. Si potr` a cos` concentrare in un unico momento (ad esempio in fase di inizializzazione) la lettura, cos` da ottenere una migliore risposta nelle operazioni successive. Il concetto di readahead viene generalizzato nello standard POSIX.1-2001 dalla funzione posix_fadvise,102 che consente di avvisare il kernel sulle modalit` con cui si intende accedere a nel futuro ad una certa porzione di un le,103 cos` che esso possa provvedere le opportune ottimizzazioni; il suo prototipo, che pu` ` disponibile solo se si denisce la macro _XOPEN_SOURCE oe ad almeno 600, `: e
#include <fcntl.h> int posix_fadvise(int fd, off_t offset, off_t len, int advice) Dichiara al kernel le future modalit` di accesso ad un le. a La funzione restituisce 0 in caso di successo e 1 in caso di errore, nel qual caso errno assumer` a uno dei valori: EBADF EINVAL ESPIPE largomento fd non ` un le descriptor valido. e il valore di advice non ` valido o fd si riferisce ad un tipo di le che non supporta e loperazione (come una pipe o un socket). previsto dallo standard se fd ` una pipe o un socket (ma su Linux viene restituito e EINVAL).

La funzione dichiara al kernel le modalit` con cui intende accedere alla regione del le indicato a da fd che inizia alla posizione offset e si estende per len byte. Se per len si usa un valore nullo la regione coperta sar` da offset alla ne del le.104 Le modalit` sono indicate dallargomento a a advice che ` una maschera binaria dei valori illustrati in tab. 11.13. Si tenga presente comunque e che la funzione d` soltanto un avvertimento, non esiste nessun vincolo per il kernel, che utilizza a semplicemente linformazione. Anche posix_fadvise si appoggia al sistema della memoria virtuale ed al meccanismo standard del readahead utilizzato dal kernel; in particolare con POSIX_FADV_SEQUENTIAL si raddoppia
per ottimizzare gli accessi al disco il kernel quando si legge un le, aspettandosi che laccesso prosegua, esegue sempre una lettura anticipata di una certa quantit` di dati; questo meccanismo viene chiamato readahead, da cui a deriva il nome della funzione. 102 anche se largomento len ` stato modicato da size_t a off_t nella revisione POSIX.1-2003 TC5. e 103 la funzione per` ` stata introdotta su Linux solo a partire dal kernel 2.5.60. oe 104 questo ` vero solo per le versioni pi` recenti, no al kernel 2.6.6 il valore nullo veniva interpretato letteralmente. e u
101

` 11.3. ALTRE MODALITA DI I/O AVANZATO


Valore POSIX_FADV_NORMAL Signicato Non ci sono avvisi specici da fare riguardo le modalit` di accesso, a il comportamento sar` identico a quello che si avrebbe senza nessun a avviso. Lapplicazione si aspetta di accedere di accedere ai dati specicati in maniera sequenziale, a partire dalle posizioni pi` basse. u I dati saranno letti in maniera completamente causale. I dati saranno acceduti una sola volta. I dati saranno acceduti a breve. I dati non saranno acceduti a breve.

319

POSIX_FADV_SEQUENTIAL POSIX_FADV_RANDOM POSIX_FADV_NOREUSE POSIX_FADV_WILLNEED POSIX_FADV_DONTNEED

Tabella 11.13: Valori dei bit dellargomento advice di posix_fadvise che indicano la modalit` con cui si intende a accedere ad un le.

la dimensione dellammontare di dati letti preventivamente rispetto al default, aspettandosi appunto una lettura sequenziale che li utilizzer`, mentre con POSIX_FADV_RANDOM si disabilita del a tutto il suddetto meccanismo, dato che con un accesso del tutto casuale ` inutile mettersi a e leggere i dati immediatamente successivi gli attuali; inne luso di POSIX_FADV_NORMAL consente di riportarsi al comportamento di default. Le due modalit` POSIX_FADV_NOREUSE e POSIX_FADV_WILLNEED danno invece inizio ad una a lettura in cache della regione del le indicata. La quantit` di dati che verranno letti ` ovviamente a e limitata in base al carico che si viene a creare sul sistema della memoria virtuale, ma in genere una lettura di qualche megabyte viene sempre soddisfatta (ed un valore superiore ` solo raramente e di qualche utilit`). In particolare luso di POSIX_FADV_WILLNEED si pu` considerare lequivalente a o POSIX di readahead. Inne con POSIX_FADV_DONTNEED si dice al kernel di liberare le pagine di cache occupate dai dati presenti nella regione di le indicata. Questa ` una indicazione utile che permette di e alleggerire il carico sulla cache, ed un programma pu` utilizzare periodicamente questa funzione o per liberare pagine di memoria da dati che non sono pi` utilizzati per far posto a nuovi dati u 105 utili. Sia posix_fadvise che readahead attengono alla ottimizzazione dellaccesso in lettura; lo standard POSIX.1-2001 prevede anche una funzione specica per le operazioni di scrittura, posix_fallocate,106 che consente di preallocare dello spazio disco per assicurarsi che una seguente scrittura non fallisca, il suo prototipo, anchesso disponibile solo se si denisce la macro _XOPEN_SOURCE ad almeno 600, `: e
#include <fcntl.h> int posix_fallocate(int fd, off_t offset, off_t len) Richiede la allocazione di spazio disco per un le. La funzione restituisce 0 in caso di successo e direttamente un codice di errore, in caso di fallimento, in questo caso errno non viene impostata, ma sar` restituito direttamente uno dei valori: a EBADF EINVAL EFBIG ENODEV ENOSPC ESPIPE largomento fd non ` un le descriptor valido o non ` aperto in scrittura. e e o offset o len sono minori di zero. il valore di (offset + len) eccede la dimensione massima consentita per un le. largomento fd non fa riferimento ad un le regolare. non c` suciente spazio disco per eseguire loperazione. e largomento fd ` una pipe. e

La funzione si assicura che venga allocato suciente spazio disco perch sia possibile scrivere e sul le indicato dallargomento fd nella regione che inizia dalla posizione offset e si estende
la pagina di manuale riporta lesempio dello streaming di le di grosse dimensioni, dove le pagine occupate dai dati gi` inviati possono essere tranquillamente scartate. a 106 la funzione ` stata introdotta a partire dalle glibc 2.1.94. e
105

320

CAPITOLO 11. LA GESTIONE AVANZATA DEI FILE

per len byte; se questa si estende oltre la ne del le le dimensioni di questultimo saranno incrementate di conseguenza. Dopo aver eseguito con successo la funzione ` garantito che una e scrittura nella regione indicata non fallir` per mancanza di spazio disco. a

11.4

Il le locking

In sez. 6.3.1 abbiamo preso in esame le modalit` in cui un sistema unix-like gestisce la condivia sione dei le da parte di processi diversi. In quelloccasione si ` visto come, con leccezione dei e le aperti in append mode, quando pi` processi scrivono contemporaneamente sullo stesso le u non ` possibile determinare la sequenza in cui essi opereranno. e Questo causa la possibilit` di una race condition; in generale le situazioni pi` comuni sono a u due: linterazione fra un processo che scrive e altri che leggono, in cui questi ultimi possono leggere informazioni scritte solo in maniera parziale o incompleta; o quella in cui diversi processi scrivono, mescolando in maniera imprevedibile il loro output sul le. In tutti questi casi il le locking ` la tecnica che permette di evitare le race condition, e attraverso una serie di funzioni che permettono di bloccare laccesso al le da parte di altri processi, cos` da evitare le sovrapposizioni, e garantire la atomicit` delle operazioni di scrittura. a

11.4.1

Ladvisory locking

La prima modalit` di le locking che ` stata implementata nei sistemi unix-like ` quella che viene a e e usualmente chiamata advisory locking,107 in quanto sono i singoli processi, e non il sistema, che si incaricano di asserire e vericare se esistono delle condizioni di blocco per laccesso ai le. Questo signica che le funzioni read o write vengono eseguite comunque e non risentono aatto della presenza di un eventuale lock ; pertanto ` sempre compito dei vari processi che intendono usare e il le locking, controllare esplicitamente lo stato dei le condivisi prima di accedervi, utilizzando le relative funzioni. In generale si distinguono due tipologie di le lock :108 la prima ` il cosiddetto shared lock, e detto anche read lock in quanto serve a bloccare laccesso in scrittura su un le anch il suo e contenuto non venga modicato mentre lo si legge. Si parla appunto di blocco condiviso in quanto pi` processi possono richiedere contemporaneamente uno shared lock su un le per proteggere il u loro accesso in lettura. La seconda tipologia ` il cosiddetto exclusive lock, detto anche write lock in quanto serve e a bloccare laccesso su un le (sia in lettura che in scrittura) da parte di altri processi mentre lo si sta scrivendo. Si parla di blocco esclusivo appunto perch un solo processo alla volta pu` e o richiedere un exclusive lock su un le per proteggere il suo accesso in scrittura.
Richiesta Read lock Write lock Stato del le Nessun lock Read lock Write lock SI SI NO SI NO NO

Tabella 11.14: Tipologie di le locking. Stevens in [1] fa riferimento a questo argomento come al record locking, dizione utilizzata anche dal manuale delle glibc; nelle pagine di manuale si parla di discrectionary le lock per fcntl e di advisory locking per flock, mentre questo nome viene usato da Stevens per riferirsi al le locking POSIX. Dato che la dizione record locking ` quantomeno ambigua, in quanto in un sistema Unix non esiste niente che possa fare riferimento al concetto di e record, alla ne si ` scelto di mantenere il nome advisory locking. e 108 di seguito ci riferiremo sempre ai blocchi di accesso ai le con la nomenclatura inglese di le lock, o pi` u brevemente con lock, per evitare confusioni linguistiche con il blocco di un processo (cio` la condizione in cui il e processo viene posto in stato di sleep).
107

11.4. IL FILE LOCKING

321

In Linux sono disponibili due interfacce per utilizzare ladvisory locking, la prima ` quella e derivata da BSD, che ` basata sulla funzione flock, la seconda ` quella standardizzata da e e POSIX.1 (derivata da System V), che ` basata sulla funzione fcntl. I le lock sono implementati e in maniera completamente indipendente nelle due interfacce, che pertanto possono coesistere senza interferenze. Entrambe le interfacce prevedono la stessa procedura di funzionamento: si inizia sempre con il richiedere lopportuno le lock (un exclusive lock per una scrittura, uno shared lock per una lettura) prima di eseguire laccesso ad un le. Se il lock viene acquisito il processo prosegue lesecuzione, altrimenti (a meno di non aver richiesto un comportamento non bloccante) viene posto in stato di sleep. Una volta nite le operazioni sul le si deve provvedere a rimuovere il lock. La situazione delle varie possibilit` ` riassunta in tab. 11.14, dove si sono riportati, per le ae varie tipologie di lock presenti su un le, il risultato che si ha in corrispondenza alle due tipologie di le lock menzionate, nel successo della richiesta. Si tenga presente inne che il controllo di accesso e la gestione dei permessi viene eettuata quando si apre un le, lunico controllo residuo che si pu` avere riguardo il le locking ` che il o e tipo di lock che si vuole ottenere su un le deve essere compatibile con le modalit` di apertura a dello stesso (in lettura per un read lock e in scrittura per un write lock).

11.4.2

La funzione flock

La prima interfaccia per il le locking, quella derivata da BSD, permette di eseguire un blocco solo su un intero le; la funzione usata per richiedere e rimuovere un le lock ` flock, ed il suo e prototipo `: e
#include <sys/file.h> int flock(int fd, int operation) Applica o rimuove un le lock sul le fd. La funzione restituisce 0 in caso di successo, e -1 in caso di errore, nel qual caso errno assumer` a uno dei valori: EWOULDBLOCK il le ha gi` un blocco attivo, e si ` specicato LOCK_NB. a e

La funzione pu` essere usata per acquisire o rilasciare un le lock a seconda di quanto o specicato tramite il valore dellargomento operation, questo viene interpretato come maschera binaria, e deve essere passato utilizzando le costanti riportate in tab. 11.15.
Valore LOCK_SH LOCK_EX LOCK_UN LOCK_NB Signicato Asserisce uno shared lock sul le. Asserisce un esclusive lock sul le. Rilascia il le lock. Impedisce che la funzione si blocchi nella richiesta di un le lock. Tabella 11.15: Valori dellargomento operation di flock.

I primi due valori, LOCK_SH e LOCK_EX permettono di richiedere un le lock, ed ovviamente devono essere usati in maniera alternativa. Se si specica anche LOCK_NB la funzione non si bloccher` qualora il lock non possa essere acquisito, ma ritorner` subito con un errore di a a EWOULDBLOCK. Per rilasciare un lock si dovr` invece usare LOCK_UN. a La semantica del le locking di BSD ` diversa da quella del le locking POSIX, in particolare e per quanto riguarda il comportamento dei lock nei confronti delle due funzioni dup e fork. Per capire queste dierenze occorre descrivere con maggiore dettaglio come viene realizzato il le locking nel kernel in entrambe le interfacce. In g. 11.14 si ` riportato uno schema essenziale dellimplementazione del le locking in e stile BSD in Linux; il punto fondamentale da capire ` che un lock, qualunque sia linterfaccia e

322

CAPITOLO 11. LA GESTIONE AVANZATA DEI FILE

che si usa, anche se richiesto attraverso un le descriptor, agisce sempre su un le; perci` le o informazioni relative agli eventuali le lock sono mantenute a livello di inode,109 dato che questo ` lunico riferimento in comune che possono avere due processi diversi che aprono lo stesso le. e

Figura 11.14: Schema dellarchitettura del le locking, nel caso particolare del suo utilizzo da parte dalla funzione flock.

La richiesta di un le lock prevede una scansione della lista per determinare se lacquisizione ` possibile, ed in caso positivo laggiunta di un nuovo elemento.110 Nel caso dei lock creati con e flock la semantica della funzione prevede che sia dup che fork non creino ulteriori istanze di un le lock quanto piuttosto degli ulteriori riferimenti allo stesso. Questo viene realizzato dal kernel secondo lo schema di g. 11.14, associando ad ogni nuovo le lock un puntatore111 alla voce nella le table da cui si ` richiesto il lock, che cos` ne identica il titolare. e Questa struttura prevede che, quando si richiede la rimozione di un le lock, il kernel acconsenta solo se la richiesta proviene da un le descriptor che fa riferimento ad una voce nella le table corrispondente a quella registrata nel lock. Allora se ricordiamo quanto visto in sez. 6.3.4 e sez. 6.3.1, e cio` che i le descriptor duplicati e quelli ereditati in un processo glio puntano e sempre alla stessa voce nella le table, si pu` capire immediatamente quali sono le conseguenze o nei confronti delle funzioni dup e fork. Sar` cos` possibile rimuovere un le lock attraverso uno qualunque dei le descriptor che a fanno riferimento alla stessa voce nella le table, anche se questo ` diverso da quello con cui lo si e 112 o se si esegue la rimozione in un processo glio; inoltre una volta tolto un le lock, ` creato, e
109 in particolare, come accennato in g. 11.14, i le lock sono mantenuti in una linked list di strutture file_lock. La lista ` referenziata dallindirizzo di partenza mantenuto dal campo i_flock della struttura inode (per le e denizioni esatte si faccia riferimento al le fs.h nei sorgenti del kernel). Un bit del campo fl_flags di specica se si tratta di un lock in semantica BSD (FL_FLOCK) o POSIX (FL_POSIX). 110 cio` una nuova struttura file_lock. e 111 il puntatore ` mantenuto nel campo fl_file di file_lock, e viene utilizzato solo per i lock creati con la e semantica BSD. 112 attenzione, questo non vale se il le descriptor fa riferimento allo stesso le, ma attraverso una voce diversa della le table, come accade tutte le volte che si apre pi` volte lo stesso le. u

11.4. IL FILE LOCKING

323

la rimozione avr` eetto su tutti i le descriptor che condividono la stessa voce nella le table, a e quindi, nel caso di le descriptor ereditati attraverso una fork, anche su processi diversi. Inne, per evitare che la terminazione imprevista di un processo lasci attivi dei le lock, quando un le viene chiuso il kernel provveda anche a rimuovere tutti i lock ad esso associati. Anche in questo caso occorre tenere presente cosa succede quando si hanno le descriptor duplicati; in tal caso infatti il le non verr` eettivamente chiuso (ed il lock rimosso) ntanto che non a viene rilasciata la relativa voce nella le table; e questo avverr` solo quando tutti i le descriptor a che fanno riferimento alla stessa voce sono stati chiusi. Quindi, nel caso ci siano duplicati o processi gli che mantengono ancora aperto un le descriptor, il lock non viene rilasciato. Si tenga presente inne che flock non ` in grado di funzionare per i le mantenuti su NFS, e in questo caso, se si ha la necessit` di eseguire il le locking, occorre usare linterfaccia basata a su fcntl che pu` funzionare anche attraverso NFS, a condizione che sia il client che il server o supportino questa funzionalit`. a

11.4.3

Il le locking POSIX

La seconda interfaccia per ladvisory locking disponibile in Linux ` quella standardizzata da e POSIX, basata sulla funzione fcntl. Abbiamo gi` trattato questa funzione nelle sue molteplici a possibilit` di utilizzo in sez. 6.3.6. Quando la si impiega per il le locking essa viene usata solo a secondo il prototipo:
#include <fcntl.h> int fcntl(int fd, int cmd, struct flock *lock) Applica o rimuove un le lock sul le fd. La funzione restituisce 0 in caso di successo, e -1 in caso di errore, nel qual caso errno assumer` a uno dei valori: EACCES ENOLCK EDEADLK loperazione ` proibita per la presenza di le lock da parte di altri processi. e il sistema non ha le risorse per il locking: ci sono troppi segmenti di lock aperti, si ` e esaurita la tabella dei lock, o il protocollo per il locking remoto ` fallito. e si ` richiesto un lock su una regione bloccata da un altro processo che ` a sua volta in e e attesa dello sblocco di un lock mantenuto dal processo corrente; si avrebbe pertanto un deadlock. Non ` garantito che il sistema riconosca sempre questa situazione. e la funzione ` stata interrotta da un segnale prima di poter acquisire un lock. e

EINTR

ed inoltre EBADF, EFAULT.

Al contrario di quanto avviene con linterfaccia basata su flock con fcntl ` possibile bloce care anche delle singole sezioni di un le, no al singolo byte. Inoltre la funzione permette di ottenere alcune informazioni relative agli eventuali lock preesistenti. Per poter fare tutto questo la funzione utilizza come terzo argomento una apposita struttura flock (la cui denizione ` e riportata in g. 11.15) nella quale inserire tutti i dati relativi ad un determinato lock. Si tenga presente poi che un lock fa sempre riferimento ad una regione, per cui si potr` avere un conitto a anche se c` soltanto una sovrapposizione parziale con unaltra regione bloccata. e
struct flock { short int l_type ; short int l_whence ; off_t l_start ; off_t l_len ; pid_t l_pid ; };

/* /* /* /* /*

Type of lock : F_RDLCK , F_WRLCK , or F_UNLCK . */ Where l_start is relative to ( like lseek ). */ Offset where the lock begins . */ Size of the locked area ; zero means until EOF . */ Process holding the lock . */

Figura 11.15: La struttura flock, usata da fcntl per il le locking.

324

CAPITOLO 11. LA GESTIONE AVANZATA DEI FILE

I primi tre campi della struttura, l_whence, l_start e l_len, servono a specicare la sezione del le a cui fa riferimento il lock: l_start specica il byte di partenza, l_len la lunghezza della sezione e inne l_whence imposta il riferimento da cui contare l_start. Il valore di l_whence segue la stessa semantica dellomonimo argomento di lseek, coi tre possibili valori SEEK_SET, SEEK_CUR e SEEK_END, (si vedano le relative descrizioni in sez. 6.2.3). Si tenga presente che un lock pu` essere richiesto anche per una regione al di l` della corrente o a ne del le, cos` che una eventuale estensione dello stesso resti coperta dal blocco. Inoltre se si specica un valore nullo per l_len il blocco si considera esteso no alla dimensione massima del le; in questo modo ` possibile bloccare una qualunque regione a partire da un certo punto no e alla ne del le, coprendo automaticamente quanto eventualmente aggiunto in coda allo stesso.
Valore F_RDLCK F_WRLCK F_UNLCK Signicato Richiede un blocco condiviso (read lock ). Richiede un blocco esclusivo (write lock ). Richiede leliminazione di un le lock.

Tabella 11.16: Valori possibili per il campo l_type di flock.

Il tipo di le lock richiesto viene specicato dal campo l_type, esso pu` assumere i tre valori o deniti dalle costanti riportate in tab. 11.16, che permettono di richiedere rispettivamente uno shared lock, un esclusive lock, e la rimozione di un lock precedentemente acquisito. Inne il campo l_pid viene usato solo in caso di lettura, quando si chiama fcntl con F_GETLK, e riporta il pid del processo che detiene il lock. Oltre a quanto richiesto tramite i campi di flock, loperazione eettivamente svolta dalla funzione ` stabilita dal valore dallargomento cmd che, come gi` riportato in sez. 6.3.6, specica e a lazione da compiere; i valori relativi al le locking sono tre: F_GETLK verica se il le lock specicato dalla struttura puntata da lock pu` essere acquisito: o in caso negativo sovrascrive la struttura flock con i valori relativi al lock gi` a esistente che ne blocca lacquisizione, altrimenti si limita a impostarne il campo l_type con il valore F_UNLCK. se il campo l_type della struttura puntata da lock ` F_RDLCK o F_WRLCK richiede il e corrispondente le lock, se ` F_UNLCK lo rilascia. Nel caso la richiesta non possa ese sere soddisfatta a causa di un lock preesistente la funzione ritorna immediatamente con un errore di EACCES o di EAGAIN. ` identica a F_SETLK, ma se la richiesta di non pu` essere soddisfatta per la presenza e o di un altro lock, mette il processo in stato di attesa ntanto che il lock precedente non viene rilasciato. Se lattesa viene interrotta da un segnale la funzione ritorna con un errore di EINTR.

F_SETLK

F_SETLKW

Si noti che per quanto detto il comando F_GETLK non serve a rilevare una presenza generica di lock su un le, perch se ne esistono altri compatibili con quello richiesto, la funzione ritorna e comunque impostando l_type a F_UNLCK. Inoltre a seconda del valore di l_type si potr` cona trollare o lesistenza di un qualunque tipo di lock (se ` F_WRLCK) o di write lock (se ` F_RDLCK). Si e e consideri poi che pu` esserci pi` di un lock che impedisce lacquisizione di quello richiesto (basta o u che le regioni si sovrappongano), ma la funzione ne riporter` sempre soltanto uno, impostando a l_whence a SEEK_SET ed i valori l_start e l_len per indicare quale ` la regione bloccata. e Inne si tenga presente che eettuare un controllo con il comando F_GETLK e poi tentare lacquisizione con F_SETLK non ` una operazione atomica (un altro processo potrebbe acquisire e

11.4. IL FILE LOCKING

325

un lock fra le due chiamate) per cui si deve sempre vericare il codice di ritorno di fcntl113 quando la si invoca con F_SETLK, per controllare che il lock sia stato eettivamente acquisito.

Figura 11.16: Schema di una situazione di deadlock.

Non operando a livello di interi le, il le locking POSIX introduce unulteriore complicazione; consideriamo la situazione illustrata in g. 11.16, in cui il processo A blocca la regione 1 e il processo B la regione 2. Supponiamo che successivamente il processo A richieda un lock sulla regione 2 che non pu` essere acquisito per il preesistente lock del processo 2; il processo 1 si o bloccher` ntanto che il processo 2 non rilasci il blocco. Ma cosa accade se il processo 2 nel a frattempo tenta a sua volta di ottenere un lock sulla regione A? Questa ` una tipica situazione e che porta ad un deadlock, dato che a quel punto anche il processo 2 si bloccherebbe, e niente potrebbe sbloccare laltro processo. Per questo motivo il kernel si incarica di rilevare situazioni di questo tipo, ed impedirle restituendo un errore di EDEADLK alla funzione che cerca di acquisire un lock che porterebbe ad un deadlock. Per capire meglio il funzionamento del le locking in semantica POSIX (che dierisce alquanto rispetto da quello di BSD, visto sez. 11.4.2) esaminiamo pi` in dettaglio come viene gestito u dal kernel. Lo schema delle strutture utilizzate ` riportato in g. 11.17; come si vede esso ` molto e e simile allanalogo di g. 11.14:114 il lock ` sempre associato allinode, solo che in questo caso la e titolarit` non viene identicata con il riferimento ad una voce nella le table, ma con il valore a del pid del processo. Quando si richiede un lock il kernel eettua una scansione di tutti i lock presenti sul le115 per vericare se la regione richiesta non si sovrappone ad una gi` bloccata, in caso aermativo a decide in base al tipo di lock, in caso negativo il nuovo lock viene comunque acquisito ed aggiunto alla lista. Nel caso di rimozione invece questa viene eettuata controllando che il pid del processo richiedente corrisponda a quello contenuto nel lock. Questa diversa modalit` ha delle conseguenze a precise riguardo il comportamento dei lock POSIX. La prima conseguenza ` che un lock POSIX e non viene mai ereditato attraverso una fork, dato che il processo glio avr` un pid diverso, a mentre passa indenne attraverso una exec in quanto il pid resta lo stesso. Questo comporta che, al contrario di quanto avveniva con la semantica BSD, quando processo termina tutti i le lock da esso detenuti vengono immediatamente rilasciati.
controllare il codice di ritorno delle funzioni invocate ` comunque una buona norma di programmazione, che e permette di evitare un sacco di errori dicili da tracciare proprio perch non vengono rilevati. e 114 in questo caso nella gura si sono evidenziati solo i campi di file_lock signicativi per la semantica POSIX, in particolare adesso ciascuna struttura contiene, oltre al pid del processo in fl_pid, la sezione di le che viene bloccata grazie ai campi fl_start e fl_end. La struttura ` comunque la stessa, solo che in questo caso nel campo e fl_flags ` impostato il bit FL_POSIX ed il campo fl_file non viene usato. e 115 scandisce cio` la linked list delle strutture file_lock, scartando automaticamente quelle per cui fl_flags e non ` FL_POSIX, cos` che le due interfacce restano ben separate. e
113

326

CAPITOLO 11. LA GESTIONE AVANZATA DEI FILE

Figura 11.17: Schema dellarchitettura del le locking, nel caso particolare del suo utilizzo secondo linterfaccia standard POSIX.

La seconda conseguenza ` che qualunque le descriptor che faccia riferimento allo stesso le e (che sia stato ottenuto con una dup o con una open in questo caso non fa dierenza) pu` essere o usato per rimuovere un lock, dato che quello che conta ` solo il pid del processo. Da questo deriva e una ulteriore sottile dierenza di comportamento: dato che alla chiusura di un le i lock ad esso associati vengono rimossi, nella semantica POSIX baster` chiudere un le descriptor qualunque a per cancellare tutti i lock relativi al le cui esso faceva riferimento, anche se questi fossero stati creati usando altri le descriptor che restano aperti. Dato che il controllo sullaccesso ai lock viene eseguito sulla base del pid del processo, possiamo anche prendere in considerazione un altro degli aspetti meno chiari di questa interfaccia e cio` cosa succede quando si richiedono dei lock su regioni che si sovrappongono fra loro alline terno stesso processo. Siccome il controllo, come nel caso della rimozione, si basa solo sul pid del processo che chiama la funzione, queste richieste avranno sempre successo. Nel caso della semantica BSD, essendo i lock relativi a tutto un le e non accumulandosi,116 la cosa non ha alcun eetto; la funzione ritorna con successo, senza che il kernel debba modicare la lista dei lock. In questo caso invece si possono avere una serie di situazioni diverse: ad esempio ` possibile rimuovere con una sola chiamata pi` lock distinti (indicando in una regione che si e u sovrapponga completamente a quelle di questi ultimi), o rimuovere solo una parte di un lock preesistente (indicando una regione contenuta in quella di un altro lock), creando un buco, o coprire con un nuovo lock altri lock gi` ottenuti, e cos` via, a secondo di come si sovrappongono le a regioni richieste e del tipo di operazione richiesta. Il comportamento seguito in questo caso che la funzione ha successo ed esegue loperazione richiesta sulla regione indicata; ` compito del kernel e preoccuparsi di accorpare o dividere le voci nella lista dei lock per far si che le regioni bloccate da essa risultanti siano coerenti con quanto necessario a soddisfare loperazione richiesta.
questa ultima caratteristica ` vera in generale, se cio` si richiede pi` volte lo stesso le lock, o pi` lock sulla e e u u stessa sezione di le, le richieste non si cumulano e basta una sola richiesta di rilascio per cancellare il lock.
116

11.4. IL FILE LOCKING

327

int main ( int argc , char * argv []) { 3 int type = F_UNLCK ; /* lock type : default to unlock ( invalid ) */ 4 off_t start = 0; /* start of the locked region : default to 0 */ 5 off_t len = 0; /* length of the locked region : default to 0 */ 6 int fd , res , i ; /* internal variables */ 7 int bsd = 0; /* semantic type : default to POSIX */ 8 int cmd = F_SETLK ; /* lock command : default to non - blocking */ 9 struct flock lock ; /* file lock structure */ 10 ... 11 if (( argc - optind ) != 1) { /* There must be remaing parameters */ 12 printf ( " Wrong number of arguments % d \ n " , argc - optind ); 13 usage (); 14 } 15 if ( type == F_UNLCK ) { /* There must be a -w or -r option set */ 16 printf ( " You should set a read or a write lock \ n " ); 17 usage (); 18 } 19 fd = open ( argv [ optind ] , O_RDWR ); /* open the file to be locked */ 20 if ( fd < 0) { /* on error exit */ 21 perror ( " Wrong filename " ); 22 exit (1); 23 } 24 /* do lock */ 25 if ( bsd ) { /* if BSD locking */ 26 /* rewrite cmd for suitables flock operation values */ 27 if ( cmd == F_SETLKW ) { /* if no - blocking */ 28 cmd = LOCK_NB ; /* set the value for flock operation */ 29 } else { /* else */ 30 cmd = 0; /* default is null */ 31 } 32 if ( type == F_RDLCK ) cmd |= LOCK_SH ; /* set for shared lock */ 33 if ( type == F_WRLCK ) cmd |= LOCK_EX ; /* set for exclusive lock */ 34 res = flock ( fd , cmd ); /* esecute lock */ 35 } else { /* if POSIX locking */ 36 /* setting flock structure */ 37 lock . l_type = type ; /* set type : read or write */ 38 lock . l_whence = SEEK_SET ; /* start from the beginning of the file */ 39 lock . l_start = start ; /* set the start of the locked region */ 40 lock . l_len = len ; /* set the length of the locked region */ 41 res = fcntl ( fd , cmd , & lock ); /* do lock */ 42 } 43 /* check lock results */ 44 if ( res ) { /* on error exit */ 45 perror ( " Failed lock " ); 46 exit (1); 47 } else { /* else write message */ 48 printf ( " Lock acquired \ n " ); 49 } 50 pause (); /* stop the process , use a signal to exit */ 51 return 0; 52 }
1 2

Figura 11.18: Sezione principale del codice del programma Flock.c.

Per fare qualche esempio sul le locking si ` scritto un programma che permette di bloccare e una sezione di un le usando la semantica POSIX, o un intero le usando la semantica BSD; in g. 11.18 ` riportata il corpo principale del codice del programma, (il testo completo ` allegato e e

328

CAPITOLO 11. LA GESTIONE AVANZATA DEI FILE

nella directory dei sorgenti). La sezione relativa alla gestione delle opzioni al solito si ` omessa, come la funzione che e stampa le istruzioni per luso del programma, essa si cura di impostare le variabili type, start e len; queste ultime due vengono inizializzate al valore numerico fornito rispettivamente tramite gli switch -s e -l, mentre il valore della prima viene impostato con le opzioni -w e -r si richiede rispettivamente o un write lock o read lock (i due valori sono esclusivi, la variabile assumer` a quello che si ` specicato per ultimo). Oltre a queste tre vengono pure impostate la variabile e bsd, che abilita la semantica omonima quando si invoca lopzione -f (il valore preimpostato ` e nullo, ad indicare la semantica POSIX), e la variabile cmd che specica la modalit` di richiesta a del lock (bloccante o meno), a seconda dellopzione -b. Il programma inizia col controllare (11-14) che venga passato un argomento (il le da bloccare), che sia stato scelto (15-18) il tipo di lock, dopo di che apre (19) il le, uscendo (20-23) in caso di errore. A questo punto il comportamento dipende dalla semantica scelta; nel caso sia BSD occorre reimpostare il valore di cmd per luso con flock; infatti il valore preimpostato fa riferimento alla semantica POSIX e vale rispettivamente F_SETLKW o F_SETLK a seconda che si sia impostato o meno la modalit` bloccante. a Nel caso si sia scelta la semantica BSD (25-34) prima si controlla (27-31) il valore di cmd per determinare se si vuole eettuare una chiamata bloccante o meno, reimpostandone il valore opportunamente, dopo di che a seconda del tipo di lock al valore viene aggiunta la relativa opzione (con un OR aritmetico, dato che flock vuole un argomento operation in forma di maschera binaria. Nel caso invece che si sia scelta la semantica POSIX le operazioni sono molto pi` immediate, si prepara (36-40) la struttura per il lock, e lo esegue (41). u In entrambi i casi dopo aver richiesto il lock viene controllato il risultato uscendo (44-46) in caso di errore, o stampando un messaggio (47-49) in caso di successo. Inne il programma si pone in attesa (50) nch un segnale (ad esempio un C-c dato da tastiera) non lo interrompa; e in questo caso il programma termina, e tutti i lock vengono rilasciati. Con il programma possiamo fare varie veriche sul funzionamento del le locking; cominciamo con leseguire un read lock su un le, ad esempio usando allinterno di un terminale il seguente comando: [piccardi@gont sources]$ ./flock -r Flock.c Lock acquired il programma segnaler` di aver acquisito un lock e si bloccher`; in questo caso si ` usato il a a e le locking POSIX e non avendo specicato niente riguardo alla sezione che si vuole bloccare sono stati usati i valori preimpostati che bloccano tutto il le. A questo punto se proviamo ad eseguire lo stesso comando in un altro terminale, e avremo lo stesso risultato. Se invece proviamo ad eseguire un write lock avremo: [piccardi@gont sources]$ ./flock -w Flock.c Failed lock: Resource temporarily unavailable come ci aspettiamo il programma terminer` segnalando lindisponibilit` del lock, dato che il le a a ` bloccato dal precedente read lock. Si noti che il risultato ` lo stesso anche se si richiede il blocco e e su una sola parte del le con il comando: [piccardi@gont sources]$ ./flock -w -s0 -l10 Flock.c Failed lock: Resource temporarily unavailable se invece blocchiamo una regione con: [piccardi@gont sources]$ ./flock -r -s0 -l10 Flock.c Lock acquired una volta che riproviamo ad acquisire il write lock i risultati dipenderanno dalla regione richiesta; ad esempio nel caso in cui le due regioni si sovrappongono avremo che: [piccardi@gont sources]$ ./flock -w -s5 -l15 Flock.c Failed lock: Resource temporarily unavailable

11.4. IL FILE LOCKING

329

ed il lock viene riutato, ma se invece si richiede una regione distinta avremo che: [piccardi@gont sources]$ ./flock -w -s11 -l15 Flock.c Lock acquired ed il lock viene acquisito. Se a questo punto si prova ad eseguire un read lock che comprende la nuova regione bloccata in scrittura: [piccardi@gont sources]$ ./flock -r -s10 -l20 Flock.c Failed lock: Resource temporarily unavailable come ci aspettiamo questo non sar` consentito. a Il programma di norma esegue il tentativo di acquisire il lock in modalit` non bloccante, se a per` usiamo lopzione -b possiamo impostare la modalit` bloccante, riproviamo allora a ripetere o a le prove precedenti con questa opzione: [piccardi@gont sources]$ ./flock -r -b -s0 -l10 Flock.c Lock acquired il primo comando acquisisce subito un read lock, e quindi non cambia nulla, ma se proviamo adesso a richiedere un write lock che non potr` essere acquisito otterremo: a [piccardi@gont sources]$ ./flock -w -s0 -l10 Flock.c il programma cio` si bloccher` nella chiamata a fcntl; se a questo punto rilasciamo il precee a dente lock (terminando il primo comando un C-c sul terminale) potremo vericare che sullaltro terminale il lock viene acquisito, con la comparsa di una nuova riga: [piccardi@gont sources]$ ./flock -w -s0 -l10 Flock.c Lock acquired Unaltra cosa che si pu` controllare con il nostro programma ` linterazione fra i due tipi di o e lock; se ripartiamo dal primo comando con cui si ` ottenuto un lock in lettura sullintero le, e possiamo vericare cosa succede quando si cerca di ottenere un lock in scrittura con la semantica BSD: [root@gont sources]# ./flock -f -w Flock.c Lock acquired che ci mostra come i due tipi di lock siano assolutamente indipendenti; per questo motivo occorre sempre tenere presente quale fra le due semantiche disponibili stanno usando i programmi con cui si interagisce, dato che i lock applicati con laltra non avrebbero nessun eetto.

11.4.4

La funzione lockf

Abbiamo visto come linterfaccia POSIX per il le locking sia molto pi` potente e essibile di u quella di BSD, questo comporta anche una maggiore complessit` per via delle varie opzioni da a passare a fcntl. Per questo motivo ` disponibile anche una interfaccia semplicata (ripresa da e System V) che utilizza la funzione lockf, il cui prototipo `: e
#include <sys/file.h> int lockf(int fd, int cmd, off_t len) Applica, controlla o rimuove un le lock sul le fd. La funzione restituisce 0 in caso di successo, e -1 in caso di errore, nel qual caso errno assumer` a uno dei valori: EWOULDBLOCK non ` possibile acquisire il lock, e si ` selezionato LOCK_NB, oppure loperazione ` e e e proibita perch il le ` mappato in memoria. e e ENOLCK il sistema non ha le risorse per il locking: ci sono troppi segmenti di lock aperti, si ` e esaurita la tabella dei lock.

ed inoltre EBADF, EINVAL.

Il comportamento della funzione dipende dal valore dellargomento cmd, che specica quale azione eseguire; i valori possibili sono riportati in tab. 11.17.

330
Valore LOCK_SH LOCK_EX LOCK_UN LOCK_NB

CAPITOLO 11. LA GESTIONE AVANZATA DEI FILE


Signicato Richiede uno shared lock. Pi` processi possono u mantenere un lock condiviso sullo stesso le. Richiede un exclusive lock. Un solo processo alla volta pu` mantenere un lock esclusivo su un le. o Sblocca il le. Non blocca la funzione quando il lock non ` die sponibile, si specica sempre insieme ad una delle altre operazioni con un OR aritmetico dei valori.

Tabella 11.17: Valori possibili per largomento cmd di lockf.

Qualora il lock non possa essere acquisito, a meno di non aver specicato LOCK_NB, la funzione si blocca no alla disponibilit` dello stesso. Dato che la funzione ` implementata utilizzando a e fcntl la semantica delle operazioni ` la stessa di questultima (pertanto la funzione non ` e e aatto equivalente a flock).

11.4.5

Il mandatory locking

Il mandatory locking ` una opzione introdotta inizialmente in SVr4, per introdurre un le locking e che, come dice il nome, fosse eettivo indipendentemente dai controlli eseguiti da un processo. Con il mandatory locking infatti ` possibile far eseguire il blocco del le direttamente al sistema, e cos` che, anche qualora non si predisponessero le opportune veriche nei processi, questo verrebbe comunque rispettato. Per poter utilizzare il mandatory locking ` stato introdotto un utilizzo particolare del bit sgid. e Se si ricorda quanto esposto in sez. 5.3.2), esso viene di norma utilizzato per cambiare il groupID eettivo con cui viene eseguito un programma, ed ` pertanto sempre associato alla presenza e del permesso di esecuzione per il gruppo. Impostando questo bit su un le senza permesso di esecuzione in un sistema che supporta il mandatory locking, fa s` che questultimo venga attivato per il le in questione. In questo modo una combinazione dei permessi originariamente non contemplata, in quanto senza signicato, diventa lindicazione della presenza o meno del mandatory locking.117 Luso del mandatory locking presenta vari aspetti delicati, dato che neanche lamministratore pu` passare sopra ad un lock; pertanto un processo che blocchi un le cruciale pu` renderlo o o completamente inaccessibile, rendendo completamente inutilizzabile il sistema118 inoltre con il mandatory locking si pu` bloccare completamente un server NFS richiedendo una lettura su un o le su cui ` attivo un lock. Per questo motivo labilitazione del mandatory locking ` di norma e e disabilitata, e deve essere attivata lesystem per lesystem in fase di montaggio (specicando lapposita opzione di mount riportata in tab. 8.9, o con lopzione -o mand per il comando omonimo). Si tenga presente inoltre che il mandatory locking funziona solo sullinterfaccia POSIX di fcntl. Questo ha due conseguenze: che non si ha nessun eetto sui lock richiesti con linterfaccia di flock, e che la granularit` del lock ` quella del singolo byte, come per fcntl. a e La sintassi di acquisizione dei lock ` esattamente la stessa vista in precedenza per fcntl e e lockf, la dierenza ` che in caso di mandatory lock attivato non ` pi` necessario controllare la e e u disponibilit` di accesso al le, ma si potranno usare direttamente le ordinarie funzioni di lettura a e scrittura e sar` compito del kernel gestire direttamente il le locking. a
117 un lettore attento potrebbe ricordare quanto detto in sez. 5.3.3 e cio` che il bit sgid viene cancellato (come e misura di sicurezza) quando di scrive su un le, questo non vale quando esso viene utilizzato per attivare il mandatory locking. 118 il problema si potrebbe risolvere rimuovendo il bit sgid, ma non ` detto che sia cos` facile fare questa operazione e con un sistema bloccato.

11.4. IL FILE LOCKING

331

Questo signica che in caso di read lock la lettura dal le potr` avvenire normalmente con a read, mentre una write si bloccher` no al rilascio del lock, a meno di non aver aperto il le a con O_NONBLOCK, nel qual caso essa ritorner` immediatamente con un errore di EAGAIN. a Se invece si ` acquisito un write lock tutti i tentativi di leggere o scrivere sulla regione del le e bloccata fermeranno il processo no al rilascio del lock, a meno che il le non sia stato aperto con O_NONBLOCK, nel qual caso di nuovo si otterr` un ritorno immediato con lerrore di EAGAIN. a Inne occorre ricordare che le funzioni di lettura e scrittura non sono le sole ad operare sui contenuti di un le, e che sia creat che open (quando chiamata con O_TRUNC) eettuano dei cambiamenti, cos` come truncate, riducendone le dimensioni (a zero nei primi due casi, a quanto specicato nel secondo). Queste operazioni sono assimilate a degli accessi in scrittura e pertanto non potranno essere eseguite (fallendo con un errore di EAGAIN) su un le su cui sia presente un qualunque lock (le prime due sempre, la terza solo nel caso che la riduzione delle dimensioni del le vada a sovrapporsi ad una regione bloccata). Lultimo aspetto della interazione del mandatory locking con le funzioni di accesso ai le ` e quello relativo ai le mappati in memoria (che abbiamo trattato in sez. 11.3.1); anche in tal caso infatti, quando si esegue la mappatura con lopzione MAP_SHARED, si ha un accesso al contenuto del le. Lo standard SVID prevede che sia impossibile eseguire il memory mapping di un le su cui sono presenti dei lock119 in Linux ` stata per` fatta la scelta implementativa120 di seguire e o questo comportamento soltanto quando si chiama mmap con lopzione MAP_SHARED (nel qual caso la funzione fallisce con il solito EAGAIN) che comporta la possibilit` di modicare il le. a

119 alcuni sistemi, come HP-UX, sono ancora pi` restrittivi e lo impediscono anche in caso di advisory locking, u anche se questo comportamento non ha molto senso, dato che comunque qualunque accesso diretto al le ` e consentito. 120 per i dettagli si possono leggere le note relative allimplementazione, mantenute insieme ai sorgenti del kernel nel le Documentation/mandatory.txt.

332

CAPITOLO 11. LA GESTIONE AVANZATA DEI FILE

Capitolo 12

La comunicazione fra processi


Uno degli aspetti fondamentali della programmazione in un sistema unix-like ` la comunicazione e fra processi. In questo capitolo aronteremo solo i meccanismi pi` elementari che permettono di u mettere in comunicazione processi diversi, come quelli tradizionali che coinvolgono pipe e fo e i meccanismi di intercomunicazione di System V e quelli POSIX. Tralasceremo invece tutte le problematiche relative alla comunicazione attraverso la rete (e le relative interfacce) che saranno arontate in dettaglio in un secondo tempo. Non aronteremo neanche meccanismi pi` complessi ed evoluti come le RPC (Remote Procedure Calls) e CORBA u (Common Object Request Brocker Architecture) che in genere sono implementati con un ulteriore livello sopra i meccanismi elementari.

12.1

La comunicazione fra processi tradizionale

Il primo meccanismo di comunicazione fra processi introdotto nei sistemi Unix, ` quello delle e cosiddette pipe; esse costituiscono una delle caratteristiche peculiari del sistema, in particolar modo dellinterfaccia a linea di comando. In questa sezione descriveremo le sue basi, le funzioni che ne gestiscono luso e le varie forme in cui si ` evoluto. e

12.1.1

Le pipe standard

Le pipe nascono sostanzialmente con Unix, e sono il primo, e tuttora uno dei pi` usati, meccaniu smi di comunicazione fra processi. Si tratta in sostanza di una coppia di le descriptor1 connessi fra di loro in modo che se quanto scrive su di uno si pu` rileggere dallaltro. Si viene cos` a o costituire un canale di comunicazione tramite i due le descriptor, nella forma di un tubo (da cui il nome) attraverso cui uiscono i dati. La funzione che permette di creare questa speciale coppia di le descriptor associati ad una pipe ` appunto pipe, ed il suo prototipo `: e e
#include <unistd.h> int pipe(int filedes[2]) Crea una coppia di le descriptor associati ad una pipe. La funzione restituisce zero in caso di successo e -1 per un errore, nel qual caso errno potr` a assumere i valori EMFILE, ENFILE e EFAULT.

La funzione restituisce la coppia di le descriptor nel vettore filedes; il primo ` aperto e in lettura ed il secondo in scrittura. Come accennato concetto di funzionamento di una pipe ` semplice: quello che si scrive nel le descriptor aperto in scrittura viene ripresentato tale e e quale nel le descriptor aperto in lettura. I le descriptor infatti non sono connessi a nessun le
1

si tenga presente che le pipe sono oggetti creati dal kernel e non risiedono su disco.

333

334

CAPITOLO 12. LA COMUNICAZIONE FRA PROCESSI

reale, ma, come accennato in sez. 11.3.3, ad un buer nel kernel, la cui dimensione ` specicata e dal parametro di sistema PIPE_BUF, (vedi sez. 8.1.3). Lo schema di funzionamento di una pipe ` e illustrato in g. 12.1, in cui sono illustrati i due capi della pipe, associati a ciascun le descriptor, con le frecce che indicano la direzione del usso dei dati.

Figura 12.1: Schema della struttura di una pipe.

Chiaramente creare una pipe allinterno di un singolo processo non serve a niente; se per` o ricordiamo quanto esposto in sez. 6.3.1 riguardo al comportamento dei le descriptor nei processi gli, ` immediato capire come una pipe possa diventare un meccanismo di intercomunicazione. e Un processo glio infatti condivide gli stessi le descriptor del padre, compresi quelli associati ad una pipe (secondo la situazione illustrata in g. 12.2). In questo modo se uno dei processi scrive su un capo della pipe, laltro pu` leggere. o

Figura 12.2: Schema dei collegamenti ad una pipe, condivisi fra processo padre e glio dopo lesecuzione fork.

Tutto ci` ci mostra come sia immediato realizzare un meccanismo di comunicazione fra o processi attraverso una pipe, utilizzando le propriet` ordinarie dei le, ma ci mostra anche qual a ` ` il principale2 limite nelluso delle pipe. E necessario infatti che i processi possano condividere i e le descriptor della pipe, e per questo essi devono comunque essere parenti (dallinglese siblings), cio` o derivare da uno stesso processo padre in cui ` avvenuta la creazione della pipe, o, pi` e e u comunemente, essere nella relazione padre/glio. A dierenza di quanto avviene con i le normali, la lettura da una pipe pu` essere bloccante o (qualora non siano presenti dati), inoltre se si legge da una pipe il cui capo in scrittura ` stato e chiuso, si avr` la ricezione di un EOF (vale a dire che la funzione read ritorner` restituendo a a 0). Se invece si esegue una scrittura su una pipe il cui capo in lettura non ` aperto il processo e
Stevens in [1] riporta come limite anche il fatto che la comunicazione ` unidirezionale, ma in realt` questo ` e a e un limite facilmente superabile usando una coppia di pipe.
2

12.1. LA COMUNICAZIONE FRA PROCESSI TRADIZIONALE

335

ricever` il segnale SIGPIPE, e la funzione di scrittura restituir` un errore di EPIPE (al ritorno a a del gestore, o qualora il segnale sia ignorato o bloccato). La dimensione del buer della pipe (PIPE_BUF) ci d` inoltre unaltra importante informazione a riguardo il comportamento delle operazioni di lettura e scrittura su di una pipe; esse infatti sono atomiche ntanto che la quantit` di dati da scrivere non supera questa dimensione. Qualora ad a esempio si eettui una scrittura di una quantit` di dati superiore loperazione verr` eettuata a a in pi` riprese, consentendo lintromissione di scritture eettuate da altri processi. u

12.1.2

Un esempio delluso delle pipe

Per capire meglio il funzionamento delle pipe faremo un esempio di quello che ` il loro uso pi` e u comune, analogo a quello eettuato della shell, e che consiste nellinviare loutput di un processo (lo standard output) sullinput di un altro. Realizzeremo il programma di esempio nella forma di un CGI 3 per Apache, che genera una immagine JPEG di un codice a barre, specicato come argomento in ingresso. Un programma che deve essere eseguito come CGI deve rispondere a delle caratteristiche speciche, esso infatti non viene lanciato da una shell, ma dallo stesso web server, alla richiesta di una specica URL, che di solito ha la forma: http://www.sito.it/cgi-bin/programma?argomento ed il risultato dellelaborazione deve essere presentato (con una intestazione che ne descrive il mime-type) sullo standard output, in modo che il web-server possa reinviarlo al browser che ha eettuato la richiesta, che in questo modo ` in grado di visualizzarlo opportunamente. e Per realizzare quanto voluto useremo in sequenza i programmi barcode e gs, il primo infatti ` in grado di generare immagini PostScript di codici a barre corrispondenti ad una qualunque e stringa, mentre il secondo serve per poter eettuare la conversione della stessa immagine in formato JPEG. Usando una pipe potremo inviare loutput del primo sullinput del secondo, secondo lo schema mostrato in g. 12.3, in cui la direzione del usso dei dati ` data dalle frecce e continue.

Figura 12.3: Schema delluso di una pipe come mezzo di comunicazione fra due processi attraverso lesecuzione una fork e la chiusura dei capi non utilizzati.

Si potrebbe obiettare che sarebbe molto pi` semplice salvare il risultato intermedio su un le u temporaneo. Questo per` non tiene conto del fatto che un CGI deve poter gestire pi` richieste o u in concorrenza, e si avrebbe una evidente race condition in caso di accesso simultaneo a detto
un CGI (Common Gateway Interface) ` un programma che permette la creazione dinamica di un oggetto da e inserire allinterno di una pagina HTML.
3

336

CAPITOLO 12. LA COMUNICAZIONE FRA PROCESSI

le.4 Luso di una pipe invece permette di risolvere il problema in maniera semplice ed elegante, oltre ad essere molto pi` eciente, dato che non si deve scrivere su disco. u Il programma ci servir` anche come esempio delluso delle funzioni di duplicazione dei le a ` descriptor che abbiamo trattato in sez. 6.3.4, in particolare di dup2. E attraverso queste funzioni infatti che ` possibile dirottare gli stream standard dei processi (che abbiamo visto in sez. 6.1.2 e e sez. 7.1.3) sulla pipe. In g. 12.4 abbiamo riportato il corpo del programma, il cui codice completo ` disponibile nel le BarCodePage.c che si trova nella directory dei sorgenti. e La prima operazione del programma (4-12) ` quella di creare le due pipe che serviranno per la e comunicazione fra i due comandi utilizzati per produrre il codice a barre; si ha cura di controllare la riuscita della chiamata, inviando in caso di errore un messaggio invece dellimmagine richiesta.5 Una volta create le pipe, il programma pu` creare (13-17) il primo processo glio, che si o incaricher` (19-25) di eseguire barcode. Questultimo legge dallo standard input una stringa di a caratteri, la converte nellimmagine PostScript del codice a barre ad essa corrispondente, e poi scrive il risultato direttamente sullo standard output. Per poter utilizzare queste caratteristiche prima di eseguire barcode si chiude (20) il capo aperto in scrittura della prima pipe, e se ne collega (21) il capo in lettura allo standard input, usando dup2. Si ricordi che invocando dup2 il secondo le, qualora risulti aperto, viene, come nel caso corrente, chiuso prima di eettuare la duplicazione. Allo stesso modo, dato che barcode scrive limmagine PostScript del codice a barre sullo standard output, per poter eettuare una ulteriore redirezione il capo in lettura della seconda pipe viene chiuso (22) mentre il capo in scrittura viene collegato allo standard output (23). In questo modo allesecuzione (25) di barcode (cui si passa in size la dimensione della pagina per limmagine) questultimo legger` dalla prima pipe la stringa da codicare che gli sar` a a inviata dal padre, e scriver` limmagine PostScript del codice a barre sulla seconda. a Al contempo una volta lanciato il primo glio, il processo padre prima chiude (26) il capo inutilizzato della prima pipe (quello in input) e poi scrive (27) la stringa da convertire sul capo in output, cos` che barcode possa riceverla dallo standard input. A questo punto luso della prima pipe da parte del padre ` nito ed essa pu` essere denitivamente chiusa (28), si attende e o poi (29) che lesecuzione di barcode sia completata. Alla conclusione della sua esecuzione barcode avr` inviato limmagine PostScript del codice a a barre sul capo in scrittura della seconda pipe; a questo punto si pu` eseguire la seconda o conversione, da PS a JPEG, usando il programma gs. Per questo si crea (30-34) un secondo processo glio, che poi (35-42) eseguir` questo programma leggendo limmagine PostScript creata a da barcode dallo standard input, per convertirla in JPEG. Per fare tutto ci` anzitutto si chiude (37) il capo in scrittura della seconda pipe, e se ne o collega (38) il capo in lettura allo standard input. Per poter formattare loutput del programma in maniera utilizzabile da un browser, si provvede anche 40) alla scrittura dellapposita stringa di identicazione del mime-type in testa allo standard output. A questo punto si pu` invocare 41) o gs, provvedendo gli appositi switch che consentono di leggere il le da convertire dallo standard input e di inviare la conversione sullo standard output. Per completare le operazioni il processo padre chiude (44) il capo in scrittura della seconda pipe, e attende la conclusione del glio (45); a questo punto pu` (46) uscire. Si tenga conto o che loperazione di chiudere il capo in scrittura della seconda pipe ` necessaria, infatti, se non e venisse chiusa, gs, che legge il suo standard input da detta pipe, resterebbe bloccato in attesa di
il problema potrebbe essere superato determinando in anticipo un nome appropriato per il le temporaneo, che verrebbe utilizzato dai vari sotto-processi, e cancellato alla ne della loro esecuzione; ma a questo le cose non sarebbero pi` tanto semplici. u 5 la funzione WriteMess non ` riportata in g. 12.4; essa si incarica semplicemente di formattare luscita ale la maniera dei CGI, aggiungendo lopportuno mime type, e formattando il messaggio in HTML, in modo che questultimo possa essere visualizzato correttamente da un browser.
4

12.1. LA COMUNICAZIONE FRA PROCESSI TRADIZIONALE

337

int main ( int argc , char * argv [] , char * envp []) { 3 ... 4 /* create two pipes , pipein and pipeout , to handle communication */ 5 if ( ( retval = pipe ( pipein )) ) { 6 WriteMess ( " input pipe creation error " ); 7 exit (0); 8 } 9 if ( ( retval = pipe ( pipeout )) ) { 10 WriteMess ( " output pipe creation error " ); 11 exit (0); 12 } 13 /* First fork : use child to run barcode program */ 14 if ( ( pid = fork ()) == -1) { /* on error exit */ 15 WriteMess ( " child creation error " ); 16 exit (0); 17 } 18 /* if child */ 19 if ( pid == 0) { 20 close ( pipein [1]); /* close pipe write end */ 21 dup2 ( pipein [0] , STDIN_FILENO ); /* remap stdin to pipe read end */ 22 close ( pipeout [0]); 23 dup2 ( pipeout [1] , STDOUT_FILENO ); /* remap stdout in pipe output */ 24 execlp ( " barcode " , " barcode " , size , NULL ); 25 } 26 close ( pipein [0]); /* close input side of input pipe */ 27 write ( pipein [1] , argv [1] , strlen ( argv [1])); /* write parameter to pipe */ 28 close ( pipein [1]); /* closing write end */ 29 waitpid ( pid , NULL , 0); /* wait child completion */ 30 /* Second fork : use child to run ghostscript */ 31 if ( ( pid = fork ()) == -1) { 32 WriteMess ( " child creation error " ); 33 exit (0); 34 } 35 /* second child , convert PS to JPEG */ 36 if ( pid == 0) { 37 close ( pipeout [1]); /* close write end */ 38 dup2 ( pipeout [0] , STDIN_FILENO ); /* remap read end to stdin */ 39 /* send mime type */ 40 write ( STDOUT_FILENO , content , strlen ( content )); 41 execlp ( " gs " , " gs " , " -q " , " - sDEVICE = jpeg " , " - sOutputFile = - " , " -" , NULL ); 42 } 43 /* still parent */ 44 close ( pipeout [1]); 45 waitpid ( pid , NULL , 0); 46 exit (0); 47 }
1 2

Figura 12.4: Sezione principale del codice del CGI BarCodePage.c.

ulteriori dati in ingresso (lunico modo che un programma ha per sapere che linput ` terminato e ` rilevare che lo standard input ` stato chiuso), e la wait non ritornerebbe. e e

12.1.3

Le funzioni popen e pclose

Come si ` visto la modalit` pi` comune di utilizzo di una pipe ` quella di utilizzarla per fare e a u e da tramite fra output ed input di due programmi invocati in sequenza; per questo motivo lo

338

CAPITOLO 12. LA COMUNICAZIONE FRA PROCESSI

standard POSIX.2 ha introdotto due funzioni che permettono di sintetizzare queste operazioni. La prima di esse si chiama popen ed il suo prototipo `: e
#include <stdio.h> FILE *popen(const char *command, const char *type) Esegue il programma command, di cui, a seconda di type, restituisce, lo standard input o lo standard output nella pipe collegata allo stream restituito come valore di ritorno. La funzione restituisce lindirizzo dello stream associato alla pipe in caso di successo e NULL per un errore, nel qual caso errno potr` assumere i valori relativi alle sottostanti invocazioni di pipe a e fork o EINVAL se type non ` valido. e

La funzione crea una pipe, esegue una fork, ed invoca il programma command attraverso la shell (in sostanza esegue /bin/sh con il ag -c); largomento type deve essere una delle due stringhe "w" o "r", per indicare se la pipe sar` collegata allo standard input o allo standard a output del comando invocato. La funzione restituisce il puntatore allo stream associato alla pipe creata, che sar` aperto a in sola lettura (e quindi associato allo standard output del programma indicato) in caso si sia indicato r, o in sola scrittura (e quindi associato allo standard input) in caso di w. Lo stream restituito da popen ` identico a tutti gli eetti ai le stream visti in cap. 7, anche e se ` collegato ad una pipe e non ad un le, e viene sempre aperto in modalit` fully-buered (vedi e a sez. 7.1.4); lunica dierenza con gli usuali stream ` che dovr` essere chiuso dalla seconda delle e a due nuove funzioni, pclose, il cui prototipo `: e
#include <stdio.h> int pclose(FILE *stream) Chiude il le stream, restituito da una precedente popen attendendo la terminazione del processo ad essa associato. La funzione restituisce 0 in caso di successo e -1 in caso di errore; nel quel caso il valore di errno deriva dalle sottostanti chiamate.

che oltre alla chiusura dello stream si incarica anche di attendere (tramite wait4) la conclusione del processo creato dalla precedente popen. Per illustrare luso di queste due funzioni riprendiamo il problema precedente: il programma mostrato in g. 12.4 per quanto funzionante, ` (volutamente) codicato in maniera piuttosto e complessa, inoltre nella pratica sconta un problema di gs che non ` in grado6 di riconoscere e correttamente lEncapsulated PostScript, per cui deve essere usato il PostScript e tutte le volte viene generata una pagina intera, invece che una immagine delle dimensioni corrispondenti al codice a barre. Se si vuole generare una immagine di dimensioni appropriate si deve usare un approccio diverso. Una possibilit` sarebbe quella di ricorrere ad ulteriore programma, epstopsf, per convertire a in PDF un le EPS (che pu` essere generato da barcode utilizzando lo switch -E). Utilizzando o un PDF al posto di un EPS gs esegue la conversione rispettando le dimensioni originarie del codice a barre e produce un JPEG di dimensioni corrette. Questo approccio per` non funziona, per via di una delle caratteristiche principali delle pipe. o Per poter eettuare la conversione di un PDF infatti ` necessario, per la struttura del formato, e potersi spostare (con lseek) allinterno del le da convertire; se si esegue la conversione con gs su un le regolare non ci sono problemi, una pipe per` ` rigidamente sequenziale, e luso di oe lseek su di essa fallisce sempre con un errore di ESPIPE, rendendo impossibile la conversione. Questo ci dice che in generale la concatenazione di vari programmi funzioner` soltanto quando a tutti prevedono una lettura sequenziale del loro input. Per questo motivo si ` dovuto utilizzare un procedimento diverso, eseguendo prima la cone versione (sempre con gs) del PS in un altro formato intermedio, il PPM,7 dal quale poi si
6 7

nella versione GNU Ghostscript 6.53 (2002-02-13). il Portable PixMap le format ` un formato usato spesso come formato intermedio per eettuare conversioni, e

12.1. LA COMUNICAZIONE FRA PROCESSI TRADIZIONALE

339

pu` ottenere unimmagine di dimensioni corrette attraverso vari programmi di manipolazione o (pnmcrop, pnmmargin) che pu` essere inne trasformata in PNG (con pnm2png). o In questo caso per` occorre eseguire in sequenza ben quattro comandi diversi, inviando o loutput di ciascuno allinput del successivo, per poi ottenere il risultato nale sullo standard output: un caso classico di utilizzazione delle pipe, in cui luso di popen e pclose permette di semplicare notevolmente la stesura del codice. Nel nostro caso, dato che ciascun processo deve scrivere il suo output sullo standard input del successivo, occorrer` usare popen aprendo la pipe in scrittura. Il codice del nuovo programma a ` riportato in g. 12.5. Come si pu` notare lordine di invocazione dei programmi ` linverso e o e di quello in cui ci si aspetta che vengano eettivamente eseguiti. Questo non comporta nessun problema dato che la lettura su una pipe ` bloccante, per cui ciascun processo, per quanto lanciato e per primo, si bloccher` in attesa di ricevere sullo standard input il risultato dellelaborazione del a precedente, bench questultimo venga invocato dopo. e
int main ( int argc , char * argv [] , char * envp []) { 3 FILE * pipe [4]; 4 FILE * pipein ; 5 char * cmd_string [4]={ 6 " pnmtopng " , 7 " pnmmargin - white 10 " , 8 " pnmcrop " , 9 " gs - sDEVICE = ppmraw - sOutputFile = - - sNOPAUSE -q - -c showpage -c quit " 10 }; 11 char content []= " Content - type : image / png \ n \ n " ; 12 int i ; 13 /* write mime - type to stdout */ 14 write ( STDOUT_FILENO , content , strlen ( content )); 15 /* execute chain of command */ 16 for ( i =0; i <4; i ++) { 17 pipe [ i ] = popen ( cmd_string [ i ] , " w " ); 18 dup2 ( fileno ( pipe [ i ]) , STDOUT_FILENO ); 19 } 20 /* create barcode ( in PS ) */ 21 pipein = popen ( " barcode " , " w " ); 22 /* send barcode string to barcode program */ 23 write ( fileno ( pipein ) , argv [1] , strlen ( argv [1])); 24 /* close all pipes ( in reverse order ) */ 25 for ( i =4; i ==0; i - -) { 26 pclose (( pipe [ i ])); 27 } 28 exit (0); 29 }
1 2

Figura 12.5: Codice completo del CGI BarCode.c.

Nel nostro caso il primo passo (14) ` scrivere il mime-type sullo standard output; a questo e punto il processo padre non necessita pi` di eseguire ulteriori operazioni sullo standard output u e pu` tranquillamente provvedere alla redirezione. o Dato che i vari programmi devono essere lanciati in successione, si ` approntato un ciclo e (15-19) che esegue le operazioni in sequenza: prima crea una pipe (17) per la scrittura eseguendo il programma con popen, in modo che essa sia collegata allo standard input, e poi redirige (18) lo standard output su detta pipe.
` infatti molto facile da manipolare, dato che usa caratteri ASCII per memorizzare le immagini, anche se per e questo ` estremamente ineciente. e

340

CAPITOLO 12. LA COMUNICAZIONE FRA PROCESSI

In questo modo il primo processo ad essere invocato (che ` lultimo della catena) scriver` e a ancora sullo standard output del processo padre, ma i successivi, a causa di questa redirezione, scriveranno sulla pipe associata allo standard input del processo invocato nel ciclo precedente. Alla ne tutto quello che resta da fare ` lanciare (21) il primo processo della catena, che nel e caso ` barcode, e scrivere (23) la stringa del codice a barre sulla pipe, che ` collegata al suo e e standard input, inne si pu` eseguire (24-27) un ciclo che chiuda, nellordine inverso rispetto a o quello in cui le si sono create, tutte le pipe create con pclose.

12.1.4

Le pipe con nome, o fo

Come accennato in sez. 12.1.1 il problema delle pipe ` che esse possono essere utilizzate solo da e processi con un progenitore comune o nella relazione padre/glio; per superare questo problema lo standard POSIX.1 ha denito dei nuovi oggetti, le fo, che hanno le stesse caratteristiche delle pipe, ma che invece di essere strutture interne del kernel, visibili solo attraverso un le descriptor, sono accessibili attraverso un inode che risiede sul lesystem, cos` che i processi le possono usare senza dovere per forza essere in una relazione di parentela. Utilizzando una fo tutti i dati passeranno, come per le pipe, attraverso un apposito buer nel kernel, senza transitare dal lesystem; linode allocato sul lesystem serve infatti solo a fornire un punto di riferimento per i processi, che permetta loro di accedere alla stessa fo; il comportamento delle funzioni di lettura e scrittura ` identico a quello illustrato per le pipe in e sez. 12.1.1. Abbiamo gi` visto in sez. 5.1.5 le funzioni mknod e mkfifo che permettono di creare una a fo; per utilizzarne una un processo non avr` che da aprire il relativo le speciale o in lettura o a scrittura; nel primo caso sar` collegato al capo di uscita della fo, e dovr` leggere, nel secondo a a al capo di ingresso, e dovr` scrivere. a Il kernel crea una singola pipe per ciascuna fo che sia stata aperta, che pu` essere acceduta o contemporaneamente da pi` processi, sia in lettura che in scrittura. Dato che per funzionare u deve essere aperta in entrambe le direzioni, per una fo di norma la funzione open si blocca se viene eseguita quando laltro capo non ` aperto. e Le fo per` possono essere anche aperte in modalit` non-bloccante, nel qual caso lapertura o a del capo in lettura avr` successo solo quando anche laltro capo ` aperto, mentre lapertura del a e capo in scrittura restituir` lerrore di ENXIO ntanto che non verr` aperto il capo in lettura. a a 8 operazione che avr` sempre In Linux ` possibile aprire le fo anche in lettura/scrittura, e a successo immediato qualunque sia la modalit` di apertura (bloccante e non bloccante); questo a pu` essere utilizzato per aprire comunque una fo in scrittura anche se non ci sono ancora o processi il lettura; ` possibile anche usare la fo allinterno di un solo processo, nel qual caso e per` occorre stare molto attenti alla possibili situazioni di stallo.9 o Per la loro caratteristica di essere accessibili attraverso il lesystem, ` piuttosto frequente e lutilizzo di una fo come canale di comunicazione nelle situazioni un processo deve ricevere informazioni da altri. In questo caso ` fondamentale che le operazioni di scrittura siano atomiche; e per questo si deve sempre tenere presente che questo ` vero soltanto ntanto che non si supera e il limite delle dimensioni di PIPE_BUF (si ricordi quanto detto in sez. 12.1.1). A parte il caso precedente, che resta probabilmente il pi` comune, Stevens riporta in [1] altre u due casistiche principali per luso delle fo: Da parte dei comandi di shell, per evitare la creazione di le temporanei quando si devono inviare i dati di uscita di un processo sullinput di parecchi altri (attraverso luso del comando tee).
lo standard POSIX lascia indenito il comportamento in questo caso. se si cerca di leggere da una fo che non contiene dati si avr` un deadlock immediato, dato che il processo si a blocca e non potr` quindi mai eseguire le funzioni di scrittura. a
9 8

12.1. LA COMUNICAZIONE FRA PROCESSI TRADIZIONALE

341

Come canale di comunicazione fra client ed server (il modello client-server ` illustrato in e sez. 14.1.1). Nel primo caso quello che si fa ` creare tante fo, da usare come standard input, quanti sono i e processi a cui i vogliono inviare i dati, questi ultimi saranno stati posti in esecuzione ridirigendo lo standard input dalle fo, si potr` poi eseguire il processo che fornisce loutput replicando a questultimo, con il comando tee, sulle varie fo. Il secondo caso ` relativamente semplice qualora si debba comunicare con un processo alla e volta (nel qual caso basta usare due fo, una per leggere ed una per scrivere), le cose diventano invece molto pi` complesse quando si vuole eettuare una comunicazione fra il server ed un u numero imprecisato di client; se il primo infatti pu` ricevere le richieste attraverso una fo o nota, per le risposte non si pu` fare altrettanto, dato che, per la struttura sequenziale delle o fo, i client dovrebbero sapere, prima di leggerli, quando i dati inviati sono destinati a loro. Per risolvere questo problema, si pu` usare unarchitettura come quella illustrata in g. 12.6 o in cui i client inviano le richieste al server su una fo nota mentre le risposte vengono reinviate dal server a ciascuno di essi su una fo temporanea creata per loccasione.

Figura 12.6: Schema dellutilizzo delle fo nella realizzazione di una architettura di comunicazione client/server.

Come esempio di uso questa architettura e delluso delle fo, abbiamo scritto un server di fortunes, che restituisce, alle richieste di un client, un detto a caso estratto da un insieme di frasi; sia il numero delle frasi dellinsieme, che i le da cui esse vengono lette allavvio, sono importabili da riga di comando. Il corpo principale del server ` riportato in g. 12.7, dove si ` tralasciata e e la parte che tratta la gestione delle opzioni a riga di comando, che eettua il settaggio delle variabili fortunefilename, che indica il le da cui leggere le frasi, ed n, che indica il numero di frasi tenute in memoria, ad un valore diverso da quelli preimpostati. Il codice completo ` nel le e FortuneServer.c. Il server richiede (12) che sia stata impostata una dimensione dellinsieme delle frasi non nulla, dato che linizializzazione del vettore fortune avviene solo quando questa dimensione viene specicata, la presenza di un valore nullo provoca luscita dal programma attraverso la funzione (non riportata) che ne stampa le modalit` duso. Dopo di che installa (13-15) la funzione a

342

CAPITOLO 12. LA COMUNICAZIONE FRA PROCESSI

char * fifoname = " / tmp / fortune . fifo " ; int main ( int argc , char * argv []) 3 { 4 /* Variables definition */ 5 int i , n = 0; 6 char * fortunefilename = " / usr / share / games / fortunes / linux " ; 7 char ** fortune ; 8 char line [80]; 9 int fifo_server , fifo_client ; 10 int nread ; 11 ... 12 if ( n ==0) usage (); /* if no pool depth exit printing usage info */ 13 Signal ( SIGTERM , HandSIGTERM ); /* set handlers for termination */ 14 Signal ( SIGINT , HandSIGTERM ); 15 Signal ( SIGQUIT , HandSIGTERM ); 16 i = FortuneParse ( fortunefilename , fortune , n ); /* parse phrases */ 17 if ( mkfifo ( fifoname , 0622)) { /* create well known fifo if does t exist */ 18 if ( errno != EEXIST ) { 19 perror ( " Cannot create well known fifo " ); 20 exit (1); 21 } 22 } 23 daemon (0 , 0); 24 /* open fifo two times to avoid EOF */ 25 fifo_server = open ( fifoname , O_RDONLY ); 26 if ( fifo_server < 0) { 27 perror ( " Cannot open read only well known fifo " ); 28 exit (1); 29 } 30 if ( open ( fifoname , O_WRONLY ) < 0) { 31 perror ( " Cannot open write only well known fifo " ); 32 exit (1); 33 } 34 /* Main body : loop over requests */ 35 while (1) { 36 nread = read ( fifo_server , line , 79); /* read request */ 37 if ( nread < 0) { 38 perror ( " Read Error " ); 39 exit (1); 40 } 41 line [ nread ] = 0; /* terminate fifo name string */ 42 n = random () % i ; /* select random value */ 43 fifo_client = open ( line , O_WRONLY ); /* open client fifo */ 44 if ( fifo_client < 0) { 45 perror ( " Cannot open " ); 46 exit (1); 47 } 48 nread = write ( fifo_client , /* write phrase */ 49 fortune [ n ] , strlen ( fortune [ n ])+1); 50 close ( fifo_client ); /* close client fifo */ 51 } 52 }
1 2

Figura 12.7: Sezione principale del codice del server di fortunes basato sulle fo.

che gestisce i segnali di interruzione (anche questa non ` riportata in g. 12.7) che si limita a e rimuovere dal lesystem la fo usata dal server per comunicare. Terminata linizializzazione (16) si eettua la chiamata alla funzione FortuneParse che legge

12.1. LA COMUNICAZIONE FRA PROCESSI TRADIZIONALE

343

dal le specicato in fortunefilename le prime n frasi e le memorizza (allocando dinamicamente la memoria necessaria) nel vettore di puntatori fortune. Anche il codice della funzione non ` e riportato, in quanto non direttamente attinente allo scopo dellesempio. Il passo successivo (17-22) ` quello di creare con mkfifo la fo nota sulla quale il server e ascolter` le richieste, qualora si riscontri un errore il server uscir` (escludendo ovviamente il a a caso in cui la funzione mkfifo fallisce per la precedente esistenza della fo). Una volta che si ` certi che la fo di ascolto esiste la procedura di inizializzazione ` completata. e e A questo punto si pu` chiamare (23) la funzione daemon per far proseguire lesecuzione del o programma in background come demone. Si pu` quindi procedere (24-33) alla apertura della o fo: si noti che questo viene fatto due volte, prima in lettura e poi in scrittura, per evitare di dover gestire allinterno del ciclo principale il caso in cui il server ` in ascolto ma non ci sono e client che eettuano richieste. Si ricordi infatti che quando una fo ` aperta solo dal capo in e lettura, lesecuzione di read ritorna con zero byte (si ha cio` una condizione di end-of-le). e Nel nostro caso la prima apertura si bloccher` ntanto che un qualunque client non apre a a sua volta la fo nota in scrittura per eettuare la sua richiesta. Pertanto allinizio non ci sono problemi, il client per`, una volta ricevuta la risposta, uscir`, chiudendo tutti i le aperti, o a compresa la fo. A questo punto il server resta (se non ci sono altri client che stanno eettuando richieste) con la fo chiusa sul lato in lettura, ed in questo stato la funzione read non si bloccher` a in attesa di input, ma ritorner` in continuazione, restituendo un end-of-le.10 a Per questo motivo, dopo aver eseguito lapertura in lettura (24-28),11 si esegue una seconda apertura in scrittura (29-32), scartando il relativo le descriptor, che non sar` mai usato, in a questo modo per` la fo resta comunque aperta anche in scrittura, cosicch le successive chiamate o e a read possono bloccarsi. A questo punto si pu` entrare nel ciclo principale del programma che fornisce le risposte ai o client (34-50); questo viene eseguito indenitamente (luscita del server viene eettuata inviando un segnale, in modo da passare attraverso la funzione di chiusura che cancella la fo). Il server ` progettato per accettare come richieste dai client delle stringhe che contengono e il nome della fo sulla quale deve essere inviata la risposta. Per cui prima (35-39) si esegue la lettura dalla stringa di richiesta dalla fo nota (che a questo punto si bloccher` tutte le volte a che non ci sono richieste). Dopo di che, una volta terminata la stringa (40) e selezionato (41) un numero casuale per ricavare la frase da inviare, si proceder` (42-46) allapertura della fo per la a risposta, che poi 47-48) vi sar` scritta. Inne (49) si chiude la fo di risposta che non serve pi`. a u Il codice del client ` invece riportato in g. 12.8, anche in questo caso si ` omessa la gestione e e delle opzioni e la funzione che stampa a video le informazioni di utilizzo ed esce, riportando solo la sezione principale del programma e le denizioni delle variabili. Il codice completo ` nel le e FortuneClient.c dei sorgenti allegati. La prima istruzione (12) compone il nome della fo che dovr` essere utilizzata per ricevere a la risposta dal server. Si usa il pid del processo per essere sicuri di avere un nome univoco; dopo di che (13-18) si procede alla creazione del relativo le, uscendo in caso di errore (a meno che il le non sia gi` presente sul lesystem). a A questo punto il client pu` eettuare linterrogazione del server, per questo prima si apre la o fo nota (19-23), e poi ci si scrive (24) la stringa composta in precedenza, che contiene il nome della fo da utilizzare per la risposta. Inne si richiude la fo del server che a questo punto non serve pi` (25). u Inoltrata la richiesta si pu` passare alla lettura della risposta; anzitutto si apre (26-30) la o
si ` usata questa tecnica per compatibilit`, Linux infatti supporta lapertura delle fo in lettura/scrittura, per e a cui si sarebbe potuto eettuare una singola apertura con O_RDWR, la doppia apertura comunque ha il vantaggio che non si pu` scrivere per errore sul capo aperto in sola lettura. o 11 di solito si eettua lapertura del capo in lettura di una fo in modalit` non bloccante, per evitare il rischio a di uno stallo: se infatti nessuno apre la fo in scrittura il processo non ritorner` mai dalla open. Nel nostro caso a questo rischio non esiste, mentre ` necessario potersi bloccare in lettura in attesa di una richiesta. e
10

344

CAPITOLO 12. LA COMUNICAZIONE FRA PROCESSI

int main ( int argc , char * argv []) { 3 /* Variables definition */ 4 int n = 0; 5 char * fortunefilename = " / tmp / fortune . fifo " ; 6 char line [80]; 7 int fifo_server , fifo_client ; 8 char fifoname [80]; 9 int nread ; 10 char buffer [ PIPE_BUF ]; 11 ... 12 snprintf ( fifoname , 80 , " / tmp / fortune .% d " , getpid ()); /* compose name */ 13 if ( mkfifo ( fifoname , 0622)) { /* open client fifo */ 14 if ( errno != EEXIST ) { 15 perror ( " Cannot create well known fifo " ); 16 exit ( -1); 17 } 18 } 19 fifo_server = open ( fortunefilename , O_WRONLY ); /* open server fifo */ 20 if ( fifo_server < 0) { 21 perror ( " Cannot open well known fifo " ); 22 exit ( -1); 23 } 24 nread = write ( fifo_server , fifoname , strlen ( fifoname )+1); /* write name */ 25 close ( fifo_server ); /* close server fifo */ 26 fifo_client = open ( fifoname , O_RDONLY ); /* open client fifo */ 27 if ( fifo_client < 0) { 28 perror ( " Cannot open well known fifo " ); 29 exit ( -1); 30 } 31 nread = read ( fifo_client , buffer , sizeof ( buffer )); /* read answer */ 32 printf ( " % s " , buffer ); /* print fortune */ 33 close ( fifo_client ); /* close client */ 34 close ( fifo_server ); /* close server */ 35 unlink ( fifoname ); /* remove client fifo */ 36 }
1 2

Figura 12.8: Sezione principale del codice del client di fortunes basato sulle fo.

fo appena creata, da cui si deve riceverla, dopo di che si eettua una lettura (31) nellapposito buer; si ` supposto, come ` ragionevole, che le frasi inviate dal server siano sempre di dimensioni e e inferiori a PIPE_BUF, tralasciamo la gestione del caso in cui questo non ` vero. Inne si stampa e (32) a video la risposta, si chiude (33) la fo e si cancella (34) il relativo le. Si noti come la fo per la risposta sia stata aperta solo dopo aver inviato la richiesta, se non si fosse fatto cos` si avrebbe avuto uno stallo, in quanto senza la richiesta, il server non avrebbe potuto aprirne il capo in scrittura e lapertura si sarebbe bloccata indenitamente. Verichiamo allora il comportamento dei nostri programmi, in questo, come in altri esempi precedenti, si fa uso delle varie funzioni di servizio, che sono state raccolte nella libreria libgapil.so, per poter usare questultima occorrer` denire la speciale variabile di ambiente a LD_LIBRARY_PATH in modo che il linker dinamico possa accedervi. In generale questa variabile indica il pathname della directory contenente la libreria. Nellipotesi (che daremo sempre per vericata) che si facciano le prove direttamente nella directory dei sorgenti (dove di norma vengono creati sia i programmi che la libreria), il comando da dare sar` export LD_LIBRARY_PATH=./; a questo punto potremo lanciare il server, facendogli leggere a una decina di frasi, con:

12.1. LA COMUNICAZIONE FRA PROCESSI TRADIZIONALE [piccardi@gont sources]$ ./fortuned -n10

345

Avendo usato daemon per eseguire il server in background il comando ritorner` immea diatamente, ma potremo vericare con ps che in eetti il programma resta un esecuzione in background, e senza avere associato un terminale di controllo (si ricordi quanto detto in sez. 10.1.5): [piccardi@gont sources]$ ps aux ... piccardi 27489 0.0 0.0 1204 356 ? piccardi 27492 3.0 0.1 2492 764 pts/2

S R

01:06 01:08

0:00 ./fortuned -n10 0:00 ps aux

e si potr` vericare anche che in /tmp ` stata creata la fo di ascolto fortune.fifo. A questo a e punto potremo interrogare il server con il programma client; otterremo cos` : [piccardi@gont sources]$ ./fortune Linux ext2fs has been stable for a long time, now its time to break it -- Linuxkongre 95 in Berlin [piccardi@gont sources]$ ./fortune Lets call it an accidental feature. --Larry Wall [piccardi@gont sources]$ ./fortune ......... Escape the Gates of Hell ::: ....... ...... ::: * ::. :: ::: .:: .:.::. .:: .:: ::. : ::: :: :: :: :: :: :::. ::: .::. .:: ::. ::::. .: ::. ...:::.....................:: .::::.. -- William E. Roadcap [piccardi@gont sources]$ ./fortune Linux ext2fs has been stable for a long time, now its time to break it -- Linuxkongre 95 in Berlin e ripetendo varie volte il comando otterremo, in ordine casuale, le dieci frasi tenute in memoria dal server. Inne per chiudere il server baster` inviare un segnale di terminazione con killall fortuned a e potremo vericare che il gestore del segnale ha anche correttamente cancellato la fo di ascolto da /tmp. Bench il nostro sistema client-server funzioni, la sua struttura ` piuttosto complessa e cone e tinua ad avere vari inconvenienti12 ; in generale infatti linterfaccia delle fo non ` adatta a e risolvere questo tipo di problemi, che possono essere arontati in maniera pi` semplice ed ecau ce o usando i socket (che tratteremo in dettaglio a partire da cap. 15) o ricorrendo a meccanismi di comunicazione diversi, come quelli che esamineremo in seguito.

12.1.5

La funzione socketpair

Un meccanismo di comunicazione molto simile alle pipe, ma che non presenta il problema della unidirezionalit` del usso dei dati, ` quello dei cosiddetti socket locali (o Unix domain socket). a e Tratteremo largomento dei socket in cap. 15,13 nellambito dellinterfaccia generale che essi
12 lo stesso Stevens, che esamina questa architettura in [1], nota come sia impossibile per il server sapere se un client ` andato in crash, con la possibilit` di far restare le fo temporanee sul lesystem, di come sia necessario e a intercettare SIGPIPE dato che un client pu` terminare dopo aver fatto una richiesta, ma prima che la risposta sia o inviata (cosa che nel nostro esempio non ` stata fatta). e 13 si tratta comunque di oggetti di comunicazione che, come le pipe, sono utilizzati attraverso dei le descriptor.

346

CAPITOLO 12. LA COMUNICAZIONE FRA PROCESSI

forniscono per la programmazione di rete; e vedremo anche (in sez. 15.3.4) come si possono denire dei le speciali (di tipo socket, analoghi a quello associati alle fo) cui si accede per` o attraverso quella medesima interfaccia; vale per` la pena esaminare qui una modalit` di uso dei o a socket locali14 che li rende sostanzialmente identici ad una pipe bidirezionale. La funzione socketpair infatti consente di creare una coppia di le descriptor connessi fra di loro (tramite un socket, appunto), senza dover ricorrere ad un le speciale sul lesystem, i descrittori sono del tutto analoghi a quelli che si avrebbero con una chiamata a pipe, con la sola dierenza ` che in questo caso il usso dei dati pu` essere eettuato in entrambe le direzioni. Il e o prototipo della funzione `: e
#include <sys/types.h> #include <sys/socket.h> int socketpair(int domain, int type, int protocol, int sv[2]) Crea una coppia di socket connessi fra loro. La funzione restituisce 0 in caso di successo e -1 in caso di errore, nel qual caso errno assumer` a uno dei valori: EAFNOSUPPORT i socket locali non sono supportati. EPROTONOSUPPORT il protocollo specicato non ` supportato. e EOPNOTSUPP il protocollo specicato non supporta la creazione di coppie di socket. ed inoltre EMFILE, EFAULT.

La funzione restituisce in sv la coppia di descrittori connessi fra di loro: quello che si scrive su uno di essi sar` ripresentato in input sullaltro e viceversa. Gli argomenti domain, type e a protocol derivano dallinterfaccia dei socket (vedi sez. 15.2) che ` quella che fornisce il substrato e per connettere i due descrittori, ma in questo caso i soli valori validi che possono essere specicati sono rispettivamente AF_UNIX, SOCK_STREAM e 0. Lutilit` di chiamare questa funzione per evitare due chiamate a pipe pu` sembrare limitata; a o in realt` lutilizzo di questa funzione (e dei socket locali in generale) permette di trasmettere a attraverso le linea non solo dei dati, ma anche dei le descriptor: si pu` cio` passare da un o e processo ad un altro un le descriptor, con una sorta di duplicazione dello stesso non allinterno di uno stesso processo, ma fra processi distinti (torneremo su questa funzionalit` in sez. 18.2.1). a

12.2

Il sistema di comunicazione fra processi di System V

Bench le pipe e le fo siano ancora ampiamente usate, esse scontano il limite fondamentale che e il meccanismo di comunicazione che forniscono ` rigidamente sequenziale: una situazione in cui e un processo scrive qualcosa che molti altri devono poter leggere non pu` essere implementata o con una pipe. Per questo nello sviluppo di System V vennero introdotti una serie di nuovi oggetti per la comunicazione fra processi ed una nuova interfaccia di programmazione, che fossero in grado di garantire una maggiore essibilit`. In questa sezione esamineremo come Linux supporta quello a che viene chiamato il Sistema di comunicazione fra processi di System V, cui da qui in avanti faremo riferimento come SysV IPC (dove IPC ` la sigla di Inter-Process Comunication). e

12.2.1

Considerazioni generali

La principale caratteristica del SysV IPC ` quella di essere basato su oggetti permanenti che e risiedono nel kernel. Questi, a dierenza di quanto avviene per i le descriptor, non mantengono
la funzione socketpair ` stata introdotta in BSD4.4, ma ` supportata in genere da qualunque sistema che e e fornisca linterfaccia dei socket.
14

12.2. IL SISTEMA DI COMUNICAZIONE FRA PROCESSI DI SYSTEM V

347

un contatore dei riferimenti, e non vengono cancellati dal sistema una volta che non sono pi` in u uso. Questo comporta due problemi: il primo ` che, al contrario di quanto avviene per pipe e fo, e la memoria allocata per questi oggetti non viene rilasciata automaticamente quando non c` pi` e u nessuno che li utilizzi, ed essi devono essere cancellati esplicitamente, se non si vuole che restino attivi no al riavvio del sistema. Il secondo problema ` che, dato che non c`, come per i le, un e e contatore del numero di riferimenti che ne indichi lessere in uso, essi possono essere cancellati anche se ci sono dei processi che li stanno utilizzando, con tutte le conseguenze (negative) del caso. Unulteriore caratteristica negativa ` che gli oggetti usati nel SysV IPC vengono creati e direttamente dal kernel, e sono accessibili solo specicando il relativo identicatore. Questo ` un e numero progressivo (un po come il pid dei processi) che il kernel assegna a ciascuno di essi quanto vengono creati (sul procedimento di assegnazione torneremo in sez. 12.2.3). Lidenticatore viene restituito dalle funzioni che creano loggetto, ed ` quindi locale al processo che le ha eseguite. e Dato che lidenticatore viene assegnato dinamicamente dal kernel non ` possibile prevedere e quale sar`, n utilizzare un qualche valore statico, si pone perci` il problema di come processi a e o diversi possono accedere allo stesso oggetto. Per risolvere il problema nella struttura ipc_perm che il kernel associa a ciascun oggetto, viene mantenuto anche un campo apposito che contiene anche una chiave, identicata da una variabile del tipo primitivo key_t, da specicare in fase di creazione delloggetto, e tramite la quale ` possibile ricavare lidenticatore.15 Oltre la chiave, la struttura, la cui denizione ` e e riportata in g. 12.9, mantiene varie propriet` ed informazioni associate alloggetto. a
struct ipc_perm { key_t key ; uid_t uid ; gid_t gid ; uid_t cuid ; gid_t cgid ; unsigned short int mode ; unsigned short int seq ; };

/* /* /* /* /* /* /*

Key . */ Owner s user ID . */ Owner s group ID . */ Creator s user ID . */ Creator s group ID . */ Read / write permission . */ Sequence number . */

Figura 12.9: La struttura ipc_perm, come denita in sys/ipc.h.

Usando la stessa chiave due processi diversi possono ricavare lidenticatore associato ad un oggetto ed accedervi. Il problema che sorge a questo punto ` come devono fare per accordarsi e sulluso di una stessa chiave. Se i processi sono imparentati la soluzione ` relativamente semplice, e in tal caso infatti si pu` usare il valore speciale IPC_PRIVATE per creare un nuovo oggetto nel o processo padre, lidenticatore cos` ottenuto sar` disponibile in tutti i gli, e potr` essere passato a a come argomento attraverso una exec. Per` quando i processi non sono imparentati (come capita tutte le volte che si ha a che o fare con un sistema client-server) tutto questo non ` possibile; si potrebbe comunque salvare e lidenticatore su un le noto, ma questo ovviamente comporta lo svantaggio di doverselo andare a rileggere. Una alternativa pi` ecace ` quella che i programmi usino un valore comune per u e la chiave (che ad esempio pu` essere dichiarato in un header comune), ma c` sempre il rischio o e che questa chiave possa essere stata gi` utilizzata da qualcun altro. Dato che non esiste una a convenzione su come assegnare queste chiavi in maniera univoca linterfaccia mette a disposizione
in sostanza si sposta il problema dellaccesso dalla classicazione in base allidenticatore alla classicazione in base alla chiave, una delle tante complicazioni inutili presenti nel SysV IPC.
15

348

CAPITOLO 12. LA COMUNICAZIONE FRA PROCESSI

una funzione apposita, ftok, che permette di ottenere una chiave specicando il nome di un le ed un numero di versione; il suo prototipo `: e
#include <sys/types.h> #include <sys/ipc.h> key_t ftok(const char *pathname, int proj_id) Restituisce una chiave per identicare un oggetto del SysV IPC. La funzione restituisce la chiave in caso di successo e -1 altrimenti, nel qual caso errno sar` uno a dei possibili codici di errore di stat.

La funzione determina un valore della chiave sulla base di pathname, che deve specicare il pathname di un le eettivamente esistente e di un numero di progetto proj_id), che di norma viene specicato come carattere, dato che ne vengono utilizzati solo gli 8 bit meno signicativi.16 Il problema ` che anche cos` non c` la sicurezza che il valore della chiave sia univoco, infatti e e esso ` costruito combinando il byte di proj_id) con i 16 bit meno signicativi dellinode del le e pathname (che vengono ottenuti attraverso stat, da cui derivano i possibili errori), e gli 8 bit meno signicativi del numero del dispositivo su cui ` il le. Diventa perci` relativamente facile e o ottenere delle collisioni, specie se i le sono su dispositivi con lo stesso minor number, come /dev/hda1 e /dev/sda1. In genere quello che si fa ` utilizzare un le comune usato dai programmi che devono coe municare (ad esempio un header comune, o uno dei programmi che devono usare loggetto in questione), utilizzando il numero di progetto per ottenere le chiavi che interessano. In ogni caso occorre sempre controllare, prima di creare un oggetto, che la chiave non sia gi` stata utilizzata. a Se questo va bene in fase di creazione, le cose possono complicarsi per i programmi che devono solo accedere, in quanto, a parte gli eventuali controlli sugli altri attributi di ipc_perm, non esiste una modalit` semplice per essere sicuri che loggetto associato ad una certa chiave sia a stato eettivamente creato da chi ci si aspetta. Questo `, insieme al fatto che gli oggetti sono permanenti e non mantengono un contatore e di riferimenti per la cancellazione automatica, il principale problema del SysV IPC. Non esiste infatti una modalit` chiara per identicare un oggetto, come sarebbe stato se lo si fosse associato a ad in le, e tutta linterfaccia ` inutilmente complessa. Per questo ne ` stata eettuata una e e revisione completa nello standard POSIX.1b, che tratteremo in sez. 12.4.

12.2.2

Il controllo di accesso

Oltre alle chiavi, abbiamo visto che ad ogni oggetto sono associate in ipc_perm ulteriori informazioni, come gli identicatori del creatore (nei campi cuid e cgid) e del proprietario (nei campi uid e gid) dello stesso, e un insieme di permessi (nel campo mode). In questo modo ` e possibile denire un controllo di accesso sugli oggetti di IPC, simile a quello che si ha per i le (vedi sez. 5.3.1). Bench questo controllo di accesso sia molto simile a quello dei le, restano delle importanti e dierenze. La prima ` che il permesso di esecuzione non esiste (e se specicato viene ignorato), e per cui si pu` parlare solo di permessi di lettura e scrittura (nel caso dei semafori poi questultimo o ` pi` propriamente un permesso di modica). I valori di mode sono gli stessi ed hanno lo stesso e u signicato di quelli riportati in tab. 5.417 e come per i le deniscono gli accessi per il proprietario, il suo gruppo e tutti gli altri.
nelle libc4 e libc5, come avviene in SunOS, largomento proj_id ` dichiarato tipo char, le glibc usano il e prototipo specicato da XPG4, ma vengono lo stesso utilizzati gli 8 bit meno signicativi. 17 se per` si vogliono usare le costanti simboliche ivi denite occorrer` includere il le sys/stat.h, alcuni o a sistemi deniscono le costanti MSG_R (0400) e MSG_W (0200) per indicare i permessi base di lettura e scrittura per il proprietario, da utilizzare, con gli opportuni shift, pure per il gruppo e gli altri, in Linux, visto la loro scarsa utilit`, queste costanti non sono denite. a
16

12.2. IL SISTEMA DI COMUNICAZIONE FRA PROCESSI DI SYSTEM V

349

Quando loggetto viene creato i campi cuid e uid di ipc_perm ed i campi cgid e gid vengono impostati rispettivamente al valore delluser-ID e del group-ID eettivo del processo che ha chiamato la funzione, ma, mentre i campi uid e gid possono essere cambiati, i campi cuid e cgid restano sempre gli stessi. Il controllo di accesso ` eettuato a due livelli. Il primo livello ` nelle funzioni che richiedono e e lidenticatore di un oggetto data la chiave. Queste specicano tutte un argomento flag, in tal caso quando viene eettuata la ricerca di una chiave, qualora flag specichi dei permessi, questi vengono controllati e lidenticatore viene restituito solo se corrispondono a quelli delloggetto. Se ci sono dei permessi non presenti in mode laccesso sar` negato. Questo controllo per` ` di a oe utilit` indicativa, dato che ` sempre possibile specicare per flag un valore nullo, nel qual caso a e lidenticatore sar` restituito comunque. a Il secondo livello di controllo ` quello delle varie funzioni che accedono direttamente (in e lettura o scrittura) alloggetto. In tal caso lo schema dei controlli ` simile a quello dei le, ed e avviene secondo questa sequenza: se il processo ha i privilegi di amministratore laccesso ` sempre consentito. e se luser-ID eettivo del processo corrisponde o al valore del campo cuid o a quello del campo uid ed il permesso per il proprietario in mode ` appropriato18 laccesso ` consentito. e e se il group-ID eettivo del processo corrisponde o al valore del campo cgid o a quello del campo gid ed il permesso per il gruppo in mode ` appropriato laccesso ` consentito. e e se il permesso per gli altri ` appropriato laccesso ` consentito. e e solo se tutti i controlli elencati falliscono laccesso ` negato. Si noti che a dierenza di quanto e avviene per i permessi dei le, fallire in uno dei passi elencati non comporta il fallimento dellaccesso. Unulteriore dierenza rispetto a quanto avviene per i le ` che per gli oggetti di IPC e il valore di umask (si ricordi quanto esposto in sez. 5.3.3) non ha alcun signicato.

12.2.3

Gli identicatori ed il loro utilizzo

Lunico campo di ipc_perm del quale non abbiamo ancora parlato ` seq, che in g. 12.9 ` e e qualicato con un criptico numero di sequenza, ne parliamo adesso dato che esso ` strettamente e attinente alle modalit` con cui il kernel assegna gli identicatori degli oggetti del sistema di IPC. a Quando il sistema si avvia, alla creazione di ogni nuovo oggetto di IPC viene assegnato un numero progressivo, pari al numero di oggetti di quel tipo esistenti. Se il comportamento fosse sempre questo sarebbe identico a quello usato nellassegnazione dei le descriptor nei processi, ed i valori degli identicatori tenderebbero ad essere riutilizzati spesso e restare di piccole dimensioni (inferiori al numero massimo di oggetti disponibili). Questo va benissimo nel caso dei le descriptor, che sono locali ad un processo, ma qui il comportamento varrebbe per tutto il sistema, e per processi del tutto scorrelati fra loro. Cos` si potrebbero avere situazioni come quella in cui un server esce e cancella le sue code di messaggi, ed il relativo identicatore viene immediatamente assegnato a quelle di un altro server partito subito dopo, con la possibilit` che i client del primo non facciano in tempo ad accorgersi dellavvenuto, a e niscano con linteragire con gli oggetti del secondo, con conseguenze imprevedibili. Proprio per evitare questo tipo di situazioni il sistema usa il valore di seq per provvedere un meccanismo che porti gli identicatori ad assumere tutti i valori possibili, rendendo molto pi` u lungo il periodo in cui un identicatore pu` venire riutilizzato. o
per appropriato si intende che ` impostato il permesso di scrittura per le operazioni di scrittura e quello di e lettura per le operazioni di lettura.
18

350

CAPITOLO 12. LA COMUNICAZIONE FRA PROCESSI

Il sistema dispone sempre di un numero sso di oggetti di IPC,19 e per ciascuno di essi viene mantenuto in seq un numero di sequenza progressivo che viene incrementato di uno ogni volta che loggetto viene cancellato. Quando loggetto viene creato usando uno spazio che era gi` a stato utilizzato in precedenza per restituire lidenticatore al numero di oggetti presenti viene sommato il valore di seq moltiplicato per il numero massimo di oggetti di quel tipo,20 si evita cos` il riutilizzo degli stessi numeri, e si fa s` che lidenticatore assuma tutti i valori possibili.

int main ( int argc , char * argv []) { 3 ... 4 switch ( type ) { 5 case q : /* Message Queue */ 6 debug ( " Message Queue Try \ n " ); 7 for ( i =0; i < n ; i ++) { 8 id = msgget ( IPC_PRIVATE , IPC_CREAT |0666); 9 printf ( " Identifier Value % d \ n " , id ); 10 msgctl ( id , IPC_RMID , NULL ); 11 } 12 break ; 13 case s : /* Semaphore */ 14 debug ( " Semaphore \ n " ); 15 for ( i =0; i < n ; i ++) { 16 id = semget ( IPC_PRIVATE , 1 , IPC_CREAT |0666); 17 printf ( " Identifier Value % d \ n " , id ); 18 semctl ( id , 0 , IPC_RMID ); 19 } 20 break ; 21 case m : /* Shared Memory */ 22 debug ( " Shared Memory \ n " ); 23 for ( i =0; i < n ; i ++) { 24 id = shmget ( IPC_PRIVATE , 1000 , IPC_CREAT |0666); 25 printf ( " Identifier Value % d \ n " , id ); 26 shmctl ( id , IPC_RMID , NULL ); 27 } 28 break ; 29 default : /* should not reached */ 30 return -1; 31 } 32 return 0; 33 }
1 2

Figura 12.10: Sezione principale del programma di test per lassegnazione degli identicatori degli oggetti di IPC IPCTestId.c.

In g. 12.10 ` riportato il codice di un semplice programma di test che si limita a creare un e oggetto (specicato a riga di comando), stamparne il numero di identicatore e cancellarlo per un numero specicato di volte. Al solito non si ` riportato il codice della gestione delle opzioni e a riga di comando, che permette di specicare quante volte eettuare il ciclo n, e su quale tipo di oggetto eseguirlo.
no al kernel 2.2.x questi valori, deniti dalle costanti MSGMNI, SEMMNI e SHMMNI, potevano essere cambiati (come tutti gli altri limiti relativi al SysV IPC ) solo con una ricompilazione del kernel, andando a modicarne la denizione nei relativi header le. A partire dal kernel 2.4.x ` possibile cambiare questi valori a sistema attivo e scrivendo sui le shmmni, msgmni e sem di /proc/sys/kernel o con luso di sysctl. 20 questo vale no ai kernel della serie 2.2.x, dalla serie 2.4.x viene usato lo stesso fattore per tutti gli oggetti, esso ` dato dalla costante IPCMNI, denita in include/linux/ipc.h, che indica il limite massimo per il numero e di tutti oggetti di IPC, ed il cui valore ` 32768. e
19

12.2. IL SISTEMA DI COMUNICAZIONE FRA PROCESSI DI SYSTEM V

351

La gura non riporta il codice di selezione delle opzioni, che permette di inizializzare i valori delle variabili type al tipo di oggetto voluto, e n al numero di volte che si vuole eettuare il ciclo di creazione, stampa, cancellazione. I valori di default sono per luso delle code di messaggi e un ciclo di 5 volte. Se si lancia il comando si otterr` qualcosa del tipo: a piccardi@gont sources]$ ./ipctestid Identifier Value 0 Identifier Value 32768 Identifier Value 65536 Identifier Value 98304 Identifier Value 131072 il che ci mostra che abbiamo un kernel della serie 2.4.x nel quale non avevamo ancora usato nessuna coda di messaggi. Se ripetiamo il comando otterremo ancora: [piccardi@gont sources]$ ./ipctestid Identifier Value 163840 Identifier Value 196608 Identifier Value 229376 Identifier Value 262144 Identifier Value 294912 che ci mostra come il valore di seq sia in eetti una quantit` mantenuta staticamente allinterno a del sistema.

12.2.4

Code di messaggi

Il primo oggetto introdotto dal SysV IPC ` quello delle code di messaggi. Le code di messaggi e sono oggetti analoghi alle pipe o alle fo, anche se la loro struttura ` diversa, ed il loro scopo e principale ` appunto quello di permettere a processi diversi di scambiarsi dei dati. e La funzione che permette di richiedere al sistema lidenticatore di una coda di messaggi esistente (o di crearne una se questa non esiste) ` msgget; il suo prototipo `: e e
#include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> int msgget(key_t key, int flag) Restituisce lidenticatore di una coda di messaggi. La funzione restituisce lidenticatore (un intero positivo) o -1 in caso di errore, nel qual caso errno assumer` uno dei valori: a EACCES EEXIST EIDRM ENOENT ENOSPC il processo chiamante non ha i privilegi per accedere alla coda richiesta. si ` richiesta la creazione di una coda che gi` esiste, ma erano specicati sia IPC_CREAT e a che IPC_EXCL. la coda richiesta ` marcata per essere cancellata. e si ` cercato di ottenere lidenticatore di una coda di messaggi specicando una chiave e che non esiste e IPC_CREAT non era specicato. si ` cercato di creare una coda di messaggi quando ` stato superato il limite massimo e e di code (MSGMNI).

ed inoltre ENOMEM.

Le funzione (come le analoghe che si usano per gli altri oggetti) serve sia a ottenere lidenticatore di una coda di messaggi esistente, che a crearne una nuova. Largomento key specica la chiave che ` associata alloggetto, eccetto il caso in cui si specichi il valore IPC_PRIVATE, e

352

CAPITOLO 12. LA COMUNICAZIONE FRA PROCESSI

nel qual caso la coda ` creata ex-novo e non vi ` associata alcuna chiave, il processo (ed i suoi e e eventuali gli) potranno farvi riferimento solo attraverso lidenticatore. Se invece si specica un valore diverso da IPC_PRIVATE21 leetto della funzione dipende dal valore di flag, se questo ` nullo la funzione si limita ad eettuare una ricerca sugli oggetti e esistenti, restituendo lidenticatore se trova una corrispondenza, o fallendo con un errore di ENOENT se non esiste o di EACCES se si sono specicati dei permessi non validi. Se invece si vuole creare una nuova coda di messaggi flag non pu` essere nullo e deve essere o fornito come maschera binaria, impostando il bit corrispondente al valore IPC_CREAT. In questo caso i nove bit meno signicativi di flag saranno usati come permessi per il nuovo oggetto, secondo quanto illustrato in sez. 12.2.2. Se si imposta anche il bit corrispondente a IPC_EXCL la funzione avr` successo solo se loggetto non esiste gi`, fallendo con un errore di EEXIST altrimenti. a a Si tenga conto che luso di IPC_PRIVATE non impedisce ad altri processi di accedere alla coda (se hanno privilegi sucienti) una volta che questi possano indovinare o ricavare (ad esempio per tentativi) lidenticatore ad essa associato. Per come sono implementati gli oggetti di IPC infatti non esiste una maniera che garantisca laccesso esclusivo ad una coda di messaggi. Usare IPC_PRIVATE o constIPC CREAT e IPC_EXCL per flag comporta solo la creazione di una nuova coda.
Costante MSGMNI MSGMAX MSGMNB Valore 16 8192 16384 File in proc msgmni msgmax msgmnb Signicato Numero massimo di code di messaggi. Dimensione massima di un singolo messaggio. Dimensione massima del contenuto di una coda.

Tabella 12.1: Valori delle costanti associate ai limiti delle code di messaggi.

Le code di messaggi sono caratterizzate da tre limiti fondamentali, deniti negli header e corrispondenti alle prime tre costanti riportate in tab. 12.1, come accennato per` in Linux ` o e possibile modicare questi limiti attraverso luso di sysctl o scrivendo nei le msgmax, msgmnb e msgmni di /proc/sys/kernel/.

Figura 12.11: Schema della struttura di una coda messaggi.

Una coda di messaggi ` costituita da una linked list;22 i nuovi messaggi vengono inseriti in e
21 22

in Linux questo signica un valore diverso da zero. una linked list ` una tipica struttura di dati, organizzati in una lista in cui ciascun elemento contiene un e

12.2. IL SISTEMA DI COMUNICAZIONE FRA PROCESSI DI SYSTEM V

353

coda alla lista e vengono letti dalla cima, in g. 12.11 si ` riportato lo schema con cui queste e strutture vengono mantenute dal kernel.23
struct msqid_ds { struct ipc_perm msg_perm ; time_t msg_stime ; time_t msg_rtime ; time_t msg_ctime ; msgqnum_t msg_qnum ; msglen_t msg_qbytes ; pid_t msg_lspid ; pid_t msg_lrpid ; struct msg * msg_first ; struct msg * msg_last ; unsigned long int msg_cbytes ; };

/* /* /* /* /* /* /* /* /* /* /*

structure for operation permission */ time of last msgsnd command */ time of last msgrcv command */ time of last change */ number of messages currently on queue */ max number of bytes allowed on queue */ pid of last msgsnd () */ pid of last msgrcv () */ first message on queue , unused */ last message in queue , unused */ current number of bytes on queue */

Figura 12.12: La struttura msqid_ds, associata a ciascuna coda di messaggi.

A ciascuna coda ` associata una struttura msgid_ds, la cui denizione, ` riportata in e e g. 12.12. In questa struttura il kernel mantiene le principali informazioni riguardo lo stato corrente della coda.24 In g. 12.12 sono elencati i campi signicativi deniti in sys/msg.h, a cui si sono aggiunti gli ultimi tre campi che sono previsti dalla implementazione originale di System V, ma non dallo standard Unix98. Quando si crea una nuova coda con msgget questa struttura viene inizializzata, in particolare il campo msg_perm viene inizializzato come illustrato in sez. 12.2.2, per quanto riguarda gli altri campi invece: il campo msg_qnum, che esprime il numero di messaggi presenti sulla coda, viene inizializzato a 0. i campi msg_lspid e msg_lrpid, che esprimono rispettivamente il pid dellultimo processo che ha inviato o ricevuto un messaggio sulla coda, sono inizializzati a 0. i campi msg_stime e msg_rtime, che esprimono rispettivamente il tempo in cui ` stato e inviato o ricevuto lultimo messaggio sulla coda, sono inizializzati a 0. il campo msg_ctime, che esprime il tempo di creazione della coda, viene inizializzato al tempo corrente. il campo msg_qbytes che esprime la dimensione massima del contenuto della coda (in byte) viene inizializzato al valore preimpostato del sistema (MSGMNB). i campi msg_first e msg_last che esprimono lindirizzo del primo e ultimo messaggio sono inizializzati a NULL e msg_cbytes, che esprime la dimensione in byte dei messaggi presenti ` inizializzato a zero. Questi campi sono ad uso interno dellimplementazione e e non devono essere utilizzati da programmi in user space).
puntatore al successivo. In questo modo la struttura ` veloce nellestrazione ed immissione dei dati dalle estremit` e a dalla lista (basta aggiungere un elemento in testa o in coda ed aggiornare un puntatore), e relativamente veloce da attraversare in ordine sequenziale (seguendo i puntatori), ` invece relativamente lenta nellaccesso casuale e e nella ricerca. 23 lo schema illustrato in g. 12.11 ` in realt` una semplicazione di quello usato eettivamente no ai kernel e a della serie 2.2.x, nei kernel della serie 2.4.x la gestione delle code di messaggi ` stata modicata ed ` eettuata e e in maniera diversa; abbiamo mantenuto lo schema precedente in quanto illustra comunque in maniera pi` che u adeguata i principi di funzionamento delle code di messaggi. 24 come accennato questo vale no ai kernel della serie 2.2.x, essa viene usata nei kernel della serie 2.4.x solo per compatibilit` in quanto ` quella restituita dalle funzioni dellinterfaccia. Si noti come ci sia una dierenza con i a e campi mostrati nello schema di g. 12.11 che sono presi dalla denizione di linux/msg.h, e fanno riferimento alla denizione della omonima struttura usata nel kernel.

354

CAPITOLO 12. LA COMUNICAZIONE FRA PROCESSI

Una volta creata una coda di messaggi le operazioni di controllo vengono eettuate con la funzione msgctl, che (come le analoghe semctl e shmctl) fa le veci di quello che ioctl ` per i e le; il suo prototipo `: e
#include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> int msgctl(int msqid, int cmd, struct msqid_ds *buf) Esegue loperazione specicata da cmd sulla coda msqid. La funzione restituisce 0 in caso di successo o -1 in caso di errore, nel qual caso errno assumer` a uno dei valori: EACCES EIDRM EPERM si ` richiesto IPC_STAT ma processo chiamante non ha i privilegi di lettura sulla coda. e la coda richiesta ` stata cancellata. e si ` richiesto IPC_SET o IPC_RMID ma il processo non ha i privilegi, o si ` richiesto di e e aumentare il valore di msg_qbytes oltre il limite MSGMNB senza essere amministratore.

ed inoltre EFAULT ed EINVAL.

La funzione permette di accedere ai valori della struttura msqid_ds, mantenuta allindirizzo buf, per la coda specicata dallidenticatore msqid. Il comportamento della funzione dipende dal valore dellargomento cmd, che specica il tipo di azione da eseguire; i valori possibili sono: IPC_STAT IPC_RMID Legge le informazioni riguardo la coda nella struttura indicata da buf. Occorre avere il permesso di lettura sulla coda. Rimuove la coda, cancellando tutti i dati, con eetto immediato. Tutti i processi che cercheranno di accedere alla coda riceveranno un errore di EIDRM, e tutti processi in attesa su funzioni di lettura o di scrittura sulla coda saranno svegliati ricevendo il medesimo errore. Questo comando pu` essere eseguito solo da un o processo con user-ID eettivo corrispondente al creatore o al proprietario della coda, o allamministratore. Permette di modicare i permessi ed il proprietario della coda, ed il limite massimo sulle dimensioni del totale dei messaggi in essa contenuti (msg_qbytes). I valori devono essere passati in una struttura msqid_ds puntata da buf. Per modicare i valori di msg_perm.mode, msg_perm.uid e msg_perm.gid occorre essere il proprietario o il creatore della coda, oppure lamministratore; lo stesso vale per msg_qbytes, ma lamministratore ha la facolt` di incrementarne il valore a limiti a superiori a MSGMNB.

IPC_SET

Una volta che si abbia a disposizione lidenticatore, per inviare un messaggio su una coda si utilizza la funzione msgsnd; il suo prototipo `: e
#include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> int msgsnd(int msqid, struct msgbuf *msgp, size_t msgsz, int msgflg) Invia un messaggio sulla coda msqid. La funzione restituisce 0, e -1 in caso di errore, nel qual caso errno assumer` uno dei valori: a EACCES EIDRM EAGAIN EINTR EINVAL non si hanno i privilegi di accesso sulla coda. la coda ` stata cancellata. e il messaggio non pu` essere inviato perch si ` superato il limite msg_qbytes sul numero o e e massimo di byte presenti sulla coda, e si ` richiesto IPC_NOWAIT in flag. e la funzione ` stata interrotta da un segnale. e si ` specicato un msgid invalido, o un valore non positivo per mtype, o un valore di e msgsz maggiore di MSGMAX.

ed inoltre EFAULT ed ENOMEM.

12.2. IL SISTEMA DI COMUNICAZIONE FRA PROCESSI DI SYSTEM V

355

La funzione inserisce il messaggio sulla coda specicata da msqid; il messaggio ha lunghezza specicata da msgsz ed ` passato attraverso il largomento msgp. Questultimo deve venire e passato sempre come puntatore ad una struttura msgbuf analoga a quella riportata in g. 12.13 che ` quella che deve contenere eettivamente il messaggio. La dimensione massima per il testo e di un messaggio non pu` comunque superare il limite MSGMAX. o La struttura di g. 12.13 ` comunque solo un modello, tanto che la denizione contenuta in e sys/msg.h usa esplicitamente per il secondo campo il valore mtext[1], che non ` di nessuna e utilit` ai ni pratici. La sola cosa che conta ` che la struttura abbia come primo membro un a e campo mtype come nellesempio; esso infatti serve ad identicare il tipo di messaggio e deve essere sempre specicato come intero positivo di tipo long. Il campo mtext invece pu` essere di o qualsiasi tipo e dimensione, e serve a contenere il testo del messaggio. In generale pertanto per inviare un messaggio con msgsnd si usa ridenire una struttura simile a quella di g. 12.13, adattando alle proprie esigenze il campo mtype, (o ridenendo come si vuole il corpo del messaggio, anche con pi` campi o con strutture pi` complesse) avendo per` u u o la cura di mantenere nel primo campo un valore di tipo long che ne indica il tipo. Si tenga presente che la lunghezza che deve essere indicata in questo argomento ` solo quella e del messaggio, non quella di tutta la struttura, se cio` message ` una propria struttura che si pase e sa alla funzione, msgsz dovr` essere uguale a sizeof(message)-sizeof(long), (se consideriamo a il caso dellesempio in g. 12.13, msgsz dovr` essere pari a LENGTH). a

struct msgbuf { long mtype ; char mtext [ LENGTH ]; };

/* message type , must be > 0 */ /* message data */

Figura 12.13: Schema della struttura msgbuf, da utilizzare come argomento per inviare/ricevere messaggi.

Per capire meglio il funzionamento della funzione riprendiamo in considerazione la struttura della coda illustrata in g. 12.11. Alla chiamata di msgsnd il nuovo messaggio sar` aggiunto a in fondo alla lista inserendo una nuova struttura msg, il puntatore msg_last di msqid_ds verr` a aggiornato, come pure il puntatore al messaggio successivo per quello che era il precedente ultimo messaggio; il valore di mtype verr` mantenuto in msg_type ed il valore di msgsz in msg_ts; il a testo del messaggio sar` copiato allindirizzo specicato da msg_spot. a Il valore dellargomento flag permette di specicare il comportamento della funzione. Di norma, quando si specica un valore nullo, la funzione ritorna immediatamente a meno che si sia ecceduto il valore di msg_qbytes, o il limite di sistema sul numero di messaggi, nel qual caso si blocca mandando il processo in stato di sleep. Se si specica per flag il valore IPC_NOWAIT la funzione opera in modalit` non bloccante, ed in questi casi ritorna immediatamente con un a errore di EAGAIN. Se non si specica IPC_NOWAIT la funzione rester` bloccata ntanto che non si liberano risorse a sucienti per poter inserire nella coda il messaggio, nel qual caso ritorner` normalmente. La a funzione pu` ritornare, con una condizione di errore anche in due altri casi: quando la coda viene o rimossa (nel qual caso si ha un errore di EIDRM) o quando la funzione viene interrotta da un segnale (nel qual caso si ha un errore di EINTR). Una volta completato con successo linvio del messaggio sulla coda, la funzione aggiorna i dati mantenuti in msqid_ds, in particolare vengono modicati: Il valore di msg_lspid, che viene impostato al pid del processo chiamante. Il valore di msg_qnum, che viene incrementato di uno. Il valore msg_stime, che viene impostato al tempo corrente.

356

CAPITOLO 12. LA COMUNICAZIONE FRA PROCESSI

La funzione che viene utilizzata per estrarre un messaggio da una coda ` msgrcv; il suo e prototipo `: e
#include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> ssize_t msgrcv(int msqid, struct msgbuf *msgp, size_t msgsz, long msgtyp, int msgflg) Legge un messaggio dalla coda msqid. La funzione restituisce il numero di byte letti in caso di successo, e -1 in caso di errore, nel qual caso errno assumer` uno dei valori: a EACCES EIDRM E2BIG EINTR EINVAL non si hanno i privilegi di accesso sulla coda. la coda ` stata cancellata. e il testo del messaggio ` pi` lungo di msgsz e non si ` specicato MSG_NOERROR in e u e msgflg. la funzione ` stata interrotta da un segnale mentre era in attesa di ricevere un e messaggio. si ` specicato un msgid invalido o un valore di msgsz negativo. e

ed inoltre EFAULT.

La funzione legge un messaggio dalla coda specicata, scrivendolo sulla struttura puntata da msgp, che dovr` avere un formato analogo a quello di g. 12.13. Una volta estratto, il messaggio a sar` rimosso dalla coda. Largomento msgsz indica la lunghezza massima del testo del messaggio a (equivalente al valore del parametro LENGTH nellesempio di g. 12.13). Se il testo del messaggio ha lunghezza inferiore a msgsz esso viene rimosso dalla coda; in caso contrario, se msgflg ` impostato a MSG_NOERROR, il messaggio viene troncato e la parte in e eccesso viene perduta, altrimenti il messaggio non viene estratto e la funzione ritorna con un errore di E2BIG. Largomento msgtyp permette di restringere la ricerca ad un sottoinsieme dei messaggi presenti sulla coda; la ricerca infatti ` fatta con una scansione della struttura mostrata in g. 12.11, e restituendo il primo messaggio incontrato che corrisponde ai criteri specicati (che quindi, visto come i messaggi vengono sempre inseriti dalla coda, ` quello meno recente); in particolare: e se msgtyp ` 0 viene estratto il messaggio in cima alla coda, cio` quello fra i presenti che ` e e e stato inserito per primo. se msgtyp ` positivo viene estratto il primo messaggio il cui tipo (il valore del campo e mtype) corrisponde al valore di msgtyp. se msgtyp ` negativo viene estratto il primo fra i messaggi con il valore pi` basso del tipo, e u fra tutti quelli il cui tipo ha un valore inferiore al valore assoluto di msgtyp. Il valore di msgflg permette di controllare il comportamento della funzione, esso pu` essere o nullo o una maschera binaria composta da uno o pi` valori. Oltre al precedente MSG_NOERROR, u sono possibili altri due valori: MSG_EXCEPT, che permette, quando msgtyp ` positivo, di leggere e il primo messaggio nella coda con tipo diverso da msgtyp, e IPC_NOWAIT che causa il ritorno immediato della funzione quando non ci sono messaggi sulla coda. Il comportamento usuale della funzione infatti, se non ci sono messaggi disponibili per la lettura, ` di bloccare il processo in stato di sleep. Nel caso per` si sia specicato IPC_NOWAIT e o la funzione ritorna immediatamente con un errore ENOMSG. Altrimenti la funzione ritorna normalmente non appena viene inserito un messaggio del tipo desiderato, oppure ritorna con errore qualora la coda sia rimossa (con errno impostata a EIDRM) o se il processo viene interrotto da un segnale (con errno impostata a EINTR). Una volta completata con successo lestrazione del messaggio dalla coda, la funzione aggiorna i dati mantenuti in msqid_ds, in particolare vengono modicati:

12.2. IL SISTEMA DI COMUNICAZIONE FRA PROCESSI DI SYSTEM V Il valore di msg_lrpid, che viene impostato al pid del processo chiamante. Il valore di msg_qnum, che viene decrementato di uno. Il valore msg_rtime, che viene impostato al tempo corrente.

357

Le code di messaggi presentano il solito problema di tutti gli oggetti del SysV IPC; essendo questi permanenti restano nel sistema occupando risorse anche quando un processo ` terminato, e al contrario delle pipe per le quali tutte le risorse occupate vengono rilasciate quanto lultimo processo che le utilizzava termina. Questo comporta che in caso di errori si pu` saturare il o sistema, e che devono comunque essere esplicitamente previste delle funzioni di rimozione in caso di interruzioni o uscite dal programma (come vedremo in g. 12.14). Laltro problema ` non facendo uso di le descriptor le tecniche di I/O multiplexing descritte e in sez. 11.1 non possono essere utilizzate, e non si ha a disposizione niente di analogo alle funzioni select e poll. Questo rende molto scomodo usare pi` di una di queste strutture alla volta; ad u esempio non si pu` scrivere un server che aspetti un messaggio su pi` di una coda senza fare o u ricorso ad una tecnica di polling che esegua un ciclo di attesa su ciascuna di esse. Come esempio delluso delle code di messaggi possiamo riscrivere il nostro server di fortunes usando queste al posto delle fo. In questo caso useremo una sola coda di messaggi, usando il tipo di messaggio per comunicare in maniera indipendente con client diversi. In g. 12.14 si ` riportato un estratto delle parti principali del codice del nuovo server (il e codice completo ` nel le MQFortuneServer.c nei sorgenti allegati). Il programma ` basato su e e un uso accorto della caratteristica di poter associate un tipo ai messaggi per permettere una comunicazione indipendente fra il server ed i vari client, usando il pid di questi ultimi come identicativo. Questo ` possibile in quanto, al contrario di una fo, la lettura di una coda di e messaggi pu` non essere sequenziale, proprio grazie alla classicazione dei messaggi sulla base o del loro tipo. Il programma, oltre alle solite variabili per il nome del le da cui leggere le fortunes e per il vettore di stringhe che contiene le frasi, denisce due strutture appositamente per la comunicazione; con msgbuf_read (8-11) vengono passate le richieste mentre con msgbuf_write (12-15) vengono restituite le frasi. La gestione delle opzioni si ` al solito omessa, essa si curer` di impostare in n il numero e a di frasi da leggere specicato a linea di comando ed in fortunefilename il le da cui leggerle; dopo aver installato (19-21) i gestori dei segnali per trattare luscita dal server, viene prima controllato (22) il numero di frasi richieste abbia senso (cio` sia maggiore di zero), le quali poi e (23) vengono lette nel vettore in memoria con la stessa funzione FortuneParse usata anche per il server basato sulle fo. Una volta inizializzato il vettore di stringhe coi messaggi presi dal le delle fortune si procede (25) con la generazione di una chiave per identicare la coda di messaggi (si usa il nome del le dei sorgenti del server) con la quale poi si esegue (26) la creazione della stessa (si noti come si sia chiamata msgget con un valore opportuno per largomento flag), avendo cura di abortire il programma (27-29) in caso di errore. Finita la fase di inizializzazione il server prima (32) chiama la funzione daemon per andare in background e poi esegue in permanenza il ciclo principale (33-40). Questo inizia (34) con il porsi in attesa di un messaggio di richiesta da parte di un client; si noti infatti come msgrcv richieda un messaggio con mtype uguale a 1: questo ` il valore usato per le richieste dato che corrisponde e al pid di init, che non pu` essere un client. Luso del ag MSG_NOERROR ` solo per sicurezza, o e dato che i messaggi di richiesta sono di dimensione ssa (e contengono solo il pid del client). Se non sono presenti messaggi di richiesta msgrcv si bloccher`, ritornando soltanto in corria spondenza dellarrivo sulla coda di un messaggio di richiesta da parte di un client, in tal caso il ciclo prosegue (35) selezionando una frase a caso, copiandola (36) nella struttura msgbuf_write usata per la risposta e calcolandone (37) la dimensione. Per poter permettere a ciascun client di ricevere solo la risposta indirizzata a lui il tipo del

358

CAPITOLO 12. LA COMUNICAZIONE FRA PROCESSI

int msgid ; /* Message queue identifier */ int main ( int argc , char * argv []) 3 { 4 /* Variables definition */ 5 int i , n = 0; 6 char ** fortune ; /* array of fortune message string */ 7 char * fortunefilename = " / usr / share / games / fortunes / linux " ; /* file name */ 8 struct msgbuf_read { /* message struct to read request from clients */ 9 long mtype ; /* message type , must be 1 */ 10 long pid ; /* message data , must be the pid of the client */ 11 } msg_read ; 12 struct msgbuf_write { /* message struct to write result to clients */ 13 long mtype ; /* message type , will be the pid of the client */ 14 char mtext [ MSGMAX ]; /* message data , will be the fortune */ 15 } msg_write ; 16 key_t key ; /* Message queue key */ 17 int size ; /* message size */ 18 ... 19 Signal ( SIGTERM , HandSIGTERM ); /* set handlers for termination */ 20 Signal ( SIGINT , HandSIGTERM ); 21 Signal ( SIGQUIT , HandSIGTERM ); 22 if ( n ==0) usage (); /* if no pool depth exit printing usage info */ 23 i = FortuneParse ( fortunefilename , fortune , n ); /* parse phrases */ 24 /* Create the queue */ 25 key = ftok ( " ./ MQFortuneServer . c " , 1); 26 msgid = msgget ( key , IPC_CREAT |0666); 27 if ( msgid < 0) { 28 perror ( " Cannot create message queue " ); 29 exit (1); 30 } 31 /* Main body : loop over requests */ 32 daemon (0 , 0); 33 while (1) { 34 msgrcv ( msgid , & msg_read , sizeof ( int ) , 1 , MSG_NOERROR ); 35 n = random () % i ; /* select random value */ 36 strncpy ( msg_write . mtext , fortune [ n ] , MSGMAX ); 37 size = min ( strlen ( fortune [ n ])+1 , MSGMAX ); 38 msg_write . mtype = msg_read . pid ; /* use request pid as type */ 39 msgsnd ( msgid , & msg_write , size , 0); 40 } 41 } 42 /* 43 * Signal Handler to manage termination 44 */ 45 void HandSIGTERM ( int signo ) { 46 msgctl ( msgid , IPC_RMID , NULL ); /* remove message queue */ 47 exit (0); 48 }
1 2

Figura 12.14: Sezione principale del codice del server di fortunes basato sulle message queue.

messaggio in uscita viene inizializzato (38) al valore del pid del client ricevuto nel messaggio di richiesta. Lultimo passo del ciclo (39) ` inviare sulla coda il messaggio di risposta. Si tenga conto e che se la coda ` piena anche questa funzione potr` bloccarsi ntanto che non venga liberato dello e a spazio. Si noti che il programma pu` terminare solo grazie ad una interruzione da parte di un segnale; o in tal caso verr` eseguito (45-48) il gestore HandSIGTERM, che semplicemente si limita a cancellare a la coda (46) ed ad uscire (47).

12.2. IL SISTEMA DI COMUNICAZIONE FRA PROCESSI DI SYSTEM V

359

int main ( int argc , char * argv []) { 3 ... 4 key = ftok ( " ./ MQFortuneServer . c " , 1); 5 msgid = msgget ( key , 0); 6 if ( msgid < 0) { 7 perror ( " Cannot find message queue " ); 8 exit (1); 9 } 10 /* Main body : do request and write result */ 11 msg_read . mtype = 1; /* type for request is always 1 */ 12 msg_read . pid = getpid (); /* use pid for communications */ 13 size = sizeof ( msg_read . pid ); 14 msgsnd ( msgid , & msg_read , size , 0); /* send request message */ 15 msgrcv ( msgid , & msg_write , MSGMAX , msg_read . pid , MSG_NOERROR ); 16 printf ( " % s " , msg_write . mtext ); 17 }
1 2

Figura 12.15: Sezione principale del codice del client di fortunes basato sulle message queue.

In g. 12.15 si ` riportato un estratto il codice del programma client. Al solito il codice e completo ` con i sorgenti allegati, nel le MQFortuneClient.c. Come sempre si sono rimosse le e parti relative alla gestione delle opzioni, ed in questo caso, anche la dichiarazione delle variabili, che, per la parte relative alle strutture usate per la comunicazione tramite le code, sono le stesse viste in g. 12.14. Il client in questo caso ` molto semplice; la prima parte del programma (4-9) si occupa di e accedere alla coda di messaggi, ed ` identica a quanto visto per il server, solo che in questo e caso msgget non viene chiamata con il ag di creazione in quanto la coda deve essere preesistente. In caso di errore (ad esempio se il server non ` stato avviato) il programma termina e immediatamente. Una volta acquisito lidenticatore della coda il client compone il messaggio di richiesta (1213) in msg_read, usando 1 per il tipo ed inserendo il proprio pid come dato da passare al server. Calcolata (14) la dimensione, provvede (15) ad immettere la richiesta sulla coda. A questo punto non resta che (16) rileggere dalla coda la risposta del server richiedendo a msgrcv di selezionare i messaggi di tipo corrispondente al valore del pid inviato nella richiesta. Lultimo passo (17) prima di uscire ` quello di stampare a video il messaggio ricevuto. e Proviamo allora il nostro nuovo sistema, al solito occorre denire LD_LIBRARY_PATH per accedere alla libreria libgapil.so, dopo di che, in maniera del tutto analoga a quanto fatto con il programma che usa le fo, potremo far partire il server con: [piccardi@gont sources]$ ./mqfortuned -n10 come nel caso precedente, avendo eseguito il server in background, il comando ritorner` immea diatamente; potremo per` vericare con ps che il programma ` eettivamente in esecuzione, e o e che ha creato una coda di messaggi: [piccardi@gont sources]$ ipcs ------ Shared Memory Segments -------key shmid owner perms ------ Semaphore Arrays -------key semid owner perms

bytes

nattch

status

nsems

360

CAPITOLO 12. LA COMUNICAZIONE FRA PROCESSI

------ Message Queues -------key msqid owner 0x0102dc6a 0 piccardi

perms 666

used-bytes 0

messages 0

a questo punto potremo usare il client per ottenere le nostre frasi: [piccardi@gont sources]$ ./mqfortune Linux ext2fs has been stable for a long time, now its time to break it -- Linuxkongre 95 in Berlin [piccardi@gont sources]$ ./mqfortune Lets call it an accidental feature. --Larry Wall con un risultato del tutto equivalente al precedente. Inne potremo chiudere il server inviando il segnale di terminazione con il comando killall mqfortuned vericando che eettivamente la coda di messaggi viene rimossa. Bench funzionante questa architettura risente dello stesso inconveniente visto anche nel caso e del precedente server basato sulle fo; se il client viene interrotto dopo linvio del messaggio di richiesta e prima della lettura della risposta, questultima resta nella coda (cos` come per le fo si aveva il problema delle fo che restavano nel lesystem). In questo caso per` il problemi sono o maggiori, sia perch ` molto pi` facile esaurire la memoria dedicata ad una coda di messaggi ee u che gli inode di un lesystem, sia perch, con il riutilizzo dei pid da parte dei processi, un client e eseguito in un momento successivo potrebbe ricevere un messaggio non indirizzato a lui.

12.2.5

Semafori

I semafori non sono meccanismi di intercomunicazione diretta come quelli (pipe, fo e code di messaggi) visti nora, e non consentono di scambiare dati fra processi, ma servono piuttosto come meccanismi di sincronizzazione o di protezione per le sezioni critiche del codice (si ricordi quanto detto in sez. 3.5.2). Un semaforo ` uno speciale contatore, mantenuto nel kernel, che permette, a seconda del suo e valore, di consentire o meno la prosecuzione dellesecuzione di un programma. In questo modo laccesso ad una risorsa condivisa da pi` processi pu` essere controllato, associando ad essa un u o semaforo che consente di assicurare che non pi` di un processo alla volta possa usarla. u Il concetto di semaforo ` uno dei concetti base nella programmazione ed ` assolutamente e e generico, cos` come del tutto generali sono modalit` con cui lo si utilizza. Un processo che a deve accedere ad una risorsa eseguir` un controllo del semaforo: se questo ` positivo il suo a e valore sar` decrementato, indicando che si ` consumato una unit` della risorsa, ed il processo a e a potr` proseguire nellutilizzo di questultima, provvedendo a rilasciarla, una volta completate le a operazioni volute, reincrementando il semaforo. Se al momento del controllo il valore del semaforo ` nullo, siamo invece in una situazione in e cui la risorsa non ` disponibile, ed il processo si bloccher` in stato di sleep n quando chi la sta e a utilizzando non la rilascer`, incrementando il valore del semaforo. Non appena il semaforo torna a positivo, indicando che la risorsa ` disponibile, il processo sar` svegliato, e si potr` operare come e a a nel caso precedente (decremento del semaforo, accesso alla risorsa, incremento del semaforo). Per poter implementare questo tipo di logica le operazioni di controllo e decremento del contatore associato al semaforo devono essere atomiche, pertanto una realizzazione di un oggetto di questo tipo ` necessariamente demandata al kernel. La forma pi` semplice di semaforo ` quella e u e del semaforo binario, o mutex, in cui un valore diverso da zero (normalmente 1) indica la libert` a di accesso, e un valore nullo loccupazione della risorsa. In generale per` si possono usare semafori o

12.2. IL SISTEMA DI COMUNICAZIONE FRA PROCESSI DI SYSTEM V

361

con valori interi, utilizzando il valore del contatore come indicatore del numero di risorse ancora disponibili. Il sistema di comunicazione inter-processo di SysV IPC prevede anche i semafori, ma gli oggetti utilizzati non sono semafori singoli, ma gruppi di semafori detti insiemi (o semaphore set); la funzione che permette di creare o ottenere lidenticatore di un insieme di semafori ` e semget, ed il suo prototipo `: e
#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int semget(key_t key, int nsems, int flag) Restituisce lidenticatore di un insieme di semafori. La funzione restituisce lidenticatore (un intero positivo) o -1 in caso di errore, nel qual caso errno assumer` i valori: a ENOSPC si ` cercato di creare una insieme di semafori quando ` stato superato o il limite per il e e numero totale di semafori (SEMMNS) o quello per il numero totale degli insiemi (SEMMNI) nel sistema. largomento nsems ` minore di zero o maggiore del limite sul numero di semafori per e ciascun insieme (SEMMSL), o se linsieme gi` esiste, maggiore del numero di semafori a che contiene. il sistema non ha abbastanza memoria per poter contenere le strutture per un nuovo insieme di semafori.

EINVAL

ENOMEM

ed inoltre EACCES, ENOENT, EEXIST, EIDRM, con lo stesso signicato che hanno per msgget.

La funzione ` del tutto analoga a msgget, solo che in questo caso restituisce lidenticatore e di un insieme di semafori, in particolare ` identico luso degli argomenti key e flag, per cui non e ripeteremo quanto detto al proposito in sez. 12.2.4. Largomento nsems permette di specicare quanti semafori deve contenere linsieme quando se ne richieda la creazione, e deve essere nullo quando si eettua una richiesta dellidenticatore di un insieme gi` esistente. a Purtroppo questa implementazione complica inutilmente lo schema elementare che abbiamo descritto, dato che non ` possibile denire un singolo semaforo, ma se ne deve creare per forza e un insieme. Ma questa in denitiva ` solo una complicazione inutile, il problema ` che i semafori e e del SysV IPC sorono di altri due, ben pi` gravi, difetti. u Il primo difetto ` che non esiste una funzione che permetta di creare ed inizializzare un e semaforo in ununica chiamata; occorre prima creare linsieme dei semafori con semget e poi inizializzarlo con semctl, si perde cos` ogni possibilit` di eseguire loperazione atomicamente. a Il secondo difetto deriva dalla caratteristica generale degli oggetti del SysV IPC di essere risorse globali di sistema, che non vengono cancellate quando nessuno le usa pi`; ci si cos` u a trova a dover arontare esplicitamente il caso in cui un processo termina per un qualche errore, lasciando un semaforo occupato, che rester` tale no al successivo riavvio del sistema. a Come vedremo esistono delle modalit` per evitare tutto ci`, ma diventa necessario indicare a o esplicitamente che si vuole il ripristino del semaforo alluscita del processo.
struct semid_ds { struct ipc_perm sem_perm ; time_t sem_otime ; time_t sem_ctime ; unsigned long int sem_nsems ; };

/* /* /* /*

operation permission struct */ last semop () time */ last time changed by semctl () */ number of semaphores in set */

Figura 12.16: La struttura semid_ds, associata a ciascun insieme di semafori.

362

CAPITOLO 12. LA COMUNICAZIONE FRA PROCESSI

A ciascun insieme di semafori ` associata una struttura semid_ds, riportata in g. 12.16.25 e Come nel caso delle code di messaggi quando si crea un nuovo insieme di semafori con semget questa struttura viene inizializzata, in particolare il campo sem_perm viene inizializzato come illustrato in sez. 12.2.2 (si ricordi che in questo caso il permesso di scrittura ` in realt` permesso e a di alterare il semaforo), per quanto riguarda gli altri campi invece: il campo sem_nsems, che esprime il numero di semafori nellinsieme, viene inizializzato al valore di nsems. il campo sem_ctime, che esprime il tempo di creazione dellinsieme, viene inizializzato al tempo corrente. il campo sem_otime, che esprime il tempo dellultima operazione eettuata, viene inizializzato a zero. Ciascun semaforo dellinsieme ` realizzato come una struttura di tipo sem che ne contiene i e 26 ` riportata in g. 12.17. Questa struttura, non ` accessibile dati essenziali, la sua denizione e e in user space, ma i valori in essa specicati possono essere letti in maniera indiretta, attraverso luso delle funzioni di controllo.
struct sem { short sempid ; ushort semval ; ushort semncnt ; ushort semzcnt ; };

/* /* /* /*

pid of last operation */ current value */ num procs awaiting increase in semval */ num procs awaiting semval = 0 */

Figura 12.17: La struttura sem, che contiene i dati di un singolo semaforo.

I dati mantenuti nella struttura, ed elencati in g. 12.17, indicano rispettivamente: semval il valore numerico del semaforo. sempid il pid dellultimo processo che ha eseguito una operazione sul semaforo. semncnt il numero di processi in attesa che esso venga incrementato. semzcnt il numero di processi in attesa che esso si annulli.
Costante SEMMNI SEMMSL SEMMNS SEMVMX SEMOPM SEMMNU SEMUME SEMAEM Valore 128 250 SEMMNI*SEMMSL 32767 32 SEMMNS SEMOPM SEMVMX Signicato Numero massimo di insiemi di semafori. Numero massimo di semafori per insieme. Numero massimo di semafori nel sistema. Massimo valore per un semaforo. Massimo numero di operazioni per chiamata a semop. Massimo numero di strutture di ripristino. Massimo numero di voci di ripristino. Valore massimo per laggiustamento alluscita.

Tabella 12.2: Valori delle costanti associate ai limiti degli insiemi di semafori, denite in linux/sem.h.

Come per le code di messaggi anche per gli insiemi di semafori esistono una serie di limiti, i cui valori sono associati ad altrettante costanti, che si sono riportate in tab. 12.2. Alcuni di
non si sono riportati i campi ad uso interno del kernel, che vedremo in g. 12.20, che dipendono dallimplementazione. 26 si ` riportata la denizione originaria del kernel 1.0, che contiene la prima realizzazione del SysV IPC in Linux. e In realt` questa struttura ormai ` ridotta ai soli due primi membri, e gli altri vengono calcolati dinamicamente. a e La si ` utilizzata a scopo di esempio, perch indica tutti i valori associati ad un semaforo, restituiti dalle funzioni e e di controllo, e citati dalle pagine di manuale.
25

12.2. IL SISTEMA DI COMUNICAZIONE FRA PROCESSI DI SYSTEM V

363

questi limiti sono al solito accessibili e modicabili attraverso sysctl o scrivendo direttamente nel le /proc/sys/kernel/sem. La funzione che permette di eettuare le varie operazioni di controllo sui semafori (fra le quali, come accennato, ` impropriamente compresa anche la loro inizializzazione) ` semctl; il e e suo prototipo `: e
#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int semctl(int semid, int semnum, int cmd) int semctl(int semid, int semnum, int cmd, union semun arg) Esegue le operazioni di controllo su un semaforo o un insieme di semafori. La funzione restituisce in caso di successo un valore positivo quanto usata con tre argomenti ed un valore nullo quando usata con quattro. In caso di errore restituisce -1, ed errno assumer` uno a dei valori: EACCES EIDRM EPERM ERANGE il processo non ha i privilegi per eseguire loperazione richiesta. linsieme di semafori ` stato cancellato. e si ` richiesto IPC_SET o IPC_RMID ma il processo non ha privilegi sucienti ad eseguire e loperazione. si ` richiesto SETALL SETVAL ma il valore a cui si vuole impostare il semaforo ` minore e e di zero o maggiore di SEMVMX.

ed inoltre EFAULT ed EINVAL.

La funzione pu` avere tre o quattro argomenti, a seconda delloperazione specicata con o cmd, ed opera o sullintero insieme specicato da semid o sul singolo semaforo di un insieme, specicato da semnum.
union semun { int val ; struct semid_ds * buf ; unsigned short * array ; struct seminfo * __buf ; };

/* /* /* /* /*

value for SETVAL */ buffer for IPC_STAT , IPC_SET */ array for GETALL , SETALL */ Linux specific part : */ buffer for IPC_INFO */

Figura 12.18: La denizione dei possibili valori di una union semun, usata come quarto argomento della funzione semctl.

Qualora la funzione operi con quattro argomenti arg ` un argomento generico, che conterr` e a un dato diverso a seconda dellazione richiesta; per unicare largomento esso deve essere passato come una semun, la cui denizione, con i possibili valori che pu` assumere, ` riportata in g. 12.18. o e Come gi` accennato sia il comportamento della funzione che il numero di argomenti con cui a deve essere invocata dipendono dal valore dellargomento cmd, che specica lazione da intraprendere; i valori validi (che cio` non causano un errore di EINVAL) per questo argomento sono e i seguenti: IPC_STAT Legge i dati dellinsieme di semafori, copiando il contenuto della relativa struttura semid_ds allindirizzo specicato con arg.buf. Occorre avere il permesso di lettura. Largomento semnum viene ignorato. Rimuove linsieme di semafori e le relative strutture dati, con eetto immediato. Tutti i processi che erano stato di sleep vengono svegliati, ritornando con un errore di EIDRM. Luser-ID eettivo del processo deve corrispondere o al creatore

IPC_RMID

364

CAPITOLO 12. LA COMUNICAZIONE FRA PROCESSI o al proprietario dellinsieme, o allamministratore. Largomento semnum viene ignorato.

IPC_SET

Permette di modicare i permessi ed il proprietario dellinsieme. I valori devono essere passati in una struttura semid_ds puntata da arg.buf di cui saranno usati soltanto i campi sem_perm.uid, sem_perm.gid e i nove bit meno signicativi di sem_perm.mode. Luser-ID eettivo del processo deve corrispondere o al creatore o al proprietario dellinsieme, o allamministratore. Largomento semnum viene ignorato. Restituisce il valore corrente di ciascun semaforo dellinsieme (corrispondente al campo semval di sem) nel vettore indicato da arg.array. Occorre avere il permesso di lettura. Largomento semnum viene ignorato. Restituisce come valore di ritorno della funzione il numero di processi in attesa che il semaforo semnum dellinsieme semid venga incrementato (corrispondente al campo semncnt di sem); va invocata con tre argomenti. Occorre avere il permesso di lettura. Restituisce come valore di ritorno della funzione il pid dellultimo processo che ha compiuto una operazione sul semaforo semnum dellinsieme semid (corrispondente al campo sempid di sem); va invocata con tre argomenti. Occorre avere il permesso di lettura. Restituisce come valore di ritorno della funzione il il valore corrente del semaforo semnum dellinsieme semid (corrispondente al campo semval di sem); va invocata con tre argomenti. Occorre avere il permesso di lettura. Restituisce come valore di ritorno della funzione il numero di processi in attesa che il valore del semaforo semnum dellinsieme semid diventi nullo (corrispondente al campo semncnt di sem); va invocata con tre argomenti. Occorre avere il permesso di lettura. Inizializza il valore di tutti i semafori dellinsieme, aggiornando il campo sem_ctime di semid_ds. I valori devono essere passati nel vettore indicato da arg.array. Si devono avere i privilegi di scrittura sul semaforo. Largomento semnum viene ignorato. Inizializza il semaforo semnum al valore passato dallargomento arg.val, aggiornando il campo sem_ctime di semid_ds. Si devono avere i privilegi di scrittura sul semaforo.

GETALL

GETNCNT

GETPID

GETVAL

GETZCNT

SETALL

SETVAL

Quando si imposta il valore di un semaforo (sia che lo si faccia per tutto linsieme con SETALL, che per un solo semaforo con SETVAL), i processi in attesa su di esso reagiscono di conseguenza al cambiamento di valore. Inoltre la coda delle operazioni di ripristino viene cancellata per tutti i semafori il cui valore viene modicato.
Operazione GETNCNT GETPID GETVAL GETZCNT Valore restituito Valore di semncnt. Valore di sempid. Valore di semval. Valore di semzcnt.

Tabella 12.3: Valori di ritorno della funzione semctl.

12.2. IL SISTEMA DI COMUNICAZIONE FRA PROCESSI DI SYSTEM V

365

Il valore di ritorno della funzione in caso di successo dipende dalloperazione richiesta; per tutte le operazioni che richiedono quattro argomenti esso ` sempre nullo, per le altre operazioni, e elencate in tab. 12.3 viene invece restituito il valore richiesto, corrispondente al campo della struttura sem indicato nella seconda colonna della tabella. Le operazioni ordinarie sui semafori, come lacquisizione o il rilascio degli stessi (in sostanza tutte quelle non comprese nelluso di semctl) vengono eettuate con la funzione semop, il cui prototipo `: e
#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int semop(int semid, struct sembuf *sops, unsigned nsops) Esegue le operazioni ordinarie su un semaforo o un insieme di semafori. La funzione restituisce 0 in caso di successo e -1 in caso di errore, nel qual caso errno assumer` a uno dei valori: EACCES EIDRM ENOMEM EAGAIN EINTR E2BIG ERANGE il processo non ha i privilegi per eseguire loperazione richiesta. linsieme di semafori ` stato cancellato. e si ` richiesto un SEM_UNDO ma il sistema non ha le risorse per allocare la struttura di e ripristino. unoperazione comporterebbe il blocco del processo, ma si ` specicato IPC_NOWAIT in e sem_flg. la funzione, bloccata in attesa dellesecuzione delloperazione, viene interrotta da un segnale. largomento nsops ` maggiore del numero massimo di operazioni SEMOPM. e per alcune operazioni il valore risultante del semaforo viene a superare il limite massimo SEMVMX.

ed inoltre EFAULT ed EINVAL.

La funzione permette di eseguire operazioni multiple sui singoli semafori di un insieme. La funzione richiede come primo argomento lidenticatore semid dellinsieme su cui si vuole operare. Il numero di operazioni da eettuare viene specicato con largomento nsop, mentre il loro contenuto viene passato con un puntatore ad un vettore di strutture sembuf nellargomento sops. Le operazioni richieste vengono eettivamente eseguite se e soltanto se ` possibile e eettuarle tutte quante.
struct sembuf { unsigned short int sem_num ; short int sem_op ; short int sem_flg ; };

/* semaphore number */ /* semaphore operation */ /* operation flag */

Figura 12.19: La struttura sembuf, usata per le operazioni sui semafori.

Il contenuto di ciascuna operazione deve essere specicato attraverso una opportuna struttura sembuf (la cui denizione ` riportata in g. 12.19) che il programma chiamante deve avere cura di e allocare in un opportuno vettore. La struttura permette di indicare il semaforo su cui operare, il tipo di operazione, ed un ag di controllo. Il campo sem_num serve per indicare a quale semaforo dellinsieme fa riferimento loperazione; si ricordi che i semafori sono numerati come in un vettore, per cui il primo semaforo corrisponde ad un valore nullo di sem_num. Il campo sem_flg ` un ag, mantenuto come maschera binaria, per il quale possono essere e impostati i due valori IPC_NOWAIT e SEM_UNDO. Impostando IPC_NOWAIT si fa si che, invece di

366

CAPITOLO 12. LA COMUNICAZIONE FRA PROCESSI

bloccarsi (in tutti quei casi in cui lesecuzione di una operazione richiede che il processo vada in stato di sleep), semop ritorni immediatamente con un errore di EAGAIN. Impostando SEM_UNDO si richiede invece che loperazione venga registrata in modo che il valore del semaforo possa essere ripristinato alluscita del processo. Inne sem_op ` il campo che controlla loperazione che viene eseguita e determina il come portamento della chiamata a semop; tre sono i casi possibili: sem_op> 0 In questo caso il valore di sem_op viene aggiunto al valore corrente di semval. La funzione ritorna immediatamente (con un errore di ERANGE qualora si sia superato il limite SEMVMX) ed il processo non viene bloccato in nessun caso. Specicando SEM_UNDO si aggiorna il contatore per il ripristino del valore del semaforo. Al processo chiamante ` richiesto il privilegio di alterazione (scrittura) sullinsieme di e semafori. sem_op= 0 Nel caso semval sia zero lesecuzione procede immediatamente. Se semval ` die verso da zero il comportamento ` controllato da sem_flg, se ` stato impostato e e IPC_NOWAIT la funzione ritorna con un errore di EAGAIN, altrimenti viene incrementato semzcnt di uno ed il processo resta in stato di sleep ntanto che non si ha una delle condizioni seguenti: semval diventa zero, nel qual caso semzcnt viene decrementato di uno. linsieme di semafori viene rimosso, nel qual caso semop ritorna un errore di EIDRM. il processo chiamante riceve un segnale, nel qual caso semzcnt viene decrementato di uno e semop ritorna un errore di EINTR. Al processo chiamante ` richiesto il privilegio di lettura dellinsieme dei semafori. e sem_op< 0 Nel caso in cui semval ` maggiore o uguale del valore assoluto di sem_op (se cio` la e e somma dei due valori resta positiva o nulla) i valori vengono sommati e la funzione ritorna immediatamente; qualora si sia impostato SEM_UNDO viene anche aggiornato il contatore per il ripristino del valore del semaforo. In caso contrario (quando cio` la somma darebbe luogo ad un valore di semval negativo) se si ` impostato e e IPC_NOWAIT la funzione ritorna con un errore di EAGAIN, altrimenti viene incrementato di uno semncnt ed il processo resta in stato di sleep ntanto che non si ha una delle condizioni seguenti: semval diventa maggiore o uguale del valore assoluto di sem_op, nel qual caso semncnt viene decrementato di uno, il valore di sem_op viene sommato a semval, e se era stato impostato SEM_UNDO viene aggiornato il contatore per il ripristino del valore del semaforo. linsieme di semafori viene rimosso, nel qual caso semop ritorna un errore di EIDRM. il processo chiamante riceve un segnale, nel qual caso semncnt viene decrementato di uno e semop ritorna un errore di EINTR. Al processo chiamante ` richiesto il privilegio di alterazione (scrittura) sullinsieme e di semafori. In caso di successo della funzione viene aggiornato il campo sempid per ogni semaforo modicato al valore del pid del processo chiamante; inoltre vengono pure aggiornati al tempo corrente i campi sem_otime e sem_ctime. Dato che, come gi` accennato in precedenza, in caso di uscita inaspettata i semafori possono a restare occupati, abbiamo visto come semop permetta di attivare un meccanismo di ripristino

12.2. IL SISTEMA DI COMUNICAZIONE FRA PROCESSI DI SYSTEM V

367

attraverso luso del ag SEM_UNDO. Il meccanismo ` implementato tramite una apposita struttura e sem_undo, associata ad ogni processo per ciascun semaforo che esso ha modicato; alluscita i semafori modicati vengono ripristinati, e le strutture disallocate. Per mantenere coerente il comportamento queste strutture non vengono ereditate attraverso una fork (altrimenti si avrebbe un doppio ripristino), mentre passano inalterate nellesecuzione di una exec (altrimenti non si avrebbe ripristino). Tutto questo per` ha un problema di fondo. Per capire di cosa si tratta occorre fare riferimeno to allimplementazione usata in Linux, che ` riportata in maniera semplicata nello schema di e g. 12.20. Si ` presa come riferimento larchitettura usata no al kernel 2.2.x che ` pi` semplice e e u (ed illustrata in dettaglio in [11]); nel kernel 2.4.x la struttura del SysV IPC ` stata modicata, e ma le denizioni relative a queste strutture restano per compatibilit`.27 a

Figura 12.20: Schema della struttura di un insieme di semafori.

Alla creazione di un nuovo insieme viene allocata una nuova strutture semid_ds ed il relativo vettore di strutture sem. Quando si richiede una operazione viene anzitutto vericato che tutte le operazioni possono avere successo; se una di esse comporta il blocco del processo il kernel crea una struttura sem_queue che viene aggiunta in fondo alla coda di attesa associata a ciascun insieme di semafori28 . Nella struttura viene memorizzato il riferimento alle operazioni richieste (nel campo sops, che ` un puntatore ad una struttura sembuf) e al processo corrente (nel campo sleeper) poi e questultimo viene messo stato di attesa e viene invocato lo scheduler per passare allesecuzione di un altro processo. Se invece tutte le operazioni possono avere successo queste vengono eseguite immediatamente, dopo di che il kernel esegue una scansione della coda di attesa (a partire da sem_pending) per vericare se qualcuna delle operazioni sospese in precedenza pu` essere eseguita, nel qual caso o
27 28

in particolare con le vecchie versioni delle librerie del C, come le libc5. che viene referenziata tramite i campi sem_pending e sem_pending_last di semid_ds.

368

CAPITOLO 12. LA COMUNICAZIONE FRA PROCESSI

la struttura sem_queue viene rimossa e lo stato del processo associato alloperazione (sleeper) viene riportato a running; il tutto viene ripetuto n quando non ci sono pi` operazioni eseguibili u o si ` svuotata la coda. Per gestire il meccanismo del ripristino tutte le volte che per unopee razione si ` specicato il ag SEM_UNDO viene mantenuta per ciascun insieme di semafori una e apposita struttura sem_undo che contiene (nel vettore puntato dal campo semadj) un valore di aggiustamento per ogni semaforo cui viene sommato lopposto del valore usato per loperazione. Queste strutture sono mantenute in due liste,29 una associata allinsieme di cui fa parte il semaforo, che viene usata per invalidare le strutture se questo viene cancellato o per azzerarle se si ` eseguita una operazione con semctl; laltra associata al processo che ha eseguito loperazione;30 e quando un processo termina, la lista ad esso associata viene scandita e le operazioni applicate al semaforo. Siccome un processo pu` accumulare delle richieste di ripristino per semafori dierenti o chiamate attraverso diverse chiamate a semop, si pone il problema di come eseguire il ripristino dei semafori alluscita del processo, ed in particolare se questo pu` essere fatto atomicamente. o Il punto ` cosa succede quando una delle operazioni previste per il ripristino non pu` essere e o eseguita immediatamente perch ad esempio il semaforo ` occupato; in tal caso infatti, se si e e pone il processo in stato di sleep aspettando la disponibilit` del semaforo (come faceva limplea mentazione originaria) si perde latomicit` delloperazione. La scelta fatta dal kernel ` pertanto a e quella di eettuare subito le operazioni che non prevedono un blocco del processo e di ignorare silenziosamente le altre; questo per` comporta il fatto che il ripristino non ` comunque garantito o e in tutte le occasioni. Come esempio di uso dellinterfaccia dei semafori vediamo come implementare con essa dei semplici mutex (cio` semafori binari), tutto il codice in questione, contenuto nel le Mutex.c e allegato ai sorgenti, ` riportato in g. 12.21. Utilizzeremo linterfaccia per creare un insieme e contenente un singolo semaforo, per il quale poi useremo un valore unitario per segnalare la disponibilit` della risorsa, ed un valore nullo per segnalarne lindisponibilit`. a a La prima funzione (2-15) ` MutexCreate che data una chiave crea il semaforo usato per e il mutex e lo inizializza, restituendone lidenticatore. Il primo passo (6) ` chiamare semget e con IPC_CREATE per creare il semaforo qualora non esista, assegnandogli i privilegi di lettura e scrittura per tutti. In caso di errore (7-9) si ritorna subito il risultato di semget, altrimenti (10) si inizializza il semaforo chiamando semctl con il comando SETVAL, utilizzando lunione semunion dichiarata ed avvalorata in precedenza (4) ad 1 per signicare che risorsa ` libera. e In caso di errore (11-13) si restituisce il valore di ritorno di semctl, altrimenti (14) si ritorna lidenticatore del semaforo. La seconda funzione (17-20) ` MutexFind, che, data una chiave, restituisce lidenticatore e del semaforo ad essa associato. La comprensione del suo funzionamento ` immediata in quanto e 31 di una chiamata a semget per cercare lidenticatore associato alla essa ` soltanto un wrapper e chiave, il valore di ritorno di questultima viene passato allindietro al chiamante. La terza funzione (22-25) ` MutexRead che, dato un identicatore, restituisce il valore del e semaforo associato al mutex. Anche in questo caso la funzione ` un wrapper per una chiamata e a semctl con il comando GETVAL, che permette di restituire il valore del semaforo. La quarta e la quinta funzione (36-44) sono MutexLock, e MutexUnlock, che permettono rispettivamente di bloccare e sbloccare il mutex. Entrambe fanno da wrapper per semop, utilizzando le due strutture sem_lock e sem_unlock denite in precedenza (27-34). Si noti come per queste ultime si sia fatto uso dellopzione SEM_UNDO per evitare che il semaforo resti bloccato in caso di terminazione imprevista del processo. Lultima funzione (46-49) della serie, ` MutexRemove, che rimuove il mutex. Anche in questo e
rispettivamente attraverso i due campi id_next e proc_next. attraverso il campo semundo di task_struct, come mostrato in 12.20. 31 si chiama cos` una funzione usata per fare da involucro alla chiamata di un altra, usata in genere per sempli care uninterfaccia (come in questo caso) o per utilizzare con la stessa funzione diversi substrati (librerie, ecc.) che possono fornire le stesse funzionalit`. a
30 29

12.2. IL SISTEMA DI COMUNICAZIONE FRA PROCESSI DI SYSTEM V

369

/* Function MutexCreate : create a mutex / semaphore */ int MutexCreate ( key_t ipc_key ) 3 { 4 const union semun semunion ={1}; /* semaphore union structure */ 5 int sem_id , ret ; 6 sem_id = semget ( ipc_key , 1 , IPC_CREAT |0666); /* get semaphore ID */ 7 if ( sem_id == -1) { /* if error return code */ 8 return sem_id ; 9 } 10 ret = semctl ( sem_id , 0 , SETVAL , semunion ); /* init semaphore */ 11 if ( ret == -1) { 12 return ret ; 13 } 14 return sem_id ; 15 } 16 /* Function MutexFind : get the semaphore / mutex Id given the IPC key value */ 17 int MutexFind ( key_t ipc_key ) 18 { 19 return semget ( ipc_key ,1 ,0); 20 } 21 /* Function MutexRead : read the current value of the mutex / semaphore */ 22 int MutexRead ( int sem_id ) 23 { 24 return semctl ( sem_id , 0 , GETVAL ); 25 } 26 /* Define sembuf structures to lock and unlock the semaphore */ 27 struct sembuf sem_lock ={ /* to lock semaphore */ 28 0, /* semaphore number ( only one so 0) */ 29 -1 , /* operation ( -1 to use resource ) */ 30 SEM_UNDO }; /* flag ( set for undo at exit ) */ 31 struct sembuf sem_ulock ={ /* to unlock semaphore */ 32 0, /* semaphore number ( only one so 0) */ 33 1, /* operation (1 to release resource ) */ 34 SEM_UNDO }; /* flag ( in this case 0) */ 35 /* Function MutexLock : to lock a mutex / semaphore */ 36 int MutexLock ( int sem_id ) 37 { 38 return semop ( sem_id , & sem_lock , 1); 39 } 40 /* Function MutexUnlock : to unlock a mutex / semaphore */ 41 int MutexUnlock ( int sem_id ) 42 { 43 return semop ( sem_id , & sem_ulock , 1); 44 } 45 /* Function MutexRemove : remove a mutex / semaphore */ 46 int MutexRemove ( int sem_id ) 47 { 48 return semctl ( sem_id , 0 , IPC_RMID ); 49 }
1 2

Figura 12.21: Il codice delle funzioni che permettono di creare o recuperare lidenticatore di un semaforo da utilizzare come mutex.

caso si ha un wrapper per una chiamata a semctl con il comando IPC_RMID, che permette di cancellare il semaforo; il valore di ritorno di questultima viene passato allindietro. Chiamare MutexLock decrementa il valore del semaforo: se questo ` libero (ha gi` valore 1) e a sar` bloccato (valore nullo), se ` bloccato la chiamata a semop si bloccher` ntanto che la risorsa a e a non venga rilasciata. Chiamando MutexUnlock il valore del semaforo sar` incrementato di uno, a

370

CAPITOLO 12. LA COMUNICAZIONE FRA PROCESSI

sbloccandolo qualora fosse bloccato. Si noti che occorre eseguire sempre prima MutexLock e poi MutexUnlock, perch se per e un qualche errore si esegue pi` volte questultima il valore del semaforo crescerebbe oltre 1, e u MutexLock non avrebbe pi` leetto aspettato (bloccare la risorsa quando questa ` considerata u e libera). Inne si tenga presente che usare MutexRead per controllare il valore dei mutex prima di proseguire in una operazione di sblocco non servirebbe comunque, dato che loperazione non sarebbe atomica. Vedremo in sez. 12.3.3 come sia possibile ottenere uninterfaccia analoga a quella appena illustrata, senza incorrere in questi problemi, usando il le locking.

12.2.6

Memoria condivisa

Il terzo oggetto introdotto dal SysV IPC ` quello dei segmenti di memoria condivisa. La funzione e che permette di ottenerne uno ` shmget, ed il suo prototipo `: e e
#include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> int shmget(key_t key, int size, int flag) Restituisce lidenticatore di una memoria condivisa. La funzione restituisce lidenticatore (un intero positivo) o -1 in caso di errore, nel qual caso errno assumer` i valori: a ENOSPC si ` superato il limite (SHMMNI) sul numero di segmenti di memoria nel sistema, o e cercato di allocare un segmento le cui dimensioni fanno superare il limite di sistema (SHMALL) per la memoria ad essi riservata. si ` richiesta una dimensione per un nuovo segmento maggiore di SHMMAX o minore di e SHMMIN, o se il segmento gi` esiste size ` maggiore delle sue dimensioni. a e il sistema non ha abbastanza memoria per poter contenere le strutture per un nuovo segmento di memoria condivisa.

EINVAL ENOMEM

ed inoltre EACCES, ENOENT, EEXIST, EIDRM, con lo stesso signicato che hanno per msgget.

La funzione, come semget, ` del tutto analoga a msgget, ed identico ` luso degli argomenti e e key e flag per cui non ripeteremo quanto detto al proposito in sez. 12.2.4. Largomento size specica invece la dimensione, in byte, del segmento, che viene comunque arrotondata al multiplo superiore di PAGE_SIZE. La memoria condivisa ` la forma pi` veloce di comunicazione fra due processi, in quanto e u permette agli stessi di vedere nel loro spazio di indirizzi una stessa sezione di memoria. Pertanto non ` necessaria nessuna operazione di copia per trasmettere i dati da un processo allaltro, in e quanto ciascuno pu` accedervi direttamente con le normali operazioni di lettura e scrittura dei o dati in memoria. Ovviamente tutto questo ha un prezzo, ed il problema fondamentale della memoria condivisa ` ` la sincronizzazione degli accessi. E evidente infatti che se un processo deve scambiare dei dati e con un altro, si deve essere sicuri che questultimo non acceda al segmento di memoria condivisa prima che il primo non abbia completato le operazioni di scrittura, inoltre nel corso di una lettura si deve essere sicuri che i dati restano coerenti e non vengono sovrascritti da un accesso in scrittura sullo stesso segmento da parte di un altro processo. Per questo in genere la memoria condivisa viene sempre utilizzata in abbinamento ad un meccanismo di sincronizzazione, il che, di norma, signica insieme a dei semafori. A ciascun segmento di memoria condivisa ` associata una struttura shmid_ds, riportata in e g. 12.22. Come nel caso delle code di messaggi quando si crea un nuovo segmento di memoria condivisa con shmget questa struttura viene inizializzata, in particolare il campo shm_perm viene inizializzato come illustrato in sez. 12.2.2, e valgono le considerazioni ivi fatte relativamente ai permessi di accesso; per quanto riguarda gli altri campi invece:

12.2. IL SISTEMA DI COMUNICAZIONE FRA PROCESSI DI SYSTEM V

371

struct shmid_ds { struct ipc_perm shm_perm ; int shm_segsz ; time_t shm_atime ; time_t shm_dtime ; time_t shm_ctime ; unsigned short shm_cpid ; unsigned short shm_lpid ; short shm_nattch ; };

/* /* /* /* /* /* /* /*

operation perms */ size of segment ( bytes ) */ last attach time */ last detach time */ last change time */ pid of creator */ pid of last operator */ no . of current attaches */

Figura 12.22: La struttura shmid_ds, associata a ciascun segmento di memoria condivisa.

il campo shm_segsz, che esprime la dimensione del segmento, viene inizializzato al valore di size. il campo shm_ctime, che esprime il tempo di creazione del segmento, viene inizializzato al tempo corrente. i campi shm_atime e shm_dtime, che esprimono rispettivamente il tempo dellultima volta che il segmento ` stato agganciato o sganciato da un processo, vengono inizializzati a zero. e il campo shm_lpid, che esprime il pid del processo che ha eseguito lultima operazione, viene inizializzato a zero. il campo shm_cpid, che esprime il pid del processo che ha creato il segmento, viene inizializzato al pid del processo chiamante. il campo shm_nattac, che esprime il numero di processi agganciati al segmento viene inizializzato a zero. Come per le code di messaggi e gli insiemi di semafori, anche per i segmenti di memoria condivisa esistono una serie di limiti imposti dal sistema. Alcuni di questi limiti sono al solito accessibili e modicabili attraverso sysctl o scrivendo direttamente nei rispettivi le di /proc/sys/kernel/. In tab. 12.4 si sono riportate le costanti simboliche associate a ciascuno di essi, il loro signicato, i valori preimpostati, e, quando presente, il le in /proc/sys/kernel/ che permettono di cambiarne il valore. Al solito la funzione che permette di eettuare le operazioni di controllo su un segmento di memoria condivisa ` shmctl; il suo prototipo `: e e
#include <sys/ipc.h> #include <sys/shm.h> int shmctl(int shmid, int cmd, struct shmid_ds *buf) Esegue le operazioni di controllo su un segmento di memoria condivisa. La funzione restituisce 0 in caso di successo e -1 in caso di errore, nel qual caso errno assumer` i a valori: EACCES EINVAL EIDRM EPERM si ` richiesto IPC_STAT ma i permessi non consentono laccesso in lettura al segmento. e o shmid non ` un identicatore valido o cmd non ` un comando valido. e e largomento shmid fa riferimento ad un segmento che ` stato cancellato. e si ` specicato un comando con IPC_SET o IPC_RMID senza i permessi necessari. e

EOVERFLOW si ` tentato il comando IPC_STAT ma il valore del group-ID o delluser-ID ` troppo e e grande per essere memorizzato nella struttura puntata da buf. EFAULT lindirizzo specicato con buf non ` valido. e

372
Costante SHMALL SHMMAX SHMMNI SHMMIN SHMLBA Valore 0x200000 0x2000000 4096 1 PAGE_SIZE

CAPITOLO 12. LA COMUNICAZIONE FRA PROCESSI


File in proc shmall shmmax msgmni Signicato Numero massimo di pagine che possono essere usate per i segmenti di memoria condivisa. Dimensione massima di un segmento di memoria condivisa. Numero massimo di segmenti di memoria condivisa presenti nel kernel. Dimensione minima di un segmento di memoria condivisa. Limite inferiore per le dimensioni minime di un segmento (deve essere allineato alle dimensioni di una pagina di memoria). Numero massimo di segmenti di memoria condivisa per ciascun processo.

SHMSEG

Tabella 12.4: Valori delle costanti associate ai limiti dei segmenti di memoria condivisa, insieme al relativo le in /proc/sys/kernel/ ed al valore preimpostato presente nel sistema.

Il comando specicato attraverso largomento cmd determina i diversi eetti della funzione; i possibili valori che esso pu` assumere, ed il corrispondente comportamento della funzione, sono o i seguenti: IPC_STAT Legge le informazioni riguardo il segmento di memoria condivisa nella struttura shmid_ds puntata da buf. Occorre che il processo chiamante abbia il permesso di lettura sulla segmento. Marca il segmento di memoria condivisa per la rimozione, questo verr` cancellato a eettivamente solo quando lultimo processo ad esso agganciato si sar` staccaa to. Questo comando pu` essere eseguito solo da un processo con user-ID eettio vo corrispondente o al creatore del segmento, o al proprietario del segmento, o allamministratore. Permette di modicare i permessi ed il proprietario del segmento. Per modicare i valori di shm_perm.mode, shm_perm.uid e shm_perm.gid occorre essere il proprietario o il creatore del segmento, oppure lamministratore. Compiuta loperazione aggiorna anche il valore del campo shm_ctime. Abilita il memory locking 32 sul segmento di memoria condivisa. Solo lamministratore pu` utilizzare questo comando. o

IPC_RMID

IPC_SET

SHM_LOCK

SHM_UNLOCK Disabilita il memory locking sul segmento di memoria condivisa. Solo lamministratore pu` utilizzare questo comando. o i primi tre comandi sono gli stessi gi` visti anche per le code di messaggi e gli insiemi di semafori, a gli ultimi due sono delle estensioni speciche previste da Linux, che permettono di abilitare e disabilitare il meccanismo della memoria virtuale per il segmento. Largomento buf viene utilizzato solo con i comandi IPC_STAT e IPC_SET nel qual caso esso dovr` puntare ad una struttura shmid_ds precedentemente allocata, in cui nel primo caso a saranno scritti i dati del segmento di memoria restituiti dalla funzione e da cui, nel secondo caso, verranno letti i dati da impostare sul segmento. Una volta che lo si ` creato, per utilizzare un segmento di memoria condivisa linterfaccia e prevede due funzioni, shmat e shmdt. La prima di queste serve ad agganciare un segmento al
impedisce cio` che la memoria usata per il segmento venga salvata su disco dal meccanismo della memoria e virtuale; si ricordi quanto trattato in sez. 2.2.4.
32

12.2. IL SISTEMA DI COMUNICAZIONE FRA PROCESSI DI SYSTEM V

373

processo chiamante, in modo che questultimo possa inserirlo nel suo spazio di indirizzi per potervi accedere; il suo prototipo `: e
#include <sys/types.h> #include <sys/shm.h> void *shmat(int shmid, const void *shmaddr, int shmflg) Aggancia al processo un segmento di memoria condivisa. La funzione restituisce lindirizzo del segmento in caso di successo, e -1 in caso di errore, nel qual caso errno assumer` i valori: a EACCES EINVAL il processo non ha i privilegi per accedere al segmento nella modalit` richiesta. a si ` specicato un identicatore invalido per shmid, o un indirizzo non allineato sul e conne di una pagina per shmaddr.

ed inoltre ENOMEM.

La funzione inserisce un segmento di memoria condivisa allinterno dello spazio di indirizzi del processo, in modo che questo possa accedervi direttamente, la situazione dopo lesecuzione di shmat ` illustrata in g. 12.23 (per la comprensione del resto dello schema si ricordi quanto e illustrato al proposito in sez. 2.2.2). In particolare lindirizzo nale del segmento dati (quello impostato da brk, vedi sez. 2.2.3) non viene inuenzato. Si tenga presente inne che la funzione ha successo anche se il segmento ` stato marcato per la cancellazione. e

Figura 12.23: Disposizione dei segmenti di memoria di un processo quando si ` agganciato un segmento di e memoria condivisa.

Largomento shmaddr specica a quale indirizzo33 deve essere associato il segmento, se il valore specicato ` NULL ` il sistema a scegliere opportunamente unarea di memoria libera e e (questo ` il modo pi` portabile e sicuro di usare la funzione). Altrimenti il kernel aggancia il e u segmento allindirizzo specicato da shmaddr; questo per` pu` avvenire solo se lindirizzo coincide o o
lo standard SVID prevede che largomento shmaddr sia di tipo char *, cos` come il valore di ritorno della funzione; in Linux ` stato cos` con le libc4 e le libc5, con il passaggio alle glibc il tipo di shmaddr ` divenuto un e e const void * e quello del valore di ritorno un void *.
33

374

CAPITOLO 12. LA COMUNICAZIONE FRA PROCESSI

con il limite di una pagina, cio` se ` un multiplo esatto del parametro di sistema SHMLBA, che in e e Linux ` sempre uguale PAGE_SIZE. e Si tenga presente per` che quando si usa NULL come valore di shmaddr, lindirizzo restituito da o shmat pu` cambiare da processo a processo; pertanto se nellarea di memoria condivisa si salvano o anche degli indirizzi, si deve avere cura di usare valori relativi (in genere riferiti allindirizzo di partenza del segmento). Largomento shmflg permette di cambiare il comportamento della funzione; esso va specicato come maschera binaria, i bit utilizzati sono solo due e sono identicati dalle costanti SHM_RND e SHM_RDONLY, che vanno combinate con un OR aritmetico. Specicando SHM_RND si evita che shmat ritorni un errore quando shmaddr non ` allineato ai conni di una pagina. Si e pu` quindi usare un valore qualunque per shmaddr, e il segmento verr` comunque agganciato, o a ma al pi` vicino multiplo di SHMLBA (il nome della costante sta infatti per rounded, e serve per u specicare un indirizzo come arrotondamento, in Linux ` equivalente a PAGE_SIZE). e Luso di SHM_RDONLY permette di agganciare il segmento in sola lettura (si ricordi che anche le pagine di memoria hanno dei permessi), in tal caso un tentativo di scrivere sul segmento comporter` una violazione di accesso con lemissione di un segnale di SIGSEGV. Il comportamento a usuale di shmat ` quello di agganciare il segmento con laccesso in lettura e scrittura (ed il e processo deve aver questi permessi in shm_perm), non ` prevista la possibilit` di agganciare un e a segmento in sola scrittura. In caso di successo la funzione aggiorna anche i seguenti campi di shmid_ds: il tempo shm_atime dellultima operazione di aggancio viene impostato al tempo corrente. il pid shm_lpid dellultimo processo che ha operato sul segmento viene impostato a quello del processo corrente. il numero shm_nattch di processi agganciati al segmento viene aumentato di uno. Come accennato in sez. 3.2.2 un segmento di memoria condivisa agganciato ad un processo viene ereditato da un glio attraverso una fork, dato che questultimo riceve una copia dello spazio degli indirizzi del padre. Invece, dato che attraverso una exec viene eseguito un diverso programma con uno spazio di indirizzi completamente diverso, tutti i segmenti agganciati al processo originario vengono automaticamente sganciati. Lo stesso avviene alluscita del processo attraverso una exit. Una volta che un segmento di memoria condivisa non serve pi`, si pu` sganciarlo esplicitau o mente dal processo usando laltra funzione dellinterfaccia, shmdt, il cui prototipo `: e
#include <sys/types.h> #include <sys/shm.h> int shmdt(const void *shmaddr) Sgancia dal processo un segmento di memoria condivisa. La funzione restituisce 0 in caso di successo, e -1 in caso di errore, la funzione fallisce solo quando non c` un segmento agganciato allindirizzo shmaddr, con errno che assume il valore EINVAL. e

La funzione sgancia dallo spazio degli indirizzi del processo un segmento di memoria condivisa; questo viene identicato con lindirizzo shmaddr restituito dalla precedente chiamata a shmat con il quale era stato agganciato al processo. In caso di successo la funzione aggiorna anche i seguenti campi di shmid_ds: il tempo shm_dtime dellultima operazione di sganciamento viene impostato al tempo corrente. il pid shm_lpid dellultimo processo che ha operato sul segmento viene impostato a quello del processo corrente. il numero shm_nattch di processi agganciati al segmento viene decrementato di uno.

12.2. IL SISTEMA DI COMUNICAZIONE FRA PROCESSI DI SYSTEM V

375

/* Function ShmCreate Create a SysV shared memory segment */ void * ShmCreate ( key_t ipc_key , int shm_size , int perm , int fill ) 3 { 4 void * shm_ptr ; 5 int shm_id ; /* ID of the IPC shared memory segment */ 6 shm_id = shmget ( ipc_key , shm_size , IPC_CREAT | perm ); /* get shm ID */ 7 if ( shm_id < 0) { 8 return NULL ; 9 } 10 shm_ptr = shmat ( shm_id , NULL , 0); /* map it into memory */ 11 if ( shm_ptr < 0) { 12 return NULL ; 13 } 14 memset (( void *) shm_ptr , fill , shm_size ); /* fill segment */ 15 return shm_ptr ; 16 } 17 /* Function ShmFind : Find a SysV shared memory segment */ 18 void * ShmFind ( key_t ipc_key , int shm_size ) 19 { 20 void * shm_ptr ; 21 int shm_id ; /* ID of the SysV shared memory segment */ 22 shm_id = shmget ( ipc_key , shm_size , 0); /* find shared memory ID */ 23 if ( shm_id < 0) { 24 return NULL ; 25 } 26 shm_ptr = shmat ( shm_id , NULL , 0); /* map it into memory */ 27 if ( shm_ptr < 0) { 28 return NULL ; 29 } 30 return shm_ptr ; 31 } 32 /* Function ShmRemove : Schedule removal for a SysV shared memory segment */ 33 int ShmRemove ( key_t ipc_key , void * shm_ptr ) 34 { 35 int shm_id ; /* ID of the SysV shared memory segment */ 36 /* first detach segment */ 37 if ( shmdt ( shm_ptr ) < 0) { 38 return -1; 39 } 40 /* schedule segment removal */ 41 shm_id = shmget ( ipc_key , 0 , 0); /* find shared memory ID */ 42 if ( shm_id < 0) { 43 if ( errno == EIDRM ) return 0; 44 return -1; 45 } 46 if ( shmctl ( shm_id , IPC_RMID , NULL ) < 0) { /* ask for removal */ 47 if ( errno == EIDRM ) return 0; 48 return -1; 49 } 50 return 0; 51 }
1 2

Figura 12.24: Il codice delle funzioni che permettono di creare, trovare e rimuovere un segmento di memoria condivisa.

inoltre la regione di indirizzi usata per il segmento di memoria condivisa viene tolta dallo spazio di indirizzi del processo. Come esempio di uso di queste funzioni vediamo come implementare una serie di funzioni di

376

CAPITOLO 12. LA COMUNICAZIONE FRA PROCESSI

libreria che ne semplichino luso, automatizzando le operazioni pi` comuni; il codice, contenuto u nel le SharedMem.c, ` riportato in g. 12.24. e La prima funzione (3-16) ` ShmCreate che, data una chiave, crea il segmento di memoria e condivisa restituendo il puntatore allo stesso. La funzione comincia (6) con il chiamare shmget, usando il ag IPC_CREATE per creare il segmento qualora non esista, ed assegnandogli i privilegi specicati dallargomento perm e la dimensione specicata dallargomento shm_size. In caso di errore (7-9) si ritorna immediatamente un puntatore nullo, altrimenti (10) si prosegue agganciando il segmento di memoria condivisa al processo con shmat. In caso di errore (11-13) si restituisce di nuovo un puntatore nullo, inne (14) si inizializza con memset il contenuto del segmento al valore costante specicato dallargomento fill, e poi si ritorna il puntatore al segmento stesso. La seconda funzione (17-31) ` ShmFind, che, data una chiave, restituisce lindirizzo del sege mento ad essa associato. Anzitutto (22) si richiede lidenticatore del segmento con shmget, ritornando (23-25) un puntatore nullo in caso di errore. Poi si prosegue (26) agganciando il segmento al processo con shmat, restituendo (27-29) di nuovo un puntatore nullo in caso di errore, se invece non ci sono errori si restituisce il puntatore ottenuto da shmat. La terza funzione (32-51) ` ShmRemove che, data la chiave ed il puntatore associati al segmento e di memoria condivisa, prima lo sgancia dal processo e poi lo rimuove. Il primo passo (37) ` la e chiamata a shmdt per sganciare il segmento, restituendo (38-39) un valore -1 in caso di errore. Il passo successivo (41) ` utilizzare shmget per ottenere lidenticatore associato al segmento data e la chiave key. Al solito si restituisce un valore di -1 (42-45) in caso di errore, mentre se tutto va bene si conclude restituendo un valore nullo. Bench la memoria condivisa costituisca il meccanismo di intercomunicazione fra processi pi` e u veloce, essa non ` sempre il pi` appropriato, dato che, come abbiamo visto, si avr` comunque la e u a necessit` di una sincronizzazione degli accessi. Per questo motivo, quando la comunicazione fra a processi ` sequenziale, altri meccanismi come le pipe, le fo o i socket, che non necessitano di e sincronizzazione esplicita, sono da preferire. Essa diventa lunico meccanismo possibile quando la comunicazione non ` sequenziale34 o quando non pu` avvenire secondo una modalit` predenita. e o a Un esempio classico di uso della memoria condivisa ` quello del monitor , in cui viene per e scambiare informazioni fra un processo server, che vi scrive dei dati di interesse generale che ha ottenuto, e i processi client interessati agli stessi dati che cos` possono leggerli in maniera completamente asincrona. Con questo schema di funzionamento da una parte si evita che ciascun processo client debba compiere loperazione, potenzialmente onerosa, di ricavare e trattare i dati, e dallaltra si evita al processo server di dover gestire linvio a tutti i client di tutti i dati (non potendo il server sapere quali di essi servono eettivamente al singolo client). Nel nostro caso implementeremo un monitor di una directory: un processo si incaricher` a di tenere sotto controllo alcuni parametri relativi ad una directory (il numero dei le contenuti, la dimensione totale, quante directory, link simbolici, le normali, ecc.) che saranno salvati in un segmento di memoria condivisa cui altri processi potranno accedere per ricavare la parte di informazione che interessa. In g. 12.25 si ` riportata la sezione principale del corpo del programma server, insieme alle e denizioni delle altre funzioni usate nel programma e delle variabili globali, omettendo tutto quello che riguarda la gestione delle opzioni e la stampa delle istruzioni di uso a video; al solito il codice completo si trova con i sorgenti allegati nel le DirMonitor.c. Il programma usa delle variabili globali (2-14) per mantenere i valori relativi agli oggetti usati per la comunicazione inter-processo; si ` denita inoltre una apposita struttura DirProp e che contiene i dati relativi alle propriet` che si vogliono mantenere nella memoria condivisa, per a laccesso da parte dei client. Il programma, dopo la sezione, omessa, relativa alla gestione delle opzioni da riga di comando
come accennato in sez. 12.2.4 per la comunicazione non sequenziale si possono usare le code di messaggi, attraverso luso del campo mtype, ma solo se questultima pu` essere eettuata in forma di messaggio. o
34

12.2. IL SISTEMA DI COMUNICAZIONE FRA PROCESSI DI SYSTEM V

377

/* global variables for shared memory segment */ struct DirProp { 3 int tot_size ; 4 int tot_files ; 5 int tot_regular ; 6 int tot_fifo ; 7 int tot_link ; 8 int tot_dir ; 9 int tot_block ; 10 int tot_char ; 11 int tot_sock ; 12 } * shmptr ; 13 key_t key ; 14 int mutex ; 15 /* main body */ 16 int main ( int argc , char * argv []) 17 { 18 int i , pause = 10; 19 ... 20 if (( argc - optind ) != 1) { /* There must be remaing parameters */ 21 printf ( " Wrong number of arguments % d \ n " , argc - optind ); 22 usage (); 23 } 24 if ( chdir ( argv [1])) { /* chdir to be sure dir exist */ 25 perror ( " Cannot find directory to monitor " ); 26 } 27 Signal ( SIGTERM , HandSIGTERM ); /* set handlers for termination */ 28 Signal ( SIGINT , HandSIGTERM ); 29 Signal ( SIGQUIT , HandSIGTERM ); 30 key = ftok ( " ~/ gapil / sources / DirMonitor . c " , 1); /* define a key */ 31 shmptr = ShmCreate ( key , 4096 , 0666 , 0); /* get a shared memory segment */ 32 if (! shmptr ) { 33 perror ( " Cannot create shared memory " ); 34 exit (1); 35 } 36 if (( mutex = MutexCreate ( key )) == -1) { /* get a Mutex */ 37 perror ( " Cannot create mutex " ); 38 exit (1); 39 } 40 /* main loop , monitor directory properties each 10 sec */ 41 daemon (1 , 0); /* demonize process , staying in monitored dir */ 42 while (1) { 43 MutexLock ( mutex ); /* lock shared memory */ 44 memset ( shmptr , 0 , sizeof ( struct DirProp )); /* erase previous data */ 45 DirScan ( argv [1] , ComputeValues ); /* execute scan */ 46 MutexUnlock ( mutex ); /* unlock shared memory */ 47 sleep ( pause ); /* sleep until next watch */ 48 } 49 }
1 2

Figura 12.25: Codice della funzione principale del programma DirMonitor.c.

(che si limitano alla eventuale stampa di un messaggio di aiuto a video ed allimpostazione della durata dellintervallo con cui viene ripetuto il calcolo delle propriet` della directory) controlla a (20-23) che sia stato specicato largomento necessario contenente il nome della directory da tenere sotto controllo, senza il quale esce immediatamente con un messaggio di errore. Poi, per vericare che largomento specichi eettivamente una directory, si esegue (24-26) su di esso una chdir, uscendo immediatamente in caso di errore. Questa funzione serve anche

378

CAPITOLO 12. LA COMUNICAZIONE FRA PROCESSI

per impostare la directory di lavoro del programma nella directory da tenere sotto controllo, in vista del successivo uso della funzione daemon.35 Inne (27-29) si installano i gestori per i vari segnali di terminazione che, avendo a che fare con un programma che deve essere eseguito come server, sono il solo strumento disponibile per concluderne lesecuzione. Il passo successivo (30-39) ` quello di creare gli oggetti di intercomunicazione necessari. e Si inizia costruendo (30) la chiave da usare come riferimento con il nome del programma,36 dopo di che si richiede (31) la creazione di un segmento di memoria condivisa con usando la funzione ShmCreate illustrata in precedenza (una pagina di memoria ` suciente per i dati e che useremo), uscendo (32-35) qualora la creazione ed il successivo agganciamento al processo non abbia successo. Con lindirizzo shmptr cos` ottenuto potremo poi accedere alla memoria condivisa, che, per come abbiamo lo abbiamo denito, sar` vista nella forma data da DirProp. a Inne (36-39) utilizzando sempre la stessa chiave, si crea, tramite le funzioni di interfaccia gi` a descritte in sez. 12.2.5, anche un mutex, che utilizzeremo per regolare laccesso alla memoria condivisa.
/* Routine to compute directory properties inside DirScan */ int ComputeValues ( struct dirent * direntry ) 3 { 4 struct stat data ; 5 stat ( direntry - > d_name , & data ); /* get stat data */ 6 shmptr - > tot_size += data . st_size ; 7 shmptr - > tot_files ++; 8 if ( S_ISREG ( data . st_mode )) shmptr - > tot_regular ++; 9 if ( S_ISFIFO ( data . st_mode )) shmptr - > tot_fifo ++; 10 if ( S_ISLNK ( data . st_mode )) shmptr - > tot_link ++; 11 if ( S_ISDIR ( data . st_mode )) shmptr - > tot_dir ++; 12 if ( S_ISBLK ( data . st_mode )) shmptr - > tot_block ++; 13 if ( S_ISCHR ( data . st_mode )) shmptr - > tot_char ++; 14 if ( S_ISSOCK ( data . st_mode )) shmptr - > tot_sock ++; 15 return 0; 16 } 17 /* Signal Handler to manage termination */ 18 void HandSIGTERM ( int signo ) { 19 MutexLock ( mutex ); 20 ShmRemove ( key , shmptr ); 21 MutexRemove ( mutex ); 22 exit (0); 23 }
1 2

Figura 12.26: Codice delle funzioni ausiliarie usate da DirMonitor.c.

Completata linizializzazione e la creazione degli oggetti di intercomunicazione il programma entra nel ciclo principale (40-49) dove vengono eseguite indenitamente le attivit` di monitoraga gio. Il primo passo (41) ` eseguire daemon per proseguire con lesecuzione in background come e si conviene ad un programma demone; si noti che si ` mantenuta, usando un valore non nullo e del primo argomento, la directory di lavoro corrente. Una volta che il programma ` andato in e background lesecuzione prosegue (42-48) allinterno di un ciclo innito: si inizia (43) bloccando il mutex con MutexLock per poter accedere alla memoria condivisa (la funzione si bloccher` a automaticamente se qualche client sta leggendo), poi (44) si cancellano i valori precedentemente immagazzinati nella memoria condivisa con memset, e si esegue (45) un nuovo calcolo degli stessi
si noti come si ` potuta fare questa scelta, nonostante le indicazioni illustrate in sez. 10.1.5, per il particolare e scopo del programma, che necessita comunque di restare allinterno di una directory. 36 si ` usato un riferimento relativo alla home dellutente, supposto che i sorgenti di GaPiL siano stati installati e direttamente in essa. Qualora si eettui una installazione diversa si dovr` correggere il programma. a
35

12.2. IL SISTEMA DI COMUNICAZIONE FRA PROCESSI DI SYSTEM V

379

utilizzando la funzione DirScan; inne (46) si sblocca il mutex con MutexUnlock, e si attende (47) per il periodo di tempo specicato a riga di comando con lopzione -p con una sleep. Si noti come per il calcolo dei valori da mantenere nella memoria condivisa si sia usata ancora una volta la funzione DirScan, gi` utilizzata (e descritta in dettaglio) in sez. 5.1.6, che a ci permette di eettuare la scansione delle voci della directory, chiamando per ciascuna di esse la funzione ComputeValues, che esegue tutti i calcoli necessari. Il codice di questultima ` riportato in g. 12.26. Come si vede la funzione (2-16) ` molto e e semplice e si limita a chiamare (5) la funzione stat sul le indicato da ciascuna voce, per ottenerne i dati, che poi utilizza per incrementare i vari contatori nella memoria condivisa, cui accede grazie alla variabile globale shmptr. Dato che la funzione ` chiamata da DirScan, si ` allinterno del ciclo principale del proe e gramma, con un mutex acquisito, perci` non ` necessario eettuare nessun controllo e si pu` o e o accedere direttamente alla memoria condivisa usando shmptr per riempire i campi della struttura DirProp; cos` prima (6-7) si sommano le dimensioni dei le ed il loro numero, poi, utilizzando le macro di tab. 5.3, si contano (8-14) quanti ce ne sono per ciascun tipo. In g. 12.26 ` riportato anche il codice (17-23) del gestore dei segnali di terminazione, usato e per chiudere il programma. Esso, oltre a provocare luscita del programma, si incarica anche di cancellare tutti gli oggetti di intercomunicazione non pi` necessari. Per questo anzitutto (19) u acquisisce il mutex con MutexLock, per evitare di operare mentre un client sta ancora leggendo i dati, dopo di che (20) distacca e rimuove il segmento di memoria condivisa usando ShmRemove. Inne (21) rimuove il mutex con MutexRemove ed esce (22).
int main ( int argc , char * argv []) { 3 key_t key ; 4 ... 5 /* create needed IPC objects */ 6 key = ftok ( " ~/ gapil / sources / DirMonitor . c " , 1); /* define a key */ 7 if (!( shmptr = ShmFind ( key , 4096))) { /* get a shared memory segment */ 8 perror ( " Cannot find shared memory " ); 9 exit (1); 10 } 11 if (( mutex = MutexFind ( key )) == -1) { /* get the Mutex */ 12 perror ( " Cannot find mutex " ); 13 exit (1); 14 } 15 /* main loop */ 16 MutexLock ( mutex ); /* lock shared memory */ 17 printf ( " Ci sono % d file dati \ n " , shmptr - > tot_regular ); 18 printf ( " Ci sono % d directory \ n " , shmptr - > tot_dir ); 19 printf ( " Ci sono % d link \ n " , shmptr - > tot_link ); 20 printf ( " Ci sono % d fifo \ n " , shmptr - > tot_fifo ); 21 printf ( " Ci sono % d socket \ n " , shmptr - > tot_sock ); 22 printf ( " Ci sono % d device a caratteri \ n " , shmptr - > tot_char ); 23 printf ( " Ci sono % d device a blocchi \ n " , shmptr - > tot_block ); 24 printf ( " Totale % d file , per % d byte \ n " , 25 shmptr - > tot_files , shmptr - > tot_size ); 26 MutexUnlock ( mutex ); /* unlock shared memory */ 27 }
1 2

Figura 12.27: Codice del programma client del monitor delle propriet` di una directory, ReadMonitor.c. a

Il codice del client usato per leggere le informazioni mantenute nella memoria condivisa ` e riportato in g. 12.27. Al solito si ` omessa la sezione di gestione delle opzioni e la funzione che e stampa a video le istruzioni; il codice completo ` nei sorgenti allegati, nel le ReadMonitor.c. e

380

CAPITOLO 12. LA COMUNICAZIONE FRA PROCESSI

Una volta conclusa la gestione delle opzioni a riga di comando il programma rigenera (7) con ftok la stessa chiave usata dal server per identicare il segmento di memoria condivisa ed il mutex, poi (8) richiede con ShmFind lindirizzo della memoria condivisa agganciando al contempo il segmento al processo, Inne (17-20) con MutexFind si richiede lidenticatore del mutex. Completata linizializzazione ed ottenuti i riferimenti agli oggetti di intercomunicazione necessari viene eseguito il corpo principale del programma (21-33); si comincia (22) acquisendo il mutex con MutexLock; qui avviene il blocco del processo se la memoria condivisa non ` disponibile. Poi e (23-31) si stampano i vari valori mantenuti nella memoria condivisa attraverso luso di shmptr. Inne (41) con MutexUnlock si rilascia il mutex, prima di uscire. Verichiamo allora il funzionamento dei nostri programmi; al solito, usando le funzioni di libreria occorre denire opportunamente LD_LIBRARY_PATH; poi si potr` lanciare il server con: a [piccardi@gont sources]$ ./dirmonitor ./ ed avendo usato daemon il comando ritorner` immediatamente. Una volta che il server ` in a e esecuzione, possiamo passare ad invocare il client per vericarne i risultati, in tal caso otterremo: [piccardi@gont sources]$ ./readmon Ci sono 68 file dati Ci sono 3 directory Ci sono 0 link Ci sono 0 fifo Ci sono 0 socket Ci sono 0 device a caratteri Ci sono 0 device a blocchi Totale 71 file, per 489831 byte ed un rapido calcolo (ad esempio con ls -a | wc per contare i le) ci permette di vericare che il totale dei le ` giusto. Un controllo con ipcs ci permette inoltre di vericare la presenza di e un segmento di memoria condivisa e di un semaforo: [piccardi@gont sources]$ ipcs ------ Shared Memory Segments -------key shmid owner perms 0xffffffff 54067205 piccardi 666 ------ Semaphore Arrays -------key semid owner perms 0xffffffff 229376 piccardi 666 ------ Message Queues -------key msqid owner

bytes 4096

nattch 1

status

nsems 1

perms

used-bytes

messages

Se a questo punto aggiungiamo un le, ad esempio con touch prova, potremo vericare che, passati nel peggiore dei casi almeno 10 secondi (o leventuale altro intervallo impostato per la rilettura dei dati) avremo: [piccardi@gont sources]$ ./readmon Ci sono 69 file dati Ci sono 3 directory Ci sono 0 link Ci sono 0 fifo Ci sono 0 socket

12.3. TECNICHE ALTERNATIVE Ci sono 0 device a caratteri Ci sono 0 device a blocchi Totale 72 file, per 489887 byte

381

A questo punto possiamo far uscire il server inviandogli un segnale di SIGTERM con il comando killall dirmonitor, a questo punto ripetendo la lettura, otterremo un errore: [piccardi@gont sources]$ ./readmon Cannot find shared memory: No such file or directory e inoltre potremo anche vericare che anche gli oggetti di intercomunicazione visti in precedenza sono stati regolarmente cancellati: [piccardi@gont sources]$ ipcs ------ Shared Memory Segments -------key shmid owner perms ------ Semaphore Arrays -------key semid owner perms ------ Message Queues -------key msqid owner

bytes

nattch

status

nsems

perms

used-bytes

messages

12.3

Tecniche alternative

Come abbiamo detto in sez. 12.2.1, e ripreso nella descrizione dei singoli oggetti che ne fan parte, il SysV IPC presenta numerosi problemi; in [1]37 Stevens ne eettua una accurata analisi (alcuni dei concetti sono gi` stati accennati in precedenza) ed elenca alcune possibili tecniche a alternative, che vogliamo riprendere in questa sezione.

12.3.1

Alternative alle code di messaggi

Le code di messaggi sono probabilmente il meno usato degli oggetti del SysV IPC ; esse infatti nacquero principalmente come meccanismo di comunicazione bidirezionale quando ancora le pipe erano unidirezionali; con la disponibilit` di socketpair (vedi sez. 12.1.5) o utilizzando una a coppia di pipe, si pu` ottenere questo risultato senza incorrere nelle complicazioni introdotte dal o SysV IPC. In realt`, grazie alla presenza del campo mtype, le code di messaggi hanno delle caratteristiche a ulteriori, consentendo una classicazione dei messaggi ed un accesso non rigidamente sequenziale; due caratteristiche che sono impossibili da ottenere con le pipe e i socket di socketpair. A queste esigenze per` si pu` comunque ovviare in maniera diversa con un uso combinato della memoria o o condivisa e dei meccanismi di sincronizzazione, per cui alla ne luso delle code di messaggi classiche ` relativamente poco diuso. e

12.3.2

I le di lock

Come illustrato in sez. 12.2.5 i semafori del SysV IPC presentano una interfaccia inutilmente complessa e con alcuni difetti strutturali, per questo quando si ha una semplice esigenza di sincronizzazione per la quale basterebbe un semaforo binario (quello che abbiamo denito come mutex ), per indicare la disponibilit` o meno di una risorsa, senza la necessit` di un contatore a a come i semafori, si possono utilizzare metodi alternativi.
37

in particolare nel capitolo 14.

382

CAPITOLO 12. LA COMUNICAZIONE FRA PROCESSI

La prima possibilit`, utilizzata n dalle origini di Unix, ` quella di usare dei le di lock (per a e i quali esiste anche una opportuna directory, /var/lock, nel lesystem standard). Per questo si usa la caratteristica della funzione open (illustrata in sez. 6.2.1) che prevede38 che essa ritorni un errore quando usata con i ag di O_CREAT e O_EXCL. In tal modo la creazione di un le di lock pu` essere eseguita atomicamente, il processo che crea il le con successo si pu` considerare o o come titolare del lock (e della risorsa ad esso associata) mentre il rilascio si pu` eseguire con una o chiamata ad unlink. Un esempio delluso di questa funzione ` mostrato dalle funzioni LockFile ed UnlockFile e riportate in g. 12.28 (sono contenute in LockFile.c, un altro dei sorgenti allegati alla guida) che permettono rispettivamente di creare e rimuovere un le di lock. Come si pu` notare entrambe o le funzioni sono elementari; la prima (4-10) si limita ad aprire il le di lock (9) nella modalit` a descritta, mentre la seconda (11-17) lo cancella con unlink.
# include < sys / types .h > # include < sys / stat .h > 3 # include < unistd .h > /* Unix standard functions */ 4 /* 5 * Function LockFile : 6 */ 7 int LockFile ( const char * path_name ) 8 { 9 return open ( path_name , O_EXCL | O_CREAT ); 10 } 11 /* 12 * Function UnlockFile : 13 */ 14 int UnlockFile ( const char * path_name ) 15 { 16 return unlink ( path_name ); 17 }
1 2

Figura 12.28: Il codice delle funzioni LockFile e UnlockFile che permettono di creare e rimuovere un le di lock.

Uno dei limiti di questa tecnica ` che, come abbiamo gi` accennato in sez. 6.2.1, questo e a comportamento di open pu` non funzionare (la funzione viene eseguita, ma non ` garantita o e latomicit` delloperazione) se il lesystem su cui si va ad operare ` su NFS; in tal caso si pu` a e o adottare una tecnica alternativa che prevede luso della link per creare come le di lock un hard link ad un le esistente; se il link esiste gi` e la funzione fallisce, signica che la risorsa ` bloccata a e e potr` essere sbloccata solo con un unlink, altrimenti il link ` creato ed il lock acquisito; il a e controllo e leventuale acquisizione sono atomici; la soluzione funziona anche su NFS, ma ha un altro difetto ` che ` quello di poterla usare solo se si opera allinterno di uno stesso lesystem. e e In generale comunque luso di un le di lock presenta parecchi problemi che non lo rendono una alternativa praticabile per la sincronizzazione: anzitutto in caso di terminazione imprevista del processo, si lascia allocata la risorsa (il le di lock) e questa deve essere sempre cancellata esplicitamente. Inoltre il controllo della disponibilit` pu` essere eseguito solo con una tecnica di a o polling, ed ` quindi molto ineciente. e La tecnica dei le di lock ha comunque una sua utilit`, e pu` essere usata con successo quando a o lesigenza ` solo quella di segnalare loccupazione di una risorsa, senza necessit` di attendere che e a questa si liberi; ad esempio la si usa spesso per evitare interferenze sulluso delle porte seriali da
questo ` quanto dettato dallo standard POSIX.1, ci` non toglie che in alcune implementazioni questa tecnica e o possa non funzionare; in particolare per Linux, nel caso di NFS, si ` comunque soggetti alla possibilit` di una race e a condition.
38

12.3. TECNICHE ALTERNATIVE

383

parte di pi` programmi: qualora si trovi un le di lock il programma che cerca di accedere alla u seriale si limita a segnalare che la risorsa non ` disponibile. e

12.3.3

La sincronizzazione con il le locking

Dato che i le di lock presentano gli inconvenienti illustrati in precedenza, la tecnica alternativa di sincronizzazione pi` comune ` quella di fare ricorso al le locking (trattato in sez. 11.4) usando u e fcntl su un le creato per loccasione per ottenere un write lock. In questo modo potremo usare il lock come un mutex : per bloccare la risorsa baster` acquisire il lock, per sbloccarla baster` a a rilasciare il lock. Una richiesta fatta con un write lock metter` automaticamente il processo a in stato di attesa, senza necessit` di ricorrere al polling per determinare la disponibilit` della a a risorsa, e al rilascio della stessa da parte del processo che la occupava si otterr` il nuovo lock a atomicamente. Questo approccio presenta il notevole vantaggio che alla terminazione di un processo tutti i lock acquisiti vengono rilasciati automaticamente (alla chiusura dei relativi le) e non ci si deve preoccupare di niente; inoltre non consuma risorse permanentemente allocate nel sistema. Lo svantaggio ` che, dovendo fare ricorso a delle operazioni sul lesystem, esso ` in genere e e leggermente pi` lento. u Il codice delle varie funzioni usate per implementare un mutex utilizzando il le locking ` e riportato in g. 12.29; si ` mantenuta volutamente una struttura analoga alle precedenti funzioni e che usano i semafori, anche se le due interfacce non possono essere completamente equivalenti, specie per quanto riguarda la rimozione del mutex. La prima funzione (1-5) ` CreateMutex, e serve a creare il mutex; la funzione ` estremamente e e semplice, e si limita (4) a creare, con una opportuna chiamata ad open, il le che sar` usato per a il successivo le locking, assicurandosi che non esista gi` (nel qual caso segnala un errore); poi a restituisce il le descriptor che sar` usato dalle altre funzioni per acquisire e rilasciare il mutex. a La seconda funzione (6-10) ` FindMutex, che, come la precedente, ` stata denita per mane e tenere una analogia con la corrispondente funzione basata sui semafori. Anchessa si limita (9) ad aprire il le da usare per il le locking, solo che in questo caso le opzioni di open sono tali che il le in questione deve esistere di gi`. a La terza funzione (11-22) ` LockMutex e serve per acquisire il mutex. La funzione denisce e (14) e inizializza (16-19) la struttura lock da usare per acquisire un write lock sul le, che poi (21) viene richiesto con fcntl, restituendo il valore di ritorno di questultima. Se il le ` libero e il lock viene acquisito e la funzione ritorna immediatamente; altrimenti fcntl si bloccher` (si a noti che la si ` chiamata con F_SETLKW) no al rilascio del lock. e La quarta funzione (24-34) ` UnlockMutex e serve a rilasciare il mutex. La funzione ` analoga e e alla precedente, solo che in questo caso si inizializza (28-31) la struttura lock per il rilascio del lock, che viene eettuato (33) con la opportuna chiamata a fcntl. Avendo usato il le locking in semantica POSIX (si riveda quanto detto sez. 11.4.3) solo il processo che ha precedentemente eseguito il lock pu` sbloccare il mutex. o La quinta funzione (36-39) ` RemoveMutex e serve a cancellare il mutex. Anche questa fune zione ` stata denita per mantenere una analogia con le funzioni basate sui semafori, e si limita e a cancellare (38) il le con una chiamata ad unlink. Si noti che in questo caso la funzione non ha eetto sui mutex gi` ottenuti con precedenti chiamate a FindMutex o CreateMutex, che cona tinueranno ad essere disponibili ntanto che i relativi le descriptor restano aperti. Pertanto per rilasciare un mutex occorrer` prima chiamare UnlockMutex oppure chiudere il le usato per il a lock. La sesta funzione (41-55) ` ReadMutex e serve a leggere lo stato del mutex. In questo caso e si prepara (46-49) la solita struttura lock come lacquisizione del lock, ma si eettua (51) la chiamata a fcntl usando il comando F_GETLK per ottenere lo stato del lock, e si restituisce (52) il valore di ritorno in caso di errore, ed il valore del campo l_type (che descrive lo stato del lock)

384

CAPITOLO 12. LA COMUNICAZIONE FRA PROCESSI

/* Function CreateMutex : Create a mutex using file locking . */ int CreateMutex ( const char * path_name ) 3 { 4 return open ( path_name , O_EXCL | O_CREAT ); 5 } 6 /* Function UnlockMutex : unlock a file . */ 7 int FindMutex ( const char * path_name ) 8 { 9 return open ( path_name , O_RDWR ); 10 } 11 /* Function LockMutex : lock mutex using file locking . */ 12 int LockMutex ( int fd ) 13 { 14 struct flock lock ; /* file lock structure */ 15 /* set flock structure */ 16 lock . l_type = F_WRLCK ; /* set type : read or write */ 17 lock . l_whence = SEEK_SET ; /* start from the beginning of the file */ 18 lock . l_start = 0; /* set the start of the locked region */ 19 lock . l_len = 0; /* set the length of the locked region */ 20 /* do locking */ 21 return fcntl ( fd , F_SETLKW , & lock ); 22 } 23 /* Function UnlockMutex : unlock a file . */ 24 int UnlockMutex ( int fd ) 25 { 26 struct flock lock ; /* file lock structure */ 27 /* set flock structure */ 28 lock . l_type = F_UNLCK ; /* set type : unlock */ 29 lock . l_whence = SEEK_SET ; /* start from the beginning of the file */ 30 lock . l_start = 0; /* set the start of the locked region */ 31 lock . l_len = 0; /* set the length of the locked region */ 32 /* do locking */ 33 return fcntl ( fd , F_SETLK , & lock ); 34 } 35 /* Function RemoveMutex : remove a mutex ( unlinking the lock file ). */ 36 int RemoveMutex ( const char * path_name ) 37 { 38 return unlink ( path_name ); 39 } 40 /* Function ReadMutex : read a mutex status . */ 41 int ReadMutex ( int fd ) 42 { 43 int res ; 44 struct flock lock ; /* file lock structure */ 45 /* set flock structure */ 46 lock . l_type = F_WRLCK ; /* set type : unlock */ 47 lock . l_whence = SEEK_SET ; /* start from the beginning of the file */ 48 lock . l_start = 0; /* set the start of the locked region */ 49 lock . l_len = 0; /* set the length of the locked region */ 50 /* do locking */ 51 if ( ( res = fcntl ( fd , F_GETLK , & lock )) ) { 52 return res ; 53 } 54 return lock . l_type ; 55 }
1 2

Figura 12.29: Il codice delle funzioni che permettono per la gestione dei mutex con il le locking.

12.4. IL SISTEMA DI COMUNICAZIONE FRA PROCESSI DI POSIX

385

altrimenti (54). Per questo motivo la funzione restituir` -1 in caso di errore e uno dei due valori a F_UNLCK o F_WRLCK39 in caso di successo, ad indicare che il mutex `, rispettivamente, libero o e occupato. Basandosi sulla semantica dei le lock POSIX valgono tutte le considerazioni relative al comportamento di questi ultimi fatte in sez. 11.4.3; questo signica ad esempio che, al contrario di quanto avveniva con linterfaccia basata sui semafori, chiamate multiple a UnlockMutex o LockMutex non si cumulano e non danno perci` nessun inconveniente. o

12.3.4

Il memory mapping anonimo

Abbiamo gi` visto che quando i processi sono correlati 40 luso delle pipe pu` costituire una valida a o alternativa alle code di messaggi; nella stessa situazione si pu` evitare luso di una memoria o condivisa facendo ricorso al cosiddetto memory mapping anonimo. In sez. 11.3.1 abbiamo visto come sia possibile mappare il contenuto di un le nella memoria di un processo, e che, quando viene usato il ag MAP_SHARED, le modiche eettuate al contenuto del le vengono viste da tutti i processi che lo hanno mappato. Utilizzare questa tecnica per creare una memoria condivisa fra processi diversi ` estremamente ineciente, in quanto occorre e passare attraverso il disco. Per` abbiamo visto anche che se si esegue la mappatura con il ag o MAP_ANONYMOUS la regione mappata non viene associata a nessun le, anche se quanto scritto rimane in memoria e pu` essere riletto; allora, dato che un processo glio mantiene nel suo spazio o degli indirizzi anche le regioni mappate, esso sar` anche in grado di accedere a quanto in esse ` a e contenuto. In questo modo diventa possibile creare una memoria condivisa fra processi diversi, purch e 41 questi abbiano almeno un progenitore comune che ha eettuato il memory mapping anonimo. Vedremo come utilizzare questa tecnica pi` avanti, quando realizzeremo una nuova versione del u monitor visto in sez. 12.2.6 che possa restituisca i risultati via rete.

12.4

Il sistema di comunicazione fra processi di POSIX

Per superare i numerosi problemi del SysV IPC, evidenziati per i suoi aspetti generali in coda a sez. 12.2.1 e per i singoli oggetti nei paragra successivi, lo standard POSIX.1b ha introdotto dei nuovi meccanismi di comunicazione, che vanno sotto il nome di POSIX IPC, denendo una interfaccia completamente nuova, che tratteremo in questa sezione.

12.4.1

Considerazioni generali

Oggi Linux supporta tutti gli oggetti denito nello standard POSIX per lIPC, ma a lungo non ` stato cos` la memoria condivisa ` presente a partire dal kernel 2.4.x, i semafori sono forniti e ; e dalle glibc nella sezione che implementa i thread POSIX di nuova generazione che richiedono il kernel 2.6, le code di messaggi sono supportate a partire dal kernel 2.6.6. La caratteristica fondamentale dellinterfaccia POSIX ` labbandono delluso degli identicae tori e delle chiavi visti nel SysV IPC, per passare ai POSIX IPC names, che sono sostanzialmente equivalenti ai nomi dei le. Tutte le funzioni che creano un oggetto di IPC POSIX prendono
non si dovrebbe mai avere il terzo valore possibile, F_RDLCK, dato che la nostra interfaccia usa solo i write lock. Per` ` sempre possibile che siano richiesti altri lock sul le al di fuori dellinterfaccia, nel qual caso si potranno oe avere, ovviamente, interferenze indesiderate. 40 se cio` hanno almeno un progenitore comune. e 41 nei sistemi derivati da SysV una funzionalit` simile a questa viene implementata mappando il le speciale a /dev/zero. In tal caso i valori scritti nella regione mappata non vengono ignorati (come accade qualora si scriva direttamente sul le), ma restano in memoria e possono essere riletti secondo le stesse modalit` usate nel memory a mapping anonimo.
39

386

CAPITOLO 12. LA COMUNICAZIONE FRA PROCESSI

come primo argomento una stringa che indica uno di questi nomi; lo standard ` molto generie co riguardo limplementazione, ed i nomi stessi possono avere o meno una corrispondenza sul lesystem; tutto quello che ` richiesto ` che: e e i nomi devono essere conformi alle regole che caratterizzano i pathname, in particolare non essere pi` lunghi di PATH_MAX byte e terminati da un carattere nullo. u se il nome inizia per una / chiamate dierenti allo stesso nome fanno riferimento allo stesso oggetto, altrimenti linterpretazione del nome dipende dallimplementazione. linterpretazione di ulteriori / presenti nel nome dipende dallimplementazione. Data la assoluta genericit` delle speciche, il comportamento delle funzioni ` subordinato a e 42 Nel caso di Linux, sia per quanto in maniera quasi completa alla relativa implementazione. riguarda la memoria condivisa ed i semafori, che per quanto riguarda le code di messaggi, tutto viene creato usando come radici delle opportune directory (rispettivamente /dev/shm e /dev/mqueue, per i dettagli si faccia riferimento a sez. 12.4.3, sez. 12.4.4 e sez. 12.4.2) ed i nomi specicati nelle relative funzioni sono considerati come un pathname assoluto (comprendente eventuali sottodirectory) rispetto a queste radici. Il vantaggio degli oggetti di IPC POSIX ` comunque che essi vengono inseriti nellalbero e dei le, e possono essere maneggiati con le usuali funzioni e comandi di accesso ai le,43 che funzionano come su dei le normali. In particolare i permessi associati agli oggetti di IPC POSIX sono identici ai permessi dei le, ed il controllo di accesso segue esattamente la stessa semantica (quella illustrata in sez. 5.3), e non quella particolare (si ricordi quanto visto in sez. 12.2.2) che viene usata per gli oggetti del SysV IPC. Per quanto riguarda lattribuzione dellutente e del gruppo proprietari delloggetto alla creazione di questultimo essa viene eettuata secondo la semantica SysV: corrispondono cio` a user-ID e group-ID eettivi del processo che esegue la creazione. e

12.4.2

Code di messaggi

Le code di messaggi POSIX sono supportate da Linux a partire dalla versione 2.6.6-rc1 del kernel,44 In generale, come le corrispettive del SysV IPC, le code di messaggi sono poco usate, dato che i socket, nei casi in cui sono sucienti, sono pi` comodi, e che in casi pi` complessi la u u comunicazione pu` essere gestita direttamente con mutex (o semafori) e memoria condivisa con o tutta la essibilit` che occorre. a Per poter utilizzare le code di messaggi, oltre ad utilizzare un kernel superiore al 2.6.6 (o precedente, se sono stati opportunamente applicati i relativi patch) occorre utilizzare la libreria libmqueue45 che contiene le funzioni dellinterfaccia POSIX.46 La libreria inoltre richiede la presenza dellapposito lesystem di tipo mqueue montato su /dev/mqueue; questo pu` essere fatto aggiungendo ad /etc/fstab una riga come: o
42 tanto che Stevens in [12] cita questo caso come un esempio della maniera standard usata dallo standard POSIX per consentire implementazioni non standardizzabili. 43 questo ` vero nel caso di Linux, che usa una implementazione che lo consente, non ` detto che altrettanto valga e e per altri kernel; in particolare, come si pu` facilmente vericare con uno strace, sia per la memoria condivisa o che per le code di messaggi le system call utilizzate da Linux sono le stesse di quelle dei le, essendo detti oggetti realizzati come tali in appositi lesystem. 44 limplementazione ` dovuta a Michal Wronski e Krzysztof Benedyczak, e le relative informazioni si possono e trovare su http://www.geocities.com/wronski12/posix_ipc/index.html. 45 i programmi che usano le code di messaggi cio` devono essere compilati aggiungendo lopzione -lmqueue al e comando gcc; in corrispondenza allinclusione del supporto nel kernel uciale anche libmqueue ` stata inserita e nelle glibc, a partire dalla versione 2.3.4 delle medesime. 46 in realt` limplementazione ` realizzata tramite delle opportune chiamate ad ioctl sui le del lesystem a e speciale su cui vengono mantenuti questi oggetti di IPC.

12.4. IL SISTEMA DI COMUNICAZIONE FRA PROCESSI DI POSIX mqueue /dev/mqueue mqueue defaults 0 0

387

ed esso sar` utilizzato come radice sulla quale vengono risolti i nomi delle code di messaggi a che iniziano con una /. Le opzioni di mount accettate sono uid, gid e mode che permettono rispettivamente di impostare lutente, il gruppo ed i permessi associati al lesystem. La funzione che permette di aprire (e crearla se non esiste ancora) una coda di messaggi POSIX ` mq_open, ed il suo prototipo `: e e
#include <mqueue.h> mqd_t mq_open(const char *name, int oflag) mqd_t mq_open(const char *name, int oflag, unsigned long mode, struct mq_attr *attr) Apre una coda di messaggi POSIX impostandone le caratteristiche. La funzione restituisce il descrittore associato alla coda in caso di successo e -1 in caso di errore; nel quel caso errno assumer` i valori: a EACCES EEXIST EINTR EINVAL ENOENT il processo non ha i privilegi per accedere al alla memoria secondo quanto specicato da oflag. si ` specicato O_CREAT e O_EXCL ma la coda gi` esiste. e a la funzione ` stata interrotta da un segnale. e il le non supporta la funzione, o si ` specicato O_CREAT con una valore non nullo di e attr e valori non validi di mq_maxmsg e mq_msgsize. non si ` specicato O_CREAT ma la coda non esiste. e

ed inoltre ENOMEM, ENOSPC, EFAULT, EMFILE ed ENFILE.

La funzione apre la coda di messaggi identicata dallargomento name restituendo il descrittore ad essa associato, del tutto analogo ad un le descriptor, con lunica dierenza che lo standard prevede un apposito tipo mqd_t.47 Se la coda esiste gi` il descrittore far` riferimento allo stesso a a oggetto, consentendo cos` la comunicazione fra due processi diversi. La funzione ` del tutto analoga ad open ed analoghi sono i valori che possono essere specicati e per oflag, che deve essere specicato come maschera binaria; i valori possibili per i vari bit sono quelli visti in tab. 6.2 dei quali per` mq_open riconosce solo i seguenti: o O_RDONLY Apre la coda solo per la ricezione di messaggi. Il processo potr` usare il descrittore a con mq_receive ma non con mq_send. Apre la coda solo per la trasmissione di messaggi. Il processo potr` usare il descrita tore con mq_send ma non con mq_receive. Apre la coda solo sia per la trasmissione che per la ricezione. Necessario qualora si debba creare la coda; la presenza di questo bit richiede la presenza degli ulteriori argomenti mode e attr. Se usato insieme a O_CREAT fa fallire la chiamata se la coda esiste gi`, altrimenti a esegue la creazione atomicamente. Imposta la coda in modalit` non bloccante, le funzioni di ricezione e trasmissione a non si bloccano quando non ci sono le risorse richieste, ma ritornano immediatamente con un errore di EAGAIN. I primi tre bit specicano la modalit` di apertura della coda, e sono fra loro esclusivi. Ma a qualunque sia la modalit` in cui si ` aperta una coda, questa potr` essere riaperta pi` volte in a e a u
47

O_WRONLY

O_RDWR O_CREAT

O_EXCL O_NONBLOCK

nella implementazione citata questo ` denito come int. e

388

CAPITOLO 12. LA COMUNICAZIONE FRA PROCESSI

una modalit` diversa, e vi si potr` sempre accedere attraverso descrittori diversi, esattamente a a come si pu` fare per i le normali. o Se la coda non esiste e la si vuole creare si deve specicare O_CREAT, in tal caso occorre anche specicare i permessi di creazione con largomento mode; i valori di questultimo sono identici a quelli usati per open, anche se per le code di messaggi han senso solo i permessi di lettura e scrittura. Oltre ai permessi di creazione possono essere specicati anche gli attributi specici della coda tramite largomento attr; questultimo ` un puntatore ad una apposita struttura e mq_attr, la cui denizione ` riportata in g. 12.30. e
struct mq_attr { long mq_flags ; long mq_maxmsg ; long mq_msgsize ; long mq_curmsgs ; };

/* /* /* /*

message queue flags maximum number of messages maximum message size number of messages currently queued

*/ */ */ */

Figura 12.30: La struttura mq_attr, contenente gli attributi di una coda di messaggi POSIX.

Per la creazione della coda i campi della struttura che devono essere specicati sono mq_msgsize e mq_maxmsg, che indicano rispettivamente la dimensione massima di un messaggio ed il numero massimo di messaggi che essa pu` contenere. Il valore dovr` essere positivo e minore dei rispettivi o a limiti di sistema MQ_MAXMSG e MQ_MSGSIZE, altrimenti la funzione fallir` con un errore di EINVAL. a Qualora si specichi per attr un puntatore nullo gli attributi della coda saranno impostati ai valori predeniti. Quando laccesso alla coda non ` pi` necessario si pu` chiudere il relativo descrittore con la e u o funzione mq_close, il cui prototipo `: e
#include <mqueue.h> int mq_close(mqd_t mqdes) Chiude la coda mqdes. La funzione restituisce 0 in caso di successo e -1 in caso di errore; nel quel caso errno assumer` i a valori EBADF o EINTR.

La funzione ` analoga a close,48 dopo la sua esecuzione il processo non sar` pi` in grado e a u di usare il descrittore della coda, ma questultima continuer` ad esistere nel sistema e potr` a a essere acceduta con unaltra chiamata a mq_open. Alluscita di un processo tutte le code aperte, cos` come i le, vengono chiuse automaticamente. Inoltre se il processo aveva agganciato una richiesta di notica sul descrittore che viene chiuso, questa sar` rilasciata e potr` essere richiesta a a da qualche altro processo. Quando si vuole eettivamente rimuovere una coda dal sistema occorre usare la funzione mq_unlink, il cui prototipo `: e
#include <mqueue.h> int mq_unlink(const char *name) Rimuove una coda di messaggi. La funzione restituisce 0 in caso di successo e -1 in caso di errore; nel quel caso errno assumer` a gli stessi valori riportati da unlink.

Anche in questo caso il comportamento della funzione ` analogo a quello di unlink per i e le,49 la funzione rimuove la coda name, cos` che una successiva chiamata a mq_open fallisce o crea una coda diversa.
48 49

in Linux, dove le code sono implementate come le su un lesystem dedicato, ` esattamente la stessa funzione. e di nuovo limplementazione di Linux usa direttamente unlink.

12.4. IL SISTEMA DI COMUNICAZIONE FRA PROCESSI DI POSIX

389

Come per i le ogni coda di messaggi ha un contatore di riferimenti, per cui la coda non viene eettivamente rimossa dal sistema n quando questo non si annulla. Pertanto anche dopo aver eseguito con successo mq_unlink la coda rester` accessibile a tutti i processi che hanno un a descrittore aperto su di essa. Allo stesso modo una coda ed i suoi contenuti resteranno disponibili allinterno del sistema anche quando questultima non ` aperta da nessun processo (questa ` una e e delle dierenze pi` rilevanti nei confronti di pipe e fo). u La sola dierenza fra code di messaggi POSIX e le normali ` che, essendo il lesystem delle e code di messaggi virtuale e basato su oggetti interni al kernel, il suo contenuto viene perduto con il riavvio del sistema. Come accennato in precedenza ad ogni coda di messaggi ` associata una struttura mq_attr, e che pu` essere letta e modicata attraverso le due funzioni mq_getattr e mq_setattr, i cui o prototipi sono:
#include <mqueue.h> int mq_getattr(mqd_t mqdes, struct mq_attr *mqstat) Legge gli attributi di una coda di messaggi POSIX. int mq_setattr(mqd_t mqdes, const struct mq_attr *mqstat, struct mq_attr *omqstat) Modica gli attributi di una coda di messaggi POSIX. Entrambe le funzioni restituiscono 0 in caso di successo e -1 in caso di errore; nel quel caso errno assumer` i valori EBADF o EINVAL. a

La funzione mq_getattr legge i valori correnti degli attributi della coda nella struttura puntata da mqstat; di questi lunico relativo allo stato corrente della coda ` mq_curmsgs che e indica il numero di messaggi da essa contenuti, gli altri indicano le caratteristiche generali della stessa. La funzione mq_setattr permette di modicare gli attributi di una coda tramite i valori contenuti nella struttura puntata da mqstat, ma pu` essere modicato solo il campo mq_flags, o gli altri campi vengono ignorati. In particolare i valori di mq_maxmsg e mq_msgsize possono essere specicati solo in fase ci creazione della coda. Inoltre i soli valori possibili per mq_flags sono 0 e O_NONBLOCK, per cui alla ne la funzione pu` essere utilizzata solo per abilitare o o disabilitare la modalit` non bloccante. Largomento omqstat viene usato, quando diverso da a NULL, per specicare lindirizzo di una struttura su cui salvare i valori degli attributi precedenti alla chiamata della funzione. Per inserire messaggi su di una coda sono previste due funzioni, mq_send e mq_timedsend, i cui prototipi sono:
#include <mqueue.h> int mq_send(mqd_t mqdes, const char *msg_ptr, size_t msg_len, unsigned int msg_prio) Esegue linserimento di un messaggio su una coda. int mq_timedsend(mqd_t mqdes, const char *msg_ptr, size_t msg_len, unsigned msg_prio, const struct timespec *abs_timeout) Esegue linserimento di un messaggio su una coda entro il tempo abs_timeout. Le funzioni restituiscono 0 in caso di successo e -1 in caso di errore; nel quel caso errno assumer` a i valori: EAGAIN EMSGSIZE ENOMEM EINVAL si ` aperta la coda con O_NONBLOCK, e la coda ` piena. e e la lunghezza del messaggio msg_len eccede il limite impostato per la coda. il kernel non ha memoria suciente. Questo errore pu` avvenire quando linserimento o del messaggio si ` specicato un valore nullo per msg_len, o un valore di msg_prio fuori dai limiti, e o un valore non valido per abs_timeout.

ETIMEDOUT linserimento del messaggio non ` stato eettuato entro il tempo stabilito. e ed inoltre EBADF ed EINTR.

390

CAPITOLO 12. LA COMUNICAZIONE FRA PROCESSI

Entrambe le funzioni richiedono un puntatore al testo del messaggio nellargomento msg_ptr e la relativa lunghezza in msg_len. Se questultima eccede la dimensione massima specicata da mq_msgsize le funzioni ritornano immediatamente con un errore di EMSGSIZE. Largomento msg_prio indica la priorit` dellargomento; i messaggi di priorit` maggiore a a vengono inseriti davanti a quelli di priorit` inferiore (e quindi saranno riletti per primi). A a parit` del valore della priorit` il messaggio sar` inserito in coda a tutti quelli con la stessa a a a priorit`. Il valore della priorit` non pu` eccedere il limite di sistema MQ_PRIO_MAX, che nel caso a a o ` pari a 32768. e Qualora la coda sia piena, entrambe le funzioni si bloccano, a meno che non sia stata selezionata in fase di apertura la modalit` non bloccante,50 nel qual caso entrambe ritornano EAGAIN. a La sola dierenza fra le due funzioni ` che la seconda, passato il tempo massimo impostato con e largomento abs_timeout,51 ritorna comunque con un errore di ETIMEDOUT, se invece il tempo ` e gi` scaduto al momento della chiamata e la coda ` vuota la funzione ritorna immediatamente. a e Come per linserimento, anche per lestrazione dei messaggi da una coda sono previste due funzioni, mq_receive e mq_timedreceive, i cui prototipi sono:
#include <mqueue.h> ssize_t mq_receive(mqd_t mqdes, char *msg_ptr, size_t msg_len, unsigned int *msg_prio) Eettua la ricezione di un messaggio da una coda. ssize_t mq_timedreceive(mqd_t mqdes, char *msg_ptr, size_t msg_len, unsigned int *msg_prio, const struct timespec *abs_timeout) Eettua la ricezione di un messaggio da una coda entro il tempo abs_timeout. Le funzioni restituiscono il numero di byte del messaggio in caso di successo e -1 in caso di errore; nel quel caso errno assumer` i valori: a EAGAIN EMSGSIZE EINVAL si ` aperta la coda con O_NONBLOCK, e la coda ` vuota. e e la lunghezza del messaggio sulla coda eccede il valore msg_len specicato per la ricezione. si ` specicato un valore nullo per msg_ptr, o un valore non valido per abs_timeout. e

ETIMEDOUT la ricezione del messaggio non ` stata eettuata entro il tempo stabilito. e ed inoltre EBADF, EINTR, ENOMEM, o EINVAL.

La funzione estrae dalla coda il messaggio a priorit` pi` alta, o il pi` vecchio fra quelli della a u u stessa priorit`. Una volta ricevuto il messaggio viene tolto dalla coda e la sua dimensione viene a restituita come valore di ritorno.52 Se la dimensione specicata da msg_len non ` suciente a contenere il messaggio, entrambe e le funzioni, al contrario di quanto avveniva nelle code di messaggi di SysV, ritornano un errore ` di EMSGSIZE senza estrarre il messaggio. E pertanto opportuno eseguire sempre una chiamata a mq_getaddr prima di eseguire una ricezione, in modo da ottenere la dimensione massima dei messaggi sulla coda, per poter essere in grado di allocare dei buer sucientemente ampi per la lettura. Se si specica un puntatore per largomento msg_prio il valore della priorit` del messaggio a viene memorizzato allindirizzo da esso indicato. Qualora non interessi usare la priorit` dei a messaggi si pu` specicare NULL, ed usare un valore nullo della priorit` nelle chiamate a mq_send. o a Si noti che con le code di messaggi POSIX non si ha la possibilit` di selezionare quale a messaggio estrarre con delle condizioni sulla priorit`, a dierenza di quanto avveniva con le code a di messaggi di SysV che permettono invece la selezione in base al valore del campo mtype.
o si sia impostato il ag O_NONBLOCK sul le descriptor della coda. deve essere specicato un tempo assoluto tramite una struttura timespec (vedi g. 8.9) indicato in numero di secondi e nanosecondi a partire dal 1 gennaio 1970. 52 si tenga presente che 0 ` una dimensione valida e che la condizione di errore ` restituita dal valore -1; Stevens e e in [12] fa notare che questo ` uno dei casi in cui vale ci` che lo standard non dice, una dimensione nulla infatti, e o pur non essendo citata, non viene proibita.
51 50

12.4. IL SISTEMA DI COMUNICAZIONE FRA PROCESSI DI POSIX

391

Qualora la coda sia vuota entrambe le funzioni si bloccano, a meno che non si sia selezionata la modalit` non bloccante; in tal caso entrambe ritornano immediatamente con lerrore a EAGAIN. Anche in questo caso la sola dierenza fra le due funzioni ` che la seconda non attende e indenitamente e passato il tempo massimo abs_timeout ritorna comunque con un errore di ETIMEDOUT. Uno dei problemi sottolineati da Stevens in [12], comuni ad entrambe le tipologie di code messaggi, ` che non ` possibile per chi riceve identicare chi ` che ha inviato il messaggio, in e e e particolare non ` possibile sapere da quale utente esso provenga. Infatti, in mancanza di un e meccanismo interno al kernel, anche se si possono inserire delle informazioni nel messaggio, queste non possono essere credute, essendo completamente dipendenti da chi lo invia. Vedremo per` come, attraverso luso del meccanismo di notica, sia possibile superare in parte questo o problema. Una caratteristica specica delle code di messaggi POSIX ` la possibilit` di usufruire di un e a meccanismo di notica asincrono; questo pu` essere attivato usando la funzione mq_notify, il o cui prototipo `: e
#include <mqueue.h> int mq_notify(mqd_t mqdes, const struct sigevent *notification) Attiva il meccanismo di notica per la coda mqdes. La funzione restituisce 0 in caso di successo e -1 in caso di errore; nel quel caso errno assumer` i a valori: EBUSY EBADF c` gi` un processo registrato per la notica. e a il descrittore non fa riferimento ad una coda di messaggi.

Il meccanismo di notica permette di segnalare in maniera asincrona ad un processo la presenza di dati sulla coda, in modo da evitare la necessit` di bloccarsi nellattesa. Per far a questo un processo deve registrarsi con la funzione mq_notify, ed il meccanismo ` disponibile e per un solo processo alla volta per ciascuna coda. Il comportamento di mq_notify dipende dal valore dellargomento notification, che ` e un puntatore ad una apposita struttura sigevent, (denita in g. 11.6) introdotta dallo standard POSIX.1b per gestire la notica di eventi; per altri dettagli si pu` vedere quanto detto o in sez. 11.2.3 a proposito delluso della stessa struttura per linvio dei segnali usati per lI/O asincrono. Attraverso questa struttura si possono impostare le modalit` con cui viene eettuata la a notica; in particolare il campo sigev_notify deve essere posto a SIGEV_SIGNAL53 ed il campo sigev_signo deve indicare il valore del segnale che sar` inviato al processo. Inoltre il campo a sigev_value ` il puntatore ad una struttura sigval_t (denita in g. 9.14) che permette di e restituire al gestore del segnale un valore numerico o un indirizzo,54 posto che questo sia installato nella forma estesa vista in sez. 9.4.3. La funzione registra il processo chiamante per la notica se notification punta ad una struttura sigevent opportunamente inizializzata, o cancella una precedente registrazione se ` e NULL. Dato che un solo processo alla volta pu` essere registrato, la funzione fallisce con EBUSY o se c` un altro processo gi` registrato. Si tenga presente inoltre che alla chiusura del descrittore e a associato alla coda (e quindi anche alluscita del processo) ogni eventuale registrazione di notica presente viene cancellata. La notica del segnale avviene allarrivo di un messaggio in una coda vuota (cio` solo se e sulla coda non ci sono messaggi) e se non c` nessun processo bloccato in una chiamata a e mq_receive, in questo caso infatti il processo bloccato ha la precedenza ed il messaggio gli viene
53 54

il meccanismo di notica basato sui thread, specicato tramite il valore SIGEV_THREAD, non ` implementato. e per il suo uso si riveda la trattazione fatta in sez. 9.5.1 a proposito dei segnali real-time.

392

CAPITOLO 12. LA COMUNICAZIONE FRA PROCESSI

immediatamente inviato, mentre per il meccanismo di notica tutto funziona come se la coda fosse rimasta vuota. Quando un messaggio arriva su una coda vuota al processo che si era registrato viene inviato il segnale specicato da notification->sigev_signo, e la coda diventa disponibile per una ulteriore registrazione. Questo comporta che se si vuole mantenere il meccanismo di notica occorre ripetere la registrazione chiamando nuovamente mq_notify allinterno del gestore del segnale di notica. A dierenza della situazione simile che si aveva con i segnali non adabili,55 questa caratteristica non congura una race-condition perch linvio di un segnale avviene solo e se la coda ` vuota; pertanto se si vuole evitare di correre il rischio di perdere eventuali ulteriori e segnali inviati nel lasso di tempo che occorre per ripetere la richiesta di notica basta avere cura di eseguire questa operazione prima di estrarre i messaggi presenti dalla coda. Linvio del segnale di notica avvalora alcuni campi di informazione restituiti al gestore attraverso la struttura siginfo_t (denita in g. 9.9). In particolare si_pid viene impostato al valore del pid del processo che ha emesso il segnale, si_uid alluserid eettivo, si_code a SI_MESGQ, e si_errno a 0. Questo ci dice che, se si eettua la ricezione dei messaggi usando esclusivamente il meccanismo di notica, ` possibile ottenere le informazioni sul processo che ha e inserito un messaggio usando un gestore per il segnale in forma estesa.56

12.4.3

Memoria condivisa

La memoria condivisa ` stato il primo degli oggetti di IPC POSIX inserito nel kernel uciale; e il supporto a questo tipo di oggetti ` realizzato attraverso il lesystem tmpfs, uno speciale lee system che mantiene tutti i suoi contenuti in memoria,57 che viene attivato abilitando lopzione CONFIG_TMPFS in fase di compilazione del kernel. Per potere utilizzare linterfaccia POSIX per la memoria condivisa le glibc 58 richiedono di compilare i programmi con lopzione -lrt; inoltre ` necessario che in /dev/shm sia montato un e lesystem tmpfs; questo di norma viene fatto aggiungendo una riga del tipo di: tmpfs /dev/shm tmpfs defaults 0 0

ad /etc/fstab. In realt` si pu` montare un lesystem tmpfs dove si vuole, per usarlo come a o RAM disk, con un comando del tipo: mount -t tmpfs -o size=128M,nr_inodes=10k,mode=700 tmpfs /mytmpfs Il lesystem riconosce, oltre quelle mostrate, le opzioni uid e gid che identicano rispettivamente utente e gruppo cui assegnarne la titolarit`, e nr_blocks che permette di specicarne a la dimensione in blocchi, cio` in multipli di PAGECACHE_SIZE che in questo caso ` lunit` di e e a allocazione elementare. La funzione che permette di aprire un segmento di memoria condivisa POSIX, ed eventualmente di crearlo se non esiste ancora, ` shm_open; il suo prototipo `: e e
#include <mqueue.h> int shm_open(const char *name, int oflag, mode_t mode) Apre un segmento di memoria condivisa. La funzione restituisce un le descriptor positivo in caso di successo e -1 in caso di errore; nel quel caso errno assumer` gli stessi valori riportati da open. a largomento ` stato arontato in 9.1.2. e di nuovo si faccia riferimento a quanto detto al proposito in sez. 9.4.3 e sez. 9.5.1. 57 il lesystem tmpfs ` diverso da un normale RAM disk, anchesso disponibile attraverso il lesystem ramfs, e proprio perch realizza una interfaccia utilizzabile anche per la memoria condivisa; esso infatti non ha dimensione e ssa, ed usa direttamente la cache interna del kernel (che viene usata anche per la shared memory in stile SysV). In pi` i suoi contenuti, essendo trattati direttamente dalla memoria virtuale possono essere salvati sullo swap u automaticamente. 58 le funzioni sono state introdotte con le glibc-2.2.
56 55

12.4. IL SISTEMA DI COMUNICAZIONE FRA PROCESSI DI POSIX

393

La funzione apre un segmento di memoria condivisa identicato dal nome name. Come gi` a spiegato in sez. 12.4.1 questo nome pu` essere specicato in forma standard solo facendolo iniziare o per / e senza ulteriori /, Linux supporta comunque nomi generici, che verranno interpretati prendendo come radice /dev/shm.59 La funzione ` del tutto analoga ad open ed analoghi sono i valori che possono essere specicati e per oflag, che deve essere specicato come maschera binaria comprendente almeno uno dei due valori O_RDONLY e O_RDWR; i valori possibili per i vari bit sono quelli visti in tab. 6.2 dei quali per` shm_open riconosce solo i seguenti: o O_RDONLY O_RDWR O_CREAT Apre il le descriptor associato al segmento di memoria condivisa per laccesso in sola lettura. Apre il le descriptor associato al segmento di memoria condivisa per laccesso in lettura e scrittura. Necessario qualora si debba creare il segmento di memoria condivisa se esso non esiste; in questo caso viene usato il valore di mode per impostare i permessi, che devono essere compatibili con le modalit` con cui si ` aperto il le. a e Se usato insieme a O_CREAT fa fallire la chiamata a shm_open se il segmento esiste gi`, altrimenti esegue la creazione atomicamente. a Se il segmento di memoria condivisa esiste gi`, ne tronca le dimensioni a 0 byte. a

O_EXCL O_TRUNC

In caso di successo la funzione restituisce un le descriptor associato al segmento di memoria condiviso con le stesse modalit` di open60 viste in sez. 6.2.1; in particolare viene impostato il a ag FD_CLOEXEC. Chiamate eettuate da diversi processi usando lo stesso nome, restituiranno le descriptor associati allo stesso segmento (cos` come, nel caso di le di dati, essi sono associati allo stesso inode). In questo modo ` possibile eettuare una chiamata ad mmap sul le descriptor e restituito da shm_open ed i processi vedranno lo stesso segmento di memoria condivisa. Quando il nome non esiste il segmento pu` essere creato specicando O_CREAT; in tal caso o il segmento avr` (cos` come i nuovi le) lunghezza nulla. Dato che un segmento di lunghezza a nulla ` di scarsa utilit`, per impostarne la dimensione si deve usare ftruncate (vedi sez. 5.2.3), e a prima di mapparlo in memoria con mmap. Si tenga presente che una volta chiamata mmap si pu` o chiudere il le descriptor (con close), senza che la mappatura ne risenta. Come per i le, quando si vuole eettivamente rimuovere segmento di memoria condivisa, occorre usare la funzione shm_unlink, il cui prototipo `: e
#include <mqueue.h> int shm_unlink(const char *name) Rimuove un segmento di memoria condivisa. La funzione restituisce 0 in caso di successo e -1 in caso di errore; nel quel caso errno assumer` a gli stessi valori riportati da unlink.

La funzione ` del tutto analoga ad unlink, e si limita a cancellare il nome del segmento da e /dev/shm, senza nessun eetto n sui le descriptor precedentemente aperti con shm_open, n sui e e segmenti gi` mappati in memoria; questi verranno cancellati automaticamente dal sistema solo a con le rispettive chiamate a close e munmap. Una volta eseguita questa funzione per`, qualora o si richieda lapertura di un segmento con lo stesso nome, la chiamata a shm_open fallir`, a meno a di non aver usato O_CREAT, in questultimo caso comunque si otterr` un le descriptor che fa a riferimento ad un segmento distinto da eventuali precedenti.
occorre pertanto evitare di specicare qualcosa del tipo /dev/shm/nome allinterno di name, perch questo e comporta, da parte delle funzioni di libreria, il tentativo di accedere a /dev/shm/dev/shm/nome. 60 in realt`, come accennato, shm_open ` un semplice wrapper per open, usare direttamente questultima avrebbe a e lo stesso eetto.
59

394

CAPITOLO 12. LA COMUNICAZIONE FRA PROCESSI

/* Function CreateShm : Create a shared memory segment mapping it */ void * CreateShm ( char * shm_name , off_t shm_size , mode_t perm , int fill ) 3 { 4 void * shm_ptr ; 5 int fd ; 6 int flag ; 7 /* first open the object , creating it if not existent */ 8 flag = O_CREAT | O_EXCL | O_RDWR ; 9 fd = shm_open ( shm_name , flag , perm ); /* get object file descriptor */ 10 if ( fd < 0) { 11 return NULL ; 12 } 13 /* set the object size */ 14 if ( ftruncate ( fd , shm_size )) { 15 return NULL ; 16 } 17 /* map it in the process address space */ 18 shm_ptr = mmap ( NULL , shm_size , PROT_WRITE | PROT_READ , MAP_SHARED , fd , 0); 19 if ( shm_ptr == MAP_FAILED ) { 20 return NULL ; 21 } 22 memset (( void *) shm_ptr , fill , shm_size ); /* fill segment */ 23 return shm_ptr ; 24 } 25 /* Function FindShm : Find a POSIX shared memory segment */ 26 void * FindShm ( char * shm_name , off_t shm_size ) 27 { 28 void * shm_ptr ; 29 int fd ; /* ID of the IPC shared memory segment */ 30 /* find shared memory ID */ 31 if (( fd = shm_open ( shm_name , O_RDWR | O_EXCL , 0)) < 0) { 32 return NULL ; 33 } 34 /* take the pointer to it */ 35 shm_ptr = mmap ( NULL , shm_size , PROT_WRITE | PROT_READ , MAP_SHARED , fd , 0); 36 if ( shm_ptr == MAP_FAILED ) { 37 return NULL ; 38 } 39 return shm_ptr ; 40 } 41 /* Function RemoveShm : Remove a POSIX shared memory segment */ 42 int RemoveShm ( char * shm_name ) 43 { 44 return shm_unlink ( shm_name ); 45 }
1 2

Figura 12.31: Il codice delle funzioni di gestione dei segmenti di memoria condivisa POSIX.

Come esempio per luso di queste funzioni vediamo come ` possibile riscrivere una interfaccia e semplicata analoga a quella vista in g. 12.24 per la memoria condivisa in stile SysV. Il codice, riportato in g. 12.31, ` sempre contenuto nel le SharedMem.c dei sorgenti allegati. e La prima funzione (1-24) ` CreateShm che, dato un nome nellargomento name crea un nuovo e segmento di memoria condivisa, accessibile in lettura e scrittura, e ne restituisce lindirizzo. Anzitutto si deniscono (8) i ag per la successiva (9) chiamata a shm_open, che apre il segmento in lettura e scrittura (creandolo se non esiste, ed uscendo in caso contrario) assegnandogli sul lesystem i permessi specicati dallargomento perm. In caso di errore (10-12) si restituisce un puntatore nullo, altrimenti si prosegue impostando (14) la dimensione del segmento con

12.4. IL SISTEMA DI COMUNICAZIONE FRA PROCESSI DI POSIX

395

ftruncate. Di nuovo (15-16) si esce immediatamente restituendo un puntatore nullo in caso di errore. Poi si passa (18) a mappare in memoria il segmento con mmap specicando dei diritti di accesso corrispondenti alla modalit` di apertura. Di nuovo si restituisce (19-21) un puntatore a nullo in caso di errore, altrimenti si inizializza (22) il contenuto del segmento al valore specicato dallargomento fill con memset, e se ne restituisce (23) lindirizzo. La seconda funzione (25-40) ` FindShm che trova un segmento di memoria condiviso gi` e a esistente, restituendone lindirizzo. In questo caso si apre (31) il segmento con shm_open richiedendo che il segmento sia gi` esistente, in caso di errore (31-33) si ritorna immediatamente un a puntatore nullo. Ottenuto il le descriptor del segmento lo si mappa (35) in memoria con mmap, restituendo (36-38) un puntatore nullo in caso di errore, o lindirizzo (39) dello stesso in caso di successo. La terza funzione (40-45) ` RemoveShm, e serve a cancellare un segmento di memoria condie visa. Dato che al contrario di quanto avveniva con i segmenti del SysV IPC gli oggetti allocati nel kernel vengono rilasciati automaticamente quando nessuna li usa pi`, tutto quello che c` da u e fare (44) in questo caso ` chiamare shm_unlink, restituendo al chiamante il valore di ritorno. e

12.4.4

Semafori

Fino alla serie 2.4.x del kernel esisteva solo una implementazione parziale dei semafori POSIX che li realizzava solo a livello di thread e non di processi,61 fornita attraverso la sezione delle estensioni real-time delle glibc.62 Esisteva inoltre una libreria che realizzava (parzialmente) linterfaccia POSIX usando le funzioni dei semafori di SysV IPC (mantenendo cos` tutti i problemi sottolineati in sez. 12.2.5). A partire dal kernel 2.5.7 ` stato introdotto un meccanismo di sincronizzazione completamene te nuovo, basato sui cosiddetti futex,63 con il quale ` stato possibile implementare una versione e nativa dei semafori POSIX. Grazie a questo con i kernel della serie 2.6 e le nuove versioni delle glibc che usano questa nuova infrastruttura per quella che viene quella che viene chiamata New Posix Thread Library, sono state implementate anche tutte le funzioni dellinterfaccia dei semafori POSIX. Anche in questo caso ` necessario appoggiarsi alla libreria per le estensioni real-time librt, e questo signica che se si vuole utilizzare questa interfaccia, oltre ad utilizzare gli opportuni le di denizione, occorrer` compilare i programmi con lopzione -lrt. a La funzione che permette di creare un nuovo semaforo POSIX, creando il relativo le, o di accedere ad uno esistente, ` sem_open, questa prevede due forme diverse a seconda che sia e utilizzata per aprire un semaforo esistente o per crearne uno nuovi, i relativi prototipi sono:
#include <semaphore.h> sem_t *sem_open(const char *name, int oflag) sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value) Crea un semaforo o ne apre uno esistente. La funzione restituisce lindirizzo del semaforo in caso di successo e SEM_FAILED in caso di errore; nel quel caso errno assumer` i valori: a EACCESS EEXIST EINVAL il semaforo esiste ma non si hanno permessi sucienti per accedervi. si sono specicati O_CREAT e O_EXCL ma il semaforo esiste. il valore di value eccede SEM_VALUE_MAX. non si ` usato O_CREAT ed il nome specicato non esiste. e

ENAMETOOLONG si ` utilizzato un nome troppo lungo. e ENOENT ed inoltre ENFILE ed ENOMEM. questo signicava che i semafori erano visibili solo allinterno dei thread creati da un singolo processo, e non potevano essere usati come meccanismo di sincronizzazione fra processi diversi. 62 quelle che si accedono collegandosi alla libreria librt. 63 la sigla sta per fast user mode mutex.
61

396

CAPITOLO 12. LA COMUNICAZIONE FRA PROCESSI

Largomento name denisce il nome del semaforo che si vuole utilizzare, ed ` quello che e permette a processi diversi di accedere allo stesso semaforo. Questo deve essere specicato con un pathname nella forma /qualchenome, che non ha una corrispondenza diretta con un pathname reale; con Linux infatti i le associati ai semafori sono mantenuti nel lesystem virtuale /dev/shm, e gli viene assegnato automaticamente un nome nella forma sem.qualchenome.64 Largomento oflag ` quello che controlla le modalit` con cui opera la funzione, ed ` passato e a e come maschera binaria; i bit corrispondono a quelli utilizzati per lanalogo argomento di open, anche se dei possibili valori visti in sez. 6.2.1 sono utilizzati soltanto O_CREAT e O_EXCL. Se si usa O_CREAT si richiede la creazione del semaforo qualora questo non esista, ed in tal caso occorre utilizzare la seconda forma della funzione, in cui si devono specicare sia un valore iniziale con largomento value,65 che una maschera dei permessi con largomento mode;66 questi verranno assegnati al semaforo appena creato. Se il semaforo esiste gi` i suddetti valori saranno a invece ignorati. Usando il ag O_EXCL si richiede invece la verica che il semaforo non esiste, usandolo insieme ad O_CREAT la funzione fallisce qualora un semaforo con lo stesso nome sia gi` a presente. La funzione restituisce in caso di successo un puntatore allindirizzo del semaforo con un valore di tipo sem_t *, ` questo valore che dovr` essere passato alle altre funzioni per operare e a sul semaforo stesso. Si tenga presente che, come accennato in sez. 12.4.1, i semafori usano la semantica standard dei le per quanto riguarda i controlli di accesso. Questo signica che un nuovo semaforo viene sempre creato con luser-ID ed il group-ID eettivo del processo chiamante, e che i permessi indicati con mode vengono ltrati dal valore della umask del processo. Inoltre per poter aprire un semaforo ` necessario avere su di esso sia e il permesso di lettura che quello di scrittura. Una volta che si sia ottenuto lindirizzo di un semaforo, sar` possibile utilizzarlo; se si ricorda a quanto detto allinizio di sez. 12.2.5, dove si sono introdotti i concetti generali relativi ai semafori, le operazioni principali sono due, quella che richiede luso di una risorsa bloccando il semaforo e quella che rilascia la risorsa liberando il semaforo. La prima operazione ` eettuata dalla funzione e sem_wait, il cui prototipo `: e
#include <semaphore.h> int sem_wait(sem_t *sem) Blocca il semaforo sem. La funzione restituisce 0 in caso di successo e 1 in caso di errore; nel quel caso errno assumer` a i valori: EINTR EINVAL la funzione ` stata interrotta da un segnale. e il semaforo sem non esiste.

La funzione cerca di decrementare il valore del semaforo indicato dal puntatore sem, se questo ha un valore positivo, cosa che signica che la risorsa ` disponibile, la funzione ha successo, il e valore del semaforo viene diminuito di 1 ed essa ritorna immediatamente; se il valore ` nullo la e funzione si blocca ntanto che il valore del semaforo non torni positivo67 cos` che poi essa possa decrementarlo con successo e proseguire. Si tenga presente che la funzione pu` sempre essere interrotta da un segnale (nel qual caso o si avr` un errore di EINTR) e che questo avverr` comunque, anche se si ` richiesta la semantica a a e
si ha cio` una corrispondenza per cui /qualchenome viene rimappato, nella creazione tramite sem_open, su e /dev/shm/sem.qualchenome. 65 e si noti come cos` diventa possibile, dierenza di quanto avviene per i semafori del SysV IPC, eettuare in maniera atomica creazione ed inizializzazione di un semaforo usando una unica funzione. 66 anche questo argomento prende gli stessi valori utilizzati per lanalogo di open, che si sono illustrati in dettaglio sez. 5.3.1. 67 ovviamente per opera di altro processo che lo rilascia chiamando sem_post.
64

12.4. IL SISTEMA DI COMUNICAZIONE FRA PROCESSI DI POSIX

397

BSD installando il relativo gestore con SA_RESTART (vedi sez. 9.4.3) per riavviare le system call interrotte. Della funzione sem_wait esistono due varianti che consentono di gestire diversamente le modalit` di attesa in caso di risorsa occupata, la prima di queste ` sem_trywait, che serve ad a e eettuare un tentativo di acquisizione senza bloccarsi; il suo prototipo `: e
#include <semaphore.h> int sem_trywait(sem_t *sem) Tenta di bloccare il semaforo sem. La funzione restituisce 0 in caso di successo e 1 in caso di errore; nel quel caso errno assumer` a gli stessi valori: EAGAIN EINVAL il semaforo non pu` essere acquisito senza bloccarsi. o il semaforo sem non esiste.

La funzione ` identica a sem_wait ed se la risorsa ` libera ha lo stesso eetto, vale a dire e e che in caso di semaforo diverso da zero la funzione lo decrementa e ritorna immediatamente; la dierenza ` che nel caso in cui il semaforo ` occupato essa non si blocca e di nuovo ritorna e e immediatamente, restituendo per` un errore di EAGAIN, cos` che il programma possa proseguire. o La seconda variante di sem_wait ` una estensione specica che pu` essere utilizzata soltanto e o se viene denita la macro _XOPEN_SOURCE ad un valore di 600 prima di includere semaphore.h, la funzione ` sem_timedwait, ed il suo prototipo `: e e
#include <semaphore.h> int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout) Blocca il semaforo sem. La funzione restituisce 0 in caso di successo e 1 in caso di errore; nel quel caso errno assumer` a gli stessi valori: ETIMEDOUT ` scaduto il tempo massimo di attesa. e EINVAL EINTR il semaforo sem non esiste. la funzione ` stata interrotta da un segnale. e

Anche in questo caso il comportamento della funzione ` identico a quello di sem_wait, la sola e dierenza consiste nel fatto che con questa funzione ` possibile impostare tramite largomento e abs_timeout un tempo limite per lattesa, scaduto il quale la funzione ritorna comunque, anche se non ` possibile acquisire il semaforo. In tal caso la funzione fallir`, riportando un errore di e a ETIMEDOUT. La seconda funzione principale utilizzata per luso dei semafori ` sem_post, che viene usata e per rilasciare un semaforo occupato o, in generale, per aumentare di una unit` il valore dello a stesso anche qualora non fosse occupato;68 il suo prototipo `: e
#include <semaphore.h> int sem_post(sem_t *sem) Rilascia il semaforo sem. La funzione restituisce 0 in caso di successo e 1 in caso di errore; nel quel caso errno assumer` a i valori: EINVAL il semaforo sem non esiste.

La funzione incrementa di uno il valore corrente del semaforo indicato dallargomento sem, se questo era nullo la relativa risorsa risulter` sbloccata, cosicch un altro processo (o thread) a e eventualmente bloccato in una sem_wait sul semaforo potr` essere svegliato e rimesso in esecua zione. Si tenga presente che la funzione ` sicura per luso allinterno di un gestore di segnali (si e ricordi quanto detto in sez. 9.4.6).
68

si ricordi che in generale un semaforo viene usato come indicatore di un numero di risorse disponibili.

398

CAPITOLO 12. LA COMUNICAZIONE FRA PROCESSI

Se invece di operare su un semaforo se ne vuole solamente leggere il valore, si pu` usare la o funzione sem_getvalue, il cui prototipo `: e
#include <semaphore.h> int sem_getvalue(sem_t *sem, int *sval) Richiede il valore del semaforo sem. La funzione restituisce 0 in caso di successo e 1 in caso di errore; nel quel caso errno assumer` a i valori: EINVAL il semaforo sem non esiste.

La funzione legge il valore del semaforo indicato dallargomento sem e lo restituisce nella variabile intera puntata dallargomento sval. Qualora ci siano uno o pi` processi bloccati in u attesa sul semaforo lo standard prevede che la funzione possa restituire un valore nullo oppure il numero di processi bloccati in una sem_wait sul suddetto semaforo; nel caso di Linux vale la prima opzione. Questa funzione pu` essere utilizzata per avere un suggerimento sullo stato di un semaforo, o ovviamente non si pu` prendere il risultato riportato in sval che come indicazione, il valore del o semaforo infatti potrebbe essere gi` stato modicato al ritorno della funzione. a Una volta che non ci sia pi` la necessit` di operare su un semaforo se ne pu` terminare luso u a o con la funzione sem_close, il cui prototipo `: e
#include <semaphore.h> int sem_close(sem_t *sem) Chiude il semaforo sem. La funzione restituisce 0 in caso di successo e 1 in caso di errore; nel quel caso errno assumer` a i valori: EINVAL il semaforo sem non esiste.

La funzione chiude il semaforo indicato dallargomento sem; questo comporta che tutte le risorse che il sistema pu` avere assegnato al processo nelluso dello stesso vengono rilasciate. o Questo signica che un altro processo bloccato sul semaforo a causa della acquisizione da parte del processo che chiama sem_close potr` essere riavviato. a Si tenga presente poi che come per i le alluscita di un processo tutti i semafori che questo aveva aperto vengono automaticamente chiusi; questo comportamento risolve il problema che si aveva con i semafori del SysV IPC (di cui si ` parlato in sez. 12.2.5) per i quali le risorse possono e restare bloccate. Si tenga poi presente che, a dierenza di quanto avviene per i le, in caso di una chiamata ad execve tutti i semafori vengono chiusi automaticamente. Come per i semafori del SysV IPC anche quelli POSIX hanno una persistenza di sistema; questo signica che una volta che si ` creato un semaforo con sem_open questo continuer` ad e a esistere ntanto che il kernel resta attivo (vale a dire no ad un successivo riavvio) a meno che non lo si cancelli esplicitamente. Per far questo si pu` utilizzare la funzione sem_unlink, il cui o prototipo `: e
#include <semaphore.h> int sem_unlink(const char *name) Rimuove il semaforo name. La funzione restituisce 0 in caso di successo e 1 in caso di errore; nel quel caso errno assumer` a i valori: EACCESS non si hanno i permessi necessari a cancellare il semaforo. il semaforo name non esiste. ENAMETOOLONG il nome indicato ` troppo lungo. e ENOENT

La funzione rimuove il semaforo indicato dallargomento name, che prende un valore identico a quello usato per creare il semaforo stesso con sem_open. Il semaforo viene rimosso dal lesystem

12.4. IL SISTEMA DI COMUNICAZIONE FRA PROCESSI DI POSIX

399

immediatamente; ma il semaforo viene eettivamente cancellato dal sistema soltanto quando tutti i processi che lo avevano aperto lo chiudono. Si segue cio` la stessa semantica usata con e unlink per i le, trattata in dettaglio in sez. 5.1.1. Una delle caratteristiche peculiari dei semafori POSIX ` che questi possono anche essere e utilizzati anche in forma anonima, senza necessit` di fare ricorso ad un nome sul lesystem o ad a altri indicativi. In questo caso si dovr` porre la variabile che contiene lindirizzo del semaforo in a un tratto di memoria che sia accessibile a tutti i processi in gioco. La funzione che consente di inizializzare un semaforo anonimo ` sem_init, il cui prototipo `: e e
#include <semaphore.h> int sem_init(sem_t *sem, int pshared, unsigned int value) Inizializza il semaforo anonimo sem. La funzione restituisce 0 in caso di successo e 1 in caso di errore; nel quel caso errno assumer` a i valori: EINVAL ENOSYS il valore di value eccede SEM_VALUE_MAX. il valore di pshared non ` nullo ed il sistema non supporta i semafori per i processi. e

La funzione inizializza un semaforo allindirizzo puntato dallargomento sem, e come per sem_open consente di impostare un valore iniziale con value. Largomento pshared serve ad indicare se il semaforo deve essere utilizzato dai thread di uno stesso processo (con un valore nullo) o condiviso fra processi diversi (con un valore non nullo). Qualora il semaforo debba essere condiviso dai thread di uno stesso processo (nel qual caso si parla di thread-shared semaphore), occorrer` che sem sia lindirizzo di una variabile visibile da a tutti i thread, si dovr` usare cio` una variabile globale o una variabile allocata dinamicamente a e nello heap. Qualora il semaforo debba essere condiviso fra pi` processi (nel qual caso si parla di processu shared semaphore) la sola scelta possibile per renderlo visibile a tutti ` di porlo in un tratto di e memoria condivisa. Questo potr` essere ottenuto direttamente sia con shmget (vedi sez. 12.2.6) a che con shm_open (vedi sez. 12.4.3), oppure, nel caso che tutti i processi in gioco abbiano un genitore comune, con una mappatura anonima con mmap (vedi sez. 11.3.1),69 a cui essi poi potranno accedere. Una volta inizializzato il semaforo anonimo con sem_init lo si potr` utilizzare nello stesso a modo dei semafori normali con sem_wait e sem_post. Si tenga presente per` che inizializzare o due volte lo stesso semaforo pu` dar luogo ad un comportamento indenito. o Una volta che non si indenda pi` utilizzare un semaforo anonimo questo pu` essere eliminato u o da sistema; per far questo di deve utilizzare una apposita funzione, sem_destroy, il cui prototipo `: e
#include <semaphore.h> int sem_destroy(sem_t *sem) Elimina il semaforo anonimo sem. La funzione restituisce 0 in caso di successo e 1 in caso di errore; nel quel caso errno assumer` a i valori: EINVAL il valore di value eccede SEM_VALUE_MAX.

La funzione prende come unico argomento lindirizzo di un semaforo che deve essere stato inizializzato con sem_init; non deve quindi essere applicata a semafori creati con sem_open. Inoltre si deve essere sicuri che il semaforo sia eettivamente inutilizzato, la distruzione di un semaforo su cui sono presenti processi (o thread) in attesa (cio` bloccati in una sem_wait) e provoca un comportamento indenito.
69

si ricordi che i tratti di memoria condivisa vengono mantenuti nei processi gli attraverso la funzione fork.

400

CAPITOLO 12. LA COMUNICAZIONE FRA PROCESSI

Si tenga presente inne che utilizzare un semaforo che ` stato distrutto con sem_destroy e di nuovo pu` dare esito a comportamenti indeniti. Nel caso ci si trovi in una tale evenienza o occorre reinizializzare il semaforo una seconda volta con sem_init.

Capitolo 13

I thread
Tratteremo in questo capitolo un modello di programmazione multitasking, quello dei thread, alternativo al modello classico dei processi, tipico di Unix. Ne esamineremo le caratteristiche, vantaggi e svantaggi, e le diverse realizzazioni che sono disponibili per Linux; nella seconda parte tratteremo in dettaglio quella che ` limplementazione principale, che fa riferimento allinterfaccia e standardizzata da POSIX.1e.

13.1

Introduzione ai thread

Questa prima sezione costituisce una introduzione ai thread e tratter` i concetti principali del a relativo modello di programmazione, esamineremo anche quali modelli sono disponibili per Linux, dando una breve panoramica sulle implementazioni alternative.

13.1.1

Una panoramica

Il modello classico dellesecuzione dei programmi nei sistemi Unix, illustrato in sez. 2, ` fondato e sui processi. Il modello nasce per assicurare la massima stabilit` al sistema e prevede una rigida a separazione fra i diversi processi, in modo che questi non possano disturbarsi a vicenda. Le applicazioni moderne per` sono altamente concorrenti, e necessitano quindi di un gran o numero di processi; questo ha portato a scontrarsi con alcuni limiti dellarchitettura precedente. In genere i fautori del modello di programmazione a thread sottolineano due problemi connessi alluso dei processi:

13.1.2 13.1.3

I thread e Linux Implementazioni alternative

13.2

Posix thread

Tratteremo in questa sezione linterfaccia di programmazione con i thread standardizzata dallo standard POSIX 1.c, che ` quella che ` stata seguita anche dalle varie implementazioni dei thread e e realizzate su Linux, ed in particolare dalla Native Thread Posix Library che ` stata integrata e con i kernel della serie 2.6 e che fa parte a pieno titolo delle glibc. 401

402

CAPITOLO 13. I THREAD

13.2.1 13.2.2 13.2.3 13.2.4

Una panoramica La gestione dei thread I mutex Le variabili di condizione

Parte II

Programmazione di rete

403

Capitolo 14

Introduzione alla programmazione di rete


In questo capitolo sar` fatta unintroduzione ai concetti generali che servono come prerequisiti a per capire la programmazione di rete, non tratteremo quindi aspetti specici ma faremo una breve introduzione al modello pi` comune usato nella programmazione di rete, per poi passare u ad un esame a grandi linee dei protocolli di rete e di come questi sono organizzati e interagiscono. In particolare, avendo assunto lottica di unintroduzione mirata alla programmazione, ci concentreremo sul protocollo pi` diuso, il TCP/IP, che ` quello che sta alla base di internet, u e avendo cura di sottolineare i concetti pi` importanti da conoscere per la scrittura dei programmi. u

14.1

Modelli di programmazione

La dierenza principale fra unapplicazione di rete e un programma normale ` che questule tima per denizione concerne la comunicazione fra processi diversi, che in generale non girano neanche sulla stessa macchina. Questo gi` pregura un cambiamento completo rispetto a allottica del programma monolitico allinterno del quale vengono eseguite tutte le istruzioni, e chiaramente presuppone un sistema operativo multitasking in grado di eseguire pi` processi u contemporaneamente. In questa prima sezione esamineremo brevemente i principali modelli di programmazione in uso. Ne daremo una descrizione assolutamente generica e superciale, che ne illustri le caratteristiche principali, non essendo fra gli scopi del testo approfondire questi argomenti.

14.1.1

Il modello client-server

Larchitettura fondamentale su cui si basa gran parte della programmazione di rete sotto Linux (e sotto Unix in generale) ` il modello client-server caratterizzato dalla presenza di due categorie e di soggetti, i programmi di servizio, chiamati server, che ricevono le richieste e forniscono le risposte, ed i programmi di utilizzo, detti client. In generale un server pu` (di norma deve) essere in grado di rispondere a pi` di un client, o u per cui ` possibile che molti programmi possano interagire contemporaneamente, quello che e contraddistingue il modello per` ` che larchitettura dellinterazione ` sempre nei termini di o e e molti verso uno, il server, che viene ad assumere un ruolo privilegiato. Seguono questo modello tutti i servizi fondamentali di internet, come le pagine web, la posta elettronica, ftp, telnet, ssh e praticamente ogni servizio che viene fornito tramite la rete, anche se, come abbiamo visto, il modello ` utilizzato in generale anche per programmi che, come gli e esempi che abbiamo usato in cap. 12 a proposito della comunicazione fra processi nello stesso sistema, non fanno necessariamente uso della rete. 405

406

CAPITOLO 14. INTRODUZIONE ALLA PROGRAMMAZIONE DI RETE

Normalmente si dividono i server in due categorie principali, e vengono detti concorrenti o iterativi, sulla base del loro comportamento. Un server iterativo risponde alla richiesta inviando i dati e resta occupato e non rispondendo ad ulteriori richieste ntanto che non ha fornito una risposta alla richiesta. Una volta completata la risposta il server diventa di nuovo disponibile. Un server concorrente al momento di trattare la richiesta crea un processo glio (o un thread) incaricato di fornire i servizi richiesti, per porsi immediatamente in attesa di ulteriori richieste. In questo modo, con sistemi multitasking, pi` richieste possono essere soddisfatte u contemporaneamente. Una volta che il processo glio ha concluso il suo lavoro esso di norma viene terminato, mentre il server originale resta sempre attivo.

14.1.2

Il modello peer-to-peer

Come abbiamo visto il tratto saliente dellarchitettura client-server ` quello della preminenza e del server rispetto ai client, le architetture peer-to-peer si basano su un approccio completamente opposto che ` quello di non avere nessun programma che svolga un ruolo preminente. e Questo vuol dire che in generale ciascun programma viene ad agire come un nodo in una rete potenzialmente paritetica; ciascun programma si trova pertanto a ricevere ed inviare richieste ed a ricevere ed inviare risposte, e non c` pi` la separazione netta dei compiti che si ritrova nelle e u architetture client-server. Le architetture peer-to-peer sono salite alla ribalta con lesplosione del fenomeno Napster, ma gli stessi protocolli di routing sono un buon esempio di architetture peer-to-peer, in cui ciascun nodo, tramite il demone che gestisce il routing, richiede ed invia informazioni ad altri nodi. In realt` in molti casi di architetture classicate come peer-to-peer non ` detto che la struttura a e sia totalmente paritetica e ci sono parecchi esempi in cui alcuni servizi vengono centralizzati o distribuiti gerarchicamente, come per lo stesso Napster, in cui le ricerche venivano eettuate su un server centrale.

14.1.3

Il modello three-tier

Bench qui sia trattato a parte, il modello three-tier in realt` ` una estensione del modello e a e client-server. Con il crescere della quantit` dei servizi forniti in rete (in particolare su internet) a ed al numero di accessi richiesto. Si ` cos` assistito anche ad una notevole crescita di complessit`, e a in cui diversi servizi venivano ad essere integrati fra di loro. In particolare sempre pi` spesso si assiste ad una integrazione di servizi di database con u servizi di web, in cui le pagine vengono costruite dinamicamente sulla base dei dati contenuti nel database. In tutti questi casi il problema fondamentale di una architettura client-server ` che e la richiesta di un servizio da parte di un gran numero di client si scontra con il collo di bottiglia dellaccesso diretto ad un unico server, con gravi problemi di scalabilit`. a Rispondere a queste esigenze di scalabilit` il modello pi` semplice (chiamato talvolta twoa u tier ) da adottare ` stata quello di distribuire il carico delle richieste su pi` server identici, e u mantenendo quindi sostanzialmente inalterata larchitettura client-server originale. Nel far questo ci si scontra per` con gravi problemi di manutenibilit` dei servizi, in particolare o a per quanto riguarda la sincronizzazione dei dati, e di inecienza delluso delle risorse. Il problema ` particolarmente grave ad esempio per i database che non possono essere replicati e sincronizzati e facilmente, e che sono molto onerosi, la loro replicazione ` costosa e complessa. e ` a partire da queste problematiche che nasce il modello three-tier, che si struttura, come E dice il nome, su tre livelli. Il primo livello, quello dei client che eseguono le richieste e gestiscono linterfaccia con lutente, resta sostanzialmente lo stesso del modello client-server, ma la parte server viene suddivisa in due livelli, introducendo un middle-tier, su cui deve appoggiarsi tutta la logica di analisi delle richieste dei client per ottimizzare laccesso al terzo livello, che ` quello che e

14.2. I PROTOCOLLI DI RETE

407

si limita a fornire i dati dinamici che verranno usati dalla logica implementata nel middle-tier per eseguire le operazioni richieste dai client. In questo modo si pu` disaccoppiare la logica dai dati, replicando la prima, che ` molto o e meno soggetta a cambiamenti ed evoluzione, e non sore di problemi di sincronizzazione, e centralizzando opportunamente i secondi. In questo modo si pu` distribuire il carico ed accedere o in maniera eciente i dati.

14.2

I protocolli di rete

Parlando di reti di computer si parla in genere di un insieme molto vasto ed eterogeneo di mezzi di comunicazione che vanno dal cavo telefonico, alla bra ottica, alle comunicazioni via satellite o via radio; per rendere possibile la comunicazione attraverso un cos` variegato insieme di mezzi sono stati adottati una serie di protocolli, il pi` famoso dei quali, quello alla base del u funzionamento di internet, ` il protocollo TCP/IP. e

14.2.1

Il modello ISO/OSI

Una caratteristica comune dei protocolli di rete ` il loro essere strutturati in livelli sovrapposti; e in questo modo ogni protocollo di un certo livello realizza le sue funzionalit` basandosi su a un protocollo del livello sottostante. Questo modello di funzionamento ` stato standardizzato e dalla International Standards Organization (ISO) che ha preparato n dal 1984 il Modello di Riferimento Open Systems Interconnection (OSI), strutturato in sette livelli, secondo quanto riportato in tab. 14.1. Livello Livello 7 Livello 6 Livello 5 Livello 4 Livello 3 Livello 2 Livello 1 Nome Application Applicazione Presentation Presentazione Session Sessione Transport Trasporto Network Rete DataLink Collegamento Dati Physical Connessione Fisica

Tabella 14.1: I sette livelli del protocollo ISO/OSI.

Il modello ISO/OSI ` stato sviluppato in corrispondenza alla denizione della serie di protoe colli X.25 per la commutazione di pacchetto; come si vede ` un modello abbastanza complesso1 , e tanto che usualmente si tende a suddividerlo in due parti, secondo lo schema mostrato in g. 14.1, con un upper layer che riguarda solo le applicazioni, che viene realizzato in user space, ed un lower layer in cui si mescolano la gestione fatta dal kernel e le funzionalit` fornite dallhardware. a Il modello ISO/OSI mira ad eettuare una classicazione completamente generale di ogni tipo di protocollo di rete; nel frattempo per` era stato sviluppato anche un altro modello, relativo o al protocollo TCP/IP, che ` quello su cui ` basata internet, che ` diventato uno standard de facto. e e e Questo modello viene talvolta chiamato anche modello DoD (sigla che sta per Department of Defense), dato che fu sviluppato dallagenzia ARPA per il Dipartimento della Difesa Americano. La scelta fra quale dei due modelli utilizzare dipende per lo pi` dai gusti personali. Come u caratteristiche generali il modello ISO/OSI ` pi` teorico e generico, basato separazioni funzionali, e u
infatti per memorizzarne i vari livelli ` stata creata la frase All people seem to need data processing, in cui e ciascuna parola corrisponde alliniziale di uno dei livelli.
1

408

CAPITOLO 14. INTRODUZIONE ALLA PROGRAMMAZIONE DI RETE

Figura 14.1: Struttura a livelli dei protocolli OSI e TCP/IP, con la relative corrispondenze e la divisione fra kernel e user space.

mentre il modello TCP/IP ` pi` vicino alla separazione concreta dei vari strati del sistema e u operativo; useremo pertanto questultimo, anche per la sua maggiore semplicit`.2 a

14.2.2

Il modello TCP/IP (o DoD)

Cos` come ISO/OSI anche il modello del TCP/IP ` stato strutturato in livelli (riassunti in e tab. 14.2); un confronto fra i due ` riportato in g. 14.1 dove viene evidenziata anche la core rispondenza fra i rispettivi livelli (che comunque ` approssimativa) e su come essi vanno ad e inserirsi allinterno del sistema rispetto alla divisione fra user space e kernel space spiegata in sez. 1.1.3 Livello Livello 4 Livello 3 Livello 2 Livello 1 Nome Application Applicazione Transport Trasporto Network Rete Link Collegamento Esempi Telnet, FTP, ecc. TCP, UDP IP, (ICMP, IGMP) Device driver & scheda di interfaccia

Tabella 14.2: I quattro livelli del protocollo TCP/IP.

Come si pu` notare come il modello TCP/IP ` pi` semplice del modello ISO/OSI ed ` struto e u e turato in soli quattro livelli. Il suo nome deriva dai due principali protocolli che lo compongono, il TCP (Trasmission Control Protocol ) che copre il livello 3 e lIP (Internet Protocol ) che copre il livello 2. Le funzioni dei vari livelli sono le seguenti: Applicazione E relativo ai programmi di interfaccia con la rete, in genere questi vengono realizzati secondo il modello client-server (vedi sez. 14.1.1), realizzando una comunicazione secondo un protocollo che ` specico di ciascuna applicazione. e Trasporto Fornisce la comunicazione tra le due stazioni terminali su cui girano gli applicativi, regola il usso delle informazioni, pu` fornire un trasporto adabile, cio` o e

2 questa semplicit` ha un costo quando si fa riferimento agli strati pi` bassi, che sono in eetti descritti meglio a u dal modello ISO/OSI, in quanto gran parte dei protocolli di trasmissione hardware sono appunto strutturati sui due livelli di Data Link e Connection. 3 in realt` ` sempre possibile accedere dallo user space, attraverso una opportuna interfaccia (come vedremo in ae sez. 15.3.6), ai livelli inferiori del protocollo.

14.2. I PROTOCOLLI DI RETE

409

con recupero degli errori o inadabile. I protocolli principali di questo livello sono il TCP e lUDP. Rete Si occupa dello smistamento dei singoli pacchetti su una rete complessa e interconnessa, a questo stesso livello operano i protocolli per il reperimento delle informazioni necessarie allo smistamento, per lo scambio di messaggi di controllo e per il monitoraggio della rete. Il protocollo su cui si basa questo livello ` IP e (sia nella attuale versione, IPv4, che nella nuova versione, IPv6).

Collegamento ` E responsabile per linterfacciamento al dispositivo elettronico che eettua la comunicazione sica, gestendo linvio e la ricezione dei pacchetti da e verso lhardware. La comunicazione fra due stazioni remote avviene secondo le modalit` illustrate in g. 14.2, a dove si ` riportato il usso dei dati reali e i protocolli usati per lo scambio di informazione su e ciascun livello. Si ` genericamente indicato ethernet per il livello 1, anche se in realt` i protocolli e a di trasmissione usati possono essere molti altri.

Figura 14.2: Strutturazione del usso dei dati nella comunicazione fra due applicazioni attraverso i protocolli della suite TCP/IP.

Per chiarire meglio la struttura della comunicazione attraverso i vari protocolli mostrata in g. 14.2, conviene prendere in esame i singoli passaggi fatti per passare da un livello al sottostante, la procedura si pu` riassumere nei seguenti passi: o Le singole applicazioni comunicano scambiandosi i dati ciascuna secondo un suo specico formato. Per applicazioni generiche, come la posta o le pagine web, viene di solito denito ed implementato quello che viene chiamato un protocollo di applicazione (esempi possono

410

CAPITOLO 14. INTRODUZIONE ALLA PROGRAMMAZIONE DI RETE essere HTTP, POP, SMTP, ecc.), ciascuno dei quali ` descritto in un opportuno standard e (di solito attraverso un RFC4 ). I dati delle applicazioni vengono inviati al livello di trasporto usando uninterfaccia opportuna (i socket, che esamineremo in dettaglio in cap. 15). Qui verranno spezzati in pacchetti di dimensione opportuna e inseriti nel protocollo di trasporto, aggiungendo ad ogni pacchetto le informazioni necessarie per la sua gestione. Questo processo viene svolto direttamente nel kernel, ad esempio dallo stack TCP, nel caso il protocollo di trasporto usato sia questo. Una volta composto il pacchetto nel formato adatto al protocollo di trasporto usato questo sar` passato al successivo livello, quello di rete, che si occupa di inserire le opportune a informazioni per poter eettuare linstradamento nella rete ed il recapito alla destinazione nale. In genere questo ` il livello di IP (Internet Protocol), a cui vengono inseriti i numeri e IP che identicano i computer su internet. Lultimo passo ` il trasferimento del pacchetto al driver della interfaccia di trasmissione, che e si incarica di incapsularlo nel relativo protocollo di trasmissione. Questo pu` avvenire sia o in maniera diretta, come nel caso di ethernet, in cui i pacchetti vengono inviati sulla linea attraverso le schede di rete, che in maniera indiretta con protocolli come PPP o SLIP, che vengono usati come interfaccia per far passare i dati su altri dispositivi di comunicazione (come la seriale o la parallela).

14.2.3

Criteri generali dellarchitettura del TCP/IP

La losoa architetturale del TCP/IP ` semplice: costruire una rete che possa sopportare il carico e in transito, ma permettere ai singoli nodi di scartare pacchetti se il carico ` temporaneamente e eccessivo, o se risultano errati o non recapitabili. Lincarico di rendere il recapito pacchetti adabile non spetta al livello di rete, ma ai livelli superiori. Pertanto il protocollo IP ` per sua natura inadabile, in quanto non ` assicurata n e e e una percentuale di successo n un limite sui tempi di consegna dei pacchetti. e ` E il livello di trasporto che si deve occupare (qualora necessiti) del controllo del usso dei dati e del recupero degli errori; questo ` realizzato dal protocollo TCP. La sede principale di e intelligenza della rete ` pertanto al livello di trasporto o ai livelli superiori. e Inne le singole stazioni collegate alla rete non fungono soltanto da punti terminali di comunicazione, ma possono anche assumere il ruolo di router (instradatori), per linterscambio di pacchetti da una rete ad unaltra. Questo rende possibile la essibilit` della rete che ` in grado a e di adattarsi ai mutamenti delle interconnessioni. La caratteristica essenziale che rende tutto ci` possibile ` la strutturazione a livelli tramite o e lincapsulamento. Ogni pacchetto di dati viene incapsulato nel formato del livello successivo, no al livello del collegamento sico. In questo modo il pacchetto ricevuto ad un livello n dalla stazione di destinazione ` esattamente lo stesso spedito dal livello n dalla sorgente. Questo rende e facile il progettare il software facendo riferimento unicamente a quanto necessario ad un singolo livello, con la condenza che questo poi sar` trattato uniformemente da tutti i nodi della rete. a

14.3

Il protocollo TCP/IP

Come accennato in sez. 14.2 il protocollo TCP/IP ` un insieme di protocolli diversi, che opee rano su 4 livelli diversi. Per gli interessi della programmazione di rete per` sono importanti o principalmente i due livelli centrali, e soprattutto quello di trasporto.
lacronimo RFC sta per Request For Comment ed ` la procedura attraverso la quale vengono proposti gli e standard per Internet.
4

14.3. IL PROTOCOLLO TCP/IP

411

La principale interfaccia usata nella programmazione di rete, quella dei socket (vedi sez. 15), ` infatti uninterfaccia nei confronti di questultimo. Questo avviene perch al di sopra del livello e e di trasporto i programmi hanno a che fare solo con dettagli specici delle applicazioni, mentre al ` di sotto vengono curati tutti i dettagli relativi alla comunicazione. E pertanto naturale denire una interfaccia di programmazione su questo conne, tanto pi` che ` proprio l` (come evidenziato u e in g. 14.1) che nei sistemi Unix (e non solo) viene inserita la divisione fra kernel space e user space. In realt` in un sistema Unix ` possibile accedere anche agli altri livelli inferiori (e non solo a e a quello di trasporto) con opportune interfacce di programmazione (vedi sez. 15.3.6), ma queste vengono usate solo quando si debbano fare applicazioni di sistema per il controllo della rete a basso livello, di uso quindi molto specialistico. In questa sezione daremo una descrizione sommaria dei vari protocolli del TCP/IP, concentrandoci, per le ragioni appena esposte, sul livello di trasporto. Allinterno di questultimo privilegeremo poi il protocollo TCP, per il ruolo centrale che svolge nella maggior parte delle applicazioni.

14.3.1

Il quadro generale

Bench si parli di TCP/IP questa famiglia di protocolli ` composta anche da molti membri. e e In g. 14.3 si ` riportato uno schema che mostra un panorama sui principali protocolli della e famiglia, e delle loro relazioni reciproche e con alcune dalle principali applicazioni che li usano.

Figura 14.3: Panoramica sui vari protocolli che compongono la suite TCP/IP.

I vari protocolli riportati in g. 14.3 sono i seguenti: IPv4 ` Internet Protocol version 4. E quello che comunemente si chiama IP. Ha origine negli anni 80 e da allora ` la base su cui ` costruita internet. Usa indirizzi a 32 bit, e e e mantiene tutte le informazioni di instradamento e controllo per la trasmissione dei pacchetti sulla rete; tutti gli altri protocolli della suite (eccetto ARP e RARP, e quelli specici di IPv6) vengono trasmessi attraverso di esso.

412 IPv6

CAPITOLO 14. INTRODUZIONE ALLA PROGRAMMAZIONE DI RETE ` Internet Protocol version 6. E stato progettato a met` degli anni 90 per rimpiazzare a IPv4. Ha uno spazio di indirizzi ampliato 128 bit che consente pi` gerarchie di indiu rizzi, lautocongurazione, ed un nuovo tipo di indirizzi, gli anycast, che consentono di inviare un pacchetto ad una stazione su un certo gruppo. Eettua lo stesso servizio di trasmissione dei pacchetti di IPv4 di cui vuole essere un sostituto. ` Trasmission Control Protocol. E un protocollo orientato alla connessione che provvede un trasporto adabile per un usso di dati bidirezionale fra due stazioni remote. Il protocollo ha cura di tutti gli aspetti del trasporto, come lacknoweledgment, i ` timeout, la ritrasmissione, ecc. E usato dalla maggior parte delle applicazioni. ` User Datagram Protocol. E un protocollo senza connessione, per linvio di dati a pacchetti. Contrariamente al TCP il protocollo non ` adabile e non c` garanzia e e che i pacchetti raggiungano la loro destinazione, si perdano, vengano duplicati, o abbiano un particolare ordine di arrivo. ` Internet Control Message Protocol. E il protocollo usato a livello 2 per gestire gli errori e trasportare le informazioni di controllo fra stazioni remote e instradatori (cio` fra e host e router ). I messaggi sono normalmente generati dal software del kernel che gestisce la comunicazione TCP/IP, anche se ICMP pu` venire usato direttamente o da alcuni programmi come ping. A volte ci si riferisce ad esso come ICPMv4 per distinguerlo da ICMPv6. Internet Group Management Protocol. E un protocollo di livello 2 usato per il multicast (vedi sez. ??). Permette alle stazioni remote di noticare ai router che supportano questa comunicazione a quale gruppo esse appartengono. Come ICMP viene implementato direttamente sopra IP. ` Address Resolution Protocol. E il protocollo che mappa un indirizzo IP in un indirizzo ` hardware sulla rete locale. E usato in reti di tipo broadcast come Ethernet, Token Ring o FDDI che hanno associato un indirizzo sico (il MAC address) alla interfaccia, ma non serve in connessioni punto-punto. ` Reverse Address Resolution Protocol. E il protocollo che esegue loperazione inversa rispetto ad ARP (da cui il nome) mappando un indirizzo hardware in un indirizzo IP. Viene usato a volte per durante lavvio per assegnare un indirizzo IP ad una macchina.

TCP

UDP

ICMP

IGMP

ARP

RARP

ICMPv6 Internet Control Message Protocol, version 6. Combina per IPv6 le funzionalit` di a ICMPv4, IGMP e ARP. EGP ` Exterior Gateway Protocol. E un protocollo di routing usato per comunicare lo stato fra gateway vicini a livello di sistemi autonomi 5 , con meccanismi che permettono di identicare i vicini, controllarne la raggiungibilit` e scambiare informazioni sullo a stato della rete. Viene implementato direttamente sopra IP. ` Open Shortest Path First. E in protocollo di routing per router su reti interne, che permette a questi ultimi di scambiarsi informazioni sullo stato delle connessioni e dei legami che ciascuno ha con gli altri. Viene implementato direttamente sopra IP. ` Generic Routing Encapsulation. E un protocollo generico di incapsulamento che permette di incapsulare un qualunque altro protocollo allinterno di IP.

OSPF

GRE
5

vengono chiamati autonomous systems i raggruppamenti al livello pi` alto della rete. u

14.3. IL PROTOCOLLO TCP/IP AH

413

Authentication Header. Provvede lautenticazione dellintegrit` e dellorigine di un a ` pacchetto. E una opzione nativa in IPv6 e viene implementato come protocollo a s su IPv4. Fa parte della suite di IPSEC che provvede la trasmissione cifrata ed e autenticata a livello IP. Encapsulating Security Payload. Provvede la cifratura insieme allautenticazione dellintegrit` e dellorigine di un pacchetto. Come per AH ` opzione nativa in IPv6 e a e viene implementato come protocollo a s su IPv4. e ` Point-to-Point Protocol. E un protocollo a livello 1 progettato per lo scambio di pacchetti su connessioni punto punto. Viene usato per congurare i collegamenti, ` denire i protocolli di rete usati ed incapsulare i pacchetti di dati. E un protocollo complesso con varie componenti. ` Serial Line over IP. E un protocollo di livello 1 che permette di trasmettere un pacchetto IP attraverso una linea seriale.

ESP

PPP

SLIP

Gran parte delle applicazioni comunicano usando TCP o UDP, solo alcune, e per scopi particolari si rifanno direttamente ad IP (ed i suoi correlati ICMP e IGMP); bench sia TCP e che UDP siano basati su IP e sia possibile intervenire a questo livello con i raw socket questa tecnica ` molto meno diusa e a parte applicazioni particolari si preferisce sempre usare i servizi e messi a disposizione dai due protocolli precedenti. Per questo, motivo a parte alcuni brevi accenni su IP in questa sezione, ci concentreremo sul livello di trasporto.

14.3.2

Internet Protocol (IP)

Quando si parla di IP ci si riferisce in genere alla versione attualmente in uso che ` la versione 4 e (e viene pertanto chiamato IPv4). Questa versione venne standardizzata nel 1981 dallRFC 719. Internet Protocol nasce per disaccoppiare le applicazioni della struttura hardware delle reti di trasmissione, e creare una interfaccia di trasmissione dei dati indipendente dal sottostante substrato di rete, che pu` essere realizzato con le tecnologie pi` disparate (Ethernet, Token o u Ring, FDDI, ecc.). Il compito di IP ` pertanto quello di trasmettere i pacchetti da un computer e allaltro della rete; le caratteristiche essenziali con cui questo viene realizzato in IPv4 sono due: Universal addressing la comunicazione avviene fra due stazioni remote identicate univocamente con un indirizzo a 32 bit che pu` appartenere ad una sola interfaccia di o rete. Best eort viene assicurato il massimo impegno nella trasmissione, ma non c` nessuna e garanzia per i livelli superiori n sulla percentuale di successo n sul tempo di consegna e e dei pacchetti di dati. Negli anni 90 la crescita vertiginosa del numero di macchine connesse a internet ha iniziato a far emergere i vari limiti di IPv4, per risolverne i problemi si ` perci` denita una nuova e o versione del protocollo, che (saltando un numero) ` diventata la versione 6. IPv6 nasce quindi e come evoluzione di IPv4, mantenendone inalterate le funzioni che si sono dimostrate valide, eliminando quelle inutili e aggiungendone poche altre per mantenere il protocollo il pi` snello e u veloce possibile. I cambiamenti apportati sono comunque notevoli e si possono essere riassunti a grandi linee nei seguenti punti: lespansione delle capacit` di indirizzamento e instradamento, per supportare una gerarchia a con pi` livelli di indirizzamento, un numero di nodi indirizzabili molto maggiore e una u autocongurazione degli indirizzi.

414

CAPITOLO 14. INTRODUZIONE ALLA PROGRAMMAZIONE DI RETE lintroduzione un nuovo tipo di indirizzamento, lanycast che si aggiunge agli usuali unicast e multicast. la semplicazione del formato dellintestazione (header ) dei pacchetti, eliminando o rendendo opzionali alcuni dei campi di IPv4, per eliminare la necessit` di rielaborazione della a stessa da parte dei router e contenere laumento di dimensione dovuto allampliamento degli indirizzi. un supporto per le opzioni migliorato, per garantire una trasmissione pi` eciente del trafu co normale, limiti meno stringenti sulle dimensioni delle opzioni, e la essibilit` necessaria a per introdurne di nuove in futuro. il supporto per delle capacit` di qualit` di servizio (QoS) che permettano di identicare a a gruppi di dati per i quali si pu` provvedere un trattamento speciale (in vista delluso di o internet per applicazioni multimediali e/o real-time).

Maggiori dettagli riguardo a caratteristiche, notazioni e funzionamento del protocollo IP sono forniti nellappendice sez. A.1.

14.3.3

User Datagram Protocol (UDP)

UDP ` un protocollo di trasporto molto semplice; la sua descrizione completa ` contenuta e e dellRFC 768, ma in sostanza esso ` una semplice interfaccia al protocollo IP dal livello di e trasporto. Quando unapplicazione usa UDP essa scrive un pacchetto di dati (il cosiddetto datagram che da il nome al protocollo) su un socket, al pacchetto viene aggiunto un header molto semplice (per una descrizione pi` accurata vedi sez. B.2), e poi viene passato al livello superiore u (IPv4 o IPv6 che sia) che lo spedisce verso la destinazione. Dato che n IPv4 n IPv6 garane e tiscono ladabilit` niente assicura che il pacchetto arrivi a destinazione, n che pi` pacchetti a e u arrivino nello stesso ordine in cui sono stati spediti. Pertanto il problema principale che si aronta quando si usa UDP ` la mancanza di adae bilit`, se si vuole essere sicuri che i pacchetti arrivino a destinazione occorrer` provvedere con a a lapplicazione, allinterno della quale si dovr` inserire tutto quanto necessario a gestire la notica a di ricevimento, la ritrasmissione, il timeout. Si tenga conto poi che in UDP niente garantisce che i pacchetti arrivino nello stesso ordine in cui sono stati trasmessi, e pu` anche accadere che i pacchetti vengano duplicati nella trasmissione, o e non solo perduti. Di tutto questo di nuovo deve tenere conto lapplicazione. Un altro aspetto di UDP ` che se un pacchetto raggiunge correttamente la destinazione esso e viene passato allapplicazione ricevente in tutta la sua lunghezza, la trasmissione avviene perci` o per record la cui lunghezza viene anche essa trasmessa allapplicazione allatto del ricevimento. Inne UDP ` un protocollo che opera senza connessione (connectionless) in quanto non ` e e necessario stabilire nessun tipo di relazione tra origine e destinazione dei pacchetti. Si hanno cos` situazioni in cui un client pu` scrivere su uno stesso socket pacchetti destinati a server diversi, o o un server ricevere su un socket pacchetti provenienti da client diversi. Il modo pi` semplice di u immaginarsi il funzionamento di UDP ` quello della radio, in cui si pu` trasmettere e ricevere e o da pi` stazioni usando la stessa frequenza. u Nonostante gli evidenti svantaggi comportati dallinadabilit` UDP ha il grande pregio a della velocit`, che in certi casi ` essenziale; inoltre si presta bene per le applicazioni in cui la a e connessione non ` necessaria, e costituirebbe solo un peso in termini di prestazioni, mentre una e perdita di pacchetti pu` essere tollerata: ad esempio le applicazioni di streaming e quelle che o usano il multicast.

14.3. IL PROTOCOLLO TCP/IP

415

14.3.4

Transport Control Protocol (TCP)

Il TCP ` un protocollo molto complesso, denito nellRFC 739 e completamente diverso da e UDP; alla base della sua progettazione infatti non stanno semplicit` e velocit`, ma la ricerca a a della massima adabilit` possibile nella trasmissione dei dati. a La prima dierenza con UDP ` che TCP provvede sempre una connessione diretta fra un e client e un server, attraverso la quale essi possono comunicare; per questo il paragone pi` approu priato per questo protocollo ` quello del collegamento telefonico, in quanto prima viene stabilita e una connessione fra due i due capi della comunicazione su cui poi eettuare questultima. Caratteristica fondamentale di TCP ` ladabilit`; quando i dati vengono inviati attraverso e a una connessione ne viene richiesto un ricevuto (il cosiddetto acknowlegment), se questo non arriva essi verranno ritrasmessi per un determinato numero di tentativi, intervallati da un periodo di tempo crescente, no a che sar` considerata fallita o caduta la connessione (e sar` generato un a a errore di timeout); il periodo di tempo dipende dallimplementazione e pu` variare far i quattro o e i dieci minuti. Inoltre, per tenere conto delle diverse condizioni in cui pu` trovarsi la linea di comunicazione, o TCP comprende anche un algoritmo di calcolo dinamico del tempo di andata e ritorno dei pacchetti fra un client e un server (il cosiddetto RTT, Round Trip Time), che lo rende in grado di adattarsi alle condizioni della rete per non generare inutili ritrasmissioni o cadere facilmente in timeout. Inoltre TCP ` in grado di preservare lordine dei dati assegnando un numero di sequenza e ad ogni byte che trasmette. Ad esempio se unapplicazione scrive 3000 byte su un socket TCP, questi potranno essere spezzati dal protocollo in due segmenti (le unit` di dati passate da TCP a a IP vengono chiamate segment) di 1500 byte, di cui il primo conterr` il numero di sequenza a 1 1500 e il secondo il numero 1501 3000. In questo modo anche se i segmenti arrivano a destinazione in un ordine diverso, o se alcuni arrivano pi` volte a causa di ritrasmissioni dovute u alla perdita degli acknowlegment, allarrivo sar` comunque possibile riordinare i dati e scartare a i duplicati. Il protocollo provvede anche un controllo di usso (ow control ), cio` specica sempre ale laltro capo della trasmissione quanti dati pu` ricevere tramite una advertised window (letteo ralmente nestra annunciata), che indica lo spazio disponibile nel buer di ricezione, cosicch e nella trasmissione non vengano inviati pi` dati di quelli che possono essere ricevuti. u Questa nestra cambia dinamicamente diminuendo con la ricezione dei dati dal socket ed aumentando con la lettura di questultimo da parte dellapplicazione, se diventa nulla il buer di ricezione ` pieno e non verranno accettati altri dati. Si noti che UDP non provvede niente di e tutto ci` per cui nulla impedisce che vengano trasmessi pacchetti ad un ritmo che il ricevente o non pu` sostenere. o Inne attraverso TCP la trasmissione ` sempre bidirezionale (in inglese si dice che ` fulle e ` duplex ). E cio` possibile sia trasmettere che ricevere allo stesso tempo, il che comporta che e quanto dicevamo a proposito del controllo di usso e della gestione della sequenzialit` dei dati a viene eettuato per entrambe le direzioni di comunicazione.

14.3.5

Limiti e dimensioni riguardanti la trasmissione dei dati

Un aspetto di cui bisogna tenere conto nella programmazione di rete, e che ritorner` in seguito, a quando tratteremo gli aspetti pi` avanzati, ` che ci sono una serie di limiti a cui la trasmisu e sione dei dati attraverso i vari livelli del protocollo deve sottostare; limiti che ` opportuno e tenere presente perch in certi casi si possono avere delle conseguenze sul comportamento delle e applicazioni. Un elenco di questi limiti, insieme ad un breve accenno alle loro origini ed alle eventuali implicazioni che possono avere, ` il seguente: e

416

CAPITOLO 14. INTRODUZIONE ALLA PROGRAMMAZIONE DI RETE La dimensione massima di un pacchetto IP ` di 65535 byte, compresa lintestazione. Questo e ` dovuto al fatto che la dimensione ` indicata da un campo apposito nellheader di IP che e e ` lungo 16 bit (vedi g. A.1). e La dimensione massima di un pacchetto normale di IPv6 ` di 65575 byte; il campo apposito e nellheader infatti ` sempre a 16 bit, ma la dimensione dellheader ` ssa e di 40 byte e e e non ` compresa nel valore indicato dal suddetto campo. Inoltre IPv6 ha la possibilit` di e a estendere la dimensione di un pacchetto usando la jumbo payload option. Molte reti siche hanno una MTU (Maximum Transfer Unit) che dipende dal protocollo specico usato al livello di connessione sica. Il pi` comune ` quello di ethernet che ` pari u e e a 1500 byte, una serie di altri valori possibili sono riportati in tab. 14.3.

Quando un pacchetto IP viene inviato su una interfaccia di rete e le sue dimensioni eccedono la MTU viene eseguita la cosiddetta frammentazione, i pacchetti cio` vengono suddivisi6 ) in e blocchi pi` piccoli che possono essere trasmessi attraverso linterfaccia. u Rete Hyperlink Token Ring IBM (16 Mbit/sec) Token Ring IEEE 802.5 (4 Mbit/sec) FDDI Ethernet X.25 MTU 65535 17914 4464 4532 1500 576

Tabella 14.3: Valori della MTU (Maximum Transfer Unit) per una serie di diverse tecnologie di rete.

La MTU pi` piccola fra due stazioni viene in genere chiamata path MTU, che dice qual ` u e la lunghezza massima oltre la quale un pacchetto inviato da una stazione ad unaltra verrebbe senzaltro frammentato. Si tenga conto che non ` aatto detto che la path MTU sia la stessa e in entrambe le direzioni, perch linstradamento pu` essere diverso nei due sensi, con diverse e o tipologie di rete coinvolte. Una delle dierenze fra IPv4 e IPv6 che per IPv6 la frammentazione pu` essere eseguita solo e o alla sorgente, questo vuol dire che i router IPv6 non frammentano i pacchetti che ritrasmettono (anche se possono frammentare i pacchetti che generano loro stessi), al contrario di quanto fanno i router IPv4. In ogni caso una volta frammentati i pacchetti possono essere riassemblati solo alla destinazione. Nellheader di IPv4 ` previsto il ag DF che specica che il pacchetto non deve essere frame mentato; un router che riceva un pacchetto le cui dimensioni eccedano quelle dellMTU della rete di destinazione generer` un messaggio di errore ICMPv4 di tipo destination unreachable, a fragmentation needed but DF bit set. Dato che i router IPv6 non possono eettuare la frammentazione la ricezione di un pacchetto di dimensione eccessiva per la ritrasmissione generer` a sempre un messaggio di errore ICMPv6 di tipo packet too big. Dato che il meccanismo di frammentazione e riassemblaggio dei pacchetti comporta inecienza, normalmente viene utilizzato un procedimento, detto path MTU discovery che permette di determinare il path MTU fra due stazioni; per la realizzazione del procedimento si usa il ag DF di IPv4 e il comportamento normale di IPv6 inviando delle opportune serie di pacchetti (per i dettagli vedere lRFC 1191 per IPv4 e lRFC 1981 per IPv6) ntanto che non si hanno pi` u errori.
questo accade sia per IPv4 che per IPv6, anche se i pacchetti frammentati sono gestiti con modalit` diverse, a IPv4 usa un ag nellheader, IPv6 una opportuna opzione, si veda sez. A.2.
6

14.3. IL PROTOCOLLO TCP/IP

417

Il TCP usa sempre questo meccanismo, che per le implementazioni di IPv4 ` opzionale, e mentre diventa obbligatorio per IPv6. Per IPv6 infatti, non potendo i router frammentare i pacchetti, ` necessario, per poter comunicare, conoscere da subito il path MTU. e Inne TCP denisce una Maximum Segment Size (da qui in avanti abbreviata in MSS) che annuncia allaltro capo della connessione la dimensione massima dimensione del segmento di dati che pu` essere ricevuto, cos` da evitare la frammentazione. Di norma viene impostato alla o dimensione della MTU dellinterfaccia meno la lunghezza delle intestazioni di IP e TCP, in Linux il default, mantenuto nella costante TCP_MSS ` 512. e

418

CAPITOLO 14. INTRODUZIONE ALLA PROGRAMMAZIONE DI RETE

Capitolo 15

Introduzione ai socket
In questo capitolo inizieremo a spiegare le caratteristiche salienti della principale interfaccia per la programmazione di rete, quella dei socket, che, pur essendo nata in ambiente Unix, ` usata e ormai da tutti i sistemi operativi. Dopo una breve panoramica sulle caratteristiche di questa interfaccia vedremo come creare un socket e come collegarlo allo specico protocollo di rete che si utilizzer` per la comunicazione. a Per evitare unintroduzione puramente teorica concluderemo il capitolo con un primo esempio di applicazione.

15.1

Una panoramica

Iniziamo con una descrizione essenziale di cosa sono i socket e di quali sono i concetti fondamentali da tenere presente quando si ha a che fare con essi.

15.1.1

I socket

I socket 1 sono uno dei principali meccanismi di comunicazione utilizzato in ambito Unix, e li abbiamo brevemente incontrati in sez. 12.1.5, fra i vari meccanismi di intercomunicazione fra processi. Un socket costituisce in sostanza un canale di comunicazione fra due processi su cui si possono leggere e scrivere dati analogo a quello di una pipe (vedi sez. 12.1.1) ma, a dierenza di questa e degli altri meccanismi esaminati nel capitolo cap. 12, i socket non sono limitati alla comunicazione fra processi che girano sulla stessa macchina, ma possono realizzare la comunicazione anche attraverso la rete. Quella dei socket costituisce infatti la principale interfaccia usata nella programmazione di rete. La loro origine risale al 1983, quando furono introdotti in BSD 4.2; linterfaccia ` rimae sta sostanzialmente la stessa, con piccole modiche, negli anni successivi. Bench siano state e sviluppate interfacce alternative, originate dai sistemi SVr4 come la XTI (X/Open Transport Interface) nessuna ha mai raggiunto la diusione e la popolarit` di quella dei socket (n tantomeno a e la stessa usabilit` e essibilit`). a a La essibilit` e la genericit` dellinterfaccia inoltre consente di utilizzare i socket con i pi` a a u disparati meccanismi di comunicazione, e non solo con linsieme dei protocolli TCP/IP, anche se questa sar` comunque quella di cui tratteremo in maniera pi` estesa. a u

15.1.2

Concetti base

Per capire il funzionamento dei socket occorre avere presente il funzionamento dei protocolli di rete (vedi cap. 14), ma linterfaccia ` del tutto generale e bench le problematiche (e quindi le e e
una traduzione letterale potrebbe essere presa, ma essendo universalmente noti come socket utilizzeremo sempre la parola inglese.
1

419

420

CAPITOLO 15. INTRODUZIONE AI SOCKET

modalit` di risolvere i problemi) siano diverse a seconda del tipo di protocollo di comunicazione a usato, le funzioni da usare restano le stesse. Per questo motivo una semplice descrizione dellinterfaccia ` assolutamente inutile, in quanto e il comportamento di questultima e le problematiche da arontare cambiano radicalmente a seconda dello stile di comunicazione usato. La scelta di questo stile va infatti ad incidere sulla semantica che verr` utilizzata a livello utente per gestire la comunicazione (su come inviare e a ricevere i dati) e sul comportamento eettivo delle funzioni utilizzate. La scelta di uno stile dipende sia dai meccanismi disponibili, sia dal tipo di comunicazione che si vuole eettuare. Ad esempio alcuni stili di comunicazione considerano i dati come una sequenza continua di byte, in quello che viene chiamato un usso (in inglese stream), mentre altri invece li raggruppano in pacchetti (in inglese datagram) che vengono inviati in blocchi separati. Un altro esempio di stile concerne la possibilit` che la comunicazione possa o meno perdere a dati, possa o meno non rispettare lordine in cui essi non sono inviati, o inviare dei pacchetti pi` u volte (come nel caso di TCP e UDP). Un terzo esempio di stile di comunicazione concerne le modalit` in cui essa avviene, in certi a casi essa pu` essere condotta con una connessione diretta con un solo corrispondente, come per o una telefonata; altri casi possono prevedere una comunicazione come per lettera, in cui si scrive lindirizzo su ogni pacchetto, altri ancora una comunicazione broadcast come per la radio, in cui i pacchetti vengono emessi su appositi canali dove chiunque si collega possa riceverli. E chiaro che ciascuno di questi stili comporta una modalit` diversa di gestire la comunicazioa ne, ad esempio se ` inadabile occorrer` essere in grado di gestire la perdita o il rimescolamento e a dei dati, se ` a pacchetti questi dovranno essere opportunamente trattati, ecc. e

15.2

La creazione di un socket

Come accennato linterfaccia dei socket ` estremamente essibile e permette di interagire con e protocolli di comunicazione anche molto diversi fra di loro; in questa sezione vedremo come ` e possibile creare un socket e come specicare il tipo di comunicazione che esso deve utilizzare.

15.2.1

La funzione socket

La creazione di un socket avviene attraverso luso della funzione socket; essa restituisce un le descriptor 2 che serve come riferimento al socket; il suo prototipo `: e
#include <sys/socket.h> int socket(int domain, int type, int protocol) Apre un socket. La funzione restituisce un intero positivo in caso di successo, e -1 in caso di fallimento, nel qual caso la variabile errno assumer` i valori: a EPROTONOSUPPORT il tipo di socket o il protocollo scelto non sono supportati nel dominio. ENFILE EMFILE EACCES EINVAL ENOBUFS il kernel non ha memoria suciente a creare una nuova struttura per il socket. si ` ecceduta la tabella dei le. e non si hanno privilegi per creare un socket nel dominio o con il protocollo specicato. protocollo sconosciuto o dominio non disponibile. non c` suciente memoria per creare il socket (pu` essere anche ENOMEM). e o

inoltre, a seconda del protocollo usato, potranno essere generati altri errori, che sono riportati nelle relative pagine di manuale.

La funzione ha tre argomenti, domain specica il dominio del socket (denisce cio`, come e vedremo in sez. 15.2.2, la famiglia di protocolli usata), type specica il tipo di socket (denisce
2

del tutto analogo a quelli che si ottengono per i le di dati e le pipe, descritti in sez. 6.1.1.

15.2. LA CREAZIONE DI UN SOCKET

421

cio`, come vedremo in sez. 15.2.3, lo stile di comunicazione) e protocol il protocollo; in genere e questultimo ` indicato implicitamente dal tipo di socket, per cui di norma questo valore viene e messo a zero (con leccezione dei raw socket). Si noti che la creazione del socket si limita ad allocare le opportune strutture nel kernel (sostanzialmente una voce nella le table) e non comporta nulla riguardo allindicazione degli indirizzi remoti o locali attraverso i quali si vuole eettuare la comunicazione.

15.2.2

Il dominio dei socket

Dati i tanti e diversi protocolli di comunicazione disponibili, esistono vari tipi di socket, che vengono classicati raggruppandoli in quelli che si chiamano domini. La scelta di un dominio equivale in sostanza alla scelta di una famiglia di protocolli, e viene eettuata attraverso largomento domain della funzione socket. Ciascun dominio ha un suo nome simbolico che convenzionalmente ` indicato da una costante che inizia per PF_, sigla che sta per protocol family, e altro nome con cui si indicano i domini. A ciascun tipo di dominio corrisponde un analogo nome simbolico, anchesso associato ad una costante, che inizia invece per AF_ (da address family) che identica il formato degli indirizzi usati in quel dominio. Le pagine di manuale di Linux si riferiscono a questi indirizzi anche come name space,3 dato che identicano il formato degli indirizzi usati in quel dominio per identicare i capi della comunicazione.
Nome PF_UNSPEC PF_LOCAL PF_UNIX, PF_FILE PF_INET PF_AX25 PF_IPX PF_APPLETALK PF_NETROM PF_BRIDGE PF_ATMPVC PF_X25 PF_INET6 PF_ROSE PF_DECnet PF_NETBEUI PF_SECURITY PF_KEY PF_NETLINK PF_ROUTE PF_PACKET PF_ASH PF_ECONET PF_ATMSVC PF_SNA PF_IRDA PF_PPPOX PF_WANPIPE PF_LLC PF_BLUETOOTH Valore 0 1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 16 17 18 19 20 22 23 24 25 26 31 Utilizzo Non specicato Local communication Sinonimi di PF_LOCAL IPv4 Internet protocols Amateur radio AX.25 protocol IPX - Novell protocols Appletalk Amateur radio NetROM Multiprotocol bridge Access to raw ATM PVCs ITU-T X.25 / ISO-8208 protocol IPv6 Internet protocols Amateur Radio X.25 PLP Reserved for DECnet project Reserved for 802.2LLC project Security callback pseudo AF PF KEY key management API Kernel user interface device Sinonimo di PF_NETLINK emula BSD. Low level packet interface Ash Acorn Econet ATM SVCs Linux SNA Project IRDA socket PPPoX socket Wanpipe API socket Linux LLC Bluetooth socket Man page unix(7) ip(7)

ddp(7)

x25(7) ipv6(7)

netlink(7) packet(7)

Tabella 15.1: Famiglie di protocolli deniti in Linux.

Lidea alla base della distinzione fra questi due insiemi di costanti era che una famiglia di protocolli potesse supportare vari tipi di indirizzi, per cui il presso PF_ si sarebbe dovuto usare
3

nome che invece il manuale delle glibc riserva a quello che noi abbiamo chiamato domini.

422

CAPITOLO 15. INTRODUZIONE AI SOCKET

nella creazione dei socket e il presso AF_ in quello delle strutture degli indirizzi; questo ` quanto e specicato anche dallo standard POSIX.1g, ma non esistono a tuttora famiglie di protocolli che supportino diverse strutture di indirizzi, per cui nella pratica questi due nomi sono equivalenti e corrispondono agli stessi valori numerici.4 I domini (e i relativi nomi simbolici), cos` come i nomi delle famiglie di indirizzi, sono deniti dallheader socket.h. Un elenco delle famiglie di protocolli disponibili in Linux ` riportato in e tab. 15.1.5 Si tenga presente che non tutte le famiglie di protocolli sono utilizzabili dallutente generico, ad esempio in generale tutti i socket di tipo SOCK_RAW possono essere creati solo da processi che hanno i privilegi di amministratore (cio` con user-ID eettivo uguale a zero) o dotati della e capability CAP_NET_RAW.

15.2.3

Il tipo di socket

La scelta di un dominio non comporta per` la scelta dello stile di comunicazione, questo infatti o viene a dipendere dal protocollo che si andr` ad utilizzare fra quelli disponibili nella famiglia a scelta. Linterfaccia dei socket permette di scegliere lo stile di comunicazione indicando il tipo di socket con largomento type di socket. Linux mette a disposizione vari tipi di socket (che corrispondono a quelli che il manuale della glibc [4] chiama styles) identicati dalle seguenti costanti:6 SOCK_STREAM Provvede un canale di trasmissione dati bidirezionale, sequenziale e adabile. Opera su una connessione con un altro socket. I dati vengono ricevuti e trasmessi come un usso continuo di byte (da cui il nome stream) e possono essere letti in blocchi di dimensioni qualunque. Pu` supportare la trasmissione o dei cosiddetti dati urgenti (o out-of-band, vedi sez. 19.1.3). Viene usato per trasmettere pacchetti di dati (datagram) di lunghezza massima pressata, indirizzati singolarmente. Non esiste una connessione e la trasmissione ` eettuata in maniera non adabile. e

SOCK_DGRAM

SOCK_SEQPACKET Provvede un canale di trasmissione di dati bidirezionale, sequenziale e adabile. Opera su una connessione con un altro socket. I dati possono vengono trasmessi per pacchetti di dimensione massima ssata, e devono essere letti integralmente da ciascuna chiamata a read. SOCK_RAW Provvede laccesso a basso livello ai protocolli di rete e alle varie interfacce. I normali programmi di comunicazione non devono usarlo, ` riservato alluso e di sistema. Provvede un canale di trasmissione di dati adabile, ma in cui non ` garantito e lordine di arrivo dei pacchetti. Obsoleto, non deve essere pi` usato.7 u

SOCK_RDM

SOCK_PACKET
4

in Linux, come si pu` vericare andando a guardare il contenuto di bits/socket.h, le costanti sono o esattamente le stesse e ciascuna AF_ ` denita alla corrispondente PF_ e con lo stesso nome. e 5 lelenco indica tutti i protocolli deniti; fra questi per` saranno utilizzabili solo quelli per i quali si ` compilato o e il supporto nel kernel (o si sono caricati gli opportuni moduli), viene denita anche una costante PF_MAX che indica il valore massimo associabile ad un dominio (nel caso il suo valore 32). 6 le pagine di manuale POSIX riportano solo i primi tre tipi, Linux supporta anche gli altri, come si pu` o vericare nel le include/linux/net.h dei sorgenti del kernel. 7 e pertanto non ne parleremo ulteriormente.

15.3. LE STRUTTURE DEGLI INDIRIZZI DEI SOCKET

423

Si tenga presente che non tutte le combinazioni fra una famiglia di protocolli e un tipo di socket sono valide, in quanto non ` detto che in una famiglia esista un protocollo per ciascuno e dei diversi stili di comunicazione appena elencati.
Famiglia PF_LOCAL PF_INET PF_INET6 PF_IPX PF_NETLINK PF_X25 PF_AX25 PF_ATMPVC PF_APPLETALK PF_PACKET SOCK_STREAM si TCP TCP SOCK_DGRAM si UDP UDP si Tipo SOCK_RAW IPv4 IPv6 si si

SOCK_RDM

SOCK_SEQPACKET

si si

si si

Tabella 15.2: Combinazioni valide di dominio e tipo di protocollo per la funzione socket.

In tab. 15.2 sono mostrate le combinazioni valide possibili per le principali famiglie di protocolli. Per ogni combinazione valida si ` indicato il tipo di protocollo, o la parola si qualora e non il protocollo non abbia un nome denito, mentre si sono lasciate vuote le caselle per le combinazioni non supportate.

15.3

Le strutture degli indirizzi dei socket

Come si ` visto nella creazione di un socket non si specica nulla oltre al tipo di famiglia di e protocolli che si vuole utilizzare, in particolare nessun indirizzo che identichi i due capi della comunicazione. La funzione infatti si limita ad allocare nel kernel quanto necessario per poter poi realizzare la comunicazione. Gli indirizzi infatti vengono specicati attraverso apposite strutture che vengono utilizzate dalle altre funzioni della interfaccia dei socket, quando la comunicazione viene eettivamente realizzata. Ogni famiglia di protocolli ha ovviamente una sua forma di indirizzamento e in corrispondenza a questa una sua peculiare struttura degli indirizzi. I nomi di tutte queste strutture iniziano per sockaddr_; quelli propri di ciascuna famiglia vengono identicati dal susso nale, aggiunto al nome precedente.

15.3.1

La struttura generica

Le strutture degli indirizzi vengono sempre passate alle varie funzioni attraverso puntatori (cio` e by reference), ma le funzioni devono poter maneggiare puntatori a strutture relative a tutti gli indirizzi possibili nelle varie famiglie di protocolli; questo pone il problema di come passare questi puntatori, il C moderno risolve questo problema coi i puntatori generici (i void *), ma linterfaccia dei socket ` antecedente alla denizione dello standard ANSI C, e per questo nel e 1982 fu scelto di denire una struttura generica per gli indirizzi dei socket, sockaddr, che si ` e riportata in g. 15.1. Tutte le funzioni dei socket che usano gli indirizzi sono denite usando nel prototipo un puntatore a questa struttura; per questo motivo quando si invocano dette funzioni passando lindirizzo di un protocollo specico occorrer` eseguire una conversione del relativo puntatore. a I tipi di dati che compongono la struttura sono stabiliti dallo standard POSIX.1g e li abbiamo riassunti in tab. 15.3 con i rispettivi le di include in cui sono deniti; la struttura ` invece denita e nellinclude le sys/socket.h.

424

CAPITOLO 15. INTRODUZIONE AI SOCKET

struct sockaddr { sa_family_t sa_family ; char sa_data [14]; };

/* address family : AF_xxx */ /* address ( protocol - specific ) */

Figura 15.1: La struttura generica degli indirizzi dei socket sockaddr. Tipo int8_t uint8_t int16_t uint16_t int32_t uint32_t sa_family_t socklen_t in_addr_t in_port_t Descrizione intero a 8 bit con segno intero a 8 bit senza segno intero a 16 bit con segno intero a 16 bit senza segno intero a 32 bit con segno intero a 32 bit senza segno famiglia degli indirizzi lunghezza (uint32_t) dellindirizzo di un socket indirizzo IPv4 (uint32_t) porta TCP o UDP (uint16_t) Header sys/types.h sys/types.h sys/types.h sys/types.h sys/types.h sys/types.h sys/socket.h sys/socket.h netinet/in.h netinet/in.h

Tabella 15.3: Tipi di dati usati nelle strutture degli indirizzi, secondo quanto stabilito dallo standard POSIX.1g.

In alcuni sistemi la struttura ` leggermente diversa e prevede un primo membro aggiuntivo e uint8_t sin_len (come riportato da R. Stevens in [2]). Questo campo non verrebbe usato direttamente dal programmatore e non ` richiesto dallo standard POSIX.1g, in Linux pertanto e non esiste. Il campo sa_family_t era storicamente un unsigned short. Dal punto di vista del programmatore lunico uso di questa struttura ` quello di fare da e riferimento per il casting, per il kernel le cose sono un po diverse, in quanto esso usa il puntatore per recuperare il campo sa_family, comune a tutte le famiglie, con cui determinare il tipo di indirizzo; per questo motivo, anche se luso di un puntatore void * sarebbe pi` immediato per u lutente (che non dovrebbe pi` eseguire il casting), ` stato mantenuto luso di questa struttura. u e

15.3.2

La struttura degli indirizzi IPv4

I socket di tipo PF_INET vengono usati per la comunicazione attraverso internet; la struttura per gli indirizzi per un socket internet (se si usa IPv4) ` denita come sockaddr_in nellheader le e netinet/in.h ed ha la forma mostrata in g. 15.2, conforme allo standard POSIX.1g.
struct sockaddr_in { sa_family_t sin_family ; in_port_t sin_port ; struct in_addr sin_addr ; }; /* Internet address . */ struct in_addr { in_addr_t s_addr ; };

/* address family : AF_INET */ /* port in network byte order */ /* internet address */

/* address in network byte order */

Figura 15.2: La struttura sockaddr_in degli indirizzi dei socket internet (IPv4) e la struttura in_addr degli indirizzi IPv4.

Lindirizzo di un socket internet (secondo IPv4) comprende lindirizzo internet di uninterfaccia pi` un numero di porta (aronteremo in dettaglio il signicato di questi numeri in u sez. 16.1.6). Il protocollo IP non prevede numeri di porta, che sono utilizzati solo dai protocolli

15.3. LE STRUTTURE DEGLI INDIRIZZI DEI SOCKET

425

di livello superiore come TCP e UDP. Questa struttura per` viene usata anche per i socket RAW o che accedono direttamente al livello di IP, nel qual caso il numero della porta viene impostato al numero di protocollo. Il membro sin_family deve essere sempre impostato a AF_INET, altrimenti si avr` un errore a di EINVAL; il membro sin_port specica il numero di porta. I numeri di porta sotto il 1024 sono chiamati riservati in quanto utilizzati da servizi standard e soltanto processi con i privilegi di amministratore (con user-ID eettivo uguale a zero) o con la capability CAP_NET_BIND_SERVICE possono usare la funzione bind (che vedremo in sez. 16.2.1) su queste porte. Il membro sin_addr contiene un indirizzo internet, e viene acceduto sia come struttura (un resto di una implementazione precedente in cui questa era una union usata per accedere alle diverse classi di indirizzi) che direttamente come intero. In netinet/in.h vengono denite anche alcune costanti che identicano alcuni indirizzi speciali, riportati in tab. 16.1, che rincontreremo pi` avanti. u Inne occorre sottolineare che sia gli indirizzi che i numeri di porta devono essere specicati in quello che viene chiamato network order, cio` con i bit ordinati in formato big endian, questo e comporta la necessit` di usare apposite funzioni di conversione per mantenere la portabilit` del a a codice (vedi sez. 15.4 per i dettagli del problema e le relative soluzioni).

15.3.3

La struttura degli indirizzi IPv6

Essendo IPv6 unestensione di IPv4, i socket di tipo PF_INET6 sono sostanzialmente identici ai precedenti; la parte in cui si trovano praticamente tutte le dierenze fra i due socket ` quella e della struttura degli indirizzi; la sua denizione, presa da netinet/in.h, ` riportata in g. 15.3. e

struct sockaddr_in6 { sa_family_t sin6_family ; /* AF_INET6 */ in_port_t sin6_port ; /* port number */ uint32_t sin6_flowinfo ; /* IPv6 flow information */ struct in6_addr sin6_addr ; /* IPv6 address */ uint32_t sin6_scope_id ; /* Scope id ( new in 2.4) */ }; struct in6_addr { uint8_t s6_addr [16]; /* IPv6 address */ };

Figura 15.3: La struttura sockaddr_in6 degli indirizzi dei socket IPv6 e la struttura in6_addr degli indirizzi IPv6.

Il campo sin6_family deve essere sempre impostato ad AF_INET6, il campo sin6_port ` e analogo a quello di IPv4 e segue le stesse regole; il campo sin6_flowinfo ` a sua volta diviso e in tre parti di cui i 24 bit inferiori indicano letichetta di usso, i successivi 4 bit la priorit` e gli a ultimi 4 sono riservati. Questi valori fanno riferimento ad alcuni campi specici dellheader dei pacchetti IPv6 (vedi sez. A.2.3) ed il loro uso ` sperimentale. e Il campo sin6_addr contiene lindirizzo a 128 bit usato da IPv6, espresso da un vettore di 16 byte. Inne il campo sin6_scope_id ` un campo introdotto in Linux con il kernel 2.4, per gestire e alcune operazioni riguardanti il multicasting. Si noti inne che sockaddr_in6 ha una dimensione maggiore della struttura sockaddr generica di g. 15.1, quindi occorre stare attenti a non avere fatto assunzioni riguardo alla possibilit` di contenere i dati nelle dimensioni di questultima. a

426

CAPITOLO 15. INTRODUZIONE AI SOCKET

15.3.4

La struttura degli indirizzi locali

I socket di tipo PF_UNIX o PF_LOCAL vengono usati per una comunicazione fra processi che stanno sulla stessa macchina (per questo vengono chiamati local domain o anche Unix domain); essi hanno la caratteristica ulteriore di poter essere creati anche in maniera anonima attraverso la funzione socketpair (che abbiamo trattato in sez. 12.1.5). Quando per` si vuole fare riferimento o esplicito ad uno di questi socket si deve usare una struttura degli indirizzi di tipo sockaddr_un, la cui denizione si ` riportata in g. 15.4. e
# define UNIX_PATH_MAX 108 struct sockaddr_un { sa_family_t sun_family ; /* AF_UNIX */ char sun_path [ UNIX_PATH_MAX ]; /* pathname */ };

Figura 15.4: La struttura sockaddr_un degli indirizzi dei socket locali (detti anche unix domain) denita in sys/un.h.

In questo caso il campo sun_family deve essere AF_UNIX, mentre il campo sun_path deve specicare un indirizzo. Questo ha due forme; pu` essere un le (di tipo socket) nel lesystem o o una stringa univoca (mantenuta in uno spazio di nomi astratto). Nel primo caso lindirizzo viene specicato come una stringa (terminata da uno zero) corrispondente al pathname del le; nel secondo invece sun_path inizia con uno zero e vengono usati come nome i restanti byte come stringa, senza terminazione.

15.3.5

La struttura degli indirizzi AppleTalk

I socket di tipo PF_APPLETALK sono usati dalla libreria netatalk per implementare la comunicazione secondo il protocollo AppleTalk, uno dei primi protocolli di rete usato nel mondo dei personal computer, usato dalla Apple per connettere fra loro computer e stampanti. Il kernel supporta solo due strati del protocollo, DDP e AARP, e di norma ` opportuno usare le funzioni e della libreria netatalk, tratteremo qui questo argomento principalmente per mostrare luso di un protocollo alternativo. I socket AppleTalk permettono di usare il protocollo DDP, che ` un protocollo a pacchetto, e ` di tipo SOCK_DGRAM; largomento protocol di socket deve essere nullo. E altres` possibile usare i socket raw specicando un tipo SOCK_RAW, nel qual caso lunico valore valido per protocol ` e ATPROTO_DDP. Gli indirizzi AppleTalk devono essere specicati tramite una struttura sockaddr_atalk, la cui denizione ` riportata in g. 15.5; la struttura viene dichiarata includendo il le netatalk/at.h. e Il campo sat_family deve essere sempre AF_APPLETALK, mentre il campo sat_port specica la porta che identica i vari servizi. Valori inferiori a 129 sono usati per le porte riservate, e possono essere usati solo da processi con i privilegi di amministratore o con la capability CAP_NET_BIND_SERVICE. Lindirizzo remoto ` specicato nella struttura sat_addr, e deve essere e in network order (vedi sez. 15.4.1); esso ` composto da un parte di rete data dal campo s_net, e che pu` assumere il valore AT_ANYNET, che indica una rete generica e vale anche per indicare la o rete su cui si `, il singolo nodo ` indicato da s_node, e pu` prendere il valore generico AT_ANYNODE e e o che indica anche il nodo corrente, ed il valore ATADDR_BCAST che indica tutti i nodi della rete.

15.3.6

La struttura degli indirizzi dei packet socket

I packet socket, identicati dal dominio PF_PACKET, sono uninterfaccia specica di Linux per inviare e ricevere pacchetti direttamente su uninterfaccia di rete, senza passare per le funzioni di

15.3. LE STRUTTURE DEGLI INDIRIZZI DEI SOCKET

427

struct sockaddr_atalk { sa_family_t sat_family ; /* address family */ uint8_t sat_port ; /* port */ struct at_addr sat_addr ; /* net / node */ }; struct at_addr { uint16_t s_net ; uint8_t s_node ; };

Figura 15.5: La struttura sockaddr_atalk degli indirizzi dei socket AppleTalk, e la struttura at_addr degli indirizzi AppleTalk.

gestione dei protocolli di livello superiore. In questo modo ` possibile implementare dei protocolli e in user space, agendo direttamente sul livello sico. In genere comunque si preferisce usare la libreria pcap,8 che assicura la portabilit` su altre piattaforme, anche se con funzionalit` ridotte. a a Questi socket possono essere di tipo SOCK_RAW o SOCK_DGRAM. Con socket di tipo SOCK_RAW si pu` operare sul livello di collegamento, ed i pacchetti vengono passati direttamente dal socket al o driver del dispositivo e viceversa. In questo modo, in fase di trasmissione, il contenuto completo dei pacchetti, comprese le varie intestazioni, deve essere fornito dallutente. In fase di ricezione invece tutto il contenuto del pacchetto viene passato inalterato sul socket, anche se il kernel analizza comunque il pacchetto, riempiendo gli opportuni campi della struttura sockaddr_ll ad esso associata. Si usano invece socket di tipo SOCK_DGRAM quando si vuole operare a livello di rete. In questo caso in fase di ricezione lintestazione del protocollo di collegamento viene rimossa prima di passare il resto del pacchetto allutente, mentre in fase di trasmissione viene creata una opportuna intestazione per il protocollo a livello di collegamento utilizzato, usando le informazioni necessarie che devono essere specicate sempre con una struttura sockaddr_ll. Nella creazione di un packet socket il valore dellargomento protocol di socket serve a specicare, in network order, il numero identicativo del protocollo di collegamento si vuole utilizzare. I valori possibili sono deniti secondo lo standard IEEE 802.3, e quelli disponibili in Linux sono accessibili attraverso opportune costanti simboliche denite nel le linux/if_ether.h. Se si usa il valore speciale ETH_P_ALL passeranno sul packet socket tutti i pacchetti, qualunque sia il loro protocollo di collegamento. Ovviamente luso di questi socket ` una operazione privilegiata e e pu` essere eettuati solo da un processo con i privilegi di amministratore (user-ID eettivo o nullo) o con la capability CAP_NET_RAW. Una volta aperto un packet socket, tutti i pacchetti del protocollo specicato passeranno attraverso di esso, qualunque sia linterfaccia da cui provengono; se si vuole limitare il passaggio ad una interfaccia specica occorre usare la funzione bind per agganciare il socket a questultima. Nel caso dei packet socket la struttura degli indirizzi ` di tipo sockaddr_ll, e la sua denie zione ` riportata in g. 15.6; essa per` viene ad assumere un ruolo leggermente diverso rispetto e o a quanto visto nora per gli altri tipi di socket. Infatti se il socket ` di tipo SOCK_RAW si deve coe munque scrivere tutto direttamente nel pacchetto, quindi la struttura non serve pi` a specicare u gli indirizzi. Essa mantiene questo ruolo solo per i socket di tipo SOCK_DGRAM, per i quali permette di specicare i dati necessari al protocollo di collegamento, mentre viene sempre utilizzata in lettura (per entrambi i tipi di socket), per la ricezione dei i dati relativi a ciascun pacchetto. Al solito il campo sll_family deve essere sempre impostato al valore AF_PACKET. Il campo sll_protocol indica il protocollo scelto, e deve essere indicato in network order, facendo uso
la libreria ` mantenuta insieme al comando tcpdump, informazioni e documentazione si possono trovare sul e sito del progetto http://www.tcpdump.org/.
8

428

CAPITOLO 15. INTRODUZIONE AI SOCKET

struct sockaddr_ll { unsigned short sll_family ; unsigned short sll_protocol ; int sll_ifindex ; unsigned short sll_hatype ; unsigned char sll_pkttype ; unsigned char sll_halen ; unsigned char sll_addr [8]; };

/* /* /* /* /* /* /*

Always AF_PACKET */ Physical layer protocol */ Interface number */ Header type */ Packet type */ Length of address */ Physical layer address */

Figura 15.6: La struttura sockaddr_ll degli indirizzi dei packet socket.

delle costanti simboliche denite in linux/if_ether.h. Il campo sll_ifindex ` lindice dele linterfaccia, che, in caso di presenza di pi` interfacce dello stesso tipo (se ad esempio si hanno u pi` schede ethernet), permette di selezionare quella con cui si vuole operare (un valore nullo u indica qualunque interfaccia). Questi sono i due soli campi che devono essere specicati quando si vuole selezionare una interfaccia specica, usando questa struttura con la funzione bind. I campi sll_halen e sll_addr indicano rispettivamente lindirizzo associato allinterfaccia sul protocollo di collegamento e la relativa lunghezza; ovviamente questi valori cambiano a seconda del tipo di collegamento che si usa, ad esempio, nel caso di ethernet, questi saranno il MAC address della scheda e la relativa lunghezza. Essi vengono usati, insieme ai campi sll_family e sll_ifindex quando si inviano dei pacchetti, in questo caso tutti gli altri campi devono essere nulli. Il campo sll_hatype indica il tipo ARP, come denito in linux/if_arp.h, mentre il campo sll_pkttype indica il tipo di pacchetto; entrambi vengono impostati alla ricezione di un pacchetto ed han senso solo in questo caso. In particolare sll_pkttype pu` assumere i seguenti o valori: PACKET_HOST per un pacchetto indirizzato alla macchina ricevente, PACKET_BROADCAST per un pacchetto di broadcast, PACKET_MULTICAST per un pacchetto inviato ad un indirizzo sico di multicast, PACKET_OTHERHOST per un pacchetto inviato ad unaltra stazione (e ricevuto su uninterfaccia in modo promiscuo), PACKET_OUTGOING per un pacchetto originato dalla propria macchina che torna indietro sul socket. Si tenga presente inne che in fase di ricezione, anche se si richiede il troncamento del pacchetto, le funzioni recv, recvfrom e recvmsg (vedi sez. 19.1.1) restituiranno comunque la lunghezza eettiva del pacchetto cos` come arrivato sulla linea.

15.4

Le funzioni di conversione degli indirizzi

In questa sezione tratteremo delle varie funzioni usate per manipolare gli indirizzi, limitandoci per` agli indirizzi internet. Come accennato gli indirizzi e i numeri di porta usati nella rete o devono essere forniti in formato opportuno (il network order ). Per capire cosa signica tutto ci` o occorre introdurre un concetto generale che torner` utile anche in seguito. a

15.4.1

La endianess

La rappresentazione di un numero binario in un computer pu` essere fatta in due modi, chiamati o rispettivamente big endian e little endian a seconda di come i singoli bit vengono aggregati per formare le variabili intere (ed in genere in diretta corrispondenza a come sono poi in realt` a cablati sui bus interni del computer). Per capire meglio il problema si consideri un intero a 32 bit scritto in una locazione di memoria posta ad un certo indirizzo. Come illustrato in g. 15.7 i singoli bit possono essere disposti in

15.4. LE FUNZIONI DI CONVERSIONE DEGLI INDIRIZZI

429

Figura 15.7: Schema della disposizione dei dati in memoria a seconda della endianess.

memoria in due modi: a partire dal pi` signicativo o a partire dal meno signicativo. Cos` u nel primo caso si trover` il byte che contiene i bit pi` signicativi allindirizzo menzionato e il a u byte con i bit meno signicativi nellindirizzo successivo; questo ordinamento ` detto big endian, e dato che si trova per prima la parte pi` grande. Il caso opposto, in cui si parte dal bit meno u signicativo ` detto per lo stesso motivo little endian. e Si pu` allora vericare quale tipo di endianess usa il proprio computer con un programma o elementare che si limita ad assegnare un valore ad una variabile per poi ristamparne il contenuto leggendolo un byte alla volta. Il codice di detto programma, endtest.c, ` nei sorgenti allegati, e allora se lo eseguiamo su un PC otterremo: [piccardi@gont sources]$ ./endtest Using value ABCDEF01 val[0]= 1 val[1]=EF val[2]=CD val[3]=AB mentre su di un Mac avremo: piccardi@anarres:~/gapil/sources$ ./endtest Using value ABCDEF01 val[0]=AB val[1]=CD val[2]=EF val[3]= 1 La endianess di un computer dipende essenzialmente dalla architettura hardware usata; Intel e Digital usano il little endian, Motorola, IBM, Sun (sostanzialmente tutti gli altri) usano il big endian. Il formato dei dati contenuti nelle intestazioni dei protocolli di rete ` anchesso big e endian; altri esempi di uso di questi due diversi formati sono quello del bus PCI, che ` little e endian, o quello del bus VME che ` big endian. e Esistono poi anche dei processori che possono scegliere il tipo di formato allavvio e alcuni che, come il PowerPC o lIntel i860, possono pure passare da un tipo di ordinamento allaltro con una specica istruzione. In ogni caso in Linux lordinamento ` denito dallarchitettura e e dopo lavvio del sistema resta sempre lo stesso, anche quando il processore permetterebbe di eseguire questi cambiamenti. Per controllare quale tipo di ordinamento si ha sul proprio computer si ` scritta una piccola e funzione di controllo, il cui codice ` riportato g. 15.8, che restituisce un valore nullo (falso) se e larchitettura ` big endian ed uno non nullo (vero) se larchitettura ` little endian. e e Come si vede la funzione ` molto semplice, e si limita, una volta assegnato (9) un valore di e test pari a 0xABCD ad una variabile di tipo short (cio` a 16 bit), a ricostruirne una copia byte a e byte. Per questo prima (10) si denisce il puntatore ptr per accedere al contenuto della prima

430

CAPITOLO 15. INTRODUZIONE AI SOCKET

int endian ( void ) { 3 /* 4 * Variables definition 5 */ 6 short magic , test ; 7 char * ptr ;
1 2 8 9 10 11 12 13

magic = 0 xABCD ; /* endianess magic number */ ptr = ( char *) & magic ; test = ( ptr [1] < <8) + ( ptr [0]&0 xFF ); /* build value byte by byte */ return ( magic == test ); /* if the same is little endian */ }

Figura 15.8: La funzione endian, usata per controllare il tipo di architettura della macchina.

variabile, ed inne calcola (11) il valore della seconda assumendo che il primo byte sia quello meno signicativo (cio`, per quanto visto in g. 15.7, che sia little endian). Inne la funzione e restituisce (12) il valore del confronto delle due variabili.

15.4.2

Le funzioni per il riordinamento

Il problema connesso allendianess ` che quando si passano dei dati da un tipo di architettura e allaltra i dati vengono interpretati in maniera diversa, e ad esempio nel caso dellintero a 16 bit ci si ritrover` con i due byte in cui ` suddiviso scambiati di posto. Per questo motivo si a e usano delle funzioni di conversione che servono a tener conto automaticamente della possibile dierenza fra lordinamento usato sul computer e quello che viene usato nelle trasmissione sulla rete; queste funzioni sono htonl, htons, ntohl e ntohs ed i rispettivi prototipi sono:
#include <netinet/in.h> unsigned long int htonl(unsigned long int hostlong) Converte lintero a 32 bit hostlong dal formato della macchina a quello della rete. unsigned short int htons(unsigned short int hostshort) Converte lintero a 16 bit hostshort dal formato della macchina a quello della rete. unsigned long int ntohl(unsigned long int netlong) Converte lintero a 32 bit netlong dal formato della rete a quello della macchina. unsigned sort int ntohs(unsigned short int netshort) Converte lintero a 16 bit netshort dal formato della rete a quello della macchina. Tutte le funzioni restituiscono il valore convertito, e non prevedono errori.

I nomi sono assegnati usando la lettera n come mnemonico per indicare lordinamento usato sulla rete (da network order ) e la lettera h come mnemonico per lordinamento usato sulla macchina locale (da host order ), mentre le lettere s e l stanno ad indicare i tipi di dato (long o short, riportati anche dai prototipi). Usando queste funzioni si ha la conversione automatica: nel caso in cui la macchina che si sta usando abbia una architettura big endian queste funzioni sono denite come macro che non fanno nulla. Per questo motivo vanno sempre utilizzate, anche quando potrebbero non essere necessarie, in modo da assicurare la portabilit` del codice su tutte le architetture. a

15.4.3

Le funzioni inet_aton, inet_addr e inet_ntoa

Un secondo insieme di funzioni di manipolazione serve per passare dal formato binario usato nelle strutture degli indirizzi alla rappresentazione simbolica dei numeri IP che si usa normalmente.

15.4. LE FUNZIONI DI CONVERSIONE DEGLI INDIRIZZI

431

Le prime tre funzioni di manipolazione riguardano la conversione degli indirizzi IPv4 da una stringa in cui il numero di IP ` espresso secondo la cosiddetta notazione dotted-decimal, (cio` e e nella forma 192.168.0.1) al formato binario (direttamente in network order ) e viceversa; in questo caso si usa la lettera a come mnemonico per indicare la stringa. Dette funzioni sono inet_addr, inet_aton e inet_ntoa, ed i rispettivi prototipi sono:
#include <arpa/inet.h> in_addr_t inet_addr(const char *strptr) Converte la stringa dellindirizzo dotted decimal in nel numero IP in network order. int inet_aton(const char *src, struct in_addr *dest) Converte la stringa dellindirizzo dotted decimal in un indirizzo IP. char *inet_ntoa(struct in_addr addrptr) Converte un indirizzo IP in una stringa dotted decimal. Tutte queste le funzioni non generano codice di errore.

La prima funzione, inet_addr, restituisce lindirizzo a 32 bit in network order (del tipo in_addr_t) a partire dalla stringa passata nellargomento strptr. In caso di errore (quando la stringa non esprime un indirizzo valido) restituisce invece il valore INADDR_NONE che tipicamente sono trentadue bit a uno. Questo per` comporta che la stringa 255.255.255.255, che pure ` un o e indirizzo valido, non pu` essere usata con questa funzione; per questo motivo essa ` generalmente o e deprecata in favore di inet_aton. La funzione inet_aton converte la stringa puntata da src nellindirizzo binario che viene memorizzato nellopportuna struttura in_addr (si veda g. 15.2) situata allindirizzo dato dallargomento dest (` espressa in questa forma in modo da poterla usare direttamente con e il puntatore usato per passare la struttura degli indirizzi). La funzione restituisce 0 in caso di successo e 1 in caso di fallimento. Se usata con dest inizializzato a NULL eettua la validazione dellindirizzo. Lultima funzione, inet_ntoa, converte il valore a 32 bit dellindirizzo (espresso in network order ) restituendo il puntatore alla stringa che contiene lespressione in formato dotted decimal. Si deve tenere presente che la stringa risiede in memoria statica, per cui questa funzione non ` e rientrante.

15.4.4

Le funzioni inet_pton e inet_ntop

Le tre funzioni precedenti sono limitate solo ad indirizzi IPv4, per questo motivo ` preferibile e usare le due nuove funzioni inet_pton e inet_ntop che possono convertire anche gli indirizzi IPv6. Anche in questo caso le lettere n e p sono degli mnemonici per ricordare il tipo di conversione eettuata e stanno per presentation e numeric. Entrambe le funzioni accettano largomento af che indica il tipo di indirizzo, e che pu` essere o soltanto AF_INET o AF_INET6. La prima funzione, inet_pton, serve a convertire una stringa in un indirizzo; il suo prototipo `: e
#include <sys/socket.h> int inet_pton(int af, const char *src, void *addr_ptr) Converte lindirizzo espresso tramite una stringa nel valore numerico. La funzione restituisce un valore negativo se af specica una famiglia di indirizzi non valida, con errno che assume il valore EAFNOSUPPORT, un valore nullo se src non rappresenta un indirizzo valido, ed un valore positivo in caso di successo.

La funzione converte la stringa indicata tramite src nel valore numerico dellindirizzo IP del tipo specicato da af che viene memorizzato allindirizzo puntato da addr_ptr, la funzione restituisce un valore positivo in caso di successo, nullo se la stringa non rappresenta un indirizzo valido, e negativo se af specica una famiglia di indirizzi non valida.

432

CAPITOLO 15. INTRODUZIONE AI SOCKET

La seconda funzione di conversione ` inet_ntop che converte un indirizzo in una stringa; il e suo prototipo `: e
#include <sys/socket.h> char *inet_ntop(int af, const void *addr_ptr, char *dest, size_t len) Converte lindirizzo dalla relativa struttura in una stringa simbolica. La funzione restituisce un puntatore non nullo alla stringa convertita in caso di successo e NULL in caso di fallimento, nel qual caso errno assume i valori: ENOSPC le dimensioni della stringa con la conversione dellindirizzo eccedono la lunghezza specicata da len.

ENOAFSUPPORT la famiglia di indirizzi af non ` una valida. e

La funzione converte la struttura dellindirizzo puntata da addr_ptr in una stringa che viene copiata nel buer puntato dallindirizzo dest; questo deve essere preallocato dallutente e la lunghezza deve essere almeno INET_ADDRSTRLEN in caso di indirizzi IPv4 e INET6_ADDRSTRLEN per indirizzi IPv6; la lunghezza del buer deve comunque venire specicata attraverso il parametro len. Gli indirizzi vengono convertiti da/alle rispettive strutture di indirizzo (una struttura in_addr per IPv4, e una struttura in6_addr per IPv6), che devono essere precedentemente allocate e passate attraverso il puntatore addr_ptr; largomento dest di inet_ntop non pu` essere nullo o e deve essere allocato precedentemente. Il formato usato per gli indirizzi in formato di presentazione ` la notazione dotted decimal e per IPv4 e quello descritto in sez. A.2.5 per IPv6.

Capitolo 16

I socket TCP
In questo capitolo tratteremo le basi dei socket TCP, iniziando con una descrizione delle principali caratteristiche del funzionamento di una connessione TCP; vedremo poi le varie funzioni che servono alla creazione di una connessione fra client e server, fornendo alcuni esempi elementari, e niremo prendendo in esame luso dellI/O multiplexing.

16.1

Il funzionamento di una connessione TCP

Prima di entrare nei dettagli delle singole funzioni usate nelle applicazioni che utilizzano i socket TCP, ` fondamentale spiegare alcune delle basi del funzionamento del protocollo, poich questa e e conoscenza ` essenziale per comprendere il comportamento di dette funzioni per questo tipo di e socket, ed il relativo modello di programmazione. Si ricordi che il protocollo TCP serve a creare degli stream socket, cio` una forma di canale e di comunicazione che stabilisce una connessione stabile fra due stazioni, in modo che queste possano scambiarsi dei dati. In questa sezione ci concentreremo sulle modalit` con le quali il a protocollo d` inizio e conclude una connessione e faremo inoltre un breve accenno al signicato a di alcuni dei vari stati ad essa associati.

16.1.1

La creazione della connessione: il three way handshake

Il processo che porta a creare una connessione TCP ` chiamato three way handshake; la succese 1 di dati che vengono scambiati) che porta alla creazione sione tipica degli eventi (e dei segmenti di una connessione ` la seguente: e 1. Il server deve essere preparato per accettare le connessioni in arrivo; il procedimento si chiama apertura passiva del socket (in inglese passive open). Questo viene fatto chiamando la sequenza di funzioni socket, bind e listen. Completata lapertura passiva il server chiama la funzione accept e il processo si blocca in attesa di connessioni. 2. Il client richiede linizio della connessione usando la funzione connect, attraverso un procedimento che viene chiamato apertura attiva, dallinglese active open. La chiamata di connect blocca il processo e causa linvio da parte del client di un segmento SYN, in sostanza viene inviato al server un pacchetto IP che contiene solo gli header IP e TCP (con il numero di sequenza iniziale e il ag SYN) e le opzioni di TCP.
si ricordi che il segmento ` lunit` elementare di dati trasmessa dal protocollo TCP al livello successivo; e a tutti i segmenti hanno un header che contiene le informazioni che servono allo stack TCP (cos` viene di solito chiamata la parte del kernel che implementa il protocollo) per realizzare la comunicazione, fra questi dati ci sono una serie di ag usati per gestire la connessione, come SYN, ACK, URG, FIN, alcuni di essi, come SYN (che sta per syncronize) corrispondono a funzioni particolari del protocollo e danno il nome al segmento, (per maggiori dettagli vedere sez. B.1).
1

433

434

CAPITOLO 16. I SOCKET TCP

3. il server deve dare ricevuto (lacknowledge) del SYN del client, inoltre anche il server deve inviare il suo SYN al client (e trasmettere il suo numero di sequenza iniziale) questo viene fatto ritrasmettendo un singolo segmento in cui sono impostati entrambi i ag SYN e ACK. 4. una volta che il client ha ricevuto lacknowledge dal server la funzione connect ritorna, lultimo passo ` dare il ricevuto del SYN del server inviando un ACK. Alla ricezione di e questultimo la funzione accept del server ritorna e la connessione ` stabilita. e Il procedimento viene chiamato three way handshake dato che per realizzarlo devono essere scambiati tre segmenti. In g. 16.1 si ` rappresentata gracamente la sequenza di scambio dei e segmenti che stabilisce la connessione.

Figura 16.1: Il three way handshake del TCP.

Si ` accennato in precedenza ai numeri di sequenza (che sono anche riportati in g. 16.1): e per gestire una connessione adabile infatti il protocollo TCP prevede nellheader la presenza di un numero a 32 bit (chiamato appunto sequence number ) che identica a quale byte nella sequenza del usso corrisponde il primo byte della sezione dati contenuta nel segmento. Il numero di sequenza di ciascun segmento viene calcolato a partire da un numero di sequenza iniziale generato in maniera casuale del kernel allinizio della connessione e trasmesso con il SYN; lacknowledgement di ciascun segmento viene eettuato dallaltro capo della connessione impostando il ag ACK e restituendo nellapposito campo dellheader un acknowledge number ) pari al numero di sequenza che il ricevente si aspetta di ricevere con il pacchetto successivo; dato che il primo pacchetto SYN consuma un byte, nel three way handshake il numero di acknowledge ` sempre pari al numero di sequenza iniziale incrementato di uno; lo stesso varr` anche (vedi e a g. 16.2) per lacknowledgement di un FIN.

16.1.2

Le opzioni TCP.

Ciascun segmento SYN contiene in genere delle opzioni per il protocollo TCP, le cosiddette TCP options,2 che vengono inserite fra lheader e i dati, e che servono a comunicare allaltro capo una serie di parametri utili a regolare la connessione. Normalmente vengono usate le seguenti opzioni: MSS option, dove MMS sta per Maximum Segment Size, con questa opzione ciascun capo della connessione annuncia allaltro il massimo ammontare di dati che vorrebbe accettare
da non confondere con le opzioni dei socket TCP che tratteremo in sez. 17.2.5, in questo caso si tratta delle opzioni che vengono trasmesse come parte di un pacchetto TCP, non delle funzioni che consentono di impostare i relativi valori.
2

16.1. IL FUNZIONAMENTO DI UNA CONNESSIONE TCP

435

` per ciascun segmento nella connessione corrente. E possibile leggere e scrivere questo valore attraverso lopzione del socket TCP_MAXSEG (vedi sez. 17.2.5). window scale option, il protocollo TCP implementa il controllo di usso attraverso una advertised window (la nestra annunciata, vedi sez. ??) con la quale ciascun capo della comunicazione dichiara quanto spazio disponibile ha in memoria per i dati. Questo ` un e numero a 16 bit dellheader, che cos` pu` indicare un massimo di 65535 byte;3 ma alcuni tipi o di connessione come quelle ad alta velocit` (sopra i 45Mbit/sec) e quelle che hanno grandi a ritardi nel cammino dei pacchetti (come i satelliti) richiedono una nestra pi` grande per u poter ottenere il massimo dalla trasmissione. Per questo esiste questa opzione che indica un fattore di scala da applicare al valore della nestra annunciata4 per la connessione corrente (espresso come numero di bit cui spostare a sinistra il valore della nestra annunciata inserito nel pacchetto). Con Linux ` possibile indicare al kernel di far negoziare il fattore e di scala in fase di creazione di una connessione tramite la sysctl tcp_window_scaling (vedi sez. 17.4.3).5 timestamp option, ` anche questa una nuova opzione necessaria per le connessioni ad alta e velocit` per evitare possibili corruzioni di dati dovute a pacchetti perduti che riappaiono; a anche questa viene negoziata come la precedente. La MSS ` generalmente supportata da quasi tutte le implementazioni del protocollo, le e ultime due opzioni (trattate nellRFC 1323) sono meno comuni; vengono anche dette long fat pipe options dato che questo ` il nome che viene dato alle connessioni caratterizzate da alta e velocit` o da ritardi elevati. In ogni caso Linux supporta pienamente entrambe le opzioni. a

16.1.3

La terminazione della connessione

Mentre per la creazione di una connessione occorre un interscambio di tre segmenti, la procedura di chiusura ne richiede normalmente quattro. In questo caso la successione degli eventi ` la e seguente: 1. Un processo ad uno dei due capi chiama la funzione close, dando lavvio a quella che viene chiamata chiusura attiva (o active close). Questo comporta lemissione di un segmento FIN, che serve ad indicare che si ` nito con linvio dei dati sulla connessione. e 2. Laltro capo della connessione riceve il FIN e dovr` eseguire la chiusura passiva (o passive a close). Al FIN, come ad ogni altro pacchetto, viene risposto con un ACK, inoltre il ricevimento del FIN viene segnalato al processo che ha aperto il socket (dopo che ogni altro eventuale dato rimasto in coda ` stato ricevuto) come un end-of-le sulla lettura: questo e perch il ricevimento di un FIN signica che non si riceveranno altri dati sulla connessione. e 3. Una volta rilevata lend-of-le anche il secondo processo chiamer` la funzione close sul a proprio socket, causando lemissione di un altro segmento FIN. 4. Laltro capo della connessione ricever` il FIN conclusivo e risponder` con un ACK. a a
in Linux il massimo ` 32767 per evitare problemi con alcune implementazioni che usano laritmetica con segno e per implementare lo stack TCP. 4 essendo una nuova opzione per garantire la compatibilit` con delle vecchie implementazioni del protocollo a la procedura che la attiva prevede come negoziazione che laltro capo della connessione riconosca esplicitamente lopzione inserendola anche lui nel suo SYN di risposta dellapertura della connessione. 5 per poter usare questa funzionalit` ` comunque necessario ampliare le dimensioni dei buer di ricezione e a e spedizione, cosa che pu` essere fatta sia a livello di sistema con le opportune sysctl (vedi sez. 17.4.3) che a livello o di singoli socket con le relative opzioni (vedi sez. 17.2.5).
3

436

CAPITOLO 16. I SOCKET TCP

Dato che in questo caso sono richiesti un FIN ed un ACK per ciascuna direzione normalmente i segmenti scambiati sono quattro. Questo non ` vero sempre giacch in alcune situazioni il FIN e e del passo 1) ` inviato insieme a dei dati. Inoltre ` possibile che i segmenti inviati nei passi 2 e 3 e e dal capo che eettua la chiusura passiva, siano accorpati in un singolo segmento. In g. 16.2 si ` rappresentato gracamente lo sequenza di scambio dei segmenti che conclude la connessione. e

Figura 16.2: La chiusura di una connessione TCP.

Come per il SYN anche il FIN occupa un byte nel numero di sequenza, per cui lACK riporter` un acknowledge number incrementato di uno. a Si noti che, nella sequenza di chiusura, fra i passi 2 e 3, ` in teoria possibile che si mantenga e un usso di dati dal capo della connessione che deve ancora eseguire la chiusura passiva a quello che sta eseguendo la chiusura attiva. Nella sequenza indicata i dati verrebbero persi, dato che si ` chiuso il socket dal lato che esegue la chiusura attiva; esistono tuttavia situazioni in cui si vuole e poter sfruttare questa possibilit`, usando una procedura che ` chiamata half-close; torneremo su a e questo aspetto e su come utilizzarlo in sez. 16.6.3, quando parleremo della funzione shutdown. La emissione del FIN avviene quando il socket viene chiuso, questo per` non avviene solo per o la chiamata esplicita della funzione close, ma anche alla terminazione di un processo, quando tutti i le vengono chiusi. Questo comporta ad esempio che se un processo viene terminato da un segnale tutte le connessioni aperte verranno chiuse. Inne occorre sottolineare che, bench nella gura (e nellesempio che vedremo pi` avanti e u in sez. 16.4.1) sia stato il client ad eseguire la chiusura attiva, nella realt` questa pu` essere a o eseguita da uno qualunque dei due capi della comunicazione (come nellesempio di g. 16.9), e anche se il caso pi` comune resta quello del client, ci sono alcuni servizi, il principale dei quali u ` lHTTP, per i quali ` il server ad eettuare la chiusura attiva. e e

16.1.4

Un esempio di connessione

Come abbiamo visto le operazioni del TCP nella creazione e conclusione di una connessione sono piuttosto complesse, ed abbiamo esaminato soltanto quelle relative ad un andamento normale. In sez. B.1.1 vedremo con maggiori dettagli che una connessione pu` assumere vari stati, che ne o caratterizzano il funzionamento, e che sono quelli che vengono riportati dal comando netstat, per ciascun socket TCP aperto, nel campo State. Non possiamo arontare qui una descrizione completa del funzionamento del protocollo; un approfondimento sugli aspetti principali si trova in sez. B.1, ma per una trattazione completa il miglior riferimento resta [13]. Qui ci limiteremo a descrivere brevemente un semplice esempio di

16.1. IL FUNZIONAMENTO DI UNA CONNESSIONE TCP

437

connessione e le transizioni che avvengono nei due casi appena citati (creazione e terminazione della connessione). In assenza di connessione lo stato del TCP ` CLOSED; quando una applicazione esegue una e apertura attiva il TCP emette un SYN e lo stato diventa SYN_SENT; quando il TCP riceve la risposta del SYN+ACK emette un ACK e passa allo stato ESTABLISHED; questo ` lo stato nale e in cui avviene la gran parte del trasferimento dei dati. Dal lato server in genere invece il passaggio che si opera con lapertura passiva ` quello di e portare il socket dallo stato CLOSED allo stato LISTEN in cui vengono accettate le connessioni. Dallo stato ESTABLISHED si pu` uscire in due modi; se unapplicazione chiama la funzione o close prima di aver ricevuto un end-of-le (chiusura attiva) la transizione ` verso lo stato e FIN_WAIT_1; se invece lapplicazione riceve un FIN nello stato ESTABLISHED (chiusura passiva) la transizione ` verso lo stato CLOSE_WAIT. e In g. 16.3 ` riportato lo schema dello scambio dei pacchetti che avviene per una un esempio e di connessione, insieme ai vari stati che il protocollo viene ad assumere per i due lati, server e client.

Figura 16.3: Schema dello scambio di pacchetti per un esempio di connessione.

La connessione viene iniziata dal client che annuncia una MSS di 1460, un valore tipico con Linux per IPv4 su Ethernet, il server risponde con lo stesso valore (ma potrebbe essere anche un valore diverso). Una volta che la connessione ` stabilita il client scrive al server una richiesta (che assumiamo e stare in un singolo segmento, cio` essere minore dei 1460 byte annunciati dal server), questule timo riceve la richiesta e restituisce una risposta (che di nuovo supponiamo stare in un singolo segmento). Si noti che lacknowledge della richiesta ` mandato insieme alla risposta: questo viene e

438

CAPITOLO 16. I SOCKET TCP

chiamato piggybacking ed avviene tutte le volte che il server ` sucientemente veloce a costruire e la risposta; in caso contrario si avrebbe prima lemissione di un ACK e poi linvio della risposta. Inne si ha lo scambio dei quattro segmenti che terminano la connessione secondo quanto visto in sez. 16.1.3; si noti che il capo della connessione che esegue la chiusura attiva entra nello stato TIME_WAIT, sul cui signicato torneremo fra poco. ` E da notare come per eettuare uno scambio di due pacchetti (uno di richiesta e uno di risposta) il TCP necessiti di ulteriori otto segmenti, se invece si fosse usato UDP sarebbero stati sucienti due soli pacchetti. Questo ` il costo che occorre pagare per avere ladabilit` e a garantita dal TCP, se si fosse usato UDP si sarebbe dovuto trasferire la gestione di tutta una serie di dettagli (come la verica della ricezione dei pacchetti) dal livello del trasporto allinterno dellapplicazione. Quello che ` bene sempre tenere presente ` allora quali sono le esigenze che si hanno in una e e applicazione di rete, perch non ` detto che TCP sia la miglior scelta in tutti i casi (ad esempio se e e si devono solo scambiare dati gi` organizzati in piccoli pacchetti loverhead aggiunto pu` essere a o eccessivo) per questo esistono applicazioni che usano UDP e lo fanno perch nel caso specico e le sue caratteristiche di velocit` e compattezza nello scambio dei dati rispondono meglio alle a esigenze che devono essere arontate.

16.1.5

Lo stato TIME_WAIT

Come riportato da Stevens in [2] lo stato TIME_WAIT ` probabilmente uno degli aspetti meno e compresi del protocollo TCP, ` infatti comune trovare domande su come sia possibile evitare che e unapplicazione resti in questo stato lasciando attiva una connessione ormai conclusa; la risposta ` che non deve essere fatto, ed il motivo cercheremo di spiegarlo adesso. e Come si ` visto nellesempio precedente (vedi g. 16.3) TIME_WAIT ` lo stato nale in cui e e il capo di una connessione che esegue la chiusura attiva resta prima di passare alla chiusura denitiva della connessione. Il tempo in cui lapplicazione resta in questo stato deve essere due volte la MSL (Maximum Segment Lifetime). La MSL ` la stima del massimo periodo di tempo che un pacchetto IP pu` vivere sulla e o rete; questo tempo ` limitato perch ogni pacchetto IP pu` essere ritrasmesso dai router un e e o numero massimo di volte (detto hop limit). Il numero di ritrasmissioni consentito ` indicato dal e campo TTL dellheader di IP (per maggiori dettagli vedi sez. A.1), e viene decrementato ad ogni passaggio da un router; quando si annulla il pacchetto viene scartato. Siccome il numero ` ad e 8 bit il numero massimo di salti ` di 255, pertanto anche se il TTL (da time to live) non ` e e propriamente un limite sul tempo di vita, si stima che un pacchetto IP non possa restare nella rete per pi` di MSL secondi. u Ogni implementazione del TCP deve scegliere un valore per la MSL (lRFC 1122 raccomanda 2 minuti, Linux usa 30 secondi), questo comporta una durata dello stato TIME_WAIT che a seconda delle implementazioni pu` variare fra 1 a 4 minuti. Lo stato TIME_WAIT viene utilizzato o dal protocollo per due motivi principali: 1. implementare in maniera adabile la terminazione della connessione in entrambe le direzioni. 2. consentire leliminazione dei segmenti duplicati dalla rete. Il punto ` che entrambe le ragioni sono importanti, anche se spesso si fa riferimento solo alla e prima; ma ` solo se si tiene conto della seconda che si capisce il perch della scelta di un tempo e e pari al doppio della MSL come durata di questo stato. Il primo dei due motivi precedenti si pu` capire tornando a g. 16.3: assumendo che lultimo o ACK della sequenza (quello del capo che ha eseguito la chiusura attiva) venga perso, chi esegue la chiusura passiva non ricevendo risposta rimander` un ulteriore FIN, per questo motivo chi a

16.1. IL FUNZIONAMENTO DI UNA CONNESSIONE TCP

439

esegue la chiusura attiva deve mantenere lo stato della connessione per essere in grado di reinviare lACK e chiuderla correttamente. Se non fosse cos` la risposta sarebbe un RST (un altro tipo si segmento) che verrebbe interpretato come un errore. Se il TCP deve poter chiudere in maniera pulita entrambe le direzioni della connessione allora deve essere in grado di arontare la perdita di uno qualunque dei quattro segmenti che costituiscono la chiusura. Per questo motivo un socket deve rimanere attivo nello stato TIME_WAIT anche dopo linvio dellultimo ACK, per potere essere in grado di gestirne leventuale ritrasmissione, in caso esso venga perduto. Il secondo motivo ` pi` complesso da capire, e necessita di una spiegazione degli scenari in e u cui pu` accadere che i pacchetti TCP si possano perdere nella rete o restare intrappolati, per o poi riemergere in un secondo tempo. Il caso pi` comune in cui questo avviene ` quello di anomalie nellinstradamento; pu` accadere u e o cio` che un router smetta di funzionare o che una connessione fra due router si interrompa. In e questo caso i protocolli di instradamento dei pacchetti possono impiegare diverso tempo (anche dellordine dei minuti) prima di trovare e stabilire un percorso alternativo per i pacchetti. Nel frattempo possono accadere casi in cui un router manda i pacchetti verso un altro e questultimo li rispedisce indietro, o li manda ad un terzo router che li rispedisce al primo, si creano cio` dei e circoli (i cosiddetti routing loop) in cui restano intrappolati i pacchetti. Se uno di questi pacchetti intrappolati ` un segmento TCP, chi lha inviato, non ricevendo e un ACK in risposta, provveder` alla ritrasmissione e se nel frattempo sar` stata stabilita una a a strada alternativa il pacchetto ritrasmesso giunger` a destinazione. a Ma se dopo un po di tempo (che non supera il limite dellMSL, dato che altrimenti verrebbe ecceduto il TTL) lanomalia viene a cessare, il circolo di instradamento viene spezzato i pacchetti intrappolati potranno essere inviati alla destinazione nale, con la conseguenza di avere dei pacchetti duplicati; questo ` un caso che il TCP deve essere in grado di gestire. e Allora per capire la seconda ragione per lesistenza dello stato TIME_WAIT si consideri il caso seguente: si supponga di avere una connessione fra lIP 195.110.112.236 porta 1550 e lIP 192.84.145.100 porta 22 (aronteremo il signicato delle porte nella prossima sezione), che questa venga chiusa e che poco dopo si ristabilisca la stessa connessione fra gli stessi IP sulle stesse porte (quella che viene detta, essendo gli stessi porte e numeri IP, una nuova incarnazione della connessione precedente); in questo caso ci si potrebbe trovare con dei pacchetti duplicati relativi alla precedente connessione che riappaiono nella nuova. Ma ntanto che il socket non ` chiuso una nuova incarnazione non pu` essere creata: per e o questo un socket TCP resta sempre nello stato TIME_WAIT per un periodo di 2MSL, in modo da attendere MSL secondi per essere sicuri che tutti i pacchetti duplicati in arrivo siano stati ricevuti (e scartati) o che nel frattempo siano stati eliminati dalla rete, e altri MSL secondi per essere sicuri che lo stesso avvenga per le risposte nella direzione opposta. In questo modo, prima che venga creata una nuova connessione, il protocollo TCP si assicura che tutti gli eventuali segmenti residui di una precedente connessione, che potrebbero causare disturbi, siano stati eliminati dalla rete.

16.1.6

I numeri di porta

In un ambiente multitasking in un dato momento pi` processi devono poter usare sia UDP che u TCP, e ci devono poter essere pi` connessioni in contemporanea. Per poter tenere distinte le u diverse connessioni entrambi i protocolli usano i numeri di porta, che fanno parte, come si pu` o vedere in sez. 15.3.2 e sez. 15.3.3 pure delle strutture degli indirizzi del socket. Quando un client contatta un server deve poter identicare con quale dei vari possibili server attivi intende parlare. Sia TCP che UDP deniscono un gruppo di porte conosciute (le cosiddette well-known port) che identicano una serie di servizi noti (ad esempio la porta 22 identica il servizio SSH) eettuati da appositi server che rispondono alle connessioni verso tali porte.

440

CAPITOLO 16. I SOCKET TCP

Daltra parte un client non ha necessit` di usare un numero di porta specico, per cui a in genere vengono usate le cosiddette porte emere (o ephemeral ports) cio` porte a cui non ` e e assegnato nessun servizio noto e che vengono assegnate automaticamente dal kernel alla creazione della connessione. Queste sono dette emere in quanto vengono usate solo per la durata della connessione, e lunico requisito che deve essere soddisfatto ` che ognuna di esse sia assegnata in e maniera univoca. La lista delle porte conosciute ` denita dallRFC 1700 che contiene lelenco delle porte e assegnate dalla IANA (la Internet Assigned Number Authority) ma lelenco viene costantemente aggiornato e pubblicato su internet (una versione aggiornata si pu` trovare allindirizzo o http://www.iana.org/assignments/port-numbers); inoltre in un sistema unix-like un analogo elenco viene mantenuto nel le /etc/services, con la corrispondenza fra i vari numeri di porta ed il nome simbolico del servizio. I numeri sono divisi in tre intervalli: 1. le porte note. I numeri da 0 a 1023. Queste sono controllate e assegnate dalla IANA. Se ` possibile la stessa porta ` assegnata allo stesso servizio sia su UDP che su TCP (ad e e esempio la porta 22 ` assegnata a SSH su entrambi i protocolli, anche se viene usata solo e dal TCP). 2. le porte registrate. I numeri da 1024 a 49151. Queste porte non sono controllate dalla IANA, che per` registra ed elenca chi usa queste porte come servizio agli utenti. Come o per le precedenti si assegna una porta ad un servizio sia per TCP che UDP anche se poi il servizio ` implementato solo su TCP. Ad esempio X Window usa le porte TCP e UDP e dal 6000 al 6063 anche se il protocollo ` implementato solo tramite TCP. e 3. le porte private o dinamiche. I numeri da 49152 a 65535. La IANA non dice nulla riguardo a queste porte che pertanto sono i candidati naturali ad essere usate come porte emere. In realt` rispetto a quanto indicato nellRFC 1700 i vari sistemi hanno fatto scelte diverse a per le porte emere, in particolare in g. 16.4 sono riportate quelle di BSD e Linux.

Figura 16.4: Allocazione dei numeri di porta.

I sistemi Unix hanno inoltre il concetto di porte riservate (che corrispondono alle porte con numero minore di 1024 e coincidono quindi con le porte note). La loro caratteristica ` che e possono essere assegnate a un socket solo da un processo con i privilegi di amministratore, per far s` che solo lamministratore possa allocare queste porte per far partire i relativi servizi. Le glibc deniscono (in netinet/in.h) IPPORT_RESERVED e IPPORT_USERRESERVED, in cui la prima (che vale 1024) indica il limite superiore delle porte riservate, e la seconda (che vale 5000) il limite inferiore delle porte a disposizione degli utenti. La convenzione vorrebbe che le porte emere siano allocate fra questi due valori. Nel caso di Linux questo ` vero solo in uno e dei due casi di g. 16.4, e la scelta fra i due possibili intervalli viene fatta dinamicamente dal kernel a seconda della memoria disponibile per la gestione delle relative tabelle.

16.1. IL FUNZIONAMENTO DI UNA CONNESSIONE TCP

441

Si tenga conto poi che ci sono alcuni client, in particolare rsh e rlogin, che richiedono una connessione su una porta riservata anche dal lato client come parte dellautenticazione, contando appunto sul fatto che solo lamministratore pu` usare queste porte. Data lassoluta inconsistenza o in termini di sicurezza di un tale metodo, al giorno doggi esso ` in completo disuso. e Data una connessione TCP si suole chiamare socket pair 6 la combinazione dei quattro numeri che deniscono i due capi della connessione e cio` lindirizzo IP locale e la porta TCP locale, e e lindirizzo IP remoto e la porta TCP remota. Questa combinazione, che scriveremo usando una notazione del tipo (195.110.112.152:22, 192.84.146.100:20100), identica univocamente una connessione su internet. Questo concetto viene di solito esteso anche a UDP, bench in questo e caso non abbia senso parlare di connessione. Lutilizzo del programma netstat permette di visualizzare queste informazioni nei campi Local Address e Foreing Address.

16.1.7

Le porte ed il modello client/server

Per capire meglio luso delle porte e come vengono utilizzate quando si ha a che fare con unapplicazione client/server (come quelle che descriveremo in sez. 16.3 e sez. 16.4) esamineremo cosa accade con le connessioni nel caso di un server TCP che deve gestire connessioni multiple. Se eseguiamo un netstat su una macchina di prova (il cui indirizzo sia 195.110.112.152) potremo avere un risultato del tipo: Active Internet connections (servers and established) Proto Recv-Q Send-Q Local Address Foreign Address tcp 0 0 0.0.0.0:22 0.0.0.0:* tcp 0 0 0.0.0.0:25 0.0.0.0:* tcp 0 0 127.0.0.1:53 0.0.0.0:* State LISTEN LISTEN LISTEN

essendo presenti e attivi un server SSH, un server di posta e un DNS per il caching locale. Questo ci mostra ad esempio che il server SSH ha compiuto unapertura passiva, mettendosi in ascolto sulla porta 22 riservata a questo servizio, e che si ` posto in ascolto per connessioni e provenienti da uno qualunque degli indirizzi associati alle interfacce locali. La notazione 0.0.0.0 usata da netstat ` equivalente allasterisco utilizzato per il numero di porta, indica il valore e generico, e corrisponde al valore INADDR_ANY denito in arpa/inet.h (vedi 16.1). Inoltre si noti come la porta e lindirizzo di ogni eventuale connessione esterna non sono specicati; in questo caso la socket pair associata al socket potrebbe essere indicata come (*:22, *:*), usando anche per gli indirizzi lasterisco come carattere che indica il valore generico. Dato che in genere una macchina ` associata ad un solo indirizzo IP, ci si pu` chiedere che e o senso abbia lutilizzo dellindirizzo generico per specicare lindirizzo locale; ma a parte il caso di macchine che hanno pi` di un indirizzo IP (il cosiddetto multihoming) esiste sempre anche u lindirizzo di loopback, per cui con luso dellindirizzo generico si possono accettare connessioni indirizzate verso uno qualunque degli indirizzi IP presenti. Ma, come si pu` vedere nellesempio o con il DNS che ` in ascolto sulla porta 53, ` possibile anche restringere laccesso ad uno specico e e indirizzo, cosa che nel caso ` fatta accettando solo connessioni che arrivino sullinterfaccia di e loopback. Una volta che ci si vorr` collegare a questa macchina da unaltra, per esempio quella con a lindirizzo 192.84.146.100, si dovr` lanciare su questultima un client ssh per creare una cona nessione, e il kernel gli assocer` una porta emera (ad esempio la 21100), per cui la connessione a sar` espressa dalla socket pair (192.84.146.100:21100, 195.110.112.152:22). a Alla ricezione della richiesta dal client il server creer` un processo glio per gestire la cona nessione, se a questo punto eseguiamo nuovamente il programma netstat otteniamo come risultato:
da non confondere con la coppia di socket della omonima funzione socketpair che fanno riferimento ad una coppia di socket sulla stessa macchina, non ai capi di una connessione TCP.
6

442

CAPITOLO 16. I SOCKET TCP

Active Internet connections (servers and established) Proto Recv-Q Send-Q Local Address Foreign Address tcp 0 0 0.0.0.0:22 0.0.0.0:* tcp 0 0 0.0.0.0:25 0.0.0.0:* tcp 0 0 127.0.0.1:53 0.0.0.0:* tcp 0 0 195.110.112.152:22 192.84.146.100:21100

State LISTEN LISTEN LISTEN ESTABLISHED

Come si pu` notare il server ` ancora in ascolto sulla porta 22, per` adesso c` un nuovo o e o e socket (con lo stato ESTABLISHED) che utilizza anchesso la porta 22, ed ha specicato lindirizzo locale, questo ` il socket con cui il processo glio gestisce la connessione mentre il padre resta in e ascolto sul socket originale. Se a questo punto lanciamo unaltra volta il client ssh per una seconda connessione quello che otterremo usando netstat sar` qualcosa del genere: a Active Internet connections (servers and established) Proto Recv-Q Send-Q Local Address Foreign Address tcp 0 0 0.0.0.0:22 0.0.0.0:* tcp 0 0 0.0.0.0:25 0.0.0.0:* tcp 0 0 127.0.0.1:53 0.0.0.0:* tcp 0 0 195.110.112.152:22 192.84.146.100:21100 tcp 0 0 195.110.112.152:22 192.84.146.100:21101

State LISTEN LISTEN LISTEN ESTABLISHED ESTABLISHED

cio` il client eettuer` la connessione usando unaltra porta emera: con questa sar` aperta la e a a connessione, ed il server creer` un altro processo glio per gestirla. a Tutto ci` mostra come il TCP, per poter gestire le connessioni con un server concorrente, non o pu` suddividere i pacchetti solo sulla base della porta di destinazione, ma deve usare tutta lino formazione contenuta nella socket pair, compresa la porta dellindirizzo remoto. E se andassimo a vedere quali sono i processi7 a cui fanno riferimento i vari socket vedremmo che i pacchetti che arrivano dalla porta remota 21100 vanno al primo glio e quelli che arrivano alla porta 21101 al secondo.

16.2

Le funzioni di base per la gestione dei socket

In questa sezione descriveremo in maggior dettaglio le varie funzioni che vengono usate per la gestione di base dei socket TCP, non torneremo per` sulla funzione socket, che ` gi` stata o e a esaminata accuratamente nel capitolo precedente in sez. 15.2.1.

16.2.1

La funzione bind

` La funzione bind assegna un indirizzo locale ad un socket.8 E usata cio` per specicare la prima e parte dalla socket pair. Viene usata sul lato server per specicare la porta (e gli eventuali indirizzi locali) su cui poi ci si porr` in ascolto. Il prototipo della funzione ` il seguente: a e
ad esempio con il comando fuser, o con lsof, o usando lopzione -p. nel nostro caso la utilizzeremo per socket TCP, ma la funzione ` generica e deve essere usata per qualunque e tipo di socket SOCK_STREAM prima che questo possa accettare connessioni.
8 7

16.2. LE FUNZIONI DI BASE PER LA GESTIONE DEI SOCKET


#include <sys/socket.h> int bind(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen) Assegna un indirizzo ad un socket. La funzione restituisce 0 in caso di successo e -1 per un errore; in caso di errore la variabile errno viene impostata secondo i seguenti codici di errore: EBADF EINVAL ENOTSOCK EACCES il le descriptor non ` valido. e il socket ha gi` un indirizzo assegnato. a il le descriptor non ` associato ad un socket. e si ` cercato di usare una porta riservata senza sucienti privilegi. e

443

EADDRNOTAVAIL il tipo di indirizzo specicato non ` disponibile. e EADDRINUSE qualche altro socket sta gi` usando lindirizzo. a ed anche EFAULT e per i socket di tipo AF_UNIX, ENOTDIR, ENOENT, ENOMEM, ELOOP, ENOSR e EROFS.

Il primo argomento ` un le descriptor ottenuto da una precedente chiamata a socket, e mentre il secondo e terzo argomento sono rispettivamente lindirizzo (locale) del socket e la dimensione della struttura che lo contiene, secondo quanto gi` trattato in sez. 15.3. a Con i socket TCP la chiamata bind permette di specicare lindirizzo, la porta, entrambi o nessuno dei due. In genere i server utilizzano una porta nota che assegnano allavvio, se questo non viene fatto ` il kernel a scegliere una porta emera quando vengono eseguite la funzioni e connect o listen, ma se questo ` normale per il client non lo ` per il server9 che in genere e e viene identicato dalla porta su cui risponde (lelenco di queste porte, e dei relativi servizi, ` in e /etc/services). Con bind si pu` assegnare un indirizzo IP specico ad un socket, purch questo appartenga o e ad una interfaccia della macchina. Per un client TCP questo diventer` lindirizzo sorgente usato a per i tutti i pacchetti inviati sul socket, mentre per un server TCP questo restringer` laccesso a al socket solo alle connessioni che arrivano verso tale indirizzo. Normalmente un client non specica mai lindirizzo di un socket, ed il kernel sceglie lindirizzo di origine quando viene eettuata la connessione, sulla base dellinterfaccia usata per trasmettere i pacchetti, (che dipender` dalle regole di instradamento usate per raggiungere il server). Se un a server non specica il suo indirizzo locale il kernel user` come indirizzo di origine lindirizzo di a destinazione specicato dal SYN del client. Per specicare un indirizzo generico, con IPv4 si usa il valore INADDR_ANY, il cui valore, come accennato in sez. 15.3.2, ` pari a zero; nellesempio g. 16.9 si ` usata unassegnazione immediata e e del tipo: serv_add . sin_addr . s_addr = htonl ( INADDR_ANY ); Si noti che si ` usato htonl per assegnare il valore INADDR_ANY, anche se, essendo questo nullo, e il riordinamento ` inutile. Si tenga presente comunque che tutte le costanti INADDR_ (riportate e in tab. 16.1) sono denite secondo lendianess della macchina, ed anche se esse possono essere invarianti rispetto allordinamento dei bit, ` comunque buona norma usare sempre la funzione e htonl.
Costante INADDR_ANY INADDR_BROADCAST INADDR_LOOPBACK INADDR_NONE Signicato Indirizzo generico (0.0.0.0) Indirizzo di broadcast. Indirizzo di loopback (127.0.0.1). Indirizzo errato.

Tabella 16.1: Costanti di denizione di alcuni indirizzi generici per IPv4. uneccezione a tutto ci` sono i server che usano RPC. In questo caso viene fatta assegnare dal kernel una porta o emera che poi viene registrata presso il portmapper ; questultimo ` un altro demone che deve essere contattato e dai client per ottenere la porta emera su cui si trova il server.
9

444

CAPITOLO 16. I SOCKET TCP

Lesempio precedente funziona correttamente con IPv4 poich che lindirizzo ` rappresentae e bile anche con un intero a 32 bit; non si pu` usare lo stesso metodo con IPv6, in cui lindirizzo o deve necessariamente essere specicato con una struttura, perch il linguaggio C non consente e luso di una struttura costante come operando a destra in una assegnazione. Per questo motivo nellheader netinet/in.h ` denita una variabile in6addr_any (dichiae rata come extern, ed inizializzata dal sistema al valore IN6ADRR_ANY_INIT) che permette di eettuare una assegnazione del tipo: serv_add . sin6_addr = in6addr_any ; in maniera analoga si pu` utilizzare la variabile in6addr_loopback per indicare lindirizzo di o loopback, che a sua volta viene inizializzata staticamente a IN6ADRR_LOOPBACK_INIT.

16.2.2

La funzione connect

La funzione connect ` usata da un client TCP per stabilire la connessione con un server TCP,10 e il prototipo della funzione ` il seguente: e
#include <sys/socket.h> int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen) Stabilisce una connessione fra due socket. La funzione restituisce zero in caso di successo e -1 per un errore, nel qual caso errno assumer` i a valori: ECONNREFUSED non c` nessuno in ascolto sullindirizzo remoto. e ETIMEDOUT si ` avuto timeout durante il tentativo di connessione. e ENETUNREACH la rete non ` raggiungibile. e EINPROGRESS il socket ` non bloccante (vedi sez. 11.1.1) e la connessione non pu` essere conclusa e o immediatamente. EALREADY EAGAIN il socket ` non bloccante (vedi sez. 11.1.1) e un tentativo precedente di connessione e non si ` ancora concluso. e non ci sono pi` porte locali libere. u

EAFNOSUPPORT lindirizzo non ha una famiglia di indirizzi corretta nel relativo campo. EACCES, EPERM si ` tentato di eseguire una connessione ad un indirizzo broadcast senza che il e socket fosse stato abilitato per il broadcast. altri errori possibili sono: EFAULT, EBADF, ENOTSOCK, EISCONN e EADDRINUSE.

Il primo argomento ` un le descriptor ottenuto da una precedente chiamata a socket, mentre e il secondo e terzo argomento sono rispettivamente lindirizzo e la dimensione della struttura che contiene lindirizzo del socket, gi` descritta in sez. 15.3. a La struttura dellindirizzo deve essere inizializzata con lindirizzo IP e il numero di porta del server a cui ci si vuole connettere, come mostrato nellesempio sez. 16.3.2, usando le funzioni illustrate in sez. 15.4. Nel caso di socket TCP la funzione connect avvia il three way handshake, e ritorna solo quando la connessione ` stabilita o si ` vericato un errore. Le possibili cause di errore sono e e molteplici (ed i relativi codici riportati sopra), quelle che per` dipendono dalla situazione della o rete e non da errori o problemi nella chiamata della funzione sono le seguenti: 1. Il client non riceve risposta al SYN: lerrore restituito ` ETIMEDOUT. Stevens riporta che e BSD invia un primo SYN alla chiamata di connect, un altro dopo 6 secondi, un terzo dopo
di nuovo la funzione ` generica e supporta vari tipi di socket, la dierenza ` che per socket senza connessione e e come quelli di tipo SOCK_DGRAM la sua chiamata si limiter` ad impostare lindirizzo dal quale e verso il quale a saranno inviati e ricevuti i pacchetti, mentre per socket di tipo SOCK_STREAM o SOCK_SEQPACKET, essa attiver` la a procedura di avvio (nel caso del TCP il three way handshake) della connessione.
10

16.2. LE FUNZIONI DI BASE PER LA GESTIONE DEI SOCKET

445

24 secondi, se dopo 75 secondi non ha ricevuto risposta viene ritornato lerrore. Linux invece ripete lemissione del SYN ad intervalli di 30 secondi per un numero di volte che pu` essere o stabilito dallutente. Questo pu` essere fatto a livello globale con una opportuna sysctl,11 o e a livello di singolo socket con lopzione TCP_SYNCNT (vedi sez. 17.2.5). Il valore predenito per la ripetizione dellinvio ` di 5 volte, che comporta un timeout dopo circa 180 secondi. e 2. Il client riceve come risposta al SYN un RST signica che non c` nessun programma in e ascolto per la connessione sulla porta specicata (il che vuol dire probabilmente che o si ` e sbagliato il numero della porta o che non ` stato avviato il server), questo ` un errore fatale e e e la funzione ritorna non appena il RST viene ricevuto riportando un errore ECONNREFUSED. Il ag RST sta per reset ed ` un segmento inviato direttamente dal TCP quando qualcosa e non va. Tre condizioni che generano un RST sono: quando arriva un SYN per una porta che non ha nessun server in ascolto, quando il TCP abortisce una connessione in corso, quando TCP riceve un segmento per una connessione che non esiste. 3. Il SYN del client provoca lemissione di un messaggio ICMP di destinazione non raggiungibile. In questo caso dato che il messaggio pu` essere dovuto ad una condizione transitoria o si ripete lemissione dei SYN come nel caso precedente, no al timeout, e solo allora si restituisce il codice di errore dovuto al messaggio ICMP, che da luogo ad un ENETUNREACH. Se si fa riferimento al diagramma degli stati del TCP riportato in g. B.1 la funzione connect porta un socket dallo stato CLOSED (lo stato iniziale in cui si trova un socket appena creato) prima allo stato SYN_SENT e poi, al ricevimento del ACK, nello stato ESTABLISHED. Se invece la connessione fallisce il socket non ` pi` utilizzabile e deve essere chiuso. e u Si noti inne che con la funzione connect si ` specicato solo indirizzo e porta del server, e quindi solo una met` della socket pair; essendo questa funzione usata nei client laltra met` a a contenente indirizzo e porta locale viene lasciata allassegnazione automatica del kernel, e non ` e necessario eettuare una bind.

16.2.3

La funzione listen

La funzione listen serve ad usare un socket in modalit` passiva, cio`, come dice il nome, per a e 12 in sostanza leetto della funzione ` di portare il metterlo in ascolto di eventuali connessioni; e socket dallo stato CLOSED a quello LISTEN. In genere si chiama la funzione in un server dopo le chiamate a socket e bind e prima della chiamata ad accept. Il prototipo della funzione, come denito dalla pagina di manuale, `: e
#include <sys/socket.h> int listen(int sockfd, int backlog) Pone un socket in attesa di una connessione. La funzione restituisce 0 in caso di successo e -1 in caso di errore. I codici di errore restituiti in errno sono i seguenti: EBADF ENOTSOCK largomento sockfd non ` un le descriptor valido. e largomento sockfd non ` un socket. e

EOPNOTSUPP il socket ` di un tipo che non supporta questa operazione. e

La funzione pone il socket specicato da sockfd in modalit` passiva e predispone una coda a per le connessioni in arrivo di lunghezza pari a backlog. La funzione si pu` applicare solo a o socket di tipo SOCK_STREAM o SOCK_SEQPACKET.
o pi` semplicemente scrivendo il valore voluto in /proc/sys/net/ipv4/tcp_syn_retries, vedi sez. 17.4.3. u questa funzione pu` essere usata con socket che supportino le connessioni, cio` di tipo SOCK_STREAM o o e SOCK_SEQPACKET.
12 11

446

CAPITOLO 16. I SOCKET TCP

Largomento backlog indica il numero massimo di connessioni pendenti accettate; se esso viene ecceduto il client al momento della richiesta della connessione ricever` un errore di tipo a ECONNREFUSED, o se il protocollo, come accade nel caso del TCP, supporta la ritrasmissione, la richiesta sar` ignorata in modo che la connessione possa venire ritentata. a Per capire meglio il signicato di tutto ci` occorre approfondire la modalit` con cui il kernel o a tratta le connessioni in arrivo. Per ogni socket in ascolto infatti vengono mantenute due code: 1. La coda delle connessioni incomplete (incomplete connection queue) che contiene un riferimento per ciascun socket per il quale ` arrivato un SYN ma il three way handshake non e si ` ancora concluso. Questi socket sono tutti nello stato SYN_RECV. e 2. La coda delle connessioni complete (complete connection queue) che contiene un ingresso per ciascun socket per il quale il three way handshake ` stato completato ma ancora accept e non ` ritornata. Questi socket sono tutti nello stato ESTABLISHED. e Lo schema di funzionamento ` descritto in g. 16.5: quando arriva un SYN da un client e il server crea una nuova voce nella coda delle connessioni incomplete, e poi risponde con il SYN+ACK. La voce rester` nella coda delle connessioni incomplete no al ricevimento dellACK a dal client o no ad un timeout. Nel caso di completamento del three way handshake la voce viene spostata nella coda delle connessioni complete. Quando il processo chiama la funzione accept (vedi sez. 16.2.4) la prima voce nella coda delle connessioni complete ` passata al programma, o, e se la coda ` vuota, il processo viene posto in attesa e risvegliato allarrivo della prima connessione e completa.

Figura 16.5: Schema di funzionamento delle code delle connessioni complete ed incomplete.

Storicamente il valore dellargomento backlog era corrispondente al massimo valore della somma del numero di voci possibili per ciascuna delle due code. Stevens in [2] riporta che BSD ha sempre applicato un fattore di 1.5 a detto valore, e fornisce una tabella con i risultati ottenuti con vari kernel, compreso Linux 2.0, che mostrano le dierenze fra diverse implementazioni. In Linux il signicato di questo valore ` cambiato a partire dal kernel 2.2 per prevenire e lattacco chiamato SYN ood. Questo si basa sullemissione da parte dellattaccante di un grande numero di pacchetti SYN indirizzati verso una porta, forgiati con indirizzo IP fasullo13 cos` che i SYN+ACK vanno perduti e la coda delle connessioni incomplete viene saturata, impedendo di fatto ulteriori connessioni. Per ovviare a questo il signicato del backlog ` stato cambiato a indicare la lunghezza e della coda delle connessioni complete. La lunghezza della coda delle connessioni incomplete pu` o essere ancora controllata usando la funzione sysctl con il parametro NET_TCP_MAX_SYN_BACKLOG
13

con la tecnica che viene detta ip spoong.

16.2. LE FUNZIONI DI BASE PER LA GESTIONE DEI SOCKET

447

o scrivendola direttamente in /proc/sys/net/ipv4/tcp_max_syn_backlog. Quando si attiva la protezione dei syncookies per` (con lopzione da compilare nel kernel e da attivare usando o /proc/sys/net/ipv4/tcp_syncookies) questo valore viene ignorato e non esiste pi` un valore u massimo. In ogni caso in Linux il valore di backlog viene troncato ad un massimo di SOMAXCONN se ` superiore a detta costante (che di default vale 128).14 e La scelta storica per il valore di questo parametro era di 5, e alcuni vecchi kernel non supportavano neanche valori superiori, ma la situazione corrente ` molto cambiata per via della e presenza di server web che devono gestire un gran numero di connessioni per cui un tale valore non ` pi` adeguato. Non esiste comunque una risposta univoca per la scelta del valore, per questo e u non conviene specicarlo con una costante (il cui cambiamento richiederebbe la ricompilazione del server) ma usare piuttosto una variabile di ambiente (vedi sez. 2.3.4). Stevens tratta accuratamente questo argomento in [2], con esempi presi da casi reali su web server, ed in particolare evidenzia come non sia pi` vero che il compito principale della coda sia u quello di gestire il caso in cui il server ` occupato fra chiamate successive alla accept (per cui la e coda pi` occupata sarebbe quella delle connessioni completate), ma piuttosto quello di gestire u la presenza di un gran numero di SYN in attesa di concludere il three way handshake. Inne va messo in evidenza che, nel caso di socket TCP, quando un SYN arriva con tutte le code piene, il pacchetto deve essere ignorato. Questo perch la condizione in cui le code sono piene e ` ovviamente transitoria, per cui se il client ritrasmette il SYN ` probabile che passato un po e e di tempo possa trovare nella coda lo spazio per una nuova connessione. Se invece si rispondesse con un RST, per indicare limpossibilit` di eettuare la connessione, la chiamata a connect a nel client ritornerebbe con una condizione di errore, costringendo a inserire nellapplicazione la gestione dei tentativi di riconnessione, che invece pu` essere eettuata in maniera trasparente o dal protocollo TCP.

16.2.4

La funzione accept

La funzione accept ` chiamata da un server per gestire la connessione una volta che sia stato e completato il three way handshake,15 la funzione restituisce un nuovo socket descriptor su cui si potr` operare per eettuare la comunicazione. Se non ci sono connessioni completate il processo a viene messo in attesa. Il prototipo della funzione ` il seguente: e
#include <sys/socket.h> int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen) Accetta una connessione sul socket specicato. La funzione restituisce un numero di socket descriptor positivo in caso di successo e -1 in caso di errore, nel qual caso errno viene impostata ai seguenti valori: EBADF ENOTSOCK largomento sockfd non ` un le descriptor valido. e largomento sockfd non ` un socket. e

EOPNOTSUPP il socket ` di un tipo che non supporta questa operazione. e EAGAIN o EWOULDBLOCK il socket ` stato impostato come non bloccante (vedi sez. 11.1.1), e non ci e sono connessioni in attesa di essere accettate. EPERM le regole del rewall non consentono la connessione. ENOBUFS, ENOMEM questo spesso signica che lallocazione della memoria ` limitata dai limiti sui e buer dei socket, non dalla memoria di sistema. EINTR la funzione ` stata interrotta da un segnale. e

Inoltre possono essere restituiti gli errori di rete relativi al nuovo socket, diversi a secondo del protocollo, come: EMFILE, EINVAL, ENOSR, ENOBUFS, EFAULT, EPERM, ECONNABORTED, ESOCKTNOSUPPORT, EPROTONOSUPPORT, ETIMEDOUT, ERESTARTSYS.
14 15

il valore di questa costante pu` essere controllato con un altro parametro di sysctl, vedi sez. 17.3.3. o la funzione ` comunque generica ed ` utilizzabile su socket di tipo SOCK_STREAM, SOCK_SEQPACKET e SOCK_RDM. e e

448

CAPITOLO 16. I SOCKET TCP

La funzione estrae la prima connessione relativa al socket sockfd in attesa sulla coda delle connessioni complete, che associa ad nuovo socket con le stesse caratteristiche di sockfd. Il socket originale non viene toccato e resta nello stato di LISTEN, mentre il nuovo socket viene posto nello stato ESTABLISHED. Nella struttura addr e nella variabile addrlen vengono restituiti indirizzo e relativa lunghezza del client che si ` connesso. e I due argomenti addr e addrlen (si noti che questultimo ` passato per indirizzo per avere e indietro il valore) sono usati per ottenere lindirizzo del client da cui proviene la connessione. Prima della chiamata addrlen deve essere inizializzato alle dimensioni della struttura il cui indirizzo ` passato come argomento in addr; al ritorno della funzione addrlen conterr` il numero e a di byte scritti dentro addr. Se questa informazione non interessa baster` inizializzare a NULL detti a puntatori. Se la funzione ha successo restituisce il descrittore di un nuovo socket creato dal kernel (detto connected socket) a cui viene associata la prima connessione completa (estratta dalla relativa coda, vedi sez. 16.2.3) che il client ha eettuato verso il socket sockfd. Questultimo (detto listening socket) ` quello creato allinizio e messo in ascolto con listen, e non viene toccato e dalla funzione. Se non ci sono connessioni pendenti da accettare la funzione mette in attesa il processo16 ntanto che non ne arriva una. La funzione pu` essere usata solo con socket che supportino la connessione (cio` di tipo o e SOCK_STREAM, SOCK_SEQPACKET o SOCK_RDM). Per alcuni protocolli che richiedono una conferma esplicita della connessione,17 la funzione opera solo lestrazione dalla coda delle connessioni, la conferma della connessione viene eseguita implicitamente dalla prima chiamata ad una read o una write, mentre il riuto della connessione viene eseguito con la funzione close. ` E da chiarire che Linux presenta un comportamento diverso nella gestione degli errori rispetto ad altre implementazioni dei socket BSD, infatti la funzione accept passa gli errori di rete pendenti sul nuovo socket come codici di errore per accept, per cui lapplicazione deve tenerne conto ed eventualmente ripetere la chiamata alla funzione come per lerrore di EAGAIN (torneremo su questo in sez. 16.5). Unaltra dierenza con BSD ` che la funzione non fa ereditare al nuovo e socket i ag del socket originale, come O_NONBLOCK,18 che devono essere rispecicati ogni volta. Tutto questo deve essere tenuto in conto se si devono scrivere programmi portabili. Il meccanismo di funzionamento di accept ` essenziale per capire il funzionamento di un e server: in generale infatti c` sempre un solo socket in ascolto, detto per questo listening socket, e che resta per tutto il tempo nello stato LISTEN, mentre le connessioni vengono gestite dai nuovi socket, detti connected socket, ritornati da accept, che si trovano automaticamente nello stato ESTABLISHED, e vengono utilizzati per lo scambio dei dati, che avviene su di essi, no alla chiusura della connessione. Si pu` riconoscere questo schema anche nellesempio elementare di g. 16.9, o dove per ogni connessione il socket creato da accept viene chiuso dopo linvio dei dati.

16.2.5

Le funzioni getsockname e getpeername

Oltre a tutte quelle viste nora, dedicate allutilizzo dei socket, esistono alcune funzioni ausiliarie che possono essere usate per recuperare alcune informazioni relative ai socket ed alle connessioni ad essi associate. Le due funzioni pi` elementari sono queste, che vengono usate per ottenere i u dati relativi alla socket pair associata ad un certo socket. La prima funzione ` getsockname e serve ad ottenere lindirizzo locale associato ad un socket; e il suo prototipo `: e
a meno che non si sia impostato il socket per essere non bloccante (vedi sez. 11.1.1), nel qual caso ritorna con lerrore EAGAIN. Torneremo su questa modalit` di operazione in sez. 16.6. a 17 attualmente in Linux solo DECnet ha questo comportamento. 18 ed in generale tutti quelli che si possono impostare con fcntl, vedi sez. 6.3.6.
16

16.2. LE FUNZIONI DI BASE PER LA GESTIONE DEI SOCKET


#include <sys/socket.h> int getsockname(int sockfd, struct sockaddr *name, socklen_t *namelen) Legge lindirizzo locale di un socket. La funzione restituisce 0 in caso di successo e -1 in caso di errore. I codici di errore restituiti in errno sono i seguenti: EBADF ENOTSOCK ENOBUFS EFAULT largomento sockfd non ` un le descriptor valido. e largomento sockfd non ` un socket. e non ci sono risorse sucienti nel sistema per eseguire loperazione. lindirizzo name non ` valido. e

449

La funzione restituisce la struttura degli indirizzi del socket sockfd nella struttura indicata dal puntatore name la cui lunghezza ` specicata tramite largomento namlen. Questultimo e viene passato come indirizzo per avere indietro anche il numero di byte eettivamente scritti nella struttura puntata da name. Si tenga presente che se si ` utilizzato un buer troppo piccolo e per name lindirizzo risulter` troncato. a La funzione si usa tutte le volte che si vuole avere lindirizzo locale di un socket; ad esempio pu` essere usata da un client (che usualmente non chiama bind) per ottenere numero IP e porta o locale associati al socket restituito da una connect, o da un server che ha chiamato bind su un socket usando 0 come porta locale per ottenere il numero di porta emera assegnato dal kernel. Inoltre quando un server esegue una bind su un indirizzo generico, se chiamata dopo il completamento di una connessione sul socket restituito da accept, restituisce lindirizzo locale che il kernel ha assegnato a quella connessione. Tutte le volte che si vuole avere lindirizzo remoto di un socket si usa la funzione getpeername, il cui prototipo `: e
#include <sys/socket.h> int getpeername(int sockfd, struct sockaddr * name, socklen_t * namelen) Legge lindirizzo remoto di un socket. La funzione restituisce 0 in caso di successo e -1 in caso di errore. I codici di errore restituiti in errno sono i seguenti: EBADF ENOTSOCK ENOTCONN ENOBUFS EFAULT largomento sockfd non ` un le descriptor valido. e largomento sockfd non ` un socket. e il socket non ` connesso. e non ci sono risorse sucienti nel sistema per eseguire loperazione. largomento name punta al di fuori dello spazio di indirizzi del processo.

La funzione ` identica a getsockname, ed usa la stessa sintassi, ma restituisce lindirizzo e remoto del socket, cio` quello associato allaltro capo della connessione. Ci si pu` chiedere a cosa e o serva questa funzione dato che dal lato client lindirizzo remoto ` sempre noto quando si esegue e la connect mentre dal lato server si possono usare, come vedremo in g. 16.10, i valori di ritorno di accept. Il fatto ` che in generale questultimo caso non ` sempre possibile. In particolare questo e e avviene quando il server, invece di gestire la connessione direttamente in un processo glio, come vedremo nellesempio di server concorrente di sez. 16.3.4, lancia per ciascuna connessione un altro programma, usando exec.19 In questo caso bench il processo glio abbia una immagine della memoria che ` copia di quella e e del processo padre (e contiene quindi anche la struttura ritornata da accept), allesecuzione di exec verr` caricata in memoria limmagine del programma eseguito, che a questo punto perde a ogni riferimento ai valori tornati da accept. Il socket descriptor per` resta aperto, e se si ` seguita o e
questa ad esempio ` la modalit` con cui opera il super-server inetd, che pu` gestire tutta una serie di servizi e a o diversi, eseguendo su ogni connessione ricevuta sulle porte tenute sotto controllo, il relativo server.
19

450

CAPITOLO 16. I SOCKET TCP

una opportuna convenzione per rendere noto al programma eseguito qual ` il socket connesso, 20 e questultimo potr` usare la funzione getpeername per determinare lindirizzo remoto del client. a Inne ` da chiarire (si legga la pagina di manuale) che, come per accept, il terzo argomento, e che ` specicato dallo standard POSIX.1g come di tipo socklen_t * in realt` deve sempre e a corrispondere ad un int * come prima dello standard perch tutte le implementazioni dei socket e BSD fanno questa assunzione.

16.2.6

La funzione close

La funzione standard Unix close (vedi sez. 6.2.2) che si usa sui le pu` essere usata con lo o stesso eetto anche sui le descriptor associati ad un socket. Lazione di questa funzione quando applicata a socket ` di marcarlo come chiuso e ritornare e immediatamente al processo. Una volta chiamata il socket descriptor non ` pi` utilizzabile dal e u processo e non pu` essere usato come argomento per una write o una read (anche se laltro o capo della connessione non avesse chiuso la sua parte). Il kernel invier` comunque tutti i dati a che ha in coda prima di iniziare la sequenza di chiusura. Vedremo pi` avanti in sez. 17.2.2 come sia possibile cambiare questo comportamento, e cosa u pu` essere fatto perch il processo possa assicurarsi che laltro capo abbia ricevuto tutti i dati. o e Come per tutti i le descriptor anche per i socket viene mantenuto un numero di riferimenti, per cui se pi` di un processo ha lo stesso socket aperto lemissione del FIN e la sequenza di u chiusura di TCP non viene innescata ntanto che il numero di riferimenti non si annulla, questo si applica, come visto in sez. 6.3.1, sia ai le descriptor duplicati che a quelli ereditati dagli eventuali processi gli, ed ` il comportamento che ci si aspetta in una qualunque applicazione e client/server. Per attivare immediatamente lemissione del FIN e la sequenza di chiusura descritta in sez. 16.1.3, si pu` invece usare la funzione shutdown su cui torneremo in seguito (vedi sez. 16.6.3). o

16.3

Un esempio elementare: il servizio daytime

Avendo introdotto le funzioni di base per la gestione dei socket, potremo vedere in questa sezione un primo esempio di applicazione elementare che implementa il servizio daytime su TCP, secondo quanto specicato dallRFC 867. Prima di passare agli esempi del client e del server, inizieremo riesaminando con maggiori dettagli una peculiarit` delle funzioni di I/O, gi` a a accennata in sez. 6.2.4 e sez. 6.2.5, che nel caso dei socket ` particolarmente rilevante. Passeremo e poi ad illustrare gli esempi dellimplementazione, sia dal lato client, che dal lato server, che si ` e realizzato sia in forma iterativa che concorrente.

16.3.1

Il comportamento delle funzioni di I/O

Una cosa che si tende a dimenticare quando si ha a che fare con i socket ` che le funzioni di e input/output non sempre hanno lo stesso comportamento che avrebbero con i normali le di dati (in particolare questo accade per i socket di tipo stream). Infatti con i socket ` comune che funzioni come read o write possano restituire in input o e scrivere in output un numero di byte minore di quello richiesto. Come gi` accennato in sez. 6.2.4 a questo ` un comportamento normale per le funzioni di I/O, ma con i normali le di dati il e problema si avverte solo in lettura, quando si incontra la ne del le. In generale non ` cos` e e , con i socket questo ` particolarmente evidente. e Quando ci si trova ad arontare questo comportamento tutto quello che si deve fare ` seme plicemente ripetere la lettura (o la scrittura) per la quantit` di byte restanti, tenendo conto che a
20

ad esempio il solito inetd fa sempre in modo che i le descriptor 0, 1 e 2 corrispondano al socket connesso.

16.3. UN ESEMPIO ELEMENTARE: IL SERVIZIO DAYTIME

451

1 2

# include < unistd .h >

ssize_t FullRead ( int fd , void * buf , size_t count ) { 5 size_t nleft ; 6 ssize_t nread ;
3 4 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23

nleft = count ; while ( nleft > 0) { if ( ( nread = read ( fd , buf , if ( errno == EINTR ) { continue ; } else { return ( nread ); } } else if ( nread == 0) { break ; } nleft -= nread ; buf += nread ; } return ( nleft ); }

/* repeat until no left */ nleft )) < 0) { /* if interrupted by system call */ /* repeat the loop */ /* otherwise exit */ /* EOF */ /* break loop here */ /* set left to read */ /* set pointer */

Figura 16.6: La funzione FullRead, che legge esattamente count byte da un le descriptor, iterando opportunamente le letture.

le funzioni si possono bloccare se i dati non sono disponibili: ` lo stesso comportamento che si e pu` avere scrivendo pi` di PIPE_BUF byte in una pipe (si riveda quanto detto in sez. 12.1.1). o u Per questo motivo, seguendo lesempio di R. W. Stevens in [2], si sono denite due funzioni, FullRead e FullWrite, che eseguono lettura e scrittura tenendo conto di questa caratteristica, ed in grado di ritornare solo dopo avere letto o scritto esattamente il numero di byte specicato; il sorgente ` riportato rispettivamente in g. 16.6 e g. 16.7 ed ` disponibile fra i sorgenti allegati e e alla guida nei le FullRead.c e FullWrite.c. Come si pu` notare le due funzioni ripetono la lettura/scrittura in un ciclo no allesaurio mento del numero di byte richiesti, in caso di errore viene controllato se questo ` EINTR (cio` e e uninterruzione della system call dovuta ad un segnale), nel qual caso laccesso viene ripetuto, altrimenti lerrore viene ritornato al programma chiamante, interrompendo il ciclo. Nel caso della lettura, se il numero di byte letti ` zero, signica che si ` arrivati alla ne e e del le (per i socket questo signica in genere che laltro capo ` stato chiuso, e quindi non sar` e a pi` possibile leggere niente) e pertanto si ritorna senza aver concluso la lettura di tutti i byte u richiesti. Entrambe le funzioni restituiscono 0 in caso di successo, ed un valore negativo in caso di errore, FullRead restituisce il numero di byte non letti in caso di end-of-le prematuro.

16.3.2

Il client daytime

Il primo esempio di applicazione delle funzioni di base illustrate in sez. 16.2 ` relativo alla creazioe ne di un client elementare per il servizio daytime, un servizio elementare, denito nellRFC 867, che restituisce lora locale della macchina a cui si eettua la richiesta, e che ` assegnato alla e porta 13. In g. 16.8 ` riportata la sezione principale del codice del nostro client. Il sorgente completo e del programma (TCP_daytime.c, che comprende il trattamento delle opzioni ed una funzione

452

CAPITOLO 16. I SOCKET TCP

1 2

# include < unistd .h >

ssize_t FullWrite ( int fd , const void * buf , size_t count ) { 5 size_t nleft ; 6 ssize_t nwritten ;
3 4 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21

nleft = count ; while ( nleft > 0) { /* repeat until no left */ if ( ( nwritten = write ( fd , buf , nleft )) < 0) { if ( errno == EINTR ) { /* if interrupted by system call */ continue ; /* repeat the loop */ } else { return ( nwritten ); /* otherwise exit with error */ } } nleft -= nwritten ; /* set left to write */ buf += nwritten ; /* set pointer */ } return ( nleft ); }

Figura 16.7: La funzione FullWrite, che scrive esattamente count byte su un le descriptor, iterando opportunamente le scritture.

per stampare un messaggio di aiuto) ` allegato alla guida nella sezione dei codici sorgente e pu` e o essere compilato su una qualunque macchina GNU/Linux. Il programma anzitutto (1-5) include gli header necessari; dopo la dichiarazione delle variabili (9-12) si ` omessa tutta la parte relativa al trattamento degli argomenti passati dalla linea di e comando (eettuata con le apposite funzioni illustrate in sez. 2.3.2). Il primo passo (14-18) ` creare un socket TCP (quindi di tipo SOCK_STREAM e di famiglia e AF_INET). La funzione socket ritorna il descrittore che viene usato per identicare il socket in tutte le chiamate successive. Nel caso la chiamata fallisca si stampa un errore (16) con la funzione perror e si esce (17) con un codice di errore. Il passo seguente (19-27) ` quello di costruire unapposita struttura sockaddr_in in cui e sar` inserito lindirizzo del server ed il numero della porta del servizio. Il primo passo (20) ` a e inizializzare tutto a zero, per poi inserire il tipo di indirizzo (21) e la porta (22), usando per questultima la funzione htons per convertire il formato dellintero usato dal computer a quello usato nella rete, inne 23-27 si pu` utilizzare la funzione inet_pton per convertire lindirizzo o numerico passato dalla linea di comando. A questo punto (28-32) usando la funzione connect sul socket creato in precedenza (29) si pu` stabilire la connessione con il server. Per questo si deve utilizzare come secondo argomento o la struttura preparata in precedenza con il relativo indirizzo; si noti come, esistendo diversi tipi di socket, si sia dovuto eettuare un cast. Un valore di ritorno della funzione negativo implica il fallimento della connessione, nel qual caso si stampa un errore (30) e si ritorna (31). Completata con successo la connessione il passo successivo (34-40) ` leggere la data dal e socket; il protocollo prevede che il server invii sempre una stringa alfanumerica, il formato della stringa non ` specicato dallo standard, per cui noi useremo il formato usato dalla funzione e ctime, seguito dai caratteri di terminazione \r\n, cio` qualcosa del tipo: e Wed Apr 4 00:53:00 2001\r\n questa viene letta dal socket (34) con la funzione read in un buer temporaneo; la stringa

16.3. UN ESEMPIO ELEMENTARE: IL SERVIZIO DAYTIME

453

# include # include 3 # include 4 # include 5 # include


1 2 6 7 8

< sys / types .h > < unistd .h > < arpa / inet .h > < sys / socket .h > < stdio .h >

/* /* /* /* /*

predefined types */ include unix standard library */ IP addresses conversion utilities */ socket library */ include standard I / O library */

int main ( int argc , char * argv []) { 9 int sock_fd ; 10 int i , nread ; 11 struct sockaddr_in serv_add ; 12 char buffer [ MAXLINE ]; 13 ... 14 /* create socket */ 15 if ( ( sock_fd = socket ( AF_INET , SOCK_STREAM , 0)) < 0) { 16 perror ( " Socket creation error " ); 17 return -1; 18 } 19 /* initialize address */ 20 memset (( void *) & serv_add , 0 , sizeof ( serv_add )); /* clear server address */ 21 serv_add . sin_family = AF_INET ; /* address type is INET */ 22 serv_add . sin_port = htons (13); /* daytime post is 13 */ 23 /* build address using inet_pton */ 24 if ( ( inet_pton ( AF_INET , argv [ optind ] , & serv_add . sin_addr )) <= 0) { 25 perror ( " Address creation error " ); 26 return -1; 27 } 28 /* extablish connection */ 29 if ( connect ( sock_fd , ( struct sockaddr *)& serv_add , sizeof ( serv_add )) < 0) { 30 perror ( " Connection error " ); 31 return -1; 32 } 33 /* read daytime from server */ 34 while ( ( nread = read ( sock_fd , buffer , MAXLINE )) > 0) { 35 buffer [ nread ]=0; 36 if ( fputs ( buffer , stdout ) == EOF ) { /* write daytime */ 37 perror ( " fputs error " ); 38 return -1; 39 } 40 } 41 /* error on read */ 42 if ( nread < 0) { 43 perror ( " Read error " ); 44 return -1; 45 } 46 /* normal exit */ 47 return 0; 48 }

Figura 16.8: Esempio di codice di un client elementare per il servizio daytime.

poi deve essere terminata (35) con il solito carattere nullo per poter essere stampata (36) sullo standard output con luso di fputs. Come si ` gi` spiegato in sez. 16.3.1 la risposta dal socket potr` arrivare in un unico pacchetto e a a di 26 byte (come avverr` senzaltro nel caso in questione) ma potrebbe anche arrivare in 26 a pacchetti di un byte. Per questo nel caso generale non si pu` mai assumere che tutti i dati o arrivino con una singola lettura, pertanto questultima deve essere eettuata in un ciclo in cui si continui a leggere ntanto che la funzione read non ritorni uno zero (che signica che laltro capo

454

CAPITOLO 16. I SOCKET TCP

ha chiuso la connessione) o un numero minore di zero (che signica un errore nella connessione). Si noti come in questo caso la ne dei dati sia specicata dal server che chiude la connessione (anche questo ` quanto richiesto dal protocollo); questa ` una delle tecniche possibili (` quella e e e usata pure dal protocollo HTTP), ma ce ne possono essere altre, ad esempio FTP marca la conclusione di un blocco di dati con la sequenza ASCII \r\n (carriage return e line feed), mentre il DNS mette la lunghezza in testa ad ogni blocco che trasmette. Il punto essenziale ` e che TCP non provvede nessuna indicazione che permetta di marcare dei blocchi di dati, per cui se questo ` necessario deve provvedere il programma stesso. e Se abilitiamo il servizio daytime 21 possiamo vericare il funzionamento del nostro client, avremo allora: [piccardi@gont sources]$ ./daytime 127.0.0.1 Mon Apr 21 20:46:11 2003 e come si vede tutto funziona regolarmente.

16.3.3

Un server daytime iterativo

Dopo aver illustrato il client daremo anche un esempio di un server elementare, che sia anche in grado di rispondere al precedente client. Come primo esempio realizzeremo un server iterativo, in grado di fornire una sola risposta alla volta. Il codice del programma ` nuovamente mostrato e in g. 16.9, il sorgente completo (TCP_iter_daytimed.c) ` allegato insieme agli altri le degli e esempi. Come per il client si includono (1-9) gli header necessari a cui ` aggiunto quello per trattare e i tempi, e si deniscono (14-18) alcune costanti e le variabili necessarie in seguito. Come nel caso precedente si sono omesse le parti relative al trattamento delle opzioni da riga di comando. La creazione del socket (20-24) ` analoga al caso precedente, come pure linizializzazione e (25-29) della struttura sockaddr_in. Anche in questo caso (28) si usa la porta standard del servizio daytime, ma come indirizzo IP si usa (27) il valore predenito INET_ANY, che corrisponde allindirizzo generico. Si eettua poi (30-34) la chiamata alla funzione bind che permette di associare la precedente struttura al socket, in modo che questultimo possa essere usato per accettare connessioni su una qualunque delle interfacce di rete locali. In caso di errore si stampa (31) un messaggio, e si termina (32) immediatamente il programma. Il passo successivo (35-39) ` quello di mettere in ascolto il socket; questo viene fatto (36) e con la funzione listen che dice al kernel di accettare connessioni per il socket che abbiamo creato; la funzione indica inoltre, con il secondo argomento, il numero massimo di connessioni che il kernel accetter` di mettere in coda per il suddetto socket. Di nuovo in caso di errore si a stampa (37) un messaggio, e si esce (38) immediatamente. La chiamata a listen completa la preparazione del socket per lascolto (che viene chiamato anche listening descriptor ) a questo punto si pu` procedere con il ciclo principale (40-53) che o viene eseguito indenitamente. Il primo passo (42) ` porsi in attesa di connessioni con la chiamata e alla funzione accept, come in precedenza in caso di errore si stampa (43) un messaggio, e si esce (44). Il processo rester` in stato di sleep n quando non arriva e viene accettata una connessione a da un client; quando questo avviene accept ritorna, restituendo un secondo descrittore, che viene chiamato connected descriptor, e che ` quello che verr` usato dalla successiva chiamata e a alla write per scrivere la risposta al client. Il ciclo quindi proseguir` determinando (46) il tempo corrente con una chiamata a time, con a il quale si potr` opportunamente costruire (47) la stringa con la data da trasmettere (48) con la a
in genere questo viene fornito direttamente dal superdemone inetd, pertanto basta assicurarsi che esso sia abilitato nel relativo le di congurazione.
21

16.3. UN ESEMPIO ELEMENTARE: IL SERVIZIO DAYTIME

455

# include < sys / types .h > /* predefined types */ # include < unistd .h > /* include unix standard library */ 3 # include < arpa / inet .h > /* IP addresses conversion utilities */ 4 # include < sys / socket .h > /* socket library */ 5 # include < stdio .h > /* include standard I / O library */ 6 # include < time .h > 7 # define MAXLINE 80 8 # define BACKLOG 10 9 int main ( int argc , char * argv []) 10 { 11 /* 12 * Variables definition 13 */ 14 int list_fd , conn_fd ; 15 int i ; 16 struct sockaddr_in serv_add ; 17 char buffer [ MAXLINE ]; 18 time_t timeval ; 19 ... 20 /* create socket */ 21 if ( ( list_fd = socket ( AF_INET , SOCK_STREAM , 0)) < 0) { 22 perror ( " Socket creation error " ); 23 exit ( -1); 24 } 25 /* initialize address */ 26 memset (( void *)& serv_add , 0 , sizeof ( serv_add )); /* clear server address */ 27 serv_add . sin_family = AF_INET ; /* address type is INET */ 28 serv_add . sin_port = htons (13); /* daytime port is 13 */ 29 serv_add . sin_addr . s_addr = htonl ( INADDR_ANY ); /* connect from anywhere */ 30 /* bind socket */ 31 if ( bind ( list_fd , ( struct sockaddr *)& serv_add , sizeof ( serv_add )) < 0) { 32 perror ( " bind error " ); 33 exit ( -1); 34 } 35 /* listen on socket */ 36 if ( listen ( list_fd , BACKLOG ) < 0 ) { 37 perror ( " listen error " ); 38 exit ( -1); 39 } 40 /* write daytime to client */ 41 while (1) { 42 if ( ( conn_fd = accept ( list_fd , ( struct sockaddr *) NULL , NULL )) <0 ) { 43 perror ( " accept error " ); 44 exit ( -1); 45 } 46 timeval = time ( NULL ); 47 snprintf ( buffer , sizeof ( buffer ) , " %.24 s \ r \ n " , ctime (& timeval )); 48 if ( ( write ( conn_fd , buffer , strlen ( buffer ))) < 0 ) { 49 perror ( " write error " ); 50 exit ( -1); 51 } 52 close ( conn_fd ); 53 } 54 /* normal exit */ 55 exit (0); 56 }
1 2

Figura 16.9: Esempio di codice di un semplice server per il servizio daytime.

456

CAPITOLO 16. I SOCKET TCP

chiamata a write. Completata la trasmissione il nuovo socket viene chiuso (52). A questo punto il ciclo si chiude ricominciando da capo in modo da poter ripetere linvio della data in risposta ad una successiva connessione. ` E importante notare che questo server ` estremamente elementare, infatti, a parte il fatto di e poter essere usato solo con indirizzi IPv4, esso ` in grado di rispondere ad un solo un client alla e volta: ` cio`, come dicevamo, un server iterativo. Inoltre ` scritto per essere lanciato da linea e e e di comando, se lo si volesse utilizzare come demone occorrerebbero le opportune modiche22 per tener conto di quanto illustrato in sez. 10.1.5. Si noti anche che non si ` inserita nessuna e forma di gestione della terminazione del processo, dato che tutti i le descriptor vengono chiusi automaticamente alla sua uscita, e che, non generando gli, non ` necessario preoccuparsi di e gestire la loro terminazione.

16.3.4

Un server daytime concorrente

Il server daytime dellesempio in sez. 16.3.3 ` un tipico esempio di server iterativo, in cui viene e servita una richiesta alla volta; in generale per`, specie se il servizio ` pi` complesso e comporta o e u uno scambio di dati pi` sostanzioso di quello in questione, non ` opportuno bloccare un server u e nel servizio di un client per volta; per questo si ricorre alle capacit` di multitasking del sistema. a Come accennato anche in sez. 3.1 una delle modalit` pi` comuni di funzionamento da parte a u dei server ` quella di usare la funzione fork per creare, ad ogni richiesta da parte di un client, e un processo glio che si incarichi della gestione della comunicazione. Si ` allora riscritto il sere ver daytime dellesempio precedente in forma concorrente, inserendo anche una opzione per la stampa degli indirizzi delle connessioni ricevute. In g. 16.10 ` mostrato un estratto del codice, in cui si sono tralasciati il trattamento e delle opzioni e le parti rimaste invariate rispetto al precedente esempio (cio` tutta la parte e riguardante lapertura passiva del socket). Al solito il sorgente completo del server, nel le TCP_cunc_daytimed.c, ` allegato insieme ai sorgenti degli altri esempi. e Stavolta (21-26) la funzione accept ` chiamata fornendo una struttura di indirizzi in cui e saranno ritornati lindirizzo IP e la porta da cui il client eettua la connessione, che in un secondo tempo, (40-44), se il logging ` abilitato, stamperemo sullo standard output. e Quando accept ritorna il server chiama la funzione fork (27-31) per creare il processo glio che eettuer` (32-46) tutte le operazioni relative a quella connessione, mentre il padre proseguir` a a lesecuzione del ciclo principale in attesa di ulteriori connessioni. Si noti come il glio operi solo sul socket connesso, chiudendo immediatamente (33) il socket list_fd; mentre il padre continua ad operare solo sul socket in ascolto chiudendo (48) conn_fd al ritorno dalla fork. Per quanto abbiamo detto in sez. 16.2.6 nessuna delle due chiamate a close causa linnesco della sequenza di chiusura perch il numero di riferimenti al le descriptor e non si ` annullato. e Infatti subito dopo la creazione del socket list_fd ha una referenza, e lo stesso vale per conn_fd dopo il ritorno di accept, ma dopo la fork i descrittori vengono duplicati nel padre e nel glio per cui entrambi i socket si trovano con due referenze. Questo fa si che quando il padre chiude sock_fd esso resta con una referenza da parte del glio, e sar` denitivamente chiuso solo a quando questultimo, dopo aver completato le sue operazioni, chiamer` (45) la funzione close. a In realt` per il glio non sarebbe necessaria nessuna chiamata a close, in quanto con la exit a nale (45) tutti i le descriptor, quindi anche quelli associati ai socket, vengono automaticamente chiusi. Tuttavia si ` preferito eettuare esplicitamente le chiusure per avere una maggiore e chiarezza del codice, e per evitare eventuali errori, prevenendo ad esempio un uso involontario del listening descriptor.
22

come una chiamata a daemon prima dellinizio del ciclo principale.

16.3. UN ESEMPIO ELEMENTARE: IL SERVIZIO DAYTIME

457

# include # include 3 # include 4 # include 5 # include 6 # include


1 2 7 8 9

< sys / types .h > < unistd .h > < arpa / inet .h > < sys / socket .h > < stdio .h > < time .h >

/* /* /* /* /*

predefined types */ include unix standard library */ IP addresses conversion utililites */ socket library */ include standard I / O library */

int main ( int argc , char * argv []) { 10 int list_fd , conn_fd ; 11 int i ; 12 struct sockaddr_in serv_add , client ; 13 char buffer [ MAXLINE ]; 14 socklen_t len ; 15 time_t timeval ; 16 pid_t pid ; 17 int logging =0; 18 ... 19 /* write daytime to client */ 20 while (1) { 21 len = sizeof ( client ); 22 if ( ( conn_fd = accept ( list_fd , ( struct sockaddr *)& client , & len )) 23 <0 ) { 24 perror ( " accept error " ); 25 exit ( -1); 26 } 27 /* fork to handle connection */ 28 if ( ( pid = fork ()) < 0 ){ 29 perror ( " fork error " ); 30 exit ( -1); 31 } 32 if ( pid == 0) { /* child */ 33 close ( list_fd ); 34 timeval = time ( NULL ); 35 snprintf ( buffer , sizeof ( buffer ) , " %.24 s \ r \ n " , ctime (& timeval )); 36 if ( ( write ( conn_fd , buffer , strlen ( buffer ))) < 0 ) { 37 perror ( " write error " ); 38 exit ( -1); 39 } 40 if ( logging ) { 41 inet_ntop ( AF_INET , & client . sin_addr , buffer , sizeof ( buffer )); 42 printf ( " Request from host %s , port % d \ n " , buffer , 43 ntohs ( client . sin_port )); 44 } 45 close ( conn_fd ); 46 exit (0); 47 } else { /* parent */ 48 close ( conn_fd ); 49 } 50 } 51 /* normal exit , never reached */ 52 exit (0); 53 }

Figura 16.10: Esempio di codice di un server concorrente elementare per il servizio daytime.

Si noti invece come sia essenziale che il padre chiuda ogni volta il socket connesso dopo la fork; se cos` non fosse nessuno di questi socket sarebbe eettivamente chiuso dato che alla

458

CAPITOLO 16. I SOCKET TCP

chiusura da parte del glio resterebbe ancora un riferimento nel padre. Si avrebbero cos` due eetti: il padre potrebbe esaurire i descrittori disponibili (che sono un numero limitato per ogni processo) e soprattutto nessuna delle connessioni con i client verrebbe chiusa. Come per ogni server iterativo il lavoro di risposta viene eseguito interamente dal processo glio. Questo si incarica (34) di chiamare time per leggere il tempo corrente, e di stamparlo (35) sulla stringa contenuta in buffer con luso di snprintf e ctime. Poi la stringa viene scritta (36-39) sul socket, controllando che non ci siano errori. Anche in questo caso si ` evitato il ricorso e a FullWrite in quanto la stringa ` estremamente breve e verr` senzaltro scritta in un singolo e a segmento. Inoltre nel caso sia stato abilitato il logging delle connessioni, si provvede anche (40-43) a stampare sullo standard output lindirizzo e la porta da cui il client ha eettuato la connessione, usando i valori contenuti nelle strutture restituite da accept, eseguendo le opportune conversioni con inet_ntop e ntohs. Ancora una volta lesempio ` estremamente semplicato, si noti come di nuovo non si sia e gestita n la terminazione del processo n il suo uso come demone, che tra laltro sarebbe stato e e incompatibile con luso della opzione di logging che stampa gli indirizzi delle connessioni sullo standard output. Un altro aspetto tralasciato ` la gestione della terminazione dei processi gli, e torneremo su questo pi` avanti quando tratteremo alcuni esempi di server pi` complessi. u u

16.4

Un esempio pi` completo: il servizio echo u

Lesempio precedente, basato sul servizio daytime, ` un esempio molto elementare, in cui il usso e dei dati va solo nella direzione dal server al client. In questa sezione esamineremo un esempio di applicazione client/server un po pi` complessa, che usi i socket TCP per una comunicazione in u entrambe le direzioni. Ci limiteremo a fornire una implementazione elementare, che usi solo le funzioni di base viste nora, ma prenderemo in esame, oltre al comportamento in condizioni normali, anche tutti i possibili scenari particolari (errori, sconnessione della rete, crash del client o del server durante la connessione) che possono avere luogo durante limpiego di unapplicazione di rete, partendo da una versione primitiva che dovr` essere rimaneggiata di volta in volta per poter tenere conto a di tutte le evenienze che si possono manifestare nella vita reale di unapplicazione di rete, no ad arrivare ad unimplementazione completa.

16.4.1

Il servizio echo

Nella ricerca di un servizio che potesse fare da esempio per una comunicazione bidirezionale, si ` deciso, seguendo la scelta di Stevens in [2], di usare il servizio echo, che si limita a restituire in e uscita quanto immesso in ingresso. Infatti, nonostante la sua estrema semplicit`, questo servizio a costituisce il prototipo ideale per una generica applicazione di rete in cui un server risponde alle richieste di un client. Nel caso di una applicazione pi` complessa quello che si potr` avere in pi` u a u ` una elaborazione dellinput del client, che in molti casi viene interpretato come un comando, e da parte di un server che risponde fornendo altri dati in uscita. Il servizio echo ` uno dei servizi standard solitamente provvisti direttamente dal superserver e inetd, ed ` denito dallRFC 862. Come dice il nome il servizio deve riscrivere indietro sul e socket i dati che gli vengono inviati in ingresso. LRFC descrive le speciche del servizio sia per TCP che UDP, e per il primo stabilisce che una volta stabilita la connessione ogni dato in ingresso deve essere rimandato in uscita ntanto che il chiamante non ha chiude la connessione. Al servizio ` assegnata la porta riservata 7. e Nel nostro caso lesempio sar` costituito da un client che legge una linea di caratteri dallo a standard input e la scrive sul server. A sua volta il server legger` la linea dalla connessione e la a

` 16.4. UN ESEMPIO PIU COMPLETO: IL SERVIZIO ECHO

459

riscriver` immutata allindietro. Sar` compito del client leggere la risposta del server e stamparla a a sullo standard output.

16.4.2

Il client echo: prima versione

Il codice della prima versione del client per il servizio echo, disponibile nel le TCP_echo_first.c, ` riportato in g. 16.11. Esso ricalca la struttura del precedente client per il servizio daytime e (vedi sez. 16.3.2), e la prima parte (10-27) ` sostanzialmente identica, a parte luso di una porta e diversa.
int main ( int argc , char * argv []) { 3 /* 4 * Variables definition 5 */ 6 int sock_fd , i ; 7 struct sockaddr_in serv_add ; 8 ... 9 /* create socket */ 10 if ( ( sock_fd = socket ( AF_INET , SOCK_STREAM , 0)) < 0) { 11 perror ( " Socket creation error " ); 12 return 1; 13 } 14 /* initialize address */ 15 memset (( void *) & serv_add , 0 , sizeof ( serv_add )); /* clear server address */ 16 serv_add . sin_family = AF_INET ; /* address type is INET */ 17 serv_add . sin_port = htons (7); /* echo port is 7 */ 18 /* build address using inet_pton */ 19 if ( ( inet_pton ( AF_INET , argv [ optind ] , & serv_add . sin_addr )) <= 0) { 20 perror ( " Address creation error " ); 21 return 1; 22 } 23 /* extablish connection */ 24 if ( connect ( sock_fd , ( struct sockaddr *)& serv_add , sizeof ( serv_add )) < 0) { 25 perror ( " Connection error " ); 26 return 1; 27 } 28 /* read daytime from server */ 29 ClientEcho ( stdin , sock_fd ); 30 /* normal exit */ 31 return 0; 32 }
1 2

Figura 16.11: Codice della prima versione del client echo.

Al solito si ` tralasciata la sezione relativa alla gestione delle opzioni a riga di comando. e Una volta dichiarate le variabili, si prosegue (10-13) con della creazione del socket con lusuale controllo degli errori, alla preparazione (14-17) della struttura dellindirizzo, che stavolta usa la porta 7 riservata al servizio echo, inne si converte (18-22) lindirizzo specicato a riga di comando. A questo punto (23-27) si pu` eseguire la connessione al server secondo la stessa o modalit` usata in sez. 16.3.2. a Completata la connessione, per gestire il funzionamento del protocollo si usa la funzione ClientEcho, il cui codice si ` riportato a parte in g. 16.12. Questa si preoccupa di gestire tutta e la comunicazione, leggendo una riga alla volta dallo standard input stdin, scrivendola sul socket e ristampando su stdout quanto ricevuto in risposta dal server. Al ritorno dalla funzione (30-31) anche il programma termina.

460

CAPITOLO 16. I SOCKET TCP

La funzione ClientEcho utilizza due buer (3) per gestire i dati inviati e letti sul socket. La comunicazione viene gestita allinterno di un ciclo (5-10), i dati da inviare sulla connessione vengono presi dallo stdin usando la funzione fgets, che legge una linea di testo (terminata da un CR e no al massimo di MAXLINE caratteri) e la salva sul buer di invio. Si usa poi (6) la funzione FullWrite, vista in sez. 16.3.1, per scrivere i dati sul socket, gestendo automaticamente linvio multiplo qualora una singola write non sia suciente. I dati vengono riletti indietro (7) con una read23 sul buer di ricezione e viene inserita (8) la terminazione della stringa e per poter usare (9) la funzione fputs per scriverli su stdout.

void ClientEcho ( FILE * filein , int socket ) { 3 char sendbuff [ MAXLINE +1] , recvbuff [ MAXLINE +1]; 4 int nread ; 5 while ( fgets ( sendbuff , MAXLINE , filein ) != NULL ) { 6 FullWrite ( socket , sendbuff , strlen ( sendbuff )); 7 nread = read ( socket , recvbuff , strlen ( sendbuff )); 8 recvbuff [ nread ] = 0; 9 fputs ( recvbuff , stdout ); 10 } 11 return ; 12 }
1 2

Figura 16.12: Codice della prima versione della funzione ClientEcho per la gestione del servizio echo.

Quando si concluder` linvio di dati mandando un end-of-le sullo standard input si avr` il a a ritorno di fgets con un puntatore nullo (si riveda quanto spiegato in sez. 7.2.5) e la conseguente uscita dal ciclo; al che la subroutine ritorna ed il nostro programma client termina. Si pu` eettuare una verica del funzionamento del client abilitando il servizio echo nella o congurazione di initd sulla propria macchina ed usandolo direttamente verso di esso in locale, vedremo in dettaglio pi` avanti (in sez. 16.4.4) il funzionamento del programma, usato per` con u o la nostra versione del server echo, che illustriamo immediatamente.

16.4.3

Il server echo: prima versione

La prima versione del server, contenuta nel le TCP_echod_first.c, ` riportata in g. 16.13. e Come abbiamo fatto per il client anche il server ` stato diviso in un corpo principale, costituito e dalla funzione main, che ` molto simile a quello visto nel precedente esempio per il server del e servizio daytime di sez. 16.3.4, e da una funzione ausiliaria ServEcho che si cura della gestione del servizio. In questo caso per`, rispetto a quanto visto nellesempio di g. 16.10 si ` preferito scrivere il o e server curando maggiormente alcuni dettagli, per tenere conto anche di alcune esigenze generali (che non riguardano direttamente la rete), come la possibilit` di lanciare il server anche in a modalit` interattiva e la cessione dei privilegi di amministratore non appena questi non sono a pi` necessari. u La sezione iniziale del programma (8-21) ` la stessa del server di sez. 16.3.4, ed ivi descritta e in dettaglio: crea il socket, inizializza lindirizzo e esegue bind; dato che questultima funzione viene usata su una porta riservata, il server dovr` essere eseguito da un processo con i privilegi a di amministratore, pena il fallimento della chiamata.
si ` fatta lassunzione implicita che i dati siano contenuti tutti in un solo segmento, cos` che la chiamata a read e li restituisca sempre tutti; avendo scelto una dimensione ridotta per il buer questo sar` sempre vero, vedremo a pi` avanti come superare il problema di rileggere indietro tutti e soli i dati disponibili, senza bloccarsi. u
23

` 16.4. UN ESEMPIO PIU COMPLETO: IL SERVIZIO ECHO

461

int main ( int argc , char * argv []) { 3 int list_fd , conn_fd ; 4 pid_t pid ; 5 struct sockaddr_in serv_add ; 6 ... 7 /* create socket */ 8 if ( ( list_fd = socket ( AF_INET , SOCK_STREAM , 0)) < 0) { 9 perror ( " Socket creation error " ); 10 exit (1); 11 } 12 /* initialize address and bind socket */ 13 memset (( void *)& serv_add , 0 , sizeof ( serv_add )); /* clear server address */ 14 serv_add . sin_family = AF_INET ; /* address type is INET */ 15 serv_add . sin_port = htons (7); /* echo port is 7 */ 16 serv_add . sin_addr . s_addr = htonl ( INADDR_ANY ); /* connect from anywhere */ 17 if ( bind ( list_fd , ( struct sockaddr *)& serv_add , sizeof ( serv_add )) < 0) { 18 perror ( " bind error " ); 19 exit (1); 20 } 21 /* give away privileges and go daemon */ 22 if ( setgid (65534) !=0) { /* first give away group privileges */ 23 perror ( " cannot give away group privileges " ); 24 exit (1); 25 } 26 if ( setuid (65534) !=0) { /* and only after user ... */ 27 perror ( " cannot give away user privileges " ); 28 exit (1); 29 } 30 if ( demonize ) { /* go daemon */ 31 openlog ( argv [0] , 0 , LOG_DAEMON ); /* open logging */ 32 if ( daemon (0 , 0) != 0) { 33 perror ( " cannot start as daemon " ); 34 exit (1); 35 } 36 } 37 /* main body */ 38 if ( listen ( list_fd , BACKLOG ) < 0 ) { /* listen on socket */ 39 PrintErr ( " listen error " ); 40 exit (1); 41 } 42 while (1) { /* handle echo to client */ 43 len = sizeof ( cli_add ); 44 if ( ( conn_fd = accept ( list_fd , NULL , NULL )) < 0) { 45 PrintErr ( " accept error " ); 46 exit (1); 47 } 48 if ( ( pid = fork ()) < 0 ) { /* fork to handle connection */ 49 PrintErr ( " fork error " ); 50 exit (1); 51 } 52 if ( pid == 0) { /* child */ 53 close ( list_fd ); /* close listening socket */ 54 ServEcho ( conn_fd ); /* handle echo */ 55 exit (0); 56 } else { /* parent */ 57 close ( conn_fd ); /* close connected socket */ 58 } 59 } 60 exit (0); /* normal exit , never reached */ 61 }
1 2

Figura 16.13: Codice del corpo principale della prima versione del server per il servizio echo.

462

CAPITOLO 16. I SOCKET TCP

Una volta eseguita la funzione bind per` i privilegi di amministratore non sono pi` necessari, o u per questo ` sempre opportuno rilasciarli, in modo da evitare problemi in caso di eventuali e vulnerabilit` del server. Per questo prima (22-26) si esegue setgid per assegnare il processo ad a un gruppo senza privilegi,24 e poi si ripete (27-30) loperazione usando setuid per cambiare anche lutente.25 Inne (30-36), qualora sia impostata la variabile demonize, prima (31) si apre il sistema di logging per la stampa degli errori, e poi (32-35) si invoca daemon per eseguire in background il processo come demone. A questo punto il programma riprende di nuovo lo schema gi` visto usato dal server per a il servizio daytime, con lunica dierenza della chiamata alla funzione PrintErr, riportata in g. 16.14, al posto di perror per la stampa degli errori. Si inizia con il porre (37-41) in ascolto il socket, e poi si esegue indenitamente il ciclo principale (42-59). Allinterno di questo si ricevono (43-47) le connessioni, creando (48-51) un processo glio per ciascuna di esse. Questultimo (52-56), chiuso (53) il listening socket, esegue (54) la funzione di gestione del servizio ServEcho, ed al ritorno di questa esce (55). Il padre invece si limita (57) a chiudere il connected socket per ricominciare da capo il ciclo in attesa di nuove connessioni. In questo modo si ha un server concorrente. La terminazione del padre non ` gestita esplicitamente, e deve essere eettuata inviando un segnale al processo. e Avendo trattato direttamente la gestione del programma come demone, si ` dovuto anche e provvedere alla necessit` di poter stampare eventuali messaggi di errore attraverso il sistema del a syslog trattato in sez. 10.1.5. Come accennato questo ` stato fatto utilizzando come wrapper la e funzione PrintErr, il cui codice ` riportato in g. 16.14. e In essa ci si limita a controllare (2) se ` stato impostato (valore attivo per default) luso come e demone, nel qual caso (3) si usa syslog (vedi sez. 10.1.5) per stampare il messaggio di errore fornito come argomento sui log di sistema. Se invece si ` in modalit` interattiva (attivabile con e a lopzione -i) si usa (5) semplicemente la funzione perror per stampare sullo standard error.
void PrintErr ( char * error ) { if ( demonize ) { /* daemon mode */ 3 syslog ( LOG_ERR , " % s : % m " , error ); /* log string and error message */ 4 } else { 5 perror ( error ); 6 } 7 return ; 8 }
1 2

Figura 16.14: Codice della funzione PrintErr per la generalizzazione della stampa degli errori sullo standard input o attraverso il syslog.

La gestione del servizio echo viene eettuata interamente nella funzione ServEcho, il cui codice ` mostrato in g. 16.15, e la comunicazione viene gestita allinterno di un ciclo (6-13). I e dati inviati dal client vengono letti (6) dal socket con una semplice read, di cui non si controlla lo stato di uscita, assumendo che ritorni solo in presenza di dati in arrivo. La riscrittura (7) viene invece gestita dalla funzione FullWrite (descritta in g. 16.7) che si incarica di tenere conto automaticamente della possibilit` che non tutti i dati di cui ` richiesta la scrittura vengano a e trasmessi con una singola write.
si ` usato il valore 65534, ovvero -1 per il formato short, che di norma in tutte le distribuzioni viene usato e per identicare il gruppo nogroup e lutente nobody, usati appunto per eseguire programmi che non richiedono nessun privilegio particolare. 25 si tenga presente che lordine in cui si eseguono queste due operazioni ` importante, infatti solo avendo i e privilegi di amministratore si pu` cambiare il gruppo di un processo ad un altro di cui non si fa parte, per cui o chiamare prima setuid farebbe fallire una successiva chiamata a setgid. Inoltre si ricordi (si riveda quanto esposto in sez. 3.3) che usando queste due funzioni il rilascio dei privilegi ` irreversibile. e
24

` 16.4. UN ESEMPIO PIU COMPLETO: IL SERVIZIO ECHO

463

void ServEcho ( int sockfd ) { char buffer [ MAXLINE ]; 3 int nread , nwrite ; 4 char debug [ MAXLINE +20]; 5 /* main loop , reading 0 char means client close connection */ 6 while ( ( nread = read ( sockfd , buffer , MAXLINE )) != 0) { 7 nwrite = FullWrite ( sockfd , buffer , nread ); 8 if ( nwrite ) { 9 PrintErr ( " write error " ); 10 } 11 } 12 return ; 13 }
1 2

Figura 16.15: Codice della prima versione della funzione ServEcho per la gestione del servizio echo.

In caso di errore di scrittura (si ricordi che FullWrite restituisce un valore nullo in caso di successo) si provvede (8-10) a stampare il relativo messaggio con PrintErr. Quando il client chiude la connessione il ricevimento del FIN fa ritornare la read con un numero di byte letti pari a zero, il che causa luscita dal ciclo e il ritorno (12) della funzione, che a sua volta causa la terminazione del processo glio.

16.4.4

Lavvio e il funzionamento normale

Bench il codice dellesempio precedente sia molto ridotto, esso ci permetter` di considerare in e a dettaglio le varie problematiche che si possono incontrare nello scrivere unapplicazione di rete. Infatti attraverso lesame delle sue modalit` di funzionamento normali, allavvio e alla terminaa zione, e di quello che avviene nelle varie situazioni limite, da una parte potremo approfondire la comprensione del protocollo TCP/IP e dallaltra ricavare le indicazioni necessarie per essere in grado di scrivere applicazioni robuste, in grado di gestire anche i casi limite. Il primo passo ` compilare e lanciare il server (da root, per poter usare la porta 7 che e ` riservata), alla partenza esso eseguir` lapertura passiva con la sequenza delle chiamate a e a socket, bind, listen e poi si bloccher` nella accept. A questo punto si potr` controllarne lo a a stato con netstat: [piccardi@roke piccardi]$ netstat -at Active Internet connections (servers and established) Proto Recv-Q Send-Q Local Address Foreign Address ... tcp 0 0 *:echo *:* ...

State LISTEN

che ci mostra come il socket sia in ascolto sulla porta richiesta, accettando connessioni da qualunque indirizzo e da qualunque porta e su qualunque interfaccia locale. A questo punto si pu` lanciare il client, esso chiamer` socket e connect; una volta como a pletato il three way handshake la connessione ` stabilita; la connect ritorner` nel client26 e la e a accept nel server, ed usando di nuovo netstat otterremmo che: Active Internet connections (servers and established)
si noti che ` sempre la connect del client a ritornare per prima, in quanto questo avviene alla ricezione del e secondo segmento (lACK del server) del three way handshake, la accept del server ritorna solo dopo un altro mezzo RTT quando il terzo segmento (lACK del client) viene ricevuto.
26

464 Proto Recv-Q Send-Q Local Address tcp 0 0 *:echo tcp 0 0 roke:echo

CAPITOLO 16. I SOCKET TCP Foreign Address *:* gont:32981 State LISTEN ESTABLISHED

mentre per quanto riguarda lesecuzione dei programmi avremo che: il client chiama la funzione ClientEcho che si blocca sulla fgets dato che non si ` ancora e scritto nulla sul terminale. il server eseguir` una fork facendo chiamare al processo glio la funzione ServEcho, a questultima si bloccher` sulla read dal socket sul quale ancora non sono presenti dati. a il processo padre del server chiamer` di nuovo accept bloccandosi no allarrivo di unaltra a connessione. e se usiamo il comando ps per esaminare lo stato dei processi otterremo un risultato del tipo: [piccardi@roke PID TTY ... ... 2356 pts/0 2358 pts/1 2359 pts/0 piccardi]$ ps ax STAT TIME COMMAND ... ... ... S 0:00 ./echod S 0:00 ./echo 127.0.0.1 S 0:00 ./echod

(dove si sono cancellate le righe inutili) da cui si evidenzia la presenza di tre processi, tutti in stato di sleep (vedi tab. 3.11). Se a questo punto si inizia a scrivere qualcosa sul client non sar` trasmesso niente n tanto a che non si prema il tasto di a capo (si ricordi quanto detto in sez. 7.2.5 a proposito dellI/O su terminale), solo allora fgets ritorner` ed il client scriver` quanto immesso sul socket, per poi a a passare a rileggere quanto gli viene inviato allindietro dal server, che a sua volta sar` inviato a sullo standard output, che nel caso ne provoca limmediata stampa a video.

16.4.5

La conclusione normale

Tutto quello che scriveremo sul client sar` rimandato indietro dal server e ristampato a video a ntanto che non concluderemo limmissione dei dati; una sessione tipica sar` allora del tipo: a [piccardi@roke sources]$ ./echo 127.0.0.1 Questa e una prova Questa e una prova Ho finito Ho finito che termineremo inviando un EOF dal terminale (usando la combinazione di tasti ctrl-D, che non compare a schermo); se eseguiamo un netstat a questo punto avremo: [piccardi@roke piccardi]$ netstat -at tcp 0 0 *:echo tcp 0 0 localhost:33032

*:* localhost:echo

LISTEN TIME_WAIT

con il client che entra in TIME_WAIT. Esaminiamo allora in dettaglio la sequenza di eventi che porta alla terminazione normale della connessione, che ci servir` poi da riferimento quando aronteremo il comportamento in a caso di conclusioni anomale:

` 16.4. UN ESEMPIO PIU COMPLETO: IL SERVIZIO ECHO

465

1. inviando un carattere di EOF da terminale la fgets ritorna restituendo un puntatore nullo che causa luscita dal ciclo di while, cos` la funzione ClientEcho ritorna. 2. al ritorno di ClientEcho ritorna anche la funzione main, e come parte del processo terminazione tutti i le descriptor vengono chiusi (si ricordi quanto detto in sez. 2.1.5); questo causa la chiusura del socket di comunicazione; il client allora invier` un FIN al server a a cui questo risponder` con un ACK. A questo punto il client verr` a trovarsi nello stato a a FIN_WAIT_2 ed il server nello stato CLOSE_WAIT (si riveda quanto spiegato in sez. 16.1.3). 3. quando il server riceve il FIN la read del processo glio che gestisce la connessione ritorna restituendo 0 causando cos` luscita dal ciclo e il ritorno di ServEcho, a questo punto il processo glio termina chiamando exit. 4. alluscita del glio tutti i le descriptor vengono chiusi, la chiusura del socket connesso fa s` che venga eettuata la sequenza nale di chiusura della connessione, viene emesso un FIN dal server che ricever` un ACK dal client, a questo punto la connessione ` conclusa e a e il client resta nello stato TIME_WAIT.

16.4.6

La gestione dei processi gli

Tutto questo riguarda la connessione, c` per` da tenere conto delleetto del procedimento e o di chiusura del processo glio nel server (si veda quanto esaminato in sez. 3.2.3). In questo caso avremo linvio del segnale SIGCHLD al padre, ma dato che non si ` installato un gestore e e che lazione predenita per questo segnale ` quella di essere ignorato, non avendo predisposto e la ricezione dello stato di terminazione, otterremo che il processo glio entrer` nello stato di a zombie (si riveda quanto illustrato in sez. 9.3.6), come risulter` ripetendo il comando ps: a 2356 pts/0 2359 pts/0 S Z 0:00 ./echod 0:00 [echod <defunct>]

Dato che non ` il caso di lasciare processi zombie, occorrer` ricevere opportunamente lo stato e a di terminazione del processo (si veda sez. 3.2.4), cosa che faremo utilizzando SIGCHLD secondo quanto illustrato in sez. 9.3.6. Una prima modica al nostro server ` pertanto quella di inserire la e gestione della terminazione dei processi gli attraverso luso di un gestore. Per questo useremo la funzione Signal (che abbiamo illustrato in g. 9.10), per installare il gestore che riceve i segnali dei processi gli terminati gi` visto in g. 9.4. Baster` allora aggiungere il seguente codice: a a ... /* install SIGCHLD handler */ Signal ( SIGCHLD , HandSigCHLD ); /* create socket */ ...

/* establish handler */

allesempio illustrato in g. 16.13. In questo modo per` si introduce un altro problema. Si ricordi infatti che, come spiegato o in sez. 9.3.1, quando un programma si trova in stato di sleep durante lesecuzione di una system call, questa viene interrotta alla ricezione di un segnale. Per questo motivo, alla ne dellesecuzione del gestore del segnale, se questo ritorna, il programma riprender` lesecuzione a ritornando dalla system call interrotta con un errore di EINTR. Vediamo allora cosa comporta tutto questo nel nostro caso: quando si chiude il client, il processo glio che gestisce la connessione terminer`, ed il padre, per evitare la creazione di zombie, a ricever` il segnale SIGCHLD eseguendo il relativo gestore. Al ritorno del gestore per` lesecuzione a o nel padre ripartir` subito con il ritorno della funzione accept (a meno di un caso fortuito in a cui il segnale arriva durante lesecuzione del programma in risposta ad una connessione) con

466

CAPITOLO 16. I SOCKET TCP

un errore di EINTR. Non avendo previsto questa eventualit` il programma considera questo un a errore fatale terminando a sua volta con un messaggio del tipo: [root@gont sources]# ./echod -i accept error: Interrupted system call Come accennato in sez. 9.3.1 le conseguenze di questo comportamento delle system call possono essere superate in due modi diversi, il pi` semplice ` quello di modicare il codice di u e Signal per richiedere il riavvio automatico delle system call interrotte secondo la semantica di BSD, usando lopzione SA_RESTART di sigaction; rispetto a quanto visto in g. 9.10. Deniremo allora la nuova funzione SignalRestart27 come mostrato in g. 16.16, ed installeremo il gestore usando questultima.
inline SigFunc * SignalRestart ( int signo , SigFunc * func ) { 3 struct sigaction new_handl , old_handl ; 4 new_handl . sa_handler = func ; /* set signal handler */ 5 new_handl . sa_flags = SA_RESTART ; /* restart system call */ 6 /* clear signal mask : no signal blocked during execution of func */ 7 if ( sigemptyset (& new_handl . sa_mask )!=0){ /* initialize signal set */ 8 return SIG_ERR ; 9 } 10 /* change action for signo signal */ 11 if ( sigaction ( signo , & new_handl , & old_handl )){ 12 return SIG_ERR ; 13 } 14 return ( old_handl . sa_handler ); 15 }
1 2

Figura 16.16: La funzione SignalRestart, che installa un gestore di segnali in semantica BSD per il riavvio automatico delle system call interrotte.

Come si pu` notare questa funzione ` identica alla precedente Signal, illustrata in g. 9.10, o e solo che in questo caso invece di inizializzare a zero il campo sa_flags di sigaction, lo si inizializza (5) al valore SA_RESTART. Usando questa funzione al posto di Signal nel server non ` e necessaria nessuna altra modica: le system call interrotte saranno automaticamente riavviate, e lerrore EINTR non si manifester` pi`. a u La seconda soluzione ` pi` invasiva e richiede di controllare tutte le volte lerrore restituito e u dalle varie system call, ripetendo la chiamata qualora questo corrisponda ad EINTR. Questa soluzione ha per` il pregio della portabilit`, infatti lo standard POSIX dice che la funzionalit` di o a a riavvio automatico delle system call, fornita da SA_RESTART, ` opzionale, per cui non ` detto che e e essa sia disponibile su qualunque sistema. Inoltre in certi casi,28 anche quando questa ` presente, e non ` detto possa essere usata con accept. e La portabilit` nella gestione dei segnali per` viene al costo di una riscrittura parziale del a o server, la nuova versione di questo, in cui si sono introdotte una serie di nuove opzioni che ci saranno utili per il debug, ` mostrata in g. 16.17, dove si sono riportate la sezioni di codice e modicate nella seconda versione del programma, il codice completo di questultimo si trova nel le TCP_echod_second.c dei sorgenti allegati alla guida. La prima modica eettuata ` stata quella di introdurre una nuova opzione a riga di coe mando, -c, che permette di richiedere il comportamento compatibile nella gestione di SIGCHLD
anche questa ` denita, insieme alle altre funzioni riguardanti la gestione dei segnali, nel le SigHand.c, il cui e contento completo pu` essere trovato negli esempi allegati. o 28 Stevens in [2] accenna che la maggior parte degli Unix derivati da BSD non fanno ripartire select; altri non riavviano neanche accept e recvfrom, cosa che invece nel caso di Linux viene sempre fatta.
27

` 16.4. UN ESEMPIO PIU COMPLETO: IL SERVIZIO ECHO

467

al posto della semantica BSD impostando la variabile compat ad un valore non nullo. Questa ` e preimpostata al valore nullo, cosicch se non si usa questa opzione il comportamento di default e del server ` di usare la semantica BSD. e Una seconda opzione aggiunta ` quella di inserire un tempo di attesa sso specicato in e secondi fra il ritorno della funzione listen e la chiamata di accept, specicabile con lopzione -w, che permette di impostare la variabile waiting. Inne si ` introdotta una opzione -d per e abilitare il debugging che imposta ad un valore non nullo la variabile debugging. Al solito si ` e omessa da g. 16.17 la sezione di codice relativa alla gestione di tutte queste opzioni, che pu` o essere trovata nel sorgente del programma.
int main ( int argc , char * argv []) { 3 ... 4 int waiting = 0; 5 int compat = 0; 6 ...
1 2 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45

/* Main code begin here */ if ( compat ) { Signal ( SIGCHLD , HandSigCHLD ); } else { SignalRestart ( SIGCHLD , HandSigCHLD ); } ...

/* install signal handler */ /* non restarting handler */ /* restarting handler */

/* main body */ if ( listen ( list_fd , BACKLOG ) < 0 ) { PrintErr ( " listen error " ); exit (1); } if ( waiting ) sleep ( waiting ); /* handle echo to client */ while (1) { /* accept connection */ len = sizeof ( cli_add ); while ((( conn_fd = accept ( list_fd , ( struct sockaddr *)& cli_add , & len )) < 0) && ( errno == EINTR )); if ( conn_fd < 0) { PrintErr ( " accept error " ); exit (1); } if ( debugging ) { inet_ntop ( AF_INET , & cli_add . sin_addr , ipaddr , sizeof ( ipaddr )); snprintf ( debug , MAXLINE , " Accepted connection form % s \ n " , ipaddr ); if ( demonize ) { syslog ( LOG_DEBUG , debug ); } else { printf ( " % s " , debug ); } } /* fork to handle connection */ ... } return ; }

Figura 16.17: La sezione nel codice della seconda versione del server per il servizio echo modicata per tener conto dellinterruzione delle system call.

468

CAPITOLO 16. I SOCKET TCP

Vediamo allora come ` cambiato il nostro server; una volta denite le variabili e trattate le e opzioni il primo passo (9-13) ` vericare la semantica scelta per la gestione di SIGCHLD, a seconda e del valore di compat (9) si installa il gestore con la funzione Signal (10) o con SignalRestart (12), essendo questultimo il valore di default. Tutta la sezione seguente, che crea il socket, cede i privilegi di amministratore ed eventualmente lancia il programma come demone, ` rimasta invariata e pertanto ` stata omessa in e e g. 16.17; lunica modica eettuata prima dellentrata nel ciclo principale ` stata quella di aver e introdotto, subito dopo la chiamata (17-20) alla funzione listen, una eventuale pausa con una condizione (21) sulla variabile waiting, che viene inizializzata, con lopzione -w Nsec, al numero di secondi da aspettare (il valore preimpostato ` nullo). e Si ` potuto lasciare inalterata tutta la sezione di creazione del socket perch nel server lunica e e chiamata ad una system call lenta, che pu` essere interrotta dallarrivo di SIGCHLD, ` quella ad o e accept, che ` lunica funzione che pu` mettere il processo padre in stato di sleep nel periodo e o in cui un glio pu` terminare; si noti infatti come le altre slow system call 29 o sono chiamate o prima di entrare nel ciclo principale, quando ancora non esistono processi gli, o sono chiamate dai gli stessi e non risentono di SIGCHLD. Per questo lunica modica sostanziale nel ciclo principale (23-42), rispetto precedente versione di g. 16.15, ` nella sezione (25-31) in cui si eettua la chiamata di accept. Questultima e viene eettuata (26-27) allinterno di un ciclo di while30 che la ripete indenitamente qualora in caso di errore il valore di errno sia EINTR. Negli altri casi si esce in caso di errore eettivo (27-29), altrimenti il programma prosegue. Si noti che in questa nuova versione si ` aggiunta una ulteriore sezione (32-40) di aiuto per e il debug del programma, che eseguita con un controllo (33) sul valore della variabile debugging impostato dallopzione -d. Qualora questo sia nullo, come preimpostato, non accade nulla. altrimenti (33) lindirizzo ricevuto da accept viene convertito in una stringa che poi (34-39) viene opportunamente stampata o sullo schermo o nei log. Inne come ulteriore miglioria si ` perfezionata la funzione ServEcho, sia per tenere conto e della nuova funzionalit` di debugging, che per eettuare un controllo in caso di errore; il codice a della nuova versione ` mostrato in g. 16.18. e Rispetto alla precedente versione di g. 16.15 in questo caso si ` provveduto a controllare e (7-10) il valore di ritorno di read per rilevare un eventuale errore, in modo da stampare (8) un messaggio di errore e ritornare (9) concludendo la connessione. Inoltre qualora sia stata attivata la funzionalit` di debug (avvalorando debugging tramite a lapposita opzione -d) si provveder` a stampare (tenendo conto della modalit` di invocazione a a del server, se interattiva o in forma di demone) il numero di byte e la stringa letta dal client (16-24).

16.5

I vari scenari critici

Con le modiche viste in sez. 16.4.6 il nostro esempio diventa in grado di arontare la gestione ordinaria delle connessioni, ma un server di rete deve tenere conto che, al contrario di quanto avviene per i server che operano nei confronti di processi presenti sulla stessa macchina, la rete ` di sua natura inadabile, per cui ` necessario essere in grado di gestire tutta una serie di e e situazioni critiche che non esistono per i processi locali.
si ricordi la distinzione fatta in sez. 9.3.1. la sintassi del C relativa a questo ciclo pu` non essere del tutto chiara. In questo caso infatti si ` usato un o e ciclo vuoto che non esegue nessuna istruzione, in questo modo quello che viene ripetuto con il ciclo ` soltanto il e codice che esprime la condizione allinterno del while.
30 29

16.5. I VARI SCENARI CRITICI

469

void ServEcho ( int sockfd ) { char buffer [ MAXLINE ]; 3 int nread , nwrite ; 4 char debug [ MAXLINE +20]; 5 /* main loop , reading 0 char means client close connection */ 6 while ( ( nread = read ( sockfd , buffer , MAXLINE )) != 0) { 7 if ( nread < 0) { 8 PrintErr ( " Errore in lettura " ); 9 return ; 10 } 11 nwrite = FullWrite ( sockfd , buffer , nread ); 12 if ( nwrite ) { 13 PrintErr ( " Errore in scrittura " ); 14 return ; 15 } 16 if ( debugging ) { 17 buffer [ nread ] = 0; 18 snprintf ( debug , MAXLINE +20 , " Letti % d byte , % s " , nread , buffer ); 19 if ( demonize ) { /* daemon mode */ 20 syslog ( LOG_DEBUG , debug ); 21 } else { 22 printf ( " % s " , debug ); 23 } 24 } 25 } 26 return ; 27 }
1 2

Figura 16.18: Codice della seconda versione della funzione ServEcho per la gestione del servizio echo.

16.5.1

La terminazione precoce della connessione

La prima situazione critica ` quella della terminazione precoce, causata da un qualche errore e sulla rete, della connessione eettuata da un client. Come accennato in sez. 16.2.4 la funzione accept riporta tutti gli eventuali errori di rete pendenti su una connessione sul connected socket. Di norma questo non ` un problema, in quanto non appena completata la connessione, accept e ritorna e lerrore sar` rilevato in seguito, dal processo che gestisce la connessione, alla prima a chiamata di una funzione che opera sul socket. ` E per` possibile, dal punto di vista teorico, incorrere anche in uno scenario del tipo di quello o mostrato in g. 16.19, in cui la connessione viene abortita sul lato client per un qualche errore di rete con linvio di un segmento RST, prima che nel server sia stata chiamata la funzione accept.

Figura 16.19: Un possibile caso di terminazione precoce della connessione.

470

CAPITOLO 16. I SOCKET TCP

Bench questo non sia un fatto comune, un evento simile pu` essere osservato con dei server e o molto occupati. In tal caso, con una struttura del server simile a quella del nostro esempio, in cui la gestione delle singole connessioni ` demandata a processi gli, pu` accadere che il three way e o handshake venga completato e la relativa connessione abortita subito dopo, prima che il padre, per via del carico della macchina, abbia fatto in tempo ad eseguire la chiamata ad accept. Di nuovo si ha una situazione analoga a quella illustrata in g. 16.19, in cui la connessione viene stabilita, ma subito dopo si ha una condizione di errore che la chiude prima che essa sia stata accettata dal programma. Questo signica che, oltre alla interruzione da parte di un segnale, che abbiamo trattato in sez. 16.4.6 nel caso particolare di SIGCHLD, si possono ricevere altri errori non fatali alluscita di accept, che come nel caso precedente, necessitano semplicemente la ripetizione della chiamata senza che si debba uscire dal programma. In questo caso anche la versione modicata del nostro server non sarebbe adatta, in quanto uno di questi errori causerebbe la terminazione dello stesso. In Linux i possibili errori di rete non fatali, riportati sul socket connesso al ritorno di accept, sono ENETDOWN, EPROTO, ENOPROTOOPT, EHOSTDOWN, ENONET, EHOSTUNREACH, EOPNOTSUPP e ENETUNREACH. Si tenga presente che questo tipo di terminazione non ` riproducibile terminando il client e prima della chiamata ad accept, come si potrebbe fare usando lopzione -w per introdurre una pausa dopo il lancio del demone, in modo da poter avere il tempo per lanciare e terminare una connessione usando il programma client. In tal caso infatti, alla terminazione del client, il socket associato alla connessione viene semplicemente chiuso, attraverso la sequenza vista in sez. 16.1.3, per cui la accept ritorner` senza errori, e si avr` semplicemente un end-of-le al primo accesso a a al socket. Nel caso di Linux inoltre, anche qualora si modichi il client per fargli gestire linvio di un segmento di RST alla chiusura dal socket (usando lopzione SO_LINGER, vedi sez. 17.2.3), non si ha nessun errore al ritorno di accept, quanto un errore di ECONNRESET al primo tentativo di accesso al socket.

16.5.2

La terminazione precoce del server

Un secondo caso critico ` quello in cui si ha una terminazione precoce del server, ad esempio e perch il programma ha un crash. In tal caso si suppone che il processo termini per un errore e fatale, cosa che potremo simulare inviandogli un segnale di terminazione. La conclusione del processo comporta la chiusura di tutti i le descriptor aperti, compresi tutti i socket relativi a connessioni stabilite; questo signica che al momento del crollo del servizio il client ricever` un a FIN dal server in corrispondenza della chiusura del socket. Vediamo allora cosa succede nel nostro caso, facciamo partire una connessione con il server e scriviamo una prima riga, poi terminiamo il server con un C-c. A questo punto scriviamo una seconda riga e poi unaltra riga ancora. Il risultato nale della sessione ` il seguente: e [piccardi@gont sources]$ ./echo 192.168.1.141 Prima riga Prima riga Seconda riga dopo il C-c Altra riga [piccardi@gont sources]$ Come si vede il nostro client, nonostante la connessione sia stata interrotta prima dellinvio della seconda riga, non solo accetta di inviarla, ma prende anche unaltra riga prima di terminare senza riportare nessun errore. Per capire meglio cosa ` successo conviene analizzare il usso dei pacchetti utilizzando un e analizzatore di traco come tcpdump. Il comando permette di selezionare, nel traco di rete generato su una macchina, i pacchetti che interessano, stampando a video (o salvando su disco) il

16.5. I VARI SCENARI CRITICI

471

loro contenuto. Non staremo qui ad entrare nei dettagli delluso del programma, che sono spiegati dalla pagina di manuale; per luso che vogliamo farne quello che ci interessa `, posizionandosi e sulla macchina che fa da client, selezionare tutti i pacchetti che sono diretti o provengono dalla macchina che fa da server. In questo modo (posto che non ci siano altre connessioni col server, cosa che avremo cura di evitare) tutti i pacchetti rilevati apparterranno alla nostra sessione di interrogazione del servizio. Il comando tcpdump permette selezioni molto complesse, basate sulle interfacce su cui passano i pacchetti, sugli indirizzi IP, sulle porte, sulle caratteristiche ed il contenuto dei pacchetti stessi, inoltre permette di combinare fra loro diversi criteri di selezione con degli operatori logici; quando un pacchetto che corrisponde ai criteri di selezione scelti viene rilevato i suoi dati vengono stampati sullo schermo (anche questi secondo un formato congurabile in maniera molto precisa). Lanciando il comando prima di ripetere la sessione di lavoro mostrata nellesempio precedente potremo allora catturare tutti pacchetti scambiati fra il client ed il server; i risultati31 prodotti in questa occasione da tcpdump sono allora i seguenti: [root@gont gapil]# tcpdump tcpdump: listening on eth0 gont.34559 > anarres.echo: anarres.echo > gont.34559: gont.34559 > anarres.echo: gont.34559 > anarres.echo: anarres.echo > gont.34559: anarres.echo > gont.34559: gont.34559 > anarres.echo: anarres.echo > gont.34559: gont.34559 > anarres.echo: gont.34559 > anarres.echo: anarres.echo > gont.34559: src 192.168.1.141 or dst 192.168.1.141 -N -t S S . P . P . F . P R 800922320:800922320(0) win 5840 511689719:511689719(0) ack 800922321 win 5792 ack 1 win 5840 1:12(11) ack 1 win 5840 ack 12 win 5792 1:12(11) ack 12 win 5792 ack 12 win 5840 12:12(0) ack 12 win 5792 ack 13 win 5840 12:37(25) ack 13 win 5840 511689732:511689732(0) win 0

Le prime tre righe vengono prodotte al momento in cui lanciamo il nostro client, e corrispondono ai tre pacchetti del three way handshake. Loutput del comando riporta anche i numeri di sequenza iniziali, mentre la lettera S indica che per quel pacchetto si aveva il SYN ag attivo. Si noti come a partire dal secondo pacchetto sia sempre attivo il campo ack, seguito dal numero di sequenza per il quale si da il ricevuto; questultimo, a partire dal terzo pacchetto, viene espresso in forma relativa per maggiore compattezza. Il campo win in ogni riga indica la advertised window di cui parlavamo in sez. 16.1.2. Allora si pu` vericare dalloutput del comando come o venga appunto realizzata la sequenza di pacchetti descritta in sez. 16.1.1: prima viene inviato dal client un primo pacchetto con il SYN che inizia la connessione, a cui il server risponde dando il ricevuto con un secondo pacchetto, che a sua volta porta un SYN, cui il client risponde con un il terzo pacchetto di ricevuto. Ritorniamo allora alla nostra sessione con il servizio echo: dopo le tre righe del three way handshake non avremo nulla n tanto che non scriveremo una prima riga sul client; al momento in cui facciamo questo si genera una sequenza di altri quattro pacchetti. Il primo, dal client al server, contraddistinto da una lettera P che signica che il ag PSH ` impostato, contiene la e nostra riga (che ` appunto di 11 caratteri), e ad esso il server risponde immediatamente con un e pacchetto vuoto di ricevuto. Poi tocca al server riscrivere indietro quanto gli ` stato inviato, per e cui sar` lui a mandare indietro un terzo pacchetto con lo stesso contenuto appena ricevuto, e a a sua volta ricever` dal client un ACK nel quarto pacchetto. Questo causer` la ricezione delleco a a nel client che lo stamper` a video. a
in realt` si ` ridotta la lunghezza delloutput rispetto al reale tagliando alcuni dati non necessari alla a e comprensione del usso.
31

472

CAPITOLO 16. I SOCKET TCP

A questo punto noi procediamo ad interrompere lesecuzione del server con un C-c (cio` e con linvio di SIGTERM): nel momento in cui facciamo questo vengono immediatamente generati altri due pacchetti. La terminazione del processo infatti comporta la chiusura di tutti i suoi le descriptor, il che comporta, per il socket che avevamo aperto, linizio della sequenza di chiusura illustrata in sez. 16.1.3. Questo signica che dal server partir` un FIN, che ` appunto il primo dei a e due pacchetti, contraddistinto dalla lettera F, cui seguir` al solito un ACK da parte del client. a A questo punto la connessione dalla parte del server ` chiusa, ed infatti se usiamo netstat e per controllarne lo stato otterremo che sul server si ha: anarres:/home/piccardi# netstat -ant Active Internet connections (servers and established) Proto Recv-Q Send-Q Local Address Foreign Address ... ... ... ... ... tcp 0 0 192.168.1.141:7 192.168.1.2:34626

State ... FIN_WAIT2

cio` essa ` andata nello stato FIN_WAIT2, che indica lavvenuta emissione del segmento FIN, e e mentre sul client otterremo che essa ` andata nello stato CLOSE_WAIT: e [root@gont gapil]# netstat -ant Active Internet connections (servers and established) Proto Recv-Q Send-Q Local Address Foreign Address ... ... ... ... ... tcp 1 0 192.168.1.2:34582 192.168.1.141:7

State ... CLOSE_WAIT

Il problema ` che in questo momento il client ` bloccato dentro la funzione ClientEcho nella e e chiamata a fgets, e sta attendendo dellinput dal terminale, per cui non ` in grado di accorgersi e di nulla. Solo quando inseriremo la seconda riga il comando uscir` da fgets e prover` a scriverla a a sul socket. Questo comporta la generazione degli ultimi due pacchetti riportati da tcpdump: il primo, inviato dal client contenente i 25 caratteri della riga appena letta, e ad esso la macchina server risponder`, non essendoci pi` niente in ascolto sulla porta 7, con un segmento di RST, a u contraddistinto dalla lettera R, che causa la conclusione denitiva della connessione anche nel client, dove non comparir` pi` nelloutput di netstat. a u Come abbiamo accennato in sez. 16.1.3 e come vedremo pi` avanti in sez. 16.6.3 la chiusura u di un solo capo di un socket ` una operazione lecita, per cui la nostra scrittura avr` comunque e a successo (come si pu` constatare lanciando usando strace32 ), in quanto il nostro programma o non ha a questo punto alcun modo di sapere che dallaltra parte non c` pi` nessuno processo e u in grado di leggere quanto scriver`. Questo sar` chiaro solo dopo il tentativo di scrittura, e la a a ricezione del segmento RST di risposta che indica che dallaltra parte non si ` semplicemente e chiuso un capo del socket, ma ` completamente terminato il programma. e Per questo motivo il nostro client proseguir` leggendo dal socket, e dato che questo ` stato a e chiuso avremo che, come spiegato in sez. 16.1.3, la funzione read ritorna normalmente con un valore nullo. Questo comporta che la seguente chiamata a fputs non ha eetto (viene stampata una stringa nulla) ed il client si blocca di nuovo nella successiva chiamata a fgets. Per questo diventa possibile inserire una terza riga e solo dopo averlo fatto si avr` la terminazione del a programma. Per capire come questa avvenga comunque, non avendo inserito nel codice nessun controllo di errore, occorre ricordare che, a parte la bidirezionalit` del usso dei dati, dal punto di vista del a funzionamento nei confronti delle funzioni di lettura e scrittura, i socket sono del tutto analoghi
il comando strace ` un comando di debug molto utile che prende come argomento un altro comando e ne e stampa a video tutte le invocazioni di una system call, coi relativi argomenti e valori di ritorno, per cui usandolo in questo contesto potremo vericare che eettivamente la write ha scritto la riga, che in eetti ` stata pure e trasmessa via rete.
32

16.5. I VARI SCENARI CRITICI

473

a delle pipe. Allora, da quanto illustrato in sez. 12.1.1, sappiamo che tutte le volte che si cerca di scrivere su una pipe il cui altro capo non ` aperto il lettura il processo riceve un segnale e di SIGPIPE, e questo ` esattamente quello che avviene in questo caso, e siccome non abbiamo e un gestore per questo segnale, viene eseguita lazione preimpostata, che ` quella di terminare il e processo. Per gestire in maniera pi` corretta questo tipo di evento dovremo allora modicare il nostro u client perch sia in grado di trattare le varie tipologie di errore, per questo dovremo riscrivere e la funzione ClientEcho, in modo da controllare gli stati di uscita delle varie chiamate. Si ` e riportata la nuova versione della funzione in g. 16.20.
void ClientEcho ( FILE * filein , int socket ) { 3 char sendbuff [ MAXLINE +1] , recvbuff [ MAXLINE +1]; 4 int nread , nwrite ; 5 while ( fgets ( sendbuff , MAXLINE , filein ) != NULL ) { 6 nwrite = FullWrite ( socket , sendbuff , strlen ( sendbuff )); 7 if ( nwrite < 0) { 8 printf ( " Errore in scrittura : % s " , strerror ( errno )); 9 return ; 10 } 11 nread = read ( socket , recvbuff , strlen ( sendbuff )); 12 if ( nread < 0) { 13 printf ( " Errore in lettura : % s \ n " , strerror ( errno )); 14 return ; 15 } 16 if ( nread == 0) { 17 printf ( " End of file in lettura % s \ n " ); 18 return ; 19 } 20 recvbuff [ nread ] = 0; 21 if ( fputs ( recvbuff , stdout ) == EOF ) { 22 perror ( " Errore in scrittura su terminale " ); 23 return ; 24 } 25 } 26 return ; 27 }
1 2

Figura 16.20: La sezione nel codice della seconda versione della funzione ClientEcho usata dal client per il servizio echo modicata per tener conto degli eventuali errori.

Come si pu` vedere in questo caso si controlla il valore di ritorno di tutte le funzioni, ed o inoltre si verica la presenza di un eventuale end of le in caso di lettura. Con questa modica il nostro client echo diventa in grado di accorgersi della chiusura del socket da parte del server, per cui ripetendo la sequenza di operazioni precedenti stavolta otterremo che: [piccardi@gont sources]$ ./echo 192.168.1.141 Prima riga Prima riga Seconda riga dopo il C-c EOF sul socket ma di nuovo si tenga presente che non c` modo di accorgersi della chiusura del socket n quando e non si esegue la scrittura della seconda riga; il protocollo infatti prevede che ci debba essere una scrittura prima di ricevere un RST che confermi la chiusura del le, e solo alle successive scritture si potr` ottenere un errore. a

474

CAPITOLO 16. I SOCKET TCP

Questa caratteristica dei socket ci mette di fronte ad un altro problema relativo al nostro client, e che cio` esso non ` in grado di accorgersi di nulla ntanto che ` bloccato nella lettura del e e e terminale fatta con gets. In questo caso il problema ` minimo, ma esso riemerger` pi` avanti, ed e a u ` quello che si deve arontare tutte le volte quando si ha a che fare con la necessit` di lavorare e a con pi` descrittori, nel qual caso diventa si pone la questione di come fare a non restare bloccati u su un socket quando altri potrebbero essere liberi. Vedremo come arontare questa problematica in sez. 16.6.

16.5.3

Altri scenari di terminazione della connessione

La terminazione del server ` solo uno dei possibili scenari di terminazione della connessione, e un altro caso ` ad esempio quello in cui si ha un crollo della rete, cosa che potremo simulare e facilmente staccando il cavo di rete. Unaltra condizione ` quella di un blocco della macchina e completo della su cui gira il server che deve essere riavviata, cosa che potremo simulare sia premendo il bottone di reset,33 che, in maniera pi` gentile, riavviando la macchina dopo aver u interrotto la connessione di rete. Cominciamo ad analizzare il primo caso, il crollo della rete. Ripetiamo la nostra sessione di lavoro precedente, lanciamo il client, scriviamo una prima riga, poi stacchiamo il cavo e scriviamo una seconda riga. Il risultato che otterremo `: e [piccardi@gont sources]$ ./echo 192.168.1.141 Prima riga Prima riga Seconda riga dopo linterruzione Errore in lettura: No route to host Quello che succede in questo ` che il programma, dopo aver scritto la seconda riga, resta e bloccato per un tempo molto lungo, prima di dare lerrore EHOSTUNREACH. Se andiamo ad osservare con strace cosa accade nel periodo in cui il programma ` bloccato vedremo che stavolta, e a dierenza del caso precedente, il programma ` bloccato nella lettura dal socket. e Se poi, come nel caso precedente, usiamo laccortezza di analizzare il traco di rete fra client e server con tcpdump, otterremo il seguente risultato: [root@gont sources]# tcpdump tcpdump: listening on eth0 gont.34685 > anarres.echo: S anarres.echo > gont.34685: S gont.34685 > anarres.echo: . gont.34685 > anarres.echo: P anarres.echo > gont.34685: . anarres.echo > gont.34685: P gont.34685 > anarres.echo: . gont.34685 > anarres.echo: P gont.34685 > anarres.echo: P gont.34685 > anarres.echo: P gont.34685 > anarres.echo: P gont.34685 > anarres.echo: P gont.34685 > anarres.echo: P gont.34685 > anarres.echo: P gont.34685 > anarres.echo: P
33

src 192.168.1.141 or dst 192.168.1.141 -N -t 1943495663:1943495663(0) win 5840 1215783131:1215783131(0) ack 1943495664 win 5792 ack 1 win 5840 1:12(11) ack 1 win 5840 ack 12 win 5792 1:12(11) ack 12 win 5792 ack 12 win 5840 12:45(33) ack 12 win 5840 12:45(33) ack 12 win 5840 12:45(33) ack 12 win 5840 12:45(33) ack 12 win 5840 12:45(33) ack 12 win 5840 12:45(33) ack 12 win 5840 12:45(33) ack 12 win 5840 12:45(33) ack 12 win 5840

un normale shutdown non va bene; in tal caso infatti il sistema provvede a terminare tutti i processi, per cui la situazione sarebbe sostanzialmente identica alla precedente.

16.5. I VARI SCENARI CRITICI gont.34685 > anarres.echo: P 12:45(33) ack 12 win 5840 arp who-has anarres tell gont arp who-has anarres tell gont arp who-has anarres tell gont arp who-has anarres tell gont arp who-has anarres tell gont arp who-has anarres tell gont ...

475

In questo caso landamento dei primi sette pacchetti ` esattamente lo stesso di prima. Solo e che stavolta, non appena inviata la seconda riga, il programma si bloccher` nella successiva a chiamata a read, non ottenendo nessuna risposta. Quello che succede ` che nel frattempo il e kernel provvede, come richiesto dal protocollo TCP, a tentare la ritrasmissione della nostra riga un certo numero di volte, con tempi di attesa crescente fra un tentativo ed il successivo, per tentare di ristabilire la connessione. Il risultato nale qui dipende dallimplementazione dello stack TCP, e nel caso di Linux anche dallimpostazione di alcuni dei parametri di sistema che si trovano in /proc/sys/net/ipv4, che ne controllano il comportamento: in questo caso in particolare da tcp_retries2 (vedi sez. 17.4.3). Questo parametro infatti specica il numero di volte che deve essere ritentata la ritrasmissione di un pacchetto nel mezzo di una connessione prima di riportare un errore di timeout. Il valore preimpostato ` pari a 15, il che comporterebbe 15 tentativi di ritrasmissione, e ma nel nostro caso le cose sono andate diversamente, dato che le ritrasmissioni registrate da tcpdump sono solo 8; inoltre lerrore riportato alluscita del client non ` stato ETIMEDOUT, come e dovrebbe essere in questo caso, ma EHOSTUNREACH. Per capire laccaduto continuiamo ad analizzare loutput di tcpdump: esso ci mostra che a un certo punto i tentativi di ritrasmissione del pacchetto sono cessati, per essere sostituiti da una serie di richieste di protocollo ARP in cui il client richiede lindirizzo del server. Come abbiamo accennato in sez. 14.3.1 ARP ` il protocollo che si incarica di trovare le e ` corrispondenze fra indirizzo IP e indirizzo hardware sulla scheda di rete. E evidente allora che nel nostro caso, essendo client e server sulla stessa rete, ` scaduta la voce nella ARP cache 34 e relativa ad anarres, ed il nostro client ha iniziato ad eettuare richieste ARP sulla rete per sapere lIP di questultimo, che essendo scollegato non poteva rispondere. Anche per questo tipo di richieste esiste un timeout, per cui dopo un certo numero di tentativi il meccanismo si ` e interrotto, e lerrore riportato al programma a questo punto ` stato EHOSTUNREACH, in quanto e non si era pi` in grado di contattare il server. u Un altro errore possibile in questo tipo di situazione, che si pu` avere quando la macchina o ` su una rete remota, ` ENETUNREACH; esso viene riportato alla ricezione di un pacchetto ICMP e e di destination unreachable da parte del router che individua linterruzione della connessione. Di nuovo anche qui il risultato nale dipende da quale ` il meccanismo pi` veloce ad accorgersi del e u problema. Se per` agiamo sui parametri del kernel, e scriviamo in tcp_retries2 un valore di tentativi o pi` basso, possiamo evitare la scadenza della ARP cache e vedere cosa succede. Cos` se ad u esempio richiediamo 4 tentativi di ritrasmissione, lanalisi di tcpdump ci riporter` il seguente a scambio di pacchetti: [root@gont gapil]# tcpdump src 192.168.1.141 or dst 192.168.1.141 -N -t tcpdump: listening on eth0 gont.34752 > anarres.echo: S 3646972152:3646972152(0) win 5840
la ARP cache ` una tabella mantenuta internamente dal kernel che contiene tutte le corrispondenze fra e indirizzi IP e indirizzi sici, ottenute appunto attraverso il protocollo ARP; le voci della tabella hanno un tempo di vita limitato, passato il quale scadono e devono essere nuovamente richieste.
34

476 anarres.echo gont.34752 > gont.34752 > anarres.echo anarres.echo gont.34752 > gont.34752 > gont.34752 > gont.34752 > gont.34752 > gont.34752 > > gont.34752: anarres.echo: anarres.echo: > gont.34752: > gont.34752: anarres.echo: anarres.echo: anarres.echo: anarres.echo: anarres.echo: anarres.echo: S . P . P . P P P P P

CAPITOLO 16. I SOCKET TCP 2735190336:2735190336(0) ack 3646972153 win 5792 ack 1 win 5840 1:12(11) ack 1 win 5840 ack 12 win 5792 1:12(11) ack 12 win 5792 ack 12 win 5840 12:45(33) ack 12 win 5840 12:45(33) ack 12 win 5840 12:45(33) ack 12 win 5840 12:45(33) ack 12 win 5840 12:45(33) ack 12 win 5840

e come si vede in questo caso i tentativi di ritrasmissione del pacchetto iniziale sono proprio 4 (per un totale di 5 voci con quello trasmesso la prima volta), ed in eetti, dopo un tempo molto pi` breve rispetto a prima ed in corrispondenza dellinvio dellultimo tentativo, quello che u otterremo come errore alluscita del client sar` diverso, e cio`: a e [piccardi@gont sources]$ ./echo 192.168.1.141 Prima riga Prima riga Seconda riga dopo linterruzione Errore in lettura: Connection timed out che corrisponde appunto, come ci aspettavamo, alla ricezione di un ETIMEDOUT. Analizziamo ora il secondo scenario, in cui si ha un crollo della macchina che fa da server. Al solito lanciamo il nostro client, scriviamo una prima riga per vericare che sia tutto a posto, poi stacchiamo il cavo e riavviamo il server. A questo punto, ritornato attivo il server, scriviamo una seconda riga. Quello che otterremo in questo caso `: e [piccardi@gont sources]$ ./echo 192.168.1.141 Prima riga Prima riga Seconda riga dopo linterruzione Errore in lettura Connection reset by peer e lerrore ricevuti da read stavolta ` ECONNRESET. Se al solito riportiamo lanalisi dei pacchetti e eettuata con tcpdump, avremo: [root@gont gapil]# tcpdump tcpdump: listening on eth0 gont.34756 > anarres.echo: anarres.echo > gont.34756: gont.34756 > anarres.echo: gont.34756 > anarres.echo: anarres.echo > gont.34756: anarres.echo > gont.34756: gont.34756 > anarres.echo: gont.34756 > anarres.echo: anarres.echo > gont.34756: src 192.168.1.141 or dst 192.168.1.141 -N -t S S . P . P . P R 904864257:904864257(0) win 5840 4254564871:4254564871(0) ack 904864258 win 5792 ack 1 win 5840 1:12(11) ack 1 win 5840 ack 12 win 5792 1:12(11) ack 12 win 5792 ack 12 win 5840 12:45(33) ack 12 win 5840 4254564883:4254564883(0) win 0

Ancora una volta i primi sette pacchetti sono gli stessi; ma in questo caso quello che succede dopo lo scambio iniziale ` che, non avendo inviato nulla durante il periodo in cui si ` riavviato il e e server, il client ` del tutto ignaro dellaccaduto per cui quando eettuer` una scrittura, dato che e a

16.6. LUSO DELLI/O MULTIPLEXING

477

la macchina server ` stata riavviata e che tutti gli stati relativi alle precedenti connessioni sono e completamente persi, anche in presenza di una nuova istanza del server echo non sar` possibile a consegnare i dati in arrivo, per cui alla loro ricezione il kernel risponder` con un segmento di a RST. Il client da parte sua, dato che neanche in questo caso non ` stato emesso un FIN, dopo aver e scritto verr` bloccato nella successiva chiamata a read, che per` adesso ritorner` immediataa o a mente alla ricezione del segmento RST, riportando appunto come errore ECONNRESET. Occorre precisare che se si vuole che il client sia in grado di accorgersi del crollo del server anche quando non sta eettuando uno scambio di dati, ` possibile usare una impostazione speciale del socket e (ci torneremo in sez. 17.2.2) che provvede allesecuzione di questo controllo.

16.6

Luso dellI/O multiplexing

Aronteremo in questa sezione lutilizzo dellI/O multiplexing, arontato in sez. 11.1, nellambito delle applicazioni di rete. Gi` in sez. 16.5.2 era emerso il problema relativo al client del servizio a echo che non era in grado di accorgersi della terminazione precoce del server, essendo bloccato nella lettura dei dati immessi da tastiera. Abbiamo visto in sez. 11.1 quali sono le funzionalit` del sistema che ci permettono di tenere a sotto controllo pi` le descriptor in contemporanea; in quella occasione non abbiamo fatto u esempi, in quanto quando si tratta con le normali questa tipologia di I/O normalmente non viene usata, ` invece un caso tipico delle applicazioni di rete quello di dover gestire varie connessioni e da cui possono arrivare dati comuni in maniera asincrona, per cui riprenderemo largomento in questa sezione.

16.6.1

Il comportamento della funzione select con i socket.

Iniziamo con la prima delle funzioni usate per lI/O multiplexing, select; il suo funzionamento ` gi` stato descritto in dettaglio in sez. 11.1 e non staremo a ripetere quanto detto l` sappiamo e a ; che la funzione ritorna quando uno o pi` dei le descriptor messi sotto controllo ` pronto per la u e relativa operazione. In quelloccasione non abbiamo per` denito cosa si intende per pronto, infatti per dei normali o le, o anche per delle pipe, la condizione di essere pronti per la lettura o la scrittura ` ovvia; e invece lo ` molto meno nel caso dei socket, visto che possono intervenire tutte una serie di e possibili condizioni di errore dovute alla rete. Occorre allora specicare chiaramente quali sono le condizioni per cui un socket risulta essere pronto quando viene passato come membro di uno dei tre le descriptor set usati da select. Le condizioni che fanno si che la funzione select ritorni segnalando che un socket (che sar` a riportato nel primo insieme di le descriptor) ` pronto per la lettura sono le seguenti: e nel buer di ricezione del socket sono arrivati dei dati in quantit` suciente a superare il a valore di una soglia di basso livello (il cosiddetto low watermark ). Questo valore ` espresso e in numero di byte e pu` essere impostato con lopzione del socket SO_RCVLOWAT (tratteremo o luso di questa opzione in sez. 17.2.2); il suo valore di default ` 1 per i socket TCP e UDP. In e questo caso una operazione di lettura avr` successo e legger` un numero di byte maggiore a a di zero. il lato in lettura della connessione ` stato chiuso; si ` cio` ricevuto un segmento FIN (si e e e ricordi quanto illustrato in sez. 16.1.3) sulla connessione. In questo caso una operazione di lettura avr` successo, ma non risulteranno presenti dati (in sostanza read ritorner` con a a un valore nullo) per indicare la condizione di end-of-le. c` stato un errore sul socket. In questo caso una operazione di lettura non si bloccher` ma e a restituir` una condizione di errore (ad esempio read restituir` -1) e imposter` la variabile a a a

478

CAPITOLO 16. I SOCKET TCP errno al relativo valore. Vedremo in sez. 17.2.2 come sia possibile estrarre e cancellare gli errori pendenti su un socket senza usare read usando lopzione SO_ERROR. quando si sta utilizzando un listening socket ed ci sono delle connessioni completate. In questo caso la funzione accept non si bloccher`.35 a

Le condizioni che fanno si che la funzione select ritorni segnalando che un socket (che sar` a riportato nel secondo insieme di le descriptor) ` pronto per la scrittura sono le seguenti: e nel buer di invio ` disponibile una quantit` di spazio superiore al valore della soglia di e a basso livello in scrittura ed inoltre o il socket ` gi` connesso o non necessita (ad esempio e a ` UDP) di connessione. Il valore della soglia ` espresso in numero di byte e pu` essere e e o impostato con lopzione del socket SO_SNDLOWAT (trattata in sez. 17.2.2); il suo valore di default ` 2048 per i socket TCP e UDP. In questo caso una operazione di scrittura non e si bloccher` e restituir` un valore positivo pari al numero di byte accettati dal livello di a a trasporto. il lato in scrittura della connessione ` stato chiuso. In questo caso una operazione di e scrittura sul socket generer` il segnale SIGPIPE. a c` stato un errore sul socket. In questo caso una operazione di scrittura non si bloccher` e a ma restituir` una condizione di errore ed imposter` opportunamente la variabile errno. a a Vedremo in sez. 17.2.2 come sia possibile estrarre e cancellare errori pendenti su un socket usando lopzione SO_ERROR. Inne c` una sola condizione che fa si che select ritorni segnalando che un socket (che sar` e a riportato nel terzo insieme di le descriptor) ha una condizione di eccezione pendente, e cio` la e ricezione sul socket di dati urgenti (o out-of-band ), una caratteristica specica dei socket TCP su cui torneremo in sez. 19.1.3. Si noti come nel caso della lettura select si applichi anche ad operazioni che non hanno nulla a che fare con lI/O di dati come il riconoscimento della presenza di connessioni pronte, in modo da consentire anche lutilizzo di accept in modalit` non bloccante. Si noti inne come a in caso di errore un socket venga sempre riportato come pronto sia per la lettura che per la scrittura. Lo scopo dei due valori di soglia per i buer di ricezione e di invio ` quello di consentire e maggiore essibilit` nelluso di select da parte dei programmi, se infatti si sa che una applicaa zione non ` in grado di fare niente ntanto che non pu` ricevere o inviare una certa quantit` di e o a dati, si possono utilizzare questi valori per far si che select ritorni solo quando c` la certezza e di avere dati a sucienza.36

16.6.2

Un esempio di I/O multiplexing

Abbiamo incontrato la problematica tipica che conduce alluso dellI/O multiplexing nella nostra analisi degli errori in sez. 16.5.1, quando il nostro client non era in grado di rendersi conto di errori sulla connessione essendo impegnato nella attesa di dati in ingresso dallo standard input. In questo caso il problema ` quello di dover tenere sotto controllo due diversi le descriptor, e lo standard input, da cui viene letto il testo che vogliamo inviare al server, e il socket connesso
35 in realt` questo non ` sempre vero, come accennato in sez. 16.5.1 una connessione pu` essere abortita dalla a e o ricezione di un segmento RST una volta che ` stata completata, allora se questo avviene dopo che select ` e e ritornata, ma prima della chiamata ad accept, questultima, in assenza di altre connessioni, potr` bloccarsi. a 36 questo tipo di controllo ` utile di norma solo per la lettura, in quanto in genere le operazioni di scrittura e sono gi` controllate dallapplicazione, che sa sempre quanti dati invia, mentre non ` detto possa conoscere la a e quantit` di dati in ricezione; per cui, nella situazione in cui si conosce almeno un valore minimo, per evitare la a penalizzazione dovuta alla ripetizione delle operazioni di lettura per accumulare dati sucienti, si pu` lasciare al o kernel il compito di impostare un minimo al di sotto del quale il socket, pur avendo disponibili dei dati, non viene dato per pronto in lettura.

16.6. LUSO DELLI/O MULTIPLEXING

479

con il server su cui detto testo sar` scritto e dal quale poi si vorr` ricevere la risposta. Luso a a dellI/O multiplexing consente di tenere sotto controllo entrambi, senza restare bloccati. Nel nostro caso quello che ci interessa ` non essere bloccati in lettura sullo standard input e in caso di errori sulla connessione o chiusura della stessa da parte del server. Entrambi questi casi possono essere rilevati usando select, per quanto detto in sez. 16.6.1, mettendo sotto osservazione i le descriptor per la condizione di essere pronti in lettura: sia infatti che si ricevano dati, che la connessione sia chiusa regolarmente (con la ricezione di un segmento FIN) che si riceva una condizione di errore (con un segmento RST) il socket connesso sar` pronto in lettura a (nellultimo caso anche in scrittura, ma questo non ` necessario ai nostri scopi). e
void ClientEcho ( FILE * filein , int socket ) { 3 char sendbuff [ MAXLINE +1] , recvbuff [ MAXLINE +1]; 4 int nread , nwrite ; 5 int maxfd ; 6 fd_set fset ; 7 /* initialize file descriptor set */ 8 FD_ZERO (& fset ); 9 maxfd = max ( fileno ( filein ) , socket ) + 1; 10 while (1) { 11 FD_SET ( socket , & fset ); /* set for the socket */ 12 FD_SET ( fileno ( filein ) , & fset ); /* set for the standard input */ 13 select ( maxfd , & fset , NULL , NULL , NULL ); /* wait for read ready */ 14 if ( FD_ISSET ( fileno ( filein ) , & fset )) { /* if ready on stdin */ 15 if ( fgets ( sendbuff , MAXLINE , filein ) == NULL ) { /* if no input */ 16 return ; /* we stopped client */ 17 } else { /* else we have to write to socket */ 18 nwrite = FullWrite ( socket , sendbuff , strlen ( sendbuff )); 19 if ( nwrite < 0) { /* on error stop */ 20 printf ( " Errore in scrittura : % s " , strerror ( errno )); 21 return ; 22 } 23 } 24 } 25 if ( FD_ISSET ( socket , & fset )) { /* if ready on socket */ 26 nread = read ( socket , recvbuff , strlen ( sendbuff )); /* do read */ 27 if ( nread < 0) { /* error condition , stop client */ 28 printf ( " Errore in lettura : % s \ n " , strerror ( errno )); 29 return ; 30 } 31 if ( nread == 0) { /* server closed connection , stop */ 32 printf ( " EOF sul socket \ n " ); 33 return ; 34 } 35 recvbuff [ nread ] = 0; /* else read is ok , write on stdout */ 36 if ( fputs ( recvbuff , stdout ) == EOF ) { 37 perror ( " Errore in scrittura su terminale " ); 38 return ; 39 } 40 } 41 } 42 }
1 2

Figura 16.21: La sezione nel codice della terza versione della funzione ClientEcho usata dal client per il servizio echo modicata per luso di select.

Riprendiamo allora il codice del client, modicandolo per luso di select. Quello che dobbiamo modicare ` la funzione ClientEcho di g. 16.20, dato che tutto il resto, che riguarda e

480

CAPITOLO 16. I SOCKET TCP

le modalit` in cui viene stabilita la connessione con il server, resta assolutamente identico. La a nostra nuova versione di ClientEcho, la terza della serie, ` riportata in g. 16.21, il codice e completo si trova nel le TCP_echo_third.c dei sorgenti allegati alla guida. In questo caso la funzione comincia (8-9) con lazzeramento del le descriptor set fset e limpostazione del valore maxfd, da passare a select come massimo per il numero di le descriptor. Per determinare questultimo si usa la macro max denita nel nostro le macro.h che raccoglie una collezione di macro di preprocessore di varia utilit`. a La funzione prosegue poi (10-41) con il ciclo principale, che viene ripetuto indenitamente. Per ogni ciclo si reinizializza (11-12) il le descriptor set, impostando i valori per il le descriptor associato al socket socket e per lo standard input (il cui valore si recupera con la funzione fileno). Questo ` necessario in quanto la successiva (13) chiamata a select comporta una e modica dei due bit relativi, che quindi devono essere reimpostati allinizio di ogni ciclo. Si noti come la chiamata a select venga eseguita usando come primo argomento il valore di maxfd, precedentemente calcolato, e passando poi il solo le descriptor set per il controllo dellattivit` in lettura, negli altri argomenti sono passati tutti puntatori nulli, non interessando a n il controllo delle altre attivit`, n limpostazione di un valore di timeout. e a e Al ritorno di select si provvede a controllare quale dei due le descriptor presenta attivit` a in lettura, cominciando (14-24) con il le descriptor associato allo standard input. In caso di attivit` (quando cio` FD_ISSET ritorna una valore diverso da zero) si esegue (15) una fgets a e per leggere gli eventuali dati presenti; se non ve ne sono (e la funzione restituisce pertanto un puntatore nullo) si ritorna immediatamente (16) dato che questo signica che si ` chiuso lo e standard input e quindi concluso lutilizzo del client; altrimenti (18-22) si scrivono i dati appena letti sul socket, prevedendo una uscita immediata in caso di errore di scrittura. Controllato lo standard input si passa a controllare (25-40) il socket connesso, in caso di attivit` (26) si esegue subito una read di cui si controlla il valore di ritorno; se questo ` negativo a e (27-30) si ` avuto un errore e pertanto si esce immediatamente segnalandolo, se ` nullo (31e e 34) signica che il server ha chiuso la connessione, e di nuovo si esce con stampando prima un messaggio di avviso, altrimenti (35-39) si eettua la terminazione della stringa e la si stampa a sullo standard output (uscendo in caso di errore), per ripetere il ciclo da capo. Con questo meccanismo il programma invece di essere bloccato in lettura sullo standard input resta bloccato sulla select, che ritorna soltanto quando viene rilevata attivit` su uno a dei due le descriptor posti sotto controllo. Questo di norma avviene solo quando si ` scritto e qualcosa sullo standard input, o quando si riceve dal socket la risposta a quanto si era appena scritto. Ma adesso il client diventa capace di accorgersi immediatamente della terminazione del server; in tal caso infatti il server chiuder` il socket connesso, ed alla ricezione del FIN la funzione a select ritorner` (come illustrato in sez. 16.6.1) segnalando una condizione di end of le, per cui a il nostro client potr` uscire immediatamente. a Riprendiamo la situazione arontata in sez. 16.5.2, terminando il server durante una connessione, in questo caso quello che otterremo, una volta scritta una prima riga ed interrotto il server con un C-c, sar`: a [piccardi@gont sources]$ ./echo 192.168.1.1 Prima riga Prima riga EOF sul socket dove lultima riga compare immediatamente dopo aver interrotto il server. Il nostro client infatti ` in grado di accorgersi immediatamente che il socket connesso ` stato chiuso ed uscire e e immediatamente. Veniamo allora agli altri scenari di terminazione anomala visti in sez. 16.5.3. Il primo di questi ` linterruzione sica della connessione; in questo caso avremo un comportamento analogo e

16.6. LUSO DELLI/O MULTIPLEXING

481

al precedente, in cui si scrive una riga e non si riceve risposta dal server e non succede niente no a quando non si riceve un errore di EHOSTUNREACH o ETIMEDOUT a seconda dei casi. La dierenza ` che stavolta potremo scrivere pi` righe dopo linterruzione, in quanto il nostro e u client dopo aver inviato i dati non si bloccher` pi` nella lettura dal socket, ma nella select; per a u questo potr` accettare ulteriore dati che scriver` di nuovo sul socket, ntanto che c` spazio sul a a e buer di uscita (ecceduto il quale si bloccher` in scrittura). Si ricordi infatti che il client non a ha modo di determinare se la connessione ` attiva o meno (dato che in molte situazioni reali e linattivit` pu` essere temporanea). Tra laltro se si ricollega la rete prima della scadenza del a o timeout, potremo anche vericare come tutto quello che si era scritto viene poi eettivamente trasmesso non appena la connessione ridiventa attiva, per cui otterremo qualcosa del tipo: [piccardi@gont sources]$ ./echo 192.168.1.1 Prima riga Prima riga Seconda riga dopo linterruzione Terza riga Quarta riga Seconda riga dopo linterruzione Terza riga Quarta riga in cui, una volta riconnessa la rete, tutto quello che abbiamo scritto durante il periodo di disconnessione restituito indietro e stampato immediatamente. Lo stesso comportamento visto in sez. 16.5.2 si riottiene nel caso di un crollo completo della macchina su cui sta il server. In questo caso di nuovo il client non ` in grado di accorgersi e di niente dato che si suppone che il programma server non venga terminato correttamente, ma si blocchi tutto senza la possibilit` di avere lemissione di un segmento FIN che segnala a la terminazione della connessione. Di nuovo ntanto che la connessione non si riattiva (con il riavvio della macchina del server) il client non ` in grado di fare altro che accettare dellinput e e tentare di inviarlo. La dierenza in questo caso ` che non appena la connessione ridiventa attiva e i dati verranno s` trasmessi, ma essendo state perse tutte le informazioni relative alle precedenti connessioni ai tentativi di scrittura del client sar` risposto con un segmento RST che provocher` a a il ritorno di select per la ricezione di un errore di ECONNRESET.

16.6.3

La funzione shutdown

Come spiegato in sez. 16.1.3 il procedimento di chiusura di un socket TCP prevede che da ` entrambe le parti venga emesso un segmento FIN. E pertanto del tutto normale dal punto di vista del protocollo che uno dei due capi chiuda la connessione, quando laltro capo la lascia aperta.37 ` E pertanto possibile avere una situazione in cui un capo della connessione non avendo pi` u nulla da scrivere, possa chiudere il socket, segnalando cos` lavvenuta terminazione della tra smissione (laltro capo ricever` infatti un end-of-le in lettura) mentre dallaltra parte si potr` a a proseguire la trasmissione dei dati scrivendo sul socket che da quel lato ` ancora aperto. Questa e ` quella situazione in cui si dice che il socket ` half closed. e e Il problema che si pone ` che se la chiusura del socket ` eettuata con la funzione close, e e come spiegato in sez. 16.2.6, si perde ogni possibilit` di poter rileggere quanto laltro capo pu` a o continuare a scrivere. Per poter permettere allora di segnalare che si ` concluso con la scrittura, e continuando al contempo a leggere quanto pu` provenire dallaltro capo del socket si pu` allora o o usare la funzione shutdown, il cui prototipo `: e
37

abbiamo incontrato questa situazione nei vari scenari critici di sez. 16.5.

482
#include <sys/socket.h> int shutdown(int sockfd, int how) Chiude un lato della connessione fra due socket.

CAPITOLO 16. I SOCKET TCP

La funzione restituisce zero in caso di successo e -1 per un errore, nel qual caso errno assumer` i a valori: ENOTSOCK ENOTCONN il le descriptor non corrisponde a un socket. il socket non ` connesso. e

ed inoltre EBADF.

La funzione prende come primo argomento il socket sockfd su cui si vuole operare e come secondo argomento un valore intero how che indica la modalit` di chiusura del socket, questultima a pu` prendere soltanto tre valori: o SHUT_RD chiude il lato in lettura del socket, non sar` pi` possibile leggere dati da esso, tutti a u gli eventuali dati trasmessi dallaltro capo del socket saranno automaticamente scartati dal kernel, che, in caso di socket TCP, provveder` comunque ad inviare i a relativi segmenti di ACK. chiude il lato in scrittura del socket, non sar` pi` possibile scrivere dati su di esso. a u Nel caso di socket TCP la chiamata causa lemissione di un segmento FIN, secondo la procedura chiamata half-close. Tutti i dati presenti nel buer di scrittura prima della chiamata saranno inviati, seguiti dalla sequenza di chiusura illustrata in sez. 16.1.3. ` chiude sia il lato in lettura che quello in scrittura del socket. E equivalente alla chiamata in sequenza con SHUT_RD e SHUT_WR.

SHUT_WR

SHUT_RDWR

Ci si pu` chiedere quale sia lutilit` di avere introdotto SHUT_RDWR quando questa sembra o a rendere shutdown del tutto equivalente ad una close. In realt` non ` cos` esiste infatti unaltra a e , dierenza con close, pi` sottile. Finora infatti non ci siamo presi la briga di sottolineare in u maniera esplicita che, come per i le e le fo, anche per i socket possono esserci pi` riferimenti u contemporanei ad uno stesso socket. Per cui si avrebbe potuto avere limpressione che sia una corrispondenza univoca fra un socket ed il le descriptor con cui vi si accede. Questo non ` e assolutamente vero, (e lo abbiamo gi` visto nel codice del server di g. 16.13), ed ` invece a e assolutamente normale che, come per gli altri oggetti, ci possano essere pi` le descriptor che u fanno riferimento allo stesso socket. Allora se avviene uno di questi casi quello che succeder` ` che la chiamata a close dar` eetae a tivamente avvio alla sequenza di chiusura di un socket soltanto quando il numero di riferimenti a questultimo diventer` nullo. Fintanto che ci sono le descriptor che fanno riferimento ad un a socket luso di close si limiter` a deallocare nel processo corrente il le descriptor utilizzato, a ma il socket rester` pienamente accessibile attraverso tutti gli altri riferimenti. Se torniamo ala lesempio originale del server di g. 16.13 abbiamo infatti che ci sono due close, una sul socket connesso nel padre, ed una sul socket in ascolto nel glio, ma queste non eettuano nessuna chiusura reale di detti socket, dato che restano altri riferimenti attivi, uno al socket connesso nel glio ed uno a quello in ascolto nel padre. Questo non avviene aatto se si usa shutdown con argomento SHUT_RDWR al posto di close; in questo caso infatti la chiusura del socket viene eettuata immediatamente, indipendentemente dalla presenza di altri riferimenti attivi, e pertanto sar` ecace anche per tutti gli altri le a descriptor con cui, nello stesso o in altri processi, si fa riferimento allo stesso socket. Il caso pi` comune di uso di shutdown ` comunque quello della chiusura del lato in scrittura, u e per segnalare allaltro capo della connessione che si ` concluso linvio dei dati, restando comunque e in grado di ricevere quanto questi potr` ancora inviarci. Questo ` ad esempio luso che ci serve per a e rendere nalmente completo il nostro esempio sul servizio echo. Il nostro client infatti presenta

16.6. LUSO DELLI/O MULTIPLEXING

483

ancora un problema, che nelluso che nora ne abbiamo fatto non ` emerso, ma che ci aspetta e dietro langolo non appena usciamo dalluso interattivo e proviamo ad eseguirlo redirigendo standard input e standard output. Cos` se eseguiamo: [piccardi@gont sources]$ ./echo 192.168.1.1 < ../fileadv.tex > copia

vedremo che il le copia risulta mancare della parte nale. Per capire cosa avviene in questo caso occorre tenere presente come avviene la comunicazione via rete; quando redirigiamo lo standard input il nostro client inizier` a leggere il contenuto a del le ../fileadv.tex a blocchi di dimensione massima pari a MAXLINE per poi scriverlo, alla massima velocit` consentitagli dalla rete, sul socket. Dato che la connessione ` con una macchina a e remota occorre un certo tempo perch i pacchetti vi arrivino, vengano processati, e poi tornino e indietro. Considerando trascurabile il tempo di processo, questo tempo ` quello impiegato nella e trasmissione via rete, che viene detto RTT (dalla denominazione inglese Round Trip Time) ed ` quello che viene stimato con luso del comando ping. e A questo punto, se torniamo al codice mostrato in g. 16.21, possiamo vedere che mentre i pacchetti sono in transito sulla rete il client continua a leggere e a scrivere ntanto che il le in ingresso nisce. Per` non appena viene ricevuto un end-of-le in ingresso il nostro client o termina. Nel caso interattivo, in cui si inviavano brevi stringhe una alla volta, cera sempre il tempo di eseguire la lettura completa di quanto il server rimandava indietro. In questo caso invece, quando il client termina, essendo la comunicazione saturata e a piena velocit`, ci saranno a ancora pacchetti in transito sulla rete che devono arrivare al server e poi tornare indietro, ma siccome il client esce immediatamente dopo la ne del le in ingresso, questi non faranno a tempo a completare il percorso e verranno persi. Per evitare questo tipo di problema, invece di uscire una volta completata la lettura del le in ingresso, occorre usare shutdown per eettuare la chiusura del lato in scrittura del socket. In questo modo il client segnaler` al server la chiusura del usso dei dati, ma potr` continuare a a a leggere quanto il server gli sta ancora inviando indietro, no a quando anchesso, riconosciuta la chiusura del socket in scrittura da parte del client, eettuer` la chiusura dalla sua parte. Solo alla a ricezione della chiusura del socket da parte del server il client potr` essere sicuro della ricezione a di tutti i dati e della terminazione eettiva della connessione. Si ` allora riportato in g. 16.22 la versione nale della nostra funzione ClientEcho, in grado e di gestire correttamente lintero usso di dati fra client e server. Il codice completo del client, comprendente la gestione delle opzioni a riga di comando e le istruzioni per la creazione della connessione, si trova nel le TCP_echo_fourth.c, distribuito coi sorgenti allegati alla guida. La nuova versione ` molto simile alla precedente di g. 16.21; la prima dierenza ` lintroe e duzione (7) della variabile eof, inizializzata ad un valore nullo, che serve a mantenere traccia dellavvenuta conclusione della lettura del le in ingresso. La seconda modica (12-15) ` stata quella di rendere subordinato ad un valore nullo di eof e limpostazione del le descriptor set per losservazione dello standard input. Se infatti il valore di eof ` non nullo signica che si ` gi` raggiunta la ne del le in ingresso ed ` pertanto inutile e e a e continuare a tenere sotto controllo lo standard input nella successiva (16) chiamata a select. Le maggiori modiche rispetto alla precedente versione sono invece nella gestione (18-22) del caso in cui la lettura con fgets restituisce un valore nullo, indice della ne del le. Questa nella precedente versione causava limmediato ritorno della funzione; in questo caso prima (19) si imposta opportunamente eof ad un valore non nullo, dopo di che (20) si eettua la chiusura del lato in scrittura del socket con shutdown. Inne (21) si usa la macro FD_CLR per togliere lo standard input dal le descriptor set. In questo modo anche se la lettura del le in ingresso ` conclusa, la funzione non esce dal e ciclo principale (11-50), ma continua ad eseguirlo ripetendo la chiamata a select per tenere sotto controllo soltanto il socket connesso, dal quale possono arrivare altri dati, che saranno letti (31), ed opportunamente trascritti (44-48) sullo standard output.

484

CAPITOLO 16. I SOCKET TCP

void ClientEcho ( FILE * filein , int socket ) { 3 char sendbuff [ MAXLINE +1] , recvbuff [ MAXLINE +1]; 4 int nread , nwrite ; 5 int maxfd ; 6 fd_set fset ; 7 int eof = 0; 8 /* initialize file descriptor set */ 9 FD_ZERO (& fset ); 10 maxfd = max ( fileno ( filein ) , socket ) + 1; 11 while (1) { 12 FD_SET ( socket , & fset ); /* set for the socket */ 13 if ( eof == 0) { 14 FD_SET ( fileno ( filein ) , & fset ); /* set for the standard input */ 15 } 16 select ( maxfd , & fset , NULL , NULL , NULL ); /* wait for read ready */ 17 if ( FD_ISSET ( fileno ( filein ) , & fset )) { /* if ready on stdin */ 18 if ( fgets ( sendbuff , MAXLINE , filein ) == NULL ) { /* if no input */ 19 eof = 1; /* EOF on input */ 20 shutdown ( socket , SHUT_WR ); /* close write half */ 21 FD_CLR ( fileno ( filein ) , & fset ); /* no more interest on stdin */ 22 } else { /* else we have to write to socket */ 23 nwrite = FullWrite ( socket , sendbuff , strlen ( sendbuff )); 24 if ( nwrite < 0) { /* on error stop */ 25 printf ( " Errore in scrittura : % s " , strerror ( errno )); 26 return ; 27 } 28 } 29 } 30 if ( FD_ISSET ( socket , & fset )) { /* if ready on socket */ 31 nread = read ( socket , recvbuff , strlen ( sendbuff )); /* do read */ 32 if ( nread < 0) { /* error condition , stop client */ 33 printf ( " Errore in lettura : % s \ n " , strerror ( errno )); 34 return ; 35 } 36 if ( nread == 0) { /* server closed connection , stop */ 37 if ( eof == 1) { 38 return ; 39 } else { 40 printf ( " EOF prematuro sul socket \ n " ); 41 return ; 42 } 43 } 44 recvbuff [ nread ] = 0; /* else read is ok , write on stdout */ 45 if ( fputs ( recvbuff , stdout ) == EOF ) { 46 perror ( " Errore in scrittura su terminale " ); 47 return ; 48 } 49 } 50 } 51 }
1 2

Figura 16.22: La sezione nel codice della versione nale della funzione ClientEcho, che usa shutdown per una conclusione corretta della connessione.

Il ritorno della funzione, e la conseguente terminazione normale del client, viene invece adesso gestito allinterno (30-49) della lettura dei dati dal socket; se infatti dalla lettura del socket si riceve una condizione di end-of-le, la si tratter` (36-43) in maniera diversa a seconda del valore a

16.6. LUSO DELLI/O MULTIPLEXING

485

di eof. Se infatti questa ` diversa da zero (37-39), essendo stata completata la lettura del le in e ingresso, vorr` dire che anche il server ha concluso la trasmissione dei dati restanti, e si potr` a a uscire senza errori, altrimenti si stamper` (40-42) un messaggio di errore per la chiusura precoce a della connessione.

16.6.4

Un server basato sullI/O multiplexing

Seguendo di nuovo le orme di Stevens in [2] vediamo ora come con lutilizzo dellI/O multiplexing diventi possibile riscrivere completamente il nostro server echo con una architettura completamente diversa, in modo da evitare di dover creare un nuovo processo tutte le volte che si ha una connessione.38 La struttura del nuovo server ` illustrata in g. 16.23, in questo caso avremo un solo processo e che ad ogni nuova connessione da parte di un client sul socket in ascolto si limiter` a registrare a lentrata in uso di un nuovo le descriptor ed utilizzer` select per rilevare la presenza di dati a in arrivo su tutti i le descriptor attivi, operando direttamente su ciascuno di essi.

Figura 16.23: Schema del nuovo server echo basato sullI/O multiplexing.

La sezione principale del codice del nuovo server ` illustrata in g. 16.24. Si ` tralasciata e e al solito la gestione delle opzioni, che ` identica alla versione precedente. Resta invariata anche e tutta la parte relativa alla gestione dei segnali, degli errori, e della cessione dei privilegi, cos` come ` identica la gestione della creazione del socket (si pu` fare riferimento al codice gi` illustrato e o a in sez. 16.4.3); al solito il codice completo del server ` disponibile coi sorgenti allegati nel le e select_echod.c. In questo caso, una volta aperto e messo in ascolto il socket, tutto quello che ci servir` sar` a a chiamare select per rilevare la presenza di nuove connessioni o di dati in arrivo, e processarli immediatamente. Per implementare lo schema mostrato in g. 16.23, il programma usa una tabella dei socket connessi mantenuta nel vettore fd_open dimensionato al valore di FD_SETSIZE, ed una variabile max_fd per registrare il valore pi` alto dei le descriptor aperti. u
38

ne faremo comunque una implementazione diversa rispetto a quella presentata da Stevens in [2].

486

CAPITOLO 16. I SOCKET TCP

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57

... memset ( fd_open , 0 , FD_SETSIZE ); /* clear array of open files */ max_fd = list_fd ; /* maximum now is listening socket */ fd_open [ max_fd ] = 1; /* main loop , wait for connection and data inside a select */ while (1) { FD_ZERO (& fset ); /* clear fd_set */ for ( i = list_fd ; i <= max_fd ; i ++) { /* initialize fd_set */ if ( fd_open [ i ] != 0) FD_SET (i , & fset ); } while ( (( n = select ( max_fd + 1 , & fset , NULL , NULL , NULL )) < 0) && ( errno == EINTR )); /* wait for data or connection */ if ( n < 0) { /* on real error exit */ PrintErr ( " select error " ); exit (1); } if ( FD_ISSET ( list_fd , & fset )) { /* if new connection */ n - -; /* decrement active */ len = sizeof ( c_addr ); /* and call accept */ if (( fd = accept ( list_fd , ( struct sockaddr *)& c_addr , & len )) < 0) { PrintErr ( " accept error " ); exit (1); } fd_open [ fd ] = 1; /* set new connection socket */ if ( max_fd < fd ) max_fd = fd ; /* if needed set new maximum */ } /* loop on open connections */ i = list_fd ; /* first socket to look */ while ( n != 0) { /* loop until active */ i ++; /* start after listening socket */ if ( fd_open [ i ] == 0) continue ; /* closed , go next */ if ( FD_ISSET (i , & fset )) { /* if active process it */ n - -; /* decrease active */ nread = read (i , buffer , MAXLINE ); /* read operations */ if ( nread < 0) { PrintErr ( " Errore in lettura " ); exit (1); } if ( nread == 0) { /* if closed connection */ close ( i ); /* close file */ fd_open [ i ] = 0; /* mark as closed in table */ if ( max_fd == i ) { /* if was the maximum */ while ( fd_open [ - - i ] == 0); /* loop down */ max_fd = i ; /* set new maximum */ break ; /* and go back to select */ } continue ; /* continue loop on open */ } nwrite = FullWrite (i , buffer , nread ); /* write data */ if ( nwrite ) { PrintErr ( " Errore in scrittura " ); exit (1); } } } } ...

Figura 16.24: La sezione principale del codice della nuova versione di server echo basati sulluso della funzione select.

16.6. LUSO DELLI/O MULTIPLEXING

487

Prima di entrare nel ciclo principale (6-56) la nostra tabella viene inizializzata (2) a zero (valore che utilizzeremo come indicazione del fatto che il relativo le descriptor non ` aperto), e mentre il valore massimo (3) per i le descriptor aperti viene impostato a quello del socket in ascolto,39 che verr` anche (4) inserito nella tabella. a La prima sezione (7-10) del ciclo principale esegue la costruzione del le descriptor set fset in base ai socket connessi in un certo momento; allinizio ci sar` soltanto il socket in ascolto, ma a nel prosieguo delle operazioni, verranno utilizzati anche tutti i socket connessi registrati nella tabella fd_open. Dato che la chiamata di select modica il valore del le descriptor set, ` e necessario ripetere (7) ogni volta il suo azzeramento, per poi procedere con il ciclo (8-10) in cui si impostano i socket trovati attivi. Per far questo si usa la caratteristica dei le descriptor, descritta in sez. 6.2.1, per cui il kernel associa sempre ad ogni nuovo le il le descriptor con il valore pi` basso disponibile. Questo fa s` u che si possa eseguire il ciclo (8) a partire da un valore minimo, che sar` sempre quello del socket a in ascolto, mantenuto in list_fd, no al valore massimo di max_fd che dovremo aver cura di tenere aggiornato. Dopo di che baster` controllare (9) nella nostra tabella se il le descriptor ` a e in uso o meno,40 e impostare fset di conseguenza. Una volta inizializzato con i socket aperti il nostro le descriptor set potremo chiamare select per fargli osservare lo stato degli stessi (in lettura, presumendo che la scrittura sia sempre consentita). Come per il precedente esempio di sez. 16.4.6, essendo questa lunica funzione che pu` bloccarsi, ed essere interrotta da un segnale, la eseguiremo (11-12) allinterno di un ciclo di o while che la ripete indenitamente qualora esca con un errore di EINTR. Nel caso invece di un errore normale si provvede (13-16) ad uscire stampando un messaggio di errore. Se invece la funzione ritorna normalmente avremo in n il numero di socket da controllare. Nello specico si danno due possibili casi diversi per cui select pu` essere ritornata: o si ` o e ricevuta una nuova connessione ed ` pronto il socket in ascolto, sul quale si pu` eseguire accept e o o c` attivit` su uno dei socket connessi, sui quali si pu` eseguire read. e a o Il primo caso viene trattato immediatamente (17-26): si controlla (17) che il socket in ascolto sia fra quelli attivi, nel qual caso anzitutto (18) se ne decrementa il numero in n; poi, inizializzata (19) la lunghezza della struttura degli indirizzi, si esegue accept per ottenere il nuovo socket connesso controllando che non ci siano errori (20-23). In questo caso non c` pi` la necessit` di e u a controllare per interruzioni dovute a segnali, in quanto siamo sicuri che accept non si bloccher`. a Per completare la trattazione occorre a questo punto aggiungere (24) il nuovo le descriptor alla tabella di quelli connessi, ed inoltre, se ` il caso, aggiornare (25) il valore massimo in max_fd. e Una volta controllato larrivo di nuove connessioni si passa a vericare se vi sono dati sui socket connessi, per questo si ripete un ciclo (29-55) ntanto che il numero di socket attivi n resta diverso da zero; in questo modo se lunico socket con attivit` era quello connesso, avendo a opportunamente decrementato il contatore, il ciclo verr` saltato, e si ritorner` immediatamente a a (ripetuta linizializzazione del le descriptor set con i nuovi valori nella tabella) alla chiamata di accept. Se il socket attivo non ` quello in ascolto, o ce ne sono comunque anche altri, il valore di e n non sar` nullo ed il controllo sar` eseguito. Prima di entrare nel ciclo comunque si inizializza a a (28) il valore della variabile i che useremo come indice nella tabella fd_open al valore minimo, corrispondente al le descriptor del socket in ascolto. Il primo passo (30) nella verica ` incrementare il valore dellindice i per posizionarsi sul e primo valore possibile per un le descriptor associato ad un eventuale socket connesso, dopo di che si controlla (31) se questo ` nella tabella dei socket connessi, chiedendo la ripetizione del e ciclo in caso contrario. Altrimenti si passa a vericare (32) se il le descriptor corrisponde ad
in quanto esso ` lunico le aperto, oltre i tre standard, e pertanto avr` il valore pi` alto. e a u si tenga presente che bench il kernel assegni sempre il primo valore libero, dato che nelle operazioni i socket e saranno aperti e chiusi in corrispondenza della creazione e conclusione delle connessioni, si potranno sempre avere dei buchi nella nostra tabella.
40 39

488

CAPITOLO 16. I SOCKET TCP

uno di quelli attivi, e nel caso si esegue (33) una lettura, uscendo con un messaggio in caso di errore (34-38). Se (39) il numero di byte letti nread ` nullo si ` in presenza del caso di un end-of-le, indice e e che una connessione che si ` chiusa, che deve essere trattato (39-48) opportunamente. Il primo e passo ` chiudere (40) anche il proprio capo del socket e rimuovere (41) il le descriptor dalla e tabella di quelli aperti, inoltre occorre vericare (42) se il le descriptor chiuso ` quello con il e valore pi` alto, nel qual caso occorre trovare (42-46) il nuovo massimo, altrimenti (47) si pu` u o ripetere il ciclo da capo per esaminare (se ne restano) ulteriori le descriptor attivi. Se per` ` stato chiuso il le descriptor pi` alto, dato che la scansione dei le descriptor o e u attivi viene fatta a partire dal valore pi` basso, questo signica che siamo anche arrivati alla ne u della scansione, per questo possiamo utilizzare direttamente il valore dellindice i con un ciclo allindietro (43) che trova il primo valore per cui la tabella presenta un le descriptor aperto, e lo imposta (44) come nuovo massimo, per poi tornare (44) al ciclo principale con un break, e rieseguire select. Se inne si sono eettivamente letti dei dati dal socket (ultimo caso rimasto) si potr` invocare a immediatamente (49) FullWrite per riscriverli indietro sul socket stesso, avendo cura di uscire con un messaggio in caso di errore (50-53). Si noti che nel ciclo si esegue una sola lettura, contrariamente a quanto fatto con la precedente versione (si riveda il codice di g. 16.18) in cui si continuava a leggere ntanto che non si riceveva un end-of-le, questo perch usando lI/O e multiplexing non si vuole essere bloccati in lettura. Luso di select ci permette di trattare automaticamente anche il caso in cui la read non ` stata in grado di leggere tutti i dati presenti e sul socket, dato che alla iterazione successiva select ritorner` immediatamente segnalando a lulteriore disponibilit`. a Il nostro server comunque sore di una vulnerabilit` per un attacco di tipo Denial of Service. a Il problema ` che in caso di blocco di una qualunque delle funzioni di I/O, non avendo usato e processi separati, tutto il server si ferma e non risponde pi` a nessuna richiesta. Abbiamo sconu giurato questa evenienza per lI/O in ingresso con luso di select, ma non vale altrettanto per lI/O in uscita. Il problema pertanto pu` sorgere qualora una delle chiamate a write eettuate o da FullWrite si blocchi. Con il funzionamento normale questo non accade in quanto il server si limita a scrivere quanto riceve in ingresso, ma qualora venga utilizzato un client malevolo che esegua solo scritture e non legga mai indietro leco del server, si potrebbe giungere alla saturazione del buer di scrittura, ed al conseguente blocco del server su di una write. Le possibili soluzioni in questo caso sono quelle di ritornare ad eseguire il ciclo di risposta alle richieste allinterno di processi separati, utilizzare un timeout per le operazioni di scrittura, o eseguire queste ultime in modalit` non bloccante, concludendo le operazioni qualora non vadano a a buon ne.

16.6.5

I/O multiplexing con poll

Finora abbiamo trattato le problematiche risolubili con lI/O multiplexing impiegando la funzione select; questo ` quello che avviene nella maggior parte dei casi, in quanto essa ` nata sotto e e BSD proprio per arontare queste problematiche con i socket. Abbiamo per` visto in sez. 11.1 o come la funzione poll possa costituire una alternativa a select, con alcuni vantaggi.41 Ancora una volta in sez. 11.1.3 abbiamo trattato la funzione in maniera generica, parlando di le descriptor, ma come per select quando si ha a che fare con dei socket il concetto di essere pronti per lI/O deve essere specicato nei dettagli, per tener conto delle condizioni della rete. Inoltre deve essere specicato come viene classicato il traco nella suddivisione fra dati normali e prioritari. In generale pertanto:
41

non sorendo delle limitazioni dovute alluso dei le descriptor set.

16.6. LUSO DELLI/O MULTIPLEXING

489

i dati inviati su un socket vengono considerati traco normale, pertanto vengono rilevati alla loro ricezione sullaltro capo da una selezione eettuata con POLLIN o POLLRDNORM; i dati urgenti out-of-band (vedi sez. 19.1.3) su un socket TCP vengono considerati traco prioritario e vengono rilevati da una condizione POLLIN, POLLPRI o POLLRDBAND. la chiusura di una connessione (cio` la ricezione di un segmento FIN) viene considerato e traco normale, pertanto viene rilevato da una condizione POLLIN o POLLRDNORM, ma una conseguente chiamata a read restituir` 0. a la disponibilit` di spazio sul socket per la scrittura di dati viene segnalata con una condia zione POLLOUT. quando uno dei due capi del socket chiude un suo lato della connessione con shutdown si riceve una condizione di POLLHUP. la presenza di un errore sul socket (sia dovuta ad un segmento RST che a timeout) viene considerata traco normale, ma viene segnalata anche dalla condizione POLLERR. la presenza di una nuova connessione su un socket in ascolto pu` essere considerata sia o traco normale che prioritario, nel caso di Linux limplementazione la classica come normale. Come esempio delluso di poll proviamo allora a reimplementare il server echo secondo lo schema di g. 16.23 usando poll al posto di select. In questo caso dovremo fare qualche modica, per tenere conto della diversa sintassi delle due funzioni, ma la struttura del programma resta sostanzialmente la stessa. In g. 16.25 ` riportata la sezione principale della nuova versione del server, la versione e completa del codice ` riportata nel le poll_echod.c dei sorgenti allegati alla guida. Al solito e nella gura si sono tralasciate la gestione delle opzioni, la creazione del socket in ascolto, la cessione dei privilegi e le operazioni necessarie a far funzionare il programma come demone, privilegiando la sezione principale del programma. Come per il precedente server basato su select il primo passo (2-8) ` quello di inizializzare e le variabili necessarie. Dato che in questo caso dovremo usare un vettore di strutture occorre anzitutto (2) allocare la memoria necessaria utilizzando il numero massimo n di socket osservabili, che viene impostato attraverso lopzione -n ed ha un valore di default di 256. Dopo di che si preimposta (3) il valore max_fd del le descriptor aperto con valore pi` u alto a quello del socket in ascolto (al momento lunico), e si provvede (4-7) ad inizializzare le strutture, disabilitando (5) losservazione con un valore negativo del campo fd ma predisponendo (6) il campo events per losservazione dei dati normali con POLLRDNORM. Inne (8) si attiva losservazione del socket in ascolto inizializzando la corrispondente struttura. Questo metodo comporta, in modalit` interattiva, lo spreco di tre strutture (quelle relative a standard input, a output ed error) che non vengono mai utilizzate in quanto la prima ` sempre quella relativa al e socket in ascolto. Una volta completata linizializzazione tutto il lavoro viene svolto allinterno del ciclo principale 10-55) che ha una struttura sostanzialmente identica a quello usato per il precedente esempio basato su select. La prima istruzione (11-12) ` quella di eseguire poll allinterno di e un ciclo che la ripete qualora venisse interrotta da un segnale, da cui si esce soltanto quando la funzione ritorna, restituendo nella variabile n il numero di le descriptor trovati attivi. Qualora invece si sia ottenuto un errore si procede (13-16) alla terminazione immediata del processo provvedendo a stampare una descrizione dello stesso. Una volta ottenuta dellattivit` su un le descriptor si hanno di nuovo due possibilit`. La a a prima possibilit` ` che ci sia attivit` sul socket in ascolto, indice di una nuova connessione, a e a

490

CAPITOLO 16. I SOCKET TCP

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56

/* initialize all needed variables */ poll_set = ( struct pollfd *) malloc ( n * sizeof ( struct pollfd )); max_fd = list_fd ; /* maximum now is listening socket */ for ( i =0; i < n ; i ++) { poll_set [ i ]. fd = -1; poll_set [ i ]. events = POLLRDNORM ; } poll_set [ max_fd ]. fd = list_fd ; /* main loop , wait for connection and data inside a select */ while (1) { while ( (( n = poll ( poll_set , max_fd + 1 , -1)) < 0) && ( errno == EINTR )); /* wait for data or connection */ if ( n < 0) { /* on real error exit */ PrintErr ( " poll error " ); exit (1); } if ( poll_set [ list_fd ]. revents & POLLRDNORM ) { /* if new connection */ n - -; /* decrement active */ len = sizeof ( c_addr ); /* and call accept */ if (( fd = accept ( list_fd , ( struct sockaddr *)& c_addr , & len )) < 0) { PrintErr ( " accept error " ); exit (1); } poll_set [ fd ]. fd = fd ; /* set new connection socket */ if ( max_fd < fd ) max_fd = fd ; /* if needed set new maximum */ } i = list_fd ; /* first socket to look */ while ( n != 0) { /* loop until active */ i ++; /* start after listening socket */ if ( poll_set [ i ]. fd == -1) continue ; /* closed , go next */ if ( poll_set [ i ]. revents & ( POLLRDNORM | POLLERR )) { n - -; /* decrease active */ nread = read (i , buffer , MAXLINE ); /* read operations */ if ( nread < 0) { PrintErr ( " Errore in lettura " ); exit (1); } if ( nread == 0) { /* if closed connection */ close ( i ); /* close file */ poll_set [ i ]. fd = -1; /* mark as closed in table */ if ( max_fd == i ) { /* if was the maximum */ while ( poll_set [ - - i ]. fd == -1); /* loop down */ max_fd = i ; /* set new maximum */ break ; /* and go back to select */ } continue ; /* continue loop on open */ } nwrite = FullWrite (i , buffer , nread ); /* write data */ if ( nwrite ) { PrintErr ( " Errore in scrittura " ); exit (1); } } } } exit (0); /* normal exit , never reached */

Figura 16.25: La sezione principale del codice della nuova versione di server echo basati sulluso della funzione poll.

16.6. LUSO DELLI/O MULTIPLEXING

491

nel qual caso si controlla (17) se il campo revents della relativa struttura ` attivo; se ` cos` si e e provvede (18) a decrementare la variabile n (che assume il signicato di numero di le descriptor attivi rimasti da controllare) per poi (19-23) eettuare la chiamata ad accept, terminando il processo in caso di errore. Se la chiamata ad accept ha successo si procede attivando (24) la struttura relativa al nuovo le descriptor da essa ottenuto, modicando (24) inne quando necessario il valore massimo dei le descriptor aperti mantenuto in max_fd. La seconda possibilit` ` che vi sia dellattivit` su uno dei socket aperti in precedenza, nel ae a qual caso si inizializza (27) lindice i del vettore delle strutture pollfd al valore del socket in ascolto, dato che gli ulteriori socket aperti avranno comunque un valore superiore. Il ciclo (28-54) prosegue ntanto che il numero di le descriptor attivi, mantenuto nella variabile n, ` diverso e da zero. Se pertanto ci sono ancora socket attivi da individuare si comincia con lincrementare (30) lindice e controllare (31) se corrisponde ad un le descriptor in uso analizzando il valore del campo fd della relativa struttura e chiudendo immediatamente il ciclo qualora non lo sia. Se invece il le descriptor ` in uso si verica (31) se c` stata attivit` controllando il campo e e a revents. Di nuovo se non si verica la presenza di attivit` il ciclo si chiude subito, altrimenti si a provveder` (32) a decrementare il numero n di le descriptor attivi da controllare e ad eseguire a (33) la lettura, ed in caso di errore (34-37) al solito lo si noticher` uscendo immediatamente. a Qualora invece si ottenga una condizione di end-of-le (38-47) si provveder` a chiudere (39) anche a il nostro capo del socket e a marcarlo (40) nella struttura ad esso associata come inutilizzato. Inne dovr` essere ricalcolato (41-45) un eventuale nuovo valore di max_fd. Lultimo passo ` a e (46) chiudere il ciclo in quanto in questo caso non c` pi` niente da riscrivere allindietro sul e u socket. Se invece si sono letti dei dati si provvede (48) ad eettuarne la riscrittura allindietro, con il solito controllo ed eventuale uscita e notica in caso si errore (49-52). Come si pu` notare la logica del programma ` identica a quella vista in g. 16.24 per lanalogo o e server basato su select; la sola dierenza signicativa ` che in questo caso non c` bisogno di e e rigenerare i le descriptor set in quanto luscita ` indipendente dai dati in ingresso. Si applicano e comunque anche a questo server le considerazioni nali di sez. 16.6.4.

492

CAPITOLO 16. I SOCKET TCP

Capitolo 17

La gestione dei socket


Esamineremo in questo capitolo una serie di funzionalit` aggiuntive relative alla gestione dei a socket, come la gestione della risoluzione di nomi e indirizzi, le impostazioni delle varie propriet` ed opzioni relative ai socket, e le funzioni di controllo che permettono di modicarne il a comportamento.

17.1

La risoluzione dei nomi

Negli esempi dei capitoli precedenti abbiamo sempre identicato le singole macchine attraverso indirizzi numerici, sfruttando al pi` le funzioni di conversione elementare illustrate in sez. 15.4 u che permettono di passare da un indirizzo espresso in forma dotted decimal ad un numero. Vedremo in questa sezione le funzioni utilizzate per poter utilizzare dei nomi simbolici al posto dei valori numerici, e viceversa quelle che permettono di ottenere i nomi simbolici associati ad indirizzi, porte o altre propriet` del sistema. a

17.1.1

La struttura del resolver

La risoluzione dei nomi ` associata tradizionalmente al servizio del Domain Name Service che e permette di identicare le macchine su internet invece che per numero IP attraverso il relativo nome a dominio.1 In realt` per DNS si intendono spesso i server che forniscono su internet questo a servizio, mentre nel nostro caso aronteremo la problematica dal lato client, di un qualunque programma che necessita di compiere questa operazione. Inoltre quella fra nomi a dominio e indirizzi IP non ` lunica corrispondenza possibile fra e nomi simbolici e valori numerici, come abbiamo visto anche in sez. 8.2.3 per le corrispondenze fra nomi di utenti e gruppi e relativi identicatori numerici; per quanto riguarda per` tutti i nomi o associati a identicativi o servizi relativi alla rete il servizio di risoluzione ` gestito in maniera e unicata da un insieme di funzioni fornite con le librerie del C, detto appunto resolver. Lo schema di funzionamento del resolver ` illustrato in g. 17.1; in sostanza i programmi e hanno a disposizione un insieme di funzioni di libreria con cui chiamano il resolver, indicate con le frecce nere. Ricevuta la richiesta ` questultimo che, sulla base della sua congurazione, esegue e le operazioni necessarie a fornire la risposta, che possono essere la lettura delle informazioni mantenute nei relativi dei le statici presenti sulla macchina, una interrogazione ad un DNS (che a sua volta, per il funzionamento del protocollo, pu` interrogarne altri) o la richiesta ad o altri server per i quali sia fornito il supporto, come LDAP.2
non staremo ad entrare nei dettagli della denizione di cosa ` un nome a dominio, dandolo per noto, una introe duzione alla problematica si trova in [14] (cap. 9) mentre per una trattazione approfondita di tutte le problematiche relative al DNS si pu` fare riferimento a [15]. o 2 la sigla LDAP fa riferimento ad un protocollo, il Lightweight Directory Access Protocol, che prevede un
1

493

494

CAPITOLO 17. LA GESTIONE DEI SOCKET

Figura 17.1: Schema di funzionamento delle funzioni del resolver.

La congurazione del resolver attiene pi` alla amministrazione di sistema che alla programu mazione, ci` non di meno, prima di trattare le varie funzioni di librerie utilizzate dai programo mi, vale la pena fare una panoramica generale. Originariamente la congurazione del resolver riguardava esclusivamente le questioni relative alla gestione dei nomi a dominio, e prevedeva solo lutilizzo del DNS e del le statico /etc/hosts. Per questo aspetto il le di congurazione principale del sistema ` /etc/resolv.conf che e contiene in sostanza lelenco degli indirizzi IP dei server DNS da contattare; a questo si aanca il le /etc/host.conf il cui scopo principale ` indicare lordine in cui eseguire la risoluzione dei e nomi (se usare prima i valori di /etc/hosts o quelli del DNS). Tralasciamo i dettagli relativi alle varie direttive che possono essere usate in questi le, che si trovano nelle rispettive pagine di manuale. Con il tempo per` ` divenuto possibile fornire diversi sostituti per lutilizzo delle associazione oe statiche in /etc/hosts, inoltre oltre alla risoluzione dei nomi a dominio ci sono anche altri nomi da risolvere, come quelli che possono essere associati ad una rete (invece che ad una singola macchina) o ai gruppi di macchine deniti dal servizio NIS,3 o come quelli dei protocolli e dei servizi che sono mantenuti nei le statici /etc/protocols e /etc/services. Molte di queste informazioni non si trovano su un DNS, ma in una rete locale pu` essere molto utile centralizzare o il mantenimento di alcune di esse su opportuni server. Inoltre luso di diversi supporti possibili per le stesse informazioni (ad esempio il nome delle macchine pu` essere mantenuto sia tramite o /etc/hosts, che con il DNS, che con NIS) comporta il problema dellordine in cui questi vengono interrogati.4 Per risolvere questa serie di problemi la risoluzione dei nomi a dominio eseguir` dal resolver ` a e stata inclusa allinterno di un meccanismo generico per la risoluzione di corrispondenze fra nomi ed informazioni ad essi associate chiamato Name Service Switch 5 cui abbiamo accennato anche
meccanismo per la gestione di elenchi di informazioni via rete; il contenuto di un elenco pu` essere assolutamente o generico, e questo permette il mantenimento dei pi` vari tipi di informazioni su una infrastruttura di questo tipo. u 3 il Network Information Service ` un servizio, creato da Sun, e poi diuso su tutte le piattaforme unix-like, che e permette di raggruppare allinterno di una rete (in quelli che appunto vengono chiamati netgroup) varie macchine, centralizzando i servizi di denizione di utenti e gruppi e di autenticazione, oggi ` sempre pi` spesso sostituito da e u LDAP. 4 con le implementazioni classiche i vari supporti erano introdotti modicando direttamente le funzioni di libreria, prevedendo un ordine di interrogazione predenito e non modicabile (a meno di una ricompilazione delle librerie stesse). 5 il sistema ` stato introdotto la prima volta nelle librerie standard di Solaris, le glibc hanno ripreso lo stesso e schema, si tenga presente che questo sistema non esiste per altre librerie standard come le libc5 o le uclib.

17.1. LA RISOLUZIONE DEI NOMI

495

in sez. 8.2.3 per quanto riguarda la gestione dei dati associati a utenti e gruppi. Il Name Service Switch (cui spesso si fa riferimento con lacronimo NSS) ` un sistema di librerie dinamiche che e permette di denire in maniera generica sia i supporti su cui mantenere i dati di corrispondenza fra nomi e valori numerici, sia lordine in cui eettuare le ricerche sui vari supporti disponibili. Il sistema prevede una serie di possibili classi di corrispondenza, quelle attualmente denite sono riportate in tab. 17.1.
Classe passwd shadow group aliases ethers hosts netgroup networks protocols rpc publickey services Tipo di corrispondenza Corrispondenze fra nome dellutente e relative propriet` a (uid, gruppo principale, ecc.). Corrispondenze fra username e password dellutente (e altre informazioni relative alle password). Corrispondenze fra nome del gruppo e propriet` dello a stesso. Alias per la posta elettronica. Corrispondenze fra numero IP e MAC address della scheda di rete. Corrispondenze fra nome a dominio e numero IP. Corrispondenze fra gruppo di rete e macchine che lo compongono. Corrispondenze fra nome di una rete e suo indirizzo IP. Corrispondenze fra nome di un protocollo e relativo numero identicativo. Corrispondenze fra nome di un servizio RPC e relativo numero identicativo. Chiavi pubbliche e private usate per gli RFC sicuri, utilizzate da NFS e NIS+. Corrispondenze fra nome di un servizio e numero di porta.

Tabella 17.1: Le diverse classi di corrispondenze denite allinterno del Name Service Switch.

Il sistema del Name Service Switch ` controllato dal contenuto del le /etc/nsswitch.conf; e 6 di congurazione per ciascuna di queste classi, che viene inizia col questo contiene una riga nome di tab. 17.1 seguito da un carattere : e prosegue con la lista dei servizi su cui le relative informazioni sono raggiungibili, scritti nellordine in cui si vuole siano interrogati. Ogni servizio ` specicato a sua volta da un nome, come file, dns, db, ecc. che identica e la libreria dinamica che realizza linterfaccia con esso. Per ciascun servizio se NAME ` il nome e utilizzato dentro /etc/nsswitch.conf, dovr` essere presente (usualmente in /lib) una libreria a libnss_NAME che ne implementa le funzioni. In ogni caso, qualunque sia la modalit` con cui ricevono i dati o il supporto su cui vengono a mantenuti, e che si usino o meno funzionalit` aggiuntive fornire dal sistema del Name Service a Switch, dal punto di vista di un programma che deve eettuare la risoluzione di un nome a dominio, tutto quello che conta sono le funzioni classiche che il resolver mette a disposizione,7 e sono queste quelle che tratteremo nelle sezioni successive.

17.1.2

Le funzioni di interrogazione del resolver

Prima di trattare le funzioni usate normalmente nella risoluzione dei nomi a dominio conviene trattare in maniera pi` dettagliata il meccanismo principale da esse utilizzato e cio` quello del u e servizio DNS. Come accennato questo, bench in teoria sia solo uno dei possibili supporti su e cui mantenere le informazioni, in pratica costituisce il meccanismo principale con cui vengono
seguendo una convezione comune per i le di congurazione le righe vuote vengono ignorate e tutto quello che segue un carattere # viene considerato un commento. 7 ` cura della implementazione fattane nelle glibc tenere conto della presenza del Name Service Switch. e
6

496

CAPITOLO 17. LA GESTIONE DEI SOCKET

risolti i nomi a dominio. Per questo motivo esistono una serie di funzioni di libreria che servono specicamente ad eseguire delle interrogazioni verso un server DNS, funzioni che poi vengono utilizzate per realizzare le funzioni generiche di libreria usate anche dal sistema del resolver. Il sistema del DNS ` in sostanza di un database distribuito organizzato in maniera gerarchica, e i dati vengono mantenuti in tanti server distinti ciascuno dei quali si occupa della risoluzione del proprio dominio; i nomi a dominio sono organizzati in una struttura ad albero analoga a quella dellalbero dei le, con domini di primo livello (come i .org), secondo livello (come .truelite.it), ecc. In questo caso le separazioni sono fra i vari livelli sono denite dal carattere . ed i nomi devono essere risolti da destra verso sinistra.8 Il meccanismo funziona con il criterio della delegazione, un server responsabile per un dominio di primo livello pu` delegare o la risoluzione degli indirizzi per un suo dominio di secondo livello ad un altro server, il quale a sua volta potr` delegare la risoluzione di un eventuale sottodominio di terzo livello ad un altro a server ancora. In realt` un server DNS ` in grado di fare altro rispetto alla risoluzione di un nome a dominio a e in un indirizzo IP; ciascuna voce nel database viene chiamata resource record, e pu` contenere o diverse informazioni. In genere i resource record vengono classicati per la classe di indirizzi cui i dati contenuti fanno riferimento, e per il tipo di questi ultimi.9 Oggigiorno i dati mantenuti nei server DNS sono quasi esclusivamente relativi ad indirizzi internet, per cui in pratica viene utilizzata soltanto una classe di indirizzi; invece le corrispondenze fra un nome a dominio ed un indirizzo IP sono solo uno fra i vari tipi di informazione che un server DNS fornisce normalmente. Lesistenza di vari tipi di informazioni ` un altro dei motivi per cui il resolver prevede, e rispetto a quelle relative alla semplice risoluzione dei nomi, un insieme di funzioni speciche dedicate allinterrogazione di un server DNS; la prima di queste funzioni ` res_init, il cui e prototipo `: e
#include <netinet/in.h> #include <arpa/nameser.h> #include <resolv.h> int res_init(void) Inizializza il sistema del resolver. La funzione restituisce 0 in caso di successo e -1 in caso di errore.

La funzione legge il contenuto dei le di congurazione (i gi` citati resolv.conf e host.conf) a per impostare il dominio di default, gli indirizzi dei server DNS da contattare e lordine delle ricerche; se non sono specicati server verr` utilizzato lindirizzo locale, e se non ` denito a e un dominio di default sar` usato quello associato con lindirizzo locale (ma questo pu` essere a o sovrascritto con luso della variabile di ambiente LOCALDOMAIN). In genere non ` necessario e eseguire questa funzione direttamente in quanto viene automaticamente chiamata la prima volta che si esegue una delle altre. Le impostazioni e lo stato del resolver vengono mantenuti in una serie di variabili raggruppate nei campi di una apposita struttura _res usata da tutte queste funzioni. Essa viene denita in resolv.h ed ` utilizzata internamente alle funzioni essendo denita come variabile globale; e questo consente anche di accedervi direttamente allinterno di un qualunque programma, una volta che la sia opportunamente dichiarata come: extern struct state _res ; Tutti i campi della struttura sono ad uso interno, e vengono usualmente inizializzati da res_init in base al contenuto dei le di congurazione e ad una serie di valori di default. Lunico
per chi si stia chiedendo quale sia la radice di questo albero, cio` lequivalente di /, la risposta ` il dominio e e speciale ., che in genere non viene mai scritto esplicitamente, ma che, come chiunque abbia congurato un server DNS sa bene, esiste ed ` gestito dai cosiddetti root DNS che risolvono i domini di primo livello. e 9 ritroveremo classi di indirizzi e tipi di record pi` avanti in tab. 17.3 e tab. 17.4. u
8

17.1. LA RISOLUZIONE DEI NOMI

497

campo che pu` essere utile modicare ` _res.options, una maschera binaria che contiene una o e serie di bit di opzione che permettono di controllare il comportamento del resolver.
Costante RES_INIT RES_DEBUG RES_AAONLY RES_USEVC RES_PRIMARY RES_IGNTC RES_RECURSE RES_DEFNAMES RES_STAYOPEN RES_DNSRCH Signicato Viene attivato se ` stata chiamata res_init. e Stampa dei messaggi di debug. Accetta solo risposte autoritative. Usa connessioni TCP per contattare i server invece che lusuale UDP. Interroga soltanto server DNS primari. Ignora gli errori di troncamento, non ritenta la richiesta con una connessione TCP. Imposta il bit che indica che si desidera eseguire una interrogazione ricorsiva. Se attivo res_search aggiunge il nome del dominio di default ai nomi singoli (che non contengono cio` un .). e Usato con RES_USEVC per mantenere aperte le connessioni TCP fra interrogazioni diverse. Se attivo res_search esegue le ricerche di nomi di macchine nel dominio corrente o nei domini ad esso sovrastanti. Blocca i controlli di sicurezza di tipo 1. Blocca i controlli di sicurezza di tipo 2. Blocca luso della variabile di ambiente HOSTALIASES. Restituisce indirizzi IPv6 con gethostbyname. Ruota la lista dei server DNS dopo ogni interrogazione. Non controlla i nomi per vericarne la correttezza sintattica. Non elimina i record di tipo TSIG. Eettua un blast inviando simultaneamente le richieste a tutti i server; non ancora implementata. Combinazione di RES_RECURSE, RES_DEFNAMES e RES_DNSRCH.

RES_INSECURE1 RES_INSECURE2 RES_NOALIASES RES_USE_INET6 RES_ROTATE RES_NOCHECKNAME RES_KEEPTSIG RES_BLAST RES_DEFAULT

Tabella 17.2: Costanti utilizzabili come valori per _res.options.

Per utilizzare questa funzionalit` per modicare le impostazioni direttamente da programma a occorrer` impostare un opportuno valore per questo campo ed invocare esplicitamente res_init, a dopo di che le altre funzioni prenderanno le nuove impostazioni. Le costanti che deniscono i vari bit di questo campo, ed il relativo signicato sono illustrate in tab. 17.2; trattandosi di una maschera binaria un valore deve essere espresso con un opportuno OR aritmetico di dette costanti; ad esempio il valore di default delle opzioni, espresso dalla costante RES_DEFAULT, ` e denito come: # define RES_DEFAULT ( RES_RECURSE | RES_DEFNAMES | RES_DNSRCH )

Non tratteremo il signicato degli altri campi non essendovi necessit` di modicarli direttaa mente; gran parte di essi sono infatti impostati dal contenuto dei le di congurazione, mentre le funzionalit` controllate da alcuni di esse possono essere modicate con luso delle opportune a variabili di ambiente come abbiamo visto per LOCALDOMAIN. In particolare con RES_RETRY si soprassiede il valore del campo retry che controlla quante volte viene ripetuto il tentativo di connettersi ad un server DNS prima di dichiarare fallimento; il valore di default ` 4, un valore e nullo signica bloccare luso del DNS. Inne con RES_TIMEOUT si soprassiede il valore del campo retrans,10 che ` il valore preso come base (in numero di secondi) per denire la scadenza di una e richiesta, ciascun tentativo di richiesta fallito viene ripetuto raddoppiando il tempo di scadenza per il numero massimo di volte stabilito da RES_RETRY.
10

preimpostato al valore della omonima costante RES_TIMEOUT di resolv.h.

498

CAPITOLO 17. LA GESTIONE DEI SOCKET

La funzione di interrogazione principale ` res_query, che serve ad eseguire una richiesta ad e un server DNS per un nome a dominio completamente specicato (quello che si chiama FQDN, Fully Qualied Domain Name); il suo prototipo `: e
#include <netinet/in.h> #include <arpa/nameser.h> #include <resolv.h> int res_query(const char *dname, int class, int type, unsigned char *answer, int anslen) Esegue una interrogazione al DNS. La funzione restituisce un valore positivo pari alla lunghezza dei dati scritti nel buer answer in caso di successo e -1 in caso di errore.

La funzione esegue una interrogazione ad un server DNS relativa al nome da risolvere passato nella stringa indirizzata da dname, inoltre deve essere specicata la classe di indirizzi in cui eseguire la ricerca con class, ed il tipo di resource record che si vuole ottenere con type. Il risultato della ricerca verr` scritto nel buer di lunghezza anslen puntato da answer che si sar` a a opportunamente allocato in precedenza. Una seconda funzione di ricerca, analoga a res_query, che prende gli stessi argomenti, ma che esegue linterrogazione con le funzionalit` addizionali previste dalle due opzioni RES_DEFNAMES a e RES_DNSRCH, ` res_search, il cui prototipo `: e e
#include <netinet/in.h> #include <arpa/nameser.h> #include <resolv.h> int res_search(const char *dname, int class, int type, unsigned char *answer, int anslen) Esegue una interrogazione al DNS. La funzione restituisce un valore positivo pari alla lunghezza dei dati scritti nel buer answer in caso di successo e -1 in caso di errore.

In sostanza la funzione ripete una serie di chiamate a res_query aggiungendo al nome contenuto nella stringa dname il dominio di default da cercare, fermandosi non appena trova un risultato. Il risultato di entrambe le funzioni viene scritto nel formato opportuno (che sar` a diverso a seconda del tipo di record richiesto) nel buer di ritorno; sar` compito del programma a (o di altre funzioni) estrarre i relativi dati, esistono una serie di funzioni interne usate per la scansione di questi dati, per chi fosse interessato una trattazione dettagliata ` riportata nel e quattordicesimo capitolo di [15]. Le classi di indirizzi supportate da un server DNS sono tre, ma di queste in pratica oggi viene utilizzata soltanto quella degli indirizzi internet; le costanti che identicano dette classi, da usare come valore per largomento class delle precedenti funzioni, sono riportate in tab. 17.3.11
Costante C_IN C_HS C_CHAOS C_ANY Signicato Indirizzi internet, in pratica i soli utilizzati oggi. Indirizzi Hesiod, utilizzati solo al MIT, oggi completamente estinti. Indirizzi per la rete Chaosnet, unaltra rete sperimentale nata al MIT. Indica un indirizzo di classe qualunque.

Tabella 17.3: Costanti identicative delle classi di indirizzi per largomento class di res_query.

Come accennato le tipologie di dati che sono mantenibili su un server DNS sono diverse, ed a ciascuna di essa corrisponde un diverso tipo di resource record. Lelenco delle costanti12 che
11 12

esisteva in realt` anche una classe C_CSNET per la omonima rete, ma ` stata dichiarata obsoleta. a e ripreso dai le di dichiarazione arpa/nameser.h e arpa/nameser_compat.h.

17.1. LA RISOLUZIONE DEI NOMI

499

deniscono i valori che si possono usare per largomento type per specicare il tipo di resource record da richiedere ` riportato in tab. 17.4; le costanti (tolto il T_ iniziale) hanno gli stessi nomi e usati per identicare i record nei le di zona di BIND,13 e che normalmente sono anche usati come nomi per indicare i record.
Costante T_A T_NS T_MD T_MF T_CNAME T_SOA T_MB T_MG T_MR T_NULL T_WKS T_PTR T_HINFO T_MINFO T_MX T_TXT T_RP T_AFSDB T_X25 T_ISDN T_RT T_NSAP T_NSAP_PTR T_SIG T_KEY T_PX T_GPOS T_AAAA T_LOC T_NXT T_EID T_NIMLOC T_SRV T_ATMA T_NAPTR T_TSIG T_IXFR T_AXFR T_MAILB T_MAILA T_ANY Signicato Indirizzo di una stazione. Server DNS autoritativo per il dominio richiesto. Destinazione per la posta elettronica. Redistributore per la posta elettronica. Nome canonico. Inizio di una zona di autorit`. a Nome a dominio di una casella di posta. Nome di un membro di un gruppo di posta. Nome di un cambiamento di nome per la posta. Record nullo. Servizio noto. Risoluzione inversa di un indirizzo numerico. Informazione sulla stazione. Informazione sulla casella di posta. Server cui instradare la posta per il dominio. Stringhe di testo (libere). Nome di un responsabile (responsible person). Database per una cella AFS. Indirizzo di chiamata per X.25. Indirizzo di chiamata per ISDN. Router. Indirizzo NSAP. Risoluzione inversa per NSAP (deprecato). Firma digitale di sicurezza. Chiave per rma. Corrispondenza per la posta X.400. Posizione geograca. Indirizzo IPv6. Informazione di collocazione. Dominio successivo. Identicatore di punto conclusivo. Posizionatore nimrod. Servizio. Indirizzo ATM. Puntatore ad una naming authority. Firma di transazione. Trasferimento di zona incrementale. Trasferimento di zona di autorit`. a Trasferimento di record di caselle di posta. Trasferimento di record di server di posta. Valore generico.

Tabella 17.4: Costanti identicative del tipo di record per largomento type di res_query.

Lelenco di tab. 17.4 ` quello di tutti i resource record deniti, con una breve descrizione del e relativo signicato. Di tutti questi per` viene impiegato correntemente solo un piccolo sottoino sieme, alcuni sono obsoleti ed altri fanno riferimento a dati applicativi che non ci interessano non avendo nulla a che fare con la risoluzione degli indirizzi IP, pertanto non entreremo nei dettagli
BIND, acronimo di Berkley Internet Name Domain, ` una implementazione di un server DNS, ed, essendo e utilizzata nella stragrande maggioranza dei casi, fa da riferimento; i dati relativi ad un certo dominio (cio` i suoi e resource record vengono mantenuti in quelli che sono usualmente chiamati le di zona, e in essi ciascun tipo di dominio ` identicato da un nome che ` appunto identico a quello delle costanti di tab. 17.4 senza il T_ iniziale. e e
13

500

CAPITOLO 17. LA GESTIONE DEI SOCKET

del signicato di tutti i resource record, ma solo di quelli usati dalle funzioni del resolver. Questi sono sostanzialmente i seguenti (per indicarli si ` usata la notazione dei le di zona di BIND): e A AAAA viene usato per indicare la corrispondenza fra un nome a dominio ed un indirizzo IPv4; ad esempio la corrispondenza fra dodds.truelite.it e lindirizzo IP 62.48.34.25. viene usato per indicare la corrispondenza fra un nome a dominio ed un indirizzo IPv6; ` chiamato in questo modo dato che la dimensione di un indirizzo IPv6 ` quattro volte e e quella di un indirizzo IPv4. per fornire la corrispondenza inversa fra un indirizzo IP ed un nome a dominio ad esso associato si utilizza questo tipo di record (il cui nome sta per pointer ).

PTR

CNAME qualora si abbiamo pi` nomi che corrispondono allo stesso indirizzo (come ad esempio u www.truelite.it e sources.truelite.it, che fanno entrambi riferimento alla stessa macchina (nel caso dodds.truelite.it) si pu` usare questo tipo di record per creao re degli alias in modo da associare un qualunque altro nome al nome canonico della macchina (si chiama cos` quello associato al record A). Come accennato in caso di successo le due funzioni di richiesta restituiscono il risultato della interrogazione al server, in caso di insuccesso lerrore invece viene segnalato da un valore di ritorno pari a -1, ma in questo caso, non pu` essere utilizzata la variabile errno per riportare o un codice di errore, in quanto questo viene impostato per ciascuna delle chiamate al sistema utilizzate dalle funzioni del resolver, non avr` alcun signicato nellindicare quale parte del a procedimento di risoluzione ` fallita. e Per questo motivo ` stata denita una variabile di errore separata, h_errno, che viene utie lizzata dalle funzioni del resolver per indicare quale problema ha causato il fallimento della risoluzione del nome. Ad essa si pu` accedere una volta che la si dichiara con: o extern int h_errno ; ed i valori che pu` assumere, con il relativo signicato, sono riportati in tab. 17.5. o
Costante HOST_NOT_FOUND NO_ADDRESS NO_RECOVERY TRY_AGAIN Signicato Lindirizzo richiesto non ` valido e la macchina indicata ` sconosciuta. e e Il nome a dominio richiesto ` valido, ma non ha un indirizzo associato ad esso e (alternativamente pu` essere indicato come NO_DATA). o Si ` avuto un errore non recuperabile nellinterrogazione di un server DNS. e Si ` avuto un errore temporaneo nellinterrogazione di un server DNS, si pu` e o ritentare linterrogazione in un secondo tempo. Tabella 17.5: Valori possibili della variabile h_errno.

Insieme alla nuova variabile vengono denite anche due nuove funzioni per stampare lerrore a video, analoghe a quelle di sez. 8.5.2 per errno, ma che usano il valore di h_errno; la prima ` herror ed il suo prototipo `: e e
#include <netdb.h> void herror(const char *string) Stampa un errore di risoluzione.

La funzione ` lanaloga di perror e stampa sullo standard error un messaggio di errore e corrispondente al valore corrente di h_errno, a cui viene anteposta la stringa string passata come argomento. La seconda funzione ` hstrerror ed il suo prototipo `: e e
#include <netdb.h> const char *hstrerror(int err) Restituisce una stringa corrispondente ad un errore di risoluzione.

17.1. LA RISOLUZIONE DEI NOMI

501

che, come lanaloga strerror, restituisce una stringa con un messaggio di errore gi` formattato, a corrispondente al codice passato come argomento (che si presume sia dato da h_errno).

17.1.3

La risoluzione dei nomi a dominio

La principale funzionalit` del resolver resta quella di risolvere i nomi a dominio in indirizzi a IP, per cui non ci dedicheremo oltre alle funzioni di richiesta generica ed esamineremo invece le funzioni a questo dedicate. La prima funzione ` gethostbyname il cui scopo ` ottenere lindirizzo e e di una stazione noto il suo nome a dominio, il suo prototipo `: e
#include <netdb.h> struct hostent *gethostbyname(const char *name) Determina lindirizzo associato al nome a dominio name. La funzione restituisce in caso di successo il puntatore ad una struttura di tipo hostent contenente i dati associati al nome a dominio, o un puntatore nullo in caso di errore.

La funzione prende come argomento una stringa name contenente il nome a dominio che si vuole risolvere, in caso di successo i dati ad esso relativi vengono memorizzati in una opportuna struttura hostent la cui denizione ` riportata in g. 17.2. e
struct hostent { char * h_name ; char ** h_aliases ; int h_addrtype ; int h_length ; char ** h_addr_list ; } # define h_addr h_addr_list [0]

/* /* /* /* /*

official name of host */ alias list */ host address type */ length of address */ list of addresses */

/* for backward compatibility */

Figura 17.2: La struttura hostent per la risoluzione dei nomi a dominio e degli indirizzi IP.

Quando un programma chiama gethostbyname e questa usa il DNS per eettuare la risoluzione del nome, ` con i valori contenuti nei relativi record che vengono riempite le varie parti e della struttura hostent. Il primo campo della struttura, h_name contiene sempre il nome canonico, che nel caso del DNS ` appunto il nome associato ad un record A. Il secondo campo della e struttura, h_aliases, invece ` un puntatore ad vettore di puntatori, terminato da un puntatore e nullo. Ciascun puntatore del vettore punta ad una stringa contenente uno degli altri possibili nomi associati allo stesso nome canonico (quelli che nel DNS vengono inseriti come record di tipo CNAME). Il terzo campo della struttura, h_addrtype, indica il tipo di indirizzo che ` stato restituito, e e pu` assumere soltanto i valori AF_INET o AF_INET6, mentre il quarto campo, h_length, indica o la lunghezza dellindirizzo stesso in byte. Inne il campo h_addr_list ` il puntatore ad un vettore di puntatori ai singoli indirizzi; il e vettore ` terminato da un puntatore nullo. Inoltre, come illustrato in g. 17.2, viene denito il e campo h_addr come sinonimo di h_addr_list[0], cio` un riferimento diretto al primo indirizzo e della lista. Oltre ai normali nomi a dominio la funzione accetta come argomento name anche indirizzi numerici, in formato dotted decimal per IPv4 o con la notazione illustrata in sez. A.2.5 per IPv6. In tal caso gethostbyname non eseguir` nessuna interrogazione remota, ma si limiter` a copiare a a la stringa nel campo h_name ed a creare la corrispondente struttura in_addr da indirizzare con h_addr_list[0]. Con luso di gethostbyname normalmente si ottengono solo gli indirizzi IPv4, se si vogliono ottenere degli indirizzi IPv6 occorrer` prima impostare lopzione RES_USE_INET6 nel campo a

502

CAPITOLO 17. LA GESTIONE DEI SOCKET

_res.options e poi chiamare res_init (vedi sez. 17.1.2) per modicare le opzioni del resolver ; dato che questo non ` molto comodo ` stata denita14 unaltra funzione, gethostbyname2, il cui e e prototipo `: e
#include <netdb.h> #include <sys/socket.h> struct hostent *gethostbyname2(const char *name, int af) Determina lindirizzo di tipo af associato al nome a dominio name. La funzione restituisce in caso di successo il puntatore ad una struttura di tipo hostent contenente i dati associati al nome a dominio, o un puntatore nullo in caso di errore.

In questo caso la funzione prende un secondo argomento af che indica (i soli valori consentiti sono AF_INET o AF_INET6, per questo ` necessario luso di sys/socket.h) la famiglia di indirizzi e che dovr` essere utilizzata nei risultati restituiti dalla funzione. Per tutto il resto la funzione ` a e identica a gethostbyname, ed identici sono i suoi risultati. Vediamo allora un primo esempio delluso delle funzioni di risoluzione, in g. 17.3 ` riportato e un estratto del codice di un programma che esegue una semplice interrogazione al resolver usando gethostbyname e poi ne stampa a video i risultati. Al solito il sorgente completo, che comprende il trattamento delle opzioni ed una funzione per stampare un messaggio di aiuto, ` e nel le mygethost.c dei sorgenti allegati alla guida. Il programma richiede un solo argomento che specichi il nome da cercare, senza il quale (15-18) esce con un errore. Dopo di che (20) si limita a chiamare gethostbyname, ricevendo il risultato nel puntatore data. Questo (21-24) viene controllato per rilevare eventuali errori, nel qual caso il programma esce dopo aver stampato un messaggio con herror. Se invece la risoluzione ` andata a buon ne si inizia (25) con lo stampare il nome canonico, e dopo di che (26-30) si stampano eventuali altri nomi. Per questo prima (26) si prende il puntatore alla cima della lista che contiene i nomi e poi (27-30) si esegue un ciclo che sar` ripetuto n a 15 per le stringhe dei nomi; prima (28) si tanto che nella lista si troveranno dei puntatori validi stamper` la stringa e poi (29) si provveder` ad incrementare il puntatore per passare al successivo a a elemento della lista. Una volta stampati i nomi si passer` a stampare gli indirizzi, il primo passo (31-38) ` allora a e quello di riconoscere il tipo di indirizzo sulla base del valore del campo h_addrtype, stampandolo a video. Si ` anche previsto di stampare un errore nel caso (che non dovrebbe mai accadere) di e un indirizzo non valido. Inne (39-44) si stamperanno i valori degli indirizzi, di nuovo (39) si inizializzer` un puntatore a alla cima della lista e si eseguir` un ciclo ntanto che questo punter` ad indirizzi validi in maniera a a analoga a quanto fatto in precedenza per i nomi a dominio. Si noti come, essendo il campo h_addr_list un puntatore ad strutture di indirizzi generiche, questo sia ancora di tipo char ** e si possa riutilizzare lo stesso puntatore usato per i nomi. Per ciascun indirizzo valido si provveder` (41) ad una conversione con la funzione inet_ntop a (vedi sez. 15.4) passandole gli opportuni argomenti, questa restituir` la stringa da stampare (42) a con il valore dellindirizzo in buffer, che si ` avuto la cura di dichiarare inizialmente (10) con e dimensioni adeguate; dato che la funzione ` in grado di tenere conto automaticamente del tipo e di indirizzo non ci sono precauzioni particolari da prendere.16 Le funzioni illustrate nora hanno un difetto: utilizzando una area di memoria interna per allocare i contenuti della struttura hostent non possono essere rientranti. Questo comporta anche che in due successive chiamate i dati potranno essere sovrascritti. Si tenga presente poi
questa ` una estensione fornita dalle glibc, disponibile anche in altri sistemi unix-like. e si ricordi che la lista viene terminata da un puntatore nullo. 16 volendo essere pignoli si dovrebbe controllarne lo stato di uscita, lo si ` tralasciato per non appesantire il e codice, dato che in caso di indirizzi non validi si sarebbe avuto un errore con gethostbyname, ma si ricordi che la sicurezza non ` mai troppa. e
15 14

17.1. LA RISOLUZIONE DEI NOMI

503

int main ( int argc , char * argv []) { 3 /* 4 * Variables definition 5 */ 6 int i ; 7 struct hostent * data ; 8 char ** alias ; 9 char * addr ; 10 char buffer [ INET6_ADDRSTRLEN ]; 11 ... 12 /* 13 * Main Body 14 */ 15 if (( argc - optind ) != 1) { 16 printf ( " Wrong number of arguments % d \ n " , argc - optind ); 17 usage (); 18 } 19 /* get resolution */ 20 data = gethostbyname ( argv [1]); 21 if ( data == NULL ) { 22 herror ( " Errore di risoluzione " ); 23 exit (1); 24 } 25 printf ( " Canonical name % s \ n " , data - > h_name ); 26 alias = data - > h_aliases ; 27 while (* alias != NULL ) { 28 printf ( " Alias % s \ n " , * alias ); 29 alias ++; 30 } 31 if ( data - > h_addrtype == AF_INET ) { 32 printf ( " Address are IPv4 \ n " ); 33 } else if ( data - > h_addrtype == AF_INET6 ) { 34 printf ( " Address are IPv6 \ n " ); 35 } else { 36 printf ( " Tipo di indirizzo non valido \ n " ); 37 exit (1); 38 } 39 alias = data - > h_addr_list ; 40 while (* alias != NULL ) { 41 addr = inet_ntop ( data - > h_addrtype , * alias , buffer , sizeof ( buffer )); 42 printf ( " Indirizzo % s \ n " , addr ); 43 alias ++; 44 } 45 exit (0); 46 }
1 2

Figura 17.3: Esempio di codice per la risoluzione di un indirizzo.

che copiare il contenuto della sola struttura non ` suciente per salvare tutti i dati, in quanto e questa contiene puntatori ad altri dati, che pure possono essere sovrascritti; per questo motivo, se si vuole salvare il risultato di una chiamata, occorrer` eseguire quella che si chiama una deep a 17 copy.
si chiama cos` quella tecnica per cui, quando si deve copiare il contenuto di una struttura complessa (con puntatori che puntano ad altri dati, che a loro volta possono essere puntatori ad altri dati) si deve copiare non solo il contenuto della struttura, ma eseguire una scansione per risolvere anche tutti i puntatori contenuti in essa (e cos` via se vi sono altre sottostrutture con altri puntatori) e copiare anche i dati da questi referenziati.
17

504

CAPITOLO 17. LA GESTIONE DEI SOCKET

Per ovviare a questi problemi nelle glibc sono denite anche delle versioni rientranti delle precedenti funzioni, al solito queste sono caratterizzate dallavere un susso _r, pertanto avremo le due funzioni gethostbyname_r e gethostbyname2_r i cui prototipi sono:
#include <netdb.h> #include <sys/socket.h> int gethostbyname_r(const char *name, struct hostent *ret, char *buf, size_t buflen, struct hostent **result, int *h_errnop) int gethostbyname2_r(const char *name, int af, struct hostent *ret, char *buf, size_t buflen, struct hostent **result, int *h_errnop) Versioni rientranti delle funzioni gethostbyname e gethostbyname2. Le funzioni restituiscono 0 in caso di successo ed un valore negativo in caso di errore.

Gli argomenti name (e af per gethostbyname2_r) hanno lo stesso signicato visto in precedenza. Tutti gli altri argomenti hanno lo stesso signicato per entrambe le funzioni. Per evitare luso di variabili globali si dovr` allocare preventivamente una struttura hostent in cui ricevere a il risultato, passandone lindirizzo alla funzione nellargomento ret. Inoltre, dato che hostent contiene dei puntatori, dovr` essere allocato anche un buer in cui le funzioni possano scrivere a tutti i dati del risultato dellinterrogazione da questi puntati; lindirizzo e la lunghezza di questo buer devono essere indicati con gli argomenti buf e buflen. Gli ultimi due argomenti vengono utilizzati per avere indietro i risultati come value result argument, si deve specicare lindirizzo della variabile su cui la funzione dovr` salvare il codice a di errore con h_errnop e quello su cui dovr` salvare il puntatore che si user` per accedere i dati a a con result. In caso di successo entrambe le funzioni restituiscono un valore nullo, altrimenti restituiscono un codice di errore negativo e allindirizzo puntato da result sar` salvato un puntatore nullo, a mentre a quello puntato da h_errnop sar` salvato il valore del codice di errore, dato che per a essere rientrante la funzione non pu` la variabile globale h_errno. In questo caso il codice di o errore, oltre ai valori di tab. 17.5, pu` avere anche quello di ERANGE qualora il buer allocato su o buf non sia suciente a contenere i dati, in tal caso si dovr` semplicemente ripetere lesecuzione a della funzione con un buer di dimensione maggiore. Una delle caratteristiche delle interrogazioni al servizio DNS ` che queste sono normalmente e eseguite con il protocollo UDP, ci sono casi in cui si preferisce che vengano usate connessioni permanenti con il protocollo TCP. Per ottenere questo18 sono previste delle funzioni apposite; la prima ` sethostent, il cui prototipo `: e e
#include <netdb.h> void sethostent(int stayopen) Richiede luso di connessioni per le interrogazioni ad un server DNS. La funzione non restituisce nulla.

La funzione permette di richiedere luso di connessioni TCP per la richiesta dei dati, e che queste restino aperte per successive richieste. Il valore dellargomento stayopen indica se attivare questa funzionalit`, un valore pari a 1 (o diverso da zero), che indica una condizione vera in C, a attiva la funzionalit`. Come si attiva luso delle connessioni TCP lo si pu` disattivare con la a o funzione endhostent; il suo prototipo `: e
#include <netdb.h> void endhostent(void) Disattiva luso di connessioni per le interrogazioni ad un server DNS. La funzione non restituisce nulla.

e come si pu` vedere la funzione ` estremamente semplice, non richiedendo nessun argomento. o e
si potrebbero impostare direttamente le opzioni di __res.options, ma queste funzioni permettono di semplicare la procedura.
18

17.1. LA RISOLUZIONE DEI NOMI

505

Inne si pu` richiedere la risoluzione inversa di un indirizzo IP od IPv6, per ottenerne il o nome a dominio ad esso associato, per fare questo si pu` usare la funzione gethostbyaddr, il o cui prototipo `: e
#include <netdb.h> #include <sys/socket.h> struct hostent *gethostbyaddr(const char *addr, int len, int type) Richiede la risoluzione inversa di un indirizzo IP. La funzione restituisce lindirizzo ad una struttura hostent in caso di successo ed NULL in caso di errore.

In questo caso largomento addr dovr` essere il puntatore ad una appropriata struttura a contenente il valore dellindirizzo IP (o IPv6) che si vuole risolvere. Luso del tipo char * per questo argomento ` storico, il dato dovr` essere fornito in una struttura in_addr19 per un e a indirizzo IPv4 ed una struttura in6_addr per un indirizzo IPv6, mentre in len se ne dovr` a specicare la dimensione (rispettivamente 4 o 16), inne largomento type indica il tipo di indirizzo e dovr` essere o AF_INET o AF_INET6. a La funzione restituisce, in caso di successo, un puntatore ad una struttura hostent, solo che in questo caso la ricerca viene eseguita richiedendo al DNS un record di tipo PTR corrispondente allindirizzo specicato. In caso di errore al solito viene usata la variabile h_errno per restituire un opportuno codice. In questo caso lunico campo del risultato che interessa ` h_name che e conterr` il nome a dominio, la funziona comunque inizializza anche il primo campo della lista a h_addr_list col valore dellindirizzo passato come argomento. Per risolvere il problema delluso da parte delle due funzioni gethostbyname e gethostbyaddr di memoria statica che pu` essere sovrascritta fra due chiamate successive, e per avere sempre o la possibilit` di indicare esplicitamente il tipo di indirizzi voluto (cosa che non ` possibile con a e 20 getipnodebyname e gethostbyname), vennero introdotte due nuove funzioni di risoluzione, getipnodebyaddr, i cui prototipi sono:
#include <netdb.h> #include <sys/types.h> #include <sys/socket.h> struct hostent *getipnodebyname(const char *name, int af, int flags, int *error_num) struct hostent *getipnodebyaddr(const void *addr, size_t len, int af, int *error_num) Richiedono rispettivamente la risoluzione e la risoluzione inversa di un indirizzo IP. Entrambe le funzioni restituiscono lindirizzo ad una struttura hostent in caso di successo ed NULL in caso di errore.

Entrambe le funzioni supportano esplicitamente la scelta di una famiglia di indirizzi con largomento af (che pu` assumere i valori AF_INET o AF_INET6), e restituiscono un codice di o errore (con valori identici a quelli precedentemente illustrati in tab. 17.5) nella variabile puntata da error_num. La funzione getipnodebyaddr richiede poi che si specichi lindirizzo come per gethostbyaddr passando anche la lunghezza dello stesso nellargomento len. La funzione getipnodebyname prende come primo argomento il nome da risolvere, inoltre prevede un apposito argomento flags, da usare come maschera binaria, che permette di specicarne il comportamento nella risoluzione dei diversi tipi di indirizzi (IPv4 e IPv6); ciascun bit dellargomento esprime una diversa opzione, e queste possono essere specicate con un OR aritmetico delle costanti riportate in tab. 17.6.
19 si ricordi che, come illustrato in g. 15.2, questo in realt` corrisponde ad un numero intero, da esprimere a comunque in network order, non altrettanto avviene per` per in6_addr, pertanto ` sempre opportuno inizializzare o e questi indirizzi con inet_pton (vedi sez. 15.4.4). 20 le funzioni sono presenti nelle glibc versione 2.1.96, ma essendo considerate deprecate (vedi sez. 17.1.4) sono state rimosse nelle versioni successive.

506
Costante AI_V4MAPPED

CAPITOLO 17. LA GESTIONE DEI SOCKET


Signicato Usato con AF_INET6 per richiedere una ricerca su un indirizzo IPv4 invece che IPv6; gli eventuali risultati saranno rimappati su indirizzi IPv6. Usato con AI_V4MAPPED; richiede sia indirizzi IPv4 che IPv6, e gli indirizzi IPv4 saranno rimappati in IPv6. Richiede che una richiesta IPv4 o IPv6 venga eseguita solo se almeno una interfaccia del sistema ` associata ad e un indirizzo di tale tipo. Il valore di default, ` equivalente alla combinazione di e AI_ADDRCONFIG e di AI_V4MAPPED.

AI_ALL AI_ADDRCONFIG

AI_DEFAULT

Tabella 17.6: Valori possibili per i bit dellargomento flags della funzione getipnodebyname.

Entrambe le funzioni restituiscono un puntatore ad una struttura hostent che contiene i risultati della ricerca, che viene allocata dinamicamente insieme a tutto lo spazio necessario a contenere i dati in essa referenziati; per questo motivo queste funzioni non sorono dei problemi dovuti alluso di una sezione statica di memoria presenti con le precedenti gethostbyname e gethostbyaddr. Luso di una allocazione dinamica per` comporta anche la necessit` di dio a sallocare esplicitamente la memoria occupata dai risultati una volta che questi non siano pi` u necessari; a tale scopo viene fornita la funzione freehostent, il cui prototipo `: e
#include <netdb.h> #include <sys/types.h> #include <sys/socket.h> void freehostent(struct hostent *ip) Disalloca una struttura hostent. La funzione non ritorna nulla.

La funzione permette di disallocare una struttura hostent precedentemente allocata in una chiamata di getipnodebyname o getipnodebyaddr, e prende come argomento lindirizzo restituito da una di queste funzioni. Inne per concludere la nostra panoramica sulle funzioni di risoluzione dei nomi dobbiamo citare le funzioni che permettono di interrogare gli altri servizi di risoluzione dei nomi illustrati in sez. 17.1.1; in generale infatti ci sono una serie di funzioni nella forma getXXXbyname e getXXXbyaddr (dove XXX indica il servizio) per ciascuna delle informazioni di rete mantenute dal Name Service Switch che permettono rispettivamente di trovare una corrispondenza cercando per nome o per numero. Lelenco di queste funzioni ` riportato nelle colonne nali di tab. 17.7, dove le si sono suddivise e rispetto al tipo di informazione che forniscono (riportato in prima colonna). Nella tabella si ` e anche riportato il le su cui vengono ordinariamente mantenute queste informazioni, che per` pu` o o essere sostituito da un qualunque supporto interno al Name Service Switch (anche se usualmente questo avviene solo per la risoluzione degli indirizzi). Ciascuna funzione fa riferimento ad una sua apposita struttura che contiene i relativi dati, riportata in terza colonna.
Informazione indirizzo servizio rete protocollo File /etc/hosts /etc/services /etc/networks /etc/protocols Struttura hostent servent netent protoent Funzioni gethostbyname gethostbyaddr getservbyname getservbyaddr getnetbyname getnetbyaddr getprotobyname getprotobyaddr

Tabella 17.7: Funzioni di risoluzione dei nomi per i vari servizi del Name Service Switch.

Delle funzioni di tab. 17.7 abbiamo trattato nora soltanto quelle relative alla risoluzione dei nomi, dato che sono le pi` usate, e prevedono praticamente da sempre la necessit` di rivolgersi ad u a

17.1. LA RISOLUZIONE DEI NOMI

507

una entit` esterna; per le altre invece, estensioni fornite dal NSS a parte, si fa sempre riferimento a ai dati mantenuti nei rispettivi le. Dopo la risoluzione dei nomi a dominio una delle ricerche pi` comuni ` quella sui nomi dei u e servizi di rete pi` comuni (cio` http, smtp, ecc.) da associare alle rispettive porte. Le due funzioni u e da utilizzare per questo sono getservbyname e getservbyaddr, che permettono rispettivamente di ottenere il numero di porta associato ad un servizio dato il nome e viceversa; i loro prototipi sono:
#include <netdb.h> struct servent *getservbyname(const char *name, const char *proto) struct servent *getservbyport(int port, const char *proto) Risolvono il nome di un servizio nel rispettivo numero di porta e viceversa. Ritornano il puntatore ad una struttura servent con i risultati in caso di successo, o NULL in caso di errore.

Entrambe le funzioni prendono come ultimo argomento una stringa proto che indica il protocollo per il quale si intende eettuare la ricerca,21 che nel caso si IP pu` avere come valori o possibili solo udp o tcp;22 se si specica un puntatore nullo la ricerca sar` eseguita su un a protocollo qualsiasi. Il primo argomento ` il nome del servizio per getservbyname, specicato tramite la stringa e name, mentre getservbyport richiede il numero di porta in port. Entrambe le funzioni eseguono una ricerca sul le /etc/services23 ed estraggono i dati dalla prima riga che corrisponde agli argomenti specicati; se la risoluzione ha successo viene restituito un puntatore ad una apposita struttura servent contenente tutti i risultati, altrimenti viene restituito un puntatore nullo. Si tenga presente che anche in questo caso i dati vengono mantenuti in una area di memoria statica e che quindi la funzione non ` rientrante. e
struct servent { char * s_name ; char ** s_aliases ; int s_port ; char * s_proto ; }

/* /* /* /*

official service name */ alias list */ port number */ protocol to use */

Figura 17.4: La struttura servent per la risoluzione dei nomi dei servizi e dei numeri di porta.

La denizione della struttura servent ` riportata in g. 17.4, il primo campo, s_name cone tiene sempre il nome canonico del servizio, mentre s_aliases ` un puntatore ad un vettore di e stringhe contenenti gli eventuali nomi alternativi utilizzabili per identicare lo stesso servizio. Inne s_port contiene il numero di porta e s_proto il nome del protocollo. Come riportato in tab. 17.7 ci sono analoghe funzioni per la risoluzione del nome dei protocolli e delle reti; non staremo a descriverle nei dettagli, in quanto il loro uso ` molto limitato, esse e comunque utilizzano una loro struttura dedicata del tutto analoga alle precedenti: tutti i dettagli relativi al loro funzionamento possono essere trovati nelle rispettive pagine di manuale. Oltre alle funzioni di ricerca esistono delle ulteriori funzioni che prevedono una lettura sequenziale delle informazioni mantenute nel Name Service Switch (in sostanza permettono di leggere i le contenenti le informazioni riga per riga), che sono analoghe a quelle elencate in tab. 8.10
21 le informazioni mantenute in /etc/services infatti sono relative sia alle porte usate su UDP che su TCP, occorre quindi specicare a quale dei due protocolli si fa riferimento. 22 in teoria si potrebbe avere un qualunque protocollo fra quelli citati in /etc/protocols, posto che lo stesso supporti il concetto di porta, in pratica questi due sono gli unici presenti. 23 il Name Service Switch astrae il concetto a qualunque supporto su cui si possano mantenere i suddetti dati.

508

CAPITOLO 17. LA GESTIONE DEI SOCKET

per le informazioni relative ai dati degli utenti e dei gruppi. Nel caso specico dei servizi avremo allora le tre funzioni setservent, getservent e endservent i cui prototipi sono:
#include <netdb.h> void setservent(int stayopen) Apre il le /etc/services e si posiziona al suo inizio. struct servent *getservent(void) Legge la voce successiva nel le /etc/services. void endservent(void) Chiude il le /etc/services. Le due funzioni setservent e endservent non restituiscono nulla, getservent restituisce il puntatore ad una struttura servent in caso di successo e NULL in caso di errore o ne del le.

La prima funzione, getservent, legge una singola voce a partire dalla posizione corrente in /etc/services, pertanto si pu` eseguire una lettura sequenziale dello stesso invocandola pi` o u volte. Se il le non ` aperto provvede automaticamente ad aprirlo, nel qual caso legger` la prima e a voce. La seconda funzione, setservent, permette di aprire il le /etc/services per una successiva lettura, ma se il le ` gi` stato aperto riporta la posizione di lettura alla prima voce del le, e a in questo modo si pu` far ricominciare da capo una lettura sequenziale. Largomento stayopen, o se diverso da zero, fa s` che il le resti aperto anche fra diverse chiamate a getservbyname e getservbyaddr.24 La terza funzione, endservent, provvede semplicemente a chiudere il le. Queste tre funzioni per la lettura sequenziale di nuovo sono presenti per ciascuno dei vari tipi di informazione relative alle reti di tab. 17.7; questo signica che esistono altrettante funzioni nella forma setXXXent, getXXXent e endXXXent, analoghe alle precedenti per la risoluzione dei servizi, che abbiamo riportato in tab. 17.8. Essendo, a parte il tipo di informazione che viene trattato, sostanzialmente identiche nel funzionamento e di scarso utilizzo, non staremo a trattarle una per una, rimandando alle rispettive pagine di manuale.
Informazione indirizzo servizio rete protocollo Funzioni gethostent getservent getnetent getprotoent

sethostent setservent setnetent setprotoent

endhostent endservent endnetent endprotoent

Tabella 17.8: Funzioni lettura sequenziale dei dati del Name Service Switch.

17.1.4

Le funzioni avanzate per la risoluzione dei nomi

Quelle illustrate nella sezione precedente sono le funzioni classiche per la risoluzione di nomi ed indirizzi IP, ma abbiamo gi` visto come esse sorano di vari inconvenienti come il fatto che a usano informazioni statiche, e non prevedono la possibilit` di avere diverse classi di indirizzi. a Anche se sono state create delle estensioni o metodi diversi che permettono di risolvere alcuni di questi inconvenienti,25 comunque esse non forniscono una interfaccia sucientemente generica. Inoltre in genere quando si ha a che fare con i socket non esiste soltanto il problema della risoluzione del nome che identica la macchina, ma anche quello del servizio a cui ci si vuole rivolgere. Per questo motivo con lo standard POSIX 1003.1-2001 sono state indicate come deprecate le varie funzioni gethostbyaddr, gethostbyname, getipnodebyname e getipnodebyaddr ed ` stata introdotta una interfaccia completamente nuova. e
di default dopo una chiamata a queste funzioni il le viene chiuso, cosicch una successiva chiamata a e getservent riparte dallinizio. 25 rimane ad esempio il problema generico che si deve sapere in anticipo quale tipo di indirizzi IP (IPv4 o IPv6) corrispondono ad un certo nome a dominio.
24

17.1. LA RISOLUZIONE DEI NOMI

509

La prima funzione di questa interfaccia ` getaddrinfo,26 che combina le funzionalit` delle e a precedenti getipnodebyname, getipnodebyaddr, getservbyname e getservbyport, consentendo di ottenere contemporaneamente sia la risoluzione di un indirizzo simbolico che del nome di un servizio; il suo prototipo `: e
#include <netdb.h> #include <sys/socket.h> #include <netdb.h> int getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res) Esegue una risoluzione di un nome a dominio e di un nome di servizio. La funzione restituisce 0 in caso di successo o un codice di errore diverso da zero in caso di fallimento.

La funzione prende come primo argomento il nome della macchina che si vuole risolvere, specicato tramite la stringa node. Questo argomento, oltre ad un comune nome a dominio, pu` o indicare anche un indirizzo numerico in forma dotted-decimal per IPv4 o in formato esadecimale per IPv6. Si pu` anche specicare il nome di una rete invece che di una singola macchina. Il o secondo argomento, service, specica invece il nome del servizio che si intende risolvere. Per uno dei due argomenti si pu` anche usare il valore NULL, nel qual caso la risoluzione verr` eettuata o a soltanto sulla base del valore dellaltro. Il terzo argomento, hints, deve essere invece un puntatore ad una struttura addrinfo usata per dare dei suggerimenti al procedimento di risoluzione riguardo al protocollo o del tipo di socket che si intender` utilizzare; getaddrinfo infatti permette di eettuare ricerche generiche sugli a indirizzi, usando sia IPv4 che IPv6, e richiedere risoluzioni sui nomi dei servizi indipendentemente dal protocollo (ad esempio TCP o UDP) che questi possono utilizzare. Come ultimo argomento in res deve essere passato un puntatore ad una variabile (di tipo puntatore ad una struttura addrinfo) che verr` utilizzata dalla funzione per riportare (come vaa lue result argument) i propri risultati. La funzione infatti ` rientrante, ed alloca autonomamente e tutta la memoria necessaria in cui verranno riportati i risultati della risoluzione. La funzione scriver` allindirizzo puntato da res il puntatore iniziale ad una linked list di strutture di tipo a addrinfo contenenti tutte le informazioni ottenute.
struct addrinfo { int ai_flags ; int ai_family ; int ai_socktype ; int ai_protocol ; socklen_t ai_addrlen ; struct sockaddr * ai_addr ; char * ai_canonname ; struct addrinfo * ai_next ; };

/* /* /* /* /* /* /* /*

Input flags . */ Protocol family for socket . */ Socket type . */ Protocol for socket . */ Length of socket address . */ Socket address for socket . */ Canonical name for service location . Pointer to next in list . */

*/

Figura 17.5: La struttura addrinfo usata nella nuova interfaccia POSIX per la risoluzione di nomi a dominio e servizi.

Come illustrato la struttura addrinfo, la cui denizione27 ` riportata in g. 17.5, viene e usata sia in ingresso, per passare dei valori di controllo alla funzione, che in uscita, per ricevere i
la funzione ` denita, insieme a getnameinfo che vedremo pi` avanti, nellRFC 2553. e u la denizione ` ripresa direttamente dal le netdb.h in questa struttura viene dichiarata, la pagina di manuale e riporta size_t come tipo di dato per il campo ai_addrlen, qui viene usata quanto previsto dallo standard POSIX, in cui viene utilizzato socklen_t; i due tipi di dati sono comunque equivalenti.
27 26

510

CAPITOLO 17. LA GESTIONE DEI SOCKET

risultati. Il primo campo, ai_flags, ` una maschera binaria di bit che permettono di controllare e le varie modalit` di risoluzione degli indirizzi, che viene usato soltanto in ingresso. I tre campi a successivi ai_family, ai_socktype, e ai_protocol contengono rispettivamente la famiglia di indirizzi, il tipo di socket e il protocollo, in ingresso vengono usati per impostare una selezione (impostandone il valore nella struttura puntata da hints), mentre in uscita indicano il tipo di risultato contenuto nella struttura. Tutti i campi seguenti vengono usati soltanto in uscita; il campo ai_addrlen indica la dimensione della struttura degli indirizzi ottenuta come risultato, il cui contenuto sar` memorizzato a nella struttura sockaddr posta allindirizzo puntato dal campo ai_addr. Il campo ai_canonname ` un puntatore alla stringa contenente il nome canonico della macchina, ed inne, quando la fune zione restituisce pi` di un risultato, ai_next ` un puntatore alla successiva struttura addrinfo u e della lista. Ovviamente non ` necessario dare dei suggerimenti in ingresso, ed usando NULL come valore e per largomento hints si possono compiere ricerche generiche. Se per` si specica un valore non o nullo questo deve puntare ad una struttura addrinfo precedentemente allocata nella quale siano stati opportunamente impostati i valori dei campi ai_family, ai_socktype, ai_protocol ed ai_flags. I due campi ai_family e ai_socktype prendono gli stessi valori degli analoghi argomenti della funzione socket; in particolare per ai_family si possono usare i valori di tab. 15.1 ma sono presi in considerazione solo PF_INET e PF_INET6, mentre se non si vuole specicare nessuna famiglia di indirizzi si pu` usare il valore PF_UNSPEC. Allo stesso modo per ai_socktype si o possono usare i valori illustrati in sez. 15.2.3 per indicare per quale tipo di socket si vuole risolvere il servizio indicato, anche se i soli signicativi sono SOCK_STREAM e SOCK_DGRAM; in questo caso, se non si vuole eettuare nessuna risoluzione specica, si potr` usare un valore a nullo. Il campo ai_protocol permette invece di eettuare la selezione dei risultati per il nome del servizio usando il numero identicativo del rispettivo protocollo di trasporto (i cui valori possibili sono riportati in /etc/protocols); di nuovo i due soli valori utilizzabili sono quelli relativi a UDP e TCP, o il valore nullo che indica di ignorare questo campo nella selezione.
Costante AI_PASSIVE Signicato Viene utilizzato per ottenere un indirizzo in formato adatto per una successiva chiamata a bind. Se specicato quando si ` usato NULL coe me valore per node gli indirizzi restituiti saranno inizializzati al valore generico (INADDR_ANY per IPv4 e IN6ADDR_ANY_INIT per IPv6), altrimenti verr` usato lindirizzo dellinterfaccia di loopback. Se invece non a ` impostato gli indirizzi verranno restituiti in formato adatto ad una e chiamata a connect o sendto. Richiede la restituzione del nome canonico della macchina, che verr` salvato in una stringa il cui indirizzo sar` restituito nel campo a a ai_canonname della prima struttura addrinfo dei risultati. Se il nome canonico non ` disponibile al suo posto viene restituita una copia e di node. Se impostato il nome della macchina specicato con node deve essere espresso in forma numerica, altrimenti sar` restituito un errore a EAI_NONAME (vedi tab. 17.10), in questo modo si evita ogni chiamata alle funzioni di risoluzione. Stesso signicato dellanaloga di tab. 17.6. Stesso signicato dellanaloga di tab. 17.6. Stesso signicato dellanaloga di tab. 17.6.

AI_CANONNAME

AI_NUMERICHOST

AI_V4MAPPED AI_ALL AI_ADDRCONFIG

Tabella 17.9: Costanti associate ai bit del campo ai_flags della struttura addrinfo.

Inne lultimo campo ` ai_flags; che deve essere impostato come una maschera binaria; e i bit di questa variabile infatti vengono usati per dare delle indicazioni sul tipo di risoluzione

17.1. LA RISOLUZIONE DEI NOMI

511

voluta, ed hanno valori analoghi a quelli visti in sez. 17.1.3 per getipnodebyname; il valore di ai_flags pu` essere impostata con un OR aritmetico delle costanti di tab. 17.9, ciascuna delle o quali identica un bit della maschera. La funzione restituisce un valore nullo in caso di successo, o un codice in caso di errore. I valori usati come codice di errore sono riportati in tab. 17.10; dato che la funzione utilizza altre funzioni e chiamate al sistema per ottenere il suo risultato in generale il valore di errno non ` e signicativo, eccetto il caso in cui si sia ricevuto un errore di EAI_SYSTEM, nel qual caso lerrore corrispondente ` riportato tramite errno. e
Costante EAI_FAMILY EAI_SOCKTYPE EAI_BADFLAGS EAI_NONAME Signicato La famiglia di indirizzi richiesta non ` supportata. e Il tipo di socket richiesto non ` supportato. e Il campo ai_flags contiene dei valori non validi. Il nome a dominio o il servizio non sono noti, viene usato questo errore anche quando si specica il valore NULL per entrambi gli argomenti node e service. Il servizio richiesto non ` disponibile per il tipo di socket richiesto, anche e se pu` esistere per altri tipi di socket. o La rete richiesta non ha nessun indirizzo di rete per la famiglia di indirizzi specicata. La macchina specicata esiste, ma non ha nessun indirizzo di rete denito. ` E stato impossibile allocare la memoria necessaria alle operazioni. Il DNS ha restituito un errore di risoluzione permanente. Il DNS ha restituito un errore di risoluzione temporaneo, si pu` o ritentare in seguito. C` stato un errore di sistema, si pu` controllare errno per i dettagli. e o

EAI_SERVICE EAI_ADDRFAMILY EAI_NODATA EAI_MEMORY EAI_FAIL EAI_AGAIN EAI_SYSTEM

Tabella 17.10: Costanti associate ai valori dei codici di errore della funzione getaddrinfo.

Come per i codici di errore di gethostbyname anche in questo caso ` fornita una apposita e funzione, analoga di strerror, che consente di utilizzarli direttamente per stampare a video un messaggio esplicativo; la funzione ` gai_strerror ed il suo prototipo `: e e
#include <netdb.h> const char *gai_strerror(int errcode) Fornisce il messaggio corrispondente ad un errore di getaddrinfo. La funzione restituisce il puntatore alla stringa contenente il messaggio di errore.

La funzione restituisce un puntatore alla stringa contenente il messaggio corrispondente dal codice di errore errcode ottenuto come valore di ritorno di getaddrinfo. La stringa ` allocata e staticamente, ma essendo costante, ed accessibile in sola lettura, questo non comporta nessun problema di rientranza della funzione. Dato che ad un certo nome a dominio possono corrispondere pi` indirizzi IP (sia IPv4 che u IPv6), e che un certo servizio pu` essere fornito su protocolli e tipi di socket diversi, in generale, o a meno di non aver eseguito una selezione specica attraverso luso di hints, si otterr` una a diversa struttura addrinfo per ciascuna possibilit`. Ad esempio se si richiede la risoluzione del a servizio echo per lindirizzo www.truelite.it, e si imposta AI_CANONNAME per avere anche la risoluzione del nome canonico, si avr` come risposta della funzione la lista illustrata in g. 17.6. a Come primo esempio di uso di getaddrinfo vediamo un programma elementare di interrogazione del resolver basato questa funzione, il cui corpo principale ` riportato in g. 17.7. Il e codice completo del programma, compresa la gestione delle opzioni in cui ` gestita leventuale e inizializzazione dellargomento hints per restringere le ricerche su protocolli, tipi di socket o famiglie di indirizzi, ` disponibile nel le mygetaddr.c dei sorgenti allegati alla guida. e Il corpo principale inizia controllando (1-5) il numero di argomenti passati, che devono essere sempre due, e corrispondere rispettivamente allindirizzo ed al nome del servizio da risolvere. A

512

CAPITOLO 17. LA GESTIONE DEI SOCKET

Figura 17.6: La linked list delle strutture addrinfo restituite da getaddrinfo.

questo segue la chiamata (7) alla funzione getaddrinfo, ed il successivo controllo (8-11) del suo corretto funzionamento, senza il quale si esce immediatamente stampando il relativo codice di errore. Se la funzione ha restituito un valore nullo il programma prosegue inizializzando (12) il puntatore ptr che sar` usato nel successivo ciclo (14-35) di scansione della lista delle strutture a addrinfo restituite dalla funzione. Prima di eseguire questa scansione (12) viene stampato il valore del nome canonico che ` presente solo nella prima struttura. e La scansione viene ripetuta (14) ntanto che si ha un puntatore valido. La selezione principale ` fatta sul campo ai_family, che stabilisce a quale famiglia di indirizzi fa riferimento la struttura e in esame. Le possibilit` sono due, un indirizzo IPv4 o IPv6, se nessuna delle due si verica si a provvede (27-30) a stampare un messaggio di errore ed uscire.28 Per ciascuno delle due possibili famiglie di indirizzi si estraggono le informazioni che poi verranno stampate alla ne del ciclo (31-34). Il primo caso esaminato (15-21) ` quello degli e indirizzi IPv4, nel qual caso prima se ne stampa lidenticazione (16) poi si provvede a ricavare la struttura degli indirizzi (17) indirizzata dal campo ai_addr, eseguendo un opportuno casting del puntatore per poter estrarre da questa la porta (18) e poi lindirizzo (19) che verr` convertito a con una chiamata ad inet_ntop. La stessa operazione (21-27) viene ripetuta per gli indirizzi IPv6, usando la rispettiva struttura degli indirizzi. Si noti anche come in entrambi i casi per la chiamata a inet_ntop si sia dovuto passare il puntatore al campo contenente lindirizzo IP nella struttura puntata dal campo ai_addr.29 Una volta estratte dalla struttura addrinfo tutte le informazioni relative alla risoluzione
questa eventualit` non dovrebbe mai vericarsi, almeno ntanto che la funzione getaddrinfo lavora a correttamente. 29 il meccanismo ` complesso a causa del fatto che al contrario di IPv4, in cui lindirizzo IP pu` essere espresso con e o un semplice numero intero, in IPv6 questo deve essere necessariamente fornito come struttura, e pertanto anche se nella struttura puntata da ai_addr sono presenti direttamente i valori nali, per luso con inet_ntop occorre comunque passare un puntatore agli stessi (ed il costrutto &addr6->sin6_addr ` corretto in quanto loperatore -> e ha on questo caso precedenza su &).
28

17.1. LA RISOLUZIONE DEI NOMI

513

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36

/* remaining argument check */ if (( argc - optind ) != 2) { printf ( " Wrong number of arguments % d \ n " , argc - optind ); usage (); } /* main body */ ret = getaddrinfo ( argv [ optind ] , argv [ optind +1] , & hint , & res ); if ( ret != 0) { printf ( " Resolution error % s \ n " , gai_strerror ( ret )); exit (1); } ptr = res ; /* init list pointer */ printf ( " Canonical name % s \ n " , ptr - > ai_canonname ); /* print cname */ while ( ptr != NULL ) { /* loop on list */ if ( ptr - > ai_family == PF_INET ) { /* if IPv4 */ printf ( " IPv4 address : \ n " ); addr = ( struct sockaddr_in *) ptr - > ai_addr ; /* address */ port = ntohs ( addr - > sin_port ); /* port */ string = inet_ntop ( addr - > sin_family , & addr - > sin_addr , buffer , sizeof ( buffer )); } else if ( ptr - > ai_family == PF_INET6 ) { /* if IPv6 */ printf ( " IPv6 address : \ n " ); addr6 = ( struct sockaddr_in6 *) ptr - > ai_addr ; /* address */ port = ntohs ( addr6 - > sin6_port ); /* port */ string = inet_ntop ( addr6 - > sin6_family , & addr6 - > sin6_addr , buffer , sizeof ( buffer )); } else { /* else is an error */ printf ( " Address family error \ n " ); exit (1); } printf ( " \ tIndirizzo % s \ n " , string ); printf ( " \ tProtocollo % i \ n " , ptr - > ai_protocol ); printf ( " \ tPorta % i \ n " , port ); ptr = ptr - > ai_next ; } exit (0);

Figura 17.7: Esempio di codice per la risoluzione di un indirizzo.

richiesta e stampati i relativi valori, lultimo passo (34) ` di estrarre da ai_next lindirizzo della e eventuale successiva struttura presente nella lista e ripetere il ciclo, n tanto che, completata la scansione, questo avr` un valore nullo e si potr` terminare (36) il programma. a a Si tenga presente che getaddrinfo non garantisce nessun particolare ordinamento della lista delle strutture addrinfo restituite, anche se usualmente i vari indirizzi IP (se ne ` presente pi` e u di uno) sono forniti nello stesso ordine in cui vengono inviati dal server DNS. In particolare nulla garantisce che vengano forniti prima i dati relativi ai servizi di un determinato protocollo o tipo di socket, se ne sono presenti di diversi. Se allora utilizziamo il nostro programma potremo vericare il risultato: [piccardi@gont sources]$ ./mygetaddr -c Canonical name sources2.truelite.it IPv4 address: Indirizzo 62.48.34.25 Protocollo 6 Porta 7 IPv4 address: gapil.truelite.it echo

514 Indirizzo 62.48.34.25 Protocollo 17 Porta 7

CAPITOLO 17. LA GESTIONE DEI SOCKET

Una volta estratti i risultati dalla linked list puntata da res se questa non viene pi` utilizzata u si dovr` avere cura di disallocare opportunamente tutta la memoria, per questo viene fornita a lapposita funzione freeaddrinfo, il cui prototipo `: e
#include <netdb.h> void freeaddrinfo(struct addrinfo *res) Libera la memoria allocata da una precedente chiamata a getaddrinfo. La funzione non restituisce nessun codice di errore.

La funzione prende come unico argomento il puntatore res, ottenuto da una precedente chiamata a getaddrinfo, e scandisce la lista delle strutture per liberare tutta la memoria allocata. Dato che la funzione non ha valori di ritorno deve essere posta molta cura nel passare un valore valido per res. Si tenga presente inne che se si copiano i risultati da una delle strutture addrinfo restituite nella lista indicizzata da res, occorre avere cura di eseguire una deep copy in cui si copiano anche tutti i dati presenti agli indirizzi contenuti nella struttura addrinfo, perch una volta disallocati e i dati con freeaddrinfo questi non sarebbero pi` disponibili. u Anche la nuova interfaccia denita da POSIX prevede una nuova funzione per eseguire la risoluzione inversa e determinare nomi di servizi e di dominio dati i rispettivi valori numerici. La funzione che sostituisce le varie gethostbyname, getipnodebyname e getservname ` e getnameinfo, ed il suo prototipo `: e
#include <sys/socket.h> #include <netdb.h> int getnameinfo(const struct sockaddr *sa, socklen_t salen, char *host, size_t hostlen, char *serv, size_t servlen, int flags) Risolve il contenuto di una struttura degli indirizzi in maniera indipendente dal protocollo. La funzione restituisce 0 in caso di successo e un codice di errore diverso da zero altrimenti.

La principale caratteristica di getnameinfo ` che la funzione ` in grado di eseguire una e e risoluzione inversa in maniera indipendente dal protocollo; il suo primo argomento sa infatti ` e il puntatore ad una struttura degli indirizzi generica, che pu` contenere sia indirizzi IPv4 che o IPv6, la cui dimensione deve comunque essere specicata con largomento salen. I risultati della funzione saranno restituiti nelle due stringhe puntate da host e serv, che dovranno essere state precedentemente allocate per una lunghezza massima che deve essere specicata con gli altri due argomenti hostlen e servlen. Si pu`, quando non si ` interessati o e ad uno dei due, passare il valore NULL come argomento, cos` che la corrispondente informazione non verr` richiesta. Inne lultimo argomento flags ` una maschera binaria i cui bit consentono a e di impostare le modalit` con cui viene eseguita la ricerca, e deve essere specicato attraverso a lOR aritmetico dei valori illustrati in tab. 17.11. La funzione ritorna zero in caso di successo, e scrive i propri risultati agli indirizzi indicati dagli argomenti host e serv come stringhe terminate dal carattere NUL, a meno che queste non debbano essere troncate qualora la loro dimensione ecceda quelle specicate dagli argomenti hostlen e servlen. Sono comunque denite le due costanti NI_MAXHOST e NI_MAXSERV30 che possono essere utilizzate come limiti massimi. In caso di errore viene restituito invece un codice che assume gli stessi valori illustrati in tab. 17.10. A questo punto possiamo fornire degli esempi di utilizzo della nuova interfaccia, adottandola per le precedenti implementazioni del client e del server per il servizio echo; dato che luso
30

in Linux le due costanti sono denite in netdb.h ed hanno rispettivamente il valore 1024 e 12.

17.1. LA RISOLUZIONE DEI NOMI


Costante NI_NOFQDN NI_NUMERICHOST NI_NAMEREQD NI_NUMERICSERV NI_DGRAM Signicato Richiede che venga restituita solo il nome della macchina allinterno del dominio al posto del nome completo (FQDN). Richiede che venga restituita la forma numerica dellindirizzo (questo succede sempre se il nome non pu` essere ottenuto). o Richiede la restituzione di un errore se il nome non pu` essere risolto. o Richiede che il servizio venga restituito in forma numerica (attraverso il numero di porta). Richiede che venga restituito il nome del servizio su UDP invece che quello su TCP per quei pichi servizi (porte 512-214) che soni diversi nei due protocolli.

515

Tabella 17.11: Costanti associate ai bit dellargomento flags della funzione getnameinfo.

delle funzioni appena illustrate (in particolare di getaddrinfo) ` piuttosto complesso, essendo e necessaria anche una impostazione diretta dei campi dellargomento hints, provvederemo una interfaccia semplicata per i due casi visti nora, quello in cui si specica nel client un indirizzo remoto per la connessione al server, e quello in cui si specica nel server un indirizzo locale su cui porsi in ascolto. La prima funzione della nostra interfaccia semplicata ` sockconn che permette di ottenere e un socket, connesso allindirizzo ed al servizio specicati. Il corpo della funzione ` riportato in e g. 17.8, il codice completo ` nel le SockUtil.c dei sorgenti allegati alla guida, che contiene e varie funzioni di utilit` per luso dei socket. a La funzione prende quattro argomenti, i primi due sono le stringhe che indicano il nome della macchina a cui collegarsi ed il relativo servizio su cui sar` eettuata la risoluzione; seguono il a protocollo da usare (da specicare con il valore numerico di /etc/protocols) ed il tipo di socket (al solito specicato con i valori illustrati in sez. 15.2.3). La funzione ritorna il valore del le descriptor associato al socket (un numero positivo) in caso di successo, o -1 in caso di errore; per risolvere il problema di non poter passare indietro i valori di ritorno di getaddrinfo contenenti i relativi codici di errore31 si sono stampati i messaggi derrore direttamente nella funzione. Una volta denite le variabili necessarie (3-5) la funzione prima (6) azzera il contenuto della struttura hint e poi provvede (7-9) ad inizializzarne i valori necessari per la chiamata (10) a getaddrinfo. Di questultima si controlla (12-16) il codice di ritorno, in modo da stampare un avviso di errore, azzerare errno ed uscire in caso di errore. Dato che ad una macchina possono corrispondere pi` indirizzi IP, e di tipo diverso (sia IPv4 che IPv6), mentre il servizio pu` essere u o in ascolto soltanto su uno solo di questi, si provvede a tentare la connessione per ciascun indirizzo restituito allinterno di un ciclo (18-40) di scansione della lista restituita da getaddrinfo, ma prima (17) si salva il valore del puntatore per poterlo riutilizzare alla ne per disallocare la lista. Il ciclo viene ripetuto (18) ntanto che si hanno indirizzi validi, ed inizia (19) con lapertura del socket; se questa fallisce si controlla (20) se sono disponibili altri indirizzi, nel qual caso si passa al successivo (21) e si riprende (22) il ciclo da capo; se non ve ne sono si stampa lerrore ritornando immediatamente (24-27). Quando la creazione del socket ha avuto successo si procede (29) direttamente con la connessione, di nuovo in caso di fallimento viene ripetuto (3038) il controllo se vi sono o no altri indirizzi da provare nella stessa modalit` fatta in precedenza, a aggiungendovi per` in entrambi i casi (32 e (36) la chiusura del socket precedentemente aperto, o che non ` pi` utilizzabile. e u Se la connessione ha avuto successo invece si termina (39) direttamente il ciclo, e prima di ritornare (31) il valore del le descriptor del socket si provvede (30) a liberare le strutture addrinfo allocate da getaddrinfo utilizzando il valore del relativo puntatore precedentemente (17) salvato. Si noti come per la funzione sia del tutto irrilevante se la struttura ritornata contiene
non si pu` avere nessuna certezza che detti valori siano negativi, ` questo ` invece necessario per evitare ogni o e e possibile ambiguit` nei confronti del valore di ritorno in caso di successo. a
31

516

CAPITOLO 17. LA GESTIONE DEI SOCKET

int sockconn ( char * host , char * serv , int prot , int type ) { 3 struct addrinfo hint , * addr , * save ; 4 int res ; 5 int sock ; 6 memset (& hint , 0 , sizeof ( struct addrinfo )); 7 hint . ai_family = PF_UNSPEC ; /* generic address ( IPv4 or IPv6 ) */ 8 hint . ai_protocol = prot ; /* protocol */ 9 hint . ai_socktype = type ; /* socket type */ 10 res = getaddrinfo ( host , serv , & hint , & addr ); /* calling getaddrinfo */ 11 if ( res != 0) { /* on error exit */ 12 fprintf ( stderr , " sockconn : resolution failed : " ); 13 fprintf ( stderr , " % s \ n " , gai_strerror ( res )); 14 errno = 0; /* clear errno */ 15 return -1; 16 } 17 save = addr ; 18 while ( addr != NULL ) { /* loop on possible addresses */ 19 sock = socket ( addr - > ai_family , addr - > ai_socktype , addr - > ai_protocol ); 20 if ( sock < 0) { /* on error */ 21 if ( addr - > ai_next != NULL ) { /* if other addresses */ 22 addr = addr - > ai_next ; /* take next */ 23 continue ; /* restart cycle */ 24 } else { /* else stop */ 25 perror ( " sockconn : cannot create socket " ); 26 return sock ; 27 } 28 } 29 if ( ( res = connect ( sock , addr - > ai_addr , addr - > ai_addrlen ) < 0)) { 30 if ( addr - > ai_next != NULL ) { /* if other addresses */ 31 addr = addr - > ai_next ; /* take next */ 32 close ( sock ); /* close socket */ 33 continue ; /* restart cycle */ 34 } else { /* else stop */ 35 perror ( " sockconn : cannot connect " ); 36 close ( sock ); 37 return res ; 38 } 39 } else break ; /* ok , we are connected ! */ 40 } 41 freeaddrinfo ( save ); /* done , release memory */ 42 return sock ; 43 }
1 2

Figura 17.8: Il codice della funzione sockconn.

indirizzi IPv6 o IPv4, in quanto si fa uso direttamente dei dati relativi alle strutture degli indirizzi di addrinfo che sono opachi rispetto alluso della funzione connect. Per usare questa funzione possiamo allora modicare ulteriormente il nostro programma client per il servizio echo; in questo caso rispetto al codice usato nora per collegarsi (vedi g. 16.11) avremo una semplicazione per cui il corpo principale del nostro client diventer` a quello illustrato in g. 17.9, in cui le chiamate a socket, inet_pton e connect sono sostituite da una singola chiamata a sockconn. Inoltre il nuovo client (il cui codice completo ` nel le e TCP_echo_fifth.c dei sorgenti allegati) consente di utilizzare come argomento del programma un nome a dominio al posto dellindirizzo numerico, e pu` utilizzare sia indirizzi IPv4 che IPv6. o La seconda funzione di ausilio ` sockbind, il cui corpo principale ` riportato in g. 17.10 (al e e solito il sorgente completo ` nel le sockbind.c dei sorgenti allegati alla guida). Come si pu` e o

17.1. LA RISOLUZIONE DEI NOMI

517

int main ( int argc , char * argv []) { 3 /* 4 * Variables definition 5 */ 6 int sock , i ; 7 int reset = 0; 8 ... 9 /* call sockaddr to get a connected socket */ 10 if ( ( sock = sockconn ( argv [ optind ] , " echo " , 6 , SOCK_STREAM )) < 0) { 11 return 1; 12 } 13 /* do read / write operations */ 14 ClientEcho ( stdin , sock ); 15 /* normal exit */ 16 return 0; 17 }
1 2

Figura 17.9: Il nuovo codice per la connessione del client echo.

notare la funzione ` del tutto analoga alla precedente sockconn, e prende gli stessi argomenti, e per` invece di eseguire una connessione con connect si limita a chiamare bind per collegare il o socket ad una porta. Dato che la funzione ` pensata per essere utilizzata da un server ci si pu` chiedere a quale e o scopo mantenere largomento host quando lindirizzo di questo ` usualmente noto. Si ricordi e per` quanto detto in sez. 16.2.1, relativamente al signicato della scelta di un indirizzo specico o come argomento di bind, che consente di porre il server in ascolto su uno solo dei possibili diversi indirizzi presenti su di una macchina. Se non si vuole che la funzione esegua bind su un indirizzo specico, ma utilizzi lindirizzo generico, occorrer` avere cura di passare un valore NULL come a valore per largomento host; luso del valore AI_PASSIVE serve ad ottenere il valore generico nella rispettiva struttura degli indirizzi. Come gi` detto la funzione ` analoga a sockconn ed inizia azzerando ed inizializzando (6-11) a e opportunamente la struttura hint con i valori ricevuti come argomenti, soltanto che in questo caso si ` usata (8) una impostazione specica dei ag di hint usando AI_PASSIVE per indicare e che il socket sar` usato per una apertura passiva. Per il resto la chiamata (12-18) a getaddrinfo a e ed il ciclo principale (20-42) sono identici, solo che si ` sostituita (31) la chiamata a connect e con una chiamata a bind. Anche la conclusione (43-44) della funzione ` identica. e Si noti come anche in questo caso si siano inserite le stampe degli errori sullo standard error, nonostante la funzione possa essere invocata da un demone. Nel nostro caso questo non ` un e problema in quanto se la funzione non ha successo il programma deve uscire immediatamente prima di essere posto in background, e pu` quindi scrivere gli errori direttamente sullo standard o error. Con luso di questa funzione si pu` modicare anche il codice del nostro server echo, che o rispetto a quanto illustrato nella versione iniziale di g. 16.13 viene modicato nella forma riportata in g. 17.11. In questo caso il socket su cui porsi in ascolto viene ottenuto (15-18) da sockbind che si cura anche della eventuale risoluzione di un indirizzo specico sul quale si voglia far ascoltare il server.

518

CAPITOLO 17. LA GESTIONE DEI SOCKET

int sockbind ( char * host , char * serv , int prot , int type ) { 3 struct addrinfo hint , * addr , * save ; 4 int res ; 5 int sock ; 6 char buf [ INET6_ADDRSTRLEN ]; 7 memset (& hint , 0 , sizeof ( struct addrinfo )); 8 hint . ai_flags = AI_PASSIVE ; /* address for binding */ 9 hint . ai_family = PF_UNSPEC ; /* generic address ( IPv4 or IPv6 ) */ 10 hint . ai_protocol = prot ; /* protocol */ 11 hint . ai_socktype = type ; /* socket type */ 12 res = getaddrinfo ( host , serv , & hint , & addr ); /* calling getaddrinfo */ 13 if ( res != 0) { /* on error exit */ 14 fprintf ( stderr , " sockbind : resolution failed : " ); 15 fprintf ( stderr , " % s \ n " , gai_strerror ( res )); 16 errno = 0; /* clear errno */ 17 return -1; 18 } 19 save = addr ; /* saving for freeaddrinfo */ 20 while ( addr != NULL ) { /* loop on possible addresses */ 21 sock = socket ( addr - > ai_family , addr - > ai_socktype , addr - > ai_protocol ); 22 if ( sock < 0) { /* on error */ 23 if ( addr - > ai_next != NULL ) { /* if other addresses */ 24 addr = addr - > ai_next ; /* take next */ 25 continue ; /* restart cycle */ 26 } else { /* else stop */ 27 perror ( " sockbind : cannot create socket " ); 28 return sock ; 29 } 30 } 31 if ( ( res = bind ( sock , addr - > ai_addr , addr - > ai_addrlen )) < 0) { 32 if ( addr - > ai_next != NULL ) { /* if other addresses */ 33 addr = addr - > ai_next ; /* take next */ 34 close ( sock ); /* close socket */ 35 continue ; /* restart cycle */ 36 } else { /* else stop */ 37 perror ( " sockbind : cannot connect " ); 38 close ( sock ); 39 return res ; 40 } 41 } else break ; /* ok , we are binded ! */ 42 } 43 freeaddrinfo ( save ); /* done , release memory */ 44 return sock ; 45 }
1 2

Figura 17.10: Il codice della funzione sockbind.

17.2

Le opzioni dei socket

Bench dal punto di vista del loro uso come canali di trasmissione di dati i socket siano trattati e allo stesso modo dei le, ed acceduti tramite i le descriptor, la normale interfaccia usata per la gestione dei le non ` suciente a poterne controllare tutte le caratteristiche, che variano tra e laltro a seconda del loro tipo (e della relativa forma di comunicazione sottostante). In questa sezione vedremo allora quali sono le funzioni dedicate alla gestione delle caratteristiche speciche dei vari tipi di socket, le cosiddette socket options.

17.2. LE OPZIONI DEI SOCKET

519

int main ( int argc , char * argv []) { 3 /* 4 * Variables definition 5 */ 6 int list_fd , conn_fd ; 7 ... 8 /* Main code begin here */ 9 if ( compat ) { /* install signal handler */ 10 Signal ( SIGCHLD , HandSigCHLD ); /* non restarting handler */ 11 } else { 12 SignalRestart ( SIGCHLD , HandSigCHLD ); /* restarting handler */ 13 } 14 /* create and bind socket */ 15 if ( ( list_fd = sockbind ( argv [ optind ] , " echo " , 6 , SOCK_STREAM )) < 0) { 16 return 1; 17 } 18 ... 19 }
1 2

Figura 17.11: Nuovo codice per lapertura passiva del server echo.

17.2.1

Le funzioni setsockopt e getsockopt

Le varie caratteristiche dei socket possono essere gestite attraverso luso di due funzioni generiche che permettono rispettivamente di impostarle e di recuperarne il valore corrente. La prima di queste due funzioni, quella usata per impostare le socket options, ` setsockopt, ed il suo e prototipo `: e
#include <sys/socket.h> #include <sys/types.h> int setsockopt(int sock, int level, int optname, const void *optval, socklen_t optlen) Imposta le opzioni di un socket. La funzione restituisce 0 in caso di successo e -1 in caso di errore, nel qual caso errno assumer` i a valori: EBADF EFAULT EINVAL il le descriptor sock non ` valido. e lindirizzo optval non ` valido. e il valore di optlen non ` valido. e il le descriptor sock non corrisponde ad un socket.

ENOPROTOOPT lopzione scelta non esiste per il livello indicato. ENOTSOCK

Il primo argomento della funzione, sock, indica il socket su cui si intende operare; per indicare lopzione da impostare si devono usare i due argomenti successivi, level e optname. Come abbiamo visto in sez. 14.2 i protocolli di rete sono strutturati su vari livelli, ed linterfaccia dei socket pu` usarne pi` di uno. Si avranno allora funzionalit` e caratteristiche diverse per o u a ciascun protocollo usato da un socket, e quindi saranno anche diverse le opzioni che si potranno impostare per ciascun socket, a seconda del livello (trasporto, rete, ecc.) su cui si vuole andare ad operare. Il valore di level seleziona allora il protocollo su cui vuole intervenire, mentre optname permette di scegliere su quale delle opzioni che sono denite per quel protocollo si vuole operare. In sostanza la selezione di una specica opzione viene fatta attraverso una coppia di valori level e optname e chiaramente la funzione avr` successo soltanto se il protocollo in questione a

520

CAPITOLO 17. LA GESTIONE DEI SOCKET

prevede quella opzione ed ` utilizzato dal socket. Inne level prevede anche il valore speciale e SOL_SOCKET usato per le opzioni generiche che sono disponibili per qualunque tipo di socket. I valori usati per level, corrispondenti ad un dato protocollo usato da un socket, sono quelli corrispondenti al valore numerico che identica il suddetto protocollo in /etc/protocols; dato che la leggibilit` di un programma non trarrebbe certo benecio dalluso diretto dei valori a numerici, pi` comunemente si indica il protocollo tramite le apposite costanti SOL_* riportate u in tab. 17.12, dove si sono riassunti i valori che possono essere usati per largomento level.32
Livello SOL_SOCKET SOL_IP SOL_TCP SOL_IPV6 SOL_ICMPV6 Signicato Opzioni generiche dei socket. Opzioni speciche per i socket che usano IPv4. Opzioni per i socket che usano TCP. Opzioni speciche per i socket che usano IPv6. Opzioni speciche per i socket che usano ICMPv6.

Tabella 17.12: Possibili valori dellargomento level delle funzioni setsockopt e getsockopt.

Il quarto argomento, optval ` un puntatore ad una zona di memoria che contiene i dati e che specicano il valore dellopzione che si vuole passare al socket, mentre lultimo argomento optlen,33 ` la dimensione in byte dei dati presenti allindirizzo indicato da optval. Dato che e il tipo di dati varia a seconda dellopzione scelta, occorrer` individuare qual ` quello che deve a e essere usato, ed utilizzare le opportune variabili. La gran parte delle opzioni utilizzano per optval un valore intero, se poi lopzione esprime una condizione logica, il valore ` sempre un intero, ma si dovr` usare un valore non nullo per e a abilitarla ed un valore nullo per disabilitarla. Se invece lopzione non prevede di dover ricevere nessun tipo di valore si deve impostare optval a NULL. Un piccolo numero di opzioni per` usano o dei tipi di dati peculiari, ` questo il motivo per cui optval ` stato denito come puntatore e e generico. La seconda funzione usata per controllare le propriet` dei socket ` getsockopt, che serve a a e leggere i valori delle opzioni dei socket ed a farsi restituire i dati relativi al loro funzionamento; il suo prototipo `: e
#include <sys/socket.h> #include <sys/types.h> int getsockopt(int s, int level, int optname, void *optval, socklen_t *optlen) Legge le opzioni di un socket. La funzione restituisce 0 in caso di successo e -1 in caso di errore, nel qual caso errno assumer` i a valori: EBADF EFAULT il le descriptor sock non ` valido. e lindirizzo optval o quello di optlen non ` valido. e il le descriptor sock non corrisponde ad un socket.

ENOPROTOOPT lopzione scelta non esiste per il livello indicato. ENOTSOCK

I primi tre argomenti sono identici ed hanno lo stesso signicato di quelli di setsockopt, anche se non ` detto che tutte le opzioni siano denite per entrambe le funzioni. In questo caso e optval viene usato per ricevere le informazioni ed indica lindirizzo a cui andranno scritti i dati
la notazione in questo caso `, purtroppo, abbastanza confusa: infatti in Linux il valore si pu` impostare sia e o usando le costanti SOL_*, che le analoghe IPPROTO_* (citate anche da Stevens in [2]); entrambe hanno gli stessi valori che sono equivalenti ai numeri di protocollo di /etc/protocols, con una eccezione specica, che ` quella e del protocollo ICMP, per la quale non esista una costante, il che ` comprensibile dato che il suo valore, 1, ` quello e e che viene assegnato a SOL_SOCKET. 33 questo argomento ` in realt` sempre di tipo int, come era nelle libc4 e libc5; luso di socklen_t ` stato e a e introdotto da POSIX (valgono le stesse considerazioni per luso di questo tipo di dato fatte in sez. 16.2.4) ed adottato dalle glibc.
32

17.2. LE OPZIONI DEI SOCKET

521

letti dal socket, inne optlen diventa un puntatore ad una variabile che viene usata come value result argument per indicare, prima della chiamata della funzione, la lunghezza del buer allocato per optval e per ricevere indietro, dopo la chiamata della funzione, la dimensione eettiva dei dati scritti su di esso. Se la dimensione del buer allocato per optval non ` suciente si avr` e a un errore.

17.2.2

Le opzioni generiche

Come accennato esiste un insieme generico di opzioni dei socket che possono applicarsi a qualunque tipo di socket,34 indipendentemente da quale protocollo venga poi utilizzato. Se si vuole operare su queste opzioni generiche il livello da utilizzare ` SOL_SOCKET; si ` riportato un elenco e e di queste opzioni in tab. 17.13.
Opzione SO_KEEPALIVE SO_OOBINLINE SO_RCVLOWAT SO_SNDLOWAT SO_RCVTIMEO SO_SNDTIMEO SO_BSDCOMPAT SO_PASSCRED SO_PEERCRED SO_BINDTODEVICE SO_DEBUG SO_REUSEADDR SO_TYPE SO_ACCEPTCONN SO_DONTROUTE SO_BROADCAST SO_SNDBUF SO_RCVBUF SO_LINGER SO_PRIORITY SO_ERROR get set ag Tipo int int int int timeval timeval int int ucred char * int int int int int int int int linger int int Descrizione Controlla lattivit` della connessione. a Lascia in linea i dati out-of-band. Basso livello sul buer di ricezione. Basso livello sul buer di trasmissione. Timeout in ricezione. Timeout in trasmissione. Abilita la compatibilit` con BSD. a Abilita la ricezione di credenziali. Restituisce le credenziali del processo remoto. Lega il socket ad un dispositivo. Abilita il debugging sul socket. Consente il riutilizzo di un indirizzo locale. Restituisce il tipo di socket. Indica se il socket ` in ascolto. e Non invia attraverso un gateway. Attiva o disattiva il broadcast. Imposta dimensione del buer di trasmissione. Imposta dimensione del buer di ricezione. Indugia nella chiusura con dati da spedire. Imposta la priorit` del socket. a Riceve e cancella gli errori pendenti.

Tabella 17.13: Le opzioni disponibili al livello SOL_SOCKET.

La tabella elenca le costanti che identicano le singole opzioni da usare come valore per optname; le due colonne seguenti indicano per quali delle due funzioni (getsockopt o setsockopt) lopzione ` disponibile, mentre la colonna successiva indica, quando di ha a che fare con un valore e di optval intero, se lopzione ` da considerare un numero o un valore logico. Si ` inoltre riportato e e sulla quinta colonna il tipo di dato usato per optval ed una breve descrizione del signicato delle singole opzioni sulla sesta. Le descrizioni delle opzioni presenti in tab. 17.13 sono estremamente sommarie, ` perci` e o necessario fornire un po pi` di informazioni. Alcune opzioni inoltre hanno una notevole rilevanza u nella gestione dei socket, e pertanto il loro utilizzo sar` approfondito separatamente in sez. 17.2.3. a Quello che segue ` quindi soltanto un elenco pi` dettagliato della breve descrizione di tab. 17.13 e u sul signicato delle varie opzioni: SO_KEEPALIVE questa opzione abilita un meccanismo di verica della persistenza di una connessione associata al socket (ed ` pertanto eettiva solo sui socket che suppore tano le connessioni, ed ` usata principalmente con il TCP). Lopzione utilize
una descrizione di queste opzioni ` generalmente disponibile nella settima sezione delle pagine di manuale, nel e caso specico la si pu` consultare con man 7 socket. o
34

522

CAPITOLO 17. LA GESTIONE DEI SOCKET za per optval un intero usato come valore logico. Maggiori dettagli sul suo funzionamento sono forniti in sez. 17.2.3.

SO_OOBINLINE se questa opzione viene abilitata i dati out-of-band vengono inviati direttamente nel usso di dati del socket (e sono quindi letti con una normale read) invece che restare disponibili solo per laccesso con luso del ag MSG_OOB di recvmsg. Largomento ` trattato in dettaglio in sez. 19.1.3. Lopzione funziona soltanto e con socket che supportino i dati out-of-band (non ha senso per socket UDP ad esempio), ed utilizza per optval un intero usato come valore logico. SO_RCVLOWAT questa opzione imposta il valore che indica il numero minimo di byte che devono essere presenti nel buer di ricezione perch il kernel passi i dati allutente, e restituendoli ad una read o segnalando ad una select (vedi sez. 16.6.1) che ci sono dati in ingresso. Lopzione utilizza per optval un intero che specica il numero di byte, ma con Linux questo valore ` sempre 1 e non pu` essere e o cambiato; getsockopt legger` questo valore mentre setsockopt dar` un errore a a di ENOPROTOOPT. questa opzione imposta il valore che indica il numero minimo di byte che devono essere presenti nel buer di trasmissione perch il kernel li invii al protocollo suce cessivo, consentendo ad una write di ritornare o segnalando ad una select (vedi sez. 16.6.1) che ` possibile eseguire una scrittura. Lopzione utilizza per optval e un intero che specica il numero di byte, come per la precedente SO_RCVLOWAT con Linux questo valore ` sempre 1 e non pu` essere cambiato; getsockopt e o legger` questo valore mentre setsockopt dar` un errore di ENOPROTOOPT. a a lopzione permette di impostare un tempo massimo sulle operazioni di lettura da un socket, e prende per optval una struttura di tipo timeval (vedi g. 8.9) identica a quella usata con select. Con getsockopt si pu` leggere il valore o attuale, mentre con setsockopt si imposta il tempo voluto, usando un valore nullo per timeval il timeout viene rimosso. Se lopzione viene attivata tutte le volte che una delle funzioni di lettura (read, readv, recv, recvfrom e recvmsg) si blocca in attesa di dati per un tempo maggiore di quello impostato, essa ritorner` un valore -1 e la variabile errno sar` a a impostata con un errore di EAGAIN e EWOULDBLOCK, cos` come sarebbe avvenuto se si fosse aperto il socket in modalit` non bloccante.35 a In genere questa opzione non ` molto utilizzata se si ha a che fare con la lettura e dei dati, in quanto ` sempre possibile usare una select che consente di specie care un timeout; luso di select non consente per` di impostare il timeout per o luso di connect, per avere il quale si pu` ricorrere a questa opzione. o SO_SNDTIMEO lopzione permette di impostare un tempo massimo sulle operazioni di scrittura su un socket, ed usa gli stessi valori di SO_RCVTIMEO. In questo caso per` si avr` o a un errore di EAGAIN o EWOULDBLOCK per le funzioni di scrittura write, writev, send, sendto e sendmsg qualora queste restino bloccate per un tempo maggiore di quello specicato.

SO_SNDLOWAT

SO_RCVTIMEO

SO_BSDCOMPAT questa opzione abilita la compatibilit` con il comportamento di BSD (in particoa lare ne riproduce i bug). Attualmente ` una opzione usata solo per il protocollo e
in teoria, se il numero di byte presenti nel buer di ricezione fosse inferiore a quello specicato da SO_RCVLOWAT, leetto potrebbe essere semplicemente quello di provocare luscita delle funzioni di lettura restituendo il numero di byte no ad allora ricevuti; dato che con Linux questo valore ` sempre 1 questo caso non esiste. e
35

17.2. LE OPZIONI DEI SOCKET

523

UDP e ne ` prevista la rimozione in futuro. Lopzione utilizza per optval un e intero usato come valore logico. Quando viene abilitata gli errori riportati da messaggi ICMP per un socket UDP non vengono passati al programma in user space. Con le versioni 2.0.x del kernel erano anche abilitate altre opzioni per i socket raw, che sono state rimosse con il passaggio al 2.2; ` consigliato correggere i programmi piuttosto che usare questa e funzione. SO_PASSCRED questa opzione abilita sui socket unix-domain (vedi sez. 18.2) la ricezione dei messaggi di controllo di tipo SCM_CREDENTIALS. Prende come optval un intero usato come valore logico. questa opzione restituisce le credenziali del processo remoto connesso al socket; lopzione ` disponibile solo per socket unix-domain e pu` essere usata solo con e o getsockopt. Utilizza per optval una apposita struttura ucred (vedi sez. 18.2).

SO_PEERCRED

SO_BINDTODEVICE questa opzione permette di legare il socket ad una particolare interfaccia, in modo che esso possa ricevere ed inviare pacchetti solo su quella. Lopzione richiede per optval il puntatore ad una stringa contenente il nome dellinterfaccia (ad esempio eth0); utilizzando una stringa nulla o un valore nullo per optlen si pu` o rimuovere un precedente collegamento. Il nome della interfaccia deve essere specicato con una stringa terminata da uno zero e di lunghezza massima pari a IFNAMSIZ; lopzione ` eettiva solo per e alcuni tipi di socket, ed in particolare per quelli della famiglia AF_INET; non ` e invece supportata per i packet socket (vedi sez. 18.3.1). SO_DEBUG questa opzione abilita il debugging delle operazioni dei socket; lopzione utilizza per optval un intero usato come valore logico, e pu` essere utilizzata solo da o un processo con i privilegi di amministratore (in particolare con la capability CAP_NET_ADMIN). Lopzione necessita inoltre dellopportuno supporto nel kernel;36 quando viene abilitata una serie di messaggi con le informazioni di debug vengono inviati direttamente al sistema del kernel log.37

SO_REUSEADDR questa opzione permette di eseguire la funzione bind su indirizzi locali che siano gi` in uso da altri socket; lopzione utilizza per optval un intero usato come a valore logico. Questa opzione modica il comportamento normale dellinterfaccia dei socket che fa fallire lesecuzione della funzione bind con un errore di EADDRINUSE quando lindirizzo locale38 ` gi` in uso da parte di un altro socket. e a Maggiori dettagli sul suo funzionamento sono forniti in sez. 17.2.3. SO_TYPE questa opzione permette di leggere il tipo di socket su cui si opera; funziona solo con getsockopt, ed utilizza per optval un intero in cui verr` restituito il valore a numerico che lo identica (ad esempio SOCK_STREAM).

deve cio` essere denita la macro di preprocessore SOCK_DEBUGGING nel le include/net/sock.h dei sorgenti e del kernel, questo ` sempre vero nei kernel delle serie superiori alla 2.3, per i kernel delle serie precedenti invece e ` necessario aggiungere a mano detta denizione; ` inoltre possibile abilitare anche il tracciamento degli stati del e e TCP denendo la macro STATE_TRACE in include/net/tcp.h. 37 si tenga presente che il comportamento ` diverso da quanto avviene con BSD, dove lopzione opera solo sui e socket TCP, causando la scrittura di tutti i pacchetti inviati sulla rete su un buer circolare che viene letto da un apposito programma, trpt. 38 pi` propriamente il controllo viene eseguito sulla porta. u

36

524 SO_ACCEPTCONN

CAPITOLO 17. LA GESTIONE DEI SOCKET

questa opzione permette di rilevare se il socket su cui opera ` stato posto in e modalit` di ricezione di eventuali connessioni con una chiamata a listen. Lopa zione pu` essere usata soltanto con getsockopt e utilizza per optval un intero o in cui viene restituito 1 se il socket ` in ascolto e 0 altrimenti. e SO_DONTROUTE questa opzione forza linvio diretto dei pacchetti del socket, saltando ogni processo relativo alluso della tabella di routing del kernel. Prende per optval un intero usato come valore logico. SO_BROADCAST questa opzione abilita il broadcast; quanto abilitata i socket di tipo SOCK_DGRAM riceveranno i pacchetti inviati allindirizzo di broadcast, e potranno scrivere pacchetti su tale indirizzo. Prende per optval un intero usato come valore logico. Lopzione non ha eetti su un socket di tipo SOCK_STREAM. SO_SNDBUF questa opzione imposta la dimensione del buer di trasmissione del socket. Prende per optval un intero indicante il numero di byte. Il valore di default ed il valore massimo che si possono specicare come argomento per questa opzione sono impostabili rispettivamente tramite gli opportuni valori di sysctl (vedi sez. 17.4.1). questa opzione imposta la dimensione del buer di ricezione del socket. Prende per optval un intero indicante il numero di byte. Il valore di default ed il valore massimo che si pu` specicare come argomento per questa opzione sono o impostabili tramiti gli opportuni valori di sysctl (vedi sez. 17.4.1). Si tenga presente che nel caso di socket TCP, per entrambe le opzioni SO_RCVBUF e SO_SNDBUF, il kernel alloca eettivamente una quantit` di memoria doppia ria spetto a quanto richiesto con setsockopt. Questo comporta che una successiva lettura con getsockopt riporter` un valore diverso da quello impostato con a setsockopt. Questo avviene perch TCP necessita dello spazio in pi` per mane u tenere dati amministrativi e strutture interne, e solo una parte viene usata come buer per i dati, mentre il valore letto da getsockopt e quello riportato nei vari parametri di sysctl 39 indica la memoria eettivamente impiegata. Si tenga presente inoltre che le modiche alle dimensioni dei buer di ricezione e trasmissione, per poter essere eettive, devono essere impostate prima della chiamata alle funzioni listen o connect. SO_LINGER questa opzione controlla le modalit` con cui viene chiuso un socket quando si a utilizza un protocollo che supporta le connessioni (` pertanto usata con i socket e TCP ed ignorata per UDP) e modica il comportamento delle funzioni close e shutdown. Lopzione richiede che largomento optval sia una struttura di tipo linger, denita in sys/socket.h ed illustrata in g. 17.15. Maggiori dettagli sul suo funzionamento sono forniti in sez. 17.2.3. questa opzione permette di impostare le priorit` per tutti i pacchetti che sono a inviati sul socket, prende per optval un valore intero. Con questa opzione il kernel usa il valore per ordinare le priorit` sulle code di rete,40 i pacchetti con a priorit` pi` alta vengono processati per primi, in modalit` che dipendono dalla a u a disciplina di gestione della coda. Nel caso di protocollo IP questa opzione permette anche di impostare i valori del campo type of service (noto come TOS,

SO_RCVBUF

SO_PRIORITY

cio` wmem_max e rmem_max in /proc/sys/net/core e tcp_wmem e tcp_rmem in /proc/sys/net/ipv4, vedi e sez. 17.4.1. 40 questo richiede che sia abilitato il sistema di Quality of Service disponibile con le opzioni di routing avanzato.

39

17.2. LE OPZIONI DEI SOCKET

525

vedi sez. A.1.2) per i pacchetti uscenti. Per impostare una priorit` al di fuori a dellintervallo di valori fra 0 e 6 sono richiesti i privilegi di amministratore con la capability CAP_NET_ADMIN. SO_ERROR questa opzione riceve un errore presente sul socket; pu` essere utilizzata solo tanto con getsockopt e prende per optval un valore intero, nel quale viene restituito il codice di errore, e la condizione di errore sul socket viene cancellata. Viene usualmente utilizzata per ricevere il codice di errore, come accennato in sez. 16.6.1, quando si sta osservando il socket con una select che ritorna a causa dello stesso.

SO_ATTACH_FILTER questa opzione permette di agganciare ad un socket un ltro di pacchetti che consente di selezionare quali pacchetti, fra tutti quelli ricevuti, verranno letti. Viene usato principalmente con i socket di tipo PF_PACKET con la libreria libpcap per implementare programmi di cattura dei pacchetti, torneremo su questo in sez. 18.3.3. SO_DETACH_FILTER consente di distaccare un ltro precedentemente aggiunto ad un socket.

17.2.3

Luso delle principali opzioni dei socket

La descrizione sintetica del signicato delle opzioni generiche dei socket, riportata nellelenco in sez. 17.2.2, ` necessariamente sintetica, alcune di queste per` possono essere utilizzate per e o controllare delle funzionalit` che hanno una notevole rilevanza nella programmazione dei socket. a Per questo motivo faremo in questa sezione un approfondimento sul signicato delle opzioni generiche pi` importanti. u Lopzione SO_KEEPALIVE La prima opzione da approfondire ` SO_KEEPALIVE che permette di tenere sotto controllo lo e stato di una connessione. Una connessione infatti resta attiva anche quando non viene eettuato alcun traco su di essa; ` allora possibile, in caso di una interruzione completa della rete, che e la caduta della connessione non venga rilevata, dato che sulla stessa non passa comunque alcun traco. Se si imposta questa opzione, ` invece cura del kernel inviare degli appositi messaggi sulla rete, e detti appunto keep-alive, per vericare se la connessione ` attiva. Lopzione funziona soltanto e con i socket che supportano le connessioni (non ha senso per socket UDP ad esempio) e si applica principalmente ai socket TCP. Con le impostazioni di default (che sono riprese da BSD) Linux emette un messaggio di keep-alive 41 verso laltro capo della connessione se questa ` rimasta senza traco per pi` di due e u ore. Se ` tutto a posto il messaggio viene ricevuto e verr` emesso un segmento ACK di risposta, e a alla cui ricezione ripartir` un altro ciclo di attesa per altre due ore di inattivit`; il tutto avviene a a allinterno del kernel e le applicazioni non riceveranno nessun dato. Qualora ci siano dei problemi di rete si possono invece vericare i due casi di terminazione precoce del server gi` illustrati in sez. 16.5.3. Il primo ` quello in cui la macchina remota ha a e avuto un crollo del sistema ed ` stata riavviata, per cui dopo il riavvio la connessione non esiste e pi`.42 In questo caso allinvio del messaggio di keep-alive si otterr` come risposta un segmento u a
in sostanza un segmento ACK vuoto, cui sar` risposto con un altro segmento ACK vuoto. a si ricordi che un normale riavvio o il crollo dellapplicazione non ha questo eetto, in quanto in tal caso si passa sempre per la chiusura del processo, e questo, come illustrato in sez. 6.2.2, comporta anche la regolare chiusura del socket con linvio di un segmento FIN allaltro capo della connessione.
42 41

526

CAPITOLO 17. LA GESTIONE DEI SOCKET

RST che indica che laltro capo non riconosce pi` lesistenza della connessione ed il socket verr` u a chiuso riportando un errore di ECONNRESET. Se invece non viene ricevuta nessuna risposta (indice che la macchina non ` pi` raggiungibile) e u lemissione dei messaggi viene ripetuta ad intervalli di 75 secondi per un massimo di 9 volte43 (per un totale di 11 minuti e 15 secondi) dopo di che, se non si ` ricevuta nessuna risposta, e il socket viene chiuso dopo aver impostato un errore di ETIMEDOUT. Qualora la connessione si sia ristabilita e si riceva un successivo messaggio di risposta il ciclo riparte come se niente fosse avvenuto. Inne se si riceve come risposta un pacchetto ICMP di destinazione irraggiungibile (vedi sez. A.3), verr` restituito lerrore corrispondente. a In generale questa opzione serve per individuare una caduta della connessione anche quando non si sta facendo traco su di essa. Viene usata principalmente sui server per evitare di mantenere impegnate le risorse che verrebbero dedicate a trattare delle connessioni che in realt` sono a gi` terminate (quelle che vengono anche chiamate connessioni semi-aperte); in tutti quei casi a cio` in cui il server si trova in attesa di dati in ingresso su una connessione che non arriveranno e mai o perch il client sullaltro capo non ` pi` attivo o perch non ` pi` in grado di comunicare e e u e e u con il server via rete.
int main ( int argc , char * argv []) { 3 /* 4 * Variables definition 5 */ 6 int list_fd , conn_fd ; 7 int waiting = 0; 8 int keepalive = 0; 9 ... 10 ...
1 2 11 12 13 14 15 16 17 18 19 20

if ( pid == 0) { /* child */ close ( list_fd ); /* close listening socket */ if ( keepalive ) { /* enable keepalive ? */ setsockopt ( conn_fd , SOL_SOCKET , SO_KEEPALIVE , & keepalive , sizeof ( keepalive )); } ServEcho ( conn_fd ); /* handle echo */ ... }

Figura 17.12: La sezione della nuova versione del server del servizio echo che prevede lattivazione del keepalive sui socket.

Abilitandola dopo un certo tempo le connessioni eettivamente terminate verranno comunque chiuse per cui, utilizzando ad esempio una select, se be potr` rilevare la conclusione e ricevere a il relativo errore. Si tenga presente per` che non pu` avere la certezza assoluta che un errore o o di ETIMEDOUT ottenuto dopo aver abilitato questa opzione corrisponda necessariamente ad una reale conclusione della connessione, il problema potrebbe anche essere dovuto ad un problema di routing che perduri per un tempo maggiore di quello impiegato nei vari tentativi di ritrasmissione del keep-alive (anche se questa non ` una condizione molto probabile). e Come esempio dellutilizzo di questa opzione introduciamo allinterno del nostro server per il servizio echo la nuova opzione -k che permette di attivare il keep-alive sui socket; tralasciando la parte relativa alla gestione di detta opzione (che si limita ad assegnare ad 1 la variabile
entrambi questi valori possono essere modicati a livello di sistema (cio` per tutti i socket) con gli opportuni e parametri illustrati in sez. 17.4.1 ed a livello di singolo socket con le opzioni TCP_KEEP* di sez. 17.2.5.
43

17.2. LE OPZIONI DEI SOCKET

527

keepalive) tutte le modiche al server sono riportate in g. 17.12. Al solito il codice completo ` contenuto nel le TCP_echod_fourth.c dei sorgenti allegati alla guida. e Come si pu` notare la variabile keepalive ` preimpostata (8) ad un valore nullo; essa viene o e utilizzata sia come variabile logica per la condizione (14) che controlla lattivazione del keepalive che come valore dellargomento optval della chiamata a setsockopt (16). A seconda del suo valore tutte le volte che un processo glio viene eseguito in risposta ad una connessione verr` pertanto eseguita o meno la sezione (14-17) che esegue limpostazione di SO_KEEPALIVE a sul socket connesso, attivando il relativo comportamento. Lopzione SO_REUSEADDR La seconda opzione da approfondire ` SO_REUSEADDR, che consente di eseguire bind su un socket e anche quando la porta specicata ` gi` in uso da parte di un altro socket. Si ricordi infatti che, e a come accennato in sez. 16.2.1, normalmente la funzione bind fallisce con un errore di EADDRINUSE se la porta scelta ` gi` utilizzata da un altro socket, proprio per evitare che possano essere lanciati e a due server sullo stesso indirizzo e la stessa porta, che verrebbero a contendersi i pacchetti aventi quella destinazione. Esistono per` situazioni ed esigenze particolari in cui non si vuole che questo comportamento o di salvaguardia accada, ed allora si pu` fare ricorso a questa opzione. La questione ` comunque o e abbastanza complessa in quanto, come sottolinea Stevens in [2], si distinguono ben quattro casi diversi in cui ` prevista la possibilit` di un utilizzo di questa opzione, il che la rende una delle e a pi` dicili da capire. u Il primo caso, che ` anche il pi` comune, in cui si fa ricorso a SO_REUSEADDR ` quello in e u e cui un server ` terminato ma esistono ancora dei processi gli che mantengono attiva almeno e una connessione remota che utilizza lindirizzo locale, mantenendo occupata la porta. Quando si riesegue il server allora questo riceve un errore sulla chiamata a bind dato che la porta ` e 44 Inoltre se si usa il protocollo TCP questo pu` ancora utilizzata in una connessione esistente. o avvenire anche dopo tutti i processi gli sono terminati, dato che una connessione pu` restare o attiva anche dopo la chiusura del socket, mantenendosi nello stato TIME_WAIT (vedi sez. 16.1.5). Usando SO_REUSEADDR fra la chiamata a socket e quella a bind si consente a questultima di ` avere comunque successo anche se la connessione ` attiva (o nello stato TIME_WAIT). E bene per` e o ricordare (si riveda quanto detto in sez. 16.1.5) che la presenza dello stato TIME_WAIT ha una ragione, ed infatti se si usa questa opzione esiste sempre una probabilit`, anche se estremamente a 45 che eventuali pacchetti rimasti intrappolati in una precedente connessione possano remota, nire fra quelli di una nuova. Come esempio di uso di questa connessione abbiamo predisposto una nuova versione della funzione sockbind (vedi g. 17.10) che consenta limpostazione di questa opzione. La nuova funzione ` sockbindopt, e le principali dierenze rispetto alla precedente sono illustrate in e g. 17.13, dove si sono riportate le sezioni di codice modicate rispetto alla versione precedente. Il codice completo della funzione si trova, insieme alle altre funzioni di servizio dei socket, allinterno del le SockUtils.c dei sorgenti allegati alla guida. In realt` tutto quello che si ` fatto ` stato introdurre nella nuova funzione (1) un nuovo a e e argomento intero, reuse, che conterr` il valore logico da usare nella successiva chiamata (14) a a setsockopt. Si ` poi aggiunta una sezione (13-17) che esegue limpostazione dellopzione fra la e chiamata a socket e quella a bind.
44 questa ` una delle domande pi` frequenti sui newsgroup dedicati allo sviluppo, in quanto ` piuttosto comune e u e trovarsi in questa situazione quando si sta sviluppando un server che si ferma e si riavvia in continuazione dopo aver fatto modiche. 45 perch ci` avvenga infatti non solo devono coincidere gli indirizzi IP e le porte degli estremi della nuova e o connessione, ma anche i numeri di sequenza dei pacchetti, e questo ` estremamente improbabile. e

528

CAPITOLO 17. LA GESTIONE DEI SOCKET

int sockbindopt ( char * host , char * serv , int prot , int type , int reuse ) { 3 struct addrinfo hint , * addr , * save ; 4 int res ; 5 int sock ; 6 char buf [ INET6_ADDRSTRLEN ]; 7 ... 8 while ( addr != NULL ) { /* loop on possible addresses */ 9 /* get a socket */ 10 sock = socket ( addr - > ai_family , addr - > ai_socktype , addr - > ai_protocol ); 11 ... 12 /* connect the socket */ 13 if ( setsockopt ( sock , SOL_SOCKET , SO_REUSEADDR , 14 & reuse , sizeof ( reuse ))) { 15 printf ( " error on socket options \ n " ); 16 return -1; 17 } 18 ...
1 2 19 20 21

return sock ; }

Figura 17.13: Le sezioni della funzione sockbindopt modicate rispetto al codice della precedente sockbind.

A questo punto baster` modicare il server per utilizzare la nuova funzione; in g. 17.14 a abbiamo riportato le sezioni modicate rispetto alla precedente versione di g. 17.11. Al solito il codice completo ` coi sorgenti allegati alla guida, nel le TCP_echod_fifth.c. e Anche in questo caso si ` introdotta (8) una nuova variabile reuse che consente di controllare e luso dellopzione e che poi sar` usata (14) come ultimo argomento di setsockopt. Il valore di a default di questa variabile ` nullo, ma usando lopzione -r nellinvocazione del server (al solito e la gestione delle opzioni non ` riportata in g. 17.14) se ne potr` impostare ad 1 il valore, per e a cui in tal caso la successiva chiamata (13-17) a setsockopt attiver` lopzione SO_REUSEADDR. a
int main ( int argc , char * argv []) { 3 /* 4 * Variables definition 5 */ 6 int list_fd , conn_fd ; 7 int keepalive = 0; 8 int reuse = 0; 9 ... 10 /* create and bind socket */ 11 if ( ( list_fd = sockbindopt ( argv [ optind ] , " echo " , 6 , 12 SOCK_STREAM , reuse )) < 0) { 13 return 1; 14 } 15 ... 16 /* normal exit , never reached */ 17 exit (0); 18 }
1 2

Figura 17.14: Il nuovo codice per lapertura passiva del server echo che usa la nuova funzione sockbindopt.

Il secondo caso in cui viene usata SO_REUSEADDR ` quando si ha una macchina cui sono e

17.2. LE OPZIONI DEI SOCKET

529

assegnati diversi numeri IP (o come suol dirsi multi-homed ) e si vuole porre in ascolto sulla stessa porta un programma diverso (o una istanza diversa dello stesso programma) per indirizzi IP diversi. Si ricordi infatti che ` sempre possibile indicare a bind di collegarsi solo su di un e indirizzo specico; in tal caso se un altro programma cerca di riutilizzare la stessa porta (anche specicando un indirizzo diverso) otterr` un errore, a meno di non aver preventivamente a impostato SO_REUSEADDR. Usando questa opzione diventa anche possibile eseguire bind sullindirizzo generico, e questo permetter` il collegamento per tutti gli indirizzi (di quelli presenti) per i quali la porta non risulti a occupata da una precedente chiamata pi` specica. Inne si tenga presente che con il protocollo u TCP non ` mai possibile far partire server che eseguano bind sullo stesso indirizzo e la stessa e porta, cio` ottenere quello che viene chiamato un completely duplicate binding. e Il terzo impiego ` simile al precedente e prevede luso di bind allinterno dello stesso proe gramma per associare indirizzi locali diversi a socket diversi. In genere questo viene fatto per i socket UDP quando ` necessario ottenere lindirizzo a cui sono rivolte le richieste del client ed e il sistema non supporta lopzione IP_RECVDSTADDR;46 in tale modo si pu` sapere a quale socket o corrisponde un certo indirizzo. Non ha senso fare questa operazione per un socket TCP dato che su di essi si pu` sempre invocare getsockname una volta che si ` completata la connessione. o e Inne il quarto caso ` quello in cui si vuole eettivamente ottenere un completely duplicate e binding, quando cio` si vuole eseguire bind su un indirizzo ed una porta che sono gi` legati ad e a un altro socket. Questo ovviamente non ha senso per il normale traco di rete, in cui i pacchetti vengono scambiati direttamente fra due applicazioni; ma quando un sistema supporta il traco in multicast, in cui una applicazione invia i pacchetti a molte altre (vedi sez. ??), allora ha senso che su una macchina i pacchetti provenienti dal traco in multicast possano essere ricevuti da pi` applicazioni47 o da diverse istanze della stessa applicazione. u In questo caso utilizzando SO_REUSEADDR si consente ad una applicazione eseguire bind sulla stessa porta ed indirizzo usata da unaltra, cos` che anche essa possa ricevere gli stessi pacchetti (chiaramente la cosa non ha alcun senso per i socket TCP, ed infatti in questo tipo di applicazione ` normale luso del protocollo UDP). La regola ` che quando si hanno pi` applicazioni che hanno e e u eseguito bind sulla stessa porta, di tutti pacchetti destinati ad un indirizzo di broadcast o di multicast viene inviata una copia a ciascuna applicazione. Non ` denito invece cosa accade e qualora il pacchetto sia destinato ad un indirizzo normale (unicast). Essendo questo un caso particolare in alcuni sistemi (come BSD) ` stata introdotta una e opzione ulteriore, SO_REUSEPORT che richiede che detta opzione sia specicata per tutti i socket per i quali si vuole eseguire il completely duplicate binding. Nel caso di Linux questa opzione non esiste, ma il comportamento di SO_REUSEADDR ` analogo, sar` cio` possibile eettuare un e a e completely duplicate binding ed ottenere il successo di bind su un socket legato allo stesso indirizzo e porta solo se il programma che ha eseguito per primo bind su di essi ha impostato questa opzione.48

nel caso di Linux questa opzione ` stata supportata per in certo periodo nello sviluppo del kernel 2.1.x, ma ` e e in seguito stata soppiantata dalluso di IP_PKTINFO (vedi sez. 17.2.4). 47 lesempio classico di traco in multicast ` quello di uno streaming di dati (audio, video, ecc.), luso del e multicast consente in tal caso di trasmettere un solo pacchetto, che potr` essere ricevuto da tutti i possibili a destinatari (invece di inviarne un duplicato a ciascuno); in questo caso ` perfettamente logico aspettarsi che sulla e stessa macchina pi` utenti possano lanciare un programma che permetta loro di ricevere gli stessi dati. u 48 questa restrizione permette di evitare il cosiddetto port stealing, in cui un programma, usando SO_REUSEADDR, pu` collegarsi ad una porta gi` in uso e ricevere i pacchetti destinati ad un altro programma; con queo a sta caratteristica ci` ` possibile soltanto se il primo programma a consentirlo, avendo usato n dallinizio o e SO_REUSEADDR.

46

530 Lopzione SO_LINGER

CAPITOLO 17. LA GESTIONE DEI SOCKET

La terza opzione da approfondire ` SO_LINGER; essa, come il nome suggerisce, consente di indue giare nella chiusura di un socket. Il comportamento standard sia di close che shutdown ` infatti e quello di terminare immediatamente dopo la chiamata, mentre il procedimento di chiusura della connessione (o di un lato di essa) ed il rispettivo invio sulla rete di tutti i dati ancora presenti nei buer, viene gestito in sottofondo dal kernel.
struct linger { int l_onoff ; int l_linger ; }

/* Nonzero to linger on close . */ /* Time to linger ( in seconds ). */

Figura 17.15: La struttura linger richiesta come valore dellargomento optval per limpostazione dellopzione dei socket SO_LINGER.

Luso di SO_LINGER con setsockopt permette di modicare (ed eventualmente ripristinare) questo comportamento in base ai valori passati nei campi della struttura linger, illustrata in g. 17.15. Fintanto che il valore del campo l_onoff di linger ` nullo la modalit` che viene e a impostata (qualunque sia il valore di l_linger) ` quella standard appena illustrata; questa e combinazione viene utilizzata per riportarsi al comportamento normale qualora esso sia stato cambiato da una precedente chiamata. Se si utilizza un valore di l_onoff diverso da zero, il comportamento alla chiusura viene a dipendere dal valore specicato per il campo l_linger; se questultimo ` nullo luso delle e funzioni close e shutdown provoca la terminazione immediata della connessione: nel caso di TCP cio` non viene eseguito il procedimento di chiusura illustrato in sez. 16.1.3, ma tutti i e dati ancora presenti nel buer vengono immediatamente scartati e sulla rete viene inviato un segmento di RST che termina immediatamente la connessione. Un esempio di questo comportamento si pu` abilitare nel nostro client del servizio echo o utilizzando lopzione -r; riportiamo in g. 17.16 la sezione di codice che permette di introdurre questa funzionalit`,; al solito il codice completo ` disponibile nei sorgenti allegati. a e
... /* check if resetting on close is required */ if ( reset ) { printf ( " Setting reset on close \ n " ); ling . l_onoff = 1; ling . l_linger = 0; if ( setsockopt ( sock , SOL_SOCKET , SO_LINGER , & ling , sizeof ( ling ))) { perror ( " Cannot set linger " ); exit (1); } } ...

1 2 3 4 5 6 7 8 9 10 11 12

Figura 17.16: La sezione del codice del client echo che imposta la terminazione immediata della connessione in caso di chiusura.

La sezione indicata viene eseguita dopo aver eettuato la connessione e prima di chiamare la funzione di gestione, cio` fra le righe (12) e (13) del precedente esempio di g. 17.9. Il codice si e limita semplicemente a controllare (3) il valore della variabile reset che assegnata nella gestione

17.2. LE OPZIONI DEI SOCKET

531

delle opzioni in corrispondenza alluso di -r nella chiamata del client. Nel caso questa sia diversa da zero vengono impostati (5-6) i valori della struttura ling che permettono una terminazione immediata della connessione. Questa viene poi usata nella successiva (7) chiamata a setsockopt. Al solito si controlla (7-10) il valore di ritorno e si termina il programma in caso di errore, stampandone il valore. Inne lultima possibilit`, quella in cui si utilizza eettivamente SO_LINGER per indugiare a nella chiusura, ` quella in cui sia l_onoff che l_linger hanno un valore diverso da zero. Se e si esegue limpostazione con questi valori sia close che shutdown si bloccano, nel frattempo viene eseguita la normale procedura di conclusione della connessione (quella di sez. 16.1.3) ma entrambe le funzioni non ritornano ntanto che non si sia concluso il procedimento di chiusura della connessione, o non sia passato un numero di secondi49 pari al valore specicato in l_linger.

17.2.4

Le opzioni per il protocollo IPv4

Il secondo insieme di opzioni dei socket che tratteremo ` quello relativo ai socket che usano il e protocollo IPv4.50 Se si vuole operare su queste opzioni generiche il livello da utilizzare ` SOL_IP e (o lequivalente IPPROTO_IP); si ` riportato un elenco di queste opzioni in tab. 17.14. Le costanti e indicanti le opzioni e tutte le altre costanti ad esse collegate sono denite in netinet/ip.h, ed accessibili includendo detto le.
Opzione IP_OPTIONS IP_PKTINFO IP_RECVTOS IP_RECVTTL IP_RECVOPTS IP_RETOPTS IP_TOS IP_TTL IP_HDRINCL IP_RECVERR IP_MTU_DISCOVER IP_MTU IP_ROUTER_ALERT IP_MULTICAST_TTL IP_MULTICAST_LOOP IP_ADD_MEMBERSHIP IP_DROP_MEMBERSHIP IP_MULTICAST_IF get set ag Tipo void * int int int int int int int int int int int int int int ip_mreqn ip_mreqn ip_mreqn Descrizione Imposta o riceve le opzioni di IP. Passa un messaggio di informazione. Passa un messaggio col campo TOS. Passa un messaggio col campo TTL. Passa un messaggio con le opzioni IP. Passa un messaggio con le opzioni IP non trattate. Imposta il valore del campo TOS. Imposta il valore del campo TTL. Passa lintestazione di IP nei dati. Abilita la gestione degli errori. Imposta il Path MTU Discovery. Legge il valore attuale della MTU. Imposta lopzione IP router alert sui pacchetti. Imposta il TTL per i pacchetti multicast. Controlla il reinvio a se stessi dei dati di multicast. Si unisce a un gruppo di multicast. Si sgancia da un gruppo di multicast. Imposta linterfaccia locale di un socket multicast.

Tabella 17.14: Le opzioni disponibili al livello SOL_IP.

Le descrizioni riportate in tab. 17.14 sono estremamente succinte, una maggiore quantit` di a dettagli sulle varie opzioni ` fornita nel seguente elenco: e IP_OPTIONS lopzione permette di impostare o leggere le opzioni del protocollo IP (si veda sez. A.1.3). Lopzione prende come valore dellargomento optval un puntatore ad un buer dove sono mantenute le opzioni, mentre optlen indica la dimensione di questultimo. Quando la si usa con getsockopt vengono lette le opzioni IP utilizzate per la spedizione, quando la si usa con setsockopt vengono impostate le opzioni specicate. Luso di questa opzione richiede una profonda

49 questa ` lunit` di misura indicata da POSIX ed adottata da Linux, altri kernel possono usare unit` di misura e a a diverse, oppure usare il campo l_linger come valore logico (ignorandone il valore) per rendere (quando diverso da zero) close e shutdown bloccanti no al completamento della trasmissione dei dati sul buer. 50 come per le precedenti opzioni generiche una descrizione di esse ` disponibile nella settima sezione delle pagine e di manuale, nel caso specico la documentazione si pu` consultare con man 7 ip. o

532

CAPITOLO 17. LA GESTIONE DEI SOCKET conoscenza del funzionamento del protocollo, torneremo in parte sullargomento in sez. 19.2.1.

IP_PKTINFO

Quando abilitata lopzione permette di ricevere insieme ai pacchetti un messaggio ancillare (vedi sez. 19.1.2) di tipo IP_PKTINFO contenente una struttura pktinfo (vedi g. 17.17) che mantiene una serie di informazioni riguardo i pacchetti in arrivo. In particolare ` possibile conoscere linterfaccia su cui ` stato e e 51 lindirizzo locale da esso utiricevuto un pacchetto (nel campo ipi_ifindex), lizzato (nel campo ipi_spec_dst) e lindirizzo remoto dello stesso (nel campo ipi_addr).

struct in_pktinfo { unsigned int ipi_ifindex ; /* Interface index */ struct in_addr ipi_spec_dst ; /* Local address */ struct in_addr ipi_addr ; /* Header Destination address */ };

Figura 17.17: La struttura pktinfo usata dallopzione IP_PKTINFO per ricavare informazioni sui pacchetti di un socket di tipo SOCK_DGRAM.

Lopzione ` utilizzabile solo per socket di tipo SOCK_DGRAM. Questa ` una opzione e e introdotta con i kernel della serie 2.2.x, ed ` specica di Linux;52 essa permette e di sostituire le opzioni IP_RECVDSTADDR e IP_RECVIF presenti in altri Unix (la relativa informazione ` quella ottenibile rispettivamente dai campi ipi_addr e e ipi_ifindex di pktinfo). Lopzione prende per optval un intero usato come valore logico, che specica soltanto se insieme al pacchetto deve anche essere inviato o ricevuto il messaggio IP_PKTINFO (vedi sez. 19.1.2); il messaggio stesso dovr` poi essere letto o scritto a direttamente con recvmsg e sendmsg (vedi sez. 19.1.1). IP_RECVTOS Quando abilitata lopzione permette di ricevere insieme ai pacchetti un messaggio ancillare (vedi sez. 19.1.2) di tipo IP_TOS, che contiene un byte con il valore del campo Type of Service dellintestazione IP del pacchetto stesso (vedi sez. A.1.2). Prende per optval un intero usato come valore logico. Quando abilitata lopzione permette di ricevere insieme ai pacchetti un messaggio ancillare (vedi sez. 19.1.2) di tipo IP_RECVTTL, contenente un byte con il valore del campo Time to Live dellintestazione IP (vedi sez. A.1.2). Lopzione richiede per optval un intero usato come valore logico. Lopzione non ` e supportata per socket di tipo SOCK_STREAM. Quando abilitata lopzione permette di ricevere insieme ai pacchetti un messaggio ancillare (vedi sez. 19.1.2) di tipo IP_OPTIONS, contenente le opzioni IP del protocollo (vedi sez. A.1.3). Le intestazioni di instradamento e le altre opzioni sono gi` riempite con i dati locali. Lopzione richiede per optval un intero usato a come valore logico. Lopzione non ` supportata per socket di tipo SOCK_STREAM. e Identica alla precedente IP_RECVOPTS, ma in questo caso restituisce i dati grezzi delle opzioni, senza che siano riempiti i capi di instradamento e le marche

IP_RECVTTL

IP_RECVOPTS

IP_RETOPTS
51 52

in questo campo viene restituito il valore numerico dellindice dellinterfaccia, sez. 17.3.2. non dovrebbe pertanto essere utilizzata se si ha a cuore la portabilit`. a

17.2. LE OPZIONI DEI SOCKET

533

temporali. Lopzione richiede per optval un intero usato come valore logico. Lopzione non ` supportata per socket di tipo SOCK_STREAM. e IP_TOS Lopzione consente di leggere o impostare il campo Type of Service dellintestazione IP (vedi sez. A.1.2) che permette di indicare le priorit` dei pacchetti. Se a impostato il valore verr` mantenuto per tutti i pacchetti del socket; alcuni valori a (quelli che aumentano la priorit`) richiedono i privilegi di amministrazione con a la capability CAP_NET_ADMIN. Il campo TOS ` di 8 bit e lopzione richiede per optval un intero che ne cone tenga il valore. Sono denite anche alcune costanti che deniscono alcuni valori standardizzati per il Type of Service, riportate in tab. A.4, il valore di default usato da Linux ` IPTOS_LOWDELAY, ma esso pu` essere modicato con le funzioe o nalit` del cosiddetto Advanced Routing. Si ricordi che la priorit` dei pacchetti a a pu` essere impostata anche in maniera indipendente dal protocollo utilizzando o lopzione SO_PRIORITY illustrata in sez. 17.2.2. IP_TTL Lopzione consente di leggere o impostare il campo Time to Live dellintestazione IP (vedi sez. A.1.2) per tutti i pacchetti associati al socket. Il campo TTL ` di e 8 bit e lopzione richiede che optval sia un intero, che ne conterr` il valore. a Se abilitata lutente deve fornire lui stesso lintestazione IP in cima ai propri dati. Lopzione ` valida soltanto per socket di tipo SOCK_RAW, e quando utilizzata evene tuali valori impostati con IP_OPTIONS, IP_TOS o IP_TTL sono ignorati. In ogni caso prima della spedizione alcuni campi dellintestazione vengono comunque modicati dal kernel, torneremo sullargomento in sez. 18.3.1 Questa ` una opzione introdotta con i kernel della serie 2.2.x, ed ` specica di e e Linux. Essa permette di usufruire di un meccanismo adabile per ottenere un maggior numero di informazioni in caso di errori. Se lopzione ` abilitata tutti e gli errori generati su un socket vengono memorizzati su una coda, dalla quale poi possono essere letti con recvmsg (vedi sez. 19.1.1) come messaggi ancillari (torneremo su questo in sez. 19.1.2) di tipo IP_RECVERR. Lopzione richiede per optval un intero usato come valore logico e non ` applicabile a socket di tipo e SOCK_STREAM.

IP_HDRINCL

IP_RECVERR

IP_MTU_DISCOVER Questa ` una opzione introdotta con i kernel della serie 2.2.x, ed ` specica di e e Linux. Lopzione permette di scrivere o leggere le impostazioni della modalit` a usata per la determinazione della Path Maximum Transfer Unit (vedi sez. 14.3.5) del socket. Lopzione prende per optval un valore intero che indica la modalit` a usata, da specicare con una delle costanti riportate in tab. 17.15.
Valore IP_PMTUDISC_DONT IP_PMTUDISC_WANT IP_PMTUDISC_DO 0 1 2 Signicato Non eettua la ricerca dalla Path MTU. Utilizza il valore impostato per la rotta utilizzata dai pacchetti (dal comando route). Esegue la procedura di determinazione della Path MTU come richiesto dallRFC 1191.

Tabella 17.15: Valori possibili per largomento optval di IP_MTU_DISCOVER.

Il valore di default applicato ai socket di tipo SOCK_STREAM ` determinato dal e parametro ip_no_pmtu_disc (vedi sez. 17.4.1), mentre per tutti gli altri socket di default la ricerca ` disabilitata ed ` responsabilit` del programma creare e e a

534

CAPITOLO 17. LA GESTIONE DEI SOCKET pacchetti di dimensioni appropriate e ritrasmettere eventuali pacchetti persi. Se lopzione viene abilitata, il kernel si incaricher` di tenere traccia automaticaa mente della Path MTU verso ciascuna destinazione, e riuter` immediatamente a la trasmissione di pacchetti di dimensioni maggiori della MTU con un errore di EMSGSIZE.53

IP_MTU

Permette di leggere il valore della Path MTU di percorso del socket. Lopzione richiede per optval un intero che conterr` il valore della Path MTU in byte. a Questa ` una opzione introdotta con i kernel della serie 2.2.x, ed ` specica di e e Linux. ` E tramite questa opzione che un programma pu` leggere, quando si ` avuto un o e errore di EMSGSIZE, il valore della MTU corrente del socket. Si tenga presente che per poter usare questa opzione, oltre ad avere abilitato la scoperta della Path MTU, occorre che il socket sia stato esplicitamente connesso con connect. Ad esempio con i socket UDP si potr` ottenere una stima iniziale della Paa th MTU eseguendo prima una connect verso la destinazione, e poi usando getsockopt con questa opzione. Si pu` anche avviare esplicitamente il proo cedimento di scoperta inviando un pacchetto di grosse dimensioni (che verr` a scartato) e ripetendo linvio coi dati aggiornati. Si tenga inne conto che durante il procedimento i pacchetti iniziali possono essere perduti, ed ` compito e dellapplicazione gestirne una eventuale ritrasmissione.

IP_ROUTER_ALERT Questa ` una opzione introdotta con i kernel della serie 2.2.x, ed ` specica di e e Linux. Prende per optval un intero usato come valore logico. Se abilitata passa tutti i pacchetti con lopzione IP Router Alert (vedi sez. A.1.3) che devono essere inoltrati al socket corrente. Pu` essere usata soltanto per socket di tipo raw. o IP_MULTICAST_TTL Lopzione permette di impostare o leggere il valore del campo TTL per i pac` chetti multicast in uscita associati al socket. E importante che questo valore sia il pi` basso possibile, ed il default ` 1, che signica che i pacchetti non potranno u e uscire dalla rete locale. Questa opzione consente ai programmi che lo richiedono di superare questo limite. Lopzione richiede per optval un intero che conterr` a il valore del TTL. IP_MULTICAST_LOOP Lopzione consente di decidere se i dati che si inviano su un socket usato con il multicast vengano ricevuti anche sulla stessa macchina da cui li si stanno inviando. Prende per optval un intero usato come valore logico. In generale se si vuole che eventuali client possano ricevere i dati che si inviano occorre che questa funzionalit` sia abilitata (come avviene di default). Qualora a per` non si voglia generare traco per dati che gi` sono disponibili in locale o a luso di questa opzione permette di disabilitare questo tipo di traco. IP_ADD_MEMBERSHIP Lopzione consente di unirsi ad gruppo di multicast, e pu` essere usata solo con o setsockopt. Largomento optval in questo caso deve essere una struttura di tipo ip_mreqn, illustrata in g. 17.18, che permette di indicare, con il campo imr_multiaddr lindirizzo del gruppo di multicast a cui ci si vuole unire, con
in caso contrario la trasmissione del pacchetto sarebbe eettuata, ottenendo o un fallimento successivo della trasmissione, o la frammentazione dello stesso.
53

17.2. LE OPZIONI DEI SOCKET

535

il campo imr_address lindirizzo dellinterfaccia locale con cui unirsi al gruppo di multicast e con imr_ifindex lindice dellinterfaccia da utilizzare (un valore nullo indica una interfaccia qualunque). Per compatibilit` ` possibile utilizzare anche un argomento di tipo ip_mreq, una ae precedente versione di ip_mreqn, che dierisce da essa soltanto per lassenza del campo imr_ifindex.

struct ip_mreqn { struct in_addr imr_multiaddr ; /* IP multicast group address */ struct in_addr imr_address ; /* IP address of local interface */ int imr_ifindex ; /* interface index */ };

Figura 17.18: La struttura ip_mreqn utilizzata dalle opzioni dei socket per le operazioni concernenti lappartenenza ai gruppi di multicast.

IP_DROP_MEMBERSHIP Lascia un gruppo di multicast, prende per optval la stessa struttura ip_mreqn (o ip_mreq) usata anche per IP_ADD_MEMBERSHIP. IP_MULTICAST_IF Imposta linterfaccia locale per lutilizzo del multicast, ed utilizza come optval le stesse strutture ip_mreqn o ip_mreq delle due precedenti opzioni.

17.2.5

Le opzioni per i protocolli TCP e UDP

In questa sezione tratteremo le varie opzioni disponibili per i socket che usano i due principali protocolli di comunicazione del livello di trasporto; UDP e TCP.54 Dato che questi due protocolli sono entrambi trasportati su IP,55 oltre alle opzioni generiche di sez. 17.2.2 saranno comunque disponibili anche le precedenti opzioni di sez. 17.2.4.56 Il protocollo che supporta il maggior numero di opzioni ` TCP; per poterle utilizzare occorre e specicare SOL_TCP (o lequivalente IPPROTO_TCP) come valore per largomento level. Si sono riportate le varie opzioni disponibili in tab. 17.16, dove sono elencate le rispettive costanti da utilizzare come valore per largomento optname. Dette costanti e tutte le altre costanti e strutture collegate alluso delle opzioni TCP sono denite in netinet/tcp.h, ed accessibili includendo detto le.57 Le descrizioni delle varie opzioni riportate in tab. 17.16 sono estremamente sintetiche ed indicative, la spiegazione del funzionamento delle singole opzioni con una maggiore quantit` di a dettagli ` fornita nel seguente elenco: e TCP_NODELAY il protocollo TCP utilizza un meccanismo di buerizzazione dei dati uscenti, per evitare la trasmissione di tanti piccoli segmenti con un utilizzo non ottimale della

come per le precedenti, una descrizione di queste opzioni ` disponibile nella settima sezione delle pagine di e manuale, che si pu` consultare rispettivamente con man 7 tcp e man 7 udp; le pagine di manuale per`, alla stesura o o di questa sezione (Agosto 2006) sono alquanto incomplete. 55 qui si sottintende IPv4, ma le opzioni per TCP e UDP sono le stesse anche quando si usa IPv6. 56 in realt` in sez. 17.2.4 si sono riportate le opzioni per IPv4, al solito, qualora si stesse utilizzando IPv6, si a potrebbero utilizzare le opzioni di questultimo. 57 in realt` questo ` il le usato dalle librerie; la denizione delle opzioni eettivamente supportate da Linux si a e trova nel le linux/tcp.h, dal quale si sono estratte le costanti di tab. 17.16.

54

536
Opzione TCP_NODELAY TCP_MAXSEG TCP_CORK TCP_KEEPIDLE TCP_KEEPINTVL TCP_KEEPCNT TCP_SYNCNT TCP_LINGER2 TCP_DEFER_ACCEPT TCP_WINDOW_CLAMP TCP_INFO TCP_QUICKACK TCP_CONGESTION get set ag Tipo int int int int int int int int int int tcp_info int char *

CAPITOLO 17. LA GESTIONE DEI SOCKET


Descrizione Spedisce immediatamente i dati in segmenti singoli. Valore della MSS per i segmenti in uscita. Accumula i dati in un unico segmento. Tempo in secondi prima di inviare un keepalive. Tempo in secondi prima fra keepalive successivi. Numero massimo di keepalive inviati. Numero massimo di ritrasmissioni di un SYN. Tempo di vita in stato FIN_WAIT2. Ritorna da accept solo in presenza di dati. Valore della advertised window. Restituisce informazioni sul socket. Abilita la modalit` quickack. a Imposta lalgoritmo per il controllo della congestione.

Tabella 17.16: Le opzioni per i socket TCP disponibili al livello SOL_TCP.

banda disponibile.58 Questo meccanismo ` controllato da un apposito algoritmo e (detto algoritmo di Nagle, vedi sez. ??). Il comportamento normale del protocollo prevede che i dati siano accumulati ntanto che non si raggiunge una quantit` a considerata adeguata per eseguire la trasmissione di un singolo segmento. Ci sono per` delle situazioni in cui questo comportamento pu` non essere desio o derabile, ad esempio quando si sa in anticipo che lapplicazione invier` soltanto a 59 in tal caso lattesa introdotta dallalgoritmo di un piccolo quantitativo di dati; buerizzazione non soltanto ` inutile, ma peggiora le prestazioni introducendo e un ritardo. Impostando questa opzione si disabilita luso dellalgoritmo di Nagle ed i dati vengono inviati immediatamente in singoli segmenti, qualunque sia la loro dimensione. Ovviamente luso di questa opzione ` dedicato a chi ha esigenze e particolari come quella illustrata, che possono essere stabilite solo per la singola applicazione. Si tenga conto che questa opzione viene sovrascritta dalleventuale impostazione dellopzione TCP_CORK (il cui scopo ` sostanzialmente lopposto) che blocca e linvio immediato. Tuttavia quando la si abilita viene sempre forzato lo scaricamento della coda di invio (con conseguente trasmissione di tutti i dati pendenti), anche qualora si fosse gi` abilitata TCP_CORK.60 a TCP_MAXSEG con questa opzione si legge o si imposta il valore della MSS (Maximum Segment Size, vedi sez. 14.3.5 e sez. ??) dei segmenti TCP uscenti. Se lopzione ` e impostata prima di stabilire la connessione, si cambia anche il valore della MSS annunciata allaltro capo della connessione. Se si specicano valori maggiori della MTU questi verranno ignorati, inoltre TCP imporr` anche i suoi limiti massimo a e minimo per questo valore. questa opzione ` il complemento naturale di TCP_NODELAY e serve a gestire a e livello applicativo la situazione opposta, cio` quella in cui si sa n dal principio e che si dovranno inviare grosse quantit` di dati. Anche in questo caso lalgoritmo a di Nagle tender` a suddividerli in dimensioni da lui ritenute opportune,61 ma a

TCP_CORK

il problema ` chiamato anche silly window syndrome, per averne unidea si pensi al risultato che si ottiene e quando un programma di terminale invia un segmento TCP per ogni tasto premuto, 40 byte di intestazione di protocollo con 1 byte di dati trasmessi; per evitare situazioni del genere ` stato introdotto lalgoritmo di Nagle. e 59 ` il caso classico di una richiesta HTTP. e 60 si tenga presente per` che TCP_CORK pu` essere specicata insieme a TCP_NODELAY soltanto a partire dal kernel o o 2.5.71. 61 lalgoritmo cerca di tenere conto di queste situazioni, ma essendo un algoritmo generico tender` comunque ad a

58

17.2. LE OPZIONI DEI SOCKET

537

sapendo n dallinizio quale ` la dimensione dei dati si potranno di nuovo ote tenere delle migliori prestazioni disabilitandolo, e gestendo direttamente linvio del nostro blocco di dati in soluzione unica. Quando questa opzione viene abilitata non vengono inviati segmenti di dati ntanto che essa non venga disabilitata; a quel punto tutti i dati rimasti in coda saranno inviati in un solo segmento TCP. In sostanza con questa opzione si pu` controllare il usso dei dati mettendo una sorta di tappo (da cui il nome o in inglese) al usso di uscita, in modo ottimizzare a mano luso della banda. Si tenga presente che per leettivo funzionamento ci si deve ricordare di disattivare lopzione al termine dellinvio del blocco dei dati. Si usa molto spesso TCP_CORK quando si eettua il trasferimento diretto di un blocco di dati da un le ad un socket con sendfile (vedi sez. 11.3.3), per inserire una intestazione prima della chiamata a questa funzione; senza di essa lintestazione potrebbe venire spedita in un segmento a parte, che a seconda delle condizioni potrebbe richiedere anche una risposta di ACK, portando ad una notevole penalizzazione delle prestazioni. Si tenga presente che limplementazione corrente di TCP_CORK non consente di bloccare linvio dei dati per pi` di 200 millisecondi, passati i quali i dati accuu mulati in coda sanno inviati comunque. Questa opzione ` tipica di Linux62 e non e ` disponibile su tutti i kernel unix-like, pertanto deve essere evitata se si vuole e scrivere codice portabile. TCP_KEEPIDLE con questa opzione si legge o si imposta lintervallo di tempo, in secondi, che deve trascorrere senza traco sul socket prima che vengano inviati, qualora si sia attivata su di esso lopzione SO_KEEPALIVE, i messaggi di keep-alive (si veda la trattazione relativa al keep-alive in sez. 17.2.3). Anche questa opzione non ` e disponibile su tutti i kernel unix-like e deve essere evitata se si vuole scrivere codice portabile. TCP_KEEPINTVL con questa opzione si legge o si imposta lintervallo di tempo, in secondi, fra due messaggi di keep-alive successivi (si veda sempre quanto illustrato in sez. 17.2.3). Come la precedente non ` disponibile su tutti i kernel unix-like e deve essere e evitata se si vuole scrivere codice portabile. TCP_KEEPCNT con questa opzione si legge o si imposta il numero totale di messaggi di keepalive da inviare prima di concludere che la connessione ` caduta per assenza e di risposte ad un messaggio di keep-alive (di nuovo vedi sez. 17.2.3). Come la precedente non ` disponibile su tutti i kernel unix-like e deve essere evitata se e si vuole scrivere codice portabile. con questa opzione si legge o si imposta il numero di tentativi di ritrasmissione dei segmenti SYN usati nel three way handshake prima che il tentativo di connessione venga abortito (si ricordi quanto accennato in sez. 16.2.2). Sovrascrive per il singolo socket il valore globale impostato con la sysctl tcp_syn_retries (vedi sez. 17.4.3). Non vengono accettati valori maggiori di 255; anche questa opzione non ` standard e deve essere evitata se si vuole scrivere codice portabile. e

TCP_SYNCNT

introdurre delle suddivisioni in segmenti diversi, anche quando potrebbero non essere necessarie, con conseguente spreco di banda. 62 lopzione ` stata introdotta con i kernel della serie 2.4.x. e

538 TCP_LINGER2

CAPITOLO 17. LA GESTIONE DEI SOCKET con questa opzione si legge o si imposta, in numero di secondi, il tempo di sussistenza dei socket terminati nello stato FIN_WAIT2 (si ricordi quanto visto in sez. 16.1.3).63 Questa opzione consente di sovrascrivere per il singolo socket il valore globale impostato con la sysctl tcp_fin_timeout (vedi sez. 17.4.3). Anche questa opzione ` da evitare se si ha a cuore la portabilit` del codice. e a

TCP_DEFER_ACCEPT questa opzione consente di modicare il comportamento standard del protocollo TCP nello stabilirsi di una connessione; se ricordiamo il meccanismo del three way handshake illustrato in g. 16.1 possiamo vedere che in genere un client inizier` ad inviare i dati ad un server solo dopo lemissione dellultimo segmento a di ACK. Di nuovo esistono situazioni (e la pi` tipica ` quella di una richiesta HTTP) in u e cui sarebbe utile inviare immediatamente la richiesta allinterno del segmento con lultimo ACK del three way handshake; si potrebbe cos` risparmiare linvio di un segmento successivo per la richiesta e il ritardo sul server fra la ricezione dellACK e quello della richiesta. Se si invoca TCP_DEFER_ACCEPT su un socket dal lato client (cio` dal lato da cui si e invoca connect) si istruisce il kernel a non inviare immediatamente lACK nale del three way handshake, attendendo per un po di tempo la prima scrittura, in modo da inviare i dati di questa insieme col segmento ACK. Chiaramente la correttezza di questo comportamento dipende in maniera diretta dal tipo di applicazione che usa il socket; con HTTP, che invia una breve richiesta, permette di risparmiare un segmento, con FTP, in cui invece si attende la ricezione del prompt del server, introduce un inutile ritardo. Allo stesso tempo il protocollo TCP prevede che sul lato del server la funzione accept ritorni dopo la ricezione dellACK nale, in tal caso quello che si fa usualmente ` lanciare un nuovo processo per leggere i successivi dati, che si bloccher` e a su una read se questi non sono disponibili; in questo modo si saranno impiegate delle risorse (per la creazione del nuovo processo) che non vengono usate immediatamente. Luso di TCP_DEFER_ACCEPT consente di intervenire anche in questa situazione; quando la si invoca sul lato server (vale a dire su un socket in ascolto) lopzione fa s` che accept ritorni soltanto quando sono presenti dei dati sul socket, e non alla ricezione dellACK conclusivo del three way handshake. Lopzione prende un valore intero che indica il numero massimo di secondi per cui mantenere il ritardo, sia per quanto riguarda il ritorno di accept su un server, che per linvio dellACK nale insieme ai dati su un client. Lopzione ` specica e di Linux non deve essere utilizzata in codice che vuole essere portabile.64 TCP_WINDOW_CLAMP con questa opzione si legge o si imposta alla dimensione specicata, in byte, il valore dichiarato della advertised window (vedi sez. ??). Il kernel impone comunque una dimensione minima pari a SOCK_MIN_RCVBUF/2. Questa opzione non deve essere utilizzata in codice che vuole essere portabile. TCP_INFO
63

questa opzione, specica di Linux, ma introdotta anche in altri kernel (ad esempio FreeBSD) permette di controllare lo stato interno di un socket TCP diret-

si tenga ben presente che questa opzione non ha nulla a che fare con lopzione SO_LINGER che abbiamo visto in sez. 17.2.3. 64 su FreeBSD ` presente una opzione SO_ACCEPTFILTER che consente di ottenere lo stesso comportamento di e TCP_DEFER_ACCEPT per quanto riguarda il lato server.

17.2. LE OPZIONI DEI SOCKET

539

struct tcp_info { u_int8_t tcpi_state ; u_int8_t tcpi_ca_state ; u_int8_t tcpi_retransmits ; u_int8_t tcpi_probes ; u_int8_t tcpi_backoff ; u_int8_t tcpi_options ; u_int8_t tcpi_snd_wscale : 4 , tcpi_rcv_wscale : 4; u_int32_t tcpi_rto ; u_int32_t tcpi_ato ; u_int32_t tcpi_snd_mss ; u_int32_t tcpi_rcv_mss ; u_int32_t tcpi_unacked ; u_int32_t tcpi_sacked ; u_int32_t tcpi_lost ; u_int32_t tcpi_retrans ; u_int32_t tcpi_fackets ; /* Times . */ u_int32_t tcpi_last_data_sent ; u_int32_t tcpi_last_ack_sent ; /* Not remembered , sorry . u_int32_t tcpi_last_data_recv ; u_int32_t tcpi_last_ack_recv ; /* Metrics . */ u_int32_t tcpi_pmtu ; u_int32_t tcpi_rcv_ssthresh ; u_int32_t tcpi_rtt ; u_int32_t tcpi_rttvar ; u_int32_t tcpi_snd_ssthresh ; u_int32_t tcpi_snd_cwnd ; u_int32_t tcpi_advmss ; u_int32_t tcpi_reordering ; };

*/

Figura 17.19: La struttura tcp_info contenente le informazioni sul socket restituita dallopzione TCP_INFO.

tamente da un programma in user space. Lopzione restituisce in una speciale struttura tcp_info, la cui denizione ` riportata in g. 17.19, tutta una serie di e dati che il kernel mantiene, relativi al socket. Anche questa opzione deve essere evitata se si vuole scrivere codice portabile. Con questa opzione diventa possibile ricevere una serie di informazioni relative ad un socket TCP cos` da poter eettuare dei controlli senza dover passare attraverso delle operazioni di lettura. Ad esempio si pu` vericare se un socket ` o e stato chiuso usando una funzione analoga a quella illustrata in g. 17.20, in cui si utilizza il valore del campo tcpi_state di tcp_info per controllare lo stato del socket. TCP_QUICKACK con questa opzione ` possibile eseguire una forma di controllo sullinvio dei sege menti ACK allinterno di in usso di dati su TCP. In genere questo invio viene gestito direttamente dal kernel, il comportamento standard, corrispondente la valore logico di vero (in genere 1) per questa opzione, ` quello di inviare ime mediatamente i segmenti ACK, in quanto normalmente questo signica che si ` e ricevuto un blocco di dati e si pu` passare allelaborazione del blocco successivo. o Qualora per` la nostra applicazione sappia in anticipo che alla ricezione di un o

540

CAPITOLO 17. LA GESTIONE DEI SOCKET

int is_closing ( int sock ) { 3 struct tcp_info info ; 4 socklen_t len = sizeof ( info ); 5 if ( getsockopt ( sock , SOL_TCP , TCP_INFO , & info , & len ) != -1) { 6 if ( info . tcpi_state == TCP_CLOSE || 7 info . tcpi_state == TCP_CLOSE_WAIT || 8 info . tcpi_state == TCP_CLOSING ) { 9 return 1; 10 } else { 11 return 0; 12 } 13 } else { 14 return errno ; 15 } 16 }
1 2

Figura 17.20: Codice della funzione is_closing.c, che controlla lo stato di un socket TCP per vericare se si sta chiudendo.

blocco di dati seguir` immediatamente linvio di un altro blocco,65 poter accora pare questultimo al segmento ACK permette di risparmiare sia in termini di dati inviati che di velocit` di risposta. Per far questo si pu` utilizzare TCP_QUICKACK a o impostando un valore logico falso (cio` 0), in questo modo il kernel attender` e a cos` da inviare il prossimo segmento di ACK insieme ai primi dati disponibili. Si tenga presente che lopzione non ` permanente, vale a dire che una volta che e la si sia impostata a 0 il kernel la riporter` al valore di default dopo il suo primo a utilizzo. Sul lato server la si pu` impostare anche una volta sola su un socket o in ascolto, ed essa verr` ereditata da tutti i socket che si otterranno da esso al a ritorno di accept. TCP_CONGESTION questa opzione permette di impostare quale algoritmo per il controllo della congestione66 utilizzare per il singolo socket. Lopzione ` stata introdotta con il e kernel 2.6.13,67 e prende come per optval il puntatore ad un buer contenente il nome dellalgoritmo di controllo che si vuole usare. Luso di un nome anzich di un valore numerico ` dovuto al fatto che gli algoritmi e e di controllo della congestione sono realizzati attraverso altrettanti moduli del kernel, e possono pertanto essere attivati a richiesta; il nome consente di caricare il rispettivo modulo e di introdurre moduli aggiuntivi che implementino altri meccanismi. Per poter disporre di questa funzionalit` occorre aver compilato il kernel attia vando lopzione di congurazione generale TCP_CONG_ADVANCED,68 e poi abilitare
caso tipico ad esempio delle risposte alle richieste HTTP. il controllo della congestione ` un meccanismo previsto dal protocollo TCP (vedi sez. ??) per evitare di e trasmettere inutilmente dati quando una connessione ` congestionata; un buon algoritmo ` fondamentale per il e e funzionamento del protocollo, dato che i pacchetti persi andrebbero ritrasmessi, per cui inviare un pacchetto su una linea congestionata potrebbe causare facilmente un peggioramento della situazione. 67 alla data di stesura di queste note (Set. 2006) ` pure scarsamente documentata, tanto che non ` neanche e e denita nelle intestazioni delle glibc per cui occorre denirla a mano al suo valore che ` 13. e 68 disponibile come TCP: advanced congestion control nel men` Network->Networking options, che a sua volta u render` disponibile un ulteriore men` con gli algoritmi presenti. a u
66 65

17.3. LA GESTIONE ATTRAVERSO LE FUNZIONI DI CONTROLLO

541

i singoli moduli voluti con le varie TCP_CONG_* presenti per i vari algoritmi disponibili; un elenco di quelli attualmente supportati nella versione uciale del kernel ` riportato in tab. 17.17.69 e Si tenga presente che prima della implementazione modulare alcuni di questi algoritmi erano disponibili soltanto come caratteristiche generali del sistema, attivabili per tutti i socket, questo ` ancora possibile con la sysctl tcp_congestion_control e (vedi sez. 17.4.3) che ha sostituito le precedenti sysctl.70
Nome reno bic cubic highspeed htcp hybla scalable vegas westwood Congurazione TCP_CONG_BIC TCP_CONG_CUBIC TCP_CONG_HSTCP TCP_CONG_HTCP TCP_CONG_HYBLA TCP_CONG_SCALABLE TCP_CONG_VEGAS TCP_CONG_WESTWOOD Riferimento Algoritmo tradizionale, usato in caso di assenza degli altri. http://www.csc.ncsu.edu/faculty/rhee/export/bitcp/index.htm. http://www.csc.ncsu.edu/faculty/rhee/export/bitcp/index.htm. http://www.icir.org/floyd/hstcp.html. http://www.hamilton.ie/net/htcp/. http://www.danielinux.net/projects.html. http://www.deneholme.net/tom/scalable/. http://www.cs.arizona.edu/protocols/. http://www.cs.ucla.edu/NRL/hpi/tcpw/.

Tabella 17.17: Gli algoritmi per il controllo della congestione disponibili con Linux con le relative opzioni di congurazione da attivare.

Il protocollo UDP, anche per la sua maggiore semplicit`, supporta un numero ridotto di a opzioni, riportate in tab. 17.18; anche in questo caso per poterle utilizzare occorrer` impostare a lopportuno valore per largomento level, che ` SOL_UDP (o lequivalente IPPROTO_UDP). Le e costanti che identicano dette opzioni sono denite in netinet/udp.h, ed accessibili includendo detto le.71
Opzione UDP_CORK UDP_ENCAP get set ag Tipo int int Descrizione Accumula tutti i dati su un unico pacchetto. Non documentata.

Tabella 17.18: Le opzioni per i socket UDP disponibili al livello SOL_UDP.

Ancora una volta le descrizioni contenute tab. 17.18 sono un semplice riferimento, una maggiore quantit` di dettagli sulle caratteristiche delle opzioni citate ` quello dellelenco seguente: a e UDP_CORK questa opzione ha lidentico eetto dellanaloga TCP_CORK vista in precedenza per il protocollo TCP, e quando abilitata consente di accumulare i dati in uscita su un solo pacchetto che verr` inviato una volta che la si disabiliti. Lopzione ` a e stata introdotta con il kernel 2.5.44, e non deve essere utilizzata in codice che vuole essere portabile. Questa opzione permette di gestire lincapsulazione dei dati nel protocollo UDP. Lopzione ` stata introdotta con il kernel 2.5.67, e non ` documentata. Come la e e precedente ` specica di Linux e non deve essere utilizzata in codice portabile. e

UDP_ENCAP

17.3

La gestione attraverso le funzioni di controllo

Bench la maggior parte delle caratteristiche dei socket sia gestibile con le funzioni setsockopt e e getsockopt, alcune propriet` possono essere impostate attraverso le funzioni fcntl e ioctl a
la lista ` presa dalla versione 2.6.17. e riportate anche, alla data di stesura di queste pagine (Set. 2006) nelle pagine di manuale, ma non pi` presenti. u 71 come per TCP, la denizione delle opzioni eettivamente supportate dal kernel si trova in realt` nel le a linux/udp.h, dal quale si sono estratte le costanti di tab. 17.18.
70 69

542

CAPITOLO 17. LA GESTIONE DEI SOCKET

gi` trattate in sez. 6.3.6 e sez. 6.3.7; in quelloccasione abbiamo parlato di queste funzioni a esclusivamente nellambito della loro applicazione a le descriptor associati a dei le normali; qui tratteremo invece i dettagli del loro utilizzo con le descriptor associati a dei socket.

17.3.1

Luso di ioctl e fcntl per i socket generici

Tratteremo in questa sezione le caratteristiche speciche delle funzioni ioctl e fcntl quando esse vengono utilizzate con dei socket generici. Quanto gi` detto in precedenza in sez. 6.3.6 e a sez. 6.3.7 continua a valere; quello che tratteremo qui sono le operazioni ed i comandi che sono validi, o che hanno signicati peculiari, quando queste funzioni vengono applicate a dei socket generici. Nellelenco seguente si riportano i valori specici che pu` assumere il secondo argomento o della funzione ioctl (request, che indica il tipo di operazione da eettuare) quando essa viene applicata ad un socket generico. Nellelenco si illustrer` anche, per ciascuna operazione, il tipo a di dato usato come terzo argomento della funzione ed il signicato che esso viene ad assumere. Dato che in caso di lettura questi dati vengono restituiti come value result argument, con queste operazioni il terzo argomento deve sempre essere passato come puntatore ad una variabile (o struttura) precedentemente allocata. Le costanti che identicano le operazioni sono le seguenti: SIOCGSTAMP restituisce il contenuto di una struttura timeval con la marca temporale dellultimo pacchetto ricevuto sul socket, questa operazione pu` essere utilizzata per o eettuare delle misurazioni precise del tempo di andata e ritorno72 dei pacchetti sulla rete. imposta il processo o il process group a cui inviare i segnali SIGIO e SIGURG quando viene completata una operazione di I/O asincrono o arrivano dei dati urgenti (out-of-band). Il terzo argomento deve essere un puntatore ad una variabile di tipo pid_t; un valore positivo indica direttamente il pid del processo, mentre un valore negativo indica (col valore assoluto) il process group. Senza privilegi di amministratore o la capability CAP_KILL si pu` impostare solo se o stessi o il proprio process group. legge le impostazioni presenti sul socket relativamente alleventuale processo o process group cui devono essere inviati i segnali SIGIO e SIGURG. Come per SIOCSPGRP largomento passato deve un puntatore ad una variabile di tipo pid_t, con lo stesso signicato. Qualora non sia presente nessuna impostazione verr` restituito un valore nullo. a Abilita o disabilita la modalit` di I/O asincrono sul socket. Questo signica a (vedi sez. 11.2.1) che verr` inviato il segnale di SIGIO (o quanto impostato con a F_SETSIG, vedi sez. 6.3.6) in caso di eventi di I/O sul socket.

SIOCSPGRP

SIOCGPGRP

FIOASYNC

Nel caso dei socket generici anche fcntl prevede un paio di comandi specici; in questo caso il secondo argomento (cmd, che indica il comando) pu` assumere i due valori FIOGETOWN o e FIOSETOWN, mentre il terzo argomento dovr` essere un puntatore ad una variabile di tipo a pid_t. Questi due comandi sono una modalit` alternativa di eseguire le stesse operazioni (lettura a o impostazione del processo o del gruppo di processo che riceve i segnali) che si eettuano chiamando ioctl con SIOCGPGRP e SIOCSPGRP.
72

il Round Trip Time cui abbiamo gi` accennato in sez. 14.3.4. a

17.3. LA GESTIONE ATTRAVERSO LE FUNZIONI DI CONTROLLO

543

17.3.2

Luso di ioctl per laccesso ai dispositivi di rete

Bench non strettamente attinenti alla gestione dei socket, vale la pena di trattare qui linterface cia di accesso a basso livello ai dispositivi di rete che viene appunto fornita attraverso la funzione ioctl. Questa non ` attinente a caratteristiche speciche di un qualche protocollo, ma si applie ca a tutti i socket, indipendentemente da tipo e famiglia degli stessi, e permette di impostare e rilevare le funzionalit` delle interfacce di rete. a
struct ifreq { char ifr_name [ IFNAMSIZ ]; /* Interface name */ union { struct sockaddr ifr_addr ; struct sockaddr ifr_dstaddr ; struct sockaddr ifr_broadaddr ; struct sockaddr ifr_netmask ; struct sockaddr ifr_hwaddr ; short ifr_flags ; int ifr_ifindex ; int ifr_metric ; int ifr_mtu ; struct ifmap ifr_map ; char ifr_slave [ IFNAMSIZ ]; char ifr_newname [ IFNAMSIZ ]; char * ifr_data ; }; };

Figura 17.21: La struttura ifreq utilizzata dalle ioctl per le operazioni di controllo sui dispositivi di rete.

Tutte le operazioni di questo tipo utilizzano come terzo argomento di ioctl il puntatore ad una struttura ifreq, la cui denizione ` illustrata in g. 17.21. Normalmente si utilizza il e primo campo della struttura, ifr_name per specicare il nome dellinterfaccia su cui si vuole operare (ad esempio eth0, ppp0, ecc.), e si inseriscono (o ricevono) i valori relativi alle diversa caratteristiche e funzionalit` nel secondo campo, che come si pu` notare ` denito come una a o e union proprio in quanto il suo signicato varia a secondo delloperazione scelta. Si tenga inoltre presente che alcune di queste operazioni (in particolare quelle che modicano le caratteristiche dellinterfaccia) sono privilegiate e richiedono i privilegi di amministratore o la capability CAP_NET_ADMIN, altrimenti si otterr` un errore di EPERM. Le costanti che identicano a le operazioni disponibili sono le seguenti: SIOCGIFNAME questa ` lunica operazione che usa il campo ifr_name per restituire un risule tato, tutte le altre lo utilizzano per indicare linterfaccia sulla quale operare. Loperazione richiede che si indichi nel campo ifr_ifindex il valore numerico dellindice dellinterfaccia, e restituisce il relativo nome in ifr_name. Il kernel infatti assegna ad ogni interfaccia un numero progressivo, detto appunto interface index, che ` quello che eettivamente la identica nelle operazioni e a basso livello, il nome dellinterfaccia ` soltanto una etichetta associata a detto e indice, che permette di rendere pi` comprensibile lindicazione dellinterfaccia u allinterno dei comandi. Una modalit` per ottenere questo valore ` usare il a e comando ip link, che fornisce un elenco delle interfacce presenti ordinato in base a tale valore (riportato come primo campo). SIOCGIFINDEX restituisce nel campo ifr_ifindex il valore numerico dellindice dellinterfaccia specicata con ifr_name, ` in sostanza loperazione inversa di SIOCGIFNAME. e

544 SIOCGIFFLAGS

CAPITOLO 17. LA GESTIONE DEI SOCKET permette di ottenere nel campo ifr_flags il valore corrente dei ag dellinterfaccia specicata (con ifr_name). Il valore restituito ` una maschera binaria i e cui bit sono identicabili attraverso le varie costanti di tab. 17.19.
Flag IFF_UP IFF_BROADCAST IFF_DEBUG IFF_LOOPBACK IFF_POINTOPOINT IFF_RUNNING IFF_NOARP IFF_PROMISC Signicato Linterfaccia ` attiva. e Linterfaccia ha impostato un indirizzo di broadcast valido. ` E attivo il ag interno di debug. Linterfaccia ` una interfaccia di loopback. e Linterfaccia ` associata ad un collegamento punto-punto. e Linterfaccia ha delle risorse allocate (non pu` quindi o essere disattivata). Linterfaccia ha il protocollo ARP disabilitato o lindirizzo del livello di rete non ` impostato. e Linterfaccia ` in modo promiscuo (riceve cio` tutti i pace e chetti che vede passare, compresi quelli non direttamente indirizzati a lei). Evita luso di trailer nei pacchetti. Riceve tutti i pacchetti di multicast. Linterfaccia ` il master di un bundle per il bilanciamento e di carico. Linterfaccia ` uno slave di un bundle per il bilanciamento e di carico. Linterfaccia ha il supporto per il multicast attivo. Linterfaccia pu` impostare i suoi parametri hardware o (con luso di ifmap). Linterfaccia ` in grado di selezionare automaticamente e il tipo di collegamento. Gli indirizzi assegnati allinterfaccia vengono persi quando questa viene disattivata.

IFF_NOTRAILERS IFF_ALLMULTI IFF_MASTER IFF_SLAVE IFF_MULTICAST IFF_PORTSEL IFF_AUTOMEDIA IFF_DYNAMIC

Tabella 17.19: Le costanti che identicano i vari bit della maschera binaria ifr_flags che esprime i ag di una interfaccia di rete.

SIOCSIFFLAGS

permette di impostare il valore dei ag dellinterfaccia specicata (sempre con ifr_name, non staremo a ripeterlo oltre) attraverso il valore della maschera binaria da passare nel campo ifr_flags, che pu` essere ottenuta con lOR o aritmetico delle costanti di tab. 17.19; questa operazione ` privilegiata. e

SIOCGIFMETRIC permette di leggere il valore della metrica del dispositivo associato allinterfaccia specicata nel campo ifr_metric. Attualmente non ` implementato, e e loperazione restituisce sempre un valore nullo. SIOCSIFMETRIC permette di impostare il valore della metrica del dispositivo al valore specicato nel campo ifr_metric, attualmente non ancora implementato, restituisce un errore di EOPNOTSUPP. SIOCGIFMTU SIOCSIFMTU permette di leggere il valore della Maximum Transfer Unit del dispositivo nel campo ifr_mtu. permette di impostare il valore della Maximum Transfer Unit del dispositivo al valore specicato campo ifr_mtu. Loperazione ` privilegiata, e si tenga e presente che impostare un valore troppo basso pu` causare un blocco del kernel. o

SIOCGIFHWADDR permette di leggere il valore dellindirizzo hardware del dispositivo associato allinterfaccia nel campo ifr_hwaddr; questo viene restituito come struttura sockaddr in cui il campo sa_family contiene un valore ARPHRD_* indicante il

17.3. LA GESTIONE ATTRAVERSO LE FUNZIONI DI CONTROLLO

545

tipo di indirizzo ed il campo sa_data il valore binario dellindirizzo hardware a partire dal byte 0. SIOCSIFHWADDR permette di impostare il valore dellindirizzo hardware del dispositivo associato allinterfaccia attraverso il valore della struttura sockaddr (con lo stesso formato illustrato per SIOCGIFHWADDR) passata nel campo ifr_hwaddr. Loperazione ` privilegiata. e SIOCSIFHWBROADCAST imposta lindirizzo broadcast hardware dellinterfaccia al valore specicato dal campo ifr_hwaddr. Loperazione ` privilegiata. e SIOCGIFMAP legge alcuni parametri hardware (memoria, interrupt, canali di DMA) del driver dellinterfaccia specicata, restituendo i relativi valori nel campo ifr_map; questultimo contiene una struttura di tipo ifmap, la cui denizione ` illustrata e in g. 17.22.

struct ifmap { unsigned unsigned unsigned unsigned unsigned unsigned };

long long short char char char

mem_start ; mem_end ; base_addr ; irq ; dma ; port ;

Figura 17.22: La struttura ifmap utilizzata per leggere ed impostare i valori dei parametri hardware di un driver di una interfaccia.

SIOCSIFMAP

imposta i parametri hardware del driver dellinterfaccia specicata, restituendo i relativi valori nel campo ifr_map. Come per SIOCGIFMAP questo deve essere passato come struttura ifmap, secondo la denizione di g. 17.22. aggiunge un indirizzo di multicast ai ltri del livello di collegamento associati dellinterfaccia. Si deve usare un indirizzo hardware da specicare attraverso il campo ifr_hwaddr, che conterr` lopportuna struttura sockaddr; loperazione a ` privilegiata. Per una modalit` alternativa per eseguire la stessa operazione si e a possono usare i packet socket, vedi sez. 18.3.3. rimuove un indirizzo di multicast ai ltri del livello di collegamento dellinterfaccia, vuole un indirizzo hardware specicato come per SIOCADDMULTI. Anche questa operazione ` privilegiata e pu` essere eseguita in forma alternativa con e o i packet socket.

SIOCADDMULTI

SIOCDELMULTI

SIOCGIFTXQLEN permette di leggere la lunghezza della coda di trasmissione del dispositivo associato allinterfaccia specicata nel campo ifr_qlen. SIOCSIFTXQLEN permette di impostare il valore della lunghezza della coda di trasmissione del dispositivo associato allinterfaccia, questo deve essere specicato nel campo ifr_qlen. Loperazione ` privilegiata. e SIOCSIFNAME consente di cambiare il nome dellinterfaccia indicata da ifr_name utilizzando il nuovo nome specicato nel campo ifr_rename.

546

CAPITOLO 17. LA GESTIONE DEI SOCKET

Una ulteriore operazione, che consente di ricavare le caratteristiche delle interfacce di rete, ` SIOCGIFCONF; per` per ragioni di compatibilit` questa operazione ` disponibile soltanto per e o a e i socket della famiglia AF_INET (vale ad dire per socket IPv4). In questo caso lutente dovr` a passare come argomento una struttura ifconf, denita in g. 17.23.
struct ifconf { int ifc_len ; /* size of buffer */ union { char * ifc_buf ; /* buffer address */ struct ifreq * ifc_req ; /* array of structures */ }; };

Figura 17.23: La struttura ifconf.

Per eseguire questa operazione occorrer` allocare preventivamente un buer di contenente a un vettore di strutture ifreq. La dimensione (in byte) di questo buer deve essere specicata nel campo ifc_len di ifconf, mentre il suo indirizzo andr` specicato nel campo ifc_req. a Qualora il buer sia stato allocato come una stringa, il suo indirizzo potr` essere fornito usando a 73 il campo ifc_buf. La funzione restituisce nel buer indicato una serie di strutture ifreq contenenti nel campo ifr_name il nome dellinterfaccia e nel campo ifr_addr il relativo indirizzo IP. Se lo spazio allocato nel buer ` suciente il kernel scriver` una struttura ifreq per ciascuna interfaccia e a attiva, restituendo nel campo ifc_len il totale dei byte eettivamente scritti. Il valore di ritorno ` 0 se loperazione ha avuto successo e negativo in caso contrario. e Si tenga presente che il kernel non scriver` mai sul buer di uscita dati eccedenti numero a di byte specicato col valore di ifc_len impostato alla chiamata della funzione, troncando il risultato se questi non dovessero essere sucienti. Questa condizione non viene segnalata come errore per cui occorre controllare il valore di ifc_len alluscita della funzione, e vericare che esso sia inferiore a quello di ingresso. In caso contrario si ` probabilmente74 avuta una situazione e di troncamento dei dati. Come esempio delluso di queste funzioni si ` riportato in g. 17.24 il corpo principale del e programma iflist in cui si utilizza loperazione SIOCGIFCONF per ottenere una lista delle interfacce attive e dei relativi indirizzi. Al solito il codice completo ` fornito nei sorgenti allegati e alla guida. Il programma inizia (7-11) con la creazione del socket necessario ad eseguire loperazione, dopo di che si inizializzano opportunamente (13-14) i valori della struttura ifconf indicando la dimensione del buer ed il suo indirizzo;75 si esegue poi loperazione invocando ioctl, controllando come sempre la corretta esecuzione, ed uscendo in caso di errore (15-19). Si esegue poi un controllo sulla quantit` di dati restituiti segnalando un eventuale overow a del buer (21-23); se invece ` tutto a posto (24-27) si calcola e si stampa a video il numero e di interfacce attive trovate. Lultima parte del programma (28-33) ` il ciclo sul contenuto delle e varie strutture ifreq restituite in cui si estrae (30) lindirizzo ad esse assegnato76 e lo si stampa
73 si noti che lindirizzo del buer ` denito in ifconf con una union, questo consente di utilizzare una delle e due forme a piacere. 74 probabilmente perch si potrebbe essere nella condizione in cui sono stati usati esattamente quel numero di e byte. 75 si noti come in questo caso si sia specicato lindirizzo usando il campo ifc_buf, mentre nel seguito del programma si acceder` ai valori contenuti nel buer usando ifc_req. a 76 si ` denito access come puntatore ad una struttura di tipo sockaddr_in per poter eseguire un casting e dellindirizzo del valore restituito nei vari campi ifr_addr, cos` poi da poterlo poi usare come argomento di inet_ntoa.

17.3. LA GESTIONE ATTRAVERSO LE FUNZIONI DI CONTROLLO

547

int i , num , ret , sock ; struct ifconf iflist ; 3 char buffer [4096]; 4 struct sockaddr_in * address ; 5 ... 6 /* create a socket for the operation */ 7 sock = socket ( PF_INET , SOCK_STREAM , 0); 8 if ( sock < 0) { 9 perror ( " Socket creation error " ); 10 return 1; 11 } 12 /* init values for the ifcon structure and do SIOCGIFCONF */ 13 iflist . ifc_len = sizeof ( buffer ); 14 iflist . ifc_buf = buffer ; 15 ret = ioctl ( sock , SIOCGIFCONF , & iflist ); 16 if ( ret < 0) { 17 perror ( " ioctl failed " ); 18 return 1; 19 } 20 /* check that we have all data */ 21 if ( iflist . ifc_len == sizeof ( buffer )) { 22 printf ( " Probable overflow , too many interfaces , cannot read \ n " ); 23 return 1; 24 } else { 25 num = iflist . ifc_len / sizeof ( struct ifreq ); 26 printf ( " Found % i interfaces \ n " , num ); 27 } 28 /* loop on interface to write data */ 29 for ( i =0; i < num ; i ++) { 30 address = ( struct sockaddr_in *) & iflist . ifc_req [ i ]. ifr_addr ; 31 printf ( " Interface %s , address % s \ n " , iflist . ifc_req [ i ]. ifr_name , 32 inet_ntoa ( address - > sin_addr )); 33 } 34 return 0;
1 2

Figura 17.24: Il corpo principale del programma iflist.c.

(31-32) insieme al nome dellinterfaccia.

17.3.3

Luso di ioctl per i socket TCP e UDP

Non esistono operazioni speciche per i socket IP in quanto tali,77 mentre per i pacchetti di altri protocolli trasportati su IP, qualora li si gestisca attraverso dei socket, si dovr` fare riferimento a direttamente alleventuale supporto presente per il tipo di socket usato: ad esempio si possono ricevere pacchetti ICMP con socket di tipo raw, nel qual caso si dovr` fare riferimento alle a operazioni di questultimo. Tuttavia la gran parte dei socket utilizzati nella programmazione di rete utilizza proprio il protocollo IP, e quello che succede ` che in realt` la funzione ioctl consente di eettuare alcune e a operazioni speciche per i socket che usano questo protocollo, ma queste vendono eseguite, invece che a livello di IP, al successivo livello di trasporto, vale a dire in maniera specica per i socket TCP e UDP. Le operazioni di controllo disponibili per i socket TCP sono illustrate dalla relativa pagina di manuale, accessibile con man 7 tcp, e prevedono come possibile valore per il secondo argomento
a parte forse SIOCGIFCONF, che per` resta attinente alle propriet` delle interfacce di rete, per cui labbiamo o a trattata in sez. 17.3.2 insieme alle altre che comunque si applicano anche ai socket IP.
77

548

CAPITOLO 17. LA GESTIONE DEI SOCKET

della funzione le costanti illustrate nellelenco seguente; il terzo argomento della funzione, gestito come value result argument, deve essere sempre il puntatore ad una variabile di tipo int: SIOCINQ SIOCATMARK restituisce la quantit` di dati non ancora letti presenti nel buer di ricezione; il a socket non deve essere in stato LISTEN, altrimenti si avr` un errore di EINVAL. a ritorna un intero non nullo, da intendere come valore logico, se il usso di dati letti sul socket ` arrivato sulla posizione (detta anche urgent mark ) in cui sono e stati ricevuti dati urgenti (vedi sez. 19.1.3). Una operazione di lettura da un socket non attraversa mai questa posizione, per cui ` possibile controllare se la e si ` raggiunta o meno con questa operazione. e Questo ` utile quando si attiva lopzione SO_OOBINLINE (vedi sez. 17.2.2) per e ricevere i dati urgenti allinterno del usso dei dati ordinari del socket;78 in tal caso quando SIOCATMARK restituisce un valore non nullo si sapr` che la successiva a lettura dal socket restituir` i dati urgenti e non il normale traco; torneremo a su questo in maggior dettaglio in sez. 19.1.3. SIOCOUTQ restituisce la quantit` di dati non ancora inviati presenti nel buer di spedizione; a come per SIOCINQ il socket non deve essere in stato LISTEN, altrimenti si avr` a un errore di EINVAL.

Le operazioni di controllo disponibili per i socket UDP, anchesse illustrate dalla relativa pagina di manuale accessibile con man 7 udp, sono quelle indicate dalle costanti del seguente elenco; come per i socket TCP il terzo argomento viene gestito come value result argument e deve essere un puntatore ad una variabile di tipo int: FIONREAD TIOCOUTQ restituisce la dimensione in byte del primo pacchetto in attesa di ricezione, o 0 qualora non ci sia nessun pacchetto. restituisce il numero di byte presenti nella coda di invio locale; questa opzione ` supportata soltanto a partire dal kernel 2.4 e

17.4

La gestione con sysctl ed il lesystem /proc

Come ultimo argomento di questo capitolo tratteremo luso della funzione sysctl (che ` stata e introdotta nelle sue funzionalit` generiche in sez. 8.2.1) per quanto riguarda le sue capacit` di a a eettuare impostazioni relative alle propriet` dei socket. Dato che le stesse funzionalit` sono a a controllabili direttamente attraverso il lesystem /proc, le tratteremo attraverso i le presenti in questultimo.

17.4.1

Luso di sysctl e /proc per le propriet` della rete a

La dierenza nelluso di sysctl e del lesystem /proc rispetto a quello delle funzioni ioctl e fcntl visto in sez. 17.3 o alluso di getsockopt e setsockopt ` che queste funzioni consentono e di controllare le propriet` di un singolo socket, mentre con sysctl e con /proc si impostano a propriet` (o valori di default) validi a livello dellintero sistema, e cio` per tutti i socket. a e Le opzioni disponibili per le propriet` della rete, nella gerarchia dei valori impostabili con a sysctl, sono riportate sotto il nodo net, o, se acceduti tramite linterfaccia del lesystem /proc, sotto /proc/sys/net. In genere sotto questa directory compaiono le sottodirectory (corrispondenti ad altrettanti sottonodi per sysctl) relative ai vari protocolli e tipi di interfacce su cui
vedremo in sez. 19.1.3 che in genere i dati urgenti presenti su un socket si leggono out-of-band usando un opportuno ag per recvmsg.
78

17.4. LA GESTIONE CON SYSCTL ED IL FILESYSTEM /PROC

549

` possibile intervenire per eettuare impostazioni; un contenuto tipico di questa directory ` il e e seguente: /proc/sys/net/ |-- core |-- ethernet |-- ipv4 |-- ipv6 |-- irda |-- token-ring -- unix e sono presenti varie centinaia di parametri, molti dei quali non sono neanche documentati; nel nostro caso ci limiteremo ad illustrare quelli pi` signicativi. u Si tenga presente inne che se ` sempre possibile utilizzare il lesystem /proc come sostituto e di sysctl, dato che i valori di nodi e sottonodi di questultima sono mappati come le e directory sotto /proc/sys/, non ` vero il contrario, ed in particolare Linux consente di impostare alcuni e parametri o leggere lo stato della rete a livello di sistema sotto /proc/net, dove sono presenti dei le che non corrispondono a nessun nodo di sysctl.

17.4.2

I valori di controllo per i socket generici

Nella directory /proc/sys/net/core/ sono presenti i le corrispondenti ai parametri generici di sysctl validi per tutti i socket. Quelli descritti anche nella pagina di manuale, accessibile con man 7 socket sono i seguenti: rmem_default imposta la dimensione di default del buer di ricezione (cio` per i dati in ingresso) e dei socket. rmem_max imposta la dimensione massima che si pu` assegnare al buer di ricezione dei o socket attraverso luso dellopzione SO_RCVBUF.

wmem_default imposta la dimensione di default del buer di trasmissione (cio` per i dati in e uscita) dei socket. wmem_max imposta la dimensione massima che si pu` assegnare al buer di trasmissione dei o socket attraverso luso dellopzione SO_SNDBUF.

message_cost, message_burst contengono le impostazioni del bucket lter che controlla lemissione di messaggi di avviso da parte del kernel per eventi relativi a problemi sulla rete, imponendo un limite che consente di prevenire eventuali attacchi di Denial of Service usando i log.79 Il bucket lter ` un algoritmo generico che permette di impostare dei limiti di e usso su una quantit`80 senza dovere eseguire medie temporali, che verrebbero a a dipendere in misura non controllabile dalla dimensione dellintervallo su cui si media e dalla distribuzione degli eventi;81 in questo caso si denisce la dimensione
79 senza questo limite un attaccante potrebbe inviare ad arte un traco che generi intenzionalmente messaggi di errore, per saturare il sistema dei log. 80 uno analogo viene usato nel netlter per imporre dei limiti sul usso dei pacchetti. 81 in caso di un picco di usso (il cosiddetto burst) il usso medio verrebbe a dipendere in maniera esclusiva dalla dimensione dellintervallo di tempo su cui calcola la media.

550

CAPITOLO 17. LA GESTIONE DEI SOCKET di un bidone (il bucket) e del usso che da esso pu` uscire, la presenza di una o dimensione iniziale consente di assorbire eventuali picchi di emissione, laver ssato un usso di uscita garantisce che a regime questo sar` il valore medio del usso a ottenibile dal bucket. I due valori indicano rispettivamente il usso a regime (non sar` inviato pi` di un a u messaggio per il numero di secondi specicato da message_cost) e la dimensione iniziale per in caso di picco di emissione (verranno accettati inizialmente no ad un massimo di message_cost/message_burst messaggi).

netdev_max_backlog numero massimo di pacchetti che possono essere contenuti nella coda di ingresso generale. optmem_max lunghezza massima dei dati ancillari e di controllo (vedi sez. 19.1.2). Oltre a questi nella directory /proc/sys/net/core si trovano altri le, la cui documentazione dovrebbe essere mantenuta nei sorgenti del kernel, nel le Documentation/networking/ip-sysctl.txt; la maggior parte di questi per` non ` documentato: o e dev_weight blocco di lavoro (work quantum) dello scheduler di processo dei pacchetti. lo_cong valore per loccupazione della coda di ricezione sotto la quale si considera di avere una bassa congestione. valore per loccupazione della coda di ricezione sotto la quale si considera di avere una congestione moderata. valore per loccupazione della coda di ricezione sotto la quale si considera di non avere congestione.

mod_cong

no_cong

no_cong_thresh valore minimo (low water mark ) per il riavvio dei dispositivi congestionati. somaxconn imposta la dimensione massima utilizzabile per il backlog della funzione listen (vedi sez. 16.2.3), e corrisponde al valore della costante SOMAXCONN; il suo valore di default ` 128. e

17.4.3

I valori di controllo per il protocollo IPv4

Nella directory /proc/sys/net/ipv4 sono presenti i le che corrispondono ai parametri dei socket che usano il protocollo IPv4, relativi quindi sia alle caratteristiche di IP, che a quelle degli altri protocolli che vengono usati allinterno di questultimo (come ICMP, TCP e UDP) o a anco dello stesso (come ARP). I le che consentono di controllare le caratteristiche speciche del protocollo IP in quanto tale, che sono descritti anche nella relativa pagina di manuale accessibile con man 7 ip, sono i seguenti: ip_default_ttl imposta il valore di default per il campo TTL (vedi sez. A.1.2) di tutti i pacchetti uscenti, stabilendo cos` il numero massimo di router che i pacchetti possono at traversare. Il valore pu` essere modicato anche per il singolo socket con lopzione o IP_TTL. Prende un valore intero, ma dato che il campo citato ` di 8 bit hanno e senso solo valori fra 0 e 255. Il valore di default ` 64, e normalmente non c` e e

17.4. LA GESTIONE CON SYSCTL ED IL FILESYSTEM /PROC

551

nessuna necessit` di modicarlo.82 Aumentare il valore ` una pratica poco gena e tile, in quanto in caso di problemi di routing si allunga inutilmente il numero di ritrasmissioni. ip_forward abilita linoltro dei pacchetti da una interfaccia ad un altra, e pu` essere impostato o anche per la singola interfaccia. Prende un valore logico (0 disabilita, diverso da zero abilita), di default ` disabilitato. e ip_dynaddr abilita la riscrittura automatica degli indirizzi associati ad un socket quando una interfaccia cambia indirizzo. Viene usato per le interfacce usate nei collegamenti in dial-up, il cui indirizzo IP viene assegnato dinamicamente dal provider, e pu` o essere modicato. Prende un valore intero, con 0 si disabilita la funzionalit`, con a 1 la si abilita, con 2 (o con qualunque altro valore diverso dai precedenti) la si abilit` in modalit` prolissa; di default la funzionalit` ` disabilitata. a a ae ip_autoconfig specica se lindirizzo IP ` stato congurato automaticamente dal kernel allavvio e attraverso DHCP, BOOTP o RARP. Riporta un valore logico (0 falso, 1 vero) accessibile solo in lettura, ` inutilizzato nei kernel recenti ed eliminato a partire e dal kernel 2.6.18. ip_local_port_range imposta lintervallo dei valori usati per lassegnazione delle porte emere, permette cio` di modicare i valori illustrati in g. 16.4; prende due valori interi separati e da spazi, che indicano gli estremi dellintervallo. Si abbia cura di non denire un intervallo che si sovrappone a quello delle porte usate per il masquerading, il kernel pu` gestire la sovrapposizione, ma si avr` una perdita di prestazioni. Si imposti o a sempre un valore iniziale maggiore di 1024 (o meglio ancora di 4096) per evitare conitti con le porte usate dai servizi noti. ip_no_pmtu_disc permette di disabilitare per i socket SOCK_STREAM la ricerca automatica della Path MTU (vedi sez. 14.3.5 e sez. 17.2.4). Prende un valore logico, e di default ` e disabilitato (cio` la ricerca viene eseguita). e In genere si abilita questo parametro quando per qualche motivo il procedimento del Path MTU discovery fallisce; dato che questo pu` avvenire a causa di o router83 o interfacce84 mal congurate ` opportuno correggere le congurazioni, e perch disabilitare globalmente il procedimento con questo parametro ha pesanti e ripercussioni in termini di prestazioni di rete. ip_always_defrag fa si che tutti i pacchetti IP frammentati siano riassemblati, anche in caso in successivo immediato inoltro.85 Prende un valore logico e di default ` disabilitato. e Con i kernel dalla serie 2.4 in poi la deframmentazione viene attivata automaticamente quando si utilizza il sistema del netlter, e questo parametro non ` pi` e u presente.
lunico motivo sarebbe per raggiungere macchine estremamente lontane in termini di hop, ma ` praticamente e impossibile trovarne. 83 ad esempio se si scartano tutti i pacchetti ICMP, il problema ` arontato anche in sez. 1.4.4 di [16]. e 84 ad esempio se i due capi di un collegamento point-to-point non si accordano sulla stessa MTU. 85 introdotto con il kernel 2.2.13, nelle versioni precedenti questo comportamento poteva essere solo stabilito un volta per tutte in fase di compilazione del kernel con lopzione CONFIG_IP_ALWAYS_DEFRAG.
82

552

CAPITOLO 17. LA GESTIONE DEI SOCKET

ipfrag_high_thresh indica il limite massimo (espresso in numero di byte) sui pacchetti IP frammentati presenti in coda; quando questo valore viene raggiunta la coda viene ripulita no al valore ipfrag_low_thresh. Prende un valore intero. ipfrag_low_thresh soglia bassa (specicata in byte) a cui viene riportata la coda dei pacchetti IP frammentati quando si raggiunge il valore massimo dato da ipfrag_high_thresh. Prende un valore intero. ip_nonlocal_bind se abilitato rende possibile ad una applicazione eseguire bind anche su un indirizzo che non ` presente su nessuna interfaccia locale. Prende un valore logico e di default e ` disabilitato. e Questo pu` risultare utile per applicazioni particolari (come gli snier ) che hanno o la necessit` di ricevere pacchetti anche non diretti agli indirizzi presenti sulla a macchina, ad esempio per intercettare il traco per uno specico indirizzo che si vuole tenere sotto controllo. Il suo uso per` pu` creare problemi ad alcune o o applicazioni. I le di /proc/sys/net/ipv4 che invece fanno riferimento alle caratteristiche speciche del protocollo TCP, elencati anche nella rispettiva pagina di manuale (accessibile con man 7 tcp), sono i seguenti: tcp_abort_on_overflow indica al kernel di azzerare le connessioni quando il programma che le riceve ` e troppo lento ed incapace di accettarle. Prende un valore logico ed ` disabilitato e di default. Questo consente di recuperare le connessioni se si ` avuto un eccesso e dovuto ad un qualche picco di traco, ma ovviamente va a discapito dei client che interrogano il server. Pertanto ` da abilitare soltanto quando si ` sicuri che non e e ` possibile ottimizzare il server in modo che sia in grado di accettare connessioni e pi` rapidamente. u tcp_adv_win_scale indica al kernel quale frazione del buer associato ad un socket86 deve essere utilizzata per la nestra del protocollo TCP87 e quale come buer applicativo per isolare la rete dalle latenze dellapplicazione. Prende un valore intero che determina la suddetta frazione secondo la formula buffer/2tcp_adv_win_scale se positivo o con buffer buffer/2tcp_adv_win_scale se negativo. Il default ` 2 che signica che al e buer dellapplicazione viene riservato un quarto del totale. tcp_app_win indica la frazione della nestra TCP che viene riservata per gestire loverhaed dovuto alla buerizzazione. Prende un valore valore intero che consente di calcolare la dimensione in byte come il massimo fra la MSS e window/2tcp_app_win . Un valore nullo signica che non viene riservato nessuno spazio; il valore di default ` 31. e tcp_dsack
86

abilita il supporto, denito nellRFC 2884, per il cosiddetto Duplicate SACK.88 Prende un valore logico e di default ` abilitato. e

quello impostato con tcp_rmem. in sostanza il valore che costituisce la advertised window annunciata allaltro capo del socket. 88 si indica con SACK (Selective Acknowledgement) unopzione TCP, denita nellRFC 2018, usata per dare un acknowledgement unico su blocchi di pacchetti non contigui, che consente di diminuire il numero di pacchetti scambiati.
87

17.4. LA GESTIONE CON SYSCTL ED IL FILESYSTEM /PROC tcp_ecn

553

abilita il meccanismo della Explicit Congestion Notication (in breve ECN) nelle connessioni TCP. Prende valore logico che di default ` disabilitato. La Explicit e Congestion Notication ` un meccanismo che consente di noticare quando una e rotta o una rete ` congestionata da un eccesso di traco,89 si pu` cos` essere e o avvisati e cercare rotte alternative oppure diminuire lemissione di pacchetti (in modo da non aumentare la congestione). Si tenga presente che se si abilita questa opzione si possono avere dei malfunzionamenti apparentemente casuali dipendenti dalla destinazione, dovuti al fatto che alcuni vecchi router non supportano il meccanismo ed alla sua attivazione scartano i relativi pacchetti, bloccando completamente il traco.

tcp_fack

abilita il supporto per il TCP Forward Acknowledgement, un algoritmo per il controllo della congestione del traco. Prende un valore logico e di default ` e abilitato.

tcp_fin_timeout specica il numero di secondi da passare in stato FIN_WAIT2 nellattesa delle ricezione del pacchetto FIN conclusivo, passati quali il socket viene comunque chiuso forzatamente. Prende un valore intero che indica i secondi e di default ` 60.90 e Luso di questa opzione realizza quella che in sostanza ` una violazione delle spee ciche del protocollo TCP, ma ` utile per fronteggiare alcuni attacchi di Denial e of Service. tcp_frto abilita il supporto per lalgoritmo F-RTO, un algoritmo usato per la ritrasmissione dei timeout del protocollo TCP, che diventa molto utile per le reti wireless dove la perdita di pacchetti ` usualmente dovuta a delle interferenze radio, piuttosto e che alla congestione dei router. Prende un valore logico e di default ` disabilitato. e

tcp_keepalive_intvl indica il numero di secondi che deve trascorrere fra lemissione di due successivi pacchetti di test quando ` abilitata la funzionalit` del keepalive (vedi sez. 17.2.3). e a Prende un valore intero che di default ` 75. e tcp_keepalive_probes indica il massimo numero pacchetti di keepalive (vedi sez. 17.2.3) che devono essere inviati senza ricevere risposta prima che il kernel decida che la connessione ` caduta e la termini. Prende un valore intero che di default ` 9. e e tcp_keepalive_time indica il numero di secondi che devono passare senza traco sulla connessione prima che il kernel inizi ad inviare pacchetti di pacchetti di keepalive.91 Prende un valore intero che di default ` 7200, pari a due ore. e tcp_low_latency indica allo stack TCP del kernel di ottimizzare il comportamento per ottenere tempi di latenza pi` bassi a scapito di valori pi` alti per lutilizzo della banda. u u Prende un valore logico che di default ` disabilitato in quanto un maggior utilizzo e della banda ` preferito, ma esistono applicazioni particolari in cui la riduzione e della latenza ` pi` importante (ad esempio per i cluster di calcolo parallelo) nelle e u quali lo si pu` abilitare. o
il meccanismo ` descritto in dettaglio nellRFC 3168 mentre gli eetti sulle prestazioni del suo utilizzo sono e documentate nellRFC 2884. 90 nei kernel della serie 2.2.x era il valore utilizzato era invece di 120 secondi. 91 ha eetto solo per i socket per cui si ` impostata lopzione SO_KEEPALIVE (vedi sez. 17.2.3. e
89

554

CAPITOLO 17. LA GESTIONE DEI SOCKET

tcp_max_orphans indica il numero massimo di socket TCP orfani (vale a dire non associati a nessun le descriptor) consentito nel sistema.92 Quando il limite viene ecceduto la connessione orfana viene resettata e viene stampato un avvertimento. Questo limite viene usato per contrastare alcuni elementari attacchi di denial of service. Diminuire il valore non ` mai raccomandato, in certe condizioni di rete pu` essere e o opportuno aumentarlo, ma si deve tenere conto del fatto che ciascuna connessione orfana pu` consumare no a 64K di memoria del kernel. Prende un valore intero, o il valore di default viene impostato inizialmente al valore del parametro del kernel NR_FILE, e viene aggiustato a seconda della memoria disponibile. tcp_max_syn_backlog indica la lunghezza della coda delle connessioni incomplete, cio` delle connessioni e per le quali si ` ricevuto un SYN di richiesta ma non lACK nale del three way e handshake (si riveda quanto illustrato in sez. 16.2.3). Quando questo valore ` superato il kernel scarter` immediatamente ogni ulterioe a re richiesta di connessione. Prende un valore intero; il default, che ` 256, viene e automaticamente portato a 1024 qualora nel sistema ci sia suciente memoria (se maggiore di 128Mb) e ridotto a 128 qualora la memoria sia poca (inferiore a 32Mb).93 tcp_max_tw_buckets indica il numero massimo di socket in stato TIME_WAIT consentito nel sistema. Prende un valore intero di default ` impostato al doppio del valore del paramee tro NR_FILE, ma che viene aggiustato automaticamente a seconda della memoria presente. Se il valore viene superato il socket viene chiuso con la stampa di un avviso; luso di questa funzionalit` consente di prevenire alcuni semplici attacchi a di denial of service. tcp_mem viene usato dallo stack TCP per gestire le modalit` con cui esso utilizzer` la a a memoria. Prende una tripletta di valori interi, che indicano un numero di pagine: il primo valore, chiamato low nelle pagine di manuale, indica il numero di pagine allocate sotto il quale non viene usato nessun meccanismo di regolazione delluso della memoria. il secondo valore, chiamato pressure indica il numero di pagine allocate passato il quale lo stack TCP inizia a moderare il suo consumo di memoria; si esce da questo stato di pressione sulla memoria quando il numero di pagine scende sotto il precedente valore low. il terzo valore, chiamato high indica il numero massimo di pagine che possono essere utilizzate dallo stack TCP/IP, e soprassiede ogni altro valore specicato dagli altri limiti del kernel. tcp_orphan_retries indica il numero massimo di volte che si esegue un tentativo di controllo sullaltro capo di una connessione che ` stata gi` chiusa dalla nostra parte. Prende un valore e a intero che di default ` 8. e
trattasi in genere delle connessioni relative a socket chiusi che non hanno completato il processo di chiusura. si raccomanda, qualora si voglia aumentare il valore oltre 1024, di seguire la procedura citata nella pagina di manuale di TCP, e modicare il valore della costante TCP_SYNQ_HSIZE nel le include/net/tcp.h dei sorgenti del kernel, in modo che sia tcp max syn backlog 16 TCP SYNQ HSIZE, per poi ricompilare il kernel.
93 92

17.4. LA GESTIONE CON SYSCTL ED IL FILESYSTEM /PROC

555

tcp_reordering indica il numero massimo di volte che un pacchetto pu` essere riordinato nel o usso di dati, prima che lo stack TCP assuma che ` andato perso e si ponga nello e stato di slow start (si veda sez. ??) viene usata questa metrica di riconoscimento dei riordinamenti per evitare inutili ritrasmissioni provocate dal riordinamento. Prende un valore intero che di default che ` 3, e che non ` opportuno modicare. e e tcp_retrans_collapse in caso di pacchetti persi durante una connessione, per ottimizzare luso della banda il kernel cerca di eseguire la ritrasmissione inviando pacchetti della massima dimensione possibile; in sostanza dati che in precedenza erano stati trasmessi su pacchetti diversi possono essere ritrasmessi riuniti su un solo pacchetto (o su un numero minore di pacchetti di dimensione maggiore). Prende un valore logico e di default ` abilitato. e tcp_retries1 imposta il massimo numero di volte che protocollo tenter` la ritrasmissione si un a pacchetto su una connessione stabilita prima di fare ricorso ad ulteriori sforzi che coinvolgano anche il livello di rete. Passato questo numero di ritrasmissioni verr` a fatto eseguire al livello di rete un tentativo di aggiornamento della rotta verso la destinazione prima di eseguire ogni successiva ritrasmissione. Prende un valore intero che di default ` 3. e tcp_retries2 imposta il numero di tentativi di ritrasmissione di un pacchetto inviato su una connessione gi` stabilita per il quale non si sia ricevuto una risposta di ACK (si a veda anche quanto illustrato in sez. 16.5.2). Prende un valore intero che di default ` 15, il che comporta un tempo variabile fra 13 e 30 minuti; questo non corrisponde e a quanto richiesto nellRFC 1122 dove ` indicato un massimo di 100 secondi, che e per` ` un valore considerato troppo basso. oe tcp_rfc1337 indica al kernel di abilitare il comportamento richiesto nellRFC 1337. Prende un valore logico e di default ` disabilitato, il che signica che alla ricezione di un e segmento RST in stato TIME_WAIT il socket viene chiuso immediatamente senza attendere la conclusione del periodo di TIME_WAIT. tcp_rmem viene usato dallo stack TCP per controllare dinamicamente le dimensioni dei propri buer di ricezione, anche in rapporto alla memoria disponibile. Prende una tripletta di valori interi separati da spazi che indicano delle dimensioni in byte: il primo valore, chiamato min nelle pagine di manuale, indica la dimensione minima in byte del buer di ricezione; il default ` 4Kb, ma in sistemi con e poca memoria viene automaticamente ridotto a PAGE_SIZE. Questo valore viene usato per assicurare che anche in situazioni di pressione sulla memoria (vedi quanto detto per tcp_rmem) le allocazioni al di sotto di questo limite abbiamo comunque successo. Questo valore non viene comunque ad incidere sulla dimensione del buer di ricezione di un singolo socket dichiarata con lopzione SO_RCVBUF. il secondo valore, denominato default nelle pagine di manuale, indica la dimensione di default, in byte, del buer di ricezione di un socket TCP. Questo valore sovrascrive il default iniziale impostato per tutti i socket con /proc/sys/net/core/mem_default che vale per qualunque protocollo. Il default ` 87380 byte, ridotto a 43689 per sistemi con poca memoria. Se si dee siderano dimensioni pi` ampie per tutti i socket si pu` aumentare questo u o

556

CAPITOLO 17. LA GESTIONE DEI SOCKET valore, ma se si vuole che in corrispondenza aumentino anche le dimensioni usate per la nestra TCP si deve abilitare il TCP window scaling (di default ` abilitato, vedi pi` avanti tcp_window_scaling). e u il terzo valore, denominato max nelle pagine di manuale, indica la dimensione massima in byte del buer di ricezione di un socket TCP; il default ` 174760 byte, che viene ridotto automaticamente a 87380 per sistemi con e poca memoria. Il valore non pu` comunque eccedere il limite generale per o tutti i socket posto con /proc/sys/net/core/rmem_max. Questo valore non viene ad incidere sulla dimensione del buer di ricezione di un singolo socket dichiarata con lopzione SO_RCVBUF.

tcp_sack

indica al kernel di utilizzare il meccanismo del TCP selective acknowledgement denito nellRFC 2018. Prende un valore logico e di default ` abilitato. e

tcp_stdurg indica al kernel di utilizzare linterpretazione che viene data dallRFC 1122 del puntatore dei dati urgenti (vedi sez. 19.1.3) in cui questo punta allultimo byte degli stessi; se disabilitato viene usata linterpretazione usata da BSD per cui esso punta al primo byte successivo. Prende un valore logico e di default ` disabilitato, e perch abilitarlo pu` dar luogo a problemi di interoperabilit`. e o a tcp_synack_retries indica il numero massimo di volte che verr` ritrasmesso il segmento SYN/ACK a nella creazione di una connessione (vedi sez. 16.1.1). Prende un valore intero ed il valore di default ` 5; non si deve superare il valore massimo di 255. e tcp_syncookies abilita i TCP syncookies.94 Prende un valore logico, e di default ` disabilitato. e Questa funzionalit` serve a fornire una protezione in caso di un attacco di tipo a SYN ood, e deve essere utilizzato come ultima risorsa dato che costituisce una violazione del protocollo TCP e conigge con altre funzionalit` come le estensioni a e pu` causare problemi per i client ed il reinoltro dei pacchetti. o tcp_syn_retries imposta il numero di tentativi di ritrasmissione dei pacchetti SYN di inizio connessione del three way handshake (si ricordi quanto illustrato in sez. 16.2.2). Prende un valore intero che di default ` 5; non si deve superare il valore massimo di 255. e tcp_timestamps abilita luso dei TCP timestamps, come deniti nellRFC 1323. Prende un valore logico e di default ` abilitato. e tcp_tw_recycle abilita il riutilizzo rapido dei socket in stato TIME_WAIT. Prende un valore logico e di default ` disabilitato. Non ` opportuno abilitare questa opzione che pu` causare e e o 95 problemi con il NAT. tcp_tw_reuse abilita il riutilizzo dello stato TIME_WAIT quando questo ` sicuro dal punto di vista e del protocollo. Prende un valore logico e di default ` disabilitato. e
94 per poter usare questa funzionalit` ` necessario avere abilitato lopzione CONFIG_SYN_COOKIES nella a e compilazione del kernel. 95 il Network Address Translation ` una tecnica, impiegata nei rewall e nei router, che consente di modicare e al volo gli indirizzi dei pacchetti che transitano per una macchina, Linux la supporta con il netlter, per maggiori dettagli si consulti il cap. 2 di [16].

17.4. LA GESTIONE CON SYSCTL ED IL FILESYSTEM /PROC

557

tcp_window_scaling un valore logico, attivo di default, che abilita la funzionalit` del TCP window scaa ling denita dallRFC 1323. Prende un valore logico e di default ` abilitato. Come e accennato in sez. 16.1.2 i 16 bit della nestra TCP comportano un limite massimo di dimensione di 64Kb, ma esiste una opportuna opzione del protocollo che permette di applicare un fattore di scale che consente di aumentarne le dimensioni. Questa ` pienamente supportata dallo stack TCP di Linux, ma se lo si disabilita e la negoziazione del TCP window scaling con laltro capo della connessione non viene eettuata. tcp_wmem viene usato dallo stack TCP per controllare dinamicamente le dimensioni dei propri buer di spedizione, adeguandole in rapporto alla memoria disponibile. Prende una tripletta di valori interi separati da spazi che indicano delle dimensioni in byte: il primo valore, chiamato min, indica la dimensione minima in byte del buffer di spedizione; il default ` 4Kb. Come per lanalogo di tcp_rmem) viene e usato per assicurare che anche in situazioni di pressione sulla memoria (vedi tcp_mem) le allocazioni al di sotto di questo limite abbiamo comunque successo. Di nuovo questo valore non viene ad incidere sulla dimensione del buer di trasmissione di un singolo socket dichiarata con lopzione SO_SNDBUF. il secondo valore, denominato default, indica la dimensione di default in byte del buer di spedizione di un socket TCP. Questo valore sovrascrive il default iniziale impostato per tutti i tipi di socket con /proc/sys/net/core/wmem_default. Il default ` 87380 byte, ridotto a 43689 per sistemi con poca memoria. e Si pu` aumentare questo valore quando si desiderano dimensioni pi` amo u pie del buer di trasmissione per i socket TCP, ma come per il precedente tcp_rmem) se si vuole che in corrispondenza aumentino anche le dimensioni usate per la nestra TCP si deve abilitare il TCP window scaling con tcp_window_scaling. il terzo valore, denominato max, indica la dimensione massima in byte del buer di spedizione di un socket TCP; il default ` 128Kb, che viene rie dotto automaticamente a 64Kb per sistemi con poca memoria. Il valore non pu` comunque eccedere il limite generale per tutti i socket posto con o /proc/sys/net/core/wmem_max. Questo valore non viene ad incidere sulla dimensione del buer di trasmissione di un singolo socket dichiarata con lopzione SO_SNDBUF.

558

CAPITOLO 17. LA GESTIONE DEI SOCKET

Capitolo 18

Gli altri tipi di socket


Dopo aver trattato in cap. 16 i socket TCP, che costituiscono lesempio pi` comune dellinterfacu cia dei socket, esamineremo in questo capitolo gli altri tipi di socket, a partire dai socket UDP, e i socket Unix domain gi` incontrati in sez. 12.1.5. a

18.1

I socket UDP

Dopo i socket TCP i socket pi` utilizzati nella programmazione di rete sono i socket UDP: u protocolli diusi come NFS o il DNS usano principalmente questo tipo di socket. Tratteremo in questa sezione le loro caratteristiche principali e le modalit` per il loro utilizzo. a

18.1.1

Le caratteristiche di un socket UDP

Come illustrato in sez.14.3.3 UDP ` un protocollo molto semplice che non supporta le connessioni e e non ` adabile: esso si appoggia direttamente sopra IP (per i dettagli sul protocollo si veda e sez. B.2). I dati vengono inviati in forma di pacchetti, e non ne ` assicurata n la eettiva e e ricezione n larrivo nellordine in cui vengono inviati. Il vantaggio del protocollo ` la velocit`, e e a non ` necessario trasmettere le informazioni di controllo ed il risultato ` una trasmissione di dati e e pi` veloce ed immediata. u Questo signica che a dierenza dei socket TCP i socket UDP non supportano una comunicazione di tipo stream in cui si ha a disposizione un usso continuo di dati che pu` essere letto o un po alla volta, ma piuttosto una comunicazione di tipo datagram, in cui i dati arrivano in singoli blocchi che devono essere letti integralmente. Questo diverso comportamento signica anche che i socket UDP, pur appartenendo alla famiglia PF_INET1 devono essere aperti quando si usa la funzione socket (si riveda quanto illustrato a suo tempo in tab. 15.2) utilizzando per il tipo di socket il valore SOCK_DGRAM. Questa dierenza comporta ovviamente che anche le modalit` con cui si usano i socket UDP a sono completamente diverse rispetto ai socket TCP, ed in particolare non esistendo il concetto di connessione non esiste il meccanismo del three way handshake n quello degli stati del protocollo. e In realt` tutto quello che avviene nella comunicazione attraverso dei socket UDP ` la trasmissione a e di un pacchetto da un client ad un server o viceversa, secondo lo schema illustrato in g. 18.1. Come illustrato in g. 18.1 la struttura generica di un server UDP prevede, una volta creato il socket, la chiamata a bind per mettersi in ascolto dei dati. Questa ` lunica parte comune e con un server TCP: non essendovi il concetto di connessione le funzioni listen ed accept non sono mai utilizzate nel caso di server UDP. La ricezione dei dati dal client avviene attraverso la funzione recvfrom, mentre una eventuale risposta sar` inviata con la funzione sendto. a
1

o PF_INET6 qualora si usasse invece il protocollo IPv6, che pure supporta UDP.

559

560

CAPITOLO 18. GLI ALTRI TIPI DI SOCKET

Figura 18.1: Lo schema di interscambio dei pacchetti per una comunicazione via UDP.

Da parte del client invece, una volta creato il socket non sar` necessario connettersi con a connect (anche se, come vedremo in sez. 18.1.6, ` possibile usare questa funzione, con un signie cato comunque diverso) ma si potr` eettuare direttamente una richiesta inviando un pacchetto a con la funzione sendto e si potr` leggere una eventuale risposta con la funzione recvfrom. a Anche se UDP ` completamente diverso rispetto a TCP resta identica la possibilit` di gestire e a pi` canali di comunicazione fra due macchine utilizzando le porte. In questo caso il server dovr` u a usare comunque la funzione bind per scegliere la porta su cui ricevere i dati, e come nel caso dei socket TCP si potr` usare il comando netstat per vericare quali socket sono in ascolto: a [piccardi@gont gapil]# netstat -anu Active Internet connections (servers and established) Proto Recv-Q Send-Q Local Address Foreign Address udp 0 0 0.0.0.0:32768 0.0.0.0:* udp 0 0 192.168.1.2:53 0.0.0.0:* udp 0 0 127.0.0.1:53 0.0.0.0:* udp 0 0 0.0.0.0:67 0.0.0.0:*

State

in questo caso abbiamo attivi il DNS (sulla porta 53, e sulla 32768 per la connessione di controllo del server named) ed un server DHCP (sulla porta 67). Si noti per` come in questo caso la colonna che indica lo stato sia vuota. I socket UDP o infatti non hanno uno stato. Inoltre anche in presenza di traco non si avranno indicazioni delle connessioni attive, proprio perch questo concetto non esiste per i socket UDP, il kernel e si limita infatti a ricevere i pacchetti ed inviarli al processo in ascolto sulla porta cui essi sono destinati, oppure a scartarli inviando un messaggio ICMP port unreachable qualora non vi sia nessun processo in ascolto.

18.1.2

Le funzioni sendto e recvfrom

Come accennato in sez. 18.1.1 le due funzioni principali usate per la trasmissione di dati attraverso i socket UDP sono sendto e recvfrom. La necessit` di usare queste funzioni ` dovuta al a e fatto che non esistendo con UDP il concetto di connessione, non si ha neanche a disposizione un socket connesso su cui sia possibile usare direttamente read e write avendo gi` stabilito (grazie a alla chiamata ad accept che lo associa ad una connessione) quali sono sorgente e destinazione dei dati.

18.1. I SOCKET UDP

561

Per questo motivo nel caso di UDP diventa essenziale utilizzare queste due funzioni, che sono comunque utilizzabili in generale per la trasmissione di dati attraverso qualunque tipo di socket. Esse hanno la caratteristica di prevedere tre argomenti aggiuntivi attraverso i quali ` possibile e specicare la destinazione dei dati trasmessi o ottenere lorigine dei dati ricevuti. La prima di queste funzioni ` sendto ed il suo prototipo2 `: e e
#include <sys/types.h> #include <sys/socket.h> ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen) Trasmette un messaggio ad un altro socket. La funzione restituisce il numero di caratteri inviati in caso di successo e -1 per un errore; nel qual caso errno viene impostata al rispettivo codice di errore: EAGAIN il socket ` in modalit` non bloccante, ma loperazione richiede che la funzione si e a blocchi.

ECONNRESET laltro capo della comunicazione ha resettato la connessione. EDESTADDRREQ il socket non ` di tipo connesso, e non si ` specicato un indirizzo di destinazione. e e EISCONN EMSGSIZE ENOBUFS ENOTCONN il socket ` gi` connesso, ma si ` specicato un destinatario. e a e il tipo di socket richiede linvio dei dati in un blocco unico, ma la dimensione del messaggio lo rende impossibile. la coda di uscita dellinterfaccia ` gi` piena (di norma Linux non usa questo messaggio e a ma scarta silenziosamente i pacchetti). il socket non ` connesso e non si ` specicata una destinazione. e e il capo locale della connessione ` stato chiuso, si ricever` anche un segnale di SIGPIPE, e a a meno di non aver impostato MSG_NOSIGNAL in flags.

EOPNOTSUPP il valore di flag non ` appropriato per il tipo di socket usato. e EPIPE

ed anche EFAULT, EBADF, EINVAL, EINTR, ENOMEM, ENOTSOCK pi` gli eventuali altri errori relativi ai u protocolli utilizzati.

I primi tre argomenti sono identici a quelli della funzione write e specicano il socket sockfd a cui si fa riferimento, il buer buf che contiene i dati da inviare e la relativa lunghezza len. Come per write la funzione ritorna il numero di byte inviati; nel caso di UDP per` questo deve o sempre corrispondere alla dimensione totale specicata da len in quanto i dati vengono sempre inviati in forma di pacchetto e non possono essere spezzati in invii successivi. Qualora non ci sia spazio nel buer di uscita la funzione si blocca (a meno di non avere aperto il socket in modalit` a non bloccante), se invece non ` possibile inviare il messaggio allinterno di un unico pacchetto (ad e esempio perch eccede le dimensioni massime del protocollo sottostante utilizzato) essa fallisce e con lerrore di EMSGSIZE. I due argomenti to e tolen servono a specicare la destinazione del messaggio da inviare, e indicano rispettivamente la struttura contenente lindirizzo di questultima e la sua dimensione; questi argomenti vanno specicati stessa forma in cui li si sarebbero usati con connect. Nel nostro caso to dovr` puntare alla struttura contenente lindirizzo IP e la porta di destinazione a verso cui si vogliono inviare i dati (questo ` indierente rispetto alluso di TCP o UDP, usando e socket diversi si sarebbero dovute utilizzare le rispettive strutture degli indirizzi). Se il socket ` di un tipo che prevede le connessioni (ad esempio un socket TCP), questo e deve essere gi` connesso prima di poter eseguire la funzione, in caso contrario si ricever` un a a errore di ENOTCONN. In questo specico caso in cui gli argomenti to e tolen non servono essi dovranno essere inizializzati rispettivamente a NULL e 0; normalmente quando si opera su un
il prototipo illustrato ` quello utilizzato dalle glibc, che seguono le Single Unix Specication, largomento e flags era di tipo int nei vari BSD4.*, mentre nelle libc4 e libc5 veniva usato un unsigned int; largomento len era int nei vari BSD4.* e nelle libc4, ma size_t nelle libc5; inne largomento tolen era int nei vari BSD4.* nelle libc4 e nelle libc5.
2

562

CAPITOLO 18. GLI ALTRI TIPI DI SOCKET

socket connesso essi vengono ignorati, ma qualora si sia specicato un indirizzo ` possibile e ricevere un errore di EISCONN. Finora abbiamo tralasciato largomento flags; questo ` un intero usato come maschera e binaria che permette di impostare una serie di modalit` di funzionamento della comunicazione a attraverso il socket (come MSG_NOSIGNAL che impedisce linvio del segnale SIGPIPE quando si ` e gi` chiuso il capo locale della connessione). Torneremo con maggiori dettagli sul signicato di a questo argomento in sez. 19.1.1, dove tratteremo le funzioni avanzate dei socket, per il momento ci si pu` limitare ad usare sempre un valore nullo. o La seconda funzione utilizzata nella comunicazione fra socket UDP ` recvfrom, che serve a e ricevere i dati inviati da un altro socket; il suo prototipo3 `: e

#include <sys/types.h> #include <sys/socket.h> ssize_t recvfrom(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *from, socklen_t *fromlen) Riceve un messaggio ad un socket. La funzione restituisce il numero di byte ricevuti in caso di successo e -1 in caso di errore; nel qual caso errno assumer` il valore: a EAGAIN il socket ` in modalit` non bloccante, ma loperazione richiede che la funzione si e a blocchi, oppure si ` impostato un timeout in ricezione e questo ` scaduto. e e

ECONNREFUSED laltro capo della comunicazione ha riutato la connessione (in genere perch il e relativo servizio non ` disponibile). e ENOTCONN il socket ` di tipo connesso, ma non si ` eseguita la connessione. e e ed anche EFAULT, EBADF, EINVAL, EINTR, ENOMEM, ENOTSOCK pi` gli eventuali altri errori relativi ai u protocolli utilizzati.

Come per sendto i primi tre argomenti sono identici agli analoghi di read: dal socket vengono letti len byte che vengono salvati nel buer buf. A seconda del tipo di socket (se di tipo datagram o di tipo stream) i byte in eccesso che non sono stati letti possono rispettivamente andare persi o restare disponibili per una lettura successiva. Se non sono disponibili dati la funzione si blocca, a meno di non aver aperto il socket in modalit` non bloccante, nel qual caso si avr` il solito a a errore di EAGAIN. Qualora len ecceda la dimensione del pacchetto la funzione legge comunque i dati disponibili, ed il suo valore di ritorno ` comunque il numero di byte letti. e I due argomenti from e fromlen sono utilizzati per ottenere lindirizzo del mittente del pacchetto che ` stato ricevuto, e devono essere opportunamente inizializzati con i puntatori alle e variabili dove la struttura contenente questultimo e la relativa lunghezza saranno scritti (si noti che fromlen ` un valore intero ottenuto come value result argument). Se non si ` interessati a e e questa informazione, entrambi gli argomenti devono essere inizializzati al valore NULL. Una dierenza fondamentale del comportamento di queste funzioni rispetto alle usuali read e write che abbiamo usato con i socket TCP ` che in questo caso ` perfettamente legale inviare e e con sendto un pacchetto vuoto (che nel caso conterr` solo le intestazioni di IP e di UDP), a specicando un valore nullo per len. Allo stesso modo ` possibile ricevere con recvfrom un valore e di ritorno di 0 byte, senza che questo possa congurarsi come una chiusura della connessione4 o come una cessazione delle comunicazioni.
3 il prototipo ` quello delle glibc che seguono le Single Unix Specication, i vari BSD4.*, le libc4 e le libc5 e usano un int come valore di ritorno; per gli argomenti flags e len vale quanto detto a proposito di sendto; inne largomento fromlen ` int per i vari BSD4.*, le libc4 e le libc5. e 4 dato che la connessione non esiste, non ha senso parlare di chiusura della connessione, questo signica anche che con i socket UDP non ` necessario usare close o shutdown per terminare la comunicazione. e

18.1. I SOCKET UDP

563

18.1.3

Un client UDP elementare

Vediamo allora come implementare un primo client elementare con dei socket UDP. Ricalcando quanto fatto nel caso dei socket TCP prenderemo come primo esempio luso del servizio daytime, utilizzando questa volta UDP. Il servizio ` denito nellRFC 867, che nel caso di uso di UDP e prescrive che il client debba inviare un pacchetto UDP al server (di contenuto non specicato), il quale risponder` a inviando a sua volta un pacchetto UDP contenente la data. a
int main ( int argc , char * argv []) { 3 int sock ; 4 int i , nread ; 5 struct sockaddr_in addr ; 6 char buffer [ MAXLINE ]; 7 ... 8 /* create socket */ 9 if ( ( sock = socket ( AF_INET , SOCK_DGRAM , 0)) < 0) { 10 perror ( " Socket creation error " ); 11 return -1; 12 } 13 /* initialize address */ 14 memset (( void *) & addr , 0 , sizeof ( addr )); /* clear server address */ 15 addr . sin_family = AF_INET ; /* address type is INET */ 16 addr . sin_port = htons (13); /* daytime port is 13 */ 17 /* build address using inet_pton */ 18 if ( ( inet_pton ( AF_INET , argv [ optind ] , & addr . sin_addr )) <= 0) { 19 perror ( " Address creation error " ); 20 return -1; 21 } 22 /* send request packet */ 23 nread = sendto ( sock , NULL , 0 , 0 , ( struct sockaddr *)& addr , sizeof ( addr )); 24 if ( nread < 0) { 25 perror ( " Request error " ); 26 return -1; 27 } 28 nread = recvfrom ( sock , buffer , MAXLINE , 0 , NULL , NULL ); 29 if ( nread < 0) { 30 perror ( " Read error " ); 31 return -1; 32 } 33 /* print results */ 34 if ( nread > 0) { 35 buffer [ nread ]=0; 36 if ( fputs ( buffer , stdout ) == EOF ) { /* write daytime */ 37 perror ( " fputs error " ); 38 return -1; 39 } 40 } 41 /* normal exit */ 42 return 0; 43 }
1 2

Figura 18.2: Sezione principale del client per il servizio daytime su UDP.

In g. 18.2 ` riportato la sezione principale del codice del nostro client, il sorgente completo si e trova nel le UDP_daytime.c distribuito con gli esempi allegati alla guida; al solito si ` tralasciato e di riportare in gura la sezione relativa alla gestione delle opzioni a riga di comando (nel caso praticamente assenti).

564

CAPITOLO 18. GLI ALTRI TIPI DI SOCKET

Il programma inizia (9-12) con la creazione del socket, al solito uscendo dopo aver stampato un messaggio in caso errore. Si noti come in questo caso, rispetto allanalogo client basato su socket TCP di g. 16.8 si sia usato per il tipo di socket il valore SOCK_DGRAM, pur mantenendosi nella stessa famiglia data da AF_INET. Il passo successivo (13-21) ` linizializzazione della struttura degli indirizzi; prima (14) si e cancella completamente la stessa con memset, (15) poi si imposta la famiglia dellindirizzo ed inne (16 la porta. Inne (18-21) si ricava lindirizzo del server da contattare dallargomento passato a riga di comando, convertendolo con inet_pton. Si noti come questa sezione sia identica a quella del client TCP di g. 16.8, in quanto la determinazione delluso di UDP al posto di TCP ` stata eettuata quando si ` creato il socket. e e Una volta completate le inizializzazioni inizia il corpo principale del programma, il primo passo ` inviare, come richiesto dal protocollo, un pacchetto al server. Questo lo si fa (16) inviando e un pacchetto vuoto (si ricordi quanto detto in sez. 18.1.2) con sendto, avendo cura di passare un valore nullo per il puntatore al buer e la lunghezza del messaggio. In realt` il protocollo non a richiede che il pacchetto sia vuoto, ma dato che il server comunque ne ignorer` il contenuto, ` a e inutile inviare dei dati. Vericato (24-27) che non ci siano stati errori nellinvio si provvede (28) ad invocare recvfrom per ricevere la risposta del server. Si controlla poi (29-32) che non vi siano stati errori in ricezione (uscendo con un messaggio in caso contrario); se ` tutto a posto la variabile nread conterr` la e a dimensione del messaggio di risposta inviato dal server che ` stato memorizzato su buffer, se e (34) pertanto il valore ` positivo si provveder` (35) a terminare la stringa contenuta nel buer e a di lettura5 e a stamparla (36) sullo standard output, controllando anche in questo caso (36-38) lesito delloperazione, ed uscendo con un messaggio in caso di errore. Se pertanto si ` avuto cura di attivare il server del servizio daytime 6 potremo vericare il e funzionamento del nostro client interrogando questultimo con: [piccardi@gont sources]$ ./daytime 127.0.0.1 Sat Mar 20 23:17:13 2004 ed osservando il traco con uno snier potremo eettivamente vedere lo scambio dei due pacchetti, quello vuoto di richiesta, e la risposta del server: [root@gont gapil]# tcpdump -i lo tcpdump: listening on lo 23:41:21.645579 localhost.32780 > localhost.daytime: udp 0 (DF) 23:41:21.645710 localhost.daytime > localhost.32780: udp 26 (DF) Una dierenza fondamentale del nostro client ` che in questo caso, non disponendo di una e connessione, ` per lui impossibile riconoscere errori di invio relativi alla rete. La funzione sendto e infatti riporta solo errori locali, i dati vengono comunque scritti e la funzione ritorna senza errori anche se il server non ` raggiungibile o non esiste un server in ascolto sullindirizzo di e destinazione. Questo comporta ad esempio che se si usa il nostro programma interrogando un server inesistente questo rester` perennemente bloccato nella chiamata a recvfrom, n quando a non lo interromperemo. Vedremo in sez. 18.1.6 come si pu` porre rimedio a questa problematica. o

18.1.4

Un server UDP elementare

Nella sezione precedente abbiamo visto come scrivere un client elementare per servizio daytime, vediamo in questa come deve essere scritto un server. Si ricordi che il compito di questultimo
5 si ricordi che, come illustrato in sez. 16.3.2, il server invia in risposta una stringa contenente la data, terminata dai due caratteri CR e LF, che pertanto prima di essere stampata deve essere opportunamente terminata con un NUL. 6 di norma questo ` un servizio standard fornito dal superdemone inetd, per cui basta abilitarlo nel le di e congurazione di questultimo, avendo cura di predisporre il servizio su UDP.

18.1. I SOCKET UDP

565

` quello di ricevere un pacchetto di richiesta ed inviare in risposta un pacchetto contenente una e stringa con la data corrente.

int main ( int argc , char * argv []) { 3 int sock ; 4 int i , n , len , verbose =0; 5 struct sockaddr_in addr ; 6 char buffer [ MAXLINE ]; 7 time_t timeval ; 8 ... 9 /* create socket */ 10 if ( ( sock = socket ( AF_INET , SOCK_DGRAM , 0)) < 0) { 11 perror ( " Socket creation error " ); 12 exit ( -1); 13 } 14 /* initialize address */ 15 memset (( void *)& addr , 0 , sizeof ( addr )); /* clear server address */ 16 addr . sin_family = AF_INET ; /* address type is INET */ 17 addr . sin_port = htons (13); /* daytime port is 13 */ 18 addr . sin_addr . s_addr = htonl ( INADDR_ANY ); /* connect from anywhere */ 19 /* bind socket */ 20 if ( bind ( sock , ( struct sockaddr *)& addr , sizeof ( addr )) < 0) { 21 perror ( " bind error " ); 22 exit ( -1); 23 } 24 /* write daytime to client */ 25 while (1) { 26 n = recvfrom ( sock , buffer , MAXLINE , 0 , ( struct sockaddr *)& addr , & len ); 27 if ( n < 0) { 28 perror ( " recvfrom error " ); 29 exit ( -1); 30 } 31 if ( verbose ) { 32 inet_ntop ( AF_INET , & addr . sin_addr , buffer , sizeof ( buffer )); 33 printf ( " Request from host %s , port % d \ n " , buffer , 34 ntohs ( addr . sin_port )); 35 } 36 timeval = time ( NULL ); 37 snprintf ( buffer , sizeof ( buffer ) , " %.24 s \ r \ n " , ctime (& timeval )); 38 n = sendto ( sock , buffer , strlen ( buffer ) , 0 , 39 ( struct sockaddr *)& addr , sizeof ( addr )); 40 if ( n < 0) { 41 perror ( " sendto error " ); 42 exit ( -1); 43 } 44 } 45 /* normal exit */ 46 exit (0); 47 }
1 2

Figura 18.3: Sezione principale del server per il servizio daytime su UDP.

In g. 18.3 ` riportato la sezione principale del codice del nostro client, il sorgente completo e si trova nel le UDP_daytimed.c distribuito con gli esempi allegati alla guida; anche in questo caso si ` omessa la sezione relativa alla gestione delle opzioni a riga di comando (la sola presente e ` -v che permette di stampare a video lindirizzo associato ad ogni richiesta). e Anche in questo caso la prima parte del server (9-23) ` sostanzialmente identica a quella e

566

CAPITOLO 18. GLI ALTRI TIPI DI SOCKET

dellanalogo server per TCP illustrato in g. 16.10; si inizia (10) con il creare il socket, uscendo con un messaggio in caso di errore (10-13), e di nuovo la sola dierenza con il caso precedente ` e il diverso tipo di socket utilizzato. Dopo di che (14-18) si inizializza la struttura degli indirizzi che poi (20) verr` usata da bind; si cancella (15) preventivamente il contenuto, si imposta (16) a la famiglia dellindirizzo, la porta (17) e lindirizzo (18) su cui si riceveranno i pacchetti. Si noti come in questultimo sia lindirizzo generico INADDR_ANY; questo signica (si ricordi quanto illustrato in sez. 16.2.1) che il server accetter` pacchetti su uno qualunque degli indirizzi presenti a sulle interfacce di rete della macchina. Completata linizializzazione tutto quello che resta da fare ` eseguire (20-23) la chiamata a e bind, controllando la presenza di eventuali errori, ed uscendo con un avviso qualora questo fosse il caso. Nel caso di socket UDP questo ` tutto quello che serve per consentire al server di ricevere e i pacchetti a lui indirizzati, e non ` pi` necessario chiamare successivamente listen. In questo e u caso infatti non esiste il concetto di connessione, e quindi non deve essere predisposta una coda delle connessioni entranti. Nel caso di UDP i pacchetti arrivano al kernel con un certo indirizzo ed una certa porta di destinazione, il kernel controlla se corrispondono ad un socket che ` stato e legato ad essi con bind, qualora questo sia il caso scriver` il contenuto allinterno del socket, cos` a che il programma possa leggerlo, altrimenti risponder` alla macchina che ha inviato il pacchetto a con un messaggio ICMP di tipo port unreachable. Una volta completata la fase di inizializzazione inizia il corpo principale (24-44) del server, mantenuto allinterno di un ciclo innito in cui si trattano le richieste. Il ciclo inizia (26) con una chiamata a recvfrom, che si bloccher` in attesa di pacchetti inviati dai client. Lo scopo della a funzione ` quello di ritornare tutte le volte che un pacchetto viene inviato al server, in modo da e poter ricavare da esso lindirizzo del client a cui inviare la risposta in addr. Per questo motivo in questo caso (al contrario di quanto fatto in g. 18.2) si ` avuto cura di passare gli argomenti e addr e len alla funzione. Dopo aver controllato (27-30) la presenza di eventuali errori (uscendo con un messaggio di errore qualora ve ne siano) si verica (31) se ` stata attivata lopzione -v e (che imposta la variabile verbose) stampando nel caso (32-35) lindirizzo da cui si ` appena e ricevuto una richiesta (questa sezione ` identica a quella del server TCP illustrato in g. 16.10). e Una volta ricevuta la richiesta resta solo da ottenere il tempo corrente (36) e costruire (37) la stringa di risposta, che poi verr` inviata (38) al client usando sendto, avendo al solito cura di a controllare (40-42) lo stato di uscita della funzione e trattando opportunamente la condizione di errore. Si noti come per le peculiarit` del protocollo si sia utilizzato un server iterativo, che processa a le richieste una alla volta via via che gli arrivano. Questa ` una caratteristica comune dei server e UDP, conseguenza diretta del fatto che non esiste il concetto di connessione, per cui non c` la e necessit` di trattare separatamente le singole connessioni. Questo signica anche che ` il kernel a a e gestire la possibilit` di richieste multiple in contemporanea; quello che succede ` semplicemente a e che il kernel accumula in un buer in ingresso i pacchetti UDP che arrivano e li restituisce al processo uno alla volta per ciascuna chiamata di recvfrom; nel nostro caso sar` poi compito del a server distribuire le risposte sulla base dellindirizzo da cui provengono le richieste.

18.1.5

Le problematiche dei socket UDP

Lesempio del servizio daytime illustrato nelle precedenti sezioni ` in realt` piuttosto particolare, e a e non evidenzia quali possono essere i problemi collegati alla mancanza di adabilit` e allassenza a del concetto di connessione che sono tipiche dei socket UDP. In tal caso infatti il protocollo ` e estremamente semplice, dato che la comunicazione consiste sempre in una richiesta seguita da una risposta, per uno scambio di dati eettuabile con un singolo pacchetto, per cui tutti gli eventuali problemi sarebbero assai pi` complessi da rilevare. u Anche qui per` possiamo notare che se il pacchetto di richiesta del client, o la risposta del o server si perdono, il client rester` permanentemente bloccato nella chiamata a recvfrom. Per a

18.1. I SOCKET UDP

567

evidenziare meglio quali problemi si possono avere proviamo allora con un servizio leggermente pi` complesso come echo. u

1 2 3

void ClientEcho ( FILE * filein , int socket , struct sockaddr_in * serv_add ); void SigTERM_hand ( int sig );

/* Program begin */ int main ( int argc , char * argv []) 6 { 7 /* 8 * Variables definition 9 */ 10 int sock , i ; 11 struct sockaddr_in serv_add ; 12 ... 13 /* create socket */ 14 if ( ( sock = socket ( AF_INET , SOCK_DGRAM , 0)) < 0) { 15 perror ( " Socket creation error " ); 16 return 1; 17 } 18 /* initialize address */ 19 memset (( void *) & serv_add , 0 , sizeof ( serv_add )); /* clear server address */ 20 serv_add . sin_family = AF_INET ; /* address type is INET */ 21 serv_add . sin_port = htons (7); /* echo port is 7 */ 22 /* build address using inet_pton */ 23 if ( ( inet_pton ( AF_INET , argv [ optind ] , & serv_add . sin_addr )) <= 0) { 24 perror ( " Address creation error " ); 25 return 1; 26 } 27 /* do read / write operations */ 28 ClientEcho ( stdin , sock , & serv_add ); 29 /* normal exit */ 30 return 0; 31 }
4 5

Figura 18.4: Sezione principale della prima versione client per il servizio echo su UDP.

In g. 18.4 ` riportato un estratto del corpo principale del nostro client elementare per il e servizio echo (al solito il codice completo ` con i sorgenti allegati). Le uniche dierenze con e lanalogo client visto in g. 16.11 sono che al solito si crea (14) un socket di tipo SOCK_DGRAM, e che non ` presente nessuna chiamata a connect. Per il resto il funzionamento del programma ` e e identico, e tutto il lavoro viene eettuato attraverso la chiamata (28) alla funzione ClientEcho che stavolta per` prende un argomento in pi`, che ` lindirizzo del socket. o u e Ovviamente in questo caso il funzionamento della funzione, il cui codice ` riportato in e g. 18.5, ` completamente diverso rispetto alla analoga del server TCP, e dato che non esie ste una connessione questa necessita anche di un terzo argomento, che ` lindirizzo del server cui e inviare i pacchetti. Data lassenza di una connessione come nel caso di TCP il meccanismo ` molto pi` semplice e u da gestire. Al solito si esegue un ciclo innito (6-30) che parte dalla lettura (7) sul buer di invio sendbuff di una stringa dallo standard input, se la stringa ` vuota (7-9), indicando che e linput ` terminato, si ritorna immediatamente causando anche la susseguente terminazione del e programma. Altrimenti si procede (10-11) allinvio della stringa al destinatario invocando sendto, utilizzando, oltre alla stringa appena letta, gli argomenti passati nella chiamata a ClientEcho, ed

568

CAPITOLO 18. GLI ALTRI TIPI DI SOCKET

void ClientEcho ( FILE * filein , int socket , struct sockaddr_in * serv_addr ) { 3 char sendbuff [ MAXLINE +1] , recvbuff [ MAXLINE +1]; 4 int nread , nwrite ; 5 /* initialize file descriptor set */ 6 while (1) { 7 if ( fgets ( sendbuff , MAXLINE , filein ) == NULL ) { 8 return ; /* if no input just return */ 9 } else { /* else we have to write to socket */ 10 nwrite = sendto ( socket , sendbuff , strlen ( sendbuff ) , 0 , 11 ( struct sockaddr *) serv_addr , sizeof (* serv_addr )); 12 if ( nwrite < 0) { /* on error stop */ 13 printf ( " Errore in scrittura : % s " , strerror ( errno )); 14 return ; 15 } 16 } 17 nread = recvfrom ( socket , recvbuff , strlen ( sendbuff ) , 0 , NULL , NULL ); 18 if ( nread < 0) { /* error condition , stop client */ 19 printf ( " Errore in lettura : % s \ n " , strerror ( errno )); 20 return ; 21 } 22 recvbuff [ nread ] = 0; /* else read is ok , write on stdout */ 23 if ( fputs ( recvbuff , stdout ) == EOF ) { 24 perror ( " Errore in scrittura su terminale " ); 25 return ; 26 } 27 } 28 }
1 2

Figura 18.5: Codice della funzione ClientEcho usata dal client per il servizio echo su UDP.

in particolare lindirizzo del server che si ` posto in serv_addr; qualora (12) si riscontrasse un e errore si provveder` al solito (13-14) ad uscire con un messaggio di errore. a Il passo immediatamente seguente (17) linvio ` quello di leggere leventuale risposta del e server con recvfrom; si noti come in questo caso si sia scelto di ignorare lindirizzo delleventuale pacchetto di risposta, controllando (18-21) soltanto la presenza di un errore (nel qual caso al solito si ritorna dopo la stampa di un adeguato messaggio). Si noti anche come, rispetto allanaloga funzione ClientEcho utilizzata nel client TCP illustrato in sez. 16.4.2 non si sia controllato il caso di un messaggio nullo, dato che, nel caso di socket UDP, questo non signica la terminazione della comunicazione. Lultimo passo (17) ` quello di terminare opportunamente la stringa di risposta nel relativo e buer per poi provvedere alla sua stampa sullo standard output, eseguendo il solito controllo (ed eventuale uscita con adeguato messaggio informativo) in caso di errore. In genere ntanto che si esegue il nostro client in locale non sorger` nessun problema, se a per` si prover` ad eseguirlo attraverso un collegamento remoto (nel caso dellesempio seguente o a su una VPN, attraverso una ADSL abbastanza congestionata) e in modalit` non interattiva, la a probabilit` di perdere qualche pacchetto aumenta, ed infatti, eseguendo il comando come: a [piccardi@gont sources]$ cat UDP_echo.c | ./echo 192.168.1.120 /* UDP_echo.c * * Copyright (C) 2004 Simone Piccardi ... ...

18.1. I SOCKET UDP /* * Include needed headers

569

si otterr` che, dopo aver correttamente stampato alcune righe, il programma si blocca completaa mente senza stampare pi` niente. Se al contempo si fosse tenuto sotto controllo il traco UDP u diretto o proveniente dal servizio echo con tcpdump si sarebbe ottenuto: [root@gont gapil]# tcpdump \( dst port 7 or src port 7 \) ... ... 18:48:16.390255 gont.earthsea.ea.32788 > 192.168.1.120.echo: 18:48:17.177613 192.168.1.120.echo > gont.earthsea.ea.32788: 18:48:17.177790 gont.earthsea.ea.32788 > 192.168.1.120.echo: 18:48:17.964917 192.168.1.120.echo > gont.earthsea.ea.32788: 18:48:17.965408 gont.earthsea.ea.32788 > 192.168.1.120.echo:

udp udp udp udp udp

4 (DF) 4 (DF) 26 (DF) 26 (DF) 4 (DF)

che come si vede il traco fra client e server si interrompe dopo linvio di un pacchetto UDP per il quale non si ` ricevuto risposta. e Il problema ` che in tutti i casi in cui un pacchetto di risposta si perde, o una richiesta non e arriva a destinazione, il nostro programma si bloccher` nellesecuzione di recvfrom. Lo stesso a avviene anche se il server non ` in ascolto, in questo caso per`, almeno dal punto di vista dello e o scambio di pacchetti, il risultato ` diverso, se si lancia al solito il programma e si prova a scrivere e qualcosa si avr` ugualmente un blocco su recvfrom ma se si osserva il traco con tcpdump si a vedr` qualcosa del tipo: a [root@gont gapil]# tcpdump \( dst 192.168.0.2 and src 192.168.1.120 \) \ or \( src 192.168.0.2 and dst 192.168.1.120 \) tcpdump: listening on eth0 00:43:27.606944 gont.earthsea.ea.32789 > 192.168.1.120.echo: udp 6 (DF) 00:43:27.990560 192.168.1.120 > gont.earthsea.ea: icmp: 192.168.1.120 udp port echo unreachable [tos 0xc0] cio` in questo caso si avr` in risposta un pacchetto ICMP di destinazione irraggiungibile che ci e a segnala che la porta in questione non risponde. Ci si pu` chiedere allora perch, bench la situazione di errore sia rilevabile, questa non venga o e e segnalata. Il luogo pi` naturale in cui riportarla sarebbe la chiamata di sendto, in quanto ` a u e causa delluso di un indirizzo sbagliato che il pacchetto non pu` essere inviato; farlo in questo o punto per` ` impossibile, dato che linterfaccia di programmazione richiede che la funzione ritorni oe non appena il kernel invia il pacchetto,7 e non pu` bloccarsi in una attesa di una risposta che o potrebbe essere molto lunga (si noti infatti che il pacchetto ICMP arriva qualche decimo di secondo pi` tardi) o non esserci aatto. u Si potrebbe allora pensare di riportare lerrore nella recvfrom che ` comunque bloccata in e attesa di una risposta che nel caso non arriver` mai. La ragione per cui non viene fatto ` piuttosto a e sottile e viene spiegata da Stevens in [12] con il seguente esempio: si consideri un client che invia tre pacchetti a tre diverse macchine, due dei quali vengono regolarmente ricevuti, mentre al terzo, non essendo presente un server sulla relativa macchina, viene risposto con un messaggio ICMP come il precedente. Detto messaggio conterr` anche le informazioni relative ad indirizzo a e porta del pacchetto che ha fallito, per` tutto quello che il kernel pu` restituire al programma o o
questo ` il classico caso di errore asincrono, una situazione cio` in cui la condizione di errore viene rilevata in e e maniera asincrona rispetto alloperazione che lha causata, una eventualit` piuttosto comune quando si ha a che a fare con la rete, tutti i pacchetti ICMP che segnalano errori rientrano in questa tipologia.
7

570

CAPITOLO 18. GLI ALTRI TIPI DI SOCKET

` un codice di errore in errno, con il quale ` impossibile di distinguere per quale dei pacchetti e e inviati si ` avuto lerrore; per questo ` stata fatta la scelta di non riportare un errore su un e e socket UDP, a meno che, come vedremo in sez. 18.1.6, questo non sia connesso.

18.1.6

Luso della funzione connect con i socket UDP

Come illustrato in sez. 18.1.1 essendo i socket UDP privi di connessione non ` necessario per i e client usare connect prima di iniziare una comunicazione con un server. Ci` non di meno abbiamo o accennato come questa possa essere utilizzata per gestire la presenza di errori asincroni. Quando si chiama connect su di un socket UDP tutto quello che succede ` che lindirizzo e passato alla funzione viene registrato come indirizzo di destinazione del socket. A dierenza di quanto avviene con TCP non viene scambiato nessun pacchetto, tutto quello che succede ` che e da quel momento in qualunque cosa si scriva sul socket sar` inviata a quellindirizzo; non sar` a a pi` necessario usare largomento to di sendto per specicare la destinazione dei pacchetti, che u potranno essere inviati e ricevuti usando le normali funzioni read e write.8 Una volta che il socket ` connesso cambia per` anche il comportamento in ricezione; prima e o infatti il kernel avrebbe restituito al socket qualunque pacchetto ricevuto con un indirizzo di destinazione corrispondente a quello del socket, senza nessun controllo sulla sorgente; una volta che il socket viene connesso saranno riportati su di esso solo i pacchetti con un indirizzo sorgente corrispondente a quello a cui ci si ` connessi. e Inne quando si usa un socket connesso, venendo meno lambiguit` segnalata alla ne di a sez. 18.1.5, tutti gli eventuali errori asincroni vengono riportati alle funzioni che operano su di esso; pertanto potremo riscrivere il nostro client per il servizio echo con le modiche illustrate in g. 18.6. Ed in questo caso rispetto alla precedente versione, il solo cambiamento ` lutilizzo (17) della e funzione connect prima della chiamata alla funzione di gestione del protocollo, che a sua volta ` stata modicata eliminando lindirizzo passato come argomento e sostituendo le chiamata a e sendto e recvfrom con chiamate a read e write come illustrato dal nuovo codice riportato in g. 18.7. Utilizzando questa nuova versione del client si pu` vericare che quando ci si rivolge verso o un indirizzo inesistente o su cui non ` in ascolto un server si ` in grado rilevare lerrore, se infatti e e eseguiamo il nuovo programma otterremo un qualcosa del tipo: [piccardi@gont sources]$ ./echo 192.168.1.1 prova Errore in lettura: Connection refused Ma si noti che a dierenza di quanto avveniva con il client TCP qui lerrore viene rilevato soltanto dopo che si ` tentato di inviare qualcosa, ed in corrispondenza al tentativo di lettura e della risposta. Questo avviene perch con UDP non esiste una connessione, e ntanto che non si e invia un pacchetto non c` traco sulla rete. In questo caso lerrore sar` rilevato alla ricezione e a del pacchetto ICMP destination unreachable emesso dalla macchina cui ci si ` rivolti, e questa e volta, essendo il socket UDP connesso, il kernel potr` riportare detto errore in user space in a maniera non ambigua, ed esso apparir` alla successiva lettura sul socket. a Si tenga presente inne che luso dei socket connessi non risolve laltro problema del client, e cio` il fatto che in caso di perdita di un pacchetto questo rester` bloccato permanentemente in e a attesa di una risposta. Per risolvere questo problema lunico modo sarebbe quello di impostare un timeout o riscrivere il client in modo da usare lI/O non bloccante.
in realt` si pu` anche continuare ad usare la funzione sendto, ma in tal caso largomento to deve essere a o inizializzato a NULL, e tolen deve essere inizializzato a zero, pena un errore.
8

18.2. I SOCKET UNIX DOMAIN

571

void ClientEcho ( FILE * filein , int socket ); /* Program begin */ 3 int main ( int argc , char * argv []) 4 { 5 /* 6 * Variables definition 7 */ 8 int sock , i ; 9 struct sockaddr_in dst_addr ; 10 ... 11 /* create socket */ 12 if ( ( sock = socket ( AF_INET , SOCK_DGRAM , 0)) < 0) { 13 perror ( " Socket creation error " ); 14 return 1; 15 } 16 /* initialize address */ 17 memset (( void *) & dst_addr , 0 , sizeof ( dst_addr )); /* clear address */ 18 dst_addr . sin_family = AF_INET ; /* address type is INET */ 19 dst_addr . sin_port = htons (7); /* echo port is 7 */ 20 /* build address using inet_pton */ 21 if ( ( inet_pton ( AF_INET , argv [ optind ] , & dst_addr . sin_addr )) <= 0) { 22 perror ( " Address creation error " ); 23 return 1; 24 } 25 connect ( sock , ( struct sockaddr *) & dst_addr , sizeof ( dst_addr )); 26 /* do read / write operations */ 27 ClientEcho ( stdin , sock ); 28 /* normal exit */ 29 return 0; 30 }
1 2

Figura 18.6: Seconda versione del client del servizio echo che utilizza socket UDP connessi.

18.2

I socket Unix domain

Bench i socket Unix domain, come meccanismo di comunicazione fra processi che girano sulla e stessa macchina, non siano strettamente attinenti alla rete, li tratteremo comunque in questa sezione. Nonostante le loro peculiarit` infatti, linterfaccia di programmazione che serve ad a utilizzarli resta sempre quella dei socket.

18.2.1

Il passaggio di le descriptor

18.3

Altri socket

Tratteremo in questa sezione gli altri tipi particolari di socket supportati da Linux, come quelli relativi a particolare protocolli di trasmissione, i socket netlink che deniscono una interfaccia di comunicazione con il kernel, ed i packet socket che consentono di inviare pacchetti direttamente a livello delle interfacce di rete.

18.3.1

I socket raw

Tratteremo in questa sezione i cosiddetti raw socket, con i quali si possono forgiare direttamente i pacchetti a tutti i livelli dello stack dei protocolli.

572

CAPITOLO 18. GLI ALTRI TIPI DI SOCKET

void ClientEcho ( FILE * filein , int socket ) { 3 char sendbuff [ MAXLINE +1] , recvbuff [ MAXLINE +1]; 4 int nread , nwrite ; 5 /* initialize file descriptor set */ 6 while (1) { 7 if ( fgets ( sendbuff , MAXLINE , filein ) == NULL ) { 8 return ; /* if no input just return */ 9 } else { /* else we have to write to socket */ 10 nwrite = write ( socket , sendbuff , strlen ( sendbuff )); 11 if ( nwrite < 0) { /* on error stop */ 12 printf ( " Errore in scrittura : % s " , strerror ( errno )); 13 return ; 14 } 15 } 16 nread = read ( socket , recvbuff , strlen ( sendbuff )); 17 if ( nread < 0) { /* error condition , stop client */ 18 printf ( " Errore in lettura : % s \ n " , strerror ( errno )); 19 return ; 20 } 21 recvbuff [ nread ] = 0; /* else read is ok , write on stdout */ 22 if ( fputs ( recvbuff , stdout ) == EOF ) { 23 perror ( " Errore in scrittura su terminale " ); 24 return ; 25 } 26 } 27 }
1 2

Figura 18.7: Seconda versione della funzione ClientEcho.

18.3.2 18.3.3

I socket netlink I packet socket

Capitolo 19

Socket avanzati
Esamineremo in questo capitolo le funzionalit` pi` evolute della gestione dei socket, le funzioni a u avanzate, la gestione dei dati urgenti e out-of-band e dei messaggi ancillari, come luso come luso del I/O multiplexing (vedi sez. 11.1) con i socket.

19.1

Le funzioni di I/O avanzate

Tratteremo in questa sezione le funzioni di I/O pi` avanzate che permettono di controllare le u funzionalit` speciche della comunicazione dei dati che sono disponibili con i vari tipi di socket. a

19.1.1

La funzioni sendmsg e recvmsg

Finora abbiamo trattato delle funzioni che permettono di inviare dati sul socket in forma semplicata. Se infatti si devono semplicemente ...

19.1.2

I messaggi ancillari

Quanto ` stata attivata lopzione IP_RECVERR il kernel attiva per il socket una speciale coe da su cui vengono inviati tutti gli errori riscontrati. Questi possono essere riletti usando il ag MSG_ERRQUEUE, nel qual caso sar` passato come messaggio ancillare una struttura di tipo a sock_extended_err illustrata in g. 19.1.

struct sock_extended_err { u_int32_t ee_errno ; u_int8_t ee_origin ; u_int8_t ee_type ; u_int8_t ee_code ; u_int8_t ee_pad ; u_int32_t ee_info ; u_int32_t ee_data ; /* More data may follow */ };

/* /* /* /*

error number */ where the error originated */ type */ code */

/* additional information */ /* other data */

Figura 19.1: La struttura sock_extended_err usata dallopzione IP_RECVERR per ottenere le informazioni relative agli errori su un socket.

573

574

CAPITOLO 19. SOCKET AVANZATI

19.1.3

I dati urgenti o out-of-band

Una caratteristica particolare dei socket TCP ` quella che consente di inviare allaltro capo e della comunicazione una sorta di messaggio privilegiato, che si richiede che sia trattato il prima possibile. Si fa riferimento a questa funzionalit` come allinvio dei cosiddetti dati urgenti (o a urgent data); talvolta essi chiamati anche dati out-of-band poich, come vedremo pi` avanti, e u possono essere letti anche al di fuori del usso di dati normale. Come gi` accennato in sez. 11.1 la presenza di dati urgenti viene rilevata in maniera specica a sia di select (con il le descriptor set exceptfds) che da poll (con la condizione POLLRDBAND). Le modalit` di lettura dei dati urgenti sono due, la prima e pi` comune prevede luso di a u recvmsg con La seconda modalit` di lettura prevede invece luso dellopzione dei socket SO_OOBINLINE a (vedi sez. 17.2.2) che consente di ricevere i dati urgenti direttamente nel usso dei dati del socket; in tal caso per` si pone il problema di come distinguere i dati normali da quelli urgenti. Come o gi` accennato in sez. 17.3.3 a questo scopo si pu` usare ioctl con loperazione SIOCATMARK, che a o consente di sapere se si ` arrivati o meno allurgent mark. e La procedura allora prevede che, una volta che si sia rilevata la presenza di dati urgenti, si ripeta la lettura ordinaria dal socket ntanto che SIOCATMARK non restituisce un valore diverso da zero; la successiva lettura restituir` i dati urgenti. a

19.2

Luso dellI/O non bloccante

Tratteremo in questa sezione le modalit` avanzate che permettono di utilizzare i socket con una a comunicazione non bloccante, in modo da

19.2.1

La gestione delle opzioni IP

Abbiamo visto in sez. 17.2.4 come di possa usare setsockopt con lopzione IP_OPTIONS per impostare le opzioni IP associate per i pacchetti associati ad un socket. Vedremo qui il signicato di tali opzioni e le modalit` con cui esse possono essere utilizzate ed impostate. a

Parte III

Appendici

575

Appendice A

Il livello di rete
In questa appendice prenderemo in esame i vari protocolli disponibili a livello di rete.1 Per ciascuno di essi forniremo una descrizione generica delle principali caratteristiche, del formato di dati usato e quanto possa essere necessario per capirne meglio il funzionamento dal punto di vista della programmazione. Data la loro prevalenza il capitolo sar` sostanzialmente incentrato sui due protocolli prina cipali esistenti su questo livello: il protocollo IP, sigla che sta per Internet Protocol, (ma che pi` propriamente si dovrebbe chiamare IPv4) ed la nuova versione di questo stesso protocollo, u denominata IPv6. Tratteremo comunque anche il protocollo ICMP e la sua versione modicata per IPv6 (cio` ICMPv6). e

A.1

Il protocollo IP

Lattuale Internet Protocol (IPv4) viene standardizzato nel 1981 dallRFC 791; esso nasce per disaccoppiare le applicazioni della struttura hardware delle reti di trasmissione, e creare una interfaccia di trasmissione dei dati indipendente dal sottostante substrato di rete, che pu` essere o realizzato con le tecnologie pi` disparate (Ethernet, Token Ring, FDDI, ecc.). u

A.1.1

Introduzione

Il compito principale di IP ` quello di trasmettere i pacchetti da un computer allaltro della rete; e le caratteristiche essenziali con cui questo viene realizzato in IPv4 sono due: Universal addressing la comunicazione avviene fra due host identicati univocamente con un indirizzo a 32 bit che pu` appartenere ad una sola interfaccia di rete. o Best eort viene assicurato il massimo impegno nella trasmissione, ma non c` nessuna e garanzia per i livelli superiori n sulla percentuale di successo n sul tempo di consegna e e dei pacchetti di dati, n sullordine in cui vengono consegnati. e Per eettuare la comunicazione e linstradamento dei pacchetti fra le varie reti di cui ` e composta Internet IPv4 organizza gli indirizzi in una gerarchia a due livelli, in cui una parte dei 32 bit dellindirizzo indica il numero di rete, e unaltra lhost al suo interno. Il numero di rete serve ai router per stabilire a quale rete il pacchetto deve essere inviato, il numero di host indica la macchina di destinazione nale allinterno di detta rete.
per la spiegazione della suddivisione in livelli dei protocolli di rete, si faccia riferimento a quanto illustrato in sez. 14.2.
1

577

578

APPENDICE A. IL LIVELLO DI RETE

Per garantire lunicit` dellindirizzo Internet esiste unautorit` centrale (la IANA, Internet a a Assigned Number Authority) che assegna i numeri di rete alle organizzazioni che ne fanno richiesta; ` poi compito di questultime assegnare i numeri dei singoli host allinterno della propria e rete. Per venire incontro alle richieste dei vari enti e organizzazioni che volevano utilizzare questo protocollo di comunicazione, originariamente gli indirizzi di rete erano stati suddivisi allinterno delle cosiddette classi, (rappresentate in tab. A.1), in modo da consentire dispiegamenti di reti di varie dimensioni a seconda delle diverse esigenze.
7 bit classe A 0 net Id 14 bit classe B 1 0 net Id 21 bit classe C 1 1 0 net Id 28 bit classe D 1 1 1 0 multicast group Id 27 bit classe E 1 1 1 1 0 reserved for future use 24 bit host Id 16 bit host Id 8 bit host Id

Tabella A.1: Le classi di indirizzi secondo IPv4.

Le classi di indirizzi usate per il dispiegamento delle reti su quella che comunemente viene chiamata Internet sono le prime tre; la classe D ` destinata al multicast mentre la classe E ` e e riservata per usi sperimentali e non viene impiegata. Come si pu` notare per` la suddivisione riportata in tab. A.1 ` largamente ineciente in o o e quanto se ad un utente necessita anche solo un indirizzo in pi` dei 256 disponibili con una classe u A occorre passare a una classe B, che ne prevede 65536,2 con un conseguente spreco di numeri. Inoltre, in particolare per le reti di classe C, la presenza di tanti indirizzi di rete diversi comporta una crescita enorme delle tabelle di instradamento che ciascun router dovrebbe tenere in memoria per sapere dove inviare il pacchetto, con conseguente crescita dei tempi di elaborazione da parte di questi ultimi ed inecienza nel trasporto.
n bit CIDR net Id 32 n bit host Id

Tabella A.2: Uno esempio di indirizzamento CIDR.

Per questo nel 1992 ` stato introdotto un indirizzamento senza classi (il CIDR, Classless e Inter-Domain Routing) in cui il limite fra i bit destinati a indicare il numero di rete e quello destinati a indicare lhost nale pu` essere piazzato in qualunque punto (vedi tab. A.2), pero mettendo di accorpare pi` classi A su ununica rete o suddividere una classe B e diminuendo al u contempo il numero di indirizzi di rete da inserire nelle tabelle di instradamento dei router.
in realt` i valori esatti sarebbero 254 e 65536, una rete con a disposizione N bit dellindirizzo IP, ha disponibili a per le singole macchine soltanto @N 2 numeri, dato che uno deve essere utilizzato come indirizzo di rete e uno per lindirizzo di broadcast.
2

A.2. IL PROTOCOLLO IPV6

579

A.1.2

Lintestazione di IP

Come illustrato in g. 14.2 (si ricordi quanto detto in sez. 14.2.2 riguardo al funzionamento generale del TCP/IP), per eseguire il suo compito il protocollo IP inserisce (come praticamente ogni protocollo di rete) una opportuna intestazione in cima ai dati che deve trasmettere, la cui schematizzazione ` riportata in g. A.1. e

Figura A.1: Lintestazione o header di IPv4.

Ciascuno dei campi illustrati in g. A.1 ha un suo preciso scopo e signicato, che si ` riportato e brevemente in tab. A.3; si noti come lintestazione riporti sempre due indirizzi IP, quello sorgente, che indica lIP da cui ` partito il pacchetto (cio` lindirizzo assegnato alla macchina che lo e e spedisce) e quello destinazione che indica lindirizzo a cui deve essere inviato il pacchetto (cio` e lindirizzo assegnato alla macchina che lo ricever`). a Il campo TOS denisce il cosiddetto Type of Service; questo permette di denire il tipo di traco contenuto nei pacchetti, e pu` essere utilizzato dai router per dare diverse priorit` in o a base al valore assunto da questo campo.

A.1.3

Le opzioni di IP

A.2

Il protocollo IPv6

Negli anni 90 con la crescita del numero di macchine connesse su Internet si arriv` a temere o lesaurimento dello spazio degli indirizzi disponibili, specie in vista di una prospettiva (per ora rivelatasi prematura) in cui ogni apparecchio elettronico sarebbe stato inserito allinterno della rete. Per questo motivo si inizi` a progettare una nuova versione del protocollo o Lattuale Internet Protocol (IPv4) viene standardizzato nel 1981 dallRFC 719; esso nasce per disaccoppiare le applicazioni della struttura hardware delle reti di trasmissione, e creare una interfaccia di trasmissione dei dati indipendente dal sottostante substrato di rete, che pu` essere o realizzato con le tecnologie pi` disparate (Ethernet, Token Ring, FDDI, ecc.). u

A.2.1

I motivi della transizione

Negli ultimi anni la crescita vertiginosa del numero di macchine connesse a internet ha iniziato a far emergere i vari limiti di IPv4; in particolare si ` iniziata a delineare la possibilit` di arrivare e a a una carenza di indirizzi disponibili.

580
Nome version head length type of service Bit 4 4 8

APPENDICE A. IL LIVELLO DI RETE


Signicato Numero di versione, nel caso specico vale sempre 4. Lunghezza dellintestazione, in multipli di 32 bit. Il tipo di servizio, ` suddiviso in: 3 bit di precedenza, che nelle attuali e implementazioni del protocollo non vengono comunque utilizzati; un bit riservato che deve essere mantenuto a 0; 4 bit che identicano il tipo di servizio richiesto, uno solo dei quali pu` essere attivo. o La lunghezza totale, indica la dimensione del carico di dati del pacchetto IP in byte. Lidenticazione, assegnato alla creazione, ` aumentato di uno allorigie ne della trasmissione di ciascun pacchetto, ma resta lo stesso per i pacchetti frammentati, consentendo cos` di identicare quelli che derivano dallo stesso pacchetto originario. I ag di controllo nellordine: il primo ` riservato e sempre nullo, il e secondo indica se il pacchetto non pu` essere frammentato, il terzo se o ci sono ulteriori frammenti. Loset di frammento, indica la posizione del frammento rispetto al pacchetto originale. Il tempo di vita, ` decrementato di uno ogni volta che un router e ritrasmette il pacchetto, se arriva a zero il pacchetto viene scartato. Il protocollo, identica il tipo di pacchetto che segue lintestazione di IPv4. La checksum di intestazione, somma di controllo per lintestazione. Lindirizzo di origine. Lindirizzo di destinazione.

total length identication

16 16

ag

fragmentation oset time to live protocol header checksum source IP destination IP

13 16 8 16 32 32

Tabella A.3: Legenda per il signicato dei campi dellintestazione di IPv4

Costante IPTOS_LOWDELAY IPTOS_THROUGHPUT IPTOS_RELIABILITY IPTOS_MINCOST

Signicato Minimizza i ritardi per il traco interattivo. Ottimizza la trasmissione per il massimo usso di dati. Ottimizza per ladabilit` della trasmisa sione. Usato per dati di riempimento, dove non interessa se c` una bassa velocit` di e a trasmissione.

Tabella A.4: Le costanti che deniscono alcuni valori standard per il campo TOS da usare come argomento optval per lopzione IP_TOS.

In realt` il problema non ` propriamente legato al numero di indirizzi disponibili; infatti a e con 32 bit si hanno 232 , cio` circa 4 miliardi, numeri diversi possibili, che sono molti di pi` dei e u computer attualmente esistenti. Il punto ` che la suddivisione di questi numeri nei due livelli rete/host e lutilizzo delle classi e di indirizzamento mostrate in precedenza, ha comportato che, nella sua evoluzione storica, il dispiegamento delle reti e lallocazione degli indirizzi siano stati inecienti; neanche luso del CIDR ha permesso di eliminare le inecienze che si erano formate, dato che il ridispiegamento degli indirizzi comporta cambiamenti complessi a tutti i livelli e la riassegnazione di tutti gli indirizzi dei computer di ogni sottorete. Diventava perci` necessario progettare un nuovo protocollo che permettesse di risolvere questi o problemi, e garantisse essibilit` suciente per poter continuare a funzionare a lungo termine; in a particolare necessitava un nuovo schema di indirizzamento che potesse rispondere alle seguenti necessit`: a un maggior numero di numeri disponibili che consentisse di non restare pi` a corto di u

A.2. IL PROTOCOLLO IPV6 indirizzi unorganizzazione gerarchica pi` essibile dellattuale u

581

uno schema di assegnazione degli indirizzi in grado di minimizzare le dimensioni delle tabelle di instradamento uno spazio di indirizzi che consentisse un passaggio automatico dalle reti locali a internet

A.2.2

Principali caratteristiche di IPv6

Per rispondere alle esigenze descritte in sez. A.2.1 IPv6 nasce come evoluzione di IPv4, mantenendone inalterate le funzioni che si sono dimostrate valide, eliminando quelle inutili e aggiungendone poche altre ponendo al contempo una grande attenzione a mantenere il protocollo il pi` snello e veloce possibile. u I cambiamenti apportati sono comunque notevoli e possono essere riassunti a grandi linee nei seguenti punti: lespansione delle capacit` di indirizzamento e instradamento, per supportare una gerarchia a con pi` livelli di indirizzamento, un numero di nodi indirizzabili molto maggiore e una u autocongurazione degli indirizzi lintroduzione un nuovo tipo di indirizzamento, lanycast che si aggiungono agli usuali unicast e multicast la semplicazione del formato dellintestazione, eliminando o rendendo opzionali alcuni dei campi di IPv4, per eliminare la necessit` di riprocessare la stessa da parte dei router e a contenere laumento di dimensione dovuto ai nuovi indirizzi un supporto per le opzioni migliorato, per garantire una trasmissione pi` eciente del trafu co normale, limiti meno stringenti sulle dimensioni delle opzioni, e la essibilit` necessaria a per introdurne di nuove in futuro il supporto per delle capacit` di qualit` di servizio (QoS) che permetta di identicare a a gruppi di dati per i quali si pu` provvedere un trattamento speciale (in vista delluso di o internet per applicazioni multimediali e/o real-time)

A.2.3

Lintestazione di IPv6

Per capire le caratteristiche di IPv6 partiamo dallintestazione usata dal protocollo per gestire la trasmissione dei pacchetti; in g. A.2 ` riportato il formato dellintestazione di IPv6 da e confrontare con quella di IPv4 in g. A.1. La spiegazione del signicato dei vari campi delle due intestazioni ` riportato rispettivamente in tab. A.5 e tab. A.3) e Come si pu` notare lintestazione di IPv6 diventa di dimensione ssa, pari a 40 byte, contro o una dimensione (minima, in assenza di opzioni) di 20 byte per IPv4; un semplice raddoppio nonostante lo spazio destinato agli indirizzi sia quadruplicato, questo grazie a una notevole semplicazione che ha ridotto il numero dei campi da 12 a 8. Abbiamo gi` anticipato in sez. A.2.2 uno dei criteri principali nella progettazione di IPv6 ` a e stato quello di ridurre al minimo il tempo di elaborazione dei pacchetti da parte dei router, un confronto con lintestazione di IPv4 (vedi g. A.1) mostra le seguenti dierenze: ` stato eliminato il campo header length in quanto le opzioni sono state tolte dallintestae zione che ha cos` dimensione ssa; ci possono essere pi` intestazioni opzionali (intestazio u ni di estensione, vedi sez. A.2.12), ciascuna delle quali avr` un suo campo di lunghezza a allinterno.

582

APPENDICE A. IL LIVELLO DI RETE

Figura A.2: Lintestazione o header di IPv6. Nome version priority ow label payload length next header Bit 4 4 24 16 8 Signicato La versione, nel caso specico vale sempre 6. La priorit`, vedi sez. A.2.15. a Letichetta di usso, vedi sez. A.2.14. La lunghezza del carico, cio` del corpo dei dati che segue e lintestazione, in byte. Lintestazione successiva, identica il tipo di pacchetto che segue lintestazione di IPv6, ed usa gli stessi valori del campo protocollo nellintestazione di IPv4. Il limite di salti, ha lo stesso signicato del time to live nellintestazione di IPv4. Lindirizzo di origine. Lindirizzo di destinazione.

hop limit source IP destination IP

8 128 128

Tabella A.5: Legenda per il signicato dei campi dellintestazione di IPv6

lintestazione e gli indirizzi sono allineati a 64 bit, questo rende pi` veloce il processo da u parte di computer con processori a 64 bit. i campi per gestire la frammentazione (identication, ag e fragment oset) sono stati eliminati; questo perch la frammentazione ` uneccezione che non deve rallentare lelaboe e razione dei pacchetti nel caso normale. ` stato eliminato il campo checksum in quanto tutti i protocolli di livello superiore (TCP, e UDP e ICMPv6) hanno un campo di checksum che include, oltre alla loro intestazione e ai dati, pure i campi payload length, next header, e gli indirizzi di origine e di destinazione; una checksum esiste anche per la gran parte protocolli di livello inferiore (anche se quelli che non lo hanno, come SLIP, non possono essere usati con grande adabilit`); con questa a scelta si ` ridotto di molto il tempo di elaborazione dato che i router non hanno pi` la e u necessit` di ricalcolare la checksum ad ogni passaggio di un pacchetto per il cambiamento a del campo hop limit. ` stato eliminato il campo type of service, che praticamente non ` mai stato utilizzato; una e e

A.2. IL PROTOCOLLO IPV6

583

parte delle funzionalit` ad esso delegate sono state reimplementate (vedi il campo priority a al prossimo punto) con altri metodi. ` stato introdotto un nuovo campo ow label, che viene usato, insieme al campo priority e (che recupera i bit di precedenza del campo type of service) per implementare la gestione di una qualit` di servizio (vedi sez. A.2.13) che permette di identicare i pacchetti a appartenenti a un usso di dati per i quali si pu` provvedere un trattamento speciale. o Oltre alle dierenze precedenti, relative ai singoli campi nellintestazione, ulteriori caratteristiche che diversicano il comportamento di IPv4 da quello di IPv6 sono le seguenti: il broadcasting non ` previsto in IPv6, le applicazioni che lo usano dovono essere reimplee mentate usando il multicasting (vedi sez. A.2.10), che da opzionale diventa obbligatorio. ` stato introdotto un nuovo tipo di indirizzi, gli anycast. e i router non possono pi` frammentare i pacchetti lungo il cammino, la frammentazione u di pacchetti troppo grandi potr` essere gestita solo ai capi della comunicazione (usando a unapposita estensione vedi sez. A.2.12). IPv6 richiede il supporto per il path MTU discovery (cio` il protocollo per la selezione e della massima lunghezza del pacchetto); seppure questo sia in teoria opzionale, senza di esso non sar` possibile inviare pacchetti pi` larghi della dimensione minima (576 byte). a u

A.2.4

Gli indirizzi di IPv6

Come gi` abbondantemente anticipato la principale novit` di IPv6 ` costituita dallampliamento a a e dello spazio degli indirizzi, che consente di avere indirizzi disponibili in un numero dellordine di quello degli atomi che costituiscono la terra. In realt` lallocazione di questi indirizzi deve tenere conto della necessit` di costruire delle a a gerarchie che consentano un instradamento rapido ed eciente dei pacchetti, e essibilit` nel a dispiegamento delle reti, il che comporta una riduzione drastica dei numeri utilizzabili; uno studio sullecienza dei vari sistemi di allocazione usati in altre architetture (come i sistemi telefonici) ` comunque giunto alla conclusione che anche nella peggiore delle ipotesi IPv6 dovrebbe essere e in grado di fornire pi` di un migliaio di indirizzi per ogni metro quadro della supercie terrestre. u

A.2.5

La notazione

Con un numero di bit quadruplicato non ` pi` possibile usare la notazione coi numeri decimali e u di IPv4 per rappresentare un numero IP. Per questo gli indirizzi di IPv6 sono in genere scritti come sequenze di otto numeri esadecimali di 4 cifre (cio` a gruppi di 16 bit) usando i due punti e come separatore; cio` qualcosa del tipo 5f1b:df00:ce3e:e200:0020:0800:2078:e3e3. e Visto che la notazione resta comunque piuttosto pesante esistono alcune abbreviazioni; si pu` o evitare di scrivere gli zeri iniziali per cui si pu` scrivere 1080:0:0:0:8:800:ba98:2078:e3e3; o se poi un intero ` zero si pu` omettere del tutto, cos` come un insieme di zeri (ma questo solo e o una volta per non generare ambiguit`) per cui il precedente indirizzo si pu` scrivere anche come a o 1080::8:800:ba98:2078:e3e3. Inne per scrivere un indirizzo IPv4 allinterno di un indirizzo IPv6 si pu` usare la vecchia o notazione con i punti, per esempio ::192.84.145.138.

584
Tipo di indirizzo riservato non assegnato riservato per NSAP riservato per IPX non assegnato non assegnato non assegnato provider-based non assegnato non assegnato geograc-based non assegnato non assegnato non assegnato non assegnato non assegnato non assegnato non assegnato unicast link-local unicast site-local multicast

APPENDICE A. IL LIVELLO DI RETE


Presso 0000 0000 0000 0001 0000 001 0000 010 0000 011 0000 1 0001 001 010 011 100 101 110 1110 1111 0 1111 10 1111 110 1111 1110 0 1111 1110 10 1111 1110 11 1111 1111 Frazione 1/256 1/256 1/128 1/128 1/128 1/32 1/16 1/8 1/8 1/8 1/8 1/8 1/8 1/16 1/32 1/64 1/128 1/512 1/1024 1/1024 1/256

Tabella A.6: Classicazione degli indirizzi IPv6 a seconda dei bit pi` signicativi u

A.2.6

La architettura degli indirizzi di IPv6

Come per IPv4 gli indirizzi sono identicatori per una singola (indirizzi unicast) o per un insieme (indirizzi multicast e anycast) di interfacce di rete. Gli indirizzi sono sempre assegnati allinterfaccia, non al nodo che la ospita; dato che ogni interfaccia appartiene ad un nodo questultimo pu` essere identicato attraverso uno qualunque o degli indirizzi unicast delle sue interfacce. A una interfaccia possono essere associati anche pi` u indirizzi. IPv6 presenta tre tipi diversi di indirizzi: due di questi, gli indirizzi unicast e multicast hanno le stesse caratteristiche che in IPv4, un terzo tipo, gli indirizzi anycast ` completamente e nuovo. In IPv6 non esistono pi` gli indirizzi broadcast, la funzione di questi ultimi deve essere u reimplementata con gli indirizzi multicast. Gli indirizzi unicast identicano una singola interfaccia: i pacchetti mandati ad un tale indirizzo verranno inviati a quella interfaccia, gli indirizzi anycast identicano un gruppo di interfacce tale che un pacchetto mandato a uno di questi indirizzi viene inviato alla pi` vicina u (nel senso di distanza di routing) delle interfacce del gruppo, gli indirizzi multicast identicano un gruppo di interfacce tale che un pacchetto mandato a uno di questi indirizzi viene inviato a tutte le interfacce del gruppo. In IPv6 non ci sono pi` le classi ma i bit pi` signicativi indicano il tipo di indirizzo; in u u tab. A.6 sono riportati i valori di detti bit e il tipo di indirizzo che loro corrispondente. I bit pi` signicativi costituiscono quello che viene chiamato il format prex ed ` sulla base di questo u e che i vari tipi di indirizzi vengono identicati. Come si vede questa architettura di allocazione supporta lallocazione di indirizzi per i provider, per uso locale e per il multicast; inoltre ` stato e riservato lo spazio per indirizzi NSAP, IPX e per le connessioni; gran parte dello spazio (pi` del u 70%) ` riservato per usi futuri. e Si noti inne che gli indirizzi anycast non sono riportati in tab. A.6 in quanto allocati al di fuori dello spazio di allocazione degli indirizzi unicast.

A.2. IL PROTOCOLLO IPV6

585

A.2.7

Indirizzi unicast provider-based

Gli indirizzi provider-based sono gli indirizzi usati per le comunicazioni globali, questi sono deniti nellRFC 2073 e sono gli equivalenti degli attuali indirizzi delle classi da A a C. Lautorit` che presiede allallocazione di questi indirizzi ` la IANA; per evitare i problemi a e di crescita delle tabelle di instradamento e una procedura eciente di allocazione la struttura di questi indirizzi ` organizzata n dallinizio in maniera gerarchica; pertanto lo spazio di questi e indirizzi ` stato suddiviso in una serie di campi secondo lo schema riportato in tab. A.7. e
3 5 bit n bit Provider Id 56 n bit Subscriber Id 64 bit Intra-Subscriber

010 Registry Id

Tabella A.7: Formato di un indirizzo unicast provider-based.

Al livello pi` alto la IANA pu` delegare lallocazione a delle autorit` regionali (i Regional u o a Register) assegnando ad esse dei blocchi di indirizzi; a queste autorit` regionali ` assegnato un a e Registry Id che deve seguire immediatamente il presso di formato. Al momento sono denite tre registri regionali (INTERNIC, RIPE NCC e APNIC), inoltre la IANA si ` riservata la e possibilit` di allocare indirizzi su base regionale; pertanto sono previsti i seguenti possibili valori a per il Registry Id; gli altri valori restano riservati per la IANA.
Regione Nord America Europa Asia Multi-regionale Registro INTERNIC RIPE NCC APNIC IANA Id 11000 01000 00100 10000

Tabella A.8: Valori dellidenticativo dei Regional Register allocati ad oggi.

Lorganizzazione degli indirizzi prevede poi che i due livelli successivi, di suddivisione fra Provider Id, che identica i grandi fornitori di servizi, e Subscriber Id, che identica i fruitori, sia gestita dai singoli registri regionali. Questi ultimi dovranno denire come dividere lo spazio di indirizzi assegnato a questi due campi (che ammonta a un totale di 56 bit), denendo lo spazio da assegnare al Provider Id e al Subscriber Id, ad essi spetter` inoltre anche lallocazione dei a numeri di Provider Id ai singoli fornitori, ai quali sar` delegata lautorit` di allocare i Subscriber a a Id al loro interno. Lultimo livello ` quello Intra-subscriber che ` lasciato alla gestione dei singoli fruitori nali, e e gli indirizzi provider-based lasciano normalmente gli ultimi 64 bit a disposizione per questo livello, la modalit` pi` immediata ` quella di usare uno schema del tipo mostrato in tab. A.9 a u e dove lInterface Id ` dato dal MAC-address a 48 bit dello standard Ethernet, scritto in genere e nellhardware delle scheda di rete, e si usano i restanti 16 bit per indicare la sottorete.
64 bit Subscriber Prex 16 bit Subnet Id 48 bit Interface Id

Tabella A.9: Formato del campo Intra-subscriber per un indirizzo unicast provider-based.

Qualora si dovesse avere a che fare con una necessit` di un numero pi` elevato di sottoreti, a u il precedente schema andrebbe modicato, per evitare lenorme spreco dovuto alluso dei MACaddress, a questo scopo si possono usare le capacit` di autocongurazione di IPv6 per assegnare a indirizzi generici con ulteriori gerarchie per sfruttare ecacemente tutto lo spazio di indirizzi. Un registro regionale pu` introdurre un ulteriore livello nella gerarchia degli indirizzi, alloo cando dei blocchi per i quali delegare lautorit` a dei registri nazionali, questultimi poi avranno a

586

APPENDICE A. IL LIVELLO DI RETE

il compito di gestire la attribuzione degli indirizzi per i fornitori di servizi nellambito del/i paese coperto dal registro nazionale con le modalit` viste in precedenza. Una tale ripartizione andr` a a eettuata allinterno dei soliti 56 bit come mostrato in tab. A.10.
3 5 bit 3 Reg. n bit Naz. m bit Prov. 56-n-m bit Subscr. 64 bit Intra-Subscriber

Tabella A.10: Formato di un indirizzo unicast provider-based che prevede un registro nazionale.

A.2.8

Indirizzi ad uso locale

Gli indirizzi ad uso locale sono indirizzi unicast che sono instradabili solo localmente (allinterno di un sito o di una sottorete), e possono avere una unicit` locale o globale. a Questi indirizzi sono pensati per luso allinterno di un sito per mettere su una comunicazione locale immediata, o durante le fasi di autocongurazione prima di avere un indirizzo globale.
10 FE80 54 bit 0000 . . . . . 0000 64 bit Interface Id

Tabella A.11: Formato di un indirizzo link-local.

Ci sono due tipi di indirizzi, link-local e site-local. Il primo ` usato per un singolo link; la e struttura ` mostrata in tab. A.11, questi indirizzi iniziano sempre con un valore nellintervallo e FE80FEBF e vengono in genere usati per la congurazione automatica dellindirizzo al bootstrap e per la ricerca dei vicini (vedi A.2.19); un pacchetto che abbia tale indirizzo come sorgente o destinazione non deve venire ritrasmesso dai router. Un indirizzo site-local invece ` usato per lindirizzamento allinterno di un sito che non necese sita di un presso globale; la struttura ` mostrata in tab. A.12, questi indirizzi iniziano sempre e con un valore nellintervallo FEC0FEFF e non devono venire ritrasmessi dai router allesterno del sito stesso; sono in sostanza gli equivalenti degli indirizzi riservati per reti private deniti su IPv4. Per entrambi gli indirizzi il campo Interface Id ` un identicatore che deve essere unico nel e dominio in cui viene usato, un modo immediato per costruirlo ` quello di usare il MAC-address e delle schede di rete.
10 FEC0 38 bit 0000 . . . 0000 16 bit Subnet Id 64 bit Interface Id

Tabella A.12: Formato di un indirizzo site-local.

Gli indirizzi di uso locale consentono ad una organizzazione che non ` (ancora) connessa ad e Internet di operare senza richiedere un presso globale, una volta che in seguito lorganizzazione venisse connessa a Internet potrebbe continuare a usare la stessa suddivisione eettuata con gli indirizzi site-local utilizzando un presso globale e la rinumerazione degli indirizzi delle singole macchine sarebbe automatica.

A.2.9

Indirizzi riservati

Alcuni indirizzi sono riservati per scopi speciali, in particolare per scopi di compatibilit`. a Un primo tipo sono gli indirizzi IPv4 mappati su IPv6 (mostrati in tab. A.13), questo sono indirizzi unicast che vengono usati per consentire ad applicazioni IPv6 di comunicare con host capaci solo di IPv4; questi sono ad esempio gli indirizzi generati da un DNS quando lhost richiesto supporta solo IPv4; luso di un tale indirizzo in un socket IPv6 comporta la generazione

A.2. IL PROTOCOLLO IPV6

587

di un pacchetto IPv4 (ovviamente occorre che sia IPv4 che IPv6 siano supportati sullhost di origine).
80 bit 0000 . . . . . . . . . . . . 0000 16 bit FFFF 32 bit IPv4 address

Tabella A.13: Formato di un indirizzo IPV4 mappato su IPv6.

Un secondo tipo di indirizzi di compatibilit` sono gli IPv4 compatibili IPv6 (vedi tab. A.14) a usati nella transizione da IPv4 a IPv6: quando un nodo che supporta sia IPv6 che IPv4 non ha un router IPv6 deve usare nel DNS un indirizzo di questo tipo, ogni pacchetto IPv6 inviato a un tale indirizzo verr` automaticamente incapsulato in IPv4. a
80 bit 0000 . . . . . . . . . . . . 0000 16 bit 0000 32 bit IPv4 address

Tabella A.14: Formato di un indirizzo IPV4 mappato su IPv6.

Altri indirizzi speciali sono il loopback address, costituito da 127 zeri ed un uno (cio` ::1) e e lindirizzo generico costituito da tutti zeri (scritto come 0::0 o ancora pi` semplicemente come u :) usato in genere quando si vuole indicare laccettazione di una connessione da qualunque host.

A.2.10

Multicasting

Gli indirizzi multicast sono usati per inviare un pacchetto a un gruppo di interfacce; lindirizzo identica uno specico gruppo di multicast e il pacchetto viene inviato a tutte le interfacce di detto gruppo. Uninterfaccia pu` appartenere ad un numero qualunque numero di gruppi di o multicast. Il formato degli indirizzi multicast ` riportato in tab. A.15: e
8 FF 4 4 112 bit Group Id Tabella A.15: Formato di un indirizzo multicast.

ag scop

Il presso di formato per tutti gli indirizzi multicast ` FF, ad esso seguono i due campi il cui e signicato ` il seguente: e ag: un insieme di 4 bit, di cui i primi tre sono riservati e posti a zero, lultimo ` zero se e lindirizzo ` permanente (cio` un indirizzo noto, assegnato dalla IANA), ed ` uno se invece e e e lindirizzo ` transitorio. e scop ` un numero di quattro bit che indica il raggio di validit` dellindirizzo, i valori e a assegnati per ora sono riportati in tab. A.16. Inne lultimo campo identica il gruppo di multicast, sia permanente che transitorio, allinterno del raggio di validit` del medesimo. Alcuni indirizzi multicast, riportati in tab. A.17 sono a gi` riservati per il funzionamento della rete. a Lutilizzo del campo di scope e di questi indirizzi predeniti serve a recuperare le funzionalit` del broadcasting (ad esempio inviando un pacchetto allindirizzo FF02:0:0:0:0:0:0:1 si a raggiungono tutti i nodi locali).

588

APPENDICE A. IL LIVELLO DI RETE


Gruppi di multicast riservato 8 organizzazione locale nodo locale 9 non assegnato collegamento locale A non assegnato non assegnato B non assegnato non assegnato C non assegnato sito locale D non assegnato non assegnato E globale non assegnato F riservato

0 1 2 3 4 5 6 7

Tabella A.16: Possibili valori del campo scop di un indirizzo multicast. Uso all-nodes all-routers all-rip-routers all-cbt-routers reserved link-name all-dhcp-agents all-dhcp-servers all-dhcp-relays solicited-nodes Indirizzi riservati FFxx:0:0:0:0:0:0:1 FFxx:0:0:0:0:0:0:2 FFxx:0:0:0:0:0:0:9 FFxx:0:0:0:0:0:0:10 FFxx:0:0:0:0:0:1:0 FFxx:0:0:0:0:0:1:1 FFxx:0:0:0:0:0:1:2 FFxx:0:0:0:0:0:1:3 FFxx:0:0:0:0:0:1:4 FFxx:0:0:0:0:1:0:0 Denizione RFC 1970 RFC 1970 RFC 2080 IANA

RFC 1970

Tabella A.17: Gruppi di multicast predeniti.

A.2.11

Indirizzi anycast

Gli indirizzi anycast sono indirizzi che vengono assegnati ad un gruppo di interfacce: un pacchetto indirizzato a questo tipo di indirizzo viene inviato al componente del gruppo pi` vicino secondo u la distanza di instradamento calcolata dai router. Questi indirizzi sono allocati nello stesso spazio degli indirizzi unicast, usando uno dei formati disponibili, e per questo, sono da essi assolutamente indistinguibili. Quando un indirizzo unicast viene assegnato a pi` interfacce (trasformandolo in un anycast) il computer su cui ` linterfaccia u e deve essere congurato per tener conto del fatto. Gli indirizzi anycast consentono a un nodo sorgente di inviare pacchetti a una destinazione su un gruppo di possibili interfacce selezionate. La sorgente non deve curarsi di come scegliere linterfaccia pi` vicina, compito che tocca al sistema di instradamento (in sostanza la sorgente u non ha nessun controllo sulla selezione). Gli indirizzi anycast, quando vengono usati come parte di una sequenza di instradamento, consentono ad esempio ad un nodo di scegliere quale fornitore vuole usare (congurando gli indirizzi anycast per identicare i router di uno stesso provider). Questi indirizzi pertanto possono essere usati come indirizzi intermedi in una intestazione di instradamento o per identicare insiemi di router connessi a una particolare sottorete, o che forniscono laccesso a un certo sotto dominio. Lidea alla base degli indirizzi anycast ` perci` quella di utilizzarli per poter raggiungere il e o fornitore di servizio pi` vicino; ma restano aperte tutta una serie di problematiche, visto che u una connessione con uno di questi indirizzi non ` possibile, dato che per una variazione delle e distanze di routing non ` detto che due pacchetti successivi niscano alla stessa interfaccia. e La materia ` pertanto ancora controversa e in via di denizione. e

A.2.12

Le estensioni

Come gi` detto in precedenza IPv6 ha completamente cambiato il trattamento delle opzioni; a queste ultime infatti sono state tolte dallintestazione del pacchetto, e poste in apposite inte-

A.2. IL PROTOCOLLO IPV6

589

stazioni di estensione (o extension header ) poste fra lintestazione di IPv6 e lintestazione del protocollo di trasporto. Per aumentare la velocit` di elaborazione, sia dei dati del livello seguente che di ulterioa ri opzioni, ciascuna estensione deve avere una lunghezza multipla di 8 byte per mantenere lallineamento a 64 bit di tutti le intestazioni seguenti. Dato che la maggior parte di queste estensioni non sono esaminate dai router durante linstradamento e la trasmissione dei pacchetti, ma solo allarrivo alla destinazione nale, questa scelta ha consentito un miglioramento delle prestazioni rispetto a IPv4 dove la presenza di unopzione comportava lesame di tutte quante. Un secondo miglioramento ` che rispetto a IPv4 le opzioni possono essere di lunghezza e arbitraria e non limitate a 40 byte; questo, insieme al modo in cui vengono trattate, consente di utilizzarle per scopi come lautenticazione e la sicurezza, improponibili con IPv4. Le estensioni denite al momento sono le seguenti: Hop by hop devono seguire immediatamente lintestazione principale; indicano le opzioni che devono venire processate ad ogni passaggio da un router, fra di esse ` da menzionare e la jumbo payload che segnala la presenza di un pacchetto di dati di dimensione superiore a 65535 byte. Destination options opzioni che devono venire esaminate al nodo di ricevimento, nessuna di esse ` tuttora denita. e Routing denisce una source route (come la analoga opzione di IPv4) cio` una lista di e indirizzi IP di nodi per i quali il pacchetto deve passare. Fragmentation viene generato automaticamente quando un host vuole frammentare un pacchetto, ed ` riprocessato automaticamente alla destinazione che riassembla i frammenti. e Authentication gestisce lautenticazione e il controllo di integrit` dei pacchetti; ` docua e mentato dallRFC 1826. Encapsulation serve a gestire la segretezza del contenuto trasmesso; ` documentato e dallRFC 1827. La presenza di opzioni ` rilevata dal valore del campo next header che indica qual ` lintestae e zione successiva a quella di IPv6; in assenza di opzioni questa sar` lintestazione di un protocollo a di trasporto del livello superiore, per cui il campo assumer` lo stesso valore del campo protocol a di IPv4, altrimenti assumer` il valore dellopzione presente; i valori possibili sono riportati in a tab. A.18. Questo meccanismo permette la presenza di pi` opzioni in successione prima del pacchetto u del protocollo di trasporto; lordine raccomandato per le estensioni ` quello riportato nellelenco e precedente con la sola dierenza che le opzioni di destinazione sono inserite nella posizione ivi indicata solo se, come per il tunnelling, devono essere esaminate dai router, quelle che devono essere esaminate solo alla destinazione nale vanno in coda.

A.2.13

Qualit` di servizio a

Una delle caratteristiche innovative di IPv6 ` quella di avere introdotto un supporto per la e qualit` di servizio che ` importante per applicazioni come quelle multimediali o real-time che a e richiedono un qualche grado di controllo sulla stabilit` della banda di trasmissione, sui ritardi o a la dispersione dei temporale del usso dei pacchetti.

590
Valore 0 1 2 3 4 5 6 17 43 44 45 51 52 59 88 89 255 Keyword HBH ICMP ICMP GGP IP ST TCP UDP RH FH IDRP AH ESP Null IGRP OSPF

APPENDICE A. IL LIVELLO DI RETE


Tipo di protocollo Riservato. Hop by Hop. Internet Control Message (IPv4 o IPv6). Internet Group Management (IPv4). Gateway-to-Gateway. IP in IP (IPv4 encapsulation). Stream. Trasmission Control. User Datagram. Routing Header (IPv6). Fragment Header (IPv6). Inter Domain Routing. Authentication Header (IPv6). Encrypted Security Payload (IPv6). No next header (IPv6). Internet Group Routing. Open Short Path First. Riservato.

Tabella A.18: Tipi di protocolli e intestazioni di estensione

A.2.14

Etichette di usso

Lintroduzione del campo ow label pu` essere usata dallorigine della comunicazione per etio chettare quei pacchetti per i quali si vuole un trattamento speciale da parte dei router come un una garanzia di banda minima assicurata o un tempo minimo di instradamento/trasmissione garantito. Questo aspetto di IPv6 ` ancora sperimentale per cui i router che non supportino queste e funzioni devono porre a zero il ow label per i pacchetti da loro originanti e lasciare invariato il campo per quelli in transito. Un usso ` una sequenza di pacchetti da una particolare origine a una particolare destinazione e per il quale lorigine desidera un trattamento speciale da parte dei router che lo manipolano; la natura di questo trattamento pu` essere comunicata ai router in vari modi (come un protocollo o di controllo o con opzioni del tipo hop-by-hop). Ci possono essere pi` ussi attivi fra unorigine e una destinazione, come del traco non u assegnato a nessun usso, un usso viene identicato univocamente dagli indirizzi di origine e destinazione e da una etichetta di usso diversa da zero, il traco normale deve avere letichetta di usso posta a zero. Letichetta di usso ` assegnata dal nodo di origine, i valori devono essere scelti in maniee ra (pseudo)casuale nel range fra 1 e FFFFFF in modo da rendere utilizzabile un qualunque sottoinsieme dei bit come chiavi di hash per i router.

A.2.15

Priorit` a

Il campo di priorit` consente di indicare il livello di priorit` dei pacchetti relativamente agli altri a a pacchetti provenienti dalla stessa sorgente. I valori sono divisi in due intervalli, i valori da 0 a 7 sono usati per specicare la priorit` del traco per il quale la sorgente provvede un controllo di a congestione cio` per il traco che pu` essere tirato indietro in caso di congestione come quello e o di TCP, i valori da 8 a 15 sono usati per i pacchetti che non hanno questa caratteristica, come i pacchetti real-time inviati a ritmo costante. Per il traco con controllo di congestione sono raccomandati i seguenti valori di priorit` a a seconda del tipo di applicazione:

A.2. IL PROTOCOLLO IPV6


Valore 0 1 2 3 4 5 Tipo di traco Traco generico. Traco di riempimento (es. news). Trasferimento dati non interattivo (es. e-mail). Riservato. Trasferimento dati interattivo (es. FTP, HTTP, NFS). Riservato. Tabella A.19: Formato di un indirizzo site-local.

591

Per il traco senza controllo di congestione la priorit` pi` bassa dovrebbe essere usata per a u quei pacchetti che si preferisce siano scartati pi` facilmente in caso di congestione. u

A.2.16

Sicurezza a livello IP

La attuale implementazione di Internet presenta numerosi problemi di sicurezza, in particolare i dati presenti nelle intestazioni dei vari protocolli sono assunti essere corretti, il che da adito alla possibilit` di varie tipologie di attacco forgiando pacchetti false, inoltre tutti questi dati passano a in chiaro sulla rete e sono esposti allosservazione di chiunque si trovi in mezzo. Con IPv4 non ` possibile realizzare un meccanismo di autenticazione e riservatezza a un e livello inferiore al primo (quello di applicazione), con IPv6 ` stato progettata la possibilit` di e a intervenire al livello di rete (il terzo) prevedendo due apposite estensioni che possono essere usate per fornire livelli di sicurezza a seconda degli utenti. La codica generale di questa architettura ` riportata nellRFC 2401. e Il meccanismo in sostanza si basa su due estensioni: una intestazione di sicurezza (authentication header ) che garantisce al destinatario lautenticit` del pacchetto a un carico di sicurezza (Encrypted Security Payload ) che assicura che solo il legittimo ricevente pu` leggere il pacchetto. o Perch tutto questo funzioni le stazioni sorgente e destinazione devono usare una stessa chiave e crittograca e gli stessi algoritmi, linsieme degli accordi fra le due stazioni per concordare chiavi e algoritmi usati va sotto il nome di associazione di sicurezza. I pacchetti autenticati e crittografati portano un indice dei parametri di sicurezza (SPI, Security Parameter Index ) che viene negoziato prima di ogni comunicazione ed ` denito dalla e stazione sorgente. Nel caso di multicast dovr` essere lo stesso per tutte le stazioni del gruppo. a

A.2.17

Autenticazione

Il primo meccanismo di sicurezza ` quello dellintestazione di autenticazione (authentication heae der ) che fornisce lautenticazione e il controllo di integrit` (ma senza riservatezza) dei pacchetti a IP. Lintestazione di autenticazione ha il formato descritto in g. A.3: il campo Next Header indica lintestazione successiva, con gli stessi valori del campo omonimo nellintestazione principale di IPv6, il campo Length indica la lunghezza dellintestazione di autenticazione in numero di parole a 32 bit, il campo riservato deve essere posto a zero, seguono poi lindice di sicurezza, stabilito nella associazione di sicurezza, e un numero di sequenza che la stazione sorgente deve incrementare di pacchetto in pacchetto. Completano lintestazione i dati di autenticazione che contengono un valore di controllo di integrit` (ICV, Integrity Check Value), che deve essere di dimensione pari a un multiplo intero a di 32 bit e pu` contenere un padding per allineare lintestazione a 64 bit. Tutti gli algoritmi di o autenticazione devono provvedere questa capacit`. a

592

APPENDICE A. IL LIVELLO DI RETE

Figura A.3: Formato dellintestazione dellestensione di autenticazione.

Lintestazione di autenticazione pu` essere impiegata in due modi diverse modalit`: modalit` o a a trasporto e modalit` tunnel. a La modalit` trasporto ` utilizzabile solo per comunicazioni fra stazioni singole che supportino a e lautenticazione. In questo caso lintestazione di autenticazione ` inserita dopo tutte le altre e intestazioni di estensione eccezion fatta per la Destination Option che pu` comparire sia prima o che dopo.

Figura A.4: Formato di un pacchetto IPv6 che usa lopzione di autenticazione.

La modalit` tunnel pu` essere utilizzata sia per comunicazioni fra stazioni singole che con a o un gateway di sicurezza; in questa modalit` ... a Lintestazione di autenticazione ` una intestazione di estensione inserita dopo lintestazione e principale e prima del carico dei dati. La sua presenza non ha perci` alcuna inuenza sui livelli o superiori dei protocolli di trasmissione come il TCP. La procedura di autenticazione cerca di garantire lautenticit` del pacchetto nella massima a estensione possibile, ma dato che alcuni campi dellintestazione di IP possono variare in maniera impredicibile alla sorgente, il loro valore non pu` essere protetto dallautenticazione. o Il calcolo dei dati di autenticazione viene eettuato alla sorgente su una versione speciale del pacchetto in cui il numero di salti nellintestazione principale ` impostato a zero, cos` come le e opzioni che possono essere modicate nella trasmissione, e lintestazione di routing (se usata) ` e posta ai valori che deve avere allarrivo. Lestensione ` indipendente dallalgoritmo particolare, e il protocollo ` ancora in fase di e e denizione; attualmente ` stato suggerito luso di una modica dellMD5 chiamata keyed MD5 e che combina alla codica anche una chiave che viene inserita allinizio e alla ne degli altri campi.

A.2.18

Riservatezza

Per garantire una trasmissione riservata dei dati, ` stata previsto la possibilit` di trasmettere e a pacchetti con i dati criptati: il cosiddetto ESP, Encripted Security Payload. Questo viene realizzato usando con una apposita opzione che deve essere sempre lultima delle intestazioni di estensione; ad essa segue il carico del pacchetto che viene criptato. Un pacchetto crittografato pertanto viene ad avere una struttura del tipo di quella mostrata in g. A.5, tutti i campi sono in chiaro no al vettore di inizializzazione, il resto ` crittografato. e

A.2. IL PROTOCOLLO IPV6

593

Figura A.5: Schema di pacchetto crittografato.

A.2.19

Autocongurazione

Una delle caratteristiche salienti di IPv6 ` quella dellautocongurazione, il protocollo infatti e fornisce la possibilit` ad un nodo di scoprire automaticamente il suo indirizzo acquisendo i a parametri necessari per potersi connettere a internet. Lautocongurazione sfrutta gli indirizzi link-local; qualora sul nodo sia presente una scheda di rete che supporta lo standard IEEE802 (ethernet) questo garantisce la presenza di un indirizzo sico a 48 bit unico; pertanto il nodo pu` assumere automaticamente senza pericoli di collisione o lindirizzo link-local FE80::xxxx:xxxx:xxxx dove xxxx:xxxx:xxxx ` lindirizzo hardware della e scheda di rete. Nel caso in cui non sia presente una scheda che supporta lo standard IEEE802 allora il nodo assumer` ugualmente un indirizzo link-local della forma precedente, ma il valore di a xxxx:xxxx:xxxx sar` generato casualmente; in questo caso la probabilit` di collisione ` di 1 a a e su 300 milioni. In ogni caso per prevenire questo rischio il nodo invier` un messaggio ICMP a Solicitation allindirizzo scelto attendendo un certo lasso di tempo; in caso di risposta lindirizzo ` duplicato e il procedimento dovr` essere ripetuto con un nuovo indirizzo (o interrotto e a richiedendo assistenza). Una volta ottenuto un indirizzo locale valido diventa possibile per il nodo comunicare con la rete locale; sono pertanto previste due modalit` di autocongurazione, descritte nelle seguenti a sezioni. In ogni caso lindirizzo link-local resta valido.

A.2.20

Autocongurazione stateless

Questa ` la forma pi` semplice di autocongurazione, possibile quando lindirizzo globale pu` e u o essere ricavato dallindirizzo link-local cambiando semplicemente il presso a quello assegnato dal provider per ottenere un indirizzo globale. La procedura di congurazione ` la seguente: allavvio tutti i nodi IPv6 iniziano si devono e aggregare al gruppo di multicast all-nodes programmando la propria interfaccia per ricevere i messaggi dallindirizzo multicast FF02::1 (vedi sez. A.2.10); a questo punto devono inviare un messaggio ICMP Router solicitation a tutti i router locali usando lindirizzo multicast FF02::2 usando come sorgente il proprio indirizzo link-local. Il router risponder` con un messaggio ICMP Router Advertisement che fornisce il presso a e la validit` nel tempo del medesimo, questo tipo di messaggio pu` essere trasmesso anche a a o intervalli regolari. Il messaggio contiene anche linformazione che autorizza un nodo a autoco-

594

APPENDICE A. IL LIVELLO DI RETE

struire lindirizzo, nel qual caso, se il presso unito allindirizzo link-local non supera i 128 bit, la stazione ottiene automaticamente il suo indirizzo globale.

A.2.21

Autocongurazione stateful

Bench estremamente semplice lautocongurazione stateless presenta alcuni problemi; il primo e ` che luso degli indirizzi delle schede di rete ` molto ineciente; nel caso in cui ci siano esigenze e e di creare una gerarchia strutturata su parecchi livelli possono non restare 48 bit per lindirizzo della singola stazione; il secondo problema ` di sicurezza, dato che basta introdurre in una rete e una stazione autocongurante per ottenere un accesso legale. Per questi motivi ` previsto anche un protocollo stateful basato su un server che ora una e versione IPv6 del DHCP; un apposito gruppo di multicast FF02::1:0 ` stato riservato per questi e server; in questo caso il nodo interrogher` il server su questo indirizzo di multicast con lindirizzo a link-local e ricever` un indirizzo unicast globale. a

A.3

Il protocollo ICMP

Come gi` accennato nelle sezioni precedenti, lInternet Control Message Protocol ` un protocollo a e di servizio fondamentale per il funzionamento del livello di rete. Il protocollo ICMP viene trasportato direttamente su IP, ma proprio per questa sua caratteristica di protocollo di servizio ` e da considerarsi a tutti gli eetti appartenente al livello di rete.

A.3.1

Lintestazione di ICMP

Il protocollo ICMP ` estremamente semplice, ed il suo unico scopo ` quello di inviare messaggi di e e controllo; in g. A.6 si ` riportata la struttura dellintestazione di un pacchetto ICMP generico. e

Figura A.6: Lintestazione del protocollo ICMP.

A.3. IL PROTOCOLLO ICMP

595

Valore any echo-reply destination-unreachable

Tipo 0 3

source-quench redirect echo-request time-exceeded parameter-problem timestamp-request timestamp-reply info-request info-reply address-mask-request address-mask-reply

4 5 8 11 12 13 14 15 16 17 18

Signicato Seleziona tutti i possibili valori Inviato in risposta ad un ICMP echo-request. Segnala una destinazione irraggiungibile, viene inviato allIP sorgente di un pacchetto quando un router realizza che questo non pu` o essere inviato a destinazione. Inviato in caso di congestione della rete per indicare allIP sorgente di diminuire il traco inviato. Inviato per segnalare un errore di routing, richiede che la macchina sorgente rediriga il traco ad un altro router da esso specicato. Richiede linvio in risposta di un echo-reply. Inviato quando il TTL di un pacchetto viene azzerato. Inviato da un router che rileva dei problemi con lintestazione di un pacchetto. Richiede linvio in risposta di un timestamp-reply. Inviato in risposta di un timestamp-request. Richiede linvio in risposta di un info-reply. Inviato in risposta di un info-request. Richiede linvio in risposta di un address-mask-reply. Inviato in risposta di un address-mask-request.

Tabella A.20: I valori del tipo per i pacchetti ICMP.

Valore network-unreachable host-unreachable protocol-unreachable port-unreachable fragmentation-needed source-route-failed network-unknown host-unknown host-isolated network-prohibited host-prohibited TOS-network-unreachable TOS-host-unreachable communication-prohibited host-precedence-violation precedence-cutoff network-redirect host-redirect TOS-network-redirect TOS-host-redirect ttl-zero-during-transit ttl-zero-during-reassembly ip-header-bad required-option-missing

Codice 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0 1 2 3 0 1 0 1

Tabella A.21: Valori del campo codice per il protocollo ICMP.

596

APPENDICE A. IL LIVELLO DI RETE

Appendice B

Il livello di trasporto
In questa appendice tratteremo i vari protocolli relativi al livello di trasporto.1 In particolare gran parte del capitolo sar` dedicato al pi` importante di questi, il TCP, che ` pure il pi` a u e u complesso ed utilizzato su internet.

B.1

Il protocollo TCP

In questa sezione prenderemo in esame i vari aspetti del protocollo TCP, il protocollo pi` u comunemente usato dalle applicazioni di rete.

B.1.1

Gli stati del TCP

In sez. 16.1 abbiamo descritto in dettaglio le modalit` con cui il protocollo TCP avvia e conclude a una connessione, ed abbiamo accennato alla presenza dei vari stati del protocollo. In generale infatti il funzionamento del protocollo segue una serie di regole, che possono essere riassunte nel comportamento di una macchina a stati, il cui diagramma di transizione ` riportato in g. B.1. e Il protocollo prevede lesistenza di 11 diversi stati per una connessione ed un insieme di regole per le transizioni da uno stato allaltro basate sullo stato corrente, sulloperazione eettuata dallapplicazione o sul tipo di segmento ricevuto; i nomi degli stati mostrati in g. B.1 sono gli stessi che vengono riportati del comando netstat nel campo State.

B.2

Il protocollo UDP

In questa sezione prenderemo in esame i vari aspetti del protocollo UDP, che dopo il TCP ` il e protocollo pi` usato dalle applicazioni di rete. u

al solito per la denizione dei livelli si faccia riferimento alle spiegazioni fornite in sez. 14.2.

597

598

APPENDICE B. IL LIVELLO DI TRASPORTO

Figura B.1: Il diagramma degli stati del TCP.

Figura B.2: Lintestazione del protocollo UDP.

Appendice C

I codici di errore
Si riportano in questa appendice tutti i codici di errore. Essi sono accessibili attraverso linclusione del le di header errno.h, che denisce anche la variabile globale errno. Per ogni errore denito riporteremo la stringa stampata da perror ed una breve spiegazione. Si tenga presente che spiegazioni pi` particolareggiate del signicato dellerrore, qualora necessarie per casi speciu ci, possono essere trovate nella descrizione del prototipo della funzione per cui detto errore si ` vericato. e I codici di errore sono riportati come costanti di tipo int, i valori delle costanti sono deniti da macro di preprocessore nel le citato, e possono variare da architettura a architettura; ` e pertanto necessario riferirsi ad essi tramite i nomi simbolici. Le funzioni perror e strerror (vedi sez. 8.5.2) possono essere usate per ottenere dei messaggi di errore pi` espliciti. u

C.1

Gli errori dei le

In questa sezione sono raccolti i codici restituiti dalle funzioni di libreria attinenti ad errori che riguardano operazioni speciche relative alla gestione dei le. EPERM Operation not permitted. Loperazione non ` permessa: solo il proprietario del le o un e processo con sucienti privilegi pu` eseguire loperazione. o ENOENT No such le or directory. Il le indicato dal pathname non esiste: o una delle componenti non esiste o il pathname contiene un link simbolico spezzato. Errore tipico di un riferimento ad un le che si suppone erroneamente essere esistente. EIO Input/output error. Errore di input/output: usato per riportare errori hardware in lettura/scrittura su un dispositivo. ENXIO No such device or address. Dispositivo inesistente: il sistema ha tentato di usare un dispositivo attraverso il le specicato, ma non lo ha trovato. Pu` signicare che il le di o dispositivo non ` corretto, che il modulo relativo non ` stato caricato nel kernel, o che il e e dispositivo ` sicamente assente o non funzionante. e ENOEXEC Invalid executable le format. Il le non ha un formato eseguibile, ` un errore riscone trato dalle funzioni exec. EBADF Bad le descriptor. File descriptor non valido: si ` usato un le descriptor inesistente, o e aperto in sola lettura per scrivere, o viceversa, o si ` cercato di eseguire unoperazione non e consentita per quel tipo di le descriptor. EACCES Permission denied. Permesso negato; laccesso al le o alla directory non ` consentito: e i permessi del le o della directory non consentono loperazione richiesta. 599

600

APPENDICE C. I CODICI DI ERRORE

ELOOP Too many symbolic links encountered. Ci sono troppi link simbolici nella risoluzione di un pathname. ENAMETOOLONG File name too long. Si ` indicato un pathname troppo lungo per un le o una e directory. ENOTBLK Block device required. Si ` specicato un le che non ` un block device in un contesto e e in cui era necessario specicare un block device (ad esempio si ` tentato di montare un le e ordinario). EEXIST File exists. Si ` specicato un le esistente in un contesto in cui ha senso solo specicare e un nuovo le. EBUSY Resource busy. Una risorsa di sistema che non pu` essere condivisa ` occupata. Ad o e esempio si ` tentato di cancellare la directory su cui si ` montato un lesystem. e e EXDEV Cross-device link. Si ` tentato di creare un link diretto che attraversa due lesystem e dierenti. ENODEV No such device. Si ` indicato un tipo di device sbagliato ad una funzione che ne richiede e uno specico. ENOTDIR Not a directory. Si ` specicato un le che non ` una directory in una operazione che e e richiede una directory. EISDIR Is a directory. Il le specicato ` una directory; non pu` essere aperto in scrittura, n e o e si possono creare o rimuovere link diretti ad essa. EMFILE Too many open les. Il processo corrente ha troppi le aperti e non pu` aprirne altri. o Anche i descrittori duplicati ed i socket vengono tenuti in conto.1 ENFILE File table overow. Il sistema ha troppi le aperti in contemporanea. Si tenga presente che anche i socket contano come le. Questa ` una condizione temporanea, ed ` molto e e dicile che si verichi nei sistemi moderni. ENOTTY Not a terminal. Si ` tentata una operazione di controllo relativa ad un terminale su un e le che non lo `. e ETXTBSY Text le busy. Si ` cercato di eseguire un le che ` aperto in scrittura, o di scrivere su e e un le che ` in esecuzione. e EFBIG File too big. Si ` ecceduto il limite imposto dal sistema sulla dimensione massima che un e le pu` avere. o ENOSPC No space left on device. La directory in cui si vuole creare il link non ha spazio per ulteriori voci, o si ` cercato di scrivere o di creare un nuovo le su un dispositivo che ` gi` e e a pieno. ESPIPE Invalid seek operation. Si cercato di eseguire una seek su un le che non supporta questa operazione (ad esempio su una pipe). EROFS Read-only le system. Si ` cercato di eseguire una operazione di scrittura su un le o e una directory che risiede su un lesystem montato un sola lettura.
il numero massimo di le aperti ` controllabile dal sistema; in Linux si pu` impostare usando il comando e o ulimit, esso ` in genere indicato dalla costante OPEN_MAX, vedi sez. 8.1.1. e
1

C.2. GLI ERRORI DEI PROCESSI

601

EMLINK Too many links. Ci sono gi` troppi link al le (il numero massimo ` specicato dalla a e variabile LINK_MAX, vedi sez. 8.1.1). EPIPE Broken pipe. Non c` un processo che stia leggendo laltro capo della pipe. Ogni funzione e che restituisce questo errore genera anche un segnale SIGPIPE, la cui azione predenita ` e terminare il programma; pertanto non si potr` vedere questo errore ntanto che SIGPIPE a non viene gestito o bloccato. ENOTEMPTY Directory not empty. La directory non ` vuota quando loperazione richiede che lo e ` sia. E lerrore tipico che si ha quando si cerca di cancellare una directory contenente dei le. EUSERS Too many users. Troppi utenti, il sistema delle quote rileva troppi utenti nel sistema. EDQUOT Quota exceeded. Si ` ecceduta la quota di disco dellutente. e ESTALE Stale NFS le handle. Indica un problema interno a NFS causato da cambiamenti del lesystem del sistema remoto. Per recuperare questa condizione in genere ` necessario e smontare e rimontare il lesystem NFS. EREMOTE Object is remote. Si ` fatto un tentativo di montare via NFS un lesystem remoto con e un nome che gi` specica un lesystem montato via NFS. a ` ENOLCK No locks available. E usato dalle utilit` per la gestione del le locking; non viene a generato da un sistema GNU, ma pu` risultare da unoperazione su un server NFS di un o altro sistema. EFTYPE Inappropriate le type or format. Il le ` di tipo sbagliato rispetto alloperazione rie chiesta o un le di dati ha un formato sbagliato. Alcuni sistemi restituiscono questo errore quando si cerca di impostare lo sticky bit su un le che non ` una directory. e

C.2

Gli errori dei processi

In questa sezione sono raccolti i codici restituiti dalle funzioni di libreria attinenti ad errori che riguardano operazioni speciche relative alla gestione dei processi. ESRCH No process matches the specied process ID. Non esiste un processo o un process group corrispondenti al valore dellidenticativo specicato. E2BIG Argument list too long. La lista degli argomenti passati ` troppo lunga: ` una condizione e e prevista da POSIX quando la lista degli argomenti passata ad una delle funzioni exec occupa troppa memoria, non pu` mai accadere in GNU/Linux. o ECHILD There are no child processes. Non esistono processi gli di cui attendere la terminazione. Viene rilevato dalle funzioni wait e waitpid.

C.3

Gli errori di rete

In questa sezione sono raccolti i codici restituiti dalle funzioni di libreria attinenti ad errori che riguardano operazioni speciche relative alla gestione dei socket e delle connessioni di rete. ENOTSOCK Socket operation on non-socket. Si ` tentata unoperazione su un le descriptor che e non ` un socket quando invece era richiesto un socket. e

602

APPENDICE C. I CODICI DI ERRORE

EMSGSIZE Message too long. Le dimensioni di un messaggio inviato su un socket sono eccedono la massima lunghezza supportata. EPROTOTYPE Protocol wrong type for socket. Protocollo sbagliato per il socket. Il socket usato non supporta il protocollo di comunicazione richiesto. ENOPROTOOPT Protocol not available. Protocollo non disponibile. Si ` richiesta unopzione per il e socket non disponibile con il protocollo usato. EPROTONOSUPPORT Protocol not supported. Protocollo non supportato. Il tipo di socket non supporta il protocollo richiesto (un probabile errore nella specicazione del protocollo). ESOCKTNOSUPPORT Socket type not supported. Socket non supportato. Il tipo di socket scelto non ` supportato. e EOPNOTSUPP Operation not supported on transport endpoint. Loperazione richiesta non ` supe portata. Alcune funzioni non hanno senso per tutti i tipi di socket, ed altre non sono implementate per tutti i protocolli di trasmissione. Questo errore quando un socket non supporta una particolare operazione, e costituisce una indicazione generica che il server non sa cosa fare per la chiamata eettuata. EPFNOSUPPORT Protocol family not supported. Famiglia di protocolli non supportata. La famiglia di protocolli richiesta non ` supportata. e EAFNOSUPPORT Address family not supported by protocol. Famiglia di indirizzi non supportata. La famiglia di indirizzi richiesta non ` supportata, o ` inconsistente con il protocollo usato e e dal socket. EADDRINUSE Address already in use. Lindirizzo del socket richiesto ` gi` utilizzato (ad esempio e a si ` eseguita bind su una porta gi` in uso). e a EADDRNOTAVAIL Cannot assign requested address. Lindirizzo richiesto non ` disponibile (ad e esempio si ` cercato di dare al socket un nome che non corrisponde al nome della stazione e locale), o linterfaccia richiesta non esiste. ENETDOWN Network is down. Loperazione sul socket ` fallita perch la rete ` sconnessa. e e e ENETUNREACH Network is unreachable. Loperazione ` fallita perch lindirizzo richiesto ` irrage e e giungibile (ad esempio la sottorete della stazione remota ` irraggiungibile). e ENETRESET Network dropped connection because of reset. Una connessione ` stata cancellata e perch lhost remoto ` caduto. e e ECONNABORTED Software caused connection abort. Una connessione ` stata abortita localmente. e ECONNRESET Connection reset by peer. Una connessione ` stata chiusa per ragioni fuori dal e controllo dellhost locale, come il riavvio di una macchina remota o un qualche errore non recuperabile sul protocollo. ENOBUFS No buer space available. Tutti i buer per le operazioni di I/O del kernel sono occupati. In generale questo errore ` sinonimo di ENOMEM, ma attiene alle funzioni di input/output. e In caso di operazioni sulla rete si pu` ottenere questo errore invece dellaltro. o EISCONN Transport endpoint is already connected. Si ` tentato di connettere un socket che ` gi` e e a connesso.

C.4. ERRORI GENERICI

603

ENOTCONN Transport endpoint is not connected. Il socket non ` connesso a niente. Si ottiene e questo errore quando si cerca di trasmettere dati su un socket senza avere specicato in precedenza la loro destinazione. Nel caso di socket senza connessione (ad esempio socket UDP) lerrore che si ottiene ` EDESTADDRREQ. e EDESTADDRREQ Destination address required. Non c` un indirizzo di destinazione predenito e per il socket. Si ottiene questo errore mandando dato su un socket senza connessione senza averne prima specicato una destinazione. ESHUTDOWN Cannot send after transport endpoint shutdown. Il socket su cui si cerca di inviare dei dati ha avuto uno shutdown. ETOOMANYREFS Too many references: cannot splice. La glibc dice ??? ETIMEDOUT Connection timed out. Unoperazione sul socket non ha avuto risposta entro il periodo di timeout. ECONNREFUSED Connection refused. Un host remoto ha riutato la connessione (in genere dipende dal fatto che non c` un server per soddisfare il servizio richiesto). e EHOSTDOWN Host is down. Lhost remoto di una connessione ` gi`. e u EHOSTUNREACH No route to host. Lhost remoto di una connessione non ` raggiungibile. e

C.4

Errori generici

In questa sezione sono raccolti i codici restituiti dalle funzioni di libreria attinenti ad errori generici, si trovano qui tutti i codici di errore non specicati nelle sezioni precedenti. EINTR Interrupted function call. Una funzione di libreria ` stata interrotta. In genere questo e avviene causa di un segnale asincrono al processo che impedisce la conclusione della chiamata, la funzione ritorna con questo errore una volta che si sia correttamente eseguito il gestore del segnale. In questo caso ` necessario ripetere la chiamata alla funzione. e ENOMEM No memory available. Il kernel non ` in grado di allocare ulteriore memoria per e completare loperazione richiesta. EDEADLK Deadlock avoided. Lallocazione di una risorsa avrebbe causato un deadlock. Non sempre il sistema ` in grado di riconoscere queste situazioni, nel qual caso si avrebbe e il blocco. EFAULT Bad address. Una stringa passata come argomento ` fuori dello spazio di indirizzi del e processo, in genere questa situazione provoca direttamente lemissione di un segnale di segment violation (SIGSEGV). EINVAL Invalid argument. Errore utilizzato per segnalare vari tipi di problemi dovuti allaver passato un argomento sbagliato ad una funzione di libreria. ` EDOM Domain error. E usato dalle funzioni matematiche quando il valore di un argomento ` al e di fuori dellintervallo in cui esse sono denite. ` ERANGE Range error. E usato dalle funzioni matematiche quando il risultato delloperazione non ` rappresentabile nel valore di ritorno a causa di un overow o di un underow. e EAGAIN Resource temporarily unavailable. La funzione ` fallita ma potrebbe funzionare se la e chiamata fosse ripetuta. Questo errore accade in due tipologie di situazioni:

604

APPENDICE C. I CODICI DI ERRORE Si ` eettuata unoperazione che si sarebbe bloccata su un oggetto che ` stato posto in e e modalit` non bloccante. Nei vecchi sistemi questo era un codice diverso, EWOULDBLOCK. a In genere questo ha a che fare con le o socket, per i quali si pu` usare la funzione o select per vedere quando loperazione richiesta (lettura, scrittura o connessione) diventa possibile. Indica la carenza di una risorsa di sistema che non ` al momento disponibile (ad e esempio fork pu` fallire con questo errore se si ` esaurito il numero di processi o e contemporanei disponibili). La ripetizione della chiamata in un periodo successivo, in cui la carenza della risorsa richiesta pu` essersi attenuata, pu` avere successo. o o Questo tipo di carenza ` spesso indice di qualcosa che non va nel sistema, ` pertanto e e opportuno segnalare esplicitamente questo tipo di errori.

EWOULDBLOCK Operation would block. Indica che loperazione richiesta si bloccherebbe, ad esempio se si apre un le in modalit` non bloccante, una read restituirebbe questo errore per a indicare che non ci sono dati; in Linux ` identico a EAGAIN, ma in altri sistemi pu` essere e o specicato un valore diverso. EINPROGRESS Operation now in progress. Operazione in corso. Unoperazione che non pu` essere o completata immediatamente ` stata avviata su un oggetto posto in modalit` non-bloccante. e a Questo errore viene riportato per operazioni che si dovrebbero sempre bloccare (come per una connect) e che pertanto non possono riportare EAGAIN, lerrore indica che loperazione ` stata avviata correttamente e occorrer` del tempo perch si possa completare. La e a e ripetizione della chiamata darebbe luogo ad un errore EALREADY. EALREADY Operation already in progress. Loperazione ` gi` in corso. Si ` tentata unoperazione e a e gi` in corso su un oggetto posto in modalit` non-bloccante. a a ENOSYS Function not implemented. Indica che la funzione non ` supportata o nelle librerie del e C o nel kernel. Pu` dipendere sia dalla mancanza di una implementazione, che dal fatto o che non si ` abilitato lopportuno supporto nel kernel; nel caso di Linux questo pu` voler e o dire anche che un modulo necessario non ` stato caricato nel sistema. e ENOTSUP Not supported. Una funzione ritorna questo errore quando gli argomenti sono validi ma loperazione richiesta non ` supportata. Questo signica che la funzione non implementa e quel particolare comando o opzione o che, in caso di oggetti specici (le descriptor o altro) non ` in grado di supportare i parametri richiesti. e EILSEQ Illegal byte sequence. Nella decodica di un carattere esteso si ` avuta una sequenza e errata o incompleta o si ` specicato un valore non valido. e EBADMSG Not a data message. Denito da POSIX come errore che arriva ad una funzione di lettura che opera su uno stream. Non essendo gli stream deniti su Linux il kernel non genera mai questo tipo di messaggio. EMULTIHOP Multihop attempted. Denito da POSIX come errore dovuto allaccesso a le remoti attraverso pi` macchine, quando ci` non ` consentito. Non viene mai generato su Linux. u o e EIDRM Identier removed. Indica che loggetto del SysV IPC a cui si fa riferimento ` stato e cancellato. ENODATA No data available. Viene indicato da POSIX come restituito da una read eseguita su un le descriptor in modalit` non bloccante quando non ci sono dati. In realt` in questo a a caso viene utilizzato EAGAIN. In Linux viene utilizzato dalle funzioni per la gestione degli attributi estesi dei le quando il nome dellattributo richiesto non viene trovato.

C.4. ERRORI GENERICI

605

` ENOLINK Link has been severed. E un errore il cui valore ` indicato come riservato nelle Single e Unix Specication. Dovrebbe indicare limpossibilit` di accedere ad un le a causa di un a errore sul collegamento di rete, ma non ci sono indicazioni precise del suo utilizzo. Per quanto riguarda Linux viene riportato nei sorgenti del kernel in alcune operazioni relative ad operazioni di rete. ENOMSG No message of desired type. Indica che in una coda di messaggi del SysV IPC non ` e presente nessun messaggio del tipo desiderato. ENOSR Out of streams resources. Errore relativo agli STREAMS, che indica lassenza di risorse sucienti a completare loperazione richiesta. Quella degli STREAMS 2 ` interfaccia di e programmazione originaria di System V, che non ` implementata da Linux, per cui questo e errore non viene utilizzato. ENOSTR Device not a stream. Altro errore relativo agli STREAMS, anchesso non utilizzato da Linux. EOVERFLOW Value too large for dened data type. Si ` chiesta la lettura di un dato dal SysV IPC e con IPC_STAT ma il valore eccede la dimensione usata nel buer di lettura. EPROTO Protocol error. Indica che c` stato un errore nel protocollo di rete usato dal socket; e viene usato come errore generico dallinterfaccia degli STREAMS quando non si ` in grado e di specicare un altro codice di errore che esprima pi` accuratamente la situazione. u ETIME Timer expired. Indica che ` avvenuto un timeout nellaccesso ad una risorsa (ad esempio e un semaforo). Compare nei sorgenti del kernel (in particolare per le funzioni relativa al bus USB) come indicazione di una mancata risposta di un dispositivo, con una descrizione alternativa di Device did not respond.

che non vanno confusi con gli stream di cap. 7.

606

APPENDICE C. I CODICI DI ERRORE

Appendice D

Gli strumenti di ausilio per la programmazione


Tratteremo in questa appendice in maniera superciale i principali strumenti che vengono utilizzati per programmare in ambito Linux, ed in particolare gli strumenti per la compilazione e la costruzione di programmi e librerie, e gli strumenti di gestione dei sorgenti e di controllo di versione. Questo materiale ` ripreso da un vecchio articolo, ed al momento ` molto obsoleto. e e

D.1

Luso di make per lautomazione della compilazione

Il comando make serve per automatizzare il processo di costruzione di un programma ed eettuare una compilazione intelligente di tutti i le relativi ad un progetto software, ricompilando solo i le necessari ed eseguendo automaticamente tutte le operazioni che possono essere necessarie alla produzione del risultato nale.1

D.1.1

Introduzione a make

Con make si possono denire i simboli del preprocessore C che consentono la compilazione condizionale dei programmi (anche in Fortran); ` pertanto possibile gestire la ricompilazione dei e programmi con diverse congurazioni con la modica di un unico le. La sintassi normale del comando (quella che si usa quasi sempre, per le opzioni vedere la pagina di manuale) ` semplicemente make. Questo comando esegue le istruzioni contenute in un e le standard (usualmente Makefile, o makefile nella directory corrente). Il formato normale dei comandi contenuti in un Makefile `: e bersaglio: dipendenza1 dipendenza2 ... regola1 regola2 ... dove lo spazio allinizio deve essere un tabulatore (metterci degli spazi ` un errore comune, e fortunatamente ben segnalato dalle ultime versioni del programma), il bersaglio e le dipendenze nomi di le e le regole comandi di shell. Il concetto di base ` che se uno dei le di dipendenza ` pi` recente (nel senso di tempo e e u di ultima modica) del le bersaglio questultimo viene ricostruito di nuovo usando le regole elencate nelle righe successive.
in realt` make non si applica solo ai programmi, ma in generale alla automazione di processi di costruzione, a ad esempio anche la creazione dei le di questa guida viene fatta con make.
1

607

608

APPENDICE D. GLI STRUMENTI DI AUSILIO PER LA PROGRAMMAZIONE

Il comando make ricostruisce di default il primo bersaglio che viene trovato nella scansione del Makefile, se in un Makefile sono contenuti pi` bersagli indipendenti, si pu` farne ricostruire un u o altro che non sia il primo passandolo esplicitamente al comando come argomento, con qualcosa del tipo di: make altrobersaglio. Si tenga presente che le dipendenze stesse possono essere dichiarate come bersagli dipendenti da altri le; in questo modo ` possibile creare una catena di ricostruzioni. e In esempio comune di quello che si fa ` mettere come primo bersaglio il programma principale e che si vuole usare, e come dipendenze tutte gli oggetti delle funzioni subordinate che utilizza, con i quali deve essere collegato; a loro volta questi oggetti sono bersagli che hanno come dipendenza i relativi sorgenti. In questo modo il cambiamento di una delle funzioni subordinate comporta solo la ricompilazione della medesima e del programma nale.

D.1.2

Utilizzo di make

Il comando make mette a disposizione una serie molto complesse di opzioni e di regole standard predenite e sottintese, che permettono una gestione estremamente rapida e concisa di progetti anche molto complessi; per questo piuttosto che fare una replica del manuale preferisco commentare un esempio di makefile, quello usato per ricompilare i programmi di analisi dei dati dei test su fascio del tracciatore di Pamela.
#---------------------------------------------------------------------# # Makefile for a Linux System: # use GNU FORTRAN compiler g77 # Makefile done for tracker test data # #---------------------------------------------------------------------# Fortran flags FC=g77 FFLAGS= -fvxt -fno-automatic -Wall -O6 -DPC # -DDEBUG CC=gcc CFLAGS= -Wall -O6 CFLADJ=-c #-DDEBUG # # FC Fortran compiler for standard rules # FFLAGS Fortran flags for standard rules # CC C Compiler for standard rules # CFLAGS C compiler flags for standard rules LIBS= -L/cern/pro/lib -lkernlib -lpacklib -lgraflib -lmathlib OBJ=cnoise.o fit2.o pedsig.o loop.o badstrp.o cutcn.o readevnt.o \ erasepedvar.o readinit.o dumpval.o writeinit.o riduzione: riduzione.F $(OBJ) commondef.f readfile.o $(FC) $(FFLAGS) -o riduzione riduzione.F readfile.o $(OBJ) $(LIBS) readfile.o: readfile.c $(CC) $(CFLAGS) -o readfile.o readfile.c $(OBJ): commondef.f .PHONY : clean clean: rm -f *.o rm -f *~ rm -f riduzione

D.1. LUSO DI MAKE PER LAUTOMAZIONE DELLA COMPILAZIONE


rm -f *.rz rm -f output

609

Anzitutto i commenti, ogni linea che inizia con un # ` un commento e non viene presa in e considerazione. Con make possono essere denite delle variabili, da potersi riusare a piacimento, per leggibilit` a si tende a denirle tutte maiuscole, nellesempio ne sono denite varie:
FC=g77 FFLAGS= -fvxt -fno-automatic -Wall -O6 -DPC # -DDEBUG CC=gcc CFLAGS= -Wall -O6 CFLADJ=-c #-DDEBUG ... LIBS= -L/cern/pro/lib -lkernlib -lpacklib -lgraflib -lmathlib OBJ=cnoise.o fit2.o pedsig.o loop.o badstrp.o cutcn.o readevnt.o \

La sintassi ` NOME=, alcuni nomi per` hanno un signicato speciale (nel caso FC, FLAGS, CC, e o CFLAGS) in quanto sono usati da make nelle cosiddette regole implicite (su cui torneremo dopo). Nel caso specico, vedi anche i commenti, abbiamo denito i comandi di compilazione da usare per il C e il Fortran, e i rispettivi ag, una variabile che contiene il pathname e la lista delle librerie del CERN e una variabile con una lista di le oggetto. Per richiamare una variabile si usa la sintassi $(NOME), ad esempio nel makele abbiamo usato: $(FC) $(FFLAGS) -o riduzione riduzione.F readfile.o $(OBJ) $(LIBS) e questo signica che la regola verr` trattata come se avessimo scritto esplicitamente i valori a delle variabili. Veniamo ora alla parte principale del makele che esegue la costruzione del programma: riduzione: riduzione.F $(OBJ) commondef.f readfile.o $(FC) $(FFLAGS) -o riduzione riduzione.F readfile.o $(OBJ) $(LIBS) readfile.o: readfile.c $(CC) $(CFLAGS) -o readfile.o readfile.c $(OBJ): commondef.f Il primo bersaglio del makele, che denisce il bersaglio di default, ` il programma di riduzione e dei dati; esso dipende dal suo sorgente da tutti gli oggetti deniti dalla variabile OBJ, dal le di denizioni commondef.f e dalla routine C readfile.o; si noti il .F del sorgente, che signica che il le prima di essere compilato viene fatto passare attraverso il preprocessore C (cosa che non avviene per i .f) che permette di usare i comandi di compilazione condizionale del preprocessore C con la relativa sintassi. Sotto segue il comando di compilazione che sfrutta le variabili denite in precedenza per specicare quale compilatore e opzioni usare e specica di nuovo gli oggetti e le librerie. Il secondo bersaglio denisce le regole per la compilazione della routine in C; essa dipende solo dal suo sorgente. Si noti che per la compilazione vengono usate le variabili relative al compilatore C. Si noti anche che se questa regola viene usata, allora lo sar` anche la precedente, dato che a riduzione dipende da readfile.o. Il terzo bersaglio ` apparentemente incomprensibile dato che vi compare solo il riferimento e alla variabile OBJ con una sola dipendenza e nessuna regola, essa per` mostra le possibilit` o a (oltre che la complessit`) di make connesse alla presenza di quelle regole implicite a cui avevamo a accennato. Anzitutto una peculiarit` di make ` che si possono anche usare pi` bersagli per una stessa a e u regola (nellesempio quelli contenuti nella variabile OBJ che viene espansa in una lista); in questo

610

APPENDICE D. GLI STRUMENTI DI AUSILIO PER LA PROGRAMMAZIONE

caso la regola di costruzione sar` applicata a ciascuno che si potr` citare nella regola stessa a a facendo riferimento con la variabile automatica: $@. Lesempio usato per la nostra costruzione per` sembra non avere neanche la regola di costruzione. o Questa mancanza sia di regola che di dipendenze (ad esempio dai vari sorgenti) illustra le capacit` di funzionamento automatico di make. Infatti ` facile immaginarsi che un oggetto a e dipenda da un sorgente, e che per ottenere loggetto si debba compilare questultimo. Il comando make sa tutto questo per cui quando un bersaglio ` un oggetto (cio` ha un nome e e tipo qualcosa.o) non ` necessario specicare il sorgente, ma il programma lo va a cercare nella e directory corrente (ma ` possibile pure dirgli di cercarlo altrove, il caso ` trattato nel manuale). e e Nel caso specico allora si ` messo come dipendenza solo il le delle denizioni che viene incluso e in ogni subroutine. Inoltre come dicevamo in genere per costruire un oggetto si deve compilarne il sorgente; make sa anche questo e sulla base dellestensione del sorgente trovato (che nel caso sar` un qualcosa.f) a applica la regola implicita. In questo caso la regola ` quella di chiamare il compilatore fortran e applicato al le oggetto e al relativo sorgente, questo viene fatto usando la variabile FC che ` e una delle variabili standard usata dalle regole implicite (come CC nel caso di le .c); per una maggiore essibilit` poi la regola standard usa anche la variabile FFLAGS per specicare, a scelta a dellutente che non ha che da denirla, quali ag di compilazione usare (nella documentazione sono riportate tutte le regole implicite e le relative variabili usate). In questo modo ` stato possibile usare una sola riga per indicare la serie di dipendenze e e relative compilazioni delle singole subroutine; inoltre con luso della variabile OBJ laggiunta di una nuova eventuale routine nuova.f comporta solo laggiunta di nuova.o alla denizione di OBJ.

D.2

Cuncurrent Version System CVS

Il programma CVS ` un sistema di archiviazione dei programmi che consente di tenere traccia e di tutte le modiche e di condividere un archivio comune per progetti portati avanti da diverse persone.

D.2.1

Introduzione

CVS ` basato sul concetto di repositorio, un archivio in cui vengono riposti e da cui vengoe no presi i sorgenti dei programmi. Larchivio tiene traccia delle diverse versioni registrate; i programmatori inviano le modiche usando una copia locale che hanno nella loro directory di lavoro. CVS pu` gestire pi` di un progetto, esso organizza i progetti in moduli identicati dal nome o u della directory in cui sono messi i le relativi, ad esempio:
[piccardi@pcpamela ~]$ ls /usr/local/cvsroot/ CVSROOT adidsp geometry muonacq pamela

in questo caso si hanno i moduli adidsp geometry muonacq pamela; un utente potr` recuperare a tutti i le relativi al modulo pamela che andaranno nella directory pamela; ` anche possibile e gestire una gerarchia di moduli, inseriti in un albero di directory. CVS ha una directory base dove tiene gli archivi (che nel ` appunto /usr/local/cvsroot) e e i vari le amministrativi; ogni progetto deve fare riferimento ad essa; in un progetto attivo essa viene memorizzata nei le amministrativi locali del progetto (ad esempio per un utente che ha il progetto muonacq nella sua home directory sar` in ~/muonacq/CVS) e non ` necessario specia e carla; in generale essa viene tenuta dalla variabile di shell CVSROOT (inizializzata allo startup), o specicata direttamente dallutente con lapposita opzione -d (ex. cvs -d /usr/local/cvsroot comando opzioni).

D.2. CUNCURRENT VERSION SYSTEM CVS

611

Normalmente larchivio si tiene su una macchina remota e vi si accede via rete. Per repositori esterni esistono varie modalit` di accesso, la via pi` semplice ` quella delluso di ssh come a u e rimpiazzo di rsh per laccesso esterno; in tal caso la directory del repositorio si pu` accedere con o la sintassi:
cvs -d :ext:utente@server:/usr/local/cvsroot comando

questo per` comporta che sulla macchina remota server sia installato il server ssh ed esista o lutente utente con privilegi di accesso ai le; inoltre sulla macchina ospite deve esser stata denita la variabile di shell CVS_RSH=ssh. In questo modo ssh si collega alla macchina remota ed esegue il comando CVS con una connessione criptata dopo aver richiesto la password di utente.

D.2.2

Utilizzo di cvs

Per creare un repositorio a partire da un gruppo di programmi esistente basta dare il comando cvs import project tag release nella directory dei sorgenti; verr` creato nel repositorio il a modulo di nome project a cui poi si potr` accedere con i successivi comandi; tag ` una etichetta a e di versione iniziale (ex. 1.1.1) dato a tutto il blocco e release una etichetta di versione data ai programmi. Una volta creato il repositorio ` duopo cancellare la directory e ripartire dal progetto appena e creato. Per recuperare ex-novo tutti i le di un progetto il comando da dare `: e
cvs [-d ...] checkout project [-r rel] [-D date]

che creer` la directory project nella directory corrente con lultima versione dei le archiviata; a se non si vuole la versione pi` aggiornata, ma una versione precedente si pu` usare lopzione u o -r che scaricher` la versione identicata dalletichetta specicata (vedi la parte seguente sul a comando tag) o lopzione -D che scaricher` la versione pi` recente prima della data specicata. a u Una volta scaricato il progetto ` possibile evitare di specicare la directory base del repoe sitorio con lopzione -d qualora i comandi vengano dati allinterno della directory creata, essa viene tenuta nel le CVS/Root, e pu` essere cambiata (qualora si usino pi` repositori) editando o u detto le. Una volta creata la propria copia locale dei programmi, ` possibile lavorare su di essi stando e nella relativa directory, e apportare tutte le modiche che si vogliono; due comandi permettono di schedulare la rimozione o laggiunta di le al repositorio:
cvs add file1.c cvs remove file2.c

ma niente viene modicato nel repositorio ntanto che non viene dato il comando commit:
cvs commit [file] (` possibile mandare le modiche anche per il singolo le). e Questi comandi comunque non eettuano le modiche se i le del repositorio nel frattempo sono stati modicati; in questo caso rilevano le dierenze e restituiscono un merging delle versioni locale/globale nella directory di lavoro, che ` compito del programmatore esaminare per eliminare eventuali contrasti. e Per esempio viene eseguito un commit su una versione gi` modicata da un altro sul repositorio, il a programma segnaler` che c` un conitto e chieder` al committente di intervenire sui le per i quali a e a sono stati rilevati i conitti . Le sezioni di codice in conitto sono separate come: <<<<<<< Makefile $(CC) $(CFLAGS) -o pamacq pamacq.c -lm ======= $(CC) $(CFLAGS) -o pamacq pamacq.c >>>>>>> 1.22

612

APPENDICE D. GLI STRUMENTI DI AUSILIO PER LA PROGRAMMAZIONE

nel caso si c` stata una modica sul le (mostrata nella parte superiore) incompatibile con quella fatta e nel repositorio (mostrata nella parte inferiore). Prima di eseguire un commit occorre pertanto integrare le modiche e risalvare il le; a questo punto il commit diventa possibile.

D.2.3

I principali sotto comandi

I principali sottocomandi di cvs utilizzati per controllare lo stato dei sorgenti sono: cvs update scarica le eventuali modiche dei le del repositorio e le applica ai le nella directory corrente. In caso di conitti con modiche locali eettua un merging con le caratteristiche esposte in precedenza. cvs status compara i le nella directory locale con quelli del repositorio e stampa a schermo le informazioni di stato di ciascuno di essi: Locally Modified il le ` stato modicato localmente e Locally Added il le ` stato aggiunto localmente e Up to date il le ` identico e Needs Patch il le ` cambiato sul repositorio (va aggiornato) e Needs Merge il le ` cambiato sul repositorio e localmente, le dierenze vanno riunite prima e del commit, in genere basta un cvs update, ma in caso di conitti questi vanno sanati. cvs log legge il giornale del modulo corrente; in questo modo si hanno le informazioni sullo stato del progetto allinterno del repositorio, per ciascun le ` vengono mostrati informazioni generali su: e release attuale presenza di eventuali ramicazioni lista di etichette simboliche i messaggi di commento salvati ad ogni commit cvs rtag Etichetta modulo permette di associare una etichetta alla corrente versione dei le nel progetto cos` come sono nel repositorio; in modo da poter ritornare a quello stato del software con il comando cvs checkout -rEtichetta module.

Appendice E

Ringraziamenti
Desidero ringraziare tutti coloro che a vario titolo e a pi` riprese mi hanno aiutato ed han contribuito a u migliorare in molteplici aspetti la qualit` di GaPiL. In ordine rigorosamente alfabetico desidero citare: a Alessio Frusciante per lapprezzamento, le innumerevoli correzioni ed i suggerimenti per rendere pi` u chiara lesposizione. Daniele Masini per la rilettura puntuale, le innumerevoli correzioni, i consigli sullesposizione ed i contributi relativi alle calling convention dei linguaggi e al confronto delle diverse tecniche di gestione della memoria. Mirko Maischberger per la rilettura, le numerose correzioni, la segnalazione dei passi poco chiari e soprattutto per il grande lavoro svolto per produrre una versione della guida in un HTML piacevole ed accurato. Fabio Rossi per la rilettura, le innumerevoli correzioni, ed i vari consigli stilistici ed i suggerimenti per il miglioramento della comprensione di vari passaggi. Inne, vorrei ringraziare il Firenze Linux User Group (FLUG), di cui mi pregio di fare parte, che ha messo a disposizione il repository CVS su cui era presente la prima versione della Guida, lo spazio web, e Truelite Srl, che fornisce il nuovo repository SVN, tutto quanto ` necessario alla pubblicazione della e guida ed il sistema di tracciamento dei sorgenti su http://gapil.truelite.it/sources.

613

614

APPENDICE E. RINGRAZIAMENTI

Appendice F

GNU Free Documentation License


Version 1.1, March 2000 Copyright c 2000 Free Software Foundation, Inc. 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.

Preamble
The purpose of this License is to make a manual, textbook, or other written document free in the sense of freedom: to assure everyone the eective freedom to copy and redistribute it, with or without modifying it, either commercially or noncommercially. Secondarily, this License preserves for the author and publisher a way to get credit for their work, while not being considered responsible for modications made by others. This License is a kind of copyleft, which means that derivative works of the document must themselves be free in the same sense. It complements the GNU General Public License, which is a copyleft license designed for free software. We have designed this License in order to use it for manuals for free software, because free software needs free documentation: a free program should come with manuals providing the same freedoms that the software does. But this License is not limited to software manuals; it can be used for any textual work, regardless of subject matter or whether it is published as a printed book. We recommend this License principally for works whose purpose is instruction or reference.

F.1

Applicability and Denitions

This License applies to any manual or other work that contains a notice placed by the copyright holder saying it can be distributed under the terms of this License. The Document, below, refers to any such manual or work. Any member of the public is a licensee, and is addressed as you. A Modied Version of the Document means any work containing the Document or a portion of it, either copied verbatim, or with modications and/or translated into another language. A Secondary Section is a named appendix or a front-matter section of the Document that deals exclusively with the relationship of the publishers or authors of the Document to the Documents overall subject (or to related matters) and contains nothing that could fall directly within that overall subject. (For example, if the Document is in part a textbook of mathematics, a Secondary Section may not explain any mathematics.) The relationship could be a matter of historical connection with the subject or with related matters, or of legal, commercial, philosophical, ethical or political position regarding them. The Invariant Sections are certain Secondary Sections whose titles are designated, as being those of Invariant Sections, in the notice that says that the Document is released under this License. The Cover Texts are certain short passages of text that are listed, as Front-Cover Texts or BackCover Texts, in the notice that says that the Document is released under this License.

615

616

APPENDICE F. GNU FREE DOCUMENTATION LICENSE

A Transparent copy of the Document means a machine-readable copy, represented in a format whose specication is available to the general public, whose contents can be viewed and edited directly and straightforwardly with generic text editors or (for images composed of pixels) generic paint programs or (for drawings) some widely available drawing editor, and that is suitable for input to text formatters or for automatic translation to a variety of formats suitable for input to text formatters. A copy made in an otherwise Transparent le format whose markup has been designed to thwart or discourage subsequent modication by readers is not Transparent. A copy that is not Transparent is called Opaque. Examples of suitable formats for Transparent copies include plain ASCII without markup, Texinfo inA put format, L TEX input format, SGML or XML using a publicly available DTD, and standard-conforming simple HTML designed for human modication. Opaque formats include PostScript, PDF, proprietary formats that can be read and edited only by proprietary word processors, SGML or XML for which the DTD and/or processing tools are not generally available, and the machine-generated HTML produced by some word processors for output purposes only. The Title Page means, for a printed book, the title page itself, plus such following pages as are needed to hold, legibly, the material this License requires to appear in the title page. For works in formats which do not have any title page as such, Title Page means the text near the most prominent appearance of the works title, preceding the beginning of the body of the text.

F.2

Verbatim Copying

You may copy and distribute the Document in any medium, either commercially or noncommercially, provided that this License, the copyright notices, and the license notice saying this License applies to the Document are reproduced in all copies, and that you add no other conditions whatsoever to those of this License. You may not use technical measures to obstruct or control the reading or further copying of the copies you make or distribute. However, you may accept compensation in exchange for copies. If you distribute a large enough number of copies you must also follow the conditions in section 3. You may also lend copies, under the same conditions stated above, and you may publicly display copies.

F.3

Copying in Quantity

If you publish printed copies of the Document numbering more than 100, and the Documents license notice requires Cover Texts, you must enclose the copies in covers that carry, clearly and legibly, all these Cover Texts: Front-Cover Texts on the front cover, and Back-Cover Texts on the back cover. Both covers must also clearly and legibly identify you as the publisher of these copies. The front cover must present the full title with all words of the title equally prominent and visible. You may add other material on the covers in addition. Copying with changes limited to the covers, as long as they preserve the title of the Document and satisfy these conditions, can be treated as verbatim copying in other respects. If the required texts for either cover are too voluminous to t legibly, you should put the rst ones listed (as many as t reasonably) on the actual cover, and continue the rest onto adjacent pages. If you publish or distribute Opaque copies of the Document numbering more than 100, you must either include a machine-readable Transparent copy along with each Opaque copy, or state in or with each Opaque copy a publicly-accessible computer-network location containing a complete Transparent copy of the Document, free of added material, which the general network-using public has access to download anonymously at no charge using public-standard network protocols. If you use the latter option, you must take reasonably prudent steps, when you begin distribution of Opaque copies in quantity, to ensure that this Transparent copy will remain thus accessible at the stated location until at least one year after the last time you distribute an Opaque copy (directly or through your agents or retailers) of that edition to the public. It is requested, but not required, that you contact the authors of the Document well before redistributing any large number of copies, to give them a chance to provide you with an updated version of the Document.

F.4. MODIFICATIONS

617

F.4

Modications

You may copy and distribute a Modied Version of the Document under the conditions of sections 2 and 3 above, provided that you release the Modied Version under precisely this License, with the Modied Version lling the role of the Document, thus licensing distribution and modication of the Modied Version to whoever possesses a copy of it. In addition, you must do these things in the Modied Version: Use in the Title Page (and on the covers, if any) a title distinct from that of the Document, and from those of previous versions (which should, if there were any, be listed in the History section of the Document). You may use the same title as a previous version if the original publisher of that version gives permission. List on the Title Page, as authors, one or more persons or entities responsible for authorship of the modications in the Modied Version, together with at least ve of the principal authors of the Document (all of its principal authors, if it has less than ve). State on the Title page the name of the publisher of the Modied Version, as the publisher. Preserve all the copyright notices of the Document. Add an appropriate copyright notice for your modications adjacent to the other copyright notices. Include, immediately after the copyright notices, a license notice giving the public permission to use the Modied Version under the terms of this License, in the form shown in the Addendum below. Preserve in that license notice the full lists of Invariant Sections and required Cover Texts given in the Documents license notice. Include an unaltered copy of this License. Preserve the section entitled History, and its title, and add to it an item stating at least the title, year, new authors, and publisher of the Modied Version as given on the Title Page. If there is no section entitled History in the Document, create one stating the title, year, authors, and publisher of the Document as given on its Title Page, then add an item describing the Modied Version as stated in the previous sentence. Preserve the network location, if any, given in the Document for public access to a Transparent copy of the Document, and likewise the network locations given in the Document for previous versions it was based on. These may be placed in the History section. You may omit a network location for a work that was published at least four years before the Document itself, or if the original publisher of the version it refers to gives permission. In any section entitled Acknowledgements or Dedications, preserve the sections title, and preserve in the section all the substance and tone of each of the contributor acknowledgements and/or dedications given therein. Preserve all the Invariant Sections of the Document, unaltered in their text and in their titles. Section numbers or the equivalent are not considered part of the section titles. Delete any section entitled Endorsements. Such a section may not be included in the Modied Version. Do not retitle any existing section as Endorsements or to conict in title with any Invariant Section. If the Modied Version includes new front-matter sections or appendices that qualify as Secondary Sections and contain no material copied from the Document, you may at your option designate some or all of these sections as invariant. To do this, add their titles to the list of Invariant Sections in the Modied Versions license notice. These titles must be distinct from any other section titles. You may add a section entitled Endorsements, provided it contains nothing but endorsements of your Modied Version by various parties for example, statements of peer review or that the text has been approved by an organization as the authoritative denition of a standard. You may add a passage of up to ve words as a Front-Cover Text, and a passage of up to 25 words as a Back-Cover Text, to the end of the list of Cover Texts in the Modied Version. Only one passage of

618

APPENDICE F. GNU FREE DOCUMENTATION LICENSE

Front-Cover Text and one of Back-Cover Text may be added by (or through arrangements made by) any one entity. If the Document already includes a cover text for the same cover, previously added by you or by arrangement made by the same entity you are acting on behalf of, you may not add another; but you may replace the old one, on explicit permission from the previous publisher that added the old one. The author(s) and publisher(s) of the Document do not by this License give permission to use their names for publicity for or to assert or imply endorsement of any Modied Version.

F.5

Combining Documents

You may combine the Document with other documents released under this License, under the terms dened in section 4 above for modied versions, provided that you include in the combination all of the Invariant Sections of all of the original documents, unmodied, and list them all as Invariant Sections of your combined work in its license notice. The combined work need only contain one copy of this License, and multiple identical Invariant Sections may be replaced with a single copy. If there are multiple Invariant Sections with the same name but dierent contents, make the title of each such section unique by adding at the end of it, in parentheses, the name of the original author or publisher of that section if known, or else a unique number. Make the same adjustment to the section titles in the list of Invariant Sections in the license notice of the combined work. In the combination, you must combine any sections entitled History in the various original documents, forming one section entitled History; likewise combine any sections entitled Acknowledgements, and any sections entitled Dedications. You must delete all sections entitled Endorsements.

F.6

Collections of Documents

You may make a collection consisting of the Document and other documents released under this License, and replace the individual copies of this License in the various documents with a single copy that is included in the collection, provided that you follow the rules of this License for verbatim copying of each of the documents in all other respects. You may extract a single document from such a collection, and distribute it individually under this License, provided you insert a copy of this License into the extracted document, and follow this License in all other respects regarding verbatim copying of that document.

F.7

Aggregation With Independent Works

A compilation of the Document or its derivatives with other separate and independent documents or works, in or on a volume of a storage or distribution medium, does not as a whole count as a Modied Version of the Document, provided no compilation copyright is claimed for the compilation. Such a compilation is called an aggregate, and this License does not apply to the other self-contained works thus compiled with the Document, on account of their being thus compiled, if they are not themselves derivative works of the Document. If the Cover Text requirement of section 3 is applicable to these copies of the Document, then if the Document is less than one quarter of the entire aggregate, the Documents Cover Texts may be placed on covers that surround only the Document within the aggregate. Otherwise they must appear on covers around the whole aggregate.

F.8

Translation

Translation is considered a kind of modication, so you may distribute translations of the Document under the terms of section 4. Replacing Invariant Sections with translations requires special permission from their copyright holders, but you may include translations of some or all Invariant Sections in addition to the original versions of these Invariant Sections. You may include a translation of this License provided that you also include the original English version of this License. In case of a disagreement between the translation and the original English version of this License, the original English version will prevail.

F.9. TERMINATION

619

F.9

Termination

You may not copy, modify, sublicense, or distribute the Document except as expressly provided for under this License. Any other attempt to copy, modify, sublicense or distribute the Document is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance.

F.10

Future Revisions of This License

The Free Software Foundation may publish new, revised versions of the GNU Free Documentation License from time to time. Such new versions will be similar in spirit to the present version, but may dier in detail to address new problems or concerns. See http://www.gnu.org/copyleft/. Each version of the License is given a distinguishing version number. If the Document species that a particular numbered version of this License or any later version applies to it, you have the option of following the terms and conditions either of that specied version or of any later version that has been published (not as a draft) by the Free Software Foundation. If the Document does not specify a version number of this License, you may choose any version ever published (not as a draft) by the Free Software Foundation.

620

APPENDICE F. GNU FREE DOCUMENTATION LICENSE

Indice analitico
Access Control List, 126, 129130 advertised window , 415, 435, 471, 536, 538, 552 algoritmo di Nagle, 536 append mode, 64, 136138, 141, 142, 144, 158, 186, 198, 297, 320 broadcast, 64, 412, 420, 428, 443, 444, 521, 524, 529, 544, 545, 578, 583, 584, 587 bucket lter , 549 buer overow , 164, 167, 169 calendar time, 199206 capabilities, 126, 127 capabilities, 23, 24, 56, 6371, 123, 126, 196, 198, 287, 422, 425427, 523, 525, 533, 543 capabilities bounding set, 65 clock tick , 39, 180, 199, 200, 204 close-on-exec, 45, 55, 145, 149, 152 cooperative multitasking, 71 copy on write, 23, 25, 41, 46, 302 core dump, 51, 195, 196, 214217 costante ADJ_ESTERROR, 204 ADJ_FREQUENCY, 204 ADJ_MAXERROR, 204 ADJ_OFFSET_SINGLESHOT, 204 ADJ_OFFSET, 204 ADJ_STATUS, 204 ADJ_TICK, 204 ADJ_TIMECONST, 204 AF_APPLETALK, 426 AF_INET6, 425, 431, 501, 502, 505, 506 AF_INET, 425, 431, 452, 501, 502, 505, 523, 546, 564 AF_PACKET, 427 AF_UNIX, 346, 426, 443 AIO_ALLDONE, 299 AIO_CANCELED, 299 AIO_LISTIO_MAX, 299 AIO_NOTCANCELED, 299 AI_ADDRCONFIG, 506, 510 AI_ALL, 506, 510 AI_CANONNAME, 510, 511 AI_DEFAULT, 506 AI_NUMERICHOST, 510 AI_PASSIVE, 510, 517 AI_V4MAPPED, 506, 510 ARG_MAX, 179, 180 ATADDR_BCAST, 426 ATPROTO_DDP, 426 AT_ANYNET, 426 AT_ANYNODE, 426 AT_EACCESS, 148 AT_FDCWD, 147 AT_REMOVEDIR, 149 AT_SYMLINK_NOFOLLOW, 148 BOOT_TIME, 192, 193 BRKINT, 262 BSDLY, 263 BUFSIZ, 172, 173 CAP_CHOWN, 64, 123 CAP_CLEAR, 69 CAP_DAC_OVERRIDE, 64 CAP_DAC_READ_SEARCH, 64 CAP_EFFECTIVE, 68 CAP_FOWNER, 64, 126, 127 CAP_FSETID, 64, 122 CAP_INHERITABLE, 68 CAP_IPC_LOCK, 23, 24, 64 CAP_IPC_OWNER, 64 CAP_KILL, 64, 542 CAP_LEASE, 64, 287 CAP_LINUX_IMMUTABLE, 64 CAP_MKNOD, 64 CAP_NET_ADMIN, 64, 523, 525, 533, 543 CAP_NET_BIND_SERVICE, 64, 425, 426 CAP_NET_BROADCAST, 64 CAP_NET_RAW, 64, 422, 427 CAP_PERMITTED, 68 CAP_SETFCAP, 64 CAP_SETGID, 64 CAP_SETPCAP, 64, 66 CAP_SETUID, 64 CAP_SET, 69 CAP_SYS_ADMIN, 64, 126 CAP_SYS_BOOT, 64 CAP_SYS_CHROOT, 64 CAP_SYS_MODULE, 64 CAP_SYS_NICE, 64, 77, 78 CAP_SYS_PACCT, 64, 198 CAP_SYS_PTRACE, 64 CAP_SYS_RAWIO, 64 CAP_SYS_RESOURCE, 64, 196 CAP_SYS_TIME, 64 CAP_SYS_TTY_CONFIG, 64 CBAUDEX, 264 CBAUD, 264

621

622
CHAR_BIT, 178 CHAR_MAX, 178 CHAR_MIN, 178 CHILD_MAX, 179 CIBAUD, 264 CLD_CONTINUED, 52 CLD_EXITED, 52 CLD_KILLED, 52 CLD_STOPPED, 52 CLK_TCK, 180, 199 CLOCAL, 264 CLOCKS_PER_SEC, 199, 200 CPU_SETSIZE, 79 CRDLY, 263 CREAD, 264 CRTSCTS, 264 CSIZE, 264 CSTOPB, 264 C_ANY, 498 C_CHAOS, 498 C_CSNET, 498 C_HS, 498 C_IN, 498 DEAD_PROCESS, 192, 193 DEFECHO, 265 DN_ACCESS, 288 DN_ATTRIB, 288 DN_CREATE, 288 DN_DELETE, 288 DN_MODIFY, 288 DN_MULTISHOT, 288 DN_RENAME, 288 DT_BLK, 103 DT_CHR, 103 DT_DIR, 103 DT_FIFO, 103 DT_REG, 103 DT_SOCK, 103 DT_UNKNOWN, 103 EAI_ADDRFAMILY, 511 EAI_AGAIN, 511 EAI_BADFLAGS, 511 EAI_FAIL, 511 EAI_FAMILY, 511 EAI_MEMORY, 511 EAI_NODATA, 511 EAI_NONAME, 510, 511 EAI_SERVICE, 511 EAI_SOCKTYPE, 511 EAI_SYSTEM, 511 ECHOCTL, 265 ECHOE, 265 ECHOKE, 265 ECHOK, 265 ECHONL, 265 ECHOPRT, 265 ECHO, 265

INDICE ANALITICO
EINTR, 396 EMPTY, 193 ENAMETOOLONG, 56 EPOLLERR, 283 EPOLLET, 282, 283 EPOLLHUP, 283 EPOLLIN, 283 EPOLLONESHOT, 283 EPOLLOUT, 283 EPOLLPRI, 283 EPOLLRDHUP, 283 EPOLL_CTL_ADD, 281, 282, 284 EPOLL_CTL_DEL, 281, 282 EPOLL_CTL_MOD, 281284 ETH_P_ALL, 427 EXIT_FAILURE, 14 EXIT_SUCCESS, 14 FD_CLOEXEC, 143, 149, 393 FD_SETSIZE, 274, 275, 485 FFDLY, 263 FIOASYNC, 152, 542 FIOCLEX, 152 FIOGETOWN, 153, 542 FIONBIO, 153 FIONCLEX, 152 FIONREAD, 153, 291, 548 FIOQSIZE, 153 FIOSETOWN, 153, 542 FLUSHO, 265 FL_FLOCK, 322 FL_POSIX, 322, 325 FOPEN_MAX, 178 FSETLOCKING_BYCALLER, 175 FSETLOCKING_INTERNAL, 175 FSETLOCKING_QUERY, 175 F_DUPFD, 146, 149 F_GETFD, 149 F_GETFL, 149, 151 F_GETLEASE, 151, 287 F_GETLK, 150, 324, 383 F_GETOWN, 150, 284 F_GETSIG, 150 F_NOTIFY, 151, 288 F_OK, 120 F_RDLCK, 287, 324, 385 F_SETFD, 149 F_SETFL, 150, 284 F_SETLEASE, 150, 287 F_SETLKW, 150, 324, 328, 383 F_SETLK, 150, 324, 325, 328 F_SETOWN, 150, 284, 285 F_SETSIG, 150, 151, 285, 286, 542 F_UNLCK, 287, 324, 385 F_WRLCK, 287, 324, 385 GETALL, 364 GETNCNT, 364 GETPID, 364

INDICE ANALITICO
GETVAL, 364, 368 GETZCNT, 364 HOST_NOT_FOUND, 500 HUPCL, 264 HZ, 39, 199, 229 ICANON, 265 ICRNL, 262 IEXTEN, 265, 266 IFF_ALLMULTI, 544 IFF_AUTOMEDIA, 544 IFF_BROADCAST, 544 IFF_DEBUG, 544 IFF_DYNAMIC, 544 IFF_LOOPBACK, 544 IFF_MASTER, 544 IFF_MULTICAST, 544 IFF_NOARP, 544 IFF_NOTRAILERS, 544 IFF_POINTOPOINT, 544 IFF_PORTSEL, 544 IFF_PROMISC, 544 IFF_RUNNING, 544 IFF_SLAVE, 544 IFF_UP, 544 IFNAMSIZ, 523 IGNBRK, 262 IGNCR, 262 IGNPAR, 262 IMAXBEL, 262 IN6ADDR_ANY_INIT, 510 IN6ADRR_ANY_INIT, 444 IN6ADRR_LOOPBACK_INIT, 444 INADDR_ANY, 441, 443, 510, 566 INADDR_BROADCAST, 443 INADDR_LOOPBACK, 443 INADDR_NONE, 431, 443 INET6_ADDRSTRLEN, 432 INET_ADDRSTRLEN, 432 INET_ANY, 454 INIT_PROCESS, 192, 193 INLCR, 262 INPCK, 262 INT_MAX, 178 INT_MIN, 178 IN_ACCESS, 290 IN_ALL_EVENTS, 290 IN_ATTRIB, 290 IN_CLOSE_NOWRITE, 290 IN_CLOSE_WRITE, 290 IN_CLOSE, 290 IN_CREATE, 290 IN_DELETE_SELF, 290 IN_DELETE, 290 IN_DONT_FOLLOW, 290, 291 IN_IGNORED, 291, 292 IN_ISDIR, 292 IN_MASK_ADD, 290, 291 IN_MODIFY, 290 IN_MOVED_FROM, 290, 292 IN_MOVED_TO, 290, 292 IN_MOVE_SELF, 290 IN_MOVE, 290 IN_ONESHOT, 291 IN_ONLYDIR, 290, 291 IN_OPEN, 290 IN_Q_OVERFLOW, 292 IN_UNMOUNT, 292 IOV_MAX, 308, 309 IPCMNI, 350 IPC_CREATE, 368, 376 IPC_CREAT, 351, 352 IPC_EXCL, 351, 352 IPC_NOWAIT, 354356, 365, 366 IPC_PRIVATE, 351, 352 IPC_RMID, 354, 363, 369, 371, 372 IPC_SET, 354, 363, 364, 371, 372 IPC_STAT, 354, 363, 371, 372, 605 IPPORT_RESERVED, 440 IPPORT_USERRESERVED, 440 IPPROTO_IP, 531 IPPROTO_TCP, 535 IPPROTO_UDP, 541 IPTOS_LOWDELAY, 533, 580 IPTOS_MINCOST, 580 IPTOS_RELIABILITY, 580 IPTOS_THROUGHPUT, 580 IP_ADD_MEMBERSHIP, 531, 534, 535 IP_DROP_MEMBERSHIP, 531, 535 IP_HDRINCL, 531, 533 IP_MTU_DISCOVER, 531, 533 IP_MTU, 531, 534 IP_MULTICAST_IF, 531, 535 IP_MULTICAST_LOOP, 531, 534 IP_MULTICAST_TTL, 531, 534 IP_OPTIONS, 531533, 574 IP_PKTINFO, 529, 531, 532 IP_PMTUDISC_DONT, 533 IP_PMTUDISC_DO, 533 IP_PMTUDISC_WANT, 533 IP_RECVDSTADDR, 529, 532 IP_RECVERR, 531, 533, 573 IP_RECVIF, 532 IP_RECVOPTS, 531, 532 IP_RECVTOS, 531, 532 IP_RECVTTL, 531, 532 IP_RETOPTS, 531, 532 IP_ROUTER_ALERT, 531, 534 IP_TOS, 531533, 580 IP_TTL, 531, 533, 550 ISIG, 265, 266 ISTRIP, 262 ITIMER_PROF, 226 ITIMER_REAL, 226 ITIMER_VIRTUAL, 226

623

624
ITIMER_VIRT, 227 IUCLC, 262 IXANY, 262 IXOFF, 262 IXON, 262, 266 LENGTH, 355, 356 LINK_MAX, 94, 181, 601 LIO_NOP, 300 LIO_NOWAIT, 300 LIO_READ, 299 LIO_WAIT, 300 LIO_WRITE, 300 LLONG_MAX, 178 LLONG_MIN, 178 LOCK_EX, 321, 330 LOCK_NB, 321, 329, 330 LOCK_SH, 321, 330 LOCK_UN, 321, 330 LOGIN_PROCESS, 192, 193 LOG_ALERT, 258 LOG_AUTHPRIV, 257 LOG_AUTH, 257 LOG_CONS, 257 LOG_CRIT, 258 LOG_CRON, 257 LOG_DAEMON, 257 LOG_DEBUG, 258 LOG_EMERG, 258 LOG_ERR, 258 LOG_FTP, 257 LOG_INFO, 258 LOG_KERN, 257 LOG_LOCAL0, 257 LOG_LOCAL7, 257 LOG_LPR, 257 LOG_MAIL, 257 LOG_NDELAY, 257 LOG_NEWS, 257 LOG_NOTICE, 258 LOG_NOWAIT, 257 LOG_ODELAY, 257 LOG_PERROR, 257 LOG_PID, 257 LOG_SYSLOG, 257 LOG_USER, 257 LOG_UUCP, 257 LOG_WARNING, 258 LONG_MAX, 178 LONG_MIN, 178 L_INCR, 139 L_SET, 139 L_XTND, 139 L_ctermid, 261 L_tmpnam, 109 MAP_32BIT, 302 MAP_ANONYMOUS, 301, 302, 385 MAP_ANON, 302

INDICE ANALITICO
MAP_DENYWRITE, 301, 302 MAP_EXECUTABLE, 302 MAP_FAILED, 301, 306 MAP_FILE, 302 MAP_FIXED, 302 MAP_GROWSDOWN, 302 MAP_LOCKED, 302 MAP_NONBLOCK, 302, 307, 308 MAP_NORESERVE, 302 MAP_POPULATE, 302, 303, 307, 308 MAP_PRIVATE, 301, 302, 304 MAP_SHARED, 301304, 307, 331, 385 MAXLINE, 460 MAXSYMLINKS, 99 MAX_CANON, 181, 259 MAX_INPUT, 181, 259 MB_LEN_MAX, 178 MCL_CURRENT, 24 MCL_FUTURE, 24 MINSIGSTKSZ, 240 MNT_FORCE, 187 MQ_MAXMSG, 388 MQ_MSGSIZE, 388 MQ_PRIO_MAX, 390 MREMAP_MAYMOVE, 306 MSGMAX, 352, 354, 355 MSGMNB, 352354 MSGMNI, 350352 MSG_ERRQUEUE, 573 MSG_EXCEPT, 356 MSG_MORE, 312 MSG_NOERROR, 356, 357 MSG_NOSIGNAL, 561, 562 MSG_OOB, 522 MSG_R, 348 MSG_W, 348 MS_ASYNC, 305 MS_BIND, 186 MS_INVALIDATE, 305 MS_MANDLOCK, 186 MS_MGC_MSK, 186 MS_MGC_VAL, 186 MS_MOVE, 186 MS_NOATIME, 186 MS_NODEV, 185, 186 MS_NODIRATIME, 186 MS_NOEXEC, 186 MS_NOSUID, 186 MS_RDONLY, 186 MS_REMOUNT, 186 MS_SYNCHRONOUS, 186 MS_SYNC, 305 NAME_MAX, 103, 181 NCCS, 265 NET_TCP_MAX_SYN_BACKLOG, 446 NEW_TIME, 192, 193 NGROUPS_MAX, 61, 179

INDICE ANALITICO
NGROUP_MAX, 180 NI_DGRAM, 515 NI_MAXHOST, 514 NI_MAXSERV, 514 NI_NAMEREQD, 515 NI_NOFQDN, 515 NI_NUMERICHOST, 515 NI_NUMERICSERV, 515 NLDLY, 263 NOFLSH, 265 NO_ADDRESS, 500 NO_DATA, 500 NO_RECOVERY, 500 NSIG, 215 NULL, 198, 311, 505, 507511, 514, 517, 520, 561, 562, 570 OCRNL, 263 OFDEL, 263 OFILL, 263 OLCUC, 263 OLD_TIME, 192, 193 ONLCR, 263 ONLRET, 263 ONOCR, 263 OPEN_MAX, 179, 600 OPOST, 263 O_ACCMODE, 151 O_APPEND, 136, 138, 141, 142, 144, 150 O_ASYNC, 136, 150, 284, 286 O_CLOEXEC, 136 O_CREATE, 114 O_CREAT, 135, 136, 144, 382, 387, 388, 393, 395, 396 O_DIRECTORY, 135137 O_DIRECT, 136 O_DSYNC, 136, 298 O_EXCL, 109, 110, 135, 136, 144, 159, 382, 387, 393, 395, 396 O_EXLOCK, 136 O_FSYNC, 136 O_LARGEFILE, 136 O_NDELAY, 136 O_NOATIME, 64, 136 O_NOBLOCK, 135, 264 O_NOCTTY, 136, 251, 255 O_NOFOLLOW, 135137 O_NONBLOCK, 136, 139, 141, 150, 273, 287, 331, 387, 389, 390, 448 O_RDONLY, 136, 151, 387, 393 O_RDWR, 136, 151, 343, 387, 393 O_READ, 136 O_RSYNC, 136 O_SHLOCK, 136 O_SYNC, 136, 298 O_TRUNC, 113, 114, 136, 331, 393 O_WRITE, 136 O_WRONLY, 135, 136, 151, 387

625
PACKET_BROADCAST, 428 PACKET_HOST, 428 PACKET_MULTICAST, 428 PACKET_OTHERHOST, 428 PACKET_OUTGOING, 428 PAGECACHE_SIZE, 392 PAGE_SIZE, 24, 197, 305, 306, 370, 372, 374, 555 PARENB, 264 PARMRK, 262 PARODD, 264 PATH_MAX, 108, 181, 386 PENDIN, 265 PF_APPLETALK, 421, 423, 426 PF_ASH, 421 PF_ATMPVC, 421, 423 PF_ATMSVC, 421 PF_AX25, 421, 423 PF_BLUETOOTH, 421 PF_BRIDGE, 421 PF_DECnet, 421 PF_ECONET, 421 PF_FILE, 421 PF_INET6, 421, 423, 425, 510, 559 PF_INET, 421, 423, 424, 510, 559 PF_INTERP, 53 PF_IPX, 421, 423 PF_IRDA, 421 PF_KEY, 421 PF_LLC, 421 PF_LOCAL, 421, 423, 426 PF_MAX, 422 PF_NETBEUI, 421 PF_NETLINK, 421, 423 PF_NETROM, 421 PF_PACKET, 421, 423, 426, 525 PF_PPPOX, 421 PF_ROSE, 421 PF_ROUTE, 421 PF_SECURITY, 421 PF_SNA, 421 PF_UNIX, 421, 426 PF_UNSPEC, 421, 510 PF_WANPIPE, 421 PF_X25, 421, 423 PID_MAX, 40 PIPE_BUF, 181, 334, 335, 340, 344, 451 POLLERR, 278, 283, 489 POLLHUP, 278, 489 POLLIN, 278, 283, 489 POLLMSG, 278 POLLNVAL, 278 POLLOUT, 278, 283, 489 POLLPRI, 278, 283, 489 POLLRDBAND, 278, 489, 574 POLLRDNORM, 278, 489 POLLWRBAND, 278

626
POLLWRNORM, 278 POSIX_FADV_DONTNEED, 319 POSIX_FADV_NOREUSE, 319 POSIX_FADV_NORMAL, 319 POSIX_FADV_RANDOM, 319 POSIX_FADV_SEQUENTIAL, 318, 319 POSIX_FADV_WILLNEED, 319 PRIO_MAX, 73 PRIO_MIN, 73 PRIO_PRGR, 74 PRIO_PROCESS, 74 PRIO_USER, 74 PROT_EXEC, 301, 302 PROT_NONE, 302 PROT_READ, 302 PROT_WRITE, 301, 302, 304, 306 PT_INTERP, 56 P_ALL, 52 P_PGID, 52 P_PID, 52 P_tmpdir, 109, 110 RES_AAONLY, 497 RES_BLAST, 497 RES_DEBUG, 497 RES_DEFAULT, 497 RES_DEFNAMES, 497, 498 RES_DNSRCH, 497, 498 RES_IGNTC, 497 RES_INIT, 497 RES_INSECURE1, 497 RES_INSECURE2, 497 RES_KEEPTSIG, 497 RES_NOALIASES, 497 RES_NOCHECKNAME, 497 RES_PRIMARY, 497 RES_RECURSE, 497 RES_ROTATE, 497 RES_STAYOPEN, 497 RES_TIMEOUT, 497 RES_USEVC, 497 RES_USE_INET6, 497, 501 RLIMIT_AS, 195 RLIMIT_CORE, 195, 196 RLIMIT_CPU, 195, 196 RLIMIT_DATA, 195 RLIMIT_FSIZE, 195 RLIMIT_LOCKS, 195 RLIMIT_MEMLOCK, 24, 195 RLIMIT_NOFILE, 195 RLIMIT_NPROC, 195 RLIMIT_RSS, 195 RLIMIT_SIGPENDING, 195 RLIMIT_STACK, 195 RLIM_INFINITY, 196 RUN_LVL, 192, 193 RUSAGE_CHILDREN, 195 RUSAGE_SELF, 194

INDICE ANALITICO
R_OK, 120 SA_NOCLDSTOP, 50, 235 SA_NOCLDWAIT, 235 SA_NODEFER, 235 SA_NOMASK, 235 SA_ONESHOT, 235 SA_ONSTACK, 235, 240 SA_RESETHAND, 235 SA_RESTART, 235, 397, 466 SA_SIGINFO, 150, 235, 244, 285, 286, 297 SCHAR_MAX, 178 SCHAR_MIN, 178 SCHED_FIFO, 75, 76, 229 SCHED_OTHER, 75, 76 SCHED_RR, 75, 229 SCM_CREDENTIALS, 523 SEEK_CUR, 139, 324 SEEK_END, 139, 324 SEEK_SET, 139, 324 SEMAEM, 362 SEMMNI, 350, 361, 362 SEMMNS, 361, 362 SEMMNU, 362 SEMMSL, 361, 362 SEMOPM, 362, 365 SEMUME, 362 SEMVMX, 362, 363, 365, 366 SEM_FAILED, 395 SEM_UNDO, 365368 SEM_VALUE_MAX, 395, 399 SETALL, 363, 364 SETVAL, 363, 364, 368 SHMALL, 370, 372 SHMLBA, 372, 374 SHMMAX, 370, 372 SHMMIN, 370, 372 SHMMNI, 350, 370, 372 SHMSEG, 372 SHM_LOCK, 372 SHM_RDONLY, 374 SHM_RND, 374 SHM_UNLOCK, 372 SHRT_MAX, 178 SHRT_MIN, 178 SIGABRT, 46, 216, 217, 225, 227 SIGALRM, 216, 218, 225, 228, 229, 231, 234, 238, 239 SIGBUS, 216, 217, 236, 303 SIGCHLD, 15, 46, 50, 52, 55, 216, 219, 229, 230, 235, 236, 254, 465, 466, 468, 470 SIGCLD, 216, 219, 229 SIGCONT, 46, 50, 51, 216, 219, 225, 252, 253 SIGEMT, 216 SIGEV_NONE, 296 SIGEV_SIGNAL, 297, 391 SIGEV_THREAD, 297, 391 SIGFPE, 216, 223, 236

INDICE ANALITICO
SIGHUP, 46, 216, 218, 252, 253, 264, 286 SIGILL, 216, 223, 236 SIGINFO, 216, 220 SIGINT, 216, 217, 221, 252, 262, 265, 266 SIGIOT, 216 SIGIO, 136, 150, 151, 153, 216, 218, 220, 236, 285, 286, 288, 542 SIGKILL, 195, 214, 216218, 223, 234 SIGLOST, 216, 220 SIGPIPE, 141, 216, 219, 335, 345, 473, 478, 561, 562, 601 SIGPOLL, 216, 218, 236 SIGPROF, 216, 218, 226 SIGPWR, 216 SIGQUEUE_MAX, 244 SIGQUIT, 216, 217, 221, 252, 265, 266 SIGRTMAX, 243 SIGRTMIN, 243 SIGSEGV, 17, 195, 216, 217, 223, 236, 241, 301 303, 374, 603 SIGSTKFLT, 216 SIGSTKSZ, 240 SIGSTOP, 72, 214, 216, 219, 223, 234, 235 SIGSUSP, 265 SIGSYS, 216, 217 SIGTERM, 216218, 252, 381, 472 SIGTRAP, 216, 217, 236 SIGTSTP, 216, 219, 235, 252, 266 SIGTTIN, 216, 219, 235, 251 SIGTTOU, 216, 219, 235, 251, 265, 267, 270 SIGUNUSED, 216 SIGURG, 150, 153, 216, 218, 542 SIGUSR1, 216, 220 SIGUSR2, 216, 220 SIGVTALRM, 216, 225 SIGWINCH, 216, 220 SIGXCPU, 195, 196, 216, 220 SIGXFSZ, 195, 216, 220 SIG_BLOCK, 238 SIG_DFL, 55, 221, 223 SIG_ERR, 222 SIG_IGN, 55, 221, 223, 229, 237 SIG_SETMASK, 238 SIG_UNBLOCK, 238 SIOCADDMULTI, 545 SIOCATMARK, 548, 574 SIOCDELMULTI, 545 SIOCGIFCONF, 546, 547 SIOCGIFFLAGS, 544 SIOCGIFHWADDR, 544, 545 SIOCGIFINDEX, 543 SIOCGIFMAP, 545 SIOCGIFMETRIC, 544 SIOCGIFMTU, 544 SIOCGIFNAME, 543 SIOCGIFTXQLEN, 545 SIOCGPGRP, 542

627
SIOCGSTAMP, 542 SIOCINQ, 548 SIOCOUTQ, 548 SIOCSIFFLAGS, 544 SIOCSIFHWADDR, 545 SIOCSIFHWBROADCAST, 545 SIOCSIFMAP, 545 SIOCSIFMETRIC, 544 SIOCSIFMTU, 544 SIOCSIFNAME, 545 SIOCSIFTXQLEN, 545 SIOCSPGRP, 542 SIVGTALRM, 218 SI_MESGQ, 392 SI_QUEUE, 244 SI_SIGIO, 285 SOCK_DGRAM, 422, 423, 426, 427, 444, 510, 524, 532, 559, 564, 567 SOCK_PACKET, 422 SOCK_RAW, 422, 423, 426, 427, 533 SOCK_RDM, 422, 423, 447, 448 SOCK_SEQPACKET, 422, 423, 444, 445, 447, 448 SOCK_STREAM, 283, 346, 422, 423, 442, 444, 445, 447, 448, 452, 510, 523, 524, 532, 533, 551 SOL_ICMPV6, 520 SOL_IPV6, 520 SOL_IP, 520, 531 SOL_SOCKET, 520, 521 SOL_TCP, 520, 535, 536 SOL_UDP, 541 SOMAXCONN, 447, 550 SO_ACCEPTCONN, 521, 524 SO_ATTACH_FILTER, 525 SO_BINDTODEVICE, 521, 523 SO_BROADCAST, 521, 524 SO_BSDCOMPAT, 521, 522 SO_DEBUG, 521, 523 SO_DETACH_FILTER, 525 SO_DONTROUTE, 521, 524 SO_ERROR, 478, 521, 525 SO_KEEPALIVE, 521, 525527, 537, 553 SO_LINGER, 470, 521, 524, 529531, 538 SO_OOBINLINE, 521, 522, 548, 574 SO_PASSCRED, 521, 523 SO_PEERCRED, 521, 523 SO_PRIORITY, 521, 524, 533 SO_RCVBUF, 521, 524, 549, 555, 556 SO_RCVLOWAT, 477, 521, 522 SO_RCVTIMEO, 521, 522 SO_REUSEADDR, 521, 523, 527529 SO_REUSEPORT, 529 SO_SNDBUF, 521, 524, 549, 557 SO_SNDLOWAT, 478, 521, 522 SO_SNDTIMEO, 521, 522 SO_TYPE, 521, 523 SPLICE_F_GIFT, 312, 315 SPLICE_F_MORE, 312

628
SPLICE_F_MOVE, 312 SPLICE_F_NONBLOCK, 312, 315 SSIZE_MAX, 179, 180 SS_DISABLE, 240, 241 SS_ONSTACK, 241 STDERR_FILENO, 135 STDIN_FILENO, 135 STDOUT_FILENO, 135 STREAM_MAX, 179, 180 SYS_NMLN, 183 S_APPEND, 186 S_IFBLK, 101, 113 S_IFCHR, 101, 113 S_IFDIR, 113 S_IFIFO, 101, 113 S_IFLNK, 113 S_IFMT, 113 S_IFREG, 101, 113 S_IFSOCK, 113 S_IGID, 119 S_IMMUTABLE, 186 S_IRGRP, 113, 117, 121 S_IROTH, 113, 117, 121 S_IRUSR, 113, 117, 121 S_IRWXG, 121 S_IRWXO, 121 S_IRWXU, 121 S_ISGID, 113, 118, 121 S_ISUID, 113, 118, 119, 121 S_ISVTX, 113, 119, 121 S_IWGRP, 113, 117, 121 S_IWOTH, 113, 117, 121 S_IWUSR, 113, 117, 121 S_IXGRP, 113, 117, 121 S_IXOTH, 113, 117, 121 S_IXUSR, 113, 117, 121 S_WRITE, 186 TABDLY, 263 TCIFLUSH, 271 TCIOFF, 271 TCIOFLUSH, 271 TCION, 271 TCOFLUSH, 271 TCOOFF, 271 TCOON, 271 TCP_CONGESTION, 536, 540 TCP_CORK, 312, 536, 537, 541 TCP_DEFER_ACCEPT, 536, 538 TCP_INFO, 536, 538, 539 TCP_KEEPCNT, 536, 537 TCP_KEEPIDLE, 536, 537 TCP_KEEPINTVL, 536, 537 TCP_LINGER2, 536, 538 TCP_MAXSEG, 435, 536 TCP_MSS, 417 TCP_NODELAY, 535, 536 TCP_QUICKACK, 536, 539, 540

INDICE ANALITICO
TCP_SYNCNT, 445, 536, 537 TCP_WINDOW_CLAMP, 536, 538 TCSADRAIN, 267 TCSAFLUSH, 267 TCSANOW, 267 TIME_BAD, 204 TIME_DEL, 204 TIME_INS, 204 TIME_OK, 204 TIME_OOP, 204 TIME_WAIT, 204 TIOCOUTQ, 548 TIOCSCTTY, 251 TMPNAME, 109 TMP_MAX, 109 TOSTOP, 265 TRY_AGAIN, 500 TZNAME_MAX, 179, 180 TZ, 205 T_AAAA, 499 T_AFSDB, 499 T_ANY, 499 T_ATMA, 499 T_AXFR, 499 T_A, 499 T_CNAME, 499 T_EID, 499 T_GPOS, 499 T_HINFO, 499 T_ISDN, 499 T_IXFR, 499 T_KEY, 499 T_LOC, 499 T_MAILA, 499 T_MAILB, 499 T_MB, 499 T_MD, 499 T_MF, 499 T_MG, 499 T_MINFO, 499 T_MR, 499 T_MX, 499 T_NAPTR, 499 T_NIMLOC, 499 T_NSAP_PTR, 499 T_NSAP, 499 T_NS, 499 T_NULL, 499 T_NXT, 499 T_PTR, 499 T_PX, 499 T_RP, 499 T_RT, 499 T_SIG, 499 T_SOA, 499 T_SRV, 499 T_TSIG, 499

INDICE ANALITICO
T_TXT, 499 T_WKS, 499 T_X25, 499 UCHAR_MAX, 178 UDP_CORK, 541 UDP_ENCAP, 541 UINT_MAX, 178 ULLONG_MAX, 178 ULONG_MAX, 178 USER_PROCESS, 192, 193 USHRT_MAX, 178 UTSLEN, 183 VDISCARD, 266 VEOF, 266 VEOL2, 266 VEOL, 266 VERASE, 266 VINTR, 266 VKILL, 266 VLNEXT, 266 VMIN, 266, 271 VQUIT, 266 VREPRINT, 266 VSTART, 266 VSTOP, 266 VSUSP, 266 VSWTC, 266 VTDLY, 263 VTIME, 266, 271 VWERASE, 266 WAIT_ANY, 49 WAIT_MYPGRP, 49 WCONTINUED, 50, 52 WEOF, 163 WEXITED, 52 WNOHANG, 4952, 230 WNOWAIT, 52 WSTOPPED, 52 WUNTRACED, 5052, 248 W_OK, 120 XATTR_CREATE, 128 XATTR_REPLACE, 128 XCASE, 265 X_OK, 120 _CHILD_MAX, 180 _IOFBF, 173 _IOLBF, 173 _IONBF, 173 _LINUX_CAPABILITY_VERSION, 66 _OPEN_MAX, 180 _PATH_UTMP, 191 _PATH_WTMP, 191 _POSIX_AIO_LISTIO_MAX, 179 _POSIX_AIO_MAX, 179 _POSIX_ARG_MAX, 179 _POSIX_CHILD_MAX, 179 _POSIX_LINK_MAX, 181 _POSIX_MAX_CANON, 181 _POSIX_MAX_INPUT, 181 _POSIX_NAME_MAX, 181 _POSIX_NGROUPS_MAX, 179 _POSIX_OPEN_MAX, 179 _POSIX_PATH_MAX, 181 _POSIX_PIPE_BUF, 181 _POSIX_SIGQUEUE_MAX, 244 _POSIX_SSIZE_MAX, 179 _POSIX_STREAM_MAX, 179 _POSIX_TZNAME_MAX, 179 _POSIX_VERSION, 179, 180 _SC_AVPHYS_PAGES, 197 _SC_IOV_MAX, 309 _SC_NPROCESSORS_CONF, 197 _SC_NPROCESSORS_ONLN, 197 _SC_PAGESIZE, 197 _SC_PHYS_PAGES, 197 _SYS_NMLN, 183 _UTSNAME_DOMAIN_LENGTH, 182 _UTSNAME_LENGTH, 182 security, 126 system, 126 trusted, 126 user, 126 CPU anity, 7880

629

deadlock , 81, 231, 239, 273, 323, 325, 340, 603 deep copy, 503, 514 Denial of Service (DoS), 136, 137, 302, 488, 549, 553 direttiva const, 30 extern, 444 inline, 237 register, 32, 35 undef, 66 union, 363, 425 volatile, 35, 80, 207 Discrectionary Access Control (DAC), 64 dnotify, 289 eetto ping-pong, 77, 78 endianess, 428430, 443 epoll , 280285 errore E2BIG, 53, 356, 365, 601 EACCESS, 198, 289, 306, 395, 398 EACCES, 53, 54, 74, 99, 100, 107, 108, 113, 115, 120, 150, 185, 249, 301, 323, 324, 351, 352, 354, 356, 363, 365, 371, 373, 387, 420, 443, 444, 599 EADDRINUSE, 443, 523, 527, 602 EADDRNOTAVAIL, 443, 602 EAFNOSUPPORT, 346, 431, 444, 602 EAGAIN, 41, 139141, 150, 195, 244, 245, 273, 284, 297, 299, 301, 306, 308, 309, 315, 324,

630

INDICE ANALITICO
331, 354, 355, 365, 366, 387, 389391, 397, ENOCHLD, 50 444, 447, 448, 522, 561, 562, 603, 604 ENODATA, 604 EALREADY, 444, 604 ENODEV, 135, 185, 301, 319, 600 EBADF, 113, 138, 145149, 274, 276, 277, 279, ENOENT, 53, 97, 115, 281, 351, 352, 387, 395, 281, 283, 291, 297, 301, 309, 311, 315, 318, 398, 599 319, 391, 443, 445, 447, 449, 519, 520, 599 ENOEXEC, 53, 599 EBADMSG, 604 ENOLCK, 323, 329, 601 EBUSY, 96, 100, 185187, 391, 600 ENOLINK, 605 ECANCELED, 298 ENOMEM, 24, 41, 183, 195, 240, 281, 289, 301, ECHILD, 49, 51, 601 306, 309, 311, 315, 361, 365, 370, 389, 447, 602, 603 ECONNABORTED, 602 ENOMSG, 356, 605 ECONNREFUSED, 444446, 562, 603 ENONET, 470 ECONNRESET, 470, 476, 477, 481, 526, 561, 602 ENOPROTOOPT, 470, 519, 520, 522, 602 EDEADLK, 323, 325, 603 ENOSPC, 99, 289, 319, 351, 361, 370, 432, 600 EDESTADDRREQ, 561, 603 ENOSR, 605 EDOM, 603 ENOSTR, 605 EDQUOT, 601 ENOSYS, 77, 187, 198, 251, 297, 299, 399, 604 EEXIST, 94, 97, 99, 100, 109, 110, 128, 135, 136, ENOTBLK, 185, 600 281, 351, 352, 387, 395, 600 ENOTCONN, 449, 482, 561, 562, 603 EFAULT, 62, 79, 234, 238, 240, 283, 305, 306, ENOTDIR, 96, 108, 135, 147, 148, 183, 600 309, 371, 449, 519, 520, 603 ENOTEMPTY, 96, 100, 601 EFBIG, 141, 195, 319, 600 ENOTSOCK, 443, 445, 447, 449, 482, 519, 520, EFTYPE, 601 601 EHOSTDOWN, 470, 603 ENOTSUP, 127129, 604 EHOSTUNREACH, 470, 474, 475, 481, 603 ENOTTY, 151, 153, 251, 260, 600 EIDRM, 351, 354356, 363, 365, 366, 371, 604 ENXIO, 135, 185, 340, 599 EILSEQ, 604 EOPNOTSUPP, 308, 346, 445, 447, 470, 561, 602 EINPROGRESS, 297, 298, 444, 604 EOVERFLOW, 371, 605 EINTR, 48, 49, 51, 109, 138141, 150, 222, 229, EPERM, 24, 53, 58, 62, 66, 7376, 78, 94, 97, 100, 245, 267, 274277, 279, 283, 299, 308, 323, 121, 123, 127129, 131, 183, 185, 186, 196, 324, 354356, 365, 366, 387, 396, 397, 447, 198, 201203, 224, 240, 244, 249, 251, 281, 451, 465, 466, 468, 487, 603 301, 354, 363, 371, 444, 447, 599 EINVAL, 24, 49, 51, 53, 62, 66, 7378, 96, 98, EPFNOSUPPORT, 602 100, 101, 107, 110, 113, 120, 138, 141, 145, EPIPE, 141, 220, 335, 561, 601 149, 151, 183, 185, 196, 208, 224, 229, 234, 238, 240, 244, 245, 249, 274, 276, 277, 279, EPROTONOSUPPORT, 346, 420, 602 281, 283, 289, 291, 297, 299, 301, 305311, EPROTOTYPE, 602 315, 318, 319, 338, 354, 356, 361, 363, 370, EPROTO, 470, 605 371, 373, 387390, 395399, 420, 425, 443, ERANGE, 107, 127129, 189, 208, 261, 363, 365, 519, 603 366, 504, 603 EIO, 251, 264, 309, 599 EREMOTE, 601 EISCONN, 561, 562, 602 EROFS, 94, 97, 120, 121, 600 EISDIR, 94, 96, 135, 600 ESHUTDOWN, 603 ELIBBAD, 53 ESOCKTNOSUPPORT, 602 ELOOP, 99, 135, 600 ESPIPE, 138, 139, 311, 318, 319, 338, 600 EMFILE, 145, 146, 149, 185, 195, 289, 420, 600 ESRCH, 66, 7379, 224, 244, 249, 601 EMLINK, 94, 99, 601 ESTALE, 601 EMSGSIZE, 389, 390, 561, 602 ETIMEDOUT, 389391, 397, 444, 475, 476, 481, EMULTIHOP, 604 526, 603 ENAMETOOLONG, 395, 398, 600 ETIME, 605 ENETDOWN, 470, 602 ETOOMANYREFS, 603 ENETRESET, 602 ETXTBSY, 53, 113, 135, 301, 302, 600 ENETUNREACH, 444, 445, 470, 475, 602 EUSERS, 601 ENFILE, 281, 289, 301, 420, 600 EUSER, 198 ENOAFSUPPORT, 432 EWOULDBLOCK, 140, 287, 321, 329, 447, 522, 604 ENOATTR, 127129 EXDEV, 94, 96, 600 Explicit Congestion Notication, 553 ENOBUFS, 420, 447, 449, 561, 602

INDICE ANALITICO
Extended Attributes, 125130 le descriptor, 86, 133135 di lock, 136, 144, 215, 381383 di congurazione /etc/fstab, 187, 386, 392 /etc/group, 6, 63, 188, 190 /etc/gshadow, 188 /etc/host.conf, 494 /etc/hosts, 494, 506 /etc/inittab, 253, 254 /etc/ld.so.conf, 13 /etc/localtime, 205 /etc/mtab, 187, 188 /etc/networks, 506 /etc/nsswitch.conf, 495 /etc/passwd, 6, 188, 190 /etc/protocols, 494, 506, 507, 510, 515, 520 /etc/resolv.conf, 494 /etc/services, 440, 443, 494, 506508 /etc/shadow, 188 /etc/syslog.conf, 256 /etc/timezone, 199 di dispositivo, 8385, 88, 95, 100, 170, 222, 273 di sistema /lib/ld-linux.so.1, 56 /lib/ld-linux.so.2, 56 /var/log/utmp, 58, 59 /var/log/wtmp, 58 dnotify, 287288 lesystem /proc /proc/filesystems, 185 /proc/sys/fs/file-max, 286 /proc/sys/fs/inotify/max_queued_events, 292 /proc/sys/fs/inotify/max_user_instances, 289 /proc/sys/fs/inotify/max_user_watches, 290 /proc/sys/fs/lease-break-time, 287 /proc/sys/kernel/acct, 198 /proc/sys/kernel/cap-bound, 65 /proc/sys/kernel/domainname, 184 /proc/sys/kernel/hostname, 184 /proc/sys/kernel/msgmax, 352 /proc/sys/kernel/msgmnb, 352 /proc/sys/kernel/msgmni, 350, 352, 372 /proc/sys/kernel/osrelease, 184 /proc/sys/kernel/ostype, 184 /proc/sys/kernel/rtsig-max, 244, 286 /proc/sys/kernel/sem, 350, 363 /proc/sys/kernel/shmall, 372 /proc/sys/kernel/shmmax, 372 /proc/sys/kernel/shmmni, 350 /proc/sys/kernel/version, 184 /proc/sys/net/core/dev_weight, 550

631
/proc/sys/net/core/lo_cong, 550 /proc/sys/net/core/mem_default, 555 /proc/sys/net/core/message_burst, 549 /proc/sys/net/core/message_cost, 549 /proc/sys/net/core/mod_cong, 550 /proc/sys/net/core/netdev_max_backlog, 550 /proc/sys/net/core/no_cong_thresh, 550 /proc/sys/net/core/no_cong, 550 /proc/sys/net/core/optmem_max, 550 /proc/sys/net/core/rmem_default, 549 /proc/sys/net/core/rmem_max, 524, 549, 556 /proc/sys/net/core/somaxconn, 550 /proc/sys/net/core/wmem_default, 549, 557 /proc/sys/net/core/wmem_max, 524, 549, 557 /proc/sys/net/ipv4/ip_always_defrag, 551 /proc/sys/net/ipv4/ip_autoconfig, 551 /proc/sys/net/ipv4/ip_default_ttl, 550 /proc/sys/net/ipv4/ip_dynaddr, 551 /proc/sys/net/ipv4/ip_forward, 551 /proc/sys/net/ipv4/ip_local_port_range, 551 /proc/sys/net/ipv4/ip_no_pmtu_disc, 551 /proc/sys/net/ipv4/ip_nonlocal_bind, 552 /proc/sys/net/ipv4/ipfrag_high_thresh, 552 /proc/sys/net/ipv4/ipfrag_low_thresh, 552 /proc/sys/net/ipv4/tcp_abort_on_overflow, 552 /proc/sys/net/ipv4/tcp_adv_win_scale, 552 /proc/sys/net/ipv4/tcp_app_win, 552 /proc/sys/net/ipv4/tcp_dsack, 552 /proc/sys/net/ipv4/tcp_ecn, 553 /proc/sys/net/ipv4/tcp_fack, 553 /proc/sys/net/ipv4/tcp_fin_timeout, 553 /proc/sys/net/ipv4/tcp_frto, 553 /proc/sys/net/ipv4/tcp_keepalive_intvl, 553 /proc/sys/net/ipv4/tcp_keepalive_probes, 553 /proc/sys/net/ipv4/tcp_keepalive_time, 553 /proc/sys/net/ipv4/tcp_low_latency, 553 /proc/sys/net/ipv4/tcp_max_orphans, 554 /proc/sys/net/ipv4/tcp_max_syn_backlog, 447, 554 /proc/sys/net/ipv4/tcp_max_tw_buckets, 554 /proc/sys/net/ipv4/tcp_mem, 554, 557 /proc/sys/net/ipv4/tcp_orphan_retries, 554 /proc/sys/net/ipv4/tcp_reordering, 555 /proc/sys/net/ipv4/tcp_retrans_collapse, 555 /proc/sys/net/ipv4/tcp_retries1, 555 /proc/sys/net/ipv4/tcp_retries2, 475, 555 /proc/sys/net/ipv4/tcp_rfc1337, 555

632
/proc/sys/net/ipv4/tcp_rmem, 524, 552, 555, 557 /proc/sys/net/ipv4/tcp_sack, 556 /proc/sys/net/ipv4/tcp_stdurg, 556 /proc/sys/net/ipv4/tcp_syn_retries, 445, 556 /proc/sys/net/ipv4/tcp_synack_retries, 556 /proc/sys/net/ipv4/tcp_syncookies, 447, 556 /proc/sys/net/ipv4/tcp_timestamps, 556 /proc/sys/net/ipv4/tcp_tw_recycle, 556 /proc/sys/net/ipv4/tcp_tw_reuse, 556 /proc/sys/net/ipv4/tcp_window_scaling, 556, 557 /proc/sys/net/ipv4/tcp_wmem, 524, 557 /proc/sys/vm/bdflush, 144 inotify, 288295 lease, 64, 150, 151, 286288 locking, 86, 138, 143, 149, 151, 195, 320331, 370, 383, 384 stream, 86, 155 le descriptor set, 274275, 279, 280, 477, 480, 483, 487, 488, 491, 574 le table, 45, 133, 134, 136, 138, 142, 322, 323, 325, 421 funzione ClientEcho, 473, 479, 480, 483, 484, 567, 568, 572 ComputeValues, 379 CreateMutex, 383 CreateShm, 394 DirScan, 105, 379 FindMutex, 383 FindShm, 395 FullRead, 451 FullWrite, 451, 452, 458, 460, 462, 463, 488 LockFile, 382 LockMutex, 383, 385 MutexCreate, 368 MutexFind, 368, 380 MutexLock, 368370, 378380 MutexRead, 368, 370 MutexRemove, 368, 379 MutexUnlock, 368370, 379, 380 PrintErr, 463 ReadMutex, 383 RemoveMutex, 383 RemoveShm, 395 SetTermAttr, 267, 268 ShmCreate, 376, 378 ShmFind, 376, 380 ShmRemove, 376, 379 SignalRestart, 466 Signal, 237, 466, 468 UnSetTermAttr, 267, 268 UnlockFile, 382

INDICE ANALITICO
UnlockMutex, 383, 385 WriteMess, 336 __clone, 46 __fbufsize denizione di, 173 __flbf denizione di, 173 __freadable denizione di, 172 __freading denizione di, 172 __fsetlocking denizione di, 175 __fwritable denizione di, 172 __fwriting denizione di, 172 _exit, 1416, 46, 47, 51, 242, 252, 255 denizione di, 15 _flushlbf denizione di, 174 abort, 14, 21, 46, 216, 217, 225, 227, 242 denizione di, 227 accept, 242, 433, 434, 445450, 454, 456, 458, 463470, 478, 487, 491, 536, 538, 540, 559, 560 denizione di, 447 access, 97, 114, 120, 148, 242 denizione di, 120 acct, 198 denizione di, 198 adjtimex, 204 denizione di, 202 adjtime, 204 denizione di, 202 aio_cancel, 298 denizione di, 298 aio_error, 242, 298, 299 denizione di, 297 aio_fsync denizione di, 298 aio_read, 297, 298 denizione di, 297 aio_return, 242, 298 denizione di, 298 aio_suspend, 242 denizione di, 299 aio_write, 297 denizione di, 297 alarm, 216, 218, 225229, 231, 232, 239, 242 denizione di, 225 alloca, 22 denizione di, 21 alphasort, 105 denizione di, 105 asctime, 204206 denizione di, 204

INDICE ANALITICO
asprintf denizione di, 169 atexit, 14, 15, 227 denizione di, 15 bind, 242, 425, 427, 428, 433, 442, 443, 445, 449, 454, 460, 462, 463, 510, 517, 523, 527, 529, 552, 559, 560, 566, 602 denizione di, 442 brk, 195, 373 denizione di, 22 calloc, 20 denizione di, 20 cap_clear denizione di, 68 cap_dup denizione di, 67 cap_free, 69, 71 denizione di, 67 cap_get_flag, 68 denizione di, 68 cap_get_proc, 71 denizione di, 69 cap_init, 67, 68 denizione di, 67 cap_set_flag, 69 denizione di, 68 cap_set_proc denizione di, 70 cap_to_text, 67, 71 denizione di, 69 capgetp denizione di, 69 capget, 66 denizione di, 66 capset, 66 denizione di, 66 cfgetispeed, 242 denizione di, 269 cfgetospeed, 242 denizione di, 269 cfree, 20 cfsetispeed, 242 denizione di, 269 cfsetospeed, 242 denizione di, 269 chdir, 97, 105, 108, 242, 254, 255, 377 denizione di, 108 chmod, 97, 114, 121, 122, 148, 242, 254, 288 denizione di, 121 chown, 97, 114, 123, 148, 242, 254, 288 denizione di, 123 chroot, 64, 84, 130, 131 denizione di, 130 clearenv, 29 denizione di, 30 clearerr_unlocked, 160 clearerr

633
denizione di, 160 clock_gettime, 242 clock denizione di, 200 closedir denizione di, 104 close, 135, 138, 159, 242, 388, 393, 435437, 448, 450, 456, 481, 482, 524, 530, 531, 562 denizione di, 138 connect, 242, 433, 434, 443445, 447, 449, 452, 463, 510, 516, 517, 522, 524, 534, 538, 560, 561, 567, 570, 604 denizione di, 444 creat, 97, 114, 144, 242, 288, 331 denizione di, 137 ctermid denizione di, 261 ctime, 204, 205, 452, 458 denizione di, 204 daemon, 343, 345, 357, 378, 380, 456, 462 denizione di, 255 dirfd, 105 denizione di, 102 dprintf, 169 drand48, 11 dup2, 145, 146, 242, 336 denizione di, 146 dup, 45, 145, 146, 158, 195, 242, 321, 322, 326 denizione di, 145 endgrent, 190 endhostent, 508 denizione di, 504 endian, 430 endnetent, 508 endprotoent, 508 endpwent, 190 endservent, 508 denizione di, 508 endutent, 191 denizione di, 191 endutxent, 193 epoll_create, 281, 283 denizione di, 281 epoll_ctlv, 282 epoll_ctl, 281, 282 denizione di, 281 epoll_wait, 282284 denizione di, 283 error_at_line denizione di, 210 error, 210 denizione di, 209 execle, 54, 242 execlp, 54 execl, 31, 33, 54 execve, 16, 46, 54, 65, 242, 398 denizione di, 53

634
execvp, 54 execv, 54 exec, 13, 16, 18, 19, 23, 25, 28, 30, 40, 43, 46, 5356, 58, 60, 65, 97, 114, 137, 145, 149, 179, 180, 196, 221, 241, 249, 253, 254, 304, 325, 347, 367, 374, 449, 599, 601 exevle, 254 exit, 1416, 39, 46, 47, 51, 56, 210, 374, 456, 465 denizione di, 14 faccessat, 147, 148 denizione di, 148 fchdir, 102, 105 denizione di, 108 fchmodat, 148 fchmod, 114, 121, 122, 242 denizione di, 121 fchownat, 148 fchown, 114, 123, 242 denizione di, 123 fcloseall denizione di, 159 fclose, 15, 158 denizione di, 159 fcntl, 55, 64, 137, 146, 149, 151, 153, 158, 171, 218, 242, 284288, 320, 321, 323325, 329, 330, 383, 448, 541, 542, 548 denizione di, 149 fd_in, 315 fdatasync, 242, 298 denizione di, 145 fdopen, 158, 159 denizione di, 158 feof, 162 denizione di, 160 ferror, 162 denizione di, 160 fflush_unlocked, 173 fflush, 158, 159 denizione di, 173 fgetc, 162 denizione di, 162 fgetgrent_r, 190 fgetgrent, 190 fgetpos denizione di, 171 fgetpwent_r, 190 fgetpwent, 190 fgets, 164166, 460, 464, 465, 472, 480, 483 denizione di, 164 fgetwc denizione di, 163 fgetws denizione di, 165 fgetxattr, 128 denizione di, 127 fileno, 480

INDICE ANALITICO
denizione di, 171 filter, 104 flistxattr denizione di, 129 flockfile denizione di, 174 flock, 320323, 328, 330 denizione di, 321 fmtmsg, 11 fopen, 158, 159 denizione di, 158 fork, 3946, 56, 60, 65, 66, 79, 137, 142, 145, 195, 196, 221, 242, 250, 253, 255, 304, 321 323, 325, 334, 335, 338, 367, 374, 399, 456, 457, 464, 604 denizione di, 41 fpathconf, 242 denizione di, 182 fprintf denizione di, 167 fpurge denizione di, 174 fputc denizione di, 163 fputs, 165, 453, 460, 472 denizione di, 165 fputws denizione di, 165 fread_unlocked denizione di, 162 fread, 161163 denizione di, 161 freeaddrinfo, 514 denizione di, 514 freehostent denizione di, 506 free, 2022, 166, 169 denizione di, 20 fremovexattr denizione di, 129 freopen, 156, 158 denizione di, 158 fscanf denizione di, 169 fseeko, 171 fseek, 159, 170, 171 denizione di, 170 fsetpos, 159 denizione di, 171 fsetxattr denizione di, 128 fstatat, 148 fstatfs, 187 denizione di, 187 fstat, 145, 242 denizione di, 111 fsync, 144, 145, 174, 242, 297, 298

INDICE ANALITICO
denizione di, 145 ftello, 171 ftell, 171 denizione di, 171 ftok, 380 denizione di, 348 ftruncate, 113, 114, 242, 288, 393, 395 denizione di, 113 ftrylockfile denizione di, 174 funlockfile denizione di, 174 futimesat, 148 fwrite_unlocked denizione di, 162 fwrite, 162, 163 denizione di, 161 gai_strerror, 511 get_avphys_pages denizione di, 197 get_phys_pages denizione di, 197 getaddrinfo, 509, 511515, 517 denizione di, 509 getchar, 162 denizione di, 162 getcwd, 108 denizione di, 107 getc, 162, 175 denizione di, 162 getdelim, 166 denizione di, 166 getegid, 59, 242 denizione di, 57 getenv, 29, 30 denizione di, 29 geteuid, 242 denizione di, 57 getgid, 242 denizione di, 57 getgrent_r, 190 getgrent, 190 getgrgid denizione di, 189 getgrnam denizione di, 189 getgrouplist denizione di, 62 getgroups, 242 denizione di, 62 gethostbyaddr, 505, 506, 508 denizione di, 505 gethostbyname2_r, 504 denizione di, 504 gethostbyname2, 504 denizione di, 502 gethostbyname_r

635
denizione di, 504 gethostbyname, 497, 501, 502, 504506, 508, 511, 514 denizione di, 501 gethostent, 508 getipnodebyaddr, 505, 506, 509 denizione di, 505 getipnodebyname, 505, 506, 509, 511, 514 denizione di, 505 getitimer, 227 denizione di, 227 getline, 166 denizione di, 166 getloadavg denizione di, 197 getnameinfo, 509, 514, 515 denizione di, 514 getnetbyaddr, 506 getnetbyname, 506 getnetent, 508 getopt, 26, 27 denizione di, 26 getpagesize, 24 denizione di, 197 getpeername, 242, 448, 450 denizione di, 449 getpgid, 248 denizione di, 248 getpgrp, 242, 248 denizione di, 248 getpid, 242 denizione di, 40 getppid, 41, 242 denizione di, 40 getpriority, 74 denizione di, 73 getprotobyaddr, 506 getprotobyname, 506 getprotoent, 508 getpwent_r, 190 getpwent, 190 getpwnam, 254 denizione di, 188 getpwuid denizione di, 188 getresgid denizione di, 61 getresuid denizione di, 61 getrlimit, 195 denizione di, 196 getrusage, 53 denizione di, 194 getservbyaddr, 506, 508 denizione di, 507 getservbyname, 506509 denizione di, 507

636
getservbyport, 507, 509 getservent, 508 denizione di, 508 getservname, 514 getsid denizione di, 249 getsockname, 242, 448, 449, 529 denizione di, 448 getsockopt, 242, 519525, 531, 534, 541, 548 denizione di, 520 gets, 164, 165, 474 denizione di, 164 gettimeofday, 201 denizione di, 201 getuid, 242 denizione di, 57 getutent_r, 193 getutent, 192 denizione di, 192 getutid_r, 193 getutid, 192 denizione di, 192 getutline_r, 193 getutline, 192 denizione di, 192 getutxent, 193 getutxid, 193 getutxline, 193 getwchar denizione di, 163 getwc denizione di, 163 getw denizione di, 163 getxattr, 127, 128 denizione di, 127 gmtime, 205 denizione di, 204 herror, 502 denizione di, 500 hsearch, 11 hstrerror denizione di, 500 htonl, 443 denizione di, 430 htons, 452 denizione di, 430 inet_addr, 430, 431 denizione di, 431 inet_aton, 430, 431 denizione di, 431 inet_ntoa, 430, 431, 546 denizione di, 431 inet_ntop, 431, 432, 458, 502, 512 denizione di, 432 inet_pton, 431, 452, 505, 516, 564 denizione di, 431

INDICE ANALITICO
initgroups, 63, 254 denizione di, 63 inotify_add_watch, 290, 291, 293 denizione di, 289 inotify_init, 290, 291, 293 denizione di, 289 inotify_rm_watch, 291, 292 denizione di, 291 ioctl, 151153, 222, 251, 260, 291, 354, 386, 541543, 546548, 574 denizione di, 151 ioperm, 64 iopl, 64 isatty denizione di, 260 killpg denizione di, 224 kill, 195, 211, 213, 214, 220, 223225, 236, 242, 244 denizione di, 224 lchown, 97, 114, 123 denizione di, 123 lgetxattr, 127, 128 denizione di, 127 linkat, 148 link, 88, 90, 93, 94, 96, 97, 114, 136, 148, 242, 288, 382 lio_listio, 296 denizione di, 299 listen, 242, 433, 443, 445, 448, 454, 463, 467, 468, 524, 550, 559, 566 denizione di, 445 listxattr denizione di, 129 llistxattr denizione di, 129 localtime, 205 denizione di, 204 lockf, 329, 330 denizione di, 329 logwtmp denizione di, 193 longjmp, 22, 34, 35, 231, 232, 239, 241, 242 denizione di, 34 lremovexattr denizione di, 129 lseek, 113, 135, 138142, 144, 145, 170, 242, 296, 324, 338 denizione di, 138 lsetxattr denizione di, 128 lstat, 97, 242 denizione di, 111 main, 1316, 25, 26, 46, 47, 51, 54 malloc, 2022, 33, 104, 158, 166, 172 denizione di, 20 memset, 376, 378, 395, 564

INDICE ANALITICO
mkdirat, 146, 148 mkdir, 97, 110, 114, 147, 148, 242, 288 denizione di, 99 mkdtemp denizione di, 110 mkfifoat, 148 mkfifo, 97, 114, 148, 242, 340, 343 denizione di, 101 mknodat, 148 mknod, 64, 97, 101, 148, 288, 340 denizione di, 100 mkstemp, 110 denizione di, 110 mktemp, 110 denizione di, 110 mktime, 204, 205 denizione di, 204 mlockall, 24, 25, 64 denizione di, 24 mlock, 24, 64 denizione di, 24 mmap, 64, 195, 302, 303, 305309, 312, 331, 393, 395, 399 denizione di, 301 mount, 186, 330 denizione di, 185 mprotect, 306 denizione di, 305 mq_close denizione di, 388 mq_getaddr, 390 mq_getattr, 389 denizione di, 389 mq_notify, 391, 392 denizione di, 391 mq_open, 387, 388 denizione di, 387 mq_receive, 387, 391 denizione di, 390 mq_send, 387, 390 denizione di, 389 mq_setattr, 389 denizione di, 389 mq_timedreceive denizione di, 390 mq_timedsend denizione di, 389 mq_unlink, 389 denizione di, 388 mremap, 195 denizione di, 306 msgctl denizione di, 354 msgget, 353, 357, 359, 361, 370 denizione di, 351 msgrcv, 357, 359 denizione di, 356

637
msgsnd, 355 denizione di, 354 msync, 302, 304, 305 denizione di, 304 munlockall, 24 denizione di, 24 munlock, 24 denizione di, 24 munmap, 302, 393 denizione di, 305 nanosleep, 229 denizione di, 229 nice denizione di, 73 ntohl denizione di, 430 ntohs, 458 denizione di, 430 ntp_adjtime, 203 offset, 140 on_exit, 15, 227 denizione di, 15 openat, 146148 denizione di, 147 opendir, 55, 97, 136 denizione di, 101 openlog, 257, 258 denizione di, 256 openpty, 272 open, 64, 88, 97, 98, 110, 114, 133, 135137, 143150, 158, 159, 195, 242, 264, 273, 286 288, 326, 331, 340, 343, 382, 383, 387, 388, 392, 393, 396 denizione di, 135 pathconf, 97, 182, 242 denizione di, 182 pause, 222, 228, 231, 232, 238, 242 denizione di, 228 pclose, 337, 339, 340 denizione di, 338 perror, 207209, 221, 452, 462, 500, 599 denizione di, 208 pipe, 114, 158, 195, 242, 338, 346 denizione di, 333 poll, 242, 277280, 282, 284, 285, 289, 357, 488490, 574 denizione di, 277 popen, 337339 denizione di, 338 posix_fadvise, 318, 319 denizione di, 318 posix_fallocate, 319 posix_trace_event, 242 ppoll, 277 denizione di, 279 pread, 140, 141, 288 denizione di, 140

638
printf, 17, 33, 44, 167170, 209, 256, 258 denizione di, 167 printk, 256 pselect, 242, 274, 277, 279 denizione di, 276 psignal, 220, 221, 276 denizione di, 221 ptrace, 50, 64 putchar denizione di, 163 putc, 175 denizione di, 163 putenv, 29, 30 denizione di, 29 putgrent, 190 putpwent, 190 puts, 165 denizione di, 165 pututline, 192, 193 denizione di, 192 pututxline, 193 putw denizione di, 163 pwrite, 140, 288 denizione di, 141 qsort, 104 raise, 211, 213, 223, 224, 227, 242 denizione di, 224 readahead, 318, 319 denizione di, 316 readdir_r, 102 readdir, 102 denizione di, 102 readlinkat, 148 readlink, 97, 148, 242 denizione di, 98 readv, 288, 308, 522 denizione di, 308 read, 114, 135137, 139141, 155, 156, 242, 259, 266, 271, 272, 275, 284, 288, 291293, 295, 297, 309311, 313, 315, 316, 320, 331, 334, 343, 422, 448, 450, 452, 453, 460, 462 465, 468, 472, 475478, 480, 487489, 522, 538, 560, 562, 570, 604 denizione di, 139 realloc, 20, 166, 306 denizione di, 20 recvfrom, 242, 428, 466, 522, 559, 560, 562, 564, 566, 568570 denizione di, 562 recvmsg, 242, 428, 522, 532, 533, 548, 573, 574 recv, 242, 428, 522 remap_file_pages, 307 denizione di, 307 removexattr denizione di, 129 remove, 95, 97, 114

INDICE ANALITICO
denizione di, 95 renameat, 148 rename, 91, 9597, 114, 148, 242, 288 denizione di, 96 res_init, 496, 497, 502 denizione di, 496 res_query, 498, 499 denizione di, 498 res_search, 497 denizione di, 498 rewinddir denizione di, 104 rewind, 159, 170 denizione di, 170 rmdir, 95, 114, 148, 149, 242, 288 denizione di, 100 sbrk, 195 denizione di, 22 scandir denizione di, 104 scanf, 169 denizione di, 169 sched_get_priority_max denizione di, 76 sched_get_priority_min denizione di, 76 sched_getaffinity denizione di, 79 sched_getparam denizione di, 76 sched_getscheduler denizione di, 77 sched_rr_get_interval denizione di, 77 sched_setaffinity, 78 denizione di, 78 sched_setparam, 76 denizione di, 76 sched_setscheduler, 75, 76 denizione di, 75 sched_yield, 75 denizione di, 77 seekdir denizione di, 103 seek, 311, 600 select, 242, 274277, 279, 280, 284, 285, 289, 357, 466, 477481, 483, 485489, 491, 522, 525, 526, 574, 604 denizione di, 274 sem_close, 398 denizione di, 398 sem_destroy, 400 denizione di, 399 sem_getvalue denizione di, 398 sem_init, 399, 400 denizione di, 399

INDICE ANALITICO
sem_open, 396, 398, 399 denizione di, 395 sem_post, 242, 396, 399 denizione di, 397 sem_timedwait, 397 sem_trywait denizione di, 397 sem_unlink denizione di, 398 sem_wait, 397399 denizione di, 396 semctl, 354, 361, 363365, 368, 369 denizione di, 363 semget, 361, 362, 368, 370 denizione di, 361 semop, 362, 366, 368, 369 denizione di, 365 sendfile, 309311, 537 denizione di, 309, 310 sendmsg, 242, 522, 532, 573 sendto, 242, 510, 522, 559, 560, 562, 564, 566, 567, 569, 570 denizione di, 561 send, 242, 522 setbuffer denizione di, 173 setbuf, 172 denizione di, 173 setegid denizione di, 60 setenv, 29, 30 denizione di, 29 seteuid denizione di, 60 setfsgid, 61 denizione di, 61 setfsuid, 61, 64 denizione di, 61 setgid, 60, 242, 254, 462 denizione di, 58 setgrent, 190 setgroups, 63 denizione di, 62 sethostent, 508 denizione di, 504 setitimer, 226, 227 denizione di, 226 setjmp, 34, 35, 232, 241 denizione di, 34 setlinebuf denizione di, 173 setlogmask denizione di, 258 setnetent, 508 setpgid, 242, 249, 253 denizione di, 249 setpgrp, 250

639
denizione di, 249 setpriority, 74 denizione di, 74 setprotoent, 508 setpwent, 190 setregid denizione di, 59 setresgid denizione di, 60 setresuid, 64 denizione di, 60 setreuid, 64 denizione di, 59 setrlimit, 195 denizione di, 196 setservent, 508 denizione di, 508 setsid, 242, 250, 252, 254, 255 denizione di, 250 setsockopt, 242, 519522, 524, 527, 528, 530, 531, 534, 541, 548, 574 denizione di, 519 settimeofday, 201, 202 denizione di, 201 setuid, 59, 60, 64, 242, 254, 462 denizione di, 58 setutent, 191 denizione di, 191 setutxent, 193 setvbuf, 173 denizione di, 172 setxattr denizione di, 128 shm_open, 393395, 399 denizione di, 392 shm_unlink, 395 denizione di, 393 shmat, 373, 374, 376 denizione di, 372 shmctl, 64, 354 denizione di, 371 shmdt, 372, 376 denizione di, 374 shmget, 370, 376, 399 denizione di, 370 shutdown, 242, 436, 450, 481484, 489, 524, 530, 531, 562 denizione di, 481, 482 sigaction, 214, 222, 223, 234238, 240, 242, 244, 245, 466 denizione di, 234 sigaddset, 242 denizione di, 233 sigaltstack, 240 denizione di, 240 sigdelset, 242 denizione di, 233

640
sigemptyset, 234, 242 denizione di, 233 sigfillset, 234, 242 denizione di, 233 sigismember, 233, 234, 242 denizione di, 233 siglongjmp, 242 denizione di, 241 signal, 214, 222, 223, 234237, 242 denizione di, 222 sigpause, 242 sigpending, 213, 242 denizione di, 240 sigprocmask, 238, 239, 241, 242, 276 denizione di, 237 sigqueue, 195, 242, 245 denizione di, 244 sigsetjmp, 242 denizione di, 241 sigset, 242 sigsuspend, 239, 242 denizione di, 238 sigtimedwait, 245 denizione di, 245 sigwaitinfo, 245 denizione di, 245 sigwait, 245 denizione di, 245 sleep, 43, 228, 231, 232, 239, 242, 379 denizione di, 228 snprintf, 458 denizione di, 167 sockbindopt, 527, 528 sockbind, 516518, 527, 528 sockconn, 515517 socketpair, 242, 345, 346, 381, 426, 441 denizione di, 346 socket, 242, 420423, 426, 427, 433, 442445, 452, 463, 510, 516, 527, 559 denizione di, 420 splice, 309316 denizione di, 311 sprintf, 167, 169 denizione di, 167 sscanf denizione di, 169 statfs, 187 denizione di, 187 stat, 88, 90, 97, 105, 111, 114, 116, 120, 127 129, 144, 148, 242, 348, 379 denizione di, 111 stime, 201, 202 denizione di, 201 strcmp, 105 strcoll, 105 strerror_r, 208 strerror, 207, 208, 220, 501, 511, 599

INDICE ANALITICO
denizione di, 207 strftime, 206 denizione di, 206 strsignal, 220, 221 denizione di, 220 strtol, 209 symlinkat, 148 symlink, 148, 242, 288 denizione di, 97 sync, 138, 144, 159, 174 denizione di, 144 sysconf, 62, 178181, 197, 199, 242, 309 denizione di, 180 sysctl, 183, 184, 198, 244, 289, 290, 350, 352, 363, 371, 445447, 524, 548, 549 denizione di, 183 syslog, 258, 462 denizione di, 257 sysv_signal, 223 tcdrain, 242 denizione di, 270 tcflow, 242, 271 denizione di, 271 tcflush, 242, 271 denizione di, 270 tcgetattr, 242, 267, 268, 270 denizione di, 267 tcgetgrp, 242 tcgetpgrp denizione di, 251 tcsendbreak, 242 denizione di, 270 tcsetattr, 242, 267270 denizione di, 267 tcsetpgrp, 242 denizione di, 251 tee, 311313, 315317 denizione di, 315 telldir, 103 denizione di, 104 tempnam, 41 denizione di, 109 timer_getoverrun, 242 timer_gettime, 242 timer_settime, 242 times, 242 denizione di, 200 time, 201, 242, 458 denizione di, 201 tmpfile, 110 denizione di, 109 tmpnam_r, 109 tmpnam, 110 denizione di, 109 truncate, 97, 113, 114, 150, 286288, 331 denizione di, 113 ttyname_r

INDICE ANALITICO
denizione di, 261 ttyname, 261 denizione di, 260 tzset denizione di, 205 umask, 242 denizione di, 122 umount2 denizione di, 187 umount, 187 denizione di, 186 uname, 182184, 242 denizione di, 182 ungetc, 164, 174 denizione di, 164 unlinkat, 147149 unlink, 90, 9397, 100, 114, 148, 149, 242, 288, 382, 383, 388, 393, 399 denizione di, 94 unsetenv, 29 denizione di, 29 updwtmp, 193 denizione di, 193 usleep denizione di, 228 utimes, 148 utime, 114, 115, 242, 288 denizione di, 115 utmpname, 191 denizione di, 191 vasprintf denizione di, 169 vdprintf, 169 versionsort, 105 denizione di, 105 vfork, 46 vfprintf denizione di, 168 vfscanf, 169 vhangup, 64 vmsplice, 311313, 316 denizione di, 313 vprintf denizione di, 168 vscanf, 169 vsnprintf denizione di, 169 vsprintf, 169 denizione di, 168 vsscanf, 169 wait3 denizione di, 53 wait4, 53, 194, 338 denizione di, 53 waitid, 50, 52 denizione di, 51

641
waitpid, 39, 4753, 201, 214, 229, 230, 235, 242, 248, 251, 601 denizione di, 49 wait, 15, 39, 4750, 52, 56, 201, 214, 222, 229, 242, 337, 601 denizione di, 48 writev, 288, 308, 522 denizione di, 308 write, 114, 135138, 141, 142, 144, 155157, 159, 173, 242, 259, 284, 288, 297, 309311, 313, 316, 320, 331, 448, 450, 454, 456, 460, 462, 472, 488, 522, 560562, 570 denizione di, 141 funzioni sicure, 242, 397 heap, 1821, 25, 30, 53, 399 inode, 8, 85, 8793, 95, 96, 99101, 103, 107, 111, 114, 115, 117, 133135, 142, 145, 322, 325, 340, 348, 360, 393 inotify, 284 interface index , 543 linked list, 92, 322, 325, 352, 509, 512, 514 Linux Security Modules, 56, 126 macro CPU_CLR, 79 CPU_ISSET, 79 CPU_SET, 79 CPU_ZERO, 79 DTTOIF, 103 FD_CLR, 274, 275, 483 FD_ISSET, 274, 275, 480 FD_SET, 274, 275 FD_ZERO, 274, 275 IFTODT, 103 LOG_MASK(p), 258 LOG_UPTO(p), 258 NULL, 67 POSIXLY_CORRECT, 27 RLIMIT_NOFILE, 277, 279 SHUT_RDWR, 482 SHUT_RD, 482 SHUT_WR, 482 SOCK_DEBUGGING, 523 STATE_TRACE, 523 S_ISBLK(m), 112 S_ISCHR(m), 112 S_ISDIR(m), 112 S_ISFIFO(m), 112 S_ISLNK(m), 112 S_ISREG(m), 112 S_ISSOCK(m), 112 WCOREDUMP(s), 51 WEXITSTATUS(s), 51 WIFCONTINUED(s), 51 WIFEXITED(s), 51

642
WIFSIGNALED(s), 51 WIFSTOPPED(s), 51 WSTOPSIG(s), 51 WTERMSIG(s), 51 _BSD_SOURCE, 11, 102, 173, 204, 249 _DIRENT_HAVE_D_NAMLEN, 102 _DIRENT_HAVE_D_OFF, 103 _DIRENT_HAVE_D_RECLEN, 103 _DIRENT_HAVE_D_TYPE, 103 _GNU_SOURCE, 12, 79, 137, 159, 165, 169, 173, 182, 220, 276, 279, 286, 288, 306, 311 _ISOC99_SOURCE, 12 _LARGEFILE_SOURCE, 12 _POSIX_ASYNCHRONOUS_IO, 296 _POSIX_C_SOURCE, 11, 12 _POSIX_JOB_CONTROL, 179, 180, 247 _POSIX_MEMLOCK_RANGE, 24 _POSIX_PRIORITIZED_IO, 296 _POSIX_PRIORITY_SCHEDULING, 76, 296 _POSIX_SAVED_IDS, 58, 179, 180 _POSIX_SOURCE, 11, 12, 63, 66 _POSIX_THREAD_SAFE_FUNCTIONS, 174 _REENTRANT, 82 _SVID_SOURCE, 12, 102, 173 _THREAD_SAFE, 82 _USE_BSD, 53 _XOPEN_SOURCE_EXTENDED, 12, 249 _XOPEN_SOURCE, 12, 223, 249, 276, 278, 318, 319, 397 __va_copy, 33 in6addr_any, 444 in6addr_loopback, 444 va_arg, 32, 33 va_copy, 33 va_end, 32, 33 va_list, 32 va_start, 32 Mandatory Access Control (MAC), 56 mandatory locking, 119, 123, 124, 186 masquerading, 551 Maximum Segment Size, 417, 434, 435, 437, 536, 552 Maximum Transfer Unit, 416417, 531, 533534, 536, 544, 551, 583 memoria virtuale, 4, 1618, 2325, 194, 196, 301, 302, 304, 306, 307, 318, 372, 392 memory leak , 21, 22, 166, 169 memory locking, 2325, 64, 372 memory mapping, 300308, 385 modo promiscuo, 428, 544 multicast, 64, 412, 414, 425, 428, 529, 531, 534535, 544, 545, 578, 581, 583, 584, 587, 593

INDICE ANALITICO
page fault, 17, 25, 74, 194, 215, 307 page table, 17, 301, 306, 307 paginazione, 17, 23, 24, 197, 300, 303, 318 pathname, 41, 54, 8384, 88, 94, 96, 99, 100, 106 108, 112, 113, 117, 135, 181, 182, 184, 185, 261, 344, 348, 386, 426, 599, 600 assoluto, 84, 130, 131, 386 relativo, 84, 106, 130, 131 polling, 274, 286, 357, 382, 383 POSIX IPC names, 385 prefaulting, 302, 307, 308 prehemptive multitasking, 3, 71 process group, 45, 49, 55, 74, 150, 224, 225, 248252, 254, 542, 601 process group leader , 249, 250, 255 process group orphaned , 252 process table, 38, 133, 213 process time, 180, 199201 race condition, 44, 8081, 108, 110, 111, 136, 137, 139, 144, 147, 212, 214, 230232, 238, 239, 250, 276, 277, 320, 335, 382 resolver , 493502, 511 Round Trip Time, 415, 483, 542

salto non-locale, 3335, 241, 242 scheduler , 3, 9, 38, 39, 43, 44, 7180, 198, 213, 214, 229, 367 segmento dati, 18, 19, 22, 25, 41, 53, 195 testo, 18, 25, 41, 53, 119, 195 self-pipe trick , 277 SELinux, 56, 126 sezione critica, 25, 81, 238, 360 sgid bit, 53, 55, 57, 58, 64, 109, 113, 116, 118119, 121124, 186, 330 signal mask , 237239 signal set, 233234, 274 socket denizione, 419432 locali, 570571 stack , 1822, 25, 30, 3235, 41, 53, 81, 164, 195, 214217, 235, 237, 240242, 302 sticky bit, 64, 95, 100, 113, 116, 118122, 124, 127, 601 struttura dati DirProp, 376, 378, 379 addrinfo, 509516 denizione di, 509 aiocb, 295, 297, 299, 300 denizione di, 296 at_addr denizione di, 427 cap_user_data_t Name Service Switch, 6, 188, 190, 494495, 506508 denizione di, 66 netlter , 549, 551, 556 cap_user_header_t out-of-band , 218, 236, 275, 278, 283, 422, 478, 489, denizione di, 66 521, 522, 542, 548, 574 dentry, 134

INDICE ANALITICO
dirent, 102105 denizione di, 103 epoll_event, 282284 denizione di, 282 f_ops, 88 file_lock, 322, 325 file_struct, 135, 142, 143, 145 files_struct, 133 file, 88, 134, 136138, 142, 143 flock, 323, 324 denizione di, 323 fs_struct, 122, 130 fstab, 188 group, 190 denizione di, 190 hostent, 501, 502, 504506 denizione di, 501 ifconf, 546 denizione di, 546 ifmap, 544, 545 denizione di, 545 ifreq, 543, 546 denizione di, 543 in6_addr, 432, 505 denizione di, 425 in_addr, 431, 432, 505 denizione di, 424 inode, 322 inotify_event, 291293 denizione di, 292 iovec, 308, 315 denizione di, 308 ip_mreqn, 531, 534, 535 denizione di, 535 ip_mreq, 535 ipc_perm, 347349 denizione di, 347 itimerval, 226 denizione di, 226 linger, 524, 530 denizione di, 530 mntent, 188 mq_attr, 388, 389 denizione di, 388 msgbuf, 355 denizione di, 355 msgid_ds, 353 msg, 355 msqid_ds, 354356 denizione di, 353 netent, 506 passwd, 189 denizione di, 189 pktinfo, 532 denizione di, 532 pollfd, 277280, 282, 491 denizione di, 278

643
protoent, 506 rlimit, 196 denizione di, 196 rusage, 53, 193, 194, 199 denizione di, 194 sched_param, 75 denizione di, 76 sem_queue, 367, 368 sem_undo, 367, 368 sembuf, 365, 367 denizione di, 365 semid_ds, 362364, 367 denizione di, 361 semunion, 368 semun, 363 denizione di, 363 sem, 362, 364, 365, 367 denizione di, 362 servent, 506508 denizione di, 507 shmid_ds, 370, 372, 374 denizione di, 371 sigaction, 234237, 466 denizione di, 235 sigevent, 244, 296, 391 denizione di, 296 siginfo_t, 52, 150, 235, 243, 244, 285, 286, 288, 297, 392 denizione di, 236 sigval_t, 244, 391 denizione di, 244 sock_extended_err, 573 denizione di, 573 sockaddr_atalk, 426 denizione di, 427 sockaddr_in6, 425 denizione di, 425 sockaddr_in, 424, 452, 454, 546 denizione di, 424 sockaddr_ll, 427 denizione di, 428 sockaddr_un, 426 denizione di, 426 sockaddr, 423, 425, 510, 544, 545 denizione di, 424 stack_t, 241 denizione di, 241 statfs, 187 denizione di, 187 stat, 94, 97, 103, 111, 112, 114, 116118, 155 denizione di, 112 task_struct, 38, 65, 73, 133, 213, 214, 237, 248250, 368 tcp_info, 536, 539 denizione di, 539 termios, 261, 264266, 269 denizione di, 261

644

INDICE ANALITICO

sa_family_t, 424 timespec, 77, 201, 229, 276, 279, 390 sig_atomic_t, 80, 237 denizione di, 202 sighandler_t, 222, 223 timeval, 194, 201, 226, 275, 522, 542 sigjmp_buf, 242 denizione di, 202 sigset_t, 8, 233, 234 timex, 202204 size_t, 8, 168, 308, 509, 561 denizione di, 203 socklen_t, 424, 509 timezone, 202 ssize_t, 8, 168, 179, 180 tms, 46, 200 tcflag_t, 262 denizione di, 200 time_t, 8, 199, 201, 204, 205 tm, 203, 205 uid_t, 8, 74 denizione di, 205 uint16_t, 424 ucred, 523 uint32_t, 424 utimbuf, 115 uint8_t, 424 denizione di, 115 uintmax_t, 168 utmp, 192, 193 elementare, 8 denizione di, 192 opaco, 32, 34, 102, 156 utsname, 182 primitivo, 8 denizione di, 183 suid bit, 53, 55, 57, 58, 64, 109, 113, 116, 118119, value result argument, 31, 61, 102, 105, 166, 169, 121124, 186 504, 509, 521, 542, 548, 562 SYN ood, 446, 556 variadic, 31, 167 system call lente, 221, 235, 273, 295, 468 Virtual File System, 84, 8689 TCP window scaling, 435, 556, 557 zombie, 47, 48, 50, 72, 229, 230, 465 thread , 399 three way handshake, 433434, 444, 446, 447, 463, 470, 471, 537, 538, 554, 556, 559 tipo DIR, 102 FILE, 156, 160 caddr_t, 8 cap_flag_t, 68 cap_flag_value_t, 68, 69 cap_t, 67, 69 cap_value_t, 68, 69 clock_t, 8, 199, 200 dev_t, 8 fd_set, 274, 276 fpos_t, 171 gid_t, 8 in_addr_t, 424, 431 in_port_t, 424 ino_t, 8 int16_t, 424 int32_t, 424 int8_t, 424 intmax_t, 168 jmp_buf, 34, 242 key_t, 8, 347 loff_t, 8 mode_t, 8, 121 mqd_t, 387 nlink_t, 8 off_t, 8, 170, 171 pid_t, 8, 40, 74, 248, 542 ptrdiff_t, 8, 168 rlim_t, 8

Bibliograa
[1] W. R. Stevens, Advanced Programming in the UNIX Environment. Prentice Hall PTR, 1995. [2] W. R. Stevens, UNIX Network Programming, volume 1. Prentice Hall PTR, 1998. [3] M. Gorman, Understanding the Linux Virtual Memory Manager. Prentice Hall PTR., 2004. [4] S. L. R. M. S. R. M. A. Oram and U. Drepper, The GNU C Library Reference Manual. Free Software Foundation, 1998. [5] A. Rubini and J. Corbet, Linux Device Driver. OReilly, 2002. [6] Aleph1, Smashing the stack for fun and prot, Phrack, 1996. [7] V. Paxson, Flex, varsion 2.5. Free Software Foundation, 1995. [8] C. Donnelly and R. M. Stallman, Bison, the YACC-compatible parser generator. Free Software Foundation, 2002. [9] S. Oullaine, Pratical C. OReilly, 2002. [10] A. Gierth, Unix programming frequently asked questions. [11] D. A. Rusling, The Linux Kernel. Linux Documentation Project, 1998. [12] W. R. Stevens, UNIX Network Programming, volume 2. Prentice Hall PTR, 1998. [13] W. R. Stevens, TCP/IP Illustrated, Volume 1, the protocols. Addison Wesley, 1994. [14] S. Piccardi, Amministrare GNU/Linux. Truelite Srl, 2004. [15] C. Liu and P. Albitz, DNS and BIND. OReilly, 1998. [16] S. Piccardi, Firewall e VPN con GNU/Linux. Truelite Srl, 2004.

645

Potrebbero piacerti anche