聊聊操作系统的I/O

IO设备

IO设备大致可以分为两大类:块设备和字符设备。
其中块设备中的每一个块都可以独立于其他的块进行读写,而且每一个块都有自己的地址,支持随机访问,块设备的传输以一个块或多个连续的块为单位。如硬盘磁盘、U盘。
字符设备以字符为单位发送或接受一个字符流,没有任何块结构,是不可寻址的,也没有任何寻道操作。如网络接口、鼠标。

IO设备一般由机械部件(设备本身)电子部件(设备控制器)组成。
为了屏蔽具体设备的差异,每个设备都对应一个管理他们的硬件设备——设备控制器,设备控制器具有芯片和寄存器,可以与CPU进行通信。CPU通过读写控制器的
寄存器
与控制器进行通信。(CPU通过控制器间接控制IO设备)

控制器的任务就是把串行的位/比特流转换为字节流,并进行必要的错误校正工作。字节块首先达到控制器内部的缓冲区,并且按位组装,然后对校验和进行校验,并证明字节块没有错误后,再将它复制到主存中。

数据离开一个设备之后,通常并不能直接放到最终的目的地。【1】需要放到某个地方进行检查、校验【2】处理数据生产者与数据消费者之间速度不匹配的问题,消除缓冲区填满速度与清空速度之间的相互影响。
一旦磁盘传送开始工作,从磁盘读出的数据就是以固定速率到达的,不论控制器是否准备好接收数据,如果控制器要将数据直接写入内存,则必须为要传送的每个字取得系统总线的控制权,如果总线很慢则必须将数据临时存放在某个位置。

IO通信

每个控制器有多个寄存器与CPU进行通信,主要为以下四种:
【1】数据输入寄存器
【2】数据输出寄存器
【3】状态寄存器
【4】控制寄存器(用于下达命令)

CPU和设备的控制寄存器、数据缓冲区通信主要有两种方案:
【1】I/O端口空间
设备的控制寄存器都被分配了一个I/O端口号(一个整数值),所有的I/O端口组成一个I/O端口空间,这些值只能被操作系统访问。而且需要使用特殊的I/O指令

例: in myRegister 88 这条指令,告诉CPU将读取88对应的控制寄存器的内容,存入CPU的寄存器myRegister中。out 88 myRegister 则是将寄存器myRegister的内容写入88对应的控制寄存器中。

这里,I/O端口空间和内存地址空间是不同的
【2】内存映射IO
将所有的控制寄存器映射到内存空间,每个控制寄存器分配唯一的内存地址,并且不会和内存地址空间的地址重合
CPU读入一个地址时,无论来自内存还是IO端口,它将地址放入总线的地址线,然后在总线的控制线上声明一个read信号,还需要使用第二条信号线表明来自IO空间还是内存空间。这个地址将会落在某一个范围内并由对应的模块进行处理。

I/O端口的方式中,需要使用特殊的I/O指令读写设备控制器,访问寄存器需要使用汇编代码,调用过程中增加了控制I/O的开销。而内存映射I/O中,控制寄存器只是内存中的变量,CPU可以像访问任何内存中的变量一样对控制寄存器进行访问。另一方面,内存映射I/O中,引用内存地址的指令也可以被用于引用控制寄存器的地址,并且执行不同的行为,达到“多态”的效果

内存映射的缺点:与高速缓存存在冲突,如果对设备控制寄存器进行高速缓存,将使得设备将不再被查询,即使设备就绪也无法被发现,因此操作系统必须管理选择性高速缓存

IO控制

设备控制器与CPU之间的I/O控制有三种实现方式

轮询

也称为程序控制I/O,说白了就是CPU指向设备驱动程序的代码,重复读取设备控制器的状态寄存器,直到设备寄存器处于“就绪状态”(准备就绪、读就绪、写就绪)。程序能够动起来那肯定少不了CPU,因此CPU是忙于执行IO程序的。(以字节为单位)

假设要在终端打印字符串ABC,首先需要在用户空间组装“ABC”,然后操作系统将字符缓冲区的内容复制到内核空间的缓冲区中,然后轮询设备控制器的状态位,直到可用为止。一旦可用,以字节为单位复制到数据输入寄存器并等待设备再一次就绪,直到打印输入完毕所有字节。

