跳转至

第四章 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

磁盘

磁道:传动装置固定,盘面旋转一周,磁头所能访问的圆环区域

柱面:在所有盘面上,半径相同的所有磁道组成一个柱面

扇区:磁道被划分为若干个扇区

磁盘的访问:以扇区作为最小的寻址和存取单位。

  1. 首先移动传动装置,通过它来移动从磁头,从而定位正确的柱面
  2. 选中对应的磁头,想要的扇区正好路过这个磁头正下方的时候,就可以对他进行访问了。

如何读写一个字节?读-修改、写

  • 读入整个扇区
  • 修改该字节
  • 写回到磁盘的扇区

磁盘的格式化

  1. 低级格式化

    1. 标出磁道和扇区,在相邻的扇区之间用狭窄的缝隙隔开
    2. 相位编码【特定的位组合形式开始,还包括柱面号、扇区号、扇区大小等信息】
    3. 数据区【512byte】
    4. 纠错码【冗余信息,一般用来纠正读取错误】
  2. 高级格式化

    1. 生成一个引导块、空闲存储管理结构、根目录、空白的文件系统

磁盘调度算法

Info

这里会考。概念和算法的过程。

  1. 柱面的定位时间【占据主要部分】
  2. 旋转的延迟时间
  3. 数据的传送时间

基本思路:调整书匈奴,减少柱面定位时间

  1. 先来先服务算法:
    1. 优点:简单、公平
    2. 缺点:效率不高
  2. 最短定位时间优先
    1. 优点:性能更好
    2. 缺点:远处的访问可能一直处于饥饿状态
  3. 电梯算法
    1. 从当前位置网一个方向移动,直到前面已经没有任何的访问请求
    2. 优点:考虑了距离,考虑了方向,克服了以上两者的缺点

关于固态硬盘

电学信号要快很多!

但耐用性能较差