终端 和 Shell 的关系
作者:互联网
早期的终端
早期的终端一般是一种叫做 电传打字机 (Teletype) 的设备。为啥呢?因为 Unix 的创始人 Ken Thompson 和 Dennis Ritchie 想让 Unix 成为一个多用户系统。多用户系统就意味着要给每个用户配置一个终端,每个用户都要有一个显示器、一个键盘。但当时所有的计算机设备都非常昂贵(包括显示器),而且键盘和主机是集成在一起的,根本没有独立的键盘。
后来他们机智地找到了一样东西,那就是 ASR-33 电传打字机。虽然电传打字机原本的用途是在电报线路上收发电报,类似如下:
+----------+ Physical Line +----------+
| teletype |<--------------------->| teletype |
+----------+ +----------+
但是它既有可以发送信号的键盘,又能把接收到的信号打印在纸带上,完全可以作为人机交互设备使用。
而且最重要的是,价格低廉。
于是,他们把很多台 ASR-33 连接到计算机上,让每个用户都可以在终端登录并操作主机。就这样,他们创造了计算机历史上第一个真正的多用户操作系统 Unix,而电传打字机就成为了第一个 Unix 终端。
终端模拟器
随着计算机的进化,我们已经见不到专门的终端硬件了,取而代之的则是键盘与显示器。
但是没有了终端,我们要怎么与那些传统的、不兼容图形接口的命令行程序(比如说 GNU 工具集里的大部分命令)交互呢?这些程序并不能直接读取我们的键盘输入,也没办法把计算结果显示在我们的显示器上。
这时候我们就需要一个程序来模拟传统终端的行为,即 终端模拟器 (Terminal Emulator)。
+----------+ +-------------------+
| Keyboard | -----> | | +-------+
+----------+ | Terninal Emulator | <----> | Shell |
| Monitor | <----- | | +-------+
+----------+ +-------------------+
对于 Shell,终端模拟器会「假装」成一个传统终端设备;而对于现代的图形接口,终端模拟器会「假装」成一个 GUI 程序。一个终端模拟器的标准工作流程是这样的:
- 捕获你的键盘输入;
- 将输入发送给 Shell(Shell 会认为这是从一个真正的终端设备输入的);
- 拿到命 Shell 的输出结果;
- 调用图形接口(比如 X11),将输出结果渲染至显示器。
终端模拟器有很多,这里就举几个经典的例子:
- GNU/Linux:gnome-terminal、Konsole;
- macOS:Terminal.app、iTerm2;
- Windows:Win32 控制台、ConEmu 等。
▲终端模拟器:Hyper 与 wsl-terminal
现在,专门的终端硬件已经基本上仅存于计算机博物馆,人们通常图省事儿,直接称呼终端模拟器为「终端」。
终端窗口 vs 虚拟控制台
大部分终端模拟器都是在图形用户界面 (GUI) 中运行的,但是也有例外。
比如在 GNU/Linux 操作系统中,按下 Ctrl + Alt + F1,F2…F6 等组合键可以切换出一个黑不溜秋的全屏终端界面,不过不要被它们唬着了,虽然它们并不运行在图形界面中,但其实它们也是终端模拟器的一种。
▲ 一个正在显示系统启动信息的虚拟控制台
这些全屏的终端界面与那些运行在 GUI 下的终端模拟器的唯一区别就是它们是 由操作系统内核直接提供的。这些由内核直接提供的终端界面被叫做 虚拟控制台 (Virtual Console),而上面提到的那些运行在图形界面上的终端模拟器则被叫做 终端窗口 (Terminal Window)。除此之外并没有什么差别。
当然了,因为终端窗口是跑在图形界面上的,所以如果图形界面宕掉了那它们也就跟着完蛋了。这时候你至少还可以切换到 Virtual Console 去救火,因为它们由内核直接提供,只要系统本身不出问题一般都可用。
什么是 shell
shell 是一个程序,它接受从键盘输入的命令,然后把命令传递给操作系统去执行。
+----------+ +-------------------+
| Keyboard | -----> | | +-------+ +----+
+----------+ | Terninal Emulator | <----> | Shell | -----> | OS |
| Monitor | <----- | | +-------+ +----+
+----------+ +-------------------+
什么是 TTY
最早的 Unix 终端是 ASR-33 电传打字机。而电传打字机 (Teletype/Teletypewriter) 的英文缩写就是 tty,即 tty 这个名称的来源。
由于 Unix 被设计为一个多用户操作系统,所以人们会在计算机上连接多个终端(在当时,这些终端全都是电传打字机)。Unix 系统为了支持这些电传打字机,就设计了名为 tty 的子系统(没错,因为当时的终端全都是 tty,所以这个系统也被命名为了 tty,就是这么简单粗暴),将具体的硬件设备抽象为操作系统内部位于 /dev/tty*
的设备文件。
▲ 还记得上面我们说过的特殊的终端,也就是通过 Ctrl + Alt + F1-6 呼出的那些虚拟控制台 (Virtual Console) 吗?其对应的就是上图中的 tty1
到 tty6
。
随着计算机的发展,终端设备已经不再限制于电传打字机,但是 tty 这个名称还是就这么留了下来。久而久之,它们的概念就混淆在了一起。所以在现代,tty 设备就是终端设备,终端设备就是 tty 设备,无需区分。
+----------------+
| TTY Driver |
| |
| +-------+ | +----------------+
+------------+ | | |<---------->| User process A |
| Terminal A |<--------->| ttyS0 | | +----------------+
+------------+ | | |<---------->| User process B |
| +-------+ | +----------------+
| |
| +-------+ | +----------------+
+------------+ | | |<---------->| User process C |
| Terminal B |<--------->| ttyS1 | | +----------------+
+------------+ | | |<---------->| User process D |
| +-------+ | +----------------+
| |
+----------------+
由于早期计算机上的 串行端口 (Serial Port) 最大的用途就是连接终端设备,所以当时的 Unix 会把串口上的设备也同样抽象为 tty 设备(位于
/dev/ttyS*
)。因此,现在人们也经常将串口设备称呼为 tty 设备。
内核 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来和终端打交道
什么是 pts
liyongjun@Box:~$ tty
/dev/pts/0
liyongjun@Box:~$ lsof /dev/pts/0
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
bash 2206 liyongjun 0u CHR 136,0 0t0 3 /dev/pts/0
bash 2206 liyongjun 1u CHR 136,0 0t0 3 /dev/pts/0
bash 2206 liyongjun 2u CHR 136,0 0t0 3 /dev/pts/0
bash 2206 liyongjun 255u CHR 136,0 0t0 3 /dev/pts/0
lsof 40676 liyongjun 0u CHR 136,0 0t0 3 /dev/pts/0
lsof 40676 liyongjun 1u CHR 136,0 0t0 3 /dev/pts/0
lsof 40676 liyongjun 2u CHR 136,0 0t0 3 /dev/pts/0
liyongjun@Box:~$ echo 666 > /dev/pts/0
666
pts 也是 tty 设备。
通过上面的 lsof 可以看出,当前运行的 bash 和 lsof 进程的 stdin(0u)、stdout(1u)、stderr(2u) 都绑定到了这个TTY 上。
下面是 tty 和进程以及 I/O 设备交互的结构图:
Input +--------------------------+ R/W +------+
----------->| |<---------->| bash |
| pts/0 | +------+
<-----------| |<---------->| lsof |
Output | Foreground process group | R/W +------+
+--------------------------+
- 可以把 tty 理解成一个管道(pipe),在一端写的内容可以从另一端读取出来,反之亦然。
- 这里input和output可以简单的理解为键盘和显示器,后面会介绍在各种情况下 input/ouput 都连接的什么东西。
- tty 里面有一个很重要的属性,叫 Foreground process group,记录了当前前端的进程组是哪一个。process group 的概念会在下一篇文章中介绍,这里可以简单的认为 process group 里面只有一个进程。
- 当 pts/0 收到 input 的输入后,会检查当前前端进程组是哪一个,然后将输入放到进程组的 leader 的输入缓存中,这样相应的 leader 进程就可以通过 read 函数得到用户的输入
- 当前端进程组里面的进程往 tty 设备上写数据时,tty 就会将数据输出到 output 设备上
- 当在 shell 中执行不同的命令时,前端进程组在不断的变化,而这种变化会由 shell 负责更新到 tty 设备中
从上面可以看出,进程和 tty 打交道很简单,只要保证后台进程不要读写 tty 就可以了,即写后台程序时,要将stdin/stdout/stderr 重定向到其它地方
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 的时候,只能看到最新的输出,以前的输出已经不在了。
不确定这里的终端模拟器对应内核中具体的哪个模块,但肯定有这么个东西存在
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服务器程序:
建立连接
- 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
liyongjun@Box:~$ sudo lsof /dev/ptmx
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
sshd 41185 liyongjun 10u CHR 5,2 0t0 88 /dev/ptmx
sshd 41185 liyongjun 12u CHR 5,2 0t0 88 /dev/ptmx
sshd 41185 liyongjun 13u CHR 5,2 0t0 88 /dev/ptmx
- 4.同时sshd创建shell进程,将新创建的pts和shell绑定
收发消息
- 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,然后将结果发给对应的客户端
键盘显示器直连 图形界面
+----------+ +------------+
| Keyboard |------>| |
+----------+ | Terminal |--------------------------+
| Monitor |<------| | fork |
+----------+ +------------+ |
| ↑ |
| | |
write | | read |
| | |
+-----|---|-------------------+ |
| | | | ↓
| ↓ | +-------+ | +-------+
| +--------+ | pts/0 |<---------->| shell |
| | | +-------+ | +-------+
| | ptmx |<->| pts/1 |<---------->| shell |
| | | +-------+ | +-------+
| +--------+ | pts/2 |<---------->| shell |
| +-------+ | +-------+
| Kernel |
+-----------------------------+
为了简化起见,本篇不讨论Linux下图形界面里Terminal程序是怎么和键盘、显示器交互的。
这里和上面的不同点就是,这里的 Terminal 不需要实现 ssh 客户端,但需要把 ssh 服务器要干的活也干了(当然ssh 通信相关的除外)。
TTY和PTS的区别
从上面的流程中应该可以看出来了,对用户空间的程序来说,他们没有区别,都是一样的;从内核里面来看,pts 的另一端连接的是 ptmx,而 tty 的另一端连接的是内核的终端模拟器,ptmx 和终端模拟器都只是负责维护会话和转发数据包;再看看 ptmx 和内核终端模拟器的另一端,ptmx 的另一端连接的是用户空间的应用程序,如 sshd、tmux 等,而内核终端模拟器的另一端连接的是具体的硬件,如键盘和显示器。
TTY相关信号
除了上面介绍配置时提到的 SIGINT,SIGTTOU,SIGWINCHU 外,还有这么几个跟 TTY 相关的信号
SIGTTIN
当后台进程读 tty 时,tty 将发送该信号给相应的进程组,默认行为是暂停进程组中进程的执行。暂停的进程如何继续执行呢?请参考下一篇文章中的 SIGCONT。
SIGHUP
当 tty 的另一端挂掉的时候,比如 ssh 的 session 断开了,于是 sshd 关闭了和 ptmx 关联的 fd,内核将会给和该tty 相关的所有进程发送 SIGHUP 信号,进程收到该信号后的默认行为是退出进程。
SIGTSTP
终端输入 CTRL+Z 时,tty 收到后就会发送 SIGTSTP 给前端进程组,其默认行为是将前端进程组放到后端,并且暂停进程组里所有进程的执行。
跟 tty 相关的信号都是可以捕获的,可以修改它的默认行为
参考
命令行界面 (CLI)、终端 (Terminal)、Shell、TTY,傻傻分不清楚?
Linux TTY/PTS概述 - SegmentFault 思否
《快乐的Linux命令行》—— 什么是 shell
流程图工具
标签:关系,tty,Shell,终端,进程,ptmx,pts,模拟器 来源: https://blog.csdn.net/lyndon_li/article/details/117250633