Следующая задача, которая позволит продолжить тему сигналов и, как оказалось, вызывает определенные трудности - работа с терминалом. Точнее, мы поговорим о работе в терминальной строке и настройке режимов работы. В этом плане достаточно безразлично, идет ли речь о терминалах или псевдотерминалах и, например, о соединении по telnet. (Упомянутые в статье файлы взяты из каталогов /usr/src/linux/drivers/char и /usr/src/linux/ include/linux из дистрибутива Linux RedHat 6.0 Linux Cyrillic Edition).

«Жму DELETE - курсор ни с места»

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

Предположим, что в вашей программе на Си содержится запрос данных с терминала (стандартный ввод) в виде scanf(?%i?, &num). Все, что мы собираемся сделать - это получить число с экрана. Однако наше благородное начинание может быть в корне испорчено пользователем - он может начать вводить всякий мусор, далее очнется и, удалив часть строки, наберет таки требуемое число. Возникает вопрос о том, кто должен отрабатывать, например, клавишу удаления последнего символа - система или программа? Обычно этим занимается система - это так называемый канонический режим ввода, т.е. программа, которая выполняет команду read для терминала, получает полностью сформированную строку только после окончания ее ввода (например, нажатия нами Enter), но ничего не знает о выполненном процессе редактирования строки. «Неканонический» режим ввода предполагает, что программы должна получать все символы с экрана и сама занимается вопросами связанными с редактированием строки, отображением действий на экране и т.п. Этот режим нужен для редакторов и программ, которые хотят предложить пользователю более приятный интерфейс, чем редактирование строки одним delete.

Режим работы терминала можно проверить или переустановить либо специальным вызовом ioctl, либо командой stty. Попробуем посмотреть, например, в каком режиме работает bash. Запускаем stty -a и видим среди установленных характеристик icanon. Значит, bash использует канонический ввод и любая другая программа, которая использует канонический ввод, тоже позволяет в стиле bash при помощи стрелочек редактировать вводимую строку? Отнюдь! В данном случае команда stty выдает не режим используемый самим командным интерпретатором, а тот, который он устанавливает для вновь запускаемых программ. Как это можно доказать? Достаточно просто. Попробуем выдать команду проверки не из этой копии bash, а с запущенной на другом терминале. Для этого в Linux нажимаем ALT-F2 и выполняем команду:

stty -a 

Обратите внимание, что и для проверки, и для выполнения установок параметров терминал открывается на чтение. Это важно с точки зрения безопасности. Обычно после входа терминал «принадлежит» сидящему за ним пользователю, но и другие могут «бросать» (если не запретить) на него сообщения, но не могут читать (и соответственно переставлять характеристики) с него. Аналогичны требования и у некоторых других программ, например, loadkeys.

Что же показала проверка? Проверка показывает, что bash для своей работы использует неканонический режим ввода (-icanon) и соответственно сам отрабатывает каждый введенный символ опираясь на описание терминала. Из этого следует печальный вывод: запущенная программа не обязана поддерживать тот же уровень редактирования, что и bash. Особенно разителен этот контраст в том случае, когда из-за неправильных установок мы вообще теряем возможность редактировать строку. Однако в нашем случае stty выдал, например, что в качестве символа erase (т.е. символа удаления последнего символа) можно использовать ^ - т.е. ^-? (его код 0177 или 0x7f - именно этот код обычно поcылают «нормальные» терминалы при нажатии DELETE). Проверяем - работает. Этот же код работает и при вводе имени и пароля. Более удачно, конечно, использовать что-либо другое. Например, мы можем изменить настройку командой stty erase CTRL-H и далее использовать клавишу возврата позиции. Аналогичного эффекта можно добиться другим способом: набрав stty erase и, не нажимая ENTER, последовательно нажать комбинацию CTRL-V (а это, заметим, значение, которое stty выдал для lnext, впрочем, если ваш unix и не знает про lnext CTRL-V почему-то работает) и клавишу возврата на одну позицию (ту что со стрелкой), ну а далее, конечно, ENTER.

Иногда забывают об этой возможности при наборе команд. Например, если нам хотелось бы найти символ табуляции в файле, то мы обнаружим, что grep ? ? ... (и аналогичные) ищет не табуляцию, а символ t. Как быть? Ответ ясен - набрать в кавычках комбинацию CTRL-V TAB.

