UNIX®开发人员经常忽略正确的错误检测和恢复。 C语言缺乏例外,而标准C库缺少基本的错误机制,无疑是造成这种情况的原因。 本文使您熟悉标准C库中的UNIX错误报告,并(希望)鼓励您以用户友好的方式报告和处理错误。
让我们开始吧!
如果要遵循本文中的代码示例,则需要下载源档案 (除非您要键入它)。 我将使用C / C ++开发工具(CDT)在Eclipse中工作。 如果您以前从未尝试过Eclipse,则“ 相关主题”部分中的链接可以帮助您入门。
这些代码示例相当简单,但是使用诸如Eclipse之类的集成开发环境(IDE),可以使您更轻松地打开系统头,查找特定符号等。 Eclipse(3.2)和CDT插件(2.0)的最新版本具有许多实用的功能。
C是UNIX平台上最常用的编程语言。 尽管UNIX上流行其他语言(例如Java™,C ++,Python或Perl),但系统的所有应用程序编程接口(API)都是针对C创建的。标准C库是每个C编译器套件的一部分,是创建UNIX标准(例如可移植操作系统接口(POSIX)和Single UNIX Specification)的基础。
在1970年代初开发C和UNIX时,异常的概念是相当新颖的或不存在的,这种异常在某些情况发生时会中断应用程序的流程。 这些库必须使用其他约定来报告错误。
在倾倒C库或几乎所有其他UNIX库时,您将发现两种常见的报告故障的方式:
errno
以指示问题的原因。 在
系统头文件中定义了errno
全局变量(或更准确地说是符号,因为在具有线程安全C库的系统上, errno
实际上是确保每个线程都有自己的errno
的函数或宏)。 ,以及所有可能的值(定义为标准常量)。
第一类中的许多函数实际上返回标准的errno
代码之一,但是如果不检查手册页的“返回”部分,就无法说出函数的行为以及返回的内容。 如果幸运的话,该函数的手册页列出了所有可能的返回值以及此特定函数上下文中的含义。 第三方库通常只有一个约定,库中的所有函数都遵循该约定,但是同样,您在进行任何假设之前,必须先检查库的文档。
让我们快速看一下一些演示errno
代码以及一些函数,您可以使用这些函数将错误代码转换为更易理解的内容。
在清单1中 ,您将找到一个简短的程序,该程序尝试使用两种不同的技术来打开一个不太可能存在的文件,并向正在运行该程序的任何人报告错误。
errno
变量记录您的失败 // errno for fun and profit
#include
#include
#include
#include
#include
const char *FILE_NAME = "/tmp/this_file_does_not_exist.yarly";
int main( int argc, char **argv )
{
int fd = 0;
printf( "Opening %s...\n", FILE_NAME );
fd = open( FILE_NAME, O_RDONLY, 0644 );
if( fd < 0 ) {
// Error, as expected.
perror( "Error opening file" );
printf( "Error opening file: %s\n", strerror( errno ) );
}
return EXIT_SUCCESS;
}
运行该程序时,您将看到清单2所示的内容 。
chrish@dhcp2 [507]$ ./Debug/errnoDemo
Opening /tmp/this_file_does_not_exist.yarly...
Error opening file: No such file or directory
Error opening file: No such file or directory
从输出( 清单2 )中可以看到, perror()
函数显示传递给它的字符串,后跟一个冒号,一个空格,然后是当前errno
值的文本表示形式。 您可以使用printf()
调用和strerror()
函数自己模拟此情况,该函数返回一个指向当前errno
值的文本表示形式的指针。
从输出中看不到的一个细节是perror()
将其消息写入标准错误通道( stderr
)。 清单1中的printf()
调用正在写入标准输出通道( stdout
)。
strerror()
函数不一定是线程安全的。 对于未知值,它将在静态缓冲区中格式化错误消息,并返回指向该缓冲区的指针。 对strerror()
其他调用将覆盖该缓冲区的内容。
POSIX 1003.1标准定义了strerror_r()
,除了错误值之外,该错误还接受指向缓冲区的指针和缓冲区大小。 清单3显示了如何使用此线程安全版本。
strerror_r()
函数 // Thread-safe usage of strerror_r().
void thread_safe( int err )
{
char buff[256];
if( strerror_r( err, buff, 256 ) == 0 ) {
printf( "Error: %s\n", buff );
}
}
当处理标准errno
值时, perror()
和strerror()/strerror_r()
函数可能是最常用的错误报告方法。 让我们看一下其他一些与错误相关的全局变量,以及由POSIX-1003.1 errno
值定义的标准。
因此,全局errno
变量是由标准C库函数设置的(可能还有其他设置;请阅读详细的手册,以了解您打算使用的函数是否设置了errno
)来指示某种错误,例如是否传入了一些错误的值参数,或者函数执行其职责时失败。
提取标准错误描述的perror()
和strerror()
函数来自全局变量sys_errlist
。
标准C库定义了两个附加的与错误相关的全局变量, sys_nerr
(一个int
)和sys_errlist
(一个指向char
的指针数组)。 第一个是sys_errlist
存储的标准错误消息的数量。 历史应用程序(即可怕的过时的遗留代码)有时直接引用它们,但由于在声明时不一致,因此在编译过程中会产生错误。
POSIX标准为errno
定义了很多可能的值; 显然,并非所有这些功能都适用于每个功能,但是它们确实为开发人员提供了一个编写自己的功能时可以选择的大型菜单。
这是Eclipse的提示:在代码中选择errno
,然后按F3键(或右键单击errno
,然后从上下文菜单中选择Open Declaration )。 Eclipse打开errno.h
系统头,并突出显示errno
的声明, 如图1所示。
errno
的声明 除了注意到我的选项卡设置与编写此文件的人的设置不匹配之外,您还将看到几个标准错误值,它们的符号名以及描述每个错误的简短注释。 大多数系统标头至少包含有关标准errno
值的大量信息,因此请不要担心。 系统标题和手册页也是有关系统可能支持的非标准值的唯一信息来源。
标准errno
值包括:
E2BIG
传递给函数的参数列表过长。 EACCESS
-访问被拒绝! 运行该程序的用户无权访问文件,目录等。 EAGAIN
所需的资源暂时不可用; 如果稍后再次尝试该操作,则可能会成功。 EBADF
函数试图使用错误的文件描述符(例如,它不引用打开的文件,或者试图将其写入以只读方式打开的文件)。 EBUSY
-请求的资源不可用。 例如,尝试在另一个应用程序读取目录时删除该目录。 注意EBUSY
和EAGAIN
之间的歧义; 显然,当阅读程序完成后,您可以在以后删除目录。 ECHILD
- wait()
或waitpid()
函数试图等待子进程退出,但所有子进程均已退出。 EDEADLK
如果请求继续,则会发生资源死锁。 请注意,这不是您在多线程代码中遇到的那种死锁errno
及其朋友绝对无法帮助您找到这些死锁。 EDOM
输入参数在数学函数的范围之外。 EEXIST
文件已存在,这是一个问题。 例如,如果您使用命名现有文件或目录的路径调用mkdir()
。 EFAULT
函数参数之一引用无效地址。 大多数实现无法检测到此情况(您的程序收到SIGSEGFAULT
信号,然后退出)。 EFBIG
请求将导致文件扩展到实现定义的最大文件大小之外。 通常大约为2GB,但是大多数现代文件系统支持更大的文件,有时需要read()/write()
和lseek()
函数的64位版本。 EINTR
函数被信号中断,该信号被程序中的信号处理程序捕获,并且信号处理程序正常返回。 EINVAL
您向函数传递了无效的参数。 EIO
发生I / O错误; 这通常是为响应硬件问题而生成的。 EISDIR
您调用了一个函数,该函数需要带有目录参数的文件参数。 ENFILE
在此过程中已经打开了太多文件。 每个进程都有OPEN_MAX
文件描述符,并且您正在尝试打开( OPEN_MAX + 1
)文件。 请记住, 文件描述符包括套接字之类的东西。 ENLINK
函数调用将导致文件具有多个LINK_MAX
链接。 ENAMETOOLONG
您创建的路径名长于PATH_MAX
,或者创建的文件或目录名长于NAME_MAX
。 ENFILE
系统同时打开文件过多。 这应该是暂时的情况,在现代系统中不太可能发生。 ENODEV
没有这样的设备,或者您正在尝试对指定的设备执行不适当的操作(例如,请勿尝试从古老的行式打印机读取数据)。 ENOENT
找不到这样的文件,或者指定的路径名不存在。 ENOEXEC
您试图运行不可执行的文件。 ENOLCK
没有锁可用; 您已达到文件或记录锁定的系统范围限制。 ENOMEM
系统内存不足。 传统上,应用程序(和OS本身)不能很好地处理此问题,这就是为什么您需要拥有比预期使用更多的RAM的原因,尤其是在无法动态增加磁盘交换空间大小的系统上。 ENOSPC
设备上没有ENOSPC
空间。 您试图在已满的设备上写入或创建文件。 同样,对于应用程序和操作系统而言,传统的做法是无法优雅地处理此问题。 ENOSYS
系统不支持该功能。 例如,如果在没有作业控制的系统上调用setpgid()
,则会收到ENOSYS
错误。 ENOTDIR
指定的路径名必须是目录,但不是。 这与EISDIR
错误相反。 ENOTEMPTY
指定的目录不为空,但必须为空。 请注意, 空目录仍包含。 和..条目。 ENOTTY
您已尝试对不支持该文件或特殊文件的I / O控制操作。 例如,请勿尝试在目录上设置波特率。 ENXIO
您已尝试对不存在的设备的特殊文件进行I / O请求。 EPERM
不允许该操作; 您没有访问指定资源的权限。 EPIPE
您试图读取或写入不存在的管道。 管道链中的程序之一已关闭其部分流(例如,通过退出)。 ERANGE
您调用了一个函数,并且返回值太大,无法用返回类型表示。 例如,如果一个函数返回一个unsigned char
值,但计算结果为256或更大(或-1
或更小),则errno
将被设置为ERANGE
并且该函数将返回一些不相关的值。 在这种情况下,重要的是检查输入数据的完整性,或者在每次调用后检查errno
。 EROFS
您试图修改存储在只读文件系统(或以只读模式挂载的文件系统)上的文件或目录。 ESPIPE
您尝试在管道或先进先出(FIFO)上查找。 ESRCH
您指定了无效的进程ID或进程组。 EXDEV
您尝试了一种会在设备之间移动链接的操作。 例如,UNIX文件系统不允许您在文件系统之间移动文件(相反,您必须先复制文件,然后删除原始文件)。 POSIX 1003.1规范的一个令人讨厌的功能是缺少无错误值。 当errno
设置为0时,您不会遇到任何问题,除非您无法使用标准的符号常量来引用它。 我已经在errno.h
中具有E_OK
, EOK
和ENOERROR
平台上进行了编程,并且看到了很多包含清单4这样的代码。 最好将其包含在规范中,以避免执行此类操作。
#if !defined( EOK )
# define EOK 0 /* no error */
#endif
使用sys_nerr
全局变量和strerror()
函数,您可以轻松地编写一些代码(请参见清单5 )以打印出系统的所有内置错误消息。 请记住,这将转储您使用的系统支持的所有其他实现定义的(即非标准) errno
值。 在符合POSIX 1003.1的系统上,只需要存在上面列出的错误,其他都是肉汁。
// Print out all known errors on the system.
void print_errs( void )
{
int idx = 0;
for( idx = 0; idx < sys_nerr; idx++ ) {
printf( "Error #%3d: %s\n", idx, strerror( idx ) );
}
}
我不会为您提供系统支持的所有errno
值的完整列表(在撰写本文时为Mac OS X 10.4.7),但这是print_errs()
函数的输出示例(请参见清单)。 6 )。
Error # 0: Unknown error: 0
Error # 1: Operation not permitted
Error # 2: No such file or directory
Error # 3: No such process
Error # 4: Interrupted system call
Error # 5: Input/output error
Error # 6: Device not configured
Error # 7: Argument list too long
Error # 8: Exec format error
Error # 9: Bad file descriptor
Error # 10: No child processes
Error # 93: Attribute not found
Error # 94: Bad message
Error # 95: EMULTIHOP (Reserved)
Error # 96: No message available on STREAM
Error # 97: ENOLINK (Reserved)
Error # 98: No STREAM resources
Error # 99: Not a STREAM
Error #100: Protocol error
Error #101: STREAM ioctl timeout
Error #102: Operation not supported on socket
这是很多错误! 幸运的是,大多数函数只会报告一些可能的错误,因此适当地处理它们通常并不难。
将错误处理代码添加到程序中可能会很烦,乏味且耗时。 它会使您的代码混乱,使您陷入困境,无法为每个可能出现的错误添加处理程序。 开发人员经常讨厌这样做。
但是,您不是为自己而做的,而是为将要实际使用您的程序的人做的。 如果某件事可能失败,那么他们需要知道失败的原因,更重要的是,他们可以采取哪些措施来解决问题。
最后一部分通常是开发人员经常错过的地方。 告诉用户未找到文件并没有告诉他们无法找到SuperWidget配置文件 ,然后为他们提供选择丢失的文件的选项(给他们一个文件选择小部件或其他东西),搜索丢失的文件,这几乎没有什么用。 (让程序在文件的可能位置查找),或使用默认数据创建文件的新版本。
是的,我知道这会中断您的代码流 ,但是精巧的错误处理和恢复确实使您的应用程序深受用户欢迎。 而且,由于其他开发人员通常在错误处理方面缺乏能力,因此比其他人做得更好。
在UNIX上,标准的错误报告机制非常简单,但这并不是应用程序通过崩溃或退出而不告诉用户正在发生的事情来处理运行时错误的理由。
标准C库和POSIX 1003.1定义了许多可能的标准错误值,以及几个方便的函数,用于报告错误并将错误转换为人类可以读取的内容。 但是,这些还远远不够,开发人员应该更加努力地告诉用户发生了什么,并为他们提供解决问题或解决问题的方法。
翻译自: https://www.ibm.com/developerworks/aix/library/au-errnovariable/index.html