Linux基础I/O(2):理解“一切皆文件”与缓冲区

理解"一切皆文件"

哪些东西都算作文件

  1. Windows 里认定的文件,在 Linux 同样是文件:文本、程序、图片、压缩包等磁盘存储文件,常规增删改查逻辑完全一致。
  2. Windows 中独立分类、不属于文件的对象,Linux 全部抽象成文件形态,可按文件方式访问:
  • 硬件设备:键盘、显示器、硬盘、网卡、鼠标
  • 进程资源:运行中的程序、进程状态信息
  • 通信载体:进程间管道、网络编程套接字 Socket

核心设计思想

  • 形态统一
    抹平硬件、软件、通信、进程的外形差异,不分设备类型、存储位置,全部归类为逻辑文件。
  • 接口统一
    对外只暴露 open/read/write/close 四个基础系统调用,读取、写入、打开、关闭行为通用,无需为不同资源编写专属函数。
  • 寻址统一
    所有打开的资源,都用文件描述符 fd作为唯一访问标识,通过进程文件表定位资源。
  • 规则统一
    权限校验、资源占用、生命周期管理、用户归属,全部沿用文件管理规则。

底层内核实现


架构层级自上而下
用户态统一接口VFS虚拟文件系统(核心抽象层)进程文件描述符体系底层资源适配层

1. 层级 1:用户态统一调用入口

应用程序无论操作什么资源,代码写法完全一致

  • 打开资源:open()
  • 读取数据:read()
  • 写入数据:write()
  • 释放资源:close()
    应用层完全感知不到底层是屏幕、磁盘还是网络,只传递文件描述符fd即可。

2. 层级 2:VFS 虚拟文件系统

VFS 本身不存储数据,是内核的中间抽象调度层

  • 定义一套标准文件操作规范,强制所有底层资源都遵循这套规范开发接口

关键核心:struct file_operations

当打开一个文件时,操作系统为了管理所打开的文件,都会为这个文件创建一个file结构体,而struct file里面有一个重要成员:

c 复制代码
struct file {
    // ... 其他成员(偏移量、权限、引用计数)
    const struct file_operations *f_op;  // 核心!
};

struct filef_op 指针,直接指向 struct file_operations 结构体struct file_operations标准文件操作函数指针集合 ,这个结构体中的成员除了struct module* owner 其余都是函数指针 。通过这个结构体的指针,就能自动执行不同的硬件 / 资源逻辑

精简源码:

c 复制代码
struct file_operations {
    struct module *owner;                                          /* 所属模块指针 */
    loff_t (*llseek) (struct file *, loff_t, int);                 /* 修改文件偏移 */
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); /* 同步读取数据 */
    ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t); /* 异步读初始化 */
    ssize_t (*write) (struct file *, const char __user *, size_t, 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); /* 兼容32位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);                                      /* 检查文件标志 */
    int (*dir_notify)(struct file *filp, unsigned long arg);      /* 目录通知 */
    int (*flock) (struct file *, int, struct file_lock *);        /* 建议性文件锁 */
    ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int); /* splice写入 */
    ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int); /* splice读取 */
    int (*setlease)(struct file *, long, struct file_lock **);    /* 设置文件租约 */
};

struct file_operations的作用:规定了「文件 / 设备 / 管道 / Socket」能做什么操作(读、写、打开、关闭)

3. 层级 3:进程私有文件寻址体系

每个进程独立维护一套文件管理结构,形成固定访问链路

bash 复制代码
task_struct 进程控制块
    ↓
files_struct 文件描述符总表
    ↓
fd_array[] 指针数组 (数组下标 = 文件描述符fd)
    ↓
struct file 打开实例
    ↓
file_operations 函数指针集
    ↓
底层硬件/文件/管道/Socket 真实逻辑

4. 层级 4:底层资源适配层

各类差异化资源,各自实现 VFS 标准接口,完成封装

如:磁盘文件:ext4、xfs 等文件系统,实现读写磁盘逻辑

缓冲区

什么是缓冲区?

缓冲区就是内存里一块临时开辟的存储空间,专门用来临时存放待读写的数据。

缓冲区的作用:攒够一批数据,再一次性批量读写外设,大幅提升程序效率。

简单理解:

缓冲区 = 快递驿站

数据 = 快递

外设 = 收件人

为什么必须存在缓冲区

  • 高速 CPU 与低速外设天生不匹配,缓冲区充当速度缓冲带
  • 减少系统调用,降低用户态与内核态切换开销
    printf/scanf 最终都会触发read/write系统调用。每一次系统调用,都会发生用户态 ↔ 内核态切换

Linux IO 两层缓冲区

