WangYu::Space

Study, think, create, and grow. Teach yourself and teach others.

MIT 6.828 - File system

分类:操作系统标签: 6.828创建时间:2024-02-20 00:00:00

本文为学习 MIT 6.828 - 文件系统章节时记录的笔记。

课程主页:https://pdos.csail.mit.edu/6.828/2023/index.html

文件系统

整个文件系统提供了 7 层抽象:

+--------------------+
|  file descriptor   |   将文件、socket、pipe 等资源抽象为文件,可以使用文件系统接口(open/read/write)来访问
+--------------------+
|     path name      |   提供层级目录
+--------------------+
|    directory       |   将文件目录抽象为特殊的文件,其内容是目录中的包含的文件
+--------------------+
|      inode         |   文件的内部表示
+--------------------+
|     logging        |   将对文件的操作封装为事务
+--------------------+
|   buffer cache     |   缓存磁盘上的块,支持对磁盘上数据的同步访问
+--------------------+
|     disk           |   读写数据块
+--------------------+

磁盘结构

磁盘上的数据按如下结构保存:

[ boot block | super block | log | inode blocks | free bit map | data blocks ]

boot block    - 用于保存启动引导代码
super block   - 保存文件系统的元信息
log           - 保存日志
inode blocks  - 存 inode
free bit map  - 用于记录数据块是否空闲,便于快速分配空闲块
data blocks   - 保存数据的块

超级块的作用是记录各个块的位置和大小,超级块固定存放在第一个块上,通过超级块可以找到其他块,下面是超级块的定义:

struct superblock {
  uint magic;        // Must be FSMAGIC
  uint size;         // Size of file system image (blocks)
  uint nblocks;      // Number of data blocks
  uint ninodes;      // Number of inodes.
  uint nlog;         // Number of log blocks
  uint logstart;     // Block number of first log block
  uint inodestart;   // Block number of first inode block
  uint bmapstart;    // Block number of first free map block
};

buffer cache

读写磁盘是比较慢的,为了加速读写,在内存中维护了一个缓存。每个缓存块,对应磁盘上的一个数据块,其定义如下:

struct buf {
  int valid;   // has data been read from disk?
  int disk;    // does disk "own" buf?
  uint dev;
  uint blockno;
  struct sleeplock lock;
  uint refcnt;
  struct buf *prev; // LRU cache list
  struct buf *next;
  uchar data[BSIZE];
};

缓存有很多好处,比如可以避免对同一个磁盘块的多次读取;对磁盘块的写入,可以先写入缓存,对磁盘同一位置的多次写入,可以只写最后的内容。

logging

往磁盘上写数据的时候,机器可能突然断电,这可能导致磁盘数据被破坏,为此希望对磁盘的写操作保持原子性,要么能写成功,要么写失败。为此文件系统中引入了事务,而日志则是实现事务采用的手段。引入日志后,数据写入分为 4 个步骤:

第一步:将待写入的 buf 写入 log 中

这些 buf 可能要写到 data 或者 inode 中,但先将其写入到 log 中:

第二步:写 log header

在 log header 中,描述了此前写入的 buf 真正应该写入的位置,第二步是将这些描述信息写入 log 中:

第三步:实际写数据

数据写入到 log 中之后,将数据写入实际要写入的位置:

第四步:清除日志

数据写完后,清除日志,清除日志只需要删除 log header 即可:

安装上面这四个步骤来写数据,就可以保证数据不会出错。下面分别描述,上述四个步骤执行时,任意时刻宕机的场景。

引入日志后,在宕机后,可以根据日志的内容执行数据的恢复。恢复过程很简单,就是读出 log header,然后根据 log header 的描述将 log 中的数据复制到指定的位置。

log header 的结构很简单:

struct logheader {
  int n;
  int block[LOGSIZE];
};

这里 n 表示有多少条日志,block 中则记录了每一条日志应该写入到那个 block 中。所有当系统启动后,只需要读出 logheader,并判断 n 是否为 0,如果不为 0,则读出后续的日志,并按照 block 的指示将这些日志写入对应的 block 中。

inode

inode 表示一个文件或目录,它存储在磁盘上的 inode blocks 中,每一个 block 中可以存储多个 inode。使用 inode 的编号,可以定位到某个 inode 存储在第几个 block 中,以及在该 block 中的偏移量。

下面是 inode 在磁盘上的表示:

// On-disk inode structure
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
};

其中包含文件类型、设备号、文件大小、链接数,文件的内容保存在 data block 中,在 inode 中包含一个数组指向 data block,表示该 inode 的数据:

在内存中 inode 使用如下结构表示:

struct inode {
  uint dev;           // Device number
  uint inum;          // Inode number
  int ref;            // Reference count
  struct sleeplock lock; // protects everything below here
  int valid;          // inode has been read from disk?

  short type;         // copy of disk inode
  short major;
  short minor;
  short nlink;
  uint size;
  uint addrs[NDIRECT+1];
};

inode 可以表示文件、目录、设备等,使用 type 字段做区分:

#define T_DIR     1   // Directory
#define T_FILE    2   // File
#define T_DEVICE  3   // Device

目录

文件使用 inode 表示,但是上层应用是按照路径来查找文件的。给定一个路径 /usr/bin/grep 需要能找到 grep 这个可执行文件对应的 inode,并从磁盘读取出其内容,这是如何实现的呢?

如果 inode 对应的是文件,inode 中 addrs 中记录的 data block 则为文件的内容。如果 inode 对应的是目录,该 inode 中存储的是目录中的文件条目,这些文件条目使用如下数据结构表示:

struct dirent {
  ushort inum;
  char name[DIRSIZ];
};

每个 inode 都有一个编号,根目录 / 对应的 inode 编号为 0,在读取文件 /usr/bin/grep 时,会先读取根目录的 inode,然后从中查找 usr 项,得到 usr 的 inode 编号,依次直到读取到 grep 文件。

文件描述符

文件描述符为一个打开的文件、socket、pipe 等资源,可以使用 readwriteopen 等来使用相同的操作处理这些资源。

在进程的结构体中有一个 ofile 字段,记录了当前进程已经打开的文件:

struct proc {
  // ...
  struct file *ofile[NOFILE];  // Open files
  // ...
};

file 的定义如下:

struct file {
  enum { FD_NONE, FD_PIPE, FD_INODE, FD_DEVICE } type;
  int ref; // reference count
  char readable;
  char writable;
  struct pipe *pipe; // FD_PIPE
  struct inode *ip;  // FD_INODE and FD_DEVICE
  uint off;          // FD_INODE
  short major;       // FD_DEVICE
};

在读写文件描述符的时候,会根据描述符查找到对应的 file 结构体,根据其类型,读写操作需要做工作是不同的。如果是文件,就是读写底层的 inode,如果是 socket 那就是操作网卡驱动来发送和接收数据。

评论 (评论内容仅博主可见,不会公开显示)