Чи безпечно для ниток errno?


175

У errno.hцій змінній оголошено extern int errno;так, як моє питання, чи безпечно перевірити errnoзначення після деяких дзвінків або використовувати perror () у багатопотоковому коді. Це безпечна змінна нитка? Якщо ні, то яка альтернатива?

Я використовую Linux з gcc на архітектурі x86.


5
2 точні протилежні відповіді, цікаво !! ;)
vinit dhatrak

Хе, перевірте мою відповідь. Я додав демонстрацію, яка, ймовірно, буде переконливою. :-)
DigitalRoss

Відповіді:


176

Так, це безпечно для ниток. У Linux глобальна змінна errno є потоковою. POSIX вимагає, щоб errno була безпечною для потоків.

Дивіться http://www.unix.org/whitepapers/reentrant.html

У POSIX.1 errno визначається як зовнішня глобальна змінна. Але це визначення є неприйнятним у багатопотоковому середовищі, оскільки його використання може призвести до недетермінованих результатів. Проблема полягає в тому, що два або більше потоків можуть зіткнутися з помилками, що спричинить встановлення одного і того ж 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
Дійсно? Коли вони це зробили? Коли я займався програмуванням на С, довіра errno була великою проблемою.
Пол Томблін

7
Людина, це врятувало б багато клопоту в моєму дні.
Пол Томблін

4
@vinit: errno насправді визначено у бітах / errno.h. Прочитайте коментарі у файлі включення. У ній написано: "Оголосити змінну errno", якщо вона не визначена як макрос бітами / errno.h. Це так у GNU, де це змінна за ниткою. Ця повторна декларація за допомогою макросу все ще працює, але вона буде декларацією функції без прототипу і може викликати попередження -Wstrict-prototypes. "
Чарльз Сальвія

2
Якщо ви використовуєте Linux 2.6, вам нічого не потрібно робити. Просто почніть програмування. :-)
Чарльз Сальвія

3
@vinit dhatrak Має бути # if !defined _LIBC || defined _LIBC_REENTRANT, _LIBC не визначено під час компіляції звичайних програм. У будь-якому випадку, запустіть ехо #include <errno.h>' | gcc -E -dM -xc - і подивіться на різницю з та без-вперед. errno є #define errno (*__errno_location ())в обох випадках.
нос

58

Так


Еррно вже не є простою змінною, це щось складне за кадром, спеціально для того, щоб бути безпечним для потоків.

Дивіться $ 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 ())); }
$ 

12

У errno.h ця змінна оголошується як extern int errno;

Ось що говорить стандарт C:

Макрос errnoне повинен бути ідентифікатором об'єкта. Він може розширитися до зміни значення, що змінюється в результаті виклику функції (наприклад, *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. Будь ласка, зверніться до обговорення іншої відповіді на те саме питання.
vinit dhatrak

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який розширюється до модифікованого значення (201), що має intтривалість локального зберігання типу і потоку, значення якого встановлюється на позитивне число помилок декількома функціями бібліотеки. Якщо визначення макросу придушено для доступу до фактичного об'єкта, або програма визначає ідентифікатор з іменем errno, поведінка не визначена. [... продовження ...]
Джонатан Леффлер

[... продовження ...] Зноска 201 говорить: Макрос errnoне повинен бути ідентифікатором об'єкта. Він може розширитися до зміни значення, що змінюється в результаті виклику функції (наприклад, *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(). Функція реалізована так, щоб бути безпечною для потоків.


9

так , як це пояснено на сторінці errno man та інших відповідях, errno - це локальна змінна нитка.

Однак є дурна деталь, яку можна легко забути. Програми повинні зберігати та відновлювати помилку на будь-якому обробці сигналу, який виконує системний виклик. Це відбувається тому, що сигнал буде оброблятися одним з потоків процесу, який міг би замінити його значення.

Тому обробники сигналів повинні зберігати і відновлювати помилку. Щось на зразок:

void sig_alarm(int signo)
{
 int errno_save;

 errno_save = errno;

 //whatever with a system call

 errno = errno_save;
}

заборонений → забутий я припускаю. Чи можете ви надати посилання на деталі збереження / відновлення системного виклику?
Крейг МакКуїн

Привіт Крейг, спасибі за інформацію про друкарську помилку, тепер виправлено. Щодо іншого питання, я не впевнений, чи правильно я розумію, що ви просите. Незалежно від виклику, що модифікує errno в обробнику сигналу, це може заважати використанню errno тим самим потоком, який був перерваний (наприклад, використання strtol всередині sig_alarm). правильно?
marcmagransdeabril

6

Я думаю, що відповідь "це залежить". Безпечно захищені потоком C бібліотеки виконання зазвичай реалізують errno як виклик функції (макрос, що розширюється до функції), якщо ви будуєте потоковий код з правильними прапорами.


@Timo, так ур правильно, будь ласка, зверніться до обговорення іншої відповіді і дайте знати, якщо чогось немає.
vinit dhatrak

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 

Зауважте, що адреса різна для всіх потоків.


Хоча пошук його на сторінці "man" (або на SO) швидший, мені подобається, що ви встигли його перевірити. +1.
Bayou
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.