18

聊聊Linux IO

 5 years ago
source link: https://www.tuicool.com/articles/QbEBv2n
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

本文转载自https://0xffffff.org/2017/05/01/41-linux-io/

作者: 浅墨

点击上方" 程序员历小冰 ",选择“置顶或者星标”

你的关注意义重大!

6zUVbqz.jpg!web

最近在看 Redis 持久化的相关原理,antirez 在他的 《Redis 持久化解密》 (链接文末) 一文中说过,数据库中 带有持久化的写操作分为如下几个步骤:

  • 1.客户端发送写操作命令和数据;(数据在客户端内存)

  • 2.服务端通过网络收到客户端发来的写操作和数据;(数据在服务端内存)

  • 3.服务端修改内存中的数据,同时调用系统函数 write 进行操作,将数据往磁盘中写;(数据在服务端的系统内存缓冲区)

  • 4.操作系统将缓冲区中的数据转移到磁盘控制器上(数据在磁盘缓存中)

  • 5.磁盘控制器将数据写到磁盘的物理介质中(数据真正落到磁盘上)

对于持久化,我们需要分析下面两种情况时能否保证数据不丢失:

  • 数据库异常崩溃

  • 整个系统断电

对于 Redis 这种应用程序(数据库服务)来说,只要步骤3操作成功,即使数据库崩溃,数据也会有内核保证写入到磁盘中,不会发生丢失;但是如果整个系统断电这种场景来说,必须要等到步骤5操作成功,才可认为写操作是真正的持久化成功。

而从步骤3到步骤5中间会涉及到大量 Linux IO 的原理,特别是 Page CacheBuffer Cache 等缓存。我在网上搜索相关学习资料很久,发现本篇文章十分详尽,争取原作者同意,并且其 博客最下面有协议,非商业使用可以随意署名转载,所以就转载过来,供大家一起学习。

文章比较长,所以我进行一部分删减,列出剩下内容中比较重要的知识点,大家可以选取自己感兴趣的部分进行阅读,节约大家的时间。

  1. Linux IO 缓存体系,stdio和内核缓存的区别, Page Cache Buffer Cache 的区别。

  2. Buffered IO、 mmap(2) 、Direct IO的区别。

  3. Write ThroughWrite back 两种缓存更新策略。

写在前边

在开始正式的讨论前,我先抛出几个问题:

  • 谈到磁盘时,常说的 HDD 磁盘和 SSD 磁盘最大的区别是什么?这些差异会影响我们的系统设计吗?

  • 单线程写文件有点慢,那多开几个线程一起写是不是可以加速呢?

  • write(2) 函数成功返回了,数据就已经成功写入磁盘了吗?此时设备断电会有影响吗?会丢失数据吗?

  • write(2) 调用是原子的吗?多线程写文件是否要对文件加锁?有没有例外,比如 O_APPEND 方式?

  • 坊间传闻, mmap(2) 的方式读文件比传统的方式要快,因为少一次拷贝。真是这样吗?为什么少一次拷贝?

如果你觉得这些问题都很简单,都能很明确的回答上来。那么很遗憾这篇文章不是为你准备的,你可以关掉网页去做其他更有意义的事情了。如果你觉得无法明确的回答这些问题,那么就耐心地读完这篇文章,相信不会浪费你的时间。

受限于个人时间和文章篇幅,部分议题如果我不能给出更好的解释或者已有专业和严谨的资料,就只会给出相关的参考文献的链接,请读者自行参阅。

言归正传,我们的讨论从存储器的层次结构开始。

存储器的金字塔结构

3IjuumZ.jpg!web

受限于存储介质的存取速率和成本,现代计算机的存储结构呈现为金字塔型[1]。越往塔顶,存取效率越高、但成本也越高,所以容量也就越小。得益于程序访问的局部性原理[2],这种节省成本的做法也能取得不俗的运行效率。从存储器的层次结构以及计算机对数据的处理方式来看,上层一般作为下层的 Cache 层来使用(广义上的 Cache )。比如寄存器缓存 CPU Cache 的数据,CPU Cache L1~L3层视具体实现彼此缓存或直接缓存内存的数据,而内存往往缓存来自本地磁盘的数据。

