前言:
我记得距离上次更新…..好像过了快一个月了,总想着在开学以前最后一舞,写完文件系统这部分的,只是实在是太懒了,所以只能拖到开学之后了。不过这不是还是来了吗,这期内容会比较长,我们一次性讲完文件系统的所有内容!
今天呢,我们先从C语言讲起,来回忆我们以前比较害怕的文件读写操作,顺便为下文铺垫;
接着呢,我们要认识几个系统调用,看看在linux系统中,我们如何对文件进行操作;
同时,我们根据第二步,要引出fd(文件描述符)的概念,在这里相当重要
然后我们来聊一聊缓冲区,同时解决我们在进程控制这一节(如下)遗留的一些问题,当时我们没有缓冲区的概念,所以理解s状态总是缺点什么,我们今天补上。

然后呢,上面都是打开了的文件,即文件到了内存之中,接下来我们要聊的是磁盘中文件
聊磁盘中文件呢:
首先我们要了解一下磁盘的构造,还要了解chs查找,在这之中对磁盘进行加深的了解
然后我们就要学习具体如何进行管理,从组到区到整块磁盘,我们依次进行介绍
之后,我们要了解路径解析和路径缓存,并且在这过程中,将这里与前面内容进行串联,知道我们如何通过file找到我们的文件/目录的属性与内容并进行操作
然后我们会聊一聊挂载分区,并解决前面的一些问题;
最后,我们要进行文件系统的总结,将之前的内容全部串联起来
正文:
C语言文件操作:
一些常用函数:
fopen 打开文件:
FILE *fopen(const char *filename, const char *mode);
fclose 关闭文件:
int fclose(FILE *stream);
fwrite 写文件:
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
- ptr:数据源缓冲区指针
- size:单个元素的字节大小
- nmemb:要写入多少个元素
- stream:目标文件指针
fgetc 读单个字符:
int fgetc(FILE *stream);
fputc 写单个字符:
int fputc(int c, FILE *stream);
fgets 读取一行字符串:
char *fgets(char *s, int size, FILE *stream);
fputs 写字符串:
int fputs(const char *s, FILE *stream);
fseek 文件指针偏移:
int fseek(FILE *stream, long offset, int whence);
whence:SEEK_SET文件开头 / SEEK_CUR当前位置 / SEEK_END文件末尾
ftell 获取当前指针位置:
long ftell(FILE *stream);
rewind 指针回到文件开头:
void rewind(FILE *stream);
C语言里面文件操作的函数还有很多,我们在此不全部列出;
经过上面的一些函数,我们发现,参数中经常出现“FILE”,那么这个FILE是什么呢,我们稍后揭晓
演示:
我们写一段代码如下
#include<stdio.h>
2 #include<string.h>
3 int main()
4 {
5 FILE* fp=fopen("chishenme","w");
W> 6 char* buf="jintianchimian";
7 fwrite(buf,1,strlen(buf),fp);
8 fclose(fp);
9 return 0;
10 }
运行结果如下:
[tsx@VM-0-10-centos 99]$ tree
.
|– haha
|– haha.c
|– makefile
`– \\344\\273\\212\\345\\244\\251\\345\\220\\203\\344\\273\\200\\344\\271\\210
0 directories, 4 files
[tsx@VM-0-10-centos 99]$ vim haha.c
[tsx@VM-0-10-centos 99]$ make
gcc -o haha haha.c
[tsx@VM-0-10-centos 99]$ ./haha
[tsx@VM-0-10-centos 99]$ tree
.
|– chishenme
|– haha
|– haha.c
|– makefile
`– \\344\\273\\212\\345\\244\\251\\345\\220\\203\\344\\273\\200\\344\\271\\210
0 directories, 5 files
[tsx@VM-0-10-centos 99]$ cat chishenme
jintianchimian
关于stdin,stdout,stderr:
我们以前在学习C语言的时候,经常也会见到这三位,那么它们分别代表什么呢?
stdin:标准输入(键盘)
stdout:标准输出(显示器)
stdout:标准错误(显示器)
我们在此列一些例子:
比如我们要把数据输出到显示器上,应该这怎么做呢?
有以下这几种方式:
#include<stdio.h>
#include<string.h>
int main()
{
char *buf="好想玩奥拉星\\n";
fwrite(buf,sizeof(buf[0]),strlen(buf),stdout);
printf("好想玩鸣潮\\n");
fprintf(stdout,"好想玩原神\\n");
return 0;
}
结果如下:
[tsx@VM-0-10-centos 99]$ make
gcc -o haha haha.c
[tsx@VM-0-10-centos 99]$ ./haha
好想玩奥拉星
好想玩鸣潮
好想玩原神
所以我们得出结论,stdout就是一个FILE*类型的指针,同理:stdin,stdout也是FILE*类型的指针,但是FILE是什么呢,我们后面要进行一个小区分
C语言中,打开文件的几种方式:
| "r" | 只读读文件 | 报错返回 NULL | 打开文件 | 文件开头 |
| "w" | 只写,清空创建 | 创建新文件 | 直接清空原有全部内容 | 文件开头 |
| "a" | 追加写 | 创建新文件 | 不清空,只能往末尾写 | 文件末尾 |
| "r+" | 读写 | 报错 NULL | 打开原有文件 | 文件开头,可读可写 |
| "w+" | 读写,清空 | 创建新文件 | 清空旧文件 | 文件开头 |
| "a+" | 追加读写 | 创建新文件 | 不清空 | 读在开头,写永远强制写到末尾 |
linux文件操作:
一些常用函数:
- open:成功返回文件描述符 fd(非负整数);失败返回 -1
- read/write:返回实际读写的字节数;0 代表读到末尾;失败 -1
- close:成功 0,失败‑1
open打开文件:
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
pathname:可以带绝对路径,也可以相对路径;
如:我直接写yuanshen.txt,那么它会在我前面,给我补上cwd这个路径
(所以在操作系统之中,路径很重要咯,还要专门给我补上?没错!!!至于为什么路径这么重要呢,我们一会在讲路径解析时细说)
这里的flags:
| O_RDONLY | 只读 |
| O_WRONLY | 只写 |
| O_RDWR | 读写 |
| O_CREAT | 文件不存在就创建;此时必须带上第三个参数 mode |
| O_TRUNC | 打开时清空文件原有内容 |
| O_APPEND | 追加模式,写总是跳到文件末尾 |
mode:新建文件权限(受 umask 影响)
注意:这里我们发现,open这个系统调用,返回的居然是一个整形!!!
很奇怪呢:C语言中打开一个文件,返回的是一个FILE* 的指针,而在操作系统之中,居然返回的仅仅是一个整形?那这个整形是什么呢?
答:这个整形,就是我们的文件描述符(fd)
小demo:
我们在传参的时候,有时候呢,我们想进行多个条件判断,那么我们就要传很多参数呀,这很麻烦,所以就诞生了一种方法:
#define con1 0001
#define con2 0002
#define con3 0004
这时候,我们有些疑惑,这是什么呀?二进制!
其实,我们用这个方法,就相当于在做位运算
比如:
int flags=con1 | con2
if(flags & con1)
{
//
}
if(flags & con2)
{
//
}
if(flags & con3)
{
//
}
这样我们就只需要传一个参数就可以进行多次判断了
write 向文件写数据:
ssize_t write(int fd, const void *buf, size_t count);
- fd:open 得到的文件描述符
- buf:要写出的数据缓冲区
- count:希望写多少字节
- 返回值:实际写入字节数;不一定等于 count(管道、终端、socket 会出现短写)
read 从文件读数据
ssize_t read(int fd, void *buf, size_t count);
- buf:存放读到内容的缓冲区
- count:最多读多少字节
- 返回:
- >0:读到的字节数
- 0:到达文件末尾 EOF
- -1:出错
close 关闭文件:
int close(int fd);
演示:
演示代码如下
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/stat.h>
#include<fcntl.h>
int main()
{
int fd=open("xinwenjian",O_WRONLY|O_CREAT|O_TRUNC,0666);
if(fd==-1)
{
perror("open");
}
else{
char *buf="aaaaaabbbbbb";
write(fd,buf,strlen(buf));
close(fd);
}
return 0;
}
结果如下:
[tsx@VM-0-10-centos 99]$ tree
.
|– haha
|– haha.c
|– makefile
|– xinwenjian
|– xixi.c
`– \\344\\273\\212\\345\\244\\251\\345\\220\\203\\344\\273\\200\\344\\271\\210
0 directories, 6 files
[tsx@VM-0-10-centos 99]$ cat xinwenjian
aaaaaabbbbbb
关于fd:
fd是什么?
我们在这里新增一个fd的打印
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/stat.h>
#include<fcntl.h>
int main()
{
int fd=open("xinwenjian",O_WRONLY|O_CREAT|O_TRUNC,0666);
if(fd==-1)
{
perror("open");
}
else{
char *buf="aaaaaabbbbbb";
write(fd,buf,strlen(buf));
printf("%d\\n",fd);
close(fd);
}
return 0;
}
我们发现
[tsx@VM-0-10-centos 99]$ make
gcc -o haha haha.c
[tsx@VM-0-10-centos 99]$ ./haha
3
结果居然为3,真是匪夷所思
我们知道,open如果打开失败,返回的fd是-1,那意思不就是,fd的取值范围,可以从0开始取吗,可是这里我们却发现,我们打开文件fd是从3开始的,为什么呢?
答:在打开我们的文件之前,系统默认打开了标准输入,标准输出,标准错误(即stdin,stdout,stderr)
那么,fd到底是什么呢?我们画一张图来讲讲:

我们既然要管理打开的文件,就一定要在内存之中,先描述它,生成对应的结构体(file),而且呢,进程要进行open的操作,所以进程就要和文件产生联系,那么如何产生联系?
进程的task_struct中,存在一个*files的指针,指向files_struct这个结构体,而在这个结构体中,最重要的就是一张由file*组成的数组,通过这个数组,我们的进程和文件就可以产生联系,所以啊,fd就是这张表的下标!
fd的分配规则:
这是我们的演示代码
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/stat.h>
#include<fcntl.h>
int main()
{
close(1);
int fd=open("xinwenjian",O_WRONLY|O_CREAT|O_TRUNC,0666);
if(fd==-1)
{
perror("open");
}
else{
char *buf="aaaaaabbbbbb";
printf("hahahahahaahahah\\n");
}
return 0;
}
结果如下:
[tsx@VM-0-10-centos 99]$ tree
.
|– haha
|– haha.c
|– makefile
|– xixi.c
`– \\344\\273\\212\\345\\244\\251\\345\\220\\203\\344\\273\\200\\344\\271\\210
0 directories, 5 files
[tsx@VM-0-10-centos 99]$ make
gcc -o haha haha.c
[tsx@VM-0-10-centos 99]$ ./haha
[tsx@VM-0-10-centos 99]$ tree
.
|– haha
|– haha.c
|– makefile
|– xinwenjian
|– xixi.c
`– \\344\\273\\212\\345\\244\\251\\345\\220\\203\\344\\273\\200\\344\\271\\210
0 directories, 6 files
[tsx@VM-0-10-centos 99]$ cat xinwenjian
hahahahahaahahah
printf不是向显示器上打印吗?在这里我把stdout(fd=1)关掉了,然后我又打开了一个新文件,同时,我用printf,是要往显示器上面打印,可是,我发现实际上,显示器上没有出现任何数据,反而数据出现在了新文件中,那么究竟发生了什么呢?
答:我们把fd=1关掉了,然后调用printf,这里的FILE*的参数我传的是stdout,我把fd=1关了,我的stdout并不知道,我以为我还是fd=1,所以我在底层调用系统调用的时候,我还是传fd=1,但我在关闭了fd=1之后,我又打开了一个新文件,就相当于我把fd=1,交给了这个新文件,所以就相当于:stdout系统调用fd=1,但是fd=1此时指向的是新文件的file,所以呢,就会出现重定向的情况
由此可见,fd的分配规则:
在files_struct中,找到一个当前未被使用的最小的一个下标,将其作为当前的文件描述符
通过以上,同时我们也就知道了重定向的本质:

用dup2进行重定向:
int dup2(int oldfd, int newfd);
把 B 号文件描述符,改成指向 A 指向的文件。
//改变后者
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main()
{
// 打开目标文件
int fd = open("out.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if(fd < 0)
{
perror("open");
return 1;
}
// dup2:把fd复制到文件描述符1(stdout标准输出)
dup2(fd, 1);
// 此时printf不再打印屏幕,写入out.txt
printf("使用dup2实现输出重定向\\n");
printf("hello linux\\n");
fflush(stdout); // 刷新stdio缓冲区
close(fd);
return 0;
}
缓冲区:
这里我们就要聊一聊缓冲区了,我们在讲这个以前,不知道同学们有没有听过一个段子,是这样的:
为什么顺丰快递送的比别的快递快呢?
别的快递:等快递积攒到一定地步,能装满一车后,发车开始派送;
顺丰快递:快递攒了一车?发车!快递只有半车?发车!快递只有一个?也发
当然,既然快,也一定要付出一些代价,不然顺丰为什么贵呢
好了,接下来我们便来聊一聊缓冲区:

我们发现,如果我要吧buffer里面 的数据写到file的文件内核级缓冲区中,是不是会有一定的代价呢?当然会产生代价,写一次当然就要先让CPU停下别的活,让它写数据嘛,可是如果我while(1),我一直写数据呢?如果我每一次都要直接调用write的话,是不是我什么都别干了,我就专门在这里写数据吧,所以是不太好的;

所以我们的缓冲区就诞生了,这个缓冲区呢,是我们语言层面的缓冲区,可以在用户写入数据的时候,先把数据存储在缓冲区中,等到满足一定条件后再刷新到文件内核级缓冲区中。
刷新方式:
①. 全缓冲区:这种缓冲⽅式要求填满整个缓冲区后才进⾏I/O系统调⽤操作。对于磁盘⽂件的操作通常使⽤全缓冲的⽅式访问。
(一般文件是全缓冲)
② ⾏缓冲区:在⾏缓冲情况下,当在输⼊和输出中遇到换⾏符时,标准I/O库函数将会执⾏系统调⽤操作。当所操作的流涉及⼀个终端时(例如标准输⼊和标准输出),使⽤⾏缓冲⽅式。因为标准
I/O库每⾏的缓冲区⻓度是固定的,所以只要填满了缓冲区,即使还没有遇到换⾏符,也会执⾏
I/O系统调⽤操作,默认⾏缓冲区的⼤⼩为1024。
(一般显示器是行缓冲)
③ ⽆缓冲区:⽆缓冲区是指标准I/O库不对字符进⾏缓存,直接调⽤系统调⽤。标准出错流stderr通常是不带缓冲区的,这使得出错信息能够尽快地显⽰出来。
除了上述列举的默认刷新⽅式,下列特殊情况也会引发缓冲区的刷新:
① 缓冲区满时;
② 执⾏flush语句;
③ 进程结束
思考:
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/stat.h>
#include<fcntl.h>
int main()
{
close(1);
int fd=open("xinwenjian",O_WRONLY|O_CREAT|O_TRUNC,0666);
if(fd==-1)
{
perror("open");
}
else{
char *buf="aaaaaabbbbbb";
printf("hahahahahaahahah\\n");
close(fd);
}
return 0;
}
我在刚才实验的代码里,加了一个close(fd),会出现什么状况呢?
[tsx@VM-0-10-centos 99]$ tree
.
|– haha
|– haha.c
|– makefile
|– xixi.c
`– \\344\\273\\212\\345\\244\\251\\345\\220\\203\\344\\273\\200\\344\\271\\210
0 directories, 5 files
[tsx@VM-0-10-centos 99]$ make
make: `haha' is up to date.
[tsx@VM-0-10-centos 99]$ ./haha
[tsx@VM-0-10-centos 99]$ tree
.
|– haha
|– haha.c
|– makefile
|– xinwenjian
|– xixi.c
`– \\344\\273\\212\\345\\244\\251\\345\\220\\203\\344\\273\\200\\344\\271\\210
0 directories, 6 files
[tsx@VM-0-10-centos 99]$ cat xinwenjian
[tsx@VM-0-10-centos 99]$
很奇怪,不仅在显示器里,新文件里也没有数据了,为什么呢?
因为我们的显示器是行刷新,而我输入的数据是有换行符号的,所以我如果没有重定向,可以在显示器上打出来。但我的文件是全刷新,所以要写入的数据还在缓冲区里,还没有写到文件,突然,我把文件关了,所以当然写不进去
如何纠正?
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/stat.h>
#include<fcntl.h>
int main()
{
close(1);
int fd=open("xinwenjian",O_WRONLY|O_CREAT|O_TRUNC,0666);
if(fd==-1)
{
perror("open");
}
else{
char *buf="aaaaaabbbbbb";
printf("hahahahahaahahah\\n");
fflush(stdout);
close(fd);
}
return 0;
}
我只需多加一个fflush函数,可以刷新缓冲区,这时我们再看结果;
[tsx@VM-0-10-centos 99]$ tree
.
|– haha
|– haha.c
|– makefile
|– xixi.c
`– \\344\\273\\212\\345\\244\\251\\345\\220\\203\\344\\273\\200\\344\\271\\210
0 directories, 5 files
[tsx@VM-0-10-centos 99]$ ./haha
[tsx@VM-0-10-centos 99]$ tree
.
|– haha
|– haha.c
|– makefile
|– xinwenjian
|– xixi.c
`– \\344\\273\\212\\345\\244\\251\\345\\220\\203\\344\\273\\200\\344\\271\\210
0 directories, 6 files
[tsx@VM-0-10-centos 99]$ cat xinwenjian
hahahahahaahahah
所以呀,我们就通过fflush刷新了缓冲区
缓冲区的意义:
读写⽂件时,如果不会开辟对⽂件操作的缓冲区,直接通过系统调⽤对磁盘进⾏操作(读、写
等),那么每次对⽂件进⾏⼀次读写操作时,都需要使⽤读写系统调⽤来处理此操作,即需要
执⾏⼀次系统调⽤,执⾏⼀次系统调⽤将涉及到CPU状态的切换,即从⽤⼾空间切换到内核
空间,实现进程上下⽂的切换,这将损耗⼀定的CPU时间,频繁的磁盘访问对程序的执⾏效
率造成很⼤的影响。
为了减少使⽤系统调⽤的次数,提⾼效率,我们就可以采⽤缓冲机制。⽐如我们从磁盘⾥取
信息,可以在磁盘⽂件进⾏操作时,可以⼀次从⽂件中 读出⼤量的数据到缓冲区中,以后对
这部分的访问就不需要再使⽤系统调⽤了,等缓冲区的数据取完后再去磁盘中读取,这样就
可以减少磁盘的读写次数再加上计算机对缓冲区的操作⼤ 快于对磁盘的操作,故应⽤缓冲区
可⼤ 提⾼计算机的运⾏速度
区分一下file和FILE
逻辑上的关系:
FILE* → 内部保存 fd → fd 索引内核struct file
事实上,我们后面要更多讨论的是file
重谈S状态:

