(莱昂氏unix源代码分析导读-51) 交互式终端

                                                               by cszhao1980

1 DL11/KL11终端

有很多种终端可供选择使用,在一台计算机上可能同时连接若干种不同类型的终端。

在我们的模型里,使用DL11 / KL 11终端。

 

一个DL11/KL11终端拥有4个设备寄存器,分为接收和发送两组,可用如下结构表示:

8016: struct klregs {

8017:     int klrcsr;   //接收状态寄存器

8018:     int klrbuf;   //接收数据缓存寄存器

8019:     int kltcsr;   //发送状态寄存器

8020:     int kltbuf;   //发送数据缓存寄存器

8021: }

 

每组寄存器的设置和用法同LP11十分类似。莱昂对这4个寄存器有比较详细的解释,我就不再啰嗦了。

 

PDP11可以拥有多个终端,我们将其分为三组:

(1)         console

其设备寄存器的起始地址为KLADDR

8008: #define KLADDR 0177560

 

(2)         2

可拥有多个终端。可用于设备寄存器分配的开始地址为KLBASE

8009: #define KLBASE 0176500 /* kl and dl11-a */

 

(3)         3

可拥有多个终端。可用于设备寄存器分配的开始地址为DLBASE

8010: #define DLBASE 0175610 /* dl-e */

 

NKL11用来配置前两组的终端数量,而NDL11用来配置第3组的终端数量。我们的模型的配置如下:

8011: #define NKL11 1

8012: #define NDL11 0

 

由此看来,我们的模型仅配置有一个终端,即consoleconsole的中断矢量地址是060(用于接收寄存

器中断)和064(用于发送寄存器中断)。其中断处理程序分别为klxintpcrint,如下所示:

0525: . = 60^.

0526: klin; br4

0527: klou; br4

 

0560: .globl    _klxint

0561: klou:     jsr     r0,call; _klxint

0563: .globl    _pcrint

0564: pcin:     jsr     r0,call; _pcrint

2 tty结构

无论使用的是哪一种硬件接口,每一个终端端口都会有一个“tty”型结构与其关连:
8015: struct tty kl11[NKL11+NDL11];

 

前面说过,inode中的i_addr[0].major记录的是端口的设备号,而i_addr[0].minor记录的

就是该端口在kl11数组内的index

 

前面说过,共有3种端口,他们拥有统一编订的minor号,consoleminor号为0。通过

minor号还可以计算出该端口的设备寄存器的起始地址:  

8039:     addr = KLADDR + 8*dev.d_minor;

8040:     if(dev.d_minor)

8041:         addr =+ KLBASE-KLADDR-8;

8042:     if(dev.d_minor >= NKL11)

8043:         addr =+ DLBASE-KLBASE-8*NKL11+8;

 

7926: struct tty

7927: {

7928:     struct clist t_rawq;     /* input chars right off device */

7929:     struct clist t_canq;     /* input chars after erase and kill */

7930:     struct clist t_outq;     /* output list to device */

7931:     int t_flags;           /* mode, settable by stty call */

7932:     int *t_addr;          /* device address (register or startup fcn) */

7933:

7934:     char t_delct;          /* number of delimiters in raw q */

7935:     char t_col;           /* printing column of device */

7936:     char t_erase;         /* erase character */

7937:     char t_kill;          /* kill character */

7938:     char t_state;         /* internal state, not visible externally */

7939:

7940:     char t_char;         /* character temporary */

7941:     int t_speeds;        /* output+input line speed */

7942:     int t_dev;           /* device name */

7943: };

 

不出所料,tty结构中有输入、输出缓冲队列。但奇怪的是,它居然有三个队列:

(1)         原始输入队列“t_rawq”;

(2)         “加工后”的输入队列“t_canq”;

(3)         输出队列“t_outq

 

为什么会有两个输入队列呢?

很简单,用户输入的某些字符会具有特殊的意义,需要进行特殊的解释。比如,用户的输入如下:

c a k r “删除键” e

 

显然,用户想输入“cake”,却错误的写成了“cakr”,于是,他就按下删除键删除“r”,然后输入“e”。

用户的原始输入(包括“r”和“删除键”)会直接放置在原始输入队列中。而“加工后”的输入队列

应该存放“cake”。

 

少数进程(如shell)对原始输入感兴趣,它们将从原始队列中读取数据。而对大多数的进程而言,它们

关心的其实是“加工后”的输入数据。

 

tty结构中,以下几个值可以通过stty系统调用设置,也可通过gtty获取:

(1)         int t_speeds

(2)         char t_eras

(3)         char t_kill

(4)         int t_flags;

 

莱昂对stty系统调用有比较详细的介绍,我在此只补充几句:

(1)         就系统调用而言,stty其实有两个参数:

i.       通过r0传入,为该端口的文件描述符;

ii.     通过argu[0]传入,为用户态的一个地址,指向3word的数组,分别为:

第一个word              speed

第二个word的低半个byte  erase

     第二个word的高半个byte  kill

     第三个word               flag

(2)          最终会调用到函数ttystty(atp, av)

         参数1atp):指向该端口的kl11数组;

         参数2v):标志符号, 0 sty调用;

1 gttty调用

 

gtty系统调用与此类似,留给读者自行分析。

3 open和close

首先是klopen,此过程用来初始化一个终端设备:

(1)   根据其minor设备号,将相应的kl11数组项分配给它;

并设置tty的相应变量,如t_statet_flags等等;

 

(2)   如当前进程还没有设置控制终端,则将控制终端设置为此设备(的tty数组项地址);

8030:    tp = &kl11[dev.d_minor];

8031:    if (u.u_procp->p_ttyp == 0) {

8032:    u.u_procp->p_ttyp = tp;

8033:       tp->t_dev = dev;

8034:    }

 

(3)         设置设备的状态寄存器

8051: addr->klrcsr =| IENABLE|DSRDY|RDRENB;    //设置接收状态寄存器

8052: addr->kltcsr =| IENABLE;                                  //设置发送状态寄存器

 

klclose用来“关闭”该设备,其实现非常简单,调用wflushtty,并将ttyt_state0

Wflushtty用来处理输入和输出队列里残存数据:

(1)         对于输出队列,等待输出内容真正输出;

(2)         对于输入队列,调用flushtty进行清空。

 

flushtty用来清空三条队列,莱昂解释的比较清楚,我就不再赘述了。

4 Read过程

 

当用户在终端设备上按键之后,会引发一个接收中断,其处理流程如下:

klin---------klrint---------ttyinput

中断处理程序

 

ttyinput的核心语句只有一句,即 8355: putc(c, &tp->t_rawq);其作用是将设备传入的字符

放入raw队列。还有一点要注意:

8356: if (t_flags&RAW || c=='\n' || c==004) {

8357:    wakeup(&tp->t_rawq);

 

当每一个字符录入后,都需要唤醒对raw感兴趣的进程;而对其他进程来说,收到换行符后,

才需要唤醒他们。

 

函数的其他语句多用于处理特殊的输入,请参考莱昂注释。

 

显然,中断处理程序只是将用户输入放入了raw队列,并没有对其进行加工。对“加工后”的数

据感兴趣的进程会调用klread(可通过cdevsw[]..d_read)读取输入:

klread ------- ttread-----canon

 

canon负责“加工”输入数据,将其放入“加工后”的队列中。而ttread使用getc获取数据,再通过

passc将数据传送至user空间中。

 

cannon函数较长,其主要篇幅都在进行繁琐的工作,莱昂有详细的解析,请参考其注释。

5  Write过程


进程通过cdevsw[]..d_write进行输出:

klwrite------- ttwrite-----ttyoutput

                 ----- ttstart

 

ttwrite的核心语句如下:

8558:    while ((c=cpass())>=0) {

8559:       spl5();

         ……

8565:       spl0();

8566:       ttyoutput(c, tp);

8567:    }

8568:    ttstart(tp);

 

通过cpass取得user空间的输出字符,然后通过ttyoutput将其放入输出队列。

最后,调用ttstart启动输出。

 

ttyoutput篇幅较长,但多数代码都在作特殊字符的处理工作,其核心代码仅有两行:

8477:    if(c)

8478:       putc(c|0200, &rtp->t_outq);

 

ttstart启动设备输出,将一个字符输出出去,其核心代码为:

8520:    if ((c=getc(&tp->t_outq)) >= 0) {

8521:       if (c<=0177)

8522:          addr->tttbuf = c | (partab[c]&0200);

 

output队列取得一个字符,然后放置入设备的输出队列——这其实就启动了真实的设备输出。

而设备发送成功后,会引起一个发送中断,表明自己已经可以接收下一个字符了。其中断处理

程序为klxint

 

8070:    klxint(dev)

8071:    {    register struct tty *tp;

8072:         tp = &kl11[dev.d_minor];

8073:         ttstart(tp);

8074:         if (tp->t_outq.c_cc == 0 || tp->t_outq.c_cc == TTLOWAT)

8075:            wakeup(&tp->t_outq);

8076:    }

 

klxint将再次调用ttstart以接收下一个字符。

如此持续下去,将会完成整个输出。

 

事实上,输出过程还有个分支,即延迟输出情况。在ttstart中,如果发现要输出的字符>0177,则

表示要进行延迟输出,会调用timeout(ttrstrt, …),在 “callout”列表中构造一项,在若干时间之后,

会调用ttrstrt进行输出。

 

博客地址:http://blog.csdn.net/cszhao1980

博客专栏地址:http://blog.csdn.net/column/details/lions-unix.html

 

你可能感兴趣的:((莱昂氏unix源代码分析导读-51) 交互式终端)