7

曾经做嵌入式开发的我,现在做服务器开发,很多思路要转变。今天学习了服务器高性能IO设计,同时自己也还发散开去学习了其他的一些参考资料,顺便结合自己已有的一些知识,做为自己的学习笔记,总结和记录一下吧~~

本文首先从硬件原理的角度,阐述提高硬盘 I/O 效率的途径。
本文包括一些小知识,与高性能服务器开发没有直接关系,不感兴趣的话可以跳过。

本文地址:https://segmentfault.com/a/1190000011743916

“硬盘” 是什么

这里我所说的 “硬盘”,也就是所谓的 “hard disk”,经常简称为 “disk” 或者 “HDD”,同时还有另外一个更加高大上的名字 “非易失性存储”。

请各位回忆一下计算机组成原理里关于存储的部分,从 CPU 开始,存储层次如下:

  • 寄存器
  • 缓存(cache),从高到低又可以分一级、二级、三级缓存,数字越高,距离 CPU 越远、容量越大、速度越慢
  • 主存,也就是内存,就是我们常见说 “内存条”
  • 硬盘,包括所有的非易失性存储,就是本文说明的内容。
  • 离线存储,包括那些 CD 啊之类的

非易失性存储就是说该存储介质上的数据,只要写入了,那么就算设备掉电了,也能够保持,而不会被清空。
广义而言,非易失性存储包含非常多的种类,包括磁盘、闪存、EEPROM 等等。而对于服务器而言,只涉及两种,那就是磁盘和 SSD。

磁盘使用磁性元件做成盘片,然后使用盘片上对应位置是否有磁性,来判断该位置存储的值是逻辑 1 还是逻辑 0。
另一种是固态硬盘,也就是 SSD。SSD 的原理其实就是我们的 “U盘”。从存储介质的角度,“U盘” 其实不是一个准确的说法,准确的说,应该叫做 “闪存”(flash memory)。

从制造原理上,闪存又分为两种,一种是 NOR-flash,另一种是 NAND-Flash。NOR-Flash 不会在服务器上使用,正文略过不讲(感兴趣的话,可以看本文的小知识)。本文关注的是使用 NAND-Flash 制作而成的硬盘,也就是 SSD

硬盘 “块设备” 是什么?

Linux 中,所有的东西都可以抽象成文件。而所有的设备,则会被分为字符设备和块设备。
字符设备的意思是,对于该设备上随机指定的地址的值,都可以直接写入。

相对应地,块设备的意思是,对于该设备上随机指定的地址上的数据,如果需要修改的话,需要连同该地址周边一整块数据都一起读到内存中、修改了指定数据之后,再整块写回。

<<<< 小知识:为什么 SSD 是块设备?为什么写入这么麻烦?>>>>

    SSD 不能简单随机地读写给定地址上的数据。SSD 读取以 ”页“ 为单位(如:256 Bytes),擦除 / 
写入以 ”块“ (与内存映射的 “4kB” 的那个 “块” 不是一回事) 为单位。

    此外,SSD 还有一个很大的特点,就是修改对应数据的时候,还不是简单地执行一个 write 动作
(不是标准 C 的 write())就可以了,而是需要首先执行一次 erase 操作,将当前的整个块擦除掉
(全部变成逻辑 0),然后再将整个块的数据重新写一遍。

    所以对于块设备而言,执行读操作还算简单,但是执行写操作的话,整个流程就变成了:read、erase、
write 了,何其复杂!SSD 之所以被设计成这么复杂的原因,主要是半导体比特密度和制造成本之间的一个
取舍。原理可以写一整章,本文就不展开讲了。

    为什么磁盘是块设备?这个很抱歉,笔者没有准确的答案。或许因为磁盘转的太快,没办法极其精
准地定位具体一个 bit?

磁盘寻址速度的考量

这里我们先放下 SSD,来讲一讲磁盘。个人电脑和服务器上所使用的磁盘结构都是一模一样的。磁盘的结构和各术语如下图所示:

