【Linux 封神之路】文件操作 + 时间编程实战:从缓冲区到时间格式化全解析
大家好,我是专注 Linux 技术分享的小杨。上一篇给大家整理了系统监控与性能分析工具,解决了服务器卡慢、程序崩溃的问题。今天接着 Linux 开发核心技能系列,聚焦 "文件操作" 和 "时间编程"------ 这两个是嵌入式 Linux 开发的高频需求,比如日志写入、配置文件读写、时间戳记录等场景都离不开。结合文件操作及时间编程资料,从 "缓冲区 / 非缓冲区文件操作" 到 "时间格式转换",手把手教你实战用法,附完整代码示例,新手直接套用!
一、先理清:文件操作的两种核心方式
Linux 文件操作分为 "基于缓冲区" 和 "基于非缓冲区" 两种,适用场景不同,核心区别在于是否通过标准库缓冲区优化 IO 效率:
- 基于缓冲区(fopen/fwrite/fread):标准库封装,自动缓冲数据,减少系统调用次数,适合普通文件读写(如文本文件、配置文件);
- 基于非缓冲区(open/write/read):直接调用系统调用,无中间缓冲,实时性高,适合设备文件操作(如串口、磁盘)、嵌入式实时场景。
下面分别拆解两种方式的核心函数和实战代码。
二、实战 1:基于缓冲区的文件操作(标准库函数)
基于缓冲区的文件操作通过<stdio.h>头文件的函数实现,用法简单、兼容性好,是开发中的首选。
1. 核心函数详解(按操作流程)
| 函数 | 功能 | 核心参数 / 返回值 |
|---|---|---|
fopen |
打开 / 创建文件 | 参数 1:文件路径(如"test.txt");参数 2:打开模式("r"读、"w"写、"a"追加、"r+"读写);返回值:文件指针(FILE *),失败返回NULL |
fwrite |
写入数据 | 参数 1:数据缓冲区地址;参数 2:单个数据大小;参数 3:数据个数;参数 4:文件指针;返回值:成功写入的个数 |
fprintf |
格式化写入(文本文件) | 类似printf,多一个文件指针参数(如fprintf(fp, "name:%s\n", "Linux")) |
fread |
读取数据 | 参数 1:接收数据的缓冲区;参数 2:单个数据大小;参数 3:读取个数;参数 4:文件指针;返回值:成功读取的个数 |
fscanf |
格式化读取(文本文件) | 类似scanf,多一个文件指针参数(如fscanf(fp, "%d", &num)) |
fseek |
移动文件光标 | 参数 1:文件指针;参数 2:偏移量(正值向右、负值向左);参数 3:基准位置(SEEK_SET文件开头、SEEK_CUR当前位置、SEEK_END文件末尾) |
fclose |
关闭文件 | 参数:文件指针;返回值:0 成功,-1 失败(必须关闭,避免资源泄漏) |
2. 实战代码:文本文件读写(日志记录场景)
c
运行
#include <stdio.h>
#include <string.h>
int main() {
// 1. 打开文件(不存在则创建,存在则追加写入)
FILE *fp = fopen("app.log", "a+");
if (fp == NULL) {
perror("fopen failed");
return 1;
}
// 2. 格式化写入数据(模拟日志:时间+内容)
char log_msg[] = "2026-01-28 16:00:00 [INFO] 程序启动成功\n";
fprintf(fp, "%s", log_msg);
// 3. 移动光标到文件开头,读取所有日志
fseek(fp, 0, SEEK_SET);
char buf[1024];
printf("日志内容:\n");
while (fgets(buf, sizeof(buf), fp) != NULL) { // 逐行读取
printf("%s", buf);
}
// 4. 关闭文件
fclose(fp);
fp = NULL; // 避免野指针
return 0;
}
3. 关键注意事项
- 打开模式选择:
"w"会清空文件原有内容,"a"会在文件末尾追加,"r+"支持读写但不会创建文件; - 必须检查
fopen返回值:文件不存在、权限不足都会导致打开失败; - 写完数据后若需立即读取,需用
fseek移动光标(否则光标在文件末尾,读取不到数据)。
三、实战 2:基于非缓冲区的文件操作(系统调用)
非缓冲区文件操作通过系统调用实现,无中间缓冲,IO 响应更快,适合嵌入式实时场景(如串口通信、磁盘 IO)。
1. 核心函数详解(按操作流程)
| 函数 | 功能 | 核心参数 / 返回值 |
|---|---|---|
open |
打开 / 创建文件 | 参数 1:文件路径;参数 2:打开标志(必选:O_RDONLY只读、O_WRONLY只写、O_RDWR读写;可选:O_CREAT创建、O_APPEND追加、O_TRUNC清空);参数 3:文件权限(O_CREAT时必填,如0644=rw-r--r--);返回值:文件描述符(int),失败返回 - 1 |
write |
写入数据 | 参数 1:文件描述符(open 返回值);参数 2:数据缓冲区;参数 3:数据长度;返回值:成功写入的字节数,失败返回 - 1 |
read |
读取数据 | 参数 1:文件描述符;参数 2:接收缓冲区;参数 3:读取长度;返回值:成功读取的字节数(0 表示文件结束),失败返回 - 1 |
lseek |
移动光标 | 参数 1:文件描述符;参数 2:偏移量;参数 3:基准位置(SEEK_SET/SEEK_CUR/SEEK_END);返回值:光标位置的字节数,失败返回 - 1 |
close |
关闭文件 | 参数:文件描述符;返回值:0 成功,-1 失败 |
2. 实战代码:二进制文件读写(数据存储场景)
c
运行
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
// 定义要存储的结构体(模拟传感器数据)
typedef struct {
int temp; // 温度
int humidity;// 湿度
long time; // 时间戳
} SensorData;
int main() {
// 1. 打开文件(创建+读写+清空原有内容,权限0644)
int fd = open("sensor.bin", O_RDWR | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror("open failed");
return 1;
}
// 2. 准备数据并写入(二进制格式)
SensorData data = {25, 60, 1738064000};
ssize_t write_len = write(fd, &data, sizeof(SensorData));
if (write_len != sizeof(SensorData)) {
perror("write failed");
close(fd);
return 1;
}
printf("写入二进制数据成功,长度:%ld字节\n", write_len);
// 3. 移动光标到文件开头,读取数据
lseek(fd, 0, SEEK_SET);
SensorData read_data;
ssize_t read_len = read(fd, &read_data, sizeof(SensorData));
if (read_len == -1) {
perror("read failed");
close(fd);
return 1;
}
printf("读取数据:温度=%d℃,湿度=%d%%,时间戳=%ld\n",
read_data.temp, read_data.humidity, read_data.time);
// 4. 关闭文件
close(fd);
return 0;
}
3. 关键注意事项
- 文件权限:
0644表示所有者可读可写,组用户和其他用户只读(嵌入式开发中常用权限); - 文件描述符:
open返回的是整数(如 3、4),而非FILE *,需注意与缓冲区操作的区别; - 无缓冲特性:
write调用后数据直接写入文件(或设备),无需刷新缓冲区,实时性更高。
四、实战 3:时间编程(时间戳 + 格式化转换)
时间编程在日志记录、数据同步、定时任务中必不可少,核心是 "时间戳→时间结构体→格式化字符串" 的转换流程。
1. 核心概念与结构体
- 时间戳:从 1970 年 1 月 1 日 0 时 0 分 0 秒(UTC)到当前时间的秒数(
time_t类型); - 时间结构体(
struct tm):拆分后的时间格式,包含秒、分、时、日、月、年等字段(注意:tm_mon从 0 开始,tm_year= 实际年份 - 1900)。
2. 核心函数详解(按转换流程)
| 函数 | 功能 | 核心参数 / 返回值 |
|---|---|---|
time |
获取当前时间戳 | 参数:保存时间戳的地址(可传NULL直接返回);返回值:当前时间戳,失败返回 - 1 |
localtime |
时间戳→本地时间结构体 | 参数:时间戳指针;返回值:struct tm *(本地时间,如北京时间) |
gmtime |
时间戳→格林威治时间结构体 | 参数:时间戳指针;返回值:struct tm *(UTC 时间,比北京时间晚 8 小时) |
ctime |
时间戳→默认格式字符串 | 参数:时间戳指针;返回值:字符串指针(如"Wed Jan 28 16:30:00 2026\n") |
asctime |
时间结构体→默认格式字符串 | 参数:struct tm *;返回值:字符串指针(格式同ctime) |
strftime |
时间结构体→自定义格式字符串 | 参数 1:结果缓冲区;参数 2:缓冲区大小;参数 3:格式字符串(如"%Y-%m-%d %H:%M:%S");参数 4:时间结构体;返回值:成功转换的字符数 |
3. 实战代码:时间格式化(日志时间戳场景)
c
运行
#include <stdio.h>
#include <time.h>
#include <string.h>
int main() {
// 1. 获取当前时间戳
time_t now = time(NULL);
if (now == -1) {
perror("time failed");
return 1;
}
printf("当前时间戳:%ld\n", now);
// 2. 时间戳→本地时间结构体
struct tm *local_tm = localtime(&now);
if (local_tm == NULL) {
perror("localtime failed");
return 1;
}
// 3. 自定义格式化时间(常用格式:年-月-日 时:分:秒)
char time_str[64];
strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", local_tm);
printf("自定义格式时间:%s\n", time_str);
// 4. 时间戳→默认格式字符串
char *default_time = ctime(&now);
printf("默认格式时间:%s", default_time);
// 5. 时间结构体→默认格式字符串
char *asctime_str = asctime(local_tm);
printf("asctime格式时间:%s", asctime_str);
return 0;
}
4. 常用时间格式符(strftime核心)
| 格式符 | 含义 | 示例 |
|---|---|---|
%Y |
4 位年份 | 2026 |
%m |
2 位月份(01-12) | 01 |
%d |
2 位日期(01-31) | 28 |
%H |
24 小时制小时(00-23) | 16 |
%M |
分钟(00-59) | 30 |
%S |
秒(00-59) | 45 |
%w |
星期(0-6,0 为周日) | 3 |
%j |
一年中的第几天(001-366) | 028 |
五、综合实战:文件操作 + 时间编程(日志系统)
结合前面的知识点,实现一个带时间戳的日志系统,自动记录程序运行状态:
c
运行
#include <stdio.h>
#include <time.h>
#include <string.h>
// 日志写入函数(带时间戳)
void write_log(const char *level, const char *msg) {
// 1. 打开日志文件(追加模式)
FILE *fp = fopen("system.log", "a");
if (fp == NULL) {
perror("fopen log failed");
return;
}
// 2. 获取当前时间并格式化
time_t now = time(NULL);
struct tm *local_tm = localtime(&now);
char time_str[64];
strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", local_tm);
// 3. 写入日志(格式:时间 级别 消息)
fprintf(fp, "[%s] [%s] %s\n", time_str, level, msg);
// 4. 关闭文件
fclose(fp);
fp = NULL;
}
int main() {
write_log("INFO", "程序启动");
write_log("INFO", "开始执行数据采集");
write_log("WARNING", "传感器数据波动较大");
write_log("INFO", "程序执行完成");
printf("日志写入成功,查看system.log文件\n");
return 0;
}
运行后查看system.log文件,输出如下:
plaintext
[2026-01-28 17:00:00] [INFO] 程序启动
[2026-01-28 17:00:01] [INFO] 开始执行数据采集
[2026-01-28 17:00:05] [WARNING] 传感器数据波动较大
[2026-01-28 17:00:10] [INFO] 程序执行完成
六、避坑指南:常见错误解决
-
文件操作失败(权限不足):
- 原因:创建文件时未指定权限(
open用O_CREAT但未传mode参数),或目录无写权限; - 解决:
open函数添加权限参数(如0644),或用sudo运行程序(仅测试场景)。
- 原因:创建文件时未指定权限(
-
时间格式化出现乱码:
- 原因:
strftime的缓冲区大小不足,或格式符错误; - 解决:增大缓冲区(如
char time_str[64]),核对格式符(如%Y而非%y)。
- 原因:
-
非缓冲区操作读取不到数据:
- 原因:
read后未判断返回值,或光标位置错误; - 解决:用
lseek移动光标到正确位置,检查read返回值(0 表示文件结束,-1 表示失败)。
- 原因:
七、总结:核心技能与应用场景
- 文件操作:
- 普通文本 / 配置文件:用缓冲区操作(
fopen/fprintf/fscanf),效率更高; - 设备文件 / 实时场景:用非缓冲区操作(
open/write/read),实时性更强;
- 普通文本 / 配置文件:用缓冲区操作(
- 时间编程:
- 日志记录:用
strftime自定义时间格式,搭配文件操作实现带时间戳日志; - 数据同步:用
time获取时间戳,实现跨设备时间同步;
- 日志记录:用
- 嵌入式适配:
- 权限控制:嵌入式设备中文件权限需严格配置(如
0644),避免权限过高; - 资源释放:文件操作后必须关闭(
fclose/close),避免资源泄漏。
- 权限控制:嵌入式设备中文件权限需严格配置(如
掌握文件操作和时间编程,能应对嵌入式开发中 80% 的 IO 场景(日志、配置、数据存储)。下一篇博客,我会给大家整理 "Linux 多线程编程",解决并发执行、资源同步等问题,敬请关注!