Ваш терминал это не терминал: введение в потоки

Перевод статьи «Your terminal is not a terminal: An Introduction to Streams».

Я люблю потоки, потому что не люблю программы.

Я всегда старался создавать как можно меньше программного обеспечения. Меньшее количество программ означает, что у вас уйдет меньше времени на их обновление, исправления, да и вообще вы меньше будете о них думать. Лучше небольшого количества ПО только полное отсутствие ПО.

Потоки позволяют программам общаться между собой и таким образом помогают нам писать меньше программ.

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

Изучение потоков поможет вам немного лучше понять работу UNIX-систем и не усложнять вашу среду разработки.

Что такое потоки

Потоки они и есть потоки. Как в реке есть поток воды, так и в программах есть потоки данных. Эту аналогию можно даже развить: при помощи труб вы можете направить поток реки с одного места к другому, а в UNIX при помощи конвейеров можно перенаправить данные из одной программы в другую. Собственно, эта аналогия и вдохновляла создателей потоков:

«Нам нужны были какие-то способы соединения программ, похожие на садовый шланг, чтобы иметь возможность добавлять сегменты при необходимости какой-то особой обработки данных. Таким образом работает ввод/вывод», – Дуглас МакИлрой.

Потоки могут использоваться для передачи данных в программы и для получения данных из них.

В UNIX программы имеют зарезервированные потоки как для ввода, так и для вывода. Они называются стандартными потоками.

Всего стандартных потока три:

  • stdin – стандартный поток ввода (standard input) – поток, наполняющий ваши программы данными;
  • stdout – стандартный поток вывода (standard output) – поток, в который ваши программы записывают результаты своей работы;
  • stderr – стандартный поток ошибок (standard error) – поток, куда программы записывают сообщения об ошибках.

Например, программа fortune пишет разные умные вещи в поток stdout.

$ fortune
It is simplicity that is difficult to make
-- Bertold Brecht

При запуске программы fortune она получает привязанные к ней stdin, stdout и stderr. Поскольку эта программа не производит никаких ошибок и не получает данных извне, она просто записывает свой результат работы в stdout.

Есть еще одна интересная программа, которая тоже пишет результат в stdout, – cowsay. Эта программа принимает строку текста и выводит изображение коровы, которая как бы произносит этот текст.

$ cowsay "Ukraine has a decent president"
 _______________________________
< Ukraine has a decent president >
 -------------------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

В отличие от fortune, cowsay не обязательно говорит что-то умное – как мы только увидели. К счастью, мы можем «накормить» cowsay чем-то умным с помощью потока stdin этой программы.

Все, что нужно сделать, чтобы cowsay повторяла мудрые цитаты fortune, это воспользоваться конвейером. Конвейер представлен вертикальной чертой | и служит для соединения stdout программы fortune со stdin программы cowsay.

$ fortune | cowsay
 _________________________________________
/ A language that doesn't have everything \
| is actually easier to program in than   |
| some that do.                           |
|                                         |
\ -- Dennis M. Ritchie                    /
 -----------------------------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

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

Вы видите результат работы программы cowsay на своем экране, потому что по умолчанию у вашего терминала тоже есть свои стандартные потоки stdin, stdout и stderr.

Данные проходят через stdout и stderr и выходят с другого конца: на вашем мониторе. Точно так же все, что вы вводите на клавиатуре, попадает в программу через stdin.

Например, команда cat использует stdin для получения входных данных с клавиатуры, а stdout – для их вывода:

$ cat
Everything I write before pressing Enter
Everything I write before pressing Enter
Gets logged right after
Gets logged right after

Мы можем улучшить вывод, заменив все вхождения I на We при каждом нажатии Enter:

$ cat | sed -E "s/I/We/"
I think streams are quite cool.
We think streams are quite cool.

Если вы не знали, название sed расшифровывается как stream editor (потоковый редактор).

Как потоки разговаривают с вашим «терминалом»

Мой последний пост разместили различные сайты, и на одном из них был интересный комментарий. Кто-то указал, что на самом деле я пользуюсь не терминалом.

Это совершенно-не-занудное замечание было справедливым. Однако вот вам фотография, на которой я в 1978 году – незадолго до своего рождения – пользуюсь терминалом HP 2647A.

Если вы не путешественник во времени (как я), то то, чем вы пользуетесь, это эмулятор терминала. Кто бы мог подумать, правда?

Эмуляторы терминалов это программные симуляторы «настоящих» терминалов. Они предоставляют вам интерфейс для взаимодействия с TTY-драйвером Linux. TTY driver отвечает за направление данных в программы и от программ.

Каждый TTY имеет свои потоки stdin, stdout и stderr. Это потоки, предоставляемые программам, чтобы читать из них (stdin) и писать в них (stdout и stderr).

Вот более точная версия происходящего при запуске cat | sed -E "s / I / We /" из приведенного выше примера:

Как и все в UNIX, tty это файл. Каждый экземпляр эмулятора терминала имеет отдельный tty-файл, связанный с ним. Поскольку каждый эмулятор читает из своего файла и пишет тоже в свой файл, вы не видите вывода всех запущенных программ во всех открытых окнах терминала.

Чтобы выяснить, какой tty связан с окном терминала, можно воспользоваться командой tty.

Когда вы открываете новое окно терминала, вот как направлены его потоки:

На этом изображении /dev/ttys/005 это лишь пример. Там мог быть любой другой файл, поскольку он в любом случае будет новый для каждого экземпляра tty.

Перенаправление

Чтобы записать результат работы программы в файл, а не в tty, вы можете перенаправить поток stdout куда-то еще.

В примере ниже мы записываем содержимое директории / в файл content_list.txt, хранящейся в директории /tmp. Мы это делаем с помощью оператора >, который используется для перенаправления потока stdout.

$ ls / 1> /tmp/content_list.txt

Чтобы проверить, что теперь находится внутри /tmp/content_list.txt, можно воспользоваться командой cat:

$ cat /tmp/content_list.txt
Applications
Library
Network
System
Users
Volumes
bin
cores
dev
etc
home
net
private
sbin
themes
tmp
usr
var

В обычных условиях команда ls / вывела бы содержание директории / вам на экран, но сейчас этого не происходит. Вместо записи результатов работы в файл /dev/tty, с которого читает ваш терминал, команда записала результат в /tmp/content_list.txt.

Если вместо > использовать 1>, эффект будет тот же.

$ ls / > /tmp/content_list.txt

Единица перед знаком > показывает, какой именно поток мы хотим перенаправить. В нашем случае 1 это файловый дескриптор stdout.

Поскольку tty это просто файл, вы можете перенаправить поток stdout из одного терминала к другому.

Если бы нам надо было перенаправить поток stderr, мы могли бы поставить его файловый дескриптор (2) перед >.

$ cat /this/path/does/not/exist 2> /tmp/cat_error.txt

Теперь /tmp/cat_error.txt содержит то, что cat записала в stderr.

$ cat /tmp/cat_error.txt
cat: /this/path/does/not/exist: No such file or directory

Для того, чтобы перенаправить и stdout, и stderr, мы используем &>.

$ cat /does/not/exist /tmp/content_list.txt &> /tmp/two_streams.txt

Теперь файл / tmp / two_streams содержать все, что было записано в stdout и stderr.

$ cat /tmp/two_streams.txt
cat: /does/not/exist: No such file or directory
Applications
Library
Network
System
Users
Volumes
bin
cores
dev
etc
home
installer.failurerequests
net
private
sbin
themes
tmp
usr
var

Когда пишете что-то в файл с помощью >, надо быть осторожным. Одинарный знак > переписывает все содержимое файла.

$ printf "Look, I have something inside" > /tmp/careful.txt

$ cat /tmp/careful.txt
Look, I have something inside

$ printf "Now I have something else" > /tmp/careful.txt

$ cat /tmp/careful.txt
Now I have something else

Чтобы добавить что-то к файлу вместо того чтобы все перезаписывать, надо использовать двойной знак >>.

$ printf "Look, I have something inside" > /tmp/careful.txt

$ cat /tmp/careful.txt
Look, I have something inside

$ printf "\nNow I have one more thing" >> /tmp/careful.txt

$ cat /tmp/careful.txt
Look, I have something inside
Now I have one more thing

Для чтения из stdin мы можем использовать оператор <.

