浅析Linux IO

2024-09-02 09:58
文章标签 linux 浅析 io

本文主要是介绍浅析Linux IO,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

点击上方“朱小厮的博客”,选择“设为星标”

后台回复”加群“获取公众号专属群聊入口

来源:0xffffff.org/

写在前面

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

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

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

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

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

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

如果你觉得这些问题都很简单,都能很明确的回答上来。那么很遗憾这篇文章不是为你准备的,你可以关掉网页去做其他更有意义的事情了。如果你觉得无法明确的回答这些问题,那么就耐心地读完这篇文章,相信不会浪费你的时间。受限于个人时间和文章篇幅,部分议题如果我不能给出更好的解释或者已有专业和严谨的资料,就只会给出相关的参考文献的链接,请读者自行参阅。言归正传,我们的讨论从存储器的层次结构开始。

存储器的金字塔结构

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

无处不在的缓存

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

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

系统调用的read/write和真实的磁盘读写之间也存在一层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栈的结构。先上一张全貌图:

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

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

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

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

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

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

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

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

Page Cache的同步

广义上Cache的同步方式有两种,即Write Through(写穿)和Write back(写回). 从名字上就能看出这两种方式都是从写操作的不同处理方式引出的概念(纯读的话就不存在Cache一致性了,不是么)。对应到Linux的Page Cache上所谓Write Through就是指write操作将数据拷贝到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)的内核线程写入磁盘,写入的时机和条件如下:

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

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

  • 用户进程调用sync、fsync、fdatasync系统调用时,内核会执行相应的写回操作。

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

# flush每隔5秒执行一次
root@082caa3dfb1d / $ sysctl vm.dirty_writeback_centisecs
vm.dirty_writeback_centisecs = 500
# 内存中驻留30秒以上的脏数据将由flush在下一次执行时写入磁盘
root@082caa3dfb1d / $ sysctl vm.dirty_expire_centisecs
vm.dirty_expire_centisecs = 3000
# 若脏页占总物理内存10%以上,则触发flush把脏数据写回磁盘
root@082caa3dfb1d / $ sysctl vm.dirty_background_ratio
vm.dirty_background_ratio = 10

默认是写回方式,如果想指定某个文件是写穿方式呢?即写操作的可靠性压倒效率的时候,能否做到呢?当然能,除了之前提到的fsync之类的系统调用外,在open打开文件时,传入O_SYNC这个flag即可实现。

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

文件操作与锁

当多个进程/线程对同一个文件发生写操作的时候会发生什么?如果写的是文件的同一个位置呢?这个问题讨论起来有点复杂了。首先write调用不是原子操作,当多个write操作对一个文件的同一部分发起写操作的时候,情况实际上和多个线程访问共享的变量没有什么区别。按照不同的逻辑执行流,会有很多种可能的结果。也许大多数情况下符合预期,但是本质上这样的代码是不可靠的。

特别的,文件操作中有两个操作是内核保证原子的。分别是open调用的O_CREAT和O_APPEND这两个flag属性。前者是文件不存在就创建,后者是每次写文件时把文件游标移动到文件最后追加写(NFS等文件系统不保证这个flag)。有意思的问题来了,以O_APPEND方式打开的文件write操作是不是原子的?文件游标的移动和调用写操作是原子的,那写操作本身会不会发生改变呢?有的开源软件比如apache写日志就是这样写的,这是可靠安全的吗?坦白讲我也不清楚,有人说Then O_APPEND is atomic and write-in-full for all reasonably-sized> writes to regular files.但是我也没有找到很权威的说法。这里给出一个邮件列表上的讨论,可以参考下:http://librelist.com/browser/usp.ruby/2013/6/5/o-append-atomicity/。今天先放过去,后面有时间的话专门研究下这个问题。如果你能给出很明确的说法和证明,还望不吝赐教。

