3、errno全局变量:工作原理、线程安全与正确使用
说到C语言的错误处理,errno 是个绕不开的话题。我个人觉得,它就像C标准库给我们留的一个「小纸条」——函数执行出错了,就往这个纸条上写个错误码。但这个小纸条用起来有不少讲究,搞不好就会踩坑。
3.1 errno 的工作原理
errno 其实就是一个全局整型变量。很多标准库函数在出错时,会设置 errno 的值。比如你打开一个不存在的文件,fopen() 就会把 errno 设为 ENOENT(2)。
它的工作流程很简单:
- 函数内部检测到错误
- 将错误码写入 errno
- 函数返回错误指示(如 NULL、-1)
- 调用方检查返回值,再查看 errno 获取具体错误
关键点:errno 只在函数返回错误时才有意义。如果函数执行成功,errno 的值不会被清零。所以正确的做法是:先判断返回值,再查看 errno。
看个例子:
#include <stdio.h>
#include <errno.h>
int main() {
FILE *fp = fopen("nonexist.txt", "r");
if (fp == NULL) {
// 只有这里才能安全地查看 errno
printf("errno = %d\n", errno);
}
return 0;
}
嗯,这里要注意:千万别在函数成功时去读 errno。我在项目中见过有人这么写:
// 错误的做法
FILE *fp = fopen("test.txt", "r");
if (errno == ENOENT) { // 如果文件存在,errno 可能是上次遗留的值
printf("文件不存在\n");
}
这种代码就是定时炸弹。你想想看,如果前面某个函数把 errno 设成了 ENOENT,然后 fopen() 成功了,errno 没被修改,你的程序就会误判。
3.2 线程安全性问题
说到 errno 的线程安全性,我得先讲个我踩过的坑。几年前做一个多线程的数据采集项目,两个线程同时调用 socket 函数,结果错误码全乱套了。A 线程的 connect 失败了,读到的 errno 却是 B 线程的 EAGAIN。
为什么会这样?因为传统的 errno 是全局变量。多线程环境下,一个线程设置的 errno 可能被另一个线程覆盖。
解决方案其实很直接——现代 Linux 和 POSIX 系统已经把 errno 实现为线程局部存储(TLS)了。每个线程有自己的 errno 副本,互不干扰。
验证方法:在 Linux 下,errno 通常被定义为类似 extern int *__errno_location() 的宏。每个线程调用这个函数,返回的是自己线程栈上的地址。
但这里有个陷阱:虽然 errno 本身是线程安全的,但如果你在信号处理函数里修改了 errno,而主线程正在检查 errno,那还是会出问题。我曾经在信号处理函数里调用了 write(),结果 write() 失败后修改了 errno,主线程的检查就全乱了。
避坑指南:信号处理函数中如果调用了可能设置 errno 的函数,一定要先保存旧的 errno,处理完再恢复。
void sig_handler(int sig) {
int saved_errno = errno; // 保存
// 处理信号...
errno = saved_errno; // 恢复
}
3.3 正确使用 perror() 和 strerror()
这两个函数是 errno 的好搭档。perror() 直接打印错误信息到 stderr,strerror() 则返回错误码对应的字符串。
我个人习惯这样用:
// 方式一:快速调试用 perror()
FILE *fp = fopen("config.ini", "r");
if (fp == NULL) {
perror("fopen failed");
// 输出类似:fopen failed: No such file or directory
}
// 方式二:需要自定义格式时用 strerror()
if (fp == NULL) {
fprintf(stderr, "Error %d opening config.ini: %s\n",
errno, strerror(errno));
}
perror() 的好处是省事,你传个前缀字符串,它自动帮你拼接错误描述。但如果你想把错误信息写到日志文件,或者需要格式化输出,那就得用 strerror()。
这里有个细节:strerror() 返回的字符串是静态分配的,在多线程环境下可能被覆盖。我记得在某个项目中,两个线程同时调用 strerror(),结果拿到的描述文本串了。
线程安全版本:使用 strerror_r(),它把结果写到用户提供的缓冲区中。
char buf[256];
strerror_r(errno, buf, sizeof(buf));
printf("Error: %s\n", buf);
3.4 常见错误码速查表
我整理了几个最常用的错误码,你写代码时肯定用得上:
| 错误码 | 值 | 含义 | 常见场景 |
|---|---|---|---|
| EPERM | 1 | 操作不允许 | 权限不足 |
| ENOENT | 2 | 文件或目录不存在 | fopen()、open() 失败 |
| EINTR | 4 | 系统调用被信号中断 | read()、write() 被信号打断 |
| EAGAIN | 11 | 资源暂时不可用 | 非阻塞 socket 无数据 |
| ENOMEM | 12 | 内存不足 | malloc() 失败 |
| EACCES | 13 | 权限被拒绝 | 文件权限不足 |
| EINVAL | 22 | 无效参数 | 函数参数错误 |
3.5 核心逻辑流程图
下面这张图展示了 errno 的完整工作流程,从函数调用到错误处理:
3.6 我的经验总结
说了这么多,总结几条我这些年用 errno 的心得:
- 先判断返回值,再读 errno——这是铁律,没得商量
- 多线程用 strerror_r()——别图省事用 strerror(),迟早会出事
- 信号处理函数里保存/恢复 errno——我吃过这个亏,你最好别吃
- 函数成功时别碰 errno——它可能是上次遗留的垃圾值
- 日志里记录 errno 和字符串描述——光记数字没用,还得看含义
errno 这套机制虽然古老,但胜在简单可靠。只要记住上面几条规则,它就是你调试程序的好帮手。说白了,错误处理就像系安全带——平时觉得麻烦,真出事了能救命。