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(), а также, что оба процесса выполняются параллельно в разных потоках.
Исходный код, уровень 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.
-
Перенаправление и 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
-
Перенаправление _только_ 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. -
Получение корректного кода завершения конвейра
По умолчанию, код завершения конвейера — код завершения последней команды в конвеере. Например, возьмем исходную команду, которая завершается с ненулевым кодом:
$ 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
В больших скриптах со сложными конструкциями и длинными конвеерами можно упустить этот момент из виду, что может привести к некорректным результатам.
-
Присвоение значений переменным в конвейере
Для начала вспомним, что все команды в конвейере выполняются в отдельных процессах, полученных вызовом 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 в том же процессе, где будет вызываться и соответствующий конвейер, поэтому скобки в примере выше обязательны. В скриптах скобки не обязательны.
- использовать
Дополнительная информация
- подробное описание синтаксиса конвеера: linux.die.net/man/1/bash (секция Pipelines), или ‘man bash’ в терминале.
- логика работы конвеера: linux.die.net/man/7/pipe или ‘man 7 pipe’
- исходный код оболочки bash: ftp.gnu.org/gnu/bash/, репозиторий: git.savannah.gnu.org/cgit/bash.git
- ядро Linux: www.kernel.org/
Автор: aikikode