Как стать автором
Обновить

Реализация пользовательского ввода, не блокируещего выполнение программы

Время на прочтение4 мин
Количество просмотров18K
Недавно у меня появилось немного свободного времени, поэтому я решил улучшить свои знания языка С. Пока что они ограничиваются уровнем простых институтских лабораторных работ и книгой С. Прата «Язык программирования C», поэтому для начала я поставил себе простую задачу — написать консольную змейку. К тому моменту, когда отображение игрового поля и самой змейки на экране и часть, отвечающая за передвижение ее по полю была написана, появилось две проблемы:
  • данные отправлялись программе только после нажатия Enter
  • программа останавливалась на считывании пользовательского ввода

Можно было решить их при помощи использования getch() из curses, но это было скучно и не интересно.
О том как были решены эти проблемы под катом.
Для начала нужно было справиться со считыванием нажатия клавиш пользователя. Реализовано это у меня через вызов функции getc(), но при вызове этой функции программа начинала считывать символы, только после того, как был нажат Enter. В ходе недолго поиска было обнаружено, что это не проблема getc(), а проблема терминала Linux, точнее драйвера терминала, который по-умолчанию работает в каноническом режиме, т.е. это драйвер терминала ожидает окончания ввода строки нажатием Enter, после чего отправляет данную строку в stdin программы. Так же было найдено решение данной проблемы: перевод драйвера в неканонический режим. Реализуется это очень просто — подключается заголовочный файл termios.h и пишутся две функции:
Первая функция для перевода в неканонический режим:
void set_keypress(void)
{
	struct termios new_settings;

	tcgetattr(0,&stored_settings);

	new_settings = stored_settings;

	/* 
		Отключение канонического режима и вывода на экран 
		и установка буфера ввода размером в 1 байт 
	*/
	new_settings.c_lflag &= (~ICANON);
	new_settings.c_lflag &= (~ECHO);
	new_settings.c_cc[VTIME] = 0;
	new_settings.c_cc[VMIN] = 1;

	tcsetattr(0,TCSANOW,&new_settings);
	return;
}

Вторая функция для возвращения в первоначальное состояние:
void reset_keypress(void)
{
	tcsetattr(0,TCSANOW,&stored_settings);
	return;
}

Переменная stored_settings должна быть объявлена как глобальная:
static struct termios stored_settings;

Затем в начале программы вызывается первая функция, а в конце, соответственно, вызывается вторая.
В готовом виде тестовый пример выглядит так:
#include <stdlib.h>
#include <stdio.h>

#include <termios.h>
#include <string.h>

static struct termios stored_settings;

void set_keypress(void)
{
	struct termios new_settings;

	tcgetattr(0,&stored_settings);

	new_settings = stored_settings;

	new_settings.c_lflag &= (~ICANON & ~ECHO);
	new_settings.c_cc[VTIME] = 0;
	new_settings.c_cc[VMIN] = 1;

	tcsetattr(0,TCSANOW,&new_settings);
	return;
}

void reset_keypress(void)
{
	tcsetattr(0,TCSANOW,&stored_settings);
	return;
}

int main(void)
{
	set_keypress();
	
	printf("Test: ");
	while(1)
	{	
		// putchar здесь вызывается для того, чтобы проверить работоспособность
		putchar(getchar()); 
	}
	return 0;
}

Теперь моя змейка умела считывать посимвольно пользовательский ввод. Но появилась вторая проблема: программа все равно останавливалась и ждала пока пользователь нажмет какую-либо клавишу. Снова начался поиск в интернете, в процессе которого я часто натыкался на «решение» этой проблеммы в виде того, что было описано выше. Результатом поиска стал совет использовать функцию select().
int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *utimeout);

Из параметров, принимаемых этой функцией нас интересуют только n, readfds и utimeout:
  • n — этот параметр должен превышать не единицу максимальный файловый дескриптор в любом из наборов. Другими словами, вы должны определить максимальное целое значение для всех ваших дескрипторов, увеличить его на 1, и передать результат в качестве параметра n.
  • readfds — этот набор мониторится на наличие данных для чтения в одном или нескольких дескрипторах. После возврата из select набор readfs будет очищен от всех дескрипторов, кроме тех, в которых есть данные, доступные для немедленного чтения функциями recv() (для сокетов) или read() (для каналов pipe, файлов и сокетов).
  • utimeout — максимальное время, в течение которого select будет ожидать смены статуса. Если этот параметр установлен в NULL, select будет заблокирован бесконечно, ожидая событий в дескрипторах. При установке параметра в 0 секунд select возвратится немедленно.

Поэтому вызов select() будет выглядеть слудеющим образом:
select(1, &rfds, NULL, NULL, &tv);

Для указания, что события будут рассматриваться только для stdin нужно вызвать
FD_SET(0, &rfds);

Структура struct timeval определена следующим образом:
struct timeval {
   time_t tv_sec;    /* секунды */
   long tv_usec;     /* микросекунды */
};

В итоге у нас получается вот такой тестовый пример (не забудем перевести терминал в неканонический режим, что уже было рассмотрено выше):
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <termios.h>

static struct termios stored_settings;

void set_keypress(void)
{
	struct termios new_settings;

	tcgetattr(0,&stored_settings);

	new_settings = stored_settings;

	new_settings.c_lflag &= (~ICANON & ~ECHO);
	new_settings.c_cc[VTIME] = 0;
	new_settings.c_cc[VMIN] = 1;

	tcsetattr(0,TCSANOW,&new_settings);
	return;
}

void reset_keypress(void)
{
	tcsetattr(0,TCSANOW,&stored_settings);
	return;
}

int main(void) 
{
	fd_set rfds;
	struct timeval tv;
	int retval;
	
	set_keypress();

	while(1)
	{
		FD_ZERO(&rfds);
		FD_SET(0, &rfds);
		
		tv.tv_sec = 0;
		tv.tv_usec = 0;	
		retval = select(2, &rfds, NULL, NULL, &tv);
		if (retval)
		{
			printf("Data is available now.\n");
			getc(stdin);
		} 
		else
		{
			printf("No data available.\n");
		}
		usleep(100000);
	}
	reset_keypress();
	exit(0);
}


Использованный материал:
Теги:
Хабы:
Всего голосов 18: ↑9 и ↓90
Комментарии5

Публикации

Истории

Ближайшие события

27 августа – 7 октября
Премия digital-кейсов «Проксима»
МоскваОнлайн
28 – 29 сентября
Конференция E-CODE
МоскваОнлайн
28 сентября – 5 октября
О! Хакатон
Онлайн
30 сентября – 1 октября
Конференция фронтенд-разработчиков FrontendConf 2024
МоскваОнлайн
3 – 18 октября
Kokoc Hackathon 2024
Онлайн
7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн