Почему python генерирует исключение sigpipe при закрытии файла fifo? - PullRequest
1 голос
/ 07 февраля 2020

TL; DR: Почему закрытие файла fifo (именованного канала), получившего исключение SIGPIPE, приводит к созданию другого исключения SIGPIPE?

Мой сценарий python записывает байты в другой процесс, который является подпроцессом мой python процесс, через файл FIFO. (Существуют некоторые ограничения, которые я должен использовать именованный канал.)

Я должен учитывать тот факт, что подпроцесс может завершиться преждевременно. Если это произойдет, мой python сценарий должен пожинать мертвый подпроцесс и запустить его снова.

Чтобы увидеть, умирает ли подпроцесс, я просто сначала пытаюсь записать в FIFO, и если я получаю исключение SIGPIPE ( на самом деле IOError указывает на разорванный канал), я знаю, что пришло время перезапустить мой подпроцесс.

Минимальный пример выглядит следующим образом:

#!/usr/bin/env python3
import os
import signal
import subprocess

# The FIFO file.
os.mkfifo('tmp.fifo')

# A subprocess to simply discard any input from the FIFO.
FNULL = open(os.devnull, 'w')
proc = subprocess.Popen(['/bin/cat', 'tmp.fifo'], stdout=FNULL, stderr=FNULL)
print('pid = %d' % proc.pid)

# Open the FIFO, and MUST BE BINARY MODE.
fifo = open('tmp.fifo', 'wb')

# Endlessly write to the FIFO.
while True:

    # Try to write to the FIFO, restart the subprocess on demand, until succeeded.
    while True:
        try:
            # Optimistically write to the FIFO.
            fifo.write(b'hello')
        except IOError as e:
            # The subprocess died. Close the FIFO and reap the subprocess.
            fifo.close()
            os.kill(proc.pid, signal.SIGKILL)
            proc.wait()

            # Start the subprocess again.
            proc = subprocess.Popen(['/bin/cat', 'tmp.fifo'], stdout=FNULL, stderr=FNULL)
            print('pid = %d' % proc.pid)
            fifo = open('tmp.fifo', 'wb')
        else:
            # The write goes on well.
            break

Чтобы воспроизвести результат, запустите этот скрипт и вручную убейте подпроцесс kill -9 <pid>. Трассировка скажет, что

Traceback (most recent call last):
  File "./test.py", line 24, in <module>
    fifo.write(b'hello')
BrokenPipeError: [Errno 32] Broken pipe

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "./test.py", line 27, in <module>
    fifo.close()
BrokenPipeError: [Errno 32] Broken pipe

Так почему закрытие файла FIFO создает другое исключение SIGPIPE?

Я выполнил тест на следующих платформах, и результаты были одинаковыми.

Python 3.7.6 @ Darwin Kernel Version 19.3.0 (MacOS 10.15.3)
Python 3.6.8 @ Linux 4.18.0-147.3.1.el8_1.x86_64 (Centos 8)

1 Ответ

1 голос
/ 07 февраля 2020

Это потому, что Python не очистит буфер записи при сбое fifo.write. Таким образом, буфер будет снова записан в сломанный канал при выполнении fifo.close, что вызывает вторую SIGPIPE.

. Я нашел причину с помощью strace. Вот некоторые подробности.

Сначала измените небольшую часть этого Python кода следующим образом:

#!/usr/bin/env python3
import os
import signal
import subprocess

# The FIFO file.
os.mkfifo('tmp.fifo')

# A subprocess to simply discard any input from the FIFO.
FNULL = open(os.devnull, 'w')
proc = subprocess.Popen(['/bin/cat', 'tmp.fifo'], stdout=FNULL, stderr=FNULL)
print('pid = %d' % proc.pid)

# Open the FIFO, and MUST BE BINARY MODE.
fifo = open('tmp.fifo', 'wb')

i = 0
# Endlessly write to the FIFO.
while True:

    # Try to write to the FIFO, restart the subprocess on demand, until succeeded.
    while True:
        try:
            # Optimistically write to the FIFO.
            fifo.write(f'hello{i}'.encode())
            fifo.flush()
        except IOError as e:
            # The subprocess died. Close the FIFO and reap the subprocess.
            print('IOError is occured.')
            fifo.close()
            os.kill(proc.pid, signal.SIGKILL)
            proc.wait()

            # Start the subprocess again.
            proc = subprocess.Popen(['/bin/cat', 'tmp.fifo'], stdout=FNULL, stderr=FNULL)
            print('pid = %d' % proc.pid)
            fifo = open('tmp.fifo', 'wb')
        else:
            # The write goes on well.
            break
    os.kill(proc.pid, signal.SIGKILL)
    i += 1

и сохраните его как test.py.

Затем запустите strace -o strace.out python3 test.py в оболочке. Отметьте strace.out, и мы найдем что-то вроде

openat(AT_FDCWD, "tmp.fifo", O_WRONLY|O_CREAT|O_TRUNC|O_CLOEXEC, 0666) = 4
fstat(4, {st_mode=S_IFIFO|0644, st_size=0, ...}) = 0
ioctl(4, TCGETS, 0x7ffcba5cd290)        = -1 ENOTTY (Inappropriate ioctl for device)
lseek(4, 0, SEEK_CUR)                   = -1 ESPIPE (Illegal seek)
write(4, "hello0", 6)                   = 6
kill(35626, SIGKILL)                    = 0
write(4, "hello1", 6)                   = 6
kill(35626, SIGKILL)                    = 0
write(4, "hello2", 6)                   = -1 EPIPE (Broken pipe)
--- SIGPIPE {si_signo=SIGPIPE, si_code=SI_USER, si_pid=35625, si_uid=1000} ---
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_KILLED, si_pid=35626, si_uid=1000, si_status=SIGKILL, si_utime=0, si_stime=0} ---
write(1, "IOError is occured.\n", 20)   = 20
write(4, "hello2", 6)                   = -1 EPIPE (Broken pipe)
--- SIGPIPE {si_signo=SIGPIPE, si_code=SI_USER, si_pid=35625, si_uid=1000} ---
close(4)                                = 0
write(2, "Traceback (most recent call last"..., 35) = 35
write(2, "  File \"test.py\", line 26, in <m"..., 39) = 39

Обратите внимание, что Python пытался написать hello2 дважды, в течение fifo.flush и fifo.close соответственно. Выходные данные объясняют, почему два исключения SIGPIPE генерируются правильно.

Чтобы решить проблему, мы можем использовать open('tmp.fifo', 'wb', buffering=0), чтобы отключить буфер записи. Тогда будет сгенерировано только одно исключение SIGPIPE.

...