1. 用户态标准库缓冲区

  • 名字:C 标准库缓冲区 / 用户态缓冲
  • 位置:进程的用户空间内存(每个进程独立一份,互不干扰)
  • 管理者:C 语言标准库 stdio.h
  • 绑定对象printf/fprintf/cout/scanf/fopen库函数

核心作用:

解决 用户态 ↔ 内核态切换开销太大 的问题

每一次系统调用(write)都要切换状态,成本极高;缓冲区先把数据攒在用户空间,攒够一批再调用一次系统调用;大幅减少切换次数,提升程序效率。

缓冲模式
1. 无缓冲:数据产生后立刻、直接写入内核缓冲区,不做任何暂存

默认绑定对象
标准错误 stderr(fd=2)

2. 行缓冲 :缓冲区遇到换行符 \n 才会刷新数据

默认绑定对象
连接「终端 / 显示器」的标准输出 stdout(fd=1)

3. 全缓冲 :缓冲区写满固定大小(默认 4KB) 才刷新数据

默认绑定对象

  1. 所有普通磁盘文件
  2. 重定向到文件的标准输出 stdout(fd=1)

其他刷新规则

  • 程序正常退出
  • 手动调用 fflush(stdout)
  • 关闭文件描述符 close(fd)

注意:close(fd) 只刷新内核缓冲区,完全不刷新用户态缓冲区

2. 内核文件缓冲区

  • 位置:内核空间内存
  • 管理者:Linux 内核(应用程序完全无法直接操作)
  • 绑定对象write/read/open/close 等系统调用

刷新规则

  • 内核自动定时刷新到硬件
  • 应用程序无需干预

看一个例子:

c 复制代码
#include <cstdio>    
#include <iostream>    
#include <cstring>    
#include <unistd.h>    
    
int main()    
{    
    printf("hello printf\n");    
    fprintf(stdout, "hello fprintf\n");    
    const char *s = "hello fwrite\n";    
    fwrite(s, strlen(s), 1, stdout);    
    
    // 系统调用    
    const char *ss = "hello write\n";    
    write(1, ss, strlen(ss));    
    
    fork();                                                                                         
    
    return 0;    
}

在场景2(对进程实现输出重定向)中,我们可以发现 printffprintffwrite(库函数)都调用了两次,而 write(系统调用)只调用了一次。为什么呢?这肯定与fork有关。

场景 1:直接运行(终端)
原理:

  • stdout行缓冲 ,遇到 \n 立即刷新用户缓冲区
  • write 系统调用直接输出到屏幕
  • fork 时用户缓冲区是空的

场景 2:重定向(文件)
原理:

  • stdout 自动切换为 全缓冲 ,规则:\n 完全失效,缓冲区不刷新,直到程序退出才刷
  • printf/fprintf/fwrite → 数据全部留在用户态缓冲区(不写入文件)
  • write 系统调用 → 直接写入文件
  • 执行 fork() 创建子进程 → 子进程会完整复制父进程的用户态缓冲区
  • 父子进程先后退出时,各自刷新自己的缓冲区,各自写入3条数据

解决方法 :在 fork() 之前,手动刷新用户态缓冲区,清空数据!

因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的。所以C库当中的FILE结构体内部,必定封装了fd。

FILE结构体

  • 归属:C 语言标准库 <stdio.h> 定义,运行在用户空间
  • 本质 :一个 封装了「文件描述符 fd + 用户态缓冲区 + 缓冲模式 + 文件状态」 的用户态结构体
  • 作用 :给 printf/fopen/fread/fflush 等库函数提供上下文管理,屏蔽底层内核细节

简化版FILE结构体:

c 复制代码
// C标准库 stdio.h 中的 FILE 结构体(简化版)
typedef struct _IO_FILE FILE;

struct _IO_FILE {
    // 1. 核心:文件描述符 fd(对应内核的 int fd)
    int _fileno;    
    // 2. 核心:用户态缓冲区(printf 数据暂存的地方!)
    char* _buf;     
    // 缓冲区总大小(默认 4KB 全缓冲,行缓冲更小)
    int _buf_size;  
    // 缓冲区当前读写位置
    int _ptr;       
    // 3. 核心:缓冲模式(无缓冲/行缓冲/全缓冲)
    int _flags;    
    // 4. 文件状态(读写权限、是否结束、错误标记)
    int _mode;      
};

简单模拟一个glibc库

补充:fsync 系统调用

函数原型

c 复制代码
#include <unistd.h>

// 成功返回0,失败返回-1
int fsync(int fd);

作用:强制将指定文件描述符对应的「内核缓冲区(页缓存)」中的所有数据,同步、阻塞地写入物理磁盘,直到数据完全落盘才返回。

总结

  • fsync = 内核系统调用,刷内核缓冲区 → 物理磁盘
  • fflush = 库函数,刷用户缓冲区 → 内核

