嵌入式C编程之错误处理(附代码例子)

嵌入式大杂烩 2020-11-26 00:00

作者:clover-toeic

原文:https://www.cnblogs.com/clover-toeic/p/3919857.html

前言

本文主要总结嵌入式系统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.h>中定义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库时,应包含使用<errno.h>头文件;在非常老的Unix 系统中,可能没有该头文件,此时可手工声明errno(如extern int errno)。

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

#include <string.h>
char *strerror(int errnum);

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

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

#include <stdio.h>
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 <setjmp.h>
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 <signal.h>
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 <stdlib.h>
void exit(int status);
void _Exit(int status);
#include <unistd.h>
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 <stdlib.h>
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 <stdlib.h>
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是诊断调试程序时经常使用的宏,定义在<assert.h>内。该宏的典型实现如下:

#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 <stdarg.h>
#include <syslog.h>
#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;
}


免责声明:本文来源网络,免费传达知识,版权归原作者所有。如涉及作品版权问题,请联系我进行删除。

最后

以上就是本次的分享,如果觉得文章不错,转发、在看,也是我们继续更新得动力。

猜你喜欢:

一个不该被遗忘的打印输出函数

为什么Linux内核里大量使用goto,而很多书籍却不提倡使用?

1024G 嵌入式资源大放送!包括但不限于C/C++、单片机、Linux等。在公众号聊天界面回复1024,即可免费获取!


