首页 > 其他分享 >手写简易操作系统(二十三)--文件系统第一部分

手写简易操作系统(二十三)--文件系统第一部分

时间:2024-04-09 18:01:07浏览次数:20  
标签:文件 -- 分区 文件系统 bitmap part sb 手写 inode

前情提要

上面我们做好了文件系统实现的基础,现在我们开始实现文件系统。

一、文件系统概念

UNIX文件系统比较先进,它将文件以索引结构来组织,避免了访问某一数据块需要从头把其前所有数据块再遍历一次的缺点。采用索引结构的文件系统,文件中的块依然可以分散到不连续的零散空间中,保留了磁盘高利用率的优点,更重要的是文件系统为每个文件的所有块建立了一个索引表,索引表就是块地址数组,每个数组元素就是块的地址,数组元素下标是文件块的索引,第n个数组元素指向文件中的第n个块,这样访问任意一个块的时候,只要从索引表中获得块地址就可以了,速度大大提升。

包含此索引表的索引结构称为inode,即index node,索引结点,用来索引、跟踪一个文件的所有块。强调下,inode是文件索引结构组织形式的具体体现,必须为每个文件都单独配备一个这样的元信息数据结构,因此在UINX文件系统中,一个文件必须对应一个inode,磁盘中有多少文件就有多少inode。

1.1、inode结构

inode 的结构如下

image-20240402163812486

可以看到,inode节点就相当于文件的内容,其中包含了这个文件的属性,包含12个直接块指针,和三个间接块指针。

1.2、目录项

在Linux中,目录和文件都用inode来表示,因此目录也是文件,只是目录是包含文件的文件。为了表述清楚这两种文件,我们这里称目录为目录文件,一般意义上的文件称为普通文件。在Linux中,只要是文件就一定会有个与之匹配的inode。按理说,既然同一种inode同时表示目录和普通文件,这说明在inode中它们是没区别的,那文件系统是如何区分目录和文件呢?

在磁盘上的文件系统中,没有一种专门称为目录的数据结构,磁盘上有的只是inode。inode用于描述一个文件实体的数据块,至于该数据块中记录的是什么,这并不是inode决定的,inode也不必关心它是什么。

区分该inode是普通文件,还是目录文件,唯一的地方只能是数据块本身的内容了。如果该inode表示的是普通文件,此inode指向的数据块中的内容应该是普通文件自己的数据。如果该inode表示的是目录文件,此inode指向的数据块中的内容应该是该目录下的目录项。本文只会支持目录文件和普通文件(不会存在管道文件、socket之类),因此目录中要么是普通文件的目录项,要么是目录文件的目录项。

不管文件是普通文件,还是目录文件,它总会存在于某个目录中,所有的普通文件或目录文件都存在于根目录’/'之下,根目录是所有目录的父目录。

这里还有一个问题,根目录在哪里?只有目录中保存着这个目录下文件的信息,例如这个文件属于什么文件,这个文件对应的inode节点在哪儿。所以目录必须有个根,也就是根目录,而且根目录还必须是已知的位置。这个位置就保存在超级块中。

其结构如下

image-20240402164848949

image-20240403134531097

1.3、超级块与文件系统布局

每个文件都有个inode,所有的inode都放在inode数组中,请问,inode数组在哪里?大小是多少?
根目录的地址是固定写死的,但每个分区都有自己的根目录,其地址并不统一,怎么找到根目录地址?

我们需要在某个固定地方去获取文件系统元信息的配置,这个地方就是超级块,超级块是保存文件系统元信息的元信息。超级块中的属性以及硬盘的布局如下

image-20240402165252645

图中只是一个分区文件系统元信息的布局,其他各分区中是同构的,咱们也按照这种布局来设计自己的文件系统。

二、创建文件系统

2.1、超级块结构

/* 超级块 */
struct super_block {
    uint32_t magic;		              // 用来标识文件系统类型,支持多文件系统的操作系统通过此标志来识别文件系统类型
    uint32_t sec_cnt;		          // 本分区总共的扇区数
    uint32_t inode_cnt;		          // 本分区中inode数量
    uint32_t part_lba_base;	          // 本分区的起始lba地址

    uint32_t block_bitmap_lba;	      // 块位图本身起始扇区地址
    uint32_t block_bitmap_sects;      // 扇区位图本身占用的扇区数量

