同步内核缓冲区 sync、fsync和fdatasync函数

本文介绍了操作系统中内核缓冲区的概念及其在文件I/O中的作用,详细解释了write()函数的不同步特性,并对比了sync、fsync及fdatasync三个用于同步缓冲区数据到磁盘的函数的区别。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

同步内核缓冲区

1.缓冲区简介

    人生三大错觉之一:在调用函数write()时,我们认为该函数一旦返回,数据便已经写到了文件中.但是这种概念只是宏观上的.实际上,操作系统实现某些文件I/O时(如磁盘文件),为了保证I/O的效率,在内核通常会用到一片专门的区域(内存或独立的I/O地址空间)作为I/O数据缓冲区.它用在输入输出设备和CPU之间,用来缓存数据,使得低速的设备和高速的CPU能够协调工作避免低速的输入输出设备长时间占用CPU,减少系统调用,提高了CPU的工作效率.

2.不同步的write()

    传统的UNIX或LINUX系统在设计时使用了内核缓冲区,设有高速缓冲区或页面高速缓冲区,大多数磁盘I/O都通过缓冲区进行.当将数据写入文件时,内核通常先将该数据复制到其中一个缓冲区,如果该缓冲区尚未写满,则并不将其排入输出队列,而是等待其写满或者当内核需要重用该缓冲区以便存放其他磁盘块数据时,再将该缓冲区排入输出队列;然后待其到达队首时,才进行实际的I/O操作.这种输出方式被称为延迟写.
    当调用write()函数写出数据时,数据一旦写到该缓冲区(关键:只是写到缓冲区),函数便立即返回.此时写出的数据可以用read()读回,也可以被其他进程读到,但是并不意味着它们已经被写到了外部永久存储介质上,即使调用close()关闭文件后也可能如此. 因为缓冲区的数据可能还在等待输出.
    因此,从数据被实际写到磁盘的角度来看,用write()写出的文件数据与外部存储设备并不是完全同步的.不同步的时间间隔非常短,一般只有几秒或十几秒,具体取决于写出的数据量和I/O数据缓冲区的状态.尽管不同步的时间间隔很短,但是如果在此期间发生掉电或者系统崩溃,则会导致所写数据来不及写至磁盘而丢失的情况.
    注意:内核将缓冲区中的数据“写”到标准输入磁盘文件中,这里“写”不是将缓冲区中的数据移动到磁盘文件中,而是拷贝到磁盘文件中,也就说此时磁盘文件中还保留一份缓冲区内容的备份.如图1所示.做出这一设计也是有其道理的,如果写出到磁盘文件上,磁盘坏了或满了等等,总之就是无法将数据送出,假如没备份,那数据不是丢掉了.也就是说内核会等待写入磁盘动作完成后,才放心的将备份的数据删除掉.在下文讨论的三个函数中也将涉及到这个过程.
 
图1 数据传入过程示意图

    为了保证磁盘上实际文件系统与缓冲区高速缓存中内容的一致性,UNIX系统提供了sync、fsync和fdatasync三个函数.

3.sync函数

头文件:#include<unistd.h>
定义函数:void sync(void);
返回值:若成功则返回0,若出错则返回-1,同时设置errno以指明错误.
函数说明:
    sync负责将系统缓冲区的数据“写入”磁盘,以确保数据的一致性和同步性.注意:sync函数只是将所有修改过的块缓冲区排入写队列,然后就返回,他并不等待实际I/O操作结束.所以不要认为调用了sync函数,就觉得数据已安全的送到磁盘文件上,有可能会出现问题,但是sync函数是无法得知的.
    系统守候进程一般每隔一段时间调用一次sync函数,确保定期刷新内核的块缓存.UNIX系统中,系统守候进程update会周期性地(一般每个30秒)调用sync函数.命令sync(1)也调用sync函数.

4.fsync函数

