Linux---磁盘与文件系统

封装C语言IO函数(demo)

目标:封装一下fclose,fputs,fflush,fopen。感受库函数底层如何封装系统调用以及缓冲区的刷新。

mystdio.h

cpp 复制代码
#pragma once
#include<stdio.h>

#define MAXSIZE 1024
#define NON_BUFFER  1 // 1  不刷新
#define LINE_BUFFER 2 // 10 行刷新
#define FULL_BUFFER 4 // 000 ... 000 100 全刷新
#define MODE 0666

//FILE结构体---里头包含了
//fd,刷新模式,打开文件的模式,缓冲区...
typedef struct _myFILE
{
    int fd;
    int flush_mode;
    int flags;
    //简单封装一个输出缓冲区
    //pos和cap的目的就是不想每次都去清空缓冲区了
    //pos标记目前写到缓冲区哪个下标了/也表示已使用了多少
    //cap标记buffer的总容量
    char outbuffer[MAXSIZE];
    int pos;
    int cap;
}myFILE;

myFILE *myfopen(const char *pathname, const char *mode);//实现r, w, a
int myfputs(const char *str, myFILE *fp);
void myfflush(myFILE *fp);
void myfclose(myFILE *fp);

mystdio.c

cpp 复制代码
#include"mystdio.h"
#include <string.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>

#define MUST_FLUSH 1
#define TRY_FLUSH 2

//fopen("log.txt", "r")
myFILE *myfopen(const char *pathname, const char *mode)
{
    int fd = -1;
    int flags = 0;
    //mode参数为打开方式读/写/追加
    if(strcmp(mode, "r") == 0)
    {
        flags = O_RDONLY;
        fd = open(pathname, flags);
    }
    else if(strcmp(mode, "w") == 0)
    {
        flags = O_WRONLY|O_CREAT|O_TRUNC;
        fd = open(pathname, flags, MODE);
    }
    else if(strcmp(mode, "a") == 0)
    {
        flags = O_WRONLY|O_CREAT|O_APPEND;
        fd = open(pathname, flags, MODE);
    }
    else
    {
        //TODO
    }
    if(fd < 0)
        return NULL;
    
    myFILE* fp = (myFILE*)malloc(sizeof(myFILE));
    if(fp == NULL)
        return NULL;

    //更新myFILE结构体里的属性值然后返回
    fp->fd = fd;
    fp->flush_mode = LINE_BUFFER;//默认为行刷新
    fp->flags = flags;
    fp->pos = 0;
    fp->cap = MAXSIZE;

    return fp;
}

//调用fflush的时候有可能是强制刷新也有可能是满足了刷新条件在刷新
//加一个参数来表示具体是上述的哪种情况
static void myfflushcore(myFILE *fp, int flag)
{
    if(fp->pos == 0)
        return;
    //从语言级缓冲区刷新到内核级文件缓冲区
    //刷新的本质就是在拷贝---但这里还是调用系统调用,程序员无法直接操作内核缓冲区啊
   
    //行刷新---篇幅原因就实现一个行刷新
    //要么就是满足条件刷新要么就是强制刷新
    if((fp->flush_mode & LINE_BUFFER) || (flag & MUST_FLUSH))
    {
        //拿abcd\n举例(如果在语言级缓冲区里的是这个字符串)---如果是行刷新代码来到这,末尾会有\n
        //pos==5,pos-1指向的才是\n
        //额外判断以下末尾是否真的有\n,因为myfopen的时候的默认刷新方式就是行刷新
        if((fp->outbuffer[fp->pos - 1] == '\n') || (flag & MUST_FLUSH))
        {
            write(fp->fd, fp->outbuffer, fp->pos);
            fp->pos = 0;//清空缓冲区
        }
    }
    //全刷新
    else if((fp->flush_mode & FULL_BUFFER))
    {

    }
    else
    {
        //NOBUFFER
    }
}

void myfflush(myFILE* fp)
{
    myfflushcore(fp, MUST_FLUSH);
}

int myfputs(const char *str, myFILE *fp)
{
    if(strlen(str) == 0)
        return 0;
    //从用户缓冲区(str)拷贝到语言级缓冲区
    //注意一定要加上pos,不然就变成从头覆盖写入了
    memcpy(fp->outbuffer + fp->pos, str, strlen(str));//不包含\0
    fp->pos += strlen(str);
    
    //如果可以,刷新一下
    myfflushcore(fp, TRY_FLUSH);
    return strlen(str);
}

void myfclose(myFILE *fp)
{
    //刷新缓冲区---强制刷新
    myfflush(fp);

    //如果还想强制OS刷新内核级文件缓冲区
    fsync(fp->fd);

    //关闭文件
    close(fp->pos);

    //释放myFILE
    free(fp);
}

main.c

cpp 复制代码
//#include"mystdio.h"
//#include <unistd.h>
//
//int main()
//{
//    myFILE* fp = myfopen("log.txt", "a");
//    if(fp == NULL)
//        return -1;
//    
//    const char* str = "hello \n";
//    int cnt = 10;
//    while(cnt--)
//    {
//        myfputs(str, fp);
//        sleep(1);
//        printf("outbuffer: %s\n", fp->outbuffer);
//    }
//
//    myfclose(fp);
//    return 0;
//}

#include "mystdio.h"
#include <unistd.h>

int main()
{
   myFILE *fp = myfopen("log.txt", "a");
   if(fp == NULL)
   {
       printf("myfopen error!\n");
       return 0;
   }
   
   const char *msg = "hello bit\n";
   int cnt = 10;
   while(cnt--)
   {
       myfputs(msg, fp);
       sleep(1);
       printf("debug: outbuffer = %s, pos = %d\n", fp->outbuffer, fp->pos);
   }
   myfclose(fp);
   printf("write file done!\n");
   return 0;
}

补充:如何理解标准错误标准输入标准输出?

文件打开的时候会默认有3个流被打开,那为什么它们三个会被自动打开?首先一个进程(程序)被创建出来肯定是要完成任务的,一般都是处理数据,我们知道012对应的硬件就是键盘和显示器,键盘负责输入数据,显示器负责拿到数据让人来分析debug,而显示器上的数据又可分为正确数据和错误信息,因此标准错误/输入/输出就肯定是要被打开的。

cpp 复制代码
#include<string.h>
#include<stdio.h>
#include<unistd.h>

int main()
{   
    //标准输出
    printf("这是一个正常消息\n");
    fprintf(stdout,"这是一个正常消息\n");
    const char* s1 = "这是一个正常消息\n";
    write(1, s1, strlen(s1));

    //标准错误
    fprintf(stderr,"这是一个错误消息\n");
    const char* s2 =  "这是一个错误消息\n";
    write(2, s2, strlen(s2));
    perror("perror, hello");

    return 0;
}

正常运行的结果:

重定向的结果:显示器上只有错误流的信息,文件里是输出流的信息。因为重定向的时候只是改变了标准输出,fd==1所指向的文件发生了改变。

重定向改变标准错误:(也可以用dup2),下边的意思就是相当于将fd==2里对应的文件重定向成了normal.txt,跟使用dup2一个效果。上文的重定向的"全称"应该是./inouterr.exe 1>normal.txt

正确消息错误消息分离输出:

正确错误信息输出到同一个文件:上下两种写法一样,2>&1的意思就相当于是dup2(1, 2),相当于把fd==2也重定向到fd==1所对应的文件里,也就是下边代码里的normal.txt。

总结:以上所有内容包括上一篇博客,基础IO,都是在将文件被打开的情况下的各种操作。从学习C语言的第一堂课开始,老师就告诉我们计算机视角下,一切全是二进制,对于文件也是一样,所谓的缓冲区里全是二进制数据,计算机可不会去区分谁是字符串,谁是图片,这些都是由人来区分的,所以上层的语言层会给你提供各种各样的文件读写IO函数,文件里到底是什么,你来区分,C语言只是给你提供一系列读取它们的接口而已。最佳实践:文件操作,尽量用语言IO函数,效率高,实在不行再用系统调用。

理解硬件

为了追求极致的轻便,笔记本电脑上现在一般用SSD(固态硬盘),但是在公司里服务器方面基本上还是使用的机械磁盘,因为SSD造价比较高,机械磁盘的容量大,价格便宜,比较慢。

常见的一些硬件

磁盘的物理结构

