errno 的实现
IEEE Std 1003.1-2024 (POSIX) 中对于 errno 的定义如下:
The lvalue to which the macro errno expands is used by many functions to return error values.
在更早期的 POSIX(Issue 5 及以前)以及 X/Open 文档中,曾经规定 errno
是一个外部变量(extern int errno),但这使得 errno 无法实现线程安全,因为所有线程共享同一个全局变量,
一个线程的系统调用返回的错误码会覆盖另一个线程的值。因此,POSIX Issue 6(即 SUSv3 / IEEE Std 1003.1-2001)
将这一要求删除,改为现在的定义:只要求 errno 是一个展开为 int 类型的可修改左值(modifiable lvalue)的宏。
这为实现者提供了足够的自由度,以支持线程安全的 errno。
ISO C 标准在 C90/C89 时期已经不再要求 errno 是外部变量。
FreeBSD 中的 errno 实现
FreeBSD 的 sys/sys/errno.h 即 /usr/include/sys/errno.h 中,errno 的定义是:
int * __error(void);
#define errno (* __error())即 errno 宏会调用 __error() 函数取得一个 int * 指针,然后对其解引用。
这意味着每次访问 errno 实际上都是一次函数调用,而通过控制 __error() 的行为,
就可以让不同线程获得各自独立的 errno 存储。
__error() 与函数指针间接调用
__error() 的实现位于 lib/libsys/__error.c,采用了函数指针间接调用的模式:
| |
默认情况下,__error_selector 函数指针指向 __error_unthreaded,
它简单地返回全局变量 __libsys_errno 的地址。这适用于单线程程序,
避免了与 TLS 相关的开销。
此外,libsys 通过 __sym_compat(errno, __libsys_errno, FBSD_1.0) 在 FBSD_1.0
符号版本下将 __libsys_errno 导出为 errno 符号。这是为了兼容那些直接引用 errno
变量(而非通过宏)的老旧二进制程序。但在现代代码中,应始终通过 <errno.h> 提供的宏来访问 errno。
libthr 的线程安全介入
当程序链接了 libthr(FreeBSD 的 POSIX 线程库)时,线程库需要将 errno
的实现替换为线程安全的版本。这一过程通过两个互补的机制实现。
机制一:构造函数与 __set_error_selector
libthr 中的 _thread_init_hack 函数标记了 __attribute__((constructor)),
因此会在库加载时由动态链接器自动调用。它的调用链如下:
_thread_init_hack()(constructor)调用_libpthread_init()_libpthread_init()调用__thr_interpose_libc()__thr_interpose_libc()调用__set_error_selector(__error_threaded)
这样就将 __error_selector 函数指针从 __error_unthreaded 替换为了 __error_threaded。
机制二:弱符号引用
libthr 的 thr_error.c 中还声明了:
__weak_reference(__error_threaded, __error);如此,在链接 libthr 时,会提供一个 __error_threaded 到 __error 的弱符号别名,
这提供了符号层面的一个兼容路径。
__error_threaded 的实现
线程安全版本的 errno 访问函数位于 lib/libthr/sys/thr_error.c:
| |
这个函数的逻辑是:
- 如果线程子系统已经初始化(
_thr_initial != NULL),并且当前线程不是初始线程(主线程), 则返回当前线程的pthread结构体中的error字段的地址。这是每个线程独立的存储。 - 否则,回退到全局的
__libsys_errno。
初始线程使用全局 __libsys_errno 而非 TLS 存储,这是因为主线程的 errno
需要在线程库完成初始化之前就可用,而且它也需要与那些不使用线程库的代码(如在 _libpthread_init
之前执行的初始化代码)共享同一个 errno 存储位置。
更广泛的介入机制
值得一提的是,__thr_interpose_libc() 不仅替换了 errno 的实现,还将一系列可能阻塞的系统调用
(如 read、write、accept、connect、select、poll、close、fork、sigaction 等)
替换为 libthr 提供的线程化版本。这些线程化版本会在调用实际系统调用前后处理取消点
(cancellation point)等线程相关的语义,确保整个 C 运行环境在多线程下正确工作。