表弟

  • 主页
  • 关于我

表弟

  • 主页
  • 关于我

IO优化基础知识

2020-04-30

在工作中,我们很多工程师对 I/O 的认识其实比较模糊,认为 I/O 就是应用程序执行 read()、write() 这样的一些操作,并不清楚这些操作背后的整个流程是怎样的。我们先从整体上来了解下Linux系统IO Stack有多么的复杂,下图是整个Linux系统的IO Stack:

我们一般意义上的IO,主要指磁盘和文件系统的管理,这是操作系统最核心的功能之一。磁盘为系统提供了最基本的持久化存储。文件系统则在磁盘的基础上,提供了一个用来管理文件的树状结构。那么,磁盘和文件系统是怎么工作的呢?又有哪些指标可以衡量它们的性能呢?我们先来看看,Linux 文件系统的工作原理

Linux文件系统基本概念

VFS

为了支持各种不同的文件系统,Linux 内核在用户进程和文件系统的中间,又引入了一个抽象层,也就是虚拟文件系统 VFS(Virtual File System)。VFS 定义了一组所有文件系统都支持的数据结构和标准接口。这样,用户进程和内核中的其他子系统,只需要跟 VFS 提供的统一接口进行交互就可以了,而不需要再关心底层各种文件系统的实现细节。

在 VFS 的下方,Linux 支持各种各样的文件系统,如 Ext4、XFS、NFS 等等。按照存储位置的不同,这些文件系统可以分为三类。

  • 第一类是基于磁盘的文件系统,也就是把数据直接存储在计算机本地挂载的磁盘中。常见的 Ext4、F2FS等,都是这类文件系统。
  • 第二类是基于内存的文件系统,也就是我们常说的虚拟文件系统。这类文件系统,不需要任何磁盘分配存储空间,但会占用内存。我们经常用到的 /proc 文件系统,其实就是一种最常见的虚拟文件系统。此外,/sys 文件系统也属于这一类,主要向用户空间导出层次化的内核对象。
  • 第三类是网络文件系统,也就是用来访问其他计算机数据的文件系统,比如NFS等。

对于 Android 来说,现在普遍使用的是 Linux 常用的 ext4 文件系统。华为率先在 EMUI 5.0 以后就使用 F2FS 取代 ext4,Google 也在最新的旗舰手机 Pixel 3 使用了 F2FS 文件系统。Flash-Friendly File System 是三星是专门为 NAND 闪存芯片开发的文件系统。F2FS 文件系统在写性能方面比 ext4 更快,随机写可以优化>20%,不足之处在于可靠性方面出现过一些问题。

磁盘文件系统,要先挂载到 VFS 目录树中的某个子目录(称为挂载点),然后才能访问其中的文件。拿第一类,也就是基于磁盘的文件系统为例,在安装系统时,要先挂载一个根目录(/),在根目录下再把其他文件系统(比如其他的磁盘分区、/proc 文件系统、/sys 文件系统、NFS 等)挂载进来。

Ext4

磁盘在执行ext4文件系统格式化时,会形成如下布局:

块组

  • 超级块: 是用于存储文件系统自身元数据的核心结构。其中的信息包括空闲与已使用块的数目、块长度、 当前文件系统状态(在启动时用于检测前一次崩溃)、各种时间戳(例如,上一次装载文件系统 的时间以及上一次写入操作的时间)。它还包括一个表示文件系统类型的魔数,这样mount例程 能够确认文件系统的类型是否正确。内核只使用第一个块组的超级块读取文件系统的元信息,即 使在几个超级块中都有超级块,也是如此。

  • 组描述符: 包含的信息反映了文件系统中各个块组的状态,例如,块组中空闲块和inode的数目。每个块组 都包含了文件系统中所有块组的组描述符信息。

  • 数据块位图和inode位图: 用于保存长的比特位串。这些结构中的每个比特位都对应于一个数据块或inode,用于表示对应的数据块 或inode是空闲的,还是被使用中。

  • inode表: 包含了块组中所有的inode,inode用于保存文件系统中与各个文件和目录相关的所有元数据。

  • 数据块: 包含了文件系统中的文件的有用数据。

索引节点和目录项

在 Linux 中一切皆文件。不仅普通的文件和目录,就连块设备、套接字、管道等,也都要通过统一的文件系统来管理。为了方便管理,Linux 文件系统为每个文件都分配两个数据结构,索引节点(index node)和目录项(directory entry)。它们主要用来记录文件的元信息和目录结构。

  • 索引节点,简称为 inode,用来记录文件的元数据,比如 inode 编号、文件大小、访问权限、修改日期、数据的位置等。索引节点和文件一一对应,它跟文件内容一样,都会被持久化存储到磁盘中。所以索引节点同样占用磁盘空间。

  • 目录项,简称为 dentry,用来记录文件的名字、索引节点指针以及与其他目录项的关联关系。多个关联的目录项,就构成了文件系统的目录结构。换句话说,索引节点是每个文件的唯一标志,而目录项维护的正是文件系统的树状结构。目录项和索引节点的关系是多对一,你可以简单理解为,一个文件可以有多个别名。不过,不同于索引节点,目录项是由内核维护的一个内存数据结构,所以通常也被叫做目录项缓存。

注意:

