思考一个问题,每个系统调用失败后都会设置 errno,如果在多线程程序中,不同线程中的系统调用设置的 errno 会不会互相干扰呢?
如果 errno 是一个全局变量,答案是肯定的。如果真是这样的话,那系统调用的局限性也就太大了,总不能在每个系统调用之前都加锁保护吧。优秀的 Linux 肯定不会这么弱,那么,这个 errno 的问题又是怎么解决的呢?
根据 man 手册,要使用 errno,首先需要包含 errno.h 这个头文件。我们先看看 errno.h 里面有什么东西。
vim /usr/include/errno.h
执行以上代码,会发现该文件中有这样几行关键内容:
#include <bits/errno.h>
.......
#ifndef errno
extern int errno;
#endif
根据官方提供的代码注释,bits/errno.h 中应该有一个 errno 的宏定义。如果没有,则会在外部变量中寻找一个名为 errno 的整数,它自然也就成了全局整数。否则,这个 errno 只是一个 per-thread 变量,每个线程都会拷贝一份。
关于 per-thread 变量更详细的信息,我们会在后面的课程中介绍。现在,你只需知道,这个 errno,每个线程都会独立拷贝一份,所以在多线程程序中使用它是不会相互影响的。
实现原理
具体是怎么做到的呢?我们可以再打开 bits/errno.h 看一眼。
<bits/errno.h>
# ifndef __ASSEMBLER__
extern int *__errno_location (void) __THROW __attribute__ ((__const__));
# if !defined _LIBC || defined _LIBC_REENTRANT
# define errno (*__errno_location ())
# endif
#endif
原来,当 libc 被定义为可重入时,errno 就会被定义成一个宏,该宏调用外部 __errno_location 函数返回的内存地址中所存储的值。在 GCC 源码中,我们还发现一个测试用例中定义了 __errno_location 函数的 Stub,是这样写的:
extern __thread int __libc_errno __attribute__ ((tls_model ("initial-exec")));
int * __errno_location (void)
{
return &__libc_errno;
}
这一简单的测试用例充分展现了 errno 的实现原理。errno 被定义为 per-thread(用 __thread 标识的线程局部存储类型)变量 __libc_errno,之后 __errno_location 函数返回了这个线程局部变量的地址。所以,在每个线程中获取和设置 errno 的时候,操作的是本线程内的一个变量,不会与其他线程相互干扰。
至于 __thread 这个关键字,需要在很“严苛”的条件下才能生效——需要 Linux 2.6 以上内核、pthreads 库、GCC 3.3 或更高版本的支持。不过,放到今天,这些条件已成为标配,也就不算什么了。