赵炯;《Linux 内核完全注释 0.11 修正版 V3.0》
在 Linux0.11 内核中,字符设备主要包括控制终端设备和串行终端设备。
1. 总体功能
本章程序分为三部分:1. RS-232 串行线路驱动程序(rs_io.s、serial.c);2. 另一部分涉及控制台驱动程序,包括键盘中断驱动程序 keyboard.S 和控制台显示驱动程序 console.c;3. 终端驱动程序与上层接口部分,包括终端输入输出程序 tty_io.c 和终端控制程序 tty_ioctl.c。
1.1 终端驱动程序基本原理
终端驱动程序用于控制终端设备,在终端设备和进程之间传输数据,并对所传输的数据进行一定的处理。用户在键盘上键入的原始数据,在通过终端程序处理后,被传送给一个接收进程;而进程向终端发送的数据,在终端程序处理后,被显示在终端屏幕上或者通过串行线路被发送到远程终端。根据终端程序对待输入或输出数据的方式,可以把终端工作模式分成两种。一种是规范模式,此时经过终端程序的数据将被进行变换处理,然后再送出。例如把 TAB 字符扩展为 8 个空格字符,用键入的删除字符控制删除前面键入的字符等。使用的处理函数一般称为行规则模块。另一种是非规范模式或称为原始模式。在这种模式下,行规则程序仅在终端与进程之间传送数据,而不对数据进行规范模式的变换处理。
1.2 Linux 支持的终端设备类型
终端是一种字符型设备,它有许多类型。通常使用 tty 来简称各种类型的终端设备。tty 是 Teletype 的缩写。Teletype 是一种由 Teletype 公司生产的最早出现的终端设备,样子像是电传打字机。
- 串行端口终端(/dev/ttySn)。串行端口终端是使用计算机串行端口连接的终端设备。计算机把每个串行端口都看作是一个字符设备。类似DOS系统下的 COM1、COM2。若要向一个端口发送数据,可以在命令行上把标准输出重定向到这些特殊文件名上即可。例如,在命令行提示符下键入:
echo test > /dev/ttyS1 ,就会把单词 test 发送到连接在 ttyS1 端口的设备上。 - 伪终端(/dev/ptyp、/dev/ttyp)。Pseudo Terminals。功能类似于一般终端的设备,但是这种设备并不与任何终端硬件相关。伪终端设备用于为其他程序提供类似于终端式样的接口,主要应用于通过网络登录主机时为网络服务器和登录shell程序之间提供一个终端接口,或为运行于 X Window 窗口中的终端程序提供终端样式的接口。
- 控制终端(/dev/tty)。字符设备文件 /dev/tty 是进程控制终端的别名,其主设备号是 5,次设备号是 0。如果当前进程有控制终端,那么 /dev/tty 就是当前进程控制终端的设备文件。对于登录shell来讲,/dev/tty 就是我们使用的终端。它有些类似于连接到实际终端设备的一个链接。
- 控制台(/dev/ttyn、/dev/console)。在 Linux 系统中,计算机显示器通常被称为控制台终端或控制台,并且有一些字符设备文件与之关联:tty0、tty1、tty2等,当我们在控制台上登陆时,使用的就是 tty1。tty1-tty6 被称为虚拟终端,而tty0则是当前所使用虚拟终端的一个别名。Linux 系统所产生的信息都会发送到 tty0 上。只有系统或超级用户root可以向/dev/tty0执行写操作。而且有时 /dev/console 也会连接至该终端设备上。
- 其他类型。
1.3 终端基本数据结构
include/linux/tty.h
每个终端设备都对应有一个 tty_struct 数据结构。
Linux 内核使用数组 tty_table[] 来保存系统中每个终端设备的信息。每个数组项是一个数据结构 tty_struct,对应一个终端设备。Linux 0.11 内核供支持三个终端设备,一个是控制台设备,另外两个是使用系统上两个串行端口的串行终端设备。
1.4 规范模式和非规范模式
规范模式
非规范模式
1.5 控制台终端和串行终端设备
在 Linux 0.11 系统中可以使用两类终端。一类是主机上的控制台终端,另一类是串行硬件终端设备。
控制台终端由内核中的键盘中断处理程序 keyboard.s 和显示控制程序 console.c 进行管理。它接收上层 tty_io.c 程序传递下来的显示字符或控制信息,并控制在主机屏幕上字符的显示,同时控制台(主机)把键盘按键产生的代码经由 keyboard.s 传送到 tty_io.c 程序去处理。
串行终端设备则通过线路连接到计算机串行端口上,并通过内核中的串行程序 rs_io.s 与 tty_io.c 直接进行信息交互。
键盘中断处理程序 keyboard.s 和显示控制程序 console.c 所实现的功能就相当于一个串行终端设备固化在 ROM 中的终端处理程序的作用,也像普通 PC 机上的一个终端仿真软件。这个模拟终端与普通的硬件终端设备主要的区别在于不需要通过串行线路通信驱动程序。键盘中断处理程序 keyboard.s 和显示控制程序 console.c 必须模拟一个实际终端设备具备的所有硬件处理功能,即终端设备固化程序中除通信以外的所有处理功能。
1.5.1 控制台驱动程序
键盘中断处理程序 keyboard.s 和显示控制程序 console.c。
当用户在键盘上键入了一个字符时,会引起键盘中断响应(中断请求信号IRQ1,对应中断号INT33),此时键盘中断处理程序就会从键盘控制器读入对应的键盘扫描码,然后根据使用的键盘扫描码映射表译成相应字符,放入 tty 读队列 read_q 中,然后调用中断处理程序的 C 函数 do_tty_interrupt(),它又直接调用行规则函数 copy_to_cooked() 对该字符进行过滤处理,并放入 tty 辅助队列 secondary 中,同时把该字符放入 tty 写队列 write_q 中,并调用写控制台函数 con_write()。此时如果该终端的回显 echo 属性是设置的,则该字符会显示到屏幕上。do_tty_interrupt() 和 copy_to_cooked() 函数在 tty_io.c 中实现。
读队列 - 过滤 - 写队列 - 显示
对于进程执行 tty 写操作,终端驱动程序是一个字符一个字符进行处理的。在写缓冲队列 write_q 没有满时,就从用户缓冲区取一个字符(keyboard.S)放入 read_q 缓冲队列,经过处理放入 write_q 中,当把用户数据全部放入 write_q 队列或者此时 write_q 已满,就调用终端结构 tty_struct 中指定的写函数,把 write_q 缓冲队列中的数据输出到控制台(console.c)。对于控制台终端,其写函数是 con_write(),在 console.c 中实现。
1.5.2 串行终端驱动程序
处理串行终端操作的程序有 serial.c 和 rs_io.s。serial.c 程序负责对串行端口进行初始化操作。
当进程需要写数据到一个串行终端上时,操作过程与写终端类似,只是此时终端的 tty_struct 数据结构中的写函数是串行终端写函数 rs_write()。
串行终端与控制台处理过程之间的主要区别是串行终端利用程序 rs_io.s 取代了控制台操作显示器和键盘的程序 console.c 和 keyboard.S,其余部分的处理过程完全一样。
1.5.6 终端驱动程序接口
用户通过文件系统与设备打交道,每个设备都有一个文件名称,并相应地也在文件系统占用一个索引节点,但该节点中的文件类型是设备类型,以便与其它正规文件相区别。用户可以直接使用文件系统调用来访问设备。终端驱动程序也同样为此目的向文件系统提供了调用接口函数。终端驱动程序与系统其他程序的接口是使用 tty_io.c 文件中的通用函数实现的。
2. keyboard.S 程序
该键盘驱动汇编程序主要包括键盘中断处理程序。该程序首先根据键盘特殊键(Alt、Shift、Ctrl、Caps 等)等状态设置程序后面要用到的状态标志变量 mode 的值,然后根据引起键盘中断的按键扫描码,调用已经编排成跳转表的相应扫描码处理子程序,把扫描码对应的字符放入字符队列(read_q)中,接下来调用 C 处理函数 do_tty_interrupt(),该函数仅包含一个对行规程函数 copy_to_cooked() 的调用。这个行规程函数的主要作用就是把 read_q 读缓冲队列中的字符经过适当处理后放入规范模式队列中(secondary),并且在处理过程中,若相应终端设备设置了回显标志,还会把字符直接放入写队列中,从而在终端屏幕上会显示刚键入的字符。
3. console.c 程序
本程序中的所有子程序都是为了实现终端屏幕写函数 con_write() 以及进行终端屏幕显示的控制操作。
当往一个控制台设备执行写操作时,就会调用 con_write() 函数。这个函数管理所有控制字符和换码字符序列,这些字符给应用程序提供全部的屏幕管理操作。
函数 con_write() 主要由转换语句组成,用于每次处理一个字符的有限长状态自动语义序列的解释。在正常方式下,显示字符使用当前属性直接写到显示内存中。该函数会从终端 tty_struct 结构的写缓冲队列 write_q 中取出字符或字符序列,然后根据字符的性质,把字符显示在终端屏幕上或进行一些光标移动、字符擦除等屏幕控制操作。
终端屏幕初始化函数 con_init() 会根据系统初始化时获得的系统信息,设置有关屏幕的一些基本参数值,用于 con_write() 函数的操作。
4. serial.c 程序
本程序实现系统串行端口初始化,为使用串行端口设备作好准备工作。在 rs_init() 初始化函数中,设置了默认的串行通信参数,并设置串行端口的中断陷阱门(中断向量)。rs_write() 函数用于把串行终端设备写缓冲队列中的字符通过串行线路发送给远端的终端设备。
rs_write() 将在文件系统中用于操作字符设备文件时被调用。当一个程序往串行设备 /dev/tty64 文件执行写操作时,就会执行系统调用 sys_write()(fs/read_write.c中),而这个系统调用在判别出所读文件是一个字符设备文件时,即会调用 rw_char() 函数(在 fs/char_dev.c 中),该函数则会根据所读设备的子设备号等信息,由字符设备读写函数表调用 rw_tty(),最终调用到这里的串行终端写操作函数 rs_write()。
rs_write() 函数实际上只是开启串行发送保持寄存器已空中断标志,在 UART 将数据发送出去后允许发中断信号。具体发送操作是在 rs_io.s 程序中完成。
5. rs_io.s 程序
该汇编程序实现 rs232 串行通信中断处理过程。在进行字符的传输和存储过程中,该中断过程主要对终端的读、写缓冲队列进行操作。它把串行线路上接收到的字符存入串行终端的读缓冲队列 read_q 中,或把写缓冲队列 write_q 中需要发送出去的字符通过串行线路发送给远端的串行终端设备。
引起系统发生串行中断的情况有 4 种:
- 由于modem状态发生了变化;
- 由于线路状态发生了变化;
- 由于接收到的字符;
- 由于在中断允许标志寄存器中设置了发送保持寄存器中断允许标志,需要发送字符。
对前两种情况,通过读取对应状态寄存器值,从而使其复位。 对于接收到的字符的情况,程序首先把该字符放入读缓冲队列 read_q 种,然后调用 copy_to_cooked() 函数进行规则化,放入 secondary 队列种。 对于第四种情况,程序首先从写缓冲队列write_q尾指针处取出一个字符发送出去,再判断写缓冲队列是否已空,若还有字符则循环执行发送操作。
6. tty_io.c 程序
每个 tty 设备有 3 个缓冲队列,分别是读缓冲队列 read_q、写缓冲队列 write_q、辅助缓冲队列 secondary,定义在 tty_struct 结构中。对于每个缓冲队列,读操作是从缓冲队列的左端取字符,并且把缓冲队列尾指针向右移动,而写操作则是往缓冲队列的右端添加字符,并且也把头指针向右移动,这两个指针中,任何一个若移动到超出了缓冲队列的末端,则折回到左端重新开始。
本程序包括字符设备的上层接口函数,主要含有终端读/写函数tty_read()和tty_write(),copy_to_cooked()。
copy_to_cooked() 函数处理流程:
7. tty_ioctl.c 程序
本文件用于字符设备的控制操作,实现了函数 tty_ioctl(),程序通过使用该函数可以修改指定终端 termios 结构中的设置标志等信息,tty_ioctl() 函数将由 fs/ioctl.c 中的输入输出控制系统调用 sys_ioctl() 来调用。
7.1 波特率与波特率因子
|