Устройство подсистемы TTY

Опубликовано: 20.10.2020

Введение

Подсистема TTY — одна из ключевых особенностей Unix и Unix-подобных операционных систем, и в частности — современных Linux, macOS, FreeBSD.

В этой статье я постараюсь кратко, но достаточно ёмко и доступно для читателей разного уровня подготовки разобрать устройство подсистемы TTY и её взаимодействие с другими частями программной среды.

Описание архитектуры TTY, изложенное здесь, не является на 100% корректным. Некоторые части были намеренно упрощены или опущены, чтобы итоговая картина получилась более простой и цельной. Эти детали не являются критичными для понимания принципов работы TTY. При необходимости все детали могут быть восстановлены чтением страниц руководств в составе операционной системы.

Статья представляет собой высокоуровневое описание функциональных частей и режимов TTY с точки зрения пользователя или прикладного программиста.

Происхождение подсистемы TTY в UNIX

В мире Unix подключение к компьютерам осуществлялось при помощи терминалов — рабочих мест, оборудованных клавиатурой и устройством вывода информации: в простейшем случае, принтером, а в ходе дальнейшего развития технологий — экраном.

Электромеханические устройства, оборудованные клавиатурой и простым принтером, назывались словом телетайпteletype, сокращенно TTY. Отсюда и название подсистемы TTY.

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

В отличие от «умных» терминалов, применявшихся в мейнфреймах, терминалы для Unix-машин были «тупыми». Это означает, что терминалы занимались в основном только тем, что отправляли на машину коды клавиш, нажимаемых пользователем, и выводили на экран (или принтер) информацию, которую присылает машина. Все задачи по «интеллектуальной» обработке информации брала на себя Unix или приложение. Терминал не имел встроенных возможностей редактирования информации или манипулирования структурироваными данными, такими как записи с именованными полями и формы.

Такой подход соответствует принятому в Unix принципу проектирования, согласно которому простые решения предпочтительнее сложных.

Вместе с тем, согласно этому же принципу, программы должны быть как можно проще, поэтому базовые функции редактирования ввода брала на себя операционная система. Приложения просто обрабатывали строки символов, не беспокоясь о том, откуда эти символы пришли: с терминала или из файла.

Еще одной необходимой возможностью является прерывание работы процесса в случае, если он завис или просто стал не нужен. Поскольку операционная система была единственным промежуточным звеном между терминалом и приложением, то задача прерывания процесса по команде пользователя также возлагалась на неё.

Впоследствии были реализованы расширенные возможности управления процессами: остановка группы процессов, переключение между группами процессов. То, что сейчас в документации обычно называется job control.

Таким образом был очерчен круг задач операционной системы при работе с терминалом:

Псевдотерминалы (PTY)

Сейчас, когда физические терминалы остались только в музеях, задачи по управлению физическим каналом связи с терминалом отошли на второй план. В этой статье я не буду останавливаться на особенностях TTY, связанных с настройками канала связи.

Впрочем, по-прежнему можно подкючиться в Unix-подобной системе через COM-порт, используя другой компьютер в качестве терминала.

В абсолютном же большинстве случаев работа с консольными приложениями осуществлятся через программные эмуляторы терминалов. Для этого в операционной системе предусмотрен механизм псевдотерминалов — PTY.

Каждый псевдотерминал состоит из пары виртуальных устойств: master и slave. Master-устройство предназначено для связи эмулятора терминала и операционной системы как если бы это был физический канал связи с реальным терминалом. Slave-устройство предназначено для связи операционной системы и консольных приложений.

Приложение эмулятора терминала при запуске запрашивает у операционной системы создание нового псевдотерминала. Master-устройство оно оставляет себе для связи с операционной системой. А slave-устройство передаётся в процесс запускаемого консольного приложения.

Получается следующая схема:

Устройство подсистемы TTY

Упрощенно внутреннее устройство TTY можно представить в виде следующей диаграммы:

