关注

炸了!Linux 文件操作的 “终极密码”:缓冲区是 “数据客栈”,描述符是 “通关密匙”,连 C 库都在偷偷这么玩!

💻作 者 简 介:曾 与 你 一 样 迷 茫,现 以 经 验 助 你 入 门 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 语 言 的 接 口 不 同,系 统 调 用 的 接 口 可 以 输 出。


总 结

  1. 缓 冲 区 一 定 不 在 操 作 系 统 内 部,不 是 系 统 级 别 的 缓 冲 区。write 可 以 输 出 是 因 为 write 直 接 将 字 符 串 写 入 了 缓 冲 区 中,close 对 显 示 没 有 影 响。

  2. C 语 言 会 提 供 一 个 用 户 级 缓 冲 区,C 语 言 的 接 口 函 数 传 递 的 数 据 实 际 上 储 存 在 C 语 言 提 供 的 用 户 级 缓 存 区 中,当 调 用 \nfclose 等 可 以 刷 新 缓 冲 区 时,才 会 刷 新 C 语 言 提 供 的 缓 冲 区,并 调 用 write 函 数,将 数 据 写 入 操 作 系 统 中。

  3. C 语 言 的 文 件 操 作 绕 不 开 FILE,FILE 中 包 含 文 件 描 述 符 fd,FILE 里 面 还 有 对 应 打 开 文 件 的 缓 冲 区 字 段。这 个 FILE 对 象 属 于 用 户,语 言 都 属 于 用 户 层。

  4. 显 示 器 的 文 件 刷 新 方 案 是 行 刷 新,所 以 在 printf 执 行 完 成 后 就 会 立 即 遇 到 \n 的 时 候,将 数 据 进 行 刷 新。刷 新 的 本 质 是 将 数 据 通 过 1 + write 通 过 write 接 口 写 入 到 内 核 中。

  5. 目 前 我 们 认 为,只 要 将 数 据 刷 新 进 入 了 内 核 中,数 据 就 会 被 刷 新 进 入 硬 件 中。
    在这里插入图片描述


2、exit 和 _exit

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 语 言 提 供 缓 冲 区 的 原 因

  1. 解 决 效 率 问 题,硬 件 设 备 的 读 写 速 度 远 慢 于 CPU 和 内 存 的 处 理 速 度。缓 冲 区 用 于 临 时 存 储 待 输 出 或 待 读 取 的 数 据。程 序 先 将 数 据 写 入 缓 冲 区,待 缓 冲 区 满、触 发 特 定 条 件 或 程 序 结 束 时,再 一 次 性 将 缓 冲 区 数 据 写 入 硬 件 设 备。这 大 幅 减 少 了 硬 件 I/O 的 次 数,从 而 提 升 整 体 效 率。
  2. 标 准 I/O 库 通 过 缓 冲 区 批 量 处 理 数 据,将 多 次 小 的 I/O 请 求 合 并 为 一 次 大 的 请 求,从 而 减 少 系 统 调 用 的 次 数,降 低 开 销。
  3. 优 化 用 户 交 互 体 验。数 据 暂 存 在 缓 冲 区,直 到 遇 到 换 行 符 \n 才 刷 新 到 屏 幕。这 种 设 计 符 合 人 类 阅 读 习 惯,用 户 更 希 望 看 到 完 整 的 一 行 内 容,而 非 字 符 逐 个 零 散 显 示。

二、模 拟 实 现 C 文 件 标 准 库

1、为 什 么 要 “造 轮 子”?

         我 们 平 时 写 C 语 言 文 件 操 作 时,习 惯 直 接 调 用 标 准 库 的 fopen、fwrite、fclose - - - 这 些 函 数 好 用,但 你 有 没 有 想 过:

  1. 为 什 么 fwrite 不 是 “写 了 就 立 刻 到 文 件”?
  2. FILE 结 构 体 里 到 底 存 了 什 么?
  3. 标 准 库 是 怎 么 跟 操 作 系 统 交 互 的?

         这 是 一 个 简 化 版 的 C 文 件 操 作 库:手 动 实 现 了 _fopen、_fwrite、_fflush、_fclose,没 有 依 赖 标 准 库 的 stdio.h,而 是 直 接 调 用 操 作 系 统 的 底 层 接 口(open、write、close),还 加 入 了 核 心 的 缓 冲 区 机 制。FILE 中 的 缓 冲 区 的 意 义 是 使 用 C 语 言 的 接 口 更 快,节 省 时 间。

2、核 心 原 理

         在 拆 解 代 码 前,先 搞 懂 3 个 核 心 概 念:系 统 调 用、缓 冲 区、刷 新 策 略 - - - 这 是 所 有 高 级 语 言 文 件 操 作 的 “通 用 逻 辑”。

(1)系 统 调 用 与 用 户 层 封 装

         Linux 操 作 系 统 提 供 了 最 底 层 的 文 件 操 作 接 口,称 为 系 统 调 用(如 open、write、close)。但 系 统 调 用 的 “开 销 很 高” - - - 每 次 调 用 都 要 从 用 户 态 切 换 到 内 核 态,频 繁 调 用 会 拖 慢 程 序。

         C 标 准 库 的 stdio 系 列 函 数(fopen、fwrite)本 质 是 对 系 统 调 用 的 封 装,核 心 目 的 是:在 用 户 层 加 一 层 “缓 冲 区”,减 少 系 统 调 用 的 次 数,从 而 提 高 IO 效 率。

         我 们 的 代 码 也 遵 循 这 个 逻 辑:

  1. 用 _FILE 结 构 体 封 装 文 件 描 述 符(fileno)和 缓 冲 区;
  2. _fopen 封 装 open 系 统 调 用;
  3. _fwrite 先 写 缓 冲 区,满 了 再 调 用 write;
  4. _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 

关 键 点

  1. 用 #ifndef MYFILE_H 防 止 头 文 件 被 重 复 包 含(比 如 main.c 和 myfile.c 都 包 含 myfile.h,编 译 时 不 会 报 错)。
  2. _FILE 结 构 体 只 实 现 了 输 出 缓 冲 区,输 入 缓 冲 区(inbuffer/in_pos)被 注 释 了。
  3. 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;  // 返回用户层的文件指针
}

关 键 点

  1. FILE_MODE 0666:文 件 权 限 的 八 进 制 表 示,rw-rw-rw-(所 有 者、组、其 他 用 户 都 有 读 写 权 限),但 实 际 权 限 会 受 umask 影 响(比 如 默 认 umask 0022,实 际 权 限 会 变 成 0644);
  2. open 的 标 志 组 合:不 同 模 式 对 应 不 同 的 标 志,比 如 w 模 式 必 须 加 O_TRUNC(清 空 文 件),a 模 式 必 须 加 O_APPEND(追 加);
  3. 内 存 分 配 检 查: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;  // 返回写入的字节数(简化处理,未检查实际写入量)
}

关 键 点

  1. 缓 冲 区 溢 出 风 险:当 前 代 码 直 接 memcpy,如 果 fp->out_pos + len > SIZE(比 如 剩 余 缓 冲 区 只 有 50 字 节,要 写 100 字 节),会 导 致 数 据 溢 出 outbuffer,破 坏 内 存 - - - 这 是 严 重 bug,后 续 优 化 会 解 决。
  2. 刷 新 策 略 的 实 际 效 果:当 前 _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结构体内存(避免内存泄漏)
}

核 心 原 则

  1. 必 须 先 刷 新 再 关 闭:如 果 不 调 用 _fflush,缓 冲 区 中 未 刷 的 数 据 会 随 着 fp 被 free 而 丢 失。
  2. 资 源 释 放 顺 序:先 释 放 用 户 层 资 源(缓 冲 区 数 据),再 释 放 内 核 层 资 源(文 件 描 述 符),最 后 释 放 内 存。

4、运 行 与 验 证

  1. 编 译 并 运 行 代 码
    在这里插入图片描述
  2. 观 察 结 果
    在这里插入图片描述
    代 码 运 行 多 次,会 继 续 追 加 内 容(因 为 打 开 模 式 是 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

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--