Errno è thread-safe?


176

In errno.h, questa variabile è dichiarata extern int errno;così com'è la mia domanda, è sicuro controllare il errnovalore dopo alcune chiamate o usare perror () nel codice multi-thread. È una variabile thread-safe? In caso contrario, qual è l'alternativa?

Sto usando Linux con gcc su architettura x86.


5
2 risposte esatte opposte, interessanti !! ;)
vinit dhatrak

Heh, ricontrolla la mia risposta. Ho aggiunto una dimostrazione che sarà probabilmente convincente. :-)
DigitalRoss

Risposte:


177

Sì, è thread-safe. Su Linux, la variabile errno globale è specifica del thread. POSIX richiede che errno sia thread-safe.

Vedi http://www.unix.org/whitepapers/reentrant.html

In POSIX.1, errno è definito come una variabile globale esterna. Ma questa definizione è inaccettabile in un ambiente multithread, poiché il suo utilizzo può portare a risultati non deterministici. Il problema è che due o più thread possono riscontrare errori, provocando tutti lo stesso errno da impostare. In queste circostanze, un thread potrebbe finire per controllare errno dopo che è già stato aggiornato da un altro thread.

Per aggirare il non determinismo risultante, POSIX.1c ridefinisce errno come servizio che può accedere al numero di errore per thread come segue (ISO / IEC 9945: 1-1996, §2.4):

Alcune funzioni possono fornire il numero di errore in una variabile accessibile tramite il simbolo errno. Il simbolo errno è definito includendo l'intestazione, come specificato dalla norma C ... Per ogni thread di un processo, il valore di errno non deve essere influenzato dalle chiamate di funzione o dalle assegnazioni a errno da parte di altri thread.

Vedi anche http://linux.die.net/man/3/errno

errno è thread-local; l'impostazione in un thread non influisce sul suo valore in nessun altro thread.


9
Veramente? Quando l'hanno fatto? Quando stavo facendo la programmazione in C, fidarmi di errno era un grosso problema.
Paul Tomblin,

7
Amico, mi avrebbe risparmiato un sacco di problemi ai miei tempi.
Paul Tomblin,

4
@vinit: errno è attualmente definito in bit / errno.h. Leggi i commenti nel file include. Dice: "Dichiara la variabile` errno ', a meno che non sia definita come una macro da bit / errno.h. Questo è il caso in GNU, dove è una variabile per thread. Questa redeclaration usando la macro funziona ancora, ma funziona sarà una dichiarazione di funzione senza prototipo e potrebbe attivare un avviso -Wstrict-prototypes. "
Charles Salvia,

2
Se stai usando Linux 2.6, non devi fare nulla. Inizia a programmare. :-)
Charles Salvia,

3
@vinit dhatrak Dovrebbe esserci # if !defined _LIBC || defined _LIBC_REENTRANT, _LIBC non è definito durante la compilazione di programmi normali. Ad ogni modo, esegui l'eco #include <errno.h>' | gcc -E -dM -xc - e osserva la differenza con e senza -pthread. errno è #define errno (*__errno_location ())in entrambi i casi.
nn.

58


Errno non è più una semplice variabile, è qualcosa di complesso dietro le quinte, in particolare per essere thread-safe.

Vedi $ man 3 errno:

ERRNO(3)                   Linux Programmers Manual                  ERRNO(3)

NAME
       errno - number of last error

SYNOPSIS
       #include <errno.h>

DESCRIPTION

      ...
       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.

Possiamo ricontrollare:

$ cat > test.c
#include <errno.h>
f() { g(errno); }
$ cc -E test.c | grep ^f
f() { g((*__errno_location ())); }
$ 

12

In errno.h, questa variabile è dichiarata come extern int errno;

Ecco cosa dice lo standard C:

La macro errnonon deve necessariamente essere l'identificatore di un oggetto. Potrebbe espandersi in un valore modificabile derivante da una chiamata di funzione (ad esempio, *errno()).

