如何处理嵌入式C编程中的错误异常

发布时间:2023-09-07  

本文主要总结系统中,主要的错误处理方式。

本文引用地址:

一、错误概念

1.1 错误分类

从严重性而言,程序错误可分为致命性和非致命性两类。对于致命性错误,无法执行恢复动作,最多只能在用户屏幕上打印出错消息或将其写入日志文件,然后终止程序;而对于非致命性错误,多数本质上是暂时的(如资源短缺),一般恢复动作是延迟一些时间后再次尝试。

从交互性而言,程序错误可分为用户错误和内部错误两类。用户错误呈现给用户,通常指明用户操作上的错误;而程序内部错误呈现给程序员(可能携带用户不可接触的数据细节),用于查错和排障。

应用程序开发者可决定恢复哪些错误以及如何恢复。例如,若磁盘已满,可考虑删除非必需或已过期的数据;若网络连接失败,可考虑短时间延迟后重建连接。选择合理的错误恢复策略,可避免应用程序的异常终止,从而改善其健壮性。

1.2 处理步骤

错误处理即处理程序运行时出现的任何意外或异常情况。典型的错误处理包含五个步骤:

  1. 程序执行时发生软件错误。该错误可能产生于被底层驱动或内核映射为软件错误的硬件响应事件(如除零)。

  2. 以一个错误指示符(如整数或结构体)记录错误的原因及相关信息。

  3. 程序检测该错误(读取错误指示符,或由其主动上报);

  4. 程序决定如何处理错误(忽略、部分处理或完全处理);

  5. 恢复或终止程序的执行。

上述步骤用代码表述如下:

int func(){
    int bIsErrOccur = 0;
    //do something that might invoke errors
    if(bIsErrOccur)  //Stage 1: error occurred
        return -1;   //Stage 2: generate error indicator
    //...
    return 0;
}
int main(void){
    if(func() != 0)  //Stage 3: detect error
    {
        //Stage 4: handle error
    }
    //Stage 5: recover or abort
    return 0;
}

调用者可能希望函数返回成功时表示完全成功,失败时程序恢复到调用前的状态(但被调函数很难保证这点)。

二、错误传递

2.1 返回值和回传参数

通常使用返回值来标志函数是否执行成功,调用者通过if等语句检查该返回值以判断函数执行情况。常见的几种调用形式如下:

if((p = malloc(100)) == NULL)
   //...
if((c = getchar()) == EOF)
   //...
if((ticks = clock()) < 0)
   //...

Unix系统调用级函数(和一些老的Posix函数)的返回值有时既包括错误代码也包括有用结果。因此,上述调用形式可在同一条语句中接收返回值并检查错误(当执行成功时返回合法的数据值)。

返回值方式的好处是简便和高效,但仍存在较多问题:

代码可读性降低

没有返回值的函数是不可靠的。但若每个函数都具有返回值,为保持程序健壮性,就必须对每个函数进行正确性验证,即调用时检查其返回值。这样,代码中很大一部分可能花费在错误处理上,且排错代码和正常流程代码搅在一起,比较混乱。

质量降级

条件语句相比其他类型的语句潜藏更多的错误。不必要的条件语句会增加排障和白盒测试的工作量。

信息有限

通过返回值只能返回一个值,因此一般只能简单地标志成功或失败,而无法作为获知具体错误信息的手段。通过按位编码可变通地返回多个值,但并不常用。

字符串处理函数可参考IntToAscii()来返回具体的错误原因,并支持链式表达:

char *IntToAscii(int dwVal, char *pszRes, int dwRadix){
    if(NULL == pszRes)
        return "Arg2Null";
    if((dwRadix < 2) || (dwRadix > 36))
        return "Arg3OutOfRange";
    //...
    return pszRes;
}

定义冲突

