【背景】设备管理:理解设备访问机制

在本章涉及的bootloader和ucore都需要对I/O设备进行访问,比如通过串口、并口和CGA显示器显示字符串,读取硬盘数据,处理时钟中断等,已经需要读者用到操作系统的I/O设备管理知识了。为此,我们需要操作系统的设备管理进行一个简要描述。

在计算机系统中,操作系统需要管理各种设备,即给它们发送控制命令、捕获中断、错误处理等;为此专门设置了一个子系统:设备管理子系统来完成这些琐碎的工作。同时设备管理子系统还需要提供一个简单易用的统一接口,并尽可能地使其他内核功能组件或应用可通过这个统一接口访问所有的设备,即实现与设备的无关性。比如在proj1中,bootloader提供了一个显示字符的函数接口cons_putc(位于bootmain.c中),在proj3中的提供了一个显示格式化信息的函数接口cprintf(位于printf.c中),这样操作系统的其他功能组件就可以直接使用这些简单易用的接口来输出信息,而不是通过繁琐的I/O命令与具体的设备打交道。cprintf的实现相对复杂,用到C语言的可变列表参数等,大家只要把它的功能理解为C语言应用库中的printf的简化版即可,并掌握最后是如何通过调用cons_putc函数完成具体的I/O字符输出。

接下来,我们将从操作系统概念的角度对I/O设备组成、控制设备的方式进行阐述,并进一步对实验中所使用的基于Programmed I/O (PIO)方式访问并口、CGA和硬盘进行具体分析。

硬件设备简介

对于硬件设备而言,操作系统所关心的并不是硬件自身的设计,而是如何来对它进行控制,即该硬件所接受的控制命令、所完成的功能,以及所返回的出错。所以在设计操作系统的设备管理子系统时,需要了解计算机系统中I/O总线上连接的I/O控制器(比如PC机中的CGA控制器、串口控制器、并口控制器、时钟控制器8254,中断控制器8259等)。

I/O控制器在物理上包含三个层次:I/O地址空间、I/O接口和设备控制器。每个连接到I/O总线上的设备都有自己的I/O地址空间(即I/O端口),这也是CPU可以直接访问的地址。在 PC机中,支持基于I/O的I/O地址空间(通过IN/OUT这类的I/O访问指令访问),也支持基于内存的I/O地址空间(通过MOV等访存指令访问)。这些I/O访问请求通过I/O总线传递给I/O接口。

I/O接口是处于一组I/O端口和对应的设备控制器之间的一种硬件电路。它将I/O访问请求中的特定值转换成设备所需要的命令和数据;并且检测设备的状态变化,及时将各种状态信息写回到特定I/O地址空间,供操作系统通过I/O访问指令来访问。I/O接口包括键盘接口、图形接口、磁盘接口、总线鼠标、网络接口、括并口、串口、通用串行总线、PCMCIA接口和SCSI接口等。

设备控制器并不是所有I/O设备所必须的,只有少数复杂的设备才需要。它负责解释从I/O接口接收到的高级命令,并将其以适当的方式发送到I/O设备;并且对I/O设备发送的消息进行解释并修改I/O端口的状态寄存器。典型的设备控制器就是磁盘控制器,它将CPU发送过来的读写数据指令转换成底层的磁盘操作。

控制设备的方式

操作系统对硬件设备的控制方式主要与三种:程序循环检测方式(Programmed I/O,简称PIO)、中断驱动方式(Interrupt-driven I/O)、直接内存访问方式(DMA, Direct Memory Access)。

在本章的proj1实验中,bootloader需要显示字符串,就是采用相对简单的PIO方式。PIO方式是一种通过CPU执行I/O端口指令来进行数据读写的数据交换模式,被广泛应用于硬盘、光驱等设备的基础传输模式中。这种I/O访问方式使用CPU I/O端口指令来传送所有的命令、状态和数据,需要处理器全程参与,效率较低,但编程很简单。后面讲到的中断方式和直接内存访问(Direct Memory Access,DMA)方式将更加高效。

对于程序循环检测方式而言,其控制方式体现在执行过程中通过不断地检测I/O设备的当前状态,来控制I/O操作。具体而言,在进行I/O操作之前,要循环地检测设备是否就绪;在I/O操作进行之中,要循环地检测设备是否已完成;在I/O操作完成之后,还要把输入的数据保存到内存(输入操作)。从硬件的角度来说,控制I/O的所有工作均由CPU来完成。所以此方式也称为繁忙等待方式(busy waiting)或轮询方式(polling)。其缺点是在进行I/O操作时,一直占用CPU时间。

