文 章 目 录
💻作 者 简 介:曾 与 你 一 样 迷 茫,现 以 经 验 助 你 入 门 Linux。
💡个 人 主 页:@笑口常开xpr 的 个 人 主 页
📚系 列 专 栏:Linux 探 索 之 旅:从 命 令 行 到 系 统 内 核
✨代 码 趣 语:缓 冲 区 是 数 据 暂 歇 的 客 栈,\n 是 催 发 的 号 角,满 是 起 程 的 船 票 - - - 而 文 件 描 述 符 那 枚 铜 匙,总 在 fflush 挥 手 时,
把 暂 存 的 故 事,郑 重 塞 进 磁 盘 的 褶 皱 里。
💪代 码 千 行,始 于 坚 持,每 日 敲 码,进 阶 编 程 之 路。
📦gitee 链 接:gitee
学 Linux 文 件 操 作 时,你 是 否 遇 过 这 些 困 惑:printf 输 出 不 即 时、fwrite 与 write 结 果 不 一 致、程 序 结 束 数 据 丢 失?其 实 问 题 根 源 在 “缓 冲 区” 和 “文 件 描 述 符”。本 文 以 “代 码 实 测 + 底 层 拆 解” 为 核 心:先 用 5 组 对 比 代 码 讲 透 缓 冲 区 3 种 刷 新 策 略,再 手 把 手 模 拟 实 现 C 文 件 标 准 库(不 依 赖 stdio.h,直 接 调 用 系 统 调 用),帮 你 打 通 “会 用” 到 “懂 原 理” 的 链 路。
一、文 件 描 述 符
1、重 新 认 识 缓 冲 区
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
const char* fstr = "hello fwrite\n";
const char* str = "hello write\n";
//C
printf("hello printf\n"); //stdout ---> 1
fprintf(stdout, "hello fprintf\n"); //stdout ---> 1
fwrite(fstr,strlen(fstr),1,stdout); //stdout ---> 1
//close(1);
//操作系统提供的系统接口
write(1,str,strlen(str)); //stdout ---> 1
fork();
return 0;
}
代 码 的 结 果 为 直 接 输 出 时 显 示 正 常 输 出,输 出 到 文 件 中 时 C 语 言 的 接 口 输 出 了 2 次,系 统 调 用 的 函 数 输 出 了 1 次。
原 因:向 显 示 器 输 出 为 行 缓 冲 方 式 会 依 次 输 出 到 显 示 器 中。当 向 文 件 中 输 出 时,缓 冲 方 式 由 行 缓 冲 变 成 了 全 缓 冲。即 遇 到 \n
,不 在 刷 新,而 是 等 缓 冲 区 被 写 满 才 刷 新。
首 先 write 通 过 缓 冲 区 直 接 刷 新,文 件 刷 新 为 全 缓 冲,C 接 口 的 函 数 的 内 容 被 储 存 在 C 语 言 提 供 的 用 户 级 缓 冲 区 中,fork 之 后 创 建 子 进 程,父 子 进 程 的 数 据 共 享,子 进 程 进 行 写 时 拷 贝,C 语 言 提 供 的 缓 冲 区 里 的 数 据 被 拷 贝 了 2 份,在 进 程 退 出 时,C 语 言 的 缓 冲 区 被 刷 新。
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
const char* fstr = "hello fwrite\n";
const char* str = "hello write\n";
printf("hello printf\n"); //stdout ---> 1
sleep(2);
fprintf(stdout, "hello fprintf\n"); //stdout ---> 1
sleep(2);
fwrite(fstr,strlen(fstr),1,stdout); //stdout ---> 1
sleep(2);
write(1,str,strlen(str)); //stdout ---> 1
sleep(5);
return 0;
}
前 6 秒 时 C 接 口 的 函 数 被 存 储 在 C 语 言 的 缓 冲 区 中,write 函 数 通 过 缓 冲 区 直 接 写 入 文 件 中,当 进 程 结 束 时 会 刷 新 缓 冲 区 将 数 据 放 入 文 件 中。文 件 刷 新 为 全 缓 冲,\n
不 会 刷 新 缓 冲 区,而是 等 缓 冲 区 写 满 才 会 被 刷 新。
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
const char* fstr = "hello fwrite\n";
const char* str = "hello write\n";
//C
printf("hello printf\n"); //stdout ---> 1
fprintf(stdout, "hello fprintf\n"); //stdout ---> 1
fwrite(fstr,strlen(fstr),1,stdout); //stdout ---> 1
close(1);
//操作系统提供的系统接口
write(1,str,strlen(str)); //stdout ---> 1
fork();
return 0;
}
和 第 1 次 代 码 的 结 果 不 同,添 加 close 后,C 语 言 的 接 口 输 出 了 1 次,系 统 调 用 的 接 口 没 有 输 出。
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
const char* fstr = "hello fwrite";
printf("hello printf"); //stdout ---> 1
fprintf(stdout, "hello fprintf"); //stdout ---> 1
fwrite(fstr,strlen(fstr),1,stdout); //stdout ---> 1
close(1);
return 0;
}
如 果 去 掉 \n
,代 码 没 有 输 出。
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
const char* str = "hello fwrite";
write(1,str,strlen(str)); //stdout ---> 1
close(1);
return 0;
}
代 码 没 有 \n
,和 C 语 言 的 接 口 不 同,系 统 调 用 的 接 口 可 以 输 出。
总 结
-
缓 冲 区 一 定 不 在 操 作 系 统 内 部,不 是 系 统 级 别 的 缓 冲 区。write 可 以 输 出 是 因 为 write 直 接 将 字 符 串 写 入 了 缓 冲 区 中,close 对 显 示 没 有 影 响。
-
C 语 言 会 提 供 一 个 用 户 级 缓 冲 区,C 语 言 的 接 口 函 数 传 递 的 数 据 实 际 上 储 存 在 C 语 言 提 供 的 用 户 级 缓 存 区 中,当 调 用
\n
、fclose
等 可 以 刷 新 缓 冲 区 时,才 会 刷 新 C 语 言 提 供 的 缓 冲 区,并 调 用 write 函 数,将 数 据 写 入 操 作 系 统 中。 -
C 语 言 的 文 件 操 作 绕 不 开 FILE,FILE 中 包 含 文 件 描 述 符 fd,FILE 里 面 还 有 对 应 打 开 文 件 的 缓 冲 区 字 段。这 个 FILE 对 象 属 于 用 户,语 言 都 属 于 用 户 层。
-
显 示 器 的 文 件 刷 新 方 案 是 行 刷 新,所 以 在 printf 执 行 完 成 后 就 会 立 即 遇 到
\n
的 时 候,将 数 据 进 行 刷 新。刷 新 的 本 质 是 将 数 据 通 过 1 + write 通 过 write 接 口 写 入 到 内 核 中。 -
目 前 我 们 认 为,只 要 将 数 据 刷 新 进 入 了 内 核 中,数 据 就 会 被 刷 新 进 入 硬 件 中。
2、exit 和 _exit
exit 是 C 语 言 的 接 口,退 出 时 会 刷 新 C 语 言 提 供 的 缓 冲 区,然 后 调 用 _exit 退 出,_exit 是 系 统 调 用,直 接 释 放 进 程 不 会 对 数 据 进 行 刷 新。
3、缓 冲 区 的 刷 新 问 题
操 作 系 统 会 维 护 缓 冲 区。
(1)无 缓 冲
无 缓 冲 区 是 一 种 写 透 模 式,不 要 在 缓 冲 区 中 做 出 各 种 数 据 残 留,直 接 刷 新,不 能 等 待,没 有 进 行 各 种 刷 新 策 略。
(2)行 缓 冲
不 刷 新 直 到 遇 到 \n
才 会 刷 新。默 认 向 显 示 器 输 出 采 用 行 刷 新 是 因 为 显 示 器 是 给 人 看 的,人 每 次 看 数 据 符 合 每 次 看 一 行 的 习 惯,需 要 尽 可 能 快 的 把 数 据 刷 新 出 来。
(3)全 缓 冲
缓 冲 区 满 了 才 会 刷 新 缓 冲 区。在 向 普 通 文 件 写 入 时 为 了 提 高 效 率 刷 新 时 不 需 要 实 时 观 看 所 以 采 用 全 缓 冲。
根 据 这 3 种 情 况 来 决 定 什 么 时 候 调 用 write 接 口 的 问 题。fflush 的 底 层 会 封 装 write。缓 冲 区 中 存 储 的 数 据 越 多,效 率 越 高。
4、进 程 退 出
进 程 退 出 时 也 会 刷 新 缓 冲 区。
示 例 1
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
printf("hello world");
return 0;
}
示 例 2
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
printf("hello world");
close(1);
return 0;
}
printf(“hello world”); 若 未 包 含 换 行 符 \n
且 程 序 未 结 束 或 未 手 动 刷 新,数 据 会 暂 时 留 在 缓 冲 区 中,不 会 立 即 显 示 在 终 端 上。当 使 用 close(1) 后,文 件 描 述 符 被 关 掉,会 导 致 缓 冲 区 中 的 数 据未 来 得 及 刷 新 就 丢 失,最 终 无 法 在 显 示 器 上 显 示。
5、C 语 言 提 供 缓 冲 区 的 原 因
- 解 决 效 率 问 题,硬 件 设 备 的 读 写 速 度 远 慢 于 CPU 和 内 存 的 处 理 速 度。缓 冲 区 用 于 临 时 存 储 待 输 出 或 待 读 取 的 数 据。程 序 先 将 数 据 写 入 缓 冲 区,待 缓 冲 区 满、触 发 特 定 条 件 或 程 序 结 束 时,再 一 次 性 将 缓 冲 区 数 据 写 入 硬 件 设 备。这 大 幅 减 少 了 硬 件 I/O 的 次 数,从 而 提 升 整 体 效 率。
- 标 准 I/O 库 通 过 缓 冲 区 批 量 处 理 数 据,将 多 次 小 的 I/O 请 求 合 并 为 一 次 大 的 请 求,从 而 减 少 系 统 调 用 的 次 数,降 低 开 销。
- 优 化 用 户 交 互 体 验。数 据 暂 存 在 缓 冲 区,直 到 遇 到 换 行 符 \n 才 刷 新 到 屏 幕。这 种 设 计 符 合 人 类 阅 读 习 惯,用 户 更 希 望 看 到 完 整 的 一 行 内 容,而 非 字 符 逐 个 零 散 显 示。
二、模 拟 实 现 C 文 件 标 准 库
1、为 什 么 要 “造 轮 子”?
我 们 平 时 写 C 语 言 文 件 操 作 时,习 惯 直 接 调 用 标 准 库 的 fopen、fwrite、fclose - - - 这 些 函 数 好 用,但 你 有 没 有 想 过:
- 为 什 么 fwrite 不 是 “写 了 就 立 刻 到 文 件”?
- FILE 结 构 体 里 到 底 存 了 什 么?
- 标 准 库 是 怎 么 跟 操 作 系 统 交 互 的?
这 是 一 个 简 化 版 的 C 文 件 操 作 库:手 动 实 现 了 _fopen、_fwrite、_fflush、_fclose,没 有 依 赖 标 准 库 的 stdio.h,而 是 直 接 调 用 操 作 系 统 的 底 层 接 口(open、write、close),还 加 入 了 核 心 的 缓 冲 区 机 制。FILE 中 的 缓 冲 区 的 意 义 是 使 用 C 语 言 的 接 口 更 快,节 省 时 间。
2、核 心 原 理
在 拆 解 代 码 前,先 搞 懂 3 个 核 心 概 念:系 统 调 用、缓 冲 区、刷 新 策 略 - - - 这 是 所 有 高 级 语 言 文 件 操 作 的 “通 用 逻 辑”。
(1)系 统 调 用 与 用 户 层 封 装
Linux 操 作 系 统 提 供 了 最 底 层 的 文 件 操 作 接 口,称 为 系 统 调 用(如 open、write、close)。但 系 统 调 用 的 “开 销 很 高” - - - 每 次 调 用 都 要 从 用 户 态 切 换 到 内 核 态,频 繁 调 用 会 拖 慢 程 序。
C 标 准 库 的 stdio 系 列 函 数(fopen、fwrite)本 质 是 对 系 统 调 用 的 封 装,核 心 目 的 是:在 用 户 层 加 一 层 “缓 冲 区”,减 少 系 统 调 用 的 次 数,从 而 提 高 IO 效 率。
我 们 的 代 码 也 遵 循 这 个 逻 辑:
- 用 _FILE 结 构 体 封 装 文 件 描 述 符(fileno)和 缓 冲 区;
- _fopen 封 装 open 系 统 调 用;
- _fwrite 先 写 缓 冲 区,满 了 再 调 用 write;
- _fclose 先 刷 新 缓 冲 区,再 调 用 close。
(2)缓 冲 区
为 什 么 缓 冲 区 能 提 高 效 率?
举 个 例 子:
如 果 要 写 1000 个 字 符 到 文 件,直 接 调 用 write 需 要 1000 次 系 统 调 用;但 如 果 先 把 字 符 存 到 1024 字 节 的 缓 冲 区,满 了 再 调 用 1 次 write,系 统 调 用 次 数 从 1000 降 到 1 - - - 效 率 天 差 地 别。
代 码 里,缓 冲 区 通 过 FILE 结 构 体 的 outbuffer(字 符 数 组)和out_pos(当 前 缓 冲 区 已 用 长 度)实 现,缓 冲 区 大 小 由 SIZE 宏 定 义 为 1024 字 节。
(3)缓 冲 刷 新 的 3 种 策 略
缓 冲 区 的 数 据 不 会 一 直 存 着,需 要 在 特 定 时 机 “刷 新” 到 磁 盘,代 码 里 定 义 了 3 种 刷 新 策 略(通 过 flag 控 制):
FLUSH_NOW:无 缓 冲,写 入 后 立 即 刷 新
FLUSH_LINE:行 缓 冲,遇 到 \n 时 刷 新
FLUSH_ALL:全 缓 冲,缓 冲 区 满 了 才 刷 新。
3、代 码 拆 解
接 下 来 逐 文 件 解 析 代 码,搞 懂 每 个 函 数 的 核 心 逻 辑。
(1)头 文 件 myfile.h
头 文 件 的 作 用 是 “对 外 声 明” - - - 定 义 结 构 体、宏、函 数 接 口,让 其 他 文 件 能 复 用。
#ifndef __MYFILE_H__ // 防止头文件重复包含(比#pragma once更通用)
#define __MYFILE_H__
// 引入依赖的系统头文件
#include<stdlib.h> // malloc/free
#include<string.h> // memcpy/strcmp
#include<sys/types.h>
#include<sys/stat.h> // 文件状态相关
#include<fcntl.h> // open系统调用的标志(O_CREAT、O_WRONLY等)
#include<assert.h> // 断言(调试用)
#include<unistd.h> // close/write/sleep系统调用
// 1. 宏定义:缓冲区大小 + 刷新策略
#define SIZE 1024 // 输出缓冲区大小:1024字节
#define FLUSH_NOW 1 // 立即刷新(无缓冲)
#define FLUSH_LINE 2 // 行刷新(遇\n刷)
#define FLUSH_ALL 4 // 全缓冲(满了刷)
// 2. 自定义文件结构体:替代标准库的FILE
typedef struct IO_FILE
{
int fileno; // 文件描述符(操作系统给文件的唯一标识)
int flag; // 缓冲刷新策略(FLUSH_NOW/FLUSH_LINE/FLUSH_ALL)
// int in_pos; // (未实现)输入缓冲区当前位置
// char inbuffer[SIZE]; // (未实现)输入缓冲区
char outbuffer[SIZE]; // 输出缓冲区:存待写入文件的数据
int out_pos; // 输出缓冲区当前已用长度(从0开始)
}_FILE; // 重命名为_FILE,简化使用
// 3. 函数声明:对外提供的文件操作接口
_FILE* _fopen(const char* filename, const char* flag); // 打开文件
int _fwrite(_FILE* fp, const char* msg, int len); // 写入数据
void _fclose(_FILE* fp); // 关闭文件
void _fflush(_FILE* fp); // 强制刷新缓冲区
#endif
关 键 点:
- 用 #ifndef MYFILE_H 防 止 头 文 件 被 重 复 包 含(比 如 main.c 和 myfile.c 都 包 含 myfile.h,编 译 时 不 会 报 错)。
- _FILE 结 构 体 只 实 现 了 输 出 缓 冲 区,输 入 缓 冲 区(inbuffer/in_pos)被 注 释 了。
- fileno 是 核 心:操 作 系 统 通 过 文 件 描 述 符 识 别 文 件,所 有 底 层 操 作 都 依 赖 它。
(2)文 件 myfile.c
myfile.c 负 责 把 myfile.h 声 明 的 接 口 落 地,核 心 是 4 个 函 数:_fopen、_fwrite、_fflush、_fclose。
1. _fopen
_fopen 的 作 用 是 根 据 用 户 传 入 的 “打 开 模 式”(flag),调 用 open 系 统 调 用 创 建 文 件 描 述 符,再 分 配 并 初 始 化 _FILE 结 构 体。
#include"myfile.h"
#define FILE_MODE 0666 // 文件创建时的权限(可读可写,所有者/组/其他用户)
_FILE* _fopen(const char* filename, const char* flag)
{
assert(filename); // 调试断言:如果filename为NULL,直接崩溃(避免非法访问)
int open_flags = 0; // 传给open系统调用的标志
int fd = -1; // 文件描述符(初始化为-1,代表无效)
// 1. 根据打开模式(flag)设置open的标志
if(strcmp(flag,"w") == 0) // 写模式:创建文件(不存在则建)、只写、覆盖原有内容
{
open_flags = O_CREAT | O_WRONLY | O_TRUNC;
// O_CREAT:文件不存在则创建;O_WRONLY:只写;O_TRUNC:清空原有内容
fd = open(filename, open_flags, FILE_MODE); // 创建文件时需指定权限
}
else if(strcmp(flag,"a") == 0) // 追加模式:创建文件、只写、在文件末尾追加
{
open_flags = O_CREAT | O_WRONLY | O_APPEND;
// O_APPEND:每次写都追加到文件末尾
fd = open(filename, open_flags, FILE_MODE);
}
else if(strcmp(flag,"r") == 0) // 读模式:只读(文件必须存在)
{
open_flags = O_RDONLY;
fd = open(filename, open_flags); // 读模式不需要权限参数
}
else
{
return NULL; // 不支持的模式,返回NULL
}
// 2. 检查open是否成功(fd == -1代表失败,比如文件不存在、权限不够)
if(fd == -1)
{
return NULL;
}
// 3. 分配_FILE结构体内存(用户层的文件对象)
_FILE* fp = (_FILE*)malloc(sizeof(_FILE));
if(fp == NULL) // 检查内存是否分配成功(避免内存不足导致崩溃)
{
return NULL;
}
// 4. 初始化_FILE结构体
fp->fileno = fd; // 绑定文件描述符
fp->flag = FLUSH_ALL; // 默认使用“全缓冲”策略
fp->out_pos = 0; // 输出缓冲区初始为空(已用长度为0)
return fp; // 返回用户层的文件指针
}
关 键 点:
- FILE_MODE 0666:文 件 权 限 的 八 进 制 表 示,rw-rw-rw-(所 有 者、组、其 他 用 户 都 有 读 写 权 限),但 实 际 权 限 会 受 umask 影 响(比 如 默 认 umask 0022,实 际 权 限 会 变 成 0644);
- open 的 标 志 组 合:不 同 模 式 对 应 不 同 的 标 志,比 如 w 模 式 必 须 加 O_TRUNC(清 空 文 件),a 模 式 必 须 加 O_APPEND(追 加);
- 内 存 分 配 检 查:malloc 可 能 失 败(比 如 内 存 不 足),必 须 检 查 fp == NULL,否 则 后 续 操 作 会 崩 溃。
2. _fwrite
_fwrite:不 直 接 写 文 件,而 是 先 把 数 据 拷 贝 到 缓 冲 区,再 根 据 flag 判 断 是 否 需 要 刷 新 到 磁 盘。
int _fwrite(_FILE* fp, const char* s, int len)
{
// 1. 把数据拷贝到输出缓冲区(当前代码未处理缓冲区溢出!)
memcpy(&fp->outbuffer[fp->out_pos], s, len);
fp->out_pos += len; // 更新缓冲区已用长度
// 2. 根据刷新策略,判断是否需要调用write刷新到文件
if(fp->flag & FLUSH_NOW) // 立即刷新:不管缓冲区是否满,直接写
{
write(fp->fileno, fp->outbuffer, fp->out_pos);
fp->out_pos = 0; // 刷新后,缓冲区重置为空
}
else if(fp->flag & FLUSH_LINE) // 行刷新:遇到\n才刷新
{
if(fp->outbuffer[fp->out_pos - 1] == '\n') // 检查最后一个字符是否是\n
{
write(fp->fileno, fp->outbuffer, fp->out_pos);
fp->out_pos = 0;
}
}
else if(fp->flag & FLUSH_ALL) // 全缓冲:缓冲区满了才刷新
{
if(fp->out_pos == SIZE) // 已用长度等于缓冲区大小,满了
{
write(fp->fileno, fp->outbuffer, fp->out_pos);
fp->out_pos = 0;
}
}
return len; // 返回写入的字节数(简化处理,未检查实际写入量)
}
关 键 点:
- 缓 冲 区 溢 出 风 险:当 前 代 码 直 接 memcpy,如 果 fp->out_pos + len > SIZE(比 如 剩 余 缓 冲 区 只 有 50 字 节,要 写 100 字 节),会 导 致 数 据 溢 出 outbuffer,破 坏 内 存 - - - 这 是 严 重 bug,后 续 优 化 会 解 决。
- 刷 新 策 略 的 实 际 效 果:当 前 _fopen 默 认 用 FLUSH_ALL,即 使 写 入 的 字 符 串 有 \n,也 不 会 立 即 刷 新,只 会 在 缓 冲 区 满(1024 字 节)或 _fclose 时 刷 新。
3. _fflush
_fflush 的 作 用 是 “不 管 缓 冲 区 是 否 满、是 否 有 \n”,强 制 把 缓 冲 区 的 数 据 写 到 文 件,常 用 于 fclose 前 或 需 要 立 即 落 盘 的 场 景。
void _fflush(_FILE* fp)
{
if(fp->out_pos > 0) // 只有缓冲区有数据时才刷新
{
write(fp->fileno, fp->outbuffer, fp->out_pos);
fp->out_pos = 0; // 重置缓冲区
}
}
判 断 缓 冲 区 是 否 有 数 据(out_pos > 0),有 就 调 用 write,然 后 重 置 out_pos。
4. _fclose
_fclose 是 “收 尾 工 作”:必 须 先 刷 新 缓 冲 区(避 免 数 据 残 留),再 关 闭 文 件 描 述 符,最 后 释 放 _FILE 结 构 体 的 内 存。
void _fclose(_FILE* fp)
{
assert(fp); // 断言:fp不能为NULL(避免非法访问)
_fflush(fp); // 1. 先强制刷新缓冲区(关键!否则缓冲区数据会丢失)
close(fp->fileno); // 2. 关闭文件描述符(归还操作系统资源)
free(fp); // 3. 释放_FILE结构体内存(避免内存泄漏)
}
核 心 原 则:
- 必 须 先 刷 新 再 关 闭:如 果 不 调 用 _fflush,缓 冲 区 中 未 刷 的 数 据 会 随 着 fp 被 free 而 丢 失。
- 资 源 释 放 顺 序:先 释 放 用 户 层 资 源(缓 冲 区 数 据),再 释 放 内 核 层 资 源(文 件 描 述 符),最 后 释 放 内 存。
4、运 行 与 验 证
- 编 译 并 运 行 代 码
- 观 察 结 果
代 码 运 行 多 次,会 继 续 追 加 内 容(因 为 打 开 模 式 是 a)。
5、完 整 代 码 展 示
myfile.c
#include"myfile.h"
#define FILE_MODE 0666
//"w" "a" "r"
_FILE* _fopen(const char* filename,const char* flag)
{
assert(filename);
int f = 0;
int fd = -1;
if(strcmp(flag,"w") == 0)
{
f = (O_CREAT|O_WRONLY|O_TRUNC);
fd = open(filename,f,FILE_MODE);
}
else if(strcmp(flag,"a") == 0)
{
f = (O_CREAT|O_WRONLY|O_APPEND);
fd = open(filename,f,FILE_MODE);
}
else if(strcmp(flag,"r") == 0)
{
f = O_RDONLY;
fd = open(filename,f);
}
else
{
return NULL;
}
if(fd == -1)
{
return NULL;
}
_FILE* fp = (_FILE*)malloc(sizeof(_FILE));
if(fp == NULL)
{
return NULL;
}
fp->fileno = fd;
fp->flag = FLUSH_ALL;
fp->out_pos = 0;
return fp;
}
int _fwrite(_FILE* fp,const char* s,int len)
{
//"abcd\n"
memcpy(&fp->outbuffer[fp->out_pos],s,len);//没有做异常处理,也不考虑局部问题
fp->out_pos += len;
if(fp->flag & FLUSH_NOW)
{
write(fp->fileno,fp->outbuffer,fp->out_pos);
fp->out_pos = 0;
}
else if(fp->flag & FLUSH_LINE)
{
if(fp->outbuffer[fp->out_pos-1] == '\n')
{
write(fp->fileno,fp->outbuffer,fp->out_pos);
fp->out_pos = 0;
}
}
else if(fp->flag & FLUSH_ALL)
{
if(fp->out_pos == SIZE)
{
write(fp->fileno,fp->outbuffer,fp->out_pos);
fp->out_pos = 0;
}
}
return len;
}
void _fflush(_FILE* fp)
{
if(fp->out_pos > 0)
{
write(fp->fileno,fp->outbuffer,fp->out_pos);
fp->out_pos = 0;
}
}
void _fclose(_FILE* fp)
{
assert(fp);
//进程结束时,缓冲区有内容直接刷新
_fflush(fp);
close(fp->fileno);
free(fp);
}
myfile.h
//防止文件被重复包含
//#pragma once
#ifndef __MYFILE_H__
#define __MYFILE_H__
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<assert.h>
#include<unistd.h>
#define SIZE 1024
#define FLUSH_NOW 1
#define FLUSH_LINE 2
#define FLUSH_ALL 4
typedef struct IO_FILE
{
int fileno;
int flag;
//int in_pos;
//char inbuffer[SIZE];
char outbuffer[SIZE];
int out_pos;
}_FILE;
_FILE* _fopen(const char* filename,const char* flag);
int _fwrite(_FILE* fp,const char* msg,int len);
void _fclose(_FILE* fp);
void _fflush(_FILE* fp);
#endif
main.c
#include "myfile.h"
#define filename "test.txt"
int main()
{
_FILE* fp = _fopen(filename,"a");
if(fp == NULL)
{
return 1;
}
const char* msg = "hello world\n";
int cnt = 10;
while(cnt)
{
_fwrite(fp,msg,strlen(msg));
sleep(1);
cnt--;
}
_fclose(fp);
return 0;
}
makefile
main:main.c myfile.c
gcc -o $@ $^ -std=c99
.PHONY:clean
rm -f main
三、总 结
当 你 跑 通 模 拟 C 库 的 代 码,那 些 文 件 操 作 的 困 惑 早 已 有 了 答 案:printf 等 \n、fwrite 藏 缓 冲、close (1) 丢 数 据,本 质 都 是 “用 户 层 与 内 核 层 交 互 逻 辑” 的 体 现。Linux 学 习 从 不 是 记 接 口,而 是 抓 本 质:想 清 “数 据 在 哪 个 缓 冲”,“通 过 哪 个 描 述 符 到 内 核”,多 数 问 题 会 迎 刃 而 解。继 续 打 磨 代 码、追 问 底 层,“懂 原 理” 的 每 一 步,都 会 让 后 续 进 阶 更 扎 实。
转载自CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/2301_78847073/article/details/151153363