О терминалах
- Полезные ссылки
- Немного важной истории
- Терминал и Компьютер
- Работа терминала
- Эмуляторы Терминалов
- Драйвер терминала или дисциплина линии
Полезные ссылки
- Старая, но часто цитируемая статья и её перевод
- Linux API. Исчерпывающее руководство. Керриск Майкл. - отдельная глава о терминалах и псевдотерминалах.
- Учебная ОС xv6 и осбенно книга по ней.
- Документация к одому из терминалов.
- ANSI Escape Sequences
- man console_codes - описание всех кодов встроенной подсистемы tty Linux-а
- Эмулятор терминала st.
Мои дальнейшие заметки здесь являются, по большей части, подробным пересказом вышеприведенных ресурсов.
Сначала Я расскажу о работе обычных, т.е физических терминалов, которые были основным способом взаимодействия пользователя с компьютером в 60-80х годах. Они оказали немалое влияния на некоторые аспекты Unix-подобных операционных систем и продолжают существовать(будучи реализованными программно) и широко использоваться даже после своего физического исчезновения. Также затрону и современные эмуляторы терминалов. На Мой взгляд это крайне полезное знание, которое снимает множество вопросов при работе в Unix-подобных ОС.
Немного важной истории
Сегодня с помощью клавиатуры, мыши и графического интерфейса Мы легко можем отдавать команды компьютеру - для нас это тривиальные действия. Но в 40-50х годах в распоряжении инженеров(основных пользователей ими же созданных компьютеров) не было ни мониторов, ни мышей, ни даже клавиатур как таковых. Как же тогда обычным пользователям взаимодействовать с компьютером не прибегая к трудозатратному вводу/выводу с помощью перфокарт? Решение оказалось совсем рядом, ведь клавиатура на самом деле уже была, правда в составе другого устройсва - печатной машинки. Какая удача, ведь вместе с клавиатурой мы сразу в комплекте получаем и лист бумаги, который может служить средой для вывода информации(свого рода монитором). Осталось лишь слегка передалать нашу машинку и подключить ее к компьютеру. Такая подключенная к компьютеру машинка получила два названия, которые и по сей день вносят сумятицу в компьютерную терминологию:
-
Первое и наиболее часто используемое - терминал. В английском слово terminal означает некую сущность на которой все заканчивается. В русском языке это отразилось в словосочетании “терминал аэропорта”, ведь это место является конечным пунктом для рейса. Дабы не придумывать еще одно подобное слово, точку отправления рейса также называют терминалом. Лист в нашей печатной машинке как раз и является той самой конечной точкой для данных, а клавиатура точкой отправления данных.
-
Второе - консоль. Здесь все интереснее и запутаннее. В общем случае слово console означает некий пульт управления, но так исторически сложилось, что консолью называли не любую печатную машинку, а только те с которых можно было осуществлять управление системой в целом(настройка, включение/выключение). Это была уже не маленькая печатная машинка, а целый стенд с кучей кнопок, то есть консоль - это главный терминал компьютера(да, к древним компьютерам могло быть подключено сразу много терминалов). Со временем такие отдельно стоящие пульты управления исчезли, и все стало управляться с обычной клавиатуры, но название осталось. Им стали называть и обычные терминалы. На мой взгляд это вносит немалую путаницу в терминологию, поэтому далее я буду использовать только первое именование -
терминал
. -
На самом деле есть еще и третье - телетайп, так называлась такая машинка будучи подкюченной не к компьютеру, а к другой такой машинке. Именно это устройство и было взято за основу инженерами при создании первых терминалов.
Очевидно, что с листом бумаги в качестве монитора на многое рассчитывать не приходится, и с развитием электроники он был заменен на экран. Интерсно то, что этот экран был текстовым, то есть мог отображать лишь определенный набор символов, управлять цветом отдельных пикселей было нельзя. Позиция печати следующего символа указывалось положением курсора
терминала. По сути это та же печатная машинка, но с текстовым дисплеем вместо листа бумаги. Очевидным преимуществом является возможность стирать произвольный символ и не бегать за бумагой. Такой механизм уже назывался непосредственно “терминал”, а точнее “видеотерминал”.
С исторей пока все, далее опишу как же осуществляется взаимодействие терминала с компьютером.
Терминал и Компьютер
Сначала в общих чертах рассмотрим их взаимодействие, а затем подробнее разберём происходящее в терминале и ОС компьютера.
Без схем теперь не разобраться:
Рассмотрим блок Hardware:
Здесь схематично изображено подключение терминала к компьютеру. UART на схеме - это разъем в компьютере для подключения терминала, т.е терминал и компьютер взаимодействуют по протоколу
UART. Это один из первых протоколов передачи данных физического уровня. В каком-то смысле это далёкий предок современного USB. Именно с помощью UART порта(также называемого Serial port) к старым компьютерам подключались терминалы, принтеры и даже другие компьютеры. Если не вдаваться в детали, то схема его работы очень проста: последовательная передача байтов в одну и в другую стороны. Так, например, нажатие кнопки на клавиатуре терминала приводит к отправке одного или нескольких байт в UART порт, а ОС компьютера может аналогично отправлять байты терминалу. Терминал определенным образом интерпретирует полученные символы, например печатает символ или передвигает курсор на новую строку, стирает строку, итд. То какие именно байты летают между терминалом и компьютером, расскажу позже.
Небольшое отсупление о протоколах и реализациях: выше Я упомянул сначала протокол
UART, а затем UART порт. Протокол представляет собой описание схемы взаимодейтсвия, т.е это своего рода инструкция согласно которой мы должны организоввывать связь. UART порт - это реализация этого протокола с помощью микросхемы, управляющей передачей сигнала пр проводу. Подробнее можно прочиать здесь.
Теперь к блоку Software:
Будем рассматривать происходящее, предполагая, что компьютер находится под управление ОС семейсва Unix. Примеры будут на языке C.
Начнём с того, как пользовательскому процессу организовать доступ к терминалу. Печатая что-либо на клавиатуре терминала мы, очевидно, хотим передать некие данные некой программе. Как же программа сможет получить эти данные и записать на экран терминала выходные данные?
Для взаимодействия с обычным файлом, мы должны “открыть” его в нашем процессе, получив файловый дескриптор:
int fd = open ("/path/to/file", flags)
Затем с его помощью можно производить операции над файлом в нашем процессе. При попытке, например, записи/чтения ОС поймет, что операции производятся над обычным(т.е находящимся на диске) файлом и с помощью драйвера диска выполнит запрос. Хорошо было бы организовать подобную схему для взаимодействия и с терминалом. В ОС семейтсва Unix именно так и делают.
Здесь Я не буду подробно описывать детали реализации(о них можно почитать в вышеупомянутой книге Керриска или xv6 book), скажу лишь то, что для терминала и других переферийных устройств ОС создаёт специальные файлы устройств
, при взаимодействии с таким файлом(точнее с его файловым дескриптором) ОС использует соответсвующий драйвер устройства для взаимодействия. Файлы терминалов традиционно называются tty
(опционально с каким-нибудь постфиксом). Это сокращение от слова teletype - выше Я не зря упоминал связь названий “телетайп” и “терминал”. Такие файлы можно в изобили найти в директории /dev.
Теперь обратим внимание на пунктирный прямоугольник. В нем перечислены части операционной системы, служащие для взаимодействия пользовательских процессов и терминала:
UART driver - это программа с помощью которой можно осуществлять чтение и запись в UART порт, а так как на другом конце UART-линии находится терминал, то это будут чтения/записи терминала. Также драйвер может позволять настраивать UART, например устанавливать скорость передачи и другие никоуровневые параметры. Пример такого драйвера из учебной ОС xv6: тык.
Казалось бы, драйвер UART предоставляет вполне достаточный набор функций - чтение и запись байтов, но создетели ОС Unix решили добавить еще один слой - специальный драйвер терминала, значильно расширяющий базовую функциональность. Именно он обрабатывает вызовы read/write для файловых дескрипторов терминалов(также еще есть вызов ioctl, с помощью которого осуществляется настройка драйвера). Далее мы увидим какие преимущества дало такое решение.
На схеме представлены блоки TTY driver и Line discipline
- здесь выделены отдельно, но фактически Line discipline или “дисциплина линии” является непосредственной частью драйвера терминала, более того она составляет основу самого драйвера. Я точно не знаю от чего пошло столь странное название, поэтому просто скажу, что она особым образом обрабатывает байты, полученные(с помощью вышеописанного драйвера UART) от терминала. Подробный разбор схемы работы дисциплины линии(т.е драйвера терминала) будет позже.
Это здесь, если не терпится: Драйвер терминала
Работа терминала
Общая схема устройства терминала:
Клавиатура
Клавиатура одного из самых распространненых терминалов VT100, мало чем отличается от современных:
Это документация к клавиатуре терминала VT102 - Я буду часто на неё ссылаться.
Рассмотрим, что отправляется компьютеру при нажатии клавиш. Каждой клавише, очевидно, должен соответствовать некий цифровой код, состоящий из одного или нескольких байт. Дабы избежать всеобщей путаницы с кодами, они были стандартизированы. Для большинства клавиш стандартом является знаменитая 7-битная кодировка ASCII. Она была специально создана для использования в телетайпах, а потом перекочевала в терминалы. Кодировка состоит из обычных и управляющих символов. Первые 32 символа и последний 128-й являются управляющими - это перевод строки, табуляция, окончание передачи, итд - полный список с описанием. Строго говоря, это не совсем симвлолы в привычном понимании, а управляющие коды с помощью которых можно контролировать работу как компьютера, так и самого терминала. Многие из них не используются по назаначению, так как имеют смысл только для старых “бумажных” телетайпов и терминалов или же просто устарели и более не востребованы. Позже Я подробнее поясню, то как коды этих клавиш обрабатываются компьютером и самим терминалом. Остальные символы - привычные нам печатаемые латинские буквы, цифры, знаки препинания - называются печатаемыми. При нажатии клавишу, например, q
мы отправим его ASCII код \x71 компьютеру, а при нажатии Return
- код управляющего символа CR: \x0D
.
Важное замечание: при нажатии любой клавиши, её код отправляется компьютеру, но не передаётся экрану терминала, т.е чтобы напечатать введенный символ нужно явно отправить его терминалу из компьютера. Дело в том, что программам далеко не всегда нужно печатать на экран введенные пользователем данные, поэтому для большей гибкости применяется такой подход.
Также существуют т.н функциональные клавиши(стрелки, Escape, Ctrl, Caps Lock итд) - полный список в документации. Некоторые из них не передают никаких символов. Например Caps Lock и Shift - устанавливают регистр буквенных символов(как и по сей день). Отдельно стоит отметить клавишу Ctrl - она используется в связке с другими клавишами для генирования управляющих символов ASCII - в документации таблица 4-2. Механизм её работы таков: она зануляет старший бит кода нажатой вместей с ней клавиши. Напримиер, код клавиши D
- 1000100
- при нажатии вместе с Ctrl превращается в 0000100
- управляющий символ с названием EOT
. Такой способ ввода управляющих символов используется очень широко и получил собственное обозначение - т.н caret notation, т.е EOT также можно обозначить как ^D
.
Для некоторых наиболее часто используемых управляющих символов выделены отдельные клавишы: Back Space, Tab, Esc, Return, Line Feed, Enter(в правом нижнем углу клавиатуры).
Особое внимание стоит уделить клавишам Return
, Enter
и Line Feed
. На современных клавиатурах остались Return
(та, что с изогнутой стрелкой) и Enter
. Мы не совсем осознанно называем клавишу Return
“энтером”, а настоящую клавишу Enter
обычно не используем. Дело в том, что исторически функции этих клавиш были одинаковы, за тем исключением, что Enter
расположена в блоке клавиш под названием Numeric Keypad. Numeric Keypad - это своего рода отдельная мини-клавиатура когда-то использовавшаяся для более удобного взаимодействия с некоторыми программами. У неё есть два режима работы. В своём обычном режиме её клавиши генерируют те же коды, что и основная клавиатура, и тогда Enter
эквивалентен Return
. Очевидно, название Enter
звучит более выразительно чем Return
, и поэтому мы все используют именно его. Если интересно, то об этом также можно почитать в документации под таблицей 4-2(Я бы не рекомендовал - и так куча всего непонятного).
На клавише Return
не зря изображено стрелка, указывающая налево. Данная клавиша генерирует управляющий символ с кодом \x0D
чаще представляемый как \r
(нотация из языка С). Называется он carriage return
(CR) или “возврат каретки” и, будучу переданным терминалу, указывает ему перевести курсор к началу строки.
Клавиша Line Feed
: здесь все интереснее. Она производит на свет повсеместно используемый управляющий символ \x0A
или \n
в нотации C. Правильно он называется не самым очевидным образом: Linefeed
(LF) - перевод строки. Почему такой клавиши нет на современных клавиатурах? Откуда, в таком случае, мы получаем символ перевода строки? Сейчас расскажу коротко: драйвер терминала(точнее та самая дисциплина линии) заменяет символ клавиши Return
- \r
на \n
или же (в зависимости от настройки) добавляет его после \r
и поэтому клавиша Line Feed
была убрана за ненадобностью. В следующем разделе Я подробнее пройдусь по этому вопросу.
Осталась последняя значимая группа клавиш: стрелки или как их называли тогда - клавишы управления курсором. Для их кодировки используется не ASCII, а специальные последовательности стандарта ANSI(сейчас это стандарт ECMA-48) состоящие из ASCII символов. Называются они ANSI Escape Sequences
или “Управляющие последовательнсоти ANSI”. Последовательнстей состоят из трех и более байт. Начинаются они с CSI - Control Sequence Introducer - двух байтов: \x1B
и [
. Первый из них назвается “Escape”(ESC). CSI нужна для того чтобы показать, что следующие символы нужно трактовать особым образом. После CSI идут аргументы последовательнсти, которые и определяют её. Например последовательность ESC[2J
предназначена для стирания содержимого экрана терминала. Кстати, запись вида \x1B
также называется escape последовательнсотью во многих языках программирования.
Небольшое отступление: Стандарт “Управляющих последовательнсотей ANSI” - это своего рода развитие концепции управляющих символов ASII, когда последних стало не достаточно для управления аппаратурой. Стоит отметить, что он включает в себя не только новые многобайтные последовательности, но также и некоторые старые управляющие символы ASII. В этом стандарте их группа называется “C0 control codes”.
Вернёмся к кодировкам наших стрелок. Как Я уже упомянул, их изначальным предназначением явлется управление курсором. Для этого существуют следующие ANSI последовательности:
Здесь CSI это ESC[
.
Как видно из описания этих четырех команд, n - это число клеток для перемещения с дефолтным значением 1. Тогда для перемещения на одну клетку n можно не указывать вовсе - как раз то, что нужно для стрелок. В документации в таблице 4-1 все они перечислены. Итак, нажатие любой из четырех стрелок генерирует трехбайтную последовательнсть отправляемую компьютеру.
Наверняка у вас возникло много вопросов касательно, того, зачем нужны все эти управляющие конструкции как ASCII, так и ANSI. Теперь настало время рассказать об экране нашего терминала.
Экран
Экран терминала весьма незатейлив. Как Я уже писал, что управлять цветом отдельных пикселей, тут нельзя. Экран поделен на знакоместа сеткой строк и столбцов. Количество столбцов у терминала VT100 80 или 132 в зависимости от настроек. Для указания того знакоместа, где будет напечатан следующий символ существует курсор
, который может перемещаться по знакоместам. Как Вы уже могли догадаться, львиная доля управляющих конструкций предназначена для передвижения курсора по экрану.
Документация к обработке получаемых терминалом символов.
В начальном сотоянии курсор находится в позиции (0,0) - в левом верхнем углу.
Получая обычный(печатаемый) ASCII символ от компьютера, терминал печатает его на месте курсора и автоматимчески передвигает курсор на следующее знакоместо - было бы странно отправлять 3-х байтную ANSI последовательность перемпщения курсора после каждого символа.
При получении управляющей последовательности ANSI, терминал выполняет соответсвующую функцию. Здесь можно посмотреть на них. Большая часть из них отвечает за перемещение курсора и удаление содержимого экрана, но таже есть, например, последовательности с помощью которых можно задавать цвет выводимиого текста.
В предыдущем разделе Я много писал про кнопки Return
/Enter
и Line feed
.
Кнопка Return
отправляет \r
(CR), а Line feed
- \n
(LF). \r
- даёт команду терминалу вернуть курсор в начало текущей
строки, а \n
- сдвигает курсор на следующую строку, не меняя столбец. То есть для полноценного перевода строки терминалу нужно передать сразу обе этих символа CR и LF. Как Я писал выше, драйвер терминала в Unix-подобных ОС проводит обработку этих символов как при чтении из терминала, так и при записи в него. Это позволяет пользоваться лишь одной клавишей Enter
и записывать в терминал только \n
для перевода строки. Стоит заметить, что в других системах, например, Windows, требуется выводить сразу \r\n
.
Эмуляторы Терминалов
В 80-х годах с развитием графических интерфейсов, терминалы были заменены на мониторы и клавиатуры общего назаначения, существующие и по сей день. К тому моменту уже существовало множество программ, нацеленных на работу именно с терминалом. Среди них крайне важные: текстовые редакторы(vi), командные оболочки(shell). Даже внутри Unix-подобных ОС появился ряд функций, заточенных под работу с терминалом. Да и вообще: сам способ терминального ввода/вывода отличался широкими возможностями при относительной простоте взаимодействия. Поэтому в таких системах терминалы начали реализовывать программно. Такие программы называюся эмуляторами терминалов или вртуальным терминалом в противовес обычным(физическим) терминалам. Связка клавиатура, монитора и эмулятора терминала по своим возможностям соответствует физическому терминалу.
Обычные терминалы помимо наличия монитора и клавиатуры обладают особой функциональностью, которую Я описывал ранее. Теперь же у нас есть только клавиатура, умеющая передавать коды символов, почти так же как и клвиатура терминала, но монитор, стал совсем “тупым” - он умеет только отрисовывать пиксели. Эмуляторы терминалов компенсируют эту утраченную функциональность.
На схеме выше изображена схема подсистемы tty
встроенной в ядро ОС Linux. Как можно заметить, Terminal emulator принимает ввод с клавиатуры и выводит изображение на экран с помощью соответсвубщих драйверов. Обмен данными с драйвером терминала теперь реализуется в ядре ОС, без участия линии UART.
В ОС Linux вышеописаная система существует и по сей день. Называется она “Linux console” или просто - консолью. В самом начале Я упоминал, в каких случаях используется название “консоль” вместо “терминал”. Здесь как раз похожий случай, так как данная система - это единственный способ связи пользователя с ОС встроенный непосредственно в неё. Вы можете преклчиться на один из таких терминалов(их несколько) с помощью сочетания клвавиш Ctrl-Alt-F[1-6]. Однако сегодня мы в большинсттве случаев используеи графический интерфейс рабочего стола при работе с ОС. Как же нам получить доступ к эмулятору терминала оттуда, не прибегая к переключению на системную консоль Linux?
Программу эмулятор терминала можно создать в качестве пользовательского процесса, наравне с другими графическими приложениями, вроде браузера или текстового редактора. Пример такой программы на схеме выше - xterm. Напомню, что эмулятор терминала, будь то пользовательский или системный, выполняет функции физического терминала, но для полноценной работы он должен взаимодействовать с драйвером терминала. У системного терминала(консоли) с этим проблем нет, но пользовательское приложение может быть только конечной точкой взаимодействия(см. схемы) и не может само выполнять роль эмулятора терминала. Для решения этой проблемы был придуман механизм псевдотерминалов
. Он позвволяет пользовательскому процессу получить доступ к драйверу терминала(дисциплине линии)
с обеих сторон взаимодействия(см. схему ниже)
Драйвер терминала или дисциплина линии
Драйвер терминала является связующим звеном между терминалом и программой. Он выполняет обработку входных и выходных данных. Я буду опираться на книгу М. Керриска, рекомендую заглянуть в главу о терминалах. Драйвер можно настраивать с помощью специальной структуры termios
:
struct termios
{
tcflag_t c_iflag; /* input mode flags */
tcflag_t c_oflag; /* output mode flags */
tcflag_t c_cflag; /* control mode flags */
tcflag_t c_lflag; /* local mode flags */
cc_t c_line; /* line discipline */
cc_t c_cc[NCCS]; /* control characters */
speed_t c_ispeed; /* input speed */
speed_t c_ospeed; /* output speed */
};
При запуске процесса из командной строки, эта структура(т.е настройки драйвера терминала) будут иметь дефолтное значние. При изменеии настроек драйвера вызовом tcsetattr
, все процессы, использующие этот терминал, увидят изменения этих настроек.
Наибольшый интерес представляют поля: c_iflag
- содержит функции обработки для входного потока символов, c_oflag
- для выходного потока символов, c_lflag
- для режима работы терминала, c_cc
- содержит значения специальных символов терминала(об этом далее).
Драйвер терминала определяет набор специальных “символов”, принимая которые, он будет выполнять определенные действия. Кавычки Я добавил потому, что эти “символы” имеют определенное название, но их конкретное значение можно задать с помощью массива c_cc
, кроме CR
и NL
- их значения фиксированы(\r
и \n
). Такой механизм позволяет гибче настраивать драйвер. Большая часть специальных символов имеют дефолтные значения.
В c_lflag
содержатся такие важные функции как:
-
ECHO
- в включенном состоянии, автоматически отправяет терминалу, символы введенные пользователем. Непечатаемые символы отображаются в виде^{A-^}
. Эта функция включена по умолчанию. Если её выключить, то программе нужно будет самой отправлять терминалу нужные для печати символы. -
ICANON
- включает т.н канонический режим работы драйвера. Включен по умолчанию. У драйвера терминала есть специальный буфер для входных данных. В этом режиме драйвер не даёт доступ пользовательскому к данным во входном буфере сразу, а только после ввода специального символа перевода строкиNL
или специального символаEOF
. Пока не введены эти сиволы, введенную строку можно редактировать: стирать символ, слово или всю строку, но перемещение курсора по строке не доступно. Такой подход позволяет многим пользовательским программам не реализовывать редактирование строки, а просто воспользоваться данным режимом. Для более продвинутого редактирования строки, такой режим нужно отключить и реализоваит весь функционал непосредственно в программе. Стоит отметить, что символNL
добавляется в буфер, аEOF
- нет. -
ISIG
- включает возможность отправлять сигналы процессам, испоьзующим данный терминал. Например, при получении символа сINTR
, имеющем дефолтное значене Ctrl-C(^C
), отправляется сигналSIGINT
, а при получении символаSUSP
-SIGTSTP
.
В c_iflag
содержатся такой важный флаг, как ICRNL
(включен по умолчанию). При активном флаге, драйвер терминала заменяет символ CR
(\r
) на NL
(\n
). Аналогичный флаг ONLCR
содержится в c_oflag
. Если включён, то драйвер заменят NL
на CRNL
при выводе.
Еще немного про флаг ICANON
…