Следующая команда использует поток stdin, чтобы скормить sed содержимое /usr/share/dict/words. После этого sed выбирает случайную строку и выводит ее в stdout.

$ sed -n "${RANDOM}p" < /usr/share/dict/words
alloestropha

Поскольку файловый дескриптор stdin это 0, мы можем достичь того же эффекта, поставив 0 перед <.

$ sed -n "${RANDOM}p" 0< /usr/share/dict/words
pentameter

Важно также подчеркнуть различия между операторами перенаправления и конвейерами. При использовании конвейеров мы объединяем stdout одной программы со stdin другой. При использовании перенаправления мы меняем направление отдельного потока при запуске программы.

Поскольку потоки это только файловые дескрипторы, мы можем создавать их сколько захотим. Для этого можно использовать команду exec – чтобы открывать файлы на конкретных файловых дескрипторах.

В приведенном ниже примере мы открываем /usr/share/dict/words для чтения на дескрипторе 3.

$ exec 3< /usr/share/dict/words

Теперь мы можем использовать этот дескриптор как stdin для программы (с помощью <&).

$ sed -n "${RANDOM}p" 0<&3
dactylic

Оператор <& дублирует дескриптор 3 и превращает 0 (stdin) в его копию.

Открыв файловый дескриптор для чтения, вы можете «воспользоваться» им только один раз. Поэтому попытка вновь использовать 3 не сработает:

$ grep dactylic 0<&3

Чтобы закрыть файловый дескриптор, можно использовать -, как будто мы пытаемся скопировать этот дефис в файловый дескриптор, который хотим закрыть.

$ exec 3<&-

Так же, как мы использовали < для открытия файла для чтения, мы можем использовать >, чтобы открыть файл на запись.

В следующем примере мы создаем файл output.txt, открываем его в режиме записи и дублируем его дескриптор в 4:

$ touch /tmp/output.txt
$ exec 4>&/tmp/output.txt

Если мы теперь захотим, чтобы cowsay писала в файл /tmp/output.txt, мы можем продублировать файловый дескриптор для 4 и скопировать его в 1 (stdout).

$ cowsay "Does this work?" 1>&4

$ cat /tmp/output.txt
 _________________
< Does this work? >
 -----------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

Как вы можете интуитивно догадаться, для открытия файла для чтения и записи можно использовать <>. Сначала давайте создадим файл под именем /tmp/lines.txt, откроем для него r/w дескриптор и скопируем его в 5.

$ touch /tmp/lines.txt
$ exec 5<> /tmp/lines.txt

В примере, приведенном ниже, мы копируем первые 3 строки /usr/share/dict/propernames в /tmp/lines.txt.

$ head -n 3 /usr/share/dict/propernames 1>&5
$ cat /tmp/lines.txt
Aaron
Adam
Adlai

Обратите внимание, что если бы мы попытались читать с 5 с помощью cat, мы не получили бы никакого вывода, так как при записи мы продвинулись к файлу и теперь 5 не является его концом.

$ cat 0<&5

Мы можем решить эту проблему, если закроем и снова откроем 5.

$ exec 5<&- $ exec 5<> /tmp/lines.txt
$ cat 0<&5
Aaron
Adam
Adlai

Постскриптум

О генерации случайных чисел

В приведенных выше примерах я использовал $RANDOM для генерации случайных чисел и передавал их в sed, чтобы выбрать случайные строки из файла /usr/share/dict/words.

Вы могли заметить, что при этом слова часто начинаются с a, b или c. Так происходит потому, что RANDOM имеет двухбайтную длину и ему доступен диапазон от 0 до 32,767.

Файл /usr/share/dict/words имеет 235 886 строк.

Поскольку наибольшее число, которое может сгенерировать RANDOM, примерно в 7 раз меньше /usr/share/dict/words, он не подходит для выбора рандомных слов из этого файла. В этом посте я его использовал только для простоты.

О TTY и устройствах ввода/вывода

Я нарочно обошел некоторые подробности, когда объяснял, какое место занимают TTY и эмулятор терминала среди девайсов ввода/вывода и процессов.

Вы можете найти гораздо больше информации и более глубокие объяснения для всех компонентов этого коммуникативного процесса в выдающемся посте «The TTY Demystified» (автор – Linus Åkesson).

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

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

Back to Top