本文主要讨论磁盘IO操作,故只聚焦于 Local Disk 的访问特性和其与 DRAM 之间的数据交互。

无处不在的缓存

yyq2aiJ.jpg!web

如图,当程序调用各类文件操作函数后,用户数据(User Data)到达磁盘(Disk)的流程如图所示[3]。图中描述了 Linux 下文件操作函数的层级关系和内存缓存层的存在位置。中间的黑色实线是用户态和内核态的分界线。

从上往下分析这张图,首先是C语言 stdio 库定义的相关文件操作函数,这些都是用户态实现的跨平台封装函数。stdio中实现的文件操作函数有自己的stdio buffer,这是在用户态实现的缓存。此处使用缓存的原因很简单——系统调用总是昂贵的。如果用户代码以较小的 size 不断的读或写文件的话,stdio 库将多次的读或者写操作通过buffer进行聚合是可以提高程序运行效率的。stdio库同时也支持 fflush(3) 函数来主动的刷新 buffer,主动的调用底层的系统调用立即更新 buffer 里的数据。特别地, setbuf(3) 函数可以对 stdio 库的用户态 buffer 进行设置,甚至取消 buffer 的使用。

系统调用的 read(2)/write(2) 和真实的磁盘读写之间也存在一层 buffer,这里用术语Kernel buffer cache来指代这一层缓存。在Linux下,文件的缓存习惯性的称之为 Page Cache ,而更低一级的设备的缓存称之为 Buffer Cache . 这两个概念很容易混淆,这里简单的介绍下概念上的区别: Page Cache 用于缓存文件的内容,和文件系统比较相关。文件的内容需要映射到实际的物理磁盘,这种映射关系由文件系统来完成; Buffer Cache 用于缓存存储设备块(比如磁盘扇区)的数据,而不关心是否有文件系统的存在(文件系统的元数据缓存在 Buffer Cache 中)。

综上,既然讨论 Linux 下的 IO 操作,自然是跳过 stdio 库的用户态这一堆东西,直接讨论系统调用层面的概念了。对 stdio 库的 IO 层有兴趣的同学可以自行去了解。从上文的描述中也介绍了文件的内核级缓存是保存在文件系统的 Page Cache 中的。所以后面的讨论基本上是讨论 IO 相关的系统调用和文件系统 Page Cache 的一些机制。

Linux内核中的IO栈

这一小节来看 Linux 内核的 IO 栈的结构。先上一张全貌图[4]:

JjqyUrJ.jpg!web

由图可见,从系统调用的接口再往下,Linux下的 IO 栈致大致有三个层次:

  1. 文件系统层,以  write(2)  为例,内核拷贝了 write(2) 参数指定的用户态数据到文件系统 Cache 中,并适时向下层同步

  2. 块层,管理块设备的 IO 队列,对 IO 请求进行合并、排序(还记得操作系统课程学习过的IO调度算法吗?)

  3. 设备层,通过 DMA 与内存直接交互,完成数据和具体设备之间的交互

结合这个图,想想Linux系统编程里用到的Buffered IO、 mmap(2) 、Direct IO,这些机制怎么和 Linux IO 栈联系起来呢?上面的图有点复杂,我画一幅简图,把这些机制所在的位置添加进去:

QVrEvmn.jpg!web

这下一目了然了吧?传统的 Buffered IO 使用 read(2) 读取文件的过程什么样的?假设要去读一个冷文件(Cache中不存在), open(2) 打开文件内核后建立了一系列的数据结构,接下来调用 read(2) ,到达文件系统这一层,发现 Page Cache 中不存在该位置的磁盘映射,然后创建相应的 Page Cache 并和相关的扇区关联。然后请求继续到达块设备层,在 IO 队列里排队,接受一系列的调度后到达设备驱动层,此时一般使用DMA方式读取相应的磁盘扇区到 Cache 中,然后 read(2) 拷贝数据到用户提供的用户态buffer 中去( read(2) 的参数指出的)

