Linux内核实现I/O主要在三个内核子系统:虚拟文件系统(VFS),页缓存,和页回写。
虚拟文件系统(有时也叫做virtual file switch)是一种Linux内核的文件操作的抽象机制。它允许内核在无需了解文件系统类型的情况下,使用文件系统函数和操作文件系统数据。VFS实现这种抽象的方法是使用一种通用文件模型,它是所有Linux文件系统的基础。基于函数指针和各种面向对象方法,通用文件模型提供了一种Linux内核文件系统必须遵循的框架。它允许VFS对文件系统发起请求。框架提供了钩子来支持读,建立链接,同步以及其他功能。每种文件系统再使用合适的函数来处理相应操作。
页缓存是一种在内存中保存最近在磁盘文件系统上访问过的数据的方式。相对于现在的处理器速度而言,磁盘访问速度过慢。在内存中保存被请求数据,内核在接下来对相同数据的后续请求可以直接从内存中读取,尽量避免重复磁盘访问。页缓存利用了引用局部性(localityofreference)的一种方法------时间局部性(temporallocality),该方法使刚被访问资源很可能会在不久后再次被访问。由于避免了费时的磁盘访问,内存在第一次访问时缓存数据的开销因而得到补偿页缓存是内核寻找文件系统数据的第一目的地。只有缓存中找不到时内核才会调用存储子系统从磁盘中读取数据。,内核使用缓冲区来延迟写操作。当一个进程发起写请求,数据被拷贝进一个缓冲区,并将该该缓冲区标记为”脏”的,这意味着内存中的拷贝要比磁盘上的新。此时,写请求就可以返回了。如果对同一个数据块有新的写请求,缓冲区就更新为新数据。在该文件其他部分的写请求则开辟新的缓冲区。
页回写是将 那些”脏”缓冲区写入磁盘,将磁盘文件和内存数据同步。有两个条件会触发回写:
1,当空闲内存小于设定的阈值时,脏的缓冲区就会回写到磁盘上,被清理的缓冲区可能会被移除,来释放内存空间;
2,当一个脏的缓冲区寿命超过设定的阈值时,缓冲区被回写至磁盘。以此来避免数据的不确定性。

I/O调度器和I/O性能
磁盘寻址要理解I/O调度器的工作机制,需要先了解一些背景知识。硬盘基于用柱面(cylinders),磁头(heads),和扇区(section)几何寻址方式来获取数据,这种方式也被成为CHS寻址。每个硬盘都是由多个盘片组成,每个盘片包括一个磁盘、一个主轴和一个读写头。

I/O调度器实现两个基本操作:
1,合并(merging)操作是将两个或多个相邻的I/O请求的过程合并为一个。考虑两次请求,一次读取5号块,另一次读取6和7上的数据。这些请求被合并为一个对块5到7的操作。总的I/O吞吐量可能一样,但是I/O的次数减少了一半。
2,排序(sorting)是选取两个操作中相对更重要的一个,并按块号递增的顺序重新安排等待的I/O请求。比如说,I/O操作要求访问块52,109,和7,I/O调度这三个请求以7,52,109的顺序进行排序.如果一个请求现在要访问81,它将被插入到访问52和109的中间。I/O调度器然后按他们在队列中的顺序一次调度:7,然后52,然后81,最后109。

