Errno est-il sûr pour les threads?

176

Dans errno.h, cette variable est déclarée telle extern int errno;que ma question est, est-il sûr de vérifier la errnovaleur après certains appels ou d'utiliser perror () dans du code multi-thread. Est-ce une variable thread-safe? Sinon, quelle est l'alternative?

J'utilise Linux avec gcc sur l'architecture x86.

vinit dhatrak
la source
5
2 réponses exactement opposées, intéressantes !! ;)
vinit dhatrak
Heh, revérifiez ma réponse. J'ai ajouté une démonstration qui sera probablement convaincante. :-)
DigitalRoss

Réponses:

177

Oui, c'est thread-safe. Sous Linux, la variable globale errno est spécifique au thread. POSIX exige que errno soit threadsafe.

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

Dans POSIX.1, errno est défini comme une variable globale externe. Mais cette définition est inacceptable dans un environnement multithread, car son utilisation peut entraîner des résultats non déterministes. Le problème est que deux ou plusieurs threads peuvent rencontrer des erreurs, provoquant toutes la définition du même numéro d'erreur. Dans ces circonstances, un thread peut finir par vérifier errno après avoir déjà été mis à jour par un autre thread.

Pour contourner le non-déterminisme résultant, POSIX.1c redéfinit errno comme un service qui peut accéder au numéro d'erreur par thread comme suit (ISO / CEI 9945: 1-1996, §2.4):

Certaines fonctions peuvent fournir le numéro d'erreur dans une variable accessible via le symbole errno. Le symbole errno est défini en incluant l'en-tête, comme spécifié par la norme C ... Pour chaque thread d'un processus, la valeur de errno ne doit pas être affectée par les appels de fonction ou les affectations à errno par d'autres threads.

Voir également http://linux.die.net/man/3/errno

errno est local au thread; le définir dans un thread n'affecte sa valeur dans aucun autre thread.

Charles Salvia
la source
9
Vraiment? Quand ont-ils fait ça? Quand je faisais de la programmation C, faire confiance à errno était un gros problème.
Paul Tomblin
7
Mec, cela aurait évité beaucoup de tracas à mon époque.
Paul Tomblin
4
@vinit: errno est en fait défini dans bits / errno.h. Lisez les commentaires dans le fichier d'inclusion. Il dit: "Déclarez la variable` errno ', sauf si elle est définie comme une macro par bits / errno.h. C'est le cas dans GNU, où il s'agit d'une variable par thread. Cette redéclaration utilisant la macro fonctionne toujours, mais elle sera une déclaration de fonction sans prototype et peut déclencher un avertissement -Wstrict-prototypes. "
Charles Salvia
2
Si vous utilisez Linux 2.6, vous n'avez rien à faire. Commencez simplement la programmation. :-)
Charles Salvia
3
@vinit dhatrak Il devrait y avoir # if !defined _LIBC || defined _LIBC_REENTRANT, _LIBC n'est pas défini lors de la compilation de programmes normaux. Quoi qu'il en soit, lancez echo #include <errno.h>' | gcc -E -dM -xc - et regardez la différence avec et sans -pthread. errno est #define errno (*__errno_location ())dans les deux cas.
nos
58

Oui


Errno n'est plus une simple variable, c'est quelque chose de complexe dans les coulisses, spécifiquement pour être thread-safe.

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

Nous pouvons vérifier:

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

Dans errno.h, cette variable est déclarée comme extern int errno;

Voici ce que dit la norme C:

La macro errnon'a pas besoin d'être l'identifiant d'un objet. Il peut s'étendre jusqu'à une lvalue modifiable résultant d'un appel de fonction (par exemple, *errno()).

Généralement, errnoest une macro qui appelle une fonction retournant l'adresse du numéro d'erreur pour le thread actuel, puis la déréférence.

Voici ce que j'ai sur Linux, dans /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

Au final, il génère ce genre de code:

> 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
Bastien Léonard
la source
10

Sur de nombreux systèmes Unix, la compilation avec -D_REENTRANTgarantit la errnosécurité des threads.

Par exemple:

