【问题标题】:Is errno thread-safe?errno 是线程安全的吗?
【发布时间】:2010-12-14 05:30:44
【问题描述】:

errno.h 中,这个变量被声明为extern int errno;,所以我的问题是,在一些调用之后检查errno 值是否安全,或者在多线程代码中使用perror()。这是一个线程安全变量吗?如果没有,那还有什么替代方案?

我在 x86 架构上使用带有 gcc 的 linux。

【问题讨论】:

标签: c linux multithreading gcc


【解决方案1】:

是的,它是线程安全的。在 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 标准 ... 对于 a 的每个线程 过程中,errno 的值不得 受函数调用的影响或 其他线程对 errno 的赋值。

另见http://linux.die.net/man/3/errno

errno 是线程本地的;在一个线程中设置它不会影响它在任何其他线程中的值。

【讨论】:

  • 真的吗?他们什么时候这样做的?当我在做 C 编程时,信任 errno 是个大问题。
  • @vinit: errno 实际上是在 bits/errno.h 中定义的。读取包含文件中的 cmets。它说:“声明 `errno' 变量,除非它被 bits/errno.h 定义为宏。在 GNU 中就是这种情况,它是每个线程的变量。使用宏的重新声明仍然有效,但它将是一个没有原型的函数声明,可能会触发 -Wstrict-prototypes 警告。”
  • @vinit,它已经为你声明了。你不需要做任何事情。 extern int errno 变量声明包含在 #ifndef errno 条件中,这将是错误的,因为 errno 已在 bits/errno.h 中定义为宏。
  • 如果您使用的是 Linux 2.6,则无需执行任何操作。刚开始编程。 :-)
  • @vinit dhatrak 应该有# if !defined _LIBC || defined _LIBC_REENTRANT,编译普通程序时没有定义_LIBC。无论如何,运行 echo #include <errno.h>' | gcc -E -dM -xc - 并查看使用和不使用 -pthread 的区别。在这两种情况下,errno 都是 #define errno (*__errno_location ())
【解决方案2】:

是的


Errno 不再是一个简单的变量,它在幕后是复杂的,特别是它是线程安全的。

$ man 3 errno:

ERRNO(3)                   Linux Programmer’s 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 ())); }
$ 

【讨论】:

    【解决方案3】:

    在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
    

    【讨论】:

      【解决方案4】:

      是的,正如errno man page 和其他回复中所解释的,errno 是线程局部变量。

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

      因此,信号处理程序应该保存和恢复 errno。比如:

      void sig_alarm(int signo)
      {
       int errno_save;
      
       errno_save = errno;
      
       //whatever with a system call
      
       errno = errno_save;
      }
      

      【讨论】:

      • 禁止→忘记了我想。您能否提供此系统调用保存/恢复详细信息的参考?
      • 嗨 Craig,感谢您提供有关错字的信息,现在已更正。关于另一个问题,我不确定我是否正确理解您的要求。在信号处理程序中修改 errno 的任何调用都可能干扰被中断的同一线程使用的 errno(例如,在 sig_alarm 中使用 strtol)。对吗?
      【解决方案5】:

      这是来自我 Mac 上的 &lt;sys/errno.h&gt;

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

      所以errno 现在是一个函数__error()。该函数被实现为线程安全的。

      【讨论】:

        【解决方案6】:

        我们可以通过在机器上运行一个简单的程序来检查。

        #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。
        【解决方案7】:

        在许多 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) */
        

        【讨论】:

        • 我猜您不必使用-D_REENTRANT 显式编译代码。请参阅有关同一问题的其他答案的讨论。
        • @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 扩展为具有类型 int 和线程本地存储持续时间的可修改左值 (201),其值由多个库设置为正错误号职能。如果为了访问实际对象而禁止宏定义,或者程序定义名称为 errno 的标识符,则行为未定义。 [...继续...]
        • [...continuation...] 脚注 201 说:errno 不必是对象的标识符。它可能会扩展为函数调用产生的可修改左值(例如,*errno())。 正文继续:程序启动时初始线程中 errno 的值为零(初始值其他线程中的 errno 是一个不确定的值),但任何库函数都不会将其设置为零。 POSIX 使用不识别线程的 C99 标准。 [...还继续...]
        【解决方案8】:

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

        【讨论】:

        • @Timo,是的,你说得对,请参阅其他答案的讨论,如果有任何遗漏,请告知。
        猜你喜欢
        • 2011-09-18
        • 2021-10-12
        • 2015-04-18
        • 2011-10-07
        • 2012-03-02
        • 2011-10-28
        • 2023-03-14
        • 2016-08-24
        相关资源
        最近更新 更多