错误:UNIX 程序中的错误代码

使用标准错误机制进行处理

学习 errno 全局变量,从而获得更多您想了解的有关 UNIX® 标准错误报告机制的知识。您还将了解两个关联的全局变量(sys_nerrsys_errlist),并了解有助于向用户报告错误的标准函数。

Chris Herborth (chrish@pobox.com), 自由撰稿人, 作者

Chris Herborth 的照片Chris Herborth 是广受好评的高级技术作家,有超过 10 年的操作系统和编程方面的写作经验。Chris 业余时间喜欢陪他的儿子 Alex 玩耍或与妻子 Lynette 闲逛,其余时间都在进行设计、写作或研究(玩)视频游戏。



2006 年 10 月 12 日

引言

UNIX® 开发人员常常忽视进行适当的错误检测和恢复。缺乏 C 语言的异常和标准 C 库的基本错误机制确实会导致出现此种情况。通过本文,您将熟悉标准 C 库中的 UNIX 错误报告,并且有望以用户友好的方式报告和处理错误。

现在就开始学习本文吧!


开始之前

如果想随同本文一起学习代码示例,将需要下载源代码存档(除非您想亲自键入它)。我将使用 C/C++ Development Tooling (CDT) 在 Eclipse 中进行处理。如果您以前未曾使用过 Eclipse,则可转到参考资料部分中的链接,它们有助于您了解 Eclipse 的入门知识。

代码示例是相当琐碎的,但使用诸如 Eclipse 之类的集成开发环境 (IDE) 可使打开系统头文件、查找特定符号等操作变得更容易。Eclipse (3.2) 的最新版本和 CDT 插件 (2.0) 包含了很有帮助的强大功能。


C 程序中的错误报告

C 语言是 UNIX 平台上最常用的编程语言。尽管其他语言在 UNIX 上很普及(如 Java™、C++、Python 或 Perl),但系统的所有应用程序编程接口(Application Programming Interface,API)均已为 C 语言而创建。标准 C 库(每个 C 编译器套件的一部分)是设立诸如可移植操作系统接口(Portable Operating System Interface,POSIX)和 Single UNIX Specification 之类的 UNIX 标准的基础。

20 世纪 70 年代早期开发 C 和 UNIX 时,在发生某一条件时中断应用程序流的异常还是相当新或尚不存在的概念。库只得使用其他约定来报告错误。

当钻研 C 库或几乎任何其他 UNIX 库时,您将会发现报告故障的两种常用方法:

  • 函数返回错误代码或成功代码;如果是错误代码,则代码本身可用于指出何处出错。
  • 函数返回特定值(或值范围)以指明错误,且设置全局变量 errno 以指明问题的起因。

errno 全局变量(或者,更准确地说应为“符号”,因为在具有线程安全的 C 库的系统上,errno 实际上是一个可确保每个线程都具有其各自的 errno 的函数或宏)在 <errno.h> 系统头文件中定义,并且其所有的可能值都定义为标准常量。

第一个类别中的许多函数实际上会返回标准 errno 代码中的一个,但是,如果不检查手册页上的“返回”部分,则无法预知函数的行为方式。如果运气好的话,函数的手册页会列出其可能返回的所有值,以及它们在此特定函数的上下文中所指的含义。第三方库通常具有单个约定,该库中的所有函数都会遵循此约定,但是,在做出任何假定之前,您将必须再次检查该库的文档。

让我们快速地了解一些代码演示 errno 的情况,并了解可用于将错误代码转换成可读性更强的内容的几个函数。


报告故障

清单 1 中,您将看到一个简短程序,该程序尝试打开一个不太可能存在的文件,并使用两种不同的方法向运行该程序的用户报告错误。

清单 1. errno 变量记录故障
// errno for fun and profit

#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

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 的内容。

清单 2. 清单 1 的输出内容
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 说明如何使用此线程安全版本。

清单 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_errlistchar 类型的指针数组)。第一个全局变量是标准错误消息(存储在 sys_errlist 中)的数量。历史应用程序(即很过时的遗留代码)有时会直接引用这些全局变量,但会在编译过程中产生错误,因为它们的声明并不一致。

POSIX 标准为 errno 定义了相当多的可能值;显然,并不是所有这些值都适用于每个函数,但它们在开发人员编写自己的函数时确实为其提供了较大的可供选择的范围。

以下是 Eclipse 的使用技巧:在代码中选择 errno,然后按下 F3 键(或右键单击 errno,然后从上下文菜单中选择“Open Declaration”)。Eclipse 随即会打开 errno.h 系统头文件并突出显示 errno 中的声明,如图 1 中所示。

图 1. 显示 errno 的声明
显示 errno 的声明

除了注意到我的选项卡设置可能与编写此文件的其他人员的设置不匹配之外,您将看到若干标准错误值、其符号名称,以及分别描述各个值的简短注释。大多数系统头文件都至少包含标准 errno 值的这些信息,因此请放心地去了解这些信息。您的系统头文件和手册页也是有关您的系统可能支持的非标准值的信息的最好来源。

