终端 (Terminal), TTY 和 Shell

目录

在使用计算机进行编程相关工作时常常要接触到终端,而在使用终端的过程中难免会接触到 Terminal, TTY 和 Shell 的概念,这篇博客就是我对这些概念的学习和理解的整理。

打字机

现在人们和计算机进行交互的硬件设备除了鼠标,最广泛使用的就是键盘了。然而键盘的历史其实比通用电子计算机还要久远。早在通用电子计算机还没出现的 19 世纪 60 年代,美国人斯托夫·拉森·肖尔斯就发明了世界第一台实用的机械打字机(单论自动打字机器的历史记载很早就有,但实用意义上的真正打字机的发明还要等到肖尔斯),而键盘正是打字机的一大重要组件,其作用和今天类似:接受并告知系统用户的输入是什么。

而后随着第二次工业革命带来了电,打字机也发展出采用电信号的电传打字机 (Teletypewriter, TTY)。在电传打字机诞生的早期,它的主要用途和通信设备连用,比如发电报等。之后随着 20 世纪 50 年代早期计算机的发展,人们将电传打字机被改装成计算机的输入设备,电传打字机成为了和计算机进行交互的重要硬件设备,之后逐渐演变成今天的键盘。

虽然打字机最后被计算机取代,但它的遗产即使是在当今的计算机中也无处不在:TTY 这个名词就是来源于电传打字机的英文缩写、最广泛使用的 QWERTY 的键盘布局早在机械打字机时代就已经产生、ASCII 码作为基础字符集的普及和串口通信的协议和接口的形成等也离不开电传打字机以及回车 (CR) 和换行 (LF) 的不同。

  • 回车 (CR, Carriage Return) 是将打字机中在纸上打印字符的打印头移到行首,一般采用 \r 表示。
  • 换行 (LF, Line Feed) 是将打字机的纸张向上移动一行,一般采用 \n 表示。

在实际文件中,Windows 系统采用的 Dos 风格换行符是 \r\n, Linux 采用的 Unix 风格换行符是 \n (同时包含换行和回车两个作用), 而 MacOS 早期采用的换行符是 \r, 之后改为了 Unix 风格 \n, 正是这些换行符的差异导致跨平台文件读写可能导致问题,比如在 Linux/Mac 中打开 Windows 的文件可能发现每行结尾可能显示 ^M 字符,这正是 CR 的显示形式。还好现代的编辑器往往都提供了自动识别和转化换行符的功能。

终端和 TTY

终端 (Terminal), 简而言之就是人类和计算机进行交互的设备。作为输入设备,终端能解析用户的输入并告知计算机;作为输出设备,终端能够给用户显示输出结果。在计算机发展早期,TTY 和物理终端是几乎等价的概念。在现代计算机中,终端往往是指虚拟终端,TTY 是操作系统中对终端的一个抽象层,负责处理用户键盘输入和屏幕输出,还处理包括输入缓冲、回显(Echo)、信号(如 Ctrl+C 终止进程)机制、输入和输出通过串行线路传输等底层交互细节。

实际上,在当今的图形化界面中,我们接触更多的终端往往还是终端模拟器,比如 MacOS 或 Windows 的“终端”、KittyAlacritty 等。终端模拟器的作用正如其名,是模拟 (物理) 终端的功能,提供输入和输出的交互。此外,一些对终端有更深入接触的人必然会了解到诸如 ssh 的远程会话或者诸如 tmux 的终端复用器。而上述的终端模拟器、远程会话和终端复用器其实现 (Linux 中,其他操作系统的实现我并不了解) 还离不开另一个重要概念,也就是下面要讲的伪终端。

伪终端

伪终端 (PTY) 是操作系统内核对终端功能的软件模拟,它允许程序在不依赖物理终端硬件的情况下,提供与终端相同的交互行为。其实现包含两部分:

  • 主设备 (PTY Master,ptmx): 连接到终端模拟器、终端复用器等,负责管理和从设备以及主设备连接的终端模拟器等的输入输出的数据交互。
  • 从设备 (PTY Slave, pts): 连接到 Shell 等应用程序,行为和物理终端一致,负责管理和连接的应用程序的输入输出的数据交互,可以有多个从设备会话独立运行。
