getchar () и стандартный ввод - PullRequest
       1

getchar () и стандартный ввод

6 голосов
/ 12 октября 2011

Смежный вопрос здесь , но мой вопрос другой.

Но я хотел бы узнать больше о внутренностях getchar () и stdin. Я знаю, что getchar () просто вызывает fgetc (stdin).

Мой вопрос о буферизации, stdin и getchar (). Приведенный классический пример K & R:

#include <stdio.h>

main()
{
    int c;

    c = getchar();
    while (c != EOF) {
        putchar(c);
        c = getchar();
    }
}

Мне кажется, что поведение getchar () можно описать следующим образом:

Если в буфере stdin ничего нет, пусть ОС принимает пользовательский ввод, пока не будет нажата клавиша [enter]. Затем верните первый символ в буфере.

Предположим, что программа запущена, и пользователь вводит "анчоусы".

Итак, в приведенном выше листинге кода первый вызов getchar () ожидает ввода пользователя и назначает первый символ в буфере переменной c. Внутри цикла первый итерационный вызов getchar () говорит: «Эй, в буфере есть вещи, верните следующий символ в буфере». Но N-я итерация цикла while приводит к тому, что getchar () говорит: «Эй, в буфере ничего нет, так что пусть stdin собирает то, что печатает пользователь.

Я провел немного времени с источником c, но, похоже, это скорее поведенческий артефакт stdin, чем fgetc ().

Я здесь не прав? Спасибо за ваше понимание.

Ответы [ 3 ]

6 голосов
/ 12 октября 2011

getchar () имеет линейный буфер, а буфер ввода ограничен, обычно это 4 кБ. Сначала вы видите эхо каждого символа, который вы печатаете. Когда вы нажимаете ENTER, getchar () начинает возвращать символы до LF (который преобразуется в CR-LF). Когда вы продолжаете нажимать клавиши без LF в течение некоторого времени, оно перестает отображаться после 4096 символов, для продолжения нажмите клавишу ENTER.

3 голосов
/ 22 января 2019

Наблюдаемое вами поведение не имеет ничего общего с C и getchar(), но с подсистемой телетайпа (TTY) в ядре ОС.

Для этого вам нужно знать, как процессы получают свой ввод с вашей клавиатуры и как они записывают свой вывод в окно терминала (я предполагаю, что вы используете UNIX, и следующие пояснения относятся конкретно к UNIX, то есть к Linux, macOS и т. Д.) :

enter image description here

Поле, озаглавленное «Терминал» на приведенной выше диаграмме, является окном вашего терминала, например, 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.

3 голосов
/ 12 октября 2011

Я знаю, что getchar() в конечном итоге вызывает fgetc(stdin).

Не обязательно. getchar и getc могут также расшириться до фактической процедуры чтения из файла, с fgetc, реализованной как

int fgetc(FILE *fp)
{
    return getc(fp);
}

Эй, в буфере ничего нет, так что пусть stdin собирает то, что печатает пользователь. [...] кажется, что это скорее поведенческий артефакт stdin, чем fgetc().

Я могу сказать вам только то, что знаю, и именно так работает Unix / Linux. На этой платформе FILE (включая то, на что указывает stdin) содержит файловый дескриптор (int), который передается в ОС, чтобы указать, из какого источника ввода FILE получает данные плюс буфер и некоторые другие бухгалтерии.

Часть «сборка» означает «вызов системного вызова read дескриптора файла для повторного заполнения буфера». Это зависит от реализации C, однако.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...