На диаграмме представлены следующие функциональные блоки:

  1. BUFFER на вывод: поток байт, ожидающий отправки в терминал. В случае физического терминала — данные передаются по физической линии через COM-порт или модем. В случае псевдотерминала — данные просто передаются через файловый дескриптор master-устройства в процесс эмулятора терминала.

  2. BUFFER на ввод: поток байт, ожидающий чтения приложением.

  3. CONTROL — блок контроля приёма-передачи данных. Позволяет пользователю временно останавливать вывод информации на терминал. При работе с физическим терминалом в его обязанности также входит согласование приёма и передачи данных между компьютером и терминалом.

  4. ECHO — блок, пересылающий вводимые с терминала символы обратно в терминал, чтобы они могли быть отображены на экране.

  5. CANON — блок, реализующий канонический режим ввода (canonical input mode), который включает в себя простейшие функции редактирования текста.

  6. SIG — блок, выполняющий отправку сигналов.

  7. TERMIOS — блок управления режимами работы TTY.

Следует заметить, что такое разделение на блоки не соответствует делению на файлы, модули или классы в какой-либо реальной операционной системы. Также подобного разделения в явном виде вы не найдёте в POSIX. Выделение этих функциональных блоков выполнено для удобства изложения.


Блок CONTROL

Блок CONTROL позволяет пользователю временно останавливать вывод на терминал.

Здесь и далее, когда речь идёт об управляющих символах, я буду указывать не их названия из документации, а конкретные «клавиатурные» значения, как они определены по умолчанию в Linux и BSD. Эти клавиатурные значения могут быть переопределены в настройках termios. Ниже в разделе, посвященном TERMIOS, приводится таблица с их документированными названиями. Я исхожу из того, что читателю намного интереснее узнать каким сочетанием клавиш вызывается то или иное действие, а не то, какое имя этого действия используется в коде подсистемы TTY.

При получении от терминала управляющего символа ^S (Ctrl + S) — останавливает вывод на терминал. Отправка данных в терминал из выходного буфера прекращается. Процессы, которые пытаются вывести данные на терминал, блокируются на вызове write().

При получении от терминала управляющего символа ^Q (Ctrl + Q) — возобновляет вывод на терминал. Отправка данных в терминал из выходного буфера продолжается. Процессы, которые ранее были заблокированы на вызове write(), продолжают работу.

При получении от терминала управляющего символа ^O (Ctrl + O) — останавливает вывод на терминал, но не блокирует процессы. Все данные, посылаемые процессами на терминал, просто отбрасываются. Режим остаётся включеным до тех пор, пока от терминала не поступит любой другой ввод. Ввод любого символа отменяет этот режим. Этот управляющий символ реализован только в ОС семества BSD и не реализован в Linux.

К этому же блоку можно отнести обработку управляющего символа ^V (Ctrl + V) — команду литеральной вставки. Любой символ, ввёденный следом за ^V, вставляется в буфер ввода как есть, игнорируя управляющее значение этого символа. Таким образом, чтобы вставить в буфер ввода ^S, следует ввести ^V^S, чтобы вставить в буфер ввода ^V, следует ввести ^V^V. Это правило распространяется также на все управляющие символы, описанные далее.


Блок CANON

Обработка ввода может производиться в каноническом или неканоническом режиме.

В неканоническом режиме вводимые символы поступают из блока CONTROL сразу в буфер ввода. Все управляющие символы, связанные с каноническим режимом, не имеют специального значения, и также передаются в буфер ввода «как есть». Приложение может считывать из буфера данные произвольной длины — в том числе нулевой длины, если буфер пуст. (То есть использовать неблокирующее чтение данных.)

В каноническом режиме вводимые символы поступают из блока CONTROL в блок CANON. Запись в буфер ввода производится не произвольно, а контролируется пользователем. Приложение считывает данные из буфера ввода построчно.

Современные консольные приложения, такие как bash, htop или Midnight Commander используют неканонический режим ввода. Для обработки ввода и вывода применяются различные библиотеки: в bash это библиотека readline для организации строки ввода команд; в htopncurses для рисования интерфейса.

