当前位置: 首页 > news >正文

Linux 基础IO-从 “一切皆文件” 到自定义 libc 缓冲区

前言

在 C 语言文件操作的学习中,我们常会遇到两个 “绕不开” 的核心问题:为什么说操作系统中 “一切皆文件”?printffwrite这些库函数比系统调用write更高效的秘密是什么?这两个问题的答案,其实都指向同一个关键概念 ——缓冲区。

很多开发者对文件操作的认知停留在 “调用函数读写数据” 的表层,却忽略了缓冲区的存在:它是 libc 库(C 标准库)为提升性能设计的 “中间层”,也是连接用户代码与系统内核的重要桥梁。而 “一切皆文件” 的理念,则为键盘、显示器、磁盘文件等不同设备提供了统一的操作接口,让缓冲区的复用成为可能。

本文将从 “一切皆文件” 的底层逻辑切入,逐步拆解缓冲区的本质、引入原因与三种缓冲类型,再通过实际现象观察验证缓冲机制的存在。最终,我们会亲手设计一个简化版的 libc 文件操作库(包含mystdio.h头文件、mystdio.c实现与usercode.c测试代码),让你从 “使用者” 转变为 “设计者”,彻底理解 C 语言文件操作的底层逻辑。无论你是刚接触文件操作的新手,还是想夯实底层基础的开发者,都能在本文中找到清晰的答案。

目录

理解“一切皆文件”

什么是缓冲区

 为什么要引入缓冲区机制

 缓冲类型

现象观察

FILE

简单设计一下libc库

mystdio.h

mystdio.c

usercode.c


理解“一切皆文件”

首先,在windows中是文件的东西,它们在linux中也是文件;其次一些在windows中不是文件的东西,比如进程、磁盘、显示器、键盘这样硬件设备也被抽象成了文件,你可以使用访问文件的方法访问它们获得信息;甚至管道,也是文件;网络编程中的socket(套接字)这样的东西, 使用的接口跟文件接口也是一致的。
这样做最明显的好处是,开发者仅需要使用一套 API 和开发工具,即可调取 Linux 系统中绝大部分的资源。举个简单的例子,Linux 中⼏乎所有读(读文件,读系统状态,读PIPE)的操作都可以用read 函数来进行;几乎所有更改(更改文件,更改系统参数,写 PIPE)的操作都可以用 write 函数来进行。
之前我们讲过,当打开一个文件时,操作系统为了管理所打开的文件,都会为这个文件创建一个file结构体。
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> 中定义
loff_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_operations {
struct module *owner;
//指向拥有该模块的指针;
loff_t (*llseek) (struct file *, loff_t, int);
//llseek ⽅法⽤作改变⽂件中的当前读/写位置, 并且新位置作为(正的)返回值.
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
//⽤来从设备中获取数据. 在这个位置的⼀个空指针导致 read 系统调⽤以 -
EINVAL("Invalid argument") 失败. ⼀个⾮负返回值代表了成功读取的字节数( 返回值是⼀个
"signed size" 类型, 常常是⽬标平台本地的整数类型).
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
//发送数据给设备. 如果 NULL, -EINVAL 返回给调⽤ write 系统调⽤的程序. 如果⾮负, 返
回值代表成功写的字节数.
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);
//对于设备⽂件这个成员应当为 NULL; 它⽤来读取⽬录, 并且仅对**⽂件系统**有⽤.
unsigned int (*poll) (struct file *, struct poll_table_struct *);
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);//mmap ⽤来请求将设备内存映射到进程的地址空间. 如果这个⽅法是 NULL, mmap 系统调⽤返
回 -ENODEV.
int (*open) (struct inode *, struct file *);
//打开⼀个⽂件
int (*flush) (struct file *, fl_owner_t id);
//flush 操作在进程关闭它的设备⽂件描述符的拷⻉时调⽤;
int (*release) (struct inode *, struct file *);
//在⽂件结构被释放时引⽤这个操作. 如同 open, release 可以为 NULL.
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 *);
//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 (*flock) (struct file *, int, struct file_lock *);
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 **);
};
file_operation 就是把系统调用和驱动程序关联起来的关键数据结构,这个结构的每一个成员都
对应着一个系统调用。读取 file_operation 中相应的函数指针,接着把控制权转交给函数,从而完成了Linux设备驱动程序的工作。
上图中的外设,每个设备都可以有自己的read、write,但一定是对应着不同的操作方法!!但通过struct file 下 file_operation 中的各种函数回调,让我们开发者只用file便可调取 Linux 系统中绝大部分的资源!!这便是“linux下一切皆文件”的核心理解。