如我在linux(5)中所提到的,如果我不断打印(while(1)),那么进程状态居然是S,原因就是,我把数据写到语言级的缓冲区的过程,是R,是CPU进行的,而我在等缓冲区满后,我进行write,这个过程很快,也不会阻塞。但是!当我把数据从内核级缓冲区写到硬件的时候,这个过程可就不是CPU做的了,由硬件进行后续搬运,此时进程放弃CPU资源,进入S状态。在这个过程之中,由于CPU的速度显著大于硬件搬运的速度,所以我们在大多数情况下,看到的是S状态
磁盘:
网上偷了张图

如何写数据呢:
我们可以把磁盘看做无数个小磁铁,有两个极,可以分别记为0,1。
我们的刺头在旋转的时候,通过电磁感应,可以把小磁铁上的磁极进行改变,从而无数个磁极的改变,形成了二进制无数的0和1,就可以实现写文件了
介绍一下各个区域
在一面上:

整体来看

比如说这个,有六个面,也就是有六个磁头。
而所有磁头是共进退的
那么我们怎么写磁盘?
我们需要三个东西:
柱⾯(cylinder),磁头(head),扇区(sector)
扇区是从磁盘读出和写⼊信息的最⼩单位,通常⼤⼩为 512 字节。
磁头(head)数:每个盘⽚⼀般有上下两⾯,分别对应1个磁头,共2个磁头
磁道(track)数:磁道是从盘⽚外圈往内圈编号0磁道,1磁道…,靠近主轴的同⼼圆⽤于停靠磁
头,不存储数据
柱⾯(cylinder)数:磁道构成柱⾯,数量上等同于磁道个数
一个面上的磁道,是磁道;所有面上的同一圈磁道,叫柱面
扇区(sector)数:每个磁道都被切分成很多扇形区域,每道的扇区数量相同
圆盘(platter)数:就是盘⽚的数量
磁盘容量=磁头数 × 磁道(柱⾯)数 × 每道扇区数 × 每扇区字节数
理解:
我们把一面上面的扇区展开,可以看做一维数组;

