MIT6.S081

MIT6.S081笔记-14.0文件系统

一、初见

我们每天无时无刻都在接触文件系统,但是我们不知道文件系统什么原理,他是怎么实现的,就像亲密的朋友,一些东西总是觉得是自然而然的,没有剖析他的底层。

文件系统的特点:

  • 文件目录的层级结构
  • 随意更改文件名
  • 持久化

文件系统背后机制:

  • 文件系统对硬件的抽像

  • 系统奔溃的文件保护,数据是天价或者是无价的,要有完善的保护机制。

  • 磁盘是系统读写最慢的部件,所以尽量避免磁盘读写,通过并行化(多线程+异步)增强文件系统的性能。


二、实现

抽象的叙述往往不能很好让人理解表达的事物,所以我们一个例子来切入。

fd = open("user/peter");
write(fd,"abc", 3)
  • 接口的路径是可读的名字,用户可以指定

  • write 系统调用隐式含有偏置,会默认从未使用位置进行操作。

    此外,UNIX特殊的地方在于可以给一个文件指定多个名字,通过link进行调用,这里其实link 名称应该与文件的inode 有关系,所以link是对inode进行操作。

link("x/y", "x/z")

那么文件系统用怎样的结构实现数据持久化的API呢?

以inode为中心, 围绕他有两个数据维护。一个link count,一个openfd count,当没有名称指向且没有打开这个文件时这个inode 将会被释放。

文件最核心的数据结构 inode 以及 file descriptor,一个与存储交互,一个与进程交互。很多东西合起来看很困难,但是分层看起来会好很多。

name [fd]
inode (read and write)
icache—>同步
logging
buf cache
disk
  • 最底层磁盘提供了持久化设备

  • block cache 或者说 buf cache 将数据保存在内存中,避免磁盘的频繁读写。

  • 数据保护至关重要,logging是为了恢复文件产生的机制。

  • inode 是实现读写文件的媒介

  • 直观的文件操作**fd*


三、磁盘使用

设备

SSD 和HDD是现在最常用的存储设备,hdd延迟10ms, SSD 0.1ms ,所以对磁盘的读写非常耗时的。

我们现在有一个问题,文件系统是怎么操作磁盘的,就像我们的仓库一样怎么存放数据呢?

所以我要区分下这些单位:

Sector:硬盘级别最小的物理存储单位
Block:文件系统级别的逻辑存储单位

我们可以将磁盘看成一个巨大的数组:

image-20250107150040184

如果我们用过diskgenius,就可以看到关于系统启动的相关分区,windos linux都有相关启动的分区文件。

image-20250107150427117

  • block 0 用作boot sector来启动操作系统

  • block 1 表示 superblock ,描述了磁盘的布局, 块的大小,块的总数起始位置等等。

  • block 2 - block 32 日志区域存储待写入数据,node 的数量是有限的,需要有一个表,来表示他是否占用。

  • bitmap block 标识哪些数据块是否占用。

  • 最后data 区,保存文件和目录的内容。


四、inode

文件

首先我们看下inode 的代码:

struct dinode {
  short type;           // File type
  short major;          // Major device number (T_DEVICE only)
  short minor;          // Minor device number (T_DEVICE only)
  short nlink;          // Number of links to inode in file system
  uint size;            // Size of file (bytes)
  uint addrs[NDIRECT+1];   // Data block addresses
};

其中 uint addrs[NDIRECT+1] 是数据块的地址,前面的bitmap 只是表示是否占用,并没有存储block得实际地址,无法定位数据块位置, 而在XV6.0 中有12个 直接块, 256 个间接块,addrs[NDIRECT+1]; 存储的是间接块地址。

q1:为什么每个block存储256个block编号?

一个block的1024字节,一个地址信息是4字节,所以该block可以保存256个块地址。

q2:如果实现read系统调用,需要读取文件的第8000个字节,那么你该读取哪个block呢?从inode的数据结构中该如何计算呢?

计算block数: 取整8000/1024 = 7

block内的偏置:取余 8000%1024 =832

目录

struct dirent {
  ushort inum;
  char name[DIRSIZ];
};
  • 前2个字节包含了目录中文件或者子目录的inode编号,
  • 接下来的14个字节包含了文件或者子目录名。

实例:

假设我们要查找路径名“/y/x”,我们该怎么做呢?

  • / :首先是查找root inode 的查找,inode从block 32开始, 读到inode的内容
  • y: 扫描root inode包含的block找到y,目录y也有一个inode 假设 251
  • x:读取inode 251,扫描block找到 文件x以及x对应inode编号。

工作实例

修改XV6.0 makefs 文件 打印出磁盘的block信息

image-20250107165554911

  • boot block
  • super block
  • 30个log block
  • 13个inode block
  • 1个bitmap block

运行实例:

$ echo"hi">X
createthefile
write:33
write:33
write:46
write:32
write:33
write"hi" to file
write:45
write:595
write: 595
write:33
--write "\n" tofile
write:595
write:33

所以我们把写入文件过程分为三个阶段

  • 文件创建阶段
  • 将"hi" 写入文件
  • 将"\n"换行符写入文件

文件创建阶段其实就是分配 inode 阶段,我们继续回看inode 的数据结构:

struct dinode {
  short type;           // File type
  short major;          // Major device number (T_DEVICE only)
  short minor;          // Minor device number (T_DEVICE only)
  short nlink;          // Number of links to inode in file system
  uint size;            // Size of file (bytes)
  uint addrs[NDIRECT+1];   // Data block addresses
};

我们的inode表分布 在32-45

所以第一次"write 33" inode 来标识他是否空闲以及文件的类型。

第二次"write 33" 写入 inode 的其他内容,相当于补全信息。

write 46 46其实已经到了填写 bitmap 的时机了。

最后为什么又重新些了 block 32 32呢?

其实在inode table 里面排第一位的 root inode,所以最后的更新是新建文件之后的重新更新root 目录。

block 32 ........... block 45
root file inode
write"hi" to file
write:45    
write:595   "write h"
write: 595  "write i"
write:33
  • write 45 45之后都是bitmap 就是我们分配data block ,找到后将bitmap 对应的值置为1。

  • write 595 在data block 区域里面分别写入write 的单个字符内容

  • write 33 再次更改33 是更新文件元数据inode 中的size大小

五、inode 代码流程分析

XV 6.0包含了文件系统所有函数,其中inode的分配发生在sys_open函数中

uint64 sys_open(void) {
    char path[MAXPATH];  // 用于存储文件路径的缓冲区
    int fd, omode;       // 文件描述符和打开模式
    struct file *f;      // 文件结构体指针
    struct inode* ip;    // i节点(inode)结构体指针
    int n;

    // 从用户空间获取参数:文件路径和打开模式
    // argstr 用于复制字符串参数到内核空间
    // largint 用于读取整数参数
    if ((n = argstr(0, path, MAXPATH)) < 0 || largint(1, &omode) < 0)
        return -1;  // 如果任何参数无效,则返回-1表示错误

    begin_op();  // 开始一个新的文件操作序列,可能涉及锁等

    // 检查是否设置了 O_CREATE 标志,表示需要创建新文件
    if (omode & O_CREATE) {
        // 尝试创建指定路径的新文件
        ip = create(path, T_FILE, 0, 0);
        if (ip == 0) {  // 如果创建失败
            end_op();   // 结束操作序列
            return -1;  // 返回-1表示错误
        }
    } else {
        // 如果不是创建新文件,尝试查找已存在的文件
        if ((ip = namei(path)) == 0) {  // namei 函数根据路径查找对应的 i 节点
            end_op();  // 如果找不到文件,结束操作序列
            return -1;  // 返回-1表示错误
        }
    }

    // 这里应该有更多代码来完成文件打开过程,例如:
    // - 检查权限
    // - 分配文件描述符
    // - 初始化文件结构体
    // - 将文件结构体与 i 节点关联
    // - 更新文件状态标志
    // - 等等

    // 假设所有这些步骤都成功了,最后应该结束操作序列并返回文件描述符
    end_op();
    // 下面是假设的返回值,实际代码中应该有相应的逻辑来确定fd
    return fd;
}

上面创建的核心函数是create 函数

主要功能概括:

  • 解析路径:查找最后一个目录名称。
  • 检查重复:确目录中没有同名条目。
  • 分配 i 节点:为新文件或目录分配一个新的 i 节点。
  • 初始化属性:设置新 i 节点的类型、设备号等属性。
  • 创建链接:在父目录中创建指向新 i 节点的条目;如果是目录,还会创建 "."".." 条目。
  • 返回结果:成功时返回新的 i 节点指针,失败时返回错误。
static struct inode* create(char *path, short type, short major, short minor) {
    struct inode *ip, *dp;  // ip: 新创建的i节点指针;dp: 父目录的i节点指针
    char name[DIRSIZ];      // 用于存储文件名的缓冲区

    // 使用nameiparent解析路径,返回父目录的i节点,并将文件名存入name
    if ((dp = nameiparent(path, name)) == 0)
        return 0;  // 如果路径无效或父目录不存在,则返回空指针

    ilock(dp);  // 锁定父目录的i节点以防止并发修改

    // 检查是否已经存在同名文件或目录
    if ((ip = dirlookup(dp, name, 0)) != 0) {  // 查找是否存在同名条目
        iunlockput(dp);  // 解锁并释放父目录的引用
        ilock(ip);       // 锁定找到的i节点
        // 如果要创建的是普通文件,且找到的i节点也是普通文件或设备文件,则返回该i节点
        if (type == T_FILE && (ip->type == T_FILE || ip->type == T_DEVICE))
            return ip;
        iunlockput(ip);  // 否则解锁并释放找到的i节点
        return 0;  // 返回空指针表示错误
    }

    // 分配新的i节点
    if ((ip = ialloc(dp->dev, type)) == 0)  // 在指定设备上分配新的i节点
        panic("create: ialloc");  // 如果分配失败,触发内核恐慌

    ilock(ip);  // 锁定新分配的i节点

    // 设置新i节点的属性
    ip->major = major;  // 主设备号(对于设备文件)
    ip->minor = minor;  // 次设备号(对于设备文件)
    ip->nlink = 1;      // 初始化硬链接计数为1

    iupdate(ip);  // 更新i节点到磁盘

    if (type == T_DIR) {  // 如果创建的是目录
        dp->nlink++;  // 增加父目录的硬链接计数,因为新目录会有一个"."指向自己
        iupdate(dp);  // 更新父目录的i节点

        // 创建目录中的 "." 和 ".." 条目
        // 注意:这里没有增加ip->nlink,以避免循环引用计数
        if (dirlink(ip, ".", ip->inum) < 0 || dirlink(ip, "..", dp->inum) < 0)
            panic("create dots");  // 如果创建"."或".."失败,触发内核恐慌

        // 将新目录链接到父目录中
        if (dirlink(dp, name, ip->inum) < 0)
            panic("create: dirlink");  // 如果链接失败,触发内核恐慌
    }

    iunlockput(dp);  // 解锁并释放父目录的引用
    return ip;  // 返回新创建的i节点指针
}

那XV6.0 是怎么分配inode 呢? 这个实现在inode ialloc函数中

  • 遍历 i 节点表:在设备的 i 节点表中查找未使用的 i 节点。
  • 分配 i 节点:找到一个未使用的 i 节点后,初始化它,并将其标记为已分配。
  • 返回 i 节点:返回一个指向新分配且已引用但未锁定的 i 节点的指针。
// 在设备 dev 上分配一个 i 节点
// 将其标记为已分配,并返回一个未锁定但已引用的 i 节点
struct inode* ialloc(uint dev, short type) {
    int inum;  // i 节点编号
    struct buf *bp;  // 缓冲区指针
    struct dinode *dip;  // 磁盘 i 节点指针

    // 遍历所有可能的 i 节点编号
    for (inum = 1; inum < sb.ninodes; inum++) {
        // 读取包含 i 节点的块到缓冲区 bp
        bp = bread(dev, IBLOCK(inum, sb));
        
        // 计算当前 i 节点在缓冲区中的位置
        dip = (struct dinode*)bp->data + inum % IPB;

        // 检查 i 节点是否为空闲(type == 0 表示空闲)
        if (dip->type == 0) {  // 找到一个空闲的 i 节点
            // 初始化新的 i 节点
            memset(dip, 0, sizeof(*dip));  // 清零整个 i 节点结构
            log_write(bp);  // 将缓冲区内容写入日志(用于崩溃恢复)

            // 设置 i 节点类型
            dip->type = type;
            
            // 释放缓冲区,但不写回磁盘(因为已经通过日志记录)
            brelse(bp);

            // 返回新分配的 i 节点(未锁定但已引用)
            return iget(dev, inum);
        }

        // 如果当前 i 节点不是空闲的,释放缓冲区
        brelse(bp);
    }

    // 如果没有找到空闲的 i 节点,触发内核恐慌
    panic("ialloc: no inodes");
}

这里其实是一个线性查找,有很大的性能问题,现代操作系统通常会利用哈希、b tree来提高搜索的效率,这里其实我们也可以多思考用一些快捷的方式来实现更快的搜索,服务好操作系统。

posted @   深度学习渣  阅读(8)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示