文章目录
- 一、系统调用接口
- 二、文件调用
- 1. 文件描述符 fd
- 2. 文件调用原理
- 3. FILE
- 三、重定向
- dup2
- 四、缓冲区
- 简易 FILE 的代码实现
- 五、ext2 文件系统
- 1. inode 和 文件名
- 2. 重新认识目录
- 3. 理解文件的增删查改
- 4. 一些补充
- 六、文件链接
- 1. 建立软连接
- 2. 建立硬连接
文件被加载之前,被存在磁盘上,操作文件,文件的部分内容则会被调度到 内存中。
要分析文件,我们也把文件分成两种:
- 磁盘上的文件(文件系统)
- 内存中的文件
这里谈论的是,内存中的文件
文件被打开,OS 会为被打开的文件创建对应的内核数据结构 struct file
,将所有这个类型的结构体用某种数据结构链接起来以供 OS 管理。
struct file
{// 各种文件属性(磁盘中读出来的)// 各种链接关系// 缓冲区相关
};
一、系统调用接口
主要介绍一个
open
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>// man 手册查看
man 2 open
int open(const char *pathname, int flags)
int open(const char *pathname, int flags, mode_t mode);
参数 pathname:
- 文件名
参数 flags:
- 标志位,man 2 手册可查
参数 mode:
- 设置(新建)文件权限
返回值:
- 为 -1 则说明,open 失败
- 非负为 文件描述符(见后文)
参数 flags
-
O_CREAT
|O_WRONLY
如果没有文件则创建生成,
默认不会对原始文件内容做清空,会从从最开始覆盖 -
O_CREAT
|O_WRONLY
|O_TRUNC
如果没有文件则创建生成
清空 写 -
O_CREAT
|O_WRONLY
|O_APPEND
如果没有文件则创建生成
追加写
注意在往文件里写入的时候,strlen(str) 不要 +1,因为 ‘\0’ 是 C 语言的结束规定,不是文件的规定,加进去会乱码。
使用系统接口进行 IO 的时候,一定要注意,\0 问题
我们 C 语言使用的一系列函数
- fopen、fclose、fwrite/fputs、fread/fgets
都是系统函数的封装
- open、close、write、read
二、文件调用
1. 文件描述符 fd
任何一个进程,在启动的时候,默认都会打开三个文件:
-
标准输入 - - 设备文件 -> 键盘文件 0
-
标准输出 - - 设备文件 -> 显示器文件 1
-
标准错误 - - 设备文件 -> 显示器文件 2
其中 标准输出 和 标准错误 都会向 显示器 打印,但他们其实是不一样的。eg:测试中,输入受重定向符的影响,而错误不受重定向符的影响
文件描述符,也是 open 对应的返回值。我们创建的文件返回的值是从 3 开始的,而 0 1 2 正是被上面默认打开的三个文件占用了。这个数字本质就是 数组下标。
一张简图:
- 进程中,文件描述符的分配规则:
- 在文件描述符表中,最小的,没有被使用的数组元素,分配给新文件
fclose(stdin);
// 等价于
close(0);
2. 文件调用原理
- 1 个进程 可以调度 n 个文件,每个文件都有 一个缓冲区
- 调用 read / write / close 这些系统接口时,都需要文件操作符。也就是说,在操作系统层面,我们必须要访问fd,才可以找到文件
- 我们所谓的 IO 类 read / write 函数,本质上是 拷贝函数
- 什么时候将缓冲区上的内容刷新到磁盘中指定的位置,由 OS 自主决定
- 进程 和 文件 并没有深度耦合,便于操作系统的管理
如何理解一切皆文件:
- 每个硬件都有一个 struct file 对象,C语言里面没有成员函数,使用的就是函数指针完成的众多行为。
- 进程通过 指针数组,访问的其实是这些 struct file 对象,包括里面的缓冲区、函数指针…
- 而用户的操作,实际上都是进程的操作
- 所以我们说,Linux 下,一切皆文件
3. FILE
#include <stdio.h>
extern FILE *stdin;
extern FILE *stdout;
extern FILE *stderr;FILE *fopen(const char *path, const char *mode);
这里的 FILE * 是 结构体类型!由 C语言提供的,跟内核的 struct file 没有任何关系。
我们指定在操作系统层面,我们必须要访问 fd,才可以找到文件。也就是说 struct FILE 里面必定封装了 fd。
我们来看 FILE 源码是这样写的:
typedef struct _IO_FILE FILE; 在/usr/include/stdio.hstruct _IO_FILE {//...int _fileno; //封装的文件描述符,就是我们说的 fd// C语言维护的缓冲区相关内容//...
};
测试如下:
print("%d\n", stdin->_fileno);
print("%d\n", stdout->_fileno);
print("%d\n", stderr->_fileno);
FILE *fp = fopen("test.txt", "w");
print("%d\n", fp->_fileno);--------
输出结果:
0
1
2
3
三、重定向
🌰<
输出重定向举例
close(1);
int fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666); // fd = 1printf("hello\n"); // stdout -> 1
printf("hello\n");
printf("hello\n");---------
hello
hello
hello
字样,被保存进了 test.txt 中
🌰>
输入重定向举例
close(0);
int fd = open("test.txt", O_RDONLY); // fd = 0int a,b;
scanf("%d %d", &a, &b); // stdin -> 0
printf("a = %d, b = %d\n", a, b);---------
在 test.txt 文件中写入 123 456
运行程序后输出
a = 123, b = 456
🌰>>
追加重定向举例
close(1);
int fd = open("test.txt", O_WRONLY | O_CREAT | O_APPEND, 0666); // fd = 1printf("hello\n"); // stdout -> 1
printf("hello\n");
printf("hello\n");---------
运行两次则有
hello
hello
hello
hello
hello
hello
字样,被保存进了 test.txt 中
回头看之前的问题
其中 标准输出 和 标准错误 都会向 显示器 打印,但他们其实是不一样的。
eg:测试中,输入受重定向符的影响,而错误不受重定向符的影响
原因如下:
stdout、cout -> 他们都是向 1 号文件描述符对应的文件打印;
stderr、cerr -> 他们都是向 2 号文件描述符对应的文件打印。
输出重定向时,更改的只是 1 号对应的指向,2 号未被影响。
当我们需要手动分离一个程序正确和错误信息的时候:
./a.out 1>log.txt 2>err.txt
当然也有直接的函数可以使用
dup2
头文件:
#include <unistd.h>
int dup2(int oldfd, int newfd);
参数 oldfd:
- 最后需要的 fd
参数 newfd:
- 需要被覆盖的 fd
相当于,把本应该到 newfd 上的,重定向到 oldfd,最后剩下的只有 oldfd
四、缓冲区
C 语言维护的 FILE 结构体 和 OS 维护的 struct file 结构体,都有自己的缓冲区(每个对象都有自己的缓冲区),这两个缓冲区是不相同的。
C库提供的刷新策略,一般有三种:
- 无缓冲
- 行缓冲(遇到 \n 刷新)
- 全缓存(缓冲区满了刷新)
- 显示器采用的刷新策略:行缓冲
普通文件采用的刷新策略:全缓冲
缓冲区的作用:节省调用者的时间
这会产生一些奇怪的现象:
// c 库
fprintf(stdout, "hello fprintf\n");
// os 系统调用
const char *msg = "hello write\n";
write(1, msg, strlen(msg));fork();
这个程序我们在 linux 下,重定向到文件,会出现如下情况
[xxx@hostname file]$ ./a.out
hello write
hello fprintf
[xxx@hostname file]$ ./a.out > test.txt
[xxx@hostname file]$ cat test.txt
hello write
hello fprintf
hello fprintf
[xxx@hostname file]$
第一个运行容易理解,分析第二次 cat 文件内容出现的结果
原因如下:
- 首先,write 正常调用输出到显示器
- fprintf 的缓冲区,对于重定向到普通文件,使用全缓冲,这里的内容显然不能将缓冲区填满,所以进程结束时刷新
- 一直到 fork 被调用,程序还没结束,此时父子进程的缓冲区里都有一份 hello fprintf。谁先结束谁就先写诗拷贝,刷新到屏幕上,于是被打了两次
如果我们想要 强制,手动 刷新 os 上的缓冲区,可以使用函数 fsync
:
#include <unistd.h>
int fsync(int fd);
简易 FILE 的代码实现
👉🔗链接如下
五、ext2 文件系统
Block Group:文件系统会根据分区的大小划分为数个 Block Group。而每个Block Group都有着相同的结构组成。
Boot Block 开机块:
- 每个分区的最开始,有一小块开机相关的。重点包括与操作系统启动相关的内容,如分区表、操作系统定向的地址。
剩下的就是数据相关的:
SB - - Super Block(超级块):存放文件系的所有属性信息
-
- 文件系统的类型
-
- 整个分组的情况
-
SB 在各个分组里面可能都会存在,而且是统一更新的,而是为了防止SB区域坏掉,如果故障,整个分区不可以被使用。(多副本保护数据安全)
-
记录的信息主要有:bolck 和 inode的总量,未使用的 block 和 inode 的数量,一个 block 和 inode 的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block 的信息被破坏,可以说整个文件系统结构就被破坏了
GDT - - Group Descriptor Table(块组描述符):描述块组内的详细统计等属性信息。
Block Bitmap(块位图):Block Bitmap 中记录着 Data Block 中哪个数据块已经被占用,哪个数据块没有被占用
inode Bitmap(inode 位图):每个 bit 表示一个 inode 是否空闲可用。
i 节点表:存放文件属性 如 文件大小,所有者,最近修改时间等
- 一般而言每个文件内部所有属性的集合,都会放在一个 inode 节点中(128字节)。一个文件,一个 inode 节点。所以,一个 group 需要有一个区域来专门保存该 group 内所有文件的 inode 节点,即 inode table ,i 节点表。
- 每个 inode 都会有自己的 inode 编号。inode 编号也属于对应文件的属性 id。
- 一个 inode 对应一个文件,而文件 inode 属性和该文件对应的数据块是有映射关系的
Data Blocks(数据区):存放文件内容
1. inode 和 文件名
Linux 系统只认 inode 号,文件的 inode 属性中,并不存在文件名。文件名只是给用户用的。
2. 重新认识目录
目录是一个文件,目录也有 inode,有内容。
任何一个文件,一定在目录内部。
目录的数据块里保存的就是,该目录下,文件名和文件 inode 标号对应的映射关系,而且,在目录内,文件名和 inode 互为 key 值。
当我们访问一个文件的时候,我们是在特定目录下访问的,cat test.txt
- 先要在当前目录下,找到 test.txt 的 inode 编号
- 一个目录也是一个文件,也一定隶属于一个分区,结合 inode,在该分区中找到分组, 在改分组中 inode table 中,找到文件的 inode
- 通过 inode 和对应的 datablock 的映射关系,找到改文件的数据块,并加载到 OS,并完成显示到显示器!
3. 理解文件的增删查改
删除文件,只需要修改位图即可
- 首先根据文件名,找到对应的 inode number
- 删除内容:根据 inode number 结合 inode属性中的映射关系,设置 block bitmap 对应的比特位,置0即可
- 删除属性:inode number 设置 inode bitmap 对应的比特位设置为 0
新增文件
- 在新增文件的目录所处的分组内,os 会在 inode Bitmap 里找一个空位置为 1
- 对应 inode Bitmap 是第几个比特位,就有了新的 inode 和 inode number,文件创建出来的默认属性,填充到 inode table 的 inode 节点中
- 数据块里面,内容中,追加一条新的文件名和 inode 的映射关系。
对文件写入
- 拿着文件的文件名找 inode,比如要添加5个块,把 5 个块的位置在 BlockBitmap 里设为 1,这 5 个编号同样填入块和 inode 映射的数组
- 向 Datablocks 对应位置里刷新数据
4. 一些补充
- 文件被误删了,怎么恢复?
使用工具(从Linux日志中)拿到(删除文件的) inode!!!在 inode bitmap 的位置置 1。
在 inode 里面找到当前文件占用的数据块,把对应数据块的 block bitmap 里的位置置 1。
使用 indoe 访问属性,再通过属性访问数据块,就可以都出来了。
-
inode 可以确定分组。inode number 是在一个分区内有效的,不能跨分区
-
分区、分组、填写系统属性是 OS 完成的。那 OS 在什么时候呢做的呢?
比如我们重装系统,分区完成之后,要让分区能够被正常使用,还需要对分区做格式化。格式化的过程,其实就是 OS 向分区写入文件系统的管理属性信息。
六、文件链接
查看当前目录下所有文件的 inode number 和详细信息:
ls -li
1. 建立软连接
ln
-s
ln -s [文件名] [链接名]
软链接是一个独立的链接文件,有自己的 inode number,必有自己的 inode 属性和内容。
软链接内部放的是自己所指向的文件的路径。
- 作用就是,将一个路径很深的程序、库、头文件…建立到当前目录下或者其他很好访问的路径下,方便我们访问。
- 类似 windows 下的快捷方式。
2. 建立硬连接
ln
ln [文件名] [链接名]
硬链接和目标文件共用一个 inode number,意味着,硬链接一定是和目标文件使用同一个 inode 的。
硬链接,建立了文件名和老的 inode 的映射关系。
其中还有一个引用计数,计数的是该 inode 的硬链接数
. 就是当级目录的硬链接
.. 是上级目录的硬链接ls -ali 可以查看 . 和 .. 文件(inode number + 详细信息)
ls -di 可以看本级目录(inode number)
但是!我们不能给目录建立硬链接哦。因为有可能会造成环路路径问题。
🥰如果本文对你有些帮助,请给个赞或收藏,你的支持是对作者大大莫大的鼓励!!(✿◡‿◡) 欢迎评论留言~~