概述
都说程序员的一生,用的最多的两个工具,一个是代码编辑器(Code Editor),另一个就是命令行终端工具(Terminal)。这两个工具是程序员的左膀右臂,对于提升开发效率至关重要。现在大部分的 IDE (Integrated Development Environment 集成开发环境)已经将这两个工具集成在一起了。记得在大学时第一次使用的就是命令行终端,那个年代有且仅有命令行终端。几十年过去了,不管图形界面做的如何炫酷,如今的终端工具,基本上还是保持原来的模样,焕发着顽强的生命力,保持着超高的生产力。
在云原生时代,开发环境已经逐步搬到云端,如TitanIDE就是云原生集成开发环境,那么在云端是否仍然可以继续方便地使用命令行终端呢?接下来的《TitanIDE云原生开发之旅》系列文章中,我将从《TTY 的前世今生》开始,讲述如何一步一步在 TitanIDE 玩转云原生命令行终端。
本文通过学习 Linus Åkesson ( 2008 )所写的 TTY 相关理论,并在TitanIDE 的 Terminal 同步实践。
前置条件
安装 TitanIDE
本文所涉及的所有命令行都是在 TitanIDE 创建一个开箱即用的 Terminal 上面执行的,如果您还未了解过 TitanIDE,请点击这里了解详情。简单来说,TitanIDE 是一款云原生集成开发环境, 最少只需一台虚拟机,十分钟即可安装好,立即开启您的全云端开发之旅!
点击,立即下载TitanIDE
创建 Terminal
在 TitanIDE 创建一个独立的 Terminal 用于执行本文接下来的所有的操作,这些操作在独立的环境中执行,请放心大胆地使用。
揭开 TTY 的神秘面纱
TTY (Teletype)子系统是 Linux 乃至 UNIX 家族中设计的核心。不幸的是,它的重要性往往被低估,并且网上也很难找到不错的相关介绍文章。对 Linux 中的 TTY 有一些基本了解对于开发者和高级用户来说是非常有帮助的。不过要注意:您即将看到的东西并不是特别优雅。事实上,TTY子系统虽然从用户的角度看来功能相当强大,但它存在一些小混乱的特殊情况。要了解这一切是如何发生的,我们必须从 TTY 的发展历史说起。
TTY 的发展历史
我们把镜头切到公元 1869 年,当时人们发明了股票报价机。它是一个用于跨长距离实时传递股票价格的机电设备,由一个打字机、一对很长的电缆和一个报价用的磁带打印机组成。后来,这一概念逐渐演变为更快的、基于 ASCII 码的电传打字机。电传打字机曾经在一个名为Telex的大型网络中连接世界各地,这个网络用于传输商业电报,但当时电传打字机还没有与任何计算机相连。
然而,与此同时,计算机——仍然相当大和原始,但能够多任务处理——正在变得足够强大,能够与用户实时交互。当命令行最终取代了旧的批处理模型时,电传打字机被用作输入和输出设备,因为它们在当时的市场上很容易买到。
当时有大量的电传打字机模型,它们都略有不同,因此需要某种软件兼容层。在 UNIX 世界中,方法是让操作系统内核处理所有低层技术细节,例如字长、波特率、流控制、奇偶校验、用于基本行编辑的控制代码等等。上世纪70年代末,人们在 VT-100 等固态视频终端设备上实现了酷炫的光标移动,而彩色输出和其他高级功能,则交给了应用程序来处理。
在当今时代,我们发现自己生活在一个物理电传打字机和视频终端几乎灭绝的世界。除非您参观博物馆或您是一位硬件发烧友,否则您可能看到的所有 TTY 都将是模拟的视频终端——对实物的软件模拟。但正如我们将看到的,古老的铸铁野兽留下的遗产仍然潜伏在表面之下。
TTY 的使用场景
用户(通过一个物理电传打印机)在一个终端上输入(打字)。这个终端通过一对电缆连接到计算机上的一个 UART(Universal Asynchronous Receiver and Transmitter,通用异步收发器)。操作系统中安装了 UART 驱动,能够处理字节的物理传输,包括奇偶校验和流控。在一个简陋的系统中,UART 驱动会将收到的字节直接发送给某个应用进程。但是,以上方式缺少下面几个必备特性。
行编辑
大部分用户都难免在打字时犯错,因此退格键(backspace key)通常很有用。这个功能当然可以由应用本身实现,但按照 UNIX 的设计哲学,应用应该越简单越好。因此 ,为了方便,操作系统提供了一个编辑缓冲区(editing buffer)以及一些基本的编辑命令(退格、删除单词、清除行、重新打印),这些功能在 line discipline(行规程)中是默认开启的。高级应用可以选择关闭这些特性,只要将行规程从默认(或 canonical) 模式改为 raw 模式就行了。大部分交互式应用(编辑器、邮件用户代理、shell,以及所有依赖 curses 或 readline 的程序)都运行在 raw 模式,自己来处理所有的行编辑命令。行规程还包含了字符回显(character echoing)和回车/换行( carriage returns and linefeeds)自动转换的功能。如果您愿意,可以将其想象成内核中的 sed(1)。
出于某些偶然的原因,内核提供了多种不同的行规程。但在任何时刻,对于某个给定的串行设备,内核只会连接(attach)其中的一种到这个设备。默认的 discipline 叫 N_TTY(drivers/char/n_tty.c —— 如果您喜欢刨根究底)。其他几种 disciplines 用于不同目的,例如管理包交换数据( 例如 ppp, IrDA, 串行鼠标等等),但这些超出了本文的范围。
会话管理
用户可能希望同时运行多个程序,并一次与它们其中的一个进行交互。如果一个程序进入无限循环,用户可能想终止或挂起这个应用程序。在后台启动的程序应该能够执行,直到它们尝试写入终端,此时它们应该被挂起。同样地,用户输入应该只指向前台程序。操作系统在TTY驱动程序(drivers/char/tty_io.c)中实现了这些特性。
操作系统进程“活着”时(有执行上下文),意味着它能够执行操作。TTY 驱动并没有活着;用面向对象的术语来说,TTY 驱动是一个被动对象。它有一些数据字段和方法,但当它的其中一个方法从进程或内核中断处理程序的上下文中被调用时,它实际上可以做一些事情。行规程(line discipline )也是一个被动实体。
UART 驱动、行规程(line discipline )实例和 TTY 驱动三者组成一个 TTY 设备, 有时简称为 TTY。用户进程能够通过操作 /dev 目录下的相应设备文件来改变 TTY 设备的行为。进程需要对设备文件有写权限,因此当一个用户登陆到某个特定的 TTY 时 ,该用户必须成为相应设备文件的 owner。传统上这是通过 login(1) 程序实现的 ,该程序需要以 root 特权执行。
前面图中的物理线路当然也可以是一个长距离电话线路:
在这张图中,除了系统此时也需要处理解调器(modem)的 hangup 情况之外,其他方 面跟前一张没有太大区别。
接下来我们来看一个典型的桌面系统。下图展示的是 Linux console 是如何工作的:
TTY 驱动和 line discipline 的行为和前面例子中的一样,但其中不再涉及 UART 或物理 终端。与前面不同的地方在于,现在多了一个软件仿真的视频终端(一个复杂的状态机,包 括一个字符帧缓冲区和一些图形字符属性),渲染到一个 VGA 显示器。
控制台(console)子系统某种程度上比较刻板。如果我们将终端仿真放到用户空间,事情就会变得更加灵活(和抽象)。下面是 Terminal(1) 及其衍生版本如何 工作的:
为了方便将终端模拟移到用户空间且同时保持 TTY 子系统(会话管理和 line discipline)的完整性,人们引入了伪终端(pseudo terminal)或称 pty。您也许已经猜到了,当在伪终端内运行伪终端时(running pseudo terminals inside pseudo terminals),事情会变得更加复杂,例如 screen(1) 或 ssh(1)。
现在让我们退后一步,来看一看这些东西是如何适配到进程模型的。
TTY 进程
一个 Linux 进程可以处于以下几种状态之一:
R: 运行中或可运行(Running or runnable (on run queue))
D: 不可中断睡眠(Uninterruptible sleep (waiting for some event))
S: 可中断睡眠(Interruptible sleep (waiting for some event or signal))
T: 停止(Stopped, either by a job control signal or because it is being traced by a debugger.)
Z: 僵尸进程(Zombie process, terminated but not yet reaped by its parent.)
运行 ps l 可以看到各进程的状态。例如,如果是 sleeping 状态,WCHAN 列(”wait channel”,等待队列的名字)会显示这个进程正在等待的内核事件(kernel event):
“wait” 等待队列(wait queue)和 wait(2) 系统调用相关,因此当这些进程的 任何一个子进程有任何状态变化时,这些进程就会被移动到 running 状态。
sleeping 状态有两种:可中断 sleep 和不可中断 sleep。可中断 sleep 最常见,它表示 虽然该进程当前在 wait 队列中,但只要它收到信号,就可以被移动到 running 状态。如 果查看内核源码,您会发现任何正在等待事件的内核代码都必须在 schedule() 返回 之后检查是否有信号 pending,如果有就 abort。
在上面 ps 命令的输出结果中,STAT 列显式了每个进程的当前状态。除此之外,这一 列还可能包含额外的属性或标记:
s:表示这个进程是 session leader
+:表示这个进程是一个前台进程组的一部分(part of a foreground process group)
N: 表示这个是一个使用 nohup 启动的后台进程
这些属性用于作业控制(job control)。
作业(Jobs)和会话
当您按下 ^Z 键,或使用 & 在后台启动一个程序时,就是在进行作业控制。
作业和进程组的概念是一样的(A job is the same as a process group)。shell 内 置的命令,例如 jobs、fg、bg 等等可以用于管理一个会话内已有的作业。每 个 session 都是由一个 session leader 管理的,这个 session leader 就是 shell —— 通过一个复杂的信号协议和系统调用来和内核紧密协作。
下面的例子展示了进程、作业和会话之间的关系:
上图中的 shell 交互对应下面的这些进程:
以及下面这些内核结构:
TTY Driver(/dev/pts/0)
Size: 45x13 # 尺寸:45x13Controlling process group: (101) # 控制进程组:101Foreground process group: (103) # 前台进程组:103UART configuration (ignored, since this is an Terminal): # UART 配置(忽略,因为这是虚拟终端 Terminal)Baud rate, parity, word length and much more.Line discipline configuration: # Line discipline 配置:cooked/raw mode, linefeed correction, # cooked/raw 模式meaning of interrupt characters etc.Line discipline state: # Line discipline 状态:edit buffer (currently empty), # 编辑缓冲区(当前为空)cursor position within buffer etc.
pipe0
Readable end (connected to PID 104 as file descriptor 0) # 可读端(作为文件描述符 0 连接到 PID 104)Writable end (connected to PID 103 as file descriptor 1) # 可写端(作为文件描述符 1 连接到 PID 103)Buffer # 缓冲区
这里的基本思想是:每个管道都是一个作业,因为每个管道内的进程都需要被同时操控(stopped, resumed, killed)。这也是为什么能够 用 kill(2) 向一整个进程组发送信号的原因。默认情况下,fork(2) 会将新创建出来 的子进程放到与其父进程相同的进程组,因此,例如一个 ^C 键就会同时影响到父子进程 。但 shell 有些不同,作为其 session leader 职责的一部分,它每次创建一个管道的时候都会创建一个新的进程组。
TTY 驱动跟踪记录前台进程组 ID(foreground process group id),但只会以被动的方 式跟踪。当有必要时,session leader 必须显式更新这项信息。类似地,TTY 驱动 也会以被动的方式跟踪所连接的终端的尺寸大小(size),但这个信息必须由终端模拟器甚 至用户来显式更新。
前面的图中可以看到,几个不同进程都将 /dev/pts/0 attach 到了它们的标准输入。但 只有前台任务(ls | sort 管道)会从 TTY 接收输入。类似地,只有前台作业是允 许写到 TTY 设备的(在默认配置下)。如果图中的 cat 进程试图写到该 TTY,内核会通 过一个信号挂起它。
简单粗暴的信号机制
现在让我们来更加近距离地看看内核中的 TTY 驱动、line discipline 和 UART 驱动 是如何与用户空间进程通信的。
UNIX 文件,包括 TTY 设备文件,都可以被读取或写入,以及通过神奇的 ioctl(2)( UNIX 中的瑞士军刀)系统调用进一步操作,内核中已经为 TTY 设备实现了很多相关的 ioctl 操作。但是,ioctl 请求必须从进程(向内核)发起,因此当内核(主动)希 望异步地与应用进行通信时,ioctl 就不适用了。
在《银河系漫游指南》中, Douglas Adams 描述了一个极其迟钝的星球,上面居住了一群 意志消沉的人以及一种带有锋利牙齿的动物,后者与前者交谈的方式就是用力撕咬他们的大 腿。这与 UNIX 非常相似,因为内核与进程通信的方式就是向进程发送能使之瘫痪或致 命的信号。进程可能会捕获其中某些信号,然后尝试解决遇到的问题,但大部分信号都是没 有被捕获的。
因此,信号是一种粗暴的内核与应用进程异步通信的机制。UNIX 中信号的设计并不整 洁或通用;每个信号都是唯一的,因此必须逐个研究。
kill -l 命令可以查看当前系统已经实现了哪些信号。这个命令的输出可能与下面的类似 :
如上所示,信号是从 1 开始编码的。但如果是掩码(bitmask)形式表示(例如 ps s 的 输出中),最不重要比特(least significant bit)表示的是 1。
本文将关下面几信号:SIHUP、SIGIT、SIGQUI、SIGPIPE、 SIGCHLD、SIGSTOP 、 SIGCONT、 SIGTSTP、 SIGTTIN、SIGTTOU 和 SIGWINCH。
• SIGHUP
– 默认动作:Terminate
– 可能动作:Terminate, Ignore, Function call
当检测到 hangup 时,UART 驱动会向整个 session 发送 SIGHUP 信号。正常情况下,这会 kill 掉所有进程。某些程序,例如 nohup(1) 和 screen(1),会从他们的 session(和 TTY)中 detach 出来, 因此这些程序的子进程无法关注到 hangup 事件。
• SIGINT
– 默认动作:Terminate
– 可能动作:Terminate, Ignore, Function call
当输入流中出现interactive attention character(交互式注意字符,通常是 ^C,ASCII 码是 3)时,TTY 驱动会向当前的前台作业发送 SIGINT 信号 ,除非这个特性被关闭了。任何对 TTY 设备有权限的人都可以修改 the interactive attention character 或打开/关闭这个特性;另外,会话管理器(session manager) 跟踪记录每个作业的 TTY 配置,当发生作业切换时会更新 TTY。
• SIGQUIT
– 默认动作:Core dump
– 可能动作:Core dump, Ignore, Function call
SIGQUIT 和 SIGINT 类似,但 quit 字符通常是 ^\,而且默认动作不同。
• SIGPIPE
– 默认动作:Terminate
– 可能动作:Terminate, Ignore, Function call
对于每个尝试向没有 reader 的 piepe 写数据的进程,内核会向其发送 SIGPIPE 信号。这很有用,因为如果没有这个信号,某些作业就无法终止。
• SIGCHLD
– 默认动作:Ignore
– 可能动作:Ignore, Function call
当一个进程死掉或状态发生改变时(stop/continue),内核会向其父进程发送此信号 。该信号还附带了其他信息,即该进程的进程 ID、用户 ID、退出状态码(或终止信号) 以及其他一些执行时统计信息(execution time statistics)。session leader 使用 这个信号跟踪它的作业。
• SIGSTOP
– 默认动作:Suspend
– 可能动作:Suspend
该信号会无条件地挂起信号接受者,例如,该信号的动作是不能被重新配置的( reconfigure)。但要注意,该信号并不是在作业控制(job control)期间被内核发送 的。^Z 通常情况下触发的是 SIGTSTP 信号,这个信号是可以被应用捕获的。例如 ,应用可以将光标移动到屏幕底部,或者将终端置于某个已知状态,随后通过 SIGSTOP 将自己置于 sleep 状态。
• SIGCONT
– 默认动作:Wake up
– 可能动作:Wake up, Wake up + Function call
该信号会唤醒(un-suspend)一个已经 stop 的进程。用户执行 fg 命令时, shell 会显式地发送这个信号。由于应用无法捕获该信号,因此如果出现未预期的 SIGCONT 信号,可能就表示某些进程在一段时间之前被挂起了,现在挂起被解除了。
• SIGTSTP
– 默认动作:Suspend
– 可能动作:Suspend, Ignore, Function call
该信号与 SIGINT 和 SIGQUIT 类似,但对应的魔法字符通常是 ^Z,默认动作是挂起进程。
• SIGTTIN
– 默认动作:Suspend
– 可能动作:Suspend, Ignore, Function call
如果一个后台作业中的进程尝试读取一个 TTY 设备,TTY 会发送该信号给整个作业。正常情况下这会挂起作业。
• SIGTTOU
– 默认动作:Suspend
– 可能动作:Suspend, Ignore, Function call
如果一个后台作业中的进程尝试写一个 TTY 设备,TTY 会发送该信号给整个作业。正常情况下这会挂起作业。可以在 per-TTY 级别打开或关闭这个特性。
• SIGWINCH
– 默认动作:Ignore
– 可能动作:Ignore, Function call
前面提到,TTY 设备会跟踪记录终端的尺寸(size),但这个信息需要手动更新。当终端尺寸发送变化时,TTY 设备会向前台作业发送该信号。行为良好的交互式应用, 例如编辑器,会对此作出响应:从 TTY 设备获取新的终端尺寸,然后根据该信息重绘自己。
一个例子
设想您在用自己的(基于终端的)编辑器编辑某个文件。光标当前位于屏幕中央,编辑器正 忙于执行某些 CPU 密集型任务,例如在一个大文件中执行搜索或替换操作。现在假设您按 下了^Z 键。因为 line discipline 已经配置了捕获此字符(^Z 是单个字节,ASCII 码 为 26),因此您无需等待编辑器完成它正在执行的任务然后开始从 TTY 设备读取数据。
此时的情况是,line discipline 子系统会立即向前台进程组发送 SIGTSTP 信号。这个进程组中包括编辑器进程,以及它创建出来的任何子进程。如上图所示,vim 被挂起。
编辑器为 SIGTSTP 进程注册了信号处理函数,因此内核此时开始执行该信号处理函数 的代码。该代码通过向 TTY 设备写入相应的控制序列(control sequences),将 光标移动到屏幕最后一行。由于编辑器仍然在前台,这个控制序列能够正常发送出去(给 TTY)。但之后,编辑器会给自己所在的进程组发送一个 SIGSTOP 信号。
编辑器此时就被挂起(stop)了。这个事件会通过一个 SIGCHLD 信号发送给 session leader, 其中包括了被挂起进程的进程 ID。当前台作业中的所有进程都被挂起后,session leader 从 TTY 设备中读取当前配置,保存以备后面恢复时用。session leader 使用 ioctl 系 统调用,继续将自己注册(install itself)为该 TTY 的当前前台进程组。然后,它打印 出类似 "[1]+ Stopped" 之类的信息,告知用户有一个作业刚被挂起了。
此时,ps l 会告诉您编辑器进程当前处于 stopped state (“T”)。
如果我们试图唤醒它 ,不管是通过 shell 内置的 bg 命令,还是使用 kill 发送 SIGCONT 信号给进程 ,都会触发编辑器执行它的 SIGCONT 信号处理函数。该信号处理函数可能会尝试通过写 TTY 设备来重绘编辑器 GUI。但由于此时编辑器是后台作业,TTY 设备是不允许其写入的。这种情况下 TTY 会给编辑器发送 SIGTTOU 信号,再次将其 stop。这个事件会通过 SIGCHLD 信号通知到 session leader,然后 shell 会再次将 "[1]+ Stopped" 之类的 消息写到终端。
但当我们输入 fg 命令时,shell 首先会恢复此前保存的 line discipline 配置。然后,它通知 TTY 驱动从现在开始编辑器作业应当被作为前台作业对待了。最后,它发送 一个 SIGCONT 信号给进程组。编辑器进程尝试重绘 GUI,而这一次它不会被 SIGTTOU 中断了,因为它现在是前台作业的一部分了。再次按下 ^Z 可以看到进程的状态。
流控和阻塞式 I/O
在 Terminal 中执行 yes 命令,您会看到大量的 "y" 一行一行地快速闪过。正常情况 下条,yes 进程产生 "yes" 输出的速度要远快于 Terminal 应用解析这些行、更新帧缓冲 区、与 X server 通信来滚动窗口等等的速度。那么,这些进程之间是如何协作的呢?
答案就是 blocking I/O(阻塞式输入/输出)。伪终端只能在其内核缓冲区中保存一定量 的数据,当缓冲区已经填满而 yes 程序仍然调用 write(2) 写入时,write(2) 会阻 塞,yes 进程会被移入可中断 sleep 状态,直到 Terminal 进程读走了一部分缓存的数据。
当 TTY 连接到的是串口(serial port)时,过程与此类似。yes 能够以很快的速度 发送数据,例如 9600 波特,但如果串口速度比这个低,内核缓冲区很快就会塞满,随后的 任何 write(2) 调用都会阻塞写进程(或者返回 EAGAIN 错误码 —— 如果进程请求的是非 阻塞 I/O)。
如果我们能够显式地将 TTY 置于阻塞状态,即使内核缓冲区中仍然有可用 空间呢?这样设置之后,每个进程调用 write(2) 进行写入时,TTY 都会自动阻塞。但 什么情况下回用到这个特性呢?
设想我们正在以 9600 波特和某个陈旧的 VT-100 硬件通信。我们刚发送了一个复杂的控制 序列要求终端滚动显示页面。此时,终端忙于执行滚动操作,无法以全速 9600 波特接收新 的数据。这种情况下,在物理上,终端 UART 仍然运行在 9600 波特,但缓冲区中没有足够 的空间来给终端存储接收到的数据。这就是一个将 TTY 置于阻塞状态的好时机。那么要实 现这个效果,我们该怎么做呢?
前面已经看到,可以配置 TTY 设备对某些特定的数据给予特殊对待。例如,在默认配 置中,TTY 收到的 ^C 字符并不会通过 read(2) 直接交给应用,而是会触发发送一个 SIGINT 信号给前台作业。类似地,可以配置 TTY 对 stop flow byte(停止流字节) 和start flow byte(开始流字节)做出响应。通常情况下,这分别是^S (ASCII code 19) 和 ^Q (ASCII code 17)。老式硬件终端能自动发送这些字节,然后期待操作 系统能够按照约定对它的数据流进行管控。这个过程称为流控(flow control),这也是 为什么有时您误按了 ^S 时,您的 Terminal 会锁定的原因。
这里要区分两种情况:
向一个由于流控或内核缓冲空间不足而 stop 的 TTY 写入:写入进程会被阻塞(block);
从后台作业向一个 TTY 写入:会导致 TTY 发送一个 SIGTTOU 给整个进程组将其挂起(suspend)。
尚不清楚 UNIX 的设计者为何发明 SIGTTOU 和 SIGTTIN 而不是依靠 blocking I/O, 能猜到的原因是:负责着作业控制(job control)的 TTY 驱动,设计用于监控和 操作全部作业,而不是作业内的单个进程。
配置 TTY 设备
要确定当前 shell 的 TTY,可以通过我们前面介绍的 ps l 命令,或者直接运行 tty 命令。
一个进程可能会通过 ioctl(2) 读取或修改一个已经打开的 TTY 设备。相应的 API 在 tty_ioctl(4) 中作了描述。由于这是 Linux 应用和内核之间的二进制接口的一部分, 因此它在不同的 Linux 版本之间是保持稳定的。但是,这个接口是不可移植的,若想编 写可移植的程序,应用应当使用 termios(3) man page 中提供的 POSIX wrapper。
这里我不会深入介绍 termios(3) 接口,但如果您正在编写 C 程序,涉及到捕获 ^C、 关闭行编辑或字符回显、修改串口的波特率、关闭流控等等工作,那您就需要去阅读前面提 到的 man page。
另外还有一个命令行工具 stty(1),用于操纵 TTY 设备。它使用了 termios(3) API。
我们来试试!
TTY 配置选项
stty -a 打印所有配置项。默认打印的是当前 shell 所 attach 的 TTY 设备配置项,但 可以通过 -F 指定其他设备。
以上选项中,某些是 UART 参数;某些影响 line discipline,某些用于作业控制。我们先 来看第一行:
• speed
– UART 参数
– 波特率。伪终端忽略此选项。
• rows 和 columns
– TTY 驱动参数
– attach 到这个 TTY 设备的终端大小(size),单位是字符数。本质上这只是内核空 间中的一对变量,可以随意修改和读取。修改这两个参数会触发 TTY 驱动发送 SIGWINCH 信号给前台作业。
• line
– Line discipline 参数
–表示 attach 到这个 TTY 的 line discipline。0 代表 N_TTY。所有的合法值列在 /proc/tty/ldiscs 下面。未列出的值似乎是 N_TTY 的 alias,但不依赖前者。
修改 Terminal 窗口尺寸
尝试下面的例子:开启一个终端(在 TitanIDE 项目列表点击 terminal-ttyd-wlotnipo 打开)。
记录下它的 TTY 设备(如下图,执行 tty 命令查看为 /dev/pts/1)以及尺 寸(执行 stty -a 命令查看)。在Terminal 中启动 vim(或其他全屏终端应用)。编辑器会询问 TTY 设 备当前的终端尺寸,以填充整个窗口。
现在,在另一个 shell 窗口中执行:
stty -F /dev/pts/1 rows 10
上面这条命令会更新内核内存中的 TTY 配置数据 ,并触发向编辑器发送一个 SIGWINCH 信号;vim 收到信号会立即重绘自身,结果是编辑 器的高度变成 10。
修改 SIGINT 对应的控制字符
stty -a 命令的输出中,第二行列出了所有的特殊字符。
➜ workspace stty -aspeed 38400 baud; rows 28; columns 88; line = 0;intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D; eol =; eol2 =;swtch =; start = ^Q; stop = ^S; susp = ^Z; rprnt = ^R; werase = ^W; lnext = ^V;discard = ^O; min = 1; time = 0;-parenb -parodd -cmspar cs8 -hupcl -cstopb cread -clocal -crtscts-ignbrk -brkint -ignpar -parmrk -inpck -istrip -inlcr -igncr icrnl ixon -ixoff -iuclc-ixany -imaxbel -iutf8opost -olcuc -ocrnl onlcr -onocr -onlret -ofill -ofdel nl0 cr0 tab0 bs0 vt0 ff0isig icanon iexten echo echoe echok -echonl -noflsh -xcase -tostop -echoprt echoctlechoke -flusho -extproc
打开一个新 Terminal 然后尝试:
stty intr o -F /dev/pts/1
现在输入字符 o —— 而不是原来默认的 ^C —— 会触发发送 SIGINT 信号给前台作业。
您可以运行着某个命令,例如 cat,然后验证此时 ^C 是不能终止其执行的。然后,再 试试输入 hello 给 cat。
退格键无法使用
某些场合下,您可能会在某个 UNIX 系统上遇到退格键无法使用的情况。
发生这种情况是因为终端模拟器发送的退格码(不管是 ASCII 8 还是 127)与 TTY 设备中的擦除设置(erase setting)不匹配。要解决这个问题,通常需要输入 stty erase ^H(ASCII 8)或 stty erase ^?(ASCII 127)。但请记住,某些终端应用使 用 readline,它们会将 line discipline 置于 raw 模式,这些应用不会受此影响。
TTY 开关项
最后,stty -a 列出了一系列的开关。这些开关并没有先后顺序。某些与 UART 相关,某 些影响 line discipline 行为,某些用于流控,某些用于作业控制。有减号(-)表示该 开关当前是关闭的;否则就是打开的。所有开关都在 stty(1) man page 中有解释,因此 这里只是简要介绍几个:
icanon 打开/关闭 canonical (line-based) 模式。尝试在一个新 Terminal 内运行:
stty -icanon; cat
执行这条命令后,所有的行编辑字符,例如退格和 ^U 将无法使用。您会注意到 cat 此 时开始按字符接收(以及打印)内容,而不是像之前一样按行。
echo 打开字符回显(character echoing),这个选项默认是打开的。重新启用 canonical mode(stty icanon),然后执行:
stty -echo; cat
输入命令时,终端模拟器会将命令信息发送给内核。通常情况下,内核会将相同的信息回显给 终端模拟器,这样我们就可以看到自己输入的内容了。没有字符回显的话,我们无法看到自己输 入的内容,但由于我们在 cooked 模式,因此行编辑设施还是仍然工作的。当按下回车键 时,line discipline 会将编辑缓冲区发送给 cat,后者就会显示输入的内容。
tostop 控制是否允许后台作业写终端。首先尝试:
stty tostop; (sleep 5; echo hello, world) &
使得前面的进程以后台作业的方式执行。5 秒之后,该作业会尝试写 TTY。TTY 驱动会 使用 SIGTTOU 来挂起该进程,shell 可能会报告这个结果,可能是立即,也可能是某个时 候弹出一个提醒框。现在 kill 掉后台作业,执行:
stty -tostop; (sleep 5; echo hello, world) &
以上命令会重新打开输入回显功能;5 秒之后,后台作业发送 hello, world 给终端,此时不管您正在输入什么,这句话都会打印出来。
执行以下命令,会将 TTY 设备恢复到某个合理的配置。
stty sane
释放资源
最后,我们将 Termianl 删除以释放云资源。
总结
本文提供了 TTY 驱动和 line discipline 相关的知识,以及它们和终端、行编辑及作业控制的联系,通过在 TitanIDE上面创建一个 Terminal 进行快速实践。总的来说,在 TitanIDE 上面做这种测试最大的好处就是开箱即用,不用担心操作 Terminal 导致电脑损坏,用完后删除 Terminal 释放云资源。希望通过这种理论和实践相结合的方式,让大家对 Terminal 有更深刻的了解。
下一篇,我将会带来“玩转云原生 Terminal 系列”第二集:《初识 wetty,想说爱你不容易》,敬请期待!
最后,感谢阅读!
点击,立即体验TitanIDE
更多干货
往/期/精/彩/推/荐
老马闲评数字化(2)您的企业是否应该急于数字化转型?
老马闲评数字化(1)数字化转型,不转得死,转了也未必活?
TitanIDE 云原生开发之旅(1)使用 Jupyter Notebook 实现数据分析
云原生干货(4)DevOps的前世今生
云原生干货(3)“单身”还是“入微”?一起聊聊微服务的二三事
年迈程序员基于Cloud IDE开发APISIX项目的故事
如何基于SolarMesh构建微服务监管平台

