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

15

我正在使用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是否将close()操作分叉到后台?这在哪里记录?
  • 如何解决这个问题?
  • 我有什么遗漏吗?
  • 添加sleep后:那是Windows / Python的错误吗?

注:(如果对方出现问题)应用程序use_data使用:

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

然后从foo.txt读取size字节的数据。


1
@unwind 源文件对于那些大文件采用不同的编码方式,我们希望保留原始文件的编码方式(使用字节似乎可以很好地实现这个目的)。 - Vyktor
1
@unwind:在Python 3中,如果您不想/不需要处理文本文件的编码,可以选择以二进制模式打开它们。使用wb是完全有效的。 - Martijn Pieters
1
你确定磁盘上有足够的空间吗?在Python 3中存在一个bug,即如果隐式调用f.flush()失败,则文件在f.close()后仍然保持打开状态。尝试在f.close()之前显式调用f.flush()。除非发生电源故障,否则不需要使用os.fsync() - jfs
我有完全相同的问题。但在我的情况下,外部程序是微软的HLSL编译器fxc.exe,所以我不知道它如何访问文件。此外,我的文件非常小,只有几千字节。你最终找到问题出在哪里了吗? - Emil Styrke
@Vyktor 哦,太糟糕了。这也是我找到的唯一解决方案。 - Emil Styrke
显示剩余14条评论
2个回答

10

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 后创建)。 - Vyktor
默认情况下,Windows 在硬盘上使用写入缓存,因此通常几乎没有数据立即写入磁盘。您可以通过在设备管理器中双击磁盘驱动器下的硬盘,然后切换到策略选项卡并取消选中该设置旁边的复选框来禁用此功能。 - martineau
在我的问题中添加sleep(5)(已添加)解决了该问题。但我仍然在寻找更加“pythonic”和正确的方法来解决这个问题。 - Vyktor
8
虽然 flush() 不一定会立即将数据写入物理磁盘,但它仍应该立即对其他应用程序可见(从缓存中)。 - Harry Johnston

6

编辑:更新了与Python 3.x相关的信息

有一个非常古老的错误报告讨论了一个非常相似的问题,网址是https://bugs.python.org/issue4944。我进行了一个小测试,以展示这个错误:https://gist.github.com/estyrke/c2f5d88156dcffadbf38

在上面链接的错误报告中,用户eryksun给出了一个很好的解释,我现在明白为什么会发生这种情况了,而且这并不是一个实际的bug。在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线程中生成子进程意味着该子进程将继承所有打开的文件句柄,甚至是在生成子进程的线程之外的其他线程中打开的文件句柄。至少对于我来说,这并不是特别直观的。
可能的解决方案:
  • Upgrade to Python 3.4, where file handles are non-inheritable by default.
  • Pass close_fds=True to subprocess.call to disable inheriting altogether (this is the default in Python 3.x). Note though that this prevents redirection of the child process' standard input/output/error.
  • Make sure all files are closed before spawning new processes.
  • Use os.open to open files with the os.O_NOINHERIT flag on Windows.
    • tempfile.mkstemp also uses this flag.
  • Use the win32api instead. Passing a NULL pointer for the lpSecurityAttributes parameter also prevents inheriting the descriptor:

    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)
    

1
OP没有报告共享违规。我认为这可能是另一个问题? - Harry Johnston
@HarryJohnston 嗯,你是对的。我认为这可能仅仅是因为提问者没有检查他的错误代码(因为症状除此之外完全相同),但也有可能是你说的那样。 - Emil Styrke
@J.F.Sebastian 我已经使用了2.7.8、2.7.9(显示问题)和3.4.1(不显示问题,这是预期的,因为你链接的 PEP 指定从3.4开始默认使句柄不可继承)。在我的机器上,你的测试都没有失败,但是我认为它们与我在答案中给出的解释无关。原帖明确表示他的实际函数更复杂,所以我不能确定是否涉及重定向 - Vyktor,你能确认一下吗? - Emil Styrke
@EmilStyrke: 这个问题涉及到Python-3.x,其中I/O不使用C的FILE*(与Python 2不同)。我本来期望测试会通过,但还是想问一下。在Python 3.4之前,如果没有重定向,句柄是否关闭? - jfs
@J.F.Sebastian: 确实,我之前错过了python-3.x标签。在2.7中,无论是否重定向,都会失败;在3.2和3.3中,它可以在没有重定向的情况下工作,但是在重定向时会失败(即它跟踪close_fds默认值)。在3.4中,它可以正常工作,因为默认情况下处理是非继承的。我猜我们得等待OP澄清是否使用了重定向。 - Emil Styrke
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接