17.基础IO_3
文章目录
- 基础IO_3
- 1、认识缓冲区
- 1.1 缓冲区的本质和存在的意义
- 1.2 缓冲区的发送策略
- 1.3 如何理解`fwrite`写入
- 2、通过现象进一步剖析
- 2.1 示例代码
- 2.2 实验现象
- 2.3 缓冲区详解
- 3、用 C 实现自己的缓冲输出系统
- 3.1 整体模块结构
- 3.2 结构体定义:`FILE_`
- 3.3 `fopen_()`:打开文件并初始化结构体
- 功能流程:
- 注意点:
- 3.4 `fwrite_()`:写入缓冲区并模拟行缓冲刷新
- 功能流程:
- 注意点:
- 3.5 `fclose()`:刷新剩余数据并释放资源
- 功能流程:
- 注意点:
- 3.6 主函数调用流程
基础IO_3
这一部分很难理解,我会尽量讲解的通俗易懂
1、认识缓冲区
1.1 缓冲区的本质和存在的意义
- 缓冲区的本质就是一块内存
- 存在的意义
- 首先是可以协调速度上的差异,在计算机中,CPU处理速度很快,而磁盘、网络、打印机等设备相对较慢。缓冲区就像一个中转站,让数据先暂存在内存中,等设备准备好再慢慢处理。
- 其次可以减少系统的调用次数,每次调用 read() 或 write() 都会涉及系统资源。如果每次只处理一点点数据,效率会很低。缓冲区可以积累一批数据后一次性处理,节省时间和资源。
简单来说就是节省进程进行数据IO的时间。
1.2 缓冲区的发送策略
策略名称 | 触发时机 | 适用场景 | 特点与说明 |
---|---|---|---|
无缓冲 | 每次调用写入函数时立即发送 | 错误日志、实时输出 | 直接写入设备,效率低但实时性高 |
行缓冲 | 遇到换行符 \n 或缓冲区满时发送 | 终端输入输出、日志文件 | 适合处理文本行,用户体验好 |
全缓冲 | 缓冲区满时发送,或手动刷新 | 大文件写入、批量数据处理 | 提高效率,减少系统调用次数,但实时性较差 |
举个例子:
- 你在终端输入一行命令,按下回车后才执行 → 行缓冲
- 程序写入大量数据到文件,只有缓冲区满了才写出 → 全缓冲
- 程序写入错误信息,立即显示 → 无缓冲
1.3 如何理解fwrite
写入
一句话总结一下,fwrite
与其理解成是写入文件的函数,不如理解成这是一个拷贝函数,把进程拷贝到对应的缓冲区之中。接下来让我们一起来看看代码的输出现象!
2、通过现象进一步剖析
2.1 示例代码
#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main()
{// C接口printf("c语言提供\n");fprintf(stdout,"c语言提供\n");const char* cString = "c语言提供\n";fputs(cString,stdout);// 系统接口const char* wString = "系统接口\n";write(1,wString,strlen(wString));fork();return 0;
}
2.2 实验现象
- 当我在终端直接运行命令,输出结果
- 当我让他重定向到
log.txt
之中时,输出结果变为
2.3 缓冲区详解
我们根据这个实验现象首先可以退出两个结论
- 这个实验现象一定时和缓冲区有关的。
- 缓冲区一定不在内核之中,不然的话
write
也会打印两次。也就是说我们之前所有谈论到的缓冲区都是用户级别的语言层面为我们提供的缓冲区!
接下来解释一下这个实验现象
- 在我们没有进行重定向之前,是向终端之中打印,也就是说整个的打印策略是行缓冲打印,
FILE
之中包含了缓冲区,所以在fork()
之前,这个缓冲区里就已经什么都没有了!再进行一点很重要的解释也是我之前存在的一个很严重的误区,子进程直接完全继承父进程的整个内存空间,公用这么一块空间,并且父进程已经将缓冲区文件输出,所以都是空的。 - 接下来我们解释进行重定向之后的实验结果,此时的行缓冲打印策略注意!!变成了全缓冲,也就是说只有等到缓冲区域满了才会进行刷新,很显然这么几条数据没有填满缓冲区,全部存储在了父进程
FILE
的缓冲区之中了。这个时候子进程继承,等**到进程退出的时候,这时发生写时拷贝!!**所以最终的数据也显示了两倍。 - 为什么
write
没有进行现实呢? 因为这是系统所提供的接口直接使用的是fd,与FILE无关(或者说时根本就没有这一部分),所以数据也无法存储到FILE中的缓冲区之中。因此只会显示一次
3、用 C 实现自己的缓冲输出系统
前面已经讲到缓冲区都是用户级别的语言层面为我们提供的,我们能不能够利用系统操作函数,简单去实现一下呢?
接下来一起来看一看
非常好!我们先来梳理你这套自定义 I/O 系统的代码结构脉络,理清每个函数的职责、调用关系和缓冲机制的设计思路。这样你后续写博客或继续扩展功能时会更有条理。
3.1 整体模块结构
要实现的是一个简化版的 stdio.h
,主要包含三个核心函数:
函数名 | 功能描述 | 关键点 |
---|---|---|
fopen_() | 打开文件并初始化自定义 FILE_ 结构体 | 封装 open() ,设置缓冲区 |
fwrite_() | 写入数据到缓冲区,遇换行符触发刷新 | 模拟行缓冲,使用 write() 写出 |
fclose() | 刷新剩余数据并关闭文件 | 清理资源,释放结构体 |
此外,还定义了一个自定义结构体 FILE_
,用于模拟标准库中的 FILE
类型。
3.2 结构体定义:FILE_
这是整个系统的核心数据结构,封装了文件描述符和缓冲区:
#define SIZE 1024
#define SYNC_LINE 1typedef struct {int fileno; // 文件描述符int pos; // 当前缓冲区写入位置int flags; // 缓冲策略标志(如行缓冲)char buffers[SIZE]; // 缓冲区
} FILE_;
设计思路:
- 用
fileno
关联底层文件(我们要写入的文件) - 用
buffers
存储待写入数据 - 用
pos
追踪写入位置 - 用
flags
控制缓冲策略(目前只支持行缓冲)
3.3 fopen_()
:打开文件并初始化结构体
FILE_ *fopen_(const char*path_name,const char* mode)
{int flag =0;flag |= O_CREAT;if(strcmp(mode,"r") == 0){flag |= O_RDONLY;}else if(strcmp(mode,"w") == 0) {flag |= O_WRONLY | O_TRUNC;}else if(strcmp(mode,"rw") == 0) {flag |= O_RDWR;}else{return NULL;}// 接下来我么就是打开文件int fd = open(path_name,flag,0666);if(fd == -1) return NULL;// 我们最后需要返回的就是一个结构体指针// 现在我们手里已经有了fd,并且我们还得让数据存储在我们的缓存区里面哇FILE_* fp = (FILE_*)malloc(sizeof(FILE_));if (!fp) {close(fd);return NULL;}fp->fileno = fd;fp->pos = 0;fp->flags = SYNC_LINE;// 默认进行行缓冲memset(fp->buffers,0,SIZE);// 初始化缓冲区return fp;
}
功能流程:
- 根据
mode
设置open()
的标志位,open
本身是并不会去创建一个FILE
结构体的。 - 调用
open()
获取文件描述符fd
- 分配并初始化
FILE_
结构体 - 设置默认行为为行缓冲
注意点:
- 支持
"r"
、"w"
、"rw"
三种模式 - 使用
O_CREAT
保证文件存在 - 如果
malloc
或open
失败,及时释放资源
3.4 fwrite_()
:写入缓冲区并模拟行缓冲刷新
// 对文件中进行写入
int fwrite_(void *ptr,int num, FILE_*fp)
{// 要确保传入的全部都不是空的否则就返回-1if(ptr == NULL | num == 0 | fp == NULL) return -1;// 数据传入自定义缓冲区的过程for (int i = 0; i < num; ++i) {char ch = ((char*)ptr)[i];fp->buffers[fp->pos++] = ch; }//int written = write(fp->fileno,fp->buffers,num);// 这样就把数据全部写入到我们的缓冲区域之中去了// 应该采取什么样子的方式去读取换行符号呢for (int i = 0;i < fp->pos; ++i) {if (fp->buffers[i] == '\n') {write(fp->fileno,fp->buffers,i);fp->pos = 0;break; // 找到换行符就停止}}// 解读出第一个换行符号然后再进行打印输出return 0;
}
功能流程:
- 将数据逐字节写入
fp->buffers
,也就是写入我们自己创建的一个缓冲区之中去 - 每写入一个字符,检查是否为
\n
- 如果遇到
\n
,调用write()
写出缓冲区内容 - 写出后清空缓冲区(或移动剩余数据)
注意点:
- 模拟行缓冲:只有遇到
\n
才触发刷新 - 使用
write()
直接写入文件描述符 - 没有
\n
的数据将暂存在缓冲区中
3.5 fclose()
:刷新剩余数据并释放资源
void fclose(FILE_* fp)
{// 关掉文件就比较简单了// 0、在关掉之前我应该先去将缓冲区的内容全部进行输出if(fp->pos != 0)write(1,fp->buffers,(fp->pos));// 1、调用系统关掉文件close(fp->fileno);// 2、整个结构体指针free(fp);
}
功能流程:
- 检查缓冲区是否还有未写出的数据
- 如果有,调用
write()
写出 - 关闭文件描述符
- 释放结构体内存
注意点:
- 确保所有数据都被写出,避免丢失
- 使用
close()
和free()
清理资源
3.6 主函数调用流程
int main() {FILE_* fp = fopen_("test.txt", "w");if (!fp) return 1;char msg[] = "Hello, world!\nThis is a test.\n";fwrite_(msg, strlen(msg), fp);fclose(fp);return 0;
}
调用顺序:
fopen_()
→fwrite_()
→fclose()