自制操作系统 - 创建文件系统

创建文件系统

Posted by 王富杰 on Sunday, February 25, 2024

一、文件系统创建

为简单起见,我们使用 minix 第一版文件系统。首先我们创建两个虚拟磁盘,master.img 和 slave.img。每个磁盘创建一个主分区。创建文件系统指令如下:

sudo mkfs.minix -1 -n 14 /dev/loop0p1

这里我们是依赖已有操作系统的fdisk、 mkfs、mount、umount、losetup等命令进行磁盘的分区、格式化与挂载。

二、文件系统简介

一般常见的文件系统分为两种类型:文件分配表 (File Allocation Table FAT) 和 索引表。 minux 文件系统使用索引表结构。

2.1、块的概念

文件系统把硬件分成了许多块。一块是两个扇区。如下所示: 图片加载失败 文件就存储在这些块内,文件系统有很多文件,因此需要记录每个文件存在于哪些块中。当一个文件使用多个块时,就需要对这些块进行管理。因此产生了两种管理块的方案,第一个是文件分配表,它的逻辑是每一块最后有一个指针指向下一块。第二个是索引表,使用一个inode结构指示下一个块的记录的位置。

2.2、inode

inode用于记录一个文件存在于哪些块中,它的结构如下:

typedef struct inode_desc_t
{
    u16 mode;    // 文件类型和属性(rwx 位)
    u16 uid;     // 用户id(文件拥有者标识符)
    u32 size;    // 文件大小(字节数)
    u32 mtime;   // 修改时间戳 这个时间戳应该用 UTC 时间,不然有瑕疵
    u8 gid;      // 组id(文件拥有者所在的组)
    u8 nlinks;   // 链接数(多少个文件目录项指向该i 节点)
    u16 zone[9]; // 直接 (0-6)、间接(7)或双重间接 (8) 逻辑块号
} inode_desc_t;

文件有大有小,对于很小的文件,即小于1kb的文件,一个块用来存储就足够了。但是对于很大的文件,需要用到多个块来存储。它使用块的存储如下: 图片加载失败 如上所示,当一个文件大小超过6个块时,前6个是直接存储的块,第7个块时1级间接块,一块是1024字节,块的索引是u16,因此一级间接块可以扩充64个块,如果仍然不够,就继续使用二级间接块。

inode本身也存在于文件系统中,因此需要一些块来存储inode信息。因此文件系统需要分为两部分,一部分存储文件,一部分存储inode。因此需要用到一个超级块,来记录一个文件系统有多少inode块,多少文件块。

2.3、超级块

超级块用来记录一个文件系统,有多少个inode和逻辑块,它的结构如下:

typedef struct super_desc_t
{
    u16 inodes;        // 节点数
    u16 zones;         // 逻辑块数
    u16 imap_blocks;   // inode 节点位图所占用的数据块数
    u16 zmap_blocks;   // 逻辑块位图所占用的数据块数
    u16 firstdatazone; // 第一个数据逻辑块号
    u16 log_zone_size; // log2(每逻辑块数据块数)
    u32 max_size;      // 文件最大长度
    u16 magic;         // 文件系统魔数
} super_desc_t;

如上,超级块中使用了两个位图,分别记录inode和逻辑块哪些被占用了。超级块位于文件系统的第一块,因为第0块是主引导块。 图片加载失败

2.4、目录

如果inode 文件 是目录,那么文件块的内容就是下面这种数据结构。

// 文件目录项结构
typedef struct dentry_t
{
    u16 nr;        // i 节点
    char name[14]; // 文件名
} dentry_t;

三、文件系统的实现

我们这里使用代码来实现一下文件系统的读写,代码如下:

void super_init()
{
    device_t *device = device_find(DEV_IDE_PART, 0);  // 找到第一个主分区(这是分区的主引导扇区,并不是磁盘的主引导扇区)
    buffer_t *boot = bread(device->dev, 0);           // 读取主引导块,这是boot都是0,因为这不是硬盘的主引导扇区
    buffer_t *super = bread(device->dev, 1);          // 读取超级块
    super_desc_t *sb = (super_desc_t *)super->data;   // 超级块的data就是超级块的描述符 (这里是buffer的结构体)
    buffer_t *imap = bread(device->dev, 2);   // 读取inode 位图
    buffer_t *zmap = bread(device->dev, 2 + sb->imap_blocks);  // 读取块位图

    // 读取第一个 inode 块。也就是根目录
    buffer_t *buf1 = bread(device->dev, 2 + sb->imap_blocks + sb->zmap_blocks);
    inode_desc_t *inode = (inode_desc_t *)buf1->data;
    buffer_t *buf2 = bread(device->dev, inode->zone[0]);  // 读取第一个inode下的文件块,即根目录下的信息。

    dentry_t *dir = (dentry_t *)buf2->data;   // 这里是根目录下的文件信息
    inode_desc_t *helloi = NULL;
    while (dir->nr)
    {
        LOGK("inode %04d, name %s\n", dir->nr, dir->name);
        if (!strcmp(dir->name, "hello.txt"))   // 查找hello.txt 文件
        {
            helloi = &((inode_desc_t *) buf1->data)[dir->nr - 1];  // dir->nr就是文件节点的编号 获取该文件的inode,并修改名字写会磁盘
            strcpy(dir->name, "world.txt");
            buf2->dirty = true;
            bwrite(buf2);
        }
        dir++;
    }

    buffer_t *buf3 = bread(device->dev, helloi->zone[0]);  // 修改文件的内容
    LOGK("content %s", buf3->data);

    strcpy(buf3->data, "This is modified content!!!\n");
    buf3->dirty = true;
    bwrite(buf3);

    helloi->size = strlen(buf3->data);  // 更新文件的长度等信息
    buf1->dirty = true;
    bwrite(buf1);
}

