Python 子进程:how/when 他们关闭文件了吗?

Python Subprocess: how/when do they close file?

我想知道为什么子进程会打开这么多文件。我有一个示例,其中某些文件似乎永远保持打开状态(在子进程完成后甚至程序崩溃后)。

考虑以下代码:

import aiofiles
import tempfile

async def main():
    return [await fds_test(i) for i in range(2000)]

async def fds_test(index):
    print(f"Writing {index}")
    handle, temp_filename = tempfile.mkstemp(suffix='.dat', text=True)
    async with aiofiles.open(temp_filename, mode='w') as fp:
        await fp.write('stuff')
        await fp.write('other stuff')
        await fp.write('EOF\n')

    print(f"Reading {index}")
    bash_cmd = 'cat {}'.format(temp_filename)
    process = await asyncio.create_subprocess_exec(*bash_cmd.split(), stdout=asyncio.subprocess.DEVNULL, close_fds=True)
    await process.wait()
    print(f"Process terminated {index}")

if __name__ == "__main__":
    import asyncio
    asyncio.run(main())

这会一个接一个地(按顺序)生成进程。我希望由此同时打开的文件数也是一个。但事实并非如此,在某些时候我收到以下错误:

/Users/cglacet/.pyenv/versions/3.8.0/lib/python3.8/subprocess.py in _execute_child(self, args, executable, preexec_fn, close_fds, pass_fds, cwd, env, startupinfo, creationflags, shell, p2cread, p2cwrite, c2pread, c2pwrite, errread, errwrite, restore_signals, start_new_session)
   1410             # Data format: "exception name:hex errno:description"
   1411             # Pickle is not used; it is complex and involves memory allocation.
-> 1412             errpipe_read, errpipe_write = os.pipe()
   1413             # errpipe_write must not be in the standard io 0, 1, or 2 fd range.
   1414             low_fds_to_close = []

OSError: [Errno 24] Too many open files

我试过 运行 在没有选项 stdout=asyncio.subprocess.DEVNULL 的情况下使用相同的代码,但它仍然崩溃。 This answer suggested 可能是问题出处,错误也指向 errpipe_read, errpipe_write = os.pipe() 行。但这似乎不是问题所在(没有该选项的 运行ning 会给出相同的错误)。

如果您需要更多信息,这里是 lsof | grep python 输出的概述:

python3.8 19529 cglacet    7u      REG                1,5        138 12918796819 /private/var/folders/sn/_pq5fxn96kj3m135j_b76sb80000gp/T/tmpuxu_o4mf.dat
# ... 
# ~ 2000 entries later : 
python3.8 19529 cglacet 2002u      REG                1,5        848 12918802386 /private/var/folders/sn/_pq5fxn96kj3m135j_b76sb80000gp/T/tmpcaakgz3f.dat

这些是我的子进程正在读取的临时文件。 lsof 的其余输出似乎是合法的东西(打开的库,如 pandas/numpy/scipy/等)。

现在我有点怀疑:也许问题来自aiofiles 异步上下文管理器?也许是没有关闭文件而不是 create_subprocess_exec

这里有一个类似的问题,但没有人真正尝试 explain/solve 这个问题(并且只建议增加限制):Python Subprocess: Too Many Open Files。我真的很想了解发生了什么,我的首要目标不一定是暂时解决问题(将来我希望能够 运行 function fds_test 根据需要多次)。我的目标是拥有一个按预期运行的功能。我可能不得不改变我的期望或我的代码,这就是我问这个问题的原因。


正如评论 here 中所建议的那样,我也尝试 运行 python -m test test_subprocess -m test_close_fds -v 给出:

== CPython 3.8.0 (default, Nov 28 2019, 20:06:13) [Clang 11.0.0 (clang-1100.0.33.12)]
== macOS-10.14.6-x86_64-i386-64bit little-endian
== cwd: /private/var/folders/sn/_pq5fxn96kj3m135j_b76sb80000gp/T/test_python_52961
== CPU count: 8
== encodings: locale=UTF-8, FS=utf-8
0:00:00 load avg: 5.29 Run tests sequentially
0:00:00 load avg: 5.29 [1/1] test_subprocess
test_close_fds (test.test_subprocess.POSIXProcessTestCase) ... ok
test_close_fds (test.test_subprocess.Win32ProcessTestCase) ... skipped 'Windows specific tests'

----------------------------------------------------------------------

Ran 2 tests in 0.142s

OK (skipped=1)

== Tests result: SUCCESS ==

1 test OK.

Total duration: 224 ms
Tests result: SUCCESS

所以看起来文件应该正确关闭,我在这里有点迷路。

问题并非来自 create_subprocess_exec 此代码中的问题是 tempfile.mkstemp() 实际上打开了文件:

mkstemp() returns a tuple containing an OS-level handle to an open file (as would be returned by os.open()) …

我以为它只会创建文件。为了解决我的问题,我只是添加了对 os.close(handle) 的调用。这消除了错误但有点奇怪(打开文件两次)。所以我将其重写为:

import aiofiles
import tempfile
import uuid


async def main():
    await asyncio.gather(*[fds_test(i) for i in range(10)])

async def fds_test(index):
    dir_name = tempfile.gettempdir()
    file_id = f"{tempfile.gettempprefix()}{uuid.uuid4()}"
    temp_filename = f"{dir_name}/{file_id}.dat"

    async with aiofiles.open(temp_filename, mode='w') as fp:
        await fp.write('stuff')

    bash_cmd = 'cat {}'.format(temp_filename)
    process = await asyncio.create_subprocess_exec(*bash_cmd.split(), close_fds=True)
    await process.wait()


if __name__ == "__main__":
    import asyncio
    asyncio.run(main())

现在我想知道为什么错误是由 subprocess 而不是 tempfile.mkstemp 引发的,可能是因为它的子进程打开了太多的文件,以至于创建临时文件不太可能破坏限制……