APE
文章标签归档关于

© 2026 APE.PUB

2026-10-26· 31 分钟

stdin-stdout

linuxfdstdinstdout

Linux 标准输入输出与文件描述符

本文整理自一次关于 stdin / stdout / stderr、文件描述符、终端设备和 ANSI 转义序列的讨论。

示例以 Linux / macOS 为准,两者的差异会单独标注。


目录

1. 三个标准流

2. 文件描述符 fd

3. stdout 与命令行的关系

4. fd 的指向由谁控制

5. 重定向与管道

6. 多个进程写同一个 fd 1 会乱吗

7. 如何查看 fd

8. 终端设备:tty、ttys、pts

9. isTTY:判断是否在真终端里

10. 用 ANSI 转义序列控制终端

11. 常见问题速查


1. 三个标准流

每个进程启动时,通常已经打开了三条通道:

| fd | 名称 | 默认去向 | 典型用途 |

|---|---|---|---|

| 0 | stdin(标准输入) | 终端键盘 / 管道读端 | 读用户输入、读管道数据 |

| 1 | stdout(标准输出) | 终端屏幕 / 文件 / 管道写端 | 正常输出、console.log |

| 2 | stderr(标准错误) | 同 stdout,但独立一条 | 错误信息、console.error |

fd 2 是存在的,它就是标准错误。错误信息和普通输出走两条不同的通道,所以:

ls /nonexistent > out.txt # 错误仍然显示在屏幕上(走 fd 2)
ls /nonexistent 2> err.txt # 这次错误被收进文件

三个流只是 POSIX 约定,不是内核强制。进程可以关掉它们,也可以把它们指向任何地方。

缓冲差异

  • 输出到终端:通常行缓冲(遇到换行刷新)
  • 输出到管道 / 文件:通常全缓冲(缓冲区满才刷新)
  • stderr:通常无缓冲,保证错误立即出现

这也是为什么程序崩溃时,普通输出可能丢了一部分,而错误信息还在。


2. 文件描述符 fd

fd(file descriptor)是进程内一张“已打开文件表”的下标编号。

进程不直接持有文件本身,只持有一个小整数,内核通过这个整数找到真正的对象:

进程 A 的 fd 表 内核中的打开文件对象(open file description)
┌────┬──────────────────┐
│ 0 │ ─────────────────┼──► 终端 pty(读)
│ 1 │ ─────────────────┼──► 终端 pty(写)
│ 2 │ ─────────────────┼──► 终端 pty(写)
│ 3 │ ─────────────────┼──► /tmp/a.txt
│ 11 │ ─────────────────┼──► socket 连接到 1.2.3.4:80
└────┴──────────────────┘

三个层次要分清

| 层次 | 是否每进程独立 | 说明 |

|---|---|---|

| fd 编号(0、1、2…) | 是 | 只是各自进程表里的下标,两个进程都有 1 并不冲突 |

| fd 表项 / open file description | 继承时共享,独立 open 时各自独立 | 保存文件偏移、读写模式等 |

| 目标(tty / 文件 / 管道 / socket) | 由 fd 指向决定 | 多个 fd 可以指向同一个目标 |

所以 Node 的 fd 1 和 Java 的 fd 1 是两个不同的表项,但通常指向同一个终端设备。关掉自己的 fd 1 不影响对方,改自己的重定向也不影响对方;但两边都往同一个终端写时,输出会交错。

编号分配规则

  • fd 号由内核分配,规则是取当前可用的最小号
  • 关闭一个 fd 后,这个号可以被后续 open 复用
  • 上限由 ulimit -n 决定(Linux 常见 1024 / 1048576,macOS 默认常常较小)

一个实际例子(Node):

const a = fs.openSync("/tmp/fd-demo.txt", "w"); // 可能是 11
const b = fs.openSync("/tmp/fd-demo.txt", "a"); // 可能是 12
fs.closeSync(a);
const c = fs.openSync("/tmp/fd-demo.txt", "a"); // 又拿到 11

为什么不从 3 开始?因为 Node 运行时自己(libuv、事件循环等)已经占用了若干 fd。fd 复用也是很多“关闭后继续使用旧 fd”类 bug 的来源。

fork / exec 时的继承

  • fork 会复制一份 fd 表,表项仍指向同一个 open file description
  • exec 会保留 fd 表(除非该 fd 设置了 close-on-exec)
  • 所以 shell 启动的 node、java、ls,默认都会继承 shell 的 0/1/2

3. stdout 与命令行的关系

“命令行”和 stdout 是两个层面的东西:

| 角色 | 是什么 | 负责什么 |

|---|---|---|

| 终端模拟器 | iTerm / Terminal / Kitty 等 GUI 程序 | 接收字节并绘制到屏幕 |

| shell | zsh / bash,一个普通进程 | 读你输入的命令、启动子进程、设置管道与重定向 |

| 你的进程 | node app.js、ls、java | 只往 fd 1 写字节,不关心终端长什么样 |

“命令行”是 shell 提供的交互界面:你打的字走 stdin,程序的输出走 stdout。stdout 不是屏幕,屏幕是终端模拟器画的。

默认靠“继承”连在一起

键盘 ──► 终端模拟器 ──► pty master ──► pty slave
│
┌───────────────────────┴──────────────┐
│ zsh fd 0/1/2 ──► pty slave │
│ │ fork + exec(继承 fd 表) │
│ ▼ │
│ node fd 0/1/2 ──► 同一个 pty │
└───────────────────────────────────────┘
│
屏幕 ◄── 终端模拟器 ◄── pty master ◄┘

echo hi(shell 内建)和 node app.js(子进程)的输出都出现在同一个窗口,不是因为它们“认识命令行”,而是因为它们的 fd 1 指向同一个终端设备。

推论

  • stdout 重定向到文件后,终端上就什么都看不到
  • 每个进程都有自己的 stdout;ls | wc 里,ls 的 stdout 是管道写端,wc 的 stdout 才是终端
  • console.log 写 fd 1,console.error 写 fd 2

4. fd 的指向由谁控制

内核只维护表和执行系统调用,不决定 fd 指向哪里。 指向由两方决定:

1. 父进程在 exec 之前(通常是 shell)布好线

2. 进程自己在运行时通过 open / dup2 / close / freopen 修改

启动时:shell 先布好线,再 exec

以 node app.js > app.log 为例:

iTerm 打开 pty 对,把 slave 端接到 shell 的 fd 0/1/2
│
▼
zsh(fd 1 → /dev/ttys003)
│ fork:子进程复制 fd 表
│
│ 子进程里(exec 之前):
│ fd = open("app.log", O_WRONLY | O_CREAT | O_TRUNC)
│ dup2(fd, 1) ← 让 fd 1 指向 app.log
│ close(fd)
▼
exec("node") ← fd 表原样保留
│
▼
node 进程的 fd 1 → app.log

>、|、2>&1 本质上都是 dup2 的语法糖:

  • 2>&1 = 让 fd 2 指向 fd 1 当前指向的位置
  • cmd > f 2>&1:先重定向 stdout,再把 stderr 接到 stdout,两者都进文件
  • cmd 2>&1 > f:先把 stderr 接到“原来的终端”,再重定向 stdout,stderr 仍然打屏

运行时:只有进程自己能改

int fd = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0644);
dup2(fd, 1); // 从此 printf 都写到 log.txt
close(fd);
  • 父进程在 exec 之后不能再改子进程的 fd 表(除非调试器 ptrace 之类)
  • 其他无关进程也不能跨进程修改你的 fd
  • Node 核心没有暴露 dup2;JS 层替换 process.stdout.write 只影响走这个对象的代码,不改真正的 OS fd

Node 中 fd 1 的去向

process.stdout 只是 Node 启动时对 fd 1 的包装:

