吃透 Linux “一切皆文件” 与缓冲区机制:从原理到实战


🔥草莓熊Lotso: 个人主页
❄️个人专栏: 《C++知识分享》 《Linux 入门到实践:零基础也能懂》
✨生活是默默的坚持,毅力是永久的享受!


🎬 博主简介:


文章目录

  • 前言:
  • [一. 深入理解 "一切皆文件":不是口号,是底层设计](#一. 深入理解 “一切皆文件”:不是口号,是底层设计)
    • [1.1 核心思想:统一的抽象接口](#1.1 核心思想:统一的抽象接口)
    • [1.2 底层实现:struct file 与 file_operations](#1.2 底层实现:struct file 与 file_operations)
    • [1.3 实战验证:不同设备的统一操作](#1.3 实战验证:不同设备的统一操作)
  • [二. 缓冲区机制:IO 效率的核心优化](#二. 缓冲区机制:IO 效率的核心优化)
    • [2.1 什么是缓冲区,为什么需要缓冲区?](#2.1 什么是缓冲区,为什么需要缓冲区?)
    • [2.2 三种缓冲区类型(标准 IO 库)](#2.2 三种缓冲区类型(标准 IO 库))
    • [2.3 实战验证:缓冲区的存在与影响](#2.3 实战验证:缓冲区的存在与影响)
  • [三. FILE 结构体:C 库 IO 的核心封装](#三. FILE 结构体:C 库 IO 的核心封装)
  • [四. 实战:自定义简易 C 标准库(模拟缓冲区机制)](#四. 实战:自定义简易 C 标准库(模拟缓冲区机制))
    • [4.1 头文件(my_stdio.h)](#4.1 头文件(my_stdio.h))
    • [4.2 实现文件(my_stdio.c)](#4.2 实现文件(my_stdio.c))
    • [4.3 测试代码(main.c)](#4.3 测试代码(main.c))
  • [五. 关键注意事项](#五. 关键注意事项)
  • 结尾:

前言:

Linux 的 "一切皆文件" 是贯穿整个系统的核心设计哲学,键盘、显示器、磁盘、网卡等所有设备都被抽象为文件,通过统一的 IO 接口操作;而缓冲区则是提升 IO 效率的关键机制,C 库函数与系统调用的核心差异之一就在于是否自带缓冲区。本文从 "一切皆文件" 的底层实现、缓冲区的类型与原理,到 FILE 结构体剖析、自定义简易 C 标准库,全程用实战代码验证,帮你彻底搞懂这两个 Linux IO 的核心知识点。


在正式开始之前,我们先来看一下下图中的问题以及解答,算是对之前文章的一个补充


一. 深入理解 "一切皆文件":不是口号,是底层设计

1.1 核心思想:统一的抽象接口

Linux 将所有设备和资源抽象为文件,并非指它们都是磁盘上的普通文件,而是通过统一的文件操作接口(open、read、write、close)来交互 。无论操作的是键盘、显示器还是网卡,都可以用一套 API 完成读写,极大降低了开发复杂度。
比如

  • 读键盘(标准输入 fd=0)和读磁盘文件,都用read函数;
  • 写显示器(标准输出 fd=1)和写网卡,都用write函数。

1.2 底层实现:struct file 与 file_operations

"一切皆文件" 的核心支撑是内核中的两个关键结构体:

  • struct file :每个打开的文件(含设备)在内核中都有一个file对象,存储文件的属性(权限、当前读写位置)、inode 指针等;
cpp 复制代码
struct file 
{
    ... 
    struct inode* f_inode; /* cached value */
    const struct file_operations* f_op;
    ... 
    atomic_long_t f_count; // 表⽰打开⽂件的引⽤计数,如果有多个⽂件指针指向它,就会增加f_count的值。 
    unsigned int f_flags; // 表⽰打开⽂件的权限
    fmode_t f_mode; // 设置对⽂件的访问模式,例如:只读,只写等。所有的标志在头⽂件<fcntl.h> 中定义 l
    off_t f_pos; // 表⽰当前读写⽂件的位置
    ...
} __attribute__((aligned(4))); /* lest something weird decides that 2 is OK
                                */

值得关注的是 struct file 中的 f_op 指针指向了⼀个 file_operations 结构体,这个结构体中的成员除了struct module* owner 其余都是函数指针。该结构和 struct file 都在fs.h下。

  • struct file_operations :函数指针结构体,封装了该文件的具体操作方法(read、write、open 等),不同设备的file_operations实现不同,但接口统一。
cpp 复制代码
struct file_operations {
    struct module *owner;                     // 拥有此结构的模块,通常设置为THIS_MODULE
    
    // 文件位置操作
    loff_t (*llseek) (struct file *, loff_t, int);          // 改变文件读写位置
    
    // 读写操作
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);      // 从设备读取数据
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); // 向设备写入数据
    
    // 异步读写操作
    ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);   // 异步读
    ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);  // 异步写
    
    int (*readdir) (struct file *, void *, filldir_t);      // 读取目录项(仅文件系统用)
    unsigned int (*poll) (struct file *, struct poll_table_struct *);  // 轮询操作
    
    // 设备控制接口(不同版本)
    int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);      // 旧版ioctl
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);            // 无锁ioctl
    long (*compat_ioctl) (struct file *, unsigned int, unsigned long);              // 兼容模式ioctl
    
    // 内存映射
    int (*mmap) (struct file *, struct vm_area_struct *);  // 设备内存映射到用户空间
    
    // 文件打开/关闭
    int (*open) (struct inode *, struct file *);          // 打开设备
    int (*flush) (struct file *, fl_owner_t id);          // 刷新缓存
    int (*release) (struct inode *, struct file *);       // 关闭设备/释放文件
    
    // 同步操作
    int (*fsync) (struct file *, struct dentry *, int datasync);  // 同步文件数据到存储设备
    int (*aio_fsync) (struct kiocb *, int datasync);              // 异步同步
    
    int (*fasync) (int, struct file *, int);              // 异步通知
    
    // 文件锁定
    int (*lock) (struct file *, int, struct file_lock *); // 文件锁定操作
    
    // 高级操作
    ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);  // 发送页到套接字
    
    unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, 
                                       unsigned long, unsigned long);  // 获取未映射区域
    
    int (*check_flags)(int);                              // 检查open标志
    int (*flock) (struct file *, int, struct file_lock *); // flock文件锁定
    
    // 管道相关操作
    ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, 
                            size_t, unsigned int);        // 从管道写入文件
    ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, 
                           size_t, unsigned int);         // 从文件读取到管道
    
    int (*setlease)(struct file *, long, struct file_lock **);  // 设置/获取文件租约
};

内核逻辑:

  • 进程通过文件描述符(fd)找到fd_array中的file对象;
  • file对象的f_op指针指向file_operations
  • 调用read(fd, ...)时,内核会通过fd找到对应的file_operations->read,执行具体设备的读操作。


上图中的外设,每个设备都可以有自己的read、write,但⼀定是对应着不同的操作方法!!但通过struct filefile_operation 中的各种函数回调,让我们开发者只用file便可调取 Linux 系统中绝大部分的资源!!这便是"linux下⼀切皆文件"的核心理解。

1.3 实战验证:不同设备的统一操作

用代码验证 "写显示器" 和 "写文件" 的接口统一性:

cpp 复制代码
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>

int main()
{
    // 写显示器(标准输出fd=1)
    const char* msg1 = "hello write to stdout\n";
    write(1, msg1, strlen(msg1));

    // 写磁盘文件(自定义fd)
    int fd = open("test.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
    const char* msg2 = "hello write to file\n";
    write(fd, msg2, strlen(msg2));
    close(fd);

    return 0;
}

编译运行后,无论是显示器还是文件,都通过write函数完成写入,这就是 "一切皆文件" 的直观体现。


二. 缓冲区机制:IO 效率的核心优化

2.1 什么是缓冲区,为什么需要缓冲区?

  • 缓冲区是内存空间的一部分。也就是说,在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区。缓冲区根据其对应的是输入设备还是输出设备,分为输入缓冲区和输出缓冲区。
  • 系统调用(如write)需要从用户态切换到内核态,上下文切换的开销较大。如果每次读写都直接调用系统调用,频繁的切换会严重降低效率。
  • 缓冲区的核心作用是批量 IO 操作:先将数据缓存到内存,达到一定条件后再一次性调用系统调用写入设备,减少上下文切换次数,提升效率。
  • 具体的可以看下图中的解释

2.2 三种缓冲区类型(标准 IO 库)

C 标准库(glibc)提供了三种缓冲区类型,适配不同场景(上面的图中也有):

  • 全缓冲区 :填满缓冲区后才执行系统调用,默认用于磁盘文件(如fopen打开的文件);
  • 行缓冲区 :遇到换行符\n或缓冲区满时执行系统调用,默认用于终端(stdin、stdout);
  • 无缓冲区:不缓存数据,直接执行系统调用,默认用于 stderr(确保错误信息及时输出)。

2.3 实战验证:缓冲区的存在与影响

用一个小实验验证 C 库函数与系统调用的缓冲区差异以及语言缓冲区和内核文件缓冲区的区别

cpp 复制代码
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
    // C库函数(带缓冲区)
    printf("hello printf\n");       // 行缓冲,遇\n刷新
    fprintf(stdout, "hello fprintf\n"); // 行缓冲
    const char* msg1 = "hello fputs\n";
    fputs(msg1, stdout);           // 行缓冲,无\n不刷新

    // 系统调用(无缓冲区)
    const char* msg2 = "hello write\n";
    write(1, msg2, strlen(msg2));  // 直接写入

    fork(); // 创建子进程,触发写时拷贝
    return 0;
}

测试结果分析:

  • 直接运行(输出到显示器,行缓冲):
  • 重定向到文件(全缓冲):
  • 这个现象是 C 库缓冲区机制和fork()的写时拷贝共同作用的结果,核心触发点在缓冲区刷新的修改动作上。

  • 首先,printf走 C 标准库的用户态缓冲区,输出到终端是行缓冲,\n会立刻刷走;但重定向到文件就变成全缓冲,内容会暂存在缓冲区里,不会马上写入内核。

  • 然后调用fork()创建子进程,此时父子进程会只读共享父进程的所有内存,包括这个未刷新的 C 库缓冲区,这一步还没触发写时拷贝,因为只是共享没修改。

  • 关键触发点来了:当父子进程退出(或主动刷缓冲区)时,会执行缓冲区刷新,这个操作会修改缓冲区(比如清空标记、移动指针),而对共享的只读内存做修改,就直接触发了写时拷贝 ------ 内核会为子进程复制一份独立的 C 库缓冲区,父子进程各自持有一份相同内容的缓冲区。

  • 最后,父子进程会各自把自己的缓冲区内容刷到内核,再写到文件里,所以printf的内容就重复了;而write是系统调用,直接写内核缓冲区,不走 C 库的用户态缓冲区,自然不会被复制,只输出一次。
    💡精简版总结

  • 重定向文件时printf的内容会暂存 C 库用户态缓冲区,fork后父子进程只读共享该缓冲区,此时未触发写时拷贝;

  • 父子进程退出刷新缓冲区时,修改共享缓冲区的动作触发写时拷贝,内核为子进程复制独立缓冲区,二者各存一份相同内容;

  • write直接写入内核缓冲区不走 C 库缓冲区,无复制过程,因此printf输出两次、write仅一次。


除了缓冲区满、换行符触发,以下情况也会刷新缓冲区

  • 调用fflush函数强制刷新(如fflush(stdout));
  • 进程正常退出(内核自动刷新缓冲区);
  • 关闭文件(fclose会先刷新缓冲区再关闭)。

三. FILE 结构体:C 库 IO 的核心封装

C 库函数(fopenfreadfwrite)的缓冲区和文件描述符都封装在FILE结构体中,定义在/usr/include/libio.h中,核心字段如下:

cpp 复制代码
struct _IO_FILE {
    int _fileno;          // 封装的文件描述符(fd)
    char* _IO_write_base; // 写缓冲区起始地址
    char* _IO_write_ptr;  // 写缓冲区当前指针(下一个写入位置)
    char* _IO_write_end;  // 写缓冲区结束地址
    char* _IO_buf_base;  // 缓冲区基地址
    // 其他字段:读缓冲区指针、标志位等
};
typedef struct _IO_FILE FILE;

核心逻辑:

  • fopen本质是调用open获取 fd,初始化FILE结构体的_fileno和缓冲区;
  • fwrite先将数据写入_IO_write_base_IO_write_end之间的缓冲区;
  • 当缓冲区满、遇\n(行缓冲)或调用~~fflush~~ 时,调用write(_fileno, ...)将缓冲区数据写入内核。

四. 实战:自定义简易 C 标准库(模拟缓冲区机制)

实现一个带缓冲区的简易 C 标准库,理解缓冲区的底层工作原理。

4.1 头文件(my_stdio.h)

cpp 复制代码
#pragma once 

#define SIZE 1024

// 定义为 1,2,4方便使用 & 操作, 都只有一个比特位为 1
#define FLUSH_NONE 1
#define FLUSH_LINE 2
#define FLUSH_FULL 4

typedef struct 
{
    int fileno; // 文件描述符
    int flags;
    int fstrategy; // 刷新策略
    char outbuffer[SIZE];
    int cap; // 容量
    int size;
    //char inbuffer[1024];
}My_FILE;


My_FILE* Myfopen(const char* pathname, const char* mode); // r w a
int Myfwrite(const char* message, int size, int num, My_FILE* fp);
void Myfflush(My_FILE* fp);
void Myfclose(My_FILE* fp);

4.2 实现文件(my_stdio.c)

cpp 复制代码
#include "my_stdio.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>

static mode_t fmode = 0666; // 文件权限

My_FILE* Myfopen(const char* pathname, const char* mode) // r w a
{
    if(pathname == NULL || mode == NULL)
        return NULL;
    umask(0);
    int fd = 0;
    int flags = 0;
    if(strcmp(mode, "w") == 0)
    {
        flags = O_CREAT | O_WRONLY | O_TRUNC;
        fd = open(pathname, flags, fmode);
        (void) fd;
    }
    if(strcmp(mode, "r") == 0)
    {
        flags = O_RDONLY;
        fd = open(pathname, flags);
        (void) fd;
    }
    if(strcmp(mode, "a") == 0)
    {
        flags = O_CREAT | O_WRONLY | O_APPEND;
        fd = open(pathname, flags, fmode);
        (void) fd;
    }
    else{}

    if(fd < 0) return NULL;
    // 创建 My_FILE对象
    My_FILE* fp = (My_FILE*)malloc(sizeof(My_FILE));
    if(!fp) return NULL;
    fp->fileno = fd;
    fp->flags = flags;
    fp->fstrategy = FLUSH_LINE;
    fp->outbuffer[0] = '\0';
   // memset(fp->outbuffer, 0, sizeof(fp->outbuffer));
   fp->cap = SIZE;
   fp->size = 0;
   return fp;
}


void Myfflush(My_FILE* fp)
{
    if(!fp) return;
    if(fp->size > 0)
    {
        // 写到内核文件的文件缓冲区中
        // 所谓的刷新就是把数据从用户缓冲区拷贝到内核
        // 从用户缓冲区拷贝到内核这种模式叫做WB模式
        // WB: Write Back(写回)
        write(fp->fileno, fp->outbuffer, fp->size);
        // 刷新到外设,不仅仅要写入到内核缓冲区,还必须写到对应的硬件上
        // WT模式,Write Though
        fsync(fp->fileno);
        fp->size = 0;
    }
}

int Myfwrite(const char* message, int size, int num, My_FILE* fp)
{
    if(message == NULL || fp == NULL) return 0;

    // C语言向文件写入实际上是向缓冲区写入
    int sizeNum = size * num;
    if(fp->size + sizeNum < fp->cap - 1) // 预留\0的位置
    {
        memcpy(fp->outbuffer + fp->size, message, sizeNum);
        fp->size += sizeNum;
        fp->outbuffer[fp->size] = 0;
    }

    // 刷新缓冲区条件: 不是每次都刷新的,这样也可以加快响应速度
    if(fp->size > 0 && fp->outbuffer[fp->size - 1] == '\n' && (fp->fstrategy & FLUSH_LINE))
    {
        Myfflush(fp);
    }

    return sizeNum;
}


void Myfclose(My_FILE* fp)
{
    if(fp->size > 0)
    {
        Myfflush(fp);
    }
    close(fp->fileno);
}

4.3 测试代码(main.c)

cpp 复制代码
#include "my_stdio.h"
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>

int main()
{
    My_FILE* fp = Myfopen("log.txt", "a");
    if(!fp) return 1;

    const char* msg = "hello Lotso \n";
    int cnt = 10;
    while(cnt--)
    {
        Myfwrite(msg, 1, strlen(msg), fp);
       //  Myfflush(fp); // 如果没有换行
        sleep(2);
    }

    Myfclose(fp);
    return 0;
}


  • 大家还可以试下吧换行符去掉之后再测试,这时候需要自己flush了。
  • 补充:一个小现象解析 :为什么带上关闭文件就无法输出fd了,不带是正常的。该怎么解决?
  • 解析:这是因为 printf 写入的是用户态缓冲区,而 close(fd) 直接关闭了内核中的文件描述符,导致程序退出时缓冲区数据无法刷新到已关闭的文件中。
cpp 复制代码
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>   // 为了printf和fflush

int main()
{
    umask(0);
    close(1);
    int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
    if(fd < 0)
    {
        perror("open");
        return 1;
    }

    // stdout -> 1
    printf("fd: %d\n", fd); // stdout->缓冲区里面->close(fd)->关闭刷新通道,数据无法写入到内核

    fflush(stdout);   // 刷新stdout缓冲区,确保数据写入文件

    close(fd); // 系统调用
    return 0;
}

五. 关键注意事项

  • 缓冲区与重定向 :stdout 默认是行缓冲(输出到终端),重定向到文件后变为全缓冲,需注意fflush强制刷新,避免数据丢失;
  • stderr 无缓冲:标准错误流 stderr 默认无缓冲,确保错误信息及时输出,无需手动刷新;
  • FILE 与 fd 的关系 :C 库函数的FILE封装了 fd,fclose会关闭 fd,不要混用 C 库函数和系统调用操作同一文件;
  • 缓冲区刷新时机 :进程异常退出(如abort)不会刷新缓冲区,可能导致数据丢失,关键场景需手动fflush

结尾:

html 复制代码
🍓 我是草莓熊 Lotso!若这篇技术干货帮你打通了学习中的卡点:
👀 【关注】跟我一起深耕技术领域,从基础到进阶,见证每一次成长
❤️ 【点赞】让优质内容被更多人看见,让知识传递更有力量
⭐ 【收藏】把核心知识点、实战技巧存好,需要时直接查、随时用
💬 【评论】分享你的经验或疑问(比如曾踩过的技术坑?),一起交流避坑
🗳️ 【投票】用你的选择助力社区内容方向,告诉大家哪个技术点最该重点拆解
技术之路难免有困惑,但同行的人会让前进更有方向~愿我们都能在自己专注的领域里,一步步靠近心中的技术目标!

结语:"一切皆文件" 是 Linux 统一 IO 接口的设计哲学,核心是通过struct file和file_operations实现设备抽象;而缓冲区是提升 IO 效率的关键,C 库函数通过封装缓冲区减少系统调用次数。本文从底层原理到实战代码,覆盖了 "一切皆文件" 的实现、缓冲区的类型与机制、FILE 结构体剖析、自定义 C 标准库,帮你打通 Linux IO 的核心知识点。掌握这些内容后,无论是日常开发中的 IO 优化,还是排查缓冲区导致的数据丢失问题,都能游刃有余。

✨把这些内容吃透超牛的!放松下吧✨ ʕ˘ᴥ˘ʔ づきらど

相关推荐
ZHOUPUYU3 小时前
PHP 8.3网关优化:我用JIT将QPS提升300%的真实踩坑录
开发语言·php
安科士andxe5 小时前
深入解析|安科士1.25G CWDM SFP光模块核心技术,破解中长距离传输痛点
服务器·网络·5g
九.九7 小时前
ops-transformer:AI 处理器上的高性能 Transformer 算子库
人工智能·深度学习·transformer
春日见7 小时前
拉取与合并:如何让个人分支既包含你昨天的修改,也包含 develop 最新更新
大数据·人工智能·深度学习·elasticsearch·搜索引擎
恋猫de小郭7 小时前
AI 在提高你工作效率的同时,也一直在增加你的疲惫和焦虑
前端·人工智能·ai编程
寻寻觅觅☆7 小时前
东华OJ-基础题-106-大整数相加(C++)
开发语言·c++·算法
deephub8 小时前
Agent Lightning:微软开源的框架无关 Agent 训练方案,LangChain/AutoGen 都能用
人工智能·microsoft·langchain·大语言模型·agent·强化学习
fpcc8 小时前
并行编程实战——CUDA编程的Parallel Task类型
c++·cuda
l1t8 小时前
在wsl的python 3.14.3容器中使用databend包
开发语言·数据库·python·databend
小白同学_C8 小时前
Lab4-Lab: traps && MIT6.1810操作系统工程【持续更新】 _
linux·c/c++·操作系统os