Как объяснить повторный запуск RuntimeError, вызванный печатью в обработчиках сигналов?

Код:

# callee.py
import signal
import sys
import time


def int_handler(*args):
    for i in range(10):
        print('INTERRUPT', args)
    sys.exit()


if __name__ == '__main__':

    signal.signal(signal.SIGINT, int_handler)
    signal.signal(signal.SIGTERM, int_handler)
    while 1:
        time.sleep(1)

# caller.py
import subprocess
import sys


def wait_and_communicate(p):
    out, err = p.communicate(timeout=1)
    print('========out==========')
    print(out.decode() if out else '')
    print('========err==========')
    print(err.decode() if err else '')
    print('=====================')


if __name__ == '__main__':

    p = subprocess.Popen(
        ['/usr/local/bin/python3', 'callee.py'],
        stdout=sys.stdout,
        stderr=subprocess.PIPE,
    )
    while 1:
        try:
            wait_and_communicate(p)
        except KeyboardInterrupt:
            p.terminate()
            wait_and_communicate(p)
            break
        except subprocess.TimeoutExpired:
            continue

Просто выполните caller.py и затем нажмите Ctrl+C, программа вызовет RuntimeError: reentrant call inside <_io.BufferedWriter name='<stdout>'> случайном порядке. Из документации я узнаю, что обработчики сигналов вызываются асинхронно, и в этом случае два сигнала SIGINT (действие Ctrl+C) и SIGTERM (p.terminate()) отправляются почти одновременно, вызывая состояние гонки.

Однако из этого поста я узнаю, что signal модуль не выполняет обработчик сигнала внутри низкоуровневого (C) обработчика. Вместо этого он устанавливает флаг, и интерпретатор проверяет флаг между инструкциями байт-кода, а затем вызывает обработчик сигнала python. Другими словами, хотя обработчики сигналов могут испортить поток управления в главном потоке, инструкция байт-кода всегда атомарна.

Это, кажется, противоречит результату моего примера программы. Насколько мне известно, print и неявный _io.BufferedWriter реализованы на чистом C, и, следовательно, вызов функции print должен потреблять только одну инструкцию байт-кода (CALL_FUNCTION). Я запутался: как одна функция может быть повторно введена в одной непрерывной инструкции в одном потоке?

Я использую Python 3.6.2.

Ответ 1

Вы можете предпочесть запретить доставку SIGINT ребенку, поэтому нет расы, возможно, если поместить его в другую группу процессов или заставить его игнорировать сигнал. Тогда только SIGTERM от родителя будет иметь значение.

Чтобы узнать, где это было прервано, используйте это:

    sig_num, frame = args
    print(dis.dis(frame.f_code.co_code))
    print(frame.f_lasti)

Смещения байт-кода в левом поле соответствуют последней выполненной инструкции смещения.

Другие элементы, представляющие интерес, включают frame.f_lineno, frame.f_code.co_filename и frame.f_code.co_names.

Эта проблема становится спорной в Python 3.7.3, который больше не проявляет симптомы.