【问题标题】:How to read from the terminal "keystrokes buffer"?如何从终端“击键缓冲区”中读取?
【发布时间】:2017-10-21 09:56:39
【问题描述】:

假设ping 命令正在运行,我在终端上键入一些内容,而ping 仍在运行。

现在当ping 终止并且bash 重新获得控制权时,bash 将在终端上打印我在ping 运行时键入的内容。这是一个显示我的意思的屏幕截图:

bash 是如何获得这些信息的?我确定它不是从stdin 得到的,因为当我输入"I typed this while ping was running" 时,我没有按Enter(所以stdin 是空的)。

因此,这些数据必须存储在“击键缓冲区”中,并且bash 从该缓冲区中读取。

我的问题是,bash 如何从这个缓冲区中读取数据(它调用了什么函数...)?

【问题讨论】:

  • Bash 没有 read 来自任何东西。您的 tty echos 您的击键(就像您键入命令时一样)ping 正在 subshel​​l 中运行,并且当它完成时,并将控制权返回给父母,您输入的内容就在您面前。如果您在ping 期间键入echo foo 并点击return,那么您将在空的PS1 提示上方的行上看到foo。 (要清楚,在 输入缓冲区return bash 确实从该缓冲区读取您的命令,但这与您输入后看到的内容无关)
  • @David C. Rankin 是的,tty 回显了我的击键,但是回显的击键与ping 的输出混合在一起。 "paul@paul-laptop:~$" 之后存在的 "I typed this while ping was running"bash (tty i> 只回显我的击键一次而不是两次)。
  • 您得到本地回显,并且该命令仍然在您的PS1 提示符前面,直到您点击enter,尽管它也在ping 期间回显到终端(您必须这样做)一个ping -c x shomehost 而不是ping somehost 然后ctrl+c 这将清空输入缓冲区。)
  • @David C. Rankin 如果bash 不是做回显的人,那么为什么当我使用sh 时,没有发生第二次回显?
  • @DavidC.Rankin 回答 1),因为readline 把它放在那里。

标签: c linux bash terminal


【解决方案1】:

bash 中没有特殊的“读取终端队列”机制。这只是普通的readwrite 系统调用。

在 Linux 上,“tty 设备”总是被缓冲的。如果您的输入速度快于接收程序读取输入的速度,则字符不会被丢弃到位桶中;它们被放置在终端设备的输入队列中,可以通过read 系统调用从中检索它们。

read 调用具有原型 ssize_t read(int fd, void *buf, size_t count);。当终端驱动程序决定应该返回read 调用时,它会从终端的输入队列中删除count 字节并将它们放入buf。如果队列不包含count字节,则读取队列中的所有字节;如果它包含超过count 字节,则剩余字节保留在队列中以供下一个read

count对终端本身的运行没有影响; read(0, buf, 1) 不会导致 read 在一个字节可用时返回。它只限制放入buf 的字节数。

有。但是,一些控制设置会影响read 呼叫的处理方式。在这种情况下,最重要的是ICANON 标志。如果设置了此标志(“规范模式”),则在输入 NL 字符之前,不会唤醒在 tty 上等待read 系统调用的进程。 (实际上,有四个字符会唤醒进程:NL、EOL、EOL2 和 EOF。)在规范模式下,内核驱动程序还处理一些行编辑字符,例如 ERASE 字符。 (所有这些字符都可以通过termios 进行配置,所以当我说“NL 字符”时,我的意思是“当前配置为 NL 的字符。默认情况下,NL 字符是“Enter”键,EOF 是 control-D。 )

如果未设置 ICANON,则终端处于非规范模式,并应用 VMIN 和 VTIME 设置。 VMIN 是在进程被唤醒之前必须存在的最小字符数; VTIME 是终端驱动程序在放弃和唤醒进程之前等待输入的最短时间。如果同时设置了 VMIN 和 VTIME,则内核驱动程序将(无限期地)等待第一个字符,然后将等待每个连续字符的 VTIME,直到读取 VMIN 字符。如果 VMIN 和 VTIME 均未设置,read 调用将始终立即返回。

独立于规范模式设置,您还可以配置终端的回显行为。在最简单的配置中,您要么设置 ECHO,要么不设置。如果设置了 ECHO,终端驱动程序在键入时会回显可打印字符。如果未设置 ECHO,则终端驱动程序不会回显字符。默认情况下,设置了 ECHO。

Bash 通常使用 readline 库来处理终端输入。您可以告诉 bash 不要使用 readline,在这种情况下行为会有所不同;我只会用readline来描述正常的情况。

当 bash 处于活动状态并控制终端时,readline 将终端置于非规范模式并关闭回显。它将 VMIN 设置为 1,因此 read 调用将在输入单个字符后立即返回,然后进入类似于以下循环的内容:

while (1) {
  ssize_t nread = read(0, buf, 1);
  if (nread && isprint(buf[0]))
    write(1, buf, 1);  /* Echo the character */
  else {
    /* Lots of complicated logic to handle other characters */
  }
}

就在 bash 让子进程执行之前,它通过将终端放回规范模式并打开 echo 来重置终端。除非正在执行的命令改变了终端模式,否则它会一直保持这种状态,直到命令退出,此时 bash 重新获得控制权,关闭规范模式和回显,并返回到 readline 循环。

因此假设正在执行的命令不会更改终端设置,并且会执行一段时间(如您问题中的ping 示例)。当ping 正在执行时,终端处于规范模式,回显打开,没有人从终端设备读取。因此,您键入的任何内容都将被放入终端队列并回显。 “任何东西”包括通常会终止read 的东西,例如 Enter 键。那是因为没有read 可以终止。

ping 最终完成时,bash 将终端更改回非规范模式并关闭回显,这样终端驱动程序就不会回显您现在键入的字符。然后它调用 readline 启动上面的循环。但是,此时终端队列中有一堆东西,远远多于 VMIN 设置所需的一个字符。所以read 调用立即返回一个字符,然后该循环中的write 调用回显刚刚读取的字符。当然,终端驱动程序已经回显了那个字符,但这个事实并没有神奇地记录在字符的二进制编码中;它只是一个普通的字符,所以它会被第二次回显。这一直持续到终端队列被清空或您之前键入的某些字符需要 readline 的注意。如果终端队列只包含一些普通的可打印字符,它们都会得到回显,并且下一个 read 调用将阻塞,直到您输入某些内容。

【讨论】:

    【解决方案2】:

    这是readline library 的事情(这里有一个more approachable page 关于它)。

    您可以通过 Python 看到这一点,它在大多数发行版上都使用 readline 支持编译:

    >>> import time
    >>> time.sleep(5)
    I am typing this during the sleep>>> I am typing this during the sleep
    

    不过,我也碰巧有一个不支持 readline:

    >>> import time
    >>> time.sleep(5)
    I am typing this during the sleep>>> 
    

    (这也可以通过cat | python -i实现,因为cat不使用readline,而python禁用readline是因为它的输入不是终端。)

    我的猜测是:

    1. Readline 禁用缓冲(可能类似于this)。这样,它就可以接收所有输入的字符。
    2. Readline 禁用将字符回显到终端,并接管控制权。 (Here 是一种可能的方式。)
    3. Bash 禁用了 readline,所以 echo 可以做它的事情。
    4. Echo 会忽略这些字符,因此在输入后它们仍在stdin 中。
    5. Readline 有助于收回控制权,禁用缓冲/回显键入的字符(不过对于已经键入的字符来说太迟了),并处理来自 stdin 的字符。

    TL;DR:它们会自动回显一次,然后由 readline 库再次回显。

    【讨论】:

    • “Readline 禁用缓冲(可能是这样的,但使用标准输入)。这样,它可以接收输入的所有字符。” 你的意思是标准输入缓冲,还是 tty 设备 (/dev/tty1) 所做的缓冲? (如果它禁用了 tty 设备的缓冲,那么就像它使终端处于原始模式(这意味着可以立即发送击键而无需用户按 Enter))。跨度>
    • @paul 它与termios 中的一些设置混淆了——这对我来说现在有点太复杂了,但你可以阅读所有关于它的内容here。快速的谷歌搜索表明,最好的方法是通过 termios(here 是一个更简单的例子)。
    • 我写了一个答案。
    • setvbuf 仅用于输出缓冲区,虚拟缓冲区完全在标准C库中实现,这是一个应用层。输入流没有等效项。
    • @rici 谢谢!如果您有更多信息、更好的解释或任何更正,请提出编辑建议!我承认这不是我的专业领域,所以请提出修改建议(甚至自己回答)。
    【解决方案3】:

    这就是我认为会发生的事情:

    1. 当 bash 运行时,它会禁用 tty 设备行缓冲,它还会禁用 tty 设备回显(现在 bash 负责回显它收到的所有内容)。
    2. bash 执行ping 之前,bash 重新启用 tty 设备行缓冲和 tty 设备回显。然后bash 执行ping
    3. 现在我输入"I typed this while ping was running",而ping 正在显示其输出。 tty 设备会将这个字符串回显到终端窗口,但这个字符串现在缓冲在 tty 设备中。
    4. ping 终止,bash 重新获得控制权。
    5. bash 重新禁用 tty 设备行缓冲和 tty 设备回显。禁用 tty 设备行缓存会导致 tty 设备行缓存刷新到bashbash 会在终端窗口显示。

    【讨论】:

      猜你喜欢
      • 2015-11-04
      • 2011-01-11
      • 1970-01-01
      • 2011-07-16
      • 1970-01-01
      • 2012-03-15
      • 2018-03-05
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多