Конспект, заметки по работе со стандартными потоками ввода-вывода, работа с каналами. Примеры проверены на Debian Linux. Буферизация STDOUT и STDERR в perl. Использование /dev/null . Mknod и mkfifo.
Стандартные потоки ввода-вывода: STDIN, STDOUT, STDERR
Процесс взаимодействия программы с окружением выполняется в терминах записи и чтения в файл. Так, вывод на экран представляется как запись в файл, а ввод данных — как чтение файла. Файл, из которого осуществляется чтение, называется стандартным потоком ввода, а в который осуществляется запись — стандартным потоком вывода.
Кроме потоков ввода и вывода, существует еще и стандартный поток ошибок, на который выводятся все сообщения об ошибках и те информативные сообщения о ходе работы программы, которые не могут быть выведены в стандартный поток вывода.
Большинство программ, которые работают с потоками ввода и вывода, работают с ними как с простыми файлами, и не рассчитывают на то, что поток подключен к терминалу.
Вывод данных на экран и чтение их с клавиатуры происходит потому, что по умолчанию стандартные потоки ассоциированы с терминалом пользователя. Это не является обязательным — потоки можно подключать к чему угодно — к файлам, программам и даже устройствам — эта операция называется перенаправлением.
Стандартные потоки привязаны к файловым дескрипторам с номерами 0, 1 и 2.
- Стандартный поток ввода (STDIN) — 0;
- Стандартный поток вывода (STDOUT) — 1;
- Стандартный поток ошибок (STDERR) — 2.
Пример
Создадим очень простую perl-программу, которая будет отправлять информацию в STDOUT и STDERR. И используем ее в дальнейшем, для проверки разных способов перенаправления вывода.
|
1 2 3 |
sleep(60); print STDOUT "Standart stdout\n"; print STDERR "Some error!\n"; |
Sleep используется для того, чтобы успеть получить информацию у запущенном процессе.
С помощью lsof просматриваем список открытых файлов и файловых дескрипторов для запущенной программы. Эта информация может оказаться полезной и интересной, когда дело доходит до работы с каналами и сокетами.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
# lsof -a -p 18656 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME perl 18656 root cwd DIR 144,52 4096 2752575 /root/perl perl 18656 root rtd DIR 144,52 4096 2528296 / perl 18656 root txt REG 144,52 1487332 4489619 /usr/bin/perl perl 18656 root mem REG 8,3 4489619 /usr/bin/perl (path dev=144,52) perl 18656 root mem REG 8,3 4457085 /usr/lib/locale/locale-archive (path dev=144,52) perl 18656 root mem REG 8,3 6565272 /lib/i386-linux-gnu/libcrypt-2.13.so (path dev=144,52) perl 18656 root mem REG 8,3 6565131 /lib/i386-linux-gnu/libc-2.13.so (path dev=144,52) perl 18656 root mem REG 8,3 6565158 /lib/i386-linux-gnu/libpthread-2.13.so (path dev=144,52) perl 18656 root mem REG 8,3 6565127 /lib/i386-linux-gnu/libm-2.13.so (path dev=144,52) perl 18656 root mem REG 8,3 6565134 /lib/i386-linux-gnu/libdl-2.13.so (path dev=144,52) perl 18656 root mem REG 8,3 6565284 /lib/i386-linux-gnu/ld-2.13.so (path dev=144,52) perl 18656 root 0u CHR 136,1 0t0 4 /dev/pts/1 perl 18656 root 1u CHR 136,1 0t0 4 /dev/pts/1 perl 18656 root 2u CHR 136,1 0t0 4 /dev/pts/1 perl 18656 root 7w FIFO 0,8 0t0 2738690077 pipe |
Кстати, следует учитывать тот факт, что по умолчанию, perl буферизирует вывод STDOUT,
но не буферизирует STDERR.
Попытка выполнения вот такого кода:
|
1 2 3 |
print STDOUT "Standart stdout "; print STDERR "Some error! "; print STDOUT "Standart stdout 2 "; |
может породить вот такую ситуацию:
|
1 2 |
perl stdout.pl Some error! Standart stdout Standart stdout 2 |
Текст об ошибке оказался в начале строки, несмотря на то, что должен был оказаться в середине.
То же самое произойдет, если попытаться выполнить код:
|
1 2 |
print time(); sleep(20); |
В данном случае, сначала программа подождет 20 секунд, и только потом на экране появится сообщение. Хотя на самом деле, сообщение будет отправлено в STDOUT раньше, чем будет вызван sleep. Когда встречаешь подобное поведение системы в первый раз, оно может озадачить.
Отключить буферизацию можно, используя переменную $| и присвоив ей значение 1.
|
1 |
$| = 1; |
Другой вариант — в конце каждой строки добавлять «\n». Если записать код программы следующим образом:
|
1 2 3 |
print STDOUT "Standart stdout\n"; print STDERR "Some error!\n"; print STDOUT "Standart stdout 2\n"; |
результат выполнения будет именно таким, как ожидается:
|
1 2 3 4 |
# perl stdout.pl Standart stdout Some error! Standart stdout 2 |
Перенаправление потоков ввода-вывода
< filename
Используем указанный файл как источник данных для стандартного потока ввода.
> filename
Перенаправляем стандартный поток вывода в указанный файл. Если файл не существует — он будет создан, если существует — перезаписан.
|
1 2 3 4 |
# perl stdout.pl > file.txt Some error! # less file.txt Standart stdout |
В данном случае, в файл перенаправлены только те данные, которые подавались на STDOUT. Поток STDERR по прежнему связан с терминалом и выдает данные на него.
Чтобы обнулить содержимое файла, можно использовать команду:
|
1 |
# > file.txt |
2> filename
Перенаправляем стандартный поток ошибок в указанный файл. Если файл не существует — он будет создан, если существует — перезаписан.
|
1 2 3 4 5 |
# perl stdout.pl 2> file.txt Standart stdout # less file.txt Some error! |
При этом, поток STDOUT продолжает выводить данные на терминал.
В данном случае, «2» — это идентификатор потока ввода-вывода. Идентификатор с номером 2, обычно соответствует потоку STDERR, но можно указать номер любого другого потока вывода.
Вызов «perl stdout.pl 1> filename» эквивалентен вызову «perl stdout.pl > filename».
Можно оба потока вывода перенаправить в файлы:
|
1 2 3 4 |
# perl stdout.pl >fileout 2> fileerr # ls fileerr fileout stdout.pl |
Если выводимые данные не требуются и вы не хотите их просматривать в дальнейшем, можно перенаправить вывод в /dev/null .
|
1 2 |
# perl stdout.pl > /dev/null Some error! |
/dev/null — это символьное псевдоустройство. Можно обращаться к нему, как к файлу. Не обязательно использовать /dev/null , можно создать свой собственный null-файл с помощью утилиты mknod. Mknod — предназначена для создания специальных символьных или блочных файлов. Кроме того, mknod можно использовать для создания именованых каналов.
1 mknod [OPTION]... NAME TYPE [MAJOR MINOR]mknod FILE c 1 3 — создает null-файл.
1234567 # mknod NULLFILE c 1 3# ls -ladrwxr-xr-x 2 root root 4096 Мар 26 05:51 .drwx------ 12 root root 4096 Мар 26 02:40 ..crw-r--r-- 1 root root 1, 3 Мар 26 05:51 NULLFILE-rw-r--r-- 1 root root 75 Мар 26 02:56 stdout.pl
12 # perl stdout.pl 2> NULLFILEStandart stdout/dev/null очень полезен, когда требуется запустить скрипт, выдающий множество предупреждений в STDERR, особенно если этот скрипт запущен через cron, а cron, в свою очередь, шлет огромные текстовые сообщения вам на почту:
12 # crontab -l00 10 * * * perl $HOME/trunk/bin/table_update.pl 2>/dev/nullС помощью /dev/null можно обнулить содержимое файла:
1 cp /dev/null newfile
>>filename
Перенаправляем стандартный поток вывода в заданный файл. Если файл не существует — он будет создан, если существует — данные будут дописаны в конец файла.
|
1 2 3 4 5 6 7 8 9 10 11 |
# perl stdout.pl > log Some error! # perl stdout.pl >> log Some error! # tail log Standart stdout Standart stdout 2 Standart stdout Standart stdout 2 |
2>>filename
Перенаправляем стандартный поток ошибок в указанный файл. Если файл не существует — он будет создан. Если существует — данные будут дописаны в конец файла.
&>filename, >&filename
Перенаправляем и стандартный поток вывода, и стандартный поток ошибок в указанный файл.
|
1 2 3 4 5 6 |
# perl stdout.pl >& log # tail log Some error! Standart stdout Standart stdout 2 |
>&-
Закрываем стандартный поток вывода.
|
1 |
# perl ipc.pl >&- |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# ps -u root -f root 19903 19879 0 08:38 pts/1 00:00:00 perl ipc.pl # lsof -a -p 19903 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME perl 19903 root cwd DIR 144,52 4096 2752575 /root/perl perl 19903 root rtd DIR 144,52 4096 2528296 / perl 19903 root txt REG 144,52 1487332 4489619 /usr/bin/perl ... perl 19903 root 0u CHR 136,1 0t0 4 /dev/pts/1 perl 19903 root 1r REG 144,52 38 2753000 /root/perl/ipc.pl perl 19903 root 2u CHR 136,1 0t0 4 /dev/pts/1 perl 19903 root 7w FIFO 0,8 0t0 2762752611 pipe |
В данном случае, для скрипта был отключен STDOUT. Скрипт был выполнен, но никаких надписей на экране терминала при этом сделано не было.
2>&-
Закрываем стандартный поток вывода ошибок.
|
1 2 |
# perl ipc.pl 2>&- 1427373871 |
В данном случае, закрыт был поток вывода ошибок, но стандартный вывод отработал как обычно.
|
1 2 3 4 5 6 7 8 9 10 11 12 |
# ps -u root -f root 19938 19879 0 08:44 pts/1 00:00:00 perl ipc.pl # lsof -a -p 19938 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME perl 19938 root cwd DIR 144,52 4096 2752575 /root/perl perl 19938 root rtd DIR 144,52 4096 2528296 / perl 19938 root txt REG 144,52 1487332 4489619 /usr/bin/perl ... perl 19938 root 0u CHR 136,1 0t0 4 /dev/pts/1 perl 19938 root 1u CHR 136,1 0t0 4 /dev/pts/1 perl 19938 root 2r REG 144,52 38 2753000 /root/perl/ipc.pl perl 19938 root 7w FIFO 0,8 0t0 2762752611 pipe |
Каналы
Неименованые каналы
Стандартные потоки можно перенаправлять не только в файлы, но и на вход других программ. Если поток вывода одной программы соединить с потоком ввода другой программы, получится конструкция, называемая каналом, конвейером.
В bash канал выглядит как последовательность команд, отделенных друг от друга символом |:
|
1 |
command1 | command2 | command3 ... |
Стандартный поток вывода command1 подключается к стандартному потоку ввода command2, стандартный поток вывода command2 в свою очередь подключается к потоку ввода command3 и т.д.
Программы, образующие канал, выполняются параллельно как независимые процессы.
Пример 1
Создадим perl-скрипт, который будет принимать данные на вход.
|
1 2 3 4 |
while (<STDIN>) { chomp; print length($_)."\n"; } |
Это простая программа, которая считывает строки со стандартного входа и печатает для каждой строки количество символов. Если запустить программу как есть, то она будет ожидать ввода строки с терминала, печатать количество введенных символов, и считывать следующую строку, до тех пор, пока вам это не надоест и вы не введете Ctrl+C.
|
1 2 3 4 |
# perl stdin.pl 565757 6 ^C |
Либо, можно использовать канал и подать программе на вход какой-нибудь файл, чтобы она подсчитала количество символов в его строках.
|
1 |
# cat file.txt | perl stdin.pl |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# ps -u root -f root 20120 19879 0 09:18 pts/1 00:00:00 perl stdin.pl # lsof -a -p 20120 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME perl 20120 root cwd DIR 144,52 4096 2752575 /root/perl perl 20120 root rtd DIR 144,52 4096 2528296 / perl 20120 root txt REG 144,52 1487332 4489619 /usr/bin/perl ... perl 20120 root 0r FIFO 0,8 0t0 2764543421 pipe perl 20120 root 1u CHR 136,1 0t0 4 /dev/pts/1 perl 20120 root 2u CHR 136,1 0t0 4 /dev/pts/1 perl 20120 root 7w FIFO 0,8 0t0 2762752611 pipe |
К сожалению, процесс cat выполняется очень быстро, передает данные в канал и завершается, поэтому его данные тут не указаны.
Пример 2
Создадим программу, которая передает данные другой программе. Для связи между ними будет использован неименованый канал.
|
1 2 3 |
sleep(60); print STDOUT "Standart stdout\n"; print STDOUT "Standart stdout 2\n"; |
Запускаем perl-скрипт и передаем сгенерированные данные утилите wc:
|
1 2 |
# perl stdout.pl | wc 2 5 34 |
Утилита wc, в своем самом простом варианте применения, подсчитывает количество строк, слов и байт в заданном файле, или в том тексте, который был передан ей на вход.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
# ps -u root -f root 20008 19879 0 08:55 pts/1 00:00:00 perl stdout.pl root 20009 19879 0 08:55 pts/1 00:00:00 wc # lsof -a -p 20008 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME perl 20008 root cwd DIR 144,52 4096 2752575 /root/perl perl 20008 root rtd DIR 144,52 4096 2528296 / perl 20008 root txt REG 144,52 1487332 4489619 /usr/bin/perl ... perl 20008 root 0u CHR 136,1 0t0 4 /dev/pts/1 perl 20008 root 1w FIFO 0,8 0t0 2763569501 pipe perl 20008 root 2u CHR 136,1 0t0 4 /dev/pts/1 perl 20008 root 7w FIFO 0,8 0t0 2762752611 pipe # lsof -a -p 20009 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME wc 20009 root cwd DIR 144,52 4096 2752575 /root/perl wc 20009 root rtd DIR 144,52 4096 2528296 / wc 20009 root txt REG 144,52 38524 4489933 /usr/bin/wc ... wc 20009 root 0r FIFO 0,8 0t0 2763569501 pipe wc 20009 root 1u CHR 136,1 0t0 4 /dev/pts/1 wc 20009 root 2u CHR 136,1 0t0 4 /dev/pts/1 wc 20009 root 7w FIFO 0,8 0t0 2762752611 pipe |
Если обратить внимание, то можно заметить, что система создала оба процесса одновременно, у них один и тот же родитель.Кроме того, был создан неименованый канал. Этот канал скрипт использует вместо /dev/pts/1 (файл, псевдоустройство — псевдотерминал, PTS, pseudo-terminal slave), в качестве стандартного вывода, и тот же самый канал указан для wc вместо /dev/pts/1, в качестве стандартного входа.
Пример 3
Из perl-скрипта вызывается на выполнение netstat, и из этого же скрипта создается канал для связи с ней. Символ вертикальной черты в функции open, как раз указывает на то, что будет создан канал.
|
1 2 3 4 5 6 7 |
open(PIPE, "netstat -an 2>/dev/null |") or die "can not fork: $!"; while(<PIPE>) { print $_ if $_ =~ /^(tcp|udp|unix)/; } close(PIPE); |
Во время выполнения скрипта, можно заметить, что в дополнение к стандартным потокам ввода-вывода, для данного скрипта был добавлен четвертый — канал.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
ps -u root -f root 20705 19879 0 09:48 pts/1 00:00:00 perl pipe.pl root 20706 20705 0 09:48 pts/1 00:00:00 [sh] <defunct> # lsof -a -p 20705 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME perl 20705 root cwd DIR 144,52 4096 2752575 /root/perl perl 20705 root rtd DIR 144,52 4096 2528296 / perl 20705 root txt REG 144,52 1487332 4489619 /usr/bin/perl ... perl 20705 root 0u CHR 136,1 0t0 4 /dev/pts/1 perl 20705 root 1u CHR 136,1 0t0 4 /dev/pts/1 perl 20705 root 2u CHR 136,1 0t0 4 /dev/pts/1 perl 20705 root 3r FIFO 0,8 0t0 2766639351 pipe perl 20705 root 7w FIFO 0,8 0t0 2762752611 pipe |
Именованные каналы
Неименованый канал можно построить только между процессами, которые порождены от одного процесса (и на практике они должны быть порождены одновременно, а не последовательно, хотя теоретически это не обязательно). Если же процессы имеют разных родителей, то между ними обычный, безымянный канал построить не получится.
Для решения этой задачи используются именованные каналы fifo (first in, first out). Они во всём повторяют обычные каналы (pipe), только имеют привязку к файловой системе. Создать именованный канал можно командой mkfifo:
|
1 |
# mkfifo /tmp/fifo |
Созданный канал можно использовать для соединения процессов между собой.
|
1 |
# programm1 > /tmp/fifo & programm2 < /tmp/fifo |
Пример использования именованого канала
Для теста будем использовать уже известный скрипт stdin.pl:
|
1 2 3 4 |
while (<STDIN>) { chomp; print length($_)."\n"; } |
Кроме того, содаем текстовый файл file.txt, который содержит пару строк:
|
1 2 |
hello, my darling! |
Создаем именованый канал:
|
1 2 3 4 5 6 7 8 |
# mkfifo mypipe # ls -la drwxr-xr-x 2 root root 4096 Мар 26 09:24 . drwx------ 12 root root 4096 Мар 26 02:40 .. -rw-r--r-- 1 root root 18 Мар 26 09:11 file.txt prw-r--r-- 1 root root 0 Мар 26 09:26 mypipe -rw-r--r-- 1 root root 64 Мар 26 09:18 stdin.pl |
Далее, вызываем утилиту cat для того, чтобы она прочитала данные из файла file.txt и передала их на вход каналу mypipe, и одновременно вызываем perl-скрипт stdin.pl, которому поручаем забрать данные из канала и обработать их.
|
1 2 3 4 5 |
# cat file.txt > mypipe & perl stdin.pl < mypipe [1] 20192 6 11 [1]+ Done cat file.txt > mypipe |
Пока скрипт отрабатывается, мы имеем возможность посмотреть, с какими файловыми дескрипторами работает stdin.pl, и убедиться, что канал mypipe действительно передает данные на вход скрипту:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# ps -u root -f root 20229 19879 0 09:30 pts/1 00:00:00 perl stdin.pl # lsof -a -p 20229 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME perl 20229 root cwd DIR 144,52 4096 2752575 /root/perl perl 20229 root rtd DIR 144,52 4096 2528296 / perl 20229 root txt REG 144,52 1487332 4489619 /usr/bin/perl ... perl 20229 root 0r FIFO 144,52 0t0 2752585 /root/perl/mypipe perl 20229 root 1u CHR 136,1 0t0 4 /dev/pts/1 perl 20229 root 2u CHR 136,1 0t0 4 /dev/pts/1 perl 20229 root 7w FIFO 0,8 0t0 2762752611 pipe |
Полезные ссылки по теме потоков ввода-вывода и каналов
Стандартные потоки ввода/вывода
Стандартный канал вывода, стандартный канал ошибок и перенаправление в командной строке
Спасибо за статью, очень познавательно. Но мне остался непонятным один момент — я не совсем понял, как работает амперсанд. Я понял, что &- закрывает поток, но как работает &>filename, >&filename мне так и осталось не понятно. Что тут поменялось от перестановки амперсандов и вообще какой они тут несут смысл?