设备是以特殊文件的形式,在文件系统中提供访问接口。但是具体的设备读写操作,还需额外的代码来实现。这些与设备相关的操作涉及很多硬件细节,因此比较难以理解。本章将分别对终端设备(含键盘、串口设备)、IDE 设备、定时器和中断控制器进行分析讨论。
终端设备是 shell 程序和其他用户程序进行输入输出的设备,以便和用户进行交互。但是 XV6 出于降低实现难度的目的,并没有以 tty
协议实现终端,而是直接用输入输出功能来实现简易的终端能。唯一的控制台设备,在文件系统中以根目录下的 console
设备文件呈现给用户。XV6 下执行 ls
查看如下:
console 3 18 0
第二列的 3 表示类型是设备,18 表示文件索引节点,0 表示文件大小。
console
、tty
、terminal
、shell
辨识这些术语需要知道 Unix 主机早期的形态,当时的主机有一个面板控制台叫做 console
可以直接操作电脑,也可以通过一个电缆链接到远处的一个终端 terminal
,早期终端是电传打字机 teletypewriter
(即 tty
),后来电传打字机换成了有屏幕的。这些都是具体的设备(当然也包括终端电缆上的传输协议),而 shell
则是软件,但是使用终端作为它的输入输出设备。
控制台强调的是操作面板(类似于管风琴的操控面板的地位),是和 Unix 主机硬件一起的。终端 terminal
强调的是通过线缆连接到远程的设备,在远离主机的、线缆的另一端。如果终端不带显式屏、利用打字机来输出信息的,则是 tty
。
但是用户通常在 shell
上操作计算机,因此 shell
、terminal
、tty
、console
常常相互指代。再加 上很多读者本来就分不清楚它们的区别,就造成了当前混用的情况。即便如此,也没有造成什 么不良后果,因此读者在大多数情况下可以不去仔细辨认它们的区别。
终端 shell 是应用程序与用户之间交互的中介,因此主要负责输入与输出功能。XV6 的终端同时使用 “键盘 + CGA 显示” 和串口。也就是说无论是按键还是串口输入的数据,都将进入到装入的输入缓冲缓冲区中,同理,输出显示也同时送往串口和CGA 显示器上。
回顾 init.c
的 main() 函数,它打开 console
设备,并将用 dup(0)
执行两次,使得标准输入、标准输出和标准出错文件都指向 console
设备。
终端的输入缓冲区的初始化函数是 consoleinit() ,完成的工作有:
cons.lock
自旋锁。devsw[CONSOLE].write
设置为 consolewrite(); 将控制台设备 devsw[CONSOLE].read
设置为 consoleread;devsw 结构体是两个函数指针,负责根据不同设备指向不同的驱动读写函数。终端的输入缓冲区是 input 结构体,它是一个循环缓冲区,分别有读指针 r、写指针 w 和编辑指针 e。
控制台的中断处理其实是键盘中断和串口中断的延续,那两个中断的处理函数 kbdintr() 和 uartinitr() 都将调用 consoleintr(),将输入的字符写入到终端输入缓冲区 input.buf[]
中。
控制台的写操作(输出显示)将直接送往 CGA 显卡或串口,读操作则是间接地从输入缓冲区 input.buf[]
中读入,因此可能因 input.buf[]
为空而阻塞睡眠。input.buf[]
中的数据是通过键盘中断或串口中断而读入的。
consoleread()
是从输入缓冲区 input.buf[]
中读入数据,如果未能读入到指定数量的字符,则调用 sleep()
进入睡眠(例如 shell 进程在等待用户输入命令时)。consoleread()
是输入缓冲区的唯一 “消费者”,它通过 cons.lock
自旋锁与该缓冲区的 “生产者”(键盘中断代码和串口中断代码)实现互斥。
向控制台设备(文件类型为 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
端口即可读取或设置光标位置。