IO设备
IO设备
设备控制器
- 控制设备的部件
- 解析主机发来的命令,控制设备进行操作
- 组成
- 与主机的接口:主机与设备间传递命令、状态、数据
- 硬件接口:PCIe SATA USB
- 控制寄存器:1个或多个,控制设备操作
- 写控制寄存器,命令设备执行指定的操作
- 读控制寄存器,获得设备的状态
- 数据缓存区(data buffer)
- 缓冲数据
- 使用主机端和设备内的DRAM缓冲数据
- 如果该缓冲区可用服务读请求,也将其视作数据缓存区
- 与主机的接口:主机与设备间传递命令、状态、数据
寻址
- 控制寄存器和设备数据缓冲区
- I/O端口:I/O地址空间(Port Mapped IO,PMIO)
- 端口号:8位或16位的数值
- I/O专用指令in/out(例如x86):特权指令
- 控制线:指示CPU发出的地址是内存空间还是I/O空间
- 内存地址空间和I/O设备地址空间隔离
- 内存映射I/O(Memory Mapped IO,MMIO)
- 统一地址空间,内存地址空间预留一部分给I/O设备内存和寄存器
- 使用常规的访存指令(例如RISC指令集)
- 内存地址与I/O地址无重叠
- CPU发出的地址,内存模块和所有设备都要解析
- 两者可用结合
- 控制寄存器采用I/O端口寻址
- 数据缓冲区采用内存映射I/O
- 好处
- 编程方便
- 保护方便 & 灵活:利用虚存的保护机制
- 高效:访问数据缓冲区不需要专用指令
- I/O端口:I/O地址空间(Port Mapped IO,PMIO)
- 内核缓冲区
- 为什么需要内核缓冲区
- 生产者(应用)和消费者(IO设备)直接速度不匹配
- 字符设备、块设备速度较慢,适配不同的数据传输速率
- 用作数据读缓冲
- 可以服务对同一数据的读请求
- 减少实际访问设备的I/O请求
- 为什么需要内核缓冲区
数据传输
- 设备数据传输
- 启动设备 + 数据传输
- 启动时间(开销)
- CPU用于启动设备进行操作的时间
- 带宽
- 启动设备后数据传输的速率(Bytes/Sec)
- 延迟
- 传输1字节的时间(启动时间 + 将1字节传输到目的地的时间)
- 通用方法:不同的传输速率
- 字符设备:对字节流传输的抽象,键盘、串口、打印机等
- 字节粒度访问:以若干字节为传输粒度,顺序读写
- 块设备:对块传输的抽象,磁盘、光盘等
- 块粒度访问:以若干块为传输粒度和存储粒度,按块寻址,整块读写,随机读写
- 字符设备:对字节流传输的抽象,键盘、串口、打印机等
数据传输方式
PIO(Programmed IO)
- 例子:RS-232串口
- 简单的串行控制器
- 状态寄存器:就绪、忙、...
- 数据寄存器
- 简单的串行控制器
- 输出数据时
- CPU:
- 等待设备状态变为非"忙"
- 写数据到数据寄存器
- 通知设备"就绪"
- 设备:
- 等待直到状态变为"就绪"
- 消除"就绪"标志,设置"忙"标志
- 从数据寄存器中拿走数据
- 清除"忙"标志
- CPU:
- PIO的轮询(Polling)
- CPU等待直到设备状态变为非"忙"
- 好处:简单
- 坏处:慢且浪费CPU资源
- 改进:使用中断机制可以避免CPU轮询
- 支持中断的设备
- 例如:鼠标
- 简单的鼠标控制器
- 状态寄存器(完成、中断、...)
- 数据寄存器(X、Y、按键)
- 输入数据时
- 鼠标
- 等待直到状态变为"完成"
- 将相应的值保存到数据寄存器
- 发中断
- CPU(中断处理)
- 清除"完成"标志
- 将数据寄存器的值读到内核缓冲区(内存)中
- 置"完成"标志
- 调用调度器
- 鼠标
DMA(Direct Memory Access)
- 基于数据寄存器的不足
- 数据寄存器满后,发送中断请求,CPU进行中断处理
- 传输大量数据时,中断频繁,需要CPU频繁处理中断
- DMA的基本思想
- 以数据块为单位进行传输,由DMA控制器控制完成外设与主机间的数据传输
- DMA需要连续的内核缓冲区
- CPU再数据开始传输前设置DMA控制器
- 数据传输结束后,DMA控制器发送中断给CPU,CPU再进行处理
- 减少占用CPU资源
- 例子:磁盘
- 简单的磁盘控制器
- 状态寄存器(完成、中断、...)
- DMA内存地址和字节数
- DMA控制寄存器:命令、设备、传输模式及粒度
- DMA数据缓冲区
- DMA写
- CPU:
- 等待DMA设备状态为"就绪"
- 清除"就绪"
- 设置DMA命令为write,地址和大小
- 设置"开始"
- 阻塞当前的线程/进程
- 磁盘控制器:
- DMA方式将数据传输到缓冲区(Count--;addr++)
- 当count==0时,发中断
- CPU(中断处理)
- 将被该DMA阻塞的线程/进程加入到就绪队列
- 磁盘
- 将数据从缓冲区写入磁盘
- CPU:
- 简单的磁盘控制器
同步I/O 和 异步I/O
同步I/O
- read()和write()将会阻塞用户进程,直到读写完成
- 在一个进程做同步I/O时,OS调度另一个进程执行
异步I/O
- aio_read()和aio_write()不阻塞用户进程
- 在I/O完成以前,用户进程可以做别的事
- I/O完成将通知用户进程
同步/异步读过程
- 用户进程P1调用read()系统调用
- 系统调用代码检查正确性和缓存
- 如果需要进行I/O,会调用设备驱动程序
- 设备驱动程序为读数据分配一个buffer,并调度I/O请求
- 启动DMA进行读传输
- 阻塞当前进程P1,调度另一个就绪的进程P2 // 异步I/O: 直接跳到第12步
- 设备控制器进行DMA读传输
- 传输完时,设备发送一个中断请求
- 中断处理程序唤醒被阻塞的用户进程P1(将P1加入就绪队列
- 设备驱动检查结果(是否有错误),返回
- 将数据从内核buffer拷贝到用户buffer // 异步I/O: 通知进程P1读操作完成
- read系统调用返回到用户程序
- 用户进程继续执行
设备驱动
- 给操作系统的其它模块提供操作设备的API
- 与设备控制器交互
- 与设备控制器交互以进行数据传输:命令、参数、数据
- 主要功能
- 初始化设备
- 解析OS发来的命令
- 多个请求的调度
- 管理数据传输
- 接收和处理中断
- 维护驱动与内核数据结构的完整性
设备驱动的主要流程
- 准备工作
- 参数检查、请求格式转换
- 设备状态检查:忙 -> 请求入队列
- 开设备或上电时执行
- 操纵设备
- 将控制命令写入设备的控制寄存器
- 检查设备状态:就绪 -> 写下一命令
- 直到设备完成所有命令
- 阻塞等待
- 等待设备完成工作
- 被中断唤醒
- 有的设备不需要等待:如显示器
- 错误处理
- 检查设备状态:错误 -> 重试
- 返回调用者
设备驱动安装
- 静态安装设备驱动
- 将设备驱动直接编译进内核,系统启动后可以直接调用
- 新设备的使用需要重启OS
- 设备驱动修改效率不高,需要重新编译内核
- 动态挂载设备驱动
- 将驱动动态加载进内核空间
- 不需要重启,而是采用间接指针
- 设备入口点表:所有设备的入口点
- 安装入口点,维护相关的数据结构
- 加载 / 删除设备驱动
- 分配内核空间 / 删除内核空间
- 存储驱动代码 / 删除驱动代码
- 与入口点关联 / 删除入口点
设备驱动的利与弊
- 灵活性:
- 用户可以下载和安装设备驱动
- 灵活接入不同硬件设备
- 安全隐患
- 设备驱动运行于内核态
- 有bug的设备驱动会导致内核崩溃,或者引入安全漏洞
- 如何让设备驱动更安全
- 检查设备驱动的代码
- 为设备驱动构建状态机模型进行检查
- 用户态驱动:面向高速设备,提升性能