C标准库之errno

<errno.h>定义了整数变量extern int errnoerrno是一个由POSIXISO C标准定义的符号,由系统调用和某些库函数根据事件来设置它,用以表明哪里有问题。这个值只有当调用的返回表明错误的时候有用(比如,对于大多数的系统调用是-1,对于大多数的库函数来说是-1或NULL),正确的函数也可以修改errno

Linux中系统调用的错误都存储于 errno 中,errno 由操作系统维护,存储就近发生的错误,即下一次的错误码会覆盖掉上一次的错误。

  • 在程序启动时,errno 设置为零,系统调用或 C 标准库中的特定函数修改它的值为一些非零值以表示某些类型的错误。有效的错误number都是非零的,系统调用和库函数不会把errno设为0。 对于某些系统调用和库函数(比如,getpriority(2)),没有错误的时候也会返回-1。在这种情况下,可以在调用之前先将errno设为0,当不确定有没有错误的时候,可以通过查看errno是不是一个非零值来确定是否发生错误。

  • errno的值不会被任何程序清除,因此在使用errno的值之前,先要通过函数(系统调用/库函数)的返回值来确定有错误发生。

  • ISO C标准将errno定义为一个可以修改的int型左值,并且不允许准确声明errno可能是一个宏,也可能被定义成一个变量,这个具体要看编译器自己的实现。

  • 系统调用或库函数正确执行,并不保证errno的值不会被改变。

✏️ 1、线程安全性

早些时候,POSIX.1曾把errno定义成extern int errno这种形式,但这种形式实现的errno在多线程环境下是不安全的,errno变量是被多个线程共享的,这样可能线程A发生某些错误改变了errno的值,线程B虽然没有发生任何错误,但是当它检测errno的值的时候,线程B会以为自己发生了错误。

在GCC中,errnothread-local,在一个线程中设置它的值不会影响它在另一个thread中的值。

其定义在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 */

error.h中:

可以清晰的看到,bits/errno.herrno进行了重定义。从 __attribute__ ((__const__))推测出__errno_location ()会返回与参数无关的与线程绑定的一个特定地址,应用层直接从该地址取出errno的。(关于__attribute__用法可以参考Using GNU C attribute)。但是上面使用了条件编译,也就是有两种方法可以使得gcc重定义errno

  • 不定义宏_LIBC

  • 定义宏_LIBC_REENTRANT

有意思的是,我们在编译时,压根不能设置_LIBC, 在gnu/stubs-64.h中会检测,如果有_LIBC宏定义,直接报错终止预编译:

因此在正常情况下,我们使用gcc编译的程序,全局变量errno一定是线程安全的。

有一个问题,__errno_location是怎么实现的? 在Linux 源代码中查找 __errno_location() 函数,在 errno-loc.c 中有解释:

显然,__errno_location() 函数的返回值就是 __hurd_threadvar_location(_HURD_THREADVAR_ERRNO) 函数的返回值被强转成 int * 了,那 __hurd_threadvar_location(_HURD_THREADVAR_ERRNO) 又是什么函数呢?

在头文件 <hurd/threadvar.h>中有:

注:在前面有定义#define _HURD_THREADVAR_H_EXTERN_INLINE extern __inline了解到其实就是 extern __inline ,其中 __inline 表示函数是内联函数。

这是先定义了函数同时在下面直接就给出了函数代码,这个函数的又是调用了__hurd_threadvar_location_from_sp(__index, __thread_stack_pointer ()) 这个函数, 同样在<hurd/threadvar.h> 中往前看,有这样的代码:

往前有这些变量的定义:

注释如下:

再从前面 __hurd_threadvar_location(_HURD_THREADVAR_ERRNO) 传过来的参数,知道 __hurd_threadvar_location_from_sp(enum hurd_threadvar_index index, void *sp) 函数第一个参数是 _HURD_THREADVAR_ERRNO ,第二个参数在 __sp 指向 __thread_stack_pointer() 函数的返回值,查询 <machine-sp.h> 有如下:

✏️ 2、宏定义

Linux中,在头文件 /usr/include/asm-generic/errno-base.h 对基础常用errno进行了宏定义

/usr/include/asm-asm-generic/errno.h 中,对剩余的errno做了宏定义

这些都是由POSIX.1确定的,错误名必须有唯一的值,有个例外是,EAGIANEWOULDBLOCK可能是相同的。

✏️ 3、打印

🖋️ 3.1、打印错误信息

作用:打印系统错误信息

头文件:#include <stdio.h>

函数原型:void perror(const char *s)

参数:s: 字符串提示符

输出形式:const char *s: strerror(errno) //提示符:发生系统错误的原因

3.2、字符串显示错误信息

作用:将错误码以字符串的信息显示出来

头文件:#include <string.h>

函数原型:char *strerror(int errnum);

参数:errno

返回值:返回错误码字符串信息

不是所有的地方发生错误的时候都可以通过error获取错误代码:

使用gethostbyname()函数,不能使用perror()来输出错误信息(因为错误代码存储在 h_errno 中而不是errno中。所以需要调用herror()函数。 简单的传给gethostbyname()一个机器名(“bbs.tsinghua.edu.cn”),然后就从返回的结构struct hostent 中得到了IP 等其他信息。程序中输出IP 地址的程序需要解释一下:h->h_addr 是一个char,但是inet_ntoa()函数需要传递的是一个struct in_addr 结构。所以上面将h->h_addr 强制转换为struct in_addr,然后通过它得到了所有数据。

最后更新于

这有帮助吗?