磁盘中会并列地摆放很多片盘片,每个盘片的正反面都会有一个磁头,使用这个磁头来读取盘片上的磁状态。读数据的时候,一般会同时从几个盘片读,以达到最高的读取速度。

所以我们可以看到,磁盘寻址的速度,主要受到以下两个因素的影响:

  • 磁头转动到指定位置的时间
  • 盘片旋转,并将指定点转动到磁头下的时间

极端情况下,磁头需要从一端转到另一端;而盘片则需要转动 180 度。

乍一看,其实这两个时间是非常短的,似乎并不会影响读取文件的速度。两边引起质变,这两个时间积累起来的时候,就会出现问题了。那么怎样才能累积呢?答案就是:频繁、随机地存取文件系统。

所以,提高磁盘效率的思路就是:避免频繁的随机存取文件

硬盘文件存取速度的考量

这里所说的包含磁盘和 SSD。前面已经说了,硬盘是块设备,也就是当读取内容的时候,只能以块为单位进行操作。

这里举一个最简单的例子吧,我们把块的单位减少到 8bits,存取的单位以 bits 来计算。比如在一段连续的内存中,数据是这样的:

Addr:  0 1 2 3 4 5 6 7
value: 0 0 1 1 0 1 0 0

我们只需要读取地址 5 上的值,这就是所谓的随机读取。但是硬盘的原理决定了我们没办法只读这么一个数据。驱动只能把这一整块的数据(00110100)一起读出来,然后再把这地址 5 的值返回给应用程序。

写入的时候就更复杂了。针对 SSD,如果我们需要把地址 5 上的值改为 0,那么驱动需要干下面几件事情:

  1. read: 把 [00110100] 读到内存中
  2. mod: 将地址 5 的值在内存中设置为 0,总共变成 [00110000]
  3. erase: 将硬盘中这一整块的存储内容擦除掉,擦除之后,这段地址的值会变成 [11111111](其实 SSD 的擦除动作还是比较快的)
  4. write: 将实际需要设置的值 [00110000] 写入到硬盘中。

磁盘还算比较简单,只需要执行 1 和 4 两个步骤就行了。

从上面的流程我们大概可以看到磁盘文件存取速度的优化方向了。如果你还没弄明白的话,我再举一个例子:

假设还是上面的多个 SSD 块,我们做一次循环操作来模拟多次存取。伪代码如下:

define SECT_SIZE        (5)

int addr = 0;

for (sectNum  = 0 to 100)
{
    bits[SECT_SIZE] = read_data(from: addr, len: SECT_SIZE);

    bits[0] = 0;
    bits[SECT_SIZE - 1] = 0;    // 只修改一部分
    write_data(data: bits, from: addr, len: SECT_SIZE)
    
    addr += SECT_SIZE
}

这段代码有什么问题呢?我们只需要取前两次循环,看看驱动做了什事情就知道了。
首先是第一次循环(Loop 0):

此时 addr = 0,
1. 从 0 号块读出 8 bits 的数据,并且取其中的 5bits 返回给应用程序(耗时 t1)
2. 应用程序修改了两个 bits
3. 将 0 号块的 8bits 写回(擦除 + 写入,耗时 t2)

第二次循环就不一样了:

此时 addr = 5
1. 从 0 号块取出 8bits 的数据,并且取最后 3bits 为有效数据(耗时 t1)
2. 从 1 号块取出 8bits 的数据,并且取前 2bits 为有效数据,与前面的 3bits 拼在一起,返回 5bits 给应用程序(耗时 t1)
3. 应用程序修改了两个 bits
4. 将 0 号块的 8bits 写回(耗时 t2)
5. 将 1 号块的 8bits 写回(耗时 t2)
总耗时是第一个循环的两倍。

可见,由于横跨了两个块,虽然应用程序操作的数据量是一样的,但是操作的时间却多出了整整一倍。

