(
...una premessa: questo post è un remake di un mio vecchio post (anzi, di due). L'ho riadattato e "modernizzato" per pubblicarlo su quell'altro bel blog collettivo dove scrivo. Visto che le modifiche sono molte e (forse) interessanti, lo ripubblico anche qui. Questo potrebbe ripetersi in futuro...)
Mascetti: Tarapìa tapiòco! Prematurata la supercazzola, o scherziamo?
Vigile: Prego?
Mascetti: No, mi permetta. No, io... scusi, noi siamo in quattro.
Come se fosse antani anche per lei soltanto in due, oppure in quattro
anche scribàcchi confaldina? Come antifurto, per esempio.
Vigile: Ma che antifurto, mi faccia il piacere! Questi signori qui stavano sonando loro. 'Un s'intrometta!
Mascetti: No, aspetti, mi porga l'indice; ecco lo alzi così...
guardi, guardi, guardi. Lo vede il dito? Lo vede che stuzzica? Che
prematura anche? Ma allora io le potrei dire, anche con il rispetto per
l'autorità, che anche soltanto le due cose come vicesindaco, capisce?
Vigile: Vicesindaco? Basta 'osì, mi seguano al commissariato, prego!
Perozzi: No, no, no, attenzione! Noo! Pàstene soppaltate secondo
l'articolo 12, abbia pazienza, sennò posterdati, per due, anche un
pochino antani in prefettura...
Mascetti: ...senza contare che la supercazzola prematurata ha perso i contatti col tarapìa tapiòco.
A volte penso che per spiegare come funziona e come usare
errno sarebbe sufficiente usare una
Supercazzola come quella dello specialista
Mascetti nel capolavoro
Amici Miei. Tanto nessuno ti ascolta,
errno funziona e tanto basta, quindi perché approfondire? Beh, non sono d'accordo, ho visto (ahimè) molti usi impropri di
errno, quindi un po' di sana teoria di base può far bene (
anzi, fa sempre bene). Ovviamente gli eventuali
"Masters of Errno" qui presenti potranno saltare a piè pari la lettura dell'articolo (
che potrebbe risultare noioso, lo ammetto...).
 |
