当前位置: 首页 > 工具软件 > PTS > 使用案例 >

Linux TTY/PTS概述

东门深
2023-12-01

原文地址:https://segmentfault.com/a/1190000009082089

1. TTY历史

1.1 支持多任务的计算机出现之前

在计算机出来以前,人们就已经在使用一种叫teletype的设备,用来相互之间传递信息,看起来像下面这样:

+----------+     Physical Line     +----------+
| teletype |<--------------------->| teletype |
+----------+                       +----------+

两个teletype之间用线连接起来,线两端可能也有类似于调制解调器之类的设备(这里将它们忽略),在一端的teletype上敲键盘时,相应的数据会发送到另一端的teletype,具体功能是干什么的,我也不太了解。(我脑袋里面想到画面是在一端敲字,另一端打印出来)

1.2 支持多任务的计算机出现之后

等到计算机支持多任务后,人们想到把这些teletype连到计算机上,作为计算机的终端,从而可以操作计算机。

使用teletype的主要原因有两个(个人见解):

现实中已经存在了大量不同厂商的teletype,可以充分利用现有资源

teletype的相关网络已经比较成熟,连起来方便

于是连接就发展成这样:


                                                                      +----------+ 
+----------+   +-------+     Physical Line     +-------+   +------+   |          |
| Terminal |<->| Modem |<--------------------->| Modem |<->| UART |<->| Computer |
+----------+   +-------+                       +-------+   +------+   |          |
                                                                      +----------+

左边的Terminal就是各种各样的teletype

物理线路两边用上了Modem,就是我们常说的“猫”,那是因为后来网络已经慢慢的变发达了,大家可以共享连接了。(大概推测,可能不对)

UART可以理解为将teletype的信号转换成计算机能识别的信号的设备

1.3 内核TTY子系统

计算机为了支持这些teletype,于是设计了名字叫做TTY的子系统,内部结构如下:

    +-----------------------------------------------+
    |                    Kernel                     |
    |                                 +--------+    |
    |   +--------+   +------------+   |        |    |       +----------------+
    |   |  UART  |   |    Line    |   |  TTY   |<---------->| User process A |
<------>|        |<->|            |<->|        |    |       +----------------+
    |   | driver |   | discipline |   | driver |<---------->| User process B |
    |   +--------+   +------------+   |        |    |       +----------------+
    |                                 +--------+    |
    |                                               |
    +-----------------------------------------------+
  • UART driver对接外面的UART设备

  • Line discipline主要是对输入和输出做一些处理,可以理解它是TTY driver的一部分

  • TTY driver用来处理各种终端设备

  • 用户空间的进程通过TTY driver来和终端打交道

为了简单起见,后面的介绍中不再单独列出UART driver和Line discipline,可以认为它们是TTY driver的一部分

1.4 TTY设备

对于每一个终端,TTY driver都会创建一个TTY设备与它对应,如果有多个终端连接过来,那么看起来就是这个样子的:


                      +----------------+
                      |   TTY Driver   |
                      |                |
                      |   +-------+    |       +----------------+
 +------------+       |   |       |<---------->| User process A |
 | Terminal A |<--------->| ttyS0 |    |       +----------------+
 +------------+       |   |       |<---------->| User process B |
                      |   +-------+    |       +----------------+
                      |                |
                      |   +-------+    |       +----------------+
 +------------+       |   |       |<---------->| User process C |
 | Terminal B |<--------->| ttyS1 |    |       +----------------+
 +------------+       |   |       |<---------->| User process D |
                      |   +-------+    |       +----------------+
                      |                |
                      +----------------+

当驱动收到一个终端的连接时,就会根据终端的型号和参数创建相应的tty设备(上图中设备名称叫ttyS0是因为大部分终端的连接都是串行连接),由于每个终端可能都不一样,有自己的特殊命令和使用习惯,于是每个tty设备的配置可能都不一样。比如按delete键的时候,有些可能是要删前面的字符,而有些可能是删后面的,如果没配置对,就会导致某些按键不是自己想要的行为,这也是我们在使用模拟终端时,如果默认的配置跟我们的习惯不符,需要做一些个性化配置的原因。

后来随着计算机的不断发展,teletype这些设备逐渐消失,我们不再需要专门的终端设备了,每个机器都有自己的键盘和显示器,每台机器都可以是其它机器的终端,远程的操作通过ssh来实现,但是内核TTY驱动这一架构没有发生变化,我们想要和系统中的进程进行I/O交互,还是需要通过TTY设备,于是出现了各种终端模拟软件,并且模拟的也是常见的几种终端,如VT100、VT220、XTerm等。

