1. 系统架构
之所以使用分层,这是由于成本和效率之间的平衡
2. 标准设备
接口:向系统其他部分展现的硬件接口
内部结构:设备相关特定实现,几个芯片,CPU和通用内存等
3. 标准协议
While (STATUS == BYSY)
;
a、轮询设备,反复读取寄存器,判断设备是否就绪
b、操作系统下发数据到数据寄存器,比如将磁盘块(4KB)传递给设备,如果在这过程有CPU参与数据移动,我们称为编程的I/O(programmed I/O)
c、操作系统将命令写入命令寄存器,设备知道数据已经准备好了,它应该开始执行命令
d、操作系统再次通过不断轮询设备,等待并判断设备是否执行完命令
4. 利用中断减少CPU开销
标准协议中轮询会占用CPU时间,为了提高CPU利用率,可以使用中断来减少CPU开销。
CPU不再需要不断轮询设备,而是向设备发出一个请求,然后就可以让对应进程睡眠,切换执行其他任务。当设备完成了自身操作,会抛出一个硬件中断,引发CPU跳转执行操作系统预先定义好的中断服务程序(Interrupt Service Routine,ISR)或者更为简单的中断处理程序(interrupt handler)。
中断处理程序是一小段操作系统代码,它会结束之前的请求并且唤醒等待I/O的进程继续执行。
需要注意的是,使用中断并非总是最佳方案,假如一个非常高性能的设备,它处理请求很快,通常在CPU第一次轮询时就可以返回结果,此时如果使用中断,反而使系统变慢:切换到其他进程,处理中断,再切换回之前的进程代价不小。
如果设备的速度未知,或者时慢时快,可以考虑使用混合策略,先尝试轮询一小段时间,如果设备没有完成操作,再使用中断。
另外一个最好不要使用中断的场景是网络,网络端收到大量数据包,如果每一个包都发生一次中断,那么有可能导致操作系统发生活锁(即不断处理中断而无法处理用户层的请求)
另一个基于中断的优化是合并,设备在抛出中断之前往往会等待一小段时间,在此期间,其他请求如果很快完成,多次中断可以合并为一个中断,从而降低处理中断的代价。
5. 利用DMA进行更高效的数据传输
DMA引擎是系统中的一个特殊设备,它可以协调完成内存和设备间的数据传递,不需要CPU介入。
DMA的工作过程如下:为了能够将数据传递给设备,操作系统会通过编程告诉DMA引擎数据所在内存位置,要拷贝的大小以及要拷贝到哪个设备,在此之后,操作系统可以去处理其他请求了,当DMA到任务完成后,DMA控制器会抛出一个中断来告诉操作系统自己已经完成数据传输。
6. 设备交互的方法
与设备通信的方法有
- 用明确的I/O指令,比如x86上的io和out指令,但是这些指令是特权指令,操作系统是唯一可以直接与设备交互的实体
- 内存映射I/O(memory-mapped I/O)。通过这种方式,硬件将设备寄存器作为内存地址提供。当需要访问设备寄存器时,操作系统装载或者存入到该内存地址,然后硬件会将装载或者存入转移到设备上,而不是物理内存,内存映射到好处是不需要引入新的指令来完成设备交互
7. 纳入操作系统:设备驱动程序
在操作系统最底层,一部分软件清楚地知道设备如何工作,我们将这部分软件称为设备驱动程序,所有设备交互的细节都封装在其中。
Linux文件系统栈
通过抽象隐藏底层细节,这种封装也有不足的地方,例如如果有一个设备可以提供很多特殊的功能,但是为了兼容大多数操作系统它不得不提供一个通用的接口,这样就使用自身特殊功能无法使用。
驱动程序在整个内核代码中占比很大,Linux中约占70%
8. 案例研究:简单的IDE磁盘驱动程序
IDE硬盘暴露给操作系统的接口比较简单,包括4种类型的寄存器,即控制、命令块、状态和错误
以下是与设备交互的简单协议:
- 等待驱动就绪
- 向命令寄存器写入参数
- 开启I/O
- 数据传输(针对写要求)
- 中断处理
- 错误处理
协议通过4个主要函数实现
- ide_rw(),它将一个请求加入队列(如果前面还有请求没有完成)或者直接将请求发送到磁盘(通过ide_start_request()),调用进程进入睡眠状态,等待请求处理完成
- ide_start_request(),它会将请求发送到磁盘,此时x86点in或out指令会被调用,以读取或者写入设备寄存器。
- ide_wait_ready,在发起请求之前,开始请求函数会使用第三个函数ide_wait_ready来确保驱动处于就绪状态
- ide_intr,当中断发生时被调用,它从设备中读取数据,并且在结束后唤醒等待的进程,如果此时在队列中还有别的未处理的请求,则调用ide_start_request接着处理下一个I/O请求
9. 小结
中断和DMA用于提高设备效率
访问设备寄存器的两种方式:I/O指令和内存映射I/O
设备驱动程序