In genere, errnoè una macro che chiama una funzione che restituisce l'indirizzo del numero di errore per il thread corrente, quindi lo dereferenzia.

Ecco cosa ho su Linux, in /usr/include/bits/errno.h:

/* Function to get address of global `errno' variable.  */
extern int *__errno_location (void) __THROW __attribute__ ((__const__));

#  if !defined _LIBC || defined _LIBC_REENTRANT
/* When using threads, errno is a per-thread value.  */
#   define errno (*__errno_location ())
#  endif

Alla fine, genera questo tipo di codice:

> cat essai.c
#include <errno.h>

int
main(void)
{
    errno = 0;

    return 0;
}
> gcc -c -Wall -Wextra -pedantic essai.c
> objdump -d -M intel essai.o

essai.o:     file format elf32-i386


Disassembly of section .text:

00000000 <main>:
   0: 55                    push   ebp
   1: 89 e5                 mov    ebp,esp
   3: 83 e4 f0              and    esp,0xfffffff0
   6: e8 fc ff ff ff        call   7 <main+0x7>  ; get address of errno in EAX
   b: c7 00 00 00 00 00     mov    DWORD PTR [eax],0x0  ; store 0 in errno
  11: b8 00 00 00 00        mov    eax,0x0
  16: 89 ec                 mov    esp,ebp
  18: 5d                    pop    ebp
  19: c3                    ret

10

Su molti sistemi Unix, la compilazione con -D_REENTRANTassicura che errnosia thread-safe.

Per esempio:

#if defined(_REENTRANT) || _POSIX_C_SOURCE - 0 >= 199506L
extern int *___errno();
#define errno (*(___errno()))
#else
extern int errno;
/* ANSI C++ requires that errno be a macro */
#if __cplusplus >= 199711L
#define errno errno
#endif
#endif  /* defined(_REENTRANT) */

1
Immagino che non devi compilare il codice esplicitamente con -D_REENTRANT. Fare riferimento alla discussione sull'altra risposta per la stessa domanda.
Vinit Dhatrak,

3
@Vinit: dipende dalla tua piattaforma - su Linux, potresti avere ragione; su Solaris, si sarebbe corretti solo se _POSIX_C_SOURCE fosse stato impostato su 199506 o una versione successiva, probabilmente utilizzando -D_XOPEN_SOURCE=500o -D_XOPEN_SOURCE=600. Non tutti si preoccupano di garantire che l'ambiente POSIX sia specificato, quindi -D_REENTRANTpossono salvare il bacon. Ma devi ancora stare attento - su ogni piattaforma - per assicurarti di ottenere il comportamento desiderato.
Jonathan Leffler,

Esiste una documentazione che indica quale standard (ovvero: C99, ANSI, ecc.) O almeno quali compilatori (ovvero: versione GCC e successive) che supportano questa funzione e se è o meno un valore predefinito? Grazie.
Cloud

Puoi guardare lo standard C11 o POSIX 2008 (2013) per errno . Lo standard C11 dice: ... e errnoche si espande in un valore modificabile (201) che ha tipo inte durata della memoria locale del thread, il cui valore è impostato su un numero di errore positivo da diverse funzioni di libreria. Se una definizione macro viene soppressa per accedere a un oggetto reale o un programma definisce un identificatore con il nome errno, il comportamento non è definito. [... continua ...]
Jonathan Leffler il

[... continuazione ...] La nota 201 dice: La macro errnonon deve essere l'identificatore di un oggetto. Potrebbe espandersi in un valore modificabile derivante da una chiamata di funzione (ad esempio, *errno()). Il testo principale continua: Il valore di errno nel thread iniziale è zero all'avvio del programma (il valore iniziale di errno in altri thread è un valore indeterminato), ma non viene mai impostato su zero da nessuna funzione di libreria. POSIX utilizza lo standard C99 che non ha riconosciuto i thread. [... continua anche ...]
Jonathan Leffler il

10

