errno是线程安全的吗?


174

在中errno.h,此变量被声明为extern int errno;是我的问题,errno在某些调用之后检查值还是在多线程代码中使用perror()是安全的。这是线程安全变量吗?如果没有,那还有什么选择呢?

我在x86体系结构上将Linux与gcc一起使用。


5
2个完全相反的答案,很有趣!;)
vinit dhatrak 09年

嘿,重新检查我的答案。我添加了一个可能令人信服的演示。:-)
DigitalRoss

Answers:


176

是的,它是线程安全的。在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是一个大问题。
Paul Tomblin,2009年

7
伙计,那将为我节省很多麻烦。
Paul Tomblin,2009年

4
@vinit:errno实际上是在bits / errno.h中定义的。阅读包含文件中的注释。它说:“声明'errno'变量,除非它是由bits / errno.h定义为宏。在GNU中就是这种情况,它是每个线程的变量。使用宏的这种重新声明仍然有效,但是它将是没有原型的函数声明,并且可能会触发-Wstrict-prototypes警告。”
查尔斯·萨尔维亚

2
如果您使用的是Linux 2.6,则无需执行任何操作。刚开始编程。:-)
Charles Salvia

3
@vinit dhatrak # if !defined _LIBC || defined _LIBC_REENTRANT编译普通程序时,应该定义_LIBC。无论如何,运行echo #include <errno.h>' | gcc -E -dM -xc - 并查看使用-pthread和不使用-pthread的区别。#define errno (*__errno_location ())两种情况下都存在errno 。

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 ())); }
$ 

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系统上,使用with进行编译-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,2009年

3
@Vinit:这取决于您的平台-在Linux上,您可能是正确的;在Solaris上,只有将_POSIX_C_SOURCE设置为199506或更高版本(可能使用-D_XOPEN_SOURCE=500或),您才是正确的-D_XOPEN_SOURCE=600。并非每个人都为确保指定POSIX环境而烦恼-然后-D_REENTRANT可以保存您的培根。但是您仍然必须在每个平台上都小心谨慎,以确保获得所需的行为。
乔纳森·莱夫勒

是否有文档说明支持该功能的标准(即C99,ANSI等)或至少是哪些编译器(即GCC版本及更高版本),以及它是否是默认功能?谢谢。
2014年

您可以查看C11标准或errno的 POSIX 2008(2013)。C11标准说:...并且errno扩展为具有类型int和线程本地存储持续时间的可修改的左值(201),其值由多个库函数设置为正错误数。如果为了访问实际对象而抑制了宏定义,或者程序使用name定义了一个标识符,则errno该行为未定义。[...续...]
乔纳森·莱夫勒2014年

[...继续...]脚注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手册页和其他答复所述,errno是线程局部变量。

但是,有一个愚蠢的细节很容易被遗忘。程序应在执行系统调用的任何信号处理程序上保存和还原errno。这是因为该信号将由可能覆盖其值的进程线程之一处理。

因此,信号处理程序应保存并还原errno。就像是:

void sig_alarm(int signo)
{
 int errno_save;

 errno_save = errno;

 //whatever with a system call

 errno = errno_save;
}

禁止→忘记我想。您可以为此系统调用保存/恢复详细信息提供参考吗?
Craig McQueen

嗨,克雷格,感谢您提供有关错字的信息,现在已更正。关于其他问题,我不确定我是否正确理解您的要求。无论在信号处理程序中修改errno的任何调用都可能干扰被中断的同一线程使用的errno(例如,在sig_alarm中使用strtol)。对?
marcmagransdeabril

6

我认为答案是“取决于”。如果使用正确的标志构建线程代码,则线程安全的C运行时库通常将errno实现为函数调用(宏扩展为函数)。


@Timo,是的,请参考其他答案的讨论,并让我们知道是否缺少任何内容。
vinit dhatrak 09年

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。
Bayou
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.