有很多种终端可供选择使用,在一台计算机上可能同时连接若干种不同类型的终端。
在我们的模型里,使用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
由此看来,我们的模型仅配置有一个终端,即console。console的中断矢量地址是060(用于接收寄存
器中断)和064(用于发送寄存器中断)。其中断处理程序分别为klxint和pcrint,如下所示:
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
无论使用的是哪一种硬件接口,每一个终端端口都会有一个“tty”型结构与其关连:
8015: struct tty kl11[NKL11+NDL11];
前面说过,inode中的i_addr[0].major记录的是端口的设备号,而i_addr[0].minor记录的
就是该端口在kl11数组内的index。
前面说过,共有3种端口,他们拥有统一编订的minor号,console的minor号为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]传入,为用户态的一个地址,指向3个word的数组,分别为:
第一个word: speed;
第二个word的低半个byte: erase;
第二个word的高半个byte: kill;
第三个word: flag
(2) 最终会调用到函数ttystty(atp, av)
参数1(atp):指向该端口的kl11数组;
参数2(v):标志符号, 0: sty调用;
1: gttty调用
gtty系统调用与此类似,留给读者自行分析。
首先是klopen,此过程用来初始化一个终端设备:
(1) 根据其minor设备号,将相应的kl11数组项分配给它;
并设置tty的相应变量,如t_state、t_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,并将tty的t_state清0。
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