博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
errno是否是thread safe的
阅读量:6819 次
发布时间:2019-06-26

本文共 4669 字,大约阅读时间需要 15 分钟。

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为例一探究竟.

先看对外暴露的stdlib/errno.h:

1 #include 
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):

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)用于拼接字符串生成对应的调用号(生成的即是内核定义的系统调用号的宏):

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

再回头看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出栈并跳转.

待续......

转载于:https://www.cnblogs.com/Five100Miles/p/8459193.html

你可能感兴趣的文章
Nginx+Keepalived(带Nginx监控脚本)
查看>>
我的友情链接
查看>>
利用SVN的post-commit钩子实现多项目自动同步
查看>>
linux 的ping 命令
查看>>
java基础
查看>>
反射之获取类,方法等
查看>>
TechEd 2012 微软技术大会简介
查看>>
ajax框架之DWR项目运行报错之org.apache.commons.logging.LogFactory
查看>>
终端市场消费减少
查看>>
鲜果CEO梁公军:Google Reader的用户是我们很看重的机会
查看>>
cocos2d-x3.0beta版+NDK-r9b在android上的启动过程
查看>>
基于Spring MVC+Spring JPA技术实战开发大型商业ERP项目教程
查看>>
黑马程序员_进程和线程的区别
查看>>
DHCP原理与实例
查看>>
类的虚继承
查看>>
MySQL批量删除指定前缀表
查看>>
JDK与TOMCAT安装
查看>>
将屏幕的全部输出存到文件 转
查看>>
postgresql学习笔记(五)备份与恢复
查看>>
从SCCM中创建并运行Powershell脚本卸载软件
查看>>