Modern Operating System Note(I) —— I/O

prologue

  这篇文章是关于《现代操作系统》的一些笔记,主要记录一些自己觉得比较有用的知识点。整体上可能是按照书中的顺序来写的,也有可能自己稍微再整合一下。主要目的还是加深自己的印象,把知识真正地读进脑子里。
  整个章节大致按照如下的顺序,首先介绍IO的一些概念,其次简单了解一下IO硬件和软件的一些原则,然后再深入了解IO软件的结构,并理解他们之间是如何进行协作的。再接下来,较详细的了解一些具体的IO设备的工作原理。

what is I/O

  什么是I/O?这是一个十分广泛的概念。简单来说,I/O就是Input/Output,即输入输出。为什么说它重要呢?在冯诺伊曼架构中,把计算机分成了存储器,控制单元,运算单元和输入输出这几个部分。换句话来书,我们可以把计算机看成是一个黑匣子,而输入输出就相当于它和外界进行信息交互的通道!对于一个计算机来说,我们希望在我们给它一定的输入后,它在经过计算后给予正确的输出。这些都涉及到了I/O。
  同时,I/O既可以指字面意义上的输入和输出,也可以指一些I/O设备,像鼠标,键盘,磁盘,显示器等。

I/O hardware

  由于I/O本身十分复杂,而且不同的I/O设备的架构区别也较大,在本章节中,我们主要关心的是可编程的I/O,即我们不会太去在意I/O内部究竟是怎样工作的。

I/O device

  I/O设备可以被分成两种类型。第一种是block devices,第二种是character devices。
  如同字面上的意思,块设备内部信息的存储是按块分类的,往往内部块的大小是规定固定的,在写入和读取的时候都是以块为单位进行。像我们常见的磁盘,USB设备等都是属于block devices。
  character devices则不同,它在输入和输出的时候是按照字节来的,即是以字节流的形式进行输入和输出操作。由于这个特性,这种设备的信息往往是不可编址的,像是打印机,网络接口都可以看成是character devices。
  当然,事实上两者之间的界限并不严格,有一些设备是很难进行划定的,这样的分类方式只是便于我们去理解这些设备而已。

Device Controllers

  I/O设备往往可以分成机械组件和电子组件,其中,电子组件被称为I/O控制器或者适配器,通常需要插入在主板的(PCIe)拓展槽中。控制器和设备本身之间的接口是非常低层次的,比如磁盘在传输的时候,往往是传递一整个字节流,以一个前导码为开始,中间是4096字节的信息(一个扇区),最后是一个检验和,或者是错误纠正码(ECC)。控制器在确认信息没有问题之后,或者有问题但是成功纠正过后,就可以把信息复制到主存当中了。

Memory-Mapped I/O

  控制器往往都有一些寄存器用于和CPU的交互,通过将信息写入寄存器当中,我们可以执行一些指令,比如读取或者写入某些信息等,这些是控制寄存器。很多设备还有一些数据的缓冲区可以供操作系统进行读写操作。CPU和这些设备之间的交互有两种形式:
  第一种是使用I/O端口号,有了这些端口后,我们就可以使用一些特别的指令进行操作了,比如

IN REG,PORT
OUT PORT,REG

  分别是PORT上的信息写入到寄存器REG当中,和把寄存器REG中的数据存储编号为PORT的I/O设备的控制寄存器中。
  第二种是使用内存映射。将所有的控制寄存器以及缓冲的数据都映射到内存空间当中,这样的话就可以通过对内存进行读写操作实现和IO设备的交互。这个时候操作系统就必须在内存空间中留出足够大的地方用于和I/O设备之间的映射。
  这两种做法都有各自的优缺点。使用内存映射的方式进行I/O的话,我们就不需要显式地访问I/O设备的控制寄存器,只需要像访问内存中的其他位置一样读取和写入数据即可。并且,如果要访问I/O设备的话,由于C/C++没有直接的可以访问的函数,我们只能在C代码中嵌入汇编代码,这就增加了编码的复杂性。
  其次,内存映射I/O简化了进行I/O操作处理时的一些保护机制。如果各个I/O设备位于内存空间中不同的页,我们可以直接给某个进程中包含I/O设备的页一定的权限,它就可以进行I/O操作了,而这对其他进程而言是未知的。同时,这样的处理方式也使得设备驱动器可以放在不同的内存空间当中,既降低了内核的大小,也避免了驱动器之间的相互干扰。
  第三,内存映射I/O使得每一个对内存的引用也可以变成对I/O设备控制寄存器的引用。如果没有这样的操作的话,在检查是否有I/O信号时,我们需要从I/O设备的控制寄存器中读取信息,移动到CPU上,再检查寄存器上的信息是否满足要求,这就增加了指令的数量,有时也会影响到了性能。
  当然,事物都是具有两面性的,内存映射I/O也带来了不少的问题。首先,就是缓存。我们在引用内存的时候,会在CPU中进行缓存,而对于I/O设备来说,它们的状态信息是不应该被缓存的。比如我们把I/O设备现在是空闲的信息缓存在CPU中,那么接下来所有的访问该I/O设备的结果都是空闲的,这会导致严重的后果。因此,为了避免这样的现象发生,在硬件层次上就必须要有一定的操作使得某些内存空间不能被缓存在CPU上,这同样也增加了设计的复杂度。

