Библиотека сайта rus-linux.net
Назад | Введение в мир программирования Глава 2. Архитектура компьютера |
Вперед |
Ввод текста. Взаимодействие с клавиатурой
Программная обработка скан-кодов и кодов клавиш
Исходный код, отвечающий за обработку скан-кодов и кодов клавиш, входит
в состав многих программ, распространяемых согласно свободной лицензии.
К ним относятся kbd (мы будем рассматривать версию 1.15.3) и
console-tools (версия 0.2.3, например).
Архивы с исходными текстами и kbd
и console-tools
содержат
файл showkey.c
, в котором реализуется механизм вывода скан-кодов и
кодов (keycodes) клавиш, генерируемых когда пользователь нажимает кнопки на
клавиатуре.
И kbd
и console-tools
являются приложениями
с текстовым интерфейсом. В среде GNU/Linux такие программы почти всегда
включают в свой состав стандартную функцию getopt_long()
,
осуществляющую синтаксический анализ аргументов командной строки. Достаточно
подробные пояснения по поводу использования этой функции даны в работе
[МитчеллОулдемСамьюэл2003, С. 31-34].
Применяется ли getopt_long()
в kbd
и console-tools
?
Да. Код в файле src/showkey.c
(из набора программ kbd-1.15.3
)
служит наглядным примером того, как это делается.
Версию пакета kbd
, используемую в вашей системе,
можно узнать, например, выполнив команду showkey --version
.
В ответ будет выведено сообщение, похожее на ``showkey from kbd 1.15''
.
Исходя из того, что одним из основных недостатков современной литературы
о программировании является почти полное отсутствие примеров кода, решающего
реальные, а не учебные задачи, остановимся на src/showkey.c
подробнее.
/*--- Начало файла src/showkey.c с комментариями ---*/
/* Директивы препроцессора, начинающиеся со слова #include
(в переводе с английского --- ``включить в состав'') подключают заголовочные файлы
(header files). Препроцессор --- это один из элементов компилятора языка Си, осуществляющий
предварительную обработку кода программы (с его помощью можно включать в состав программы
внешние файлы, осуществлять макроподстановки и условные включения).
Подробные сведения о препроцессоре Си указаны в отличной книге [КерниганРитчи2006, С. 101-104]. */
/* В системах GNU/Linux заголовочные файлы обычно хранятся в каталоге /usr/include
.
Иными словами, если в командной строке ввести ``less /usr/include/stdio.h''
,
можно будет увидеть содержание заголовочного файла, ответственного за реализацию
ввода/вывода в приложении. В английском языке словам ``ввод'' и ``вывод'' соответствуют
``input'' и ``output''. Таким образом, stdio.h
расшифровывается
как ``Standard Input/Output Header file''. */
#include <stdio.h> #include <unistd.h> #include <getopt.h> #include <fcntl.h> #include <signal.h> #include <termios.h> #include <sys/ioctl.h> #include <linux/kd.h> #include <linux/keyboard.h>
/* Имена заголовочных файлов, находящихся в каталоге /usr/include
(или его аналоге),
заключаются в угловые скобки. В свою очередь, двойные кавычки применяются для
выделения имён заголовочных файлов, расположенных в одном каталоге с кодом
программы, которую предстоит компилировать (переводить с языка высокого уровня
на язык понятный компьютеру, то есть --- в машинный код). */
#include "getfd.h" #include "nls.h" #include "version.h"
/* Имена переменных tmp
, fd
и oldkbmode
говорят сами за себя.
Tmp
- сокращение от слова temporary
(временный);
fd
- сокращение от file descriptor
(дескриптор файла,
являющийся ссылкой на ``объект'' открытого файла; подробнее см.
[Вахалия2003, С. 338-340]);
oldkbmode
(old keyboard mode) служит для хранения названия режима, в котором
находится драйвер клавиатуры (об этих режимах мы ещё поговорим).
Сокращение int
указывает на тип переменной (см. [КерниганРитчи2006, С. 50-54]). */
int tmp; /* for debugging (для отладки программы) */ int fd; int oldkbmode;
/* Структура termios
используется функциями управления терминалом
в Unix-подобных системах. Подробнее см. [Рочкинд2005, С. 226-228].
Структура
--- это совокупность нескольких переменных, часто различных типов,
сгруппированных под единым именем для удобства обращения. [КернигаРитчи2006, с. 139] */
struct termios old; /* Данная строка создаёт копию (old) текущей структуры termios */ /* version 0.81 of showkey would restore kbmode unconditially to XLATE, thus making the console unusable when it was called under X. Здесь авторы обращают внимание на сбои в обработке сигналов с клавиатуры, которые могут появиться если запустить программу showkey версии 0.81 в среде X Window. */
/* Область действия функции get_mode()
ограничивается данным файлом исходного кода:
от точки объявления до конца (на это указывает слово static
перед именем функции).
Иными словами, к функции get_mode()
нельзя обращаться за пределами файла, в котором
она объявлена. */
/* В свою очередь, тип void
(в переводе с английского --- ``пустой'') обозначает пустое
множество значений. Он используется для указания типа возвращаемого функцией
значения в случае если функция ничего не возвращает. */
static void get_mode(void) {
/* Предшествующая строка кода указывает, что функция get_mode()
не только
не возвращает, но и не получает никаких значений на вход. */
/* *m
является указателем и представляет из себя группу ячеек памяти, которые
могут содержать адрес переменной типа char
. */
char *m;
/* KDGKMODE (Get Keyboard Mode)
объявляется в файле /usr/include/kd.h
и хранит название
текущего режима работы драйвера клавиатуры. Нам не удалось найти документации, где пояснялось бы
как расшифровывается ``KD''
. Вероятнее всего, ``KD'' --- это Keyboard Driver
. */
if (ioctl(fd, KDGKBMODE, &oldkbmode)) { perror("KDGKBMODE");
/* Функция perror()
входит в стандартную библиотеку языка Си и
определяется в заголовочном файле stdio.h
. Она выводит содержимое
своего аргумента, а также сообщение об ошибке. */
exit(1); }
/* ioctl
- системный вызов общего назначения, служащий
для управления символьными устройствами всех типов. Его синтаксис описывается следующим образом:
ioctl(fd, cmd, arg)
, где fd
- дескриптор файла; cmd
- целое число, указывающее
вызываемую команду; arg
- дополнительный аргумент команды (обычно - адрес блока параметров). */
/* KDGKBMODE
- это имя, которое в результате макроподстановки,
осуществляемой препроцессором (подробнее см. [КерниганРитчи2006, С. 102-103]),
заменяется на число, указывающее на команду, результатом которой
является получение информации о текущем режиме работы драйвера клавиатуры.
Упомянутая макроподстановка происходит благодаря содержимому файла
/usr/include/linux/kd.h
. */
/* Аргумент &oldkbmode
содержит адрес переменной oldkbmode
. Именно по этому
адресу будет записан результат выполнения вызовы ioctl
.
K_RAW
, K_XLATE
, K_MEDIUMRAW
, K_UNICODE
- это имена, используемые для
макроподстановки в файле /usr/include/linux/kd.h
. */
/* Таким образом, вместо KDGKBMODE
, K_RAW
, K_XLATE
, K_MEDIUMRAW
и K_UNICODE
подставляются числа, указанные через макроопределение в файле
/usr/include/linux/kd.h
: 0x4B44, 0x00, 0x01, 0x02, 0x03 соответственно.
Именно поэтому переменная oldkbmode
имеет тип int
. */
switch(oldkbmode) { case K_RAW: m = "RAW"; break; case K_XLATE: m = "XLATE"; break; case K_MEDIUMRAW: m = "MEDIUMRAW"; break; case K_UNICODE: m = "UNICODE"; break; default: m = _("?UNKNOWN?"); break; } printf(_("kb mode was %s\n"), m); if (oldkbmode != K_XLATE) { printf(_("[ if you are trying this under X, it might not work\n" "since the X server is also reading /dev/console ]\n")); } printf("\n"); } static void clean_up(void) {
/* KDSKBMODE (Set Keyboard Mode)
содержит число, указывающее на команду установки
режима работы драйвера клавиатуры. */
if (ioctl(fd, KDSKBMODE, oldkbmode)) { perror("KDSKBMODE");
/* Конструкция еxit(выражение)
эквивалентна конструкции return
.
Обычно она возравщает 0
если всё идёт хорошо, а 1
- когда произошла ошибка. */
exit(1); }
/* Системный вызов tcsetattr
задаёт атрибуты терминала,
предварительно сохранённые в структуре old
(копия структуры termios
). */
if (tcsetattr(fd, 0, &old) == -1) perror("tcsetattr"); close(fd); }
/* Атрибут attr_noreturn
указывает на то, что функция никогда
не возвращает значений. */
static void attr_noreturn die(int x) { printf(_("caught signal %d, cleaning up...\n"), x);
/* Функция clean_up()
восстанавливает режим работы драйвера клавиатуры,
исходя из данных, полученных с помощью системного вызова ioctl
. */
clean_up(); exit(1); }
/* Атрибут attr_unused
указывает на то, что аргумент функции никак не обрабатывается. */
static void attr_noreturn watch_dog(attr_unused int x) { clean_up(); exit(0); }
/* Функция usage()
, выводящая информацию об аргументах, которые
принимает программа, не нуждается в особых пояснениях. */
static void attr_noreturn usage(void) { fprintf(stderr, _( "showkey version %s\n\n" "usage: showkey [options...]\n" "\n" "valid options are:\n" "\n" " -h --help display this help text\n" " -a --ascii display the decimal/octal/hex values of the keys\n" " -s --scancodes display only the raw scan-codes\n" " -k --keycodes display only the interpreted keycodes (default)\n" ), PACKAGE_VERSION); exit(1); } int main (int argc, char *argv[]) {
/* *short_opts
- это указатель. Значение записанное в адрес, на который
ссылается этот указатель, впоследствии не меняется, так как в начале строки
с его объявлением стоит модификатор const
.
Строка, хранимая по адресу *short_opts
, содержит возможные короткие
опции, каждая из которых представлена одной буквой. */
const char *short_opts = "haskV";
/* long_opts[]
(в переводе --- ``длинные опции'', то есть содержащие более одной
буквы в своём имени) представляет из себя массив структур.
Каждая структура массива имеет следующие элементы:
{ "имя опции", наличие_аргумента, флаг, значение}. */
/* Крайняя структура в массиве должна содержать только нули. */
/* Элемент "наличие_аргумента"
может содержать либо числа (0, 1 ,2), либо имена
имена для макроподстановки, указанные в файле /usr/include/getopt.h
и
соответствующие этим числам (no_argument
соответствует нулю; required_argument
соответствует единице; optional_argument
соответствует двойке). */
/* Если элемент "значение"
содержит короткую опцию из строки, на которую указывает
*short_opts
, то элемент "флаг"
, будучи установленным в NULL
, позволяет
обеспечить соответствие между короткой и длинной опцией (например, как показано ниже, короткая опция
-h
соответствует длинной опции --help
). Если же заменить элемент "флаг"
на
указатель, то он будет указывать на переменную, имя которой находится в элементе "значение"
. */
const struct option long_opts[] = { { "help", no_argument, NULL, 'h' }, { "ascii", no_argument, NULL, 'a' }, { "scancodes", no_argument, NULL, 's' }, { "keycodes", no_argument, NULL, 'k' }, { "version", no_argument, NULL, 'V' }, { NULL, 0, NULL, 0 } }; int c; int show_keycodes = 1; int print_ascii = 0; struct termios new; unsigned char buf[18]; /* divisible by 3 */ int i, n; set_progname(argv[0]);
/* Следующие три строки кода отвечают за локализацию интерфейса программы средствами GNU gettext. */
setlocale(LC_ALL, ""); bindtextdomain(PACKAGE_NAME, LOCALEDIR); textdomain(PACKAGE_NAME);
/* Пятый арумент функции getopt_long
может содержать указатель
на структуру в массиве long_opts[]
. Подробнее см. man getopt_long
(данная страница справочного руководства, помимо всего прочего, содержит пример исходного
кода, где пятый аргумент является указателем, а не нулём, как в рассматриваемом нами файле). */
while ((c = getopt_long(argc, argv, short_opts, long_opts, NULL)) != -1) { switch (c) { case 's': show_keycodes = 0; break; case 'k': show_keycodes = 1; break; case 'a': print_ascii = 1; break; case 'V': print_version_and_exit(); case 'h': case '?': usage(); } } if (optind < argc) usage(); if (print_ascii) { /* no mode and signal and timer stuff - just read stdin */ fd = 0; if (tcgetattr(fd, &old) == -1) perror("tcgetattr"); if (tcgetattr(fd, &new) == -1) perror("tcgetattr");
/* Как уже отмечалось выше, структура new
- это копия
структуры termios
. */
/* Две строки кода, представленные далее, иллюстрируют приёмы работы
с элементами структуры (локальными флагами копии структуры
termios).
Флаги ICANON
и ISIG
устанавливаются в нуль,
ECHO
и ECHOCTL
--- в единицу
(подробнее см. [КерниганРитчи2006, с. 160]). */
/* При флаге ICANON
равном нулю, вводимые символы
не составляются в строки вплоть до чтения (выполнения системного вызова
read
). ISIG
, будучи обнулённым, блокирует обработку
управляющих символов (терминал перестаёт реагировать на управляющие
последовательности, вызывающие появление сигналов). К этим символам
относятся: INTR (Ctrl+C), QUIT (Ctrl+\), SUSP (Ctrl+z) и DSUSP (Ctrl+y).
Подробнее см. [ТаненбаумВудхалл2006, С. 270-277].*/
/* Обратите внимание, что рассматриваемый блок кода выполняется только
при условии если установлена в 1
опция -a
(print_ascii).
Согласно документации (man 3 termios
), при ECHOCTL
==1
и ECHO
==1
вводимые пользователем управляющие символы ASCII
(за исключением TAB, NL, START и STOP) отображаются в виде ^X
, где
вместо X
размещается символ из таблицы ASCII с кодом на 0x40 больше,
чем у управляющего символа. Например, в ответ на символ возврата каретки
(клавиша с надписью Enter) появляется ^M
(шестнадцатеричный код M
в таблице ASCII равен 0x4D
, a ``Enter'' соответствует коду 0xD
).
Также следует отметить, что ECHOCTL
требует, чтобы в коде
программы было определено (через #define в файле /usr/include/features.h)
имя _BSD_SOURCE
или _SVID_SOURCE
(подробнее
об этих именах см. http://www.aquaphoenix.com/ref/gnu_c_library/libc_12.html). */
new.c_lflag &= ~ (ICANON | ISIG); new.c_lflag |= (ECHO | ECHOCTL);
/* Следующая строка устанавливает все флаги ввода (IGNBRK, BRKINT, IGNPAR,
PARMRK, INPCK, ISTRIP, INLCR, IGNCR, ICRNL, IUCLC, IXON, IXANY, IXOFF, IMAXBEL, IUTF8)
в нуль. Подробнее см. /usr/include/bits/termios.h
, а также
комментарии по флагам режимов. */
new.c_iflag = 0;
/* Когда число символов в очереди будет равно значению, записанному в new.c_cc[VMIN]
,
или когда пройдёт количество десятых долей секунды, указанное в new.c_cc[VTIME]
(код рассматриваемого файла отключает таймер так как в new.c_cc[VTIME]
заносится нуль)
будет выполнен системный вызов read
. Подробнее см. [Рочкинд2005, С. 233-236]. */
new.c_cc[VMIN] = 1; new.c_cc[VTIME] = 0;
/* Изменения в структуре termios
, хранимые в структуре new
, применяются
системным вызовом tcsetattr()
. Аргумент TCSAFLUSH
говорит о том, что помимо
применения изменений должны быть очищены буферы ввода/вывода терминала
(подробнее см. раздел 3.1 документа
Serial Programming Guide for POSIX Operating Systems и его
не самый качественный, но всё же перевод на русский язык. */
if (tcsetattr(fd, TCSAFLUSH, &new) == -1) perror("tcgetattr"); printf(_("\nPress any keys - " "Ctrl-D will terminate this program\n\n")); while (1) { n = read(fd, buf, 1); if (n == 1) printf(" \t%3d 0%03o 0x%02x\n", buf[0], buf[0], buf[0]); if (n != 1 || buf[0] == 04) break; } if (tcsetattr(fd, 0, &old) == -1) perror("tcsetattr"); exit(0); }
/* Функция getfd()
определена в файле getfd.c. */
fd = getfd(NULL);
/* По умолчанию showkey
прекращает работу при отсутствии
активности пользователя (нажатых клавиш) в течение 10 секунд. */
/* the program terminates when there is no input for 10 secs */ signal(SIGALRM, watch_dog);
/* Системный вызов signal
устанавливает реакцию программы showkey
на сигналы (запросы на прерывание, осуществляемые на уровне процессов),
имена которых определены в файле /usr/include/bits/signum.h
(см. /usr/include/signal.h
). По сути речь
идёт о назначении подпрограмм обработки для каждого из сигналов.
Разработчики showkey
сделали всё достаточно прямолинейно, назначив
функцию die()
обрабатывать все возможные сигналы, кроме того, что генерируется
по истечении времени, заданного в качестве аргумента функции alarm()
(см. выше). */
/* Очень подробные и наглядные материалы о сигналах Unix представлены в книге [НеметСнайдерСибассХейн2003, С. 68-71]. */
/* if we receive a signal, we want to exit nicely, in order not to leave the keyboard in an unusable mode (единообразная обработка сигналов позволяет быть уверенным в том, что драйвер клавиатуры находится в нужном режиме). */ signal(SIGHUP, die); signal(SIGINT, die); signal(SIGQUIT, die); signal(SIGILL, die); signal(SIGTRAP, die); signal(SIGABRT, die); signal(SIGIOT, die); signal(SIGFPE, die); signal(SIGKILL, die); signal(SIGUSR1, die); signal(SIGSEGV, die); signal(SIGUSR2, die); signal(SIGPIPE, die); signal(SIGTERM, die); #ifdef SIGSTKFLT signal(SIGSTKFLT, die); #endif signal(SIGCHLD, die); signal(SIGCONT, die); signal(SIGSTOP, die); signal(SIGTSTP, die); signal(SIGTTIN, die); signal(SIGTTOU, die); get_mode(); /* Вызов функции get_mode(), определённой выше по тексту. */ if (tcgetattr(fd, &old) == -1) perror("tcgetattr"); if (tcgetattr(fd, &new) == -1) perror("tcgetattr"); new.c_lflag &= ~ (ICANON | ECHO | ISIG); new.c_iflag = 0; new.c_cc[VMIN] = sizeof(buf); new.c_cc[VTIME] = 1; /* 0.1 sec intercharacter timeout */ if (tcsetattr(fd, TCSAFLUSH, &new) == -1) perror("tcsetattr"); if (ioctl(fd, KDSKBMODE, show_keycodes ? K_MEDIUMRAW : K_RAW)) { perror("KDSKBMODE"); exit(1); } printf(_("press any key (program terminates 10s after last keypress)...\n"));
/* Как видно, чтение скан-кодов осуществляется с помощью системного вызова
read
, считывающего в бесконечном цикле байты из файла, представленного
дескриптором fd
(не забывайте, что в GNU/Linux устройства также являются файлами). */
/* show scancodes */ if (!show_keycodes) { while (1) { alarm(10); n = read(fd, buf, sizeof(buf)); for (i = 0; i < n; i++) printf("0x%02x ", buf[i]); printf("\n"); } clean_up(); exit(0); } /* show keycodes - 2.6 allows 3-byte reports */ while (1) { alarm(10); n = read(fd, buf, sizeof(buf)); i = 0; while (i < n) { int kc; char *s;
/* Как отмечалось ранее (при обсуждении вывода программы
getkeycodes
), отпускание клавиши результируется в скан-коде,
старший бит которого равен единице (напомним, что речь идёт об
особенностях набора скан-кодов Set 1
). Иными словами, к скан-коду нажатой
клавиши прибавляется шестнадцатеричное число 80
, которое
в программах на языке Си записывается как 0x80
. */
s = (buf[i] & 0x80) ? _("release") : _("press"); if (i+2 < n && (buf[i] & 0x7f) == 0 && (buf[i+1] & 0x80) != 0 && (buf[i+2] & 0x80) != 0) { kc = ((buf[i+1] & 0x7f) < 7) | (buf[i+2] & 0x7f); i += 3; } else { kc = (buf[i] & 0x7f); i++; } printf(_("keycode %3d %s\n"), kc, s); } } clean_up(); exit(0); }
/*--- Конец файла src/showkey.c с комментариями ---*/
Компилирировать представленный выше исходный код нужно вместе с файлом src/getfd.c
,
содержащем определение функции getfd()
.
gcc showkey.c getfd.c
Проверить работоспособность получившегося исполняемого файла (a.out
)
лучше всего вне X сессии. Например, в виртуальном терминале, доступном по нажатию
<Ctrl>+<Alt>+<F2>.
Разумеется, для удобства, было бы хорошо иметь возможность управлять длительностью временного
промежутка, который программа showkey
ожидает ввода пользователя перед
тем, как завершить свою работу (в kbd-1.15.3
этот промежуток времени равен 10 секундам).
В рамках проекта console-tools
данное пожелание реализовано (см. файл kbdtools/showkey.c
в архиве). Комментарии по поводу использования некоторых приложений,
входящих в состав console-tools
см. в статье
"VGA console basics and Linux console-tools".
Предыдущий раздел: | Оглавление | Следующий раздел: |
Взаимодействие с клавиатурой. Общие замечания | Режимы работы драйвера клавиатуры |