《返璞归真--UNIX技术内幕》--第9章 字符设备驱动

本系统中的输入输出设备都是字符设备,它们包括:KL-11/DL-11A—电传串行接口、PC-11—纸带打孔机和LP-11—行打印机。其中KL-11用于连接终端(terminal),作为用户键盘输入和显示输出的交互接口。纸带打孔机作为纸带输入和输出,而行打印机只是输出设备。

它们的驱动函数指针都记录在cdevsw数组中。

9.1  交互电传打字机teletypewriter

什么是终端(terminal)?终端是操作系统提供用于和用户实现人机交互的设备和应用程序。

UNIX下,该设备通常是某种字符设备,比如KL-11,对应的设备文件类似于/dev/ttyx,而相应的应用程序就是shsh通过打开文件/dev/ttyx来处理用户的输入,并输出结果。

KL-11可以用来连接显示终端和键盘,还可以通过modem连接远程终端。这里它被用来连接电传打字机(teletypewriter)作为用户输入输出终端,KL-11接口的示意图如图9-1所示。

《返璞归真--UNIX技术内幕》--第9章   字符设备驱动

9-1  KL-11接口示意图

“电传打字机”是在“键盘+显示器”的输入输出设备出现以前电脑主要的交互式输入输出设备,你可以把它想像成一个上盖带有键盘的打印机,用户所打的字和电脑输出的结果都会在键盘前方的打印输出口上打印出来。“电传打字机”是大型计算机(MAINCOMPUTER)和小型计算机(SMALLCOMPUTER)时代最主要的电脑交互式输入输出设备。

每当用户在键盘上输入一个字符时,纸张上会相应打印出输入的字符。但如果电传打字机工作在行模式下,那么只有当用户键入“回车-换行符”(“\r\n”)时,刚才键入的一行字符才会显示。“回车-换行符”最早也起源于这里,回车是把显示位置移动到本行首,而换行是进纸移动到下一行,这样用户就可以在下一行接着输入了。但UNIX系统中只使用换行符“\n”来代替“回车-换行符”。

《返璞归真--UNIX技术内幕》--第9章   字符设备驱动

9-2  电传打字机

在打字过程中产生错误时,用户可以键入“删除键”(相当于现在的backspace键)删掉刚刚输入的字符,这时刚才输入的字符就会被黑色的矩形框所覆盖。用户也可以删除整行。

电传打字机不仅可用于本地输入输出,它还可以用于远距离通信。电传既具有电话的快速,又具有打字机的准确,尤其是当电文中有资料时,这种优点表现得特别明显。人们普遍认为,电传这种通信方式,除了具备高效性和精确性之外,还比电报和电话更为便宜。

70年代中期以后,随着显示器设计的成熟,电传打字机就逐渐退出了电脑的世界,而键盘则从中摆脱出来成为了独立的一种设备。现在tty(teletypewriter的缩写)已经成为UNIX中输入输出设备的标准名称(一般输入输出设备名都叫/dev/tty*),尽管它早已失去了最初的含义。


 

9.1.1  设备特性

顾名思义,KL-11CPU和外设之间的异步串行接口,它通过众所周知的UARTUniversal Asynchronous Receiver-Transmitter)双缓存集成电路实现带有“启动停止位”数据的“串行←→并行”转换。当向接口上传送数据时,把并行数据转成串行输出;当从接口上读取数据时,把串行数据转成并行。它含有40根引脚,包括输入和输出缓存寄存器。提供字符长度和停止位设置功能,并可以提供每个传输字符的状态信息。它可以在全双工或半双工模式下工作,提供13种不同的波特率,从40b/s9600b/s不等。为保证传输正确性,它含2位停止位。UART包含接收器和传送器。


接收器用来从外设(这里是电传打字机)接收数据,它实现串行→并行的转换。移除了起始、停止位的字符被存放在接收缓存寄存器(RBUF)中的位70。它包含两个操作寄存器:RCSR(接收控制状态寄存器)和RBUF(接收缓存寄存器)。