    uint32_t inode_bitmap_lba;	      // i结点位图起始扇区lba地址
    uint32_t inode_bitmap_sects;	  // i结点位图占用的扇区数量

    uint32_t inode_table_lba;	      // i结点表起始扇区lba地址
    uint32_t inode_table_sects;	      // i结点表占用的扇区数量

    uint32_t data_start_lba;	      // 数据区开始的第一个扇区号
    uint32_t root_inode_no;	          // 根目录所在的I结点号
    uint32_t dir_entry_size;	      // 目录项大小,现在是32字节

    uint8_t  pad[460];		          // 加上460字节,凑够512字节1扇区大小
} __attribute__ ((packed));

2.2、inode结构

/* inode结构 */
struct inode {
    uint32_t i_no;           // inode编号
    uint32_t i_size;         // 当此inode是文件时,i_size是指文件大小,若此inode是目录,i_size是指该目录下所有目录项大小之和
    uint32_t i_open_cnts;    // 记录此文件被打开的次数
    uint32_t user_id;        // 文件所属用户id
    uint64_t ctime;          // inode上一次变动时间
    uint64_t mtime;          // 文件内容上一次变动时间
    uint64_t atime;          // 文件上一次打开时间
    uint32_t privilege;      // 权限,可读可写可执行 1-7
    bool write_deny;	     // 写文件不能并行,进程写文件前检查此标识
    uint32_t i_sectors[13];  // i_sectors[0-11]是直接块, i_sectors[12]用来存储一级间接块指针
    struct list_elem inode_tag;
};

2.3、目录和目录项结构

/* 目录结构 */
struct dir {
    struct inode* inode;
    uint32_t dir_pos;	        // 记录在目录内的偏移
    uint8_t dir_buf[512];       // 目录的数据缓存
};

/* 目录项结构 */
struct dir_entry {
    char filename[MAX_FILE_NAME_LEN];  // 普通文件或目录名称
    uint32_t i_no;		               // 普通文件或目录对应的inode编号
    enum file_types f_type;	           // 文件类型
};

2.4、文件系统类型、打开文件训练

/* 文件类型 */
enum file_types {
    FT_UNKNOWN,	  // 不支持的文件类型
    FT_REGULAR,	  // 普通文件
    FT_DIRECTORY  // 目录
};

/* 打开文件的选项 */
enum oflags {
    O_RDONLY,	  // 只读
    O_WRONLY,	  // 只写
    O_RDWR,	      // 读写
    O_CREAT = 4	  // 创建
};

2.5、创建文件系统

如果检测到默认的分区存在文件系统,那么不做处理,加载这个文件系统。

如果检测到默认的分区不存在文件系统,那么初始化这个分区,也就是在这个分区上创建文件系统

