深入理解 Linux 基础 IO:从用户态到内核态的文件操作全解析----《Hello Linux!》(9)
文章目录
前言
文件操作是编程世界中最基础也最核心的能力之一 —— 无论是日常的日志写入、配置读取,还是大型系统的磁盘 IO 调度,本质上都离不开对 “文件” 的操作。而在 Linux 系统中,“一切皆文件” 的设计哲学,更让 IO 操作成为连接用户程序、内核与硬件的关键桥梁。
对于计算机专业的学生或初级开发者而言,我们往往从 C 语言的 fopen、fwrite 开始接触文件操作,却很少深究:这些库函数的底层是如何与操作系统交互的?系统调用 open、read 与 C 库接口有何区别?“文件描述符” 到底是什么,为何它能成为访问文件的 “钥匙”?重定向的底层逻辑是什么,为什么 printf 有时会输出两次?缓冲区的存在又解决了什么问题,为何会出现 “写入却看不到内容” 的困惑?
带着这些疑问,本文将从 “文件的本质” 出发,层层递进地剖析 Linux 基础 IO 的核心知识:从文件的属性与存储机制,到 C 语言用户态接口的用法与注意事项,再到系统调用的底层实现(文件描述符、open/read/close),最后深入探讨重定向原理、缓冲区机制,并通过模拟实现 C 库文件操作函数,帮你打通 “用户态 - 内核态” 的 IO 链路。
本文不仅包含清晰的理论解析,还附带可直接运行的代码示例与底层原理可视化分析,既适合初学者夯实基础,也能帮助开发者跳出 “API 调用” 的表层,理解 IO 操作的本质逻辑。无论你是想搞懂实验报告中的缓冲区诡异现象,还是想为后续深入学习网络 IO、并发编程打下基础,这份 “从用法到原理,从表层到底层” 的 Linux 基础 IO 指南,都能为你提供清晰的思路与实用的知识。
文件的介绍
文件 = 内容+属性
文件分为打开的文件和没打开的文件
打开的文件是进程打开的
文件被打开,必须先被加载到内存(先加载文件的属性) 一个进程有多个通过该进程被打开的文件
–所以操作系统内部,一定存在大量被打开的文件–OS管理他们也是用的先描述再组织
没打开的文件放在磁盘上,是被分门别类的放置好的(为了能让我们快速找到文件)
如何理解:Linux下一切皆文件
比如系统接口
read里面需要文件描述符,进而找到对应的描述文件的那个结构体,这个结构体里面又会有struct operation_func,里面可以去联系设备–在
struct_file那层就是一切皆文件了,这一层又称为VFS(虚拟文件系统)
Linux的文件在磁盘中存储时,是将属性和内容分开存储的
C语言的文件操作
比如:
fopen还有freadfwrite
fopen以写模式打开的话,并且第一个位置是写的不存在的文件名的话默认是在当前路径下新建一个文件(除非path那里带了绝对路径或者相对路径)
fwrite在写入时eg:fwrite(message,strlen(message),1,fp)这个strlen不加1,加1就把\0带进去了–但是对于文本编辑器eg: vim来说打出来就是乱码 (跟\n不一样哈,\n不用额外加1)
a和w都是写入,w是清空并从头写,a是在文件结尾追加写
系统的文件操作
各种语言的文件操作的底层都是封装了系统接口–因为文件在磁盘上存储,磁盘是外部设备,访问磁盘就是访问硬件(操作系统为了防止别人做坏事,只允许系统调用去访问eg:
open)进程会默认打开
stdin 键盘文件stdout 显示器文件stderr 显示器文件
open
这个是进行文件操作的系统接口
pathname是打开文件–默认是在当前路径找(除非加了相对路径或者绝对路径)
flags:打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”(|)运算,构成flagsO_RDONLY: 只读打开 O_WRONLY: 只写打开 O_RDWR : 读,写打开 这三个常量,必须指定一个且只能指定一个 O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限 O_APPEND: 追加写 用法eg: int fd = open("log.txt",O_WRONLY|O_CREAT,0666);
mode:文件的8进制访问权限eg: 0644(加上umask的值后才是真的访问权限) 但是要注意umask的作用--可以在单个程序里面改
open的返回值:失败返回-1,成功则返回文件操作符(一般用fd表示)
umask(系统接口版)
eg:
umask(0)
引申:
flags的原理:那些写的东西是宏,其实用的是比特位方式的标志位传递eg:
O_WRONLY对应一个比特位为1(表示 “只写打开”)
函数声明里面的
void*表示的是通用指针,什么类型的指针都能搞到里面
文件描述符fd
它的类型是
intLinux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2
标准输入:
stdin标准输出:stdout标准错误:stderr
文件描述符的原理

