不一定。Python 的系统调用本身不会自动创建 PTY,要看你调用的是什么 API。
可以把它分成三种情况:
subprocess.run()
Popen()
例如:
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。
/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
你也可以自己使用:
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+C
Ctrl+Z
Ctrl+D
Ctrl+L
Python 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 的输入输出。
Github开源生信云平台 DEMO