/* 格式化分区,也就是初始化分区的元信息,创建文件系统 */
static void partition_format(struct partition* part) {
    // 导引块占一个块
    uint32_t boot_sector_sects = 1;
    // 超级块占一个块
    uint32_t super_block_sects = 1;
    // inode位图占的块数,这里也是一个块
    uint32_t inode_bitmap_sects = DIV_ROUND_UP(MAX_FILES_PER_PART, BITS_PER_SECTOR);
    // inode占的块数
    uint32_t inode_table_sects = DIV_ROUND_UP(((sizeof(struct inode) * MAX_FILES_PER_PART)), SECTOR_SIZE);
    // 已使用的块数
    uint32_t used_sects = boot_sector_sects + super_block_sects + inode_bitmap_sects + inode_table_sects;
    // 空闲块数
    uint32_t free_sects = part->sec_cnt - used_sects;
    // 空闲块位图占据的块数
    uint32_t block_bitmap_sects = DIV_ROUND_UP(free_sects, BITS_PER_SECTOR);
    // 空闲块位图长度
    uint32_t block_bitmap_bit_len = DIV_ROUND_UP((free_sects - block_bitmap_sects), BITS_PER_SECTOR);


    /* 超级块初始化 */
    struct super_block sb;
    sb.magic = SUPER_BLOCK_MAGIC;
    sb.sec_cnt = part->sec_cnt;
    sb.inode_cnt = MAX_FILES_PER_PART;
    sb.part_lba_base = part->start_lba;
    // 第0块是引导块,第一块是超级块,之后是空闲块位图
    sb.block_bitmap_lba = sb.part_lba_base + 2;
    sb.block_bitmap_sects = block_bitmap_sects;
    sb.inode_bitmap_lba = sb.block_bitmap_lba + sb.block_bitmap_sects;
    sb.inode_bitmap_sects = inode_bitmap_sects;
    sb.inode_table_lba = sb.inode_bitmap_lba + sb.inode_bitmap_sects;
    sb.inode_table_sects = inode_table_sects;
    sb.data_start_lba = sb.inode_table_lba + sb.inode_table_sects;
    sb.root_inode_no = 0;
    sb.dir_entry_size = sizeof(struct dir_entry);

    printk("%s info:\n", part->name);
    printk("magic:0x%x\n   \
            part_lba_base:0x%x\n   \
            all_sectors:0x%x\n   \
            inode_cnt:0x%x\n   \
            block_bitmap_lba:0x%x\n   \
            block_bitmap_sectors:0x%x\n   \
            inode_bitmap_lba:0x%x\n   \
            inode_bitmap_sectors:0x%x\n   \
            inode_table_lba:0x%x\n   \
            inode_table_sectors:0x%x\n   \
            data_start_lba:0x%x\n", \
        sb.magic, \
        sb.part_lba_base, \
        sb.sec_cnt, \
        sb.inode_cnt, \
        sb.block_bitmap_lba, \
        sb.block_bitmap_sects, \
        sb.inode_bitmap_lba, \
        sb.inode_bitmap_sects, \
        sb.inode_table_lba, \
        sb.inode_table_sects, \
        sb.data_start_lba);

    // 拿到硬盘的指针
    struct disk* hd = part->my_disk;
    // 将超级块写入本分区的第一扇区
    ide_write(hd, part->start_lba + 1, &sb, 1);
    // 超级块的lba
    printk("   super_block_lba:0x%x\n", part->start_lba + 1);

    // 找出数据量最大的元信息,用其尺寸做存储缓冲区
    uint32_t buf_size = (sb.block_bitmap_sects >= sb.inode_bitmap_sects ? sb.block_bitmap_sects : sb.inode_bitmap_sects);
    buf_size = (buf_size >= sb.inode_table_sects ? buf_size : sb.inode_table_sects) * SECTOR_SIZE;
    // 申请的内存由内存管理系统清0后返回
    uint8_t* buf = (uint8_t*)sys_malloc(buf_size);
    // 将块位图初始化并写入sb.block_bitmap_lba,第0个块留给根目录
    buf[0] |= 0x01;
    uint32_t block_bitmap_last_byte = block_bitmap_bit_len / 8;
    uint8_t  block_bitmap_last_bit = block_bitmap_bit_len % 8;
    uint32_t last_size = SECTOR_SIZE - (block_bitmap_last_byte % SECTOR_SIZE);
    // 先将位图最后一字节到其所在的扇区的结束全置为1,即超出实际块数的部分直接置为已占用
    memset(&buf[block_bitmap_last_byte], 0xff, last_size);
    // 再将上一步中覆盖的最后一字节内的有效位重新置0
    for (uint8_t bit_idx = 0; bit_idx <= block_bitmap_last_bit; bit_idx++) {
        buf[block_bitmap_last_byte] &= ~(1 << bit_idx);
    }
    // 写入块位图
    ide_write(hd, sb.block_bitmap_lba, buf, sb.block_bitmap_sects);

    // 清空缓冲区
    memset(buf, 0, buf_size);
    // 第0个inode节点是根目录
    buf[0] |= 0x01;
    // 写入inode节点位图,这里直接写入的原因是一共就支持4096个节点,正好一个扇区
    ide_write(hd, sb.inode_bitmap_lba, buf, sb.inode_bitmap_sects);

    // 清空缓冲区
    memset(buf, 0, buf_size);
    // 将缓冲区指正改为指向inode节点
    struct inode* inode = (struct inode*)buf;
    // 修改第0个inode的节点大小
    inode->i_size = sb.dir_entry_size * 2;
    // 修改第0个inode的节点编号
    inode->i_no = 0;
    // 修改第0个inode的第一个扇区指针
    inode->i_sectors[0] = sb.data_start_lba;
    // 修改第0个inode的时间
    inode->ctime = get_time();
    inode->mtime = get_time();
    inode->atime = get_time();
    // 修改第0个inode的权限
    inode->privilege = 4;
    // 写入inode数组
    ide_write(hd, sb.inode_table_lba, buf, sb.inode_table_sects);

    // 写入根目录的两个目录项.和..
    // 清空缓冲区
    memset(buf, 0, buf_size);
    // 目录项指正指向buf
    struct dir_entry* p_de = (struct dir_entry*)buf;
    // 初始化当前目录
    memcpy(p_de->filename, ".", 1);
    p_de->i_no = 0;
    p_de->f_type = FT_DIRECTORY;
    p_de++;
    // 初始化当前目录的父目录
    memcpy(p_de->filename, "..", 2);
    p_de->i_no = 0;   // 根目录的父目录依然是根目录自己
    p_de->f_type = FT_DIRECTORY;
    // 将目录项写入硬盘
    ide_write(hd, sb.data_start_lba, buf, 1);

    // 完成了一个分区的全部初始化工作,释放buf
    printk("   root_dir_lba:0x%x\n", sb.data_start_lba);
    printk("%s format done\n", part->name);
    sys_free(buf);
}