程序控制I/O十分简单,就是不断在循环中判断是否设备就绪,但是全部I/O完成之前,需要占用CPU的全部时间。(在这期间CPU工作与执行操作系统代码,而不是用户进程的代码)

中断驱动

程序驱动IO是同步进行的,而中断可以用于处理异步事件

当一个I/O设备完成它的任务时(如输出一个字节或一个块),它就会产生一个中断。设备中断控制器之间的连接使用总线上的中断请求线
如果没有其他等待响应的中断,中断控制器将会立刻对中断进行处理,如果有另一个中断正在处理中,或者另一个设备在总线上具有更高优先级的中断线上同时发出中断请求,当前设备的中断请求将被延迟处理。设备会在中断线上发出中断信号,直到得到CPU响应。
为了处理中断,中断控制器在地址线上放置一个数字,表明哪个设备需要被处理,并且发出一个中断CPU的信号。

地址上的数字用于指向中断向量的索引——二维数组**<数字id,中断处理程序在内存中的地址>**,拿到地址后,程序计数器指向相应的中断服务过程的开始位置,并对设备的中断请求作出应答(将一个确定的值写入中断控制器的某个I/O端口,之后中断控制器可以发出另一个中断)。

CPU执行完每当指令时,都会检查中断请求线,当检测到有中断信号,则会停下当前工作并保存CPU上下文,并(修改程序计数器)开始指向中断处理程序。

发出中断时,会导致用户态修改为内核态——CPU修改PSW寄存器、保存CPU上下文(至少需要保存程序计数器、堆栈指针)、栈指针指向内核栈的地址、程序计数器装入系统代码的起始地址。当切出内核态时,很有可能发生进/线程切换,CPU被抢占。

中断驱动使得设备变成就绪状态之前,CPU可以被用于干其他的事情,而当设备准备好接收下一个字节而进入就绪状态时,它将发出中断主动通知CPU。
虽然就绪之前,CPU可以被用于干其他的事情,但是中断是发生在每个字节上的,CPU在内存中搬运字节很快,但是设备总是“一个字节、一个字节去通知CPU”,处理中断的一系列时间仍然是一种开销。

DMA

使用DMA传送时,开始时,CPU向DMA控制器下达命令,然后继续工作,而DMA控制前在没有CPU帮助下,完成数据传输工作,当完成整个传输时,发出中断通知CPU。

用户进程调用read()发出IO请求,请求将磁盘中的数据读入内存缓冲区,随后进入阻塞状态。
CPU向DMA下达命令,要求DMA将磁盘缓冲区的数据拷贝到内核缓冲区(非直接IO),而DMA控制器向磁盘控制器下达IO指令,磁盘控制器将磁盘中的数据读入磁盘控制器的缓冲区,并通知DMA控制器,DMA控制器将数据再拷贝进内核缓冲区,然后发出中断信号,通知任务完成或者缓冲区已满。CPU得知内核缓冲区的数据准备完毕,于是将数据从内核缓冲区拷贝到用户空间,系统调用返回。

DMA控制器的介入,使得CPU仅仅在传送开始时需要下命令,和传输结束后需要访问内核缓冲区。DMA收到磁盘控制器的通知后,将磁盘控制器缓冲区数据拷贝到内核缓冲区,这个阶段不需要CPU介入,直到任务完成则发出中断信号通知CPU
本质上,DMA芯片代替CPU去轮询(程序控制)I/O。
当DMA控制器占用内存总线时,CPU被暂时阻止访问内存,而如果CPU想要访问内存总线则需要等待,这被称为周期窃取——设备控制器偷走了CPU一个临时的总线周期,从而轻微延迟CPU,但是使用DMA控制器能够改进总的性能。

并不是所有的计算机都使用DMA,因为主CPU通常比DMA(搬运字节块)要快得多,但限制因素不再是I/O设备时,(如果CPU没有事情做)让CPU等待DMA控制器完成搬运工作是没有意义的。