嵌入式大杂烩 专注于嵌入式技术,包括但不限于C/C++、嵌入式、物联网、Linux等编程学习笔记,同时,内包含大量的学习资源。欢迎关注,一同交流学习,共同进步!
评论
  • By Toradex 秦海1). 简介嵌入式平台设备基于Yocto Linux 在开发后期量产前期,为了安全以及提高启动速度等考虑,希望将 ARM 处理器平台的 Debug Console 输出关闭,本文就基于 NXP i.MX8MP ARM 处理器平台来演示相关流程。 本文所示例的平台来自于 Toradex Verdin i.MX8MP 嵌入式平台。  2. 准备a). Verdin i.MX8MP ARM核心版配合Dahlia载板并
    hai.qin_651820742 2025-01-07 14:52 108浏览
  • 故障现象一辆2017款东风风神AX7车,搭载DFMA14T发动机,累计行驶里程约为13.7万km。该车冷起动后怠速运转正常,热机后怠速运转不稳,组合仪表上的发动机转速表指针上下轻微抖动。 故障诊断 用故障检测仪检测,发动机控制单元中无故障代码存储;读取发动机数据流,发现进气歧管绝对压力波动明显,有时能达到69 kPa,明显偏高,推断可能的原因有:进气系统漏气;进气歧管绝对压力传感器信号失真;发动机机械故障。首先从节气门处打烟雾,没有发现进气管周围有漏气的地方;接着拔下进气管上的两个真空
    虹科Pico汽车示波器 2025-01-08 16:51 70浏览
  • 大模型的赋能是指利用大型机器学习模型(如深度学习模型)来增强或改进各种应用和服务。这种技术在许多领域都显示出了巨大的潜力,包括但不限于以下几个方面: 1. 企业服务:大模型可以用于构建智能客服系统、知识库问答系统等,提升企业的服务质量和运营效率。 2. 教育服务:在教育领域,大模型被应用于个性化学习、智能辅导、作业批改等,帮助教师减轻工作负担,提高教学质量。 3. 工业智能化:大模型有助于解决工业领域的复杂性和不确定性问题,尽管在认知能力方面尚未完全具备专家级的复杂决策能力。 4. 消费
    丙丁先生 2025-01-07 09:25 117浏览
  • PLC组态方式主要有三种,每种都有其独特的特点和适用场景。下面来简单说说: 1. 硬件组态   定义:硬件组态指的是选择适合的PLC型号、I/O模块、通信模块等硬件组件,并按照实际需求进行连接和配置。    灵活性:这种方式允许用户根据项目需求自由搭配硬件组件,具有较高的灵活性。    成本:可能需要额外的硬件购买成本,适用于对系统性能和扩展性有较高要求的场合。 2. 软件组态   定义:软件组态主要是通过PLC
    丙丁先生 2025-01-06 09:23 98浏览
  • 「他明明跟我同梯进来,为什么就是升得比我快?」许多人都有这样的疑问:明明就战绩也不比隔壁同事差,升迁之路却比别人苦。其实,之间的差异就在于「领导力」。並非必须当管理者才需要「领导力」,而是散发领导力特质的人,才更容易被晓明。许多领导力和特质,都可以通过努力和学习获得,因此就算不是天生的领导者,也能成为一个具备领导魅力的人,进而被老板看见,向你伸出升迁的橘子枝。领导力是什么?领导力是一种能力或特质,甚至可以说是一种「影响力」。好的领导者通常具备影响和鼓励他人的能力,并导引他们朝着共同的目标和愿景前
    优思学院 2025-01-08 14:54 66浏览
  • 每日可见的315MHz和433MHz遥控模块,你能分清楚吗?众所周知,一套遥控设备主要由发射部分和接收部分组成,发射器可以将控制者的控制按键经过编码,调制到射频信号上面,然后经天线发射出无线信号。而接收器是将天线接收到的无线信号进行解码,从而得到与控制按键相对应的信号,然后再去控制相应的设备工作。当前,常见的遥控设备主要分为红外遥控与无线电遥控两大类,其主要区别为所采用的载波频率及其应用场景不一致。红外遥控设备所采用的射频信号频率一般为38kHz,通常应用在电视、投影仪等设备中;而无线电遥控设备
    华普微HOPERF 2025-01-06 15:29 164浏览
  • 村田是目前全球量产硅电容的领先企业,其在2016年收购了法国IPDiA头部硅电容器公司,并于2023年6月宣布投资约100亿日元将硅电容产能提升两倍。以下内容主要来自村田官网信息整理,村田高密度硅电容器采用半导体MOS工艺开发,并使用3D结构来大幅增加电极表面,因此在给定的占位面积内增加了静电容量。村田的硅技术以嵌入非结晶基板的单片结构为基础(单层MIM和多层MIM—MIM是指金属 / 绝缘体/ 金属) 村田硅电容采用先进3D拓扑结构在100um内,使开发的有效静电容量面积相当于80个
    知白 2025-01-07 15:02 144浏览
  • 根据Global Info Research项目团队最新调研,预计2030年全球封闭式电机产值达到1425百万美元,2024-2030年期间年复合增长率CAGR为3.4%。 封闭式电机是一种电动机,其外壳设计为密闭结构,通常用于要求较高的防护等级的应用场合。封闭式电机可以有效防止外部灰尘、水分和其他污染物进入内部,从而保护电机的内部组件,延长其使用寿命。 环洋市场咨询机构出版的调研分析报告【全球封闭式电机行业总体规模、主要厂商及IPO上市调研报告,2025-2031】研究全球封闭式电机总体规
    GIRtina 2025-01-06 11:10 124浏览
  • 根据环洋市场咨询(Global Info Research)项目团队最新调研,预计2030年全球无人机锂电池产值达到2457百万美元,2024-2030年期间年复合增长率CAGR为9.6%。 无人机锂电池是无人机动力系统中存储并释放能量的部分。无人机使用的动力电池,大多数是锂聚合物电池,相较其他电池,锂聚合物电池具有较高的能量密度,较长寿命,同时也具有良好的放电特性和安全性。 全球无人机锂电池核心厂商有宁德新能源科技、欣旺达、鹏辉能源、深圳格瑞普和EaglePicher等,前五大厂商占有全球
    GIRtina 2025-01-07 11:02 124浏览
  •  在全球能源结构加速向清洁、可再生方向转型的今天,风力发电作为一种绿色能源,已成为各国新能源发展的重要组成部分。然而,风力发电系统在复杂的环境中长时间运行,对系统的安全性、稳定性和抗干扰能力提出了极高要求。光耦(光电耦合器)作为一种电气隔离与信号传输器件,凭借其优秀的隔离保护性能和信号传输能力,已成为风力发电系统中不可或缺的关键组件。 风力发电系统对隔离与控制的需求风力发电系统中,包括发电机、变流器、变压器和控制系统等多个部分,通常工作在高压、大功率的环境中。光耦在这里扮演了
    晶台光耦 2025-01-08 16:03 61浏览
  • 本文介绍编译Android13 ROOT权限固件的方法,触觉智能RK3562开发板演示,搭载4核A53处理器,主频高达2.0GHz;内置独立1Tops算力NPU,可应用于物联网网关、平板电脑、智能家居、教育电子、工业显示与控制等行业。关闭selinux修改此文件("+"号为修改内容)device/rockchip/common/BoardConfig.mkBOARD_BOOT_HEADER_VERSION ?= 2BOARD_MKBOOTIMG_ARGS :=BOARD_PREBUILT_DTB
    Industio_触觉智能 2025-01-08 00:06 92浏览
  • 彼得·德鲁克被誉为“现代管理学之父”,他的管理思想影响了无数企业和管理者。然而,关于他的书籍分类,一种流行的说法令人感到困惑:德鲁克一生写了39本书,其中15本是关于管理的,而其中“专门写工商企业或为企业管理者写的”只有两本——《为成果而管理》和《创新与企业家精神》。这样的表述广为流传,但深入探讨后却发现并不完全准确。让我们一起重新审视这一说法,解析其中的矛盾与根源,进而重新认识德鲁克的管理思想及其著作的真正价值。从《创新与企业家精神》看德鲁克的视角《创新与企业家精神》通常被认为是一本专为企业管
    优思学院 2025-01-06 12:03 158浏览
  • 本文介绍Linux系统更换开机logo方法教程,通用RK3566、RK3568、RK3588、RK3576等开发板,触觉智能RK3562开发板演示,搭载4核A53处理器,主频高达2.0GHz;内置独立1Tops算力NPU,可应用于物联网网关、平板电脑、智能家居、教育电子、工业显示与控制等行业。制作图片开机logo图片制作注意事项(1)图片必须为bmp格式;(2)图片大小不能大于4MB;(3)BMP位深最大是32,建议设置为8;(4)图片名称为logo.bmp和logo_kernel.bmp;开机
    Industio_触觉智能 2025-01-06 10:43 93浏览
  • 在智能家居领域中,Wi-Fi、蓝牙、Zigbee、Thread与Z-Wave等无线通信协议是构建短距物联局域网的关键手段,它们常在实际应用中交叉运用,以满足智能家居生态系统多样化的功能需求。然而,这些协议之间并未遵循统一的互通标准,缺乏直接的互操作性,在进行组网时需要引入额外的网关作为“翻译桥梁”,极大地增加了系统的复杂性。 同时,Apple HomeKit、SamSung SmartThings、Amazon Alexa、Google Home等主流智能家居平台为了提升市占率与消费者
    华普微HOPERF 2025-01-06 17:23 204浏览
  • 这篇内容主要讨论三个基本问题,硅电容是什么,为什么要使用硅电容,如何正确使用硅电容?1.  硅电容是什么首先我们需要了解电容是什么?物理学上电容的概念指的是给定电位差下自由电荷的储藏量,记为C,单位是F,指的是容纳电荷的能力,C=εS/d=ε0εrS/4πkd(真空)=Q/U。百度百科上电容器的概念指的是两个相互靠近的导体,中间夹一层不导电的绝缘介质。通过观察电容本身的定义公式中可以看到,在各个变量中比较能够改变的就是εr,S和d,也就是介质的介电常数,金属板有效相对面积以及距离。当前
    知白 2025-01-06 12:04 223浏览
我要评论
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