Linux pipes tips & tricks

Pipe — что это?

Pipe (конвеер) – это однонаправленный канал межпроцессного взаимодействия. Термин был придуман Дугласом Макилроемдля командной оболочки Unix и назван по аналогии с трубопроводом. Конвейеры чаще всего используются в shell-скриптах для связи нескольких команд путем перенаправления вывода одной команды (stdout) на вход (stdin) последующей, используя символ конвеера ‘|’:

cmd1 | cmd2 | .... | cmdN

Например:

$ grep -i “error” ./log | wc -l
43

grep выполняет регистронезависимый поиск строки “error” в файле log, но результат поиска не выводится на экран, а перенаправляется на вход (stdin) команды wc, которая в свою очередь выполняет подсчет количества строк.

Логика

Конвеер обеспечивает асинхронное выполнение команд с использованием буферизации ввода/вывода. Таким образом все команды в конвейере работают параллельно, каждая в своем процессе.

Размер буфера начиная с ядра версии 2.6.11 составляет 65536 байт (64Кб) и равен странице памяти в более старых ядрах. При попытке чтения из пустого буфера процесс чтения блокируется до появления данных. Аналогично при попытке записи в заполненный буфер процесс записи будет заблокирован до освобождения необходимого места.
Важно, что несмотря на то, что конвейер оперирует файловыми дескрипторами потоков ввода/вывода, все операции выполняются в памяти, без нагрузки на диск.
Вся информация, приведенная ниже, касается оболочки bash-4.2 и ядра 3.10.10.

Простой дебаг

Утилита strace позволяет отследить системные вызовы в процессе выполнения программы:

$ strace -f bash -c ‘/bin/echo foo | grep bar’
....
getpid() = 13726                   <– PID основного процесса
...
pipe([3,  4])                       <– системный вызов для создания конвеера
....
clone(....) = 13727                <– подпроцесс для первой команды конвеера (echo)
...
[pid 13727] execve("/bin/echo",  ["/bin/echo",  "foo"],  [/* 61 vars */] 
.....
[pid 13726] clone(....) = 13728    <– подпроцесс для второй команды (grep) создается так же основным процессом
...
[pid 13728] stat("/home/aikikode/bin/grep",   
...

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

Tips & trics

В примерах ниже будем выполнять ls на существующую директорию Documents и два несуществующих файла: ./non-existent_file и. /other_non-existent_file.

  1. Перенаправление и stdout, и stderr в pipe
    ls -d ./Documents ./non-existent_file ./other_non-existent_file 2>&1 | egrep “Doc|other”
    ls: cannot access ./other_non-existent_file: No such file or directory
    ./Documents
    

    или же можно использовать комбинацию символов ‘|&’ (о ней можно узнать как из документации к оболочке (man bash), так и из исходников выше, где мы разбирали Yacc парсер bash):

    ls -d ./Documents ./non-existent_file ./other_non-existent_file |& egrep “Doc|other”
    ls: cannot access ./other_non-existent_file: No such file or directory
    ./Documents
    
  2. Перенаправление _только_ stderr в pipe
    $ ls -d ./Documents ./non-existent_file ./other_non-existent_file 2>&1 >/dev/null | egrep “Doc|other”
    ls: cannot access ./other_non-existent_file: No such file or directory
    

    Shoot yourself in the foot
    Важно соблюдать порядок перенаправления stdout и stderr. Например, комбинация ‘>/dev/null 2>&1′ перенаправит и stdout, и stderr в /dev/null.

  3. Получение корректного кода завершения конвейра

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

    $ ls -d ./non-existent_file 2>/dev/null; echo $?
    2
    

    И поместим ее в pipe:

    $ ls -d ./non-existent_file 2>/dev/null | wc; echo $?
          0       0       0
    0
    

    Теперь код завершения конвейера — это код завершения команды wc, т.е. 0.

    Обычно же нам нужно знать, если в процессе выполнения конвейера произошла ошибка. Для этого следует выставить опцию pipefail, которая указывает оболочке, что код завершения конвейера будет совпадать с первым ненулевым кодом завершения одной из команд конвейера или же нулю в случае, если все команды завершились корректно:

    $ set -o pipefail
    $ ls -d ./non-existent_file 2>/dev/null | wc; echo $?
          0       0       0
    2
    

    Shoot yourself in the foot
    Следует иметь в виду “безобидные” команды, которые могут вернуть не ноль. Это касается не только работы с конвейерами. Например, рассмотрим пример с grep:

    $ egrep “^foo=[0-9]+” ./config | awk ‘{print “new_”$0;}’
    

    Здесь мы печатаем все найденные строки, приписав ‘new_’ в начале каждой строки, либо не печатаем ничего, если ни одной строки нужного формата не нашлось. Проблема в том, что grep завершается с кодом 1, если не было найдено ни одного совпадения, поэтому если в нашем скрипте выставлена опция pipefail, этот пример завершится с кодом 1:

    $ set -o pipefail
    $ egrep “^foo=[0-9]+” ./config | awk ‘{print “new_”$0;}’ >/dev/null; echo $?
    1
    

    В больших скриптах со сложными конструкциями и длинными конвеерами можно упустить этот момент из виду, что может привести к некорректным результатам.

  4. Присвоение значений переменным в конвейере

    Для начала вспомним, что все команды в конвейере выполняются в отдельных процессах, полученных вызовом clone(). Как правило, это не создает проблем, за исключением случаев изменения значений переменных.
    Рассмотрим следующий пример:

    $ a=aaa
    $ b=bbb
    $ echo “one two” | read a b
    

    Мы ожидаем, что теперь значения переменных a и b будут “one” и “two” соответственно. На самом деле они останутся “aaa” и “bbb”. Вообще любое изменение значений переменных в конвейере за его пределами оставит переменные без изменений:

    $ filefound=0
    $ find . -type f -size +100k |
        while true
        do
            read f
            echo “$f is over 100KB”
            filefound=1
            break          # выходим после первого найденного файла
        done
    $ echo $filefound;
    

    Даже если find найдет файл больше 100Кб, флаг filefound все равно будет иметь значение 0.
    Возможны несколько решений этой проблемы:

    • использовать
      set -- $var
      

      Данная конструкция выставит позиционные переменные согласно содержимому переменной var. Например, как в первом примере выше:

      $ var=”one two”
      $ set -- $var
      $ a=$1   # “one”
      $ b=$2   # “two”
      

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

    • перенести всю логику обработки значения переменной в тот же подпроцесс в конвейере:
      $ echo “one” | (read a; echo $a;)
      one 
      
    • изменить логику, чтобы избежать присваивания переменных внутри конвеера.
      Например, изменим наш пример с find:

      $ filefound=0
      $ for f in $(find . -type f -size +100k)  # мы убрали конвейер,  заменив его на цикл
          do
              read f
              echo “$f is over 100KB”
              filefound=1
              break
          done
      $ echo $filefound;
      
    • (только для bash-4.2 и новее) использовать опцию lastpipe
      Опция lastpipe дает указание оболочке выполнить последнюю команду конвейера в основном процессе.

      $ (shopt -s lastpipe; a=”aaa”; echo “one” | read a; echo $a)
      one
      

      Важно, что в командной строке необходимо выставить опцию lastpipe в том же процессе, где будет вызываться и соответствующий конвейер, поэтому скобки в примере выше обязательны. В скриптах скобки не обязательны.

 

Дополнительная информация

 

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.