Программирование PC FAQ программирования Linux: ввод/вывод терминала Tue, October 08 2024  

Поделиться

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

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

FAQ программирования Linux: ввод/вывод терминала Печать
Добавил(а) microsin   

Здесь приведен перевод главы 3 из FAQ по программированию Linux [1], посвященной вводу/вывода терминала. Описание незнакомых терминов и аббревиатур см. в Словарике статьи [2].

[Список вопросов]

3. Terminal I/O

  3.1 Как сделать, чтобы моя консольная программа не печатала клавиатурные нажатия (echo input)?

  3.2 Как прочитать одиночные нажатия на клавиатуре в терминале?

  3.3 Как узнать, была ли нажата клавиша?

  3.4 Как перемещать курсор по экрану?

  3.5 Что такое pttys?

  3.6 Как работать с последовательным портом или модемом?

    3.6.1 Имена и типы последовательных устройств

    3.6.2 Установка флагов termios

      c_iflag, c_oflag, c_cflag, c_lflag, c_cc

3.1 Как сделать, чтобы моя консольная программа не печатала клавиатурные нажатия (echo input)?
==============================================================================================

Как сделать так, чтобы программа не выводила символы как эхо ввода, наподобие как это сделано при запросе системой пароля?

Существует простой способ, а также способ посложнее.

Для простого варианта используйте функцию getpass(), которая вероятно есть почти во всех операционных системах семейства Unix. Функция принимает в качестве параметра строку, которая будет выведена для приглашения ввода (prompt). Она будет считывать символы до поступления EOF или символа новой строки, и возвратит указатель на статическую область памяти, где будет находиться введенная строка.

Более сложный способ - использовать tcgetattr() и tcsetattr() вместе со структурой termios, чтобы манипулировать терминалом. Следующие две подпрограммы разрешают режим с эхом и режим без эха.

   #include < stdlib.h>
   #include < stdio.h>
   
   #include < termios.h>
   #include < string.h>
   
   static struct termios stored_settings;
   
   void echo_off(void)
   {
      struct termios new_settings;
      tcgetattr(0,&stored_settings);
      new_settings = stored_settings;
      new_settings.c_lflag &= (~ECHO);
      tcsetattr(0,TCSANOW,&new_settings);
      return;
   }
   
   void echo_on(void)
   {
      tcsetattr(0,TCSANOW,&stored_settings);
      return;
   }

Обе эти подпрограммы используются и определены стандартом POSIX.

3.2 Как прочитать одиночные нажатия на клавиатуре в терминале?
==============================================================

Как прочитать одиночные символы из терминала? Моя программа всегда ждет, когда пользователь нажмет клавишу Enter.

Терминалы обычно работают в каноническом режиме (canonical mode), когда ввод считывается строками, после того как строка полностью отредактирована. Это можно перенастроить для не канонического (non-canonical) режима, где вы устанавливаете, сколько символов должно быть прочитано до того, как они будут переданы вашей программе. Также вы можете установить в 0 таймер не канонического режима терминала, этот таймер сбрасывает (flush) буфер в установленные интервалы времени. Таким способом вы можете использовать getc() для немедленного получения нажатой пользователем клавиши. Мы используем tcgetattr() и tcsetattr(), как они определены стандартом POSIX для манипуляции структурой termios.

   #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;
   
      /* Запрет канонического режима и установка размера буфера в 1 байт */
      new_settings.c_lflag &= (~ICANON);
      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;
   }

См. также [3], где описан аналог функции getch() в Linux, которая позволяет получать ввод в терминале по одному символу.

3.3 Как узнать, была ли нажата клавиша?
=======================================

Как проверить, что в терминале была нажата клавиша? В DOS можно использовать функцию kbhit(), есть ли её эквивалент в Linux?

Если вы установили терминал в режим одиночного ввода символов (см. ответ на предыдущий вопрос), то (на большинстве систем) вы можете использовать select() или poll() для проверки читаемости терминала. См. также [4], разделы 2.1.1 и 2.1.2.

3.4 Как перемещать курсор по экрану?
====================================

Если серьезно, то вероятно вы НЕ ХОТИТЕ делать это. Многие программисты жалуются на различные странности, которые проявляют разные типы терминалов в своем поведении. В то время как termcap/terminfo укажут вам, обладает ли какой-то конкретный терминал определенной особенностью, вы вероятно обнаружите, что корректная обработка всех проблем поведения терминала обернется ОЧЕНЬ БОЛЬШИМИ трудозатратами.

Однако если вы все же настоятельно хотите увязнуть в этой проблеме, то смотрите в сторону функций termcap, в частности tputs(), tparm() и tgoto().

3.5 Что такое pttys?
====================

Псевдо-терминалы (pttys, ptys, существуют и другие варианты аббревиатур) это псевдо-устройства, состоящие из двух частей: сторона "master", которая может представлять пользователя, и сторона "slave", которая ведет себя как стандартное устройство tty.

Псевдо-терминалы существуют для того, чтобы обеспечить средство для эмуляции поведения последовательного терминала под управлением программы. Например, утилита telnet использует псевдо-терминал на удаленной (подключенной через сетевое соединение) системе; remote login shell предоставляет поведение терминала так, как если бы это было локальное устройство tty, однако сторона master псевдо-терминала управляется демоном операционной системы Linux, который перенаправляет все данные через сеть. Псевдо-терминалы также используются такими программами, как xterm, expect, script, screen, emacs и многие другие.

См. также описание терминов PTY и TTY в статье [5].

3.6 Как работать с последовательным портом или модемом?
=======================================================

На обработку последовательных устройств в Unix сильно влияет традиционное использование использование последовательных терминалов. Исторически были необходимы различные комбинации ioctls и другие хаки для точного управления последовательным устройством. Но к счастью, это одна из областей, в которой POSIX предприняла некоторые усилия для стандартизации.

Если вы используете систему, которая не понимает termios.h, tcsetattr() и связанные с этим функции, то вам понадобится дополнительный поиск для получения информации по обновлению вашей системы, или установки чего-нибудь менее археологического.

Однако между системами все еще есть значительные различия, в основном касающиеся области имен устройств, обработки аппаратного управления потоком (hardware flow control) и сигналов модема. Всегда насколько возможно оставляйте драйверу устройства работу по квитированию, и не пытайтесь напрямую управлять сигналами квитирования.

Для открытия и инициализации последовательного устройства предпринимайте следующие шаги:

   * Используйте open() для открытия устройства; это может потребовать использования определенных флагов:

    O_NONBLOCK. Открытие dial-in устройства или управляемого модема будет заблокировано, пока будет присутствовать несущая, если этот флаг не используется. Не блокирующее открытие позволит вам отключить элементы управления модемом (см. далее CLOCAL), если это необходимо.
    O_NOCTTY. На системах, происходящих от 4.4BSD, это избыточно, но на других системах этот флаг контролирует, может ли последовательное устройство стать управляющим терминалом для сессии. В большинстве случаев вы возможно НЕ ЗАХОТИТЕ захватить управление терминалом, поэтому нужно установить этот флаг, однако есть исключения.

   * Используйте tcgetattr() для получения текущих режимов устройства. В то время как игнорируются большинство или все первоначальные настройки, полученные таким способом, это все еще удобный способ инициализации структуры termios.
   * Установите подходящие значения для полей c_oflag, c_cflag, c_lflag и c_cc в структуре termios (см. далее).
   * Используйте cfsetispeed() и cfsetospeed() для установки желаемой скорости обмена (baud rate). Очень немногие системы позволят вам установить разные скорости для ввода и вывода, так что что главное правило - следует установить одинаковые обе эти скорости.
   * Используйте tcsetattr() для установки режимов устройства.
   * Вы можете захотеть, если используете флаг O_NONBLOCK при открытии порта, использовать fcntl() для гарантии, что O_NONBLOCK снова выключен. Все выглядит так, что системы отличаются в том, повлияет ли открытый в неблокирующем режиме tty на последующие вызовы read().

Как только вы открыли и настроили порт, можете затем использовать вызовы read() и write() обычным способом. Обратите внимание, что поведение read() будет управляться настройками флагов, которые вы предоставили для tcsetattr().

Также следует знать о дополнительных полезных функциях tcflush(), tcdrain(), tcsendbreak() и tcflow().

Когда вы завершили работу с портом и хотите его закрыть, имейте в виду очень неприятную опасность, которая может возникнуть на некоторых системах. Если есть какие-то выходные данные, ожидающие записи в устройство (т. е. если поток вывода был остановлен аппаратным или программным квитированием), то ваш процесс может зависнуть "не убиваемым" образом на вызове close() до тех пор, пока вывод не закончится. Вызовите tcflush(), чтобы отбросить любой ожидающий передачи вывод, что будет вероятно разумным шагом.

3.6.1 Имена и типы последовательных устройств
---------------------------------------------

Имена, используемые для устройств последовательного порта, довольно значительно различаются между системами. Вот некоторые примеры:

   /dev/tty[0-9][a-z] для устройств с прямым доступом, и /dev/tty[0-9][A-Z] для устройств, управляемых как модем (например в SCO Unix).
   /dev/cua[0-9]p[0-9] для устройств с прямым доступом, /dev/cul[0-9]p[0-9] для устройство дозвона (dial-out) и /dev/ttyd[0-9]p[0-9] для устройств приема звонков (dial-in) (например в HP-UX).
   /dev/cua[a-z][0-9] для устройств dial-out и /dev/tty[a-z][0-9] для устройств dial-in (например в FreeBSD).

Точное взаимодействие с используемым именем устройства и влияние на любые аппаратные линии установки связи (hardware handshake) зависит от системы, конфигурации и аппаратуры, однако обычно следуют примерно таким правилам (в предположении, что оборудование RS-232 DTE):

   - Успешное открытие любого устройства должно установить сигналы DTR и RTS.
   - Блокирующее открытие модемного (modem-control) устройства или устройства приема входящих звонков (dial-in) будет ждать сигнала DCD (Data Carrier Detect и возможно также сигналов DSR и/или CTS), обычно после установки DTR/RTS.
   - Открытие устройства дозвона (dial-out), пока существует блокировка на вызове соответствующего устройства dial-in, МОЖЕТ ПРИВЕСТИ (или нет) а завершению открытия порта dial-in. Некоторые системы реализуют простую систему совместного использования портов dial-in и dial-out, в соответствии с которой порт dial-in фактически переводится в "спящий режим", в то время как порт dial-out используется. Другие системы этого не делают, и совместное использование порта между dial-in и dial-out в таких системах требует внешнего взаимодействия (например, использования файлов блокировки, UUCP lockfiles), чтобы избежать проблем с конфликтами доступа.

3.6.2 Установка флагов termios
------------------------------

Некоторые советы по настройке флагов termios при использовании последовательного устройства, которое вы открыли самостоятельно (в отличие от использования существующего элемента управления tty):

c_iflag. Вы возможно хотите установить ВСЕ биты c_iflag в значение 0, за исключением ситуации, когда хотите использовать программное управление потоком, software flow control (ick). В таком случае установите IXON и IXOFF.

c_oflag. Большинство бит в c_oflag это хаки того или иного рода, чтобы заставить работать вывод на медленные терминалы, и поэтому некоторые новые системы сбросили почти все эти биты как устаревшие (особенно все кровавые опции дополнения вывода). Как и с битами c_iflag обнуление всех бит в c_oflag будет резонным для большинства приложений.

c_cflag. Когда устанавливается размер символа, не забудьте сначала применить маску CSIZE. Например, для установки 8-битных символов используйте:

         attr.c_cflag &= ~CSIZE;
         attr.c_cflag |= CS8;

Другие важные флаги, которые вы вероятно захотите установить в состояние ON, это флаги CREAD и HUPCL.

Если вам нужно генерировать четную parity, то установите PARENB и очистите PARODD; если вам надо генерировать нечетную parity, то установите оба PARENB и PARODD. Если вам вообще не нужна проверка четности parity, то убедитесь, что очищен PARENB.

Очистите CSTOPB за исключением ситуации, что вам реально надо генерировать 2 стоп-бита.

Флаги для разрешения аппаратного управления потоком (hardware flow control) также могут находиться в c_cflag, однако они не стандартизованы (а жаль).

c_lflag. Большинство приложений вероятно захотят выключить ICANON (каноничность, т. е. обработка ввода по строкам, с ожиданием клавиши Enter), ECHO и ISIG.

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

c_cc. Это массив символов, которые для ввода имеют специальный смысл. Эти символы получили имена наподобие VINTR, VSTOP и т. д.; имена представляют собой индексы в этом массиве.

Замечание: два из этих "символов" на самом деле не символы, но они управляют поведением read(), когда запрещен ICANON; это VMIN и VTIME.

Индексы часто называют реальными переменными, например "установите VMIN в 1" фактически означает "установите c_cc[VMIN] в 1". Такое упрощение полезно, но слегка сбивает с толку.

Многие из слотов массива c_cc используются только при определенных комбинациях установленных флагов:

Используются, только если установлен ICANON: VEOF, VEOL, VERASE, VKILL (а также VEOL2, VSTATUS и VWERASE, если установлен IEXTEN).
Используются, только если установлен ISIG: VINTR, VQUIT, VSUSP (а также VDSUSP, если он определен, и установлен IEXTEN).
Используются, только если установлен IXON или IXOFF: VSTOP, VSTART.
Используются, только если НЕ установлен ICANON: VMIN, VTIME.

Реализации могут определять дополнительные элементы в c_cc. Может быть целесообразным инициализировать все записи в _POSIX_VDISABLE (константа NCCS дает размер массива) перед установкой определенных значений, которые вы хотите использовать.

VMIN и VTIME (которые могут совместно использовать слоты с VEOF и VEOL соответственно, в зависимости от реализации) имеют следующий смысл. Значение VTIME (если не 0) всегда интерпретируется как таймер в десятых долях секунды.

c_cc[VMIN] > 0, c_cc[VTIME] > 0
     read() выполнит возврат, когда либо поступит VMIN на вводе, либо если как минимум 1 символ был прочитан, и между символами истек интервал таймаута VTIME, или если функция была прервана сигналом.

c_cc[VMIN] > 0, c_cc[VTIME] == 0
     read() выполнит возврат, когда на вводе поступило VMIN байт, или когда было прерывание. Иначе ожидание бесконечное.

c_cc[VMIN] == 0, c_cc[VTIME] > 0
     read() выполнит возврат как только на вводе поступит любое количество байт; если интервал VTIME истек, и никаких данных не было, то будет выполнен возврат без прочитанных символов (это несколько конфликтует с индикацией end-of-file, полученной в событии modem hangup; использование 1 для VMIN и либо alarm(), либо select() для таймаута позволяет избежать этой частной проблемы).

c_cc[VMIN] == 0, c_cc[VTIME] == 0
     read() выполнит немедленный возврат; если данных не было, то она не возвратит прочитанные символы (существует та же самая проблема, что обозначена выше).

[Ссылки]

1. Unix Programming FAQ (v1.37) site:opennet.ru.
2. FAQ программирования Linux: управление процессами.
3. Различия между функциями getc, getchar, getch, getche.
4. FAQ программирования Linux: управление файлами.
5. Что такое PTY и TTY?
6Опции GCC для поддержки отладки.
7. Linux: как добиться не блокирующего ввода/вывода консоли.

 

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


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

Top of Page