错误处理机制:errno的使用、错误码定义规范、错误传播策略、集中式错误处理
错误处理,说白了就是代码的「安全气囊」。你开车上路,不能指望永远不出事故。写代码也一样,不能指望所有输入都合法、所有资源都申请成功、所有硬件都乖乖听话。
我刚开始做嵌入式那几年,吃过不少「不处理错误」的亏。有一次,一个串口驱动在内存分配失败后直接返回了垃圾数据,结果上层模块拿着这个数据去控制电机……嗯,电机直接飞车了。从那以后,我对错误处理的态度就变成了:宁可代码多写三行,不让bug多跑一秒。
1. errno 的使用:C语言最古老的错误信号
errno 是 C 标准库提供的一个全局整数变量。很多库函数在出错时,会设置 errno 的值,然后返回一个特殊值(比如 -1 或 NULL)来告诉你「出事了」。
核心原则:调用可能失败的函数后,立即检查返回值。如果返回错误,再读取 errno 获取具体原因。
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
FILE *fp = fopen("/config/settings.bin", "r");
if (fp == NULL) {
// 立刻检查 errno
switch (errno) {
case ENOENT:
printf("配置文件不存在,使用默认配置\n");
break;
case EACCES:
printf("权限不足,请检查文件权限\n");
return -1;
case ENOMEM:
printf("系统内存不足,无法打开文件\n");
return -1;
default:
printf("未知错误,错误码: %d\n", errno);
return -1;
}
}
我的习惯:每次调用系统函数后,如果可能失败,我会在下一行就检查返回值。绝不隔行。因为 errno 可能被中间的其他函数调用覆盖掉。
你可能会问:为什么不直接用 perror() 打印错误?嗯,在调试阶段可以,但在正式产品里,你不能把错误信息直接怼到用户脸上。你需要的是结构化的错误处理,而不是简单的打印。
2. 错误码定义规范:给你的错误一个「身份证」
errno 是系统级的。在应用层,我们需要自己的错误码体系。我见过太多项目,错误码定义得乱七八糟——有的用宏,有的用枚举,有的直接写数字。维护起来简直是一场噩梦。
我个人推荐的做法是:用枚举 + 前缀命名法。
// 错误码定义:模块_错误类型
typedef enum {
// 通用错误 (0x0000 - 0x00FF)
ERR_OK = 0x0000, // 成功
ERR_GENERAL = 0x0001, // 通用错误
ERR_NOMEM = 0x0002, // 内存不足
ERR_TIMEOUT = 0x0003, // 超时
ERR_INVALID_PARAM = 0x0004, // 参数无效
// 传感器模块 (0x0100 - 0x01FF)
ERR_SENSOR_INIT = 0x0100, // 传感器初始化失败
ERR_SENSOR_READ = 0x0101, // 传感器读取失败
ERR_SENSOR_CALIB = 0x0102, // 传感器校准失败
// 通信模块 (0x0200 - 0x02FF)
ERR_COMM_OPEN = 0x0200, // 通信端口打开失败
ERR_COMM_SEND = 0x0201, // 发送失败
ERR_COMM_RECV = 0x0202, // 接收失败
ERR_COMM_CRC = 0x0203, // CRC校验错误
} ErrorCode_t;
规范要点:
- 每个错误码是唯一的,用 16 位整数表示
- 高 8 位表示模块,低 8 位表示具体错误
- 命名清晰:ERR_模块_具体错误
- 预留空间:每个模块留 256 个码位
我曾经在一个项目里接手过这样的代码:错误码用 #define 定义,值从 1 排到 200,没有任何分组。你想查一个错误是什么意思,得翻遍整个头文件。后来我花了三天时间,把所有错误码重构成了枚举分组。从那以后,新来的同事再也没抱怨过「看不懂错误码」。
3. 错误传播策略:错误该「往上抛」还是「就地消化」?
这是嵌入式开发里最纠结的问题之一。一个函数出错了,是就地处理掉,还是把错误返回给调用者?
我的经验是:底层函数只负责「检测错误」,不负责「处理错误」。处理错误是调用者的责任。
// 底层驱动:只检测,不处理
int sensor_read_temperature(float *temp) {
if (temp == NULL) {
return ERR_INVALID_PARAM; // 返回错误码,不自己处理
}
// 假设通过 I2C 读取传感器
int ret = i2c_read(0x48, 0x00, (uint8_t*)temp, 4);
if (ret != 0) {
return ERR_SENSOR_READ; // 返回错误码,不自己处理
}
return ERR_OK;
}
// 上层应用:负责处理错误
void control_loop(void) {
float temperature;
int err = sensor_read_temperature(&temperature);
if (err != ERR_OK) {
// 集中处理:记录日志、使用默认值、报警
log_error("温度读取失败,错误码: 0x%04X", err);
temperature = 25.0f; // 使用默认值
// 或者:触发看门狗复位
// 或者:切换到备用传感器
}
// 继续执行控制逻辑
adjust_heater(temperature);
}
避坑指南:我曾经在一个项目中,底层函数在检测到错误后,自己调用了 printf 打印错误信息,然后返回了 0。上层调用者看到返回 0,以为一切正常,继续往下执行。结果系统在错误状态下运行了整整两天,直到数据完全错乱才被发现。教训是:底层函数不要「悄悄消化」错误,一定要把错误信号明确传递出去。
错误传播的几种常见策略:
| 策略 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 返回错误码 | 大多数函数 | 简单、明确、可组合 | 调用者必须检查返回值 |
| 全局错误变量 | 中断服务程序、回调函数 | 不改变函数签名 | 线程不安全、容易遗漏 |
| 异常(setjmp/longjmp) | 深度嵌套的错误处理 | 跳过多层调用栈 | 资源泄漏风险、调试困难 |
| 断言(assert) | 不可恢复的错误 | 快速失败、便于调试 | 发布版本中会被移除 |
我个人最常用的是「返回错误码」。它最直接,也最容易让代码的意图变得清晰。你想想看,一个函数返回 int,调用者看到 if (ret != ERR_OK) 就知道要处理错误,多清爽。
4. 集中式错误处理:把「救火队」放在一个地方
如果每个函数都自己处理错误,代码里会到处都是 if-else,读起来像一团乱麻。集中式错误处理,就是把所有错误处理的逻辑收拢到一个地方。
我常用的模式是:函数内部只检查错误并返回,在调用链的「顶层」统一处理。
// 集中式错误处理器
void error_handler(int err_code, const char *module, const char *func) {
static int error_count = 0;
error_count++;
// 记录错误日志
log_system("ERROR [%s][%s] 错误码: 0x%04X, 累计错误: %d",
module, func, err_code, error_count);
// 根据错误严重级别采取不同措施
switch (err_code) {
case ERR_NOMEM:
// 内存不足,尝试回收缓存
cache_cleanup();
break;
case ERR_TIMEOUT:
// 超时错误,重试
break;
case ERR_SENSOR_READ:
// 传感器故障,切换到备用传感器
sensor_switch_to_backup();
break;
default:
// 未知严重错误,系统复位
system_reset();
break;
}
}
// 使用示例
int result = sensor_read_temperature(&temp);
if (result != ERR_OK) {
error_handler(result, "SENSOR", __func__);
// 根据错误类型决定是否继续执行
if (result == ERR_SENSOR_READ) {
temp = 25.0f; // 使用默认值继续
} else {
return; // 严重错误,退出当前任务
}
}
集中式错误处理的好处:
- 错误处理逻辑集中在一处,修改方便
- 可以统一记录日志、统计错误次数
- 可以根据错误严重级别统一决策(重试、降级、复位)
- 代码可读性大幅提升,业务逻辑和错误处理分离
嗯,这里要注意一点:集中式处理不是让你把所有错误都丢到一个函数里不管了。你还是要区分错误的严重程度。比如,一个校验和错误可能只需要重发数据包,但一个内存访问越界可能就需要立即复位系统。集中式处理器的职责是「根据错误码做出统一决策」,而不是「把所有错误都当成一样的」。
知识体系总览
下面这张图,是我对本章知识体系的总结。你可以把它当作一个「错误处理决策地图」来用。
最后说一句:错误处理不是「锦上添花」,而是「雪中送炭」。你花在错误处理上的每一分钟,都是在为未来的自己减少一个通宵调试的夜晚。别偷懒,把错误处理当成代码的一部分,而不是事后补丁。