头文件:#include<unistd.h>
定义函数:int fsync(int filedes);
返回值:若成功则返回0,若出错则返回-1,同时设置errno以指明错误.
函数说明:
    与sync函数不同,fsync函数只对由文件描符filedes指定的单一文件起作用,强制与描述字fildes相连文件的所有修改过的数据(包括核内I/O缓冲区中的数据)传送到外部永久介质,即刷新fildes给出的文件的所有信息,并且等待写磁盘操作结束,然后返回.调用 fsync()的进程将阻塞直到设备报告传送已经完成.这个fsync就安全点了.
    一个程序在写出数据之后,如果继续进行后续处理之前要求确保所写数据已写到磁盘,则应当调用fsync().例如,数据库应用通常会在调用write()保存关键交易数据的同时也调用fsync().这样更能保证数据的安全可靠.

5.fdatasync函数

头文件:#include<unistd.h>
定义函数:int fdatasync(int filedes);
返回值:若成功则返回0,若出错则返回-1,同时设置errno以指明错误.
函数说明:
    fdatasync函数类似于fsync函数,但它只影响文件数据部分,强制传送用户已写出的数据至物理存储设备,不包括文件本身的特征数据.这样可以适当减少文件刷新时的数据传送量.而除数据外,fdatasync还会同步更新文件的属性.

6.错误代码

EBADF:文件描述符无效,或文件已关闭.
EIO : 读写的过程中发生错误 .
EROFSEINVAL:文件所在的文件系统不支持同步.

7.fflush()与fsync()的联系

    内核I/O缓冲区是由操作系统管理的空间,而流缓冲区是由标准I/O库管理的用户空间.fflush()只刷新位于用户空间中的流缓冲区.fflush()返回后,只保证数据已不在流缓冲区中,并不保证它们一定被写到了磁盘.此时,从流缓冲区刷新的数据可能已被写至磁盘,也可能还待在内核I/O缓冲区中.要确保流I/O写出的数据已写至磁盘,那么在调用fflush()后还应当调用fsync().

8.综述

    虽然延迟写减少了磁盘读写次数,但是却降低了文件内容的更新速度,使得欲写到文件中数据在一段时间内并没有写到磁盘上。当系统发生故障时,这种延迟可能造成文件更新内容的丢失。