Direct Memory Access

  不管CPU有没有使用内存映射I/O,它总是需要对设备控制器进行编址以进行数据的交换。这个时候,CPU虽然可以从I/O控制器中一次获取一个字节的数据,但这明显太慢了。因此很多情况下采用了另外一种方案——DMA。在有DMA控制器的硬件上,操作系统往往必须使用它。DMA控制器往往有自己独立的系统总线,它还包括几个用于和CPU交互的寄存器,有自己独立的字节计数器,运输单元等。
  书中给了这样一个图以直观地看出DMA的作用。
I-O
  首先我们考虑一下没有DMA的情况下磁盘是如何读取数据的。磁盘读到一块数据后,放进自己的缓冲区中,在检查没问题后,磁盘控制器触发中断。操作系统收到后,在一个循环里一次读取一个字节的信息,然后存储在主存中。在有DMA的情况下,CPU首先对DMA进行编程,告诉它需要运输哪些东西到哪里。然后,DMA向磁盘控制器发出请求。磁盘控制器在确认信息无误后,将信息存储在主存中,并向DMA返回一个Ack进行确认。当所有信息传递完毕后,DMA向CPU发出中断。这个时候对操作系统来说,信息已经自动地从磁盘运输到内存当中了。

Interrupts Revisited

  另外,硬件的中断的产生是需要一个叫控制器的东西的管理的。当I/O设备完成工作后,它向总线中发出一个信号,这个信号会被中断控制器检测到,以此决定接下来说什么。
  如果没有其他中断在等待的话,这个时候中断控制器马上处理对应的中断,否则,该中断需要进行等待。处理中断时,控制器将一个数字放进指定的位置,以告诉CPU中断的产生源。CPU进行一定的终端处理后,返回Ack给中断控制器,此后中断控制器可以再次组织中断。

precise and imprecise Interrupts

  中断产生时,需要对对应的中断进行处理。但我们知道,CPU内部是流水线结构的,即在同一个时刻,可能有多个指令在进行处理,不同的指令的处理进度不一定相同,这个时候如果触发了中断,则需要对已进入流水线但是还未处理完成的指令进行处理。其中,一种方法就是等待到当前执行的所有指令都执行完后再触发中断,这就是precise Interrupts。另外一种处理方法就是直接停下,但是需要在栈中记录每一个指令的处理进度。两种方法都各有优劣。等待的话有时会导致终端无法及时处理,而如果直接停下,又涉及到指令的执行状态的保存问题,这会极大程度上加大编码的复杂性。在x86系列中,采用的是两种混合的模式。   

I/O software

  I/O软件层次较为复杂。其中一个关键概念就是设备的独立性。我们需要能写出一个通用 达成需能够同时访问任意的I/O设备,而不需要对命令进行更改。比如输入sort < input > output,程序就应该能读取一个文件作为输入,并输出到另一个文件中,而不管文件是位于磁盘,USB或者是其他I/O设备中。
  另外一个重点的问题是错误的处理。有一些错误可能只是暂时的,比如磁盘上沾了一点灰尘,可能再次进行读取就好,而有些错误则需要对应的处理措施。
  还有另外一个重要的问题是同步与异步。以及缓存等

three fundamental way

  I/O有三种基本的执行方式。包括programmed I/O, interrupt-driven I/O和I/O using DMA

Programmed I/O

  第一种重要的I/O方式是程序化的I/O。比如我们要打印“ABCD”这个字符串时,操作系统首先将字符串拷贝到内核空间中,然后向I/O设备发出请求,当可以访问时(I/O设备往往有对应的状态寄存器),系统将”A”拷贝到设备中,然后等待,继续再将”B”拷贝到设备中,直至所有的字节拷贝完成。这种方式每次只输出一个字符,CPU不断地询问设备,确认设备是否可以进行下一个字节的传输。这种方式又被成为轮询(polling)或忙等待(busy waiting)。这种方式很占用CPU时间。当如果CPU经常处于空闲状态的话,这种方式也是一个可行的选择。

