写文件时的 flush 到底是什么?为什么它会影响你的实时程序

在实时系统开发中,性能与稳定性往往是一对需要精心平衡的矛盾体。而文件 I/O 操作,尤其是 flush() 函数的使用,常常成为影响系统实时性的关键因素。

最近我就踩了一个「低级但致命」的坑:在开发机器人碰撞检测系统时,程序原本以 100Hz 稳定运行,一旦加上 CSV 数据记录功能就开始抖动、掉频,甚至偶尔出现"最后一段数据没写进文件"的情况。后来发现,关键原因之一就是我在每次写入一行 CSV 后都执行了 flush()

这篇博文我将用"工程可落地"的方式,为你解释:

  • flush 到底是什么(它做了什么、不做什么)

  • 为什么默认会有缓冲(buffer)机制

  • 为什么 flush 在实时循环中会成为性能杀手

  • 最推荐的日志写入方式(既不丢数据、又不拖慢循环)

1. 核心结论:flush 的本质

在 Python 中,flush() 的核心作用是:

将用户态(Python)缓冲区中的数据,立即推送到操作系统层面,使数据尽快进入 OS 的写入流程。

典型用法:

f.write("hello\n")
f.flush()

很多人误以为 flush() 就是"马上写到磁盘",但这并不完全准确:它通常保证数据离开 Python 的缓冲区,但未必保证数据已经"物理落盘"(关于更严格的落盘保证,后面会介绍 fsync)。


2. 为什么需要缓冲?

可以不使用缓冲,但通常会导致性能大幅下降。

文件写入不是纯内存操作,它涉及多个环节:

  • 系统调用(syscall)

  • 内核态与用户态切换

  • 文件系统处理

  • 物理设备写入(eMMC/SSD/SD 卡等)

如果每次 write() 都立即触发实际 I/O 操作,系统吞吐量会非常低。因此,几乎所有编程语言和操作系统都会采用一种策略:

先将数据写入内存缓冲区,积累到一定量后再批量写入物理存储。

这就是所谓的"缓冲"(buffering)机制。在高频数据记录场景(如 100Hz、200Hz、500Hz)中,缓冲是保证性能的关键。


3. 写文件到底经过了哪些"层"?(非常重要)

理解 flush 的前提是知道写入链路通常分多层。以下是从 Python 写入到物理磁盘的完整流程:

物理操作

内存操作

f.write

f.flush

os.fsync

f.close

操作系统后台

Python 代码

Python 层缓冲

操作系统页缓存

文件系统与块设备队列

物理存储介质

各操作的作用:

  • f.write(...):通常将数据写入 Python 或 OS 缓冲区(具体取决于缓冲策略)

  • f.flush():将 Python 层缓冲区的数据推送到 OS 层(使数据更快"可见"、减少丢失风险)

  • f.close():关闭文件时会隐式执行 flush(正常退出时非常重要)

  • os.fsync(f.fileno()):请求 OS 将数据真正同步到物理磁盘(更"硬核",但也更慢)


4. flush 的保证与限制

从工程实践角度,你可以这样理解 flush

4.1 它保证的是:数据离开 Python 缓冲区

如果你不执行 flush,数据可能仍停留在 Python 的缓冲区中,根本没有交给操作系统。一旦程序崩溃,这部分数据就会直接丢失。

4.2 它不保证的是:数据已物理写入磁盘

很多情况下,flush 后数据只是进入了 OS 的缓存,操作系统可能会在稍后才真正将其写入物理磁盘。

如果你需要更强的保证(例如防止电源突然断电导致数据丢失),则需要:

import os
f.flush()  # 先推到 OS 层
os.fsync(f.fileno())  # 再强制 OS 写入物理磁盘

但需要注意:fsync 对性能的影响更大,一般不建议在高频循环中使用。


5. 为什么"每行 flush"会拖慢实时程序?

以机器人 100Hz 数据记录为例:每 10ms 写入一行 CSV,如果每行都执行 flush()

  • 相当于每 10ms 强制触发一次写入流程

  • 对于 eMMC/SD 卡等存储介质,频繁的小块写入效率极低

  • Python 解释器开销 + I/O 操作开销叠加,会导致回调时间抖动

