|
|
@@ -0,0 +1,545 @@
|
|
|
+# Linux tty设备
|
|
|
+
|
|
|
+`dev/tty` 是一个在 Linux 和其他 Unix-like 系统中非常特殊的设备文件。从本质上讲,**它是一个指向当前进程的控制终端(Controlling Terminal)的别名或快捷方式**。
|
|
|
+
|
|
|
+### 1. 核心概念:终端(Terminal)
|
|
|
+
|
|
|
+在计算机早期,用户通过物理设备与计算机交互,这些设备被称为“终端”。一个典型的物理终端包含一个键盘用于输入和一个屏幕(或打印机)用于输出。
|
|
|
+
|
|
|
+在现代 Linux 系统中,物理终端已经不常见,取而代之的是**终端模拟器 (Terminal Emulator)**,例如 GNOME Terminal, Konsole, xterm, iTerm2 等。这些是图形界面下的软件程序,它们模拟了物理终端的行为。
|
|
|
+
|
|
|
+此外,还有**控制台 (Console)**,这是直接连接到计算机硬件的终端,通常在没有图形界面或图形界面崩溃时使用。在 Linux 中,你可以通过 `Ctrl + Alt + F1-F6` 切换到虚拟控制台。
|
|
|
+
|
|
|
+无论是哪种形式,系统都通过一个名为 **TTY** 的驱动程序子系统来管理这些终端会话。TTY 这个名字来源于早期的“Teletypewriter”(电传打字机)。
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+### 2. TTY 设备文件
|
|
|
+
|
|
|
+在 Linux 中,“一切皆文件”。系统通过 `/dev` 目录下的特殊文件与硬件设备进行通信。对于终端,也有一系列的设备文件,通常位于 `/dev/` 目录下,例如:
|
|
|
+
|
|
|
+- **/dev/ttyS0, /dev/ttyS1, ...**: 物理串口设备(Serial Ports)。
|
|
|
+- **/dev/tty1, /dev/tty2, ...**: 虚拟控制台(Virtual Consoles)。
|
|
|
+- **/dev/pts/0, /dev/pts/1, ...**: 伪终端(Pseudo-terminals)。这是我们最常用的,当你打开一个终端模拟器窗口时,系统就会创建一个伪终端,并为其分配一个像 `/dev/pts/0` 这样的设备文件。
|
|
|
+
|
|
|
+每个终端会话(比如你打开的一个终端窗口)都与一个特定的 TTY 设备文件相关联。你可以通过 `tty` 命令来查看当前终端对应的设备文件:
|
|
|
+
|
|
|
+Bash
|
|
|
+
|
|
|
+```
|
|
|
+$ tty
|
|
|
+/dev/pts/0
|
|
|
+```
|
|
|
+
|
|
|
+### 3. `/dev/tty` 的作用:一个动态的、指向“当前”的链接
|
|
|
+
|
|
|
+现在我们回到主角 `/dev/tty`。
|
|
|
+
|
|
|
+想象一下你正在编写一个程序,这个程序需要与用户直接交互(读取用户的输入或向用户的屏幕显示信息),无论这个程序最终从哪里运行。
|
|
|
+
|
|
|
+- 如果用户在虚拟控制台 `tty2` 上运行你的程序,程序应该向 `/dev/tty2` 读写。
|
|
|
+- 如果用户在 GNOME Terminal 的一个窗口里运行,程序可能需要向 `/dev/pts/5` 读写。
|
|
|
+- 如果用户通过 `ssh` 远程登录运行,程序又需要向另一个伪终端设备读写。
|
|
|
+
|
|
|
+如果让程序自己去判断当前在哪个终端上运行,将会非常复杂和不可靠。
|
|
|
+
|
|
|
+`/dev/tty` 就是为了解决这个问题而存在的。**无论一个进程的“控制终端”是哪一个具体的设备(`/dev/tty2` 或 `/dev/pts/5` 等),`/dev/tty` 始终是指向这个控制终端的链接。**
|
|
|
+
|
|
|
+当一个程序打开 `/dev/tty` 文件时,内核会自动将这个文件描述符重定向到当前进程的实际控制终端。这样,程序开发者就不需要关心底层的具体 TTY 设备是什么,只需要统一地对 `/dev/tty` 进行读写,就能确保与当前用户进行交互。
|
|
|
+
|
|
|
+**简单来说,`/dev/tty` 就是对程序说:“把信息发送给那个启动了你的用户,无论他在哪里。”**
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+### 4. `/dev/tty` 与标准输入/输出/错误 (stdin, stdout, stderr) 的区别
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+你可能会问:这听起来和标准输入(stdin)、标准输出(stdout)很像,有什么区别?
|
|
|
+
|
|
|
+在大多数情况下,进程的标准输入、输出和错误流默认就是连接到其控制终端的。例如,当你运行 `ls` 命令时,它的 `stdout` 默认就是你的终端,所以你能在屏幕上看到文件列表。
|
|
|
+
|
|
|
+然而,**重定向 (Redirection)** 会改变这种默认行为。
|
|
|
+
|
|
|
+- `ls > files.txt`:`ls` 命令的 `stdout` 被重定向到了 `files.txt` 文件,而不是终端。
|
|
|
+- `cat my_script.sh | bash`:`bash` 进程的 `stdin` 被重定向到了管道 (`|`),它从 `cat` 命令的输出中读取内容,而不是从键盘。
|
|
|
+
|
|
|
+在这种情况下,如果程序内部仍然希望强制与用户交互(例如,一个需要用户输入密码的脚本),它就不能再依赖 `stdin` 或 `stdout` 了。因为它们可能已经被重定向到文件或管道,不再是用户的屏幕和键盘了。
|
|
|
+
|
|
|
+**这时,`/dev/tty` 就派上了用场。**
|
|
|
+
|
|
|
+对 `/dev/tty` 的读写操作会绕过标准输入/输出的重定向,直接访问控制终端。
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+#### 示例:
|
|
|
+
|
|
|
+来看一个实际的例子。假设我们有一个脚本 `ask_password.sh`:
|
|
|
+
|
|
|
+Bash
|
|
|
+
|
|
|
+```
|
|
|
+#!/bin/bash
|
|
|
+
|
|
|
+# 尝试从标准输入读取密码
|
|
|
+echo "Enter password (from stdin):"
|
|
|
+read password_stdin
|
|
|
+
|
|
|
+# 现在,强制从控制终端读取密码
|
|
|
+echo "Enter password again (from /dev/tty):"
|
|
|
+read password_tty < /dev/tty
|
|
|
+
|
|
|
+echo "Password from stdin: $password_stdin"
|
|
|
+echo "Password from tty: $password_tty"
|
|
|
+```
|
|
|
+
|
|
|
+现在,我们正常运行它:
|
|
|
+
|
|
|
+Bash
|
|
|
+
|
|
|
+```
|
|
|
+$ ./ask_password.sh
|
|
|
+Enter password (from stdin):
|
|
|
+my_secret_pass
|
|
|
+Enter password again (from /dev/tty):
|
|
|
+my_secret_pass
|
|
|
+Password from stdin: my_secret_pass
|
|
|
+Password from tty: my_secret_pass
|
|
|
+```
|
|
|
+
|
|
|
+看起来没有区别。但是,现在我们尝试用重定向的方式运行它:
|
|
|
+
|
|
|
+Bash
|
|
|
+
|
|
|
+```
|
|
|
+$ echo "password_from_file" | ./ask_password.sh
|
|
|
+Enter password (from stdin):
|
|
|
+Enter password again (from /dev/tty):
|
|
|
+my_real_secret <-- 这里光标会停住,等待你从键盘输入
|
|
|
+Password from stdin: password_from_file
|
|
|
+Password from tty: my_real_secret
|
|
|
+```
|
|
|
+
|
|
|
+**分析:**
|
|
|
+
|
|
|
+1. 第一个 `read` 命令从 `stdin` 读取。由于我们通过管道将 `echo` 的输出重定向到了脚本的 `stdin`,所以它读到了 "password_from_file"。
|
|
|
+2. 第二个 `read` 命令被明确地重定向为从 `/dev/tty` 读取 (`< /dev/tty`)。这个操作绕过了 `stdin` 管道,直接访问了你的键盘和屏幕。因此,它会停下来等待你手动输入密码。
|
|
|
+
|
|
|
+这就是 `/dev/tty` 的核心价值:**提供一个无论标准流如何重定向,都能保证与用户终端进行交互的可靠通道。** 像 `ssh`、`sudo` 等需要安全输入密码的程序,内部都会使用这种机制。
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+### 总结
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+| 特性 | 描述 |
|
|
|
+| -------------------------- | ------------------------------------------------------------ |
|
|
|
+| **定义** | 一个特殊的设备文件,作为当前进程控制终端的别名或快捷方式。 |
|
|
|
+| **作用** | 为程序提供一个稳定、可靠的方式来与启动它的用户终端进行交互。 |
|
|
|
+| **动态性** | 它本身不是一个具体的设备,而是一个由内核管理的、动态指向具体 TTY 设备的链接。 |
|
|
|
+| **与 stdin/stdout 的区别** | 当标准输入/输出/错误流被重定向到文件、管道或其他进程时,`/dev/tty` 仍然可以用来直接访问用户的屏幕和键盘。 |
|
|
|
+| **典型用途** | - 需要用户输入密码或确认的程序(如 `sudo`, `ssh`)。<br>- 需要在脚本中明确与用户交互,即使用户通过管道或重定向运行该脚本。 |
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+### 在用户程序中,通常是怎么使用/dev/tty的
|
|
|
+
|
|
|
+好的,在用户程序中使用 `/dev/tty` 的核心目标是:**绕过可能被重定向的标准输入/输出流,强制与用户的控制终端进行直接交互。**
|
|
|
+
|
|
|
+这在以下几个场景中非常常见:
|
|
|
+
|
|
|
+1. **请求敏感信息**:比如密码、私钥密码等。即使脚本的输出被重定向到日志文件,你也不希望密码提示和输入过程被记录下来。
|
|
|
+2. **交互式确认**:在一个可能被自动化调用的脚本中,执行危险操作前(如 `rm -rf /`),需要强制用户手动确认。
|
|
|
+3. **诊断和调试**:向用户的屏幕打印调试信息,即使用户已经将脚本的标准输出重定向到了别处。
|
|
|
+4. **全屏或基于光标的应用程序**:像 `vim`, `top` 这样的程序需要直接控制终端的屏幕、颜色和光标位置,它们会直接与 TTY 设备打交道。
|
|
|
+
|
|
|
+#### 总结
|
|
|
+
|
|
|
+在用户程序中使用 `/dev/tty` 的模式非常一致:
|
|
|
+
|
|
|
+1. **打开文件**:像打开普通文件一样打开 `/dev/tty`,通常需要读写权限 (`r+`)。
|
|
|
+2. **错误处理**:检查打开操作是否成功。如果一个进程没有控制终端(例如,一个由 `systemd` 启动的后台守护进程),打开 `/dev/tty` 将会失败。程序需要妥善处理这种情况。
|
|
|
+3. **写入(输出)**:使用标准的文件写入函数(如 `fprintf`, `write`)向打开的 `/dev/tty` 文件描述符写入数据,这将会在用户的屏幕上显示提示信息。
|
|
|
+4. **读取(输入)**:使用标准的文件读取函数(如 `fgets`, `read`)从 `/dev/tty` 读取数据,这将获取用户的键盘输入。
|
|
|
+5. **关闭文件**:完成交互后,关闭文件描述符。
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+## /dev/ptmx和 /dev/pts/下文件有什么作用?
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+好的,我们来详细讲解 `/dev/ptmx` 和 `/dev/pts/` 目录下的文件。这两个组件是现代 Linux 系统中实现**伪终端(Pseudo-terminals, PTY)**机制的核心,对于我们日常使用的终端模拟器、SSH 远程登录等功能至关重要。
|
|
|
+
|
|
|
+简单来说,它们共同创建了一个“假的”终端设备,让程序(如 `bash`)以为自己正在和一个物理终端对话,而实际上它是在和一个软件(如 GNOME Terminal 或 `sshd`)对话。
|
|
|
+
|
|
|
+这个机制包含两个部分:
|
|
|
+
|
|
|
+- **主设备 (Master)**:由 `/dev/ptmx` 代表。
|
|
|
+- **从设备 (Slave)**:位于 `/dev/pts/` 目录下,例如 `/dev/pts/0`, `/dev/pts/1` 等。
|
|
|
+
|
|
|
+下面我们来深入了解它们各自的作用以及如何协同工作。
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+### 1. 伪终端 (PTY) 的概念
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+首先,理解为什么需要伪终端。在早期的 Unix 系统中,用户通过物理串口(如 `/dev/ttyS0`)连接的物理终端与计算机交互。后来,随着图形界面和网络的发展,我们需要一种在软件层面模拟这种硬件终端的方法。
|
|
|
+
|
|
|
+伪终端就是这种软件模拟的终端。它像一个管道一样,在两端各有一个“设备”:
|
|
|
+
|
|
|
+- **主端 (Master Side)**:由终端模拟器(如 xterm, GNOME Terminal)或远程登录服务(如 `sshd`)持有和控制。
|
|
|
+- **从端 (Slave Side)**:提供给应用程序(如 `shell`, `vim`, `top`)使用。这个从设备看起来和行为上都与一个真正的物理终端设备一模一样。
|
|
|
+
|
|
|
+当你在终端模拟器里敲击键盘时,终端模拟器程序从主端写入数据;内核将这些数据转发到从端,`shell` 程序就能从从端读到你的输入。反之,当 `shell` 程序产生输出时(例如 `ls` 的结果),它向从端写入数据;内核将其转发到主端,终端模拟器读取这些数据并在窗口中显示出来。
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+### 2. `/dev/ptmx`:伪终端的主设备复用器 (Master Multiplexer)
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+`/dev/ptmx` 是一个特殊的字符设备文件,它的名字是 "pseudo-terminal multiplexer" 的缩写。可以把它理解为**创建伪终端主/从设备对的工厂**。
|
|
|
+
|
|
|
+它的核心作用是:
|
|
|
+
|
|
|
+1. **创建新的 PTY 对**:当一个程序(如终端模拟器)需要一个新的伪终端时,它会打开 `/dev/ptmx` 文件。
|
|
|
+2. **返回主设备的文件描述符**:这个 `open` 操作会成功返回一个文件描述符。这个文件描述符就代表了新创建的 PTY 对的**主端 (Master)**。
|
|
|
+3. **动态创建从设备**:在打开 `/dev/ptmx` 的同时,内核会在 `/dev/pts/` 目录下动态地创建一个对应的**从设备 (Slave)** 文件,比如 `/dev/pts/0`。
|
|
|
+4. **提供控制接口**:程序可以通过对 `/dev/ptmx` 返回的文件描述符执行 `ioctl()` 系统调用,来对 PTY 进行配置,例如获取从设备的名称、解锁从设备等。
|
|
|
+
|
|
|
+**关键点**:你不能直接对 `/dev/ptmx` 进行大量的读写。它的主要目的是通过 `open()` 调用来请求和创建一个新的 PTY 对。之后所有的读写操作都通过 `open()` 返回的那个文件描述符来进行。每次打开 `/dev/ptmx` 都会创建一个全新的、独立的 PTY 主/从设备对。
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+### 3. `/dev/pts/` 目录和其下的文件
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+`/dev/pts` 是一个特殊的文件系统,类型是 `devpts`。这个目录专门用来存放伪终端的**从设备 (Slave)** 文件。
|
|
|
+
|
|
|
+- **动态创建**:这个目录下的文件(如 `/dev/pts/0`, `/dev/pts/1`, ...)不是永久存在的。它们是在对应的 PTY 主设备被创建时(即 `/dev/ptmx` 被打开时)由内核动态创建的。
|
|
|
+- **从设备的角色**:每个 `/dev/pts/N` 文件都扮演着 PTY 对中从端的角色。它是一个标准的 TTY 设备,应用程序(如 `bash`)可以像对待任何其他终端设备一样打开它、读取用户输入、写入程序输出。
|
|
|
+- **分配给 Shell**:终端模拟器在创建了 PTY 对之后,会 `fork` 一个子进程,并在子进程中将标准输入、标准输出和标准错误都重定向到这个新创建的从设备(例如 `/dev/pts/0`)上,然后执行 `bash` 或其他 shell。这样,`bash` 就“拥有”了这个伪终端作为它的控制终端。
|
|
|
+
|
|
|
+你可以通过 `tty` 命令查看当前 shell 关联的从设备:
|
|
|
+
|
|
|
+Bash
|
|
|
+
|
|
|
+```
|
|
|
+$ tty
|
|
|
+/dev/pts/0
|
|
|
+```
|
|
|
+
|
|
|
+如果你再打开一个新的终端窗口,在新窗口里执行 `tty`,你可能会看到:
|
|
|
+
|
|
|
+Bash
|
|
|
+
|
|
|
+```
|
|
|
+$ tty
|
|
|
+/dev/pts/1
|
|
|
+```
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+### 4. 完整的创建流程
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+让我们把整个过程串起来,看看当你打开一个新的终端窗口时,后台发生了什么:
|
|
|
+
|
|
|
+1. **打开 ptmx**:GNOME Terminal 程序调用 `open("/dev/ptmx", O_RDWR)`。
|
|
|
+2. **创建 PTY 对**:内核接收到请求,创建一个新的伪终端主/从设备对。
|
|
|
+3. **返回主设备FD**:`open` 调用返回一个文件描述符(比如 `fd=3`)给 GNOME Terminal。这个 `fd` 就是 PTY 的主端。
|
|
|
+4. **创建从设备文件**:同时,内核在 `/dev/pts/` 目录下创建一个新的从设备文件,比如 `/dev/pts/5`。
|
|
|
+5. **解锁和授权**:GNOME Terminal 程序通过对主设备的文件描述符 `fd=3` 执行一系列 `ioctl` 调用(如 `grantpt` 和 `unlockpt`),来设置从设备 `/dev/pts/5` 的权限和状态,使其可用。
|
|
|
+6. **获取从设备名**:GNOME Terminal 通过 `ioctl` 调用 `ptsname` 来查询与 `fd=3` 对应的主设备关联的从设备名称,得到字符串 "/dev/pts/5"。
|
|
|
+7. **创建子进程**:GNOME Terminal 调用 `fork()` 创建一个子进程。
|
|
|
+8. **设置会话和重定向**:在子进程中:
|
|
|
+ - 创建一个新的会话 (`setsid()`),并将 `/dev/pts/5` 设置为该会话的控制终端。
|
|
|
+ - 关闭标准输入、输出、错误(文件描述符 0, 1, 2)。
|
|
|
+ - 打开 `/dev/pts/5` 并将其复制到文件描述符 0, 1, 2。现在,子进程的 `stdin`, `stdout`, `stderr` 都指向了这个伪终端从设备。
|
|
|
+9. **执行 Shell**:子进程调用 `execve("/bin/bash", ...)` 来启动 `bash`。`bash` 继承了已经设置好的文件描述符,所以它自然地从 `/dev/pts/5` 读取命令并向其写入结果。
|
|
|
+10. **数据转发**:
|
|
|
+ - 你在 GNOME Terminal 窗口输入 `ls`。
|
|
|
+ - GNOME Terminal 程序从键盘事件中读取输入,并通过主设备的文件描述符 `fd=3` **写入** "ls\n"。
|
|
|
+ - 内核将数据从主端转发到从端 `/dev/pts/5`。
|
|
|
+ - `bash` 从它的标准输入(即 `/dev/pts/5`)**读取** "ls\n",执行命令。
|
|
|
+ - `ls` 的输出被 `bash` **写入** 到它的标准输出(即 `/dev/pts/5`)。
|
|
|
+ - 内核将数据从从端转发到主端。
|
|
|
+ - GNOME Terminal 程序通过主设备的文件描述符 `fd=3` **读取** `ls` 的输出结果,并将其渲染显示在窗口中。
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+### 总结
|
|
|
+
|
|
|
+| 组件 | 角色 | 作用 |
|
|
|
+| ------------------------ | ------------------------------- | ------------------------------------------------------------ |
|
|
|
+| **`/dev/ptmx`** | **主设备工厂 (Master Factory)** | 作为一个单一的入口点,当被打开时,它会创建一个新的伪终端主/从设备对,并返回代表**主设备**的文件描述符。 |
|
|
|
+| **`/dev/pts/` 下的文件** | **从设备 (Slave Devices)** | 这些是动态创建的设备文件,每一个都代表一个伪终端的**从设备**。它们为应用程序(如 shell)提供了一个标准的 TTY 接口,使其可以像与物理终端一样进行交互。 |
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+### 简单的用户程序展示/dev/ptmx的使用
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+好的,下面是一个简单的 C 语言用户程序,它清晰地展示了如何使用 `/dev/ptmx` 来创建一个伪终端,并在这个伪终端中启动一个 shell (`/bin/bash`)。
|
|
|
+
|
|
|
+这个程序将扮演一个最基础的“终端模拟器”的角色。它会处理主设备(master)端,而 `bash` shell 会在它创建的从设备(slave)端运行。
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+#### 程序功能分解
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+1. **打开 `/dev/ptmx`**:获取一个伪终端主设备(master PTY)的文件描述符。
|
|
|
+2. **初始化从设备**:调用 `grantpt()` 和 `unlockpt()` 来设置从设备(slave PTY)的权限和状态。
|
|
|
+3. **获取从设备名**:调用 `ptsname()` 来得到对应的从设备路径(例如 `/dev/pts/3`)。
|
|
|
+4. **创建子进程**:使用 `fork()` 创建一个子进程,这个子进程将用来运行 shell。
|
|
|
+5. **子进程设置**:在子进程中,将其会话(session)与从设备关联,并将标准输入、输出、错误重定向到从设备,然后执行 `/bin/bash`。
|
|
|
+6. **父进程通信**:在父进程中,监听用户在当前终端的输入和来自 `bash` 的输出,并在它们之间来回传递数据,实现交互。
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+#### 源代码 (`ptmx_demo.c`)
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+C
|
|
|
+
|
|
|
+```
|
|
|
+#define _XOPEN_SOURCE 600 // Needed for grantpt, unlockpt, ptsname
|
|
|
+#include <stdio.h>
|
|
|
+#include <stdlib.h>
|
|
|
+#include <fcntl.h>
|
|
|
+#include <unistd.h>
|
|
|
+#include <sys/ioctl.h>
|
|
|
+#include <sys/select.h>
|
|
|
+#include <sys/wait.h>
|
|
|
+#include <termios.h> // Not strictly needed for the demo, but good practice
|
|
|
+#include <string.h>
|
|
|
+
|
|
|
+int main() {
|
|
|
+ int master_fd;
|
|
|
+ char *slave_name;
|
|
|
+ pid_t pid;
|
|
|
+
|
|
|
+ // 1. 打开 /dev/ptmx 来获取一个主设备文件描述符
|
|
|
+ master_fd = open("/dev/ptmx", O_RDWR | O_NOCTTY);
|
|
|
+ if (master_fd < 0) {
|
|
|
+ perror("Error opening /dev/ptmx");
|
|
|
+ return 1;
|
|
|
+ }
|
|
|
+ printf("1. Master PTY opened with fd: %d\n", master_fd);
|
|
|
+
|
|
|
+ // 2. 授权并解锁从设备
|
|
|
+ if (grantpt(master_fd) != 0) {
|
|
|
+ perror("Error calling grantpt");
|
|
|
+ close(master_fd);
|
|
|
+ return 1;
|
|
|
+ }
|
|
|
+ if (unlockpt(master_fd) != 0) {
|
|
|
+ perror("Error calling unlockpt");
|
|
|
+ close(master_fd);
|
|
|
+ return 1;
|
|
|
+ }
|
|
|
+ printf("2. Slave PTY permissions granted and unlocked.\n");
|
|
|
+
|
|
|
+ // 3. 获取从设备的名字
|
|
|
+ slave_name = ptsname(master_fd);
|
|
|
+ if (slave_name == NULL) {
|
|
|
+ perror("Error calling ptsname");
|
|
|
+ close(master_fd);
|
|
|
+ return 1;
|
|
|
+ }
|
|
|
+ printf("3. Slave PTY name is: %s\n", slave_name);
|
|
|
+
|
|
|
+ // 4. 创建子进程
|
|
|
+ pid = fork();
|
|
|
+ if (pid < 0) {
|
|
|
+ perror("Error calling fork");
|
|
|
+ close(master_fd);
|
|
|
+ return 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 5. 子进程的代码
|
|
|
+ if (pid == 0) {
|
|
|
+ int slave_fd;
|
|
|
+
|
|
|
+ // 创建一个新的会话,使子进程成为会话领导者
|
|
|
+ // 这是让从设备成为控制终端的关键步骤
|
|
|
+ if (setsid() < 0) {
|
|
|
+ perror("setsid failed");
|
|
|
+ exit(1);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 打开从设备
|
|
|
+ slave_fd = open(slave_name, O_RDWR);
|
|
|
+ if (slave_fd < 0) {
|
|
|
+ perror("Error opening slave pty");
|
|
|
+ exit(1);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 将从设备设置为该进程的控制终端
|
|
|
+ // TIOCSCTTY 是 "Set Controlling TTY" 的意思
|
|
|
+ if (ioctl(slave_fd, TIOCSCTTY, NULL) < 0) {
|
|
|
+ perror("ioctl TIOCSCTTY failed");
|
|
|
+ exit(1);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 将子进程的标准输入、输出、错误重定向到从设备
|
|
|
+ dup2(slave_fd, STDIN_FILENO); // fd 0
|
|
|
+ dup2(slave_fd, STDOUT_FILENO); // fd 1
|
|
|
+ dup2(slave_fd, STDERR_FILENO); // fd 2
|
|
|
+
|
|
|
+ // 关闭不再需要的文件描述符
|
|
|
+ close(master_fd); // 子进程不需要主设备
|
|
|
+ close(slave_fd); // 因为已经 dup2 了,这个原始的也可以关了
|
|
|
+
|
|
|
+ // 执行一个新的 bash shell
|
|
|
+ printf("--- Starting Bash Shell in Slave PTY ---\n\n");
|
|
|
+ fflush(stdout);
|
|
|
+ execlp("/bin/bash", "bash", NULL);
|
|
|
+
|
|
|
+ // 如果 execlp 成功,下面的代码不会被执行
|
|
|
+ perror("execlp failed");
|
|
|
+ exit(1);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 6. 父进程的代码
|
|
|
+ printf("4. Forked child process with PID: %d\n", pid);
|
|
|
+ printf("5. Parent process will now forward data between stdin and master PTY.\n");
|
|
|
+ printf("--- You are now interacting with the new shell. Type 'exit' to quit. ---\n\n");
|
|
|
+
|
|
|
+ // 父进程不需要从设备
|
|
|
+ // close(slave_fd) in parent - it was never opened here
|
|
|
+
|
|
|
+ char buffer[256];
|
|
|
+ ssize_t nread;
|
|
|
+
|
|
|
+ // 循环,直到子进程退出
|
|
|
+ while (1) {
|
|
|
+ fd_set read_fds;
|
|
|
+ FD_ZERO(&read_fds);
|
|
|
+ FD_SET(STDIN_FILENO, &read_fds); // 监听当前终端的输入
|
|
|
+ FD_SET(master_fd, &read_fds); // 监听主设备的输出 (来自子进程shell)
|
|
|
+
|
|
|
+ // 使用 select 阻塞,直到有数据可读
|
|
|
+ if (select(master_fd + 1, &read_fds, NULL, NULL, NULL) < 0) {
|
|
|
+ perror("select failed");
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否是当前终端有输入
|
|
|
+ if (FD_ISSET(STDIN_FILENO, &read_fds)) {
|
|
|
+ nread = read(STDIN_FILENO, buffer, sizeof(buffer));
|
|
|
+ if (nread > 0) {
|
|
|
+ // 将用户的输入写入主设备,数据会流向子进程的shell
|
|
|
+ write(master_fd, buffer, nread);
|
|
|
+ } else {
|
|
|
+ break; // 读错误或EOF
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否是主设备有输出
|
|
|
+ if (FD_ISSET(master_fd, &read_fds)) {
|
|
|
+ nread = read(master_fd, buffer, sizeof(buffer));
|
|
|
+ if (nread > 0) {
|
|
|
+ // 将来自shell的输出写入当前终端的屏幕
|
|
|
+ write(STDOUT_FILENO, buffer, nread);
|
|
|
+ } else {
|
|
|
+ // 读取到 0 或 -1,意味着子进程的另一端关闭了连接
|
|
|
+ // 通常是 shell 执行了 exit
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ printf("\n--- Shell terminated. Parent process is shutting down. ---\n");
|
|
|
+ close(master_fd);
|
|
|
+ wait(NULL); // 等待子进程完全终止
|
|
|
+
|
|
|
+ return 0;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+#### 如何编译和运行
|
|
|
+
|
|
|
+1. **保存代码**:将上面的代码保存为 `ptmx_demo.c`。
|
|
|
+
|
|
|
+2. **编译**:使用 gcc 进行编译。
|
|
|
+
|
|
|
+ Bash
|
|
|
+
|
|
|
+ ```
|
|
|
+ gcc -o ptmx_demo ptmx_demo.c
|
|
|
+ ```
|
|
|
+
|
|
|
+3. **运行**:执行生成的可执行文件。
|
|
|
+
|
|
|
+ Bash
|
|
|
+
|
|
|
+ ```
|
|
|
+ ./ptmx_demo
|
|
|
+ ```
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+#### 运行时的输出和交互
|
|
|
+
|
|
|
+当你运行程序时,你会看到类似下面的输出:
|
|
|
+
|
|
|
+```
|
|
|
+1. Master PTY opened with fd: 3
|
|
|
+2. Slave PTY permissions granted and unlocked.
|
|
|
+3. Slave PTY name is: /dev/pts/2
|
|
|
+4. Forked child process with PID: 12345
|
|
|
+5. Parent process will now forward data between stdin and master PTY.
|
|
|
+--- You are now interacting with the new shell. Type 'exit' to quit. ---
|
|
|
+
|
|
|
+bash-5.1$
|
|
|
+```
|
|
|
+
|
|
|
+**发生了什么?**
|
|
|
+
|
|
|
+- 你的 `./ptmx_demo` 程序创建了一个新的伪终端 `/dev/pts/2`。
|
|
|
+- 它启动了一个新的 `bash` 进程,这个 `bash` 的“世界”就是 `/dev/pts/2`。
|
|
|
+- 你的 `./ptmx_demo` 程序正在作为中间人:
|
|
|
+ - 它读取你在当前终端(比如 `/dev/pts/1`)的键盘输入。
|
|
|
+ - 将你的输入(如 `ls -l`)写入到主设备(`master_fd`)。
|
|
|
+ - 内核将数据转发给从设备 `/dev/pts/2`。
|
|
|
+ - `bash` 从它的标准输入(`/dev/pts/2`)读取到 `ls -l` 并执行。
|
|
|
+ - `bash` 将 `ls -l` 的结果写入到它的标准输出(`/dev/pts/2`)。
|
|
|
+ - 内核将数据转发给主设备。
|
|
|
+ - 你的 `./ptmx_demo` 程序从主设备(`master_fd`)读取到结果,并将其打印到自己的标准输出,所以你就在屏幕上看到了 `ls -l` 的结果。
|
|
|
+
|
|
|
+你可以像在普通 shell 中一样执行命令:
|
|
|
+
|
|
|
+```
|
|
|
+bash-5.1$ pwd
|
|
|
+/home/user/test
|
|
|
+bash-5.1$ whoami
|
|
|
+user
|
|
|
+bash-5.1$ ps -f
|
|
|
+UID PID PPID C STIME TTY TIME CMD
|
|
|
+user 12344 5678 0 10:30 pts/1 00:00:00 ./ptmx_demo
|
|
|
+user 12345 12344 0 10:30 pts/2 00:00:00 bash
|
|
|
+user 12350 12345 0 10:31 pts/2 00:00:00 ps -f
|
|
|
+```
|
|
|
+
|
|
|
+注意 `ps` 命令的输出!我们的 `ptmx_demo` 程序运行在 `pts/1`(你原来的终端),而它创建的 `bash` 进程则运行在一个全新的终端 `pts/2` 上。
|
|
|
+
|
|
|
+当你输入 `exit` 并回车时,`bash` 进程会终止,这会导致伪终端连接关闭。父进程中的 `read(master_fd, ...)` 会返回 0,循环中断,程序优雅地退出。
|
|
|
+
|
|
|
+这个例子完整地展示了 `/dev/ptmx` 的核心用途:创建一个隔离的终端会话环境,并允许一个程序(父进程)完全控制另一个程序(子进程)的输入和输出。这也是所有终端模拟器、`ssh` 服务和 `tmux`/`screen` 等工具的基础工作原理。
|