Czy wątek errno jest bezpieczny?

175

W errno.h, ta zmienna jest zadeklarowana jako, extern int errno;więc moje pytanie brzmi, czy bezpiecznie jest sprawdzić errnowartość po niektórych wywołaniach lub użyć perror () w kodzie wielowątkowym. Czy to zmienna bezpieczna dla wątków? Jeśli nie, to jaka jest alternatywa?

Używam Linuksa z GCC na architekturze x86.

vinit dhatrak
źródło
5
2 dokładnie przeciwne odpowiedzi, interesujące !! ;)
vinit dhatrak
Heh, sprawdź ponownie moją odpowiedź. Dodałem demonstrację, która prawdopodobnie będzie przekonująca. :-)
DigitalRoss

Odpowiedzi:

176

Tak, jest bezpieczny dla wątków. W systemie Linux globalna zmienna errno jest specyficzna dla wątku. POSIX wymaga, aby errno było bezpieczne dla wątków.

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

W POSIX.1 errno jest zdefiniowane jako zewnętrzna zmienna globalna. Jednak definicja ta jest nie do przyjęcia w środowisku wielowątkowym, ponieważ jej użycie może skutkować niedeterministycznymi wynikami. Problem polega na tym, że dwa lub więcej wątków może napotkać błędy, które powodują ustawienie tego samego errno. W takich okolicznościach wątek może zakończyć sprawdzanie errno po tym, jak został już zaktualizowany przez inny wątek.

Aby obejść wynikowy niedeterminizm, POSIX.1c przedefiniowuje errno jako usługę, która może uzyskać dostęp do numeru błędu dla każdego wątku w następujący sposób (ISO / IEC 9945: 1-1996, §2.4):

Niektóre funkcje mogą podawać numer błędu w zmiennej, do której można uzyskać dostęp poprzez symbol errno. Symbol errno jest definiowany przez dołączenie nagłówka, zgodnie ze standardem C… Dla każdego wątku procesu, wywołania funkcji lub przypisania do errno przez inne wątki nie będą miały wpływu na wartość errno.

Zobacz także http://linux.die.net/man/3/errno

errno jest lokalnym wątkiem; ustawienie go w jednym wątku nie wpływa na jego wartość w żadnym innym wątku.

