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:文件系统级别的逻辑存储单位
我们可以将磁盘看成一个巨大的数组:
如果我们用过diskgenius,就可以看到关于系统启动的相关分区,windos linux都有相关启动的分区文件。
-
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信息
- 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