因此从这里我们可以看出,提高硬盘存取效率的另一个途径是:尽可能以块为单位,进行读写操作。在操作系统中,软件层面关心的块的单位一般是 4096 字节(4kB)。

关于 NOR Flash

一句话,NOR Flash 只用在嵌入式设备中,做服务器开发的不需要关心。本小节没有正片,只有小知识。

<<<< 小知识:NOR Flash 是什么?有什么特点? >>>>

    NOR Flash 和本文所说的 SDD(NAND Flash)的区别,首先在硬件结构上显然是不同的。但是嘛,
如前文所述,硬件的咱不讲。

    NOR Flash 的特点有下面几点:
    1. NOR Flash 也是块设备,但 NOR Flash 支持绝对的随机读取,要读多长就读多长,并且读取
       速度高于 NAND Flash
    2. 相同条件下,NOR Flash 的擦除和写入耗时远远大于 NAND Flash
    3. NOR Flash 支持有限的随机写能力,所谓有限,意思是 NOR Flash 可以将任意逻辑 0 的位改
       写为逻辑 1,但是却没办法将 1 改写为 0
    4. NOR 执行块的擦除操作后,整个块都会变成逻辑 0。因此对于绝大部分数据(0 和 1 混杂的)
       写入操作,实际上和 NAND Flash 的操作流程是一样的
    5. 当容量小于 16MB 时,NOR Flash 的成本小于 NAND Flash。再往上就不如 NAND 了。

    上面的特性,使得 NOR Flash 几乎不会用在 PC 的存储体系中。但却在嵌入式设备中(包括计算
机主板上 BIOS 的存储器)应用极广,因为:
    1. 嵌入式设备主要是保存大量的代码数据(只读,特点1),和少数并且很少变动的用户数据(读写,
       特点1)
    2. 嵌入式设备的程序空间很小,裁减过的 Linux 内核经过压缩可以达到2MB 甚至更低(特点5)
    3. 嵌入式设备可能随时断电,支持 jffs2 文件系统之后,可以保护文件内容(特点3)
<<<< 小知识:为什么支持了 jffs2 就支持断电保护?>>>>

    可以说 jffs2 简直就是为 NOR Flash 量身定做的文件系统了。一般而言,比如我们使用 FAT32 
对 U盘进行格式化之后,如果 U盘在写入数据的时候突然从主机设备上拔掉,那么文件系统很可能会崩
溃、损坏。这就是为什么操作系统会有 “安全移除存储设备” 的功能。

    Jffs2 完美地利用了 NOR Flash “可以将 0 改写为 1” 的特性。首先在格式化的时候,jffs2 以
块为单位划分存储空间,然后在创建索引表的时候。文件系统在将数据写完之前,会对应的块设置为 0,
也就是标记为无效块。当数据完全写入完成之后,就将标记从 0 修改为 1。而 jffs2 每次加载的时候,
会遍历所有的块,并且在内存中重建索引表。

    如果在写入数据的过程中断电了,那么相应的块继续保持无效状态。这样就保证了文件系统的断电
保护。
    当然 jffs2 的原理远远没有这么简单。Linux 里面就有 jffs2 的代码,各位看官可以自行取用。

小结

针对硬盘作为块设备的各种特点,从硬件原理上优化磁盘效率,我们的思路主要就是两个方向:

  1. 避免或者尽可能减少存取磁盘上随机地址的数据,也就是尽量顺序地存取文件
  2. 存取文件时尽可能以块为单位存取(2 的倍数,最好是 4kB 的倍数)

实际代码中具体应该怎么操作,在下一篇文中给出。

参考资料

Nor/Nand FLASH的读写
让 CPU 告诉你硬盘和网络到底有多慢


amc
924 声望223 粉丝

电子和互联网深耕多年,拥有丰富的嵌入式和服务器开发经验。现负责腾讯心悦俱乐部后台开发