标准 errno 值包括:

  • E2BIG:传递给函数的参数列表太长。
  • EACCESS:拒绝访问!运行程序的用户不具有访问文件、目录等的权限。
  • EAGAIN:所要求的资源暂时不可用;如果稍后再尝试此操作,则可能会成功。
  • EBADF:函数尝试使用错误的文件描述符(例如,它不引用打开的文件,或者用于尝试写入以只读方式打开的文件)。
  • EBUSY:请求的资源不可用。例如,在另一个程序正在读取目录时尝试删除该目录。请注意 EBUSYEAGAIN 之间的模糊性;您显然能够稍后在读取程序完成后删除该目录。
  • ECHILDwait()waitpid() 函数尝试等待退出子进程,但所有子项都已经退出。
  • EDEADLK:如果继续请求,则会出现资源死锁。请注意,这与从多线程代码中获得的死锁类型不同——errno 及其相关项一定能够帮助您跟踪到这些。
  • EDOM:输入参数在数学函数的域之外。
  • EEXIST:文件已存在,且这是问题所在。例如,如果使用指定现有文件或目录的路径调用 mkdir()
  • EFAULT:函数参数之一引用了无效的地址。大部分实现均无法检测到此种情况(相反,程序会接收到 SIGSEGFAULT 信号并退出)。
  • EFBIG:请求会导致文件扩展并超过实现所定义的最大文件大小。所定义的最大文件大小通常约为 2GB,但大部分现代文件系统都支持更大的文件,有时要求 64 位版本的 read()/write()lseek() 函数。
  • 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:系统内存不足。一般而言,应用程序(和操作系统本身)并不能适当地处理此种情况,因此,需要的 RAM 比预期使用的要多,尤其是在系统无法动态地增加磁盘上交换空间的大小的情况下。
  • 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_OKEOKENOERROR)上进行编程,我发现许多代码都包括类似清单 4 的内容。最好将此内容包含在规范中以避免处理此类内容。

清单 4. 无错误的错误值
#if !defined( EOK )
#  define EOK 0         /* no error */
#endif

通过使用 sys_nerr 全局变量和 strerror() 函数,将可以很轻松地快速编写一些代码(请参见清单 5)以打印出系统的所有内置错误消息。请记住,此操作会转储您正在使用的系统所支持的其他所有的实现定义(即,非标准)的 errno 值。上面列出的错误必须存在于符合 POSIX 1003.1 的系统上,其他的错误则不必如此。

清单 5. 显示所有的错误
// 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 ) );
    }
}

我不会让您因我的系统(在撰写本文时为 Mac OS X 10.4.7)所支持的所有 errno 值的完整列表而感到厌烦,不过这里有 print_errs() 函数的输出示例(请参见清单 6)。

清单 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 配置文件 比告知他们未找到文件 更有帮助,然后提供选项让他们选择缺少的文件(为他们提供文件选择 Widget 工具或其他部件),搜索缺少的文件(让程序在可能的地方查找文件),或者创建填充了缺省数据的文件的新版本。

是的,我知道此操作中断了您的代码,但巧妙地解决错误处理和恢复问题会使您的应用程序大受用户的欢迎。而且,由于其他开发人员通常缺乏错误处理方面的知识,因此在处理这方面的问题时会很容易比别人做得更好。


总结

UNIX 对标准错误报告机制的要求极低,但是没有理由让您的应用程序在应对运行时错误时只能导致崩溃或退出,而不告知用户所发生的事情。

标准 C 库和 POSIX 1003.1 定义了许多可能的标准错误值以及几个唾手可得的函数,以用于报告错误并将错误转换成具有可读性的内容。但这些实在不够,开发人员应更努力地尝试告知用户所发生的事情并为他们提供修复或解决问题的方法。


下载

描述名字大小
Demoau-errnoDemo.zip21KB

参考资料

学习

  • 您可以参阅本文在 developerWorks 全球站点上的 英文原文
  • Exception handling:有关描述异常的文章以及无法在 C 中使用的错误处理构造,请阅读 Wikipedia 上的这篇文章。它们通过其他语言(C++、Objective-C 和 Python 等)在 UNIX 系统上受支持。
  • What is Eclipse, and how do I use it?(developerWorks,2001 年 11 月):阅读这篇文章以大概了解 Eclipse 平台。
  • 使用 Eclipse 平台进行调试(developerWorks,2005 年 10 月):了解如何可以在 Eclipse 平台中使用内置调试功能。
  • 现在开始使用 Eclipse:获取有关 Eclipse 的更多信息和链接。
  • 在 Eclipse 平台上开发 C/C++(developerWorks,2006 年 6 月):获取如何在 C/C++ 开发项目中使用 Eclipse 平台的概述。
  • AIX and UNIX:访问 developerWorks 的“AIX and UNIX”专区以拓展 UNIX 技能。
  • AIX and UNIX 新手入门:访问“AIX and UNIX 新手入门”页面以了解更多关于 AIX 和 UNIX 的内容。
  • Open source:访问 developerWorks 的“Open source”专区以获得大量“How to”信息、工具和项目更新,以帮助您使用开放源代码技术进行开发,以及在 IBM 产品中使用它们。
  • developerWorks 技术活动和网络广播:跟踪最新的 developerWorks 技术活动与网络广播。
  • AIX 5L Wiki:AIX 相关技术信息的协作环境。
  • 播客:收听播客并保持与 IBM 技术专家同步。

获得产品和技术

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=AIX and UNIX, Open source
ArticleID=167468
ArticleTitle=错误:UNIX 程序中的错误代码
publish-date=10122006