Documenti di Didattica
Documenti di Professioni
Documenti di Cultura
Simone Piccardi
13 dicembre 2011
ii
Un preambolo xiii
Prefazione xv
I Programmazione di sistema 1
iii
iv INDICE
9 I segnali 259
9.1 Introduzione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 259
9.1.1 I concetti base . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 259
9.1.2 Le semantiche del funzionamento dei segnali . . . . . . . . . . . . . . . . 260
9.1.3 Tipi di segnali . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 261
INDICE vii
13 I thread 479
13.1 Introduzione ai thread . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 479
13.1.1 Una panoramica . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 479
13.1.2 I thread e Linux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 479
13.1.3 Implementazioni alternative . . . . . . . . . . . . . . . . . . . . . . . . . . 479
13.2 Posix thread . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 479
13.2.1 Una panoramica . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 480
13.2.2 La gestione dei thread . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 480
13.2.3 I mutex . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 480
INDICE ix
E Ringraziamenti 691
Questa guida nasce dalla mia profonda convinzione che le istanze di libertà e di condivisione
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.
L’ambito più comune in cui questa filosofia viene applicata è quello della documentazione
perché il software, per quanto possa essere libero, se non accompagnato da una buona docu-
mentazione che aiuti a comprenderne il funzionamento, rischia di essere fortemente deficitario
riguardo ad una delle libertà fondamentali, quella di essere studiato e migliorato.
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, modificati e ampliati, o anche ridotti per usi specifici, 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
capacità, un po’ della conoscenza che ho ricevuto, mettendo a disposizione un testo che possa
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.
E, come per il software libero, anche in questo caso è importante la possibilità di accedere ai
sorgenti (e non solo al risultato finale, sia questo una stampa o un file formattato) e la libertà
di modificarli 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 differenze che comunque ci
sono fra un testo ed un programma.
Una di queste differenze è che in un testo, come in questa sezione, possono venire espresse
quelle che sono le idee ed i punti di vista dell’autore, e mentre trovo che sia necessario permettere
cambiamenti nei contenuti tecnici, che devono essere aggiornati e corretti, non vale lo stesso per
l’espressione 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à
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 programma-
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 specifica 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
GNU, le cosiddette GNU C Library o glibc, che ormai sono usate nella stragrande maggioranza
dei casi, senza tralasciare, là dove note, le differenze con altre implementazioni come le libc5 o
le uclib.
L’obiettivo finale di questo progetto è quello di riuscire a ottenere un testo utilizzabile per
apprendere i concetti fondamentali della programmazione di sistema della stessa qualità dei libri
del compianto R. W. Stevens (è un progetto molto ambizioso ...).
Infatti benché le pagine di manuale del sistema (quelle che si accedono con il comando man)
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
inadatti ad una trattazione che vada oltre la descrizione delle caratteristiche particolari dello
specifico 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
particolare [1] e [2]) per rendere la trattazione dei vari argomenti in una sequenza logica il più
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
programmi e gli esempi descritti nel testo è lo GNU GCC. Il testo presuppone una conoscenza
media del linguaggio, e di quanto necessario per scrivere, compilare ed eseguire un programma.
Infine, dato che lo scopo del progetto è la produzione di un libro, si è scelto di usare LATEX
come ambiente di sviluppo del medesimo, sia per l’impareggiabile qualità tipografica ottenibile,
che per la congruenza dello strumento con il fine, tanto sul piano pratico, quanto su quello
filosofico.
Il testo sarà, almeno inizialmente, in italiano. Per il momento lo si è suddiviso in due parti,
la prima sulla programmazione di sistema, in cui si trattano le varie funzionalità disponibili per i
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
macchine diverse.
xv
Parte I
Programmazione di sistema
1
Capitolo 1
In questo primo capitolo sarà fatta un’introduzione ai concetti generali su cui è basato un
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
che riguarda la programmazione.
Dopo un’introduzione sulle caratteristiche principali di un sistema di tipo Unix passeremo ad
illustrare alcuni dei concetti base dell’architettura di GNU/Linux (che sono comunque comuni
a tutti i sistemi unix-like) ed introdurremo alcuni degli standard principali a cui viene fatto
riferimento.
3
4 CAPITOLO 1. L’ARCHITETTURA DEL SISTEMA
necessario il processo potrà accedere alle risorse hardware soltanto attraverso delle opportune
chiamate al sistema che restituiranno il controllo al kernel.
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 l’ausilio della unità di gestione della memoria, si incaricherà di rimappare au-
tomaticamente sulla memoria disponibile, salvando su disco quando necessario (nella cosiddetta
area di swap) le pagine di memoria in eccedenza.
Le periferiche infine vengono viste in genere attraverso un’interfaccia astratta che permette
di trattarle come fossero file, secondo il concetto per cui everything is a file, su cui torneremo in
dettaglio in cap. 4. Questo non è vero per le interfacce di rete, che non rispondendo bene a detta
astrazione hanno un’interfaccia diversa, ma resta valido anche per loro il concetto generale che
tutto il lavoro di accesso e gestione delle periferiche a basso livello è effettuato dal kernel.
diffusi 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.
scheduler VM driver
kernel
user space
GNU C Library
Figura 1.1: Schema di massima della struttura di interazione fra processi, kernel e dispositivi in Linux.
Normalmente ciascuna di queste chiamate al sistema fornite dal kernel viene rimappata
in opportune funzioni con lo stesso nome definite dentro la libreria fondamentale del sistema,
chiamata Libreria Standard del C (C Standard Library) in ragione del fatto che il primo Unix
venne scritto con il linguaggio C ed usando le librerie ad esso associato. Detta libreria, oltre alle
interfacce alle system call, contiene anche tutta la serie delle ulteriori funzioni di base definite
nei vari standard, che sono comunemente usate nella programmazione.
Questo è importante da capire perché programmare in Linux significa anzitutto essere in
grado di usare le varie funzioni fornite dalla Libreria Standard del C, in quanto né il kernel,
né il linguaggio C implementano direttamente operazioni comuni come l’allocazione dinamica
6 CAPITOLO 1. L’ARCHITETTURA DEL SISTEMA
della memoria, l’input/output bufferizzato sui file 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
breve glibc), ovvero la libreria realizzata dalla Free Software Foundation nella quale sono state
implementate tutte le funzioni essenziali definite negli standard POSIX e ANSI C, utilizzate da
qualunque programma.
Le funzioni della libreria standard 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 delle chiamate al sistema del kernel; è importante avere presente questa distinzione,
fondamentale dal punto di vista dell’implementazione, anche se poi, nella realizzazione di normali
programmi, non si hanno differenze pratiche fra l’uso di una funzione di libreria e quello di una
chiamata al sistema.
Le librerie standard del C GNU consentono comunque, nel caso non sia presente una specifica
funzione di libreria corrispondente, di eseguire una system call generica tramite la funzione
syscall, il cui prototipo, accessibile se si è definita la macro _GNU_SOURCE, (vedi sez. 1.2.7) è:
#include <unistd.h>
#include <sys/syscall.h>
int syscall(int number, ...)
Esegue la system call indicata da number.
La funzione richiede come primo argomento il numero della system call da invocare, seguita
dagli argomenti da passare alla stessa (che ovviamente dipendono da quest’ultima), e restituisce
il codice di ritorno della system call invocata. In generale un valore nullo indica il successo ed
un valore negativo è un codice di errore che poi viene memorizzato nella variabile errno (sulla
gestione degli errori torneremo in dettaglio in sez. 8.5).
Il valore di number dipende sia dalla versione di kernel che dall’architettura,3 ma ciascuna
system call viene in genere identificata da una costante nella forma SYS_* dove al prefisso viene
aggiunto il nome che spesso corrisponde anche alla omonima funzione di libreria; queste costanti
sono definite nel file sys/syscall.h, ma si possono anche usare direttamente valori numerici.
attraverso la richiesta di una parola d’ordine (la password ), anche se sono possibili meccanismi
diversi.4 Eseguita la procedura di riconoscimento in genere il sistema manda in esecuzione un
programma di interfaccia (che può essere la shell su terminale o un’interfaccia grafica) che mette
a disposizione dell’utente un meccanismo con cui questo può impartire comandi o eseguire altri
programmi.
Ogni utente appartiene anche ad almeno un gruppo (il cosiddetto default group), ma può
essere associato ad altri gruppi (i supplementary group), questo permette di gestire i permessi
di accesso ai file e quindi anche alle periferiche, in maniera più flessibile, definendo gruppi di
lavoro, di accesso a determinate risorse, ecc.
L’utente e il gruppo sono identificati da due numeri, la cui corrispondenza ad un nome
espresso in caratteri è inserita nei due file /etc/passwd e /etc/group.5 Questi numeri sono
l’user identifier, detto in breve user-ID, ed indicato dall’acronimo uid, e il group identifier, detto
in breve group-ID, ed identificato dall’acronimo gid, e sono quelli che vengono usati dal kernel
per identificare l’utente; torneremo in dettaglio su questo argomento in sez. 3.3.
In questo modo il sistema è in grado di tenere traccia dell’utente a cui appartiene ciascun
processo ed impedire ad altri utenti di interferire con quest’ultimo. Inoltre con questo sistema
viene anche garantita una forma base di sicurezza interna in quanto anche l’accesso ai file (vedi
sez. 5.3) è regolato da questo meccanismo di identificazione.
Infine in ogni Unix è presente un utente speciale privilegiato, il cosiddetto superuser, il cui
username è di norma root, ed il cui uid è zero. Esso identifica l’amministratore del sistema,
che deve essere in grado di fare qualunque operazione; per l’utente root infatti i meccanismi di
controllo descritti in precedenza sono disattivati.6
Scopo dello standard è quello di garantire la portabilità dei programmi C fra sistemi operativi
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à
con questo standard, fornendo le funzioni di libreria da esso previste. Queste sono dichiarate in
una serie di header file 7 (anch’essi provvisti dalla glibc); in tab. 1.1 si sono riportati i principali
header file definiti negli standard POSIX ed ANSI C, che sono anche quelli definiti negli altri
standard descritti nelle sezioni successive.
Standard
Header Contenuto
ANSI C POSIX
assert.h • – Verifica le asserzioni fatte in un programma.
ctype.h • – Tipi standard.
dirent.h – • Manipolazione delle directory.
errno.h – • Errori di sistema.
fcntl.h – • Controllo sulle opzioni dei file.
limits.h – • Limiti e parametri del sistema.
malloc.h • – Allocazione della memoria.
setjmp.h • – Salti non locali.
signal.h – • Gestione dei segnali.
stdarg.h • – Gestione di funzioni a argomenti variabili.
stdio.h • – I/O bufferizzato in standard ANSI C.
stdlib.h • – Definizioni della libreria standard.
string.h • – Manipolazione delle stringhe.
time.h – • Gestione dei tempi.
times.h • – Gestione dei tempi.
unistd.h – • Unix standard library.
utmp.h – • Registro connessioni utenti.
Tabella 1.1: Elenco dei vari header file definiti dallo standard POSIX.
In realtà le glibc ed i relativi header file definiscono un insieme di funzionalità in cui sono
incluse come sottoinsieme anche quelle previste dallo standard ANSI C. È possibile ottenere
una conformità stretta allo standard (scartando le funzionalità addizionali) usando il gcc con
l’opzione -ansi. Questa opzione istruisce il compilatore a definire nei vari header file soltanto le
funzionalità previste dallo standard ANSI C e a non usare le varie estensioni al linguaggio e al
preprocessore da esso supportate.
alcuni di questi tipi si sono rivelati inadeguati o sono cambiati, ci si è trovati di fronte ad una
infinita serie di problemi di portabilità.
Tipo Contenuto
caddr_t Core address.
clock_t Contatore del tempo di sistema.
dev_t Numero di dispositivo (vedi sez. 5.1.5).
gid_t Identificatore di un gruppo.
ino_t Numero di inode.
key_t Chiave per il System V IPC.
loff_t Posizione corrente in un file.
mode_t Attributi di un file.
nlink_t Contatore dei link su un file.
off_t Posizione corrente in un file.
pid_t Identificatore di un processo.
rlim_t Limite sulle risorse.
sigset_t Insieme di segnali.
size_t Dimensione di un oggetto.
ssize_t Dimensione in numero di byte ritornata dalle funzioni.
ptrdiff_t Differenza fra due puntatori.
time_t Numero di secondi (in tempo di calendario, vedi sez. 8.4).
uid_t Identificatore di un utente.
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 definiti nell’header file sys/types.h, in modo da mantenere completamente indipendenti i tipi
utilizzati dalle funzioni di sistema dai tipi elementari supportati dal compilatore C.
Linux e le glibc implementano le principali funzionalità richieste dalle specifiche SVID che
non sono già incluse negli standard POSIX ed ANSI C, per compatibilità con lo Unix System
V e con altri Unix (come SunOS) che le includono. Tuttavia le funzionalità più oscure e meno
utilizzate (che non sono presenti neanche in System V) sono state tralasciate.
Le funzionalità implementate sono principalmente il meccanismo di intercomunicazione fra
i processi e la memoria condivisa (il cosiddetto System V IPC, che vedremo in sez. 11.2) le
funzioni della famiglia hsearch e drand48, fmtmsg e svariate funzioni matematiche.
Benché l’insieme degli standard POSIX siano basati sui sistemi Unix, essi definiscono comun-
que un’interfaccia di programmazione generica e non fanno riferimento ad una implementazione
specifica (ad esempio esiste un’implementazione di POSIX.1 anche sotto Windows NT).
Linux e le glibc implementano tutte le funzioni definite nello standard POSIX.1, queste
ultime forniscono in più alcune ulteriori capacità (per funzioni di pattern matching e per la
manipolazione delle regular expression), che vengono usate dalla shell e dai comandi di sistema
e che sono definite nello standard POSIX.2.
Nelle versioni più recenti del kernel e delle librerie sono inoltre supportate ulteriori funziona-
lità aggiunte dallo standard POSIX.1c per quanto riguarda i thread (vedi cap. 13), e dallo stan-
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. 11.4) e l’I/O asincrono (sez. 12.3.3).
Lo standard principale resta comunque POSIX.1, che continua ad evolversi; la versione più
nota, cui 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 ISO/IEC 9945-1:1996 ed include i precedenti POSIX.1b e POSIX.1c. In genere si fa
riferimento ad essa come POSIX.1-1996.
Nel 2001 è stata poi eseguita una sintesi degli standard POSIX.1, POSIX.2 e SUSv3 (vedi
sez. 1.2.6) in un unico documento, redatto sotto gli auspici del cosiddetto gruppo Austin che
va sotto il nome di POSIX.1-2001. Questo standard definisce due livelli di conformità, quello
POSIX, in cui sono presenti solo le interfacce di base, e quello XSI che richiede la presenza di
una serie di estensioni opzionali per lo standard POSIX, riprese da SUSv3. Inoltre lo standard
è stato allineato allo standard C99, e segue lo stesso nella definizione delle interfacce.
A questo standard sono stati aggiunti due documenti di correzione e perfezionamento deno-
minati Technical Corrigenda, il TC1 del 2003 ed il TC2 del 2004, e talvolta si fa riferimento agli
stessi con le sigle POSIX.1-2003 e POSIX.1-2004.
Una ulteriore revisione degli standard POSIX e SUS è stata completata e ratificata nel 2008,
cosa che ha portato al rilascio di una nuova versione sotto il nome di POSIX.1-2008 (e SUSv4),
con l’incorporazione di alcune nuove interfacce, la obsolescenza di altre, la trasformazione da
opzionali a richieste di alcune specifiche di base, oltre alle solite precisazioni ed aggiornamenti.
Anche in questo caso è prevista la suddivisione in una conformità di base, e delle interfacce
aggiuntive.
12 CAPITOLO 1. L’ARCHITETTURA DEL SISTEMA
_XOPEN_SOURCE_EXTENDED
definendo questa macro si rendono disponibili le ulteriori funzionalità neces-
sarie ad essere conformi al rilascio del marchio X/Open Unix corrisponden-
ti allo standard Unix95, vale a dire quelle specificate da SUSv1/XPG4v2.
Questa macro viene definita implicitamente tutte le volte che si imposta
_XOPEN_SOURCE ad un valore maggiore o uguale a 500.
Benché Linux supporti in maniera estensiva gli standard più diffusi, esistono comunque delle
estensioni e funzionalità specifiche, non presenti in altri standard e lo stesso vale per le glibc
stesse, che definiscono anche delle ulteriori funzioni di libreria. Ovviamente l’uso di queste fun-
zionalità deve essere evitato se si ha a cuore la portabilità, ma qualora questo non sia un requisito
esse possono rivelarsi molto utili.
Come per l’aderenza ai vari standard, le funzionalità aggiuntive possono essere rese esplici-
tamente disponibili tramite la definizione di opportune macro di preprocessore, alcune di que-
ste vengono attivate con la definizione di _GNU_SOURCE, mentre altre devono essere attivate
esplicitamente, inoltre alcune estensioni possono essere attivate indipendentemente tramite una
opportuna macro; queste estensioni sono illustrate nel seguente elenco:
_LARGEFILE_SOURCE
definendo questa macro si rendono disponibili alcune funzioni che consentono
di superare una inconsistenza presente negli standard con i file di grandi
dimensioni, ed in particolare definire le due funzioni fseeko e ftello che
al contrario delle corrispettive fseek e ftell usano il tipo di dato specifico
off_t (vedi sez. 7.2.7).
_LARGEFILE64_SOURCE
definendo questa macro si rendono disponibili le funzioni di una interfaccia
alternativa al supporto di valori a 64 bit nelle funzioni di gestione dei file (non
supportati in certi sistemi), caratterizzate dal suffisso 64 aggiunto ai vari nomi
di tipi di dato e funzioni (come off64_t al posto di off_t o lseek64 al posto
di lseek).
16 CAPITOLO 1. L’ARCHITETTURA DEL SISTEMA
Se non è stata specificata esplicitamente nessuna di queste macro il default assunto è che
siano definite _BSD_SOURCE, _SVID_SOURCE, _POSIX_SOURCE e, con le glibc più recenti, che la
macro _POSIX_C_SOURCE abbia il valore “200809L”, per versioni precedenti delle glibc il valore
assegnato a _POSIX_C_SOURCE era di “200112L” prima delle 2.10, di “199506L” prima delle 2.4,
di “199506L” prima delle 2.1. Si ricordi infine che perché queste macro abbiano effetto devono
essere sempre definite prima dell’inclusione dei file di dichiarazione.
18 CAPITOLO 1. L’ARCHITETTURA DEL SISTEMA
Capitolo 2
Come accennato nell’introduzione il processo è l’unità di base con cui un sistema unix-like alloca
ed utilizza le risorse. Questo capitolo tratterà l’interfaccia base fra il sistema e i processi, come
vengono passati gli argomenti, come viene gestita e allocata la memoria, come un processo può
richiedere servizi al sistema e cosa deve fare quando ha finito la sua esecuzione. Nella sezione
finale 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 affronteremo l’avvio e il funzionamento di un singolo processo
partendo dal punto di vista del programma che viene messo in esecuzione.
19
20 CAPITOLO 2. L’INTERFACCIA BASE CON I PROCESSI
In realtà nei sistemi Unix esiste un altro modo per definire la funzione main, che prevede
la presenza di un terzo argomento, char *envp[], che fornisce (vedi sez. 2.3.3) l’ambiente del
programma; questa forma però non è prevista dallo standard POSIX.1 per cui se si vogliono
scrivere programmi portabili è meglio evitarla.
#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
le librerie standard del C; essa esegue tutte le funzioni che sono state registrate con atexit
2.1. ESECUZIONE E CONCLUSIONE DI UN PROGRAMMA 21
e on_exit (vedi sez. 2.1.4), e chiude tutti gli stream effettuando il salvataggio dei dati sospe-
si (chiamando fclose, vedi sez. 7.2.1), infine 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 immediata-
mente il processo; i dati sospesi nei buffer degli stream non vengono salvati e le eventuali funzioni
registrate con atexit e on_exit non vengono eseguite. Il prototipo della funzione è:
#include <unistd.h>
void _exit(int status)
Causa la conclusione immediata del programma.
La funzione chiude tutti i file descriptor appartenenti al processo; si tenga presente che questo
non comporta il salvataggio dei dati bufferizzati degli stream, (torneremo sulle due interfacce
dei file a partire da cap. 4), fa sı̀ che ogni figlio del processo sia adottato da init (vedi cap. 3),
manda un segnale SIGCHLD al processo padre (vedi sez. 9.2.6) ed infine ritorna lo stato di uscita
specificato in status che può essere raccolto usando la funzione wait (vedi sez. 3.2.4).
La funzione restituisce 0 in caso di successo e −1 in caso di fallimento, errno non viene modificata.
la funzione richiede come argomento l’indirizzo di una opportuna funzione di pulizia da chiamare
all’uscita del programma, che non deve prendere argomenti e non deve ritornare niente (deve
essere cioè definita come void function(void)).
Un’estensione di atexit è la funzione on_exit, che le glibc includono per compatibilità con
SunOS, ma che non è detto sia definita su altri sistemi; il suo prototipo è:
#include <stdlib.h>
void on_exit(void (*function)(int , void *), void *arg)
Registra la funzione function per la chiamata all’uscita dal programma.
La funzione restituisce 0 in caso di successo e −1 in caso di fallimento, errno non viene modificata.
In questo caso la funzione da chiamare all’uscita prende i due argomenti specificati nel
prototipo, dovrà cioè essere definita come void function(int status, void *argp). Il primo
argomento sarà inizializzato allo stato di uscita con cui è stata chiamata exit ed il secondo al
puntatore arg passato come secondo argomento di on_exit. Cosı̀ diventa possibile passare dei
dati alla funzione di chiusura.
22 CAPITOLO 2. L’INTERFACCIA 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ù
volte); poi verranno chiusi tutti gli stream aperti, infine verrà chiamata _exit.
2.1.5 Conclusioni
Data l’importanza dell’argomento è opportuno sottolineare ancora una volta che in un sistema
Unix l’unico modo in cui un programma può essere eseguito dal kernel è attraverso la chiamata
alla system call execve (o attraverso una delle funzioni della famiglia exec che vedremo in
sez. 3.2.5).
Allo stesso modo l’unico modo in cui un programma può concludere volontariamente la sua
esecuzione è attraverso una chiamata alla system call _exit, o esplicitamente, o in maniera
indiretta attraverso l’uso di exit o il ritorno di main.
Uno schema riassuntivo che illustra le modalità con cui si avvia e conclude normalmente un
programma è riportato in fig. 2.1.
_exit
funzione
exit
exit handler
_exit exit
main exit exit handler
chiusura stream
ld-linux.so _exit
exec
kernel
Si ricordi infine che un programma può anche essere interrotto dall’esterno attraverso l’uso
di un segnale (modalità di conclusione non mostrata in fig. 2.1); tratteremo nei dettagli i segnali
e la loro gestione nel capitolo 9.
ogni processo uno spazio virtuale di indirizzamento lineare, in cui gli indirizzi vanno da zero ad
un qualche valore massimo.2
Come accennato in cap. 1 questo spazio di indirizzi è virtuale e non corrisponde all’effettiva
posizione dei dati nella RAM del computer; in genere detto spazio non è neppure continuo
(cioè non tutti gli indirizzi possibili sono utilizzabili, e quelli usabili non sono necessariamente
adiacenti).
Per la gestione da parte del kernel la memoria viene divisa in pagine di dimensione fissa,3
e ciascuna pagina nello spazio di indirizzi virtuale è associata ad un supporto che può essere
una pagina di memoria reale o ad un dispositivo di stoccaggio secondario (come lo spazio disco
riservato alla swap, o i file 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
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
memoria reale che farà da supporto a tutte le pagine di memoria virtuale di tutti i processi che
hanno detta funzione nel loro codice.
La corrispondenza fra le pagine della memoria virtuale di un processo e quelle della memoria
fisica della macchina viene gestita in maniera trasparente dal kernel.5 Poiché in genere la memoria
fisica è solo una piccola frazione della memoria virtuale, è necessario un meccanismo che permetta
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
principali del kernel.
Quando un processo cerca di accedere ad una pagina che non è nella memoria reale, avviene
quello che viene chiamato un page fault; la gestione della memoria genera un’interruzione e
passa il controllo al kernel il quale sospende il processo e si incarica di mettere in RAM la pagina
richiesta (effettuando 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
avviene come se tutte le pagine fossero sempre disponibili in memoria. L’unica differenza avver-
tibile è quella dei tempi di esecuzione, che passano dai pochi nanosecondi necessari per l’accesso
in RAM, a tempi molto più lunghi, dovuti all’intervento del kernel.
Normalmente questo è il prezzo da pagare per avere un multitasking reale, ed in genere il
sistema è molto efficiente in questo lavoro; quando però ci siano esigenze specifiche di prestazioni
è possibile usare delle funzioni che permettono di bloccare il meccanismo della paginazione
e mantenere fisse delle pagine in memoria (vedi sez. 2.2.4). Inoltre per certe applicazioni gli
algoritmi di gestione della memoria
genera quella che viene chiamata una segment violation. Se si tenta cioè di leggere o scrivere
da un indirizzo per il quale non esiste un’associazione della pagina virtuale, il kernel risponde
al relativo page fault mandando un segnale SIGSEGV al processo, che normalmente ne causa la
terminazione immediata.
È 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
può accedere. Solitamente un programma C viene suddiviso nei seguenti segmenti:
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 modifichino le istruzioni.
Viene allocato da exec all’avvio del programma e resta invariato per tutto il tempo
dell’esecuzione.
2. Il segmento dei dati o data segment. Contiene le variabili globali (cioè quelle definite al di
fuori di tutte le funzioni che compongono il programma) e le variabili statiche (cioè quelle
dichiarate con l’attributo static). Di norma è diviso in due parti.
La prima parte è il segmento dei dati inizializzati, che contiene le variabili il cui valore è
stato assegnato esplicitamente. Ad esempio se si definisce:
double pi = 3.14;
questo vettore sarà immagazzinato in questo segmento. Anch’esso viene allocato all’avvio,
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 è fissa.
3. Lo heap. Tecnicamente lo si può considerare l’estensione del segmento dati, a cui di solito è
posto giusto di seguito. È qui che avviene l’allocazione dinamica della memoria; può essere
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
fissa.
4. Il segmento di stack, che contiene quello che viene chiamato stack del programma. Tutte
le volte che si effettua una chiamata ad una funzione è qui che viene salvato l’indirizzo
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 sull’altro; in
questo modo le funzioni possono essere chiamate ricorsivamente. Al ritorno della funzione
lo spazio è automaticamente rilasciato e “ripulito”.7
6
si ricordi che questo vale solo per le variabili che vanno nel segmento dati, e non è affatto vero in generale.
7
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
2.2. I PROCESSI E L’USO DELLA MEMORIA 25
La dimensione di questo segmento aumenta seguendo la crescita dello stack del programma,
ma non viene ridotta quando quest’ultimo si restringe.
environment
0xC0000000
stack
heap
0x08xxxxxx
dati inizializzati
text
0x08000000
Figura 2.2: Disposizione tipica dei segmenti di memoria di un processo.
Una disposizione tipica dei vari segmenti (testo, heap, stack, ecc.) è riportata in fig. 2.2.
Usando il comando size su un programma se ne può stampare le dimensioni dei segmenti di
testo e di dati (inizializzati e BSS); si tenga presente però che il BSS non è mai salvato sul file che
contiene l’eseguibile, dato che viene sempre inizializzato a zero al caricamento del programma.
caricati nello stack dal chiamante da destra a sinistra, e che sia 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
sono inseriti da sinistra a destra ed è compito del chiamato ripulire lo stack ), in genere non ci si deve preoccupare
di questo fintanto che non si mescolano funzioni scritte con linguaggi diversi.
26 CAPITOLO 2. L’INTERFACCIA BASE CON I PROCESSI
Esiste però un terzo tipo di allocazione, l’allocazione dinamica della memoria, che non è
prevista direttamente all’interno del linguaggio C, ma che è necessaria quando il quantitativo di
memoria che serve è determinabile solo durante il corso dell’esecuzione del programma.
Il C non consente di usare variabili allocate dinamicamente, non è possibile cioè definire in fase
di programmazione una variabile le cui dimensioni possano essere modificate durante l’esecuzione
del programma. Per questo le librerie del C forniscono una serie opportuna di funzioni per
eseguire l’allocazione dinamica di memoria (in genere nello heap).
Le variabili il cui contenuto è allocato in questo modo non potranno essere usate direttamente
come le altre (quelle nello stack ), ma l’accesso sarà possibile solo in maniera indiretta, attraverso
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 un’area 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.
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.
void *realloc(void *ptr, size_t size)
Cambia la dimensione del blocco allocato all’indirizzo 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.
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 cor-
rettamente per tutti i tipi di dati; ad esempio sulle macchine a 32 bit in genere è allineato a
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
memoria necessaria al programma indicata da size,8 e siccome i puntatori ritornati sono di tipo
generico non è necessario effettuare un cast per assegnarli a puntatori al tipo di variabile per la
quale si effettua l’allocazione.
La memoria allocata dinamicamente deve essere esplicitamente rilasciata usando free9 una
volta che non sia più necessaria. Questa funzione vuole come argomento un puntatore restitui-
to da una precedente chiamata a una qualunque delle funzioni di allocazione che non sia già
stato liberato da un’altra chiamata a free, in caso contrario il comportamento della funzione è
indefinito.
La funzione realloc si usa invece per cambiare (in genere aumentare) la dimensione di
un’area 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
comporta come malloc)10 ad esempio quando si deve far crescere la dimensione di un vettore. In
8
queste funzioni presentano un comportamento diverso fra le glibc e le uClib quando il valore di size è nullo.
Nel primo caso viene comunque restituito un puntatore valido, anche se non è chiaro a cosa esso possa fare
riferimento, nel secondo caso viene restituito NULL. Il comportamento è analogo con realloc(NULL, 0).
9
le glibc provvedono anche una funzione cfree definita per compatibilità con SunOS, che è deprecata.
10
questo è vero per Linux e l’implementazione secondo lo standard ANSI C, ma non è vero per alcune vecchie
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
funzionalità è totalmente deprecata e non è consentita sotto Linux.
2.2. I PROCESSI E L’USO DELLA MEMORIA 27
questo caso se è disponibile dello spazio adiacente al precedente la funzione lo utilizza, altrimenti
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 un’estensione di quello che gli si è passato in ingresso; per questo si dovrà
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 all’interno del blocco di
dati ridimensionato.
Un errore abbastanza frequente (specie se si ha a che fare con vettori di puntatori) è quello di
chiamare free più di una volta sullo stesso puntatore; per evitare questo problema una soluzione
di ripiego è quella di assegnare sempre a NULL ogni puntatore liberato con free, dato che, quando
l’argomento è un puntatore nullo, free non esegue nessuna operazione.
Le glibc hanno un’implementazione delle funzioni di allocazione che è controllabile dall’utente
attraverso alcune variabili di ambiente (vedi sez. 2.3.3), in particolare diventa possibile tracciare
questo tipo di errori usando la variabile di ambiente MALLOC_CHECK_ che quando viene definita
mette in uso una versione meno efficiente delle funzioni suddette, che però è più tollerante nei
confronti di piccoli errori come quello di chiamate doppie a free. In particolare:
• se la variabile è posta a zero gli errori vengono ignorati;
• se è posta ad 1 viene stampato un avviso sullo standard error (vedi sez. 7.1.3);
• se è posta a 2 viene chiamata abort, che in genere causa l’immediata conclusione del
programma.
Il problema più comune e più difficile da risolvere che si incontra con le funzioni di allocazione
è quando non viene opportunamente liberata la memoria non più utilizzata, quello che in inglese
viene chiamato memory leak, cioè una perdita di memoria.
Un caso tipico che illustra il problema è quello in cui in una subroutine si alloca della me-
moria per uso locale senza liberarla prima di uscire. La memoria resta cosı̀ allocata fino alla
terminazione del processo. Chiamate ripetute alla stessa subroutine continueranno ad effettua-
re altre allocazioni, causando a lungo andare un esaurimento della memoria disponibile (e la
probabile impossibilità di proseguire l’esecuzione del programma).
Il problema è che l’esaurimento della memoria può avvenire in qualunque momento, in cor-
rispondenza ad una qualunque chiamata di malloc che può essere in una sezione del codice che
non ha alcuna relazione con la subroutine che contiene l’errore. Per questo motivo è sempre
molto difficile trovare un memory leak.
In C e C++ il problema è particolarmente sentito. In C++, per mezzo della programmazione
ad oggetti, il problema dei memory leak è notevolmente ridimensionato attraverso l’uso accurato
di appositi oggetti come gli smartpointers. Questo però in genere va a scapito delle prestazioni
dell’applicazione in esecuzione.
Per limitare l’impatto di questi problemi, e semplificare la ricerca di eventuali errori, l’imple-
mentazione delle funzioni di allocazione delle glibc mette a disposizione una serie di funzionalità
che permettono di tracciare le allocazioni e le disallocazioni, e definisce anche una serie di pos-
sibili hook (ganci) che permettono di sostituire alle funzioni di libreria una propria versione
(che può essere più o meno specializzata per il debugging). Esistono varie librerie che forniscono
dei sostituti opportuni delle funzioni di allocazione in grado, senza neanche ricompilare il pro-
gramma,11 di eseguire diagnostiche anche molto complesse riguardo l’allocazione della memoria.
Vedremo alcune delle funzionalità di ausilio presenti nelle glibc in sez. 2.2.5.
Una possibile alternativa all’uso di malloc, per evitare di soffrire dei problemi di memory leak
descritti in precedenza, è di allocare la memoria nel segmento di stack della funzione corrente
11
esempi sono Dmalloc http://dmalloc.com/ di Gray Watson ed Electric Fence di Bruce Perens.
28 CAPITOLO 2. L’INTERFACCIA BASE CON I PROCESSI
invece che nello heap, per farlo si può usare la funzione alloca, la cui sintassi è identica a quella
di malloc; il suo prototipo è:
#include <stdlib.h>
void *alloca(size_t size)
Alloca size byte nello stack.
La funzione restituisce 0 in caso di successo e −1 in caso di fallimento, nel qual caso errno assumerà
il valore ENOMEM.
La funzione è un’interfaccia all’omonima system call ed imposta l’indirizzo finale del segmento
dati di un processo all’indirizzo specificato da end_data_segment. Quest’ultimo deve essere un
12
questo comporta anche il fatto che non è possibile sostituirla con una propria versione o modificarne il
comportamento collegando il proprio programma con un’altra libreria.
13
le due funzioni sono state definite con BSD 4.3, sono marcate obsolete in SUSv2 e non fanno parte delle
librerie standard del C e mentre sono state esplicitamente rimosse dallo standard POSIX/1-2001.
2.2. I PROCESSI E L’USO DELLA MEMORIA 29
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.
Il valore di ritorno della funzione fa riferimento alla versione fornita dalle glibc, in realtà
in Linux la system call corrispondente restituisce come valore di ritorno il nuovo valore della
fine del segmento dati in caso di successo e quello corrente in caso di fallimento, è la funzione
di interfaccia usata dalle glibc che fornisce i valori di ritorno appena descritti, questo può non
accadere se si usano librerie diverse.
Una seconda funzione per la manipolazione diretta delle dimensioni del segmento dati14 è
sbrk, ed il suo prototipo è:
#include <unistd.h>
void *sbrk(ptrdiff_t increment)
Incrementa la dimensione dello spazio dati.
La funzione restituisce il puntatore all’inizio della nuova zona di memoria allocata in caso di
successo e NULL in caso di fallimento, nel qual caso errno assumerà il valore ENOMEM.
Per ottenere informazioni sulle modalità in cui un programma sta usando la memoria virtuale
è disponibile una apposita funzione, mincore, che però non è standardizzata da POSIX e pertanto
non è disponibile su tutte le versioni di kernel unix-like;15 il suo prototipo è:
#include <unistd.h>
#include <sys/mman.h>
int mincore(void *addr, size_t length, unsigned char *vec)
Ritorna lo stato delle pagine di memoria occupate da un processo.
La funzione ritorna 0 in caso di successo e −1 in caso di errore, nel qual caso errno assumerà uno
dei valori seguenti:
ENOMEM o addr + length eccede la dimensione della memoria usata dal processo o l’intervallo
di indirizzi specificato non è mappato.
EINVAL addr non è un multiplo delle dimensioni di una pagina.
EFAULT vec punta ad un indirizzo non valido.
EAGAIN il kernel è temporaneamente non in grado di fornire una risposta.
La funzione permette di ottenere le informazioni sullo stato della mappatura della memoria
per il processo chiamante, specificando l’intervallo da esaminare con l’indirizzo iniziale (indicato
con l’argomento addr) e la lunghezza (indicata con l’argomento length). L’indirizzo iniziale deve
essere un multiplo delle dimensioni di una pagina, mentre la lunghezza può essere qualunque,
fintanto che si resta nello spazio di indirizzi del processo,16 ma il risultato verrà comunque fornito
per l’intervallo compreso fino al multiplo successivo.
I risultati della funzione vengono forniti nel vettore puntato da vec, che deve essere allocato
preventivamente e deve essere di dimensione sufficiente a contenere tanti byte quante sono le
pagine contenute nell’intervallo di indirizzi specificato.17 Al ritorno della funzione il bit meno
significativo di ciascun byte del vettore sarà acceso se la pagina di memoria corrispondente è
al momento residente in memoria, o cancellato altrimenti. Il comportamento sugli altri bit è
indefinito, essendo questi al momento riservati per usi futuri. Per questo motivo in genere è
comunque opportuno inizializzare a zero il contenuto del vettore, cosı̀ che le pagine attualmente
residenti in memoria saranno indicata da un valore non nullo del byte corrispondente.
Dato che lo stato della memoria di un processo può cambiare continuamente, il risultato
di mincore è assolutamente provvisorio e lo stato delle pagine potrebbe essere già cambiato al
ritorno stesso della funzione, a meno che, come vedremo ora, non si sia attivato il meccanismo
che forza il mantenimento di una pagina sulla memoria.
Il meccanismo che previene la paginazione di parte della memoria virtuale di un processo è
chiamato memory locking (o blocco della memoria). Il blocco è sempre associato alle pagine della
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
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.
Il memory lock persiste fintanto che il processo che detiene la memoria bloccata non la
sblocca. Chiaramente la terminazione del processo comporta anche la fine dell’uso della sua
memoria virtuale, e quindi anche di tutti i suoi memory lock. Infine i memory lock non sono
ereditati dai processi figli,18 e vengono automaticamente rimossi se si pone in esecuzione un altro
programma con exec (vedi sez. 3.2.5).
15
nel caso di Linux devono essere comunque definite le macro _BSD_SOURCE e _SVID_SOURCE.
16
in caso contrario si avrà un errore di ENOMEM; fino al kernel 2.6.11 in questo caso veniva invece restituito
EINVAL, in considerazione che il caso più comune in cui si verifica questo errore è quando si usa per sbaglio un
valore negativo di length, che nel caso verrebbe interpretato come un intero positivo di grandi dimensioni.
17
la dimensione cioè deve essere almeno pari a (length+PAGE_SIZE-1)/PAGE_SIZE.
18
ma siccome Linux usa il copy on write (vedi sez. 3.2.2) gli indirizzi virtuali del figlio sono mantenuti sullo
stesso segmento di RAM del padre, quindi fintanto che un figlio non scrive su un segmento, può usufruire del
memory lock del padre.
2.2. I PROCESSI E L’USO DELLA MEMORIA 31
#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:
ENOMEM alcuni indirizzi dell’intervallo specificato non corrispondono allo spazio di indirizzi del
processo o si è ecceduto il numero massimo consentito di pagine bloccate.
EINVAL len non è un valore positivo.
EPERM con un kernel successivo al 2.6.9 il processo non è privilegiato e si un limite nullo per
RLIMIT_MEMLOCK.
e, per mlock, anche EPERM quando il processo non ha i privilegi richiesti per l’operazione.
#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
l’uso di munlockall senza la capability CAP_IPC_LOCK genera un errore di EPERM.
MCL_CURRENT blocca tutte le pagine correntemente mappate nello spazio di indirizzi del pro-
cesso.
MCL_FUTURE blocca tutte le pagine che verranno mappate nello spazio di indirizzi del processo.
Con mlockall si possono bloccare tutte le pagine mappate nello spazio di indirizzi del pro-
cesso, sia che comprendano il segmento di testo, di dati, lo stack, lo heap e pure le funzioni di
libreria chiamate, i file mappati in memoria, i dati del kernel mappati in user space, la memoria
condivisa. L’uso dei flag permette di selezionare con maggior finezza 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 sufficiente prima dell’ingresso, per scongiurare l’occorrenza 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
durante l’esecuzione 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à
sufficientemente ampia di variabili automatiche, in modo che esse vengano mappate in RAM dallo
stack, dopo di che, per essere sicuri che esse siano state effettivamente portate in memoria, ci si
scrive sopra.
#include <malloc.h>
void *valloc(size_t size)
Alloca un blocco di memoria allineato alla dimensione di una pagina di memoria.
void *memalign(size_t boundary, size_t size)
Alloca un blocco di memoria allineato ad un multiplo di boundary.
Le funzioni restituiscono il puntatore al buffer di memoria allocata, che per memalign sarà
un multiplo di boundary mentre per valloc un multiplo della dimensione di una pagina di
memoria. Nel caso della versione fornita dalle glibc la memoria allocata con queste funzioni deve
essere liberata con free, cosa che non è detto accada con altre implementazioni.
2.2. I PROCESSI E L’USO DELLA MEMORIA 33
Nessuna delle due funzioni ha una chiara standardizzazione (nessuna delle due compare in
POSIX.1), ed inoltre ci sono indicazioni discordi sui file che ne contengono la definizione;22 per
questo motivo il loro uso è sconsigliato, essendo state sostituite dalla nuova posix_memalign,
che è stata standardizzata in POSIX.1d; il suo prototipo è:
#include <stdlib.h>
posix_memalign(void **memptr, size_t alignment, size_t size)
Alloca un buffer di memoria allineato ad un multiplo di alignment.
La funzione restituisce 0 in caso di successo e NULL in caso di fallimento, o uno dei due codici di
errore ENOMEM o EINVAL; errno non viene impostata.
La funzione restituisce 0 in caso di successo e −1 in caso di fallimento; errno non viene impostata.
Se come argomento di mcheck si passa NULL verrà utilizzata una funzione predefinita che
stampa un messaggio di errore ed invoca la funzione abort (vedi sez. 9.3.4), altrimenti si dovrà
create una funzione personalizzata che verrà eseguita ricevendo un unico argomento di tipo
mcheck_status,26 un tipo enumerato che può assumere soltanto i valori di tab. 2.1.
Valore Significato
MCHECK_OK riportato (a mprobe) se nessuna inconsistenza è
presente.
MCHECK_DISABLED riportato (a mprobe) se si è chiamata mcheck dopo
aver già usato malloc.
MCHECK_HEAD i dati immediatamente precedenti il buffer sono
stati modificati, avviene in genere quando si de-
crementa eccessivamente il valore di un puntatore
scrivendo poi prima dell’inizio del buffer.
MCHECK_TAIL i dati immediatamente seguenti il buffer sono stati
modificati, succede quando si va scrivere oltre la
dimensione corretta del buffer.
MCHECK_FREE il buffer è già stato disallocato.
Tabella 2.1: Valori dello stato dell’allocazione di memoria ottenibili dalla funzione di terminazione installata con
mcheck.
Una volta che si sia chiamata mcheck con successo si può anche controllare esplicitamente lo
stato delle allocazioni (senza aspettare un errore nelle relative funzioni) utilizzando la funzione
mprobe, il cui prototipo è:
#include <mcheck.h>
enum mcheck_status mprobe(ptr)
Esegue un controllo di consistenza delle allocazioni.
per individuare le parole che la compongono, ciascuna delle quali potrà essere considerata un
argomento o un’opzione. Di norma per individuare le parole che andranno a costituire la lista
degli argomenti viene usato come carattere di separazione lo spazio o il tabulatore, ma la cosa
dipende ovviamente dalle modalità con cui si effettua la scansione.
Figura 2.3: Esempio dei valori di argv e argc generati nella scansione di una riga di comando.
Indipendentemente da come viene eseguita, il risultato della scansione deve essere la co-
struzione del vettore di puntatori argv in cui si devono inserire in successione i puntatori alle
stringhe costituenti i vari argomenti ed opzioni, e della variabile argc che deve essere inizializzata
al numero di stringhe passate. Nel caso della shell questo comporta che il primo argomento sia
sempre il nome del programma; un esempio di questo meccanismo è mostrato in fig. 2.3.
#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 l’opzione, ’:’ se manca un parametro all’opzione, ’?’ se l’opzione
è sconosciuta, e −1 se non esistono altre opzioni.
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 effettua la scansione della lista degli
argomenti ricercando ogni stringa che comincia con - e ritorna ogni volta che trova un’opzione
valida.
La stringa optstring indica quali sono le opzioni riconosciute ed è costituita da tutti i
caratteri usati per identificare le singole opzioni, se l’opzione ha un parametro al carattere deve
essere fatto seguire un segno di due punti ’:’; nel caso di fig. 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 all’interno
di un ciclo, fintanto che essa non ritorna il valore −1 che indica che non ci sono più opzioni. Nel
caso si incontri un’opzione non dichiarata in optstring viene ritornato il carattere ’?’ mentre
se un’opzione che lo richiede non è seguita da un parametro viene ritornato il carattere ’:’,
36 CAPITOLO 2. L’INTERFACCIA BASE CON I PROCESSI
infine 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 un’opzione essa ritorna il valore numerico del carattere, in questo
modo si possono eseguire azioni specifiche usando uno switch; getopt inoltre inizializza alcune
variabili globali:
In fig. 2.4 è mostrata la sezione del programma ForkTest.c (che useremo nel prossimo
capitolo per effettuare dei test sulla creazione dei processi) deputata alla decodifica delle opzioni
a riga di comando.
Si può notare che si è anzitutto (1) disabilitata la stampa di messaggi di errore per opzioni
non riconosciute, per poi passare al ciclo per la verifica delle opzioni (2-27); per ciascuna delle
opzioni possibili si è poi provveduto ad un’azione opportuna, ad esempio per le tre opzioni che
prevedono un parametro si è effettuata la decodifica del medesimo (il cui indirizzo è contenuto
nella variabile optarg) avvalorando la relativa variabile (12-14, 15-17 e 18-20). Completato il
ciclo troveremo in optind l’indice in argv[] del primo degli argomenti rimanenti nella linea di
comando.
2.3. ARGOMENTI, AMBIENTE ED ALTRE PROPRIETÀ DI UN PROCESSO 37
Normalmente getopt compie una permutazione degli elementi di argv cosicché alla fine
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
’+’ (o è impostata la variabile di ambiente POSIXLY_CORRECT) la scansione viene fermata non
appena si incontra un elemento che non è un’opzione.
L’ultima modalità, usata quando un programma può gestire la mescolanza fra opzioni e
argomenti, ma se li aspetta in un ordine definito, si attiva quando optstring inizia con il
carattere ’-’. In questo caso ogni elemento che non è un’opzione viene considerato comunque
un’opzione e associato ad un valore di ritorno pari ad 1, questo permette di identificare gli
elementi che non sono opzioni, ma non effettua il riordinamento del vettore argv.
Per convenzione le stringhe che definiscono l’ambiente sono tutte del tipo nome=valore ed
in questa forma che le funzioni di gestione che vedremo a breve se le aspettano, se pertanto
si dovesse costruire manualmente un ambiente si abbia cura di rispettare questa convenzione.
Inoltre alcune variabili, come quelle elencate in fig. 2.5, sono definite dal sistema per essere usate
da diversi programmi e funzioni: per queste c’è l’ulteriore convenzione di usare nomi espressi in
caratteri maiuscoli.27
Il kernel non usa mai queste variabili, il loro uso e la loro interpretazione è riservata alle
applicazioni e ad alcune funzioni di libreria; in genere esse costituiscono un modo comodo per
27
ma si tratta solo di una convenzione, niente vieta di usare caratteri minuscoli.
38 CAPITOLO 2. L’INTERFACCIA BASE CON I PROCESSI
definire un comportamento specifico senza dover ricorrere all’uso di opzioni a linea di comando o
di file di configurazione. É 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 indicare la lista
delle directory in cui effettuare la ricerca dei comandi o PS1 per impostare il proprio prompt. Al-
cune di esse, come HOME, USER, ecc. sono invece definite al login (per i dettagli si veda sez. 10.1.4),
ed in genere è cura della propria distribuzione definire le opportune variabili di ambiente in uno
script di avvio. Alcune servono poi come riferimento generico per molti programmi, come EDITOR
che indica l’editor preferito da invocare in caso di necessità. Una in particolare, LANG, serve a
controllare la localizzazione del programma (su cui torneremo in sez. 2.3.4) per adattarlo alla
lingua ed alle convezioni dei vari paesi.
Gli standard POSIX e XPG3 definiscono alcune di queste variabili (le più comuni), come
riportato in tab. 2.2. GNU/Linux le supporta tutte e ne definisce anche altre, in particolare
poi alcune funzioni di libreria prevedono la presenza di specifiche variabili di ambiente che ne
modificano il comportamento, come quelle usate per indicare una localizzazione e quelle per
indicare un fuso orario; una lista più completa che comprende queste ed ulteriori variabili si può
ottenere con il comando man 7 environ.
Variabile POSIX XPG3 Linux Descrizione
USER • • • Nome utente
LOGNAME • • • Nome di login
HOME • • • Directory base dell’utente
LANG • • • Localizzazione
PATH • • • Elenco delle directory dei programmi
PWD • • • Directory corrente
SHELL • • • Shell in uso
TERM • • • Tipo di terminale
PAGER • • • Programma per vedere i testi
EDITOR • • • Editor preferito
BROWSER • • • Browser preferito
TMPDIR • • • Directory dei file temporanei
Tabella 2.2: Esempi delle variabili di ambiente più comuni definite da vari standard.
Lo standard ANSI C prevede l’esistenza di un ambiente, e pur non entrando nelle specifiche
di come sono strutturati i contenuti, definisce la funzione getenv che permette di ottenere i
valori delle variabili di ambiente; il suo prototipo è:
#include <stdlib.h>
char *getenv(const char *name)
Esamina l’ambiente del processo cercando una stringa che corrisponda a quella specificata
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 è l’unica definita dallo standard ANSI C, nell’evolu-
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.3.
In Linux28 sono definite tutte le funzioni elencate in tab. 2.3. La prima, getenv, l’abbiamo
appena esaminata; delle restanti le prime due, putenv e setenv, servono per assegnare nuove
variabili di ambiente, i loro prototipi sono i seguenti:
28
in realtà nelle libc4 e libc5 sono definite solo le prime quattro, clearenv è stata introdotta con le glibc 2.0.
2.3. ARGOMENTI, AMBIENTE ED ALTRE PROPRIETÀ DI UN PROCESSO 39
#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 all’ambiente.
Entrambe le funzioni ritornano 0 in caso di successo e −1 per un errore, che è sempre ENOMEM.
La terza funzione della lista, unsetenv, serve a cancellare una variabile dall’ambiente, il suo
prototipo è:
#include <stdlib.h>
void unsetenv(const char *name)
Rimuove la variabile di ambiente name.
la funzione elimina ogni occorrenza della variabile specificata; se la variabile non esiste non
succede nulla. Non è prevista (dato che la funzione è void) nessuna segnalazione di errore.
Per modificare o aggiungere una variabile di ambiente si possono usare sia setenv che putenv.
La prima permette di specificare separatamente nome e valore della variabile di ambiente, inoltre
il valore di overwrite specifica il comportamento della funzione nel caso la variabile esista già,
sovrascrivendola se diverso da zero, lasciandola immutata se uguale a zero.
La seconda funzione prende come argomento una stringa analoga a quella restituita da ge-
tenv, e sempre nella forma NOME=valore. Se la variabile specificata non esiste la stringa sarà
aggiunta all’ambiente, se invece esiste il suo valore sarà impostato a quello specificato da string.
Si tenga presente che, seguendo lo standard SUSv2, le glibc successive alla versione 2.1.2
aggiungono string alla lista delle variabili di ambiente;29 pertanto ogni cambiamento alla stringa
in questione si riflette automaticamente sull’ambiente, 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 infine
presente che se si passa a putenv solo il nome di una variabile (cioè string è nella forma NAME
e non contiene un carattere ’=’) allora questa viene cancellata dall’ambiente.
Infine quando chiamata a putenv comporta la necessità di creare una nuova versione del
vettore environ questo sarà allocato automaticamente, ma la versione corrente sarà deallocata
solo se anch’essa è risultante da un’allocazione fatta in precedenza da un’altra putenv. Questo
avviene perché il vettore delle variabili di ambiente iniziale, creato dalla chiamata ad exec (vedi
sez. 3.2.5) è piazzato nella memoria al di sopra dello stack, (vedi fig. 2.2) e non nello heap e quindi
non può essere deallocato. Inoltre la memoria associata alle variabili di ambiente eliminate non
viene liberata.
L’ultima funzione per la gestione dell’ambiente è clearenv, che viene usata per cancellare
completamente tutto l’ambiente; il suo prototipo è:
29
il comportamento è lo stesso delle vecchie libc4 e libc5; nelle glibc, dalla versione 2.0 alla 2.1.1, veniva invece
fatta una copia, seguendo il comportamento di BSD4.4; dato che questo può dar luogo a perdite di memoria e
non rispetta lo standard. Il comportamento è stato modificato a partire dalle 2.1.2, eliminando anche, sempre in
conformità a SUSv2, l’attributo const dal prototipo.
40 CAPITOLO 2. L’INTERFACCIA BASE CON I PROCESSI
#include <stdlib.h>
int clearenv(void)
Cancella tutto l’ambiente.
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
non controllati. In tal caso si provvede alla cancellazione di tutto l’ambiente per costruirne una
versione “sicura” da zero.
2.3.4 La localizzazione
Abbiamo accennato in sez. 2.3.3 come la variabile di ambiente LANG sia usata per indicare
ai processi il valore della cosiddetta localizzazione. Si tratta di una funzionalità fornita dalle
librerie di sistema30 che consente di gestire in maniera automatica sia la lingua in cui vengono
stampati i vari messaggi (come i messaggi associati agli errori che vedremo in sez. 8.5.2) che le
convenzioni usate nei vari paesi per una serie di aspetti come il formato dell’ora, quello delle
date, gli ordinamenti alfabetici, le espressioni della valute, ecc.
La localizzazione di un programma si può selezionare con la
In realtà perché un programma sia effettivamente localizzato non è sufficiente
Talvolta però è necessario che la funzione possa restituire indietro alla funzione chiamante un
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
di questa modalità nelle funzioni che gestiscono i socket (in sez. 16.2), in cui, per permettere al
kernel di restituire informazioni sulle dimensioni delle strutture degli indirizzi utilizzate, viene
usato questo meccanismo.
• Dichiarare la funzione come variadic usando un prototipo che contenga una ellipsis.
• Definire 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 specificando prima gli argomenti fissi, ed a seguire quelli addizionali.
Lo standard ISO C prevede che una variadic function abbia sempre almeno un argomento
fisso; prima di effettuare la dichiarazione deve essere incluso l’apposito header file stdarg.h; un
esempio di dichiarazione è il prototipo della funzione execl che vedremo in sez. 3.2.5:
int execl ( const char * path , const char * arg , ...);
in questo caso la funzione prende due argomenti fissi 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 l’ultimo degli argomenti fissi sia di tipo self-
promoting 31 il che esclude vettori, puntatori a funzioni e interi di tipo char o short (con segno
o meno). Una restrizione ulteriore di alcuni compilatori è di non dichiarare l’ultimo argomento
fisso come register.
Una volta dichiarata la funzione il secondo passo è accedere ai vari argomenti quando la si
va a definire. Gli argomenti fissi infatti hanno un loro nome, ma quelli variabili vengono indicati
in maniera generica dalla ellipsis.
L’unica modalità in cui essi possono essere recuperati è pertanto quella sequenziale; essi
verranno estratti dallo stack secondo l’ordine in cui sono stati scritti. Per fare questo in stdarg.h
sono definite delle apposite macro; la procedura da seguire è la seguente:
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.
3. Dichiarare la conclusione dell’estrazione degli argomenti invocando la macro va_end.
momento ed i restanti argomenti saranno ignorati; se invece si richiedono più argomenti di quelli
forniti si otterranno dei valori indefiniti. Nel caso del gcc l’uso di va_end è inutile, ma si consiglia
di usarla ugualmente per compatibilità. Le definizioni delle macro citate 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 l’ultimo
degli argomenti fissi.
type va_arg(va_list ap, type)
Restituisce il valore del successivo argomento opzionale, modificando opportunamente ap;
la macro richiede che si specifichi il tipo dell’argomento attraverso il parametro type che
deve essere il nome del tipo dell’argomento in questione. Il tipo deve essere self-promoting.
void va_end(va_list ap)
Conclude l’uso di ap.
In generale si possono avere più puntatori alla lista degli argomenti, ciascuno andrà inizia-
lizzato con va_start e letto con va_arg e ciascuno potrà scandire la lista degli argomenti per
conto suo. Dopo l’uso di va_end la variabile ap diventa indefinita e successive chiamate a va_arg
non funzioneranno. Si avranno risultati indefiniti anche chiamando va_arg specificando un tipo
che non corrisponde a quello dell’argomento.
Un altro limite delle macro è che i passi 1) e 3) devono essere eseguiti nel corpo principale
della funzione, il passo 2) invece può essere eseguito anche in una subroutine passandole il
puntatore alla lista di argomenti; in questo caso però si richiede che al ritorno della funzione il
puntatore non venga più usato (lo standard richiederebbe la chiamata esplicita di va_end), dato
che il valore di ap risulterebbe indefinito.
Esistono dei casi in cui è necessario eseguire più volte la scansione degli argomenti e poter
memorizzare una posizione durante la stessa. In questo caso sembrerebbe naturale copiarsi il
puntatore alla lista degli argomenti con una semplice assegnazione. Dato che una delle realiz-
zazioni più comuni di va_list è quella di un puntatore nello stack all’indirizzo dove sono stati
salvati gli argomenti, è assolutamente normale pensare di poter effettuare questa operazione.
In generale però possono esistere anche realizzazioni diverse, per questo motivo va_list
è definito come tipo opaco e non può essere assegnato direttamente ad un’altra variabile dello
stesso tipo. Per risolvere questo problema lo standard ISO C9932 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 l’attuale 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 corrispon-
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 definita come tale, non prevede nulla di particolare; l’invocazione è identica alle
altre, con gli argomenti, sia quelli fissi che quelli opzionali, separati da virgole. Quello che però
è necessario tenere presente è come verranno convertiti gli argomenti variabili.
In Linux gli argomenti dello stesso tipo sono passati allo stesso modo, sia che siano fissi sia
che siano opzionali (alcuni sistemi trattano diversamente gli opzionali), ma dato che il prototipo
non può specificare il tipo degli argomenti opzionali, questi verranno sempre promossi, pertanto
nella ricezione dei medesimi occorrerà tenerne conto (ad esempio un char verrà visto da va_arg
come int).
Uno dei problemi che si devono affrontare con le funzioni con un numero variabile di argo-
menti è che non esiste un modo generico che permetta di stabilire quanti sono gli argomenti
passati effettivamente in una chiamata.
32
alcuni sistemi che non hanno questa macro provvedono al suo posto __va_copy che era il nome proposto in
una bozza dello standard.
2.4. PROBLEMATICHE DI PROGRAMMAZIONE GENERICA 43
Esistono varie modalità per affrontare questo problema; una delle più immediate è quella di
specificare il numero degli argomenti opzionali come uno degli argomenti fissi. Una variazione di
questo metodo è l’uso di un argomento per specificare anche il tipo degli argomenti (come fa la
stringa di formato per printf).
Una modalità diversa, che può essere applicata solo quando il tipo degli argomenti lo rende
possibile, è quella che prevede di usare un valore speciale come ultimo argomento (come fa ad
esempio execl che usa un puntatore NULL per indicare la fine della lista degli argomenti).
La funzione ritorna zero quando è chiamata direttamente e un valore diverso da zero quando
ritorna da una chiamata di longjmp che usa il contesto salvato in precedenza.
33
a meno che, come precisa [5], alla chiusura di ciascuna fase non siano associate operazioni di pulizia specifiche
(come deallocazioni, chiusure di file, ecc.), che non potrebbero essere eseguite con un salto non-locale.
44 CAPITOLO 2. L’INTERFACCIA BASE CON I PROCESSI
Quando si esegue la funzione il contesto corrente dello stack viene salvato nell’argomento
env, una variabile di tipo jmp_buf34 che deve essere stata definita in precedenza. In genere le
variabili di tipo jmp_buf vengono definite 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 un’altra
parte del programma che ripristina lo stack effettuando 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)
per il processo.
Come accennato per effettuare un salto non-locale ad un punto precedentemente stabilito
con setjmp si usa la funzione longjmp; il suo prototipo è:
#include <setjmp.h>
void longjmp(jmp_buf env, int val)
Ripristina il contesto dello stack.
Quello che succede infatti è che i valori delle variabili che sono tenute in memoria manterranno
il valore avuto al momento della chiamata di longjmp, mentre quelli tenuti nei registri del
processore (che nella chiamata ad un’altra 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 l’ottimizzazione che porta le variabili nei
registri dichiarandole tutte come volatile.36
2.4.5 La endianess
Uno dei problemi di programmazione che può dar luogo ad effetti imprevisti è quello relativo
alla cosiddetta endianess. Questa è una caratteristica generale dell’architettura hardware di un
computer che dipende dal fatto che la rappresentazione di un numero binario può essere fatta
in due modi, chiamati 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à cablati sui bus interni del computer).
Figura 2.6: Schema della disposizione dei dati in memoria a seconda della endianess.
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 fig. 2.6 i singoli bit possono essere disposti in
memoria in due modi: a partire dal più significativo o a partire dal meno significativo. Cosı̀
nel primo caso si troverà il byte che contiene i bit più significativi all’indirizzo menzionato e il
byte con i bit meno significativi nell’indirizzo successivo; questo ordinamento è detto big endian,
dato che si trova per prima la parte più grande. Il caso opposto, in cui si parte dal bit meno
significativo è detto per lo stesso motivo little endian.
Si può allora verificare quale tipo di endianess usa il proprio computer con un programma
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,
allora se lo eseguiamo su un normale PC compatibile, che è little endian otterremo qualcosa del
tipo:
mentre su un vecchio Macintosh con PowerPC, che è big endian avremo qualcosa del tipo:
36
la direttiva volatile informa il compilatore che la variabile che è dichiarata può essere modificata, durante
l’esecuzione del nostro, da altri programmi. Per questo motivo occorre dire al compilatore che non deve essere
mai utilizzata l’ottimizzazione per cui quanto opportuno essa viene mantenuta in un registro, poiché in questo
modo si perderebbero le eventuali modifiche fatte dagli altri programmi (che avvengono solo in una copia posta
in memoria).
46 CAPITOLO 2. L’INTERFACCIA BASE CON I PROCESSI
piccardi@anarres:~/gapil/sources$ ./endtest
Using value ABCDEF01
val[0]=AB
val[1]=CD
val[2]=EF
val[3]= 1
Figura 2.7: La funzione endian, usata per controllare il tipo di architettura della macchina.
Per controllare quale tipo di ordinamento si ha sul proprio computer si è scritta una piccola
funzione di controllo, il cui codice è riportato fig. 2.7, che restituisce un valore nullo (falso) se
l’architettura è big endian ed uno non nullo (vero) se l’architettura è little endian.
Come si vede la funzione è molto semplice, e si limita, una volta assegnato (9) un valore di
test pari a 0xABCD ad una variabile di tipo short (cioè a 16 bit), a ricostruirne una copia byte a
byte. Per questo prima (10) si definisce il puntatore ptr per accedere al contenuto della prima
variabile, ed infine calcola (11) il valore della seconda assumendo che il primo byte sia quello
meno significativo (cioè, per quanto visto in fig. 2.6, che sia little endian). Infine la funzione
restituisce (12) il valore del confronto delle due variabili.
37
su architettura PowerPC è possibile cambiarlo, si veda sez. 3.5.2.
Capitolo 3
Come accennato nell’introduzione in un sistema Unix tutte le operazioni vengono svolte tramite
opportuni processi. In sostanza questi ultimi vengono a costituire l’unità base per l’allocazione
e l’uso delle risorse del sistema.
Nel precedente capitolo abbiamo esaminato il funzionamento di un processo come unità a
se stante, in questo esamineremo il funzionamento dei processi all’interno del sistema. Saranno
cioè affrontati i dettagli della creazione e della terminazione dei processi, della gestione dei
loro attributi e privilegi, e di tutte le funzioni a questo connesse. Infine nella sezione finale
introdurremo alcune problematiche generiche della programmazione in ambiente multitasking.
3.1 Introduzione
Inizieremo con un’introduzione generale ai concetti che stanno alla base della gestione dei processi
in un sistema unix-like. Introdurremo in questa sezione l’architettura della gestione dei processi
e le sue principali caratteristiche, dando una panoramica sull’uso delle principali funzioni di
gestione.
47
48 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
lanciare al posto di init qualunque altro programma, e in casi di emergenza (ad esempio se il
file di init si fosse corrotto) è ad esempio possibile lanciare una shell al suo posto, passando la
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: L’albero 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
figli1 si possono classificare i processi con la relazione padre/figlio in un’organizzazione gerarchica
ad albero, in maniera analoga a come i file sono organizzati in un albero di directory (si veda
sez. 4.1.1); in fig. 3.1 si è mostrato il risultato del comando pstree che permette di visualizzare
questa struttura, alla cui base c’è init che è progenitore di tutti gli altri processi.
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 nell’header file linux/sched.h, ed uno schema semplificato, che
riporta la struttura delle principali informazioni contenute nella task_struct (che in seguito
incontreremo a più riprese), è mostrato in fig. 3.2.
Come accennato in sez. 1.1 è lo scheduler che decide quale processo mettere in esecuzione;
esso viene eseguito ad ogni system call ed ad ogni interrupt,2 ma può essere anche attivato
1
in realtà questo non è del tutto vero, in Linux ci sono alcuni processi speciali che pur comparendo come figli
di init, o con pid successivi, sono in realtà generati direttamente dal kernel, (come keventd, kswapd, ecc.).
2
più in una serie di altre occasioni.
3.1. INTRODUZIONE 49
Figura 3.2: Schema semplificato dell’architettura delle strutture usate dal kernel nella gestione dei processi.
esplicitamente. Il timer di sistema provvede comunque a che esso sia invocato periodicamente;
generando un interrupt periodico secondo la frequenza specificata dalla costante HZ,3 definita in
asm/param.h, ed il cui valore è espresso in Hertz.4
Ogni volta che viene eseguito, lo scheduler effettua il calcolo delle priorità dei vari processi
attivi (torneremo su questo in sez. 3.4) e stabilisce quale di essi debba essere posto in esecuzione
fino alla successiva invocazione.
del processo però termina completamente solo quando la notifica della sua conclusione viene
ricevuta dal processo padre, a quel punto tutte le risorse allocate nel sistema ad esso associate
vengono rilasciate.
Avere due processi che eseguono esattamente lo stesso codice non è molto utile, normalmente
si genera un secondo processo per affidargli l’esecuzione di un compito specifico (ad esempio gesti-
re una connessione dopo che questa è stata stabilita), o fargli eseguire (come fa la shell) un altro
programma. Per quest’ultimo caso si usa la seconda funzione fondamentale per programmazione
coi processi che è la exec.
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 quest’ultimo all’immagine corrente; questo fa sı̀ che l’immagine precedente venga
completamente cancellata. Questo significa che quando il nuovo programma termina, anche il
processo termina, e non si può tornare alla precedente immagine.
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 figlio) mentre
la seconda non ritorna mai (in quanto con essa viene eseguito un altro programma).
#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.
5
in genere viene assegnato il numero successivo a quello usato per l’ultimo processo creato, a meno che questo
numero non sia già utilizzato per un altro pid, pgid o sid (vedi sez. 10.1.2).
6
questi valori, fino al kernel 2.4.x, sono definiti 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 modificato; il valore massimo è impostabile attraverso il file /proc/sys/kernel/pid_max e di default
vale 32768.
3.2. LE FUNZIONI DI BASE 51
esempi dell’uso di queste funzioni sono riportati in fig. 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 l’unicità: ad esempio
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.
Tutti i processi figli dello stesso processo padre sono detti sibling, questa è una delle relazioni
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 esamine-
remo gli altri identificativi associati ad un processo e le varie relazioni fra processi utilizzate per
definire 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 identificatori che vengono usati per il controllo
di accesso. Questi servono per determinare se un processo può eseguire o meno le operazioni
richieste, a seconda dei privilegi e dell’identità di chi lo ha posto in esecuzione; l’argomento è
complesso e sarà affrontato in dettaglio in sez. 3.3.
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void)
Crea un nuovo processo.
In caso di successo restituisce il pid del figlio al padre e zero al figlio; ritorna -1 al padre (senza
creare il figlio) in caso di errore; errno può assumere i valori:
EAGAIN non ci sono risorse sufficienti per creare un altro processo (per allocare la tabella delle
pagine e le strutture del task) o si è esaurito il numero di processi disponibili.
ENOMEM non è stato possibile allocare la memoria per le strutture necessarie al kernel per creare
il nuovo processo.
Dopo il successo dell’esecuzione di una fork sia il processo padre che il processo figlio con-
tinuano ad essere eseguiti normalmente a partire dall’istruzione successiva alla fork; il processo
figlio è però una copia del padre, e riceve una copia dei segmenti di testo, stack e dati (ve-
di sez. 2.2.2), ed esegue esattamente lo stesso codice del padre. Si tenga presente però che la
memoria è copiata, non condivisa, pertanto padre e figlio vedono variabili diverse.
Per quanto riguarda la gestione della memoria, in generale il segmento di testo, che è identico
per i due processi, è condiviso e tenuto in read-only per il padre e per i figli. Per gli altri segmenti
Linux utilizza la tecnica del copy on write; questa tecnica comporta che una pagina di memoria
viene effettivamente copiata per il nuovo processo solo quando ci viene effettuata sopra una
scrittura (e si ha quindi una reale differenza fra padre e figlio). In questo modo si rende molto
più efficiente il meccanismo della creazione di un nuovo processo, non essendo più necessaria la
copia di tutto lo spazio degli indirizzi virtuali del padre, ma solo delle pagine di memoria che
sono state modificate, e solo al momento della modifica stessa.
7
in realtà oggi la system call usata da Linux per creare nuovi processi è clone (vedi 3.5.1), anche perché a
partire dalle glibc 2.3.3 non viene più usata la system call originale, ma la stessa fork viene implementata tramite
clone, cosa che consente una migliore interazione coi thread.
8
oggi questa rilevanza, con la diffusione dell’uso dei thread che tratteremo al cap. 13, è in parte minore, ma
fork resta comunque la funzione principale per la creazione di processi.
52 CAPITOLO 3. LA GESTIONE DEI PROCESSI
La differenza che si ha nei due processi è che nel processo padre il valore di ritorno della
funzione fork è il pid del processo figlio, mentre nel figlio è zero; in questo modo il programma
può identificare se viene eseguito dal padre o dal figlio. Si noti come la funzione fork ritorni
due volte: una nel padre e una nel figlio.
La scelta di questi valori di ritorno non è casuale, un processo infatti può avere più figli, ed il
valore di ritorno di fork è l’unico modo che gli permette di identificare quello appena creato; al
contrario un figlio ha sempre un solo padre (il cui pid può sempre essere ottenuto con getppid,
vedi sez. 3.2.1) per cui si usa il valore nullo, che non è il pid di nessun processo.
Normalmente la chiamata a fork può fallire solo per due ragioni, o ci sono già troppi processi
nel sistema (il che di solito è sintomo che qualcos’altro non sta andando per il verso giusto) o
3.2. LE FUNZIONI DI BASE 53
si è ecceduto il limite sul numero totale di processi permessi all’utente (vedi sez. 8.3.2, ed in
particolare tab. 8.12).
L’uso di fork avviene secondo due modalità principali; la prima è quella in cui all’interno
di un programma si creano processi figli cui viene affidata l’esecuzione di una certa sezione di
codice, mentre il processo padre ne esegue un’altra. È 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
parte dei programmi client, per ciascuna delle quali pone in esecuzione un figlio che è incaricato
di fornire il servizio.
La seconda modalità è quella in cui il processo vuole eseguire un altro programma; questo è
ad esempio il caso della shell. In questo caso il processo crea un figlio la cui unica operazione è
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 un’unica operazione che viene chiamata spawn. Nei
sistemi unix-like è stato scelto di mantenere questa separazione, dato che, come per la prima
modalità d’uso, esistono numerosi scenari in cui si può usare una fork senza aver bisogno di
eseguire una exec. Inoltre, anche nel caso della seconda modalità d’uso, avere le due funzioni
separate permette al figlio di cambiare gli attributi del processo (maschera dei segnali, redirezione
dell’output, identificatori) prima della exec, rendendo cosı̀ relativamente facile intervenire sulle
le modalità di esecuzione del nuovo programma.
In fig. 3.3 è riportato il corpo del codice del programma di esempio forktest, che permette
di illustrare molte caratteristiche dell’uso della funzione fork. Il programma crea un numero di
figli specificato 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 figlio (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 file ForkTest.c, distribuito insieme agli
altri sorgenti degli esempi su http://gapil.truelite.it/gapil source.tgz.
Decifrato il numero di figli da creare, il ciclo principale del programma (24-40) esegue in
successione la creazione dei processi figli controllando il successo della chiamata a fork (25-
29); ciascun figlio (31-34) si limita a stampare il suo numero di successione, eventualmente
attendere il numero di secondi specificato e scrivere un messaggio prima di uscire. Il processo
padre invece (36-38) stampa un messaggio di creazione, eventualmente attende il numero di
secondi specificato, e procede nell’esecuzione del ciclo; alla conclusione del ciclo, prima di uscire,
può essere specificato un altro periodo di attesa.
Se eseguiamo il comando9 senza specificare attese (come si può notare in (17-19) i valori
predefiniti specificano 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 quale processo fra il padre ed il figlio venga eseguito per primo dopo la chiamata a fork;
9
che è preceduto dall’istruzione export LD_LIBRARY_PATH=./ per permettere l’uso delle librerie dinamiche.
54 CAPITOLO 3. LA GESTIONE DEI PROCESSI
dall’esempio si può notare infatti come nei primi due cicli sia stato eseguito per primo il padre
(con la stampa del pid del nuovo processo) per poi passare all’esecuzione del figlio (completata
con i due avvisi di esecuzione ed uscita), e tornare all’esecuzione del padre (con la stampa del
passaggio al ciclo successivo), mentre la terza volta è stato prima eseguito il figlio (fino alla
conclusione) e poi il padre.
In generale l’ordine di esecuzione dipenderà, oltre che dall’algoritmo di scheduling usato
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
numero diverso di figli, si sono ottenute situazioni completamente diverse, compreso il caso in cui
il processo padre ha eseguito più di una fork prima che uno dei figli venisse messo in esecuzione.
Pertanto non si può fare nessuna assunzione sulla sequenza di esecuzione delle istruzioni
del codice fra padre e figli, né sull’ordine in cui questi potranno essere messi in esecuzione. Se
è necessaria una qualche forma di precedenza occorrerà provvedere ad espliciti meccanismi di
sincronizzazione, pena il rischio di incorrere nelle cosiddette race condition (vedi sez. 3.6.2).
In realtà a partire dal kernel 2.5.2-pre10 il nuovo scheduler di Ingo Molnar esegue sempre
per primo il figlio;10 questa è una ottimizzazione che serve a evitare che il padre, effettuan-
do per primo una operazione di scrittura in memoria, attivi il meccanismo del copy on write.
Questa operazione infatti potrebbe risultare del tutto inutile qualora il figlio fosse stato creato
solo per eseguire una exec, in tal caso infatti si invocherebbe un altro programma scartando
completamente lo spazio degli indirizzi, rendendo superflua la copia della memoria modificata
dal padre.
Eseguendo sempre per primo il figlio la exec verrebbe effettuata subito avendo cosı̀ la certezza
che il copy on write viene utilizzato solo quando necessario. Quanto detto in precedenza vale
allora soltanto per i kernel fino al 2.4; per mantenere la portabilità è però opportuno non fare
affidamento su questo comportamento, che non si riscontra in altri Unix e nelle versioni del
kernel precedenti a quella indicata.
Si noti inoltre che essendo i segmenti di memoria utilizzati dai singoli processi completamente
separati, le modifiche delle variabili nei processi figli (come l’incremento di i in 31) sono visibili
solo a loro (ogni processo vede solo la propria copia della memoria), e non hanno alcun effetto
sul valore che le stesse variabili hanno nel processo padre (ed in eventuali altri processi figli che
eseguano lo stesso codice).
Un secondo aspetto molto importante nella creazione dei processi figli è quello dell’interazione
dei vari processi con i file; per illustrarlo meglio proviamo a redirigere su un file l’output del
nostro programma di test, quello che otterremo è:
[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
10
i risultati precedenti sono stati ottenuti usando un kernel della serie 2.4.
3.2. LE FUNZIONI DI BASE 55
2. L’esecuzione di padre e figlio procede indipendentemente. In questo caso ciascuno dei due
processi deve chiudere i file che non gli servono una volta che la fork è stata eseguita, per
evitare ogni forma di interferenza.
Oltre ai file aperti i processi figli ereditano dal padre una serie di altre proprietà; la lista
dettagliata delle proprietà che padre e figlio hanno in comune dopo l’esecuzione di una fork è
la seguente:
• i file aperti e gli eventuali flag di close-on-exec impostati (vedi sez. 3.2.5 e sez. 6.3.6);
• gli identificatori per il controllo di accesso: l’user-ID reale, il group-ID reale, l’user-ID
effettivo, il group-ID effettivo ed i group-ID supplementari (vedi sez. 3.3.1);
• gli identificatori 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.5);
• la maschera dei permessi di creazione dei file (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. 11.2.6);
• i limiti sulle risorse (vedi sez. 8.3.2);
• il valori di nice, le priorità real-time e le affinità di processore (vedi sez. 3.4.2, sez. 3.4.3 e
sez. 3.4.4);
• le variabili di ambiente (vedi sez. 2.3.3).
Le differenze fra padre e figlio dopo la fork invece sono:11
• il valore di ritorno di fork;
• il pid (process id ), assegnato ad un nuovo valore univoco;
• il ppid (parent process id ), quello del figlio viene impostato al pid del padre;
• i valori dei tempi di esecuzione (vedi sez. 8.4.2) e delle risorse usate (vedi sez. 8.3.1), che
nel figlio sono posti a zero;
• i lock sui file (vedi sez. 12.1) e sulla memoria (vedi sez. 2.2.4), che non vengono ereditati
dal figlio;
• gli allarmi, i timer (vedi sez. 9.3.4) ed i segnali pendenti (vedi sez. 9.3.1), che per il figlio
vengono cancellati.
• le operazioni di I/O asincrono in corso (vedi sez. 12.3.3) che non vengono ereditate dal
figlio;
• gli aggiustamenti fatti dal padre ai semafori con semop (vedi sez. 11.2.5).
• le notifiche sui cambiamenti delle directory con dnotify (vedi sez. 9.1.4), che non vengono
ereditate dal figlio;
• le mappature di memoria marcate come MADV_DONTFORK (vedi sez. 12.4.1) che non vengono
ereditate dal figlio;
• l’impostazione con prctl (vedi sez. 3.5.2) che notifica al figlio la terminazione del padre
viene cancellata;
• il segnale di terminazione del figlio è sempre SIGCHLD anche qualora nel padre fosse stato
modificato (vedi sez. 3.5.1).
Una seconda funzione storica usata per la creazione di un nuovo processo è vfork, che è
esattamente identica a fork ed ha la stessa semantica e gli stessi errori; la sola differenza è che
non viene creata la tabella delle pagine né la struttura dei task per il nuovo processo. Il processo
padre è posto in attesa fintanto che il figlio non ha eseguito una execve o non è uscito con una
_exit. Il figlio condivide la memoria del padre (e modifiche possono avere effetti imprevedibili)
e non deve ritornare o uscire con exit ma usare esplicitamente _exit.
11
a parte le ultime quattro, relative a funzionalità specifiche di Linux, le altre sono esplicitamente menzionate
dallo standard POSIX.1-2001.
3.2. LE FUNZIONI DI BASE 57
Questa funzione è un rimasuglio dei vecchi tempi in cui eseguire una fork comportava anche
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 trascura-
bile, e l’uso di questa funzione, che resta un caso speciale della system call clone (che tratteremo
in dettaglio in sez. 3.5.1) è deprecato; per questo eviteremo di trattarla ulteriormente.
processo ha un padre, non è detto che sia cosı̀ alla sua conclusione, dato che il padre potrebbe
essere già terminato; si potrebbe avere cioè quello che si chiama un processo orfano.
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
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
possiamo parlare di un padre adottivo) cui riportare il suo stato di terminazione. Come verifica
di questo comportamento possiamo eseguire il nostro programma forktest imponendo a ciascun
processo figlio due secondi di attesa prima di uscire, il risultato è:
[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 figli, tornando alla
shell, che stampa il prompt sul terminale: circa due secondi dopo viene stampato a video anche
l’output dei tre figli che terminano, e come si può notare in questo caso, al contrario di quanto
visto in precedenza, essi riportano 1 come ppid.
Altrettanto rilevante è il caso in cui il figlio termina prima del padre, perché non è detto che
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.
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 file aperti vengono rilasciati immediatamente. I
processi che sono terminati, ma il cui stato di terminazione non è stato ancora ricevuto dal padre
sono chiamati zombie, essi restano presenti nella tabella dei processi ed in genere possono essere
identificati dall’output di ps per la presenza di una Z nella colonna che ne indica lo stato (vedi
tab. 3.8). Quando il padre effettuerà la lettura dello stato di uscita anche questa informazione,
non più necessaria, verrà scartata e la terminazione potrà dirsi completamente conclusa.
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
figli sono ancora presenti pur essendosi conclusi, con lo stato di zombie e l’indicazione che sono
stati terminati.
3.2. LE FUNZIONI DI BASE 59
La possibilità di avere degli zombie deve essere tenuta sempre presente quando si scrive un
programma che deve essere mantenuto in esecuzione a lungo e creare molti figli. In questo caso si
deve sempre avere cura di far leggere l’eventuale stato di uscita di tutti i figli (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
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
processi cui fa da padre, completandone la terminazione. Questo è quanto avviene anche quando,
come nel caso del precedente esempio con forktest, il padre termina con dei figli in stato di
zombie: alla sua terminazione infatti tutti i suoi figli (compresi gli zombie) verranno adottati da
init, il quale provvederà a completarne la terminazione.
Si tenga presente infine che siccome gli zombie sono processi già usciti, non c’è modo di
eliminarli con il comando kill; l’unica possibilità di cancellarli dalla tabella dei processi è
quella di terminare il processo che li ha generati, in modo che init possa adottarli e provvedere
a concluderne la terminazione.
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status)
Sospende il processo corrente finché un figlio non è uscito, o finché un segnale termina il
processo o chiama una funzione di gestione.
La funzione restituisce il pid del figlio in caso di successo e -1 in caso di errore; errno può assumere
i valori:
EINTR la funzione è stata interrotta da un segnale.
Questa funzione è presente fin dalle prime versioni di Unix; essa ritorna non appena un
qualunque processo figlio termina. Se un figlio è già terminato prima della chiamata la funzione
ritorna immediatamente, se più di un figlio è già terminato occorre continuare chiamare la
funzione più volte se si vuole recuperare lo stato di terminazione di tutti quanti.
Al ritorno della funzione lo stato di terminazione del figlio viene salvato nella variabile punta-
ta da status e tutte le risorse del kernel relative al processo (vedi sez. 3.2.3) vengono rilasciate.
Nel caso un processo abbia più figli il valore di ritorno della funzione sarà impostato al pid del
processo di cui si è ricevuto lo stato di terminazione, cosa che permette di identificare qual è il
figlio che è terminato.
Questa funzione ha il difetto di essere poco flessibile, in quanto ritorna all’uscita di un
qualunque processo figlio. Nelle occasioni in cui è necessario attendere la conclusione di un
processo specifico occorrerebbe predisporre un meccanismo che tenga conto dei processi già
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 una seconda funzione che effettua lo
stesso servizio, ma dispone di una serie di funzionalità più ampie, legate anche al controllo di
60 CAPITOLO 3. LA GESTIONE DEI PROCESSI
sessione (si veda sez. 10.1). Dato che è possibile ottenere lo stesso comportamento di wait12 si
consiglia di utilizzare sempre questa nuova funzione, waitpid, il cui prototipo è:
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options)
Attende la conclusione di un processo figlio.
La funzione restituisce il pid del processo che è uscito, 0 se è stata specificata l’opzione WNOHANG
e il processo non è uscito e -1 per un errore, nel qual caso errno assumerà i valori:
EINTR non è stata specificata l’opzione WNOHANG e la funzione è stata interrotta da un segnale.
ECHILD il processo specificato da pid non esiste o non è figlio del processo chiamante.
EINVAL si è specificato un valore non valido per l’argomento options.
La prima differenza fra le due funzioni è che con waitpid si può specificare in maniera
flessibile quale processo attendere, sulla base del valore fornito dall’argomento pid, questo può
assumere diversi valori, secondo lo specchietto riportato in tab. 3.1, dove si sono riportate anche
le costanti definite per indicare alcuni di essi.
Tabella 3.1: Significato dei valori dell’argomento pid della funzione waitpid.
Il comportamento di waitpid può inoltre essere modificato passando alla funzione delle
opportune opzioni tramite l’argomento options; questo deve essere specificato come maschera
binaria dei flag riportati nella prima parte in tab. 3.2 che possono essere combinati fra loro con
un OR aritmetico. Nella seconda parte della stessa tabella si sono riportati anche alcuni valori
non standard specifici di Linux, che consentono un controllo più dettagliato per i processi creati
con la system call generica clone (vedi sez. 3.5.1) usati principalmente per la gestione della
terminazione dei thread (vedi sez. ??).
Macro Descrizione
WNOHANG La funzione ritorna immediatamente anche se non è
terminato nessun processo figlio.
WUNTRACED Ritorna anche se un processo figlio è stato fermato.
WCONTINUED Ritorna anche quando un processo figlio che era stato
fermato ha ripreso l’esecuzione.13
__WCLONE Attende solo per i figli creati con clone (vedi sez. 3.5.1),
vale a dire processi che non emettono nessun segna-
le o emettono un segnale diverso da SIGCHLD alla
terminazione.
__WALL Attende per qualunque processo figlio.
__WNOTHREAD Non attende per i figli di altri thread dello stesso gruppo.
Tabella 3.2: Costanti che identificano i bit dell’argomento options della funzione waitpid.
12
in effetti il codice wait(&status) è del tutto equivalente a waitpid(WAIT_ANY, &status, 0).
13
disponibile solo a partire dal kernel 2.6.10.
3.2. LE FUNZIONI DI BASE 61
L’uso dell’opzione WNOHANG consente di prevenire il blocco della funzione qualora nessun figlio
sia uscito (o non si siano verificate le altre condizioni per l’uscita della funzione); in tal caso la
funzione ritornerà un valore nullo anziché positivo.14
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 figlio
entra nello stato stopped 15 (vedi tab. 3.8), mentre con WCONTINUED la funzione ritorna quando
un processo in stato stopped riprende l’esecuzione per la ricezione del segnale SIGCONT (l’uso di
questi segnali per il controllo di sessione è dettagliato in sez. 10.1.3).
La terminazione di un processo figlio (cosı̀ come gli altri eventi osservabili con waitpid)
è chiaramente un evento asincrono rispetto all’esecuzione di un programma e può avvenire in
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
al padre. L’azione predefinita (si veda sez. 9.1.1) per questo segnale è di essere ignorato, ma la
sua generazione costituisce il meccanismo di comunicazione asincrona con cui il kernel avverte
il processo padre che uno dei suoi figli è terminato.
Il comportamento delle funzioni è però cambiato nel passaggio dal kernel 2.4 al kernel 2.6,
quest’ultimo infatti si è adeguato alle prescrizioni dello standard POSIX.1-2001,16 e come da
esso richiesto se SIGCHLD viene ignorato, o se si imposta il flag di SA_NOCLDSTOP nella ricezione
dello stesso (si veda sez. 9.4.3) i processi figli che terminano non diventano zombie e sia wait che
waitpid si bloccano fintanto che tutti i processi figli non sono terminati, dopo di che falliscono
con un errore di ENOCHLD.17
Con i kernel della serie 2.4 e tutti i kernel delle serie precedenti entrambe le funzioni di attesa
ignorano questa prescrizione18 e si comportano sempre nello stesso modo, indipendentemente dal
fatto SIGCHLD sia ignorato o meno: attendono la terminazione di un processo figlio e ritornano
il relativo pid e lo stato di terminazione nell’argomento status.
In generale in un programma non si vuole essere forzati ad attendere la conclusione di un
processo figlio per proseguire l’esecuzione, 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 di chiamare queste funzioni è quella di utilizzarle al-
l’interno di un signal handler (vedremo un 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 figlio,
avremo la certezza che la chiamata a waitpid non si bloccherà.
Come accennato sia wait che waitpid restituiscono lo stato di terminazione del processo
tramite il puntatore status (se non interessa memorizzare lo stato si può passare un puntatore
nullo). Il valore restituito da entrambe le funzioni dipende dall’implementazione, ma tradizio-
nalmente alcuni bit (in genere 8) sono riservati per memorizzare lo stato di uscita, e altri per
14
anche in questo caso un valore positivo indicherà il pid del processo di cui si è ricevuto lo stato ed un valore
negativo un errore.
15
in realtà viene notificato soltanto il caso in cui il processo è stato fermato da un segnale di stop (vedi sez. 10.1.3),
e non quello in cui lo stato stopped è dovuto all’uso di ptrace (vedi sez. 3.5.3).
16
una revisione del 2001 dello standard POSIX.1 che ha aggiunto dei requisiti e delle nuove funzioni, come
waitid.
17
questo è anche il motivo per cui le opzioni WUNTRACED e WCONTINUED sono utilizzabili soltanto qualora non si
sia impostato il flag di SA_NOCLDSTOP per il segnale SIGCHLD.
18
lo standard POSIX.1 originale infatti lascia indefinito il comportamento di queste funzioni quando SIGCHLD
viene ignorato.
20
questa macro non è definita dallo standard POSIX.1-2001, ma è presente come estensione sia in Linux che
in altri Unix, deve essere pertanto utilizzata con attenzione (ad esempio è il caso di usarla in un blocco #ifdef
WCOREDUMP ... #endif.
20
è presente solo a partire dal kernel 2.6.10.
62 CAPITOLO 3. LA GESTIONE DEI PROCESSI
Macro Descrizione
WIFEXITED(s) Condizione vera (valore non nullo) per un processo figlio che sia
terminato normalmente.
WEXITSTATUS(s) Restituisce gli otto bit meno significativi dello stato di uscita del pro-
cesso (passato attraverso _exit, exit o come valore di ritorno di main);
può essere valutata solo se WIFEXITED ha restituito un valore non nullo.
WIFSIGNALED(s) Condizione vera se il processo figlio è terminato in maniera anomala a
causa di un segnale che non è stato catturato (vedi sez. 9.1.4).
WTERMSIG(s) Restituisce il numero del segnale che ha causato la terminazione ano-
mala del processo; può essere valutata solo se WIFSIGNALED ha restituito
un valore non nullo.
WCOREDUMP(s) Vera se il processo terminato ha generato un file di core dump; può
essere valutata solo se WIFSIGNALED ha restituito un valore non nullo.19
WIFSTOPPED(s) Vera se il processo che ha causato il ritorno di waitpid è bloccato; l’uso
è possibile solo con waitpid avendo specificato l’opzione WUNTRACED.
WSTOPSIG(s) Restituisce il numero del segnale che ha bloccato il processo; può essere
valutata solo se WIFSTOPPED ha restituito un valore non nullo.
WIFCONTINUED(s) Vera se il processo che ha causato il ritorno è stato riavviato da un
SIGCONT.20
Tabella 3.3: Descrizione delle varie macro di preprocessore utilizzabili per verificare lo stato di terminazione s
di un processo.
indicare il segnale che ha causato la terminazione (in caso di conclusione anomala), uno per
indicare se è stato generato un core dump, ecc.21
Lo standard POSIX.1 definisce una serie di macro di preprocessore da usare per analizzare
lo stato di uscita. Esse sono definite sempre in <sys/wait.h> ed elencate in tab. 3.3; si tenga
presente che queste macro prevedono che gli si passi come parametro la variabile di tipo int
puntata dall’argomento status restituito da wait o waitpid.
Si tenga conto che nel caso di conclusione anomala il valore restituito da WTERMSIG può essere
confrontato con le costanti che identificano i segnali definite 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 in-
trodotta una nuova funzione di attesa che consente di avere un controllo molto più preciso sui
possibili cambiamenti di stato dei processi figli e più dettagli sullo stato di uscita; la funzione è
waitid ed il suo prototipo è:
#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 figlio.
La funzione restituisce 0 in caso di successo e -1 per un errore, nel qual caso errno assumerà i
valori:
EINTR se non è stata specificata l’opzione WNOHANG e la funzione è stata interrotta da un
segnale.
ECHILD il processo specificato da pid non esiste o non è figlio del processo chiamante.
EINVAL si è specificato un valore non valido per l’argomento options.
La funzione prevede che si specifichi quali processi si intendono osservare usando i due ar-
gomenti idtype ed id; il primo indica se ci si vuole porre in attesa su un singolo processo, un
gruppo di processi o un processo qualsiasi, e deve essere specificato 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.
21
le definizioni esatte si possono trovare in <bits/waitstatus.h> ma questo file non deve mai essere usato
direttamente, esso viene incluso attraverso <sys/wait.h>.
3.2. LE FUNZIONI DI BASE 63
Macro Descrizione
P_PID Indica la richiesta di attendere per un processo figlio il
cui pid corrisponda al valore dell’argomento id.
P_PGID Indica la richiesta di attendere per un processo figlio ap-
partenente al process group (vedi sez. 10.1.2) il cui pgid
corrisponda al valore dell’argomento id.
P_ALL Indica la richiesta di attendere per un processo figlio
generico, il valore dell’argomento id viene ignorato.
Tabella 3.4: Costanti per i valori dell’argomento idtype della funzione waitid.
Tabella 3.5: Costanti che identificano i bit dell’argomento 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 specificata l’opzione WNOHANG e la funzione è ritornata
immediatamente senza che nessun figlio sia terminato. Pertanto per verificare il motivo del
ritorno della funzione occorre analizzare le informazioni che essa restituisce; queste, al contrario
delle precedenti wait e waitpid che usavano un semplice valore numerico, sono ritornate in una
struttura di tipo siginfo_t (vedi fig. 9.9) all’indirizzo puntato dall’argomento infop.
Tratteremo nei dettagli la struttura siginfo_t ed il significato 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_status con lo stato di uscita del figlio o con il segnale che lo ha terminato, fermato o
riavviato.
Infine Linux, seguendo un’estensione 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 (vedi
sez. 8.3) usate dal processo terminato e dai vari figli. Le due funzioni sono wait3 e wait4, che
diventano accessibili definendo 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)
È 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
favore di wait4.
la struttura rusage è definita in sys/resource.h, e viene utilizzata anche dalla funzione getrusage
(vedi sez. 8.3.1) per ottenere le risorse di sistema usate da un processo; la sua definizione è
riportata in fig. 8.6.
#include <unistd.h>
int execve(const char *filename, char *const argv[], char *const envp[])
Esegue il programma contenuto nel file filename.
La funzione ritorna solo in caso di errore, restituendo -1; nel qual caso errno può assumere i valori:
EACCES il file non è eseguibile, oppure il filesystem è montato in noexec, oppure non è un file
regolare o un interprete.
EPERM il file ha i bit suid o sgid, l’utente non è root, il processo viene tracciato, o il filesystem
è montato con l’opzione nosuid.
ENOEXEC il file è in un formato non eseguibile o non riconosciuto come tale, o compilato per
un’altra architettura.
ENOENT il file o una delle librerie dinamiche o l’interprete necessari per eseguirlo non esistono.
ETXTBSY l’eseguibile è aperto in scrittura da uno o più processi.
EINVAL l’eseguibile ELF ha più di un segmento PF_INTERP, cioè chiede di essere eseguito da
più di un interprete.
ELIBBAD un interprete ELF non è in un formato riconoscibile.
E2BIG la lista degli argomenti è troppo grande.
ed inoltre anche EFAULT, ENOMEM, EIO, ENAMETOOLONG, ELOOP, ENOTDIR, ENFILE, EMFILE.
possono essere acceduti dal nuovo programma quando la sua funzione main è dichiarata nella
forma main(int argc, char *argv[], char *envp[]).
Le altre funzioni della famiglia servono per fornire all’utente 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 l’immagine corrente del processo con quella indicata nel primo argomento. Gli
argomenti successivi consentono di specificare gli argomenti a linea di comando e l’ambiente
ricevuti dal nuovo processo.
Queste funzioni ritornano solo in caso di errore, restituendo -1; nel qual caso errno assumerà i
valori visti in precedenza per execve.
Per capire meglio le differenze fra le funzioni della famiglia si può fare riferimento allo spec-
chietto riportato in tab. 3.6. La prima differenza riguarda le modalità di passaggio dei valori che
poi andranno a costituire gli argomenti a linea di comando (cioè i valori di argv e argc visti
dalla funzione main del programma chiamato).
Queste modalità sono due e sono riassunte dagli mnemonici v e l che stanno rispettivamente
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 file che contiene il
programma che verrà eseguito.
Caratteristiche Funzioni
execl execlp execle execv execvp execve
argomenti a lista • • •
argomenti a vettore • • •
filename completo • • • •
ricerca su PATH • •
ambiente a vettore • •
uso di environ • • • •
Tabella 3.6: Confronto delle caratteristiche delle varie funzioni della famiglia exec.
La seconda differenza fra le funzioni riguarda le modalità con cui si specifica il programma che
si vuole eseguire. Con lo mnemonico p si indicano le due funzioni che replicano il comportamento
della shell nello specificare il comando da eseguire; quando l’argomento file non contiene una
“/” esso viene considerato come un nome di programma, e viene eseguita automaticamente una
ricerca fra i file presenti nella lista di directory specificate dalla variabile di ambiente PATH. Il file
che viene posto in esecuzione è il primo che viene trovato. Se si ha un errore relativo a permessi
di accesso insufficienti (cioè l’esecuzione della sottostante execve ritorna un EACCES), la ricerca
viene proseguita nelle eventuali ulteriori directory indicate in PATH; solo se non viene trovato
nessun altro file viene finalmente restituito EACCES.
Le altre quattro funzioni si limitano invece a cercare di eseguire il file indicato dall’argomento
path, che viene interpretato come il pathname del programma.
66 CAPITOLO 3. LA GESTIONE DEI PROCESSI
La terza differenza è come viene passata la lista delle variabili di ambiente. Con lo mnemonico
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.3) del processo di partenza per costruire l’ambiente.
Oltre a mantenere lo stesso pid, il nuovo programma fatto partire da exec mantiene la gran
parte delle proprietà del processo chiamante; una lista delle più significative è la seguente:
• il process id (pid) ed il parent process id (ppid);
• l’user-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 file (umask, vedi sez. 5.3.3) ed i lock sui file (vedi sez. 12.1);
• 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);
• la maschera dei segnali (si veda sez. 9.4.4).
Una serie di proprietà del processo originale, che non avrebbe senso mantenere in un pro-
gramma che esegue un codice completamente diverso in uno spazio di indirizzi totalmente indi-
pendente e ricreato da zero, vengono perse con l’esecuzione di exec; lo standard POSIX.1-2001
prevede che le seguenti proprietà non vengano preservate:
• l’insieme dei segnali pendenti (vedi sez. 9.3.1), che viene cancellato;
• gli eventuali stack alternativi per i segnali (vedi sez. 9.5.3);
• i directory stream (vedi sez. 5.1.6), che vengono chiusi;
• le mappature dei file in memoria (vedi sez. 12.4.1);
• i segmenti di memoria condivisa SysV (vedi sez. 11.2.6) e POSIX (vedi sez. 11.4.3);
• i blocchi sulla memoria (vedi sez. 2.2.4);
• le funzioni registrate all’uscita (vedi sez. 2.1.4);
• i semafori e le code di messaggi POSIX (vedi sez. 11.4.4 e sez. 11.4.2);
• i timer POSIX (vedi sez. 9.5.2).
I segnali che sono stati impostati per essere ignorati nel processo chiamante mantengono
la stessa impostazione pure nel nuovo programma, ma tutti gli altri segnali, ed in particolare
quelli per i quali è stato installato un gestore vengono impostati alla loro azione predefinita (vedi
sez. 9.3.1). Un caso speciale è il segnale SIGCHLD che, quando impostato a SIG_IGN, potrebbe
anche essere reimpostato a SIG_DFL, anche se questo con Linux non avviene.22
22
lo standard POSIX.1-2001 prevede che questo comportamento sia deciso dalla singola implementazione, quella
di Linux è di non modificare l’impostazione precedente.
3.2. LE FUNZIONI DI BASE 67
Oltre alle precedenti che sono completamente generali e disponibili anche su altri sistemi
unix-like, esistono altre proprietà dei processi, attinenti caratteristiche specifiche di Linux, che
non vengono preservate nell’esecuzione della funzione exec, queste sono:
La gestione dei file aperti nel passaggio al nuovo programma lanciato con exec dipende dal
valore che ha il flag di close-on-exec (vedi anche sez. 6.3.6) per ciascun file descriptor. I file per
cui è impostato vengono chiusi, tutti gli altri file restano aperti. Questo significa che il com-
portamento predefinito è che i file restano aperti attraverso una exec, a meno di una chiamata
esplicita a fcntl che imposti il suddetto flag. 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 effettua da sola l’impostazione del flag di close-on-exec sulle directory che apre,
in maniera trasparente all’utente.
Il comportamento della funzione in relazione agli identificatori relativi al controllo di accesso
verrà trattato in dettaglio in sez. 3.3, qui è sufficiente anticipare (si faccia riferimento a sez. 3.3.1
per la definizione di questi identificatori) come l’user-ID reale ed il group-ID reale restano sempre
gli stessi, mentre l’user-ID salvato ed il group-ID salvato vengono impostati rispettivamente
all’user-ID effettivo ed il group-ID effettivo, questi ultimi normalmente non vengono modificati,
a meno che il file di cui viene chiesta l’esecuzione non abbia o il suid bit o lo sgid bit impostato,
in questo caso l’user-ID effettivo ed il group-ID effettivo vengono impostati rispettivamente
all’utente o al gruppo cui il file appartiene.
Se il file da eseguire è in formato a.out e necessita di librerie condivise, viene lanciato il
linker dinamico /lib/ld.so prima del programma per caricare le librerie necessarie ed effettuare
il link dell’eseguibile.23 Se il programma è in formato ELF per caricare le librerie dinamiche
viene usato l’interprete indicato nel segmento PT_INTERP previsto dal formato stesso, in genere
questo è /lib/ld-linux.so.1 per programmi collegati con le libc5, e /lib/ld-linux.so.2 per
programmi collegati con le glibc.
Infine nel caso il file sia uno script esso deve iniziare con una linea nella forma #!/path/to/interpreter
[argomenti] dove l’interprete indicato deve essere un programma valido (binario, non un al-
tro script) che verrà chiamato come se si fosse eseguito il comando interpreter [argomenti]
filename.24
Con la famiglia delle exec si chiude il novero delle funzioni su cui è basata la gestione dei
processi in Unix: con fork si crea un nuovo processo, con exec si lancia un nuovo programma,
23
il formato è ormai in completo disuso, per cui è molto probabile che non il relativo supporto non sia disponibile.
24
si tenga presente che con Linux quanto viene scritto come argomenti viene passato all’interprete co-
me un unico argomento con una unica stringa di lunghezza massima di 127 caratteri e se questa di-
mensione viene ecceduta la stringa viene troncata; altri Unix hanno dimensioni massime diverse, e diver-
si 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
http://www.in-ulm.de/˜mascheck/various/shebang/.
68 CAPITOLO 3. LA GESTIONE DEI PROCESSI
con exit e wait si effettua e verifica la conclusione dei processi. Tutte le altre funzioni sono
ausiliarie e servono per la lettura e l’impostazione dei vari parametri connessi ai processi.
Tabella 3.7: Identificatori di utente e gruppo associati a ciascun processo con indicazione dei suffissi usati dalle
varie funzioni di manipolazione.
25
in realtà già esistono estensioni di questo modello base, che lo rendono più flessibile e controllabile, come le
capabilities illustrate in sez. 5.4.4, le ACL per i file (vedi sez. 5.4.2) o il Mandatory Access Control di SELinux;
inoltre basandosi sul lavoro effettuato con SELinux, a partire dal kernel 2.5.x, è iniziato lo sviluppo di una
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.
3.3. IL CONTROLLO DI ACCESSO 69
Al primo gruppo appartengono l’user-ID reale ed il group-ID reale: questi vengono impo-
stati al login ai valori corrispondenti all’utente con cui si accede al sistema (e relativo gruppo
principale). Servono per l’identificazione dell’utente e normalmente non vengono mai cambiati.
In realtà vedremo (in sez. 3.3.2) che è possibile modificarli, ma solo ad un processo che abbia
i privilegi di amministratore; questa possibilità è usata proprio dal programma login che, una
volta completata la procedura di autenticazione, lancia una shell per la quale imposta questi
identificatori ai valori corrispondenti all’utente che entra nel sistema.
Al secondo gruppo appartengono lo user-ID effettivo ed il group-ID effettivo (a cui si aggiun-
gono gli eventuali group-ID supplementari dei gruppi dei quali l’utente fa parte). Questi sono
invece gli identificatori usati nelle verifiche dei permessi del processo e per il controllo di accesso
ai file (argomento affrontato in dettaglio in sez. 5.3.1).
Questi identificatori 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
bit suid o sgid impostati (il significato di questi bit è affrontato in dettaglio in sez. 5.3.2). In
questo caso essi saranno impostati all’utente e al gruppo proprietari del file. Questo consente,
per programmi in cui ci sia necessità, di dare a qualunque utente normale privilegi o permessi
di un altro (o dell’amministratore).
Come nel caso del pid e del ppid, anche tutti questi identificatori 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 l’user-ID reale del processo corrente.
uid_t geteuid(void)
Restituisce l’user-ID effettivo del processo corrente.
gid_t getgid(void)
Restituisce il group-ID reale del processo corrente.
gid_t getegid(void)
Restituisce il group-ID effettivo del processo corrente.
Queste funzioni non riportano condizioni di errore.
In generale l’uso di privilegi superiori deve essere limitato il più possibile, per evitare abusi e
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 effettuate 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 identificatori, il saved ed il filesystem.
Il primo gruppo è lo stesso usato in SVr4, e previsto dallo standard POSIX quando è definita la
costante _POSIX_SAVED_IDS,26 il secondo gruppo è specifico di Linux e viene usato per migliorare
la sicurezza con NFS.
L’user-ID salvato ed il group-ID salvato sono copie dell’user-ID effettivo e del group-ID
effettivo del processo padre, e vengono impostati dalla funzione exec all’avvio del processo,
come copie dell’user-ID effettivo e del group-ID effettivo 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 effettivi all’inizio dell’esecuzione di un nuovo programma.
L’user-ID di filesystem e il group-ID di filesystem sono un’estensione introdotta in Linux per
rendere più sicuro l’uso di NFS (torneremo sull’argomento in sez. 3.3.2). Essi sono una replica dei
corrispondenti identificatori del gruppo effective, ai quali si sostituiscono per tutte le operazioni
di verifica dei permessi relativi ai file (trattate in sez. 5.3.1). Ogni cambiamento effettuato sugli
identificatori effettivi viene automaticamente riportato su di essi, per cui in condizioni normali si
può tranquillamente ignorarne l’esistenza, in quanto saranno del tutto equivalenti ai precedenti.
26
in caso si abbia a cuore la portabilità del programma su altri Unix è buona norma controllare sempre la
disponibilità di queste funzioni controllando se questa costante è definita.
70 CAPITOLO 3. LA GESTIONE DEI PROCESSI
in questo modo, dato che il group-ID effettivo è quello giusto, il programma può accedere a
/var/log/utmp in scrittura ed aggiornarlo. A questo punto il programma può eseguire una
setgid(getgid()) per impostare il group-ID effettivo a quello dell’utente (e dato che il group-
ID reale corrisponde la funzione avrà successo), in questo modo non sarà possibile lanciare dal
terminale programmi che modificano detto file, in tal caso infatti la situazione degli identificatori
sarebbe:
e ogni processo lanciato dal terminale avrebbe comunque gid come group-ID effettivo. All’uscita
dal terminale, per poter di nuovo aggiornare lo stato di /var/log/utmp il programma esegui-
rà una setgid(utmp) (dove utmp è il valore numerico associato al gruppo utmp, ottenuto ad
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:
#include <unistd.h>
#include <sys/types.h>
int setreuid(uid_t ruid, uid_t euid)
Imposta l’user-ID reale e l’user-ID effettivo del processo corrente ai valori specificati da
ruid e euid.
int setregid(gid_t rgid, gid_t egid)
Imposta il group-ID reale ed il group-ID effettivo del processo corrente ai valori specificati
da rgid e egid.
Le funzioni restituiscono 0 in caso di successo e -1 in caso di fallimento: l’unico errore possibile è
EPERM.
La due funzioni sono analoghe ed il loro comportamento è identico; quanto detto per la
prima riguardo l’user-ID, si applica immediatamente alla seconda per il group-ID. I processi
non privilegiati possono impostare solo i valori del loro user-ID effettivo o reale; valori diver-
si comportano il fallimento della chiamata; l’amministratore invece può specificare un valore
qualunque. Specificando un argomento di valore -1 l’identificatore corrispondente verrà lasciato
inalterato.
Con queste funzioni si possono scambiare fra loro gli user-ID reale e effettivo, e pertanto
è possibile implementare un comportamento simile a quello visto in precedenza per setgid,
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
fase intermedia in cui si sono scambiati gli identificatori, in questo caso infatti essi avranno un
user-ID reale privilegiato, che dovrà essere esplicitamente eliminato prima di porre in esecuzione
un nuovo programma (occorrerà cioè eseguire un’altra chiamata dopo la fork e prima della exec
per uniformare l’user-ID reale a quello effettivo) in caso contrario il nuovo programma potrebbe
a sua volta effettuare uno scambio e riottenere privilegi non previsti.
Lo stesso problema di propagazione dei privilegi ad eventuali processi figli si pone per l’user-
ID salvato: questa funzione deriva da un’implementazione che non ne prevede la presenza, e
quindi non è possibile usarla per correggere la situazione come nel caso precedente. Per questo
27
almeno fino alla versione 4.3+BSD.
72 CAPITOLO 3. LA GESTIONE DEI PROCESSI
motivo in Linux tutte le volte che si imposta un qualunque valore diverso da quello dall’user-
ID reale corrente, l’user-ID salvato viene automaticamente uniformato al valore dell’user-ID
effettivo.
Altre due funzioni, seteuid e setegid, sono un’estensione dello standard POSIX.1, ma
sono comunque supportate dalla maggior parte degli Unix; esse vengono usate per cambiare gli
identificatori del gruppo effective ed i loro prototipi sono:
#include <unistd.h>
#include <sys/types.h>
int seteuid(uid_t uid)
Imposta l’user-ID effettivo del processo corrente a uid.
int setegid(gid_t gid)
Imposta il group-ID effettivo del processo corrente a gid.
Le funzioni restituiscono 0 in caso di successo e -1 in caso di fallimento: l’unico errore è EPERM.
Come per le precedenti le due funzioni sono identiche, per cui tratteremo solo la prima. Gli
utenti normali possono impostare l’user-ID effettivo solo al valore dell’user-ID reale o dell’user-
ID salvato, l’amministratore può specificare qualunque valore. Queste funzioni sono usate per
permettere all’amministratore di impostare solo l’user-ID effettivo, dato che l’uso normale di
setuid comporta l’impostazione di tutti gli identificatori.
Le due funzioni setresuid e setresgid sono invece un’estensione introdotta in Linux,28 e
permettono un completo controllo su tutti e tre i gruppi di identificatori (real, effective 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 l’user-ID reale, l’user-ID effettivo e l’user-ID salvato del processo corrente ai valori
specificati 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 effettivo ed il group-ID salvato del processo corrente
ai valori specificati rispettivamente da rgid, egid e sgid.
Anche queste funzioni sono un’estensione specifica di Linux, e non richiedono nessun privi-
legio. I valori sono restituiti negli argomenti, che vanno specificati come puntatori (è un altro
esempio di value result argument). Si noti che queste funzioni sono le uniche in grado di leggere
gli identificatori del gruppo saved.
28
per essere precisi a partire dal kernel 2.1.44.
3.3. IL CONTROLLO DI ACCESSO 73
Infine le funzioni setfsuid e setfsgid servono per impostare gli identificatori del gruppo
filesystem che sono usati da Linux per il controllo dell’accesso ai file. Come già accennato in
sez. 3.3.1 Linux definisce questo ulteriore gruppo di identificatori, che in circostanze normali
sono assolutamente equivalenti a quelli del gruppo effective, 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 differenza fra gli identificatori dei
gruppi effective e filesystem, ed è per ovviare ad un problema di sicurezza che si presenta quando
si deve implementare un server NFS.
Il server NFS infatti deve poter cambiare l’identificatore con cui accede ai file per assumere
l’identità del singolo utente remoto, ma se questo viene fatto cambiando l’user-ID effettivo o
l’user-ID reale il server si espone alla ricezione di eventuali segnali ostili da parte dell’utente di
cui ha temporaneamente assunto l’identità. Cambiando solo l’user-ID di filesystem si ottengono
i privilegi necessari per accedere ai file, mantenendo quelli originari per quanto riguarda tutti
gli altri controlli di accesso, cosı̀ che l’utente non possa inviare segnali al server NFS.
Le due funzioni usate per cambiare questi identificatori sono setfsuid e setfsgid, ovvia-
mente sono specifiche 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 l’user-ID di filesystem del processo corrente a fsuid.
int setfsgid(gid_t fsgid)
Imposta il group-ID di filesystem del processo corrente a fsgid.
Le funzioni restituiscono 0 in caso di successo e -1 in caso di fallimento: l’unico errore possibile è
EPERM.
#include <sys/types.h>
#include <unistd.h>
int getgroups(int size, gid_t list[])
Legge gli identificatori 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:
EFAULT list non ha un indirizzo valido.
EINVAL il valore di size è diverso da zero ma minore del numero di gruppi supplementari del
processo.
La funzione legge gli identificatori dei gruppi supplementari del processo sul vettore list
di dimensione size. Non è specificato se la funzione inserisca o meno nella lista il group-ID
29
il numero massimo di gruppi secondari può essere ottenuto con sysconf (vedi sez. 8.1.2), leggendo il parametro
_SC_NGROUPS_MAX.
74 CAPITOLO 3. LA GESTIONE DEI PROCESSI
effettivo del processo. Se si specifica un valore di size uguale a 0 list non viene modificato, ma
si ottiene il numero di gruppi supplementari.
Una seconda funzione, getgrouplist, può invece essere usata per ottenere tutti i gruppi a
cui appartiene un certo utente; il suo prototipo è:
#include <sys/types.h>
#include <grp.h>
int getgrouplist(const char *user, gid_t group, gid_t *groups, int *ngroups)
Legge i gruppi supplementari.
#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à
i valori:
EFAULT list non ha un indirizzo valido.
EPERM il processo non ha i privilegi di amministratore.
EINVAL il valore di size è maggiore del valore massimo consentito.
La funzione imposta i gruppi supplementari del processo corrente ai valori specificati nel
vettore passato con l’argomento list, di dimensioni date dall’argomento size. Il numero mas-
simo di gruppi supplementari è un parametro di sistema, che può essere ricavato con le modalità
spiegate in sez. 8.1.
Se invece si vogliono impostare i gruppi supplementari del processo a quelli di un utente
specifico, si può usare initgroups il cui prototipo è:
#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à
gli stessi valori di setgroups più ENOMEM quando non c’è memoria sufficiente per allocare lo spazio
per informazioni dei gruppi.
La funzione esegue la scansione del database dei gruppi (usualmente /etc/group) cercando i
gruppi di cui è membro l’utente user con cui costruisce una lista di gruppi supplementari, a cui
aggiunge anche group, infine imposta questa lista per il processo corrente usando setgroups.
Si tenga presente che sia setgroups che initgroups non sono definite nello standard POSIX.1
e che pertanto non è possibile utilizzarle quando si definisce _POSIX_SOURCE o si compila con il
flag -ansi, è pertanto meglio evitarle se si vuole scrivere codice portabile.
3.4. LA GESTIONE DELLA PRIORITÀ DEI PROCESSI 75
Tabella 3.8: Elenco dei possibili stati di un processo in Linux, nella colonna STAT si è riportata la corrispondente
lettera usata dal comando ps nell’omonimo campo.
real-time,31 in cui è vitale che i processi che devono essere eseguiti in un determinato momento
non debbano aspettare la conclusione di altri che non hanno questa necessità.
Il concetto di priorità assoluta dice che quando due processi si contendono l’esecuzione, vince
sempre quello con la priorità assoluta più alta. Ovviamente questo avviene solo per i processi che
sono pronti per essere eseguiti (cioè nello stato runnable). La priorità assoluta viene in genere
indicata con un numero intero, ed un valore più alto comporta una priorità maggiore. Su questa
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
data una priorità assoluta pari a zero, e la decisione di assegnazione della CPU è fatta solo con
il meccanismo tradizionale della priorità dinamica. In Linux tuttavia è possibile assegnare anche
una priorità assoluta, nel qual caso un processo avrà la precedenza su tutti gli altri di priorità
inferiore, che saranno eseguiti solo quando quest’ultimo non avrà bisogno della CPU.
31
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 l’hard real-time in cui è necessario che i tempi di esecuzione di un programma siano
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.
32
per alcune delle quali sono state introdotte delle varianti specifiche.
33
quella che viene mostrata nella colonna PR del comando top.
34
e dipende strettamente dalla versione di kernel; in particolare a partire dalla serie 2.6.x lo scheduler è stato
riscritto completamente, con molte modifiche susseguitesi per migliorarne le prestazioni, per un certo periodo ed
è stata anche introdotta la possibilità di usare diversi algoritmi, selezionabili sia in fase di compilazione, che,
nelle versioni più recenti, all’avvio (addirittura è stato ideato un sistema modulare che permette di cambiare lo
scheduler a sistema attivo).
3.4. LA GESTIONE DELLA PRIORITÀ DEI PROCESSI 77
diminuito tutte le volte che un processo è in stato Runnable ma non viene posto in esecuzione.35
Lo scheduler infatti mette sempre in esecuzione, fra tutti i processi in stato Runnable, quello
che ha il valore di priorità dinamica più basso.36 Il fatto che questo valore venga diminuito
quando un processo non viene posto in esecuzione pur essendo pronto, significa che la priorità
dei processi che non ottengono l’uso del processore viene progressivamente incrementata, cosı̀
che anche questi alla fine hanno la possibilità di essere eseguiti.
Sia la dimensione della time-slice che il valore di partenza della priorità dinamica sono
determinate dalla cosiddetta nice (o niceness) del processo.37 L’origine del nome di questo
parametro sta nel fatto che generalmente questo viene usato per diminuire la priorità di un
processo, come misura di cortesia nei confronti degli altri. I processi infatti vengono creati dal
sistema con un valore di nice nullo e nessuno è privilegiato rispetto agli altri; specificando un
valore positivo si avrà una time-slice più breve ed un valore di priorità dinamica iniziale più
alto, mentre un valore negativo darà una time-slice più lunga ed un valore di priorità dinamica
iniziale più basso.
Esistono diverse funzioni che consentono di modificare la niceness di un processo; la più
semplice è funzione nice, che opera sul processo corrente, il suo prototipo è:
#include <unistd.h>
int nice(int inc)
Aumenta il valore di nice per il processo corrente.
La funzione ritorna zero o il nuovo valore di nice in caso di successo e -1 in caso di errore, nel
qual caso errno può assumere i valori:
EPERM non si ha il permesso di specificare un valore di inc negativo.
L’argomento inc indica l’incremento da effettuare rispetto al valore di nice corrente: que-
st’ultimo può assumere valori compresi fra PRIO_MIN e PRIO_MAX; nel caso di Linux sono fra
−20 e 19,38 ma per inc si può specificare un valore qualunque, positivo o negativo, ed il sistema
provvederà a troncare il risultato nell’intervallo consentito. Valori positivi comportano maggiore
cortesia e cioè una diminuzione della priorità, valori negativi comportano invece un aumento
della priorità. Con i kernel precedenti il 2.6.12 solo l’amministratore39 può specificare valori
negativi di inc che permettono di aumentare la priorità di un processo, a partire da questa
versione è consentito anche agli utenti normali alzare (entro certi limiti, che vedremo più avanti)
la priorità dei propri processi.
Gli standard SUSv2 e POSIX.1 prevedono che la funzione ritorni il nuovo valore di nice del
processo; tuttavia la system call di Linux non segue questa convenzione e restituisce sempre 0 in
caso di successo e −1 in caso di errore; questo perché −1 è un valore di nice legittimo e questo
comporta una confusione con una eventuale condizione di errore. La system call originaria inoltre
non consente, se non dotati di adeguati privilegi, di diminuire un valore di nice precedentemente
innalzato.
35
in realtà il calcolo della priorità dinamica e la conseguente scelta di quale processo mettere in esecuzione
avviene con un algoritmo molto più complicato, che tiene conto anche della interattività del processo, utilizzando
diversi fattori, questa è una brutale semplificazione per rendere l’idea del funzionamento, per una trattazione
più dettagliata, anche se non aggiornatissima, dei meccanismi di funzionamento dello scheduler si legga il quarto
capitolo di [6].
36
con le priorità dinamiche il significato del valore numerico ad esse associato è infatti invertito, un valore più
basso significa una priorità maggiore.
37
questa è una delle tante proprietà che ciascun processo si porta dietro, essa viene ereditata dai processi figli e
mantenuta attraverso una exec; fino alla serie 2.4 essa era mantenuta nell’omonimo campo nice della task_struct,
con la riscrittura dello scheduler eseguita nel 2.6 viene mantenuta nel campo static_prio come per le priorità
statiche.
38
in realtà l’intervallo varia a seconda delle versioni di kernel, ed è questo a partire dal kernel 1.3.43, anche se
oggi si può avere anche l’intervallo fra −20 e 20.
39
o un processo con la capability CAP_SYS_NICE, vedi sez. 5.4.4.
78 CAPITOLO 3. LA GESTIONE DEI PROCESSI
Fino alle glibc 2.2.4 la funzione di libreria riportava direttamente il risultato dalla system
call, violando lo standard, per cui per ottenere il nuovo valore occorreva una successiva chiamata
alla funzione getpriority. A partire dalla glibc 2.2.4 nice è stata reimplementata e non viene
più chiamata la omonima system call, con questa versione viene restituito come valore di ritorno
il valore di nice, come richiesto dallo standard.40 In questo caso l’unico modo per rilevare in
maniera affidabile una condizione di errore è quello di azzerare errno prima della chiamata della
funzione e verificarne il valore quando nice restituisce −1.
Per leggere il valore di nice di un processo occorre usare la funzione getpriority, derivata
da BSD; il suo prototipo è:
#include <sys/resource.h>
int getpriority(int which, int who)
Restituisce il valore di nice per l’insieme dei processi specificati.
La funzione ritorna la priorità in caso di successo e -1 in caso di errore, nel qual caso errno può
assumere i valori:
ESRCH non c’è nessun processo che corrisponda ai valori di which e who.
EINVAL il valore di which non è valido.
nelle vecchie versioni può essere necessario includere anche <sys/time.h>, questo non è più
necessario con versioni recenti delle librerie, ma è comunque utile per portabilità.
La funzione permette, a seconda del valore di which, di leggere la priorità di un processo, di
un gruppo di processi (vedi sez. 10.1.2) o di un utente, specificando un corrispondente valore per
who secondo la legenda di tab. 3.9; un valore nullo di quest’ultimo indica il processo, il gruppo
di processi o l’utente correnti.
which who Significato
PRIO_PROCESS pid_t processo
PRIO_PRGR pid_t process group
PRIO_USER uid_t utente
Tabella 3.9: Legenda del valore dell’argomento which e del tipo dell’argomento 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
specificati; di nuovo, dato che −1 è un valore possibile, per poter rilevare una condizione di errore
è necessario cancellare sempre errno prima della chiamata alla funzione per verificare che essa
resti uguale a zero.
Analoga a getpriority è la funzione setpriority che permette di impostare la priorità di
uno o più processi; il suo prototipo è:
#include <sys/resource.h>
int setpriority(int which, int who, int prio)
Imposta la priorità per l’insieme dei processi specificati.
La funzione ritorna 0 in caso di successo e -1 in caso di errore, nel qual caso errno può assumere
i valori:
ESRCH non c’è nessun processo che corrisponda ai valori di which e who.
EINVAL il valore di which non è valido.
EACCES si è richiesto un aumento di priorità senza avere sufficienti privilegi.
EPERM un processo senza i privilegi di amministratore ha cercato di modificare la priorità di
un processo di un altro utente.
La funzione imposta la priorità al valore specificato da prio per tutti i processi indicati dagli
argomenti which e who. In questo caso come valore di prio deve essere specificato il valore di
40
questo viene fatto chiamando al suo interno setpriority, che tratteremo a breve.
3.4. LA GESTIONE DELLA PRIORITÀ DEI PROCESSI 79
nice da assegnare, e non un incremento (positivo o negativo) come nel caso di nice. La funzione
restituisce il valore di nice assegnato in caso di successo e −1 in caso di errore, e come per
nice anche in questo caso per rilevare un errore occorre sempre porre a zero errno prima della
chiamata della funzione, essendo −1 un valore di nice valido.
Si tenga presente che solo l’amministratore41 ha la possibilità di modificare arbitrariamente
le priorità di qualunque processo. Un utente normale infatti può modificare solo la priorità dei
suoi processi ed in genere soltanto diminuirla. Fino alla versione di kernel 2.6.12 Linux ha seguito
le specifiche dello standard SUSv3, e come per tutti i sistemi derivati da SysV veniva richiesto
che l’user-ID reale o quello effettivo del processo chiamante corrispondessero all’user-ID reale (e
solo a quello) del processo di cui si intendeva cambiare la priorità. A partire dalla versione 2.6.12
è stata adottata la semantica in uso presso i sistemi derivati da BSD (SunOS, Ultrix, *BSD), in
cui la corrispondenza può essere anche con l’user-ID effettivo.
Sempre a partire dal kernel 2.6.12 è divenuto possibile anche per gli utenti ordinari poter
aumentare la priorità dei propri processi specificando un valore di prio negativo. Questa opera-
zione non è possibile però in maniera indiscriminata, ed in particolare può essere effettuata solo
nell’intervallo consentito dal valore del limite RLIMIT_NICE (torneremo su questo in sez. 8.3.2).
FIFO First In First Out. Il processo viene eseguito fintanto che non cede volontariamente la
CPU (con sched_yield), si blocca, finisce o viene interrotto da un processo a priorità
più alta. Se il processo viene interrotto da uno a priorità più alta esso resterà in cima alla
41
o più precisamente un processo con la capability CAP_SYS_NICE, vedi sez. 5.4.4.
42
questo a meno che non si siano installate le patch di RTLinux, RTAI o Adeos, con i quali è possibile ottenere
un sistema effettivamente hard real-time. In tal caso infatti gli interrupt vengono intercettati dall’interfaccia real-
time (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.
43
in particolare a partire dalla versione 2.6.18 sono stati inserite nel kernel una serie di modifiche che consentono
di avvicinarsi sempre di più ad un vero e proprio sistema real-time estendendo il concetto di preemption alle
operazioni dello stesso kernel; esistono vari livelli a cui questo può essere fatto, ottenibili attivando in fase di
compilazione una fra le opzioni CONFIG_PREEMPT_NONE, CONFIG_PREEMPT_VOLUNTARY e CONFIG_PREEMPT_DESKTOP.
80 CAPITOLO 3. LA GESTIONE DEI PROCESSI
lista e sarà il primo ad essere eseguito quando i processi a priorità più alta diverranno
inattivi. Se invece lo si blocca volontariamente sarà posto in coda alla lista (ed altri
processi con la stessa priorità potranno essere eseguiti).
RR Round Robin. Il comportamento è del tutto analogo a quello precedente, con la sola
differenza 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
di tutti i processi, da cui il nome della politica. Solo i processi con la stessa priorità ed
in stato Runnable entrano nel girotondo.
Lo standard POSIX.1-2001 prevede una funzione che consenta sia di modificare le politiche di
scheduling, passando da real-time a ordinarie o viceversa, che di specificare, in caso di politiche
real-time, la eventuale priorità statica; la funzione è sched_setscheduler ed il suo prototipo è:
#include <sched.h>
int sched_setscheduler(pid_t pid, int policy, const struct sched_param *p)
Imposta priorità e politica di scheduling.
La funzione ritorna 0 in caso di successo e −1 in caso di errore, nel qual caso errno può assumere
i valori:
ESRCH il processo pid non esiste.
EINVAL il valore di policy non esiste o il relativo valore di p non è valido.
EPERM il processo non ha i privilegi per attivare la politica richiesta.
Policy Significato
SCHED_FIFO Scheduling real-time con politica FIFO.
SCHED_RR Scheduling real-time con politica Round Robin.
SCHED_OTHER Scheduling ordinario.
SCHED_BATCH Scheduling ordinario con l’assunzione ulteriore di lavoro CPU intensive.44
SCHED_IDLE Scheduling di priorità estremamente bassa.45
Con le versioni più recenti del kernel sono state introdotte anche delle varianti sulla politica
di scheduling tradizionale per alcuni carichi di lavoro specifici, queste due nuove politiche sono
specifiche di Linux e non devono essere usate se si vogliono scrivere programmi portabili.
La politica SCHED_BATCH è una variante della politica ordinaria con la sola differenza che
i processi ad essa soggetti non ottengono, nel calcolo delle priorità dinamiche fatto dallo sche-
duler, il cosiddetto bonus di interattività che mira a favorire i processi che si svegliano dallo
stato di Sleep.46 La si usa pertanto, come indica il nome, per processi che usano molta CPU
(come programmi di calcolo) che in questo modo sono leggermente sfavoriti rispetto ai processi
interattivi che devono rispondere a dei dati in ingresso, pur non perdendo il loro valore di nice.
La politica SCHED_IDLE invece è una politica dedicata ai processi che si desidera siano eseguiti
con la più bassa priorità possibile, ancora più bassa di un processo con il minimo valore di nice.
44
introdotto con il kernel 2.6.16.
45
introdotto con il kernel 2.6.23.
46
cosa che accade con grande frequenza per i processi interattivi, dato che essi sono per la maggior parte del
tempo in attesa di dati in ingresso da parte dell’utente.
3.4. LA GESTIONE DELLA PRIORITÀ DEI PROCESSI 81
In sostanza la si può utilizzare per processi che devono essere eseguiti se non c’è niente altro
da fare. Va comunque sottolineato che anche un processo SCHED_IDLE avrà comunque una sua
possibilità di utilizzo della CPU, sia pure in percentuale molto bassa.
Qualora si sia richiesta una politica real-time il valore della priorità statica viene impostato
attraverso la struttura sched_param, riportata in fig. 3.5, il cui solo campo attualmente definito è
sched_priority. Il campo deve contenere il valore della priorità statica da assegnare al processo;
lo standard prevede che questo debba essere assegnato all’interno di un intervallo fra un massimo
ed un minimo che nel caso di Linux sono rispettivamente 1 e 99.
struct sched_param {
int sched_priority ;
};
I processi con politica di scheduling ordinaria devono sempre specificare un valore nullo di
sched_priority altrimenti si avrà un errore EINVAL, questo valore infatti non ha niente a che
vedere con la priorità dinamica determinata dal valore di nice, che deve essere impostato con le
funzioni viste in precedenza.
Lo standard POSIX.1b prevede comunque che i due valori della massima e minima priorità
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.
int sched_get_priority_min(int policy)
Legge il valore minimo della priorità statica per la politica di scheduling policy.
La funzioni ritornano il valore della priorità in caso di successo e −1 in caso di errore, nel qual
caso errno può assumere i valori:
EINVAL il valore di policy non è valido.
Si tenga presente che quando si imposta una politica di scheduling real-time per un processo
o se ne cambia la priorità statica questo viene messo in cima alla lista dei processi con la stessa
priorità; questo comporta che verrà eseguito subito, interrompendo eventuali altri processi con
la stessa priorità in quel momento in esecuzione.
Il kernel mantiene i processi con la stessa priorità assoluta in una lista, ed esegue sempre il
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
automaticamente rimesso in coda alla lista, e la sua esecuzione continua fintanto 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); l’esecuzione viene ripresa subito solo
nel caso che esso sia stato interrotto da un processo a priorità più alta.
Solo un processo con i privilegi di amministratore47 può impostare senza restrizioni priorità
assolute diverse da zero o politiche SCHED_FIFO e SCHED_RR. Un utente normale può modificare
solo le priorità di processi che gli appartengono; è cioè richiesto che l’user-ID effettivo del processo
chiamante corrisponda all’user-ID reale o effettivo del processo indicato con pid.
Fino al kernel 2.6.12 gli utenti normali non potevano impostare politiche real-time o modi-
ficare la eventuale priorità statica di un loro processo. A partire da questa versione è divenuto
possibile anche per gli utenti normali usare politiche real-time fintanto che la priorità assoluta
47
più precisamente con la capacità CAP_SYS_NICE, vedi sez. 5.4.4.
82 CAPITOLO 3. LA GESTIONE DEI PROCESSI
che si vuole impostare è inferiore al limite RLIMIT_RTPRIO (vedi sez. 8.3.2) ad essi assegnato.
Unica eccezione a questa possibilità sono i processi SCHED_IDLE, che non possono cambiare po-
litica di scheduling indipendentemente dal valore di RLIMIT_RTPRIO. Inoltre, in caso di processo
già sottoposto ad una politica real-time, un utente può sempre, indipendentemente dal valore di
RLIMIT_RTPRIO, diminuirne la priorità o portarlo ad una politica ordinaria.
Se si intende operare solo sulla priorità statica di un processo si possono usare le due funzioni
sched_setparam e sched_getparam che consentono rispettivamente di impostarne e leggerne il
valore, i loro prototipi sono:
#include <sched.h>
int sched_setparam(pid_t pid, const struct sched_param *param)
Imposta la priorità statica del processo pid.
int sched_getparam(pid_t pid, struct sched_param *param)
Legge la priorità statica del processo pid.
Entrambe le funzioni ritornano 0 in caso di successo e −1 in caso di errore, nel qual caso errno
può assumere i valori:
ESRCH il processo pid non esiste.
EINVAL il valore di param non ha senso per la politica usata dal processo.
EPERM non si hanno privilegi sufficienti per eseguire l’operazione.
#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:
ESRCH il processo pid non esiste.
EPERM non si hanno privilegi sufficienti per eseguire l’operazione.
La funzione restituisce il valore, secondo quanto elencato in tab. 3.10, della politica di sche-
duling per il processo specificato; se l’argomento pid è nullo viene restituito il valore relativo al
processo chiamante.
L’ultima funzione che permette di leggere le informazioni relative ai processi real-time è
sched_rr_get_interval, che permette di ottenere la lunghezza della time-slice usata dalla
politica round robin; il suo prototipo è:
#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
i valori:
ESRCH il processo pid non esiste.
ENOSYS la system call non è stata implementata.
3.4. LA GESTIONE DELLA PRIORITÀ DEI PROCESSI 83
La funzione restituisce il valore dell’intervallo di tempo usato per la politica round robin in
una struttura timespec, (la cui definizione si può trovare in fig. 5.7). In realtà dato che in Linux
questo intervallo di tempo è prefissato e non modificabile, questa funzione ritorna sempre un
valore di 150 millisecondi, e non importa specificare il PID di un processo reale.
Come accennato ogni processo può rilasciare volontariamente la CPU in modo da consentire
agli altri processi di essere eseguiti; la funzione che consente di fare tutto ciò è sched_yield, il
cui prototipo è:
#include <sched.h>
int sched_yield(void)
Rilascia volontariamente l’esecuzione.
La funzione ritorna 0 in caso di successo e -1 in caso di errore, nel qual caso errno viene impostata
opportunamente.
Questa funzione ha un utilizzo effettivo soltanto quando si usa lo scheduling real-time, e serve
a far sı̀ che il processo corrente rilasci la CPU, in modo da essere rimesso in coda alla lista dei
processi con la stessa priorità per permettere ad un altro di essere eseguito; se però il processo
è l’unico ad essere presente sulla coda l’esecuzione non sarà interrotta. In genere usano questa
funzione i processi con politica SCHED_FIFO, per permettere l’esecuzione degli altri processi con
pari priorità quando la sezione più urgente è finita.
La funzione può essere utilizzata anche con processi che usano lo scheduling ordinario, ma in
questo caso il comportamento non è ben definito, e dipende dall’implementazione. Fino al kernel
2.6.23 questo comportava che i processi venissero messi in fondo alla coda di quelli attivi, con la
possibilità di essere rimessi in esecuzione entro breve tempo, con l’introduzione del Completely
Fair Scheduler questo comportamento è cambiato ed un processo che chiama la funzione viene
inserito nella lista dei processi inattivo, con un tempo molto maggiore.48
Per ovviare a questo tipo di problemi è nato il concetto di affinità di processore (o CPU
affinity); la possibilità cioè di far sı̀ che un processo possa essere assegnato per l’esecuzione
sempre allo stesso processore. Lo scheduler dei kernel della serie 2.4.x aveva una scarsa CPU
affinity, e l’effetto 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
processore.
In certi casi però resta l’esigenza di poter essere sicuri che un processo sia sempre eseguito
dallo stesso processore,49 e per poter risolvere questo tipo di problematiche nei nuovi kernel50 è
stata introdotta l’opportuna infrastruttura ed una nuova system call che permette di impostare
su quali processori far eseguire un determinato processo attraverso una maschera di affinità. La
corrispondente funzione di libreria è sched_setaffinity ed il suo prototipo è:
#include <sched.h>
int sched_setaffinity (pid_t pid, unsigned int cpusetsize, const cpu_set_t
*cpuset)
Imposta la maschera di affinità del processo pid.
La funzione ritorna 0 in caso di successo e -1 in caso di errore, nel qual caso errno può assumere
i valori:
ESRCH il processo pid non esiste.
EINVAL il valore di cpuset contiene riferimenti a processori non esistenti nel sistema.
EPERM il processo non ha i privilegi sufficienti per eseguire l’operazione.
ed inoltre anche EFAULT.
Infine se un gruppo di processi accede alle stesse risorse condivise (ad esempio una applica-
zione con più thread ) può avere senso usare lo stesso processore in modo da sfruttare meglio l’uso
della sua cache; questo ovviamente riduce i benefici di un sistema multiprocessore nell’esecuzio-
ne contemporanea dei thread, ma in certi casi (quando i thread sono inerentemente serializzati
nell’accesso ad una risorsa) possono esserci sufficienti vantaggi nell’evitare la perdita della cache
da rendere conveniente l’uso dell’affinità di processore.
Per facilitare l’uso dell’argomento cpuset le glibc hanno introdotto un apposito dato di
tipo, cpu_set_t,52 che permette di identificare un insieme di processori. Il dato è una maschera
binaria: in generale è un intero a 32 bit in cui ogni bit corrisponde ad un processore, ma dato
che per architetture particolari il numero di bit di un intero può non essere sufficiente, è stata
creata questa che è una interfaccia generica che permette di usare a basso livello un tipo di dato
qualunque rendendosi indipendenti dal numero di bit e dalla loro disposizione.
Questa interfaccia, oltre alla definizione 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 verificare se vi è già presente:
#include <sched.h>
void CPU_ZERO(cpu_set_t *set)
Inizializza l’insieme (vuoto).
void CPU_SET(int cpu, cpu_set_t *set)
Inserisce il processore cpu nell’insieme.
void CPU_CLR(int cpu, cpu_set_t *set)
Rimuove il processore cpu nell’insieme.
int CPU_ISSET(int cpu, cpu_set_t *set)
Controlla se il processore cpu è nell’insieme.
Oltre a queste macro, simili alle analoghe usate per gli insiemi di file descriptor (vedi
sez. 12.2.2) è definita la costante CPU_SETSIZE che indica il numero massimo di processori che
possono far parte dell’insieme, e che costituisce un limite massimo al valore dell’argomento cpu.
In generale la maschera di affinità è preimpostata in modo che un processo possa essere
eseguito su qualunque processore, se può comunque leggere il valore per un processo specifico
usando la funzione sched_getaffinity, il suo prototipo è:
#include <sched.h>
int sched_getaffinity (pid_t pid, unsigned int cpusetsize, const cpu_set_t
*cpuset)
Legge la maschera di affinità del processo pid.
La funzione ritorna 0 in caso di successo e -1 in caso di errore, nel qual caso errno può assumere
i valori:
ESRCH il processo pid non esiste.
EFAULT il valore di cpuset non è un indirizzo valido.
altrettanto importante per le prestazioni, è quella dell’accesso a disco. Per questo motivo sono
stati introdotti diversi I/O scheduler in grado di distribuire in maniera opportuna questa risorsa
ai vari processi. Fino al kernel 2.6.17 era possibile soltanto differenziare le politiche generali
di gestione, scegliendo di usare un diverso I/O scheduler ; a partire da questa versione, con
l’introduzione dello scheduler CFQ (Completely Fair Queuing) è divenuto possibile, qualora si
usi questo scheduler, impostare anche delle diverse priorità di accesso per i singoli processi.53
La scelta dello scheduler di I/O si può fare in maniera generica a livello di avvio del kernel
assegnando il nome dello stesso al parametro elevator, mentre se ne può indicare uno per
l’accesso al singolo disco scrivendo nel file /sys/block/dev /queue/scheduler (dove dev è il
nome del dispositivo associato al disco); gli scheduler disponibili sono mostrati dal contenuto
dello stesso file che riporta fra parentesi quadre quello attivo, il default in tutti i kernel recenti
è proprio il cfq,54 che supporta le priorità. Per i dettagli sulle caratteristiche specifiche degli
altri scheduler, la cui discussione attiene a problematiche di ambito sistemistico, si consulti la
documentazione nella directory Documentation/block/ dei sorgenti del kernel.
Una volta che si sia impostato lo scheduler CFQ ci sono due specifiche system call, specifiche
di Linux, che consentono di leggere ed impostare le priorità di I/O.55 Dato che non esiste una
interfaccia diretta nelle glibc per queste due funzioni occorrerà invocarle tramite la funzione
syscall (come illustrato in sez. 1.1.3). Le due funzioni sono ioprio_get ed ioprio_set; i
rispettivi prototipi sono:
#include <linux/ioprio.h>
int ioprio_get(int which, int who)
int ioprio_set(int which, int who, int ioprio)
Rileva o imposta la priorità di I/O di un processo.
Le funzioni leggono o impostano la priorità di I/O sulla base dell’indicazione dei due ar-
gomenti which e who che hanno lo stesso significato già visto per gli omonimi argomenti di
getpriority e setpriority. Anche in questo caso si deve specificare il valore di which tramite
le opportune costanti riportate in tab. 3.11 che consentono di indicare un singolo processo, i
processi di un process group (tratteremo questo argomento in sez. 10.1.2) o tutti o processi di
un utente.
which who Significato
IPRIO_WHO_PROCESS pid_t processo
IPRIO_WHO_PRGR pid_t process group
IPRIO_WHO_USER uid_t utente
Tabella 3.11: Legenda del valore dell’argomento which e del tipo dell’argomento who delle funzioni ioprio_get
e ioprio_set per le tre possibili scelte.
In caso di successo ioprio_get restituisce un intero positivo che esprime il valore della priori-
tà di I/O, questo valore è una maschera binaria composta da due parti, una che esprime la classe
di scheduling di I/O del processo, l’altra che esprime, quando la classe di scheduling lo prevede,
la priorità del processo all’interno della classe stessa. Questo stesso formato viene utilizzato per
indicare il valore della priorità da impostare con l’argomento ioprio di ioprio_set.
53
al momento (kernel 2.6.31), le priorità di I/O sono disponibili soltanto per questo scheduler.
54
nome con cui si indica appunto lo scheduler Completely Fair Queuing.
55
se usate in corrispondenza ad uno scheduler diverso il loro utilizzo non avrà alcun effetto.
3.4. LA GESTIONE DELLA PRIORITÀ DEI PROCESSI 87
Per la gestione dei valori che esprimono le priorità di I/O sono state definite delle opportune
macro di preprocessore, riportate in tab. 3.12. I valori delle priorità si ottengono o si impostano
usando queste macro. Le prime due si usano con il valore restituito da ioprio_get e per ottenere
rispettivamente la classe di scheduling56 e l’eventuale valore della priorità. La terza macro viene
invece usata per creare un valore di priorità da usare come argomento di ioprio_set per eseguire
una impostazione.
Macro Significato
IOPRIO_PRIO_CLASS(value ) dato il valore di una priorità come restituito da
ioprio_get estrae il valore della classe.
IOPRIO_PRIO_DATA(value ) dato il valore di una priorità come restituito da
ioprio_get estrae il valore della priorità.
IOPRIO_PRIO_VALUE(class,prio ) dato un valore di priorità ed una classe ottiene il valore
numerico da passare a ioprio_set.
Le classi di scheduling previste dallo scheduler CFQ sono tre, e ricalcano tre diverse modalità
di distribuzione delle risorse analoghe a quelle già adottate anche nel funzionamento dello sche-
duler del processore. Ciascuna di esse è identificata tramite una opportuna costante, secondo
quanto riportato in tab. 3.13.
La classe di priorità più bassa è IOPRIO_CLASS_IDLE; i processi in questa classe riescono ad
accedere a disco soltanto quando nessun altro processo richiede l’accesso. Occorre pertanto usarla
con molta attenzione, perché un processo in questa classe può venire completamente bloccato
quando ci sono altri processi in una qualunque delle altre due classi che stanno accedendo al
disco. Quando si usa questa classe non ha senso indicare un valore di priorità, dato che in questo
caso non esiste nessuna gerarchia e la priorità è identica, la minima possibile, per tutti i processi.
Classe Significato
IOPRIO_CLASS_RT Scheduling di I/O real time.
IOPRIO_CLASS_BE Scheduling di I/O ordinario.
IOPRIO_CLASS_IDLE Scheduling di I/O di priorità minima.
La seconda classe di priorità di I/O è IOPRIO_CLASS_BE (il nome sta per best-effort) che
è quella usata ordinariamente da tutti processi. In questo caso esistono priorità diverse che
consentono di assegnazione di una maggiore banda passante nell’accesso a disco ad un processo
rispetto agli altri, con meccanismo simile a quello dei valori di nice in cui si evita che un processo
a priorità più alta possa bloccare indefinitamente quelli a priorità più bassa. In questo caso però
le diverse priorità sono soltanto otto, indicate da un valore numerico fra 0 e 7 e come per nice
anche in questo caso un valore più basso indica una priorità maggiore.
Infine la classe di priorità di I/O real-time IOPRIO_CLASS_RT ricalca le omonime priorità di
processore: un processo in questa classe ha sempre la precedenza nell’accesso a disco rispetto
a tutti i processi delle altre classi e di un processo nella stessa classe ma con priorità inferiore,
ed è pertanto in grado di bloccare completamente tutti gli altri. Anche in questo caso ci sono
8 priorità diverse con un valore numerico fra 0 e 7, con una priorità più elevata per valori più
bassi.
In generale nel funzionamento ordinario la priorità di I/O di un processo viene impostata
in maniera automatica nella classe IOPRIO_CLASS_BE con un valore ottenuto a partire dal cor-
rispondente valore di nice tramite la formula: prio = (nice + 20)/5. Un utente ordinario può
56
restituita dalla macro con i valori di tab. 3.13.
88 CAPITOLO 3. LA GESTIONE DEI PROCESSI
modificare con ioprio_set soltanto le priorità dei processi che gli appartengono,57 cioè quelli
il cui user-ID reale corrisponde all’user-ID reale o effettivo del chiamante. Data la possibilità
di ottenere un blocco totale del sistema, solo l’amministratore58 può impostare un processo ad
una priorità di I/O nella classe IOPRIO_CLASS_RT, lo stesso privilegio era richiesto anche per la
classe IOPRIO_CLASS_IDLE fino al kernel 2.6.24, ma dato che in questo caso non ci sono effetti
sugli altri processi questo limite è stato rimosso a partire dal kernel 2.6.25.
chiamante allochi preventivamente un’area di memoria. In genere lo si fa con una malloc che
allochi un buffer che la funzione imposterà come stack del nuovo processo, avendo ovviamente
cura di non utilizzarlo direttamente nel processo chiamante. In questo modo i due task avranno
degli stack indipendenti e non si dovranno affrontare problematiche di race condition. Si tenga
presente inoltre che in molte architetture di processore lo stack cresce verso il basso, pertanto
in tal caso non si dovrà specificare per child_stack il puntatore restituito da malloc, ma un
puntatore alla fine del buffer da essa allocato.
Dato che tutto ciò è necessario solo per i thread che condividono la memoria, la system
call, a differenza della funzione di libreria che vedremo a breve, consente anche di passare per
child_stack il valore NULL, che non imposta un nuovo stack. Se infatti si crea un processo,
questo ottiene un suo nuovo spazio degli indirizzi,60 ed in questo caso si applica la semantica
del copy on write illustrata in sez. 3.2.2, per cui le pagine dello stack verranno automaticamente
copiate come le altre e il nuovo processo avrà un suo stack totalmente indipendente da quello
del padre.
Dato che l’uso principale della nuova system call è quello relativo alla creazione dei thread,
le glibc definiscono una funzione di libreria con una sintassi diversa, orientata a questo scopo,
e la system call resta accessibile solo se invocata esplicitamente come visto in sez. 1.1.3.61 La
funzione di libreria si chiama semplicemente clone ed il suo prototipo è:
#include <sys/sched.h>
int clone(int (*fn)(void *), void *child_stack, int flags, void *arg, ...
/* pid_t *ptid, struct user_desc *tls, pid_t *ctid */)
Crea un nuovo processo o thread eseguendo la funzione fn.
La funzione ritorna al chiamante il Thread ID assegnato al nuovo processo in caso di successo e
−1 in caso di errore, nel qual caso errno può assumere i valori:
EAGAIN sono già in esecuzione troppi processi.
EINVAL si è usata una combinazione non valida di flag o un valore nullo per child_stack.
ENOMEM non c’è memoria sufficiente per creare una nuova task_struct o per copiare le parti
del contesto del chiamante necessarie al nuovo task.
EPERM non si hanno i privilegi di amministratore richiesti dai flag indicati.
La funzione prende come primo argomento il puntatore alla funzione che verrà messa in
esecuzione nel nuovo processo, che può avere un unico argomento di tipo puntatore a void,
il cui valore viene passato dal terzo argomento arg; per quanto il precedente prototipo possa
intimidire nella sua espressione, in realtà l’uso è molto semplice basterà definire una qualunque
funzione fn del tipo indicato, e fn(arg) sarà eseguita in un nuovo processo.
Il nuovo processo resterà in esecuzione fintanto che la funzione fn non ritorna, o esegue exit
o viene terminata da un segnale. Il valore di ritorno della funzione (o quello specificato con exit)
verrà utilizzato come stato di uscita della funzione.
I tre argomenti ptid, tls e ctid sono opzionali e sono presenti solo a partire dal kernel 2.6.
Il comportamento di clone, che si riflette sulle caratteristiche del nuovo processo da essa
creato, è controllato dall’argomento flags,
CLONE_CHILD_CLEARTID
CLONE_CHILD_SETTID
CLONE_FILES
CLONE_FS
60
è sottinteso cioè che non si stia usando il flag CLONE_VM.
61
ed inoltre per questa system call non è disponibile la chiamata veloce con vsyscall.
90 CAPITOLO 3. LA GESTIONE DEI PROCESSI
CLONE_IO
CLONE_NEWIPC
CLONE_NEWNET
CLONE_NEWNS
CLONE_NEWPID
CLONE_NEWUTS
CLONE_PARENT
CLONE_PARENT_SETTID
CLONE_PID
CLONE_PTRACE
CLONE_SETTLS
CLONE_SIGHAND
CLONE_STOPPED
CLONE_SYSVSEM
CLONE_THREAD
CLONE_UNTRACED
CLONE_VFORK
CLONE_VM
PR_CAPBSET_READ Controlla la disponibilità di una delle capabilities (vedi sez. 5.4.4). La funzione
ritorna 1 se la capacità specificata nell’argomento arg2 (con una delle costanti di tab. 5.20)
è presente nel capabilities bounding set del processo e zero altrimenti, se arg2 non è un
valore valido si avrà un errore di EINVAL. Introdotta a partire dal kernel 2.6.25.
PR_CAPBSET_DROP Rimuove permanentemente una delle capabilities (vedi sez. 5.4.4) dal proces-
so e da tutti i suoi discendenti. La funzione cancella la capacità specificata nell’argomento
arg2 con una delle costanti di tab. 5.20 dal capabilities bounding set del processo. L’o-
perazione richiede i privilegi di amministratore (la capacità CAP_SETPCAP), altrimenti la
chiamata fallirà con un errore di EPERM; se il valore di arg2 non è valido o se il supporto
per le file capabilities non è stato compilato nel kernel la chiamata fallirà con un errore di
EINVAL. Introdotta a partire dal kernel 2.6.25.
PR_GET_DUMPABLE Ottiene come valore di ritorno della funzione lo stato corrente del flag che
controlla la effettiva generazione dei core dump. Introdotta a partire dal kernel 2.3.20.
PR_SET_ENDIAN Imposta la endianess del processo chiamante secondo il valore fornito in arg2. I
valori possibili sono sono: PR_ENDIAN_BIG (big endian), PR_ENDIAN_LITTLE (little endian),
e PR_ENDIAN_PPC_LITTLE (lo pseudo little endian del PowerPC). Introdotta a partire dal
kernel 2.6.18, solo per architettura PowerPC.
PR_GET_ENDIAN Ottiene il valore della endianess del processo chiamante, salvato sulla variabile
puntata da arg2 che deve essere passata come di tipo (int *). Introdotta a partire dal
kernel 2.6.18, solo su PowerPC.
PR_SET_FPEMU Imposta i bit di controllo per l’emulazione della virgola mobile su architettura
ia64, secondo il valore di arg2, si deve passare PR_FPEMU_NOPRINT per emulare in maniera
trasparente l’accesso alle operazioni in virgola mobile, o PR_FPEMU_SIGFPE per non emularle
ed inviare il segnale SIGFPE (vedi sez. 9.2.2). Introdotta a partire dal kernel 2.4.18, solo su
ia64.
PR_GET_FPEMU Ottiene il valore dei flag di controllo dell’emulazione della virgola mobile, salvato
all’indirizzo puntato da arg2, che deve essere di tipo (int *). Introdotta a partire dal
kernel 2.4.18, solo su ia64.
PR_GET_FPEXC Ottiene il valore della modalità delle eccezioni delle operazioni in virgola mobile,
salvata all’indirizzo puntato arg2, che deve essere di tipo (int *). Introdotta a partire
dal kernel 2.4.21, solo su PowerPC.
PR_GET_KEEPCAPS Ottiene come valore di ritorno della funzione il valore del flag di controllo
impostato con PR_SET_KEEPCAPS. Introdotta a partire dal kernel 2.2.18.
PR_SET_NAME Imposta il nome del processo chiamante alla stringa puntata da arg2, che deve
essere di tipo (char *). Il nome può essere lungo al massimo 16 caratteri, e la stringa
deve essere terminata da NUL se più corta. Introdotta a partire dal kernel 2.6.9.
PR_GET_NAME Ottiene il nome del processo chiamante nella stringa puntata da arg2, che deve
essere di tipo (char *); si devono allocare per questo almeno 16 byte, e il nome sarà
terminato da NUL se più corto. Introdotta a partire dal kernel 2.6.9.
PR_SET_PDEATHSIG Consente di richiedere l’emissione di un segnale, che sarà ricevuto dal pro-
cesso chiamante, in occorrenza della terminazione del proprio processo padre; in sostanza
consente di invertire il ruolo di SIGCHLD. Il valore di arg2 deve indicare il numero del
segnale, o 0 per disabilitare l’emissione. Il valore viene automaticamente cancellato per un
processo figlio creato con fork. Introdotta a partire dal kernel 2.1.57.
PR_GET_PDEATHSIG Ottiene il valore dell’eventuale segnale emesso alla terminazione del padre,
salvato all’indirizzo puntato arg2, che deve essere di tipo (int *). Introdotta a partire
dal kernel 2.3.15.
PR_SET_SECCOMP Imposta il cosiddetto secure computing mode per il processo corrente. Prevede
come unica possibilità che arg2 sia impostato ad 1. Una volta abilitato il secure computing
mode il processo potrà utilizzare soltanto un insieme estremamente limitato di system
call : read, write, _exit e sigreturn, ogni altra system call porterà all’emissione di un
SIGKILL (vedi sez. 9.2.3). Il secure computing mode è stato ideato per fornire un supporto
per l’esecuzione di codice esterno non fidato e non verificabile a scopo di calcolo;65 in
genere i dati vengono letti o scritti grazie ad un socket o una pipe, e per evitare problemi
di sicurezza non sono possibili altre operazioni se non quelle citate. Introdotta a partire dal
kernel 2.6.23, disponibile solo se si è abilitato il supporto nel kernel con CONFIG_SECCOMP.
64
trattasi di gestione specialistica della gestione delle eccezioni dei calcoli in virgola mobile che, i cui dettagli al
momento vanno al di là dello scopo di questo testo.
65
lo scopo è quello di poter vendere la capacità di calcolo della proprio macchina ad un qualche servizio di
calcolo distribuito senza comprometterne la sicurezza eseguendo codice non sotto il proprio controllo.
3.5. FUNZIONI DI GESTIONE AVANZATA 93
PR_GET_SECCOMP Ottiene come valore di ritorno della funzione lo stato corrente del secure com-
puting mode, al momento attuale la funzione è totalmente inutile in quanto l’unico valore
ottenibile è 0, dato che la chiamata di questa funzione in secure computing mode compor-
terebbe l’emissione di SIGKILL, è stata comunque definita per eventuali estensioni future.
Introdotta a partire dal kernel 2.6.23.
PR_GET_SECUREBITS Ottiene come valore di ritorno della funzione l’impostazione corrente per i
securebits. Introdotta a partire dal kernel 2.6.26.
PR_GET_TIMING Ottiene come valore di ritorno della funzione il metodo di temporizzazione del
processo attualmente in uso. Introdotta a partire dal kernel 2.6.0-test4.
PR_SET_TSC Imposta il flag che indica se il processo chiamante può leggere il registro di pro-
cessore contenente il contatore dei timestamp (TSC, o Time Stamp Counter ) da indi-
care con il valore di arg2. Si deve specificare PR_TSC_ENABLE per abilitare la lettura
o PR_TSC_SIGSEGV per disabilitarla con la generazione di un segnale di SIGSEGV (vedi
sez. 9.2.2). La lettura viene automaticamente disabilitata se si attiva il secure computing
mode. Introdotta a partire dal kernel 2.6.26, solo su x86.
PR_GET_TSC Ottiene il valore del flag che controlla la lettura del contattore dei timestamp,
salvato all’indirizzo puntato arg2, che deve essere di tipo (int *). Introdotta a partire
dal kernel 2.6.26, solo su x86.
PR_SET_UNALIGN Imposta la modalità di controllo per l’accesso a indirizzi di memoria non alli-
neati, che in varie architetture risultano illegali, da indicare con il valore di arg2. Si deve
specificare il valore PR_UNALIGN_NOPRINT per ignorare gli accessi non allineati, ed il valore
PR_UNALIGN_SIGBUS per generare un segnale di SIGBUS (vedi sez. 9.2.2) in caso di accesso
non allineato. Introdotta con diverse versioni su diverse architetture.
PR_GET_UNALIGN Ottiene il valore della modalità di controllo per l’accesso a indirizzi di memoria
non allineati, salvato all’indirizzo puntato arg2, che deve essere di tipo (int *). Introdotta
con diverse versioni su diverse architetture.
PR_MCE_KILL Imposta la politica di gestione degli errori dovuti a corruzione della memoria
per problemi hardware. Questo tipo di errori vengono riportati dall’hardware di controllo
della RAM e vengono gestiti dal kernel,66 ma devono essere opportunamente riportati
ai processi che usano quella parte di RAM che presenta errori; nel caso specifico questo
avviene attraverso l’emissione di un segnale di SIGBUS (vedi sez. 9.2.2).67
66
la funzionalità è disponibile solo sulle piattaforme più avanzate che hanno il supporto hardware per questo
tipo di controlli.
67
in particolare viene anche impostato il valore di si_code in siginfo_t a BUS_MCEERR_AO; per il significato di
tutto questo si faccia riferimento alla trattazione di sez. 9.4.3.
94 CAPITOLO 3. LA GESTIONE DEI PROCESSI
Il comportamento di default prevede che per tutti i processi si applichi la politica generale
di sistema definita nel file /proc/sys/vm/memory_failure_early_kill, ma specificando
per arg2 il valore PR_MCE_KILL_SET è possibile impostare con il contenuto di arg3 una
politica specifica del processo chiamante. Si può tornare alla politica di default del sistema
utilizzando invece per arg2 il valore PR_MCE_KILL_CLEAR. In tutti i casi, per compatibilità
con eventuali estensioni future, tutti i valori degli argomenti non utilizzati devono essere
esplicitamente posti a zero, pena il fallimento della chiamata con un errore di EINVAL.
In caso di impostazione di una politica specifica del processo con PR_MCE_KILL_SET i valori
di arg3 possono essere soltanto due, che corrispondono anche al valore che si trova nell’im-
postazione generale di sistema di memory_failure_early_kill, con PR_MCE_KILL_EARLY
si richiede l’emissione immediata di SIGBUS non appena viene rilevato un errore, men-
tre con PR_MCE_KILL_LATE il segnale verrà inviato solo quando il processo tenterà un
accesso alla memoria corrotta. Questi due valori corrispondono rispettivamente ai valo-
ri 1 e 0 di memory_failure_early_kill.68 Si può usare per arg3 anche un terzo valore,
PR_MCE_KILL_DEFAULT, che corrisponde a impostare per il processo la politica di default.69
Introdotta a partire dal kernel 2.6.32.
PR_MCE_KILL_GET Ottiene come valore di ritorno della funzione la politica di gestione degli
errori dovuti a corruzione della memoria. Tutti gli argomenti non utilizzati (al momento
tutti) devono essere nulli pena la ricezione di un errore di EINVAL. Introdotta a partire dal
kernel 2.6.32.
In un ambiente multitasking il concetto è essenziale, dato che un processo può essere interrot-
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.6.2) derivanti da operazioni interrotte in una fase in cui non erano ancora state completate.
Nel caso dell’interazione fra processi la situazione è molto più semplice, ed occorre preoc-
cuparsi della atomicità delle operazioni solo quando si ha a che fare con meccanismi di inter-
comunicazione (che esamineremo in dettaglio in cap. 11) o nelle operazioni con i file (vedremo
alcuni esempi in sez. 6.3.2). In questi casi in genere l’uso delle appropriate funzioni di libreria
per compiere le operazioni necessarie è garanzia sufficiente di atomicità in quanto le system call
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
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 l’assegnazione di una variabile possono non essere più operazioni atomiche
(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
essere atomico. In pratica comunque si può assumere che, in ogni piattaforma su cui è imple-
mentato Linux, il tipo int, gli altri interi di dimensione inferiore ed i puntatori sono atomici.
Non è affatto detto che lo stesso valga per interi di dimensioni maggiori (in cui l’accesso può
comportare più istruzioni in assembler) o per le strutture. In tutti questi casi è anche opportuno
marcare come volatile le variabili che possono essere interessate ad accesso condiviso, onde
evitare problemi con le ottimizzazioni del codice.
L’esempio tipico di una situazione che può condurre ad un deadlock è quello in cui un flag di
“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
per lo sblocco. In questo caso, dato che l’evento di sblocco del flag è avvenuto senza che ce ne
accorgessimo proprio fra il controllo e la messa in attesa, quest’ultima diventerà perpetua (da
cui il nome di deadlock ).
In tutti questi casi è di fondamentale importanza il concetto di atomicità visto in sez. 3.6.1;
questi problemi infatti possono essere risolti soltanto assicurandosi, quando essa sia richiesta,
che sia possibile eseguire in maniera atomica le operazioni necessarie.
70
si ricordi quanto illustrato in sez. 1.2.7.
Capitolo 4
97
98 CAPITOLO 4. L’ARCHITETTURA DEI FILE
All’avvio del sistema, completata la fase di inizializzazione, il kernel riceve dal bootloader
l’indicazione di quale dispositivo contiene il filesystem da usare come punto di partenza e que-
sto viene montato come radice dell’albero (cioè nella directory /); tutti gli ulteriori filesystem
che possono essere su altri dispositivi dovranno poi essere inseriti nell’albero montandoli su
opportune directory del filesystem montato come radice.
Alcuni filesystem speciali (come /proc che contiene un’interfaccia ad alcune strutture interne
del kernel) sono generati automaticamente dal kernel stesso, ma anche essi devono essere montati
all’interno dell’albero dei file.
Una directory, come vedremo in maggior dettaglio in sez. 4.2.2, è anch’essa un file, solo che
è un file particolare che il kernel riconosce come tale. Il suo scopo è quello di contenere una lista
di nomi di file e le informazioni che associano ciascun nome al contenuto. Dato che questi nomi
possono corrispondere ad un qualunque oggetto del filesystem, compresa un’altra directory, si
ottiene naturalmente un’organizzazione ad albero inserendo nomi di directory in altre directory.
Un file può essere indicato rispetto alla directory corrente semplicemente specificandone il
nome2 da essa contenuto. All’interno dello stesso albero si potranno poi inserire anche tutti gli
altri oggetti visti attraverso l’interfaccia che manipola i file come le fifo, i link, i socket e gli stessi
file di dispositivo (questi ultimi, per convenzione, sono inseriti nella directory /dev).
Il nome completo di un file viene chiamato pathname ed il procedimento con cui si individua
il file a cui esso fa riferimento è chiamato risoluzione del nome (filename resolution o pathname
resolution). La risoluzione viene fatta esaminando il pathname da sinistra a destra e localizzando
ogni nome nella directory indicata dal nome precedente usando il carattere “/” come separatore3 :
ovviamente, perché il procedimento funzioni, occorre che i nomi indicati come directory esistano
e siano effettivamente directory, inoltre i permessi (si veda sez. 5.3) devono consentire l’accesso
all’intero pathname.
Se il pathname comincia con il carattere “/” la ricerca parte dalla directory radice del processo;
questa, a meno di un chroot (su cui torneremo in sez. 5.4.5) è la stessa per tutti i processi ed
equivale alla directory radice dell’albero dei file: 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 pathname relativo.
I nomi “.” e “..” hanno un significato 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
coincida con la directory radice, allora il riferimento è a se stessa.
Una delle differenze principali con altri sistemi operativi (come il VMS o Windows) è che
per Unix tutti i file di dati sono identici e contengono un flusso continuo di byte. Non esiste cioè
differenza per come vengono visti dal sistema file di diverso contenuto o formato (come nel caso
di quella fra file di testo e binari che c’è in Windows) né c’è una strutturazione a record per il
cosiddetto “accesso diretto” come nel caso del VMS.5
Una seconda differenza è nel formato dei file di testo: in Unix la fine riga è codificata in
maniera diversa da Windows o dal vecchio MacOS, in particolare il fine riga è il carattere LF
(o \n) al posto del CR (\r) del vecchio MacOS e del CR LF di Windows.6 Questo può causare
alcuni problemi qualora nei programmi si facciano assunzioni sul terminatore della riga.
Si ricordi infine che un kernel Unix non fornisce nessun supporto per la tipizzazione dei file
di dati e che non c’è nessun supporto del sistema per le estensioni come parte del filesystem.7
Ciò nonostante molti programmi adottano delle convenzioni per i nomi dei file, ad esempio il
codice C normalmente si mette in file con l’estensione .c; un’altra tecnica molto usata è quella
di utilizzare i primi 4 byte del file per memorizzare un magic number che classifichi il contenuto;
entrambe queste tecniche, per quanto usate ed accettate in maniera diffusa, restano solo delle
convenzioni il cui rispetto è demandato alle applicazioni stesse.
La prima è l’interfaccia standard di Unix, quella che il manuale delle glibc chiama interfaccia
dei descrittori di file (o file descriptor ). È un’interfaccia specifica dei sistemi unix-like e fornisce
un accesso non bufferizzato; la tratteremo in dettaglio in cap. 6.
L’interfaccia è primitiva ed essenziale, l’accesso viene detto non bufferizzato in quanto la
lettura e la scrittura vengono eseguite chiamando direttamente le system call del kernel (in realtà
il kernel effettua al suo interno alcune bufferizzazioni per aumentare l’efficienza nell’accesso ai
dispositivi); i file descriptor sono rappresentati da numeri interi (cioè semplici variabili di tipo
int). L’interfaccia è definita nell’header unistd.h.
La seconda interfaccia è quella che il manuale della glibc chiama degli stream.8 Essa fornisce
funzioni più evolute e un accesso bufferizzato (controllato dalla implementazione fatta dalle
glibc), la tratteremo in dettaglio nel cap. 7.
Questa è l’interfaccia standard specificata dall’ANSI C e perciò si trova anche su tutti i
sistemi non Unix. Gli stream sono oggetti complessi e sono rappresentati da puntatori ad un
opportuna struttura definita dalle librerie del C; si accede ad essi sempre in maniera indiretta
utilizzando il tipo FILE *. L’interfaccia è definita nell’header stdio.h.
Entrambe le interfacce possono essere usate per l’accesso ai file come agli altri oggetti del
VFS (fifo, 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 l’interfaccia standard di Unix con i file descriptor. Allo stesso
modo devono essere usati i file descriptor se si vuole ricorrere a modalità speciali di I/O come
il file locking o l’I/O non-bloccante (vedi cap. 12).
Gli stream forniscono un’interfaccia di alto livello costruita sopra quella dei file descriptor, che
permette di poter scegliere tra diversi stili di bufferizzazione. Il maggior vantaggio degli stream
è che l’interfaccia per le operazioni di input/output è enormemente più ricca di quella dei file
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 l’input e
l’output adatte per manipolare anche i dati in forma di linee o singoli caratteri.
In ogni caso, dato che gli stream sono implementati sopra l’interfaccia standard di Unix, è
sempre possibile estrarre il file descriptor da uno stream ed eseguirvi operazioni di basso livello,
o associare in un secondo tempo uno stream ad un file descriptor.
In generale, se non necessitano specificatamente le funzionalità di basso livello, è opportuno
usare sempre gli stream per la loro maggiore portabilità, essendo questi ultimi definiti nello
standard ANSI C; l’interfaccia con i file descriptor infatti segue solo lo standard POSIX.1 dei
sistemi Unix, ed è pertanto di portabilità più limitata.
di indirezione che permette di collegare le operazioni di manipolazione sui file alle operazioni
di I/O, e gestisce l’organizzazione di queste ultime nei vari modi in cui i diversi filesystem le
effettuano, permettendo la coesistenza di filesystem differenti all’interno dello stesso albero delle
directory.
Quando un processo esegue una system call che opera su un file, il kernel chiama sempre una
funzione implementata nel VFS; la funzione eseguirà le manipolazioni sulle strutture generiche e
utilizzerà poi la chiamata alle opportune funzioni del filesystem specifico a cui si fa riferimento.
Saranno queste a chiamare le funzioni di più basso livello che eseguono le operazioni di I/O sul
dispositivo fisico, secondo lo schema riportato in fig. 4.1.
Il VFS definisce un insieme di funzioni che tutti i filesystem devono implementare. L’inter-
faccia comprende tutte le funzioni che riguardano i file; le operazioni sono suddivise su tre tipi
di oggetti: filesystem, inode e file, corrispondenti a tre apposite strutture definite nel kernel.
Il VFS usa una tabella mantenuta dal kernel che contiene il nome di ciascun filesystem suppor-
tato: quando si vuole inserire il supporto di un nuovo filesystem tutto quello che occorre è chia-
mare la funzione register_filesystem passandole un’apposita struttura file_system_type
che contiene i dettagli per il riferimento all’implementazione del medesimo, che sarà aggiunta
alla citata tabella.
In questo modo quando viene effettuata la richiesta di montare un nuovo disco (o qualunque
altro block device che può contenere un filesystem), il VFS può ricavare dalla citata tabella il
puntatore alle funzioni da chiamare nelle operazioni di montaggio. Quest’ultima è responsabile
di leggere da disco il superblock (vedi sez. 4.2.4), inizializzare tutte le variabili interne e restituire
uno speciale descrittore dei filesystem montati al VFS; attraverso quest’ultimo diventa possibile
accedere alle funzioni specifiche per l’uso di quel filesystem.
102 CAPITOLO 4. L’ARCHITETTURA DEI FILE
Il primo oggetto usato dal VFS è il descrittore di filesystem, un puntatore ad una apposita
struttura che contiene vari dati come le informazioni comuni ad ogni filesystem, i dati privati
relativi a quel filesystem specifico, e i puntatori alle funzioni del kernel relative al filesystem.
Il VFS può cosı̀ usare le funzioni contenute nel filesystem descriptor per accedere alle funzioni
specifiche di quel filesystem.
Gli altri due descrittori usati dal VFS sono relativi agli altri due oggetti su cui è strutturata
l’interfaccia. Ciascuno di essi contiene le informazioni relative al file in uso, insieme ai puntatori
alle funzioni dello specifico filesystem usate per l’accesso dal VFS; in particolare il descrittore
dell’inode contiene i puntatori alle funzioni che possono essere usate su qualunque file (come
link, stat e open), mentre il descrittore di file contiene i puntatori alle funzioni che vengono
usate sui file già aperti.
Funzione Operazione
open Apre il file (vedi sez. 6.2.1).
read Legge dal file (vedi sez. 6.2.4).
write Scrive sul file (vedi sez. 6.2.5).
llseek Sposta la posizione corrente sul file (vedi sez. 6.2.3).
ioctl Accede alle operazioni di controllo (vedi sez. 6.3.7).
readdir Legge il contenuto di una directory (vedi sez. 5.1.6).
poll Usata nell’I/O multiplexing (vedi sez. 12.2).
mmap Mappa il file in memoria (vedi sez. 12.4.1).
release Chiamata quando l’ultimo riferimento a un file aperto è
chiuso.
fsync Sincronizza il contenuto del file (vedi sez. 6.3.3).
fasync Abilita l’I/O asincrono (vedi sez. 12.3.3) sul file.
disponibili, però con questo sistema l’utilizzo di diversi filesystem (come quelli usati da Windows
o MacOS) è immediato e (relativamente) trasparente per l’utente ed il programmatore.
Da fig. 4.3 si evidenziano alcune delle caratteristiche di base di un filesystem, sulle quali è
bene porre attenzione visto che sono fondamentali per capire il funzionamento delle funzioni che
manipolano i file e le directory che tratteremo nel prossimo capitolo; in particolare è opportuno
ricordare sempre che:
4. Quando si cambia nome ad un file senza cambiare filesystem, il contenuto del file non viene
spostato fisicamente, viene semplicemente creata una nuova voce per l’inode in questione e
rimossa la vecchia (questa è la modalità in cui opera normalmente il comando mv attraverso
la funzione rename). Questa operazione non modifica minimamente neanche l’inode del file
dato che non si opera su questo ma sulla directory che lo contiene.
5. Gli inode dei file, che contengono i metadati ed i blocchi di spazio disco, che contengono
i dati, sono risorse indipendenti ed in genere vengono gestite come tali anche dai diversi
filesystem; è pertanto possibile sia esaurire lo spazio disco (caso più comune) che lo spazio
per gli inode, nel primo caso non sarà possibile allocare ulteriore spazio, ma si potranno
creare file (vuoti), nel secondo non si potranno creare nuovi file, ma si potranno estendere
quelli che ci sono.
Infine si noti che, essendo file pure loro, il numero di riferimenti esiste anche per le directory;
per cui, se a partire dalla situazione mostrata in fig. 4.3 creiamo una nuova directory img nella
directory gapil, avremo una situazione come quella in fig. 4.4, dove per chiarezza abbiamo
aggiunto dei numeri di inode.
La nuova directory avrà allora un numero di riferimenti pari a due, in quanto è referenziata
dalla directory da cui si era partiti (in cui è inserita la nuova voce che fa riferimento a img) e
dalla voce “.” che è sempre inserita in ogni directory; questo vale sempre per ogni directory che
non contenga a sua volta altre directory. Al contempo, la directory da cui si era partiti avrà un
numero di riferimenti di almeno tre, in quanto adesso sarà referenziata anche dalla voce “..” di
img.
è stato dichiarato stabile il nuovo filsesystem ext4, ulteriore evoluzione di ext3 dotato di molte
caratteristiche avanzate, che sta iniziando a sostituirlo gradualmente.
Il filesystem ext2 nasce come filesystem nativo di Linux a partire dalle prime versioni del
kernel e supporta tutte le caratteristiche di un filesystem standard Unix: è in grado di gestire
nomi di file lunghi (256 caratteri, estensibili a 1012) e supporta una dimensione massima dei file
fino a 4 Tb. I successivi filesystem ext3 ed ext4 sono evoluzioni di questo filesystem, e sia pure
con molti miglioramenti ed estensioni significative ne mantengono in sostanza le caratteristiche
fondamentali.
Oltre alle caratteristiche standard, ext2 fornisce alcune estensioni che non sono presenti su
un classico filesystem di tipo Unix; le principali sono le seguenti:
• l’amministratore può scegliere la dimensione dei blocchi del filesystem in fase di creazione,
a seconda delle sue esigenze (blocchi più grandi permettono un accesso più veloce, ma
sprecano più spazio disco).
• il filesystem implementa link simbolici veloci, in cui il nome del file non è salvato su un
blocco, ma tenuto all’interno dell’inode (evitando letture multiple e spreco di spazio), non
tutti i nomi però possono essere gestiti cosı̀ per limiti di spazio (il limite è 60 caratteri).
• vengono supportati i file immutabili (che possono solo essere letti) per la protezione di file
di configurazione sensibili, o file append-only che possono essere aperti in scrittura solo per
aggiungere dati (caratteristica utilizzabile per la protezione dei file di log).
La struttura di ext2 è stata ispirata a quella del filesystem di BSD: un filesystem è composto
da un insieme di blocchi, la struttura generale è quella riportata in fig. 4.3, in cui la partizione
è divisa in gruppi di blocchi.10
Ciascun gruppo di blocchi contiene una copia delle informazioni essenziali del filesystem
(superblock e descrittore del filesystem sono quindi ridondati) per una maggiore affidabilità e
possibilità di recupero in caso di corruzione del superblock principale. L’utilizzo di raggruppa-
menti di blocchi ha inoltre degli effetti 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. Cia-
scuna voce della lista contiene il numero di inode , la sua lunghezza, il nome del file e la sua
lunghezza, secondo lo schema in fig. 4.5; in questo modo è possibile implementare nomi per i file
anche molto lunghi (fino a 1024 caratteri) senza sprecare spazio disco.
Con l’introduzione del filesystem ext3 sono state introdotte anche alcune modifiche strut-
turali, la principale di queste è quella che ext3 è un filesystem jounrnaled, è cioè in grado di
eseguire una registrazione delle operazioni di scrittura su un giornale (uno speciale file interno)
10
non si confonda questa definizione con quella riportata in fig. 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 filesystem ext2, definita nel file ext2_fs.h nella directory include/linux dei sorgenti del
kernel.
4.2. L’ARCHITETTURA DELLA GESTIONE DEI FILE 107
in modo da poter garantire il ripristino della coerenza dei dati del filesystem11 in brevissimo
tempo in caso di interruzione improvvisa della corrente o di crollo del sistema che abbia causato
una interruzione della scrittura dei dati sul disco.
Oltre a questo ext3 introduce ulteriori modifiche volte a migliorare sia le prestazioni che la
semplicità di gestione del filesystem, in particolare per le directory si è passato all’uso di alberi
binari con indicizzazione tramite hash al posto delle linked list, ottenendo un forte guadagno di
prestazioni in caso di directory contenenti un gran numero di file.
11
si noti bene che si è parlato di dati del filesystem, non di dati nel filesystem, quello di cui viene garantito un
veloce ripristino è relativo ai dati della struttura interna del filesystem, non di eventuali dati contenuti nei file che
potrebbero essere stati persi.
108 CAPITOLO 4. L’ARCHITETTURA DEI FILE
Capitolo 5
File e directory
In questo capitolo tratteremo in dettaglio le modalità con cui si gestiscono file e directory, inizian-
do dalle funzioni di libreria che si usano per copiarli, spostarli e cambiarne i nomi. Esamineremo
poi l’interfaccia che permette la manipolazione dei vari attributi di file e directory ed alla fine
prenderemo in esame la struttura di base del sistema delle protezioni e del controllo dell’acces-
so ai file e le successive estensioni (Extended Attributes, ACL, quote disco, capabilities). Tutto
quello che riguarda invece la manipolazione del contenuto dei file è lasciato ai capitoli successivi.
109
110 CAPITOLO 5. FILE E DIRECTORY
Per aggiungere ad una directory una voce che faccia riferimento ad un inode già esistente si
utilizza la funzione link; si suole chiamare questo tipo di associazione un collegamento diretto,
o hard link. Il prototipo della funzione è il seguente:
#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 i file oldpath e newpath non fanno riferimento ad un filesystem montato sullo stesso
mount point.
EPERM il filesystem che contiene oldpath e newpath non supporta i link diretti o è una
directory.
EEXIST un file (o una directory) di nome newpath esiste già.
EMLINK ci sono troppi link al file oldpath (il numero massimo è specificato dalla variabile
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 file indicato da oldpath.
Per quanto detto la creazione di un nuovo collegamento diretto non copia il contenuto del file,
ma si limita a creare una voce nella directory specificata da newpath e ad aumentare di uno il
numero di riferimenti al file (riportato nel campo st_nlink della struttura stat, vedi sez. 5.2.1)
aggiungendo il nuovo nome ai precedenti. Si noti che uno stesso file può essere cosı̀ chiamato con
vari nomi in diverse directory.
Per quanto dicevamo in sez. 4.2.3 la creazione di un collegamento diretto è possibile solo se
entrambi i pathname sono nello stesso filesystem; inoltre il filesystem deve supportare i collega-
menti diretti (il meccanismo non è disponibile ad esempio con il filesystem vfat di Windows). In
realtà la funzione ha un ulteriore requisito, e cioè che non solo che i due file siano sullo stesso
filesystem, ma anche che si faccia riferimento ad essi sullo stesso mount point.1
La funzione inoltre opera sia sui file ordinari che sugli altri oggetti del filesystem, con l’ec-
cezione delle directory. In alcune versioni di Unix solo l’amministratore è in grado di creare un
collegamento diretto ad un’altra directory: questo viene fatto perché con una tale operazione è
possibile creare dei loop nel filesystem (vedi l’esempio 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 filesystem).
Data la pericolosità di questa operazione e la disponibilità dei link simbolici che possono
fornire la stessa funzionalità senza questi problemi, nel caso di Linux questa capacità è stata
completamente disabilitata, e al tentativo di creare un link diretto ad una directory la funzione
link restituisce l’errore EPERM.
Un ulteriore comportamento peculiare di Linux è quello in cui si crea un hard link ad un
link simbolico. In questo caso lo standard POSIX prevederebbe che quest’ultimo venga risolto e
che il collegamento sia effettuato rispetto al file cui esso punta, e che venga riportato un errore
qualora questo non esista o non sia un file. Questo era anche il comportamento iniziale di Linux
ma a partire dai kernel della serie 2.0.x2 è stato adottato un comportamento che non segue più
lo standard per cui l’hard link viene creato rispetto al link simbolico, e non al file cui questo
punta.
1
si tenga presente infatti (vedi sez. 8.2.2) che a partire dal kernel 2.4 uno stesso filesystem può essere montato
più volte su directory diverse.
2
per la precisione il comportamento era quello previsto dallo standard POSIX fino al kernel di sviluppo 1.3.56, ed
è stato temporaneamente ripristinato anche durante lo sviluppo della serie 2.1.x, per poi tornare al comportamento
attuale fino ad oggi (per riferimento si veda http://lwn.net/Articles/293902).
5.1. LA GESTIONE DI FILE E DIRECTORY 111
La ragione di questa differenza rispetto allo standard, presente anche in altri sistemi unix-
like, sono dovute al fatto che un link simbolico può fare riferimento anche ad un file non esistente
o a una directory, per i quali l’hard link non può essere creato, per cui la scelta di seguire il link
simbolico è stata ritenuta una scelta scorretta nella progettazione dell’interfaccia. Infatti se non
ci fosse il comportamento adottato da Linux sarebbe impossibile creare un hard link ad un link
simbolico, perché la funzione lo risolverebbe e l’hard link verrebbe creato verso la destinazione.
Invece evitando di seguire lo standard l’operazione diventa possibile, ed anche il comportamento
della funzione risulta molto più comprensibile. Tanto più che se proprio se si vuole creare un
hard link rispetto alla destinazione di un link simbolico è sempre possibile farlo direttamente.3
La rimozione di un file (o più precisamente della voce che lo referenzia all’interno di una
directory) si effettua con la funzione unlink; il suo prototipo è il seguente:
#include <unistd.h>
int unlink(const char *pathname)
Cancella un file.
La funzione restituisce zero in caso di successo e -1 per un errore, nel qual caso il file non viene
toccato. La variabile errno viene impostata secondo i seguenti codici di errore:
4
EISDIR pathname si riferisce ad una directory.
EROFS pathname è su un filesystem montato in sola lettura.
EISDIR pathname fa riferimento a una directory.
ed inoltre: EACCES, EFAULT, ENOENT, ENOTDIR, ENOMEM, EROFS, ELOOP, EIO.
traccia in nessuna directory, e lo spazio occupato su disco viene immediatamente rilasciato alla
conclusione del processo (quando tutti i file vengono chiusi).
#include <stdio.h>
int remove(const char *pathname)
Cancella un nome dal filesystem.
La funzione restituisce zero in caso di successo e -1 per un errore, nel qual caso il file non viene
toccato.
I codici di errore riportati in errno sono quelli della chiamata utilizzata, pertanto si può fare
riferimento a quanto illustrato nelle descrizioni di unlink e rmdir.
La funzione utilizza la funzione unlink6 per cancellare i file 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 file ancora in uso.
Per cambiare nome ad un file o a una directory (che devono comunque essere nello stesso
filesystem) si usa invece la funzione rename,7 il cui prototipo è:
#include <stdio.h>
int rename(const char *oldpath, const char *newpath)
Rinomina un file.
La funzione restituisce zero in caso di successo e -1 per un errore, nel qual caso il file non viene
toccato. La variabile errno viene impostata secondo i seguenti codici di errore:
EISDIR newpath è una directory mentre oldpath non è una directory.
EXDEV oldpath e newpath non sono sullo stesso filesystem.
ENOTEMPTY newpath è una directory già esistente e non vuota.
EBUSY 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).
EINVAL newpath contiene un prefisso di oldpath o più in generale si è cercato di creare una
directory come sotto-directory di se stessa.
ENOTDIR uno dei componenti dei pathname non è una directory o oldpath è una directory e
newpath esiste e non è una directory.
ed inoltre EACCES, EPERM, EMLINK, ENOENT, ENOMEM, EROFS, ELOOP e ENOSPC.
Se oldpath è una directory allora newpath, se esiste, deve essere una directory vuota, al-
trimenti si avranno gli errori ENOTDIR (se non è una directory) o ENOTEMPTY (se non è vuota).
Chiaramente newpath non può contenere oldpath altrimenti si avrà un errore EINVAL.
Se oldpath si riferisce ad un link simbolico questo sarà rinominato; se newpath è un link
simbolico verrà cancellato come qualunque altro file. Infine qualora oldpath e newpath siano
due nomi dello stesso file 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.
Il vantaggio nell’uso di questa funzione al posto della chiamata successiva di link e unlink
è che l’operazione è eseguita atomicamente, non può esistere cioè nessun istante in cui un altro
processo può trovare attivi entrambi i nomi dello stesso file, o, in caso di sostituzione di un file
esistente, non trovare quest’ultimo prima che la sostituzione sia stata eseguita.
In ogni caso se newpath esiste e l’operazione fallisce per un qualche motivo (come un crash
del kernel), rename garantisce di lasciare presente un’istanza di newpath. Tuttavia nella sovra-
scrittura potrà esistere una finestra in cui sia oldpath che newpath fanno riferimento allo stesso
file.
#include <unistd.h>
int symlink(const char *oldpath, const char *newpath)
Crea un nuovo link simbolico di nome newpath il cui contenuto è oldpath.
La funzione restituisce zero in caso di successo e -1 per un errore, nel qual caso la variabile errno
assumerà i valori:
EPERM il filesystem che contiene newpath non supporta i link simbolici.
ENOENT una componente di newpath non esiste o oldpath è una stringa vuota.
EEXIST esiste già un file newpath.
EROFS newpath è su un filesystem montato in sola lettura.
ed inoltre EFAULT, EACCES, ENAMETOOLONG, ENOTDIR, ENOMEM, ELOOP, ENOSPC e EIO.
Si tenga presente che la funzione non effettua nessun controllo sull’esistenza di un file di nome
oldpath, ma si limita ad inserire quella stringa nel link simbolico. Pertanto un link simbolico
può anche riferirsi ad un file che non esiste: in questo caso si ha quello che viene chiamato un
dangling link, letteralmente un link ciondolante.
8
è uno dei diversi tipi di file visti in tab. 4.1, contrassegnato come tale nell’inode, e riconoscibile dal valore del
campo st_mode della struttura stat (vedi sez. 5.2.1).
114 CAPITOLO 5. FILE E DIRECTORY
Come accennato i link simbolici sono risolti automaticamente dal kernel all’invocazione delle
varie system call; in tab. 5.1 si è riportato un elenco dei comportamenti delle varie funzioni di
libreria che operano sui file nei confronti della risoluzione dei link simbolici, specificando quali
seguono il link simbolico e quali invece possono operare direttamente sul suo contenuto.
Si noti che non si è specificato il comportamento delle funzioni che operano con i file de-
scriptor, in quanto la risoluzione del link simbolico viene in genere effettuata dalla funzione che
restituisce il file descriptor (normalmente la open, vedi sez. 6.2.1) e tutte le operazioni seguenti
fanno riferimento solo a quest’ultimo.
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 file a cui esso fa
riferimento. Quando si vuole leggere il contenuto di un link simbolico si usa la funzione readlink,
il cui prototipo è:
#include <unistd.h>
int readlink(const char *path, char *buff, size_t size)
Legge il contenuto del link simbolico indicato da path nel buffer 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:
EINVAL path non è un link simbolico o size non è positiva.
ed inoltre ENOTDIR, ENAMETOOLONG, ENOENT, EACCES, ELOOP, EIO, EFAULT e ENOMEM.
La funzione apre il link simbolico, ne legge il contenuto, lo scrive nel buffer, e lo richiude.
Si tenga presente che la funzione non termina la stringa con un carattere nullo e la tronca alla
dimensione specificata da size per evitare di sovrascrivere oltre le dimensioni del buffer.
Un caso comune che si può avere con i link simbolici è la creazione dei cosiddetti loop. La
situazione è illustrata in fig. 5.1, che riporta la struttura della directory /boot. Come si vede si
è creato al suo interno un link simbolico che punta di nuovo a /boot.10
9
a partire dalla serie 2.0, e contrariamente a quanto indicato dallo standard POSIX, si veda quanto detto in
sez. 5.1.1.
10
il loop mostrato in fig. 5.1 è un usato per poter permettere a grub (un bootloader in grado di leggere di-
5.1. LA GESTIONE DI FILE E DIRECTORY 115
Figura 5.1: Esempio di loop nel filesystem creato con un link simbolico.
Questo può causare problemi per tutti quei programmi che effettuano la scansione di una di-
rectory senza tener conto dei link simbolici, ad esempio se lanciassimo un comando del tipo grep
-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 è specificato dalla
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ò
fare riferimento anche ad un file che non esiste; ad esempio possiamo creare un file 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
scrittura temporaneo verrà creato /tmp/tmp_file e scritto; ma accedendo in sola lettura 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 un’ispezione con ls ci mostrerebbe invece
l’esistenza di temporaneo.
rettamente da vari filesystem il file da lanciare come sistema operativo) di vedere i file 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, all’avvio, vede come radice).
116 CAPITOLO 5. FILE E DIRECTORY
La funzione restituisce zero in caso di successo e -1 per un errore, nel qual caso errno assumerà i
valori:
EEXIST un file (o una directory) con quel nome esiste di già.
EACCES non c’è il permesso di scrittura per la directory in cui si vuole inserire la nuova
directory.
EMLINK la directory in cui si vuole creare la nuova directory contiene troppi file; sotto Linux
questo normalmente non avviene perché il filesystem standard consente la creazione di
un numero di file maggiore di quelli che possono essere contenuti nel disco, ma potendo
avere a che fare anche con filesystem di altri sistemi questo errore può presentarsi.
ENOSPC non c’è abbastanza spazio sul file system per creare la nuova directory o si è esaurita
la quota disco dell’utente.
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
presenti in ogni directory (cioè “.” e “..”), con il nome indicato dall’argomento dirname. Il
nome può essere indicato sia come pathname assoluto che come pathname relativo.
I permessi di accesso (vedi sez. 5.3) con cui la directory viene creata sono specificati dal-
l’argomento mode, i cui possibili valori sono riportati in tab. 5.9; si tenga presente che questi
sono modificati dalla maschera di creazione dei file (si veda sez. 5.3.3). La titolarità della nuova
directory è impostata secondo quanto riportato in sez. 5.3.4.
La funzione che permette la cancellazione di una directory è invece rmdir, ed il suo prototipo
è:
#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
valori:
EPERM il filesystem non supporta la cancellazione di directory, oppure la directory che contiene
dirname ha lo sticky bit impostato e l’user-ID effettivo del processo non corrisponde
al proprietario della directory.
EACCES non c’è il permesso di scrittura per la directory che contiene la directory che si vuo-
le cancellare, o non c’è il permesso di attraversare (esecuzione) una delle directory
specificate in dirname.
EBUSY la directory specificata è la directory di lavoro o la radice di qualche processo.
ENOTEMPTY la directory non è vuota.
ed inoltre anche EFAULT, ENAMETOOLONG, ENOENT, ENOTDIR, ENOMEM, ELOOP, EROFS.
La funzione cancella la directory dirname, che deve essere vuota (la directory deve cioè
contenere soltanto le due voci standard “.” e “..”). Il nome può essere indicato con il pathname
assoluto o relativo.
11
questo è quello che permette anche, attraverso l’uso del VFS, l’utilizzo di diversi formati per la gestione dei
suddetti elenchi, dalle semplici liste a strutture complesse come alberi binari, hash, ecc. che consentono una ricerca
veloce quando il numero di file è molto grande.
5.1. LA GESTIONE DI FILE E DIRECTORY 117
La modalità con cui avviene la cancellazione è analoga a quella di unlink: fintanto che il
numero di link all’inode 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 all’inode e nel caso sia l’ultimo, pure le voci standard “.” e “..”, a
questo punto il kernel non consentirà di creare più nuovi file nella directory.
#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 del tipo specificato sul filesystem.
La funzione restituisce zero in caso di successo e -1 per un errore, nel qual caso errno assumerà i
valori:
EPERM non si hanno privilegi sufficienti a creare l’inode, o il filesystem su cui si è cercato di
creare pathname non supporta l’operazione.
EINVAL il valore di mode non indica un file, una fifo, un socket o un dispositivo.
EEXIST pathname esiste già o è un link simbolico.
ed inoltre anche EFAULT, EACCES, ENAMETOOLONG, ENOENT, ENOTDIR, ENOMEM, ELOOP, ENOSPC, EROFS.
La funzione, come suggerisce il nome, permette di creare un “nodo” sul filesystem, e viene
in genere utilizzata per creare i file di dispositivo, ma si può usare anche per creare file regolari.
L’argomento mode specifica sia il tipo di file che si vuole creare che i relativi permessi, secondo i
valori riportati in tab. 5.4, che vanno combinati con un OR binario. I permessi sono comunque
modificati nella maniera usuale dal valore di umask (si veda sez. 5.3.3).
Per il tipo di file può essere specificato solo uno fra i seguenti valori: S_IFREG per un file
regolare (che sarà creato vuoto), S_IFBLK per un dispositivo a blocchi, S_IFCHR per un dispositivo
a caratteri, S_IFSOCK per un socket e S_IFIFO per una fifo;12 un valore diverso comporterà
l’errore EINVAL.
Qualora si sia specificato in mode un file di dispositivo (vale a dire o S_IFBLK o S_IFCHR), il
valore di dev dovrà essere usato per indicare a quale dispositivo si fa riferimento, altrimenti il
suo valore verrà ignorato. Solo l’amministratore può creare un file di dispositivo usando questa
funzione (il processo deve avere la capability CAP_MKNOD), ma in Linux13 l’uso per la creazione
di un file ordinario, di una fifo o di un socket è consentito anche agli utenti normali.
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 filesystem (si veda sez. 5.3.4) in cui si va a creare l’inode.
12
con Linux la funzione non può essere usata per creare directory o link simbolici, si dovranno usare le funzioni
mkdir e symlink a questo dedicate.
13
questo è un comportamento specifico di Linux, la funzione non è prevista dallo standard POSIX.1 originale,
mentre è presente in SVr4 e 4.4BSD, ma esistono differenze nei comportamenti e nei codici di errore, tanto che
questa è stata introdotta in POSIX.1-2001 con una nota che la definisce portabile solo quando viene usata per
creare delle fifo, ma comunque deprecata essendo utilizzabile a tale scopo la specifica mkfifo.
118 CAPITOLO 5. FILE E DIRECTORY
Nella creazione di un file di dispositivo occorre poi specificare correttamente il valore di dev;
questo infatti è di tipo dev_t, che è un tipo primitivo (vedi tab. 1.2) riservato per indicare
un numero di dispositivo; il kernel infatti identifica ciascun dispositivo con un valore numerico.
Originariamente questo era un intero a 16 bit diviso in due parti di 8 bit chiamate rispettivamente
major number e minor number, che sono poi i due numeri mostrati dal comando ls -l al posto
della dimensione quando lo si esegue su un file di dispositivo.
Il major number identifica una classe di dispositivi (ad esempio la seriale, o i dischi IDE)
e serve in sostanza per indicare al kernel quale è il modulo che gestisce quella classe di dispo-
sitivi; per identificare uno specifico dispositivo di quella classe (ad esempio una singola por-
ta seriale, o una partizione di un disco) si usa invece il minor number. L’elenco aggiornato
di questi numeri con le relative corrispondenze ai vari dispositivi può essere trovato nel file
Documentation/devices.txt allegato alla documentazione dei sorgenti del kernel.
Data la crescita nel numero di dispositivi supportati, ben presto il limite massimo di 256 si
è rivelato troppo basso, e nel passaggio dai kernel della serie 2.4 alla serie 2.6 è stata aumentata
a 32 bit la dimensione del tipo dev_t, con delle dimensioni passate a 12 bit per il major number
e 20 bit per il minor number. La transizione però ha anche comportato il passaggio di dev_t a
tipo opaco, e la necessità di specificare il numero tramite delle opportune macro, cosı̀ da non
avere problemi di compatibilità con eventuali ulteriori estensioni.
Le macro sono definite nel file sys/sysmacros.h, che viene automaticamente incluso quando
si include sys/types.h; si possono pertanto ottenere i valori del major number e minor number
di un dispositivo rispettivamente con le macro major e minor:
#include <sys/types.h>
int major(dev_t dev)
Restituisce il major number del dispositivo dev.
int minor(dev_t dev)
Restituisce il minor number del dispositivo dev.
mentre una volta che siano noti major number e minor number si potrà costruire il relativo
identificativo con la macro makedev:
#include <sys/types.h>
dev_t minor(int major, int minor)
Restituisce l’identificativo di un dispositivo dati major number e minor number.
Infine con lo standard POSIX.1-2001 è stata introdotta una funzione specifica per creare una
fifo (tratteremo le fifo in in sez. 11.1.4); la funzione è mkfifo ed il suo prototipo è:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode)
Crea una fifo.
La funzione restituisce zero in caso di successo e -1 per un errore, nel qual caso errno assumerà i
valori EACCES, EEXIST, ENAMETOOLONG, ENOENT, ENOSPC, ENOTDIR e EROFS.
La funzione crea la fifo pathname con i permessi mode. Come per mknod il file pathname
non deve esistere (neanche come link simbolico); al solito i permessi specificati da mode vengono
modificati dal valore di umask.
Ma se la scrittura e l’aggiornamento dei dati delle directory è compito del kernel, sono molte
le situazioni in cui i processi necessitano di poterne leggere il contenuto. Benché questo possa
essere fatto direttamente (vedremo in sez. 6.2.1 che è possibile aprire una directory come se
fosse un file, anche se solo in sola lettura) in generale il formato con cui esse sono scritte può
dipendere dal tipo di filesystem, tanto che, come riportato in tab. 4.2, il VFS del kernel prevede
una apposita funzione per la lettura delle directory.
Tutto questo si riflette nello standard POSIX14 che ha introdotto una apposita interfaccia per
la lettura delle directory, basata sui cosiddetti directory stream (chiamati cosı̀ per l’analogia con
i file stream dell’interfaccia standard ANSI C di cap. 7). La prima funzione di questa interfaccia
è opendir, il cui prototipo è:
#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.
#include <sys/types.h>
#include <dirent.h>
int dirfd(DIR * dir)
Restituisce il file descriptor associato ad un directory stream.
La funzione restituisce il file descriptor (un valore positivo) in caso di successo e -1 in caso di
errore.
La funzione restituisce il file descriptor associato al directory stream dir. Di solito si utilizza
questa funzione in abbinamento a funzioni che operano sui file descriptor, ad esempio si potrà
usare fstat per ottenere le proprietà della directory, o fchdir per spostare su di essa la directory
di lavoro (vedi sez. 5.1.7).
Viceversa se si è aperto un file descriptor corrispondente ad una directory è possibile asso-
ciarvi un directory stream con la funzione fdopendir,16 il cui prototipo è:
14
le funzioni erano presenti in SVr4 e 4.3BSD, la loro specifica è riportata in POSIX.1-2001.
15
questa funzione è una estensione introdotta con BSD 4.3-Reno ed è presente in Linux con le libc5 (a partire
dalla versione 5.1.2) e con le glibc ma non presente in POSIX fino alla revisione POSIX.1-2008, per questo per
poterla utilizzare fino alla versione 2.10 delle glibc era necessario definire le macro _BSD_SOURCE o _SVID_SOURCE,
dalla versione 2.10 si possono usare anche _POSIX_C_SOURCE >= 200809L o _XOPEN_SOURCE >= 700.
16
questa funzione è però disponibile solo a partire dalla versione 2.4 delle glibc, ed è stata introdotta nello
standard POSIX solo a partire dalla revisione POSIX.1-2008, prima della versione 2.10 delle glibc per poterla
utilizzare era necessario definire la macro _GNU_SOURCE, dalla versione 2.10 si possono usare anche _POSIX_C_SOURCE
>= 200809L o _XOPEN_SOURCE >= 700 .
120 CAPITOLO 5. FILE E DIRECTORY
#include <sys/types.h>
#include <dirent.h>
DIR * fdopendir(int fd)
Associa un directory stream al file descriptor fd.
La funzione restituisce un puntatore al directory stream in caso di successo e NULL per un errore,
nel qual caso errno assumerà il valore EBADF.
#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 directory stream non valido errno assumerà il valore EBADF, il valore NULL
viene restituito anche quando si raggiunge la fine dello stream.
La funzione legge la voce corrente nella directory, posizionandosi sulla voce successiva. Per-
tanto se si vuole leggere l’intero contenuto di una directory occorrerà ripetere l’esecuzione della
funzione fintanto che non si siano esaurite tutte le voci in essa presenti.
struct dirent {
ino_t d_ino ; /* inode number */
off_t d_off ; /* offset to the next dirent */
unsigned short int d_reclen ; /* length of this record */
unsigned char d_type ; /* type of file ;
by all file system types */
char d_name [256]; /* filename */
};
Figura 5.2: La struttura dirent per la lettura delle informazioni dei file.
I dati vengono memorizzati in una struttura dirent, la cui definizione è riportata in fig. 5.2.17
La funzione restituisce il puntatore alla struttura; si tenga presente però che quest’ultima è
allocata staticamente, per cui viene sovrascritta tutte le volte che si ripete la lettura di una voce
sullo stesso directory stream.
Di questa funzione esiste anche una versione rientrante, readdir_r,18 che non usa una
struttura allocata staticamente, e può essere utilizzata anche con i thread, il suo prototipo è:
17
la definizione è quella usata da Linux, che si trova nel file /usr/include/bits/dirent.h, essa non contempla
la presenza del campo d_namlen che indica la lunghezza del nome del file.
18
per usarla è necessario definire una qualunque delle macro _POSIX_C_SOURCE >= 1, _XOPEN_SOURCE,
_BSD_SOURCE, _SVID_SOURCE, _POSIX_SOURCE.
5.1. LA GESTIONE DI FILE E DIRECTORY 121
#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) l’indirizzo dove sono stati
salvati i dati, che di norma corrisponde a quello della struttura precedentemente allocata e
specificata dall’argomento entry, anche se non è assicurato che la funzione usi lo spazio fornito
dall’utente.
I vari campi di dirent contengono le informazioni relative alle voci presenti nella directory;
sia BSD che SVr4 prevedono che siano sempre presenti il campo d_name,19 che contiene il nome
del file nella forma di una stringa terminata da uno zero,20 ed il campo d_ino, che contiene il
numero di inode cui il file è associato e corrisponde al campo st_ino di stat.
La presenza di ulteriori campi opzionali oltre i due citati è segnalata dalla definizione di
altrettante macro nella forma _DIRENT_HAVE_D_XXX dove XXX è il nome del relativo campo; nel
caso di Linux sono pertanto definite le macro _DIRENT_HAVE_D_TYPE, _DIRENT_HAVE_D_OFF e
_DIRENT_HAVE_D_RECLEN, mentre non è definita la macro _DIRENT_HAVE_D_NAMLEN.
Tabella 5.2: Costanti che indicano i vari tipi di file nel campo d_type della struttura dirent.
Per quanto riguarda il significato dei campi opzionali, il campo d_type indica il tipo di file
(se fifo, directory, link simbolico, ecc.), e consente di evitare una successiva chiamata a lstat
per determinarlo. I suoi possibili valori sono riportati in tab. 5.2. Si tenga presente che questo
valore è disponibile solo per i filesystem che ne supportano la restituzione (fra questi i più noti
sono btrfs, ext2, ext3, e ext4), per gli altri si otterrà sempre il valore DT_UNKNOWN.21
Per la conversione da e verso l’analogo valore mantenuto dentro il campo st_mode di stat
sono definite anche due macro di conversione, IFTODT e DTTOIF:
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,
19
lo standard POSIX prevede invece solo la presenza del campo d_fileno, identico d_ino, che in Linux è definito
come alias di quest’ultimo; il campo d_name è considerato dipendente dall’implementazione.
20
lo standard POSIX non specifica una lunghezza, ma solo un limite NAME_MAX; in SVr4 la lunghezza del campo
è definita come NAME_MAX+1 che di norma porta al valore di 256 byte usato anche in Linux.
21
inoltre fino alla versione 2.1 delle glibc, pur essendo il campo d_type presente, il suo uso non era implementato,
e veniva restituito comunque il valore DT_UNKNOWN.
122 CAPITOLO 5. FILE E DIRECTORY
determinando la posizione delle varie voci, spostarsi all’interno dello stream usando la funzione
seekdir,22 il cui prototipo è:
#include <dirent.h>
void seekdir(DIR *dir, off_t offset)
Cambia la posizione all’interno di un directory stream.
La funzione non ritorna nulla e non segnala errori, è però necessario che il valore dell’ar-
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 quest’ultima è:23
#include <dirent.h>
long 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.
#include <sys/types.h>
#include <dirent.h>
void rewinddir(DIR *dir)
Si posiziona all’inizio di un directory stream.
Una volta completate le operazioni si può chiudere il directory stream, ed il file descriptor ad
esso associato, con la funzione closedir, il cui prototipo è:
#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 venne introdotta un’altra funzione che permette
di eseguire una scansione completa, con tanto di ricerca ed ordinamento, del contenuto di una
directory; la funzione è scandir24 ed il suo prototipo è:
#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.
Al solito, per la presenza fra gli argomenti di due puntatori a funzione, il prototipo non è
molto comprensibile; queste funzioni però sono quelle che controllano rispettivamente la selezione
di una voce (quella passata con l’argomento filter) e l’ordinamento di tutte le voci selezionate
(quella specificata dell’argomento compar).
22
sia questa funzione che telldir, sono estensioni prese da BSD, ed introdotte nello standard POSIX solo a
partire dalla revisione POSIX.1-2001, per poterle utilizzare deve essere definita una delle macro _XOPEN_SOURCE,
_BSD_SOURCE o _SVID_SOURCE.
23
prima delle glibc 2.1.1 la funzione restituiva un valore di tipo off_t, sostituito a partire dalla versione 2.1.2
da long per conformità a POSIX.1-2001.
24
in Linux questa funzione è stata introdotta fin dalle libc4 e richiede siano definite le macro _BSD_SOURCE o
_SVID_SOURCE.
5.1. LA GESTIONE DI FILE E DIRECTORY 123
La funzione legge tutte le voci della directory indicata dall’argomento dir, passando un
puntatore a ciascuna di esse (una struttura dirent) come argomento della funzione di selezione
specificata 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 specifichi un valore NULL
per l’argomento 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
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
positivo, nullo o negativo per indicarne l’ordinamento; alla fine l’indirizzo della lista ordinata dei
puntatori alle strutture dirent viene restituito nell’argomento namelist.25
Per l’ordinamento, vale a dire come valori possibili per l’argomento compar sono disponibili
due funzioni predefinite, 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 l’ordinamento 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 fin dalle libc4 26 e deve essere
specificata come argomento compar per ottenere un ordinamento alfabetico (secondo il valore
del campo d_name delle varie voci). Le glibc prevedono come estensione27 anche versionsort,
che ordina i nomi tenendo conto del numero di versione (cioè qualcosa per cui file10 viene
comunque dopo file4.)
Un semplice esempio dell’uso di queste funzioni è riportato in fig. 5.3, dove si è riportata la
sezione principale di un programma che, usando la funzione di scansione illustrata in fig. 5.4,
stampa i nomi dei file contenuti in una directory e la relativa dimensione (in sostanza una
versione semplificata del comando ls).
Il programma è estremamente semplice; in fig. 5.3 si è omessa la parte di gestione delle
opzioni (che prevede solo l’uso di una funzione per la stampa della sintassi, anch’essa omessa)
ma il codice completo potrà essere trovato coi sorgenti allegati nel file myls.c.
In sostanza tutto quello che fa il programma, dopo aver controllato (12-15) di avere almeno
un argomento (che indicherà la directory da esaminare) è chiamare (16) la funzione DirScan per
eseguire la scansione, usando la funzione do_ls (22-29) per fare tutto il lavoro.
Quest’ultima si limita (26) a chiamare stat sul file indicato dalla directory entry passata
come argomento (il cui nome è appunto direntry->d_name), memorizzando in una opportuna
struttura data i dati ad esso relativi, per poi provvedere (27) a stampare il nome del file e la
dimensione riportata in data.
Dato che la funzione verrà chiamata all’interno di DirScan per ogni voce presente questo è
sufficiente a stampare la lista completa dei file e delle relative dimensioni. Si noti infine 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 fig. 5.4. La funzione è
volutamente generica e permette di eseguire una funzione, passata come secondo argomento, su
tutte le voci di una directory. La funzione inizia con l’aprire (18-22) uno stream sulla directory
passata come primo argomento, stampando un messaggio in caso di errore.
25
la funzione alloca automaticamente la lista, e restituisce, come value result argument, l’indirizzo della stessa;
questo significa che namelist deve essere dichiarato come struct dirent **namelist ed alla funzione si deve
passare il suo indirizzo.
26
la versione delle libc4 e libc5 usa però come argomenti dei puntatori a delle strutture dirent; le glibc usano
il prototipo originario di BSD, mostrato anche nella definizione, che prevede puntatori a void.
27
le glibc, a partire dalla versione 2.1, effettuano anche l’ordinamento alfabetico tenendo conto delle varie
localizzazioni, usando strcoll al posto di strcmp.
124 CAPITOLO 5. FILE E DIRECTORY
Figura 5.3: Esempio di codice per eseguire la lista dei file contenuti in una directory.
Il passo successivo (23-24) è cambiare directory di lavoro (vedi sez. 5.1.7), usando in sequenza
le funzioni dirfd e fchdir (in realtà si sarebbe potuto usare direttamente chdir su dirname), in
modo che durante il successivo ciclo (26-30) sulle singole voci dello stream ci si trovi all’interno
della directory.28
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
una voce alla volta (26) all’interno di una istruzione di while e fintanto che si riceve una voce
valida, cioè un puntatore diverso da NULL, si esegue (27) la funzione di elaborazione compare (che
nel nostro caso sarà do_ls), ritornando con un codice di errore (28) qualora questa presenti una
anomalia, identificata da un codice di ritorno negativo. Una volta terminato il ciclo la funzione
si conclude con la chiusura (32) dello stream29 e la restituzione (32) del codice di operazioni
concluse con successo.
28
questo è essenziale al funzionamento della funzione do_ls, e ad ogni funzione che debba usare il campo d_name,
in quanto i nomi dei file memorizzati all’interno 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.
29
nel nostro caso, uscendo subito dopo la chiamata, questo non servirebbe, in generale però l’operazione è ne-
cessaria, dato che la funzione può essere invocata molte volte all’interno dello stesso processo, per cui non chiudere
i directory stream comporterebbe un consumo progressivo di risorse, con conseguente rischio di esaurimento delle
stesse.
5.1. LA GESTIONE DI FILE E DIRECTORY 125
Figura 5.4: Codice della funzione di scansione di una directory contenuta nel file DirScan.c.
Come accennato in sez. 3.2.2 a ciascun processo è associata una directory nel filesystem,30 che
è chiamata directory corrente o directory di lavoro (in inglese current working directory). La
directory di lavoro è quella da cui si parte quando un pathname è espresso in forma relativa,
dove il “relativa” fa riferimento appunto a questa directory.
Quando un utente effettua 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
directory ad un’altra, il comando pwd la stampa sul terminale. Siccome la directory corrente
resta la stessa quando viene creato un processo figlio (vedi sez. 3.2.2), la directory corrente della
shell diventa anche la directory corrente di qualunque comando da essa lanciato.
Dato che è il kernel che tiene traccia per ciascun processo dell’inode della directory di lavo-
ro, per ottenerne il pathname occorre usare una apposita funzione di libreria, getcwd,31 il cui
prototipo è:
30
questa viene mantenuta all’interno dei dati della sua task_struct (vedi fig. 3.2), più precisamente nel campo
pwd della sotto-struttura fs_struct.
31
con Linux getcwd è una system call dalla versione 2.1.9, in precedenza il valore doveva essere ottenuto tramite
il filesystem /proc da /proc/self/cwd.
126 CAPITOLO 5. FILE E DIRECTORY
#include <unistd.h>
char *getcwd(char *buffer, size_t size)
Legge il pathname della directory di lavoro corrente.
La funzione restituisce il pathname completo della directory di lavoro corrente nella stringa
puntata da buffer, che deve essere precedentemente allocata, per una dimensione massima di
size. Il buffer deve essere sufficientemente largo da poter contenere il pathname completo più
lo zero di terminazione della stringa. Qualora esso ecceda le dimensioni specificate con size la
funzione restituisce un errore.
Si può anche specificare un puntatore nullo come buffer,32 nel qual caso la stringa sarà
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à
all’indietro con BSD, che non consente di specificare la dimensione del buffer; 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,
per cui non è detto che il buffer sia sufficiente a contenere il nome del file, e questa è la ragione
principale per cui questa funzione è deprecata.
Un uso comune di getcwd è quello di salvare la directory di lavoro iniziale per poi potervi
tornare in un tempo successivo, un metodo alternativo più veloce, se non si è a corto di file
descriptor, è invece quello di aprire la directory corrente (vale a dire “.”) e tornarvi in seguito
con fchdir.
Una seconda usata per ottenere la directory di lavoro è char *get_current_dir_name(void)
che è sostanzialmente equivalente ad una getcwd(NULL, 0), con la sola differenza che essa ri-
torna il valore della variabile di ambiente PWD, che essendo costruita dalla shell può contenere un
pathname comprendente anche dei link simbolici. Usando getcwd infatti, essendo il pathname
ricavato risalendo all’indietro l’albero 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
di shell cd) il cui nome sta appunto per change directory, il suo prototipo è:
#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
valori:
ENOTDIR non si è specificata una directory.
EACCES 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 file, è possibile riferirsi ad esse anche tramite il file descriptor,
e non solo tramite il pathname, per fare questo si usa fchdir, il cui prototipo è:
32
questa è un’estensione allo standard POSIX.1, supportata da Linux e dalla glibc.
5.1. LA GESTIONE DI FILE E DIRECTORY 127
#include <unistd.h>
int fchdir(int fd)
Identica a chdir, ma usa il file descriptor fd invece del pathname.
La funzione restituisce zero in caso di successo e -1 per un errore, in caso di errore errno assumerà
i valori EBADF o EACCES.
anche in questo caso fd deve essere un file descriptor valido che fa riferimento ad una directory.
Inoltre l’unico errore di accesso possibile (tutti gli altri sarebbero occorsi all’apertura di fd), è
quello in cui il processo non ha il permesso di accesso alla directory specificata da fd.
La funzione ritorna il puntatore alla stringa con il nome o NULL in caso di fallimento. Non sono
definiti errori.
La funzione restituisce il puntatore ad una stringa contente un nome di file valido e non
esistente al momento dell’invocazione; se si è passato come argomento string un puntatore non
nullo ad un buffer di caratteri questo deve essere di dimensione L_tmpnam ed il nome generato
vi verrà copiato automaticamente; altrimenti il nome sarà generato in un buffer statico interno
che verrà sovrascritto ad una chiamata successiva. Successive invocazioni della funzione conti-
nueranno a restituire nomi unici fino ad un massimo di TMP_MAX volte, limite oltre il quale il
comportamento è indefinito. Al nome viene automaticamente aggiunto come prefisso la directory
specificata dalla costante P_tmpdir.34
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 specificare un prefisso per il
file esplicitamente, il suo prototipo è:
#include <stdio.h>
char *tempnam(const char *dir, const char *pfx)
Genera un nome univoco per un file temporaneo.
La funzione ritorna il puntatore alla stringa con il nome o NULL in caso di fallimento, errno viene
impostata a ENOMEM qualora fallisca l’allocazione della stringa.
La funzione alloca con malloc la stringa in cui restituisce il nome, per cui è sempre rientrante,
occorre però ricordarsi di disallocare con free il puntatore che restituisce. L’argomento pfx
specifica un prefisso di massimo 5 caratteri per il nome provvisorio. La funzione assegna come
directory per il file temporaneo, verificando che esista e sia accessibile, la prima valida fra le
seguenti:
• La variabile di ambiente TMPDIR (non ha effetto se non è definita o se il programma
chiamante è suid o sgid, vedi sez. 5.3.2).
33
la funzione è stata deprecata nella revisione POSIX.1-2008 dello standard POSIX.
34
le costanti L_tmpnam, P_tmpdir e TMP_MAX sono definite in stdio.h.
128 CAPITOLO 5. FILE E DIRECTORY
In ogni caso, anche se la generazione del nome è casuale, ed è molto difficile ottenere un
nome duplicato, nulla assicura che un altro processo non possa avere creato, fra l’ottenimento
del nome e l’apertura del file, un altro file con lo stesso nome; per questo motivo quando si usa
il nome ottenuto da una di queste funzioni occorre sempre aprire il nuovo file in modalità di
esclusione (cioè con l’opzione O_EXCL per i file descriptor o con il flag x per gli stream) che fa
fallire l’apertura in caso il file sia già esistente.
Per evitare di dovere effettuare a mano tutti questi controlli, lo standard POSIX definisce la
funzione tmpfile, che permette di ottenere in maniera sicura l’accesso ad un file temporaneo, il
suo prototipo è:
#include <stdio.h>
FILE *tmpfile(void)
Restituisce un file temporaneo aperto in lettura/scrittura.
La funzione ritorna il puntatore allo stream associato al file temporaneo in caso di successo e NULL
in caso di errore, nel qual caso errno assumerà i valori:
EINTR la funzione è stata interrotta da un segnale.
EEXIST non è stato possibile generare un nome univoco.
ed inoltre EFAULT, EMFILE, ENFILE, ENOSPC, EROFS e EACCES.
La funzione restituisce direttamente uno stream già aperto (in modalità r+b, si veda sez. 7.2.1)
e pronto per l’uso, che viene automaticamente cancellato alla sua chiusura o all’uscita dal pro-
gramma. Lo standard non specifica in quale directory verrà aperto il file, ma le glibc prima
tentano con P_tmpdir e poi con /tmp. Questa funzione è rientrante e non soffre di problemi di
race condition.
Alcune versioni meno recenti di Unix non supportano queste funzioni; in questo caso si
possono usare le vecchie funzioni mktemp e mkstemp che modificano 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 è:
#include <stlib.h>
char *mktemp(char *template)
Genera un nome univoco per un file temporaneo.
La funzione ritorna il puntatore template in caso di successo e NULL in caso di errore, nel qual
caso errno assumerà i valori:
EINVAL template non termina con XXXXXX.
La funzionane genera un nome univoco sostituendo le XXXXXX finali di template; dato che
template deve poter essere modificata dalla funzione non si può usare una stringa 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à diverse
per il nome del file, e rende il nome temporaneo facile da indovinare. Per tutti questi motivi la
funzione è deprecata e non dovrebbe mai essere usata.
La seconda funzione, mkstemp è sostanzialmente equivalente a tmpfile, ma restituisce un
file descriptor invece di un nome; il suo prototipo è:
5.2. LA MANIPOLAZIONE DELLE CARATTERISTICHE DEI FILE 129
#include <stlib.h>
int mkstemp(char *template)
Genera un file temporaneo.
La funzione ritorna il file descriptor in caso di successo e -1 in caso di errore, nel qual caso errno
assumerà i valori:
EINVAL template non termina con XXXXXX.
EEXIST non è riuscita a creare un file temporaneo, il contenuto di template è indefinito.
Come per mktemp anche in questo caso template non può essere una stringa costante. La
funzione apre un file in lettura/scrittura con la funzione open, usando l’opzione O_EXCL (si veda
sez. 6.2.1), in questo modo al ritorno della funzione si ha la certezza di essere stati i creatori del
file, i cui permessi (si veda sez. 5.3.1) sono impostati al valore 0600 (lettura e scrittura solo per
il proprietario).35 Di questa funzione esiste una variante mkostemp, introdotta specificamente
dalla glibc,36 il cui prototipo è:
#include <stlib.h>
int mkostemp(char *template, int flags)
Genera un file temporaneo.
La funzione ritorna il file descriptor in caso di successo e -1 in caso di errore, con gli stessi errori
di mkstemp.
la cui sola differenza è la presenza dell’ulteriore argomento flags che consente di specificare i
flag da passare ad open nell’apertura del file.
In OpenBSD è stata introdotta un’altra funzione simile alle precedenti, mkdtemp, che crea
invece una directory temporanea;37 il suo prototipo è:
#include <stlib.h>
char *mkdtemp(char *template)
Genera una directory temporanea.
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:
EINVAL template non termina con XXXXXX.
più gli altri eventuali codici di errore di mkdir.
La funzione genera una directory il cui nome è ottenuto sostituendo le XXXXXX finali di
template con permessi 0700 (al solito si veda cap. 6 per i dettagli); dato che la creazione della
directory è sempre esclusiva i precedenti problemi di race condition non si pongono.
35
questo è vero a partire dalle glibc 2.0.7, le versioni precedenti delle glibc e le vecchie libc5 e libc4 usavano il
valore 0666 che permetteva a chiunque di leggere e scrivere i contenuti del file.
36
la funzione è stata introdotta nella versione 2.7 delle librerie e richiede che sia definita la macro _GNU_SOURCE.
37
la funzione è stata introdotta nelle glibc a partire dalla versione 2.1.91 ed inserita nello standard POSIX.1-2008.
130 CAPITOLO 5. FILE E DIRECTORY
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int stat(const char *file_name, struct stat *buf)
int lstat(const char *file_name, struct stat *buf)
int fstat(int filedes, struct stat *buf)
Legge le informazioni di un file.
Le funzioni restituiscono 0 in caso di successo e -1 per un errore, nel qual caso errno assumerà
uno dei valori: EBADF, ENOENT, ENOTDIR, ELOOP, EFAULT, EACCES, ENOMEM, ENAMETOOLONG.
La funzione stat legge le informazioni del file il cui pathname è specificato dalla stringa
puntata da file_name e le inserisce nel buffer puntato dall’argomento buf; la funzione lstat
è identica a stat eccetto che se file_name è un link simbolico vengono lette le informazioni
relative ad esso e non al file a cui fa riferimento. Infine fstat esegue la stessa operazione su un
file già aperto, specificato tramite il suo file descriptor filedes.
La struttura stat usata da queste funzioni è definita nell’header sys/stat.h e in generale
dipende dall’implementazione; la versione usata da Linux è mostrata in fig. 5.5, cosı̀ come ri-
portata dalla pagina di manuale di stat; in realtà la definizione effettivamente usata nel kernel
dipende dall’architettura e ha altri campi riservati per estensioni come tempi dei file più precisi
(vedi sez. 5.2.4), o per il padding dei campi.
struct stat {
dev_t st_dev ; /* device */
ino_t st_ino ; /* inode */
mode_t st_mode ; /* protection */
nlink_t st_nlink ; /* number of hard links */
uid_t st_uid ; /* user ID of owner */
gid_t st_gid ; /* group ID of owner */
dev_t st_rdev ; /* device type ( if inode device ) */
off_t st_size ; /* total size , in bytes */
unsigned long st_blksize ; /* blocksize for filesystem I / O */
unsigned long st_blocks ; /* number of blocks allocated */
time_t st_atime ; /* time of last access */
time_t st_mtime ; /* time of last modification */
time_t st_ctime ; /* time of last change */
};
Figura 5.5: La struttura stat per la lettura delle informazioni dei file.
Si noti come i vari membri della struttura siano specificati come tipi primitivi del sistema
(di quelli definiti in tab. 1.2, e dichiarati in sys/types.h).
che supporta pure le estensioni allo standard per i link simbolici e i socket definite da BSD;
l’elenco completo delle macro con cui è possibile estrarre l’informazione da st_mode è riportato
in tab. 5.3.
Macro Tipo del file
S_ISREG(m) file normale.
S_ISDIR(m) directory.
S_ISCHR(m) dispositivo a caratteri.
S_ISBLK(m) dispositivo a blocchi.
S_ISFIFO(m) fifo.
S_ISLNK(m) link simbolico.
S_ISSOCK(m) socket.
Oltre alle macro di tab. 5.3 è possibile usare direttamente il valore di st_mode per ricavare
il tipo di file controllando direttamente i vari bit in esso memorizzati. Per questo sempre in
sys/stat.h sono definite le costanti numeriche riportate in tab. 5.4.
Il primo valore dell’elenco di tab. 5.4 è la maschera binaria che permette di estrarre i bit nei
quali viene memorizzato il tipo di file, i valori successivi sono le costanti corrispondenti ai singoli
bit, e possono essere usati per effettuare la selezione sul tipo di file voluto, con un’opportuna
combinazione.
Flag Valore Significato
S_IFMT 0170000 Maschera per i bit del tipo di file.
S_IFSOCK 0140000 Socket.
S_IFLNK 0120000 Link simbolico.
S_IFREG 0100000 File regolare.
S_IFBLK 0060000 Dispositivo a blocchi.
S_IFDIR 0040000 Directory.
S_IFCHR 0020000 Dispositivo a caratteri.
S_IFIFO 0010000 Fifo.
S_ISUID 0004000 Set UID bit .
S_ISGID 0002000 Set GID bit .
S_ISVTX 0001000 Sticky bit .
S_IRUSR 00400 Il proprietario ha permesso di lettura.
S_IWUSR 00200 Il proprietario ha permesso di scrittura.
S_IXUSR 00100 Il proprietario ha permesso di esecuzione.
S_IRGRP 00040 Il gruppo ha permesso di lettura.
S_IWGRP 00020 Il gruppo ha permesso di scrittura.
S_IXGRP 00010 Il gruppo ha permesso di esecuzione.
S_IROTH 00004 Gli altri hanno permesso di lettura.
S_IWOTH 00002 Gli altri hanno permesso di esecuzione.
S_IXOTH 00001 Gli altri hanno permesso di esecuzione.
Tabella 5.4: Costanti per l’identificazione dei vari bit che compongono il campo st_mode (definite in sys/stat.h).
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 fd non è un file descriptor.
EINVAL fd è un riferimento ad un socket, non a un file o non è aperto in scrittura.
per truncate si hanno:
EACCES il file non ha permesso di scrittura o non si ha il permesso di esecuzione una delle
directory del pathname.
ETXTBSY il file è un programma in esecuzione.
ed anche ENOTDIR, ENAMETOOLONG, ENOENT, EROFS, EIO, EFAULT, ELOOP.
Entrambe le funzioni fan sı̀ che la dimensione del file sia troncata ad un valore massimo
specificato da length, e si distinguono solo per il fatto che il file viene indicato con il pathname
file_name per truncate e con il file descriptor fd per ftruncate; se il file è più lungo della
lunghezza specificata i dati in eccesso saranno perduti.
Il comportamento in caso di lunghezza inferiore non è specificato e dipende dall’implemen-
tazione: il file può essere lasciato invariato o esteso fino alla lunghezza scelta; nel caso di Linux
viene esteso con la creazione di un buco nel file e ad una lettura si otterranno degli zeri; si tenga
presente però che questo comportamento è supportato solo per filesystem nativi, ad esempio su
un filesystem non nativo come il VFAT di Windows questo non è possibile.
Il primo punto da tenere presente è la differenza fra il cosiddetto tempo di ultima modifica
(il modification time, st_mtime) e il tempo di ultimo cambiamento di stato (il change time,
st_ctime). Il primo infatti fa riferimento ad una modifica del contenuto di un file, mentre il
secondo ad una modifica dell’inode. Dato che esistono molte operazioni, come la funzione link
e altre che vedremo in seguito, che modificano solo le informazioni contenute nell’inode senza
toccare il contenuto del file, diventa necessario l’utilizzo di questo secondo tempo.
Il tempo di ultima modifica viene usato ad esempio da programmi come make per decidere
quali file necessitano di essere ricompilati o (talvolta insieme anche al tempo di cambiamento di
stato) per decidere quali file devono essere archiviati per il backup. Il tempo di ultimo accesso
viene di solito usato per identificare i file che non vengono più utilizzati per un certo lasso di
tempo. Ad esempio un programma come leafnode lo usa per cancellare gli articoli letti più
vecchi, mentre mutt lo usa per marcare i messaggi di posta che risultano letti. Il sistema non
tiene conto dell’ultimo accesso all’inode, pertanto funzioni come access o stat non hanno alcuna
influenza sui tre tempi. Il comando ls (quando usato con le opzioni -l o -t) mostra i tempi dei
file secondo lo schema riportato nell’ultima colonna di tab. 5.5.
L’aggiornamento del tempo di ultimo accesso è stato a lungo considerato un difetto pro-
gettuale di Unix, questo infatti comporta la necessità di effettuare un accesso in scrittura sul
disco anche in tutti i casi in cui questa informazione non interessa e sarebbe possibile avere un
semplice accesso in lettura sui dati bufferizzati. Questo comporta un ovvio costo sia in termini
di prestazioni, che di consumo di risorse come la batteria per i portatili, o cicli di riscrittura per
i dischi su memorie riscrivibili.
Per questo motivo, onde evitare di mantenere una informazione che nella maggior parte
dei casi non interessa, è sempre stato possibile disabilitare l’aggiornamento del tempo di ultimo
accesso con l’opzione di montaggio noatime. Dato però che questo può creare problemi a qualche
programma, in Linux è stata introdotta la opzione relatime che esegue l’aggiornamento soltanto
se il tempo di ultimo accesso è precedente al tempo di ultima modifica o cambiamento, cosı̀ da
rendere evidente che vi è stato un accesso dopo la scrittura, ed evitando al contempo ulteriori
operazioni su disco negli accessi successivi. In questo modo l’informazione relativa al fatto che un
file sia stato letto resta disponibile, e ad esempio i programmi citati in precedenza continuano a
funzionare. Questa opzione, a partire dal kernel 2.6.30, è diventata il comportamento di default
e non deve più essere specificata esplicitamente.38
L’effetto delle varie funzioni di manipolazione dei file sui relativi tempi è illustrato in tab. 5.6,
facendo riferimento al comportamento classico per quanto riguarda st_atime. Si sono riportati
gli effetti sia per il file 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 sono
anch’esse file 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 un’operazione su un file che comporta una
modifica del nome contenuto nella directory, andremo anche a scrivere sulla directory che lo
contiene cambiandone il tempo di modifica. Un esempio di questo tipo di operazione può essere
la cancellazione di un file, invece leggere o scrivere o cambiare i permessi di un file ha effetti solo
sui tempi di quest’ultimo.
38
si può comunque riottenere il vecchio comportamento usando la opzione di montaggio strictatime.
134 CAPITOLO 5. FILE E DIRECTORY
Tabella 5.6: Prospetto dei cambiamenti effettuati sui tempi di ultimo accesso (a), ultima modifica (m) e ultimo
cambiamento (c) dalle varie funzioni operanti su file e directory.
Si noti infine come st_ctime non abbia nulla a che fare con il tempo di creazione del file,
usato in molti altri sistemi operativi, ma che in Unix non esiste. Per questo motivo quando si
copia un file, a meno di preservare esplicitamente i tempi (ad esempio con l’opzione -p di cp)
esso avrà sempre il tempo corrente come data di ultima modifica.
I tempi di ultimo accesso e modifica possono essere modificati esplicitamente usando la
funzione utime, il cui prototipo è:
#include <utime.h>
int utime(const char *filename, struct utimbuf *times)
Modifica i tempi di ultimo accesso e modifica di un file.
La funzione restituisce 0 in caso di successo e -1 in caso di errore, nel qual caso errno assumerà
uno dei valori:
EACCES non si ha il permesso di scrittura sul file.
EPERM non si è proprietari del file.
ed inoltre EROFS e ENOENT.
La funzione cambia i tempi di ultimo accesso e modifica del file specificato dall’argomento
filename, e richiede come secondo argomento il puntatore ad una struttura utimbuf, la cui
definizione è riportata in fig. 5.6, con i nuovi valori di detti tempi (rispettivamente nei campi
actime e modtime). Se si passa un puntatore nullo verrà impostato il tempo corrente.
struct utimbuf {
time_t actime ; /* access time */
time_t modtime ; /* modification time */
};
Figura 5.6: La struttura utimbuf, usata da utime per modificare i tempi dei file.
5.2. LA MANIPOLAZIONE DELLE CARATTERISTICHE DEI FILE 135
L’effetto della funzione e i privilegi necessari per eseguirla dipendono da cosa è l’argomento
times; se è NULL la funzione imposta il tempo corrente ed è sufficiente avere accesso in scrittura
al file; se invece si è specificato un valore la funzione avrà successo solo se si è proprietari del file
o si hanno i privilegi di amministratore.
Si tenga presente che non è comunque possibile specificare il tempo di cambiamento di stato
del file, che viene aggiornato direttamente dal kernel tutte le volte che si modifica l’inode (quindi
anche alla chiamata di utime). Questo serve anche come misura di sicurezza per evitare che si
possa modificare un file nascondendo completamente le proprie tracce. In realtà la cosa resta
possibile se si è in grado di accedere al file di dispositivo, scrivendo direttamente sul disco senza
passare attraverso il filesystem, ma ovviamente in questo modo la cosa è più complicata da
realizzare.
A partire dal kernel 2.6 la risoluzione dei tempi dei file, che nei campi di tab. 5.5 è espressa
in secondi, è stata portata ai nanosecondi per la gran parte dei filesystem. La ulteriore in-
formazione può essere acceduta attraverso altri campi appositamente aggiunti alla struttura
stat. Se si sono definite le macro _BSD_SOURCE o _SVID_SOURCE questi sono st_atim.tv_nsec,
st_mtim.tv_nsec e st_ctim.tv_nsec se queste non sono definite, st_atimensec, st_mtimensec
e st_mtimensec. Qualora il supporto per questa maggior precisione sia assente questi campi
aggiuntivi saranno nulli.
Per la gestione di questi nuovi valori è stata definita una seconda funzione di modifica,
utimes, che consente di specificare tempi con maggior precisione; il suo prototipo è:
#include <sys/time.h>
int utimes(const char *filename, struct timeval times[2])
Modifica i tempi di ultimo accesso e modifica di un file.
La funzione restituisce 0 in caso di successo e -1 in caso di errore, nel qual caso errno assumerà
uno dei valori:
EACCES non si ha il permesso di scrittura sul file.
EPERM non si è proprietari del file.
ed inoltre EROFS e ENOENT.
La funzione è del tutto analoga alla precedente utime ma usa come secondo argomento un
vettore di due strutture timeval, la cui definizione è riportata in fig. 5.7, che consentono di
indicare i tempi con una precisione del microsecondo. Il primo elemento di times indica il valore
per il tempo di ultimo accesso, il secondo quello per il tempo di ultima modifica. Se si indica
come secondo argomento un puntatore nullo di nuovo verrà utilizzato il tempo corrente.
struct timeval
{
time_t tv_sec ; /* seconds */
suseconds_t tv_usec ; /* microseconds */
};
Figura 5.7: La struttura timeval usata per indicare valori di tempo con la precisione del microsecondo.
Oltre ad utimes su Linux sono presenti altre due funzioni,39 futimes e lutimes, che con-
sentono rispettivamente di effettuare la modifica utilizzando un file già aperto o di eseguirla
direttamente su un link simbolico. I relativi prototipi sono:
39
le due funzioni non sono definite in nessuno standard, ma sono presenti, oltre che su Linux, anche su BSD.
136 CAPITOLO 5. FILE E DIRECTORY
#include <sys/time.h>
int futimes(int fd, const struct timeval tv[2])
Cambia i tempi di un file già aperto specificato tramite il file descriptor fd.
int lutimes(const char *filename, const struct timeval tv[2])
Cambia i tempi di filename anche se questo è un link simbolico.
Le funzioni restituiscono zero in caso di successo e −1 per un errore, nel qual caso errno assumerà
gli stessi valori di utimes, con in più per futimes:
EBADF fd non è un file descriptor.
ENOSYS il filesystem /proc non è accessibile.
Le due funzioni anno lo stesso comportamento di utimes e richiedono gli stessi privilegi
per poter operare, la differenza è che con futimes si può indicare il file su cui operare facendo
riferimento al relativo file descriptor mentre con lutimes nel caso in cui filename sia un link
simbolico saranno modificati i suoi tempi invece di quelli del file a cui esso punta.
Nonostante il kernel, come accennato, supporti risoluzioni dei tempi dei file fino al nanose-
condo, le funzioni fin qui esaminate non consentono di impostare valori con questa precisione.
Per questo sono state introdotte due nuove funzioni, futimens e utimensat, in grado di eseguire
questo compito; i rispettivi prototipi sono:
#include <sys/time.h>
futimens(int fd, const struct timespec times[2])
Cambia i tempi di un file già aperto, specificato dal file descriptor fd.
int utimensat(int dirfd, const char *pathname, const struct timespec times[2],
int flags)
Cambia i tempi del file pathname.
Le funzioni restituiscono zero in caso di successo e −1 per un errore, nel qual caso errno assumerà
gli stessi valori di utimes, con in più per futimes:
EBADF fd non è un file descriptor.
ENOSYS il filesystem /proc non è accessibile.
Entrambe le funzioni utilizzano per indicare i valori dei tempi un vettore times di due
strutture timespec che permette di specificare un valore di tempo con una precisione fino al
nanosecondo, la cui definizione è riportata in fig. 5.8.
struct timespec {
time_t tv_sec ; /* seconds */
long int tv_nsec ; /* nanoseconds */
};
Figura 5.8: La struttura timespec usata per indicare valori di tempo con la precisione del nanosecondo.
Come per le precedenti funzioni il primo elemento di times indica il tempo di ultimo accesso
ed il secondo quello di ultima modifica, e se si usa il valore NULL verrà impostato il tempo corrente
sia per l’ultimo accesso che per l’ultima modifica. Nei singoli elementi di times si possono inoltre
utilizzare due valori speciali per il campo tv_nsec: con UTIME_NOW si richiede l’uso del tempo
corrente, mentre con UTIME_OMIT si richiede di non impostare il tempo. Si può cosı̀ aggiornare
in maniera specifica soltanto uno fra il tempo di ultimo accesso e quello di ultima modifica.
Quando si usa uno di questi valori speciali per tv_nsec il corrispondente valore di tv_sec viene
ignorato.
Queste due funzioni sono una estensione definita in una recente revisione dello standard
POSIX (la POSIX.1-2008); sono state introdotte a partire dal kernel 2.6.22, e supportate dalle
5.3. IL CONTROLLO DI ACCESSO AI FILE 137
glibc a partire dalla versione 2.6.40 La prima è sostanzialmente una estensione di futimes che
consente di specificare i tempi con precisione maggiore, la seconda supporta invece, rispetto ad
utimes, una sintassi più complessa che, come vedremo in sez. 6.3.5 consente una indicazione
sicura dei pathname relativi specificando la directory da usare come riferimento in dirfd e la
possibilità di usare flags per indicare alla funzione di dereferenziare o meno i link simbolici;
si rimanda pertanto la spiegazione del significato degli argomenti aggiuntivi alla trattazione
generica delle varie funzioni che usano la stessa sintassi, effettuata in sez. 6.3.5.
L’insieme dei permessi viene espresso con un numero a 12 bit; di questi i nove meno signifi-
cativi 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.
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
(in sez. 5.3.2); lo schema di allocazione dei bit è riportato in fig. 5.9.
40
in precedenza, a partire dal kernel 2.6.16, era stata introdotta la funzione futimesat seguendo una bozza della
revisione dello standard poi modificata, questa funzione, sostituita da utimensat, è stata dichiarata obsoleta, non
è supportata da nessuno standard e non deve essere più utilizzata: pertanto non la tratteremo.
41
per standard si intende che implementa le caratteristiche previste dallo standard POSIX; in Linux sono
disponibili anche una serie di altri filesystem, come quelli di Windows e del Mac, che non supportano queste
caratteristiche.
42
questo è vero solo per filesystem di tipo Unix, ad esempio non è vero per il filesystem vfat di Windows, che
non fornisce nessun supporto per l’accesso multiutente, e per il quale i permessi vengono assegnati in maniera
fissa con un opzione in fase di montaggio.
43
come le Access Control List che sono state aggiunte ai filesystem standard con opportune estensioni (vedi
sez. 5.4.2) per arrivare a meccanismi di controllo ancora più sofisticati come il mandatory access control di
SE-Linux.
138 CAPITOLO 5. FILE E DIRECTORY
Figura 5.9: Lo schema dei bit utilizzati per specificare i permessi di un file contenuti nel campo st_mode di stat.
Anche i permessi, come tutte le altre informazioni pertinenti al file, sono memorizzati nell’inode;
in particolare essi sono contenuti in alcuni bit del campo st_mode della struttura stat (si veda
di nuovo fig. 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.
Tabella 5.7: I bit dei permessi di accesso ai file, come definiti in <sys/stat.h>
I permessi vengono usati in maniera diversa dalle varie funzioni, e a seconda che si riferiscano a
dei file, dei link simbolici o delle directory; qui ci limiteremo ad un riassunto delle regole generali,
entrando nei dettagli più avanti.
La prima regola è che per poter accedere ad un file attraverso il suo pathname occorre il
permesso di esecuzione in ciascuna delle directory che compongono il pathname; lo stesso vale
per aprire un file nella directory corrente (per la quale appunto serve il diritto di esecuzione).
Per una directory infatti il permesso di esecuzione significa che essa può essere attraversata
nella risoluzione del pathname, ed è distinto dal permesso di lettura che invece implica che si
può leggere il contenuto della directory.
Questo significa che se si ha il permesso di esecuzione senza permesso di lettura si potrà lo
stesso aprire un file 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
directory).
Avere il permesso di lettura per un file 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 file in sola scrittura o lettura/scrittura e modificarne
il contenuto, lo stesso permesso è necessario per poter troncare il file.
Non si può creare un file fintanto che non si disponga del permesso di esecuzione e di quello
di scrittura per la directory di destinazione; gli stessi permessi occorrono per cancellare un file
5.3. IL CONTROLLO DI ACCESSO AI FILE 139
da una directory (si ricordi che questo non implica necessariamente la rimozione del contenuto
del file dal disco), non è necessario nessun tipo di permesso per il file stesso (infatti esso non
viene toccato, viene solo modificato il contenuto della directory, rimuovendo la voce che ad esso
fa riferimento).
Per poter eseguire un file (che sia un programma compilato od uno script di shell, od un altro
tipo di file eseguibile riconosciuto dal kernel), occorre avere il permesso di esecuzione, inoltre
solo i file regolari possono essere eseguiti.
I permessi per un link simbolico sono ignorati, contano quelli del file 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
controllati solo quando viene richiesta la rimozione del link e quest’ultimo è in una directory con
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 l’utente e il gruppo a cui il file appartiene (i
valori di st_uid e st_gid accennati in precedenza) e l’user-ID effettivo, il group-ID effettivo e
gli eventuali group-ID supplementari del processo.44
Per una spiegazione dettagliata degli identificatori associati ai processi si veda sez. 3.3; nor-
malmente, a parte quanto vedremo in sez. 5.3.2, l’user-ID effettivo e il group-ID effettivo corri-
spondono ai valori dell’uid e del gid dell’utente che ha lanciato il processo, mentre i group-ID
supplementari sono quelli dei gruppi cui l’utente appartiene.
I passi attraverso i quali viene stabilito se il processo possiede il diritto di accesso sono i
seguenti:
2. Se l’user-ID effettivo del processo è uguale all’uid del proprietario del file (nel qual caso si
dice che il processo è proprietario del file) allora:
3. Se il group-ID effettivo del processo o uno dei group-ID supplementari dei processi corri-
spondono al gid del file allora:
4. Se il bit dei permessi d’accesso per tutti gli altri è impostato, l’accesso è consentito,
altrimenti l’accesso è negato.
Si tenga presente che questi passi vengono eseguiti esattamente in quest’ordine. Questo vuol
dire che se un processo è il proprietario di un file, l’accesso è consentito o negato solo sulla base
dei permessi per l’utente; 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.
44
in realtà Linux, per quanto riguarda l’accesso ai file, utilizza gli identificatori del gruppo filesystem (si ricordi
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 differenza.
45
per relativo si intende il bit di user-read se il processo vuole accedere in lettura, quello di user-write per
l’accesso in scrittura, ecc.
140 CAPITOLO 5. FILE E DIRECTORY
46
per motivi di sicurezza il kernel ignora i bit suid e sgid per gli script eseguibili.
5.3. IL CONTROLLO DI ACCESSO AI FILE 141
Ovviamente per evitare che gli utenti potessero intasare la swap solo l’amministratore 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 filesystem rendono
sostanzialmente inutile questo procedimento.
Benché ormai non venga più utilizzato per i file, lo sticky bit ha invece assunto un uso
importante per le directory;47 in questo caso se tale bit è impostato un file potrà essere rimosso
dalla directory soltanto se l’utente ha il permesso di scrittura su di essa ed inoltre è vera una
delle seguenti condizioni:
un classico esempio di directory che ha questo bit impostato è /tmp, i permessi infatti di solito
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
file in questa directory (che, come suggerisce il nome, è normalmente utilizzata per la creazione
di file temporanei), ma solo l’utente che ha creato un certo file potrà cancellarlo o rinominarlo.
In questo modo si evita che un utente possa, più o meno consapevolmente, cancellare i file
temporanei creati degli altri utenti.
La funzione verifica i permessi di accesso, indicati da mode, per il file indicato da pathname. I
valori possibili per l’argomento 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 verifica dell’esistenza del file, se si vuole verificare solo quest’ultima si può usare F_OK, o
anche direttamente stat. Nel caso in cui pathname si riferisca ad un link simbolico, questo viene
seguito ed il controllo è fatto sul file a cui esso fa riferimento.
47
lo sticky bit per le directory è un’estensione non definita nello standard POSIX, Linux però la supporta, cosı̀
come BSD e SVr4.
142 CAPITOLO 5. FILE E DIRECTORY
La funzione controlla solo i bit dei permessi di accesso, si ricordi che il fatto che una directory
abbia permesso di scrittura non significa che ci si possa scrivere come in un file, e il fatto che
un file 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 Significato
R_OK Verifica il permesso di lettura.
W_OK Verifica il permesso di scrittura.
X_OK Verifica il permesso di esecuzione.
F_OK Verifica l’esistenza del file.
Tabella 5.8: Valori possibile per l’argomento mode della funzione access.
Un esempio tipico per l’uso di questa funzione è quello di un processo che sta eseguendo un
programma coi privilegi di un altro utente (ad esempio attraverso l’uso del suid bit) che vuole
controllare se l’utente originale ha i permessi per accedere ad un certo file.
Del tutto analoghe a access sono le due funzioni euidaccess e eaccess che ripetono lo
stesso controllo usando però gli identificatori del gruppo effettivo, verificando quindi le effettive
capacità di accesso ad un file. Le funzioni hanno entrambe lo stesso prototipo48 che è del tutto
identico a quello di access. Prendono anche gli stessi valori e restituiscono gli stessi risultati e
gli stessi codici di errore.
Per cambiare i permessi di un file il sistema mette ad disposizione due funzioni chmod e
fchmod, che operano rispettivamente su un filename e su un file 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 file indicato da path al valore indicato da mode.
int fchmod(int fd, mode_t mode)
Analoga alla precedente, ma usa il file descriptor fd per indicare il file.
Le funzioni restituiscono zero in caso di successo e -1 per un errore, in caso di errore errno può
assumere i valori:
EPERM l’user-ID effettivo non corrisponde a quello del proprietario del file o non è zero.
EROFS il file è su un filesystem in sola lettura.
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 dell’apposito
tipo primitivo mode_t (vedi tab. 1.2) utilizzato per specificare i permessi sui file.
Le costanti con cui specificare 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
bit, o specificato direttamente, come per l’omonimo 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ò
calcolare direttamente usando lo schema si utilizzo dei bit illustrato in fig. 5.9.
Ad esempio i permessi standard assegnati ai nuovi file (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 file eseguito attraverso queste funzioni ha comunque
alcune limitazioni, previste per motivi di sicurezza. L’uso delle funzioni infatti è possibile solo se
48
in realtà eaccess è solo un sinonimo di euidaccess fornita per compatibilità con l’uso di questo nome in altri
sistemi.
5.3. IL CONTROLLO DI ACCESSO AI FILE 143
Tabella 5.9: Valori delle costanti usate per indicare i vari bit di mode utilizzato per impostare i permessi dei file.
l’user-ID effettivo del processo corrisponde a quello del proprietario del file o dell’amministratore,
altrimenti esse falliranno con un errore di EPERM.
Ma oltre a questa regola generale, di immediata comprensione, esistono delle limitazioni
ulteriori. Per questo motivo, anche se si è proprietari del file, non tutti i valori possibili di mode
sono permessi o hanno effetto; in particolare accade che:
1. siccome solo l’amministratore può impostare lo sticky bit, se l’user-ID effettivo del processo
non è zero esso viene automaticamente cancellato (senza notifica di errore) qualora sia stato
indicato in mode.
2. per quanto detto in sez. 5.3.4 riguardo la creazione dei nuovi file, si può avere il caso in cui il
file creato da un processo è assegnato ad un gruppo per il quale il processo non ha privilegi.
Per evitare che si possa assegnare il bit sgid ad un file appartenente ad un gruppo per cui
non si hanno diritti, questo viene automaticamente cancellato da mode (senza notifica di
errore) qualora il gruppo del file non corrisponda a quelli associati al processo (la cosa non
avviene quando l’user-ID effettivo del processo è zero).
Per alcuni filesystem49 è inoltre prevista un’ulteriore misura di sicurezza, volta a scongiurare
l’abuso dei bit suid e sgid; essa consiste nel cancellare automaticamente questi bit dai permessi
di un file qualora un processo che non appartenga all’amministratore50 effettui una scrittura. In
questo modo anche se un utente malizioso scopre un file suid su cui può scrivere, un’eventuale
modifica comporterà la perdita di questo privilegio.
Le funzioni chmod e fchmod ci permettono di modificare i permessi di un file, resta però il
problema di quali sono i permessi assegnati quando il file viene creato. Le funzioni dell’interfaccia
nativa di Unix, come vedremo in sez. 6.2.1, permettono di indicare esplicitamente i permessi di
creazione di un file, ma questo non è possibile per le funzioni dell’interfaccia standard ANSI C che
non prevede l’esistenza di utenti e gruppi, ed inoltre il problema si pone anche per l’interfaccia
nativa quando i permessi non vengono indicati esplicitamente.
Per le funzioni dell’interfaccia standard ANSI C l’unico riferimento possibile è quello della
modalità di apertura del nuovo file (lettura/scrittura o sola lettura), che però può fornire un
valore che è lo stesso per tutti e tre i permessi di sez. 5.3.1 (cioè 666 nel primo caso e 222
nel secondo). Per questo motivo il sistema associa ad ogni processo51 una maschera di bit, la
49
i filesystem più comuni (ext2, ext3, ext4, ReiserFS) supportano questa caratteristica, che è mutuata da BSD.
50
per la precisione un processo che non dispone della capacità CAP_FSETID, vedi sez. 5.4.4.
51
è infatti contenuta nel campo umask della struttura fs_struct, vedi fig. 3.2.
144 CAPITOLO 5. FILE E DIRECTORY
cosiddetta umask, che viene utilizzata per impedire che alcuni permessi possano essere assegnati
ai nuovi file in sede di creazione. I bit indicati nella maschera vengono infatti cancellati dai
permessi quando un nuovo file viene creato.52
La funzione che permette di impostare il valore di questa maschera di controllo è umask, ed
il suo prototipo è:
#include <stat.h>
mode_t umask(mode_t mask)
Imposta la maschera dei permessi dei bit al valore specificato da mask (di cui vengono presi
solo i 9 bit meno significativi).
La funzione ritorna il precedente valore della maschera. È una delle poche funzioni che non
restituisce codici di errore.
In genere si usa questa maschera per impostare un valore predefinito che escluda preventi-
vamente 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
permessi non voluti. Di norma questo valore viene impostato una volta per tutte al login a 022,
e gli utenti non hanno motivi per modificarlo.
in genere BSD usa sempre la seconda possibilità, che viene per questo chiamata semantica BSD.
Linux invece segue quella che viene chiamata semantica SVr4; di norma cioè il nuovo file viene
creato, seguendo la prima opzione, con il gid del processo, se però la directory in cui viene creato
il file 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 offre la possibilità di scegliere, ma per ottenere lo stesso risultato di
coerenza che si ha con BSD necessita che quando si creano nuove directory venga anche propagato
anche il bit sgid. Questo è il comportamento predefinito del comando mkdir, ed è in questo modo
ad esempio che le varie distribuzioni assicurano che le sotto-directory create nella home di un
utente restino sempre con il gid del gruppo primario dello stesso.
La presenza del bit sgid è inoltre molto comoda quando si hanno directory contenenti file
condivisi all’intero di un gruppo in cui possono scrivere tutti i membri dello stesso, dato che
assicura che i file che gli utenti vi creano appartengano sempre allo stesso gruppo. Questo non
risolve però completamente i problemi di accesso da parte di altri utenti dello stesso gruppo, in
quanto i permessi assegnati al gruppo potrebbero non essere sufficienti; in tal caso si deve aver
cura di usare un valore di umask che ne lasci di sufficienti.53
52
l’operazione viene fatta sempre: anche qualora si indichi esplicitamente un valore dei permessi nelle funzioni
di creazione che lo consentono, i permessi contenuti nella umask verranno tolti.
53
in tal caso si può assegnare agli utenti del gruppo una umask di 002, anche se la soluzione migliore in questo
caso è usare una ACL di default (vedi sez. 5.4.2).
5.3. IL CONTROLLO DI ACCESSO AI FILE 145
Come avviene nel caso dei permessi il sistema fornisce anche delle funzioni, chown, fchown e
lchown, che permettono di cambiare sia l’utente che il gruppo a cui un file appartiene; i rispettivi
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 file ai valori specificati dalle
variabili owner e group.
Le funzioni restituiscono 0 in caso di successo e -1 per un errore, nel qual caso caso errno assumerà
i valori:
EPERM l’user-ID effettivo non corrisponde a quello del proprietario del file o non è zero, o
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.
Con Linux solo l’amministratore54 può cambiare il proprietario di un file; in questo viene
seguita la semantica usata da BSD che non consente agli utenti di assegnare i loro file ad altri
utenti evitando eventuali aggiramenti delle quote. L’amministratore può cambiare sempre il
gruppo di un file, il proprietario può cambiare il gruppo solo dei file che gli appartengono e solo
se il nuovo gruppo è il suo gruppo primario o uno dei gruppi di cui fa parte.
La funzione chown segue i link simbolici, per operare direttamente su un link simbolico si
deve usare la funzione lchown.55 La funzione fchown opera su un file aperto, essa è mutuata da
BSD, ma non è nello standard POSIX. Un’altra estensione rispetto allo standard POSIX è che
specificando -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 file
è attivo il mandatory locking (vedi sez. 12.1.5).
Tabella 5.10: Tabella riassuntiva del significato dei bit dei permessi per un file e directory.
riferimento soltanto alla combinazione di bit per i quali è stato riportato esplicitamente un
valore. Si rammenti infine che il valore dei bit dei permessi non ha alcun effetto qualora il
processo possieda i privilegi di amministratore.
informazioni ai singoli file.56 Gli attributi estesi non sono altro che delle coppie nome/valore
che sono associate permanentemente ad un oggetto sul filesystem, analoghi di quello che sono le
variabili di ambiente (vedi sez. 2.3.3) per un processo.
Altri sistemi (come Solaris, MacOS e Windows) hanno adottato un meccanismo diverso in
cui ad un file sono associati diversi flussi 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
quantità limitata di informazione) hanno il grande vantaggio di essere molto più semplici da
realizzare, più efficienti,57 e di garantire l’atomicità di tutte le operazioni.
In Linux gli attributi estesi sono sempre associati al singolo inode e l’accesso viene sempre
eseguito in forma atomica, in lettura il valore corrente viene scritto su un buffer in memoria,
mentre la scrittura prevede che ogni valore precedente sia sovrascritto.
Si tenga presente che non tutti i filesystem supportano gli Extended Attributes; al momento
della scrittura di queste dispense essi sono presenti solo sui vari extN, ReiserFS, JFS, XFS e
Btrfs.58 Inoltre a seconda della implementazione ci possono essere dei limiti sulla quantità di
attributi che si possono utilizzare.59 Infine lo spazio utilizzato per mantenere gli attributi estesi
viene tenuto in conto per il calcolo delle quote di utente e gruppo proprietari del file.
Come meccanismo per mantenere informazioni aggiuntive associate al singolo file, gli Exten-
ded 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 l’accesso e la gestione. Per questo
motivo il nome di un attributo deve essere sempre specificato nella forma namespace.attribute,
dove namespace fa riferimento alla classe a cui l’attributo appartiene, mentre attribute è il
nome ad esso assegnato. In tale forma il nome di un attributo esteso deve essere univoco. Al
momento60 sono state definite le quattro classi di attributi riportate in tab. 5.11.
Nome Descrizione
security Gli extended security attributes: vengono utilizzati dalle estensioni di sicurezza del ker-
nel (i Linux Security Modules), per le realizzazione di meccanismi evoluti di controllo
di accesso come SELinux o le capabilities dei file di sez. 5.4.4.
system Gli extended security attributes: sono usati dal kernel per memorizzare dati di sistema
associati ai file come le ACL (vedi sez. 5.4.2) o le capabilities (vedi sez. 5.4.4).
trusted I trusted extended attributes: vengono utilizzati per poter realizzare in user space
meccanismi che consentano di mantenere delle informazioni sui file che non devono
essere accessibili ai processi ordinari.
user Gli extended user attributes: utilizzati per mantenere informazioni aggiuntive sui file
(come il mime-type, la codifica dei caratteri o del file) accessibili dagli utenti.
Tabella 5.11: 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
estensioni (come le ACL, SELinux, ecc.) al tradizionale meccanismo dei controlli di accesso di
Unix, l’accesso 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.11, si hanno i seguenti casi:
56
l’uso più comune è quello della ACL, che tratteremo nella prossima sezione.
57
cosa molto importante, specie per le applicazioni che richiedono una gran numero di accessi, come le ACL.
58
l’elenco è aggiornato a Luglio 2011.
59
ad esempio nel caso di ext2 ed ext3 è richiesto che essi siano contenuti all’interno di un singolo blocco (pertanto
con dimensioni massime pari a 1024, 2048 o 4096 byte a seconda delle dimensioni di quest’ultimo impostate in fase
di creazione del filesystem), mentre con XFS non ci sono limiti ed i dati vengono memorizzati in maniera diversa
(nell’inode stesso, in un blocco a parte, o in una struttura ad albero dedicata) per mantenerne la scalabilità.
60
della scrittura di questa sezione, kernel 2.6.23, ottobre 2007.
148 CAPITOLO 5. FILE E DIRECTORY
security L’accesso agli extended security attributes dipende dalle politiche di sicurezza stabilite
da loro stessi tramite l’utilizzo di un sistema di controllo basato sui Linux Security
Modules (ad esempio SELinux). Pertanto l’accesso in lettura o scrittura dipende
dalle politiche di sicurezza implementate all’interno dal modulo di sicurezza che si
sta utilizzando al momento (ciascuno avrà le sue). Se non è stato caricato nessun
modulo di sicurezza l’accesso in lettura sarà consentito a tutti i processi, mentre
quello in scrittura solo ai processi con privilegi amministrativi dotati della capability
CAP_SYS_ADMIN.
system Anche l’accesso 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 l’accesso è consentito in lettura ai processi che hanno la capacità
di eseguire una ricerca sul file (cioè hanno il permesso di lettura sulla directory che
contiene il file) ed in scrittura al proprietario del file o ai processi dotati della capability
CAP_FOWNER.61
trusted L’accesso ai trusted extended attributes, sia per la lettura che per la scrittura, è
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.
user L’accesso agli extended user attributes è regolato dai normali permessi dei file: occorre
avere il permesso di lettura per leggerli e quello di scrittura per scriverli o modificarli.
Dato l’uso di questi attributi si è scelto di applicare al loro accesso gli stessi criteri che
si usano per l’accesso al contenuto dei file (o delle directory) cui essi fanno riferimento.
Questa scelta vale però soltanto per i file e le directory ordinarie, se valesse in generale
infatti si avrebbe un serio problema di sicurezza dato che esistono diversi oggetti sul
filesystem per i quali è normale avere avere il permesso di scrittura consentito a tutti
gli utenti, come i link simbolici, o alcuni file di dispositivo come /dev/null. Se fosse
possibile usare su di essi gli extended user attributes un utente qualunque potrebbe
inserirvi dati a piacere.62
La semantica del controllo di accesso indicata inoltre non avrebbe alcun senso al di
fuori di file e directory: i permessi di lettura e scrittura per un file di dispositivo
attengono alle capacità di accesso al dispositivo sottostante,63 mentre per i link sim-
bolici questi vengono semplicemente ignorati: in nessuno dei due casi hanno a che
fare con il contenuto del file, e nella discussione relativa all’uso degli extended user
attributes nessuno è mai stato capace di indicare una qualche forma sensata di utiliz-
zo degli stessi per link simbolici o file di dispositivo, e neanche per le fifo o i socket.
Per questo motivo essi sono stati completamente disabilitati per tutto ciò che non
sia un file regolare o una directory.64 Inoltre per le directory è stata introdotta una
ulteriore restrizione, dovuta di nuovo alla presenza ordinaria di permessi di scrittura
completi su directory come /tmp. Per questo motivo, per evitare eventuali abusi, se
una directory ha lo sticky bit attivo sarà consentito scrivere i suoi extended user at-
tributes soltanto se si è proprietari della stessa, o si hanno i privilegi amministrativi
della capability CAP_FOWNER.
61
vale a dire una politica di accesso analoga a quella impiegata per gli ordinari permessi dei file.
62
la cosa è stata notata su XFS, dove questo comportamento permetteva, non essendovi limiti sullo spazio
occupabile dagli Extended Attributes, di bloccare il sistema riempiendo il disco.
63
motivo per cui si può formattare un disco anche se /dev è su un filesystem in sola lettura.
64
si può verificare la semantica adottata consultando il file fs/xattr.c dei sorgenti del kernel.
5.4. CARATTERISTICHE E FUNZIONALITÀ AVANZATE 149
Le funzioni per la gestione degli attributi estesi, come altre funzioni di gestione avanzate
specifiche di Linux, non fanno parte delle glibc, e sono fornite da una apposita libreria, libattr,
che deve essere installata a parte;65 pertanto se un programma le utilizza si dovrà indicare
esplicitamente l’uso della suddetta libreria invocando il compilatore con l’opzione -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 file, a un
link simbolico e ad un file 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 dell’attributo richiesto in caso
di successo, e −1 in caso di errore, nel qual caso errno assumerà i valori:
ENOATTR l’attributo richiesto non esiste.
ERANGE la dimensione size del buffer value non è sufficiente per contenere il risultato.
ENOTSUP gli attributi estesi non sono supportati dal filesystem o sono disabilitati.
e tutti gli errori di stat, come EPERM se non si hanno i permessi di accesso all’attributo.
Le funzioni getxattr e lgetxattr prendono come primo argomento un pathname che indica
il file di cui si vuole richiedere un attributo, la sola differenza è che la seconda, se il pathname
indica un link simbolico, restituisce gli attributi di quest’ultimo e non quelli del file a cui esso
fa riferimento. La funzione fgetxattr prende invece come primo argomento un numero di file
descriptor, e richiede gli attributi del file ad esso associato.
Tutte e tre le funzioni richiedono di specificare nell’argomento name il nome dell’attributo di
cui si vuole ottenere il valore. Il nome deve essere indicato comprensivo di prefisso del namespace
cui appartiene (uno dei valori di tab. 5.11) nella forma namespace.attributename, come stringa
terminata da un carattere NUL. Il suo valore verrà restituito nel buffer puntato dall’argomento
value per una dimensione massima di size byte;66 se quest’ultima non è sufficiente si avrà un
errore di ERANGE.
Per evitare di dover indovinare la dimensione di un attributo per tentativi si può eseguire
una interrogazione utilizzando un valore nullo per size; in questo caso non verrà letto nessun
dato, ma verrà restituito come valore di ritorno della funzione chiamata la dimensione totale
dell’attributo esteso richiesto, che si potrà usare come stima per allocare un buffer di dimensioni
sufficienti.67
Un secondo gruppo di funzioni è quello che consente di impostare il valore di un attributo
esteso, queste sono setxattr, lsetxattr e fsetxattr, e consentono di operare rispettivamente
su un file, su un link simbolico o specificando un file descriptor; i loro prototipi sono:
65
la versione corrente della libreria è libattr1.
66
gli attributi estesi possono essere costituiti arbitrariamente da dati testuali o binari.
67
si parla di stima perché anche se le funzioni restituiscono la dimensione esatta dell’attributo al momento in
cui sono eseguite, questa potrebbe essere modificata in qualunque momento da un successivo accesso eseguito da
un altro processo.
150 CAPITOLO 5. FILE E DIRECTORY
#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à
i valori:
ENOATTR si è usato il flag XATTR_REPLACE e l’attributo richiesto non esiste.
EEXIST si è usato il flag XATTR_CREATE ma l’attributo esiste già.
ENOTSUP gli attributi estesi non sono supportati dal filesystem 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 all’attributo.
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
suffisso) dell’attributo su cui si vuole operare.
Il valore che verrà assegnato all’attributo dovrà essere preparato nel buffer puntato da value,
e la sua dimensione totale (in byte) sarà indicata dall’argomento size. Infine l’argomento flag
consente di controllare le modalità di sovrascrittura dell’attributo esteso, esso può prendere
due valori: con XATTR_REPLACE si richiede che l’attributo esista, nel qual caso verrà sovrascritto,
altrimenti si avrà errore, mentre con XATTR_CREATE si richiede che l’attributo non esista, nel qual
caso verrà creato, altrimenti si avrà errore ed il valore attuale non sarà modificato. Utilizzando
per flag un valore nullo l’attributo verrà modificato se è già presente, o creato se non c’è.
Le funzioni finora illustrate permettono di leggere o scrivere gli attributi estesi, ma sarebbe
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 file.
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:
ERANGE la dimensione size del buffer value non è sufficiente per contenere il risultato.
ENOTSUP gli attributi estesi non sono supportati dal filesystem 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 all’attributo.
Come per le precedenti le tre funzioni leggono gli attributi rispettivamente di un file, un link
simbolico o specificando un file descriptor, da specificare con il loro primo argomento. Gli altri
due argomenti, identici per tutte e tre, indicano rispettivamente il puntatore list al buffer dove
deve essere letta la lista e la dimensione size di quest’ultimo.
La lista viene fornita come sequenza non ordinata dei nomi dei singoli attributi estesi (sempre
comprensivi del prefisso della loro classe) ciascuno dei quali è terminato da un carattere nullo.
I nomi sono inseriti nel buffer uno di seguito all’altro. Il valore di ritorno della funzione indica
la dimensione totale della lista in byte.
5.4. CARATTERISTICHE E FUNZIONALITÀ AVANZATE 151
Come per le funzioni di lettura dei singoli attributi se le dimensioni del buffer non sono
sufficienti si avrà un errore, ma è possibile ottenere dal valore di ritorno della funzione una stima
della dimensione totale della lista usando per size un valore nullo.
Infine 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 file.
Le funzioni restituiscono 0 in caso di successo, e −1 in caso di errore, nel qual caso errno assumerà
i valori:
ENOATTR l’attributo richiesto non esiste.
ENOTSUP gli attributi estesi non sono supportati dal filesystem o sono disabilitati.
ed inoltre tutti gli errori di stat.
per poterle utilizzare le ACL devono essere attivate esplicitamente montando il filesystem70 su
cui le si vogliono utilizzare con l’opzione acl attiva. Dato che si tratta di una estensione è infatti
opportuno utilizzarle soltanto laddove siano necessarie.
Una ACL è composta da un insieme di voci, e ciascuna voce è a sua volta costituita da un
tipo, da un eventuale qualificatore,71 e da un insieme di permessi. Ad ogni oggetto sul filesystem
si può associare una ACL che ne governa i permessi di accesso, detta access ACL. Inoltre per le
directory si può impostare una ACL aggiuntiva, detta default ACL, che serve ad indicare quale
dovrà essere la ACL assegnata di default nella creazione di un file all’interno della directory
stessa. Come avviene per i permessi le ACL possono essere impostate solo del proprietario del
file, o da un processo con la capability CAP_FOWNER.
Tipo Descrizione
ACL_USER_OBJ voce che contiene i diritti di accesso del proprietario del
file.
ACL_USER voce che contiene i diritti di accesso per l’utente indicato
dal rispettivo qualificatore.
ACL_GROUP_OBJ voce che contiene i diritti di accesso del gruppo
proprietario del file.
ACL_GROUP voce che contiene i diritti di accesso per il gruppo indicato
dal rispettivo qualificatore.
ACL_MASK voce che contiene la maschera dei massimi permessi di
accesso che possono essere garantiti da voci del tipo
ACL_USER, ACL_GROUP e ACL_GROUP_OBJ.
ACL_OTHER voce che contiene i diritti di accesso di chi non
corrisponde a nessuna altra voce dell’ACL.
Tabella 5.12: Le costanti che identificano i tipi delle voci di una ACL.
L’elenco dei vari tipi di voci presenti in una ACL, con una breve descrizione del relativo signi-
ficato, è riportato in tab. 5.12. Tre di questi tipi, ACL_USER_OBJ, ACL_GROUP_OBJ e ACL_OTHER,
corrispondono direttamente ai tre permessi ordinari dei file (proprietario, gruppo proprietario
e tutti gli altri) e per questo una ACL valida deve sempre contenere una ed una sola voce per
ciascuno di questi tipi.
Una ACL può poi contenere un numero arbitrario di voci di tipo ACL_USER e ACL_GROUP,
ciascuna delle quali indicherà i permessi assegnati all’utente e al gruppo indicato dal relativo
qualificatore; ovviamente ciascuna di queste voci dovrà fare riferimento ad un utente o ad un
gruppo diverso, e non corrispondenti a quelli proprietari del file. Inoltre se in una ACL esiste
una voce di uno di questi due tipi è obbligatoria anche la presenza di una ed una sola voce di
tipo ACL_MASK, che negli altri casi è opzionale.
Quest’ultimo tipo di voce contiene la maschera dei permessi che possono essere assegnati
tramite voci di tipo ACL_USER, ACL_GROUP e ACL_GROUP_OBJ; se in una di queste voci si fosse
specificato un permesso non presente in ACL_MASK questo verrebbe ignorato. L’uso di una ACL di
tipo ACL_MASK è di particolare utilità quando essa associata ad una default ACL su una directory,
in quanto i permessi cosı̀ specificati verranno ereditati da tutti i file creati nella stessa directory.
Si ottiene cosı̀ una sorta di umask associata ad un oggetto sul filesystem piuttosto che a un
processo.
Dato che le ACL vengono a costituire una estensione dei permessi ordinari, uno dei pro-
blemi che si erano posti nella loro standardizzazione era appunto quello della corrispondenza
fra questi e le ACL. Come accennato i permessi ordinari vengono mappati le tre voci di tipo
ACL_USER_OBJ, ACL_GROUP_OBJ e ACL_OTHER che devono essere presenti in qualunque ACL; un
cambiamento ad una di queste voci viene automaticamente riflesso sui permessi ordinari dei
70
che deve supportarle, ma questo è ormai vero per praticamente tutti i filesystem più comuni, con l’eccezione
di NFS per il quale esiste però un supporto sperimentale.
71
deve essere presente soltanto per le voci di tipo ACL_USER e ACL_GROUP.
5.4. CARATTERISTICHE E FUNZIONALITÀ AVANZATE 153
file72 e viceversa. In realtà la mappatura è diretta solo per le voci ACL_USER_OBJ e ACL_OTHER,
nel caso di ACL_GROUP_OBJ questo vale soltanto se non è presente una voce di tipo ACL_MASK, se
invece questa è presente verranno tolti dai permessi di ACL_GROUP_OBJ tutti quelli non presenti
in ACL_MASK.73
Un secondo aspetto dell’incidenza delle ACL sul comportamento del sistema è quello relativo
alla creazione di nuovi file,74 che come accennato può essere modificato dalla presenza di una
default ACL sulla directory che contiene quel file. Se questa non c’è valgono le regole usuali
illustrate in sez. 5.3.3, per cui essi sono determinati dalla umask del processo, e la sola differenza è
che i permessi ordinari da esse risultanti vengono automaticamente rimappati anche su una ACL
di accesso assegnata automaticamente al nuovo file, che contiene soltanto le tre corrispondenti
voci di tipo ACL_USER_OBJ, ACL_GROUP_OBJ e ACL_OTHER.
Se invece è presente una ACL di default sulla directory che contiene il nuovo file questa
diventerà automaticamente la sua ACL di accesso, a meno di non aver indicato, nelle funzioni
di creazione che lo consentono, uno specifico valore per i permessi ordinari;75 in tal caso saranno
eliminati dalle voci corrispondenti nella ACL tutti quelli non presenti in tale indicazione.
Dato che questa è la ragione che ha portato alla loro creazione, la principale modifica intro-
dotta con la presenza della ACL è quella alle regole del controllo di accesso ai file illustrate in
sez. 5.3.1. Come nel caso ordinario per il controllo vengono sempre utilizzati gli identificatori del
gruppo effective del processo, ma in presenza di ACL i passi attraverso i quali viene stabilito se
esso ha diritto di accesso sono i seguenti:
1. Se l’user-ID del processo è nullo l’accesso è sempre garantito senza nessun controllo.
2. Se l’user-ID del processo corrisponde al proprietario del file allora:
• se la voce ACL_USER_OBJ contiene il permesso richiesto, l’accesso è consentito;
• altrimenti l’accesso è negato.
3. Se l’user-ID del processo corrisponde ad un qualunque qualificatore presente in una voce
ACL_USER allora:
• se la voce ACL_USER corrispondente e la voce ACL_MASK contengono entrambe il
permesso richiesto, l’accesso è consentito;
• altrimenti l’accesso è negato.
4. Se è il group-ID del processo o uno dei group-ID supplementari corrisponde al gruppo
proprietario del file allora:
• se la voce ACL_GROUP_OBJ e una eventuale voce ACL_MASK (se non vi sono voci di
tipo ACL_GROUP questa può non essere presente) contengono entrambe il permesso
richiesto, l’accesso è consentito;
• altrimenti l’accesso è negato.
5. Se è il group-ID del processo o uno dei group-ID supplementari corrisponde ad un qualun-
que qualificatore presente in una voce ACL_GROUP allora:
• se la voce ACL_GROUP corrispondente e la voce ACL_MASK contengono entrambe il
permesso richiesto, l’accesso è consentito;
• altrimenti l’accesso è negato.
72
per permessi ordinari si intende quelli mantenuti nell’inode, che devono restare dato che un filesystem può
essere montato senza abilitare le ACL.
73
questo diverso comportamento a seconda delle condizioni è stato introdotto dalla standardizzazione POSIX
1003.1e Draft 17 per mantenere il comportamento invariato sui sistemi dotati di ACL per tutte quelle applicazioni
che sono conformi soltanto all’ordinario standard POSIX 1003.1.
74
o oggetti sul filesystem, il comportamento discusso vale per le funzioni open e creat (vedi sez. 6.2.1), mkdir
(vedi sez. 5.1.4), mknod e mkfifo (vedi sez. 5.1.5).
75
tutte le funzioni citate in precedenza supportano un argomento mode che indichi un insieme di permessi
iniziale.
154 CAPITOLO 5. FILE E DIRECTORY
#include <sys/types.h>
#include <sys/acl.h>
acl_t acl_init(int count)
Inizializza un’area di lavoro per una ACL di count voci.
La funzione restituisce un puntatore all’area di lavoro in caso di successo e NULL in caso di errore,
nel qual caso errno assumerà uno dei valori:
EINVAL il valore di count è negativo.
ENOMEM non c’è sufficiente memoria disponibile.
La funzione alloca ed inizializza un’area di memoria che verrà usata per mantenere i dati
di una ACL contenente fino ad un massimo di count voci. La funzione ritorna un valore di
tipo acl_t, da usare in tutte le altre funzioni che operano sulla ACL. La funzione si limita alla
allocazione iniziale e non inserisce nessun valore nella ACL che resta vuota. Si tenga presente
che pur essendo acl_t un tipo opaco che identifica “l’oggetto” ACL, il valore restituito dalla
funzione non è altro che un puntatore all’area di memoria allocata per i dati richiesti; pertanto
in caso di fallimento verrà restituito un puntatore nullo e si dovrà confrontare il valore di ritorno
della funzione con “(acl_t) NULL”.
Una volta che si siano completate le operazioni sui dati di una ACL la memoria allocata dovrà
essere liberata esplicitamente attraverso una chiamata alla funzione acl_free, il cui prototipo
è:
#include <sys/types.h>
#include <sys/acl.h>
int acl_free(void * obj_p)
Disalloca la memoria riservata per i dati di una ACL.
La funzione restituisce 0 in caso di successo e −1 se obj_p non è un puntatore valido, nel qual
caso errno assumerà il valore EINVAL
Si noti come la funzione richieda come argomento un puntatore di tipo “void *”, essa infatti
può essere usata non solo per liberare la memoria allocata per i dati di una ACL, ma anche
per quella usata per creare le stringhe di descrizione testuale delle ACL o per ottenere i valori
dei qualificatori di una voce; pertanto a seconda dei casi occorrerà eseguire un cast a “void
*” del tipo di dato di cui si vuole eseguire la disallocazione. Si tenga presente poi che oltre a
acl_init esistono molte altre funzioni che possono allocare memoria per i dati delle ACL, è
pertanto opportuno tenere traccia di tutte queste funzioni perché alla fine delle operazioni tutta
la memoria allocata dovrà essere liberata con acl_free.
Una volta che si abbiano a disposizione i dati di una ACL tramite il riferimento ad oggetto
di tipo acl_t questi potranno essere copiati con la funzione acl_dup, il cui prototipo è:
76
fino a definire un tipo di dato e delle costanti apposite per identificare i permessi standard di lettura, scrittura
ed esecuzione.
5.4. CARATTERISTICHE E FUNZIONALITÀ AVANZATE 155
#include <sys/types.h>
#include <sys/acl.h>
acl_t acl_dup(acl_t acl)
Crea una copia della ACL acl.
La funzione crea una copia dei dati della ACL indicata tramite l’argomento acl, allocando
autonomamente tutto spazio necessario alla copia e restituendo un secondo oggetto di tipo acl_t
come riferimento a quest’ultima. Valgono per questo le stesse considerazioni fatte per il valore
di ritorno di acl_init, ed in particolare il fatto che occorrerà prevedere una ulteriore chiamata
esplicita a acl_free per liberare la memoria occupata dalla copia.
Se si deve creare una ACL manualmente l’uso di acl_init è scomodo, dato che la funzione
restituisce una ACL vuota, una alternativa allora è usare acl_from_mode che consente di creare
una ACL a partire da un valore di permessi ordinari, il prototipo della funzione è:
#include <sys/types.h>
#include <sys/acl.h>
acl_t acl_from_mode(mode_t mode)
Crea una ACL inizializzata con i permessi di mode.
La funzione restituisce una ACL inizializzata con le tre voci obbligatorie ACL_USER_OBJ,
ACL_GROUP_OBJ e ACL_OTHER già impostate secondo la corrispondenza ai valori dei permessi
ordinari indicati dalla maschera passata nell’argomento mode. Questa funzione è una estensione
usata dalle ACL di Linux e non è portabile, ma consente di semplificare l’inizializzazione in
maniera molto comoda.
Altre due funzioni che consentono di creare una ACL già inizializzata sono acl_get_fd e
acl_get_file, che però sono per lo più utilizzate per leggere la ACL corrente di un file; i
rispettivi prototipi sono:
#include <sys/types.h>
#include <sys/acl.h>
acl_t acl_get_file(const char *path_p, acl_type_t type)
acl_t acl_get_fd(int fd)
Ottiene i dati delle ACL di un file.
Le due funzioni ritornano, con un oggetto di tipo acl_t, il valore della ACL correntemente
associata ad un file, che può essere identificato tramite un file descriptor usando acl_get_fd o
con un pathname usando acl_get_file. Nel caso di quest’ultima funzione, che può richiedere
anche la ACL relativa ad una directory, il secondo argomento type consente di specificare se
si vuole ottenere la ACL di default o quella di accesso. Questo argomento deve essere di tipo
acl_type_t e può assumere solo i due valori riportati in tab. 5.13.
Si tenga presente che nel caso di acl_get_file occorrerà che il processo chiamante abbia pri-
vilegi di accesso sufficienti a poter leggere gli attributi estesi dei file (come illustrati in sez. 5.4.1);
156 CAPITOLO 5. FILE E DIRECTORY
Tipo Descrizione
ACL_TYPE_ACCESS indica una ACL di accesso.
ACL_TYPE_DEFAULT indica una ACL di default.
inoltre una ACL di tipo ACL_TYPE_DEFAULT potrà essere richiesta soltanto per una directory, e
verrà restituita solo se presente, altrimenti verrà restituita una ACL vuota.
Infine si potrà creare una ACL direttamente dalla sua rappresentazione testuale con la
funzione acl_from_text, il cui prototipo è:
#include <sys/types.h>
#include <sys/acl.h>
acl_t acl_from_text(const char *buf_p)
Crea una ACL a partire dalla sua rappresentazione testuale.
tipo:qualificatore:permessi
dove il tipo può essere uno fra user, group, other e mask. Il qualificatore è presente solo per
user e group e indica l’utente o il gruppo a cui la voce si riferisce; i permessi sono espressi con
una tripletta di lettere analoga a quella usata per i permessi dei file.77
Va precisato che i due tipi user e group sono usati rispettivamente per indicare delle voci
relative ad utenti e gruppi,78 applicate sia a quelli proprietari del file che a quelli generici; quelle
dei proprietari si riconoscono per l’assenza di un qualificatore, ed in genere si scrivono per prima
delle altre. Il significato delle voci di tipo mask e mark è evidente. In questa forma si possono
anche inserire dei commenti precedendoli con il carattere “#”.
La forma breve prevede invece la scrittura delle singole voci su una riga, separate da virgole;
come specificatori del tipo di voce si possono usare le iniziali dei valori usati nella forma estesa
(cioè “u”, “g”, “o” e “m”), mentre le altri parte della voce sono le stesse. In questo caso non sono
consentiti permessi.
Per la conversione inversa, che consente di ottenere la rappresentazione testuale di una ACL,
sono invece disponibili due funzioni, la prima delle due, di uso più immediato, è acl_to_text,
il cui prototipo è:
77
vale a dire r per il permesso di lettura, w per il permesso di scrittura, x per il permesso di esecuzione (scritti
in quest’ordine) e - per l’assenza del permesso.
78
cioè per voci di tipo ACL_USER_OBJ e ACL_USER per user e ACL_GROUP_OBJ e ACL_GROUP per group.
5.4. CARATTERISTICHE E FUNZIONALITÀ AVANZATE 157
#include <sys/types.h>
#include <sys/acl.h>
char * acl_to_text(acl_t acl, ssize_t *len_p)
Produce la rappresentazione testuale di una ACL.
La funzione restituisce il puntatore ad una stringa con la rappresentazione testuale della ACL in
caso di successo e (acl t)NULL in caso di errore, nel qual caso errno assumerà uno dei valori:
ENOMEM non c’è memoria sufficiente per allocare i dati.
EINVAL la ACL indicata da acl non è valida.
#include <sys/types.h>
#include <sys/acl.h>
char * acl_to_any_text(acl_t acl, const char *prefix, char separator, int
options)
Produce la rappresentazione testuale di una ACL.
La funzione restituisce il puntatore ad una stringa con la rappresentazione testuale della ACL in
caso di successo e NULL in caso di errore, nel qual caso errno assumerà uno dei valori:
ENOMEM non c’è memoria sufficiente per allocare i dati.
EINVAL la ACL indicata da acl non è valida.
La funzione converte in formato testo la ACL indicata dall’argomento acl, usando il carattere
separator come separatore delle singole voci; se l’argomento prefix non è nullo la stringa da
esso indicata viene utilizzata come prefisso per le singole voci.
L’ultimo argomento, options, consente di controllare la modalità con cui viene generata
la rappresentazione testuale. Un valore nullo fa si che vengano usati gli identificatori standard
user, group, other e mask con i nomi di utenti e gruppi risolti rispetto ai loro valori numerici.
Altrimenti si può specificare un valore in forma di maschera binaria, da ottenere con un OR
aritmetico dei valori riportati in tab. 5.14.
Tipo Descrizione
TEXT_ABBREVIATE stampa le voci in forma abbreviata.
TEXT_NUMERIC_IDS non effettua la risoluzione numerica di user-ID e group-
ID.
TEXT_SOME_EFFECTIVE per ciascuna voce che contiene permessi che vengono
eliminati dalla ACL_MASK viene generato un commento
con i permessi effettivamente risultanti; il commento è
separato con un tabulatore.
TEXT_ALL_EFFECTIVE viene generato un commento con i permessi effettivi per
ciascuna voce che contiene permessi citati nella ACL_MASK,
anche quando questi non vengono modificati da essa; il
commento è separato con un tabulatore.
TEXT_SMART_INDENT da usare in combinazione con le precedenti
TEXT_SOME_EFFECTIVE e TEXT_ALL_EFFECTIVE aumenta
automaticamente il numero di spaziatori prima degli
eventuali commenti in modo da mantenerli allineati.
Come per acl_to_text anche in questo caso il buffer contenente la rappresentazione testua-
le dell’ACL, di cui la funzione restituisce l’indirizzo, viene allocato automaticamente, e dovrà
essere esplicitamente disallocato con una chiamata ad acl_free. Si tenga presente infine che
questa funzione è una estensione specifica di Linux, e non è presente nella bozza dello standard
POSIX.1e.
Per quanto utile per la visualizzazione o l’impostazione da comando delle ACL, la forma
testuale non è la più efficiente per poter memorizzare i dati relativi ad una ACL, ad esempio
quando si vuole eseguirne una copia a scopo di archiviazione. Per questo è stata prevista la
possibilità di utilizzare una rappresentazione delle ACL in una apposita forma binaria contigua e
persistente. È cosı̀ possibile copiare il valore di una ACL in un buffer e da questa rappresentazione
tornare indietro e generare una ACL.
Lo standard POSIX.1e prevede a tale scopo tre funzioni, la prima e più semplice è acl_size,
che consente di ottenere la dimensione che avrà la citata rappresentazione binaria, in modo da
poter allocare per essa un buffer di dimensione sufficiente, il suo prototipo è:
#include <sys/types.h>
#include <sys/acl.h>
ssize_t acl_size(acl_t acl)
Determina la dimensione della rappresentazione binaria di una ACL.
La funzione salverà la rappresentazione binaria della ACL indicata da acl sul buffer posto
all’indirizzo buf_p e lungo size byte, restituendo la dimensione della stessa come valore di
ritorno. Qualora la dimensione della rappresentazione ecceda il valore di size la funzione fallirà
con un errore di ERANGE. La funzione non ha nessun effetto sulla ACL indicata da acl.
Viceversa se si vuole ripristinare una ACL a partire dalla rappresentazione binaria della
stessa disponibile in un buffer si potrà usare la funzione acl_copy_int, il cui prototipo è:
#include <sys/types.h>
#include <sys/acl.h>
ssize_t acl_copy_int(const void *buf_p)
Ripristina la rappresentazione binaria di una ACL.
La funzione restituisce un oggetto di tipo acl_t in caso di successo e (acl_t)NULL in caso di
errore, nel qual caso errno assumerà uno dei valori:
EINVAL il buffer all’indirizzo buf_p non contiene una rappresentazione corretta di una ACL.
ENOMEM non c’è memoria sufficiente per allocare un oggetto acl_t per la ACL richiesta.
5.4. CARATTERISTICHE E FUNZIONALITÀ AVANZATE 159
La funzione in caso di successo alloca autonomamente un oggetto di tipo acl_t che viene
restituito come valore di ritorno con il contenuto della ACL rappresentata dai dati contenuti nel
buffer puntato da buf_p. Si ricordi che come per le precedenti funzioni l’oggetto acl_t dovrà
essere disallocato esplicitamente al termine del suo utilizzo.
Una volta che si disponga della ACL desiderata, questa potrà essere impostata su un file o
una directory. Per impostare una ACL sono disponibili due funzioni; la prima è acl_set_file,
che opera sia su file che su directory, ed il cui prototipo è:
#include <sys/types.h>
#include <sys/acl.h>
int acl_set_file(const char *path, acl_type_t type, acl_t acl)
Imposta una ACL su un file o una directory.
La funzione restituisce 0 in caso di successo e −1 in caso di errore, nel qual caso errno assumerà
uno dei valori:
EACCES o un generico errore di accesso a path o il valore di type specifica una ACL il cui tipo
non può essere assegnato a path.
EINVAL o acl non è una ACL valida, o type ha in valore non corretto.
ENOSPC non c’è spazio disco sufficiente per contenere i dati aggiuntivi della ACL.
ENOTSUP si è cercato di impostare una ACL su un file contenuto in un filesystem che non
supporta le ACL.
ed inoltre ENOENT, ENOTDIR, ENAMETOOLONG, EROFS, EPERM.
La funzione consente di assegnare la ACL contenuta in acl al file o alla directory indicate
dal pathname path, mentre con type si indica il tipo di ACL utilizzando le costanti di tab. 5.13,
ma si tenga presente che le ACL di default possono essere solo impostate qualora path indichi
una directory. Inoltre perché la funzione abbia successo la ACL dovrà essere valida, e contenere
tutti le voci necessarie, unica eccezione è quella in cui si specifica una ACL vuota per cancellare
la ACL di default associata a path.79 La seconda funzione che consente di impostare una ACL
è acl_set_fd, ed il suo prototipo è:
#include <sys/types.h>
#include <sys/acl.h>
int acl_set_fd(int fd, acl_t acl)
Imposta una ACL su un file descriptor.
La funzione restituisce 0 in caso di successo e −1 in caso di errore, nel qual caso errno assumerà
uno dei valori:
EINVAL o acl non è una ACL valida, o type ha in valore non corretto.
ENOSPC non c’è spazio disco sufficiente per contenere i dati aggiuntivi della ACL.
ENOTSUP si è cercato di impostare una ACL su un file contenuto in un filesystem che non
supporta le ACL.
ed inoltre EBADF, EROFS, EPERM.
La funzione è del tutto è analoga a acl_set_file ma opera esclusivamente sui file identificati
tramite un file descriptor. Non dovendo avere a che fare con directory (e con la conseguente
possibilità di avere una ACL di default) la funzione non necessita che si specifichi il tipo di ACL,
che sarà sempre di accesso, e prende come unico argomento, a parte il file descriptor, la ACL da
impostare.
Le funzioni viste finora operano a livello di una intera ACL, eseguendo in una sola volta
tutte le operazioni relative a tutte le voci in essa contenuta. In generale è possibile modificare
un singolo valore all’interno di una singola voce direttamente con le funzioni previste dallo
79
questo però è una estensione della implementazione delle ACL di Linux, la bozza di standard POSIX.1e
prevedeva l’uso della apposita funzione acl_delete_def_file, che prende come unico argomento il pathname
della directory di cui si vuole cancellare l’ACL di default, per i dettagli si ricorra alla pagina di manuale.
160 CAPITOLO 5. FILE E DIRECTORY
standard POSIX.1e. Queste funzioni però sono alquanto macchinose da utilizzare per cui è
molto più semplice operare direttamente sulla rappresentazione testuale. Questo è il motivo per
non tratteremo nei dettagli dette funzioni, fornendone solo una descrizione sommaria; chi fosse
interessato potrà ricorrere alle pagina di manuale.
Se si vuole operare direttamente sui contenuti di un oggetto di tipo acl_t infatti occor-
re fare riferimento alle singole voci tramite gli opportuni puntatori di tipo acl_entry_t, che
possono essere ottenuti dalla funzione acl_get_entry (per una voce esistente) o dalla funzio-
ne acl_create_entry per una voce da aggiungere. Nel caso della prima funzione si potrà poi
ripetere la lettura per ottenere i puntatori alle singole voci successive alla prima.
Una volta ottenuti detti puntatori si potrà operare sui contenuti delle singole voci; con le fun-
zioni acl_get_tag_type, acl_get_qualifier, acl_get_permset si potranno leggere rispetti-
vamente tipo, qualificatore e permessi mentre con le corrispondente funzioni acl_set_tag_type,
acl_set_qualifier, acl_set_permset si possono impostare i valori; in entrambi i casi vengono
utilizzati tipi di dato ad hoc.80 Si possono poi copiare i valori di una voce da una ACL ad un
altra con acl_copy_entry o eliminare una voce da una ACL con acl_delete_entry.
La funzione che consente di controllare tutti i vari aspetti della gestione delle quote è
quotactl, ed il suo prototipo è:
#include <sys/types.h>
#include <sys/quota.h>
quotactl(int cmd, const char *dev, int id, caddr_t addr)
Esegue una operazione di controllo sulle quote disco.
La funzione restituisce 0 in caso di successo e −1 in caso di errore, nel qual caso errno assumerà
uno dei valori:
EACCES il file delle quote non è un file ordinario.
EBUSY si è richiesto Q_QUOTAON ma le quote sono già attive.
EFAULT l’indirizzo addr non è valido.
EIO errore di lettura/scrittura sul file delle quote.
EMFILE non si può aprire il file delle quote avendo superato il limite sul numero di file aperti
nel sistema.
EINVAL o cmd non è un comando valido, o il dispositivo dev non esiste.
ENODEV dev non corrisponde ad un mount point attivo.
ENOPKG il kernel è stato compilato senza supporto per le quote.
ENOTBLK dev non è un dispositivo a blocchi.
EPERM non si hanno i permessi per l’operazione richiesta.
ESRCH è stato richiesto uno fra Q_GETQUOTA, Q_SETQUOTA, Q_SETUSE, Q_SETQLIM per un
filesystem senza quote attivate.
La funzione richiede che il filesystem sul quale si vuole operare sia montato con il supporto
delle quote abilitato; esso deve essere specificato con il nome del file di dispositivo nell’argomento
dev. Per le operazioni che lo richiedono inoltre si dovrà indicare con l’argomento id l’utente o
il gruppo (specificati rispettivamente per uid e gid) su cui si vuole operare. Alcune operazioni
usano l’argomento addr per indicare un indirizzo ad un area di memoria il cui utilizzo dipende
dall’operazione stessa.
Il tipo di operazione che si intende effettuare deve essere indicato tramite il primo argomento
cmd, questo in genere viene specificato con l’ausilio della macro QCMD:
int QCMD(subcmd,type)
Imposta il comando subcmd per il tipo di quote (utente o gruppo) type.
che consente di specificare, oltre al tipo di operazione, se questa deve applicarsi alle quote utente
o alle quote gruppo, nel qual caso type deve essere rispettivamente USRQUOTA o GRPQUOTA.
Le diverse operazioni supportate da quotactl, da indicare con l’argomento subcmd di QCMD,
sono riportate in tab. 5.15. In generale le operazione di attivazione, disattivazione e di modifica
dei limiti delle quote sono riservate e richiedono i privilegi di amministratore.83 Inoltre gli utenti
possono soltanto richiedere i dati relativi alle proprie quote, solo l’amministratore può ottenere
i dati di tutti.
Alcuni dei comandi di tab. 5.15 sono alquanto complessi e richiedono un approfondimento
maggiore, in particolare Q_GETQUOTA e Q_SETQUOTA fanno riferimento ad una specifica struttura
dqblk, la cui definizione è riportata in fig. 5.10,84 nella quale vengono inseriti i dati relativi alle
quote di un singolo utente.
La struttura viene usata sia con Q_GETQUOTA per ottenere i valori correnti dei limiti e dell’oc-
cupazione delle risorse, che con Q_SETQUOTA per effettuare modifiche ai limiti; come si può notare
83
per essere precisi tutte le operazioni indicate come privilegiate in tab. 5.15 richiedono la capability
CAP_SYS_ADMIN.
84
la definizione mostrata è quella usata fino dal kernel 2.4.22, non prenderemo in considerazione le versioni
obsolete.
162 CAPITOLO 5. FILE E DIRECTORY
Comando Descrizione
Q_QUOTAON Attiva l’applicazione delle quote disco per il filesystem indicato da dev,
si deve passare in addr il pathname al file che mantiene le quote, che
deve esistere, e id deve indicare la versione del formato con uno dei
valori di tab. 5.16; l’operazione richiede i privilegi di amministratore.
Q_QUOTAOFF Disattiva l’applicazione delle quote disco per il filesystem indicato da
dev, id e addr vengono ignorati; l’operazione richiede i privilegi di
amministratore.
Q_GETQUOTA Legge i limiti ed i valori correnti delle quote nel filesystem indicato da
dev per l’utente o il gruppo specificato da id; si devono avere i privilegi
di amministratore per leggere i dati relativi ad altri utenti o a gruppi di
cui non si fa parte, il risultato viene restituito in una struttura dqblk
all’indirizzo indicato da addr.
Q_SETQUOTA Imposta i limiti per le quote nel filesystem indicato da dev per l’u-
tente o il gruppo specificato da id secondo i valori ottenuti dalla
struttura dqblk puntata da addr; l’operazione richiede i privilegi di
amministratore.
Q_GETINFO Legge le informazioni (in sostanza i grace time) delle quote del filesy-
stem indicato da dev sulla struttura dqinfo puntata da addr, id viene
ignorato.
Q_SETINFO Imposta le informazioni delle quote del filesystem indicato da dev come
ottenuti dalla struttura dqinfo puntata da addr, id viene ignorato;
l’operazione richiede i privilegi di amministratore.
Q_GETFMT Richiede il valore identificativo (quello di tab. 5.16) per il formato del-
le quote attualmente in uso sul filesystem indicato da dev, che sarà
memorizzato sul buffer di 4 byte puntato da addr.
Q_SYNC Aggiorna la copia su disco dei dati delle quote del filesystem indicato
da dev; in questo caso dev può anche essere NULL nel qual caso verran-
no aggiornati i dati per tutti i filesystem con quote attive, id e addr
vengono comunque ignorati.
Q_GETSTATS Ottiene statistiche ed altre informazioni generali relative al sistema delle
quote per il filesystem indicato da dev, richiede che si passi come ar-
gomento addr l’indirizzo di una struttura dqstats, mentre i valori di
id e dev vengono ignorati; l’operazione è obsoleta e non supportata nei
kernel più recenti, che espongono la stessa informazione nei file sotto
/proc/self/fs/quota/.
struct dqblk
{
u_int64_t dqb_bhardlimit ; /* absolute limit on disk quota blocks alloc */
u_int64_t dqb_bsoftlimit ; /* preferred limit on disk quota blocks */
u_int64_t dqb_curspace ; /* current quota block count */
u_int64_t dqb_ihardlimit ; /* maximum # allocated inodes */
u_int64_t dqb_isoftlimit ; /* preferred inode limit */
u_int64_t dqb_curinodes ; /* current # allocated inodes */
u_int64_t dqb_btime ; /* time limit for excessive disk use */
u_int64_t dqb_itime ; /* time limit for excessive files */
u_int32_t dqb_valid ; /* bitmask of QIF_ * constants */
};
ci sono alcuni campi (in sostanza dqb_curspace, dqb_curinodes, dqb_btime, dqb_itime) che
hanno senso solo in lettura in quanto riportano uno stato non modificabile da quotactl, come
l’uso corrente di spazio e inode o il tempo che resta nel caso si sia superato un soft limit.
Inoltre in caso di modifica di un limite si può voler operare solo su una delle risorse (blocchi
5.4. CARATTERISTICHE E FUNZIONALITÀ AVANZATE 163
Identificatore Descrizione
QFMT_VFS_OLD il vecchio (ed obsoleto) formato delle quote.
QFMT_VFS_V0 la versione 0 usata dal VFS di Linux (supporta uid e gid a 32 bit e
limiti fino a 242 byte e 232 file.
QFMT_VFS_V1 la versione 1 usata dal VFS di Linux (supporta uid e GID a 32 bit e
limiti fino a 264 byte e 264 file.
Costante Descrizione
QIF_BLIMITS Limiti sui blocchi di spazio disco (dqb_bhardlimit e dqb_bsoftlimit).
QIF_SPACE Uso corrente dello spazio disco (dqb_curspace).
QIF_ILIMITS Limiti sugli inode (dqb_ihardlimit e dqb_isoftlimit).
QIF_INODES Uso corrente degli inode (dqb_curinodes).
QIF_BTIME Tempo di sforamento del soft limit sul numero di blocchi (dqb_btime).
QIF_ITIME Tempo di sforamento del soft limit sul numero di inode (dqb_itime).
QIF_LIMITS L’insieme di QIF_BLIMITS e QIF_ILIMITS.
QIF_USAGE L’insieme di QIF_SPACE e QIF_INODES.
QIF_TIMES L’insieme di QIF_BTIME e QIF_ITIME.
QIF_ALL Tutti i precedenti.
o inode);85 per questo la struttura prevede un campo apposito, dqb_valid, il cui scopo è quello
di indicare quali sono gli altri campi che devono essere considerati validi. Questo campo è una
maschera binaria che deve essere espressa nei termini di OR aritmetico delle apposite costanti di
tab. 5.17, dove si è riportato il significato di ciascuna di esse ed i campi a cui fanno riferimento.
In lettura con Q_SETQUOTA eventuali valori presenti in dqblk vengono comunque ignorati, al
momento la funzione sovrascrive tutti i campi e li marca come validi in dqb_valid. Si possono
invece usare QIF_BLIMITS o QIF_ILIMITS per richiedere di impostare solo la rispettiva tipologia
di limiti con Q_SETQUOTA. Si tenga presente che il sistema delle quote richiede che l’occupazione
di spazio disco sia indicata in termini di blocchi e non di byte; dato che questo dipende da come
si è creato il filesystem potrà essere necessario effettuare qualche controllo.86
Altre due operazioni che necessitano di un approfondimento sono Q_GETINFO e Q_SETINFO,
che sostanzialmente consentono di ottenere i dati relativi alle impostazioni delle altre proprietà
delle quote, che si riducono poi alla durata del grace time per i due tipi di limiti. In questo caso
queste si proprietà generali sono identiche per tutti gli utenti, per cui viene usata una operazione
distinta dalle precedenti. Anche in questo caso le due operazioni richiedono l’uso di una apposita
struttura dqinfo, la cui definizione è riportata in fig. 5.11.
struct dqinfo
{
u_int64_t dqi_bgrace ;
u_int64_t dqi_igrace ;
u_int32_t dqi_flags ;
u_int32_t dqi_valid ;
};
85
non è possibile modificare soltanto uno dei limiti (hard o soft) occorre sempre rispecificarli entrambi.
86
in genere viene usato un default di 1024 byte per blocco, ma quando si hanno file di dimensioni medie maggiori
può convenire usare valori più alti per ottenere prestazioni migliori in conseguenza di un minore frazionamento
dei dati e di indici più corti.
164 CAPITOLO 5. FILE E DIRECTORY
Come per dqblk anche in questo caso viene usato un campo della struttura, dqi_valid
come maschera binaria per dichiarare quale degli altri campi sono validi; le costanti usate per
comporre questo valore sono riportate in tab. 5.18 dove si è riportato il significato di ciascuna
di esse ed i campi a cui fanno riferimento.
Costante Descrizione
IIF_BGRACE Il grace period per i blocchi (dqi_bgrace).
IIF_IGRACE Il grace period per gli inode (dqi_igrace).
IIF_FLAGS I flag delle quote (dqi_flags) (inusato ?).
IIF_ALL Tutti i precedenti.
Come in precedenza con Q_GETINFO tutti i valori vengono letti sovrascrivendo il contenuto
di dqinfo e marcati come validi in dqi_valid. In scrittura con Q_SETINFO si può scegliere quali
impostare, si tenga presente che i tempi dei campi dqi_bgrace e dqi_igrace devono essere
specificati in secondi.
Come esempi dell’uso di quotactl utilizzeremo estratti del codice di un modulo Python usato
per fornire una interfaccia diretta a quotactl senza dover passare dalla scansione dei risultati
di un comando. Il modulo si trova fra i pacchetti Debian messi a disposizione da Truelite Srl,
all’indirizzo http://labs.truelite.it/projects/packages.87
Il primo esempio, riportato in fig. 5.12, riporta il codice della funzione che consente di leggere
le quote. La funzione fa uso dell’interfaccia dal C verso Python, che definisce i vari simboli Py*
(tipi di dato e funzioni). Non staremo ad approfondire i dettagli di questa interfaccia, per la
quale esistono numerose trattazioni dettagliate, ci interessa solo esaminare l’uso di quotactl.
In questo caso la funzione prende come argomenti (1) l’intero who che indica se si vuole
operare sulle quote utente o gruppo, l’identificatore id dell’utente o del gruppo scelto, ed il
nome del file di dispositivo del filesystem su cui si sono attivate le quote.88 Questi argomenti
vengono passati direttamente alla chiamata a quotactl (5), a parte who che viene abbinato con
QCMD al comando Q_GETQUOTA per ottenere i dati.
87
in particolare il codice C del modulo è nel file quotamodule.c visionabile a partire dall’indirizzo indicato nella
sezione Repository.
88
questi vengono passati come argomenti dalle funzioni mappate come interfaccia pubblica del modulo (una per
gruppi ed una per gli utenti) che si incaricano di decodificare i dati passati da una chiamata nel codice Python.
5.4. CARATTERISTICHE E FUNZIONALITÀ AVANZATE 165
La funzione viene eseguita all’interno di un condizionale (5-16) che in caso di successo prov-
vede a costruire (6-12) opportunamente una risposta restituendo tramite la opportuna funzione
di interfaccia un oggetto Python contenente i dati della struttura dqblk relativi a uso corrente
e limiti sia per i blocchi che per gli inode. In caso di errore (13-15) si usa un’altra funzione
dell’interfaccia per passare il valore di errno come eccezione.
1 PyObject * set_block_quota ( int who , int id , const char * dev , int soft , int hard )
2 {
3 struct dqblk dq ;
4
5 dq . dqb_bsoftlimit = soft ;
6 dq . dqb_bhardlimit = hard ;
7 dq . dqb_valid = QIF_BLIMITS ;
8
9 if (! quotactl ( QCMD ( Q_SETQUOTA , who ) , dev , id , ( caddr_t ) & dq )) {
10 Py_RETURN_NONE ;
11 } else {
12 PyErr_SetFromErrno ( PyExc_OSError );
13 return NULL ;
14 }
15 }
Figura 5.13: Esempio di codice per impostare i limiti sullo spazio disco.
Per impostare i limiti sullo spazio disco si potrà usare una seconda funzione, riportata in
fig. 5.13, che prende gli stessi argomenti della precedente, con lo stesso significato, a cui si aggiun-
gono i valori per il soft limit e l’hard limit. In questo caso occorrerà, prima di chiamare quotactl,
inizializzare opportunamente (5-7) i campi della struttura dqblk che si vogliono utilizzare (quelli
relativi ai limiti sui blocchi) e specificare gli stessi con QIF_BLIMITS in dq.dqb_valid.
Fatto questo la chiamata a quotactl, stavolta con il comando Q_SETQUOTA, viene eseguita
come in precedenza all’interno di un condizionale (9-14). In questo caso non essendovi da re-
stituire nessun dato in caso di successo si usa (10) una apposita funzione di uscita, mentre si
restituisce come prima una eccezione con il valore di errno in caso di errore (12-13).
all’amministratore in un insieme di capacità distinte. L’idea era che queste capacità potessero
essere abilitate e disabilitate in maniera indipendente per ciascun processo con privilegi di am-
ministratore, permettendo cosı̀ una granularità molto più fine nella distribuzione degli stessi che
evitasse la originaria situazione di “tutto o nulla”.
Il meccanismo completo delle capabilities 91 prevede inoltre la possibilità di associare le stesse
ai singoli file eseguibili, in modo da poter stabilire quali capacità possono essere utilizzate quando
viene messo in esecuzione uno specifico programma; ma il supporto per questa funzionalità,
chiamata file capabilities, è stato introdotto soltanto a partire dal kernel 2.6.24. Fino ad allora
doveva essere il programma stesso ad eseguire una riduzione esplicita delle sue capacità, cosa
che ha reso l’uso di questa funzionalità poco diffuso, vista la presenza di meccanismi alternativi
per ottenere limitazioni delle capacità dell’amministratore a livello di sistema operativo, come
SELinux.
Con questo supporto e con le ulteriori modifiche introdotte con il kernel 2.6.25 il meccanismo
delle capabilities è stato totalmente rivoluzionato, rendendolo più aderente alle intenzioni origi-
nali dello standard POSIX, rimuovendo il significato che fino ad allora aveva avuto la capacità
CAP_SETPCAP e cambiando le modalità di funzionamento del cosiddetto capabilities bounding set.
Ulteriori modifiche sono state apportate con il kernel 2.6.26 per consentire la rimozione non
ripristinabile dei privilegi di amministratore. Questo fa sı̀ che il significato ed il comportamento
del kernel finisca per dipendere dalla versione dello stesso e dal fatto che le nuove file capabilities
siano abilitate o meno. Per capire meglio la situazione e cosa è cambiato conviene allora spiegare
con maggiori dettagli come funziona il meccanismo delle capabilities.
Il primo passo per frazionare i privilegi garantiti all’amministratore, supportato fin dalla
introduzione iniziale del kernel 2.2, è stato quello in cui a ciascun processo sono stati associati
tre distinti insiemi di capabilities, denominati rispettivamente permitted, inheritable ed effective.
Questi insiemi vengono mantenuti in forma di tre diverse maschere binarie,92 in cui ciascun bit
corrisponde ad una capacità diversa.
L’utilizzo di tre distinti insiemi serve a fornire una interfaccia flessibile per l’uso delle ca-
pabilities, con scopi analoghi a quelli per cui sono mantenuti i diversi insiemi di identificatori
di sez. 3.3.2; il loro significato, che è rimasto sostanzialmente lo stesso anche dopo le modifiche
seguite alla introduzione delle file capabilities è il seguente:
permitted l’insieme delle capabilities “permesse”, cioè l’insieme di quelle capacità che un pro-
cesso può impostare come effettive o come ereditabili. Se un processo cancella una
capacità da questo insieme non potrà più riassumerla.93
inheritable l’insieme delle capabilities “ereditabili”, cioè di quelle che verranno trasmesse come
insieme delle permesse ad un nuovo programma eseguito attraverso una chiamata
ad exec.
effective l’insieme delle capabilities “effettive”, cioè di quelle che vengono effettivamente
usate dal kernel quando deve eseguire il controllo di accesso per le varie operazioni
compiute dal processo.
Con l’introduzione delle file capabilities sono stati introdotti altri tre insiemi associabili a
ciascun file.94 Le file capabilities hanno effetto soltanto quando il file che le porta viene eseguito
91
l’implementazione si rifà ad una bozza di quello che doveva diventare lo standard POSIX.1e, poi abbandonato.
92
il kernel li mantiene, come i vari identificatori di sez. 3.3.2, all’interno della task_struct di ciascun processo
(vedi fig. 3.2), nei tre campi cap_effective, cap_inheritable, cap_permitted del tipo kernel_cap_t; questo
era, fino al kernel 2.6.25 definito come intero a 32 bit per un massimo di 32 capabilities distinte, attualmente è
stato aggiornato ad un vettore in grado di mantenerne fino a 64.
93
questo nei casi ordinari, sono previste però una serie di eccezioni, dipendenti anche dal tipo di supporto, che
vedremo meglio in seguito dato il notevole intreccio nella casistica.
94
la realizzazione viene eseguita con l’uso di uno specifico attributo esteso, security.capability, la cui modifica
è riservata, (come illustrato in sez. 5.4.1) ai processi dotato della capacità CAP_SYS_ADMIN.
5.4. CARATTERISTICHE E FUNZIONALITÀ AVANZATE 167
come programma con una exec, e forniscono un meccanismo che consente l’esecuzione dello
stesso con maggiori privilegi; in sostanza sono una sorta di estensione del suid bit limitato ai
privilegi di amministratore. Anche questi tre insiemi sono identificati con gli stessi nomi di quello
dei processi, ma il loro significato è diverso:
permitted (chiamato originariamente forced ) l’insieme delle capacità che con l’esecuzione del
programma verranno aggiunte alle capacità permesse del processo.
inheritable (chiamato originariamente allowed ) l’insieme delle capacità che con l’esecuzione del
programma possono essere ereditate dal processo originario (che cioè non vengono
tolte dall’inheritable set del processo originale all’esecuzione di exec).
effective in questo caso non si tratta di un insieme ma di un unico valore logico; se attivo al-
l’esecuzione del programma tutte le capacità che risulterebbero permesse verranno
pure attivate, inserendole automaticamente nelle effettive, se disattivato nessuna
capacità verrà attivata (cioè l’effective set resterà vuoto).
Infine come accennato, esiste un ulteriore insieme, chiamato capabilities bounding set, il cui
scopo è quello di costituire un limite alle capacità che possono essere attivate per un programma.
Il suo funzionamento però è stato notevolmente modificato con l’introduzione delle file capabilities
e si deve pertanto prendere in considerazione una casistica assai complessa.
Per i kernel fino al 2.6.25, o se non si attiva il supporto per le file capabilities, il capa-
bilities bounding set è un parametro generale di sistema, il cui valore viene riportato nel file
/proc/sys/kernel/cap-bound. Il suo valore iniziale è definito in sede di compilazione del kernel,
e da sempre ha previsto come default la presenza di tutte le capabilities eccetto CAP_SETPCAP.
In questa situazione solo il primo processo eseguito nel sistema (quello con pid 1, di norma
/sbin/init) ha la possibilità di modificarlo; ogni processo eseguito successivamente, se dotato
dei privilegi di amministratore, è in grado soltanto di rimuovere una delle capabilities già presenti
dell’insieme.95
In questo caso l’effetto complessivo del capabilities bounding set è che solo le capacità in
esso presenti possono essere trasmesse ad un altro programma attraverso una exec. Questo in
sostanza significa che se un qualunque programma elimina da esso una capacità, considerato che
init (almeno nelle versioni ordinarie) non supporta la reimpostazione del bounding set, questa
non sarà più disponibile per nessun processo a meno di un riavvio, eliminando cosı̀ in forma
definitiva quella capacità per tutti, compreso l’amministratore.96
Con il kernel 2.6.25 e le file capabilities il bounding set è diventato una proprietà di ciascun
processo, che viene propagata invariata sia attraverso una fork che una exec. In questo caso il file
/proc/sys/kernel/cap-bound non esiste e init non ha nessun ruolo speciale, inoltre in questo
caso all’avvio il valore iniziale prevede la presenza di tutte le capacità (compresa CAP_SETPCAP).
Con questo nuovo meccanismo il bounding set continua a ricoprire un ruolo analogo al prece-
dente nel passaggio attraverso una exec, come limite alle capacità che possono essere aggiunte
al processo in quanto presenti nel permitted set del programma messo in esecuzione, in sostanza
il nuovo programma eseguito potrà ricevere una capacità presente nel suo permitted set (quello
del file) solo se questa è anche nel bounding set (del processo). In questo modo si possono ri-
muovere definitivamente certe capacità da un processo, anche qualora questo dovesse eseguire
un programma privilegiato che prevede di riassegnarle.
Si tenga presente però che in questo caso il bounding set blocca esclusivamente le capacità
indicate nel permitted set del programma che verrebbero attivate in caso di esecuzione, e non
quelle eventualmente già presenti nell’inheritable set del processo (ad esempio perché presenti
95
per essere precisi occorreva la capacità CAP_SYS_MODULE.
96
la qual cosa, visto il default usato per il capabilities bounding set, significa anche che CAP_SETPCAP non è stata
praticamente mai usata nella sua forma originale.
168 CAPITOLO 5. FILE E DIRECTORY
prima di averle rimosse dal bounding set). In questo caso eseguendo un programma che abbia
anche lui dette capacità nel suo inheritable set queste verrebbero assegnate.
In questa seconda versione inoltre il bounding set costituisce anche un limite per le capacità
che possono essere aggiunte all’inheritable set del processo stesso con capset, sempre nel senso
che queste devono essere presenti nel bounding set oltre che nel permitted set del processo.
Questo limite vale anche per processi con i privilegi di amministratore,97 per i quali invece non
vale la condizione che le capabilities da aggiungere nell’inheritable set debbano essere presenti
nel proprio permitted set.98
Come si può notare per fare ricorso alle capabilities occorre comunque farsi carico di una
notevole complessità di gestione, aggravata dalla presenza di una radicale modifica del loro
funzionamento con l’introduzione delle file capabilities. Considerato che il meccanismo originale
era incompleto e decisamente problematico nel caso di programmi che non ne sapessero tener
conto,99 ci soffermeremo solo sulla implementazione completa presente a partire dal kernel 2.6.25,
tralasciando ulteriori dettagli riguardo la versione precedente.
Riassumendo le regole finora illustrate tutte le capabilities vengono ereditate senza modifiche
attraverso una fork mentre, indicati con orig_* i valori degli insiemi del processo chiamante,
con file_* quelli del file eseguito e con bound_set il capabilities bounding set, dopo l’invocazione
di exec il processo otterrà dei nuovi insiemi di capacità new_* secondo la formula (espressa in
pseudo-codice C) di fig. 5.14; si noti come in particolare il capabilities bounding set non viene
comunque modificato e resta lo stesso sia attraverso una fork che attraverso una exec.
Figura 5.14: Espressione della modifica delle capabilities attraverso una exec.
1. se si passa da effective user-ID nullo a non nullo l’effective set del processo viene totalmente
azzerato, se viceversa si passa da effective user-ID non nullo a nullo il permitted set viene
copiato nell’effective set;
2. se si passa da file system user-ID nullo a non nullo verranno cancellate dall’effective set
del processo tutte le capacità attinenti i file, e cioè CAP_LINUX_IMMUTABLE, CAP_MKNOD,
CAP_DAC_OVERRIDE, CAP_DAC_READ_SEARCH, CAP_MAC_OVERRIDE, CAP_CHOWN, CAP_FSETID
e CAP_FOWNER (le prime due a partire dal kernel 2.2.30), nella transizione inversa verranno
invece inserite nell’effective set quelle capacità della precedente lista che sono presenti nel
suo permitted set.
3. se come risultato di una transizione riguardante gli identificativi dei gruppi real, saved ed
effective in cui si passa da una situazione in cui uno di questi era nullo ad una in cui sono
tutti non nulli,100 verranno azzerati completamente sia il permitted set che l’effective set.
La combinazione di tutte queste regole consente di riprodurre il comportamento ordinario di
un sistema di tipo Unix tradizionale, ma può risultare problematica qualora si voglia passare ad
una configurazione di sistema totalmente basata sull’applicazione delle capabilities; in tal caso
infatti basta ad esempio eseguire un programma con suid bit di proprietà dell’amministratore
per far riottenere ad un processo tutte le capacità presenti nel suo bounding set, anche se si era
avuta la cura di cancellarle dal permitted set.
Per questo motivo a partire dal kernel 2.6.26, se le file capabilities sono abilitate, ad ogni
processo viene stata associata una ulteriore maschera binaria, chiamata securebits flags, su cui
sono mantenuti una serie di flag (vedi tab. 5.19) il cui valore consente di modificare que-
ste regole speciali che si applicano ai processi con user-ID nullo. La maschera viene sempre
mantenuta attraverso una fork, mentre attraverso una exec viene sempre cancellato il flag
SECURE_KEEP_CAPS.
Flag Descrizione
SECURE_KEEP_CAPS Il processo non subisce la cancellazione delle sue capabilities quando
tutti i suoi user-ID passano ad un valore non nullo (regola di compa-
tibilità per il cambio di user-ID n. 3 del precedente elenco), sostituisce
il precedente uso dell’operazione PR_SET_KEEPCAPS di prctl.
SECURE_NO_SETUID_FIXUP Il processo non subisce le modifiche delle sue capabilities nel passaggio
da nullo a non nullo degli user-ID dei gruppi effective e file system
(regole di compatibilità per il cambio di user-ID nn. 1 e 2 del precedente
elenco).
SECURE_NOROOT Il processo non assume nessuna capacità aggiuntiva quando esegue un
programma, anche se ha user-ID nullo o il programma ha il suid bit
attivo ed appartiene all’amministratore (regola di compatibilità per
l’esecuzione di programmi senza capabilities).
Tabella 5.19: Costanti identificative dei flag che compongono la maschera dei securebits.
A ciascuno dei flag di tab. 5.19 è inoltre abbinato un corrispondente flag di blocco, identi-
ficato da una costante omonima con l’estensione _LOCKED, la cui attivazione è irreversibile ed
ha l’effetto di rendere permanente l’impostazione corrente del corrispondente flag ordinario; in
sostanza con SECURE_KEEP_CAPS_LOCKED si rende non più modificabile SECURE_KEEP_CAPS, ed
analogamente avviene con SECURE_NO_SETUID_FIXUP_LOCKED per SECURE_NO_SETUID_FIXUP e
con SECURE_NOROOT_LOCKED per SECURE_NOROOT.
Per l’impostazione di questi flag sono stata predisposte due specifiche operazioni di prctl
(vedi sez. 3.5.2), PR_GET_SECUREBITS, che consente di ottenerne il valore, e PR_SET_SECUREBITS,
che consente di modificarne il valore; per quest’ultima sono comunque necessari i privilegi di
amministratore ed in particolare la capacità CAP_SETPCAP. Prima dell’introduzione dei securebits
100
in sostanza questo è il caso di quando si chiama setuid per rimuovere definitivamente i privilegi di
amministratore da un processo.
170 CAPITOLO 5. FILE E DIRECTORY
era comunque possibile ottenere lo stesso effetto di SECURE_KEEP_CAPS attraverso l’uso di un’altra
operazione di prctl, PR_SET_KEEPCAPS.
Oltre alla gestione dei securebits la nuova versione delle file capabilities prevede l’uso di
prctl anche per la gestione del capabilities bounding set, attraverso altre due operazioni dedicate,
PR_CAPBSET_READ per controllarne il valore e PR_CAPBSET_DROP per modificarlo; quest’ultima
di nuovo è una operazione privilegiata che richiede la capacità CAP_SETPCAP e che, come indica
chiaramente il nome, permette solo la rimozione di una capability dall’insieme; per i dettagli
sull’uso di tutte queste operazioni si rimanda alla rilettura di sez. 3.5.2.
Un elenco delle delle capabilities disponibili su Linux, con una breve descrizione ed il nome
delle costanti che le identificano, è riportato in tab. 5.20;101 la tabella è divisa in due parti,
la prima riporta le capabilities previste anche nella bozza dello standard POSIX1.e, la seconda
quelle specifiche di Linux. Come si può notare dalla tabella alcune capabilities attengono a
singole funzionalità e sono molto specializzate, mentre altre hanno un campo di applicazione
molto vasto, che è opportuno dettagliare maggiormente.
Prima di dettagliare il significato della capacità più generiche, conviene però dedicare un
discorso a parte a CAP_SETPCAP, il cui significato è stato completamente cambiato con l’intro-
duzione delle file capabilities nel kernel 2.6.24. In precedenza questa capacità era quella che
permetteva al processo che la possedeva di impostare o rimuovere le capabilities che fossero pre-
senti nel permitted set del chiamante di un qualunque altro processo. In realtà questo non è mai
stato l’uso inteso nelle bozze dallo standard POSIX, ed inoltre, come si è già accennato, dato
che questa capacità è assente nel capabilities bounding set usato di default, essa non è neanche
mai stata realmente disponibile.
Con l’introduzione file capabilities e il cambiamento del significato del capabilities bounding
set la possibilità di modificare le capacità di altri processi è stata completamente rimossa, e
CAP_SETPCAP ha acquisito quello che avrebbe dovuto essere il suo significato originario, e cioè
la capacità del processo di poter inserire nel suo inheritable set qualunque capacità presente
nel bounding set. Oltre a questo la disponibilità di CAP_SETPCAP consente ad un processo di
eliminare una capacità dal proprio bounding set (con la conseguente impossibilità successiva di
eseguire programmi con quella capacità), o di impostare i securebits delle capabilities.
La prima fra le capacità “ampie” che occorre dettagliare maggiormente è CAP_FOWNER, che
rimuove le restrizioni poste ad un processo che non ha la proprietà di un file in un vasto campo di
operazioni;103 queste comprendono i cambiamenti dei permessi e dei tempi del file (vedi sez. 5.3.3
e sez. 5.2.4), le impostazioni degli attributi dei file (vedi sez. 6.3.7) e delle ACL (vedi sez. 5.4.1
e 5.4.2), poter ignorare lo sticky bit nella cancellazione dei file (vedi sez. 5.3.2), la possibilità di
impostare il flag di O_NOATIME con open e fcntl (vedi sez. 6.2.1 e sez. 6.3.6) senza restrizioni.
Una seconda capacità che copre diverse operazioni, in questo caso riguardanti la rete, è
CAP_NET_ADMIN, che consente di impostare le opzioni privilegiate dei socket (vedi sez. 17.2.2),
abilitare il multicasting, eseguire la configurazione delle interfacce di rete (vedi sez. 17.3.2) ed
impostare la tabella di instradamento.
Una terza capability con vasto campo di applicazione è CAP_SYS_ADMIN, che copre una serie di
operazioni amministrative, come impostare le quote disco (vedi sez.5.4.3), attivare e disattivare la
swap, montare, rimontare e smontare filesystem (vedi sez. 8.2.2), effettuare operazioni di controllo
su qualunque oggetto dell’IPC di SysV (vedi sez. 11.2), operare sugli attributi estesi dei file di
classe security o trusted (vedi sez. 5.4.1), specificare un user-ID arbitrario nella trasmissione
delle credenziali dei socket (vedi sez. ??), assegnare classi privilegiate (IOPRIO_CLASS_RT e prima
101
l’elenco presentato questa tabella, ripreso dalla pagina di manuale (accessibile con man capabilities) e dalle
definizioni in linux/capabilities.h, è aggiornato al kernel 2.6.26.
102
vale a dire i permessi caratteristici del modello classico del controllo di accesso chiamato Discrectionary Access
Control (da cui il nome DAC).
103
vale a dire la richiesta che l’user-ID effettivo del processo (o meglio il filesystem user-ID, vedi sez. 3.3.2)
coincida con quello del proprietario.
5.4. CARATTERISTICHE E FUNZIONALITÀ AVANZATE 171
Capacità Descrizione
CAP_AUDIT_CONTROL La capacità di abilitare e disabilitare il controllo dell’auditing (dal kernel 2.6.11).
CAP_AUDIT_WRITE La capacità di scrivere dati nel giornale di auditing del kernel (dal kernel 2.6.11).
CAP_CHOWN La capacità di cambiare proprietario e gruppo proprietario di un file (vedi sez. 5.3.4).
CAP_DAC_OVERRIDE La capacità di evitare il controllo dei permessi di lettura, scrittura ed esecuzione dei
file,102 (vedi sez. 5.3).
CAP_DAC_READ_SEARCH La capacità di evitare il controllo dei permessi di lettura ed esecuzione per le directory
(vedi sez. 5.3).
CAP_FOWNER La capacità di evitare il controllo della proprietà di un file per tutte le operazioni
privilegiate non coperte dalle precedenti CAP_DAC_OVERRIDE e CAP_DAC_READ_SEARCH.
CAP_FSETID La capacità di evitare la cancellazione automatica dei bit suid e sgid quando un file
per i quali sono impostati viene modificato da un processo senza questa capacità e
la capacità di impostare il bit sgid su un file anche quando questo è relativo ad un
gruppo cui non si appartiene (vedi sez. 5.3.3).
CAP_KILL La capacità di mandare segnali a qualunque processo (vedi sez. 9.3.3).
CAP_SETFCAP La capacità di impostare le capabilities di un file (dal kernel 2.6.24).
CAP_SETGID La capacità di manipolare i group ID dei processi, sia il principale che i supplementari,
(vedi sez. 3.3.3) che quelli trasmessi tramite i socket unix domain (vedi sez. 18.2).
CAP_SETUID La capacità di manipolare gli user ID del processo (vedi sez. 3.3.2) e di trasmettere
un user ID arbitrario nel passaggio delle credenziali coi socket unix domain (vedi
sez. 18.2).
CAP_IPC_LOCK La capacità di effettuare il memory locking con le funzioni mlock, mlockall, shmctl,
mmap (vedi sez. 2.2.4 e sez. 12.4.1).
CAP_IPC_OWNER La capacità di evitare il controllo dei permessi per le operazioni sugli oggetti di
intercomunicazione fra processi (vedi sez. 11.2).
CAP_LEASE La capacità di creare dei file lease (vedi sez. 12.3.2) pur non essendo proprietari del
file (dal kernel 2.4).
CAP_LINUX_IMMUTABLE La capacità di impostare sui file gli attributi immutable e append only (se supportati).
CAP_MKNOD La capacità di creare file di dispositivo con mknod (vedi sez. 5.1.5) (dal kernel 2.4).
CAP_NET_ADMIN La capacità di eseguire alcune operazioni privilegiate sulla rete.
CAP_NET_BIND_SERVICE La capacità di porsi in ascolto su porte riservate (vedi sez. 16.2.1).
CAP_NET_BROADCAST La capacità di consentire l’uso di socket in broadcast e multicast.
CAP_NET_RAW La capacità di usare socket RAW e PACKET (vedi sez. 15.2.3).
CAP_SETPCAP La capacità di modifiche privilegiate alle capabilities.
CAP_SYS_ADMIN La capacità di eseguire una serie di compiti amministrativi.
CAP_SYS_BOOT La capacità di fare eseguire un riavvio del sistema (vedi sez. ??).
CAP_SYS_CHROOT La capacità di eseguire la funzione chroot (vedi sez. 5.4.5).
CAP_MAC_ADMIN La capacità amministrare il Mandatory Access Control di Smack (dal kernel 2.6.25).
CAP_MAC_OVERRIDE La capacità evitare il Mandatory Access Control di Smack (dal kernel 2.6.25).
CAP_SYS_MODULE La capacità di caricare e rimuovere moduli del kernel.
CAP_SYS_NICE La capacità di modificare le varie priorità dei processi (vedi sez. 3.4).
CAP_SYS_PACCT La capacità di usare le funzioni di accounting dei processi (vedi sez. 8.3.4).
CAP_SYS_PTRACE La capacità di tracciare qualunque processo con ptrace (vedi sez. 3.5.3).
CAP_SYS_RAWIO La capacità di operare sulle porte di I/O con ioperm e iopl (vedi sez. 3.5.4).
CAP_SYS_RESOURCE La capacità di superare le varie limitazioni sulle risorse.
CAP_SYS_TIME La capacità di modificare il tempo di sistema (vedi sez. 8.4).
CAP_SYS_TTY_CONFIG La capacità di simulare un hangup della console, con la funzione vhangup.
CAP_SYSLOG La capacità di gestire il buffer dei messaggi del kernel, (vedi sez. 10.1.5), introdotta
dal kernel 2.6.38 come capacità separata da CAP_SYS_ADMIN.
CAP_WAKE_ALARM La capacità di usare i timer di tipo CLOCK_BOOTTIME_ALARM e CLOCK_REALTIME_ALARM,
vedi sez. 9.5.2 (dal kernel 3.0).
del kernel 2.6.25 anche IOPRIO_CLASS_IDLE) per lo scheduling dell’I/O (vedi sez. 3.4.5), superare
il limite di sistema sul numero massimo di file aperti,104 effettuare operazioni privilegiate sulle
chiavi mantenute dal kernel (vedi sez. ??), usare la funzione lookup_dcookie, usare CLONE_NEWNS
con unshare e clone, (vedi sez. 3.5.1).
Originariamente CAP_SYS_NICE riguardava soltanto la capacità di aumentare le priorità di
esecuzione dei processi, come la diminuzione del valore di nice (vedi sez. 3.4.2), l’uso delle priorità
real-time (vedi sez. 3.4.3), o l’impostazione delle affinità di processore (vedi sez. 3.4.4); ma con
l’introduzione di priorità anche riguardo le operazioni di accesso al disco, e, nel caso di sistemi
NUMA, alla memoria, essa viene a coprire anche la possibilità di assegnare priorità arbitrarie
nell’accesso a disco (vedi sez. 3.4.5) e nelle politiche di allocazione delle pagine di memoria ai
nodi di un sistema NUMA.
Infine la capability CAP_SYS_RESOURCE attiene alla possibilità di superare i limiti imposti
sulle risorse di sistema, come usare lo spazio disco riservato all’amministratore sui filesystem
che lo supportano, usare la funzione ioctl per controllare il journaling sul filesystem ext3, non
subire le quote disco, aumentare i limiti sulle risorse di un processo (vedi sez. 8.3.2) e quelle
sul numero di processi, ed i limiti sulle dimensioni dei messaggi delle code del SysV IPC (vedi
sez. 11.2.4).
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 l’interfaccia 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:
ESRCH si è fatto riferimento ad un processo inesistente.
EPERM si è tentato di aggiungere una capacità nell’insieme delle capabilities permesse, o di
impostare una capacità non presente nell’insieme di quelle permesse negli insieme delle
effettive o ereditate, o si è cercato di impostare una capability di un altro processo
senza avare CAP_SETPCAP.
ed inoltre EFAULT ed EINVAL.
Queste due funzioni prendono come argomenti due tipi di dati dedicati, definiti come pun-
tatori a due strutture specifiche di Linux, illustrate in fig. 5.15. Per un certo periodo di tempo
era anche indicato che per poterle utilizzare fosse necessario che la macro _POSIX_SOURCE risul-
tasse non definita (ed era richiesto di inserire una istruzione #undef _POSIX_SOURCE prima di
includere sys/capability.h) requisito che non risulta più presente.105
Si tenga presente che le strutture di fig. 5.15, come i prototipi delle due funzioni capget e
capset, sono soggette ad essere modificate con il cambiamento del kernel (in particolare i tipi
di dati delle strutture) ed anche se finora l’interfaccia è risultata stabile, non c’è nessuna assi-
curazione che questa venga mantenuta,106 Pertanto se si vogliono scrivere programmi portabili
che possano essere eseguiti senza modifiche o adeguamenti su qualunque versione del kernel è
opportuno utilizzare le interfacce di alto livello che vedremo più avanti.
La struttura a cui deve puntare l’argomento hdrp serve ad indicare, tramite il campo pid, il
PID del processo del quale si vogliono leggere o modificare le capabilities. Con capset questo,
104
quello indicato da /proc/sys/fs/file-max.
105
e non è chiaro neanche quanto sia mai stato davvero necessario.
106
viene però garantito che le vecchie funzioni continuino a funzionare.
5.4. CARATTERISTICHE E FUNZIONALITÀ AVANZATE 173
# define _ LI N UX _ CA PA B IL I TY _ VE RS I ON _ 1 0 x19980330
# define _ LINU X_CAPA BILIT Y_U32S _1 1
# define _ LI N UX _ CA PA B IL I TY _ VE RS I ON _ 3 0 x20080522
# define _ LINU X_CAPA BILIT Y_U32S _3 2
Figura 5.15: Definizione delle strutture a cui fanno riferimento i puntatori cap_user_header_t e
cap_user_data_t usati per l’interfaccia di gestione di basso livello delle capabilities.
se si usano le file capabilities, può essere solo 0 o PID del processo chiamante, che sono equi-
valenti. Il campo version deve essere impostato al valore della versione delle stesse usata dal
kernel (quello indicato da una delle costanti _LINUX_CAPABILITY_VERSION_n di fig. 5.15) altri-
menti le funzioni ritorneranno con un errore di EINVAL, restituendo nel campo stesso il valore
corretto della versione in uso. La versione due è comunque deprecata e non deve essere usata
(il kernel stamperà un avviso). I valori delle capabilities devono essere passati come maschere
binarie;107 con l’introduzione delle capabilities a 64 bit inoltre il puntatore datap non può essere
più considerato come relativo ad una singola struttura, ma ad un vettore di due strutture.108
Dato che le precedenti funzioni, oltre ad essere specifiche di Linux, non garantiscono la
stabilità nell’interfaccia, è sempre opportuno effettuare la gestione delle capabilities utilizzando
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,109
pertanto se un programma le utilizza si dovrà indicare esplicitamente l’uso della suddetta libreria
attraverso l’opzione -lcap del compilatore.
Le funzioni dell’interfaccia delle bozze di POSIX.1e prevedono l’uso di uno tipo di dato opaco,
cap_t, come puntatore ai dati mantenuti nel cosiddetto capability state,110 in sono memorizzati
tutti i dati delle capabilities. In questo modo è possibile mascherare i dettagli della gestione di
basso livello, che potranno essere modificati senza dover cambiare le funzioni dell’interfaccia,
che faranno riferimento soltanto ad oggetti di questo tipo. L’interfaccia pertanto non soltanto
fornisce le funzioni per modificare e leggere le capabilities, ma anche quelle per gestire i dati
attraverso cap_t.
La prima funzione dell’interfaccia è quella che permette di inizializzare un capability state,
107
e si tenga presente che i valori di tab. 5.20 non possono essere combinati direttamente, indicando il numero
progressivo del bit associato alla relativa capacità.
108
è questo cambio di significato che ha portato a deprecare la versione 2, che con capget poteva portare ad
un buffer overflow per vecchie applicazioni che continuavano a considerare datap come puntatore ad una singola
struttura.
109
la libreria è libcap2, nel caso di Debian può essere installata con il pacchetto omonimo.
110
si tratta in sostanza di un puntatore ad una struttura interna utilizzata dalle librerie, i cui campi non devono
mai essere acceduti direttamente.
174 CAPITOLO 5. FILE E DIRECTORY
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.
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 sufficiente ad allocare i dati) viene resti-
tuito 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 non
è più necessaria utilizzando, per questo l’interfaccia fornisce una apposita funzione, cap_free,
il cui prototipo è:
#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
valore EINVAL.
La funzione permette di liberare la memoria allocata dalle altre funzioni della libreria sia per
un capability state, nel qual caso l’argomento dovrà essere un dato di tipo cap_t, che per una
descrizione testuale dello stesso,111 nel qual caso l’argomento dovrà essere un dato di tipo char
*. Per questo motivo l’argomento obj_d è dichiarato come void * e deve sempre corrispondere
ad un puntatore ottenuto tramite le altre funzioni della libreria, altrimenti la funzione fallirà
con un errore di EINVAL.
Infine si può creare una copia di un capability state ottenuto in precedenza tramite la funzione
cap_dup, il cui prototipo è:
#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.
La funzione crea una copia del capability state posto all’indirizzo cap_p che si è passato
come argomento, restituendo il puntatore alla copia, che conterrà gli stessi valori delle capabi-
lities presenti nell’originale. La memoria necessaria viene allocata automaticamente dalla fun-
zione. Una volta effettuata la copia i due capability state potranno essere modificati in maniera
completamente indipendente.112
Una seconda classe di funzioni di servizio previste dall’interfaccia sono quelle per la gestio-
ne dei dati contenuti all’interno di un capability state; la prima di queste è cap_clear, il cui
prototipo è:
#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
valore EINVAL.
La funzione si limita ad azzerare tutte le capabilities presenti nel capability state all’indirizzo
cap_p passato come argomento, restituendo uno stato vuoto, analogo a quello che si ottiene nella
creazione con cap_init.
111
cioè quanto ottenuto tramite la funzione cap_to_text.
112
alla fine delle operazioni si ricordi però di disallocare anche la copia, oltre all’originale.
5.4. CARATTERISTICHE E FUNZIONALITÀ AVANZATE 175
Valore Significato
CAP_EFFECTIVE Capacità dell’insieme effettivo.
CAP_PERMITTED Capacità dell’insieme permesso.
CAP_INHERITABLE Capacità dell’insieme ereditabile.
Tabella 5.21: Valori possibili per il tipo di dato cap_flag_t che identifica gli insiemi delle capabilities.
La funzione ritorna 0 se i capability state sono identici ed un valore positivo se differiscono, non
sono previsti errori.
La funzione esegue un confronto fra i due capability state passati come argomenti e ritorna in
un valore intero il risultato, questo è nullo se sono identici o positivo se vi sono delle differenze.
Il valore di ritorno della funzione consente inoltre di per ottenere ulteriori informazioni su quali
sono gli insiemi di capabilities che risultano differenti. Per questo si può infatti usare la apposita
macro CAP_DIFFERS:
int CAP_DIFFERS(value, flag)
Controlla lo stato di eventuali differenze delle capabilities nell’insieme flag.
La macro che richiede si passi nell’argomento value il risultato della funzione cap_compare
e in flag l’indicazione (coi valori di tab. 5.21) dell’insieme che si intende controllare; restituirà
un valore diverso da zero se le differenze rilevate da cap_compare sono presenti nell’insieme
indicato.
Per la gestione dei singoli valori delle capabilities presenti in un capability state l’interfaccia
prevede due funzioni specifiche, cap_get_flag e cap_set_flag, che permettono rispettivamente
di leggere o impostare il valore di una capacità all’interno in uno dei tre insiemi già citati; 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
valore EINVAL.
113
si tratta in effetti di un tipo enumerato, come si può verificare dalla sua definizione che si trova in
/usr/include/sys/capability.h.
176 CAPITOLO 5. FILE E DIRECTORY
In entrambe le funzioni l’argomento cap_p indica il puntatore al capability state su cui ope-
rare, mentre l’argomento flag indica su quale dei tre insiemi si intende operare, sempre con i
valori di tab. 5.21.
La capacità che si intende controllare o impostare invece deve essere specificata attraverso una
variabile di tipo cap_value_t, che può prendere come valore uno qualunque di quelli riportati in
tab. 5.20, in questo caso però non è possibile combinare diversi valori in una maschera binaria,
una variabile di tipo cap_value_t può indicare una sola capacità.114
Infine lo stato di una capacità è descritto ad una variabile di tipo cap_flag_value_t, che a
sua volta può assumere soltanto uno115 dei valori di tab. 5.22.
Valore Significato
CAP_CLEAR La capacità non è impostata.
CAP_SET La capacità è impostata.
Tabella 5.22: Valori possibili per il tipo di dato cap_flag_value_t che indica lo stato di una capacità.
La funzione cap_get_flag legge lo stato della capacità indicata dall’argomento cap all’inter-
no dell’insieme indicato dall’argomento flag lo restituisce nella variabile puntata dall’argomento
value_p. Questa deve essere di tipo cap_flag_value_t ed assumerà uno dei valori di tab. 5.22.
La funzione consente pertanto di leggere solo lo stato di una capacità alla volta.
La funzione cap_set_flag può invece impostare in una sola chiamata più capabilities, anche
se solo all’interno dello stesso insieme ed allo stesso valore. Per questo motivo essa prende un
vettore di valori di tipo cap_value_t nell’argomento caps, la cui dimensione viene specificata
dall’argomento ncap. Il tipo di impostazione da eseguire (cancellazione o impostazione) per le
capacità elencate in caps viene indicato dall’argomento value sempre con i valori di tab. 5.22.
Per semplificare la gestione delle capabilities l’interfaccia prevede che sia possibile utilizzare
anche una rappresentazione testuale del contenuto di un capability state e fornisce le opportune
funzioni di gestione;116 la prima di queste, che consente di ottenere la rappresentazione testuale,
è cap_to_text, il cui prototipo è:
#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.
La funzione ritorna l’indirizzo di una stringa contente la descrizione testuale del contenuto del
capability state caps passato come argomento, e, qualora l’argomento 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 pertanto dovrà essere liberata con
cap_free.
La rappresentazione testuale, che viene usata anche di programmi di gestione a riga di co-
mando, prevede che lo stato venga rappresentato con una stringa di testo composta da una serie
di proposizioni separate da spazi, ciascuna delle quali specifica una operazione da eseguire per
creare lo stato finale. Nella rappresentazione si fa sempre conto di partire da uno stato in cui
tutti gli insiemi sono vuoti e si provvede a impostarne i contenuti.
Ciascuna proposizione è nella forma di un elenco di capacità, espresso con i nomi di tab. 5.20
separati da virgole, seguito da un operatore, e dall’indicazione degli insiemi a cui l’operazione
si applica. I nomi delle capacità possono essere scritti sia maiuscoli che minuscoli, viene inoltre
114
in sys/capability.h il tipo cap_value_t è definito come int, ma i valori validi sono soltanto quelli di
tab. 5.20.
115
anche questo è un tipo enumerato.
116
entrambe erano previste dalla bozza dello standard POSIX.1e.
5.4. CARATTERISTICHE E FUNZIONALITÀ AVANZATE 177
riconosciuto il nome speciale all che è equivalente a scrivere la lista completa. Gli insiemi sono
identificati dalle tre lettere iniziali: “p” per il permitted, “i” per l’inheritable ed “e” per l’effective
che devono essere sempre minuscole e se ne può indicare più di uno.
Gli operatori possibili sono solo tre: “+” che aggiunge le capacità elencate agli insiemi indicati,
“-” che le toglie e “=” che le assegna esattamente. I primi due richiedono che sia sempre indicato
sia un elenco di capacità che gli insiemi a cui esse devono applicarsi, e rispettivamente attiveranno
o disattiveranno le capacità elencate nell’insieme o negli insiemi specificati, ignorando tutto il
resto. I due operatori possono anche essere combinati nella stessa proposizione, per aggiungere
e togliere le capacità dell’elenco da insiemi diversi.
L’assegnazione si applica invece su tutti gli insiemi allo stesso tempo, pertanto l’uso di “=”
è equivalente alla cancellazione preventiva di tutte le capacità ed alla impostazione di quelle
elencate negli insiemi specificati, questo significa che in genere lo si usa una sola volta all’inizio
della stringa. In tal caso l’elenco delle capacità può non essere indicato e viene assunto che si
stia facendo riferimento a tutte quante senza doverlo scrivere esplicitamente.
Come esempi avremo allora che un processo non privilegiato di un utente, che non ha nessuna
capacità attiva, avrà una rappresentazione nella forma “=” che corrisponde al fatto che nessuna
capacità viene assegnata a nessun insieme (vale la cancellazione preventiva), mentre un processo
con privilegi di amministratore avrà una rappresentazione nella forma “=ep” in cui tutte le
capacità vengono assegnate agli insiemi permitted ed effective (e l’inheritable è ignorato in quanto
per le regole viste a pag. 5.4.4 le capacità verranno comunque attivate attraverso una exec).
Infine, come esempio meno banale dei precedenti, otterremo per init una rappresentazione nella
forma “=ep cap_setpcap-e” dato che come accennato tradizionalmente CAP_SETPCAP è sempre
stata rimossa da detto processo.
Viceversa per passare ottenere un capability state dalla sua rappresentazione testuale si può
usare cap_from_text, il cui prototipo è:
#include <sys/capability.h>
cap_t cap_from_text(const char *string)
Crea un capability state dalla sua rappresentazione testuale.
La funzione ritorna un puntatore valido in caso di successo e NULL in caso di errore, nel qual caso
errno può assumere i valori EINVAL o ENOMEM.
#include <sys/capability.h>
char * cap_to_name(cap_value_t cap)
int cap_from_name(const char *name, cap_value_t *cap_p)
Convertono le capabilities dalle costanti alla rappresentazione testuale e viceversa.
La funzione cap_to_name ritorna un valore diverso da NULL in caso di successo e NULL in caso di
errore, mentre cap_to_name ritorna rispettivamente 0 e −1; per entrambe in caso di errore errno
può assumere i valori EINVAL o ENOMEM.
La prima funzione restituisce la stringa (allocata automaticamente e che dovrà essere liberata
con cap_free) che corrisponde al valore della capacità cap, mentre la seconda restituisce nella
variabile puntata da cap_p il valore della capacità rappresentata dalla stringa name.
Fin quei abbiamo trattato solo le funzioni di servizio relative alla manipolazione dei capability
state come strutture di dati; l’interfaccia di gestione prevede però anche le funzioni per trattare
178 CAPITOLO 5. FILE E DIRECTORY
le capabilities presenti nei processi. La prima di queste funzioni è cap_get_proc che consente la
lettura delle capabilities del processo corrente, il suo prototipo è:
#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.
La funzione legge il valore delle capabilities associate al processo da cui viene invocata,
restituendo il risultato tramite il puntatore ad un capability state contenente tutti i dati che
provvede ad allocare autonomamente e che di nuovo occorrerà liberare con cap_free quando
non sarà più utilizzato.
Se invece si vogliono leggere le capabilities di un processo specifico occorre usare la funzione
capgetp, il cui prototipo117 è:
#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
i valori EINVAL, EPERM o ENOMEM.
La funzione legge il valore delle capabilities del processo indicato con l’argomento pid, e
restituisce il risultato nel capability state posto all’indirizzo indicato con l’argomento cap_d; a
differenza della precedente in questo caso il capability state deve essere stato creato in precedenza.
Qualora il processo indicato non esista si avrà un errore di ESRCH. Gli stessi valori possono
essere letti direttamente nel filesystem proc, nei file /proc/<pid>/status; ad esempio per init
si otterrà qualcosa del tipo:
...
CapInh: 0000000000000000
CapPrm: 00000000fffffeff
CapEff: 00000000fffffeff
...
Infine per impostare le capabilities del processo corrente (non esiste una funzione che per-
metta di cambiare le capabilities di un altro processo) si deve usare la funzione cap_set_proc,
il cui prototipo è:
#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
i valori EINVAL, EPERM o ENOMEM.
La funzione modifica le capabilities del processo corrente secondo quanto specificato con
l’argomento cap_p, posto che questo sia possibile nei termini spiegati in precedenza (non sarà
ad esempio possibile impostare capacità non presenti nell’insieme di quelle permesse). In caso di
successo i nuovi valori saranno effettivi al ritorno della funzione, in caso di fallimento invece lo
stato delle capacità resterà invariato. Si tenga presente che tutte le capacità specificate tramite
cap_p devono essere permesse; se anche una sola non lo è la funzione fallirà, e per quanto
117
su alcune pagine di manuale la funzione è descritta con un prototipo sbagliato, che prevede un valore di
ritorno di tipo cap_t, ma il valore di ritorno è intero, come si può verificare anche dalla dichiarazione della stessa
in sys/capability.h.
5.4. CARATTERISTICHE E FUNZIONALITÀ AVANZATE 179
appena detto, lo stato delle capabilities non verrà modificato (neanche per le parti eventualmente
permesse).
Come esempio di utilizzo di queste funzioni nei sorgenti allegati alla guida si è distribuito
il programma getcap.c, che consente di leggere le capabilities del processo corrente118 o tra-
mite l’opzione -p, quelle di un processo qualunque il cui pid viene passato come parametro
dell’opzione.
1 if (! pid ) {
2 capab = cap_get_proc ();
3 if ( capab == NULL ) {
4 perror ( " cannot get current process capabilities " );
5 return 1;
6 }
7 } else {
8 capab = cap_init ();
9 res = capgetp ( pid , capab );
10 if ( res ) {
11 perror ( " cannot get process capabilities " );
12 return 1;
13 }
14 }
15
16 string = cap_to_text ( capab , NULL );
17 printf ( " Capability : % s \ n " , string );
18
19 cap_free ( capab );
20 cap_free ( string );
21 return 0;
La sezione principale del programma è riportata in fig. 5.16, e si basa su una condizione sulla
variabile pid che se si è usato l’opzione -p è impostata (nella sezione di gestione delle opzioni,
che si è tralasciata) al valore del pid del processo di cui si vuole leggere le capabilities e nulla
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)
si legge il valore delle capacità del processo indicato.
Il passo successivo è utilizzare (16) cap_to_text per tradurre in una stringa lo stato, e
poi (17) stamparlo; infine (19-20) si libera la memoria allocata dalle precedenti funzioni con
cap_free per poi ritornare dal ciclo principale della funzione.
118
vale a dire di sé stesso, quando lo si lancia, il che può sembrare inutile, ma serve a mostrarci quali sono le
capabilities standard che ottiene un processo lanciato dalla riga di comando.
119
entrambe sono contenute in due campi (rispettivamente pwd e root) di fs_struct; vedi fig. 3.2.
180 CAPITOLO 5. FILE E DIRECTORY
di directory rispetto alla quale vengono risolti i pathname assoluti.120 Il fatto che questo valore
sia specificato per ogni processo apre allora la possibilità di modificare le modalità di 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 filesystem
usata dal kernel, e dato che il suo valore viene ereditato dal padre da ogni processo figlio, in
generale i processi risolvono i pathname assoluti a partire sempre dalla stessa directory, che
corrisponde alla radice del sistema.
In certe situazioni però è utile poter impedire che un processo possa accedere a tutto il
filesystem; per far questo si può cambiare la sua directory radice con la funzione chroot, il cui
prototipo è:
#include <unistd.h>
int chroot(const char *path)
Cambia la directory radice del processo a quella specificata da path.
La funzione restituisce zero in caso di successo e -1 per un errore, in caso di errore errno può
assumere i valori:
EPERM l’user-ID effettivo del processo non è zero.
ed inoltre EFAULT, ENAMETOOLONG, ENOENT, ENOMEM, ENOTDIR, EACCES, ELOOP; EROFS e EIO.
in questo modo la directory radice del processo diventerà path (che ovviamente deve esistere)
ed ogni pathname assoluto usato dalle funzioni chiamate nel processo sarà risolto a partire da
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 file al di fuori della sezione
di albero in cui è stato imprigionato.
Solo un processo con i privilegi di amministratore può usare questa funzione, e la nuova radice,
per quanto detto in sez. 3.2.2, sarà ereditata da tutti i suoi processi figli. Si tenga presente però
che la funzione non cambia la directory di lavoro, che potrebbe restare fuori dalla chroot jail.
Questo è il motivo per cui la funzione è efficace solo se dopo averla eseguita si cedono i
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 filesystem usando pathname
relativi, i quali, partendo dalla directory di lavoro che è fuori della chroot jail, potranno (con
l’uso di “..”) risalire fino alla radice effettiva del filesystem.
Ma se ad un processo restano i privilegi di amministratore esso potrà comunque portare la
sua directory di lavoro fuori dalla chroot jail in cui si trova. Basta infatti creare una nuova chroot
jail con l’uso di chroot su una qualunque directory contenuta nell’attuale directory di lavoro.
Per questo motivo l’uso 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
si vuole che il server veda solo i file che deve trasferire, per cui in genere si esegue una chroot
sulla directory che contiene i file. Si tenga presente però che in questo caso occorrerà replicare
all’interno della chroot jail tutti i file (in genere programmi e librerie) di cui il server potrebbe
avere bisogno.
120
cioè quando un processo chiede la risoluzione di un pathname, il kernel usa sempre questa directory come
punto di partenza.
Capitolo 6
Esamineremo in questo capitolo la prima delle due interfacce di programmazione per i file, quella
dei file descriptor, nativa di Unix. Questa è l’interfaccia di basso livello provvista direttamente
dalle system call, che non prevede funzionalità evolute come la bufferizzazione o funzioni di
lettura o scrittura formattata, e sulla quale è costruita anche l’interfaccia definita dallo standard
ANSI C che affronteremo al cap. 7.
181
182 CAPITOLO 6. I FILE: L’INTERFACCIA STANDARD UNIX
La file table è una tabella che contiene una voce per ciascun file che è stato aperto nel sistema.
In Linux è costituita da strutture di tipo file; in ciascuna di esse sono tenute varie informazioni
relative al file, fra cui:
In fig. 6.1 si è riportato uno schema in cui è illustrata questa architettura, ed in cui si sono
evidenziate le interrelazioni fra le varie strutture di dati sulla quale essa è basata. Ritorneremo
su questo schema più volte, dato che esso è fondamentale per capire i dettagli del funzionamento
dell’interfaccia dei file descriptor.
Figura 6.1: Schema della architettura dell’accesso ai file attraverso l’interfaccia dei file descriptor.
terminale per l’uscita. Lo standard POSIX.1 provvede, al posto dei valori numerici, tre costanti
simboliche, definite in tab. 6.1.
Costante Significato
STDIN_FILENO file descriptor dello standard input
STDOUT_FILENO file descriptor dello standard output
STDERR_FILENO file descriptor dello standard error
Tabella 6.1: Costanti definite in unistd.h per i file standard aperti alla creazione di ogni processo.
#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 file indicato da pathname nella modalità indicata da flags, e, nel caso il file sia
creato, con gli eventuali permessi specificati da mode.
La funzione ritorna il file descriptor in caso di successo e −1 in caso di errore. In questo caso la
variabile errno assumerà uno dei valori:
EEXIST pathname esiste e si è specificato O_CREAT e O_EXCL.
EISDIR pathname indica una directory e si è tentato l’accesso in scrittura.
ENOTDIR si è specificato O_DIRECTORY e pathname non è una directory.
ENXIO si sono impostati O_NOBLOCK o O_WRONLY ed il file è una fifo che non viene letta da
nessun processo o pathname è un file di dispositivo ma il dispositivo è assente.
ENODEV pathname si riferisce a un file di dispositivo che non esiste.
ETXTBSY si è cercato di accedere in scrittura all’immagine di un programma in esecuzione.
ELOOP si sono incontrati troppi link simbolici nel risolvere il pathname o si è indicato
O_NOFOLLOW e pathname è un link simbolico.
ed inoltre EACCES, ENAMETOOLONG, ENOENT, EROFS, EFAULT, ENOSPC, ENOMEM, EMFILE e ENFILE.
184 CAPITOLO 6. I FILE: L’INTERFACCIA STANDARD UNIX
La funzione apre il file usando il primo file descriptor libero, e crea l’opportuna voce, cioè la
struttura file, nella file table del processo. Viene sempre restituito come valore di ritorno il file
descriptor con il valore più basso disponibile.
Flag Descrizione
O_RDONLY Apre il file in sola lettura, le glibc definiscono anche O_READ come sinonimo.
O_WRONLY Apre il file in sola scrittura, le glibc definiscono anche O_WRITE come sinonimo.
O_RDWR Apre il file sia in lettura che in scrittura.
O_CREAT Se il file non esiste verrà creato, con le regole di titolarità del file viste in sez. 5.3.4. Con
questa opzione l’argomento mode deve essere specificato.
O_EXCL Usato in congiunzione con O_CREAT fa sı̀ che la precedente esistenza del file diventi un errore2
che fa fallire open con EEXIST.
O_NONBLOCK Apre il file in modalità non bloccante, e comporta che open ritorni immediatamente anche
quando dovrebbe bloccarsi (l’opzione ha senso solo per le fifo, vedi sez. 11.1.4).
O_NOCTTY Se pathname si riferisce ad un dispositivo di terminale, questo non diventerà il terminale di
controllo, anche se il processo non ne ha ancora uno (si veda sez. 10.1.3).
O_SHLOCK Apre il file con uno shared lock (vedi sez. 12.1). Specifica di BSD, assente in Linux.
O_EXLOCK Apre il file con un lock esclusivo (vedi sez. 12.1). Specifica di BSD, assente in Linux.
O_TRUNC Se usato su un file di dati aperto in scrittura, ne tronca la lunghezza a zero; con un terminale
o una fifo viene ignorato, negli altri casi il comportamento non è specificato.
O_NOFOLLOW Se pathname è un link simbolico la chiamata fallisce. Questa è un’estensione BSD aggiunta
in Linux dal kernel 2.1.126. Nelle versioni precedenti i link simbolici sono sempre seguiti, e
questa opzione è ignorata.
O_DIRECTORY Se pathname non è una directory la chiamata fallisce. Questo flag è specifico di Linux ed è
stato introdotto con il kernel 2.1.126 per evitare dei DoS 3 quando opendir viene chiamata
su una fifo o su un dispositivo associato ad una unità a nastri, non deve dispositivo a nastri;
non deve essere utilizzato al di fuori dell’implementazione di opendir.
O_LARGEFILE Nel caso di sistemi a 32 bit che supportano file di grandi dimensioni consente di aprire file
le cui dimensioni non possono essere rappresentate da numeri a 31 bit.
O_APPEND Il file viene aperto in append mode. Prima di ciascuna scrittura la posizione corrente viene
sempre impostata alla fine del file. Con NFS si può avere una corruzione del file se più di un
processo scrive allo stesso tempo.4
O_NONBLOCK Il file viene aperto in modalità non bloccante per le operazioni di I/O (che tratteremo in
sez. 12.2.1): questo significa 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
per le fifo e per alcuni file di dispositivo.
O_NDELAY In Linux5 è sinonimo di O_NONBLOCK.
O_ASYNC Apre il file per l’I/O in modalità asincrona (vedi sez. 12.3.3). Quando è impostato viene
generato il segnale SIGIO tutte le volte che sono disponibili dati in input sul file.
O_SYNC Apre il file per l’input/output sincrono: ogni write bloccherà fino al completamento della
scrittura di tutti i dati sull’hardware sottostante.
O_FSYNC Sinonimo di O_SYNC, usato da BSD.
O_DSYNC Variante di I/O sincrono definita da POSIX; presente dal kernel 2.1.130 come sinonimo di
O_SYNC.
O_RSYNC Variante analoga alla precedente, trattata allo stesso modo.
O_NOATIME Blocca l’aggiornamento dei tempi di accesso dei file (vedi sez. 5.2.4). Per molti filesystem
questa funzionalità non è disponibile per il singolo file ma come opzione generale da specificare
in fase di montaggio.
O_DIRECT Esegue l’I/O direttamente dai buffer 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 buffer in user space siano allineati alle dimensioni dei blocchi del filesystem; per il kernel
2.6 basta che siano allineati a multipli di 512 byte.
O_CLOEXEC Attiva la modalità di close-on-exec (vedi sez. 6.3.1 e 6.3.6).7
Tabella 6.2: Valori e significato dei vari bit del file status flag.
2
la pagina di manuale di open segnala che questa opzione è difettosa su NFS, e che i programmi che la usano
per stabilire un file di lock possono incorrere in una race condition. Si consiglia come alternativa di usare un file
con un nome univoco e la funzione link per verificarne l’esistenza (vedi sez. 11.3.2).
6.2. LE FUNZIONI BASE 185
Questa caratteristica permette di prevedere qual è il valore del file descriptor che si otterrà al
ritorno di open, e viene talvolta usata da alcune applicazioni per sostituire i file corrispondenti
ai file standard visti in sez. 6.1.2: se ad esempio si chiude lo standard input e si apre subito dopo
un nuovo file questo diventerà il nuovo standard input (avrà cioè il file descriptor 0).
Il nuovo file descriptor non è condiviso con nessun altro processo (torneremo sulla condivisione
dei file, in genere accessibile dopo una fork, in sez. 6.3.1) ed è impostato per restare aperto
attraverso una exec (come accennato in sez. 3.2.5); l’offset è impostato all’inizio del file.
L’argomento mode indica i permessi con cui il file viene creato; i valori possibili sono gli stessi
già visti in sez. 5.3.1 e possono essere specificati come OR binario delle costanti descritte in
tab. 5.7. Questi permessi sono filtrati dal valore di umask (vedi sez. 5.3.3) per il processo.
La funzione prevede diverse opzioni, che vengono specificate usando vari bit dell’argomento
flags. Alcuni di questi bit vanno anche a costituire il flag di stato del file (o file status flag),
che è mantenuto nel campo f_flags della struttura file (al solito si veda lo schema di fig. 6.1).
Essi sono divisi in tre categorie principali:
• i bit delle modalità di accesso: specificano con quale modalità si accederà al file: i valori
possibili sono lettura, scrittura o lettura/scrittura. Uno di questi bit deve essere sempre
specificato quando si apre un file. Vengono impostati alla chiamata da open, e possono
essere riletti con fcntl (fanno parte del file status flag), ma non possono essere modificati.
• i bit delle modalità di apertura: permettono di specificare alcune delle caratteristiche
del comportamento di open quando viene eseguita. Hanno effetto solo al momento della
chiamata della funzione e non sono memorizzati né possono essere riletti.
• i bit delle modalità di operazione: permettono di specificare alcune caratteristiche del
comportamento delle future operazioni sul file (come read o write). Anch’essi fan parte
del file status flag. Il loro valore è impostato alla chiamata di open, ma possono essere
riletti e modificati (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
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) dell’argomento
flags da passare alla open. I due flag O_NOFOLLOW e O_DIRECTORY sono estensioni specifiche di
Linux, e deve essere definita la macro _GNU_SOURCE per poterli usare.
Nelle prime versioni di Unix i valori di flag specificabili per open erano solo quelli relativi
alle modalità di accesso del file. Per questo motivo per creare un nuovo file c’era una system call
apposita, creat, il cui prototipo è:
#include <fcntl.h>
int creat(const char *pathname, mode_t mode)
Crea un nuovo file vuoto, con i permessi specificati da mode. È del tutto equivalente a
open(filedes, O_CREAT|O_WRONLY|O_TRUNC, mode).
adesso questa funzione resta solo per compatibilità con i vecchi programmi.
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 all’attacco.
4
il problema è che NFS non supporta la scrittura in append, ed il kernel deve simularla, ma questo comporta
la possibilità di una race condition, vedi sez. 6.3.2.
5
l’opzione origina da SVr4, dove però causava il ritorno da una read con un valore nullo e non con un errore,
questo introduce un’ambiguità, dato che come vedremo in sez. 6.2.4 il ritorno di zero da parte di read ha il
significato di una end-of-file.
6
l’opzione è stata introdotta dalla SGI in IRIX, e serve sostanzialmente a permettere ad alcuni programmi (in
genere database) la gestione diretta della bufferizzazione dell’I/O in quanto essi sono in grado di ottimizzarla al
meglio per le loro prestazioni; l’opzione è presente anche in FreeBSD, senza limiti di allineamento dei buffer. In
Linux è stata introdotta con il kernel 2.4.10, le versioni precedenti la ignorano.
7
introdotto con il kernel 2.6.23, per evitare una race condition che si può verificare con i thread, fra l’apertura
del file e l’impostazione della suddetta modalità con fcntl.
186 CAPITOLO 6. I FILE: L’INTERFACCIA STANDARD UNIX
La funzione ritorna 0 in caso di successo e −1 in caso di errore, con errno che assume i valori:
EBADF fd non è un descrittore valido.
EINTR la funzione è stata interrotta da un segnale.
ed inoltre EIO.
La chiusura di un file rilascia ogni blocco (il file locking è trattato in sez. 12.1) che il processo
poteva avere acquisito su di esso; se fd è l’ultimo riferimento (di eventuali copie) ad un file
aperto, tutte le risorse nella file table vengono rilasciate. Infine se il file descriptor era l’ultimo
riferimento ad un file su disco quest’ultimo viene cancellato.
Si ricordi che quando un processo termina anche tutti i suoi file descriptor vengono chiusi,
molti programmi sfruttano questa caratteristica e non usano esplicitamente close. In genere
comunque chiudere un file senza controllarne lo stato di uscita è errore; infatti molti filesystem
implementano la tecnica del write-behind, per cui una write può avere successo anche se i dati
non sono stati scritti, un eventuale errore di I/O allora può sfuggire, ma verrà riportato alla
chiusura del file: per questo motivo non effettuare il controllo può portare ad una perdita di dati
inavvertita.8
In ogni caso una close andata a buon fine non garantisce che i dati siano stati effettivamente
scritti su disco, perché il kernel può decidere di ottimizzare l’accesso a disco ritardandone la
scrittura. L’uso della funzione sync (vedi sez. 6.3.3) effettua esplicitamente il flush dei dati,
ma anche in questo caso resta l’incertezza dovuta al comportamento dell’hardware (che a sua
volta può introdurre ottimizzazioni dell’accesso al disco che ritardano la scrittura dei dati, da
cui l’abitudine di ripetere tre volte il comando prima di eseguire lo shutdown).
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:
ESPIPE fd è una pipe, un socket o una fifo.
EINVAL whence non è un valore valido.
EOVERFLOW offset non può essere rappresentato nel tipo off_t.
ed inoltre EBADF.
8
in Linux questo comportamento è stato osservato con NFS e le quote su disco.
6.2. LE FUNZIONI BASE 187
SEEK_SET si fa riferimento all’inizio del file: il valore (sempre positivo) di offset indica
direttamente la nuova posizione corrente.
SEEK_CUR si fa riferimento alla posizione corrente del file: ad essa viene sommato offset (che
può essere negativo e positivo) per ottenere la nuova posizione corrente.
SEEK_END si fa riferimento alla fine del file: alle dimensioni del file viene sommato offset (che
può essere negativo e positivo) per ottenere la nuova posizione corrente.
Si tenga presente che la chiamata a lseek non causa nessun accesso al file, si limita a
modificare la posizione corrente (cioè il valore f_pos in file, vedi fig. 6.1). Dato che la funzione
ritorna la nuova posizione, usando il valore zero per offset si può riottenere la posizione corrente
nel file chiamando la funzione con lseek(fd, 0, SEEK_CUR).
Si tenga presente inoltre che usare SEEK_END non assicura affatto che la successiva scrittura
avvenga alla fine del file, infatti se questo è stato aperto anche da un altro processo che vi
ha scritto, la fine del file può essersi spostata, ma noi scriveremo alla posizione impostata in
precedenza (questa è una potenziale sorgente di race condition, vedi sez. 6.3.2).
Non tutti i file supportano la capacità di eseguire una lseek, in questo caso la funzione
ritorna l’errore 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 file di terminale.10
Lo standard POSIX però non specifica niente in proposito. Inoltre alcuni file speciali, ad esempio
/dev/null, non causano un errore ma restituiscono un valore indefinito.
Infine si tenga presente che, come accennato in sez. 5.2.3, con lseek è possibile impostare
una posizione anche oltre la corrente fine del file; ed in tal caso alla successiva scrittura il file sarà
esteso a partire da detta posizione. In questo caso si ha quella che viene chiamata la creazione
di un buco nel file, accade cioè che nonostante la dimensione del file sia cresciuta in seguito alla
scrittura effettuata, lo spazio vuoto fra la precedente fine del file ed la nuova parte scritta dopo lo
spostamento, non corrisponda ad una allocazione effettiva di spazio su disco, che sarebbe inutile
dato che quella zona è effettivamente vuota.
Questa è una delle caratteristiche spcifiche della gestione dei file di un sistema unix-like, ed
in questo caso si ha appunto quello che in gergo si chiama un hole nel file e si dice che il file
in questione è uno sparse file. In sostanza, se si ricorda la struttura di un filesystem illustrata
in fig. 4.3, quello che accade è che nell’inode del file viene segnata l’allocazione di un blocco di
dati a partire dalla nuova posizione, ma non viene allocato nulla per le posizioni intermedie; in
caso di lettura sequenziale del contenuto del file il kernel si accorgerà della presenza del buco, e
restituirà degli zeri come contenuto di quella parte del file.
Questa funzionalità comporta una delle caratteristiche della gestione dei file su Unix che
spesso genera più confusione in chi non la conosce, per cui sommando le dimensioni dei file si
può ottenere, se si hanno molti sparse file, un totale anche maggiore della capacità del proprio
disco e comunque maggiore della dimensione che riporta un comando come du, che calcola lo
spazio disco occupato in base al numero dei blocchi effettivamente allocati per il file.
Questo avviene proprio perché in un sistema unix-like la dimensione di un file è una carat-
teristica del tutto indipendente dalla quantità di spazio disco effettivamente allocato, e viene
registrata sull’inode come le altre proprietà del file. La dimensione viene aggiornata automa-
ticamente quando si estende un file scrivendoci, e viene riportata dal campo st_size di una
struttura stat quando si effettua chiamata ad una delle funzioni *stat viste in sez. 5.2.1.
9
per compatibilità con alcune vecchie notazioni questi valori possono essere rimpiazzati rispettivamente con 0,
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.
188 CAPITOLO 6. I FILE: L’INTERFACCIA STANDARD UNIX
#include <unistd.h>
ssize_t read(int fd, void * buf, size_t count)
Cerca di leggere count byte dal file fd al buffer 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:
EINTR la funzione è stata interrotta da un segnale prima di aver potuto leggere qualsiasi dato.
EAGAIN la funzione non aveva nessun dato da restituire e si era aperto il file in modalità
O_NONBLOCK.
ed inoltre EBADF, EIO, EISDIR, EBADF, EINVAL e EFAULT ed eventuali altri errori dipendenti dalla
natura dell’oggetto connesso a fd.
La funzione tenta di leggere count byte a partire dalla posizione corrente nel file. Dopo
la lettura la posizione sul file è spostata automaticamente in avanti del numero di byte letti.
Se count è zero la funzione restituisce zero senza nessun altro risultato. Si deve sempre tener
presente che non è detto che la funzione read restituisca sempre il numero di byte richiesto, ci
sono infatti varie ragioni per cui la funzione può restituire un numero di byte inferiore; questo è
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 file ne
contenga. In questo caso il file viene letto fino alla sua fine, e la funzione ritorna regolarmente il
numero di byte letti effettivamente. Raggiunta la fine del file, alla ripetizione di un’operazione di
lettura, otterremmo il ritorno immediato di read con uno zero. La condizione di raggiungimento
della fine del file non è un errore, e viene segnalata appunto da un valore di ritorno di read
nullo. Ripetere ulteriormente la lettura non avrebbe nessun effetto se non quello di continuare a
ricevere zero come valore di ritorno.
Con i file regolari questa è l’unica situazione in cui si può avere un numero di byte letti
inferiore a quello richiesto, ma questo non è vero quando si legge da un terminale, da una fifo o
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. 12.2.1) e ritorna solo quando ne arrivano;
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
in sez. 16.3.1), o per la lettura da certi file di dispositivo, come le unità a nastro, che restituiscono
sempre i dati ad un singolo blocco alla volta, o come le linee seriali, che restituiscono solo i dati
ricevuti fino al momento della lettura.
Infine anche le due condizioni segnalate dagli errori EINTR ed EAGAIN non sono propriamente
degli errori. La prima si verifica quando la read è bloccata in attesa di dati in ingresso e viene
interrotta da un segnale; in tal caso l’azione da intraprendere è quella di rieseguire la funzione.
Torneremo in dettaglio sull’argomento in sez. 9.3.1. La seconda si verifica quando il file è aperto in
modalità non bloccante (vedi sez. 12.2.1) e non ci sono dati in ingresso: la funzione allora ritorna
6.2. LE FUNZIONI BASE 189
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 fin dagli albori di Unix, ma
nella seconda versione delle Single Unix Specification 12 (quello che viene chiamato normalmente
Unix98, vedi sez. 1.2.6) è stata introdotta la definizione di un’altra funzione di lettura, pread,
il cui prototipo è:
#include <unistd.h>
ssize_t pread(int fd, void * buf, size_t count, off_t offset)
Cerca di leggere count byte dal file fd, a partire dalla posizione offset, nel buffer 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.
La funzione prende esattamente gli stessi argomenti di read con lo stesso significato, a cui si
aggiunge l’argomento offset che indica una posizione sul file. Identico è il comportamento ed
il valore di ritorno. La funzione serve quando si vogliono leggere dati dal file senza modificare la
posizione corrente.
L’uso di pread è equivalente all’esecuzione di una read seguita da una lseek che riporti al
valore precedente la posizione corrente sul file, ma permette di eseguire l’operazione atomica-
mente. Questo può essere importante quando la posizione sul file viene condivisa da processi
diversi (vedi sez. 6.3.1). Il valore di offset fa sempre riferimento all’inizio del file.
La funzione pread è disponibile anche in Linux, però diventa accessibile solo attivando il
supporto delle estensioni previste dalle Single Unix Specification con la definizione della macro:
#define _XOPEN_SOURCE 500
e si ricordi di definire questa macro prima dell’inclusione del file di dichiarazioni unistd.h.
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:
EINVAL fd è connesso ad un oggetto che non consente la scrittura.
EFBIG si è cercato di scrivere oltre la dimensione massima consentita dal filesystem o il limite
per le dimensioni dei file del processo o su una posizione oltre il massimo consentito.
EPIPE fd è connesso ad una pipe il cui altro capo è chiuso in lettura; in questo caso viene
anche generato il segnale SIGPIPE, se questo viene gestito (o bloccato o ignorato) la
funzione ritorna questo errore.
EINTR si è stati interrotti da un segnale prima di aver potuto scrivere qualsiasi dato.
EAGAIN ci si sarebbe bloccati, ma il file era aperto in modalità O_NONBLOCK.
ed inoltre EBADF, EIO, EISDIR, EBADF, ENOSPC, EINVAL e EFAULT ed eventuali altri errori dipendenti
dalla natura dell’oggetto connesso a fd.
Come nel caso di read la funzione tenta di scrivere count byte a partire dalla posizione
corrente nel file e sposta automaticamente la posizione in avanti del numero di byte scritti. Se
11
in BSD si usa per questo errore la costante EWOULDBLOCK, in Linux, con le glibc, questa è sinonima di EAGAIN.
12
questa funzione, e l’analoga pwrite sono state aggiunte nel kernel 2.1.60, il supporto nelle glibc, compresa
l’emulazione per i vecchi kernel che non hanno la system call, è stato aggiunto con la versione 2.1, in versioni
precedenti sia del kernel che delle librerie la funzione non è disponibile.
190 CAPITOLO 6. I FILE: L’INTERFACCIA STANDARD UNIX
il file è aperto in modalità O_APPEND i dati vengono sempre scritti alla fine del file. Lo standard
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
i filesystem supportino questa capacità.
Se count è zero la funzione restituisce zero senza fare nient’altro. Per i file ordinari il numero
di byte scritti è sempre uguale a quello indicato da count, a meno di un errore. Negli altri casi
si ha lo stesso comportamento di read.
Anche per write lo standard Unix98 definisce un’analoga pwrite per scrivere alla posizione
indicata senza modificare la posizione corrente nel file, il suo prototipo è:
#include <unistd.h>
ssize_t pwrite(int fd, void * buf, size_t count, off_t offset)
Cerca di scrivere sul file fd, a partire dalla posizione offset, count byte dal buffer 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.
• ciascun processo può scrivere indipendentemente; dopo ciascuna write la posizione cor-
rente sarà cambiata solo nel processo. Se la scrittura eccede la dimensione corrente del file
questo verrà esteso automaticamente con l’aggiornamento del campo i_size nell’inode.
• se un file è in modalità O_APPEND tutte le volte che viene effettuata una scrittura la posizione
corrente viene prima impostata alla dimensione corrente del file letta dall’inode. Dopo la
scrittura il file viene automaticamente esteso.
• l’effetto di lseek è solo quello di cambiare il campo f_pos nella struttura file della file
table, non c’è nessuna operazione sul file su disco. Quando la si usa per porsi alla fine del
file la posizione viene impostata leggendo la dimensione corrente dall’inode.
6.3. CARATTERISTICHE AVANZATE 191
Figura 6.2: Schema dell’accesso allo stesso file da parte di due processi diversi
Il secondo caso è quello in cui due file descriptor di due processi diversi puntino alla stessa
voce nella file table; questo è ad esempio il caso dei file aperti che vengono ereditati dal processo
figlio all’esecuzione di una fork (si ricordi quanto detto in sez. 3.2.2). La situazione è illustrata
in fig. 6.3; dato che il processo figlio riceve una copia dello spazio di indirizzi del padre, riceverà
anche una copia di file_struct e relativa tabella dei file aperti.
In questo modo padre e figlio avranno gli stessi file descriptor che faranno riferimento alla
stessa voce nella file table, condividendo cosı̀ la posizione corrente sul file. Questo ha le con-
seguenze descritte a suo tempo in sez. 3.2.2: in caso di scrittura contemporanea la posizione
corrente nel file varierà per entrambi i processi (in quanto verrà modificato f_pos che è lo stesso
per entrambi).
Si noti inoltre che anche i flag di stato del file (quelli impostati dall’argomento flag di open)
essendo tenuti nella voce della file table 13 , vengono in questo caso condivisi. Ai file però sono
associati anche altri flag, dei quali l’unico usato al momento è FD_CLOEXEC, detti file descriptor
flags. Questi ultimi sono tenuti invece in file_struct, e perciò sono specifici di ciascun processo
e non vengono modificati dalle azioni degli altri anche in caso di condivisione della stessa voce
della file table.
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ò
fornisce in alcuni casi la possibilità di eseguire alcune operazioni di scrittura in maniera coor-
dinata anche senza utilizzare meccanismi di sincronizzazione più complessi (come il file locking,
che esamineremo in sez. 12.1).
Un caso tipico di necessità di accesso condiviso in scrittura è quello in cui vari processi devono
scrivere alla fine di un file (ad esempio un file di log). Come accennato in sez. 6.2.3 impostare la
posizione alla fine del file e poi scrivere può condurre ad una race condition: infatti può succedere
che un secondo processo scriva alla fine del file fra la lseek e la write; in questo caso, come
abbiamo appena visto, il file sarà esteso, ma il nostro primo processo avrà ancora la posizione
corrente impostata con la lseek che non corrisponde più alla fine del file, e la successiva write
sovrascriverà i dati del secondo processo.
Il problema è che usare due system call in successione non è un’operazione atomica; il pro-
blema è stato risolto introducendo la modalità O_APPEND. In questo caso infatti, come abbiamo
descritto in precedenza, è il kernel che aggiorna automaticamente la posizione alla fine del file
prima di effettuare la scrittura, e poi estende il file. Tutto questo avviene all’interno di una
singola system call (la write) che non essendo interrompibile da un altro processo costituisce
un’operazione atomica.
Un altro caso tipico in cui è necessaria l’atomicità è quello in cui si vuole creare un file di
lock , bloccandosi se il file esiste. In questo caso la sequenza logica porterebbe a verificare prima
l’esistenza del file con una stat per poi crearlo con una creat; di nuovo avremmo la possibilità
di una race condition da parte di un altro processo che crea lo stesso file fra il controllo e la
creazione.
Per questo motivo sono stati introdotti per open i due flag O_CREAT e O_EXCL. In questo
modo l’operazione di controllo dell’esistenza del file (con relativa uscita dalla funzione con un
errore) e creazione in caso di assenza, diventa atomica essendo svolta tutta all’interno di una
6.3. CARATTERISTICHE AVANZATE 193
singola system call (per i dettagli sull’uso di questa caratteristica si veda sez. 11.3.2).
#include <unistd.h>
int sync(void)
Sincronizza il buffer della cache dei file col disco.
La funzione ritorna sempre zero.
i vari standard prevedono che la funzione si limiti a far partire le operazioni, ritornando imme-
diatamente; 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 fissi: il valore tradizionale, usato da BSD, per l’update dei dati è ogni 30 secondi, ma in
Linux il valore utilizzato è di 5 secondi; con le nuove versioni15 poi, è il kernel che si occupa
direttamente di tutto quanto attraverso il demone interno bdflush, il cui comportamento può
essere controllato attraverso il file /proc/sys/vm/bdflush (per il significato dei valori si può
leggere la documentazione allegata al kernel in Documentation/sysctl/vm.txt).
Quando si vogliono scaricare soltanto i dati di un file (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 meta-dati del file fd
int fdatasync(int fd)
Sincronizza i dati del file 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 file speciale che non supporta la sincronizzazione.
ed inoltre EBADF, EROFS e EIO.
Entrambe le funzioni forzano la sincronizzazione col disco di tutti i dati del file specificato,
ed attendono fino alla conclusione delle operazioni; fsync forza anche la sincronizzazione dei
meta-dati del file (che riguardano sia le modifiche alle tabelle di allocazione dei settori, che gli
altri dati contenuti nell’inode che si leggono con fstat, come i tempi del file).
Si tenga presente che questo non comporta la sincronizzazione della directory che contiene il
file (e scrittura della relativa voce su disco) che deve essere effettuata esplicitamente.16
14
come già accennato neanche questo dà la garanzia assoluta che i dati siano integri dopo la chiamata, l’hardware
dei dischi è in genere dotato di un suo meccanismo interno di ottimizzazione per l’accesso al disco che può ritardare
ulteriormente la scrittura effettiva.
15
a partire dal kernel 2.2.8
16
in realtà per il filesystem ext2, quando lo si monta con l’opzione sync, il kernel provvede anche alla
sincronizzazione automatica delle voci delle directory.
194 CAPITOLO 6. I FILE: L’INTERFACCIA STANDARD UNIX
#include <unistd.h>
int dup(int oldfd)
Crea una copia del file descriptor oldfd.
La funzione ritorna il nuovo file descriptor in caso di successo e −1 in caso di errore, nel qual caso
errno assumerà uno dei valori:
EBADF oldfd non è un file aperto.
EMFILE si è raggiunto il numero massimo consentito di file descriptor aperti.
La funzione ritorna, come open, il primo file descriptor libero. Il file descriptor è una copia
esatta del precedente ed entrambi possono essere interscambiati nell’uso. Per capire meglio il
funzionamento della funzione si può fare riferimento a fig. 6.4: l’effetto della funzione è sem-
plicemente quello di copiare il valore nella struttura file_struct, cosicché anche il nuovo file
descriptor fa riferimento alla stessa voce nella file table; per questo si dice che il nuovo file
descriptor è duplicato, da cui il nome della funzione.
Si noti che per quanto illustrato in fig. 6.4 i file descriptor duplicati condivideranno eventuali
lock, file status flag, e posizione corrente. Se ad esempio si esegue una lseek per modificare
la posizione su uno dei due file descriptor, essa risulterà modificata anche sull’altro (dato che
quello che viene modificato è lo stesso campo nella voce della file table a cui entrambi fanno
riferimento). L’unica differenza fra due file descriptor duplicati è che ciascuno avrà il suo file
descriptor flag; a questo proposito va specificato che nel caso di dup il flag di close-on-exec (vedi
sez. 3.2.5 e sez. 6.3.6) viene sempre cancellato nella copia.
L’uso principale di questa funzione è per la redirezione dell’input e dell’output fra l’esecuzione
di una fork e la successiva exec; diventa cosı̀ possibile associare un file (o una pipe) allo standard
input o allo standard output (torneremo sull’argomento in sez. 11.1.2, quando tratteremo le pipe).
Per fare questo in genere occorre prima chiudere il file che si vuole sostituire, cosicché il suo file
descriptor possa esser restituito alla chiamata di dup, come primo file descriptor disponibile.
6.3. CARATTERISTICHE AVANZATE 195
Dato che questa è l’operazione più comune, è prevista una diversa versione della funzione,
dup2, che permette di specificare esplicitamente qual è il valore di file descriptor che si vuole
avere come duplicato; il suo prototipo è:
#include <unistd.h>
int dup2(int oldfd, int newfd)
Rende newfd una copia del file descriptor oldfd.
La funzione ritorna il nuovo file descriptor in caso di successo e −1 in caso di errore, nel qual caso
errno assumerà uno dei valori:
EBADF oldfd non è un file aperto o newfd ha un valore fuori dall’intervallo consentito per i
file descriptor.
EMFILE si è raggiunto il numero massimo consentito di file descriptor aperti.
e qualora il file descriptor newfd sia già aperto (come avviene ad esempio nel caso della dupli-
cazione di uno dei file standard) esso sarà prima chiuso e poi duplicato (cosı̀ che il file duplicato
sarà connesso allo stesso valore per il file descriptor).
La duplicazione dei file descriptor può essere effettuata anche usando la funzione di controllo
dei file fcntl (che esamineremo in sez. 6.3.6) con il parametro F_DUPFD. L’operazione ha la
sintassi fcntl(oldfd, F_DUPFD, newfd) e se si usa 0 come valore per newfd diventa equivalente
a dup.
La sola differenza fra le due funzioni17 è che dup2 chiude il file descriptor newfd se questo
è già aperto, garantendo che la duplicazione sia effettuata esattamente su di esso, invece fcntl
restituisce il primo file descriptor libero di valore uguale o maggiore di newfd (e se newfd è
aperto la duplicazione avverrà su un altro file descriptor).
relativo file descriptor alle varie funzioni che useranno quella directory come punto di partenza
per la risoluzione.20
Questo metodo, oltre a risolvere i problemi di race condition, consente anche di ottenere au-
menti di prestazioni significativi quando si devono eseguire molte operazioni su sezioni dell’albero
dei file che prevedono delle gerarchie di sottodirectory molto profonde; infatti in questo caso ba-
sta eseguire la risoluzione del pathname della directory di partenza una sola volta (nell’apertura
iniziale) e non tutte le volte che si deve accedere a ciascun file che essa contiene.
La sintassi generale di queste nuove funzioni è che esse prevedono come primo argomento il
file 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
è definita 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 file usando come directory di lavoro corrente dirfd.
la funzione restituisce gli stessi valori e gli stessi codici di errore di open, ed in più:
EBADF dirfd non è un file descriptor valido.
ENOTDIR pathname è un pathname relativo, ma dirfd fa riferimento ad un file.
Il comportamento delle nuove funzioni è del tutto analogo a quello delle corrispettive classi-
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 pathna-
me assoluto dirfd verrà semplicemente ignorato. Infine se per dirfd si usa il valore speciale
AT_FDCWD,21 la risoluzione sarà effettuata rispetto alla directory di lavoro corrente del processo.
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
valori errati per dirfd; in particolare si avrà un errore di EBADF se esso non è un file descriptor
valido, ed un errore di ENOTDIR se esso non fa riferimento ad una directory.22
In tab. 6.3 si sono riportate le funzioni introdotte con questa nuova interfaccia, con a fianco
la corrispondente funzione classica.23 La gran parte di queste seguono la convenzione appena
vista per openat, in cui agli argomenti della corrispondente funzione classica viene anteposto
l’argomento dirfd.24 Per una parte di queste, indicate dal contenuto della omonima colonna di
tab. 6.3, oltre al nuovo argomento iniziale, è prevista anche l’aggiunta di un ulteriore argomento
finale, flags.
Per tutte le funzioni che lo prevedono, a parte unlinkat e faccessat, l’ulteriore argomento
è stato introdotto solo per fornire un meccanismo con cui modificarne il comportamento nel caso
si stia operando su un link simbolico, cosı̀ da poter scegliere se far agire la funzione direttamente
sullo stesso o sul file da esso referenziato. Dato che in certi casi esso può fornire ulteriori in-
dicazioni per modificare il comportamento delle funzioni, flags deve comunque essere passato
come maschera binaria, ed impostato usando i valori delle appropriate costanti AT_*, definite in
fcntl.h.
20
in questo modo, anche quando si lavora con i thread, si può mantenere una directory di lavoro diversa per
ciascuno di essi.
21
questa, come le altre costanti AT_*, è definita in fcntl.h, pertanto se la si vuole usare occorrerà includere
comunque questo file, anche per le funzioni che non sono definite in esso.
22
tranne il caso in cui si sia specificato un pathname assoluto, nel qual caso, come detto, il valore di dirfd sarà
completamente ignorato.
23
in realtà, come visto in sez. 5.1.8, le funzioni utimes e lutimes non sono propriamente le corrispondenti di
utimensat, dato che questa ha una maggiore precisione nella indicazione dei tempi dei file.
24
non staremo pertanto a riportarle una per una.
25
in questo caso l’argomento flags è disponibile ed utilizzabile solo a partire dal kernel 2.6.18.
6.3. CARATTERISTICHE AVANZATE 197
Tabella 6.3: Corrispondenze fra le nuove funzioni “at” e le corrispettive funzioni classiche.
Come esempio di questo secondo tipo di funzioni possiamo considerare fchownat, che può
essere usata per sostituire sia chown che lchown; il suo prototipo è:
#include <unistd.h>
#include <fcntl.h>
int fchownat(int dirfd, const char *pathname, uid_t owner, gid_t group, int
flags)
.Modifica la proprietà di un file.
la funzione restituisce gli stessi valori e gli stessi codici di errore di chown, ed in più:
EBADF dirfd non è un file descriptor valido.
EINVAL flags non ha un valore valido.
ENOTDIR pathname è un pathname relativo, ma dirfd fa riferimento ad un file.
#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ù:
EBADF dirfd non è un file descriptor valido.
EINVAL flags non ha un valore valido.
ENOTDIR pathname è un pathname relativo, ma dirfd fa riferimento ad un file.
La funzione esegue lo stesso controllo di accesso effettuabile con access, ma si può utilizzare
l’argomento flags per modificarne il comportamento rispetto a quello ordinario di access. In
questo caso esso può essere specificato come maschera binaria di due valori:
26
in fcntl.h è definito anche AT_SYMLINK_FOLLOW, che richiede di dereferenziare i link simbolici, essendo questo
però il comportamento adottato per un valore nullo di flags questo valore non viene mai usato.
198 CAPITOLO 6. I FILE: L’INTERFACCIA STANDARD UNIX
Il primo argomento della funzione è sempre il numero di file descriptor fd su cui si vuole
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 dell’argomento cmd che in sostanza cor-
risponde all’esecuzione di un determinato comando; in sez. 6.3.4 abbiamo incontrato un esempio
dell’uso di fcntl per la duplicazione dei file descriptor, una lista di tutti i possibili valori per
cmd è riportata di seguito:
27
anche se flags è una maschera binaria, essendo questo l’unico flag disponibile per questa funzione, lo si può
assegnare direttamente.
28
ad esempio si gestiscono con questa funzione varie modalità di I/O asincrono (vedi sez. 12.3.1) e il file locking
(vedi sez. 12.1).
6.3. CARATTERISTICHE AVANZATE 199
F_DUPFD trova il primo file descriptor disponibile di valore maggiore o uguale ad arg e ne fa
una copia di fd. Ritorna il nuovo file descriptor in caso di successo e −1 in caso di
errore. Gli errori possibili sono EINVAL se arg è negativo o maggiore del massimo
consentito o EMFILE se il processo ha già raggiunto il massimo numero di descrittori
consentito.
F_SETFD imposta il valore del file descriptor flag al valore specificato con arg. Al momento
l’unico bit usato è quello di close-on-exec, identificato dalla costante FD_CLOEXEC,
che serve a richiedere che il file 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.
F_GETFD ritorna il valore del file descriptor flag di fd o −1 in caso di errore; se FD_CLOEXEC
è impostato i file descriptor aperti vengono chiusi attraverso una exec altrimenti
(il comportamento predefinito) restano aperti.
F_GETFL ritorna il valore del file status flag in caso di successo o −1 in caso di errore; per-
mette cioè di rileggere quei bit impostati da open all’apertura del file che vengono
memorizzati (quelli riportati nella prima e terza sezione di tab. 6.2).
F_SETFL imposta il file status flag al valore specificato 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.29
F_GETLK richiede un controllo sul file lock specificato 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. 12.1.3.
F_SETLK richiede o rilascia un file lock a seconda di quanto specificato nella struttura puntata
da lock. Se il lock è tenuto da qualcun altro ritorna immediatamente restituendo
−1 e imposta errno a EACCES o EAGAIN, in caso di successo ritorna un valore nullo.
Questa funzionalità è trattata in dettaglio in sez. 12.1.3.
F_SETLKW identica a F_SETLK eccetto per il fatto che la funzione non ritorna subito ma attende
che il blocco sia rilasciato. Se l’attesa 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. 12.1.3.
F_GETOWN restituisce il pid del processo o l’identificatore del process group 30 che è preposto alla
ricezione dei segnali SIGIO31 per gli eventi associati al file descriptor fd32 e SIGURG
per la notifica dei dati urgenti di un socket.33 Nel caso di un process group viene
restituito un valore negativo il cui valore assoluto corrisponde all’identificatore del
process group. In caso di errore viene restituito −1.
F_SETOWN imposta, con il valore dell’argomento arg, l’identificatore del processo o del process
group che riceverà i segnali SIGIO e SIGURG per gli eventi associati al file descriptor
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 all’identificatore del process group.
29
la pagina di manuale riporta come impostabili solo O_APPEND, O_NONBLOCK e O_ASYNC.
30
i process group sono (vedi sez. 10.1.2) raggruppamenti di processi usati nel controllo di sessione; a ciascuno di
essi è associato un identificatore (un numero positivo analogo al pid).
31
o qualunque altro segnale alternativo impostato con F_FSETSIG.
32
il segnale viene usato sia per il Signal Drive I/O, che tratteremo in sez. 12.3.1, e dai vari meccanismi di notifica
asincrona, che tratteremo in sez. 12.3.2.
33
vedi sez. 19.1.3.
200 CAPITOLO 6. I FILE: L’INTERFACCIA STANDARD UNIX
F_GETSIG restituisce il valore del segnale inviato quando ci sono dati disponibili in ingresso su
un file descriptor aperto ed impostato per l’I/O asincrono (si veda sez. 12.3.3). Il
valore 0 indica il valore predefinito (che è SIGIO), un valore diverso da zero indica
il segnale richiesto, (che può essere anche lo stesso SIGIO). In caso di errore ritorna
−1.
F_SETSIG imposta il segnale da inviare quando diventa possibile effettuare I/O sul file de-
scriptor 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 predefinito, SIGIO. Un altro
valore diverso da zero (compreso lo stesso SIGIO) specifica il segnale voluto; l’uso
di un valore diverso da zero permette inoltre, se si è installato il gestore del segnale
come sa_sigaction usando SA_SIGINFO, (vedi sez. 9.4.3), di rendere disponibili al
gestore informazioni ulteriori riguardo il file che ha generato il segnale attraverso i
valori restituiti in siginfo_t (come vedremo in sez. 12.3.3).34
F_SETLEASE imposta o rimuove un file lease 35 sul file descriptor fd a seconda del valore del
terzo argomento, che in questo caso è un int, ritorna un valore nullo in caso di
successo o −1 in caso di errore. Questa funzionalità avanzata è trattata in dettaglio
in sez. 12.3.2.
F_GETLEASE restituisce il tipo di file lease che il processo detiene nei confronti del file descriptor
fd o −1 in caso di errore. Con questo comando il terzo argomento può essere omesso.
Questa funzionalità avanzata è trattata in dettaglio in sez. 12.3.2.
F_NOTIFY attiva un meccanismo di notifica per cui viene riportata al processo chiamante,
tramite il segnale SIGIO (o altro segnale specificato con F_SETSIG) ogni modifica
eseguita o direttamente sulla directory cui fd fa riferimento, o su uno dei file 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
dettaglio in sez. 12.3.2.
La maggior parte delle funzionalità di fcntl sono troppo avanzate per poter essere affrontate
in tutti i loro aspetti a questo punto; saranno pertanto riprese più avanti quando affronteremo
le problematiche ad esse relative. In particolare le tematiche relative all’I/O asincrono e ai vari
meccanismi di notifica saranno trattate in maniera esaustiva in sez. 12.3 mentre quelle relative al
file locking saranno esaminate in sez. 12.1). L’uso di questa funzione con i socket verrà trattato
in sez. 17.3.
Si tenga presente infine che quando si usa la funzione per determinare le modalità di accesso
con cui è stato aperto il file (attraverso l’uso del comando F_GETFL) è necessario estrarre i bit
corrispondenti nel file status flag che si è ottenuto. Infatti la definizione corrente di quest’ultimo
non assegna bit separati alle tre diverse modalità O_RDONLY, O_WRONLY e O_RDWR.36 Per questo
motivo il valore della modalità di accesso corrente si ottiene eseguendo un AND binario del valore
di ritorno di fcntl con la maschera O_ACCMODE (anch’essa definita in fcntl.h), che estrae i bit
di accesso dal file status flag.
le stesse funzioni usate per i normali file di dati, esisteranno sempre caratteristiche peculiari,
specifiche dell’hardware e della funzionalità che ciascun dispositivo può provvedere, che non
possono venire comprese in questa interfaccia astratta (un caso tipico è l’impostazione della
velocità di una porta seriale, o le dimensioni di un framebuffer).
Per questo motivo nell’architettura del sistema è stata prevista l’esistenza di una funzione
apposita, ioctl, con cui poter compiere le operazioni specifiche di ogni dispositivo particolare,
usando come riferimento il solito file descriptor. Il prototipo di questa funzione è:
#include <sys/ioctl.h>
int ioctl(int fd, int request, ...)
Esegue l’operazione di controllo specificata da request sul file descriptor fd.
La funzione nella maggior parte dei casi ritorna 0, alcune operazioni usano però il valore di ritorno
per restituire informazioni. In caso di errore viene sempre restituito −1 ed errno assumerà uno
dei valori:
ENOTTY il file fd non è associato con un dispositivo, o la richiesta non è applicabile all’oggetto
a cui fa riferimento fd.
EINVAL 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 nell’interfaccia ordinaria della gestione dei file e che non è possibile effettuare con
le funzioni esaminate finora. La funzione richiede che si passi come primo argomento un file
descriptor regolarmente aperto, e l’operazione da compiere viene selezionata attraverso il valore
dell’argomento request. Il terzo argomento dipende dall’operazione prescelta; tradizionalmente
è specificato come char * argp, da intendersi come puntatore ad un area di memoria generica,37
ma per certe operazioni può essere omesso, e per altre è un semplice intero.
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
utilizzato come parametro di uscita. È più comune comunque restituire i risultati all’indirizzo
puntato dal terzo argomento.
Data la genericità dell’interfaccia non è possibile classificare in maniera sistematica le ope-
razioni che si possono gestire con ioctl, un breve elenco di alcuni esempi di esse è il seguente:
alcuni casi, relativi a valori assegnati prima che questa differenziazione diventasse pratica cor-
rente, si potrebbero usare valori validi anche per il dispositivo corrente, con effetti imprevedibili
o indesiderati.
Data la assoluta specificità della funzione, il cui comportamento varia da dispositivo a di-
spositivo, non è possibile fare altro che dare una descrizione sommaria delle sue caratteristiche;
torneremo ad esaminare in seguito40 quelle relative ad alcuni casi specifici (ad esempio la ge-
stione dei terminali è effettuata attraverso ioctl in quasi tutte le implementazioni di Unix), qui
riportiamo solo l’elenco delle operazioni che sono predefinite per qualunque file,41 caratterizzate
dal prefisso FIO:
FIOCLEX imposta il flag di close-on-exec sul file, in questo caso, essendo usata come opera-
zione logica, ioctl non richiede un terzo argomento, il cui eventuale valore viene
ignorato.
FIONCLEX cancella il flag di close-on-exec sul file, in questo caso, essendo usata come opera-
zione logica, ioctl non richiede un terzo argomento, il cui eventuale valore viene
ignorato.
FIOASYNC abilita o disabilita la modalità di I/O asincrono sul file (vedi sez. 12.3.1); il terzo
argomento deve essere un puntatore ad un intero (cioè di tipo const int *) che
contiene un valore logico (un valore nullo disabilita, un valore non nullo abilita).
FIONBIO abilita o disabilita sul file l’I/O in modalità non bloccante; il terzo argomento deve
essere un puntatore ad un intero (cioè di tipo const int *) che contiene un valore
logico (un valore nullo disabilita, un valore non nullo abilita).
FIOSETOWN imposta il processo che riceverà i segnali SIGURG e SIGIO generati sul file; il terzo
argomento deve essere un puntatore ad un intero (cioè di tipo const int *) il cui
valore specifica il PID del processo.
FIOGETOWN legge il processo che riceverà i segnali SIGURG e SIGIO generati sul file; il terzo
argomento deve essere un puntatore ad un intero (cioè di tipo int *) su cui sarà
scritto il PID del processo.
FIONREAD legge il numero di byte disponibili in lettura sul file descriptor;42 il terzo argomento
deve essere un puntatore ad un intero (cioè di tipo int *) su cui sarà restituito il
valore.
Esamineremo in questo capitolo l’interfaccia standard ANSI C per i file, quella che viene co-
munemente detta interfaccia degli stream. Dopo una breve sezione introduttiva tratteremo le
funzioni base per la gestione dell’input/output, mentre tratteremo le caratteristiche più avanzate
dell’interfaccia nell’ultima sezione.
7.1 Introduzione
Come visto in cap. 6 le operazioni di I/O sui file sono gestibili a basso livello con l’interfaccia
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
invece sono realizzate attraverso opportune funzioni di libreria, queste, insieme alle altre funzioni
definite dallo standard, vengono a costituire il nucleo1 delle glibc.
203
204 CAPITOLO 7. I FILE: L’INTERFACCIA STANDARD ANSI C
FILE *stdin Lo standard input cioè lo stream da cui il processo riceve ordinariamente i
dati in ingresso. Normalmente è associato dalla shell all’input del terminale
e prende i caratteri dalla tastiera.
FILE *stdout Lo standard output cioè lo stream su cui il processo invia ordinariamente i
dati in uscita. Normalmente è associato dalla shell all’output del terminale e
scrive sullo schermo.
FILE *stderr Lo standard error cioè lo stream su cui il processo è supposto inviare i mes-
saggi di errore. Normalmente anch’esso è associato dalla shell all’output del
terminale e scrive sullo schermo.
Nelle glibc stdin, stdout e stderr sono effettivamente tre variabili di tipo FILE * che
possono essere usate come tutte le altre, ad esempio si può effettuare una redirezione dell’output
di un programma con il semplice codice:
fclose ( stdout );
stdout = fopen ( " standard - output - file " , " w " );
ma in altri sistemi queste variabili possono essere definite da macro, e se si hanno problemi di
portabilità e si vuole essere sicuri, diventa opportuno usare la funzione freopen.
si ha un accesso contemporaneo allo stesso file (ad esempio da parte di un altro processo) si
potranno vedere solo le parti effettivamente scritte, e non quelle ancora presenti nel buffer.
Per lo stesso motivo, in tutte le situazioni in cui si sta facendo dell’input/output interattivo,
bisognerà tenere presente le caratteristiche delle operazioni di scaricamento dei dati, poiché non
è detto che ad una scrittura sullo stream corrisponda una immediata scrittura sul dispositivo
(la cosa è particolarmente evidente quando con le operazioni di input/output su terminale).
Per rispondere ad esigenze diverse, lo standard definisce tre distinte modalità in cui può
essere eseguita la bufferizzazione, delle quali occorre essere ben consapevoli, specie in caso di
lettura e scrittura da dispositivi interattivi:
• unbuffered : in questo caso non c’è bufferizzazione ed i caratteri vengono trasmessi diretta-
mente al file non appena possibile (effettuando immediatamente una write).
• line buffered : in questo caso i caratteri vengono normalmente trasmessi al file in blocco
ogni volta che viene incontrato un carattere di newline (il carattere ASCII \n).
• fully buffered : in questo caso i caratteri vengono trasmessi da e verso il file in blocchi di
dimensione opportuna.
Lo standard ANSI C specifica inoltre che lo standard output e lo standard input siano aperti
in modalità fully buffered quando non fanno riferimento ad un dispositivo interattivo, e che lo
standard error non sia mai aperto in modalità fully buffered.
Linux, come BSD e SVr4, specifica il comportamento predefinito in maniera ancora più
precisa, e cioè impone che lo standard error sia sempre unbuffered (in modo che i messaggi di
errore siano mostrati il più rapidamente possibile) e che standard input e standard output siano
aperti in modalità line buffered quando sono associati ad un terminale (od altro dispositivo
interattivo) ed in modalità fully buffered altrimenti.
Il comportamento specificato 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 file o del dispositivo scelto.
La modalità line buffered è quella che necessita di maggiori chiarimenti e attenzioni per
quel che concerne il suo funzionamento. Come già accennato nella descrizione, di norma i dati
vengono inviati al kernel alla ricezione di un carattere di a capo (newline); questo non è vero
in tutti i casi, infatti, dato che le dimensioni del buffer usato dalle librerie sono fisse, se le si
eccedono si può avere uno scarico dei dati anche prima che sia stato inviato un carattere di
newline.
Un secondo punto da tenere presente, particolarmente quando si ha a che fare con I/O
interattivo, è che quando si effettua una lettura da uno stream che comporta l’accesso al kernel3
viene anche eseguito lo scarico di tutti i buffer degli stream in scrittura.
In sez. 7.3.2 vedremo come la libreria definisca delle opportune funzioni per controllare le
modalità di bufferizzazione e lo scarico dei dati.
#include <stdio.h>
FILE *fopen(const char *path, const char *mode)
Apre il file specificato da path.
FILE *fdopen(int fildes, const char *mode)
Associa uno stream al file descriptor fildes.
FILE *freopen(const char *path, const char *mode, FILE *stream)
Apre il file specificato da path associandolo allo stream specificato da stream, se questo è
già aperto prima lo chiude.
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 l’esecuzione.
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 file specificato
nella modalità specificata da mode, che è una stringa che deve iniziare con almeno uno dei valori
indicati in tab. 7.1 (sono possibili varie estensioni che vedremo in seguito).
L’uso più comune di freopen è per redirigere uno dei tre file standard (vedi sez. 7.1.3): il
file path viene associato a stream e se questo è uno stream già aperto viene preventivamente
chiuso.
Infine fdopen viene usata per associare uno stream ad un file 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 file come le fifo o i socket, che non possono essere aperti con le
funzioni delle librerie standard del C.
Valore Significato
r Il file viene aperto, l’accesso viene posto in sola lettura,
lo stream è posizionato all’inizio del file.
r+ Il file viene aperto, l’accesso viene posto in lettura e
scrittura, lo stream è posizionato all’inizio del file.
w Il file viene aperto e troncato a lunghezza nulla (o creato
se non esiste), l’accesso viene posto in sola scrittura, lo
stream è posizionato all’inizio del file.
w+ Il file viene aperto e troncato a lunghezza nulla (o creato
se non esiste), l’accesso viene posto in scrittura e lettura,
lo stream è posizionato all’inizio del file.
a Il file viene aperto (o creato se non esiste) in append mode,
l’accesso viene posto in sola scrittura.
a+ Il file viene aperto (o creato se non esiste) in append mode,
l’accesso viene posto in lettura e scrittura.
b Specifica che il file è binario, non ha alcun effetto.
x L’apertura fallisce se il file esiste già.
Tabella 7.1: Modalità di apertura di uno stream dello standard ANSI C che sono sempre presenti in qualunque
sistema POSIX.
In realtà lo standard ANSI C prevede un totale di 15 possibili valori diversi per mode, ma in
tab. 7.1 si sono riportati solo i sei valori effettivi, ad essi può essere aggiunto pure il carattere b
(come ultimo carattere o nel mezzo agli altri per le stringhe di due caratteri) che in altri sistemi
operativi serve a distinguere i file binari dai file di testo; in un sistema POSIX questa distinzione
non esiste e il valore viene accettato solo per compatibilità, ma non ha alcun effetto.
4
fopen e freopen fanno parte dello standard ANSI C, fdopen è parte dello standard POSIX.1.
7.2. FUNZIONI BASE 207
Le glibc supportano alcune estensioni, queste devono essere sempre indicate dopo aver speci-
ficato il mode con uno dei valori di tab. 7.1. L’uso del carattere x serve per evitare di sovrascrivere
un file già esistente (è analoga all’uso dell’opzione O_EXCL in open), se il file specificato già esiste
e si aggiunge questo carattere a mode la fopen fallisce.
Un’altra 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 codifica dei
caratteri e fopen marca il file per l’uso dei caratteri estesi e abilita le opportune funzioni di
conversione in lettura e scrittura.
Nel caso si usi fdopen i valori specificati da mode devono essere compatibili con quelli con
cui il file descriptor è stato aperto. Inoltre i modi w e w+ non troncano il file. La posizione nello
stream viene impostata a quella corrente nel file descriptor, e le variabili di errore e di fine del
file (vedi sez. 7.2.2) sono cancellate. Il file non viene duplicato e verrà chiuso alla chiusura dello
stream.
I nuovi file saranno creati secondo quanto visto in sez. 5.3.4 ed avranno i permessi di ac-
cesso impostati al valore S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH (pari a 0666)
modificato secondo il valore di umask per il processo (si veda sez. 5.3.3).
In caso di file aperti in lettura e scrittura occorre ricordarsi che c’è di mezzo una bufferizza-
zione; per questo motivo lo standard ANSI C richiede che ci sia un’operazione di posizionamento
fra un’operazione di output ed una di input o viceversa (eccetto il caso in cui l’input ha incontra-
to la fine del file), altrimenti una lettura può ritornare anche il risultato di scritture precedenti
l’ultima effettuata.
Per questo motivo è una buona pratica (e talvolta necessario) far seguire ad una scrittura
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 un’operazione nominalmente nulla
come fseek(file, 0, SEEK_CUR) è sufficiente a garantire la sincronizzazione.
Una volta aperto lo stream, si può cambiare la modalità di bufferizzazione (si veda sez. 7.3.2)
fintanto che non si è effettuato alcuna operazione di I/O sul file.
Uno stream viene chiuso con la funzione fclose il cui prototipo è:
#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 file descriptor indicato da stream non è valido, o uno dei valori specificati dalla sottostante
funzione che è fallita (close, write o fflush).
La funzione effettua lo scarico di tutti i dati presenti nei buffer di uscita e scarta tutti i dati
in ingresso; se era stato allocato un buffer per lo stream questo verrà rilasciato. La funzione
effettua lo scarico solo per i dati presenti nei buffer in user space usati dalle glibc; se si vuole
essere sicuri che il kernel forzi la scrittura su disco occorrerà effettuare una sync (vedi sez. 6.3.3).
Linux supporta anche una altra funzione, fcloseall, come estensione GNU implementata
dalle glibc, accessibile avendo definito _GNU_SOURCE, il suo prototipo è:
#include <stdio.h>
int fcloseall(void)
Chiude tutti gli stream.
la funzione esegue lo scarico dei dati bufferizzati in uscita e scarta quelli in ingresso, chiudendo
tutti i file. Questa funzione è provvista solo per i casi di emergenza, quando si è verificato un
errore ed il programma deve essere abortito, ma si vuole compiere qualche altra operazione dopo
aver chiuso i file e prima di uscire (si ricordi quanto visto in sez. 2.1.3).
208 CAPITOLO 7. I FILE: L’INTERFACCIA STANDARD ANSI C
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 bufferizzazione 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.
#include <stdio.h>
int feof(FILE *stream)
Controlla il flag di end-of-file di stream.
int ferror(FILE *stream)
Controlla il flag di errore di stream.
Entrambe le funzioni ritornano un valore diverso da zero se i relativi flag sono impostati.
si tenga presente comunque che la lettura di questi flag segnala soltanto che c’è stato un errore,
o che si è raggiunta la fine del file in una qualunque operazione sullo stream, il controllo quindi
deve essere effettuato ogni volta che si chiama una funzione di libreria.
Entrambi i flag (di errore e di end-of-file) possono essere cancellati usando la funzione
clearerr, il cui prototipo è:
#include <stdio.h>
void clearerr(FILE *stream)
Cancella i flag di errore ed end-of-file di stream.
in genere si usa questa funzione una volta che si sia identificata e corretta la causa di un errore
per evitare di mantenere i flag 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 209
Entrambe le funzioni ritornano il numero di elementi letti o scritti, in caso di errore o fine del file
viene restituito un numero di elementi inferiore al richiesto.
In genere si usano queste funzioni quando si devono trasferire su file blocchi di dati binari in
maniera compatta e veloce; un primo caso di uso tipico è quello in cui si salva un vettore (o un
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 specificate 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 file una
struttura; si avrà allora una chiamata tipo:
struct histogram {
int nbins ;
double max , min ;
double * bin ;
} histo ;
In caso di errore (o fine del file per fread) entrambe le funzioni restituiscono il numero
di oggetti effettivamente letti o scritti, che sarà inferiore a quello richiesto. Contrariamente a
quanto avviene per i file descriptor, questo segnala una condizione di errore e occorrerà usare
feof e ferror per stabilire la natura del problema.
Benché queste funzioni assicurino la massima efficienza per il salvataggio dei dati, i dati me-
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 l’occupazione di spazio, impac-
chettando più strettamente i dati, o la velocità inserendo opportuni padding per l’allineamento
dei medesimi generando quindi output binari diversi. Inoltre altre incompatibilità si possono
presentare quando entrano in gioco differenze di architettura hardware, come la dimensione del
bus o la modalità di ordinamento dei bit o il formato delle variabili in floating point.
Per questo motivo quando si usa l’input/output binario occorre sempre prendere le opportune
precauzioni (in genere usare un formato di più alto livello che permetta di recuperare l’informa-
zione completa), per assicurarsi che versioni diverse del programma siano in grado di rileggere i
dati tenendo conto delle eventuali differenze.
Le glibc definiscono altre due funzioni per l’I/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.
l’indirizzo, che può essere passato come argomento ad un altra funzione (e non si hanno i problemi
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 l’espansione del segno). In questo modo il valore di ritorno è sempre
positivo, tranne in caso di errore o fine del file.
Nelle estensioni GNU che provvedono la localizzazione sono definite 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 è:
#include <stdio.h>
#include <wchar.h>
wint_t getwc(FILE *stream)
Legge un carattere esteso da stream. In genere è implementata come una macro.
wint_t fgetwc(FILE *stream)
Legge un carattere esteso da stream. È 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 fine del file il valore di
ritorno è WEOF.
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.
int fputc(int c, FILE *stream)
Scrive il carattere c su stream. È una sempre una funzione.
int putchar(int c)
Equivalente a putc(stdout).
Le funzioni scrivono sempre un carattere alla volta, il cui valore viene restituito in caso di successo;
in caso di errore o fine del file il valore di ritorno è EOF.
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 fine del file il valore di ritorno è EOF.
Come nel caso dell’I/O binario con fread e fwrite le glibc provvedono come estensione,
per ciascuna delle funzioni precedenti, un’ulteriore funzione, il cui nome è ottenuto aggiungendo
un _unlocked, che esegue esattamente le stesse operazioni, evitando però il lock implicito dello
stream.
Per compatibilità con SVID sono inoltre provviste anche due funzioni, getw e putw, da usare
per leggere e scrivere una word (cioè due byte in una volta); i loro prototipi sono:
#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 leggono e scrivono una word di due byte, usando comunque una variabile di
tipo int; il loro uso è deprecato in favore dell’uso di fread e fwrite, in quanto non è possibile
distinguere il valore -1 da una condizione di errore che restituisce EOF.
Uno degli usi più frequenti dell’input/output a caratteri è nei programmi di parsing in cui si
analizza il testo; in questo contesto diventa utile poter analizzare il carattere successivo da uno
stream senza estrarlo effettivamente (la tecnica è detta peeking ahead ) in modo che il programma
possa regolarsi avendo dato una sbirciatina a quello che viene dopo.
212 CAPITOLO 7. I FILE: L’INTERFACCIA STANDARD ANSI C
Nel nostro caso questo tipo di comportamento può essere realizzato prima leggendo il ca-
rattere, e poi rimandandolo indietro, cosicché ridiventi disponibile per una lettura successiva; la
funzione che inverte la lettura si chiama ungetc ed il suo prototipo è:
#include <stdio.h>
int ungetc(int c, FILE *stream)
Rimanda indietro il carattere c, con un cast a unsigned char, sullo stream stream.
benché lo standard ANSI C preveda che l’operazione possa essere ripetuta per un numero arbi-
trario di caratteri, alle implementazioni è richiesto di garantire solo un livello; questo è quello
che fa la glibc, che richiede che avvenga un’altra operazione fra due ungetc successive.
Non è necessario che il carattere che si manda indietro sia l’ultimo che si è letto, e non è
necessario neanche avere letto nessun carattere prima di usare ungetc, ma di norma la funzione
è intesa per essere usata per rimandare indietro l’ultimo carattere letto.
Nel caso c sia un EOF la funzione non fa nulla, e restituisce sempre EOF; cosı̀ si può usare
ungetc anche con il risultato di una lettura alla fine del file.
Se si è alla fine del file si può comunque rimandare indietro un carattere, il flag di end-of-file
verrà automaticamente cancellato perché c’è un nuovo carattere disponibile che potrà essere
riletto successivamente.
Infine si tenga presente che ungetc non altera il contenuto del file, ma opera esclusivamen-
te sul buffer interno. Se si esegue una qualunque delle operazioni di riposizionamento (vedi
sez. 7.2.7) i caratteri rimandati indietro vengono scartati.
Entrambe le funzioni effettuano la lettura (dal file specificato 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 fine del file (o c’è
un errore) viene restituito un NULL, ed il buffer buf non viene toccato. L’uso di gets è deprecato
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 buffer, si avrà un buffer overflow, con
sovrascrittura della memoria del processo adiacente al buffer.6
Questa è una delle vulnerabilità più sfruttate per guadagnare accessi non autorizzati al
sistema (i cosiddetti exploit), basta infatti inviare una stringa sufficientemente lunga ed oppor-
tunamente forgiata per sovrascrivere gli indirizzi di ritorno nello stack (supposto che la gets sia
stata chiamata da una subroutine), in modo da far ripartire l’esecuzione nel codice inviato nella
stringa stessa (in genere uno shell code cioè una sezione di programma che lancia una shell).
6
questa tecnica è spiegata in dettaglio e con molta efficacia nell’ormai famoso articolo di Aleph1 [8].
7.2. FUNZIONI BASE 213
La funzione fgets non ha i precedenti problemi di gets in quanto prende in input la di-
mensione del buffer size, che non verrà mai ecceduta in lettura. La funzione legge fino ad un
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 di-
mensione del buffer verranno letti solo size-1 caratteri, ma la stringa sarà sempre terminata
correttamente con uno zero finale; sarà possibile leggere i rimanenti caratteri in una chiamata
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
genere la forma più immediata per scrivere messaggi sullo standard output; la funzione prende
una stringa terminata da uno zero ed aggiunge automaticamente il ritorno a capo. La differenza
con fputs (a parte la possibilità di specificare un file diverso da stdout) è che quest’ultima non
aggiunge il newline, che deve essere previsto esplicitamente.
Come per le analoghe funzioni di input/output a caratteri, anche per l’I/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 file stream al buffer ws.
int fputws(const wchar_t *ws, FILE *stream)
Scrive la linea ws di caratteri estesi sul file stream.
Il comportamento di queste due funzioni è identico a quello di fgets e fputs, a parte il fatto
che tutto (numero di caratteri massimo, terminatore della stringa, newline) è espresso in termini
di caratteri estesi anziché di normali caratteri ASCII.
Come per l’I/O binario e quello a caratteri, anche per l’I/O di linea le glibc supportano
una serie di altre funzioni, estensioni di tutte quelle illustrate finora (eccetto gets e puts), che
eseguono esattamente le stesse operazioni delle loro equivalenti, evitando però il lock implicito
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 l’aggiunta dell’estensione _unlocked.
Come abbiamo visto, le funzioni di lettura per l’input/output di linea previste dallo standard
ANSI C presentano svariati inconvenienti. Benché fgets non abbia i gravissimi problemi di gets,
può comunque dare risultati ambigui se l’input contiene degli zeri; questi infatti saranno scritti
sul buffer di uscita e la stringa in output apparirà come più corta dei byte effettivamente letti.
Questa è una condizione che è sempre possibile controllare (deve essere presente un newline prima
della effettiva conclusione della stringa presente nel buffer), 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 buffer.
Per questo motivo le glibc prevedono, come estensione GNU, due nuove funzioni per la
gestione dell’input/output di linea, il cui uso permette di risolvere questi problemi. L’uso di
queste funzioni deve essere attivato definendo la macro _GNU_SOURCE prima di includere stdio.h.
214 CAPITOLO 7. I FILE: L’INTERFACCIA 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 è:
#include <stdio.h>
ssize_t getline(char **buffer, size_t *n, FILE *stream)
Legge una linea dal file stream copiandola sul buffer indicato da buffer riallocandolo se
necessario (l’indirizzo del buffer e la sua dimensione vengono sempre riscritte).
La funzione permette di eseguire una lettura senza doversi preoccupare della eventuale lun-
ghezza eccessiva della stringa da leggere. Essa prende come primo argomento l’indirizzo del
puntatore al buffer su cui si vuole copiare la linea. Quest’ultimo deve essere stato allocato in
precedenza con una malloc (non si può passare l’indirizzo di un puntatore ad una variabile loca-
le); come secondo argomento la funzione vuole l’indirizzo della variabile contenente le dimensioni
del buffer suddetto.
Se il buffer di destinazione è sufficientemente ampio la stringa viene scritta subito, altrimenti
il buffer 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
sez. 2.4.1).
Se si passa alla funzione l’indirizzo di un puntatore impostato a NULL e *n è zero, la funzione
provvede da sola all’allocazione della memoria necessaria a contenere la linea. In tutti i casi si
ottiene dalla funzione un puntatore all’inizio del testo della linea letta. Un esempio di codice
può essere il seguente:
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 è
alla fine del file e non si è potuto leggere nulla o c’è stato un errore la funzione restituisce -1.
La seconda estensione GNU è una generalizzazione di getline per poter usare come sepa-
ratore un carattere qualsiasi, la funzione si chiama getdelim ed il suo prototipo è:
#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.
L’output formattato viene eseguito con una delle 13 funzioni della famiglia printf; le tre
più usate sono printf, fprintf e sprintf, i cui prototipi sono:
#include <stdio.h>
int printf(const char *format, ...)
Stampa su stdout gli argomenti, secondo il formato specificato da format.
int fprintf(FILE *stream, const char *format, ...)
Stampa su stream gli argomenti, secondo il formato specificato da format.
int sprintf(char *str, const char *format, ...)
Stampa sulla stringa str gli argomenti, secondo il formato specificato da format.
le prime due servono per stampare su file (lo standard output o quello specificato) la terza
permette di stampare su una stringa, in genere l’uso di sprintf è sconsigliato in quanto è
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 buffer overflow ;
per questo motivo si consiglia l’uso dell’alternativa snprintf, il cui prototipo è:
#include <stdio.h>
snprintf(char *str, size_t size, const char *format, ...)
Identica a sprintf, ma non scrive su str più di size caratteri.
La parte più complessa delle funzioni di scrittura formattata è il formato della stringa format
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 è specificato in format).
Tabella 7.2: Valori possibili per gli specificatori di conversione in una stringa di formato di printf.
La stringa è costituita da caratteri normali (tutti eccetto %), che vengono passati invariati
all’output, e da direttive di conversione, in cui devono essere sempre presenti il carattere %,
che introduce la direttiva, ed uno degli specificatori 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 specificatore di conversione. In generale essa è sempre del tipo:
Valore Significato
# Chiede la conversione in forma alternativa.
0 La conversione è riempita con zeri alla sinistra del valore.
- 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.
in cui tutti i valori tranne il % e lo specificatore di conversione sono opzionali (e per questo
sono indicati fra parentesi quadre); si possono usare più elementi opzionali, nel qual caso devono
essere specificati in questo ordine:
• uno specificatore del parametro da usare (terminato da un $),
• uno o più flag (i cui valori possibili sono riassunti in tab. 7.3) che controllano il formato di
stampa della conversione,
• uno specificatore di larghezza (un numero decimale), eventualmente seguito (per i numeri
in virgola mobile) da un specificatore di precisione (un altro numero decimale),
• uno specificatore 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 Significato
hh Una conversione intera corrisponde a un char con o senza segno, o il
puntatore per il numero dei parametri n è di tipo char.
h Una conversione intera corrisponde a uno short con o senza segno, o il
puntatore per il numero dei parametri n è di tipo short.
l 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
la stringa seguenti sono in formato esteso.
ll 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.
L Una conversione in virgola mobile corrisponde a un double.
q Sinonimo di ll.
j Una conversione intera corrisponde a un intmax_t o uintmax_t.
z Una conversione intera corrisponde a un size_t o ssize_t.
t Una conversione intera corrisponde a un ptrdiff_t.
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 specificato da format.
int vfprintf(FILE *stream, const char *format, va_list ap)
Stampa su stream gli argomenti della lista ap, secondo il formato specificato da format.
int vsprintf(char *str, const char *format, va_list ap)
Stampa sulla stringa str gli argomenti della lista ap, secondo il formato specificato da
format.
con queste funzioni diventa possibile selezionare gli argomenti che si vogliono passare ad una
funzione di stampa, passando direttamente la lista tramite l’argomento ap. Per poter far questo
7.2. FUNZIONI BASE 217
ovviamente la lista degli argomenti dovrà essere opportunamente trattata (l’argomento è esa-
minato in sez. 2.4.2), e dopo l’esecuzione della funzione l’argomento ap non sarà più utilizzabile
(in generale dovrebbe essere eseguito un va_end(ap) ma in Linux questo non è necessario).
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.
Entrambe le funzioni prendono come argomento strptr che deve essere l’indirizzo di un
puntatore ad una stringa di caratteri, in cui verrà restituito (si ricordi quanto detto in sez. 2.4.1
a proposito dei value result argument) l’indirizzo 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.
Infine una ulteriore estensione GNU definisce le due funzioni dprintf e vdprintf, che pren-
dono un file 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
w davanti a print, sono trattate in dettaglio nella documentazione delle glibc.
In corrispondenza alla famiglia di funzioni printf che si usano per l’output formattato,
l’input formattato viene eseguito con le funzioni della famiglia scanf; fra queste le tre più
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 specificato da format, ed effettua le relative conversione memorizzando il risultato
negli argomenti seguenti.
int fscanf(FILE *stream, const char *format, ...)
Analoga alla precedente, ma effettua la scansione su stream.
int sscanf(char *str, const char *format, ...)
Analoga alle precedenti, ma effettua la scansione dalla stringa str.
Le funzioni ritornano il numero di elementi assegnati. Questi possono essere in numero inferiore a
quelli specificati, ed anche zero. Quest’ultimo valore significa che non si è trovata corrispondenza.
In caso di errore o fine del file 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
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
218 CAPITOLO 7. I FILE: L’INTERFACCIA STANDARD ANSI C
l’analogo di printf, ma ci sono varie differenze. Le funzioni di input infatti sono più orientate
verso la lettura di testo libero che verso un input formattato in campi fissi. 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 dall’input, scartano i separatori (e gli eventuali caratteri
diversi indicati dalla stringa di formato) effettuando le conversioni richieste; in caso la corri-
spondenza fallisca (o la funzione non sia in grado di effettuare 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 defini-
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,
l’input formattato non è molto usato. In genere infatti quando si ha a che fare con un input
relativamente semplice si preferisce usare l’input di linea ed effettuare 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
analizzatore lessicale o il bison8 per generare un parser.
#include <stdio.h>
int fseek(FILE *stream, long offset, int whence)
Sposta la posizione nello stream secondo quanto specificato tramite offset e whence.
void rewind(FILE *stream)
Riporta la posizione nello stream all’inizio del file.
L’uso di fseek è del tutto analogo a quello di lseek per i file descriptor, e gli argomenti, a
parte il tipo, hanno lo stesso significato; in particolare whence assume gli stessi valori già visti
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 all’inizio dello stream, ma non esattamente
7
il programma flex, è una implementazione libera di lex un generatore di analizzatori lessicali. Per i dettagli
si può fare riferimento al manuale [9].
8
il programma bison è un clone del generatore di parser yacc, maggiori dettagli possono essere trovati nel
relativo manuale [10].
9
dato che in un sistema Unix esistono vari tipi di file, come le fifo ed i file di dispositivo, non è scontato che
questo sia sempre vero.
7.3. FUNZIONI AVANZATE 219
equivalente ad una fseek(stream, 0L, SEEK_SET) in quanto vengono cancellati anche i flag di
errore e fine del file.
Per ottenere la posizione corrente si usa invece la funzione ftell, il cui prototipo è:
#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
al fatto che il file non supporta il riposizionamento che al fatto che la posizione non può essere
espressa con un long int
Restituisce il numero del file descriptor in caso di successo, e -1 qualora stream non sia valido, nel
qual caso imposta errno a EBADF.
aperto. La cosa può essere complessa se le operazioni vengono effettuate in una subroutine, che
a questo punto necessiterà di informazioni aggiuntive rispetto al semplice puntatore allo stream;
questo può essere evitato con le due funzioni __freadable e __fwritable i cui prototipi sono:
#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.
#include <stdio_ext.h>
int __freading(FILE *stream)
Restituisce un valore diverso da zero se stream è aperto in sola lettura o se l’ultima
operazione è stata di lettura.
int __fwriting(FILE *stream)
Restituisce un valore diverso da zero se stream è aperto in sola scrittura o se l’ultima
operazione è stata di scrittura.
Le due funzioni permettono di determinare di che tipo è stata l’ultima operazione eseguita
su uno stream aperto in lettura/scrittura; ovviamente se uno stream è aperto in sola lettura
(o sola scrittura) la modalità dell’ultima operazione è sempre determinata; l’unica ambiguità è
quando non sono state ancora eseguite operazioni, in questo caso le funzioni rispondono come
se una operazione ci fosse comunque stata.
#include <stdio.h>
int setvbuf(FILE *stream, char *buf, int mode, size_t size)
Imposta la bufferizzazione dello stream stream nella modalità indicata da mode, usando buf
come buffer 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 bufferizzazione; l’utente può specifi-
care un buffer da usare al posto di quello allocato dal sistema passandone alla funzione l’indirizzo
in buf e la dimensione in size.
Ovviamente se si usa un buffer specificato dall’utente 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 file; ma fintanto che il file è usato all’interno di
una funzione, può anche essere usata una variabile automatica. In stdio.h è definita la macro
BUFSIZ, che indica le dimensioni generiche del buffer di uno stream; queste vengono usate dalla
funzione setbuf. Non è detto però che tale dimensione corrisponda sempre al valore ottimale
(che può variare a seconda del dispositivo).
7.3. FUNZIONI AVANZATE 221
Dato che la procedura di allocazione manuale è macchinosa, comporta dei rischi (come delle
scritture accidentali sul buffer) e non assicura la scelta delle dimensioni ottimali, è sempre meglio
lasciare allocare il buffer alle funzioni di libreria, che sono in grado di farlo in maniera ottimale
e trasparente all’utente (in quanto la deallocazione avviene automaticamente). Inoltre siccome
alcune implementazioni usano parte del buffer per mantenere delle informazioni di controllo, non
è detto che le dimensioni dello stesso coincidano con quelle su cui viene effettuato l’I/O.
Valore Modalità
_IONBF unbuffered
_IOLBF line buffered
_IOFBF fully buffered
Tabella 7.5: Valori dell’argomento mode di setvbuf per l’impostazione delle modalità di bufferizzazione.
Per evitare che setvbuf imposti il buffer basta passare un valore NULL per buf e la funzione
ignorerà l’argomento size usando il buffer allocato automaticamente dal sistema. Si potrà co-
munque modificare la modalità di bufferizzazione, passando in mode uno degli opportuni valori
elencati in tab. 7.5. Qualora si specifichi la modalità non bufferizzata i valori di buf e size
vengono sempre ignorati.
Oltre a setvbuf le glibc definiscono altre tre funzioni per la gestione della bufferizzazione di
uno stream: setbuf, setbuffer e setlinebuf; i loro prototipi sono:
#include <stdio.h>
void setbuf(FILE *stream, char *buf)
Disabilita la bufferizzazione se buf è NULL, altrimenti usa buf come buffer di dimensione
BUFSIZ in modalità fully buffered.
void setbuffer(FILE *stream, char *buf, size_t size)
Disabilita la bufferizzazione se buf è NULL, altrimenti usa buf come buffer di dimensione
size in modalità fully buffered.
void setlinebuf(FILE *stream)
Pone lo stream in modalità line buffered.
tutte queste funzioni sono realizzate con opportune chiamate a setvbuf e sono definite solo per
compatibilità con le vecchie librerie BSD. Infine le glibc provvedono le funzioni non standard10
__flbf e __fbufsize che permettono di leggere le proprietà di bufferizzazione di uno stream; i
cui prototipi sono:
#include <stdio_ext.h>
int __flbf(FILE *stream)
Restituisce un valore diverso da zero se stream è in modalità line buffered.
size_t __fbufsize(FILE *stream)
Restituisce le dimensioni del buffer di stream.
Come già accennato, indipendentemente dalla modalità di bufferizzazione scelta, si può
forzare lo scarico dei dati sul file con la funzione fflush, il suo prototipo è:
#include <stdio.h>
int fflush(FILE *stream)
Forza la scrittura di tutti i dati bufferizzati 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.
anche di questa funzione esiste una analoga fflush_unlocked11 che non effettua il blocco dello
stream.
Se stream è NULL lo scarico dei dati è forzato per tutti gli stream aperti. Esistono però
circostanze, ad esempio quando si vuole essere sicuri che sia stato eseguito tutto l’output su
10
anche queste funzioni sono originarie di Solaris.
11
accessibile definendo _BSD_SOURCE o _SVID_SOURCE o _GNU_SOURCE.
222 CAPITOLO 7. I FILE: L’INTERFACCIA STANDARD ANSI C
terminale, in cui serve poter effettuare lo scarico dei dati solo per gli stream in modalità line
buffered; per questo motivo le glibc supportano una estensione di Solaris, la funzione _flushlbf,
il cui prototipo è:
#include <stdio-ext.h>
void _flushlbf(void)
Forza la scrittura di tutti i dati bufferizzati degli stream in modalità line buffered.
Si ricordi comunque che lo scarico dei dati dai buffer effettuato da queste funzioni non
comporta la scrittura di questi su disco; se si vuole che il kernel dia effettivamente avvio alle
operazioni di scrittura su disco occorre usare sync o fsync (si veda sez. 6.3.3).
Infine esistono anche circostanze in cui si vuole scartare tutto l’output pendente; per questo
si può usare fpurge, il cui prototipo è:
#include <stdio.h>
int fpurge(FILE *stream)
Cancella i buffer di input e di output dello stream stream.
La funzione scarta tutti i dati non ancora scritti (se il file è aperto in scrittura), e tutto l’input
non ancora letto (se è aperto in lettura), compresi gli eventuali caratteri rimandati indietro con
ungetc.
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)
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
programmatore pigro un’altra via13 da poter utilizzare per disabilitare in blocco il locking degli
stream: l’uso della funzione __fsetlocking, il cui prototipo è:
#include <stdio_ext.h>
int __fsetlocking (FILE *stream, int type)
Specifica o richiede a seconda del valore di type la modalità in cui le operazioni di I/O su
stream vengono effettuate rispetto all’acquisizione 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
del locking a seconda del valore specificato con type, che può essere uno dei seguenti:
FSETLOCKING_BYCALLER Al ritorno della funzione sarà l’utente a dover gestire da solo il locking
dello stream.
12
in certi casi dette funzioni possono essere usate, visto che sono molto più efficienti, anche in caso di necessità
di locking, una volta che questo sia stato acquisito manualmente.
13
anche questa mutuata da estensioni introdotte in Solaris.
224 CAPITOLO 7. I FILE: L’INTERFACCIA STANDARD ANSI C
Capitolo 8
In questo capitolo tratteremo varie interfacce che attengono agli aspetti più generali del sistema,
come quelle per la gestione dei parametri e della configurazione dello stesso, quelle per la lettura
dei limiti e delle caratteristiche, quelle per il controllo dell’uso delle risorse dei processi, quelle
per la gestione ed il controllo dei filesystem, degli utenti, dei tempi e degli errori.
La prima funzionalità si può ottenere includendo gli opportuni header file che contengono le
costanti necessarie definite 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
di questi limiti sono fissi in un’implementazione mentre possono variare in un altra. Tutto questo
crea una ambiguità che non è sempre possibile risolvere in maniera chiara; in generale quello
che succede è che quando i limiti del sistema sono fissi essi vengono definiti come macro di
225
226 CAPITOLO 8. LA GESTIONE DEL SISTEMA, DEL TEMPO E DEGLI ERRORI
preprocessore nel file limits.h, se invece possono variare, il loro valore sarà ottenibile tramite
la funzione sysconf (che esamineremo in sez. 8.1.2).
Lo standard ANSI C definisce dei limiti che sono tutti fissi, pertanto questo saranno sempre
disponibili al momento della compilazione. Un elenco, ripreso da limits.h, è riportato in tab. 8.1.
Come si può vedere per la maggior parte questi limiti attengono alle dimensioni dei dati interi,
che sono in genere fissati dall’architettura hardware (le analoghe informazioni per i dati in
virgola mobile sono definite a parte, ed accessibili includendo float.h). Lo standard prevede
anche un’altra costante, FOPEN_MAX, che può non essere fissa e che pertanto non è definita in
limits.h; essa deve essere definita in stdio.h ed avere un valore minimo di 8.
A questi valori lo standard ISO C90 ne aggiunge altri tre, relativi al tipo long long intro-
dotto con il nuovo standard, i relativi valori sono in tab. 8.2.
Tabella 8.2: Macro definite in limits.h in conformità allo standard ISO C90.
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 definite in gran parte dallo standard POSIX.1,
che tratta anche i limiti relativi alle caratteristiche dei file che vedremo in sez. 8.1.3.
Purtroppo la sezione dello standard che tratta questi argomenti è una delle meno chiare3 .
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 file, riportate in
tab. 8.7).
Lo standard dice che queste macro devono essere definite in limits.h quando i valori a cui
fanno riferimento sono fissi, e altrimenti devono essere lasciate indefinite, ed i loro valori dei
limiti devono essere accessibili solo attraverso sysconf. In realtà queste vengono sempre definite
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.
2
il valore può essere UCHAR_MAX o SCHAR_MAX a seconda che il sistema usi caratteri con segno o meno.
3
tanto che Stevens, in [1], la porta come esempio di “standardese”.
8.1. CAPACITÀ E CARATTERISTICHE DEL SISTEMA 227
molto elevati (come CHILD_MAX), e non è pertanto il caso di utilizzarli per allocare staticamente
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 definiscono i valori minimi le stesse caratteristiche
devono avere, perché una implementazione possa dichiararsi conforme allo standard; detti valori
sono riportati in tab. 8.4.
Tabella 8.4: Macro dei valori minimi delle caratteristiche generali del sistema per la conformità allo standard
POSIX.1.
In genere questi valori non servono a molto, la loro unica utilità è quella di indicare un limite
superiore che assicura la portabilità senza necessità di ulteriori controlli. Tuttavia molti di essi
sono ampiamente superati in tutti i sistemi POSIX in uso oggigiorno. Per questo è sempre meglio
utilizzare i valori ottenuti da sysconf.
Macro Significato
_POSIX_JOB_CONTROL Il sistema supporta il job control (vedi sez. 10.1).
_POSIX_SAVED_IDS Il sistema supporta gli identificatori del gruppo saved
(vedi sez. 3.3.1) per il controllo di accesso dei processi
_POSIX_VERSION Fornisce la versione dello standard POSIX.1 supportata
nel formato YYYYMML (ad esempio 199009L).
Tabella 8.5: Alcune macro definite in limits.h in conformità allo standard POSIX.1.
228 CAPITOLO 8. LA GESTIONE DEL SISTEMA, DEL TEMPO E DEGLI ERRORI
Oltre ai precedenti valori (e a quelli relativi ai file elencati in tab. 8.8), che devono essere
obbligatoriamente definiti, lo standard POSIX.1 ne prevede parecchi altri. La lista completa si
trova dall’header file bits/posix1_lim.h (da non usare mai direttamente, è incluso automatica-
mente all’interno di limits.h). Di questi vale la pena menzionare alcune macro di uso comune,
(riportate in tab. 8.5), che non indicano un valore specifico, ma denotano la presenza di alcune
funzionalità nel sistema (come il supporto del job control o degli identificatori del gruppo saved ).
Oltre allo standard POSIX.1, anche lo standard POSIX.2 definisce 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 nell’header file bits/posix2_lim.h, e alcuni
di loro sono descritti nella pagina di manuale di sysconf e nel manuale delle glibc.
#include <unistd.h>
long sysconf(int name)
Restituisce il valore del parametro di sistema name.
La funzione prende come argomento un intero che specifica quale dei limiti si vuole conoscere;
uno specchietto contenente i principali valori disponibili in Linux è riportato in tab. 8.6; l’elenco
completo è contenuto in bits/confname.h, ed una lista più esaustiva, con le relative spiegazioni,
si può trovare nel manuale delle glibc.
In generale ogni limite o caratteristica del sistema per cui è definita una macro, sia dagli
standard ANSI C e ISO C90, che da POSIX.1 e POSIX.2, può essere ottenuto attraverso una
chiamata a sysconf. Il valore si otterrà specificando come valore dell’argomento name il nome
ottenuto aggiungendo _SC_ ai nomi delle macro definite dai primi due, o sostituendolo a _POSIX_
per le macro definite dagli gli altri due.
In generale si dovrebbe fare uso di sysconf solo quando la relativa macro non è definita,
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 definite, indicando però un limite generico.
Per questo motivo è sempre meglio usare i valori restituiti da sysconf.
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.
Tabella 8.8: Costanti dei valori minimi delle caratteristiche dei file per la conformità allo standard POSIX.1.
230 CAPITOLO 8. LA GESTIONE DEL SISTEMA, DEL TEMPO E DEGLI ERRORI
Tutti questi limiti sono definiti in limits.h; come nel caso precedente il loro uso è di scarsa
utilità in quanto ampiamente superati in tutte le implementazioni moderne.
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 all’accesso a path).
E si noti come la funzione in questo caso richieda un argomento che specifichi a quale file si fa
riferimento, dato che il valore del limite cercato può variare a seconda del filesystem. Una seconda
versione della funzione, fpathconf, opera su un file descriptor invece che su un pathname. Il suo
prototipo è:
#include <unistd.h>
long fpathconf(int fd, int name)
Restituisce il valore del parametro name per il file fd.
È identica a pathconf solo che utilizza un file descriptor invece di un pathname; pertanto gli errori
restituiti cambiano di conseguenza.
La funzione ritorna 0 in caso di successo e -1 in caso di fallimento, nel qual caso errno assumerà
il valore EFAULT.
La funzione, che viene usata dal comando uname, restituisce le informazioni richieste nella
struttura info; anche questa struttura è definita in sys/utsname.h, secondo quanto mostrato
in fig. 8.1, e le informazioni memorizzate nei suoi membri indicano rispettivamente:
l’ultima informazione è stata aggiunta di recente e non è prevista dallo standard POSIX, essa è
accessibile, come mostrato in fig. 8.1, solo definendo _GNU_SOURCE.
In generale si tenga presente che le dimensioni delle stringhe di una struttura utsname non è
specificata, e che esse sono sempre terminate con NUL; il manuale delle glibc indica due diver-
se dimensioni, _UTSNAME_LENGTH per i campi standard e _UTSNAME_DOMAIN_LENGTH per quello
8.2. OPZIONI E CONFIGURAZIONE DEL SISTEMA 231
struct utsname {
char sysname [];
char nodename [];
char release [];
char version [];
char machine [];
# ifdef _GNU_SOURCE
char domainname [];
# endif
};
specifico per il nome di dominio; altri sistemi usano nomi diversi come SYS_NMLN o _SYS_NMLN
o UTSLEN che possono avere valori diversi.4
La funzione restituisce 0 in caso di successo e -1 in caso di errore, nel qual caso errno assumerà
uno dei valori:
EPERM non si ha il permesso di accedere ad uno dei componenti nel cammino specificato per
il parametro, o di accedere al parametro nella modalità scelta.
ENOTDIR non esiste un parametro corrispondente al nome name.
EINVAL o si è specificato un valore non valido per il parametro che si vuole impostare o lo
spazio provvisto per il ritorno di un valore non è delle giuste dimensioni.
ENOMEM talvolta viene usato più correttamente questo errore quando non si è specificato
sufficiente spazio per ricevere il valore di un parametro.
ed inoltre EFAULT.
4
nel caso di Linux uname corrisponde in realtà a 3 system call diverse, le prime due usano rispettivamente
delle lunghezze delle stringhe di 9 e 65 byte; la terza usa anch’essa 65 byte, ma restituisce anche l’ultimo campo,
domainname, con una lunghezza di 257 byte.
232 CAPITOLO 8. LA GESTIONE DEL SISTEMA, DEL TEMPO E DEGLI ERRORI
Come accennato in sez. 4.1.1 per poter accedere ai file occorre prima rendere disponibile al
sistema il filesystem su cui essi sono memorizzati; l’operazione di attivazione del filesystem è
chiamata montaggio, per far questo in Linux7 si usa la funzione mount il cui prototipo è:
#include <sys/mount.h>
mount(const char *source, const char *target, const char *filesystemtype,
unsigned long mountflags, const void *data)
Monta il filesystem 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 filesystem che possono essere restituiti in errno sono:
EPERM il processo non ha i privilegi di amministratore.
ENODEV filesystemtype non esiste o non è configurato nel kernel.
ENOTBLK non si è usato un block device per source quando era richiesto.
EBUSY source è già montato, o non può essere rimontato in read-only perché ci sono ancora
file aperti in scrittura, o target è ancora in uso.
EINVAL il device source presenta un superblock non valido, o si è cercato di rimontare un
filesystem non ancora montato, o di montarlo senza che target sia un mount point o
di spostarlo quando target non è un mount point o è /.
EACCES non si ha il permesso di accesso su uno dei componenti del pathname, o si è cercato
di montare un filesystem disponibile in sola lettura senza averlo specificato o il device
source è su un filesystem montato con l’opzione MS_NODEV.
ENXIO il major number del device source è sbagliato.
EMFILE la tabella dei device dummy è piena.
ed inoltre ENOTDIR, EFAULT, ENOMEM, ENAMETOOLONG, ENOENT o ELOOP.
La funzione monta sulla directory target, detta mount point, il filesystem contenuto in
source. In generale un filesystem è contenuto su un disco, e l’operazione di montaggio corri-
sponde a rendere visibile al sistema il contenuto del suddetto disco, identificato attraverso il file
di dispositivo ad esso associato.
Ma la struttura del virtual filesystem vista in sez. 4.2.1 è molto più flessibile e può essere
usata anche per oggetti diversi da un disco. Ad esempio usando il loop device si può montare
un file qualunque (come l’immagine di un CD-ROM o di un floppy) che contiene un filesystem,
inoltre alcuni filesystem, 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 filesystem è specificato da filesystemtype, che deve essere una delle stringhe
riportate nel file /proc/filesystems, che contiene l’elenco dei filesystem supportati dal kernel;
nel caso si sia indicato uno dei filesystem virtuali, il contenuto di source viene ignorato.
Dopo l’esecuzione della funzione il contenuto del filesystem viene resto disponibile nella direc-
tory specificata come mount point, il precedente contenuto di detta directory viene mascherato
dal contenuto della directory radice del filesystem montato.
Dal kernel 2.4.x inoltre è divenuto possibile sia spostare atomicamente un mount point da
una directory ad un’altra, sia montare in diversi mount point lo stesso filesystem, sia montare più
filesystem sullo stesso mount point (nel qual caso vale quanto appena detto, e solo il contenuto
dell’ultimo filesystem montato sarà visibile).
Ciascun filesystem è dotato di caratteristiche specifiche che possono essere attivate o meno,
alcune di queste sono generali (anche se non è detto siano disponibili in ogni filesystem), e
vengono specificate come opzioni di montaggio con l’argomento mountflags.
7
la funzione è specifica di Linux e non è portabile.
234 CAPITOLO 8. LA GESTIONE DEL SISTEMA, DEL TEMPO E DEGLI ERRORI
In Linux mountflags deve essere un intero a 32 bit i cui 16 più significativi sono un magic
number 8 mentre i 16 meno significativi sono usati per specificare 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.
Per l’impostazione delle caratteristiche particolari di ciascun filesystem si usa invece l’argo-
mento data che serve per passare le ulteriori informazioni necessarie, che ovviamente variano da
filesystem a filesystem.
La funzione mount può essere utilizzata anche per effettuare il rimontaggio di un filesystem,
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
di mountflags, MS_REMOUNT, che se impostato specifica che deve essere effettuato il rimontaggio
del filesystem (con le opzioni specificate dagli altri bit), anche in questo caso il valore di source
viene ignorato.
Una volta che non si voglia più utilizzare un certo filesystem è possibile smontarlo usando la
funzione umount, il cui prototipo è:
#include <sys/mount.h>
umount(const char *target)
Smonta il filesystem montato sulla directory target.
La funzione ritorna 0 in caso di successo e -1 in caso di fallimento, nel qual caso errno assumerà
uno dei valori:
EPERM il processo non ha i privilegi di amministratore.
EBUSY target è la directory di lavoro di qualche processo, o contiene dei file aperti, o un
altro mount point.
ed inoltre ENOTDIR, EFAULT, ENOMEM, ENAMETOOLONG, ENOENT o ELOOP.
la funzione prende il nome della directory su cui il filesystem è montato e non il file o il dispositivo
che è stato montato,9 in quanto con il kernel 2.4.x è possibile montare lo stesso dispositivo in più
punti. Nel caso più di un filesystem sia stato montato sullo stesso mount point viene smontato
quello che è stato montato per ultimo.
Si tenga presente che la funzione fallisce quando il filesystem è occupato, questo avviene
quando ci sono ancora file aperti sul filesystem, se questo contiene la directory di lavoro corrente
8
cioè un numero speciale usato come identificativo, che nel caso è 0xC0ED; si può usare la costante MS_MGC_MSK
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
essere usata anche specificando il file di dispositivo.
8.2. OPZIONI E CONFIGURAZIONE DEL SISTEMA 235
di un qualunque processo o il mount point di un altro filesystem; in questo caso l’errore restituito
è EBUSY.
Linux provvede inoltre una seconda funzione, umount2, che in alcuni casi permette di forzare
lo smontaggio di un filesystem, anche quando questo risulti occupato; il suo prototipo è:
#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ò
specificare se forzare lo smontaggio.
Il valore di flags è una maschera binaria, e al momento l’unico valore definito è il bit
MNT_FORCE; gli altri bit devono essere nulli. Specificando MNT_FORCE la funzione cercherà di
liberare il filesystem anche se è occupato per via di una delle condizioni descritte in precedenza.
A seconda del tipo di filesystem alcune (o tutte) possono essere superate, evitando l’errore di
EBUSY. In tutti i casi prima dello smontaggio viene eseguita una sincronizzazione dei dati.
Altre due funzioni specifiche di Linux,10 utili per ottenere in maniera diretta informazioni
riguardo al filesystem su cui si trova un certo file, 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 filesystem su cui è posto il file specificato.
Le funzioni ritornano 0 in caso di successo e -1 in caso di errore, nel qual caso errno assumerà
uno dei valori:
ENOSYS il filesystem su cui si trova il file specificato 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 filesy-
stem su cui si trova il file specificato; queste vengono restituite all’indirizzo buf di una struttura
statfs definita come in fig. 8.2, ed i campi che sono indefiniti per il filesystem in esame sono
impostati a zero. I valori del campo f_type sono definiti per i vari filesystem nei relativi file
di header dei sorgenti del kernel da costanti del tipo XXX_SUPER_MAGIC, dove XXX in genere è il
nome del filesystem stesso.
struct statfs {
long f_type ; /* tipo di filesystem */
long f_bsize ; /* dimensione ottimale dei blocchi di I / O */
long f_blocks ; /* blocchi totali nel filesystem */
long f_bfree ; /* blocchi liberi nel filesystem */
long f_bavail ; /* blocchi liberi agli utenti normali */
long f_files ; /* inode totali nel filesystem */
long f_ffree ; /* inode liberi nel filesystem */
fsid_t f_fsid ; /* filesystem id */
long f_namelen ; /* lunghezza massima dei nomi dei file */
long f_spare [6]; /* riservati per uso futuro */
};
Le glibc provvedono infine una serie di funzioni per la gestione dei due file /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.
236 CAPITOLO 8. LA GESTIONE DEL SISTEMA, DEL TEMPO E DEGLI ERRORI
#include <pwd.h>
#include <sys/types.h>
struct passwd *getpwuid(uid_t uid)
struct passwd *getpwnam(const char *name)
Restituiscono le informazioni relative all’utente specificato.
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 specificato.
11
in realtà oltre a questi nelle distribuzioni più recenti è stato introdotto il sistema delle shadow password che
prevede anche i due file /etc/shadow e /etc/gshadow, in cui sono state spostate le informazioni di autentica-
zione (ed inserite alcune estensioni) per toglierle dagli altri file che devono poter essere letti per poter effettuare
l’associazione fra username e uid.
12
nella quinta sezione, quella dei file di configurazione, occorre cioè usare man 5 passwd dato che altrimenti si
avrebbe la pagina di manuale del comando passwd.
13
il Pluggable Authentication Method è un sistema modulare, in cui è possibile utilizzare anche più meccanismi
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 l’interfaccia di PAM.
8.2. OPZIONI E CONFIGURAZIONE DEL SISTEMA 237
Le due funzioni forniscono le informazioni memorizzate nel registro degli utenti (che nelle
versioni più recenti possono essere ottenute attraverso PAM) relative all’utente specificato at-
traverso il suo uid o il nome di login. Entrambe le funzioni restituiscono un puntatore ad una
struttura di tipo passwd la cui definizione (anch’essa eseguita in pwd.h) è riportata in fig. 8.3,
dove è pure brevemente illustrato il significato dei vari campi.
struct passwd {
char * pw_name ; /* user name */
char * pw_passwd ; /* user password */
uid_t pw_uid ; /* user id */
gid_t pw_gid ; /* group id */
char * pw_gecos ; /* real name */
char * pw_dir ; /* home directory */
char * pw_shell ; /* 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
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 all’utente specificato.
Le funzioni ritornano 0 in caso di successo e un codice d’errore altrimenti, nel qual caso errno
sarà impostata opportunamente.
In questo caso l’uso è molto più complesso, in quanto bisogna prima allocare la memoria
necessaria a contenere le informazioni. In particolare i valori della struttura passwd saranno
restituiti all’indirizzo password mentre la memoria allocata all’indirizzo buffer, per un massimo
di buflen byte, sarà utilizzata per contenere le stringhe puntate dai campi di password. Infine
all’indirizzo puntato da result viene restituito il puntatore ai dati ottenuti, cioè buffer nel caso
l’utente esista, o NULL altrimenti. Qualora i dati non possano essere contenuti nei byte specificati
da buflen, la funzione fallirà restituendo ERANGE (e result sarà comunque impostato a NULL).
Del tutto analoghe alle precedenti sono le funzioni getgrnam e getgrgid (e le relative ana-
loghe 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 specificato.
Le funzioni ritornano 0 in caso di successo e un codice d’errore altrimenti, nel qual caso errno
sarà impostata opportunamente.
238 CAPITOLO 8. LA GESTIONE DEL SISTEMA, DEL TEMPO E DEGLI ERRORI
Il comportamento di tutte queste funzioni è assolutamente identico alle precedenti che leggono
le informazioni sugli utenti, l’unica differenza è che in questo caso le informazioni vengono
restituite in una struttura di tipo group, la cui definizione è riportata in fig. 8.4.
struct group {
char * gr_name ; /* group name */
char * gr_passwd ; /* group password */
gid_t gr_gid ; /* group id */
char ** gr_mem ; /* group members */
};
Figura 8.4: La struttura group contenente le informazioni relative ad un gruppo del sistema.
Le funzioni viste finora sono in grado di leggere le informazioni sia direttamente dal file 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
password.14 Dato che POSIX non prevede questa possibilità esiste un’altra interfaccia che lo
fa, derivata da SVID le cui funzioni sono riportate in tab. 8.10. Questa però funziona soltanto
quando le informazioni sono mantenute su un apposito file di registro di utenti e gruppi, con il
formato classico di /etc/passwd e /etc/group.
Funzione Significato
fgetpwent Legge una voce dal file di registro degli utenti specificato.
fgetpwent_r Come la precedente, ma rientrante.
putpwent Immette una voce in un file di registro degli utenti.
getpwent Legge una voce da /etc/passwd.
getpwent_r Come la precedente, ma rientrante.
setpwent Ritorna all’inizio di /etc/passwd.
endpwent Chiude /etc/passwd.
fgetgrent Legge una voce dal file di registro dei gruppi specificato.
fgetgrent_r Come la precedente, ma rientrante.
putgrent Immette una voce in un file di registro dei gruppi.
getgrent Legge una voce da /etc/group.
getgrent_r Come la precedente, ma rientrante.
setgrent Ritorna all’inizio di /etc/group.
endgrent Chiude /etc/group.
Tabella 8.10: Funzioni per la manipolazione dei campi di un file 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 modifiche rispetto al formato classico del file /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 specificando tutti i
contenuti necessari rispetto a questa estensione. Per questo motivo l’uso di queste funzioni è
deprecato, in quanto comunque non funzionale, pertanto ci limiteremo a fornire soltanto l’elenco
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
per i dettagli del funzionamento.
14
in realtà questo può essere fatto ricorrendo a PAM, ma questo è un altro discorso.
8.2. OPZIONI E CONFIGURAZIONE DEL SISTEMA 239
#include <utmp.h>
void utmpname(const char *file)
Specifica il file da usare come registro.
void setutent(void)
Apre il file del registro, posizionandosi al suo inizio.
void endutent(void)
Chiude il file del registro.
e si tenga presente che le funzioni non restituiscono nessun valore, pertanto non è possi-
bile accorgersi di eventuali errori (ad esempio se si è impostato un nome di file sbagliato con
utmpname).
Nel caso non si sia utilizzata utmpname per specificare un file di registro alternativo, sia
setutent che endutent operano usando il default che è /var/run/utmp. Il nome di questo
file, cosı̀ come una serie di altri valori di default per i pathname di uso più comune, viene
mantenuto nei valori di una serie di costanti definite includendo paths.h, in particolare quelle
che ci interessano sono:
_PATH_UTMP specifica il file che contiene il registro per gli utenti correntemente collegati; questo
è il valore che viene usato se non si è utilizzato utmpname per modificarlo.
_PATH_WTMP specifica il file che contiene il registro per l’archivio storico degli utenti collegati.
15
questa è la locazione specificata dal Linux Filesystem Hierarchy Standard, adottato dalla gran parte delle
distribuzioni.
16
non si confonda quest’ultimo con il simile /var/log/btmp dove invece vengono memorizzati dal programma
di login tutti tentativi di accesso fallito.
240 CAPITOLO 8. LA GESTIONE DEL SISTEMA, DEL TEMPO E DEGLI ERRORI
che nel caso di Linux hanno un valore corrispondente ai file /var/run/utmp e /var/log/wtmp
citati in precedenza.
Una volta aperto il file del registro degli utenti si può eseguire una scansione leggendo 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
specificata 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 definizione in
Linux è riportata in fig. 8.5. Le prime tre funzioni servono per leggere una voce dal registro;
getutent legge semplicemente la prima voce disponibile; le altre due permettono di eseguire
una ricerca.
struct utmp
{
short int ut_type ; /* Type of login . */
pid_t ut_pid ; /* Process ID of login process . */
char ut_line [ UT_LINESIZE ]; /* Devicename . */
char ut_id [4]; /* Inittab ID . */
char ut_user [ UT_NAMESIZE ]; /* Username . */
char ut_host [ UT_HOSTSIZE ]; /* Hostname for remote login . */
struct exit_status ut_exit ; /* Exit status of a process marked
as DEAD_PROCESS . */
long int ut_session ; /* Session ID , used for windowing . */
struct timeval ut_tv ; /* Time entry was made . */
int32_t ut_addr_v6 [4]; /* Internet address of remote host . */
char __unused [20]; /* Reserved for future use . */
};
Figura 8.5: La struttura utmp contenente le informazioni di una voce del registro di contabilità.
Con getutid si può cercare una voce specifica, a seconda del valore del campo ut_type
dell’argomento ut. Questo può assumere i valori riportati in tab. 8.11, quando assume i valori
RUN_LVL, BOOT_TIME, OLD_TIME, NEW_TIME, verrà restituito la prima voce che corrisponde al tipo
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 specificato
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 specifica il de-
vice17 di terminale che interessa. Lo stesso criterio di ricerca è usato da pututline per trovare
uno spazio dove inserire la voce specificata, qualora non sia trovata la voce viene aggiunta in
coda al registro.
17
espresso senza il /dev/ iniziale.
8.3. IL CONTROLLO DELL’USO DELLE RISORSE 241
Valore Significato
EMPTY Non contiene informazioni valide.
RUN_LVL Identica il runlevel del sistema.
BOOT_TIME Identifica il tempo di avvio del sistema.
OLD_TIME Identifica quando è stato modificato l’orologio di sistema.
NEW_TIME Identifica da quanto è stato modificato il sistema.
INIT_PROCESS Identifica un processo lanciato da init.
LOGIN_PROCESS Identifica un processo di login.
USER_PROCESS Identifica un processo utente.
DEAD_PROCESS Identifica un processo terminato.
Tabella 8.11: Classificazione 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 standar-
dizzate, e che in sistemi diversi possono esserci differenze; ad esempio pututline restituisce void
in vari sistemi (compreso Linux, fino 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
file) di tipo utmpx, che sono un sovrainsieme di utmp.
Le glibc utilizzano già una versione estesa di utmp, che rende inutili queste nuove strutture;
pertanto esse e le relative funzioni di gestione (getutxent, getutxid, getutxline, pututxline,
setutxent e endutxent) sono ridefinite come sinonimi delle funzioni appena viste.
Come visto in sez. 8.2.3, l’uso 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 pren-
dono due argomenti aggiuntivi. Le funzioni si comportano esattamente come le analoghe non
rientranti, solo che restituiscono il risultato all’indirizzo specificato dal primo argomento aggiun-
tivo (di tipo struct utmp *buffer) mentre il secondo (di tipo struct utmp **result) viene
usato per restituire il puntatore allo stesso buffer.
Infine 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 specificati.
La prima funzione permette l’aggiunta di una voce a wmtp specificando direttamente una
struttura utmp, mentre la seconda utilizza gli argomenti line, name e host per costruire la voce
che poi aggiunge chiamando updwtmp.
struct rusage {
struct timeval ru_utime ; /* user time used */
struct timeval ru_stime ; /* system time used */
long ru_maxrss ; /* maximum resident set size */
long ru_ixrss ; /* integral shared memory size */
long ru_idrss ; /* integral unshared data size */
long ru_isrss ; /* integral unshared stack size */
long ru_minflt ; /* page reclaims */
long ru_majflt ; /* page faults */
long ru_nswap ; /* swaps */
long ru_inblock ; /* block input operations */
long ru_oublock ; /* block output operations */
long ru_msgsnd ; /* messages sent */
long ru_msgrcv ; /* messages received */
long ru_nsignals ; ; /* signals received */
long ru_nvcsw ; /* voluntary context switches */
long ru_nivcsw ; /* involuntary context switches */
};
Figura 8.6: La struttura rusage per la lettura delle informazioni dei delle risorse usate da un processo.
La definizione della struttura in fig. 8.6 è ripresa da BSD 4.3,18 ma attualmente (con i kernel
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
nell’eseguire le istruzioni in user space, e quello impiegato dal kernel nelle system call eseguite
per conto del processo.
Gli altri tre campi servono a quantificare l’uso della memoria virtuale e corrispondono ri-
spettivamente 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
essere inserito nello swap.
In genere includere esplicitamente <sys/time.h> non è più strettamente necessario, ma
aumenta la portabilità, e serve comunque quando, come nella maggior parte dei casi, si debba
accedere ai campi di rusage relativi ai tempi di utilizzo del processore, che sono definiti come
strutture di tipo timeval (vedi fig. 5.7).
Questa è la stessa struttura utilizzata da wait4 (si ricordi quando visto in sez. 3.2.4) per
ricavare la quantità di risorse impiegate dal processo di cui si è letto lo stato di terminazione,
ma essa può anche essere letta direttamente utilizzando la funzione getrusage, il cui prototipo
è:
#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.
La funzione ritorna 0 in caso di successo e -1 in caso di errore, nel qual caso errno può essere
EINVAL o EFAULT.
L’argomento who permette di specificare il processo di cui si vuole leggere l’uso delle ri-
sorse; esso può assumere solo i due valori RUSAGE_SELF per indicare il processo corrente e
18
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
da ciascun processo in maniera molto più dettagliata.
8.3. IL CONTROLLO DELL’USO DELLE RISORSE 243
RUSAGE_CHILDREN per indicare l’insieme dei processi figli di cui si è ricevuto lo stato di ter-
minazione.
Valore Significato
RLIMIT_AS La dimensione massima della memoria virtuale di un processo, il cosiddetto Address
Space, (vedi sez. 2.2.1). Se il limite viene superato dall’uso 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.
RLIMIT_CORE La massima dimensione per di un file di core dump (vedi sez. 9.2.2) creato nella
terminazione di un processo; file di dimensioni maggiori verranno troncati a questo
valore, mentre con un valore si bloccherà la creazione dei core dump.
RLIMIT_CPU Il massimo tempo di CPU (vedi sez. 8.4.2) che il processo può usare. Il superamento del
limite corrente comporta l’emissione di un segnale di SIGXCPU, la cui azione predefinita
(vedi sez. 9.2) è terminare il processo, una volta al secondo fino al raggiungimento
del limite massimo. Il superamento del limite massimo comporta l’emissione di un
segnale di SIGKILL.19
RLIMIT_DATA 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
funzione di allocazione (brk o sbrk) con un errore di ENOMEM.
RLIMIT_FSIZE La massima dimensione di un file che un processo può creare. Se il processo cerca di
scrivere oltre questa dimensione riceverà un segnale di SIGXFSZ, che di norma termina
il processo; se questo viene intercettato la system call che ha causato l’errore fallirà
con un errore di EFBIG.
RLIMIT_LOCKS È un limite presente solo nelle prime versioni del kernel 2.4 sul numero massimo di
file lock (vedi sez. 12.1) che un processo poteva effettuare.
RLIMIT_MEMLOCK L’ammontare massimo di memoria che può essere bloccata in RAM da un processo
(vedi sez. 2.2.4). Dal kernel 2.6.9 questo limite comprende anche la memoria che può
essere bloccata da ciascun utente nell’uso della memoria condivisa (vedi sez. 11.2.6)
che viene contabilizzata separatamente ma sulla quale viene applicato questo stesso
limite.
RLIMIT_NOFILE Il numero massimo di file che il processo può aprire. L’apertura di un ulteriore file
farà fallire la funzione (open, dup o pipe) con un errore EMFILE.
RLIMIT_NPROC Il numero massimo di processi che possono essere creati sullo stesso user id real. Se il
limite viene raggiunto fork fallirà con un EAGAIN.
RLIMIT_SIGPENDING 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
per sigqueue, con kill si potrà sempre inviare un segnale che non sia già presente
su una coda.20
RLIMIT_STACK La massima dimensione dello stack del processo. Se il processo esegue operazioni che
estendano lo stack oltre questa dimensione riceverà un segnale di SIGSEGV.
RLIMIT_RSS L’ammontare massimo di pagine di memoria dato al testo del processo. Il limite è solo
una indicazione per il kernel, qualora ci fosse un surplus di memoria questa verrebbe
assegnata.
Tabella 8.12: Valori possibili dell’argomento resource delle funzioni getrlimit e setrlimit.
244 CAPITOLO 8. LA GESTIONE DEL SISTEMA, DEL TEMPO E DEGLI ERRORI
#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à
uno dei valori:
EINVAL i valori per resource non sono validi.
EPERM un processo senza i privilegi di amministratore ha cercato di innalzare i propri limiti.
ed EFAULT.
struct rlimit {
rlim_t rlim_cur ; /* Soft limit */
rlim_t rlim_max ; /* 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 specificare un limite, oltre a fornire dei valori specifici, si può anche usare la costante
RLIM_INFINITY che permette di sbloccare l’uso 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
limite massimo ed usare un valore qualsiasi per entrambi i limiti. Si tenga conto infine 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).
In genere tutto ciò è del tutto trasparente al singolo processo, ma in certi casi, come per
l’I/O mappato in memoria (vedi sez. 12.4.1) che usa lo stesso meccanismo per accedere ai file, è
necessario conoscere le dimensioni delle pagine usate dal kernel. Lo stesso vale quando si vuole
gestire in maniera ottimale l’interazione della memoria che si sta allocando con il meccanismo
della paginazione.
Di solito la dimensione delle pagine di memoria è fissata dall’architettura hardware, per cui
il suo valore di norma veniva mantenuto in una costante che bastava utilizzare in fase di compi-
lazione, 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.
Dato che si tratta di una caratteristica generale del sistema, questa dimensione può essere
ottenuta come tutte le altre attraverso una chiamata a sysconf, 24 ma in BSD 4.2 è stata
introdotta una apposita funzione, getpagesize, che restituisce la dimensione delle pagine di
memoria; il suo prototipo è:
#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
come obsoleta, mentre lo standard POSIX 1003.1-2001 la ha eliminata. In Linux è implementata
come una system call nelle architetture in cui essa è necessaria, ed in genere restituisce il valore
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 specifica 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.
Queste funzioni sono equivalenti all’uso della funzione sysconf rispettivamente con i pa-
rametri _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 effettivamente 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 comun-
que ottenibili attraverso sysconf utilizzando rispettivamente i parametri _SC_NPROCESSORS_CONF
e _SC_NPROCESSORS_ONLN.
Infine 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
eventuali nuovi processi. Il suo prototipo è:
#include <stdlib.h>
int getloadavg(double loadavg[], int nelem)
Legge il carico medio della macchina.
#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
dei valori:
EACCESS non si hanno i permessi per accedere a pathname.
EPERM il processo non ha privilegi sufficienti ad abilitare il BSD accounting.
ENOSYS il kernel non supporta il BSD accounting.
EUSER non sono disponibili nel kernel strutture per il file o si è finita la memoria.
ed inoltre EFAULT, EIO, ELOOP, ENAMETOOLONG, ENFILE, ENOENT, ENOMEM, ENOTDIR, EROFS.
La funzione attiva il salvataggio dei dati sul file 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. 5.4.4). Se si specifica il valore NULL per
filename il BSD accounting viene invece disabilitato. Un semplice esempio per l’uso di questa
funzione è riportato nel programma AcctCtrl.c dei sorgenti allegati alla guida.
Quando si attiva la contabilità, il file che si indica deve esistere; esso verrà aperto in sola
scrittura;27 le informazioni verranno registrate in append in coda al file tutte le volte che un pro-
cesso termina. Le informazioni vengono salvate in formato binario, e corrispondono al contenuto
della apposita struttura dati definita all’interno del kernel.
Il funzionamento di acct viene inoltre modificato da uno specifico parametro di sistema,
modificabile 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). Infine l’ultimo valore indica la
frequenza in secondi con cui deve essere controllata detta percentuale.
i vari tempi nelle differenti rappresentazioni che vengono utilizzate, a quelle della gestione di
data e ora.
In genere la somma di user time e system time indica il tempo di processore totale che il
sistema ha effettivamente 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
esegue un qualsiasi programma lanciando quest’ultimo come argomento del comando time.
#include <time.h>
clock_t clock(void)
Legge il valore corrente del tempo di CPU.
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.
Come accennato in sez. 8.4.1 il tempo di CPU è la somma di altri due tempi, l’user time ed
il system time che sono quelli effettivamente mantenuti dal kernel per ciascun processo. Questi
possono essere letti attraverso la funzione times, il cui prototipo è:
#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 dall’avvio 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 definizione è riportata in fig. 8.8. La struttura prevede quattro campi; i primi due,
tms_utime e tms_stime, sono l’user time ed il system time del processo, cosı̀ come definiti in
sez. 8.4.1.
struct tms {
clock_t tms_utime ; /* user time */
clock_t tms_stime ; /* system time */
clock_t tms_cutime ; /* user time of children */
clock_t tms_cstime ; /* system time of children */
};
Gli altri due campi mantengono rispettivamente la somma dell’user time ed del system time
di tutti i processi figli che sono terminati; il kernel cioè somma in tms_cutime il valore di
29
le glibc seguono lo standard ANSI C, POSIX richiede che CLOCKS_PER_SEC sia definito pari a 1000000
indipendentemente dalla risoluzione del timer di sistema.
8.4. LA GESTIONE DEI TEMPI DEL SISTEMA 249
tms_utime e tms_cutime per ciascun figlio del quale è stato ricevuto lo stato di terminazione, e
lo stesso vale per tms_cstime.
Si tenga conto che l’aggiornamento di tms_cutime e tms_cstime viene eseguito solo quando
una chiamata a wait o waitpid è ritornata. Per questo motivo se un processo figlio termina
prima di ricevere lo stato di terminazione di tutti i suoi figli, questi processi “nipoti” non verranno
considerati nel calcolo di questi tempi.
La funzione ritorna il valore del calendar time in caso di successo e -1 in caso di errore, che può
essere solo EFAULT.
dove t, se non nullo, deve essere l’indirizzo di una variabile su cui duplicare il valore di ritorno.
Analoga a time è la funzione stime che serve per effettuare l’operazione inversa, e cioè per
impostare il tempo di sistema qualora questo sia necessario; il suo prototipo è:
#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.
dato che modificare l’ora ha un impatto su tutto il sistema il cambiamento dell’orologio è una
operazione privilegiata e questa funzione può essere usata solo da un processo con i privilegi di
amministratore, altrimenti la chiamata fallirà con un errore di EPERM.
Data la scarsa precisione nell’uso di time_t (che ha una risoluzione massima di un secondo)
quando si devono effettuare operazioni sui tempi di norma l’uso delle funzioni precedenti è
sconsigliato, ed esse sono di solito sostituite da gettimeofday e settimeofday,31 i cui prototipi
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.
Si noti come queste funzioni utilizzino per indicare il tempo una struttura di tipo timeval,
la cui definizione si è già vista in fig. 5.7, questa infatti permette una espressione alternativa dei
valori del calendar time, con una precisione, rispetto a time_t, fino al microsecondo.32
30
in realtà il kernel usa una rappresentazione interna di che fornisce una precisione molto maggiore, e consente
per questo anche di usare rappresentazioni diverse del calendar time.
31
le due funzioni time e stime sono più antiche e derivano da SVr4, gettimeofday e settimeofday sono state
introdotte da BSD, ed in BSD4.3 sono indicate come sostitute delle precedenti.
32
la precisione è solo teorica, la precisione reale della misura del tempo dell’orologio di sistema non dipende
dall’uso di queste strutture.
250 CAPITOLO 8. LA GESTIONE DEL SISTEMA, DEL TEMPO E DEGLI ERRORI
Come nel caso di stime anche settimeofday (la cosa continua a valere per qualunque funzio-
ne che vada a modificare l’orologio di sistema, quindi anche per quelle che tratteremo in seguito)
può essere utilizzata solo da un processo coi privilegi di amministratore.33
Il secondo argomento di entrambe le funzioni è una struttura timezone, che storicamente
veniva utilizzata per specificare appunto la time zone, cioè l’insieme del fuso orario e delle
convenzioni per l’ora legale che permettevano il passaggio dal tempo universale all’ora locale.
Questo argomento oggi è obsoleto ed in Linux non è mai stato utilizzato; esso non è supportato
né dalle vecchie libc5, né dalle glibc: pertanto quando si chiama questa funzione deve essere
sempre impostato a NULL.
Modificare l’orologio di sistema con queste funzioni è comunque problematico, in quanto
esse effettuano un cambiamento immediato. Questo può creare dei buchi o delle ripetizioni
nello scorrere dell’orologio di sistema, con conseguenze indesiderate. Ad esempio se si porta
avanti l’orologio si possono perdere delle esecuzioni di cron programmate nell’intervallo che si
è saltato. Oppure se si porta indietro l’orologio si possono eseguire due volte delle operazioni
previste nell’intervallo di tempo che viene ripetuto.
Per questo motivo la modalità più corretta per impostare l’ora è quella di usare la funzione
adjtime, il cui prototipo è:
#include <sys/time.h>
int adjtime(const struct timeval *delta, struct timeval *olddelta)
Aggiusta del valore delta l’orologio di sistema.
La funzione restituisce 0 in caso di successo e -1 in caso di errore, nel qual caso errno assumerà
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 l’orologio; se è positivo l’orologio sarà accelerato per un certo tempo in modo da
guadagnare il tempo richiesto, altrimenti sarà rallentato. Il secondo argomento viene usato, se
non nullo, per ricevere il valore dell’ultimo aggiustamento effettuato.
struct timex {
unsigned int modes ; /* mode selector */
long int offset ; /* time offset ( usec ) */
long int freq ; /* frequency offset ( scaled ppm ) */
long int maxerror ; /* maximum error ( usec ) */
long int esterror ; /* estimated error ( usec ) */
int status ; /* clock command / status */
long int constant ; /* pll time constant */
long int precision ; /* clock precision ( usec ) ( read only ) */
long int tolerance ; /* clock frequency tolerance ( ppm ) ( read only ) */
struct timeval time ; /* ( read only ) */
long int tick ; /* ( modified ) usecs between clock ticks */
long int ppsfreq ; /* pps frequency ( scaled ppm ) ( ro ) */
long int jitter ; /* pps jitter ( us ) ( ro ) */
int shift ; /* interval duration ( s ) ( shift ) ( ro ) */
long int stabil ; /* pps stability ( scaled ppm ) ( ro ) */
long int jitcnt ; /* jitter limit exceeded ( ro ) */
long int calcnt ; /* calibration intervals ( ro ) */
long int errcnt ; /* calibration errors ( ro ) */
long int stbcnt ; /* stability limit exceeded ( ro ) */
};
33
più precisamente la capabitity CAP_SYS_TIME.
8.4. LA GESTIONE DEI TEMPI DEL SISTEMA 251
Linux poi prevede un’altra funzione, che consente un aggiustamento molto più dettagliato del
tempo, permettendo ad esempio anche di modificare anche la velocità dell’orologio di sistema.
La funzione è adjtimex ed il suo prototipo è:
#include <sys/timex.h>
int adjtimex(struct timex *buf)
Aggiusta del valore delta l’orologio di sistema.
La funzione restituisce lo stato dell’orologio (un valore > 0) in caso di successo e -1 in caso di
errore, nel qual caso errno assumerà i valori EFAULT, EINVAL ed EPERM.
La funzione richiede una struttura di tipo timex, la cui definizione, cosı̀ come effettuata
in sys/timex.h, è riportata in fig. 8.9. L’azione della funzione dipende dal valore del campo
mode, che specifica quale parametro dell’orologio di sistema, specificato 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 specificati come OR binario delle costanti riportate in tab. 8.13.
La funzione utilizza il meccanismo di David L. Mills, descritto nell’RFC 1305, che è alla base
del protocollo NTP. La funzione è specifica di Linux e non deve essere usata se la portabilità è un
requisito, le glibc provvedono anche un suo omonimo ntp_adjtime. La trattazione completa di
questa funzione necessita di una lettura approfondita del meccanismo descritto nell’RFC 1305,
ci limitiamo a descrivere in tab. 8.13 i principali valori utilizzabili per il campo mode, un elenco
più dettagliato del significato dei vari campi della struttura timex può essere ritrovato in [5].
Nome Valore Significato
ADJ_OFFSET 0x0001 Imposta la differenza fra il tempo reale e l’orologio di sistema:
deve essere indicata in microsecondi nel campo offset di
timex.
ADJ_FREQUENCY 0x0002 Imposta la differenze in frequenza fra il tempo reale e l’oro-
logio di sistema: deve essere indicata in parti per milione nel
campo frequency di timex.
ADJ_MAXERROR 0x0004 Imposta il valore massimo dell’errore sul tempo, espresso in
microsecondi nel campo maxerror di timex.
ADJ_ESTERROR 0x0008 Imposta la stima dell’errore sul tempo, espresso in
microsecondi nel campo esterror di timex.
ADJ_STATUS 0x0010 Imposta alcuni valori di stato interni usati dal sistema nella
gestione dell’orologio specificati nel campo status di timex.
ADJ_TIMECONST 0x0020 Imposta la larghezza di banda del PLL implementato dal
kernel, specificato nel campo constant di timex.
ADJ_TICK 0x4000 Imposta il valore dei tick del timer in microsecondi, espresso
nel campo tick di timex.
ADJ_OFFSET_SINGLESHOT 0x8001 Imposta uno spostamento una tantum dell’orologio secondo
il valore del campo offset simulando il comportamento di
adjtime.
Tabella 8.13: Costanti per l’assegnazione del valore del campo mode della struttura timex.
Il valore delle costanti per mode può essere anche espresso, secondo la sintassi specificata per
la forma equivalente di questa funzione definita come ntp_adjtime, utilizzando il prefisso MOD
al posto di ADJ.
La funzione ritorna un valore positivo che esprime lo stato dell’orologio di sistema; questo
può assumere i valori riportati in tab. 8.14. Un valore di -1 viene usato per riportare un errore;
al solito se si cercherà di modificare l’orologio di sistema (specificando un mode diverso da zero)
senza avere i privilegi di amministratore si otterrà un errore di EPERM.
non sono molto intuitive quando si deve esprimere un’ora o una data. Per questo motivo è stata
introdotta una ulteriore rappresentazione, detta broken-down time, che permette appunto di
suddividere il calendar time usuale in ore, minuti, secondi, ecc.
Questo viene effettuato attraverso una opportuna struttura tm, la cui definizione è riportata
in fig. 8.10, ed è in genere questa struttura che si utilizza quando si deve specificare un tempo
a partire dai dati naturali (ora e data), dato che essa consente anche di trattare la gestione del
fuso orario e dell’ora legale.34
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 l’ora 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 nell’ora
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.
struct tm {
int tm_sec ; /* seconds */
int tm_min ; /* minutes */
int tm_hour ; /* hours */
int tm_mday ; /* day of the month */
int tm_mon ; /* month */
int tm_year ; /* year */
int tm_wday ; /* day of the week */
int tm_yday ; /* day in the year */
int tm_isdst ; /* daylight saving time */
long int tm_gmtoff ; /* Seconds east of UTC . */
const char * tm_zone ; /* Timezone abbreviation . */
};
Figura 8.10: La struttura tm per una rappresentazione del tempo in termini di ora, minuti, secondi, ecc.
34
in realtà i due campi tm_gmtoff e tm_zone sono estensioni previste da BSD e dalle glibc, che, quando è definita
_BSD_SOURCE, hanno la forma in fig. 8.10.
8.4. LA GESTIONE DEI TEMPI DEL SISTEMA 253
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:
"Wed Jun 30 21:49:08 1993\n"
e impostano anche la variabile tzname con l’informazione della time zone corrente; ctime è
banalmente definita in termini di asctime come asctime(localtime(t). Dato che l’uso 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
char *buf, in cui l’utente deve specificare il buffer 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 effettua la conversione usando il tempo coordinato
universale (UTC), cioè l’ora di Greenwich; mentre localtime usa l’ora locale; mktime esegue la
conversione inversa.
Anche in questo caso le prime due funzioni restituiscono l’indirizzo di una struttura allocata
staticamente, per questo sono state definite 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.
Come mostrato in fig. 8.10 il broken-down time permette di tenere conto anche della differenza
fra tempo universale e ora locale, compresa l’eventuale ora legale. Questo viene fatto attraverso
le tre variabili globali mostrate in fig. 8.11, cui si accede quando si include time.h. Queste va-
riabili vengono impostate quando si chiama una delle precedenti funzioni di conversione, oppure
invocando direttamente la funzione tzset, il cui prototipo è:
#include <sys/timex.h>
void tzset(void)
Imposta le variabili globali della time zone.
La funzione non ritorna niente e non dà errori.
La funzione inizializza le variabili di fig. 8.11 a partire dal valore della variabile di ambiente
TZ, se quest’ultima non è definita verrà usato il file /etc/localtime.
Figura 8.11: Le variabili globali usate per la gestione delle time zone.
La variabile tzname contiene due stringhe, che indicano i due nomi standard della time zone
corrente. La prima è il nome per l’ora solare, la seconda per l’ora legale.35 La variabile timezone
indica la differenza di fuso orario in secondi, mentre daylight indica se è attiva o meno l’ora
legale.
Benché la funzione asctime fornisca la modalità più immediata per stampare un tempo o
una data, la flessibilità non fa parte delle sue caratteristiche; quando si vuole poter stampare
solo una parte (l’ora, o il giorno) di un tempo si può ricorrere alla più sofisticata strftime, il
cui prototipo è:
#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.
35
anche se sono indicati come char * non è il caso di modificare queste stringhe.
254 CAPITOLO 8. LA GESTIONE DEL SISTEMA, DEL TEMPO E DEGLI ERRORI
Tabella 8.15: Valori previsti dallo standard ANSI C per modificatore della stringa di formato di strftime.
Il risultato della funzione è controllato dalla stringa di formato format, tutti i caratteri
restano invariati eccetto % che viene utilizzato come modificatore; alcuni36 dei possibili valori
che esso può assumere sono riportati in tab. 8.15. La funzione tiene conto anche della presenza
di una localizzazione per stampare in maniera adeguata i vari nomi.
In genere le funzioni di libreria usano un valore speciale per indicare che c’è stato un errore.
Di solito questo valore è -1 o un puntatore nullo o la costante EOF (a seconda della funzione);
ma questo valore segnala solo che c’è stato un errore, non il tipo di errore.
Per riportare il tipo di errore il sistema usa la variabile globale errno,37 definita nell’hea-
der errno.h; la variabile è in genere definita come volatile dato che può essere cambiata in
modo asincrono da un segnale (si veda sez. 9.3.6 per un esempio, ricordando quanto trattato
in sez. 3.6.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.
I valori che può assumere errno sono riportati in app. C, nell’header errno.h sono anche
definiti i nomi simbolici per le costanti numeriche che identificano 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 l’opzione -l.
Il valore di errno viene sempre impostato a zero all’avvio di un programma, gran parte delle
funzioni di libreria impostano errno ad un valore diverso da zero in caso di errore. Il valore è
invece indefinito in caso di successo, perché anche se una funzione ha successo, può chiamarne
altre al suo interno che falliscono, modificando cosı̀ errno.
Pertanto un valore non nullo di errno non è sintomo di errore (potrebbe essere il risultato di
un errore precedente) e non lo si può usare per determinare quando o se una chiamata a funzione
è fallita. La procedura da seguire è sempre quella di controllare errno immediatamente dopo
aver verificato il fallimento della funzione attraverso il suo codice di ritorno.
#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 l’indirizzo del messaggio in caso di successo e NULL in caso di errore; nel
qual caso errno assumerà i valori:
EINVAL si è specificato un valore di errnum non valido.
ERANGE la lunghezza di buf è insufficiente a contenere la stringa di errore.
La funzione è analoga a strerror ma restituisce la stringa di errore nel buffer buf che il
singolo thread deve allocare autonomamente per evitare i problemi connessi alla condivisione del
buffer statico. Il messaggio è copiato fino alla dimensione massima del buffer, specificata dal-
l’argomento 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 è:
#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 all’ultimo errore avvenuto. La stringa specificata con
message viene stampato prima del messaggio d’errore, seguita dai due punti e da uno spazio, il
messaggio è terminato con un a capo.
Il messaggio può essere riportato anche usando le due variabili globali:
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, l’utilizzo di questa stringa è
sostanzialmente equivalente a quello di strerror.
In fig. 8.12 è riportata la sezione attinente del codice del programma errcode, che può essere
usato per stampare i messaggi di errore e le costanti usate per identificare i singoli errori; il
8.5. LA GESTIONE DEGLI ERRORI 257
sorgente completo del programma è allegato nel file ErrCode.c e contiene pure la gestione delle
opzioni e tutte le definizioni necessarie ad associare il valore numerico alla costante simbolica.
In particolare si è riportata la sezione che converte la stringa passata come argomento in un
intero (1-2), controllando con i valori di ritorno di strtol che la conversione sia avvenuta
correttamente (4-10), e poi stampa, a seconda dell’opzione scelta il messaggio di errore (11-14)
o la macro (15-17) associate a quel codice.
La funzione fa parte delle estensioni GNU per la gestione degli errori, l’argomento format
prende la stessa sintassi di printf, ed i relativi argomenti devono essere forniti allo stesso modo,
mentre errnum indica l’errore 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
da format e dagli argomenti seguenti, seguita da due punti ed uno spazio infine il messaggio di
errore relativo ad errnum, il tutto è terminato da un a capo.
Il comportamento della funzione può essere ulteriormente controllato se si definisce una
variabile error_print_progname come puntatore ad una funzione void che restituisce void che
si incarichi di stampare il nome del programma.
L’argomento status può essere usato per terminare direttamente il programma in caso di
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 un’altra
variabile globale, error_message_count, che tiene conto di quanti errori ci sono stati.
Un’altra funzione per la stampa degli errori, ancora più sofisticata, che prende due argomenti
aggiuntivi per indicare linea e file su cui è avvenuto l’errore è error_at_line; il suo prototipo
è:
#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.
ed il suo comportamento è identico a quello di error se non per il fatto che, separati con il
solito due punti-spazio, vengono inseriti un nome di file indicato da fname ed un numero di
linea subito dopo la stampa del nome del programma. Inoltre essa usa un’altra 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.
Nella loro versione originale essi portano con sé nessuna informazione che non sia il loro tipo; si
tratta in sostanza di un’interruzione 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 elemen-
tare di comunicazione fra processi (ad esempio vengono usati per il controllo di sessione), per
notificare eventi (come la terminazione di un processo figlio), 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 affrontarne la
classificazione a secondo di uso e modalità di generazione fino ad esaminare in dettaglio le
funzioni e le metodologie di gestione avanzate e le estensioni fatte all’interfaccia classica nelle
nuovi versioni dello standard POSIX.
9.1 Introduzione
In questa sezione esamineremo i concetti generali relativi ai segnali, vedremo le loro caratteri-
stiche di base, introdurremo le nozioni di fondo relative all’architettura del funzionamento dei
segnali e alle modalità con cui il sistema gestisce l’interazione fra di essi ed i processi.
• 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 figlio;
• la scadenza di un timer o di un allarme;
• il tentativo di effettuare un’operazione di input/output che non può essere eseguita;
• una richiesta dell’utente di terminare o fermare il programma. In genere si realizza attra-
verso un segnale mandato dalla shell in corrispondenza della pressione di tasti del terminale
come C-c o C-z;1
• l’esecuzione 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).
259
260 CAPITOLO 9. I SEGNALI
Ciascuno di questi eventi (compresi gli ultimi due che pure sono controllati dall’utente o da
un altro processo) comporta l’intervento 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 ese-
guita una azione predefinita o una apposita funzione di gestione (quello che da qui in avanti
chiameremo il gestore del segnale, dall’inglese signal handler ) che può essere stata specificata
dall’utente (nel qual caso si dice che si intercetta il segnale).
Questa è la ragione per cui l’implementazione dei segnali secondo questa semantica viene
chiamata inaffidabile; infatti la ricezione del segnale e la reinstallazione del suo gestore non sono
operazioni atomiche, e sono sempre possibili delle race condition (sull’argomento vedi quanto
detto in sez. 3.6).
Un altro problema è che in questa semantica non esiste un modo per bloccare i segnali quando
non si vuole che arrivino; i processi possono ignorare il segnale, ma non è possibile istruire il
sistema a non fare nulla in occasione di un segnale, pur mantenendo memoria del fatto che è
avvenuto.
Nella semantica affidabile (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 261
i segnali vengono generati dal kernel per un processo all’occorrenza dell’evento che causa il
segnale. In genere questo viene fatto dal kernel impostando l’apposito campo della task_struct
del processo nella process table (si veda fig. 3.2).
Si dice che il segnale viene consegnato al processo (dall’inglese delivered ) quando viene ese-
guita l’azione 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 effettuata
dallo scheduler quando, riprendendo l’esecuzione del processo in questione, verifica 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
caso, se l’azione per il suddetto segnale non è quella di ignorarlo, il segnale resta pendente fintanto
che il processo non lo sblocca (nel qual caso viene consegnato) o imposta l’azione corrispondente
per ignorarlo.
Si tenga presente che il kernel stabilisce cosa fare con un segnale che è stato bloccato al
momento della consegna, non quando viene generato; questo consente di cambiare l’azione per il
segnale prima che esso venga consegnato, e si può usare la funzione sigpending (vedi sez. 9.4.4)
per determinare quali segnali sono bloccati e quali sono pendenti.
il segnale diventa pendente (o pending), e rimane tale fino al momento in cui verrà notificato al
processo (o verrà specificata come azione quella di ignorarlo).
Normalmente l’invio al processo che deve ricevere il segnale è immediato ed avviene non
appena questo viene rimesso in esecuzione dallo scheduler che esegue l’azione specificata. Questo
a meno che il segnale in questione non sia stato bloccato prima della notifica, nel qual caso
l’invio non avviene ed il segnale resta pendente indefinitamente. Quando lo si sblocca il segnale
pendente sarà subito notificato. Si tenga presente però che i segnali pendenti non si accodano,
alla generazione infatti il kernel marca un flag nella task_struct del processo, per cui se prima
della notifica ne vengono generati altri il flag è comunque marcato, ed il gestore viene eseguito
sempre una sola volta.
Si ricordi però che se l’azione specificata per un segnale è quella di essere ignorato questo sarà
scartato immediatamente al momento della sua generazione, e questo anche se in quel momento
il segnale è bloccato (perché bloccare su un segnale significa bloccarne la notifica). Per questo
motivo un segnale, fintanto che viene ignorato, non sarà mai notificato, anche se prima è stato
bloccato ed in seguito si è specificata una azione diversa (nel qual caso solo i segnali successivi
alla nuova specificazione saranno notificati).
Una volta che un segnale viene notificato (che questo avvenga subito o dopo una attesa più
o meno lunga) viene eseguita l’azione specificata per il segnale. Per alcuni segnali (SIGKILL e
SIGSTOP) questa azione è fissa e non può essere cambiata, ma per tutti gli altri si può selezionare
una delle tre possibilità seguenti:
• ignorare il segnale;
• catturare il segnale, ed utilizzare il gestore specificato;
• accettare l’azione predefinita per quel segnale.
Un programma può specificare queste scelte usando le due funzioni signal e sigaction
(vedi sez. 9.3.2 e sez. 9.4.3). Se si è installato un gestore sarà quest’ultimo ad essere eseguito alla
notifica del segnale. Inoltre il sistema farà si che mentre viene eseguito il gestore di un segnale,
quest’ultimo venga automaticamente bloccato (cosı̀ si possono evitare race condition).
Nel caso non sia stata specificata un’azione, viene utilizzata l’azione standard che (come
vedremo in sez. 9.2.1) è propria di ciascun segnale; nella maggior parte dei casi essa porta alla
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
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
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 file di core dump che registra lo stato del processo
(ed in particolare della memoria e dello stack ) prima della terminazione. Questo può essere
esaminato in seguito con un debugger per investigare sulla causa dell’errore. Lo stesso avviene
se i suddetti segnali vengono generati con una kill.
del sistema, e nel caso di Linux, anche a seconda dell’architettura hardware. Per questo motivo
ad ogni segnale viene associato un nome, definendo 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 definiti nell’header di sistema signal.h.
Il numero totale di segnali presenti è dato dalla macro NSIG, e dato che i numeri dei segnali
sono allocati progressivamente, essa corrisponde anche al successivo del valore numerico asse-
gnato all’ultimo segnale definito. In tab. 9.3 si è riportato l’elenco completo dei segnali definiti
in Linux (estratto dalle pagine di manuale), comparati con quelli definiti in vari standard.
Sigla Significato
A L’azione predefinita è terminare il processo.
B L’azione predefinita è ignorare il segnale.
C L’azione predefinita è terminare il processo e scrivere un
core dump.
D L’azione predefinita è fermare il processo.
E Il segnale non può essere intercettato.
F Il segnale non può essere ignorato.
Tabella 9.1: Legenda delle azioni predefinite dei segnali riportate in tab. 9.3.
In tab. 9.3 si sono anche riportate le azioni predefinite di ciascun segnale (riassunte con delle
lettere, la cui legenda completa è in tab. 9.1), quando nessun gestore è installato un segnale può
essere ignorato o causare la terminazione del processo. Nella colonna standard sono stati indicati
anche gli standard in cui ciascun segnale è definito, secondo lo schema di tab. 9.2.
Sigla Standard
P POSIX
B BSD
L Linux
S 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 file (posto nella
directory corrente del processo e chiamato core) su cui viene salvata un’immagine della memoria
del processo (il cosiddetto core dump), che può essere usata da un debugger per esaminare lo
stato dello stack e delle variabili al momento della ricezione del segnale.
La descrizione dettagliata del significato dei vari segnali, raggruppati per tipologia, verrà
affrontata nei paragrafi successivi.
core dump che viene scritto in un file core nella directory corrente del processo al momento
dell’errore, che il debugger può usare per ricostruire lo stato del programma al momento della
terminazione. Questi segnali sono:
SIGFPE Riporta un errore aritmetico fatale. Benché il nome derivi da floating point exception
si applica a tutti gli errori aritmetici compresa la divisione per zero e l’overflow.
Se il gestore ritorna il comportamento del processo è indefinito, ed ignorare questo
segnale può condurre ad un ciclo infinito.
SIGILL Il nome deriva da illegal instruction, significa 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
il file eseguibile è corrotto o si stanno cercando di eseguire dei dati. Quest’ultimo
caso può accadere quando si passa un puntatore sbagliato al posto di un puntatore
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 overflow dello stack
9.2. LA CLASSIFICAZIONE DEI SEGNALI 265
SIGHUP Il nome sta per hang-up. Segnala che il terminale dell’utente si è disconnesso (ad
esempio perché si è interrotta la rete). Viene usato anche per riportare la termina-
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 file di configurazione.
SIGALRM Il nome sta per alarm. Segnale la scadenza di un timer misurato sul tempo reale o
sull’orologio di sistema. È normalmente usato dalla funzione alarm.
SIVGTALRM Il nome sta per virtual alarm. È analogo al precedente ma segnala la scadenza di
un timer sul tempo di CPU usato dal processo.
SIGPROF Il nome sta per profiling. 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 quest’ultimo. In genere viene usato dagli strumenti che servono a fare la
profilazione dell’utilizzo del tempo di CPU da parte del processo.
SIGIO Questo segnale viene inviato quando un file descriptor è pronto per eseguire del-
l’input/output. In molti sistemi solo i socket e i terminali possono generare questo
segnale, in Linux questo può essere usato anche per i file, posto che la fcntl abbia
avuto successo.
SIGURG Questo segnale è inviato quando arrivano dei dati urgenti o out-of-band su di un
socket; per maggiori dettagli al proposito si veda sez. 19.1.3.
SIGPOLL Questo segnale è equivalente a SIGIO, è definito solo per compatibilità con i sistemi
System V.
9.2. LA CLASSIFICAZIONE DEI SEGNALI 267
SIGCHLD Questo è il segnale mandato al processo padre quando un figlio termina o viene
fermato. L’azione predefinita è di ignorare il segnale, la sua gestione è trattata in
sez. 3.2.4.
SIGCLD Per Linux questo è solo un segnale identico al precedente, il nome è obsoleto e
andrebbe evitato.
SIGCONT Il nome sta per continue. Il segnale viene usato per fare ripartire un programma
precedentemente fermato da SIGSTOP. Questo segnale ha un comportamento spe-
ciale, e fa sempre ripartire il processo prima della sua consegna. Il comportamento
predefinito è di fare solo questo; il segnale non può essere bloccato. Si può anche
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
quanto esso è completamente trasparente rispetto all’esecuzione che riparte senza
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 Il segnale ferma un processo (lo porta cioè in uno stato di sleep, vedi sez. 3.4.1); il
segnale non può essere né intercettato, né ignorato, né bloccato.
SIGTSTP 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
di SIGSTOP può essere intercettato e ignorato. In genere un programma installa un
gestore per questo segnale quando vuole lasciare il sistema o il terminale in uno
stato definito prima di fermarsi; se per esempio un programma ha disabilitato l’eco
sul terminale può installare un gestore per riabilitarlo prima di fermarsi.
SIGTTIN Un processo non può leggere dal terminale se esegue una sessione di lavoro in
background. Quando un processo in background tenta di leggere da un terminale
viene inviato questo segnale a tutti i processi della sessione di lavoro. L’azione
predefinita è di fermare il processo. L’argomento è trattato in sez. 10.1.1.
SIGPIPE Sta per Broken pipe. Se si usano delle pipe, (o delle FIFO o dei socket) è necessario,
prima che un processo inizi a scrivere su una di esse, che un altro l’abbia aperta
in lettura (si veda sez. 11.1.1). Se il processo in lettura non è partito o è termina-
to inavvertitamente alla scrittura sulla pipe il kernel genera questo segnale. Se il
268 CAPITOLO 9. I SEGNALI
SIGLOST Sta per Resource lost. Tradizionalmente è il segnale che viene generato quando si
perde un advisory lock su un file su NFS perché il server NFS è stato riavviato. Il
progetto GNU lo utilizza per indicare ad un client il crollo inaspettato di un server.
In Linux è definito come sinonimo di SIGIO.2
SIGXCPU Sta per CPU time limit exceeded. Questo segnale è generato quando un processo
eccede il limite impostato per il tempo di CPU disponibile, vedi sez. 8.3.2.
SIGXFSZ Sta per File size limit exceeded. Questo segnale è generato quando un processo
tenta di estendere un file oltre le dimensioni specificate dal limite impostato per le
dimensioni massime di un file, vedi sez. 8.3.2.
SIGUSR2 È il secondo segnale a disposizione degli utenti. Vedi quanto appena detto per
SIGUSR1.
SIGWINCH Il nome sta per window (size) change e viene generato in molti sistemi (GNU/Li-
nux compreso) quando le dimensioni (in righe e colonne) di un terminale vengono
cambiate. Viene usato da alcuni programmi testuali per riformattare l’uscita su
schermo quando si cambia dimensione a quest’ultimo. L’azione predefinita è di
essere ignorato.
SIGINFO Il segnale indica una richiesta di informazioni. È 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.
dato che la stringa è allocata staticamente non se ne deve modificare il contenuto, che resta
valido solo fino alla successiva chiamata di strsignal. Nel caso si debba mantenere traccia del
messaggio sarà necessario copiarlo.
2
ed è segnalato come BUG nella pagina di manuale.
9.3. LA GESTIONE DI BASE DEI SEGNALI 269
La seconda funzione, psignal, deriva da BSD ed è analoga alla funzione perror descritta
sempre in sez. 8.5.2; il suo prototipo è:
#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.
perché questo renderebbe impossibile una risposta pronta al segnale, per cui il gestore viene
eseguito prima che la system call sia ritornata. Un elenco dei casi in cui si presenta questa
situazione è il seguente:
• la lettura da file che possono bloccarsi in attesa di dati non ancora presenti (come per certi
file di dispositivo, i socket o le pipe);
• la scrittura sugli stessi file, nel caso in cui dati non possano essere accettati immediatamente
(di nuovo comune per i socket);
• l’apertura di un file di dispositivo che richiede operazioni non immediate per una risposta
(ad esempio l’apertura di un nastro che deve essere riavvolto);
• le operazioni eseguite con ioctl che non è detto possano essere eseguite immediatamente;
• le funzioni di intercomunicazione che si bloccano in attesa di risposte da altri processi;
• la funzione pause (usata appunto per attendere l’arrivo di un segnale);
• la funzione wait (se nessun processo figlio è ancora terminato).
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 l’errore
di EINTR. Questa è a tutt’oggi una scelta corrente, ma comporta che i programmi che usano
dei gestori controllino lo stato di uscita delle funzioni che eseguono una system call lenta per
ripeterne la chiamata qualora l’errore fosse questo.
Dimenticarsi di richiamare una system call interrotta da un segnale è un errore comune,
tanto che le glibc provvedono una macro TEMP_FAILURE_RETRY(expr) che esegue l’operazione
automaticamente, ripetendo l’esecuzione dell’espressione expr fintanto che il risultato non è
diverso dall’uscita con un errore EINTR.
La soluzione è comunque poco elegante e BSD ha scelto un approccio molto diverso, che
è quello di fare ripartire automaticamente una system call interrotta invece di farla fallire. In
questo caso ovviamente non c’è bisogno di preoccuparsi di controllare il codice di errore; si perde
però la possibilità di eseguire azioni specifiche all’occorrenza di questa particolare condizione.
Linux e le glibc consentono di utilizzare entrambi gli approcci, attraverso una opportuna
opzione di sigaction (vedi sez. 9.4.3). È 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.
#include <signal.h>
sighandler_t signal(int signum, sighandler_t handler)
Installa la funzione di gestione handler (il gestore) per il segnale signum.
In questa definizione si è usato un tipo di dato, sighandler_t, che è una estensione GNU,
definita 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 è definita come:
3
in realtà in alcune vecchie implementazioni (SVr4 e 4.3+BSD in particolare) vengono usati alcuni argomenti
aggiuntivi per definire il comportamento della funzione, vedremo in sez. 9.4.3 che questo è possibile usando la
funzione sigaction.
9.3. LA GESTIONE DI BASE DEI SEGNALI 271
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ò
dedurre la definizione di sighandler_t che è:
typedef void (* sighandler_t )( int )
e cioè un puntatore ad una funzione void (cioè senza valore di ritorno) e che prende un argo-
mento di tipo int.4 La funzione signal quindi restituisce e prende come secondo argomento un
puntatore a una funzione di questo tipo, che è appunto la funzione che verrà usata come gestore
del segnale.
Il numero di segnale passato nell’argomento signum può essere indicato direttamente con
una delle costanti definite in sez. 9.2.1. L’argomento handler che indica il gestore invece, oltre
all’indirizzo della funzione da chiamare all’occorrenza del segnale, può assumere anche i due
valori costanti SIG_IGN e SIG_DFL; il primo indica che il segnale deve essere ignorato,5 mentre
il secondo ripristina l’azione predefinita.6
La funzione restituisce l’indirizzo dell’azione precedente, che può essere salvato per poterlo
ripristinare (con un’altra 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 predefinita è di
essere ignorato), tutti i segnali pendenti saranno scartati, e non verranno mai notificati.
L’uso di signal è soggetto a problemi di compatibilità, dato che essa si comporta in maniera
diversa per sistemi derivati da BSD o da System V. In questi ultimi infatti la funzione è conforme
al comportamento originale dei primi Unix in cui il gestore viene disinstallato alla sua chiamata,
secondo la semantica inaffidabile; anche Linux seguiva questa convenzione con le vecchie librerie
del C come le libc4 e le libc5.7
Al contrario BSD segue la semantica affidabile, non disinstallando il gestore e bloccando il
segnale durante l’esecuzione dello stesso. Con l’utilizzo delle glibc dalla versione 2 anche Linux
è passato a questo comportamento. Il comportamento della versione originale della funzione, il
cui uso è deprecato per i motivi visti in sez. 9.1.2, può essere ottenuto chiamando sysv_signal,
una volta che si sia definita la macro _XOPEN_SOURCE. In generale, per evitare questi problemi,
l’uso di signal, che tra l’altro ha un comportamento indefinito in caso di processo multi-thread,
è da evitare; tutti i nuovi programmi dovrebbero usare sigaction.
È 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) è indefinito. Un gestore che ritorna da questi segnali può dare luogo
ad un ciclo infinito.
La prima funzione è raise, che è definita dallo standard ANSI C, e serve per inviare un
segnale al processo corrente,8 il suo prototipo è:
#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
qualora si sia specificato un numero di segnale invalido.
Il valore di sig specifica il segnale che si vuole inviare e può essere specificato con una delle
macro definite in sez. 9.2. In genere questa funzione viene usata per riprodurre il comportamento
predefinito di un segnale che sia stato intercettato. In questo caso, una volta eseguite le operazioni
volute, il gestore dovrà prima reinstallare l’azione predefinita, per poi attivarla chiamando raise.
Mentre raise è una funzione di libreria, quando si vuole inviare un segnale generico ad
un processo occorre utilizzare la apposita system call, questa può essere chiamata attraverso la
funzione kill, il cui prototipo è:
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig)
Invia il segnale sig al processo specificato con pid.
La funzione restituisce 0 in caso di successo e −1 in caso di errore nel qual caso errno assumerà
uno dei valori:
EINVAL il segnale specificato non esiste.
ESRCH il processo selezionato non esiste.
EPERM non si hanno privilegi sufficienti ad inviare il segnale.
Lo standard POSIX prevede che il valore 0 per sig sia usato per specificare 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
necessari ed un errore ESRCH se il processo specificato non esiste. Si tenga conto però che il
sistema ricicla i pid (come accennato in sez. 3.2.1) per cui l’esistenza di un processo non significa
che esso sia realmente quello a cui si intendeva mandare il segnale.
Il valore dell’argomento pid specifica il processo (o i processi) di destinazione a cui il segnale
deve essere inviato e può assumere i valori riportati in tab. 9.4.
Si noti pertanto che la funzione raise(sig) può essere definita in termini di kill, ed è
sostanzialmente equivalente ad una kill(getpid(), sig). Siccome raise, che è definita nello
standard ISO C, non esiste in alcune vecchie versioni di Unix, in generale l’uso di kill finisce
per essere più portabile.
Una seconda funzione che può essere definita in termini di kill è killpg, che è sostanzial-
mente equivalente a kill(-pidgrp, signal); il suo prototipo è:
#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.
Valore Significato
>0 Il segnale è mandato al processo con il pid indicato.
0 Il segnale è mandato ad ogni processo del process group del chiamante.
−1 Il segnale è mandato ad ogni processo (eccetto init).
< −1 Il segnale è mandato ad ogni processo del process group |pid|.
o all’user-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
al processo 1 (cioè a init) segnali per i quali esso non abbia un gestore installato.
Infine, seguendo le specifiche POSIX 1003.1-2001, l’uso della chiamata kill(-1, sig) com-
porta 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 esclude-
re alcuni processi specifici: nel caso in questione Linux non invia il segnale al processo che ha
effettuato la chiamata.
#include <unistd.h>
unsigned int alarm(unsigned int seconds)
Predispone l’invio di SIGALRM dopo seconds secondi.
• un real-time timer che calcola il tempo reale trascorso (che corrisponde al clock time). La
scadenza di questo timer provoca l’emissione di SIGALRM;
• un virtual timer che calcola il tempo di processore usato dal processo in user space (che
corrisponde all’user time). La scadenza di questo timer provoca l’emissione di SIGVTALRM;
• un profiling 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
quello che in sez. 8.4.1 abbiamo chiamato CPU time). La scadenza di questo timer provoca
l’emissione di SIGPROF.
274 CAPITOLO 9. I SEGNALI
Il timer usato da alarm è il clock time, e corrisponde cioè al tempo reale. La funzione come
abbiamo visto è molto semplice, ma proprio per questo presenta numerosi limiti: non consente di
usare gli altri timer, non può specificare intervalli di tempo con precisione maggiore del secondo
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 l’invio di segnali periodici, al costo però di una maggiore complessità d’uso
e di una minore portabilità. Il suo prototipo è:
#include <sys/time.h>
int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue)
Predispone l’invio di un segnale di allarme alla scadenza dell’intervallo value sul timer
specificato da which.
La funzione restituisce 0 in caso di successo e −1 in caso di errore, nel qual caso errno assumerà
uno dei valori EINVAL o EFAULT.
Il valore di which permette di specificare quale dei tre timer illustrati in precedenza usare; i
possibili valori sono riportati in tab. 9.5.
Valore Timer
ITIMER_REAL real-time timer
ITIMER_VIRTUAL virtual timer
ITIMER_PROF profiling timer
Il valore della struttura specificata 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
attraverso una struttura itimerval, definita in fig. 5.5.
La struttura è composta da due membri, il primo, it_interval definisce il periodo del timer;
il secondo, it_value il tempo mancante alla scadenza. Entrambi esprimono i tempi tramite una
struttura timeval che permette una precisione fino al microsecondo.
Ciascun timer decrementa il valore di it_value fino 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
it_interval è nullo il timer si ferma.
struct itimerval
{
struct timeval it_interval ; /* next value */
struct timeval it_value ; /* current value */
};
Figura 9.2: La struttura itimerval, che definisce i valori dei timer di sistema.
effettuato per eccesso).11 L’uso del contatore dei jiffies, un intero a 32 bit, comportava inoltre
l’impossibilità di specificare tempi molto lunghi.12 Con il cambiamento della rappresentazione
effettuato nel kernel 2.6.16 questo problema è scomparso e con l’introduzione dei timer ad alta
risoluzione (vedi sez. 9.5.2) nel kernel 2.6.21 la precisione è diventata quella fornita dall’hardware
disponibile.
Una seconda causa di potenziali ritardi è che il segnale viene generato alla scadenza del
timer, ma poi deve essere consegnato al processo; se quest’ultimo è attivo (questo è sempre vero
per ITIMER_VIRT) la consegna è immediata, altrimenti può esserci un ulteriore ritardo che può
variare a seconda del carico del sistema.
Questo ha una conseguenza che può indurre ad errori molto subdoli, si tenga conto poi che
in caso di sistema molto carico, si può avere il caso patologico in cui un timer scade prima che il
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. Per questo oggi l’uso di questa
funzione è deprecato a favore dei POSIX timer che tratteremo in sez. 9.5.2.
Dato che sia alarm che setitimer non consentono di leggere il valore corrente di un timer
senza modificarlo, è possibile usare la funzione getitimer, il cui prototipo è:
#include <sys/time.h>
int getitimer(int which, struct itimerval *value)
Legge in value il valore del timer specificato da which.
La funzione restituisce 0 in caso di successo e −1 in caso di errore e restituisce gli stessi errori di
getitimer.
11
questo in realtà non è del tutto vero a causa di un bug, presente fino al kernel 2.6.12, che in certe circostanze
causava l’emissione del segnale con un arrotondamento per difetto.
12
superiori al valore della costante MAX_SEC_IN_JIFFIES, pari, nel caso di default di un valore di HZ di 250, a
circa 99 giorni e mezzo.
276 CAPITOLO 9. I SEGNALI
La differenza fra questa funzione e l’uso di raise è che anche se il segnale è bloccato o
ignorato, la funzione ha effetto lo stesso. Il segnale può però essere intercettato per effettuare
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 diret-
tamente dal gestore sia la stessa abort a farlo al ritorno dello stesso. Inoltre, sempre seguendo
lo standard POSIX, prima della terminazione tutti i file aperti e gli stream saranno chiusi ed i
buffer scaricati su disco. Non verranno invece eseguite le eventuali funzioni registrate con atexit
e on_exit.
La funzione ritorna solo dopo che un segnale è stato ricevuto ed il relativo gestore è ritornato, nel
qual caso restituisce −1 e errno assumerà il valore EINTR.
La funzione segnala sempre una condizione di errore (il successo sarebbe quello di aspettare
indefinitamente). In genere si usa questa funzione quando si vuole mettere un processo in attesa
di un qualche evento specifico che non è sotto il suo diretto controllo (ad esempio la si può
usare per interrompere l’esecuzione del processo fino all’arrivo di un segnale inviato da un altro
processo).
Quando invece si vuole fare attendere un processo per un intervallo di tempo già noto nello
standard POSIX.1 viene definita la funzione sleep, il cui prototipo è:
#include <unistd.h>
unsigned int sleep(unsigned int seconds)
Pone il processo in stato di sleep per seconds secondi.
La funzione restituisce zero se l’attesa viene completata, o il numero di secondi restanti se viene
interrotta da un segnale.
La funzione attende per il tempo specificato, 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
la riattivazione del processo può avvenire in un qualunque momento, ma il valore restituito
sarà sempre arrotondato al secondo, con la conseguenza che, se la successione dei segnali è
particolarmente sfortunata e le differenze si accumulano, si potranno avere ritardi anche di
parecchi secondi. In genere la scelta più sicura è quella di stabilire un termine per l’attesa, e
ricalcolare tutte le volte il numero di secondi da aspettare.
In alcune implementazioni inoltre l’uso di sleep può avere conflitti con quello di SIGALRM,
dato che la funzione può essere realizzata con l’uso di pause e alarm (in maniera analoga
all’esempio che vedremo in sez. 9.4.1). In tal caso mescolare chiamata di alarm e sleep o
modificare l’azione di SIGALRM, può causare risultati indefiniti. Nel caso delle glibc è stata usata
una implementazione completamente indipendente e questi problemi non ci sono.
13
si tratta in sostanza di funzioni che permettono di portare esplicitamente il processo in stato di sleep, vedi
sez. 3.4.1.
9.3. LA GESTIONE DI BASE DEI SEGNALI 277
La granularità di sleep permette di specificare attese soltanto in secondi, per questo sia sotto
BSD4.3 che in SUSv2 è stata definita la funzione usleep (dove la u è intesa come sostituzione
di µ); i due standard hanno delle definizioni diverse, ma le glibc seguono14 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 l’attesa viene completata, o −1 in caso di errore, nel qual caso
errno assumerà il valore EINTR.
Anche questa funzione, a seconda delle implementazioni, può presentare problemi nell’inte-
razione con alarm e SIGALRM. È pertanto deprecata in favore della funzione nanosleep, definita
dallo standard POSIX1.b, il cui prototipo è:
#include <unistd.h>
int nanosleep(const struct timespec *req, struct timespec *rem)
Pone il processo in stato di sleep per il tempo specificato da req. In caso di interruzione
restituisce il tempo restante in rem.
La funzione restituisce zero se l’attesa viene completata, o −1 in caso di errore, nel qual caso
errno assumerà uno dei valori:
EINVAL si è specificato un numero di secondi negativo o un numero di nanosecondi maggiore
di 999.999.999.
EINTR la funzione è stata interrotta da un segnale.
Lo standard richiede che la funzione sia implementata in maniera del tutto indipendente
da alarm15 e sia utilizzabile senza interferenze con l’uso di SIGALRM. La funzione prende come
argomenti delle strutture di tipo timespec, la cui definizione è riportata in fig. 5.8, che permette
di specificare un tempo con una precisione fino al nanosecondo.
La funzione risolve anche il problema di proseguire l’attesa dopo l’interruzione dovuta ad un
segnale; infatti in tal caso in rem viene restituito il tempo rimanente rispetto a quanto richiesto
inizialmente,16 e basta richiamare la funzione per completare l’attesa.17
Chiaramente, anche se il tempo può essere specificato con risoluzioni fino al nanosecondo, la
precisione di nanosleep è determinata dalla risoluzione temporale del timer di sistema. Perciò
la funzione attenderà comunque il tempo specificato, ma prima che il processo possa tornare ad
essere eseguito occorrerà almeno attendere la successiva interruzione del timer di sistema, cioè
un tempo che a seconda dei casi può arrivare fino a 1/HZ, (sempre che il sistema sia scarico ed
il processa venga immediatamente rimesso in esecuzione); per questo motivo il valore restituito
in rem è sempre arrotondato al multiplo successivo di 1/HZ.
Con i kernel della serie 2.4 in realtà era possibile ottenere anche pause più precise del cen-
tesimo di secondo usando politiche di scheduling real-time come SCHED_FIFO o SCHED_RR; in tal
caso infatti il calcolo sul numero di interruzioni del timer veniva evitato utilizzando direttamen-
te un ciclo di attesa con cui si raggiungevano pause fino ai 2 ms con precisioni del µs. Questa
estensione è stata rimossa con i kernel della serie 2.6, che consentono una risoluzione più alta
del timer di sistema; inoltre a partire dal kernel 2.6.21, nanosleep può avvalersi del supporto
dei timer ad alta risoluzione, ottenendo la massima precisione disponibile sull’hardware della
propria macchina.
14
secondo la pagina di manuale almeno dalla versione 2.2.2.
15
nel caso di Linux questo è fatto utilizzando direttamente il timer del kernel.
16
con l’eccezione, valida solo nei kernel della serie 2.4, in cui, per i processi riavviati dopo essere stati fermati
da un segnale, il tempo passato in stato T non viene considerato nel calcolo della rimanenza.
17
anche qui però occorre tenere presente che i tempi sono arrotondati, per cui la precisione, per quanto migliore
di quella ottenibile con sleep, è relativa e in caso di molte interruzioni si può avere una deriva, per questo esiste
la funzione clock_nanosleep (vedi sez. 9.5.2) che permette di specificare un tempo assoluto anziché un tempo
relativo.
278 CAPITOLO 9. I SEGNALI
Figura 9.4: Codice di una funzione generica di gestione per il segnale SIGCHLD.
Il codice del gestore è di lettura immediata; come buona norma di programmazione (si
ricordi quanto accennato sez. 8.5.1) si comincia (6-7) con il salvare lo stato corrente di errno, in
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,
cosa che viene eseguita nel ciclo in (9-15). Il ciclo è necessario a causa di una caratteristica
fondamentale della gestione dei segnali: abbiamo già accennato come fra la generazione di un
segnale e l’esecuzione 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
figli terminino in rapida successione. Esso inoltre si presenta tutte le volte che un segnale viene
18
in realtà in SVr4 eredita la semantica di System V, in cui il segnale si chiama SIGCLD e viene trattato in
maniera speciale; in System V infatti se si imposta esplicitamente l’azione 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). L’azione
predefinita è sempre quella di ignorare il segnale, ma non attiva questo comportamento. Linux, come BSD e
POSIX, non supporta questa semantica ed usa il nome di SIGCLD come sinonimo di SIGCHLD.
9.4. LA GESTIONE AVANZATA DEI SEGNALI 279
bloccato: per quanti siano i segnali emessi durante il periodo di blocco, una volta che quest’ultimo
sarà rimosso verrà recapitato un solo segnale.
Allora, nel caso della terminazione dei processi figli, 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 indefinito.
Per questo occorre ripetere la chiamata di waitpid fino 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.
Dato che è nostra intenzione utilizzare SIGALRM il primo passo della nostra implementazione
sarà quello di installare il relativo gestore salvando il precedente (14-17). Si effettuerà poi una
chiamata ad alarm per specificare il tempo d’attesa per l’invio del segnale a cui segue la chiamata
a pause per fermare il programma (18-20) fino alla sua ricezione. Al ritorno di pause, causato
dal ritorno del gestore (1-9), si ripristina il gestore originario (21-22) restituendo l’eventuale
tempo rimanente (23-24) che potrà essere diverso da zero qualora l’interruzione di pause venisse
causata da un altro segnale.
Questo codice però, a parte il non gestire il caso in cui si è avuta una precedente chiamata
a alarm (che si è tralasciato per brevità), presenta una pericolosa race condition. Infatti, se
il processo viene interrotto fra la chiamata di alarm e pause, può capitare (ad esempio se
il sistema è molto carico) che il tempo di attesa scada prima dell’esecuzione di quest’ultima,
cosicché essa sarebbe eseguita dopo l’arrivo di SIGALRM. In questo caso ci si troverebbe di fronte
ad un deadlock, in quanto pause non verrebbe mai più interrotta (se non in caso di un altro
segnale).
Questo problema può essere risolto (ed è la modalità con cui veniva fatto in SVr2) usando
la funzione longjmp (vedi sez. 2.4.4) per uscire dal gestore; in questo modo, con una condizione
sullo stato di uscita di quest’ultima, si può evitare la chiamata a pause, usando un codice del
tipo di quello riportato in fig. 9.6.
In questo caso il gestore (18-27) non ritorna come in fig. 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.
Ma anche questa implementazione comporta dei problemi; in questo caso infatti non viene
gestita correttamente l’interazione con gli altri segnali; se infatti il segnale di allarme interrompe
un altro gestore, l’esecuzione non riprenderà nel gestore in questione, ma nel ciclo principale,
9.4. LA GESTIONE AVANZATA DEI SEGNALI 281
1 sig_atomic_t flag ;
2 int main ()
3 {
4 flag = 0;
5 ...
6 if ( flag ) { /* test if signal occurred */
7 flag = 0; /* reset flag */
8 do_response (); /* do things */
9 } else {
10 do_other (); /* do other things */
11 }
12 ...
13 }
14 void alarm_hand ( int sig )
15 {
16 /* set the flag */
17 flag = 1;
18 return ;
19 }
Figura 9.7: Un esempio non funzionante del codice per il controllo di un evento generato da un segnale.
Questo è il tipico esempio di caso, già citato in sez. 3.6.2, in cui si genera una race condition;
infatti, in una situazione in cui un segnale è già arrivato (e flag è già ad 1) se un altro segnale
arriva immediatamente dopo l’esecuzione del controllo (6) ma prima della cancellazione del flag
(7), la sua occorrenza sarà perduta.
Questi esempi ci mostrano che per una gestione effettiva dei segnali occorrono delle funzioni
più sofisticate di quelle finora illustrate, queste hanno la loro origine nella semplice interfaccia
dei primi sistemi Unix, ma con esse non è possibile gestire in maniera adeguata di tutti i possibili
aspetti con cui un processo deve reagire alla ricezione di un segnale.
Come evidenziato nel paragrafo precedente, le funzioni di gestione dei segnali originarie, nate con
la semantica inaffidabile, hanno dei limiti non superabili; in particolare non è prevista nessuna
funzione che permetta di gestire il blocco dei segnali o di verificare 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
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
282 CAPITOLO 9. I SEGNALI
pari al numero di bit dell’architettura della macchina,19 ciascun bit del quale è associato ad uno
specifico segnale; in questo modo è di solito possibile implementare le operazioni direttamente
con istruzioni elementari del processore. Lo standard POSIX.1 definisce 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).
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 all’insieme di segnali set.
int sigdelset(sigset_t *set, int signum)
Toglie il segnale signum dall’insieme di segnali set.
int sigismember(const sigset_t *set, int signum)
Controlla se il segnale signum è nell’insieme di segnali set.
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
errore possibile è che signum non sia un segnale valido).
Dato che in generale non si può fare conto sulle caratteristiche di una implementazione (non
è detto che si disponga di un numero di bit sufficienti per mettere tutti i segnali in un intero,
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 specificare 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 definiti 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. Infine sigismember permette di verificare la presenza di uno specifico segnale
in un insieme.
#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à
i valori:
EINVAL si è specificato un numero di segnale invalido o si è cercato di installare il gestore per
SIGKILL o SIGSTOP.
EFAULT si sono specificati 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 specificare le varie
19
nel caso dei PC questo comporta un massimo di 32 segnali distinti: dato che in Linux questi sono sufficienti
non c’è necessità di nessuna struttura più complicata.
9.4. LA GESTIONE AVANZATA DEI SEGNALI 283
caratteristiche della risposta al segnale, non solo la funzione che verrà eseguita alla sua occor-
renza. Per questo lo standard raccomanda di usare sempre questa funzione al posto di signal
(che in genere viene definita 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à d’uso.
Se il puntatore act non è nullo, la funzione installa la nuova azione da esso specificata,
se oldact non è nullo il valore dell’azione corrente viene restituito indietro. Questo permette
(specificando act nullo e oldact non nullo) di superare uno dei limiti di signal, che non consente
di ottenere l’azione corrente senza installarne una nuova.
Entrambi i puntatori fanno riferimento alla struttura sigaction, tramite la quale si specifica-
no tutte le caratteristiche dell’azione associata ad un segnale. Anch’essa è descritta dallo standard
POSIX.1 ed in Linux è definita secondo quanto riportato in fig. 9.8. Il campo sa_restorer, non
previsto dallo standard, è obsoleto e non deve essere più usato.
struct sigaction
{
void (* sa_handler )( int );
void (* sa_sigaction )( int , siginfo_t * , void *);
sigset_t sa_mask ;
int sa_flags ;
void (* sa_restorer )( void );
}
Il campo sa_mask serve ad indicare l’insieme dei segnali che devono essere bloccati durante
l’esecuzione del gestore, ad essi viene comunque sempre aggiunto il segnale che ne ha causato la
chiamata, a meno che non si sia specificato 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 l’invocazione.
L’uso di questo campo permette ad esempio di risolvere il problema residuo dell’implemen-
tazione di sleep mostrata in fig. 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
valore di sa_flag permette di specificare vari aspetti del comportamento di sigaction, e della
reazione del processo ai vari segnali; i valori possibili ed il relativo significato sono riportati in
tab. 9.6.
Come si può notare in fig. 9.8 sigaction permette di utilizzare due forme diverse di gestore,21
da specificare, a seconda dell’uso o meno del flag SA_SIGINFO, rispettivamente attraverso i campi
sa_sigaction o sa_handler,22 Quest’ultima è quella classica usata anche con signal, mentre
la prima permette di usare un gestore più complesso, in grado di ricevere informazioni più
dettagliate dal sistema, attraverso la struttura siginfo_t, riportata in fig. 9.9.
Installando un gestore di tipo sa_sigaction diventa allora possibile accedere alle informa-
zioni 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
da zero, il codice dell’errore associato al segnale, e si_code, che viene usato dal kernel per
specificare maggiori dettagli riguardo l’evento che ha causato l’emissione del segnale.
20
questa funzionalità è stata introdotta nel kernel 2.6 e va a modificare il comportamento di waitpid.
21
la possibilità è prevista dallo standard POSIX.1b, ed è stata aggiunta nei kernel della serie 2.1.x con l’intro-
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.
22
i due campi devono essere usati in maniera alternativa, in certe implementazioni questi campi vengono
284 CAPITOLO 9. I SEGNALI
Valore Significato
SA_NOCLDSTOP Se il segnale è SIGCHLD allora non deve essere notificato
quando il processo figlio viene fermato da uno dei segnali
SIGSTOP, SIGTSTP, SIGTTIN o SIGTTOU.
SA_RESETHAND Ristabilisce l’azione per il segnale al valore predefinito
una volta che il gestore è stato lanciato, riproduce cioè il
comportamento della semantica inaffidabile.
SA_ONESHOT Nome obsoleto, sinonimo non standard di SA_RESETHAND;
da evitare.
SA_ONSTACK Stabilisce l’uso di uno stack alternativo per l’esecuzione
del gestore (vedi sez. 9.5.3).
SA_RESTART Riavvia automaticamente le slow system call quando ven-
gono interrotte dal suddetto segnale; riproduce cioè il
comportamento standard di BSD.
SA_NODEFER Evita che il segnale corrente sia bloccato durante
l’esecuzione del gestore.
SA_NOMASK Nome obsoleto, sinonimo non standard di SA_NODEFER.
SA_SIGINFO Deve essere specificato quando si vuole usare un ge-
store in forma estesa usando sa_sigaction al posto di
sa_handler.
SA_NOCLDWAIT Se il segnale è SIGCHLD allora i processi figli non diventano
zombie quando terminano.20
siginfo_t {
int si_signo ; /* Signal number */
int si_errno ; /* An errno value */
int si_code ; /* Signal code */
int si_trapno ; /* Trap number that caused hardware - generated
signal ( unused on most architectures ) */
pid_t si_pid ; /* Sending process ID */
uid_t si_uid ; /* Real user ID of sending process */
int si_status ; /* Exit value or signal */
clock_t si_utime ; /* User time consumed */
clock_t si_stime ; /* System time consumed */
sigval_t si_value ; /* Signal value */
int si_int ; /* POSIX .1 b signal */
void * si_ptr ; /* POSIX .1 b signal */
int si_overrun ; /* Timer overrun count ; POSIX .1 b timers */
int si_timerid ; /* Timer ID ; POSIX .1 b timers */
void * si_addr ; /* Memory location which caused fault */
long si_band ; /* Band event ( was int before glibc 2.3.2) */
int si_fd ; /* File descriptor */
}
In generale si_code contiene, per i segnali generici, per quelli real-time e per tutti quelli
inviati tramite da un processo con kill o affini, le informazioni circa l’origine del segnale stesso,
ad esempio se generato dal kernel, da un timer, da kill, ecc. Il valore viene sempre espresso
come una costante,23 ed i valori possibili in questo caso sono riportati in tab. 9.7.
Nel caso di alcuni segnali però il valore di si_code viene usato per fornire una informazione
specifica relativa alle motivazioni della ricezione dello stesso; ad esempio i vari segnali di errore
(SIGILL, SIGFPE, SIGSEGV e SIGBUS) lo usano per fornire maggiori dettagli riguardo l’errore,
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 specifiche.
Valore Significato
SI_USER generato da kill o raise.
SI_KERNEL inviato dal kernel.
SI_QUEUE inviato con sigqueue (vedi sez. 9.5.1).
SI_TIMER scadenza di un POSIX timer (vedi sez. 9.5.2).
SI_MESGQ inviato al cambiamento di stato di una coda di messaggi
POSIX (vedi sez. 11.4.2).24
SI_ASYNCIO una operazione di I/O asincrono (vedi sez. 12.3) è stata
completata.
SI_SIGIO segnale di SIGIO da una coda (vedi sez. 12.3.1).
SI_TKILL inviato da tkill o tgkill (vedi sez. ??).25
Tabella 9.7: Valori del campo si_code della struttura sigaction per i segnali generici.
In questo caso il valore del campo si_code deve essere verificato nei confronti delle diverse
costanti previste per ciascuno di detti segnali;26 l’elenco dettagliato dei nomi di queste costanti
è riportato nelle diverse sezioni di tab. 9.8 che sono state ordinate nella sequenza in cui si sono
appena citati i rispettivi segnali.27
Il resto della struttura siginfo_t è definito come union ed i valori eventualmente presenti
dipendono 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,
SIGCHLD avvalora anche i campi si_status, si_utime e si_stime che indicano rispettivamente
lo stato di uscita, l’user time e il system time (vedi sez. 8.4.2) usati dal processo; SIGILL,
SIGFPE, SIGSEGV e SIGBUS avvalorano si_addr con l’indirizzo in cui è avvenuto l’errore, SIGIO
(vedi sez. 12.3.3) avvalora si_fd con il numero del file descriptor e si_band per i dati urgenti
(vedi sez. 19.1.3) su un socket, il segnale inviato alla scadenza di un timer POSIX (vedi sez. 9.5.2)
avvalora i campi si_timerid e si_overrun.
Benché sia possibile usare nello stesso programma sia sigaction che signal occorre molta
attenzione, in quanto le due funzioni possono interagire in maniera anomala. Infatti l’azione
specificata con sigaction contiene un maggior numero di informazioni rispetto al semplice
indirizzo del gestore restituito da signal. Per questo motivo se si usa quest’ultima per installare
un gestore sostituendone uno precedentemente installato con sigaction, non sarà possibile
effettuare un ripristino corretto dello stesso.
Per questo è sempre opportuno usare sigaction, che è in grado di ripristinare correttamente
un gestore precedente, anche se questo è stato installato con signal. In generale poi non è il
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 definitiva dunque, a meno che non si sia vincolati
all’aderenza stretta allo standard ISO C, è sempre il caso di evitare l’uso di signal a favore di
sigaction.
Per questo motivo si è provveduto, per mantenere un’interfaccia semplificata che abbia le stes-
se caratteristiche di signal, a definire attraverso sigaction una funzione equivalente Signal,
il cui codice è riportato in fig. 9.10 (il codice completo si trova nel file SigHand.c nei sorgenti al-
24
introdotto con il kernel 2.6.6.
25
introdotto con il kernel 2.4.19.
26
dato che si tratta di una costante, e non di una maschera binaria, i valori numerici vengono riutilizzati e
ciascuno di essi avrà un significato diverso a seconda del segnale a cui è associato.
27
il prefisso del nome indica comunque in maniera diretta il segnale a cui le costanti fanno riferimento.
286 CAPITOLO 9. I SEGNALI
Valore Significato
ILL_ILLOPC codice di operazione illegale.
ILL_ILLOPN operando illegale.
ILL_ILLADR modo di indirizzamento illegale.
ILL_ILLTRP trappola di processore illegale.
ILL_PRVOPC codice di operazione privilegiato.
ILL_PRVREG registro privilegiato.
ILL_COPROC errore del coprocessore.
ILL_BADSTK errore nello stack interno.
FPE_INTDIV divisione per zero intera.
FPE_INTOVF overflow intero.
FPE_FLTDIV divisione per zero in virgola mobile.
FPE_FLTOVF overflow in virgola mobile.
FPE_FLTUND underflow in virgola mobile.
FPE_FLTRES risultato in virgola mobile non esatto.
FPE_FLTINV operazione in virgola mobile non valida.
FPE_FLTSUB mantissa? fuori intervallo.
SEGV_MAPERR indirizzo non mappato.
SEGV_ACCERR permessi non validi per l’indirizzo.
BUS_ADRALN allineamento dell’indirizzo non valido.
BUS_ADRERR indirizzo fisico inesistente.
BUS_OBJERR errore hardware sull’indirizzo.
TRAP_BRKPT breakpoint sul processo.
TRAP_TRACE trappola di tracciamento del processo.
CLD_EXITED il figlio è uscito.
CLD_KILLED il figlio è stato terminato.
CLD_DUMPED il figlio è terminato in modo anormale.
CLD_TRAPPED un figlio tracciato ha raggiunto una trappola.
CLD_STOPPED il figlio è stato fermato.
CLD_CONTINUED il figlio è ripartito.
POLL_IN disponibili dati in ingresso.
POLL_OUT spazio disponibile sul buffer di uscita.
POLL_MSG disponibili messaggi in ingresso.
POLL_ERR errore di I/O.
POLL_PRI disponibili dati di alta priorità in ingresso.
POLL_HUP il dispositivo è stato disconnesso.
Tabella 9.8: Valori del campo si_code della struttura sigaction impostati rispettivamente dai segnali SIGILL,
SIGFPE, SIGSEGV, SIGBUS, SIGCHLD, SIGTRAP e SIGPOLL/SIGIO.
legati). Si noti come, essendo la funzione estremamente semplice, essa è definita come inline;28
per semplificare ulteriormente la definizione si è poi definito un apposito tipo SigFunc.
#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à
i valori:
EINVAL si è specificato un numero di segnale invalido.
EFAULT si sono specificati indirizzi non validi.
La funzione usa l’insieme di segnali dato all’indirizzo set per modificare la maschera dei
segnali del processo corrente. La modifica viene effettuata a seconda del valore dell’argomento
how, secondo le modalità specificate in tab. 9.9. Qualora si specifichi un valore non nullo per
oldset la maschera dei segnali corrente viene salvata a quell’indirizzo.
Valore Significato
SIG_BLOCK L’insieme dei segnali bloccati è l’unione fra quello
specificato e quello corrente.
SIG_UNBLOCK I segnali specificati in set sono rimossi dalla maschera
dei segnali, specificare la cancellazione di un segnale non
bloccato è legale.
SIG_SETMASK La maschera dei segnali è impostata al valore specificato
da set.
28
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
di effettuare da soli queste manipolazioni (impostando le opportune ottimizzazioni) questa è una tecnica usata
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 l’uso 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 [11]) che in questo modo possono essere evitati.
29
nel caso di Linux essa è mantenuta dal campo blocked della task_struct del processo.
288 CAPITOLO 9. I SEGNALI
In questo modo diventa possibile proteggere delle sezioni di codice bloccando l’insieme di
segnali voluto per poi riabilitarli alla fine della sezione critica. La funzione permette di risolvere
problemi come quelli mostrati in fig. 9.7, proteggendo la sezione fra il controllo del flag e la sua
cancellazione.
La funzione può essere usata anche all’interno di un gestore, ad esempio per riabilitare la
consegna del segnale che l’ha invocato, in questo caso però occorre ricordare che qualunque
modifica alla maschera dei segnali viene perduta alla conclusione del terminatore.
Benché con l’uso di sigprocmask si possano risolvere la maggior parte dei casi di race con-
dition restano aperte alcune possibilità legate all’uso di pause; il caso è simile a quello del
problema illustrato nell’esempio di fig. 9.6, e cioè la possibilità che il processo riceva il segnale
che si intende usare per uscire dallo stato di attesa invocato con pause immediatamente prima
dell’esecuzione di quest’ultima. Per poter effettuare atomicamente la modifica della maschera
dei segnali (di solito attivandone uno specifico) insieme alla sospensione del processo lo standard
POSIX ha previsto la funzione sigsuspend, il cui prototipo è:
#include <signal.h>
int sigsuspend(const sigset_t *mask)
Imposta la signal mask specificata, mettendo in attesa il processo.
La funzione restituisce zero in caso di successo e −1 per un errore, nel qual caso errno assumerà
i valori:
EINVAL si è specificato un numero di segnale invalido.
EFAULT si sono specificati indirizzi non validi.
Come esempio dell’uso di queste funzioni proviamo a riscrivere un’altra volta l’esempio di
implementazione di sleep. Abbiamo accennato in sez. 9.4.3 come con sigaction sia possibile
bloccare SIGALRM nell’installazione dei gestori degli altri segnali, per poter usare l’implemen-
tazione vista in fig. 9.6 senza interferenze. Questo però comporta una precauzione ulteriore al
semplice uso della funzione, vediamo allora come usando la nuova interfaccia è possibile ottenere
un’implementazione, riportata in fig. 9.11 che non presenta neanche questa necessità.
Per evitare i problemi di interferenza con gli altri segnali in questo caso non si è usato
l’approccio di fig. 9.6 evitando l’uso 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 l’opportuno gestore per SIGALRM,
salvando quello originario, che sarà ripristinato alla conclusione della stessa (23); il passo succes-
sivo è quello di bloccare SIGALRM (11-14) per evitare che esso possa essere ricevuto dal processo
fra l’esecuzione di alarm (16) e la sospensione dello stesso. Nel fare questo si salva la maschera
corrente dei segnali, che sarà ripristinata alla fine (22), e al contempo si prepara la maschera dei
segnali sleep_mask per riattivare SIGALRM all’esecuzione di sigsuspend.
In questo modo non sono più possibili race condition dato che SIGALRM viene disabilitato
con sigprocmask fino alla chiamata di sigsuspend. Questo metodo è assolutamente generale e
può essere applicato a qualunque altra situazione in cui si deve attendere per un segnale, i passi
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 im-
mediatamente dopo, in questo modo si evita il deadlock dovuto all’arrivo del segnale prima
dell’esecuzione di sigsuspend.
9.4. LA GESTIONE AVANZATA DEI SEGNALI 289
Abbiamo finora 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
esso può essere eseguito in corrispondenza all’interruzione in un punto qualunque del programma
principale, cosa che ad esempio può rendere problematico chiamare all’interno di un gestore di
segnali la stessa funzione che dal segnale è stata interrotta.
Il concetto è comunque più generale e porta ad una distinzione fra quelle che POSIX chiama
funzioni insicure (signal unsafe function) e funzioni sicure (o più precisamente signal safe func-
tion); 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 indefinito, la cosa non
avviene invece per le funzioni sicure.
Tutto questo significa che la funzione che si usa come gestore di segnale deve essere pro-
grammata con molta cura per evirare questa evenienza e che non è possibile utilizzare al suo
interno una qualunque funzione di sistema, se si vogliono evitare questi problemi si può ricorrere
soltanto all’uso delle funzioni considerate sicure.
L’elenco delle funzioni considerate sicure varia a seconda della implementazione utilizzata e
dello standard a cui si fa riferimento;30 secondo quanto riportato dallo standard POSIX 1003.1
30
non è riportata una lista specifica delle funzioni sicure per Linux, si suppone pertanto che siano quelle richieste
dallo standard.
290 CAPITOLO 9. I SEGNALI
nella revisione del 2003, le “signal safe function” che possono essere chiamate anche all’interno
di un gestore di segnali sono tutte quelle della lista riportata in fig. 9.12.
_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.12: Elenco delle funzioni sicure secondo lo standard POSIX 1003.1-2003.
execl, execv, faccessat, fchmodat, fchownat, fexecve, fstatat, futimens, linkat, mkdirat,
mkfifoat, mknod, mknodat, openat, readlinkat, renameat, symlinkat, unlinkat, utimensat,
utimes.
Figura 9.13: Ulteriori funzioni sicure secondo lo standard POSIX.1-2008.
Per poter superare queste limitazioni lo standard POSIX.1b ha introdotto delle nuove carat-
teristiche, che sono state associate ad una nuova classe di segnali, che vengono chiamati segnali
real-time, in particolare le funzionalità aggiunte sono:
1. i segnali sono inseriti in una coda che permette di consegnare istanze multiple dello stesso
segnale qualora esso venga inviato più volte prima dell’esecuzione del gestore; si assicura
cosı̀ che il processo riceva un segnale per ogni occorrenza dell’evento che lo genera.
2. è stata introdotta una priorità nella consegna dei segnali: i segnali vengono consegnati in
ordine a seconda del loro valore, partendo da quelli con un numero minore, che pertanto
hanno una priorità maggiore.
Tutte queste nuove funzionalità eccetto l’ultima, che, come illustrato in sez. 9.4.3, è dispo-
nibile anche con i segnali ordinari, si applicano solo ai nuovi segnali real-time; questi ultimi
sono accessibili in un intervallo di valori specificati dalle due costanti SIGRTMIN e SIGRTMAX, che
specificano il numero minimo e massimo associato ad un segnale real-time.
Su Linux di solito il primo valore è 33, mentre il secondo è _NSIG-1, che di norma (vale
a dire sulla piattaforma i386) è 64. Questo dà un totale di 32 segnali disponibili, contro gli
almeno 8 richiesti da POSIX.1b. Si tenga presente però che i primi segnali real-time disponibili
vendono usati dalle glibc per l’implementazione dei thread POSIX (vedi sez. 13.2), ed il valore
di SIGRTMIN viene modificato di conseguenza.32
Per questo motivo nei programmi che usano i segnali real-time non si deve mai usare un
valore assoluto dato che si correrebbe il rischio di utilizzare un segnale in uso alle librerie, ed
il numero del segnale deve invece essere sempre specificato in forma relativa a SIGRTMIN (come
SIGRTMIN + n) avendo inoltre cura di controllare di non aver mai superato SIGRTMAX.
I segnali con un numero più basso hanno una priorità maggiore e vengono consegnati per
primi, inoltre i segnali real-time non possono interrompere l’esecuzione di un gestore di un segnale
a priorità più alta; la loro azione predefinita è quella di terminare il programma. I segnali ordinari
hanno tutti la stessa priorità, che è più alta di quella di qualunque segnale real-time.33
Si tenga presente che questi nuovi segnali non sono associati a nessun evento specifico, a
meno di non richiedere specificamente il loro utilizzo in meccanismi di notifica come quelli per
l’I/O asincrono (vedi sez. 12.3.3) o per le code di messaggi POSIX (vedi sez. 11.4.2); pertanto
devono essere inviati esplicitamente.
Inoltre, per poter usufruire della capacità di restituire dei dati, i relativi gestori devono essere
installati con sigaction, specificando per sa_flags la modalità SA_SIGINFO che permette di
32
vengono usati i primi tre per la vecchia implementazione dei LinuxThread ed i primi due per la nuova NTPL
(New Thread Posix Library), il che comporta che SIGRTMIN a seconda dei casi può essere 34 o 35.
33
lo standard non definisce niente al riguardo ma Linux, come molte altre implementazioni, adotta questa
politica.
292 CAPITOLO 9. I SEGNALI
utilizzare la forma estesa sa_sigaction (vedi sez. 9.4.3). In questo modo tutti i segnali real-
time possono restituire al gestore una serie di informazioni aggiuntive attraverso l’argomento
siginfo_t, la cui definizione è stata già vista in fig. 9.9, nella trattazione dei gestori in forma
estesa.
In particolare i campi utilizzati dai segnali real-time sono si_pid e si_uid in cui vengono
memorizzati rispettivamente il pid e l’user-ID effettivo del processo che ha inviato il segnale,
mentre per la restituzione dei dati viene usato il campo si_value.
Figura 9.14: La definizione dell’unione sigval, definita anche come tipo sigval_t.
Questo è una union di tipo sigval (la sua definizione è in fig. 9.14) in cui può essere
memorizzato o un valore numerico, se usata nella forma sival_int, o un indirizzo, se usata nella
forma sival_ptr. L’unione viene usata dai segnali real-time e da vari meccanismi di notifica34
per restituire dati al gestore del segnale; in alcune definizioni essa viene identificata anche con
l’abbreviazione sigval_t.
A causa delle loro caratteristiche, la funzione kill non è adatta ad inviare segnali real-time,
poiché non è in grado di fornire alcun valore per sigval; per questo motivo lo standard ha
previsto una nuova funzione, sigqueue, il cui prototipo è:
#include <signal.h>
int sigqueue(pid_t pid, int signo, const union sigval 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à
uno dei valori:
EAGAIN la coda è esaurita, ci sono già SIGQUEUE_MAX segnali in attesa si consegna.
EPERM non si hanno privilegi appropriati per inviare il segnale al processo specificato.
ESRCH il processo pid non esiste.
EINVAL si è specificato un valore non valido per signo.
ed inoltre ENOMEM.
la coda ha una dimensione variabile; fino alla versione 2.6.7 c’era un limite massimo globale che
poteva essere impostato come parametro del kernel in /proc/sys/kernel/rtsig-max;36 a parti-
re dal kernel 2.6.8 il valore globale è stato rimosso e sostituito dalla risorsa RLIMIT_SIGPENDING
associata al singolo utente, che può essere modificata con setrlimit come illustrato in sez. 8.3.2.
Lo standard POSIX.1b definisce inoltre delle nuove funzioni che permettono di gestire l’attesa
di segnali specifici 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 è:
#include <signal.h>
int sigwait(const sigset_t *set, int *sig)
Attende che uno dei segnali specificati in set sia pendente.
La funzione restituisce 0 in caso di successo e −1 in caso di errore, nel qual caso errno assumerà
uno dei valori:
EINTR la funzione è stata interrotta.
EINVAL si è specificato un valore non valido per set.
ed inoltre EFAULT.
La funzione estrae dall’insieme dei segnali pendenti uno qualunque dei segnali specificati
da set, il cui valore viene restituito in sig. Se sono pendenti più segnali, viene estratto quello
a priorità più alta (cioè con il numero più basso). Se, nel caso di segnali real-time, c’è più di
un segnale pendente, ne verrà estratto solo uno. Una volta estratto il segnale non verrà più
consegnato, e se era in una coda il suo posto sarà liberato. Se non c’è nessun segnale pendente
il processo viene bloccato fintanto 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 conflitto 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
ricevuto da sigwait, il tutto in maniera non prevedibile.
Lo standard POSIX.1b definisce altre due funzioni, anch’esse 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 *info, const struct timespec
*timeout)
Analoga a sigwaitinfo, con un la possibilità di specificare un timeout in timeout.
Le funzioni restituiscono 0 in caso di successo e −1 in caso di errore, nel qual caso errno assumerà
uno dei valori già visti per sigwait, ai quali si aggiunge, per sigtimedwait:
EAGAIN si è superato il timeout senza che un segnale atteso fosse emesso.
L’uso di queste funzioni è principalmente associato alla gestione dei segnali con i thread. In
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 i segnali gestiti in questa
maniera, per evitare che venga eseguita l’azione predefinita, devono essere mascherati per tutti i
thread, compreso quello dedicato alla gestione, che potrebbe riceverlo fra due chiamate successive.
Valore Significato
CLOCK_REALTIME Orologio real-time di sistema, può essere impostato solo
con privilegi amministrativi.
CLOCK_MONOTONIC Orologio che indica un tempo monotono crescente (a
partire da un tempo iniziale non specificato) che non
può essere modificato e non cambia neanche in caso di
reimpostazione dell’orologio di sistema.
CLOCK_MONOTONIC_RAW Simile al precedente, ma non subisce gli aggiustamenti
dovuti all’uso di NTP (viene usato per fare riferimento
ad una fonte hardware).42
CLOCK_PROCESS_CPUTIME_ID contatore del tempo di CPU usato da un processo (il
process time di sez. 8.4.2, nel totale di system time e user
time) comprensivo di tutto il tempo di CPU usato da
eventuali thread.
CLOCK_THREAD_CPUTIME_ID contatore del tempo di CPU (user time e system time)
usato da un singolo thread.
Tabella 9.10: Valori possibili per una variabile di tipo clockid_t usata per indicare a quale tipo di orologio si
vuole fare riferimento.
essere controllato dalla definizione della macro _POSIX_TIMERS ad un valore maggiore di 0, e che
le ulteriori macro _POSIX_MONOTONIC_CLOCK, _POSIX_CPUTIME e _POSIX_THREAD_CPUTIME indi-
cano la presenza dei rispettivi orologi di tipo CLOCK_MONOTONIC, CLOCK_PROCESS_CPUTIME_ID e
CLOCK_PROCESS_CPUTIME_ID.43 Infine se il kernel ha il supporto per gli high resolution timer un
elenco degli orologi e dei timer può essere ottenuto tramite il file /proc/timer_list.
Le due funzioni che ci consentono rispettivamente di modificare o leggere il valore per uno
degli orologi real-time sono clock_settime e clock_gettime; i rispettivi prototipi sono:
#include <time.h>
int clock_settime(clockid_t clockid, const struct timespec *tp)
int clock_gettime(clockid_t clockid, struct timespec *tp)
Imposta o legge un orologio real-time.
La funzione restituisce 0 in caso di successo e −1 in caso di errore, nel qual caso errno assumerà
uno dei seguenti valori:
EINVAL il valore specificato per clockid non è valido o il relativo orologio real-time non è
supportato dal sistema.
EPERM non si ha il permesso di impostare l’orologio indicato (solo per clock_settime).
EFAULT l’indirizzo tp non è valido.
Entrambe le funzioni richiedono che si specifichi come primo argomento il tipo di orologio
su cui si vuole operare con uno dei valori di tab. 9.10 o con il risultato di una chiamata a
clock_getcpuclockid (che tratteremo a breve), il secondo argomento invece è sempre il punta-
tore tp ad una struttura timespec (vedi fig. 5.8) che deve essere stata precedentemente allocata;
nel primo caso questa dovrà anche essere stata inizializzata con il valore che si vuole impostare
sull’orologio, mentre nel secondo verrà restituito al suo interno il valore corrente dello stesso.
Si tenga presente inoltre che per eseguire un cambiamento sull’orologio generale di sistema
CLOCK_REALTIME occorrono i privilegi amministrativi;44 inoltre ogni cambiamento ad esso ap-
portato non avrà nessun effetto sulle temporizzazioni effettuate in forma relativa, come quelle
impostate sulle quantità di process time o per un intervallo di tempo da trascorrere, ma solo
su quelle che hanno richiesto una temporizzazione ad un istante preciso (in termini di calendar
time). Si tenga inoltre presente che nel caso di Linux CLOCK_REALTIME è l’unico orologio per cui
43
tutte queste macro sono definite in unistd.h, che pertanto deve essere incluso per poterle controllarle.
44
ed in particolare la capability CAP_SYS_TIME.
296 CAPITOLO 9. I SEGNALI
si può effettuare una modifica, infatti nonostante lo standard preveda la possibilità di modifiche
anche per CLOCK_PROCESS_CPUTIME_ID e CLOCK_THREAD_CPUTIME_ID, il kernel non le consente.
Oltre alle due funzioni precedenti, lo standard POSIX prevede una terza funzione che con-
senta di ottenere la risoluzione effettiva fornita da un certo orologio, la funzione è clock_getres
ed il suo prototipo è:
#include <time.h>
int clock_getres(clockid_t clockid, struct timespec *res)
Legge la risoluzione di un orologio real-time.
La funzione restituisce 0 in caso di successo e −1 in caso di errore, nel qual caso errno assumerà
uno dei seguenti valori:
EINVAL il valore specificato per clockid non è valido.
EFAULT l’indirizzo di res non è valido.
La funzione richiede come primo argomento l’indicazione dell’orologio di cui si vuole conoscere
la risoluzione (effettuata allo stesso modo delle due precedenti) e questa verrà restituita in una
struttura timespec all’indirizzo puntato dall’argomento res.
Come accennato il valore di questa risoluzione dipende sia dall’hardware disponibile che dalla
implementazione delle funzioni, e costituisce il limite minimo di un intervallo di tempo che si può
indicare. Qualunque valore si voglia utilizzare nelle funzioni di impostazione che non corrisponda
ad un multiplo intero di questa risoluzione, sarà troncato in maniera automatica.
Si tenga presente inoltre che con l’introduzione degli high resolution timer i due orologi
CLOCK_PROCESS_CPUTIME_ID e CLOCK_THREAD_CPUTIME_ID fanno riferimento ai contatori pre-
senti in opportuni registri interni del processore; questo sui sistemi multiprocessore può avere
delle ripercussioni sulla precisione delle misure di tempo che vanno al di là della risoluzione
teorica ottenibile con clock_getres, che può essere ottenuta soltanto quando si è sicuri che un
processo (o un thread ) sia sempre stato eseguito sullo stesso processore.
Con i sistemi multiprocessore infatti ogni singola CPU ha i suoi registri interni, e se ciascu-
na di esse utilizza una base di tempo diversa (se cioè il segnale di temporizzazione inviato ai
processori non ha una sola provenienza) in genere ciascuna di queste potrà avere delle frequenze
leggermente diverse, e si otterranno pertanto dei valori dei contatori scorrelati fra loro, senza
nessuna possibilità di sincronizzazione.
Il problema si presenta, in forma più lieve, anche se la base di tempo è la stessa, dato che
un sistema multiprocessore non avvia mai tutte le CPU allo stesso istante, si potrà cosı̀ avere
di nuovo una differenza fra i contatori, soggetta però soltanto ad uno sfasamento costante. Per
questo caso il kernel per alcune architetture ha del codice che consente di ridurre al minimo
la differenza, ma non può essere comunque garantito che questa si annulli (anche se in genere
risulta molto piccola e trascurabile nella gran parte dei casi).
Per poter gestire questo tipo di problematiche lo standard ha previsto una apposita funzione
che sia in grado di ottenere l’identificativo dell’orologio associato al process time di un processo,
la funzione è clock_getcpuclockid ed il suo prototipo è:
#include <time.h>
int clock_getcpuclockid(pid_t pid, clockid_t *clockid)
Ottiene l’identificatore dell’orologio di CPU usato da un processo.
La funzione restituisce 0 in caso di successo o un numero positivo in caso di errore, nel qual caso
errno assumerà uno dei seguenti valori:
ENOSYS non c’è il supporto per ottenere l’orologio relativo al process time di un altro processo,
e pid non corrisponde al processo corrente.
EPERM il chiamante non ha il permesso di accedere alle informazioni relative al processo pid.
ESRCH non esiste il processo pid.
9.5. FUNZIONALITÀ AVANZATE 297
Con l’introduzione degli orologi ad alta risoluzione è divenuto possibile ottenere anche una ge-
stione più avanzata degli allarmi; abbiamo già visto in sez. 9.3.4 come l’interfaccia di setitimer
derivata da BSD presenti delle serie limitazioni,46 tanto che nello standard POSIX.1-2008 questa
viene marcata come obsoleta, e ne viene fortemente consigliata la sostituzione con nuova inter-
faccia definita dallo standard POSIX.1-2001 che va sotto il nome di Posix Timer API. Questa
interfaccia è stata introdotta a partire dal kernel 2.6, anche se il supporto di varie funzionalità
è stato aggiunto solo in un secondo tempo.
Una delle principali differenze della nuova interfaccia è che un processo può utilizzare un nu-
mero arbitrario di timer; questi vengono creati (ma non avviati) tramite la funzione timer_create,
il cui prototipo è:
#include <signal.h>
#include <time.h>
int timer_create(clockid_t clockid, struct sigevent *evp, timer_t *timerid)
Crea un nuovo timer Posix.
La funzione restituisce 0 in caso di successo e −1 in caso di errore, nel qual caso errno assumerà
uno dei seguenti valori:
EAGAIN fallimento nel tentativo di allocare le strutture dei timer.
EINVAL uno dei valori specificati per clockid o per i campi sigev_notify, sigev_signo o
sigev_notify_thread_id di evp non è valido.
ENOMEM errore di allocazione della memoria.
La funzione richiede tre argomenti: il primo argomento serve ad indicare quale tipo di orologio
si vuole utilizzare e prende uno dei valori di tab. 9.10,47 si può cosı̀ fare riferimento sia ad un
tempo assoluto che al tempo utilizzato dal processo (o thread ) stesso.
Il secondo argomento richiede una trattazione più dettagliata, in quanto introduce una strut-
tura di uso generale, sigevent, che viene utilizzata anche da altre funzioni, come quelle per l’I/O
asincrono (vedi sez. 12.3.3) o le code di messaggi POSIX (vedi sez. 11.4.2)) e che serve ad indicare
in maniera generica un meccanismo di notifica.
La struttura sigevent (accessibile includendo time.h) è riportata in fig. 9.15;48 il campo
sigev_notify è il più importante essendo quello che indica le modalità della notifica, gli altri
dipendono dal valore che si è specificato per sigev_notify, si sono riportati in tab. 9.11. La scelta
45
per poter usare la funzione, come per qualunque funzione che faccia riferimento ai thread, occorre effettuare
il collegamento alla relativa libreria di gestione compilando il programma con -lpthread.
46
in particolare la possibilità di perdere un segnale sotto carico.
47
di detti valori però non è previsto l’uso di CLOCK_MONOTONIC_RAW mentre CLOCK_PROCESS_CPUTIME_ID e
CLOCK_THREAD_CPUTIME_ID sono disponibili solo a partire dal kernel 2.6.12.
48
la definizione effettiva dipende dall’implementazione, quella mostrata è la versione descritta nella pagina di
manuale di timer_create.
298 CAPITOLO 9. I SEGNALI
struct sigevent {
int sigev_notify ; /* Notification method */
int sigev_signo ; /* Timer expiration signal */
union sigval sigev_value ; /* Value accompanying signal or
passed to thread function */
/* Function used for thread notifications ( SIGEV_THREAD ) */
void (* sigev_notify_function ) ( union sigval );
/* Attributes for notification thread ( SIGEV_THREAD ) */
void * sigev_notify_attributes ;
/* ID of thread to signal ( SIGEV_THREAD_ID ) */
pid_t sigev_notify_thread_id ;
};
Figura 9.15: La struttura sigevent, usata per specificare in maniera generica diverse modalità di notifica degli
eventi.
del meccanismo di notifica viene fatta impostando uno dei valori di tab. 9.11 per sigev_notify,
e fornendo gli eventuali ulteriori argomenti necessari a secondo della scelta effettuata. Diventa
cosı̀ possibile indicare l’uso di un segnale o l’esecuzione (nel caso di uso dei thread ) di una
funzione di modifica in un thread dedicato.
Valore Significato
SIGEV_NONE Non viene inviata nessuna notifica.
SIGEV_SIGNAL La notifica viene effettuata inviando al processo chiamante il segnale
specificato dal campo sigev_signo; se il gestore di questo segnale è
stato installato con SA_SIGINFO gli verrà restituito il valore specificato
con sigev_value (una union sigval, la cui definizione è in fig. 9.14)
come valore del campo si_value di siginfo_t.
SIGEV_THREAD La notifica viene effettuata creando un nuovo thread che esegue la fun-
zione di notifica specificata da sigev_notify_function con argomento
sigev_value. Se questo è diverso da NULL, il thread viene creato con gli
attributi specificati da sigev_notify_attribute.49
SIGEV_THREAD_ID Invia la notifica come segnale (con le stesse modalità di
SIGEV_SIGNAL) che però viene recapitato al thread indicato dal campo
sigev_notify_thread_id. Questa modalità è una estensione specifica
di Linux, creata come supporto per le librerie di gestione dei thread,
pertanto non deve essere usata da codice normale.
Tabella 9.11: Valori possibili per il campo sigev_notify in una struttura sigevent.
Nel caso di timer_create occorrerà passare alla funzione come secondo argomento l’indirizzo
di una di queste strutture per indicare le modalità con cui si vuole essere notificati della scadenza
del timer, se non si specifica nulla (passando un valore NULL) verrà inviato il segnale SIGALRM al
processo corrente, o per essere più precisi verrà utilizzato un valore equivalente all’aver specificato
SIGEV_SIGNAL per sigev_notify, SIGALRM per sigev_signo e l’identificatore del timer come
valore per sigev_value.sival_int.
Il terzo argomento deve essere l’indirizzo di una variabile di tipo timer_t dove sarà scritto
l’identificativo associato al timer appena creato, da usare in tutte le successive funzioni di gestio-
ne. Una volta creato questo identificativo resterà univoco all’interno del processo stesso fintanto
che il timer non viene cancellato.
Si tenga presente che eventuali POSIX timer creati da un processo non vengono ereditati dai
processi figli creati con fork e che vengono cancellati nella esecuzione di un programma diverso
49
nel caso dei timer questa funzionalità è considerata un esempio di pessima implementazione di una interfaccia,
richiesta dallo standard POSIX, ma da evitare totalmente, a causa della possibilità di creare disservizi generando
una gran quantità di processi, tanto che ne è stata richiesta addirittura la rimozione.
9.5. FUNZIONALITÀ AVANZATE 299
attraverso una delle funzioni exec. Si tenga presente inoltre che il kernel prealloca l’uso di un
segnale real-time per ciascun timer che viene creato con timer_create; dato che ciascuno di essi
richiede un posto nella coda dei segnali real-time, il numero massimo di timer utilizzabili da un
processo è limitato dalle dimensioni di detta coda, ed anche, qualora questo sia stato impostato,
dal limite RLIMIT_SIGPENDING.
Una volta creato il timer timer_create ed ottenuto il relativo identificatore, si può attivare
o disattivare un allarme (in gergo armare o disarmare il timer) con la funzione timer_settime,
il cui prototipo è:
#include <signal.h>
#include <time.h>
int timer_settime(timer_t timerid, int flags, const struct itimerspec *new_value,
struct itimerspec *old_value)
Arma o disarma il timer POSIX.
La funzione restituisce 0 in caso di successo e −1 in caso di errore, nel qual caso errno assumerà
uno dei seguenti valori:
EINVAL all’interno di new_value.value si è specificato un tempo negativo o un numero di
nanosecondi maggiore di 999999999.
EFAULT si è specificato un indirizzo non valido per new_value o old_value.
La funzione richiede che si indichi la scadenza del timer con l’argomento new_value, che
deve essere specificato come puntatore ad una struttura di tipo itimerspec, la cui definizione
è riportata in fig. 9.16; se il puntatore old_value è diverso da NULL il valore corrente della
scadenza verrà restituito in una analoga struttura, ovviamente in entrambi i casi le strutture
devono essere state allocate.
struct itimerspec {
struct timespec it_interval ; /* Timer interval */
struct timespec it_value ; /* Initial expiration */
};
Ciascuno dei due campi di itimerspec indica un tempo, da specificare con una precisione
fino al nanosecondo tramite una struttura timespec (la cui definizione è riportata fig. 5.8). Il
campo it_value indica la prima scadenza dell’allarme. Di default, quando il valore di flags
è nullo, questo valore viene considerato come un intervallo relativo al tempo corrente,50 se
invece si usa per flags il valore TIMER_ABSTIME,51 it_value viene considerato come un valore
assoluto rispetto al valore usato dall’orologio a cui è associato il timer.52 Infine un valore nullo di
it_value53 può essere utilizzato, indipendentemente dal tipo di orologio utilizzato, per disarmare
l’allarme.
Il campo it_interval di itimerspec viene invece utilizzato per impostare un allarme pe-
riodico. Se il suo valore è nullo (se cioè sono nulli tutti e due i valori di detta struttura timespec)
l’allarme scatterà una sola volta secondo quando indicato con it_value, altrimenti il valore spe-
cificato verrà preso come l’estensione del periodo di ripetizione della generazione dell’allarme,
che proseguirà indefinitamente fintanto che non si disarmi il timer.
50
il primo allarme scatterà cioè dopo il numero di secondi e nanosecondi indicati da questo campo.
51
al momento questo è l’unico valore valido per flags.
52
quindi a seconda dei casi lo si potrà indicare o come un tempo assoluto, quando si opera rispetto all’orologio
di sistema (nel qual caso il valore deve essere in secondi e nanosecondi dalla epoch) o come numero di secondi o
nanosecondi rispetto alla partenza di un orologio di CPU, quando si opera su uno di questi.
53
per nullo si intende con valori nulli per entrambi i i campi tv_sec e tv_nsec.
300 CAPITOLO 9. I SEGNALI
Se il timer era già stato armato la funzione sovrascrive la precedente impostazione, se invece
si indica come prima scadenza un tempo già passato, l’allarme verrà notificato immediatamente
e al contempo verrà incrementato il contatore dei superamenti. Questo contatore serve a fornire
una indicazione al programma che riceve l’allarme su un eventuale numero di scadenze che sono
passate prima della ricezione della notifica dell’allarme.
É infatti possibile, qualunque sia il meccanismo di notifica scelto, che quest’ultima venga
ricevuta dopo che il timer è scaduto più di una volta.54 Nel caso dell’uso di un segnale infatti il
sistema mette in coda un solo segnale per timer,55 e se il sistema è sotto carico o se il segnale
è bloccato, prima della sua ricezione può passare un intervallo di tempo sufficientemente lungo
ad avere scadenze multiple, e lo stesso può accadere anche se si usa un thread di notifica.
Per questo motivo il gestore del segnale o il thread di notifica può ottenere una indicazione di
quante volte il timer è scaduto dall’invio della notifica utilizzando la funzione timer_getoverrun,
il cui prototipo è:
#include <time.h>
int timer_getoverrun(timer_t timerid)
Ottiene il numero di scadenze di un timer POSIX.
La funzione restituisce il numero di scadenze di un timer in caso di successo e −1 in caso di errore,
nel qual caso errno assumerà il valore:
EINVAL timerid non indica un timer valido.
La funzione ritorna il numero delle scadenze avvenute, che può anche essere nullo se non ve
ne sono state. Come estensione specifica di Linux,56 quando si usa un segnale come meccanismo
di notifica, si può ottenere direttamente questo valore nel campo si_overrun della struttura
siginfo_t (illustrata in fig. 9.9) restituita al gestore del segnale installato con sigaction; in
questo modo non è più necessario eseguire successivamente una chiamata a questa funzione per
ottenere il numero delle scadenze. Al gestore del segnale viene anche restituito, come ulteriore
informazione, l’identificativo del timer, in questo caso nel campo si_timerid.
Qualora si voglia rileggere lo stato corrente di un timer, ed ottenere il tempo mancante ad
una sua eventuale scadenza, si deve utilizzare la funzione timer_gettime, il cui prototipo è:
#include <time.h>
int timer_gettime(timer_t timerid, int flags, struct itimerspec *curr_value)
Legge lo stato di un timer POSIX.
La funzione restituisce 0 in caso di successo e −1 in caso di errore, nel qual caso errno assumerà
uno dei seguenti valori:
EINVAL timerid non indica un timer valido.
EFAULT si è specificato un indirizzo non valido per curr_value.
il timer non era stato impostato per una ripetizione e doveva operare, come suol dirsi, a colpo
singolo (in gergo one shot).
Infine, quando un timer non viene più utilizzato, lo si può cancellare, rimuovendolo dal
sistema e recuperando le relative risorse, effettuando in sostanza l’operazione inversa rispetto a
timer_create. Per questo compito lo standard prevede una apposita funzione timer_delete,
il cui prototipo è:
#include <time.h>
int timer_delete(timer_t timerid)
Cancella un timer POSIX.
La funzione restituisce 0 in caso di successo e −1 in caso di errore, nel qual caso errno assumerà
uno dei seguenti valori:
EINVAL timerid non indica un timer valido.
La funzione elimina il timer identificato da timerid, disarmandolo se questo era stato attiva-
to. Nel caso, poco probabile ma comunque possibile, che un timer venga cancellato prima della
ricezione del segnale pendente per la notifica di una scadenza, il comportamento del sistema è
indefinito.
La funzione permette di ricavare quali sono i segnali pendenti per il processo in corso, cioè i
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
poco utile, dato che essa può solo assicurare che un segnale è stato inviato, dato che escluderne
l’avvenuto invio al momento della chiamata non significa 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
stack alternativo per i segnali; è cioè possibile fare usare al sistema un altro stack (invece di
quello relativo al processo, vedi sez. 2.2.2) solo durante l’esecuzione di un gestore. L’uso di uno
stack alternativo è del tutto trasparente ai gestori, occorre però seguire una certa procedura:
In genere il primo passo viene effettuato allocando un’opportuna area di memoria con malloc;
in signal.h sono definite due costanti, SIGSTKSZ e MINSIGSTKSZ, che possono essere utilizzate
per allocare una quantità di spazio opportuna, in modo da evitare overflow. La prima delle due è
la dimensione canonica per uno stack di segnali e di norma è sufficiente per tutti gli usi normali.
La seconda è lo spazio che occorre al sistema per essere in grado di lanciare il gestore e la
dimensione di uno stack alternativo deve essere sempre maggiore di questo valore. Quando si
302 CAPITOLO 9. I SEGNALI
conosce esattamente quanto è lo spazio necessario al gestore gli si può aggiungere questo valore
per allocare uno stack di dimensione sufficiente.
Come accennato, per poter essere usato, lo stack per i segnali deve essere indicato al sistema
attraverso la funzione sigaltstack; il suo prototipo è:
#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à
i valori:
La funzione prende come argomenti puntatori ad una struttura di tipo stack_t, definita in
fig. 9.17. 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 ; /* Base address of stack */
int ss_flags ; /* Flags */
size_t ss_size ; /* Number of bytes in stack */
} stack_t ;
Il campo ss_sp di stack_t indica l’indirizzo base dello stack, mentre ss_size ne indica
la dimensione; il campo ss_flags invece indica lo stato dello stack. Nell’indicare 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
nei relativi campi, mentre ss_flags potrà assumere il valore SS_ONSTACK se il processo è in
esecuzione sullo stack alternativo (nel qual caso non è possibile cambiarlo) e SS_DISABLE se
questo non è abilitato.
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 l’esecuzione sullo stack alternativo con-
tinueranno ad usare quest’ultimo, che, al contrario di quanto avviene per lo stack ordinario dei
processi, non si accresce automaticamente (ed infatti eccederne le dimensioni può portare a con-
seguenze imprevedibili). Si ricordi infine che una chiamata ad una funzione della famiglia exec
cancella ogni stack alternativo.
Abbiamo visto in fig. 9.6 come si possa usare longjmp per uscire da un gestore rientrando
direttamente nel corpo del programma; sappiamo però che nell’esecuzione di un gestore il segnale
che l’ha invocato viene bloccato, e abbiamo detto che possiamo ulteriormente modificarlo con
sigprocmask.
9.5. FUNZIONALITÀ AVANZATE 303
Resta quindi il problema di cosa succede alla maschera dei segnali quando si esce da un
gestore usando questa funzione. Il comportamento dipende dall’implementazione; in particola-
re la semantica usata da BSD prevede che sia ripristinata la maschera dei segnali precedente
l’invocazione, come per un normale ritorno, mentre quella usata da System V no.
Lo standard POSIX.1 non specifica 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.7.
Lo standard POSIX però prevede anche la presenza di altre due funzioni sigsetjmp e
siglongjmp, che permettono di decidere quale dei due comportamenti il programma deve assu-
mere; 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
specificare il comportamento sul ripristino o meno della maschera dei 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 specifico essa è di tipo sigjmp_buf, 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 specifica un valore di savesigs diverso da zero la maschera dei
valori sarà salvata in env e ripristinata in un successivo siglongjmp; quest’ultima funzione, a
parte l’uso di sigjmp_buf per env, è assolutamente identica a longjmp.
304 CAPITOLO 9. I SEGNALI
Capitolo 10
A lungo l’unico modo per interagire con sistema di tipo Unix è stato tramite l’interfaccia dei
terminali, ma anche oggi, nonostante la presenza di diverse interfacce grafiche, essi continuano
ad essere estensivamente usati per il loro stretto legame la linea di comando.
Nella prima parte esamineremo i concetti base in cui si articola l’interfaccia dei terminali, a
partire dal sistema del job control e delle sessioni di lavoro, toccando infine anche le problematiche
dell’interazione con programmi non interattivi. Nella seconda parte tratteremo il funzionamento
dell’I/O su terminale, e delle varie peculiarità che esso viene ad assumere nell’uso come interfaccia
di accesso al sistema da parte degli utenti. La terza parte coprirà le tematiche relative alla
creazione e gestione dei terminali virtuali, che consentono di replicare via software l’interfaccia
dei terminali.
305
306 CAPITOLO 10. INTERFACCIA UTENTE: TERMINALI E SESSIONI DI LAVORO
Il job control è una caratteristica opzionale, introdotta in BSD negli anni ’80, e successi-
vamente standardizzata da POSIX.1; la sua disponibilità nel sistema è verificabile attraverso il
controllo 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, l’utente avrà a
disposizione una shell dalla quale eseguire i comandi e potrà iniziare quella che viene chiamata
una sessione, che riunisce (vedi sez. 10.1.2) tutti i processi eseguiti all’interno 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
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
una “&” alla fine del comando, più programmi in contemporanea, mandandoli in background (o
sullo sfondo), nel qual caso essi saranno eseguiti senza essere collegati al terminale.
Si noti come si sia parlato di comandi e non di programmi o processi; fra le funzionalità
della shell infatti c’è anche quella di consentire di concatenare più programmi in una sola riga di
comando con le pipe, ed in tal caso verranno eseguiti più programmi. Inoltre, anche quando si
invoca un singolo programma, questo potrà sempre lanciare eventuali sotto-processi per eseguire
dei compiti specifici.
Per questo l’esecuzione di un comando può originare più di un processo; quindi nella gestione
del job control non si può far riferimento ai singoli processi. Per questo il kernel prevede la pos-
sibilità di raggruppare più processi in un cosiddetto process group (detto anche raggruppamento
di processi, vedi sez. 10.1.2). Deve essere cura della shell far sı̀ che tutti i processi che originano
da una stessa riga di comando appartengano allo stesso raggruppamento di processi, in modo
che le varie funzioni di controllo, ed i segnali inviati dal terminale, possano fare riferimento ad
esso.
In generale all’interno di una sessione avremo un eventuale (può non esserci) process group
in foreground, che riunisce i processi che possono accedere al terminale, e più process group in
background, che non possono accedervi. Il job control prevede che quando un processo appar-
tenente 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 in-
viati dal terminale, che vengono inviati a tutti i processi del raggruppamento in foreground.
In particolare C-z interrompe l’esecuzione del comando, che può poi essere mandato in back-
ground con il comando bg.3 Il comando fg consente invece di mettere in foreground un comando
precedentemente lanciato in background.
Di norma la shell si cura anche di notificare all’utente (di solito prima della stampa a video
del prompt) lo stato dei vari processi; essa infatti sarà in grado, grazie all’uso di waitpid, di
rilevare sia i processi che sono terminati, sia i raggruppamenti che sono bloccati (in questo caso
usando l’opzione WUNTRACED, secondo quanto illustrato in sez. 3.2.4).
3
si tenga presente che bg e fg sono parole chiave che indicano comandi interni alla shell, e nel caso non
comportano l’esecuzione di un programma esterno ma operazioni di gestione compiute direttamente dalla shell
stessa.
10.1. L’INTERAZIONE CON I TERMINALI 307
che il kernel associa a ciascun processo:4 l’identificatore del process group e l’identificatore 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 identificatori possono essere visualizzati dal comando ps usando
l’opzione -j.
Un process group è pertanto definito da tutti i processi che hanno lo stesso pgid; è possibile
leggere il valore di questo identificatore con le funzioni getpgid e getpgrp,5 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 specificare 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
a getpgid(0).
In maniera analoga l’identificatore della sessione può essere letto dalla funzione getsid, che
però nelle glibc 6 è accessibile solo definendo _XOPEN_SOURCE e _XOPEN_SOURCE_EXTENDED; il suo
prototipo è:
#include <unistd.h>
pid_t getsid(pid_t pid)
Legge l’identificatore di sessione del processo pid.
Entrambi gli identificatori 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ù
process group all’interno della stessa sessione, e spostare i processi dall’uno all’altro, ma sempre
all’interno di una stessa sessione.
Ciascun raggruppamento di processi ha sempre un processo principale, il cosiddetto process
group leader, che è identificato dall’avere un pgid uguale al suo pid, in genere questo è il primo
processo del raggruppamento, che si incarica di lanciare tutti gli altri. Un nuovo raggruppamento
si crea con la funzione setpgrp,7 il cui prototipo è:
#include <unistd.h>
int setpgrp(void)
Modifica il pgid al valore del pid del processo corrente.
4
in Linux questi identificatori sono mantenuti nei campi pgrp e session della struttura task_struct definita
in sched.h.
5
getpgrp è definita nello standard POSIX.1, mentre getpgid è richiesta da SVr4.
6
la system call è stata introdotta in Linux a partire dalla versione 1.3.44, il supporto nelle librerie del C è
iniziato dalla versione 5.2.19. La funzione non è prevista da POSIX.1, che parla solo di processi leader di sessione,
e non di identificatori di sessione.
7
questa è la definizione di POSIX.1, BSD definisce una funzione con lo stesso nome, che però è identica a
setpgid; nelle glibc viene sempre usata sempre questa definizione, a meno di non richiedere esplicitamente la
compatibilità all’indietro con BSD, definendo la macro _BSD_SOURCE.
308 CAPITOLO 10. INTERFACCIA UTENTE: TERMINALI E SESSIONI DI LAVORO
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. È possibile invece spostare un processo
da un raggruppamento ad un altro con la funzione setpgid, il cui prototipo è:
#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:
ESRCH il processo selezionato non esiste.
EPERM il cambiamento non è consentito.
EACCES il processo ha già eseguito una exec.
EINVAL il valore di pgid è negativo.
La funzione permette di cambiare il pgid del processo pid, ma il cambiamento può essere
effettuato solo se pgid indica un process group che è nella stessa sessione del processo chiamante.
Inoltre la funzione può essere usata soltanto sul processo corrente o su uno dei suoi figli, ed in
quest’ultimo caso ha successo soltanto se questo non ha ancora eseguito una exec.8 Specificando
un valore nullo per pid si indica il processo corrente, mentre specificando un valore nullo per
pgid si imposta il process group al valore del pid del processo selezionato; pertanto setpgrp è
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 figlio, che da quest’ultimo, per
sé stesso, in modo che il cambiamento di process group sia immediato per entrambi; una delle
due chiamate sarà ridondante, ma non potendo determinare quale dei due processi viene eseguito
per primo, occorre eseguirle comunque entrambe per evitare di esporsi ad una race condition.
Si noti come nessuna delle funzioni esaminate finora permetta di spostare un processo da
una sessione ad un altra; infatti l’unico modo di far cambiare sessione ad un processo è quello
di crearne una nuova con l’uso di setsid; il suo prototipo è:
#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,
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 sessione9 se il suo sid è uguale al suo pid) ed unico componente.
Inoltre la funzione distacca il processo da ogni terminale di controllo (torneremo sull’argomento
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
per usarla di norma si esegue una fork e si esce, per poi chiamare setsid nel processo figlio, in
modo che, avendo questo lo stesso pgid del padre ma un pid diverso, non ci siano possibilità di
errore.10 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.
8
questa caratteristica è implementata dal kernel che mantiene allo scopo un altro campo, did_exec, in
task_struct.
9
in Linux la proprietà è mantenuta in maniera indipendente con un apposito campo leader in task_struct.
10
potrebbe sorgere il dubbio che, per il riutilizzo dei valori dei pid fatto nella creazione dei nuovi processi (vedi
sez. 3.2.1), il figlio venga ad assumere un valore corrispondente ad un process group esistente; questo viene evitato
10.1. L’INTERAZIONE CON I TERMINALI 309
La funzione restituisce 0 in caso di successo, e -1 in caso di errore, nel qual caso errno assumerà
i valori:
ENOTTY il file fd non corrisponde al terminale di controllo del processo chiamante.
ENOSYS il sistema non supporta il job control.
EPERM il process group specificato non è nella stessa sessione del processo chiamante.
ed inoltre EBADF ed EINVAL.
la funzione può essere eseguita con successo solo da un processo nella stessa sessione e con lo
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à l’invio da parte del kernel di uno dei due segnali SIGTTIN o
SIGTTOU (a seconda che l’accesso 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
fermare il processo, di norma questo comporta che tutti i membri del gruppo verranno fermati,
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.
11
nel caso di login grafico la cosa può essere più complessa, e di norma l’I/O è effettuato tramite il server X, ma
ad esempio per i programmi, anche grafici, lanciati da un qualunque emulatore di terminale, sarà quest’ultimo a
fare da terminale (virtuale) di controllo.
12
lo standard POSIX.1 non specifica nulla riguardo l’implementazione; in Linux anch’esso viene mantenuto nella
solita struttura task_struct, nel campo tty.
13
solo quando ciò è necessario, cosa che, come vedremo in sez. 10.1.5, non è sempre vera.
14
a meno di non avere richiesto esplicitamente che questo non diventi un terminale di controllo con il flag
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.
310 CAPITOLO 10. INTERFACCIA UTENTE: TERMINALI E SESSIONI DI LAVORO
ma non si avranno condizioni di errore.15 Se però si bloccano o ignorano i due segnali citati, le
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
funzione tcgetpgrp, il cui prototipo è:
#include <unistd.h>
#include <termios.h>
pid_t tcgetpgrp(int fd)
Legge il process group di foreground del terminale associato al file 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:
ENOTTY non c’è un terminale di controllo o fd non corrisponde al terminale di controllo del
processo chiamante.
ed inoltre EBADF ed ENOSYS.
Si noti come entrambe le funzioni usino come argomento il valore di un file descriptor, il
risultato comunque non dipende dal file descriptor che si usa ma solo dal terminale cui fa riferi-
mento; il kernel inoltre permette a ciascun processo di accedere direttamente al suo terminale di
controllo attraverso il file speciale /dev/tty, che per ogni processo è un sinonimo per il proprio
terminale di controllo. Questo consente anche a processi che possono aver rediretto l’output di
accedere al terminale di controllo, pur non disponendo più del file descriptor originario; un caso
tipico è il programma crypt che accetta la redirezione sullo standard input di un file da decifrare,
ma deve poi leggere la password dal terminale.
Un’altra caratteristica del terminale di controllo usata nel job control è che utilizzando su
di esso le combinazioni di tasti speciali (C-z, C-c, C-y e C-|) si farà sı̀ che il kernel invii i corri-
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
e l’interruzione dei vari comandi.
Per completare la trattazione delle caratteristiche del job control legate al terminale di con-
trollo, 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 verifica questa condizione, ad esempio se si interrompe la linea, o va giù la rete
o più semplicemente si chiude forzatamente la finestra di terminale su cui si stava lavorando, il
kernel provvederà ad inviare il segnale di SIGHUP al processo di controllo. L’azione preimpostata
in questo caso è la terminazione del processo, il problema che si pone è cosa accade agli altri
processi nella sessione, che non han più un processo di controllo che possa gestire l’accesso al
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 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
terminale (di norma tutto ciò comporta la terminazione anche di questi ultimi).
Restano però gli eventuali processi in background, che non ricevono il segnale; in effetti se
il terminale non dovesse più servire essi potrebbero proseguire fino al completamento della loro
esecuzione; ma si pone il problema di come gestire quelli che sono bloccati, o che si bloccano
nell’accesso al terminale, in assenza di un processo che sia in grado di effettuare il controllo dello
stesso.
15
la shell in genere notifica comunque un avvertimento, avvertendo la presenza di processi bloccati grazie all’uso
di waitpid.
10.1. L’INTERAZIONE CON I TERMINALI 311
Questa è la situazione in cui si ha quello che viene chiamato un orphaned process group. Lo
standard POSIX.1 lo definisce come un process group i cui processi hanno come padri esclusiva-
mente 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 definizione può sembrare complicata, e a prima vista non è chiaro cosa tutto ciò abbia
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 definizione quest’ultimo è sempre orfano, dato che il padre del leader di
sessione è fuori dalla stessa e il nuovo process group contiene solo il leader di sessione. Questo è
un caso limite, e non viene emesso nessun segnale perché quanto previsto dallo standard riguarda
solo i raggruppamenti che diventano orfani in seguito alla terminazione di un processo.16
Il leader di sessione provvederà a creare nuovi raggruppamenti che a questo punto non sono
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
sez. 3.2.3, tutti i suoi figli vengono adottati da init, che è fuori dalla sessione. Questo renderà
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 l’esecuzione, ed
essendo stato nel frattempo inviato anche SIGHUP, se non c’è un gestore per quest’ultimo, i
processi bloccati verranno automaticamente terminati.
16
l’emissione dei segnali infatti avviene solo nella fase di uscita del processo, come una delle operazioni legate
all’esecuzione di _exit, secondo quanto illustrato in sez. 3.2.3.
17
in generale nel caso di login via rete o di terminali lanciati dall’interfaccia grafica cambia anche il processo da
cui ha origine l’esecuzione della shell.
18
in realtà negli ultimi tempi questa situazione sta cambiando, e sono state proposte diversi possibili rimpiazzi
per il tradizionale init di System V, come upstart o systemd, ma per quanto trattato in questa sezione il risultato
finale non cambia, si avrà comunque il lancio di un programma che consenta l’accesso al terminale.
312 CAPITOLO 10. INTERFACCIA UTENTE: TERMINALI E SESSIONI DI LAVORO
Un terminale, che esso sia un terminale effettivo, 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 un’interfaccia comune su un apposito file 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
per lanciare una istanza di questo programma su un terminale, il tutto ripetuto per ciascuno
dei terminali che si hanno a disposizione (o per un certo numero di essi, nel caso delle console
virtuali), secondo quanto indicato dall’amministratore nel file di configurazione 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 effettuerà, qualora servano, ulteriori impostazioni.19 Alla fine il programma stamperà un
messaggio di benvenuto per poi porsi in attesa dell’immissione 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 opportuna-
mente costruito che contenga quanto necessario; ad esempio di solito viene opportunamente
inizializzata la variabile di ambiente TERM per identificare il terminale su cui si sta operando, a
beneficio dei programmi che verranno lanciati in seguito.
A sua volta login, che mantiene i privilegi di amministratore, usa il nome dell’utente per
effettuare una ricerca nel database degli utenti,20 e richiede una password. Se l’utente non esiste
o se la password non corrisponde21 la richiesta viene ripetuta un certo numero di volte dopo di
che login esce ed init provvede a rilanciare un’altra istanza di getty.
Se invece la password corrisponde login esegue chdir per impostare come directory di
lavoro la home directory dell’utente, cambia i diritti di accesso al terminale (con chown e chmod)
per assegnarne la titolarità all’utente ed al suo gruppo principale, assegnandogli al contempo
i diritti di lettura e scrittura.22 Inoltre il programma provvede a costruire gli opportuni valori
per le variabili di ambiente, come HOME, SHELL, ecc. Infine attraverso l’uso di setuid, setgid
19
ad esempio, come qualcuno si sarà accorto scrivendo un nome di login in maiuscolo, può effettuare la conver-
sione automatica dell’input in minuscolo, ponendosi in una modalità speciale che non distingue fra i due tipi di
caratteri (a beneficio di alcuni vecchi terminali che non supportavano le minuscole).
20
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.
21
il confronto non viene effettuato 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.
22
oggi queste operazioni, insieme ad altre relative alla contabilità ed alla tracciatura degli accessi, vengono
gestite dalle distribuzioni più recenti in una maniera generica appoggiandosi a servizi di sistema come ConsoleKit,
ma il concetto generale resta sostanzialmente lo stesso.
10.1. L’INTERAZIONE CON I TERMINALI 313
e initgroups verrà cambiata l’identità del proprietario del processo, infatti, come spiegato in
sez. 3.3.2, avendo invocato tali funzioni con i privilegi di amministratore, tutti gli user-ID ed i
group-ID (reali, effettivi e salvati) saranno impostati a quelli dell’utente.
A questo punto login provvederà (fatte salve eventuali altre azioni iniziali, come la stampa
di messaggi di benvenuto o il controllo della posta) ad eseguire con un’altra exec la shell, che
si troverà con un ambiente già pronto con i file standard di sez. 6.1.2 impostati sul terminale,
e pronta, nel ruolo di leader di sessione e di processo di controllo per il terminale, a gestire
l’esecuzione dei comandi come illustrato in sez. 10.1.1.
Dato che il processo padre resta sempre init quest’ultimo potrà provvedere, ricevendo un
SIGCHLD all’uscita della shell quando la sessione di lavoro è terminata, a rilanciare getty sul
terminale per ripetere da capo tutto il procedimento.
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 file aperti che non servono più (in generale tutti); in particolare vanno
chiusi i file standard che di norma sono ancora associati al terminale (un’altra opzione è
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 è:
#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.
La funzione esegue una fork, per uscire subito, con _exit, nel padre, mentre l’esecuzione
prosegue nel figlio che esegue subito una setsid. In questo modo si compiono automaticamente
i passi 1 e 2 della precedente lista. Se nochdir è nullo la funzione imposta anche la directory di
lavoro su /, se noclose è nullo i file standard vengono rediretti su /dev/null (corrispondenti ai
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
come fare per la notifica di eventuali errori, non potendosi più utilizzare lo standard error; per il
normale I/O infatti ciascun demone avrà le sue modalità di interazione col sistema e gli utenti
a seconda dei compiti e delle funzionalità che sono previste; ma gli errori devono normalmente
essere notificati all’amministratore del sistema.
Una soluzione può essere quella di scrivere gli eventuali messaggi su uno specifico file (cosa
che a volte viene fatta comunque) ma questo comporta il grande svantaggio che l’amministratore
dovrà tenere sotto controllo un file diverso per ciascun demone, e che possono anche generarsi
conflitti 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 permette ai demoni di inviare messaggi all’amministratore
in una maniera standardizzata.
Il servizio prevede vari meccanismi di notifica, e, come ogni altro servizio in un sistema unix-
like, viene gestito attraverso un apposito programma, che è anch’esso un demone. In generale i
messaggi di errore vengono raccolti dal file speciale /dev/log, un socket locale (vedi sez. 15.3.4)
dedicato a questo scopo, o via rete, con un socket UDP e trattati dal demone che gestisce il
servizio. Il più comune di questi è syslogd, che consente un semplice smistamento dei messaggi
sui file in base alle informazioni in esse presenti.24
Il servizio del syslog permette infatti di trattare i vari messaggi classificandoli attraverso
due indici; il primo, chiamato facility, suddivide in diverse categorie i messaggi in modo di
raggruppare quelli provenienti da operazioni che hanno attinenza fra loro, ed è organizzato in
sottosistemi (kernel, posta elettronica, demoni di stampa, ecc.). Il secondo, chiamato priority,
identifica l’importanza dei vari messaggi, e permette di classificarli e differenziare le modalità di
notifica degli stessi.
Il sistema del syslog attraverso il proprio demone di gestione provvede poi a riportare i
messaggi all’amministratore attraverso una serie differenti meccanismi come:
• ignorarli completamente.
le modalità con cui queste azioni vengono realizzate dipendono ovviamente dal demone che si
usa, per la gestione del quale si rimanda ad un testo di amministrazione di sistema.25
Le glibc definiscono una serie di funzioni standard con cui un processo può accedere in
maniera generica al servizio di syslog, che però funzionano solo localmente; se si vogliono inviare
i messaggi ad un altro sistema occorre farlo esplicitamente con un socket UDP, o utilizzare le
capacità di reinvio del servizio.
La prima funzione definita dall’interfaccia è openlog, che apre una connessione al servizio di
syslog; essa in generale non è necessaria per l’uso del servizio, ma permette di impostare alcuni
valori che controllano gli effetti delle chiamate successive; il suo prototipo è:
#include <syslog.h>
void openlog(const char *ident, int option, int facility)
Apre una connessione al sistema del syslog.
La funzione permette di specificare, tramite ident, l’identità di chi ha inviato il messaggio (di
norma si passa il nome del programma, come specificato da argv[0]); la stringa verrà preposta
all’inizio di ogni messaggio. Si tenga presente che il valore di ident che si passa alla funzione è
un puntatore, se la stringa cui punta viene cambiata lo sarà pure nei successivi messaggi, e se
viene cancellata i risultati potranno essere impredicibili, per questo è sempre opportuno usare
una stringa costante.
L’argomento facility permette invece di preimpostare per le successive chiamate l’omonimo
indice che classifica la categoria del messaggio. L’argomento è interpretato come una maschera
binaria, e pertanto è possibile inviare i messaggi su più categorie alla volta; i valori delle costanti
che identificano ciascuna categoria sono riportati in tab. 10.1, il valore di facility deve essere
specificato con un OR aritmetico.
Valore Significato
LOG_AUTH Messaggi relativi ad autenticazione e sicurezza, obsoleto,
è sostituito da LOG_AUTHPRIV.
LOG_AUTHPRIV Sostituisce LOG_AUTH.
LOG_CRON Messaggi dei demoni di gestione dei comandi program-
mati (cron e at).
LOG_DAEMON Demoni di sistema.
LOG_FTP Servizio FTP.
LOG_KERN Messaggi del kernel.
LOG_LOCAL0 Riservato all’amministratore per uso locale.
— ...
LOG_LOCAL7 Riservato all’amministratore per uso locale.
LOG_LPR Messaggi del sistema di gestione delle stampanti.
LOG_MAIL Messaggi del sistema di posta elettronica.
LOG_NEWS Messaggi del sistema di gestione delle news (USENET).
LOG_SYSLOG Messaggi generati dal demone di gestione del syslog.
LOG_USER Messaggi generici a livello utente.
LOG_UUCP Messaggi del sistema UUCP (Unix to Unix CoPy, ormai
in disuso).
L’argomento option serve invece per controllare il comportamento della funzione openlog e
delle modalità con cui le successive chiamate scriveranno i messaggi, esso viene specificato come
maschera binaria composta con un OR aritmetico di una qualunque delle costanti riportate in
tab. 10.2.
25
l’argomento è ad esempio coperto dal capitolo 3.2.3 si [3].
316 CAPITOLO 10. INTERFACCIA UTENTE: TERMINALI E SESSIONI DI LAVORO
Valore Significato
LOG_CONS Scrive sulla console in caso di errore nell’invio del
messaggio al sistema del syslog.
LOG_NDELAY Apre la connessione al sistema del syslog subito invece di
attendere l’invio del primo messaggio.
LOG_NOWAIT Non usato su Linux, su altre piattaforme non attende i
processi figli creati per inviare il messaggio.
LOG_ODELAY Attende il primo messaggio per aprire la connessione al
sistema del syslog.
LOG_PERROR Stampa anche su stderr (non previsto in POSIX.1-2001).
LOG_PID Inserisce nei messaggi il pid del processo chiamante.
La funzione che si usa per generare un messaggio è syslog, dato che l’uso di openlog è
opzionale, sarà quest’ultima a provvede a chiamare la prima qualora ciò non sia stato fatto (nel
qual caso il valore di ident è NULL). Il suo prototipo è:
#include <syslog.h>
void syslog(int priority, const char *format, ...)
Genera un messaggio di priorità priority.
Tabella 10.3: Valori possibili per l’indice di importanza del messaggio da specificare nell’argomento priority di
syslog.
Una funzione sostanzialmente identica a syslog, la cui sola differenza è prendere invece di una
lista esplicita di argomenti un unico argomento finale nella forma di una lista di argomenti passato
come va_list, utile qualora si ottengano questi nella invocazione di una funzione variadic (si
rammenti quanto visto in sez. 2.4.2), è vsyslog,27 il suo prototipo è:
26
le glibc, seguendo POSIX.1-2001, prevedono otto diverse priorità ordinate da 0 a 7, in ordine di importanza
decrescente; questo comporta che i tre bit meno significativi dell’argomento priority sono occupati da questo
valore, mentre i restanti bit più significativi vengono usati per specificare la facility.
27
la funzione è originaria di BSD e per utilizzarla deve essere definito _BSD_SOURCE.
10.1. L’INTERAZIONE CON I TERMINALI 317
#include <syslog.h>
void vsyslog(int priority, const char *format, va_list src)
Genera un messaggio di priorità priority.
La funzione restituisce il valore della maschera corrente, e se si passa un valore nullo per
mask la maschera corrente non viene modificata; in questo modo si può leggere il valore della
maschera corrente. Indicando un valore non nullo per mask la registrazione dei messaggi viene
disabilitata per tutte quelle priorità che non rientrano nella maschera. In genere il valore viene
impostato usando la macro LOG_MASK(p) dove p è una delle costanti di tab. 10.3. É inoltre
disponibile anche la macro LOG_UPTO(p) che permette di specificare automaticamente tutte le
priorità fino a quella indicata da p.
Una volta che si sia certi che non si intende registrare più nessun messaggio si può chiudere
esplicitamente la connessione al syslog con la funzione closelog, il cui prototipo è:
#include <syslog.h>
void closelog(void)
Chiude la connessione al syslog.
Figura 10.2: Definizione delle stringhe coi relativi valori numerici che indicano le priorità dei messaggi del kernel
(ripresa da linux/kernel.h).
se superano una certa priorità, in modo che sia possibile vederli anche in caso di blocco totale
del sistema (nell’assunzione che la console sia collegata).
In particolare la stampa dei messaggi sulla console è controllata dal contenuto del file
/proc/sys/kernel/printk (o con l’equivalente parametro di sysctl) che prevede quattro va-
lori numerici interi: il primo (console loglevel ) indica la priorità corrente oltre la quale vengono
stampati i messaggi sulla console, il secondo (default message loglevel ) la priorità di default as-
segnata ai messaggi che non ne hanno impostata una, il terzo (minimum console level ) il valore
minimo che si può assegnare al primo valore,31 ed il quarto (default console loglevel ) il valore di
default.32
Per la lettura dei messaggi del kernel e la gestione del relativo buffer circolare esiste una
apposita system call chiamata anch’essa syslog, ma dato il conflitto di nomi questa viene
rimappata su un’altra funzione di libreria, in particolare nelle glibc essa viene invocata tramite
la funzione klogctl,33 il cui prototipo è:
#include <sys/klog.h>
int klogctl(int op, char *buffer, int len)
Gestisce i messaggi di log del kernel.
La funzione prevede che si passi come primo argomento op un codice numerico che indica l’o-
perazione richiesta, il secondo argomento deve essere, per le operazioni che compiono una lettura
di dati, l’indirizzo del buffer su cui copiarli, ed il terzo quanti leggerne. L’effettivo uso di que-
sti due argomenti dipende comunque dall’operazione richiesta, ma essi devono essere comunque
specificati, anche quando non servono, nel qual caso verranno semplicemente ignorati.
Si sono riportati in tab. 10.4 i possibili valori utilizzabili per op, con una breve spiegazione
della relativa operazione e a come vengono usati gli altri due argomenti. Come si può notare la
funzione è una sorta di interfaccia comune usata per eseguire operazioni completamente diverse
fra loro.
31
quello che può essere usato con una delle operazioni di gestione che vedremo a breve per “silenziare” il kernel.
32
anch’esso viene usato nelle operazioni di controllo per tornare ad un valore predefinito.
33
nelle libc4 e nelle libc5 la funzione invece era SYS_klog.
10.1. L’INTERAZIONE CON I TERMINALI 319
Valore Significato
0 apre il log (attualmente non fa niente), buffer e len sono ignorati.
1 chiude il log (attualmente non fa niente), buffer e len sono ignorati.
2 legge len byte nel buffer buffer dal log dei messaggi.
3 legge len byte nel buffer buffer dal buffer circolare dei messaggi.
4 legge len byte nel buffer buffer dal buffer circolare dei messaggi e lo
svuota.
5 svuota il buffer circolare dei messaggi, buffer e len sono ignorati.
6 disabilita la stampa dei messaggi sulla console, buffer e len sono
ignorati.
7 abilita la stampa dei messaggi sulla console, buffer e len sono ignorati.
8 imposta a len il livello dei messaggi stampati sulla console, buffer è
ignorato.
9 ritorna il numero di byte da leggere presenti sul buffer di log, buffer e
len sono ignorati (dal kernel 2.4.10).
10 ritorna la dimensione del buffer di log, buffer e len sono ignorati (dal
kernel 2.6.6).
e 5) sono privilegiate; fino al kernel 2.6.30 era richiesta la capacità CAP_SYS_ADMIN, a partire dal
2.6.38 detto privilegio è stato assegnato ad una capacità aggiuntiva, CAP_SYSLOG. Tutto questo è
stato fatto per evitare che processi eseguiti all’interno di un sistema di virtualizzazione “leggera”
(come i Linux Container di LXC) che necessitano di CAP_SYS_ADMIN per operare all’interno del
proprio ambiente ristretto, potessero anche avere la capacità di influire sui log del kernel al di
fuori di questo.
10.2.1 L’architettura
I terminali sono una classe speciale di dispositivi a caratteri (si ricordi la classificazione 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 l’interazione con un utente (deve essere cioè in
grado di fare da terminale di controllo per una sessione), che comporta la presenza di ulteriori
capacità.
L’interfaccia per i terminali è una delle più oscure e complesse, essendosi stratificata dagli
inizi dei sistemi Unix fino ad oggi. Questo comporta una grande quantità di opzioni e controlli
relativi ad un insieme di caratteristiche (come ad esempio la velocità della linea) necessarie per
dispositivi, come i terminali seriali, che al giorno d’oggi sono praticamente in disuso.
Storicamente i primi terminali erano appunto terminali di telescriventi (teletype), da cui
deriva sia il nome dell’interfaccia, TTY, che quello dei relativi file di dispositivo, che sono sempre
della forma /dev/tty*.37 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).
L’I/O sui terminali si effettua con le stesse modalità dei file normali: si apre il relativo file
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
write scriverà sullo schermo. In realtà questo è vero solo a grandi linee, perché non tiene conto
delle caratteristiche specifiche dei terminali; una delle principali infatti è che essi prevedono
due modalità di operazione, dette rispettivamente “modo canonico” e “modo non canonico”, che
hanno dei comportamenti nettamente diversi.
La modalità preimpostata all’apertura del terminale è quella canonica, in cui le operazioni
di lettura vengono sempre effettuate assemblando i dati in una linea;38 ed in cui alcuni carat-
teri vengono interpretati per compiere operazioni (come la generazione dei segnali illustrata in
sez. 9.2.6), questa di norma è la modalità in cui funziona la shell.
Un terminale in modo non canonico invece non effettua nessun accorpamento dei dati in linee
né li interpreta; esso viene di solito usato dai programmi (gli editor ad esempio) che necessitano
di poter leggere un carattere alla volta e che gestiscono al loro interno i vari comandi.
37
ciò vale solo in parte per i terminali virtuali, essi infatti hanno due lati, un master, che può assumere i nomi
/dev/pty[p-za-e][0-9a-f] ed un corrispondente slave con nome /dev/tty[p-za-e][0-9a-f].
38
per cui eseguendo una read su un terminale in modo canonico la funzione si bloccherà, anche se si sono scritti
dei caratteri, fintanto che non si preme il tasto di ritorno a capo: a questo punto la linea sarà completa e la
funzione ritornerà.
10.2. L’I/O SU TERMINALE 321
Per capire le caratteristiche dell’I/O sui terminali, occorre esaminare le modalità con cui esso
viene effettuato; l’accesso, come per tutti i dispositivi, viene gestito da un driver apposito, la cui
struttura generica è mostrata in fig. 10.3. Ad un terminale sono sempre associate due code per
gestire l’input e l’output, che ne implementano una bufferizzazione all’interno del kernel.39
La coda di ingresso mantiene i caratteri che sono stati letti dal terminale ma non ancora
letti da un processo, la sua dimensione è definita dal parametro di sistema MAX_INPUT (si veda
sez. 8.1.3), che ne specifica il limite minimo, in realtà la coda può essere più grande e cambiare
dimensione dinamicamente. Se è stato abilitato il controllo di flusso in ingresso il driver emette
i caratteri di STOP e START per bloccare e sbloccare l’ingresso 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) sull’output quando si
eccedono le dimensioni della coda. Se è abilitato il modo canonico i caratteri in ingresso restano
nella coda fintanto che non viene ricevuto un a capo; un altro parametro del sistema, MAX_CANON,
specifica la dimensione massima di una riga in modo canonico.
La coda di uscita è analoga a quella di ingresso e contiene i caratteri scritti dai processi ma
non ancora inviati al terminale. Se è abilitato il controllo di flusso in uscita il driver risponde ai
caratteri di START e STOP inviati dal terminale. Le dimensioni della coda non sono specificate,
ma non hanno molta importanza, in quanto qualora esse vengano eccedute il driver provvede
automaticamente a bloccare la funzione chiamante.
#include <unistd.h>
int isatty(int fd)
Controlla se il file descriptor fd è un terminale.
ma si tenga presente che la funzione restituisce un indirizzo di dati statici, che pertanto possono
essere sovrascritti da successive chiamate.
Della funzione esiste anche una versione rientrante, ttyname_r, che non presenta il problema
dell’uso di una zona di memoria statica; il suo prototipo è:
#include <unistd.h>
int ttyname_r(int fd, char *buff, size_t len)
Restituisce il nome del terminale associato a fd.
La funzione restituisce 0 in caso di successo e -1 in caso di errore, nel qual caso errno assumerà i
valori:
ERANGE la lunghezza del buffer len non è sufficiente per contenere la stringa restituita.
oltre ai precedenti EBADF ed ENOTTY.
La funzione prende due argomenti in più, il puntatore buff alla zona di memoria in cui
l’utente vuole che il risultato venga scritto, che dovrà essere stata allocata in precedenza, e la
relativa dimensione, len. Se la stringa che deve essere restituita, compreso lo zero di terminazione
finale, eccede questa dimensione si avrà una condizione di errore.
Una funzione funzione analoga alle precedenti anch’essa prevista da POSIX.1, che restituisce
sempre il nome di un file di dispositivo, è ctermid, il cui prototipo è:
#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 o NULL se
non non riesce ad eseguire l’operazione.
scritta su di essa, ma in questo caso il buffer preallocato deve essere di almeno L_ctermid40
caratteri.
Si tenga presente che il pathname restituito dalla funzione potrebbe non identificare univo-
camente il terminale (ad esempio potrebbe essere /dev/tty), inoltre non è detto che il processo
possa effettivamente essere in grado di aprire il terminale.
I vari attributi associati ad un terminale vengono mantenuti per ciascuno di essi in una
struttura termios che viene usata dalle varie funzioni dell’interfaccia. In fig. 10.4 si sono riportati
tutti i campi della definizione di questa struttura 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.41
struct termios {
tcflag_t c_iflag ; /* input mode flagss */
tcflag_t c_oflag ; /* output modes flags */
tcflag_t c_cflag ; /* control modes flags */
tcflag_t c_lflag ; /* local modes flags */
cc_t c_line ; /* line discipline */
cc_t c_cc [ NCCS ]; /* control characters */
speed_t c_ispeed ; /* input speed */
speed_t c_ospeed ; /* output speed */
};
I primi quattro campi sono quattro flag che controllano il comportamento del terminale; essi
sono realizzati come maschera binaria, pertanto il tipo tcflag_t è di norma realizzato con un
intero senza segno di lunghezza opportuna. I valori devono essere specificati bit per bit, avendo
cura di non modificare i bit su cui non si interviene.
Il primo flag, mantenuto nel campo c_iflag, è detto flag di input e controlla le modalità di
funzionamento dell’input dei caratteri sul terminale, come il controllo di parità, il controllo di
flusso, la gestione dei caratteri speciali; un elenco dei vari bit, del loro significato e delle costanti
utilizzate per identificarli è riportato in tab. 10.5.
Si noti come alcuni di questi flag (come quelli per la gestione del flusso) fanno riferimento
a delle caratteristiche che ormai sono completamente obsolete; la maggior parte inoltre è tipica
di terminali seriali, e non ha alcun effetto su dispositivi diversi come le console virtuali o gli
pseudo-terminali usati nelle connessioni di rete.
Il secondo flag, mantenuto nel campo c_oflag, è detto flag di output e controlla le modalità
di funzionamento dell’output dei caratteri, come l’impacchettamento dei caratteri sullo schermo,
la traslazione degli a capo, la conversione dei caratteri speciali; un elenco dei vari bit, del loro
significato e delle costanti utilizzate per identificarli è riportato in tab. 10.6, di questi solo OPOST
era previsto da POSIX.1, buona parte degli altri sono stati aggiunti in POSIX.1-2001, quelli
ancora assenti sono stati indicati esplicitamente.
Si noti come alcuni dei valori riportati in tab. 10.6 fanno riferimento a delle maschere di bit;
essi infatti vengono utilizzati per impostare alcuni valori numerici relativi ai ritardi nell’output
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.
40
L_ctermid è una delle varie costanti del sistema, non trattata esplicitamente in sez. 8.1 che indica la dimensione
che deve avere una stringa per poter contenere il nome di un terminale.
41
la definizione della struttura si trova in bits/termios.h, da non includere mai direttamente, Linux, seguendo
l’esempio di BSD, aggiunge i due campi c_ispeed e c_ospeed per mantenere le velocità delle linee seriali, ed un
campo ulteriore, c_line per indicare la disciplina di linea.
324 CAPITOLO 10. INTERFACCIA UTENTE: TERMINALI E SESSIONI DI LAVORO
Valore Significato
IGNBRK Ignora le condizioni di BREAK sull’input. Una condizione di BREAK
è definita nel contesto di una trasmissione seriale asincrona come una
sequenza di bit nulli più lunga di un byte.
BRKINT Controlla la reazione ad un BREAK quando IGNBRK non è impostato.
Se BRKINT è impostato il BREAK causa lo scarico delle code, e se il
terminale è il terminale di controllo per un gruppo in foreground anche
l’invio di SIGINT ai processi di quest’ultimo. Se invece BRKINT non è
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.
IGNPAR Ignora gli errori di parità, il carattere viene passato come ricevuto. Ha
senso solo se si è impostato INPCK.
PARMRK Controlla come vengono riportati gli errori di parità. Ha senso solo se
INPCK è impostato e IGNPAR no. Se impostato inserisce una sequenza
0xFF 0x00 prima di ogni carattere che presenta errori di parità, se non
impostato un carattere con errori di parità viene letto come uno 0x00.
Se un carattere ha il valore 0xFF e ISTRIP non è impostato, per evitare
ambiguità esso viene sempre riportato come 0xFF 0xFF.
INPCK Abilita il controllo di parità in ingresso. Se non viene impostato non
viene fatto nessun controllo ed i caratteri vengono passati in input
direttamente.
ISTRIP Se impostato i caratteri in input sono tagliati a sette bit mettendo a
zero il bit più significativo, altrimenti vengono passati tutti gli otto bit.
INLCR Se impostato in ingresso il carattere di a capo (’\n’) viene
automaticamente trasformato in un ritorno carrello (’\r’).
IGNCR Se impostato il carattere di ritorno carrello (carriage return, ’\r’) viene
scartato dall’input. Può essere utile per i terminali che inviano entrambi
i caratteri di ritorno carrello e a capo (newline, ’\n’).
ICRNL Se impostato un carattere di ritorno carrello (’\r’) sul terminale viene
automaticamente trasformato in un a capo (’\n’) sulla coda di input.
IUCLC Se impostato trasforma i caratteri maiuscoli dal terminale in minuscoli
sull’ingresso (opzione non POSIX).
IXON Se impostato attiva il controllo di flusso in uscita con i caratteri di
START e STOP. se si riceve uno STOP l’output 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.
IXANY Se impostato con il controllo di flusso permette a qualunque carattere
di far ripartire l’output bloccato da un carattere di STOP.
IXOFF Se impostato abilita il controllo di flusso in ingresso. Il computer emette
un carattere di STOP per bloccare l’input dal terminale e lo sblocca
con il carattere START.
IMAXBEL 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
impostato (è una estensione BSD).
IUTF8 Indica che l’input è in UTF-8, cosa che consente di utilizzare la cancel-
lazione dei caratteri in maniera corretta (dal kernel 2.6.4 e non previsto
in POSIX).
Tabella 10.5: Costanti identificative dei vari bit del flag di controllo c_iflag delle modalità di input di un
terminale.
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:
c_oflag &= (~ CRDLY );
c_oflag |= CR1 ;
10.2. L’I/O SU TERMINALE 325
Valore Significato
OPOST Se impostato i caratteri vengono convertiti opportunamente (in maniera
dipendente dall’implementazione) per la visualizzazione sul terminale,
ad esempio al carattere di a capo (NL) può venire aggiunto un ritorno
carrello (CR).
OLCUC Se impostato trasforma i caratteri minuscoli in ingresso in caratteri
maiuscoli sull’uscita (non previsto da POSIX).
ONLCR Se impostato converte automaticamente il carattere di a capo (NL) in
un carattere di ritorno carrello (CR).
OCRNL Se impostato converte automaticamente il carattere di a capo (NL)
nella coppia di caratteri ritorno carrello, a capo (CR-NL).
ONOCR Se impostato converte il carattere di ritorno carrello (CR) nella coppia
di caratteri CR-NL.
ONLRET Se impostato rimuove dall’output il carattere di ritorno carrello (CR).
OFILL Se impostato in caso di ritardo sulla linea invia dei caratteri di
riempimento invece di attendere.
OFDEL Se impostato il carattere di riempimento è DEL (0x3F), invece che NUL
(0x00), (non previsto da POSIX e non implementato su Linux).
NLDLY Maschera per i bit che indicano il ritardo per il carattere di a capo
(NL), i valori possibili sono NL0 o NL1.
CRDLY Maschera per i bit che indicano il ritardo per il carattere ritorno carrello
(CR), i valori possibili sono CR0, CR1, CR2 o CR3.
TABDLY Maschera per i bit che indicano il ritardo per il carattere di tabulazione,
i valori possibili sono TAB0, TAB1, TAB2 o TAB3.
BSDLY Maschera per i bit che indicano il ritardo per il carattere di ritorno
indietro (backspace), i valori possibili sono BS0 o BS1.
VTDLY Maschera per i bit che indicano il ritardo per il carattere di tabulazione
verticale, i valori possibili sono VT0 o VT1.
FFDLY Maschera per i bit che indicano il ritardo per il carattere di pagina
nuova (form feed ), i valori possibili sono FF0 o FF1.
Tabella 10.6: Costanti identificative dei vari bit del flag di controllo c_oflag delle modalità di output di un
terminale.
che prima cancella i bit della maschera in questione e poi setta il valore.
Il terzo flag, mantenuto nel campo c_cflag, è detto flag di controllo ed è legato al funziona-
mento delle linee seriali, permettendo di impostarne varie caratteristiche, come il numero di bit
di stop, le impostazioni della parità, il funzionamento del controllo di flusso; esso ha senso solo
per i terminali connessi a linee seriali. Un elenco dei vari bit, del loro significato e delle costanti
utilizzate per identificarli è riportato in tab. 10.7.
I valori di questo flag sono molto specifici, e completamente indirizzati al controllo di un
terminale che opera attraverso una linea seriale; essi pertanto non hanno nessuna rilevanza per
i terminali che usano un’altra interfaccia fisica, come le console virtuali e gli pseudo-terminali
usati dalle connessioni di rete.
Inoltre alcuni valori di questi flag sono previsti solo per quelle implementazioni (lo standard
POSIX non specifica nulla riguardo l’implementazione, ma solo delle funzioni di lettura e scrit-
tura) che mantengono le velocità delle linee seriali all’interno dei flag; come accennato in Linux
questo viene fatto (seguendo l’esempio di BSD) attraverso due campi aggiuntivi, c_ispeed e
c_ospeed, nella struttura termios (mostrati in fig. 10.4).
Il quarto flag, mantenuto nel campo c_lflag, è detto flag locale, e serve per controllare il
funzionamento dell’interfaccia fra il driver e l’utente, come abilitare l’eco, gestire i caratteri di
controllo e l’emissione dei segnali, impostare modo canonico o non canonico; un elenco dei vari
bit, del loro significato e delle costanti utilizzate per identificarli è riportato in tab. 10.8. Con i
terminali odierni l’unico flag con cui probabilmente si può avere a che fare è questo, in quanto
è con questo che si impostano le caratteristiche generiche comuni a tutti i terminali.
Si tenga presente che i flag che riguardano le modalità di eco dei caratteri (ECHOE, ECHOPRT,
326 CAPITOLO 10. INTERFACCIA UTENTE: TERMINALI E SESSIONI DI LAVORO
Valore Significato
CBAUD Maschera dei bit (4+1) usati per impostare della velocità della linea
(il baud rate) in ingresso; non è presente in POSIX ed in Linux non è
implementato in quanto viene usato un apposito campo di termios.
CBAUDEX Bit aggiuntivo per l’impostazione della velocità della linea, non è
presente in POSIX e per le stesse motivazioni del precedente non è
implementato in Linux.
CSIZE Maschera per i bit usati per specificare 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.
CSTOPB Se impostato vengono usati due bit di stop sulla linea seriale, se non
impostato ne viene usato soltanto uno.
CREAD Se è impostato si può leggere l’input del terminale, altrimenti i caratteri
in ingresso vengono scartati quando arrivano.
PARENB Se impostato abilita la generazione il controllo di parità. La reazione
in caso di errori dipende dai relativi valori per c_iflag, riportati in
tab. 10.5. Se non è impostato i bit di parità non vengono generati e i
caratteri non vengono controllati.
PARODD Ha senso solo se è attivo anche PARENB. Se impostato viene usata una
parità è dispari, altrimenti viene usata una parità pari.
HUPCL Se è impostato viene distaccata la connessione del modem quando l’ul-
timo dei processi che ha ancora un file aperto sul terminale lo chiude o
esce.
LOBLK Se impostato blocca l’output su un layer di shell non corrente, non è
presente in POSIX e non è implementato da Linux.
CLOCAL Se impostato indica che il terminale è connesso in locale e che le linee
di controllo del modem devono essere ignorate. Se non impostato effet-
tuando una chiamata ad open senza aver specificato il flag di O_NOBLOCK
si bloccherà il processo finché non si è stabilita una connessione con il
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 file e la
scrittura un errore di EIO.
CIBAUD Maschera dei bit della velocità della linea in ingresso; analogo a CBAUD,
non è previsto da POSIX e non è implementato in Linux dato che è
mantenuto in un apposito campo di termios.
CMSPAR imposta un bit di parità costante: se PARODD è impostato la parità è
sempre 1 (MARK ) se non è impostato la parità è sempre 0 (SPACE ),
non è previsto da POSIX.
CRTSCTS Abilita il controllo di flusso hardware sulla seriale, attraverso l’utilizzo
delle dei due fili di RTS e CTS.
Tabella 10.7: Costanti identificative dei vari bit del flag di controllo c_cflag delle modalità di controllo di un
terminale.
Valore Significato
ISIG Se impostato abilita il riconoscimento dei caratteri INTR, QUIT, e
SUSP generando il relativo segnale.
ICANON Se impostato il terminale opera in modo canonico, altrimenti opera in
modo non canonico.
XCASE Se impostato il terminale funziona solo con le maiuscole. L’input è
convertito in minuscole tranne per i caratteri preceduti da una “\”. In
output le maiuscole sono precedute da una “\” e le minuscole convertite
in maiuscole. Non è presente in POSIX.
ECHO Se è impostato viene attivato l’eco dei caratteri in input sull’output del
terminale.
ECHOE Se è impostato l’eco mostra la cancellazione di un carattere in input
(in reazione al carattere ERASE) cancellando l’ultimo carattere della
riga corrente dallo schermo; altrimenti il carattere è rimandato in eco
per mostrare quanto accaduto (usato per i terminali con l’uscita su una
stampante).
ECHOK Se impostato abilita il trattamento della visualizzazione del caratte-
re KILL, andando a capo dopo aver visualizzato lo stesso, altrimenti
viene solo mostrato il carattere e sta all’utente ricordare che l’input
precedente è stato cancellato.
ECHONL Se impostato viene effettuato l’eco di un a capo (\n) anche se non è
stato impostato ECHO.
ECHOCTL 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). Non è presente in POSIX.
ECHOPRT Se impostato abilita la visualizzazione del carattere di cancellazione in
una modalità adatta ai terminali con l’uscita su stampante; l’invio del
carattere di ERASE comporta la stampa di un “|” seguito dal carattere
cancellato, e cosı̀ via in caso di successive cancellazioni, quando si ri-
prende ad immettere carattere normali prima verrà stampata una “/”.
Non è presente in POSIX.
ECHOKE Se impostato abilita il trattamento della visualizzazione del carattere
KILL cancellando i caratteri precedenti nella linea secondo le modalità
specificate dai valori di ECHOE e ECHOPRT. Non è presente in POSIX.
DEFECHO Se impostato effettua l’eco solo se c’è un processo in lettura. Non è
presente in POSIX e non è supportato da Linux.
FLUSHO Effettua la cancellazione della coda di uscita. Viene attivato dal ca-
rattere DISCARD. Non è presente in POSIX e non è supportato da
Linux.
NOFLSH Se impostato disabilita lo scarico delle code di ingresso e uscita quando
vengono emessi i segnali SIGINT, SIGQUIT e SIGSUSP.
TOSTOP Se abilitato, con il supporto per il job control presente, genera il se-
gnale SIGTTOU per un processo in background che cerca di scrivere sul
terminale.
PENDIN Indica che la linea deve essere ristampata, viene attivato dal carattere
REPRINT e resta attivo fino alla fine della ristampa. Non è presente
in POSIX e non è supportato in Linux.
IEXTEN Abilita alcune estensioni previste dalla implementazione. Deve essere
impostato perché caratteri speciali come EOL2, LNEXT, REPRINT e
WERASE possano essere interpretati.
Tabella 10.8: Costanti identificative dei vari bit del flag di controllo c_lflag delle modalità locali di un terminale.
di controllo, con le costanti e delle funzionalità associate è riportato in tab. 10.9, usando quelle
definizioni diventa possibile assegnare un nuovo carattere di controllo con un codice del tipo:
La maggior parte di questi caratteri (tutti tranne VTIME e VMIN) hanno effetto solo quando
328 CAPITOLO 10. INTERFACCIA UTENTE: TERMINALI E SESSIONI DI LAVORO
Tabella 10.9: Valori dei caratteri di controllo mantenuti nel campo c_cc della struttura termios.
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 finora lo standard POSIX
prevede due funzioni che utilizzano come argomento un puntatore ad una struttura termios
che sarà quella in cui andranno immagazzinate le impostazioni. Le funzioni sono tcgetattr e
tcsetattr ed il loro prototipo è:
#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:
EINTR la funzione è stata interrotta.
ed inoltre EBADF, ENOTTY ed EINVAL.
Le funzioni operano sul terminale cui fa riferimento il file descriptor fd utilizzando la struttu-
10.2. L’I/O SU TERMINALE 329
ra indicata dal puntatore termios_p per lo scambio dei dati. Si tenga presente che le impostazioni
sono associate al terminale e non al file descriptor; questo significa che se si è cambiata una im-
postazione un qualunque altro processo che apra lo stesso terminale, od un qualunque altro file
descriptor che vi faccia riferimento, vedrà le nuove impostazioni pur non avendo nulla a che fare
con il file descriptor che si è usato per effettuare i cambiamenti.
Questo significa che non è possibile usare file descriptor diversi per utilizzare automaticamen-
te il terminale in modalità diverse, se esiste una necessità di accesso differenziato di questo tipo
occorrerà cambiare esplicitamente la modalità tutte le volte che si passa da un file descriptor ad
un altro.
La funzione tcgetattr legge i valori correnti delle impostazioni di un terminale qualunque
nella struttura puntata da termios_p; tcsetattr invece effettua la scrittura delle impostazioni
e quando viene invocata sul proprio terminale di controllo può essere eseguita con successo solo
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
chiamante non abbia SIGTTOU ignorato o bloccato, nel qual caso l’operazione sarà eseguita.
La funzione tcsetattr prevede tre diverse modalità di funzionamento, specificabili attraverso
l’argomento optional_actions, che permette di stabilire come viene eseguito il cambiamento
delle impostazioni del terminale, i valori possibili sono riportati in tab. 10.10; 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 Significato
TCSANOW Esegue i cambiamenti in maniera immediata.
TCSADRAIN I cambiamenti vengono eseguiti dopo aver atteso che
tutto l’output presente sulle code è stato scritto.
TCSAFLUSH È identico a TCSADRAIN, ma in più scarta tutti i dati
presenti sulla coda di input.
Tabella 10.10: Possibili valori per l’argomento optional_actions della funzione tcsetattr.
Occorre infine tenere presente che tcsetattr ritorna con successo anche se soltanto uno dei
cambiamenti richiesti è stato eseguito. Pertanto se si effettuano più cambiamenti è buona norma
controllare con una ulteriore chiamata a tcgetattr che essi siano stati eseguiti tutti quanti.
Come già accennato per i cambiamenti effettuati ai vari flag di controllo occorre che i valori
di ciascun bit siano specificati 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
modificare i valori impostati.
In fig. 10.5 e fig. 10.6 si è riportato rispettivamente il codice delle due funzioni SetTermAttr e
UnSetTermAttr, che possono essere usate per impostare o rimuovere, con le dovute precauzioni,
un qualunque bit di c_lflag. Il codice completo di entrambe le funzioni può essere trovato nel
file SetTermAttr.c dei sorgenti allegati alla guida.
La funzione SetTermAttr provvede ad impostare il bit specificato dall’argomento flag; prima
si leggono i valori correnti (8) con tcgetattr, uscendo con un messaggio in caso di errore (9-10),
poi si provvede a impostare solo i bit richiesti (possono essere più di uno) con un OR binario
(12); infine si scrive il nuovo valore modificato con tcsetattr (13), notificando un eventuale
errore (14-15) o uscendo normalmente.
La seconda funzione, UnSetTermAttr, è assolutamente identica alla prima, solo che in questo
caso (9) si rimuovono i bit specificati dall’argomento flag usando un AND binario del valore
negato.
Al contrario di tutte le altre caratteristiche dei terminali, che possono essere impostate espli-
citamente utilizzando gli opportuni campi di termios, per le velocità della linea (il cosiddetto
baud rate) non è prevista una implementazione standardizzata, per cui anche se in Linux sono
330 CAPITOLO 10. INTERFACCIA UTENTE: TERMINALI E SESSIONI DI LAVORO
Figura 10.5: Codice della funzione SetTermAttr che permette di impostare uno dei flag di controllo locale del
terminale.
Figura 10.6: Codice della funzione UnSetTermAttr che permette di rimuovere uno dei flag di controllo locale del
terminale.
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
per la velocità della linea di ingresso e cfsetospeed per la velocità della linea di uscita; i loro
prototipi sono:
#include <unistd.h>
#include <termios.h>
int cfsetispeed(struct termios *termios_p, speed_t speed)
Imposta la velocità delle linee seriali in ingresso.
int cfsetospeed(struct termios *termios_p, speed_t speed)
Imposta la velocità delle linee seriali in uscita.
Entrambe le funzioni restituiscono 0 in caso di successo e -1 in caso di errore, che avviene solo
quando il valore specificato non è valido.
10.2. L’I/O SU TERMINALE 331
Si noti che le funzioni si limitano a scrivere opportunamente il valore della velocità prescelta
speed all’interno della struttura puntata da termios_p; per effettuare l’impostazione effettiva
occorrerà poi chiamare tcsetattr.
Si tenga presente che per le linee seriali solo alcuni valori di velocità sono validi; questi
possono essere specificati 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 specificare le velocità tipiche
delle linee seriali:
Un terminale può utilizzare solo alcune delle velocità possibili, le funzioni però non control-
lano se il valore specificato è valido, dato che non possono sapere a quale terminale le velo-
cità saranno applicate; sarà l’esecuzione di tcsetattr a fallire quando si cercherà di eseguire
l’impostazione. 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 quest’ultimo
non potrà funzionare: quando il terminale non è seriale il valore non influisce sulla velocità di
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
questo valore per la linea di input l’effetto sarà quello di rendere la sua velocità identica a quella
della linea di output.
Dato che in genere si imposta sempre la stessa velocità sulle linee di uscita e di ingresso è
supportata anche la funzione cfsetspeed, una estensione di BSD,43 il cui prototipo è:
#include <unistd.h>
#include <termios.h>
int cfsetspeed(struct termios *termios_p, speed_t speed)
Imposta la velocità delle linee seriali.
La funzione restituisce 0 in caso di successo e -1 in caso di errore, che avviene solo quando il valore
specificato non è valido.
la funzione è identica alle due precedenti ma imposta la stessa velocità sia per la linea di ingresso
che per quella di uscita.
Analogamente a quanto avviene per l’impostazione, le velocità possono essere lette da una
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.
speed_t cfgetospeed(struct termios *termios_p)
Legge la velocità delle linee seriali in uscita.
Entrambe le funzioni restituiscono la velocità della linea, non sono previste condizioni di errore.
Anche in questo caso le due funzioni estraggono i valori della velocità della linea da una
struttura, il cui indirizzo è specificato dall’argomento termios_p che deve essere stata letta in
precedenza con tcgetattr.
Infine sempre da BSD è stata ripresa una funzione che consente di impostare il teminale in
una modalità analoga all cosiddetta modalità “raw ” di System V, in cui i dati in input vengono
43
la funzione origina da 4.4BSD e richiede sua definita la macro _BSD_SOURCE.
332 CAPITOLO 10. INTERFACCIA UTENTE: TERMINALI E SESSIONI DI LAVORO
resi disponibili un carattere alla volta, e l’echo e tutte le interpretazioni dei caratteri in entrata
e uscita sono disabilitate. La funzione è cfmakeraw ed il suo prototipo è:
#include <unistd.h>
#include <termios.h>
void cfmakeraw(struct termios *termios_p)
Importa il terminale in modalità “raw ” alla System V.
La funzione imposta solo i valori in termios_p, e non sono previste condizioni di errore.
Anche in questo caso la funzione si limita a preparare i valori che poi saranno impostato con
una successiva chiamata a tcsetattr, in sostanza la funzione è equivalente a:
termios_p - > c_iflag &= ~( IGNBRK | BRKINT | PARMRK | ISTRIP
| INLCR | IGNCR | ICRNL | IXON );
termios_p - > c_oflag &= ~ OPOST ;
termios_p - > c_lflag &= ~( ECHO | ECHONL | ICANON | ISIG | IEXTEN );
termios_p - > c_cflag &= ~( CSIZE | PARENB );
termios_p - > c_cflag |= CS8 ;
La funzione restituisce 0 in caso di successo e -1 in caso di errore, nel qual caso errno assumerà i
valori EBADF o ENOTTY.
La funzione invia un flusso di bit nulli (che genera una condizione di break) sul terminale
associato a fd; un valore nullo di duration implica una durata del flusso fra 0.25 e 0.5 secondi,
un valore diverso da zero implica una durata pari a duration*T dove T è un valore compreso
fra 0.25 e 0.5.45
Le altre funzioni previste da POSIX servono a controllare il comportamento dell’interazione
fra le code associate al terminale e l’utente; la prima è tcdrain, il cui prototipo è:
#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
valori EBADF o ENOTTY.
44
con la stessa eccezione, già vista per tcsetattr, che quest’ultimo sia bloccato o ignorato dal processo
chiamante.
45
lo standard POSIX specifica il comportamento solo nel caso si sia impostato un valore nullo per duration; il
comportamento negli altri casi può dipendere dalla implementazione.
10.2. L’I/O SU TERMINALE 333
La funzione blocca il processo fino a che tutto l’output presente sulla coda di uscita non è
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 è:
#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
valori EBADF o ENOTTY.
La funzione agisce sul terminale associato a fd, l’argomento queue permette di specificare
su quale coda (ingresso, uscita o entrambe), operare. Esso può prendere i valori riportati in
tab. 10.11, nel caso si specifichi la coda di ingresso cancellerà i dati ricevuti ma non ancora letti,
nel caso si specifichi la coda di uscita cancellerà i dati scritti ma non ancora trasmessi.
Valore Significato
TCIFLUSH Cancella i dati sulla coda di ingresso.
TCOFLUSH Cancella i dati sulla coda di uscita.
TCIOFLUSH Cancella i dati su entrambe le code.
Tabella 10.11: Possibili valori per l’argomento queue della funzione tcflush.
L’ultima funzione dell’interfaccia che interviene sulla disciplina di linea è tcflow, che viene
usata per sospendere la trasmissione e la ricezione dei dati sul terminale; il suo prototipo è:
#include <unistd.h>
#include <termios.h>
int tcflow(int fd, int action)
Sospende e riavvia il flusso dei dati sul terminale.
La funzione restituisce 0 in caso di successo e -1 in caso di errore, nel qual caso errno assumerà i
valori EBADF o ENOTTY.
Tabella 10.12: Possibili valori per l’argomento action della funzione tcflow.
precedenti) di cancellare i caratteri, bloccare e riavviare il flusso 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ù l’input in linee, il sistema non ha più un limite definito per quando
ritornare i dati ad un processo. Per questo motivo abbiamo visto che in c_cc sono previsti due
caratteri speciali, MIN e TIME (specificati 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 è
passato un certo tempo.
Come accennato nella relativa spiegazione in tab. 10.9, TIME e MIN non sono in realtà
caratteri ma valori numerici. Il comportamento del sistema per un terminale in modalità non
canonica prevede quattro casi distinti:
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 l’altro. 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
possono essere restituiti di più); se invece TIME scade vengono restituiti i byte ricevuti
fino 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 significa che una read può bloccarsi indefinitamente.
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 è
possibile che read ritorni con un valore nullo.
MIN= 0, TIME= 0 In questo caso una read ritorna immediatamente restituendo tutti i ca-
ratteri ricevuti. Anche in questo caso può ritornare con un valore nullo.
#include <unistd.h>
int pipe(int filedes[2])
Crea una coppia di file descriptor associati ad una pipe.
La funzione restituisce zero in caso di successo e -1 per un errore, nel qual caso errno potrà
assumere i valori EMFILE, ENFILE e EFAULT.
La funzione restituisce la coppia di file descriptor nel vettore filedes; il primo è aperto
in lettura ed il secondo in scrittura. Come accennato concetto di funzionamento di una pipe
è semplice: quello che si scrive nel file descriptor aperto in scrittura viene ripresentato tale e
quale nel file descriptor aperto in lettura. I file descriptor infatti non sono connessi a nessun file
1
si tenga presente che le pipe sono oggetti creati dal kernel e non risiedono su disco.
335
336 CAPITOLO 11. L’INTERCOMUNICAZIONE FRA PROCESSI
reale, ma, come accennato in sez. 12.4.3, ad un buffer nel kernel, la cui dimensione è specificata
dal parametro di sistema PIPE_BUF, (vedi sez. 8.1.3). Lo schema di funzionamento di una pipe è
illustrato in fig. 11.1, in cui sono illustrati i due capi della pipe, associati a ciascun file descriptor,
con le frecce che indicano la direzione del flusso dei dati.
Chiaramente creare una pipe all’interno di un singolo processo non serve a niente; se però
ricordiamo quanto esposto in sez. 6.3.1 riguardo al comportamento dei file descriptor nei processi
figli, è immediato capire come una pipe possa diventare un meccanismo di intercomunicazione.
Un processo figlio infatti condivide gli stessi file descriptor del padre, compresi quelli associati
ad una pipe (secondo la situazione illustrata in fig. 11.2). In questo modo se uno dei processi
scrive su un capo della pipe, l’altro può leggere.
Figura 11.2: Schema dei collegamenti ad una pipe, condivisi fra processo padre e figlio dopo l’esecuzione fork.
Tutto ciò ci mostra come sia immediato realizzare un meccanismo di comunicazione fra
processi attraverso una pipe, utilizzando le proprietà ordinarie dei file, ma ci mostra anche qual
è il principale2 limite nell’uso delle pipe. È necessario infatti che i processi possano condividere i
file descriptor della pipe, e per questo essi devono comunque essere parenti (dall’inglese siblings),
cioè o derivare da uno stesso processo padre in cui è avvenuta la creazione della pipe, o, più
comunemente, essere nella relazione padre/figlio.
A differenza di quanto avviene con i file normali, la lettura da una pipe può essere bloccante
(qualora non siano presenti dati), inoltre se si legge da una pipe il cui capo in scrittura è stato
chiuso, si avrà la ricezione di un EOF (vale a dire che la funzione read ritornerà restituendo
0). Se invece si esegue una scrittura su una pipe il cui capo in lettura non è aperto il processo
2
Stevens in [1] riporta come limite anche il fatto che la comunicazione è unidirezionale, ma in realtà questo è
un limite facilmente superabile usando una coppia di pipe.
11.1. L’INTERCOMUNICAZIONE FRA PROCESSI TRADIZIONALE 337
riceverà il segnale SIGPIPE, e la funzione di scrittura restituirà un errore di EPIPE (al ritorno
del gestore, o qualora il segnale sia ignorato o bloccato).
La dimensione del buffer della pipe (PIPE_BUF) ci dà inoltre un’altra importante informazione
riguardo il comportamento delle operazioni di lettura e scrittura su di una pipe; esse infatti sono
atomiche fintanto che la quantità di dati da scrivere non supera questa dimensione. Qualora ad
esempio si effettui una scrittura di una quantità di dati superiore l’operazione verrà effettuata
in più riprese, consentendo l’intromissione di scritture effettuate da altri processi.
http://www.sito.it/cgi-bin/programma?argomento
ed il risultato dell’elaborazione 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
effettuato la richiesta, che in questo modo è in grado di visualizzarlo opportunamente.
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
stringa, mentre il secondo serve per poter effettuare la conversione della stessa immagine in
formato JPEG. Usando una pipe potremo inviare l’output del primo sull’input del secondo,
secondo lo schema mostrato in fig. 11.3, in cui la direzione del flusso dei dati è data dalle frecce
continue.
Figura 11.3: Schema dell’uso di una pipe come mezzo di comunicazione fra due processi attraverso l’esecuzione
una fork e la chiusura dei capi non utilizzati.
Si potrebbe obiettare che sarebbe molto più semplice salvare il risultato intermedio su un file
temporaneo. Questo però non tiene conto del fatto che un CGI deve poter gestire più richieste
in concorrenza, e si avrebbe una evidente race condition in caso di accesso simultaneo a detto
3
un CGI (Common Gateway Interface) è un programma che permette la creazione dinamica di un oggetto da
inserire all’interno di una pagina HTML.
338 CAPITOLO 11. L’INTERCOMUNICAZIONE FRA PROCESSI
file.4 L’uso di una pipe invece permette di risolvere il problema in maniera semplice ed elegante,
oltre ad essere molto più efficiente, dato che non si deve scrivere su disco.
Il programma ci servirà anche come esempio dell’uso delle funzioni di duplicazione dei file
descriptor che abbiamo trattato in sez. 6.3.4, in particolare di dup2. È attraverso queste funzioni
infatti che è possibile dirottare gli stream standard dei processi (che abbiamo visto in sez. 6.1.2
e sez. 7.1.3) sulla pipe. In fig. 11.4 abbiamo riportato il corpo del programma, il cui codice
completo è disponibile nel file BarCodePage.c che si trova nella directory dei sorgenti.
La prima operazione del programma (4-12) è quella di creare le due pipe che serviranno per la
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 dell’immagine richiesta.5
Una volta create le pipe, il programma può creare (13-17) il primo processo figlio, che si
incaricherà (19-25) di eseguire barcode. Quest’ultimo legge dallo standard input una stringa di
caratteri, la converte nell’immagine 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 file, qualora risulti aperto, viene, come
nel caso corrente, chiuso prima di effettuare la duplicazione. Allo stesso modo, dato che barcode
scrive l’immagine PostScript del codice a barre sullo standard output, per poter effettuare 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 all’esecuzione (25) di barcode (cui si passa in size la dimensione della
pagina per l’immagine) quest’ultimo leggerà dalla prima pipe la stringa da codificare che gli sarà
inviata dal padre, e scriverà l’immagine PostScript del codice a barre sulla seconda.
Al contempo una volta lanciato il primo figlio, 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 l’uso della
prima pipe da parte del padre è finito ed essa può essere definitivamente chiusa (28), si attende
poi (29) che l’esecuzione di barcode sia completata.
Alla conclusione della sua esecuzione barcode avrà inviato l’immagine PostScript del codice
a barre sul capo in scrittura della seconda pipe; a questo punto si può eseguire la seconda
conversione, da PS a JPEG, usando il programma gs. Per questo si crea (30-34) un secondo
processo figlio, che poi (35-42) eseguirà questo programma leggendo l’immagine PostScript creata
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
collega (38) il capo in lettura allo standard input. Per poter formattare l’output del programma
in maniera utilizzabile da un browser, si provvede anche 40) alla scrittura dell’apposita stringa di
identificazione del mime-type in testa allo standard output. A questo punto si può invocare 41)
gs, provvedendo gli appositi switch che consentono di leggere il file 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 figlio (45); a questo punto può (46) uscire. Si tenga conto
che l’operazione di chiudere il capo in scrittura della seconda pipe è necessaria, infatti, se non
venisse chiusa, gs, che legge il suo standard input da detta pipe, resterebbe bloccato in attesa di
4
il problema potrebbe essere superato determinando in anticipo un nome appropriato per il file temporaneo,
che verrebbe utilizzato dai vari sotto-processi, e cancellato alla fine della loro esecuzione; ma a questo punto le
cose non sarebbero più tanto semplici.
5
la funzione WriteMess non è riportata in fig. 11.4; essa si incarica semplicemente di formattare l’uscita al-
la maniera dei CGI, aggiungendo l’opportuno mime type, e formattando il messaggio in HTML, in modo che
quest’ultimo possa essere visualizzato correttamente da un browser.
11.1. L’INTERCOMUNICAZIONE FRA PROCESSI TRADIZIONALE 339
ulteriori dati in ingresso (l’unico modo che un programma ha per sapere che l’input è terminato
è rilevare che lo standard input è stato chiuso), e la wait non ritornerebbe.
Come si è visto la modalità più comune di utilizzo di una pipe è quella di utilizzarla per fare
da tramite fra output ed input di due programmi invocati in sequenza; per questo motivo lo
340 CAPITOLO 11. L’INTERCOMUNICAZIONE 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 è:
#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 l’indirizzo 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
e fork o EINVAL se type non è valido.
La funzione crea una pipe, esegue una fork, ed invoca il programma command attraverso la
shell (in sostanza esegue /bin/sh con il flag -c); l’argomento type deve essere una delle due
stringhe "w" o "r", per indicare se la pipe sarà collegata allo standard input o allo standard
output del comando invocato.
La funzione restituisce il puntatore allo stream associato alla pipe creata, che sarà aperto
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 effetti ai file stream visti in cap. 7, anche
se è collegato ad una pipe e non ad un file, e viene sempre aperto in modalità fully-buffered (vedi
sez. 7.1.4); l’unica differenza con gli usuali stream è che dovrà essere chiuso dalla seconda delle
due nuove funzioni, pclose, il cui prototipo è:
#include <stdio.h>
int pclose(FILE *stream)
Chiude il file 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 l’uso di queste due funzioni riprendiamo il problema precedente: il programma
mostrato in fig. 11.4 per quanto funzionante, è (volutamente) codificato in maniera piuttosto
complessa, inoltre nella pratica sconta un problema di gs che non è in grado6 di riconoscere
correttamente l’Encapsulated 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 diver-
so. Una possibilità sarebbe quella di ricorrere ad ulteriore programma, epstopsf, per convertire
in PDF un file EPS (che può essere generato da barcode utilizzando lo switch -E). Utilizzando
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.
Per poter effettuare la conversione di un PDF infatti è necessario, per la struttura del formato,
potersi spostare (con lseek) all’interno del file da convertire; se si esegue la conversione con
gs su un file regolare non ci sono problemi, una pipe però è rigidamente sequenziale, e l’uso di
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
tutti prevedono una lettura sequenziale del loro input.
6
nella versione GNU Ghostscript 6.53 (2002-02-13).
11.1. L’INTERCOMUNICAZIONE FRA PROCESSI TRADIZIONALE 341
Per questo motivo si è dovuto utilizzare un procedimento diverso, eseguendo prima la con-
versione (sempre con gs) del PS in un altro formato intermedio, il PPM,7 dal quale poi si
può ottenere un’immagine di dimensioni corrette attraverso vari programmi di manipolazione
(pnmcrop, pnmmargin) che può essere infine trasformata in PNG (con pnm2png).
In questo caso però occorre eseguire in sequenza ben quattro comandi diversi, inviando
l’output di ciascuno all’input del successivo, per poi ottenere il risultato finale sullo standard
output: un caso classico di utilizzazione delle pipe, in cui l’uso di popen e pclose permette di
semplificare 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
è riportato in fig. 11.5. Come si può notare l’ordine di invocazione dei programmi è l’inverso
di quello in cui ci si aspetta che vengano effettivamente eseguiti. Questo non comporta nessun
problema dato che la lettura su una pipe è bloccante, per cui ciascun processo, per quanto lanciato
per primo, si bloccherà in attesa di ricevere sullo standard input il risultato dell’elaborazione del
precedente, benché quest’ultimo venga invocato dopo.
Nel nostro caso il primo passo (14) è scrivere il mime-type sullo standard output; a questo
punto il processo padre non necessita più di eseguire ulteriori operazioni sullo standard output
e può tranquillamente provvedere alla redirezione.
7
il Portable PixMap file format è un formato usato spesso come formato intermedio per effettuare conversioni,
è infatti molto facile da manipolare, dato che usa caratteri ASCII per memorizzare le immagini, anche se per
questo è estremamente inefficiente.
342 CAPITOLO 11. L’INTERCOMUNICAZIONE FRA PROCESSI
Dato che i vari programmi devono essere lanciati in successione, si è approntato un ciclo
(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.
In questo modo il primo processo ad essere invocato (che è l’ultimo della catena) scriverà
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 fine tutto quello che resta da fare è lanciare (21) il primo processo della catena, che nel
caso è barcode, e scrivere (23) la stringa del codice a barre sulla pipe, che è collegata al suo
standard input, infine si può eseguire (24-27) un ciclo che chiuda, nell’ordine inverso rispetto a
quello in cui le si sono create, tutte le pipe create con pclose.
• Da parte dei comandi di shell, per evitare la creazione di file temporanei quando si devono
inviare i dati di uscita di un processo sull’input di parecchi altri (attraverso l’uso del
comando tee).
• Come canale di comunicazione fra client ed server (il modello client-server è illustrato in
sez. 14.1.1).
Nel primo caso quello che si fa è creare tante fifo, da usare come standard input, quanti sono i
processi a cui i vogliono inviare i dati, questi ultimi saranno stati posti in esecuzione ridirigendo
lo standard input dalle fifo, si potrà poi eseguire il processo che fornisce l’output replicando
quest’ultimo, con il comando tee, sulle varie fifo.
Il secondo caso è relativamente semplice qualora si debba comunicare con un processo alla
volta (nel qual caso basta usare due fifo, una per leggere ed una per scrivere), le cose diventano
invece molto più complesse quando si vuole effettuare una comunicazione fra il server ed un
numero imprecisato di client; se il primo infatti può ricevere le richieste attraverso una fifo
“nota”, per le risposte non si può fare altrettanto, dato che, per la struttura sequenziale delle
fifo, i client dovrebbero sapere, prima di leggerli, quando i dati inviati sono destinati a loro.
Per risolvere questo problema, si può usare un’architettura come quella illustrata in fig. 11.6
in cui i client inviano le richieste al server su una fifo nota mentre le risposte vengono reinviate
dal server a ciascuno di essi su una fifo temporanea creata per l’occasione.
Figura 11.6: Schema dell’utilizzo delle fifo nella realizzazione di una architettura di comunicazione client/server.
Come esempio di uso questa architettura e dell’uso delle fifo, 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 dell’insieme, che i file da cui esse vengono lette all’avvio, sono importabili
da riga di comando. Il corpo principale del server è riportato in fig. 11.7, dove si è tralasciata
la parte che tratta la gestione delle opzioni a riga di comando, che effettua il settaggio delle
variabili fortunefilename, che indica il file 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 file
FortuneServer.c.
Il server richiede (12) che sia stata impostata una dimensione dell’insieme delle frasi non
nulla, dato che l’inizializzazione del vettore fortune avviene solo quando questa dimensione
344 CAPITOLO 11. L’INTERCOMUNICAZIONE FRA PROCESSI
Figura 11.7: Sezione principale del codice del server di fortunes basato sulle fifo.
viene specificata, la presenza di un valore nullo provoca l’uscita dal programma attraverso la
funzione (non riportata) che ne stampa le modalità d’uso. Dopo di che installa (13-15) la funzione
che gestisce i segnali di interruzione (anche questa non è riportata in fig. 11.7) che si limita a
11.1. L’INTERCOMUNICAZIONE FRA PROCESSI TRADIZIONALE 345
Figura 11.8: Sezione principale del codice del client di fortunes basato sulle fifo.
della fifo da utilizzare per la risposta. Infine si richiude la fifo del server che a questo punto non
serve più (25).
Inoltrata la richiesta si può passare alla lettura della risposta; anzitutto si apre (26-30) la
fifo appena creata, da cui si deve riceverla, dopo di che si effettua una lettura (31) nell’apposito
buffer; si è supposto, come è ragionevole, che le frasi inviate dal server siano sempre di dimensioni
inferiori a PIPE_BUF, tralasciamo la gestione del caso in cui questo non è vero. Infine si stampa
(32) a video la risposta, si chiude (33) la fifo e si cancella (34) il relativo file. Si noti come la
fifo 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 l’apertura si sarebbe bloccata indefinitamente.
Verifichiamo allora il comportamento dei nostri programmi, in questo, come in altri esem-
pi precedenti, si fa uso delle varie funzioni di servizio, che sono state raccolte nella libreria
libgapil.so, per poter usare quest’ultima occorrerà definire la speciale variabile di ambiente
LD_LIBRARY_PATH in modo che il linker dinamico possa accedervi.
In generale questa variabile indica il pathname della directory contenente la libreria. Nell’i-
potesi (che daremo sempre per verificata) che si facciano le prove direttamente nella directory
11.1. L’INTERCOMUNICAZIONE FRA PROCESSI TRADIZIONALE 347
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
una decina di frasi, con:
Avendo usato daemon per eseguire il server in background il comando ritornerà imme-
diatamente, ma potremo verificare con ps che in effetti il programma resta un esecuzione
in background, e senza avere associato un terminale di controllo (si ricordi quanto detto in
sez. 10.1.5):
e si potrà verificare anche che in /tmp è stata creata la fifo di ascolto fortune.fifo. A questo
punto potremo interrogare il server con il programma client; otterremo cosı̀:
e ripetendo varie volte il comando otterremo, in ordine casuale, le dieci frasi tenute in memoria
dal server.
Infine per chiudere il server basterà inviare un segnale di terminazione con killall fortuned
e potremo verificare che il gestore del segnale ha anche correttamente cancellato la fifo di ascolto
da /tmp.
Benché il nostro sistema client-server funzioni, la sua struttura è piuttosto complessa e con-
tinua ad avere vari inconvenienti12 ; in generale infatti l’interfaccia delle fifo non è adatta a
risolvere questo tipo di problemi, che possono essere affrontati in maniera più semplice ed effica-
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
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 fifo temporanee sul filesystem, di come sia necessario
intercettare SIGPIPE dato che un client può terminare dopo aver fatto una richiesta, ma prima che la risposta sia
inviata (cosa che nel nostro esempio non è stata fatta).
348 CAPITOLO 11. L’INTERCOMUNICAZIONE FRA PROCESSI
#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à
uno dei valori:
EAFNOSUPPORT i socket locali non sono supportati.
EPROTONOSUPPORT il protocollo specificato non è supportato.
EOPNOTSUPP il protocollo specificato 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 sull’altro e viceversa. Gli argomenti domain, type e
protocol derivano dall’interfaccia dei socket (vedi sez. 15.2) che è quella che fornisce il substrato
per connettere i due descrittori, ma in questo caso i soli valori validi che possono essere specificati
sono rispettivamente AF_UNIX, SOCK_STREAM e 0.
L’utilità di chiamare questa funzione per evitare due chiamate a pipe può sembrare limitata;
in realtà l’utilizzo di questa funzione (e dei socket locali in generale) permette di trasmettere
attraverso le linea non solo dei dati, ma anche dei file descriptor: si può cioè passare da un
processo ad un altro un file descriptor, con una sorta di duplicazione dello stesso non all’interno
di uno stesso processo, ma fra processi distinti (torneremo su questa funzionalità in sez. 18.2.1).
struct ipc_perm
{
key_t key ; /* Key . */
uid_t uid ; /* Owner ’s user ID . */
gid_t gid ; /* Owner ’s group ID . */
uid_t cuid ; /* Creator ’s user ID . */
gid_t cgid ; /* Creator ’s group ID . */
unsigned short int mode ; /* Read / write permission . */
unsigned short int seq ; /* Sequence number . */
};
Usando la stessa chiave due processi diversi possono ricavare l’identificatore associato ad un
oggetto ed accedervi. Il problema che sorge a questo punto è come devono fare per accordarsi
sull’uso di una stessa chiave. Se i processi sono imparentati la soluzione è relativamente semplice,
in tal caso infatti si può usare il valore speciale IPC_PRIVATE per creare un nuovo oggetto nel
processo padre, l’identificatore cosı̀ ottenuto sarà disponibile in tutti i figli, e potrà essere passato
come argomento attraverso una exec.
Però quando i processi non sono imparentati (come capita tutte le volte che si ha a che
fare con un sistema client-server) tutto questo non è possibile; si potrebbe comunque salvare
l’identificatore su un file noto, ma questo ovviamente comporta lo svantaggio di doverselo andare
15
in sostanza si sposta il problema dell’accesso dalla classificazione in base all’identificatore alla classificazione
in base alla chiave, una delle tante complicazioni inutili presenti nel SysV IPC.
350 CAPITOLO 11. L’INTERCOMUNICAZIONE FRA PROCESSI
a rileggere. Una alternativa più efficace è quella che i programmi usino un valore comune per
la chiave (che ad esempio può essere dichiarato in un header comune), ma c’è sempre il rischio
che questa chiave possa essere stata già utilizzata da qualcun altro. Dato che non esiste una
convenzione su come assegnare queste chiavi in maniera univoca l’interfaccia mette a disposizione
una funzione apposita, ftok, che permette di ottenere una chiave specificando il nome di un file
ed un numero di versione; il suo prototipo è:
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id)
Restituisce una chiave per identificare un oggetto del SysV IPC.
La funzione restituisce la chiave in caso di successo e -1 altrimenti, nel qual caso errno sarà uno
dei possibili codici di errore di stat.
La funzione determina un valore della chiave sulla base di pathname, che deve specificare il
pathname di un file effettivamente esistente e di un numero di progetto proj_id), che di norma
viene specificato come carattere, dato che ne vengono utilizzati solo gli 8 bit meno significativi.16
Il problema è che anche cosı̀ non c’è la sicurezza che il valore della chiave sia univoco, infatti
esso è costruito combinando il byte di proj_id) con i 16 bit meno significativi dell’inode del file
pathname (che vengono ottenuti attraverso stat, da cui derivano i possibili errori), e gli 8 bit
meno significativi del numero del dispositivo su cui è il file. Diventa perciò relativamente facile
ottenere delle collisioni, specie se i file sono su dispositivi con lo stesso minor number, come
/dev/hda1 e /dev/sda1.
In genere quello che si fa è utilizzare un file comune usato dai programmi che devono co-
municare (ad esempio un header comune, o uno dei programmi che devono usare l’oggetto 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.
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 l’oggetto associato ad una certa chiave sia
stato effettivamente creato da chi ci si aspetta.
Questo è, insieme al fatto che gli oggetti sono permanenti e non mantengono un contatore
di riferimenti per la cancellazione automatica, il principale problema del SysV IPC. Non esiste
infatti una modalità chiara per identificare un oggetto, come sarebbe stato se lo si fosse associato
ad in file, e tutta l’interfaccia è inutilmente complessa. Per questo ne è stata effettuata una
revisione completa nello standard POSIX.1b, che tratteremo in sez. 11.4.
16
nelle libc4 e libc5, come avviene in SunOS, l’argomento proj_id è dichiarato tipo char, le glibc usano il
prototipo specificato da XPG4, ma vengono lo stesso utilizzati gli 8 bit meno significativi.
11.2. L’INTERCOMUNICAZIONE FRA PROCESSI DI SYSTEM V 351
significato di quelli riportati in tab. 5.417 e come per i file definiscono gli accessi per il proprietario,
il suo gruppo e tutti gli altri.
Quando l’oggetto viene creato i campi cuid e uid di ipc_perm ed i campi cgid e gid
vengono impostati rispettivamente al valore dell’user-ID e del group-ID effettivo 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 è effettuato a due livelli. Il primo livello è nelle funzioni che richiedono
l’identificatore di un oggetto data la chiave. Queste specificano tutte un argomento flag, in tal
caso quando viene effettuata la ricerca di una chiave, qualora flag specifichi dei permessi, questi
vengono controllati e l’identificatore viene restituito solo se corrispondono a quelli dell’oggetto.
Se ci sono dei permessi non presenti in mode l’accesso sarà negato. Questo controllo però è di
utilità indicativa, dato che è sempre possibile specificare per flag un valore nullo, nel qual caso
l’identificatore sarà restituito comunque.
Il secondo livello di controllo è quello delle varie funzioni che accedono direttamente (in
lettura o scrittura) all’oggetto. In tal caso lo schema dei controlli è simile a quello dei file, ed
avviene secondo questa sequenza:
solo se tutti i controlli elencati falliscono l’accesso è negato. Si noti che a differenza di quanto
avviene per i permessi dei file, fallire in uno dei passi elencati non comporta il fallimento del-
l’accesso. Un’ulteriore differenza rispetto a quanto avviene per i file è che per gli oggetti di IPC
il valore di umask (si ricordi quanto esposto in sez. 5.3.3) non ha alcun significato.
Proprio per evitare questo tipo di situazioni il sistema usa il valore di seq per provvedere un
meccanismo che porti gli identificatori ad assumere tutti i valori possibili, rendendo molto più
lungo il periodo in cui un identificatore può venire riutilizzato.
Il sistema dispone sempre di un numero fisso 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 l’oggetto viene cancellato. Quando l’oggetto viene creato usando uno spazio che era già
stato utilizzato in precedenza per restituire l’identificatore 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 l’identificatore assuma tutti i valori possibili.
Figura 11.10: Sezione principale del programma di test per l’assegnazione degli identificatori degli oggetti di
IPC IPCTestId.c.
In fig. 11.10 è riportato il codice di un semplice programma di test che si limita a creare un
oggetto (specificato a riga di comando), stamparne il numero di identificatore e cancellarlo per
un numero specificato di volte. Al solito non si è riportato il codice della gestione delle opzioni
19
fino al kernel 2.2.x questi valori, definiti 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 modificarne
la definizione nei relativi header file. A partire dal kernel 2.4.x è possibile cambiare questi valori a sistema attivo
scrivendo sui file shmmni, msgmni e sem di /proc/sys/kernel o con l’uso di sysctl.
20
questo vale fino 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, definita in include/linux/ipc.h, che indica il limite massimo per il numero
di tutti oggetti di IPC, ed il cui valore è 32768.
11.2. L’INTERCOMUNICAZIONE FRA PROCESSI DI SYSTEM V 353
a riga di comando, che permette di specificare quante volte effettuare il ciclo n, e su quale tipo
di oggetto eseguirlo.
La figura 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 effettuare il ciclo
di creazione, stampa, cancellazione. I valori di default sono per l’uso delle code di messaggi e un
ciclo di 5 volte. Se si lancia il comando si otterrà qualcosa del tipo:
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:
che ci mostra come il valore di seq sia in effetti una quantità mantenuta staticamente all’interno
del sistema.
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int flag)
Restituisce l’identificatore di una coda di messaggi.
La funzione restituisce l’identificatore (un intero positivo) o -1 in caso di errore, nel qual caso
errno assumerà uno dei valori:
EACCES il processo chiamante non ha i privilegi per accedere alla coda richiesta.
EEXIST si è richiesta la creazione di una coda che già esiste, ma erano specificati sia IPC_CREAT
che IPC_EXCL.
EIDRM la coda richiesta è marcata per essere cancellata.
ENOENT si è cercato di ottenere l’identificatore di una coda di messaggi specificando una chiave
che non esiste e IPC_CREAT non era specificato.
ENOSPC si è cercato di creare una coda di messaggi quando è stato superato il limite massimo
di code (MSGMNI).
ed inoltre ENOMEM.
Le funzione (come le analoghe che si usano per gli altri oggetti) serve sia a ottenere l’identi-
ficatore di una coda di messaggi esistente, che a crearne una nuova. L’argomento key specifica
354 CAPITOLO 11. L’INTERCOMUNICAZIONE FRA PROCESSI
la chiave che è associata all’oggetto, eccetto il caso in cui si specifichi il valore IPC_PRIVATE,
nel qual caso la coda è creata ex-novo e non vi è associata alcuna chiave, il processo (ed i suoi
eventuali figli) potranno farvi riferimento solo attraverso l’identificatore.
Se invece si specifica un valore diverso da IPC_PRIVATE21 l’effetto della funzione dipende
dal valore di flag, se questo è nullo la funzione si limita ad effettuare una ricerca sugli oggetti
esistenti, restituendo l’identificatore se trova una corrispondenza, o fallendo con un errore di
ENOENT se non esiste o di EACCES se si sono specificati dei permessi non validi.
Se invece si vuole creare una nuova coda di messaggi flag non può essere nullo e deve essere
fornito come maschera binaria, impostando il bit corrispondente al valore IPC_CREAT. In questo
caso i nove bit meno significativi di flag saranno usati come permessi per il nuovo oggetto,
secondo quanto illustrato in sez. 11.2.2. Se si imposta anche il bit corrispondente a IPC_EXCL la
funzione avrà successo solo se l’oggetto non esiste già, fallendo con un errore di EEXIST altrimenti.
Si tenga conto che l’uso di IPC_PRIVATE non impedisce ad altri processi di accedere alla coda
(se hanno privilegi sufficienti) una volta che questi possano indovinare o ricavare (ad esempio
per tentativi) l’identificatore ad essa associato. Per come sono implementati gli oggetti di IPC
infatti non esiste una maniera che garantisca l’accesso 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 Valore File in proc Significato
MSGMNI 16 msgmni Numero massimo di code di messaggi.
MSGMAX 8192 msgmax Dimensione massima di un singolo messaggio.
MSGMNB 16384 msgmnb Dimensione massima del contenuto di una coda.
Tabella 11.1: Valori delle costanti associate ai limiti delle code di messaggi.
Le code di messaggi sono caratterizzate da tre limiti fondamentali, definiti negli header e
corrispondenti alle prime tre costanti riportate in tab. 11.1, come accennato però in Linux è
possibile modificare questi limiti attraverso l’uso di sysctl o scrivendo nei file msgmax, msgmnb
e msgmni di /proc/sys/kernel/.
Una coda di messaggi è costituita da una linked list;22 i nuovi messaggi vengono inseriti in
21
in Linux questo significa un valore diverso da zero.
22
una linked list è una tipica struttura di dati, organizzati in una lista in cui ciascun elemento contiene un
puntatore al successivo. In questo modo la struttura è veloce nell’estrazione ed immissione dei dati dalle estremità
11.2. L’INTERCOMUNICAZIONE FRA PROCESSI DI SYSTEM V 355
coda alla lista e vengono letti dalla cima, in fig. 11.11 si è riportato lo schema con cui queste
strutture vengono mantenute dal kernel.23
struct msqid_ds {
struct ipc_perm msg_perm ; /* structure for operation permission */
time_t msg_stime ; /* time of last msgsnd command */
time_t msg_rtime ; /* time of last msgrcv command */
time_t msg_ctime ; /* time of last change */
msgqnum_t msg_qnum ; /* number of messages currently on queue */
msglen_t msg_qbytes ; /* max number of bytes allowed on queue */
pid_t msg_lspid ; /* pid of last msgsnd () */
pid_t msg_lrpid ; /* pid of last msgrcv () */
struct msg * msg_first ; /* first message on queue , unused */
struct msg * msg_last ; /* last message in queue , unused */
unsigned long int msg_cbytes ; /* current number of bytes on queue */
};
• il campo msg_qnum, che esprime il numero di messaggi presenti sulla coda, viene inizializ-
zato a 0.
• i campi msg_lspid e msg_lrpid, che esprimono rispettivamente il pid dell’ultimo 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
inviato o ricevuto l’ultimo 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 l’indirizzo 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 dell’implementazione e
non devono essere utilizzati da programmi in user space).
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 nell’accesso casuale e
nella ricerca.
23
lo schema illustrato in fig. 11.11 è in realtà una semplificazione di quello usato effettivamente fino ai kernel
della serie 2.2.x, nei kernel della serie 2.4.x la gestione delle code di messaggi è stata modificata ed è effettuata
in maniera diversa; abbiamo mantenuto lo schema precedente in quanto illustra comunque in maniera più che
adeguata i principi di funzionamento delle code di messaggi.
24
come accennato questo vale fino 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 dell’interfaccia. Si noti come ci sia una differenza con i
campi mostrati nello schema di fig. 11.11 che sono presi dalla definizione di linux/msg.h, e fanno riferimento alla
definizione della omonima struttura usata nel kernel.
356 CAPITOLO 11. L’INTERCOMUNICAZIONE FRA PROCESSI
Una volta creata una coda di messaggi le operazioni di controllo vengono effettuate con la
funzione msgctl, che (come le analoghe semctl e shmctl) fa le veci di quello che ioctl è per i
file; il suo prototipo è:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf)
Esegue l’operazione specificata da cmd sulla coda msqid.
La funzione restituisce 0 in caso di successo o −1 in caso di errore, nel qual caso errno assumerà
uno dei valori:
EACCES si è richiesto IPC_STAT ma processo chiamante non ha i privilegi di lettura sulla coda.
EIDRM la coda richiesta è stata cancellata.
EPERM si è richiesto IPC_SET o IPC_RMID ma il processo non ha i privilegi, o si è richiesto di
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 all’indirizzo
buf, per la coda specificata dall’identificatore msqid. Il comportamento della funzione dipende
dal valore dell’argomento cmd, che specifica il tipo di azione da eseguire; i valori possibili sono:
IPC_STAT Legge le informazioni riguardo la coda nella struttura indicata da buf. Occorre
avere il permesso di lettura sulla coda.
IPC_RMID Rimuove la coda, cancellando tutti i dati, con effetto 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
processo con user-ID effettivo corrispondente al creatore o al proprietario della
coda, o all’amministratore.
IPC_SET Permette di modificare i permessi ed il proprietario della coda, ed il limite mas-
simo 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 mo-
dificare i valori di msg_perm.mode, msg_perm.uid e msg_perm.gid occorre essere
il proprietario o il creatore della coda, oppure l’amministratore; lo stesso vale per
msg_qbytes, ma l’amministratore ha la facoltà di incrementarne il valore a limiti
superiori a MSGMNB.
Una volta che si abbia a disposizione l’identificatore, per inviare un messaggio su una coda
si utilizza la funzione msgsnd; il suo prototipo è:
#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:
EACCES non si hanno i privilegi di accesso sulla coda.
EIDRM la coda è stata cancellata.
EAGAIN il messaggio non può essere inviato perché si è superato il limite msg_qbytes sul numero
massimo di byte presenti sulla coda, e si è richiesto IPC_NOWAIT in flag.
EINVAL si è specificato un msgid invalido, o un valore non positivo per mtype, o un valore di
msgsz maggiore di MSGMAX.
ed inoltre EFAULT, EINTR ed ENOMEM.
11.2. L’INTERCOMUNICAZIONE FRA PROCESSI DI SYSTEM V 357
struct msgbuf {
long mtype ; /* message type , must be > 0 */
char mtext [ LENGTH ]; /* message data */
};
Figura 11.13: Schema della struttura msgbuf, da utilizzare come argomento per inviare/ricevere messaggi.
La funzione che viene utilizzata per estrarre un messaggio da una coda è msgrcv; il suo
prototipo è:
#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:
EACCES non si hanno i privilegi di accesso sulla coda.
EIDRM la coda è stata cancellata.
E2BIG il testo del messaggio è più lungo di msgsz e non si è specificato MSG_NOERROR in
msgflg.
EINTR la funzione è stata interrotta da un segnale mentre era in attesa di ricevere un
messaggio.
EINVAL si è specificato un msgid invalido o un valore di msgsz negativo.
ed inoltre EFAULT.
La funzione legge un messaggio dalla coda specificata, scrivendolo sulla struttura puntata da
msgp, che dovrà avere un formato analogo a quello di fig. 11.13. Una volta estratto, il messaggio
sarà rimosso dalla coda. L’argomento msgsz indica la lunghezza massima del testo del messaggio
(equivalente al valore del parametro LENGTH nell’esempio di fig. 11.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
eccesso viene perduta, altrimenti il messaggio non viene estratto e la funzione ritorna con un
errore di E2BIG.
L’argomento msgtyp permette di restringere la ricerca ad un sottoinsieme dei messaggi pre-
senti sulla coda; la ricerca infatti è fatta con una scansione della struttura mostrata in fig. 11.11,
restituendo il primo messaggio incontrato che corrisponde ai criteri specificati (che quindi, visto
come i messaggi vengono sempre inseriti dalla coda, è quello meno recente); in particolare:
• se msgtyp è 0 viene estratto il messaggio in cima alla coda, cioè quello fra i presenti che è
stato inserito per primo.
• se msgtyp è positivo viene estratto il primo messaggio il cui tipo (il valore del campo
mtype) corrisponde al valore di msgtyp.
• se msgtyp è negativo viene estratto il primo fra i messaggi con il valore più basso del tipo,
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
nullo o una maschera binaria composta da uno o più valori. Oltre al precedente MSG_NOERROR,
sono possibili altri due valori: MSG_EXCEPT, che permette, quando msgtyp è positivo, di leggere
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 specificato IPC_NOWAIT
la funzione ritorna immediatamente con un errore ENOMSG. Altrimenti la funzione ritorna nor-
malmente 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 l’estrazione del messaggio dalla coda, la funzione aggiorna
i dati mantenuti in msqid_ds, in particolare vengono modificati:
11.2. L’INTERCOMUNICAZIONE FRA PROCESSI DI SYSTEM V 359
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,
al contrario delle pipe per le quali tutte le risorse occupate vengono rilasciate quanto l’ultimo
processo che le utilizzava termina. Questo comporta che in caso di errori si può saturare il
sistema, e che devono comunque essere esplicitamente previste delle funzioni di rimozione in
caso di interruzioni o uscite dal programma (come vedremo in fig. 11.14).
L’altro problema è non facendo uso di file descriptor le tecniche di I/O multiplexing descritte
in sez. 12.2 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
esempio non si può scrivere un server che aspetti un messaggio su più di una coda senza fare
ricorso ad una tecnica di polling che esegua un ciclo di attesa su ciascuna di esse.
Come esempio dell’uso delle code di messaggi possiamo riscrivere il nostro server di fortunes
usando queste al posto delle fifo. In questo caso useremo una sola coda di messaggi, usando il
tipo di messaggio per comunicare in maniera indipendente con client diversi.
In fig. 11.14 si è riportato un estratto delle parti principali del codice del nuovo server (il
codice completo è nel file MQFortuneServer.c nei sorgenti allegati). Il programma è basato su
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
identificativo. Questo è possibile in quanto, al contrario di una fifo, la lettura di una coda di
messaggi può non essere sequenziale, proprio grazie alla classificazione dei messaggi sulla base
del loro tipo.
Il programma, oltre alle solite variabili per il nome del file da cui leggere le fortunes e
per il vettore di stringhe che contiene le frasi, definisce 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
di frasi da leggere specificato a linea di comando ed in fortunefilename il file da cui leggerle;
dopo aver installato (19-21) i gestori dei segnali per trattare l’uscita dal server, viene prima
controllato (22) il numero di frasi richieste abbia senso (cioè sia maggiore di zero), le quali poi
(23) vengono lette nel vettore in memoria con la stessa funzione FortuneParse usata anche per
il server basato sulle fifo.
Una volta inizializzato il vettore di stringhe coi messaggi presi dal file delle fortune si procede
(25) con la generazione di una chiave per identificare la coda di messaggi (si usa il nome del file
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 l’argomento 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
al pid di init, che non può essere un client. L’uso del flag MSG_NOERROR è solo per sicurezza,
dato che i messaggi di richiesta sono di dimensione fissa (e contengono solo il pid del client).
Se non sono presenti messaggi di richiesta msgrcv si bloccherà, ritornando soltanto in corri-
spondenza dell’arrivo 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.
360 CAPITOLO 11. L’INTERCOMUNICAZIONE FRA PROCESSI
Figura 11.14: Sezione principale del codice del server di fortunes basato sulle message queue.
Per poter permettere a ciascun client di ricevere solo la risposta indirizzata a lui il tipo del
messaggio in uscita viene inizializzato (38) al valore del pid del client ricevuto nel messaggio di
richiesta. L’ultimo passo del ciclo (39) è inviare sulla coda il messaggio di risposta. Si tenga conto
che se la coda è piena anche questa funzione potrà bloccarsi fintanto che non venga liberato dello
spazio.
Si noti che il programma può terminare solo grazie ad una interruzione da parte di un segnale;
in tal caso verrà eseguito (45-48) il gestore HandSIGTERM, che semplicemente si limita a cancellare
11.2. L’INTERCOMUNICAZIONE FRA PROCESSI DI SYSTEM V 361
Figura 11.15: Sezione principale del codice del client di fortunes basato sulle message queue.
In fig. 11.15 si è riportato un estratto il codice del programma client. Al solito il codice
completo è con i sorgenti allegati, nel file MQFortuneClient.c. Come sempre si sono rimosse le
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 fig. 11.14.
Il client in questo caso è molto semplice; la prima parte del programma (4-9) si occupa di
accedere alla coda di messaggi, ed è identica a quanto visto per il server, solo che in questo
caso msgget non viene chiamata con il flag di creazione in quanto la coda deve essere preesi-
stente. In caso di errore (ad esempio se il server non è stato avviato) il programma termina
immediatamente.
Una volta acquisito l’identificatore della coda il client compone il messaggio di richiesta (12-
13) 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.
L’ultimo passo (17) prima di uscire è quello di stampare a video il messaggio ricevuto.
Proviamo allora il nostro nuovo sistema, al solito occorre definire 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 fifo, potremo far partire il server con:
come nel caso precedente, avendo eseguito il server in background, il comando ritornerà imme-
diatamente; potremo però verificare con ps che il programma è effettivamente in esecuzione, e
che ha creato una coda di messaggi:
con un risultato del tutto equivalente al precedente. Infine potremo chiudere il server inviando
il segnale di terminazione con il comando killall mqfortuned verificando che effettivamente
la coda di messaggi viene rimossa.
Benché funzionante questa architettura risente dello stesso inconveniente visto anche nel caso
del precedente server basato sulle fifo; se il client viene interrotto dopo l’invio del messaggio di
richiesta e prima della lettura della risposta, quest’ultima resta nella coda (cosı̀ come per le fifo
si aveva il problema delle fifo che restavano nel filesystem). In questo caso però il problemi sono
maggiori, sia perché è molto più facile esaurire la memoria dedicata ad una coda di messaggi
che gli inode di un filesystem, sia perché, con il riutilizzo dei pid da parte dei processi, un client
eseguito in un momento successivo potrebbe ricevere un messaggio non indirizzato a lui.
11.2.5 Semafori
I semafori non sono meccanismi di intercomunicazione diretta come quelli (pipe, fifo e code di
messaggi) visti finora, 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.6.2).
Un semaforo è uno speciale contatore, mantenuto nel kernel, che permette, a seconda del suo
valore, di consentire o meno la prosecuzione dell’esecuzione di un programma. In questo modo
l’accesso ad una risorsa condivisa da più processi può essere controllato, associando ad essa un
semaforo che consente di assicurare che non più di un processo alla volta possa usarla.
Il concetto di semaforo è uno dei concetti base nella programmazione ed è assolutamente
generico, cosı̀ come del tutto generali sono modalità con cui lo si utilizza. Un processo che
deve accedere ad una risorsa eseguirà un controllo del semaforo: se questo è positivo il suo
valore sarà decrementato, indicando che si è consumato una unità della risorsa, ed il processo
potrà proseguire nell’utilizzo di quest’ultima, provvedendo a rilasciarla, una volta completate le
operazioni volute, reincrementando il semaforo.
Se al momento del controllo il valore del semaforo è nullo, siamo invece in una situazione in
cui la risorsa non è disponibile, ed il processo si bloccherà in stato di sleep fin quando chi la sta
utilizzando non la rilascerà, incrementando il valore del semaforo. Non appena il semaforo torna
positivo, indicando che la risorsa è disponibile, il processo sarà svegliato, e si potrà operare come
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
del semaforo binario, o mutex, in cui un valore diverso da zero (normalmente 1) indica la libertà
di accesso, e un valore nullo l’occupazione della risorsa. In generale però si possono usare semafori
11.2. L’INTERCOMUNICAZIONE FRA PROCESSI DI SYSTEM V 363
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 l’identificatore di un insieme di semafori è
semget, ed il suo prototipo è:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int flag)
Restituisce l’identificatore di un insieme di semafori.
La funzione restituisce l’identificatore (un intero positivo) o -1 in caso di errore, nel qual caso
errno assumerà i valori:
ENOSPC si è cercato di creare una insieme di semafori quando è stato superato o il limite per il
numero totale di semafori (SEMMNS) o quello per il numero totale degli insiemi (SEMMNI)
nel sistema.
EINVAL l’argomento nsems è minore di zero o maggiore del limite sul numero di semafori per
ciascun insieme (SEMMSL), o se l’insieme già esiste, maggiore del numero di semafori
che contiene.
ENOMEM il sistema non ha abbastanza memoria per poter contenere le strutture per un nuovo
insieme di semafori.
ed inoltre EACCES, ENOENT, EEXIST, EIDRM, con lo stesso significato che hanno per msgget.
La funzione è del tutto analoga a msgget, solo che in questo caso restituisce l’identificatore
di un insieme di semafori, in particolare è identico l’uso degli argomenti key e flag, per cui non
ripeteremo quanto detto al proposito in sez. 11.2.4. L’argomento nsems permette di specificare
quanti semafori deve contenere l’insieme quando se ne richieda la creazione, e deve essere nullo
quando si effettua una richiesta dell’identificatore di un insieme già esistente.
Purtroppo questa implementazione complica inutilmente lo schema elementare che abbiamo
descritto, dato che non è possibile definire un singolo semaforo, ma se ne deve creare per forza
un insieme. Ma questa in definitiva è solo una complicazione inutile, il problema è che i semafori
del SysV IPC soffrono di altri due, ben più gravi, difetti.
Il primo difetto è che non esiste una funzione che permetta di creare ed inizializzare un
semaforo in un’unica chiamata; occorre prima creare l’insieme dei semafori con semget e poi
inizializzarlo con semctl, si perde cosı̀ ogni possibilità di eseguire l’operazione atomicamente.
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ı̀
a trova a dover affrontare esplicitamente il caso in cui un processo termina per un qualche
errore, lasciando un semaforo occupato, che resterà tale fino al successivo riavvio del sistema.
Come vedremo esistono delle modalità per evitare tutto ciò, ma diventa necessario indicare
esplicitamente che si vuole il ripristino del semaforo all’uscita del processo.
struct semid_ds
{
struct ipc_perm sem_perm ; /* operation permission struct */
time_t sem_otime ; /* last semop () time */
time_t sem_ctime ; /* last time changed by semctl () */
unsigned long int sem_nsems ; /* number of semaphores in set */
};
A ciascun insieme di semafori è associata una struttura semid_ds, riportata in fig. 11.16.25
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. 11.2.2 (si ricordi che in questo caso il permesso di scrittura è in realtà permesso
di alterare il semaforo), per quanto riguarda gli altri campi invece:
Ciascun semaforo dell’insieme è realizzato come una struttura di tipo sem che ne contiene i
dati essenziali, la sua definizione26 è riportata in fig. 11.17. Questa struttura, non è accessibile
in user space, ma i valori in essa specificati possono essere letti in maniera indiretta, attraverso
l’uso delle funzioni di controllo.
struct sem {
short sempid ; /* pid of last operation */
ushort semval ; /* current value */
ushort semncnt ; /* num procs awaiting increase in semval */
ushort semzcnt ; /* num procs awaiting semval = 0 */
};
Tabella 11.2: Valori delle costanti associate ai limiti degli insiemi di semafori, definite 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. 11.2. Alcuni di
25
non si sono riportati i campi ad uso interno del kernel, che vedremo in fig. 11.20, che dipendono
dall’implementazione.
26
si è riportata la definizione originaria del kernel 1.0, che contiene la prima realizzazione del SysV IPC in Linux.
In realtà questa struttura ormai è ridotta ai soli due primi membri, e gli altri vengono calcolati dinamicamente.
La si è utilizzata a scopo di esempio, perché indica tutti i valori associati ad un semaforo, restituiti dalle funzioni
di controllo, e citati dalle pagine di manuale.
11.2. L’INTERCOMUNICAZIONE FRA PROCESSI DI SYSTEM V 365
questi limiti sono al solito accessibili e modificabili attraverso sysctl o scrivendo direttamente
nel file /proc/sys/kernel/sem.
La funzione che permette di effettuare le varie operazioni di controllo sui semafori (fra le
quali, come accennato, è impropriamente compresa anche la loro inizializzazione) è semctl; il
suo prototipo è:
#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
dei valori:
EACCES il processo non ha i privilegi per eseguire l’operazione richiesta.
EIDRM l’insieme di semafori è stato cancellato.
EPERM si è richiesto IPC_SET o IPC_RMID ma il processo non ha privilegi sufficienti ad eseguire
l’operazione.
ERANGE si è richiesto SETALL SETVAL ma il valore a cui si vuole impostare il semaforo è minore
di zero o maggiore di SEMVMX.
ed inoltre EFAULT ed EINVAL.
La funzione può avere tre o quattro argomenti, a seconda dell’operazione specificata con
cmd, ed opera o sull’intero insieme specificato da semid o sul singolo semaforo di un insieme,
specificato da semnum.
union semun {
int val ; /* value for SETVAL */
struct semid_ds * buf ; /* buffer for IPC_STAT , IPC_SET */
unsigned short * array ; /* array for GETALL , SETALL */
/* Linux specific part : */
struct seminfo * __buf ; /* buffer for IPC_INFO */
};
Figura 11.18: La definizione 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à
un dato diverso a seconda dell’azione richiesta; per unificare l’argomento esso deve essere passato
come una semun, la cui definizione, con i possibili valori che può assumere, è riportata in fig. 11.18.
Come già accennato sia il comportamento della funzione che il numero di argomenti con cui
deve essere invocata dipendono dal valore dell’argomento cmd, che specifica l’azione da intra-
prendere; i valori validi (che cioè non causano un errore di EINVAL) per questo argomento sono
i seguenti:
IPC_STAT Legge i dati dell’insieme di semafori, copiando il contenuto della relativa strut-
tura semid_ds all’indirizzo specificato con arg.buf. Occorre avere il permesso di
lettura. L’argomento semnum viene ignorato.
IPC_RMID Rimuove l’insieme di semafori e le relative strutture dati, con effetto immediato.
Tutti i processi che erano stato di sleep vengono svegliati, ritornando con un
errore di EIDRM. L’user-ID effettivo del processo deve corrispondere o al creatore
366 CAPITOLO 11. L’INTERCOMUNICAZIONE FRA PROCESSI
GETNCNT Restituisce come valore di ritorno della funzione il numero di processi in attesa
che il semaforo semnum dell’insieme semid venga incrementato (corrispondente al
campo semncnt di sem); va invocata con tre argomenti. Occorre avere il permesso
di lettura.
GETPID Restituisce come valore di ritorno della funzione il pid dell’ultimo processo che ha
compiuto una operazione sul semaforo semnum dell’insieme semid (corrispondente
al campo sempid di sem); va invocata con tre argomenti. Occorre avere il permesso
di lettura.
GETVAL Restituisce come valore di ritorno della funzione il il valore corrente del semaforo
semnum dell’insieme semid (corrispondente al campo semval di sem); va invocata
con tre argomenti. Occorre avere il permesso di lettura.
GETZCNT Restituisce come valore di ritorno della funzione il numero di processi in attesa che
il valore del semaforo semnum dell’insieme semid diventi nullo (corrispondente al
campo semncnt di sem); va invocata con tre argomenti. Occorre avere il permesso
di lettura.
Quando si imposta il valore di un semaforo (sia che lo si faccia per tutto l’insieme 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 modificato.
Operazione Valore restituito
GETNCNT Valore di semncnt.
GETPID Valore di sempid.
GETVAL Valore di semval.
GETZCNT Valore di semzcnt.
Il valore di ritorno della funzione in caso di successo dipende dall’operazione richiesta; per
tutte le operazioni che richiedono quattro argomenti esso è sempre nullo, per le altre operazioni,
elencate in tab. 11.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 l’acquisizione o il rilascio degli stessi (in sostanza
tutte quelle non comprese nell’uso di semctl) vengono effettuate con la funzione semop, il cui
prototipo è:
#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à
uno dei valori:
EACCES il processo non ha i privilegi per eseguire l’operazione richiesta.
EIDRM l’insieme di semafori è stato cancellato.
ENOMEM si è richiesto un SEM_UNDO ma il sistema non ha le risorse per allocare la struttura di
ripristino.
EAGAIN un’operazione comporterebbe il blocco del processo, ma si è specificato IPC_NOWAIT in
sem_flg.
EINTR la funzione, bloccata in attesa dell’esecuzione dell’operazione, viene interrotta da un
segnale.
E2BIG l’argomento nsops è maggiore del numero massimo di operazioni SEMOPM.
ERANGE per alcune operazioni il valore risultante del semaforo viene a superare il limite
massimo SEMVMX.
ed inoltre EFAULT ed EINVAL.
struct sembuf
{
unsigned short int sem_num ; /* semaphore number */
short int sem_op ; /* semaphore operation */
short int sem_flg ; /* operation flag */
};
Il contenuto di ciascuna operazione deve essere specificato attraverso una opportuna struttura
sembuf (la cui definizione è riportata in fig. 11.19) che il programma chiamante deve avere cura di
allocare in un opportuno vettore. La struttura permette di indicare il semaforo su cui operare, il
tipo di operazione, ed un flag di controllo. Il campo sem_num serve per indicare a quale semaforo
dell’insieme fa riferimento l’operazione; 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 flag, mantenuto come maschera binaria, per il quale possono essere
impostati i due valori IPC_NOWAIT e SEM_UNDO. Impostando IPC_NOWAIT si fa si che, invece di
368 CAPITOLO 11. L’INTERCOMUNICAZIONE FRA PROCESSI
bloccarsi (in tutti quei casi in cui l’esecuzione 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 l’operazione venga registrata in modo che il valore del semaforo possa essere
ripristinato all’uscita del processo.
Infine sem_op è il campo che controlla l’operazione che viene eseguita e determina il com-
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. Specificando
SEM_UNDO si aggiorna il contatore per il ripristino del valore del semaforo. Al pro-
cesso chiamante è richiesto il privilegio di alterazione (scrittura) sull’insieme di
semafori.
sem_op= 0 Nel caso semval sia zero l’esecuzione procede immediatamente. Se semval è di-
verso da zero il comportamento è controllato da sem_flg, se è stato impostato
IPC_NOWAIT la funzione ritorna con un errore di EAGAIN, altrimenti viene incre-
mentato semzcnt di uno ed il processo resta in stato di sleep fintanto che non si ha
una delle condizioni seguenti:
• semval diventa zero, nel qual caso semzcnt viene decrementato di uno.
• l’insieme di semafori viene rimosso, nel qual caso semop ritorna un errore di
EIDRM.
• il processo chiamante riceve un segnale, nel qual caso semzcnt viene decre-
mentato di uno e semop ritorna un errore di EINTR.
sem_op< 0 Nel caso in cui semval è maggiore o uguale del valore assoluto di sem_op (se cioè la
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 aggiorna-
to 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
IPC_NOWAIT la funzione ritorna con un errore di EAGAIN, altrimenti viene incremen-
tato di uno semncnt ed il processo resta in stato di sleep fintanto 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.
• l’insieme di semafori viene rimosso, nel qual caso semop ritorna un errore di
EIDRM.
• il processo chiamante riceve un segnale, nel qual caso semncnt viene decre-
mentato di uno e semop ritorna un errore di EINTR.
attraverso l’uso del flag SEM_UNDO. Il meccanismo è implementato tramite una apposita struttura
sem_undo, associata ad ogni processo per ciascun semaforo che esso ha modificato; all’uscita
i semafori modificati 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 nell’esecuzione di una exec (altrimenti
non si avrebbe ripristino).
Tutto questo però ha un problema di fondo. Per capire di cosa si tratta occorre fare riferimen-
to all’implementazione usata in Linux, che è riportata in maniera semplificata nello schema di
fig. 11.20. Si è presa come riferimento l’architettura usata fino al kernel 2.2.x che è più semplice
(ed illustrata in dettaglio in [13]); nel kernel 2.4.x la struttura del SysV IPC è stata modificata,
ma le definizioni relative a queste strutture restano per compatibilità.27
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 verificato 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
quest’ultimo viene messo stato di attesa e viene invocato lo scheduler per passare all’esecuzione
di un altro processo.
Se invece tutte le operazioni possono avere successo queste vengono eseguite immediatamen-
te, dopo di che il kernel esegue una scansione della coda di attesa (a partire da sem_pending) per
verificare se qualcuna delle operazioni sospese in precedenza può essere eseguita, nel qual caso
la struttura sem_queue viene rimossa e lo stato del processo associato all’operazione (sleeper)
27
in particolare con le vecchie versioni delle librerie del C, come le libc5.
28
che viene referenziata tramite i campi sem_pending e sem_pending_last di semid_ds.
370 CAPITOLO 11. L’INTERCOMUNICAZIONE FRA PROCESSI
viene riportato a running; il tutto viene ripetuto fin quando non ci sono più operazioni eseguibili
o si è svuotata la coda. Per gestire il meccanismo del ripristino tutte le volte che per un’ope-
razione si è specificato il flag SEM_UNDO viene mantenuta per ciascun insieme di semafori una
apposita struttura sem_undo che contiene (nel vettore puntato dal campo semadj) un valore di
aggiustamento per ogni semaforo cui viene sommato l’opposto del valore usato per l’operazione.
Queste strutture sono mantenute in due liste,29 una associata all’insieme 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; l’altra associata al processo che ha eseguito l’operazione;30
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 differenti
chiamate attraverso diverse chiamate a semop, si pone il problema di come eseguire il ripristino
dei semafori all’uscita del processo, ed in particolare se questo può essere fatto atomicamente.
Il punto è cosa succede quando una delle operazioni previste per il ripristino non può essere
eseguita immediatamente perché ad esempio il semaforo è occupato; in tal caso infatti, se si
pone il processo in stato di sleep aspettando la disponibilità del semaforo (come faceva l’imple-
mentazione originaria) si perde l’atomicità dell’operazione. La scelta fatta dal kernel è pertanto
quella di effettuare 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
in tutte le occasioni.
Come esempio di uso dell’interfaccia dei semafori vediamo come implementare con essa dei
semplici mutex (cioè semafori binari), tutto il codice in questione, contenuto nel file Mutex.c
allegato ai sorgenti, è riportato in fig. 11.21. Utilizzeremo l’interfaccia per creare un insieme
contenente un singolo semaforo, per il quale poi useremo un valore unitario per segnalare la
disponibilità della risorsa, ed un valore nullo per segnalarne l’indisponibilità.
La prima funzione (2-15) è MutexCreate che data una chiave crea il semaforo usato per
il mutex e lo inizializza, restituendone l’identificatore. Il primo passo (6) è chiamare semget
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 l’unione
semunion dichiarata ed avvalorata in precedenza (4) ad 1 per significare che risorsa è libera.
In caso di errore (11-13) si restituisce il valore di ritorno di semctl, altrimenti (14) si ritorna
l’identificatore del semaforo.
La seconda funzione (17-20) è MutexFind, che, data una chiave, restituisce l’identificatore
del semaforo ad essa associato. La comprensione del suo funzionamento è immediata in quanto
essa è soltanto un wrapper 31 di una chiamata a semget per cercare l’identificatore associato alla
chiave, il valore di ritorno di quest’ultima viene passato all’indietro al chiamante.
La terza funzione (22-25) è MutexRead che, dato un identificatore, restituisce il valore del
semaforo associato al mutex. Anche in questo caso la funzione è un wrapper per una chiamata
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, utiliz-
zando le due strutture sem_lock e sem_unlock definite in precedenza (27-34). Si noti come per
queste ultime si sia fatto uso dell’opzione SEM_UNDO per evitare che il semaforo resti bloccato in
caso di terminazione imprevista del processo.
29
rispettivamente attraverso i due campi id_next e proc_next.
30
attraverso il campo semundo di task_struct, come mostrato in 11.20.
31
si chiama cosı̀ una funzione usata per fare da involucro alla chiamata di un altra, usata in genere per sempli-
ficare un’interfaccia (come in questo caso) o per utilizzare con la stessa funzione diversi substrati (librerie, ecc.)
che possono fornire le stesse funzionalità.
11.2. L’INTERCOMUNICAZIONE FRA PROCESSI DI SYSTEM V 371
Figura 11.21: Il codice delle funzioni che permettono di creare o recuperare l’identificatore di un semaforo da
utilizzare come mutex.
L’ultima funzione (46-49) della serie, è MutexRemove, che rimuove il mutex. Anche in questo
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 quest’ultima viene passato all’indietro.
Chiamare MutexLock decrementa il valore del semaforo: se questo è libero (ha già valore 1)
sarà bloccato (valore nullo), se è bloccato la chiamata a semop si bloccherà fintanto che la risorsa
372 CAPITOLO 11. L’INTERCOMUNICAZIONE FRA PROCESSI
non venga rilasciata. Chiamando MutexUnlock il valore del semaforo sarà incrementato di uno,
sbloccandolo qualora fosse bloccato.
Si noti che occorre eseguire sempre prima MutexLock e poi MutexUnlock, perché se per
un qualche errore si esegue più volte quest’ultima il valore del semaforo crescerebbe oltre 1, e
MutexLock non avrebbe più l’effetto aspettato (bloccare la risorsa quando questa è considerata
libera). Infine 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 l’operazione non
sarebbe atomica. Vedremo in sez. 11.3.3 come sia possibile ottenere un’interfaccia analoga a
quella appena illustrata, senza incorrere in questi problemi, usando il file locking.
La funzione, come semget, è del tutto analoga a msgget, ed identico è l’uso degli argomenti
key e flag per cui non ripeteremo quanto detto al proposito in sez. 11.2.4. L’argomento size
specifica 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
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 all’altro, in
quanto ciascuno può accedervi direttamente con le normali operazioni di lettura e scrittura dei
dati in memoria.
Ovviamente tutto questo ha un prezzo, ed il problema fondamentale della memoria condivisa
è la sincronizzazione degli accessi. È evidente infatti che se un processo deve scambiare dei dati
con un altro, si deve essere sicuri che quest’ultimo 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, significa insieme a dei semafori.
A ciascun segmento di memoria condivisa è associata una struttura shmid_ds, riportata in
fig. 11.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. 11.2.2, e valgono le considerazioni ivi fatte relativamente ai
permessi di accesso; per quanto riguarda gli altri campi invece:
11.2. L’INTERCOMUNICAZIONE FRA PROCESSI DI SYSTEM V 373
struct shmid_ds {
struct ipc_perm shm_perm ; /* operation perms */
int shm_segsz ; /* size of segment ( bytes ) */
time_t shm_atime ; /* last attach time */
time_t shm_dtime ; /* last detach time */
time_t shm_ctime ; /* last change time */
unsigned short shm_cpid ; /* pid of creator */
unsigned short shm_lpid ; /* pid of last operator */
short shm_nattch ; /* no . of current attaches */
};
• 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.
• il campo shm_lpid, che esprime il pid del processo che ha eseguito l’ultima 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.
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 so-
lito accessibili e modificabili attraverso sysctl o scrivendo direttamente nei rispettivi file di
/proc/sys/kernel/.
In tab. 11.4 si sono riportate le costanti simboliche associate a ciascuno di essi, il loro signi-
ficato, i valori preimpostati, e, quando presente, il file in /proc/sys/kernel/ che permettono
di cambiarne il valore.
Costante Valore File in proc Significato
SHMALL 0x200000 shmall Numero massimo di pagine che possono essere
usate per i segmenti di memoria condivisa.
SHMMAX 0x2000000 shmmax Dimensione massima di un segmento di memoria
condivisa.
SHMMNI 4096 msgmni Numero massimo di segmenti di memoria
condivisa presenti nel kernel.
SHMMIN 1 — Dimensione minima di un segmento di memoria
condivisa.
SHMLBA PAGE_SIZE — Limite inferiore per le dimensioni minime di un
segmento (deve essere allineato alle dimensioni di
una pagina di memoria).
SHMSEG — — Numero massimo di segmenti di memoria
condivisa per ciascun processo.
Tabella 11.4: Valori delle costanti associate ai limiti dei segmenti di memoria condivisa, insieme al relativo file
in /proc/sys/kernel/ ed al valore preimpostato presente nel sistema.
374 CAPITOLO 11. L’INTERCOMUNICAZIONE FRA PROCESSI
Il comando specificato attraverso l’argomento cmd determina i diversi effetti della funzione; i
possibili valori che esso può assumere, ed il corrispondente comportamento della funzione, sono
i seguenti:
IPC_RMID Marca il segmento di memoria condivisa per la rimozione, questo verrà cancellato
effettivamente solo quando l’ultimo processo ad esso agganciato si sarà stacca-
to. Questo comando può essere eseguito solo da un processo con user-ID effetti-
vo corrispondente o al creatore del segmento, o al proprietario del segmento, o
all’amministratore.
SHM_LOCK Abilita il memory locking 32 sul segmento di memoria condivisa. Solo l’ammini-
stratore può utilizzare questo comando.
SHM_UNLOCK Disabilita il memory locking sul segmento di memoria condivisa. Solo l’ammini-
stratore può utilizzare questo comando.
i primi tre comandi sono gli stessi già visti anche per le code di messaggi e gli insiemi di semafori,
gli ultimi due sono delle estensioni specifiche previste da Linux, che permettono di abilitare e
disabilitare il meccanismo della memoria virtuale per il segmento.
L’argomento 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
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 l’interfaccia
prevede due funzioni, shmat e shmdt. La prima di queste serve ad agganciare un segmento al
32
impedisce cioè che la memoria usata per il segmento venga salvata su disco dal meccanismo della memoria
virtuale; si ricordi quanto trattato in sez. 2.2.4.
11.2. L’INTERCOMUNICAZIONE FRA PROCESSI DI SYSTEM V 375
processo chiamante, in modo che quest’ultimo possa inserirlo nel suo spazio di indirizzi per
potervi accedere; il suo prototipo è:
#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 l’indirizzo del segmento in caso di successo, e -1 in caso di errore, nel qual
caso errno assumerà i valori:
EACCES il processo non ha i privilegi per accedere al segmento nella modalità richiesta.
EINVAL si è specificato un identificatore invalido per shmid, o un indirizzo non allineato sul
confine di una pagina per shmaddr.
ed inoltre ENOMEM.
Figura 11.23: Disposizione dei segmenti di memoria di un processo quando si è agganciato un segmento di
memoria condivisa.
con il limite di una pagina, cioè se è un multiplo esatto del parametro di sistema SHMLBA, che in
Linux è sempre uguale PAGE_SIZE.
Si tenga presente però che quando si usa NULL come valore di shmaddr, l’indirizzo restituito da
shmat può cambiare da processo a processo; pertanto se nell’area di memoria condivisa si salvano
anche degli indirizzi, si deve avere cura di usare valori relativi (in genere riferiti all’indirizzo di
partenza del segmento).
L’argomento shmflg permette di cambiare il comportamento della funzione; esso va spe-
cificato come maschera binaria, i bit utilizzati sono solo due e sono identificati dalle costanti
SHM_RND e SHM_RDONLY, che vanno combinate con un OR aritmetico. Specificando SHM_RND si
evita che shmat ritorni un errore quando shmaddr non è allineato ai confini di una pagina. Si
può quindi usare un valore qualunque per shmaddr, e il segmento verrà comunque agganciato,
ma al più vicino multiplo di SHMLBA (il nome della costante sta infatti per rounded, e serve per
specificare un indirizzo come arrotondamento, in Linux è equivalente a PAGE_SIZE).
L’uso 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 l’emissione di un segnale di SIGSEGV. Il comportamento
usuale di shmat è quello di agganciare il segmento con l’accesso in lettura e scrittura (ed il
processo deve aver questi permessi in shm_perm), non è prevista la possibilità di agganciare un
segmento in sola scrittura.
In caso di successo la funzione aggiorna anche i seguenti campi di shmid_ds:
#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 all’indirizzo shmaddr, con errno che assume il valore EINVAL.
La funzione sgancia dallo spazio degli indirizzi del processo un segmento di memoria con-
divisa; questo viene identificato con l’indirizzo 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:
Figura 11.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
378 CAPITOLO 11. L’INTERCOMUNICAZIONE FRA PROCESSI
libreria che ne semplifichino l’uso, automatizzando le operazioni più comuni; il codice, contenuto
nel file SharedMem.c, è riportato in fig. 11.24.
La prima funzione (3-16) è ShmCreate che, data una chiave, crea il segmento di memoria
condivisa restituendo il puntatore allo stesso. La funzione comincia (6) con il chiamare shmget,
usando il flag IPC_CREATE per creare il segmento qualora non esista, ed assegnandogli i privilegi
specificati dall’argomento perm e la dimensione specificata dall’argomento shm_size. In caso di
errore (7-9) si ritorna immediatamente un puntatore nullo, altrimenti (10) si prosegue aggancian-
do il segmento di memoria condivisa al processo con shmat. In caso di errore (11-13) si restituisce
di nuovo un puntatore nullo, infine (14) si inizializza con memset il contenuto del segmento al
valore costante specificato dall’argomento fill, e poi si ritorna il puntatore al segmento stesso.
La seconda funzione (17-31) è ShmFind, che, data una chiave, restituisce l’indirizzo del seg-
mento ad essa associato. Anzitutto (22) si richiede l’identificatore del segmento con shmget,
ritornando (23-25) un puntatore nullo in caso di errore. Poi si prosegue (26) agganciando il seg-
mento 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
di memoria condivisa, prima lo sgancia dal processo e poi lo rimuove. Il primo passo (37) è la
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 l’identificatore associato al segmento data
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ù
veloce, essa non è sempre il più appropriato, dato che, come abbiamo visto, si avrà comunque la
necessità di una sincronizzazione degli accessi. Per questo motivo, quando la comunicazione fra
processi è sequenziale, altri meccanismi come le pipe, le fifo o i socket, che non necessitano di
sincronizzazione esplicita, sono da preferire. Essa diventa l’unico meccanismo possibile quando la
comunicazione non è sequenziale34 o quando non può avvenire secondo una modalità predefinita.
Un esempio classico di uso della memoria condivisa è quello del “monitor ”, in cui viene per
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 l’operazione, potenzialmente onerosa, di ricavare e trattare i dati,
e dall’altra si evita al processo server di dover gestire l’invio a tutti i client di tutti i dati (non
potendo il server sapere quali di essi servono effettivamente al singolo client).
Nel nostro caso implementeremo un “monitor” di una directory: un processo si incaricherà
di tenere sotto controllo alcuni parametri relativi ad una directory (il numero dei file contenuti,
la dimensione totale, quante directory, link simbolici, file 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 fig. 11.25 si è riportata la sezione principale del corpo del programma server, insieme alle
definizioni 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 file DirMonitor.c.
Il programma usa delle variabili globali (2-14) per mantenere i valori relativi agli oggetti
usati per la comunicazione inter-processo; si è definita inoltre una apposita struttura DirProp
che contiene i dati relativi alle proprietà che si vogliono mantenere nella memoria condivisa, per
l’accesso da parte dei client.
34
come accennato in sez. 11.2.4 per la comunicazione non sequenziale si possono usare le code di messaggi,
attraverso l’uso del campo mtype, ma solo se quest’ultima può essere effettuata in forma di messaggio.
11.2. L’INTERCOMUNICAZIONE FRA PROCESSI DI SYSTEM V 379
Il programma, dopo la sezione, omessa, relativa alla gestione delle opzioni da riga di comando
(che si limitano alla eventuale stampa di un messaggio di aiuto a video ed all’impostazione della
durata dell’intervallo con cui viene ripetuto il calcolo delle proprietà della directory) controlla
(20-23) che sia stato specificato l’argomento necessario contenente il nome della directory da
tenere sotto controllo, senza il quale esce immediatamente con un messaggio di errore.
Poi, per verificare che l’argomento specifichi effettivamente una directory, si esegue (24-26)
380 CAPITOLO 11. L’INTERCOMUNICAZIONE FRA PROCESSI
su di esso una chdir, uscendo immediatamente in caso di errore. Questa funzione serve anche
per impostare la directory di lavoro del programma nella directory da tenere sotto controllo, in
vista del successivo uso della funzione daemon.35 Infine (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 l’esecuzione.
Il passo successivo (30-39) è quello di creare gli oggetti di intercomunicazione necessari.
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 è sufficiente per i dati
che useremo), uscendo (32-35) qualora la creazione ed il successivo agganciamento al processo
non abbia successo. Con l’indirizzo shmptr cosı̀ ottenuto potremo poi accedere alla memoria
condivisa, che, per come abbiamo lo abbiamo definito, sarà vista nella forma data da DirProp.
Infine (36-39) utilizzando sempre la stessa chiave, si crea, tramite le funzioni di interfaccia già
descritte in sez. 11.2.5, anche un mutex, che utilizzeremo per regolare l’accesso alla memoria
condivisa.
immagazzinati nella memoria condivisa con memset, e si esegue (45) un nuovo calcolo degli stessi
utilizzando la funzione DirScan; infine (46) si sblocca il mutex con MutexUnlock, e si attende
(47) per il periodo di tempo specificato a riga di comando con l’opzione -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
ci permette di effettuare la scansione delle voci della directory, chiamando per ciascuna di esse
la funzione ComputeValues, che esegue tutti i calcoli necessari.
Il codice di quest’ultima è riportato in fig. 11.26. Come si vede la funzione (2-16) è molto
semplice e si limita a chiamare (5) la funzione stat sul file 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 è all’interno del ciclo principale del pro-
gramma, con un mutex acquisito, perciò non è necessario effettuare nessun controllo e si può
accedere direttamente alla memoria condivisa usando shmptr per riempire i campi della struttu-
ra DirProp; cosı̀ prima (6-7) si sommano le dimensioni dei file ed il loro numero, poi, utilizzando
le macro di tab. 5.3, si contano (8-14) quanti ce ne sono per ciascun tipo.
In fig. 11.26 è riportato anche il codice (17-23) del gestore dei segnali di terminazione, usato
per chiudere il programma. Esso, oltre a provocare l’uscita del programma, si incarica anche di
cancellare tutti gli oggetti di intercomunicazione non più necessari. Per questo anzitutto (19)
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.
Infine (21) rimuove il mutex con MutexRemove ed esce (22).
Figura 11.27: Codice del programma client del monitor delle proprietà di una directory, ReadMonitor.c.
Il codice del client usato per leggere le informazioni mantenute nella memoria condivisa è
riportato in fig. 11.27. Al solito si è omessa la sezione di gestione delle opzioni e la funzione che
382 CAPITOLO 11. L’INTERCOMUNICAZIONE FRA PROCESSI
stampa a video le istruzioni; il codice completo è nei sorgenti allegati, nel file ReadMonitor.c.
Una volta conclusa la gestione delle opzioni a riga di comando il programma rigenera (7)
con ftok la stessa chiave usata dal server per identificare il segmento di memoria condivisa ed il
mutex, poi (8) richiede con ShmFind l’indirizzo della memoria condivisa agganciando al contempo
il segmento al processo, Infine (17-20) con MutexFind si richiede l’identificatore del mutex.
Completata l’inizializzazione 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
(23-31) si stampano i vari valori mantenuti nella memoria condivisa attraverso l’uso di shmptr.
Infine (41) con MutexUnlock si rilascia il mutex, prima di uscire.
Verifichiamo allora il funzionamento dei nostri programmi; al solito, usando le funzioni di
libreria occorre definire opportunamente LD_LIBRARY_PATH; poi si potrà lanciare il server con:
ed avendo usato daemon il comando ritornerà immediatamente. Una volta che il server è in
esecuzione, possiamo passare ad invocare il client per verificarne i risultati, in tal caso otterremo:
ed un rapido calcolo (ad esempio con ls -a | wc per contare i file) ci permette di verificare che
il totale dei file è giusto. Un controllo con ipcs ci permette inoltre di verificare la presenza di
un segmento di memoria condivisa e di un semaforo:
Se a questo punto aggiungiamo un file, ad esempio con touch prova, potremo verificare che,
passati nel peggiore dei casi almeno 10 secondi (o l’eventuale altro intervallo impostato per la
rilettura dei dati) avremo:
Ci sono 0 socket
Ci sono 0 device a caratteri
Ci sono 0 device a blocchi
Totale 72 file, per 489887 byte
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:
e inoltre potremo anche verificare che anche gli oggetti di intercomunicazione visti in precedenza
sono stati regolarmente cancellati:
mutex ), per indicare la disponibilità o meno di una risorsa, senza la necessità di un contatore
come i semafori, si possono utilizzare metodi alternativi.
La prima possibilità, utilizzata fin dalle origini di Unix, è quella di usare dei file di lock (per
i quali esiste anche una opportuna directory, /var/lock, nel filesystem 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 flag di O_CREAT e O_EXCL. In tal modo la creazione di un file di
lock può essere eseguita atomicamente, il processo che crea il file con successo si può considerare
come titolare del lock (e della risorsa ad esso associata) mentre il rilascio si può eseguire con una
chiamata ad unlink.
Un esempio dell’uso di questa funzione è mostrato dalle funzioni LockFile ed UnlockFile
riportate in fig. 11.28 (sono contenute in LockFile.c, un altro dei sorgenti allegati alla guida) che
permettono rispettivamente di creare e rimuovere un file di lock. Come si può notare entrambe
le funzioni sono elementari; la prima (4-10) si limita ad aprire il file di lock (9) nella modalità
descritta, mentre la seconda (11-17) lo cancella con unlink.
Figura 11.28: Il codice delle funzioni LockFile e UnlockFile che permettono di creare e rimuovere un file di
lock.
Uno dei limiti di questa tecnica è che, come abbiamo già accennato in sez. 6.2.1, questo
comportamento di open può non funzionare (la funzione viene eseguita, ma non è garantita
l’atomicità dell’operazione) se il filesystem su cui si va ad operare è su NFS; in tal caso si può
adottare una tecnica alternativa che prevede l’uso della link per creare come file di lock un hard
link ad un file esistente; se il link esiste già e la funzione fallisce, significa che la risorsa è bloccata
e potrà essere sbloccata solo con un unlink, altrimenti il link è creato ed il lock acquisito; il
controllo e l’eventuale acquisizione sono atomici; la soluzione funziona anche su NFS, ma ha un
altro difetto è che è quello di poterla usare solo se si opera all’interno di uno stesso filesystem.
In generale comunque l’uso di un file 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 file di lock) e questa deve essere sempre cancellata
esplicitamente. Inoltre il controllo della disponibilità può essere eseguito solo con una tecnica di
polling, ed è quindi molto inefficiente.
38
questo è quanto dettato dallo standard POSIX.1, ciò non toglie che in alcune implementazioni questa tecnica
possa non funzionare; in particolare per Linux, nel caso di NFS, si è comunque soggetti alla possibilità di una race
condition.
11.3. TECNICHE ALTERNATIVE 385
La tecnica dei file di lock ha comunque una sua utilità, e può essere usata con successo quando
l’esigenza è solo quella di segnalare l’occupazione di una risorsa, senza necessità di attendere che
questa si liberi; ad esempio la si usa spesso per evitare interferenze sull’uso delle porte seriali da
parte di più programmi: qualora si trovi un file di lock il programma che cerca di accedere alla
seriale si limita a segnalare che la risorsa non è disponibile.
Figura 11.29: Il codice delle funzioni che permettono per la gestione dei mutex con il file locking.
11.4. L’INTERCOMUNICAZIONE FRA PROCESSI DI POSIX 387
La sesta funzione (41-55) è ReadMutex e serve a leggere lo stato del mutex. In questo caso
si prepara (46-49) la solita struttura lock come l’acquisizione del lock, ma si effettua (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)
altrimenti (54). Per questo motivo la funzione restituirà -1 in caso di errore e uno dei due valori
F_UNLCK o F_WRLCK39 in caso di successo, ad indicare che il mutex è, rispettivamente, libero o
occupato.
Basandosi sulla semantica dei file lock POSIX valgono tutte le considerazioni relative al
comportamento di questi ultimi fatte in sez. 12.1.3; questo significa ad esempio che, al contrario
di quanto avveniva con l’interfaccia basata sui semafori, chiamate multiple a UnlockMutex o
LockMutex non si cumulano e non danno perciò nessun inconveniente.
• 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.
• se il nome inizia per una / chiamate differenti allo stesso nome fanno riferimento allo stesso
oggetto, altrimenti l’interpretazione del nome dipende dall’implementazione.
• l’interpretazione di ulteriori / presenti nel nome dipende dall’implementazione.
ed esso sarà utilizzato come radice sulla quale vengono risolti i nomi delle code di messaggi
che iniziano con una “/”. Le opzioni di mount accettate sono uid, gid e mode che permettono
rispettivamente di impostare l’utente, il gruppo ed i permessi associati al filesystem.
La funzione che permette di aprire (e crearla se non esiste ancora) una coda di messaggi
POSIX è mq_open, ed il suo prototipo è:
#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 per un errore; nel
quel caso errno assumerà i valori:
EACCES il processo non ha i privilegi per accedere al alla memoria secondo quanto specificato
da oflag.
EEXIST si è specificato O_CREAT e O_EXCL ma la coda già esiste.
EINVAL il file non supporta la funzione, o si è specificato O_CREAT con una valore non nullo di
attr e valori non validi di mq_maxmsg e mq_msgsize.
ENOENT non si è specificato O_CREAT ma la coda non esiste.
ed inoltre ENOMEM, ENOSPC, EFAULT, EMFILE, EINTR ed ENFILE.
O_RDONLY Apre la coda solo per la ricezione di messaggi. Il processo potrà usare il descrittore
con mq_receive ma non con mq_send.
O_WRONLY Apre la coda solo per la trasmissione di messaggi. Il processo potrà usare il
descrittore con mq_send ma non con mq_receive.
O_RDWR Apre la coda solo sia per la trasmissione che per la ricezione.
O_CREAT Necessario qualora si debba creare la coda; la presenza di questo bit richiede la
presenza degli ulteriori argomenti mode e attr.
O_EXCL Se usato insieme a O_CREAT fa fallire la chiamata se la coda esiste già, altrimenti
esegue la creazione atomicamente.
I primi tre bit specificano la modalità di apertura della coda, e sono fra loro esclusivi. Ma
qualunque sia la modalità in cui si è aperta una coda, questa potrà essere riaperta più volte in
una modalità diversa, e vi si potrà sempre accedere attraverso descrittori diversi, esattamente
come si può fare per i file normali.
Se la coda non esiste e la si vuole creare si deve specificare O_CREAT, in tal caso occorre anche
specificare i permessi di creazione con l’argomento mode;48 i valori di quest’ultimo 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 specificati anche gli attributi specifici
della coda tramite l’argomento attr; quest’ultimo è un puntatore ad una apposita struttura
mq_attr, la cui definizione è riportata in fig. 11.30.
struct mq_attr {
long mq_flags ; /* message queue flags */
long mq_maxmsg ; /* maximum number of messages */
long mq_msgsize ; /* maximum message size */
long mq_curmsgs ; /* number of messages currently queued */
};
Figura 11.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 specificati sono mq_maxmsg
e mq_msgsize, che indicano rispettivamente il numero massimo di messaggi che può contenere
e la dimensione massima di un messaggio. Il valore dovrà essere positivo e minore dei rispettivi
limiti di sistema MQ_MAXMSG e MQ_MSGSIZE, altrimenti la funzione fallirà con un errore di EINVAL.
Se attr è un puntatore nullo gli attributi della coda saranno impostati ai valori predefiniti.
Quando l’accesso alla coda non è più necessario si può chiudere il relativo descrittore con la
funzione mq_close, il cui prototipo è:
#include <mqueue.h>
int mq_close(mqd_t mqdes)
Chiude la coda mqdes.
La funzione restituisce 0 in caso di successo e -1 per un errore; nel quel caso errno assumerà i
valori EBADF o EINTR.
La funzione è analoga a close,49 dopo la sua esecuzione il processo non sarà più in grado
di usare il descrittore della coda, ma quest’ultima continuerà ad esistere nel sistema e potrà
essere acceduta con un’altra chiamata a mq_open. All’uscita di un processo tutte le code aperte,
cosı̀ come i file, vengono chiuse automaticamente. Inoltre se il processo aveva agganciato una
richiesta di notifica sul descrittore che viene chiuso, questa sarà rilasciata e potrà essere richiesta
da qualche altro processo.
Quando si vuole effettivamente rimuovere una coda dal sistema occorre usare la funzione
mq_unlink, il cui prototipo è:
#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à
gli stessi valori riportati da unlink.
48
fino al 2.6.14 per un bug i valori della umask del processo non venivano applicati a questi permessi.
49
in Linux, dove le code sono implementate come file su un filesystem dedicato, è esattamente la stessa funzione.
11.4. L’INTERCOMUNICAZIONE FRA PROCESSI DI POSIX 391
Anche in questo caso il comportamento della funzione è analogo a quello di unlink per i
file,50 la funzione rimuove la coda name, cosı̀ che una successiva chiamata a mq_open fallisce o
crea una coda diversa.
Come per i file ogni coda di messaggi ha un contatore di riferimenti, per cui la coda non
viene effettivamente rimossa dal sistema fin 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
descrittore aperto su di essa. Allo stesso modo una coda ed i suoi contenuti resteranno disponibili
all’interno del sistema anche quando quest’ultima non è aperta da nessun processo (questa è una
delle differenze più rilevanti nei confronti di pipe e fifo). La sola differenza fra code di messaggi
POSIX e file normali è che, essendo il filesystem delle code di messaggi virtuale e basato su
oggetti interni al kernel, il suo contenuto viene perduto con il riavvio del sistema.
Come accennato ad ogni coda di messaggi è associata una struttura mq_attr, che può essere
letta e modificata attraverso le due funzioni mq_getattr e mq_setattr, i cui 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)
Modifica 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.
La funzione mq_getattr legge i valori correnti degli attributi della coda nella struttura
puntata da mqstat; di questi l’unico relativo allo stato corrente della coda è mq_curmsgs che
indica il numero di messaggi da essa contenuti, gli altri indicano le caratteristiche generali della
stessa.
La funzione mq_setattr permette di modificare gli attributi di una coda tramite i valori
contenuti nella struttura puntata da mqstat, ma può essere modificato solo il campo mq_flags,
gli altri campi vengono ignorati. In particolare i valori di mq_maxmsg e mq_msgsize possono
essere specificati solo in fase ci creazione della coda. Inoltre i soli valori possibili per mq_flags
sono 0 e O_NONBLOCK, per cui alla fine la funzione può essere utilizzata solo per abilitare o
disabilitare la modalità non bloccante. L’argomento omqstat viene usato, quando diverso da
NULL, per specificare l’indirizzo 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 l’inserimento 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 l’inserimento di un messaggio su una coda entro il tempo abs_timeout.
Le funzioni restituiscono 0 in caso di successo e −1 per un errore; nel quel caso errno assumerà i
valori:
EAGAIN si è aperta la coda con O_NONBLOCK, e la coda è piena.
EMSGSIZE la lunghezza del messaggio msg_len eccede il limite impostato per la coda.
EINVAL si è specificato un valore nullo per msg_len, o un valore di msg_prio fuori dai limiti,
o un valore non valido per abs_timeout.
ETIMEDOUT l’inserimento del messaggio non è stato effettuato entro il tempo stabilito.
ed inoltre EBADF, ENOMEM ed EINTR.
50
di nuovo l’implementazione di Linux usa direttamente unlink.
392 CAPITOLO 11. L’INTERCOMUNICAZIONE FRA PROCESSI
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:
EAGAIN si è aperta la coda con O_NONBLOCK, e la coda è vuota.
EMSGSIZE la lunghezza del messaggio sulla coda eccede il valore msg_len specificato per la
ricezione.
EINVAL si è specificato un valore nullo per msg_ptr, o un valore non valido per abs_timeout.
ETIMEDOUT la ricezione del messaggio non è stata effettuata entro il tempo stabilito.
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
stessa priorità. Una volta ricevuto il messaggio viene tolto dalla coda e la sua dimensione viene
restituita come valore di ritorno.53
Se la dimensione specificata da msg_len non è sufficiente a contenere il messaggio, entrambe
le funzioni, al contrario di quanto avveniva nelle code di messaggi di SysV, ritornano un errore
di EMSGSIZE senza estrarre il messaggio. È 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 buffer sufficientemente ampi per la
lettura.
Se si specifica un puntatore per l’argomento msg_prio il valore della priorità del messaggio
viene memorizzato all’indirizzo da esso indicato. Qualora non interessi usare la priorità dei
messaggi si può specificare NULL, ed usare un valore nullo della priorità nelle chiamate a mq_send.
Si noti che con le code di messaggi POSIX non si ha la possibilità di selezionare quale
messaggio estrarre con delle condizioni sulla priorità, a differenza di quanto avveniva con le code
di messaggi di SysV che permettono invece la selezione in base al valore del campo mtype.
51
o si sia impostato il flag O_NONBLOCK sul file descriptor della coda.
52
deve essere specificato un tempo assoluto tramite una struttura timespec (vedi fig. 5.8) indicato in numero
di secondi e nanosecondi a partire dal 1 gennaio 1970.
53
si tenga presente che 0 è una dimensione valida e che la condizione di errore è restituita dal valore -1; Stevens
in [14] fa notare che questo è uno dei casi in cui vale ciò che lo standard non dice, una dimensione nulla infatti,
pur non essendo citata, non viene proibita.
11.4. L’INTERCOMUNICAZIONE FRA PROCESSI DI POSIX 393
Qualora la coda sia vuota entrambe le funzioni si bloccano, a meno che non si sia selezio-
nata la modalità non bloccante; in tal caso entrambe ritornano immediatamente con l’errore
EAGAIN. Anche in questo caso la sola differenza fra le due funzioni è che la seconda non attende
indefinitamente e passato il tempo massimo abs_timeout ritorna comunque con un errore di
ETIMEDOUT.
Uno dei problemi sottolineati da Stevens in [14], comuni ad entrambe le tipologie di code
messaggi, è che non è possibile per chi riceve identificare chi è che ha inviato il messaggio, in
particolare non è possibile sapere da quale utente esso provenga. Infatti, in mancanza di un
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 l’uso del meccanismo di notifica, sia possibile superare in parte questo
problema.
Una caratteristica specifica delle code di messaggi POSIX è la possibilità di usufruire di un
meccanismo di notifica asincrono; questo può essere attivato usando la funzione mq_notify, il
cui prototipo è:
#include <mqueue.h>
int mq_notify(mqd_t mqdes, const struct sigevent *notification)
Attiva il meccanismo di notifica per la coda mqdes.
La funzione restituisce 0 in caso di successo e -1 in caso di errore; nel quel caso errno assumerà i
valori:
EBUSY c’è già un processo registrato per la notifica.
EBADF il descrittore non fa riferimento ad una coda di messaggi.
immediatamente inviato, mentre per il meccanismo di notifica 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 specificato da notification->sigev_signo, e la coda diventa disponibile per una
ulteriore registrazione. Questo comporta che se si vuole mantenere il meccanismo di notifica
occorre ripetere la registrazione chiamando nuovamente mq_notify all’interno del gestore del
segnale di notifica. A differenza della situazione simile che si aveva con i segnali non affidabili,57
questa caratteristica non configura una race condition perché l’invio di un segnale avviene solo
se la coda è vuota; pertanto se si vuole evitare di correre il rischio di perdere eventuali ulteriori
segnali inviati nel lasso di tempo che occorre per ripetere la richiesta di notifica basta avere cura
di eseguire questa operazione prima di estrarre i messaggi presenti dalla coda.
L’invio del segnale di notifica avvalora alcuni campi di informazione restituiti al gestore
attraverso la struttura siginfo_t (definita in fig. 9.9). In particolare si_pid viene impostato
al valore del pid del processo che ha emesso il segnale, si_uid all’userid effettivo, si_code a
SI_MESGQ, e si_errno a 0. Questo ci dice che, se si effettua la ricezione dei messaggi usando
esclusivamente il meccanismo di notifica, è possibile ottenere le informazioni sul processo che ha
inserito un messaggio usando un gestore per il segnale in forma estesa.58
ad /etc/fstab. In realtà si può montare un filesystem tmpfs dove si vuole, per usarlo come
RAM disk, con un comando del tipo:
Il filesystem riconosce, oltre quelle mostrate, le opzioni uid e gid che identificano rispetti-
vamente utente e gruppo cui assegnarne la titolarità, e nr_blocks che permette di specificarne
la dimensione in blocchi, cioè in multipli di PAGECACHE_SIZE che in questo caso è l’unità di
allocazione elementare.
La funzione che permette di aprire un segmento di memoria condivisa POSIX, ed eventual-
mente di crearlo se non esiste ancora, è shm_open; il suo prototipo è:
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
int shm_open(const char *name, int oflag, mode_t mode)
Apre un segmento di memoria condivisa.
La funzione restituisce un file descriptor positivo in caso di successo e -1 in caso di errore; nel quel
caso errno assumerà gli stessi valori riportati da open.
57
l’argomento è stato affrontato in 9.1.2.
58
di nuovo si faccia riferimento a quanto detto al proposito in sez. 9.4.3 e sez. 9.5.1.
59
le funzioni sono state introdotte con le glibc-2.2.
11.4. L’INTERCOMUNICAZIONE FRA PROCESSI DI POSIX 395
La funzione apre un segmento di memoria condivisa identificato dal nome name. Come già
spiegato in sez. 11.4.1 questo nome può essere specificato in forma standard solo facendolo iniziare
per “/” e senza ulteriori “/”. Linux supporta comunque nomi generici, che verranno interpretati
prendendo come radice /dev/shm.60
La funzione è del tutto analoga ad open ed analoghi sono i valori che possono essere specificati
per oflag, che deve essere specificato 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_RDONLY Apre il file descriptor associato al segmento di memoria condivisa per l’accesso in
sola lettura.
O_RDWR Apre il file descriptor associato al segmento di memoria condivisa per l’accesso in
lettura e scrittura.
O_CREAT 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 file.
O_EXCL Se usato insieme a O_CREAT fa fallire la chiamata a shm_open se il segmento esiste
già, altrimenti esegue la creazione atomicamente.
O_TRUNC Se il segmento di memoria condivisa esiste già, ne tronca le dimensioni a 0 byte.
In caso di successo la funzione restituisce un file descriptor associato al segmento di memoria
condiviso con le stesse modalità di open61 viste in sez. 6.2.1; in particolare viene impostato il
flag FD_CLOEXEC. Chiamate effettuate da diversi processi usando lo stesso nome, restituiranno
file descriptor associati allo stesso segmento (cosı̀ come, nel caso di file di dati, essi sono associati
allo stesso inode). In questo modo è possibile effettuare una chiamata ad mmap sul file descriptor
restituito da shm_open ed i processi vedranno lo stesso segmento di memoria condivisa.
Quando il nome non esiste il segmento può essere creato specificando O_CREAT; in tal caso
il segmento avrà (cosı̀ come i nuovi file) lunghezza nulla. Dato che un segmento di lunghezza
nulla è di scarsa utilità, per impostarne la dimensione si deve usare ftruncate (vedi sez. 5.2.3),
prima di mapparlo in memoria con mmap. Si tenga presente che una volta chiamata mmap si può
chiudere il file descriptor (con close), senza che la mappatura ne risenta.
Come per i file, quando si vuole effettivamente rimuovere segmento di memoria condivisa,
occorre usare la funzione shm_unlink, il cui prototipo è:
#include <sys/mman.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à
gli stessi valori riportati da unlink.
La funzione è del tutto analoga ad unlink, e si limita a cancellare il nome del segmento da
/dev/shm, senza nessun effetto né sui file descriptor precedentemente aperti con shm_open, né sui
segmenti già mappati in memoria; questi verranno cancellati automaticamente dal sistema solo
con le rispettive chiamate a close e munmap. Una volta eseguita questa funzione però, qualora
si richieda l’apertura di un segmento con lo stesso nome, la chiamata a shm_open fallirà, a meno
di non aver usato O_CREAT, in quest’ultimo caso comunque si otterrà un file descriptor che fa
riferimento ad un segmento distinto da eventuali precedenti.
60
occorre pertanto evitare di specificare qualcosa del tipo /dev/shm/nome all’interno di name, perché questo
comporta, da parte delle funzioni di libreria, il tentativo di accedere a /dev/shm/dev/shm/nome.
61
in realtà, come accennato, shm_open è un semplice wrapper per open, usare direttamente quest’ultima avrebbe
lo stesso effetto.
396 CAPITOLO 11. L’INTERCOMUNICAZIONE FRA PROCESSI
Figura 11.31: Il codice delle funzioni di gestione dei segmenti di memoria condivisa POSIX.
Come esempio per l’uso di queste funzioni vediamo come è possibile riscrivere una interfaccia
semplificata analoga a quella vista in fig. 11.24 per la memoria condivisa in stile SysV. Il codice,
riportato in fig. 11.31, è sempre contenuto nel file SharedMem.c dei sorgenti allegati.
La prima funzione (1-24) è CreateShm che, dato un nome nell’argomento name crea un nuovo
segmento di memoria condivisa, accessibile in lettura e scrittura, e ne restituisce l’indirizzo.
Anzitutto si definiscono (8) i flag 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
filesystem i permessi specificati dall’argomento perm. In caso di errore (10-12) si restituisce
un puntatore nullo, altrimenti si prosegue impostando (14) la dimensione del segmento con
11.4. L’INTERCOMUNICAZIONE FRA PROCESSI DI POSIX 397
11.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,62 fornita attraverso la sezione delle estensioni
real-time delle glibc.63 Esisteva inoltre una libreria che realizzava (parzialmente) l’interfaccia
POSIX usando le funzioni dei semafori di SysV IPC (mantenendo cosı̀ tutti i problemi sottolineati
in sez. 11.2.5).
A partire dal kernel 2.5.7 è stato introdotto un meccanismo di sincronizzazione completamen-
te nuovo, basato sui cosiddetti futex,64 con il quale è stato possibile implementare una versione
nativa dei semafori POSIX. Grazie a questo con i kernel della serie 2.6 e le nuove versioni del-
le 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 dell’interfaccia dei
semafori POSIX.
Anche in questo caso è necessario appoggiarsi alla libreria per le estensioni real-time librt,
questo significa che se si vuole utilizzare questa interfaccia, oltre ad utilizzare gli opportuni file
di definizione, occorrerà compilare i programmi con l’opzione -lrt.
La funzione che permette di creare un nuovo semaforo POSIX, creando il relativo file, o
di accedere ad uno esistente, è sem_open, questa prevede due forme diverse a seconda che sia
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 l’indirizzo del semaforo in caso di successo e SEM_FAILED in caso di errore;
nel quel caso errno assumerà i valori:
EACCESS il semaforo esiste ma non si hanno permessi sufficienti per accedervi.
EEXIST si sono specificati O_CREAT e O_EXCL ma il semaforo esiste.
EINVAL il valore di value eccede SEM_VALUE_MAX.
ENAMETOOLONG si è utilizzato un nome troppo lungo.
ENOENT non si è usato O_CREAT ed il nome specificato non esiste.
ed inoltre ENFILE ed ENOMEM.
62
questo significava che i semafori erano visibili solo all’interno dei thread creati da un singolo processo, e non
potevano essere usati come meccanismo di sincronizzazione fra processi diversi.
63
quelle che si accedono collegandosi alla libreria librt.
64
la sigla sta per fast user mode mutex.
398 CAPITOLO 11. L’INTERCOMUNICAZIONE FRA PROCESSI
L’argomento name definisce il nome del semaforo che si vuole utilizzare, ed è quello che
permette a processi diversi di accedere allo stesso semaforo. Questo deve essere specificato con un
pathname nella forma /qualchenome, che non ha una corrispondenza diretta con un pathname
reale; con Linux infatti i file associati ai semafori sono mantenuti nel filesystem virtuale /dev/shm,
e gli viene assegnato automaticamente un nome nella forma sem.qualchenome.65
L’argomento oflag è quello che controlla le modalità con cui opera la funzione, ed è passato
come maschera binaria; i bit corrispondono a quelli utilizzati per l’analogo 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 specificare sia un valore
iniziale con l’argomento value,66 che una maschera dei permessi con l’argomento mode;67 questi
verranno assegnati al semaforo appena creato. Se il semaforo esiste già i suddetti valori saranno
invece ignorati. Usando il flag O_EXCL si richiede invece la verifica che il semaforo non esiste,
usandolo insieme ad O_CREAT la funzione fallisce qualora un semaforo con lo stesso nome sia già
presente.
La funzione restituisce in caso di successo un puntatore all’indirizzo del semaforo con un
valore di tipo sem_t *, è questo valore che dovrà essere passato alle altre funzioni per operare
sul semaforo stesso. Si tenga presente che, come accennato in sez. 11.4.1, i semafori usano la
semantica standard dei file per quanto riguarda i controlli di accesso.
Questo significa che un nuovo semaforo viene sempre creato con l’user-ID ed il group-ID
effettivo del processo chiamante, e che i permessi indicati con mode vengono filtrati dal valore
della umask del processo. Inoltre per poter aprire un semaforo è necessario avere su di esso sia
il permesso di lettura che quello di scrittura.
Una volta che si sia ottenuto l’indirizzo di un semaforo, sarà possibile utilizzarlo; se si ricorda
quanto detto all’inizio di sez. 11.2.5, dove si sono introdotti i concetti generali relativi ai semafori,
le operazioni principali sono due, quella che richiede l’uso di una risorsa bloccando il semaforo e
quella che rilascia la risorsa liberando il semaforo. La prima operazione è effettuata dalla funzione
sem_wait, il cui prototipo è:
#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à
i valori:
EINTR la funzione è stata interrotta da un segnale.
EINVAL 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 significa che la risorsa è disponibile, la funzione ha successo, il
valore del semaforo viene diminuito di 1 ed essa ritorna immediatamente; se il valore è nullo la
funzione si blocca fintanto che il valore del semaforo non torni positivo68 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
si avrà un errore di EINTR) e che questo avverrà comunque, anche se si è richiesta la semantica
65
si ha cioè una corrispondenza per cui /qualchenome viene rimappato, nella creazione tramite sem_open, su
/dev/shm/sem.qualchenome.
66
e si noti come cosı̀ diventa possibile, differenza di quanto avviene per i semafori del SysV IPC, effettuare in
maniera atomica creazione ed inizializzazione di un semaforo usando una unica funzione.
67
anche questo argomento prende gli stessi valori utilizzati per l’analogo di open, che si sono illustrati in dettaglio
sez. 5.3.1.
68
ovviamente per opera di altro processo che lo rilascia chiamando sem_post.
11.4. L’INTERCOMUNICAZIONE FRA PROCESSI DI POSIX 399
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
effettuare un tentativo di acquisizione senza bloccarsi; il suo prototipo è:
#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à
gli stessi valori:
EAGAIN il semaforo non può essere acquisito senza bloccarsi.
EINVAL il semaforo sem non esiste.
La funzione restituisce 0 in caso di successo e −1 in caso di errore; nel quel caso errno assumerà
gli stessi valori:
ETIMEDOUT è scaduto il tempo massimo di attesa.
EINVAL il semaforo sem non esiste.
EINTR la funzione è stata interrotta da un segnale.
Anche in questo caso il comportamento della funzione è identico a quello di sem_wait, la sola
differenza consiste nel fatto che con questa funzione è possibile impostare tramite l’argomento
abs_timeout un tempo limite per l’attesa, scaduto il quale la funzione ritorna comunque, anche
se non è possibile acquisire il semaforo. In tal caso la funzione fallirà, riportando un errore di
ETIMEDOUT.
La seconda funzione principale utilizzata per l’uso dei semafori è sem_post, che viene usata
per rilasciare un semaforo occupato o, in generale, per aumentare di una unità il valore dello
stesso anche qualora non fosse occupato;69 il suo prototipo è:
#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à
i valori:
EINVAL il semaforo sem non esiste.
La funzione incrementa di uno il valore corrente del semaforo indicato dall’argomento sem,
se questo era nullo la relativa risorsa risulterà sbloccata, cosicché un altro processo (o thread )
eventualmente bloccato in una sem_wait sul semaforo potrà essere svegliato e rimesso in esecu-
zione. Si tenga presente che la funzione è sicura per l’uso all’interno di un gestore di segnali (si
ricordi quanto detto in sez. 9.4.5).
69
si ricordi che in generale un semaforo viene usato come indicatore di un numero di risorse disponibili.
400 CAPITOLO 11. L’INTERCOMUNICAZIONE FRA PROCESSI
La funzione restituisce 0 in caso di successo e −1 in caso di errore; nel quel caso errno assumerà
i valori:
EINVAL il semaforo sem non esiste.
La funzione legge il valore del semaforo indicato dall’argomento sem e lo restituisce nella
variabile intera puntata dall’argomento sval. Qualora ci siano uno o più processi bloccati in
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,
ovviamente non si può prendere il risultato riportato in sval che come indicazione, il valore del
semaforo infatti potrebbe essere già stato modificato al ritorno della funzione.
Una volta che non ci sia più la necessità di operare su un semaforo se ne può terminare l’uso
con la funzione sem_close, il cui prototipo è:
#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à
i valori:
EINVAL il semaforo sem non esiste.
La funzione chiude il semaforo indicato dall’argomento sem; questo comporta che tutte le
risorse che il sistema può avere assegnato al processo nell’uso dello stesso vengono rilasciate.
Questo significa che un altro processo bloccato sul semaforo a causa della acquisizione da parte
del processo che chiama sem_close potrà essere riavviato.
Si tenga presente poi che come per i file all’uscita 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. 11.2.5) per i quali le risorse possono
restare bloccate. Si tenga poi presente che, a differenza di quanto avviene per i file, 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 significa che una volta che si è creato un semaforo con sem_open questo continuerà ad
esistere fintanto che il kernel resta attivo (vale a dire fino ad un successivo riavvio) a meno che
non lo si cancelli esplicitamente. Per far questo si può utilizzare la funzione sem_unlink, il cui
prototipo è:
#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à
i valori:
EACCESS non si hanno i permessi necessari a cancellare il semaforo.
ENAMETOOLONG il nome indicato è troppo lungo.
ENOENT il semaforo name non esiste.
La funzione rimuove il semaforo indicato dall’argomento name, che prende un valore identico a
quello usato per creare il semaforo stesso con sem_open. Il semaforo viene rimosso dal filesystem
11.4. L’INTERCOMUNICAZIONE FRA PROCESSI DI POSIX 401
#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à
i valori:
EINVAL il valore di value eccede SEM_VALUE_MAX.
ENOSYS il valore di pshared non è nullo ed il sistema non supporta i semafori per i processi.
#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à
i valori:
EINVAL il valore di value eccede SEM_VALUE_MAX.
La funzione prende come unico argomento l’indirizzo 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 effettivamente inutilizzato, la distruzione di un
semaforo su cui sono presenti processi (o thread ) in attesa (cioè bloccati in una sem_wait)
provoca un comportamento indefinito.
70
si ricordi che i tratti di memoria condivisa vengono mantenuti nei processi figli attraverso la funzione fork.
402 CAPITOLO 11. L’INTERCOMUNICAZIONE FRA PROCESSI
Si tenga presente infine che utilizzare un semaforo che è stato distrutto con sem_destroy
di nuovo può dare esito a comportamenti indefiniti. Nel caso ci si trovi in una tale evenienza
occorre reinizializzare il semaforo una seconda volta con sem_init.
Come esempio di uso sia della memoria condivisa che dei semafori POSIX si sono scritti
due semplici programmi con i quali è possibile rispettivamente monitorare il contenuto di un
segmento di memoria condivisa e modificarne il contenuto.
Il corpo principale del primo dei due, il cui codice completo è nel file message_getter.c
dei sorgenti allegati, è riportato in fig. 11.32; si è tralasciata la parte che tratta la gestione
delle opzioni a riga di comando (che consentono di impostare un nome diverso per il semaforo
e il segmento di memoria condivisa) ed il controllo che al programma venga fornito almeno un
argomento, contenente la stringa iniziale da inserire nel segmento di memoria condivisa.
Lo scopo del programma è quello di creare un segmento di memoria condivisa su cui registrare
una stringa, e tenerlo sotto osservazione stampando la stessa una volta al secondo. Si utilizzerà
un semaforo per proteggere l’accesso in lettura alla stringa, in modo che questa non possa essere
modificata dall’altro programma prima di averla finita di stampare.
La parte iniziale del programma contiene le definizioni (1-8) del gestore del segnale usato per
liberare le risorse utilizzate, delle variabili globali contenenti i nomi di default del segmento di
memoria condivisa e del semaforo (il default scelto è messages), e delle altre variabili utilizzate
dal programma.
Come prima istruzione (10) si è provveduto ad installare un gestore di segnale che consentirà
di effettuare le operazioni di pulizia (usando la funzione Signal illustrata in fig. 9.10), dopo di
che (10-16) si è creato il segmento di memoria condivisa con la funzione CreateShm che abbiamo
appena trattato in sez. 11.4.3, uscendo con un messaggio in caso di errore.
Si tenga presente che la funzione CreateShm richiede che il segmento non sia già presente e
fallirà qualora un’altra istanza, o un altro programma abbia già allocato un segmento con quello
stesso nome. Per semplicità di gestione si è usata una dimensione fissa pari a 256 byte, definita
tramite la costante MSGMAXSIZE.
Il passo successivo (17-21) è quello della creazione del semaforo che regola l’accesso al seg-
mento di memoria condivisa con sem_open; anche in questo caso si gestisce l’uscita con stampa
di un messaggio in caso di errore. Anche per il semaforo, avendo specificato la combinazione
di flag O_CREAT|O_EXCL come secondo argomento, si esce qualora fosse già esistente; altrimenti
esso verrà creato con gli opportuni permessi specificati dal terzo argomento, (indicante lettura e
scrittura in notazione ottale). Infine il semaforo verrà inizializzato ad un valore nullo (il quarto
argomento), corrispondete allo stato in cui risulta bloccato.
A questo punto (23) si potrà inizializzare il messaggio posto nel segmento di memoria condi-
visa usando la stringa passata come argomento al programma. Essendo il semaforo stato creato
già bloccato non ci si dovrà preoccupare di eventuali race condition qualora il programma di
modifica del messaggio venisse lanciato proprio in questo momento. Una volta inizializzato il
messaggio occorrerà però rilasciare il semaforo (25-28) per consentirne l’uso; in tutte queste
operazioni si provvederà ad uscire dal programma con un opportuno messaggio in caso di errore.
Una volta completate le inizializzazioni il ciclo principale del programma (29-47) viene ripetu-
to indefinitamente (29) per stampare sia il contenuto del messaggio che una serie di informazioni
di controllo. Il primo passo (30-34) è quello di acquisire (con sem_getvalue, con uscita in caso di
errore) e stampare il valore del semaforo ad inizio del ciclo; seguito (35-36) dal tempo corrente.
Prima della stampa del messaggio invece si deve acquisire il semaforo (31-34) per evitare
accessi concorrenti alla stringa da parte del programma di modifica. Una volta eseguita la stampa
404 CAPITOLO 11. L’INTERCOMUNICAZIONE FRA PROCESSI
(41) il semaforo dovrà essere rilasciato (42-45). Il passo finale (46) è attendere per un secondo
prima di eseguire da capo il ciclo.
Per uscire in maniera corretta dal programma sarà necessario interromperlo con il break da
tastiera (C-c), che corrisponde all’invio del segnale SIGINT, per il quale si è installato (10) una
opportuna funzione di gestione, riportata in fig. 11.33. La funzione è molto semplice e richiama
le funzioni di rimozione sia per il segmento di memoria condivisa che per il semaforo, garantendo
cosı̀ che possa essere riaperto ex-novo senza errori in un futuro riutilizzo del comando.
In questo capitolo affronteremo le tematiche relative alla gestione avanzata dei file. Inizieremo con
la trattazione delle problematiche del file locking e poi prenderemo in esame le varie funzionalità
avanzate che permettono una gestione più sofisticata dell’I/O su file, a partire da quelle che
consentono di gestire l’accesso contemporaneo a più file esaminando le varie modalità alternative
di gestire l’I/O per concludere con la gestione dei file mappati in memoria e le altre funzioni
avanzate che consentono un controllo più dettagliato delle modalità di I/O.
407
408 CAPITOLO 12. LA GESTIONE AVANZATA DEI FILE
In generale si distinguono due tipologie di file lock ;2 la prima è il cosiddetto shared lock,
detto anche read lock in quanto serve a bloccare l’accesso in scrittura su un file affinché il suo
contenuto non venga modificato mentre lo si legge. Si parla appunto di blocco condiviso in quanto
più processi possono richiedere contemporaneamente uno shared lock su un file per proteggere il
loro accesso in lettura.
La seconda tipologia è il cosiddetto exclusive lock, detto anche write lock in quanto serve
a bloccare l’accesso su un file (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ò
richiedere un exclusive lock su un file per proteggere il suo accesso in scrittura.
In Linux sono disponibili due interfacce per utilizzare l’advisory locking, la prima è quella
derivata da BSD, che è basata sulla funzione flock, la seconda è quella recepita dallo standard
POSIX.1 (che è derivata dall’interfaccia usata in System V), che è basata sulla funzione fcntl.
I file lock sono implementati in maniera completamente indipendente nelle due interfacce,3 che
pertanto possono coesistere senza interferenze.
Entrambe le interfacce prevedono la stessa procedura di funzionamento: si inizia sempre con
il richiedere l’opportuno file lock (un exclusive lock per una scrittura, uno shared lock per una
lettura) prima di eseguire l’accesso ad un file. Se il blocco viene acquisito il processo prosegue
l’esecuzione, altrimenti (a meno di non aver richiesto un comportamento non bloccante) viene
posto in stato di sleep. Una volta finite le operazioni sul file si deve provvedere a rimuovere il
blocco.
La situazione delle varie possibilità che si possono verificare è riassunta in tab. 12.1, dove si
sono riportati, a seconda delle varie tipologie di blocco già presenti su un file, il risultato che si
avrebbe in corrispondenza di una ulteriore richiesta da parte di un processo di un blocco nelle
due tipologie di file lock menzionate, con un successo o meno della richiesta.
Richiesta Stato del file
Nessun lock Read lock Write lock
Read lock SI SI NO
Write lock SI NO NO
Si tenga presente infine che il controllo di accesso e la gestione dei permessi viene effettuata
quando si apre un file, l’unico controllo residuo che si può avere riguardo il file locking è che il
tipo di blocco che si vuole ottenere su un file deve essere compatibile con le modalità di apertura
dello stesso (in lettura per un read lock e in scrittura per un write lock ).
La funzione restituisce 0 in caso di successo, e -1 in caso di errore, nel qual caso errno assumerà
uno dei valori:
EWOULDBLOCK il file ha già un blocco attivo, e si è specificato LOCK_NB.
2
di seguito ci riferiremo sempre ai blocchi di accesso ai file con la nomenclatura inglese di file lock, o più
brevemente con lock, per evitare confusioni linguistiche con il blocco di un processo (cioè la condizione in cui il
processo viene posto in stato di sleep).
3
in realtà con Linux questo avviene solo dalla serie 2.0 dei kernel.
12.1. IL FILE LOCKING 409
La funzione può essere usata per acquisire o rilasciare un file lock a seconda di quanto
specificato tramite il valore dell’argomento operation; questo viene interpretato come maschera
binaria, e deve essere passato costruendo il valore con un OR aritmetico delle costanti riportate
in tab. 12.2.
Valore Significato
LOCK_SH Richiede uno shared lock sul file.
LOCK_EX Richiede un esclusive lock sul file.
LOCK_UN Rilascia il file lock.
LOCK_NB Impedisce che la funzione si blocchi nella
richiesta di un file lock.
I primi due valori, LOCK_SH e LOCK_EX permettono di richiedere un file lock rispettivamente
condiviso o esclusivo, ed ovviamente non possono essere usati insieme. Se con essi si specifica
anche LOCK_NB la funzione non si bloccherà qualora il file lock non possa essere acquisito, ma
ritornerà subito con un errore di EWOULDBLOCK. Per rilasciare un file lock si dovrà invece usare
direttamente constLOCK UN.
Si tenga presente che non esiste una modalità per eseguire atomicamente un cambiamento
del tipo di blocco (da shared lock a esclusive lock ), il blocco deve essere prima rilasciato e poi
richiesto, ed è sempre possibile che nel frattempo abbia successo un’altra richiesta pendente,
facendo fallire la riacquisizione.
Si tenga presente infine che flock non è supportata per i file mantenuti su NFS, in questo
caso, se si ha la necessità di utilizzare il file locking, occorre usare l’interfaccia del file locking PO-
SIX basata su fcntl che è in grado di funzionare anche attraverso NFS, a condizione ovviamente
che sia il client che il server supportino questa funzionalità.
La semantica del file locking di BSD inoltre è diversa da quella del file locking POSIX, in
particolare per quanto riguarda il comportamento dei file lock nei confronti delle due funzioni
dup e fork. Per capire queste differenze occorre descrivere con maggiore dettaglio come viene
realizzato dal kernel il file locking per entrambe le interfacce.
In fig. 12.1 si è riportato uno schema essenziale dell’implementazione del file locking in stile
BSD su Linux. Il punto fondamentale da capire è che un file lock, qualunque sia l’interfaccia
che si usa, anche se richiesto attraverso un file descriptor, agisce sempre su di un file; perciò le
informazioni relative agli eventuali file lock sono mantenute dal kernel a livello di inode,4 dato
che questo è l’unico riferimento in comune che possono avere due processi diversi che aprono lo
stesso file.
La richiesta di un file lock prevede una scansione della lista per determinare se l’acquisizione
è possibile, ed in caso positivo l’aggiunta di un nuovo elemento.5 Nel caso dei blocchi creati con
flock la semantica della funzione prevede che sia dup che fork non creino ulteriori istanze di
un file lock quanto piuttosto degli ulteriori riferimenti allo stesso. Questo viene realizzato dal
kernel secondo lo schema di fig. 12.1, associando ad ogni nuovo file lock un puntatore6 alla voce
nella file table da cui si è richiesto il blocco, che cosı̀ ne identifica il titolare.
Questa struttura prevede che, quando si richiede la rimozione di un file lock, il kernel accon-
senta solo se la richiesta proviene da un file descriptor che fa riferimento ad una voce nella file
table corrispondente a quella registrata nel blocco. Allora se ricordiamo quanto visto in sez. 6.3.4
4
in particolare, come accennato in fig. 12.1, i file lock sono mantenuti in una linked list di strutture file_lock.
La lista è referenziata dall’indirizzo di partenza mantenuto dal campo i_flock della struttura inode (per le
definizioni esatte si faccia riferimento al file fs.h nei sorgenti del kernel). Un bit del campo fl_flags di specifica
se si tratta di un lock in semantica BSD (FL_FLOCK) o POSIX (FL_POSIX).
5
cioè una nuova struttura file_lock.
6
il puntatore è mantenuto nel campo fl_file di file_lock, e viene utilizzato solo per i file lock creati con la
semantica BSD.
410 CAPITOLO 12. LA GESTIONE AVANZATA DEI FILE
Figura 12.1: Schema dell’architettura del file locking, nel caso particolare del suo utilizzo da parte dalla funzione
flock.
e sez. 6.3.1, e cioè che i file descriptor duplicati e quelli ereditati in un processo figlio puntano
sempre alla stessa voce nella file table, si può capire immediatamente quali sono le conseguenze
nei confronti delle funzioni dup e fork.
Sarà cosı̀ possibile rimuovere un file lock attraverso uno qualunque dei file descriptor che
fanno riferimento alla stessa voce nella file table, anche se questo è diverso da quello con cui lo
si è creato,7 o se si esegue la rimozione in un processo figlio. Inoltre una volta tolto un file lock
su un file, la rimozione avrà effetto su tutti i file descriptor che condividono la stessa voce nella
file table, e quindi, nel caso di file descriptor ereditati attraverso una fork, anche per processi
diversi.
Infine, per evitare che la terminazione imprevista di un processo lasci attivi dei file lock,
quando un file viene chiuso il kernel provvede anche a rimuovere tutti i blocchi ad esso associati.
Anche in questo caso occorre tenere presente cosa succede quando si hanno file descriptor du-
plicati; in tal caso infatti il file non verrà effettivamente chiuso (ed il blocco rimosso) fintanto
che non viene rilasciata la relativa voce nella file table; e questo avverrà solo quando tutti i
file descriptor che fanno riferimento alla stessa voce sono stati chiusi. Quindi, nel caso ci siano
duplicati o processi figli che mantengono ancora aperto un file descriptor, il file lock non viene
rilasciato.
possibilità di utilizzo in sez. 6.3.6. Quando la si impiega per il file locking essa viene usata solo
secondo il seguente prototipo:
#include <fcntl.h>
int fcntl(int fd, int cmd, struct flock *lock)
Applica o rimuove un file lock sul file fd.
La funzione restituisce 0 in caso di successo, e -1 in caso di errore, nel qual caso errno assumerà
uno dei valori:
EACCES l’operazione è proibita per la presenza di file lock da parte di altri processi.
ENOLCK il sistema non ha le risorse per il blocco: ci sono troppi segmenti di lock aperti, si è
esaurita la tabella dei file lock, o il protocollo per il blocco remoto è fallito.
EDEADLK si è richiesto un lock su una regione bloccata da un altro processo che è a sua volta in
attesa dello sblocco di un lock mantenuto dal processo corrente; si avrebbe pertanto
un deadlock. Non è garantito che il sistema riconosca sempre questa situazione.
EINTR la funzione è stata interrotta da un segnale prima di poter acquisire un file lock.
ed inoltre EBADF, EFAULT.
Al contrario di quanto avviene con l’interfaccia basata su flock con fcntl è possibile bloc-
care anche delle singole sezioni di un file, fino al singolo byte. Inoltre la funzione permette di
ottenere alcune informazioni relative agli eventuali blocchi preesistenti. Per poter fare tutto que-
sto la funzione utilizza come terzo argomento una apposita struttura flock (la cui definizione è
riportata in fig. 12.2) nella quale inserire tutti i dati relativi ad un determinato blocco. Si tenga
presente poi che un file lock fa sempre riferimento ad una regione, per cui si potrà avere un
conflitto anche se c’è soltanto una sovrapposizione parziale con un’altra regione bloccata.
struct flock {
short int l_type ; /* Type of lock : F_RDLCK , F_WRLCK , or F_UNLCK . */
short int l_whence ; /* Where ‘ l_start ’ is relative to ( like ‘ lseek ’). */
off_t l_start ; /* Offset where the lock begins . */
off_t l_len ; /* Size of the locked area ; zero means until EOF . */
pid_t l_pid ; /* Process holding the lock . */
};
I primi tre campi della struttura, l_whence, l_start e l_len, servono a specificare la sezione
del file a cui fa riferimento il blocco: l_start specifica il byte di partenza, l_len la lunghez-
za della sezione e infine l_whence imposta il riferimento da cui contare l_start. Il valore di
l_whence segue la stessa semantica dell’omonimo 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 file lock può essere richiesto anche per una regione al di là della
corrente fine del file, cosı̀ che una eventuale estensione dello stesso resti coperta dal blocco.
Inoltre se si specifica un valore nullo per l_len il blocco si considera esteso fino alla dimensione
massima del file; in questo modo è possibile bloccare una qualunque regione a partire da un
certo punto fino alla fine del file, coprendo automaticamente quanto eventualmente aggiunto in
coda allo stesso.
Valore Significato
F_RDLCK Richiede un blocco condiviso (read lock ).
F_WRLCK Richiede un blocco esclusivo (write lock ).
F_UNLCK Richiede l’eliminazione di un file lock.
Il tipo di file lock richiesto viene specificato dal campo l_type, esso può assumere i tre valori
definiti dalle costanti riportate in tab. 12.3, che permettono di richiedere rispettivamente uno
shared lock, un esclusive lock, e la rimozione di un blocco precedentemente acquisito. Infine 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 file lock.
Oltre a quanto richiesto tramite i campi di flock, l’operazione effettivamente svolta dalla
funzione è stabilita dal valore dall’argomento cmd che, come già riportato in sez. 6.3.6, specifica
l’azione da compiere; i valori relativi al file locking sono tre:
F_GETLK verifica se il file lock specificato dalla struttura puntata da lock può essere acquisito:
in caso negativo sovrascrive la struttura flock con i valori relativi al blocco già
esistente che ne blocca l’acquisizione, altrimenti si limita a impostarne il campo
l_type con il valore F_UNLCK.
F_SETLK se il campo l_type della struttura puntata da lock è F_RDLCK o F_WRLCK richiede il
corrispondente file lock, se è F_UNLCK lo rilascia. Nel caso la richiesta non possa esse-
re soddisfatta a causa di un blocco preesistente la funzione ritorna immediatamente
con un errore di EACCES o di EAGAIN.
F_SETLKW è identica a F_SETLK, ma se la richiesta di non può essere soddisfatta per la presenza
di un altro blocco, mette il processo in stato di attesa fintanto che il blocco pre-
cedente non viene rilasciato. Se l’attesa viene interrotta da un segnale la funzione
ritorna con un errore di EINTR.
Si noti che per quanto detto il comando F_GETLK non serve a rilevare una presenza generica
di blocco su un file, perché se ne esistono altri compatibili con quello richiesto, la funzione
ritorna comunque impostando l_type a F_UNLCK. Inoltre a seconda del valore di l_type si
potrà controllare o l’esistenza di un qualunque tipo di blocco (se è F_WRLCK) o di write lock
(se è F_RDLCK). Si consideri poi che può esserci più di un blocco che impedisce l’acquisizione
di quello richiesto (basta che le regioni si sovrappongano), ma la funzione ne riporterà sempre
soltanto uno, impostando l_whence a SEEK_SET ed i valori l_start e l_len per indicare quale
è la regione bloccata.
Infine si tenga presente che effettuare un controllo con il comando F_GETLK e poi tentare
l’acquisizione con F_SETLK non è una operazione atomica (un altro processo potrebbe acquisire
un blocco fra le due chiamate) per cui si deve sempre verificare il codice di ritorno di fcntl8
quando la si invoca con F_SETLK, per controllare che il blocco sia stato effettivamente acquisito.
8
controllare il codice di ritorno delle funzioni invocate è comunque una buona norma di programmazione, che
permette di evitare un sacco di errori difficili da tracciare proprio perché non vengono rilevati.
12.1. IL FILE LOCKING 413
Non operando a livello di interi file, il file locking POSIX introduce un’ulteriore complicazione;
consideriamo la situazione illustrata in fig. 12.3, 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
bloccherà fintanto che il processo 2 non rilasci il blocco. Ma cosa accade se il processo 2 nel
frattempo tenta a sua volta di ottenere un lock sulla regione A? Questa è una tipica situazione
che porta ad un deadlock, dato che a quel punto anche il processo 2 si bloccherebbe, e niente
potrebbe sbloccare l’altro 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 blocco che porterebbe ad un deadlock.
Per capire meglio il funzionamento del file locking in semantica POSIX (che differisce alquan-
to rispetto da quello di BSD, visto sez. 12.1.2) esaminiamo più in dettaglio come viene gestito
dal kernel. Lo schema delle strutture utilizzate è riportato in fig. 12.4; come si vede esso è molto
simile all’analogo di fig. 12.1:9 il blocco è sempre associato all’inode, solo che in questo caso la
titolarità non viene identificata con il riferimento ad una voce nella file table, ma con il valore
del pid del processo.
Figura 12.4: Schema dell’architettura del file locking, nel caso particolare del suo utilizzo secondo l’interfaccia
standard POSIX.
Quando si richiede un file lock il kernel effettua una scansione di tutti i blocchi presenti
sul file10 per verificare se la regione richiesta non si sovrappone ad una già bloccata, in caso
affermativo decide in base al tipo di blocco, in caso negativo il nuovo blocco viene comunque
acquisito ed aggiunto alla lista.
Nel caso di rimozione invece questa viene effettuata controllando che il pid del processo
richiedente corrisponda a quello contenuto nel blocco. Questa diversa modalità ha delle conse-
9
in questo caso nella figura si sono evidenziati solo i campi di file_lock significativi per la semantica POSIX,
in particolare adesso ciascuna struttura contiene, oltre al pid del processo in fl_pid, la sezione di file che viene
bloccata grazie ai campi fl_start e fl_end. La struttura è comunque la stessa, solo che in questo caso nel campo
fl_flags è impostato il bit FL_POSIX ed il campo fl_file non viene usato.
10
scandisce cioè la linked list delle strutture file_lock, scartando automaticamente quelle per cui fl_flags
non è FL_POSIX, cosı̀ che le due interfacce restano ben separate.
414 CAPITOLO 12. LA GESTIONE AVANZATA DEI FILE
guenze precise riguardo il comportamento dei file lock POSIX. La prima conseguenza è che un
file lock POSIX non viene mai ereditato attraverso una fork, dato che il processo figlio avrà
un pid diverso, mentre passa indenne attraverso una exec in quanto il pid resta lo stesso. Que-
sto comporta che, al contrario di quanto avveniva con la semantica BSD, quando un processo
termina tutti i file lock da esso detenuti vengono immediatamente rilasciati.
La seconda conseguenza è che qualunque file descriptor che faccia riferimento allo stesso
file (che sia stato ottenuto con una dup o con una open in questo caso non fa differenza) può
essere usato per rimuovere un blocco, dato che quello che conta è solo il pid del processo. Da
questo deriva una ulteriore sottile differenza di comportamento: dato che alla chiusura di un
file i blocchi ad esso associati vengono rimossi, nella semantica POSIX basterà chiudere un file
descriptor qualunque per cancellare tutti i blocchi relativi al file cui esso faceva riferimento,
anche se questi fossero stati creati usando altri file descriptor che restano aperti.
Dato che il controllo sull’accesso ai blocchi 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 blocchi su regioni che si sovrappongono fra loro
all’interno 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 file e non accumulandosi,11 la
cosa non ha alcun effetto; la funzione ritorna con successo, senza che il kernel debba modificare
la lista dei file lock. In questo caso invece si possono avere una serie di situazioni diverse: ad
esempio è possibile rimuovere con una sola chiamata più file lock distinti (indicando in una
regione che si sovrapponga completamente a quelle di questi ultimi), o rimuovere solo una parte
di un blocco preesistente (indicando una regione contenuta in quella di un altro blocco), creando
un buco, o coprire con un nuovo blocco altri file lock già ottenuti, e cosı̀ via, a secondo di come
si sovrappongono le regioni richieste e del tipo di operazione richiesta. Il comportamento seguito
in questo caso che la funzione ha successo ed esegue l’operazione richiesta sulla regione indicata;
è compito del kernel preoccuparsi di accorpare o dividere le voci nella lista dei file lock per far
si che le regioni bloccate da essa risultanti siano coerenti con quanto necessario a soddisfare
l’operazione richiesta.
Per fare qualche esempio sul file locking si è scritto un programma che permette di bloccare
una sezione di un file usando la semantica POSIX, o un intero file usando la semantica BSD; in
fig. 12.5 è riportata il corpo principale del codice del programma, (il testo completo è allegato
nella directory dei sorgenti, nel file Flock.c).
La sezione relativa alla gestione delle opzioni al solito si è omessa, come la funzione che
stampa le istruzioni per l’uso 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à
quello che si è specificato per ultimo). Oltre a queste tre vengono pure impostate la variabile
bsd, che abilita la semantica omonima quando si invoca l’opzione -f (il valore preimpostato è
nullo, ad indicare la semantica POSIX), e la variabile cmd che specifica la modalità di richiesta
del file lock (bloccante o meno), a seconda dell’opzione -b.
Il programma inizia col controllare (11-14) che venga passato un argomento (il file da bloc-
care), che sia stato scelto (15-18) il tipo di blocco, dopo di che apre (19) il file, 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 l’uso 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.
11
questa ultima caratteristica è vera in generale, se cioè si richiede più volte lo stesso file lock, o più blocchi sulla
stessa sezione di file, le richieste non si cumulano e basta una sola richiesta di rilascio per cancellare il blocco.
12.1. IL FILE LOCKING 415
Nel caso si sia scelta la semantica BSD (25-34) prima si controlla (27-31) il valore di cmd
per determinare se si vuole effettuare una chiamata bloccante o meno, reimpostandone il valore
opportunamente, dopo di che a seconda del tipo di blocco al valore viene aggiunta la relativa
416 CAPITOLO 12. LA GESTIONE AVANZATA DEI FILE
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).
In entrambi i casi dopo aver richiesto il blocco viene controllato il risultato uscendo (44-46)
in caso di errore, o stampando un messaggio (47-49) in caso di successo. Infine il programma si
pone in attesa (50) finché un segnale (ad esempio un C-c dato da tastiera) non lo interrompa;
in questo caso il programma termina, e tutti i blocchi vengono rilasciati.
Con il programma possiamo fare varie verifiche sul funzionamento del file locking; cominciamo
con l’eseguire un read lock su un file, ad esempio usando all’interno di un terminale il seguente
comando:
[piccardi@gont sources]$ ./flock -r Flock.c
Lock acquired
il programma segnalerà di aver acquisito un blocco e si bloccherà; in questo caso si è usato il
file locking POSIX e non avendo specificato niente riguardo alla sezione che si vuole bloccare
sono stati usati i valori preimpostati che bloccano tutto il file. 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 l’indisponibilità del blocco, dato che il
file è bloccato dal precedente read lock. Si noti che il risultato è lo stesso anche se si richiede il
blocco su una sola parte del file 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
ed il blocco viene rifiutato, ma se invece si richiede una regione distinta avremo che:
[piccardi@gont sources]$ ./flock -w -s11 -l15 Flock.c
Lock acquired
ed il blocco 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.
Il programma di norma esegue il tentativo di acquisire il lock in modalità non bloccante, se
però usiamo l’opzione -b possiamo impostare la modalità bloccante, riproviamo allora a ripetere
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:
[piccardi@gont sources]$ ./flock -w -s0 -l10 Flock.c
il programma cioè si bloccherà nella chiamata a fcntl; se a questo punto rilasciamo il precedente
blocco (terminando il primo comando un C-c sul terminale) potremo verificare che sull’altro
terminale il blocco viene acquisito, con la comparsa di una nuova riga:
12.1. IL FILE LOCKING 417
La funzione restituisce 0 in caso di successo, e -1 in caso di errore, nel qual caso errno assumerà
uno dei valori:
EWOULDBLOCK non è possibile acquisire il lock, e si è selezionato LOCK_NB, oppure l’operazione è
proibita perché il file è mappato in memoria.
ENOLCK il sistema non ha le risorse per il blocco: ci sono troppi segmenti di lock aperti, si è
esaurita la tabella dei file lock.
ed inoltre EBADF, EINVAL.
Il comportamento della funzione dipende dal valore dell’argomento cmd, che specifica quale
azione eseguire; i valori possibili sono riportati in tab. 12.4.
Valore Significato
LOCK_SH Richiede uno shared lock. Più processi possono
mantenere un blocco condiviso sullo stesso file.
LOCK_EX Richiede un exclusive lock. Un solo processo alla
volta può mantenere un blocco esclusivo su un file.
LOCK_UN Sblocca il file.
LOCK_NB Non blocca la funzione quando il blocco non è di-
sponibile, si specifica sempre insieme ad una delle
altre operazioni con un OR aritmetico dei valori.
Qualora il blocco non possa essere acquisito, a meno di non aver specificato LOCK_NB, la
funzione si blocca fino alla disponibilità dello stesso. Dato che la funzione è implementata utiliz-
zando fcntl la semantica delle operazioni è la stessa di quest’ultima (pertanto la funzione non
è affatto equivalente a flock).
cosı̀ che, anche qualora non si predisponessero le opportune verifiche nei processi, questo verrebbe
comunque rispettato.
Per poter utilizzare il mandatory locking è stato introdotto un utilizzo particolare del bit sgid.
Se si ricorda quanto esposto in sez. 5.3.2), esso viene di norma utilizzato per cambiare il group-
ID effettivo con cui viene eseguito un programma, ed è pertanto sempre associato alla presenza
del permesso di esecuzione per il gruppo. Impostando questo bit su un file senza permesso
di esecuzione in un sistema che supporta il mandatory locking, fa sı̀ che quest’ultimo venga
attivato per il file in questione. In questo modo una combinazione dei permessi originariamente
non contemplata, in quanto senza significato, diventa l’indicazione della presenza o meno del
mandatory locking.12
L’uso del mandatory locking presenta vari aspetti delicati, dato che neanche l’amministratore
può passare sopra ad un file lock ; pertanto un processo che blocchi un file cruciale può renderlo
completamente inaccessibile, rendendo completamente inutilizzabile il sistema13 inoltre con il
mandatory locking si può bloccare completamente un server NFS richiedendo una lettura su
un file su cui è attivo un blocco. Per questo motivo l’abilitazione del mandatory locking è di
norma disabilitata, e deve essere attivata filesystem per filesystem in fase di montaggio (specifi-
cando l’apposita opzione di mount riportata in tab. 8.9, o con l’opzione -o mand per il comando
omonimo).
Si tenga presente inoltre che il mandatory locking funziona solo sull’interfaccia POSIX di
fcntl. Questo ha due conseguenze: che non si ha nessun effetto sui file lock richiesti con
l’interfaccia di flock, e che la granularità del blocco è quella del singolo byte, come per fcntl.
La sintassi di acquisizione dei blocchi è esattamente la stessa vista in precedenza per fcntl
e lockf, la differenza è che in caso di mandatory lock attivato non è più necessario controllare la
disponibilità di accesso al file, ma si potranno usare direttamente le ordinarie funzioni di lettura
e scrittura e sarà compito del kernel gestire direttamente il file locking.
Questo significa che in caso di read lock la lettura dal file potrà avvenire normalmente con
read, mentre una write si bloccherà fino al rilascio del blocco, a meno di non aver aperto il file
con O_NONBLOCK, nel qual caso essa ritornerà immediatamente con un errore di EAGAIN.
Se invece si è acquisito un write lock tutti i tentativi di leggere o scrivere sulla regione del
file bloccata fermeranno il processo fino al rilascio del blocco, a meno che il file non sia stato
aperto con O_NONBLOCK, nel qual caso di nuovo si otterrà un ritorno immediato con l’errore di
EAGAIN.
Infine occorre ricordare che le funzioni di lettura e scrittura non sono le sole ad operare sui
contenuti di un file, e che sia creat che open (quando chiamata con O_TRUNC) effettuano dei
cambiamenti, cosı̀ come truncate, riducendone le dimensioni (a zero nei primi due casi, a quanto
specificato 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 file su cui sia presente un
qualunque blocco (le prime due sempre, la terza solo nel caso che la riduzione delle dimensioni
del file vada a sovrapporsi ad una regione bloccata).
L’ultimo aspetto della interazione del mandatory locking con le funzioni di accesso ai file è
quello relativo ai file mappati in memoria (che abbiamo trattato in sez. 12.4.1); anche in tal caso
infatti, quando si esegue la mappatura con l’opzione MAP_SHARED, si ha un accesso al contenuto
del file. Lo standard SVID prevede che sia impossibile eseguire il memory mapping di un file su
12
un lettore attento potrebbe ricordare quanto detto in sez. 5.3.3 e cioè che il bit sgid viene cancellato (come
misura di sicurezza) quando di scrive su un file, questo non vale quando esso viene utilizzato per attivare il
mandatory locking.
13
il problema si potrebbe risolvere rimuovendo il bit sgid, ma non è detto che sia cosı̀ facile fare questa operazione
con un sistema bloccato.
12.2. L’I/O MULTIPLEXING 419
cui sono presenti dei blocchi14 in Linux è stata però fatta la scelta implementativa15 di seguire
questo comportamento soltanto quando si chiama mmap con l’opzione MAP_SHARED (nel qual caso
la funzione fallisce con il solito EAGAIN) che comporta la possibilità di modificare il file.
La funzione in caso di successo restituisce il numero di file descriptor (anche nullo) che sono attivi,
e -1 in caso di errore, nel qual caso errno assumerà uno dei valori:
EBADF si è specificato un file descriptor sbagliato in uno degli insiemi.
EINTR la funzione è stata interrotta da un segnale.
EINVAL si è specificato per ndfs un valore negativo o un valore non valido per timeout.
ed inoltre ENOMEM.
La funzione mette il processo in stato di sleep (vedi tab. 3.8) fintanto che almeno uno dei file
descriptor degli insiemi specificati (readfds, writefds e exceptfds), non diventa attivo, per
un tempo massimo specificato da timeout.
Per specificare quali file descriptor si intende selezionare la funzione usa un particolare og-
getto, il file descriptor set, identificato dal tipo fd_set, che serve ad identificare un insieme di
file descriptor, in maniera analoga a come un signal set (vedi sez. 9.4.2) identifica un insieme di
segnali. Per la manipolazione di questi file 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 l’insieme (vuoto).
void FD_SET(int fd, fd_set *set)
Inserisce il file descriptor fd nell’insieme.
void FD_CLR(int fd, fd_set *set)
Rimuove il file descriptor fd dall’insieme.
int FD_ISSET(int fd, fd_set *set)
Controlla se il file descriptor fd è nell’insieme.
In genere un file descriptor set può contenere fino ad un massimo di FD_SETSIZE file descrip-
tor. Questo valore in origine corrispondeva al limite per il numero massimo di file aperti18 , ma
da quando, come nelle versioni più recenti del kernel, questo limite è stato rimosso, esso indica
le dimensioni massime dei numeri usati nei file descriptor set.19
Si tenga presente che i file 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 file descriptor il cui valore eccede FD_SETSIZE
può dare luogo ad un comportamento indefinito.
La funzione richiede di specificare tre insiemi distinti di file descriptor; il primo, readfds,
verrà osservato per rilevare la disponibilità di effettuare una lettura,20 il secondo, writefds, per
17
la funzione select è apparsa in BSD4.2 e standardizzata in BSD4.4, ma è stata portata su tutti i sistemi che
supportano i socket, compreso le varianti di System V.
18
ad esempio in Linux, fino alla serie 2.0.x, c’era un limite di 256 file per processo.
19
il suo valore, secondo lo standard POSIX 1003.1-2001, è definito in sys/select.h, ed è pari a 1024.
20
per essere precisi la funzione ritornerà in tutti i casi in cui la successiva esecuzione di read risulti non bloccante,
quindi anche in caso di end-of-file; inoltre con Linux possono verificarsi 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
file descriptor come leggibile, ma una successiva read si blocchi.
12.2. L’I/O MULTIPLEXING 421
verificare la possibilità di effettuare una scrittura ed il terzo, exceptfds, per verificare l’esistenza
di eccezioni (come i dati urgenti su un socket, vedi sez. 19.1.3).
Dato che in genere non si tengono mai sotto controllo fino a FD_SETSIZE file contemporanea-
mente la funzione richiede di specificare qual è il valore più alto fra i file descriptor indicati nei
tre insiemi precedenti. Questo viene fatto per efficienza, per evitare di passare e far controllare
al kernel una quantità di memoria superiore a quella necessaria. Questo limite viene indicato
tramite l’argomento ndfs, che deve corrispondere al valore massimo aumentato di uno.21
Infine l’argomento timeout, espresso con una struttura di tipo timeval (vedi fig. 5.7) speci-
fica un tempo massimo di attesa prima che la funzione ritorni; se impostato a NULL la funzione
attende indefinitamente. Si può specificare anche un tempo nullo (cioè una struttura timeval
con i campi impostati a zero), qualora si voglia semplicemente controllare lo stato corrente dei
file descriptor.
La funzione restituisce il numero di file descriptor pronti,22 e ciascun insieme viene sovra-
scritto per indicare quali sono i file 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 modificati. In caso di errore la funzione restituisce -1, ed i valori dei
tre insiemi sono indefiniti e non si può fare nessun affidamento sul loro contenuto.
Una volta ritornata la funzione si potrà controllare quali sono i file descriptor pronti ed
operare su di essi, si tenga presente però che si tratta solo di un suggerimento, esistono infatti
condizioni23 in cui select può riportare in maniera spuria che un file descriptor è pronto in
lettura, quando una successiva lettura si bloccherebbe. Per questo quando si usa I/O multiplexing
è sempre raccomandato l’uso delle funzioni di lettura e scrittura in modalità non bloccante.
In Linux select modifica anche il valore di timeout, impostandolo al tempo restante, quan-
do la funzione viene interrotta da un segnale. In tal caso infatti si ha un errore di EINTR, ed
occorre rilanciare la funzione; in questo modo non è necessario ricalcolare tutte le volte il tem-
po rimanente. Questo può causare problemi di portabilità sia quando si usa codice scritto su
Linux che legge questo valore, sia quando si usano programmi scritti per altri sistemi che non
dispongono di questa caratteristica e ricalcolano timeout tutte le volte.24
Uno dei problemi che si presentano con l’uso di select è che il suo comportamento dipende
dal valore del file 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 file descriptor da tenere sotto controllo
dovrà effettuare una scansione su tutto l’intervallo, che può anche essere molto ampio anche se
i file descriptor sono solo poche unità; tutto ciò ha ovviamente delle conseguenze ampiamente
negative per le prestazioni.
Inoltre c’è anche il problema che il numero massimo dei file che si possono tenere sotto con-
trollo, la funzione è nata quando il kernel consentiva un numero massimo di 1024 file descriptor
per processo, adesso che il numero può essere arbitrario si viene a creare una dipendenza del
tutto artificiale dalle dimensioni della struttura fd_set, che può necessitare di essere estesa, con
ulteriori perdite di prestazioni.
Lo standard POSIX è rimasto a lungo senza primitive per l’I/O multiplexing, introdotto solo
con le ultime revisioni dello standard (POSIX 1003.1g-2000 e POSIX 1003.1-2001). La scelta
è stata quella di seguire l’interfaccia creata da BSD, ma prevede che tutte le funzioni ad esso
21
si ricordi che i file 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.
22
questo è il comportamento previsto dallo standard, ma la standardizzazione della funzione è recente, ed
esistono ancora alcune versioni di Unix che non si comportano in questo modo.
23
ad esempio quando su un socket arrivano dei dati che poi vengono scartati perché corrotti.
24
in genere questa caratteristica è disponibile nei sistemi che derivano da System V e non è disponibile per
quelli che derivano da BSD; lo standard POSIX.1-2001 non permette questo comportamento.
422 CAPITOLO 12. LA GESTIONE AVANZATA DEI FILE
#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 file descriptor degli insiemi specificati diventi attivo.
La funzione in caso di successo restituisce il numero di file descriptor (anche nullo) che sono attivi,
e -1 in caso di errore, nel qual caso errno assumerà uno dei valori:
EBADF si è specificato un file descriptor sbagliato in uno degli insiemi.
EINTR la funzione è stata interrotta da un segnale.
EINVAL si è specificato per ndfs un valore negativo o un valore non valido per timeout.
ed inoltre ENOMEM.
La funzione è sostanzialmente identica a select, solo che usa una struttura timespec (vedi
fig. 5.8) per indicare con maggiore precisione il timeout e non ne aggiorna il valore in caso di inter-
ruzione.26 Inoltre prende un argomento aggiuntivo sigmask che è il puntatore ad una maschera
di segnali (si veda sez. 9.4.4). La maschera corrente viene sostituita da questa immediatamente
prima di eseguire l’attesa, e ripristinata al ritorno della funzione.
L’uso di sigmask è stato introdotto allo scopo di prevenire possibili race condition quando
ci si deve porre in attesa sia di un segnale che di dati. La tecnica classica è quella di utilizza-
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 l’esecuzione delle relative operazioni, onde evitare l’arrivo
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 file descriptor con select, in questo caso si può fare conto sul fatto che all’arrivo di
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 ) {
continue ;
}
} else handle_filedata ();
}
qui però emerge una race condition, perché se il segnale arriva prima della chiamata a select,
questa non verrà interrotta, e la ricezione del segnale non sarà rilevata.
25
il supporto per lo standard POSIX 1003.1-2001, ed l’header 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 definizione sbagliata
di psignal, senza l’argomento sigmask, la definizione corretta è presente dalle glibc 2.1-2.2.1 se si è definito
_GNU_SOURCE e nelle glibc 2.2.2-2.2.4 se si è definito _XOPEN_SOURCE con valore maggiore di 600.
26
in realtà la system call di Linux aggiorna il valore al tempo rimanente, ma la funzione fornita dalle glibc
modifica questo comportamento passando alla system call una variabile locale, in modo da mantenere l’aderenza
allo standard POSIX che richiede che il valore di timeout non sia modificato.
12.2. L’I/O MULTIPLEXING 423
Per questo è stata introdotta pselect che attraverso l’argomento sigmask permette di ria-
bilitare la ricezione il segnale contestualmente all’esecuzione della funzione,27 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 l’esecuzione di pselect la ricezione del segnale sarà
abilitata, ed in caso di interruzione si potranno eseguire le relative operazioni.
#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 file descriptor.
La funzione restituisce il numero di file descriptor con attività in caso di successo, o 0 se c’è stato
un timeout e -1 in caso di errore, ed in quest’ultimo caso errno assumerà uno dei valori:
EBADF si è specificato un file descriptor sbagliato in uno degli insiemi.
EINTR la funzione è stata interrotta da un segnale.
EINVAL il valore di nfds eccede il limite RLIMIT_NOFILE.
ed inoltre EFAULT e ENOMEM.
controllare, mentre in revents il kernel restituirà il relativo risultato. Usando un valore negativo
per fd la corrispondente struttura sarà ignorata da poll. Dato che i dati in ingresso sono del tutto
indipendenti da quelli in uscita (che vengono restituiti in revents) non è necessario reinizializzare
tutte le volte il valore delle strutture pollfd a meno di non voler cambiare qualche condizione.
struct pollfd {
int fd ; /* file descriptor */
short events ; /* requested events */
short revents ; /* returned events */
};
Figura 12.6: La struttura pollfd, utilizzata per specificare le modalità di controllo di un file descriptor alla
funzione poll.
Le costanti che definiscono i valori relativi ai bit usati nelle maschere binarie dei campi
events e revents sono riportati in tab. 12.5, insieme al loro significato. Le si sono suddivise in
tre gruppi, nel primo gruppo si sono indicati i bit utilizzati per controllare l’attività in ingresso,
nel secondo quelli per l’attività in uscita, mentre il terzo gruppo contiene dei valori che vengono
utilizzati solo nel campo revents per notificare delle condizioni di errore.
Flag Significato
POLLIN È possibile la lettura.
POLLRDNORM Sono disponibili in lettura dati normali.
POLLRDBAND Sono disponibili in lettura dati prioritari.
POLLPRI È possibile la lettura di dati urgenti.
POLLOUT È possibile la scrittura immediata.
POLLWRNORM È possibile la scrittura di dati normali.
POLLWRBAND È possibile la scrittura di dati prioritari.
POLLERR C’è una condizione di errore.
POLLHUP Si è verificato un hung-up.
POLLRDHUP Si è avuta una half-close su un socket.29
POLLNVAL Il file descriptor non è aperto.
POLLMSG Definito per compatibilità con SysV.
Tabella 12.5: Costanti per l’identificazione dei vari bit dei campi events e revents di pollfd.
Il valore POLLMSG non viene utilizzato ed è definito solo per compatibilità con l’implementa-
zione di SysV che usa gli stream;30 è da questi che derivano i nomi di alcune costanti, in quanto
per essi sono definite 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 normali e prioritari, vale
a dire POLLRDNORM, POLLWRNORM, POLLRDBAND e POLLWRBAND fanno riferimento alle implementa-
zioni in stile SysV (in particolare le ultime due non vengono usate su Linux), e sono utilizzabili
soltanto qualora si sia definita la macro _XOPEN_SOURCE.31
In caso di successo funzione ritorna restituendo il numero di file (un valore positivo) per i
quali si è verificata una delle condizioni di attesa richieste o per i quali si è verificato un errore,
29
si tratta di una estensione specifica di Linux, disponibile a partire dal kernel 2.6.17 definendo la marco
_GNU_SOURCE, che consente di riconoscere la chiusura in scrittura dell’altro capo di un socket, situazione che si
viene chiamata appunto half-close (mezza chiusura) su cui torneremo con maggiori dettagli in sez. 16.6.3.
30
essi sono una interfaccia specifica di SysV non presente in Linux, e non hanno nulla a che fare con i file stream
delle librerie standard del C.
31
e ci si ricordi di farlo sempre in testa al file, definirla soltanto prima di includere sys/poll.h non è sufficiente.
12.2. L’I/O MULTIPLEXING 425
nel qual caso vengono utilizzati i valori di tab. 12.5 esclusivi di revents. Un valore nullo indica
che si è raggiunto il timeout, mentre un valore negativo indica un errore nella chiamata, il cui
codice viene riportato al solito tramite errno.
L’uso 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 file descriptor set e la dimensione
dei dati passati al kernel dipende solo dal numero dei file descriptor che si vogliono controllare,
non dal loro valore.32
Inoltre con select lo stesso file descriptor set è usato sia in ingresso che in uscita, e questo
significa che tutte le volte che si vuole ripetere l’operazione occorre reinizializzarlo da capo.
Questa operazione, che può essere molto onerosa se i file descriptor da tenere sotto osservazione
sono molti, non è invece necessaria con poll.
Abbiamo visto in sez. 12.2.2 come lo standard POSIX preveda una variante di select che
consente di gestire correttamente la ricezione dei segnali nell’attesa su un file descriptor. Con
l’introduzione di una implementazione reale di pselect nel kernel 2.6.16, è stata aggiunta anche
una analoga funzione che svolga lo stesso ruolo per poll.
In questo caso si tratta di una estensione che è specifica di Linux e non è prevista da nessuno
standard; essa può essere utilizzata esclusivamente se si definisce la macro _GNU_SOURCE ed
ovviamente non deve essere usata se si ha a cuore la portabilità. La funzione è ppoll, ed il suo
prototipo è:
#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 file descriptor.
La funzione restituisce il numero di file descriptor con attività in caso di successo, o 0 se c’è stato
un timeout e -1 in caso di errore, ed in quest’ultimo caso errno assumerà uno dei valori:
EBADF si è specificato un file descriptor sbagliato in uno degli insiemi.
EINTR la funzione è stata interrotta da un segnale.
EINVAL 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ò specificare, con l’argomento
sigmask, il puntatore ad una maschera di segnali; questa sarà la maschera utilizzata per tutto il
tempo che la funzione resterà in attesa, all’uscita viene ripristinata la maschera originale. L’uso
di questa funzione è cioè equivalente, come illustrato nella pagina di manuale, all’esecuzione
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 significato, e la funzione resti-
tuisce gli stessi risultati illustrati in precedenza. Come nel caso di pselect la system call che
implementa ppoll restituisce, se la funzione viene interrotta da un segnale, il tempo mancan-
te in timeout, e come per pselect la funzione di libreria fornita dalle glibc maschera questo
comportamento non modificando mai il valore di timeout.33
32
anche se usando dei bit un file descriptor set può essere più efficiente di un vettore di strutture pollfd,
qualora si debba osservare un solo file descriptor con un valore molto alto ci si troverà ad utilizzare inutilmente
un maggiore quantitativo di memoria.
33
anche se in questo caso non esiste nessuno standard che richiede questo comportamento.
426 CAPITOLO 12. LA GESTIONE AVANZATA DEI FILE
ottenere detto file descriptor chiamando una delle funzioni epoll_create e epoll_create1,41 i
cui prototipi sono:
#include <sys/epoll.h>
int epoll_create(int size)
int epoll_create1(int flags)
Apre un file descriptor per epoll.
Le funzioni restituiscono un file descriptor per epoll in caso di successo, o −1 in caso di errore, nel
qual caso errno assumerà uno dei valori:
EINVAL si è specificato un valore di size non positivo o non valido per flags.
ENFILE si è raggiunto il massimo di file descriptor aperti nel sistema.
EMFILE si è raggiunto il limite sul numero massimo di istanze di epoll per utente stabilito da
/proc/sys/fs/epoll/max_user_instances.
ENOMEM non c’è sufficiente memoria nel kernel per creare l’istanza.
Entrambe le funzioni restituiscono un file descriptor speciale,42 detto anche epoll descriptor,
che viene associato alla infrastruttura utilizzata dal kernel per gestire la notifica degli eventi.
Nel caso di epoll_create l’argomento size serviva a dare l’indicazione del numero di file de-
scriptor che si vorranno tenere sotto controllo, e costituiva solo un suggerimento per semplificare
l’allocazione di risorse sufficienti, non un valore massimo.43
La seconda versione della funzione, epoll_create1 è stata introdotta44 come estensione della
precedente, per poter passare dei flag di controllo come maschera binaria in fase di creazione del
file descriptor. Al momento l’unico valore legale per flags (a parte lo zero) è EPOLL_CLOEXEC,
che consente di impostare in maniera atomica sul file descriptor il flag di close-on-exec (si veda il
significato di O_CLOEXEC in tab. 6.2), senza che sia necessaria una successiva chiamata a fcntl.
Una volta ottenuto un file descriptor per epoll il passo successivo è indicare quali file descrip-
tor mettere sotto osservazione e quali operazioni controllare, per questo si deve usare la seconda
funzione dell’interfaccia, epoll_ctl, il cui prototipo è:
#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à
uno dei valori:
EBADF il file descriptor epfd o fd non sono validi.
EEXIST l’operazione richiesta è EPOLL_CTL_ADD ma fd è già stato inserito in epfd.
EINVAL il file descriptor epfd non è stato ottenuto con epoll_create, o fd è lo stesso epfd o
l’operazione richiesta con op non è supportata.
ENOENT l’operazione richiesta è EPOLL_CTL_MOD o EPOLL_CTL_DEL ma fd non è inserito in epfd.
ENOMEM non c’è sufficiente memoria nel kernel gestire l’operazione richiesta.
EPERM il file fd non supporta epoll.
ENOSPC si è raggiunto il limite massimo di registrazioni per utente di file descriptor da osservare
imposto da /proc/sys/fs/epoll/max_user_watches.
Il comportamento della funzione viene controllato dal valore dall’argomento op che consente
di specificare quale operazione deve essere eseguita. Le costanti che definiscono i valori utilizzabili
per op sono riportate in tab. 12.6, assieme al significato delle operazioni cui fanno riferimento.
41
l’interfaccia di epoll è stata inserita nel kernel a partire dalla versione 2.5.44, ed il supporto è stato aggiunto
alle glibc 2.3.2.
42
esso non è associato a nessun file su disco, inoltre a differenza dei normali file descriptor non può essere inviato
ad un altro processo attraverso un socket locale (vedi sez. 18.2.1).
43
ma a partire dal kernel 2.6.8 esso viene totalmente ignorato e l’allocazione è sempre dinamica.
44
è disponibile solo a partire dal kernel 2.6.27.
428 CAPITOLO 12. LA GESTIONE AVANZATA DEI FILE
Valore Significato
EPOLL_CTL_ADD Aggiunge un nuovo file descriptor da osservare fd alla
lista dei file descriptor controllati tramite epfd, in event
devono essere specificate le modalità di osservazione.
EPOLL_CTL_MOD Modifica le modalità di osservazione del file descriptor fd
secondo il contenuto di event.
EPOLL_CTL_DEL Rimuove il file descriptor fd dalla lista dei file controllati
tramite epfd.
Tabella 12.6: Valori dell’argomento op che consentono di scegliere quale operazione di controllo effettuare con
la funzione epoll_ctl.
La funzione prende sempre come primo argomento un file descriptor di epoll, epfd, che deve
essere stato ottenuto in precedenza con una chiamata a epoll_create. L’argomento fd indica
invece il file descriptor che si vuole tenere sotto controllo, quest’ultimo può essere un qualunque
file descriptor utilizzabile con poll, ed anche un altro file descriptor di epoll, ma non lo stesso
epfd.
L’ultimo argomento, event, deve essere un puntatore ad una struttura di tipo epoll_event,
ed ha significato 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. L’argomento
viene ignorato con l’operazione EPOLL_CTL_DEL.45
struct epoll_event {
__uint32_t events ; /* Epoll events */
epoll_data_t data ; /* User data variable */
};
Figura 12.7: La struttura epoll_event, che consente di specificare gli eventi associati ad un file descriptor
controllato con epoll.
45
fino 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ò specificare anche un valore NULL ma se si vuole mantenere la compatibilità con le versioni
precedenti occorre usare un puntatore valido.
12.2. L’I/O MULTIPLEXING 429
Valore Significato
EPOLLIN Il file è pronto per le operazioni di lettura (analogo di
POLLIN).
EPOLLOUT Il file è pronto per le operazioni di scrittura (analogo di
POLLOUT).
EPOLLRDHUP L’altro 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).46
EPOLLPRI Ci sono dati urgenti disponibili in lettura (analogo di
POLLPRI); questa condizione viene comunque riportata
in uscita, e non è necessaria impostarla in ingresso.
EPOLLERR Si è verificata una condizione di errore (analogo di
POLLERR); questa condizione viene comunque riportata
in uscita, e non è necessaria impostarla in ingresso.
EPOLLHUP Si è verificata una condizione di hung-up; questa condizio-
ne viene comunque riportata in uscita, e non è necessaria
impostarla in ingresso.
EPOLLET Imposta la notifica in modalità edge triggered per il file
descriptor associato.
EPOLLONESHOT Imposta la modalità one-shot per il file descriptor
associato.47
Tabella 12.7: Costanti che identificano i bit del campo events di epoll_event.
Le modalità di utilizzo di epoll prevedono che si definisca qual’è l’insieme dei file descriptor
da tenere sotto controllo tramite un certo epoll descriptor epfd attraverso una serie di chiamate
a EPOLL_CTL_ADD.48 L’uso di EPOLL_CTL_MOD consente in seguito di modificare le modalità di
osservazione di un file descriptor che sia già stato aggiunto alla lista di osservazione.
Le impostazioni di default prevedono che la notifica degli eventi richiesti sia effettuata in
modalità level triggered, a meno che sul file descriptor non si sia impostata la modalità edge
triggered, registrandolo con EPOLLET attivo nel campo events. Si tenga presente che è possibile
tenere sotto osservazione uno stesso file descriptor su due epoll descriptor diversi, ed entrambi
riceveranno le notifiche, anche se questa pratica è sconsigliata.
Qualora non si abbia più interesse nell’osservazione di un file descriptor lo si può rimuovere
dalla lista associata a epfd con EPOLL_CTL_DEL; si tenga conto inoltre che i file descriptor sotto
osservazione che vengono chiusi sono eliminati dalla lista automaticamente e non è necessario
usare EPOLL_CTL_DEL.
Infine una particolare modalità di notifica è quella impostata con EPOLLONESHOT: a causa
dell’implementazione di epoll infatti quando si è in modalità edge triggered l’arrivo in rapida
successione di dati in blocchi separati49 può causare una generazione di eventi (ad esempio
segnalazioni di dati in lettura disponibili) anche se la condizione è già stata rilevata.50
Anche se la situazione è facile da gestire, la si può evitare utilizzando EPOLLONESHOT per
impostare la modalità one-shot, in cui la notifica di un evento viene effettuata una sola volta, dopo
di che il file descriptor osservato, pur restando nella lista di osservazione, viene automaticamente
disattivato,51 e per essere riutilizzato dovrà essere riabilitato esplicitamente con una successiva
47
questa modalità è disponibile solo a partire dal kernel 2.6.17, ed è utile per riconoscere la chiusura di una
connessione dall’altro capo quando si lavora in modalità edge triggered.
48
questa modalità è disponibile solo a partire dal kernel 2.6.2.
48
un difetto dell’interfaccia è che queste chiamate devono essere ripetute per ciascun file descriptor, incorrendo
in una perdita di prestazioni qualora il numero di file descriptor sia molto grande; per questo è stato proposto
di introdurre come estensione una funzione epoll_ctlv che consenta di effettuare con una sola chiamata le
impostazioni per un blocco di file descriptor.
49
questo è tipico con i socket di rete, in quanto i dati arrivano a pacchetti.
50
si avrebbe cioè una rottura della logica edge triggered.
51
la cosa avviene contestualmente al ritorno di epoll_wait a causa dell’evento in questione.
430 CAPITOLO 12. LA GESTIONE AVANZATA DEI FILE
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)
Attende che uno dei file descriptor osservati sia pronto.
La funzione restituisce il numero di file descriptor pronti in caso di successo o −1 in caso di errore,
nel qual caso errno assumerà uno dei valori:
EBADF il file descriptor epfd non è valido.
EFAULT il puntatore events non è valido.
EINTR la funzione è stata interrotta da un segnale prima della scadenza di timeout.
EINVAL il file descriptor epfd non è stato ottenuto con epoll_create, o maxevents non è
maggiore di zero.
La funzione si blocca in attesa di un evento per i file descriptor registrati nella lista di
osservazione di epfd fino ad un tempo massimo specificato in millisecondi tramite l’argomento
timeout. Gli eventi registrati vengono riportati in un vettore di strutture epoll_event (che
deve essere stato allocato in precedenza) all’indirizzo indicato dall’argomento events, fino ad
un numero massimo di eventi impostato con l’argomento maxevents.
La funzione ritorna il numero di eventi rilevati, o un valore nullo qualora sia scaduto il tempo
massimo impostato con timeout. Per quest’ultimo, oltre ad un numero di millisecondi, si può
utilizzare il valore nullo, che indica di non attendere e ritornare immediatamente,52 o il valore
−1, che indica un’attesa indefinita. L’argomento maxevents dovrà invece essere sempre un intero
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 flag relativi
agli eventi accaduti, mentre nel campo data sarà restituito il valore che era stato impostato per il
file descriptor per cui si è verificato l’evento quando questo era stato registrato con le operazioni
EPOLL_CTL_MOD o EPOLL_CTL_ADD, in questo modo il campo data consente di identificare il file
descriptor.53
Si ricordi che le occasioni per cui epoll_wait ritorna dipendono da come si è impostata la
modalità di osservazione (se level triggered o edge triggered ) del singolo file descriptor. L’interfac-
cia assicura che se arrivano più eventi fra due chiamate successive ad epoll_wait questi vengano
combinati. Inoltre qualora su un file descriptor fossero presenti eventi non ancora notificati, e si
effettuasse una modifica dell’osservazione con EPOLL_CTL_MOD, questi verrebbero riletti alla luce
delle modifiche.
Si tenga presente infine che con l’uso della modalità edge triggered il ritorno di epoll_wait
indica che un file descriptor è pronto e resterà tale fintanto che non si sono completamente
esaurite le operazioni su di esso. Questa condizione viene generalmente rilevata dall’occorrere
di un errore di EAGAIN al ritorno di una read o una write,54 ma questa non è la sola modalità
possibile, ad esempio la condizione può essere riconosciuta anche per il fatto che sono stati
restituiti meno dati di quelli richiesti.
Come già per select e poll anche per l’interfaccia di epoll si pone il problema di gestire
l’attesa di segnali e di dati contemporaneamente per le osservazioni fatte in sez. 12.2.2, per fare
questo di nuovo è necessaria una variante della funzione di attesa che consenta di reimpostare
52
anche in questo caso il valore di ritorno sarà nullo.
53
ed è per questo che, come accennato, è consuetudine usare per data il valore del file descriptor stesso.
54
è opportuno ricordare ancora una volta che l’uso dell’I/O multiplexing richiede di operare sui file in modalità
non bloccante.
12.2. L’I/O MULTIPLEXING 431
all’uscita una maschera di segnali, analoga alle estensioni pselect e ppoll che abbiamo visto
in precedenza per select e poll; in questo caso la funzione si chiama epoll_pwait55 ed il suo
prototipo è:
#include <sys/epoll.h>
int epoll_pwait(int epfd, struct epoll_event * events, int maxevents, int
timeout, const sigset_t *sigmask)
Attende che uno dei file descriptor osservati sia pronto, mascherando i segnali.
La funzione restituisce il numero di file descriptor pronti in caso di successo o −1 in caso di errore,
nel qual caso errno assumerà uno dei valori già visti con epoll_wait.
La funzione è del tutto analoga epoll_wait, soltanto che alla sua uscita viene ripristinata la
maschera di segnali originale, sostituita durante l’esecuzione da quella impostata con l’argomento
sigmask; in sostanza la chiamata a questa funzione è equivalente al seguente codice, eseguito
però in maniera atomica:
sigset_t origmask ;
Abbiamo visto però in sez. 9.5.1 che insieme ai segnali real-time sono state introdotte anche
delle interfacce di gestione sincrona dei segnali con la funzione sigwait e le sue affini. Queste
funzioni consentono di gestire i segnali bloccando un processo fino alla avvenuta ricezione e
disabilitando l’esecuzione asincrona rispetto al resto del programma del gestore del segnale.
Questo consente di risolvere i problemi di atomicità nella gestione degli eventi associati ai segnali,
avendo tutto il controllo nel flusso principale del programma, ottenendo cosı̀ una gestione simile a
quella dell’I/O multiplexing, ma non risolve i problemi delle interazioni con quest’ultimo, perché
o si aspetta la ricezione di un segnale o si aspetta che un file descriptor sia accessibile e nessuna
delle rispettive funzioni consente di fare contemporaneamente entrambe le cose.
Per risolvere questo problema nello sviluppo del kernel si è pensato di introdurre un meccani-
smo alternativo per la notifica dei segnali (esteso anche ad altri eventi generici) che, ispirandosi
di nuovo alla filosofia di Unix per cui tutto è un file, consentisse di eseguire la notifica con l’uso
di opportuni file descriptor.57
In sostanza, come per sigwait, si può disabilitare l’esecuzione di un gestore in occasione
dell’arrivo di un segnale, e rilevarne l’avvenuta ricezione leggendone la notifica tramite l’uso di
uno speciale file descriptor. Trattandosi di un file descriptor questo potrà essere tenuto sotto
osservazione con le ordinarie funzioni dell’I/O multiplexing (vale a dire con le solite select,
poll e epoll_wait) allo stesso modo di quelli associati a file o socket, per cui alla fine si potrà
attendere in contemporanea sia l’arrivo del segnale che la disponibilità di accesso ai dati relativi
a questi ultimi.
La funzione che permette di abilitare la ricezione dei segnali tramite file descriptor è signalfd,58
il cui prototipo è:
#include <sys/signalfd.h>
int signalfd(int fd, const sigset_t *mask, int flags)
Crea o modifica un file descriptor per la ricezione dei segnali.
La funzione restituisce un numero di file descriptor in caso di successo o −1 in caso di errore, nel
qual caso errno assumerà uno dei valori:
EBADF il valore fd non indica un file descriptor.
EINVAL il file descriptor fd non è stato ottenuto con signalfd o il valore di flags non è valido.
ENOMEM non c’è memoria sufficiente per creare un nuovo file descriptor di signalfd.
ENODEV il kernel non può montare internamente il dispositivo per la gestione anonima degli
inode associati al file descriptor.
ed inoltre EMFILE e ENFILE.
si intende operare con signalfd; l’elenco può essere modificato con una successiva chiamata a
signalfd. Dato che SIGKILL e SIGSTOP non possono essere intercettati (e non prevedono nean-
che la possibilità di un gestore) un loro inserimento nella maschera verrà ignorato senza generare
errori.
L’argomento flags consente di impostare direttamente in fase di creazione due flag per il
file descriptor analoghi a quelli che si possono impostare con una creazione ordinaria con open,
evitando una impostazione successiva con fcntl.59 L’argomento deve essere specificato come
maschera binaria dei valori riportati in tab. 12.8.
Valore Significato
SFD_NONBLOCK imposta sul file descriptor il flag di O_NONBLOCK per
renderlo non bloccante.
SFD_CLOEXEC imposta il flag di O_CLOEXEC per la chiusura automatica
del file descriptor nella esecuzione di exec.
Tabella 12.8: Valori dell’argomento flags per la funzione signalfd che consentono di impostare i flag del file
descriptor.
Si tenga presente che la chiamata a signalfd non disabilita la gestione ordinaria dei segnali
indicati da mask; questa, se si vuole effettuare la ricezione tramite il file descriptor, dovrà essere
disabilitata esplicitamente bloccando gli stessi segnali con sigprocmask, altrimenti verranno
comunque eseguite le azioni di default (o un eventuale gestore installato in precedenza).60 Si
tenga presente inoltre che la lettura di una struttura signalfd_siginfo relativa ad un segnale
pendente è equivalente alla esecuzione di un gestore, vale a dire che una volta letta il segnale
non sarà più pendente e non potrà essere ricevuto, qualora si ripristino le normali condizioni di
gestione, né da un gestore né dalla funzione sigwaitinfo.
Come anticipato, essendo questo lo scopo principale della nuova interfaccia, il file descriptor
può essere tenuto sotto osservazione tramite le funzioni dell’I/O multiplexing (vale a dire con le
solite select, poll e epoll_wait), e risulterà accessibile in lettura quando uno o più dei segnali
indicati tramite mask sarà pendente.
La funzione può essere chiamata più volte dallo stesso processo, consentendo cosı̀ di tenere
sotto osservazione segnali diversi tramite file descriptor diversi. Inoltre è anche possibile tenere
sotto osservazione lo stesso segnale con più file descriptor, anche se la pratica è sconsigliata; in
tal caso la ricezione del segnale potrà essere effettuata con una lettura da uno qualunque dei file
descriptor a cui è associato, ma questa potrà essere eseguita soltanto una volta.61
Quando il file descriptor per la ricezione dei segnali non serve più potrà essere chiuso con
close liberando tutte le risorse da esso allocate. In tal caso qualora vi fossero segnali pendenti
questi resteranno tali, e potranno essere ricevuti normalmente una volta che si rimuova il blocco
imposto con sigprocmask.
Oltre che con le funzioni dell’I/O multiplexing l’uso del file descriptor restituito da signalfd
cerca di seguire la semantica di un sistema unix-like anche con altre system call ; in particolare
esso resta aperto (come ogni altro file descriptor) attraverso una chiamata ad exec, a meno che
non lo si sia creato con il flag di SFD_CLOEXEC o si sia successivamente impostato il close-on-exec
con fcntl. Questo comportamento corrisponde anche alla ordinaria semantica relativa ai segnali
bloccati, che restano pendenti attraverso una exec.
59
questo è un argomento aggiuntivo, introdotto con la versione fornita a partire dal kernel 2.6.27, per kernel
precedenti il valore deve essere nullo.
60
il blocco non ha invece nessun effetto sul file descriptor restituito da signalfd, dal quale sarà possibile pertanto
ricevere qualunque segnale, anche se questo risultasse bloccato.
61
questo significa che tutti i file descriptor su cui è presente lo stesso segnale risulteranno pronti in lettura per
le funzioni di I/O multiplexing, ma una volta eseguita la lettura su uno di essi il segnale sarà considerato ricevuto
ed i relativi dati non saranno più disponibili sugli altri file descriptor, che (a meno di una ulteriore occorrenza del
segnale nel frattempo) di non saranno più pronti.
434 CAPITOLO 12. LA GESTIONE AVANZATA DEI FILE
Analogamente il file descriptor resta sempre disponibile attraverso una fork per il processo
figlio, che ne riceve una copia; in tal caso però il figlio potrà leggere dallo stesso soltanto i dati
relativi ai segnali ricevuti da lui stesso. Nel caso di thread viene nuovamente seguita la semantica
ordinaria dei segnali, che prevede che un singolo thread possa ricevere dal file descriptor solo le
notifiche di segnali inviati direttamente a lui o al processo in generale, e non quelli relativi ad
altri thread appartenenti allo stesso processo.
L’interfaccia fornita da signalfd prevede che la ricezione dei segnali sia eseguita leggendo i
dati relativi ai segnali pendenti dal file descriptor restituito dalla funzione con una normalissima
read. Qualora non vi siano segnali pendenti la read si bloccherà a meno di non aver impostato
la modalità di I/O non bloccante sul file descriptor, o direttamente in fase di creazione con il
flag SFD_NONBLOCK, o in un momento successivo con fcntl.
struct signalfd_siginfo {
uint32_t ssi_signo ; /* Signal number */
int32_t ssi_errno ; /* Error number ( unused ) */
int32_t ssi_code ; /* Signal code */
uint32_t ssi_pid ; /* PID of sender */
uint32_t ssi_uid ; /* Real UID of sender */
int32_t ssi_fd ; /* File descriptor ( SIGIO ) */
uint32_t ssi_tid ; /* Kernel timer ID ( POSIX timers ) */
uint32_t ssi_band ; /* Band event ( SIGIO ) */
uint32_t ssi_overrun ; /* POSIX timer overrun count */
uint32_t ssi_trapno ; /* Trap number that caused signal */
int32_t ssi_status ; /* Exit status or signal ( SIGCHLD ) */
int32_t ssi_int ; /* Integer sent by sigqueue (2) */
uint64_t ssi_ptr ; /* Pointer sent by sigqueue (2) */
uint64_t ssi_utime ; /* User CPU time consumed ( SIGCHLD ) */
uint64_t ssi_stime ; /* System CPU time consumed ( SIGCHLD ) */
uint64_t ssi_addr ; /* Address that generated signal
( for hardware - generated signals ) */
uint8_t pad [ X ]; /* Pad size to 128 bytes ( allow for
additional fields in the future ) */
};
Figura 12.8: La struttura signalfd_siginfo, restituita in lettura da un file descriptor creato con signalfd.
I dati letti dal file descriptor vengono scritti sul buffer indicato come secondo argomento di
read nella forma di una sequenza di una o più strutture signalfd_siginfo (la cui definizione si è
riportata in fig. 12.8) a seconda sia della dimensione del buffer che del numero di segnali pendenti.
Per questo motivo il buffer deve essere almeno di dimensione pari a quella di signalfd_siginfo,
qualora sia di dimensione maggiore potranno essere letti in unica soluzione i dati relativi ad
eventuali più segnali pendenti, fino al numero massimo di strutture signalfd_siginfo che
possono rientrare nel buffer.
Il contenuto di signalfd_siginfo ricalca da vicino quella della analoga struttura siginfo_t
(illustrata in fig. 9.9) usata dall’interfaccia ordinaria dei segnali, e restituisce dati simili. Come
per siginfo_t i campi che vengono avvalorati dipendono dal tipo di segnale e ricalcano i valori
che abbiamo già illustrato in sez. 9.4.3.62
Come esempio di questa nuova interfaccia ed anche come esempio di applicazione della in-
terfaccia di epoll, si è scritto un programma elementare che stampi sullo standard output sia
quanto viene scritto da terzi su una named fifo, che l’avvenuta ricezione di alcuni segnali. Il
codice completo si trova al solito nei sorgenti allegati alla guida (nel file FifoReporter.c).
62
si tenga presente però che per un bug i kernel fino al 2.6.25 non avvalorano correttamente i campi ssi_ptr e
ssi_int per segnali inviati con sigqueue.
12.2. L’I/O MULTIPLEXING 435
In fig. 12.9 si è riportata la parte iniziale del programma in cui vengono effettuate le varie
inizializzazioni necessarie per l’uso di epoll e signalfd, a partire (12-16) dalla definizione delle
varie variabili e strutture necessarie. Al solito si è tralasciata la parte dedicata alla decodifica
delle opzioni che consentono ad esempio di cambiare il nome del file associato alla fifo.
1 ...
2 # include < sys / epoll .h > /* Linux epoll interface */
3 # include < sys / signalfd .h > /* Linux signalfd interface */
4
5 void die ( char *); /* print error and exit function */
6 # define MAX_EPOLL_EV 10
7 int main ( int argc , char * argv [])
8 {
9 /* Variables definition */
10 int i , n , nread , t = 10;
11 char buffer [4096];
12 int fifofd , epfd , sigfd ;
13 sigset_t sigmask ;
14 char * fifoname = " / tmp / reporter . fifo " ;
15 struct epoll_event epev , events [ MAX_EPOLL_EV ];
16 struct signalfd_siginfo siginf ;
17 ...
18 /* Initial setup */
19 if (( epfd = epoll_create (5)) < 0) // epoll init
20 die ( " Failing on epoll_create " );
21 /* Signal setup for signalfd and epoll use */
22 sigemptyset (& sigmask );
23 sigaddset (& sigmask , SIGINT );
24 sigaddset (& sigmask , SIGQUIT );
25 sigaddset (& sigmask , SIGTERM );
26 if ( sigprocmask ( SIG_BLOCK , & sigmask , NULL ) == -1) // block signals
27 die ( " Failing in sigprocmask " );
28 if (( sigfd = signalfd ( -1 , & sigmask , SFD_NONBLOCK )) == -1) // take a signalfd
29 die ( " Failing in signalfd " );
30 epev . data . fd = sigfd ; // add fd to epoll
31 epev . events = EPOLLIN ;
32 if ( epoll_ctl ( epfd , EPOLL_CTL_ADD , sigfd , & epev ))
33 die ( " Failing in signal epoll_ctl " );
34 /* Fifo setup for epoll use */
35 if ( mkfifo ( fifoname , 0622)) { // create well known fifo if does ’t exist
36 if ( errno != EEXIST )
37 die ( " Cannot create well known fifo " );
38 }
39 if (( fifofd = open ( fifoname , O_RDWR | O_NONBLOCK )) < 0) // open fifo
40 die ( " Cannot open read only well known fifo " );
41 epev . data . fd = fifofd ; // add fd to epoll
42 epev . events = EPOLLIN ;
43 if ( epoll_ctl ( epfd , EPOLL_CTL_ADD , fifofd , & epev ))
44 die ( " Failing in fifo epoll_ctl " );
45 /* Main body : wait something to report */
46 ...
47 }
Il primo passo (19-20) è la crezione di un file descriptor epfd di epoll con epoll_create
che è quello che useremo per il controllo degli altri. É poi necessario disabilitare la ricezione
dei segnali (nel caso SIGINT, SIGQUIT e SIGTERM) per i quali si vuole la notifica tramite file
436 CAPITOLO 12. LA GESTIONE AVANZATA DEI FILE
descriptor. Per questo prima li si inseriscono (22-25) in una maschera di segnali sigmask che
useremo con (26) sigprocmask per disabilitarli. Con la stessa maschera si potrà per passare
all’uso (28-29) di signalfd per abilitare la notifica sul file descriptor sigfd. Questo poi (30-33)
dovrà essere aggiunto con epoll_ctl all’elenco di file descriptor controllati con epfd.
Occorrerà infine (35-38) creare la named fifo se questa non esiste ed aprirla per la lettura
(39-40); una volta fatto questo sarà necessario aggiungere il relativo file descriptor (fifofd)
a quelli osservati da epoll in maniera del tutto analoga a quanto fatto con quello relativo alla
notifica dei segnali.
Una volta completata l’inizializzazione verrà eseguito indefinitamente il ciclo principale del
programma (2-45) che si è riportato in fig. 12.10, fintanto che questo non riceva un segnale di
SIGINT (ad esempio con la pressione di C-c). Il ciclo prevede che si attenda (2-3) la presenza
di un file descriptor pronto in lettura con epoll_wait,63 che si bloccherà fintanto che non siano
stati scritti dati sulla fifo o che non sia arrivato un segnale.64
Anche se in questo caso i file descriptor pronti possono essere al più due, si è comunque
adottato un approccio generico in cui questi verranno letti all’interno di un opportuno ciclo
(5-44) sul numero restituito da epoll_wait, esaminando i risultati presenti nel vettore events
all’interno di una catena di condizionali alternativi sul valore del file descriptor riconosciuto
come pronto.65
Il primo condizionale (6-24) è relativo al caso che si sia ricevuto un segnale e che il file de-
scriptor pronto corrisponda (6) a sigfd. Dato che in generale si possono ricevere anche notifiche
relativi a più di un singolo segnale, si è scelto di leggere una struttura signalfd_siginfo alla
volta, eseguendo la lettura all’interno di un ciclo (8-24) che prosegue fintanto che vi siano dati
da leggere.
Per questo ad ogni lettura si esamina (9-14) se il valore di ritorno della funzione read è
negativo, uscendo dal programma (11) in caso di errore reale, o terminando il ciclo (13) con un
break qualora si ottenga un errore di EAGAIN per via dell’esaurimento dei dati.66
In presenza di dati invece il programma proseguirà l’esecuzione stampando (19-20) il nome
del segnale ottenuto all’interno della struttura signalfd_siginfo letta in siginf67 ed il pid
del processo da cui lo ha ricevuto; inoltre (21-24) si controllerà anche se il segnale ricevuto è
SIGINT, che si è preso come segnale da utilizzare per la terminazione del programma, che verrà
eseguita dopo aver rimosso il file della name fifo.
Il secondo condizionale (26-39) è invece relativo al caso in cui ci siano dati pronti in lettura
sulla fifo e che il file descriptor pronto corrisponda (26) a fifofd. Di nuovo si effettueranno le
letture in un ciclo (28-39) ripetendole fin tanto che la funzione read non resituisce un errore di
EAGAIN (29-35).68 Se invece vi sono dati validi letti dalla fifo si inserirà (36) una terminazione di
stringa sul buffer e si stamperà il tutto (37-38) sullo standard output. L’ultimo condizionale (40-
44) è semplicemente una condizione di cattura per una eventualità che comunque non dovrebbe
mai verificarsi, e che porta alla uscita dal programma con una opportuna segnalazione di errore.
A questo punto si potrà eseguire il comando lanciandolo su un terminale, ed osservarne le
reazioni agli eventi generati da un altro terminale; lanciando il programma otterremo qualcosa
del tipo:
piccardi@hain:~/gapil/sources$ ./a.out
FifoReporter starting, pid 4568
si otterrà:
Message from fifo:
prova
end message
mentre inviando un segnale:
root@hain:~# kill 4568
si avrà:
Signal received:
Got SIGTERM
From pid 3361
ed infine premendo C-\ sul terminale in cui è in esecuzione si vedrà:
^\Signal received:
Got SIGQUIT
From pid 0
e si potrà far uscire il programma con C-c ottenendo:
^CSignal received:
Got SIGINT
From pid 0
SIGINT means exit
Lo stesso paradigma di notifica tramite file descriptor usato per i segnali è stato adottato
anche per i timer. In questo caso, rispetto a quanto visto in sez. 9.5.2, la scadenza di un timer
potrà essere letta da un file descriptor senza dover ricorrere ad altri meccanismi di notifica come
un segnale o un thread. Di nuovo questo ha il vantaggio di poter utilizzare le funzioni dell’I/O
multiplexing per attendere allo stesso tempo la disponibilità di dati o la ricezione della scadenza
di un timer.69
Le funzioni di questa nuova interfaccia ricalcano da vicino la struttura delle analoghe versioni
ordinarie introdotte con lo standard POSIX.1-2001, che abbiamo già illustrato in sez. 9.5.2.70
La prima funzione prevista, quella che consente di creare un timer, è timerfd_create, il cui
prototipo è:
#include <sys/timerfd.h>
int timerfd_create(int clockid, int flags)
Crea un timer associato ad un file descriptor per la notifica.
La funzione restituisce un numero di file descriptor in caso di successo o −1 in caso di errore, nel
qual caso errno assumerà uno dei valori:
EINVAL l’argomento clockid non è CLOCK_MONOTONIC o CLOCK_REALTIME, o l’argomento flag
non è valido, o è diverso da zero per kernel precedenti il 2.6.27.
ENOMEM non c’è memoria sufficiente per creare un nuovo file descriptor di signalfd.
ENODEV il kernel non può montare internamente il dispositivo per la gestione anonima degli
inode associati al file descriptor.
ed inoltre EMFILE e ENFILE.
69
in realtà per questo sarebbe già sufficiente signalfd per ricevere i segnali associati ai timer, ma la nuova
interfaccia semplifica notevolmente la gestione e consente di fare tutto con una sola system call.
70
questa interfaccia è stata introdotta in forma considerata difettosa con il kernel 2.6.22, per cui è stata im-
mediatamente tolta nel successivo 2.6.23 e reintrodotta in una forma considerata adeguata nel kernel 2.6.25, il
supporto nelle glibc è stato introdotto a partire dalla versione 2.8.6, la versione del kernel 2.6.22, presente solo su
questo kernel, non è supportata e non deve essere usata.
12.2. L’I/O MULTIPLEXING 439
La funzione prende come primo argomento un intero che indica il tipo di orologio a cui il
timer deve fare riferimento, i valori sono gli stessi delle funzioni dello standard POSIX-1.2001 già
illustrati in tab. 9.10, ma al momento i soli utilizzabili sono CLOCK_REALTIME e CLOCK_MONOTONIC.
L’argomento flags, come l’analogo di signalfd, consente di impostare i flag per l’I/O non
bloccante ed il close-on-exec sul file descriptor restituito,71 e deve essere specificato come una
maschera binaria delle costanti riportate in tab. 12.9.
Valore Significato
TFD_NONBLOCK imposta sul file descriptor il flag di O_NONBLOCK per
renderlo non bloccante.
TFD_CLOEXEC imposta il flag di O_CLOEXEC per la chiusura automatica
del file descriptor nella esecuzione di exec.
Tabella 12.9: Valori dell’argomento flags per la funzione timerfd_create che consentono di impostare i flag
del file descriptor.
In caso di successo la funzione restituisce un file descriptor sul quale verranno notificate le
scadenze dei timer. Come per quelli restituiti da signalfd anche questo file descriptor segue
la semantica dei sistemi unix-like, in particolare resta aperto attraverso una exec,72 e viene
duplicato attraverso una fork; questa ultima caratteristica comporta però che anche il figlio
può utilizzare i dati di un timer creato nel padre, a differenza di quanto avviene invece con i
timer impostati con le funzioni ordinarie.73
Una volta creato il timer con timerfd_create per poterlo utilizzare occorre armarlo impo-
standone un tempo di scadenza ed una eventuale periodicità di ripetizione, per farlo si usa la
funzione omologa di timer_settime per la nuova interfaccia; questa è timerfd_settime ed il
suo prototipo è:
#include <sys/timerfd.h>
int timerfd_settime(int fd, int flags, const struct itimerspec *new_value, struct
itimerspec *old_value)
Crea un timer associato ad un file descriptor per la notifica.
La funzione restituisce un numero di file descriptor in caso di successo o −1 in caso di errore, nel
qual caso errno assumerà uno dei valori:
EBADF l’argomento fd non corrisponde ad un file descriptor.
EINVAL il file descriptor fd non è stato ottenuto con timerfd_create, o i valori di flag o dei
campi tv_nsec in new_value non sono validi.
EFAULT o new_value o old_value non sono puntatori validi.
In questo caso occorre indicare su quale timer si intende operare specificando come primo
argomento il file descriptor ad esso associato, che deve essere stato ottenuto da una precedente
chiamata a timerfd_create. I restanti argomenti sono del tutto analoghi a quelli della omolo-
ga funzione timer_settime, e prevedono l’uso di strutture itimerspec (vedi fig. 9.16) per le
indicazioni di temporizzazione.
I valori ed il significato di questi argomenti sono gli stessi che sono già stati illustrati in
dettaglio in sez. 9.5.2 e non staremo a ripetere quanto detto in quell’occasione;74 l’unica differenza
riguarda l’argomento flags che serve sempre ad indicare se il tempo di scadenza del timer è
71
esso è stato introdotto a partire dal kernel 2.6.27, per le versioni precedenti deve essere passato un valore
nullo.
72
a meno che non si sia impostato il flag di close-on exec con TFD_CLOEXEC.
73
si ricordi infatti che, come illustrato in sez. 3.2.2, allarmi, timer e segnali pendenti nel padre vengono cancellati
per il figlio dopo una fork.
74
per brevità si ricordi che con new_value.it_value si indica la prima scadenza del timer e con
new_value.it_interval la sua periodicità.
440 CAPITOLO 12. LA GESTIONE AVANZATA DEI FILE
#include <sys/timerfd.h>
int timerfd_gettime(int fd, struct itimerspec *curr_value)
Crea un timer associato ad un file descriptor per la notifica.
La funzione restituisce un numero di file descriptor in caso di successo o −1 in caso di errore, nel
qual caso errno assumerà uno dei valori:
EBADF l’argomento fd non corrisponde ad un file descriptor.
EINVAL il file descriptor fd non è stato ottenuto con timerfd_create.
EFAULT o curr_value non è un puntatore valido.
Questo infatti diverrà pronto in lettura per tutte le varie funzioni dell’I/O multiplexing in
presenza di una o più scadenze del timer ad esso associato.
Inoltre sarà possibile ottenere il numero di volte che il timer è scaduto dalla ultima imposta-
zione
che può essere usato per leggere le notifiche delle scadenze dei timer. Queste possono essere
ottenute leggendo in maniera ordinaria il file descriptor con una read,
del segnale, e non ci sarà più la necessità di restare bloccati in attesa della disponibilità di accesso
ai file.
Per questo motivo Stevens, ed anche le pagine di manuale di Linux, chiamano questa moda-
lità “Signal driven I/O”. Si tratta di un’altra modalità di gestione dell’I/O, alternativa all’uso
di epoll,78 che consente di evitare l’uso delle funzioni poll o select che, come illustrato in
sez. 12.2.4, quando vengono usate con un numero molto grande di file descriptor, non hanno
buone prestazioni.
Tuttavia con l’implementazione classica dei segnali questa modalità di I/O presenta note-
voli problemi, dato che non è possibile determinare, quando i file descriptor sono più di uno,
qual è quello responsabile dell’emissione del segnale. Inoltre dato che i segnali normali non
si accodano (si ricordi quanto illustrato in sez. 9.1.4), in presenza di più file descriptor attivi
contemporaneamente, più segnali emessi nello stesso momento verrebbero notificati una volta
sola.
Linux però supporta le estensioni POSIX.1b dei segnali real-time, che vengono accodati e
che permettono di riconoscere il file descriptor che li ha emessi. In questo caso infatti si può
fare ricorso alle informazioni aggiuntive restituite attraverso la struttura siginfo_t, utilizzando
la forma estesa sa_sigaction del gestore installata con il flag 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)
impostando esplicitamente con il comando F_SETSIG di fcntl un segnale real-time da inviare in
caso di I/O asincrono (il segnale predefinito è SIGIO). In questo caso il gestore, tutte le volte che
riceverà SI_SIGIO come valore del campo si_code79 di siginfo_t, troverà nel campo si_fd il
valore del file descriptor che ha generato il segnale.
Un secondo vantaggio dell’uso dei segnali real-time è che essendo questi ultimi dotati di una
coda di consegna ogni segnale sarà associato ad uno solo file descriptor; inoltre sarà possibile
stabilire delle priorità nella risposta a seconda del segnale usato, dato che i segnali real-time
supportano anche questa funzionalità. In questo modo si può identificare immediatamente un
file su cui l’accesso è diventato possibile evitando completamente l’uso di funzioni come poll e
select, almeno fintanto che non si satura la coda.
Se infatti si eccedono le dimensioni di quest’ultima, il kernel, non potendo più assicurare il
comportamento corretto per un segnale real-time, invierà al suo posto un solo SIGIO, su cui si
saranno accumulati tutti i segnali in eccesso, e si dovrà allora determinare con un ciclo quali
sono i file diventati attivi. L’unico modo per essere sicuri che questo non avvenga è di impostare
la lunghezza della coda dei segnali real-time ad una dimensione identica al valore massimo del
numero di file descriptor utilizzabili.80
qualche modo82 se il loro file di configurazione è stato modificato, perché possano rileggerlo e
riconoscere le modifiche.
Questa scelta è stata fatta perché provvedere un simile meccanismo a livello generico per
qualunque file comporterebbe un notevole aumento di complessità dell’architettura della gestione
dei file, il tutto per fornire una funzionalità che serve soltanto in alcuni casi particolari. Dato
che all’origine 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 notifica.
Visto però il crescente interesse nei confronti di una funzionalità di questo tipo, che è mol-
to richiesta specialmente nello sviluppo dei programmi ad interfaccia grafica, quando si deve
presentare all’utente lo stato del filesystem, sono state successivamente introdotte delle esten-
sioni che permettessero la creazione di meccanismi di notifica più efficienti dell’unica soluzione
disponibile con l’interfaccia tradizionale, che è quella del polling.
Queste nuove funzionalità sono delle estensioni specifiche, non standardizzate, che sono di-
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 l’uso di alcuni coman-
di aggiuntivi per la funzione fcntl (vedi sez. 6.3.6), che divengono disponibili soltanto se si è
definita la macro _GNU_SOURCE prima di includere fcntl.h.
La prima di queste funzionalità è quella del cosiddetto file lease; questo è un meccanismo
che consente ad un processo, detto lease holder, di essere notificato quando un altro processo,
chiamato a sua volta lease breaker, cerca di eseguire una open o una truncate sul file del quale
l’holder detiene il lease. La notifica avviene in maniera analoga a come illustrato in precedenza
per l’uso di O_ASYNC: di default viene inviato al lease holder il segnale SIGIO, ma questo segnale
può essere modificato usando il comando F_SETSIG di fcntl.83 Se si è fatto questo84 e si è
installato il gestore del segnale con SA_SIGINFO si riceverà nel campo si_fd della struttura
siginfo_t il valore del file descriptor del file sul quale è stato compiuto l’accesso; in questo
modo un processo può mantenere anche più di un file lease.
Esistono due tipi di file lease: di lettura (read lease) e di scrittura (write lease). Nel primo
caso la notifica avviene quando un altro processo esegue l’apertura del file in scrittura o usa
truncate per troncarlo. Nel secondo caso la notifica avviene anche se il file viene aperto in
lettura; in quest’ultimo caso però il lease può essere ottenuto solo se nessun altro processo ha
aperto lo stesso file.
Come accennato in sez. 6.3.6 il comando di fcntl che consente di acquisire un file lease è
F_SETLEASE, che viene utilizzato anche per rilasciarlo. In tal caso il file descriptor fd passato
a fcntl servirà come riferimento per il file su cui si vuole operare, mentre per indicare il tipo
di operazione (acquisizione o rilascio) occorrerà specificare come valore dell’argomento arg di
fcntl uno dei tre valori di tab. 12.10.
Valore Significato
F_RDLCK Richiede un read lease.
F_WRLCK Richiede un write lease.
F_UNLCK Rilascia un file lease.
Tabella 12.10: Costanti per i tre possibili valori dell’argomento arg di fcntl quando usata con i comandi
F_SETLEASE e F_GETLEASE.
82
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 configurazione.
83
anche in questo caso si può rispecificare lo stesso SIGIO.
84
è in genere è opportuno farlo, come in precedenza, per utilizzare segnali real-time.
12.3. L’ACCESSO ASINCRONO AI FILE 443
Se invece si vuole conoscere lo stato di eventuali file lease occorrerà chiamare fcntl sul
relativo file descriptor fd con il comando F_GETLEASE, e si otterrà indietro nell’argomento arg
uno dei valori di tab. 12.10, che indicheranno la presenza del rispettivo tipo di lease, o, nel caso
di F_UNLCK, l’assenza di qualunque file lease.
Si tenga presente che un processo può mantenere solo un tipo di lease su un file, e che un
lease può essere ottenuto solo su file di dati (pipe e dispositivi sono quindi esclusi). Inoltre un
processo non privilegiato può ottenere un lease soltanto per un file appartenente ad un uid
corrispondente a quello del processo. Soltanto un processo con privilegi di amministratore (cioè
con la capability CAP_LEASE, vedi sez. 5.4.4) può acquisire lease su qualunque file.
Se su un file è presente un lease quando il lease breaker esegue una truncate o una open
che confligge con esso,85 la funzione si blocca86 e viene eseguita la notifica al lease holder, cosı̀
che questo possa completare le sue operazioni sul file e rilasciare il lease. In sostanza con un read
lease si rilevano i tentativi di accedere al file per modificarne 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 notifica avvengono solo in fase di apertura del file e non sulle singole operazioni
di lettura e scrittura.
L’utilizzo dei file lease consente al lease holder di assicurare la consistenza di un file, 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 notifica deve provvedere a completare
le necessarie operazioni (ad esempio scaricare eventuali buffer), 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 confligge 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 specificato
dal parametro di sistema mantenuto in /proc/sys/fs/lease-break-time sarà il kernel stesso
a rimuoverlo (o declassarlo) automaticamente.87 Una volta che un lease è stato rilasciato o
declassato (che questo sia fatto dal lease holder o dal kernel è lo stesso) le chiamate a open o
truncate eseguite dal lease breaker rimaste bloccate proseguono automaticamente.
Benché possa risultare utile per sincronizzare l’accesso ad uno stesso file da parte di più
processi, l’uso dei file lease non consente comunque di risolvere il problema di rilevare automati-
camente quando un file o una directory vengono modificati,88 che è quanto necessario ad esempio
ai programma di gestione dei file dei vari desktop grafici.
Per risolvere questo problema a partire dal kernel 2.4 è stata allora creata un’altra inter-
faccia,89 chiamata dnotify, che consente di richiedere una notifica quando una directory, o uno
qualunque dei file in essa contenuti, viene modificato. Come per i file lease la notifica avviene
di default attraverso il segnale SIGIO, ma se ne può utilizzare un altro.90 Inoltre, come in pre-
cedenza, si potrà ottenere nel gestore del segnale il file descriptor che è stato modificato tramite
il contenuto della struttura siginfo_t.
Ci si può registrare per le notifiche dei cambiamenti al contenuto di una certa directory
eseguendo la funzione fcntl su un file descriptor associato alla stessa con il comando F_NOTIFY.
85
in realtà truncate confligge sempre, mentre open, se eseguita in sola lettura, non confligge se si tratta di un
read lease.
86
a meno di non avere aperto il file con O_NONBLOCK, nel qual caso open fallirebbe con un errore di EWOULDBLOCK.
87
questa è una misura di sicurezza per evitare che un processo blocchi indefinitamente l’accesso ad un file
acquisendo un lease.
88
questa funzionalità venne aggiunta principalmente ad uso di Samba per poter facilitare l’emulazione del
comportamento di Windows sui file, ma ad oggi viene considerata una interfaccia mal progettata ed il suo uso è
fortemente sconsigliato a favore di inotify.
89
si ricordi che anche questa è una interfaccia specifica di Linux che deve essere evitata se si vogliono scri-
vere programmi portabili, e che le funzionalità illustrate sono disponibili soltanto se è stata definita la macro
_GNU_SOURCE.
90
e di nuovo, per le ragioni già esposte in precedenza, è opportuno che si utilizzino dei segnali real-time.
444 CAPITOLO 12. LA GESTIONE AVANZATA DEI FILE
Valore Significato
DN_ACCESS Un file è stato acceduto, con l’esecuzione di una fra read,
pread, readv.
DN_MODIFY Un file è stato modificato, con l’esecuzione di una fra
write, pwrite, writev, truncate, ftruncate.
DN_CREATE È stato creato un file nella directory, con l’esecuzione di
una fra open, creat, mknod, mkdir, link, symlink, rename
(da un’altra directory).
DN_DELETE È stato cancellato un file dalla directory con l’esecuzione
di una fra unlink, rename (su un’altra directory), rmdir.
DN_RENAME È stato rinominato un file all’interno della directory (con
rename).
DN_ATTRIB È stato modificato un attributo di un file con l’esecuzione
di una fra chown, chmod, utime.
DN_MULTISHOT Richiede una notifica permanente di tutti gli eventi.
Tabella 12.11: Le costanti che identificano le varie classi di eventi per i quali si richiede la notifica con il comando
F_NOTIFY di fcntl.
In questo caso l’argomento arg di fcntl serve ad indicare per quali classi eventi si vuole ricevere
la notifica, e prende come valore una maschera binaria composta dall’OR aritmetico di una o
più delle costanti riportate in tab. 12.11.
A meno di non impostare in maniera esplicita una notifica permanente usando il valore
DN_MULTISHOT, la notifica è singola: viene cioè inviata una sola volta quando si verifica uno
qualunque fra gli eventi per i quali la si è richiesta. Questo significa che un programma deve
registrarsi un’altra volta se desidera essere notificato di ulteriori cambiamenti. Se si eseguono
diverse chiamate con F_NOTIFY e con valori diversi per arg questi ultimi si accumulano; cioè
eventuali nuovi classi di eventi specificate in chiamate successive vengono aggiunte a quelle già
impostate nelle precedenti. Se si vuole rimuovere la notifica si deve invece specificare un valore
nullo.
Il maggiore problema di dnotify è quello della scalabilità: si deve usare un file descriptor per
ciascuna directory che si vuole tenere sotto controllo, il che porta facilmente ad avere un eccesso
di file aperti. Inoltre quando la directory che si controlla è all’interno di un dispositivo rimovibile,
mantenere il relativo file descriptor aperto comporta l’impossibilità di smontare il dispositivo e
di rimuoverlo, il che in genere complica notevolmente la gestione dell’uso di questi dispositivi.
Un altro problema è che l’interfaccia di dnotify consente solo di tenere sotto controllo il
contenuto di una directory; la modifica di un file viene segnalata, ma poi è necessario verificare
di quale file si tratta (operazione che può essere molto onerosa quando una directory contiene
un gran numero di file). Infine l’uso dei segnali come interfaccia di notifica 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 ed il suo uso oggi è
fortemente sconsigliato.
Per risolvere i problemi appena illustrati è stata introdotta una nuova interfaccia per l’os-
servazione delle modifiche a file o directory, chiamata inotify.91 Anche questa è una interfaccia
specifica di Linux (pertanto non deve essere usata se si devono scrivere programmi portabili),
ed è basata sull’uso di una coda di notifica degli eventi associata ad un singolo file descriptor,
il che permette di risolvere il principale problema di dnotify. La coda viene creata attraverso la
funzione inotify_init, il cui prototipo è:
91
l’interfaccia è disponibile a partire dal kernel 2.6.13, le relative funzioni sono state introdotte nelle glibc 2.4.
12.3. L’ACCESSO ASINCRONO AI FILE 445
#include <sys/inotify.h>
int inotify_init(void)
Inizializza una istanza di inotify.
La funzione restituisce un file descriptor in caso di successo, o −1 in caso di errore, nel qual caso
errno assumerà uno dei valori:
EMFILE si è raggiunto il numero massimo di istanze di inotify consentite all’utente.
ENFILE si è raggiunto il massimo di file descriptor aperti nel sistema.
ENOMEM non c’è sufficiente memoria nel kernel per creare l’istanza.
La funzione non prende alcun argomento; inizializza una istanza di inotify e restituisce un
file descriptor attraverso il quale verranno effettuate le operazioni di notifica;92 si tratta di un
file descriptor speciale che non è associato a nessun file su disco, e che viene utilizzato solo per
notificare gli eventi che sono stati posti in osservazione. Dato che questo file descriptor non è
associato a nessun file o directory reale, l’inconveniente di non poter smontare un filesystem i
cui file sono tenuti sotto osservazione viene completamente eliminato.93
Inoltre trattandosi di un file descriptor a tutti gli effetti, esso potrà essere utilizzato come
argomento per le funzioni select e poll e con l’interfaccia di epoll ;94 siccome gli eventi vengono
notificati come dati disponibili in lettura, dette funzioni ritorneranno tutte le volte che si avrà un
evento di notifica. Cosı̀, invece di dover utilizzare i segnali,95 si potrà gestire l’osservazione degli
eventi con una qualunque delle modalità di I/O multiplexing illustrate in sez. 12.2. Qualora si
voglia cessare l’osservazione, sarà sufficiente chiudere il file descriptor e tutte le risorse allocate
saranno automaticamente rilasciate.
Infine l’interfaccia di inotify consente di mettere sotto osservazione, oltre che una directory,
anche singoli file. Una volta creata la coda di notifica si devono definire gli eventi da tenere sotto
osservazione; questo viene fatto attraverso una lista di osservazione (o watch list) che è associata
alla coda. Per gestire la lista di osservazione l’interfaccia fornisce due funzioni, la prima di queste
è inotify_add_watch, il cui prototipo è:
#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:
EACCESS non si ha accesso in lettura al file indicato.
EINVAL mask non contiene eventi legali o fd non è un file descriptor di inotify.
ENOSPC si è raggiunto il numero massimo di voci di osservazione o il kernel non ha potuto
allocare una risorsa necessaria.
ed inoltre EFAULT, ENOMEM e EBADF.
La funzione consente di creare un “osservatore” (il cosiddetto “watch”) nella lista di osser-
vazione di una coda di notifica, che deve essere indicata specificando il file descriptor ad essa
associato nell’argomento fd.96 Il file o la directory da porre sotto osservazione vengono invece
indicati per nome, da passare nell’argomento pathname. Infine il terzo argomento, mask, indica
che tipo di eventi devono essere tenuti sotto osservazione e le modalità della stessa. L’operazione
92
per evitare abusi delle risorse di sistema è previsto che un utente possa utilizzare un numero limitato di istanze
di inotify; il valore di default del limite è di 128, ma questo valore può essere cambiato con sysctl o usando il file
/proc/sys/fs/inotify/max_user_instances.
93
anzi, una delle capacità dell’interfaccia di inotify è proprio quella di notificare il fatto che il filesystem su cui
si trova il file o la directory osservata è stato smontato.
94
ed a partire dal kernel 2.6.25 è stato introdotto anche il supporto per il signal-driven I/O trattato in
sez. 12.3.1.
95
considerati una pessima scelta dal punto di vista dell’interfaccia utente.
96
questo ovviamente dovrà essere un file descriptor creato con inotify_init.
446 CAPITOLO 12. LA GESTIONE AVANZATA DEI FILE
può essere ripetuta per tutti i file e le directory che si vogliono tenere sotto osservazione,97 e si
utilizzerà sempre un solo file descriptor.
Il tipo di evento che si vuole osservare deve essere specificato nell’argomento mask come
maschera binaria, combinando i valori delle costanti riportate in tab. 12.12 che identificano i
singoli bit della maschera ed il relativo significato. In essa si sono marcati con un “•” gli eventi
che, quando specificati per una directory, vengono osservati anche su tutti i file che essa contiene.
Nella seconda parte della tabella si sono poi indicate alcune combinazioni predefinite dei flag
della prima parte.
Valore Significato
IN_ACCESS • C’è stato accesso al file in lettura.
IN_ATTRIB • Ci sono stati cambiamenti sui dati dell’inode (o sugli attributi estesi,
vedi sez. 5.4.1).
IN_CLOSE_WRITE • È stato chiuso un file aperto in scrittura.
IN_CLOSE_NOWRITE • È stato chiuso un file aperto in sola lettura.
IN_CREATE • È stato creato un file o una directory in una directory sotto osservazione.
IN_DELETE • È stato cancellato un file o una directory in una directory sotto
osservazione.
IN_DELETE_SELF – È stato cancellato il file (o la directory) sotto osservazione.
IN_MODIFY • È stato modificato il file.
IN_MOVE_SELF È stato rinominato il file (o la directory) sotto osservazione.
IN_MOVED_FROM • Un file è stato spostato fuori dalla directory sotto osservazione.
IN_MOVED_TO • Un file è stato spostato nella directory sotto osservazione.
IN_OPEN • Un file è stato aperto.
IN_CLOSE Combinazione di IN_CLOSE_WRITE e IN_CLOSE_NOWRITE.
IN_MOVE Combinazione di IN_MOVED_FROM e IN_MOVED_TO.
IN_ALL_EVENTS Combinazione di tutti i flag possibili.
Tabella 12.12: Le costanti che identificano i bit della maschera binaria dell’argomento mask di
inotify_add_watch che indicano il tipo di evento da tenere sotto osservazione.
Oltre ai flag di tab. 12.12, 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
flag,98 riportati in tab. 12.13, che indicano le modalità di osservazione (da passare sempre nel-
l’argomento mask) e che al contrario dei precedenti non vengono mai impostati nei risultati in
uscita.
Valore Significato
IN_DONT_FOLLOW Non dereferenzia pathname se questo è un link simbolico.
IN_MASK_ADD Aggiunge a quelli già impostati i flag indicati nell’argomento mask,
invece di sovrascriverli.
IN_ONESHOT Esegue l’osservazione su pathname per una sola volta, rimuovendolo poi
dalla watch list.
IN_ONLYDIR Se pathname è una directory riporta soltanto gli eventi ad essa relativi
e non quelli per i file che contiene.
Tabella 12.13: Le costanti che identificano i bit della maschera binaria dell’argomento mask di
inotify_add_watch che indicano le modalità di osservazione.
Se non esiste nessun watch per il file o la directory specificata questo verrà creato per gli eventi
specificati dall’argomento mask, altrimenti la funzione sovrascriverà le impostazioni precedenti,
a meno che non si sia usato il flag IN_MASK_ADD, nel qual caso gli eventi specificati saranno
aggiunti a quelli già presenti.
97
anche in questo caso c’è un limite massimo che di default è pari a 8192, ed anche questo valore può essere
cambiato con sysctl o usando il file /proc/sys/fs/inotify/max_user_watches.
98
i flag 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 definirli a mano.
12.3. L’ACCESSO ASINCRONO AI FILE 447
Come accennato quando si tiene sotto osservazione una directory vengono restituite le in-
formazioni sia riguardo alla directory stessa che ai file che essa contiene; questo comportamento
può essere disabilitato utilizzando il flag IN_ONLYDIR, che richiede di riportare soltanto gli even-
ti relativi alla directory stessa. Si tenga presente inoltre che quando si osserva una directory
vengono riportati solo gli eventi sui file che essa contiene direttamente, non quelli relativi a file
contenuti in eventuali sottodirectory; se si vogliono osservare anche questi sarà necessario creare
ulteriori watch per ciascuna sottodirectory.
Infine usando il flag IN_ONESHOT è possibile richiedere una notifica singola;99 una volta
verificatosi uno qualunque fra gli eventi richiesti con inotify_add_watch l’osservatore verrà
automaticamente rimosso dalla lista di osservazione e nessun ulteriore evento sarà più notificato.
In caso di successo inotify_add_watch ritorna un intero positivo, detto watch descriptor,
che identifica univocamente un osservatore su una coda di notifica; 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 notifica, che permette di rimuovere un
osservatore, è inotify_rm_watch, ed il suo prototipo è:
#include <sys/inotify.h>
int inotify_rm_watch(int fd, uint32_t wd)
Rimuove un osservatore da una coda di notifica.
La funzione restituisce 0 in caso di successo, o −1 in caso di errore, nel qual caso errno assumerà
uno dei valori:
EBADF non si è specificato in fd un file descriptor valido.
EINVAL il valore di wd non è corretto, o fd non è associato ad una coda di notifica.
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 12.11: La struttura inotify_event usata dall’interfaccia di inotify per riportare gli eventi.
99
questa funzionalità però è disponibile soltanto a partire dal kernel 2.6.16.
100
ovviamente deve essere usato per questo argomento un valore ritornato da inotify_add_watch, altrimenti si
avrà un errore di EINVAL.
448 CAPITOLO 12. LA GESTIONE AVANZATA DEI FILE
Una ulteriore caratteristica dell’interfaccia di inotify è che essa permette di ottenere con
ioctl, come per i file descriptor associati ai socket (si veda sez. 17.3.3) il numero di byte
disponibili in lettura sul file descriptor, utilizzando su di esso l’operazione FIONREAD.101 Si può
cosı̀ utilizzare questa operazione, oltre che per predisporre una operazione di lettura con un buffer
di dimensioni adeguate, anche per ottenere rapidamente il numero di file che sono cambiati.
Una volta effettuata la lettura con read a ciascun evento sarà associata una struttura
inotify_event contenente i rispettivi dati. Per identificare a quale file o directory l’evento
corrisponde viene restituito nel campo wd il watch descriptor con cui il relativo osservatore è
stato registrato. Il campo mask contiene invece una maschera di bit che identifica il tipo di even-
to verificatosi; in essa compariranno sia i bit elencati nella prima parte di tab. 12.12, che gli
eventuali valori aggiuntivi102 di tab. 12.14.
Valore Significato
IN_IGNORED L’osservatore è stato rimosso, sia in maniera esplicita con l’uso di
inotify_rm_watch, che in maniera implicita per la rimozione dell’og-
getto osservato o per lo smontaggio del filesystem su cui questo si
trova.
IN_ISDIR L’evento 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 file che essa contiene).
IN_Q_OVERFLOW Si sono eccedute le dimensioni della coda degli eventi (overflow della
coda); in questo caso il valore di wd è −1.103
IN_UNMOUNT Il filesystem contenente l’oggetto posto sotto osservazione è stato
smontato.
Tabella 12.14: Le costanti che identificano i bit aggiuntivi usati nella maschera binaria del campo mask di
inotify_event.
Il campo cookie contiene invece un intero univoco che permette di identificare eventi cor-
relati (per i quali avrà lo stesso valore), al momento viene utilizzato soltanto per rilevare lo
spostamento di un file, consentendo cosı̀ all’applicazione di collegare la corrispondente coppia di
eventi IN_MOVED_TO e IN_MOVED_FROM.
Infine due campi name e len sono utilizzati soltanto quando l’evento è relativo ad un file
presente in una directory posta sotto osservazione, in tal caso essi contengono rispettivamente il
nome del file (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
terminazione, a seconda di eventuali necessità di allineamento del risultato, ed il valore di len
corrisponde al totale della dimensione di name, zeri aggiuntivi compresi. La stringa con il nome
del file viene restituita nella lettura subito dopo la struttura inotify_event; questo significa
che le dimensioni di ciascun evento di inotify saranno pari a sizeof(inotify_event) + len.
Vediamo allora un esempio dell’uso dell’interfaccia di inotify con un semplice programma
che permette di mettere sotto osservazione uno o più file e directory. Il programma si chiama
inotify_monitor.c ed il codice completo è disponibile coi sorgenti allegati alla guida, il corpo
principale del programma, che non contiene la sezione di gestione delle opzioni e le funzioni di
ausilio è riportato in fig. 12.12.
101
questa è una delle operazioni speciali per i file (vedi sez. 6.3.7), che è disponibile solo per i socket e per i file
descriptor creati con inotify_init.
102
questi compaiono solo nel campo mask di inotify_event, e non utilizzabili in fase di registrazione
dell’osservatore.
103
la coda di notifica ha una dimensione massima specificata 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.
12.3. L’ACCESSO ASINCRONO AI FILE 449
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 file o directory
mettere sotto osservazione (e qualora questo non avvenga esce stampando la pagina di aiuto);
dopo di che passa (16-20) all’inizializzazione di inotify ottenendo con inotify_init il relativo
file descriptor (oppure usce in caso di errore).
Il passo successivo è aggiungere (21-30) alla coda di notifica gli opportuni osservatori per
ciascuno dei file o directory indicati all’invocazione del comando; questo viene fatto eseguendo
un ciclo (22-29) fintanto che la variabile i, inizializzata a zero (21) all’inizio del ciclo, è minore
del numero totale di argomenti rimasti. All’interno 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 l’indice (29).
Completa l’inizializzazione 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
all’inizio del ciclo (33) una read che si bloccherà fintanto che non si saranno verificati eventi.
Dato che l’interfaccia di inotify può riportare anche più eventi in una sola lettura, si è avuto
cura di passare alla read un buffer di dimensioni adeguate, inizializzato in (7) ad un valore di
approssimativamente 512 eventi.104 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 fine invece si esegue un ciclo (43-52) per leggere tutti gli
eventi restituiti, al solito si inizializza l’indice i a zero (42) e si ripetono le operazioni (43)
fintanto che esso non supera il numero di byte restituiti in lettura. Per ciascun evento all’interno
del ciclo si assegna105 alla variabile event l’indirizzo nel buffer della corrispondente struttura
inotify_event (44), e poi si stampano il numero di watch descriptor (45) ed il file 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 file associato all’evento 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.106 Si utilizza poi (50) la funzione printevent,
che interpreta il valore del campo event->mask per stampare il tipo di eventi accaduti.107 Infine
(51) si provvede ad aggiornare l’indice i per farlo puntare all’evento 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,
Watch descriptor 1
Observed event on /home/piccardi/gapil/
IN_CLOSE_NOWRITE,
I lettori più accorti si saranno resi conto che nel ciclo di lettura degli eventi appena illustrato
non viene trattato il caso particolare in cui la funzione read restituisce in nread un valore nullo.
104
si ricordi che la quantità di dati restituita da inotify è variabile a causa della diversa lunghezza del nome del
file restituito insieme a inotify_event.
105
si noti come si sia eseguito un opportuno casting del puntatore.
106
l’interfaccia infatti, qualora il nome non sia presente, non avvalora il campo event->name, che si troverà a
contenere quello che era precedentemente presente nella rispettiva locazione di memoria, nel caso più comune il
puntatore al nome di un file osservato in precedenza.
107
per il relativo codice, che non riportiamo in quanto non essenziale alla comprensione dell’esempio, si possono
utilizzare direttamente i sorgenti allegati alla guida.
12.3. L’ACCESSO ASINCRONO AI FILE 451
Lo si è fatto perché con inotify il ritorno di una read con un valore nullo avviene soltanto,
come forma di avviso, quando si sia eseguita la funzione specificando un buffer di dimensione
insufficiente a contenere anche un solo evento. Nel nostro caso le dimensioni erano senz’altro
sufficienti, per cui tale evenienza non si verificherà mai.
Ci si potrà però chiedere cosa succede se il buffer è sufficiente per un evento, ma non per
tutti gli eventi verificatisi. Come si potrà notare nel codice illustrato in precedenza non si è
presa nessuna precauzione per verificare che non ci fossero stati troncamenti dei dati. Anche in
questo caso il comportamento scelto è corretto, perché l’interfaccia di inotify garantisce automa-
ticamente, anche quando ne sono presenti in numero maggiore, di restituire soltanto il numero
di eventi che possono rientrare completamente nelle dimensioni del buffer specificato.108 Se gli
eventi sono di più saranno restituiti solo quelli che entrano interamente nel buffer e gli altri
saranno restituiti alla successiva chiamata di read.
Infine un’ultima caratteristica dell’interfaccia di inotify è che gli eventi restituiti nella lettura
formano una sequenza ordinata, è cioè garantito che se si esegue uno spostamento di un file gli
eventi vengano generati nella sequenza corretta. L’interfaccia garantisce anche che se si verificano
più eventi consecutivi identici (vale a dire con gli stessi valori dei campi wd, mask, cookie, e
name) questi vengono raggruppati in un solo evento.
struct aiocb
{
int aio_fildes ; /* File descriptor . */
off_t aio_offset ; /* File offset */
int aio_lio_opcode ; /* Operation to be performed . */
int aio_reqprio ; /* Request priority offset . */
volatile void * aio_buf ; /* Location of buffer . */
size_t aio_nbytes ; /* Length of transfer . */
struct sigevent aio_sigevent ; /* Signal number and value . */
};
asincrona, il concetto di posizione corrente sul file viene a mancare; pertanto si deve sempre
specificare nel campo aio_offset la posizione sul file da cui i dati saranno letti o scritti. Nel
campo aio_buf deve essere specificato l’indirizzo del buffer usato per l’I/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.109 La prio-
rità 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, come vedremo, permette di eseguire con una sola chiamata una serie di operazioni, usando
un vettore di control block. Tramite questo campo si specifica quale è la natura di ciascuna di
esse.
Infine il campo aio_sigevent è una struttura di tipo sigevent (illustrata in in fig. 9.15)
che serve a specificare il modo in cui si vuole che venga effettuata la notifica del completamento
delle operazioni richieste; per la trattazione delle modalità di utilizzo della stessa si veda quanto
già visto in proposito in sez. 9.5.2.
Le due funzioni base dell’interfaccia per l’I/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 specificato con aiocbp.
int aio_write(struct aiocb *aiocbp)
Richiede una scrittura asincrona secondo quanto specificato con aiocbp.
Le funzioni restituiscono 0 in caso di successo, e -1 in caso di errore, nel qual caso errno assumerà
uno dei valori:
EBADF si è specificato un file descriptor sbagliato.
ENOSYS la funzione non è implementata.
EINVAL si è specificato un valore non valido per i campi aio_offset o aio_reqprio di aiocbp.
EAGAIN la coda delle richieste è momentaneamente piena.
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. É per questo motivo che occorre
chiamare la funzione solo dopo che l’operazione cui aiocbp fa riferimento si è completata. Una
chiamata precedente il completamento delle operazioni darebbe risultati indeterminati.
La funzione restituisce il valore di ritorno relativo all’operazione eseguita, cosı̀ come ricavato
dalla sottostante system call (il numero di byte letti, scritti o il valore di ritorno di fsync). É
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 l’interfaccia POSIX.1b mette a disposizione un’altra
operazione, quella di sincronizzazione dell’I/O, compiuta dalla funzione aio_fsync, che ha lo
stesso effetto della analoga fsync, ma viene eseguita in maniera asincrona; il suo prototipo è:
#include <aio.h>
int aio_fsync(int op, struct aiocb *aiocbp)
Richiede la sincronizzazione dei dati per il file indicato da aiocbp.
La funzione restituisce 0 in caso di successo e -1 in caso di errore, che può essere, con le stesse
modalità di aio_read, EAGAIN, EBADF o EINVAL.
Il successo della chiamata assicura la sincronizzazione delle operazioni fino allora richieste,
niente è garantito riguardo la sincronizzazione dei dati relativi ad eventuali operazioni richieste
successivamente. Se si è specificato un meccanismo di notifica questo sarà innescato una volta
che le operazioni di sincronizzazione dei dati saranno completate.
In alcuni casi può essere necessario interrompere le operazioni (in genere quando viene richie-
sta un’uscita 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 è:
#include <aio.h>
int aio_cancel(int fildes, struct aiocb *aiocbp)
Richiede la cancellazione delle operazioni sul file fildes specificate da aiocbp.
La funzione permette di cancellare una operazione specifica sul file fildes, o tutte le opera-
zioni pendenti, specificando 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 co-
dice di ritorno sarà -1, inoltre il meccanismo di notifica non verrà invocato. Se si specifica una
operazione relativa ad un altro file descriptor il risultato è indeterminato. In caso di successo, i
possibili valori di ritorno per aio_cancel (anch’essi definiti in aio.h) sono tre:
AIO_ALLDONE indica che le operazioni di cui si è richiesta la cancellazione sono state già
completate,
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
sono le operazioni effettivamente cancellate. Le operazioni che non sono state cancellate prose-
guiranno il loro corso normale, compreso quanto richiesto riguardo al meccanismo di notifica del
loro avvenuto completamento.
Benché l’I/O asincrono preveda un meccanismo di notifica, l’interfaccia fornisce anche una
apposita funzione, aio_suspend, che permette di sospendere l’esecuzione del processo chiamante
fino al completamento di una specifica operazione; il suo prototipo è:
#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 specificate
da list.
La funzione restituisce 0 se una (o più) operazioni sono state completate, e -1 in caso di errore nel
qual caso errno assumerà uno dei valori:
EAGAIN nessuna operazione è stata completata entro timeout.
ENOSYS la funzione non è implementata.
EINTR la funzione è stata interrotta da un segnale.
La funzione permette di bloccare il processo fintanto che almeno una delle nent operazioni
specificate nella lista list è completata, per un tempo massimo specificato da timout, o fintanto
che non arrivi un segnale.110 La lista deve essere inizializzata con delle strutture aiocb relative
ad operazioni effettivamente richieste, ma può contenere puntatori nulli, che saranno ignorati.
110
si tenga conto che questo segnale può anche essere quello utilizzato come meccanismo di notifica.
12.4. ALTRE MODALITÀ DI I/O AVANZATO 455
In caso si siano specificati valori non validi l’effetto è indefinito. Un valore NULL per timout
comporta l’assenza di timeout.
Lo standard POSIX.1b infine ha previsto pure una funzione, lio_listio, che permette di
effettuare la richiesta di una intera lista di operazioni di lettura o scrittura; il suo prototipo è:
#include <aio.h>
int lio_listio(int mode, struct aiocb * const list[], int nent, struct sigevent
*sig)
Richiede l’esecuzione delle operazioni di I/O elencata da list, secondo la modalità mode.
La funzione restituisce 0 in caso di successo, e -1 in caso di errore, nel qual caso errno assumerà
uno dei valori:
EAGAIN nessuna operazione è stata completata entro timeout.
EINVAL si è passato un valore di mode non valido o un numero di operazioni nent maggiore di
AIO_LISTIO_MAX.
ENOSYS la funzione non è implementata.
EINTR la funzione è stata interrotta da un segnale.
La funzione esegue la richiesta delle nent operazioni indicate nella lista list che deve con-
tenere gli indirizzi di altrettanti control block opportunamente inizializzati; in particolare dovrà
essere specificato il tipo di operazione con il campo aio_lio_opcode, che può prendere i valori:
dove LIO_NOP viene usato quando si ha a che fare con un vettore di dimensione fissa, per poter
specificare solo alcune operazioni, o quando si sono dovute cancellare delle operazioni e si deve
ripetere la richiesta per quelle non completate.
L’argomento mode controlla il comportamento della funzione, se viene usato il valore LIO_WAIT
la funzione si blocca fino 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 chia-
mante può richiedere la notifica del completamento di tutte le richieste, impostando l’argomento
sig in maniera analoga a come si fa per il campo aio_sigevent di aiocb.
Figura 12.14: Disposizione della memoria di un processo quando si esegue la mappatura in memoria di un file.
le pagine che vengono salvate e rilette nella swap, si incaricherà di sincronizzare il contenuto
di quel segmento di memoria con quello del file mappato su di esso. Per questo motivo si può
parlare tanto di file mappato in memoria, quanto di memoria mappata su file.
L’uso del memory-mapping comporta una notevole semplificazione delle operazioni di I/O, in
quanto non sarà più necessario utilizzare dei buffer intermedi su cui appoggiare i dati da traferire,
poiché questi potranno essere acceduti direttamente nella sezione di memoria mappata; inoltre
questa interfaccia è più efficiente delle usuali funzioni di I/O, in quanto permette di caricare in
memoria solo le parti del file che sono effettivamente usate ad un dato istante.
Infatti, dato che l’accesso è fatto direttamente attraverso la memoria virtuale, la sezione di
memoria mappata su cui si opera sarà a sua volta letta o scritta sul file una pagina alla volta e
solo per le parti effettivamente usate, il tutto in maniera completamente trasparente al processo;
l’accesso alle pagine non ancora caricate avverrà allo stesso modo con cui vengono caricate in
memoria le pagine che sono state salvate sullo swap.
Infine in situazioni in cui la memoria è scarsa, le pagine che mappano un file vengono salvate
automaticamente, cosı̀ come le pagine dei programmi vengono scritte sulla swap; questo consente
di accedere ai file su dimensioni il cui solo limite è quello dello spazio di indirizzi disponibile, e
non della memoria su cui possono esserne lette delle porzioni.
L’interfaccia POSIX implementata da Linux prevede varie funzioni per la gestione del me-
mory mapped I/O, la prima di queste, che serve ad eseguire la mappatura in memoria di un file,
è mmap; il suo prototipo è:
12.4. ALTRE MODALITÀ DI I/O AVANZATO 457
#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 specificata del file 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:
EBADF il file descriptor non è valido, e non si è usato MAP_ANONYMOUS.
EACCES o fd non si riferisce ad un file regolare, o si è usato MAP_PRIVATE ma fd non è aperto
in lettura, o si è usato MAP_SHARED e impostato PROT_WRITE ed fd non è aperto in
lettura/scrittura, o si è impostato PROT_WRITE ed fd è in append-only.
EINVAL i valori di start, length o offset non sono validi (o troppo grandi o non allineati
sulla dimensione delle pagine).
ETXTBSY si è impostato MAP_DENYWRITE ma fd è aperto in scrittura.
EAGAIN il file è bloccato, o si è bloccata troppa memoria rispetto a quanto consentito dai limiti
di sistema (vedi sez. 8.3.2).
ENOMEM non c’è memoria o si è superato il limite sul numero di mappature possibili.
ENODEV il filesystem di fd non supporta il memory mapping.
EPERM l’argomento prot ha richiesto PROT_EXEC, ma il filesystem di fd è montato con l’opzione
noexec.
ENFILE si è superato il limite del sistema sul numero di file aperti (vedi sez. 8.3.2).
La funzione richiede di mappare in memoria la sezione del file fd a partire da offset per
length byte, preferibilmente all’indirizzo start. Il valore di offset deve essere un multiplo
della dimensione di una pagina di memoria.
Valore Significato
PROT_EXEC Le pagine possono essere eseguite.
PROT_READ Le pagine possono essere lette.
PROT_WRITE Le pagine possono essere scritte.
PROT_NONE L’accesso alle pagine è vietato.
Tabella 12.15: Valori dell’argomento prot di mmap, relativi alla protezione applicate alle pagine del file mappate
in memoria.
Valore Significato
MAP_FIXED Non permette di restituire un indirizzo diverso da start, se questo non può
essere usato mmap fallisce. Se si imposta questo flag il valore di start deve
essere allineato alle dimensioni di una pagina.
MAP_SHARED I cambiamenti sulla memoria mappata vengono riportati sul file e saranno
immediatamente visibili agli altri processi che mappano lo stesso file.112 Il file
su disco però non sarà aggiornato fino alla chiamata di msync o munmap), e solo
allora le modifiche saranno visibili per l’I/O convenzionale. Incompatibile con
MAP_PRIVATE.
MAP_PRIVATE I cambiamenti sulla memoria mappata non vengono riportati sul file. Ne viene
fatta una copia privata cui solo il processo chiamante ha accesso. Le modifiche
sono mantenute attraverso il meccanismo del copy on write e salvate su swap in
caso di necessità. Non è specificato se i cambiamenti sul file originale vengano
riportati sulla regione mappata. Incompatibile con MAP_SHARED.
MAP_DENYWRITE In Linux viene ignorato per evitare DoS (veniva usato per segnalare che
tentativi di scrittura sul file dovevano fallire con ETXTBSY).
MAP_EXECUTABLE Ignorato.
MAP_NORESERVE Si usa con MAP_PRIVATE. Non riserva delle pagine di swap ad uso del meccani-
smo del copy on write per mantenere le modifiche fatte alla regione mappata,
in questo caso dopo una scrittura, se non c’è più memoria disponibile, si ha
l’emissione di un SIGSEGV.
MAP_LOCKED Se impostato impedisce lo swapping delle pagine mappate.
MAP_GROWSDOWN Usato per gli stack. Indica che la mappatura deve essere effettuata con gli
indirizzi crescenti verso il basso.
MAP_ANONYMOUS La mappatura non è associata a nessun file. Gli argomenti fd e offset sono
ignorati.113
MAP_ANON Sinonimo di MAP_ANONYMOUS, deprecato.
MAP_FILE Valore di compatibilità, ignorato.
MAP_32BIT Esegue la mappatura sui primi 2Gb dello spazio degli indirizzi, viene suppor-
tato solo sulle piattaforme x86-64 per compatibilità con le applicazioni a 32
bit. Viene ignorato se si è richiesto MAP_FIXED.
MAP_POPULATE Esegue il prefaulting delle pagine di memoria necessarie alla mappatura.
MAP_NONBLOCK Esegue un prefaulting più limitato che non causa I/O.114
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à l’emissione
di un segnale di violazione di accesso (SIGSEGV), dato che i permessi sul segmento di memoria
relativo non consentono questo tipo di accesso.
È invece assai diversa la questione relativa agli accessi al di fuori della regione di cui si è
richiesta la mappatura. A prima vista infatti si potrebbe ritenere che anch’essi debbano generare
un segnale di violazione di accesso; questo però non tiene conto del fatto che, essendo basata
sul meccanismo della paginazione, la mappatura in memoria non può che essere eseguita su un
segmento di dimensioni rigorosamente multiple di quelle di una pagina, ed in generale queste
potranno non corrispondere alle dimensioni effettive del file o della sezione che si vuole mappare.
Il caso più comune è quello illustrato in fig. 12.15, in cui la sezione di file non rientra nei
confini di una pagina: in tal caso verrà il file sarà mappato su un segmento di memoria che si
estende fino al bordo della pagina successiva.
In questo caso è possibile accedere a quella zona di memoria che eccede le dimensioni speci-
ficate da length, senza ottenere un SIGSEGV poiché essa è presente nello spazio di indirizzi del
processo, anche se non è mappata sul file. Il comportamento del sistema è quello di restituire un
valore nullo per quanto viene letto, e di non riportare su file quanto viene scritto.
Un caso più complesso è quello che si viene a creare quando le dimensioni del file mappato
sono più corte delle dimensioni della mappatura, oppure quando il file è stato troncato, dopo
che è stato mappato, ad una dimensione inferiore a quella della mappatura in memoria.
12.4. ALTRE MODALITÀ DI I/O AVANZATO 459
Figura 12.15: Schema della mappatura in memoria di una sezione di file di dimensioni non corrispondenti al
bordo di una pagina.
In questa situazione, per la sezione di pagina parzialmente coperta dal contenuto del file,
vale esattamente quanto visto in precedenza; invece per la parte che eccede, fino alle dimensioni
date da length, l’accesso non sarà più possibile, ma il segnale emesso non sarà SIGSEGV, ma
SIGBUS, come illustrato in fig. 12.16.
Non tutti i file possono venire mappati in memoria, dato che, come illustrato in fig. 12.14, la
mappatura introduce una corrispondenza biunivoca fra una sezione di un file ed una sezione di
memoria. Questo comporta che ad esempio non è possibile mappare in memoria file descriptor
relativi a pipe, socket e fifo, per i quali non ha senso parlare di sezione. Lo stesso vale anche per
alcuni file 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 è
l’interfaccia al ponte PCI-VME del chip Universe) che sono utilizzabili solo con questa interfaccia.
Figura 12.16: Schema della mappatura in memoria di file di dimensioni inferiori alla lunghezza richiesta.
Dato che passando attraverso una fork lo spazio di indirizzi viene copiato integralmente, i file
mappati in memoria verranno ereditati in maniera trasparente dal processo figlio, mantenendo
gli stessi attributi avuti nel padre; cosı̀ se si è usato MAP_SHARED padre e figlio accederanno allo
stesso file in maniera condivisa, mentre se si è usato MAP_PRIVATE ciascuno di essi manterrà una
sua versione privata indipendente. Non c’è invece nessun passaggio attraverso una exec, dato
che quest’ultima sostituisce tutto lo spazio degli indirizzi di un processo con quello di un nuovo
programma.
460 CAPITOLO 12. LA GESTIONE AVANZATA DEI FILE
Quando si effettua la mappatura di un file vengono pure modificati 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
a partire dal momento in cui la mappatura è stata effettuata: il primo riferimento ad una pagina
mappata su un file aggiorna questo tempo. I valori di st_ctime e st_mtime possono venir
cambiati solo quando si è consentita la scrittura sul file (cioè per un file mappato con PROT_WRITE
e MAP_SHARED) e sono aggiornati dopo la scrittura o in corrispondenza di una eventuale msync.
Dato per i file mappati in memoria le operazioni di I/O sono gestite direttamente dalla
memoria virtuale, occorre essere consapevoli delle interazioni che possono esserci con operazioni
effettuate con l’interfaccia standard dei file di cap. 6. Il problema è che una volta che si è mappato
un file, 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 modifica un file con l’interfaccia standard queste modifiche potranno essere
visibili o meno a seconda del momento in cui la memoria virtuale trasporterà dal disco in memoria
quella sezione del file, perciò è del tutto imprevedibile il risultato della modifica di un file nei
confronti del contenuto della memoria su cui è mappato.
Per questo, è sempre sconsigliabile eseguire scritture su file attraverso l’interfaccia standard
quando lo si è mappato in memoria, è invece possibile usare l’interfaccia standard per leggere un
file mappato in memoria, purché si abbia una certa cura; infatti l’interfaccia dell’I/O mappato
in memoria mette a disposizione la funzione msync per sincronizzare il contenuto della memoria
mappata con il file su disco; il suo prototipo è:
#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 file mappato in memoria.
La funzione restituisce 0 in caso di successo, e -1 in caso di errore nel qual caso errno assumerà
uno dei valori:
EINVAL o start non è multiplo di PAGE_SIZE, o si è specificato un valore non valido per flags.
EFAULT l’intervallo specificato non ricade in una zona precedentemente mappata.
L’argomento flag è specificato come maschera binaria composta da un OR dei valori ripor-
tati in tab. 12.17, di questi però MS_ASYNC e MS_SYNC sono incompatibili; con il primo valore
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 effettivamente eseguita. Il terzo flag fa sı̀ che vengano invalidate, per tutte le mappature
dello stesso file, le pagine di cui si è richiesta la sincronizzazione, cosı̀ che esse possano essere
immediatamente aggiornate con i nuovi valori.
Una volta che si sono completate le operazioni di I/O si può eliminare la mappatura della
memoria usando la funzione munmap, il suo prototipo è:
12.4. ALTRE MODALITÀ DI I/O AVANZATO 461
#include <unistd.h>
#include <sys/mman.h>
int munmap(void *start, size_t length)
Rilascia la mappatura sulla sezione di memoria specificata.
La funzione restituisce 0 in caso di successo, e -1 in caso di errore nel qual caso errno assumerà
uno dei valori:
EINVAL l’intervallo specificato non ricade in una zona precedentemente mappata.
La funzione cancella la mappatura per l’intervallo specificato con start e length; ogni
successivo accesso a tale regione causerà un errore di accesso in memoria. L’argomento start
deve essere allineato alle dimensioni di una pagina, e la mappatura di tutte le pagine contenute
anche parzialmente nell’intervallo indicato, verrà rimossa. Indicare un intervallo che non contiene
mappature non è un errore. Si tenga presente inoltre che alla conclusione di un processo ogni
pagina mappata verrà automaticamente rilasciata, mentre la chiusura del file descriptor usato
per il memory mapping non ha alcun effetto 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
memoria virtuale. Questa funzione è mprotect ed il suo prototipo è:
#include <sys/mman.h>
int mprotect(const void *addr, size_t len, int prot)
Modifica le protezioni delle pagine di memoria comprese nell’intervallo specificato.
La funzione restituisce 0 in caso di successo, e -1 in caso di errore nel qual caso errno assumerà
uno dei valori:
EINVAL il valore di addr non è valido o non è un multiplo di PAGE_SIZE.
EACCESS l’operazione non è consentita, ad esempio si è cercato di marcare con PROT_WRITE un
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 specificata in
prot con una combinazione dei valori di tab. 12.15. La nuova protezione verrà applicata a tutte
le pagine contenute, anche parzialmente, dall’intervallo fra addr e addr+size-1.
Infine Linux supporta alcune operazioni specifiche non disponibili su altri kernel unix-like.
La prima di queste è la possibilità di modificare un precedente memory mapping, ad esempio
per espanderlo o restringerlo. Questo è realizzato dalla funzione mremap, il cui prototipo è:
#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 file.
La funzione restituisce l’indirizzo 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:
EINVAL il valore di old_address non è un puntatore valido.
EFAULT ci sono indirizzi non validi nell’intervallo specificato da old_address e old_size, o ci
sono altre mappature di tipo non corrispondente a quella richiesta.
ENOMEM non c’è memoria sufficiente oppure l’area di memoria non può essere espansa
all’indirizzo virtuale corrente, e non si è specificato MREMAP_MAYMOVE nei flag.
EAGAIN il segmento di memoria scelto è bloccato e non può essere rimappato.
La funzione richiede come argomenti old_address (che deve essere allineato alle dimensioni
di una pagina di memoria) che specifica il precedente indirizzo del memory mapping e old_size,
462 CAPITOLO 12. LA GESTIONE AVANZATA DEI FILE
che ne indica la dimensione. Con new_size si specifica invece la nuova dimensione che si vuole
ottenere. Infine l’argomento flags è una maschera binaria per i flag che controllano il compor-
tamento della funzione. Il solo valore utilizzato è MREMAP_MAYMOVE115 che consente di eseguire
l’espansione anche quando non è possibile utilizzare il precedente indirizzo. Per questo motivo,
se si è usato questo flag, la funzione può restituire un indirizzo della nuova zona di memoria che
non è detto coincida con old_address.
La funzione si appoggia al sistema della memoria virtuale per modificare l’associazione fra
gli indirizzi virtuali del processo e le pagine di memoria, modificando i dati direttamente nella
page table del processo. Come per mprotect la funzione può essere usata in generale, anche per
pagine di memoria non corrispondenti ad un memory mapping, e consente cosı̀ di implementare
la funzione realloc in maniera molto efficiente.
Una caratteristica comune a tutti i sistemi unix-like è che la mappatura in memoria di un file
viene eseguita in maniera lineare, cioè parti successive di un file vengono mappate linearmente su
indirizzi successivi in memoria. Esistono però delle applicazioni116 in cui è utile poter mappare
sezioni diverse di un file su diverse zone di memoria.
Questo è ovviamente sempre possibile eseguendo ripetutamente la funzione mmap per ciascuna
delle diverse aree del file che si vogliono mappare in sequenza non lineare,117 ma questo approccio
ha delle conseguenze molto pesanti in termini di prestazioni. Infatti per ciascuna mappatura in
memoria deve essere definita nella page table del processo una nuova area di memoria virtuale118
che corrisponda alla mappatura, in modo che questa diventi visibile nello spazio degli indirizzi
come illustrato in fig. 12.14.
Quando un processo esegue un gran numero di mappature diverse119 per realizzare a mano
una mappatura non-lineare si avrà un accrescimento eccessivo della sua page table, e lo stesso
accadrà per tutti gli altri processi che utilizzano questa tecnica. In situazioni in cui le applicazioni
hanno queste esigenze si avranno delle prestazioni ridotte, dato che il kernel dovrà impiegare
molte risorse120 solo per mantenere i dati di una gran quantità di memory mapping.
Per questo motivo con il kernel 2.5.46 è stato introdotto, ad opera di Ingo Molnar, un
meccanismo che consente la mappatura non-lineare. Anche questa è una caratteristica specifica di
Linux, non presente in altri sistemi unix-like. Diventa cosı̀ possibile utilizzare una sola mappatura
iniziale121 e poi rimappare a piacere all’interno di questa i dati del file. Ciò è possibile grazie ad
una nuova system call, remap_file_pages, il cui prototipo è:
#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à
uno dei valori:
EINVAL si è usato un valore non valido per uno degli argomenti o start non fa riferimento ad
un memory mapping valido creato con MAP_SHARED.
Per poter utilizzare questa funzione occorre anzitutto effettuare preliminarmente una chiama-
ta a mmap con MAP_SHARED per definire l’area di memoria che poi sarà rimappata non linearmente.
Poi di chiamerà questa funzione per modificare le corrispondenze fra pagine di memoria e pagine
del file; si tenga presente che remap_file_pages permette anche di mappare la stessa pagina di
un file in più pagine della regione mappata.
115
per poter utilizzare questa costante occorre aver definito _GNU_SOURCE prima di includere sys/mman.h.
116
in particolare la tecnica è usata dai database o dai programmi che realizzano macchine virtuali.
117
ed in effetti è quello che veniva fatto anche con Linux prima che fossero introdotte queste estensioni.
118
quella che nel gergo del kernel viene chiamata VMA (virtual memory area).
119
si può arrivare anche a centinaia di migliaia.
120
sia in termini di memoria interna per i dati delle page table, che di CPU per il loro aggiornamento.
121
e quindi una sola virtual memory area nella page table del processo.
12.4. ALTRE MODALITÀ DI I/O AVANZATO 463
La funzione richiede che si identifichi la sezione del file che si vuole riposizionare all’interno
del memory mapping con gli argomenti pgoff e size; l’argomento start invece deve indicare
un indirizzo all’interno dell’area definita dall’mmap iniziale, a partire dal quale la sezione di file
indicata verrà rimappata. L’argomento prot deve essere sempre nullo, mentre flags prende gli
stessi valori di mmap (quelli di tab. 12.15) ma di tutti i flag solo MAP_NONBLOCK non viene ignorato.
Insieme alla funzione remap_file_pages nel kernel 2.5.46 con sono stati introdotti anche
due nuovi flag 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 file di grosse dimensioni.
Il comportamento normale del sistema della memoria virtuale è quello per cui la regione mappata
viene aggiunta alla page table del processo, ma i dati verranno effettivamente utilizzati (si avrà
cioè un page fault che li trasferisce dal disco alla memoria) soltanto in corrispondenza dell’accesso
a ciascuna delle pagine interessate dal memory mapping.
Questo vuol dire che il passaggio dei dati dal disco alla memoria avverrà una pagina alla
volta con un gran numero di page fault, chiaramente se si sa in anticipo che il file verrà utilizzato
immediatamente, è molto più efficiente eseguire un prefaulting in cui tutte le pagine di memoria
interessate alla mappatura vengono “popolate” in una sola volta, questo comportamento viene
abilitato quando si usa con mmap il flag MAP_POPULATE.
Dato che l’uso di MAP_POPULATE comporta dell’I/O su disco che può rallentare l’esecuzione
di mmap è stato introdotto anche un secondo flag, MAP_NONBLOCK, che esegue un prefaulting più
limitato in cui vengono popolate solo le pagine della mappatura che già si trovano nella cache
del kernel.122
Per i vantaggi illustrati all’inizio del paragrafo l’interfaccia del memory mapped I/O viene
usata da una grande varietà di programmi, spesso con esigenze molto diverse fra di loro riguardo
le modalità con cui verranno eseguiti gli accessi ad un file; è ad esempio molto comune per i
database effettuare accessi ai dati in maniera pressoché casuale, mentre un riproduttore audio o
video eseguirà per lo più letture sequenziali.
Per migliorare le prestazioni a seconda di queste modalità di accesso è disponibile una appo-
sita funzione, madvise,123 che consente di fornire al kernel delle indicazioni su dette modalità,
cosı̀ che possano essere adottate le opportune strategie di ottimizzazione. Il suo prototipo è:
#include <sys/mman.h>
int madvise(void *start, size_t length, int advice)
Fornisce indicazioni sull’uso previsto di un memory mapping.
La funzione restituisce 0 in caso di successo e −1 in caso di errore, nel qual caso errno assumerà
uno dei valori:
EBADF la mappatura esiste ma non corrisponde ad un file.
EINVAL start non è allineato alla dimensione di una pagina, length ha un valore negativo,
o advice non è un valore valido, o si è richiesto il rilascio (con MADV_DONTNEED) di
pagine bloccate o condivise.
EIO la paginazione richiesta eccederebbe i limiti (vedi sez. 8.3.2) sulle pagine residenti in
memoria del processo (solo in caso di MADV_WILLNEED).
ENOMEM gli indirizzi specificati non sono mappati, o, in caso MADV_WILLNEED, non c’è sufficiente
memoria per soddisfare la richiesta.
ed inoltre EAGAIN e ENOSYS.
La sezione di memoria sulla quale si intendono fornire le indicazioni deve essere indicata con
l’indirizzo iniziale start e l’estensione length, il valore di start deve essere allineato, mentre
122
questo può essere utile per il linker dinamico, in particolare quando viene effettuato il prelink delle applicazioni.
123
tratteremo in sez. 12.4.4 le funzioni che consentono di ottimizzare l’accesso ai file con l’interfaccia classica.
464 CAPITOLO 12. LA GESTIONE AVANZATA DEI FILE
length deve essere un numero positivo.124 L’indicazione viene espressa dall’argomento advice
che deve essere specificato con uno dei valori125 riportati in tab. 12.18.
Valore Significato
MADV_NORMAL nessuna indicazione specifica, questo è il valore di default usato quando
non si è chiamato madvise.
MADV_RANDOM ci si aspetta un accesso casuale all’area indicata, pertanto l’applicazio-
ne di una lettura anticipata con il meccanismo del read-ahead (vedi
sez. 12.4.4) è di scarsa utilità e verrà disabilitata.
MADV_SEQUENTIAL ci si aspetta un accesso sequenziale al file, quindi da una parte sarà op-
portuno eseguire una lettura anticipata, e dall’altra si potranno scartare
immediatamente le pagine una volta che queste siano state lette.
MADV_WILLNEED ci si aspetta un accesso nell’immediato futuro, pertanto l’applicazione
del read-ahead deve essere incentivata.
MADV_DONTNEED non ci si aspetta nessun accesso nell’immediato futuro, pertanto le pa-
gine possono essere liberate dal kernel non appena necessario; l’area di
memoria resterà accessibile, ma un accesso richiederà che i dati vengano
ricaricati dal file a cui la mappatura fa riferimento.
MADV_REMOVE libera un intervallo di pagine di memoria ed il relativo supporto
sottostante; è supportato soltanto sui filesystem in RAM tmpfs e
shmfs.126
MADV_DONTFORK impedisce che l’intervallo specificato venga ereditato dal processo figlio
dopo una fork; questo consente di evitare che il meccanismo del copy on
write effettui la rilocazione delle pagine quando il padre scrive sull’area
di memoria dopo la fork, cosa che può causare problemi per l’hardware
che esegue operazioni in DMA su quelle pagine.
MADV_DOFORK rimuove l’effetto della precedente MADV_DONTFORK.
MADV_MERGEABLE marca la pagina come accorpabile (indicazione principalmente ad uso
dei sistemi di virtualizzazione).127
La funzione non ha, tranne il caso di MADV_DONTFORK, nessun effetto sul comportamento di un
programma, ma può influenzarne le prestazioni fornendo al kernel indicazioni sulle esigenze dello
stesso, cosı̀ che sia possibile scegliere le opportune strategie per la gestione del read-ahead e del
caching dei dati. A differenza da quanto specificato nello standard POSIX.1b, per il quale l’uso
di madvise è a scopo puramente indicativo, Linux considera queste richieste come imperative,
per cui ritorna un errore qualora non possa soddisfarle.128
su un file. Benché l’operazione sia facilmente eseguibile attraverso una serie multipla di chiamate
a read e write, ci sono casi in cui si vuole poter contare sulla atomicità delle operazioni.
Per questo motivo fino da BSD 4.2 vennero introdotte delle nuove system call che permet-
tessero di effettuare con una sola chiamata una serie di letture o scritture su una serie di buffer,
con quello che viene normalmente chiamato I/O vettorizzato. Queste funzioni sono readv e
writev,129 ed 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:
EINVAL si è specificato un valore non valido per uno degli argomenti (ad esempio count è
maggiore di IOV_MAX).
EINTR la funzione è stata interrotta da un segnale prima di di avere eseguito una qualunque
lettura o scrittura.
EAGAIN fd è stato aperto in modalità non bloccante e non ci sono dati in lettura.
EOPNOTSUPP la coda delle richieste è momentaneamente piena.
ed anche EISDIR, EBADF, ENOMEM, EFAULT (se non sono stati allocati correttamente i buffer specificati
nei campi iov_base), più gli eventuali errori delle funzioni di lettura e scrittura eseguite su fd.
Entrambe le funzioni usano una struttura iovec, la cui definizione è riportata in fig. 12.17,
che definisce dove i dati devono essere letti o scritti ed in che quantità. Il primo campo della
struttura, iov_base, contiene l’indirizzo del buffer ed il secondo, iov_len, la dimensione dello
stesso.
struct iovec {
void * iov_base ; /* Starting address */
size_t iov_len ; /* Length in bytes */
};
La lista dei buffer da utilizzare viene indicata attraverso l’argomento vector che è un vettore
di strutture iovec, la cui lunghezza è specificata dall’argomento count.130 Ciascuna struttura
dovrà essere inizializzata opportunamente per indicare i vari buffer da e verso i quali verrà
eseguito il trasferimento dei dati. Essi verranno letti (o scritti) nell’ordine in cui li si sono
specificati nel vettore vector.
La standardizzazione delle due funzioni all’interno 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, definita 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 l’argomento _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
un wrapper per le system call che si accorge se una operazione supererà il precedente limite, in
tal caso i dati verranno letti o scritti con le usuali read e write usando un buffer di dimensioni
sufficienti appositamente allocato e sufficiente a contenere tutti i dati indicati da vector. L’o-
129
in Linux le due funzioni sono riprese da BSD4.4, esse sono previste anche dallo standard POSIX.1-2001.
130
fino alle libc5, Linux usava size_t come tipo dell’argomento count, una scelta logica, che però è stata dismessa
per restare aderenti allo standard POSIX.1-2001.
466 CAPITOLO 12. LA GESTIONE AVANZATA DEI FILE
#include <sys/uio.h>
int preadv(int fd, const struct iovec *vector, int count, off_t offset)
int pwritev(int fd, const struct iovec *vector, int count, off_t offset)
Eseguono una lettura o una scrittura vettorizzata a partire da una data posizione sul file.
Le funzioni hanno gli stessi valori di ritorno delle corrispondenti readv e writev; anche gli eventuali
errori sono gli stessi già visti in precedenza, ma ad essi si possono aggiungere per errno anche i
valori:
EOVERFLOW offset ha un valore che non può essere usato come off_t.
ESPIPE fd è associato ad un socket o una pipe.
Le due funzioni eseguono rispettivamente una lettura o una scrittura vettorizzata a partire
dalla posizione offset sul file indicato da fd, la posizione corrente sul file, come vista da eventuali
altri processi che vi facciano riferimento, non viene alterata. A parte la presenza dell’ulteriore
argomento il comportamento delle funzioni è identico alle precedenti readv e writev.
Con l’uso di queste funzioni si possono evitare eventuali race condition quando si deve ese-
guire la una operazione di lettura e scrittura vettorizzata a partire da una certa posizione su
un file, mentre al contempo si possono avere in concorrenza processi che utilizzano lo stesso file
descriptor (si ricordi quanto visto in sez. 6.3) con delle chiamate a lseek.
sente né in POSIX.1-2001 né in altri standard,134 per cui per essa vengono utilizzati prototipi e
semantiche differenti; nel caso di Linux il prototipo di sendfile è:
#include <sys/sendfile.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count)
Copia dei dati da un file 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:
EAGAIN si è impostata la modalità non bloccante su out_fd e la scrittura si bloccherebbe.
EINVAL i file descriptor non sono validi, o sono bloccati (vedi sez. 12.1), o mmap non è disponibile
per in_fd.
EIO si è avuto un errore di lettura da in_fd.
ENOMEM non c’è memoria sufficiente per la lettura da in_fd.
ed inoltre EBADF e EFAULT.
La funzione copia direttamente count byte dal file descriptor in_fd al file descriptor out_fd;
in caso di successo funzione ritorna il numero di byte effettivamente 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
richiesto con count.
Se il puntatore offset è nullo la funzione legge i dati a partire dalla posizione corrente
su in_fd, altrimenti verrà usata la posizione indicata dal valore puntato da offset; in questo
caso detto valore sarà aggiornato, come value result argument, per indicare la posizione del byte
successivo all’ultimo che è stato letto, mentre la posizione corrente sul file non sarà modificata.
Se invece offset è nullo la posizione corrente sul file sarà aggiornata tenendo conto dei byte
letti da in_fd.
Fino ai kernel della serie 2.4 la funzione è utilizzabile su un qualunque file descriptor, e
permette di sostituire la invocazione successiva di una read e una write (e l’allocazione del
relativo buffer) con una sola chiamata a sendfile. In questo modo si può diminuire il numero di
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 file su disco
ad un socket di rete,135 dato che in questo caso diventa possibile effettuare il trasferimento
diretto via DMA dal controller del disco alla scheda di rete, senza neanche allocare un buffer nel
kernel,136 ottenendo la massima efficienza possibile senza pesare neanche sul processore.
In seguito però ci si è accorti che, fatta eccezione per il trasferimento diretto da file a socket,
non sempre sendfile comportava miglioramenti significativi delle prestazioni rispetto all’uso in
sequenza di read e write,137 e che anzi in certi casi si potevano avere anche dei peggioramenti.
Questo ha portato, per i kernel della serie 2.6,138 alla decisione di consentire l’uso della funzione
soltanto quando il file 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 l’uso di sendfile darà
luogo ad un errore di EINVAL.
Nonostante ci possano essere casi in cui sendfile non migliora le prestazioni, resta il dubbio
se la scelta di disabilitarla sempre per il trasferimento fra file di dati sia davvero corretta. Se
134
pertanto si eviti di utilizzarla se si devono scrivere programmi portabili.
135
questo è il caso classico del lavoro eseguito da un server web, ed infatti Apache ha una opzione per il supporto
esplicito di questa funzione.
136
il meccanismo è detto zerocopy in quanto i dati non vengono mai copiati dal kernel, che si limita a programmare
solo le operazioni di lettura e scrittura via DMA.
137
nel caso generico infatti il kernel deve comunque allocare un buffer ed effettuare 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.
138
per alcune motivazioni di questa scelta si può fare riferimento a quanto illustrato da Linus Torvalds in
http://www.cs.helsinki.fi/linux/linux-kernel/2001-03/0200.html.
468 CAPITOLO 12. LA GESTIONE AVANZATA DEI FILE
ci sono peggioramenti di prestazioni infatti si può sempre fare ricorso al metodo ordinario, ma
lasciare a disposizione la funzione consentirebbe se non altro di semplificare la gestione della
copia dei dati fra file, evitando di dover gestire l’allocazione di un buffer temporaneo per il loro
trasferimento.
Questo dubbio si può comunque ritenere superato con l’introduzione, avvenuta a partire dal
kernel 2.6.17, della nuova system call splice. Lo scopo di questa funzione è quello di fornire
un meccanismo generico per il trasferimento di dati da o verso un file utilizzando un buffer
gestito internamente dal kernel. Descritta in questi termini splice sembra semplicemente un
“dimezzamento” di sendfile.139 In realtà le due system call sono profondamente diverse nel
loro meccanismo di funzionamento;140 sendfile infatti, come accennato, non necessita di avere
a disposizione un buffer interno, perché esegue un trasferimento diretto di dati; questo la rende
in generale più efficiente, ma anche limitata nelle sue applicazioni, dato che questo tipo di
trasferimento è possibile solo in casi specifici.141
Il concetto che sta dietro a splice invece è diverso,142 si tratta semplicemente di una funzione
che consente di fare in maniera del tutto generica delle operazioni di trasferimento di dati fra
un file e un buffer gestito interamente in kernel space. In questo caso il cuore della funzione (e
delle affini vmsplice e tee, che tratteremo più avanti) è appunto l’uso di un buffer in kernel
space, e questo è anche quello che ne ha semplificato l’adozione, perché l’infrastruttura per la
gestione di un tale buffer è presente fin dagli albori di Unix per la realizzazione delle pipe (vedi
sez. 11.1). Dal punto di vista concettuale allora splice non è altro che una diversa interfaccia
(rispetto alle pipe) con cui utilizzare in user space l’oggetto “buffer in kernel space”.
Cosı̀ se per una pipe o una fifo il buffer viene utilizzato come area di memoria (vedi fig. 11.1)
dove appoggiare i dati che vengono trasferiti da un capo all’altro della stessa per creare un
meccanismo di comunicazione fra processi, nel caso di splice il buffer viene usato o come fonte
dei dati che saranno scritti su un file, o come destinazione dei dati che vengono letti da un file.
La funzione splice fornisce quindi una interfaccia generica che consente di trasferire dati da
un buffer ad un file o viceversa; il suo prototipo, accessibile solo dopo aver definito la macro
_GNU_SOURCE,143 è il seguente:
#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 file 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:
EBADF uno o entrambi fra fd_in e fd_out non sono file descriptor validi o, rispettivamente,
non sono stati aperti in lettura o scrittura.
EINVAL il filesystem su cui si opera non supporta splice, oppure nessuno dei file descriptor è
una pipe, oppure si è dato un valore a off_in o off_out ma il corrispondente file è
un dispositivo che non supporta la funzione seek.
ENOMEM non c’è memoria sufficiente per l’operazione richiesta.
ESPIPE o off_in o off_out non sono NULL ma il corrispondente file descriptor è una pipe.
139
nel senso che un trasferimento di dati fra due file con sendfile non sarebbe altro che la lettura degli stessi su
un buffer seguita dalla relativa scrittura, cosa che in questo caso si dovrebbe eseguire con due chiamate a splice.
140
questo fino al kernel 2.6.23, dove sendfile è stata reimplementata in termini di splice, pur mantenendo
disponibile la stessa interfaccia verso l’user space.
141
e nel caso di Linux questi sono anche solo quelli in cui essa può essere effettivamente utilizzata.
142
in realtà la proposta originale di Larry Mc Voy non differisce poi tanto negli scopi da sendfile, quel-
lo che rende splice davvero diversa è stata la reinterpretazione che ne è stata fatta nell’implementazione
su Linux realizzata da Jens Anxboe, concetti che sono esposti sinteticamente dallo stesso Linus Torvalds in
http://kerneltrap.org/node/6505.
143
si ricordi che questa funzione non è contemplata da nessuno standard, è presente solo su Linux, e pertanto
deve essere evitata se si vogliono scrivere programmi portabili.
12.4. ALTRE MODALITÀ DI I/O AVANZATO 469
La funzione esegue un trasferimento di len byte dal file descriptor fd_in al file descriptor
fd_out, uno dei quali deve essere una pipe; l’altro file descriptor può essere qualunque.144 Come
accennato una pipe non è altro che un buffer in kernel space, per cui a seconda che essa sia usata
per fd_in o fd_out si avrà rispettivamente la copia dei dati dal buffer al file o viceversa.
In caso di successo la funzione ritorna il numero di byte trasferiti, che può essere, come per
le normali funzioni di lettura e scrittura su file, inferiore a quelli richiesti; un valore negativo
indicherà un errore mentre un valore nullo indicherà che non ci sono dati da trasferire (ad
esempio si è giunti alla fine del file in lettura). Si tenga presente che, a seconda del verso del
trasferimento dei dati, la funzione si comporta nei confronti del file descriptor che fa riferimento
al file ordinario, come read o write, e pertanto potrà anche bloccarsi (a meno che non si sia
aperto il suddetto file in modalità non bloccante).
I due argomenti off_in e off_out consentono di specificare, come per l’analogo offset di
sendfile, la posizione all’interno del file da cui partire per il trasferimento dei dati. Come per
sendfile un valore nullo indica di usare la posizione corrente sul file, ed essa sarà aggiornata
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
ritorno della funzione, al byte successivo all’ultimo byte trasferito. Ovviamente soltanto uno di
questi due argomenti, e più precisamente quello che fa riferimento al file descriptor non associato
alla pipe, può essere specificato come valore non nullo.
Infine l’argomento flags consente di controllare alcune caratteristiche del funzionamento
della funzione; il contenuto è una maschera binaria e deve essere specificato come OR aritmetico
dei valori riportati in tab. 12.19. 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 effetto su splice.
Valore Significato
SPLICE_F_MOVE Suggerisce al kernel di spostare le pagine di memoria contenenti i dati
invece di copiarle;145 viene usato soltanto da splice.
SPLICE_F_NONBLOCK Richiede di operare in modalità non bloccante; questo flag influisce
solo sulle operazioni che riguardano l’I/O da e verso la pipe. Nel caso
di splice questo significa che la funzione potrà comunque bloccarsi
nell’accesso agli altri file descriptor (a meno che anch’essi non siano
stati aperti in modalità non bloccante).
SPLICE_F_MORE Indica al kernel che ci sarà l’invio di ulteriori dati in una splice suc-
cessiva, questo è un suggerimento utile che viene usato quando fd_out
è un socket.146 Attualmente viene usato solo da splice, potrà essere
implementato in futuro anche per vmsplice e tee.
SPLICE_F_GIFT Le pagine di memoria utente sono “donate” al kernel;147 se impostato
una seguente splice che usa SPLICE_F_MOVE potrà spostare le pagine
con successo, altrimenti esse dovranno essere copiate; per usare que-
sta opzione i dati dovranno essere opportunamente allineati in posizio-
ne ed in dimensione alle pagine di memoria. Viene usato soltanto da
vmsplice.
Tabella 12.19: Le costanti che identificano i bit della maschera binaria dell’argomento flags di splice, vmsplice
e tee.
144
questo significa che può essere, oltre che un file di dati, anche un altra pipe, o un socket.
120
per una maggiore efficienza 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
buffer non corrisponda a pagine intere esse saranno comunque copiate.
121
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.
147
questo significa che la cache delle pagine e i dati su disco potranno differire, e che l’applicazione non potrà
modificare quest’area di memoria.
470 CAPITOLO 12. LA GESTIONE AVANZATA DEI FILE
Per capire meglio il funzionamento di splice vediamo un esempio con un semplice program-
ma che usa questa funzione per effettuare la copia di un file su un altro senza utilizzare buffer in
user space. Il programma si chiama splicecp.c ed il codice completo è disponibile coi sorgenti
allegati alla guida, il corpo principale del programma, che non contiene la sezione di gestione
delle opzioni e le funzioni di ausilio è riportato in fig. 12.19.
Lo scopo del programma è quello di eseguire la copia dei con splice, questo significa che si
dovrà usare la funzione due volte, prima per leggere i dati e poi per scriverli, appoggiandosi ad
un buffer in kernel space (vale a dire ad una pipe); lo schema del flusso dei dati è illustrato in
fig. 12.18.
Figura 12.18: Struttura del flusso di dati usato dal programma splicecp.
Una volta trattate le opzioni il programma verifica che restino (13-16) i due argomenti che
indicano il file sorgente ed il file destinazione. Il passo successivo è aprire il file sorgente (18-22),
quello di destinazione (23-27) ed infine (28-31) la pipe che verrà usata come buffer.
Il ciclo principale (33-58) inizia con la lettura dal file sorgente tramite la prima splice (34-
35), in questo caso si è usato come primo argomento il file descriptor del file sorgente e come
terzo quello del capo in scrittura della pipe (il funzionamento delle pipe e l’uso della coppia di
file descriptor ad esse associati è trattato in dettaglio in sez. 11.1; non ne parleremo qui dato
che nell’ottica dell’uso di splice questa operazione corrisponde semplicemente al trasferimento
dei dati dal file al buffer).
La lettura viene eseguita in blocchi pari alla dimensione specificata dall’opzione -s (il default
è 4096); essendo in questo caso splice equivalente ad una read sul file, se ne controlla il valore di
uscita in nread che indica quanti byte sono stati letti, se detto valore è nullo (36) questo significa
che si è giunti alla fine del file sorgente e pertanto l’operazione di copia è conclusa e si può uscire
dal ciclo arrivando alla conclusione del programma (59). In caso di valore negativo (37-44) c’è
stato un errore ed allora si ripete la lettura (36) se questo è dovuto ad una interruzione, o
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. 11.1) mentre il terzo sia il file descriptor del
file di destinazione.
Di nuovo si controlla il numero di byte effettivamente 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
messaggio negli altri casi (48-55). Infine si chiude il ciclo di scrittura sottraendo (57) il numero
12.4. ALTRE MODALITÀ DI I/O AVANZATO 471
1 # define _GNU_SOURCE
2 # include < fcntl .h > /* file control functions */
3 ...
4
5 int main ( int argc , char * argv [])
6 {
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 12.19: Esempio di codice che usa splice per effettuare la copia di un file.
472 CAPITOLO 12. LA GESTIONE AVANZATA DEI FILE
di byte scritti a quelli di cui è richiesta la scrittura,148 cosı̀ che il ciclo di scrittura venga ripetuto
fintanto che il valore risultante sia maggiore di zero, indice che la chiamata a splice non ha
esaurito tutti i dati presenti sul buffer.
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 buffer in user
space per eseguire la copia dei dati, solo che in questo caso non è stato necessario allocare nessun
buffer e non si è trasferito nessun dato in user space.
Si noti anche come si sia usata la combinazione SPLICE_F_MOVE | SPLICE_F_MORE per
l’argomento flags di splice, infatti anche se un valore nullo avrebbe dato gli stessi risultati,
l’uso di questi flag, che si ricordi servono solo a dare suggerimenti al kernel, permette in genere
di migliorare le prestazioni.
Come accennato con l’introduzione di splice sono state realizzate anche 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 buffer in kernel space; benché queste non
attengono strettamente ad operazioni di trasferimento dati fra file descriptor, le tratteremo qui,
essendo strettamente correlate fra loro.
La prima funzione, vmsplice, è la più simile a splice e come indica il suo nome consente
di trasferire i dati dalla memoria virtuale di un processo (ad esempio per un file mappato in
memoria) verso una pipe; il suo prototipo è:
#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:
EBADF o fd non è un file descriptor valido o non fa riferimento ad una pipe.
EINVAL si è usato un valore nullo per nr_segs oppure si è usato SPLICE_F_GIFT ma la memoria
non è allineata.
ENOMEM non c’è memoria sufficiente per l’operazione richiesta.
La pipe indicata da fd dovrà essere specificata tramite il file descriptor corrispondente al suo
capo aperto in scrittura (di nuovo si faccia riferimento a sez. 11.1), mentre per indicare quali
segmenti della memoria del processo devono essere trasferiti verso di essa si dovrà utilizzare un
vettore di strutture iovec (vedi fig. 12.17), esattamente con gli stessi criteri con cui le si usano
per l’I/O vettorizzato, indicando gli indirizzi e le dimensioni di ciascun segmento di memoria
su cui si vuole operare; le dimensioni del suddetto vettore devono essere passate nell’argomento
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. 12.4.2.
In caso di successo la funzione ritorna il numero di byte trasferiti sulla pipe. In genera-
le, se i dati una volta creati non devono essere riutilizzati (se cioè l’applicazione che chiama
vmsplice non modificherà più la memoria trasferita), è opportuno utilizzare per flag il valore
SPLICE_F_GIFT; questo fa sı̀ che il kernel possa rimuovere le relative pagine dalla cache della
memoria virtuale, cosı̀ che queste possono essere utilizzate immediatamente senza necessità di
eseguire una copia dei dati che contengono.
La seconda funzione aggiunta insieme a splice è tee, che deve il suo nome all’omonimo
comando in user space, perché in analogia con questo permette di duplicare i dati in ingresso
su una pipe su un’altra pipe. In sostanza, sempre nell’ottica della manipolazione dei dati su dei
148
in questa parte del ciclo nread, il cui valore iniziale è dato dai byte letti dalla precedente chiamata a splice,
viene ad assumere il significato di byte da scrivere.
12.4. ALTRE MODALITÀ DI I/O AVANZATO 473
buffer in kernel space, la funzione consente di eseguire una copia del contenuto del buffer stesso.
Il prototipo di tee è il seguente:
#include <fcntl.h>
long tee(int fd_in, int fd_out, size_t len, unsigned int flags)
Duplica len byte da una pipe ad un’altra.
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:
EINVAL o uno fra fd_in e fd_out non fa riferimento ad una pipe o entrambi fanno riferimento
alla stessa pipe.
ENOMEM non c’è memoria sufficiente per l’operazione richiesta.
La funzione copia len byte del contenuto di una pipe su di un’altra; fd_in deve essere
il capo in lettura della pipe sorgente e fd_out il capo in scrittura della pipe destinazione; a
differenza 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. 11.1). Al momento149 il solo valore utilizzabile per flag, fra quelli elencati in
tab. 12.19, è SPLICE_F_NONBLOCK che rende la funzione non bloccante.
La funzione restituisce il numero di byte copiati da una pipe all’altra (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.150 Un esempio di realizzazione del comando tee usando questa funzione,
ripreso da quello fornito nella pagina di manuale e dall’esempio allegato al patch originale, è
riportato in fig. 12.20. Il programma consente di copiare il contenuto dello standard input sullo
standard output e su un file specificato come argomento, il codice completo si trova nel file 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 file su cui scrivere), di aprirlo (15–19) 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
nell’esempio di fig. 12.19 si controlla il valore di ritorno della funzione in len; se questo è nullo
significa che non ci sono più dati da leggere e si chiude il ciclo (40), se è negativo c’è stato un
errore, ed allora si ripete la chiamata se questo è dovuto ad una interruzione (42-44) o si stampa
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 stan-
dard input e scrivere sul file, di nuovo su usa un ciclo di scrittura (50-58) in cui si ripete una
chiamata a splice (51) fintanto che non si sono scritti tutti i len byte copiati in precedenza
con tee (il funzionamento è identico all’analogo ciclo di scrittura del precedente esempio di
fig. 12.19).
Infine una nota finale riguardo splice, vmsplice e tee: occorre sottolineare che benché
finora si sia parlato di trasferimenti o copie di dati in realtà nella implementazione di queste
system call non è affatto detto che i dati vengono effettivamente spostati o copiati, il kernel infatti
realizza le pipe come un insieme di puntatori151 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 significa che
anche con tee non viene mai copiato nessun byte, vengono semplicemente copiati i puntatori.
149
quello della stesura di questo paragrafo, avvenuta il Gennaio 2010, in futuro potrebbe essere implementato
anche SPLICE_F_MORE.
150
si tenga presente però che questo non avviene se si è impostato il flag SPLICE_F_NONBLOCK, in tal caso infatti
si avrebbe un errore di EAGAIN.
151
per essere precisi si tratta di un semplice buffer circolare, un buon articolo sul tema si trova su
http://lwn.net/Articles/118750/.
474 CAPITOLO 12. LA GESTIONE AVANZATA DEI FILE
1 # define _GNU_SOURCE
2 # 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 }
Figura 12.20: Esempio di codice che usa tee per copiare i dati dello standard input sullo standard output e su
un file.
12.4. ALTRE MODALITÀ DI I/O AVANZATO 475
#include <fcntl.h>
ssize_t readahead(int fd, off64_t *offset, size_t count)
Esegue una lettura preventiva del contenuto di un file in cache.
La funzione restituisce 0 in caso di successo e −1 in caso di errore, nel qual caso errno assumerà
uno dei valori:
EBADF l’argomento fd non è un file descriptor valido o non è aperto in lettura.
EINVAL l’argomento fd si riferisce ad un tipo di file che non supporta l’operazione (come una
pipe o un socket).
La funzione richiede che venga letto in anticipo il contenuto del file 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 vengono
arrotondati di conseguenza.
La funzione estende quello che è un comportamento normale del kernel che quando si legge
un file, aspettandosi che l’accesso prosegua, esegue sempre una lettura preventiva di una certa
quantità di dati; questo meccanismo di lettura anticipata viene chiamato read-ahead, da cui
deriva il nome della funzione. La funzione readahead, per ottimizzare gli accessi a disco, effettua
la lettura in cache della sezione richiesta e si blocca fintanto che questa non viene completata.
La posizione corrente sul file non viene modificata ed indipendentemente da quanto indicato con
count la lettura dei dati si interrompe una volta raggiunta la fine del file.
Si può utilizzare questa funzione per velocizzare le operazioni di lettura all’interno di un pro-
gramma tutte le volte che si conosce in anticipo quanti dati saranno necessari nelle elaborazioni
successive. Si potrà cosı̀ concentrare in un unico momento (ad esempio in fase di inizializzazione)
la lettura dei dati da disco, cosı̀ da ottenere una migliore velocità di risposta nelle operazioni
successive.
Il concetto di readahead viene generalizzato nello standard POSIX.1-2001 dalla funzione
posix_fadvise,153 che consente di “avvisare” il kernel sulle modalità con cui si intende accedere
nel futuro ad una certa porzione di un file,154 cosı̀ che esso possa provvedere le opportune
152
questa è una funzione specifica di Linux, introdotta con il kernel 2.4.13, e non deve essere usata se si vogliono
scrivere programmi portabili.
153
anche se l’argomento len è stato modificato da size_t a off_t nella revisione POSIX.1-2003 TC5.
154
la funzione però è stata introdotta su Linux solo a partire dal kernel 2.5.60.
476 CAPITOLO 12. LA GESTIONE AVANZATA DEI FILE
La funzione restituisce 0 in caso di successo e −1 in caso di errore, nel qual caso errno assumerà
uno dei valori:
EBADF l’argomento fd non è un file descriptor valido.
EINVAL il valore di advice non è valido o fd si riferisce ad un tipo di file che non supporta
l’operazione (come una pipe o un socket).
ESPIPE previsto dallo standard se fd è una pipe o un socket (ma su Linux viene restituito
EINVAL).
La funzione dichiara al kernel le modalità con cui intende accedere alla regione del file indicato
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 fine del file.155 Le modalità sono indicate dall’argomento
advice che è una maschera binaria dei valori illustrati in tab. 12.20, che riprendono il significato
degli analoghi già visti in sez. 12.4.1 per madvise.156 Si tenga presente comunque che la funzione
dà soltanto un avvertimento, non esiste nessun vincolo per il kernel, che utilizza semplicemente
l’informazione.
Valore Significato
POSIX_FADV_NORMAL Non ci sono avvisi specifici da fare riguardo le modalità di accesso,
il comportamento sarà identico a quello che si avrebbe senza nessun
avviso.
POSIX_FADV_SEQUENTIAL L’applicazione si aspetta di accedere di accedere ai dati specificati in
maniera sequenziale, a partire dalle posizioni più basse.
POSIX_FADV_RANDOM I dati saranno letti in maniera completamente causale.
POSIX_FADV_NOREUSE I dati saranno acceduti una sola volta.
POSIX_FADV_WILLNEED I dati saranno acceduti a breve.
POSIX_FADV_DONTNEED I dati non saranno acceduti a breve.
Tabella 12.20: Valori delle costanti usabili per l’argomento advice di posix_fadvise, che indicano la modalità
con cui si intende accedere ad un file.
alleggerire il carico sulla cache, ed un programma può utilizzare periodicamente questa funzione
per liberare pagine di memoria da dati che non sono più utilizzati per far posto a nuovi dati
utili.157
Sia posix_fadvise che readahead attengono alla ottimizzazione dell’accesso in lettura; lo
standard POSIX.1-2001 prevede anche una funzione specifica per le operazioni di scrittura,
posix_fallocate,158 che consente di preallocare dello spazio disco per assicurarsi che una se-
guente scrittura non fallisca, il suo prototipo, anch’esso disponibile solo se si definisce la macro
_XOPEN_SOURCE ad almeno 600, è:
#include <fcntl.h>
int posix_fallocate(int fd, off_t offset, off_t len)
Richiede la allocazione di spazio disco per un file.
La funzione assicura che venga allocato sufficiente spazio disco perché sia possibile scrivere
sul file indicato dall’argomento fd nella regione che inizia dalla posizione offset e si estende per
len byte; se questa regione si estende oltre la fine del file le dimensioni di quest’ultimo saranno
incrementate di conseguenza. Dopo aver eseguito con successo la funzione è garantito che una
successiva scrittura nella regione indicata non fallirà per mancanza di spazio disco. La funzione
non ha nessun effetto né sul contenuto, né sulla posizione corrente del file.
Ci si può chiedere a cosa possa servire una funzione come posix_fallocate dato che è
sempre possibile ottenere l’effetto voluto eseguendo esplicitamente sul file la scrittura159 di una
serie di zeri per l’estensione di spazio necessaria qualora il file debba essere esteso o abbia dei
buchi.160 In realtà questa è la modalità con cui la funzione veniva realizzata nella prima versione
fornita dalle glibc, per cui la funzione costituiva in sostanza soltanto una standardizzazione delle
modalità di esecuzione di questo tipo di allocazioni.
Questo metodo, anche se funzionante, comporta però l’effettiva esecuzione una scrittura su
tutto lo spazio disco necessario, da fare al momento della richiesta di allocazione, pagandone
il conseguente prezzo in termini di prestazioni; il tutto quando in realtà servirebbe solo poter
riservare lo spazio per poi andarci a scrivere, una sola volta, quando il contenuto finale diventa
effettivamente disponibile.
Per poter fare tutto questo è però necessario il supporto da parte del kernel, e questo è
divenuto disponibile solo a partire dal kernel 2.6.23 in cui è stata introdotta la nuova system call
fallocate,161 che consente di realizzare direttamente all’interno del kernel l’allocazione dello
157
la pagina di manuale riporta l’esempio dello streaming di file di grosse dimensioni, dove le pagine occupate
dai dati già inviati possono essere tranquillamente scartate.
158
la funzione è stata introdotta a partire dalle glibc 2.1.94.
159
usando pwrite per evitare spostamenti della posizione corrente sul file.
160
si ricordi che occorre scrivere per avere l’allocazione e che l’uso di truncate per estendere un file creerebbe
soltanto uno sparse file (vedi sez. 6.2.3) senza una effettiva allocazione dello spazio disco.
161
non è detto che la funzione sia disponibile per tutti i filesystem, ad esempio per XFS il supporto è stato
introdotto solo a partire dal kernel 2.6.25.
478 CAPITOLO 12. LA GESTIONE AVANZATA DEI FILE
spazio disco cosı̀ da poter realizzare una versione di posix_fallocate con prestazioni molto più
elevate.162
Trattandosi di una funzione di servizio, ed ovviamente disponibile esclusivamente su Linux,
inizialmente fallocate non era stata definita come funzione di libreria,163 ma a partire dalle
glibc 2.10 è stato fornito un supporto esplicito; il suo prototipo è:
#include <linux/fcntl.h>
int fallocate(int fd, int mode, off_t offset, off_t len)
Prealloca dello spazio disco per un file.
La funzione ritorna 0 in caso di successo e −1 in caso di errore, nel qual caso errno può assumere
i valori:
EBADF fd non fa riferimento ad un file descriptor valido aperto in scrittura.
EFBIG la somma di offset e len eccede le dimensioni massime di un file.
EINVAL offset è minore di zero o len è minore o uguale a zero.
ENODEV fd non fa riferimento ad un file ordinario o a una directory.
ENOSPC non c’è spazio disco sufficiente per l’operazione.
ENOSYS il filesystem contenente il file associato a fd non supporta fallocate.
EOPNOTSUPP il filesystem contenente il file associato a fd non supporta l’operazione mode.
ed inoltre EINTR, EIO.
La funzione prende gli stessi argomenti di posix_fallocate con lo stesso significato, a cui si
aggiunge l’argomento mode che indica le modalità di allocazione; al momento quest’ultimo può
soltanto essere nullo o assumere il valore FALLOC_FL_KEEP_SIZE che richiede che la dimensione
del file164 non venga modificata anche quando la somma di offset e len eccede la dimensione
corrente.
Se mode è nullo invece la dimensione totale del file in caso di estensione dello stesso viene
aggiornata, come richiesto per posix_fallocate, ed invocata in questo modo si può considerare
fallocate come l’implementazione ottimale di posix_fallocate a livello di kernel.
162
nelle glibc la nuova system call viene sfruttata per la realizzazione di posix_fallocate a partire dalla versione
2.10.
163
pertanto poteva essere invocata soltanto in maniera indiretta con l’ausilio di syscall, vedi sez. 1.1.3, come
long fallocate(int fd, int mode, loff_t offset, loff_t len).
164
quella ottenuta nel campo st_size di una struttura stat dopo una chiamata a fstat.
Capitolo 13
I thread
479
480 CAPITOLO 13. I THREAD
Programmazione di rete
481
Capitolo 14
In questo capitolo sarà fatta un’introduzione ai concetti generali che servono come prerequisiti
per capire la programmazione di rete, non tratteremo quindi aspetti specifici ma faremo una
breve introduzione al modello più comune usato nella programmazione di rete, per poi passare
ad un esame a grandi linee dei protocolli di rete e di come questi sono organizzati e interagiscono.
In particolare, avendo assunto l’ottica di un’introduzione mirata alla programmazione, ci
concentreremo sul protocollo più diffuso, il TCP/IP, che è quello che sta alla base di internet,
avendo cura di sottolineare i concetti più importanti da conoscere per la scrittura dei programmi.
483
484 CAPITOLO 14. INTRODUZIONE ALLA PROGRAMMAZIONE DI RETE
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
meno soggetta a cambiamenti ed evoluzione, e non soffre di problemi di sincronizzazione, e
centralizzando opportunamente i secondi. In questo modo si può distribuire il carico ed accedere
in maniera efficiente i dati.
Livello Nome
Livello 7 Application Applicazione
Livello 6 Presentation Presentazione
Livello 5 Session Sessione
Livello 4 Transport Trasporto
Livello 3 Network Rete
Livello 2 DataLink Collegamento Dati
Livello 1 Physical Connessione Fisica
Il modello ISO/OSI è stato sviluppato in corrispondenza alla definizione della serie di proto-
colli X.25 per la commutazione di pacchetto; come si vede è un modello abbastanza complesso1 ,
tanto che usualmente si tende a suddividerlo in due parti, secondo lo schema mostrato in fig. 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 dall’hardware.
Il modello ISO/OSI mira ad effettuare una classificazione completamente generale di ogni
tipo di protocollo di rete; nel frattempo però era stato sviluppato anche un altro modello, relativo
al protocollo TCP/IP, che è quello su cui è basata internet, che è diventato uno standard de facto.
Questo modello viene talvolta chiamato anche modello DoD (sigla che sta per Department of
Defense), dato che fu sviluppato dall’agenzia ARPA per il Dipartimento della Difesa Americano.
La scelta fra quale dei due modelli utilizzare dipende per lo più dai gusti personali. Come
caratteristiche generali il modello ISO/OSI è più teorico e generico, basato separazioni funzionali,
1
infatti per memorizzarne i vari livelli è stata creata la frase All people seem to need data processing, in cui
ciascuna parola corrisponde all’iniziale di uno dei livelli.
486 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
operativo; useremo pertanto quest’ultimo, anche per la sua maggiore semplicità.2
Come si può notare come il modello TCP/IP è più semplice del modello ISO/OSI ed è strut-
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 l’IP (Internet Protocol ) che copre
il livello 2. Le funzioni dei vari livelli sono le seguenti:
Trasporto Fornisce la comunicazione tra le due stazioni terminali su cui girano gli applica-
tivi, regola il flusso delle informazioni, può fornire un trasporto affidabile, cioè
2
questa semplicità ha un costo quando si fa riferimento agli strati più bassi, che sono in effetti descritti meglio
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
sez. 15.3.6), ai livelli inferiori del protocollo.
14.2. I PROTOCOLLI DI RETE 487
Figura 14.2: Strutturazione del flusso 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 fig. 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:
• Le singole applicazioni comunicano scambiandosi i dati ciascuna secondo un suo specifico
formato. Per applicazioni generiche, come la posta o le pagine web, viene di solito definito
ed implementato quello che viene chiamato un protocollo di applicazione (esempi possono
essere HTTP, POP, SMTP, ecc.), ciascuno dei quali è descritto in un opportuno standard
(di solito attraverso un RFC4 ).
4
l’acronimo RFC sta per Request For Comment ed è la procedura attraverso la quale vengono proposti gli
standard per Internet.
488 CAPITOLO 14. INTRODUZIONE ALLA PROGRAMMAZIONE DI RETE
• I dati delle applicazioni vengono inviati al livello di trasporto usando un’interfaccia op-
portuna (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
informazioni per poter effettuare l’instradamento nella rete ed il recapito alla destinazione
finale. In genere questo è il livello di IP (Internet Protocol), a cui vengono inseriti i numeri
IP che identificano i computer su internet.
• L’ultimo passo è il trasferimento del pacchetto al driver della interfaccia di trasmissione, che
si incarica di incapsularlo nel relativo protocollo di trasmissione. Questo può avvenire sia
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).
una interfaccia di programmazione su questo confine, tanto più che è proprio lı̀ (come evidenziato
in fig. 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 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, con-
centrandoci, per le ragioni appena esposte, sul livello di trasporto. All’interno di quest’ultimo
privilegeremo poi il protocollo TCP, per il ruolo centrale che svolge nella maggior parte delle
applicazioni.
Figura 14.3: Panoramica sui vari protocolli che compongono la suite TCP/IP.
TCP Trasmission Control Protocol. È un protocollo orientato alla connessione che provvede
un trasporto affidabile per un flusso di dati bidirezionale fra due stazioni remote.
Il protocollo ha cura di tutti gli aspetti del trasporto, come l’acknoweledgment, i
timeout, la ritrasmissione, ecc. È usato dalla maggior parte delle applicazioni.
UDP User Datagram Protocol. È un protocollo senza connessione, per l’invio di dati a
pacchetti. Contrariamente al TCP il protocollo non è affidabile e non c’è garanzia
che i pacchetti raggiungano la loro destinazione, si perdano, vengano duplicati, o
abbiano un particolare ordine di arrivo.
ICMP Internet Control Message Protocol. È il protocollo usato a livello 2 per gestire gli errori
e trasportare le informazioni di controllo fra stazioni remote e instradatori (cioè fra
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
da alcuni programmi come ping. A volte ci si riferisce ad esso come ICPMv4 per
distinguerlo da ICMPv6.
IGMP Internet Group Management Protocol. É un protocollo di livello 2 usato per il mul-
ticast (vedi sez. ??). Permette alle stazioni remote di notificare ai router che sup-
portano questa comunicazione a quale gruppo esse appartengono. Come ICMP viene
implementato direttamente sopra IP.
RARP Reverse Address Resolution Protocol. È il protocollo che esegue l’operazione inversa
rispetto ad ARP (da cui il nome) mappando un indirizzo hardware in un indirizzo
IP. Viene usato a volte per durante l’avvio per assegnare un indirizzo IP ad una
macchina.
ICMPv6 Internet Control Message Protocol, version 6. Combina per IPv6 le funzionalità di
ICMPv4, IGMP e ARP.
EGP Exterior Gateway Protocol. È un protocollo di routing usato per comunicare lo stato
fra gateway vicini a livello di sistemi autonomi 5 , con meccanismi che permettono
di identificare i vicini, controllarne la raggiungibilità e scambiare informazioni sullo
stato della rete. Viene implementato direttamente sopra IP.
OSPF Open Shortest Path First. È 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.
SLIP Serial Line over IP. È un protocollo di livello 1 che permette di trasmettere un
pacchetto IP attraverso una linea seriale.
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
che UDP siano basati su IP e sia possibile intervenire a questo livello con i raw socket questa
tecnica è molto meno diffusa e a parte applicazioni particolari si preferisce sempre usare i servizi
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.
• Universal addressing la comunicazione avviene fra due stazioni remote identificate uni-
vocamente con un indirizzo a 32 bit che può appartenere ad una sola interfaccia di
rete.
• Best effort viene assicurato il massimo impegno nella trasmissione, ma non c’è nessuna
garanzia per i livelli superiori né sulla percentuale di successo né sul tempo di consegna
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ò definita una nuova
versione del protocollo, che (saltando un numero) è diventata la versione 6. IPv6 nasce quindi
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
veloce possibile.
I cambiamenti apportati sono comunque notevoli e si possono essere riassunti a grandi linee
nei seguenti punti:
• l’introduzione un nuovo tipo di indirizzamento, l’anycast che si aggiunge agli usuali unicast
e multicast.
492 CAPITOLO 14. INTRODUZIONE ALLA PROGRAMMAZIONE DI RETE
La prima differenza con UDP è che TCP provvede sempre una connessione diretta fra un
client e un server, attraverso la quale essi possono comunicare; per questo il paragone più appro-
priato per questo protocollo è quello del collegamento telefonico, in quanto prima viene stabilita
una connessione fra due i due capi della comunicazione su cui poi effettuare quest’ultima.
Caratteristica fondamentale di TCP è l’affidabilità; quando i dati vengono inviati attraverso
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, fino a che sarà considerata fallita o caduta la connessione (e sarà generato un
errore di timeout); il periodo di tempo dipende dall’implementazione e può variare far i quattro
e i dieci minuti.
Inoltre, per tenere conto delle diverse condizioni in cui può trovarsi la linea di comunicazione,
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 l’ordine dei dati assegnando un numero di sequenza
ad ogni byte che trasmette. Ad esempio se un’applicazione scrive 3000 byte su un socket TCP,
questi potranno essere spezzati dal protocollo in due segmenti (le unità di dati passate da TCP
a IP vengono chiamate segment) di 1500 byte, di cui il primo conterrà il numero di sequenza
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
alla perdita degli acknowlegment, all’arrivo sarà comunque possibile riordinare i dati e scartare
i duplicati.
Il protocollo provvede anche un controllo di flusso (flow control ), cioè specifica sempre al-
l’altro capo della trasmissione quanti dati può ricevere tramite una advertised window (lette-
ralmente “finestra annunciata”), che indica lo spazio disponibile nel buffer di ricezione, cosicché
nella trasmissione non vengano inviati più dati di quelli che possono essere ricevuti.
Questa finestra cambia dinamicamente diminuendo con la ricezione dei dati dal socket ed
aumentando con la lettura di quest’ultimo da parte dell’applicazione, se diventa nulla il buffer
di ricezione è pieno e non verranno accettati altri dati. Si noti che UDP non provvede niente di
tutto ciò per cui nulla impedisce che vengano trasmessi pacchetti ad un ritmo che il ricevente
non può sostenere.
Infine attraverso TCP la trasmissione è sempre bidirezionale (in inglese si dice che è full-
duplex ). È cioè possibile sia trasmettere che ricevere allo stesso tempo, il che comporta che
quanto dicevamo a proposito del controllo di flusso e della gestione della sequenzialità dei dati
viene effettuato per entrambe le direzioni di comunicazione.
• Molte reti fisiche hanno una MTU (Maximum Transfer Unit) che dipende dal protocollo
specifico usato al livello di connessione fisica. Il più comune è quello di ethernet che è pari
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
blocchi più piccoli che possono essere trasmessi attraverso l’interfaccia.
Rete MTU
Hyperlink 65535
Token Ring IBM (16 Mbit/sec) 17914
Token Ring IEEE 802.5 (4 Mbit/sec) 4464
FDDI 4532
Ethernet 1500
X.25 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 è
la lunghezza massima oltre la quale un pacchetto inviato da una stazione ad un’altra verrebbe
senz’altro frammentato. Si tenga conto che non è affatto detto che la path MTU sia la stessa
in entrambe le direzioni, perché l’instradamento può essere diverso nei due sensi, con diverse
tipologie di rete coinvolte.
Una delle differenze fra IPv4 e IPv6 é che per IPv6 la frammentazione può essere eseguita solo
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.
Nell’header di IPv4 è previsto il flag DF che specifica che il pacchetto non deve essere fram-
mentato; un router che riceva un pacchetto le cui dimensioni eccedano quelle dell’MTU della
rete di destinazione genererà un messaggio di errore ICMPv4 di tipo destination unreachable,
fragmentation needed but DF bit set. Dato che i router IPv6 non possono effettuare la fram-
mentazione la ricezione di un pacchetto di dimensione eccessiva per la ritrasmissione genererà
sempre un messaggio di errore ICMPv6 di tipo packet too big.
Dato che il meccanismo di frammentazione e riassemblaggio dei pacchetti comporta ineffi-
cienza, 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 flag
DF di IPv4 e il comportamento normale di IPv6 inviando delle opportune serie di pacchetti (per
i dettagli vedere l’RFC 1191 per IPv4 e l’RFC 1981 per IPv6) fintanto che non si hanno più
errori.
Il TCP usa sempre questo meccanismo, che per le implementazioni di IPv4 è opzionale,
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.
Infine TCP definisce una Maximum Segment Size (da qui in avanti abbreviata in MSS) che
annuncia all’altro capo della connessione la dimensione massima dimensione del segmento di
6
questo accade sia per IPv4 che per IPv6, anche se i pacchetti frammentati sono gestiti con modalità diverse,
IPv4 usa un flag nell’header, IPv6 una opportuna opzione, si veda sez. A.2.
14.3. IL PROTOCOLLO TCP/IP 495
dati che può essere ricevuto, cosı̀ da evitare la frammentazione. Di norma viene impostato alla
dimensione della MTU dell’interfaccia meno la lunghezza delle intestazioni di IP e TCP, in Linux
il default, mantenuto nella costante TCP_MSS è 512.
496 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
ormai da tutti i sistemi operativi.
Dopo una breve panoramica sulle caratteristiche di questa interfaccia vedremo come creare
un socket e come collegarlo allo specifico protocollo di rete che si utilizzerà per la comunicazione.
Per evitare un’introduzione puramente teorica concluderemo il capitolo con un primo esempio
di applicazione.
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. 11.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. 11.1.1) ma, a
differenza di questa e degli altri meccanismi esaminati nel capitolo cap. 11, 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; l’interfaccia è rima-
sta sostanzialmente la stessa, con piccole modifiche, negli anni successivi. Benché siano state
sviluppate interfacce alternative, originate dai sistemi SVr4 come la XTI (X/Open Transport In-
terface) nessuna ha mai raggiunto la diffusione e la popolarità di quella dei socket (né tantomeno
la stessa usabilità e flessibilità).
La flessibilità e la genericità dell’interfaccia inoltre consente di utilizzare i socket con i più
disparati meccanismi di comunicazione, e non solo con l’insieme dei protocolli TCP/IP, anche
se questa sarà comunque quella di cui tratteremo in maniera più estesa.
497
498 CAPITOLO 15. INTRODUZIONE AI SOCKET
modalità di risolvere i problemi) siano diverse a seconda del tipo di protocollo di comunicazione
usato, le funzioni da usare restano le stesse.
Per questo motivo una semplice descrizione dell’interfaccia è assolutamente inutile, in quanto
il comportamento di quest’ultima e le problematiche da affrontare 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
ricevere i dati) e sul comportamento effettivo delle funzioni utilizzate.
La scelta di uno stile dipende sia dai meccanismi disponibili, sia dal tipo di comunicazione
che si vuole effettuare. Ad esempio alcuni stili di comunicazione considerano i dati come una
sequenza continua di byte, in quello che viene chiamato un flusso (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
dati, possa o meno non rispettare l’ordine in cui essi non sono inviati, o inviare dei pacchetti più
volte (come nel caso di TCP e UDP).
Un terzo esempio di stile di comunicazione concerne le modalità in cui essa avviene, in certi
casi essa può essere condotta con una connessione diretta con un solo corrispondente, come per
una telefonata; altri casi possono prevedere una comunicazione come per lettera, in cui si scrive
l’indirizzo 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.
É chiaro che ciascuno di questi stili comporta una modalità diversa di gestire la comunicazio-
ne, ad esempio se è inaffidabile occorrerà essere in grado di gestire la perdita o il rimescolamento
dei dati, se è a pacchetti questi dovranno essere opportunamente trattati, ecc.
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:
EPROTONOSUPPORT il tipo di socket o il protocollo scelto non sono supportati nel dominio.
ENFILE il kernel non ha memoria sufficiente a creare una nuova struttura per il socket.
EMFILE si è ecceduta la tabella dei file.
EACCES non si hanno privilegi per creare un socket nel dominio o con il protocollo specificato.
EINVAL protocollo sconosciuto o dominio non disponibile.
ENOBUFS non c’è sufficiente memoria per creare il socket (può essere anche ENOMEM).
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 specifica il dominio del socket (definisce cioè, come
vedremo in sez. 15.2.2, la famiglia di protocolli usata), type specifica il tipo di socket (definisce
2
del tutto analogo a quelli che si ottengono per i file di dati e le pipe, descritti in sez. 6.1.1.
15.2. LA CREAZIONE DI UN SOCKET 499
cioè, come vedremo in sez. 15.2.3, lo stile di comunicazione) e protocol il protocollo; in genere
quest’ultimo è indicato implicitamente dal tipo di socket, per cui di norma questo valore viene
messo a zero (con l’eccezione dei raw socket).
Si noti che la creazione del socket si limita ad allocare le opportune strutture nel kernel
(sostanzialmente una voce nella file table) e non comporta nulla riguardo all’indicazione degli
indirizzi remoti o locali attraverso i quali si vuole effettuare la comunicazione.
L’idea 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 prefisso PF_ si sarebbe dovuto usare
3
nome che invece il manuale delle glibc riserva a quello che noi abbiamo chiamato domini.
500 CAPITOLO 15. INTRODUZIONE AI SOCKET
nella creazione dei socket e il prefisso AF_ in quello delle strutture degli indirizzi; questo è quanto
specificato 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 definiti
dall’header socket.h. Un elenco delle famiglie di protocolli disponibili in Linux è riportato in
tab. 15.1.5
Si tenga presente che non tutte le famiglie di protocolli sono utilizzabili dall’utente 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 effettivo uguale a zero) o dotati della
capability CAP_NET_RAW.
SOCK_DGRAM Viene usato per trasmettere pacchetti di dati (datagram) di lunghezza mas-
sima prefissata, indirizzati singolarmente. Non esiste una connessione e la
trasmissione è effettuata in maniera non affidabile.
SOCK_RAW Provvede l’accesso a basso livello ai protocolli di rete e alle varie interfacce.
I normali programmi di comunicazione non devono usarlo, è riservato all’uso
di sistema.
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
dei diversi stili di comunicazione appena elencati.
Famiglia Tipo
SOCK_STREAM SOCK_DGRAM SOCK_RAW SOCK_RDM SOCK_SEQPACKET
PF_LOCAL si si
PF_INET TCP UDP IPv4
PF_INET6 TCP UDP IPv6
PF_IPX
PF_NETLINK si si
PF_X25 si
PF_AX25
PF_ATMPVC
PF_APPLETALK si si
PF_PACKET 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 pro-
tocolli. Per ogni combinazione valida si è indicato il tipo di protocollo, o la parola si qualora
non il protocollo non abbia un nome definito, mentre si sono lasciate vuote le caselle per le
combinazioni non supportate.
struct sockaddr {
sa_family_t sa_family ; /* address family : AF_xxx */
char sa_data [14]; /* address ( protocol - specific ) */
};
Tabella 15.3: Tipi di dati usati nelle strutture degli indirizzi, secondo quanto stabilito dallo standard POSIX.1g.
struct sockaddr_in {
sa_family_t sin_family ; /* address family : AF_INET */
in_port_t sin_port ; /* port in network byte order */
struct in_addr sin_addr ; /* internet address */
};
/* Internet address . */
struct in_addr {
in_addr_t s_addr ; /* 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.
di livello superiore come TCP e UDP. Questa struttura però viene usata anche per i socket RAW
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
di EINVAL; il membro sin_port specifica 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 effettivo 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 definite anche
alcune costanti che identificano alcuni indirizzi speciali, riportati in tab. 16.1, che rincontreremo
più avanti.
Infine occorre sottolineare che sia gli indirizzi che i numeri di porta devono essere specificati
in quello che viene chiamato network order, cioè con i bit ordinati in formato big endian (vedi
sez. 2.4.5), questo comporta la necessità di usare apposite funzioni di conversione per mantenere
la portabilità del codice (vedi sez. 15.4 per i dettagli del problema e le relative soluzioni).
Essendo IPv6 un’estensione di IPv4, i socket di tipo PF_INET6 sono sostanzialmente identici ai
precedenti; la parte in cui si trovano praticamente tutte le differenze fra i due socket è quella
della struttura degli indirizzi; la sua definizione, presa da netinet/in.h, è riportata in fig. 15.3.
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.
Figura 15.4: La struttura sockaddr_un degli indirizzi dei socket locali (detti anche unix domain) definita in
sys/un.h.
In questo caso il campo sun_family deve essere AF_UNIX, mentre il campo sun_path deve
specificare un indirizzo. Questo ha due forme; può essere un file (di tipo socket) nel filesystem o
una stringa univoca (mantenuta in uno spazio di nomi astratto). Nel primo caso l’indirizzo viene
specificato come una stringa (terminata da uno zero) corrispondente al pathname del file; nel
secondo invece sun_path inizia con uno zero e vengono usati come nome i restanti byte come
stringa, senza terminazione.
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.
15.3. LE STRUTTURE DEGLI INDIRIZZI DEI SOCKET 505
Il campo sat_family deve essere sempre AF_APPLETALK, mentre il campo sat_port specifica
la porta che identifica 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. L’indirizzo remoto è specificato nella struttura sat_addr, e deve essere
in network order (vedi sez. 2.4.5); esso è composto da un parte di rete data dal campo s_net, che
può assumere il valore AT_ANYNET, che indica una rete generica e vale anche per indicare la rete
su cui si è, il singolo nodo è indicato da s_node, e può prendere il valore generico AT_ANYNODE
che indica anche il nodo corrente, ed il valore ATADDR_BCAST che indica tutti i nodi della rete.
struct sockaddr_ll {
unsigned short sll_family ; /* Always AF_PACKET */
unsigned short sll_protocol ; /* Physical layer protocol */
int sll_ifindex ; /* Interface number */
unsigned short sll_hatype ; /* Header type */
unsigned char sll_pkttype ; /* Packet type */
unsigned char sll_halen ; /* Length of address */
unsigned char sll_addr [8]; /* Physical layer address */
};
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 l’intero a 32 bit hostlong dal formato della macchina a quello della rete.
unsigned short int htons(unsigned short int hostshort)
Converte l’intero a 16 bit hostshort dal formato della macchina a quello della rete.
unsigned long int ntohl(unsigned long int netlong)
Converte l’intero a 32 bit netlong dal formato della rete a quello della macchina.
unsigned sort int ntohs(unsigned short int netshort)
Converte l’intero a 16 bit netshort dal formato della rete a quello della macchina.
I nomi sono assegnati usando la lettera n come mnemonico per indicare l’ordinamento usato
sulla rete (da network order ) e la lettera h come mnemonico per l’ordinamento 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 definite 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.
#include <arpa/inet.h>
in_addr_t inet_addr(const char *strptr)
Converte la stringa dell’indirizzo dotted decimal in nel numero IP in network order.
int inet_aton(const char *src, struct in_addr *dest)
Converte la stringa dell’indirizzo 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 l’indirizzo a 32 bit in network order (del tipo
in_addr_t) a partire dalla stringa passata nell’argomento 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
indirizzo valido, non può essere usata con questa funzione; per questo motivo essa è generalmente
deprecata in favore di inet_aton.
La funzione inet_aton converte la stringa puntata da src nell’indirizzo binario che vie-
ne memorizzato nell’opportuna struttura in_addr (si veda fig. 15.2) situata all’indirizzo dato
dall’argomento dest (è espressa in questa forma in modo da poterla usare direttamente con
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 effettua la validazione
dell’indirizzo.
508 CAPITOLO 15. INTRODUZIONE AI SOCKET
La funzione restituisce un valore negativo se af specifica 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 dell’indirizzo IP
del tipo specificato da af che viene memorizzato all’indirizzo 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 specifica una famiglia di indirizzi non valida.
La seconda funzione di conversione è inet_ntop che converte un indirizzo in una stringa; il
suo prototipo è:
#include <sys/socket.h>
char *inet_ntop(int af, const void *addr_ptr, char *dest, size_t len)
Converte l’indirizzo 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 dell’indirizzo eccedono la lunghezza
specificata da len.
ENOAFSUPPORT la famiglia di indirizzi af non è una valida.
La funzione converte la struttura dell’indirizzo puntata da addr_ptr in una stringa che viene
copiata nel buffer puntato dall’indirizzo dest; questo deve essere preallocato dall’utente e la lun-
ghezza deve essere almeno INET_ADDRSTRLEN in caso di indirizzi IPv4 e INET6_ADDRSTRLEN per
indirizzi IPv6; la lunghezza del buffer deve comunque venire specificata 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; l’argomento dest di inet_ntop non può essere nullo
e deve essere allocato precedentemente.
Il formato usato per gli indirizzi in formato di presentazione è la notazione dotted decimal
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 finiremo prendendo in esame l’uso dell’I/O multiplexing.
509
510 CAPITOLO 16. I SOCKET TCP
3. il server deve dare ricevuto (l’acknowledge) 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 flag SYN e ACK.
4. una volta che il client ha ricevuto l’acknowledge dal server la funzione connect ritorna,
l’ultimo passo è dare il ricevuto del SYN del server inviando un ACK. Alla ricezione di
quest’ultimo la funzione accept del server ritorna e la connessione è stabilita.
Il procedimento viene chiamato three way handshake dato che per realizzarlo devono essere
scambiati tre segmenti. In fig. 16.1 si è rappresentata graficamente la sequenza di scambio dei
segmenti che stabilisce la connessione.
Si è accennato in precedenza ai numeri di sequenza (che sono anche riportati in fig. 16.1):
per gestire una connessione affidabile infatti il protocollo TCP prevede nell’header la presenza
di un numero a 32 bit (chiamato appunto sequence number ) che identifica a quale byte nella
sequenza del flusso 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 all’inizio della connessione e trasmesso con il
SYN; l’acknowledgement di ciascun segmento viene effettuato dall’altro capo della connessione
impostando il flag ACK e restituendo nell’apposito campo dell’header 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
fig. 16.2) per l’acknowledgement di un FIN.
• MSS option, dove MMS sta per Maximum Segment Size, con questa opzione ciascun capo
della connessione annuncia all’altro il massimo ammontare di dati che vorrebbe accettare
2
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.
16.1. IL FUNZIONAMENTO DI UNA CONNESSIONE TCP 511
per ciascun segmento nella connessione corrente. È possibile leggere e scrivere questo valore
attraverso l’opzione del socket TCP_MAXSEG (vedi sez. 17.2.5).
• window scale option, il protocollo TCP implementa il controllo di flusso attraverso una
advertised window (la “finestra annunciata”, vedi sez. ??) con la quale ciascun capo della
comunicazione dichiara quanto spazio disponibile ha in memoria per i dati. Questo è un
numero a 16 bit dell’header, che cosı̀ può indicare un massimo di 65535 byte;3 ma alcuni tipi
di connessione come quelle ad alta velocità (sopra i 45Mbit/sec) e quelle che hanno grandi
ritardi nel cammino dei pacchetti (come i satelliti) richiedono una finestra più grande per
poter ottenere il massimo dalla trasmissione. Per questo esiste questa opzione che indica un
fattore di scala da applicare al valore della finestra annunciata4 per la connessione corrente
(espresso come numero di bit cui spostare a sinistra il valore della finestra annunciata
inserito nel pacchetto). Con Linux è possibile indicare al kernel di far negoziare il fattore
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
velocità per evitare possibili corruzioni di dati dovute a pacchetti perduti che riappaiono;
anche questa viene negoziata come la precedente.
1. Un processo ad uno dei due capi chiama la funzione close, dando l’avvio a quella che viene
chiamata chiusura attiva (o active close). Questo comporta l’emissione di un segmento FIN,
che serve ad indicare che si è finito con l’invio dei dati sulla connessione.
2. L’altro capo della connessione riceve il FIN e dovrà eseguire la chiusura passiva (o passive
close). Al FIN, come ad ogni altro pacchetto, viene risposto con un ACK, inoltre il rice-
vimento 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-file sulla lettura: questo
perché il ricevimento di un FIN significa che non si riceveranno altri dati sulla connessione.
3. Una volta rilevata l’end-of-file anche il secondo processo chiamerà la funzione close sul
proprio socket, causando l’emissione di un altro segmento FIN.
4. L’altro capo della connessione riceverà il FIN conclusivo e risponderà con un ACK.
3
in Linux il massimo è 32767 per evitare problemi con alcune implementazioni che usano l’aritmetica con segno
per implementare lo stack TCP.
4
essendo una nuova opzione per garantire la compatibilità con delle vecchie implementazioni del protocollo
la procedura che la attiva prevede come negoziazione che l’altro capo della connessione riconosca esplicitamente
l’opzione inserendola anche lui nel suo SYN di risposta dell’apertura della connessione.
5
per poter usare questa funzionalità è comunque necessario ampliare le dimensioni dei buffer di ricezione e
spedizione, cosa che può essere fatta sia a livello di sistema con le opportune sysctl (vedi sez. 17.4.3) che a livello
di singoli socket con le relative opzioni (vedi sez. 17.2.5).
512 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
del passo 1) è inviato insieme a dei dati. Inoltre è possibile che i segmenti inviati nei passi 2 e 3
dal capo che effettua la chiusura passiva, siano accorpati in un singolo segmento. In fig. 16.2 si
è rappresentato graficamente lo sequenza di scambio dei segmenti che conclude la connessione.
Come per il SYN anche il FIN occupa un byte nel numero di sequenza, per cui l’ACK
riporterà un acknowledge number incrementato di uno.
Si noti che, nella sequenza di chiusura, fra i passi 2 e 3, è in teoria possibile che si mantenga
un flusso 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
poter sfruttare questa possibilità, usando una procedura che è chiamata half-close; torneremo su
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
la chiamata esplicita della funzione close, ma anche alla terminazione di un processo, quando
tutti i file vengono chiusi. Questo comporta ad esempio che se un processo viene terminato da
un segnale tutte le connessioni aperte verranno chiuse.
Infine occorre sottolineare che, benché nella figura (e nell’esempio che vedremo più avanti
in sez. 16.4.1) sia stato il client ad eseguire la chiusura attiva, nella realtà questa può essere
eseguita da uno qualunque dei due capi della comunicazione (come nell’esempio di fig. 16.9), e
anche se il caso più comune resta quello del client, ci sono alcuni servizi, il principale dei quali
è l’HTTP, per i quali è il server ad effettuare la chiusura attiva.
In assenza di connessione lo stato del TCP è CLOSED; quando una applicazione esegue una
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 finale
in cui avviene la gran parte del trasferimento dei dati.
Dal lato server in genere invece il passaggio che si opera con l’apertura passiva è quello di
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 un’applicazione chiama la funzione
close prima di aver ricevuto un end-of-file (chiusura attiva) la transizione è verso lo stato
FIN_WAIT_1; se invece l’applicazione riceve un FIN nello stato ESTABLISHED (chiusura passiva)
la transizione è verso lo stato CLOSE_WAIT.
In fig. 16.3 è riportato lo schema dello scambio dei pacchetti che avviene per una un esempio
di connessione, insieme ai vari stati che il protocollo viene ad assumere per i due lati, server e
client.
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
stare in un singolo segmento, cioè essere minore dei 1460 byte annunciati dal server), quest’ul-
timo riceve la richiesta e restituisce una risposta (che di nuovo supponiamo stare in un singolo
segmento). Si noti che l’acknowledge della richiesta è mandato insieme alla risposta: questo viene
chiamato piggybacking ed avviene tutte le volte che il server è sufficientemente veloce a costruire
la risposta; in caso contrario si avrebbe prima l’emissione di un ACK e poi l’invio della risposta.
514 CAPITOLO 16. I SOCKET TCP
Infine 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 significato torneremo fra poco.
È da notare come per effettuare 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 sufficienti due soli pacchetti. Questo è il costo che occorre pagare per avere l’affidabilità
garantita dal TCP, se si fosse usato UDP si sarebbe dovuto trasferire la gestione di tutta una
serie di dettagli (come la verifica della ricezione dei pacchetti) dal livello del trasporto all’interno
dell’applicazione.
Quello che è bene sempre tenere presente è allora quali sono le esigenze che si hanno in una
applicazione di rete, perché non è detto che TCP sia la miglior scelta in tutti i casi (ad esempio se
si devono solo scambiare dati già organizzati in piccoli pacchetti l’overhead aggiunto può essere
eccessivo) per questo esistono applicazioni che usano UDP e lo fanno perché nel caso specifico
le sue caratteristiche di velocità e compattezza nello scambio dei dati rispondono meglio alle
esigenze che devono essere affrontate.
Il punto è che entrambe le ragioni sono importanti, anche se spesso si fa riferimento solo alla
prima; ma è solo se si tiene conto della seconda che si capisce il perché della scelta di un tempo
pari al doppio della MSL come durata di questo stato.
Il primo dei due motivi precedenti si può capire tornando a fig. 16.3: assumendo che l’ultimo
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
esegue la chiusura attiva deve mantenere lo stato della connessione per essere in grado di reinviare
16.1. IL FUNZIONAMENTO DI UNA CONNESSIONE TCP 515
l’ACK 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 affrontare la perdita di uno qualunque dei quattro segmenti che costitui-
scono la chiusura. Per questo motivo un socket deve rimanere attivo nello stato TIME_WAIT anche
dopo l’invio dell’ultimo ACK, per potere essere in grado di gestirne l’eventuale ritrasmissione,
in caso esso venga perduto.
Il secondo motivo è più complesso da capire, e necessita di una spiegazione degli scenari in
cui può accadere che i pacchetti TCP si possano perdere nella rete o restare intrappolati, per
poi riemergere in un secondo tempo.
Il caso più comune in cui questo avviene è quello di anomalie nell’instradamento; può accadere
cioè che un router smetta di funzionare o che una connessione fra due router si interrompa. In
questo caso i protocolli di instradamento dei pacchetti possono impiegare diverso tempo (anche
dell’ordine 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 quest’ultimo
li rispedisce indietro, o li manda ad un terzo router che li rispedisce al primo, si creano cioè dei
circoli (i cosiddetti routing loop) in cui restano intrappolati i pacchetti.
Se uno di questi pacchetti intrappolati è un segmento TCP, chi l’ha inviato, non ricevendo
un ACK in risposta, provvederà alla ritrasmissione e se nel frattempo sarà stata stabilita una
strada alternativa il pacchetto ritrasmesso giungerà a destinazione.
Ma se dopo un po’ di tempo (che non supera il limite dell’MSL, dato che altrimenti verrebbe
ecceduto il TTL) l’anomalia viene a cessare, il circolo di instradamento viene spezzato i pacchetti
intrappolati potranno essere inviati alla destinazione finale, con la conseguenza di avere dei
pacchetti duplicati; questo è un caso che il TCP deve essere in grado di gestire.
Allora per capire la seconda ragione per l’esistenza dello stato TIME_WAIT si consideri il caso
seguente: si supponga di avere una connessione fra l’IP 195.110.112.236 porta 1550 e l’IP
192.84.145.100 porta 22 (affronteremo il significato 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 fintanto che il socket non è chiuso una nuova incarnazione non può essere creata: per
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.
D’altra parte un client non ha necessità di usare un numero di porta specifico, per cui
in genere vengono usate le cosiddette porte effimere (o ephemeral ports) cioè porte a cui non è
assegnato nessun servizio noto e che vengono assegnate automaticamente dal kernel alla creazione
della connessione. Queste sono dette effimere in quanto vengono usate solo per la durata della
connessione, e l’unico requisito che deve essere soddisfatto è che ognuna di esse sia assegnata in
maniera univoca.
La lista delle porte conosciute è definita dall’RFC 1700 che contiene l’elenco delle porte
assegnate dalla IANA (la Internet Assigned Number Authority) ma l’elenco viene costante-
mente aggiornato e pubblicato su internet (una versione aggiornata si può trovare all’indirizzo
http://www.iana.org/assignments/port-numbers); inoltre in un sistema unix-like un analogo elen-
co viene mantenuto nel file /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
esempio la porta 22 è assegnata a SSH su entrambi i protocolli, anche se viene usata solo
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
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
dal 6000 al 6063 anche se il protocollo è implementato solo tramite TCP.
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 effimere.
In realtà rispetto a quanto indicato nell’RFC 1700 i vari sistemi hanno fatto scelte diverse
per le porte effimere, in particolare in fig. 16.4 sono riportate quelle di BSD e Linux.
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
possono essere assegnate a un socket solo da un processo con i privilegi di amministratore, per
far sı̀ che solo l’amministratore possa allocare queste porte per far partire i relativi servizi.
Le glibc definiscono (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 effimere siano allocate fra questi due valori. Nel caso di Linux questo è vero solo in uno
dei due casi di fig. 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 517
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 dell’autenticazione, contando
appunto sul fatto che solo l’amministratore può usare queste porte. Data l’assoluta inconsistenza
in termini di sicurezza di un tale metodo, al giorno d’oggi esso è in completo disuso.
Data una connessione TCP si suole chiamare socket pair 6 la combinazione dei quattro numeri
che definiscono i due capi della connessione e cioè l’indirizzo IP locale e la porta TCP locale, e
l’indirizzo 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), identifica univocamente una
connessione su internet. Questo concetto viene di solito esteso anche a UDP, benché in questo
caso non abbia senso parlare di connessione. L’utilizzo del programma netstat permette di
visualizzare queste informazioni nei campi Local Address e Foreing Address.
Come si può notare il server è ancora in ascolto sulla porta 22, però adesso c’è un nuovo
socket (con lo stato ESTABLISHED) che utilizza anch’esso la porta 22, ed ha specificato l’indirizzo
locale, questo è il socket con cui il processo figlio gestisce la connessione mentre il padre resta in
ascolto sul socket originale.
Se a questo punto lanciamo un’altra volta il client ssh per una seconda connessione quello
che otterremo usando netstat sarà qualcosa del genere:
cioè il client effettuerà la connessione usando un’altra porta effimera: con questa sarà aperta la
connessione, ed il server creerà un altro processo figlio per gestirla.
Tutto ciò mostra come il TCP, per poter gestire le connessioni con un server concorrente, non
può suddividere i pacchetti solo sulla base della porta di destinazione, ma deve usare tutta l’in-
formazione contenuta nella socket pair, compresa la porta dell’indirizzo 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 figlio e quelli che arrivano alla porta 21101 al
secondo.
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
esaminata accuratamente nel capitolo precedente in sez. 15.2.1.
La funzione bind assegna un indirizzo locale ad un socket.8 È usata cioè per specificare la prima
parte dalla socket pair. Viene usata sul lato server per specificare la porta (e gli eventuali indirizzi
locali) su cui poi ci si porrà in ascolto. Il prototipo della funzione è il seguente:
7
ad esempio con il comando fuser, o con lsof, o usando l’opzione -p.
8
nel nostro caso la utilizzeremo per socket TCP, ma la funzione è generica e deve essere usata per qualunque
tipo di socket SOCK_STREAM prima che questo possa accettare connessioni.
16.2. LE FUNZIONI DI BASE PER LA GESTIONE DEI SOCKET 519
#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 il file descriptor non è valido.
EINVAL il socket ha già un indirizzo assegnato.
ENOTSOCK il file descriptor non è associato ad un socket.
EACCES si è cercato di usare una porta riservata senza sufficienti privilegi.
EADDRNOTAVAIL il tipo di indirizzo specificato non è disponibile.
EADDRINUSE qualche altro socket sta già usando l’indirizzo.
ed anche EFAULT e per i socket di tipo AF_UNIX, ENOTDIR, ENOENT, ENOMEM, ELOOP, ENOSR e EROFS.
9
un’eccezione a tutto ciò sono i server che usano RPC. In questo caso viene fatta assegnare dal kernel una porta
effimera che poi viene registrata presso il portmapper ; quest’ultimo è un altro demone che deve essere contattato
dai client per ottenere la porta effimera su cui si trova il server.
520 CAPITOLO 16. I SOCKET TCP
L’esempio precedente funziona correttamente con IPv4 poiché che l’indirizzo è rappresenta-
bile anche con un intero a 32 bit; non si può usare lo stesso metodo con IPv6, in cui l’indirizzo
deve necessariamente essere specificato con una struttura, perché il linguaggio C non consente
l’uso di una struttura costante come operando a destra in una assegnazione.
Per questo motivo nell’header netinet/in.h è definita una variabile in6addr_any (dichia-
rata come extern, ed inizializzata dal sistema al valore IN6ADRR_ANY_INIT) che permette di
effettuare una assegnazione del tipo:
serv_add . sin6_addr = in6addr_any ;
in maniera analoga si può utilizzare la variabile in6addr_loopback per indicare l’indirizzo di
loopback, che a sua volta viene inizializzata staticamente a IN6ADRR_LOOPBACK_INIT.
#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
valori:
ECONNREFUSED non c’è nessuno in ascolto sull’indirizzo remoto.
ETIMEDOUT si è avuto timeout durante il tentativo di connessione.
ENETUNREACH la rete non è raggiungibile.
EINPROGRESS il socket è non bloccante (vedi sez. 12.2.1) e la connessione non può essere conclusa
immediatamente.
EALREADY il socket è non bloccante (vedi sez. 12.2.1) e un tentativo precedente di connessione
non si è ancora concluso.
EAGAIN non ci sono più porte locali libere.
EAFNOSUPPORT l’indirizzo 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
socket fosse stato abilitato per il broadcast.
altri errori possibili sono: EFAULT, EBADF, ENOTSOCK, EISCONN e EADDRINUSE.
Il primo argomento è un file descriptor ottenuto da una precedente chiamata a socket, mentre
il secondo e terzo argomento sono rispettivamente l’indirizzo e la dimensione della struttura che
contiene l’indirizzo del socket, già descritta in sez. 15.3.
La struttura dell’indirizzo deve essere inizializzata con l’indirizzo IP e il numero di porta del
server a cui ci si vuole connettere, come mostrato nell’esempio 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 è verificato un errore. Le possibili cause di errore sono
molteplici (ed i relativi codici riportati sopra), quelle che però dipendono dalla situazione della
rete e non da errori o problemi nella chiamata della funzione sono le seguenti:
1. Il client non riceve risposta al SYN: l’errore restituito è ETIMEDOUT. Stevens riporta che
BSD invia un primo SYN alla chiamata di connect, un altro dopo 6 secondi, un terzo dopo
10
di nuovo la funzione è generica e supporta vari tipi di socket, la differenza è che per socket senza connessione
come quelli di tipo SOCK_DGRAM la sua chiamata si limiterà ad impostare l’indirizzo dal quale e verso il quale
saranno inviati e ricevuti i pacchetti, mentre per socket di tipo SOCK_STREAM o SOCK_SEQPACKET, essa attiverà la
procedura di avvio (nel caso del TCP il three way handshake) della connessione.
16.2. LE FUNZIONI DI BASE PER LA GESTIONE DEI SOCKET 521
24 secondi, se dopo 75 secondi non ha ricevuto risposta viene ritornato l’errore. Linux invece
ripete l’emissione del SYN ad intervalli di 30 secondi per un numero di volte che può essere
stabilito dall’utente. Questo può essere fatto a livello globale con una opportuna sysctl,11
e a livello di singolo socket con l’opzione TCP_SYNCNT (vedi sez. 17.2.5). Il valore predefinito
per la ripetizione dell’invio è di 5 volte, che comporta un timeout dopo circa 180 secondi.
2. Il client riceve come risposta al SYN un RST significa che non c’è nessun programma in
ascolto per la connessione sulla porta specificata (il che vuol dire probabilmente che o si è
sbagliato il numero della porta o che non è stato avviato il server), questo è un errore fatale
e la funzione ritorna non appena il RST viene ricevuto riportando un errore ECONNREFUSED.
Il flag RST sta per reset ed è un segmento inviato direttamente dal TCP quando qualcosa
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 l’emissione di un messaggio ICMP di destinazione non raggiungi-
bile. In questo caso dato che il messaggio può essere dovuto ad una condizione transitoria
si ripete l’emissione dei SYN come nel caso precedente, fino 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 fig. 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.
Si noti infine che con la funzione connect si è specificato solo indirizzo e porta del server,
quindi solo una metà della socket pair; essendo questa funzione usata nei client l’altra metà
contenente indirizzo e porta locale viene lasciata all’assegnazione automatica del kernel, e non è
necessario effettuare una bind.
#include <sys/socket.h>
int listen(int sockfd, int backlog)
Pone un socket in attesa di una connessione.
La funzione pone il socket specificato da sockfd in modalità passiva e predispone una coda
per le connessioni in arrivo di lunghezza pari a backlog. La funzione si può applicare solo a
socket di tipo SOCK_STREAM o SOCK_SEQPACKET.
11
o più semplicemente scrivendo il valore voluto in /proc/sys/net/ipv4/tcp_syn_retries, vedi sez. 17.4.3.
12
questa funzione può essere usata con socket che supportino le connessioni, cioè di tipo SOCK_STREAM o
SOCK_SEQPACKET.
522 CAPITOLO 16. I SOCKET TCP
1. La coda delle connessioni incomplete (incomplete connection queue) che contiene un rife-
rimento per ciascun socket per il quale è arrivato un SYN ma il three way handshake non
si è ancora concluso. Questi socket sono tutti nello stato SYN_RECV.
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
non è ritornata. Questi socket sono tutti nello stato ESTABLISHED.
Figura 16.5: Schema di funzionamento delle code delle connessioni complete ed incomplete.
14
il valore di questa costante può essere controllato con un altro parametro di sysctl, vedi sez. 17.3.3.
15
la funzione è comunque generica ed è utilizzabile su socket di tipo SOCK_STREAM, SOCK_SEQPACKET e SOCK_RDM.
524 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.
I due argomenti addr e addrlen (si noti che quest’ultimo è passato per indirizzo per avere
indietro il valore) sono usati per ottenere l’indirizzo 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
di byte scritti dentro addr. Se questa informazione non interessa basterà inizializzare a NULL detti
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 effettuato verso il socket sockfd. Quest’ultimo (detto
listening socket) è quello creato all’inizio e messo in ascolto con listen, e non viene toccato
dalla funzione. Se non ci sono connessioni pendenti da accettare la funzione mette in attesa il
processo16 fintanto che non ne arriva una.
La funzione può essere usata solo con socket che supportino la connessione (cioè di tipo
SOCK_STREAM, SOCK_SEQPACKET o SOCK_RDM). Per alcuni protocolli che richiedono una conferma
esplicita della connessione,17 la funzione opera solo l’estrazione dalla coda delle connessioni, la
conferma della connessione viene eseguita implicitamente dalla prima chiamata ad una read o
una write, mentre il rifiuto della connessione viene eseguito con la funzione close.
È 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 l’applicazione deve tenerne
conto ed eventualmente ripetere la chiamata alla funzione come per l’errore di EAGAIN (torneremo
su questo in sez. 16.5). Un’altra differenza con BSD è che la funzione non fa ereditare al nuovo
socket i flag del socket originale, come O_NONBLOCK,18 che devono essere rispecificati 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
server: in generale infatti c’è sempre un solo socket in ascolto, detto per questo listening socket,
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, fino alla chiusura
della connessione. Si può riconoscere questo schema anche nell’esempio elementare di fig. 16.9,
dove per ogni connessione il socket creato da accept viene chiuso dopo l’invio dei dati.
Oltre a tutte quelle viste finora, dedicate all’utilizzo 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
dati relativi alla socket pair associata ad un certo socket.
La prima funzione è getsockname e serve ad ottenere l’indirizzo locale associato ad un socket;
il suo prototipo è:
16
a meno che non si sia impostato il socket per essere non bloccante (vedi sez. 12.2.1), nel qual caso ritorna con
l’errore EAGAIN. Torneremo su questa modalità di operazione in sez. 16.6.
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.2. LE FUNZIONI DI BASE PER LA GESTIONE DEI SOCKET 525
#include <sys/socket.h>
int getsockname(int sockfd, struct sockaddr *name, socklen_t *namelen)
Legge l’indirizzo locale di un socket.
La funzione restituisce la struttura degli indirizzi del socket sockfd nella struttura indicata
dal puntatore name la cui lunghezza è specificata tramite l’argomento namlen. Quest’ultimo
viene passato come indirizzo per avere indietro anche il numero di byte effettivamente scritti
nella struttura puntata da name. Si tenga presente che se si è utilizzato un buffer troppo piccolo
per name l’indirizzo risulterà troncato.
La funzione si usa tutte le volte che si vuole avere l’indirizzo locale di un socket; ad esempio
può essere usata da un client (che usualmente non chiama bind) per ottenere numero IP e porta
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 effimera 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 l’indirizzo locale
che il kernel ha assegnato a quella connessione.
Tutte le volte che si vuole avere l’indirizzo remoto di un socket si usa la funzione getpeername,
il cui prototipo è:
#include <sys/socket.h>
int getpeername(int sockfd, struct sockaddr * name, socklen_t * namelen)
Legge l’indirizzo remoto di un socket.
una opportuna convenzione per rendere noto al programma eseguito qual è il socket connesso, 20
quest’ultimo potrà usare la funzione getpeername per determinare l’indirizzo remoto del client.
Infine è da chiarire (si legga la pagina di manuale) che, come per accept, il terzo argomento,
che è specificato dallo standard POSIX.1g come di tipo socklen_t * in realtà deve sempre
corrispondere ad un int * come prima dello standard perché tutte le implementazioni dei socket
BSD fanno questa assunzione.
Figura 16.6: La funzione FullRead, che legge esattamente count byte da un file descriptor, iterando
opportunamente le letture.
le funzioni si possono bloccare se i dati non sono disponibili: è lo stesso comportamento che si
può avere scrivendo più di PIPE_BUF byte in una pipe (si riveda quanto detto in sez. 11.1.1).
Per questo motivo, seguendo l’esempio di R. W. Stevens in [2], si sono definite 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 specificato;
il sorgente è riportato rispettivamente in fig. 16.6 e fig. 16.7 ed è disponibile fra i sorgenti allegati
alla guida nei file FullRead.c e FullWrite.c.
Come si può notare le due funzioni ripetono la lettura/scrittura in un ciclo fino all’esauri-
mento del numero di byte richiesti, in caso di errore viene controllato se questo è EINTR (cioè
un’interruzione della system call dovuta ad un segnale), nel qual caso l’accesso viene ripetuto,
altrimenti l’errore viene ritornato al programma chiamante, interrompendo il ciclo.
Nel caso della lettura, se il numero di byte letti è zero, significa che si è arrivati alla fine
del file (per i socket questo significa in genere che l’altro capo è stato chiuso, e quindi non sarà
più possibile leggere niente) e pertanto si ritorna senza aver concluso la lettura di tutti i byte
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-file prematuro.
Il primo esempio di applicazione delle funzioni di base illustrate in sez. 16.2 è relativo alla creazio-
ne di un client elementare per il servizio daytime, un servizio elementare, definito nell’RFC 867,
che restituisce l’ora locale della macchina a cui si effettua la richiesta, e che è assegnato alla
porta 13.
In fig. 16.8 è riportata la sezione principale del codice del nostro client. Il sorgente completo
del programma (TCP_daytime.c, che comprende il trattamento delle opzioni ed una funzione
528 CAPITOLO 16. I SOCKET TCP
Figura 16.7: La funzione FullWrite, che scrive esattamente count byte su un file descriptor, iterando
opportunamente le scritture.
per stampare un messaggio di aiuto) è allegato alla guida nella sezione dei codici sorgente e può
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
comando (effettuata 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
AF_INET). La funzione socket ritorna il descrittore che viene usato per identificare 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 un’apposita struttura sockaddr_in in cui
sarà inserito l’indirizzo del server ed il numero della porta del servizio. Il primo passo (20) è
inizializzare tutto a zero, per poi inserire il tipo di indirizzo (21) e la porta (22), usando per
quest’ultima la funzione htons per convertire il formato dell’intero usato dal computer a quello
usato nella rete, infine 23-27 si può utilizzare la funzione inet_pton per convertire l’indirizzo
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
la struttura preparata in precedenza con il relativo indirizzo; si noti come, esistendo diversi tipi
di socket, si sia dovuto effettuare 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
socket; il protocollo prevede che il server invii sempre una stringa alfanumerica, il formato della
stringa non è specificato dallo standard, per cui noi useremo il formato usato dalla funzione
ctime, seguito dai caratteri di terminazione \r\n, cioè qualcosa del tipo:
Wed Apr 4 00:53:00 2001\r\n
questa viene letta dal socket (34) con la funzione read in un buffer temporaneo; la stringa
poi deve essere terminata (35) con il solito carattere nullo per poter essere stampata (36) sullo
16.3. UN ESEMPIO ELEMENTARE: IL SERVIZIO DAYTIME 529
Si noti come in questo caso la fine dei dati sia specificata dal server che chiude la connessione
(anche questo è quanto richiesto dal protocollo); questa è una delle tecniche possibili (è quella
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 è
che TCP non provvede nessuna indicazione che permetta di marcare dei blocchi di dati, per cui
se questo è necessario deve provvedere il programma stesso.
Se abilitiamo il servizio daytime 21 possiamo verificare il funzionamento del nostro client,
avremo allora:
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 l’invio della data in risposta
ad una successiva connessione.
È importante notare che questo server è estremamente elementare, infatti, a parte il fatto di
poter essere usato solo con indirizzi IPv4, esso è in grado di rispondere ad un solo un client alla
volta: è cioè, come dicevamo, un server iterativo. Inoltre è scritto per essere lanciato da linea
di comando, se lo si volesse utilizzare come demone occorrerebbero le opportune modifiche22
per tener conto di quanto illustrato in sez. 10.1.5. Si noti anche che non si è inserita nessuna
forma di gestione della terminazione del processo, dato che tutti i file descriptor vengono chiusi
automaticamente alla sua uscita, e che, non generando figli, non è necessario preoccuparsi di
gestire la loro terminazione.
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 effettivamente chiuso dato che alla
chiusura da parte del figlio resterebbe ancora un riferimento nel padre. Si avrebbero cosı̀ due
534 CAPITOLO 16. I SOCKET TCP
effetti: 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
figlio. Questo si incarica (34) di chiamare time per leggere il tempo corrente, e di stamparlo (35)
sulla stringa contenuta in buffer con l’uso 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
a FullWrite in quanto la stringa è estremamente breve e verrà senz’altro scritta in un singolo
segmento.
Inoltre nel caso sia stato abilitato il logging delle connessioni, si provvede anche (40-43) a
stampare sullo standard output l’indirizzo e la porta da cui il client ha effettuato la connessione,
usando i valori contenuti nelle strutture restituite da accept, eseguendo le opportune conversioni
con inet_ntop e ntohs.
Ancora una volta l’esempio è estremamente semplificato, si noti come di nuovo non si sia
gestita né la terminazione del processo né il suo uso come demone, che tra l’altro sarebbe stato
incompatibile con l’uso della opzione di logging che stampa gli indirizzi delle connessioni sullo
standard output. Un altro aspetto tralasciato è la gestione della terminazione dei processi figli,
torneremo su questo più avanti quando tratteremo alcuni esempi di server più complessi.
riscriverà immutata all’indietro. Sarà compito del client leggere la risposta del server e stamparla
sullo standard output.
Al solito si è tralasciata la sezione relativa alla gestione delle opzioni a riga di comando.
Una volta dichiarate le variabili, si prosegue (10-13) con della creazione del socket con l’usuale
controllo degli errori, alla preparazione (14-17) della struttura dell’indirizzo, che stavolta usa
la porta 7 riservata al servizio echo, infine si converte (18-22) l’indirizzo specificato a riga di
comando. A questo punto (23-27) si può eseguire la connessione al server secondo la stessa
modalità usata in sez. 16.3.2.
Completata la connessione, per gestire il funzionamento del protocollo si usa la funzione
ClientEcho, il cui codice si è riportato a parte in fig. 16.12. Questa si preoccupa di gestire tutta
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.
536 CAPITOLO 16. I SOCKET TCP
La funzione ClientEcho utilizza due buffer (3) per gestire i dati inviati e letti sul socket.
La comunicazione viene gestita all’interno 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 fino al massimo di MAXLINE caratteri) e la salva sul buffer di invio.
Si usa poi (6) la funzione FullWrite, vista in sez. 16.3.1, per scrivere i dati sul socket, gesten-
do automaticamente l’invio multiplo qualora una singola write non sia sufficiente. I dati vengono
riletti indietro (7) con una read23 sul buffer di ricezione e viene inserita (8) la terminazione della
stringa e per poter usare (9) la funzione fputs per scriverli su stdout.
Figura 16.12: Codice della prima versione della funzione ClientEcho per la gestione del servizio echo.
Quando si concluderà l’invio di dati mandando un end-of-file sullo standard input si avrà il
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ò effettuare una verifica del funzionamento del client abilitando il servizio echo nella
configurazione 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
la nostra versione del server echo, che illustriamo immediatamente.
Figura 16.13: Codice del corpo principale della prima versione del server per il servizio echo.
538 CAPITOLO 16. I SOCKET TCP
Una volta eseguita la funzione bind però i privilegi di amministratore non sono più necessari,
per questo è sempre opportuno rilasciarli, in modo da evitare problemi in caso di eventuali
vulnerabilità del server. Per questo prima (22-26) si esegue setgid per assegnare il processo ad
un gruppo senza privilegi,24 e poi si ripete (27-30) l’operazione usando setuid per cambiare
anche l’utente.25 Infine (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
il servizio daytime, con l’unica differenza della chiamata alla funzione PrintErr, riportata in
fig. 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 indefinitamente il ciclo
principale (42-59). All’interno di questo si ricevono (43-47) le connessioni, creando (48-51) un
processo figlio per ciascuna di esse. Quest’ultimo (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 effettuata inviando un segnale al processo.
Avendo trattato direttamente la gestione del programma come demone, si è dovuto anche
provvedere alla necessità di poter stampare eventuali messaggi di errore attraverso il sistema del
syslog trattato in sez. 10.1.5. Come accennato questo è stato fatto utilizzando come wrapper la
funzione PrintErr, il cui codice è riportato in fig. 16.14.
In essa ci si limita a controllare (2) se è stato impostato (valore attivo per default) l’uso come
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
l’opzione -i) si usa (5) semplicemente la funzione perror per stampare sullo standard error.
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 effettuata interamente nella funzione ServEcho, il cui
codice è mostrato in fig. 16.15, e la comunicazione viene gestita all’interno di un ciclo (6-13). I
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 fig. 16.7) che si incarica di tenere conto
automaticamente della possibilità che non tutti i dati di cui è richiesta la scrittura vengano
trasmessi con una singola write.
24
si è usato il valore 65534, ovvero -1 per il formato short, che di norma in tutte le distribuzioni viene usato
per identificare il gruppo nogroup e l’utente nobody, usati appunto per eseguire programmi che non richiedono
nessun privilegio particolare.
25
si tenga presente che l’ordine in cui si eseguono queste due operazioni è importante, infatti solo avendo i
privilegi di amministratore si può cambiare il gruppo di un processo ad un altro di cui non si fa parte, per cui
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.
16.4. UN ESEMPIO PIÙ COMPLETO: IL SERVIZIO ECHO 539
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 l’uscita dal ciclo e il ritorno (12) della funzione, che a sua volta causa la
terminazione del processo figlio.
• il server eseguirà una fork facendo chiamare al processo figlio la funzione ServEcho,
quest’ultima si bloccherà sulla read dal socket sul quale ancora non sono presenti dati.
• il processo padre del server chiamerà di nuovo accept bloccandosi fino all’arrivo di un’altra
connessione.
e se usiamo il comando ps per esaminare lo stato dei processi otterremo un risultato del tipo:
[piccardi@roke piccardi]$ ps ax
PID TTY STAT TIME COMMAND
... ... ... ... ...
2356 pts/0 S 0:00 ./echod
2358 pts/1 S 0:00 ./echo 127.0.0.1
2359 pts/0 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.8).
Se a questo punto si inizia a scrivere qualcosa sul client non sarà trasmesso niente fin tanto
che non si prema il tasto di a capo (si ricordi quanto detto in sez. 7.2.5 a proposito dell’I/O su
terminale), solo allora fgets ritornerà ed il client scriverà quanto immesso sul socket, per poi
passare a rileggere quanto gli viene inviato all’indietro dal server, che a sua volta sarà inviato
sullo standard output, che nel caso ne provoca l’immediata stampa a video.
2. al ritorno di ClientEcho ritorna anche la funzione main, e come parte del processo termi-
nazione tutti i file 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
cui questo risponderà con un ACK. A questo punto il client verrà a trovarsi nello stato
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 figlio che gestisce la connessione ritorna
restituendo 0 causando cosı̀ l’uscita dal ciclo e il ritorno di ServEcho, a questo punto il
processo figlio termina chiamando exit.
4. all’uscita del figlio tutti i file descriptor vengono chiusi, la chiusura del socket connesso fa
sı̀ che venga effettuata la sequenza finale di chiusura della connessione, viene emesso un
FIN dal server che riceverà un ACK dal client, a questo punto la connessione è conclusa e
il client resta nello stato TIME_WAIT.
Dato che non è il caso di lasciare processi zombie, occorrerà ricevere opportunamente lo stato
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 modifica al nostro server è pertanto quella di inserire la
gestione della terminazione dei processi figli attraverso l’uso di un gestore. Per questo useremo la
funzione Signal (che abbiamo illustrato in fig. 9.10), per installare il gestore che riceve i segnali
dei processi figli terminati già visto in fig. 9.4. Basterà allora aggiungere il seguente codice:
...
/* install SIGCHLD handler */
Signal ( SIGCHLD , HandSigCHLD ); /* establish handler */
/* create socket */
...
all’esempio illustrato in fig. 16.13.
In questo modo però si introduce un altro problema. Si ricordi infatti che, come spiegato
in sez. 9.3.1, quando un programma si trova in stato di sleep durante l’esecuzione di una
system call, questa viene interrotta alla ricezione di un segnale. Per questo motivo, alla fine
dell’esecuzione del gestore del segnale, se questo ritorna, il programma riprenderà l’esecuzione
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 pro-
cesso figlio che gestisce la connessione terminerà, ed il padre, per evitare la creazione di zombie,
riceverà il segnale SIGCHLD eseguendo il relativo gestore. Al ritorno del gestore però l’esecuzione
nel padre ripartirà subito con il ritorno della funzione accept (a meno di un caso fortuito in
cui il segnale arriva durante l’esecuzione del programma in risposta ad una connessione) con
un errore di EINTR. Non avendo previsto questa eventualità il programma considera questo un
errore fatale terminando a sua volta con un messaggio del tipo:
542 CAPITOLO 16. I SOCKET TCP
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 modificare il codice di
Signal per richiedere il riavvio automatico delle system call interrotte secondo la semantica di
BSD, usando l’opzione SA_RESTART di sigaction; rispetto a quanto visto in fig. 9.10. Definiremo
allora la nuova funzione SignalRestart27 come mostrato in fig. 16.16, ed installeremo il gestore
usando quest’ultima.
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 fig. 9.10,
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 è
necessaria nessuna altra modifica: le system call interrotte saranno automaticamente riavviate,
e l’errore EINTR non si manifesterà più.
La seconda soluzione è più invasiva e richiede di controllare tutte le volte l’errore restituito
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
riavvio automatico delle system call, fornita da SA_RESTART, è opzionale, per cui non è detto che
essa sia disponibile su qualunque sistema. Inoltre in certi casi,28 anche quando questa è presente,
non è detto possa essere usata con accept.
La portabilità nella gestione dei segnali però viene al costo di una riscrittura parziale del
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 fig. 16.17, dove si sono riportate la sezioni di codice
modificate nella seconda versione del programma, il codice completo di quest’ultimo si trova nel
file TCP_echod_second.c dei sorgenti allegati alla guida.
La prima modifica effettuata è stata quella di introdurre una nuova opzione a riga di co-
mando, -c, che permette di richiedere il comportamento compatibile nella gestione di SIGCHLD
al posto della semantica BSD impostando la variabile compat ad un valore non nullo. Questa è
27
anche questa è definita, insieme alle altre funzioni riguardanti la gestione dei segnali, nel file SigHand.c, il cui
contento completo può essere trovato negli esempi allegati.
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.
16.4. UN ESEMPIO PIÙ COMPLETO: IL SERVIZIO ECHO 543
preimpostata al valore nullo, cosicché se non si usa questa opzione il comportamento di default
del server è di usare la semantica BSD.
Una seconda opzione aggiunta è quella di inserire un tempo di attesa fisso specificato in
secondi fra il ritorno della funzione listen e la chiamata di accept, specificabile con l’opzione
-w, che permette di impostare la variabile waiting. Infine si è introdotta una opzione -d per
abilitare il debugging che imposta ad un valore non nullo la variabile debugging. Al solito si è
omessa da fig. 16.17 la sezione di codice relativa alla gestione di tutte queste opzioni, che può
essere trovata nel sorgente del programma.
Figura 16.17: La sezione nel codice della seconda versione del server per il servizio echo modificata per tener
conto dell’interruzione delle system call.
544 CAPITOLO 16. I SOCKET TCP
Vediamo allora come è cambiato il nostro server; una volta definite le variabili e trattate le
opzioni il primo passo (9-13) è verificare la semantica scelta per la gestione di SIGCHLD, a seconda
del valore di compat (9) si installa il gestore con la funzione Signal (10) o con SignalRestart
(12), essendo quest’ultimo il valore di default.
Tutta la sezione seguente, che crea il socket, cede i privilegi di amministratore ed even-
tualmente lancia il programma come demone, è rimasta invariata e pertanto è stata omessa in
fig. 16.17; l’unica modifica effettuata prima dell’entrata nel ciclo principale è stata quella di aver
introdotto, subito dopo la chiamata (17-20) alla funzione listen, una eventuale pausa con una
condizione (21) sulla variabile waiting, che viene inizializzata, con l’opzione -w Nsec, al numero
di secondi da aspettare (il valore preimpostato è nullo).
Si è potuto lasciare inalterata tutta la sezione di creazione del socket perché nel server l’unica
chiamata ad una system call lenta, che può essere interrotta dall’arrivo di SIGCHLD, è quella ad
accept, che è l’unica funzione che può mettere il processo padre in stato di sleep nel periodo
in cui un figlio può terminare; si noti infatti come le altre slow system call 29 o sono chiamate
prima di entrare nel ciclo principale, quando ancora non esistono processi figli, o sono chiamate
dai figli stessi e non risentono di SIGCHLD.
Per questo l’unica modifica sostanziale nel ciclo principale (23-42), rispetto precedente ver-
sione di fig. 16.15, è nella sezione (25-31) in cui si effettua la chiamata di accept. Quest’ultima
viene effettuata (26-27) all’interno di un ciclo di while30 che la ripete indefinitamente qualora
in caso di errore il valore di errno sia EINTR. Negli altri casi si esce in caso di errore effettivo
(27-29), altrimenti il programma prosegue.
Si noti che in questa nuova versione si è aggiunta una ulteriore sezione (32-40) di aiuto per
il debug del programma, che eseguita con un controllo (33) sul valore della variabile debugging
impostato dall’opzione -d. Qualora questo sia nullo, come preimpostato, non accade nulla. altri-
menti (33) l’indirizzo ricevuto da accept viene convertito in una stringa che poi (34-39) viene
opportunamente stampata o sullo schermo o nei log.
Infine come ulteriore miglioria si è perfezionata la funzione ServEcho, sia per tenere conto
della nuova funzionalità di debugging, che per effettuare un controllo in caso di errore; il codice
della nuova versione è mostrato in fig. 16.18.
Rispetto alla precedente versione di fig. 16.15 in questo caso si è provveduto a controllare
(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
l’apposita opzione -d) si provvederà a stampare (tenendo conto della modalità di invocazione
del server, se interattiva o in forma di demone) il numero di byte e la stringa letta dal client
(16-24).
29
si ricordi la distinzione fatta in sez. 9.3.1.
30
la sintassi del C relativa a questo ciclo può non essere del tutto chiara. In questo caso infatti si è usato un
ciclo vuoto che non esegue nessuna istruzione, in questo modo quello che viene ripetuto con il ciclo è soltanto il
codice che esprime la condizione all’interno del while.
16.5. I VARI SCENARI CRITICI 545
Figura 16.18: Codice della seconda versione della funzione ServEcho per la gestione del servizio echo.
Benché questo non sia un fatto comune, un evento simile può essere osservato con dei server
546 CAPITOLO 16. I SOCKET TCP
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 figli, può accadere che il three way
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 fig. 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 significa 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 all’uscita 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 modificata del no-
stro 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
prima della chiamata ad accept, come si potrebbe fare usando l’opzione -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-file al primo accesso
al socket. Nel caso di Linux inoltre, anche qualora si modifichi il client per fargli gestire l’invio
di un segmento di RST alla chiusura dal socket (usando l’opzione 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.
dalla pagina di manuale; per l’uso che vogliamo farne quello che ci interessa è, posizionandosi
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 configurabile in maniera molto precisa).
Lanciando il comando prima di ripetere la sessione di lavoro mostrata nell’esempio 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 src 192.168.1.141 or dst 192.168.1.141 -N -t
tcpdump: listening on eth0
gont.34559 > anarres.echo: S 800922320:800922320(0) win 5840
anarres.echo > gont.34559: S 511689719:511689719(0) ack 800922321 win 5792
gont.34559 > anarres.echo: . ack 1 win 5840
gont.34559 > anarres.echo: P 1:12(11) ack 1 win 5840
anarres.echo > gont.34559: . ack 12 win 5792
anarres.echo > gont.34559: P 1:12(11) ack 12 win 5792
gont.34559 > anarres.echo: . ack 12 win 5840
anarres.echo > gont.34559: F 12:12(0) ack 12 win 5792
gont.34559 > anarres.echo: . ack 13 win 5840
gont.34559 > anarres.echo: P 12:37(25) ack 13 win 5840
anarres.echo > gont.34559: R 511689732:511689732(0) win 0
Le prime tre righe vengono prodotte al momento in cui lanciamo il nostro client, e corrispon-
dono ai tre pacchetti del three way handshake. L’output del comando riporta anche i numeri di
sequenza iniziali, mentre la lettera S indica che per quel pacchetto si aveva il SYN flag 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; quest’ultimo, a partire dal terzo pacchetto, viene espres-
so 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ò verificare dall’output del comando come
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 fin 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 significa che il flag PSH è impostato, contiene la
nostra riga (che è appunto di 11 caratteri), e ad esso il server risponde immediatamente con un
pacchetto vuoto di ricevuto. Poi tocca al server riscrivere indietro quanto gli è stato inviato, per
cui sarà lui a mandare indietro un terzo pacchetto con lo stesso contenuto appena ricevuto, e a
sua volta riceverà dal client un ACK nel quarto pacchetto. Questo causerà la ricezione dell’eco
nel client che lo stamperà a video.
A questo punto noi procediamo ad interrompere l’esecuzione del server con un C-c (cioè
con l’invio di SIGTERM): nel momento in cui facciamo questo vengono immediatamente generati
31
in realtà si è ridotta la lunghezza dell’output rispetto al reale tagliando alcuni dati non necessari alla
comprensione del flusso.
548 CAPITOLO 16. I SOCKET TCP
altri due pacchetti. La terminazione del processo infatti comporta la chiusura di tutti i suoi file
descriptor, il che comporta, per il socket che avevamo aperto, l’inizio della sequenza di chiusura
illustrata in sez. 16.1.3. Questo significa che dal server partirà un FIN, che è appunto il primo dei
due pacchetti, contraddistinto dalla lettera F, cui seguirà al solito un ACK da parte del client.
A questo punto la connessione dalla parte del server è chiusa, ed infatti se usiamo netstat
per controllarne lo stato otterremo che sul server si ha:
cioè essa è andata nello stato FIN_WAIT2, che indica l’avvenuta emissione del segmento FIN,
mentre sul client otterremo che essa è andata nello stato CLOSE_WAIT:
Il problema è che in questo momento il client è bloccato dentro la funzione ClientEcho nella
chiamata a fgets, e sta attendendo dell’input dal terminale, per cui non è in grado di accorgersi
di nulla. Solo quando inseriremo la seconda riga il comando uscirà da fgets e proverà a scriverla
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,
contraddistinto dalla lettera R, che causa la conclusione definitiva della connessione anche nel
client, dove non comparirà più nell’output di netstat.
Come abbiamo accennato in sez. 16.1.3 e come vedremo più avanti in sez. 16.6.3 la chiusura
di un solo capo di un socket è una operazione lecita, per cui la nostra scrittura avrà comunque
successo (come si può constatare lanciando usando strace32 ), in quanto il nostro programma
non ha a questo punto alcun modo di sapere che dall’altra parte non c’è più nessuno processo
in grado di leggere quanto scriverà. Questo sarà chiaro solo dopo il tentativo di scrittura, e la
ricezione del segmento RST di risposta che indica che dall’altra parte non si è semplicemente
chiuso un capo del socket, ma è completamente terminato il programma.
Per questo motivo il nostro client proseguirà leggendo dal socket, e dato che questo è stato
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 effetto (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
programma.
Per capire come questa avvenga comunque, non avendo inserito nel codice nessun controllo
di errore, occorre ricordare che, a parte la bidirezionalità del flusso dei dati, dal punto di vista del
funzionamento nei confronti delle funzioni di lettura e scrittura, i socket sono del tutto analoghi
a delle pipe. Allora, da quanto illustrato in sez. 11.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
32
il comando strace è un comando di debug molto utile che prende come argomento un altro comando e ne
stampa a video tutte le invocazioni di una system call, coi relativi argomenti e valori di ritorno, per cui usandolo
in questo contesto potremo verificare che effettivamente la write ha scritto la riga, che in effetti è stata pure
trasmessa via rete.
16.5. I VARI SCENARI CRITICI 549
di SIGPIPE, e questo è esattamente quello che avviene in questo caso, e siccome non abbiamo
un gestore per questo segnale, viene eseguita l’azione preimpostata, che è quella di terminare il
processo.
Per gestire in maniera più corretta questo tipo di evento dovremo allora modificare il nostro
client perché sia in grado di trattare le varie tipologie di errore, per questo dovremo riscrivere
la funzione ClientEcho, in modo da controllare gli stati di uscita delle varie chiamate. Si è
riportata la nuova versione della funzione in fig. 16.20.
Figura 16.20: La sezione nel codice della seconda versione della funzione ClientEcho usata dal client per il
servizio echo modificata per tener conto degli eventuali errori.
Come si può vedere in questo caso si controlla il valore di ritorno di tutte le funzioni, ed
inoltre si verifica la presenza di un eventuale end of file in caso di lettura. Con questa modifica
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 fin quando
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 file, e solo alle successive scritture
si potrà ottenere un errore.
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 fintanto che è bloccato nella lettura del
550 CAPITOLO 16. I SOCKET TCP
terminale fatta con gets. In questo caso il problema è minimo, ma esso riemergerà più avanti, ed
è quello che si deve affrontare tutte le volte quando si ha a che fare con la necessità di lavorare
con più descrittori, nel qual caso diventa si pone la questione di come fare a non restare bloccati
su un socket quando altri potrebbero essere liberi. Vedremo come affrontare questa problematica
in sez. 16.6.
Quello che succede in questo è che il programma, dopo aver scritto la seconda riga, resta
bloccato per un tempo molto lungo, prima di dare l’errore EHOSTUNREACH. Se andiamo ad osser-
vare con strace cosa accade nel periodo in cui il programma è bloccato vedremo che stavolta,
a differenza del caso precedente, il programma è bloccato nella lettura dal socket.
Se poi, come nel caso precedente, usiamo l’accortezza di analizzare il traffico di rete fra client
e server con tcpdump, otterremo il seguente risultato:
In questo caso l’andamento dei primi sette pacchetti è esattamente lo stesso di prima. Solo
che stavolta, non appena inviata la seconda riga, il programma si bloccherà nella successiva
chiamata a read, non ottenendo nessuna risposta. Quello che succede è che nel frattempo il
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 finale qui dipende dall’implementazione dello stack TCP, e nel caso di Linux an-
che dall’impostazione 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 specifica 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,
ma nel nostro caso le cose sono andate diversamente, dato che le ritrasmissioni registrate da
tcpdump sono solo 8; inoltre l’errore riportato all’uscita del client non è stato ETIMEDOUT, come
dovrebbe essere in questo caso, ma EHOSTUNREACH.
Per capire l’accaduto continuiamo ad analizzare l’output 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 l’indirizzo del server.
Come abbiamo accennato in sez. 14.3.1 ARP è il protocollo che si incarica di trovare le
corrispondenze fra indirizzo IP e indirizzo hardware sulla scheda di rete. È evidente allora che
nel nostro caso, essendo client e server sulla stessa rete, è scaduta la voce nella ARP cache 34
relativa ad anarres, ed il nostro client ha iniziato ad effettuare richieste ARP sulla rete per
sapere l’IP di quest’ultimo, 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 è
interrotto, e l’errore riportato al programma a questo punto è stato EHOSTUNREACH, in quanto
non si era più in grado di contattare il server.
Un altro errore possibile in questo tipo di situazione, che si può avere quando la macchina
è su una rete remota, è ENETUNREACH; esso viene riportato alla ricezione di un pacchetto ICMP
di destination unreachable da parte del router che individua l’interruzione della connessione. Di
nuovo anche qui il risultato finale dipende da quale è il meccanismo più veloce ad accorgersi del
problema.
Se però agiamo sui parametri del kernel, e scriviamo in tcp_retries2 un valore di tentativi
più basso, possiamo evitare la scadenza della ARP cache e vedere cosa succede. Cosı̀ se ad
esempio richiediamo 4 tentativi di ritrasmissione, l’analisi di tcpdump ci riporterà il seguente
scambio di pacchetti:
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 effetti, dopo un tempo
molto più breve rispetto a prima ed in corrispondenza dell’invio dell’ultimo tentativo, quello che
otterremo come errore all’uscita del client sarà diverso, e cioè:
e l’errore ricevuti da read stavolta è ECONNRESET. Se al solito riportiamo l’analisi dei pacchetti
effettuata con tcpdump, avremo:
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
server, il client è del tutto ignaro dell’accaduto per cui quando effettuerà una scrittura, dato che
la macchina server è stata riavviata e che tutti gli stati relativi alle precedenti connessioni sono
completamente persi, anche in presenza di una nuova istanza del server echo non sarà possibile
16.6. L’USO DELL’I/O MULTIPLEXING 553
consegnare i dati in arrivo, per cui alla loro ricezione il kernel risponderà con un segmento di
RST.
Il client da parte sua, dato che neanche in questo caso non è stato emesso un FIN, dopo aver
scritto verrà bloccato nella successiva chiamata a read, che però adesso ritornerà immediata-
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 effettuando uno scambio di dati, è possibile usare una impostazione speciale del socket
(ci torneremo in sez. 17.2.2) che provvede all’esecuzione di questo controllo.
• nel buffer di ricezione del socket sono arrivati dei dati in quantità sufficiente a superare il
valore di una soglia di basso livello (il cosiddetto low watermark ). Questo valore è espresso
in numero di byte e può essere impostato con l’opzione del socket SO_RCVLOWAT (tratteremo
l’uso di questa opzione in sez. 17.2.2); il suo valore di default è 1 per i socket TCP e UDP. In
questo caso una operazione di lettura avrà successo e leggerà un numero di byte maggiore
di zero.
• il lato in lettura della connessione è stato chiuso; si è cioè ricevuto un segmento FIN (si
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
un valore nullo) per indicare la condizione di end-of-file.
• c’è stato un errore sul socket. In questo caso una operazione di lettura non si bloccherà ma
restituirà una condizione di errore (ad esempio read restituirà -1) e imposterà la variabile
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 l’opzione SO_ERROR.
554 CAPITOLO 16. I SOCKET TCP
Le condizioni che fanno si che la funzione select ritorni segnalando che un socket (che sarà
riportato nel secondo insieme di file descriptor) è pronto per la scrittura sono le seguenti:
• nel buffer di invio è disponibile una quantità di spazio superiore al valore della soglia di
basso livello in scrittura ed inoltre o il socket è già connesso o non necessita (ad esempio
è UDP) di connessione. Il valore della soglia è espresso in numero di byte e può essere
impostato con l’opzione 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
si bloccherà e restituirà un valore positivo pari al numero di byte accettati dal livello di
trasporto.
• il lato in scrittura della connessione è stato chiuso. In questo caso una operazione di
scrittura sul socket genererà il segnale SIGPIPE.
• c’è stato un errore sul socket. In questo caso una operazione di scrittura non si bloccherà
ma restituirà una condizione di errore ed imposterà opportunamente la variabile errno.
Vedremo in sez. 17.2.2 come sia possibile estrarre e cancellare errori pendenti su un socket
usando l’opzione SO_ERROR.
Infine c’è una sola condizione che fa si che select ritorni segnalando che un socket (che sarà
riportato nel terzo insieme di file descriptor) ha una condizione di eccezione pendente, e cioè la
ricezione sul socket di dati urgenti (o out-of-band ), una caratteristica specifica 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 l’I/O di dati come il riconoscimento della presenza di connessioni pronte,
in modo da consentire anche l’utilizzo di accept in modalità non bloccante. Si noti infine come
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 buffer di ricezione e di invio è quello di consentire
maggiore flessibilità nell’uso di select da parte dei programmi, se infatti si sa che una applica-
zione non è in grado di fare niente fintanto che non può ricevere o inviare una certa quantità di
dati, si possono utilizzare questi valori per far si che select ritorni solo quando c’è la certezza
di avere dati a sufficienza.36
Nel nostro caso quello che ci interessa è non essere bloccati in lettura sullo standard input
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 file 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
(nell’ultimo caso anche in scrittura, ma questo non è necessario ai nostri scopi).
Figura 16.21: La sezione nel codice della terza versione della funzione ClientEcho usata dal client per il servizio
echo modificata per l’uso di select.
Riprendiamo allora il codice del client, modificandolo per l’uso di select. Quello che dob-
biamo modificare è la funzione ClientEcho di fig. 16.20, dato che tutto il resto, che riguarda
le modalità in cui viene stabilita la connessione con il server, resta assolutamente identico. La
556 CAPITOLO 16. I SOCKET TCP
nostra nuova versione di ClientEcho, la terza della serie, è riportata in fig. 16.21, il codice
completo si trova nel file TCP_echo_third.c dei sorgenti allegati alla guida.
In questo caso la funzione comincia (8-9) con l’azzeramento del file descriptor set fset
e l’impostazione del valore maxfd, da passare a select come massimo per il numero di file
descriptor. Per determinare quest’ultimo si usa la macro max definita nel nostro file macro.h che
raccoglie una collezione di macro di preprocessore di varia utilità.
La funzione prosegue poi (10-41) con il ciclo principale, che viene ripetuto indefinitamente.
Per ogni ciclo si reinizializza (11-12) il file descriptor set, impostando i valori per il file 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
modifica dei due bit relativi, che quindi devono essere reimpostati all’inizio 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 file descriptor set per il controllo
dell’attività in lettura, negli altri argomenti sono passati tutti puntatori nulli, non interessando
né il controllo delle altre attività, né l’impostazione di un valore di timeout.
Al ritorno di select si provvede a controllare quale dei due file descriptor presenta attività
in lettura, cominciando (14-24) con il file descriptor associato allo standard input. In caso di
attività (quando cioè FD_ISSET ritorna una valore diverso da zero) si esegue (15) una fgets
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 significa che si è chiuso lo
standard input e quindi concluso l’utilizzo 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
(27-30) si è avuto un errore e pertanto si esce immediatamente segnalandolo, se è nullo (31-
34) significa che il server ha chiuso la connessione, e di nuovo si esce con stampando prima un
messaggio di avviso, altrimenti (35-39) si effettua 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
dei due file descriptor posti sotto controllo. Questo di norma avviene solo quando si è scritto
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
select ritornerà (come illustrato in sez. 16.6.1) segnalando una condizione di end of file, per cui
il nostro client potrà uscire immediatamente.
Riprendiamo la situazione affrontata in sez. 16.5.2, terminando il server durante una con-
nessione, in questo caso quello che otterremo, una volta scritta una prima riga ed interrotto il
server con un C-c, sarà:
[piccardi@gont sources]$ ./echo 192.168.1.1
Prima riga
Prima riga
EOF sul socket
dove l’ultima riga compare immediatamente dopo aver interrotto il server. Il nostro client in-
fatti è in grado di accorgersi immediatamente che il socket connesso è stato chiuso ed uscire
immediatamente.
Veniamo allora agli altri scenari di terminazione anomala visti in sez. 16.5.3. Il primo di
questi è l’interruzione fisica della connessione; in questo caso avremo un comportamento analogo
al precedente, in cui si scrive una riga e non si riceve risposta dal server e non succede niente
fino a quando non si riceve un errore di EHOSTUNREACH o ETIMEDOUT a seconda dei casi.
16.6. L’USO DELL’I/O MULTIPLEXING 557
La differenza è che stavolta potremo scrivere più righe dopo l’interruzione, in quanto il nostro
client dopo aver inviato i dati non si bloccherà più nella lettura dal socket, ma nella select; per
questo potrà accettare ulteriore dati che scriverà di nuovo sul socket, fintanto che c’è spazio sul
buffer di uscita (ecceduto il quale si bloccherà in scrittura). Si ricordi infatti che il client non
ha modo di determinare se la connessione è attiva o meno (dato che in molte situazioni reali
l’inattività può essere temporanea). Tra l’altro se si ricollega la rete prima della scadenza del
timeout, potremo anche verificare come tutto quello che si era scritto viene poi effettivamente
trasmesso non appena la connessione ridiventa attiva, per cui otterremo qualcosa del tipo:
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
di niente dato che si suppone che il programma server non venga terminato correttamente,
ma si blocchi tutto senza la possibilità di avere l’emissione di un segmento FIN che segnala
la terminazione della connessione. Di nuovo fintanto che la connessione non si riattiva (con il
riavvio della macchina del server) il client non è in grado di fare altro che accettare dell’input e
tentare di inviarlo. La differenza in questo caso è che non appena la connessione ridiventa attiva
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à
il ritorno di select per la ricezione di un errore di ECONNRESET.
#include <sys/socket.h>
int shutdown(int sockfd, int how)
Chiude un lato della connessione fra due socket.
La funzione restituisce zero in caso di successo e -1 per un errore, nel qual caso errno assumerà i
valori:
ENOTSOCK il file descriptor non corrisponde a un socket.
ENOTCONN il socket non è connesso.
ed inoltre EBADF.
La funzione prende come primo argomento il socket sockfd su cui si vuole operare e come se-
condo argomento un valore intero how che indica la modalità di chiusura del socket, quest’ultima
può prendere soltanto tre valori:
SHUT_RD chiude il lato in lettura del socket, non sarà più possibile leggere dati da esso, tutti
gli eventuali dati trasmessi dall’altro capo del socket saranno automaticamente
scartati dal kernel, che, in caso di socket TCP, provvederà comunque ad inviare i
relativi segmenti di ACK.
SHUT_WR chiude il lato in scrittura del socket, non sarà più possibile scrivere dati su di esso.
Nel caso di socket TCP la chiamata causa l’emissione di un segmento FIN, secondo
la procedura chiamata half-close. Tutti i dati presenti nel buffer di scrittura prima
della chiamata saranno inviati, seguiti dalla sequenza di chiusura illustrata in
sez. 16.1.3.
SHUT_RDWR chiude sia il lato in lettura che quello in scrittura del socket. È equivalente alla
chiamata in sequenza con SHUT_RD e SHUT_WR.
Ci si può chiedere quale sia l’utilità di avere introdotto SHUT_RDWR quando questa sembra
rendere shutdown del tutto equivalente ad una close. In realtà non è cosı̀, esiste infatti un’altra
differenza con close, più sottile. Finora infatti non ci siamo presi la briga di sottolineare in
maniera esplicita che, come per i file e le fifo, anche per i socket possono esserci più riferimenti
contemporanei ad uno stesso socket. Per cui si avrebbe potuto avere l’impressione che sia una
corrispondenza univoca fra un socket ed il file descriptor con cui vi si accede. Questo non è
assolutamente vero, (e lo abbiamo già visto nel codice del server di fig. 16.13), ed è invece
assolutamente normale che, come per gli altri oggetti, ci possano essere più file descriptor che
fanno riferimento allo stesso socket.
Allora se avviene uno di questi casi quello che succederà è che la chiamata a close darà effet-
tivamente avvio alla sequenza di chiusura di un socket soltanto quando il numero di riferimenti
a quest’ultimo diventerà nullo. Fintanto che ci sono file descriptor che fanno riferimento ad un
socket l’uso di close si limiterà a deallocare nel processo corrente il file descriptor utilizzato,
ma il socket resterà pienamente accessibile attraverso tutti gli altri riferimenti. Se torniamo al-
l’esempio originale del server di fig. 16.13 abbiamo infatti che ci sono due close, una sul socket
connesso nel padre, ed una sul socket in ascolto nel figlio, ma queste non effettuano nessuna
chiusura reale di detti socket, dato che restano altri riferimenti attivi, uno al socket connesso nel
figlio ed uno a quello in ascolto nel padre.
Questo non avviene affatto se si usa shutdown con argomento SHUT_RDWR al posto di close; in
questo caso infatti la chiusura del socket viene effettuata immediatamente, indipendentemente
dalla presenza di altri riferimenti attivi, e pertanto sarà efficace anche per tutti gli altri file
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,
per segnalare all’altro capo della connessione che si è concluso l’invio dei dati, restando comunque
in grado di ricevere quanto questi potrà ancora inviarci. Questo è ad esempio l’uso che ci serve per
rendere finalmente completo il nostro esempio sul servizio echo. Il nostro client infatti presenta
16.6. L’USO DELL’I/O MULTIPLEXING 559
ancora un problema, che nell’uso che finora ne abbiamo fatto non è emerso, ma che ci aspetta
dietro l’angolo non appena usciamo dall’uso 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 file copia risulta mancare della parte finale.
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
del file ../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
remota occorre un certo tempo perché i pacchetti vi arrivino, vengano processati, e poi tornino
indietro. Considerando trascurabile il tempo di processo, questo tempo è quello impiegato nella
trasmissione via rete, che viene detto RTT (dalla denominazione inglese Round Trip Time) ed
è quello che viene stimato con l’uso del comando ping.
A questo punto, se torniamo al codice mostrato in fig. 16.21, possiamo vedere che mentre
i pacchetti sono in transito sulla rete il client continua a leggere e a scrivere fintanto che il
file in ingresso finisce. Però non appena viene ricevuto un end-of-file in ingresso il nostro client
termina. Nel caso interattivo, in cui si inviavano brevi stringhe una alla volta, c’era 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
ancora pacchetti in transito sulla rete che devono arrivare al server e poi tornare indietro, ma
siccome il client esce immediatamente dopo la fine del file 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 file
in ingresso, occorre usare shutdown per effettuare la chiusura del lato in scrittura del socket. In
questo modo il client segnalerà al server la chiusura del flusso dei dati, ma potrà continuare a
leggere quanto il server gli sta ancora inviando indietro, fino a quando anch’esso, riconosciuta la
chiusura del socket in scrittura da parte del client, effettuerà la chiusura dalla sua parte. Solo alla
ricezione della chiusura del socket da parte del server il client potrà essere sicuro della ricezione
di tutti i dati e della terminazione effettiva della connessione.
Si è allora riportato in fig. 16.22 la versione finale della nostra funzione ClientEcho, in grado
di gestire correttamente l’intero flusso 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 file TCP_echo_fourth.c, distribuito coi sorgenti allegati alla guida.
La nuova versione è molto simile alla precedente di fig. 16.21; la prima differenza è l’intro-
duzione (7) della variabile eof, inizializzata ad un valore nullo, che serve a mantenere traccia
dell’avvenuta conclusione della lettura del file in ingresso.
La seconda modifica (12-15) è stata quella di rendere subordinato ad un valore nullo di eof
l’impostazione del file descriptor set per l’osservazione dello standard input. Se infatti il valore
di eof è non nullo significa che si è già raggiunta la fine del file in ingresso ed è pertanto inutile
continuare a tenere sotto controllo lo standard input nella successiva (16) chiamata a select.
Le maggiori modifiche rispetto alla precedente versione sono invece nella gestione (18-22)
del caso in cui la lettura con fgets restituisce un valore nullo, indice della fine del file. Questa
nella precedente versione causava l’immediato ritorno della funzione; in questo caso prima (19)
si imposta opportunamente eof ad un valore non nullo, dopo di che (20) si effettua la chiusura
del lato in scrittura del socket con shutdown. Infine (21) si usa la macro FD_CLR per togliere lo
standard input dal file descriptor set.
In questo modo anche se la lettura del file in ingresso è conclusa, la funzione non esce dal
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.
560 CAPITOLO 16. I SOCKET TCP
Figura 16.22: La sezione nel codice della versione finale 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 all’interno (30-49) della lettura dei dati dal socket; se infatti dalla lettura del socket si
riceve una condizione di end-of-file, la si tratterà (36-43) in maniera diversa a seconda del valore
16.6. L’USO DELL’I/O MULTIPLEXING 561
di eof. Se infatti questa è diversa da zero (37-39), essendo stata completata la lettura del file in
ingresso, vorrà dire che anche il server ha concluso la trasmissione dei dati restanti, e si potrà
uscire senza errori, altrimenti si stamperà (40-42) un messaggio di errore per la chiusura precoce
della connessione.
Figura 16.23: Schema del nuovo server echo basato sull’I/O multiplexing.
La sezione principale del codice del nuovo server è illustrata in fig. 16.24. Si è tralasciata
al solito la gestione delle opzioni, che è identica alla versione precedente. Resta invariata anche
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
in sez. 16.4.3); al solito il codice completo del server è disponibile coi sorgenti allegati nel file
select_echod.c.
In questo caso, una volta aperto e messo in ascolto il socket, tutto quello che ci servirà sarà
chiamare select per rilevare la presenza di nuove connessioni o di dati in arrivo, e processarli
immediatamente. Per implementare lo schema mostrato in fig. 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 file descriptor aperti.
38
ne faremo comunque una implementazione diversa rispetto a quella presentata da Stevens in [2].
562 CAPITOLO 16. I SOCKET TCP
1 ...
2 memset ( fd_open , 0 , FD_SETSIZE ); /* clear array of open files */
3 max_fd = list_fd ; /* maximum now is listening socket */
4 fd_open [ max_fd ] = 1;
5 /* main loop , wait for connection and data inside a select */
6 while (1) {
7 FD_ZERO (& fset ); /* clear fd_set */
8 for ( i = list_fd ; i <= max_fd ; i ++) { /* initialize fd_set */
9 if ( fd_open [ i ] != 0) FD_SET (i , & fset );
10 }
11 while ( (( n = select ( max_fd + 1 , & fset , NULL , NULL , NULL )) < 0)
12 && ( errno == EINTR )); /* wait for data or connection */
13 if ( n < 0) { /* on real error exit */
14 PrintErr ( " select error " );
15 exit (1);
16 }
17 if ( FD_ISSET ( list_fd , & fset )) { /* if new connection */
18 n - -; /* decrement active */
19 len = sizeof ( c_addr ); /* and call accept */
20 if (( fd = accept ( list_fd , ( struct sockaddr *)& c_addr , & len )) < 0) {
21 PrintErr ( " accept error " );
22 exit (1);
23 }
24 fd_open [ fd ] = 1; /* set new connection socket */
25 if ( max_fd < fd ) max_fd = fd ; /* if needed set new maximum */
26 }
27 /* loop on open connections */
28 i = list_fd ; /* first socket to look */
29 while ( n != 0) { /* loop until active */
30 i ++; /* start after listening socket */
31 if ( fd_open [ i ] == 0) continue ; /* closed , go next */
32 if ( FD_ISSET (i , & fset )) { /* if active process it */
33 n - -; /* decrease active */
34 nread = read (i , buffer , MAXLINE ); /* read operations */
35 if ( nread < 0) {
36 PrintErr ( " Errore in lettura " );
37 exit (1);
38 }
39 if ( nread == 0) { /* if closed connection */
40 close ( i ); /* close file */
41 fd_open [ i ] = 0; /* mark as closed in table */
42 if ( max_fd == i ) { /* if was the maximum */
43 while ( fd_open [ - - i ] == 0); /* loop down */
44 max_fd = i ; /* set new maximum */
45 break ; /* and go back to select */
46 }
47 continue ; /* continue loop on open */
48 }
49 nwrite = FullWrite (i , buffer , nread ); /* write data */
50 if ( nwrite ) {
51 PrintErr ( " Errore in scrittura " );
52 exit (1);
53 }
54 }
55 }
56 }
57 ...
Figura 16.24: La sezione principale del codice della nuova versione di server echo basati sull’uso della funzione
select.
16.6. L’USO DELL’I/O MULTIPLEXING 563
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 file descriptor non è aperto),
mentre il valore massimo (3) per i file descriptor aperti viene impostato a quello del socket in
ascolto,39 che verrà anche (4) inserito nella tabella.
La prima sezione (7-10) del ciclo principale esegue la costruzione del file descriptor set fset
in base ai socket connessi in un certo momento; all’inizio ci sarà soltanto il socket in ascolto, ma
nel prosieguo delle operazioni, verranno utilizzati anche tutti i socket connessi registrati nella
tabella fd_open. Dato che la chiamata di select modifica il valore del file descriptor set, è
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 file descriptor, descritta in sez. 6.2.1, per cui il kernel
associa sempre ad ogni nuovo file il file descriptor con il valore più basso disponibile. Questo fa sı̀
che si possa eseguire il ciclo (8) a partire da un valore minimo, che sarà sempre quello del socket
in ascolto, mantenuto in list_fd, fino al valore massimo di max_fd che dovremo aver cura di
tenere aggiornato. Dopo di che basterà controllare (9) nella nostra tabella se il file descriptor è
in uso o meno,40 e impostare fset di conseguenza.
Una volta inizializzato con i socket aperti il nostro file 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 l’unica funzione che
può bloccarsi, ed essere interrotta da un segnale, la eseguiremo (11-12) all’interno di un ciclo di
while che la ripete indefinitamente 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 specifico si danno due possibili casi diversi per cui select può essere ritornata: o si è
ricevuta una nuova connessione ed è pronto il socket in ascolto, sul quale si può eseguire accept
o c’è attività su uno dei socket connessi, sui quali si può eseguire read.
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
controllare per interruzioni dovute a segnali, in quanto siamo sicuri che accept non si bloccherà.
Per completare la trattazione occorre a questo punto aggiungere (24) il nuovo file descriptor alla
tabella di quelli connessi, ed inoltre, se è il caso, aggiornare (25) il valore massimo in max_fd.
Una volta controllato l’arrivo di nuove connessioni si passa a verificare se vi sono dati sui
socket connessi, per questo si ripete un ciclo (29-55) fintanto che il numero di socket attivi n
resta diverso da zero; in questo modo se l’unico socket con attività era quello connesso, avendo
opportunamente decrementato il contatore, il ciclo verrà saltato, e si ritornerà immediatamente
(ripetuta l’inizializzazione del file 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
n non sarà nullo ed il controllo sarà eseguito. Prima di entrare nel ciclo comunque si inizializza
(28) il valore della variabile i che useremo come indice nella tabella fd_open al valore minimo,
corrispondente al file descriptor del socket in ascolto.
Il primo passo (30) nella verifica è incrementare il valore dell’indice i per posizionarsi sul
primo valore possibile per un file descriptor associato ad un eventuale socket connesso, dopo di
che si controlla (31) se questo è nella tabella dei socket connessi, chiedendo la ripetizione del
ciclo in caso contrario. Altrimenti si passa a verificare (32) se il file descriptor corrisponde ad
39
in quanto esso è l’unico file aperto, oltre i tre standard, e pertanto avrà il valore più alto.
40
si tenga presente che benché il kernel assegni sempre il primo valore libero, dato che nelle operazioni i socket
saranno aperti e chiusi in corrispondenza della creazione e conclusione delle connessioni, si potranno sempre avere
dei buchi nella nostra tabella.
564 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-file, indice
che una connessione che si è chiusa, che deve essere trattato (39-48) opportunamente. Il primo
passo è chiudere (40) anche il proprio capo del socket e rimuovere (41) il file descriptor dalla
tabella di quelli aperti, inoltre occorre verificare (42) se il file descriptor chiuso è quello con il
valore più alto, nel qual caso occorre trovare (42-46) il nuovo massimo, altrimenti (47) si può
ripetere il ciclo da capo per esaminare (se ne restano) ulteriori file descriptor attivi.
Se però è stato chiuso il file descriptor più alto, dato che la scansione dei file descriptor
attivi viene fatta a partire dal valore più basso, questo significa che siamo anche arrivati alla fine
della scansione, per questo possiamo utilizzare direttamente il valore dell’indice i con un ciclo
all’indietro (43) che trova il primo valore per cui la tabella presenta un file descriptor aperto, e
lo imposta (44) come nuovo massimo, per poi tornare (44) al ciclo principale con un break, e
rieseguire select.
Se infine si sono effettivamente letti dei dati dal socket (ultimo caso rimasto) si potrà invocare
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 fig. 16.18) in cui
si continuava a leggere fintanto che non si riceveva un end-of-file, questo perché usando l’I/O
multiplexing non si vuole essere bloccati in lettura. L’uso di select ci permette di trattare
automaticamente anche il caso in cui la read non è stata in grado di leggere tutti i dati presenti
sul socket, dato che alla iterazione successiva select ritornerà immediatamente segnalando
l’ulteriore disponibilità.
Il nostro server comunque soffre di una vulnerabilità per un attacco di tipo Denial of Service.
Il problema è che in caso di blocco di una qualunque delle funzioni di I/O, non avendo usato
processi separati, tutto il server si ferma e non risponde più a nessuna richiesta. Abbiamo scon-
giurato questa evenienza per l’I/O in ingresso con l’uso di select, ma non vale altrettanto per
l’I/O in uscita. Il problema pertanto può sorgere qualora una delle chiamate a write effettuate
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 l’eco del server, si potrebbe giungere alla
saturazione del buffer 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 all’interno 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 buon fine.
• i dati inviati su un socket vengono considerati traffico normale, pertanto vengono rilevati
alla loro ricezione sull’altro capo da una selezione effettuata con POLLIN o POLLRDNORM;
• i dati urgenti out-of-band (vedi sez. 19.1.3) su un socket TCP vengono considerati traffico
prioritario e vengono rilevati da una condizione POLLIN, POLLPRI o POLLRDBAND.
• la disponibilità di spazio sul socket per la scrittura di dati viene segnalata con una condi-
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 traffico normale, ma viene segnalata anche dalla condizione POLLERR.
• la presenza di una nuova connessione su un socket in ascolto può essere considerata sia
traffico normale che prioritario, nel caso di Linux l’implementazione la classifica come
normale.
Come esempio dell’uso di poll proviamo allora a reimplementare il server echo secondo
lo schema di fig. 16.23 usando poll al posto di select. In questo caso dovremo fare qualche
modifica, per tenere conto della diversa sintassi delle due funzioni, ma la struttura del programma
resta sostanzialmente la stessa.
In fig. 16.25 è riportata la sezione principale della nuova versione del server, la versione
completa del codice è riportata nel file poll_echod.c dei sorgenti allegati alla guida. Al solito
nella figura 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
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 l’opzione -n ed ha un valore di default di 256.
Dopo di che si preimposta (3) il valore max_fd del file descriptor aperto con valore più
alto a quello del socket in ascolto (al momento l’unico), e si provvede (4-7) ad inizializzare le
strutture, disabilitando (5) l’osservazione con un valore negativo del campo fd ma predisponendo
(6) il campo events per l’osservazione dei dati normali con POLLRDNORM. Infine (8) si attiva
l’osservazione del socket in ascolto inizializzando la corrispondente struttura. Questo metodo
comporta, in modalità interattiva, lo spreco di tre strutture (quelle relative a standard input,
output ed error) che non vengono mai utilizzate in quanto la prima è sempre quella relativa al
socket in ascolto.
Una volta completata l’inizializzazione tutto il lavoro viene svolto all’interno del ciclo prin-
cipale 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 all’interno di
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 file 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 dell’attività su un file descriptor si hanno di nuovo due possibilità. La
prima possibilità è che ci sia attività sul socket in ascolto, indice di una nuova connessione,
566 CAPITOLO 16. I SOCKET TCP
Figura 16.25: La sezione principale del codice della nuova versione di server echo basati sull’uso della funzione
poll.
16.6. L’USO DELL’I/O MULTIPLEXING 567
nel qual caso si controlla (17) se il campo revents della relativa struttura è attivo; se è cosı̀ si
provvede (18) a decrementare la variabile n (che assume il significato di numero di file descriptor
attivi rimasti da controllare) per poi (19-23) effettuare 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 file descriptor da essa ottenuto, modificando (24) infine quando
necessario il valore massimo dei file descriptor aperti mantenuto in max_fd.
La seconda possibilità è che vi sia dell’attività su uno dei socket aperti in precedenza, nel
qual caso si inizializza (27) l’indice 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 fintanto che il numero di file descriptor attivi, mantenuto nella variabile n, è diverso
da zero. Se pertanto ci sono ancora socket attivi da individuare si comincia con l’incrementare
(30) l’indice e controllare (31) se corrisponde ad un file descriptor in uso analizzando il valore
del campo fd della relativa struttura e chiudendo immediatamente il ciclo qualora non lo sia.
Se invece il file descriptor è in uso si verifica (31) se c’è stata attività controllando il campo
revents.
Di nuovo se non si verifica la presenza di attività il ciclo si chiude subito, altrimenti si
provvederà (32) a decrementare il numero n di file descriptor attivi da controllare e ad eseguire
(33) la lettura, ed in caso di errore (34-37) al solito lo si notificherà uscendo immediatamente.
Qualora invece si ottenga una condizione di end-of-file (38-47) si provvederà a chiudere (39) anche
il nostro capo del socket e a marcarlo (40) nella struttura ad esso associata come inutilizzato.
Infine dovrà essere ricalcolato (41-45) un eventuale nuovo valore di max_fd. L’ultimo passo è
(46) chiudere il ciclo in quanto in questo caso non c’è più niente da riscrivere all’indietro sul
socket.
Se invece si sono letti dei dati si provvede (48) ad effettuarne la riscrittura all’indietro, con
il solito controllo ed eventuale uscita e notifica in caso si errore (49-52).
Come si può notare la logica del programma è identica a quella vista in fig. 16.24 per l’analogo
server basato su select; la sola differenza significativa è che in questo caso non c’è bisogno di
rigenerare i file descriptor set in quanto l’uscita è indipendente dai dati in ingresso. Si applicano
comunque anche a questo server le considerazioni finali di sez. 16.6.4.
Esamineremo in questo capitolo una serie di funzionalità aggiuntive relative alla gestione dei
socket, come la gestione della risoluzione di nomi e indirizzi, le impostazioni delle varie pro-
prietà ed opzioni relative ai socket, e le funzioni di controllo che permettono di modificarne il
comportamento.
La risoluzione dei nomi è associata tradizionalmente al servizio del Domain Name Service che
permette di identificare 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
servizio, mentre nel nostro caso affronteremo 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 è l’unica corrispondenza possibile fra
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 identificatori numerici; per quanto riguarda però tutti i nomi
associati a identificativi o servizi relativi alla rete il servizio di risoluzione è gestito in maniera
unificata da un insieme di funzioni fornite con le librerie del C, detto appunto resolver.
Lo schema di funzionamento del resolver è illustrato in fig. 17.1; in sostanza i programmi
hanno a disposizione un insieme di funzioni di libreria con cui chiamano il resolver, indicate con le
frecce nere. Ricevuta la richiesta è quest’ultimo che, sulla base della sua configurazione, esegue
le operazioni necessarie a fornire la risposta, che possono essere la lettura delle informazioni
mantenute nei relativi dei file statici presenti sulla macchina, una interrogazione ad un DNS
1
non staremo ad entrare nei dettagli della definizione di cosa è un nome a dominio, dandolo per noto, una intro-
duzione alla problematica si trova in [3] (cap. 9) mentre per una trattazione approfondita di tutte le problematiche
relative al DNS si può fare riferimento a [16].
569
570 CAPITOLO 17. LA GESTIONE DEI SOCKET
(che a sua volta, per il funzionamento del protocollo, può interrogarne altri) o la richiesta ad
altri server per i quali sia fornito il supporto, come LDAP.2
La configurazione del resolver attiene più alla amministrazione di sistema che alla program-
mazione, ciò non di meno, prima di trattare le varie funzioni di librerie utilizzate dai program-
mi, vale la pena fare una panoramica generale. Originariamente la configurazione del resolver
riguardava esclusivamente le questioni relative alla gestione dei nomi a dominio, e prevedeva solo
l’utilizzo del DNS e del file statico /etc/hosts.
Per questo aspetto il file di configurazione principale del sistema è /etc/resolv.conf che
contiene in sostanza l’elenco degli indirizzi IP dei server DNS da contattare; a questo si affianca
il file /etc/host.conf il cui scopo principale è indicare l’ordine in cui eseguire la risoluzione dei
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 file, che si trovano nelle rispettive pagine
di manuale.
Con il tempo però è divenuto possibile fornire diversi sostituti per l’utilizzo delle associazione
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 definiti dal servizio NIS,3 o come quelli dei protocolli e dei
servizi che sono mantenuti nei file 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
il mantenimento di alcune di esse su opportuni server. Inoltre l’uso di diversi supporti possibili
per le stesse informazioni (ad esempio il nome delle macchine può essere mantenuto sia tramite
/etc/hosts, che con il DNS, che con NIS) comporta il problema dell’ordine in cui questi vengono
interrogati.4
Per risolvere questa serie di problemi la risoluzione dei nomi a dominio eseguirà dal resolver è
stata inclusa all’interno di un meccanismo generico per la risoluzione di corrispondenze fra nomi
2
la sigla LDAP fa riferimento ad un protocollo, il Lightweight Directory Access Protocol, che prevede un
meccanismo per la gestione di elenchi di informazioni via rete; il contenuto di un elenco può essere assolutamente
generico, e questo permette il mantenimento dei più vari tipi di informazioni su una infrastruttura di questo tipo.
3
il Network Information Service è un servizio, creato da Sun, e poi diffuso su tutte le piattaforme unix-like, che
permette di raggruppare all’interno di una rete (in quelli che appunto vengono chiamati netgroup) varie macchine,
centralizzando i servizi di definizione di utenti e gruppi e di autenticazione, oggi è sempre più spesso sostituito da
LDAP.
4
con le implementazioni classiche i vari supporti erano introdotti modificando direttamente le funzioni di
libreria, prevedendo un ordine di interrogazione predefinito e non modificabile (a meno di una ricompilazione delle
librerie stesse).
17.1. LA RISOLUZIONE DEI NOMI 571
ed informazioni ad essi associate chiamato Name Service Switch 5 cui abbiamo accennato anche
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 l’acronimo NSS) è un sistema di librerie dinamiche che
permette di definire in maniera generica sia i supporti su cui mantenere i dati di corrispondenza
fra nomi e valori numerici, sia l’ordine in cui effettuare le ricerche sui vari supporti disponibili. Il
sistema prevede una serie di possibili classi di corrispondenza, quelle attualmente definite sono
riportate in tab. 17.1.
Tabella 17.1: Le diverse classi di corrispondenze definite all’interno del Name Service Switch.
Il sistema del Name Service Switch è controllato dal contenuto del file /etc/nsswitch.conf;
questo contiene una riga6 di configurazione per ciascuna di queste classi, che viene inizia col
nome di tab. 17.1 seguito da un carattere “:” e prosegue con la lista dei servizi su cui le relative
informazioni sono raggiungibili, scritti nell’ordine in cui si vuole siano interrogati.
Ogni servizio è specificato a sua volta da un nome, come file, dns, db, ecc. che identifica
la libreria dinamica che realizza l’interfaccia con esso. Per ciascun servizio se NAME è il nome
utilizzato dentro /etc/nsswitch.conf, dovrà essere presente (usualmente in /lib) una libreria
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
mantenuti, e che si usino o meno funzionalità aggiuntive fornire dal sistema del Name Service
Switch, dal punto di vista di un programma che deve effettuare 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.
servizio DNS. Come accennato questo, benché in teoria sia solo uno dei possibili supporti su
cui mantenere le informazioni, in pratica costituisce il meccanismo principale con cui vengono
risolti i nomi a dominio. Per questo motivo esistono una serie di funzioni di libreria che servono
specificamente 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,
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 dell’albero dei file, 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 definite 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
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 sotto-dominio di terzo livello ad un altro
server ancora.
In realtà un server DNS è in grado di fare altro rispetto alla risoluzione di un nome a dominio
in un indirizzo IP; ciascuna voce nel database viene chiamata resource record, e può contenere
diverse informazioni. In genere i resource record vengono classificati 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.
L’esistenza di vari tipi di informazioni è un altro dei motivi per cui il resolver prevede,
rispetto a quelle relative alla semplice risoluzione dei nomi, un insieme di funzioni specifiche
dedicate all’interrogazione di un server DNS; la prima di queste funzioni è res_init, il cui
prototipo è:
#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 file di configurazione (i già citati resolv.conf e host.conf)
per impostare il dominio di default, gli indirizzi dei server DNS da contattare e l’ordine delle
ricerche; se non sono specificati server verrà utilizzato l’indirizzo locale, e se non è definito
un dominio di default sarà usato quello associato con l’indirizzo locale (ma questo può essere
sovrascritto con l’uso della variabile di ambiente LOCALDOMAIN). In genere non è necessario
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 definita in
resolv.h ed è utilizzata internamente alle funzioni essendo definita come variabile globale;
questo consente anche di accedervi direttamente all’interno di un qualunque programma, una
volta che la sia opportunamente dichiarata come:
extern struct state _res ;
8
per chi si stia chiedendo quale sia la radice di questo albero, cioè l’equivalente di “/”, la risposta è il dominio
speciale “.”, che in genere non viene mai scritto esplicitamente, ma che, come chiunque abbia configurato un server
DNS sa bene, esiste ed è gestito dai cosiddetti root DNS che risolvono i domini di primo livello.
9
ritroveremo classi di indirizzi e tipi di record più avanti in tab. 17.3 e tab. 17.4.
17.1. LA RISOLUZIONE DEI NOMI 573
Tutti i campi della struttura sono ad uso interno, e vengono usualmente inizializzati da
res_init in base al contenuto dei file di configurazione e ad una serie di valori di default. L’unico
campo che può essere utile modificare è _res.options, una maschera binaria che contiene una
serie di bit di opzione che permettono di controllare il comportamento del resolver.
Costante Significato
RES_INIT Viene attivato se è stata chiamata res_init.
RES_DEBUG Stampa dei messaggi di debug.
RES_AAONLY Accetta solo risposte autoritative.
RES_USEVC Usa connessioni TCP per contattare i server invece che
l’usuale UDP.
RES_PRIMARY Interroga soltanto server DNS primari.
RES_IGNTC Ignora gli errori di troncamento, non ritenta la richiesta
con una connessione TCP.
RES_RECURSE Imposta il bit che indica che si desidera eseguire una
interrogazione ricorsiva.
RES_DEFNAMES Se attivo res_search aggiunge il nome del dominio di
default ai nomi singoli (che non contengono cioè un “.”).
RES_STAYOPEN Usato con RES_USEVC per mantenere aperte le connessioni
TCP fra interrogazioni diverse.
RES_DNSRCH Se attivo res_search esegue le ricerche di nomi di
macchine nel dominio corrente o nei domini ad esso
sovrastanti.
RES_INSECURE1 Blocca i controlli di sicurezza di tipo 1.
RES_INSECURE2 Blocca i controlli di sicurezza di tipo 2.
RES_NOALIASES Blocca l’uso della variabile di ambiente HOSTALIASES.
RES_USE_INET6 Restituisce indirizzi IPv6 con gethostbyname.
RES_ROTATE Ruota la lista dei server DNS dopo ogni interrogazione.
RES_NOCHECKNAME Non controlla i nomi per verificarne la correttezza
sintattica.
RES_KEEPTSIG Non elimina i record di tipo TSIG.
RES_BLAST Effettua un “blast” inviando simultaneamente le richieste
a tutti i server; non ancora implementata.
RES_DEFAULT Combinazione di RES_RECURSE, RES_DEFNAMES e
RES_DNSRCH.
richiesta, ciascun tentativo di richiesta fallito viene ripetuto raddoppiando il tempo di scadenza
per il numero massimo di volte stabilito da RES_RETRY.
La funzione di interrogazione principale è res_query, che serve ad eseguire una richiesta ad
un server DNS per un nome a dominio completamente specificato (quello che si chiama FQDN,
Fully Qualified Domain Name); il suo prototipo è:
#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 buffer 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 specificata 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 buffer di lunghezza anslen puntato da answer che si sarà
opportunamente allocato in precedenza.
Una seconda funzione di ricerca, analoga a res_query, che prende gli stessi argomenti, ma che
esegue l’interrogazione con le funzionalità addizionali previste dalle due opzioni RES_DEFNAMES
e RES_DNSRCH, è res_search, il cui prototipo è:
#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 buffer answer in
caso di successo e -1 in caso di errore.
Costante Significato
C_IN Indirizzi internet, in pratica i soli utilizzati oggi.
C_HS Indirizzi Hesiod, utilizzati solo al MIT, oggi completa-
mente estinti.
C_CHAOS Indirizzi per la rete Chaosnet, un’altra rete sperimentale
nata al MIT.
C_ANY Indica un indirizzo di classe qualunque.
Tabella 17.3: Costanti identificative delle classi di indirizzi per l’argomento class di res_query.
11
esisteva in realtà anche una classe C_CSNET per la omonima rete, ma è stata dichiarata obsoleta.
17.1. LA RISOLUZIONE DEI NOMI 575
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. L’elenco delle costanti12 che
definiscono i valori che si possono usare per l’argomento type per specificare il tipo di resource
record da richiedere è riportato in tab. 17.4; le costanti (tolto il T_ iniziale) hanno gli stessi nomi
usati per identificare i record nei file di zona di BIND,13 e che normalmente sono anche usati
come nomi per indicare i record.
Costante Significato
T_A Indirizzo di una stazione.
T_NS Server DNS autoritativo per il dominio richiesto.
T_MD Destinazione per la posta elettronica.
T_MF Redistributore per la posta elettronica.
T_CNAME Nome canonico.
T_SOA Inizio di una zona di autorità.
T_MB Nome a dominio di una casella di posta.
T_MG Nome di un membro di un gruppo di posta.
T_MR Nome di un cambiamento di nome per la posta.
T_NULL Record nullo.
T_WKS Servizio noto.
T_PTR Risoluzione inversa di un indirizzo numerico.
T_HINFO Informazione sulla stazione.
T_MINFO Informazione sulla casella di posta.
T_MX Server cui instradare la posta per il dominio.
T_TXT Stringhe di testo (libere).
T_RP Nome di un responsabile (responsible person).
T_AFSDB Database per una cella AFS.
T_X25 Indirizzo di chiamata per X.25.
T_ISDN Indirizzo di chiamata per ISDN.
T_RT Router.
T_NSAP Indirizzo NSAP.
T_NSAP_PTR Risoluzione inversa per NSAP (deprecato).
T_SIG Firma digitale di sicurezza.
T_KEY Chiave per firma.
T_PX Corrispondenza per la posta X.400.
T_GPOS Posizione geografica.
T_AAAA Indirizzo IPv6.
T_LOC Informazione di collocazione.
T_NXT Dominio successivo.
T_EID Identificatore di punto conclusivo.
T_NIMLOC Posizionatore nimrod.
T_SRV Servizio.
T_ATMA Indirizzo ATM.
T_NAPTR Puntatore ad una naming authority.
T_TSIG Firma di transazione.
T_IXFR Trasferimento di zona incrementale.
T_AXFR Trasferimento di zona di autorità.
T_MAILB Trasferimento di record di caselle di posta.
T_MAILA Trasferimento di record di server di posta.
T_ANY Valore generico.
Tabella 17.4: Costanti identificative del tipo di record per l’argomento type di res_query.
L’elenco di tab. 17.4 è quello di tutti i resource record definiti, con una breve descrizione del
relativo significato. Di tutti questi però viene impiegato correntemente solo un piccolo sottoin-
12
ripreso dai file di dichiarazione arpa/nameser.h e arpa/nameser_compat.h.
13
BIND, acronimo di Berkley Internet Name Domain, è una implementazione di un server DNS, ed, essendo
utilizzata nella stragrande maggioranza dei casi, fa da riferimento; i dati relativi ad un certo dominio (cioè i suoi
resource record vengono mantenuti in quelli che sono usualmente chiamati file di zona, e in essi ciascun tipo di
dominio è identificato da un nome che è appunto identico a quello delle costanti di tab. 17.4 senza il T_ iniziale.
576 CAPITOLO 17. LA GESTIONE DEI SOCKET
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
del significato 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 file di zona di BIND):
A viene usato per indicare la corrispondenza fra un nome a dominio ed un indirizzo IPv4;
ad esempio la corrispondenza fra dodds.truelite.it e l’indirizzo IP 62.48.34.25.
AAAA 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
quella di un indirizzo IPv4.
PTR 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 ).
CNAME qualora si abbiamo più nomi che corrispondono allo stesso indirizzo (come ad esempio
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 crea-
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 l’errore invece viene segnalato da un valore di
ritorno pari a -1, ma in questo caso, non può essere utilizzata la variabile errno per riportare
un codice di errore, in quanto questo viene impostato per ciascuna delle chiamate al sistema
utilizzate dalle funzioni del resolver, non avrà alcun significato nell’indicare quale parte del
procedimento di risoluzione è fallita.
Per questo motivo è stata definita una variabile di errore separata, h_errno, che viene uti-
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:
extern int h_errno ;
ed i valori che può assumere, con il relativo significato, sono riportati in tab. 17.5.
Costante Significato
HOST_NOT_FOUND L’indirizzo richiesto non è valido e la macchina indicata è sconosciuta.
NO_ADDRESS Il nome a dominio richiesto è valido, ma non ha un indirizzo associato ad esso
(alternativamente può essere indicato come NO_DATA).
NO_RECOVERY Si è avuto un errore non recuperabile nell’interrogazione di un server DNS.
TRY_AGAIN Si è avuto un errore temporaneo nell’interrogazione di un server DNS, si può
ritentare l’interrogazione in un secondo tempo.
Insieme alla nuova variabile vengono definite anche due nuove funzioni per stampare l’errore
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 è:
#include <netdb.h>
void herror(const char *string)
Stampa un errore di risoluzione.
#include <netdb.h>
const char *hstrerror(int err)
Restituisce una stringa corrispondente ad un errore di risoluzione.
che, come l’analoga strerror, restituisce una stringa con un messaggio di errore già formattato,
corrispondente al codice passato come argomento (che si presume sia dato da h_errno).
#include <netdb.h>
struct hostent *gethostbyname(const char *name)
Determina l’indirizzo 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 definizione è riportata in fig. 17.2.
struct hostent {
char * h_name ; /* official name of host */
char ** h_aliases ; /* alias list */
int h_addrtype ; /* host address type */
int h_length ; /* length of address */
char ** h_addr_list ; /* list of addresses */
}
# define h_addr h_addr_list [0] /* 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 effettuare la riso-
luzione del nome, è con i valori contenuti nei relativi record che vengono riempite le varie parti
della struttura hostent. Il primo campo della struttura, h_name contiene sempre il nome cano-
nico, che nel caso del DNS è appunto il nome associato ad un record A. Il secondo campo della
struttura, h_aliases, invece è un puntatore ad vettore di puntatori, terminato da un puntatore
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 può assumere soltanto i valori AF_INET o AF_INET6, mentre il quarto campo, h_length, indica
la lunghezza dell’indirizzo stesso in byte.
Infine il campo h_addr_list è il puntatore ad un vettore di puntatori ai singoli indirizzi; il
vettore è terminato da un puntatore nullo. Inoltre, come illustrato in fig. 17.2, viene definito il
campo h_addr come sinonimo di h_addr_list[0], cioè un riferimento diretto al primo indirizzo
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
578 CAPITOLO 17. LA GESTIONE DEI SOCKET
la stringa nel campo h_name ed a creare la corrispondente struttura in_addr da indirizzare con
h_addr_list[0].
Con l’uso di gethostbyname normalmente si ottengono solo gli indirizzi IPv4, se si vogliono
ottenere degli indirizzi IPv6 occorrerà prima impostare l’opzione RES_USE_INET6 nel campo
_res.options e poi chiamare res_init (vedi sez. 17.1.2) per modificare le opzioni del resolver ;
dato che questo non è molto comodo è stata definita14 un’altra funzione, gethostbyname2, il cui
prototipo è:
#include <netdb.h>
#include <sys/socket.h>
struct hostent *gethostbyname2(const char *name, int af)
Determina l’indirizzo 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 l’uso di sys/socket.h) la famiglia di indirizzi
che dovrà essere utilizzata nei risultati restituiti dalla funzione. Per tutto il resto la funzione è
identica a gethostbyname, ed identici sono i suoi risultati.
Vediamo allora un primo esempio dell’uso delle funzioni di risoluzione, in fig. 17.3 è riportato
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, è
nel file mygethost.c dei sorgenti allegati alla guida.
Il programma richiede un solo argomento che specifichi 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 fine si inizia (25) con lo stampare il nome canonico,
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 fin
tanto che nella lista si troveranno dei puntatori validi15 per le stringhe dei nomi; prima (28) si
stamperà la stringa e poi (29) si provvederà ad incrementare il puntatore per passare al successivo
elemento della lista.
Una volta stampati i nomi si passerà a stampare gli indirizzi, il primo passo (31-38) è allora
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
un indirizzo non valido.
Infine (39-44) si stamperanno i valori degli indirizzi, di nuovo (39) si inizializzerà un puntatore
alla cima della lista e si eseguirà un ciclo fintanto che questo punterà ad indirizzi validi in maniera
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
(vedi sez. 15.4) passandole gli opportuni argomenti, questa restituirà la stringa da stampare (42)
con il valore dell’indirizzo in buffer, che si è avuto la cura di dichiarare inizialmente (10) con
14
questa è una estensione fornita dalle glibc, disponibile anche in altri sistemi unix-like.
15
si ricordi che la lista viene terminata da un puntatore nullo.
17.1. LA RISOLUZIONE DEI NOMI 579
dimensioni adeguate; dato che la funzione è in grado di tenere conto automaticamente del tipo
di indirizzo non ci sono precauzioni particolari da prendere.16
Le funzioni illustrate finora 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
16
volendo essere pignoli si dovrebbe controllarne lo stato di uscita, lo si è tralasciato per non appesantire il
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.
580 CAPITOLO 17. LA GESTIONE DEI SOCKET
che copiare il contenuto della sola struttura non è sufficiente per salvare tutti i dati, in quanto
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
copy.17
Per ovviare a questi problemi nelle glibc sono definite anche delle versioni rientranti delle
precedenti funzioni, al solito queste sono caratterizzate dall’avere un suffisso _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.
Gli argomenti name (e af per gethostbyname2_r) hanno lo stesso significato visto in prece-
denza. Tutti gli altri argomenti hanno lo stesso significato per entrambe le funzioni. Per evitare
l’uso di variabili globali si dovrà allocare preventivamente una struttura hostent in cui ricevere
il risultato, passandone l’indirizzo alla funzione nell’argomento ret. Inoltre, dato che hostent
contiene dei puntatori, dovrà essere allocato anche un buffer in cui le funzioni possano scrivere
tutti i dati del risultato dell’interrogazione da questi puntati; l’indirizzo e la lunghezza di questo
buffer 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 specificare l’indirizzo della variabile su cui la funzione dovrà salvare il codice
di errore con h_errnop e quello su cui dovrà salvare il puntatore che si userà per accedere i dati
con result.
In caso di successo entrambe le funzioni restituiscono un valore nullo, altrimenti restituiscono
un codice di errore negativo e all’indirizzo puntato da result sarà salvato un puntatore nullo,
mentre a quello puntato da h_errnop sarà salvato il valore del codice di errore, dato che per
essere rientrante la funzione non può la variabile globale h_errno. In questo caso il codice di
errore, oltre ai valori di tab. 17.5, può avere anche quello di ERANGE qualora il buffer allocato su
buf non sia sufficiente a contenere i dati, in tal caso si dovrà semplicemente ripetere l’esecuzione
della funzione con un buffer di dimensione maggiore.
Una delle caratteristiche delle interrogazioni al servizio DNS è che queste sono normalmente
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 è:
#include <netdb.h>
void sethostent(int stayopen)
Richiede l’uso di connessioni per le interrogazioni ad un server DNS.
La funzione non restituisce nulla.
La funzione permette di richiedere l’uso di connessioni TCP per la richiesta dei dati, e che
queste restino aperte per successive richieste. Il valore dell’argomento stayopen indica se attivare
questa funzionalità, un valore pari a 1 (o diverso da zero), che indica una condizione vera in C,
17
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 sotto-strutture con altri puntatori) e copiare anche i dati da questi referenziati.
18
si potrebbero impostare direttamente le opzioni di __res.options, ma queste funzioni permettono di
semplificare la procedura.
17.1. LA RISOLUZIONE DEI NOMI 581
attiva la funzionalità. Come si attiva l’uso delle connessioni TCP lo si può disattivare con la
funzione endhostent; il suo prototipo è:
#include <netdb.h>
void endhostent(void)
Disattiva l’uso di connessioni per le interrogazioni ad un server DNS.
e come si può vedere la funzione è estremamente semplice, non richiedendo nessun argomento.
Infine si può richiedere la risoluzione inversa di un indirizzo IP od IPv6, per ottenerne il
nome a dominio ad esso associato, per fare questo si può usare la funzione gethostbyaddr, il
cui prototipo è:
#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 l’indirizzo ad una struttura hostent in caso di successo ed NULL in caso di
errore.
In questo caso l’argomento addr dovrà essere il puntatore ad una appropriata struttura
contenente il valore dell’indirizzo IP (o IPv6) che si vuole risolvere. L’uso del tipo char * per
questo argomento è storico, il dato dovrà essere fornito in una struttura in_addr19 per un
indirizzo IPv4 ed una struttura in6_addr per un indirizzo IPv6, mentre in len se ne dovrà
specificare la dimensione (rispettivamente 4 o 16), infine l’argomento type indica il tipo di
indirizzo e dovrà essere o AF_INET o AF_INET6.
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
all’indirizzo specificato. In caso di errore al solito viene usata la variabile h_errno per restituire
un opportuno codice. In questo caso l’unico campo del risultato che interessa è h_name che
conterrà il nome a dominio, la funziona comunque inizializza anche il primo campo della lista
h_addr_list col valore dell’indirizzo passato come argomento.
Per risolvere il problema dell’uso da parte delle due funzioni gethostbyname e gethostbyaddr
di memoria statica che può essere sovrascritta fra due chiamate successive, e per avere sempre
la possibilità di indicare esplicitamente il tipo di indirizzi voluto (cosa che non è possibile con
gethostbyname), vennero introdotte due nuove funzioni di risoluzione,20 getipnodebyname e
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 l’indirizzo ad una struttura hostent in caso di successo ed NULL
in caso di errore.
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 specifichi l’indirizzo come per
gethostbyaddr passando anche la lunghezza dello stesso nell’argomento 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 spe-
cificarne il comportamento nella risoluzione dei diversi tipi di indirizzi (IPv4 e IPv6); ciascun
bit dell’argomento esprime una diversa opzione, e queste possono essere specificate con un OR
aritmetico delle costanti riportate in tab. 17.6.
Costante Significato
AI_V4MAPPED Usato con AF_INET6 per richiedere una ricerca su un indi-
rizzo IPv4 invece che IPv6; gli eventuali risultati saranno
rimappati su indirizzi IPv6.
AI_ALL Usato con AI_V4MAPPED; richiede sia indirizzi IPv4 che
IPv6, e gli indirizzi IPv4 saranno rimappati in IPv6.
AI_ADDRCONFIG Richiede che una richiesta IPv4 o IPv6 venga eseguita
solo se almeno una interfaccia del sistema è associata ad
un indirizzo di tale tipo.
AI_DEFAULT Il valore di default, è equivalente alla combinazione di
AI_ADDRCONFIG e di AI_V4MAPPED.
Tabella 17.6: Valori possibili per i bit dell’argomento flags della funzione getipnodebyname.
Tabella 17.7: Funzioni di risoluzione dei nomi per i vari servizi del Name Service Switch.
una entità esterna; per le altre invece, estensioni fornite dal NSS a parte, si fa sempre riferimento
ai dati mantenuti nei rispettivi file.
Dopo la risoluzione dei nomi a dominio una delle ricerche più comuni è quella sui nomi dei
servizi di rete più comuni (cioè http, smtp, ecc.) da associare alle rispettive porte. Le due funzioni
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 pro-
tocollo per il quale si intende effettuare la ricerca,21 che nel caso si IP può avere come valori
possibili solo udp o tcp;22 se si specifica un puntatore nullo la ricerca sarà eseguita su un
protocollo qualsiasi.
Il primo argomento è il nome del servizio per getservbyname, specificato tramite la stringa
name, mentre getservbyport richiede il numero di porta in port. Entrambe le funzioni eseguono
una ricerca sul file /etc/services23 ed estraggono i dati dalla prima riga che corrisponde agli
argomenti specificati; 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.
struct servent {
char * s_name ; /* official service name */
char ** s_aliases ; /* alias list */
int s_port ; /* port number */
char * s_proto ; /* protocol to use */
}
Figura 17.4: La struttura servent per la risoluzione dei nomi dei servizi e dei numeri di porta.
La definizione della struttura servent è riportata in fig. 17.4, il primo campo, s_name con-
tiene sempre il nome canonico del servizio, mentre s_aliases è un puntatore ad un vettore di
stringhe contenenti gli eventuali nomi alternativi utilizzabili per identificare lo stesso servizio.
Infine s_port contiene il numero di porta e s_proto il nome del protocollo.
21
le informazioni mantenute in /etc/services infatti sono relative sia alle porte usate su UDP che su TCP,
occorre quindi specificare 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.
584 CAPITOLO 17. LA GESTIONE DEI SOCKET
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
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 sequen-
ziale delle informazioni mantenute nel Name Service Switch (in sostanza permettono di leggere
i file contenenti le informazioni riga per riga), che sono analoghe a quelle elencate in tab. 8.10
per le informazioni relative ai dati degli utenti e dei gruppi. Nel caso specifico dei servizi avremo
allora le tre funzioni setservent, getservent e endservent i cui prototipi sono:
#include <netdb.h>
void setservent(int stayopen)
Apre il file /etc/services e si posiziona al suo inizio.
struct servent *getservent(void)
Legge la voce successiva nel file /etc/services.
void endservent(void)
Chiude il file /etc/services.
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ù
volte. Se il file non è aperto provvede automaticamente ad aprirlo, nel qual caso leggerà la prima
voce. La seconda funzione, setservent, permette di aprire il file /etc/services per una succes-
siva lettura, ma se il file è già stato aperto riporta la posizione di lettura alla prima voce del file,
in questo modo si può far ricominciare da capo una lettura sequenziale. L’argomento stayopen,
se diverso da zero, fa sı̀ che il file resti aperto anche fra diverse chiamate a getservbyname e
getservbyaddr.24 La terza funzione, endservent, provvede semplicemente a chiudere il file.
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 significa 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 Funzioni
indirizzo sethostent gethostent endhostent
servizio setservent getservent endservent
rete setnetent getnetent endnetent
protocollo setprotoent getprotoent endprotoent
Tabella 17.8: Funzioni lettura sequenziale dei dati del Name Service Switch.
Inoltre in genere quando si ha a che fare con i socket non esiste soltanto il problema della
risoluzione del nome che identifica 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 depre-
cate le varie funzioni gethostbyaddr, gethostbyname, getipnodebyname e getipnodebyaddr
ed è stata introdotta una interfaccia completamente nuova.
La prima funzione di questa interfaccia è getaddrinfo,26 che combina le funzionalità delle
precedenti getipnodebyname, getipnodebyaddr, getservbyname e getservbyport, consenten-
do di ottenere contemporaneamente sia la risoluzione di un indirizzo simbolico che del nome di
un servizio; il suo prototipo è:
#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 prende come primo argomento il nome della macchina che si vuole risolvere,
specificato tramite la stringa node. Questo argomento, oltre ad un comune nome a dominio, può
indicare anche un indirizzo numerico in forma dotted-decimal per IPv4 o in formato esadecimale
per IPv6. Si può anche specificare il nome di una rete invece che di una singola macchina. Il
secondo argomento, service, specifica 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à effettuata
soltanto sulla base del valore dell’altro.
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 effettuare ricerche generiche sugli
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 va-
lue result argument) i propri risultati. La funzione infatti è rientrante, ed alloca autonomamente
tutta la memoria necessaria in cui verranno riportati i risultati della risoluzione. La funzione
scriverà all’indirizzo puntato da res il puntatore iniziale ad una linked list di strutture di tipo
addrinfo contenenti tutte le informazioni ottenute.
struct addrinfo
{
int ai_flags ; /* Input flags . */
int ai_family ; /* Protocol family for socket . */
int ai_socktype ; /* Socket type . */
int ai_protocol ; /* Protocol for socket . */
socklen_t ai_addrlen ; /* Length of socket address . */
struct sockaddr * ai_addr ; /* Socket address for socket . */
char * ai_canonname ; /* Canonical name for service location . */
struct addrinfo * ai_next ; /* 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.
26
la funzione è definita, insieme a getnameinfo che vedremo più avanti, nell’RFC 2553.
586 CAPITOLO 17. LA GESTIONE DEI SOCKET
Come illustrato la struttura addrinfo, la cui definizione27 è riportata in fig. 17.5, viene
usata sia in ingresso, per passare dei valori di controllo alla funzione, che in uscita, per ricevere i
risultati. Il primo campo, ai_flags, è una maschera binaria di bit che permettono di controllare
le varie modalità di risoluzione degli indirizzi, che viene usato soltanto in ingresso. I tre campi
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 dimen-
sione della struttura degli indirizzi ottenuta come risultato, il cui contenuto sarà memorizzato
nella struttura sockaddr posta all’indirizzo puntato dal campo ai_addr. Il campo ai_canonname
è un puntatore alla stringa contenente il nome canonico della macchina, ed infine, quando la fun-
zione restituisce più di un risultato, ai_next è un puntatore alla successiva struttura addrinfo
della lista.
Ovviamente non è necessario dare dei suggerimenti in ingresso, ed usando NULL come valore
per l’argomento hints si possono compiere ricerche generiche. Se però si specifica un valore non
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 specificare nessuna
famiglia di indirizzi si può usare il valore PF_UNSPEC. Allo stesso modo per ai_socktype si
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 significativi sono SOCK_STREAM e SOCK_DGRAM; in
questo caso, se non si vuole effettuare nessuna risoluzione specifica, si potrà usare un valore
nullo.
Il campo ai_protocol permette invece di effettuare la selezione dei risultati per il nome del
servizio usando il numero identificativo 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.
Infine l’ultimo campo è ai_flags; che deve essere impostato come una maschera binaria;
i bit di questa variabile infatti vengono usati per dare delle indicazioni sul tipo di risoluzione
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
quali identifica 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 è
significativo, eccetto il caso in cui si sia ricevuto un errore di EAI_SYSTEM, nel qual caso l’errore
corrispondente è riportato tramite errno.
Come per i codici di errore di gethostbyname anche in questo caso è fornita una apposita
funzione, analoga di strerror, che consente di utilizzarli direttamente per stampare a video un
messaggio esplicativo; la funzione è gai_strerror ed il suo prototipo è:
27
la definizione è ripresa direttamente dal file netdb.h in questa struttura viene dichiarata, la pagina di manuale
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.
17.1. LA RISOLUZIONE DEI NOMI 587
Costante Significato
AI_PASSIVE Viene utilizzato per ottenere un indirizzo in formato adatto per una
successiva chiamata a bind. Se specificato quando si è usato NULL co-
me valore per node gli indirizzi restituiti saranno inizializzati al valore
generico (INADDR_ANY per IPv4 e IN6ADDR_ANY_INIT per IPv6), altri-
menti verrà usato l’indirizzo dell’interfaccia di loopback. Se invece non
è impostato gli indirizzi verranno restituiti in formato adatto ad una
chiamata a connect o sendto.
AI_CANONNAME Richiede la restituzione del nome canonico della macchina, che ver-
rà salvato in una stringa il cui indirizzo sarà restituito nel campo
ai_canonname della prima struttura addrinfo dei risultati. Se il no-
me canonico non è disponibile al suo posto viene restituita una copia
di node.
AI_NUMERICHOST Se impostato il nome della macchina specificato con node deve es-
sere espresso in forma numerica, altrimenti sarà restituito un errore
EAI_NONAME (vedi tab. 17.10), in questo modo si evita ogni chiamata
alle funzioni di risoluzione.
AI_V4MAPPED Stesso significato dell’analoga di tab. 17.6.
AI_ALL Stesso significato dell’analoga di tab. 17.6.
AI_ADDRCONFIG Stesso significato dell’analoga di tab. 17.6.
Tabella 17.9: Costanti associate ai bit del campo ai_flags della struttura addrinfo.
Costante Significato
EAI_FAMILY La famiglia di indirizzi richiesta non è supportata.
EAI_SOCKTYPE Il tipo di socket richiesto non è supportato.
EAI_BADFLAGS Il campo ai_flags contiene dei valori non validi.
EAI_NONAME Il nome a dominio o il servizio non sono noti, viene usato questo errore
anche quando si specifica il valore NULL per entrambi gli argomenti node
e service.
EAI_SERVICE Il servizio richiesto non è disponibile per il tipo di socket richiesto, anche
se può esistere per altri tipi di socket.
EAI_ADDRFAMILY La rete richiesta non ha nessun indirizzo di rete per la famiglia di
indirizzi specificata.
EAI_NODATA La macchina specificata esiste, ma non ha nessun indirizzo di rete
definito.
EAI_MEMORY È stato impossibile allocare la memoria necessaria alle operazioni.
EAI_FAIL Il DNS ha restituito un errore di risoluzione permanente.
EAI_AGAIN Il DNS ha restituito un errore di risoluzione temporaneo, si può
ritentare in seguito.
EAI_SYSTEM C’è stato un errore di sistema, si può controllare errno per i dettagli.
Tabella 17.10: Costanti associate ai valori dei codici di errore della funzione getaddrinfo.
#include <netdb.h>
const char *gai_strerror(int errcode)
Fornisce il messaggio corrispondente ad un errore di getaddrinfo.
risoluzione del nome canonico, si avrà come risposta della funzione la lista illustrata in fig. 17.6.
dovuto passare il puntatore al campo contenente l’indirizzo IP nella struttura puntata dal campo
ai_addr.29
Una volta estratte dalla struttura addrinfo tutte le informazioni relative alla risoluzione
richiesta e stampati i relativi valori, l’ultimo passo (34) è di estrarre da ai_next l’indirizzo della
eventuale successiva struttura presente nella lista e ripetere il ciclo, fin tanto che, completata la
scansione, questo avrà un valore nullo e si potrà terminare (36) il programma.
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ù
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
verificare il risultato:
29
il meccanismo è complesso a causa del fatto che al contrario di IPv4, in cui l’indirizzo IP può essere espresso con
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 finali, per l’uso con inet_ntop occorre
comunque passare un puntatore agli stessi (ed il costrutto &addr6->sin6_addr è corretto in quanto l’operatore ->
ha on questo caso precedenza su &).
590 CAPITOLO 17. LA GESTIONE DEI SOCKET
Costante Significato
NI_NOFQDN Richiede che venga restituita solo il nome della macchina all’interno del
dominio al posto del nome completo (FQDN).
NI_NUMERICHOST Richiede che venga restituita la forma numerica dell’indirizzo (questo
succede sempre se il nome non può essere ottenuto).
NI_NAMEREQD Richiede la restituzione di un errore se il nome non può essere risolto.
NI_NUMERICSERV Richiede che il servizio venga restituito in forma numerica (attraverso
il numero di porta).
NI_DGRAM 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.
Tabella 17.11: Costanti associate ai bit dell’argomento flags della funzione getnameinfo.
hostlen e servlen. Sono comunque definite 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 l’uso
delle funzioni appena illustrate (in particolare di getaddrinfo) è piuttosto complesso, essendo
necessaria anche una impostazione diretta dei campi dell’argomento hints, provvederemo una
interfaccia semplificata per i due casi visti finora, quello in cui si specifica nel client un indirizzo
remoto per la connessione al server, e quello in cui si specifica nel server un indirizzo locale su
cui porsi in ascolto.
La prima funzione della nostra interfaccia semplificata è sockconn che permette di ottenere
un socket, connesso all’indirizzo ed al servizio specificati. Il corpo della funzione è riportato in
fig. 17.8, il codice completo è nel file SockUtil.c dei sorgenti allegati alla guida, che contiene
varie funzioni di utilità per l’uso dei socket.
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à effettuata la risoluzione; seguono il
protocollo da usare (da specificare con il valore numerico di /etc/protocols) ed il tipo di socket
(al solito specificato con i valori illustrati in sez. 15.2.3). La funzione ritorna il valore del file
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 d’errore direttamente nella funzione.
Una volta definite 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 quest’ultima 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
in ascolto soltanto su uno solo di questi, si provvede a tentare la connessione per ciascun indirizzo
restituito all’interno 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 fine per disallocare la lista.
Il ciclo viene ripetuto (18) fintanto che si hanno indirizzi validi, ed inizia (19) con l’apertura
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
l’errore 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 (30-
38) il controllo se vi sono o no altri indirizzi da provare nella stessa modalità fatta in precedenza,
30
in Linux le due costanti sono definite in netdb.h ed hanno rispettivamente il valore 1024 e 12.
31
non si può avere nessuna certezza che detti valori siano negativi, è questo è invece necessario per evitare ogni
possibile ambiguità nei confronti del valore di ritorno in caso di successo.
592 CAPITOLO 17. LA GESTIONE DEI SOCKET
1 int sockconn ( char * host , char * serv , int prot , int type )
2 {
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 }
aggiungendovi però in entrambi i casi (32 e (36) la chiusura del socket precedentemente aperto,
che non è più utilizzabile.
Se la connessione ha avuto successo invece si termina (39) direttamente il ciclo, e prima
di ritornare (31) il valore del file 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
indirizzi IPv6 o IPv4, in quanto si fa uso direttamente dei dati relativi alle strutture degli indirizzi
di addrinfo che sono opachi rispetto all’uso della funzione connect.
Per usare questa funzione possiamo allora modificare ulteriormente il nostro programma
client per il servizio echo; in questo caso rispetto al codice usato finora per collegarsi (vedi
fig. 16.11) avremo una semplificazione per cui il corpo principale del nostro client diventerà
17.1. LA RISOLUZIONE DEI NOMI 593
quello illustrato in fig. 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 file
TCP_echo_fifth.c dei sorgenti allegati) consente di utilizzare come argomento del programma
un nome a dominio al posto dell’indirizzo numerico, e può utilizzare sia indirizzi IPv4 che IPv6.
La seconda funzione di ausilio è sockbind, il cui corpo principale è riportato in fig. 17.10 (al
solito il sorgente completo è nel file sockbind.c dei sorgenti allegati alla guida). Come si può
notare la funzione è del tutto analoga alla precedente sockconn, e prende gli stessi argomenti,
però invece di eseguire una connessione con connect si limita a chiamare bind per collegare il
socket ad una porta.
Dato che la funzione è pensata per essere utilizzata da un server ci si può chiedere a quale
scopo mantenere l’argomento host quando l’indirizzo di questo è usualmente noto. Si ricordi
però quanto detto in sez. 16.2.1, relativamente al significato della scelta di un indirizzo specifico
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
specifico, ma utilizzi l’indirizzo generico, occorrerà avere cura di passare un valore NULL come
valore per l’argomento host; l’uso 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)
opportunamente la struttura hint con i valori ricevuti come argomenti, soltanto che in questo
caso si è usata (8) una impostazione specifica dei flag di hint usando AI_PASSIVE per indicare
che il socket sarà usato per una apertura passiva. Per il resto la chiamata (12-18) a getaddrinfo
e ed il ciclo principale (20-42) sono identici, solo che si è sostituita (31) la chiamata a connect
con una chiamata a bind. Anche la conclusione (43-44) della funzione è identica.
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
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
error.
Con l’uso di questa funzione si può modificare anche il codice del nostro server echo, che
rispetto a quanto illustrato nella versione iniziale di fig. 16.13 viene modificato nella forma
riportata in fig. 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 specifico sul quale si voglia
594 CAPITOLO 17. LA GESTIONE DEI SOCKET
1 int sockbind ( char * host , char * serv , int prot , int type )
2 {
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 }
Figura 17.11: Nuovo codice per l’apertura passiva del server echo.
sezione vedremo allora quali sono le funzioni dedicate alla gestione delle caratteristiche specifiche
dei vari tipi di socket, le cosiddette socket options.
La funzione restituisce 0 in caso di successo e -1 in caso di errore, nel qual caso errno assumerà i
valori:
EBADF il file descriptor sock non è valido.
EFAULT l’indirizzo optval non è valido.
EINVAL il valore di optlen non è valido.
ENOPROTOOPT l’opzione scelta non esiste per il livello indicato.
ENOTSOCK il file descriptor sock non corrisponde ad un socket.
Il primo argomento della funzione, sock, indica il socket su cui si intende operare; per indicare
l’opzione 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 l’interfaccia
dei socket può usarne più di uno. Si avranno allora funzionalità e caratteristiche diverse per
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 definite per quel protocollo si vuole operare.
596 CAPITOLO 17. LA GESTIONE DEI SOCKET
In sostanza la selezione di una specifica opzione viene fatta attraverso una coppia di valori
level e optname e chiaramente la funzione avrà successo soltanto se il protocollo in questione
prevede quella opzione ed è utilizzato dal socket. Infine level prevede anche il valore speciale
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 identifica il suddetto protocollo in /etc/protocols;
dato che la leggibilità di un programma non trarrebbe certo beneficio dall’uso diretto dei valori
numerici, più comunemente si indica il protocollo tramite le apposite costanti SOL_* riportate
in tab. 17.12, dove si sono riassunti i valori che possono essere usati per l’argomento level.32
Livello Significato
SOL_SOCKET Opzioni generiche dei socket.
SOL_IP Opzioni specifiche per i socket che usano IPv4.
SOL_TCP Opzioni per i socket che usano TCP.
SOL_IPV6 Opzioni specifiche per i socket che usano IPv6.
SOL_ICMPV6 Opzioni specifiche per i socket che usano ICMPv6.
Tabella 17.12: Possibili valori dell’argomento level delle funzioni setsockopt e getsockopt.
Il quarto argomento, optval è un puntatore ad una zona di memoria che contiene i dati
che specificano il valore dell’opzione che si vuole passare al socket, mentre l’ultimo argomento
optlen,33 è la dimensione in byte dei dati presenti all’indirizzo indicato da optval. Dato che
il tipo di dati varia a seconda dell’opzione scelta, occorrerà individuare qual è quello che deve
essere usato, ed utilizzare le opportune variabili.
La gran parte delle opzioni utilizzano per optval un valore intero, se poi l’opzione esprime
una condizione logica, il valore è sempre un intero, ma si dovrà usare un valore non nullo per
abilitarla ed un valore nullo per disabilitarla. Se invece l’opzione non prevede di dover ricevere
nessun tipo di valore si deve impostare optval a NULL. Un piccolo numero di opzioni però usano
dei tipi di dati peculiari, è questo il motivo per cui optval è stato definito come puntatore
generico.
La seconda funzione usata per controllare le proprietà dei socket è getsockopt, che serve a
leggere i valori delle opzioni dei socket ed a farsi restituire i dati relativi al loro funzionamento;
il suo prototipo è:
#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
valori:
EBADF il file descriptor sock non è valido.
EFAULT l’indirizzo optval o quello di optlen non è valido.
ENOPROTOOPT l’opzione scelta non esiste per il livello indicato.
ENOTSOCK il file descriptor sock non corrisponde ad un socket.
32
la notazione in questo caso è, purtroppo, abbastanza confusa: infatti in Linux il valore si può impostare sia
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 specifica, che è quella
del protocollo ICMP, per la quale non esista una costante, il che è comprensibile dato che il suo valore, 1, è quello
che viene assegnato a SOL_SOCKET.
33
questo argomento è in realtà sempre di tipo int, come era nelle libc4 e libc5; l’uso di socklen_t è stato
introdotto da POSIX (valgono le stesse considerazioni per l’uso di questo tipo di dato fatte in sez. 16.2.4) ed
adottato dalle glibc.
17.2. LE OPZIONI DEI SOCKET 597
I primi tre argomenti sono identici ed hanno lo stesso significato di quelli di setsockopt,
anche se non è detto che tutte le opzioni siano definite per entrambe le funzioni. In questo caso
optval viene usato per ricevere le informazioni ed indica l’indirizzo a cui andranno scritti i dati
letti dal socket, infine optlen diventa un puntatore ad una variabile che viene usata come value
result argument per indicare, prima della chiamata della funzione, la lunghezza del buffer allocato
per optval e per ricevere indietro, dopo la chiamata della funzione, la dimensione effettiva dei
dati scritti su di esso. Se la dimensione del buffer allocato per optval non è sufficiente si avrà
un errore.
La tabella elenca le costanti che identificano le singole opzioni da usare come valore per
optname; le due colonne seguenti indicano per quali delle due funzioni (getsockopt o setsockopt)
l’opzione è disponibile, mentre la colonna successiva indica, quando di ha a che fare con un valore
di optval intero, se l’opzione è da considerare un numero o un valore logico. Si è inoltre riportato
sulla quinta colonna il tipo di dato usato per optval ed una breve descrizione del significato
delle singole opzioni sulla sesta.
Le descrizioni delle opzioni presenti in tab. 17.13 sono estremamente sommarie, è perciò
necessario fornire un po’ più di informazioni. Alcune opzioni inoltre hanno una notevole rilevanza
nella gestione dei socket, e pertanto il loro utilizzo sarà approfondito separatamente in sez. 17.2.3.
Quello che segue è quindi soltanto un elenco più dettagliato della breve descrizione di tab. 17.13
sul significato delle varie opzioni:
34
una descrizione di queste opzioni è generalmente disponibile nella settima sezione delle pagine di manuale, nel
caso specifico la si può consultare con man 7 socket.
598 CAPITOLO 17. LA GESTIONE DEI SOCKET
SO_KEEPALIVE questa opzione abilita un meccanismo di verifica della persistenza di una con-
nessione associata al socket (ed è pertanto effettiva solo sui socket che suppor-
tano le connessioni, ed è usata principalmente con il TCP). L’opzione utiliz-
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 flusso di dati del socket (e sono quindi letti con una normale read) invece
che restare disponibili solo per l’accesso con l’uso del flag MSG_OOB di recvmsg.
L’argomento è trattato in dettaglio in sez. 19.1.3. L’opzione funziona soltanto
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 buffer di ricezione perché il kernel passi i dati all’utente,
restituendoli ad una read o segnalando ad una select (vedi sez. 16.6.1) che
ci sono dati in ingresso. L’opzione utilizza per optval un intero che specifica
il numero di byte, ma con Linux questo valore è sempre 1 e non può essere
cambiato; getsockopt leggerà questo valore mentre setsockopt darà un errore
di ENOPROTOOPT.
SO_SNDLOWAT questa opzione imposta il valore che indica il numero minimo di byte che devono
essere presenti nel buffer di trasmissione perché il kernel li invii al protocollo suc-
cessivo, consentendo ad una write di ritornare o segnalando ad una select (vedi
sez. 16.6.1) che è possibile eseguire una scrittura. L’opzione utilizza per optval
un intero che specifica il numero di byte, come per la precedente SO_RCVLOWAT
con Linux questo valore è sempre 1 e non può essere cambiato; getsockopt
leggerà questo valore mentre setsockopt darà un errore di ENOPROTOOPT.
SO_BSDCOMPAT questa opzione abilita la compatibilità con il comportamento di BSD (in partico-
lare ne riproduce i bug). Attualmente è una opzione usata solo per il protocollo
UDP e ne è prevista la rimozione in futuro. L’opzione utilizza per optval un
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
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.
SO_PEERCRED questa opzione restituisce le credenziali del processo remoto connesso al socket;
l’opzione è disponibile solo per socket unix-domain e può essere usata solo con
getsockopt. Utilizza per optval una apposita struttura ucred (vedi sez. 18.2).
SO_BINDTODEVICE
questa opzione permette di legare il socket ad una particolare interfaccia, in mo-
do che esso possa ricevere ed inviare pacchetti solo su quella. L’opzione richiede
per optval il puntatore ad una stringa contenente il nome dell’interfaccia (ad
esempio eth0); utilizzando una stringa nulla o un valore nullo per optlen si può
rimuovere un precedente collegamento.
Il nome della interfaccia deve essere specificato con una stringa terminata da
uno zero e di lunghezza massima pari a IFNAMSIZ; l’opzione è effettiva solo per
alcuni tipi di socket, ed in particolare per quelli della famiglia AF_INET; non è
invece supportata per i packet socket (vedi sez. 18.3.1).
SO_DEBUG questa opzione abilita il debugging delle operazioni dei socket; l’opzione utilizza
per optval un intero usato come valore logico, e può essere utilizzata solo da
un processo con i privilegi di amministratore (in particolare con la capability
CAP_NET_ADMIN). L’opzione necessita inoltre dell’opportuno supporto nel ker-
nel;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; l’opzione utilizza per optval un intero usato come
valore logico. Questa opzione modifica il comportamento normale dell’interfac-
cia dei socket che fa fallire l’esecuzione della funzione bind con un errore di
EADDRINUSE quando l’indirizzo locale38 è già in uso da parte di un altro socket.
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
numerico che lo identifica (ad esempio SOCK_STREAM).
36
deve cioè essere definita la macro di preprocessore SOCK_DEBUGGING nel file include/net/sock.h dei sorgenti
del kernel, questo è sempre vero nei kernel delle serie superiori alla 2.3, per i kernel delle serie precedenti invece
è necessario aggiungere a mano detta definizione; è inoltre possibile abilitare anche il tracciamento degli stati del
TCP definendo la macro STATE_TRACE in include/net/tcp.h.
37
si tenga presente che il comportamento è diverso da quanto avviene con BSD, dove l’opzione opera solo sui
socket TCP, causando la scrittura di tutti i pacchetti inviati sulla rete su un buffer circolare che viene letto da un
apposito programma, trpt.
38
più propriamente il controllo viene eseguito sulla porta.
600 CAPITOLO 17. LA GESTIONE DEI SOCKET
SO_ACCEPTCONN
questa opzione permette di rilevare se il socket su cui opera è stato posto in
modalità di ricezione di eventuali connessioni con una chiamata a listen. L’op-
zione può essere usata soltanto con getsockopt e utilizza per optval un intero
in cui viene restituito 1 se il socket è in ascolto e 0 altrimenti.
SO_DONTROUTE questa opzione forza l’invio diretto dei pacchetti del socket, saltando ogni pro-
cesso relativo all’uso 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 all’indirizzo di broadcast, e potranno scrivere pac-
chetti su tale indirizzo. Prende per optval un intero usato come valore logico.
L’opzione non ha effetti su un socket di tipo SOCK_STREAM.
SO_SNDBUF questa opzione imposta la dimensione del buffer di trasmissione del socket. Pren-
de per optval un intero indicante il numero di byte. Il valore di default ed il
valore massimo che si possono specificare come argomento per questa opzione
sono impostabili rispettivamente tramite gli opportuni valori di sysctl (vedi
sez. 17.4.1).
SO_RCVBUF questa opzione imposta la dimensione del buffer 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ò specificare come argomento per questa opzione sono
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 effettivamente una quantità di memoria doppia ri-
spetto a quanto richiesto con setsockopt. Questo comporta che una successiva
lettura con getsockopt riporterà un valore diverso da quello impostato con
setsockopt. Questo avviene perché TCP necessita dello spazio in più per man-
tenere dati amministrativi e strutture interne, e solo una parte viene usata come
buffer per i dati, mentre il valore letto da getsockopt e quello riportato nei va-
ri parametri di sysctl 39 indica la memoria effettivamente impiegata. Si tenga
presente inoltre che le modifiche alle dimensioni dei buffer di ricezione e trasmis-
sione, per poter essere effettive, 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
utilizza un protocollo che supporta le connessioni (è pertanto usata con i socket
TCP ed ignorata per UDP) e modifica il comportamento delle funzioni close e
shutdown. L’opzione richiede che l’argomento optval sia una struttura di tipo
linger, definita in sys/socket.h ed illustrata in fig. 17.15. Maggiori dettagli
sul suo funzionamento sono forniti in sez. 17.2.3.
SO_PRIORITY questa opzione permette di impostare le priorità per tutti i pacchetti che sono
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
priorità più alta vengono processati per primi, in modalità che dipendono dalla
disciplina di gestione della coda. Nel caso di protocollo IP questa opzione per-
mette anche di impostare i valori del campo type of service (noto come TOS,
39
cioè wmem_max e rmem_max in /proc/sys/net/core e tcp_wmem e tcp_rmem in /proc/sys/net/ipv4, vedi
sez. 17.4.1.
40
questo richiede che sia abilitato il sistema di Quality of Service disponibile con le opzioni di routing avanzato.
17.2. LE OPZIONI DEI SOCKET 601
vedi sez. A.1.2) per i pacchetti uscenti. Per impostare una priorità al di fuori
dell’intervallo 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 sol-
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 filtro di pacchetti che
consente di selezionare quali pacchetti, fra tutti quelli ricevuti, verranno let-
ti. 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 filtro precedentemente aggiunto ad un socket.
L’opzione SO_KEEPALIVE
La prima opzione da approfondire è SO_KEEPALIVE che permette di tenere sotto controllo lo
stato di una connessione. Una connessione infatti resta attiva anche quando non viene effettuato
alcun traffico su di essa; è allora possibile, in caso di una interruzione completa della rete, che
la caduta della connessione non venga rilevata, dato che sulla stessa non passa comunque alcun
traffico.
Se si imposta questa opzione, è invece cura del kernel inviare degli appositi messaggi sulla rete,
detti appunto keep-alive, per verificare se la connessione è attiva. L’opzione funziona soltanto
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 l’altro capo della connessione se questa è rimasta senza traffico per più di due
ore. Se è tutto a posto il messaggio viene ricevuto e verrà emesso un segmento ACK di risposta,
alla cui ricezione ripartirà un altro ciclo di attesa per altre due ore di inattività; il tutto avviene
all’interno del kernel e le applicazioni non riceveranno nessun dato.
Qualora ci siano dei problemi di rete si possono invece verificare i due casi di terminazione
precoce del server già illustrati in sez. 16.5.3. Il primo è quello in cui la macchina remota ha
avuto un crollo del sistema ed è stata riavviata, per cui dopo il riavvio la connessione non esiste
più.42 In questo caso all’invio del messaggio di keep-alive si otterrà come risposta un segmento
41
in sostanza un segmento ACK vuoto, cui sarà risposto con un altro segmento ACK vuoto.
42
si ricordi che un normale riavvio o il crollo dell’applicazione non ha questo effetto, 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 l’invio di un segmento FIN all’altro capo della connessione.
602 CAPITOLO 17. LA GESTIONE DEI SOCKET
RST che indica che l’altro capo non riconosce più l’esistenza della connessione ed il socket verrà
chiuso riportando un errore di ECONNRESET.
Se invece non viene ricevuta nessuna risposta (indice che la macchina non è più raggiungibile)
l’emissione 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,
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. Infine se si riceve come risposta un pacchetto ICMP di destinazione irraggiungibile
(vedi sez. A.3), verrà restituito l’errore corrispondente.
In generale questa opzione serve per individuare una caduta della connessione anche quando
non si sta facendo traffico su di essa. Viene usata principalmente sui server per evitare di mante-
nere impegnate le risorse che verrebbero dedicate a trattare delle connessioni che in realtà sono
già terminate (quelle che vengono anche chiamate connessioni semi-aperte); in tutti quei casi
cioè in cui il server si trova in attesa di dati in ingresso su una connessione che non arriveranno
mai o perché il client sull’altro capo non è più attivo o perché non è più in grado di comunicare
con il server via rete.
Figura 17.12: La sezione della nuova versione del server del servizio echo che prevede l’attivazione del keepalive
sui socket.
keepalive) tutte le modifiche al server sono riportate in fig. 17.12. Al solito il codice completo
è contenuto nel file TCP_echod_fourth.c dei sorgenti allegati alla guida.
Come si può notare la variabile keepalive è preimpostata (8) ad un valore nullo; essa viene
utilizzata sia come variabile logica per la condizione (14) che controlla l’attivazione del keep-
alive che come valore dell’argomento optval della chiamata a setsockopt (16). A seconda del
suo valore tutte le volte che un processo figlio viene eseguito in risposta ad una connessione
verrà pertanto eseguita o meno la sezione (14-17) che esegue l’impostazione di SO_KEEPALIVE
sul socket connesso, attivando il relativo comportamento.
L’opzione SO_REUSEADDR
1 int sockbindopt ( char * host , char * serv , int prot , int type , int reuse )
2 {
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 ...
19
20 return sock ;
21 }
Figura 17.13: Le sezioni della funzione sockbindopt modificate rispetto al codice della precedente sockbind.
A questo punto basterà modificare il server per utilizzare la nuova funzione; in fig. 17.14
abbiamo riportato le sezioni modificate rispetto alla precedente versione di fig. 17.11. Al solito
il codice completo è coi sorgenti allegati alla guida, nel file TCP_echod_fifth.c.
Anche in questo caso si è introdotta (8) una nuova variabile reuse che consente di controllare
l’uso dell’opzione e che poi sarà usata (14) come ultimo argomento di setsockopt. Il valore di
default di questa variabile è nullo, ma usando l’opzione -r nell’invocazione del server (al solito
la gestione delle opzioni non è riportata in fig. 17.14) se ne potrà impostare ad 1 il valore, per
cui in tal caso la successiva chiamata (13-17) a setsockopt attiverà l’opzione SO_REUSEADDR.
Figura 17.14: Il nuovo codice per l’apertura 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
17.2. LE OPZIONI DEI SOCKET 605
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
indirizzo specifico; in tal caso se un altro programma cerca di riutilizzare la stessa porta (an-
che specificando un indirizzo diverso) otterrà un errore, a meno di non aver preventivamente
impostato SO_REUSEADDR.
Usando questa opzione diventa anche possibile eseguire bind sull’indirizzo generico, e questo
permetterà il collegamento per tutti gli indirizzi (di quelli presenti) per i quali la porta non risulti
occupata da una precedente chiamata più specifica. Infine si tenga presente che con il protocollo
TCP non è mai possibile far partire server che eseguano bind sullo stesso indirizzo e la stessa
porta, cioè ottenere quello che viene chiamato un completely duplicate binding.
Il terzo impiego è simile al precedente e prevede l’uso di bind all’interno dello stesso pro-
gramma per associare indirizzi locali diversi a socket diversi. In genere questo viene fatto per i
socket UDP quando è necessario ottenere l’indirizzo a cui sono rivolte le richieste del client ed
il sistema non supporta l’opzione IP_RECVDSTADDR;46 in tale modo si può sapere a quale socket
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.
Infine il quarto caso è quello in cui si vuole effettivamente ottenere un completely duplicate
binding, quando cioè si vuole eseguire bind su un indirizzo ed una porta che sono già legati ad
un altro socket. Questo ovviamente non ha senso per il normale traffico di rete, in cui i pacchetti
vengono scambiati direttamente fra due applicazioni; ma quando un sistema supporta il traffico
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 traffico in multicast possano essere ricevuti da
più applicazioni47 o da diverse istanze della stessa applicazione.
In questo caso utilizzando SO_REUSEADDR si consente ad una applicazione eseguire bind sulla
stessa porta ed indirizzo usata da un’altra, 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 l’uso del protocollo UDP). La regola è che quando si hanno più applicazioni che hanno
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 è definito invece cosa accade
qualora il pacchetto sia destinato ad un indirizzo normale (unicast).
Essendo questo un caso particolare in alcuni sistemi (come BSD) è stata introdotta una
opzione ulteriore, SO_REUSEPORT che richiede che detta opzione sia specificata 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 effettuare un
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
46
nel caso di Linux questa opzione è stata supportata per in certo periodo nello sviluppo del kernel 2.1.x, ma è
in seguito stata soppiantata dall’uso di IP_PKTINFO (vedi sez. 17.2.4).
47
l’esempio classico di traffico in multicast è quello di uno streaming di dati (audio, video, ecc.), l’uso del
multicast consente in tal caso di trasmettere un solo pacchetto, che potrà essere ricevuto da tutti i possibili
destinatari (invece di inviarne un duplicato a ciascuno); in questo caso è perfettamente logico aspettarsi che sulla
stessa macchina più utenti possano lanciare un programma che permetta loro di ricevere gli stessi dati.
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 que-
sta caratteristica ciò è possibile soltanto se il primo programma a consentirlo, avendo usato fin dall’inizio
SO_REUSEADDR.
606 CAPITOLO 17. LA GESTIONE DEI SOCKET
L’opzione SO_LINGER
La terza opzione da approfondire è SO_LINGER; essa, come il nome suggerisce, consente di “indu-
giare” nella chiusura di un socket. Il comportamento standard sia di close che shutdown è infatti
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 buffer, viene gestito in sottofondo dal kernel.
struct linger
{
int l_onoff ; /* Nonzero to linger on close . */
int l_linger ; /* Time to linger ( in seconds ). */
}
Figura 17.15: La struttura linger richiesta come valore dell’argomento optval per l’impostazione dell’opzione
dei socket SO_LINGER.
1 ...
2 /* check if resetting on close is required */
3 if ( reset ) {
4 printf ( " Setting reset on close \ n " );
5 ling . l_onoff = 1;
6 ling . l_linger = 0;
7 if ( setsockopt ( sock , SOL_SOCKET , SO_LINGER , & ling , sizeof ( ling ))) {
8 perror ( " Cannot set linger " );
9 exit (1);
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 effettuato la connessione e prima di chiamare la
funzione di gestione, cioè fra le righe (12) e (13) del precedente esempio di fig. 17.9. Il codice si
limita semplicemente a controllare (3) il valore della variabile reset che assegnata nella gestione
17.2. LE OPZIONI DEI SOCKET 607
delle opzioni in corrispondenza all’uso 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.
Infine l’ultima possibilità, quella in cui si utilizza effettivamente SO_LINGER per indugiare
nella chiusura, è quella in cui sia l_onoff che l_linger hanno un valore diverso da zero. Se
si esegue l’impostazione 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 fintanto che non si sia concluso il procedimento di chiusura
della connessione, o non sia passato un numero di secondi49 pari al valore specificato in l_linger.
Le descrizioni riportate in tab. 17.14 sono estremamente succinte, una maggiore quantità di
dettagli sulle varie opzioni è fornita nel seguente elenco:
IP_OPTIONS l’opzione permette di impostare o leggere le opzioni del protocollo IP (si veda
sez. A.1.3). L’opzione prende come valore dell’argomento optval un puntatore
ad un buffer dove sono mantenute le opzioni, mentre optlen indica la dimensio-
ne di quest’ultimo. Quando la si usa con getsockopt vengono lette le opzioni
49
questa è l’unità di misura indicata da POSIX ed adottata da Linux, altri kernel possono usare unità di misura
diverse, oppure usare il campo l_linger come valore logico (ignorandone il valore) per rendere (quando diverso
da zero) close e shutdown bloccanti fino al completamento della trasmissione dei dati sul buffer.
50
come per le precedenti opzioni generiche una descrizione di esse è disponibile nella settima sezione delle pagine
di manuale, nel caso specifico la documentazione si può consultare con man 7 ip.
608 CAPITOLO 17. LA GESTIONE DEI SOCKET
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 dall’opzione IP_PKTINFO per ricavare informazioni sui pacchetti di un
socket di tipo SOCK_DGRAM.
L’opzione è utilizzabile solo per socket di tipo SOCK_DGRAM. Questa è una opzione
introdotta con i kernel della serie 2.2.x, ed è specifica di Linux;52 essa permette
di sostituire le opzioni IP_RECVDSTADDR e IP_RECVIF presenti in altri Unix (la
relativa informazione è quella ottenibile rispettivamente dai campi ipi_addr e
ipi_ifindex di pktinfo).
L’opzione prende per optval un intero usato come valore logico, che specifica
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
direttamente con recvmsg e sendmsg (vedi sez. 19.1.1).
IP_RETOPTS Identica alla precedente IP_RECVOPTS, ma in questo caso restituisce i dati grez-
zi delle opzioni, senza che siano riempiti i capi di instradamento e le marche
temporali. L’opzione richiede per optval un intero usato come valore logico.
L’opzione non è supportata per socket di tipo SOCK_STREAM.
IP_TOS L’opzione consente di leggere o impostare il campo Type of Service dell’intesta-
zione IP (per una trattazione più dettagliata, che riporta anche i valori possibili
e le relative costanti di definizione si veda sez. A.1.2) che permette di indica-
re le priorità dei pacchetti. Se impostato il valore verrà mantenuto per tutti i
pacchetti del socket; alcuni valori (quelli che aumentano la priorità) richiedono
i privilegi di amministrazione con la capability CAP_NET_ADMIN.
Il campo TOS è di 8 bit e l’opzione richiede per optval un intero che ne con-
tenga il valore. Sono definite anche alcune costanti che definiscono 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 modificato con le funzio-
nalità del cosiddetto Advanced Routing. Si ricordi che la priorità dei pacchetti
può essere impostata anche in maniera indipendente dal protocollo utilizzando
l’opzione SO_PRIORITY illustrata in sez. 17.2.2.
IP_TTL L’opzione consente di leggere o impostare per tutti i pacchetti associati al socket
il campo Time to Live dell’intestazione IP che indica il numero massimo di hop
(passaggi da un router ad un altro) restanti al paccheto (per una trattazione
più estesa si veda sez. A.1.2). Il campo TTL è di 8 bit e l’opzione richiede che
optval sia un intero, che ne conterrà il valore.
IP_MINTTL L’opzione, introdotta con il kernel 2.6.34, imposta un valore minimo per il campo
Time to Live dei pacchetti associati al socket su cui è attivata, che se non
rispettato ne causa lo scarto automatico. L’opzione è nata per implementare
l’RFC 5082 che la prevede come forma di protezione per i router che usano il
protocollo BGP poiché questi, essendo in genere adiacenti, possono, impostando
un valore di 255, scartare automaticamente tutti gli eventuali pacchetti falsi
creati da un attacco a questo protocollo, senza doversi curare di verificarne la
validità.53
IP_HDRINCL Se abilitata l’utente deve fornire lui stesso l’intestazione IP in cima ai propri dati.
L’opzione è valida soltanto per socket di tipo SOCK_RAW, e quando utilizzata even-
tuali valori impostati con IP_OPTIONS, IP_TOS o IP_TTL sono ignorati. In ogni
caso prima della spedizione alcuni campi dell’intestazione vengono comunque
modificati dal kernel, torneremo sull’argomento in sez. 18.3.1
IP_RECVERR Questa è una opzione introdotta con i kernel della serie 2.2.x, ed è specifica di
Linux. Essa permette di usufruire di un meccanismo affidabile per ottenere un
maggior numero di informazioni in caso di errori. Se l’opzione è abilitata tutti
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. L’opzione richiede per
optval un intero usato come valore logico e non è applicabile a socket di tipo
SOCK_STREAM.
53
l’attacco viene in genere portato per causare un Denial of Service aumentando il consumo di CPU del router
nella verifica dell’autenticità di un gran numero di pacchetti di pacchetti falsi; questi, arrivando da sorgenti diverse
da un router adiacente, non potrebbero più avere un TTL di 255 anche qualora questo fosse stato il valore di
partenza, e l’impostazione dell’opzione consente di scartarli senza carico aggiuntivo sulla CPU (che altrimenti
dovrebbe calcolare una checksum).
610 CAPITOLO 17. LA GESTIONE DEI SOCKET
IP_MTU_DISCOVER
Questa è una opzione introdotta con i kernel della serie 2.2.x, ed è specifica di
Linux. L’opzione permette di scrivere o leggere le impostazioni della modalità
usata per la determinazione della Path Maximum Transfer Unit (vedi sez. 14.3.5)
del socket. L’opzione prende per optval un valore intero che indica la modalità
usata, da specificare con una delle costanti riportate in tab. 17.15.
Valore Significato
IP_PMTUDISC_DONT 0 Non effettua la ricerca dalla Path MTU.
IP_PMTUDISC_WANT 1 Utilizza il valore impostato per la rotta utilizzata
dai pacchetti (dal comando route).
IP_PMTUDISC_DO 2 Esegue la procedura di determinazione della Path
MTU come richiesto dall’RFC 1191.
IP_MTU Permette di leggere il valore della Path MTU di percorso del socket. L’opzione
richiede per optval un intero che conterrà il valore della Path MTU in byte.
Questa è una opzione introdotta con i kernel della serie 2.2.x, ed è specifica di
Linux.
È tramite questa opzione che un programma può leggere, quando si è avuto un
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 Pa-
th MTU eseguendo prima una connect verso la destinazione, e poi usando
getsockopt con questa opzione. Si può anche avviare esplicitamente il pro-
cedimento di scoperta inviando un pacchetto di grosse dimensioni (che verrà
scartato) e ripetendo l’invio coi dati aggiornati. Si tenga infine conto che du-
rante il procedimento i pacchetti iniziali possono essere perduti, ed è compito
dell’applicazione gestirne una eventuale ritrasmissione.
IP_ROUTER_ALERT
Questa è una opzione introdotta con i kernel della serie 2.2.x, ed è specifica di
Linux. Prende per optval un intero usato come valore logico. Se abilitata passa
tutti i pacchetti con l’opzione IP Router Alert (vedi sez. A.1.3) che devono essere
inoltrati al socket corrente. Può essere usata soltanto per socket di tipo raw.
IP_MULTICAST_TTL
L’opzione permette di impostare o leggere il valore del campo TTL per i pac-
chetti multicast in uscita associati al socket. È importante che questo valore sia
il più basso possibile, ed il default è 1, che significa che i pacchetti non potranno
54
in caso contrario la trasmissione del pacchetto sarebbe effettuata, ottenendo o un fallimento successivo della
trasmissione, o la frammentazione dello stesso.
17.2. LE OPZIONI DEI SOCKET 611
uscire dalla rete locale. Questa opzione consente ai programmi che lo richiedono
di superare questo limite. L’opzione richiede per optval un intero che conterrà
il valore del TTL.
IP_MULTICAST_LOOP
L’opzione 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
però non si voglia generare traffico per dati che già sono disponibili in locale
l’uso di questa opzione permette di disabilitare questo tipo di traffico.
IP_ADD_MEMBERSHIP
L’opzione consente di unirsi ad gruppo di multicast, e può essere usata solo con
setsockopt. L’argomento optval in questo caso deve essere una struttura di
tipo ip_mreqn, illustrata in fig. 17.18, che permette di indicare, con il campo
imr_multiaddr l’indirizzo del gruppo di multicast a cui ci si vuole unire, con
il campo imr_address l’indirizzo dell’interfaccia locale con cui unirsi al gruppo
di multicast e con imr_ifindex l’indice dell’interfaccia da utilizzare (un valore
nullo indica una interfaccia qualunque).
Per compatibilità è possibile utilizzare anche un argomento di tipo ip_mreq, una
precedente versione di ip_mreqn, che differisce da essa soltanto per l’assenza 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
l’appartenenza 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 l’interfaccia locale per l’utilizzo del multicast, ed utilizza come optval
le stesse strutture ip_mreqn o ip_mreq delle due precedenti opzioni.
sono entrambi trasportati su IP,56 oltre alle opzioni generiche di sez. 17.2.2 saranno comunque
disponibili anche le precedenti opzioni di sez. 17.2.4.57
Il protocollo che supporta il maggior numero di opzioni è TCP; per poterle utilizzare occorre
specificare SOL_TCP (o l’equivalente IPPROTO_TCP) come valore per l’argomento level. Si sono
riportate le varie opzioni disponibili in tab. 17.16, dove sono elencate le rispettive costanti da
utilizzare come valore per l’argomento optname. Dette costanti e tutte le altre costanti e strutture
collegate all’uso delle opzioni TCP sono definite in netinet/tcp.h, ed accessibili includendo
detto file.58
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
dettagli è fornita nel seguente elenco:
TCP_NODELAY il protocollo TCP utilizza un meccanismo di bufferizzazione dei dati uscenti, per
evitare la trasmissione di tanti piccoli segmenti con un utilizzo non ottimale della
banda disponibile.59 Questo meccanismo è controllato da un apposito algoritmo
(detto algoritmo di Nagle, vedi sez. ??). Il comportamento normale del protocollo
prevede che i dati siano accumulati fintanto che non si raggiunge una quantità
considerata adeguata per eseguire la trasmissione di un singolo segmento.
Ci sono però delle situazioni in cui questo comportamento può non essere desi-
derabile, ad esempio quando si sa in anticipo che l’applicazione invierà soltanto
un piccolo quantitativo di dati;60 in tal caso l’attesa introdotta dall’algoritmo di
bufferizzazione non soltanto è inutile, ma peggiora le prestazioni introducendo
un ritardo. Impostando questa opzione si disabilita l’uso dell’algoritmo di Nagle
ed i dati vengono inviati immediatamente in singoli segmenti, qualunque sia la
loro dimensione. Ovviamente l’uso di questa opzione è dedicato a chi ha esigenze
particolari come quella illustrata, che possono essere stabilite solo per la singola
applicazione.
56
qui si sottintende IPv4, ma le opzioni per TCP e UDP sono le stesse anche quando si usa IPv6.
57
in realtà in sez. 17.2.4 si sono riportate le opzioni per IPv4, al solito, qualora si stesse utilizzando IPv6, si
potrebbero utilizzare le opzioni di quest’ultimo.
58
in realtà questo è il file usato dalle librerie; la definizione delle opzioni effettivamente supportate da Linux si
trova nel file linux/tcp.h, dal quale si sono estratte le costanti di tab. 17.16.
59
il problema è chiamato anche silly window syndrome, per averne un’idea si pensi al risultato che si ottiene
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 l’algoritmo di Nagle.
60
è il caso classico di una richiesta HTTP.
17.2. LE OPZIONI DEI SOCKET 613
TCP_MAXSEG con questa opzione si legge o si imposta il valore della MSS (Maximum Seg-
ment Size, vedi sez. 14.3.5 e sez. ??) dei segmenti TCP uscenti. Se l’opzione è
impostata prima di stabilire la connessione, si cambia anche il valore della MSS
annunciata all’altro capo della connessione. Se si specificano valori maggiori della
MTU questi verranno ignorati, inoltre TCP imporrà anche i suoi limiti massimo
e minimo per questo valore.
TCP_KEEPIDLE con questa opzione si legge o si imposta l’intervallo di tempo, in secondi, che
deve trascorrere senza traffico sul socket prima che vengano inviati, qualora si
sia attivata su di esso l’opzione SO_KEEPALIVE, i messaggi di keep-alive (si veda
la trattazione relativa al keep-alive in sez. 17.2.3). Anche questa opzione non è
disponibile su tutti i kernel unix-like e deve essere evitata se si vuole scrivere
codice portabile.
61
si tenga presente però che TCP_CORK può essere specificata insieme a TCP_NODELAY soltanto a partire dal kernel
2.5.71.
62
l’algoritmo cerca di tenere conto di queste situazioni, ma essendo un algoritmo generico tenderà comunque ad
introdurre delle suddivisioni in segmenti diversi, anche quando potrebbero non essere necessarie, con conseguente
spreco di banda.
63
l’opzione è stata introdotta con i kernel della serie 2.4.x.
614 CAPITOLO 17. LA GESTIONE DEI SOCKET
TCP_KEEPINTVL
con questa opzione si legge o si imposta l’intervallo 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
evitata se si vuole scrivere codice portabile.
TCP_KEEPCNT con questa opzione si legge o si imposta il numero totale di messaggi di keep-
alive da inviare prima di concludere che la connessione è caduta per assenza
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
si vuole scrivere codice portabile.
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 dall’opzione TCP_INFO.
TCP_INFO questa opzione, specifica di Linux, ma introdotta anche in altri kernel (ad esem-
pio FreeBSD) permette di controllare lo stato interno di un socket TCP diret-
tamente da un programma in user space. L’opzione restituisce in una speciale
65
su FreeBSD è presente una opzione SO_ACCEPTFILTER che consente di ottenere lo stesso comportamento di
TCP_DEFER_ACCEPT per quanto riguarda il lato server.
616 CAPITOLO 17. LA GESTIONE DEI SOCKET
struttura tcp_info, la cui definizione è riportata in fig. 17.19, tutta una serie di
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 effettuare dei controlli senza dover passare
attraverso delle operazioni di lettura. Ad esempio si può verificare se un socket è
stato chiuso usando una funzione analoga a quella illustrata in fig. 17.20, in cui
si utilizza il valore del campo tcpi_state di tcp_info per controllare lo stato
del socket.
Figura 17.20: Codice della funzione is_closing.c, che controlla lo stato di un socket TCP per verificare se si
sta chiudendo.
TCP_QUICKACK con questa opzione è possibile eseguire una forma di controllo sull’invio dei seg-
menti ACK all’interno di in flusso 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 im-
mediatamente i segmenti ACK, in quanto normalmente questo significa che si è
ricevuto un blocco di dati e si può passare all’elaborazione del blocco successivo.
Si tenga presente che l’opzione non è permanente, vale a dire che una volta che
la si sia impostata a 0 il kernel la riporterà al valore di default dopo il suo primo
utilizzo. Sul lato server la si può impostare anche una volta sola su un socket
in ascolto, ed essa verrà ereditata da tutti i socket che si otterranno da esso al
ritorno di accept.
66
caso tipico ad esempio delle risposte alle richieste HTTP.
17.2. LE OPZIONI DEI SOCKET 617
TCP_CONGESTION
questa opzione permette di impostare quale algoritmo per il controllo della con-
gestione67 utilizzare per il singolo socket. L’opzione è stata introdotta con il
kernel 2.6.13,68 e prende come per optval il puntatore ad un buffer contenente
il nome dell’algoritmo di controllo che si vuole usare.
L’uso di un nome anziché di un valore numerico è dovuto al fatto che gli algoritmi
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 atti-
vando l’opzione di configurazione generale TCP_CONG_ADVANCED,69 e poi abilitare
i singoli moduli voluti con le varie TCP_CONG_* presenti per i vari algoritmi di-
sponibili; un elenco di quelli attualmente supportati nella versione ufficiale del
kernel è riportato in tab. 17.17.70
Si tenga presente che prima della implementazione modulare alcuni di questi al-
goritmi erano disponibili soltanto come caratteristiche generali del sistema, atti-
vabili per tutti i socket, questo è ancora possibile con la sysctl tcp_congestion_control
(vedi sez. 17.4.3) che ha sostituito le precedenti sysctl.71
Tabella 17.17: Gli algoritmi per il controllo della congestione disponibili con Linux con le relative opzioni di
configurazione da attivare.
Il protocollo UDP, anche per la sua maggiore semplicità, supporta un numero ridotto di
opzioni, riportate in tab. 17.18; anche in questo caso per poterle utilizzare occorrerà impostare
l’opportuno valore per l’argomento level, che è SOL_UDP (o l’equivalente IPPROTO_UDP). Le
costanti che identificano dette opzioni sono definite in netinet/udp.h, ed accessibili includendo
detto file.72
Ancora una volta le descrizioni contenute tab. 17.18 sono un semplice riferimento, una mag-
giore quantità di dettagli sulle caratteristiche delle opzioni citate è quello dell’elenco seguente:
67
il controllo della congestione è un meccanismo previsto dal protocollo TCP (vedi sez. ??) per evitare di
trasmettere inutilmente dati quando una connessione è congestionata; un buon algoritmo è fondamentale per il
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.
68
alla data di stesura di queste note (Set. 2006) è pure scarsamente documentata, tanto che non è neanche
definita nelle intestazioni delle glibc per cui occorre definirla a mano al suo valore che è 13.
69
disponibile come TCP: advanced congestion control nel menù Network->Networking options, che a sua volta
renderà disponibile un ulteriore menù con gli algoritmi presenti.
70
la lista è presa dalla versione 2.6.17.
71
riportate anche, alla data di stesura di queste pagine (Set. 2006) nelle pagine di manuale, ma non più presenti.
72
come per TCP, la definizione delle opzioni effettivamente supportate dal kernel si trova in realtà nel file
linux/udp.h, dal quale si sono estratte le costanti di tab. 17.18.
618 CAPITOLO 17. LA GESTIONE DEI SOCKET
UDP_ENCAP Questa opzione permette di gestire l’incapsulazione dei dati nel protocollo UDP.
L’opzione è stata introdotta con il kernel 2.5.67, e non è documentata. Come la
precedente è specifica di Linux e non deve essere utilizzata in codice portabile.
SIOCSPGRP 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
stessi o il proprio process group.
73
il Round Trip Time cui abbiamo già accennato in sez. 14.3.4.
17.3. LA GESTIONE ATTRAVERSO LE FUNZIONI DI CONTROLLO 619
FIOASYNC Abilita o disabilita la modalità di I/O asincrono sul socket. Questo significa
(vedi sez. 12.3.1) che verrà inviato il segnale di SIGIO (o quanto impostato con
F_SETSIG, vedi sez. 6.3.6) in caso di eventi di I/O sul socket.
Nel caso dei socket generici anche fcntl prevede un paio di comandi specifici; in questo
caso il secondo argomento (cmd, che indica il comando) può assumere i due valori FIOGETOWN
e FIOSETOWN, mentre il terzo argomento dovrà essere un puntatore ad una variabile di tipo
pid_t. Questi due comandi sono una modalità alternativa di eseguire le stesse operazioni (lettura
o impostazione del processo o del gruppo di processo che riceve i segnali) che si effettuano
chiamando ioctl con SIOCGPGRP e SIOCSPGRP.
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 definizione è illustrata in fig. 17.21. Normalmente si utilizza il
primo campo della struttura, ifr_name per specificare il nome dell’interfaccia 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 è definito come una
union proprio in quanto il suo significato varia a secondo dell’operazione scelta.
Si tenga inoltre presente che alcune di queste operazioni (in particolare quelle che modificano
le caratteristiche dell’interfaccia) sono privilegiate e richiedono i privilegi di amministratore o la
620 CAPITOLO 17. LA GESTIONE DEI SOCKET
SIOCGIFNAME questa è l’unica operazione che usa il campo ifr_name per restituire un risul-
tato, tutte le altre lo utilizzano per indicare l’interfaccia sulla quale operare.
L’operazione richiede che si indichi nel campo ifr_ifindex il valore numerico
dell’indice dell’interfaccia, e restituisce il relativo nome in ifr_name.
Il kernel infatti assegna ad ogni interfaccia un numero progressivo, detto appun-
to interface index, che è quello che effettivamente la identifica nelle operazioni
a basso livello, il nome dell’interfaccia è soltanto una etichetta associata a detto
indice, che permette di rendere più comprensibile l’indicazione dell’interfaccia
all’interno dei comandi. Una modalità per ottenere questo valore è usare il
comando ip link, che fornisce un elenco delle interfacce presenti ordinato in
base a tale valore (riportato come primo campo).
SIOCGIFFLAGS permette di ottenere nel campo ifr_flags il valore corrente dei flag dell’inter-
faccia specificata (con ifr_name). Il valore restituito è una maschera binaria i
cui bit sono identificabili attraverso le varie costanti di tab. 17.19.
Flag Significato
IFF_UP L’interfaccia è attiva.
IFF_BROADCAST L’interfaccia ha impostato un indirizzo di broadcast
valido.
IFF_DEBUG È attivo il flag interno di debug.
IFF_LOOPBACK L’interfaccia è una interfaccia di loopback.
IFF_POINTOPOINT L’interfaccia è associata ad un collegamento punto-punto.
IFF_RUNNING L’interfaccia ha delle risorse allocate (non può quindi
essere disattivata).
IFF_NOARP L’interfaccia ha il protocollo ARP disabilitato o
l’indirizzo del livello di rete non è impostato.
IFF_PROMISC L’interfaccia è in modo promiscuo (riceve cioè tutti i pac-
chetti che vede passare, compresi quelli non direttamente
indirizzati a lei).
IFF_NOTRAILERS Evita l’uso di trailer nei pacchetti.
IFF_ALLMULTI Riceve tutti i pacchetti di multicast.
IFF_MASTER L’interfaccia è il master di un bundle per il bilanciamento
di carico.
IFF_SLAVE L’interfaccia è uno slave di un bundle per il bilanciamento
di carico.
IFF_MULTICAST L’interfaccia ha il supporto per il multicast attivo.
IFF_PORTSEL L’interfaccia può impostare i suoi parametri hardware
(con l’uso di ifmap).
IFF_AUTOMEDIA L’interfaccia è in grado di selezionare automaticamente
il tipo di collegamento.
IFF_DYNAMIC Gli indirizzi assegnati all’interfaccia vengono persi
quando questa viene disattivata.
Tabella 17.19: Le costanti che identificano i vari bit della maschera binaria ifr_flags che esprime i flag di una
interfaccia di rete.
SIOCSIFFLAGS permette di impostare il valore dei flag dell’interfaccia specificata (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 l’OR
aritmetico delle costanti di tab. 17.19; questa operazione è privilegiata.
17.3. LA GESTIONE ATTRAVERSO LE FUNZIONI DI CONTROLLO 621
SIOCGIFMETRIC permette di leggere il valore della metrica del dispositivo associato all’inter-
faccia specificata nel campo ifr_metric. Attualmente non è implementato, e
l’operazione restituisce sempre un valore nullo.
SIOCSIFMETRIC permette di impostare il valore della metrica del dispositivo al valore specificato
nel campo ifr_metric, attualmente non ancora implementato, restituisce un
errore di EOPNOTSUPP.
SIOCGIFMTU permette di leggere il valore della Maximum Transfer Unit del dispositivo nel
campo ifr_mtu.
SIOCSIFMTU permette di impostare il valore della Maximum Transfer Unit del dispositivo
al valore specificato campo ifr_mtu. L’operazione è privilegiata, e si tenga
presente che impostare un valore troppo basso può causare un blocco del kernel.
SIOCGIFMAP legge alcuni parametri hardware (memoria, interrupt, canali di DMA) del dri-
ver dell’interfaccia specificata, restituendo i relativi valori nel campo ifr_map;
quest’ultimo contiene una struttura di tipo ifmap, la cui definizione è illustrata
in fig. 17.22.
struct ifmap
{
unsigned long mem_start ;
unsigned long mem_end ;
unsigned short base_addr ;
unsigned char irq ;
unsigned char dma ;
unsigned char port ;
};
Figura 17.22: La struttura ifmap utilizzata per leggere ed impostare i valori dei parametri hardware di un driver
di una interfaccia.
struct ifconf {
int ifc_len ; /* size of buffer */
union {
char * ifc_buf ; /* buffer address */
struct ifreq * ifc_req ; /* array of structures */
};
};
Come esempio dell’uso di queste funzioni si è riportato in fig. 17.24 il corpo principale del
programma iflist in cui si utilizza l’operazione SIOCGIFCONF per ottenere una lista delle in-
terfacce attive e dei relativi indirizzi. Al solito il codice completo è fornito nei sorgenti allegati
alla guida.
Il programma inizia (7-11) con la creazione del socket necessario ad eseguire l’operazione,
dopo di che si inizializzano opportunamente (13-14) i valori della struttura ifconf indican-
do la dimensione del buffer ed il suo indirizzo;76 si esegue poi l’operazione 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 overflow
del buffer (21-23); se invece è tutto a posto (24-27) si calcola e si stampa a video il numero
di interfacce attive trovate. L’ultima parte del programma (28-33) è il ciclo sul contenuto delle
varie strutture ifreq restituite in cui si estrae (30) l’indirizzo ad esse assegnato77 e lo si stampa
(31-32) insieme al nome dell’interfaccia.
76
si noti come in questo caso si sia specificato l’indirizzo usando il campo ifc_buf, mentre nel seguito del
programma si accederà ai valori contenuti nel buffer usando ifc_req.
77
si è definito access come puntatore ad una struttura di tipo sockaddr_in per poter eseguire un casting
dell’indirizzo del valore restituito nei vari campi ifr_addr, cosı̀ poi da poterlo poi usare come argomento di
inet_ntoa.
624 CAPITOLO 17. LA GESTIONE DEI SOCKET
effettuare impostazioni relative alle proprietà dei socket. Dato che le stesse funzionalità sono
controllabili direttamente attraverso il filesystem /proc, le tratteremo attraverso i file presenti
in quest’ultimo.
/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ù significativi.
Si tenga presente infine che se è sempre possibile utilizzare il filesystem /proc come sostituto
di sysctl, dato che i valori di nodi e sotto-nodi di quest’ultima sono mappati come file e directory
sotto /proc/sys/, non è vero il contrario, ed in particolare Linux consente di impostare alcuni
parametri o leggere lo stato della rete a livello di sistema sotto /proc/net, dove sono presenti
dei file che non corrispondono a nessun nodo di sysctl.
rmem_default
imposta la dimensione di default del buffer di ricezione (cioè per i dati in ingresso)
dei socket.
rmem_max imposta la dimensione massima che si può assegnare al buffer di ricezione dei
socket attraverso l’uso dell’opzione SO_RCVBUF.
wmem_default
imposta la dimensione di default del buffer di trasmissione (cioè per i dati in
uscita) dei socket.
wmem_max imposta la dimensione massima che si può assegnare al buffer di trasmissione dei
socket attraverso l’uso dell’opzione SO_SNDBUF.
626 CAPITOLO 17. LA GESTIONE DEI SOCKET
message_cost, message_burst
contengono le impostazioni del bucket filter che controlla l’emissione 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.80
Il bucket filter è un algoritmo generico che permette di impostare dei limiti di
flusso su una quantità81 senza dovere eseguire medie temporali, che verrebbero
a dipendere in misura non controllabile dalla dimensione dell’intervallo su cui si
media e dalla distribuzione degli eventi;82 in questo caso si definisce la dimensione
di un “bidone” (il bucket) e del flusso che da esso può uscire, la presenza di una
dimensione iniziale consente di assorbire eventuali picchi di emissione, l’aver fissato
un flusso di uscita garantisce che a regime questo sarà il valore medio del flusso
ottenibile dal bucket.
I due valori indicano rispettivamente il flusso a regime (non sarà inviato più di un
messaggio per il numero di secondi specificato da message_cost) e la dimensione
iniziale per in caso di picco di emissione (verranno accettati inizialmente fino 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 file, la cui documentazione
dovrebbe essere mantenuta nei sorgenti del kernel, nel file Documentation/networking/ip-sysctl.txt;
la maggior parte di questi però non è documentato:
dev_weight blocco di lavoro (work quantum) dello scheduler di processo dei pacchetti.
lo_cong valore per l’occupazione della coda di ricezione sotto la quale si considera di avere
una bassa congestione.
mod_cong valore per l’occupazione della coda di ricezione sotto la quale si considera di avere
una congestione moderata.
no_cong valore per l’occupazione della coda di ricezione sotto la quale si considera di non
avere congestione.
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.
80
senza questo limite un attaccante potrebbe inviare ad arte un traffico che generi intenzionalmente messaggi
di errore, per saturare il sistema dei log.
81
uno analogo viene usato nel netfilter per imporre dei limiti sul flusso dei pacchetti.
82
in caso di un picco di flusso (il cosiddetto burst) il flusso medio verrebbe a dipendere in maniera esclusiva
dalla dimensione dell’intervallo di tempo su cui calcola la media.
17.4. LA GESTIONE CON SYSCTL ED IL FILESYSTEM /PROC 627
ip_forward abilita l’inoltro dei pacchetti da una interfaccia ad un altra, e può essere impostato
anche per la singola interfaccia. Prende un valore logico (0 disabilita, diverso da
zero abilita), di default è disabilitato.
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ò
essere modificato. Prende un valore intero, con 0 si disabilita la funzionalità, con
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.
ip_autoconfig
specifica se l’indirizzo IP è stato configurato automaticamente dal kernel all’avvio
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
dal kernel 2.6.18.
ip_local_port_range
imposta l’intervallo dei valori usati per l’assegnazione delle porte effimere, permet-
te cioè di modificare i valori illustrati in fig. 16.4; prende due valori interi separati
da spazi, che indicano gli estremi dell’intervallo. Si abbia cura di non definire 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
sempre un valore iniziale maggiore di 1024 (o meglio ancora di 4096) per evitare
conflitti con le porte usate dai servizi noti.
ip_no_pmtu_disc
permette di disabilitare per i socket SOCK_STREAM la ricerca automatica della Pa-
th MTU (vedi sez. 14.3.5 e sez. 17.2.4). Prende un valore logico, e di default è
disabilitato (cioè la ricerca viene eseguita).
In genere si abilita questo parametro quando per qualche motivo il procedimen-
to del Path MTU discovery fallisce; dato che questo può avvenire a causa di
83
l’unico motivo sarebbe per raggiungere macchine estremamente “lontane” in termini di hop, ma è praticamente
impossibile trovarne.
628 CAPITOLO 17. LA GESTIONE DEI SOCKET
I file di /proc/sys/net/ipv4 che invece fanno riferimento alle caratteristiche specifiche 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 è
troppo lento ed incapace di accettarle. Prende un valore logico ed è disabilitato
di default. Questo consente di recuperare le connessioni se si è avuto un eccesso
dovuto ad un qualche picco di traffico, ma ovviamente va a discapito dei client che
interrogano il server. Pertanto è da abilitare soltanto quando si è sicuri che non
è possibile ottimizzare il server in modo che sia in grado di accettare connessioni
più rapidamente.
tcp_adv_win_scale
indica al kernel quale frazione del buffer associato ad un socket87 deve essere
utilizzata per la finestra del protocollo TCP88 e quale come buffer applicativo per
isolare la rete dalle latenze dell’applicazione. Prende un valore intero che determina
la suddetta frazione secondo la formula buffer/2tcp_adv_win_scale se positivo o con
84
ad esempio se si scartano tutti i pacchetti ICMP, il problema è affrontato anche in sez. 1.4.4 di [17].
85
ad esempio se i due capi di un collegamento point-to-point non si accordano sulla stessa MTU.
86
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 l’opzione CONFIG_IP_ALWAYS_DEFRAG.
87
quello impostato con tcp_rmem.
88
in sostanza il valore che costituisce la advertised window annunciata all’altro capo del socket.
17.4. LA GESTIONE CON SYSCTL ED IL FILESYSTEM /PROC 629
tcp_dsack abilita il supporto, definito nell’RFC 2884, per il cosiddetto Duplicate SACK.89
Prende un valore logico e di default è abilitato.
tcp_ecn abilita il meccanismo della Explicit Congestion Notification (in breve ECN) nelle
connessioni TCP. Prende valore logico che di default è disabilitato. La Explicit
Congestion Notification è un meccanismo che consente di notificare quando una
rotta o una rete è congestionata da un eccesso di traffico,90 si può cosı̀ essere
avvisati e cercare rotte alternative oppure diminuire l’emissione di pacchetti (in
modo da non aumentare la congestione).
Si tenga presente che se si abilita questa opzione si possono avere dei malfunzio-
namenti 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 traffico.
tcp_frto abilita il supporto per l’algoritmo 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
che alla congestione dei router. Prende un valore logico e di default è disabilitato.
tcp_keepalive_intvl
indica il numero di secondi che deve trascorrere fra l’emissione di due successivi
pacchetti di test quando è abilitata la funzionalità del keepalive (vedi sez. 17.2.3).
Prende un valore intero che di default è 75.
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.
89
si indica con SACK (Selective Acknowledgement) un’opzione TCP, definita nell’RFC 2018, usata per dare
un acknowledgement unico su blocchi di pacchetti non contigui, che consente di diminuire il numero di pacchetti
scambiati.
90
il meccanismo è descritto in dettaglio nell’RFC 3168 mentre gli effetti sulle prestazioni del suo utilizzo sono
documentate nell’RFC 2884.
91
nei kernel della serie 2.2.x era il valore utilizzato era invece di 120 secondi.
630 CAPITOLO 17. LA GESTIONE DEI SOCKET
tcp_keepalive_time
indica il numero di secondi che devono passare senza traffico sulla connessione
prima che il kernel inizi ad inviare pacchetti di pacchetti di keepalive.92 Prende un
valore intero che di default è 7200, pari a due ore.
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 l’utilizzo della banda.
Prende un valore logico che di default è disabilitato in quanto un maggior utilizzo
della banda è preferito, ma esistono applicazioni particolari in cui la riduzione
della latenza è più importante (ad esempio per i cluster di calcolo parallelo) nelle
quali lo si può abilitare.
tcp_max_orphans
indica il numero massimo di socket TCP “orfani” (vale a dire non associati a
nessun file descriptor) consentito nel sistema.93 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
opportuno aumentarlo, ma si deve tenere conto del fatto che ciascuna connessione
orfana può consumare fino a 64K di memoria del kernel. Prende un valore intero,
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
per le quali si è ricevuto un SYN di richiesta ma non l’ACK finale del three way
handshake (si riveda quanto illustrato in sez. 16.2.3).
Quando questo valore è superato il kernel scarterà immediatamente ogni ulterio-
re richiesta di connessione. Prende un valore intero; il default, che è 256, viene
automaticamente portato a 1024 qualora nel sistema ci sia sufficiente memoria
(se maggiore di 128Mb) e ridotto a 128 qualora la memoria sia poca (inferiore a
32Mb).94
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 parame-
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; l’uso di questa funzionalità consente di prevenire alcuni semplici attacchi
di denial of service.
tcp_mem viene usato dallo stack TCP per gestire le modalità con cui esso utilizzerà la
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 pa-
gine allocate sotto il quale non viene usato nessun meccanismo di regolazione
dell’uso della memoria.
92
ha effetto solo per i socket per cui si è impostata l’opzione SO_KEEPALIVE (vedi sez. 17.2.3.
93
trattasi in genere delle connessioni relative a socket chiusi che non hanno completato il processo di chiusura.
94
si raccomanda, qualora si voglia aumentare il valore oltre 1024, di seguire la procedura citata nella pagina di
manuale di TCP, e modificare il valore della costante TCP_SYNQ_HSIZE nel file 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.
17.4. LA GESTIONE CON SYSCTL ED IL FILESYSTEM /PROC 631
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
problemi con il NAT.96
tcp_tw_reuse
abilita il riutilizzo dello stato TIME_WAIT quando questo è sicuro dal punto di vista
del protocollo. Prende un valore logico e di default è disabilitato.
tcp_window_scaling
un valore logico, attivo di default, che abilita la funzionalità del TCP window sca-
ling definita dall’RFC 1323. Prende un valore logico e di default è abilitato. Come
accennato in sez. 16.1.2 i 16 bit della finestra TCP comportano un limite massimo
di dimensione di 64Kb, ma esiste una opportuna opzione del protocollo che per-
mette 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
la negoziazione del TCP window scaling con l’altro capo della connessione non
viene effettuata.
tcp_wmem viene usato dallo stack TCP per controllare dinamicamente le dimensioni dei pro-
pri buffer 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 buf-
fer di spedizione; il default è 4Kb. Come per l’analogo di tcp_rmem) viene
usato per assicurare che anche in situazioni di pressione sulla memoria (vedi
tcp_mem) le allocazioni al di sotto di questo limite abbiamo comunque succes-
so. Di nuovo questo valore non viene ad incidere sulla dimensione del buffer
di trasmissione di un singolo socket dichiarata con l’opzione SO_SNDBUF.
• il secondo valore, denominato default, indica la dimensione di default in byte
del buffer 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.
Si può aumentare questo valore quando si desiderano dimensioni più am-
pie del buffer di trasmissione per i socket TCP, ma come per il precedente
tcp_rmem) se si vuole che in corrispondenza aumentino anche le dimensio-
ni usate per la finestra TCP si deve abilitare il TCP window scaling con
tcp_window_scaling.
• il terzo valore, denominato max, indica la dimensione massima in byte del
buffer di spedizione di un socket TCP; il default è 128Kb, che viene ri-
dotto automaticamente a 64Kb per sistemi con poca memoria. Il valore
non può comunque eccedere il limite generale per tutti i socket posto con
/proc/sys/net/core/wmem_max. Questo valore non viene ad incidere sul-
la dimensione del buffer di trasmissione di un singolo socket dichiarata con
l’opzione SO_SNDBUF.
96
il Network Address Translation è una tecnica, impiegata nei firewall e nei router, che consente di modificare
al volo gli indirizzi dei pacchetti che transitano per una macchina, Linux la supporta con il netfilter, per maggiori
dettagli si consulti il cap. 2 di [17].
634 CAPITOLO 17. LA GESTIONE DEI SOCKET
Capitolo 18
Dopo aver trattato in cap. 16 i socket TCP, che costituiscono l’esempio più comune dell’interfac-
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. 11.1.5.
635
636 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
connect (anche se, come vedremo in sez. 18.1.6, è possibile usare questa funzione, con un signifi-
cato comunque diverso) ma si potrà effettuare direttamente una richiesta inviando un pacchetto
con la funzione sendto e si potrà leggere una eventuale risposta con la funzione recvfrom.
Anche se UDP è completamente diverso rispetto a TCP resta identica la possibilità di gestire
più canali di comunicazione fra due macchine utilizzando le porte. In questo caso il server dovrà
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 verificare quali socket sono in ascolto:
[piccardi@gont gapil]# netstat -anu
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State
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:*
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
infatti non hanno uno stato. Inoltre anche in presenza di traffico non si avranno indicazioni
delle connessioni attive, proprio perché questo concetto non esiste per i socket UDP, il kernel
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.
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
specificare la destinazione dei dati trasmessi o ottenere l’origine dei dati ricevuti. La prima di
queste funzioni è sendto ed il suo prototipo2 è:
#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 l’operazione richiede che la funzione si
blocchi.
ECONNRESET l’altro capo della comunicazione ha resettato la connessione.
EDESTADDRREQ il socket non è di tipo connesso, e non si è specificato un indirizzo di destinazione.
EISCONN il socket è già connesso, ma si è specificato un destinatario.
EMSGSIZE il tipo di socket richiede l’invio dei dati in un blocco unico, ma la dimensione del
messaggio lo rende impossibile.
ENOBUFS la coda di uscita dell’interfaccia è già piena (di norma Linux non usa questo messaggio
ma scarta silenziosamente i pacchetti).
ENOTCONN il socket non è connesso e non si è specificata una destinazione.
EOPNOTSUPP il valore di flag non è appropriato per il tipo di socket usato.
EPIPE il capo locale della connessione è stato chiuso, si riceverà anche un segnale di SIGPIPE,
a meno di non aver impostato MSG_NOSIGNAL in flags.
ed anche EFAULT, EBADF, EINVAL, EINTR, ENOMEM, ENOTSOCK più gli eventuali altri errori relativi ai
protocolli utilizzati.
I primi tre argomenti sono identici a quelli della funzione write e specificano il socket sockfd
a cui si fa riferimento, il buffer 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
sempre corrispondere alla dimensione totale specificata 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 buffer di uscita la funzione si blocca (a meno di non avere aperto il socket in modalità
non bloccante), se invece non è possibile inviare il messaggio all’interno di un unico pacchetto (ad
esempio perché eccede le dimensioni massime del protocollo sottostante utilizzato) essa fallisce
con l’errore di EMSGSIZE.
I due argomenti to e tolen servono a specificare la destinazione del messaggio da inviare, e
indicano rispettivamente la struttura contenente l’indirizzo di quest’ultima e la sua dimensione;
questi argomenti vanno specificati stessa forma in cui li si sarebbero usati con connect. Nel
nostro caso to dovrà puntare alla struttura contenente l’indirizzo IP e la porta di destinazione
verso cui si vogliono inviare i dati (questo è indifferente rispetto all’uso di TCP o UDP, usando
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
deve essere già connesso prima di poter eseguire la funzione, in caso contrario si riceverà un
errore di ENOTCONN. In questo specifico 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
2
il prototipo illustrato è quello utilizzato dalle glibc, che seguono le Single Unix Specification, l’argomento
flags era di tipo int nei vari BSD4.*, mentre nelle libc4 e libc5 veniva usato un unsigned int; l’argomento len
era int nei vari BSD4.* e nelle libc4, ma size_t nelle libc5; infine l’argomento tolen era int nei vari BSD4.*
nelle libc4 e nelle libc5.
638 CAPITOLO 18. GLI ALTRI TIPI DI SOCKET
socket connesso essi vengono ignorati, ma qualora si sia specificato un indirizzo è possibile
ricevere un errore di EISCONN.
Finora abbiamo tralasciato l’argomento flags; questo è un intero usato come maschera
binaria che permette di impostare una serie di modalità di funzionamento della comunicazione
attraverso il socket (come MSG_NOSIGNAL che impedisce l’invio del segnale SIGPIPE quando si è
già chiuso il capo locale della connessione). Torneremo con maggiori dettagli sul significato di
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.
La seconda funzione utilizzata nella comunicazione fra socket UDP è recvfrom, che serve a
ricevere i dati inviati da un altro socket; il suo prototipo3 è:
#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:
EAGAIN il socket è in modalità non bloccante, ma l’operazione richiede che la funzione si
blocchi, oppure si è impostato un timeout in ricezione e questo è scaduto.
ECONNREFUSED l’altro capo della comunicazione ha rifiutato la connessione (in genere perché il
relativo servizio non è disponibile).
ENOTCONN il socket è di tipo connesso, ma non si è eseguita la connessione.
ed anche EFAULT, EBADF, EINVAL, EINTR, ENOMEM, ENOTSOCK più gli eventuali altri errori relativi ai
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 buffer 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
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.
I due argomenti from e fromlen sono utilizzati per ottenere l’indirizzo del mittente del pac-
chetto che è stato ricevuto, e devono essere opportunamente inizializzati; il primo deve contenere
il puntatore alla struttura (di tipo sockaddr) che conterrà l’indirizzo e il secondo il puntatore
alla variabile con la dimensione di detta struttura. Si tenga presente che mentre il contenuto
della struttura sockaddr cui punta from può essere qualunque, la variabile puntata da fromlen
deve essere opportunamente inizializzata a sizeof(sockaddr), assicurandosi che la dimensione
sia sufficiente a contenere tutti i dati dell’indirizzo.4 Al ritorno della funzione si otterranno i dati
dell’indirizzo e la sua effettiva lunghezza, (si noti che fromlen è un valore intero ottenuto come
value result argument). Se non si è interessati a questa informazione, entrambi gli argomenti
devono essere inizializzati al valore NULL.
Una differenza 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
con sendto un pacchetto vuoto (che nel caso conterrà solo le intestazioni di IP e di UDP),
specificando un valore nullo per len. Allo stesso modo è possibile ricevere con recvfrom un valore
3
il prototipo è quello delle glibc che seguono le Single Unix Specification, i vari BSD4.*, le libc4 e le libc5
usano un int come valore di ritorno; per gli argomenti flags e len vale quanto detto a proposito di sendto; infine
l’argomento fromlen è int per i vari BSD4.*, le libc4 e le libc5.
4
si ricordi che sockaddr è un tipo generico che serve ad indicare la struttura corrispondente allo specifico tipo
di indirizzo richiesto, il valore di fromlen pone un limite alla quantità di dati che verranno scritti sulla struttura
puntata da from e se è insufficiente l’indirizzo risulterà corrotto.
18.1. I SOCKET UDP 639
di ritorno di 0 byte, senza che questo possa configurarsi come una chiusura della connessione5 o
come una cessazione delle comunicazioni.
ed osservando il traffico con uno sniffer potremo effettivamente vedere lo scambio dei due
pacchetti, quello vuoto di richiesta, e la risposta del server:
5
dato che la connessione non esiste, non ha senso parlare di chiusura della connessione, questo significa anche
che con i socket UDP non è necessario usare close o shutdown per terminare la comunicazione.
6
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.
7
di norma questo è un servizio standard fornito dal superdemone inetd, per cui basta abilitarlo nel file di
configurazione di quest’ultimo, avendo cura di predisporre il servizio su UDP.
640 CAPITOLO 18. GLI ALTRI TIPI DI SOCKET
Figura 18.2: Sezione principale del client per il servizio daytime su UDP.
Una differenza fondamentale del nostro client è che in questo caso, non disponendo di una
connessione, è per lui impossibile riconoscere errori di invio relativi alla rete. La funzione sendto
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 sull’indirizzo di
destinazione. Questo comporta ad esempio che se si usa il nostro programma interrogando un
server inesistente questo resterà perennemente bloccato nella chiamata a recvfrom, fin quando
18.1. I SOCKET UDP 641
non lo interromperemo. Vedremo in sez. 18.1.6 come si può porre rimedio a questa problematica.
Figura 18.3: Sezione principale del server per il servizio daytime su UDP.
642 CAPITOLO 18. GLI ALTRI TIPI DI SOCKET
In fig. 18.3 è riportato la sezione principale del codice del nostro client, il sorgente completo
si trova nel file 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
è -v che permette di stampare a video l’indirizzo associato ad ogni richiesta).
Anche in questo caso la prima parte del server (9-23) è sostanzialmente identica a quella
dell’analogo server per TCP illustrato in fig. 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 differenza con il caso precedente è
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)
la famiglia dell’indirizzo, la porta (17) e l’indirizzo (18) su cui si riceveranno i pacchetti. Si
noti come in quest’ultimo sia l’indirizzo generico INADDR_ANY; questo significa (si ricordi quanto
illustrato in sez. 16.2.1) che il server accetterà pacchetti su uno qualunque degli indirizzi presenti
sulle interfacce di rete della macchina.
Completata l’inizializzazione tutto quello che resta da fare è eseguire (20-23) la chiamata a
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
i pacchetti a lui indirizzati, e non è più necessario chiamare successivamente listen. In questo
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
legato ad essi con bind, qualora questo sia il caso scriverà il contenuto all’interno del socket, cosı̀
che il programma possa leggerlo, altrimenti risponderà alla macchina che ha inviato il pacchetto
con un messaggio ICMP di tipo port unreachable.
Una volta completata la fase di inizializzazione inizia il corpo principale (24-44) del server,
mantenuto all’interno di un ciclo infinito 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
funzione è quello di ritornare tutte le volte che un pacchetto viene inviato al server, in modo da
poter ricavare da esso l’indirizzo del client a cui inviare la risposta in addr. Per questo motivo
in questo caso (al contrario di quanto fatto in fig. 18.2) si è avuto cura di passare gli argomenti
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 verifica (31) se è stata attivata l’opzione -v
(che imposta la variabile verbose) stampando nel caso (32-35) l’indirizzo da cui si è appena
ricevuto una richiesta (questa sezione è identica a quella del server TCP illustrato in fig. 16.10).
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
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
le richieste una alla volta via via che gli arrivano. Questa è una caratteristica comune dei server
UDP, conseguenza diretta del fatto che non esiste il concetto di connessione, per cui non c’è la
necessità di trattare separatamente le singole connessioni. Questo significa anche che è il kernel a
gestire la possibilità di richieste multiple in contemporanea; quello che succede è semplicemente
che il kernel accumula in un buffer 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
server distribuire le risposte sulla base dell’indirizzo da cui provengono le richieste.
estremamente semplice, dato che la comunicazione consiste sempre in una richiesta seguita da
una risposta, per uno scambio di dati effettuabile con un singolo pacchetto, per cui tutti gli
eventuali problemi sarebbero assai più complessi da rilevare.
Anche qui però possiamo notare che se il pacchetto di richiesta del client, o la risposta del
server si perdono, il client resterà permanentemente bloccato nella chiamata a recvfrom. Per
evidenziare meglio quali problemi si possono avere proviamo allora con un servizio leggermente
più complesso come echo.
Figura 18.4: Sezione principale della prima versione client per il servizio echo su UDP.
In fig. 18.4 è riportato un estratto del corpo principale del nostro client elementare per il
servizio echo (al solito il codice completo è con i sorgenti allegati). Le uniche differenze con
l’analogo client visto in fig. 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 è
identico, e tutto il lavoro viene effettuato attraverso la chiamata (28) alla funzione ClientEcho
che stavolta però prende un argomento in più, che è l’indirizzo del socket.
Ovviamente in questo caso il funzionamento della funzione, il cui codice è riportato in
fig. 18.5, è completamente diverso rispetto alla analoga del server TCP, e dato che non esi-
ste una connessione questa necessita anche di un terzo argomento, che è l’indirizzo del server cui
inviare i pacchetti.
Data l’assenza di una connessione come nel caso di TCP il meccanismo è molto più semplice
da gestire. Al solito si esegue un ciclo infinito (6-30) che parte dalla lettura (7) sul buffer di
invio sendbuff di una stringa dallo standard input, se la stringa è vuota (7-9), indicando che
644 CAPITOLO 18. GLI ALTRI TIPI DI SOCKET
Figura 18.5: Codice della funzione ClientEcho usata dal client per il servizio echo su UDP.
*
* Copyright (C) 2004 Simone Piccardi
...
...
/*
* Include needed headers
si otterrà che, dopo aver correttamente stampato alcune righe, il programma si blocca completa-
mente senza stampare più niente. Se al contempo si fosse tenuto sotto controllo il traffico UDP
diretto o proveniente dal servizio echo con tcpdump si sarebbe ottenuto:
che come si vede il traffico fra client e server si interrompe dopo l’invio di un pacchetto UDP
per il quale non si è ricevuto risposta.
Il problema è che in tutti i casi in cui un pacchetto di risposta si perde, o una richiesta non
arriva a destinazione, il nostro programma si bloccherà nell’esecuzione di recvfrom. Lo stesso
avviene anche se il server non è in ascolto, in questo caso però, almeno dal punto di vista dello
scambio di pacchetti, il risultato è diverso, se si lancia al solito il programma e si prova a scrivere
qualcosa si avrà ugualmente un blocco su recvfrom ma se si osserva il traffico con tcpdump si
vedrà qualcosa del tipo:
cioè in questo caso si avrà in risposta un pacchetto ICMP di destinazione irraggiungibile che ci
segnala che la porta in questione non risponde.
Ci si può chiedere allora perché, benché la situazione di errore sia rilevabile, questa non venga
segnalata. Il luogo più naturale in cui riportarla sarebbe la chiamata di sendto, in quanto è a
causa dell’uso di un indirizzo sbagliato che il pacchetto non può essere inviato; farlo in questo
punto però è impossibile, dato che l’interfaccia di programmazione richiede che la funzione ritorni
non appena il kernel invia il pacchetto,8 e non può bloccarsi in una attesa di una risposta che
potrebbe essere molto lunga (si noti infatti che il pacchetto ICMP arriva qualche decimo di
secondo più tardi) o non esserci affatto.
Si potrebbe allora pensare di riportare l’errore nella recvfrom che è comunque bloccata in
attesa di una risposta che nel caso non arriverà mai. La ragione per cui non viene fatto è piuttosto
sottile e viene spiegata da Stevens in [14] con il seguente esempio: si consideri un client che invia
8
questo è il classico caso di errore asincrono, una situazione cioè in cui la condizione di errore viene rilevata in
maniera asincrona rispetto all’operazione che l’ha causata, una eventualità piuttosto comune quando si ha a che
fare con la rete, tutti i pacchetti ICMP che segnalano errori rientrano in questa tipologia.
646 CAPITOLO 18. GLI ALTRI TIPI DI SOCKET
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
e porta del pacchetto che ha fallito, però tutto quello che il kernel può restituire al programma
è un codice di errore in errno, con il quale è impossibile di distinguere per quale dei pacchetti
inviati si è avuto l’errore; per questo è stata fatta la scelta di non riportare un errore su un
socket UDP, a meno che, come vedremo in sez. 18.1.6, questo non sia connesso.
Figura 18.6: Seconda versione del client del servizio echo che utilizza socket UDP connessi.
attesa di una risposta. Per risolvere questo problema l’unico modo sarebbe quello di impostare
un timeout o riscrivere il client in modo da usare l’I/O non bloccante.
Benché i socket Unix domain, come meccanismo di comunicazione fra processi che girano sulla
stessa macchina, non siano strettamente attinenti alla rete, li tratteremo comunque in que-
sta sezione. Nonostante le loro peculiarità infatti, l’interfaccia di programmazione che serve ad
utilizzarli resta sempre quella dei 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 definiscono una interfaccia di
comunicazione con il kernel, ed i packet socket che consentono di inviare pacchetti direttamente
a livello delle interfacce di rete.
648 CAPITOLO 18. GLI ALTRI TIPI DI SOCKET
Socket avanzati
Esamineremo in questo capitolo le funzionalità più evolute della gestione dei socket, le funzioni
avanzate, la gestione dei dati urgenti e out-of-band e dei messaggi ancillari, come l’uso come
l’uso del I/O multiplexing (vedi sez. 12.2) con i socket.
Finora abbiamo trattato delle funzioni che permettono di inviare dati sul socket in forma
semplificata. Se infatti si devono semplicemente ...
Quanto è stata attivata l’opzione IP_RECVERR il kernel attiva per il socket una speciale co-
da su cui vengono inviati tutti gli errori riscontrati. Questi possono essere riletti usando il
flag MSG_ERRQUEUE, nel qual caso sarà passato come messaggio ancillare una struttura di tipo
sock_extended_err illustrata in fig. 19.1.
struct sock_extended_err {
u_int32_t ee_errno ; /* error number */
u_int8_t ee_origin ; /* where the error originated */
u_int8_t ee_type ; /* type */
u_int8_t ee_code ; /* code */
u_int8_t ee_pad ;
u_int32_t ee_info ; /* additional information */
u_int32_t ee_data ; /* other data */
/* More data may follow */
};
Figura 19.1: La struttura sock_extended_err usata dall’opzione IP_RECVERR per ottenere le informazioni
relative agli errori su un socket.
649
650 CAPITOLO 19. SOCKET AVANZATI
Appendici
651
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 prin-
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,
denominata IPv6. Tratteremo comunque anche il protocollo ICMP e la sua versione modificata
per IPv6 (cioè ICMPv6).
A.1 Il protocollo IP
L’attuale Internet Protocol (IPv4) viene standardizzato nel 1981 dall’RFC 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
realizzato con le tecnologie più disparate (Ethernet, Token Ring, FDDI, ecc.).
A.1.1 Introduzione
Il compito principale di IP è quello di trasmettere i pacchetti da un computer all’altro della rete;
le caratteristiche essenziali con cui questo viene realizzato in IPv4 sono due:
• Universal addressing la comunicazione avviene fra due host identificati univocamente con
un indirizzo a 32 bit che può appartenere ad una sola interfaccia di rete.
• Best effort viene assicurato il massimo impegno nella trasmissione, ma non c’è nessuna
garanzia per i livelli superiori né sulla percentuale di successo né sul tempo di consegna
dei pacchetti di dati, né sull’ordine in cui vengono consegnati.
Per effettuare la comunicazione e l’instradamento dei pacchetti fra le varie reti di cui è
composta Internet IPv4 organizza gli indirizzi in una gerarchia a due livelli, in cui una parte dei
32 bit dell’indirizzo indica il numero di rete, e un’altra l’host 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 finale all’interno di detta rete.
1
per la spiegazione della suddivisione in livelli dei protocolli di rete, si faccia riferimento a quanto illustrato in
sez. 14.2.
653
654 APPENDICE A. IL LIVELLO DI RETE
Per garantire l’unicità dell’indirizzo Internet esiste un’autorità centrale (la IANA, Internet
Assigned Number Authority) che assegna i numeri di rete alle organizzazioni che ne fanno richie-
sta; è poi compito di quest’ultime assegnare i numeri dei singoli host all’interno della propria
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 all’interno
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 24 bit
classe A 0 net Id host Id
14 bit 16 bit
classe B 1 0 net Id host Id
21 bit 8 bit
classe C 1 1 0 net Id host 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
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 è
riservata per usi sperimentali e non viene impiegata.
Come si può notare però la suddivisione riportata in tab. A.1 è largamente inefficiente in
quanto se ad un utente necessita anche solo un indirizzo in più dei 256 disponibili con una classe
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 com-
porta 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 inefficienza nel trasporto.
n bit 32 − n bit
CIDR net Id host Id
Per questo nel 1992 è stato introdotto un indirizzamento senza classi (il CIDR, Classless
Inter-Domain Routing) in cui il limite fra i bit destinati a indicare il numero di rete e quello
destinati a indicare l’host finale può essere piazzato in qualunque punto (vedi tab. A.2), per-
mettendo di accorpare più classi A su un’unica rete o suddividere una classe B e diminuendo al
contempo il numero di indirizzi di rete da inserire nelle tabelle di instradamento dei router.
2
in realtà i valori esatti sarebbero 254 e 65536, una rete con a disposizione N bit dell’indirizzo IP, ha disponibili
per le singole macchine soltanto @N − 2 numeri, dato che uno deve essere utilizzato come indirizzo di rete e uno
per l’indirizzo di broadcast.
A.1. IL PROTOCOLLO IP 655
A.1.2 L’intestazione di IP
Come illustrato in fig. 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 fig. A.1.
Ciascuno dei campi illustrati in fig. A.1 ha un suo preciso scopo e significato, che si è riportato
brevemente in tab. A.3; si noti come l’intestazione riporti sempre due indirizzi IP, quello sorgente,
che indica l’IP da cui è partito il pacchetto (cioè l’indirizzo assegnato alla macchina che lo
spedisce) e quello destinazione che indica l’indirizzo a cui deve essere inviato il pacchetto (cioè
l’indirizzo assegnato alla macchina che lo riceverà).
Il campo TOS definisce il cosiddetto Type of Service; questo permette di definire il tipo di
traffico contenuto nei pacchetti, e può essere utilizzato dai router per dare diverse priorità in
base al valore assunto da questo campo. Abbiamo già visto come il valore di questo campo
può essere impostato sul singolo socket con l’opzione IP_TOS (vedi sez. 17.2.4), esso inoltre
può essere manipolato sia dal sistema del netfilter di Linux con il comando iptables che dal
sistema del routing avanzato del comando ip route per consentire un controllo più dettagliato
dell’instradamento dei pacchetti e l’uso di priorità e politiche di distribuzione degli stessi.
I possibili valori del campo TOS, insieme al relativo significato ed alle costanti numeriche ad
esso associati, sono riportati in tab. A.4. Per il valore nullo, usato di default per tutti i pacchetti,
e relativo al traffico normale, non esiste nessuna costante associata.
Il campo TTL, acromino di Time To Live, viene utilizzato per stabilire una sorta di tempo
di vita massimo dei pacchetti sulla rete. In realtà più che di un tempo, il campo serve a limitare
il numero massimo di salti (i cosiddetti hop) che un pacchetto IP può compiere nel passare da
un router ad un altro nel suo attraversamento della rete verso la destinazione.
Il protocollo IP prevede infatti che il valore di questo campo venga decrementato di uno
da ciascun router che ritrasmette il pacchetto verso la sua destinazione, e che quando questo
diventa nullo il router lo debba scartare, inviando all’indirizzo sorgente un pacchetto ICMP di
tipo time-exceeded con un codice ttl-zero-during-transit se questo avviene durante il transito
sulla rete o ttl-zero-during-reassembly se questo avviene alla destinazione finale (vedi sez. A.3).
In sostanza grazie all’uso di questo accorgimento un pacchetto non può continuare a vagare
indefinitamente sulla rete, e viene comunque scartato dopo un certo tempo, o meglio, dopo che ha
attraversato in certo numero di router. Nel caso di Linux il valore iniziale utilizzato normalmente
è 64 (vedi sez. 17.4.3).
656 APPENDICE A. IL LIVELLO DI RETE
Valore Significato
IPTOS_LOWDELAY 0x10 Minimizza i ritardi per rendere più veloce possibile la
ritrasmissione dei pacchetti (usato per traffico interattivo
di controllo come SSH).
IPTOS_THROUGHPUT 0x8 Ottimizza la trasmissione per rendere il più elevato pos-
sibile il flusso netto di dati (usato su traffico dati, come
quello di FTP).
IPTOS_RELIABILITY 0x4 Ottimizza la trasmissione per ridurre al massimo le per-
dite di pacchetti (usato su traffico soggetto a rischio di
perdita di pacchetti come TFTP o DHCP).
IPTOS_MINCOST 0x2 Indica i dati di riempimento, dove non interessa se si
ha una bassa velocità di trasmissione, da utilizzare per
i collegamenti con minor costo (usato per i protocolli di
streaming).
Normal-Service 0x0 Nessuna richiesta specifica.
Tabella A.4: Le costanti che definiscono alcuni valori standard per il campo TOS da usare come argomento
optval per l’opzione IP_TOS.
A.1.3 Le opzioni di IP
Da fare ...
L’attuale Internet Protocol (IPv4) viene standardizzato nel 1981 dall’RFC 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
realizzato con le tecnologie più disparate (Ethernet, Token Ring, FDDI, ecc.).
• un maggior numero di numeri disponibili che consentisse di non restare più a corto di
indirizzi
• uno spazio di indirizzi che consentisse un passaggio automatico dalle reti locali a internet
• un supporto per le opzioni migliorato, per garantire una trasmissione più efficiente del traf-
fico normale, limiti meno stringenti sulle dimensioni delle opzioni, e la flessibilità necessaria
per introdurne di nuove in futuro
• il supporto per delle capacità di qualità di servizio (QoS) che permetta di identificare
gruppi di dati per i quali si può provvedere un trattamento speciale (in vista dell’uso di
internet per applicazioni multimediali e/o “real-time”)
Come si può notare l’intestazione di IPv6 diventa di dimensione fissa, pari a 40 byte, contro
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
semplificazione 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 è
stato quello di ridurre al minimo il tempo di elaborazione dei pacchetti da parte dei router, un
confronto con l’intestazione di IPv4 (vedi fig. A.1) mostra le seguenti differenze:
• è stato eliminato il campo header length in quanto le opzioni sono state tolte dall’intesta-
zione che ha cosı̀ dimensione fissa; ci possono essere più intestazioni opzionali (intestazio-
ni di estensione, vedi sez. A.2.12), ciascuna delle quali avrà un suo campo di lunghezza
all’interno.
• l’intestazione e gli indirizzi sono allineati a 64 bit, questo rende più veloce il processo da
parte di computer con processori a 64 bit.
• i campi per gestire la frammentazione (identification, flag e fragment offset) sono stati
eliminati; questo perché la frammentazione è un’eccezione che non deve rallentare l’elabo-
razione dei pacchetti nel caso normale.
A.2. IL PROTOCOLLO IPV6 659
• è stato eliminato il campo checksum in quanto tutti i protocolli di livello superiore (TCP,
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 affidabilità); con questa
scelta si è ridotto di molto il tempo di elaborazione dato che i router non hanno più la
necessità di ricalcolare la checksum ad ogni passaggio di un pacchetto per il cambiamento
del campo hop limit.
• è stato eliminato il campo type of service, che praticamente non è mai stato utilizzato; una
parte delle funzionalità ad esso delegate sono state reimplementate (vedi il campo priority
al prossimo punto) con altri metodi.
• è stato introdotto un nuovo campo flow label, che viene usato, insieme al campo priority
(che recupera i bit di precedenza del campo type of service) per implementare la gestio-
ne di una “qualità di servizio” (vedi sez. A.2.13) che permette di identificare i pacchetti
appartenenti a un “flusso” di dati per i quali si può provvedere un trattamento speciale.
Oltre alle differenze precedenti, relative ai singoli campi nell’intestazione, ulteriori caratteri-
stiche che diversificano il comportamento di IPv4 da quello di IPv6 sono le seguenti:
• il broadcasting non è previsto in IPv6, le applicazioni che lo usano dovono essere reimple-
mentate usando il multicasting (vedi sez. A.2.10), che da opzionale diventa obbligatorio.
• IPv6 richiede il supporto per il path MTU discovery (cioè il protocollo per la selezione
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).
In realtà l’allocazione di questi indirizzi deve tenere conto della necessità di costruire delle
gerarchie che consentano un instradamento rapido ed efficiente dei pacchetti, e flessibilità nel
dispiegamento delle reti, il che comporta una riduzione drastica dei numeri utilizzabili; uno studio
sull’efficienza 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
in grado di fornire più di un migliaio di indirizzi per ogni metro quadro della superficie terrestre.
A.2.5 La notazione
Con un numero di bit quadruplicato non è più possibile usare la notazione coi numeri decimali
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
come separatore; cioè qualcosa del tipo 5f1b:df00:ce3e:e200:0020:0800:2078:e3e3.
Visto che la notazione resta comunque piuttosto pesante esistono alcune abbreviazioni; si può
evitare di scrivere gli zeri iniziali per cui si può scrivere 1080:0:0:0:8:800:ba98:2078:e3e3;
se poi un intero è zero si può omettere del tutto, cosı̀ come un insieme di zeri (ma questo solo
una volta per non generare ambiguità) per cui il precedente indirizzo si può scrivere anche come
1080::8:800:ba98:2078:e3e3.
Infine per scrivere un indirizzo IPv4 all’interno di un indirizzo IPv6 si può usare la vecchia
notazione con i punti, per esempio ::192.84.145.138.
Tabella A.6: Classificazione degli indirizzi IPv6 a seconda dei bit più significativi
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
nuovo. In IPv6 non esistono più gli indirizzi broadcast, la funzione di questi ultimi deve essere
reimplementata con gli indirizzi multicast.
Gli indirizzi unicast identificano una singola interfaccia: i pacchetti mandati ad un tale
indirizzo verranno inviati a quella interfaccia, gli indirizzi anycast identificano un gruppo di
interfacce tale che un pacchetto mandato a uno di questi indirizzi viene inviato alla più vicina
(nel senso di distanza di routing) delle interfacce del gruppo, gli indirizzi multicast identificano
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ù significativi indicano il tipo di indirizzo; in
tab. A.6 sono riportati i valori di detti bit e il tipo di indirizzo che loro corrispondente. I bit
più significativi costituiscono quello che viene chiamato il format prefix ed è sulla base di questo
che i vari tipi di indirizzi vengono identificati. Come si vede questa architettura di allocazione
supporta l’allocazione di indirizzi per i provider, per uso locale e per il multicast; inoltre è stato
riservato lo spazio per indirizzi NSAP, IPX e per le connessioni; gran parte dello spazio (più del
70%) è riservato per usi futuri.
Si noti infine che gli indirizzi anycast non sono riportati in tab. A.6 in quanto allocati al di
fuori dello spazio di allocazione degli indirizzi unicast.
Al livello più alto la IANA può delegare l’allocazione a delle autorità regionali (i Regional
Register) assegnando ad esse dei blocchi di indirizzi; a queste autorità regionali è assegnato un
Registry Id che deve seguire immediatamente il prefisso di formato. Al momento sono definite
tre registri regionali (INTERNIC, RIPE NCC e APNIC), inoltre la IANA si è riservata la
possibilità di allocare indirizzi su base regionale; pertanto sono previsti i seguenti possibili valori
per il Registry Id; gli altri valori restano riservati per la IANA.
Regione Registro Id
Nord America INTERNIC 11000
Europa RIPE NCC 01000
Asia APNIC 00100
Multi-regionale IANA 10000
L’organizzazione degli indirizzi prevede poi che i due livelli successivi, di suddivisione fra
Provider Id, che identifica i grandi fornitori di servizi, e Subscriber Id, che identifica i fruitori, sia
gestita dai singoli registri regionali. Questi ultimi dovranno definire come dividere lo spazio di
indirizzi assegnato a questi due campi (che ammonta a un totale di 56 bit), definendo lo spazio
662 APPENDICE A. IL LIVELLO DI RETE
da assegnare al Provider Id e al Subscriber Id, ad essi spetterà inoltre anche l’allocazione dei
numeri di Provider Id ai singoli fornitori, ai quali sarà delegata l’autorità di allocare i Subscriber
Id al loro interno.
L’ultimo livello è quello Intra-subscriber che è lasciato alla gestione dei singoli fruitori finali,
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
dove l’Interface Id è dato dal MAC-address a 48 bit dello standard Ethernet, scritto in genere
nell’hardware delle scheda di rete, e si usano i restanti 16 bit per indicare la sottorete.
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 sotto-reti,
il precedente schema andrebbe modificato, per evitare l’enorme spreco dovuto all’uso dei MAC-
address, a questo scopo si possono usare le capacità di auto-configurazione di IPv6 per assegnare
indirizzi generici con ulteriori gerarchie per sfruttare efficacemente tutto lo spazio di indirizzi.
Un registro regionale può introdurre un ulteriore livello nella gerarchia degli indirizzi, allo-
cando dei blocchi per i quali delegare l’autorità a dei registri nazionali, quest’ultimi poi avranno
il compito di gestire la attribuzione degli indirizzi per i fornitori di servizi nell’ambito del/i paese
coperto dal registro nazionale con le modalità viste in precedenza. Una tale ripartizione andrà
effettuata all’interno dei soliti 56 bit come mostrato in tab. A.10.
3 5 bit n bit m bit 56-n-m bit 64 bit
3 Reg. Naz. Prov. Subscr. Intra-Subscriber
Tabella A.10: Formato di un indirizzo unicast provider-based che prevede un registro nazionale.
10 54 bit 64 bit
FE80 0000 . . . . . 0000 Interface Id
Ci sono due tipi di indirizzi, link-local e site-local. Il primo è usato per un singolo link; la
struttura è mostrata in tab. A.11, questi indirizzi iniziano sempre con un valore nell’intervallo
FE80–FEBF e vengono in genere usati per la configurazione automatica dell’indirizzo 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 l’indirizzamento all’interno di un sito che non neces-
sita di un prefisso globale; la struttura è mostrata in tab. A.12, questi indirizzi iniziano sempre
con un valore nell’intervallo FEC0–FEFF e non devono venire ritrasmessi dai router all’esterno
del sito stesso; sono in sostanza gli equivalenti degli indirizzi riservati per reti private definiti su
IPv4. Per entrambi gli indirizzi il campo Interface Id è un identificatore che deve essere unico nel
A.2. IL PROTOCOLLO IPV6 663
dominio in cui viene usato, un modo immediato per costruirlo è quello di usare il MAC-address
delle schede di rete.
10 38 bit 16 bit 64 bit
FEC0 0000 . . . 0000 Subnet Id Interface Id
Gli indirizzi di uso locale consentono ad una organizzazione che non è (ancora) connessa ad
Internet di operare senza richiedere un prefisso globale, una volta che in seguito l’organizzazione
venisse connessa a Internet potrebbe continuare a usare la stessa suddivisione effettuata con gli
indirizzi site-local utilizzando un prefisso globale e la rinumerazione degli indirizzi delle singole
macchine sarebbe automatica.
Un secondo tipo di indirizzi di compatibilità sono gli IPv4 compatibili IPv6 (vedi tab. A.14)
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.
Altri indirizzi speciali sono il loopback address, costituito da 127 zeri ed un uno (cioè ::1) e
l’indirizzo generico costituito da tutti zeri (scritto come 0::0 o ancora più semplicemente come
:) usato in genere quando si vuole indicare l’accettazione di una connessione da qualunque host.
A.2.10 Multicasting
Gli indirizzi multicast sono usati per inviare un pacchetto a un gruppo di interfacce; l’indirizzo
identifica uno specifico gruppo di multicast e il pacchetto viene inviato a tutte le interfacce di
detto gruppo. Un’interfaccia può appartenere ad un numero qualunque numero di gruppi di
multicast. Il formato degli indirizzi multicast è riportato in tab. A.15:
Il prefisso di formato per tutti gli indirizzi multicast è FF, ad esso seguono i due campi il cui
significato è il seguente:
664 APPENDICE A. IL LIVELLO DI RETE
8 4 4 112 bit
FF flag scop Group Id
• flag: un insieme di 4 bit, di cui i primi tre sono riservati e posti a zero, l’ultimo è zero se
l’indirizzo è permanente (cioè un indirizzo noto, assegnato dalla IANA), ed è uno se invece
l’indirizzo è transitorio.
• scop è un numero di quattro bit che indica il raggio di validità dell’indirizzo, i valori
assegnati per ora sono riportati in tab. A.16.
Gruppi di multicast
0 riservato 8 organizzazione locale
1 nodo locale 9 non assegnato
2 collegamento locale A non assegnato
3 non assegnato B non assegnato
4 non assegnato C non assegnato
5 sito locale D non assegnato
6 non assegnato E globale
7 non assegnato F riservato
Infine l’ultimo campo identifica il gruppo di multicast, sia permanente che transitorio, all’in-
terno del raggio di validità del medesimo. Alcuni indirizzi multicast, riportati in tab. A.17 sono
già riservati per il funzionamento della rete.
L’utilizzo del campo di scope e di questi indirizzi predefiniti serve a recuperare le funzio-
nalità del broadcasting (ad esempio inviando un pacchetto all’indirizzo FF02:0:0:0:0:0:0:1 si
raggiungono tutti i nodi locali).
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
l’interfaccia più vicina, compito che tocca al sistema di instradamento (in sostanza la sorgente
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 (configurando gli
indirizzi anycast per identificare i router di uno stesso provider).
Questi indirizzi pertanto possono essere usati come indirizzi intermedi in una intestazione
di instradamento o per identificare insiemi di router connessi a una particolare sottorete, o che
forniscono l’accesso a un certo sotto dominio.
L’idea alla base degli indirizzi anycast è perciò quella di utilizzarli per poter raggiungere il
fornitore di servizio più vicino; ma restano aperte tutta una serie di problematiche, visto che
una connessione con uno di questi indirizzi non è possibile, dato che per una variazione delle
distanze di routing non è detto che due pacchetti successivi finiscano alla stessa interfaccia.
La materia è pertanto ancora controversa e in via di definizione.
A.2.12 Le estensioni
Come già detto in precedenza IPv6 ha completamente cambiato il trattamento delle opzioni;
queste ultime infatti sono state tolte dall’intestazione del pacchetto, e poste in apposite inte-
stazioni di estensione (o extension header ) poste fra l’intestazione di IPv6 e l’intestazione del
protocollo di trasporto.
Per aumentare la velocità di elaborazione, sia dei dati del livello seguente che di ulterio-
ri opzioni, ciascuna estensione deve avere una lunghezza multipla di 8 byte per mantenere
l’allineamento a 64 bit di tutti le intestazioni seguenti.
Dato che la maggior parte di queste estensioni non sono esaminate dai router durante l’instra-
damento e la trasmissione dei pacchetti, ma solo all’arrivo alla destinazione finale, questa scelta
ha consentito un miglioramento delle prestazioni rispetto a IPv4 dove la presenza di un’opzione
comportava l’esame di tutte quante.
Un secondo miglioramento è che rispetto a IPv4 le opzioni possono essere di lunghezza
arbitraria e non limitate a 40 byte; questo, insieme al modo in cui vengono trattate, consente di
utilizzarle per scopi come l’autenticazione e la sicurezza, improponibili con IPv4.
Le estensioni definite al momento sono le seguenti:
• Destination options opzioni che devono venire esaminate al nodo di ricevimento, nessuna
di esse è tuttora definita.
• Routing definisce una source route (come la analoga opzione di IPv4) cioè una lista di
indirizzi IP di nodi per i quali il pacchetto deve passare.
La presenza di opzioni è rilevata dal valore del campo next header che indica qual è l’intesta-
zione successiva a quella di IPv6; in assenza di opzioni questa sarà l’intestazione di un protocollo
di trasporto del livello superiore, per cui il campo assumerà lo stesso valore del campo protocol
di IPv4, altrimenti assumerà il valore dell’opzione presente; i valori possibili sono riportati in
tab. A.18.
Valore Keyword Tipo di protocollo
0 Riservato.
HBH Hop by Hop.
1 ICMP Internet Control Message (IPv4 o IPv6).
2 IGMP Internet Group Management (IPv4).
3 GGP Gateway-to-Gateway.
4 IP IP in IP (IPv4 encapsulation).
5 ST Stream.
6 TCP Trasmission Control.
17 UDP User Datagram.
43 RH Routing Header (IPv6).
44 FH Fragment Header (IPv6).
45 IDRP Inter Domain Routing.
51 AH Authentication Header (IPv6).
52 ESP Encrypted Security Payload (IPv6).
59 Null No next header (IPv6).
88 IGRP Internet Group Routing.
89 OSPF Open Short Path First.
255 Riservato.
Questo meccanismo permette la presenza di più opzioni in successione prima del pacchetto
del protocollo di trasporto; l’ordine raccomandato per le estensioni è quello riportato nell’elenco
precedente con la sola differenza 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 finale vanno in coda.
Ci possono essere più flussi attivi fra un’origine e una destinazione, come del traffico non
assegnato a nessun flusso, un flusso viene identificato univocamente dagli indirizzi di origine e
destinazione e da una etichetta di flusso diversa da zero, il traffico normale deve avere l’etichetta
di flusso posta a zero.
L’etichetta di flusso è assegnata dal nodo di origine, i valori devono essere scelti in manie-
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à
Il campo di priorità consente di indicare il livello di priorità dei pacchetti relativamente agli altri
pacchetti provenienti dalla stessa sorgente. I valori sono divisi in due intervalli, i valori da 0 a 7
sono usati per specificare la priorità del traffico per il quale la sorgente provvede un controllo di
congestione cioè per il traffico che può essere “tirato indietro” in caso di congestione come quello
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 traffico con controllo di congestione sono raccomandati i seguenti valori di priorità a
seconda del tipo di applicazione:
Per il traffico senza controllo di congestione la priorità più bassa dovrebbe essere usata per
quei pacchetti che si preferisce siano scartati più facilmente in caso di congestione.
• un carico di sicurezza (Encrypted Security Payload ) che assicura che solo il legittimo
ricevente può leggere il pacchetto.
Perché tutto questo funzioni le stazioni sorgente e destinazione devono usare una stessa chiave
crittografica e gli stessi algoritmi, l’insieme degli accordi fra le due stazioni per concordare chiavi
e algoritmi usati va sotto il nome di associazione di sicurezza.
668 APPENDICE A. IL LIVELLO DI RETE
A.2.17 Autenticazione
Il primo meccanismo di sicurezza è quello dell’intestazione di autenticazione (authentication hea-
der ) che fornisce l’autenticazione e il controllo di integrità (ma senza riservatezza) dei pacchetti
IP.
L’intestazione di autenticazione ha il formato descritto in fig. A.3: il campo Next Header
indica l’intestazione successiva, con gli stessi valori del campo omonimo nell’intestazione princi-
pale di IPv6, il campo Length indica la lunghezza dell’intestazione di autenticazione in numero
di parole a 32 bit, il campo riservato deve essere posto a zero, seguono poi l’indice di sicurezza,
stabilito nella associazione di sicurezza, e un numero di sequenza che la stazione sorgente deve
incrementare di pacchetto in pacchetto.
Completano l’intestazione 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
di 32 bit e può contenere un padding per allineare l’intestazione a 64 bit. Tutti gli algoritmi di
autenticazione devono provvedere questa capacità.
L’intestazione di autenticazione può essere impiegata in due modi diverse modalità: modalità
trasporto e modalità tunnel.
La modalità trasporto è utilizzabile solo per comunicazioni fra stazioni singole che supportino
l’autenticazione. In questo caso l’intestazione di autenticazione è inserita dopo tutte le altre
intestazioni di estensione eccezion fatta per la Destination Option che può comparire sia prima
che dopo.
La modalità tunnel può essere utilizzata sia per comunicazioni fra stazioni singole che con
un gateway di sicurezza; in questa modalità ...
L’intestazione di autenticazione è una intestazione di estensione inserita dopo l’intestazione
principale e prima del carico dei dati. La sua presenza non ha perciò alcuna influenza sui livelli
superiori dei protocolli di trasmissione come il TCP.
A.2. IL PROTOCOLLO IPV6 669
A.2.18 Riservatezza
Per garantire una trasmissione riservata dei dati, è stata previsto la possibilità di trasmettere
pacchetti con i dati criptati: il cosiddetto ESP, Encripted Security Payload. Questo viene rea-
lizzato usando con una apposita opzione che deve essere sempre l’ultima 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 fig. A.5, tutti i campi sono in chiaro fino al vettore di inizializzazione, il resto è crittografato.
A.2.19 Auto-configurazione
Una delle caratteristiche salienti di IPv6 è quella dell’auto-configurazione, il protocollo infatti
fornisce la possibilità ad un nodo di scoprire automaticamente il suo indirizzo acquisendo i
parametri necessari per potersi connettere a internet.
L’auto-configurazione 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
fisico a 48 bit unico; pertanto il nodo può assumere automaticamente senza pericoli di collisione
l’indirizzo link-local FE80::xxxx:xxxx:xxxx dove xxxx:xxxx:xxxx è l’indirizzo hardware della
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
xxxx:xxxx:xxxx sarà generato casualmente; in questo caso la probabilità di collisione è di 1
su 300 milioni. In ogni caso per prevenire questo rischio il nodo invierà un messaggio ICMP
670 APPENDICE A. IL LIVELLO DI RETE
Solicitation all’indirizzo scelto attendendo un certo lasso di tempo; in caso di risposta l’indi-
rizzo è duplicato e il procedimento dovrà essere ripetuto con un nuovo indirizzo (o interrotto
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 auto-configurazione, descritte nelle seguenti
sezioni. In ogni caso l’indirizzo link-local resta valido.
Valore Codice
network-unreachable 0
host-unreachable 1
protocol-unreachable 2
port-unreachable 3
fragmentation-needed 4
source-route-failed 5
network-unknown 6
host-unknown 7
host-isolated 8
network-prohibited 9
host-prohibited 10
TOS-network-unreachable 11
TOS-host-unreachable 12
communication-prohibited 13
host-precedence-violation 14
precedence-cutoff 15
network-redirect 0
host-redirect 1
TOS-network-redirect 2
TOS-host-redirect 3
ttl-zero-during-transit 0
ttl-zero-during-reassembly 1
ip-header-bad 0
required-option-missing 1
Il livello di trasporto
1
al solito per la definizione dei livelli si faccia riferimento alle spiegazioni fornite in sez. 14.2.
673
674 APPENDICE B. IL LIVELLO DI TRASPORTO
I codici di errore
Si riportano in questa appendice tutti i codici di errore. Essi sono accessibili attraverso l’inclu-
sione del file di header errno.h, che definisce anche la variabile globale errno. Per ogni errore
definito riporteremo la stringa stampata da perror ed una breve spiegazione. Si tenga presente
che spiegazioni più particolareggiate del significato dell’errore, qualora necessarie per casi speci-
fici, possono essere trovate nella descrizione del prototipo della funzione per cui detto errore si
è verificato.
I codici di errore sono riportati come costanti di tipo int, i valori delle costanti sono definiti
da macro di preprocessore nel file citato, e possono variare da architettura a architettura; è
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.
EPERM Operation not permitted. L’operazione non è permessa: solo il proprietario del file o un
processo con sufficienti privilegi può eseguire l’operazione.
ENOENT No such file or directory. Il file 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 file che si suppone erroneamente essere esistente.
EIO Input/output error. Errore di input/output: usato per riportare errori hardware in lettu-
ra/scrittura su un dispositivo.
ENXIO No such device or address. Dispositivo inesistente: il sistema ha tentato di usare un
dispositivo attraverso il file specificato, ma non lo ha trovato. Può significare che il file di
dispositivo non è corretto, che il modulo relativo non è stato caricato nel kernel, o che il
dispositivo è fisicamente assente o non funzionante.
ENOEXEC Invalid executable file format. Il file non ha un formato eseguibile, è un errore riscon-
trato dalle funzioni exec.
EBADF Bad file descriptor. File descriptor non valido: si è usato un file descriptor inesistente, o
aperto in sola lettura per scrivere, o viceversa, o si è cercato di eseguire un’operazione non
consentita per quel tipo di file descriptor.
EACCES Permission denied. Permesso negato; l’accesso al file o alla directory non è consentito:
i permessi del file o della directory non consentono l’operazione richiesta.
675
676 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 file o una
directory.
ENOTBLK Block device required. Si è specificato un file che non è un block device in un contesto
in cui era necessario specificare un block device (ad esempio si è tentato di montare un file
ordinario).
EEXIST File exists. Si è specificato un file esistente in un contesto in cui ha senso solo specificare
un nuovo file.
EBUSY Resource busy. Una risorsa di sistema che non può essere condivisa è occupata. Ad
esempio si è tentato di cancellare la directory su cui si è montato un filesystem.
EXDEV Cross-device link. Si è tentato di creare un link diretto che attraversa due filesystem
differenti.
ENODEV No such device. Si è indicato un tipo di device sbagliato ad una funzione che ne richiede
uno specifico.
ENOTDIR Not a directory. Si è specificato un file che non è una directory in una operazione che
richiede una directory.
EISDIR Is a directory. Il file specificato è una directory; non può essere aperto in scrittura, né
si possono creare o rimuovere link diretti ad essa.
EMFILE Too many open files. Il processo corrente ha troppi file aperti e non può aprirne altri.
Anche i descrittori duplicati ed i socket vengono tenuti in conto.1
ENFILE File table overflow. Il sistema ha troppi file aperti in contemporanea. Si tenga presente
che anche i socket contano come file. Questa è una condizione temporanea, ed è molto
difficile che si verifichi nei sistemi moderni.
ETXTBSY Text file busy. Si è cercato di eseguire un file che è aperto in scrittura, o di scrivere su
un file che è in esecuzione.
EFBIG File too big. Si è ecceduto il limite imposto dal sistema sulla dimensione massima che un
file può avere.
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 file su un dispositivo che è già
pieno.
ESPIPE Invalid seek operation. Si cercato di eseguire una seek su un file che non supporta
questa operazione (ad esempio su una pipe).
EROFS Read-only file system. Si è cercato di eseguire una operazione di scrittura su un file o
una directory che risiede su un filesystem montato un sola lettura.
1
il numero massimo di file aperti è controllabile dal sistema; in Linux si può impostare usando il comando
ulimit, esso è in genere indicato dalla costante OPEN_MAX, vedi sez. 8.1.1.
C.2. GLI ERRORI DEI PROCESSI 677
EMLINK Too many links. Ci sono già troppi link al file (il numero massimo è specificato dalla
variabile LINK_MAX, vedi sez. 8.1.1).
EPIPE Broken pipe. Non c’è un processo che stia leggendo l’altro capo della pipe. Ogni funzione
che restituisce questo errore genera anche un segnale SIGPIPE, la cui azione predefinita è
terminare il programma; pertanto non si potrà vedere questo errore fintanto che SIGPIPE
non viene gestito o bloccato.
ENOTEMPTY Directory not empty. La directory non è vuota quando l’operazione richiede che lo
sia. È l’errore tipico che si ha quando si cerca di cancellare una directory contenente dei
file.
EUSERS Too many users. Troppi utenti, il sistema delle quote rileva troppi utenti nel sistema.
ESTALE Stale NFS file handle. Indica un problema interno a NFS causato da cambiamenti del
filesystem del sistema remoto. Per recuperare questa condizione in genere è necessario
smontare e rimontare il filesystem NFS.
EREMOTE Object is remote. Si è fatto un tentativo di montare via NFS un filesystem remoto con
un nome che già specifica un filesystem montato via NFS.
ENOLCK No locks available. È usato dalle utilità per la gestione del file locking; non viene
generato da un sistema GNU, ma può risultare da un’operazione su un server NFS di un
altro sistema.
EFTYPE Inappropriate file type or format. Il file è di tipo sbagliato rispetto all’operazione ri-
chiesta o un file di dati ha un formato sbagliato. Alcuni sistemi restituiscono questo errore
quando si cerca di impostare lo sticky bit su un file che non è una directory.
ESRCH No process matches the specified process ID. Non esiste un processo o un process group
corrispondenti al valore dell’identificativo specificato.
E2BIG Argument list too long. La lista degli argomenti passati è troppo lunga: è una condizione
prevista da POSIX quando la lista degli argomenti passata ad una delle funzioni exec
occupa troppa memoria, non può mai accadere in GNU/Linux.
ECHILD There are no child processes. Non esistono processi figli di cui attendere la terminazione.
Viene rilevato dalle funzioni wait e waitpid (vedi sez. 3.2.4).
EPROCLIM Too many processes. Il limite dell’utente per nuovi processi (vedi sez. 8.3.2) sarà
ecceduto alla prossima fork; è un codice di errore di BSD, che non viene utilizzato al
momento su Linux.
678 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 un’opzione per il
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 specificazione del protocollo).
ESOCKTNOSUPPORT Socket type not supported. Socket non supportato. Il tipo di socket scelto
non è supportato.
EOPNOTSUPP Operation not supported on transport endpoint. L’operazione richiesta non è sup-
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 effettuata.
EPFNOSUPPORT Protocol family not supported. Famiglia di protocolli non supportata. La famiglia
di protocolli richiesta non è supportata.
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
dal socket.
EADDRINUSE Address already in use. L’indirizzo del socket richiesto è già utilizzato (ad esempio
si è eseguita bind su una porta già in uso).
EADDRNOTAVAIL Cannot assign requested address. L’indirizzo richiesto non è disponibile (ad
esempio si è cercato di dare al socket un nome che non corrisponde al nome della stazione
locale), o l’interfaccia richiesta non esiste.
ENETDOWN Network is down. L’operazione sul socket è fallita perché la rete è sconnessa.
ENETRESET Network dropped connection because of reset. Una connessione è stata cancellata
perché l’host remoto è caduto.
ECONNABORTED Software caused connection abort. Una connessione è stata abortita localmente.
ECONNRESET Connection reset by peer. Una connessione è stata chiusa per ragioni fuori dal
controllo dell’host locale, come il riavvio di una macchina remota o un qualche errore non
recuperabile sul protocollo.
C.4. ERRORI GENERICI 679
ENOBUFS No buffer space available. Tutti i buffer per le operazioni di I/O del kernel sono occupa-
ti. In generale questo errore è sinonimo di ENOMEM, ma attiene alle funzioni di input/output.
In caso di operazioni sulla rete si può ottenere questo errore invece dell’altro.
EISCONN Transport endpoint is already connected. Si è tentato di connettere un socket che è già
connesso.
ENOTCONN Transport endpoint is not connected. Il socket non è connesso a niente. Si ottiene
questo errore quando si cerca di trasmettere dati su un socket senza avere specificato in
precedenza la loro destinazione. Nel caso di socket senza connessione (ad esempio socket
UDP) l’errore che si ottiene è EDESTADDRREQ.
ESHUTDOWN Cannot send after transport endpoint shutdown. Il socket su cui si cerca di inviare
dei dati ha avuto uno shutdown.
ETIMEDOUT Connection timed out. Un’operazione sul socket non ha avuto risposta entro il
periodo di timeout.
EINTR Interrupted function call. Una funzione di libreria è stata interrotta. In genere questo
avviene causa di un segnale asincrono al processo che impedisce la conclusione della chia-
mata, 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.
ENOMEM No memory available. Il kernel non è in grado di allocare ulteriore memoria per
completare l’operazione richiesta.
EDEADLK Deadlock avoided. L’allocazione di una risorsa avrebbe causato un deadlock. Non
sempre il sistema è in grado di riconoscere queste situazioni, nel qual caso si avrebbe
il blocco.
EFAULT Bad address. Una stringa passata come argomento è fuori dello spazio di indirizzi del
processo, in genere questa situazione provoca direttamente l’emissione di un segnale di
segment violation (SIGSEGV).
EINVAL Invalid argument. Errore utilizzato per segnalare vari tipi di problemi dovuti all’aver
passato un argomento sbagliato ad una funzione di libreria.
680 APPENDICE C. I CODICI DI ERRORE
EDOM Domain error. È usato dalle funzioni matematiche quando il valore di un argomento è al
di fuori dell’intervallo in cui esse sono definite.
ERANGE Range error. È usato dalle funzioni matematiche quando il risultato dell’operazione
non è rappresentabile nel valore di ritorno a causa di un overflow o di un underflow.
EAGAIN Resource temporarily unavailable. La funzione è fallita ma potrebbe funzionare se la
chiamata fosse ripetuta. Questo errore accade in due tipologie di situazioni:
• Si è effettuata un’operazione che si sarebbe bloccata su un oggetto che è stato posto in
modalità non bloccante. Nei vecchi sistemi questo era un codice diverso, EWOULDBLOCK.
In genere questo ha a che fare con file o socket, per i quali si può usare la funzione
select per vedere quando l’operazione richiesta (lettura, scrittura o connessione)
diventa possibile.
• Indica la carenza di una risorsa di sistema che non è al momento disponibile (ad
esempio fork può fallire con questo errore se si è esaurito il numero di processi
contemporanei disponibili). La ripetizione della chiamata in un periodo successivo,
in cui la carenza della risorsa richiesta può essersi attenuata, può avere successo.
Questo tipo di carenza è spesso indice di qualcosa che non va nel sistema, è pertanto
opportuno segnalare esplicitamente questo tipo di errori.
EWOULDBLOCK Operation would block. Indica che l’operazione richiesta si bloccherebbe, ad esem-
pio se si apre un file in modalità non bloccante, una read restituirebbe questo errore per
indicare che non ci sono dati; in Linux è identico a EAGAIN, ma in altri sistemi può essere
specificato un valore diverso.
EINPROGRESS Operation now in progress. Operazione in corso. Un’operazione che non può essere
completata immediatamente è stata avviata su un oggetto posto in modalità non-bloccante.
Questo errore viene riportato per operazioni che si dovrebbero sempre bloccare (come per
una connect) e che pertanto non possono riportare EAGAIN, l’errore indica che l’opera-
zione è stata avviata correttamente e occorrerà del tempo perché si possa completare. La
ripetizione della chiamata darebbe luogo ad un errore EALREADY.
EALREADY Operation already in progress. L’operazione è già in corso. Si è tentata un’operazione
già in corso su un oggetto posto in modalità non-bloccante.
ENOSYS Function not implemented. Indica che la funzione non è supportata o nelle librerie del
C o nel kernel. Può dipendere sia dalla mancanza di una implementazione, che dal fatto
che non si è abilitato l’opportuno supporto nel kernel; nel caso di Linux questo può voler
dire anche che un modulo necessario non è stato caricato nel sistema.
ENOTSUP Not supported. Una funzione ritorna questo errore quando gli argomenti sono validi ma
l’operazione richiesta non è supportata. Questo significa che la funzione non implementa
quel particolare comando o opzione o che, in caso di oggetti specifici (file descriptor o altro)
non è in grado di supportare i parametri richiesti.
EILSEQ Illegal byte sequence. Nella decodifica di un carattere esteso si è avuta una sequenza
errata o incompleta o si è specificato un valore non valido.
EBADMSG Not a data message. Definito da POSIX come errore che arriva ad una funzione di
lettura che opera su uno stream. Non essendo gli stream definiti su Linux il kernel non
genera mai questo tipo di messaggio.
EMULTIHOP Multihop attempted. Definito da POSIX come errore dovuto all’accesso a file remoti
attraverso più macchine, quando ciò non è consentito. Non viene mai generato su Linux.
C.4. ERRORI GENERICI 681
EIDRM Identifier removed. Indica che l’oggetto del SysV IPC a cui si fa riferimento è stato
cancellato.
ENODATA No data available. Viene indicato da POSIX come restituito da una read eseguita su un
file descriptor in modalità non bloccante quando non ci sono dati. In realtà in questo caso
su Linux viene utilizzato EAGAIN. Lo stesso valore valore però viene usato come sinonimo
di ENOATTR.
ENOATTR No such attribute. È un codice di errore specifico di Linux utilizzato dalle funzioni
per la gestione degli attributi estesi dei file (vedi sez. 5.4.1) quando il nome dell’attributo
richiesto non viene trovato.
ENOLINK Link has been severed. È un errore il cui valore è indicato come riservato nelle Single
Unix Specification. Dovrebbe indicare l’impossibilità di accedere ad un file a causa di un
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 è
presente nessun messaggio del tipo desiderato.
ENOSR Out of streams resources. Errore relativo agli STREAMS, che indica l’assenza di risorse
sufficienti a completare l’operazione richiesta. Quella degli STREAMS 2 è interfaccia di
programmazione originaria di System V, che non è implementata da Linux, per cui questo
errore non viene utilizzato.
ENOSTR Device not a stream. Altro errore relativo agli STREAMS, anch’esso non utilizzato da
Linux.
EOVERFLOW Value too large for defined data type. Si è chiesta la lettura di un dato dal SysV IPC
con IPC_STAT ma il valore eccede la dimensione usata nel buffer di lettura.
EPROTO Protocol error. Indica che c’è stato un errore nel protocollo di rete usato dal socket;
viene usato come errore generico dall’interfaccia degli STREAMS quando non si è in grado
di specificare un altro codice di errore che esprima più accuratamente la situazione.
ETIME Timer expired. Indica che è avvenuto un timeout nell’accesso ad una risorsa (ad esempio
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.
2
che non vanno confusi con gli stream di cap. 7.
682 APPENDICE C. I CODICI DI ERRORE
Appendice D
Tratteremo in questa appendice in maniera superficiale i principali strumenti che vengono uti-
lizzati 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.
dove lo spazio all’inizio deve essere un tabulatore (metterci degli spazi è un errore comune,
fortunatamente ben segnalato dalle ultime versioni del programma), il bersaglio e le dipendenze
nomi di file e le regole comandi di shell.
Il concetto di base è che se uno dei file di dipendenza è più recente (nel senso di tempo
di ultima modifica) del file bersaglio quest’ultimo viene ricostruito di nuovo usando le regole
elencate nelle righe successive.
1
in realtà make non si applica solo ai programmi, ma in generale alla automazione di processi di costruzione,
ad esempio anche la creazione dei file di questa guida viene fatta con make.
683
684 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
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 file; in questo modo è possibile creare una catena di ricostruzioni.
In esempio comune di quello che si fa è mettere come primo bersaglio il programma principale
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 finale.
#----------------------------------------------------------------------
#
# 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
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. L’USO DI MAKE PER L’AUTOMAZIONE DELLA COMPILAZIONE 685
rm -f *.rz
rm -f output
Anzitutto i commenti, ogni linea che inizia con un # è un commento e non viene presa in
considerazione.
Con make possono essere definite delle variabili, da potersi riusare a piacimento, per leggibilità
si tende a definirle tutte maiuscole, nell’esempio ne sono definite 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 significato speciale (nel caso FC, FLAGS, CC,
CFLAGS) in quanto sono usati da make nelle cosiddette regole implicite (su cui torneremo dopo).
Nel caso specifico, vedi anche i commenti, abbiamo definito i comandi di compilazione da
usare per il C e il Fortran, e i rispettivi flag, una variabile che contiene il pathname e la lista
delle librerie del CERN e una variabile con una lista di file oggetto.
Per richiamare una variabile si usa la sintassi $(NOME), ad esempio nel makefile abbiamo
usato:
$(FC) $(FFLAGS) -o riduzione riduzione.F readfile.o $(OBJ) $(LIBS)
e questo significa che la regola verrà trattata come se avessimo scritto esplicitamente i valori
delle variabili.
Veniamo ora alla parte principale del makefile 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 makefile, che definisce il bersaglio di default, è il programma di riduzione
dei dati; esso dipende dal suo sorgente da tutti gli oggetti definiti dalla variabile OBJ, dal file di
definizioni commondef.f e dalla routine C readfile.o; si noti il .F del sorgente, che significa che
il file 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 definite
in precedenza per specificare quale compilatore e opzioni usare e specifica di nuovo gli oggetti e
le librerie.
Il secondo bersaglio definisce 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
riduzione dipende da readfile.o.
Il terzo bersaglio è apparentemente incomprensibile dato che vi compare solo il riferimento
alla variabile OBJ con una sola dipendenza e nessuna regola, essa però mostra le possibilità
(oltre che la complessità) di make connesse alla presenza di quelle regole implicite a cui avevamo
accennato.
Anzitutto una peculiarità di make è che si possono anche usare più bersagli per una stessa
regola (nell’esempio quelli contenuti nella variabile OBJ che viene espansa in una lista); in questo
686 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
facendo riferimento con la variabile automatica: $@. L’esempio usato per la nostra costruzione
però sembra non avere neanche la regola di costruzione.
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
dipenda da un sorgente, e che per ottenere l’oggetto si debba compilare quest’ultimo.
Il comando make sa tutto questo per cui quando un bersaglio è un oggetto (cioè ha un nome
tipo qualcosa.o) non è necessario specificare il sorgente, ma il programma lo va a cercare nella
directory corrente (ma è possibile pure dirgli di cercarlo altrove, il caso è trattato nel manuale).
Nel caso specifico allora si è messo come dipendenza solo il file delle definizioni che viene incluso
in ogni subroutine.
Inoltre come dicevamo in genere per costruire un oggetto si deve compilarne il sorgente; make
sa anche questo e sulla base dell’estensione del sorgente trovato (che nel caso sarà un qualcosa.f)
applica la regola implicita. In questo caso la regola è quella di chiamare il compilatore fortran
applicato al file oggetto e al relativo sorgente, questo viene fatto usando la variabile FC che è
una delle variabili standard usata dalle regole implicite (come CC nel caso di file .c); per una
maggiore flessibilità poi la regola standard usa anche la variabile FFLAGS per specificare, a scelta
dell’utente che non ha che da definirla, quali flag 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
relative compilazioni delle singole subroutine; inoltre con l’uso della variabile OBJ l’aggiunta di
una nuova eventuale routine nuova.f comporta solo l’aggiunta di nuova.o alla definizione di
OBJ.
Una delle caratteristiche che contraddistinguono Subversion dal suo predecessore CVS è
quella di essere gestibile in maniera molto flessibile l’accesso al repository, che può avvenire sia
in maniera diretta facendo riferimento alla directory in cui questo è stato installato che via rete,
tramite diversi protocolli. L’accesso più comune è fatto direttamente via HTTP, utilizzando
opportune estensioni del protocollo DAV, ma è possibile passare attraverso SSH o fornire un
servizio di rete dedicato.2
In generale è comunque necessario preoccuparsi delle modalità di accesso al codice soltanto
in fase di primo accesso al repository, che occorrerà identificare o con il pathname alla directory
dove questo si trova o con una opportuna URL (con il comune accesso via web del tutto analoga
a quella che si usa in un browser), dopo di che detto indirizzo sarà salvato nella propria copia
locale dei dati ed il riferimento diventerà implicito.
Il programma prevede infatti che in ogni directory che si è ottenuta come copia locale sia
presente una directory .svn contenente tutti i dati necessari al programma. Inoltre il programma
usa la directory .subversion nella home dell’utente per mantenere le configurazioni generali del
client e le eventuali informazioni di autenticazione.
Tutte le operazioni di lavoro sul repository vengono effettuate lato client tramite il comando
svn che vedremo in sez. D.2.1 ma la creazione e la inizializzazione dello stesso (cosı̀ come la
gestione lato server) devono essere fatte tramite il comando svnadmin eseguito sulla macchina
che lo ospita. In generale infatti il comando svn richiede che si faccia riferimento ad un repository
(al limite anche vuoto) esistente e questo deve essere opportunamente creato.
Il comando svnadmin utilizza una sintassi che richiede sempre l’ulteriore specificazione di un
sotto-comando, seguito da eventuali altri argomenti. L’inizializzazione di un repository (che sarà
creato sempre vuoto) viene eseguita con il comando:
dove /path/to/repository è la directory dove verranno creati e mantenuti tutti i file, una volta
creato il repository si potrà iniziare ad utilizzarlo ed inserirvi i contenuti con il comando svn.
Non essendo questo un testo di amministrazione di sistema non tratteremo qui i dettagli
della configurazione del server per l’accesso via rete al repository, per i quali si rimanda alla
documentazione del progetto ed alla documentazione sistemistica scritta per Truelite Srl.3
tags contiene le diverse versioni fotografate ad un certo istante del processo di svi-
luppo, ad esempio in occasione del rilascio di una versione stabile, cosı̀ che sia
possibile identificarle facilmente;
branches contiene rami alternativi di sviluppo, ad esempio quello delle correzioni eseguite
ad una versione stabile, che vengono portati avanti in maniera indipendente
dalla versione principale.
2
esiste all’uopo il programma svnserve, ma il suo uso è sconsigliato per le scarse prestazioni e le difficoltà
riscontrate a gestire accessi di utenti diversi; la modalità di accesso preferita resta quella tramite le estensioni al
protocollo DAV.
3
rispettivamente disponibili su svn.tigris.org e labs.truelite.it/truedoc.
688 APPENDICE D. GLI STRUMENTI DI AUSILIO PER LA PROGRAMMAZIONE
In genere però è piuttosto raro iniziare un progetto totalmente da zero, è molto più comune
avere una qualche versione iniziale dei propri file all’interno di una cartella. In questo caso il
primo passo è quello di eseguire una inizializzazione del repository importando al suo interno
quanto già esistente. Per far questo occorre eseguire il comando:
svn import [/pathname] URL
questo può essere eseguito direttamente nella directory contenente la versione iniziale dei propri
sorgenti nel qual caso il comando richiede come ulteriore argomento la directory o la URL con
la quale indicare il repository da usare. Alternativamente si può passare come primo argomento
il pathname della directory da importare, seguito dall’indicazione della URL del repository.
Si tenga presente che l’operazione di importazione inserisce sul repository il contenuto com-
pleto della directory indicata, compresi eventuali file nascosti e sotto-directory. È anche possibile
eseguire l’importazione di più directory da inserire in diverse sezioni del repository, ma un tal
caso ciascuna importazione sarà vista con una diversa release. Ad ogni operazione di modifica del
repository viene infatti assegnato un numero progressivo che consente di identificarne la storia
delle modifiche e riportarsi ad un dato punto della stessa in ogni momento successivo.4
Una volta eseguita l’importazione di una versione iniziale è d’uopo cancellare la directory
originale e ripartire dal progetto appena creato. L’operazione di recuperare ex-novo di tutti i file
che fanno parte di un progetto, chiamata usualmente checkout, viene eseguita con il comando:5
svn checkout URL [/pathname]
che creerà nella directory corrente una directory corrispondente al nome specificato in coda alla
URL passata come argomento, scaricando l’ultima versione dei file archiviati sul repository;
alternativamente si può specificare come ulteriore argomento la directory su cui scaricare i file.
4
a differenza di CVS Subversion non assegna un numero di versione progressivo distinto ad ogni file, ma un
numero di release progressivo ad ogni cambiamento globale del repository, pertanto non esiste il concetto di
versione di un singolo file, quanto di stato di tutto il repository ad un dato momento, è comunque possibile
richiedere in maniera indipendente la versione di ogni singolo file a qualunque release si desideri.
5
alternativamente si può usare l’abbreviazione svn co.
D.2. SOURCE CONTROL MANAGEMENT 689
Sia in caso di import che di checkout è sempre possibile operare su una qualunque sotto
cartella contenuta all’interno di un repository, ignorando totalmente quello che sta al di sopra,
basterà indicare in sede di importazione o di estrazione iniziale un pathname o una URL che
identifichi quella parte del progetto.
Se quando si effettua lo scaricamento non si vuole usare la versione più aggiornata, ma una
versione precedente si può usare l’opzione -r seguita da un numero che scaricherà esattamente
quella release, alternativamente al posto del numero si può indicare una data, e verrà presa la
release più prossima a quella data.
A differenza di CVS Subversion non supporta l’uso di etichette associate ad una certa versione
del proprio progetto, per questo è invalso l’uso di strutturare il repository secondo lo schema
illustrato inizialmente; è infatti molto semplice (e non comporta nessun tipo di aggravio) creare
delle copie complete di una qualunque parte del repository su un’altra parte dello stesso, per cui
se si è eseguito lo sviluppo sulla cartella trunk sarà possibile creare banalmente una versione
con etichetta label (o quel che si preferisce) semplicemente con una copia eseguita con:
Il risultato di questo comando è la creazione della nuova cartella label sotto tags, che sarà
assolutamente identica, nel contenuto (e nella sua storia) a quanto presente in trunk al momento
dell’esecuzione del comando. In questo modo, una volta salvate le modifiche,6 si potrà ottenere
la versione label del proprio progetto semplicemente eseguendo un checkout di tags/label in
un’altra directory.7
Una volta creata la propria copia locale dei programmi, è possibile lavorare su di essi po-
nendosi nella relativa directory, e apportare tutte le modifiche che si vogliono ai file ivi presenti;
due comandi permettono inoltre di schedulare la rimozione o l’aggiunta di file al repository:8
ma niente viene modificato sul repository fintanto che non viene eseguito il cosiddetto commit
delle modifiche, vale a dire fintanto che non viene dato il comando:9
ed è possibile eseguire il commit delle modifiche per un singolo file, indicandolo come ulteriore
argomento, mentre se non si indica nulla verranno inviate tutte le modifiche presenti.
Si tenga presente però che il commit non verrà eseguito se nel frattempo i file del repository
sono stati modificati; in questo caso svn rileverà la presenza di differenze fra la propria release
e quella del repository e chiederà che si effettui preventivamente un aggiornamento. Questa è
una delle operazioni di base di Subversion, che in genere si compie tutte le volte che si inizia a
lavorare, il comando che la esegue è:
svn update
Questo comando opera a partire dalla directory in cui viene eseguito e ne aggiorna il contenuto
(compreso quello di eventuali sotto-directory) alla versione presente, scaricando le ultime versioni
dei file esistenti o nuovi file o directory aggiunte, cancellando eventuali file e directory rimossi
dal repository. Esso inoltre esso cerca, in caso di presenza di modifiche eseguite in maniera
6
la copia viene eseguita localmente verrà creata anche sul repository solo dopo un commit.
7
ovviamente una volta presa la suddetta versione si deve aver cura di non eseguire nessuna modifica a partire
dalla stessa, per questo se si deve modificare una versione etichettata si usa branches.
8
a differenza di CVS si possono aggiungere e rimuovere, ed anche spostare con svn mv, sia file che directory.
9
in genere anche questo viene abbreviato, con svn ci.
690 APPENDICE D. GLI STRUMENTI DI AUSILIO PER LA PROGRAMMAZIONE
indipendente sulla propria copia locale, di eseguire un raccordo (il cosiddetto merging) delle
stesse con quelle presenti sulla versione del repository.
Fintanto che sono state modificate parti indipendenti di un file di testo in genere il processo
di merging ha successo e le modifiche vengono incorporate automaticamente in conseguenza del-
l’aggiornamento, ma quando le modifiche attengono alla stessa parte di un file nel ci si troverà di
fronte ad un conflitto ed a quel punto sarà richiesto al “committente” di intervenire manualmente
sui file per i quali sono stati rilevati i conflitti per risolverli.
Per aiutare il committente nel suo compito quando l’operazione di aggiornamento fallisce nel
raccordo delle modifiche lascia sezioni di codice in conflitto opportunamente marcate e separate
fra loro come nell’esempio seguente:
<<<<<<< .mine
$(CC) $(CFLAGS) -o pamacq pamacq.c -lm
=======
$(CC) $(CFLAGS) -o pamacq pamacq.c
>>>>>>> r.122
In questo caso si c’è stata una modifica sul file (mostrata nella parte superiore) incompatibile
con quella fatta nel repository (mostrata nella parte inferiore). Prima di eseguire un commit
occorrerà pertanto integrare le modifiche e salvare nuovamente il file rimuovendo i marcatori,
inoltre prima che il commit ritorni possibile si dovrà esplicitare la risoluzione del conflitto con il
comando:
Flag Significato
? File sconosciuto.
M File modificato localmente.
A File aggiunto.
C File con conflitto.
Infine per capire la situazione della propria copia locale si può utilizzare il comando svn
status che confronta i file presenti nella directory locale rispetto alla ultima versione scaricata
dal repository e per tutti quelli che non corrispondono stampa a schermo delle informazioni di
stato nella forma di un carattere seguito dal nome del file, secondo quanto illustrato in tab. D.2.
Appendice E
Ringraziamenti
Desidero ringraziare tutti coloro che a vario titolo e a più riprese mi hanno aiutato ed han con-
tribuito a migliorare in molteplici aspetti la qualità di GaPiL. In ordine rigorosamente alfabetico
desidero citare:
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 suggeri-
menti per il miglioramento della comprensione di vari passaggi.
Infine, 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,
ed il relativo spazio web, e Truelite Srl, l’azienda che ho fondato e di cui sono responsabile
tecnico, che fornisce il nuovo repository SVN, tutto quanto è necessario alla pubblicazione della
guida ed il sistema di tracciamento dei sorgenti su http://gapil.truelite.it/sources.
691
692 APPENDICE E. RINGRAZIAMENTI
Appendice F
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 effective 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 modifications 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.
693
694 APPENDICE F. GNU FREE DOCUMENTATION LICENSE
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
Back-Cover Texts, in the notice that says that the Document is released under this License.
A “Transparent” copy of the Document means a machine-readable copy, represented in a
format whose specification 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 file format whose
markup has been designed to thwart or discourage subsequent modification 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 input format, LATEX input format, SGML or XML using a publicly available DTD, and
standard-conforming simple HTML designed for human modification. 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 work’s title, preceding the beginning of the body of the text.
If the required texts for either cover are too voluminous to fit legibly, you should put the first
ones listed (as many as fit 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
You may copy and distribute a Modified Version of the Document under the conditions of
sections 2 and 3 above, provided that you release the Modified Version under precisely this
License, with the Modified Version filling the role of the Document, thus licensing distribution
and modification of the Modified Version to whoever possesses a copy of it. In addition, you
must do these things in the Modified 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 modifications in the Modified Version, together with at least five of the principal
authors of the Document (all of its principal authors, if it has less than five).
• State on the Title page the name of the publisher of the Modified Version, as the publisher.
• Preserve all the copyright notices of the Document.
• Add an appropriate copyright notice for your modifications adjacent to the other copyright
notices.
• Include, immediately after the copyright notices, a license notice giving the public permis-
sion to use the Modified 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 Document’s 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 Modified 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 Modified Version as stated in the previous sentence.
696 APPENDICE F. GNU FREE DOCUMENTATION LICENSE
• Preserve the network location, if any, given in the Document for public access to a Trans-
parent 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 section’s ti-
tle, 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
Modified Version.
• Do not retitle any existing section as “Endorsements” or to conflict in title with any
Invariant Section.
If the Modified 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 Modified Version’s license notice. These titles must be distinct from
any other section titles.
You may add a section entitled “Endorsements”, provided it contains nothing but endorse-
ments of your Modified Version by various parties – for example, statements of peer review or
that the text has been approved by an organization as the authoritative definition of a standard.
You may add a passage of up to five 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 Modified Version.
Only one passage of 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 Modified Version.
F.8 Translation
Translation is considered a kind of modification, so you may distribute translations of the Do-
cument 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
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.
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.
Indice analitico
Access Control List, 147, 148, 151–160 AF_INET6, 503, 508, 577, 578, 581, 582
advertised window , 493, 511, 547, 612, 615, 628 AF_INET, 503, 508, 528, 577, 578, 581, 599,
algoritmo di Nagle, 612, 613 622, 639
append mode, 171, 184–186, 190, 192, 206, 234, AF_PACKET, 505
246, 407, 452 AF_UNIX, 348, 504, 519
AIO_ALLDONE, 454
broadcast, 171, 490, 498, 506, 519, 520, 597, AIO_CANCELED, 454
600, 605, 620, 621, 654, 659, 661, 664 AIO_LISTIO_MAX, 455
bucket filter , 626 AIO_NOTCANCELED, 454
buffer overflow , 212, 215, 217 AI_ADDRCONFIG, 582, 587
buffer overrun, 33 AI_ALL, 582, 587
AI_CANONNAME, 587
calendar time, 9, 132, 247–254
AI_DEFAULT, 582
capabilities, 148, 152
AI_NUMERICHOST, 587
capabilities, 31, 67, 68, 77, 79, 81, 117, 143, 145,
AI_PASSIVE, 587, 593
147, 165–179, 244, 246, 443, 500, 503,
AI_V4MAPPED, 582, 587
505, 599, 601, 609, 620
ARG_MAX, 227, 228
capabilities bounding set, 166–168
ATADDR_BCAST, 505
clock tick , 49, 228, 247, 248, 251
close-on-exec, 56, 67, 119, 120, 184, 194, 199, ATPROTO_DDP, 504
202, 427 AT_ANYNET, 505
cooperative multitasking, 75 AT_ANYNODE, 505
copy-on-write, 89 AT_EACCESS, 198
copy on write, 30, 32, 51, 54, 57, 458, 464 AT_FDCWD, 196
core dump, 62, 91, 243, 244, 262–265 AT_REMOVEDIR, 198
costante AT_SYMLINK_FOLLOW, 197
ACL_GROUP_OBJ, 152–156 AT_SYMLINK_NOFOLLOW, 197, 198
ACL_GROUP, 152, 153, 156 BOOT_TIME, 240, 241
ACL_MASK, 152, 153, 157 BRKINT, 324
ACL_OTHER, 152, 153, 155 BSDLY, 325
ACL_TYPE_ACCESS, 156 BUFSIZ, 220, 221
ACL_TYPE_DEFAULT, 156 BUS_ADRALN, 286
ACL_USER_OBJ, 152–156 BUS_ADRERR, 286
ACL_USER, 152–154, 156 BUS_MCEERR_AO, 93
ADJ_ESTERROR, 251 BUS_OBJERR, 286
ADJ_FREQUENCY, 251 CAP_AUDIT_CONTROL, 171
ADJ_MAXERROR, 251 CAP_AUDIT_WRITE, 171
ADJ_OFFSET_SINGLESHOT, 251 CAP_CHOWN, 145, 169, 171
ADJ_OFFSET, 251 CAP_CLEAR, 176
ADJ_STATUS, 251 CAP_DAC_OVERRIDE, 169, 171
ADJ_TICK, 251 CAP_DAC_READ_SEARCH, 169, 171
ADJ_TIMECONST, 251 CAP_EFFECTIVE, 175
AF_APPLETALK, 505 CAP_FOWNER, 148, 152, 169–171
699
700 INDICE ANALITICO
_POSIX_PATH_MAX, 229 EAGAIN, 30, 51, 89, 188, 189, 199, 243, 292,
_POSIX_PIPE_BUF, 229 293, 297, 356, 357, 367, 368, 389, 391–
_POSIX_SIGQUEUE_MAX, 292 393, 399, 412, 418, 419, 430, 437, 452,
_POSIX_SSIZE_MAX, 227 454, 455, 457, 461, 465, 467, 473, 520,
_POSIX_STREAM_MAX, 227 523, 524, 598, 637, 638, 680, 681
_POSIX_TZNAME_MAX, 227 EALREADY, 520, 680
_POSIX_VERSION, 227, 228 EBADF, 132, 136, 186, 194–198, 322, 393,
_SC_AVPHYS_PAGES, 245 420, 422, 423, 425, 427, 430, 432, 439,
_SC_IOV_MAX, 465 440, 447, 452, 453, 457, 463, 467, 468,
_SC_NPROCESSORS_CONF, 245 472, 475–478, 519, 521, 523, 525, 595,
_SC_NPROCESSORS_ONLN, 245 596, 675
_SC_PAGESIZE, 245 EBADMSG, 680
_SC_PHYS_PAGES, 245 EBUSY, 112, 116, 161, 233–235, 393, 676
_SYS_NMLN, 231 ECANCELED, 454
_UTSNAME_DOMAIN_LENGTH, 230 ECHILD, 60, 62, 677
_UTSNAME_LENGTH, 230 ECONNABORTED, 678
__WALL, 60 ECONNREFUSED, 520–522, 638, 679
__WCLONE, 60 ECONNRESET, 546, 552, 553, 557, 602, 637,
__WNOTHREAD, 60 678
signalfd_siginfo, 437 EDEADLK, 411, 413, 679
CPU affinity, 83–85 EDESTADDRREQ, 637, 679
EDOM, 680
deadlock , 95–96, 280, 288, 342, 411–413, 419, EDQUOT, 677
679 EEXIST, 110, 113, 116, 117, 128, 129, 150,
deep copy, 580, 590 183, 184, 353, 354, 389, 397, 427, 676
Denial of Service (DoS), 184, 185, 458, 564, EFAULT, 30, 73, 74, 85, 161, 282, 287, 288,
626, 629 295, 296, 299, 300, 302, 374, 430, 439,
direttiva 440, 460, 461, 467, 525, 595, 596, 679
const, 39 EFBIG, 189, 243, 477, 478, 676
extern, 520 EFTYPE, 677
inline, 287 EHOSTDOWN, 546, 679
register, 41, 44 EHOSTUNREACH, 546, 550, 551, 556, 679
union, 365, 503 EIDRM, 353, 356–358, 365, 367, 368, 374,
volatile, 45, 95, 255 681
Discrectionary Access Control (DAC), 170 EILSEQ, 680
dnotify, 443–444 EINPROGRESS, 453, 520, 680
EINTR, 59, 60, 62, 128, 186, 188, 189, 199,
effetto ping-pong, 83, 84 270, 277, 293, 328, 357, 358, 367, 368,
endianess, 45–46, 506, 519 398, 399, 411, 412, 420–423, 425, 430,
epoll , 426–431, 434–436, 441 454, 455, 465, 523, 527, 541, 542, 544,
errore 563, 679
E2BIG, 64, 358, 367, 677 EINVAL, 30–33, 60, 62, 64, 73, 74, 78, 80–
EACCESS, 246, 397, 400, 445, 461 82, 84, 86, 89, 112–114, 117, 126, 128,
EACCES, 64, 65, 78, 116, 126, 132, 134, 135, 129, 132, 141, 154–159, 161, 173, 186,
141, 159, 161, 199, 233, 308, 353, 354, 189, 193, 197–199, 201, 231, 233, 244,
356, 358, 365, 367, 374, 375, 389, 411, 256, 272, 277, 282, 287, 288, 292, 293,
412, 457, 498, 519, 520, 675 295–297, 299–302, 308, 318, 322, 340,
EADDRINUSE, 519, 599, 603, 678 356, 358, 363, 365, 372, 374, 375, 389–
EADDRNOTAVAIL, 519, 678 392, 397–401, 420, 422, 423, 425, 427,
EAFNOSUPPORT, 348, 508, 520, 678 430, 432, 438–440, 445, 447, 452, 453,
712 INDICE ANALITICO
455, 457, 460–463, 465, 467, 468, 472, ENOTSUP, 149–151, 155, 159, 680
473, 475–478, 498, 503, 519, 595, 679 ENOTTY, 201, 202, 309, 310, 322, 676
EIO, 161, 310, 326, 463, 467, 675 ENXIO, 183, 233, 342, 675
EISCONN, 637, 638, 679 EOPNOTSUPP, 348, 465, 478, 521, 523, 546,
EISDIR, 111, 112, 183, 676 637, 678
ELIBBAD, 64 EOVERFLOW, 186, 374, 466, 681
ELOOP, 115, 183, 676 EPERM, 31, 64, 70, 74, 77, 78, 80, 82, 84, 86,
EMFILE, 161, 194, 195, 199, 233, 243, 427, 89, 110, 111, 113, 116, 117, 134, 135,
445, 498, 676 142, 143, 145, 149, 150, 161, 172, 180,
EMLINK, 110, 116, 677 231, 233, 234, 244, 246, 249–251, 272,
EMSGSIZE, 391, 392, 637, 678 292, 295, 296, 302, 307–309, 318, 356,
EMULTIHOP, 680 365, 374, 427, 457, 520, 523, 675
ENAMETOOLONG, 397, 400, 676 EPFNOSUPPORT, 678
ENETDOWN, 546, 678 EPIPE, 189, 268, 337, 637, 677
ENETRESET, 678 EPROCLIM, 677
ENETUNREACH, 520, 521, 546, 551, 678 EPROTONOSUPPORT, 348, 498, 678
ENFILE, 427, 445, 457, 498, 676 EPROTOTYPE, 678
ENOAFSUPPORT, 508 EPROTO, 546, 681
ENOATTR, 149–151, 681 ERANGE, 126, 149, 150, 158, 237, 256, 322,
ENOBUFS, 498, 523, 525, 637, 679 365, 367, 368, 580, 680
ENOCHLD, 61 EREMOTE, 677
ENODATA, 681 ERESTARTSYS, 318
EROFS, 111, 113, 141, 142, 676
ENODEV, 161, 183, 233, 432, 438, 457, 477,
ESHUTDOWN, 679
478, 676
ESOCKTNOSUPPORT, 678
ENOENT, 64, 113, 126, 297, 353, 354, 389,
ESPIPE, 186, 187, 340, 466, 468, 476, 477,
397, 400, 427, 675
676
ENOEXEC, 64, 675
ESRCH, 78, 80, 82, 84–86, 161, 172, 272, 292,
ENOLCK, 411, 417, 677
296, 297, 307, 308, 677
ENOLINK, 681
ESTALE, 677
ENOMEM, 30–33, 51, 89, 154–158, 231, 243,
ETIMEDOUT, 391–393, 399, 520, 551, 552,
297, 302, 363, 367, 372, 427, 432, 438,
556, 602, 679
445, 457, 461, 463, 467, 468, 472, 473,
ETIME, 681
523, 679
ETOOMANYREFS, 679
ENOMSG, 358, 681
ETXTBSY, 64, 132, 183, 457, 458, 676
ENONET, 546
EUSERS, 677
ENOPKG, 161
EUSER, 246
ENOPROTOOPT, 546, 595, 596, 598, 678
EWOULDBLOCK, 189, 408, 409, 417, 443, 523,
ENOSPC, 116, 159, 353, 363, 372, 427, 445, 598, 680
477, 478, 508, 676 EXDEV, 110, 112, 676
ENOSR, 681 Explicit Congestion Notification, 629
ENOSTR, 681 Extended Attributes, 146–151
ENOSYS, 82, 136, 235, 246, 296, 309, 318,
401, 452, 454, 455, 464, 478, 680 file
ENOTBLK, 161, 233, 676 hole, 132, 187, 477
ENOTCONN, 525, 558, 637, 638, 679 descriptor, 100, 181–183
ENOTDIR, 112, 113, 126, 183, 196–198, 231, di lock, 184, 192, 263, 383–385
676 di configurazione
ENOTEMPTY, 112, 113, 116, 677 /etc/fstab, 235, 389, 394
ENOTSOCK, 519, 521, 523, 525, 558, 595, 596, /etc/group, 7, 74, 236, 238
678 /etc/gshadow, 236
INDICE ANALITICO 713
/proc/sys/net/ipv4/tcp_frto, 629 file descriptor set, 420–421, 425, 426, 553, 556,
/proc/sys/net/ipv4/tcp_keepalive_intvl, 559, 563, 564, 567, 650
629 file table, 55, 181, 182, 184, 186, 190, 191, 409,
/proc/sys/net/ipv4/tcp_keepalive_probes, 410, 413, 499
629 funzione
/proc/sys/net/ipv4/tcp_keepalive_time, ClientEcho, 549, 555, 556, 559, 560, 643,
630 644, 648
/proc/sys/net/ipv4/tcp_low_latency, ComputeValues, 381
630 CreateMutex, 385
/proc/sys/net/ipv4/tcp_max_orphans, CreateShm, 396, 403
630 DirScan, 123, 381
/proc/sys/net/ipv4/tcp_max_syn_backlog, FindMutex, 385
523, 630 FindShm, 397, 404
/proc/sys/net/ipv4/tcp_max_tw_buckets, FullRead, 527
630 FullWrite, 527, 528, 534, 536, 538, 539,
/proc/sys/net/ipv4/tcp_mem, 630, 633 564
/proc/sys/net/ipv4/tcp_orphan_retries, LockFile, 384
631 LockMutex, 385, 387
/proc/sys/net/ipv4/tcp_reordering, 631 MutexCreate, 370
/proc/sys/net/ipv4/tcp_retrans_collapse,MutexFind, 370, 382
631 MutexLock, 370–372, 380–382
/proc/sys/net/ipv4/tcp_retries1, 631 MutexRead, 370, 372
MutexRemove, 371, 381
/proc/sys/net/ipv4/tcp_retries2, 551,
MutexUnlock, 370, 372, 381, 382
631
PrintErr, 539
/proc/sys/net/ipv4/tcp_rfc1337, 631
ReadMutex, 387
/proc/sys/net/ipv4/tcp_rmem, 600, 628,
RemoveMutex, 385
631–633
RemoveShm, 397
/proc/sys/net/ipv4/tcp_sack, 632
SIGKILL, 92
/proc/sys/net/ipv4/tcp_stdurg, 632
SYS_klog, 318
/proc/sys/net/ipv4/tcp_syn_retries,
SetTermAttr, 329, 330
521, 632
ShmCreate, 378, 380
/proc/sys/net/ipv4/tcp_synack_retries, ShmFind, 378, 382
632
ShmRemove, 378, 381
/proc/sys/net/ipv4/tcp_syncookies, 523, SignalRestart, 542
632 Signal, 285, 286, 403, 542, 544
/proc/sys/net/ipv4/tcp_timestamps, 632 UnSetTermAttr, 329, 330
/proc/sys/net/ipv4/tcp_tw_recycle, 633 UnlockFile, 384
/proc/sys/net/ipv4/tcp_tw_reuse, 633 UnlockMutex, 385, 387
/proc/sys/net/ipv4/tcp_window_scaling, WriteMess, 338
632, 633 _Exit, 290
/proc/sys/net/ipv4/tcp_wmem, 600, 633 __fbufsize
/proc/sys/vm/bdflush, 193 definizione di, 221
/proc/sys/vm/memory_failure_early_kill, __flbf
94 definizione di, 221
/proc/timer_list, 295 __freadable
lease, 171, 200, 442–443 definizione di, 220
locking, 100, 186, 192, 198, 200, 243, 372, __freading
385, 386, 407–419 definizione di, 220
stream, 100, 203 __fsetlocking
INDICE ANALITICO 715
bind, 290, 503, 505, 506, 509, 518, 519, 521, definizione di, 330
525, 530, 536, 538, 539, 587, 593, 599, cfsetspeed
603, 605, 628, 635, 636, 642, 678 definizione di, 331
definizione di, 518 chdir, 114, 124, 127, 290, 312, 313, 380
brk, 243, 375 definizione di, 126
definizione di, 28 chmod, 114, 133, 134, 142, 143, 197, 290,
calloc, 26, 33 312, 444
definizione di, 26 definizione di, 142
cap_clear_flag chown, 114, 134, 145, 197, 290, 312, 444
definizione di, 175 definizione di, 145
cap_clear, 175 chroot, 98, 171, 179, 180
definizione di, 174 definizione di, 180
cap_compare, 175 clearenv, 38, 39
definizione di, 175 definizione di, 39
cap_dup clearerr_unlocked, 208
definizione di, 174 clearerr
cap_free, 176–179 definizione di, 208
definizione di, 174 clock_getcpuclockid, 295, 297
cap_from_name, 177 definizione di, 296
cap_from_text clock_getres, 296
definizione di, 177 definizione di, 296
cap_get_flag, 176 clock_gettime, 290
definizione di, 175 definizione di, 295
cap_get_proc, 179 clock_nanosleep, 277
definizione di, 178 clock_settime, 295
cap_init, 174 definizione di, 295
definizione di, 174 clock
cap_set_flag, 176 definizione di, 248
definizione di, 175 clone, 51, 57, 60, 88, 89, 172
cap_set_proc definizione di, 89
definizione di, 178 closedir, 120
cap_to_name, 177 definizione di, 122
cap_to_text, 174, 179 closelog
definizione di, 176 definizione di, 317
capgetp close, 183, 186, 207, 290, 390, 395, 433,
definizione di, 178 511–513, 524, 526, 532, 557, 558, 600,
capget, 172, 173 606, 607, 639
definizione di, 172 definizione di, 186
capset, 168, 172 connect, 290, 509, 510, 519–521, 523, 525,
definizione di, 172 528, 539, 587, 592, 593, 598, 600, 610,
cfgetispeed, 290 614, 636, 637, 643, 646, 680
definizione di, 331 definizione di, 520
cfgetospeed, 290 creat, 114, 134, 153, 192, 290, 418, 444
definizione di, 331 definizione di, 185
cfmakeraw ctermid
definizione di, 332 definizione di, 322
cfree, 26 ctime, 253, 528, 534
cfsetispeed, 290 definizione di, 252
definizione di, 330 daemon, 345, 347, 359, 380, 382, 532, 538
cfsetospeed, 290 definizione di, 314
INDICE ANALITICO 717
lseek, 15, 132, 183, 186, 187, 189, 190, 192, definizione di, 31
194, 218, 290, 340, 411, 451, 466 mmap, 171, 243, 395, 397, 401, 419, 457–459,
definizione di, 186 461–463, 467, 469
lsetxattr definizione di, 456
definizione di, 149 mount, 234, 418
lstat, 114, 121, 130, 197, 290 definizione di, 233
definizione di, 130 mprobe, 34
lutimes, 136, 196, 197 definizione di, 34
definizione di, 135 mprotect, 462
madvise, 464, 476 definizione di, 461
definizione di, 463 mq_close
main, 19–22, 34, 35, 57, 62, 65 definizione di, 390
malloc, 26–28, 32–34, 43, 89, 123, 206, 214, mq_getaddr, 392
220 mq_getattr, 391
definizione di, 26 definizione di, 391
mcheck, 34 mq_notify, 389, 393, 394
definizione di, 33 definizione di, 393
memalign, 32, 33 mq_open, 389–391
definizione di, 32 definizione di, 389
memcpy, 16 mq_receive, 389, 393
memmove, 16 definizione di, 392
mempcpy, 16 mq_send, 389, 392
memset, 16, 378, 381, 397, 639 definizione di, 391
mincore, 30 mq_setattr, 391
definizione di, 30 definizione di, 391
mkdirat, 195, 197, 290 mq_timedreceive
mkdir, 114, 117, 129, 134, 153, 195, 197, definizione di, 392
290, 444 mq_timedsend
definizione di, 116 definizione di, 391
mkdtemp mq_unlink, 391
definizione di, 129 definizione di, 390
mkfifoat, 197, 290 mremap, 243
mkfifo, 114, 117, 134, 153, 197, 290, 342, definizione di, 461
345 msgctl
definizione di, 118 definizione di, 356
mknodat, 197, 290 msgget, 355, 359, 361, 363, 372
mknod, 114, 117, 118, 153, 171, 197, 290, definizione di, 353
342, 444 msgrcv, 359, 361
definizione di, 117 definizione di, 358
mkostemp msgsnd, 357
definizione di, 129 definizione di, 356
mkstemp, 128, 129 msync, 458, 460
definizione di, 128 definizione di, 460
mktemp, 129 munlockall, 31
definizione di, 128 definizione di, 31
mktime, 252, 253 munlock, 31
definizione di, 252 definizione di, 31
mlockall, 31, 32, 171 munmap, 395, 458
definizione di, 31 definizione di, 460
mlock, 31, 171 nanosleep, 277
722 INDICE ANALITICO
_POSIX_MONOTONIC_CLOCK, 295 out-of-band , 266, 285, 421, 424, 429, 500, 554,
_POSIX_PRIORITIZED_IO, 452 565, 597, 598, 618, 624, 650
_POSIX_PRIORITY_SCHEDULING, 82, 452
_POSIX_SAVED_IDS, 69, 227, 228 page fault, 23, 24, 32, 79, 242, 263, 463
_POSIX_SOURCE, 13–15, 17, 74, 120, 172 page table, 23, 457, 462, 463
_POSIX_THREAD_CPUTIME, 295 paginazione, 23, 29–31, 245, 455, 458, 475
_POSIX_THREAD_SAFE_FUNCTIONS, 222 pathname, 51, 65, 97–98, 102, 110, 112, 115,
_POSIX_TIMERS, 295 116, 125–127, 131, 132, 138, 183, 229,
_REENTRANT, 16, 96 230, 232, 233, 323, 346, 350, 388, 504,
_SVID_SOURCE, 14, 15, 17, 28, 30, 119, 120, 675, 676
122, 135, 221 assoluto, 98, 180, 388
_THREAD_SAFE, 16, 96 relativo, 98, 125, 180
_USE_BSD, 64 polling, 359, 384, 385, 419, 442
_XOPEN_SOURCE_EXTENDED, 14, 15, 307 POSIX IPC names, 388
_XOPEN_SOURCE, 14, 15, 28, 119, 120, 122, preemptive multitasking, 3, 75
271, 307, 399, 422, 424, 476, 477 prefaulting, 458, 463
__STRICT_ANSI__, 13 process group, 56, 60, 66, 78, 86, 199, 272, 273,
__va_copy, 42 306–309, 311, 312, 618, 619, 677
in6addr_any, 520 process group leader , 307, 308, 313
in6addr_loopback, 520 process group orphaned , 311
major, 118 process table, 48, 181, 261
process time, 228, 247–249
makedev, 118
minor, 118
race condition, 54, 88, 89, 95–96, 127–129, 184,
va_arg, 41, 42
185, 187, 192, 195, 196, 260, 262, 279–
va_copy, 42 281, 288, 308, 337, 384, 394, 403, 405,
va_end, 41, 42 407, 422, 423, 431, 466
va_list, 41, 42, 316 read-ahead , 464, 475
va_start, 41, 42 resolver , 569–578, 588
major number , 118, 233 Round Trip Time, 493, 559, 618
Mandatory Access Control (MAC), 68
mandatory locking, 140, 145, 146, 234 salto non-locale, 43–45, 303
masquerading, 627 scheduler , 3, 11, 48, 49, 54, 75–85, 246, 261,
Maximum Segment Size, 494, 510, 511, 513, 262, 277, 369
612, 613, 629 securebits, 92, 93, 169–170
Maximum Transfer Unit, 494–495, 607, 609– secure computing mode, 92
610, 613, 621, 627, 659 segmento
memoria virtuale, 4, 22–24, 29–32, 242, 244, dati, 24, 25, 28, 29, 32, 51, 64, 243
374, 455, 456, 458, 460, 462, 463, 475 testo, 24, 32, 51, 64, 140, 243
memory leak , 27, 28, 33, 214, 217 segment violation, 24, 28, 265, 376, 457, 679
memory locking, 30–32, 171, 374 self-pipe trick , 423
memory mapping, 387, 455–464 SELinux, 68, 147, 148, 166
minor number , 118, 350 sezione critica, 32, 95, 288, 362
modo promiscuo, 506, 620 sgid bit, 64, 67, 69, 70, 127, 131, 137, 140, 143,
multicast, 170, 171, 490–492, 503, 506, 605, 607, 145, 171, 234, 418
610–611, 620–622, 654, 657, 659–661, signal driven I/O, 440–441, 445
663–664, 670 signal mask , 287–288
signal set, 281–282, 420
Name Service Switch, 7, 236, 238, 570–571, 582– socket
584 definizione, 497–508
netfilter , 626, 628, 633 locali, 647
INDICE ANALITICO 729
key_t, 9, 349
loff_t, 9
mcheck_status, 34
mode_t, 9, 142
mqd_t, 389
nlink_t, 9
off_t, 9, 122, 186, 218, 219
pid_t, 9, 50, 78, 86, 307, 618, 619
ptrdiff_t, 9, 216
rlim_t, 9
sa_family_t, 502
sig_atomic_t, 95, 287
sighandler_t, 270, 271
sigjmp_buf, 303
sigset_t, 9, 281, 282
sigval_t, 292
size_t, 9, 216, 465, 586, 637
socklen_t, 502, 586
ssize_t, 9, 216, 227, 228
tcflag_t, 323
time_t, 9, 247, 249, 252, 253
timer_t, 298
uid_t, 9, 78, 86
uint16_t, 502
uint32_t, 502
uint8_t, 502
uintmax_t, 216
elementare, 8
opaco, 42, 44, 119, 204
primitivo, 9
[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.
[4] M. Gorman, Understanding the Linux Virtual Memory Manager. Prentice Hall PTR., 2004.
[8] Aleph1, “Smashing the stack for fun and profit,” Phrack, 1996.
[10] C. Donnelly and R. M. Stallman, Bison, the YACC-compatible parser generator. Free
Software Foundation, 2002.
[14] W. R. Stevens, UNIX Network Programming, volume 2. Prentice Hall PTR, 1998.
[15] W. R. Stevens, TCP/IP Illustrated, Volume 1, the protocols. Addison Wesley, 1994.
733