嵌入式系统中错误的处理方式

strongerHuang 2024-09-11 08:20

关注+星标公众,不错过精彩内容

素材来源 | 网络

前言

本文主要总结嵌入式系统C语言编程中,主要的错误处理方式。文中涉及的代码运行环境如下:

一、错误概念

1.1 错误分类

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

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

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

1.2 处理步骤

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

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

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

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

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

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

上述步骤用C语言代码表述如下:

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 返回值和回传参数

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

if((p = malloc(100)) == NULL)
   //...

if((c = getchar()) == EOF)
   //...

if((ticks = clock()) < 0)
   //...

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

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

  1. 代码可读性降低

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

  1. 质量降级

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

  1. 信息有限

通过返回值只能返回一个值,因此一般只能简单地标志成功或失败,而无法作为获知具体错误信息的手段。通过按位编码可变通地返回多个值,但并不常用。字符串处理函数可参考IntToAscii()来返回具体的错误原因,并支持链式表达:

char *IntToAscii(int dwVal, char *pszRes, int dwRadix)
{
    if(NULL == pszRes)
        return "Arg2Null";

    if((dwRadix < 2) || (dwRadix > 36))
        return "Arg3OutOfRange";

    //...
    return pszRes;
}
  1. 定义冲突

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

  1. 无约束性

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

新的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() failed\n");
    if(errno == ...) { ... }
}

somecall()函数出错返回时设置errno。但当检查errno时,其值可能已被printf()函数改变。若要正确使用somecall()函数设置的errno,须在调用printf()函数前保存其值:

