细说C、C++、Python和go的标准库和fsync()

概要

上一篇文章(https://zp001.blog.csdn.net/article/details/139305428)提到,在Linux用普通IO写文件,fsync()是个非常重要的系统调用,只有它才能立即保障数据完整,否则数据只会保存在Linux缓存中,等待Linux定期刷脏落盘。
现在我们来看看C、C++、Python和Go四种语言的IO标准库是否都使用到了它。

C 和 fsync()

查证C stream函数是否使用了fsync()

先上代码

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main(void) {
#define FILENAME "/mnt/ext4/stdio.txt"
  FILE *fp;

  fp = fopen(FILENAME, "w");
  if (fp == NULL) {
    fprintf(stderr, "failed to open file: %s, error:%s\n", FILENAME,
            strerror(errno));
    return EXIT_FAILURE;
  }

  fputc('h', fp);
  fputc('e', fp);
  fputc('l', fp);
  fputc('l', fp);
  fputc('o', fp);
  fputc('\n', fp);

  fputs("hello from fputs()\n", fp);

  const char *str = "hello from fwrite()\n";
  fwrite(str, strlen(str), 1, fp);

  if (ferror(fp) || feof(fp)) {
    fprintf(stderr, "failed to fputc()\n");
    goto out;
  }

  fflush(fp);

out:
  fclose(fp);
  return EXIT_SUCCESS;
}


使用strace工具来看看上面的程序是否调用到了fsync()

openat(AT_FDCWD, "/mnt/ext4/stdio.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
newfstatat(3, "", {st_mode=S_IFREG|0644, st_size=0, ...}, AT_EMPTY_PATH) = 0
write(3, "hello\nhello from fputs()\nhello f"..., 45) = 45
close(3)                                = 0

可以看到是其实它并没有调用到fsync()。

fflush()的作用

有个常见的误解是以为fflush()会刷脏写盘,其实并不是。
fflush的man page是这样说的:

For output streams, fflush() forces a write of all user-space buffered data for the given output or update stream via the stream’s underlying write function.

直译过来就是,强制把用户态缓存写入到输出。
这里面有两件事:

  1. 用户态缓存
  2. 使用底层write函数写入

下面细说这两件事

1. 用户态缓存

fflush() man page说到它用了用户态缓存,为什么特意说用户态缓存?难道还有一个缓存吗?
是的。
如果你是用C标准库的stream函数来做IO,那么会用到两个缓存,一个是用户态缓存,一个是内核态缓存(这层缓存的名称很容易让人混淆,其实很多文章的“文件系统缓存”、“操作系统缓存”、page cache、“文件数据缓存”都是这一个缓存)。
写入的数据,先写入用户态缓存,再写入内核态缓存,然后再写盘,一共三步。
fputc() fputs() fwrite()这些函数,是把数据写入用户态缓存,这时第一步。
fflush() 调用底层的write()函数,把用户态缓存写入内核态缓存,这是第二步。
那第三步呢?第三步需要直接调用fsync()完成,这也是上述示例程序缺失的步骤。我们得再补上几行代码:

  int fd = fileno(fp);
  if (fsync(fd) < 0) {
    fprintf(stderr, "failed to fsync()\n");
  }

然后再使用strace观察一下,输出变成了:

openat(AT_FDCWD, "/mnt/ext4/stdio.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
newfstatat(3, "", {st_mode=S_IFREG|0644, st_size=0, ...}, AT_EMPTY_PATH) = 0
write(3, "hello\nhello from fputs()\nhello f"..., 45) = 45
fsync(3)                                = 0 // 多了这行
close(3)                                = 0

现在数据就有保障了

2. 底层write()函数

上述第二步的fflush(),使用strace可以看到它其实是调用了write(),这就是man page所说的“底层write函数”。这是个系统调用,作用是把用户态的数据写入到内核态缓存。

从strace的输出还可以观察到,通过一次write()的调用,把之前fputs()等函数的所有数据一次性写入了内核缓存,这就是C标准库函数的作用:减少了系统调用的次数。

更详细的说明请见《细说Linux中三种常见的IO模式》(https://zp001.blog.csdn.net/article/details/139305428)

c stream函数总结

  1. 它并没有封装fsync(),需要程序员手工调用它来保障数据完整。
  2. stream的函数的作用是减少了系统调用的次数,相应地,也付出了双重缓存的成本。

CPP 和 fsync()

查证CPP stream是否使用fsync()

和上面C的情况类似,C++的标准库iostream,默认的行为也是没有封装fsync()的。

比如下面简单的例子:

#include <fstream>
#include <ios>
#include <iostream>

int main(int argc, char *argv[]) {
  const char *file_name{"/mnt/ext4/hello.txt"};

  std::ofstream ofile{file_name, std::ios::trunc};
  if (!ofile.good()) {
    std::cerr << "failed to open file: " << file_name << '\n';
    return 1;
  }

  ofile << "hello\n";
  ofile.flush(); // 下面可以看到这个flush()并不是调用fsync()
  return 0;
}

strace一下,可以看到它也是没有调用fsync()。strace的输入节选如下:

openat(AT_FDCWD, "/mnt/ext4/hello.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
write(3, "hello\n", 6)                  = 6
close(3)                                = 0

cpp的flush()

上面示例程序是调用了flush()的,但是和C类似,这个函数的行为并不是fsync()。
不少人以为调用了flush(),数据就万事大吉了,其实不是,数据还没落盘呢。

cpp调用fsync()

既然如此,cpp如何调用fsync()实现数据完整性呢?
遗憾的是,我并没有发现简便或者权威的方法。
要调用fsync(),就需要知道文件描述符(fd),但CPP已经把获取fd的正规途径封死了。
网上有一种办法,就是先继承std::filebuf类,然后在子类中获取该类的protected属性_M_file,从而获取到fd。

int get_fd(std::filebuf *fb) {
  class MyFileBuf : public std::filebuf {
  public:
    int my_get_fd() { return _M_file.fd(); }
  };

  return static_cast<MyFileBuf *>(fb)->my_get_fd();
};

但这种办法无论如何算不上优雅。

python 和 fsync()

python和上面也是类似的,它的flush()函数也不是调用fsync()的,写个demo例子,然后strace它就可以知道结论,就不再赘述了。

def main():
    with open("/mnt/ext4/hello.txt", "wb") as fh:
        fh.write("hello\n".encode("utf-8"))
        fh.flush()
    return 0

strace的输出如下,也是可以看到没有fsync()的。

openat(AT_FDCWD, "/mnt/ext4/hello.txt", O_WRONLY|O_CREAT|O_TRUNC|O_CLOEXEC, 0666) = 3
newfstatat(3, "", {st_mode=S_IFREG|0644, st_size=0, ...}, AT_EMPTY_PATH) = 0
ioctl(3, TCGETS, 0x7ffc921c3290)        = -1 ENOTTY (Inappropriate ioctl for device)
lseek(3, 0, SEEK_CUR)                   = 0
write(3, "hello\n", 6)                  = 6
close(3)                                = 0

但在python,调用fsync()就容易一点了,只需加多如下这一行即可:

os.fsync(fh)

golang 和 fsync()

fsync()在golang标准库这边,终于得到应有的尊重,把它给封装成了Sync()。
示例代码:

package main

import (
	"fmt"
	"os"
)

func main() {
	fName := "/mnt/ext4/hello.txt"
	file, err := os.OpenFile(fName, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0664)
	if err != nil {
		fmt.Fprintf(os.Stderr, "failed to OpenFile(%s), err: %s\n",
			fName, err.Error())
	}
	defer file.Close()

	_, err = file.WriteString("hello world\n")
	if err != nil {
		fmt.Fprintf(os.Stderr, "failed to WriteString(), err: %s\n",
			err.Error())
	}

	file.Sync() // 这里会调用fsync()
}

strace的输出如下,可以看到程序是会调用fsync(),这样数据完整性就得到了保证。

openat(AT_FDCWD, "/mnt/ext4/hello.txt", O_WRONLY|O_CREAT|O_TRUNC|O_CLOEXEC, 0664) = 3
futex(0x531de0, FUTEX_WAKE_PRIVATE, 1)  = 1
futex(0x531cf8, FUTEX_WAKE_PRIVATE, 1)  = 1
fcntl(3, F_GETFL)                       = 0x8001 (flags O_WRONLY|O_LARGEFILE)
fcntl(3, F_SETFL, O_WRONLY|O_NONBLOCK|O_LARGEFILE) = 0
epoll_create1(EPOLL_CLOEXEC)            = 4
pipe2([5, 6], O_NONBLOCK|O_CLOEXEC)     = 0
epoll_ctl(4, EPOLL_CTL_ADD, 5, {events=EPOLLIN, data={u32=5833896, u64=5833896}}) = 0
epoll_ctl(4, EPOLL_CTL_ADD, 3, {events=EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, data={u32=1048576001, u64=9153862171220770817}}) = -1 EPERM (Operation not permitted)
fcntl(3, F_GETFL)                       = 0x8801 (flags O_WRONLY|O_NONBLOCK|O_LARGEFILE)
fcntl(3, F_SETFL, O_WRONLY|O_LARGEFILE) = 0
write(3, "hello world\n", 12)           = 12
fsync(3)                                = 0
futex(0x531de0, FUTEX_WAKE_PRIVATE, 1)  = 1
futex(0x531cf8, FUTEX_WAKE_PRIVATE, 1)  = 1
close(3)                                = 0

另外一方面,我们也可以看到,go除了调用应该调用的几个syscall之外,还额外添加了很多系统调用,比如fcntl()和epoll等函数。这和golang的底层实现机制相关。

写在最后

上一篇文章(https://zp001.blog.csdn.net/article/details/139305428)提到,部分人总是误以为write()就是写入磁盘,本文说到的这些可能也是误导因素之一:C、C++、Python的标准库并无刻意封装fsync(),而是要程序员额外调用。
这显得fsync()不太重要,事实上,相当重要。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值