Linux/Unix系统编程 三:文件IO缓存

出于速度效率考虑,系统I/O调用(即内核)和标准 C 语言库 I/O 函数(即stdio函数)在操作磁盘文件时会对数据进行缓存

一、文件I/O的内核缓冲:缓冲区高速缓存

read()write() 系统调用在操作磁盘文件不会直接发起磁盘访问,而是仅仅用户空间缓冲区内核缓冲区高速缓存之间复制数据

缓冲区大小对 I/O 系统调用性能影响

如果与文件发生大量的数据传输,通过采用大块空间缓冲数据,以及执行更少的系统调用,可以极大提高 I/O 性能

二、stdio 库的缓冲

操作磁盘文件时,缓冲大块数据减少系统调用,C 语言函数库的 I/O 函数(比如,fprintf()、fscanf()、fgets()、fputs()、fputc()、fgetc())正是这么做的。因此,使用 stdio 库可以使编程者免于自行处理对数据的缓冲

1、设置 stdio 流缓冲模式

调用 setvbuf() 函数,可以控制 stdio 库使用缓冲的形式。

#include <stdio.h>

int setvbuf(FILE *stream, char *buf, int mode, size_t size);

1、参数

1、stream

标识要修改哪个文件流的缓冲,stream 为 fopen() 函数返回值。

打开流后,必须在调用任何其他 stdio 函数之前调用 setvbuf()。setvbuf() 调用将影响后续在指定流上进行的所有 stdio 操作

2、buf

如果 buf 不为 NULL,使用 size 标识缓存器大小。

如果 buf 为 NULL,那么 stdio 库会为 stream 自动分配一个缓冲区。

3、mode

_IONBF:不对 I/O 进行缓存。stderr 默认属于这一类,从而保证错误能立即输出。

_IOLBF:采用行缓存 I/O。对于输出流,在输出一个换行符前将缓冲数据。对于输入流,每次读取一行数据。

_IOFBF:采用全缓冲 I/O。单次读、写数据的大小与缓冲区相同。

2、返回值

0:执行成功

非0:执行失败

3、其他

#include <stdio.h>

void setbuf(FILE *stream, char *buf);

相当于

setvbuf(stream, buf, (buf != NULL)?_IOFBF:_IONBF, BUFSIZ);

buf 为 NULL 时,表示无缓冲。

buf 不为 NULL 时,指向由调用者分配的 BUFSIZ 个字节大小的缓冲。

BUFSIZ 定义在 <stdio.h> 头文件中。

#include <stdio.h>

void setbuffer(FILE *stream, char *buf, size_t size);

相当于

setvbuf(stream, buf, (buf != NULL)?_IOFBF:_IONBF, size);

2、刷新 stdio 缓冲区

无论当前采用何种缓冲区模式,在任何时候,都可以使用 fflush() 库函数 强制将 stdio 输出流中数据(即通过 write())刷新到内核缓冲区中。

#include <stdio.h>

int fflush(FILE *stream);

1、参数

1、stream

如果 stream 为 NULL,则 fflush() 将刷新所有的 stdio 缓冲区。

如果 stream 不为 NULL,刷新指定 stdio 流。

2、返回值

执行成功,返回0。

执行失败,返回非0。

3、其他

如果将 fflush() 函数应用于输入流,将丢弃已缓冲的输入数据。

关闭相应流时,将自动刷新其 stdio 缓冲区。

三、控制文件I/O的内核缓冲

强制刷新内核缓冲区到输出文件是可能的,也是必要的

1、同步I/O数据完整性和同步I/O文件完整性

同步I/O完成:某一 I/O 操作,要么已经成功完成到磁盘的数据传递,要么被诊断为不成功

SUSv3定义了两种不同类型同步 I/O 完成,二者之间区别涉及用于描述文件的元数据,亦即内核针对文件而存储的数据

1、同步 I/O 数据完整性:确保针对文件的一次更新传递了足够的信息(到磁盘),以便于之后对数据的获取。

  • 读操作而言,这意味着被请求的文件已经(从磁盘)传递给进程。
  • 写操作而言,这意味着写请求所指定的数据已传递(至磁盘)完毕,且用于获取数据的所有文件元数据已传递(至磁盘)完毕。

2、同步 I/O 文件完整性:是同步 I/O 数据完整性的超级。对于文件的一次更新过程中,要将所有发生更新的文件元数据都传递到磁盘上,即使有些在后续对文件数据的读操作中并不需要。

2、控制文件I/O内核缓冲的系统调用

#include <unistd.h>

int fsync(int fd);

int fdatasync(int fd);

fsync() 系统调用将缓冲数据已打开文件描述符 fd 相关所有元数据刷新到磁盘上。调用 fsync() 会强制文件处于同步 I/O 文件完整性状态。

fdatasync() 系统调用的运作类似于 fsync(),只是强制文件处于 同步 I/O 数据完整性状态。

fsync() 系统调用:更新数据 ,并且更新元数据。

fdatasync() 系统调用:更新数据,不更新元数据。