以在终端模拟器中使用 ls 命令为例:
1. 用户在终端模拟器输入 ls 后按下回车,终端模拟器获取用户输入并将输入数据发送给主设备,主设备再将输入数据发送到从设备。
2. Shell 从从设备读取并解析输入数据,执行 ls 命令,将程序的输出数据写回从设备,从设备再将输出数据发送给主设备。
3. 主设备接收到从设备的输出数据并转发给终端模拟器,终端模拟器解析输出数据,将结果显示在屏幕上。

关于伪终端更详细的内容可以通过 Man 手册 man pty, man pts/ptmx 查看。

TTY 文件

在 Unix 的一切皆文件哲学下,TTY 被抽象为一种特殊的设备文件,比如在 Linux 中这些文件就是 /dev/ 下一系列名字含 tty 的文件,其中包括:

  • /dev/tty[NUMBER] 表示虚拟终端,常用于用户登录会话,例如登录图形化界面通常使用的就是 tty1tty7 ,其他 tty2-6 往往是 TUI 界面。可以通过 Ctrl+Alt+F1-F7 切换到对应的虚拟终端。还可以通过命令 who 查看当前用户 tty。
    • 特别的, /dev/tty0 指向当前活动的虚拟终端。例如切换到 tty2 时, tty0 就会指向 tty2
  • /dev/ttyS[NUMBER] 表示物理串口终端, /dev/ttyUSB0 表示 USB 转串口适配器设备的串口终端,这些都和硬件相关。
  • /dev/pts/[NUMBER] 表示伪终端的从设备, /dev/ptsm 表示伪终端的主设备。对从设备文件的读写会指向从设备连接的终端。
    • 例如在一个对应 /dev/pts/1 的终端模拟器中使用命令 echo "Hello World" > /dev/pts/1 就会发现 “Hello World” 被直接输出到了当前终端模拟器中。
  • /dev/tty 表示当前进程的控制终端,无论当前进程是在物理终端、远程会话、终端复用器还是终端模拟器等终端中运行,读写此文件均指向当前终端。
    • 例如在终端模拟器中使用命令 echo "Hello World" > /dev/tty 就会发现 “Hello World” 被直接输出到了当前终端模拟器中。

此外,GNU coreutils 还提供了 tty 命令来显示当前连接到标准输入的终端的文件名,使用 echo "Hello World" > $(tty) 可以实现和上面的 echo 命令一样的效果。

Shell

Shell 在概念上其实和 Python 没什么区别。都是可以作为交互式的 REPL 程序、都是可以作为脚本的解释器运行脚本、都是可以作为编程语言,通过自己的语法和关键字等来写脚本。只不过 Shell 可以默认可以根据用户输入调用外部程序。(当然 Python 也可以实现,只是默认不支持)

就像浏览器有 Chrome、Firefox、Safari 等软件一样,Shell 也有不同的软件,常见的 Shell 有:

名称描述
shPOSIX 标准1中定义的 Shell 的实现, 现代的许多 Linux 发行版中作为 bash 等其他 Shell 的符号链接, 主要用来作为脚本解释器
bash最常见的 Shell, 是现代的许多 Linux 发行版的默认交互式 Shell, 符合 POSIX 标准
zsh目标是 bash 的拓展版本, 现在的 MacOS 默认交互式 Shell, 大部分符合 POSIX 标准
fish现代交互式 Shell, 注重用户友好, 开箱即用, 现代化脚本语法, 不符合 POSIX 标准
cmdWindows 系统的默认 Shell
powershellWindows 系统的 Shell, 可以看作高级的 cmd

  1. 在 Unix 类操作系统中有 POSIX(可移植操作系统接口)标准这一说法,其是由 IEEE 定义的一系列标准,旨在为不同的 UNIX 操作系统提供统一的 API 和工具规范,从而提高跨平台兼容性。而在 Shell 语法和行为上 POSIX 标准是以 sh 作为一种标准化 Shell 来定义的。 ↩︎