可以通过命令toe -a列出系统支持的所有终端类型

dan@ubuntu:~$ toe -a
cygwin    	ansi emulation for Cygwin
cons25-debian	freebsd console with debian backspace (25-line ansi mode)
cons25    	freebsd console (25-line ansi mode)
Eterm     	Eterm with xterm-style color support (X Window System)
linux     	linux console
pcansi    	ibm-pc terminal programs claiming to be ansi
wsvt25    	NetBSD wscons in 25 line DEC VT220 mode
wsvt25m   	NetBSD wscons in 25 line DEC VT220 mode with Meta
rxvt-unicode	rxvt-unicode terminal (X Window System)
rxvt      	rxvt terminal emulator (X Window System)
rxvt-basic	rxvt terminal base (X Window System)
ansi      	ansi/pc-term compatible with color
screen-256color-bce	GNU Screen with 256 colors and BCE
screen-w  	VT 100/ANSI X3.64 virtual terminal with 132 cols
sun       	Sun Microsystems Inc. workstation console
screen-bce	VT 100/ANSI X3.64 virtual terminal with bce
screen    	VT 100/ANSI X3.64 virtual terminal
screen-256color	GNU Screen with 256 colors
screen-s  	VT 100/ANSI X3.64 virtual terminal with hardstatus line
mach-gnu  	GNU Mach
mach      	Mach Console
mach-color	Mach Console with ANSI color
mach-gnu-color	Mach Console with ANSI color
mach-bold 	Mach Console with bold instead of underline
hurd      	The GNU Hurd console server
xterm     	X11 terminal emulator
xterm-xfree86	xterm terminal emulator (XFree86 4.4 Window System)
xterm-vt220	xterm emulating vt220
xterm-256color	xterm with 256 colors
xterm-r5  	xterm R5 version
xterm-color	generic "ANSI" color xterm (X Window System)
xterm-r6  	xterm X11R6 version
xterm-mono	monochrome xterm
dumb      	80-column dumb tty
vt102     	dec vt102
vt220     	dec vt220
vt100     	dec vt100 (w/advanced video)
vt52      	dec vt52

可以通过命令infocmp来比较两个终端的区别,比如infocmp vt100 vt220将会输出vt100和vt220的区别。

dan@ubuntu:~$ infocmp vt100 vt220
comparing vt100 to vt220.
    comparing booleans.
	mir: F:T.
    comparing numbers.
    comparing strings.
	blink: '\E[5m$<2>', '\E[5m'.
	bold: '\E[1m$<2>', '\E[1m'.
	clear: '\E[H\E[J$<50>', '\E[H\E[J'.
	cuf1: '\E[C$<2>', '\E[C'.
	cup: '\E[%i%p1%d;%p2%dH$<5>', '\E[%i%p1%d;%p2%dH'.
	cuu1: '\E[A$<2>', '\E[A'.
	dch: NULL, '\E[%p1%dP'.
	dch1: NULL, '\E[P'.
	dl: NULL, '\E[%p1%dM'.
	dl1: NULL, '\E[M'.
	ech: NULL, '\E[%p1%dX'.
	........

2. 程序如何和TTY打交道

在讨论TTY设备是如何被创建及配置之前,我们先来看看TTY是如何被进程使用的:

先用tty命令看看当前bash关联到了哪个tty

dan@ubuntu:~$ tty
/dev/pts/8

看tty都被哪些进程打开了

dan@ubuntu:~$ lsof /dev/pts/8
COMMAND    PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
gnome-ter 2281  dan   16u   CHR  136,8      0t0   11 /dev/pts/8
bash      2287  dan    0u   CHR  136,8      0t0   11 /dev/pts/8
bash      2287  dan    1u   CHR  136,8      0t0   11 /dev/pts/8
bash      2287  dan    2u   CHR  136,8      0t0   11 /dev/pts/8
bash      2287  dan  255u   CHR  136,8      0t0   11 /dev/pts/8
lsof      2866  dan    0u   CHR  136,8      0t0   11 /dev/pts/8
lsof      2866  dan    1u   CHR  136,8      0t0   11 /dev/pts/8
lsof      2866  dan    2u   CHR  136,8      0t0   11 /dev/pts/8

往tty里面直接写数据跟写标准输出是一样的效果
dev@dev:~$ echo aaa > /dev/pts/8
aaa

pts也是tty设备,它们的关系后面会介绍到

通过上面的lsof可以看出,当前运行的bash和lsof进程的stdin(0u)、stdout(1u)、stderr(2u)都绑定到了这个TTY上。

