Python Подпроцесс: как / когда они закрывают файл? - PullRequest
0 голосов
/ 30 марта 2020

Интересно, почему подпроцессы держат так много открытых файлов. У меня есть пример, в котором некоторые файлы остаются открытыми навсегда (после завершения подпроцесса и даже после сбоя программы).

Рассмотрим следующий код:

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, но он все равно вылетает. Этот ответ предполагает , возможно, причина возникновения проблемы, и ошибка также указывает на строку errpipe_read, errpipe_write = os.pipe(). Но не похоже, что это проблема (запуск без этой опции выдает ту же ошибку).

Если вам нужна дополнительная информация, вот обзор из вывода 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 выглядят как файлы le git (библиотеки открываются, например, pandas / numpy / scipy / et c.).

Теперь у меня есть некоторые сомнения: может быть, проблема в aiofiles асинхронном диспетчере контекста? Может это тот, который не закрывает файлы и не create_subprocess_exec?

Здесь есть похожий вопрос, но никто действительно не пытается объяснить / решить проблему (и только предлагает увеличить ограничение): Python Подпроцесс: слишком много открытых файлов . Мне бы очень хотелось понять, что происходит, моя первая цель - не обязательно временно решить проблему (в будущем я хочу иметь возможность запускать функцию fds_test столько раз, сколько потребуется). Моя цель - иметь функцию, которая ведет себя так, как ожидалось. Я, вероятно, должен изменить либо мое ожидание, либо мой код, поэтому я задаю этот вопрос.


Как предлагается в комментариях здесь , я также попытался запустить 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

Так что, похоже, файлы должны быть правильно закрыты Я немного потерян здесь.

1 Ответ

1 голос
/ 31 марта 2020

Проблема не в create_subprocess_exec проблема в этом коде в том, что tempfile.mkstemp() фактически открывает файл:

mkstemp () возвращает кортеж, содержащий дескриптор уровня операционной системы для открытого файла (как будет возвращено функцией 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, возможно, потому что подпроцесс открывает так много файлов, что делает маловероятным, что временный создание файла - это то, что нарушает предел

...