1.目录项不是目录,目录也是一种文件,是文件就有对应的索引节点,索引节点同样占用磁盘空间。
2.目录项与索引节点的对应关系是多对一关系(例如:通过硬链接为文件创建的别名,就会对应不同的目录项,不过这些目录项本质上还是链接同一个文件,所以,它们的索引节点相同。),但是目录的索引节点和目录项目只能是一对一。

真实案例:sdcardfs由于目录的目录项和索引节点多对一造成的系统死锁,系统Hung住。

https://github.com/novelinux/toolbox/blob/master/debug/ramdump/example1.md

索引节点和目录项纪录了文件的元数据,以及文件间的目录关系,那么具体来说,文件数据到底是怎么存储的呢?是不是直接写到磁盘中就好了呢?

实际上,磁盘读写的最小单位是扇区,然而扇区只有 512B 大小,如果每次都读写这么小的单位,效率一定很低。所以,文件系统又把连续的扇区组成了逻辑块,然后每次都以逻辑块为最小单元,来管理数据。常见的逻辑块大小为 4KB,也就是由连续的 8 个扇区组成。如下图所示:

文件系统IO

把文件系统挂载到挂载点后,就能通过挂载点,再去访问它管理的文件了。VFS 提供了一组标准的文件访问接口。这些接口以系统调用的方式,提供给应用程序使用。

文件读写方式的各种差异,导致 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,需要你在系统调用中,指定 O_DIRECT 标志。如果没有设置过,默认的是非直接 I/O。不过要注意,直接 I/O、非直接 I/O,本质上还是和文件系统交互。如果是在数据库等场景中,你还会看到,跳过文件系统读写磁盘的情况,也就是我们通常所说的裸 I/O。

  • 第三,根据应用程序是否阻塞自身运行,可以把文件 I/O 分为阻塞 I/O 和非阻塞 I/O:

所谓阻塞 I/O,是指应用程序执行 I/O 操作后,如果没有获得响应,就会阻塞当前线程,自然就不能执行其他任务。所谓非阻塞 I/O,是指应用程序执行 I/O 操作后,不会阻塞当前的线程,可以继续执行其他的任务,随后再通过轮询或者事件通知的形式,获取调用的结果。比方说,访问管道或者网络套接字时,设置 O_NONBLOCK 标志,就表示用非阻塞方式访问;而如果不做任何设置,默认的就是阻塞访问。

  • 第四,根据是否等待响应结果,可以把文件 I/O 分为同步和异步 I/O:

所谓同步 I/O,是指应用程序执行 I/O 操作后,要一直等到整个 I/O 完成后,才能获得 I/O 响应。所谓异步 I/O,是指应用程序执行 I/O 操作后

性能分析

容量

对文件系统来说,最常见的一个问题就是空间不足。用 df 命令,就能查看文件系统的磁盘空间使用情况。比如:

1
2
3
whyred:/ # df -h /data/
Filesystem Size Used Avail Use% Mounted on
/dev/block/dm-0 50G 20G 30G 40% /data

不过有时候,碰到了空间不足的问题,可是用 df 查看磁盘空间后,却发现剩余空间还有很多。这是怎么回事呢?不知道你还记不记得,刚才我强调的一个细节。除了文件数据,索引节点也占用磁盘空间。你可以给 df 命令加上 -i 参数,查看索引节点的使用情况,如下所示:

1
2
3
whyred:/ # df -ih /data/
Filesystem Inodes IUsed IFree IUse% Mounted on
/dev/block/dm-0 3.2M 59K 3.1M 2% /data

索引节点的容量,(也就是 Inode 个数)是在格式化磁盘时设定好的,一般由格式化工具自动生成。当你发现索引节点空间不足,但磁盘空间充足时,很可能就是过多小文件导致的。所以,一般来说,删除这些小文件,或者把它们移动到索引节点充足的其他磁盘中,就可以解决这个问题。

缓存

可以用 free 或 vmstat,来观察页缓存的大小。free 输出的 Cache是页缓存和可回收 Slab 缓存的和,可以从 /proc/meminfo ,直接得到它们的大小:

1
2
3
4
5
6
7
8
9
10
11
12
13
whyred:/ # cat /proc/meminfo
# Memory in buffer cache, so relatively temporary storage for raw disk blocks. This shouldn't get very large.
Buffers: 59704 kB
# Memory in the pagecache (Diskcache and Shared Memory)
Cached: 1700040 kB
# Memory that is present within main memory, but also in the swapfile. (If memory is needed this area does not need to be swapped out AGAIN because it is already in the swapfile. This saves I/O and increases performance if machine runs short on memory.)
SwapCached: 5308 kB
...
# The part of the Slab that might be reclaimed (such as caches)
SReclaimable: 92544 kB
# The part of the Slab that can't be reclaimed under memory pressure
SUnreclaim: 160464 kB
...

文件系统中的目录项和索引节点缓存,又该如何观察呢?实际上,内核使用 Slab 机制,管理目录项和索引节点的缓存。/proc/meminfo 只给出了 Slab 的整体大小,具体到每一种 Slab 缓存,还要查看 /proc/slabinfo 这个文件。比如,运行下面的命令,你就可以得到,所有目录项和各种文件系统索引节点的缓存情况:

1
cat /proc/slabinfo | grep -E '^#|dentry|inode'

但是一般Android手机系统不会打开对应内核的配置,所以我们一般也不容易看到。

  • IO
  • IO

扫一扫,分享到微信

微信分享二维码
Hello World
© 2020 表弟
Hexo Theme Yilia by Litten