不同函数在成功和失败时返回值的取值规则可能不同。例如,Unix系统调用级函数返回0代表成功,-1代表失败;新的Posix函数返回0代表成功,非0代表失败;标准C库中isxxx函数返回1表示成功,0表示失败。

无约束性

调用者可以忽略和丢弃返回值。未检查和处理返回值时,程序仍然能够运行,但结果不可预知。

新的Posix函数返回值只携带状态和异常信息,并通过参数列表中的指针回传有用的结果。回传参数绑定到相应的实参上,因此调用者不可能完全忽略它们。通过回传参数(如结构体指针)可返回多个值,也可携带更多的信息。

综合返回值和回传参数的优点,可对Get类函数采用返回值(含有用结果)方式,而对Set类函数采用返回值+回传参数方式。

对于纯粹的返回值,可按需提供如下解析接口:

typedef enum{
    S_OK,                   //成功
    S_ERROR,                //失败(原因未明确),通用状态
    S_NULL_POINTER,         //入参指针为NULL
    S_ILLEGAL_PARAM,        //参数值非法,通用
    S_OUT_OF_RANGE,         //参数值越限
    S_MAX_STATUS            //不可作为返回值状态,仅作枚举最值使用
}FUNC_STATUS;
#define RC_NAME(eRetCode) 
    ((eRetCode) == S_OK                   ?    "Success"             : 
    ((eRetCode) == S_ERROR                ?    "Failure"             : 
    ((eRetCode) == S_NULL_POINTER         ?    "NullPointer"         : 
    ((eRetCode) == S_ILLEGAL_PARAM        ?    "IllegalParas"        : 
    ((eRetCode) == S_OUT_OF_RANGE         ?    "OutOfRange"          : 
      "Unknown")))))

当返回值错误码来自下游模块时,可能与本模块错误码冲突。此时,建议不要将下游错误码直接向上传递,以免引起混乱。若允许向终端或文件输出错误信息,则可详细记录出错现场(如函数名、错误描述、参数取值等),并转换为本模块定义的错误码再向上传递。

2.2 全局状态标志(errno)

Unix系统调用或某些C标准库函数出错时,通常返回一个负值,并设置全局整型变量errno为一个含有错误信息的值。例如,open函数出错时返回-1,并设置errno为EACESS(权限不足)等值。

C标准库头文件中定义errno及其可能的非零常量取值(以字符'E'开头)。在ANSI C中已定义一些基本的errno常量,操作系统也会扩展一部分(但其对错误描述仍显匮乏)。Linux系统中,出错常量在errno(3)手册页中列出,可通过man 3 errno命令查看。除EAGAIN和EWOULDBLOCK取值相同外,POSIX.1指定的所有出错编号取值均不同。

Posix和ISO C将errno定义为一个可修改的整型左值(lvalue),可以是包含出错编号的一个整数,或是一个返回出错编号指针的函数。以前使用的定义为:

extern int errno;

但在多线程环境中,多个线程共享进程地址空间,每个线程都有属于自己的局部errno(thread-local)以避免一个线程干扰另一个线程。例如,Linux支持多线程存取errno,将其定义为:

extern int *__errno_location(void);
#define errno (*__errno_location())

函数__ errno_location在不同的库版本下有不同的定义,在单线程版本中,直接返回全局变量errno的地址;而在多线程版本中,不同线程调用__errno_location返回的地址则各不相同。

C运行库中主要在math.h(数学运算)和stdio.h(I/O操作)头文件声明的函数中使用errno。

使用errno时应注意以下几点:

  1. 函数返回成功时,允许其修改errno。

例如,调用fopen函数新建文件时,内部可能会调用其他库函数检测是否存在同名文件。而用于检测文件的库函数在文件不存在时,可能会失败并设置errno。这样, fopen函数每次新建一个事先并不存在的文件时,即使没有任何程序错误发生(fopen本身成功返回),errno也仍然可能被设置。

因此,调用库函数时应先检测作为错误指示的返回值。仅当函数返回值指明出错时,才检查errno值:

