Файловые дескрипторы не считываются подпроцессом после .readline()

#python #subprocess

Вопрос:

Сегодня я написал следующий сценарий, который не работает:

 #!/usr/bin/env python3

'''
hsort.py
sort, but keeping the header at the top
'''

import sys
from subprocess import Popen
from contextlib import suppress

def main() -> None:
    print(sys.stdin.readline(), end='')
    with suppress(EOFError, KeyboardInterrupt):
        Popen(['sort', *sys.argv[1:]], stdin=sys.stdin, stdout=sys.stdout)

if __name__ == '__main__':
    main()
 

После значительного объема отладки я нашел этот подпроцесс.Popen не может прочитать оставшиеся строки из файлового объекта (в данном случае sys.stdin), если fd.readline() он был вызван ранее.

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

Является ли это ожидаемым поведением и есть ли простой способ объяснить, почему это происходит?

Комментарии:

1. Я бы предположил, что это проблема с буферизацией.

2. Это, безусловно, может быть проблемой с буферизацией; если sys.stdin.readline() он не читает байт за байтом (что неизбежно медленно, поскольку ему приходится выполнять отдельный системный вызов на байт ввода, что означает множество переключений контекста), он должен читать большими кусками, что означает, что он будет потреблять больше, чем одну строку. Это неизбежное техническое ограничение, лежащее read в основе того, что команда баша очень медленная.

3. @PatrickGray, …кстати, вы увидите ту же проблему с вызовами типа readline в C: они либо буферизованы (и поэтому делают больше содержимого, чем просто одна строка, недоступной для подпроцессов и небуферизованного ввода-вывода), либо они медленные.

Ответ №1:

Две проблемы:

  1. Вы не делаете ничего осмысленного с Popen() только что созданным объектом. Вам нужно будет вызвать .communicate() или .wait() включить его, чтобы подпроцесс фактически выполнялся до его завершения, точно так же, как это было бы необходимо wait(2) для дочернего процесса, созданного с fork(2) помощью . Если вы этого не сделаете, то дочерний процесс, скорее всего, просто выдаст ошибку, как только попытается прочитать что-либо из своих стандартных входных данных, потому что ваш сценарий завершился слишком рано, и файловый дескриптор стал недействительным.
  2. sys.stdin.readline() Вызов, который вы выполняете перед выполнением sort , скорее всего, потребляет больше одной строки ввода. Если стандартный ввод Python буферизован (что, скорее всего, так и есть), то .readline() он просто считывает произвольно большой объем данных (в моей системе до 8 КБ), а затем обрабатывает его, чтобы найти первый символ новой строки.

    После первого чтения sort будет запущено, но больше ничего не останется для чтения, потому что все уже было прочитано инициалом .readline() и сохранено во внутреннем буфере Python для последующего использования.

    Чтобы решить эту проблему, вам следует либо избегать чтения первой строки, либо делать это без буфера. Это несколько раздражает, вам нужно open() будет снова ввести стандартный ввод из его файлового дескриптора, указав buffering=0 , чтобы получить новый «файловый объект», который не буферизован, а затем прочитать каждый байт ввода вручную.

В заключение, что вы хотите сделать, это следующее:

 #!/usr/bin/env python3

import sys
from subprocess import Popen

def main() -> None:
    first_line = b''

    with open(sys.stdin.fileno(), 'rb', closefd=False, buffering=0) as f:
        while not first_line.endswith(b'n'):
            first_line  = f.read(1)

    print(first_line.decode(), end='')

    p = Popen(['sort', *sys.argv[1:]], stdin=sys.stdin, stdout=sys.stdout)
    p.wait()

if __name__ == '__main__':
    main()
 

Комментарии:

1. Строго говоря, я думаю sort , что он завершится только тогда, когда попытается выполнить запись в канал, другой конец которого закрыт выходящим скриптом Python, а не просто потому, что скрипт завершается.

2. @chepner хм, вы правы, хотя, строго говоря, в данном случае это не труба, а просто старый недопустимый дескриптор умершего родителя. Я обновлю ответ.

3. @MarcoBonelli, вы совершенно уверены, что sys.stdin.readline() это снижает производительность, чтобы выполнять байтовое чтение без буфера, чтобы использовать только одну строку, а остальные читать в очереди для будущих процессов? Если нет, я бы ожидал, что какой-то контент все равно будет удален (находится в буфере Python для будущих чтений и недоступен sort ).

4. @CharlesDuffy исправил это.

5. По — моему, выглядит неплохо.