文件描述符就是
struct file* fd_array[]的下标这里的
struct file*又指向描述文件信息的结构体这个文件描述表就有点像C++里面的多态
引申:
files_struct结构体(是用来描述一个被打开文件的信息的)里面直接或间接包含这些属性:1.在磁盘的什么位置 2.文件的基本属性:权限,大小,读写位置,谁打开的
3.文件的内核缓冲区信息 4.
struct file *next指针5.引用计数count这里的引用计数的作用:有时会出现多个文件描述符指向一个文件的那个结构体的情况–如果这时有一个文件操作符要关闭,那么引用计数就可以判断是否需要释放这个文件的结构体
文件描述符的分配规则
在
struct file* fd_array[]数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。–如果一个下标本来被用了,但是被
close了,也算没使用
close
这个可以用来关闭文件描述符 eg:close(1);
read
这个就是从
fd中读取数据,然后存到缓冲区buf中,最多读取fd里面count个字节eg:
buf可以是数组的指针这个
ssize_t其实就是int类型
重定向
就是eg:本来改输出到显示器上的内容让它输出到其他地方
一般用
dup(系统调用) 或> >> <实现重定向
程序替换之后,之前搞得重定向还在–但是进程结束了就没了
–因为程序替换并不影响文件的访问
dup

一般常用的是
dup2
dup2的话是让newfd也指向oldfd指向文件对象(注意引用计数的变化) --这个容易混
eg: dup2(fd,1);
之后printf的东西会到fd里面
除非eg: fprintf(stdout,"xxxxxx");
> >> <的重定向
这个重定向跟
dup的重定向的比较eg: ./text > text.txt 会先把程序的标准输出改成写到text.txt的,然后再执行程序
它的几种用法:
./text > text.txt:把标准输出改成写到text.txt里面的
./text 1>normal.log 2>err.log可以简写成./text >mormal.log 2>err.log这个的意思是把标准输出的改成写到
normal.log,把标准错误输出的写到err.log
./text 1> all.log 2>&1可以简写成:./text > all.log 2>&1就是先把
1指向all.log,再让2也指向1指向的东西
对缓冲区的深入理解
const char *fstr = "hello fwrite\n";
const char *str = "hello write\n";
printf("hello printf\n");
fprintf(stdout, "hello fprintf\n");
fwrite(fstr, strlen(fstr), 1, stdout);
write(1, str, strlen(str));
fork();
这段代码出来的结果:
./text运行的话是正常的但是
./text > log.txt的话:C接口会被打印出来两次(而且write的先被打印出来)
这里的话是因为两个进程其中一个退出时,缓冲区就会被刷新,算修改了共同部分的数据,就触发了写时拷贝
注意:上面的
./text > log.txt时,因为是向普通文件写入,所以缓冲区变成了全缓冲,而不是行缓冲了
如果代码变成:
const char *fstr = "hello fwrite";
const char *str = "hello write";
printf("hello printf");
fprintf(stdout, "hello fprintf");
fwrite(fstr, strlen(fstr), 1, stdout);
close(1);
这样的话,什么都打印不出来
在
close的时候,是会刷新对应流里面的缓冲区的,那就只有一种可能:这个缓冲区不是系统级别的缓冲区,不在操作系统内部,是C语言的缓冲区
C语言的文件操作的接口eg:
fprintf都是C语言的缓冲区刷新了,就把数据交给write去操作了引申:
fflush是强行刷新缓冲区,然后交给write去搞在
FILE结构体里面存了这个C语言缓冲区的位置–这种缓冲区是属于用户的,属于用户级别的缓冲区 --用返回值是FILE的打开文件–每个文件都有独立的一个缓冲区引申:语言都属于用户层
引申:语言具有跨平台性
原因:采用了比如:编译器的多平台适配
标准 I/O 库缓冲区刷新的规则
1.无缓冲 --直接刷新
2.行缓冲 --碰到
\n才刷新 比如:显示器显示3.全缓冲 --缓冲区满了才刷新 比如:写入数据到普通文件
注意:进程结束时,也是会刷新缓冲区的
–上面缓冲区没被刷新是因为
close直接把那个缓冲区给关闭了
只要数据刷新到了内核,数据就到可以硬件了
缓冲区的意义
1.可以解决效率问题 --因为调用系统调用是成本的
2.配合格式化 --比如:
printf会把格式化好的数据(比如字符转成整型)给缓冲区
模拟实现C语言库里面的几个文件操作
#define SIZE 1024
#define FLUSH_NOW 1
#define FLUSH_LINE 2
#define FLUSH_ALL 4
#define FILE_MODE 0666
typedef struct IO_FILE{
int fileno;
int flag;
char inbuffer[SIZE];
int in_pos;
char outbuffer[SIZE];
int out_pos;
}_FILE;
_FILE * _fopen(const char*filename, const char *flag)
{
assert(filename);
assert(flag);
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)
{
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'){
// 没考虑其他情况,比如:len为0会越界 中间有\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)
{
if(fp == NULL) return;
_fflush(fp);
close(fp->fileno);
free(fp);
}
更多推荐










所有评论(0)