Charles Salvia
źródło
9
Naprawdę? Kiedy to zrobili? Kiedy robiłem programowanie w C, zaufanie do errno było dużym problemem.
Paul Tomblin
7
Człowieku, to zaoszczędziłoby mi wielu kłopotów za moich czasów.
Paul Tomblin
4
@vinit: errno jest faktycznie zdefiniowane w bits / errno.h. Przeczytaj komentarze w dołączonym pliku. Mówi: „Zadeklaruj zmienną` errno ', chyba że jest zdefiniowana jako makro przez bits / errno.h. Tak jest w przypadku GNU, gdzie jest to zmienna dla wątku. Ta ponowna deklaracja przy użyciu makra nadal działa, ale będzie deklaracją funkcji bez prototypu i może wywołać ostrzeżenie -Wstrict-prototypes. "
Charles Salvia
2
Jeśli używasz Linuksa 2.6, nie musisz nic robić. Po prostu zacznij programować. :-)
Charles Salvia
3
@vinit dhatrak Powinno być # if !defined _LIBC || defined _LIBC_REENTRANT, _LIBC nie jest zdefiniowane podczas kompilacji normalnych programów. W każdym razie uruchom echo #include <errno.h>' | gcc -E -dM -xc - i spójrz na różnicę zi bez -pthread. errno jest #define errno (*__errno_location ())w obu przypadkach.
nr
58

tak


Errno nie jest już prostą zmienną, jest czymś złożonym za kulisami, szczególnie w celu zapewnienia bezpieczeństwa wątków.

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

Możemy dwukrotnie sprawdzić:

$ cat > test.c
#include <errno.h>
f() { g(errno); }
$ cc -E test.c | grep ^f
f() { g((*__errno_location ())); }
$ 
DigitalRoss
źródło
12

W errno.h ta zmienna jest zadeklarowana jako extern int errno;

Oto, co mówi standard C:

Makro errnonie musi być identyfikatorem obiektu. Może się rozwinąć do modyfikowalnej lwartości wynikającej z wywołania funkcji (na przykład *errno()).

Ogólnie errnojest to makro, które wywołuje funkcję zwracającą adres numeru błędu dla bieżącego wątku, a następnie wyłuskuje go.

Oto co mam w Linuksie, w /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

W końcu generuje taki kod:

> 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
źródło
10

W wielu systemach uniksowych kompilowanie z -D_REENTRANTzapewnia, że errnojest to bezpieczne wątkowo.

Na przykład:

#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
źródło
1
Myślę, że nie musisz jawnie kompilować kodu -D_REENTRANT. Zapoznaj się z dyskusją na temat innej odpowiedzi na to samo pytanie.
vinit dhatrak
3
@Vinit: to zależy od twojej platformy - na Linuksie możesz mieć rację; w Solarisie byłbyś poprawny tylko wtedy, gdy ustawiłeś _POSIX_C_SOURCE na 199506 lub nowszą wersję - prawdopodobnie używając -D_XOPEN_SOURCE=500lub -D_XOPEN_SOURCE=600. Nie każdy zadaje sobie trud, aby upewnić się, że zostało określone środowisko POSIX - a potem -D_REENTRANTmoże uratować twój boczek. Ale nadal musisz uważać - na każdej platformie - aby uzyskać pożądane zachowanie.
Jonathan Leffler
Czy istnieje dokumentacja wskazująca, który standard (np.: C99, ANSI itp.) Lub przynajmniej kompilatory (tj. Wersja GCC i nowsze) obsługują tę funkcję i czy jest ona domyślna? Dziękuję Ci.
Cloud
Możesz spojrzeć na standard C11 lub POSIX 2008 (2013) dla errno . Standard C11 mówi: ... i errnoktóry rozwija się do modyfikowalnej lwartości (201), która ma typ inti czas trwania lokalnego przechowywania wątku, której wartość jest ustawiana na dodatnią liczbę błędów przez kilka funkcji bibliotecznych. Jeśli definicja makra jest blokowana w celu uzyskania dostępu do rzeczywistego obiektu lub program definiuje identyfikator z nazwą errno, zachowanie jest niezdefiniowane. [... ciąg dalszy ...]
Jonathan Leffler
[… kontynuacja…] Przypis 201 mówi: Makro errnonie musi być identyfikatorem obiektu. Może się rozwinąć do modyfikowalnej lwartości wynikającej z wywołania funkcji (na przykład *errno()). Główny tekst kontynuuje: Wartość errno w początkowym wątku wynosi zero podczas uruchamiania programu (początkowa wartość errno w innych wątkach jest wartością nieokreśloną), ale nigdy nie jest zerowana przez żadną funkcję biblioteczną. POSIX używa standardu C99, który nie rozpoznaje wątków. [… także ciąg dalszy…]
Jonathan Leffler
10

To jest z <sys/errno.h>mojego Maca:

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

Więc errnojest teraz funkcją __error(). Funkcja jest zaimplementowana tak, aby była bezpieczna dla wątków.

vy32
źródło
9

tak , jak jest to wyjaśnione na stronie podręcznika errno i w innych odpowiedziach, errno jest lokalną zmienną wątku.

jednak , nie jest głupi szczegół, który mógłby być łatwo zapomnieć. Programy powinny zapisywać i odtwarzać errno w każdym programie obsługi sygnału wykonującym wywołanie systemowe. Dzieje się tak, ponieważ sygnał będzie obsługiwany przez jeden z wątków procesu, który mógłby nadpisać jego wartość.

Dlatego programy obsługi sygnału powinny zapisywać i odtwarzać errno. Coś jak:

void sig_alarm(int signo)
{
 int errno_save;

 errno_save = errno;

 //whatever with a system call

 errno = errno_save;
}
marcmagransdeabril
źródło
zabronione → zapomniane, jak przypuszczam. Czy możesz podać odniesienie do szczegółów dotyczących zapisywania / odtwarzania wywołań systemowych?
Craig McQueen,
Cześć Craig, dzięki za informację o literówce, teraz została poprawiona. Jeśli chodzi o drugą kwestię, nie jestem pewien, czy dobrze rozumiem, o co prosisz. Każde wywołanie, które modyfikuje errno w programie obsługi sygnału, mogłoby kolidować z errno używanym przez ten sam wątek, który został przerwany (np. Używając strtol wewnątrz sig_alarm). dobrze?
marcmagransdeabril
6

Myślę, że odpowiedź brzmi „to zależy”. Biblioteki środowiska uruchomieniowego C z ochroną wątków zwykle implementują errno jako wywołanie funkcji (makro rozwijające się do funkcji), jeśli tworzysz kod z wątkami z odpowiednimi flagami.

Timo Geusch
źródło
@Timo, tak, masz rację, zapoznaj się z dyskusją na temat innej odpowiedzi i daj znać, jeśli czegoś brakuje.
vinit dhatrak
3

Możemy to sprawdzić, uruchamiając prosty program na maszynie.

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

Uruchomienie tego programu i możesz zobaczyć różne adresy dla errno w każdym wątku. Wynik uruchomienia na moim komputerze wyglądał następująco: -

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 

Zauważ, że adres jest inny dla wszystkich wątków.

Rajat Paliwal
źródło
Chociaż wyszukiwanie tego na stronie podręcznika (lub na SO) jest szybsze, to lubię, że masz czas, aby to sprawdzić. +1.
Bayou