Errno потокобезопасный?

176

В errno.h, эта переменная объявлена ​​так, extern int errno;поэтому мой вопрос: безопасно ли проверять errnoзначение после некоторых вызовов или использовать perror () в многопоточном коде. Это потокобезопасная переменная? Если нет, то какая альтернатива?

Я использую Linux с GCC на архитектуре x86.

Винит Дхатрак
источник
5
2 точных противоположных ответа, интересно !! ;)
Винит Дхатрак
Хех, перепроверь мой ответ. Я добавил демонстрацию, которая, вероятно, будет убедительной. :-)
DigitalRoss

Ответы:

177

Да, это потокобезопасно. В Linux глобальная переменная errno зависит от потока. POSIX требует, чтобы errno был безопасен для потоков.

См. Http://www.unix.org/whitepapers/reentrant.html.

В POSIX.1 errno определяется как внешняя глобальная переменная. Но это определение неприемлемо в многопоточной среде, потому что его использование может привести к недетерминированным результатам. Проблема состоит в том, что два или более потоков могут столкнуться с ошибками, и все они вызывают одинаковые ошибки. При таких обстоятельствах поток может закончить проверку errno после того, как он уже был обновлен другим потоком.

Чтобы обойти результирующий недетерминизм, POSIX.1c переопределяет errno как сервис, который может получить доступ к номеру ошибки для каждого потока следующим образом (ISO / IEC 9945: 1-1996, §2.4):

Некоторые функции могут предоставлять номер ошибки в переменной, доступ к которой осуществляется через символ errno. Символ errno определяется путем включения заголовка, как указано в стандарте C ... Для каждого потока процесса на значение errno не должны влиять вызовы функций или назначения errno другими потоками.

Также см. Http://linux.die.net/man/3/errno

errno является локальным для потока; установка его в одном потоке не влияет на его значение в любом другом потоке.

Чарльз Сальвия
источник
9
В самом деле? Когда они это сделали? Когда я занимался программированием на C, доверие к errno было большой проблемой.
Пол Томблин
7
Чувак, это избавило бы меня от многих хлопот в мой день.
Пол Томблин
4
@vinit: errno определено в битах / errno.h. Прочитайте комментарии во включаемом файле. В нем говорится: «Объявите переменную« errno », если она не определена как макрос с помощью bits / errno.h. Это имеет место в GNU, где это переменная для каждого потока. Это переопределение с помощью макроса все еще работает, но это будет объявлением функции без прототипа и может вызвать предупреждение -Wstrict-prototypes. "
Чарльз Сальвиа
2
Если вы используете Linux 2.6, вам не нужно ничего делать. Просто начните программировать. :-)
Чарльз Сальвиа
3
@vinit dhatrak Должно быть # if !defined _LIBC || defined _LIBC_REENTRANT, _LIBC не определяется при компиляции обычных программ. В любом случае, запустите echo #include <errno.h>' | gcc -E -dM -xc - и посмотрите на разницу с и без -pthread. errno #define errno (*__errno_location ())в обоих случаях.
NOS
58

да


Errno больше не простая переменная, это нечто сложное за кулисами, специально для того, чтобы оно было поточно-ориентированным.

Смотрите $ 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.

Мы можем перепроверить:

$ cat > test.c
#include <errno.h>
f() { g(errno); }
$ cc -E test.c | grep ^f
f() { g((*__errno_location ())); }
$ 
DigitalRoss
источник
12

В errno.h эта переменная объявлена ​​как extern int errno;

Вот что говорит стандарт C:

Макрос errnoне обязательно должен быть идентификатором объекта. Он может расширяться до модифицируемого lvalue, получаемого в результате вызова функции (например, *errno()).

Обычно errnoэто макрос, который вызывает функцию, возвращающую адрес номера ошибки для текущего потока, а затем разыменовывает его.

Вот что у меня есть в Linux, в /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

В конце концов, он генерирует такой код:

> 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

Во многих системах Unix компиляция с -D_REENTRANTгарантирует, что errnoэто потокобезопасно.

Например:

