写点什么

终端闲思录(2)- 终端的源流嬗变

作者:黑客不够黑
  • 2023-12-21
    江苏
  • 本文字数:3883 字

    阅读完需:约 13 分钟

终端闲思录(2)- 终端的源流嬗变

终端(terminal),源自拉丁语 terminalis,意为“与边界或结尾有关,最终的”,"与计算机通信的设备"之意首次记录于 1954 年,时间上距今不足百年,而计算机日新月异的发展速度使得很多事物快速出现又快速消亡,变成随时间层层累积的沉积岩,搞清楚其源流嬗变殊为不易,以至于后来的我们很难看清事情的原貌。我实在很想拥有钩沉索微的能力,去近距离感受每处痕迹背后的波澜壮阔,而不是如今只能通过抚摸巨岩的横截面,来想象那个时代的风云际会。

1

终端是一种和计算机交互的硬件设备(早期是硬件,如今已是软件),用于处理输入和输出。最早的硬件终端是电传打印机(teleprinterteletypewriterteletypeTTY 指的都是电传打印机),显示内容需要打印到纸上,这也是为什么我们在编程中向终端打印使用 print 而不是 display 的原因,使用屏幕显示内容还要等到 CRT(阴极射线管)设备的出现。



这些硬件终端与计算机通过串口直连,或者通过调制解调器远程连接,不过这种连接方式的距离和终端数量都很有限。如果把计算机比作一条章鱼,那么终端就是触手的顶端,从拉丁词源 terminalis “与边界或结尾有关,最终的”之意中,我们多少还能窥见这一层含义。



图 1-2 是 DEC 公司生产的图形终端 VT-100 ,广泛流行的终端模拟器和 SSH 客户端软件 SecureCRT 中,可以设置模拟的终端类型,其中就有 VT-100 系列。从 SecureCRT 各种终端类型中,依然可以看出当年终端设备市场是怎样一个山头林立的状态,这些设备没有统一的标准,各自有各自的字符转义序列。所谓的字符转义序列是指向终端发送的特殊控制字符,终端会将这些特殊字符解释为相应的功能,比如调整终端的显示,vi 类软件特别需要标准化,再比如Ctrl+C是向会话中的前台进程发送SIGINT信号(终端如何得知哪一个是前台进程请参考 Unix/Linux 手册进程组部分的内容),Ctrl+D会使得从终端读取输入的进程读取到一个end-of-file


现代意义上的终端已经几乎全部虚拟化、软件化了,Unix/Linux 系统可以通过Ctrl+Alt+Fn 组合键切换虚拟终端,现代 Linux 系统通常在其中一个终端启动图形界面,我的 Manjaro 桌面就启动在 F2 上。Unix/Linux 在功能键上启动的这些终端即是虚拟终端/dev/ttyn


另一个与终端有关的概念是 Console —— 控制台,控制台其实是一种终端,大多时候和计算机长在一起,不一定要有屏幕,摇杆、按钮也可称为控制台,只要是能控制计算机的都属于控制台。


现代个人计算机已经没有控制台了,终端和控制台已经虚拟化,且大多时候混用这两概念也没有问题。但寻其本意依然都有处理计算机输入与输出的意思,又因终端和控制台经常和标准三剑客关联,有些软件不免就会混淆其中,例如 java 著名的日志框架 logback 有一个输出目的地 ConsoleAppender ,其实是用 System.outSystem.err 将内容写往标准输出和标准错误的,而标准输出和标准错误并不一定连接终端或控制台,标准三剑客的相关内容我会在后面详述,现在我们有必要看一下传统的终端登录过程,看看终端是如何被打开的。




SysV 系统中,init 进程是系统启动后用户空间拉起的第一个进程,也叫 1 号进程,现代 Linux 如果采用 Systemd 系统启动,其 1 号进程是 Systemd 进程。传统 Unix 在启动时 init 进程会扫描/etc/ttys中的内容,/etc/ttys中配置有连接到该计算机上的终端设备列表,init 进程会遍历每个设备,针对每个设备都 fork 一个进程来处理,图 1-3 展示了这个过程。


fork 之后子进程会执行getty程序,getty 会打开终端,如果终端是通过调制解调器连接的,getty 会等待对方拨号,一旦设备打开成功,文件描述符0,1,2就被设置到终端上了,而0,1,2就是标准三剑客,如果不出意外(文件描述符未被重定向),后续对0,1,2的读写都是对终端的读写,在内核中由终端驱动提供服务。不难想见,终端驱动会吸收键盘的输入,会将输出打印到设备屏幕。


getty 的最后一项使命是向终端打印“login:”,等待我们输入用户名,一旦我们输入用户名回车,getty 便功成身退,它会以类似如下方式调用login程序:


execle("/bin/login", "login", "-p", username, (char *)0, envp);
复制代码


login 程序会向终端打印“Password:”来提示用户输入密码,当然终端的回显功能会被关掉。接下来 login 会做一系列的工作,比如鉴权、设置环境变量、设置 HOME 目录、开启会话、设置进程组等等,最后会调用 shell 程序:


execl("/bin/sh", "-sh", (char *)0);
复制代码


如图 1-4 所示,execl 后进程变为 shell,但0,1,2文件描述符得以保留,毋庸置疑的是,此时三剑客指向终端,shell 进程的读写都依赖终端驱动程序处理。我们现在将终端驱动部分放大,看看其内部对输入输出的处理过程,如图 1-5 所示:



进程对于终端设备的读写由终端驱动处理,终端驱动维护着两个队列:输入队列和输出队列,键盘的输入会进入输入队列,最后被进程读取;进程的输出会进入输出队列,由终端显示。如果该终端设备的回显功能被打开,进入输入队列的内容会同时发送到输出队列,由终端设备显示在屏幕上,这就是你敲击键盘后终端中显示内容的原因,前文的 login 程序在收集密码时就会将回显功能关闭。


