subprocess.run() / Popen():默认不是 PTYsubprocess + PTYPTY 很重要?不一定。Python 的系统调用本身不会自动创建 PTY,要看你调用的是什么 API。
可以把它分成三种情况:
subprocess.run() / Popen():默认不是 PTY例如:
import subprocess
subprocess.run(["bash"])
默认情况下,Python 会创建子进程,并给它:
stdin → pipe
stdout → pipe
stderr → pipe
大致是:
Python
│
├── pipe ──> bash stdin
│
├── pipe <── bash stdout
│
└── pipe <── bash stderr
这里没有 PTY。
所以:
python test.py
里面启动:
subprocess.run(["bash"])
这个 bash 通常会发现:
tty
不是一个真正的 /dev/pts/N。
Python 有标准库:
import pty
pty.spawn("/bin/bash")
这时候才会创建 PTY。
结构变成:
Python
│
│ forkpty()
▼
PTY master
│
│
▼
/dev/pts/XX
│
▼
bash
更准确地说,Linux 内核会创建:
PTY master
PTY slave
slave 对应:
/dev/pts/12
bash 运行在 slave 上。
subprocess + PTY你也可以自己使用:
import os
import pty
pid, fd = pty.fork()
if pid == 0:
os.execvp("bash", ["bash"])
else:
# fd 是 PTY master
...
这就是很多 Web Terminal、SSH Terminal、终端管理器的基础。
结构:
Python
│
pty.fork()
│
┌────────┴────────┐
▼ ▼
PTY master PTY slave
fd = 5 /dev/pts/12
│ │
│ ▼
│ bash
│
▼
Python 读写
PTY 很重要?这正好和你前面研究的 Zellij / ttyd / Web SSH 联系起来。
如果你:
subprocess.Popen(
["bash"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
)
bash 看到的不是终端,而是 pipe。
所以很多终端程序的行为会不一样,例如:
bash
vim
top
htop
ssh
sudo
它们通常需要知道:
“我是不是连接在一个真正的 terminal 上?”
PTY 会提供终端语义,例如:
isatty()Ctrl+CCtrl+ZCtrl+DCtrl+LPython subprocess
│
├── 默认 ──> pipe
│
└── 使用 pty ──> PTY
而 Zellij 更接近:
Zellij
│
├── 创建 PTY
│ │
│ └── bash
│
├── Client A
│
├── Client B
│
└── Client C
所以如果你正在用 Rust 实现你前面说的那个极简 Zellij,核心其实就是:
forkpty()
↓
bash
↓
PTY master
↓
你的 Session Manager
↓
多个 terminal client
PTY master 是“程序控制 Shell”的入口;多个终端 attach 时,实际上是多个 client 共享/复用这个 PTY 的输入输出。