Linux 的 write 返回成功,数据到底落在哪了

一个排查过的人都不会忘的场景:服务把审计日志写进文件,write 每次返回都正常,进程 check 也不报错。然后机器掉电重启,最后十几秒的日志全没了。

这不是文件系统的 bug。write 的语义从来就不是「写到磁盘」,而是「写进了内核的页缓存」。搞清楚这条边界,能避免一大批「数据明明写了为什么会丢」的争论。

write() 返回成功代表什么?

man 手册在 NOTES 里有一句非常直接的话:

A successful return from write() does not make any guarantee that data has been committed to disk.

这句话是整篇的地基。返回值的含义是「内核已经接收了这批数据」,仅此而已。数据此刻躺在内存的 page cache 里,标记为 dirty,等着被回写。

想让数据真正落盘,唯一的办法是显式调用同步接口。man 手册紧接着给出了那个唯一的答案------调用 fsync。

所以完整的持久化链路是三层:write 到内核缓冲、fsync 让内核刷盘、磁盘控制器确认。缺任何一层,掉电都可能丢。

脏页是什么时候被写回去的?

内核不会等脏页攒到极限才动手。它有两个阈值,一个软一个硬。

软阈值是 dirty_background_ratio,默认 10%。脏页达到总内存的这个比例时,后台 flusher 线程开始异步回写。注意是后台线程动手,写入的进程不受影响,继续跑。

硬阈值是 dirty_ratio,默认 20%。超过这条线,产生脏页的进程自己会被拉去同步回写,表现为写入突然变慢甚至卡住。

内核文档对两者的区分是:

the background kernel flusher threads will start writing out dirty data.
a process which is generating disk writes will itself start writing out dirty data.

一个容易踩的坑是这两个值的来源。内核文档的 sysctl 章节没有 写默认值,只解释了含义。默认的 10 和 20 在内核源码的 page-writeback.c 里定义。所以想确认你机器上的实际值,不要查文档,直接读 /proc/sys/vm/ 下的对应文件。

参数 默认值 触发者 触发后行为
dirty_background_ratio 10% 内核 flusher 线程 异步回写,写入者无感
dirty_ratio 20% 产生脏页的进程 同步回写,写入被阻塞
dirty_expire_centisecs 3000 定时回写 超过 30 秒的脏页被刷
dirty_writeback_centisecs 500 回写线程周期 每 5 秒唤醒一次

表中后两个参数说明了另一件事:即使脏页量很低,超过 30 秒也会被刷。所以「我不 fsync 也不会丢太多」这个侥幸,最多撑 30 秒,而且这 30 秒不受你控制。

free 显示内存快没了要紧吗?

大多数情况下不要紧。

free 输出里的 buff/cache 那一列,包含了 page cache。它看起来吃掉了大量内存,但性质是「可回收」------当应用需要内存时,内核会优先丢弃干净的缓存页。

真正该看的是 available 列。man 手册对 MemAvailable 的定义是:

An estimate of how much memory is available for starting new applications, without swapping.

这里要诚实一点:官方并没有「free 数值低是正常的」这种原句,这个结论是从 Cached 和 MemAvailable 的定义推出来的。但推得很稳:既然 cache 可回收,那么「free 小了」本身不构成内存不足的证据。

判断内存是否真的紧张,看两个信号:available 持续低于总量的 10%,以及 swap 在增长。只盯着 free 那一列,是把自己吓一跳。

O_DIRECT 能绕过 page cache 吗?

这是本篇最想纠正的一条措辞。

中文资料里普遍的说法是「O_DIRECT 绕过 page cache」。官方措辞不是这样:

Try to minimize cache effects of the I/O to and from this file.

是 minimize,尽量减少,不是 bypass 绕过。

这个差别不是文字游戏。同一个手册页还说明了两件事:数据确实直接在用户缓冲区和磁盘之间传输,但未对齐时会回退到缓冲 I/O。也就是说,如果你没满足对齐要求,O_DIRECT 悄悄退化成了普通 I/O,你的「绕过」根本没发生,而且不会有任何报错。

对齐要求本身是:

The O_DIRECT flag may impose alignment restrictions on the length and address of user-space buffers and the file offset of I/Os.

具体到数值:早期的要求是文件系统块大小的整数倍,2.6 内核之后放宽到逻辑块大小,通常是 512 字节。不满足会返回 EINVAL,或者静默走缓冲路径。前者还算好的,后者最难查。

所以准确的说法是:O_DIRECT 尽量减少页缓存的影响,但不保证完全绕过,且在未对齐时会退化。

fsync 之后就绝对安全了吗?

不绝对,还有一个目录项的问题。

fsync 保证的是这个文件的数据和元数据 落盘。但如果文件是新建的,它的目录项(也就是「这个文件名存在」这件事)属于目录的元数据,不在文件的 fsync 范围内。手册里说得很清楚:

Calling fsync() does not necessarily ensure that the entry in the directory containing the file has also reached disk.

结果是:文件内容安全了,但掉电后可能找不到这个文件。对于「先写临时文件再 rename」这种原子替换模式,这是致命的------rename 本身也可能没落盘。

正确做法是在 rename 之后,对父目录 的 fd 也调一次 fsync:

c 复制代码
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

int write_atomic(const char *dir, const char *name, const char *buf, size_t len) {
    char tmp[512], dst[512];
    snprintf(tmp, sizeof(tmp), "%s/.tmp.XXXXXX", dir);
    snprintf(dst, sizeof(dst), "%s/%s", dir, name);

    int fd = open(tmp, O_CREAT | O_WRONLY | O_TRUNC, 0644);
    if (fd < 0) return -1;

    if (write(fd, buf, len) != (ssize_t)len) { close(fd); return -1; }
    if (fsync(fd) != 0) { close(fd); return -1; }   /* 数据落盘 */
    close(fd);

    if (rename(tmp, dst) != 0) return -1;

    int dfd = open(dir, O_DIRECTORY | O_RDONLY);
    if (dfd < 0) return -1;
    if (fsync(dfd) != 0) { close(dfd); return -1; } /* 目录项落盘 */
    close(dfd);
    return 0;
}

少了最后那几行,这套「原子写入」只在进程崩溃时原子,掉电时不保证。这个细节在各类「如何安全写文件」的教程里经常被省略。

drop_caches 能解决内存问题吗?

不能,而且官方不建议。

use outside of a testing or debugging environment is not recommended.

这句话后面还跟了一条更关键的说明:这个操作是非破坏性的,不会释放脏页。

This is a non-destructive operation and will not free any dirty objects.

所以 drop_caches 之后,脏页依然在,内存并没有真正腾出来。它清掉的只是干净的缓存页------而那些页本来就是内核在内存紧张时会自动回收的。手动清等于做了一件内核本来就会做的事,还顺便让后续读操作全部退化成磁盘读。

真正需要它的场景只有一种:压测前后想要一个一致的缓存基线。生产环境上按这个按钮,通常只是在掩盖真正的问题。

怎么判断数据到底安全没有

把问题拆成两个,答案就清楚了。

第一个问题:进程崩溃会丢数据吗?答案是不会,因为数据已经在内核里了,进程死了内核还在。这类场景只需要正常的 write。

第二个问题:机器掉电会丢数据吗?答案是会,除非 fsync 过。而且要连目录一起 fsync,否则文件可能「有内容但没名字」。

按这个二分法,很多争论其实是在争不同的问题。说「我 write 完就返回了,怎么会丢」的人,问的是第一个;说「不 fsync 一定会丢」的人,答的是第二个。两边都对,只是没对齐场景。

工程上的取舍也很清楚:fsync 的代价是一次磁盘确认,通常在毫秒级,机械盘上更久。对于能容忍丢几秒数据的日志,用 dirty_expire_centisecs 的 30 秒兜底就够了;对于账目、审计、元数据这类不能丢的,必须每条同步,别指望阈值。

最后回到那句原文:a successful return from write() does not make any guarantee that data has been committed to disk。这句话不需要记,只要记住它否定的是什么------它否定的不是「写入成功」,而是「写入持久」。这两件事在内存和磁盘之间隔着一次 fsync。

相关推荐
我命由我1234539 分钟前
Python 开发 - Python 项目部署(Windows 与 Linux)
java·linux·windows·后端·python·java-ee·python3.11
erzong23139 分钟前
Linux操作系统:基于环形队列&信号量实现多生产者多消费者的简易模型
linux·运维·服务器
小羊没烦恼!44 分钟前
TypedSql:在 C# 类型系统上实现一个 SQL 查询引擎
java·linux·运维·服务器·开发语言·数据库·mysql
星恒随风1 小时前
Linux线程同步与互斥(二):从日志系统与线程池设计,到线程安全单例、可重入函数和死锁原理
linux·运维·状态模式
mengge.cloud1 小时前
1022小白入门教程(番外筑基)— nerdctl 容器基本功 + K8s 资源编写
linux·网络·云原生·容器·kubernetes·云计算·k8s
张小姐的猫1 小时前
【MySQL】表的约束
android·linux·服务器·数据库·c++·mysql·adb
无足鸟ICT2 小时前
【RHCA+】yum命令(查询、安装、升级和卸载软件包)
linux
kyle~2 小时前
数据结构 --- 无锁队列(Lock-Free Queue)
linux·数据结构·c++·多线程·并发
邪修king2 小时前
Re:Linux系统篇(三十六):进程间通信Chapter3:从匿名管道到命名管道:Linux 进程间通信实战(FIFO 深度解析)
linux·运维·服务器