什么是缓冲区

缓冲区是内存空间的一部分。也就是说,在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区。缓冲区根据其对应的是输入设备还是输出设备,分为输入缓冲区和输出缓冲区。

 为什么要引入缓冲区机制

读写文件时,如果不会开辟对文件操作的缓冲区,直接通过系统调用对磁盘进行操作(读、写等),那么每次对文件进行一次读写操作时,都需要使用读写系统调用来处理此操作,即需要执行一次系统调 用,执行一次系统调用将涉及到CPU状态的切换,即从用户空间切换到内核空间,实现进程上下文的 切换,这将损耗一定的CPU时间,频繁的磁盘访问对程序的执行效率造成很大的影响。
为了减少使用系统调用的次数,提高效率,我们就可以采用缓冲机制。比如我们从磁盘里取信息,可 以在磁盘文件进行操作时,可以一次从文件中读出大量的数据到缓冲区中,以后对这部分的访问就不 需要再使用系统调用了,等缓冲区的数据取完后再去磁盘中读取,这样就可以减少磁盘的读写次数, 再加上计算机对缓冲区的操作大 快于对磁盘的操作,故应用缓冲区可大提高计算机的运行速度。 又比如,我们使用打印机打印文档,由于打印机的打印速度相对较慢,我们先把文档输出到打印机相 应的缓冲区,打印机再自行逐步打印,这时我们的CPU可以处理别的事情。
可以看出,缓冲区就是一块内存区,它用在输入输出设备和CPU之间,用来缓存数据。它使得低速的输入输出设备和高速的 CPU能够协调工作,避免低速的输入输出设备占用CPU,解放出CPU,使其能够高效率工作。

 缓冲类型

多次printf以及其他函数的数据放到语言层缓冲区,后面只需要一次刷新,调用一次系统调用就可以写到文件中。

数据交给系统,交给硬件 ---本质全是拷贝!!!

计算器数据流动的本质 :一切皆拷贝!!!

标准I/O提供了3种类型的缓冲区。
全缓冲区:这种缓冲方式要求填满整个缓冲区后才进⾏I/O系统调用操作。对于磁盘文件的操作通
常使用全缓冲的方式访问。
行缓冲区:在行缓冲情况下,当在输入和输出中遇到换行符时,标准I/O库函数将会执行系统调用操作。当所操作的流涉及一个终端时(例如标准输入和标准输出),使用行缓冲方式。因为标准I/O库每行的缓冲区长度是固定的,所以只要填满了缓冲区,即使还没有遇到换行符,也会执行I/O系统调用操作,默认行缓冲区的大小为1024。
无缓冲区:无缓冲区是指标准I/O库不对字符进行缓存,直接调用系统调用。标准出错流stderr通常是不带缓冲区的,这使得出错信息能够尽快地显示出来。
除了上述列举的默认刷新方式,下列特殊情况也会引发缓冲区的刷新:
1. 缓冲区满时;
2. 执行flush语句;

现象观察

close(1);
int fd1=open("log1.txt",O_CREAT | O_WRONLY | O_APPEND,0666);
if(fd1<0) exit(1);
printf("fd1:%d\n",fd1);
printf("hello C!\n");
printf("hello C!\n");
close(fd1);   

当加入close(fd1),关闭文件后,发现没有写入log1.txt

用write就写进去了。文件描述符关不关闭无所谓。

根据前面的知识,printf是库函数,write是系统调用

我们在关闭前可以fflush一下。

大致可以理解,在文件描述符关闭之前,用户没有刷新等操作,没有进行下一步,且文件描述符关闭后,就不能通过系统调用进行刷新到文件缓冲区。

像fopen,fflush 都提到了FILE*,C语言中,一个文件,都要有自己的缓冲区。

我们提到的printf,fprintf,fputs等都与stdout有关,而stdout也是FILE*类型的。

而FILE是C语言的一个结构体,里面封装了文件描述符,和缓冲区。

FILE

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

#include <stdio.h>
#include <string.h>
int main()
{
const char *msg0="hello printf\n";
const char *msg1="hello fwrite\n";
const char *msg2="hello write\n";
printf("%s", msg0);
fwrite(msg1, strlen(msg1), 1, stdout);
write(1, msg2, strlen(msg2));
fork();
return 0;
}

但如果对进程实现输出重定向呢? ./myfile > file , 我们发现结果变成了:

发现 printf 和 fwrite (库函数)都输出了2次,而 write 只输出了一次(系统调用)。为
什么呢?肯定和fork有关!
一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲。
printf fwrite 库函数+会自带缓冲区(进度条例子就可以说明),当发生重定向到普通文
件时,数据的缓冲方式由行缓冲变成了全缓冲。
而我们放在缓冲区中的数据,就不会被立即刷新,甚至fork之后
但是进程退出之后,会统一刷新,写入文件当中。
但是fork的时候,父子数据会发生写时拷贝,所以当你父进程准备刷新的时候,子进程也就有了同样的一份数据,随即产生两份数据。
write 没有变化,说明没有所谓的缓冲。
综上: printf fwrite 库函数会自带缓冲区,而 write 系统调用没有带缓冲区。另外,我们这
里所说的缓冲区,都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区,不过不再我们讨论范围之内。
那这个缓冲区谁提供呢? printf fwrite 是库函数, write 是系统调用,库函数在系统调用的
“上层”, 是对系统调用的“封装”,但是 write 没有缓冲区,而 printf fwrite 有,足以说明,该缓冲区是二次加上的,又因为是C,所以由C标准库提供。

简单设计一下libc库

mystdio.h

#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*);

mystdio.c

  1 #include "mystdio.h"2 #include <sys/types.h>3 #include <sys/stat.h>4 #include <fcntl.h>5 #include <string.h>6 #include <stdlib.h>7 #include <unistd.h>8 9 static MyFile *BuyFile(int fd,int flag){10     MyFile *f=(MyFile*)malloc(sizeof(MyFile));11     if(f==NULL) return NULL;12     f->bufferlen=0;13     f->fileno=fd;14     f->flag=flag;15     f->flush_method=LINE_FLUSH;16     memset(f->outbuffer,0,sizeof(f->outbuffer));17     return f;18 }19 MyFile *MyFopen(const char*path,const char*mode){20     int fd=-1;21     int flag=0;22     if(strcmp(mode,"w")==0){23         flag=O_CREAT | O_WRONLY | O_TRUNC;24         fd=open(path,flag,0666);25 26     }27     else if(strcmp(mode,"a")==0){28         flag=O_CREAT | O_WRONLY | O_APPEND;29         fd=open(path,flag,0666);30 }31 else if(strcmp(mode,"r")==0){32     flag= O_RDWR;33     fd=open(path,flag);34 }35 else{3637 }                                                                                                                                                                                                            38     if(fd<0) return NULL;39     return BuyFile(fd,flag);40 }41 void MyFclose(MyFile* file){42     if(file->fileno <0 )return;43     MyFFlush(file);44     close(file->fileno);45     free(file);46 47 }48 int MyFwrite(MyFile *file,void *str,int len){49    //拷贝50     memcpy(file->outbuffer+file->bufferlen,str,len);51     file->bufferlen+=len;52    //尝试判断是否满足刷新条件53     if(file->flush_method & LINE_FLUSH && file->outbuffer[file->bufferlen-1]=='\n'){54         MyFFlush(file);55     }56     return 0;57 }58 void MyFFlush(MyFile* file){59     if(file->bufferlen <0) return ;60     int n=write(file->fileno,file->outbuffer,file->bufferlen);61     (void)n;62     fsync(file->fileno);63     file->bufferlen=0;64 }

usercode.c

  1 #include <string.h>2 #include "mystdio.h"3 #include <unistd.h>4 int main()5 {6     MyFile* filep=MyFopen("./log.txt","a");7     if(!filep){8         printf("fopen error!\n");9         return 1;10     }11     int cnt=6;12     while(cnt--){13     //char *msg="hello myfile !\n";                                                                                                                                                                          14     char *msg="hello myfile !!!";15     MyFwrite(filep,msg,strlen(msg));16     MyFFlush(filep);17     printf("buffer:%s\n",filep->outbuffer);18     sleep(1);19     }20     MyFclose(filep);21     return 0;22 }

当写入的字符串有“\n”和强制刷新时:

没有‘\n’和强制刷新:

有强制刷新但没有"\n"

结束语

从 “一切皆文件” 的统一接口,到缓冲区的性能优化,再到自定义 libc 库的实践,C 语言文件操作的核心逻辑始终围绕 “高效、统一” 两个关键词展开。缓冲区看似是 “额外的中间层”,实则是 libc 库平衡 “系统调用开销” 与 “用户操作便捷性” 的精妙设计;而 “一切皆文件” 的理念,则让这种设计能无缝适配键盘、显示器、磁盘等不同设备,极大降低了开发者的学习与使用成本。