直观表现通常是:

  • 定时回调开始"赶不上"预定周期

  • 100Hz 的循环可能下降到 80Hz/60Hz(甚至更低)

  • 控制台输出也会出现卡顿(如果同时在每个周期执行 stdout.write

结论:flush 是为了"更安全/更及时",但它有性能成本。在高频场景下,这个成本可能直接破坏系统的实时性。


6. 何时应该使用 flush?何时不应该?

6.1 建议使用 flush 的场景

  • 程序可能随时被强制终止(如 kill -9、异常崩溃),需要尽量保存数据

  • 需要其他进程/工具尽快读取文件的最新内容(例如实时监控)

  • 写入频率较低(1Hz、5Hz),flush 的性能成本可以忽略

  • 关键日志(如审计、重要状态),允许以性能换取数据安全性

6.2 不建议每次都使用 flush 的场景

  • 高频数据流(100Hz、200Hz、500Hz 及以上)

  • 系统对周期稳定性要求高(如控制回路、检测系统)

  • 写入设备为 SD 卡/eMMC 等对随机小块写入不友好的介质


7. 高频记录的最佳实践:定期 flush + 退出前 flush

基于工程经验,我建议采用以下策略:

  1. 每行 writerow() 时不执行 flush

  2. 每 N 行执行一次 flush(或每 T 秒执行一次)

  3. 停止记录或退出程序时,执行 flush + close

7.1 按行数 flush(简单可靠)

对于 100Hz 的场景:

  • flush_every=100:约 1 秒 flush 一次

  • flush_every=200:约 2 秒 flush 一次

示例代码:

import csv
import time

# 打开文件
with open("log.csv", "w", newline="") as f:
    w = csv.writer(f)
    flush_every = 100
    row_count = 0
    
    for k in range(10000):
        # 写入数据
        w.writerow([time.time(), k])
        row_count += 1
        
        # 每 flush_every 行执行一次 flush
        if row_count % flush_every == 0:
            f.flush()
    
    # 退出前最后一次 flush
    f.flush()

7.2 按时间 flush(更灵活)

如果你不希望依赖固定频率,可以按时间间隔执行 flush:

import time

last_flush = time.time()
flush_period = 1.0  # 每 1 秒 flush 一次

# 在循环中
w.writerow(row)
current_time = time.time()
if current_time - last_flush >= flush_period:
    f.flush()
    last_flush = current_time

8. flush 与 close 的关系

为什么"正常退出"时即使不手动 flush 也常常没问题?

因为 close() 方法通常会执行两个操作:

  1. 将缓冲区内容写出(相当于执行 flush

  2. 释放文件句柄

所以,如果你的程序总是能正常结束(执行到 close()),即使不手动 flush,多数情况下文件也会完整。

但在机器人系统中,以下情况更为常见:

  • Ctrl+C 中断

  • 节点异常崩溃

  • 进程被监控系统重启

  • 系统突然重启

这些情况下,"最后几秒"的数据最容易丢失,因此才建议采用定期 flush + 退出前 flush 的策略。


9. ROS2/机器人系统的实际建议

如果你正在开发类似这样的系统:

  • joint_states 等高频输入(可能达到 500Hz)

  • 检测循环以 100Hz 运行

  • 使用 CSV 进行离线分析/数据回放

那么建议:

  1. 优先保证检测循环的稳定性,避免写盘操作影响控制周期

  2. CSV 写入采用"缓冲写"策略,例如每 1 秒执行一次 flush

  3. 如果你同时在控制台输出信息,也建议降低输出频率(例如每 10Hz 更新一次)

实施这三条建议后,100Hz 的控制循环通常会稳定很多。


10. 常见误区澄清

误区 A:flush 就是立即写入磁盘

错误。多数情况下,flush 只是将数据从 Python 缓冲区推送到 OS 的写入路径,并不保证物理落盘。要实现真正的落盘保证,需要使用 fsync,但会带来更大的性能开销。

误区 B:不 flush 就一定会丢数据

错误。正常执行 close() 时会自动执行 flush。但在异常退出或断电情况下,未 flush 的数据丢失风险会显著增加。

误区 C:flush 越多越安全

不完全正确。更频繁的 flush 确实能提高数据安全性,但会带来更大的性能开销,可能导致实时系统变得不稳定,在检测/控制系统中反而得不偿失。


11. 100Hz 场景下的 flush 参数选择

以 100Hz 数据记录为例,不同 flush 间隔的影响:

  • flush_every=100:约 1 秒的丢失风险窗口(最坏情况下丢失 1 秒内的数据)

  • flush_every=50:约 0.5 秒的窗口(更安全,但开销略高)

  • flush_every=200:约 2 秒的窗口(更节省 I/O,但最坏情况下可能丢失 2 秒数据)

具体选择应根据你的应用场景和对数据丢失的容忍度来决定。


12. 总结

flush() 的本质是在 性能数据及时性/安全性 之间进行权衡:

  • 需要实时性/稳定频率:避免每行都执行 flush

  • 需要尽量不丢数据:采用定期 flush + 退出前 flush 的策略

  • 需要硬实时落盘:考虑使用 fsync(但要接受性能损失)

如果你正在开发 100Hz 的机器人监测/碰撞检测系统,将"每行 flush"改为"定期 flush",几乎总能获得立竿见影的性能提升。

希望这篇文章能帮助你在实际工程中更好地平衡数据安全性与系统性能!

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