linux错误检测及处理办法

本文内容来自:高级Unix编程第二版 14.2 节
错误检测
    大多数的系统调用都会返回值,如read函数,返回的是一个已读取的字节数。为了表示错误,系统通常会返回一个不会和有效数据混淆的
数值,最常见的是-1。因此,一般的错误处理程序写出来如下:
if ((amt = read(fd, buf, numbyte)) == -1) {
    fprintf(stderr, "Read failed!/n");
    exit(EXIT_FAILURE);
}
注意:符号EXIT_FAILURE是属于标准C的。
返回错误指示系统调用失败的原因有很多种。其中大部分情况下,证书符号errno包含的代码可以指明原因。要得到errno,应包含头文件
errno.h。虽然errno不一定非是个整数变量,但可像证书那样使用它。如果使用线程,那么errno用起来会象函数调用,因为不同的线程不可能
全部都能使用同一个全局变量。因此,不要自己声明errno,而应使用头文件中的定义,如下所示:
#include <errno.h>
if ((amt = read(fd, buf, numbyte)) == -1) {
    fprintf(stderr, "Read failed! errno = %d/n", errno);
    exit(EXIT_FAILURE);
}
如果是文件描述符错误,其输出应为:
Read failed! errno = 9
另外需要注意,只有首先执行了错误检测的前提下,才可使用errno的值,不能仅检测errno查看是否发生错误:
//errno = 0;
amt = read(fd, buf, numbyte);
if (errno != 0) { /* wrong! */
    fprintf(stderr, "Read failed! errno = %d/n", errno);
    exit(EXIT_FAILURE);
}
但这依然不是一个好办法,因为:
    1、如果以后要修改代码,要在调用read之前加入其它系统调用,或加入其他最后会执行系统调用的函数时,errno的值可能会被那个调用
所设定;
    2、不是所有的系统调用都会设置errno的值,这样检查错误不够严格。
此时,建议最好使用标准C函数perror,如下所示:
if ((amt = read(fd, buf, numbyte)) == -1) {
    perror("Read failed!");
    exit(EXIT_FAILURE);
}
现在输出的是:
Read failed!: Bad file number
另外的一个有用的标准C函数是strerror,它不像perror那样显示信息,而提供一个字符串信息,使用如:
sprintf(stderr, "%s/n", strerror(errno));
C错误检测宏
    将每个系统调用加入到if语句,并在其后编写显示错误信息以及退出或返回的代码是很单调的,而且清理工作也很难做,有些程序员会使
用一个goto语句,这个办法使得清理代码只需要编写一次。但是编写那些if、fprintf和goto代码,仍然是一种效率低的办法,一种非常简便的
办法就是使用宏定义,如下所示:
ec_null( p = malloc(sizeof(buf)) )
ec_negl( fdin = open(filein, O_RDONLY) )
ec_negl( fdout = open(fileout, O_WRONLY) )
linux C中提供的这些错误检测宏可以提供以下功能:
    1、简单易读的错误检测
    2、自动跳转以清理代码
    3、提供完整的错误信息和回溯追踪
以下是错误检测宏的头文件(ec.h)的大部分内容(忽略了一些函数声明和少量其它的次要细节):
extern const bool ec_in_cleanup;
typedef enum (EC_ERRNO, EC_EAI) EC_ERRTYPE;
#define EC_CLEANUP_BGN /
    ec_earn();/
    ec_cleanup_bgn;/
    {/
        bool ec_in_cleanup;/
        ec_in_cleanup = true;
#define EC_ECLEANUP_END /
    }