其中红色的部分就是一个个扇区;
我们再把哪一面的概念加进来,就是一个二维数组;

这时候,我们再把每个柱面的概念加进来,那么就变成了一个三维数组

CHS
所以寻找⼀个扇区:先找到哪⼀个柱⾯(Cylinder) ,在确定柱⾯内哪⼀个磁道(其实就是磁头位置,
Head),在确定扇区(Sector),所以就有了 CHS
LBA
但是呢,在我们的C语言里面,所有的数组,归根到底还是一维数组,我们把这个三维数组展开,就得到了:

所以,每⼀个扇区都有⼀个下标,我们叫做 LBA(Logical Block Address) 地址,其实就是线性
地址
CHS地址和LBA地址互转
CHS转成LBA:
磁头数*每磁道扇区数 = 单个柱⾯的扇区总数
LBA = 柱⾯号C*单个柱⾯的扇区总数 + 磁头号H*每磁道扇区数 + 扇区号S – 1
即:LBA = 柱⾯号C*(磁头数*每磁道扇区数) + 磁头号H*每磁道扇区数 + 扇区号S – 1
扇区号通常是从1开始的,⽽在LBA中,地址是从0开始的
柱⾯和磁道都是从0开始编号的
总柱⾯,磁道个数,扇区总数等信息,在磁盘内部会⾃动维护,上层开机的时候,会获取到这些参
数。
LBA转成CHS:
柱⾯号C = LBA // (磁头数*每磁道扇区数)【就是单个柱⾯的扇区总数】
磁头号H = (LBA % (磁头数*每磁道扇区数)) // 每磁道扇区数
扇区号S = (LBA % 每磁道扇区数) + 1
"//": 表⽰除取整
正式进入文件系统
块的引入
硬件在读取的时候,一个扇区一个扇区读取的话,效率太低了,所以有了块的概念
硬盘的每个分区是被划分为⼀个个的”块”。⼀个”块”的⼤⼩是由格式化的时候确定的,并且不可
以更改,最常⻅的是4KB,即连续⼋个扇区组成⼀个 ”块”。”块”是⽂件存取的最⼩单位
磁盘就是⼀个三维数组,我们把它看待成为⼀个"⼀维数组",数组下标就是LBA,每个元素都是扇
区
每个扇区都有LBA,那么8个扇区⼀个块,每⼀个块的地址我们也能算出来。
知道LBA:块号 = LBA/8
知道块号:LAB=块号*8 + n. (n是块内第⼏个扇区)
inode的引入
我们老早就说了,文件=内容+属性
我们又说过,先描述再组织
想要对它进行管理,先抽象出结构体,所以为了管理文件属性,就有了inode
这个时候,就有好奇宝贝就要问了,我们上面说了file,file里面有着找到文件属性的方法,而inode又是记录的文件属性,那二者有什么关系呢?还差最后一步,路径解析
我这里搞了一份inode的源码
#define EXT2_NDIR_BLOCKS 12 // 直接块数量
#define EXT2_IND_BLOCK EXT2_NDIR_BLOCKS // 一级间接块下标
#define EXT2_DIND_BLOCK (EXT2_IND_BLOCK + 1) // 二级间接块下标
#define EXT2_TIND_BLOCK (EXT2_DIND_BLOCK + 1) // 三级间接块下标
#define EXT2_N_BLOCKS (EXT2_TIND_BLOCK + 1) // 数组总长度 = 15
struct ext2_inode {
__le16 i_mode; /* 文件权限、文件类型 */
__le16 i_uid; /* 所有者UID低16位 */
__le32 i_size; /* 文件大小(字节) */
__le32 i_atime; /* 最后访问时间 access */
__le32 i_ctime; /* 创建时间 change */
__le32 i_mtime; /* 文件内容修改时间 modify */
__le32 i_dtime; /* 删除时间 */
__le16 i_gid; /* 用户组ID低16位 */
__le16 i_links_count; /* 硬链接计数 */
__le32 i_blocks; /* 文件占用块数量 */
__le32 i_flags; /* 文件标志位 */
union {
struct {
__le32 l_i_reserved1;
} linux1;
struct {
__le32 h_i_translator;
} hurd1;
struct {
__le32 m_i_reserved1;
} masix1;
} osd1; /* 操作系统保留域1 */
__le32 i_block[EXT2_N_BLOCKS]; /* 数据块指针数组,一共15项:12直接块 + 一级间接 + 二级间接 + 三级间接 */
__le32 i_generation; /* 文件版本号,NFS使用 */
__le32 i_file_acl; /* 文件ACL */
__le32 i_dir_acl; /* 目录ACL */
__le32 i_faddr; /* 碎片地址 */
union {
struct {
__u8 l_i_frag; /* 碎片编号 */
__u8 l_i_fsize; /* 碎片大小 */
__u16 i_pad1;
__le16 l_i_uid_high; /* UID高16位 */
__le16 l_i_gid_high; /* GID高16位 */
__u32 l_i_reserved2;
} linux2;
struct {
__u8 h_i_frag;
__u8 h_i_fsize;
__le16 h_i_mode_high;
__le16 h_i_uid_high;
__le16 h_i_gid_high;
__le32 h_i_author;
} hurd2;
struct {
__u8 m_i_frag;
__u8 m_i_fsize;
__u16 m_pad1;
__u32 m_i_reserved2[2];
} masix2;
} osd2; /* 操作系统保留域2 */
};
不过呢……要讲清楚路径解析,我们得先把文件系统谈一谈:
ext2文件系统:
我们想要在硬盘上储⽂件,必须先把硬盘格 式化为某种格式的⽂件系统,才能存储⽂件。⽂件系统的⽬的就是组织和管理硬盘中的⽂件。
这张图清晰地展示出了文件系统的层次:我们这里先笼统从上往下,再细节从下往上说:

首先呢,一块盘,太大了不好管理,比如说我有块800G的盘,我可以把它划分为200,250,150,200这四个分区。我只要能够管好每一个分区,我的整个磁盘自然就管理好了。但是呢,这四个分区还是有点大,所以我把200G的分区拿出来,我再划分十个组,每一个20G这样就好管理了,只要我管好每一个组,我就管好了这一个区,我管好每一个区,我就管理好了整一块盘。
从组开始:

这就是我们的一个组:
超级块(super block)
存放⽂件系统本⾝的结构信息,描述整个分区的⽂件系统信息。记录的信息主要有:bolck 和
inode的总量,未使⽤的block和inode的数量,⼀个block和inode的⼤⼩,最近⼀次挂载的时间,最
近⼀次写⼊数据的时间,最近⼀次检验磁盘的时间等其他⽂件系统的相关信息。Super Block的信
息被破坏,可 以说整个⽂件系统结构就被破坏了。
这里有个小问题:既然存放的是系统本身的,那就不需要每一个组都有吧?
事实的确如此,不是每一个组都有超级块:
超级块在每个块组的开头都有⼀份拷⻉(第⼀个块组必须有,后⾯的块组可以没有)。 为了保证⽂ 件系统在磁盘部分扇区出现物理问题的情况下还能正常⼯作,就必须保证⽂件系统的super block信 息在这种情况下也能正常访问。所以⼀个⽂件系统的super block会在多个block group中进⾏备份, 这些super block区域的数据保持⼀致
GDT(Group Descriptor Table)
块组描述符表,描述块组属性信息,整个分区分成多个块组就对应有多少个块组描述符。每个块组描述符存储⼀个块组 的描述信息,如在这个块组中从哪⾥开始是inode Table,从哪⾥开始是Data
Blocks,空闲的inode和数据块还有多少个等等。块组描述符在每个块组的开头都有⼀份拷⻉。
搞一份代码看看:
struct ext2_group_desc
{
__le32 bg_block_bitmap; /* Blocks bitmap block */
__le32 bg_inode_bitmap; /* Inodes bitmap */
__le32 bg_inode_table; /* Inodes table block*/
__le16 bg_free_blocks_count; /* Free blocks count */
__le16 bg_free_inodes_count; /* Free inodes count */
__le16 bg_used_dirs_count; /* Directories count */
__le16 bg_pad;
__le32 bg_reserved[3];
};
块位图(Block Bitmap)
Block Bitmap中记录着Data Block中哪个数据块已经被占⽤,哪个数据块没有被占⽤
inode位图(Inode Bitmap)
每个bit表⽰⼀个inode是否空闲可⽤。
节点表(Inode Table)
存放⽂件属性 如 ⽂件⼤⼩,所有者,最近修改时间等
当前分组所有Inode属性的集合
inode编号以分区为单位,整体划分,不可跨分区
Data Block
数据区:存放⽂件内容,也就是⼀个⼀个的Block。根据不同的⽂件类型有以下⼏种情况:
对于普通⽂件,⽂件的数据存储在数据块中。
对于⽬录,该⽬录下的所有⽂件名和⽬录名存储在所在⽬录的数据块中,除了⽂件名外,ls -l命令
看到的其它信息保存在该⽂件的inode中。
Block 号按照分区划分,不可跨分区
特别注意:目录里面的数据块里存放的,是inode与文件/目录名的映射关系,这也是为什么,我们底层实际用到的是inode,可是我们只要用文件/目录名就可以达到我们的效果。所以!!!!我们在访问目标文件或者目录的时候,一定要带上路径!!!!!就算你自己不带,操作系统也会用cwd给你补上
//我们在进行增删查改的时候呢,基本上都是要对数据块进行操作,那怎么找到数据块?应该先找到inode,inode中存放着数据块的表,所以归根结底,我们都是要用inode
例:
完整链路(以cat test.txt举例):
路径解析&&路径缓存:
理解:
路径解析:把用户传入的文件路径字符串(如 /home/me/test.txt),一层一层目录查找,最终转换成目标文件对应的 inode。 dentry(目录项)缓存:内核把已经解析过的目录 / 文件名 ↔ inode映射关系,放在内存缓存里。下次再访问同一路径,不用再次读磁盘,直接从缓存拿,大幅提速。
dentry(目录项):
文件名 + inode 号的映射
找一份源码
struct dentry {
atomic_t d_count;
unsigned int d_flags; /* protected by d_lock */
spinlock_t d_lock; /* per dentry lock */
struct inode *d_inode; /* Where the name belongs to – NULL is
* negative */
/*
* The next three fields are touched by __d_lookup. Place them here
* so they all fit in a cache line.
*/
struct hlist_node d_hash; /* lookup hash list */
struct dentry *d_parent; /* parent directory */
struct qstr d_name;
struct list_head d_lru; /* LRU list */
/*
* d_child and d_rcu can share memory
*/
union {
struct list_head d_child; /* child of parent list */
struct rcu_head d_rcu;
} d_u;
struct list_head d_subdirs; /* our children */
struct list_head d_alias; /* inode alias list */
unsigned long d_time; /* used by d_revalidate */
struct dentry_operations *d_op;
struct super_block *d_sb; /* The root of the dentry tree */
void *d_fsdata; /* fs-specific data */
#ifdef CONFIG_PROFILING
struct dcookie_struct *d_cookie; /* cookie, if any */
#endif
int d_mounted;
unsigned char d_iname[DNAME_INLINE_LEN_MIN]; /* small names */
};
别的我们都先不要管:
它里面有inode!!!!
![]()
那么,我们的dentry,不就和我们的inode串起来了吗,同时……
struct path {
struct vfsmount *mnt;
struct dentry *dentry;
};
我们有没有发现,很眼熟啊?
struct file {
union {
struct llist_node fu_llist;
struct rcu_head fu_rcuhead;
} f_u;
struct path f_path;
#define f_dentryf_path.dentry
#define f_vfsmntf_path.mnt
const struct file_operations *f_op;
/*
* Protects f_ep_links, f_flags.
* Must not be taken from IRQ context.
*/
spinlock_t f_lock;
atomic_long_t f_count;
unsigned int f_flags;
fmode_t f_mode;
struct mutex f_pos_lock;
loff_t f_pos; // 文件读写偏移量
struct fown_struct f_owner;
const struct cred *f_cred;
struct file_ra_state f_ra;
u64 f_version;
#ifdef CONFIG_SECURITY
void *f_security;
#endif
/* needed for tty driver, and maybe others */
void *private_data;
#ifdef CONFIG_EPOLL
/* Used by fs/eventpoll.c to link all the hooks to this file */
struct list_head f_ep_links;
struct list_head f_tfile_llink;
#endif /* #ifdef CONFIG_EPOLL */
struct address_space *f_mapping;
} __attribute__((aligned(4)));/* lest something weird decides otherwise */
file里面包含着dentry!!!!!!
那么我们串一下:
文件名字符串 → 路径解析 → 查找 / 构建 dentry 缓存(struct dentry)→ dentry 里面d_inode指向 inode → open 成功,生成struct file,file->f_path.dentry保存这个 dentry,返回文件描述符给用户。
注意:
每个⽂件其实都要有对应的dentry结构,包括普通⽂件。这样所有被打开的⽂件,就可以在内存中
形成整个树形结构
整个树形节点也同时会⾪属于LRU(Least Recently Used,最近最少使⽤)结构中,进⾏节点淘汰
整个树形节点也同时会⾪属于Hash,⽅便快速查找
更重要的是,这个树形结构,整体构成了Linux的路径缓存结构,打开访问任何⽂件,都在先在这
棵树下根据路径进⾏查找,找到就返回属性inode和内容,没找到就从磁盘加载路径,添加dentry
结构,缓存新路径
挂载分区
我们已经可以通过inode在指定分区中寻找文件了,但是inode是不可以跨分区的,所以我们应该如何知道我们所要寻找的文件在哪个分区呢,这里我们就要引入挂载分区;
分区写⼊⽂件系统,⽆法直接使⽤,需要和指定的⽬录关联,进⾏挂载才能使⽤。
所以,可以根据访问⽬标⽂件的"路径前缀"准确判断我在哪⼀个分区
本质上:就是让区和目录产生关联,从而使得我们在做路径解析的时候,需要经过挂载后产生的目录,从而得以区分是在哪一个分区;
最终大总结:
task_struct → files指针 → files_struct
files_struct 里 fd 数组:数组下标 = 文件描述符 fd
数组元素:struct file *
struct file 包含 struct path
struct path 包含 struct dentry *
struct dentry 内部 d_inode 指向 inode
inode 通过块指针找到数据块,读写文件
尾:
这篇文章写的好累,早上花了一个多小时回忆以前的知识,列了个大纲,下午正式开始动手花了近五个小时,总之是为暑假的偷懒付出代价了,空悲切







