磁盘读写

前言

本文主要是为后续涉及到的kafka,rocketmq,pulsar 现在较为流行的基于磁盘存储的消息队列做一个OS层的简单介绍。了解和回顾为什么基于磁盘这种比较“慢”的IO系统能够承载出要求高吞吐的消息队列系统。主要涉及到:文件读写和部分socket,仅仅针对linux系统。

文件系统

机械硬盘

img

上图就是一个大概的机械硬盘的内部组件,核心就是有个磁体臂带着磁头在磁盘感知或者修改当前磁盘位置上的磁体正负极,正负极是天然的二进制。通过感知正负极就能够了解当前位置的数据是0 还是 1 ,修改的话可以通过电流将磁体的正负极反转。不由得想起绝命毒师里面他们使用大的磁场销毁证据,但是现实生活中,用磁铁是很难将机械硬盘里面的数据进行修改的。google的[大佬 jeff](有谁可以介绍一下谷歌大牛Jeff Dean以及与他相关的事迹么? - 极客时间的回答 - 知乎 https://www.zhihu.com/question/22081653/answer/617167013) 曾遇到超新星爆炸导致计算机芯片中的数据从0变成1,这种基于底层的0 1 转换能够查出问题的简直是神人。

这里不再详细说明磁盘里面的材料和反转的细节实现,反正读数据就是获取当前磁体正负极,写数据就是根据电流将磁体修改为指定的正负极。整个过程中不是磁头臂在旋转,而是磁盘在旋转,磁头臂只是去对应的磁盘上读取数据。这也就有个磁道的概念,就是每一圈磁体,相同圈的磁体在不同磁盘上称之为柱面。

这里遇到一个问题,因为磁体只管读写,他的目标单位只是一圈的每一个磁体的正负极。不能每次写入都记录好当前的位置和下一个位置,这样的话,记录数据的元数据也是非常大的一个数据。于是有了扇区的概念,其实就是将磁道分成一段段的,每一段称之为一个扇区。
扇区

扇区是文件读取的最小单位,一般为512B,后面也有4KB,而磁盘的总容量就等于
$$
扇区容量 \times 扇区数 \times 柱面数 \times 盘面
$$
总的来说,每次读取或者写入,首先就是找到对应数据的扇区,然后将扇区里面的数据进行读写。这些扇区可能在一起,也可能没在一起。在操作系统看来,一个扇区才区区512字节,实在是有点太小了,于是根据局部性原理将一次性获取的数据设置在了4KB,一次性获取8个扇区。这里涉及到的就是文件系统和机械硬盘对数据进行管理的抽象,在文件系统,最小的读写单元是4Kb,即一块,在机械硬盘是8个扇区也就是可能是连续或者非连续的8个扇区。

固态硬盘

固态硬盘相对机械硬盘就是快,体积也小很多,没有深入了解过。。

在机械硬盘的介绍中,可以看出来,一次机械硬盘的读取可能涉及到

  • 寻道时间
  • 等待磁盘旋转可能时间
  • 传输数据时间

传送数据是肯定无法进行优化的,但是如果我们能缩短上面两个的时间,那么可以将硬盘的效率拉高。如果能够顺序的写入数据,那么当前的写入就基本上没有了寻道的时间了。所以顺序写入的速度远远高于随机写入的原因就在于此。

操作系统读写文件

读写过程

这里不赘述文件系统的详细读写过程,只是大概说下文件读写过程中会经历的过程。根据冯诺依曼的计算机架构。

img

冯诺依曼结构

计算单元和控制单元CPU只会主存(内存)进行数据交互。如果从键盘输入到显示器显示,首先是连接输入设备,打开键盘的io,键盘开始写入,数据被放到内存中,涉及到解码编码等,输出到显示器。这里会涉及到一个问题,CPU何时知道输入数据了?答案是中断,每次写入完成的时候会发送一个中断给CPU,CPU根据中断的类型做出对应的行为。如果当前我们输入M这个字母,首先有M输入这个事件发生会触发输入的中断,操作系统接收到这个中断,保存当前执行的上下文,然后调用输入的中断程序,将数据M进行处理,将他处理为能被输出设备解析的字符,然后发送给输出端缓冲区,最后被输出端显示。也是因为这个原因,我们可以实时的感知到我们输入的数据,因为这个是按照字符来的。

01-04 system hardwares

如果是执行已经在计算机磁盘上的数据呢?比如运行hello world的程序,首先键盘输入指令,shell 程序会将字符逐一的输出到显示器上,同时这个数据被放入内存中,当我们回车后,shell 知道当前指令结束,开始执行hello world 的程序。一般情况下,肯定是需要整个程序被写入内存才开始执行,如果是学键盘,每个字符一个中断,那么这个中断的数据量可能是非常庞大的。于是有个DMA( 直接存储读取)技术,该技术可以让CPU告诉IO,我需要哪里的数据,IO你负责去搬运,搬运好了放在某个内存中。这样数据在读开始和结束两个中断就搞定了。

操作系统内核态和用户态

CPU 虽然很高速,但是其实是很傻的,就只会勤勤恳恳地按照指令执行,而有些指令是很危险的,比如将内存里面的数据全部清理,删除xxx核心文件等。Linux 将权限分为两种,一种是可以使用所有指令的核心态权限,还有一种是只能执行CPU普通指令级的数据,其他的如IO读写,网卡访问,甚至申请内存等等都不允许执行。但是运行的程序必然需要和其他的IO进行一个交互,如何交互呢?答案就是系统调用。

操作系统

从程序的角度,如果发送了系统调用,就是程序主动从用户态切换到核心态。这个切换的过程涉及到

  • 保留用户态现场(上下文,寄存器,栈等)
  • 复制系统调用参数到内核栈空间,进入内核 态
  • 检查
  • 执行内核态
  • 复制返回结果,切换到用户态
  • 恢复用户态现场

大家可以看到,如果说需要去磁盘或者网卡读数据,首先是将当前的上下文保存好,然后等到系统调用完成,复制数据。最后拿到数据。那么数据其实有两份在内存中,而且上下文切换相对是比较昂贵的操作。如何能够减少这个时间呢?

优化措施

虚拟内存

为了和物理地址解耦,操作系统对外提供的是虚拟地址。这样的好处就是如果升级了内存,除非超过当前能够表示的最大值,如32位最大能到4G内存,如果从2G升级到4G内存,那么操作系统不需要做额外的数据更改。还有就是可以做到多对一,多个虚拟地址可以指向一个物理地址。让进程以为自己对某个值是独占的,其实是共享的。看上去很危险,但是在内核态和用户态而言,可能仅仅需要将两边的虚拟地址修改到同一物理地址就能够实现数据共享,而不是说真的将数据完全复制一遍。

DMA

上文已经提到过DMA,即直接内存访问。在没有DMA技术的时候,CPU和IO的交互都是通过系统总线连接并且传输。在数据传输完成之前,CPU是一直处于等待的状态,CPU 本来就是较为昂贵的资源,这无疑会影响其他正在运行的程序。

DMA 就是将IO数据的拷贝进行了外包,每次CPU需要读取数据,会给DMA发送一个IO请求,DMA 会从IO设备的数据缓冲区读取数据,等到读取到CPU告诉的数据后,发送数据读取完毕的信号。

img

这样就释放出了读取数据阶段的CPU,通过中断的方式唤醒CPU。这里数据读取完的信号,其实也是中断,不过是CPU的中断,这种中断只是将CPU处理的上下文进行一个切换,其实就是保存当前CPU寄存器中的数据,而不需要学用户态和内核态一样需要保存当前的用户态堆栈等信息。

减少数据拷贝
mmap

上文提到,如果当前程序需要获取IO里的数据,需要切换到内核态,等内核收到数据后,再将数据从内核态拷贝到用户态,一次读取都涉及到2次数据拷贝,如果是一次读写,就涉及到4次数据拷贝。然后结合上文的虚拟内存,就可以减少数据的拷贝。这个技术叫MMAP 即内存映射

img

粗暴的理解就是,用户态在写入数据的时候是和内核态共享的一块内存。

  • 用户调用mmap方法,进行系统调用,进入内核态
  • CPU 使用DMA 技术从硬件中获取数据到内核缓冲区,返回
  • 切换 回到用户态

这里只涉及到两次切换和一次DMA 数据拷贝。如果需要将当前的文件发送,则。

  • 用户调用write ,再次切换成内核态
  • CPU 将上文的数据拷贝到socket缓冲区中
  • CPU 将数据从socket缓冲区拷贝到网卡,发回
  • 切换回用户态,返回write

需要注意的是,mmap 方法被调用的时候并没有写入数据,首先该方法会返回一个指向进程逻辑地址空间的地址,然后进程不需要调用read或者write,只需要通过指针操作文件。当操作进行的时候,该地址其实没有数据,所以会触发缺页中断,缺页中断发生后,操作系统会将数据写入到这个页中进行真正的数据copy,图片来源

img

从读写的过程中,当前一共执行了3次数据拷贝,4次上下文切换,3次中只有一次需要CPU,其他的都是通过DMA来进行的。

sendfile

从上文可以看到,当前仍然进行了4次切换,sendfile进一步减少了这个上下文切换。DMA copy数据不再需要mmap返回后触发缺页这么获取数据,而是调用后就可以实现两个文件之间的传输。

img

因为mmap主要是简历了虚拟地址和实际地址的映射,需要用户态和内核态,但是sendfile 确实直接将两个文件连在一起,不在通过虚拟内存映射同一块地址了。但是仍然需要cpu将内核态的数据copy到缓冲区。

  • 用户态发起sendfile 系统调用,切换到内核态
  • DMA 拷贝数据到内核态
  • CPU拷贝数到socket缓冲区
  • 完成,切换回用户态
SG-DMA

如果说当前网卡支持SG-DMA 功能,能够进一步减少数据的复制。

zero-copy

零拷贝其实主要指的是数据在内核态和用户态的数据备份,或者说CPU是否进行了数据拷贝。核心是因为前期为了安全,将操作限制的比较全,部分只读或者只用于传送的数据是没有必要定义这么死板的。

senfile-零拷贝

总结

本文主要介绍了一些文件读写和socket读写的基础概念,并没有很深入的探讨,甚至可能有自己理解不到位的地方。

note: 2023-07-13