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出棧並跳轉.
待續......