Questo è dal <sys/errno.h>mio Mac:

#include <sys/cdefs.h>
__BEGIN_DECLS
extern int * __error(void);
#define errno (*__error())
__END_DECLS

Quindi errnoora è una funzione __error(). La funzione è implementata in modo da essere thread-safe.


9

, come spiegato nella pagina man di errno e nelle altre risposte, errno è una variabile locale di thread.

Tuttavia , c'è un dettaglio stupido che potrebbe essere facilmente dimenticato. I programmi dovrebbero salvare e ripristinare l'errno su qualsiasi gestore di segnale che esegue una chiamata di sistema. Questo perché il segnale verrà gestito da uno dei thread di processo che potrebbe sovrascriverne il valore.

Pertanto, i gestori di segnale dovrebbero salvare e ripristinare errno. Qualcosa di simile a:

void sig_alarm(int signo)
{
 int errno_save;

 errno_save = errno;

 //whatever with a system call

 errno = errno_save;
}

proibito → dimenticato presumo. Potete fornire un riferimento per questo dettaglio di salvataggio / ripristino delle chiamate di sistema?
Craig McQueen,

Ciao Craig, grazie per le informazioni sull'errore di battitura, ora è corretto. Per quanto riguarda l'altro problema, non sono sicuro di aver capito correttamente cosa stai chiedendo. Qualsiasi chiamata che modifichi errno nel gestore del segnale potrebbe interferire con l'errno utilizzato dallo stesso thread che è stato interrotto (ad es. Usando strtol dentro sig_alarm). giusto?
marcmagransdeabril,

6

Penso che la risposta sia "dipende". Le librerie di runtime C thread-safe di solito implementano errno come una chiamata di funzione (macro che si espande in una funzione) se stai costruendo un codice thread con i flag corretti.


@Timo, sì, hai ragione, fai riferimento alla discussione su un'altra risposta e fai sapere se manca qualcosa.
Vinit Dhatrak,

3

Possiamo verificare eseguendo un semplice programma su una macchina.

#include <stdio.h>                                                                                                                                             
#include <pthread.h>                                                                                                                                           
#include <errno.h>                                                                                                                                             
#define NTHREADS 5                                                                                                                                             
void *thread_function(void *);                                                                                                                                 

int                                                                                                                                                            
main()                                                                                                                                                         
{                                                                                                                                                              
   pthread_t thread_id[NTHREADS];                                                                                                                              
   int i, j;                                                                                                                                                   

   for(i=0; i < NTHREADS; i++)                                                                                                                                 
   {
      pthread_create( &thread_id[i], NULL, thread_function, NULL );                                                                                            
   }                                                                                                                                                           

   for(j=0; j < NTHREADS; j++)                                                                                                                                 
   {                                                                                                                                                           
      pthread_join( thread_id[j], NULL);                                                                                                                       
   }                                                                                                                                                           
   return 0;                                                                                                                                                   
}                                                                                                                                                              

void *thread_function(void *dummyPtr)                                                                                                                          
{                                                                                                                                                              
   printf("Thread number %ld addr(errno):%p\n", pthread_self(), &errno);                                                                                       
}

Eseguendo questo programma e puoi vedere diversi indirizzi per errno in ogni thread. L'output di una corsa sulla mia macchina sembrava: -

Thread number 140672336922368 addr(errno):0x7ff0d4ac0698                                                                                                       
Thread number 140672345315072 addr(errno):0x7ff0d52c1698                                                                                                       
Thread number 140672328529664 addr(errno):0x7ff0d42bf698                                                                                                       
Thread number 140672320136960 addr(errno):0x7ff0d3abe698                                                                                                       
Thread number 140672311744256 addr(errno):0x7ff0d32bd698 

Si noti che l'indirizzo è diverso per tutti i thread.


Sebbene cercarlo in una pagina man (o su SO) sia più veloce, mi piace che tu abbia avuto il tempo di verificarlo. +1.
Bayou,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.