通过亲手设计简化版mystdio库,我们不仅理清了FILE结构体、缓冲策略、读写接口的实现逻辑,更能体会到 “从抽象到具体” 的编程思维 —— 日常使用的库函数并非 “黑箱”,只要拆解其核心模块,就能理解其设计本质。

当然,真实的 libc 库(如 GNU C 库)远比我们设计的简化版复杂,还包含缓冲刷新策略、线程安全、错误处理等进阶功能,但本文搭建的 “框架” 已能覆盖核心逻辑。希望这篇文章能成为你理解 C 语言文件操作的 “敲门砖”,让你在后续使用或优化文件操作代码时,能从底层逻辑出发,做出更合理的选择。


文章转载自:

http://3iYthnx8.nbrdx.cn
http://mbF8K4D9.nbrdx.cn
http://FTCefrMD.nbrdx.cn
http://buspCpq9.nbrdx.cn
http://d2dOCki4.nbrdx.cn
http://JJ04Zd8Z.nbrdx.cn
http://E6zaByg0.nbrdx.cn
http://scMvqYtA.nbrdx.cn
http://4Gs24P1V.nbrdx.cn
http://3NX2oV9p.nbrdx.cn
http://OBFxLS1d.nbrdx.cn
http://h3FXEqti.nbrdx.cn
http://u9XA2shl.nbrdx.cn
http://0kaSc1ph.nbrdx.cn
http://rC6pu7Dy.nbrdx.cn
http://CiOId0m3.nbrdx.cn
http://JlFKWUU3.nbrdx.cn
http://rWIJS3Cg.nbrdx.cn
http://pxC1JmTW.nbrdx.cn
http://8VZaxSk1.nbrdx.cn
http://b8BieiTq.nbrdx.cn
http://mv1yVmmn.nbrdx.cn
http://mS72cori.nbrdx.cn
http://gAlLTDou.nbrdx.cn
http://EgWTzmJ6.nbrdx.cn
http://OXnHt08a.nbrdx.cn
http://NthGlAgN.nbrdx.cn
http://iKr75XWp.nbrdx.cn
http://MOVttYna.nbrdx.cn
http://TzcxyVjU.nbrdx.cn
http://www.dtcms.com/a/366146.html

相关文章:

  • 字符串(1)
  • 关于多Agent协作框架的讨论:以产品经理工作流为例对比Sub Agent与AutoGen
  • 论文阅读:arixv 2024 Adversarial Attacks on Large Language Models in Medicine
  • SpringMVC —— 响应和请求处理
  • 低代码开发平台技术总结
  • Coze源码分析-资源库-删除提示词-后端源码
  • Selenium
  • 一个基于 axios 的请求封装工具 - request-fruge365
  • Energy期刊论文学习——基于集成学习模型的多源域迁移学习方法用于小样本实车数据锂离子电池SOC估计
  • scss 转为原子css unocss
  • 【Linux】环境变量与程序地址空间详解
  • Linux——服务器多线程压缩工具介绍
  • 深入探讨AI三大领域的核心技术、实践方法以及未来发展趋势,结合具体代码示例、流程图和Prompt工程实践,全面展示AI编程的强大能力。
  • Makefile学习笔记 (1)
  • Horse3D游戏引擎研发笔记(九):使用现代图形引擎的元数据管理纹理创建过程(类Unity、Unreal Engine与Godot)
  • vue2 打包生成的js文件过大优化
  • 【iOS】对象复制与属性关键字
  • Linux编程——网络编程(UDP)
  • 当液态玻璃计划遭遇反叛者:一场 iOS 26 界面的暗战
  • 大语言模型推理的幕后英雄:深入解析Prompt Processing工作机制
  • 计算机大数据毕业设计推荐:基于Spark的新能源汽车保有量可视化分析系统
  • 如何轻松地将联系人从 Mac 同步到 iPhone
  • 如何本地编译servicecomb-java-chassis
  • 系统越拆越乱?你可能误解了微服务的本质!
  • 商城源码后端性能优化:JVM 参数调优与内存泄漏排查实战
  • SVN和Git两种版本管理系统对比
  • Clang 编译器:下载安装指南与实用快捷键全解析
  • Java全栈开发面试实录:从基础到微服务的深度探索
  • CentOS系统如何查看当前内存容量
  • SuperSocket 动态协议服务端开发全解析