fsync() 系统调用会使包含更新文件信息的所有内核缓冲区(数据块、指针块、元数据等)刷新到磁盘上。

3、使所有写入同步:O_SYNC

调用 open() 函数时如指定 O_SYNC 标志,则会使所有后续输出同步。

fd = open(pathname, O_WRONLY|O_SYNC);

调用 open() 后,每个 write() 调用会自动将文件数据元数据刷新到磁盘上。

4、O_SYNC 对性能的影响

采用 O_SYNC 标志(或者频繁调用 fsync()、fdatasync() 或 sync() )对性能影响极大

表 13-3 所示为采用不同缓冲区大小,在有、无 O_SYNC 标识的情况下将一百万字节写入一个新创建文件所需要的时间。
在这里插入图片描述

  • 使用 O_SYNC 标志使运行总用时大为增加
  • 使用 O_SYNC 标志执行写操作时,运行总用时CPU 时间巨大差异(原因:向磁盘传递数据时,程序会阻塞)。

总之,在程序设计时,如果需要强制刷新内核缓冲区使用大尺寸 write() 缓冲区,或者调用 fsync() 或 fdatasync(),而不是打开文件设置 O_SYNC 标志

5、O_DSYNC 和 O_RSYNC 标志

O_DSYNC:要求写操作按照同步 I/O 数据完整性来执行。

O_SYNC:要求写操作按照同步 I/O 文件完整性来执行。

O_RSYNC:与 O_DSYNC 和 O_SYNC 标志一起使用,将这些标志对写操作作用结合到读操作中。

四、I/O 缓冲小结

在这里插入图片描述

五、就 I/O 模式向内核提出建议

posix_fadvise() 系统调用允许进程自身访问文件数据时可能采取的模式通知内核

#include <fcntl.h>

int posix_fadvise(int fd, off_t offset, off_t len, int advice);

内核可以(但不必非要)根据 posix_fadvise() 所提供的信息来优化对缓冲区高速缓存的使用,进而提高进程和整个系统的性能

1、参数

1、fd

fd 指定文件描述符。

2、offset

offset 指定了区域起始的偏移量。

3、len

len 指定了区域的大小。如果 len 为 0 表示从 offset 开始,直至文件结尾。

4、advice

  • POSIX_FADV_NORMAL:进程对访问模式无特别建议。Linux 将文件预读窗口大小设置为默认值
  • POSIX_FADV_SEQUENTIAL:进程预计会从低偏移量到高偏移量顺序读取数据。Linux 将文件预读窗口大小设置为默认值的两倍
  • POSIX_FADV_RANDOM:进程预计以随机顺序访问数据。Linux 禁用文件预读
  • POSIX_FADV_WILLNEED:进程预计会在不久的将来访问指定的文件区域。内核将由 offset 和 len 指定区域的文件数据预先填充到缓冲区高速缓存中
  • POSIX_FADV_DONTNEED:进程预计会在不久的将来不会访问指定的文件区域。这一操作将给内核的建议是释放相关的高速缓存页面(如果存在的话)。
  • POSIX_FADV_NOREUSE:进程预计会一次性地访问指定文件区域,不在复用。这等于提示内核对指定区域访问一次后即可释放页面

2、返回值

成功返回 0,错误返回错误码。

六、绕过缓冲区高速缓存:直接 I/O

始于内核 2.4,Linux 允许应用程序在执行磁盘 I/O绕过缓冲区高速缓存,从用户空间直接将数据传递到文件或磁盘设备。有时也称此为直接 I/O 或者裸 I/O

通过在调用 open() 系统调用时指定 O_DIRECT 标志,开启直接 I/O。

1、直接I/O的对齐限制

因为直接 I/O (针对磁盘设备和文件)涉及对磁盘的直接访问,所以执行 I/O 时,必须遵守一些限制。

  • 用于传递数据的缓冲区,其内存边界必须对齐为块的整数倍。
  • 数据传输的开始点,亦即文件和设备的偏移量,必须是块大小的整数倍。
  • 待传递数据的长度必须是块大小的整数倍。

七、混合使用库函数和系统调用进行文件 I/O

同一文件上执行 I/O 操作时,还可以将系统调用标准 C 语言库函数混合使用。fileno() 和 fdopen() 函数有助于完成这一工作。

#include <stdio.h>

int fileno(FILE *stream);

给定一个(文件)流,fileno() 函数返回相应的文件描述符。随即可以在 read()、write() 等系统调用中正常使用该文件描述符。

#include <stdio.h>

FILE *fdopen(int fd, const char *mode);

给定一个文件描述符,fdopen() 函数创建一个使用该描述符进行文件 I/O 的相应流。mode 参数和 fopen() 函数中 mode 含义相同。若该参数与文件描述符 fd 的访问模式不一致,fdopen() 调用将失败。

fdopen() 使用场景:创建套接字和管道的系统调用总返回文件描述符,为了在这些文件类型上使用 stdio 库函数,必须使用 fdopen() 函数来创建相应文件流。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值