C/C++ errno的处理及多线程安全问题

在 C 语言中,对于存放错误码的全局变量 errno,相信大家都不陌生。为防止和正常的返回值混淆,系统调用一般并不直接返回错误码,而是将错误码(是一个整数值,不同的值代表不同的含义)存入一个名为 errno 的全局变量中,errno 不同数值所代表的错误消息定义在 <errno.h> 文件中。如果一个系统调用或库函数调用失败,可以通过读出 errno 的值来确定问题所在,推测程序出错的原因,这也是调试程序的一个重要方法。

配合 perror 和 strerror 函数,还可以很方便地查看出错的详细信息。其中:

  • perror 在 <stdio.h> 中定义,用于打印错误码及其消息描述;
  • strerror 在 <string.h> 中定义,用于获取错误码对应的消息描述;

调用errno之前必须先将其清零。

在 C 语言中,如果系统调用或库函数被正确地执行,那么 errno 的值不会被清零。换句话说,errno 的值只有在一个库函数调用发生错误时才会被设置,当库函数调用成功运行时,errno 的值不会被修改,当然也不会主动被置为 0。也正因为如此,在实际编程中进行错误诊断会有不少问题。

举个例子:

/// @file main.cpp
#include <stdio.h>
#include <errno.h>
#include <string.h>

int main(void)
{
    /*调用errno之前必须先将其清零*/
    errno=0;
    FILE *fp = fopen("test.txt","r");
    if(errno!=0)
    {
        printf("errno值: %d\n",errno);
        printf("错误信息: %s\n",strerror(errno));
    }
    fp = fopen("main.cpp","r");
    if(errno!=0)
    {
        printf("errno值: %d\n",errno);
        printf("错误信息: %s\n",strerror(errno));
    }
    fclose(fp);
    return 0;
}

输出结果如下:

$ g++ main.cpp -o main
$ ./main
errno值: 2
错误信息: No such file or directory
errno值: 2
错误信息: No such file or directory

避免重定义errno。

对于 errno,它是一个由 ISO C 与 POSIX 标准定义的符号。早些时候,POSIX 标准曾经将 errno 定义成“extern int errno”这种形式,但现在这种定义方式比较少见了,那是因为这种形式定义的 errno 对多线程来说是致命的。

在多线程环境下,errno 变量是被多个线程共享的,这样就可能引发如下情况:线程 A 发生某些错误而改变了 errno 的值,那么线程 B 虽然没有发生任何错误,但是当它检测 errno 的值时,线程 B 同样会以为自己发生了错误。

我们知道,在多线程环境中,多个线程共享进程地址空间,因此就要求每个线程都必须有属于自己的局部 errno,以避免一个线程干扰另一个线程。其实,现在的大多部分编译器都是通过将 errno 设置为线程局部变量的实现形式来保证线程之间的错误原因不会互相串改。

例如,在 Linux 下的 GCC 编译器中,标准的 errno 在“/usr/include/errno.h”中的定义如下:

/* Get the error number constants from the system-specific file.
   This file will test __need_Emath and _ERRNO_H.  */
#include <bits/errno.h>
#undef   __need_Emath
#ifdef   _ERRNO_H
/* Declare the `errno' variable, unless it's defined as a macro by bits/errno.h.  This is the case in GNU, where it is a per-thread variable.  This redeclaration using the macro still works, but it will be a function declaration without a prototype and may trigger a -Wstrict-prototypes warning.  */
#ifndef   errno
extern int errno;
#endif

其中,errno在“/usr/include/bits/errno.h”文件中的具体实现如下:

# ifndef __ASSEMBLER__
/* Function to get address of global 'errno' variable.  */
extern int *__errno_location (void) __THROW __attribute__ ((__const__));
# if !defined _LIBC || defined _LIBC_REENTRANT
/* When using threads,errno is a per-thread value.  */
# define errno (*__errno_location ())
# endif
# endif /* !__ASSEMBLER__ */
# endif /* _ERRNO_H */

这样,通过“extern int*__errno_location(void)、__THROW__attribute__((__const__));”与“#define errno(*__errno_location())”定义,使每个线程都有自己的 errno,不管哪个线程修改 errno 都是修改自己的局部变量,从而达到线程安全的目的。

避免使用errno检查文件流错误。

上面已经阐述过,在 POSIX 标准中,可以通过 errno 值来检查 fopen 函数调用是否发生了错误。但是,对特定文件流操作是否出错的检查则必须使用 ferror 函数,而不能够使用 errno 进行文件流错误检查。如下面的示例代码所示:

int main(void)
{
    FILE* fp=NULL;
    /*调用errno之前必须先将其清零*/
    errno=0;
    fp = fopen("Test.txt","w");
    if(fp == NULL)
    {
        if(errno!=0)
        {
            /*处理错误*/
        }
    }
    else
    {
        /*错误地从fp所指定的文件中读取一个字符*/
        fgetc(fp);
        /*判断是否读取出错*/
        if(ferror(fp))
        {
            /*处理错误*/
            clearerr(fp);
        }
        fclose(fp);
        return 0;
    }
}
发布了257 篇原创文章 · 获赞 13 · 访问量 6万+

猜你喜欢

转载自blog.csdn.net/LU_ZHAO/article/details/105119476