process.stdout.fd // 1
process.stdout.isTTY // true / undefined
fs.fstatSync(1) // dev / ino / rdev / isCharacterDevice()

不同启动方式下 fd 1 的指向:

| 启动方式 | fd 1 指向 | isTTY |

|---|---|---|

| 在终端直接 node app.js | pty slave | true |

| node app.js > app.log | 普通文件 | undefined |

| node app.js \| less | 管道写端 | undefined |

| IDE 运行面板 / CI | 通常是管道 | undefined |

| Docker(不带 -t) | 容器运行时接的管道 | undefined |

| nohup node app.js & | nohup.out 或 /dev/null | undefined |

| GUI 程序(从 Finder 启动) | /dev/null 或系统日志 | undefined |

Java 的 System.out 同理,是 fd 1 的包装;System.setOut(...) 只改 Java 层的输出对象,不改 OS fd。


5. 重定向与管道

| 语法 | 含义 |

|---|---|

| cmd > file | stdout 覆盖写入文件(O_TRUNC) |

| cmd >> file | stdout 追加写入(O_APPEND) |

| cmd 2> file | stderr 写入文件 |

| cmd 2>&1 | 让 stderr 指向 stdout 当前的目标 |

| cmd < file | stdin 从文件读 |

| cmd1 \| cmd2 | cmd1 的 stdout 接到 cmd2 的 stdin |

| cmd > /dev/null | 丢弃 stdout |

| cmd > /dev/tty | 强制写回当前控制终端,无视重定向 |

| cmd > out 2>&1 | stdout 和 stderr 都进 out |

共享与不共享

  • fork 继承:两个进程共享同一个 open file description,文件偏移也共享
  • 各自独立 open():即使打开同一个文件,也是两个 open file description,偏移各自独立
  • >>(O_APPEND):每次写入前定位到文件末尾,多次追加不会互相覆盖;但内容仍可能按块交错

对终端来说文件偏移无所谓,所以平时感知不到;对日志文件就会表现为“互相覆盖”或“各写各的”。


6. 多个进程写同一个 fd 1 会乱吗

会乱,而且这是常态。 平时看起来不乱,只是因为同一时刻通常只有一个前台进程在写。

前提是多个进程的 fd 1 指向同一个终端 / 管道 / 文件;指向不同目标时互不影响。

内核的保证边界

内核只保证到“单次 write() 调用”这一层:

| 目标 | 保证 |

|---|---|

| 管道 / FIFO | 单次 write() ≤ PIPE_BUF 是原子的(Linux 4096,macOS 实测 512);超过可能被拆开插入 |

| 普通文件 | 共享打开文件描述时,内核串行更新写位置;O_APPEND 的追加是原子的 |

| 终端 / pty | 有 tty 锁,小写入通常整块进入缓冲;但 POSIX 不保证终端写入的原子性 |

注意:这是“一次 write 不被打断”,不是“一条逻辑消息不被打断”。不同 write 之间的顺序完全由调度器决定。

交错示例

两个进程各写 4 行,每行一次 os.write:

os.write(1, b"A" * 40 + b"\n")

实际输出可能交替为:

AAAAAAAA...
BBBBBBBB...
AAAAAAAA...
BBBBBBBB...

每行完整,但 A/B 的先后顺序不确定。

什么情况下会花屏

