欢迎光临
我们一直在努力

linux(7) 文件系统

前言:

我记得距离上次更新…..好像过了快一个月了,总想着在开学以前最后一舞,写完文件系统这部分的,只是实在是太懒了,所以只能拖到开学之后了。不过这不是还是来了吗,这期内容会比较长,我们一次性讲完文件系统的所有内容!

今天呢,我们先从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:C 标准库结构体(stdio.h),用户态。fopen/fprintf/fflush 使用,里面封装了缓冲区、底层 fd。 属于 libc。
  • struct file:Linux 内核结构体,内核态。 open 系统调用返回 fd,fd 在进程文件描述表对应struct 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举例):

  • 用户传入文件名 test.txt
  • 内核一层层检索父目录的目录项(dir_entry),目录项里存:文件名 → inode编号
  • 通过 inode 编号,在 inode 表找到对应的inode 结构体
  • inode 里面有i_block[]数据块指针表,依靠这个就能找到文件的数据块,读写内容
  • 路径解析&&路径缓存:

    理解:

    路径解析:把用户传入的文件路径字符串(如 /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 通过块指针找到数据块,读写文件

    尾:

    这篇文章写的好累,早上花了一个多小时回忆以前的知识,列了个大纲,下午正式开始动手花了近五个小时,总之是为暑假的偷懒付出代价了,空悲切

    赞(0)
    未经允许不得转载:171主机测评 » linux(7) 文件系统
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址