c - getchar() 和标准输入

标签 c

一个相关的问题是 here ,但我的问题是不同的。

但是,我想更多地了解 getchar() 和 stdin 的内部结构。我知道 getchar() 最终只会调用 fgetc(stdin)。

我的问题是关于缓冲、标准输入和 getchar() 行为。鉴于经典的 K&R 示例:

#include <stdio.h>

main()
{
    int c;

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

在我看来,getchar() 的行为可以描述如下:

如果 stdin 缓冲区中没有任何内容,请让操作系统接受用户输入,直到按下 [enter]。然后返回缓冲区中的第一个字符。

假设程序运行并且用户键入“anchovies”。

因此,在上面的代码 list 中,对 getchar() 的第一次调用等待用户输入并将缓冲区中的第一个字符分配给变量 c。在循环内部,第一次迭代调用 getchar() 说“嘿,缓冲区中有东西,返回缓冲区中的下一个字符。”但是 while 循环的第 N 次迭代导致 getchar() 说“嘿,缓冲区中什么都没有,所以让 stdin 收集用户键入的内容。

我花了一些时间在 c 源代码上,但这似乎更像是 stdin 而不是 fgetc() 的行为工件。

我在这里错了吗?感谢您的洞察力。

最佳答案

您观察到的行为与 C 和 getchar() 无关。 ,但使用 OS 内核中的电传 (TTY) 子系统。

为此,您需要了解进程如何从您的键盘获取输入以及它们如何将输出写入您的终端窗口(我假设您使用 UNIX,以下解释专门适用于 UNIX,即 Linux、macOS 等):

enter image description here

上图中标题为“终端”的框是您的终端窗口,例如xterm、iTerm 或 Terminal.app。在过去,终端是单独的硬件设备,由键盘和屏幕组成,它们通过串行线路 (RS-232) 连接到(可能是远程的)计算机。在终端键盘上输入的每个字符都通过这条线发送到计算机,并由连接到终端的应用程序使用。应用程序作为输出生成的每个字符都通过同一行发送到在屏幕上显示的终端。

如今,终端不再是硬件设备,而是移动到计算机“内部”并成为被称为的进程。终端模拟器 . xterm、iTerm2、Terminal.app等,都是终端模拟器。

但是,应用程序和终端模拟器之间的通信机制保持不变 就像硬件终端一样。终端模拟器 模拟 硬件终端。这意味着,从应用程序的角度来看,如今与终端仿真器(例如 iTerm2 )的对话与 1979 年与真实终端(例如 DEC VT100 )的对话相同。该机制保持不变,因此为硬件终端开发的应用程序仍然可以与软件终端仿真器一起使用。

那么这种通信机制是如何工作的呢? UNIX 有一个子系统叫做 TTY 在内核中(TTY 代表电传打字机,这是最早的计算机终端形式,甚至没有屏幕,只有键盘和打印机)。您可以将 TTY 视为 通用驱动程序 用于终端。 TTY 从终端连接的端口(来自终端的键盘)读取字节,并将字节写入该端口(发送到终端的显示器)。

每个连接到计算机的终端(或计算机上运行的每个终端模拟器进程)都有一个 TTY 实例。因此,TTY 实例也称为 TTY 设备 (从应用程序的角度来看,与 TTY 实例交谈就像与终端设备交谈)。在使驱动程序接口(interface)作为文件可用的 UNIX 方式中,这些 TTY 设备显示为 /dev/tty*以某种形式,例如,在 macOS 上,它们是 /dev/ttys001 , /dev/ttys002 , 等等。

应用程序可以将其标准流(stdin、stdout、stderr)定向到 TTY 设备(实际上,这是默认设置,您可以使用 tty 命令找出您的 shell 连接到哪个 TTY 设备)。这意味着用户在键盘上输入的任何内容都会成为应用程序的标准输入,而应用程序写入其标准输出的任何内容都会发送到终端屏幕(或终端模拟器的终端窗口)。这一切都是通过TTY设备发生的,即应用程序只与内核中的TTY设备(这种类型的驱动程序)进行通信。

现在,关键点是:TTY 设备不仅仅是将每个输入字符传递给应用程序的标准输入。默认情况下,TTY 设备应用所谓的 行纪到接收到的字符。也就是说,它在本地缓存它们并解释删除、退格和其他行编辑字符,并且只有在收到回车或换行时才将它们传递给应用程序的标准输入,这意味着用户已经完成了整个输入和编辑线。

这意味着直到用户点击返回,getchar()在标准输入中看不到任何东西。就好像到目前为止没有输入任何内容。只有当用户点击回车时,TTY 设备才会将这些字符发送到应用程序的标准输入,其中 getchar()立即将它们读作。

从这个意义上说,getchar() 的行为没有什么特别之处。 .它只是在 stdin 中的字符可用时立即读取它们。您观察到的行缓冲发生在内核的 TTY 设备中。

现在到了有趣的部分:可以配置这个 TTY 设备。例如,您可以从带有 stty 的 shell 中执行此操作。命令。这允许您配置 TTY 设备应用于传入字符的线路规则的几乎所有方面。或者您可以通过将 TTY 设备设置为 来禁用任何处理。原始模式 .在这种情况下,TTY 设备会立即将每个接收到的字符转发到应用程序的 stdin,无需任何形式的编辑。

如果在 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 设备设置为原始模式,然后使用 getchar()在循环中从标准输入读取和打印字符。字符以十六进制和八进制表示法打印为 ASCII 代码。程序专门解读ETX字符(ASCII 代码 0x03)作为终止的触发器。您可以通过键入 Ctrl-C 在键盘上生成此字符。 .

关于c - getchar() 和标准输入,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/7741930/

相关文章:

c++ - 查找字节顺序指针类型转换的代码

c - 同时读取和写入变量

c - 数组与 memcpy 比较 vs 逐元素比较

c - 为什么我使用 fputc 编写的文件不会终止?

c - mvaddch 不会覆盖屏幕上的字符

c - 如何释放 C 中的 union 数组?

c - C 中的堆排序实现有什么问题?

c - 将数组指针传递给对数字进行排序的函数

c - 非IDE C开发环境

c - main 之前的段错误