1. ANSI 序列被拦腰截断:\x1b[31m 是多字节、有状态的,若在 \x1b 与 [ 之间插入其它字节,终端解析器状态会错位

2. 两个全屏程序抢同一个 tty:一个进备用屏、另一个移动光标,互相踩

3. 跨多次 write 的进度条 / \r 重绘:必然被插入

4. 独立 open 同一文件且不带 O_APPEND:偏移各自独立,会互相覆盖

如何避免

  • 物理隔离:tmux / screen 每个 pane 一个独立 pty
  • 重定向:后台进程 cmd > a.log 2>&1 &,别往终端写
  • 单写者:多线程 / 多协程输出时加锁,或汇总到一个日志队列,整行一次 write
  • TUI 独占终端:全屏程序运行时,任何 console.log、后台任务、shell 的 job 通知都会破坏画面;日志应写文件
  • 合成写入:把一帧的控制序列拼成一个字符串、一次 write,缩小被插入的窗口

7. 如何查看 fd

查看当前 shell / 当前进程

ls -l /dev/fd # 列出 0..N
lsof -p $$ -a -d 0-2 # 只看 0/1/2($$ = 当前 shell PID)

macOS 的 /dev/fd/1 会直接显示解析后的类型:

crw-rw-rw- ... /dev/fd/0 # 字符设备 → 终端
-rw-r--r-- ... /dev/fd/1 # 普通文件 → 被重定向
prw-r--r-- ... /dev/fd/2 # FIFO → 管道

Linux 上 /dev/fd 是 /proc/self/fd 的软链,会显示 1 -> /dev/pts/3。

注意:lsof 的多个筛选条件默认是 OR。要“某进程的某个 fd”,必须加 -a(AND):

lsof -p -a -d 1。漏掉 -a 会变成“该进程的所有文件 + 所有进程的 fd 1”。

查看任意进程

lsof -p <PID> # 该进程打开的所有对象
lsof -p <PID> -a -d 0-2 # 只看标准流
lsof -p <PID> -a -d 1 # 只看 fd 1
sudo lsof -p <PID> # root 进程需要 sudo
lsof -p <PID> -F pftn # 机器可读:pid / fd / type / name

Linux 专属、更直接:

ls -l /proc/<PID>/fd
readlink /proc/<PID>/fd/1

macOS 没有 /proc,且 /dev/fd/N 不是可 readlink 的软链,查别的进程基本就用 lsof。

反查:谁占着某个资源

lsof $(tty) # 谁打开了当前终端
lsof -t /dev/ttys003 # 只输出 PID(Linux 对应 /dev/pts/3)
lsof /path/to/file # 谁打开了这个文件
lsof -i :3000 # 谁占着 3000 端口
lsof -U # 所有 Unix socket
lsof +D /some/dir # 目录下所有被打开的文件
lsof +L1 # 链接数为 0 的已删除文件(仍被进程持有)

读 lsof 输出

COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
node 48211 jiayaooli 1u CHR 16,3 0t0 1234 /dev/ttys003
node 48211 jiayaooli 3w REG 1,16 4096 567 /tmp/app.log
bash 48002 jiayaooli 1u PIPE 0x... 0t0 ->0x7f...
  • FD 列:1u = fd 1,u 表示可读写,r / w 表示只读 / 只写;cwd、txt、mem 是伪条目,不是真正的 fd
  • TYPE 列:CHR 字符设备(终端)、REG 普通文件、DIR 目录、PIPE 管道、IPv4 / IPv6 网络、unix Unix socket
  • NAME 列:终端是 /dev/ttys00x,文件是路径,管道是 ->0x...,socket 是 本地->远端
  • 管道两端可以通过 lsof | grep 0x7f... 找到持有相同 id 的进程

在程序里查

Node:

process.stdout.fd // 1
process.stdin.fd // 0
process.stdout.isTTY // 是否终端
fs.fstatSync(1) // dev / ino / rdev / isCharacterDevice()
fs.readdirSync('/dev/fd') // 自己的 fd 号列表

Python:

import os
os.listdir('/dev/fd') # ['0', '1', '2', ...]
os.fstat(1) # 目标元信息

查不到的常见原因

  • 权限不够:加 sudo
  • fd 已经关闭:表里没有这一项
  • 进程已退出:fd 表随进程消失
  • macOS SIP:受保护进程即使 root 也可能看不全

8. 终端设备:tty、ttys、pts

ttys001、ttys007 是 macOS 的伪终端(pty)从设备节点,路径是 /dev/ttys001。一个终端会话(终端 App 的一个窗口 / 标签页、一个 tmux pane、一次 ssh 登录)对应一个这样的设备。

名字拆解

ttys001
│ │└── 实例编号:第几个 pty,内核按需分配
│ └── slave(从端)
└── teletype,终端设备的传统叫法

ttys001 和 ttys007 没有功能差异,只是两个不同的 pty 实例。

pty 是一对设备

iTerm2 / Terminal.app
│ 持有主端(通过 /dev/ptmx 分配)
▼
┌─────────────┐
│ pty 设备对 │
└─────────────┘
│
▼
/dev/ttys007 ←── shell 和它启动的所有进程,fd 0/1/2 指向这里
  • 键盘输入:终端模拟器写主端 → 内核转给从端 → shell 从 fd 0 读到
  • 程序输出:写从端 → 主端 → 终端模拟器收到字节并绘制

编号与生命周期

  • 内核分配时取当前可用的最小号
  • 会话关闭后号码可被回收复用
  • 看到 ttys007 不代表现在开着 7 个终端,只说明至少创建过这个实例
  • 关闭窗口 → pty 从端销毁;会话首进程会收到 SIGHUP,这是“关终端导致程序被杀”的根源
  • 节点属于登录用户,权限通常是 crw--w----,其他普通用户不能读写
  • 后台进程读控制终端会收到 SIGTTIN;开启 TOSTOP 时后台写会收到 SIGTTOU

容易混淆的设备名

| 名字 | 是什么 |

|---|---|

| /dev/ttys007(macOS) | pty 从端,一个终端会话 |

| /dev/pts/0(Linux) | Linux 的 pty 从端 |

| /dev/tty | 特殊别名,指向当前进程的控制终端,不固定是哪个 |

| /dev/console | 系统控制台(图形登录界面) |

| /dev/tty1(Linux) | 虚拟控制台(VT),不是 pty |

| /dev/ttyS0(Linux) | 串口,大写 S,和 ttys 无关 |

| /dev/tty.usbserial-xxx(macOS) | 真正的串口设备,名字里带点 |

查终端相关信息的命令

tty # 当前 pty → /dev/ttys007
echo $TTY # zsh 中也可以

ls -l /dev/ttys* # 所有 pty 节点(Linux 用 /dev/pts/*)
lsof /dev/ttys007 # 谁打开了这个 pty
ps -t ttys007 # 挂在这个终端上的进程(Linux 用 ps -t pts/7)
who # 哪些用户登录在哪些 tty 上
w # 更详细的登录会话信息

“当前终端的 PID”到底指什么

终端窗口本身和 pty 设备都没有 PID;有 PID 的是下面三者:

# 1. 当前 shell
echo $$
ps -o pid=,ppid=,comm= -p $$

# 2. 终端模拟器(大多数情况下是 shell 的父进程)
ps -o pid=,comm= -p $PPID
pgrep -x iTerm2
pgrep -x Terminal
pgrep -x WezTerm # 其他:kitty / Alacritty / ghostty / Warp

# 3. 挂在这个 tty 上的所有进程
lsof -t $(tty) | xargs ps -o pid=,ppid=,comm=

# 4. 当前前台命令
pgrep -P $$

如果父进程不是终端 App(VS Code 集成终端、tmux、ssh),需要沿进程树往上找:

p=$$
while (( p > 1 )); do
ps -o pid=,ppid=,comm= -p $p
p=$(ps -o ppid= -p $p | tr -d ' ')
done

在 tmux 中:

echo $TMUX # 有值说明在 tmux 里
tmux display-message -p '#{pid} #{client_pid}' # server PID 和客户端 PID

9. isTTY:判断是否在真终端里

Node 的 process.stdin.isTTY / process.stdout.isTTY:

  • 流指向真正的终端设备(tty / pty)时是 true
  • 被重定向到文件、管道、/dev/null 时是 undefined(注意不是 false)

常见守卫

if (!stdin.isTTY || !stdout.isTTY) {
console.error('这个程序需要在真正的终端里运行(不能重定向 / 管道)。');
process.exit(1);
}

等价于 !(stdin.isTTY && stdout.isTTY):两个都是 TTY 才放行。

拆解:

  • !stdin.isTTY = stdin 不是终端
  • !stdout.isTTY = stdout 不是终端
  • || = 任意一个不是终端,就成立

为什么必须两个都查

TUI 依赖两件只有真终端支持的事:

1. stdin.setRawMode(true):逐键读取;管道输入上调用会直接抛错

2. ANSI 转义序列:备用屏、隐藏光标、鼠标事件;重定向到文件时写进去全是乱码

只查 stdout 不够:echo hi | node app.js 时 stdout 仍是终端,但 stdin 是管道,raw mode 会挂。

其他语言 / 工具里的等价写法

isatty(1) // C
[ -t 1 ] # shell:stdout 是否终端
tty -s # 静默判断
sys.stdout.isatty()

常见陷阱

  • 不要写 === false:非 TTY 时是 undefined,undefined === false 为假,守卫会失效
  • 正向写法:const isInteractive = Boolean(process.stdin.isTTY && process.stdout.isTTY)

10. 用 ANSI 转义序列控制终端

ANSI 控制终端靠的是“往终端设备写入特殊字节”,终端模拟器收到后自己执行动作。 shell 不“执行”这些指令,只是把字节写到 stdout。

例如 \x1b[?1049h 是 6 个字节:

1b 5b 3f 31 30 34 39 68
ESC [ ? 1 0 4 9 h

终端模拟器看到后切换到备用屏。

为什么裸写会 bad pattern

在 zsh 里直接输入:

\x1b[?1049h

会报 zsh: bad pattern: x1b[?1049h。这不是终端的问题,而是 shell 在解析阶段就失败了:

1. \x1b 的反斜杠被 shell 的词法分析吃掉,得到词 x1b[?1049h

2. zsh 把裸词当 glob 模式,[...] 是字符类,而 [ 没有闭合 → bad pattern

3. 即使加引号,它仍是一个命令名:zsh: command not found: ...

要在命令行“输出”,必须有程序把字节写到 stdout。

正确写法

printf '\x1b[?1049h' # 最推荐,zsh / bash 通用,不加换行
printf '\033[?1049h' # 八进制写法,更 POSIX
echo -e '\x1b[?1049h' # bash 需要 -e
echo '\x1b[?1049h' # zsh 内置 echo 默认就解释(会多一个换行)
print -n -- $'\x1b[?1049h' # zsh 惯用法
printf '%s' $'\x1b[?1049h' # 通用:$'...' 先产生真实 ESC 字节

实测差异:

| 命令 | 实际写出的字节 |

|---|---|

| zsh -c 'echo "\x1b[?1049h"' | 1b 5b 3f 31 30 34 39 68 0a(有 ESC,多了换行) |

| zsh -c 'printf "\x1b[?1049h"' | 1b 5b 3f 31 30 34 39 68(干净) |

| bash -c 'echo "\x1b[?1049h"' | 字面量 \x1b[?1049h(bash echo 不解释) |

echo 会补 \n;进备用屏后多这一个换行,光标位置就和预期差一行。TUI 必须无换行,所以用 printf 或 -n。

必须写到终端

printf '\x1b[?1049h' > /dev/tty # 强制写回当前终端,无视重定向

如果 stdout 被重定向到文件或管道,字节只是进了文件,终端不会理你。

检查实际字节

printf '\x1b[?1049h' | xxd # 1b5b3f3130343968
printf '\x1b[?1049h' | cat -v # ^[[?1049h

常用序列

| 序列 | 作用 |

|---|---|

| \x1b[?1049h / \x1b[?1049l | 进入 / 离开备用屏 |

| \x1b[?25l / \x1b[?25h | 隐藏 / 显示光标 |

| \x1b[2J + \x1b[H | 清屏 + 光标回左上 |

| \x1b[;H | 光标定位到第 r 行第 c 列 |

| \x1b[?1000h / \x1b[?1006h | 开启鼠标事件 / SGR 鼠标编码 |

| \x1b[?2004h / \x1b[?2004l | 开启 / 关闭括号粘贴 |

用 tput 抽象

不想记转义码,可以用 terminfo:

tput smcup # 进入备用屏
tput rmcup # 离开备用屏
tput civis # 隐藏光标
tput cnorm # 显示光标

clear、less、vim 内部都是同一套写法。

在 TUI 中使用

stdout.write(T.altOn + T.bracketedOn + T.mouseOn + T.hide);

把一串控制序列拼成一个字符串、一次 write,可以减少被其他进程插入的机会。退出时按相反顺序恢复:

stdout.write(T.bracketedOff + T.mouseOff + T.show + T.altOff);

顺序很重要:先关粘贴 / 鼠标 / 光标,最后才离开备用屏,否则恢复过程会显示在错误的屏幕上。


11. 常见问题速查

| 问题 | 答案 |

|---|---|

| stdout 是什么? | 标准输出,文件描述符 1 |

| 有 fd 2 吗? | 有,stderr,标准错误 |

| fd 1 和 fd 2 会互相覆盖吗? | 不会,两条独立通道,除非显式 2>&1 |

| Node 的 fd 1 和 Java 的 fd 1 冲突吗? | fd 表互相独立;但目标可能都是同一个终端,所以输出会交错 |

| 谁决定 fd 指向哪里? | 父进程在 exec 前布好线,进程自己在运行时用 open / dup2 / close 修改 |

| process.stdout.fd 是多少? | 1 |

| 为什么 isTTY 是 undefined? | fd 指向的不是终端(重定向、管道、IDE、CI、Docker 等) |

| 多个进程写同一个终端会乱吗? | 会;内核只保证单次 write() 的原子性,不保证跨进程顺序 |

| PIPE_BUF 是多少? | Linux 4096,macOS 512(本机实测) |

| 怎么查 fd 指向? | lsof -p -a -d ;Linux 也可以 readlink /proc//fd/ |

| 怎么查当前终端是哪个设备? | tty |

| ttys001 是什么? | macOS 的 pty 从端设备节点;Linux 对应 /dev/pts/N |

| 终端窗口有 PID 吗? | 窗口和 pty 没有;终端 App、shell、进程各有 PID |

| 怎么查终端 App 的 PID? | ps -o pid=,comm= -p $PPID,或 pgrep -x iTerm2 |

| echo "\x1b[?1049h" 为什么执行了指令? | echo 把 ESC 字节写给了终端,终端模拟器解析并执行 |

| 裸写 \x1b[?1049h 为什么报 bad pattern? | shell 先做词法分析和 glob,[ 未闭合,根本没把字节写给终端 |

| 怎么可靠输出 ANSI 序列? | printf '\x1b[...';必要时 > /dev/tty |

| TUI 为什么要独占终端? | 多个进程写同一个 tty 会破坏光标、备用屏和转义序列状态 |


平台差异小结

| 主题 | Linux | macOS |

|---|---|---|

| 查看其他进程的 fd | /proc//fd + readlink | lsof -p |

| /dev/fd | 指向 /proc/self/fd 的软链 | 特殊节点,ls -l 直接显示目标类型 |

| pty 从端命名 | /dev/pts/N | /dev/ttysNNN |

| 虚拟控制台 | /dev/tty1 等 | 通常没有 Linux 风格的 VT |

| 串口 | /dev/ttyS0(大写 S) | /dev/tty.usbserial-* |

| PIPE_BUF | 4096 | 512 |

| ps -t 参数 | pts/7 | ttys007 |

| echo 的转义行为 | bash 默认不解释,需要 -e | zsh 内置 echo 默认解释 |

← 返回首页