目录
3.3.1 低级格式化(Low-levle Formatting)
1.I/O软件原理
1.1 I/O软件的目的
设备独立性:
-
程序可以访问任何I/O设备,而无需事先执行指定的设备(如从软盘、磁盘、CD-ROM读写而无需修改程序)
-
I/O软件应该抽象设备细节
统一命名:
-
我们执行文件或设备的方式应该以相同的方式完成
-
文件或目录的名称可以是字符串或整数,不依赖于设备
-
同一类型的不同设备名称相似
-
-
在UNIX中,所有I/O都与文件系统集成
错误处理:
-
尽可能在靠近硬件的地方处理错误
-
只有当底层无法处理错误时,才向上传播
-
尽可能隐藏错误——许多硬件错误是瞬态的
同步与异步传输:
-
大多数I/O硬件都是异步操作的
-
同步的读写在应用级层面,同步读写更容易编程
-
一些非常高性能的应用程序需要控制I/O的所有细节,所以操作系统需要使异步I/O对它们可用
缓冲:缓冲是为了应对数据流的生产者和消费者之间的速度不匹配或适应具有不同数据传输大小的设备
可共享与专用设备:
-
磁盘是可共享的
-
磁带机是专用的,可能会引入各种问题,如死锁
-
操作系统必须能够处理共享和专用设备:使专用设备看起来可共享,如打印机
1.2 I/O操作
编程I/O:
-
CPU总是忙于IO直到IO完成
-
适用于单进程系统
-
MS-DOS
-
嵌入式系统
-
-
但对于多编程和分时系统来说不是一个好方法
-
-
编程IO操作:
-
将数据从用户空间拷贝到内核空间
-
OS进入一个循环,轮询是否准备好接受更多的数据
-
一旦设备准备好接受更多的数据,操作系统将数据复制到设备寄存器
-
一旦所有的数据被复制,控制返回给用户程序
-
中断驱动I/O:
-
优点:
-
它比编程IO快,效率也更高
-
-
缺点:
-
更难实现
-
2.I/O软件层
2.1 大纲
层次结构:
-
中断处理程序(Interrupt Handlers):处理硬件中断并执行相应的中断服务程序,负责响应设备的状态变化和数据传输完成事件
-
设备驱动程序(Device Drivers):提供具体硬件设备通信的接口,负责管理设备的操作和数据传输
-
设备独立 I/O(Device-Independent I/O):提供通用的I/O接口和功能,使得应用程序可以以统一的方式访问不同类型的设备,而不必关心设备的具体细节
-
用户级I/O软件:提供给用户程序的接口,通过系统调用实现对设备的读取、写入和控制操作
抽象、封装和分层:任何复杂的软件工程问题,都可以通过层次化结构,从而使得上下层互不影响
2.2 中断处理
概述:
-
可以在任何时间执行
-
中断处理程序最好能隐藏
-
中断处理程序完成工作后,解除启动它的程序的阻塞
中断处理程序:
-
中断(异步,外部进程)基本上使用与异常和陷阱相同的机制(同步,内部进程)
-
当中断发生时,CPU保存少量状态并跳转到内存中固定地址的中断处理程序,中断处理程序的位置由中断向量确定
中断处理的步骤:
-
保存硬件中断机制尚未保存的寄存器
-
(可选)为中断服务过程设置上下文,例如TLB,MMU,页表
-
通常,处理程序在当前运行的进程的上下文中运行
-
没有昂贵的上下文切换
-
-
为中断服务程序设置堆栈:Handler通常运行在当前进程的内核栈堆上
-
确认/屏蔽中断控制器,重新启用其他中断
-
运行中断服务程序
-
在设备级中确认中断
-
找到导致中断的原因
-
收到网络数据包,磁盘读取完成,UART发送队列为空
-
如果需要,它会通知阻塞的设备驱动程序
-
-
在某些情况下,会唤醒一个优先级更高的阻塞线程
-
选择新唤醒的线程调度下一个
-
为下一步运行的进程设置MMU上下文
-
-
加载原始进程寄存器
-
开始运行新进程
2.3 设备驱动
概述:
-
设备驱动时管理设备控制器和操作系统之间交互的特定模块
-
设备驱动程序将设备独立请求转换为设备相关的请求
-
设备独立请求:指那些与具体硬件设备无关的I/O操作请求,它们在OS的高层抽象中处理,目的是提供一种统一的接口,使应用程序能够不关心底层硬件中的具体实现细节,特点:
-
统一接口:无论使读写硬盘、打印机还是网络设备,应用程序通过统一的系统调用接口进行操作
-
抽象层次高:这些请求在操作系统的文件系统或I/O管理层处理,而不设计具体设备的硬件细节
-
例子:
read(file_descriptor, buffer, count); write(file_descriptor, buffer, count);
-
-
设备相关请求:指那些与具体硬件设备密切相关的I/O操作请求,它们直接与硬件设备驱动交互,处理具体设备的控制和数据传输。这些请求涉及硬件涉笔的底层细节,包括寄存器操作、中断处理、DMA等。特点:
-
特定于设备:这些请求的格式和内容取决于具体的硬件设备
-
底层操作:它们直接与设备控制器交互,进行具体的I/O操作
-
例子:
void send_command_to_disk_controller(command); void read_sector_from_disk_controller(sector, buffer); void write_sector_to_disk_controller(sector, buffer);
-
-
通常由设备制造商编写
-
一个驱动程序可以为多个操作系统编写,一个驱动程序也可以处理一种类型的设备
-
驱动程序一般时内核的一部分
-
Unix:使用设备驱动程序编译的内核
-
Linux:使用可加载模块,是在系统运行时加载到内核中的代码块
-
Windows:在启动和即插即用时动态加载到系统中
-
驱动程序的功能:
-
接受设备独立请求,并将其转换为具体的设备相关请求
-
调度请求:优化排队请求顺序,以获得最佳设备利用率(例如磁臂)
-
根据需要对设备进行初始化
-
管理数据传输
-
维护驱动和内核数据结构的完整性
-
电源需求管理和设备事件日志
设备驱动程序的典型代码组织:
-
检查从上面输入参数的有效性:如果有效,翻译成具体的命令。例如,将块号转换为磁头,磁道和扇区在磁盘的几何
-
检查设备当前是否在使用中:如果是,排队请求;如果没有,可以启动设备、预热、初始化
-
向控制器发出适当命令顺序
-
如果需要等待,则阻塞
-
当设备完成操作并发出中断时,驱动程序会检查操作是否有错误,并将数据传递回上层
-
处理下一个请求
驱动程序的通信:
-
操作系统和驱动程序:在OS和驱动之间传输命令和数据
-
驱动程序和硬件:在驱动和硬件之间传输命令和数据
驱动程序类型:
-
块设备驱动程序:固定大小的块数据传输
-
字符设备驱动程序:可变大小的块数据传输
-
终端设备驱动程序:带有终端控制的字符设备驱动程序
-
网络涉设备驱动程序:网络流
驱动程序接口:
-
大多数操作系统都为设备驱动程序定义了标准接口
-
块设备接口:
-
read(deviceNumber, deivceAddr, bufferAddr):从设备地址传输数据块到缓存地址
-
write(deivceNumber, deviceAddr, bufferAddr):从缓存地址传输数据到设备地址
-
seek(deviceNumber, deviceAddress):将头部移动到当前位置
-
-
字符设备接口:
-
read(deviceNumber, bufferAddr, size):从字节流设备读取size字节到缓存地址
-
write(deviceNumber, bufferAddr, size):将size字节数据从缓存地址写入字节流设备
-
其他:
-
驱动程序代码必须是可重入的,因为当一个进程已经阻塞在驱动程序中时,它们可以被另一个进程调用
-
可重入:可以被多个线程同时执行的代码
-
驱动程序代码需要处理可插拔设备
-
驱动程序不能直接进行系统调用,但可以调用一些内核提供的过程
-
例如:内存管理单元,定时器,DMA控制器,中断控制器等
-
2.4 设备独立I/O软件
概述:
-
不同的操作系统和设备之间,设备驱动程序与设备独立软件的职责分界线有所不同
-
设备独立I/O软件的功能
-
统一的设备驱动接口
-
缓冲
-
错误报告
-
分配和释放专用设备
-
提供与设备无关的块大小
-
统一接口:
-
设备驱动程序和操作系统部分之间的接口
-
内核代码的统一设备接口
-
允许不同的设备以相同的方式使用
-
允许内部更改设备驱动程序,而不用担心破环内核代码
-
-
设备代码的统一内核接口
-
驱动程序使用一个定义的接口到内核服务(例如安装IRQ处理等)
-
允许内核在不破坏现有驱动程序的情况下进化
-
-
这两种接口一起使用,避免了实现新接口的大量编程工作,采用了常见的设计模式中的适配器模式,具体可以看这篇软件分析设计与建模(UML)期末复习笔记_软件动态行为分析和设计-CSDN博客,搜索”适配器“
-
I/O设备是如何命名的
-
设备独立软件将符号设备名映射到正确的驱动
-
在UNIX中,设备被建模成特殊的文件
-
它们通过使用系统调用来访问,例如open(), read(), write(). close(). ioctl()等
-
每个设备对应一个文件名
-
-
例子:
-
主设备号定位相应的驱动程序
-
次设备号(以i-node排序)作为参数传递给驱动程序,以指定要读取或写入的单元
-
-
缓冲:
-
在设备和应用程序之间传输数据时,内核空间存储数据的内存区域
-
处理生产者和消费者之间的速度不匹配,例如,调制解调器比磁盘慢千倍
-
不同数据传输规模的业务之间的适应,例如网络数据包的分片和重组
-
没有缓冲:
-
进程必须一次读/写一个字节
-
每个单独的系统调用增加了显著的开销
-
进程必须等待每个I/O的完成
-
阻塞/中断/唤醒增加了开销
-
许多短时间运行的进程效率地下
-
-
用户级缓冲:
-
进程指定一个内存缓冲区,传入的数据被放置在该缓冲区中,直到它被填满为止
-
填充数据的过程可以通过中断服务程序完成
-
只有一个系统调用,每个数据缓冲区阻塞/唤醒
-
-
问题:如果缓冲区被分页到磁盘会发生什么?
-
在缓冲区分页入内存时可能会丢失数据
-
可以锁定内存中的缓冲区,但是许多进程进行I/O会减少可以用于分页的RAM。而RAM是有限的资源,会导致死锁
-
-
-
单缓冲:操作系统为I/O请求在主存中分配一个缓冲区
-
双缓冲:
-
使用两个系统缓冲区,而不是一个
-
当操作系统清空或填充另一个缓冲区时,进程可以将数据从一个缓冲区的数据传输到另一个缓冲区
-
-
循环缓冲区:
-
使用了两个以上的缓冲区
-
每个单独的缓冲区是一个循环缓冲区的一个单元
-
当I/O操作需要与进程同步时使用
-
错误报告:
-
部分错误由设备控制器处理:如校验,使用冗余位重新纠正块
-
部分由设备驱动程序实现:如无法读取磁盘块,请重新执行从磁盘中读取块的操作
-
部分由设备独立软件层实现:
-
重试I/O,向用户报告错误代码
-
程序编程错误:例如尝试去写一个只读设备
-
I/O错误:例如摄像机已经关闭,因此我们无法访问,将此错误提示返回给调用的应用程序
-
分配和释放专用设备:
-
禁止对CD-RWs等专用设备进行并发访问
-
要求进程直接对设备的特殊文件执行open():如果open()失败,进程会重试
-
具有请求和释放专用设别的特殊机制:获取不可用设备会阻塞调用者
设备独立块大小:
-
有不同类型的磁盘驱动器,每个扇区可能有不同的物理扇区大小
-
文件系统在将文件映射到逻辑磁盘块时使用块大小
-
该层可以隐藏不同磁盘的物理扇区大小,并可以为更高层(如文件系统)提供固定和统一的磁盘块大小
2.5 用户级I/O软件
概述:
-
提供I/O函数实现的I/O库,这些函数反过来调用相应的I/O系统调用
-
这些库还实现了格式化的I/O函数,如print()和scanf()
-
有些程序不直接写入I/O设备,而是写入假脱机程序
3.磁盘(Disk)
3.1 磁盘硬件
概述:
-
由一个个cylinder组成:
-
每个cylinder包含的磁道数量等于磁盘驱动器中磁头的数量
-
每个磁道分为若干扇区
-
扇区大小一般为512字节
-
-
-
一个cylinder:
-
磁道是一整个圆轨道
-
扇区是磁道上的一个扇区部分
-
硬盘类型有IDE、SCSI、光纤通信和SATA
-
硬盘:
分区位记录:
-
外磁道要比内磁道长
-
两种选项:
-
Bits are "bigger":在外磁道上使用更大的位密度
-
More bits:在外磁道上放置更多的扇区,以实现更高的数据传输
-
-
现代硬盘驱动器使用第二种选择
-
磁盘被划分位一个一个分区
-
分区中每个磁道有固定的扇区数
-
磁盘上有8-20个分区
-
磁盘虚拟几何结构:
-
为了隐藏每个磁道有多少扇区的细节,大多数现代操作系统都有一个呈现给操作系统的虚拟几何结构
-
(x cylinders, y 磁头数, z 扇区数)
-
控制器将(x, y, z)的请求重新映射到实际的cylinder,磁头(磁头可以指定磁道)和扇区
指定数据块位置的方法:
-
CHS
-
CHS寻址是通过磁道中的位置来识别磁盘上单个扇区的过程,其中磁道由磁头和cylinder编号决定
-
为硬盘驱动器上的每个物理数据块提供地址的一种早期方法
-
CHS不能很好地映射到硬盘意外的设备(如磁带和网络存储),并且通常不用于它们
-
-
LBA
-
LBA是一种特别简单的线性寻址方案
-
在LBA寻址方案中,扇区用整数索引编号,第一块为LAB=0,第二个LBA=1,以此类推
-
3.2 磁盘格式化(Disk Formatting)
3.3.1 低级格式化(Low-levle Formatting)
概述:
-
低级格式化通常由供应商完成
-
扇区具有以下的格式:
柱面偏移(Cylinder Skew):
-
当读取顺序块时,寻道时间可能导致丢失下一个磁道中的块0
-
可以使用柱面偏移来格式化磁盘以避免这种情况
-
定义:在不同的点开始处理柱面,以便磁头有时间跳到下一个柱面
-
Example:
交织(Interleaving):
-
如果我们在磁盘控制器中有一个扇区缓冲区,在我们将一个扇区数据从硬盘传输到控制器缓冲区后:
-
将控制器缓冲区的数据复制到内存
-
在这段时间内,磁头通过了一些扇区
-
因此,下一个逻辑扇区不应该是磁头的下一个物理扇区,应该是有一些交错的
-
-
无交叉:
-
单交叉(更常见的):在读取之间跳过一个扇区
-
双交叉:在读取之间跳过两个扇区
-
现代驱动程序通过简单地将整个磁道(或一部分)写入磁盘控制器并缓存它来客服交错类型问题
磁盘分区:
-
每个分区就像一个单独的磁盘
-
扇区0为MBR(CHS(0, 0, 1))
-
引导代码(Bootstrap code):446 字节
-
从分区表中查找活动分区
-
从该分区读取引导扇区
-
-
主分区表(Master partition table):64字节
-
分区表中有分区的扇区和大小
-
-
引导签名(Boot Signature):2字节
-
BIOS用于判断所选启动驱动器是否实际可启动
-
-
-
开机时会发生:
-
BIOS加载MBR,引导程序检查是否有活动分区
-
从该分区读取引导扇区,然后加载操作系统内核等
-
3.3.2 高级格式化
概述:
-
对每个分区执行
-
指定引导块、空闲列表、根目录、空文件系统
3.3 磁盘Arm调度算法
3.3.1 磁盘队列
-
每个磁盘都有一个等待访问磁盘的队列
-
write job
-
read job
-
-
队列中的entry包含以下内容:
-
指向要读/写的内存位置
-
要访问的sector number
-
指向队列中下一个job的指针
-
-
示例:
3.3.2 磁盘Arm调度算法
读取一个sector需要多长时间:
-
定位时间(Postioning time) + 传输时间(Transfer time)
-
定位时间:
-
寻道时间(Seek time)
-
移动arm到正确的cylinder
-
-
旋转延迟(Rotational Delay)
-
等待直到正确的sector在磁头下
-
-
-
实际传输数据的时间
假设磁盘负载很重:
-
有很多对磁盘块的请求到达硬盘驱动程序
-
驱动程序将对请求进行排队
-
驱动程序直到每个块请求应该存储在磁盘的哪个位置(the cylinder number)
-
-
由于在cylinders之间移动arm是expensive的,应该尽量减少寻道时间
磁盘驱动程序:
-
保存请求表
-
表格按cylinder number索引
-
对同一cylinder的请求放在同一个链表中
目标: 最小化定位时间
-
由操作系统和磁盘本身执行
调度算法:
-
FCFS(First Come First Served)
-
优点:公平
-
缺点:搜索成本高,轮换次数很多
-
-
SSF(Shortest Seek First)
-
优点:减少寻道时间
-
缺点:不公平
-
-
Elevator(电梯)算法 (SCAN)
-
优点:对请求更公平,性能和SSF相似
-
-
C-SCAN
-
提供比SCAN更统一的等待时间
-
3.3.2.1 FCFS
先来的先访问,直接看例子:
3.2.2.2 SSF
-
满足从当前位置寻道最短的要求
-
按上面的示例,该算法按照以下顺序满足请求
-
12 9 16 1 34 36
-
Totoal cost = 61
-
-
问题:最小化响应时间和公平性的目标在这里是冲突的
3.2.2.3 电梯算法
-
步骤:
-
向一个方向移动,直到该方向的所有请求得到满足
-
然后改变方向
-
向新方向移动,直到改方向上的所有请求都被满足
-
重复1,2,3步骤
-
-
按上面的示例,该算法按照以下顺序满足请求
-
12 16 34 36 9 1
-
Total cost = 60
-
-
优点:表现良好且公平
-
应该有一个bit表示方向,电梯也应该知道它是向上还是向下(从小到大排序,往大的是向上)
3.2.2.4 C-SCAN
Circular SCAN
-
提供比SCAN更统一的等待时间
-
磁头从磁盘一端移动到另一端
-
满足经过的请求
-
当它到达另一端时,立即返回磁盘的开头
-
回程不会满足任何请求
-
-
-
将圆柱体作为循环列表处理
-
从最后一个cylinder移动到第一个cylinder
-
-
示例:
3.3.3 选择策略
-
SSF很常见
-
SCAN和C-SCAN在磁盘负载较大的系统中表现更好
-
性能取决于请求的数量和类型
-
磁盘服务请求受文件分配方式的影响
3.4 错误处理
硬盘缺陷导致坏块的出现:
-
如果缺陷较小,修复ECC即可
-
否则,让整个扇区坏掉,在控制器或操作系统中处理
处理坏扇区的两种方法:
-
在控制器中处理它们
-
作磁盘测试在出厂前
-
标记坏扇区并用备用扇区替换
-
-
在操作系统中处理它们:操作系统检查坏扇区并记录