linux系统编程——文件IO——缓冲IO
1. 为什么需要缓冲IO
因为所有磁盘操作都 用 块 作为基本操作单位,所以IO要求数据大小 对齐块。
比如,应用程序 写 4.5个块大小的数据,则内核会 写5个块大小数据,读取最后一个块数据,更新(删除)后半部分数据,然后再将整个块写出去,所以若内核一定会保证所有操作都在 块大小整数倍上进行,必要时对 IO进行“修补”。
对齐影响的是 BIO 部分,不是写页缓存。即 写页缓存时,内核不会管是否块对齐,但是 pdflush 工作时,脏块加入 BIO队列时,内核会修补数据以块对齐。
所以页缓存一定程度上缓解了块对齐的问题,导致write和fwrite的更大差异在于系统调用的消耗。
因此,应用程序的IO操作数据大小 若没有 块对齐,则内核会进行冗余操作保证块对齐,导致IO性能严重下降。
正如前面提及的 内核通过延迟写,合并同块IO请求,预读等操作 ,以提高性能。用户空间缓冲IO也是为了提高性能。
2. 如何进行用户空间IO
关键是保证 IO 操作对齐块,所以 IO大小必须为块的整数倍,如 4096
所以 先分配一个 块整数倍大小的 buffer,当写输入时,缓冲到buffer中,直到数据到达一定长度(可能没有满,但是将多次 IO修补 变成一次 IO修补),再调用系统调用,将数据交给内核,
当读数据时,一次尽量读 buffer大小,再从buffer中读取数据。
应用程序可以自己实现缓冲机制,也可以使用 标准IO
3. 标准IO
3.1 fopen
FILE * fopen(const char *path, const char *mode);
mode 的可能值:
r : 读打开,流位于文件开端
r+ : 读写打开
w :写打开,若文件已存在,则截断长度为0,若不存在则创建
w+ : 读写打开,若文件已存在,则截断长度为0,若不存在则创建
a : 写打开,若不存在则创建,流位于文件末尾
a+ : 读写打开,若不存在则创建,流位于文件末尾
mode虽然还有b,但是linux忽略此值,因为linux以相同方式处理文本文件和二进制文件
3.2 fgetc
int fgetc(FILE *stream);
从流读取一个字符。
他把unsigned char 转换为 int并返回,目的是提供足够空间给 EOF或错误通知。
fgetc返回值必须保存再int变量中,将他存放在char变量是常见但危险的错误。
int c;
c = fgetc(stream);
if (c == EOF)
/*error*/
else
printf("c = %c\n", (char )c);
3.3 ungetc
int ungetc(int c, FILE *stream);
将一个字符推回流,让你可以偷看一下流,如果它不是你想要的字符,可以把它还回去。
linux允许无限多个推回操作,只要内存够用。
3.4 fgets
char *fgets(char *str, int size, FILE *stream);
读取size-1个字节,并存储到str,最后加上\0,读到EOF或一个newline后,便会停止读取动作。如果读到newline字符,则\n被存入str.
char buf[LINE_MAX];
if (! fgets(buf, LINE_MAX, stream))
/* error */
LINE_MAX定义于 <limits.h>,是posix定义的输入行最大尺寸,使用这个值 让 程序不需要担心 输入行尺寸限制。
由于 fgets会将\n存入buf,因此可以用 fgetc代替fgets
char *s;
int c;
s = str;
while (--n > 0 && (c = fgetc(stream)) != EOF)
*s++ = c;
*s = '\0';
还可以改为遇到 d所指定的定界符则停止读取
char *s;
int c= 0;
s = str;
while (--n > 0 && (c = fgetc(stream)) != EOF && (*s++ = c) != d)
;
if (c == d)
*--s = '\0';
else
*s = '\0';
尽管重复调用 fgetc,会导致一定开销,但相对于IO对齐,所以这个开销并不大。
3.5 fread
size_t fread(void *buf, size_t size, size_t nr, FILE *stream);
读取 size * nr 个字节,
使用此函数,用于读取二进制数据
3.6 fputc
int fputc(int c, FILE *stream);
将c转换成unsigned char ,写入stream
3.7 fputs
int fputs(const char *str, FILE *stream);
写入 以null结尾字符串
3.8 fwrite
size_t fwrite(void *buf, size_t size, size_t nr, FILE *stream);
写入 size* nr 字节数据,设计用于写二进制
3.9 控制缓冲机制
标准IO提供三种缓冲机制
- 无缓冲:直接提交给内核,不常用
- 行缓冲:遇到newline字符,缓冲区会被提交被内核,适用于输出至屏幕的流。
- 块缓冲:默认缓冲,适用于文件
int setbuf(FILE *stream, char *buf, int mode, size_t size);
将stream缓冲类型设为mode
_IONBF: 无缓冲
_IOLBUF
_IOFBUF
除了无缓存mode外,其他mode下还需提供 buf和 size(buf的大小),这个buf将作为IO流缓冲区,如果buf为NULL,glibc会自动分配。
BUFSIZ(定于于<stdio.h>)是块缓冲的默认大小,为块大小的数倍。
3.10 线程安全
标准IO具备线程安全。在内部,它有一个锁(lock)和锁计数(lock count)。因此单个函数执行环境内,标准IO是原子的。
但是许多程序希望的原子性大于函数调用层次。如应用希望多个写操作都能原子。为此标准IO提供了一系列函数。
3.10.1 手动文件锁定
void flockfile(FILE *stream);
void funlockfile(FILE *stream);
int ftrylockfile(FILE *stream);
3.11 标准IO的缺陷
标准IO最大的问题: 性能受到 两次复制的影响。及 内核页缓存 到 标准IO缓冲 到 用户提供buf。
改进方法:
读操作:读返回一个指向标准IO缓冲区的指针,用户可以直接操作缓冲区里的数据,用户自己决定是否复制这些数据
写操作:调用写操作时,不复制数据,只记录指针,当准备刷新数据到内核时,扫描存储的指针列表,以写出数据,使用 writev 实现。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?