O errno thread-safe?

175

Em errno.h, essa variável é declarada como extern int errno;minha pergunta é: é seguro verificar o errnovalor após algumas chamadas ou usar perror () no código multiencadeado. Essa é uma variável segura de thread? Se não, então qual é a alternativa?

Estou usando o linux com gcc na arquitetura x86.

Vinit Dhatrak
fonte
5
2 respostas opostas exatas, interessantes !! ;)
vinit dhatrak 7/11/2009
Heh, verifique novamente a minha resposta. Eu adicionei uma demonstração que provavelmente será convincente. :-)
DigitalRoss

Respostas:

176

Sim, é seguro para threads. No Linux, a variável global errno é específica do segmento. O POSIX exige que o erro não seja seguro.

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

No POSIX.1, errno é definido como uma variável global externa. Mas essa definição é inaceitável em um ambiente multithread, porque seu uso pode resultar em resultados não determinísticos. O problema é que dois ou mais encadeamentos podem encontrar erros, todos causando o mesmo erro. Nessas circunstâncias, um encadeamento pode acabar verificando errno depois de já ter sido atualizado por outro encadeamento.

Para contornar o não determinismo resultante, o POSIX.1c redefine errno como um serviço que pode acessar o número de erro por thread da seguinte maneira (ISO / IEC 9945: 1-1996, §2.4):

Algumas funções podem fornecer o número do erro em uma variável acessada através do símbolo errno. O símbolo errno é definido pela inclusão do cabeçalho, conforme especificado pelo Padrão C ... Para cada encadeamento de um processo, o valor de errno não deve ser afetado por chamadas ou atribuições de função a errno por outros encadeamentos.

Veja também http://linux.die.net/man/3/errno

errno é thread-local; defini-lo em um thread não afeta seu valor em nenhum outro thread.

Charles Salvia
fonte
9
Realmente? Quando eles fizeram isso? Quando eu estava fazendo programação C, confiar em errno era um grande problema.
Paul Tomblin
7
Cara, isso teria economizado muito aborrecimento nos meus dias.
Paul Tomblin
4
@vinit: errno é realmente definido em bits / errno.h. Leia os comentários no arquivo de inclusão. Ele diz: "Declare a variável` errno ', a menos que seja definida como uma macro por bits / errno.h. É o caso do GNU, onde é uma variável por thread. Essa redeclaração usando a macro ainda funciona, mas será uma declaração de função sem um protótipo e poderá acionar um aviso de protótipo restrito. "
Charles Salvia
2
Se você estiver usando o Linux 2.6, não precisará fazer nada. Basta começar a programar. :-)
Charles Salvia
3
@vinit dhatrak Deveria haver # if !defined _LIBC || defined _LIBC_REENTRANT_LIBC não está definido ao compilar programas normais. De qualquer forma, execute eco #include <errno.h>' | gcc -E -dM -xc - e veja a diferença com e sem -pthread. errno é #define errno (*__errno_location ())nos dois casos.
Nos
58

sim


Errno não é mais uma variável simples, é algo complexo nos bastidores, especificamente para ser seguro para threads.

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

Podemos verificar:

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

Em errno.h, essa variável é declarada como extern int errno;

Aqui está o que o padrão C diz:

A macro errnonão precisa ser o identificador de um objeto. Pode expandir para um valor l modificável resultante de uma chamada de função (por exemplo, *errno()).

Geralmente, errnoé uma macro que chama uma função retornando o endereço do número do erro para o encadeamento atual e o desreferencia.

Aqui está o que eu tenho no Linux, em /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

No final, ele gera esse tipo de código:

> 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
fonte
10

Em muitos sistemas Unix, a compilação com -D_REENTRANTgarante errnosegurança de thread.

Por exemplo:

