«Негерметичные» трубы в linux

12

Предположим, у вас есть конвейер, подобный следующему:

$ a | b

Если bостановка обработки stdin, через некоторое время канал заполняется, и запись, начиная aс его stdout, будет блокироваться (пока либо не bначнется обработка снова, либо он не умрет).

Если бы я хотел избежать этого, у меня мог бы возникнуть соблазн использовать более крупную трубу (или, проще, buffer(1)) следующим образом:

$ a | buffer | b

Это просто выиграет мне больше времени, но в конце aконцов остановится.

Что я хотел бы иметь (для очень специфического сценария, к которому я обращаюсь), так это иметь «негерметичный» канал, который при заполнении удалял бы некоторые данные (в идеале построчно) из буфера, чтобы позволить aпродолжить обработка (как вы, вероятно, можете себе представить, данные, которые передаются по конвейеру, являются расходуемыми, т. е. обработка данных bменее важна, чем aвозможность работать без блокировки).

Подводя итог, я хотел бы иметь что-то вроде ограниченного, протекающего буфера:

$ a | leakybuffer | b

Я мог бы реализовать это довольно легко на любом языке, мне просто было интересно, есть ли что-то «готовое к использованию» (или что-то вроде однострочного bash), которое мне не хватает.

Примечание: в примерах я использую обычные каналы, но вопрос в равной степени относится и к именованным каналам.


Хотя я получил ответ ниже, я также решил реализовать команду leakybuffer, поскольку простое решение, приведенное ниже, имеет некоторые ограничения: https://github.com/CAFxX/leakybuffer

CAFxX
источник
Именованные каналы действительно заполняются? Я бы подумал, что именованные каналы являются решением этой проблемы, но я не могу сказать наверняка.
Wildcard
3
Именованные каналы имеют (по умолчанию) такую ​​же емкость, что и неназванные каналы,
AFAIK

Ответы:

14

Проще всего было бы передать через некую программу, которая устанавливает неблокирующий вывод. Вот простой Perl Oneliner (который вы можете сохранить как Lakyybuffer ), который делает это:

так твоя a | bстановится:

a | perl -MFcntl -e \
    'fcntl STDOUT,F_SETFL,O_NONBLOCK; while (<STDIN>) { print }' | b

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

Вы можете проверить, например:

seq 1 500000 | perl -w -MFcntl -e \
    'fcntl STDOUT,F_SETFL,O_NONBLOCK; while (<STDIN>) { print }' | \
    while read a; do echo $a; done > output

вы получите outputфайл с потерянными строками (точный вывод зависит от скорости вашей оболочки и т. д.) примерно так:

12768
12769
12770
12771
12772
12773
127775610
75611
75612
75613

Вы видите, где оболочка теряла строки после 12773, но также и аномалию - у perl не было достаточно буфера, 12774\nно он сделал для этого, 1277поэтому он записал только это - и поэтому следующее число 75610не начинается в начале строки, что делает его небольшим некрасиво.

Это можно улучшить, если perl определит, когда запись не удалась полностью, а затем попытаться сбросить оставшуюся часть строки, игнорируя при этом новые поступающие строки, но это значительно усложнит сценарий perl, поэтому оставляется в качестве упражнения для заинтересованный читатель :)

Обновление (для двоичных файлов): если вы не обрабатываете строки, заканчивающиеся символом новой строки (например, файлы журнала или аналогичные), вам нужно слегка изменить команду, иначе perl будет занимать большие объемы памяти (в зависимости от того, как часто символы ввода строки появляются в ваших входных данных):

perl -w -MFcntl -e 'fcntl STDOUT,F_SETFL,O_NONBLOCK; while (read STDIN, $_, 4096) { print }' 

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

Update2 - более хороший вывод текстового файла: Избегание выходных буферов ( syswriteвместо print):

seq 1 500000 | perl -w -MFcntl -e \
    'fcntl STDOUT,F_SETFL,O_NONBLOCK; while (<STDIN>) { syswrite STDOUT,$_ }' | \
    while read a; do echo $a; done > output

кажется, исправить проблемы с "объединенными линиями" для меня:

12766
12767
12768
16384
16385
16386

(Примечание: можно проверить, на каких линиях был обрезан вывод: perl -ne '$c++; next if $c==$_; print "$c $_"; $c=$_' outputoneliner)

Матия Налис
источник
Мне нравится oneliner: я не эксперт по Perl, если бы кто-нибудь мог предложить улучшения, описанные выше, это было бы здорово
CAFxX
1
Это, кажется, работает в некоторой степени . Но когда я наблюдаю за моей командой perl -w -MFcntl -e 'fcntl STDOUT,F_SETFL,O_WRONLY|O_NONBLOCK; while (<STDIN>) { print }' | aplay -t raw -f dat --buffer-size=16000, perl, кажется, постоянно выделяет больше памяти, пока не будет уничтожен менеджером OOM.
Ponkadoodle
@Wallacoloo спасибо за указание на то, что мой случай был потоковой передачей файлов журнала ... Смотрите обновленный ответ для небольшого изменения, необходимого для поддержки двоичных файлов.
Матия Налис
Смотрите также GNU dd«S dd oflag=nonblock status=none.
Стефан Шазелас
1
Извините, опять же плохо, на самом деле записи меньше PIPE_BUF байтов (4096 в Linux, POSIX должно быть не менее 512) гарантированно будут атомарными, поэтому $| = 1ваш syswrite()подход действительно предотвращает короткие записи, пока строки достаточно короткие.
Стефан Шазелас