综合题目
当键盘敲下一个字母,则键盘控制器将接收来自键盘设备发出的数据,并且存储在缓冲区中,控制器将会在中断线上发出中断信号并等待中断控制器的处理,中断控制器在地址线上放置一个数字,并且向CPU发出中断信号,CPU停止当前工作,陷入内核,保存当前CPU上下文到堆栈中,CPU拿到地址线上的数字并从中断向量取得中断处理程序的地址,修改程序计数器和堆栈指针,开始执行中断处理程序,从控制器的缓冲区中拿到字符并翻译为ASCII字符,放入读缓冲区,显示设备驱动程序定时从读缓冲区中读取数据,并放入写缓冲队列,最终字符被写入显示设备控制器的数据缓冲区中,最终写入显示设备。最终,恢复被中断进程的上下文。

IO层次

IO软件结构可以分为(从上到下):
【1】用户级I/O软件(用户进程)
【2】与设备无关的操作系统软件(内核I/O子系统)
【3】设备驱动程序
【4】中断处理程序
【5】硬件

其中设备控制器是一个硬件,可以看作设备的一个代理,直接与软件打交道,而CPU通过执行对应的设备驱动程序与之打交道,内核IO子结构对接各种设备驱动程序。设备驱动程序为IO子系统提供了统一的设备访问接口,而内核IO子结构统一控制设备。

