errno是不是thread safe的

errno是線程安全的嗎? 假設有A, B兩個線程都執行系統調用, 其中A返回EIO, B返回EAGAIN, 在判斷返回值時是否會引發混淆?
簡單的經過man errno就能夠獲取答案: errno is thread-local; setting it in one thread does not affect its value in any other thread.
可是errno到底是如何實現的? 爲何errno還多是一個宏? 帶着疑問咱們來研究下glibc. 官網下載到最新的是2.25版本的源碼, 咱們就以glibc-2.25爲例一探究竟. linux

先看對外暴露的stdlib/errno.h: 緩存

1 #include <bits/errno.h> 2 #undef __need_Emath 3 #ifndef errno 4 extern int errno; 
5 #endif

 

這裏的註釋指明兩點:
1. bits/errno.h是系統相關頭文件, 在該文件中會測試__need_Emath與_ERRNO_H宏.
2. 若是bits/errno.h未定義errno爲宏則聲明外部變量errno. 安全

sysdeps/unix/sysv/linux/bits/errno.h中將其定義爲函數: 架構

1 #ifdef _ERRNO_H 2 # ifndef __ASSEMBLER__ 3 extern int *__errno_location (void) __THROW __attribute__ ((__const__)); 
4 #  if !defined _LIBC || defined _LIBC_REENTRANT 5 #  define errno (*__errno_location ()) 6 #  endif 7 # endif 8 #endif

 

搞清楚errno的定義後再來看看errno的修改. 因爲不一樣架構系統調用部分相同部分不一樣, glibc使用腳原本動態生成系統調用函數的封裝, sysdeps/unix/make-syscalls.sh即生成函數封裝的腳本. 它會先去讀取syscalls.list保存在calls變量中, 經過sed將註釋行與空行刪除, 將獲得的文件按行輸入(讀入的前三個參數分別爲file caller rest)並判斷對應架構目錄下是否存在$file.c $file.S $caller.c $caller.S(若是$caller不爲-)文件中一個, 若是有則記錄在calls變量中. 接下來根據系統調用類型及參數配置不一樣參數, 最後將其輸出, 注意line 256開始的宏定義與包含的文件. 此處有點不明白, 輸出的文件是怎麼肯定的?
make-syscalls.sh腳本輸出的信息有何做用? 上文中line 256能夠解答這個問題. 先來看下系統調用的模板(defined in sysdeps/unix/syscall-template.S): 函數

1 #define T_PSEUDO(SYMBOL, NAME, N) PSEUDO (SYMBOL, NAME, N) 2 #define T_PSEUDO_END(SYMBOL) PSEUDO_END (SYMBOL) 3 T_PSEUDO (SYSCALL_SYMBOL, SYSCALL_NAME, SYSCALL_NARGS) 4     ret 
5 T_PSEUDO_END (SYSCALL_SYMBOL)

 

syscall-template.S定義了一組宏用於定義系統調用的接口, 這裏僅分析最多見的狀況.
看下以PSEUDO開頭命名的宏(defined in sysdeps/unix/sysv/linux/arm/sysdep.h): 測試

 1 #undef PSEUDO  2 #define PSEUDO(name, syscall_name, args)    \  3     .text;                                  \ 
 4 ENTRY (name);                               \ 
 5     DO_CALL (syscall_name, args);           \ 
 6     cmn r0, $4096; 
 7 #undef PSEUDO_END  8 #define PSEUDO_END(name)                    \  9     SYSCALL_ERROR_HANDLER;                  \ 
10 END (name)

 

因以PSEUDO開頭命名的宏較多, 此處僅分析下PSEUDO與PSEUDO_END, 可見兩個宏需成對使用, 分別用於系統調用與錯誤返回, 繼續分析DO_CALL(defined in sysdeps/unix/sysv/linux/arm/sysdep.h): spa

1 #undef DO_CALL 2 #define DO_CALL(syscall_name, args)         \ 3     DOARGS_##args;                          \ 
4     ldr r7, =SYS_ify (syscall_name);        \ 
5     swi 0x0;                                \ 
6     UNDOARGS_##args

 

