0 缘起
为什么想要分享I/O
先说为什么想要分享一下磁盘I/O相关的内容。我们都知道,计算机是由CPU、内存和各种I/O设备组成的,其中CPU和内存的运行速度是很快的,纳秒级和微秒级,I/O设备的运行速度是很慢的,是毫秒级的。如果程序逻辑设计的有些冗余,只会对CPU、内存产生一些浪费,但是由于他们的运行速度很快,通常不会构成系统性能的瓶颈,但是I/O不一样,它本身速度很慢,如果设计的不好,就会容易导致系统性能产生瓶颈,因此,I/O相当于木桶理论中最短的板,水桶能装多少水(性能达到多少),往往由I/O设计的好坏决定,既然I/O如此重要,希望将它总结分享出来。
本文主旨脉络
本文会分成三大part进行介绍,第一part会介绍磁盘相关的内容,磁盘是数据存储的基础,它提供了基于扇区的数据读写能力,如果没有磁盘,就谈不上文件系统,更谈不上I/O了,所以,磁盘是数据组织和管理的物理基础。但是光有磁盘是无法完成数据的组织和管理的,因为,用户对数据使用的最小颗粒度是文件,而文件除了能够存储数据,还能提供更加复杂的功能,比如按照目录结构的数据组织方式、文件的权限控制、硬链接、软链接等,为了支持这些复杂的文件管理功能,就需要有文件系统,因此,什么是文件系统呢?文件系统是基于磁盘基础之上,为数据的访问和管理提供更加复杂功能的一个产物,这是对一个对文件系统定性的理解方式。有了文件系统,我们就可以开始对数据进行访问了,访问的过程中,需要CPU、内存等其他硬件的共同支持和配合,完成一次数据访问的过程,就叫做一次I/O。综上,磁盘是数据存取的物理基础,文件系统是基于磁盘提供文件组织管理功能,I/O是结合其他硬件完成一次文件访问的整个过程,这就是对磁盘、文件系统、I/O的定性理解。本文接下来也会按照这个思路进行内容组织。
1 磁盘物理结构和运行原理
1.1 磁盘的物理结构
磁盘的物理结构如下图所示:
1-1 磁盘的物理结构
一个磁盘有多个盘面组成,一个盘面的上下两个面都是可以进行数据读写的,每个面都有一个读写磁头(Head),每个盘面是由很多半径不同的同心圆组成的,每个同心圆是一个磁道(Track),不同盘面的同一磁道组成一个柱面(Cylinder),每个磁道又可以划分为多个扇区(Sector),每个扇区能存储512字节数据,因此,磁盘容量 = C * H * S * 512 byte。
磁盘的最小单位是扇区,计算磁盘的容量等价于计算磁盘中扇区的数量。
仔细分析一下,其实柱面的数量和磁道的数量是相等的,为什么磁盘容量的计算没有用磁道数,而是用柱面数?很多资料上没有说明这个原因,但我的理解是:这跟磁盘的读写顺序是有关的,磁盘是以柱面为单位进行数据读写的(后面会详细说明)。
1.2 磁盘的运行原理
由于每个盘面上都有磁头,当磁盘高速旋转的时候,会与盘面保持一个很小的距离,磁头就可以进行数据读取,这就是磁盘运行的基本原理。
给定任意一个扇区,如果想要读取该扇区的数据,需要三个步骤:
- 寻道:通过移动磁头,找到数据所在的磁道(柱面),这个过程叫寻道延迟,大概会消耗3~15ms;
- 旋转:通过旋转定位到磁头上具体的扇区,这个过程叫旋转延迟,大概会消耗2~4ms;
- 读写数据:这个过程速度很快,可以忽略不计; 因此,读写一个扇区的数据需要大约10ms左右。磁盘上的任意扇区都可以通过(磁头号,磁道号,扇区号)的三元组唯一标识,这个三元组就是磁盘的物理地址,磁头是从0开始编号,如果有M个磁头,则磁头编号是0 ~ M-1,磁道也是从0开始编号,如果有N个磁道,则磁道编号从0 ~ N-1,扇区与前两者不同,扇区是从1开始编号,如果有Q个扇区,则扇区编号从1 ~ Q,为什么唯独扇区是从1开始编号,不得而知,猜测是因为命名习惯吧。
从上述过程中,我们可以分析出两个结论:1、做数据读写的时候,寻道和旋转是最耗时的,因为他们都是机械运动,而数据读取的速度很快,为了充分利用这次寻找扇区所花费的时间,我们可以多读取一些数据,比如4KB,这样并不会增加一次数据读写的时间,反而提升了数据读写的效率;2、既然寻道和循转最耗时,在数据读写过程中,如果能减少这两种操作,就可以提升数据读写的效率。
1.3 磁盘的读写顺序
你是否想过这个问题:当往磁盘中写入一个非常大的数据,数据大到会占据整个磁盘,这个时候磁盘是按照怎样的顺序来写数据的?先写哪个扇区?
在磁盘开始运转的时候,磁头默认是放到0号磁道的1号扇区的,这个初始状态是人为设定的,所以,磁盘是以这个位置为起点开始写入数据,为了弄懂接下来的逻辑,需要把时间放慢很多倍,想象:当写完0号磁头0号磁道的1号扇区时,由于读写数据的速度非常快,磁盘才旋转了一点点,还要等很久磁盘才能旋转到0号磁头0号磁道的2号扇区,磁盘会这样一直等下去?当然不会,磁盘会把磁头切换到1号,然后把数据写入1号磁头0号磁道1号扇区,写完之后还没旋转到2号扇区,因此,继续写2号磁头0号磁道1号扇区,直到把所有磁头的0号磁道1号扇区的数据都写完,这些数据从上到下连成一条线。
接下来等到磁盘旋转到2号扇区的位置,再开始写0号磁头0号磁道2号扇区,1号磁头0号磁道2号扇区,就这样又写完一条线,循环下去数据就写满了第0号柱面,接下来,机械臂控制磁头往圆心方向移动到1号磁道,按照上述的方式,1号柱面的数据也被写满,最终,所有柱面的数据都被写满。总的来讲,数据写入的顺序是:以柱面为单位,从上到下,从外到内。具体过程可以参考下图:
1.4 磁盘的抽象
磁盘的每个扇区都有一个唯一的物理地址,即(磁头号,磁道号,扇区号)的三元组,假设磁头的数量是M,每个盘上磁道的数量是N,每个磁道上扇区的数量是Q,则一共有MNQ个扇区,再通过一个简单的数学变换就可以把磁盘的物理地址空间转换为线性地址空间,线性地址的范围是0 ~ MNQ-1,具体的转换过程如下:
以一个80个磁道(柱面),18个扇区,2个磁头(1张磁盘)的磁盘为例,以柱面为单位,对扇区进行编号,扇区的编号从1~2880,即,可以将磁盘抽象成数组。这个抽象非常重要,文件系统的设计都是以数组模型为基础的。
1.5 小结
磁盘提供了基于扇区进行数据读写的基本能力:就是在扇区上读写数据,这是构建文件系统的基础,文件除了可以存储数据,还有文件名,文件的访问权限控制等功能,这些功能需要交给文件系统来完成。虽然磁盘是圆柱形的结构,但它本质上是对连续的扇区进行读写操作,每个操作单元是512个字节,这跟数组的结构很像,因此,我们可以将磁盘抽象为数组,每个元素可以存储512个字节(一个扇区大小),因此,文件系统的构建就从数组模型作为起始点。
2 Linux物理文件系统原理与实现
在一般的操作系统书籍中,谈到Linux文件系统,都称作虚拟文件系统,我觉得这种叫法存在严重的误导,尤其是对于初学者,在我看来,Linux的文件系统是由物理文件系统和虚拟文件系统两部分构成的。所谓物理文件系统,是指持久化在磁盘上的一种数据结构,这个数据结构可以实现文件的所有功能,并且这个数据结构是以数组来建模的。下面会介绍一个简单的物理文件系统的实现(ext2),来说明物理文件的原理。
2.1 磁盘的进一步抽象
在构建一个文件系统之前,需要对磁盘做进一步的抽象。最初,磁盘被抽象成一个数组,数组的每个元素是512个字节,将数组的8个元素合并成1个,这样数组的长度就缩短了,如果原来的数组长度为N,合并之后的数组长度为N/8,但是数组中每个元素的大小增加到4K,叫做一个block(块),如下图所示:
经过这一层抽象后,由原来的一个扇区数组变为块数组。
2.2 block数组划分5个区域
接下来将block数组划分成5个区域,分别是:超级块、inode位图、逻辑块位图、inode、逻辑块。如下图所示:
这5个区域分别是由若干个逻辑块组成的,其中,最为核心的就是inode,我们知道,在Linux系统中,并不是用文件名来唯一标识一个文件的,而是用inode来唯一标识一个文件,即,inode与文件是一一对应的。一个inode代表一个文件,一个文件由两部分构成:元数据(inode)和文件内容(逻辑块),元数据用来存储文件管理相关的数据,逻辑块用来存储文件内容。inode位图用来存储inode是否被分配的信息,如果没有被分配,则代表该inode的bit为0,如果已经分别,则为1,逻辑块位图也是如此,是用来存储逻辑块是否被分配的信息。
2.3 定义inode数据结构
经过上面两个步骤,就可以开始物理文件系统最最核心的设计,即inode数据结构的设计,文件系统的核心功能需要用到的数据都在inode的结构里面。以ext2为例,inode结构如下:
可以看出,inode中包含三部分信息:文件权限相关的信息、文件的基本信息和文件存储的数据对应的逻辑块信息。
到这里,一个物理文件系统就设计完毕了,就可以实现文件系统的全部功能了,但也许你还没搞懂是如何实现的,不急,接下来会举一个简单文件系统实现和文件系统的核心功能是如何通过inode结构的实现过程。
2.4 物理文件系统实现举例
假设一个磁盘共包含64个block,构建一个简单的文件系统,步骤如下:
首先,对磁盘空间进行划分,分成5个部分,1个block用来存超级块、1个block用来存inode位图、1个block用来存逻辑块位图、5个block用来存inode,剩余的56个block做逻辑块,如下图:
接下来对每个部分进行编号,如下:
- inode位图:1个bit用来表示一个inode节点是否被空闲,4K大小可以表示32768个inode的状态,编号从0~32767
- 逻辑块位图:1个bit用来表示一个inode节点是否被空闲,4K大小可以表示32768个inode的状态,编号从0~32767
- inode:用256个字节表示一个inode,一个block可以保存16个inode,5个block一共可以保存80个inode,编号从0~79
- 逻辑块:1个block表示一个逻辑块,共有56个逻辑块,编号从0~55 从上面的编号可以看出,inode位图和逻辑块位图所能表示的范围是完全够用的,将inode的划分进一步细化,如下图:
block的划分和编号完成后,inode再按照上述的数据结构来设计,就可以实现一个简单的文件系统功能了。
2.5 目录是如何实现的
Linux系统号称一切皆文件,这句话不是盖的,目录也是一种文件,只不过这种文件比较特殊,他存储的内容是目录下面的子文件夹或者文件的名称和inode号,如下图所示:
进入/usr目录,执行ls -ai命令,可以看到里面都是子文件夹,/etc前面的163339就是/etc这个文件夹文件的inode号,如果想查看/etc这个文件夹下面都存储了哪些文件,可以读取163339这个inode,并根据逻辑块指针读取文件的内容,内容的格式也是类似,一个文件或文件夹对应了一个inode号。
每个文件夹都有2个特殊的inode号,.和..,我们知道,在linux系统中,.表示当前路径,..表示上一级文件夹路径,..的inode为64表示上一级文件夹的inode号为64,即/路径的inode号为64。再打开/文件夹查看,如下图所示:
可以看到根文件夹的.和..的inode相等都是64,说明根文件夹再往上没有文件夹了。根文件夹的inode号是在操作系统启动的时候,文件系统挂载到虚拟目录之前就已经缓存在内核中了,只要有了根节点的inode号,就可以顺藤摸瓜遍历下去,找到所有文件的文件夹。
寻找文件夹很重要,因为要想访问一个文件,就需要先找到文件对应的inode号,这个数据是保存在上级目录的文件中的,找到了文件对应的inode号以后,才可以对文件进行访问。比如,如果你想访问/home/iceli/abc.txt,需要先访问/的目录文件,由于/的inode是缓存在内核中,因此可以直接打开,拿到/home对应的inode 398920,然后再打开398920拿到/home/iceli的inode 987123,再打开987123拿到/home/iceli/abc.txt的inode 337092,然后就可以访问abc.txt文件了。
但是这里有个问题,访问一个文件的过程太繁琐了,要根据文件的目录层级依次访问每个文件目录的文件,每访问下一层目录都需要访问一次磁盘,这样效率岂不是很低?操作系统早就考虑到这一点,因此把所有目录文件的inode和文件内容都直接缓存在内核中,这样当访问一个文件的时候就不用访问磁盘了,直接通过内核找到/home/iceli/abc.txt的inode 337092并访问。甚至abc.txt文件的内容也会缓存在操作系统的page cache中(后面讲到虚拟文件系统的时候会讲到),提升下次访问时候的效率。
2.6 硬链接和软链接是如何实现的
Linux系统支持硬链接和软链接功能,可以通过ln命令来创建一个硬链接,创建了/root/abc.txt的一个硬链接xxoo,通过stat命令查看,如下图:
可以看到,abc.txt和xxoo的inode号是相同的,这说明abc.txt和xxoo无论是inode还是逻辑块都是相同的,文件名不同是因为文件名是存储在目录文件里面的,因此,硬链接只是文件的别名,除了文件名称不同,其他(inode、逻辑块)都相同。
再给abc.txt增加一个软链接xxyy,通过stat命令查看,如下图:
可以看到,xxyy的inode和abc.txt的inode是不同的,是新创建出来的,但是软链接的Blocks又是0,可以看出软链接本身并没有指向任何逻辑块,而是只有一个inode,它是逻辑块指针是指向的abc.txt的逻辑块。因此,软链接可以看作是一个没有逻辑块的inode。因此,如果把原文件删除,则原文件的inode和逻辑块都没有了,而软链接又只有一个inode,此时,就会出现指向的数据不存在的问题,如下图所示:
2.7 一个文件的访问过程
接下来从物理文件系统的视角来介绍一个文件的完整访问过程,强调一下,之所以说是从物理文件系统的视角,是因为真实的文件访问过程不是这样的,这个例子只是为了更进一步说明文件和目录是怎样在磁盘上存储的。
举例:在/home/iceli下打开一个文件demo.txt,并写入字符串abc
首先,在访问demo.txt之前,先要找到demo.txt文件对应的inode,而这个inode号是保存在/home/iceli的目录文件中,因此,先根据/文件的inode,顺藤摸瓜一直找到/home/iceli的inode号,假设是8,打开8号inode并查看里面的block 108,inode 108就代表了/home/iceli这个目录的文件内容,这里保存了demo.txt的inode,通过比较发现demo.txt的inode号为47;找到demo.txt的inode号以后,就可以开始访问demo.txt的文件了,读取47号inode,得到block为627,即,demo.txt的数据部分保存在627号block中,于是往627号block写入数据abc,任务完成。
3 Linux虚拟文件系统的原理和实现
我认为,Linux文件系统是由物理文件系统和虚拟文件系统共同组成的,尽管很多书籍都只讲了虚拟文件系统。无疑,虚拟文件系统是Linux文件系统的核心,物理文件系统只是持久化在磁盘上的一个数据结构,它为文件系统的功能提供了基础支撑,但它是静态的,虚拟文件系统实现了访问文件的整个过程,是动态的,有点类似程序和进程的关系。下面将详细介绍linux虚拟文件系统的架构和实现过程。
3.1 用户视角看待windows和linux文件系统的区别
使用windows的人对磁盘和文件系统的感受是这样的:比如你有一台windows的笔记本,硬盘1T,分了4个逻辑分区,分别是:C盘、D盘、E盘、F盘,当然,你还可以给每个盘起一个名字C盘(system),D盘(program)、E盘(work)、F盘(data),4个逻辑分区都是NTFS格式。在使用windows系统时,你有多少个逻辑分区,每个分区是什么格式(物理文件系统的类型),你是非常清楚的,这是使用windows系统的人的直观感受。
对于使用linux的人感受是这样的:比如你有一台linux虚拟机,你在宿主机上虚拟了一块硬盘,大小为20G,在创建系统的时候,会自动将硬盘划分成多个逻辑分区,这可以通过df -i命令来查看,如下:
这里列出的devtmpfs、tmpfs、/dev/sda3都是文件系统,但是,在用户视角来看,你并不能直观的看到这些分区,永远看到的都是一个虚拟目录树,所谓虚拟目录树就是以根结点/为开始,/usr、/root、/etc等文件夹组成的一颗树。如下图:
这就是windows系统和linxu系统从用户视角看到的最大区别:windows系统可以直观的看到每个逻辑分区,而linux系统只能看到一颗虚拟目录树,而具体的文件系统是挂载(mount on)到虚拟目录树的某个路径上,只能够通过df命令进行查看。
3.2 虚拟文件系统VFS架构
虚拟目录树只是linux虚拟文件系统的一个直观感受,虚拟目录树下面是虚拟文件系统的一系列复杂实现。先看一下VFS的架构,如下图:
VFS由四大部分构成,分别是:
- 对应用程序提供的统一的文件系统API访问接口,这是一组接口,没有实现,但它同时是一个标准,所有的文件系统都要实现这个标准才能接入被应用程序访问。这一层接口类似java的interface,所有文件系统都要实现这个interface集合,应用程序在调用文件系统功能的时候,比如,应用程序调用open函数打开一个文件,虚拟文件系统会调用具体文件系统的open函数,调用是通过接口形式实现的,对于虚拟文件系统来讲,它不需要知道具体的文件系统是如何open这个文件的,这些细节都由具体的文件系统来实现,这一层标准的API定义可以参考如下一段linux源码截图,
通过上面源码的截图可以看到,基于文件的很多操作都在API里面有定义,但是却没有具体实现。
- 各种物理文件系统对于虚拟文件系统的API的具体实现,如ext2系统的实现,ext4系统的实现;
- 物理文件系统的元数据的缓存:虚拟文件系统缓存了目录项、inode等数据,提高文件的访问速度,
- 页缓存:通过页缓存 (page cache)优化I/O速度
3.3 页缓存(PageCache)
PageCache是文件系统访问磁盘数据的一种优化策略,由于访问磁盘的速度很慢,为了提升访问速度,可以考虑把从磁盘访问拿到的数据在PageCache中做缓存,这样,第一次访问这的时候由于要从磁盘中获取数据,所以速度很慢,但是第二次访问的时候,由于数据在PageCache中已经存在,就可以直接获取到数据,而不用再访问磁盘,大大提升了访问速度。这有点像应用开发中数据库的速度慢,引入redis做缓存来提升访问速度,是同样的思路。
但是, //TODO