Вопрос или проблема
Это лучше всего проиллюстрировать на примере:
{ printf 'foo\nbar\n' ; sleep 2 ; } | grep -m1 foo { printf 'foo\n' ; sleep 2 ; printf 'bar\n' ; sleep 2 ; } | grep -m1 foo
Обе эти команды, выполненные в Bash с использованием GNU coreutils, по какой-то причине ведут себя совершенно одинаково:
- Первый grep выводит «foo» с символом новой строки после него, но продолжает блокироваться.
- Затем grep ждет 2 секунды и завершает работу.
Ожидаемое поведение для меня в обоих случаях заключается в том, что grep должен напечатать «foo» с новой строкой и сразу же выйти, не дожидаясь двух секунд. В конце концов, он уже выполнил свое условие одной точной совпадения и знает, что любое последующее введенное нельзя изменить. Действительно, если я сделаю это:
{ printf 'foo' ; sleep 2 ; } | grep -m1 foo
Без новой строки после «foo» он сначала ждет две секунды, ничего не делая, а затем выходит, печатая «foo» с новой строкой после него. Это имеет смысл: grep еще не получил никаких новых строк, поэтому он еще не знает, что может следовать после этих двух секунд, поэтому он не может еще распечатать то, что будет на строке с совпадением.
Но я, в частности, не понимаю, почему первые две команды функционируют так, как они это делают. Во втором случае, GNU grep сразу же выходит после получения второй строки и не ждет окончания двух секунд сна для завершения команды перед трубой, тогда как в первой команде он получает foo\nbar\n
сразу на двух строках, и, тем не менее, он не выходит немедленно после получения второй строки. Я предполагаю, что это как-то связано с тем, как работает буферизация.
Если вам интересен мой реальный случай использования и почему я исследую это. Я использую это в скрипте с udevadm monitor
, чтобы отфильтровать конкретное событие, и когда это событие достигается, я хочу прекратить блокировку, поэтому я использую udevadm monitor | egrep -m1 <regex>
. Все шло хорошо, за исключением того, что я заметил, что он не прекращает блокировку, когда конкретно искомое событие является последним, о котором сообщил udevadm, тогда он только прекращает блокировку после отправки нового события. По какой-то причине в этом случае grep только выходит и выводит первое совпадение после получения строки или получения конца файла. Почему это происходит и как сделать так, чтобы это не происходило?
Я на самом деле написал этот вопрос, и потом кто-то другой помог мне приблизиться к ответу, но я все равно решил создать эту тему и ответить самостоятельно, чтобы распространить знания, так как я думаю, что многие могут столкнуться с этим.
Простой ответ заключается в том, что трубы в оболочке на самом деле не выходят, когда последний процесс в пайплайне завершается, а когда все они завершаются. Если одно из них содержит бесконечный цикл, весь пайплайн будет продолжать работать вечно.
Часть grep
на самом деле завершается, когда она находит первую строку, которую может соответствовать и возвращает. Причина, по которой весь пайплайн завершается через две секунды, заключается в том, что записывающая часть затем сталкивается с зависшей трубой, когда пытается снова записать и получает SIGPIPE
, а затем завершается на этой попытке записи, но она не знает, что другая конечность зависла, пока не начнет записывать. Возможно, в идеальном мире она бы получила SIGPIPE
в момент, когда другая конечность зависает, хотя я предполагаю, что другая конечность может вновь открыть, но она знает, что другая конечность зависла, только когда фактически пытается записать. Она просит прощения, а не разрешения, именно поэтому она заканчивается только тогда.
В случае записи двух строк сразу, они сначала записываются в одном буферизированном потоке, затем процесс grep зависает, выполнив свою задачу.
Способ решения этой проблемы в конкретном скрипте – это вручную проверить статус завершения grep с анонимной именованной трубой:
# мы создаем трубу, а затем отвязываем ее и назначаем ее файловый дескриптор 3, чтобы создать анонимную трубу # создать анонимный fifo на файловом дескрипторе 3, создав его # затем открыть файл дескриптора к нему и удалить его снова # мы используем каталог, чтобы убедиться, что это атомарно fifodir=$(mktemp -d) mkfifo -- "$fifodir/fifo" exec 3<>"$fifodir/fifo" rm -r -- "$fifodir" # теперь выполняем трубу вручную udevadm monitor <args> >&3 & udevadmpid=$! # и ждем только на процессе grep egrep -m1 <pattern> <&3 & wait $! # мы даже убиваем это для чистоты, хотя это не обязательно kill $udevadmpid
Используйте опцию --line-buffered
вывода GNU grep.
В противном случае, вывод grep
остается в буфере STDOUT
до тех пор, пока он не будет сброшен, когда grep
завершит работу.
Ответ или решение
Проблема взаимодействия GNU grep с буферизацией и конвейерами может показаться странной и сложной для понимания, особенно когда grep ведет себя не так, как ожидалось. Однако, если рассмотреть основы буферизации и работу конвейеров в Bash, можно разобраться в ситуации и предложить способы ее решения.
Теория: Буферизация и конвейеры в Unix
Буферизация — это способ временного хранения данных в памяти перед их записью или чтением. В Unix-подобных системах потоки ввода-вывода буферизуются по-разному в зависимости от обстоятельств. Обычно можно выделить три вида буферизации: построчная (line-buffered), буферизация фиксированного размера (block-buffered) и неблокирующая или немедленная (unbuffered).
-
Построчная буферизация активируется, если выходной поток истинно связан с терминалом. Это означает, что каждая строка данных записывается в поток сразу после ее формирования.
-
Буферизация фиксированного размера или блочная буферизация, как правило, используется для записей в файлы или каналы, не привязанные к терминалу. Она означает, что данные собираются и записываются сразу после достижения определенного объема — обычно 4096 байт.
-
Неблокирующая буферизация практически не используется по умолчанию, так как она требует производить запись или чтение данных сразу без их временного хранения.
Пример: Взаимодействие grep и элементарные примеры конвейеров
В вашем примере, два теста команды приводят к различным временным задержкам, которые можно объяснить следующим образом:
{ printf 'foo\n' ; sleep 2 ; printf 'bar\n' ; sleep 2 ; } | grep -m1 foo
Каждый printf
в данном сценарии записывает строки "foo" и "bar" в выходной поток, который затем обрабатывается grep
в режиме отсутствия записи на экран до тех пор, пока не поступит последующая запись или не завершится весь поток. Когда grep
находит совпадение, конвейер сохраняет свое выполнение до тех пор, пока следующая команда в линии командного интерфейса не закончит свое выполнение.
Этот процесс можно объяснить временем ожидания, так как выполнение будет продолжаться до попытки записи в уже закрытую линию, что приведет к отправке SIGPIPE процессу на стороне писателя, заставляя его завершить свое выполнение.
Применение: Решение проблемы
Чтобы решить эту проблему и получить мгновенный выход grep
, можно использовать ключ --line-buffered
. Таким образом, мы превращаем grep
в построчный режим буферизации, что позволяет ему немедленно обрабатывать и выводить результаты после сопоставления строки, что отлично подходит для случаев, когда интересует непосредственно первая строка с совпадением:
{ printf 'foo\nbar\n' ; sleep 2 ; } | grep -m1 --line-buffered foo
Такое использование предоставляет немедленный вывод совпадающей строки, не заставляя ждать завершения всех процессов в линии командного интерфейса. Этот подход особенно полезен для сценариев, когда нужно отреагировать на событие, определяемое в больших потоках данных в реальном времени — например, когда вы отслеживаете события через udevadm monitor
и хотите мгновенно отреагировать при появлении соответствующих сигналов состояния.
Альтернативные подходы
-
Ручное управление каналами с использованием временного файла FIFO:
Создавая и управляя FIFO-файлом на низком уровне, можно точнее контролировать работу потоков, фактически отделяя процессы записи и чтения и отслеживая завершение, не дожидаясь дополнительных команд или выхода.
-
Проверка состояния выполнения непосредственным обращением к статусу выхода
grep
, что позволяет запускать цикл работы программы в зависимости от успешного выполнения.
Этот подход гарантирует, что вашему скрипту не придется зависеть от таймаутов или других управляемых процессов. Вместо этого можно использовать сигналы или обработчики событий для более точного управления.
Заключение
Таким образом, проблема, которую вы наблюдаете, в первую очередь обусловлена природой буферизации и механизмом обработки каналов в Unix-подобных системах. Зная эти нюансы и применяя соответствующие флаги и параметры, можно оптимизировать выполнение программных сценариев, учитывая особенности работы с буферизированными данными и потоками. В конечном итоге, выбор подхода зависит от конкретных требований сценария и ожидаемого времени отклика системы.