每次读请求必须返回最新的数据。因此,当请求的数据不在页缓存中时,读请求在数据从磁盘读出前一直会阻塞——这可能是一个相当漫长的操作。我们将这种性能损失称为读延迟(read latency)。一个典型的程序可能在短时期有几个I/O请求。因为每个请求都分别进行同步,稍后的请求将依赖于前面请求。当写操作需要在队列中插入多个块时,队列尾部的块读延迟会变得非常严重。这种现象就是著名的writes-starving-reads问题。
I/O 调度器使用一种机制避免”饿死”的发生。最简单的方法就是像2.4内核那样采用Linux电梯调度法。在该方法中,如果队列中有一定数量的旧的请求,则停止插入新的请求。这样整体上可以做到平等对待每个请求,但在读的时候,却增加了读延迟(read latency)。2.6内核丢弃了Linus电梯调度算法,转而使用了几种新的调度器算法。
1,Deadline I/O调度器,是为了解决2.4调度程序及传统的电梯调度算法的问题。Linus电梯算法维护了一个经过排序的I/O等待列表。队列首的I/O请求是下一个被调度的。DeadlineI/O调度器保留了这个队列,为了进一步改进了原来的调度器,增加了两个新的队列:读FIFO队列和写FIFO队列。FIFO 队列中的每个请求都设置一个过期时间。读 FIFO队列的过期时间设置为500毫秒,写队列则为5 秒。
2,Anticipatory I/O调度器,Deadline I/O调度器表现很好,但是并不完美。当面对众多独立的读请求时,问题依然会出现-每个读请求在前一个请求返回后才会执行,当应用程序得到数据,准备运行并提交了下一个读请求时, I/O 调度程序已经去处理其他的请求了。这样导致了每次搜索时都要进行不必要的寻道操作:查找数据,读数据,返回。
AnticipatoryI/O调度器在Deadline I/O调度器中增加了预测机制,当一个读操作被提交,anticipatory I/O 调度器在它的终止期限前调度它。不同于Deadline I/O 调度器的是, anticipatory I/O 调度器会等待6毫秒。如果应用程序在6 毫秒内对硬盘同一部分发出另一次读请求,读请求立刻被响应, anticipatory I/O 调度器继续等待。
3,CFQ I/O调度器,尽管在方法上有所区别,但Complete Fair Queuing(CFQ)I/O调度器和上述调度程序的目标是相同的。使用CFQ时,每个进程都有自己的队列,每个队列分配一个时间片。I/O调度程序使用轮转方式访问并处理队列中的请求,直到队列的时间片耗尽或所有的请求都被处理完。后一种情况,CFQ I/O调度器将会空转一段时间(默认10毫秒),等待当前队列中新的请求。如果预测成功,I/O调度器避免了查找操作。如果预测无效,调度程序转而处理下一个进程的队列。
4,Noop I/O调度器,NoopI/O调度程序是目前最简单的调度器。无论什么情况,它都不进行排序操作,只是简单的合并。它一般用在不需要对请求排队的特殊设备上。

调度器默认的I/O调度器可以在启动时可以通过内核参数iosched来指定。有效的选项有as,cfq,deadline,和noop。也可以在运行时针对每个块设备进行选择,可以通过修改/sys/block/device/queue/scheduler来完成。读这个文件可以知道当前的I/O调度器是什么,把上述有效选项写入这个文件可以更改I/O调度程序。例如,要设置设备hda的I/O调度程序为CFQ , 可以使用如下方式:
#echo cfq >/sys/block/hda/queue/scheduler

因为磁盘I/O相比系统其它部分很慢,同时I/O系统又是现代计算机很重要的一个部分,因此使I/O性能达到最优是非常重要的。减少I/O操作的次数(通过将很多小的操作聚集为一些大的操作),实现块对齐的I/O,或者使用用户空间缓冲,利用高级I/O的优点,如向量I/O,定位I/O和异步I/O,都是系统编程过程中需要经常考虑的重要步骤。为了使I/O请求能以有利于寻址操作的顺序提交,用户空间程序可以做不同的处理。它们可按照以下方式进行排序:
1,完整路径:在大部分文件系统采用的布局算法中,每个目录里的文件, 倾向于在磁盘上相邻分布。
2,inode编号:使用inode排序比路径排序更有效 ,通常情况下,inode的顺序意味着物理块的顺序。
3,文件的物理块:通过文件逻辑块获得物理块,然后再排序。。第一步,确定文件中块的数量。这可以通过stat()调用来完成。其次,对每个逻辑块,我们用ioctl()调用获得与它相关的物理块。