头文件

c 复制代码
#pragma once     
    
#include <stdio.h>    
    
#define MAX 1024    
#define NONE_FLUSH (1<<0)    
#define LINE_FLUSH (1<<1)    
#define FULL_FLUSH (1<<2)                                                                           
    
typedef struct IO_FILE    
{    
    int fileno;    
    int flag;    
    char outbuffer[MAX];    
    int bufferlen;    
    int flush_method;    
}MyFile;    
    
MyFile *MyFopen(const char *path, const char *mode);    
void MyFclose(MyFile *);    
int MyFwrite(MyFile *, void *str, int len);    
void MyFFlush(MyFile *);

实现文件

c 复制代码
#include "mystdio.h"                                                                                
#include <sys/types.h>    
#include <sys/stat.h>                  
#include <fcntl.h>    
#include <string.h>       
#include <stdlib.h>               
#include <unistd.h>    
            
static MyFile *BuyFile(int fd, int flag)    
{                  
    MyFile *f = (MyFile*)malloc(sizeof(MyFile));    
    if(f == NULL) return NULL;    
    f->bufferlen = 0;            
    f->fileno = fd;    
    f->flag = flag;    
    f->flush_method = LINE_FLUSH;    
    memset(f->outbuffer, 0, sizeof(f->outbuffer));    
    return f;                       
}                      
                            
MyFile *MyFopen(const char *path, const char *mode)    
{    
    int fd = -1;    
    int flag = 0;                                 
    if(strcmp(mode, "w") == 0)    
    {             
        flag = O_CREAT | O_WRONLY | O_TRUNC;              
        fd = open(path, flag, 0666);    
    }                                 
    else if(strcmp(mode, "a") == 0)                                                        
    {    
        flag = O_CREAT | O_WRONLY | O_APPEND;    
        fd = open(path, flag, 0666);    
    }            
    else if(strcmp(mode, "r") == 0)    
    {    
        flag = O_RDWR;         
        fd = open(path, flag);    
    }                                   
    else                                     
    {                                                                 
        // TODO    
    }    
    if(fd < 0) return NULL;    
    return BuyFile(fd, flag);
    }

void MyFclose(MyFile *file)
{
    if(file->fileno < 0) return;
    MyFFlush(file);
    close(file->fileno);
    free(file);
}

int MyFwrite(MyFile *file, void *str, int len)
{                                                                                                   
    // 1. 拷贝
    memcpy(file->outbuffer+file->bufferlen, str, len);
    file->bufferlen += len;
    // 2. 尝试判断是否满足刷新条件
    if((file->flush_method & LINE_FLUSH) && file->outbuffer[file->bufferlen-1] == '\n')
    {
        MyFFlush(file);
    }
    return 0;
}

void MyFFlush(MyFile *file)
{
    if(file->bufferlen <= 0) return;
    // 把数据从用户拷贝到内核文件缓冲区中
    int n = write(file->fileno, file->outbuffer, file->bufferlen);
    (void)n;
    fsync(file->fileno);
    file->bufferlen = 0;
}

测试代码

c 复制代码
#include "mystdio.h"    
#include <string.h>    
#include <unistd.h>    
    
int main()    
{    
    MyFile *filep = MyFopen("./log.txt", "a");    
    if(!filep)    
    {    
        printf("fopen error\n");    
        return 1;    
    }    
    
    int cnt = 10;    
    while(cnt--)    
    {    
        char *msg = (char*)"hello myfile!!!";    
        MyFwrite(filep, msg, strlen(msg));    
        MyFFlush(filep);                                                                            
        printf("buffer: %s\n", filep->outbuffer);    
        sleep(1);    
    }    
    MyFclose(filep); // FILE *fp    
    return 0;    
}
相关推荐
苏宸啊9 小时前
库的使用和制作
运维·服务器
爱吃龙利鱼9 小时前
MobaXterm连接ubuntu26.04无法在vim界面粘贴问题解决方法(粘贴会提示进入进入可视模式VISUAL))
linux·ubuntu·编辑器·vim
.柒宇.9 小时前
Zabbix7.0部署完整指南
linux·运维·zabbix·监控
learndiary9 小时前
Linux 维修案例视频12则
linux·维修
wanhengidc9 小时前
云手机手游搬砖 梦境护卫队
运维·服务器·安全·web安全·智能手机
小小de风呀9 小时前
de风——【从零开始学Linu】 - 基础指令详解(二)
linux·运维·服务器
littleschemer9 小时前
Go:实现游戏服务器网关
服务器·网关·游戏·golang
楼田莉子9 小时前
C#学习:分支与循环
服务器·后端·学习·c#
cws2004019 小时前
网络安全基本知识-2
运维·网络