由于串口的缘故,TTY是Linux系统中最普遍的一类设备,稍微了解Linux系统的同学,对它都不陌生。尽管如此,相信很少有人能回到这样的问题:TTY到底是什么东西?我们常常挂在嘴边的终端(terminal)、控制台(console)等概念,到底是什么意思?
本文是Linux TTY framework分析文章的第一篇,将带着上述疑问,介绍TTY有关的基本概念,为后续的TTY软件框架的分析,以及Linux serial subsystem的分析,打好基础。
关于终端最典型的例子,就是电传打字机(Teletype)[1][2]—-一种基于电报技术的远距离信息传送器械。电传打字机通常由键盘、收发报器和印字机构等组成。发报时,按下某一字符键,就能将该字符的电码信号自动发送到信道(input);收报时,能自动接收来自信道的电码信号,并打印出相应的字符(output)。
2.2 Unix终端
在计算机的世界里,键盘和显示器,是最常用的终端设备,一个用于向计算机输入信息,一个用于显示计算机的输出信息。
在大型机(mainframe)和小型机(minicomputer)的时代里,终端设备和计算机主机都同属一个整体。但到PC时代,情况发生了变化。Unix创始人肯•汤普逊和丹尼斯•里奇想让Unix成为一个多用户系统。多用户系统意味着要给每个用户配置一个终端,每个用户都要有一个显示器、一个键盘。但当时所有的计算机设备(包括显示器)价格都非常昂贵,而且键盘和主机是集成在一起的,根本没有独立的键盘。
最后他们找到了一样东西,那就是ASR33电传打字机。虽然电传打字机的用途是在电报线路上收发电报,但是它也可以作为人与计算机的接口,而且价格低廉。ASR33打字机的键盘用来输入信息,打印纸用来输出信息。所以他们把ASR33电传打字机作为终端,很多个ASR33连接到同一个主机,每个用户都可以在终端输入用户名和密码登录主机。这样他们创造了计算机历史上的第一个真正的多用户系统Unix,而ASR33成为第一个Unix终端。
2.3 TTY设备
由上面的介绍可知,第一个Unix终端是一个名字为ASR33的电传打字机,而电传打字机的英文单词为Teletype(或Teletypewritter),缩写为TTY。因此,该终端设备也被称为TTY设备。这就是TTY这个名称的来源,当然,在现在的Unix/Linux系统中,TTY设备已经演变为不同的意义了,后面我们会介绍演变的过程。
注1:读到这里,希望读者再仔细思索一下“设备”的概念。ASR33的电传打字机本身是一个硬件设备,在Unix/Linux系统中,这个硬件设备被抽象为“TTY设备”。
2.4 串口终端(Serials Terminal)
早期的TTY终端(这里暂时特指电传打字机),一般通过串口和Unix设备连接的,如下所示:
tty_teletype
然后,正如你我所熟知的,我们可以把上面红色部分(电传打字机),替换为任意的具有键盘、显示器、串口的硬件设备(如另一台PC),如下:
tty_any
因此,对Unix/Linux系统来说,只要是通过串口连接的设备,都可以作为终端设备,因而不再需要关注具体的终端形态。久而久之,终端设备、TTY设备、串口设备等概念,逐渐混在一起,就不再区分了,总结来说,在当今的Linux系统中:
1)TTY设备就是终端设备,终端设备就是TTY设备,无需区分。
2)所有的串口设备都是TTY设备。
3)当然,除了串口设备,也发展出来了其它形式的TTY设备,例如虚拟终端(VT)、伪终端(Pseudo Terminal)等等,这些概念本文就不展开描述了,后续会使用专门的文章分析。
回到Unix系统刚刚支持多用户(2.2小节的描述)的时代,此时的PC有一个自带的、昂贵的终端(自身的键盘、显示器等),另外为了支持多用户,可以通过串口线连接多个TTY终端(Teletype)。为了彰显自带终端崇高的江湖地位,人们称它为console。
当然,“江湖地位”之说,纯属玩笑,不过从console的中文翻译—–控制台,可以看出,自带终端(console)有别于TTY终端的地方如下:
1)控制台(console)是昂贵的。
2)控制台(console)比TTY终端拥有更多的权限,例如用户建立、密码更改、权限分配等等,这也是“控制”的意义所在。
3)系统的运行日志、出错信息等内容,通常只会输出到控制台(console)终端中,以方便管理员进行“控制”和“管理”。
不过,随着计算机技术的发展、操作系统的改进,控制台(console)终端和普通TTY终端的界限越来越模糊,console能做的事情,普通终端也都能做了。因此,console逐渐退化,以至于在当前的Linux系统中,它仅仅保留了第三点“日志输出”的功能,这就是Linux TTY framework中console的概念
在Linux kernel中,TTY就是各类终端(Terminal)的简称。为了简化终端的使用,以及终端驱动程序的编写,Linux kernel抽象出了TTY framework:对上,向应用程序提供使用终端的统一接口;对下,提供编写终端驱动程序(如serial driver)的统一框架。
本文是Linux TTY framework分析的第二篇文章,将从整体架构的角度,介绍Linux TTY framework,以便分解出功能相对独立的子模块,以便后续的分析。
2.1 TTY Core
TTY core是TTY framework的核心逻辑,功能包括:
1)以字符设备的形式,向用户空间提供访问TTY设备的接口,例如:
设备号(主, 次) 字符设备 备注
(5, 0) /dev/tty 控制终端(Controlling Terminal)
(5, 1) /dev/console 控制台终端(Console Terminal)
(4, 0) /dev/vc/0 or /dev/tty0 虚拟终端(Virtual Terminal)
(4, 1) /dev/vc/1 or /dev/tty1 同上
… … …
(x, x) /dev/ttyS0 串口终端(名称和设备号由驱动自行决定)
… … …
(x, x) /dev/ttyUSB0 USB转串口终端
… … …
注1:控制终端、控制台终端、虚拟终端等概念,比较抽象,我会在后续的文章中详细介绍。
2)通过设备模型中的struct device结构抽象TTY设备,并通过struct tty_driver抽象该设备的驱动,并提供相应的register接口。TTY驱动程序的编写,简化为填充并注册相应的struct tty_driver结构。
注2:TTY framework弱化了TTY设备(图片1中使用虚线框标注)的概念,通常情况下,可以在注册TTY驱动的时候,自动分配并注册TTY设备。
3)使用struct tty_struct、struct tty_port等数据结构,从逻辑上抽象TTY设备及其“组件”,以实现硬件无关的逻辑。
4)抽象出名称为线路规程(Line Disciplines)的模块,在向TTY硬件发送数据之前,以及从TTY设备接收数据之后,进行相应的处理(如特殊字符的转换等)。
2.2 System Console Core
Linux kernel的system console主要有两个功能:
1)向系统提供控制台终端(Console Terminal) ,以便让用户登录进行交互操作。
2)提供printk功能,以便kernel代码进行日志输出。
System console core模块使用struct console结构抽象system console功能,具体的driver不需要关心console的内部逻辑,填充该接口并注册给kernel即可。
2.3 TTY Line Disciplines
线路规程(Line Disciplines)在TTY framework中是一个非常优雅的设计,我们可以把它看成设备驱动和应用接口之间的一个适配层。从字面意思理解,就是辅助TTY driver,将我们通过TTY设备键入的字符转换成一行一行的数据[3],当然,实际情况远比这复杂,例如在蜗窝x project所使用的kernel版本中,存在如下的Line Disciplines(以n_为前缀,我们后续的文章会更为详细的介绍):
pengo@DESKTOP-CH8SB7C:~/work/xprj/linux$ ls drivers/tty/n_*
drivers/tty/n_gsm.c drivers/tty/n_r3964.c drivers/tty/n_tracesink.c drivers/tty/n_tty.c
drivers/tty/n_hdlc.c drivers/tty/n_tracerouter.c drivers/tty/n_tracesink.h
2.4 TTY Drivers以及System Console Drivers
最后,对内核以及驱动工程师来说,更关注的还是具体的TTY设备驱动。在kernel为我们搭建的如此beauty的框架下面,编写相应的driver就成为一件比较简单的事情了。当然的kernel中,主要的TTY driver有两类:
1)虚拟终端(Virtual Terminal,VT)驱动,位于drivers/tty/vt中,负责实现VT(后续文章会详细介绍)有关的功能。
2)串口终端驱动,也即我们所熟知的serial subsystem(话说终于到重点了,哈哈),位于drivers/tty/serial中。
控制终端、控制台终端、虚拟终端等概念的理解及解释;
TTY core的分析;
System Console Core的分析;
Serial subsystem(串口子系统)的分析;
虚拟终端(VT)的分析;
常用线路规程(Line Disciplines)的介绍和分析;
等等。
可以毫不夸张的说,我们在使用Linux系统的过程中,每时每刻都在和TTY打交道,显示输出、键盘输入、用户登录、shell终端、等等。
与此同时,作为软件工程师的我们,也会或多或少的困惑:这些习以为常的行为,怎么和kernel中的这些冷冰冰的代码联系起来的?
因此,在Linux TTY framework分析工作正式开始之前,让我们带着上面的疑问,以这些熟悉的应用场景为视角,进一步理解TTY有关的概念。这就是本文的目的。
输入(input),向计算机发送指令;
输出(output),计算机将执行结果显示出来。
与此同时,关于输入/输出,可提出如下疑问(为了简单,将输入、输出看作一个统一的整体,即终端):
输入、输出设备是什么?
计算机怎么接收和输出?
人怎么接收和输出(这个就不用回答了,大家都知道,呵呵)。
关于这两个疑问,下面我们分别讨论。
2.1 终端的类型
根据不同的输入、输出设备类型,我们常见的终端有如下几类:
1)控制台终端(console)
这类终端的输入设备通常是键盘,输出设备通常是显示器;
输入、输出设备通过各类总线直接和计算机相连;
“终端”其实是这些设备的一个逻辑抽象。
2)虚拟终端(VT)
控制台终端的输出设备(显示器)一般只有一个,同一时刻由一个应用程序独占;
但在多任务的操作环境中,有时需要在将终端切换给另一个应用程序之前,保留当前应用在终端上的输出,以方便后面查看;
因此Unix/Linux系统在控制台终端的基础上,又虚拟出来6个终端—-称作虚拟终端,不同的应用程序可以在这些虚拟终端上独立的输出,在需要的时候,可以通过键盘的组合键(CTRL+ALT+ F1~F6)将某一个虚拟终端调出来在屏幕上显示。
3)串口终端(TTY)
这是正牌的TTY设备:
输入设备和输出设备集成在一个独立的硬件上(称作TTY设备),这个硬件和计算机通过串口连接;
输入设备(键盘)的输入动作,将会转换为串口上的RX数据包(以计算机为视角),发送给计算机;
计算机的输出会以TX数据包的形式发送给TTY设备,TTY设备转换后在输出设备(屏幕)上显示。
4)软件终端
这是我们现在最常用的终端:
既然人机交互的数据流可以封装后经过串口传输,那么终端设备的形式就不再受限了,只要可以接收用户的输入并打包通过串口发送给计算机,以及接收计算机从串口发来的输出并显示出来,任何设备都可以变成终端设备,例如另一台计算机;
当另一台计算机被当作终端设备时,通常不会把它的所有资源都用来和对端进行人机交互,常用的方法是,在这个计算机上利用软件,模拟出来一个“终端设备”。该软件就像一个中间商:从键盘接收用户输入,然后控制串口发送给对端;从串口接收对端的输出,然后在软件界面上显示出来;
平时大家经常使用的PuTTY、SecureCRT、Windows超级终端、等等,都是“软件终端”。
5)USB、网络等终端
既然串口可以作为人机交互数据的传说媒介,其它通信接口一样可以,例如USB、Ethernet、等等,其原理和串口终端完全一样,这里不再过多说明。
6)图形终端
前面所介绍的那些终端,人机交互的输出界面都是字符界面,随着计算机技术的发展,GUI界面慢慢出现并成为主流,这些通过GUI交互的形式,也可以称作图形终端。不过这已经超出了TTY framework系列文章的讨论范围了,因为TTY的势力范围只涵盖字符界面。
2.2 输入和输出
首先声明,这里的输入和输出,都是针对计算机设备而言。
另外,计算机是一个硬件设备,它本身没有输入和输出的自主意识,因此,输入和输出,都是指运行在计算机中的软件,所谓的人机交互,其实是人和一个个的软件交互。
以Linux系统为例:
kernel会通过控制台终端输出日志信息(printk);
kernel启动之后,init进程会打开控制台终端,以便和我们交互(接收一些指令并应答、输出日志信息、等等);
随后,getty应用可以打开任意的终端设备,和我们交互,以便我们可以登入系统;
登入系统之后,getty将终端的控制权交给shell(bash、sh等等),我们与之交互,进行着愉快的人机对话,巴拉巴拉……
当然,在人机对话的过程中,我们可以命令shell启动其它的应用,例如地图应用,该应用可能会打开另一个TTY设备(例如GPS UART),并与之交互(由此可见,上面提到的人机交互其实是狭隘的概念,也可以机机交互~~);
等等,等等。
注1:建议读者结合“Linux TTY framework框架[1]”阅读本节内容。
3.1 console driver
Linux系统中可以存在多个种类各异的终端设备,工程师会使用一个个的TTY driver(struct tty_driver)驱动它们。如果某些我们需要让某些终端作为控制台终端,可以基于TTY driver,创建对应的console driver,并注册给kernel。
关于console driver,kernel有如下的策略:
1)可以同时注册多个console driver,并有选择的使用。
2)可以在kernel启动的时候,通过命令行(或者后来的device tree),告诉kernel使用哪个或者哪些控制台终端。例如:
console=”/dev/ttyS0”, console=”/dev/ttyUSB0”
3)对kernel的日志输出来说,可以在所有被选中的控制台终端上输出。
4)后续可用作人机交互的控制台终端只能有一个(后指定的那个)。
3.2 控制台终端(/dev/console)
前面提到过,控制台终端只能有一个(最后指定的那个),那么“/dev/console”又是怎么回事呢?
了解linux kernel启动过程的同学都知道,kernel启动的后期,会在kernel_init线程(最后会退化为init进程)中打开控制台终端。但是由上面3.1小节的介绍可知,控制台终端的类型、名称是五花八门的,怎么让kernel的核心代码无视这些差异呢?这就是“/dev/console”的存在意义:
由“Linux TTY framework(2)_软件架构[2]”中的介绍可知,/dev/console的设备号固定为(5, 1) ,当init线程打开该设备的时候,TTY core会问system console core:喂,哪一个终端适合做控制台终端啊?
因此,最终打开的是那个具体的、可以当作控制台终端的设备,而“/dev/console”,仅仅是一个占位坑,如下图所示:
dev_console
3.3 虚拟终端(/dev/ttyN)
正如其名,虚拟终端是虚拟出来的一个终端,不对应具体的设备(屏幕和键盘)。应用程序可以打开某一个虚拟终端,以便和人进行交互。
对应用程序而言,这个终端和具体的物理终端,没有任何区别(应用程序也无法区分)。而对整个系统来说,由于物理资源(键盘和屏幕)只有一套,因此同一时刻只能和某一个虚拟终端对接。从另一个角度看,各个虚拟终端轮流使用物理资源和人进行交互,如下所示:
vt
3.4 伪终端(Pseudo Terminal,pty)
前面提到过,既然串口(serial)可用作终端和计算机之间的数据传输通道,那么其它诸如Ethernet的通信介质,也可以实现类似的功能。但这里面有一个问题(以网络为例):
Linux系统中的网络驱动,并不是以TTY的形式提供API(众所周知,网络使用socket接口)。因此,系统中的应用程序,无法直接打开网络设备,进而和对应的终端设备通信。怎么办?
解决方案就是伪终端(英文为Pseudo Terminal,简称pty)。字面上理解,伪终端根本不是终端(虚拟终端好歹还是),是为了让应用程序可以从网络等非串口类的接口上,和终端设备交互而伪造出来的一种东西。它的原理如下:
pty由pts(pseudo-terminal slave)和ptm(pseudo-terminal master)两部分组成。
pts伪造出一个标准的TTY设备,应用程序可以直接访问。应用程序向pts写入的数据,会直接反映到ptm上,同样,应用程序从pts读数据,则相当于直接从ptm读取。
而pym,则根据具体情况,具体实现。例如:要通过网络接口和终端设备交互,则pym需要打开对应的socket,将pts写来的数据,从socket送出,将从socket读取的数据,送回给pts。
有了伪终端,Unix/Linux系统中的终端设备就可以脱离具体的物理限制,可以是任何形态,例如在GUI环境中,使用Terminal软件(如xterm)模拟出来的终端,其原理为:
用户启动xterm等Terminal软件的时候,该软件会生成一对pts和ptm,同时执行shell应用;
Terminal软件会将pts交给shell应用,shell应用傻傻分不清,还以为它是一个标准的TTY设备呢,欢快的运行了;
shell从pts读入数据,相当于从ptm读入;shell向pts输出数据,相当于向ptm写入;这是由TTY framework自动完成的,不需要Terminal软件关心;
在另一端,Terminal软件将会从ptm读取数据(实际上是shell通过pts写入的数据),并显示在自己的GUI窗口上;于此同时,Terminal软件会从自己的GUI窗口接收用户输入,并通过pts转给shell,太完美了。
上面提到了有关伪终端的两个例子,其示意图图下:
pty
3.5 控制终端(control terminal,/dev/tty)
讲到控制终端(control terminal),就不得不提Unix/Linux的Job control[3]功能。有关job control,感兴趣的读者可以参考其它文章(如[3]),这里简单总结如下:
1)job control是Unix/Linux shell(记住,shell是一个应用程序)中的概念,是shell用来管理、控制jobs的一种方法。
2)job是进程组(process group[4])在shell中的体现。换句话说,shell借用进程组实现了job。
3)进程组(或者job,在本文中可以等同,后面不在区分)是多个进程的组合。
基于上面简单的知识(不一定准确,但不影响我们对控制终端的理解),我们引入控制终端的概念:
1)通常情况下,Linux启动后,终端(以后都用TTY指代)的控制权会交给shell(一种应用程序)。所谓的控制权,就是指shell程序可以通过TTY读取终端的输入,以及通过TTY向终端输出。
2)通过shell,可以启动其它的应用程序,相应地,应用程序在需要的时候也会获得TTY的控制权。
3)同一时刻,只能有一个应用可以占有TTY,即只有一个应用可以通过TTY输入、输出。
4)那个占有TTY、可以进行输入输出的应用,称作前台应用。相应的,不能进行输入输出的应用,称作后台应用。因此,shell中只有一个前台应用,可以有多个后台应用。
5)然后,问题就来了:如果某个后台应用,就是想输入输出,怎么办?有一个办法,就是通过控制终端(control terminal)。
6)控制终端在Linux中的名称固定为/dev/tty, 设备号为(5, 0),作用和/dev/console类似,进程可以通过TTY core提供的ioctl,选择控制终端所对应的实际的终端设备。
7)暂且抛开前台应用不谈(因为人家有TTY设备),对于那些后台应用,如果想输入输出,可以读取或者写入控制终端。此时,一般情况下,TTY core会向后台应用发送SIGTTIN(读取控制终端时) 或者 SIGTTOU(写入控制终端时)信号,这会终止该后台应用。
8)不过,shell会重设收到 SIGTTOU信号时的行为,于是,后台应用写入的内容,可以通过控制终端显示出来。