磁盘的光面跟光盘的差不多,但是光盘是只有一面光的,还有一面是磨砂的,磁盘是两面都是光的,并且一个磁盘可能有好几个两面都是光滑的光盘组成,光的一面都可以存数据。磁盘上存的都是01数据,就恰巧和磁铁的南北极一样,所以可以将磁盘的盘片想象成无数个小磁铁组成(磁盘不是有两面嘛,一面N一面S),修改磁盘上的01数据本质就是在改变磁铁的南北极,磁铁可以放几十年,相当于可以永久存储,所以要报废一个磁盘,必须要消磁,可以用火烧或者利用一些软件将南北级全置为1或者0,保护国家信息安全。

磁头和盘片能挨着吗?不能,因为盘片在高速旋转,磁头在前后移动,(这也解释了为什么叫机械磁盘的机械二字),一旦挨着摩擦生热,容易消磁,将数据带走。所以笔记本里也不太好装磁盘,因为笔记本电脑里带来带去容易让它俩碰到,机房里或者台式机不会动的就比较适合。

具体磁盘的各个物理名称在上文里已经有贴图。接下来来看看数据是存储在磁盘哪里的(具体)?磁盘的一个盘片是由一圈圈同心圆组成,一个个同心圆叫做磁道,磁道不是连续的,会有间隙,一个间隙两边的类似于小格子的是扇区(具体看图一)。真正的磁盘有很多的盘面(下图b),每一个盘面的结构一模一样,由磁道和扇区组成。

正是因为磁盘有好多个盘面,每一个盘面都要有一个磁头,所以真正的磁盘是长成下边那种样子的。每一个盘面都有相同半径的磁道,这些磁道连起来就是一个柱面,基本上往后的博客里,磁道和柱面的概念可以模糊一点,理解为一个东西。

综上:磁盘的3个最重要的结构,磁道,柱面,扇区。其中扇区是以512字节为单位进行IO的,它是一个块设备。困惑:离圆心越近的扇区越小,反之越大,我们认为它们存储容量大小一样,只是密度不太一样。

问题:我想向磁盘写入512字节的数据,我应该怎么写?有两种讲法。

第一种:CHS定位(软件上很少用,底层本质还是这个)

1.根据磁头编号找到对应在哪一个盘面,因为盘面是没有编号的。

2.在哪一个磁道

3.确认在当前磁道的哪一个扇区

根据磁头,磁道,扇区的英文首字母将这种方式叫做CHS地址定位,定位到一个扇区。能定位一个就能定位多个,文件=内容+属性=数据,文件存入磁盘扇区,就是先找到再存储的过程。

磁盘的逻辑结构

磁盘有许多个盘面,先来看一个盘面,一个盘面上有许多个磁道,一圈圈的类似于磁带一样卷起来了,当把它们从逻辑上拉直之后就变成了线性结构,一个盘面就在逻辑上抽象成了一维数组。一个柱面就由多个大小一样的一维数组组成了一个二维数组,一个磁盘就由多个柱面(二维数组)组成了一个三维数组,因此磁盘在逻辑上就是一个三维数组。而三维数组其实在物理结构上也是一维数组,三维数组的下标(本质就是一个大一维数组的下标)就是每一个扇区的地址,叫做LBA地址。(一个盘面上有多个同心圆,具体可以看看上边的图,其实每一个磁道的扇区个数是一样的,因此每一个柱面上的扇区数量也是一样的),因此定位一个扇区就用数组下标(LBA地址)去定位就可以了。(注:磁盘在逻辑上是由一个个柱面卷起来的,要把它们当成整体去看,而不是一个个盘面拆开看)

回过头去看上文的那个写入512字节的问题,磁盘在进行访问的时候是先确定我们访问的是哪一个柱面,因为磁头共进退在摆动(具体看下图),磁头摆动的意义本质就是去访问哪一个磁道或者柱面。盘面在不停的高速旋转,它旋转的意义就是定位到指定扇区。读取哪一个扇区,选定哪一个磁头即可。上文说的CHS本质就是先确定柱面,再确定磁头(确定在哪一个盘面),最后确定在哪一个扇区,CHS地址本质就是三维数组的一个下标。

