---
title: 'Linux 的伪终端的基本原理及其在远程登录（SSH，telnet等）中的应用'
pubDate: 2018-07-24
tags:
  - 'linux'
description: 'Linux 的伪终端的基本原理及其在远程登录（SSH，telnet等）中的应用'
---

本文介绍了linux中伪终端的创建，介绍了终端的回显、行缓存、控制字符等特性，并在此基础上解释和模拟了telnet、SSH开启远程会话的过程。

## 轻量级远程登录

之前制作的一块嵌入式板子，安装了嵌入式linux操作系统，可以通过串口（Console）登录。为了方便使用，需要寻找通过网线远程登录的方法。最初的想法是SSH，不过板子的ROM太小，存不了体积庞大庞大的OpenSSH套装。后来换用了telnet，直接拿busybox的telnetd做服务器，效果很好。

后来有一天，发现了Linux中有一个直接建立TCP连接的工具：nc 。在服务端使用`nc -l 端口号`来进行监听，在客户端使用`nc IP地址 端口号`来建立连接。建立连接后，nc会把从stdin读入的字节流发送给另一方，把接收到的字节流写入stdout中。配合方便的管道操作，不正可以将shell的输入/输出传送到远端机器上吗？于是在Ubuntu中实验操作如下（之后发现这种操作叫做“反弹shell”）：

打开一个终端A，输入命令

```
mkfifo /tmp/p  # 创建临时管道
sh -i </tmp/p |& nc -l 2333 >/tmp/p
```

该命令将bash的标准输入输出与nc的标准输出输入连接起来，并由nc将其与socket连接起来。同时，nc监听2333端口（如果使用小于1024的端口，需要root权限），等待远程连接。现在打开另一个终端B，准备连接：

```
nc localhost 2333
```

这时，在终端B中出现了sh的提示符。输入一般的shell命令后可以执行并得到结果。看来linux自带的工具已经灵活、强大到足够搭建一个小型的远程登录系统。这个过程可以使用下面的图来描述：