Канонический режим используют приложения, не предназначенные для интерактивного взаимодействия с пользователем, такие как cat, grep и т.п. Точнее сказать, они вообще не «используют» терминал целенаправленно, а просто читают данные со стандартного потока ввода. TTY при этом обычно находится в каноническом режиме.

Старые или урезанные командные интерпретаторы такие как dash, tclsh, ed также обычно работают в каноническом режиме.

Блок CANON отвечает за работу канонического режима. Он накапливает вводимые символы в своём внутреннем буфере и реагирует на следующие управляющие символы:

^J (Ctrl + J). По нажатию Ctrl + J код конца строки ^J дописывается к накопленной строке, и строка отправляется в буфер ввода. В Unix применяется код конца строки ^J, также известный по таблице контрольных кодов ASCII как Line Feed. В языке Си ему соответствует обозначение \n.

^M (Ctrl + M или Enter). По нажатию Ctrl + M или Enter код конца строки ^J дописывается к накопленной строке, и строка отправляется в буфер ввода. Код ^M также известен по таблице контрольных кодов ASCII как Carriage Return. В языке Си ему соответствует обозначение \r. Следует обратить внимание, что в буфер записывается не код ^M, а код ^J. Таким образом при вводе с терминала подсистема TTY автоматически заменяет Carriage Return на Line Feed, когда пользователь нажимает Enter в каноническом режиме.

Это поведение — конвертировать Carriage Return в Line Feed или воспринимать его как рядовой символ — задаётся в настройках termios.

EOL и EOL2. Это «дополнительные» коды конца строки, которые приложение может сконфигурировать для себя. По умолчанию это неназначенные коды. Через termios можно назначить этим кодам реальные значения. В этом случае они будут действовать так же, как ^J: дописывать себя в строку и отправлять строку в буфер ввода. Также обратите внимание, что в отличие от ^M, они не заменяются на ^J, а записываются как есть.

^D (Ctrl + D). По нажатию Ctrl + D накопленная строка пересылается в буфер ввода. Код ^D не дописывается к строке.

Если нажать Ctrl + D в начале строки, то в буфер ввода будет отправлена строка нулевой длины. Программы, ожидающие ввод в каноническом режиме, воспринимают такой ввод как «конец файла» и, как правило, завершают работу или переходят к обработке следующего файла.

Backspace. По нажатию клавиши Backspace последний символ в накопленной строке стирается, и строка перерисовывается на экране терминала. (Если эхо-режим включен.)

^W (Ctrl + W). По нажатию Ctrl + W последнее слово в накопленной строке стирается, и строка перерисовывается на экране терминала. (Если эхо-режим включен.)

^U (Ctrl + U). По нажатию Ctrl + U вся накопленная строка стирается, и строка перерисовывается на экране терминала. (Если эхо-режим включен.)

^R (Ctrl + R). По нажатию Ctrl + R, если эхо-режим включен, в терминал посылается команда перехода на новую строку, после чего накопленная строка перерисовывается на экране. Это может быть необходимо, если во время ввода строки какая-нибудь программа напечатала что-то на экран и тем самым испортила отображение вводимой строки.

Следует отметить, что возможности редактирования строк в каноническом режиме весьма ограничены. Возможно только стирать последний символ, последнее слово или же всю строку. Кроме того, поскольку подсистема TTY ничего не знает о ширине терминала, она не может правильно обработать стирание в случае, когда одна длинная строка представлена как несколько экранных строк. Когда в терминале произошел перенос строки, команды стирания символов могут стереть только последнюю экранную строку, но не могут подняться выше. В этом случае помогает ^R, чтобы увидеть актуальное содержимое.

При этом, в POSIX стандартизированы только ^J, ^M, EOL, ^D и Backspace. Остальное хоть и реализовано одинаково в Linux и BSD, не является стандартом.


Блок ECHO

Блок ECHO просто посылает символы обратно на терминал.

Эхо-режим на уровне TTY может быть включён и выключен приложением через настройку termios.