Linux下的文件锁有两种,分别是flock的方式和fcntl的方式,前者源于BSD,后者源于System V,各有限制和应用场景。老规矩,TLPI上讲的很清楚的这里不赘述。我个人是没有用过文件锁的,系统设计的时候一般会避免多个执行流写一个文件的情况,或者在代码逻辑上以mutex加锁,而不是直接加锁文件本身。数据库场景下这样的操作可能会多一些(这个纯属臆测),这就不是我了解的范畴了。

磁盘的性能测试

在具体的机器上跑服务程序,如果涉及大量IO的话,首先要对机器本身的磁盘性能有明确的了解,包括不限于IOPS、IO Depth等等。这些数据不仅能指导系统设计,也能帮助资源规划以及定位系统瓶颈。由于磁盘的工作原理不同,机械磁盘需要旋转来寻找数据存放的磁道,所以其随机存取的效率受到了“寻道时间”的严重影响,远远小于连续存取的效率;而SSD磁盘读写任意扇区可以认为是相同的时间,随机存取的性能远远超过机械盘。所以呢,在机械磁盘作为底层存储时,如果一个线程写文件很慢的话,多个线程分别去写这个文件的各个部分能否加速呢?不见得吧?如果这个文件很大,各个部分的寻道时间带来极大的时间消耗的话,效率就很低了(先不考虑Page Cache)。SSD呢?可以明确,设计合理的话,SSD多线程读写文件的效率会高于单线程。当前的SSD盘很多都以高并发的读取为卖点的,一个线程压根就喂不饱一块SSD盘。一般SSD的IO Depth都在32甚至更高,使用32或者64个线程才能跑满一个SSD磁盘的带宽(同步IO情况下)。

具体的SSD原理不在本文计划内,这里给出一篇详细的参考文章:https://dirtysalt.github.io/coding-for-ssd.html。有时候一些文章中所谓的STAT磁盘一般说的就是机械盘(虽然STAT本身只是一个总线接口)。接口会影响存储设备的最大速率,基本上是STAT -> PCI-E -> NVMe的发展路径,具体请自行Google了解。

具体的设备一般使用fio工具来测试相关磁盘的读写性能。fio的介绍和使用教程有很多,不再赘述。这里不想贴性能数据的原因是存储介质的发展实在太快了,一方面不想贴某些很快就过时的数据以免让初学者留下不恰当的第一印象,另一方面也希望读写自己实践下fio命令。

前文提到存储介质的原理会影响程序设计,我想稍微的解释下。这里说的“影响”不是说具体的读写能到某个速率,程序中就依赖这个数值,换个工作环境就性能大幅度降低(当然,为专门的机型做过优化的结果很可能有这个副作用)。而是说根据存储介质的特性,程序的设计起码要遵循某个设计套路。举个简单的例子,SATA机械盘的随机存取很慢,那系统设计时,就要尽可能的避免随机的IO出现,尽可能的转换成连续的文件存取来加速运行。比如Google的LevelDB就是转换随机的Key-Value写入为Binlog(连续文件写入)+ 内存插入MemTable(内存随机读写可以认为是O(1)的性能),之后批量dump到磁盘(连续文件写入)。这种LSM-Tree的设计便是合理的利用了存储介质的特性,做到了最大化的性能利用(磁盘换成SSD也依旧能有很好的运行效率)。

想知道更多?描下面的二维码关注我

后台回复”加群“获取公众号专属群聊入口

字节跳动2020春季实习生招聘及校招全职补录全面启动!

【精彩推荐】

  • 咱们从头到尾说一次Java垃圾回收

  • Netty、Kafka中的零拷贝技术到底有多牛?

  • go为什么这么快?

  • 面试前,我们要复习多少Redis知识?

  • 《深入理解Java虚拟机》第2版挖的坑终于在第3版中被R大填平了

  • Redis性能问题分析

  • 浅谈CAP和Paxos共识算法

  • 聊一聊Java中的文件锁

  • Kafka为什么这么快?

  • Paxos、Raft不是一致性算法嘛?

  • 聊聊Java的几把JVM级锁

朕已阅 

这篇关于浅析Linux IO的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1129646

相关文章

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

linux-基础知识3

打包和压缩 zip 安装zip软件包 yum -y install zip unzip 压缩打包命令: zip -q -r -d -u 压缩包文件名 目录和文件名列表 -q:不显示命令执行过程-r:递归处理,打包各级子目录和文件-u:把文件增加/替换到压缩包中-d:从压缩包中删除指定的文件 解压:unzip 压缩包名 打包文件 把压缩包从服务器下载到本地 把压缩包上传到服务器(zip

Linux 网络编程 --- 应用层

一、自定义协议和序列化反序列化 代码: 序列化反序列化实现网络版本计算器 二、HTTP协议 1、谈两个简单的预备知识 https://www.baidu.com/ --- 域名 --- 域名解析 --- IP地址 http的端口号为80端口,https的端口号为443 url为统一资源定位符。CSDNhttps://mp.csdn.net/mp_blog/creation/editor

【Python编程】Linux创建虚拟环境并配置与notebook相连接

1.创建 使用 venv 创建虚拟环境。例如,在当前目录下创建一个名为 myenv 的虚拟环境: python3 -m venv myenv 2.激活 激活虚拟环境使其成为当前终端会话的活动环境。运行: source myenv/bin/activate 3.与notebook连接 在虚拟环境中,使用 pip 安装 Jupyter 和 ipykernel: pip instal

Linux_kernel驱动开发11

一、改回nfs方式挂载根文件系统         在产品将要上线之前,需要制作不同类型格式的根文件系统         在产品研发阶段,我们还是需要使用nfs的方式挂载根文件系统         优点:可以直接在上位机中修改文件系统内容,延长EMMC的寿命         【1】重启上位机nfs服务         sudo service nfs-kernel-server resta

【Linux 从基础到进阶】Ansible自动化运维工具使用

Ansible自动化运维工具使用 Ansible 是一款开源的自动化运维工具,采用无代理架构(agentless),基于 SSH 连接进行管理,具有简单易用、灵活强大、可扩展性高等特点。它广泛用于服务器管理、应用部署、配置管理等任务。本文将介绍 Ansible 的安装、基本使用方法及一些实际运维场景中的应用,旨在帮助运维人员快速上手并熟练运用 Ansible。 1. Ansible的核心概念

Linux服务器Java启动脚本

Linux服务器Java启动脚本 1、初版2、优化版本3、常用脚本仓库 本文章介绍了如何在Linux服务器上执行Java并启动jar包, 通常我们会使用nohup直接启动,但是还是需要手动停止然后再次启动, 那如何更优雅的在服务器上启动jar包呢,让我们一起探讨一下吧。 1、初版 第一个版本是常用的做法,直接使用nohup后台启动jar包, 并将日志输出到当前文件夹n

[Linux]:进程(下)

✨✨ 欢迎大家来到贝蒂大讲堂✨✨ 🎈🎈养成好习惯,先赞后看哦~🎈🎈 所属专栏:Linux学习 贝蒂的主页:Betty’s blog 1. 进程终止 1.1 进程退出的场景 进程退出只有以下三种情况: 代码运行完毕,结果正确。代码运行完毕,结果不正确。代码异常终止(进程崩溃)。 1.2 进程退出码 在编程中,我们通常认为main函数是代码的入口,但实际上它只是用户级

【Linux】应用层http协议

一、HTTP协议 1.1 简要介绍一下HTTP        我们在网络的应用层中可以自己定义协议,但是,已经有大佬定义了一些现成的,非常好用的应用层协议,供我们直接使用,HTTP(超文本传输协议)就是其中之一。        在互联网世界中,HTTP(超文本传输协议)是一个至关重要的协议,他定义了客户端(如浏览器)与服务器之间如何进行通信,以交换或者传输超文本(比如HTML文档)。