文件系统是对存储设备上的文件,进行组织管理的一种机制。而linux在各种文件系统实现上,又抽象了一层虚拟文件系统VFS,它定义了一组,所有文件系统都支持的,数据结构和标准接口。 这样,对应用程序来说,只需要跟VFS提供的统一接口交互,而不需要关注文件系统的具体实现;对具体的文件系统来说,只需要按照VFS的标准,就可以无缝支持各种应用程序。 VFS内部又通过目录项,索引节点,逻辑块以及超级块等数据结构,来管理文件。
。目录项,记录了文件的名字,以及文件与其他目录项之间的目录关系。
。索引节点,记录了文件的元数据
。逻辑块,是由连续磁盘扇区构成的最小读写单元,用来存储文件系统
。超级块,用来记录文件系统整体的状态,如索引节点和逻辑块的使用情况等。
其中,目录项是一个内存缓存;而超级块,索引节点和逻辑块,都是存储在磁盘中的持久数据。
磁盘
磁盘是可以持久存储的设备,根据存储介质的不同,常见磁盘可以分为两类:机械磁盘和固态磁盘
第一类,机械磁盘,也称为硬盘驱动器,通常缩写为HDD。机械磁盘主要有盘片和读写磁头组成,数据就存储在盘片的环状磁道中。在读写数据前,需要移动读写磁头,定位到数据所在的磁道,然后才能访问数据。
显然,如果I/O请求刚好连续,那就不需要磁道寻址,自然可以获得最佳性能。这其实就是连续I/O的工作原理。与之相应,当然就是随机I/O,它需要不停地移动磁头,来定位数据位置,所以读写速度就会比较慢。
第二类,固态磁盘,通常缩写为SSD,有固态电子元器件组成。固态磁盘不需要磁道寻址,所以,不管是连续I/O还是随机I/O的性能,都比机械磁盘要好得多。
其实,无论机械磁盘,还是固态磁盘,相同磁盘的随机I/O都要比连续I/O慢很多,原因也很明显。
。对机械硬盘来说,由于随机I/O需要更多的磁头寻址和盘片旋转,它的性能自然要比连续I/O慢
。而对固态硬盘来说,虽然它的随机性能比机械硬盘好很多,但同样存在“先擦除再写入”的限制。随机读写会导致大量的垃圾回收,所以相对应的,随机I/O的性能比起连续I/O来,也还是差了很多。
。此外,连续I/O还可以通过预读的方式,来减少I/O请求的次数,这也是其性能优异的一个原因。很多性能优化的方案,也都会从这个角度出发,来优化I/O性能。
此外,机械磁盘和固态磁盘还分别有一个最小的读写单位。
。机械硬盘的最小读写单位是扇区,一般大小为512字节
。而固态磁盘的最小读写单位是页,通常大小是4KB,8KB等。
如果每次都读写512字节这么小的单位的话,效率很低。所以,文件系统会把连续的扇区或页,组成逻辑块,然后以逻辑块作为最小单元来管理数据。常见的逻辑块的大小是4KB,也就是说,连续8个扇区,或者单独的一个页,都可以组成一个逻辑块。
除了可以按照存储介质来分类,另一个常见的分类方法,是按照接口来分类,比如可以把硬盘分为IDE,SCSI,SAS,SATA,FC等
不同的接口,往往分配不同的设备名称。比如,IDE设备会分配一个hd前缀的设备名,SCSI和SATA设备会分配一个sd前缀的设备名。如果是多块同类型的磁盘,就会按照a,b,c等的字母顺序来编号。
除了磁盘本身的分类外,当你把磁盘接入服务器后,按照不同的使用方式,有可以把他们划分为多种不同的架构。
最简单的,就是直接作为独立磁盘设备来使用。这些磁盘,往往还会根据需要,划分为不同的逻辑分区,每个分区再用数字编号。比如我们前面多次用到的/dev/sda,还可以分成两个分区/dev/sda1和/dev/sda2
另一个比较常用的架构,是把多块磁盘组成一个逻辑磁盘,构成冗余独立磁盘阵列,也就是RAID,从而可以提高数据访问的性能,并且增强数据存储的可靠性。
最后一种架构,是把这些磁盘组合成一个网络存储集群,再通过NFS,SMB,ISCSI等网络存储协议,暴露给服务器使用
其实在linux中,磁盘实际上是作为一个块设备来管理的,也就是以块为单位读写数据,并且支持随机读写。每个块设备都会被赋予两个设备号,分别是主,次设备号。主设备号用在驱动程序中,用来区分设备类型;而次设备号则是用来给多个同类设备编号。
通用块层
为了减小不同块设备的差异带来的影响,linux通过一个统一的通用块层,来管理各种不同的块设备。 通用块层,其实是处在文件系统和磁盘驱动中间的一个块设备抽象层。它主要有两个功能。
。第一个功能跟虚拟文件系统的功能类似。向上,为文件系统和应用程序,提供访问块设备的标准接口;向下,把各种异构的磁盘设备抽象为统一的块设备,并提供统一框架来管理这些设备的驱动程序。
。第二个功能,通用块层还会给文件系统和应用程序发来的I/O请求排队,并通过重新排序,请求合并等方式,提高磁盘读写的效率。
其中,对I/O请求排序的过程,也就是I/O调度。事实上,linux内核支持四种I/O调度算法,分别是NONE,NOOP,CFQ以及DeadLine
第一种NONE,更确切来说,并不能算I/O调度算法,因为它完全不使用任何I/O调度器,对文件系统和应用程序的I/O其实不做任何处理,常用在虚拟机中(此时磁盘I/O调用完全有物理机负责)
第二种NOOP,是最简单的一个I/O调度算法。它实际上是一个先入先出的队列,只做一些最基本的请求合并,常用于SSD磁盘
第三种CFQ,也被称为完全公平调度器,是现在很多发行版的默认I/O调度器,它为每个进程维护了一个I/O调度队列。并按照时间片来均匀分布每个进程的I/O请求 类似于进程CPU调度,CFQ还支持进程I/O的优先级调度,所以它适用于运用大量进程的系统,像是桌面环境,多媒体应用等。
最后一种DeadLine调度算法,分别为读,写请求创建了不同的I/O队列,可以提高机械磁盘的吞吐量,并确保达到最终期限的请求被优先处理。DeadLine调度算法,多用在I/O压力比较重的场景,比如数据库等。
I/O栈
可以把linux存储系统的I/O栈,由上到下分为三个层次,分别是文件系统层,通用块层和设备层。这三个I/O层的关系如下图所示,这其实也是linux存储系统的I/O栈全景图。

