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出栈并跳转.

待续......

相关文章
相关标签/搜索