Какие нюансы с файловыми дескрипторами при fork
Что проверяют
Понимаете ли вы разницу между дескриптором и открытым файлом в ядре, и чем это оборачивается в Python-сервисах с pre-fork моделью: Gunicorn, Celery, multiprocessing. Следом обычно спрашивают, почему после форка соединение с базой начинает отдавать чужие ответы.
Как отвечать
- что копируется:
fork()копирует таблицу дескрипторов, но оба номера указывают на одно открытое описание файла в ядре. Смещение и флаги общие: сдвинул позицию родитель, сдвинулась и у потомка. Закрытие дескриптора в одном процессе на другой не влияет; - буферы Python:
printи файловые объекты держат данные в пользовательском буфере, и он копируется вместе с памятью. Безflush()до форка одна строка попадёт в файл дважды. По той же причине потомок завершают черезos._exit(), а неsys.exit(); - сокеты и соединения: общий слушающий сокет это основа pre-fork серверов, а общее соединение с PostgreSQL или Redis ломает протокол, ответы перемешиваются. Соединения открывают после форка: хук
post_forkв Gunicorn, сигналworker_process_initв Celery,engine.dispose(close=False)в SQLAlchemy; - exec: с Python 3.4 (PEP 446) дескрипторы, созданные Python, ненаследуемые, то есть закрываются при
exec. На самforkэто не влияет.subprocessпо умолчанию работает сclose_fds=True; - потоки: в потомке остаётся только вызвавший поток, а блокировки, занятые другими потоками, так и остаются занятыми. Отсюда дедлоки в
logging. Python 3.12 предупреждает оforkв многопоточном процессе, а в 3.14multiprocessingна Linux по умолчанию перешёл наforkserver.
Где спотыкаются
Говорят, что дескрипторы в потомке «независимые копии». Номера независимые, а смещение общее, и параллельная запись без O_APPEND перетирает данные.
Забывают закрыть ненужный конец пайпа. Читатель так и не получит EOF, потому что пишущий конец остался открытым в другом процессе.
Предлагают защищать общий дескриптор мьютексом из threading. Между процессами он не работает: правильнее не делить дескриптор вовсе или взять multiprocessing.Lock.