根据这种I/O栈的全景图,可以更清楚地理解,存储系统I/O的工作原理。
。文件系统层,包括虚拟文件系统和其他各种文件系统的具体实现。它为上层的应用程序,提供标准的文件访问接口;对下会通过调用块层,来存储和管理磁盘数据。
。调用块层,包扣块设备I/O队列和I/O调度器。它会对文件系统的I/O请求进行排队,再通过重新排序和请求合并,然后才要发送给下一级的设备层。
。设备层,包扣存储设备和相应的驱动程序,负责最终物理设备的I/O操作。
存储系统的I/O,通常是整个系统中最慢的一环。所以,linux通过多种缓存机制来优化I/O效率。
比方说,为了优化文件访问的性能,会使用页缓存,索引节点缓存,目录项缓存等多种缓存机制,以减少对下层块设备的调用。
同样,为了优化块设备的访问效率,会使用缓冲区,来缓存块设备的数据。
通用块层是linux磁盘I/O的核心。向上,它为文件系统和应用程序,提供访问了块设备的标准接口;向下,把各种异构的磁盘设备,抽象为统一的块设备,并会对文件系统和应用程序发来的I/O请求进行重新排序,请求合并等,提高了磁盘访问的效率。
磁盘性能指标
磁盘性能的衡量标准,必须要提到这5个常见指标,也就是我们经常用到的,使用率,饱和度,IOPS,吞吐量以及响应时间等。这5个指标,是衡量磁盘性能的基本指标。
。使用率,是指磁盘处理I/O的时间百分比。过高的使用率(比如超过80%),通常意味着磁盘I/O存在性能瓶颈。
。饱和度,是指磁盘处理I/O的繁忙程度。过高的饱和度,意味着磁盘存在严重的性能瓶颈。当饱和度为100%时,磁盘无法接受新的I/O请求。
。IOPS是指每秒的I/O请求数。
。吞吐量,是指每秒的I/O请求大小
。响应时间,是指I/O请求从发出到收到响应的间隔时间
要注意的是,使用率只考虑有没有I/O,而不考虑I/O的大小。换句话说,当使用率是100%的时候,磁盘依然有可能接受新的I/O请求。
在数据库,大量小文件等这类随机读写比较多的场景中,IOPS更能反映系统的整体性能;而在多媒体等顺序读写较多的场景中,吞吐量才更能反映系统的整体性能。
一般来说,我们在为应用程序的服务器选型时,要先对磁盘的I/O性能进行基准测试,以便可以准确评估,磁盘性能是否可以满足应用程序的需求。
阻塞io,非阻塞io与同步,异步io的区别
根据应用程序是否阻塞自身运行,可以把I/O分为阻塞I/O和非阻塞I/O。
.所谓阻塞I/O,是指应用程序在执行I/O操作后,如果没有获得响应,就会阻塞当前线程,不能执行其他任务。
.所谓非阻塞I/O,是指应用程序在执行I/O操作后,不会阻塞当前的线程,可以继续执行其他的任务
根据I/O响应的通知方式的不同,可以把文件I/O分为同步I/O和异步I/O。
.所谓同步I/O,是指收到I/O请求后,系统不会立刻响应应用程;等到处理完成,系统才会通过系统调用的方式,告诉应用程序I/O结果
.所谓异步I/O,是指收到I/O请求后,系统会先告诉应用程序I/O请求已经收到,随后再去处理异步处理;等处理完成后,系统再通过事件通知的方式,告诉应用程序结果。
阻塞/非阻塞和同步/异步,其实就是两个不同角度的I/O划分方式。他们描述的对象也不同,阻塞/非阻塞针对的是I/O调用者(即应用程序),而同步/异步针对的是I/O执行者(即系统)
比如在LinuxI/O调用中,
.系统调用read是同步读,所以,在没有得到磁盘数据前,read不会响应应用程序
.而aio_read是异步读,系统收到AIO读请求后不等处理就返回了,而具体的read结果,再通过回调异步通知应用程序
再比如,在网络套接字的接口中,
.使用send()直接向套接字发送数据时,如果套接字没有设置O_NONBLOCK标识,那么send()操作就会一直阻塞,当前线程也没法去做其他事情。
.如果用了epoll,系统会告诉你这个套接字的状态,那就可以用非阻塞的方式使用。当这个套接字不可写的时候,你可以去做其他事情,比如读写其他套接字