1
2
3
4
5
6
copy_from_user(buffer, p, count); // p 是数据的首指针
for (i = 0; i < count; i++) {
while (*printer_status_reg != READY);
*printer_data_register = p[i];
}
return_to_user()

Interrupt-Driven I/O

  另外一种处理的方式是中断驱动。即每次拷贝信息后,判断信息是否处理完毕,未完毕则进入阻塞或进行其他代码的执行,直到收到中断。这是一种经典的异步编程方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
copy_from_user(buffer, p, count);
enable_interrupts()
while (*printer_status_reg != READY);
*printer_data_register = p[0];
scheduler();
//
if (count == 0) {
unlock_user()
} else {
*printer_data_register = p[i];
count = count - 1;
i = i + 1;
acknowledge_interrupt();
return_from_interrupt();
}

使用DMA进行I/O操作

  使用中断驱动的I/O有个缺陷,就是当打印的速度较快时,常常会陷入中断状态,这也是很占用CPU时间的。因此,另一种做法是使用DMA。像在硬件层次一样,DMA负责将信息全部发送到printer,CPU就可以做自己的事情了。DMA中使用忙等待模式,当处理完成后,触发中断通知CPU即可。

1
2
3
4
5
6
7
8
// CPU操作
copy_from_user(buffer, p, count);
set_up_DMA_controller();
scheduler();
// 中断处理操作
acknowledge_interrupt();
unblock_user();
return_from_interrupt();

I/O sofrware layers

  I/O软件可以组织成4个层次,如下图。每一个层次都能提供完善的接口给相邻的层次使用。

I/O sofrware layers
User-level I/O software
Device-independent operating system software
Device driver
Interrupt handlers
Hardware

Interrupt handlers

  中断处理是I/O操作中不可或缺的部分。但是,处理一个中断的过程是十分复杂的。简单来说,进程会先进行阻塞(比如使用信号量),直到收到中断后,取消阻塞状态。在中断处理时,可能包括但不限于以下操作:

  1. 保存寄存器数据
  2. 重新设置上下文,更新TLB,MMU和页表和栈,用于中断处理
  3. 发送Ack给中断控制器
  4. 复制寄存器的信息
  5. 运行中断处理函数
  6. 选择接下来运行的进程,并进行上下文的设置(TLB,MMU,页表等)
  7. 读取新的进程的寄存器数据
  8. 运行新的进程

Device Driver

  设备驱动器,顾名思义是用于设备的驱动的。由于不同的设备之间的区别是很大的。比如鼠标的驱动器需要接受鼠标的信息,得到鼠标移动了多远,点击了哪个按钮等。而磁盘的驱动器需要知道磁道,扇区,柱面,磁臂等信息。因此,每一个I/O设备需要一个和设备高度相关的驱动器与之关联,这样才便于我们对设备的使用。
device_driver

Device-Independent I/O

  尽管有些I/O软件是设备相关的,有部分的设备需要设备无关。不过这两种类型的边界并不确定,和具体的系统有关。
统一的设备驱动器接口:
  对于相似的I/O设备,如果每一种设备对上层的接口都不相同的话,那么势必会造成严重的混乱,因此,对于比如SATA disk driver, USB disk driver, SCSI disk driver这几种类型,我们应该使用一个标准的接口,这样更便于进行设备的管理和使用。
缓存:
  很明显,每次都读取一小点信息到用户空间中是效率很低的,因此需要一定的缓存。我们可以在内核空间中开辟一段内存,然后设备获取到的信息直接复制到该内存区域。当内存满时,在复制给用户即可。但是,这样又有问题。如果在字符到来的时候,对应的缓存已满,那么就会发生一些问题。这个时候可以采用双缓冲。即一个缓冲在进行消息从内核空间到用户空间的复制的同时,另一个缓冲在进行从I/O设备到用户空间的复制。也可以采用另一种方式,环形缓冲,只需要保存头尾指针即可。

summary

epilogue

  这篇笔记主要都还是按照书中的内容写的,基本上写的都是自己觉得比较重要一点的知识点,但感觉还是很乱,可能是操作系统这块本身就比较复杂,很多知识点理解还是不太到位,感觉总是有一些矛盾的地方,希望在后面能逐渐解决。

------------- The artical is over Thanks for your reading -------------