![](https://source.mkshell.com/15324411936429.jpg)

过tty命令，我们看到，此时的shell并没有一个tty终端。确实，它的标准输入输出都是管道。这会带来一个问题，需要操纵tty的一些命令，比如vi、less、sudo等都无法正常使用（可以动手试试效果怎么样）。更为要命的是，在终端B中按下Ctrl+C这样的控制键，内核把结束信号发送给了客户端nc，而不是远程的程序！

![](https://source.mkshell.com/15324412168736.jpg)

Ctrl+C直接杀死nc，结束了会话。对比telnet，我们的登录系统还缺少什么东西。这就是伪终端（pseudoterminal）。

## 了解伪终端

### 终端和它的作用

终端（terminal）这个词拥有很多含义，这里尽量将其分开说明。

历史上，终端（有时被成为tty，tele typewriter）是用户访问计算机主机的硬件设备，可以理解为一个显示器和一个键盘的组合。

- 现代Linux里面比较接近此概念的是（一系列）虚拟控制台（virtual console）。在Ubuntu等发行版本中按下Ctrl+Alt+F1(或F2, F3, ...)即可切换到相应控制台下。`/dev/tty1`等文件是这些硬件在linux下的设备文件。程序通过这些文件的读写实现对控制台的读写，通过ioctl实现对硬件参数的设置。
- 终端还可以指代设备文件，实现软件接口。比如常见的`/dev/tty1`文件，还有/dev/pts目录下的所有文件。
- 对终端设备文件进行读写，能够从键盘读取输入，从显示器进行输出，实现交互式的输入输出
- linux中的每个进程有一个“控制终端（control terminal）”的属性（取值为设备文件），用于实现作业控制。在终端上输入Ctrl+C、Ctrl+Z，则以该终端为控制终端的前台进程组会收到终止、暂停的信号。
- 对终端设备进行ioctl操作，可以实现终端相关的硬件参数设置。login、sudo的不显示密码，都离不开对终端设备的操作。
  终端还可以指代“终端模拟器”。终端模拟器是应用程序，用于模拟一个终端。它一般是GUI程序，带有窗口。从窗口输入的字符作为模拟键盘的输入，在窗口上打印的字符作为模拟显示器的输出。终端模拟器还需要创建模拟的终端设备（如`/dev/pts/1`），用于当做命令行进程（CLI进程）的输入输出、控制终端。当键盘键入一个字符，它要让CLI进程从终端设备中读到这个字符，当CLI进程写入终端设备时，终端模拟器要读到并显示出来。

终端模拟器的这个需求，恰恰和telnet这种远程登录服务器的需求相似。telnet服务器也要创建模拟的终端设备，用于当做命令行进程（CLI进程）的输入输出、控制终端。当从网络收到一个字符，它要让CLI进程从终端设备中读到这个字符，当CLI进程写入终端设备时，telnet要把输出发送到网络。

这种共同的需求在linux中有一个统一实现——伪终端（pseudoterminal）。没错，上面的`/dev/pts/`文件夹里的以数字命名的文件就是伪终端的设备文件。

### 伪终端的介绍

通过`man pts`可以查阅linux对伪终端的介绍。伪终端是伪终端master和伪终端slave（终端设备文件）这一对字符设备。`/dev/ptmx`是用于创建一对master、slave的文件。当一个进程打开它时，获得了一个master的文件描述符（file descriptor），同时在`/dev/pts`下创建了一个slave设备文件。

master端是更接近用户显示器、键盘的一端，slave端是在虚拟终端上运行的CLI（Command Line Interface，命令行接口）程序。Linux的伪终端驱动程序，会把“master端（如键盘）写入的数据”转发给slave端供程序输入，把“程序写入slave端的数据”转发给master端供（显示器驱动等）读取。

![](https://source.mkshell.com/15324413302349.jpg)

我们打开的“终端”桌面程序，其实是一种终端模拟器。当终端模拟器运行时，它通过`/dev/ptmx`打开master端，创建了一个伪终端对，并让shell运行在slave端。当用户在终端模拟器中按下键盘按键时，它产生字节流并写入master中，shell便可从slave中读取输入；shell和它的子程序，将输出内容写入slave中，由终端模拟器负责将字符打印到窗口中。

（终端模拟器的显示原理就不在这里展开了，这里认为键盘按键形成一列字节流、向显示器输出字节流后便打印到屏幕上）

linux中为什么要提出伪终端这个概念呢？shell等命令行程序不可以直接从显示器和键盘读取数据吗？为了同屏运行多个终端模拟器、并实现远程登录，还真不能让bash直接跨过伪终端这一层。在操作系统的一大思想——虚拟化的指导下，为多个终端模拟器、远程用户分配多个虚拟的终端是有必要的。上图中的shell使用的slave端就是一个虚拟化的终端。master端是模拟用户一端的交互。之所以称为虚拟化的终端，它除了转发数据流外，还要有点终端的样子。

### 作为终端的伪终端

最为一个虚拟的终端，每一个伪终端里面封装了一个终端驱动，让它能做到这些事情：

- 为程序提供一些输入输出模式的帮助，比如输入密码时隐藏字符
- 为用户提供对进程的控制，比如按下Ctrl+C结束前台进程

对，这些就是转发数据之外的控制。

#### 终端的属性：回显控制和行控制

当用户按下一个按键时，字符会出现在屏幕上。这可不是CLI进程写回来的。不信的话可以在终端里运行`cat`，随便输入些什么按回车。第二行是`cat`返回来的，第一行正是终端的特性。

终端驱动里存储了一个状态——回显控制：是否将写入master的字符再次送回master的读端（显示器）。默认情况下这个是启用的。在命令行里可以使用`stty`来更改终端的状态。比如在终端中运行

```
stty -echo
```

则会关掉当前终端的回显。这时按下按键，已经没有字符显示出来了。输入`ls`等命令，能够看到shell正常接收到我们的命令（此时回车并没有显示出来）。这时`cat`后，盲打一些文字，按下回车后看到只有一条文字了。

![](https://source.mkshell.com/15324414282276.jpg)

除了用户通过命令行方式，CLI的程序还能通过系统调用来设置终端的回显，比如`login`，`sudo`等程序就是通过暂时关闭回显来隐藏密码的。具体方式是在slave的文件描述符上调用`ioctl`函数（参考`man tty_ioctl`），不过推荐使用更友好的`tcsetattr`函数。详细设置可查阅`man tcsetattr`。

另外，终端驱动还提供有行缓冲功能。还是以`cat`为例：当我们输入文字，在键入回车之前，`cat`并不能读取到我们输入的字符。这里的`cat`的行为可以理解为逐字符读写：

```
while(read(0, &c, 1) > 0) //read from stdin, while not EOF
    write(1, &c, 1);  //write to stdout
```

是谁阻止`cat`及时读入字符了呢？其实是终端驱动。它默认开启了一个行缓冲区，这样等程序要调用`read`系统调用时，先让程序阻塞着（blocked），等用户输入一整行后，才解除阻塞。我们可以使用下列命令将行缓存大小设置为1：

```
stty min 1 -icanon
```

这时，运行`cat`，尝试输入文字。每输入一个字符，能够立即返回一个字符。（把`min`改为`time`，还能设置输入字符最长被阻塞1秒）

这些终端的状态属性信息还有很多，比如设置终端的宽度、高度等。具体可以参考`man stty`。

#### 特殊控制字符

特殊控制字符，是指Ctrl和其他键的组合。如Ctrl+C、Ctrl+Z等等。用户按下这些按键，终端模拟器（键盘）会在master端写入一个字节。规则是：Ctrl+字母得到的字节是（大写）字母的ascii码减去0x40。比如Ctrl+C是0x03，Ctrl+Z是0x1A。参见下表：

![](https://source.mkshell.com/15324415385595.jpg)

驱动收到这些特殊字符，并不会像收到正常字节那样处理。在echo的时候，它返回两个可见字符。比如键入Ctrl+C（0x03），就会回显^和C（0x5E 0x03）两个字符。更重要的是，驱动将会拦截某些控制字符，他们不会被转发给slave端，而是触发作业控制（job control）的规则：向前台进程组发送SIGINT信号。

要想绕过这一机制，我们可以使用stty的一些设置。下面的命令能够同时关闭控制字符的特殊语义、设置行缓冲大小为1：

```
stty raw
```

然后，运行`cat`命令，我们键入的所有字符，包括控制字符Ctrl+C（0x03），都会成功传递给`cat`，并且被原样返回。（可以试试上下左右、回车键的效果）

## 结语

linux中终端驱动本身有回显、行缓存、作业控制等丰富的属性，在此基础上实现的伪终端在终端模拟器、远程登录等场合下能够得到多种应用。

在实验过程中也牵扯到进程控制、输入输出重定向、网络通信这么多的知识，更体现出linux的复杂精致的结构。我感觉，linux 就像一个包罗万象、又自成体统的小宇宙，它采用独特的虚拟化技术，灵活的模块化和重用机制，虚拟出各种设备，实现了驱动程序的随意拼插。在这里，所有模块都得到了充分的利用，并能够像变形金刚那样对各类需求提出面面俱到的解决方案。

原文链接：[https://www.cnblogs.com/zzdyyy/p/7538077.html](https://www.cnblogs.com/zzdyyy/p/7538077.html)
