首页 > 其他分享 >MIT6.S081

MIT6.S081

时间:2025-01-08 15:01:27浏览次数:1  
标签:文件 MIT6 S081 write ip inode 节点 block

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来提高搜索的效率,这里其实我们也可以多思考用一些快捷的方式来实现更快的搜索,服务好操作系统。

标签:文件,MIT6,S081,write,ip,inode,节点,block
From: https://www.cnblogs.com/haungmang/p/18659676

相关文章

  • MIT6.824----GFS
    GFS组织架构客户端向MASTER节点发出请求,Master节点中有两张表,一是文件名字和chunkhandle的映射,二是chunkhandle和服务器列表的对应。chunkhandle就是文件存储块,每一个文件存储块可能同时分布在若干服务器上,文件被分为若干个chunkhandle存储起来。每个chunk会以Linux......
  • 【MIT-OS6.S081作业1.4】Lab1-utilities find
    本文记录MIT-OS6.S081Lab1utilities的find函数的实现过程文章目录1.作业要求find(moderate)2.实现过程2.1代码实现1.作业要求find(moderate)WriteasimpleversionoftheUNIXfindprogram:findallthefilesinadirectorytreewithaspeci......
  • MIT 操作系统6.S081第一章
    1.1进程和内存fork父进程中fork返回子进程的PID子进程中fork返回0exitexit会让当前进程停止执行并释放资源(包括内存和打开的文件)通常:0表示成功1表示失败waitwait系统调用并返回当前进程已退出或杀死的进程PID,并将子进程的状态复制到wait的地址另外:如果子进......
  • MIT6.824 课程-GFS
    GFS原文:https://zhuanlan.zhihu.com/p/113161014搬运用于参考学习概述存储(Storage)是一个非常关键的抽象,用途广泛。GFS论文还提到了很多关于容错、备份和一致性的问题。GFS本身是Google内部一个很成功的实用系统,其关键点被很好的组织到一块发表成为了学术论文,从硬件到......
  • MIT6.824 课程-PrimaryBackupReplication
    PrimaryBackupReplication背景为实现可容错的服务器,主从备份是一种常用的解决方案:在开启了主动备份的系统中,备份服务器的状态需要在几乎任何时候都与主服务器保持一致,这样当主服务器失效后备份服务器才能立刻接管。实现主备间的状态同步主要包括以下两种方式:StateTransfer(......
  • MIT6.824 课程-MapReduce
    MapReduce:在大型集群上简化数据处理概要MapReduce是一种编程模型,它是一种用于处理和生成大型数据集的实现。用户通过指定一个用来处理键值对(Key/Value)的map函数来生成一个中间键值对集合。然后,再指定一个reduce函数,它用来合并所有的具有相同中间key的中间value。现实生活中......
  • [MIT6.5840]Lab3A leader election
    文章目录Part3A:leaderelection大致框架详细过程数据结构初始化选举计时器选举过程心跳机制LeaderRPC其他函数测试结果完整代码Part3A:leaderelection实验地址https://pdos.csail.mit.edu/6.824/labs/lab-raft.html论文地址https://pdos.csail.mit.ed......
  • 全网最最实用--基于Mac ARM 芯片实现操作系统MIT 6.S081-lab3
    文章目录实验三页表一、代码理解1.对于内存布局定义的理解2.对虚拟内存的理解3.对分配和释放物理内存的理解--删除或者分配物理内存为啥不需更改相应的页表?二、Printapagetable1.题目描述2.题目思考3.提交实验三、Akernelpagetableperprocess1.题目描述2.题目......
  • MIT6.824-2022 分布式系统课程实验笔记 Lab 2A Raft-领导者选举(leader election)--xu
    Lab2A:Raft文章目录Lab2A:RaftLab1:MapReduceLab2:Raft实验解读Lab2B:日志复制我的代码实现(附带详细代码注释)前言Part2A:[leaderelection](中等难度)提示错误:实现细节(包含对于方法的解释如有错误大佬勿喷)结构体GetState()获取节点任期和角色sendAllRequestVote()发起投票......
  • MIT6.824-2022 分布式系统课程实验笔记 Lab 2B Raft-日志复制(Log Replication)--xunznu
    Part2B:LogReplication日志复制(困难)文章目录Part2B:LogReplication日志复制(困难)Lab1:MapReduceLab2:Raft实验解读Lab2A:领导者选举leaderelection我的代码实现(附带详细代码注释)提示:实现细节:1、commitIndex和lastApplied2、nextIndex和matchIndex3、Co......