第四章 I/O设备管理¶
I/O 设备¶
关注的:该设备给软件提供了何种编程接口。
包括:
- 接受的控制命令
- 完成的功能
- 返回的出错报告
类型:
- 交互方向:输入设备、输出设备、输入/输出
- 数据组织:块设备(有地址,可以并行)、字符设备(字符流 ,无地址)、网络设备
I/O设备的组成:
- 机械部分——“物理设备本身”
- 电子部分——设备控制器【一组芯片】、适配器【印刷电路卡】
- 完成设备和主机之间的连接和通讯
主机与 IO 设备的通讯¶
设备控制器上会有
-
寄存器
-
数据缓冲区
供 OS 读取。
编址方法:
-
IO独立编址
- 寄存器 → I/O 端口编号【I/O端口地址】
- 用专门的 I/O 指令对端口进行操作
-
内存映像编址
- 寄存器 → 内存地址,专门用于I/O操作(功能上)
-
端口地址空间 \(\subset\) 内存的地址空间统一编址,端口位于内存的顶端。
-
不能缓存设备控制器中的寄存器!
-
混合编址
- 寄存器:独立编址;
- 数据缓冲区:内存映像编址。
检测方式¶
循环检测方式¶
轮询:循环检测设备是否就绪;循环检测设备是否完成。
控制的所有工作均由CPU来完成
中断驱动方式¶
循环检测会造成CPU时间的浪费
思路:
- I/O设备完成任务 ---> 控制器通过总线向中断控制器发出一个信号(1)
- 中断控制器接受信号,把设备编号放在地址线上,并向CPU发出一个中断信号(2)。
- CPU中断,以编号从中断向量表中取出处理程序的起始地址,并在该程序运行后向中断控制器发出确认信号(3)。
流程:
- 用户进程通过系统调用函数来发起I/O操作
- 系统调用函数阻塞用户进程,让其他先用。
- I/O操作完成时,设备向CPU发出中断,然后在中断处理程序中做进一步的处理【内核态,可能是继续IO】。
直接内存访问方式¶
例:读操作
- CPU向设备控制器发出命令,启动读操作;
- 设备控制器控制I/O设备完成此次读操作,并将数据保存在设备控制器内部的数据寄存器或缓冲区中,然后中断CPU;
- 然后把数据读入内存。
在硬件上需要一个DMA控制器。
- 访问系统总线,代替CPU去指挥I/O设备与内存之间的数据传送。
- DMA控制器包含了一些寄存器,可被CPU来读或写。包括:
- 一个内存地址寄存器 ---> 数据存储到哪
- 一个字节计数器 ---> 准备的字节的多少
- 一个或多个控制寄存器(指明了I/O设备的端口地址、数据传送方向、传送单位,以及每一次传送的字节数)
尽量减少 CPU 的参与,释放 CPU 时间
I/O 软件¶
本身的接口?层次关系?
OS 提供给用户程序编写者的接口¶
- 设备独立性:操作不同类型外设时、接口一致
- 统一命名:用简单的字符串或整数的方式来命名一个文件或设备
- 阻塞与非阻塞I/O
- 阻塞:进程被阻塞起来,直到I/O操作完成,同步
- 非阻塞:I/O调用立即返回,异步
OS 提供给底层 I/O 设备的接口¶
OS把各种类型的设备划分为三类,并为每一类定义了一个标准接口
- 块设备
- 字符设备
- 网络设备
I/O软件的层次结构¶
--- 下面是用户自己写的 --- |
---|
用户空间的 I/O 软件 |
--- 下面是 OS 给出的 --- |
设备独立的系统软件 |
--- 下面由设备厂商给出 --- |
设备驱动程序[平台相关] |
--- 下方随硬件一起 --- |
中断处理程序 |
硬件 |
如何进行输入输出?¶
方案1¶
当用户进程需要输入输出服务时,会调用相应的系统调用函数(->sys_read);
该函数又调用相应的设备驱动程序,驱动程序在启动I/O操作后被阻塞(->driver_read);
I/O操作完成后,将产生一个中断,然后中断处理程序将接管CPU,并唤醒被阻塞的驱动程序。
foo_read(),该设备对read接口函数(read 调用 foo_read)的具体实现。 foo_interrupt(),中断处理函数。
size_t foo_read(struct file *filp, char *buf, size_t count, loff_t *ppos)
{
foo_dev_t *foo_dev = filp->private_data;
if(down_interruptible(&foo_dev->sem))//互斥
return -ERESTARTSYS;
foo_dev->intr = 0; //同步
outb(DEV_FOO_READ, DEV_FOO_CONTROL_PORT);
wait_event_interruptible(foo_dev->wait,(foo_dev->intr == 1)); // 被阻塞
if (put_user(foo_dev->data, buf))
return -EFAULT;
up(&foo_dev->sem);
return 1;
}
void foo_interrupt(int irq,void *dev_id, struct pt_regs *regs)
{
foo->data = inb(DEV_FOO_DATA_PORT);
foo->intr = 1;
wake_up_interruptible(&foo->wait);
}
用户进程A → 系统调用(read) → foo_read → A 被阻塞 →
用户进程B → 被中断 → foo_interrupt → A被唤醒
方案2¶
-
块设备驱动程序:上层函数,负责管理请求队列;
-
底层函数,负责与硬件打交道,完成真正的I/O;
I/O请求的提交与真正实现是分离的。
各个用户进程(通过内核)调用上层函数,提交I/O请求(make_request),然后阻塞;底层函数则从队列中取出每个I/O请求,并完成之。
没有什么是加一层解决不了的,如果有,那就加两层
设备独立的I/O软件¶
系统内核的一部分
基本任务:实现所有设备都需要的一些通用的I/O功能,并向用户级软件提供一个统一的接口。
实现的主要功能:
- 给上层应用的统一接口;
- 与设备驱动程序的统一接口;
- 提供与设备无关的数据块大小;
- 缓冲技术;
用户空间的I/O软件¶
库函数:如C语言里与I/O有关的库函数write、read等,它们实质上只是将它们的参数再传递给系统调用函数,并由后者来完成实际的I/O操作;
SPOOLing技术:在多道系统中,一种处理独占设备的方法。
实现:加一层虚拟的程序,提供虚拟 I/O
磁盘¶
磁道:传动装置固定,盘面旋转一周,磁头所能访问的圆环区域
柱面:在所有盘面上,半径相同的所有磁道组成一个柱面
扇区:磁道被划分为若干个扇区
磁盘的访问:以扇区作为最小的寻址和存取单位。
- 首先移动传动装置,通过它来移动从磁头,从而定位正确的柱面
- 选中对应的磁头,想要的扇区正好路过这个磁头正下方的时候,就可以对他进行访问了。
如何读写一个字节?读-修改、写¶
- 读入整个扇区
- 修改该字节
- 写回到磁盘的扇区
磁盘的格式化¶
-
低级格式化
- 标出磁道和扇区,在相邻的扇区之间用狭窄的缝隙隔开
- 相位编码【特定的位组合形式开始,还包括柱面号、扇区号、扇区大小等信息】
- 数据区【512byte】
- 纠错码【冗余信息,一般用来纠正读取错误】
-
高级格式化
- 生成一个引导块、空闲存储管理结构、根目录、空白的文件系统
磁盘调度算法¶
Info
这里会考。概念和算法的过程。
- 柱面的定位时间【占据主要部分】
- 旋转的延迟时间
- 数据的传送时间
基本思路:调整书匈奴,减少柱面定位时间
- 先来先服务算法:
- 优点:简单、公平
- 缺点:效率不高
- 最短定位时间优先
- 优点:性能更好
- 缺点:远处的访问可能一直处于饥饿状态
- 电梯算法
- 从当前位置网一个方向移动,直到前面已经没有任何的访问请求
- 优点:考虑了距离,考虑了方向,克服了以上两者的缺点
关于固态硬盘¶
电学信号要快很多!
但耐用性能较差