公司有一个同事做的项目,其中有一个 Python 写的程序会反复降低 CPU 的电压直至死机重启,程序会在降压前保存本次的数据。听起来很合理,先保存数据再降低电压,如果死机了导致重启,那上次的数据也保存到本地了。但在 windows 电脑上实际运行时,每次程序导致 windows 死机重启后,保存的数据文件都为空。他没搞定这个就离职了,于是我就接手来查这个 bug 了。

查找问题

大概的代码示例如下:

1
2
3
4
5
6
7
# 写入文件
with open(file_path, "w") as f:
    f.write(some_data)
logging.warning("Write to checkpoint file")

# 降低 CPU 电压,过低会导致死机
set_voltage_offset(v_off)

从代码结构来看确实没什么问题,是先保存数据再降低电压,即使后边的操作导致死机也是在写入操作完成后,应该不会影响保存的数据才对。但事实是确实有影响,在 windows 上测试了好几次保存的数据都为空。with open() 语句是 Python 中常用的文件操作语句,不应该会导致写入异常,于是怀疑是降压操作导致的。

分析问题

在 Python 中,当使用 with open() 语句来写入文件时,它会负责管理文件的打开和关闭,通常情况下, with 语句块结束后,Python 会自动关闭文件,并确保所有数据写入硬盘。但,这个操作不是立即发生的。

当写入文件时,操作系统通常会缓存这些操作,以便一次性的将多个写入操作合并,从而提高效率。这意味着即使 Python 代码执行了写入操作(也就是 write()),也不能保证这些数据已经永久的保存到了硬盘上。如果在 with 语句块结束后立即死机,这些数据可能会丢失。

解决方法

现在知道了导致数据保存失败的原因是出在 windows 的系统缓存机制上,那只要找到方法可以强制系统将缓存的数据写入硬盘就好了。修改后的代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 写入文件
with open(file_path, "w") as f:
    f.write(some_data)
  
    # 确保数据从 Python 的内部缓冲区写入操作系统的缓冲区
    f.flush()
  
    # 确保数据从操作系统的缓冲区写入磁盘
    os.fsync(f.fileno())
logging.warning("Write to checkpoint file")

# 降低 CPU 电压,过低会导致死机
set_voltage_offset(v_off)

新增了两行代码。

  • f.flush() 的作用为刷新 Python 的内部缓冲区,确保所有数据写入操作系统的缓冲区。但这个并不能保证操作系统会立刻将数据写入硬盘。
  • os.fsync(f.fileno()) 的作用为强制操作系统将其缓冲区的数据写入硬盘。这样就保证了如果之后的代码导致系统死机,这部分数据也会完整的保存在硬盘上。

将修改后的程序在 windows 上测试,数据每次都会完整的保存在硬盘上。

后记

在 Microsoft 的一篇官方文档中有提到 disk write caching ,也就是写入缓存,并给出了关闭的方法。

关于 disk write caching ,官方的描述为:

Additionally, turning disk write caching on may increase operating system performance; however, it may also result in the loss of information if a power failure, equipment failure, or software failure occurs.

确实与我遇到的情况一样。

还有一篇更详细一点的介绍:https://learn.microsoft.com/en-US/windows/client-management/client-tools/change-default-removal-policy-external-storage-media

总的来说 disk write caching 在一般情况下可以提高性能。但在需要确保极端情况下写入数据完整性时,可以考虑关闭或者手动强制写入。