1. 程式人生 > >C errno是否是執行緒安全的

C errno是否是執行緒安全的

本文同時發表在https://github.com/zhangyachen/zhangyachen.github.io/issues/138

在使用多執行緒時,遇到了一個問題:執行緒例程中如果需要使用errno全域性變數,如何保證errno的執行緒安全性?例如一個簡單的執行緒池程式碼:

for(int i=0;i<THREADNUM;i++){
    pthread_create(&pid,NULL,start_routine,NULL);
}

while(1){
    connfd = accept(listenfd,(struct sockaddr *)&clientaddr,&clientlen);
    sbuf_insert(&buf,connfd);      //put connfd into pool
}

void *start_routine(void *argv){
    int connfd;
    int p = pthread_detach(pthread_self());
    while(1){
        connfd = sbuf_remove(&buf);        //thread get connfd
        doit(connfd);         //what if doit set global variable errno 
        close(connfd);
    }

    return NULL;
}

關於C中錯誤處理的問題,可以參考Error Handling in C programs,簡單的說很多系統呼叫只會返回成功或者失敗,具體失敗的原因會設定全域性變數errno供呼叫方自己讀取,所以引發了多執行緒裡errno執行緒安全的問題。
如何解決這個問題?畢竟設定errno的過程我們不能干預。上網搜了才發現,在POSIX標準中,重定義了errno,使之為執行緒安全的變數:

Redefinition of errno
In POSIX.1, errno is defined as an external global variable. But this definition is unacceptable in a multithreaded environment, because its use can result in nondeterministic results. The problem is that two or more threads can encounter errors, all causing the same errno to be set. Under these circumstances, a thread might end up checking errno after it has already been updated by another thread.
To circumvent the resulting nondeterminism, POSIX.1c redefines errno as a service that can access the per-thread error number as follows (ISO/IEC 9945:1-1996, §2.4):
Some functions may provide the error number in a variable accessed through the symbol errno. The symbol errno is defined by including the header <errno.h>, as specified by the C Standard ... For each thread of a process, the value of errno shall not be affected by function calls or assignments to errno by other threads.

顛覆了我的世界觀呀,那這怎麼實現的全域性變數能夠執行緒安全呢?

在error.h中(vim下按gf跳到庫檔案),看到如下定義:

/* Declare the `errno' variable, unless it's defined as a macro by
   bits/errno.h.  This is the case in GNU, where it is a per-thread
   variable.  This redeclaration using the macro still works, but it
   will be a function declaration without a prototype and may trigger
   a -Wstrict-prototypes warning.  */
#ifndef errno
extern int errno;
#endif

如果bits/error.h中沒有定義errno,才會定義errno。

在bits/errno.h中,找到關於errno定義部分:

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

可以清晰的看到,bits/errno.h對errno進行了重定義。從 __attribute__ ((__const__))推測出__errno_location ()會返回與引數無關的與執行緒繫結的一個特定地址,應用層直接從該地址取出errno的。(關於__attribute__用法可以參考Using GNU C attribute)。但是上面使用了條件編譯,也就是有兩種方法可以使得gcc重定義errno:

  • 不定義巨集_LIBC
  • 定義巨集_LIBC_REENTRANT

但是很有意思的是,我們在編譯時,壓根不能設定_LIBC,感興趣的可以自己試一下:

gcc -D_LIBC a.c
In file included from /usr/include/gnu/stubs.h:9:0,
                 from /usr/include/features.h:385,
                 from /usr/include/stdio.h:28,
                 from a.c:1:
/usr/include/gnu/stubs-64.h:7:3: error: #error Applications may not define the macro _LIBC
  #error Applications may not define the macro _LIBC
   ^

我們在編譯時設定了巨集_LIBC,但是編譯失敗,原來在gnu/stubs-64.h中會檢測,如果有_LIBC巨集定義,直接報錯終止預編譯:

#ifdef _LIBC
 #error Applications may not define the macro _LIBC
#endif

也就是在正常情況下,我們使用gcc編譯的程式,全域性變數errno一定是執行緒安全的。現在只剩下了一個問題,__errno_location是怎麼實現的。遺憾的是,我並沒有找到這個函式的實現,我們可以寫個小程式反彙編看一下在哪實現的:

#include <stdio.h>
#include <errno.h>

int main(){

    int i = errno;
    printf("%d",i);

    return 0;
}

反彙編程式碼:

0x0000000000400588 in main ()
=> 0x0000000000400588 <main+8>: e8 7b fe ff ff  callq  0x400408 <[email protected]>

從@plt看出應該是動態庫延遲繫結,進去看看:

0x0000003dc8e148c0 in _dl_runtime_resolve () from /lib64/ld-linux-x86-64.so.2
=> 0x0000003dc8e148c0 <_dl_runtime_resolve+0>:  48 83 ec 38     sub    $0x38,%rsp

呃呃,難道__errno_location定義在linux程式碼中?算了不看了,到這已經基本解決了我的問題:多執行緒如何保證errno全域性變數的執行緒安全性,哈哈。猜測實現方式應該跟thread-local有關。

最後,雖然在多執行緒中我們不用保證errno的執行緒安全,但是如果需要編寫訊號處理函式時,我們仍然要保證errno的安全性,因為作業系統可能不會新建立一個執行緒來處理訊號處理函式:

void handle_signal(int sig){
    int savedErrno;
    savedErrno = errno;
    /* Do something when recevied this sig */

    errno = savedErrno;
}

參考資料: