本文转载自Linux/UNIX编程如何保证文件落盘

导语

我们编写程序write数据到文件中时,其实数据不会立马写入磁盘,而是会经过层层缓存。每层缓存都有自己的刷新时机,每层缓存都刷新后才会写入磁盘。这些缓存的存在是为了加速读写操作,因为如果每次读写都对应真实磁盘操作,那么读写的效率会大大降低。带来的坏处是如果期间发生掉电或者别的故障,还未写入磁盘的数据就丢失了。对于数据安全敏感的应用,比如数据库,比如交易程序,这是无法忍受的。所以操作系统提供了保证文件落盘的机制。我们来看下这些机制的原理和使用。

I/O缓冲区机制



图片来自:https://lwn.net/Articles/457667/

上图说明了操作系统到磁盘的数据流,以及经过的缓冲区。首先数据会先存在于应用的内存空间,如果调用库函数写入,库函数可能还会把数据缓存在库函数所维护的缓冲区空间中,比如C标准库stdio提供的方法就会进行缓存,目的是为了减少系统调用的次数。这两个缓存都是在用户空间中的。库函数缓存刷新时,会调用write系统调用写入内核空间,内核同样维护了一个页缓存(page cache),操作系统会在合适的时间把脏页的数据写入磁盘。即使是写入磁盘了,磁盘也可能维护了一个缓存,在这个时候掉电依然会丢失数据的,只有写入了磁盘的持久存储物理介质上,数据才是真正的落盘了,是安全的。我们接下来就是要研究如何做到这一步。

用户空间缓冲区

用户空间的缓存分为应用程序本身维护的缓冲区与库维护的缓冲区。

应用本身维护的缓冲区需要开发者自己刷新,调用库函数写入到库函数的缓冲区中。如果应用程序不依赖任何库函数,而是直接使用系统调用,那么则是把数据写入系统的缓冲区去。

库函数一般都会维护缓冲区,目的是简化应用程序的编写,应用程序就不需要编写维护缓冲区的代码,同时性能也得到了提高,因为缓冲区大大减少了系统调用的次数,而系统调用是非常耗时的,系统调用涉及到用户态到内核态的切换,这个切换需要很多的步骤与校验,较为耗时。

比如C标准库stdio就维护着一个缓冲区,对应这个缓冲区,C标准库提供了fflush方法强制把缓冲区数据写入操作系统。

Java的OutputStream接口提供了一个flush方法,具体的作用要看实现类的具体实现。BufferedOutputStream#flush就会把自己维护的缓冲区数据写入下一层的OutputStream。如果是new BufferedOutputStream(new FileOutputStream("/"))这样的模式,则调用BufferedOutputStream#flush会将数据写入操作系统。

内核缓冲区

应用程序直接或者通过库函数间接的使用系统调用write将数据写入操作系统缓冲区。

UNIX系统在内核中设有高速缓存或页面高速缓存。目的是为了减少磁盘读写次数。

用户写入系统的数据先写入系统缓冲区,系统缓冲区写满后,将其排入输出队列,然后得到队首时,才进行实际的IO操作。这种输出方式被称为延迟写

UNIX系统提供了三个系统调用来执行刷新内核缓冲区:syncfsyncfdatasync

sync

void sync(void)

sync函数只是将所有修改过的块缓冲区排入输出队列就返回,并不等待实际的写磁盘操作返回。

操作系统的update系统守护进程会周期地调用sync函数,来保证系统中的数据能定期落盘。

根据sync(2) - Linux manual page的描述,Linux对sync的实现与POSIX规范不太一样,POSIX规范中,sync可能在文件真正落盘前就返回,而Linux的实现则是文件真正落盘后才会返回。所以Linux中,syncfsync的效果是一样的!但是1.3.20之前的Linux存在BUG,导致sync并不会在真正落盘后返回。

fsync

void fsync(int filedes)

fsync对指定的文件起作用,它传输内核缓冲区中这个文件的数据到存储设备中,并阻塞直到存储设备响应说数据已经保存好了。

fsync对文件数据与文件元数据都有效。文件的元数据可以理解为文件的属性数据,比如文件的更新时间,访问时间,长度等。

fdatasync

void fdatasync(int filedes)

fdatasyncfsync类似,两者的区别是,fdatasync不一定需要刷新文件的元数据部分到存储设备。

是否需要刷新文件的元数据,是要看元数据的变化部分是否对之后的读取有影响,比如文件元数据的访问时间st_atime和修改时间st_mtime变化了,fdatasync不会去刷新元数据数据到存储设备,因为即使这个数据丢失了不一致了,也不影响故障恢复后的文件读取。但是如果文件的长度st_size变化了,那么就需要刷新元数据数据到存储设备。