有了上边的认知我们就轻松知道LBA和CHS的关系,一个磁盘抽象成一个一维数组,下标就是LBA地址,而CHS地址是一维数组抽象前的三维数组下标,不用多想,它俩一定可以互相转化,具体怎么转化其实是磁盘的事,软件层的我们只需要知道LBA地址就可以了,根据磁盘的总容量/扇区大小(扇区的大小固定)=扇区的个数,根据首地址就可以计算任何一个扇区的地址(LBA地址),磁盘的总容量在OS开机的时候就会知道。所以到底怎么写入512字节?拿到LBA地址之后交给磁盘,它内部会转化为CHS地址,也就找到了对应的扇区,将数据写入。更具体一点来说,磁盘里会有一个伺服系统,还有控制伺服系统的控制器,在伺服系统里主要有三个寄存器,分别是表示是向磁盘里r还是w的,表示LBA地址的,表示向磁盘写入的数据data的,未来,OS只要给磁盘三个信息,是写还是读,往哪里写LBA,写什么data,磁盘内部就会将LBA转化为CHS,找到对应的扇区,通过磁头向扇区写入数据。

文件系统

OS读取磁盘是以块为单位的,一个块一般为4kb,也就是8个扇区。为什么要以块为单位?本质上就是减小硬件查询的次数,提高效率,如果一个个扇区的读写,就要一个个找,以块为单位,就是利用磁头共进退,多个磁头可以统一定位到一个柱面上,进而定位到多个扇区,一起读写效率高。既然是以块为单位,每一个块就会有块地址/块号,OS可以将一个块号转化为该块里边每一个扇区的LBA地址,将这8个LBA地址交给磁盘,磁盘一起读取。同理,有了LBA地址也可以转化为块号。至此,无论是从磁盘到OS,还是OS到磁盘,地址之间可以互相转化,块地址->LBA->CHS,反之也可。

为什么OS不直接用扇区(512)来访问磁盘,转而用4KB呢?

1.提高效率(主因)->上文已解释过

2.软硬件解耦(其他因素)->如果以扇区为单位读写,未来磁盘结构变了,扇区的大小变了,OS对磁盘的读写方式就得全部改了,但现在引入块的概念,往后硬件有变化,无非就是改变一下块地址转化为LBA地址的方式。

分区分组

文件=内容+属性,在Linux系统中,内容和属性要分开来存。一个组里边应该要包含哪些内容?文件数据+管理文件数据的信息,它俩都是数据。在开辟了一个组之后,第一步就是写入管理信息,这个过程就叫做写入文件系统,也叫格式化(写入全新文件系统的过程)。

对于每一个分组来说,管理信息和文件属性都是占比较少量的空间,大部分空间是给文件内容用的,data blocks里是一个个的4KB块,每一个data blocks里的块都会有唯一编号。inode table里存的也是一个个4kb的块,每一个块里存的是一个个inode结构体,inode结构体里存着一个文件的所有属性(文件名不属于inode属性),包括inode编号(ls -l -i可以查看),理论上一个文件一个inode。

问题1:为什么inode结构体的大小固定为128字节?方便管理,要给每一个inode编号不可能还是变长的,就不好编号了,所以文件名不会在inode里,看看inode结构体底层源码就知道文件的每一个属性都变成了8/16/32bit的数字存在inode里,固定数字,好计算好分块好管理。

问题2:文件的内容怎么去找?我怎么知道哪几块4kb里的内容是我当前这个文件的内容?在inode结构体里有一个i_block数组,里边存着该inode编号对应的文件内容在data blocks里的所有4kb块的编号,说白了就是i_block里存储了data blocks里4kb块的编号。

结论:在一个分组里面找到一个文件,根据文件的inode编号->inode->i_block\[\]->data block里对应的块。

block bitmap:是位图,内容表示data block里哪个数据块被占用,哪个没有被占用,下标就表示数据块的编号。

inode bitmap:是位图,下标表示inode编号,内容表示该inode编号是否被占用。

问题3:每一个组里都有super block吗?不是,只有2~3个组有super block,本质就是备份。如果只有一个super block会有风险,磁盘是机械设备,磁头和盘面有可能会发生碰撞摩擦导致数据缺失,万一正好把super block里的数据搞掉了,损失的可是整整一个分区的数据,量很大。GDT不备份的原因就是一个分组数据量不大。

理解创建、删除、修改、查看

前提:接下来说的所有场景,都先从文件的inode编号开始讨论。

创建:我们使用touch指令创建文件,底层调用系统调用,创建出文件属性在内存的缓冲区里,之后需要刷新到磁盘里,会先在inode bitmap里找到最近的没有被使用过的比特位,然后内容置为1(得先将inode bitmap对应的块加载到内存才能修改,下边都同理,无法直接在硬件上修改),下标就是对应的inode编号,回过头到inode table里对应inode编号位置将文件属性从内存刷新进去,如果还要向文件写入内容,同理,先去block bitmap里找到没有被使用的比特位,置为1,下标就是对应在data blocks里的块编号,内容就从内存刷新进去,最后将块编号写入inode结构体里的i_block里。(假设就写入1bit数据,只要一个4kb块即可)

删除:先得拿到这个文件的inode编号(怎么拿到的之后说),根据inode编号去inode bitmap里查看对应的下标(就是inode编号数字)是否真的为1,为1就说明是有文件的,将其置为0,然后根据inode编号去inode table里找该文件的inode结构体,根据结构体里的i_block数组里的块编号去将block bitmap里对应下标位置置为0。说白了,只需要把两个bitmap对应位置的内容设置成0就可以了,并不用真正意义上删除内容。因此恢复一个文件,其实就只需要反操作就可以了,你其实可以去日志里查到删除文件的inode编号,有了inode编号就可以去inode bitmap里对应位置由0改为1,再由inode编号找到inode结构体里的i_block,将里边的块编号到block bitmap里恢复成1。所以往后误删文件的话,第一步不要干创建文件的事情,因为这样容易覆盖掉之前删除的那个文件在inode table和data blocks里的真实数据,这样就彻底恢复不了了,第二步就是找专业人去帮你恢复,这个毕竟是运维干的事情,我们几乎不会涉及到。

修改:先拿到inode编号,到对应inode bitmap下标看看是否真的存在,文件=内容+属性,无论是修改哪一个,都是读改写的过程,先根据inode编号找到inode结构体,将属性全部加载到内存里(struct file)去修改了之后在写进来,修改数据的话就是先根据inode编号找到inode结构体里的i_block里的所有块编号,将data blocks里对应的块全部加载到内核级文件缓冲区里修改,最后刷新回磁盘。

查看:先拿到inode编号,到对应inode bitmap下标看看是否真的存在,然后就是找到inode结构体加载属性到内存查看,要么就是根据i_block里的块编号在data blocks里找到对应的块然后查看。

上述只是笼统理解,还有些许偏差,我们平时查看文件的属性和内容都是根据文件名字来的呀,压根都没有用过inode number,那这到底是怎么回事呢?要想知道这个,得先理解一下目录。Linux下一切皆文件,目录也是文件,只要是文件就有inode编号,文件=内容+属性,属性好理解,目录的inode里存着呢,那目录里的内容是什么?当前目录里所包含的文件:文件名和inode number的映射表。它们也是数据,也得存在data blocks的数据块里。所以我们用ls -l test.c去查看文件属性的时候本先根据当前目录的inode编号去inode里找i_block,再根据里边的块编号找到data blocks里该目录的内容质就是(映射表),从而找到了test.c的inode编号,接下来的步骤就跟上文的一模一样了。

问题1:在磁盘和文件系统角度,存储目录和存储普通文件有区别吗?没区别,都是inode+data block,只不过里边的内容不一样而已,一切皆文件的另一个角度。

问题2:目录rw权限问题,为什么目录没了r或者w权限就没法对目录做操作?目录内容里面保存的是当前目录下的文件名和inode number的映射关系,本质就是不让你读不让你写映射表,你没没有文件的inode编号,啥也干不了啊。

所以为什么查看文件内容和属性用文件名就可以了?因为文件所处的目录有rw权限,就能获取到文件的inode编号,从而就回归到上文说过的根据inode编号进行增删查改操作了。补充:一个目录里的文件名字能重复吗?不能,文件名要当作键值去查inode的。

重谈inode块号和编号

你怎么知道根据某个inode编号就去比如说组1里找,你是怎么知道哪个inode编号对应哪个组的?

a.inode编号和块号不是组内唯一,而是整个分区(文件系统)内唯一,也就是说inode块号和编号是可以跨组的,但不能跨分区。

