errno 的实现

• 本文约 1554 字,阅读大致需要 4 分钟 | Development | #FreeBSD | #POSIX | #C | #Threading

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 实现

FreeBSDsys/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,采用了函数指针间接调用的模式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/* 单线程程序的 errno 存储;对更早的程序而言,这是 errno@FBSD_1.0 */
int __libsys_errno;

/* 单线程程序的左值指针获取函数 */
static int *
__error_unthreaded(void)
{
	return (&__libsys_errno);
}

/* 左值指针获取函数的默认值(__error() 会用到) */
static int *(*__error_selector)(void) = __error_unthreaded;

/* 设置左值指针获取函数的方法,用于 libthr 介入操作 */
void
__set_error_selector(int *(*arg)(void))
{
	__error_selector = arg;
}

/* 用来实现 errno API */
int *
__error(void)
{
	return (__error_selector());
}

默认情况下,__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)), 因此会在库加载时由动态链接器自动调用。它的调用链如下:

  1. _thread_init_hack()(constructor)调用 _libpthread_init()
  2. _libpthread_init() 调用 __thr_interpose_libc()
  3. __thr_interpose_libc() 调用 __set_error_selector(__error_threaded)

这样就将 __error_selector 函数指针从 __error_unthreaded 替换为了 __error_threaded

机制二:弱符号引用

libthrthr_error.c 中还声明了:

__weak_reference(__error_threaded, __error);

如此,在链接 libthr 时,会提供一个 __error_threaded__error 的弱符号别名, 这提供了符号层面的一个兼容路径。

__error_threaded 的实现

线程安全版本的 errno 访问函数位于 lib/libthr/sys/thr_error.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
int *
__error_threaded(void)
{
	struct pthread *curthread;

	if (_thr_initial != NULL) {
		curthread = _get_curthread();
		if (curthread != NULL && curthread != _thr_initial)
			return (&curthread->error);
	}
	return (&__libsys_errno);
}

这个函数的逻辑是:

  • 如果线程子系统已经初始化(_thr_initial != NULL),并且当前线程不是初始线程(主线程), 则返回当前线程的 pthread 结构体中的 error 字段的地址。这是每个线程独立的存储。
  • 否则,回退到全局的 __libsys_errno

初始线程使用全局 __libsys_errno 而非 TLS 存储,这是因为主线程的 errno 需要在线程库完成初始化之前就可用,而且它也需要与那些不使用线程库的代码(如在 _libpthread_init 之前执行的初始化代码)共享同一个 errno 存储位置。

更广泛的介入机制

值得一提的是,__thr_interpose_libc() 不仅替换了 errno 的实现,还将一系列可能阻塞的系统调用 (如 readwriteacceptconnectselectpollcloseforksigaction 等) 替换为 libthr 提供的线程化版本。这些线程化版本会在调用实际系统调用前后处理取消点 (cancellation point)等线程相关的语义,确保整个 C 运行环境在多线程下正确工作。