所以如果你每次都更新文件长度,那么调用fsyncfdatasync的效果是一样的。

但是如果更新能做到不修改文件长度,那么fdatasync能比fsync少了一次磁盘写入,这个是非常大的速度提升。

O_SYNCO_DSYNC

除了上面三个系统调用,open系统调用在打开文件时,可以设置和同步相关的标志位:O_SYNCO_DSYNC

设置O_SYNC的效果相当于是每次write后自动调用fsync

设置O_DSYNC的效果相当于是每次write后自动调用fdatasync

关于新建文件

在一个文件上调用fsync/fdatasync只能保证文件本身的数据落盘,但是对于文件系统来说,目录中也保存着文件信息,fsync/fdatasync的调用并不会保证这部分的数据落盘。如果此时发生掉电,这个文件就无法被找到了。

所以对于新建文件来说,还需要在父目录上调用fsync

关于覆盖现有文件

覆盖现有文件时,如果发生掉电,新的数据是不会写入成功,但是可能会污染现有的数据,导致现有数据丢失。

所以最佳实践是新建一个临时文件,写入成功后,再替换原有文件。具体步骤:

  1. 新建一个临时文件
  2. 向临时文件写入数据
  3. 对临时文件调用fsync,保证数据落盘。期间发生掉电对现有文件无影响。
  4. 重命名临时文件为目标文件名
  5. 对父目录调用fsync

存储设备缓冲区

存储设备为了提高性能,也会加入缓存。高级的存储设备能提供非易失性的缓存,比如有掉电保护的缓存。但是无法对所有设备做出这种保证,所以如果数据只是写入了存储设备的缓存的话,遇到掉电等故障,依然会导致数据丢失。

对于保证数据能保存到存储设备的持久化存储介质上,而不管设备本身是否有易失性缓存,操作系统提供了write barriers这个机制。

开启了write barriers的文件系统,能保证调用fsync/fdatasync数据持久化保存,无论是否发生了掉电等其他故障,但是会导致性能下降。

许多文件系统提供了配置write barriers的功能。比如ext3, ext4, xfsbtrfsmount参数-o barrier表示开启写屏障,调用fsync/fdatasync能保证刷新存储设备的缓存到持久化介质上。-o nobarrier则表示关闭写屏障,调用fsync/fdatasync无法保证数据落盘。

Linux默认开启写屏障,所以默认情况下,我们调用fsync/fdatasync,就可以认为是文件真正的可靠落盘了。

对于这个层面的数据安全保证来说,应用程序是不需要去考虑的,因为如果这台机器的硬盘被挂载为没有开启写屏障,那么可以认为这个管理员知道这个风险,他选择了更高的性能,而不是更高的安全性。

总结

  • 文件数据从应用程序写入磁盘,需要经过多个缓冲区:应用本身的缓冲区,库的缓冲区,操作系统缓冲区,磁盘缓冲区
  • 如果文件数据只是写入缓冲区,而还未写入硬盘的持久化存储设备上,那么断电等故障会导致数据丢失
  • 库层面刷新缓冲区:C标准库的fflush,JDK的OutputStream#flush
  • 操作系统层面刷新缓冲区:
    • fsync可以刷新文件数据+元数据缓冲区
    • fdatasync可以刷新文件数据,在不影响读取的情况下,可以不刷新文件元数据,性能更好一些
    • open系统调用的O_SYNC标志位可以在每次write后自动调用fsync
    • open系统调用的O_DSYNC标志位可以在每次write后自动调用fdatasync
  • 存储设备层面刷新缓冲区:文件系统支持开启/关闭写屏障write barriers,如果开启写屏障,则fsync/fdatasync可以保证文件写入磁盘的持久化设备中,如果关闭写屏障,则fsync/fdatasync只能保证文件写入磁盘,此时文件可能存在于磁盘的缓存中

参考资料

Linux/UNIX编程如何保证文件落盘的更多相关文章

  1. Java如何保证文件落盘?

    本文转载自Java如何保证文件落盘? 导语 在之前的文章Linux/UNIX编程如何保证文件落盘中,我们聊了从应用到操作系统,我们要如何保证文件落盘,来确保掉电等故障不会导致数据丢失.JDK也封装了对 ...

  2. Linux/UNIX编程:使用C语言实现简单的 ls 命令

    刚好把 Linux/UNIX 编程中的文件和IO部分学完了,就想编写个 ls 命令练习一下,本以为很简单,调用个 stat 就完事了,没想到前前后后弄了七八个小时,90%的时间都用在格式化(像 ls ...

  3. 学习linux/unix编程方法的建议(转)

    假设你是计算机科班出身,计算机系的基本课程如数据结构.操作系统.体系结构.编译原理.计算机网络你全修过 我想大概可以分为4个阶段,水平从低到高从安装使用=>linux常用命令=>linux ...

  4. 学习linux/unix编程方法的建议,学习Linux的四个步骤(转)

    解答:学习Linux的四个步骤假设你是计算机科班出身,计算机系的基本课程如数据结构.操作系统.体系结构.编译原理.计算机网络你全修过我想大概可以分为4个阶段,水平从低到高从安装使用=>linux ...

  5. 在linux/unix中查找大文件

    在linux/unix中查找大文件,如查找大于100M文件的位置路径,查找等于10M文件的位置路径等等,下面就介绍几个实现快速查找的命令: 1. 查找指定目录下所有大于100M的文件,命令为 find ...

  6. Linux系统:保证数据安全落盘

    在很多IO场景中,我们经常需要确保数据已经安全的写到磁盘上,以便在系统宕机重启之后还能读到这些数据.但是我们都知道,linux系统的IO路径还是很复杂的,分为很多层,每一层都可能会有buffer来加速 ...

  7. Linux:保证数据安全落盘

    背景 在很多IO场景中,我们经常需要确保数据已经安全的写到磁盘上,以便在系统宕机重启之后还能读到这些数据.但是我们都知道,linux系统的IO路径还是很复杂的,分为很多层,每一层都可能会有buffer ...

  8. Linux/Unix编程中的线程安全问题【转】

    转自:http://blog.csdn.net/zhengzhoudaxue2/article/details/6432984 在目前的计算机科学中,线程是操作系统调度的最小单元,进程是资源分配的最小 ...

  9. [linux]Socket编程的头文件

    socket编程中需要用到的头文件 sys/types.h:数据类型定义 sys/socket.h:提供socket函数及数据结构 netinet/in.h:定义数据结构sockaddr_in arp ...

随机推荐

  1. jQuery——开发插件

    当我们编写的代码可以供其他人甚至我们自己重用的时候,可以通过将这些代码打包成一个新插件. ###**在插件中使用别名∗∗自定义的插件就应该始终都使用jQuery这个名字来调用jQuery方法,或者也可 ...

  2. 将Oracle数据,以及表结构如何传输至MySQL

    最近研究数据库,将Oracle数据库中的表结构以及数据传输给MySQL数据库,自己通过学习采用两种方式,效率较高. 方式一:Navicat 自从下载了Navicat,真的发现这是一款操作数据库十分优秀 ...

  3. 环境变量jdk版本与java -version显示不一致

    问题描述: 问题产生原因: 1.Path环境变量配置了Oracle 2.C:\windows\System32下,还有java.exe. 问题描述: 今天遇到一个小bug,我电脑环境变量配置的版本是j ...

  4. 关于Spring Boot的博客集合

    掘金: 关于Spring Boot的博客集合 CSDN: Spring Boot教程 掘金: SpringBoot2 简书: Spring Boot 核心技术 天码营 Spring Data JPA: ...

  5. centos /usr/local 和/opt 安装软件你什么不同../configure --prefix=/usr...

    /usr/local下一般是你安装软件的目录,这个目录就相当于在windows下的programefiles这个目录(所有文件在一个文件夹) /opt这个目录是一些大型软件的安装目录,或者是一些服务程 ...

  6. java日期

    // 完整显示日期时间 String str = (new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS")).format(new Date ...

  7. charles(1)解决charles抓包乱码问题

    前言 当使用Charles抓包时,发现数据都是乱码,这时需要安装证书 解决办法 1.点击charles窗口,点击左上角Help-> SSL Proxying -> Install Char ...

  8. POJ - 3376 Finding Palindromes(拓展kmp+trie)

    传送门:POJ - 3376 题意:给你n个字符串,两两结合,问有多少个是回文的: 题解:这个题真的恶心,我直接经历了5种错误类型 : ) ... 因为卡内存,所以又把字典树改成了指针版本的. 字符串 ...

  9. 使用 Nginx 在 Linux 上托管 ASP.NET Core

    server { listen 80; server_name example.com *.example.com; location / { proxy_pass http://localhost: ...

  10. windows常用快捷键和dos命令

    windows常用快捷键 Ctrl+C (复制) Ctrl+V (粘贴) Ctrl+S (保存) Ctrl+A (全选) Ctrl+Z (撤销) Alt+F4 (关闭当前窗口) shift+Delet ...