#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
Я думаю, вам не нужно явно компилировать код -D_REENTRANT. Пожалуйста, обратитесь к обсуждению другого ответа на тот же вопрос.
Винит Дхатрак
3
@Vinit: это зависит от вашей платформы - в Linux вы можете быть правы; в Solaris вы будете правы, только если для _POSIX_C_SOURCE установлено значение 199506 или более позднюю версию - возможно, с помощью -D_XOPEN_SOURCE=500или -D_XOPEN_SOURCE=600. Не каждый заботится о том, чтобы среда POSIX была указана, а затем -D_REENTRANTможет сохранить бекон. Но вы все равно должны быть осторожны - на каждой платформе - чтобы гарантировать, что вы получите желаемое поведение.
Джонатан Леффлер
Есть ли документация, в которой указано, какой стандарт (например: C99, ANSI и т. Д.) Или, по крайней мере, какие компиляторы (например, версия GCC и более поздние), поддерживающие эту функцию, и является ли это по умолчанию? Спасибо.
Облако
Вы можете посмотреть на стандарт C11 или на POSIX 2008 (2013) для errno . Стандарт C11 гласит: ... и errnoкоторый расширяется до модифицируемого lvalue (201), который имеет тип intи продолжительность локального хранилища потока, значение которого устанавливается в положительное число ошибки несколькими библиотечными функциями. Если определение макроса подавлено, чтобы получить доступ к реальному объекту, или программа определяет идентификатор с именем errno, поведение не определено. [... продолжение ...]
Джонатан Леффлер
[... продолжение ...] Сноска 201 гласит: макрос errnoне должен быть идентификатором объекта. Он может расширяться до модифицируемого lvalue, получаемого в результате вызова функции (например, *errno()). Основной текст продолжается: значение errno в начальном потоке равно нулю при запуске программы (начальное значение errno в других потоках является неопределенным значением), но никогда не устанавливается равным нулю какой-либо библиотечной функцией. POSIX использует стандарт C99, который не распознает потоки. [... также продолжение ...]
Джонатан Леффлер
10

Это из <sys/errno.h>моего Mac:

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

Так errnoчто теперь функция __error(). Функция реализована так, чтобы быть поточно-ориентированной.

vy32
источник
9

да , как объясняется на странице руководства errno и в других ответах, errno является локальной переменной потока.

Однако есть глупая деталь, о которой можно легко забыть. Программы должны сохранять и восстанавливать ошибки в любом обработчике сигналов, выполняющем системный вызов. Это связано с тем, что сигнал будет обрабатываться одним из потоков процесса, который может перезаписать его значение.

Поэтому обработчики сигналов должны сохранять и восстанавливать ошибки. Что-то вроде:

void sig_alarm(int signo)
{
 int errno_save;

 errno_save = errno;

 //whatever with a system call

 errno = errno_save;
}
marcmagransdeabril
источник
запрещено → забыто, я полагаю. Можете ли вы предоставить ссылку на этот системный вызов сохранить / восстановить детали?
Крейг МакКуин
Привет Крейг, спасибо за информацию о опечатке, теперь исправлено. Что касается другого вопроса, я не уверен, правильно ли я понимаю, что вы просите. Любой вызов, который изменяет errno в обработчике сигнала, может помешать errno, используемому тем же потоком, который был прерван (например, используя strtol внутри sig_alarm). право?
marcmagransdeabril
6

Я думаю, что ответ "это зависит". Поточно-ориентированные библиотеки времени выполнения C обычно реализуют errno как вызов функции (расширение макроса до функции), если вы создаете многопоточный код с правильными флагами.

Тимо Гойш
источник
@ Тимо, да, ты прав, пожалуйста, обратитесь к обсуждению другого ответа и дайте знать, если что-то отсутствует
Винит Дхатрак
3

Мы можем проверить, запустив простую программу на машине.

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

Запуск этой программы, и вы можете увидеть разные адреса для errno в каждом потоке. Вывод пробега на моей машине выглядел так:

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 

Обратите внимание, что адрес отличается для всех потоков.

Раджат Паливал
источник
Хотя поиск на странице руководства (или на SO) происходит быстрее, мне нравится, что вы нашли время, чтобы проверить это. +1.
Байу