下面是tty和进程以及I/O设备交互的结构图:


   Input    +--------------------------+    R/W     +------+
----------->|                          |<---------->| bash |
            |          pts/1           |            +------+
<-----------|                          |<---------->| lsof |
   Output   | Foreground process group |    R/W     +------+
            +--------------------------+             

可以把tty理解成一个管道(pipe),在一端写的内容可以从另一端读取出来,反之亦然。

这里input和output可以简单的理解为键盘和显示器,后面会介绍在各种情况下input/ouput都连接的什么东西。

tty里面有一个很重要的属性,叫Foreground process group,记录了当前前端的进程组是哪一个。process group的概念会在下一篇文章中介绍,这里可以简单的认为process group里面只有一个进程。

当pts/1收到input的输入后,会检查当前前端进程组是哪一个,然后将输入放到进程组的leader的输入缓存中,这样相应的leader进程就可以通过read函数得到用户的输入

当前端进程组里面的进程往tty设备上写数据时,tty就会将数据输出到output设备上

当在shell中执行不同的命令时,前端进程组在不断的变化,而这种变化会由shell负责更新到tty设备中

从上面可以看出,进程和tty打交道很简单,只要保证后台进程不要读写tty就可以了,即写后台程序时,要将stdin/stdout/stderr重定向到其它地方(当然deamon程序还需要做很多其它处理)。

先抛出两个问题(后面有答案):

  • 当非前端进程组里面的进程(后台进程)往tty设备上写数据时,会发生什么?会输出到outpu上吗?
  • 当非前端进程组里面的进程(后台进程)从tty设备上读数据时,会发生什么?进程会阻塞吗?

3.TTY是如何被创建的

下面介绍几种常见的情况下tty设备是如何创建的,以及input和output设备都是啥。

键盘显示器直连(终端)
先看图再说话:


                   +-----------------------------------------+
                   |          Kernel                         |
                   |                           +--------+    |       +----------------+ 
 +----------+      |   +-------------------+   |  tty1  |<---------->| User processes |
 | Keyboard |--------->|                   |   +--------+    |       +----------------+
 +----------+      |   | Terminal Emulator |<->|  tty2  |<---------->| User processes |
 | Monitor  |<---------|                   |   +--------+    |       +----------------+
 +----------+      |   +-------------------+   |  tty3  |<---------->| User processes |
                   |                           +--------+    |       +----------------+
                   |                                         |
                   +-----------------------------------------+

键盘、显示器都和内核中的终端模拟器相连,由模拟器决定创建多少tty,比如你在键盘上输入ctrl+alt+F1时,模拟器首先捕获到该输入,然后激活tty1,这样键盘的输入会转发到tty1,而tty1的输出会转发到显示器,同理用输入ctrl+alt+F2,就会切换到tty2。

当模拟器激活tty时如果发现没有进程与之关联,意味着这是第一次打开该tty,于是会启动配置好的进程并和该tty绑定,一般该进程就是负责login的进程。

当切换到tty2后,tty1里面的输出会输出到哪里呢?tty1的输出还是会输出给模拟器,模拟器里会有每个tty的缓存,不过由于模拟器的缓存空间有限,所以下次切回tty1的时候,只能看到最新的输出,以前的输出已经不在了。

不确定这里的终端模拟器对应内核中具体的哪个模块,但肯定有这么个东西存在

4. SSH远程访问

 +----------+       +------------+
 | Keyboard |------>|            |
 +----------+       |  Terminal  |
 | Monitor  |<------|            |
 +----------+       +------------+
                          |
                          |  ssh protocol
                          |
                          ↓
                    +------------+
                    |            |
                    | ssh server |--------------------------+
                    |            |           fork           |
                    +------------+                          |
                        |   ↑                               |
                        |   |                               |
                  write |   | read                          |
                        |   |                               |
                  +-----|---|-------------------+           |
                  |     |   |                   |           ↓
                  |     ↓   |      +-------+    |       +-------+
                  |   +--------+   | pts/0 |<---------->| shell |
                  |   |        |   +-------+    |       +-------+
                  |   |  ptmx  |<->| pts/1 |<---------->| shell |
                  |   |        |   +-------+    |       +-------+
                  |   +--------+   | pts/2 |<---------->| shell |
                  |                +-------+    |       +-------+
                  |    Kernel                   |
                  +-----------------------------+

这里的Terminal可能是任何地方的程序,比如windows上的putty,所以不讨论客户端的Terminal程序是怎么和键盘、显示器交互的。由于Terminal要和ssh服务器打交道,所以肯定要实现ssh的客户端功能。