b.在一个分区内部,也就是一个文件系统内部,有多少个inode以及每个inode在data blocks里对应多少个数据块都是固定的,都是提前设计好的。注意:一个磁盘内的空间难以百分百全部用掉,有可能inode全部用完了,data blocks里的数据块还有剩的,反过来有可能如果磁盘里存着大文件,一个inode比如要对应几百个data blocks里的数据块,那也有可能inode还有,但是data blocks里的数据块不够用了。

一个分区(文件系统)里的每个块里有多少个inode都是固定的数字,我现在拿到一个inode编号就可以去计算它在哪一个组里,编号/每个块的inode总数(结果要取整数),从这里也可以知道,每个组里的inode bitmap的下标其实表示的是偏移量,因为inode编号在逻辑上是在分区里连续的,比如说我现在在第1个组里,这个组的第一个inode编号为10000,然后我当前查看的文件的inode为10005,也就是说这个文件在组1里,然后inode bitmap里下标为5(相较于起始编号的偏移量)的内容就为1。data blocks里的块编号也是分区内逻辑上连续的。如果是创建文件,就需要每个组里GDT记录的内容去知道哪个组里的inode以及对应data blocks里的数据块还有空余。

综上,上文的创建、删除、修改、查看文件的步骤还得加一步,先得根据inode编号找到对应在哪一个组。

路径解析与路径缓存

如果我要访问我当前文件内容或者属性,首先要做的就是打开当前目录,访问当前目录的数据块。打开当前目录是不是需要知道当前目录的inode number,从而才能一步步找到data blocks里的数据块?那当前目录的inode number是不是在上一级文件目录的数据块里存着,那我要知道当前目录的inode number,我就必须要打开上一级目录访问里边内容,要打开上一级目录访问里边内容就要知道上一级目录的inode number,上一级目录的inode number又在上上级的目录的数据块里.......直至根目录,整个过程是一个递归的过程。

当我们要访问任何文件的时候,Linux内核都要为我们做从/开始的路径解析,所以访问文件的时候必须要有路径。

问题1:每次打开曾经打开过的路径都要做路径解析吗?这样做是不是太慢了?很显然不会,对于用户访问过的路径,Linux是会做路径缓存的,OS里有那么多使用过的路径,Linux都要去管理好它们,要想管理就得先描述再组织,那必然就需要依赖结构体,struct dentry{......}。假如说现在要访问的路径是/home/test.txt,/是我们直接能访问到的(内核启动时,它的dentry和inode就在缓存里有了),在它的dentry里有指向inode的指针,从而找到它的inode number,根据inode编号去找到数据块里的home和其inode编号,在缓存里创建home的dentry并且里头有指针指向/的dentry....依次往下找到test.txt,我们访问到了test.txt这个文件,我们也将这个路径给缓存了起来。所以open打开文件的过程其实也是路径解析与缓存的过程。文件打开加载到内存后会有struct file,它里面会有指向dentry的指针,dentry里又有指向inode结构体的指针,从而找到了inode编号,就可以去访问文件的属性和内容了。简单理解,同一条路径,第一次需要做一次完整的路径解析并且构建缓存树的某一个分支,第二次访问这个路径就直接便利缓存树,去对比路径里各个目录的名字和结点里存的目录名去找。

子问题1:这棵多叉树会动态变化吗?会的,你新打开的路径在里头没有,就相当于会给你创建一个分支存这个路径,如果某些路径长时间不用,它也会自动清理掉。

子问题2:只有目录才有dentry吗?不是的,Linux下一切皆文件,每一个被访问过的文件都有dentry,只不过普通文件就是多叉树的叶子结点。

子问题3:之前用find指令搜索的时候,可能第一次比较卡顿,第二次就快了,为什么?原因就是第一次需要路径解析与路径缓存,而第二次访问,路上的路径已经被缓存了,就直接可以找到,不用去解析。

子问题4:相对路径如何处理?跟绝对路径一样,比如说../XXX/XXX,当前路径肯定是知道的,在缓存树里有,如果没有,进程的/proc/pid里也能找到,找到后加载到缓存树里,所以上一级路径也能找到,根据当前路径dentry里的parent指针,找到上一级路径之后接下去的路径,就先在缓存里找,看有没有,没有就根据..dentry里的指针找到inode从而找到inode编号进而找到数据块里的映射表拿到该文件的inode编号......

