计算机存储器
存储器也是计算机的核心部件之一,它应该满足以下几个要求:
- 存取速度足够快,最好能和 CPU 相提并论,这样 CPU 在工作的时候才不会受限于存储器而可以充分的发挥。
- 容量足够大,最好能一次性存储我们所有所需的数据,包括文件、图片、视频、日志等等。
- 价格足够便宜,这样所有的计算机都可以配备足够多的存储器。
然后现实情况是,上面的要求不可能同时满足,所以现代计算机的存储器采用了一种分层的结构,从顶到底,存储容量递增,访问速度递减。
第一层是「寄存器」,是最靠近 CPU 的一层,制作工艺和材料和 CPU 一样,因此 CPU 访问寄存器是没有时延的, 但是寄存器价格昂贵因此数量也是极少,一般 32 位的 CPU 配备的寄存器容量是 32✖️32 Bit,64 位的 CPU 则是 64✖️64 Bit,且寄存器大多都有固定的作用,比如栈基地址寄存器、栈顶地址寄存器、程序计数器寄存器等。
第二层是「CPU高速缓存」,有 L1 L2 L3 三个级别的高速缓存,L1 级别是每个 CPU 核心独占的,L2 级别不同架构的 CPU 不同,以 Cortex-A53 架构举例,其用 cluster 为单位把 CPU 核心分组,同一个 cluster 的CPU 共享 L2 级别,L3 级别是所有 CPU 核心共享的,通过总线和主存相联。
高速缓存是以 line 缓存行为单位进行数据的读取和写入的,一般是 64 个字节作为一个缓存行,就算是从主存中读取 1 个字节的数据,CPU 也会读取 64 个字节的数据到高速缓存中。
高速缓存中有着较为复杂的映射机制去把小容量的 L1 L2 L3 缓存映射到大容量的主存以及协议去保证高速缓存内部和高速缓存&主存间的数据一致性。
第三层是「主存」,也即我们常称呼的内存,其正式名称叫随机访问存储器(Random Access Memory),是我们/程序经常打交道的存储器,程序运行过程中的栈、堆以及代码段就是存储在主存中。CPU 是通过地址总线把数据的地址传输给内存来访问主存中的数据的,主存通过数据总线把数据返回给 CPU,走的是电信号,因此速度比硬盘要快上几个数量级。
第四层是「硬盘」,硬盘的容量非常大,相比较内存条的十GB而言,其可达百GB甚至TB级别,但是它的访问速度非常慢,因为相比较主存的电信号而言,其是通过机械运动来进行数据的寻址的。下面来细说硬盘。
硬盘的工作原理
硬盘的物理结构
1. 盘片
盘片上附着着一层磁性材料,磁性具有南北极,南北极的不同朝向即可代表0和1(比如左边是 S 极而右边是 N 极代表 0;反过来代表 1),因此盘片上即可记录比特信息。一个硬盘通常由有多个通过主轴串起来的盘片,主轴带动所有盘片旋转,需要注意的是,盘边的旋转永远是朝一个方向的,不会倒回去。
2. 磁头组件:磁头&传动臂&转动轴
每一个盘片上会有两个(因为盘片的上下两面都可以读写)无限接近于它的磁头,磁头可以感应和修改盘片上的磁极方向因此可以读取盘片和修改盘片记录的比特信息。硬盘的由步进电机驱动的转动轴通过传动臂可以使磁头到达盘片上的指定位置,即后面会讲到的磁道Track,此过程叫寻道,属于机械运动。
3. 传输接口
上图中左边的就是传输接口,右边的是硬盘的电源线路,传输接口即外部和硬盘之间数据传输的线路,目前主流的有
SATA 这种类型。
| 版本 | 带宽(Gbit/s) | 速度(MByte/s) |
|---|---|---|
| SATA 1.0 | 1.5 | 150 |
| SATA 2.0 | 3 | 300 |
| SATA 3.0 | 6 | 600 |
带宽即该接口每秒通过的 bit 数据量,但是由于每 10 bit 数据中有 2 bit 是做数据校验的,所以真实传输的用户数据只有 8 bit,因此速度换算下来不是除以 8 而是除以 10,因此,目前主流的 SATA3硬盘的理论极限速度是 600 MByte 每秒,但是由于硬盘内部的物理结构限制,其实际速度只有 100 MByte 到 200 MByte 每秒。
逻辑结构
1. 盘面 Side
一般,盘片的上下两面都是可以读写的,因此一个盘片配备两个磁头,各自负责一个盘面,盘面从上到下依次从 0 开始编号,磁头同理。
2. 磁道 Track
设想一下,磁头在距盘面圆心一厘米远的点不动,盘面旋转一圈,磁头在盘面上划出来的一个同心圆即磁道,若磁头在距盘面圆心两厘米远的点不动,盘面旋转一圈,磁头在盘面上划出来的一个同心圆也是一个磁道,因此盘面上从外到内依次从0开始编号,有 0 到 N 个被编号的磁道,直至覆盖整个盘面的可用空间。
3. 柱面 Cylinder
所有盘面的同一个磁道为一个柱面,硬盘的读/写是以柱面进行的,只有当当前柱面上每一个盘面的磁道都被 写过了,才会切换下一个柱面(切换柱面就是摆动磁头,也即寻道),因为切换柱面是非常慢的机械运动,而切换盘面是程序控制的电子行为,所以应当尽量减少磁头的移动,最大化利用每一次寻道后的柱面,读同理。
4. 扇区 Sector
但是比特数据并不是连续的直接的记录在磁道上的,而是扇区。一个磁道会被均匀的划分为多个扇区,硬盘组织数据的最小单元是扇区,扇区一般是 512 字节大小,你把一个文件写入到硬盘,硬盘首先会为你这个文件分配一个扇区,如果这个文件只有 12 字节大小,那么硬盘就有 500 字节的空间是浪费的了,因为一个扇区只能分配一个文件(具体会浪费多少空间,还和文件系统有关,因为扇区只是硬盘组织数据的最小单位)。
上图是扇区的逻辑结构,扇区头记录了扇区的坐标,即 [ 盘面号/磁头号,磁道号/柱面号, 扇区号] 这样一个三元组,以及一些状态标识等;间隙是一段空白区域,目的是为了让磁头的读写切换留有缓冲的时间,或者避免错过了某一个扇区(否则得等盘面再转一圈了);数据区即我们实际的比如文件、图片、视频等内容的二进制数据。
5. 分区 Partition
分区也是一个逻辑概念,指的是把磁盘上连续的柱面划分成多个分区,可以想想成一个接一个的空心圆柱体层层套娃的样子,每一个空心圆柱体就代表一段连续的柱面,也就代表一个分区。比如 Windows 系统常见的 C盘、D盘、E盘就是一个个分区,如果有熟悉 Linux 的同学,可能会观察到 /dev 目录下有 /dev/sda1、/dev/sda2 等等设备文件,这两个文件就代表 /dev/sda 硬盘的两个分区。目前常见的有两种分区方式,分别是较早诞生的 MBR 以及后来更优越的 GPT。
5.1 MBR(Master Boot Record)
该格式的分区规定硬盘的第一个扇区(即 0 柱面 0 磁头 1 扇区)称为主引导扇区,该扇区的结构如下:
红圈上面的是引导程序的代码区,引导程序即主机上电自检后会执行的一段程序,该程序的主要作用是从硬盘主分区中加载操作系统到内存中并且执行操作系统内核,如 GRUB 就是常见的引导程序。
红圈部分即分区表,分区表一共有 4 个 16 bytes 的分区项,因此一个硬盘最多只能有 4 个分区(但是可以通过扩展分区来实现给一块硬盘分 4 个以上的分区)。
上图是分区项的结构,可以留意一下最后一个字段,它记录了该分区的扇区数,有 4 个 bytes,因此每个分区最大只能有 2^32 个扇区,这就是 MBR 分区方式的硬盘 2.2TB 大小限制的由来。分区项里面还记录了该分区的起始、结束的磁头号、扇区号、柱面号,这里也可以判断到,分区并不是最小只能以柱面为单位来进行划分的,而是可以细致到扇区号。后面的就不再展开了,但可以留意一下其中一个 1 byets 大小的
Partition type字段,这个字段指定了这个分区的文件系统类型,文件系统后面会讲到。
扩展分区及其原理
回到刚刚到 MBR 只能有 4 个分区项的问题,4 个分区项不代表一块硬盘设备只能划分出 4 个分区,因为分区又分主分区Primary Partition和扩展分区 Extened Partition,一块硬盘设备可以有以下几种分区方式:
- 4 个主分区
- 3 个主分区 + 1 个扩展分区(一个 MBR 最多只能有一个扩展分区)
扩展分区内部是可以拆分成多个逻辑分区 Logical Partition的,逻辑分区和主分区没有太大区别,都可以被格式化后正常使用,但是扩展分区不能被格式化,因为扩展分区实际上只是一个记录了逻辑分区信息的表格,这个表格称之为 EBR(Extened Boot Record),结构如下:
| name | starting sector | number of sector |
|---|---|---|
| 1st entry | 该 EBR 记录和其指向的逻辑分区的首个扇区的偏移量 | 该 EBR 记录指向的逻辑扇区的扇区数量 |
| 2st entry | 该 EBR 记录和下一个 EBR 记录的偏移量 | 下一个 EBR 记录 A 的扇区数量(包括 A 指向的逻辑分区的扇区数) |
再看一个示例:这里假设一个有 6000 个 512 bytes 扇区的总大小为 3MB 的扩展分区,该扩展分区又分为了 3 个逻辑分区,则这个 3 MB 大小的逻辑分区一共会有 3 个 EBR,分别对应一个逻辑分区,该逻辑分区的具体结构如下图所示:
上面的表格可以得出以下结果:
-
1st 逻辑分区及其 EBR 记录:第 5000 ~ 6999 扇区 总大小 2000 扇区
- 第 5000 ~ 5019 扇区:存储了 1st EBR 记录
- 第 5020 ~ 6999 扇区:1st 逻辑分区
-
2st 逻辑分区及其 EBR 记录:第 7000 ~ 7999 扇区 总大小 1000 扇区
- 第 7000 ~ 7019 扇区:存储了 2st EBR 记录
- 第 7020 ~ 7999 扇区:2st 逻辑分区
-
3st 逻辑分区及其 EBR 记录:第 8000 ~ 10999 扇区 总大小 3000 扇区
- 第 8000 ~ 8019 扇区:存储了 3st EBR 记录
- 第 8020 ~ 10999 扇区:3st 逻辑分区
该扩展分区的起始扇区是 5000,结束扇区是 10999,总计 6000 扇区。 聪明的你应该可以看出来,实际上每个 EBR 记录的 1st entry 记录了其代表的逻辑分区的扇区位置,而 2st entry 则记录了其下一个 EBR 记录(即下一个逻辑分区)的扇区位置,这样一个扩展分区的所有逻辑分区就可以通过 EBR 记录的 2st entry 链起来,形成一个链表。
5.2 GPT(GUID Partition Table)
todo 以后再补吧,毕竟不是玩 BIOS 的。
5.3 为什么要分区
- 性能问题:上面有提到,分区其实把硬盘上一段连续的柱面进行分区,以 linux 为例,现在有一个 5000 ~ 10000 柱面号的 /dev/sda1 分区我挂载到了 /data 目录下,有一个程序会会频繁的输出日志文件到 /data 目录下,这样我们的磁盘写入应该会集中在 5000 ~ 10000 这段连续的柱面内,能减少寻道时间,而寻道时间是磁盘 IO 耗时的罪魁祸首。
- 安全问题:以 Windows 系统为例,C 盘一般是系统盘,而 D 盘是数据盘,日常来说 D 盘应该是频繁使用的,当 D 盘的损坏时,不至于影响到 C盘系统盘,而 C 盘系统盘重装系统时也不会影响到 D 盘数据盘。
- 文件系统:不同的分区是可以格式化成不同的文件系统的。
文件系统
文件系统解决的问题是,高效且充分的利用硬盘提供的物理空间存储和检索管理文件。文件系统抽象出来了一个逻辑空间,这个逻辑空间类似一个数组,数组元素是一个逻辑块,文件系统以逻辑块为单位管理文件(硬盘以扇区为单位管理数据),逻辑块是连续的,但逻辑块只是一个逻辑概念,最终还是需要映射到物理块,物理块是 IO 的基本单位,即扇区。但是这个物理块是扇区吗?可以是柱面吗?都是,也可以都不是,这涉及文件系统的效率问题。
假设一个扇区是 512 byte 且一个柱面有 4 个扇区即 2 MB,若物理块是一个柱面,则一个 1 MB 的文件,就浪费了 50% 的空间,但是只需要一次的顺序 IO 就可以把这个文件读出来;若物理块是一个扇区,则该文件需要两个扇区,没有浪费一丝空间,但是这两个扇区不一定是在一个柱面上,因此读取这个文件时,相比柱面的盘片旋转延迟,还额外多了磁头寻道延迟(耗时大户)。因此,文件系统的时间效率和空间效率是冲突的。物理块的大小是文件系统在创建之初可以由用户指定的,但是一定是扇区大小的整数倍,用户自己在时间效率和空间使用效率取舍(这也是系统设计常见的问题)。
上图只是文件系统的简略架构,实际上的文件系统实现会更加的复杂,以 Linux 系统为例,其标准文件系统 Ext2
(The Second Extended File System)有 super block、inode、block group、fragment、directory 等诸多概念,还会涉及到虚拟文件系统、高速页缓存(Linux 内存管理相关)。
虚拟文件系统:众所周知,Linux 中一切皆文件,有普通文件、socket 文件等等,所以 Linux 有一个虚拟文件系统用于对文件进行抽象,即定义好 open、read、write、seek、close 等接口方法,不同的文件系统各自实现这些方法。
高速页缓存:硬盘鉴于其物理结构,数据的存取速度是非常慢的,内核希望尽可能少的访问低速的硬盘,因此硬盘上的数据是会映射到内存上的,当我们读取硬盘上某个扇区的数据时,如果内存中有映射到这一扇区的内存页,则直接访问该内存页就可以了;再比如说我们要写数据到硬盘上某个扇区时,如果内存中有映射到这一扇区的内存页,则会直接写入到该内存页中,由内核线程异步的把内存页的数据同步到硬盘上。
零拷贝
在 Linux 操作系统中,内存空间是分为内核态和用户态的,我们的网络IO、硬盘IO等等的设备IO的数据都需要从内核态拷贝到用户态或者从用户态拷贝到内核态,对于一些如消息队列、RPC框架等注重性能的中间件来说,这样的行为是不能接受的,那有没有办法避免数据的拷贝呢?答案是有的,就是『零拷贝』技术。
// todo
顺序 / 随机 「IO」
todo