В неканоническом режиме приложение, как правило, само реализует эхо при необходимости. В каноническом режиме эхо обычно должно быть включено, чтобы было возможно видеть редактируемую строку.


Блок SIG

Блок SIG отвечает за управление заданиями и отправку сигналов.

Для управления заданиями используются группы процессов:

  1. Каждый процесс может принадлежать так называемой группе процессов.
  2. Группа процессов может быть ассоциирована с терминалом.
  3. Среди всех групп процессов, связанных с конкретным терминалом, одна группа может являться активной для заданного терминала, а остальные — фоновыми.

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

Если процесс, принадлежащий фоновой группе, пытается прочитать данные из терминала, подсистема TTY посылает сигнал SIGTTIN всей группе. По умолчанию этот сигнал приостанавливает выполнение процессов.

Если процесс, принадлежащий фоновой группе, пытается вывести данные на терминала, поведение TTY зависит от флага TOSTOP в настройках termios. Если флаг установлен, то аналогичным образом подсистема TTY посылает сигнал SIGTTOU всей группе. Этот сигнал по умолчанию также приостанавливает выполнение процессов.

Если флаг TOSTOP не установлен, вывод на экран для фоновых процессов разрешен.

Пользователь может иницировать отправку сигналов, используя следующие управляющие символы. Во всех случаях сигнал посылается активной группе процессов:

^C (Ctrl + C). Посылает сигнал SIGINT: по умолчанию завершает работу программы. Смысловое назначение этого сигнала: “пользователь хочет прервать работу программы, так как она ему не нужна”.

^\ (Ctrl + \). Посылает сигнал SIGQUIT: по умолчанию завершает работу и выполняет дамп памяти. Смысловое назначение этого сигнала: “пользователь хочет прервать работу программы из-за сбоя или для отладки”.

^Z (Ctrl + Z). Посылает сигнал SIGTSTP: по умолчанию приостанавливает выполнение процессов.

^Y (Ctrl + Y). Отложенная остановка. Аналогично ^Z, но приостановка выполняется не сразу при получении символа, а когда процесс попытается прочитать этот символ вызовом read(). Этот управляющий символ поддерживается в BSD и не поддерживается в Linux.

^T (Ctrl + T). Выводит на терминал информацию о состоянии выполняемого процесса. Также посылает сигнал SIGINFO, при получении которого процесс может вывести расширенную информацию о своём состоянии. Этот управляющий символ поддерживается в BSD и не поддерживается в Linux.

Когда в оболочке, имеющей поддержку управления заданиями, такой как bash или ksh, вы запускаете на исполнение команду, оболочка создаёт для выполнения команды новую группу процессов и назначает её активной.

Если пользователь приостанавливает выполнение активной группы нажатием Ctrl + Z, процессы активной группы получают сигнал SIGTSTP, а оболочка после этого получает сигнал SIGCHLD. Таким образом она узнает, что процессы приостановлены.

Оболочка делает приостановленную активную группу фоновой, а свой собственный процесс делает активной группой.

Далее пользователь может запускать новые команды, а также использовать команды оболочки для управления заданиями:

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

Блок TERMIOS

Блок TERMIOS отвечает за настройку всех частей подсистемы TTY. Приложение может получить текущие настройки при помощи tcgetattr() и установить новые настройки при помощи tcsetattr().

Среди множества всех настроек termios можно выделить основные флаги управления режимами:

Флаг Назначение
IXON Включает/выключает управление пересылкой данных по ^S, ^Q.
IXANY Если включен, любой следующий символ выводит TTY из состояния STOP. Если выключен, только ^Q делает это.
IEXTEN Включает ^V и ^O.
ICANON Включает/выключает канонический режим.
ECHO Включает/выключает эхо.
ISIG Включает/выключает отправку сигналов по ^C, ^\, ^Z, ^Y.
TOSTOP Выполнять приостановку процессов фоновых групп, если они пытаются выводить данные на терминал.

termios позволяет переопределять управляющие символы. Можно переопределить любые символы, кроме ^J (Line Feed) и ^M (Carriage Return).

В следующей таблице приведены названия и назначение управляющих символов:

Название Символ по умолчанию Назначение
DISCARD Ctrl-O (Нет в POSIX; не поддерживается в Linux.) Включить/выключить отбрасывание ожидающего вывода. Распознаётся, если включен IEXTEN.
DSUSP Ctrl-Y (Нет в POSIX; не поддерживается в Linux.) Отложенный приостанов процесса. Посылает сигнал SIGTSTP при считывании символа программой пользователя. Распознаётся, если включены IEXTEN и ISIG, и система поддерживает управление заданиями.
EOF Ctrl-D Конец файла. Этот символ заставляет переслать буфер tty программе пользователя без ожидания конца строки. Если это первый символ в строке, то read(2) вернёт программе 0, что означает конец файла. Распознаётся, если включен ICANON.
EOL (NUL) Дополнительный символ конца строки. Распознаётся, если включен ICANON.
EOL2 (NUL) (Нет в POSIX) Ещё один символ конца строки. Распознаётся, если включен ICANON.
ERASE Backspace (^? или ^H) Забой. Стирает предыдущий ещё не стёртый символ, но не стирает за EOF или начало строки. Распознаётся, если включен ICANON.
INTR Ctrl-C Посылает сигнал SIGINT активной группе процессов. Распознаётся, если включен ISIG.
KILL Ctrl-U Стирает ввод начиная вплоть до EOF или начала строки. Распознаётся, если включен ICANON.
LNEXT Ctrl-V (Нет в POSIX.) Экранирует следующий введённый символ, отменяя его возможное специальное значение. Распознаётся, если включен IEXTEN.
QUIT Ctrl-\ Посылает сигнал SIGQUIT активной группе процессов. Распознаётся, если включен ISIG.
REPRINT Ctrl-R (Нет в POSIX.) Вывести заново непрочитанные символы. Распознаётся, если включены ICANON и IEXTEN.
START Ctrl-Q Перезапускает вывод, остановленный символом STOP. Распознаётся, если включен IXON.
STATUS Ctrl-T (Нет в POSIX; не поддерживается в Linux.) Выводит информацию о состоянии на терминал, включая состояние активного процесса и количество потраченного времени ЦП. Также посылает сигнал SIGINFO (не поддерживается в Linux) активной группе процессов. Распознаётся, если включен ICANON.
STOP Ctrl-S Приостанавливает вывод до появления символа запуска. Распознаётся, если включен IXON.
SUSP Ctrl-Z Посылает сигнал SIGTSTP активной группе процессов. Распознаётся, если включен ISIG, и система поддерживает управление заданиями.
WERASE Ctrl-W (Нет в POSIX.) Стирание слова. Распознаётся, если включены ICANON и IEXTEN.

Просмотреть или изменить настройки termios можно при помощи программы stty.

Пример отображения настроек stty в графическом эмуляторе терминала под Linux:

И в ядерной консоли NetBSD:

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

Два основных режима работы TTY для приложений

Итак, старые или неинтерактивные приложения используют «приготовленный» режим работы TTY, в котором:

Современные интерактивные приложения стремятся реализовать обработку ввода своими силами. Поэтому они отключают «лишние» возможности:

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

«Приготовленный» режим является режимом по умолчанию для программ, которые «не ожидают ничего другого». Если приложению нужен «сырой» режим, оно обязано позаботиться о том, чтобы сохранить настройки «приготовленного» режима и вовремя восстанавливать их.

Почему за отображение на экране вводимых символов отвечает подсистема TTY? Почему терминал сам не делает эхо?

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

Конечно, терминалы 80-х годов, работающие под микропроцессорным управлением, легко бы справлялись с этой задачей. Так же как и современные эмуляторы терминалов. Но принцип работы терминала к тому времени уже устоялся.

