XV6

设备是以特殊文件的形式,在文件系统中提供访问接口。但是具体的设备读写操作,还需额外的代码来实现。这些与设备相关的操作涉及很多硬件细节,因此比较难以理解。本章将分别对终端设备(含键盘、串口设备)、IDE 设备、定时器和中断控制器进行分析讨论。

终端设备是 shell 程序和其他用户程序进行输入输出的设备,以便和用户进行交互。但是 XV6 出于降低实现难度的目的,并没有以 tty 协议实现终端,而是直接用输入输出功能来实现简易的终端能。唯一的控制台设备,在文件系统中以根目录下的 console 设备文件呈现给用户。XV6 下执行 ls 查看如下:

console  3 18 0

第二列的 3 表示类型是设备,18 表示文件索引节点,0 表示文件大小。

1. 背景知识

consolettyterminalshell

辨识这些术语需要知道 Unix 主机早期的形态,当时的主机有一个面板控制台叫做 console 可以直接操作电脑,也可以通过一个电缆链接到远处的一个终端 terminal,早期终端是电传打字机 teletypewriter(即 tty),后来电传打字机换成了有屏幕的。这些都是具体的设备(当然也包括终端电缆上的传输协议),而 shell 则是软件,但是使用终端作为它的输入输出设备。

控制台强调的是操作面板(类似于管风琴的操控面板的地位),是和 Unix 主机硬件一起的。终端 terminal 强调的是通过线缆连接到远程的设备,在远离主机的、线缆的另一端。如果终端不带显式屏、利用打字机来输出信息的,则是 tty

但是用户通常在 shell 上操作计算机,因此 shellterminalttyconsole 常常相互指代。再加 上很多读者本来就分不清楚它们的区别,就造成了当前混用的情况。即便如此,也没有造成什 么不良后果,因此读者在大多数情况下可以不去仔细辨认它们的区别。

2. 终端

终端 shell 是应用程序与用户之间交互的中介,因此主要负责输入与输出功能。XV6 的终端同时使用 “键盘 + CGA 显示” 和串口。也就是说无论是按键还是串口输入的数据,都将进入到装入的输入缓冲缓冲区中,同理,输出显示也同时送往串口和CGA 显示器上。

回顾 init.cmain() 函数,它打开 console 设备,并将用 dup(0) 执行两次,使得标准输入、标准输出和标准出错文件都指向 console 设备。

2.1 初始化

终端的输入缓冲区的初始化函数是 consoleinit() ,完成的工作有:

  1. 初始化 cons.lock 自旋锁。
  2. 建立文件系统与控制台设备的联系:将 devsw[CONSOLE].write 设置为 consolewrite(); 将控制台设备 devsw[CONSOLE].read 设置为 consolereaddevsw 结构体是两个函数指针,负责根据不同设备指向不同的驱动读写函数。
  3. 使能键盘中断 ,利用 ioapicenable() 实现。

2.2 输入缓冲区

终端的输入缓冲区是 input 结构体,它是一个循环缓冲区,分别有读指针 r、写指针 w 和编辑指针 e。

2.3 中断处理

控制台的中断处理其实是键盘中断和串口中断的延续,那两个中断的处理函数 kbdintr()uartinitr() 都将调用 consoleintr(),将输入的字符写入到终端输入缓冲区 input.buf[] 中。

2.4 读写操作

控制台的写操作(输出显示)将直接送往 CGA 显卡或串口,读操作则是间接地从输入缓冲区 input.buf[] 中读入,因此可能因 input.buf[] 为空而阻塞睡眠。input.buf[] 中的数据是通过键盘中断或串口中断而读入的。

consoleread()

consoleread() 是从输入缓冲区 input.buf[] 中读入数据,如果未能读入到指定数量的字符,则调用 sleep() 进入睡眠(例如 shell 进程在等待用户输入命令时)。consoleread() 是输入缓冲区的唯一 “消费者”,它通过 cons.lock 自旋锁与该缓冲区的 “生产者”(键盘中断代码和串口中断代码)实现互斥。

consolewrite()

向控制台设备(文件类型为 3)文件进行写操作,可以实现输出显示。该文件操作实际指向 consolewrite() 函数,它将根据输出缓冲区内容逐个字节输出到 CGA 显卡和串口终端上,通过 uartputc()cgaputc()

QEMU 为 XV6 仿真的是 CGA 彩色字符模式显卡,cgaputc() 是在 CGA 显卡上显示字符。CGA 显卡有一个输出显示缓冲区,往里面写入数据即可显示对应的字符。该显示缓冲区定义为 *crt ,它对应于 CGA 设备使用的 0xb8000 地址物理区间,因此需要通过 P2V() 转换成虚地址。缓冲区中使用 2 个字节来表示一个要显示的字符,屏幕共有 25 line * 80 coum,共有 25*80*2 字节。每个字符的 2 个字节中的高 8 位显示色彩和属性(例如闪烁),低 8 位是字符的 ASCII 码。

因此 cgaputc() 主要任务就是处理光标位置 pos 的计算、将指定的字符写入到 crt[pos] 位置即可完成显示任务,其中 crt[pos] 的低 8 位是 ASCII,高 8 位是显示属性(0x07 表示黑白色)。对于回车符,需要跳到下一行的行首;对于退格键 BACKSPACE 需要将光标左移一格;必要时还需要滚屏。XV6 的 shell 代码中并没有处理 up、down、left、right 方向键,因此 shell 命令修改只能通过 BACKSPACE 退格键。

由于光标不属于 “显示” 内容,无法在显示缓冲区中表示,因此单独控制,通过 CGA 的 CRTC 控制寄存器来设置。光标位置表示为 col + 80*row,需要两个字节的空间。这两个字节保存到 CRTC 的 14 号和 15 号寄存器上,首先用 outb(CRTPORT, 14)outb(CRTPORT, 15) 指出要操作的字节,然后用读、写 CRTPORT + 1 端口即可读取或设置光标位置。