中断驱动方式的基本思路是用户任务通过系统调用函数来发起I/O操作。执行系统调用后会阻塞该任务,调度其他的任务使用CPU。在I/O操作完成时,设备向CPU发出中断,然后在中断服务例程中做进一步的处理。在中断驱动方式下,数据的每次读写还是通过CPU来完成。但是当I/O设备在进行数据处理时,CPU不必等待,可以继续执行其他的任务。采用这种方式可提供CPU利用率。编程方面,要考虑异步特性,相对麻烦一些。

使用DMA的控制方式,首先需要有DMA控制器。该控制器可集成在设备控制器中,也可集成在主板上。DMA控制器可以直接去访问系统总线,它能代替CPU指挥I/O设备与内存之间的数据传送,在执行完毕后再通知CPU。这种方式可大大减少CPU的执行开销,适合大数据量的设备数据传送。在编程方面,需要对DMA进行编程和异步中断编程,相对更加复杂一些。

串口(serial port)访问控制

串口是一个字符设备,proj1通过串口输出需要显示的信息。考虑到简单性,在proj1中没有对串口设备进行初始化,通过串口进行输出的过程也很简单:第一步:执行inb指令读取串口的I/O地址(COM1 + COM_LSR)的值,如果发现发现读出的值代表串口忙,则空转一小会(0x84是什么地址???);如果发现发现读出的值代表串口空闲,则执行outb指令把字符写到串口的I/O地址(COM1 + COM_TX),这样就完成了一个字符的串口输出。在proj1的bootmain.c中的serial_putc函数完成了串口输出字符的工作,可参看其函数来了解大致实现。有关串口的硬件细节可参考附录 补充材料。

并口(parallel port)访问控制

并口也是一个字符设备,proj1也通过并口输出需要显示的信息。考虑到简单性,在proj1中没有对并口设备进行初始化,通过并口进行输出的过程也很简单:第一步:执行inb指令读取并口的I/O地址(LPTPORT + 1)的值,如果发现发现读出的值代表并口忙,则空转一小会再读;如果发现发现读出的值代表并口空闲,则执行outb指令把字符写到并口的I/O地址(LPTPORT ),这样就完成了一个字符的并口输出。在proj1的bootmain.c中的lpt_putc函数完成了并口输出字符的工作,可参看其函数来了解大致实现。有关并口的硬件细节可参考附录 补充材料。

CGA字符显示控制

彩色图形适配器(Color Graphics Adapter,CGA)支持7种彩色和文本/图形显示方式,proj1也通过CGA进行信息显示。在80列×25行的文本字符显示方式下,有单色和16色两种显示方式。CGA显示控制器标配有16KB显示内存(占用内存地址范围0xb8000~0xbc000),可以看成是一种内存块设备,即bootloader和操作系统可以直接对显存进行内存访问,从而完成信息显示。在CGA显示控制器中,字符显示内存从线性地址0x000B8000开始,在80列×25行的范围内,共2000字符。每个字符需要两个字节来显示:第一个字节是想要显示的字符 ,第二个字节用来确定前景色和背景色。前景色用低4位(0~3位)来表示,背景色用第4位到第6位来表示。最高位表示这个字符是否闪烁,1表示闪烁,0表示不闪烁。

如果要在屏幕上设置光标,则它须通过CGA显示控制器的I/O端口开控制。显示控制索引寄存器的I/O端口地址为0x3d4;数据寄存器I/O端口地址为0x3d5。CGA显示控制器内部有一系列寄存器可以用来访问其状态。0x3d4和0x3d5两个端口可以用来读写CGA显示控制器的内部寄存器。方法是先向0x3d4端口写入要访问的寄存器编号,再通过0x3d5端口来读写寄存器数据。存放光标位置的寄存器编号为14和15。两个寄存器合起来组成一个16位整数,这个整数就是光标的位置。比如0表示光标在第0行第0列,81表示第1 行第1列(设屏幕共有80列)。

在proj1中没有对CGA显示控制器进行初始化,通过CGA显示控制器进行输出的过程也很简单:首先通过in/out指令获取当前光标位置;然后根据得到的位置计算出显存的地址,直接通过访存指令写内存来完成字符的输出;最后通过in/out指令更新当前光标位置。在proj1的bootmain.c中的cga_putc函数完成了CGA字符方式在某位置输出字符的工作,可参看其函数了解大致实现。

设备管理封装

proj1把上述三种设备进行了一个封装,提供了一个cons_puts函数接口:完成字符串的输出;和一个cons_putc函数接口,完成字符的输出。其他内核功能模块只需调用cons_puts或cons_putc就可完成向上述三个设备进行字符输出的功能。这也就体现了设备管理子系统提供一个简单易用的统一接口的操作系统设计思想。