if (somecall() == -1)
{
    int dwErrSaved = errno;
    printf("somecall() failed\n");
    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,则看不到任何输出。

注意,perror()函数中errno对应的错误消息集合与strerror()相同。但后者可提供更多定位信息和输出方式。

两个函数的用法示例如下:

int main(int argc, char** argv)
{
    errno = 0;
    FILE *pFile = fopen(argv[1], "r");
    if(NULL == pFile)
    {
        printf("Cannot open file '%s'(%s)!\n", argv[1], strerror(errno));
        perror("Open file failed");
    }
    else
    {
        printf("Open file '%s'(%s)!\n", argv[1], strerror(errno));
        perror("Open file");
        fclose(pFile);
    }

    return 0;
}

执行结果为:

[wangxiaoyuan_@localhost test1]$ ./GlbErr /sdb1/wangxiaoyuan/linux_test/test1/test.c
Open file '/sdb1/wangxiaoyuan/linux_test/test1/test.c'(Success)!
Open file: Success
[wangxiaoyuan_@localhost test1]$ ./GlbErr NonexistentFile.h
Cannot open file 'NonexistentFile.h'(No such file or directory)!
Open file failed: No such file or directory
[wangxiaoyuan_@localhost test1]$ ./GlbErr NonexistentFile.h > test
Open file failed: No such file or directory
[wangxiaoyuan_@localhost test1]$ ./GlbErr NonexistentFile.h 2> test
Cannot open file 'NonexistentFile.h'(No such file or directory)!

也可仿照errno的定义和处理,定制自己的错误代码:

int *_fpErrNo(void)
{
   static int dwLocalErrNo = 0;
   return &dwLocalErrNo;
}

#define ErrNo (*_fpErrNo())
#define EOUTOFRANGE  1
//define other error macros...

int Callee(void)
{
    ErrNo = 1;
    return -1;
}

int main(void)
{
    ErrNo = 0;
    if((-1 == Callee()) && (EOUTOFRANGE == ErrNo))
        printf("Callee failed(ErrNo:%d)!\n", ErrNo);
    return 0;
}

借助全局状态标志,可充分利用函数的接口(返回值和参数表)。但与返回值一样,它隐含地要求调用者在调用函数后检查该标志,而这种约束同样脆弱。

此外,全局状态标志存在重用和覆盖的风险。而函数返回值是无名的临时变量,由函数产生且只能被调用者访问。调用完成后即可检查或拷贝返回值,然后原始的返回对象将消失而不能被重用。又因为无名,返回值不能被覆盖。

2.3 局部跳转(goto)

使用goto语句可直接跳转到函数内的错误处理代码处。以除零错误为例:

double Division(double fDividend, double fDivisor)
{
    return fDividend/fDivisor;
}
int main(void)
{
    int dwFlag = 0;
    if(1 == dwFlag)
    {
    RaiseException:
        printf("The divisor cannot be 0!\n");
        exit(1);
    }
    dwFlag = 1;

    double fDividend = 0.0, fDivisor = 0.0;
    printf("Enter the dividend: ");
    scanf("%lf", &fDividend);
    printf("Enter the divisor : ");
    scanf("%lf", &fDivisor);
    if(0 == fDivisor) //不太严谨的浮点数判0比较
        goto RaiseException;
    printf("The quotient is %.2lf\n", Division(fDividend, fDivisor));

    return 0;
}

执行结果如下:

[wangxiaoyuan_@localhost test1]$ ./test
Enter the dividend: 10
Enter the divisor : 0
The divisor cannot be 0!
[wangxiaoyuan_@localhost test1]$ ./test
Enter the dividend: 10
Enter the divisor : 2
The quotient is 5.00

虽然goto语句会破坏代码结构性,但却非常适用于集中错误处理。伪代码示例如下:

CallerFunc()
{
    if((ret = CalleeFunc1()) < 0);
        goto ErrHandle;
    if((ret = CalleeFunc2()) < 0);
        goto ErrHandle;
    if((ret = CalleeFunc3()) < 0);
        goto ErrHandle;
    //...

    return;

ErrHandle:
    //Handle Error(e.g. printf)
    return;
}

2.4 非局部跳转(setjmp/longjmp)

局部goto语句只能跳到所在函数内部的标号上。若要跨越函数跳转,需要借助标准C库提供非局部跳转函数setjmp()和longjmp()。它们分别承担非局部标号和goto的作用,非常适用于处理发生在深层嵌套函数调用中的出错情况。“非局部跳转”是在栈上跳过若干调用帧,返回到当前函数调用路径上的某个函数内。

#include 
int setjmp(jmp_buf env);
void longjmp(jmp_buf env,int val);

函数setjmp()将程序运行时的当前系统堆栈环境保存在缓冲区env结构中。初次调用该函数时返回值为0。longjmp()函数根据setjmp()所保存的env结构恢复先前的堆栈环境,即“跳回”先前调用setjmp时的程序执行点。此时,setjmp()函数返回longjmp()函数所设置的参数val值,程序将继续执行setjmp调用后的下一条语句(仿佛从未离开setjmp)。参数val为非0值,若设置为0,则setjmp()函数返回1。

可见,setjmp()有两类返回值,用于区分是首次直接调用(返回0)和还是由其他地方跳转而来(返回非0值)。对于一个setjmp可有多个longjmp,因此可由不同的非0返回值区分这些longjmp。

举个简单例子说明 setjmp/longjmp的非局部跳转:

jmp_buf gJmpBuf;
void Func1(){
    printf("Enter Func1\n");
    if(0)longjmp(gJmpBuf, 1);
}
void Func2(){
    printf("Enter Func2\n");
    if(0)longjmp(gJmpBuf, 2);
}
void Func3(){
    printf("Enter Func3\n");
    if(1)longjmp(gJmpBuf, 3);
}

int main(void)
{
    int dwJmpRet = setjmp(gJmpBuf);
    printf("dwJmpRet = %d\n", dwJmpRet);
    if(0 == dwJmpRet)
    {
        Func1();
        Func2();
        Func3();
    }
    else
    {
        switch(dwJmpRet)
        {
            case 1:
                printf("Jump back from Func1\n");
            break;
            case 2:
                printf("Jump back from Func2\n");
            break;
            case 3:
                printf("Jump back from Func3\n");
            break;
            default:
                printf("Unknown Func!\n");
            break;
        }
    }
    return 0;
}

执行结果为:

dwJmpRet = 0
Enter Func1
Enter Func2
Enter Func3
dwJmpRet = 3
Jump back from Func3

当setjmp/longjmp嵌在单个函数中使用时,可模拟PASCAL语言中嵌套函数定义(即函数内中定义一个局部函数)。当setjmp/longjmp跨越函数使用时,可模拟面向对象语言中的异常(exception) 机制。

模拟异常机制时,首先通过setjmp()函数设置一个跳转点并保存返回现场,然后使用try块包含那些可能出现错误的代码。可在try块代码中或其调用的函数内,通过longjmp()函数抛出(throw)异常。抛出异常后,将跳回setjmp()函数所设置的跳转点并执行catch块所包含的异常处理程序。

以除零错误为例:

jmp_buf gJmpBuf;
void RaiseException(void)
{
   printf("Exception is raised: ");
   longjmp(gJmpBuf, 1);  //throw,跳转至异常处理代码
   printf("This line should never get printed!\n");
}
double Division(double fDividend, double fDivisor)
{
    return fDividend/fDivisor;
}
int main(void)
{
    double fDividend = 0.0, fDivisor = 0.0;
    printf("Enter the dividend: ");
    scanf("%lf", &fDividend);
    printf("Enter the divisor : ");
    if(0 == setjmp(gJmpBuf))  //try块
    {
        scanf("%lf", &fDivisor);
        if(0 == fDivisor) //也可将该判断及RaiseException置于Division内
            RaiseException();
        printf("The quotient is %.2lf\n", Division(fDividend, fDivisor));
    }
    else  //catch块(异常处理代码)
    {
        printf("The divisor cannot be 0!\n");
    }

    return 0;
}

执行结果为:

Enter the dividend: 10
Enter the divisor : 0
Exception is raised: The divisor cannot be 0!

通过组合使用setjmp/longjmp函数,可对复杂程序中可能出现的异常进行集中处理。根据longjmp()函数所传递的返回值来区分处理各种不同的异常。

使用setjmp/longjmp函数时应注意以下几点:

  1. 必须先调用setjmp()函数后调用longjmp()函数,以恢复到先前被保存的程序执行点。若调用顺序相反,将导致程序的执行流变得不可预测,很容易导致程序崩溃。

  2. longjmp()函数必须在setjmp()函数的作用域之内。在调用setjmp()函数时,它保存的程序执行点环境只在当前主调函数作用域以内(或以后)有效。若主调函数返回或退出到上层(或更上层)的函数环境中,则setjmp()函数所保存的程序环境也随之失效(函数返回时堆栈内存失效)。这就要求setjmp()不可该封装在一个函数中,若要封装则必须使用宏(详见《C语言接口与实现》“第4章 异常与断言”)。

  3. 通常将jmp_buf变量定义为全局变量,以便跨函数调用longjmp。

  4. 通常,存放在存储器中的变量将具有longjmp时的值,而在CPU和浮点寄存器中的变量则恢复为调用setjmp时的值。因此,若在调用setjmp和longjmp之间修改自动变量或寄存器变量的值,当setjmp从longjmp调用返回时,变量将维持修改后的值。若要编写使用非局部跳转的可移植程序,必须使用volatile属性。

  5. 使用异常机制不必每次调用都检查一次返回值,但因为程序中任何位置都可能抛出异常,必须时刻考虑是否捕捉异常。在大型程序中,判断是否捕捉异常会是很大的思维负担,影响开发效率。相比之下,通过返回值指示错误有利于调用者在最近出错的地方进行检查。此外,返回值模式中程序的运行顺序一目了然,对维护者可读性更高。因此,应用程序中不建议使用setjmp/longjmp“异常处理”机制(除非库或框架)。

2.5 信号(signal/raise)

在某些情况下,主机环境或操作系统可能发出信号(signal)事件,指示特定的编程错误或严重事件(如除0或中断等)。这些信号本意并非用于错误捕获,而是指示与正常程序流不协调的外部事件。

为处理信号,需要使用以下信号相关函数:

#include 
typedef void (*fpSigFunc)(int);
fpSigFunc signal(int signo, fpSigFunc fpHandler);
int raise(int signo);

其中,参数signo是Unix系统定义的信号编号(正整数),不允许用户自定义信号。参数fpHandler是常量SIG_DFL、常量SIG_IGN或当接收到此信号后要调用的信号处理函数(signal handler)的地址。若指定SIG_DFL,则接收到此信号后调用系统的缺省处理函数;若指定SIG_ IGN,则向内核表明忽略此信号(SIGKILL和SIGSTOP不可忽略)。某些异常信号(如除数为零)不太可能恢复,此时信号处理函数可在程序终止前正确地清理某些资源。信号处理函数所收到的异常信息仅是一个整数(待处理的信号事件),这点与setjmp()函数类似。

signal()函数执行成功时返回前次挂接的处理函数地址,失败时则返回SIG_ERR。信号通过调用raise()函数产生并被处理函数捕获。

以除零错误为例:

void fphandler(int dwSigNo)
{
    printf("Exception is raised, dwSigNo=%d!\n", dwSigNo);
}
int main(void)
{
    if(SIG_ERR == signal(SIGFPE, fphandler))
    {
        fprintf(stderr"Fail to set SIGFPE handler!\n");
        exit(EXIT_FAILURE);
    }

    double fDividend = 10.0, fDivisor = 0.0;
    if(0 == fDivisor)
    {
        raise(SIGFPE);
        exit(EXIT_FAILURE);
    }
    printf("The quotient is %.2lf\n", fDividend/fDivisor);

    return 0;
}

执行结果为"Exception is raised, dwSigNo=8!"(0.0不等同于0,因此系统未检测到浮点异常)。

若将被除数(Dividend)和除数(Divisor)改为整型变量:

int main(void)
{
    if(SIG_ERR == signal(SIGFPE, fphandler))
    {
        fprintf(stderr"Fail to set SIGFPE handler!\n");
        exit(EXIT_FAILURE);
    }

    int dwDividend = 10, dwDivisor = 0;
    double fQuotient = dwDividend/dwDivisor;
    printf("The quotient is %.2lf\n", fQuotient);

    return 0;
}

则执行后循环输出"Exception is raised, dwSigNo=8!"。这是因为进程捕捉到信号并对其进行处理时,进程正在执行的指令序列被信号处理程序临时中断,它首先执行该信号处理程序中的指令。若从信号处理程序返回(未调用exit或longjmp),则继续执行在捕捉到信号时进程正在执行的正常指令序列。因此,每次系统调用信号处理函数后,异常控制流还会返回除0指令继续执行。而除0异常不可恢复,导致反复输出异常。

规避方法有两种:

  1. 将SIGFPE信号变成系统默认处理,即signal(SIGFPE, SIG_DFL)。

此时执行输出为"Floating point exception"。

  1. 利用setjmp/longjmp跳过引发异常的指令:
jmp_buf gJmpBuf;
void fphandler(int dwSigNo)
{
    printf("Exception is raised, dwSigNo=%d!\n", dwSigNo);
    longjmp(gJmpBuf, 1);
}
int main(void)
{
    if(SIG_ERR == signal(SIGFPE, SIG_DFL))
    {
        fprintf(stderr"Fail to set SIGFPE handler!\n");
        exit(EXIT_FAILURE);
    }

    int dwDividend = 10, dwDivisor = 0;
    if(0 == setjmp(gJmpBuf))
    {
        double fQuotient = dwDividend/dwDivisor;
        printf("The quotient is %.2lf\n", fQuotient);
    }
    else
    {
        printf("The divisor cannot be 0!\n");
    }

    return 0;
}

注意,在信号处理程序中还可使用sigsetjmp/siglongjmp函数进行非局部跳转。相比setjmp函数,sigsetjmp函数增加一个信号屏蔽字参数。

三  错误处理

3.1 终止(abort/exit)

致命性错误无法恢复,只能终止程序。例如,当空闲堆管理程序无法提供可用的连续空间时(调用malloc返回NULL),用户程序的健壮性将严重受损。若恢复的可能性渺茫,则最好终止或重启程序。

标准C库提供exit()和abort()函数,分别用于程序正常终止和异常终止。两者都不会返回到调用者中,且都导致程序被强行结束。

exit()及其相似函数原型声明如下:

#include 
void exit(int status);
void _Exit(int status);
#include 
void _exit(int status);

其中,exit和_Exit由ISO C说明,而_exit由Posix.1说明。因此使用不同的头文件。

ISO C定义_Exit旨在为进程提供一种无需运行终止处理程序(exit handler)或信号处理程序(signal handler)而终止的方法,是否冲洗标准I/O流则取决于实现。Unix系统中_Exit 和_exit同义,两者均直接进入内核,而不冲洗标准I/O流。_exit函数由exit调用,处理Unix特定的细节。

exit()函数首先调用执行各终止处理程序,然后按需多次调用fclose函数关闭所有已打开的标准I/O流(将所有缓冲的输出数据冲洗写到文件上),然后调用_exit函数进入内核。

标准函数库中有一种“缓冲I/O(buffered I/O)”机制。该机制对于每个打开的文件,在内存中维护一片缓冲区。每次读文件时会连续读出若干条记录,下次读文件时就可直接从内存缓冲区中读取;每次写文件时也仅仅写入内存缓冲区,等满足一定条件(如缓冲区填满,或遇到换行符等特定字符)时再将缓冲区内容一次性写入文件。

通过尽可能减少read和write调用的次数,该机制可显著提高文件读写速度,但也给编程带来某些麻烦。例如,向文件内写入一些数据时,若未满足特定条件,数据会暂存在缓冲区内。开发者并不知晓这点,而调用_ exit()函数直接关闭进程,导致缓冲区数据丢失。因此,若要保证数据完整性,必须调用exit()函数,或在调用 _exit()函数前先通过fflush()函数将缓冲区内容写入指定的文件。

例如,调用printf函数(遇到换行符'\n'时自动读出缓冲区中内容)函数后再调用exit:

int main(void)
{
    printf("Using exit...\n");
    printf("This is the content in buffer");
    exit(0);
    printf("This line will never be reached\n");
}

执行输出为:

Using exit...
This is the content in buffer(结尾无换行符)

调用printf函数后再调用_exit:

int main(void)
{
    printf("Using _exit...\n");
    printf("This is the content in buffer");
    fprintf(stdout"Standard output stream");
    fprintf(stderr"Standard error stream");
    //fflush(stdout);
    _exit(0);
}

执行输出为:

Using _exit...
Standard error stream(结尾无换行符)

若取消fflush句注释,则执行输出为:

Using _exit...
Standard error streamThis is the content in bufferStandard output stream(结尾无换行符)

通常,标准错误是不带缓冲的,打开至终端设备的流(如标准输入和标准输出)是行缓冲的(遇换行符则执行I/O操作);其他所有流则是全缓冲的(填满标准I/O缓冲区后才执行I/O操作)。

三个exit函数都带有一个整型参数status,称之为终止状态(或退出状态)。该参数取值通常为两个宏,即EXIT_SUCCESS(0)和EXIT_FAILURE(1)。大多数Unix shell都可检查进程的终止状态。若(a)调用这些函数时不带终止状态,或(b)main函数执行了无返回值的return语句,或(c) main函数未声明返回类型为整型,则该进程的终止状态未定义。但若main函数的返回类型为整型,且执行到最后一条语句时返回(隐式返回),则该进程的终止状态为0。

exit系列函数是最简单直接的错误处理方式,但程序出错终止时无法捕获异常信息。ISO C规定一个进程可以注册32个终止处理函数。这些函数可编写为自定义的清理代码,将由exit()函数自动调用,并可使用atexit()函数进行注册。

#include 
int atexit(void (*func)(void));

该函数的参数是一个无参数无返回值的终止处理函数。exit()函数按注册的相反顺序调用这些函数。同一函数若注册多次,则被调用多次。即使不调用exit函数,程序退出时也会执行atexit注册的函数。

通过结合exit()和atexit()函数,可在程序出错终止时抛出异常信息。以除零错误为例:

double Division(double fDividend, double fDivisor)
{
    return fDividend/fDivisor;
}
void RaiseException1(void)
{
    printf("Exception is raised: \n");
}
void RaiseException2(void)
{
    printf("The divisor cannot be 0!\n");
}

int main(void)
{
    double fDividend = 0.0, fDivisor = 0.0;
    printf("Enter the dividend: ");
    scanf("%lf", &fDividend);
    printf("Enter the divisor : ");
    scanf("%lf", &fDivisor);
    if(0 == fDivisor)
    {
        atexit(RaiseException2);
        atexit(RaiseException1);
        exit(EXIT_FAILURE);
    }
    printf("The quotient is %.2lf\n", Division(fDividend, fDivisor));

    return 0;
}

执行结果为:

Enter the dividend: 10
Enter the divisor : 0
Exception is raised: 
The divisor cannot be 0!

注意,通过atexit()注册的终止处理函数必须显式(使用return语句)或隐式地正常返回,而不能通过调用exit()或longjmp()等其他方式终止,否则将导致未定义的行为。例如,在GCC4.1.2编译环境下,调用exit()终止时仍等效于正常返回;而VC6.0编译环境下,调用exit()的处理函数将阻止其他已注册的处理函数被调用,并且可能导致程序异常终止甚至崩溃。

嵌套调用exit()函数将导致未定义的行为,因此在终止处理函数或信号处理函数中尽量不要调用exit()。

abort()函数原型声明如下:

#include 
void abort(void);

该函数将SIGABRT信号发送给调用进程(进程不应忽略此信号)。

ISO C规定,调用abort将向主机环境递送一个未成功终止的通知,其方法是调用raise(SIGABRT)函数。因此,abort()函数理论上的实现为:

void abort(void)
{
    raise(SIGABRT);
    exit(EXIT_FAILURE);
}

可见,即使捕捉到SIGABRT信号且相应信号处理程序返回,abort()函数仍然终止程序。Posix.1也说明abort()函数并不理会进程对此信号的阻塞和忽略。

进程捕捉到SIGABRT信号后,可在其终止之前执行所需的清理操作(如调用exit)。若进程不在信号处理程序中终止自己,Posix.1声明当信号处理程序返回时,abort()函数终止该进程。

ISO C规定,abort()函数是否冲洗输出流、关闭已打开文件及删除临时文件由实现决定。Posix.1则要求若abort()函数终止进程,则它对所有打开标准I/O流的效果应当与进程终止前对每个流调用fclose相同。为提高可移植性,若希望冲洗标准I/O流,则应在调用abort()之前执行这种操作。

3.2 断言(assert)

abort()和exit()函数无条件终止程序。也可使用断言(assert)有条件地终止程序。

assert是诊断调试程序时经常使用的宏,定义在内。该宏的典型实现如下:

#ifdef    NDEBUG
    #define assert(expr)        ((void) 0)
#else
    extern void __assert((const char *, const char *, intconst char *));
    #define assert(expr) \
        ((void) ((expr) || \
         (__assert(#expr, __FILE__, __LINE__, __FUNCTION__), 0)))

#endif

可见,assert宏仅在调试版本(未定义NDEBUG)中有效,且调用__assert()函数。该函数将输出发生错误的文件名、代码行、函数名以及条件表达式:

void __assert(const char *assertion, const char * filename,
              int linenumber, register const char * function)
{
    fprintf(stderr" [%s(%d)%s] Assertion '%s' failed.\n",
            filename, linenumber,
            ((function == NULL) ? "UnknownFunc" : function),
            assertion);
    abort();
}

因此,assert宏实际上是一个带有错误说明信息的abort(),并做了前提条件检查。若检查失败(断言表达式为逻辑假),则报告错误并终止程序;否则继续执行后面的语句。

使用者也可按需定制assert宏。例如,另一实现版本为:

#undef assert
#ifdef NDEBUG
    #define assert(expr)        ((void) 0)
#else
    #define assert(expr)        ((void) ((expr) || \
         (fprintf(stderr, "[%s(%d)] Assertion '%s' failed.\n", \
         __FILE__, __LINE__, #expr), abort(), 0)))

#endif

注意,expr1||expr2表达式作为单独语句出现时,等效于条件语句if(!(expr1))expr2。这样,assert宏就可扩展为一个表达式,而不是一条语句。逗号表达式expr2返回最后一个表达式的值(即0),以符合||操作符的要求。

使用断言时应注意以下几点:

  1. 断言用于检测理论上绝不应该出现的情况,如入参指针为空、除数为0等。

对比以下两种情况:

char *Strcpy(char *pszDst, const char *pszSrc)
{
    char *pszDstOrig = pszDst;
    assert((pszDst != NULL) && (pszSrc != NULL));
    while((*pszDst++ = *pszSrc++) != '\0');
        return pszDstOrig;
}
FILE *OpenFile(const char *pszName, const char *pszMode)
{
    FILE *pFile = fopen(pszName, pszMode);
    assert(pFile != NULL);
    if(NULL == pFile)
        return NULL;

    //...
    return pFile;
}

Strcpy()函数中断言使用正确,因为入参字符串指针不应为空。OpenFile()函数中则不能使用断言,因为用户可能需要检查某个文件是否存在,而这并非错误或异常。

2)assert是宏不是函数,在调试版本和非调试版本中行为不同。因此必须确保断言表达式的求值不会产生副作用,如修改变量和改变方法的返回值。不过,可根据这一副作用测试断言是否打开:

int main(void)
{
    int dwChg = 0;
    assert(dwChg = 1);
    if(0 == dwChg)
        printf("Assertion should be enabled!\n");
    return 0;
}
  1. 不应使用断言检查公共方法的参数(应使用参数校验代码),但可用于检查传递给私有方法的参数。

  2. 可使用断言测试方法执行的前置条件和后置条件,以及执行前后的不变性。

  3. 断言条件不成立时,会调用abort()函数终止程序,应用程序没有机会做清理工作(如关闭文件和数据库)。

3.3 封装

为减少错误检查和处理代码的重复性,可对函数调用或错误输出进行封装。

  1. 封装具有错误返回值的函数

通常针对频繁调用的基础性系统函数,如内存和内核对象操作等。举例如下:

pid_t Fork(void) //首字母大写,以区分系统函数fork()
{
    pid_t pid;
    if((pid = fork())<0)
    {
        fprintf(stderr"Fork error: %s\n", strerror(errno));
        exit(0);
    }
    return pid;
}

Fork()函数出错退出时依赖系统清理资源。若还需清理其他资源(如已创建的临时文件),可增加一个负责清理的回调函数。

注意,并非所有系统函数都可封装,应根据具体业务逻辑确定。

  1. 封装错误输出

通常需要使用ISO C变长参数表特性。例如《Unix网络编程》中将输出至标准出错文件的代码封装如下:

#include 
#include 
#define HAVE_VSNPRINTF  1
#define MAXLINE         4096  /* max text line length */
int daemon_proc;  /* set nonzero by daemon_init() */
static void err_doit(int errnoflag, int level, const char * fmt, va_list ap)
{
    int errno_save, n;
    char buf[MAXLINE + 1];

    errno_save = errno;    /* Value caller might want printed. */
#ifdef HAVE_VSNPRINTF
    vsnprintf(buf, MAXLINE, fmt, ap);
#else
    vsprintf(buf, fmt, ap);    /* This is not safe */
#endif
    n = strlen(buf);
    if (errnoflag) {
        snprintf(buf + n, MAXLINE - n, ": %s", strerror(errno_save));
    }
    strcat(buf, "\n");

    if (daemon_proc) {
        syslog(level, buf);
    } else {
        fflush(stdout);    /* In case stdout and stderr are the same */
        fputs(buf, stderr);
        fflush(stderr);
    }

    return;
}

void err_ret(const char * fmt, ...)
{
    va_list ap;

    va_start(ap, fmt);
    err_doit(1, LOG_INFO, fmt, ap);
    va_end(ap);

    return;
}
来源地址:
https://www.cnblogs.com/clover-toeic/p/3919857.html
声明:本文素材来源网络,版权归原作者所有。如涉及作品版权问题,请与我联系删除。

------------ END ------------


●专栏《嵌入式工具
●专栏《嵌入式开发》
●专栏《Keil教程》
●嵌入式专栏精选教程

关注公众号回复“加群”按规则加入技术交流群,回复“1024”查看更多内容。

点击“阅读原文”查看更多分享。

strongerHuang 作者黄工,高级嵌入式软件工程师,分享嵌入式软硬件、物联网、单片机、开发工具、电子等内容。
评论 (0)
  • 在SMT贴片加工时,可能会出现焊接不良现象,这些现象会影响焊接工艺品质,最直观的表现就是产品品质下降,所以要对这类现象分析,并加以解决。下面就由贴片加工厂_安徽英特丽小编为大家分析一下焊接时出现焊点拉尖现象吧,一起看下去吧。 一、PCBA焊点拉尖的含义 焊点拉尖是指PCB板在进行焊接加工后,焊盘上的焊点上有明显的尖锐突出,这种现象被叫做焊点拉尖。 二、PCBA焊点拉尖的原因 1、手工焊接时:烙铁头的温度过低,导致锡丝受热不足,表现为可以融化但是不能够湿润焊点; 在焊料融化后未完全固化后过早地
    贴片加工小安 2024-10-14 15:55 97浏览
  •     电容器是四大基本元件之一。从低频到高频,电容器都扮演重要角色。高中物理不讲电容器,要到了大学在电工学或者模电这样的基础课上才开始接触。我认为对电容器的了解程度可以判断一个工程师的知识、经验水平。    根据GJB 8118的分类法,电容器分类如下:    工作中会混合上面的分类法,把电容器分为陶瓷电容、电解电容、膜电容等。下面分别介绍。 陶瓷电容器 Ceramic Capacitor    陶瓷电容
    电子知识打边炉 2024-10-14 22:56 57浏览
  •         手动探针台、半自动探针台和全自动探针台是三种不同类型的探针台,它们在使用类型、功能、操作方式和价格等方面都有所不同。        手动探针台是一种手动控制的探针台,通常用于没有很多待测器件需要测量或数据需要收集的情况下。该类探针台的优点是灵活、可变性高,易于配置环境和转换测试环境,并且不需要涉及额外培训和设置时间的电子设备、PC或软件。手动探针台系统只需要少量的培训,因此非常适合研发人员使用。&nb
    锦正茂科技 2024-10-14 13:05 75浏览
  • 在现代社会,不间断电源(UPS)系统已成为保障关键设备和数据安全的关键设施,广泛应用于企业数据中心、家庭电子设备等场景。UPS能在电力中断或波动时提供稳定电力,确保设备持续运行。而在这套系统中,光耦(光电耦合器)作为核心组件,发挥着至关重要的作用。光耦在UPS中的核心价值电气隔离,保障安全:UPS系统中,直流电源与交流负载间需实现良好的电气隔离。光耦能有效隔离高压与低压电路,防止高电压损坏控制电路,特别是在电源故障或短路时,更能保护敏感设备和系统,提升整体安全性。信号传输精准:UPS需实时监测电
    晶台光耦 2024-10-14 10:08 67浏览
  • 在当今快速发展的自动驾驶技术领域,传感器的作用日益凸显,它们是实现车辆环境感知的基石。其中,毫米波雷达因其独特的优势,已成为自动驾驶传感器套件中不可或缺的一部分。这种雷达不仅能够在各种恶劣的天气条件下稳定工作,还能提供精确的距离和速度信息,这对于车辆的安全导航至关重要。一、毫米波雷达概述RADAR(RAdio Dectecting And Ranging)是指利用毫米波信号(30-300GHz)来探测和测量目标的雷达系统,其中毫米波是微波的一个子频段。在汽车领域,使用的毫米波雷达主要在24GHz
    康谋 2024-10-15 10:36 113浏览
  • 概述 Altera被Intel收购后,开发软件改名叫“Quartus Prime”,且分两个分支,一个叫Standard(即标准版),另一个叫Pro(即专业版)。官网下载基于Cyclone 10 GX的PCIe参考设计,需要使用专业版,这些参考设计和资料如图1所示。图1:从官网下载到的资料与参考设计 依据提示,双击图1中的.par文件,Quartus就会启动并打开该项目,如图2所示。不过问题是双击后自动启动的Quartus的是当前电脑中安装的标准版。标准版不支持10代GX器件,所以这样打开是行不
    coyoo 2024-10-14 14:10 73浏览
  • 本文介绍瑞芯微RK3562/RK3568在Android11系统OTA(U盘/TF卡)升级固件方法,使用的是触觉智能的Purple Pi OH鸿蒙开源主板,搭载了瑞芯微RK3566芯片,类树莓派设计,是Laval官方社区主荐的一款鸿蒙开发主板。1、OTA包生成在源码根目录上执行以下命令编译OTA包# make installclean# make -j4# make otapackage -j4根编译成功后会在out/target/product/rk3566_r/目录下生成rk3566_r-o
    Industio_触觉智能 2024-10-12 13:11 83浏览
  •  在很多的应用场景都有电池供电,同时也有充电器同时存在的场景,这种供电系统希望在没有充电器的时候,为了保证充电头不带电就需要防止电流倒灌的产品比如肖特基或者是理想二极管,通常在小电流情况下比如1A以及一下的场景,肖特基便宜,不在乎功耗,但是当充电电流比较大的时候,肖特基压降明显的降低了效率,产生了很大的热损耗,给工程师散热造成了很大的挑战,这时候就需要用到理想二极管。无锡明芯微提供了多种选择的理想二极管控制和模块,覆盖不同的电压需求。 当有充电器在的时候,为了不让电池供电,提高电池的使
    王萌 2024-10-12 14:34 236浏览
  • 2024年10月14日  Global Info Research行业调研机构发布的《全球工业多级油雾收集器行业总体规模、主要厂商及IPO上市调研报告,2024-2030》分析了全球工业多级油雾收集器总体规模,主要地区规模,主要企业规模和份额,主要产品分类规模,下游主要应用规模等。统计维度包括收入和市场份额等。不仅全面分析全球范围内主要企业竞争态势,收入和市场份额等。同时也重点分析全球市场主要厂商(品牌)产品特点、产品规格、收入、毛利率及市场份额、及发展动态。历史数据为2019至202
    GIRtina 2024-10-14 13:36 95浏览
  • 1.  背景、挑战与业界进展1.1. 虚拟化技术背景在云计算的浪潮中,虚拟化技术扮演着举足轻重的角色。它通过将物理机集群的资源进行抽象整合,构建出一个统一的虚拟资源池。在这个资源池中,每一台物理机上都会运行宿主机以及虚拟化的系统软件。这些系统软件负责为用户提供虚拟的计算环境,包括虚拟CPU、虚拟内存、虚拟网络等,都是由Hypervisor这一核心组件来实现的。Hypervisor不仅确保了虚拟机计算能力的平稳性和弹性,还为用户提供了灵活的资源调度和分配能力。虚拟化技术在云数据中心、大数
    Yusur_Tech 2024-10-14 14:58 104浏览
  • 晶体管光耦是一种常用的光电耦合器,用于隔离和传输电信号,它包含有光电发射管和接收晶体管两个主要部分。解读晶体管光耦的主要指标有助于了解其性能和应用范围,主要指标包括:最大工作电压(V_R_MAX):这是发射管能够承受的最大工作电压,超过该电压会导致器件损坏。最大工作电流(I_F_MAX):这是发射管的最大工作电流,超过该电流可能会导致器件过载或损坏。电流传输比(CTR,Current Transfer Ratio):传输比指的是输出电流与输入电流之比,通常以百分比(%)表示。传输比越高,表示光电
    晶台光耦 2024-10-15 14:10 57浏览
  • 10月12日,钱塘江畔,「极」系列高性能电摩迎来首发。作为首款「天能INSIDE」车型,「极」系列由天能与五星钻豹联合打造,搭载天能微纳铅碳大动力电池,在动力、续航及充电速率等方面展露出越级表现。动力性能领先单只电池多12片极板:采用高密度集群技术,通过矩阵式集群结构设计,帮助电池在空间不增加的情况下,装入更多极板,扩大活性物质反应面积,动力性能较同型号普通电池提升30%。电摩也能“涡轮增压”:微纳铅碳关键技术的应用,相当于在电池内部添加了法拉第-超级电容双功能内混型铅碳负极,使电池兼具超级电容
    电子资讯报 2024-10-14 14:50 95浏览
  • 10月12日,以“技术引领筑生态,万物智联创未来”为主题的第三届OpenHarmony技术大会(以下简称“大会”)在上海成功举办。本次大会由OpenHarmony项目群技术指导委员会(TSC)主办,华为、上海交通大学、中国科学院软件研究所等单位支持,邀请了来自全球的开源操作系统技术领袖、前沿实践专家、广大开发者以及学术界大咖,面向全球展示了OpenHarmony的最新技术、生态、人才进展与行业实践,同时探讨了开源生态未来发展方向以及万物智联时代下操作系统的演进趋势,为下一代操作系统发展指明了前进
    Industio_触觉智能 2024-10-15 09:12 60浏览
我要评论
0
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