我之所以讲“你敲击键盘后终端中显示内容”而不是“shell 中显示内容”,是为了强调一个事实:你能看到的所有内容都是你的输入和 shell 的输出。你永远不会在 shell 里面,你只能给它提供输入,观察它的输出,当然是在终端上。


而此刻,shell 向终端输出了root@hostname:~ #的提示符,终端驱动程序将其打印到可见终端,当然,这个终端有可能是Ctrl+Alt+Fn 组合键下显示器呈现的虚拟终端,也有可能是视窗界面下的终端模拟器,但基本不可能是最初的终端设备了。


这当然是因为那些设备现在已经消失了,物理设备一经消亡,原本显示的重任就交给显卡来处理了,终端驱动想必会因此生出黍离一样的悲痛。而我们当然不会在乎终端驱动是否悲痛,我们只是对这种割裂感有所不适。


Ctrl+Alt+Fn 组合键大概是对早期硬件终端连接场景的模拟,毕竟在 PC 个人化以及 shell 作业控制广泛应用的今天,实在看不出它还有什么其它的意义。因为在这种场景下进入终端,我们只能看到显示设备(不能称之为终端,如显示器)以及 shell 的输出,终端一词就像无源之水、无根之木一样没了着落。


请允许我用夸张的舞台腔背诵加缪《西西弗神话》中的段落:"在一个突然被剥夺了幻觉和光明的宇宙中,人就感到自己是个局外人。这种放逐无可救药,因为人被剥夺了对故乡的回忆和对乐土的希望。这种人和生活的分离,演员和布景的分离,正是荒诞感。"


终端和 shell 的概念纠缠正是出于这种荒诞感!


讲到这里,似乎不再需要刻意去辨析终端和 shell 的区别了,现象已经很明朗:连接到终端的最终程序是 shell,shell 以及由此 shell 启动的任何程序其三剑客都指向终端,它们的输出也会打印到终端。作为计算机行业的新新人类,失去了物理真实的触摸,极目所望,尽是 shell 及其子孙的输出,终端的概念早已淹没其中,混淆也就在所难免了。


视窗界面下的终端模拟器似乎将这种荒诞缓和了一些,但也仅仅是一些,在一个单薄的窗口中,shell 的输出占据了绝大多数的领地,只有边框和工具栏在隐隐的提示人们:我是有形的!


无论如何,由电传打印机沿袭下来的 tty 一词却在 Unix 中留下了深深的印记,tty 子系统、tty 驱动、虚拟终端 /dev/ttyn、伪终端 pty 等都有着teletype的影子。

2

上述终端登录场景,进程打开的设备是/dev/ttyn,通常被称为虚拟终端,基本只有在Ctrl+Alt+Fn 组合键和虚拟机管理界面的控制台进入的终端属于此类。大多数场景用的都是伪终端,比如终端模拟器和网络 SSH 登录,本节我们就梳理一下这两种常见的终端场景。


伪终端其实是 IPC(进程间通信)的一种,它有一对主从设备, 也叫伪终端对,分别连接着两个进程:



图 2-1 是使用伪终端相关进程的典型结构,伪终端主设备和从设备组成了一个双向管道,连接了两个进程。通常连接从设备的进程是 shell,所以,对 shell 来讲伪终端从设备表现的就像原来的终端设备,终端驱动也是和从设备相连,进程对终端的读写都发往从设备。


与以往不同的是,进程眼中的终端设备在这里不以显示为直接目的,而是将输出发往另一个进程,输入也要从另一个进程读取,而这正是为终端模拟器和网络 SSH 登录设计的,我们先看一看终端模拟器的情况:



图 2-2 展示了一个打开了两个窗口的终端模拟器,终端模拟器是一个图形化的视窗程序,针对每一个窗口创建一个伪终端对,并 fork 出 shell 进程,将 shell 进程的标准三剑客连接到伪终端从设备,如此一来,shell 便从终端模拟器程序读取输入,输出发往终端模拟器,最后被渲染到窗口界面。这是连接本地终端的情况,下面再看一下 SSH 登录:



SSH 是使用伪终端的另一个例子,它允许本地用户安全地通过网络连接到远程机器上登录 shell,图 2-3 展示了这种情况,ssh server 为每个登录请求创建伪终端对,并 fork 出 shell 进程连接到伪终端从设备。客户端的输入通过网络抵达 ssh server,ssh server 发往伪终端主设备,最终变为 shell 进程的标准输入;同样,由 shell 产生的输出经过伪终端主设备抵达 ssh server,再经 ssh server 发送到网络,最终被 ssh client 接收。现在请你思考一下:ssh client 会如何处理接收到的 shell 输出呢?


从图中不难看出,ssh client 要将 shell 的输出送往终端显示,问题是你能猜出图中的terminal是什么设备吗?


其实我们已经讲过了,此处的terminal可以看作图 2-2 的缩影,ssh client 连接的是本地伪终端对中的从设备,用户使用的可能是终端模拟器,模拟器 fork 的进程就是 ssh client,shell 的提示符root@localhost:~ # 历经千山万水,终于呈现在你本地的终端模拟器上了。


发布于: 刚刚阅读数: 5
用户头像

感而后应,迫而后动,不得已而后起 2018-11-20 加入

非著名程序员,任职过测试,前端,devops,DBA、Go 后端开发等等 个人网站: https://liupzmin.com 联系方式: liupzmin@gmail.com

评论

发布
暂无评论
终端闲思录(2)- 终端的源流嬗变_终端_黑客不够黑_InfoQ写作社区