文件与fd
前置知识:
1.对于linux,什么是文件:
文件=内容+属性(属性也会被记录,如:空文件夹有大小)
2.文件什么时候被打开是什么被打开:
被进程打开,运行到fopen成功就被打开,访问文件本质是进程访问
3.为什么访问文件要先fopen打开它:
cpu要跑进程,可是文件在磁盘上 -> 打开文件就是将文件加载到磁盘(冯诺依曼)
4.加载什么
文件的内容和属性
===>操作系统要管理文件的,如何管理文件?先描述在组织(文件在内存的存在方式:数据结构+内容)
研究文件就是研究:1.打开的文件(研究打开的文件,就是研究进程与打开文件的关系) 2.没被打开的文件
=========================================================================
文件打开方式:
系统调用:
参数
1.flag
flag是32位位图,在相应位置1开启模式,可以用对应宏的或运算组合,O_WRONLY|O_CREAT|O_APPEND,表示对文件进行读,写, 如果不存在,创建 以追加方式打开文件(还有
-
O_TRUNC
:如果文件存在且可写,则截断文件长度为 0(就是从头写,没写到的保留
)
如果不指定 O_APPEND
、O_TRUNC
或 O_EXCL等打开方式
,文件的打开行为取决于 flags
的基本模式(O_RDONLY
、O_WRONLY
或 O_RDWR
) 以及文件是否已存在。以下是默认行为:
1. 默认打开方式(不指定 O_APPEND
、O_TRUNC
、O_EXCL
)
打开模式(flags ) | 文件已存在 | 文件不存在 |
---|---|---|
O_RDONLY (只读) | 正常打开,从头读取 | 失败(返回 -1 ,errno=ENOENT ) |
O_WRONLY (只写) | 正常打开,从头写入(覆盖原有内容) | 失败(除非同时指定 O_CREAT ) |
O_RDWR (读写) | 正常打开,从头读写(写入时覆盖原有内容) | 失败(除非同时指定 O_CREAT ) |
2.mode
mode是创建文件时的默认起始权限,(创建文件和权限在操作系统这是解耦合的,系统调用不会设默认权限,乱码),0000&!umask决定文件权限,传8进制即可。(umask可以通过umask()系统调用设置此进程的文件创建掩码,进程使用就近找umask)
返回值
文件描述符
<0创建文件失败,其余是文件
=========================================================================
文件在内存中组织:
操作系统从磁盘读取文件,创建文件结构体对象struct_file保存文件属性,因为有多个文件被打开,所以struct_file通过内部struct_file*指针连成链表,对文件的管理变成对链表的增删改。
多个进程打开多个文件,为了记录关系,进程PCB内部存有 文件管理结构体 的指针,指向一个包含文件结构体指针的数组和其余属性(文件数量等),将其打开的文件的结构体地址放进去就行。
这个存文件结构体数组和其余属性的结构体叫文件描述符表
-->构建进程和文件关系的表,进程通过描述符去表找文件,(访问文件的唯一方式,C语言的FILE是描述符的类型的封装,描述符存在其中_fileno变量中)
键盘,显示器,显示器占据这数组的0,1,2下标
=========================================================================
文件流:
C语言默认stdin stdout stderr 三个文件流,对应键盘,显示器,显示器
C++ cin,cout,cerr
进程默认会打开三个输入输出流,不同语言做了不同封装
->所有语言访问硬件都不是直接访问,是封装了操作系统的系统调用。(fopen封装open)
========================================================================
一切皆文件(进程角度)实现:
VFS:虚拟文件系统
屏蔽细节差别
内核管理硬件,封装了device结构体,因为这些外设有相同的类别,只是属性方法不同,所以可以让它们封装自己的读写方法,封装成相同的接口,给文件结构体内的read,write函数指针,进程读写硬件,只需调系统调用,系统调用找到文件,直接调read/write指针,自动使用各外设提供的函数,完成任务。(类似多态)
=========================================================================
为什么语言喜欢做封装:
1.方便用户操作:
对显示器(叫做字符设备)打印只能写入字符类型,但是1234整形打印还有手动转,语言封装,直接printf不同类型。scanf直接读入字符串转为所需类型
2.代码可移植多平台
=========================================================================
文本写入与二进制写入
计算机都是二进制写入,文本写入是语言做的封装处理
对比项 | 文本模式 | 二进制模式 |
---|---|---|
数据转换 | 换行符转换、编码处理 | 无转换,直接写入原始字节 |
跨平台一致性 | 可能因 OS 不同导致文件差异 | 字节一致,无平台差异 |
linux的键盘的回车键就是\n,windows的回车是\r\n
windows的\n只是将光标移动到下一行的相同x位置 :
文本模式("w"
/"r"
):
当程序以文本模式打开文件时,Windows 会自动转换:
-
写入时:
\n
→\r\n
(保证文件兼容性)。 -
读取时:
\r\n
→\n
(程序看到的是\n
)
Windows 控制台(命令行)中的 \n
行为
-
单独使用
\n
:
在 Windows 控制台(如cmd.exe
或 PowerShell)中,仅输出\n
时,光标会移动到下一行的相同列位置,不会回到行首。
如何确保文件的跨平台一致性?
-
统一使用二进制模式(如
"wb"
/"rb"
)避免自动转换。
=========================================================================
IO基本过程--文件内核级缓冲区
写入::
进程调用系统调用write,先将write的内容拷贝到struct_file内指针指向的文件缓冲区,再由操作系统决定是否调用struct_file里的write方法。
系统调用的write只是一个拷贝函数。不同于struct_file里的write函数指针。
读取::
进程调用read(),将文件缓冲区内的内容拷贝到指定内存区域,可能文件缓冲区没有指定内容,会触发操作系统调用struct_file里的read函数,将部分从磁盘读到缓冲区。
系统调用的read也只是一个拷贝函数。不同于struct_file里的read函数指针。
修改::
先读,再写read(),write()(也是文件修改不保存就刷没了的原因,还没从缓冲区刷到磁盘上)
注意:
1.在 Linux 中加载文件时,文件的内容会被按需动态加载到内核的 Page Cache(文件缓冲区)中,而不是直接加载到其他独立的内存区域。
加载文件就是将文件部分加载到内存的文件缓冲区的部分
2.内核层的文件缓冲区是所有文件共有一个
用户层的缓冲区(stdio 库缓冲)
-
每个文件流(
FILE*
)拥有独立的用户态缓冲区(如fopen
返回的FILE
结构体)。 -
作用:标准 I/O 库(如 glibc)通过缓冲区减少系统调用(如
read
/write
)。 -
缓冲模式:
-
全缓冲(Fully Buffered):默认用于普通文件,缓冲区满或调用
fflush()
时写入内核。 -
行缓冲(Line Buffered):用于终端(如
stdout
),遇到换行符\n
时刷新。 -
无缓冲(Unbuffered):如
stderr
,数据直接写入内核。
-
内核层的缓冲区(Page Cache)
-
所有文件共享同一套内核缓冲区(Page Cache),并非每个文件独立拥有。
-
作用:内核将磁盘文件数据缓存在内存中,减少直接磁盘 I/O。
-
特点:
-
全局性:所有进程访问同一文件时,共享同一份缓存。
-
自动管理:内核通过 LRU(最近最少使用)算法自动回收缓存。
-
同步机制:通过
fsync()
、sync()
或内核线程(pdflush
)将脏页写回磁盘。
-
=========================================================================
重定向:
1.文件描述符分配规则:文件描述符分配最小的没使用的给新的文件结构体。
例:close(1);关闭文件流,就是移除文件描述符表中指定文件描述符位置的指针。新open一个,新的文件描述符为1.
2.操作:间接:关闭一个文件流,再打开一个文件分配到刚才的文件描述符上
直接:dup2系统调用
oldfd替换品文件描述符,newfd为被替换的。替换文件描述符表的指定位置的指针
3.分类:追加重定向,输出重定向,输入重定向
只是打开文件的方式不同open完dup
=========================================================================
补充:
1.宏+位图传参可以通过传多个标记位组合的一个参数向函数传达所有信息。
7 #include<iostream>
8
9 using namespace std;
10 #define ONE (1)
11 #define TWO (1<<1)
12 #define THREE (1<<2)
13 #define FOUR (1<<3)
14 void callout(int num)
15 {
16 if(num&ONE)cout<<"one"<<endl;
17 if(num&TWO)cout<<"two"<<endl;
18 if(num&THREE)cout<<"THREE"<<endl;
19 if(num&FOUR)cout<<"FOUR"<<endl;
20
21 }
22 int main()
23 {
24 callout(ONE|TWO|THREE);
25 return 0;
26
27 }
2.printf,scanf等默认从键盘和显示器输入输出数据的函数,封装的stdout,stdin里的_fileno文件描述符都是写死的,语言和操作系统文件描述符表的约定,但具体是不是,操作系统能改。
3.文件什么时候退出打开状态:
struct file
内部维护一个原子引用计数器(atomic_long_t f_count
),表示当前有多少个指针引用它。这些指针可能来自:
-
进程的文件描述符表(
fd_array
)。 -
内核其他模块(如通过
get_file()
显式增加引用)。 -
文件操作过程中的临时引用(如正在执行的
read()
/write()
系统调用)归零销毁struct_file,退出