你观察到的行为与 C 和 getchar()
无关,而与操作系统内核中的电传打字机 (TTY) 子系统有关。
为此,您需要了解进程如何从键盘获取输入,以及它们如何将输出写入终端窗口(我假设您使用UNIX,并且以下解释特别适用于UNIX,即Linux、macOS等):
![enter image description here](https://istack.dev59.com/DXjqx.webp)
上图中标题为“Terminal”的方框是您的终端窗口,例如 xterm、iTerm 或 Terminal.app。在旧时代,终端是单独的硬件设备,由键盘和屏幕组成,它们通过串行线 (RS-232) 连接到一台(可能是远程的)计算机上。在终端键盘上键入的每个字符都会通过该行发送到计算机并被连接到终端的应用程序消费掉。应用程序生成的每个字符作为输出发送到相同的行上,然后由终端显示在屏幕上。
现在,终端不再是硬件设备,而是移至计算机内部,成为被称为终端仿真器的进程。xterm、iTerm2、Terminal.app 等都是终端仿真器。
然而,应用程序和终端仿真器之间的通信机制与硬件终端相同。终端仿真器模拟硬件终端。这意味着,从应用程序的角度来看,今天与终端仿真器(例如iTerm2)交谈的工作方式与 1979 年与真正的终端设备(例如DEC VT100)交谈的方式相同。保留了这种机制,以便为硬件终端开发的应用程序仍可与软件终端仿真器配合使用。
那么,这个通信机制是如何工作的?UNIX 内核中有一个名为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设备对接收到的字符应用所谓的行规则。这意味着它会本地缓冲它们并解释删除、退格和其他行编辑字符,并且只有在接收到回车符或换行符时才将它们传递给应用程序的标准输入,这意味着用户已经完成了输入和编辑整行。
也就是说,在用户按下返回键之前,getchar()
在stdin中看不到任何东西。就好像到目前为止什么也没有输入一样。只有当用户按下返回键时,TTY设备才将这些字符发送到应用程序的标准输入,getchar()
随即读取。
从这个意义上讲, getchar()
的行为没有什么特别之处。它只是在stdin中立即读取可用的字符。您观察到的行缓冲发生在内核中的TTY设备中。
现在进入有趣的地方:可以配置此TTY设备。例如,您可以使用 stty
命令从shell中进行配置。这使您可以配置TTY设备应用于传入字符的行规则的几乎所有方面。或者,通过将TTY设备设置为原始模式,可以禁用任何处理。在这种情况下,TTY设备会将每个接收到的字符立即转发到应用程序的标准输入而不进行任何形式的编辑。
如果您在TTY设备中启用原始模式,则会发现 getchar ()
立即 接收到您在键盘上输入的每个字符。以下C程序演示了这一点:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#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));
tcgetattr(STDIN_FILENO, &tty_opts_backup);
cfmakeraw(&tty_opts_raw);
tcsetattr(STDIN_FILENO, TCSANOW, &tty_opts_raw);
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");
tcsetattr(STDIN_FILENO, TCSANOW, &tty_opts_backup);
}
该程序将当前进程的TTY设备设置为原始模式,然后使用getchar()
循环读取和打印来自stdin的字符。这些字符以十六进制和八进制表示法打印为ASCII代码。该程序特别将ETX
字符(ASCII代码0x03)解释为终止触发器。您可以通过键入Ctrl-C
在键盘上生成此字符。
fflush(stdin)
存在未定义行为!我现在无法立即检查... - pmg