设备控制器虽然屏蔽了同一种设备的差异,但是各种设备控制器的寄存器和缓冲区仍然具有差异,为了进一步屏蔽差异,引入设备驱动程序(属于操作系统软件的一种)。设备驱动程序屏蔽不同控制器的差异,并且提供统一的接口给操作系统。
设备驱动程序可以响应控制器发来的中断请求,并根据这个中断的类型,调用相应的中断处理程序(设备驱动程序使用中断处理程序提供的服务,设备的中断处理程序在驱动程序初始化的时候注册进操作系统

设备驱动程序可以阻塞自己,直到I/O完成并且产生一个中断,当中断发生时,中断处理程序将对中断进行处理,并解除设备驱动程序的阻塞状态。
每个连接到计算机的I/O设备都需要设备特定的代码来对设备进行控制,这样的代码就是设备控制程序,一般由设备的制造商编写并同设备一起交付。设备驱动程序的一个功能:接收来自其上方的、与设备无关的软件所发出的抽象的读写请求,执行从抽象到具体的转换。(如:磁盘块号 -> 具体的磁头、扇区、磁道和柱面号)

与设备无关的软件的基本功能:执行对所有设备公共的I/O功能,并且向用户层软件提供一个统一的接口

I/O请求生命周期
【1】用户进程请求I/O,进程调用阻塞的系统调用read(),陷入内核。
【2】内核I/O子系统检查调用代码,并从内核缓冲区中查找数据是否存在,存在则将数据复制到用户进程的地址空间,否则继续往下层委托(需要执行物理I/O请求,进程被挂起,移入等待队列),I/O子系统发送请求到设备驱动程序
【3】设备驱动程序分配内核缓冲区,并向设备控制器发送命令(通过写命令寄存器),然后设备驱动程序进入阻塞状态(这里也可能是轮询,行为取决于具体实现)(设备驱动程序启动I/O)。
此时,进程将被阻塞,直到I/O操作完成,此时CPU被让出给别的进程使用。
【4】设备控制器(硬件)监控和管理设备,当设备输入(I/O动作)完成时,产生中断(中断控制器发出中断信号,并且把设备数字放入总线,CPU可以查询中断向量拿到相应的中断处理程序地址)。
【5】CPU收到中断信号,保存CPU上下文,转去执行
中断处理程序
,从发出中断的设备控制器的寄存器中提取信息,向内核的设备驱动程序发出信号,从中断返回
【6】设备驱动程序被唤醒,确定I/O请求是否完成,并向上方的内核I/O子系统发送信号,通知请求已经完成
【7】内核空间的数据被复制到用户空间的缓冲区,进程从等待队列移入就绪队列
【8】当调度程序为进程分配CPU时,该进程从系统调用返回,继续向下执行。

程序想要运行肯定离不开CPU,上面的主语“程序”也可以更换为“CPU”

缓冲

缓冲属于与设备无关软件的功能。
对于无缓冲的情况,每个字节都会引起中断,而且非常频繁,基本属于最原始的中断驱动IO模型,而引入缓冲区后,直到字节填满缓冲区之后才会引发一次中断。
如果这个缓冲仅仅存在于用户空间,那么一个字节到来时,该用户缓冲区很有可能被调出内存,因此我们可以在内核空间创建一个缓冲区,中断处理程序将字节放入内核缓冲区,当该缓冲区被填满时,将包含用户缓冲区的页面调入内存,并在一次操作内将内核缓冲区的内容复制到用户缓冲区中,这大大提升了效率。
更优化的办法,可以使用内核双缓存区或循环缓冲区,防止缓冲区满而导致没有地方放置新到来的字节

当某个程序或者已存在的进程需要某段数据时,它只能在用户空间中属于它自己的内存访问、修改,这段内存称为用户缓冲区。假设需要的数据在磁盘上,那么进程首先得发起系统调用,委托OS去磁盘上加载文件。正常情况下,出于安全和稳定性以防止用户进程破坏内核空间,数据只能加载到内核的缓冲区,统称为内核缓冲区。数据加载到内核缓冲区之后,还需要内陷,内核进程将内核缓冲区的数据拷贝到用户缓冲区上

内核缓冲区 和用户缓冲区之间的复制,是两段内存空间的数据传输,只能由内核进程来完成。所以,加载IO设备数据到内核缓冲区的过程是DMA拷贝方式,而从内核缓冲区到用户缓冲区拷贝数据过程是CPU参与的拷贝方式

内核缓冲区的作用:
相当于磁盘的高速缓冲区,因为相比于直接读写内存,直接读写磁盘控制器的缓冲区太慢了,
先通过DMA将数据搬运到内存,再从内存读取数据,可以通过使用“读内存”替换“读磁盘”加速数据的读写。
根据局部性原理,通常将最近被访问的数据存储在pageCache中,当空间不足时使用LRU淘汰。

缓冲IO:先将数据存入库函数实现的缓冲队列,然后再通过系统调用操作读写文件。减少系统调用次数。
非缓冲IO:直接调用系统调用访问文件。

根据是否利用OS的缓冲区
直接IO:不会发生内核缓存和用户进程之间的数据复制,直接经过文件系统访问磁盘
非直接IO:读操作时,数据从内核缓冲区拷贝给用户进程。写操作时,数据从用户进程拷贝给内核缓存,在由内核决定什么事件写入数据给磁盘。

直接IO应用场景
【1】应用层已经实现了磁盘数据的缓存,不需要内核重复缓存。如mysql的buffer pool已经相当于pageCache。
【2】传输大文件时候,由于大文件难以命中pageCache,而且会占满pageCache导致热点文件被排挤出去,增大性能开销

**绕开内核缓冲区(pageCache)**的IO叫做直接IO,对应磁盘,异步IO只支持直接IO。
内核向磁盘控制器发送读请求,不等待数据就位就返回,进程可以干别的事,磁盘控制器准备好后通知DMA,DMA将数据拷贝到进程缓冲区,并且通知进程。

在高并发的场景,针对大文件的传输,可以使用“异步I/O+直接I/O”替换零拷贝。
传输小文件的时候,使用“零拷贝技术”

零拷贝:没有在内存层面拷贝数据,全程没有通过CPU来搬运数据,所有数据都是通过DMA进行运输的相比传统的文件传输方式,减少了两次CPU上下文切换和数据拷贝次数,而且两次数据拷贝的过程,都是通过DMA完成,不需要CPU介入

内核对内核缓冲区的优化
【1】内核的IO调度算法会缓存尽可能多的IO请求在pageCache,最后合并为一个更大的IO请求,再发送给磁盘,减少磁盘的寻址操作。
【2】内核会预读后续的IO请求放在pageCache,可以减少对磁盘的操作。

os将缓冲区数据写入磁盘的时机
【1】调用write(),发现内核缓冲区的数据过多时,写入磁盘
【2】用户主动调用sync(),将内核缓冲区的数据写入磁盘。
【3】内存紧张,无法分配新的页面,内核缓冲区数据刷入磁盘
【4】内核缓存的数据的缓冲时间超过了某个时间。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值