这里将建立连接和收发数据分两条线路解释,为了描述简洁,这里以sshd代替ssh服务器程序:

4.1建立连接

  1. Terminal请求和sshd建立连接

  2. 如果验证通过,sshd将创建一个新的session

  3. 调用API(posix_openpt())请求ptmx创建一个pts,创建成功后,sshd将得到和ptmx关联的fd,并将该fd和session关联起来。

#pty(pseudo terminal device)由两部分构成,ptmx是master端,pts是slave端,
#进程可以通过调用API请求ptmx创建一个pts,然后将会得到连接到ptmx的读写fd和一个新创建的pts,
#ptmx在内部会维护该fd和pts的对应关系,随后往这个fd的读写会被ptmx转发到对应的pts。

#这里可以看到sshd已经打开了/dev/ptmx

dev@debian:~$ sudo lsof /dev/ptmx
COMMAND  PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
sshd    1191  dev    8u   CHR    5,2      0t0 6531 /dev/ptmx
sshd    1191  dev   10u   CHR    5,2      0t0 6531 /dev/ptmx
sshd    1191  dev   11u   CHR    5,2      0t0 6531 /dev/ptmx
  1. 同时sshd创建shell进程,将新创建的pts和shell绑定

4.2 收发消息

  1. Terminal收到键盘的输入,Terminal通过ssh协议将数据发往sshd

  2. sshd收到客户端的数据后,根据它自己管理的session,找到该客户端对应的关联到ptmx上的fd

  3. 往找到的fd上写入客户端发过来的数据

  4. ptmx收到数据后,根据fd找到对应的pts(该对应关系由ptmx自动维护),将数据包转发给对应的pts

  5. pts收到数据包后,检查绑定到自己上面的当前前端进程组,将数据包发给该进程组的leader

  6. 由于pts上只有shell,所以shell的read函数就收到了该数据包

  7. shell对收到的数据包进行处理,然后输出处理结果(也可能没有输出)

  8. shell通过write函数将结果写入pts

  9. pts将结果转发给ptmx

  10. ptmx根据pts找到对应的fd,往该fd写入结果

  11. sshd收到该fd的结果后,找到对应的session,然后将结果发给对应的客户端

5. 键盘显示器直连(图形界面)

 +----------+       +------------+
 | Keyboard |------>|            |
 +----------+       |  Terminal  |--------------------------+
 | Monitor  |<------|            |           fork           |
 +----------+       +------------+                          |
                        |   ↑                               |
                        |   |                               |
                  write |   | read                          |
                        |   |                               |
                  +-----|---|-------------------+           |
                  |     |   |                   |           ↓
                  |     ↓   |      +-------+    |       +-------+
                  |   +--------+   | pts/0 |<---------->| shell |
                  |   |        |   +-------+    |       +-------+
                  |   |  ptmx  |<->| pts/1 |<---------->| shell |
                  |   |        |   +-------+    |       +-------+
                  |   +--------+   | pts/2 |<---------->| shell |
                  |                +-------+    |       +-------+
                  |    Kernel                   |
                  +-----------------------------+

6.TTY和PTS的区别

从上面的流程中应该可以看出来了,对用户空间的程序来说,他们没有区别,都是一样的;从内核里面来看,pts的另一端连接的是ptmx,而tty的另一端连接的是内核的终端模拟器,ptmx和终端模拟器都只是负责维护会话和转发数据包;再看看ptmx和内核终端模拟器的另一端,ptmx的另一端连接的是用户空间的应用程序,如sshd、tmux等,而内核终端模拟器的另一端连接的是具体的硬件,如键盘和显示器。

7.常见的TTY配置

7.1 rows 51; columns 204;

这个配置一般由终端控制,当终端的窗口大小发生变化时,需要通过一定的手段修改该配置,比如ssh协议里面就有修改窗口大小的参数,sshd收到客户端的请求后,会通过API修改tty的这个参数,然后由tty通过信号SIGWINCH通知前端程序(比如shell或者vim),前端程序收到信号后,再去读tty的这个参数,然后就知道如何调整自己的输出排版了。

7.2 intr = ^C

tty除了在终端和前端进程之间转发数据之外,还支持很多控制命令,比如终端输入了CTRL+C,那么tty不会将该输入串转发给前端进程,而是将它转换成信号SIGINT发送给前端进程。这个就是用来配置控制命令对应的输入组合的,比如我们可以配置“intr = ^E”表示用CTRL+E代替CTRL+C。

 类似资料: