10、文件操作安全:文件打开失败处理、文件读写完整性检查、文件描述符泄漏防范、临时文件安全
文件操作,是嵌入式开发里最容易被轻视的环节。我见过太多同事,写文件操作代码时只想着“能跑就行”,结果一到现场就出幺蛾子。SD卡突然拔了、文件系统满了、断电导致数据写到一半……这些坑,我几乎都踩过。今天咱们就把文件操作安全这块,掰开揉碎了讲清楚。
核心原则:文件操作不是“打开-读写-关闭”这么简单。每一步都要假设“会失败”,然后做好应对。
10.1 文件打开失败处理
你想想看,fopen() 返回 NULL 的概率有多大?其实不小。SD卡接触不良、文件系统损坏、路径不存在,都会导致打开失败。我早期做的一个数据采集项目,就因为没检查 fopen 返回值,程序直接崩溃,现场数据全丢了。
正确的做法:
FILE *fp = fopen("/data/log.txt", "w");
if (fp == NULL) {
// 记录错误日志
syslog(LOG_ERR, "Failed to open log file: %s", strerror(errno));
// 尝试备用路径
fp = fopen("/tmp/log.txt", "w");
if (fp == NULL) {
// 实在打不开,也要优雅处理
return -1;
}
}
我的习惯:打开文件时,我会同时检查 errno。比如 EACCES 表示权限不足,ENOSPC 表示空间不够。不同错误码,处理策略不一样。权限问题可以提示用户,空间问题可以尝试清理旧文件。
10.2 文件读写完整性检查
文件打开了,读写就安全了吗?不一定。写入一半断电、写入过程中存储介质故障,都会导致数据不完整。我曾经在一个车载项目中,就因为没做完整性检查,导致配置文件写了一半,系统重启后读取到错误配置,直接让设备跑飞了。
完整性检查的三种手段:
| 方法 | 适用场景 | 缺点 |
|---|---|---|
| CRC32/校验和 | 小文件、配置类文件 | 无法定位错误位置 |
| 写入后回读比对 | 关键数据、日志文件 | 速度慢,占用双倍时间 |
| 事务日志(Journal) | 数据库、文件系统 | 实现复杂,占用额外空间 |
我推荐的做法:对于嵌入式场景,最实用的方案是“写入后回读”。写完后,把文件关闭再打开,读出来跟原始数据比对。虽然慢一点,但心里踏实。
int write_with_check(const char *path, const uint8_t *data, size_t len) {
FILE *fp = fopen(path, "wb");
if (!fp) return -1;
size_t written = fwrite(data, 1, len, fp);
fclose(fp);
if (written != len) {
unlink(path); // 写入不完整,删除文件
return -1;
}
// 回读检查
uint8_t *buf = malloc(len);
fp = fopen(path, "rb");
size_t read = fread(buf, 1, len, fp);
fclose(fp);
int ret = (read == len && memcmp(data, buf, len) == 0) ? 0 : -1;
free(buf);
return ret;
}
注意:回读检查不能保证100%可靠。如果存储介质本身有坏块,回读可能也是错的。对于极端可靠性要求,建议使用 CRC + 回读双重校验。
10.3 文件描述符泄漏防范
文件描述符泄漏,是嵌入式系统里最隐蔽的杀手。每个进程能打开的文件描述符数量是有限的(通常是 1024 或 256)。一旦泄漏,系统会报“Too many open files”,然后所有文件操作都失败。
我遇到过最离谱的一次:一个同事在循环里打开文件,忘记 fclose,跑了三天后系统彻底瘫痪。排查了一整天,才发现是文件描述符用光了。
防范措施:
- RAII 思想:打开文件后,立即想到关闭。可以用 goto 统一出口,或者用函数封装。
- 使用 valgrind 或 strace:开发阶段定期检查文件描述符泄漏。
- 设置文件描述符上限:在程序启动时,用 setrlimit() 设置软限制和硬限制。
// 使用 goto 统一出口,避免忘记关闭
int process_file(const char *path) {
FILE *fp = fopen(path, "r");
if (!fp) return -1;
// ... 处理逻辑 ...
int ret = 0;
goto cleanup;
cleanup:
if (fp) fclose(fp);
return ret;
}
一个小技巧:我习惯在 fclose 之后,把 fp 置为 NULL。这样即使后面误用了,也能通过检查 NULL 发现 bug。
10.4 临时文件安全
临时文件,看着简单,其实坑最多。多线程同时写同一个临时文件、临时文件路径被猜到导致安全漏洞、程序崩溃后临时文件残留……这些问题我都处理过。
安全创建临时文件的要点:
- 使用 mkstemp() 而不是 tmpnam():tmpnam() 有竞态条件,mkstemp() 是原子操作。
- 设置合适的权限:临时文件默认权限可能是 0666,建议用 umask 限制为 0600。
- 程序退出时清理:注册 atexit() 或信号处理函数,确保临时文件被删除。
- 避免使用固定路径:比如 /tmp/myapp_xxx,容易被预测。用随机后缀。
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int create_safe_tempfile(char *path, size_t pathlen) {
// 模板必须包含 XXXXXX
strncpy(path, "/tmp/myapp_XXXXXX", pathlen);
int fd = mkstemp(path);
if (fd == -1) return -1;
// 设置权限为 0600
fchmod(fd, S_IRUSR | S_IWUSR);
return fd;
}
避坑指南:我曾经在信号处理函数里删除临时文件,结果因为信号处理函数不能调用不可重入函数(如 free()),导致死锁。正确的做法是:在信号处理函数里只设置一个标志,在主循环里检查并清理。
知识体系总览
下面这张图,把文件操作安全的四个核心维度串起来了。你可以把它当作一个检查清单,写代码时对照着看。
文件操作安全,说白了就是四个字:防御为主。打开时检查返回值,读写时校验完整性,用完记得关闭,临时文件要安全创建。这些习惯养成了,你的代码会稳定很多。
我做了十几年嵌入式,最深的体会就是:文件操作看似简单,但出问题往往是大问题。希望今天的内容,能帮你少踩几个坑。
公众号:蓝海资料掘金营,微信deep3321