整个初始化过程的前半部分就是超级块初始化,空闲块位图初始化,inode位图初始化,inode表初始化。也就是整个文件系统的信息的一个初始化过程。

后半部分就是根目录的初始化了,包括创建一个inode作为根目录,根目录中填充两个目录项,一个是 . 一个是 ..

2.6、文件系统初始化

虽然我们支持12个分区,但并不表示硬盘中存在12个分区,因此在进行格式化分区之前,先判断分区是否存在,这是通过分区的sec_cnt是否为0来判断的,之所以可以用此变量来判断,原因是分区part所在的硬盘作为全局数组channels的内嵌成员,全局变量会被初始化为0。我们在扫描分区表的时候会把分区的信息写到part中,因此只要分区不存在,分区part中任意成员的值都会是0,只是我们这里用sec_cnt来判断而已。

// sb_buf用来存储从硬盘上读入的超级块
struct super_block* sb_buf = (struct super_block*)sys_malloc(SECTOR_SIZE);
    for (uint8_t channel_no = 0; channel_no < channel_cnt; channel_no++) {
        for (uint8_t dev_no = 0; dev_no < 2; dev_no++) {
            // 跨过存放操作系统的主盘
            if (dev_no == 0) continue;
            // 拿到硬盘指针
            struct disk* hd = &channels[channel_no].devices[dev_no];
            // 拿到分区指针
            struct partition* part = hd->prim_parts;

            for (uint8_t part_idx = 0; part_idx < 12; part_idx++) {
                // 4个主分区处理完了就开始处理四个逻辑分区
                if (part_idx == 4) part = hd->logic_parts;
                // 如果分区存在
                if (part->sec_cnt != 0) {
                    // 初始化buf
                    memset(sb_buf, 0, SECTOR_SIZE);
                    // 读出分区的超级块,根据魔数是否正确来判断是否存在文件系统
                    ide_read(hd, part->start_lba + 1, sb_buf, 1);
                    // 如果魔数正确,那么说明分区已经正确初始化
                    if (sb_buf->magic == SUPER_BLOCK_MAGIC) {
                        printk("%s has filesystem\n", part->name);
                    }
                    // 魔数不正确,直接初始化分区
                    else {
                        printk("formatting %s`s partition %s......\n", hd->name, part->name);
                        partition_format(part);
                    }
                }
                // 下一个分区
                part++;
            }
        }
    }
    // 释放buf
    sys_free(sb_buf);
}

2.7、仿真

可以看到,五个分区初始化完成

image-20240402171935756

三、挂载分区

Windows 系统的分区盘符简单明了,C、D、E 盘直接摆在那,用不用都看得到。而Linux中却大不相同,分区使用的时候可以单独“拿”出来,不用的时候还可以“收”起来。

Linux 内核所在的分区是默认分区,自系统启动后就以该分区为默认分区,该分区的根目录是固定存在的,要想使用其他新分区的话,需要用mount命令手动把新的分区挂载到默认分区的某个目录下,这就是上面所说的“拿”出来。尽管其他分区都有自己的根目录,但是默认分区的根目录才是所有分区的父目录,因此挂载分区之后,整个路径树就像一串葡萄。分区不用的时候还可以通过umount命令卸载,这就是上面所说的“收”起来。

但是我们这里简单起见,不做那么复杂的操作,毕竟操作系统还不是在文件系统上呢。所以我们要实现的挂载很简单,直接选择待操作的分区。

