本文同時發表在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 <__errno_location@plt>
從@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;
}
參考資料: