Наблюдаемое вами поведение не имеет ничего общего с C и getchar()
, но с подсистемой телетайпа (TTY) в ядре ОС.
Для этого вам нужно знать, как процессы получают свой ввод с вашей клавиатуры и как они записывают свой вывод в окно терминала (я предполагаю, что вы используете UNIX, и следующие пояснения относятся конкретно к UNIX, то есть к Linux, macOS и т. Д.) :
Поле, озаглавленное «Терминал» на приведенной выше диаграмме, является окном вашего терминала, например, xterm, iTerm или Terminal.app. В старые времена терминалы были отдельными аппаратными устройствами, состоящими из клавиатуры и экрана, и они были подключены к (возможно, удаленному) компьютеру по последовательной линии (RS-232). Каждый символ, набранный на клавиатуре терминала, отправлялся по этой строке на компьютер и использовался приложением, которое было подключено к терминалу. И каждый символ, полученный приложением в качестве вывода, отправлялся по той же строке на терминал, который отображал его на экране.
В настоящее время терминалы больше не являются аппаратными устройствами, но они переместились «внутрь» компьютера и стали процессами, которые называются эмуляторами терминалов . xterm, iTerm2, Terminal.app и т. д. - все это эмуляторы терминала.
Однако механизм связи между приложениями и эмуляторами терминала остался таким же , как и для аппаратных терминалов. Эмуляторы терминалов эмулируют аппаратные терминалы. Это означает, что с точки зрения приложения разговор с эмулятором терминала сегодня (например, iTerm2 ) работает так же, как разговор с реальным терминалом (например, DEC VT100 ) назад в 1979 году. Этот механизм был оставлен без изменений, так что приложения, разработанные для аппаратных терминалов, будут по-прежнему работать с программными эмуляторами терминалов.
Так как же работает этот механизм связи? В ядре UNIX есть подсистема под названием TTY (TTY означает телетайп, который был самой ранней формой компьютерных терминалов, у которых даже не было экрана, только клавиатура и принтер). Вы можете думать о TTY как универсальный драйвер для терминалов. TTY считывает байты из порта, к которому подключен терминал (поступает с клавиатуры терминала), и записывает байты в этот порт (отправляется на дисплей терминала).
Существует экземпляр TTY для каждого терминала, подключенного к компьютеру (или для каждого процесса эмулятора терминала, запущенного на компьютере). Следовательно, экземпляр TTY также называется устройством TTY (с точки зрения приложения, разговор с экземпляром TTY подобен разговору с терминальным устройством). В способе UNIX сделать интерфейсы драйверов доступными в виде файлов, эти устройства TTY отображаются как /dev/tty*
в некоторой форме, например, в macOS они /dev/ttys001
, /dev/ttys002
и т. Д.
Приложение может иметь свои стандартные потоки (stdin, stdout, stderr), направленные на устройство TTY (фактически это значение по умолчанию, и вы можете узнать, к какому устройству TTY ваша оболочка подключена с помощью команды tty
). Это означает, что все, что пользователь вводит на клавиатуре, становится стандартным вводом приложения, а все, что приложение записывает в свой стандартный вывод, отправляется на экран терминала (или в окно терминала эмулятора терминала). Все это происходит через устройство TTY, то есть приложение взаимодействует только с устройством TTY (драйвером этого типа) в ядре.
Теперь решающий момент: устройство TTY делает больше, чем просто передает каждый входной символ на стандартный ввод приложения. По умолчанию устройство TTY применяет к полученным символам так называемую линейную дисциплину . Это означает, что он локально буферизует их и интерпретирует delete , backspace и другие символы редактирования строки и передает их на стандартный ввод приложения только тогда, когда получает возврат каретки или перевод строки , что означает, что пользователь завершил ввод и редактирование всей строки.
Это означает, что пока пользователь не нажмет return , getchar()
не увидит ничего в stdin. Как будто ничего не было напечатано до сих пор. Только когда пользователь нажимает return , устройство TTY отправляет эти символы на стандартный ввод приложения, где getchar()
немедленно считывает их как.
В этом смысле в поведении getchar()
нет ничего особенного. Он просто сразу читает символы в stdin по мере их появления. Наблюдаемая вами буферизация строки происходит в устройстве TTY в ядре.
Теперь к интересной части: это устройство TTY можно настраивать. Вы можете сделать это, например, из оболочки с помощью команды stty
. Это позволяет вам настроить почти каждый аспект дисциплины линии, который устройство TTY применяет к входящим символам. Или вы можете отключить любую обработку, установив на устройстве TTY значение raw mode . В этом случае устройство TTY немедленно перенаправляет каждый полученный символ на стандартный экран приложения без какой-либо формы редактирования.
Если вы включите режим raw в устройстве TTY, вы увидите, что getchar()
немедленно получает каждый символ, который вы вводите на клавиатуре. Следующая программа на C демонстрирует это:
#include <stdio.h>
#include <unistd.h> // STDIN_FILENO, isatty(), ttyname()
#include <stdlib.h> // exit()
#include <termios.h>
int main() {
struct termios tty_opts_backup, tty_opts_raw;
if (!isatty(STDIN_FILENO)) {
printf("Error: stdin is not a TTY\n");
exit(1);
}
printf("stdin is %s\n", ttyname(STDIN_FILENO));
// Back up current TTY settings
tcgetattr(STDIN_FILENO, &tty_opts_backup);
// Change TTY settings to raw mode
cfmakeraw(&tty_opts_raw);
tcsetattr(STDIN_FILENO, TCSANOW, &tty_opts_raw);
// Read and print characters from stdin
int c, i = 1;
for (c = getchar(); c != 3; c = getchar()) {
printf("%d. 0x%02x (0%02o)\r\n", i++, c, c);
}
printf("You typed 0x03 (003). Exiting.\r\n");
// Restore previous TTY settings
tcsetattr(STDIN_FILENO, TCSANOW, &tty_opts_backup);
}
Программа переводит устройство TTY текущего процесса в режим raw, затем использует getchar()
для чтения и печати символов из stdin в цикле. Символы печатаются в виде кодов ASCII в шестнадцатеричной и восьмеричной форме. Программа специально интерпретирует символ ETX
(код ASCII 0x03) как триггер для завершения. Вы можете создать этот символ на клавиатуре, набрав Ctrl-C
.