【干货分享】嵌入式C编程之错误处理(附代码例子)
扫描二维码
随时随地手机看文章
前言
本文主要总结嵌入式系统C语言编程中,主要的错误处理方式。文中涉及的代码运行环境如下:一、错误概念
1.1 错误分类
从严重性而言,程序错误可分为致命性和非致命性两类。对于致命性错误,无法执行恢复动作,最多只能在用户屏幕上打印出错消息或将其写入日志文件,然后终止程序;而对于非致命性错误,多数本质上是暂时的(如资源短缺),一般恢复动作是延迟一些时间后再次尝试。从交互性而言,程序错误可分为用户错误和内部错误两类。用户错误呈现给用户,通常指明用户操作上的错误;而程序内部错误呈现给程序员(可能携带用户不可接触的数据细节),用于查错和排障。
应用程序开发者可决定恢复哪些错误以及如何恢复。例如,若磁盘已满,可考虑删除非必需或已过期的数据;若网络连接失败,可考虑短时间延迟后重建连接。选择合理的错误恢复策略,可避免应用程序的异常终止,从而改善其健壮性。
1.2 处理步骤
错误处理即处理程序运行时出现的任何意外或异常情况。典型的错误处理包含五个步骤:
-
程序执行时发生软件错误。该错误可能产生于被底层驱动或内核映射为软件错误的硬件响应事件(如除零)。
-
以一个错误指示符(如整数或结构体)记录错误的原因及相关信息。
-
程序检测该错误(读取错误指示符,或由其主动上报);
-
程序决定如何处理错误(忽略、部分处理或完全处理);
-
恢复或终止程序的执行。
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函数)的返回值有时既包括错误代码也包括有用结果。因此,上述调用形式可在同一条语句中接收返回值并检查错误(当执行成功时返回合法的数据值)。
返回值方式的好处是简便和高效,但仍存在较多问题:
- 代码可读性降低
- 质量降级
- 信息有限
char *IntToAscii(int dwVal, char *pszRes, int dwRadix)
{
if(NULL == pszRes)
return "Arg2Null";
if((dwRadix < 2) || (dwRadix > 36))
return "Arg3OutOfRange";
//...
return pszRes;
}
- 定义冲突
- 无约束性
新的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标准库头文件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时应注意以下几点:
- 函数返回成功时,允许其修改errno。
因此,调用库函数时应先检测作为错误指示的返回值。仅当函数返回值指明出错时,才检查errno值:
//调用库函数
if(返回错误值)
//检查errno
-
库函数返回失败时,不一定会设置errno,取决于具体的库函数。
-
errno在程序开始时设置为0,任何库函数都不会将errno再次清零。
- 使用errno前,应避免调用其他可能设置errno的库函数。如:
{
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值。
-
使用现代版本的C库时,应包含使用
头文件;在非常老的Unix 系统中,可能没有该头文件,此时可手工声明errno(如extern int errno)。
#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