/* 在分区链表中找到名为part_name的分区,并将其指针赋值给cur_part */
static bool mount_partition(struct list_elem* pelem, int arg) {
    // 获得分区名
    char* part_name = (char*)arg;
    // 获得链表遍历的分区
    struct partition* part = elem2entry(struct partition, part_tag, pelem);
    // 如果找到了链表中的分区
    if (!strcmp(part->name, part_name)) {
        // 默认情况下的分区改为当前分区
        cur_part = part;
        // 拿到当前分区的硬盘指针
        struct disk* hd = cur_part->my_disk;
        // sb_buf用来存储从硬盘上读入的超级块
        struct super_block* sb_buf = (struct super_block*)sys_malloc(SECTOR_SIZE);
        // 分区的超级块指针指向一块空白区域
        cur_part->sb = (struct super_block*)sys_malloc(sizeof(struct super_block));
        // 初始化buf
        memset(sb_buf, 0, SECTOR_SIZE);
        // 读入超级块
        ide_read(hd, cur_part->start_lba + 1, sb_buf, 1);
        // 把sb_buf中超级块的信息复制到分区的超级块sb中
        memcpy(cur_part->sb, sb_buf, sizeof(struct super_block));

        // 将块位图读入内存
        cur_part->block_bitmap.bits = (uint8_t*)sys_malloc(sb_buf->block_bitmap_sects * SECTOR_SIZE);
        if (cur_part->block_bitmap.bits == NULL) {
            PANIC("alloc memory failed!");
        }
        cur_part->block_bitmap.btmp_bytes_len = sb_buf->block_bitmap_sects * SECTOR_SIZE;
        ide_read(hd, sb_buf->block_bitmap_lba, cur_part->block_bitmap.bits, sb_buf->block_bitmap_sects);

        // 将inode位图读入内存
        cur_part->inode_bitmap.bits = (uint8_t*)sys_malloc(sb_buf->inode_bitmap_sects * SECTOR_SIZE);
        if (cur_part->inode_bitmap.bits == NULL) {
            PANIC("alloc memory failed!");
        }
        cur_part->inode_bitmap.btmp_bytes_len = sb_buf->inode_bitmap_sects * SECTOR_SIZE;
        ide_read(hd, sb_buf->inode_bitmap_lba, cur_part->inode_bitmap.bits, sb_buf->inode_bitmap_sects);

        // 初始化以打开inode链表
        list_init(&cur_part->open_inodes);

        // 打印挂载成功
        printk("mount %s done!\n", part->name);

        // 使list_traversal停止遍历
        return true;
    }
    // 使list_traversal继续遍历
    return false;
}
    // 确定默认的操作分区
    char default_part[8] = "sdb1";
    // 挂载分区
    list_traversal(&partition_list, mount_partition, (int)default_part);

3.1、仿真

image-20240402172623712

四、文件描述符

Linux中所有文件操作都基于文件描述符,为此咱们简短介绍一下这个概念。

4.1、文件描述符概念

文件是用inode来表示的,,用于描述文件的存储、权限等。但inode是操作系统为自己的文件系统准备的数据结构,它用于文件存储的管理,与用户关系不大,咱们要介绍的文件描述符才是面向用户的。

文件描述符即file descriptor,但凡叫“描述符”的数据结构都用于描述一个对象,文件描述符所描述的对象是文件的操作。为了搞清楚文件描述符的意义,咱们先看下它与inode的区别和联系。

还是拿Linux举例,读写文件的本质是:先通过文件的inode找到文件数据块的扇区地址,随后读写该扇区,从而实现了文件的读写。几乎所有的操作系统都允许一个进程同时、多次、打开同一个文件(并不关闭),同样该文件也可以被多个不同的进程同时打开。为实现文件任意位置的读写,执行读写操作时可以指定偏移量作为该文件内的起始地址,此偏移量相当于文件内的指针。也就是说,该文件每被打开一次,文件读写的偏移量都可以任意指定,即对同一个文件的多次读写都是各自操作各自的,任意一个文件操作的偏移量都不影响其他文件操作的偏移量。注意,这里所说的是“互不影响”是指文件内的“偏移量”,并不是文件内容,因为文件内容是共享的,对文件内容的修改必然会相互影响。另外,通常情况下对文件的操作都只是读写文件的一小部分数据,即使想读写整个文件的话,由于一般文件的体积都较大,而内存缓冲区较小,所以文件的读写并不是一次性从头到尾操作整个文件,往往是通过连续多次的小数据量读写完成的,下一次读写的位置必须以上一次的读写位置为起始,因此,文件系统需要把任意时刻的偏移量记录下来。

