11. Символьный ввод/вывод
Введение
Ключевым
требованием для любой операционной системы реального времени
является наличие высокопроизводительной системы ввода/вывода
символьных данных. В устройствах символьного ввода/вывода, в
отличие от блок-ориентированных устройств (например, дисков),
операции ввода/вывода применяются для серий последовательно
передаваемых байтов.
В соответствии
со спецификациями POSIX и UNIX, устройства символьного
ввода/вывода помещаются в пространстве путевых имен в каталоге
/dev. Например, последовательный
порт, к которому можно присоединить модем или терминал, может
иметь следующее путевое имя:
/dev/ser1
Как правило,
устройства символьного ввода/вывода встречаются следующих видов:
-
устройства последовательных портов;
-
устройства параллельных портов;
-
консоли с выводом в текстовом режиме;
-
псевдотерминалы (ptys).
Программы могут обращаться к устройствам символьного
ввода/вывода посредством стандартных API-функций open(), close(), read() и write(). Для управления другими параметрами устройств
символьного ввода/вывода (например, скорость двоичной
передачи, четность, сигналы управления потоками данных
и т. д.) предусмотрены дополнительные функции.
Поскольку в
системе часто имеется несколько устройств символьного
ввода/вывода, в ОС QNX Neutrino предусмотрено семейство
драйверов в виде библиотеки под названием io-char, которая позволяет повысить уровень
повторного использования кода.
Рис. 11.1. Модуль io-char реализован в виде библиотеки
Как видно на рис. 11.1, модуль io-char реализован в виде библиотеки. Этот модуль
содержит весь код, необходимый для поддержки POSIX-семантики
на данном устройстве. Он также содержит значительный объем
кода для реализации тех возможностей символьного ввода/вывода,
которые не предусмотрены спецификацией POSIX, но требуются для
той или иной системы реального времени. Так как данный
программный код находится в общей библиотеке, все драйверы
наследуют эти возможности.
Драйвер
представляет собой процесс, который посылает вызовы
библиотеке. При работе устройства сначала запускается драйвер,
который затем вызывает модуль io-char.
В ОС QNX Neutrino драйверы сами по себе являются обычными
процессами, которые могут выполняться с разными приоритетами в
зависимости от характеристик устройства и запросов клиента.
После запуска
первого устройства символьного ввода/вывода присоединение
других устройств требует минимальных расходов памяти, так как
для этого понадобится только код, реализующий новую структуру
драйверов.
Взаимодействие
между драйверами и модулем io-char
Библиотека io-char управляет потоком данных между
приложением и драйвером устройства. Обмен данными между
модулем io-char и драйвером происходит в памяти
с помощью очередей, связанных с каждым соответствующим
устройством (рис. 11.2).
Для каждого
устройства используется три очереди. Каждая очередь
реализуется с помощью FIFO-механизма.
Рис. 11.2. Ввод/вывод данных на устройства в ОС QNX
Neutrino
Полученные данные драйвер помещает в очередь "сырых" входных данных (raw input
queue), и затем они используются модулем io-char, только когда приложение выполняет
обработку запрошенных данных. (Более подробно о "сырых", а
также редактируемых (или канонических) входных данных, см. в разделе "Режимы ввода".)
Обработчики
прерываний в драйверах, как правило, вызывают соответствующую
библиотечную подпрограмму из модуля io-char для того, чтобы добавить данные в очередь
ввода. Это обеспечивает согласованную процедуру ввода и
минимизирует функциональность, возложенную на драйвер (а также
усилия разработчика при создании новых драйверов).
Модуль io-char помещает выходные
данные в очередь вывода, которая обрабатывается драйвером по
мере того, как символы физически передаются на устройство. Он
вызывает безопасную процедуру внутри
драйвера всякий раз, когда поступают новые данные, и
инициирует работу драйвера (в случае если он был неактивен).
Так как выходные данные организуются в очереди, модуль io-char применяет кеширование при
записи (write-behind) для всех устройств символьного
ввода/вывода. Только в случае переполнения буфера выходных
данных модуль io-char может остановить
процесс во время записи.
Каноническая
очередь (canonical queue) используется при обработке входных данных в редактируемом режиме (edited
mode) и
полностью управляется модулем io-char.
Размер этой очереди определяет максимальную длину строки
редактируемых входных данных, которая может быть обработана
при работе с данным устройством.
Размер этих
очередей может быть установлен с помощью ключей командной
строки. Значения, принятые по умолчанию, как правило, вполне
достаточны для большинства аппаратных конфигураций. Тем не
менее, можно применять собственные "настройки" для того, чтобы
снижать общие системные требования на память, учитывать
необычные аппаратные ситуации или применять особые требования
протокола.
Драйверы
устройств просто добавляют полученные данные в очередь "сырых"
входных данных или принимают данные из очереди выходных данных
и передают их на устройство. Модуль io-char решает, когда приостановить передачу
выходных данных (если это необходимо), когда и как
осуществлять эхо-возврат данных и т. д.
Управление
устройствами
Низкоуровневое
управление устройствами осуществляется с помощью вызова devctl(). В стандарте POSIX реализованы
следующие функции управления терминалами на основе вызова devctl():
-
tcgetattr() — получить атрибуты терминала;
-
tcsetattr() — установить атрибуты
терминала;
-
tcgetpgrp() — получить
идентификатор лидера группы процессов для терминала;
-
tcsetpgrp() — установить идентификатор
лидера группы процессов для терминала;
-
tcsendbreak() — отправить команду
остановки передачи данных (break);
-
tcflow() — приостановить или
перезапустить передачу/прием данных.
QNX-расширения
ОС QNX Neutrino
имеет следующие расширения программного интерфейса для
управления терминалами:
Модуль io-char работает непосредственно с типовым набором
команд devctl(), которые поддерживаются
большинством устройств. Команды набора devctl(), специфичные для конкретного устройства,
приложения отправляют драйверам с помощью модуля io-char.
Режимы ввода
Каждое
устройство может работать либо в режиме "сырых" (raw) входных данных, либо в режиме редактируемых (edited) входных данных.
Режим "сырых"
входных данных
В режиме
"сырых" (raw) входных данных модуль io-char не применяет редактирования к принимаемым
данным. Это уменьшает обработку каждого символа до минимума и
дает наивысшую производительность при чтении данных.
Полноэкранные
программы и программы обмена данными по последовательному
порту представляют собой примеры приложений, в которых
используются устройства в режиме "сырых" входных данных.
В этом режиме
каждый символ принимается в буфер "сырых" входных данных с
помощью обработчика прерываний. Когда приложение запрашивает
данные с устройства, оно может указать, при каких условиях
данный запрос должен завершиться. До тех пор пока не возникнут
заданные условия, обработчик прерываний не запустит драйвер, а
драйвер, соответственно, не возвратит данные приложению.
Простая операция чтения, выполняемая приложением в типовом
случае, будет блокирована, пока не будет получен хотя бы один
символ.
На
рис. 11.3 показан весь набор условий чтения данных.

MIN – Ответить, если принято не менее данного
количества символов
TIME – Ответить в случае паузы в потоке символов
TIMEOUT – Ответить по истечении заданного периода
времени
FORWARD – Ответить при получении кадрирующего символа
Рис. 11.3.
Условия выполнения запросов на ввод данных
В случае если задано множество условий чтения данных,
запрос на чтение будет выполнен при удовлетворении любого из
них.
MIN
Условие MIN полезно в тех случаях, когда
приложение знает, какое именно количество символов оно должно
принять.
Условие MIN может применяться для ожидания
всего кадра целиком в любом протоколе, в котором известно
количество символов в кадре данных. Это значительно уменьшает
межзадачный обмен сообщениями и упрощает планирование
процессов. Условие MIN часто используется вместе с
условием TIME или TIMEOUT. Оно входит в спецификацию POSIX.
TIME
Условие TIME полезно в тех случаях, когда
приложение принимает поток данных и при этом необходимо
следить за возникновением пауз или остановок в потоке. Период
паузы задается в десятых долях секунды. Условие TIME входит в спецификацию POSIX.
TIMEOUT
Условие TIMEOUT полезно в тех случаях, когда
приложение знает, в течение какого времени оно должно ожидать
получения данных. Период ожидания задается в десятых долях
секунды.
Условие TIMEOUT может применяться в любом
протоколе, в котором известно количество символов в кадре
данных. В сочетании со значением скорости передачи это условие
позволяет надежно определять время ожидания данных. Условие
служит в качестве таймера безопасности для выявления случаев
пропуска символов. Оно также может использоваться в
интерактивных программах с целью создания паузы для ожидания
пользовательских данных.
Условие TIMEOUT является расширением, принятым
в QNX, и не входит в спецификацию POSIX.
FORWARD
Условие FORWARD полезно в тех случаях, когда в
протоколе используются специальные кадрирующие символы.
Например, в протоколе PPP, применяемом для реализации TCP/IP
на последовательных соединениях, пакеты данных начинаются и
заканчиваются кадрирующим символом. В сочетании с условием TIMEOUT, условие FORWARD позволяет значительно повысить
производительность протокола. Процесс, реализующий протокол,
принимает данные не отдельными символами, а целыми кадрами. В
случае пропуска кадрирующего символа условие TIMEOUT или TIME позволяет быстро продолжить работу.
Это значительно
сокращает межзадачный обмен сообщениями и в результате
позволяет сократить нагрузку на процессор для данной скорости
передачи данных по протоколу TCP/IP. Интересно отметить, что в
протоколе PPP не предусмотрен подсчет символов в кадре.
Отсутствие символа перенаправления данных могло бы привести к
необходимости чтения данных посимвольно.
Условие FORWARD является расширением, принятым
в QNX, и не входит в спецификацию POSIX.
Возможность
перенести функциональность, отвечающую за уведомление
приложений, в сервисные компоненты ОС, сокращает частоту, с
которой должна происходить обработка на пользовательском
уровне. Это уменьшает работу механизма межзадачного
взаимодействия и освобождает процессор для работы с
приложением. Кроме того, если приложение, реализующее
протокол, выполняется на одном сетевом узле, а
коммуникационный порт находится на другом, это позволяет
минимизировать количество сетевых транзакций.
Механизм
определения символа перенаправления данных (data-forwarding)
также может быть реализован внутри интеллектуальных
многопортовых последовательных адаптеров, что позволяет
значительно сократить частоту, с которой адаптер должен
прерывать главный процессор для обработки прерываний.
Режим
редактируемых входных данных
В редактируемом
режиме модуль io-char выполняет редактирование строки
при получении каждого символа. Строка символов передается
приложению только после того, как она "полностью принята" (как
правило, после получения символа возврата каретки). Этот режим
называют каноническим
(canonical),
или иногда "режим с обработкой" (cooked mode).
Большинство
неполноэкранных приложений работают в редактируемом режиме,
так как в этом случае они могут обрабатывать данные построчно,
а не посимвольно (в последнем случае приложению приходится
определять символы конца строки).
В редактируемом
режиме каждый символ принимается в буфер "сырых" входных
данных обработчиком прерываний. В отличие от режима "сырых"
входных данных, в котором драйвер запускается только при
заданных условиях, в редактируемом режиме обработчик
прерываний запускает драйвер при каждом полученном символе.
Это обусловлено
двумя причинами. Во-первых, режим редактируемых входных данных
редко используется для высокопроизводительных коммуникационных
протоколов. Во-вторых, редактирование требует большой работы и
поэтому не подходит для обработчика прерываний.
Во время работы
драйвера модуль io-char проверяет символ и помещает его
в канонический буфер, в котором происходит сборка строки.
После завершения формирования строки и запроса приложения на
чтение, строка передается из канонического буфера в приложение
(передача строки производится напрямую из канонического буфера
в буфер приложения без промежуточного копирования).
Редактируемый
режим позволяет выполнять корректную обработку нескольких
строк входных данных, находящихся в каноническом буфере, а
также чтение неполных строк (например, если приложение
запросило только один символ из 10-символьной строки). В этом
случае, следующая операция чтения выполняется там, где
закончилась предыдущая.
Модуль io-char имеет широкий набор функций редактирования,
в том числе полную поддержку перемещения по строке, а также
изменение, вставку и удаление отдельных символов. Приведем
некоторые из основных функций:
-
LEFT — переместить курсор на один
символ влево;
-
RIGHT — переместить курсор на
один символ вправо;
-
HOME — переместить курсор в
начало строки;
-
END — переместить курсор в конец
строки;
-
ERASE — удалить символ слева от
курсора;
-
DEL — удалить символ на текущей
позиции курсора;
-
KILL — удалить всю строку ввода;
-
UP — удалить текущую строку и
восстановить предыдущую строку;
-
DOWN — удалить текущую строку и
восстановить следующую строку;
-
INS — переключение между режимом
вставки и замещения. (Каждая новая строка начинается в
режиме вставки.)
Символы редактирования строки могут быть разными для
разных терминалов. Консоль всегда запускается с полным набором
кодов редактирования.
Если терминал
присоединен к последовательному порту, то необходимо
использовать символы редактирования, которые применяются для
данного терминала. Для этого можно использовать утилиты stty. Например, если к последовательному порту
присоединен ANSI-терминал (с именем /dev/ser1), то необходимо использовать
следующую инструкцию, чтобы извлечь соответствующие коды
редактирования из базы данных terminfo
и применить их к /dev/ser1:
stty term=ansi
</dev/ser1
Производительность
устройств
Поток событий
внутри устройства организуется таким образом, чтобы
минимизировать накладные расходы и довести до максимума
производительность устройства при работе в режиме "сырых" входных данных. Для этого
применяются специальные правила
-
Обработчики прерываний помещают
полученные данные непосредственно в очередь в памяти.
Обработчик прерываний может запланировать запуск драйвера
только в том случае, когда поступает запрос операции
чтения и этот запрос может быть
выполнен. Во всех других случаях прерывание просто
возвращается. Более того, если модуль io-char уже запущен, операция
планирования не применяется, поскольку наличие данных
будет обнаружено без дополнительного извещения.
-
При выполнении запроса на чтение драйвер
отвечает приложению непосредственно из буфера "сырых" входных данных в приемный
буфер приложения. Таким образом, данные копируются только
один раз.
В сочетании с чрезвычайно небольшой задержкой
обработки прерывания и задержкой планирования, которые
свойственны ОС QNX Neutrino, эти правила дают очень компактную
модель ввода, которая соответствует спецификации POSIX и имеет
расширения, удовлетворяющие требованиям реального времени в
различных протоколах.
Системные консоли (с VGA-совместимым графическим
процессором, работающим в текстовом режиме) управляются с помощью драйвера devc-con или devc-con-hid. Видеоплата и дисплей, а также клавиатура
вместе называются физической
консолью.
Драйвер devc-con позволяет одновременно запускать
несколько виртуальных
консолей (virtual console) на одной физической консоли. Как правило,
драйвер devc-con управляет несколькими наборами
очередей ввода/вывода, предназначенными для модуля io-char. Для пользовательских процессов эти очереди
отображаются в виде группы устройств символьного ввода/вывода
с именами /dev/con1, /dev/con2 и т. д. C точки зрения приложения, они
представляют собой несколько "реальных" консолей.
В
действительности, конечно, имеется только одна физическая консоль (дисплей и клавиатура), поэтому
только одна из этих виртуальных консолей
отображается в каждый момент времени. Клавиатура просто
"присоединяется" к той виртуальной консоли, которая видна в
данный момент.
Эмуляция терминала
Консольные драйвера эмулируют ANSI-терминал.
Устройства
последовательного порта
Последовательные
коммуникационные каналы управляются с помощью семейства
драйверов devc-ser*. Эти драйверы могут управлять
несколькими физическими каналами и регистрируют имена для
устройств символьного ввода/вывода (/dev/ser1, /dev/ser2
и т. д.).
При запуске
драйвера devc-ser* аргументы командной строки
позволяют задать, какие последовательные порты должны быть
установлены и в каком количестве. В PC-совместимой системе,
как правило, это могут быть два стандартных последовательных
порта, которые часто обозначаются как com1 и com2.
Драйвер devc-ser*
напрямую поддерживает
большинство неинтеллектуальных многопортовых последовательных
плат.
В ОС QNX
Neutrino имеется несколько драйверов последовательного порта
(например, devc-ser8250, devc-serppc800 и т. д.). Более подробные сведения о
драйверах devc-ser* содержатся в руководстве "Справочник по утилитам".
Драйверы devc-ser* поддерживают функции
аппаратного управления обменом данных (кроме редактируемого
режима) при наличии этих возможностей в самом устройстве.
Например, драйверу можно указать, что при потере несущей
частоты, генерируемой модемом, процессу, реализующему
приложение, следует передать сигнал SIGHUP (в соответствии со
стандартом POSIX).
Устройства параллельного
порта
Параллельные
(принтерные) порты управляются с помощью драйвера devc-par. При запуске драйвера devc-par аргументы командной строки позволяют
задать, какой параллельный порт должен быть установлен.
Данный драйвер
управляет только выводом данных, поэтому в нем нет очередей
"сырых" или канонических входных данных. Размер выходного
буфера может быть задан с помощью аргумента командной строки.
При задании большого размера выходного буфера возникает эффект
программного буфера печати.
Псевдотерминальные
устройства (pty)
Псевдотерминалы
управляются с помощью драйвера devc-pty
(рис. 11.4). Аргументы командной строки позволяют задать
для драйвера devc-pty количество создаваемых
псевдотерминалов.
Псевдотерминал
(pty) представляет собой пару
устройств символьного ввода/вывода: ведущее (master) и ведомое
(slave) устройства. Ведомое устройство обеспечивает интерфейс,
аналогичный интерфейсу tty-устройств в стандарте POSIX. Однако,
в то время как обычные tty-устройства являются физическим
оборудованием, pty-устройству сопоставлен еще один процесс,
который управляет им посредством ведущей части псевдотерминала.
Таким образом, любые данные, записываемые на ведущее устройство,
передаются на ведомое как входные данные, а любые данные,
записываемые на ведомое устройство, передаются как входные
данные на ведущее устройство. В результате псевдотерминальные
устройства (pseudo-tty) могут использоваться для подключения к
процессам, которые рассчитаны на работу с устройством
символьного ввода/вывода.
Рис. 11.4. Псевдотерминальные устройства (pseudo-tty)
Псевдотерминалы
обычно используются с целью создания псевдотерминальных
интерфейсов для таких программ, как pterm
(терминальный эмулятор, который работает в графической оболочке
Photon microGUI) и telnet (которая использует протокол
TCP/IP для обеспечения терминальной сессии с удаленной
системой).
10
В оригинале
"trusted". Вызов подпрограммы между модулями — небезопасная
и нерекомендуемая практика, но она может дать серьезный выигрыш
по производительности. В данном случае наличие безопасного
механизма межмодульного вызова позволяет соблюдать компромисс
между безопасностью и производительностью. — Прим. научн. ред.