Программирование PC Linux: как добиться не блокирующего ввода/вывода консоли Mon, July 15 2024  

Поделиться

Нашли опечатку?

Пожалуйста, сообщите об этом - просто выделите ошибочное слово или фразу и нажмите Shift Enter.

Linux: как добиться не блокирующего ввода/вывода консоли Печать
Добавил(а) microsin   

Не блокирующего ввода можно добиться, если указать флаг O_NONBLOCK для вызова функции fcntl. Простой пример:

#include < unistd.h>
#include < fcntl.h>
#include < stdio.h>

int main(int argc, char const *argv[]) { char buf[20]; fcntl(STDIN_FILENO, F_SETFL, fcntl(STDIN_FILENO, F_GETFL) | O_NONBLOCK); sleep(4); int numRead = read(STDIN_FILENO, buf, 4); if (numRead > 0) { printf("You said: %s", buf); } }

Когда вы запустите эту программу, то у вас будет 4 секунды, чтобы что-то ввести м клавиатуры. Если ничего не было введено, то просто произойдет выход из программы без каких-либо сообщений. Если же были нажатия, то они будут выведены в консоль. Пример двух таких запусков:

$ ./a.out
fda
You said: fda
$ ./a.out
$

Пример функции, которая проверяет, есть ли какие-нибудь символы на вводе:

#include < unistd.h>
#include < stdio.h>
#include < sys/select.h>

// Аналогичная функция в мире Windows это kbhit() [2], которая зеркально
// выводит набранные символы в консоль. В остальном код такой же.
bool inputAvailable() { struct timeval tv; fd_set fds; tv.tv_sec = 0; tv.tv_usec = 0; FD_ZERO(&fds); FD_SET(STDIN_FILENO, &fds); select(STDIN_FILENO+1, &fds, NULL, NULL, &tv); return (FD_ISSET(0, &fds)); }

Эта функция должна быть вызвана перед любой функцией, которая производит любое чтение из stdin, иначе произойдет бесконечное зависание на ожидании ввода. Проблему можно решить добавлением отдельного потока для чтения клавиатурных нажатий, который будет пересылать введенные команды в очередь, а очередь может периодически обрабатываться в основном потоке (цикле функции main).

THREAD_RETURN inputThread()
{
   while( !cancelled() )
   {
      if (inputAvailable())
      {
         std::string nextCommand;
         getline(std::cin, nextCommand);
         commandQueue.lock();
         commandQueue.add(nextCommand);
         commandQueue.unlock();
      }
      else
      {
         sleep(1);
      }
   }
   return 0;
}

[Не блокирующий ввод в цикле без использования ncurses]

Например, в программе необходимо реализовать пользовательский ввод, но при этом должны осуществляться и другие операции обработки (например прием и передача через последовательный порт). Таким образом, в программе должен быть не блокирующий ввод с клавиатуры. Основной алгоритм должен быть следующий: программа в цикле ждет ввода пользователя, при получении ввода обрабатывает его, и одновременно в фоне обрабатывает другие циклические операции.

Обычно для этой цели используют библиотеку ncurses, которая упрощает реализую не блокирующего ввода с помощью функций timeout(0) и getch() [3]. Здесь описывается альтернативный метод.

int kbhit()
{
   struct timeval tv;
   fd_set fds;
   tv.tv_sec = 0;
   tv.tv_usec = 0;
   FD_ZERO(&fds);
   FD_SET(STDIN_FILENO, &fds);
   select(STDIN_FILENO+1, &fds, NULL, NULL, &tv);
   return FD_ISSET(STDIN_FILENO, &fds);
}

Эта функция выполняет неблокирующую проверку стандартного ввода (stdin) без таймаута ожидания, потому что и tv.tv_sec, и tv.tv_usec установлены в 0. Вызов select обычно используют в случае, когда существует необходимость обработать нескольких потоков I/O, или для одновременной их проверки. Но в этом случае интерес представляет только стандартный ввод, поэтому триггером будет только один FD_SET(STDIN_FILENO, &fds). Поскольку нас интересует только ввод, fd_set помещается во второй параметр вызова select(), третий и четвертый параметр задают ввод и исключения соответственно.

После select, если триггер был на пользовательском вводе, то FD_ISSET вернет ненулевое значение, иначе вернет 0. Таким образом, можно использовать код наподобие следующего:

while(!kbhit())
{
   // тут какие-то фоновые операции...
}

// обработка пользовательских нажатий

В каноническом режиме терминала (canonical mode), используемом по умолчанию, необходимо нажимать Enter для подтверждения пользовательского ввода. Т. е. в каноническом режиме система ждет нажатия Enter, чтобы принять ввод пользователя. Если это не ваш случай, и необходимо получать отклик на каждый введенный символ, то потребуется еще одна функция для перенастройки режима терминала.

void nonblock(int state)
{
   struct termios ttystate;
 
   // Получение текущего состояния терминала:
   tcgetattr(STDIN_FILENO, &ttystate);
 
   if (state==NB_ENABLE)
   {
      // Выключение канонического режима терминала:
      ttystate.c_lflag &= ~ICANON;
      // Минимальное количество введенных пользователем символов:
      ttystate.c_cc[VMIN] = 1;
   }
   else if (state==NB_DISABLE)
   {
      // Включение канонического режима:
      ttystate.c_lflag |= ICANON;
   }
   // Установка атрибутов терминала.
   tcsetattr(STDIN_FILENO, TCSANOW, &ttystate);
}

Строка ttystate.c_cc[VMIN] устанавливает минимальное количество пользовательских нажатий на клавиатуре. Например, если вы установите это в 2, то select будет ждать, пока не будет введено 2 символа, затем он захватит полученные символы в качестве ввода.

Теперь посмотрим, как это может работать:

int main()
{
   char c;
   int i=0;
 
   nonblock(NB_ENABLE);
   while(!i)
   {
      usleep(1);
      i=kbhit();
      if (i!=0)
      {
         c=fgetc(stdin);
         if (c=='q')
            i=1;
         else
            i=0;
      }
 
      fprintf(stderr,"%d ",i);
   }
   printf("\n you hit %c. \n",c);
   nonblock(NB_DISABLE);
 
   return 0;
}

Нажатие 'q' приведет к выходу из программы, иначе программа будет постоянно выводить на экран '0'. Использование короткой задержки usleep(1) вместо sleep() делает программу более отзывчивой, однако при этом больше нагружается CPU.

Если сравнивать эту программу с аналогичной реализацией на основе библиотеки ncurses, то программа на ncurses потребляет больше памяти. По ресурсам CPU для обоих вариантов программ утилиты ps и top показали 0.0%.

Программа на ncurses выглядит следующим образом:

#include < stdio.h>
#include < curses.h>
#include < unistd.h>
int main () { int i=0; initscr();// инициализация ncurses timeout(0); while(!i) { usleep(1); i=getch(); printw("%d ",i); if(i > 0) i=1; else i=0; } endwin(); printf("\nhitkb end\n"); return 0; }

[Ссылки]

1. How do you do non-blocking console I/O on Linux in C site:stackoverflow.com.
2. FAQ программирования Linux: ввод/вывод терминала.
3. Различия между функциями getc, getchar, getch, getche.
4. Linux: задержки в программе на языке C.

 

Добавить комментарий


Защитный код
Обновить

Top of Page