<think>嗯,用户想了解Linux中FILE结构体的语言级缓冲区文件内核缓冲区区别。这是个涉及系统编程标准库实现的专业问题。 用户可能是正在学习Linux系统编程的开发者,或者遇到了I/O性能问题需要优化。从问题表述看,ta已经对缓冲机制有基本认知,但需要更系统的对比分析。 需要明确三个层次: 第一层是stdio库的FILE缓冲区(用户空间),属于语言运行时层面的优化,通过setbuf可配置三种模式。第二层是内核页缓存(内核空间),用页对齐块减少磁盘操作。第三层才是实际硬件设备。 特别要注意强调缓冲区同步的触发条件:fflush()fsync()的区别很重要。用户可能混淆这两个调用,导致数据丢失风险。示例代码选择用fprintfwrite对比很合适,能直观展示缓冲层级。 安全方面要提醒:突然断电时内核缓冲区可能丢失数据,关键应用需要O_SYNC标志。这个实际建议对开发者很有价值。 最后设计的问题要覆盖: 1. 配置方法(实用) 2. 性能影响(调优) 3. 同步机制(可靠性) 4. 错误处理(健壮性) 这样能帮助用户建立完整知识链。</think>在 Linux 系统中,文件 I/O 相关的缓冲区主要涉及两个层次: 1. **语言级缓冲区 (stdio 缓冲区 / 用户空间缓冲区)** * **位置:** 用户空间内存 (由你的应用程序管理)。 * **管理者:** C 标准库 (如 glibc 的 `stdio` 函数:`printf`, `fprintf`, `fwrite`, `fread`, `fgets` 等)。 * **目的:** 减少系统调用的次数。频繁调用 `read/write` 系统调用开销很大。`stdio` 函数将多次小的读写操作累积起来,在缓冲区满、遇到换行符 (对于行缓冲)、程序正常结束或显式调用 `fflush()` 时,才一次性将数据通过系统调用写入内核缓冲区或从内核缓冲区读取。 * **类型:** * *全缓冲:* 默认用于普通文件。缓冲区满时刷新。 * *行缓冲:* 默认用于终端 (stdout)。遇到换行符 `'\n'` 或缓冲区满时刷新。 * *无缓冲:* 直接调用系统调用 (如 stderr)。 * **控制:** 使用 `setbuf()`, `setvbuf()` 函数设置缓冲区大小模式。 * **结构:** 封装在 `FILE` 结构体中 (包含指向缓冲区的指针、大小、当前填充状态等)。 2. **内核缓冲区 (页缓存 / 内核空间缓冲区)** * **位置:** 内核空间内存 (由操作系统管理)。 * **管理者:** Linux 内核 (具体是虚拟文件系统 VFS 块设备层)。 * **目的:** * 减少物理磁盘 I/O 次数:将磁盘数据缓存在内存中,将应用程序的写操作累积在内存中,延迟写入磁盘。 * 提高读性能:缓存最近访问过的磁盘数据,后续读取可直接从内存提供。 * 统一管理磁盘访问:合并相邻的 I/O 请求。 * **刷新时机:** 由内核策略决定,通常在缓冲区满、系统内存压力大、特定时间间隔后,或者应用程序显式调用 `fsync()`, `fdatasync()`, `sync()` 系统调用强制刷新到磁盘。 * **结构:** 内核用 `struct page` 结构管理,组织成页缓存。 **交互流程 (以写入为例):** 1. 应用程序调用 `fprintf(fp, "data")`。 2. `stdio` 库将 "data" 写入 `FILE` 结构体关联的语言级缓冲区。 3. 语言级缓冲区满 (或行缓冲遇到 `\n`,或调用 `fflush(fp)`),`stdio` 库调用 `write()` 系统调用。 4. 内核将 `write()` 传递的数据复制到内核缓冲区 (页缓存) 中。 5. 内核在合适的时机 (延迟写入) 将脏页 (被修改过的页) 真正写入物理磁盘设备。调用 `fsync()`/`fdatasync()` 会强制要求内核立即将该文件描述符关联的所有脏页写回磁盘。 **关键区别总结:** | 特性 | 语言级缓冲区 (stdio) | 内核缓冲区 (页缓存) | | :----------- | :--------------------------------------- | :--------------------------------------- | | **位置** | 用户空间内存 | 内核空间内存 | | **管理者** | C 标准库 (`libc.so`) | Linux 内核 | | **目的** | 减少 `read/write` 系统调用次数 | 减少物理磁盘 I/O 次数,提高读写性能 | | **关联对象** | `FILE*` (文件流) | 文件描述符 (`int fd`), 磁盘块 | | **控制函数** | `setbuf`, `setvbuf`, `fflush` | `fsync`, `fdatasync`, `sync`, 挂载选项 | | **刷新触发** | 缓冲区满、换行符、`fflush`、程序退出、流关闭 | 内核策略、内存压力、`fsync/fdatasync/sync` | | **可见性** | 对应用程序代码可见 | 对应用程序透明 | **示例代码说明:** ```c #include <stdio.h> #include <unistd.h> int main() { FILE *fp = fopen("file.txt", "w"); // 写入到 stdio 缓冲区 (语言级) fprintf(fp, "This data is in stdio buffer...\n"); // 可能还在 stdio buffer fflush(fp); // 强制 stdio buffer 刷新到内核 buffer (页缓存) // 此时数据在内核 buffer,但未必在磁盘! // 直接使用系统调用写入 (绕过 stdio buffer) const char *msg = "This data goes straight to kernel buffer\n"; write(fileno(fp), msg, strlen(msg)); // 数据在内核 buffer (页缓存) // 强制内核 buffer (页缓存) 刷新到物理磁盘 fsync(fileno(fp)); fclose(fp); // 关闭文件也会隐式刷新 stdio buffer (到内核 buffer) return 0; } ``` **重要性:** 理解这两层缓冲对于编写高效、可靠的 I/O 程序至关重要。不当的缓冲策略可能导致性能瓶颈,而忽略了缓冲区刷新 (`fflush` 或 `fsync`) 则可能导致关键数据在程序崩溃或系统断电时丢失。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

且漫CN

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值