【问题标题】:Ring buffer log file on unixunix 上的环形缓冲区日志文件
【发布时间】:2025-12-20 21:55:11
【问题描述】:

我正在尝试提出一个 unix 命令管道,它允许我仅将程序输出的最新 n 行记录到文本文件中。

文本文件的长度不得超过 n 行。 (第一次填文件的时候可能会少)

它将在内存/资源有限的设备上运行,因此保持较小的文件大小是当务之急。

我试过这样的东西(n=500):

program_spitting_out_text > output.txt
cat output.txt | tail -500 > recent_output.txt
rm output.txt

program_spitting_out_text | tee output.txt | tail -500 > recent_output.txt

显然这两种方法都不适合我的目的......

任何人有一个很好的方法来做到这一点?还是我必须编写脚本/实用程序?

注意:我不想与 dmesg 有任何关系,并且必须使用标准的 BSD unix 命令。 “program_spitting_out_text”每秒打印大约 60 行。

提前致谢!

【问题讨论】:

  • program_spitting_out_text 是保持文件打开还是打开/关闭每个日志行输出?
  • 更重要的是 - 假设您想要日志,为什么您只想要一些临时行。假设您遇到错误情况。你打算实时阅读吗?这就是这个设计所需要的。您需要考虑将数据移到其他地方。否则根本不要理会日志。
  • 第二个选项有什么问题? (当然省略了tee 部分)
  • @paxdiablo 好问题,我不知道答案。由于 program_spitting_out_text 只是将文本打印到屏幕上,“ > output.txt”的行为真的取决于程序吗?我会假设这一行 program_spitting_out_text > output.txt 将使文件保持打开状态。
  • @jimmcnamara program_spitting_out_text 会在某个时候死掉。我想先捕获 500 行输出。

标签: unix logging pipeline tail


【解决方案1】:

如果program_spitting_out_text 连续运行并保持文件打开,那么您无能为力。

即使删除文件也无济于事,因为它仍然会继续写入现在“隐藏”的文件(数据仍然存在,但没有目录条目),直到它关闭它,此时它真的是删除。


如果它定期关闭并重新打开日志文件(每行或每十秒或其他),那么您有一个相对简单的选择。

只需监视文件直到达到一定大小,然后翻转文件,例如:

while true; do
    sleep 5
    lines=$(wc -l <file.log)
    if [[ $lines -ge 5000 ]]; then
        rm -f file2.log
        mv file.log file2.log
        touch file.log
    fi
done

此脚本将每 5 秒检查一次文件,如果超过 5000 行,则将其移至备份文件。写入它的程序将继续写入该备份文件(因为它有打开的句柄)直到它关闭它,然后它将重新打开新文件。

这意味着您将始终(大约)在日志文件集中有 5 到 10000 行,您可以使用将两者结合的命令来搜索它们:

grep ERROR file2.log file.log

另一种可能性是您是否可以定期重新启动程序而不会影响其功能。举例来说,一个每秒查找一次文件是否存在并报告该文件的程序可能可以毫无问题地重新启动。一个计算 PI 到 1000 亿有效数字可能不会在没有影响的情况下重新启动。

如果它可重新启动的,那么您基本上可以执行与上述相同的技巧。当日志文件达到一定大小时,终止当前程序(您将作为脚本的后台任务启动该程序),执行您需要的任何魔法来滚动日志文件,然后重新启动程序。

例如,考虑以下(可重新启动)程序prog.sh,它只是连续输出当前日期和时间:

#!/usr/bin/bash
while true; do
    date
done

然后,以下脚本将负责根据需要启动和停止另一个脚本,方法是每 5 秒检查一次日志文件以查看它是否已超出其限制:

#!/usr/bin/bash

exe=./prog.sh
log1=prog.log
maxsz=500

pid=-1
touch ${log1}
log2=${log1}-prev

while true; do
    if [[ ${pid} -eq -1 ]]; then
        lines=${maxsz}
    else
        lines=$(wc -l <${log1})
    fi
    if [[ ${lines} -ge ${maxsz} ]]; then
        if [[ $pid -ge 0 ]]; then
            kill $pid >/dev/null 2>&1
        fi
        sleep 1
        rm -f ${log2}
        mv ${log1} ${log2}
        touch ${log1}
        ${exe} >> ${log1} &
        pid=$!
    fi
    sleep 5
done

这个输出(来自两个日志文件上的每秒wc -l)显示了切换时发生的情况,并指出它只是近似值,因为切换时涉及延迟:

474 prog.log       0 prog.log-prev
496 prog.log       0 prog.log-prev
518 prog.log       0 prog.log-prev
539 prog.log       0 prog.log-prev
542 prog.log       0 prog.log-prev
 21 prog.log     542 prog.log-prev

现在请记住,这是一个示例脚本。它相对智能,但可能需要一些错误处理,以便在您关闭监视器时它不会让可执行文件继续运行。


最后,如果这些都不够,那么没有什么能阻止您编写自己的过滤器程序,该程序接受标准输入并将其连续输出到真正的环形缓冲区文件。

那么你只需这样做:

program_spitting_out_text | ringbuffer 4096 last4k.log

该程序可能是一个真正的环形缓冲区,因为它将 4k 文件视为循环字符缓冲区,但当然,您需要在文件中使用特殊标记来指示写入点,以及一个程序可以将其变回真实的流。

或者,它可以与上面的脚本做很多相同的事情,重写文件,使其始终低于所需的大小。

【讨论】:

  • 谢谢,@paxdiablo。我考虑过这种在日志文件之间进行乒乓球的方法,但我希望我忽略了一些可以完成工作的很酷的单线。
【解决方案2】:

由于 GNU/Linux 上显然不存在这个基本功能(循环文件),并且因为我需要它来跟踪存储空间有限的 Raspberry Pi 上的日志,所以我只是按照上面的建议编写了代码!

看:circFS

与本文中引用的其他工具和其他类似工具不同,最大大小是任意的,仅受实际可用存储空间的限制。 它不会与多个文件一起旋转,所有文件都保存在单个文件中,该文件在“发布”时被重写。 您可以在虚拟目录中拥有任意数量的日志文件。

它是一个单一的 C 文件(包括 cmets 约 600 行),在安装了 fuse 开发依赖项后,它使用单一的编译行构建。

第一个版本非常基础(参见 README),如果您想通过一些 TODO(参见 TODO)对其进行改进,欢迎提交拉取请求。

开个玩笑,这是我的第一个“只写”保险丝驱动程序! :-)

【讨论】: