Skip to the content.

pty

必须成人 pty 不是一个很容易理解的机制,非常的违背直觉。

我打开了一个 ghostty ,我可以里面敲各种命令。

在我的设想中,数据的处理流程是这样的,

  1. 用户输入
  2. ghostty 接受到字符串,fork 程序,获取结果
  3. ghostty 展示出来

1. ghostty 和 bash 如何通信?

很明显,bash 才是真的交互对象,ghostty 需要将结果都传递到 bash 中去,然后接受 bash 的输出,在展示出来。 这是两个 process ,他们需要互相通信。Linux 中,进程通信的方法有非常多,Unix domain socket 是一个非常方便的方法, 性能不算太差,

终端 A:

socat UNIX-LISTEN:/tmp/serial-demo.sock STDIO,raw,echo=0,escape=0x1d

终端 B:

socat STDIO,raw,echo=0,escape=0x1d UNIX-CONNECT:/tmp/serial-demo.sock

连接后,两边输入的字符会立即显示到对方,按 Ctrl-] 退出。 这里的 /tmp/serial-demo.sock 是 socket 文件。socat 手册 (https://man7.org/linux/man-pages/man1/socat.1.html)

但是,你会发现测试数据收发、命令交互,这样可以。

这里的 STDIO,raw 仍在配置你当前终端;替换成 socket 的是两个 socat 之间的连接。

### 把终端 A 的 STDIO 换成启动 Bash,就能在终端 B 执行命令。 命令会以终端 A 的当前用户身份在本机运行。

终端 A,启动服务:

socat UNIX-LISTEN:/tmp/serial-demo.sock,mode=0600
EXEC:’bash –noprofile –norc -i’,pty,setsid,ctty,stderr

socat UNIX-LISTEN:/tmp/serial-demo.sock,mode=0600
EXEC:’bash –noprofile –norc’,stderr

终端 B,连接:

socat STDIO,raw,echo=0,escape=0x1d
UNIX-CONNECT:/tmp/serial-demo.sock

socat STDIO UNIX-CONNECT:/tmp/serial-demo.sock

连接后会看到 Bash 提示符,可以测试:

pwd tty ls sleep 100

运行 sleep 100 后按 Ctrl-C 可以中断它;输入 exit 正常退出,或按 Ctrl-] 断开连接。

服务端的几个参数分别是:

此时数据路径为:

终端 B ↔ socat ↔ Unix socket ↔ socat ↔ PTY ↔ Bash

这里不用手动指定 /dev/pts/N,但底层仍创建了 PTY,在 Bash 中执行 tty 就能看到。若你仍希望完全不创建新的 PTY,可以把终端 A 的第二行改为:

  EXEC:'bash --noprofile --norc',stderr

这样也能逐行执行命令、返回结果,但没有正常的交互提示符、终端编辑和作业控制。

除了 ctrl c 可以处理外,还可以做什么?

• PTY 的核心作用是:让程序面对一个具有终端语义的设备,而不只是能读写字节的管道或 socket。 除了 Ctrl-C,主要还提供这些能力:

能力 具体表现 ━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 行缓冲和基本编辑 canonical 模式下,输入先保存在内核中,按回车后程序才读到;支持退格、Ctrl-U 清除当前行 ────────────────── ─────────────────────────────────────────────────────────────────────────────────────── 输入回显 输入的字符自动显示;输入密码时,程序可以通过关闭 ECHO 隐藏字符 ────────────────── ─────────────────────────────────────────────────────────────────────────────────────── 特殊字符处理 Ctrl-D 表示输入结束的语义,Ctrl-Z 触发挂起,Ctrl-\ 触发退出信号;这些行为可以配置 ────────────────── ─────────────────────────────────────────────────────────────────────────────────────── 作业控制 配合控制终端和前台进程组,让 Bash 的 fg、bg、挂起和恢复正常工作 ────────────────── ─────────────────────────────────────────────────────────────────────────────────────── 终端尺寸 程序可以查询行列数;尺寸变化时,前台进程组可收到 SIGWINCH ────────────────── ─────────────────────────────────────────────────────────────────────────────────────── 输入输出转换 根据配置处理回车、换行,以及 Ctrl-S / Ctrl-Q 软件流控 ────────────────── ─────────────────────────────────────────────────────────────────────────────────────── 终端接口 提供 isatty()、tcgetattr()、tcsetattr() 等接口,让程序识别和配置终端

拿你刚才的实验来说,有几个区别特别容易观察。

密码输入:程序通过 termios 关闭 PTY 的回显,用户仍然能输入,但屏幕上不会显示密码。普通 socket 没有这个终端属性。

挂起和恢复:执行 sleep 100,按 Ctrl-Z,Bash 会重新显示提示符;输入 fg,它又回到前台。这依赖终端的前台进程组机制,以及 shell 对作业的管理。