总结,本文从理解创建删除修改查看文件开始,先是知道inode编号去找到具体哪个分组,进而就可以去操作文件,再往外一层,文件的inode编号哪里来?是从当前目录的内容,根据文件名与inode编号的映射表里来的,再往外一层,当前目录的inode编号哪里来,因为只有知道了目录的inode编号才能去找到data blocks里目录的内容呀,由路径解析和路径缓存的整个过程里一步一步来,再往外一层,路径哪里来,之前说的所有大前提是不是得先由路径才能去路径解析和缓存一步步往下找到文件的inode编号,文件的属性和内容就都能找到了。

路径谁提供?

你访问⽂件,都是指令/⼯具访问,本质是进程访问,进程有CWD!进程提供路径。进程的CWD由父进程bash提供,bash的CWD从系统和环境变量里来

你open⽂件的时候,自己就提供了路径

最开始的路径从哪⾥来?

1.所以Linux为什么要有根⽬录, 根⽬录下为什么要有那么多缺省⽬录?

2.你为什么要有家⽬录,你⾃⼰可以新建⽬录?

1和2是系统构建好的,不同用户操心。

综上:

上⾯所有⾏为:本质就是在磁盘⽂件系统中,新建⽬录⽂件。⽽你新建的任何⽂件,都在你或者系统指定的⽬录下新建,这不就是天然就有路径了嘛!系统+⽤户共同构建Linux路径结构

存储大文件问题

先明确一件事,在inode里有i_block数组,它是一个静态数组,大小为15(源码里定义了),按照之前的说法,i_block就对应15个数据块,那是不是就是说一个分组里最多存储15*4==60kb的文件?如果是这样不就说明Linux里边都存不下大文件吗?

不是的,i_block确实是15个元素,前12个下标叫直接块指针,就是存普通的块号,这里多说一点,广义上来说,数组下标也可以当成指针,指针的本质就是映射/指向,在剩下3个下标当中,12号下标叫一级间接块索引表指针,内容也是存的块号,但是这个块就不是存普通的数据了,而是存的data blocks里的块号,意思就是说一级间接块索引表指针里存的是一个块号,然后这个块里存的不是数据,而是其他该文件数据所占据的块的块号,拿一个4kb去存块号。13号下标叫二级间接块索引表指针,其指向的块里存的是其他块号,这个块里边的块号所指向的块里还是存的块号,就相当于二级间接块索引表指针指向的块里存的是一级间接块索引表指针。14号下标叫三级间接块索引指针表,同理,其指向的块里相当于是存了二级间接块索引表指针。有了12,13,14号下标,整个分组能存储的文件容量就很大,很显然一个组里都没这么多数据块,所以以这种方式就能存储大文件了。

块号是分区内唯一的,也就是说块号是分区内连续且唯一的,一个分组里存的文件如果很大,就可以借助间接块索引表指针存别的组里的块号,借助其他组里的块来存我当前这个分组里的文件。怎么写间接块索引表呢?读改写嘛,先将inode加载进内存,然后比如说将一级间接块索引表加载到内存,然后去各个分组里查(GDT里不是有各分组的块的数量...,包括位图)没存数据的块,然后将对应的块号直接写入我的表某一块申请的空缺的地方,最后再将表写入磁盘。

块内不止存储文件自己的数据,也可以存储自己文件用的更多的块号。

最后一层

上述的所有事情都是在一个分区里完成的,那我怎么知道我的文件存在哪一个分区里?(分区相当于windows里的C盘D盘),上文里说的所有文件操作都针对的是某一个特定的分区,就是我已经知道我的文件在哪一个分区里了,可是我怎么知道它在哪一个分区里呢?对于一个磁盘来说,先分区然后格式化,格式化的本质就是写入管理信息,这个管理信息就叫磁盘级文件系统,分区格式化之后,这个文件系统和分区是不能直接用的,OS要先把你的分区或者文件系统挂载到指定的目录下,也就是说你访问每一个文件的时候(访问文件一定要有路径),文件所处路径里一定会知道当前分区是哪一个(通过路径前缀来区分),说白了就还是根据路径找到具体是哪一个分区,就可以进行文件操作了。