Попробуем аналогичным образом в Linux определить в качестве erase символа DELETE (в большинстве unix-ов это можно сделать командой stty erase DEL). Нажимаем, требуемую комбинацию, но нас посылают куда подальше, да и сама команда на экране выглядит как-то странно. Где грабли? Поищем корешок.

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

Кнопка -> последовательность сканкодов -> код клавиши -> набор ASCII символов -> ...

... -> отредактированный набор символов -> буфер нашей программы

Ясно, что сначала в систему поступает поток сканкодов (любители сканкодов могут наблюдать их при помощи команды ?showkey -s?, а командой kbd_mode установить такой ввод для последующих программ, однако, это может сделать клавиатуру «unusable»). Первым на страже стоят процедуры из файла keyboard.c - именно там расположена процедура обработки прерывания (C-часть). Как мы видим, после получения сканкодов, желательно точно определить конкретную клавишу (если, конечно, программа не работает непосредственно с сканкодами). В этом процессе процедуре handle_scancode из keyboard.c помогают процедуры pc_kbd.c. Итак мы точно знаем, что было нажато (или отжато, заметим, например, что некоторые функциональные клавиши работают именно по отпусканию). Далее keyboard обращается к таблице key_maps и узнает две характеристики: тип клавиши и обрабатываемый код клавиши. Что-же это за таблица key_maps? Таблица key_maps задает генерируемый код и способ обработки клавиш (тип) в зависимости от используемых в текущий момент модификаторов, т.е. CTRL/Shift/ALT и т.п.. Например, именно здесь указывается, что комбинации ALT-F1 ... ALT-F6 используются для переключения консолей. Начальная таблица задана в файле defkeymap.map, который при помощи loadkeys был преобразован в defkeymap.c. В этом же defkeymap можно обнаружить и escape-последовательности, которые генерируются при нажатии на функциональные клавиши. Таким образом, если нам не нравиться раскладка клавиатуры и мы хотим иметь что-то свое, да еще и переключаться между раскладками, то нам необходима возможность подмены ее на ходу. Для этого (например, руссификация того или иного типа) мы тоже можем использовать программу loadkeys.

На выходе keyboard мы получаем либо сканкоды, либо специальные кейкоды (уникальные коды клавиш), ASCII или UTF-8. По умолчанию - ASCII. Кроме того, keyboard в некоторой степени отвечает за мышку.

Что далее? А вот далее-то и появляются процедуры, которые разбираются с параметрами stty - их можно найти в файлах tty_io.c, tty_ioctl.c и, самое любопытное, n_tty.c (эти файлы образуют «верхний» уровень работы с терминалами). Среди параметров, которые вы можете найти у stty есть line, чаще всего этот параметр равен нулю. Параметр line определяет используемую line

discipline - способ организации работы с терминалом. Таких дисциплин в системе может быть несколько, они могут отвечать за поддержку специальных протоколов, переключения между консолями или просто за специфическое поведение некоторого типа терминала. В последнем случае в системе имеются специальные символьные имена для дисциплин и их можно явно указывать, например, в команде подобной getty /dev/ttycons console myterm в /etc/ inittab. Заметим, что допускается динамическая подмена дисциплины.

Если можно посмотреть текст n_tty.c, то можно обнаружить, что именно там и обрабатываются символы заданные командой stty (intr, quit, erase, lnext, eof и т.п.) - заметим, символы, а не комбинации символов, т.е. если клавиша, благодаря установленной таблице преобразования, генерирует последовательность кодов, то использовать ее нельзя. Есть здесь еще одни грабельки. X-программы (о X-Window мы еще поговорим особо) могут использовать собственные таблицы преобразования символов, которые не совпадают с терминальным режимом. В результате, например, в xterm вы обнаруживаете, что даже CTRL-? не работает, хотя именно он и заявлен в stty - оказывается, что CTRL-? в действительности генерирует CTRL-_ комбинацию. В любом случае работает CTRL-8 (да и DELETE в X заработал). Уж лучше жить с CTRL-H- соответствующую команду вы можете записать в profile. Кое-что добавляемое X-Window мы расмотрим позже.

Итак, минимальные требования для того, чтобы у нас был корректный ответ на клавишу выбранную для той или иной функции stty: клавиша (в «окончательном» варианте) порождает один символ, этот символ задан stty, установлен канонический режим ввода и тип дисциплины соответствующий терминалу. Что будет, если мы установим неканонический режим ввода? Bash ничего не заметит, менее «разумный» shell не сможет корректно работать и, например, удалять последний символ или заканчивать работу по CTRL-D (нужно набирать команду exit). Правда, CTRL-C отработает.

Мы поговорили о вводе. Но, заметим, все вводимые нами символы должны где-то отображаться. Об этом-то кто волнуется? За отображение вводимых в каноническом режиме символов отвечает в некоторой степени тоже дисциплина - она пересылает вводимые символы обратно на терминал. Если надо, то заменяет символ на группу, например, удаление последнего символа выполняется последовательностью: « » - «возврат на позицию пробел возврат». В комментарии к n_tty.c можно найти, что этот файл содержит «ортогональные» процедуры - ему наплевать, с кем работать. Эти процедуры работают и с обычными терминалами и с консолью. Кто выполняет комбинации подобные « » на консоли? За это отвечает console.c - эмулятор консоли. Именно в этом файле расположены процедуры, которые обрабатывают (а иногда и генерируют, например, в ответ на запрос текущего положения курсора) разнообразные управляющие коды. Кое-что перепадает и vt.c (управление загрузкой шрифтов, звоночек и т.п.).

Еще одно полезное замечание. Как правило, в системе имеется стандартный режим работы для терминалов. В этот режим устройство автоматически вваливается после того, как нет процессов использующих его. Поэтому, например, команды stty 1200 не обеспечивают работу myprog в режиме 1200. Правильная комбинация (без освобождения канала) следующая: (stty 1200; myprog) . Для экспериментов: (stty 1200; sleep 30000) Во втором случае можно без проблем вызывать myprog много раз, т.к. канал «удерживает» sleep.

Терминал - однолюб

Вернемся к сигналам. Некоторая часть сигналов сразу указывается в stty и уже поэтому не очень интересна. С SIGINT и SIGQUIT все более или менее понятно (они тоже генерируются в n_tty.c). Там же вы можете найти SIGIO (сигнал асинхронного ввода). Более интересны другие. Попробуем провести еще один эксперимент. Например, попытаемся запустить из bash копию его в параллель: bash &. Мы видим, что запущенный bash будет немедленно остановлен. Полученная диагностика сообщает, что он остановлен по сигналу SIGTTIN (это сделано стараниями n_tty.c). Аналогично, предположим, что мы работали с mc и из командной строки (CTRL-O) набрали cat .... &. Из полученной диагностики мы узнаем, что процесс остановлен по сигналу SIGTTOU (а это уже tty_io.c). Данный механизм обеспечивает нам защиту от таких неприятностей, как необходимость отвечать на два запроса одновременно. Как он работает? Попробуем посмотреть значения pgrp и sid для запущенных процессов:

ps -ejf (стиль System V) 
	или 
ps j (BSD)

Мы обнаружим, что любой интерактивный процесс будет работать в рамках новой группы процессов (новое задание - job - поэтому и ps j) с тем же sid - в том же сеансе. Соответственно, например, sleep 10; cat последовательно использует две (плюс 1 на shell), а (sleep 10; cat) - одну группу процессов. Если процесс одной из групп уже использует данный терминал (bash), то процесс другой группы будет заблокирован. Остановленные таким образом задания можно перезапустить при помощи команды fg (bg немедленно приведет к повторной диагностике). Для остановленных по CTRL-Z (SIGTSTP - n_tty.c) в принципе возможно использование и bg, и fg. Заметим, что для того, чтобы это работало и на экране, в частности, появилась диагностика, папаша (bash) тоже должен быть специально оповещен о событиях подобного рода (см. описание системных вызовов waitpid/wait4) и, например, в случае SIGTSTP проснутся.

Если в вашей системе этот механизм защиты не работает (или вам удалось его отключить), то возможна ситуация, когда два и более процесса используют один терминал для ввода. Если они используют канонический ввод, то все происходит относитьно пристойно - вы просто видите последовательные запросы от них. А если среди них, например, редактор и обычный shell? Тогда жизнь становится намного забавнее. Может возникнуть необходимость ввода заклинания типа eexxiitt и пара ENTER с надеждой, что половина символов достанется shell и он отвалиться - страшно, однако, да и получается не с первого раза.

Присылайте свои вопросы и пожелания по адресу: oblakov@bigfoot.com