概要
上一篇文章(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.
直译过来就是,强制把用户态缓存写入到输出。
这里面有两件事:
- 用户态缓存
- 使用底层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函数总结
- 它并没有封装fsync(),需要程序员手工调用它来保障数据完整。
- 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()不太重要,事实上,相当重要。