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Кб) для архитектуры asm:
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/
ссылка на оригинал статьи http://habrahabr.ru/post/195152/
Добавить комментарий