| ...errno come se fosse antani... |
Allora: cos'è
errno? Molte funzioni di libreria e
system calls in
caso di errore aggiornano il valore di una variabile globale (
errno, giust'appunto) che quindi contiene, in ogni momento, il valore dell'ultimo errore di esecuzione. Anticamente
errno era definito così nel
header (della
libc)
errno.h:
extern int errno;
e si riferiva a una semplice variabile globale della
libc, per l'appunto un
int chiamato
errno. Poi sono arrivati i
thread, con lo standard
POSIX 1003.1c (detto anche
POSIX.1c, ma fa lo stesso) e quindi
errno ha dovuto adeguarsi al modo
thread-safe. Come si può ben leggere nel manuale di
errno (dopo
POSIX.1c) ora la definizione è:
errno is defined by the ISO C standard to be a modifiable lvalue of
type int, and must not be explicitly declared; errno may be a macro.
errno is thread-local; setting it in one thread does not affect its
value in any other thread.
Quindi la nuova definizione di
errno è ora in
bits/errno.h (che viene incluso dal
errno.h classico). Semplificando un po' (
ho omesso alcuni dettagli per facilitare la lettura) il nuovo giro del fumo è:
nel header-file errno.h
#include <bits/errno.h>
/* Declare the `errno' variable, unless it's defined as a macro by
bits/errno.h. This is the case in GNU, where it is a per-thread
variable. This redeclaration using the macro still works, but it
will be a function declaration without a prototype and may trigger
a -Wstrict-prototypes warning. */
#ifndef errno
extern int errno;
#endif
nel header-file bits/errno.h
/* Function to get address of global `errno' variable. */
extern int *__errno_location (void);
/* When using threads, errno is a per-thread value. */
#define errno (*__errno_location ())
Quindi, in parole povere, ora
errno non è più un
int globale ma è "
il contenuto di un indirizzo ritornato da una funzione globale". Ovviamente la variabile
int a cui punta la funzione in oggetto è il nuovo
errno locale di un
thread (ossia: ogni
thread ha il suo
errno). Un esempio (
mooolto semplificato) di come si potrebbe implementare la
__errno_location() è il seguente:
// errno locale di un thread: è una variabile di tipo thread-local storage (TLS)
__thread int th_errno;
int *__errno_location(void)
{
// ritorna l'indirizzo della variabile th_errno
return &th_errno;
}
E, alla fine della fiera, nonostante i cambi descritti, sarà ancora possibile fare operazioni di questo tipo:
int my_errno = errno; // Ok, equivale a: int my_errno = (* __errno_location());
errno = EADDRINUSE; // Ok, equivale a: (* __errno_location()) = EADDRINUSE;
perché, ovviamente, tutto è stato pensato per essere retro-compatibile, e quindi
errno, nonostante ora sia una macro, deve ancora comportarsi come quando era un semplice
int.
E, a questo punto, non possiamo farci mancare un piccolo estratto dello standard
POSIX.1c, che puntualizza tutto quello detto fin'ora:
Redefinition of errno
In POSIX.1, errno is defined as an external global variable. But this definition
is unacceptable in a multithreaded environment, because its use can result in
nondeterministic results. The problem is that two or more threads can encounter
errors, all causing the same errno to be set. Under these circumstances, a thread
might end up checking errno after it has already been updated by another thread.
To circumvent the resulting nondeterminism, POSIX.1c redefines errno as a service
that can access the per-thread error number as follows (ISO/IEC 9945:1-1996, n2.4):
Some functions may provide the error number in a variable accessed through the
symbol errno. The symbol errno is defined by including the header <errno.h>, as
specified by the C Standard ... For each thread of a process, the value of errno
shall not be affected by function calls or assignments to errno by other threads...
E ora facciamo un passo avanti:
come possiamo sfruttare l'esistenza di
errno? A parte usarlo
direttamente come numero e agire opportunamente nel codice in base all'errore indicato, è prassi abituale cercare la descrizione dell'errore (come stringa di testo) con un meccanismo che ci viene gentilmente offerta dalla nostra amata
libc: infatti, parallelamente a
errno, viene mantenuta un'altra variabile globale,
_sys_errlist, che contiene le stringhe corrispondenti a ogni
error number, per cui (
dettaglio importantissimo di cui molti si dimenticano)
prima che qualche altra parte del programma in esecuzione alteri il valore di
errno, si può localizzare in
_sys_errlist la stringa di errore che vogliamo trattare. Questa ultima operazione si può fare usando la
funzione strerror(), in una delle sue (tante) varianti.
Ebbene si: la
strerror() ha molte personalità, quindi quale scelgo? la
strerror() o la
strerror_r()? E se uso quest'ultima quale scelgo, la versione
XSI-compliant o la versione
GNU-specific? (
per non parlare, poi, delle altre varianti, la strerror_l(), la strerror_s(), ecc., ma queste sono varianti secondarie).
Cominciamo con la prima domanda:
strerror() o
strerror_r()?
Anticamente esisteva solo la prima, ma poi, come detto sopra, sono apparsi i thread e anche qui sono
cominciati i problemi, perché nel codice
multithread ci sono alcune
parti critiche dove bisognerebbe usare solo funzioni
thread-safe. La
strerror() non è dichiarata
thread-safe
nello standard, e per capire il perché basta analizzare una
implementazione semplificata (però molto simile alle implementazioni
reali che possiamo trovare nelle varie
libc disponibili). Vai col codice!
#include <stdio.h> // stdio.h include sys_errlist.h che dichiara le variabili
// globali _sys_errlist (array errori) e _sys_nerr (num.errori)
static char buf[256]; // buffer globale statico per la stringa da ritornare
char *strerror(int errnum)
{
// test se errnum è un valore valido
if (errnum < 0 || errnum >= _sys_nerr || _sys_errlist[errnum] == NULL) {
// errore sconosciuto: copio in buf un messaggio di errore generico
snprintf(buf, sizeof(buf), "Unknown error %d", errnum);
}
else {
// errore conosciuto: copio in buf il messaggio corrispondente
snprintf(buf, sizeof(buf), "%s", _sys_errlist[errnum]);
}
// ritorno buf che ora contiene il messaggio di errore
return buf;
}
risulta evidente dal codice (
ben commentato, come sempre, così non devo spiegarlo riga per riga) che la
strerror() non ritorna direttamente
_sys_errlist[errnum] (e se fosse così sarebbe intrinsecamente
thread-safe) ma compone un messaggio di errore (per trattare anche gli
error number non validi) usando un buffer globale statico
buf: quindi, se due thread di una applicazione usano (quasi) contemporaneamente la
strerror() il contenuto di
buf non sarà attendibile (prevale il thread che ha scritto per ultimo).
(...apro una parentesi: non è vietato scrivere una strerror() che sia thread-safe,
e in alcuni sistemi lo è: ma visto che secondo lo standard non lo è,
non possiamo essere sicuri che sul sistema che stiamo usando (o sul
sistema su cui, un giorno, girerà la applicazione che stiamo scrivendo)
non ci sia una implementazione come quella appena descritta, quindi...)
Allora, per il Software
multithread è nata la
strerror_r() che è
thread-safe. Come funziona? vai col codice!
#include <stdio.h> // stdio.h include sys_errlist.h che dichiara le variabili
// globali _sys_errlist (array errori) e _sys_nerr (num.errori)
char *strerror_r(int errnum, char *buf, size_t buflen);
{
// test se errnum è un valore valido
if (errnum < 0 || errnum >= _sys_nerr || _sys_errlist[errnum] == NULL) {
// errore sconosciuto: copio in buf un messaggio di errore generico
snprintf(buf, buflen, "Unknown error %d", errnum);
}
else {
// errore conosciuto: copio in buf il messaggio corrispondente
snprintf(buf, buflen, "%s", _sys_errlist[errnum]);
}
// ritorno buf che ora contiene il messaggio di errore
return buf;
}
anche in questo caso si tratta di codice semplificato, ma molto vicino
alla realtà: il trucco è semplice, invece di usare un buffer globale
statico (che è la fonte dei problemi della
strerror()) il chiamante della funzione si deve preoccupare di allocare e passare un buffer (e la sua lunghezza) alla
strerror_r(). In questo modo il buffer che usa la
strerror_r()
è locale al thread che la chiama, e non può essere sovrascritto da un
altro thread concorrente. Abbiamo sacrificato un po' di semplicità d'uso
ma abbiamo ottenuto l'agognato comportamento
thread-safe!
Ed ora aggiungiamo un po' di complicazione: la versione di
strerror_r() appena mostrata è la
GNU-specific. Ma, sfortunatamente, esiste anche la
XSI-compliant, che è la seguente:
int strerror_r(int errnum, char *buf, size_t buflen);
Come si nota questa seconda versione non ritorna il buffer con la
error-string,
ma ritorna, invece, un codice di errore, e la stringa trovata bisogna
ripescarla direttamente nel buffer che abbiamo passato. Per quanto
riguarda il codice di ritorno è 0 in caso di successo e, in base alla
versione di
libc in uso, potrebbe ritornare -1 in caso di errore (settando
errno al valore specifico di errore) oppure un valore positivo corrispondente a
errno (
bah, questo doppio comportamento non è proprio il massimo della semplicità d'uso...). Per usare questa versione o la
GNU-specific bisogna giocare opportunamente con i flag
_GNU_SOURCE,
_POSIX_C_SOURCE e
_XOPEN_SOURCE del preprocessore (come descritto nel manuale della
strerror()).
Il motivo per cui esiste la versione
XSI-compliant è descritto nello stesso estratto dello standard
POSIX.1c citato precedentemente, al posto dei puntini di sospensione finali c'è questa parte:
...In addition, all POSIX.1c functions avoid using errno and, instead, return the
error number directly as the function return value, with a return value of zero
indicating that no error was detected. This strategy is, in fact, being followed
on a POSIX-wide basis for all new functions.
E ora siamo pronti per la seconda domanda: quale usiamo, la
GNU-specific o la
XSI-compliant?
Beh, io direi che quando scriviamo del codice per trattare dei codici
di errore probabilmente non ci interessa trattare anche gli errori
generati in questa fase (
e nella fase successiva, ecc., ecc., un loop infinito di ricerca degli errori!);
ci interessa, invece, scrivere codice lineare e semplice... per
toglierci il dubbio possiamo analizzare due piccoli esempi d'uso:
GNU-specific
if ((my_socket = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
// errore socket()
char errbuf[MAX_ERROR_LEN]; // buffer per strerror_r()
printf("socket() error (%s)\n", strerror_r(errno, errbuf, sizeof(errbuf)));
return EXIT_FAILURE;
}
XSI-compliant
if ((my_socket = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
// errore socket()
char errbuf[MAX_ERROR_LEN]; // buffer per strerror_r()
int my_error = strerror_r(errno, errbuf, sizeof(errbuf)));
if (! my_error)
printf("socket() error (%s)\n", errbuf);
else {
// tratto l'errore (magari usando di nuovo strerror_r()?)
...
}
return EXIT_FAILURE;
}
Non so voi cosa ne pensate, ma io uso sempre la versione
GNU-specific!
A voi la scelta...
In conclusione: abbiamo analizzato, nel dettaglio, come funziona
errno. Poi abbiamo visto come usarlo. E, ancora, abbiamo esaminato le varianti principali della
strerror(), notando che, molto spesso, conviene usare la
strerror_r() che è la versione
thread-safe... Cosa ci manca da approfondire? Ah si, ecco:
perchè la strerror_r() si chiama così? E perchè c'è una intera famiglia di funzioni che finiscono in "_r"? Uhm... la "
_r" sta per
reentrant che, al contrario delle credenze comuni, non è esattamente la stessa cosa di
thread-safe... Si, forse dobbiamo approfondire ulteriormente... (
SPOILER ALERT: sarà nel prossimo articolo!).
Ciao, e al prossimo post!