调用 close() 后大文件没有立即刷新到磁盘?

Posted

技术标签:

【中文标题】调用 close() 后大文件没有立即刷新到磁盘?【英文标题】:Large file not flushed to disk immediately after calling close()? 【发布时间】:2012-11-25 13:33:29 【问题描述】:

我正在使用我的 python 脚本创建大文件(超过 1GB,实际上有 8 个)。在我创建它们之后,我必须创建将使用这些文件的进程。

脚本如下:

# This is more complex function, but it basically does this:
def use_file():
    subprocess.call(['C:\\use_file', 'C:\\foo.txt']);


f = open( 'C:\\foo.txt', 'wb')
for i in 10000:
    f.write( one_MB_chunk)
f.flush()
os.fsync( f.fileno())
f.close()

time.sleep(5) # With this line added it just works fine

t = threading.Thread( target=use_file)
t.start()

但是应用程序use_file 的行为就像foo.txt 是空的。发生了一些奇怪的事情:

如果我在控制台中执行C:\use_file C:\foo.txt(脚本完成后)我会得到正确的结果 如果我在另一个 python 控制台中手动执行 use_file() 我会得到正确的结果 C:\foo.txt 在调用 open() 后立即在磁盘上可见,但在脚本结束前保持大小 0B 如果我添加 time.sleep(5),它就会按预期(或者更确切地说是需要)开始工作

我已经找到了:

os.fsync() 但它似乎不起作用(use_file 的结果好像 C:\foo.txt 是空的) 使用buffering=(1<<20)(打开文件时)似乎也不起作用

我对这种行为越来越好奇。

问题:

python fork close() 是否在后台运行?这是在哪里记录的? 如何解决这个问题? 我错过了什么吗? 添加sleep后:这是windows/python的bug吗?

注意事项:(以防对方出现问题)应用use_data使用:

handle = CreateFile("foo.txt", GENERIC_READ, FILE_SHARE_READ, NULL,
                               OPEN_EXISTING, 0, NULL);
size = GetFileSize(handle, NULL)

然后处理来自foo.txtsize 字节。

【问题讨论】:

我怀疑你遗漏了一些东西,比如完整路径与本地文件。 为什么将使用"wb"(二进制模式)编写的文件命名为foo.txt?令人困惑。 @unwind 这些大文件的源文件采用不同的编码,我们希望保留原始文件的编码(为此目的,使用字节似乎工作得很好)。 @unwind:在 python 3 中,如果你不想/不需要处理文本文件的编码,你可以用二进制模式打开它们。在那里完全有效地使用wb 您确定磁盘上有足够的空间吗?有一个bug in Python 3 that a file stays open after f.close() if implicit call to f.flush() fails。尝试在 f.close() 之前显式调用 f.flush()。 os.fsync() 不应该是必要的,除非出现电源故障。 【参考方案1】:

f.close() 调用f.flush(),后者将数据发送到操作系统。 不一定将数据写入磁盘,因为操作系统会缓冲它。正如您正确计算的那样,如果您想强制操作系统将其写入磁盘,您需要os.fsync()

您是否考虑过将数据直接传送到use_file


编辑:你说os.fsync()'不起作用'。澄清一下,如果你这样做了

f = open(...)
# write data to f
f.flush()
os.fsync(f.fileno())
f.close()

import pdb; pdb.set_trace()

然后看看磁盘上的文件,有没有数据?

【讨论】:

不幸的是,管道数据不是一个选项(它是我们正在使用的专有应用程序)。我现在要检查这 3 个命令是否在它们应该执行的时间和地点执行。请注意subprocess.call 在新线程中执行(调用close 后创建)。 默认情况下,Windows 在硬盘上使用写入缓存,因此通常几乎没有数据会立即写入磁盘。您可以在设备管理器中通过双击磁盘驱动器下的硬盘然后切换到策略选项卡并取消设置旁边的复选标记来禁用此功能, 添加sleep(5)(添加到我的问题中)解决了这个问题。但我仍在寻找更多 pythonic正确 方法来做到这一点。 虽然flush()不一定会立即将数据写入物理磁盘,但其他应用程序(从缓存中)应该仍然可以立即看到它。【参考方案2】:

