这一篇,让我们搞懂 errno 的实现原理,不过为了防止名字冲突,我们换一个名字,叫 myerrno.
1. 思路
讲一下用到的技术吧:
- pthread once,参考 《只被执行一次的函数》
- 线程私有变量,参考 《线程私有变量》
本质上 errno 并不是一个真正意义上的变量,而是通过宏定义扩展为语句,而这一行语句实际上是在调用函数,该函数返回保存了指向 errno 变量的指针。
这里我们直接看程序就行了。
2. 程序清单
2.1 代码
// myerrno.c
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <pthread.h>
// 实际上 myerrno 就是一个宏定义
#define myerrno (*_myerrno())
pthread_key_t key;
pthread_once_t init_done = PTHREAD_ONCE_INIT;
// 使用 pthread once 对键进行初始化
void thread_init() {
puts("I'm thread_init");
pthread_key_create(&key, free); // 这里注册了析构函数就是 free
}
// 该函数用来获取真正的 myerrno 的地址
int *_myerrno() {
int *p;
pthread_once(&init_done, thread_init);
// 如果根据键拿到的是一个空地址,说明之前还未分配内存
p = (int*) pthread_getspecific(key);
if (p == NULL) {
p = (int*)malloc(sizeof(int));
pthread_setspecific(key, (void*)p);
}
/**************************************/
return p;
}
void* fun1() {
errno = 5;
myerrno = 5; // 这一行被扩展成 (*_myerrno()) = 5
sleep(1);
// printf 后面的 myerrno 会被扩展成 (*_myerrno())
printf("fun1: errno = %d, myerrno = %d\n", errno, myerrno);
return NULL;
}
void* fun2() {
errno = 10;
myerrno = 10; // 这一行被扩展成 (*_myerrno()) = 10
printf("fun2: errno = %d, myerrno = %d\n", errno, myerrno);
return NULL;
}
int main() {
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, fun1, NULL);
pthread_create(&tid2, NULL, fun2, NULL);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
return 0;
}
2.2 编译和运行
- 编译和运行
$ gcc myerrno.c -o myerrno -lpthread
$ ./myerrno
- 运行结果
图1 运行结果
图1 的结果也正是我们期望的。上面的程序也很容易读懂,关键技术在于宏定义上的一个小 trick,这里就不再分析了。
再看一下 bits/errno.h 中的 errno 就知道,它也是这么做的:
图2 errno 变量,在多线程环境中就是一个宏定义
图 2 中的 __errno_location 函数就相当于我们程序的 _myerrno 函数。
3. 总结
- 理解 errno 并不是真正意义上的变量