DO_CALL宏有三條註釋, 分別說明:
1. ARM EABI用戶接口將系統調用號放在R7中, 而非swi中傳遞. 這種方式更加高效, 由於內核無需從內存中獲取調用號, 這對於指令cache與數據cache分開的架構比較麻煩. 所以swi中必須傳遞0.
2. 內核經過R0-R6共傳遞7個參數, 而編譯器一般只使用4個參數寄存器其他以入棧方式傳參(見AAPCS), 此處須要作轉換防止棧幀毀壞並保證內核正確獲取參數.
3. 因爲緩存系統調用號在發生系統調用時必須保存並恢復R7.
根據註釋理解代碼就方便多了, 先保存R7並將參數傳遞給對應寄存器, 將系統調用號傳遞給R7並調用swi 0x0, 最後恢復寄存器. DOARGS_#args根據傳入args值不一樣展開爲不一樣的宏(都定義在同一文件下), 此處僅分析DOARGS_7狀況(UNDOARGS_#args相似, 不展開分析): 線程

 1 #undef  DOARGS_7  2 #define DOARGS_7                            \  3     .fnstart;                               \ 
 4     mov ip, sp;                             \ 
 5     push {r4, r5, r6, r7};                  \ 
 6     cfi_adjust_cfa_offset (16);             \ 
 7     cfi_rel_offset (r4, 0);                 \ 
 8     cfi_rel_offset (r5, 4);                 \ 
 9     cfi_rel_offset (r6, 8);                 \ 
10     cfi_rel_offset (r7, 12);                \ 
11     .save { r4, r5, r6, r7 };               \ 
12     ldmia ip, {r4, r5, r6}

 

先將當前棧指針保存在IP中, 將R4-R7依次入棧, 最後經過IP將已經入棧的參數傳遞給R4-R7. 中間以cfi開頭的宏都是僞指令(defined in sysdeps/generic/sysdep.h), 用於debugger分析程序調用間寄存器狀態, 不詳細分析了, 具體可參見(http://dwarfstd.org/doc/DWARF5.pdf).
SYS_ify宏(defined in sysdeps/unix/sysv/linux/arm/sysdep.h)用於拼接字符串生成對應的調用號(生成的便是內核定義的系統調用號的宏): debug

#define SYS_ify(syscall_name) (__NR_##syscall_name) unix

再回頭看PSEUDO_END, 其展開即調用SYSCALL_ERROR_HANDLER(sysdeps/unix/sysv/linux/arm/sysdep.h)而後聲明函數結束. SYSCALL_ERROR_HANDLER根據不一樣預處理宏有不一樣定義, 此處僅分析使用libc的errno且架構不支持THUMB_INTERWORK狀況:

 1 #define SYSCALL_ERROR_HANDLER               \  2 __local_syscall_error:                      \  3     push { lr };                            \ 
 4     cfi_adjust_cfa_offset (4);              \ 
 5     cfi_rel_offset (lr, 0);                 \ 
 6     push { r0 };                            \ 
 7     cfi_adjust_cfa_offset (4);              \ 
 8     bl PLTJMP(C_SYMBOL_NAME(__errno_location)); \ 
 9     pop { r1 };                             \ 
10     cfi_adjust_cfa_offset (-4);             \ 
11     rsb r1, r1, #0;                         \ 
12     str r1, [r0];                           \ 
13     mvn r0, #0;                             \ 
14     POP_PC;

 

代碼仍是比較簡單的, 首先將LR壓棧, 再將R0壓棧(注意此時R0爲系統調用返回值). 而後獲取errno的地址, PLTJMP宏代表__errno_location符號是由程序連接表指定而非靜態生成的. 因爲函數返回值保存在R0, 出棧時使用R1保存系統調用返回值, 又系統調用返回值爲複數, 此處再作一次減法取正, 再將其保存在R0給定的地址上(errno). 最後將R0設置爲-1, 將LR出棧並跳轉.

待續......

相關文章
相關標籤/搜索