伪终端PTY原理解析:SSH远程会话与TTY分配机制

不知道你有没有经历过这种场景:半夜两点被叫起来处理线上问题,你 SSH 登上一台业务服务器,顺手敲了一个htop,屏幕刷出来进程列表,CPU 那一列红得刺眼。但你用ps aux --sort=-%cpu查,却找不到对应进程。折腾半天才发现,之前跑脚本启动服务的人用的是 nohup 重定向到文件,所有输出都被吞了。等你想用script或者expect去复现现场,才发现很多东西在“非交互”模式下根本玩不转——颜色没了、退格键变成^?、全屏程序直接报错退出。

这一刻你就该意识到:SSH 连接背后不是一条简单的“管道”,而是一整套关于会话、输入输出、信号转发的机制。这个机制的核心,就是伪终端,也就是 PTY。这篇文章我会以 SSH 为例,把 PTY 的来龙去脉拆开聊透:它到底在内核里扮演什么角色,为什么远程执行命令和本地敲命令表现不一样,以及遇到“没有 TTY”报错时该怎么排查。无论你是刚接触运维的萌新,还是已经在生产环境摸爬滚打了很久的工程师,这篇文章都能让你把终端这层窗户纸捅破。

1. 为什么你敲的命令和脚本跑的命令,表现完全不一样

1.1 一次最简单的对比实验

先在本地打个比方。你直接在服务器控制台或者 SSH 登录之后敲ls /tmp,和你在 CI 脚本里执行ssh user@host ls /tmp,看起来都是执行同一件事。但结果经常是两个世界:前者输出是白字黑底,直接打在屏幕上;后者输出会被原样塞进一个缓冲区,等脚本结束后再一次吐出来,而且极有可能没有任何颜色。

原因很简单:你“在终端里敲命令”时,你的命令是运行在一个由内核分配的终端设备之上的;而 CI 脚本里的ssh host command,默认情况下 SSH 不会分配终端设备,它只是启动了一个远程进程,把这个进程的标准输入、标准输出、标准错误接到 SSH 的 channel 上。所以,有没有 PTY,决定了远程命令是被当作“交互式终端程序”还是“无头批处理程序”来对待。

很多程序会主动检测自己运行在什么环境下:isatty(STDIN_FILENO),如果返回真,就认为自己是交互式的,会开启行编辑、颜色输出、分页;如果返回假,就认为自己在非交互环境,直接输出纯文本甚至不输出。这个判断在本地执行和远程执行之间的差异,就是 PTY 存在与否的表现。

1.2 终端、TTY、PTY 这些名词到底指什么

老 Unix 用户聊天时常说“我在 tty1 上”、“pseudo terminal session”。这些词不搞清楚,后面排查问题会很懵。

简单梳理一下名词体系:

  • TTY(Teletype):历史上是电传打字机,现在泛指一切“内核管理的字符终端设备”。在 Linux 下,/dev/ttyN是虚拟控制台,/dev/ttyS0是串口设备,你在/dev/pts/0看到的也是某种 tty。
  • Console(控制台):物理上直接连接主机的那套键盘+显示器抽象,或者是虚拟控制台。
  • PTY(Pseudo Terminal,伪终端):它不是一个真实存在的硬件设备,而是内核模拟出来的“终端设备对”。一端像终端,一端像用户程序,中间由内核做数据搬运和信号处理。

现代 Linux 下,打开一个新的伪终端,会得到一对设备:主设备是/dev/ptmx,从设备是/dev/pts/N(N 是分配出来的编号)。进程把数据写入主设备,对从设备那一侧的程序来说,就像是有人在键盘上敲进来一样;程序往从设备写入,主设备那一侧读到的就是程序的“屏幕输出”。

这就解决了软件层最核心的问题:让一个本没有物理终端会话的程序,以为自己连接着一个终端。

1.3 SSH 登录一瞬间,终端会话是如何建立起来的

拿最常用的 OpenSSH 举例。你执行ssh user@host后,整个握手流程可以简化为四个阶段:

  1. TCP 连接建立、版本协商、密钥交换、用户认证。
  2. 认证通过后,SSH 服务器端会为这个会话请求一个 PTY。
  3. 服务端 fork 出一个 shell 子进程,把 shell 的标准输入、标准输出、标准错误全都重定向到 PTY 的从设备侧。
  4. 客户端与服务端之间,通过 SSH 的 channel 协议,把终端上的按键字节流和应用层的输出字节流双向搬运。

