¿Errno es seguro para subprocesos?

175

En errno.h, esta variable se declara extern int errno;así , así que mi pregunta es, ¿es seguro verificar el errnovalor después de algunas llamadas o usar perror () en código de subprocesos múltiples? ¿Es esta una variable segura para subprocesos? Si no, ¿cuál es la alternativa?

Estoy usando Linux con gcc en la arquitectura x86.

vinit dhatrak
fuente
55
2 respuestas opuestas exactas, interesantes !! ;)
vinit dhatrak el
Je, vuelva a verificar mi respuesta. He agregado una demostración que probablemente sea convincente. :-)
DigitalRoss el

Respuestas:

176

Sí, es seguro para subprocesos. En Linux, la variable global errno es específica de subproceso. POSIX requiere que errno sea threadsafe.

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

En POSIX.1, errno se define como una variable global externa. Pero esta definición es inaceptable en un entorno multiproceso, porque su uso puede dar lugar a resultados no deterministas. El problema es que dos o más subprocesos pueden encontrar errores, todos causando que se establezca el mismo error. En estas circunstancias, un hilo podría terminar comprobando errno después de que otro hilo lo haya actualizado.

Para eludir el no determinismo resultante, POSIX.1c redefine errno como un servicio que puede acceder al número de error por hilo de la siguiente manera (ISO / IEC 9945: 1-1996, §2.4):

Algunas funciones pueden proporcionar el número de error en una variable a la que se accede mediante el símbolo errno. El símbolo errno se define al incluir el encabezado, como se especifica en el Estándar C ... Para cada subproceso de un proceso, el valor de errno no se verá afectado por las llamadas a funciones o asignaciones a errno por otros subprocesos.

Ver también http://linux.die.net/man/3/errno

errno es hilo local; configurarlo en un hilo no afecta su valor en ningún otro hilo.

Charles Salvia
fuente
9
De Verdad? ¿Cuándo hicieron eso? Cuando estaba haciendo programación en C, confiar en errno era un gran problema.
Paul Tomblin el
77
Hombre, eso me habría ahorrado muchas molestias en mi día.
Paul Tomblin, el
44
@vinit: errno se define realmente en bits / errno.h. Lea los comentarios en el archivo de inclusión. Dice: "Declare la variable 'errno', a menos que esté definida como una macro por bits / errno.h. Este es el caso en GNU, donde es una variable por subproceso. Esta redeclaración usando la macro aún funciona, pero funciona será una declaración de función sin un prototipo y puede activar una advertencia -Wstrict-prototypes ".
Charles Salvia el
2
Si está utilizando Linux 2.6, no necesita hacer nada. Solo comienza a programar. :-)
Charles Salvia el
3
@vinit dhatrak Debe haber # if !defined _LIBC || defined _LIBC_REENTRANT, _LIBC no está definido al compilar programas normales. De todos modos, ejecuta echo #include <errno.h>' | gcc -E -dM -xc - y mira la diferencia con y sin hilo. errno es #define errno (*__errno_location ())en ambos casos.
nos
58

si


Errno ya no es una variable simple, es algo complejo detrás de escena, específicamente para que sea seguro para subprocesos.

Ver $ 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 dos veces:

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

En errno.h, esta variable se declara como extern int errno;

Esto es lo que dice el estándar C:

La macro errnono necesita ser el identificador de un objeto. Puede expandirse a un valor l modificable como resultado de una llamada de función (por ejemplo, *errno()).

En general, errnoes una macro que llama a una función que devuelve la dirección del número de error para el subproceso actual y luego la desreferencia.

Esto es lo que tengo en Linux, en /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

Al final, genera este 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
fuente
10

En muchos sistemas Unix, compilar con -D_REENTRANTgarantiza que errnosea ​​seguro para subprocesos.

Por ejemplo:

