终端、TTY 与 Shell
---
I've been curious about how terminals and shells work for a while now. I know that when I open a terminal emulator like iTerm and run `ls`, somehow the shell and the program `ls` run and print out results. But how does the terminal emulator communicate with the shell? What is a TTY? What even is a pseudoterminal?
I've found a lot of existing resources on this kind of confusing or incomplete, so I'm going to try to write the clearest explanation I can.
我对终端和 Shell 的工作原理一直很好奇。我知道当我打开像 iTerm 这样的终端模拟器并运行 `ls` 时,Shell 和 `ls` 程序会运行并打印出结果。但终端模拟器是如何与 Shell 通信的呢?TTY 是什么?伪终端又是什么?
我发现现有的很多资料要么令人困惑,要么不够完整,所以我打算尽力写出最清晰的解释。
---
## some history: physical terminals
Before we talk about terminal emulators, let's discuss their predecessor: physical terminals.
The very first computers were programmed not with a terminal but with physical switches. You would flip the switches to enter a binary program and see the output on some LEDs.
Then computers got terminals. The earliest terminals were teletypes. These were basically fancy typewriters: they had a keyboard that you could type commands to the computer on, and a printer that would print out the results.
Later, CRT screens replaced the printers. But the terminals still worked the same way: you type a character on the keyboard, the character gets sent to the computer, the computer sends the character back (this is called "echoing"), and the character gets displayed on the screen. The terminal itself wasn't doing a lot of computing – it was mainly just a way to communicate with the computer.
Here's a picture of a VT100 terminal, which was introduced by DEC in 1978. This terminal was very important historically because it introduced support for ANSI escape codes (more on those later).
## 一些历史:物理终端
在谈论终端模拟器之前,我们先来聊聊它的前身:物理终端。
最早的计算机并不是通过终端来编程的,而是通过物理开关。你需要拨动开关来输入二进制程序,并通过一些 LED 灯查看输出结果。
后来,计算机有了终端。最早的终端是电传打字机(teletype)。这些基本上就是高级打字机:它们有一个键盘,可以向计算机输入命令,还有一台打印机,用于打印输出结果。
再后来,CRT 屏幕取代了打印机。但终端的工作方式仍然相同:你在键盘上输入一个字符,字符被发送到计算机,计算机再将字符发回(这称为"回显"),然后字符显示在屏幕上。终端本身并不承担太多计算工作——它主要只是一种与计算机通信的方式。
这是 VT100 终端的图片,由 DEC 于 1978 年推出。这款终端在历史上非常重要,因为它引入了对 ANSI 转义码的支持(后面会详细介绍)。
---
## what's a TTY?
TTY is short for "teletypewriter" – basically just another name for "terminal". In Linux, if you look at `/dev/`, you'll see a bunch of TTY devices.
Historically, these TTY devices corresponded to physical terminals plugged into the computer. Each physical terminal connected to the computer had a corresponding TTY device in `/dev/`, and the operating system would communicate with those physical terminals through those TTY devices.
## 什么是 TTY?
TTY 是"teletypewriter"(电传打字机)的缩写——基本上就是"终端"的另一种叫法。在 Linux 中,如果你查看 `/dev/` 目录,会看到一堆 TTY 设备。
从历史上看,这些 TTY 设备对应的是连接到计算机的物理终端。每台连接到计算机的物理终端在 `/dev/` 下都有一个对应的 TTY 设备,操作系统通过这些 TTY 设备与物理终端进行通信。
---
## terminal emulators
These days, most of us aren't using physical terminals. We're using terminal emulators – programs like iTerm2, GNOME Terminal, Terminal.app, etc.
A terminal emulator emulates a physical terminal in software. Terminal emulators need to:
1. Emulate some physical terminal (usually VT100 or a variant of it), including ANSI escape codes. For example, if your shell sends the escape code `\x1b[34m`, that means "change the text colour to blue" and your terminal emulator needs to render the following text as blue.
2. Show the terminal on your screen
3. Accept keyboard/mouse input and send it to the shell
Now, how does the terminal emulator communicate with the shell?
## 终端模拟器
如今,我们大多数人都不再使用物理终端,而是使用终端模拟器——如 iTerm2、GNOME Terminal、Terminal.app 等程序。
终端模拟器在软件层面模拟物理终端。终端模拟器需要:
1. 模拟某种物理终端(通常是 VT100 或其变体),包括 ANSI 转义码。例如,如果你的 Shell 发送转义码 `\x1b[34m`,这意味着"将文本颜色改为蓝色",你的终端模拟器需要将随后的文本以蓝色渲染。
2. 在屏幕上显示终端界面
3. 接收键盘/鼠标输入并将其发送给 Shell
那么,终端模拟器是如何与 Shell 通信的呢?
---
## pseudoterminals
The way terminal emulators communicate with shells is through a **pseudoterminal** (also called a pty).
A pseudoterminal is a fake terminal: it's an API that looks to programs like a terminal but is just two file descriptors in a Linux program: a "master" (or "controller") side and a "secondary" (or "replica") side.
The way pseudoterminals work is:
1. The terminal emulator opens the master side of a pseudoterminal
2. The shell (and its child processes, like `ls`) gets the secondary side of the pseudoterminal as its stdin/stdout/stderr
3. The terminal emulator reads characters from the master side (which is the shell's output) and displays them on the screen
4. The terminal emulator also writes characters to the master side (which is the keyboard input) and the shell reads them from the secondary side
Here's a diagram:
```
keyboard --> terminal emulator --> PTY master --> PTY secondary --> shell
screen <-- terminal emulator <-- PTY master <-- PTY secondary <-- shell
```
The PTY (pseudoterminal) is managed by the operating system – the terminal emulator writes to the master side and the OS is responsible for making the data available on the secondary side.
## 伪终端
终端模拟器与 Shell 通信的方式是通过**伪终端**(也称为 pty)。
伪终端是一种虚拟终端:它是一个 API,对程序来说看起来像一个终端,但在 Linux 程序中实际上只是两个文件描述符:一个"主"(master,也称 controller)端和一个"从"(secondary,也称 replica)端。
伪终端的工作方式如下:
1. 终端模拟器打开伪终端的主端
2. Shell(及其子进程,如 `ls`)将伪终端的从端作为其 stdin/stdout/stderr
3. 终端模拟器从主端读取字符(即 Shell 的输出)并显示在屏幕上
4. 终端模拟器也向主端写入字符(即键盘输入),Shell 从从端读取这些字符
下面是一个示意图:
```
键盘输入 --> 终端模拟器 --> PTY 主端 --> PTY 从端 --> Shell
屏幕显示 <-- 终端模拟器 <-- PTY 主端 <-- PTY 从端 <-- Shell
```
PTY(伪终端)由操作系统管理——终端模拟器向主端写入数据,操作系统负责将数据在从端提供给 Shell。
---
## what is the TTY driver?
But there's something else in the middle: the TTY driver. The TTY driver sits between the PTY master and secondary.
```
keyboard --> terminal emulator --> PTY master --> TTY driver --> PTY secondary --> shell
screen <-- terminal emulator <-- PTY master <-- TTY driver <-- PTY secondary <-- shell
```
The TTY driver does a few things:
1. Line editing. The TTY driver keeps a buffer of the current line. When you press backspace, the TTY driver removes the last character from the buffer, and the character gets deleted from the terminal screen. When you press enter, the TTY driver sends the buffer to the shell. This is called "cooked mode" and is the default mode for terminals.
2. Echo. The TTY driver echoes characters back to the terminal emulator, so that the characters you type are displayed on the screen.
3. Handling special characters like Ctrl+C, Ctrl+Z, and Ctrl+D: when you press Ctrl+C, the TTY driver sends a SIGINT signal to the foreground process group, which usually causes the process to exit.
The reason for the TTY driver is historical: back in the day, the TTY driver was a kernel module that handled communication with physical terminals. The pseudoterminal was designed to fit into the same framework. So the OS has a TTY driver even when there's no physical terminal.
## 什么是 TTY 驱动?
但中间还有另一个组件:TTY 驱动。TTY 驱动位于 PTY 主端和从端之间。
```
键盘输入 --> 终端模拟器 --> PTY 主端 --> TTY 驱动 --> PTY 从端 --> Shell
屏幕显示 <-- 终端模拟器 <-- PTY 主端 <-- TTY 驱动 <-- PTY 从端 <-- Shell
```
TTY 驱动负责以下几件事:
1. **行编辑**。TTY 驱动维护当前行的缓冲区。当你按下退格键时,TTY 驱动从缓冲区中删除最后一个字符,该字符也会从终端屏幕上消失。当你按下回车键时,TTY 驱动将缓冲区内容发送给 Shell。这称为"熟模式"(cooked mode),是终端的默认模式。
2. **回显**。TTY 驱动将字符回显给终端模拟器,使你输入的字符显示在屏幕上。
3. **处理特殊字符**,如 Ctrl+C、Ctrl+Z 和 Ctrl+D:当你按下 Ctrl+C 时,TTY 驱动向前台进程组发送 SIGINT 信号,通常会导致进程退出。
TTY 驱动存在的原因是历史性的:早年间,TTY 驱动是一个内核模块,负责处理与物理终端的通信。伪终端的设计是为了融入同一套框架。因此,即使没有物理终端,操作系统中也有 TTY 驱动。
---
## raw mode and cooked mode
As I mentioned above, in "cooked mode", the TTY driver does line editing. Programs can put the TTY into "raw mode" which disables line editing.
Most interactive programs (like `vim`, `python`, `bash`) put the terminal into raw mode and handle all the keyboard input themselves, because they need to do their own line editing (e.g. handling arrow keys for history in bash) and they don't want the TTY driver to intercept special characters like Ctrl+C.
For example, when you run vim, vim puts the terminal into raw mode. If you press Ctrl+C in vim, vim will handle the Ctrl+C itself (by cancelling the current operation), instead of the TTY driver sending a SIGINT signal to vim and killing it.
## 原始模式与熟模式
如前所述,在"熟模式"(cooked mode)下,TTY 驱动负责行编辑。程序可以将 TTY 设置为"原始模式"(raw mode),这会禁用行编辑。
大多数交互式程序(如 `vim`、`python`、`bash`)会将终端切换到原始模式,自行处理所有键盘输入,因为它们需要实现自己的行编辑功能(例如 bash 中用方向键浏览历史记录),并且不希望 TTY 驱动拦截 Ctrl+C 等特殊字符。
例如,当你运行 vim 时,vim 会将终端设置为原始模式。如果你在 vim 中按下 Ctrl+C,vim 会自行处理这个 Ctrl+C(取消当前操作),而不是让 TTY 驱动向 vim 发送 SIGINT 信号并将其终止。
---
## ANSI escape codes
Earlier I mentioned ANSI escape codes. These are sequences of characters that terminals interpret as commands. For example:
- `\x1b[34m` means "change the text colour to blue"
- `\x1b[0m` means "reset the text colour to the default"
- `\x1b[1m` means "bold text"
- `\x1b[2J` means "clear the screen"
The `\x1b` is the escape character (ASCII code 27). The `[` character is the "Control Sequence Introducer". The rest of the sequence is the command.
When a shell script uses `echo -e "\x1b[34mhello\x1b[0m"`, it's telling the terminal emulator to display "hello" in blue.
## ANSI 转义码
前面我提到了 ANSI 转义码。这些是终端将其解释为命令的字符序列。例如:
- `\x1b[34m` 表示"将文本颜色改为蓝色"
- `\x1b[0m` 表示"将文本颜色重置为默认值"
- `\x1b[1m` 表示"粗体文本"
- `\x1b[2J` 表示"清屏"
`\x1b` 是转义字符(ASCII 码 27)。`[` 字符是"控制序列引导符"(Control Sequence Introducer)。序列的其余部分是具体命令。
当 Shell 脚本使用 `echo -e "\x1b[34mhello\x1b[0m"` 时,它是在告诉终端模拟器以蓝色显示"hello"。
---
## what's a shell?
I've been talking about shells throughout, but I haven't defined what a shell is. A shell is a program that:
1. Reads commands from the user (from a TTY or from a script file)
2. Runs those commands
3. Shows the output to the user
Examples of shells are bash, zsh, fish, and sh.
How does a shell run a command? The shell uses the `fork` and `exec` system calls. `fork` creates a copy of the shell process, and `exec` replaces the copy with the new program. The shell then waits for the program to finish.
The shell also sets up the child process's stdin/stdout/stderr, and handles job control (foreground/background processes).
## 什么是 Shell?
我在整篇文章中一直在提 Shell,但还没有给出定义。Shell 是一个程序,它:
1. 从用户处读取命令(来自 TTY 或脚本文件)
2. 执行这些命令
3. 将输出显示给用户
Shell 的例子有 bash、zsh、fish 和 sh。
Shell 是如何运行命令的?Shell 使用 `fork` 和 `exec` 系统调用。`fork` 创建 Shell 进程的一个副本,`exec` 将该副本替换为新程序。然后 Shell 等待程序执行完毕。
Shell 还负责设置子进程的 stdin/stdout/stderr,以及处理任务控制(前台/后台进程)。
---
## what happens when you run `ls`?
Let's walk through what happens when you type `ls` in a terminal emulator.
1. You type `l` on the keyboard
2. The terminal emulator receives the `l` character and sends it to the PTY master
3. The TTY driver receives the `l` character and (in cooked mode) echoes it back to the PTY master
4. The terminal emulator receives the echoed `l` character and displays it on the screen
5. Same for `s` and then Enter
6. When Enter is pressed, the TTY driver sends the buffer `ls\n` to the PTY secondary
7. The shell reads `ls\n` from the PTY secondary
8. The shell forks and execs `ls`
9. `ls` writes its output to its stdout (which is the PTY secondary)
10. The TTY driver passes the output through to the PTY master
11. The terminal emulator reads the output from the PTY master and displays it on the screen
## 运行 `ls` 时发生了什么?
让我们来梳理一下在终端模拟器中输入 `ls` 时发生的事情。
1. 你在键盘上按下 `l`
2. 终端模拟器接收到字符 `l`,并将其发送到 PTY 主端
3. TTY 驱动接收到字符 `l`,并(在熟模式下)将其回显给 PTY 主端
4. 终端模拟器接收到回显的字符 `l`,并将其显示在屏幕上
5. `s` 和回车键同理
6. 当按下回车键时,TTY 驱动将缓冲区内容 `ls\n` 发送到 PTY 从端
7. Shell 从 PTY 从端读取 `ls\n`
8. Shell 执行 fork 和 exec 来运行 `ls`
9. `ls` 将其输出写入 stdout(即 PTY 从端)
10. TTY 驱动将输出传递到 PTY 主端
11. 终端模拟器从 PTY 主端读取输出,并将其显示在屏幕上
---
## what about SSH?
SSH is interesting because it involves two computers. Here's what happens when you run `ssh user@server` and then run `ls`:
1. You type `ls` on the keyboard
2. Your terminal emulator sends `ls` to the PTY master on your local computer
3. The TTY driver echoes the characters back
4. The terminal emulator displays the echoed characters on the screen
5. The **SSH client** reads the characters from the PTY master and sends them over the network to the SSH server
6. The SSH server receives the characters and writes them to the PTY secondary on the remote computer
7. The shell running on the remote computer reads the characters from the PTY secondary
8. The shell runs `ls` and writes the output to the PTY secondary on the remote computer
9. The SSH server reads the output from the PTY master on the remote computer and sends it over the network to the SSH client
10. The SSH client writes the output to the PTY secondary on the local computer
11. The terminal emulator on the local computer reads the output from the PTY master and displays it on the screen
Wait, that doesn't make sense. Let me reconsider. The SSH client is running on the local computer and the SSH server is running on the remote computer. The SSH client replaces the shell in the local PTY.
## SSH 呢?
SSH 很有趣,因为它涉及两台计算机。下面是当你运行 `ssh user@server` 后再运行 `ls` 时发生的事情:
1. 你在键盘上输入 `ls`
2. 你的终端模拟器将 `ls` 发送到本地计算机的 PTY 主端
3. TTY 驱动将字符回显
4. 终端模拟器将回显的字符显示在屏幕上
5. **SSH 客户端**从 PTY 主端读取字符,并通过网络将其发送到 SSH 服务器
6. SSH 服务器接收字符,并将其写入远程计算机的 PTY 从端
7. 在远程计算机上运行的 Shell 从 PTY 从端读取字符
8. Shell 运行 `ls`,并将输出写入远程计算机的 PTY 从端
9. SSH 服务器从远程计算机的 PTY 主端读取输出,并通过网络发送给 SSH 客户端
10. SSH 客户端将输出写入本地计算机的 PTY 从端
11. 本地计算机上的终端模拟器从 PTY 主端读取输出,并将其显示在屏幕上
等等,这说不通。让我重新理一理。SSH 客户端运行在本地计算机上,SSH 服务器运行在远程计算机上。SSH 客户端在本地 PTY 中取代了 Shell 的角色。
---
Here's a better diagram for what happens with SSH:
**Local computer:**
```
keyboard --> terminal emulator --> PTY master --> TTY driver --> PTY secondary --> ssh client
screen <-- terminal emulator <-- PTY master <-- TTY driver <-- PTY secondary <-- ssh client
```
**Remote computer:**
```
(network) --> ssh server --> PTY master --> TTY driver --> PTY secondary --> shell
(network) <-- ssh server <-- PTY master <-- TTY driver <-- PTY secondary <-- shell
```
So the SSH client is the "shell" from the perspective of the local terminal emulator, and the SSH server is the "terminal emulator" from the perspective of the remote shell. The SSH client and server communicate over the network using the SSH protocol.
One interesting thing about SSH is that both the local and remote computers have a PTY. The local PTY handles things like Ctrl+C on the local side (in addition to the remote PTY handling Ctrl+C on the remote side).
下面是 SSH 场景下更清晰的示意图:
**本地计算机:**
```
键盘输入 --> 终端模拟器 --> PTY 主端 --> TTY 驱动 --> PTY 从端 --> ssh 客户端
屏幕显示 <-- 终端模拟器 <-- PTY 主端 <-- TTY 驱动 <-- PTY 从端 <-- ssh 客户端
```
**远程计算机:**
```
(网络) --> ssh 服务器 --> PTY 主端 --> TTY 驱动 --> PTY 从端 --> Shell
(网络) <-- ssh 服务器 <-- PTY 主端 <-- TTY 驱动 <-- PTY 从端 <-- Shell
```
因此,从本地终端模拟器的角度来看,SSH 客户端扮演的是"Shell"的角色;而从远程 Shell 的角度来看,SSH 服务器扮演的是"终端模拟器"的角色。SSH 客户端和服务器通过 SSH 协议在网络上进行通信。
SSH 有一个有趣的地方:本地和远程计算机各自都有一个 PTY。本地 PTY 在本地端处理 Ctrl+C 等操作(同时远程 PTY 在远程端也处理 Ctrl+C)。
---
## what about `tmux`?
`tmux` is a terminal multiplexer: it lets you have multiple terminal sessions in a single window. It's also interesting from a PTY perspective.
When you run `tmux`, `tmux` creates a new PTY for each window/pane and runs a shell in each PTY. `tmux` itself is the "terminal emulator" for those shells, but `tmux` is also running inside a PTY (the one created by your actual terminal emulator).
So if you're running `tmux` inside iTerm, the chain is:
```
iTerm --> PTY --> tmux --> PTY --> shell
```
`tmux` acts as both a terminal emulator (for the shells it manages) and as a program (from iTerm's perspective).
This is why `tmux` can keep running even if you close iTerm – the `tmux` process and its shells are attached to PTYs that don't depend on iTerm. When you reconnect to `tmux`, it re-attaches to the existing PTYs.
## `tmux` 呢?
`tmux` 是一个终端复用器:它允许你在单个窗口中拥有多个终端会话。从 PTY 的角度来看,它也很有趣。
当你运行 `tmux` 时,`tmux` 为每个窗口/面板创建一个新的 PTY,并在每个 PTY 中运行一个 Shell。`tmux` 本身充当这些 Shell 的"终端模拟器",但 `tmux` 自身也运行在一个 PTY 中(由你实际使用的终端模拟器创建的那个)。
因此,如果你在 iTerm 中运行 `tmux`,整个链路是:
```
iTerm --> PTY --> tmux --> PTY --> shell
```
`tmux` 既充当终端模拟器(对其管理的 Shell 而言),又作为一个普通程序(从 iTerm 的角度而言)。
这就是为什么即使你关闭 iTerm,`tmux` 仍然可以继续运行——`tmux` 进程及其 Shell 附加在不依赖 iTerm 的 PTY 上。当你重新连接到 `tmux` 时,它会重新附加到已有的 PTY 上。
---
## summary
Here's a summary of what we've covered:
- **Physical terminals**: the historical predecessors to terminal emulators. Teletypes that sent characters to the computer and received characters back.
- **TTY**: short for teletypewriter. In Linux, TTY devices are in `/dev/`. Historically they corresponded to physical terminals.
- **Terminal emulators**: programs that emulate physical terminals in software (iTerm2, GNOME Terminal, etc.)
- **Pseudoterminals (PTYs)**: fake terminals – an API that looks like a terminal to programs. Has a master side (used by the terminal emulator) and a secondary side (used by the shell).
- **TTY driver**: sits between PTY master and secondary. Handles line editing (cooked mode), echo, and special characters (Ctrl+C, etc.)
- **Raw mode vs cooked mode**: in cooked mode, the TTY driver does line editing. In raw mode, the program handles all input itself.
- **ANSI escape codes**: sequences of characters that terminals interpret as commands (e.g. change text colour, clear screen).
- **Shell**: a program that reads commands, runs them, and shows output. Uses `fork` and `exec` system calls. Examples: bash, zsh, fish.
## 总结
以下是我们所涵盖内容的总结:
- **物理终端**:终端模拟器的历史前身。向计算机发送字符并接收字符的电传打字机。
- **TTY**:teletypewriter 的缩写。在 Linux 中,TTY 设备位于 `/dev/` 下。历史上对应物理终端。
- **终端模拟器**:在软件层面模拟物理终端的程序(iTerm2、GNOME Terminal 等)。
- **伪终端(PTY)**:虚拟终端——一种对程序来说看起来像终端的 API。有主端(由终端模拟器使用)和从端(由 Shell 使用)。
- **TTY 驱动**:位于 PTY 主端和从端之间。处理行编辑(熟模式)、回显和特殊字符(Ctrl+C 等)。
- **原始模式与熟模式**:熟模式下,TTY 驱动负责行编辑;原始模式下,程序自行处理所有输入。
- **ANSI 转义码**:终端将其解释为命令的字符序列(例如更改文本颜色、清屏)。
- **Shell**:读取命令、执行命令并显示输出的程序。使用 `fork` 和 `exec` 系统调用。例如:bash、zsh、fish。