#define ec_cmp(var, errrtn)/
    {/
        assert(!ec_in_cleanup);/
        if ((intptr_t)(var) == (intptr_t)(errrtn)) {/
            ec_push(__func__, __FILE__, __LINE__, #var, errno, EC_ERRNO);/
            goto ec_cleanup_bgn;
        }/
    }
#define ec_rv(var)/
    {/
        int errrtn;/
        assert(!ec_inc_cleanup);/
        if ((errrtn == (var)) != 0) {/
            ec_push(__func__, __FILE__, __LINE__, #var, errno, EC_ERRNO);/
            goto ec_cleanup_bgn;/
        }/
    }
#define ac_ai(var)/
    {/
        int errrtn;
        assert(!ac_in_cleanup);/
        if ((errrtn == (var)) != 0) {/
            ec_push(__func__, __FILE__, __LINE__, #var, errno, EC_EAI);/
            goto ec_cleanup_bgn;/
        }/
    }
#define ec_negl(x) ec_cmp(x, -1)
#define ec_null(x) ec_cmp(x, NULL)
#define ec_false(x) ec_cmp(x, false)
#define ec_eof(x) ec_cmp(x, EOF)
#define ec_nzero(x)/
    {/
        if ((x) != 0)/
            EC_FALL/
    }
#define EC_FALL ec_cmp(0, 0)
#define EC_CLEANUP goto ec_cleanup_bgn;
#define EC_FLUSH(str)/
    {/
        ec_print();/
        ec_print();/
    }
在解释宏之前,我们先讨论一个问题并提出解决方案,该问题是如果你在清理代码中调用了一个错误检测宏(如ec_negl)并出现了错误,那么很
可能形成一个无限循环,因为宏还会跳转到清理代码中!下面正式这样一个例子:
EC_CLEANUP_BGN
    free(p);
    if (fdin != -1)
        ec_negl( close(fdin) )
    if (fdout != -1)
        ec_negl( close(fdout) )
    return false;
EC_CLEANUP_END
    看上去程序员是在非常小心地检测close返回的错误,但其做法会带来非常严重的后果,其真正糟糕的是只有在某错误后存在错误清理时才
会发生循环,而这是一种在测试时很难捕获的非常不常见的情况。应谨防这种情况--错误检测宏应当增加可靠性,而不是降低可靠性!
    解决方案就是在ec_in_cleanup里面加入一个局部变量,并将其值设置为true,然后针对它的测试存在宏ec_cmp(如果设置了该值,assert
就会启动,从而就知道出错了)中。
下面逐一介绍宏:
    EC_CLEANUP_BGN包括清理代码的标签,在其前面的函数调用输出一条控制流入标签的警告。这可以避免忘记在标签之前放置一条return语
句而导致在没有错误时跳入清理代码只类的常见错误。
    EC_CLEANUP_END只是起到一个封闭大括号的作用。需要这些大括号来创建局部上下文。
    ec_cmp做了大部分的工作:确保没有处于清理代码中,检测错误,调用ec_push把位置信息压入堆栈,并跳到清理代码。类型typedef定为
long也可以。如果要更强的安全性,可以在程序的某处粘贴一些用于测试sizeof(void*)等于sizeof(long)的代码。(如果不熟悉符号#var, 请
研读C语言--它把无论什么var都扩大到一个字符串。)
    ec_rv,它用于返回一个非零错误代码以指示错误,而不使用errno本身的函数。然而。返回的代码是errno值,因此可以直接传递给
ec_push。
    EC_CLEANUP仅用于需要跳入清理代码的情况
    EC_FLUSH仅用于需要显示错误信息而不需要等待退出的情况。
    ec_push将传递给它的错误及其上下文信息压入堆栈中。
用ateexit注册的函数,在程序退出时显示堆栈中的信息:
static void ec_atexit_fcn(void)
{
    ec_print();
}
ec_print遍历堆栈以显示跟踪和错误信息。
ec_reinit清空堆栈内容,使得错误检测可以开始全新的跟踪。

最后就是介绍一下函数assert,abort,exit,atexit,strerror
文章出处: http://www.diybl.com/course/3_program/c++/cppjs/2008727/134161.html
1. assert()
assert宏的原型定义在<assert.h>中,其作用是如果它的条件返回错误,则终止程序执行,原型定义:
#include <assert.h>
void assert( int expression );
assert的作用是现计算表达式 expression ,如果其值为假(即为0),那么它先向stderr打印一条出错信息,然后通过调用 abort 来终止程
序运行。
 
请看下面的程序清单badptr.c:
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
int main( void )
{
    FILE *fp;
    fp = fopen( "test.txt", "w" );//以可写的方式打开一个文件,如果不存在就创建一个同名文件
    assert( fp );                        //所以这里不会出错
    fclose( fp );
    fp = fopen( "noexitfile.txt", "r" );//以只读的方式打开一个文件,如果不存在就打开文件失败
    assert( fp );                        //所以这里出错
    fclose( fp );                        //程序永远都执行不到这里来
    return 0;
}
使用assert的缺点是,频繁的调用会极大的影响程序的性能,增加额外的开销。
在调试结束后,可以通过在包含#include <assert.h>的语句之前插入 #define NDEBUG 来禁用assert调用,示例代码如下:
#include <stdio.h>
#define NDEBUG
#include <assert.h>
用法总结与注意事项:
1)在函数开始处检验传入参数的合法性
如:
int resetBufferSize(int nNewSize)
{
    //功能:改变缓冲区大小,
    //参数:nNewSize 缓冲区新长度
    //返回值:缓冲区当前长度
    //说明:保持原信息内容不变  nNewSize<=0表示清除缓冲区
    assert(nNewSize >= 0);
    assert(nNewSize <= MAX_BUFFER_SIZE);
    ...
}
2)每个assert只检验一个条件,因为同时检验多个条件时,如果断言失败,无法直观的判断是哪个条件失败
不好: assert(nOffset>=0 && nOffset+nSize<=m_nInfomationSize);
好: assert(nOffset >= 0);
 assert(nOffset+nSize <= m_nInfomationSize);
3)不能使用改变环境的语句,因为assert只在DEBUG个生效,如果这么做,会使用程序在真正运行时遇到问题
错误: assert(i++ < 100)
这是因为如果出错,比如在执行之前i=100,那么这条语句就不会执行,那么i++这条命令就没有执行。
正确: assert(i < 100)
      i++;
