第10章 字符设备驱动程序
[466页]
10-1 总体功能
本章的程序可分成三部分:
第一部分是是关于RS-232串行线路驱动程序,包括程序rs_io.s和serial.c;
第二部分是涉及控制台的驱动程序,包括键盘中断驱动程序keyboard.S和控制台显示驱动程序console.c;
第三部分是终端驱动程序与上层接口部分,包括终端输入输出程序tty_io.c和终端控制程序tty_ioctl.c。
下面首先概述终端控制驱动程序实现的基本原理,然后分这三部分说明它们的基本功能。
10-1-1 终端驱动程序基本原理
终端驱动程序用于控制终端设备,在终端设备和进程之间传输数据,并对所传输的数据进行一定的处理。
用户在键盘上键入对的原始数据(raw data),在通过终端程序处理后,被传送给一个接收进程;而进程向终端发送的数据,在终端程序处理后,被显示在终端屏幕上或者通过串行线路被送到远程终端。
根据终端程序对待输入或输出数据的方式,可以把终端工作模式分成两种。
一种是规范模式(canonical),此时经过终端程序的数据将被进行变换处理,然后再送出。例如把TAB字符
扩展为8个空格字符,用键入的删除字符(backspace)控制删除前面键入的字符等。使用的处理函数一般称
为行规则(line discipline)模块。
另一种是非规范模式或秤原始(raw)模式。在这种模式下,行规则程序仅在终端与进程之间传送数据,而不对数据进行规范模式变换处理。
在终端驱动程序中,根据它们与设备的关系,以及在执行流程中的位置,可以分为字符设备的直接驱动程序和上层直接联系的接口程序。
可以用图10-1表示这种控制关系。
10-1-2 Linux支持的终端设备类型
终端是一种字符型设备,它有多种类型。通常使用tty来简称各种类型的终端设备。tty是Teletype的缩写。Teletype是一种由Teletype公司生产的最早的终端设备,外观很像电传打字机。在Linux0.1x系统设备文件目录/dev/中,通常包含以下一些终端设备文件:
这些终端设备文件可以分为以下几种类型:
(1)串行端口终端(/dev/ttySn)
(2)伪终端(/dev/ptyp、/dev/ttyp)
(3)控制终端(/dev/tty)
(4)控制台(/dev/ttyn,/dev/console)
(5)其他类型
10-1-3 中断基本数据结构
每个终端设备都对应一个tty_struct数据结构,主要用来保存终端设备当前参数设置、
所属的前台进程组ID合字符IO缓冲队列等信息。该结构定义在include/linux/tty.h文件中,
其结构如下所示:
struct tty_struct {
struct termios termios; //终端io属性和控制字符数据结构。
int pgrp; //所属进程组。
int session; //
int stopped; //停止标志。
void (*write)(struct tty_struct * tty); //tty写函数指针。
struct tty_queue *read_q; //tty读队列。
struct tty_queue *write_q; //tty写队列。
struct tty_queue *secondary; //tty辅助队列(存放规范模式字符序列),可称为规范(熟)模式队列。
};
extern struct tty_struct tty_table[];//tty结构数组。
Linux内核使用了数组tty_table[]来保存系统中每个终端设备的信息。每个数组项是一个数据结构tty_struct,对应系统中一个终端设备。
Linux0.12内核共支持三个终端设备。
一个是控制台设备,另外两个是使用系统上两个串行端口的串行终端设备。
termios结构用于存放对应终端设备的io属性。有关该结构的详细描述下面说明。
pgrp是进程组标识,它指明一个会话处于前台的进程组,即当前拥有该终端设备的进程组。
pgrp主要用于进程的作业控制操作。
stopped是一个标志,标识对应终端设备是否已经停止使用。
函数指针*write是该终端设备的输出处理函数,
对于控制台终端,它负责驱动显示硬件,在屏幕上显示字符等信息。对于通过系统串行端口连接的串行终端,它负责把输出字符发送到串行端口。
终端所处理的数据被保存在3个tty_queue结构的字符缓冲队列中(或称为字符表),如下所示:
struct tty_queue {
unsigned long data; //等待队列缓冲区中当前数据统计值。
//对于串口终端,则存放串口端口地址。
unsigned long head; //缓冲区中数据头指针。
unsigned long tail; //缓冲区中数据尾指针。
struct task_struct * proc_list; //等待本缓冲队列的进程列表。
char buf[TTY_BUF_SIZE]; //队列的缓冲区。
};
每个字符缓冲队列的长度是1KB。
其中读缓冲队列read_q用于临时存放从键盘或串行终端输入的原始(raw)字符序列;
写缓冲队列write_q用于存放写到控制台显示屏或串行终端去的数据;
根据ICANON标志,辅助队列secondary用于存放从read_q中取出的经过规则程序处理(过滤)的数据,或称为熟(cooked)模式数据。这是在行规则程序把原始数据中的特殊字符如删除(backspace)字符变换后的规范输入数据,以字符行为单位供应用程序读取使用。
上层终端读函数tty_read()即用于读取secondary队列中的字符。
在读入用户键入的数据时,中断处理汇编程序只负责把原始字符数据放入输入缓冲队列中,而由中断处理过程中调用的C函数(copy_to_cooked())来处理字符的变换工作。例如当进程向一个终端写数据时,终端驱动程序就会调用行规则函数cpy_to_cooked(),把用户缓冲区中的所有数据到写缓冲队列汇总,并将数据发送到终端上显示。在终端上按下一个键时,所引发的键盘中断处理过程会把按键扫描码对应的字符放入读队列read_q中,并调用规范模式处理程序把read_q
中的字符经过处理再放入辅助队列secondary中。与此同时,如果终端设备设置了回显标志(L_ECHO),则也把该字符放入写队列write_q中,并调用终端写函数把该字符显示在屏幕上。通过除了像键入密码或其他特殊要求以外,回显标志都是置位的。可以通过修改终端的termios结构中的信息来改变这些标志值。
在上述tty_struct结构中还包括一个termios结构,该结构定义在include/termios.h头文件中,
其字段内容如下所示:
#define NCCS 17
struct termios {
tcflag_t c_iflag; /* input mode flags */ //输入模式标志。
tcflag_t c_oflag; /* output mode flags */ //输出模式标志。
tcflag_t c_cflag; /* control mode flags */ //控制模式标志。
tcflag_t c_lflag; /* local mode flags */ //本地模式标志。
cc_t c_line; /* line discipline */ //线路规程(速率)。
cc_t c_cc[NCCS]; /* control characters */ //控制字符数组。
};
c_iflag是输入模式标志集。Linux0.12内核实现了POSIX.1定义的所有11个输入标志,参见termios.h头文件中的说明。终端设备驱动程序用这些标志来控制如何对终端输入的字符
进行变换(过滤)处理。例如是否需要把输入的换行符(NL)转换成回车符(CR)、是否需要把输入的大写字符转换成小写字符(因为以前有些终端设备只能输入大写字符)等。在Linux0.12内核中,相关的处理函数是tty_io.c文件中的copy_to_cooked()。参见termios.h文件第83~96行。
c_oflag是输出模式标志集。终端设备驱动程序使用这些标志控制如何把字符输出到终端上,主要在tty_io.c的tty_write()函数中使用。参见termios.h文件第99~129行。
c_cflag是控制模式标志集。主要用于定义串行终端传输特性,包括波特率、字符位数以及停止位数等。参见termios.h文件第132~166行。
c_lflag是本地模式标志集。主要用于控制驱动程序与用户的交互。例如是否需要回显(Echo)字符、是否需要把擦除字符直接显示在屏幕上、是否需要让终端上键入的控制字符产生信号。这些操作主要在copy_to_cooked()函数和tty_read()中使用。
例如,若设置了ICANON标志,则表示终端处于规范模式输入状态,否则终端处于非规范模式。
如果设置ISIG标志,则表示收到终端发出的控制字符INTR、QUIT、SUSP时系统需要产生相应的信号。
参见termios.h文件第169~183行。
上述4中标志集的类型都是unsigned long,每个位可表示一种标志,因此每个标志集最多可有32个输入标志。所有
这些标志及其含义可参见termios.h头文件。
c_cc[]数组包含了终端所有可以修改的特殊字符。例如你可以通过修改其中的中断字符(^C)由其他按键产生。其中NCCS是数组的长度值。终端默认的c_cc[]数组初始值定义在include/linux/tty.h文件中。程序引用该数组中各项时定义了数组项符号名,这些名称都以字母V开头,例如VINTR/VMIN。
参见termios.h文件第64~80行。
因此,利用系统调用ioctl或使用相关函数(tcsetattr()),我们可以通过修改termios结构中的信息来改变终端的设置参数。行规则函数即是根据这些设置参数进行操作。例如,控制
终端是否要对键入的字符进行回显、设置串行终端传输的波特率、清空读缓冲队列和写缓冲队列。
当用户修改终端参数,将规范模式标志复位,就会把终端设置为工作在原始模式,此时 行规则程序会把用户键入的数据原封不动地传送给用户,而回车符也被当作普通字符处理。
因此,在用户使用系统调用read时,就应该作出某种决策方案以判断系统调用read什么时候算完成并返回。这将由终端termios结构中的VTIME和VMIN控制字符决定。这两个是读操作的超时定时值。VMIN表示为了满足读操作,需要读取的最少字符数;VTIME则是一个读操作等待定时值。
我们可以使用命令stty来查看当前终端设备termios结构中标志的设置情况。在Linux0.1x系统命令行提示
符下键入stty命令会显示以下信息:
其中带有减号的标志标志没有设置。另外对于现在的Linux系统,需要键入"sty-a"才能
显示所有这些信息,并且显示格式有所区别。
终端程序所使用的上述主要数据结构和它们之间的关系如图10-2所示。
10-1-4 规范模式和非规范模式
1、 规范模式
当c_lflag中的ICANON标志置位时,则按照规范模式对终端输入数据进行处理。此时输入字符被装配成行,进程以字符行的形式读取。当一行字符输入后,终端驱动程序会立即返回。行的定界符有NL、EOL、EOL2和EOF。其中除最后一个EOF(文件结束)将被处理程序删除外,其余4个字符将被作为一行的最后一个字符返回给调用程序。
在规范模式下,终端输入的以下字符将被处理:ERASE、KILL、EOF、EOL、REPRINT、WERASE和EOL2。
具体字符的作用看赵老师。[472页]
2、 非规范模式
如果ICANON处于复位状态,则终端程序工作在非规范模式下。此时终端程序不对上述字符进行处理,而是将它们当作普通字符处理。输入数据也没有行的概念。终端程序何时返回读进程是由MIN和TIME的 值确定的。这两个变量时c_cc[]数组中的变量。通过修改它们即可改变在非规范模式下进程读字符的处理方式。
MIN指明读操作最少需要读取的字符;TIME指定等待读取字符的超时值(计量单位是1/10S)。
根据它们的值可分4中情况来说。
(1)MIN>0,TIME>0
(2)MIN>0,TIME=0
(3)MIN=0,TIME>0
(4)MIN=0,TIME=0
[472页]
10-1-5 控制台终端和串行终端设备
在Linux0.12系统中可以使用两类终端:
一类是主机上的控制台终端;
控制台终端由内核中的键盘中断处理程序keyboard.S和显示控制程序console.c进行管理。
它接收上层tty_io.c程序传递下来的显示字符或控制信息,并控制在主机屏幕上字符的显示,
同时控制台(主机)把键盘按键产生的代码由keyboard.S传递到tty_io.c程序去处理。
一类是串行硬件终端设备。
串行终端设备则通过线路连接到计算机串行端口上,并通过内核中的串行程序rs_io.s与tty_io.c直接进行信息交互。
keyboard.S和console.c这两个程序实际上是Linux系统主机中使用显示器和键盘模拟一个硬件终端设备的仿真程序。只是由于在主机上,因此我们称这个模拟终端环境为控制台终端,或直接称为控制台。这两个程序所实现的功能就相当于一个串行终端设备固化再ROM中的终端处理程序的作用
(除了通信部分),也像普通PC上的一个终端仿真软件。因此虽然程序在内核中,但我们还是可以独立地看待它们。这个模拟终端与普通的硬件终端设备主要的区别在于不需要通过串行线路通信驱动程序。因此keyboard.S和console.c程序必须模拟一个实际终端设备(例如DEC的VT100终端)具备的所有硬件处理功能,即 终端设备固化程序中除通信以外的所有处理功能。控制台终端和串行终端设备在处理结构上的相互去呗与类似之处 参见图10-3。所以如果我们对一般硬件终端设备或终端仿真程序工作原理有一定了解,那么阅读这两个程序就不会遇到什么困难。
图10-3
1、 控制台驱动程序
在Linux0.12内核中,终端控制台驱动程序涉及keyboard.S和console.c程序。keyboard.S用于处理用户键入的字符,把它们放入读缓冲队列read_q中,并调用copy_to_cooked()函数读取read_q中的字符,经转换后放入辅助缓冲队列secondary。console.c程序实现控制台终端收到代码的输出处理。
看看赵老师举的例子 [473页]
图10-4 控制台键盘中断处理过程
对于进程执行tty写操作,终端驱动程序是一个字符一个字符进行处理的。在写缓冲队列write_q没有满时,就从用户缓冲区取一个字符,经过处理放入write_q中。当把用户数据全部放入write_q队列或者此时write_q已满,就调用终端结构tty_struct中指定的写函数,把write_q缓冲队列中的
数据输出到控制台。对于控制台终端,其写函数是con_write(),在console.c程序中实现。
有关控制台终端操作的驱动程序,主要涉及两个程序:
一个是键盘中断处理程序keyboard.S,主要用于把用户键入的字符放入read_q缓冲队列中;
另一个是屏幕显示处理程序console.c,用于从write_q队列中取出字符并显示在屏幕上。
所有这三个字符缓冲队列与上述函数或文件的关系都可以用图10-5清晰地表示出来。
2、串行终端驱动程序
处理串行终端操作的程序有serial.c和rs_io.s。
serial.c程序负责对串行端口进行初始化操作。另外,通过取消对发送保持寄存器空中断允许的屏蔽来开启串行中断发送字符操作。rs_io.s程序是串行中断处理过程。主要根据引发中的4种原因分别进行处理。
引起系统发生串行中断的情况有:
由于modem状态发生了变化;
由于线路状态发生了变化;
由于接收到字符;
由于在中断允许标志寄存器中设置了发送保持寄存器中断允许标志,需要发送字符。
10-1-6 中断驱动程序接口
通常,用户通过文件系统与设备打交道。每个设备都有一个文件名称,并相应地也在文件系统中占一个索引节点(i节点)。但该i节点中的文件类型是设备类型,以便与其他正规文件区别。用户就可以直接使用文件系统调用来访问设备。终端驱动程序也同样为此目的向文件系统提供了调用接口函数。终端驱动程序与系统其他程序的接口是使用tty_io.c文件中的通用函数实现的。其中实现了读终端函数tty_read()和写终端函数tty_write(),以及输入行规则函copy_to_cooked()。
另外,在tty_ioctl.c程序中,实现了修改终端参数的输入输出控制函数(或系统调用)tty_ioclt()。终端的设置参数放在终端结构中的termios结构中,其中的参数比较多,也比较复杂,
请参考include/termios.h文件中的说明。
对于不同终端设备,可以有不同的行规则程序与之匹配。但在Linux0.12中仅有一个行规则函数,因此termios结构中的行规则字段"c_line"不起作用,都被设置为0。