接下来我们来调试一下这段代码,我们这里使用的ubuntu,因此磁盘挂载需要使用loop, 可以根据自身环境修改原始项目挂载的loop设备

sed -i 's/loop0p1/loop100p1/g; s/loop1/loop101/g; s/loop0/loop100/g; s/${USER}/root/g' utils/image.mk

这里说明一下为什么这么替换,因为笔者使用的是容器版的ubuntu镜像没有${USER}变量,且loop0和loop1设备被占用了。这也是在本系统文章中第一篇配置文件中做容器初始化创建loop100的原因。接下来我们进入调试看一下变量信息: 图片加载失败 如上为超级块的信息,inode为5056个,也就是最多5056个文件。第一个数据块在163块。inode位图占了一块,块位图占了两块。

四、根超级块

我们先说一下超级块和根超级块,每个分区如果被格式化成某种文件系统,那么它就会在分区里写入一个 超级块,以:每个分区都有自己的超级块。根超级块是内存里的概念,当操作系统挂载一个文件系统时,会把该分区的超级块从磁盘读出来,加载到内存中,形成一个内核的 super_block 数据结构。例如分区 1 上有 minix 的超级块 → 内存里变成 / 的根超级块。

创建超级块表,以及读取根超级块;这里使用已经创建好的文件系统,后续再自行实现文件系统的格式化等操作。我们这里首先添加两个结构,分为是inode和超级块在内存中的结构,上文中的结构是在磁盘上的结构:

typedef struct inode_t   // inode在内存中的结构
{
    inode_desc_t *desc;   // inode 描述符
    struct buffer_t *buf; // inode 描述符对应 buffer
    dev_t dev;            // 设备号
    idx_t nr;             // i 节点号
    u32 count;            // 引用计数
    time_t atime;         // 访问时间
    time_t ctime;         // 创建时间
    list_node_t node;     // 链表结点
    dev_t mount;          // 安装设备
} inode_t;

typedef struct super_block_t   // 超级块在内存中的结构
{
    super_desc_t *desc;              // 超级块描述符
    struct buffer_t *buf;            // 超级块描述符 buffer
    struct buffer_t *imaps[IMAP_NR]; // inode 位图缓冲
    struct buffer_t *zmaps[ZMAP_NR]; // 块位图缓冲
    dev_t dev;                       // 设备号
    list_t inode_list;               // 使用中 inode 链表
    inode_t *iroot;                  // 根目录 inode
    inode_t *imount;                 // 安装到的 inode
} super_block_t;

inode在内存中的结构这里我们暂时使用不到。 接下来我们看实现的代码:

#define SUPER_NR 16

static super_block_t super_table[SUPER_NR]; // 超级块表, 最多支持挂载16个文件系统。
static super_block_t *root;                 // 根文件系统超级块

static super_block_t *get_free_super()    // 从超级块表中查找一个空闲块
{
    for (size_t i = 0; i < SUPER_NR; i++)
    {
        super_block_t *sb = &super_table[i];
        if (sb->dev == EOF)
        {
            return sb;
        }
    }
    panic("no more super block!!!");
}

super_block_t *get_super(dev_t dev)     // 获得设备 dev 的超级块
{
    for (size_t i = 0; i < SUPER_NR; i++)
    {
        super_block_t *sb = &super_table[i];
        if (sb->dev == dev)
        {
            return sb;
        }
    }
    return NULL;
}

super_block_t *read_super(dev_t dev)  // 读设备 dev 的超级块
{
    super_block_t *sb = get_super(dev);
    if (sb)
    {
        return sb;
    }

    LOGK("Reading super block of device %d\n", dev);

    sb = get_free_super();   // 获得空闲超级块
    buffer_t *buf = bread(dev, 1);   // 读取超级块

    sb->buf = buf;
    sb->desc = (super_desc_t *)buf->data;
    sb->dev = dev;
    assert(sb->desc->magic == MINIX1_MAGIC);
    memset(sb->imaps, 0, sizeof(sb->imaps));
    memset(sb->zmaps, 0, sizeof(sb->zmaps));
    // 读取 inode 位图
    int idx = 2; // 块位图从第 2 块开始,第 0 块 引导块,第 1 块 超级块
    for (int i = 0; i < sb->desc->imap_blocks; i++)
    {
        assert(i < IMAP_NR);
        if ((sb->imaps[i] = bread(dev, idx)))
            idx++;
        else
            break;
    }

    for (int i = 0; i < sb->desc->zmap_blocks; i++)    // 读取超级块
    {
        assert(i < ZMAP_NR);
        if ((sb->zmaps[i] = bread(dev, idx)))
            idx++;
        else
            break;
    }

    return sb;
}

static void mount_root()     // 挂载根文件系统
{
    LOGK("Mount root file system...\n");
    device_t *device = device_find(DEV_IDE_PART, 0);   // 假设主硬盘第一个分区是根文件系统
    assert(device);
    root = read_super(device->dev);   // 读取文件系统超级块
}

void super_init()
{
    for (size_t i = 0; i < SUPER_NR; i++)
    {
        super_block_t *sb = &super_table[i];
        sb->dev = EOF;
        sb->desc = NULL;
        sb->buf = NULL;
        sb->iroot = NULL;
        sb->imount = NULL;
        list_init(&sb->inode_list);
    }

    mount_root();
}

超级块表初始化时,所有块的dev都初始化为EOF。再查找空闲块是只要判断dev是EOF那就是空闲块。mount_root用来获取主硬盘的第一个分区作文根文件系统,

「真诚赞赏,手留余香」

WangFuJie Blog

真诚赞赏,手留余香

使用微信扫描二维码完成支付