注意第 2 步的关键点:这个 PTY 是 SSH 服务端在远程主机上申请的,而不是客户端。你本地电脑上敲键盘的时候,按键信息会被封装成字节流穿过网络送到服务器,服务器把这串字节写入到 PTY 主设备(/dev/ptmx),PTY 从设备那一头的 shell 才“看到”键盘输入。输出反过来。

所以,SSH 会话里真正干活的那个终端接口,是存在于远程内核里的一个 pts 设备。这也是为什么你在本地tty会打印/dev/ttys002,而 SSH 进去远程主机执行tty,会打印/dev/pts/0之类的结果。

2. 内核里的那个“伪装键盘”:PTY 设备对的工作机制

2.1 打开一个 PTY:从打开 /dev/ptmx 说起

在 Linux 上,任何打开了/dev/ptmx并成功执行posix_openpt的进程,都等于向内核申请了一个新的伪终端。内核会动态分配一个从设备号,过程大致如下:

  • 调用open("/dev/ptmx", O_RDWR),拿到主设备端的文件描述符。
  • 调用ptsname()拿到对应的从设备路径,比如/dev/pts/5。
  • 调用grantpt()修正从设备的权限,让当前用户可以访问。
  • 调用unlockpt()解除内核对该从设备的锁定。
  • 然后可以fork()出一个子进程,在子进程里把 stdin、stdout、stderr 全部打开成同一个从设备文件,再exec()你真正想跑的程序。

在 C 语言中,很多项目会直接用openpty()或forkpty()封装这些步骤。forkpty在你 fork 出去的子进程里自动完成了“打开从设备并作为标准三流”的操作,非常省事。不过在 Python 里,你可以直接用内置的pty模块,就几行代码:

import os import pty import subprocess master_fd, slave_fd = pty.openpty() p = subprocess.Popen( "/bin/bash", stdin=slave_fd, stdout=slave_fd, stderr=slave_fd, close_fds=True, ) # 主设备端我们可以主动写数据,模拟终端敲入指令 os.write(master_fd, b"ls -l\n") output = os.read(master_fd, 4096) print(output.decode()) p.terminate()

这里主设备端就是我们“假装成键盘和屏幕”的地方,子进程里的 bash 会把从设备端当成自己正经的控制台。你用 Python 跑一下这段代码,可以看到os.read读出来的内容除了ls -l的结果,还有控制序列、提示符等。

2.2 行规程(line discipline):输入为什么会有“行”的错觉

终端协议真正复杂的地方不在“搬运”,而在“加工”。你敲Ctrl+C,程序收到的是 SIGINT 信号;你敲退格键,普通程序会收到一个字节修改显示内容。这些都不是应用层天然具备的,而是 PTY 的“行规程”在起作用。

行规程是一个很老的抽象,可以理解为内核给每一个 tty 设备配的一位“处理器”。当数据从主设备端到达从设备端时,它不是立刻原封不动地通过,而是被行规程按模式处理。最常见的是ICANON模式,也就是“规范模式”:

  • 输入会进入一个行缓冲,直到你按下回车,整行才被送到读取的程序里。
  • 在缓冲过程中,退格键可以修改当前行缓冲里的内容。
  • 某些特殊字符会被转成信号:Ctrl+C触发 SIGINT,Ctrl+Z触发 SIGTSTP,Ctrl+\触发 SIGQUIT。
  • 输出侧,内核会根据终端能力把\n转成\r\n,否则你敲回车,光标不会回到行首。

之所以强调这一点,是因为它引出了一个重要结论:SSH 传输的不是“鼠标键盘事件”,而是已经经过了行规程处理之后的字节流。你在手机上按一下退格,服务员(SSH 客户端)把它当作退格字节发过去,远程内核收到后,才知道自己应该做“回删一格”的动作。

2.3 会话、进程组和控制终端的关系

PTY 不只管输入输出,还负责“控制终端”。这一点非常容易被忽略,但是排查后台进程、挂断问题、信号问题的时候逃不掉。

每个进程都会属于一个会话(session),一个会话中可以存在多个进程组。如果一个会话有一个控制终端,那么当这个终端断开连接时(比如拔掉网线、关闭 SSH 客户端),内核会向控制终端所在的前台进程组发送 SIGHUP 挂断信号。

所以你会看到一个非常经典的现象:你 SSH 登录之后跑一个tail -f或sleep 100,直接关掉终端窗口,这个进程通常也死了。不是 SSH 杀掉了它,而是终端断开时内核挂断信号把前台进程组打掉了。反过来,如果你用nohup或者把进程放到后台然后用disown脱离 shell 的作业控制,进程不再属于这个控制终端的前台进程组,终端断开就不会直接把它带走了。

这其实是很多人第一次真正需要理解“控制终端”概念的契机。PTY 从设备分配出来的那一瞬间,子进程通过setsid()创建新会话,并把这个 PTY 从设备设为其控制终端。所以一个远程 shell 生命周期里,控制终端与 PTY 是强绑定的。

3. 非交互执行的真相:当 SSH 不分配 PTY 时发生了什么

3.1 “不是终端”与“没有终端”是两码事

排查报错的时候,你肯定见过这些提示:

  • sudo: no tty present and no askpass program specified
  • TERM environment variable not set.
  • stty: standard input: Inappropriate ioctl for device
  • error: TERM not set, please specify a terminal type

这些报错的根源,大多数不是线路质量,而是进程运行的 stdin/stdout 没有落在任何 tty 设备上。对一个没有 tty 的进程执行ioctl(fd, TIOCGWINSZ, &winsize)来查询窗口大小,返回的必然是 ENOTTY 错误,也就是“inappropriate ioctl for device”。

所以,你跑ssh host cmd默认不分配 PTY,这个远程进程就像是一个孤儿:输入输出都被接到了 SSH 的 channel 上,但没有任何终端设备参与。对程序来说,它检测到isatty()是假,就自动进入非交互模式。很多程序的行为会因此发生剧烈变化:

程序有 PTY 时无 PTY 时
vim完整 TUI 界面,支持方向键、分屏拒绝启动,提示需要终端
systemctl彩色状态输出,支持分页无颜色,输出格式变成批量可解析的样式
sudo可以读键盘密码报“no tty present”,直接失败
mysql支持历史命令、补全、表格对齐仍然能跑但无交互补全
top/htop实时刷新整个屏幕输出一屏后直接退出或疯刷日志

3.2 SSH 分配 TTY 的三个开关:-t、-T 和 ForceCommand

如果你确实需要远程执行一个交互式命令,明知 SSH 默认不分配 PTY,有几种做法:

  • ssh user@host -t:强制分配 PTY。哪怕你后面跟的是一个普通命令,ssh host -t ls -l也会给 ls 分配一个 PTY,ls 的输出就会是“带终端”的格式。
  • ssh user@host -T:显式禁用 PTY 分配。这是 CI 类工具常用的,防止远程命令因为交互检测出现奇怪行为。
  • 在服务端sshd_config里设置ForceCommand:某些受限 shell 会强制不分配 tty,但你依然可以在内部用ssh -t请求。

从协议角度讲,SSH 客户端在会话建立时可以发送PtyRequest报文,携带终端类型(比如xterm-256color)、终端行列数、像素宽高。服务端收到后,就会在远程内核里打开一个 PTY,并把终端类型设置成你指定的TERM值。注意这个值非常关键:如果你在客户端设置了xterm-256color,但远程的 terminfo 库里没有这个条目,某些程序会直接罢工,比如老版本的 screen/tmux 会报unknown terminal type。

3.3 管道、重定向和“不经意间杀死了交互”

再往前推一步,很多人不仅不分配 TTY,还顺手加了一个管道。比如:

ssh user@host 'top -b -n 1' | grep 'load average'

这里比较妙的是-b(batch mode),它是top专门为非交互环境准备的模式。如果没有-b,直接ssh host top,top 检测到 stdout 不是终端,就会拒绝刷新,甚至报错。而加了管道之后,即便你用了-t想强制分配 PTY,管道右边的 grep 也会让终端输出变得相当不可控——因为混合了终端控制序列的数据会被 grep 一并吃掉。

一个常见的经验法则是:只有当需要真正的交互式界面时,才考虑用-t;如果只是想拿到一批输出,宁可用非 PTY 模式,然后显式关闭交互选项。

