getchar()和stdin

时间:2011-10-12 14:45:26

标签: c

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

但是,我想更多地了解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缓冲区中没有任何内容,请让OS接受用户输入,直到按下[enter]。然后返回缓冲区中的第一个字符。

假设程序已运行且用户输入“凤尾鱼”。

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

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

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

3 个答案:

答案 0 :(得分:6)

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

答案 1 :(得分:3)

  

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

不一定。 getchargetc也可以扩展到从文件中读取的实际过程,fgetc实现为

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

嘿,缓冲区中没有任何东西,所以让stdin收集用户输入的内容。   [...]这似乎更像是stdin而不是fgetc()的行为神器。

我只能告诉你我所知道的,这就是Unix / Linux的工作方式。在该平台上,FILE(包括stdin指向的东西)包含传递给操作系统的文件描述符(int),以指示从哪个输入源{{ 1}}获取数据,加上缓冲区和其他一些簿记内容。

“聚集”部分则表示“在文件描述符上调用FILE系统调用以再次填充缓冲区”。但是,根据C的实现情况,这会有所不同。

答案 2 :(得分:2)

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

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

enter image description here

上图中标题为“ Terminal”的框是您的终端窗口,例如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设备(实际上,这是默认设置),并且您可以找出您的外壳与{{1}连接到的TTY设备}命令)。这意味着,无论用户在键盘上键入什么内容,都将成为应用程序的标准输入,并且应用程序将其标准输出所写的内容发送到终端屏幕(或终端仿真器的终端窗口)。所有这些都是通过TTY设备进行的,也就是说,应用程序仅与内核中的TTY设备(这种类型的驱动程序)进行通信。

现在,关键点是:TTY设备所做的不仅仅是将每个输入字符传递到应用程序的标准输入。默认情况下,TTY设备对接收到的字符应用所谓的行规。这意味着,它将在本地缓冲它们并解释 delete backspace 和其他行编辑字符,并且仅在收到 carriage时将它们传递给应用程序的标准输入返回换行,这表示用户已完成输入和编辑整行。

这意味着,直到用户按下 return 为止,tty不会在stdin中看到任何内容。好像到目前为止没有输入任何内容。仅当用户按下 return 时,TTY设备才会将这些字符发送到应用程序的标准输入,其中getchar()立即将其读取为。

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

现在有趣的部分:可以配置此TTY设备。例如,您可以使用getchar()命令从外壳中执行此操作。这使您可以配置TTY设备应用于输入字符的行规的几乎每个方面。或者,您可以通过将TTY设备设置为原始模式来禁用任何处理。在这种情况下,TTY设备会立即将接收到的每个字符立即转发到应用程序的stdin,而无需进行任何形式的编辑。

如果在TTY设备中启用了原始模式,您将看到stty 立即会收到您在键盘上键入的每个字符。以下C程序演示了这一点:

getchar()

程序将当前进程的TTY设备设置为原始模式,然后使用#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); } 循环读取和打印来自stdin的字符。字符以十六进制和八进制表示的ASCII码形式打印。该程序特别将getchar()字符(ASCII代码0x03)解释为终止触发。您可以通过输入ETX在键盘上产生此字符。