编辑:更新了特定于 Python 3.x 的信息

https://bugs.python.org/issue4944 上有一个超级老的错误报告讨论了一个可疑的类似问题。我做了一个显示错误的小测试:https://gist.github.com/estyrke/c2f5d88156dcffadbf38

在上面的错误链接中得到用户 eryksun 的精彩解释后,我现在明白为什么会发生这种情况,这本身并不是错误。在 Windows 上创建子进程时,默认情况下它会从父进程继承所有打开的文件句柄。因此,您看到的可能实际上是共享冲突,因为您尝试在子进程中读取的文件已打开,可以通过 另一个 子进程中的继承句柄进行写入。导致这种情况的可能事件序列(使用上面 Gist 中的再现示例):

Thread 1 opens file 1 for writing
  Thread 2 opens file 2 for writing
  Thread 2 closes file 2
  Thread 2 launches child 2
  -> Inherits the file handle from file 1, still open with write access
Thread 1 closes file 1
Thread 1 launches child 1
-> Now it can't open file 1, because the handle is still open in child 2
Child 2 exits
-> Last handle to file 1 closed
Child 1 exits

当我编译简单的 C 子程序并在我的机器上运行脚本时,它在 Python 2.7.8 的大多数情况下至少在一个线程中失败。在 Python 3.2 和 3.3 中,没有重定向的测试脚本不会失败,因为当不使用重定向时,subprocess.callclose_fds 参数的默认值现在是 True。使用重定向的其他测试脚本在这些版本中仍然失败。在 Python 3.4 中,这两个测试都成功了,因为 PEP 446 默认情况下使所有文件句柄都不可继承。

结论

从 Python 中的线程生成子进程意味着子进程继承所有打开的文件句柄,即使是从生成子进程的线程之外的其他线程。这至少对我来说不是特别直观。

可能的解决方案:

升级到 Python 3.4,默认情况下文件句柄是不可继承的。 将close_fds=True 传递给subprocess.call 以完全禁用继承(这是Python 3.x 中的默认设置)。请注意,这会阻止子进程的标准输入/输出/错误的重定向。 确保在生成新进程之前关闭所有文件。 在 Windows 上使用os.open 打开带有os.O_NOINHERIT 标志的文件。 tempfile.mkstemp 也使用此标志。

请改用 win32api。为 lpSecurityAttributes 参数传递 NULL 指针也可以防止继承描述符:

from contextlib import contextmanager
import win32file

@contextmanager
def winfile(filename):
    try:
        h = win32file.CreateFile(filename, win32file.GENERIC_WRITE, 0, None, win32file.CREATE_ALWAYS, 0, 0)
        yield h
    finally:
        win32file.CloseHandle(h)

with winfile(tempfilename) as infile:
    win32file.WriteFile(infile, data)

【讨论】:

OP 没有报告共享冲突。我想也许这是一个不同的问题? @HarryJohnston 嗯,你是对的。我认为这仅仅是因为 OP 根本没有检查他的错误代码(因为症状完全一样),但它可能如你所说。 @HarryJohnston 我重写了答案以使其更清晰,并且总体上更清晰。 你使用什么 Python 版本? the tests 中的任何一个在您的 Windows 机器上都失败了吗? All handles are closed in OPs code (no redirection). @JFSebastian 我使用过 2.7.8、2.7.9(显示问题)和 3.4.1(不显示问题,鉴于您链接的 PEP,这是预期的:默认是制作句柄自 3.4 起不可继承)。您的所有测试都没有在我的机器上失败,但话又说回来,据我所知,它们与我在回答中给出的解释无关。 OP 明确表示他的实际功能更复杂,所以我不相信不涉及重定向 - Vyktor,你能确认一下吗?

以上是关于调用 close() 后大文件没有立即刷新到磁盘?的主要内容,如果未能解决你的问题,请参考以下文章

Linux系统中如何操作文件?

刷新磁盘写缓存

File.WriteAllText 不将数据刷新到磁盘

如何在 C# 中清空/刷新 Windows READ 磁盘缓存?

如果并行执行磁盘文件操作会更快吗?

Commit后数据会被立即写进磁盘文件吗?