#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
fuente
1
Supongo que no tienes que compilar código explícitamente -D_REENTRANT. Consulte la discusión sobre otra respuesta para la misma pregunta.
vinit dhatrak el
3
@Vinit: depende de su plataforma; en Linux, puede ser correcto; en Solaris, sería correcto solo si ha configurado _POSIX_C_SOURCE en 199506 o una versión posterior, probablemente utilizando -D_XOPEN_SOURCE=500o -D_XOPEN_SOURCE=600. No todos se molestan en asegurarse de que se especifique el entorno POSIX, y luego -D_REENTRANTpueden guardar su tocino. Pero aún debe tener cuidado, en cada plataforma, para asegurarse de obtener el comportamiento deseado.
Jonathan Leffler el
¿Existe alguna documentación que indique qué estándar (es decir: C99, ANSI, etc.) o al menos qué compiladores (es decir: versión GCC y posteriores) que admiten esta característica y si es o no un valor predeterminado? Gracias.
Nube
Puede mirar el estándar C11 o POSIX 2008 (2013) para errno . El estándar C11 dice: ... y errnoque se expande a un valor l modificable (201) que tiene intuna duración de almacenamiento local de tipo e hilo, cuyo valor se establece en un número de error positivo por varias funciones de la biblioteca. Si se suprime una definición de macro para acceder a un objeto real, o un programa define un identificador con el nombre errno, el comportamiento es indefinido. [... continúa ...]
Jonathan Leffler
[... continuación ...] La nota 201 dice: La macro errnono necesita ser el identificador de un objeto. Puede expandirse a un valor l modificable como resultado de una llamada de función (por ejemplo, *errno()). El texto principal continúa: el valor de errno en el subproceso inicial es cero al inicio del programa (el valor inicial de errno en otros subprocesos es un valor indeterminado), pero ninguna función de biblioteca establece nunca el valor en cero. POSIX usa el estándar C99 que no reconoció los hilos. [... también continuó ...]
Jonathan Leffler
10

Esto es de <sys/errno.h>mi Mac:

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

Entonces errnoahora es una función __error(). La función se implementa para ser segura para subprocesos.

vy32
fuente
9

, como se explica en la página del manual de errno y las otras respuestas, errno es una variable local de hilo.

Sin embargo , hay un detalle tonto que podría olvidarse fácilmente. Los programas deben guardar y restaurar el error en cualquier controlador de señal que ejecute una llamada al sistema. Esto se debe a que la señal será manejada por uno de los hilos del proceso que podría sobrescribir su valor.

Por lo tanto, los manejadores de señal deben guardar y restaurar errno. Algo como:

void sig_alarm(int signo)
{
 int errno_save;

 errno_save = errno;

 //whatever with a system call

 errno = errno_save;
}
marcmagransdeabril
fuente
prohibido → olvidado supongo. ¿Puede proporcionar una referencia para este sistema de llamada guardar / restaurar detalles?
Craig McQueen
Hola Craig, gracias por la información sobre el error tipográfico, ahora está corregido. Con respecto al otro tema, no estoy seguro si entiendo correctamente lo que está pidiendo. Cualquier llamada que modifique errno en el controlador de señal podría interferir con el uso de errno por el mismo subproceso que se interrumpió (por ejemplo, usando strtol dentro de sig_alarm). ¿Derecha?
marcmagransdeabril
6

Creo que la respuesta es "depende". Las bibliotecas de tiempo de ejecución C seguras para subprocesos generalmente implementan errno como una llamada de función (macro que se expande a una función) si está creando código roscado con los indicadores correctos.

Timo Geusch
fuente
@Timo, sí, está bien, consulte la discusión sobre otra respuesta y sepa si falta algo.
vinit dhatrak el
3

Podemos verificar ejecutando un programa simple en una 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);                                                                                       
}

Al ejecutar este programa, puede ver diferentes direcciones para errno en cada hilo. El resultado de una ejecución en mi máquina se parecía a:

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 

Tenga en cuenta que la dirección es diferente para todos los hilos.

Rajat Paliwal
fuente
Aunque buscarlo en una página de manual (o en SO) es más rápido, me gusta que hayas hecho tiempo para verificarlo. +1.
Bayou