全屏程序:Vim、top 等程序会查询终端尺寸,并把终端切换到逐字符输入模式,这样按键不必等回车才生效。不过,PTY 本身不负责画界 面、颜色和移动光标,这些由终端模拟器解释程序输出的转义序列完成。

还有两个容易混淆的地方:

所以完整的交互体验来自三者配合:PTY 提供终端机制,shell/应用处理交互逻辑,终端模拟器负责显示和键盘输入。

window size 的过程

可以用一个具体场景理解:你正在终端里运行 Vim,然后把终端窗口从 80 列拉宽到 120 列。Vim 需要知道这个变化,才能重新排版。

这里有两个独立的机制:查询当前尺寸,以及通知尺寸发生变化。

  1. “行列数”是内核保存的一份终端属性

每个 TTY 的 tty_struct 中都有 winsize,其中主要是:

struct winsize { unsigned short ws_row; /* 行数 / unsigned short ws_col; / 列数 */ unsigned short ws_xpixel; unsigned short ws_ypixel; };

例如 ws_row = 24、ws_col = 80,表示显示区域能容纳 24 行、每行 80 个字符单元。

程序通过下面的调用查询:

struct winsize ws; ioctl(tty_fd, TIOCGWINSZ, &ws);

你执行:

stty size

就能看到当前终端的行数和列数。接口文档 (https://man7.org/linux/man-pages/man2/TIOCSWINSZ.2const.html)

  1. 谁告诉内核“窗口变大了”?

对于终端模拟器里的 PTY,通常是终端模拟器自己,比如 Kitty、WezTerm。

它收到图形窗口的尺寸变化事件后,根据字体和显示区域计算新的行列数,然后对 PTY 调用:

ioctl(master_fd, TIOCSWINSZ, &new_size);

内核不会自己观察屏幕窗口。它需要用户态程序把尺寸告诉它。

完整过程是:

你拖动终端窗口 ↓ 终端模拟器计算新的行列数 ↓ ioctl(PTY master, TIOCSWINSZ, …) ↓ 内核记录新尺寸,并向前台进程组发送 SIGWINCH ↓ Vim 收到通知,再通过 TIOCGWINSZ 查询尺寸 ↓ Vim 按新尺寸重新绘制界面

SIGWINCH 可以记成 window change。这个信号本身不携带新的行列数,只表示“尺寸变了,去查询”。

  1. 为什么是“前台进程组”?

因为一个终端里可以存在很多进程,但当前与用户交互的通常只有一个作业:

Bash 提示符等待输入时:前台进程组是 Bash 所在的组

Bash 启动 Vim 后: 前台进程组切换为 Vim 所在的组

因此,运行 Vim 时,内核会通知 Vim 所在的前台进程组。对于一个前台管道作业,同组的多个进程都可能收到信号。

这里的“前台”指的是 TTY 的作业控制状态,不是桌面上哪个窗口被鼠标选中。

  1. 对应的内核实现

你本地源码中:

联系你之前的 socat 实验:终端 B 的窗口与服务端 Bash 的 PTY 是两个不同的终端。 那组 socat 命令只转发字节,没有转发窗口尺寸 变化,所以拖动终端 B,不会自动更新 Bash 那个 PTY 的尺寸。SSH 则专门实现了这条通知和同步路径。

这个需要继续调查可以实现什么功能

  1. 输入处理:实现行编辑(回删、Ctrl-W 删除单词等)而无需用户态程序介入

TTY 的历史演变

┌─────────────────────────────────────────────────────────────────────────────┐
│  Phase 1: 物理电传打字机 (1960s-1970s)                                        │
│  ┌─────────┐      Serial      ┌───────────────┐                              │
│  │ 用户终端 │  ═══════════════► │ 大型机 (Unix) │                              │
│  │(Teletype)│      RS-232      │               │                              │
│  └─────────┘                   └───────────────┘                              │
│                              硬件实现: UART 芯片                               │
└─────────────────────────────────────────────────────────────────────────────┘
                                      ↓
┌─────────────────────────────────────────────────────────────────────────────┐
│  Phase 2: 物理终端 + 视频终端 (1970s-1980s)                                   │
│  ┌─────────┐      Serial      ┌───────────────┐     ┌─────────────┐          │
│  │ 视频终端 │  ═══════════════► │   Line        │────►│   Shell     │          │
│  │ (VT100)  │                  │  Discipline   │     │  (用户态)    │          │
│  └─────────┘                  │  (n_tty)      │     └─────────────┘          │
│                               └───────────────┘                              │
└─────────────────────────────────────────────────────────────────────────────┘
                                      ↓
┌─────────────────────────────────────────────────────────────────────────────┐
│  Phase 3: 虚拟终端 VT (1990s-现在)                                            │
│  ┌──────────┐                ┌───────────────┐     ┌─────────────┐           │
│  │ 键盘+显卡 │ ─────────────► │ Virtual Term. │────►│   Shell     │           │
│  │(本地物理) │                │  (/dev/ttyN)  │     │             │           │
│  └──────────┘                └───────────────┘     └─────────────┘           │
│                               绕过 X11,直接使用 frame buffer                        │
└─────────────────────────────────────────────────────────────────────────────┘
                                      ↓
┌─────────────────────────────────────────────────────────────────────────────┐
│  Phase 4: 伪终端 PTY (现代图形界面时代)                                        │
│  ┌─────────────┐             ┌───────────────┐     ┌─────────────┐           │
│  │ Terminal    │             │  PTY Master   │     │  PTY Slave  │           │
│  │ Emulator    │◄═══════════►│  (如 pts/0)   │◄────│  (/dev/pts/N)│           │
│  │(alacritty)  │             │               │     │             │           │
│  └─────────────┘             └───────────────┘     │  Shell/vim  │           │
│                                                    └─────────────┘           │
│  终端模拟器在用户态实现,PTY 在内核中实现,完全兼容 TTY 接口                      │
└─────────────────────────────────────────────────────────────────────────────┘

(这个是真的吗?)

Phase 2 和 phase 3 的区别是什么?

一个是键盘接受中断,然后到 tty 机制中去,然后写入到 frame buffer 中去

一个是 serial 接受中断,然后还是写入 serial 中去

TODO 这里的 backtrace 都可以看看

ghostty -> zsh -> tmux

这是有多个 tty 和 pts ,那么特殊符号是被谁劫持的?

pty 太神奇了

来连接 pty 的方法:

# 这时候你立刻在另一个终端执行:
screen /dev/pts/12 115200
# 或者
minicom -D /dev/pts/12

思考一下在 bmc 界面中,使用 aspeed 显卡,然后使用 vim 的结果是什么

也就是清屏的操作现在是在驱动来实现了, 可以分别找一下,在 alacritty 和 驱动中实现这个的证据

有趣的观察,标准输入输出在 systemd 中切换了

systemd 中程序

total 0
dr-x------ 2 root root  0 Jun  3 15:41 .
dr-xr-xr-x 9 root root  0 Jun  3 15:41 ..
lr-x------ 1 root root 64 Jun  3 15:41 0 -> /dev/null
lrwx------ 1 root root 64 Jun  3 15:41 1 -> 'socket:[37045]'
lrwx------ 1 root root 64 Jun  3 15:41 2 -> 'socket:[37045]'
lrwx------ 1 root root 64 Jun  3 15:41 7 -> 'socket:[25320]'

普通模式运行:

lrwx------ - martins3  4 Jun 12:18  0 -> /dev/pts/17
lrwx------ - martins3  4 Jun 12:18  1 -> /dev/pts/17
lrwx------ - martins3  4 Jun 12:18  2 -> /dev/pts/17
lrwx------ - martins3  4 Jun 12:18  7 -> socket:[11477421]

我是没想到,原来 /dev/pts 也有文件系统的定义

fs/devpts/inode.c

static struct file_system_type devpts_fs_type = {
	.name		= "devpts",
	.mount		= devpts_mount,
	.kill_sb	= devpts_kill_sb,
	.fs_flags	= FS_USERNS_MOUNT,
};

mesg 工具

https://www.man7.org/linux/man-pages/man1/mesg.1.html

mesg 用来控制其他用户能否向你的终端写入消息,例如通过 write 或 talk。手册说明 (https://www.man7.org/linux/man-pages/man1/mesg.1.html)

命令 作用 ━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━ mesg 查看当前状态:is y 或 is n ──────── ──────────────────────────── mesg y 允许接收消息 ──────── ──────────────────────────── mesg n 禁止接收消息

它的实现是修改终端设备文件的组写权限;通常对应 /dev/pts/N。因此,它控制的是终端写入权限,不是聊天软件或桌面通知开关。自 util-linux 2.41 起,只修改 group 权限。DESCRIPTION (https://www.man7.org/linux/man-pages/man1/mesg.1.html#DESCRIPTION)

两个容易忽略的细节:

• 操作对象是 标准错误输出 stderr 所关联的终端。 • 退出码 0 表示允许,1 表示禁止,>1 才表示出错;没有关联终端时返回 2,默认不打印警告,可用 mesg -v 查看。手册说明 (https://www.man7.org/linux/man-pages/man1/mesg.1.html)

write 配合使用

write -h

Usage:
 write [options] <user> [<ttyname>]

Send a message to another user.

Options:
 -h, --help     display this help
 -V, --version  display version

For more details see write(1).

本站所有文章转发 CSDN 将按侵权追究法律责任,其它情况随意。