#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
fonte
1
Eu acho que você não precisa compilar código explicitamente -D_REENTRANT. Consulte a discussão em outra resposta para a mesma pergunta.
vinit dhatrak
3
@Vinit: depende da sua plataforma - no Linux, você pode estar correto; no Solaris, você estaria correto apenas se tiver definido _POSIX_C_SOURCE como 199506 ou uma versão posterior - provavelmente usando -D_XOPEN_SOURCE=500ou -D_XOPEN_SOURCE=600. Nem todo mundo se preocupa em garantir que o ambiente POSIX seja especificado - e então você -D_REENTRANTpode salvar seu bacon. Mas você ainda precisa ter cuidado - em cada plataforma - para garantir o comportamento desejado.
Jonathan Leffler
Existe uma documentação que indica qual padrão (por exemplo: C99, ANSI, etc) ou pelo menos quais compiladores (por exemplo: versão do GCC e seguintes) que suportam esse recurso e se é ou não um padrão? Obrigado.
Cloud
Você pode olhar para o padrão C11 ou para POSIX 2008 (2013) quanto a erros . O padrão C11 diz: ... e errnoque se expande para um lvalue modificável (201) que possui intduração de armazenamento local de tipo e encadeamento, cujo valor é definido como um número de erro positivo por várias funções da biblioteca. Se uma definição de macro for suprimida para acessar um objeto real ou um programa definir um identificador com o nome errno, o comportamento será indefinido. [... continuação ...]
Jonathan Leffler
[... continuação ...] A nota de rodapé 201 diz: A macro errnonão precisa ser o identificador de um objeto. Pode expandir para um valor l modificável resultante de uma chamada de função (por exemplo, *errno()). O texto principal continua: O valor de errno no encadeamento inicial é zero na inicialização do programa (o valor inicial de errno em outros encadeamentos é um valor indeterminado), mas nunca é definido como zero por nenhuma função da biblioteca. O POSIX usa o padrão C99 que não reconheceu threads. [... também continuou ...]
Jonathan Leffler
10

Isto é do <sys/errno.h>meu Mac:

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

Então errnoagora é uma função __error(). A função é implementada de modo a ser segura para threads.

vy32
fonte
9

sim , como é explicado na página do manual errno e nas outras respostas, errno é uma variável local do encadeamento.

No entanto , há um detalhe bobo que pode ser facilmente esquecido. Os programas devem salvar e restaurar o erro em qualquer manipulador de sinal que esteja executando uma chamada do sistema. Isso ocorre porque o sinal será tratado por um dos threads do processo que pode sobrescrever seu valor.

Portanto, os manipuladores de sinal devem salvar e restaurar o erro. Algo como:

void sig_alarm(int signo)
{
 int errno_save;

 errno_save = errno;

 //whatever with a system call

 errno = errno_save;
}
marcmagransdeabril
fonte
proibido → esquecido, eu presumo. Você pode fornecer uma referência para este detalhe de salvar / restaurar de chamada do sistema?
Craig McQueen
Olá Craig, obrigado pela informação sobre o erro de digitação, agora está corrigido. Em relação à outra questão, não sei se entendi corretamente o que você está pedindo. Qualquer chamada que modifique errno no manipulador de sinal pode interferir com o erro sendo usado pelo mesmo encadeamento interrompido (por exemplo, usando strtol dentro de sig_alarm). certo?
Marcmagransdeabril
6

Eu acho que a resposta é "depende". As bibliotecas de tempo de execução C seguras para threads geralmente implementam errno como uma chamada de função (macro expandida para uma função) se você estiver criando código encadeado com os sinalizadores corretos.

Timo Geusch
fonte
@ Timo, Sim, você está certo, consulte a discussão sobre outra resposta e informe se algo está faltando.
Vinit Dhatrak
3

Podemos verificar executando um programa simples em uma máquina.

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

Executando este programa e você pode ver endereços diferentes para errno em cada thread. A saída de uma execução na minha máquina parecia: -

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 

Observe que o endereço é diferente para todos os segmentos.

Rajat Paliwal
fonte
Embora procurá-lo em uma página de manual (ou no SO) seja mais rápido, gosto de ter tempo para verificá-lo. +1.
Bayou