解密TTY
本文内容来自The TTY demystified ,讲述了*NIX系统中TTY的历史与工作原理,看完后解决了我很多疑惑,于是做此翻译,与大家分享。
译者:李秋豪江家伟
审校:
V1.0 Sun May 13 12:42:01 CST 2018
一直以来,TTY子系统都是Linux/Unix设计中的一个关键点。不幸的是,这种重要性通常都被忽略了,并且也很难找到相关的介绍性文章。我认为,对Linux中TTYs的基础知识理解应是每一个开发人员和高级使用者所必备的。
注意:你将阅读到的东西并不是那么“优雅”。事实上,尽管在用户角度看非常实用,TTY子系统是由很多繁杂的东西和特殊情况组成的。为了理解它们的由来,我们必须回到过去:
历史
在1869年,证券报价机(stock ticker)被发明了。这是一台由打字机,一对长电缆和一个自动收录机打印机组成的电动机械机器,其目的是长距离实时传播股票的价格。这个概念逐渐演变成更快的基于ASCII的电传机(teletype)。Teletypes曾经在世界各地的大型网络中连接,并被称为Telex,其主要用于传输商业电报,但此时尚未连接到任何计算机。
与此同时,计算机(虽然还是又笨重又昂贵)也开始支持多任务处理了,即能够实时和多个用户进行交互。当命令行最终取代了古老的批处理模型后,teletypes被用作输入和输出设备,因为它们在市场上很容易买到。
但是在市场上有许多种电传机,它们的模型都略有不同,因此需要计算机在软件层形成兼容。在UNIX世界中,使用的方法是让操作系统内核处理所有底层细节,例如字长,波特率,流量控制,奇偶校验,用于基本行编辑(rudimentary line)的控制代码等等。而视频终端(例如20世纪70年代后期出现的VT-100等)的光标移动,彩色输出和其他高级功能则留给了应用层。
现在,物理电传机和视频终端实际上已经灭绝了。除非你在访问博物馆或者你是一个硬件爱好者,否则你看到的所有TTY都是模拟视频终端,即软件仿真出来的终端。但我们即将看到,这些远古的知识依然潜藏在现代TTY设计之中。
用例
如下图所示,用户在终端(terminal)打字(物理电传机),该终端通过一对电缆连接到计算机上的UART(通用异步接收器和发送器)。操作系统中有一个UART驱动程序,用于管理字节的物理传输,包括奇偶校验和流量控制。在一个原始的系统中,UART驱动程序会将传入的字节直接传送给某个应用程序进程,但是这种方法将缺乏以下基本特征:
行编辑。大多数用户都会在输入时犯错,所以退格键会很有用。这当然可以由应用程序本身来实现,但是根据UNIX设计“哲学”,应用程序应尽可能保持简单。为了方便起见,操作系统提供了一个编辑缓冲区和一些基本的编辑命令(退格,清除单个单词,清除行,重新打印),这些命令在行规范(line discipline)内默认启用。高级应用程序可以通过将行规范设置为原始模式(raw mode)而不是默认的成熟或准则模式(cooked and canonical)来禁用这些功能。大多数交互程序(编辑器,邮件客户端,shell,及所有依赖curses
或readline
的程序)均以原始模式运行,并自行处理所有的行编辑命令。行规范还包含字符回显和回车换行(译者注:\r\n
和 \n
)间自动转换的选项。如果你喜欢,可以把它看作是一个原始的内核级sed(1)
。
另外,内核提供了几种不同的行规范。一次只能将其中一个连接到给定的串行设备。行规范的默认规则称为N_TTY(drivers/char/n_tty.c
,如果你想继续探索的话)。其他的规则被用于其他目的,例如管理数据包交换(ppp,IrDA,串行鼠标),但这不在本文的讨论范围之内。
会话(Session)管理。用户可能想要同时运行多个程序,并且一次只与其中一个交互。如果一个程序进入无限循环,用户可能想要终止或挂起它。在后台启动的程序应该能够独立运行,直到它们尝试向终端写入(被挂起)。同样,用户的输入应该指向前台程序。对于这些功能,操作系统是在TTY驱动程序( TTY driverdrivers/char/tty_io.c
)中实现的。
在操作系统中,如果已经进程有执行上下文,我们就说它是“活着的”(有一个执行上下文),这也意味着它可以独立执行操作。而TTY驱动程序不是“活”的; 在面向对象的术语中,TTY驱动程序是被动对象(passive object)。它有一些数据字段和一些方法,但让它做某事的唯一方法是当它的某个方法从别的进程的上下文或内核中断处理程序中调用时。行规范(line discipline)同样是一个被动对象。
现在把它们放在一起看,UART驱动,行规范和TTY驱动这个三元组就可以被称为TTY设备,即我们常说的TTY。用户进程可以通过在/dev
下操作相应的设备文件来影响任何TTY设备的行为。由于对设备文件写入权限是必需的,因此当用户登录特定的TTY时,该用户必须成为设备文件的所有者——这通常由login(1)
程序完成,该程序以root权限运行。
上图中的物理电线也可以是长途电话线路(Modem),除了系统必须处理调制解调器挂断的情况,这并没有带来其他的改变:
让我们继续讨论典型的桌面系统。下图是Linux控制台的工作原理:
在上图中,TTY驱动和行规范的行为与前面的示例类似,但不再有UART或物理终端。相反,软件仿真出视频终端(字符和图形字符属性帧缓冲器的复杂状态机),并最终被渲染到VGA显示器。
如果我们在用户空间也进行终端仿真,情况会变得更加灵活(和抽象)。下图是xterm(1)
及其克隆的工作方式:
为了便于将终端仿真移入用户空间,同时仍保持TTY子系统(会话管理和行规范)的完整,伪终端被发明了出来(pseudo terminal 或 pty )。你可能已经猜到,当你开始在伪终端中运行伪终端时,事情变得更加复杂,例如 screen(1)
或 ssh(1)
。
现在让我们退一步看看所有这些东西是如何和进程联系起来的。
进程
Linux进程可以处于下面状态之一:
标志位 | 说明 |
---|---|
D | 不可中断睡眠(等待某个事件) |
S | 可中断睡眠(等待一些事件或者信号) |
T | 停止(收到了工作管理信号或者进程正在被调试器追踪) |
Z | 僵尸进程(被它的父进程终止但是没有被回收的进程) |
R | 运行或者可运行(在运行队列中) |
通过运行 ps l
, 你可以看到哪个进程正在运行,以及哪个进程正在睡眠。如果一个进程处于睡眠状态, WCHAN
列("wait channel", 等待队列的名字)将会告诉你这个进程正在等待哪个内核事件。
$ ps l F UID PID PPID PRI NI VSZ RSS WCHAN STAT TTY TIME COMMAND 0 500 5942 5928 15 0 12916 1460 wait Ss pts/14 0:00 -/bin/bash 0 500 12235 5942 15 0 21004 3572 wait S+ pts/14 0:01 vim index.php 0 500 12580 12235 15 0 8080 1440 wait S+ pts/14 0:00 /bin/bash -c (ps l) >/tmp/v727757/1 2>&1 0 500 12581 12580 15 0 4412 824 - R+ pts/14 0:00 ps l
"wait
"等待队列对应于系统调用 wait(2)
,因此这个队列中的进程的子进程不论什么时候改变了状态,它们都会被移入运行状态。有两种睡眠状态:可中断睡眠和不可中断睡眠。可中断睡眠(最常见的情况)意味着当进程在等待队列中时,它实际上也可能由于收到了一个信号而被移入运行状态。如果你深入到内核源码中,你将会发现每个处理等待事件的内核源码都会检查在schedule()调用返回之后是否有待处理的信号,如果有,就从系统调用wait(2)
中返回。
在上面列出的 ps
结果中, STAT
列展示了每个进程的当前状态。这一列中可能会显示一个或多个属性或标记:
s | 这个进程是会话领导 |
---|---|
+ | 这个进程是前台进程组的一员 |
这些属性被用于工作管理。
译者注:我之前翻译过两篇有关于进程标志的文章,可参考
Linux 进程状态标识 Process State Definition
Linux 可运行进程 Runnable Process Definition
工作与会话管理
当你按下 ^Z
挂起程序或者使用 &
在后台运行程序时,工作管理就发生了。一个工作(job)等同于一个进程组。shell内置的命令如 jobs
, fg
和 bg
可以用来管理一个会话(session)中的所有工作。每一个会话是由一个会话领导(session leader),即shell来管理的,它会利用复杂的协议,例如信号和一些系统调用和内核打交道。
下面的例子解释了进程、工作、会话之间的关系。
下面的shell交互...
...对应这些进程...
...和这些内核数据结构
- TTY 驱动 (/dev/pts/0).
Size: 45x13 Controlling process group: (101) Foreground process group: (103) UART configuration (忽略d, since this is an xterm): Baud rate, parity, word length and much more. Line discipline configuration: cooked/raw mode, linefeed correction, meaning of interrupt characters etc. Line discipline state: edit buffer (currently empty), cursor position within buffer etc.
- pipe0
Readable end (connected to PID 104 as file descriptor 0) Writable end (connected to PID 103 as file descriptor 1) Buffer
其中基本的思想是每个管道都是一项工作,因为管道中的每个进程都应该被同时进行操作(停止,恢复,终止)。这也是为什么 kill(2)
允许你发送信号到整个进程组。默认情况下, fork(2)
将新创建的子进程放置在与其父进程相同的进程组中,例如,键盘上的 ^C
会影响父进程和子进程。但是,作为会话领导责任的一部分,每次启动管道时,shell都会创建一个新的进程组。
TTY驱动程序会记录前台进程组ID(PID),但这只能以被动方式进行。会话领导必须在必要时主动更新此信息。同样,TTY驱动程序会记录连接终端的属性(例如窗口大小),但这些信息必须由终端仿真程序甚至用户主动更新。
正如在上图中所看到的,几个进程将 /dev/pts/0
作为它们的标准输入。但只有前台工作 ls | sort
才会接收来自TTY的输入。同样,只有前台工作才被允许写入TTY设备(默认配置下)。如果cat
进程试图写入TTY,内核将使用信号将它挂起。
信号控制
现在让我们更近距离地看看内核中的TTY驱动、行规范和UART驱动是如何和用户态进程交互的。
UNIX文件,包括TTY设备文件,可以被读和写,并且由于许多TTY相关的操作都已经被定义,可以使用神奇的 ioctl(2)
系统调用(UNIX的“瑞士军刀”)进行进一步操作。但是,ioctl
请求必须在进程内被初始化,因此它们不能在内核需要和应用进行异步通信的场景下被使用。
在The Hitchhiker's Guide to the Galaxy(银河系漫游指南)中,Douglas Adams提到了一个“死星”,上面居住这一群消沉的人类和某种长着尖牙的动物。这些动物通过狠狠地咬人类的大腿来和人类交流(译者:喵喵喵?)。这和UNIX惊人地相似:在UNIX中,内核通过发送“瘫痪或者致命”的信号给用户进程来和进程通信。一些进程可能能够拦截一些信号,并且尝试调整适应当前的情况,但是大多数进程不会这么做。
因此信号是一个“粗暴”的机制,它允许内核和进程进行异步通信。UNIX中的信号定义是不规整或者不统一的;相反,每个信号都是独特的,我们必须单独研究它们。
你可以使用命令 kill -l
来看看你的系统实现了哪些命令。结果看起来像下面这样:
$ kill -l 1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM 16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ 26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR 31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3 38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8 43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13 48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12 53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7 58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2 63) SIGRTMAX-1 64) SIGRTMAX
正如你看到的,信号被从1开始的数字编号。然而当它们被在掩码中(例如在ps -s
的输出里)被使用时,最低有效位对应信号1。
这篇文章将会关注以下信号: SIGHUP
, SIGINT
, SIGQUIT
, SIGPIPE
, SIGCHLD
,SIGSTOP
, SIGCONT
, SIGTSTP
, SIGTTIN
, SIGTTOU
以及SIGWINCH
.
SIGHUP
- 默认操作: 终止
- 可能的操作: 终止, 忽略, 函数调用
当检测到挂断(hangup)条件时,UART驱动会将SIGHUP
发送到整个会话。通常情况下,这会杀死所有进程。某些程序(如 nohup(1)
和 screen(1)
)会从其会话(和TTY)中分离,以便其子进程不会注意到挂断。
SIGINT
- 默认操作: 终止
- 可能的操作: 终止, 忽略, 函数调用
如果输入流中出现交互式注意( interactive attention )字符(通常为 ^C
,其代码为ASCII码3),那么SIGINT
就会由TTY驱动发送到当前的前台工作,除非此配置已被关闭。任何有权访问TTY设备的人都可以更改交互式注意字符并开关此配置; 此外,会话管理器会跟踪每个工作的TTY配置,并在有工作切换时更新TTY。
SIGQUIT
- 默认操作: 内核转储(core dump)
- 可能的操作: 内核转储, 忽略, 函数调用
SIGQUIT
的工作方式和 SIGINT
相似, 但是使用的字符是 ^\
并且默认操作不同。
SIGPIPE
- 默认操作: 终止
- 可能的操作: 终止, 忽略, 函数调用
内核会给每一个试图往没有读取者的管道中写数据的进程发送 SIGPIPE
信号。 这是很有用的,因为没有这个信号的话,类似 yes | head
这样的工作就永远不会停止了。
SIGCHLD
- 默认操作: 忽略
- 可能的操作: 忽略, 函数调用
当进程死亡或更改状态(停止/继续)时,内核会向其父进程发送一个 SIGCHLD
。 SIGCHLD
信号携带着终止进程的附加信息,即进程标识,用户标识,退出状态(或终止信号)以及一些执行时间的统计信息。会话领导(shell)使用这个信号追踪其工作。
SIGSTOP
- 默认操作: 挂起
- 可能的操作: 挂起
该信号将无条件地挂起接收者,即其信号动作不能被重新配置。要注意的是,在工作控制期间,SIGSTOP
不会由内核发送。相反,^Z
通常会触发一个 SIGTSTP
,它可以被应用程序拦截。然后应用程序可以进行例如将光标移动到屏幕底部等操作,然后使用SIGSTOP
将自己置于睡眠状态。
SIGCONT
- 默认操作: 唤醒
- 可能的操作: 唤醒, 唤醒 + 函数调用
SIGCONT
将“反挂起”(un-suspend,continue)一个停止的进程。当用户调用fg
命令时,它会由shell发送出去。由于 SIGSTOP
不能被应用程序拦截,因此意料之外的SIGCONT
信号可能表明该进程在某段时间之前被挂起,然后被唤醒。
SIGTSTP
- 默认操作: 挂起
- 可能的操作: 挂起, 忽略, 函数调用
SIGTSTP
与 SIGINT
和 SIGQUIT
的工作原理相似,但是它使用的是 ^Z
字符,并且默认的操作是挂起进程。
SIGTTIN
- 默认操作: 挂起
- 可能的操作: 挂起, 忽略, 函数调用
如果一个后台工作中的进程尝试从TTY设备中进行读取,TTY会向整个工作(组)发送一个 SIGTTIN
信号,这通常会挂起这个工作。
SIGTTOU
- 默认操作: 挂起
- 可能的操作: 挂起, 忽略, 函数调用
如果一个后台工作中的进程尝试向TTY设备中进行写入,TTY会向整个工作(组)发送一个 SIGTTIN
信号,这通常会挂起这个工作。这种行为可以通过配置TTY关闭。
SIGWINCH
- 默认操作: 忽略
- 可能的操作: 忽略, 函数调用
如前所述,TTY设备会记录终端的窗口大小,但这些信息需要手动更新。只要发生这种更新,TTY设备就会向前台工作发送 SIGWINCH
。行为良好的交互式应用程序(例如编辑器)会对此作出反应,从TTY设备获取新的终端窗口大小并重绘GUI。
译者注:我之前翻译过一篇有关于进程和信号的文章,可参考
Linux 进程与信号的概念和操作
一个例子
假设你正在编辑(基于终端的)编辑器中的文件。此时光标位于屏幕中间的某个位置,编辑器正在执行一些任务,例如对大文件执行搜索和替换操作。现在你按 ^Z
,由于行规范已被配置为拦截此字符(^Z
是一个单字节,ASCII码为26),因此你无需等待编辑器完成其任务然后从TTY设备开始读取。相反,行规范子系统会立即将 SIGTSTP
发送到前台进程组。该进程组包含编辑器以及由其创建的任何子进程。
编辑器为 SIGTSTP
安装(install)了一个信号处理程序,因此内核将程序执行流转移到信号处理程序代码中。通过将相应的控制序列写入TTY设备,该代码将光标移动到屏幕的最后一行。由于编辑器仍处于前台,控制序列按要求发送。随后编辑器会将 SIGSTOP
发送到其自己的进程组(正如上节信号中说的那样)。
编辑器现在已经停止,SIGCHLD
信号向会话领导通告这个事件,其中包括该进程的ID。当前台工作中的所有进程都被挂起时,会话领导从TTY设备读取当前配置,并将其存储起来以供以后使用。会话领导继续使用 ioctl
调用将其自身安装为TTY的当前前台进程组。然后,它会打印类似 "[1]+ Stopped" 的内容,以通知用户工作已暂停。
此时, ps(1)
会告诉您编辑器进程处于停止状态(“T
”)。如果我们试图使用内置shell命令bg
或使用 kill(1)
向进程发送 SIGCONT
来唤醒它,编辑器将开始执行其 SIGCONT
信号处理程序。而该处理程序会尝试通过写入TTY设备来重新绘制编辑器的GUI界面。但现在编辑器是一个后台工作,TTY设备将不允许它进行写入。所以,TTY会给编辑器发送 SIGTTOU
信号,令其再次停止。这个事件将通过使用 SIGCHLD
传递给会话领导(shell),而shell会再次向终端写入“[1] + Stopped”。
但是,当我们键入fg
时,shell首先恢复先前保存的行规范配置。它通知TTY驱动编辑器工作应该从现在起作为前台工作。最后,它向进程组发送一个SIGCONT
信号。编辑器试图重绘它的GUI,这次它不会被SIGTTOU
中断,因为它现在是前台工作的一部分。
译者注:
流控制与I/O阻塞
在 xterm
中运行 yes
,你会看到很多“y
”出现在你眼前。自然,yes
进程能够很快的产生y
,以至于xterm
来不及进行帧缓冲区更新,与X服务器通信(译者注:X Window System)以便滚动窗口等操作。那么这些程序是如何进行配合的呢?
答案在于I/O阻塞。伪终端只能在其内核缓冲区内保存一定数量的数据,当该缓冲区满并且 yes
尝试调用 write(2)
时,write(2)
将被阻止,并将yes
进程移至可中断的睡眠状态,直到xterm
能够读取缓冲中的字节。
如果TTY连接到串行端口,也会发生同样的情况。假设 yes
能够以比9600波特的速率传输数据,但是如果串行端口被限制在低的多速度上,内核缓冲区很快就会被填满,并且任何后续的 write(2)
调用都会导致进程睡眠(或收到返回的错误号 EAGAIN
,如果进程要求非阻塞I/O的话)。
如果我告诉过你,即使内核缓冲区中还有剩余空间,也可以主动地将TTY置于阻塞状态,更进一步的说,每个试图 write(2)
到TTY的进程都会自动阻塞。那么这种功能的用途是什么?
假设我们正在以9600波特率的速度与一些旧的VT-100通信。我们刚刚发送了一个复杂的控制序列,要求终端滚动显示。此时,终端会因执行滚动操作无法以9600波特的全速率接收新数据。实际上,UART仍然以9600波特运行,但终端中没有足够的缓冲空间来保持接收字符。现在就是将TTY置于阻塞状态的好时机。但是,我们该如何从终端做到这一点?
我们已经看到,TTY设备可以被配置为给某些数据字节特殊的处理。例如,在默认配置中,收到的 ^C
字节不会通过read(2)
传递给应用程序,而是会将 SIGINT
信号传递到前台工作。类似地,可以将TTY配置为对停止流和开始流做出反应,通常分别是 ^S
(ASCII码19)和 ^Q
(ASCII码17)。旧的硬件终端会自动传输这些字节,并期望操作系统相应地调节其数据流。这被称为流控制,这就是为什么当你偶然按下 ^S
时,你的xterm
会“锁定”。
这里有一个重要的区别:写入由于流控制而停止的TTY,或者由于缺少内核缓冲区空间,只会阻塞你的进程,而从后台工作中写入TTY将导致SIGTTOU
暂停整个进程组。我不知道为什么UNIX的设计师必须发明 SIGTTOU
和 SIGTTIN
,而不是仅仅依靠I/O阻塞,但我最好的猜测是负责工作控制的TTY驱动是为了监视和操纵整个工作——而不是其中的单个进程。
配置TTY设备
为了找出你的shell调用的控制TTY,你可以使用前面说过的ps l
,或者运行tty(1)
命令。
进程可以使用 ioctl(2)
读取或修改打开的TTY设备的配置。 该API在 tty_ioctl(4)
中有描述。 由于它是Linux应用程序和内核之间的二进制接口的一部分,它将在Linux版本迭代中得到保持。 但是,该接口是不可移植的,应用程序应该使用 termios(3)
手册页中描述的POSIX包装器。
我不会详细讨论 termios(3)
接口的细节,但是如果你正在编写C程序并希望在 ^C
变成 SIGINT
之前拦截 ^C
,或者禁用行规范或字符回显,或将更改一个串的口波特率,关闭流控制等,你就会发现你需要上述的手册页(man page)。
这里还有一个名为 stty(1)
的命令行工具来操作TTY设备。 它使用的是 termios(3)
API。
让我们试试吧!
$ stty -a speed 38400 baud; rows 73; columns 238; line = 0; intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D; eol = <undef>; eol2 = <undef>; swtch = <undef>; start = ^Q; stop = ^S; susp = ^Z; rprnt = ^R; werase = ^W; lnext = ^V; flush = ^O; min = 1; time = 0; -parenb -parodd cs8 -hupcl -cstopb cread -clocal -crtscts -ignbrk brkint ignpar -parmrk -inpck -istrip -inlcr -igncr icrnl ixon -ixoff -iuclc -ixany imaxbel -iutf8 opost -olcuc -ocrnl onlcr -onocr -onlret -ofill -ofdel nl0 cr0 tab0 bs0 vt0 ff0 isig icanon iexten echo echoe echok -echonl -noflsh -xcase -tostop -echoprt echoctl echoke
-a
参数是让stty
显示所有的设置。默认情况下,它将查看连接到shell的TTY设备,但可以通过-F
指定其他的设备。
在上面显示出的设置中,一些会改变UART参数,一些会影响行规范,一些则用于工作控制。我们先来看看第一行:
属性 | 设备 | 说明 |
---|---|---|
rows, columns | TTY驱动 | 该TTY设备的终端的大小(以字符作为基准)。基本上,它只是内核空间中的一对变量,你可以自由设置和获取。设置它们将导致TTY驱动程序向前台工作发送SIGWINCH 。 |
line | 行规范 | 该TTY设备的行规范. 0 代表 N_TTY . 所有可用的数值在 /proc/tty/ldiscs 中有列出. 使用未列出的数值等价于使用 N_TTY , 但是不要依赖于这一点. |
speed | UART | 波特率。伪终端忽略这个参数。 |
尝试以下操作:启动一个 xterm
。记下它的TTY设备( tty
命令获得)及其窗口大小(由stty -a获得
)。接着在xterm
中启动 vim
(或其他一些全屏终端应用程序)。vim
编辑器会向TTY设备查询当前的终端窗口大小,以此填充整个窗口。现在,从另一个shell窗口输入:
stty -F X rows Y
其中X是刚才获得的TTY设备,Y是终端高度的一半。这将更新内核内存中的TTY数据结构,并向编辑器发送 SIGWINCH
,vim
将使用可用窗口区域的上半部分重绘GUI。
stty -a
输出的第二行列出了所有特殊的字符,开一个新的 xterm
然后试试这个:
stty intr o
现在,"o
"而不是 ^C
将向前台工作发送 SIGINT
。尝试运行一些程序,比如 cat
,并看看你能不能用 ^C
杀死它。然后,尝试在其中输入“hello”。
有时候,你可能会遇到退格键不起作用的Unix系统——当终端仿真器发送与TTY设备中的擦除设置不匹配的退格码(ASCII 8或ASCII 127)时,就会发生这种情况。为了解决这个问题,请设置 stty erase ^H
(ASCII 8)或 stty erase ^?
(ASCII 127)。要注意的是,许多终端应用程序使用readline
,这使得行规范处于原始模式,即这些应用程序不受到影响。
最后,stty -a
列出了一系列开关(没有特定顺序列出)。其中一些与UART相关,一些影响线路规范行为,一些用于流量控制,一些用于工作控制。短划线( - )表示开关关闭;否则它是开着的。所有的开关都在stty(1)手册页中进行了解释,所以我将简单地提一下:
icanon用于将行规范切换为规则(基于行)模式。在一个新的 xterm
中试试这个,关闭这个模式:
stty -icanon; cat
现在所有的行编辑字符,例如退格或者^U
都会停止工作。另外注意到cat
会一次接受一个字符(并连续输出),而不是一次接受一行。
echo 是启用字符回显的开关(默认也是开着的)。现在重新启动规则模式(stty icanon
)然后试试这个:
stty -echo; cat
当你输入时,你的终端仿真器将信息传送给内核,而内核通常会将相同的信息回显给终端仿真器,以便让你看到之前键入的内容。现在没有了字符回显,你就不能看到你输入的内容。不过我们处于熟化(cooked)模式,所以行编辑工具仍在工作。一旦你按下回车键,行规范就会把编辑缓冲区的数据传送给cat,显示出你刚刚键入的内容。
tostop 是控制后台进程是否允许写入终端的开关,先试试这个:
stty tostop; (sleep 5; echo hello, world) &
&
会使得该命令作为后台工作运行。五秒钟后,该工作将尝试写入TTY。 TTY驱动程序将使用 SIGTTOU
将其挂起,并且shell可能会立即报告此事件,或者发出别的提示。现在尝试下面的代码:
stty -tostop; (sleep 5; echo hello, world) &
五秒钟之后,后台工作会在你当前的光标位置输出 hello, world
。
最后, stty sane
会将你的TTY设置成一个相对合理的配置。
结语
我希望这篇文章为你提供了足够的信息去了解TTY驱动和行规范,以及它们与终端,行编辑和工作控制之间的关系。 更多细节可以在我提到的各种手册页以及glibc手册(info libc
,"Job Control")中找到。
最后,尽管我没有足够的时间来回答所有问题,但我欢迎任何对本网站上的其他网页提出的反馈意见。 谢谢阅读!
译者注:更多有关于tty、shell、console的知识,可以参考
- What is the exact difference between a 'terminal', a 'shell', a 'tty' and a 'console'?
- Terminal emulator
- xterm
X Window System
- TTY(4)
- pty(7)
ptmx(4)