整个过程有几次拷贝?从磁盘到 Page Cache 算第一次的话,从 Page Cache 到用户态buffer就是第二次了。而 mmap(2) 做了什么? mmap(2) 直接把 Page Cache 映射到了用户态的地址空间里了,所以 mmap(2) 的方式读文件是没有第二次拷贝过程的。那Direct IO做了什么?这个机制更狠,直接让用户态和块 IO 层对接,直接放弃 Page Cache ,从磁盘直接和用户态拷贝数据。好处是什么?写操作直接映射进程的buffer到磁盘扇区,以 DMA 的方式传输数据,减少了原本需要到 Page Cache 层的一次拷贝,提升了写的效率。对于读而言,第一次肯定也是快于传统的方式的,但是之后的读就不如传统方式了(当然也可以在用户态自己做 Cache,有些商用数据库就是这么做的)。

除了传统的 Buffered IO 可以比较自由的用偏移+长度的方式读写文件之外, mmap(2) 和 Direct IO 均有数据按页对齐的要求,Direct IO 还限制读写必须是底层存储设备块大小的整数倍(甚至 Linux 2.4 还要求是文件系统逻辑块的整数倍)。所以接口越来越底层,换来表面上的效率提升的背后,需要在应用程序这一层做更多的事情。所以想用好这些高级特性,除了深刻理解其背后的机制之外,也要在系统设计上下一番功夫。

Page Cache 的同步

广义上 Cache 的同步方式有两种,即 Write Through (写穿)和 Write back (写回). 从名字上就能看出这两种方式都是从写操作的不同处理方式引出的概念(纯读的话就不存在 Cache 一致性了,不是么)。对应到 Linux 的 Page Cache 上所谓 Write Through 就是指 write(2) 操作将数据拷贝到 Page Cache 后立即和下层进行同步的写操作,完成下层的更新后才返回。而 Write back 正好相反,指的是写完 Page Cache 就可以返回了。 Page Cache 到下层的更新操作是异步进行的。

Linux下Buffered IO默认使用的是 Write back 机制,即文件操作的写只写到 Page Cache 就返回,之后 Page Cache 到磁盘的更新操作是异步进行的。 Page Cache 中被修改的内存页称之为脏页(Dirty Page),脏页在特定的时候被一个叫做 pdflush (Page Dirty Flush)的内核线程写入磁盘,写入的时机和条件如下:

  1. 当空闲内存低于一个特定的阈值时,内核必须将脏页写回磁盘,以便释放内存。

  2. 当脏页在内存中驻留时间超过一个特定的阈值时,内核必须将超时的脏页写回磁盘吧

  3. 用户进程调用 sync(2) fsync(2) fdatasync(2) 系统调用时,内核会执行相应的写回操作。

刷新策略由以下几个参数决定(数值单位均为1/100秒):

默认是写回方式,如果想指定某个文件是写穿方式呢?即写操作的可靠性压倒效率的时候,能否做到呢?当然能,除了之前提到的 fsync(2) 之类的系统调用外,在 open(2) 打开文件时,传入 O_SYNC 这个 flag 即可实现。这里给篇参考文章[5],不再赘述(更好的选择是去读TLPI相关章节)。

文件读写遭遇断电时,数据还安全吗?相信你有自己的答案了。使用 O_SYNC 或者 fsync(2) 刷新文件就能保证安全吗?现代磁盘一般都内置了缓存,代码层面上也只能讲数据刷新到磁盘的缓存了。当数据已经进入到磁盘的高速缓存时断电了会怎么样?这个恐怕不能一概而论了。不过可以使用 hdparm -W0 命令关掉这个缓存,相应的,磁盘性能必然会降低。

写在最后

每天抽出不到半个小时,零零散散地写了一周,这是说是入门都有些谬赞了,只算是对Linux下的 IO 机制稍微深入的介绍了一点。无论如何,希望学习完 Linux 系统编程的同学,能继续的往下走一走,尝试理解系统调用背后隐含的机制和原理。探索的结果无所谓,重要的是探索的过程以及相关的学习经验和方法。前文提出的几个问题我并没有刻意去解答所有的,但是读到现在,不知道你自己能回答上几个了?

-关注我

uMzIbeN.jpg!web

推荐阅读

TCP/IP的底层队列

基于Redis和Lua的分布式限流

AbstractQueuedSynchronizer超详细原理解析

TCP拥塞控制算法简介

参考文献