#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) */
Jonathan Leffler
la source
1
Je suppose que vous n'avez pas à compiler le code explicitement avec -D_REENTRANT. Veuillez vous référer à la discussion sur une autre réponse pour la même question.
vinit dhatrak
3
@Vinit: cela dépend de votre plate-forme - sous Linux, vous avez peut-être raison; sous Solaris, vous n’auriez raison que si vous avez défini _POSIX_C_SOURCE sur 199506 ou une version ultérieure - probablement en utilisant -D_XOPEN_SOURCE=500ou -D_XOPEN_SOURCE=600. Tout le monde ne se soucie pas de s'assurer que l'environnement POSIX est spécifié - et -D_REENTRANTpeut ensuite sauver votre bacon. Mais il faut quand même faire attention - sur chaque plateforme - pour s'assurer d'obtenir le comportement souhaité.
Jonathan Leffler
Existe-t-il une documentation qui indique quelle norme (c'est-à-dire: C99, ANSI, etc.) ou au moins quels compilateurs (c'est-à-dire: version GCC et suivantes) qui prennent en charge cette fonctionnalité, et s'il s'agit ou non d'une valeur par défaut? Je vous remercie.
Cloud
Vous pouvez consulter la norme C11 ou POSIX 2008 (2013) pour errno . La norme C11 dit: ... et errnoqui se développe en une lvalue modifiable (201) qui a intune durée de stockage locale de type et de thread, dont la valeur est définie sur un nombre d'erreur positif par plusieurs fonctions de bibliothèque. Si une définition de macro est supprimée pour accéder à un objet réel, ou si un programme définit un identifiant avec le nom errno, le comportement n'est pas défini. [... suite ...]
Jonathan Leffler
[... suite ...] La note de bas de page 201 dit: La macro errnon'a pas besoin d'être l'identifiant d'un objet. Il peut s'étendre jusqu'à une lvalue modifiable résultant d'un appel de fonction (par exemple, *errno()). Le texte principal continue: La valeur de errno dans le thread initial est zéro au démarrage du programme (la valeur initiale de errno dans les autres threads est une valeur indéterminée), mais n'est jamais mise à zéro par aucune fonction de bibliothèque. POSIX utilise le standard C99 qui ne reconnaissait pas les threads. [... également continué ...]
Jonathan Leffler
10

C'est à partir <sys/errno.h>de mon Mac:

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

Alors errnoc'est maintenant une fonction __error(). La fonction est implémentée de manière à être thread-safe.

vy32
la source
9

oui , comme il est expliqué dans la page de manuel errno et les autres réponses, errno est une variable locale de thread.

Cependant , il y a un détail idiot qui pourrait être facilement oublié. Les programmes doivent enregistrer et restaurer le numéro d'erreur sur tout gestionnaire de signal exécutant un appel système. En effet, le signal sera traité par l'un des threads de processus qui pourrait écraser sa valeur.

Par conséquent, les gestionnaires de signaux doivent enregistrer et restaurer errno. Quelque chose comme:

void sig_alarm(int signo)
{
 int errno_save;

 errno_save = errno;

 //whatever with a system call

 errno = errno_save;
}
marcmagransdeabril
la source
interdit → oublié je présume. Pouvez-vous fournir une référence pour ce détail d'enregistrement / restauration d'appel système?
Craig McQueen
Salut Craig, merci pour les informations sur la faute de frappe, est maintenant corrigée. En ce qui concerne l’autre question, je ne suis pas sûr de bien comprendre ce que vous demandez. Tout appel qui modifie errno dans le gestionnaire de signal pourrait interférer avec le errno utilisé par le même thread qui a été interrompu (par exemple en utilisant strtol dans sig_alarm). droite?
marcmagransdeabril
6

Je pense que la réponse est "ça dépend". Les bibliothèques d'exécution C thread-safe implémentent généralement errno en tant qu'appel de fonction (macro se développant en fonction) si vous créez du code thread avec les indicateurs appropriés.

Timo Geusch
la source
@Timo, oui, vous avez raison, veuillez vous référer à la discussion sur une autre réponse et faites savoir si quelque chose manque.
vinit dhatrak
3

Nous pouvons vérifier en exécutant un programme simple sur une machine.

#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);                                                                                       
}

En exécutant ce programme, vous pouvez voir différentes adresses pour errno dans chaque thread. La sortie d'une course sur ma machine ressemblait à: -

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 

Notez que l'adresse est différente pour tous les threads.

Rajat Paliwal
la source
Bien que la recherche dans une page de manuel (ou sur SO) soit plus rapide, j'aime que vous ayez pris le temps de la vérifier. +1.
Bayou