问题来了,偏移量应该记录在哪里呢?inode中肯定不记录这些与文件操作相关的数据,人家只把有限的空间记录与存储相关的信息。为解决这个问题,Linux提供了称为“文件结构”的数据结构(也称为file结构),专门用于记录与文件操作相关的信息,每次打开一个文件就会产生一个文件结构,多次打开该文件就为该文件生成多个文件结构,各自文件操作的偏移量分别记录在不同的文件结构中,从而实现了“即使同一个文件被同时多次打开,各自操作的偏移量也互不影响”的灵活性。

文件结构如下

/* 文件结构 */
struct file {
    uint32_t fd_pos;          // 记录当前文件操作的偏移地址,以0为起始,最大为文件大小-1
    uint32_t fd_flag;         // 权限      
    struct inode* fd_inode;   // 当前文件对应的inode节点指针
};

/* 标准输入输出描述符 */
enum std_fd {
    stdin_no,   // 0 标准输入
    stdout_no,  // 1 标准输出
    stderr_no   // 2 标准错误
};

/* 位图类型 */
enum bitmap_type {
    INODE_BITMAP,     // inode位图
    BLOCK_BITMAP	  // 空闲块位图
};

Linux把所有的“文件结构”组织到一起形成数组统一管理,该数组称为文件表。

先总结一下,inode用于描述文件存储相关信息,文件结构用于描述“文件打开”后,文件读写偏移量等信息。文件与inode一一对应,一个文件仅有一个inode,一个inode仅对应一个文件。一个文件可以被多次打开,因此一个inode可以有多个文件结构,多个文件结构可以对应同一个inode。

4.2、进程的文件描述符

文件描述符只是个整数,准确地说,它是PCB中文件描述符数组元素的下标,只不过此数字并不用来表示“数量”,而是用来表示“位置”,它是位于进程PCB中的文件描述符数组的元素的下标,而文件描述符数组元素中的信息又指向文件表中的某个文件结构。

在Linux中每个进程都有单独的、完全相同的一套文件描述符,因此它们与其他进程的文件描述符互不干涉,这些文件描述符被组织成文件描述符数组统一管理。

打开一个文件时会产生一个文件结构,这要是该任务同时打开N多个文件,文件表可就大了。通常情况下为避免文件表占用过大的内存空间,文件结构的数量必须是有限的,这就是进程可打开的最大文件数有限的原因(在Linux中可用ulimit命令来修改)。

文件描述符数组中的前3个都是标准的文件描述符,如文件描述符0表示标准输入,1表示标准输出,2表示标准错误。为什么文件描述符是数字,而不是像其他描述符那样,是个具有多个成员属性的复合数据结构?原因有两个。

(1)为了使各进程可打开的文件数是一样的,各进程必须有独立的、大小完全一样的一套文件描述符数组,而不能所有进程共享同一套文件描述符数组。

(2)文件结构中包含进程执行文件操作的偏移量,它属于与各个任务单独绑定的资源,因此最好放在PCB中管理。但当进程打开的文件数增多的时候,文件表(由文件结构组成的数组)占用的空间较大,而PCB占用的内存通常就是几个页框,Linux中的PCB也只是2页框大小,咱们PCB只占用1页框。

结合以上两个原因,通常情况下不会把“真正的、庞大的”文件表塞到狭小的PCB中,一般只要在PCB中建立个文件描述符数组就可以了,该数组成员不需要是真正的文件结构(至少包括3个成员)

Linux中的文件操作通常是先用open打开文件以获取该文件的文件描述符,然后将文件描述符作为参数传给read或write等函数对该文件进行读写操作。

其过程如下:

image-20240402174234212

现在梳理一下上图:某进程把文件描述符作为参数提交给文件系统时,文件系统用此文件描述符在该进程的PCB中的文件描述符数组中索引对应的元素,从该元素中获取对应的文件结构的下标,用该下标在文件表中索引相应的文件结构,从该文件结构中获取文件的inode,最终找到了文件的数据块。提示一下,若该inode在inode队列中不存在,此时会多一个处理过程:文件系统会从硬盘上将该inode加载到inode队列中,并使文件结构中的fd_inode指向它。