我有一次排查某套自动化脚本,发现里面跑的是ssh host 'stty columns 200'然后再跑一个需要宽输出的脚本。结果实际上stty columns这个命令在非交互模式下没法设置任何东西,因为stty会尝试去ioctl当前标准输入,而标准输入是 SSH channel,不是 TTY。整个脚本白忙活。正确做法是,在服务端脚本里先export COLUMNS=200,或者干脆给 SSH 加上-t分配 PTY 再设置。

4. 控制序列是怎么让一个“假终端”拥有真实屏幕的

4.1 从 ANSI 转义序列到全屏应用的刷新逻辑

如果你已经理解 PTY 搬运的是字节,而不是“按键”,那再往前一步:这些字节里除了可打印字符,还混着一大堆控制序列。最常见的比如:

  • \x1b[31m红色前景
  • \x1b[2J清屏
  • \x1b[H光标回家
  • \x1b[1;1H移动到第 1 行第 1 列

这些序列属于终端协议层面的“指令”,目标是让“对端设备”移动光标、改变颜色、擦除字符。问题是,SSH 客户端本身不是一个终端模拟器,它只是一个字节搬运工。真正把序列翻译成“像素”的,是你本地终端模拟器(比如 iTerm2、Windows Terminal、GNOME Terminal 的底层组件)。

所以当你 SSH 进去跑vim时,vim 认为它在控制一个 80x24 的老终端,于是拼命向 stdout 刷控制序列;这些序列经过 SSH 隧道,传到你本地,本地终端模拟器还原成 UI。整个过程里,远程内核里的 PTY 就是那个“虚拟显示器”,本地终端模拟器是一个真正的“显示器”。

4.2 SIGWINCH 与终端窗口尺寸的同步

那窗口尺寸变了怎么办?你本地终端模拟器把窗口从 80x24 拉伸到 160x50 时,如果远端应用还不知道,渲染就会错位。解决办法是:

  1. 本地终端模拟器检测到自己窗口大小变化,向前台程序发送 SIGWINCH 信号。
  2. 你本地运行的 SSH 客户端进程收到 SIGWINCH 后,会通过 SSH 协议的window_change报文,把新的行列数发往远程。
  3. 远程 SSH 服务端收到后,对 PTY 从设备执行ioctl(TIOCSWINSZ),设置新的窗口大小。
  4. 内核向当前前台进程组发送 SIGWINCH。
  5. 远程应用收到 SIGWINCH 后,重新查询窗口尺寸并刷新布局。

这套机制在 tmux、screen 以及各种基于终端的 UI 程序里表现非常明显。如果你在使用 SSH 时发现远程全屏程序出现奇怪的换行错乱,十有八九是某一段网络代理或者中间跳板机没有正确转发 window_change 报文,远端只知道终端存在,却永远不知道客户端的“窗口变大了”。

我曾经在某个模拟项目里,把 SSH 通过堡垒机转发,嵌套了三层 SSH。最里层的 vim 经常渲染出一半屏就不动了。查到最后,是因为中间那层跳板机的 SSH 版本比较老,没有正确转发 SIGWINCH 相关的窗口更新,终端尺寸信息就丢失了。解决方式也很暴力:直接在最终目标机上强制设置stty rows 50 cols 200,让它忽略上游传播的窗口尺寸。

4.3 终端断开重连和作业恢复:两次 SIGWINCH 又是什么

还有一个容易混淆的场景:你在 SSH 里开着 vim,本地网络闪断。重新连上之后,vim 已经没了,因为 SSH 服务端会话断了之后,PTY 主设备端被关闭,内核会给控制终端进程以及会话中的进程发 SIGHUP。但有时候你用了 tmux,这时因为你是在 tmux 的“客户端/服务端”模型下工作,真正的 vim 进程挂在 tmux 服务端控制的 PTY 上,SSH 断开只是让 tmux 客户端退出,服务端继续保留原 PTY,所以重连后不会丢会话。

这也能解释为什么很多老练的运维宁可 tmux 套 SSH,也绝不裸 SSH 跑长任务。裸 SSH 跑长任务,终端断了,任务就 GG。用 tmux,等于把 PTY 的持有权转移到了 tmux 服务端这个“中间人来”,SSH 只是扮演一个看屏幕的客户端角色。

5. 实战:手写出一个“强制交互”的最小 PTY 工具

5.1 用 Python 复刻 SSH 分配 PTY 的核心逻辑

不要觉得 PTY 只能靠系统命令来用,我们自己写几行代码也能把它跑起来。下面这个例子的核心不是为了造个轮子,而是让你看清 “分配 PTY → 控制子进程 → 读写字节流” 的骨架长得什么样。

import os import pty import subprocess import select def run_interactive_command(command, env): master_fd, slave_fd = pty.openpty() # 设置一个新的会话,并让 slave 成为控制终端 p = subprocess.Popen( command, shell=True, stdin=slave_fd, stdout=slave_fd, stderr=slave_fd, preexec_fn=os.setsid, env=env, close_fds=True, ) os.close(slave_fd) output = b"" while True: r, _, _ = select.select([master_fd], [], [], 0.5) if r: try: data = os.read(master_fd, 4096) except OSError: break if not data: break output += data # 实时打印,模拟屏幕输出 os.write(1, data) if p.poll() is not None: break # 等子进程结束,关闭主设备 os.close(master_fd) return output if __name__ == "__main__": # 制造一个「交互式」场景:bash 里跑 top 之后退出 run_interactive_command("top -b -n 1", {"TERM": "xterm-256color", "PATH": "/bin:/usr/bin"})

注意到几个细节:

  • preexec_fn=os.setsid:子进程会创建一个新会话,把 PTY 从设备设成控制终端,这是还原 SSH 服务端 fork 出 shell 时的行为。
  • 父进程专注于读主设备端,读到的数据包括 bash 的提示符、命令输出、还有各种控制序列。
  • 如果你不给TERM变量,很多交互程序(比如 vim、htop、tmux)会直接报TERM environment variable not set。

5.2 当“非交互”程序被塞进 PTY 会发生什么

有人会问:是不是把所有程序都塞进 PTY,就能让它变得智能、可交互、有颜色?答案是部分可以,但有代价。比如ls --color=auto只有在 stdout 是终端时才默认输出颜色,你把它塞进 PTY,ls 就真的会输出带颜色的序列。但如果这个 PTY 另一头的“观看者”只是一个日志采集程序,这些颜色序列就会变成不可读的转义字符,日志直接花了。

所以在自动化场景里,处理 PTY 输出之前要想清楚:我是要“模拟人的操作”,还是只要“接近人的视觉输出”。前面的场景,用script -q -c command /dev/null可以直接把 stderr/控制序列完整录下来;后面的场景,最好加-T强制无 TTY。

5.3 需要小心的系统调用:TIOCSTI 与终端注入

Linux 下有一个攻击性很强的 ioctl 叫TIOCSTI,它能直接把字节注入到终端输入队列,效果等同于一个隐藏的键盘。如果有人能对你控制的 PTY 主设备端执行TIOCSTI,它就能伪造输入——这在远程执行、沙箱逃逸的讨论中经常被提到。

这提醒了我们一个安全视角的常识:PTY 并不仅是一个开发/运维便利设施,它也是一个系统边界。给一个不受信任的程序分配 PTY,就意味着它获得了“看到一个终端”和“模拟一个终端部分行为”的能力。比如某些恶意程序会通过检测\x1b[2J清屏序列来掩盖自己的行为,这在日志审计里非常致命。所以很多高强度审计环境会刻意避免给命令执行接口提供 PTY,宁可牺牲一点可交互性。

6. 诊断与排错:遇到“远程执行玄学”该怎么下手

6.1 先回答三个问题

远程执行出的问题千奇百怪,但归结下来诊断起点就三件事:

  1. 这个程序是否认为自己在和终端交互?
  2. 如果它认为自己是交互式程序,它期望的 TERM 类型是什么?
  3. 如果它不是交互式的,是不是有人在它前面包了一层会改变行为的包装命令?

对应到命令上,第一问看isatty:你在远程跑bash -c 'test -t 0 && echo "stdin is tty" || echo "no tty"'。第二问看echo $TERM。第三问就是排查你命令前面是不是加了nohup、setsid、script、unbuffer之类的包装层。

6.2 用 script、strace、w 观察 PTY 现场

诊断终端问题时,有几个工具非常顺手:

  • script -q -c "top -b -n 1" /dev/null:可以强制让程序以为自己有终端,并把输出转存下来。很多 CI 里跑交互程序刷怪问题,先这样复现差不多就能场景还原。
  • strace -f -e trace=ioctl -p <pid>:跟踪进程对终端的 ioctl 调用,看它到底在查询什么、设置什么。比如程序反复执行TIOCGWINSZ,说明它特别依赖窗口尺寸。
  • w或who:查看当前登录会话对应的 tty 设备和登录时间,可以快速确认有多少个“幽灵会话”占在线上。
  • ls -l /proc/<pid>/fd/0:查看进程 0 号描述符到底指向/dev/pts/N还是pipe:[12345]。这一步几乎是判断程序是否“有终端”的终极验证。

下面是一个典型的判断链路。问题现象是:某个工具通过 SSH 执行时,报错提示inappropriate ioctl for device。我的排查顺序是:

# 先看是不是真的没 TTY ssh user@host 'tty || echo "no tty"' # 再看进程 0 号 fd 指向哪里 ssh user@host 'ls -l /proc/self/fd/0' # 看看 stty 是否正常 ssh user@host 'stty -a || echo "stty failed"'

如果是 “not a tty” 这类问题,就回到上一节说的-t / -T的取舍上。

6.3 常见坑位:TERM 未设置、宽高为 0、Ctrl-C 无效

在我实际排查过的终端问题里,有几个高频坑值得拿出来单独说说。

第一个坑:TERM 变量缺失。

如果 SSH 分配了 PTY 但你客户端没有声明终端类型(比如某些老旧终端工具),服务端设置的 TERM 为空。遇到这种情况,vim 直接罢工、tmux 拒绝启动、各种 TUI 应用全部崩掉。解决办法是客户端指定,比如TERM=xterm-256color ssh host;或者在服务端.bashrc里兜底export TERM=${TERM:-xterm}。

第二个坑:窗口尺寸是 0x0。

如果你在脚本里申请 PTY,但没有正确初始化 winsize,程序查到的行列数就可能是 0x0。特别是 tmux 里的某个 pane 刚创建时,如果没有执行tmux resize-window,内部 tty 窗口尺寸可能就是空值,整屏程序画出来是乱码。用stty size可以快速查看当前行列数。

第三个坑:Ctrl+C 无效。

这个坑通常不是 PTY 本身的问题,而是你把终端设成了 raw 模式但忘了还原。在 raw 模式下,输入字符不再经过行规程的特殊字符处理,Ctrl+C不再触发 SIGINT,而是作为一个字节进入输入流。一个常见的“灵异事件”是:在某程序退出后,你的整个 shell 都变得“软绵绵”的——退格不删字、Ctrl+C 失灵、输入回显乱了。这时执行reset一般能救回来。原因就是之前的程序把自己的终端模式改成了 raw,退出时没有恢复,而 shell 也不会自动恢复。

6.4 给批量执行工具的设计建议

最后,给我的自动化类工具提供一个关于“是否启用 PTY”的决策参考。很多自研的远程执行组件,会提供给用户一个--pty开关或者--force-interactive开关。这个设计是对的,因为不同场景需求确实不同:

场景是否建议分配 PTY理由
批量执行 shell 脚本否避免交互程序卡住,避免控制序列污染日志
远程安装软件包并看见进度条是很多安装器判断 isatty 后才输出彩色进度
执行 sudo 需要输密码是否则 sudo 报 no tty present
捕获应用崩溃时的交互输出是让应用认为自己在前台,才能触发它的完整输出逻辑
日志采集管道否减少转义字符,避免日志分析被控制序列干扰

这个表其实说明了一件事:PTY 不是越多越好,它是一层“假包装”。包装得好,程序工作正常;包装过度,输出失真、隐藏吞错、日志解析困难。

我的个人经验是:给远程执行接口默认关闭 PTY,但一定要提供显式开关;同时入门级文档里要写清楚ssh -t和ssh -T的区别。很多第一次遇到“脚本里 sudo 老报错”的开发者,看到no tty present之后一脸懵,就是因为不知道 PTY 这层存在。等他们理解了这个概念,再遇到相似的问题,基本一眼就能定位到是“谁在申请终端,谁在拒绝终端”在作怪。

如果你后面再去读 OpenSSH 源码或者 Linux 内核drivers/tty/pty.c的实现,你会发现这些概念其实一点都不神秘:主设备和从设备之间不过是一对内核里的环形缓冲区队列,外加行规程的加工规则而已。但正是这个简单的设计,让远程终端和本地终端在体验上几乎无缝统一,也让无数自动化工具在“要不要假装自己是终端”之间反复摇摆。希望这篇文章能帮你把这个摇摆的边界看清楚。在实际工作中,我最后记住的一件小事是:凡是涉及 sudo、全屏程序、彩色输出、交互提示符的远程执行,先默认它需要 PTY;凡是你只需要稳定捕获文本输出的时候,坚决远离 PTY。二分的准绳拿稳了,终端相关的问题在你这里就不再是玄学了。

相关新闻

原生XMLHttpRequest实现文件上传:进度、取消与并发控制全解析

原生XMLHttpRequest实现文件上传:进度、取消与并发控制全解析

前两天接手一个内部小工具&#xff0c;需要做一个文件上传功能&#xff0c;前端不能引第三方库&#xff0c;也没法上构建工具&#xff0c;就一个纯静态页面加原生JavaScript。算了一圈&#xff0c;最后回到最基础的XMLHttpRequest&#xff0c;也就是大家常说的原生xhr。很多人觉…

2026/10/21 4:17:43 阅读更多 →
高校固定资产管理系统开发实战:SpringBoot+Vue+MySQL全解析

高校固定资产管理系统开发实战:SpringBoot+Vue+MySQL全解析

高校固定资产管理系统&#xff0c;这活儿听起来挺传统的&#xff0c;但真做起来你会发现里面门道不少。我前前后后给两所不同规模的学校搞过类似的系统&#xff0c;从最初单纯记录资产编号&#xff0c;到后来接入扫码盘点、维修报废审批流&#xff0c;算是把这类业务系统的底细…

2026/10/21 4:17:43 阅读更多 →
Agent异常行为实时检测:Harness Engineering的监控告警实战

Agent异常行为实时检测:Harness Engineering的监控告警实战

最近我带的Agent应用一上生产&#xff0c;最先顶不住的已经不是模型回答得对不对&#xff0c;而是它在后台到底干了什么。一套AI Agent如果没有把Harness Engineering那层监控告警做好&#xff0c;异常行为实时检测基本就是靠人肉盯日志&#xff0c;等发现问题时&#xff0c;钱…

2026/10/21 4:17:43 阅读更多 →

最新新闻

Windows下PyTorch复现PointNet++点云分类实战

Windows下PyTorch复现PointNet++点云分类实战

1. 为什么要在Windows上折腾PointNet搞点云深度学习的人&#xff0c;绕不开PointNet这个经典网络。它2017年出来之后&#xff0c;几乎成了点云分类和分割任务的基线模型&#xff0c;后续的PointConv、DGCNN、RandLA-Net多多少少都受了它的影响。但真到自己动手复现的时候&#…

2026/10/21 4:58:59 阅读更多 →
别只收藏skills清单:项目式拆解+刻意练习,30天做出作品

别只收藏skills清单:项目式拆解+刻意练习,30天做出作品

1. 先搞清楚一件事&#xff1a;你缺的真是一份“skills清单”吗这个标题其实特别有意思。单独扔过来一个“skills”&#xff0c;乍一看像是职场简历里的那个区块名&#xff0c;又像是一个待办事项列表——今天学Python&#xff0c;明天学剪辑&#xff0c;后天再补个AI绘画。但我…

2026/10/21 4:58:59 阅读更多 →
Windows 10下PyTorch 1.x复现PointNet++点云分类实战

Windows 10下PyTorch 1.x复现PointNet++点云分类实战

1. 点云分类任务的核心价值与复现思路1.1 为什么选择PointNet做点云分类点云分类这件事&#xff0c;放在三维视觉里算是入门级的任务&#xff0c;但真正动手做的人都知道&#xff0c;从数据加载到模型收敛&#xff0c;中间要踩的坑一点都不少。PointNet作为PointNet的升级版&am…

2026/10/21 4:58:59 阅读更多 →
JavaScript时间处理工具类:从时区错位到日期计算的实战指南

JavaScript时间处理工具类:从时区错位到日期计算的实战指南

1. 我先梳理了时间处理的四大痛点场景在动手写任何一行代码之前&#xff0c;我先做了一个动作&#xff1a;把最近几个项目里跟时间打交道的代码全部翻出来&#xff0c;统计了一下到底有多少地方出现过“时间相关bug”。统计结果把我自己都吓了一跳——光一个中等规模的项目里&a…

2026/10/21 4:58:59 阅读更多 →
深度计算 Runtime 并发调度:执行流、事件与异步提交实践

深度计算 Runtime 并发调度:执行流、事件与异步提交实践

1. 并发无上限是个假象&#xff1a;先看清深度计算硬件的真实执行模型1.1 从“线程并发”到“任务并发”的思维转换做深度计算 Runtime 这些年&#xff0c;我见过太多团队在“并发”这件事上栽跟头。最常见的想法是&#xff1a;任务能塞就塞&#xff0c;队列能开多深就开多深&a…

2026/10/21 4:58:59 阅读更多 →
离线环境Dify+Milvus知识库部署:连通性排查与解决全记录

离线环境Dify+Milvus知识库部署:连通性排查与解决全记录

规划内网知识库的时候&#xff0c;最头疼的往往不是算法选型&#xff0c;而是环境本身。最近搭了一套离线环境下的 Dify Milvus 知识库系统&#xff0c;折腾了整整两天&#xff0c;最后发现所有问题都集中在“连不上、起不来、版本对不上”这几个坎上。这里把整个排查过程和解…

2026/10/21 4:57:59 阅读更多 →

日新闻

统一内存+量化:锐龙AI Max PRO 400如何本地跑320B大模型

统一内存+量化:锐龙AI Max PRO 400如何本地跑320B大模型

这段时间硬件圈和AI圈都在传一个事&#xff1a;AMD锐龙AI Max PRO 400系列把最高192GB统一内存搬进了PC形态的产品里&#xff0c;按宣传口径&#xff0c;本地跑320B参数的大家伙不再是纸面概念。我第一反应倒不是“内存真大”&#xff0c;而是这话背后的含金量——它把过去只能…

2026/10/21 0:00:11 阅读更多 →
AI助手深度使用体验:对话质量、长文本处理与指令遵循如何留住用户

AI助手深度使用体验:对话质量、长文本处理与指令遵循如何留住用户

1. 从“封号重开”这个行为说起&#xff1a;用户到底在追求什么“封号重开”这四个字&#xff0c;放在任何产品语境里都带着一股决绝的味道。一个账号从零开始积累使用记录、调教偏好、沉淀工作流&#xff0c;背后是大量的时间和精力成本。正常情况下&#xff0c;没有人愿意主动…

2026/10/21 0:00:11 阅读更多 →
ROS中YOLOv3工程落地:通信同步、坐标变换与性能调优

ROS中YOLOv3工程落地:通信同步、坐标变换与性能调优

1. 为什么在ROS里硬刚YOLOv3不是“炫技”&#xff0c;而是工程落地的必经之路我第一次在某高校机器人实验室看到那台搭载Jetson TX2的移动底盘原地打转、反复识别错垃圾桶时&#xff0c;就意识到&#xff1a;单纯跑通darknet_ros demo和真正让YOLOv3在ROS里“干活”&#xff0c…

2026/10/21 0:00:11 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型&#xff08;GMM&#xff09;的语音识别系统&#xff0c;很多人第一反应就是一堆数学公式&#xff0c;觉得门槛很高。但如果你真的在MATLAB里把它落地跑通&#xff0c;会发现核心其实就是三件事&#xff1a;把声音变成特征向量&#xff0c;用GMM给每个类别…

2026/10/20 10:02:13 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人&#xff0c;但落到具体操作上&#xff0c;无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程&#xff1a;对图像做反白、改调色板、彩色转灰度&#xff0c;最后把像素数据导出成txt文件。单看每一…

2026/10/20 20:43:59 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词&#xff0c;你会发现一个很有意思的现象&#xff1a;每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型&#xff0c;有人指的是运动控制系统&#xff0c;还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/21 4:27:17 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 10:02:08 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:45 阅读更多 →