//调用库函数
if(返回错误值)
    //检查errno

  1. 库函数返回失败时,不一定会设置errno,取决于具体的库函数。

  2. errno在程序开始时设置为0,任何库函数都不会将errno再次清零。

因此,在调用可能设置errno的运行库函数之前,最好先将errno设置为0。调用失败后再检查errno的值。

  1. 使用errno前,应避免调用其他可能设置errno的库函数。如:

if (somecall() == -1)
{
    printf("somecall() failedn");
    if(errno == ...) { ... }
}

somecall()函数出错返回时设置errno。但当检查errno时,其值可能已被printf()函数改变。

若要正确使用somecall()函数设置的errno,须在调用printf()函数前保存其值:

if (somecall() == -1)
{
    int dwErrSaved = errno;
    printf("somecall() failedn");
    if(dwErrSaved == ...) { ... }
}

类似地,当在信号处理程序中调用可重入函数时,应在其前保存其后恢复errno值。

  1. 使用现代版本的C库时,应包含使用头文件;在非常老的Unix 系统中,可能没有该头文件,此时可手工声明errno(如extern int errno)。

C标准定义strerror和perror两个函数,以帮助打印错误信息。

#include 
char *strerror(int errnum);

该函数将errnum(即errno值)映射为一个出错信息字符串,并返回指向该字符串的指针。可将出错字符串和其它信息组合输出到用户界面,或保存到日志文件中,如通过fprintf(fp, "somecall failed(%s)", strerror(errno))将错误消息打印到fp指向的文件中。

perror函数将当前errno对应的错误消息的字符串输出到标准错误(即stderr或2)上。

#include 
void perror(const char *msg);

该函数首先输出由msg指向的字符串(用户自己定义的信息),后面紧跟一个冒号和空格,然后是当前errno值对应的错误类型描述,最后是一个换行符。未使用重定向时,该函数输出到控制台上;若将标准错误输出重定向到/dev/null,则看不到任何输出。

文章来源于:电子产品世界    原文链接

本站所有转载文章系出于传递更多信息之目的,且明确注明来源,不希望被转载的媒体或个人可与我们联系,我们将立即进行删除处理。

我们与500+贴片厂合作,完美满足客户的定制需求。为品牌提供定制化的推广方案、专属产品特色页,多渠道推广,SEM/SEO精准营销以及与公众号的联合推广...详细>>

利用葫芦芯平台的卓越技术服务和新产品推广能力,原厂代理能轻松打入消费物联网(IOT)、信息与通信(ICT)、汽车及新能源汽车、工业自动化及工业物联网、装备及功率电子...详细>>

充分利用其强大的电子元器件采购流量,创新性地为这些物料提供了一个全新的窗口。我们的高效数字营销技术,不仅可以助你轻松识别与连接到需求方,更能够极大地提高“闲置物料”的处理能力,通过葫芦芯平台...详细>>

我们的目标很明确:构建一个全方位的半导体产业生态系统。成为一家全球领先的半导体互联网生态公司。目前,我们已成功打造了智能汽车、智能家居、大健康医疗、机器人和材料等五大生态领域。更为重要的是...详细>>

我们深知加工与定制类服务商的价值和重要性,因此,我们倾力为您提供最顶尖的营销资源。在我们的平台上,您可以直接接触到100万的研发工程师和采购工程师,以及10万的活跃客户群体...详细>>

凭借我们强大的专业流量和尖端的互联网数字营销技术,我们承诺为原厂提供免费的产品资料推广服务。无论是最新的资讯、技术动态还是创新产品,都可以通过我们的平台迅速传达给目标客户...详细>>

我们不止于将线索转化为潜在客户。葫芦芯平台致力于形成业务闭环,从引流、宣传到最终销售,全程跟进,确保每一个potential lead都得到妥善处理,从而大幅提高转化率。不仅如此...详细>>