用 fopen + fseek 模拟 tail -f 需保持单 FILE* 打开、以 "rb" 模式操作,每次轮询先 fseek(fp, 0, SEEK_END) + ftell 获取当前大小,与上次偏移比较;若增大则从旧偏移 fread 新增字节;需用 fstat 检测 inode 变化应对 logrotate;手动缓冲解析换行,避免 fgets 截断。

如何用 fopen + fseek 模拟 tail -f 的文件尾部追加监听
Linux 的 tail -f 本质是持续检查文件末尾是否有新字节写入,C++ 没有内置等价物,得自己轮询 + 定位。关键不是“读完就停”,而是“读到 EOF 后不退出,等新内容”。
常见错误是每次 fopen 重开文件——这会丢失文件描述符位置,且在日志轮转(logrotate)时可能跳过新文件;正确做法是保持单个 FILE* 打开,用 fseek(fp, 0, SEEK_END) 定位到当前末尾,再用 ftell 记下偏移量,下次从该位置开始读。
- 必须用
"rb"模式打开,避免文本模式下的换行符转换干扰字节定位 - 每次读前先
fseek(fp, 0, SEEK_END),再ftell获取最新长度,和上次记录的偏移比较 - 若新长度更大,说明有追加:用
fseek(fp, last_offset, SEEK_SET)回到旧位置,再fread差值字节 - 注意:如果日志被截断(
truncate或重写),last_offset可能大于当前文件大小,此时需重置为 0 并 warn
遇到日志轮转(logrotate)怎么办:检测 st_ino 和 st_dev 是否变化
真实生产环境里,logrotate 会 rename 原文件、新建同名文件,导致你一直读着一个已停止写入的旧 inode。只靠文件路径判断完全失效。
必须在每次循环中调用 stat() 检查当前打开的 FILE* 对应的 inode 和设备号是否和上次一致。C++ 中可通过 fileno(fp) 获取 fd,再用 fstat() 读取元数据。
立即学习“C++免费学习笔记(深入)”;
- 保存首次打开时的
st_ino和st_dev - 每次轮询前调用
fstat(fd, &sb),若sb.st_ino != saved_ino || sb.st_dev != saved_dev,说明文件已被轮转 - 此时应
fclose当前句柄,open新文件(路径不变),并重置last_offset = 0 - 不要依赖
access()或stat()路径——rename 后路径存在但内容已变,inode 才是唯一标识
poll() or inotify?Linux 下更轻量的替代方案
纯轮询(比如每 100ms stat 一次)浪费 CPU,但 inotify 又要处理事件队列、线程同步,对简单日志监控有点重。折中方案是用 poll() 监听文件 fd 的可读事件——仅适用于支持 POLLIN 的文件系统(ext4/xfs 通常支持,但 NFS 不一定)。
实测发现:对普通本地日志文件,poll() 配合 O_NONBLOCK 打开,比 busy-loop 更省资源,且无需额外权限或 inotify 限额管理。
- 用
open(path, O_RDONLY | O_NONBLOCK)替代fopen - 构造
struct pollfd,设置.fd = fd,.events = POLLIN -
poll()返回 > 0 时,说明有新数据可读(不一定整行,需自行缓冲解析) - 注意:
poll()不保证返回时数据已落盘,仍需结合lseek(fd, 0, SEEK_END)校验长度,防止误触发
解析日志行时为什么不能直接用 fgets()?缓冲与换行边界问题
日志写入是流式的,一行可能分多次 write(),而 fgets() 在遇到 EOF 或换行符前会阻塞或返回不完整结果。更糟的是,如果最后一行没换行符(常见于 crash 日志末尾),fgets() 就永远卡住或丢掉它。
必须自己维护读缓冲区,按字节累积,手动查找 '\n' 边界,保留未完成行供下次拼接。
- 分配固定大小缓冲区(如 4KB),每次
read()或fread()追加到 buffer 末尾 - 扫描 buffer 中所有
'\n',逐行提取(注意 Windows 的"\r\n"要统一处理) - 将最后一个
'\n'后的内容移到 buffer 开头,更新长度,作为下一轮的“残留” - 避免用
std::string频繁拼接——日志量大时内存抖动明显,用std::vector<char></char>管理更可控
真正麻烦的从来不是“怎么读到新内容”,而是“怎么确认这一行确实写完了、没被截断、也没和下一行粘连”。inode 检测、偏移校验、缓冲切分,三者缺一不可。漏掉任何一个,线上跑一周后就会开始丢日志。



