结束语

这是文件系统的第一部分,下一部分将介绍文件系统对应的文件操作的相关函数。

老规矩,本节的代码地址:https://github.com/lyajpunov/os

标签:文件,--,分区,文件系统,bitmap,part,sb,手写,inode
From: https://blog.csdn.net/weixin_43903639/article/details/137561835

相关文章

  • JS基础- 语句
            表达式和语句表达式是可以被求值的代码,JavaScript引擎会将其计算出一个结果。语句是一段可以执行的代码。        表达式和语句两者的区别:    表达式:因为表达式可被求值,所以它可以写在赋值语句的右侧         ......
  • C#使用PaddleOCR进行图片文字识别✨
    PaddlePaddle介绍✨PaddlePaddle(飞桨)是百度开发的深度学习平台,旨在为开发者提供全面、灵活的工具集,用于构建、训练和部署各种深度学习模型。它具有开放源代码、高度灵活性、可扩展性和分布式训练等特点。PaddlePaddle支持端到端的部署,可以将模型轻松应用于服务器、移动设备和边缘......
  • 【C语言】结构体structure
    【C语言】结构体structure:C语言可以自定义数据类型。结构体是其中一个自定义的数据类型。结构体类型是复杂的数据类型,将多个不同数据按一定功能进行整体封装,用一个名称来给结构体命名。可用typedef为结构体提供别名。关键字struct。结构体包括结构体名称、结构体成员(由成员类......
  • Windows系统下汇编环境的搭建
    Windows系统下汇编环境的搭建最近在学习assembly64时,需要对程序进行编写->生成汇编代码->调试->执行.本文聚焦于如果在Windows环境下,尽可能精简并且完整的构建一个汇编环境.基于Windows11,WSLUbuntu22.04,vscode,其他的系统/WSL发行版本.您可以以本文作为简......
  • 瑞_23种设计模式_备忘录模式(快照模式)
    文章目录1备忘录模式(MementoPattern)★1.1介绍1.2概述1.3备忘录模式的结构1.4备忘录模式的优缺点1.5备忘录模式的使用场景2案例一2.1需求2.2“白箱”备忘录模式2.3“黑箱”备忘录模式★★★3案例二3.1需求3.2代码实现......
  • nvm node版本管理器
    介绍在前端项目开发或安装依赖过程中,本地node版本与项目node版本不对应,会出现报错,手动安装删除切换node版本太过麻烦,这时候就需要一款node版本管理器win64nvm下载安装安装包下载地址:https://github.com/coreybutler/nvm-windows/releasesctrl+f全局查找【nvm-setup】,......
  • Composer安装与配置:简化PHP依赖管理的利器(包括加速镜像设置)
    在现代的PHP开发中,我们经常会使用许多第三方库和工具来构建强大的应用程序。然而,手动管理这些依赖项可能会变得复杂和耗时。为了解决这个问题,Composer应运而生。Composer是一个PHP的依赖管理工具,它可以帮助我们轻松地安装、更新和管理项目的依赖项。本文将介绍如何安装和配置C......
  • Vue中component lists rendered with v-for should have explicit keys异常
    在Vue.js中,当你在组件列表中使用v-for指令渲染多个组件时,每个组件元素都应当有一个明确的key属性。Vue.js引擎通过这个key来优化虚拟DOM的diff过程,提升页面更新效率,并确保状态保持一致。例如,如果你有这样的代码:Vue<ul><liv-for="iteminitems">{{......
  • 【spring】@Scope注解学习
    @Scope介绍@Scope注解是Spring框架中用于指定bean作用域的注解。在Spring中,一个bean的作用域定义了该bean的生命周期和创建bean实例的上下文。Spring提供了几种预定义的作用域,同时也支持自定义作用域。通过使用@Scope注解,开发者可以更精确地控制Spring容器如何创建和维护bean......
  • ROS笔记Day04----服务通信(实现排序--xxb第二次作业)
    一、服务通信简介服务通信是基于请求响应模式的,是一种应答机制。一个节点A向另一个节点B发送请求,B接收处理请求并产生响应结果返回给A。服务通信适用于实时性要求比较高的场景,例如设计一款自动搭讪机器人,每当摄像头检测到有搭讪目标出现,则摄像头这个节点就会向底盘......