传送器用来向外设—电传打字机输出字符,它实现并行→串行的转换。它也包含两个操作寄存器TCSR(传送控制状态寄存器)和TBUF(传送缓存寄存器)。这4个操作寄存器在空间上是连续的,地址由低到高依次是:RCSRRBUFTCSRTBUF

《返璞归真--UNIX技术内幕》--第9章   字符设备驱动

9-3  KL-11/DL-11寄存器分布示意图

由于系统中可能存在多个KL-11DL-11连接的终端,这样它们每一个终端都会有自己独立的寄存器空间。对于KL-11DL-11A/B接口,其第一个接口(unit 0)寄存器起始地址是0o177560;而unit115寄存器空间是0o1765000o176676。对于DL-11CDE接口,其unit 0寄存器起始地址位于0o175610unit1位于0o175620…,直到unit 30位于0o176170

 

9.1.2  操作寄存器

1.接收控制状态寄存器(RCSR

   

   

15

数据状态改变

只读位。在下列情况下该位被设:

a.位101213发生改变;

b.位140改成1

它在下列情况下被清除:

c.总线复位;

d.从RCSR中读取数据。

如果位5被设,则该位被设时,会产生中断

续表 

   

   

14

振铃指示

只读位。它和数据电路上的22号管脚(pin 22)相连,该引脚如果产生高电平则该位为1,低电平该位为0

13

清除完成待发送

只读位。它和5号引脚相连,该引脚如果产生高电平则该位为1,低电平则该位为0

12

进位检测

只读位。它和8号引脚—接收信号检测线相连,该引脚如果产生高电平则该位为1,低电平该位为0

11

接收器激活

只读位。该位被设,则表示UART的接收单元检测到数据起始位。它和3号引脚相连。当位7(接收完成)被设或总线复位时,该位被清除

10

第二接收数据

只读位。该位被设,表示已接收到第二数据位,它和12号引脚相连

98

保留

 

7

接收完成

只读位。该位被设,表示UART已经接收并传送一个字符至RBUF寄存器中。当位0(打开接收器)被设、读写RBUF或总线复位时,它被清除。如果位6被设,那么会产生中断

6

接收器中断使能

读写位。如该位被设,则每次位7被设时,会产生中断。它可以被程序或者总线复位操作清除

5

数据集中断使能

读写位。如该位被设,则每次位15被设时,都会产生中断。它可以被程序或者总线复位操作清除

4

保留

 

3

第二传输数据

读写位。它和11号引脚相连。该位被设,则导致引脚高电平;清除导致引脚低电平

2

请求发送位

读写位。该位被设则表示请求发送数据

1

数据终端就绪

读写位。该位被设表示数据终端就绪

0

接收器打开

只写位。该位被设表示接收器打开,接口可以接收数据了


其中位1512KL-11DL11-A/B上并未使用,值恒为0。可以看出,位5如果被设,则每接收到1位时,就会产生中断;而位6如果被设,则在接收完一个完整字符时,才会产生中断。实际上,UNIX代码只用到了位0、位1、位6和位7

2.接收缓存寄存器(RBUF

   

   

15

错误位

只读位。只要位1413中任何1位被设,则该位被设。表示接收过程产生错误

14

重叠运行

只读位。在UART企图向RBUF中写入新字符时,如果RCSR的位7没有被清除,则该位被设。因为RCSR的位7没有被清除,表示RBUF中数据还没有被读走,所以这时再有新字符,以前的字符就会被冲掉

13

帧错误

只读位。它被设是当UART在数据接收线上采样时,在第1个停止位中发现空白(停顿)。这时可能产生了下列错误:

a.某输入线断开;

b.接收到“中止”(“BREAK”)信号;

c.在接收字符中,产生了过多的失真变形信号

12

奇偶校验错

只读位。接收数据产生奇偶校验错

118

保留

 

70

接收数据

只读。接收到的字符,如果字符长度小于8,则字符存储在低位,未使用的最高位为0

其中位1412表示当前字符(位70)的状态,程序在读取下一个字符前,没必要清除它们。对于KL-11DL-11A/B,它们未使用,为0

3.传送控制状态寄存器(TCSR

   

   

158

保留

 

7

传送器就绪

只读位。该位被设表示传送器已经就绪,TBUF可以接收下一个字符。当有字符被写入TBUF时,该位被清除;而当TBUF中的字符被传送后,该位被设

                6

传送器中断时能

读写位。它被设,则当位7被设时,产生中断

53

保留

 

2

维护位

读写位。它被设,则传送器和接收器将连接起来,接收器会收到传送器发送的字符,主要用于测试目的

1

保留

 

0

中止位

读写位。如果被设,则传送器将输出一段空白字符(0

0不适用于KL-11DL-11A/B。如果位6被设,则每成功传送1个字符后,会产生输出中断。

4.传送缓存寄存器(TBUF

   

   

158

保留

 

70

传送字符

只写位,待传送字符

电传接口中断优先级为4级,其读取(输入)中断和写入(输出)中断向量不相同,读取中断向量位于地址60处,写入中断向量位于地址64处。下面是其驱动程序的实现。

9.1.3  驱动框架

在电传打字机和其处理进程(比如shell进程)之间,存在着一个缓存,该缓存包括两个部分:输入缓存和输出缓存。输入缓存用于暂存用户输入的字符,最大长度为电传打字机一行输入的字符256个;输出缓存用于暂存输出字符,它可能来自于用户输入回显,或者用户输入命令处理的结果。输入缓存实际上又包含2个:原始字符缓存和正规字符缓存。之所以引入缓存有下列原因:

1.输入的字符中可能包含转义字符,比如‘\r’,它事实上由2个字符‘\’‘n’组成,所以需要先存储到缓存后再统一做处理。

2.一般在遇到回车符后,进程才处理用户的输入。比如“ls –l”命令,只有当用户在电传打字机上输入回车-换行后,该命令才会被执行。所以这之前需要暂时缓存用户输入一行的字符。因为即使进程接收了换行前的字符,也不能马上作出正确的解析和处理。

3.在用户输入字符时,处理进程未必能够接收该字符,比如它正在进行磁盘访问而挂起等,或者用户采用行模式输入时。这时如果不缓存用户的输入字符,则它在下一次输入时就会丢失,因为RBUF中只能存储1个字符。而在中断服务函数中处理字符也不现实,因为它是一次性的,不能有太长时间的延迟,必须要保证在下一次中断触发前,本次处理完成。

4.在进程向电传打印机输出字符时,如果没有缓存,那么进程就需要等待所有字符都输出完毕后才能够继续处理其他事情,这显然是系统性能上的浪费。而有了输出缓存后,进程可以把待输出字符写到缓存中,启动传送操作后就返回了(实际情况更复杂一点,是为了照顾用户等待结果的感受)。这就提高了系统性能。

原始字符缓存即tty结构中的t_rawq字段(定义在tty.c中),它记录了用户从打字机上输入的原始字符(包括删除符、转义符等)。正规字符缓存则记录了把用户输入的原始字符进行相应处理(删除、转义等)后的字符,它是tty结构中的t_canq字段,进程应该对这里面的字符进行处理。输出缓存则记录了要向终端输出的字符,它是tty结构中的t_outq字段。

变量canonb[CANBSIZ]用于在将t_rawq中的原始字符转换成正规字符的过程中,临时存储使用。它的长度是一行字符的最大长度256。终端输入输出处理流程示意图如图9-4所示。

《返璞归真--UNIX技术内幕》--第9章   字符设备驱动

如图9-4所示,在用户每输入1个字符后,该字符通过KL-11接口被传送到RBUF寄存器中,同时触发输入中断。中断服务函数调用ttyinputRBUF中的字符复制到缓存tty.t_rawq中,如果符号是行结束符或者进程有处理原始字符的需求,则唤醒处理进程。

如果有进程挂起在上面,则通常是由于它之前调用klread从终端读取字符、而t_rawq中并没有字符引起的。处理进程在被输入中断唤醒后,调用canont_rawq中读取字符并转换成正规字符,最后存到t_canq中(比如‘\\’—> ‘\’)。然后处理进程把t_canq中的字符读取到进程空间,进行相应处理。处理完成后,它调用klwrite(其内部实现是调用ttyoutput)输出结果。ttyoutput事实上把字符输出到t_outq缓存。进程然后调用ttstart把字符从t_outq输入到TBUF寄存器中,这样字符就被输出到了终端。在1个字符成功输出后,输出中断被触发,它继续读取t_outq中的字符并输出到TBUF中。周而复始,直到所有字符都被输出到终端。用户就可以看到自己输入命令的执行结果。

另外,如果终端tty结构中的回显标志(ECHO)被设,则输入中断会把RBUF中的字符写入到t_outq中,并调用ttstart把它立即输出到终端,而不管当前有没有进程在处理这些字符(当然通常情况下是有的)。这样用户就能够及时看到自己输入的内容。

如果用户停止输入字符,则t_rawq中就没有新的字符,从而为空,处理进程获知这一点后,就会挂起,并不占用CPU。这是一个好的设计理念。如果用户再次输入字符,则输入中断会唤醒处理进程,处理进程又会读入字符进行处理。这样,一个交互式处理系统就实现了。

下面讲一下tty结构,它是和终端相关的一个很重要的结构,定义在tty.h中。

--------------------------选自光盘文件/usr/sys/tty.h------------------------
/*
 * 每一个UNIX字符设备都需要一个tty结构,
  * 它用于正常的终端I/O操作。
  * tty.c中的函数是处理这些结构公共部分的
  * 代码(设备无关代码)。
  * 每一个具体设备驱动的定义和设备相关代码
  * 在每一个驱动文件中定义(kl.c dc.c dh.c)。
  */

/*
  * A tty structure is needed for
  * each UNIX character device that
  * is used for normal terminal IO.
  * The routines in tty.c handle the
  * common code associated with
  * these structures.
  * The definition and device dependent
  * code is in each driver (kl.c dc.c dh.c)
  */

 struct tty
 {
  struct clist t_rawq; /* input chars right off device */
  struct clist t_canq; /* input chars after erase and kill */
  struct clist t_outq; /* output list to device */
  int t_flags; /* mode, settable by stty call */
  int *t_addr; /* device address (register or startup fcn) */
  char t_delct; /* number of delimiters in raw q */
  char t_col; /* printing column of device */
  char t_erase; /* erase character */
  char t_kill; /* kill character */
  char t_state; /* internal state, not visible externally */
  char t_char; /* character temporary */
  int t_speeds; /* output+input line speed */
  int t_dev; /* device name */
};


tty结构是对终端的一个抽象,它不仅可用于电传打字机,也可以用于任何其他输入输出设备包括键盘和显示器。这也是为什么至今UNIX系统中依然将终端简称为tty,尽管我们早已不用电传打字机。

t_rawqt_canqt_outq字段已经讲过。它们是以链表形式实现的,看一下clistcblock结构就知道了。


/*
  * A clist structure is the head
  * of a linked list queue of characters.
  * The characters are stored in 4-word
  * blocks containing a link and 6 characters.
  * The routines getc and putc (m45.s or m40.s)
  * manipulate these structures.
  */

 struct clist
 {
    int c_cc;/* character count */
    int c_cf;/* pointer to first block */
    int c_cl;/* pointer to last block */
 };

c_cc是链表中字符总数;c_cf指向第一个字符块,c_cl指向最后一个字符块,它们实际都是cblock指针,cblock结构定义在tty.c中。


------------------------选自光盘文件/usr/sys/dmr/tty.c-----------------------
/* The actual structure of a clist block manipulated by
  * getc and putc (mch.s)
  */

 struct cblock {
  struct cblock *c_next;
  char info[6];
};


cblock结构事实上定义了一个可记录6个字符(info字段)的字符块,c_next指向下一个字符块,缓存构造的示意如图9-5所示。

之所以每个cblock记录6个字符,是因为如果只记录1个字符,那么链表浪费的空间比较大(c_next属于浪费空间);但如果记录字符数太大,那么最后一个字符块浪费的空间又可能太大,最大是“字符块长度-1”,所以6是一个比较折中的方案。这有点类似于用磁盘块来存储文件,磁盘块既不能太大,也不能太小。

《返璞归真--UNIX技术内幕》--第9章   字符设备驱动

9-5  缓存构造示意图

系统中这样的cblock变量一共有100个,记录在cfree数组中。

/* The character lists-- space for 6*NCLIST characters */ 

struct cblock cfree[NCLIST];

 

 /* List head for unused character blocks. */

struct cblock *cfreelist;

系统初始化时,调用cinit(参见main函数)把它们形成链表记录在cfreelist中,程序每需要一个字符块,就从cfreelist中分配一个cblock指针变量,直到cfreelist为空。比较第8章块缓存的实现可以发现,cfreelist类似于bfreelist

函数原型:void cinit();

功能描述:把数组c_free中的所有cblock变量都形成链表,记录到cfreelist中。链表头就是cfreelist变量。同时通过判断cdevsw驱动数组判断系统中字符设备的个数,记录到nchrdev中。

参数说明:无。


------------------------选自光盘文件/usr/sys/dmr/tty.c-----------------------
/* Initialize clist by freeing all character blocks, & coun
  * number of character devices. (Once-only routine)
  */

 cinit()    
 {
   register int ccp;
   register struct cblock *cp;
   register struct cdevsw *cdp;
   ccp = cfree;
   for (cp=(ccp+07)&~07; cp = &cfree[NCLIST-1]; cp++) {
         cp->c_next = cfreelist;
         cfreelist = cp;
   }
   ccp = 0;
   for(cdp = cdevsw; cdp->d_open; cdp++)
         ccp++;
   nchrdev = ccp;
}


可用字符块链表示意图如图9-6所示。


《返璞归真--UNIX技术内幕》--第9章   字符设备驱动

9-6  可用字符块链表示意图

下面看看tty结构中的其他成员。

1t_flags

它定义终端特性和一些状态。


它定义终端特性和一些状态。
/* modes */
 #define HUPCL 01
  #define XTABS 02
 #define LCASE 04
 #define ECHO 010
 #define CRMOD 020
 #define RAW 040
 #define ODDP 0100
 #define EVENP 0200
 #define NLDELAY 001400
 #define TBDELAY 006000
 #define CRDELAY 030000
 #define VTDELAY 040000


HUPCLHang Up Carrier Line的缩写,标识设备关闭时要不要挂起数据位检测线(CARRIER LINE)。

XTABS标识需不需要把TAB键转成空格键。

LCASE标识用户输入的字符是不是需要转成小写。

ECHO标识用户输入时,需不需要回显。

CRMOD标识需不需要在回车-换行符和’\n’之间做转换。

RAW标识终端服务进程需不需要接收处理原始字符。

ODDP标识在输入输出字符时,有没有奇校验。

EVENP标识在输入输出字符时,有没有偶校验。

NLDELAY标识在输出换行符后,需不需要延时。

TBDELAY标识在输出水平TAB键后,需不需要延时。

CRDELAY标识在输出回车符后,需不需要延时。

VTDELAY标识在输出垂直TAB键或换页后,需不需要延时。

本系统中只有XTABSLCASEECHOCRMODRAW标志被使用到。


上一章 文件系统                         目录                    

本书在全国各大书店均有销售!

上一篇:《返璞归真--UNIX技术内幕》-- 第11章 UNIX可执行文件


下一篇:在Solaris下如何在程序中获得当前调用栈信息(函数名等)