1. 引言:从物理终端到伪终端
在 Linux/Unix 系统中,终端是用户与系统交互的核心界面。当我们打开一个终端模拟器(如 GNOME Terminal、iTerm2、Windows Terminal),输入命令并看到输出时,背后是一整套复杂的终端设备抽象体系在运作。TTY、PTS、PTY 这些术语正是这套体系的核心组成部分。
一句话理解:
- TTY(Teletypewriter):任何类型的终端设备的总称,源于早期电传打字机
- PTS(Pseudo-Terminal Slave):伪终端从设备,现代终端模拟器的“客户端”
- PTY(Pseudo-Terminal):伪终端设备对(主设备 PTMX + 从设备 PTS)
graph LR
subgraph "物理终端时代"
A[电传打字机] --> B[TTY设备]
end
subgraph "现代虚拟终端"
C[终端模拟器] --> D[PTY主设备]
D --> E[PTS从设备]
E --> F[Shell进程]
end
style A fill:#f9f9f9
style C fill:#e1f5fe
style E fill:#c8e6c9
本文将系统性地剖析这些终端概念的历史渊源、工作原理和现代应用场景,帮助读者彻底理解每次打开终端时后台发生的一切。
2. TTY 的起源与演进
2.1 历史:电传打字机(Teletypewriter)
TTY 是 Teletypewriter 的缩写,直译为“电传打字机”。在计算机早期时代(1960-1970年代),大型机(Mainframe)非常昂贵,通常放在专门的机房中。用户无法直接坐在“电脑前”操作,而是通过一种叫做“电传打字机”的设备连接到中央计算机。
物理 TTY 的工作方式:
- 用户在电传打字机上敲击键盘
- 按键信号通过串行电缆发送给计算机
- 计算机处理后将输出结果打印在纸上(后来才出现显示器)
著名的 ASR-33 Teletype 是当时最流行的型号,每分钟只能打印 10 个字符。Unix 系统最初就是为连接这种 TTY 设备而设计的,TTY 这个名字也因此被固化在系统中。
2.2 终端类型演进
| 时代 | 终端类型 | 特点 | 典型例子 |
|---|---|---|---|
| 1960s-1970s | 物理 TTY | 电传打字机,纸带输入/输出 | ASR-33 Teletype |
| 1970s-1980s | 智能终端 | 独立显示器,支持光标控制 | VT100、VT220 |
| 1980s-1990s | 控制台终端 | 直接连接在主机上的显示设备 | Linux 虚拟控制台 |
| 1990s-至今 | 伪终端 | 图形界面下的终端模拟器 | GNOME Terminal、iTerm2 |
2.3 现代 Linux 中的 TTY 类型
在现代 Linux 系统中,/dev 目录下存在多种终端设备:
# 查看系统中的终端设备
ls -la /dev/tty*
ls -la /dev/pts/*
| 设备类型 | 设备路径示例 | 说明 |
|---|---|---|
| 当前控制终端 | /dev/tty |
指向当前进程的控制终端 |
| 虚拟控制台 | /dev/tty1 ~ /dev/tty63 |
物理机上的文本控制台(Ctrl+Alt+F1~F6) |
| 伪终端从设备 | /dev/pts/0 ~ /dev/pts/255 |
终端模拟器使用的从设备 |
| 串口终端 | /dev/ttyS0, /dev/ttyUSB0 |
通过串口连接的物理终端 |
| SSH 会话 | /dev/pts/* |
SSH 登录也会分配伪终端 |
3. PTY 与 PTS:伪终端的核心架构
3.1 什么是伪终端(PTY)?
伪终端(PTY, Pseudo-Terminal) 是一个软件模拟的终端设备,用于在图形界面下模拟传统物理终端的行为。它由一对双向设备组成:
- 主设备(PTMX / PTY Master):由终端模拟器程序打开和控制
- 从设备(PTS, Pseudo-Terminal Slave):由 Shell 进程使用,行为类似于传统 TTY
graph TD
subgraph "用户空间"
A[终端模拟器<br>GNOME Terminal]
C[Shell 进程<br>bash/zsh]
end
subgraph "内核空间"
B[PTY Master<br>/dev/ptmx]
D[PTS Slave<br>/dev/pts/0]
end
A -->|写入| B
B -->|传递| D
D -->|读取| C
C -->|写入| D
D -->|传递| B
B -->|读取| A
style B fill:#e1f5fe
style D fill:#c8e6c9
3.2 PTY 的数据流
以用户在终端模拟器中输入 ls 命令为例:
1. 用户按下键盘 → 终端模拟器捕获按键
2. 终端模拟器将 'l'、's'、'\n' 写入 PTY Master (/dev/ptmx)
3. 内核将数据从 Master 传递给对应的 Slave (/dev/pts/0)
4. Shell 从 /dev/pts/0 读取数据,识别为命令
5. Shell 执行命令,输出 "file1 file2..."
6. Shell 将输出写入 /dev/pts/0
7. 内核将输出从 Slave 传回 Master
8. 终端模拟器从 Master 读取输出并显示到屏幕
3.3 PTS 与 PTMX 的关系
| 组件 | 全称 | 角色 | 打开方式 |
|---|---|---|---|
| PTMX | PTY Master | “服务端”,由终端模拟器控制 | open("/dev/ptmx", ...) |
| PTS | PTY Slave | “客户端”,由 Shell 使用 | ptsname() 获取路径后打开 |
历史变迁:
- 传统 Unix:
/dev/pty[p-za-e][0-9a-f](大量预先创建的设备文件) - 现代 Linux:
/dev/ptmx+/dev/pts/(动态分配,Unix 98 标准)
# 查看当前使用的伪终端
$ tty
/dev/pts/0
# 查看系统中所有 PTS 设备
$ ls /dev/pts/
0 1 2 3 ptmx
4. TTY 命令与工具
4.1 tty 命令:查看当前终端
# 查看当前终端的设备路径
$ tty
/dev/pts/0
# 在 SSH 会话中
$ tty
/dev/pts/1
# 在物理控制台(Ctrl+Alt+F2)
$ tty
/dev/tty2
4.2 who 命令:查看登录用户及其终端
$ who
user :0 2024-01-15 09:00 (:0)
user pts/0 2024-01-15 09:05 (192.168.1.100)
user tty2 2024-01-15 09:10
输出解读:
:0:图形界面显示(第一个 X 会话)pts/0:伪终端,来自远程 SSH 连接tty2:虚拟控制台
4.3 ps 命令查看进程关联的终端
# 查看进程的 TTY 关联
$ ps aux | grep bash
user 1234 0.0 0.1 12345 6789 pts/0 Ss 09:00 0:00 -bash
user 5678 0.0 0.1 23456 7890 tty2 Ss+ 09:10 0:00 /bin/login
# TTY 列说明
# pts/0 -> 关联到伪终端
# tty2 -> 关联到虚拟控制台
# ? -> 守护进程,无控制终端
4.4 stty 命令:配置终端参数
# 查看当前终端设置
stty -a
# 禁用回车时的回车字符转换
stty -icrnl
# 设置删除键为 Backspace
stty erase ^H
# 显示特殊字符设置
stty -a | grep -E 'erase|kill|intr|quit'
常见终端控制字符:
| 控制字符 | 功能 | 默认按键 |
|---|---|---|
| INTR | 中断当前进程 | Ctrl+C |
| QUIT | 退出并生成 core dump | Ctrl+\ |
| SUSP | 挂起当前进程 | Ctrl+Z |
| ERASE | 删除前一个字符 | Backspace / Ctrl+H |
| KILL | 删除整行 | Ctrl+U |
| EOF | 文件结束 | Ctrl+D |
5. 终端的工作模式
5.1 规范模式 vs 原始模式
终端驱动可以工作在不同的模式下:
| 模式 | 特点 | 典型应用 |
|---|---|---|
| 规范模式 | 行缓冲,编辑功能(退格、删除) | 普通 Shell 输入 |
| 原始模式 | 字符立即传递,无行编辑 | vim、less、top 等全屏程序 |
| cbreak 模式 | 字符立即传递,但信号处理保留 | 部分交互式程序 |
graph LR
A[按键] --> B{终端模式}
B -->|规范模式| C[行缓冲<br>Enter 后才发送]
B -->|原始模式| D[立即发送]
C --> E[Shell 处理]
D --> E
5.2 通过 stty 切换模式
# 原始模式(常用于编程)
stty raw
# 恢复到规范模式
stty cooked
# 保存当前设置
stty -g > stty.save
stty raw
# ... 执行需要原始模式的操作 ...
stty $(cat stty.save)
5.3 作业控制与信号
终端与进程组、会话、信号密切关联:
| 信号 | 触发方式 | 作用 |
|---|---|---|
SIGINT |
Ctrl+C |
中断前台进程组 |
SIGTSTP |
Ctrl+Z |
挂起前台进程组 |
SIGQUIT |
Ctrl+\ |
退出前台进程组 |
SIGHUP |
终端关闭 | 通知进程控制终端断开 |
# 后台运行并忽略挂起信号
nohup ./long_running_task &
# 使用 disown 将进程从当前终端脱离
./task &
disown
6. 实战场景
6.1 场景一:理解 SSH 登录的终端分配
当通过 SSH 登录远程服务器时,SSH 服务端会自动分配一个伪终端:
# 客户端执行 SSH
$ ssh user@server
# 服务器端产生一个新 PTS
$ who
user pts/2 2024-01-15 10:00 (192.168.1.100)
# 查看进程树
$ pstree -p
sshd(1234)───sshd(5678)───bash(5679)───pstree(5680)
关键点:
- SSH 守护进程监听连接
- 认证成功后,
sshd调用fork()创建子进程 - 子进程打开
/dev/ptmx获取 PTY 主设备 - 子进程
fork()后再setsid()创建新会话 - 子进程将 STDIN/STDOUT/STDERR 重定向到 PTS 从设备
- 最后
exec()执行 Shell
6.2 场景二:screen / tmux 的终端工作原理
终端复用器本质上是创建了一个独立的伪终端层次:
graph TD
A[终端模拟器<br>/dev/pts/0] --> B[tmux 客户端]
B --> C[tmux 服务器]
C --> D[tmux 窗格1<br>/dev/pts/10]
C --> E[tmux 窗格2<br>/dev/pts/11]
C --> F[tmux 窗格3<br>/dev/pts/12]
D --> G[Shell 1]
E --> H[Shell 2]
F --> I[Shell 3]
style C fill:#e1f5fe
工作原理:
tmux服务器启动时,创建自己的 PTY 主设备- 每个窗格对应一个 PTY 从设备
tmux attach时,客户端读取服务器状态并显示tmux detach时,客户端退出但服务器继续运行- 重新
attach时,客户端重新连接到同一个服务器
验证:
# 开启 tmux
$ tmux
$ tty
/dev/pts/10 # tmux 内的终端
# 在另一个终端查看
$ ps aux | grep tmux
user 1234 0.0 0.1 12345 6789 ? Ss 09:00 0:00 tmux
# 注意:tmux 服务器的 TTY 列是 ?,表示无控制终端
6.3 场景三:守护进程为何脱离终端
守护进程(如 nginx、sshd、cron)通常会主动脱离终端:
// 典型的守护进程创建步骤
pid_t pid = fork();
if (pid > 0) exit(0); // 父进程退出
setsid(); // 创建新会话,脱离原终端
chdir("/"); // 切换到根目录
umask(0); // 重设文件掩码
close(0); close(1); close(2); // 关闭标准文件描述符
open("/dev/null", O_RDWR); // 重定向到 /dev/null
原因:
- 避免终端关闭时收到
SIGHUP信号 - 防止被前台进程组信号影响
- 实现后台独立运行
6.4 场景四:/dev/tty 的特殊用途
/dev/tty 始终指向当前进程的控制终端,无论它是什么类型:
# 在任何终端中执行
$ echo "hello" > /dev/tty
hello # 输出到当前屏幕
# 编写需要密码输入的程序时
$ cat > getpass.sh << 'EOF'
#!/bin/bash
echo -n "Enter password: "
stty -echo
read password < /dev/tty
stty echo
echo
echo "Password received"
EOF
$ chmod +x getpass.sh
应用场景:
- 从标准输入重定向的程序中读取用户输入
su、sudo等程序读取密码- 交互式脚本确保从终端读取,不受输入重定向影响
7. 常见问题与排查
7.1 PTS 耗尽问题
# 问题:无法打开新的终端窗口
# 错误:ptyp: No more ptys
# 查看当前 PTS 使用情况
$ ls /dev/pts/ | wc -l
256
# 修改系统最大 PTS 数量
$ echo "fs.nr_open=1048576" >> /etc/sysctl.conf
$ sysctl -p
# 清理僵尸 PTS(需要找到并终止对应进程)
$ fuser -v /dev/pts/0
7.2 终端乱码问题
# 问题:输出显示乱码
# 查看当前编码
$ locale
LANG=en_US.UTF-8
# 设置正确的编码
$ export LANG=en_US.UTF-8
$ export LC_ALL=en_US.UTF-8
# 重置终端
$ reset
# 查看终端类型
$ echo $TERM
xterm-256color
7.3 作业控制失效
# Ctrl+Z 无法挂起进程
# 检查作业控制是否启用
$ set -o | grep monitor
monitor on
# 启用作业控制
$ set -m
# 检查终端是否支持作业控制
$ stty -a | grep -E 'isig|ixon'
7.4 SIGHUP 导致的进程终止
# 场景:关闭终端后后台进程被终止
# 方法1:使用 nohup
nohup ./long_task &
# 方法2:使用 disown
./long_task &
disown
# 方法3:使用 setsid
setsid ./long_task
# 方法4:在子 shell 中使用 &(屏蔽 HUP)
( ./long_task & )
8. 总结:终端抽象的本质
8.1 核心概念速记表
| 概念 | 含义 | 定位 | 现代对应 |
|---|---|---|---|
| TTY | 终端设备的总称 | 抽象概念 | 任何终端 |
| PTY | 伪终端设备对 | 内核机制 | ptmx + pts |
| PTS | 伪终端从设备 | 实际使用者 | /dev/pts/n |
| PTMX | 伪终端主设备 | 模拟器控制端 | /dev/ptmx |
8.2 层次关系图
graph TD
subgraph "用户交互层"
A[终端模拟器<br>GNOME Terminal]
B[SSH 客户端]
C[getty 登录]
end
subgraph "终端抽象层"
D[PTY 主设备<br>/dev/ptmx]
E[PTY 从设备<br>/dev/pts/0]
F[虚拟控制台<br>/dev/tty2]
end
subgraph "应用层"
G[Shell<br>bash/zsh]
H[用户程序<br>vim/top]
end
A --> D
B --> D
C --> F
D --> E
E --> G
F --> G
G --> H
style D fill:#e1f5fe
style E fill:#c8e6c9
style F fill:#fff3e0
8.3 最终总结
-
TTY 是历史遗产,PTS 是现代化实现
- TTY 作为概念保留,但实际使用的是 PTS
- 理解历史有助于理解系统设计决策
-
终端是程序交互的桥梁
- 每个 Shell 进程都关联到一个终端设备
- 终端不仅负责输入输出,还处理信号和作业控制
-
伪终端是现代图形界面的基础
- 从 GUI 终端模拟器到 SSH 远程连接,都依赖 PTY/PTS
- 终端复用器(tmux/screen)基于 PTY 构建多路复用能力
掌握 TTY、PTS 等终端概念,意味着你理解了 Linux/Unix 系统与用户交互的底层机制。这不仅是系统编程的基础,也是排查终端相关问题的必备知识。无论是处理 SSH 连接异常、编写需要终端交互的程序,还是理解容器终端的实现原理,这些概念都是绕不开的知识点。
原文 https://blog.csdn.net/2301_79518550/article/details/145815565