Однако во многих терминалах режим эхо всё же был. Его и сейчас можно вручную включить ESC-последовательностью ^[[12l (и выключить при помощи ^[[12h). Правда если это сделать, вводимые символы будут двоиться на экране, ведь теперь у нас два эхо-режима работает одновременно.

Мне неизвестно, для чего был введён режим, включаемый ESC-последовательностью ^[[12l, и как он использовался. Может быть, он был полезен при подключении терминала к компьютерам под управлением не Unix-подобных систем? Но в современном мире этот режим, похоже, совсем не используется.

Что делать, если режим TTY и/или режим работы терминала сбился?

Бывает, что программа завершилась некоррректно и оставила терминал в «странном» состоянии, или же бинарные данные по ошибке были выведены на терминал, и он проинтерпретировал их как ESC-команды.

Для такого случая будет полезно заранее добавить в конфиг оболочки следующий алиас:

alias fixterm='stty sane; tput rs1; echo -e "\033c"'

Команда stty sane сбрасывает настройки подсистемы TTY. Команда tput rs1 сбрасывает настройки терминала в соответствии с данными из базы данных terminfo. И ESC-код ^[c также сбрасывает настройки терминала.

Почему для Backspace указаны два символа: ^? и ^H?

Исторически сложилось, что разные терминалы и эмуляторы терминалов при нажатии на физическую клавишу Backspace посылают разный код клавиши — ^? или ^H:

ASCII BS  == 0x08 == ^H
ASCII DEL == 0x7f == ^?

Сейчас вариант ^? победил практически повсеместно.

В частности, ^? используют по умолчанию:

^H используют по умолчанию:

Это приводит к двум проблемам:

  1. termios должен быть правильно настроен на работу с конкретным эмулятором терминала.
  2. При использовании ^H для стирания ввода сочетание Ctrl-H становится недоступным для приложений под другие команды.

К счастью, с проблемой неправильной настройки termios на практике сталкиваться практически не приходится.

Что касается Ctrl-H, то действительно: при использовании xterm с настройками по умолчанию вы не сможете задействовать это сочетание клавиш под другие функции приложения.

Подробное объяснение этого исторического казуса и советы по настройке приложений можно найти в статье Consistent BackSpace and Delete Configuration.

Почему канонический режим ввода такой бедный? Можно ли расширить канонический режим новыми возможностями, такими как полноценная правка с передвижением курсора по всей строке и вставка/удаление символов в любой позиции?

Возможности канонического режима отражают возможности аппаратуры 70-х годов. Как и во многих случаях, вместо совершенствования существующих систем, люди предпочли изобретение новых.

Теоретически, ничего не мешает расширить канонический режим новыми функциями. Но в практическом плане это слишком трудоёмко, поскольку TTY реализована как часть ядра. Если бы эта подсистема была выполнена отдельным процессом, как например в микроядерной GNU/Hurd, создать альтернативную реализацию было бы не сложно. В случае с ядром Linux (или даже BSD) трудозатраты достаточно велики, чтобы к настоящему времени никто таких возможностей не реализовал.

Возможно это и правильно. А может быть, всё-таки стоило вынести логику TTY в отдельный пользовательский процесс?..

Замена каноническому режиму?

Если программа выполняет ввод в каноническом режиме, то для расширенной поддержки редактирования можно воспользоваться утилитой rlwrap:

rlwrap runs the specified command, intercepting user input in order to provide readline’s line editing, persistent history and completion.

rlwrap tries (and almost succeeds) to be completely transparent - you (or your shell) shouldn’t notice any difference between command and rlwrap command - except the added readline functionality, of course. This should even hold true when you are re-directing, piping and sending signals from and to command, and when command manipulates its terminal settings or working directory.

Таким образом можно запустить, например, интерпретатор dash:

rlwrap dash

Или tclsh:

rlwrap tclsh

Также можно использовать rlwrap в скриптах:

printf "Enter the URL:\n"
url="`rlwrap -o cat`"
test -n "$url" && elinks "$url"

Ссылки:

  1. Single UNIX Specification, version 3
  2. GNU Libc Manual
  3. NetBSD termios(4)

Оставить комментарий к статье можно: