Наши партнеры

UnixForum





Библиотека сайта rus-linux.net

На главную -> MyLDP -> Электронные книги по ОС Linux
Назад Введение в мир программирования
Глава 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".


Предыдущий раздел: Оглавление Следующий раздел:
Взаимодействие с клавиатурой. Общие замечания   Режимы работы драйвера клавиатуры