4)assert和后面的语句应空一行,以形成逻辑和视觉上的一致感
5)有的地方,assert不能代替条件过滤
2.       abort()
函数名: abort
功 能: 异常终止一个进程
用 法: void abort(void);
头文件:#include <stdlib.h>
说明:abort函数是一个比较严重的函数,当调用它时,会导致程序异常终止,而不会进行一些常规的清除工作,比如释放内存等。
程序例:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    puts( "About to abort..../n" );
    abort();
    puts( "This will never be executed!/n" );
    exit( EXIT_SUCCESS );
}
3. exit()
表头文件: #include<stdlib.h>
定义函数: void exit(int status);
exit()用来正常终结目前进程的执行,并把参数 status 返回给父进程,而进程所有的缓冲区数据会自动写回并关闭未关闭的文件。它并不像
abort那样不做任何清理工作就退出,而是在完成所有的清理工作后才退出程序。
4. atexit(设置程序正常结束前调用的函数)
表头文件 #include<stdlib.h>
定义函数 int atexit (void (*function)(void));
atexit()用来设置一个程序正常结束前调用的函数。当程序通过调用 exit()或从 main 中返回时,参数function所指定的函数会先被调用,然后
才真正由exit()结束程序。
返回值  如果执行成功则返回 0,否则返回-1,失败原因存于 errno 中。
#include <stdlib.h>
#include <stdio.h>
void my_exit(void)
{
    printf( "Before exit..../n" );
}
int main(void)
{
    atexit( my_exit );
    return 0;
}
5. strerror(返回错误原因的描述字符串)
表头文件     #include<string.h>
定义函数     char * strerror(int errnum);
strerror()   用来依参数 errnum 的错误代码来查询其错误原因的描述字符串,然后将该字符串指针返回。这时如果把 errno 传个strerror,
就可以得到可读的提示信息,而不再是一个冷冰冰的数字了。
返回值      返回描述错误原因的字符串指针。
#include <string.h>
#include <stdio.h>
int main(void)
{
    int i;
    for ( i=0; i<10; i++ )
    {
        printf( "%d:%s/n", i, strerror(i) );
    }
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值