getchar()和stdin

yba*_*kos 7 c

一个相关的问题在这里,但我的问题是不同的.

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

我的问题是缓冲,stdin和getchar()行为.鉴于经典的K&R示例:

#include <stdio.h>

main()
{
    int c;

    c = getchar();
    while (c != EOF) {
        putchar(c);
        c = getchar();
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

假设程序运行并且用户键入"凤尾鱼".

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

我花了一点时间使用c源代码,但似乎这更像是stdin而不是fgetc()的行为神器.

我错了吗?感谢您的见解.

wei*_*eld 15

您观察到的行为与 C 和 无关getchar(),而是与操作系统内核中的电传 (TTY) 子系统有关。

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

在此处输入图片说明

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

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

但是,应用程序和终端仿真器之间的通信机制与硬件终端的通信机制保持一致。终端模拟器模拟硬件终端。这意味着,从应用程序的角度来看,今天与终端仿真器(例如iTerm2)交谈的工作方式与1979 年与真实终端(例如DEC VT100)交谈的工作原理相同。这种机制保持不变,因此应用程序开发对于硬件终端,仍然可以与软件终端模拟器一起使用。

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

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

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

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

这意味着在用户点击return 之前getchar()在标准输入中看不到任何内容。就好像到目前为止没有输入任何内容。只有当用户点击return 时,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);
}
Run Code Online (Sandbox Code Playgroud)

该程序将当前进程的 TTY 设备设置为原始模式,然后使用getchar()循环从 stdin 读取和打印字符。字符以十六进制和八进制表示法打印为 ASCII 代码。程序专门将ETX字符(ASCII 码 0x03)解释为触发终止。您可以通过键入在键盘上生成此字符Ctrl-C


ott*_*t-- 6

getchar()的输入是行缓冲的,输入缓冲区是有限的,通常是4 kB.你最初看到的是你输入的每个角色的回声.按ENTER键时,getchar()开始将字符返回到LF(转换为CR-LF).当你持续按下没有LF的键一段时间后,它会在4096个字符后停止回显,你必须按ENTER才能继续.


Fre*_*Foo 5

我知道getchar()最终会调用fgetc(stdin).

不必要。getchargetc可能扩展到从文件读取的实际过程,实现fgetc

int fgetc(FILE *fp)
{
    return getc(fp);
}
Run Code Online (Sandbox Code Playgroud)

嘿,缓冲区中没有任何内容,所以让 stdin 收集用户输入的内容。[...] 看来这更像是一种行为产物,stdin而不是fgetc()

我只能告诉你我所知道的,这就是 Unix/Linux 的工作原理。在该平台上, a FILE(包括指向的内容stdin)保存一个文件描述符( an int),该文件描述符被传递到操作系统以指示从哪个输入源获取FILE数据,以及一个缓冲区和一些其他簿记内容。

“收集”部分意味着“read在文件描述符上调用系统调用以再次填充缓冲区”。不过,这因 C 的实现而异。