[1] 图片引自《Computer Systems: A Programmer’s Perspective》Chapter 6 The Memory Hierarchy, 另可参考https://zh.wikipedia.org/wiki/%E5%AD%98%E5%82%A8%E5%99%A8%E5%B1%B1

[2] Locality of reference,https://en.wikipedia.org/wiki/Locality_of_reference

[3] 图片引自《The Linux Programming Interface》Chapter 13 FILE I/O BUFFERING

[4] Linux Storage Stack Diagram, https://www.thomas-krenn.com/en/wiki/Linux_Storage_Stack_Diagram

[5] O_DIRECT和O_SYNC详解, http://www.cnblogs.com/suzhou/p/5381738.html

[6] http://librelist.com/browser/usp.ruby/2013/6/5/o-append-atomicity/

[7] Coding for SSD, https://dirtysalt.github.io/coding-for-ssd.html

[8] fio作者Jens Axboe是Linux内核IO部分的maintainer,工具主页 http://freecode.com/projects/fio/

[9] How to benchmark disk, https://www.binarylane.com.au/support/solutions/articles/1000055889-how-to-benchmark-disk-i-o

[10] 深入Linux内核架构, (德)莫尔勒, 人民邮电出版社

redis 持久化解密 http://oldblog.antirez.com/post/redis-persistence-demystified.html


Recommend

  • 131
    • www.infoq.com 6 years ago
    • Cache

    聊聊Apache Arrow

    聊聊 Apache Arrow基本介绍Apache Arrow 是一种基于内存的列式数据结构,正像上面这张图的箭头,它的出现就是为了解决系统到系统之间的数据传输问题,2016 年 2 月...

  • 212
    • 掘金 juejin.im 6 years ago
    • Cache

    聊聊Vue.js的template编译

    因为对Vue.js很感兴趣,而且平时工作的技术栈也是Vue.js,这几个月花了些时间研究学习了一下Vue.js源码,并做了总结与输出。 文章的原地址:

  • 106
    • 掘金 juejin.im 6 years ago
    • Cache

    聊聊全站HTTPS带来的技术挑战

       昨天写的文章里了讨论了数据传输的安全性的问题,最后一部分提到了通过HTTPS解决数据传输安全性的方案。那么一个新问题又来了,实施全站HTTPS的过程中,我们可能会遇到哪些技术问题?所以我今天和大家一起来算一下这个账,将技术成本理清楚。...

  • 172

    因为对Vue.js很感兴趣,而且平时工作的技术栈也是Vue.js,这几个月花了些时间研究学习了一下Vue.js源码,并做了总结与输出。 文章的原地址:

  • 120

    本文主要聊一聊主流开源产品的replication方式。 replication replication和partition/sharding是分布式系统必备的两种能力。具体详见复制、分片和路由.

  • 82

    本文主要聊一下开源主流产品的partition方式。 partition 一般来说,数据库的繁忙体现在:不同用户需要访问数据集中的不同部分,这种情况下,我们把数据的各个部分存放在不同的服务器/节点中,每个服务器/节点负责自身数据的读取与写入...

  • 110
    • www.infoq.com 6 years ago
    • Cache

    随笔,聊聊架构

    最近刚读完《聊聊架构》,我也多次在微信朋友圈推荐过本书;推荐的原因不是因为行文优美流畅,也不是因为它是什么名篇巨著,仅仅是因为它回答了很多困扰我许久的问题,让我重新思考软件工程、架构和软件本身。

  • 127

    Spring Cloud作为一套微服务治理的框架,几乎考虑到了微服务治理的方方面面,之前也写过一些关于Spring Cloud文章,主要偏重各组件的使用,本篇主要解答这两个问题:Spring Cloud在微服务的架构中都做了哪些事情?Spring Cloud提供的这些功能对微服务的架构提供了怎...

  • 4

    作者:小牛呼噜噜 | https://xiaoniuhululu.com 计算机内功、JAVA底层、面试相关资料等更多精彩文章在公众号「小牛呼噜噜 」 什么是CPU上...

  • 2

    聊聊Linux中线程和进程的联系与区别! 作者:张彦飞allen 2022-10-12 09:01:52 在 Linux 内核中并没有对线程做特殊处理,还是由 task_struct 来管理。从内核的角度看,用户态的线程本质上还是一个进程。只不过和普...

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK