Java 模拟二级文件系统 中

简介: 本系列将记述使用 Java 实现一个简单的二级文件系统的过程。本项目意图效仿 Linux 的文件管理,但是学的又没那么像,仅仅是一些皮毛。因此使用类似 Inode 的东西记录文件在磁盘上的信息。

Inode展开目录

每个 Inode 的描述如下:

• package info.skyblond.os.exp.experiment3.model;
• import info.skyblond.os.exp.experiment3.bytes.WrappedGenericType;
• import info.skyblond.os.exp.experiment3.bytes.array.WrappedString;
• import info.skyblond.os.exp.experiment3.bytes.primitive.WrappedLong;
• import info.skyblond.os.exp.experiment3.bytes.primitive.WrappedShort;
• import info.skyblond.os.exp.utils.CommonUtils;
• import java.util.ArrayList;
• import java.util.Arrays;
• import java.util.List;
• public class Inode extends WrappedGenericType {
• /**
•      * 盘块大小:4KB
•      */
• public static final int BLOCK_SIZE = 4096;
• /** 
•      * 块号索引,都为直接索引。
•      * 用16位Short表示盘块号,最大可表示64K个盘块,文件系统最大支持256MiB容量。
•      * 该索引方式支持的文件容量:10 * 4KB  = 40KiB
•      */
• private final WrappedShort[] blockNumbers = new WrappedShort[10];
• /**
•      * 文件所有者名称
•      */
• private final WrappedString owner = new WrappedString(User.USER_NAME_LENGTH);
• /**
•      * 文件大小(字节为单位)
•      */
• private final WrappedLong size = new WrappedLong();
•     Inode() {
• for (int i = 0; i < this.blockNumbers.length; i++) {
• this.blockNumbers[i] = new WrappedShort();
• this.parameters.add(this.blockNumbers[i]);
•         }
• this.parameters.addAll(Arrays.asList(this.owner, this.size));
•     }
• /**
•      * 增加一个块
•      */
• public boolean appendBlock(short blockNum) {
• for (WrappedShort blockNumber : this.blockNumbers) {
• if (blockNumber.getValue() == -1) {
• // 直接写入新的块号
•                 blockNumber.setValue(blockNum);
• return true;
•             }
•         }
• return false;
•     }
• /**
•      * 释放最后一个占用的块,返回块号
•      */
• public short releaseLastBlock() {
• if (this.blockNumbers[0].getValue() == -1) {
• // 如果没分配块,返回-1
• return -1;
•         }
• for (int i = 0; i < this.blockNumbers.length - 1; i++) {
• if (this.blockNumbers[i + 1].getValue() == -1) {
• // 如果下一个是未占用块,则当前是最后一个未使用的块
• // 释放
• var temp = this.blockNumbers[i].getValue();
• this.blockNumbers[i].setValue((short) -1);
• return temp;
•             }
•         }
• // 全部都在占用,则释放最后一个块
• var temp = this.blockNumbers[this.blockNumbers.length - 1].getValue();
• this.blockNumbers[this.blockNumbers.length - 1].setValue((short) -1);
• return temp;
•     }
• /**
•      * 获取当前已经分配的块
•      */
• public List<Short> getBlocks() {
• var result = new ArrayList<Short>();
• for (WrappedShort blockNumber : this.blockNumbers) {
• if (blockNumber.getValue() != -1) {
• // 直接写入新的块号
•                 result.add(blockNumber.getValue());
•             }
•         }
• return result;
•     }
• /**
•      * 根据要访问的偏移量返回其所在的盘块号
•      */
• public short getBlockNumber(long offset) {
•         CommonUtils.require(offset <= this.size.getValue(), "Offset bigger than file size.");
• // 原始盘块数,如果小于5则为直接索引,直接返回对应盘块号
• var rawBlock = offset / BLOCK_SIZE;
• return this.blockNumbers[(int) rawBlock].getValue();
•     }
• /**
•      * 将所有索引项写为-1,重置
•      */
• public void resetBlockNumber() {
• for (WrappedShort blockNumber : this.blockNumbers) {
•             blockNumber.setValue((short) -1);
•         }
• this.size.setValue((long) 0);
•     }
• public String getOwner() {
• return this.owner.getValue();
•     }
• public long getSize() {
• return this.size.getValue();
•     }
• public void setOwner(String newOwner) {
• this.owner.setValue(newOwner);
•     }
• public void setSize(long newSize) {
• this.size.setValue(newSize);
•     }
• @Override
• public String toString() {
• return "Inode{" +
• "blockNumbers=" + Arrays.toString(this.blockNumbers) +
• ", owner=" + this.owner +
• ", size=" + this.size +
• '}';
•     }
• }

每个 Inode 有 10 个直接索引,内容为盘块号,存储在 blockNumbers 中,盘块上就是文件的数据。本来之前还想做多级索引,后来觉得做起来太麻烦,又砍了。多级索引原理上就是重复几遍读盘块找索引的步骤,但是细节处理上还是挺费时间的,因此就没有做。

每个 Inode 还存储文件节点所有者的用户名(owner),这个其实可有可无,一开始想加上文件的共享,其他用户可以通过 Inode 编号创建一个硬链接指向其他用户的文件,然后再加上读写权限标志什么的,这个功能后来也砍了。如果要加上的话也好办:所有者拥有全部读写权限,非所有者有三种权限:无权限、只读和读写(如果想再偷懒的话,默认非所有者只读就行了)。之后再在创建文件的时候询问创建普通文件还是硬链接,读写的时候检查权限即可。后文的模拟器是单道系统,同一时间只能登陆一个用户,操作一个文件,因此不必考虑加锁什么的。如果要考虑加锁,打开文件的时候检查 Inode 唯一性即可(逻辑上就是独占锁)。

最后每个 Inode 还要记录文件的实际大小 size,因为占用一个盘块不代表文件一定要用完这个盘块。除此之外这个类还定义了 BLOCK_SIZE,即一个盘块(簇)大小为 4KB。

对于构造函数,为了保证 Inode 只能在 InodeTable 中被创建,因此让它的构造函数只在包内可见,到了模拟器(简单操作系统)那里就不能直接创建 Inode 了。原因后面再说。在构造函数里可以看到我们用到了上文中的 WrappedGenericType,构造时维护 this.parameters 即可让我们在上文中编写的基类自动处理成员变量与字节数组之间的转换,而无需我们再操心了。

值得一提的是我们需要修改 IDEA 默认生成的 getter 和 setter,使之利用 WrappedBaseType 的 setter 和 getter,避免外界代码能够直接操作对象,而只能操作对象里面包含的值。

对于 Inode 的常用操作也提供了对应的成员方法。

appendBlock 会向索引中追加一个块,-1 表示该索引未被使用(毕竟盘块号最小是 0),代码将查找第一个为 -1 的索引,并将其修改为新追加的盘块号。如果找不到未使用的索引,说明文件大小已经达到极限,将返回 false 告诉调用者操作失败。

除了追加一个块,还要能够释放块:releaseLastBlock 方法将释放最后一个块,并返回被释放的块号。如果当前没有使用中的块(0 号索引值为 - 1)则返回 - 1,否则将查找下一个索引为 - 1 的块,即是最后一个使用中的块,取值后覆盖为 - 1,返回其值。

除了释放最后一个块,在删除文件时需要撤销整个 Inode,如果这时候再一个一个释放的话,会很浪费时间,因此提供查询所有占用的块的功能,通过操作系统直接释放占用的块并撤销 Inode。getBlocks 将返回所有使用中的块号。

对于访问的需求,最直接的就是将文件内的偏移量转换为对应的块号:getBlockNumber 根据传入的文件内偏移量,查询其对应的索引,并返回占用的盘块号。

最后当需要回收 Inode 时,resetBlockNumber 方法可以将索引全部置为 -1。

由 Inode 作为基本单位,多个 Inode 集合便成为一个 InodeTable。

InodeTable展开目录

为了避免 Inode 对象的不一致性(即多个 Inode 对象对应磁盘中同一个 Inode 数据),所有创建 Inode 实例的行为都必须在 InodeTable 中进行,因此 Inode 的构造函数不能为 public,只能包内可见。同时使用单例模式来避免 InodeTable 的不一致性(磁盘上就一个 InodeTable):

• package info.skyblond.os.exp.experiment3.model;
• import info.skyblond.os.exp.experiment3.bytes.WrappedGenericType;
• import info.skyblond.os.exp.experiment3.bytes.array.WrappedBitArray;
• import info.skyblond.os.exp.utils.CommonUtils;
• import java.util.Arrays;
• import java.util.Objects;
• /**
•  * 用户表
•  */
• public class InodeTable extends WrappedGenericType {
• /**
•      * Inode数量:32768,最多可以有这么多个文件/目录。
•      */
• public static final int INODE_NUM = 32768;
• private final int length;
• private final WrappedBitArray allocationMap;
• private final Inode[] inodes;
• // 使用单例模式
• private static InodeTable instance;
• public static synchronized InodeTable getInstance() {
• if (instance == null) {
•             instance = new InodeTable(INODE_NUM);
•         }
• return instance;
•     }
• /**
•      * 定长索引表
•      */
• private InodeTable(int length) {
•         CommonUtils.require(length > 0, "Length must bigger than 0.");
• this.length = length;
• this.allocationMap = new WrappedBitArray(length);
• this.inodes = new Inode[length];
• for (int i = 0; i < this.length; i++) {
• this.allocationMap.set(i, false);
• this.inodes[i] = new Inode();
• this.parameters.add(this.inodes[i]);
•         }
• this.parameters.add(this.allocationMap);
•     }
• /**
•      * 申请新inode,需要调用者更新Inode表
•      */
• public int requestNewInode(String owner) {
• // 获取空闲的Inode节点索引
• var iIndex = this.nextAvailableIndex();
• if (iIndex < 0) {
• return -1; // 无可用Inode时返回-1表示创建失败
•         }
• // 获取节点
• var inode = this.get(iIndex);
• // 清空
•         inode.resetBlockNumber();
• // 设置属主
•         inode.setOwner(owner);
• // 设置占用
• this.set(iIndex);
• return iIndex;
•     }
• /**
•      * 设置Inode已分配,修改和更新需要使用{@link #get(int)}获取Inode并修改
•      */
• public void set(int inodeIndex) {
• this.allocationMap.set(inodeIndex, true);
•     }
• /**
•      * 获取下一个未分配的Inode索引号,-1表示全满了
•      */
• public int nextAvailableIndex() {
• for (int i = 0; i < this.length; i++) {
• if (!this.allocationMap.get(i)) {
• return i;
•             }
•         }
• return -1;
•     }
• /**
•      * 搜索Inode,如果需要对inode进行更改,同样使用
•      */
• public Inode get(int inodeIndex) {
• return this.inodes[inodeIndex];
•     }
• /**
•      * 删除Inode,该函数并不会删除已经分配给该Inode的盘块,需要调用者使用bitmap重置盘块分配情况
•      */
• public void delete(int inodeIndex) {
• this.allocationMap.set(inodeIndex, false);
•     }
• public int getLength() {
• return this.length;
•     }
• @Override
• public boolean equals(Object o) {
• if (this == o) {
• return true;
•         }
• if (o == null || this.getClass() != o.getClass()) {
• return false;
•         }
•         InodeTable that = (InodeTable) o;
• return this.length == that.length &&
• this.allocationMap.equals(that.allocationMap) &&
•                 Arrays.equals(this.inodes, that.inodes);
•     }
• @Override
• public int hashCode() {
• int result = Objects.hash(this.length, this.allocationMap);
•         result = 31 * result + Arrays.hashCode(this.inodes);
• return result;
•     }
• @Override
• public String toString() {
• return "UserIndexTable{" +
• "length=" + this.length +
• ", allocationMap=" + this.allocationMap +
• ", inodes=" + Arrays.toString(this.inodes) +
• '}';
•     }
• }

InodeTable 类规定了该文件系统最大能够支持的 Inode 数量:INODE_NUM,该值将指导 InodeTable 初始化多少个 Inode 的位置。而为了标识哪一个 Inode 被使用,哪一个是空闲的,利用对应长度的 WrappedBitArray 追踪使用情况(allocationMap),而最终的 Inode 数据则存储在 inodes,一个 Inode 数组。

对象数组在初始化时需要一一赋值,否则后续操作会产生空指针异常(对象引用默认为 null)。这些初始化的操作都在构造函数中进行了,单例模式保证只有一个对象会被创建。对于常用的操作也有对应的方法提供:

nextAvailableIndex 查询第一个未被使用的 Inode 节点(即 allocationMap 中值为 false 的)的索引,如果没有则返回 - 1。set、get 和 delete 分别提供了基础的操作:set 将 allocationMap 中对应给定索引的值置为 true,表示已经分配;get 则返回 inodes 中对应索引的对象,无论其是否已被分配;而 delete 则与 set 相反,将 allocationMap 中对应给定索引的值置为 false,表示未在使用。有了这四个基本操作,可以在这基础上增加一些额外操作,而避免每个常用操作直接对底层数据修改,产生不一致性。

requestNewInode 将分配一个全新的 Inode 节点给指定用户。首先要查找一个空闲的 Inode,将其清空并设定所有者,还要调用 set 方法告知 allocationMap 已经占用。如果上述操作全都成功,返回新分配的 Inode 的索引号,如果失败,返回 - 1。

User 和 UserTable展开目录

文件是磁盘上最基础的表现形式。现在 Inode 有了,我们来说一说用户。上一篇文章提到实验指导书还要求系统有对用户的基本操作,因此用户的数据也要在磁盘上管理。

User展开目录

每一个用户的定义如下:

• package info.skyblond.os.exp.experiment3.model;
• import info.skyblond.os.exp.experiment3.bytes.WrappedGenericType;
• import info.skyblond.os.exp.experiment3.bytes.array.WrappedString;
• import info.skyblond.os.exp.experiment3.bytes.primitive.WrappedInteger;
• import info.skyblond.os.exp.utils.CommonUtils;
• import java.nio.charset.StandardCharsets;
• import java.util.Arrays;
• import java.util.Objects;
• public class User extends WrappedGenericType {
• /**
•      * 用户名最大长度
•      */
• public static final int USER_NAME_LENGTH = 10;
• /**
•      * 密码最大长度
•      */
• public static final int USER_PASSWORD_LENGTH = 20;
• /**
•      * 用户名
•      */
• private final WrappedString username = new WrappedString(USER_NAME_LENGTH);
• /**
•      * 密码
•      */
• private final WrappedString password = new WrappedString(USER_PASSWORD_LENGTH);
• /**
•      * home目录对应的inode索引
•      */
• private final WrappedInteger homeInodeIndex = new WrappedInteger();
•     User() {
• this.parameters.addAll(Arrays.asList(
• this.username, this.password, this.homeInodeIndex
•         ));
•     }
• /**
•      * 获取用户名
•      */
• public String getUsername() {
• return this.username.getValue();
•     }
• /**
•      * 更新用户名。使用时需要注意修改用户名对于索引表的影响
•      */
• public void setUsername(String username) {
•         CommonUtils.require(username.getBytes(StandardCharsets.UTF_8).length <= USER_NAME_LENGTH, "User name reached max length.");
• this.username.setValue(username);
•     }
• public String getPassword() {
• return this.password.getValue();
•     }
• public void setPassword(String password) {
•         CommonUtils.require(password.getBytes(StandardCharsets.UTF_8).length <= USER_NAME_LENGTH, "Password reached max length.");
• this.password.setValue(password);
•     }
• public int getHomeInodeIndex() {
• return this.homeInodeIndex.getValue();
•     }
• public void setHomeInodeIndex(int value) {
• this.homeInodeIndex.setValue(value);
•     }
• /**
•      * 用户名相同即为一个用户
•      */
• @Override
• public boolean equals(Object o) {
• if (this == o) {
• return true;
•         }
• if (o == null || this.getClass() != o.getClass()) {
• return false;
•         }
•         User user = (User) o;
• return this.username.equals(user.username);
•     }
• @Override
• public int hashCode() {
• return Objects.hash(this.username);
•     }
• @Override
• public String toString() {
• return "User{" +
• "username=" + this.username +
• ", password=" + this.password +
• ", homeInodeAddr=" + this.homeInodeIndex +
• '}';
•     }
• }

用户描述起来就比 Inode 简单许多:首先规定用户名和密码的最大长度,这些参数将传给 WrappedString,它将在违反约定的时候抛出异常,因此我们不必显示的处理违约情况(但实践中还是建议检查用户输入的合规性,否则动不动就崩溃,最终用户会骂街的)。

每个用户拥有一个 username 和 password,并且拥有一个 homeInodeIndex 来记录该用户的目录文件对应的 Inode 的索引。目录文件也是普通的文件,它的内容将在稍后介绍。

在 getter 和 setter 上和 Inode 一样,必须保证外界代码操作的是 Wrapped 的核心值,而不是 Wrapped 对象。

在 equal 方法上,只要用户名相同就认为是同一个用户。

有了 User,我们就可以构建 UserTable 了。

UserTable展开目录

UserTable 同样使用单例模式:

• package info.skyblond.os.exp.experiment3.model;
• import info.skyblond.os.exp.experiment3.bytes.WrappedGenericType;
• import info.skyblond.os.exp.experiment3.bytes.array.WrappedBitArray;
• import info.skyblond.os.exp.utils.CommonUtils;
• import java.util.ArrayList;
• import java.util.Arrays;
• import java.util.List;
• import java.util.Objects;
• /**
•  * 用户表
•  */
• public class UserTable extends WrappedGenericType {
• /**
•      * 最大用户数量:16个
•      */
• public static final int USER_NUM = 16;
• private final int length;
• private final WrappedBitArray allocationMap;
• private final User[] users;
• // 使用单例模式
• private static UserTable instance;
• public static synchronized UserTable getInstance() {
• if (instance == null) {
•             instance = new UserTable(USER_NUM);
•         }
• return instance;
•     }
• /**
•      * 定长索引表
•      */
• private UserTable(int length) {
•         CommonUtils.require(length > 0, "Length must bigger than 0.");
• this.length = length;
• this.allocationMap = new WrappedBitArray(length);
• this.users = new User[length];
• for (int i = 0; i < this.length; i++) {
• this.allocationMap.set(i, false);
• this.users[i] = new User();
• this.parameters.add(this.users[i]);
•         }
• this.parameters.add(this.allocationMap);
•     }
• /**
•      * 列出所有可用的用户名
•      */
• public List<String> listUsername() {
• var result = new ArrayList<String>();
• for (int i = 0; i < this.length; i++) {
• if (this.allocationMap.get(i)) {
•                 result.add(this.users[i].getUsername());
•             }
•         }
• return result;
•     }
• /**
•      * 新增用户
•      */
• public boolean set(String username, String password, int homeInodeIndex) {
•         CommonUtils.require(homeInodeIndex >= 0, "homeInodeIndex must bigger than 0. Negative addr is meaningless.");
• // 查看是否已有索引,有则更新
• for (int i = 0; i < this.length; i++) {
• if (this.allocationMap.get(i)) {
• // 已分配,搜索
• if (this.users[i].getUsername().equals(username)) {
• this.users[i].setPassword(password);
• this.users[i].setHomeInodeIndex(homeInodeIndex);
• return true;
•                 }
•             }
•         }
• // 寻找空位,没有直接返回false
• int emptyIndex = 0;
• for (; emptyIndex < this.length; emptyIndex++) {
• if (!this.allocationMap.get(emptyIndex)) {
• break;
•             }
•         }
• if (emptyIndex >= this.length) {
• return false;
•         }
• // 修改空位的内容以记录值
• this.users[emptyIndex].setUsername(username);
• this.users[emptyIndex].setPassword(password);
• this.users[emptyIndex].setHomeInodeIndex(homeInodeIndex);
• this.allocationMap.set(emptyIndex, true);
• return true;
•     }
• /**
•      * 搜索用户
•      */
• public User get(String username) {
• for (int i = 0; i < this.length; i++) {
• if (this.allocationMap.get(i)) {
• // 已分配,搜索
• if (this.users[i].getUsername().equals(username)) {
• // Key匹配
• return this.users[i];
•                 }
•             }
•         }
• return null;
•     }
• /**
•      * 删除用户
•      */
• public boolean delete(String username) {
• // 查看是否已有索引,有再删除
• for (int i = 0; i < this.length; i++) {
• if (this.allocationMap.get(i)) {
• if (this.users[i].getUsername().equals(username)) {
• // 直接标记释放
• this.allocationMap.set(i, false);
• return true;
•                 }
•             }
•         }
• return false;
•     }
• public int getLength() {
• return this.length;
•     }
• @Override
• public boolean equals(Object o) {
• if (this == o) {
• return true;
•         }
• if (o == null || this.getClass() != o.getClass()) {
• return false;
•         }
•         UserTable that = (UserTable) o;
• return this.length == that.length &&
• this.allocationMap.equals(that.allocationMap) &&
•                 Arrays.equals(this.users, that.users);
•     }
• @Override
• public int hashCode() {
• int result = Objects.hash(this.length, this.allocationMap);
•         result = 31 * result + Arrays.hashCode(this.users);
• return result;
•     }
• @Override
• public String toString() {
• return "UserIndexTable{" +
• "length=" + this.length +
• ", allocationMap=" + this.allocationMap +
• ", users=" + Arrays.toString(this.users) +
• '}';
•     }
• }

初始化与 InodeTable 类似,全部在构造函数中执行。

常用操作有 listUsername,该方法将列出所有已知的用户名。

set 则根据传入的用户名、密码和目录文件的 Inode 索引创建或更新一个用户。首先所有已分配的用户,如果找到同名的用户,则更新它的密码和目录文件 Inode 索引,否则查找空闲项,设定对应内容及 allocationMap 后返回。如果一切顺利,返回 true,否则返回 false。这里面唯一能够返回 false 的就是用户数量达到上限,UserTable 满了,找不到可用空闲项。

get 则根据用户名搜索用户。如果找到了则返回 User 对象,找不到则返回一个 null。

delete 根据用户名删除用户。首先查找用户,找到了直接在 allocationMap 中标记为未使用即可,最后返回 true。如果找不到用户,将返回 false。

BitMap展开目录

处理完了 InodeTable 和 UserTable,我们还剩下一个 BitMap 和 SystemInfo。BitMap 定义如下:

• package info.skyblond.os.exp.experiment3.model;
• import info.skyblond.os.exp.experiment3.bytes.WrappedGenericType;
• import info.skyblond.os.exp.experiment3.bytes.array.WrappedBitArray;
• import info.skyblond.os.exp.utils.CommonUtils;
• public class BitMap extends WrappedGenericType {
• /**
•      * 盘块数量:32769个,总文件大小:128MiB
•      */
• public static final int BLOCK_NUM = 32768;
• private final int length;
• private final WrappedBitArray internalBitMap;
• // 使用单例模式
• private static BitMap instance;
• public static synchronized BitMap getInstance() {
• if (instance == null) {
•             instance = new BitMap(BLOCK_NUM);
•         }
• return instance;
•     }
• /**
•      * 定长索引表
•      */
• private BitMap(int length) {
•         CommonUtils.require(length > 0, "Length must bigger than 0.");
• this.length = length;
• this.internalBitMap = new WrappedBitArray(length);
• this.parameters.add(this.internalBitMap);
•     }
• /**
•      * 设置使用
•      */
• public void set(int index) {
• this.internalBitMap.set(index, true);
•     }
• /**
•      * 清除
•      */
• public void clear(int index) {
• this.internalBitMap.set(index, false);
•     }
• /**
•      * 查询
•      */
• public boolean get(int index) {
• return this.internalBitMap.get(index);
•     }
• /**
•      * 获取下一个可用块号
•      */
• public int nextAvailable() {
• for (int i = 0; i < this.length; i++) {
• if (!this.internalBitMap.get(i)) {
• return i;
•             }
•         }
• return -1;
•     }
• public int getLength() {
• return this.length;
•     }
• @Override
• public String toString() {
• return "BitMap{" +
• "length=" + this.length +
• ", internalBitMap=" + this.internalBitMap +
• '}';
•     }
• }

这里定义了一个 BLOCK_NUM 来指定磁盘有多少个盘块。BitMap 正如其名:使用位图法管理盘块,一个 bit 对应一个盘块的使用情况。其核心就是对 WrappedBitArray(internalBitMap)的简单包装。由于其在磁盘上的唯一性,BitMap 同样使用单例模式。

在 BitMap 中,我们将 WrappedBitArray 的 set 细分为两个方法:set 负责置位为 true,clear 表示置位为 false。get 则不变。

除此之外还额外提供了 nextAvailable 方法,用于获取一个空闲盘块的盘块号,如果找不到空闲盘块,那么就返回一个 - 1。

SystemInfo展开目录

三大件(User 表、Inode 表和 BitMap)都齐了,下面就可以说 SystemInfo 了。实际上 SystemInfo 非常简单:

• package info.skyblond.os.exp.experiment3.model;
• import info.skyblond.os.exp.experiment3.bytes.WrappedGenericType;
• import info.skyblond.os.exp.experiment3.bytes.primitive.WrappedLong;
• import java.util.Arrays;
• import java.util.Objects;
• /**
•  * 系统信息只读不写,因此不做单例模式
•  */
• public class SystemInfo extends WrappedGenericType {
• /**
•      * 用户索引表的起始地址
•      */
• private final WrappedLong userIndexTableAddr = new WrappedLong();
• /**
•      * i节点索引表的起始地址
•      */
• private final WrappedLong inodeIndexTableAddr = new WrappedLong();
• /**
•      * 位图的起始地址
•      */
• private final WrappedLong bitmapAddr = new WrappedLong();
• /**
•      * 实例化,内容全0
•      */
• public SystemInfo() {
• this.parameters.addAll(Arrays.asList(
• this.userIndexTableAddr, this.inodeIndexTableAddr, this.bitmapAddr
•         ));
•     }
• /**
•      * 实例化,根据内容大小填充内容
•      */
• public SystemInfo(
•             long userIndexTableByteCount,
•             long inodeIndexTableByteCount
•     ) {
• this();
• long accu = this.byteCount();
• this.userIndexTableAddr.setValue(accu);
•         accu += userIndexTableByteCount;
• this.inodeIndexTableAddr.setValue(accu);
•         accu += inodeIndexTableByteCount;
• this.bitmapAddr.setValue(accu);
•     }
• public long getUserIndexTableAddr() {
• return this.userIndexTableAddr.getValue();
•     }
• public long getInodeIndexTableAddr() {
• return this.inodeIndexTableAddr.getValue();
•     }
• public long getBitmapAddr() {
• return this.bitmapAddr.getValue();
•     }
• @Override
• public boolean equals(Object o) {
• if (this == o) {
• return true;
•         }
• if (o == null || this.getClass() != o.getClass()) {
• return false;
•         }
•         SystemInfo that = (SystemInfo) o;
• return this.userIndexTableAddr.equals(that.userIndexTableAddr) &&
• this.inodeIndexTableAddr.equals(that.inodeIndexTableAddr) &&
• this.bitmapAddr.equals(that.bitmapAddr);
•     }
• @Override
• public int hashCode() {
• return Objects.hash(this.userIndexTableAddr, this.inodeIndexTableAddr, this.bitmapAddr);
•     }
• @Override
• public String toString() {
• return "SystemInfo{" +
• "userIndexTableAddr=" + this.userIndexTableAddr +
• ", inodeIndexTableAddr=" + this.inodeIndexTableAddr +
• ", bitmapAddr=" + this.bitmapAddr +
• '}';
•     }
• }

无非就是根据 User 表和 Inode 表的大小,分别计算出 User 表、Inode 表和 BitMap 的起始位置,便于后面的代码读取时直接 seek 寻址过去。

说完了物理上的数据结构,下面我们来说一说逻辑上的。

IndexEntry展开目录

上文提到:用户的目录文件也是普通文件,那它的内容是什么的?就是本节要说的 IndexEntry:

• package info.skyblond.os.exp.experiment3.model;
• import info.skyblond.os.exp.experiment3.bytes.WrappedGenericType;
• import info.skyblond.os.exp.experiment3.bytes.array.WrappedString;
• import info.skyblond.os.exp.experiment3.bytes.primitive.WrappedInteger;
• import java.util.Arrays;
• /**
•  * 目录项,是用户根目录文件的内容
•  */
• public class IndexEntry extends WrappedGenericType {
• /**
•      * 文件名最大字节数。60字节加上Inode索引的4字节正好64B,每个盘块正好记录64个文件。
•      */
• public static final int FILENAME_LENGTH = 60;
• private final WrappedInteger inodeIndex = new WrappedInteger();
• private final WrappedString fileName = new WrappedString(FILENAME_LENGTH);
• public IndexEntry() {
• this.parameters.addAll(Arrays.asList(this.inodeIndex, this.fileName));
•     }
• public int getInodeIndex() {
• return this.inodeIndex.getValue();
•     }
• public String getFileName() {
• return this.fileName.getValue();
•     }
• public void setInodeIndex(int newIndex) {
• this.inodeIndex.setValue(newIndex);
•     }
• public void setFileName(String newName) {
• this.fileName.setValue(newName);
•     }
• }

这个结构也十分简单:就是文件名对 Inode 索引的映射。文件系统查询用户目录下的文件时,就会读取用户的目录文件,根据里面的内容知晓用户有什么文件。因此这里使得文件分享非常简单:只要把 Inode 索引写成别的用户的文件就可以了,只要在读写的时候检查权限即可。

最后还要说一个只保存在内存中的数据结构。

OpenedFile展开目录

这个描述类只存在于内存中,它的作用就是记录已打开的文件的信息。例如当前文件的读写指针:

• package info.skyblond.os.exp.experiment3.model;
• /**
•  * 已打开的文件,只驻留内存
•  */
• public class OpenedFile {
• private final Inode inode;
• private long pos = 0L;
• public OpenedFile(Inode inode) {
• this.inode = inode;
•     }
• public Inode getInode() {
• return this.inode;
•     }
• public long getPos() {
• return this.pos;
•     }
• public void setPos(long pos) {
• this.pos = pos;
•     }
• @Override
• public String toString() {
• return "OpenedFile{" +
• "inode=" + this.inode +
• ", pos=" + this.pos +
• '}';
•     }
• }

它只保存文件的 Inode 对象和一个指针,后者指示当前读写文件的哪一部分。

总结展开目录

至此我们已经完成了元数据的代码描述。其中一些操作的实现可以用其他方法完成,例如使用 Boolean 返回值表示一个可失败的操作并不严谨,实践中一般会使用自定义的异常来做。但是我觉得 100 行代码有 50 行都是 try 和 catch,显得很不优雅。Java 编程固然有许多范式来指导程序员如何极力避免 bug 的出现,但是我认为写了一定数量的代码之后,消除 bug 是习惯,代码写得优雅是追求。

另外值得一提的是:本文中所有盘块号相关的都使用 short 存储,索引相关的全是 Integer,地址相关的全是 Long。

目录
相关文章
|
网络协议 NoSQL Java
模拟面试一(Java)
模拟面试一(Java)
161 1
模拟面试一(Java)
|
存储 Java 索引
不可上位!数据结构队列,老实排队,Java实现数组模拟队列及可复用环形队列
不可上位!数据结构队列,老实排队,Java实现数组模拟队列及可复用环形队列
151 0
不可上位!数据结构队列,老实排队,Java实现数组模拟队列及可复用环形队列
|
存储 设计模式 Java
【Java作业】模拟停车场(超详细!)
【Java作业】模拟停车场(超详细!)
【Java作业】模拟停车场(超详细!)
|
存储 Java 人机交互
Java 模拟二级文件系统 上
本系列将记述使用 Java 实现一个简单的二级文件系统的过程。
165 1
|
Java
字符串得结果!Java数组模拟栈以实现中缀表达式综合计算器,字符串表达式计算器
字符串得结果!Java数组模拟栈以实现中缀表达式综合计算器,字符串表达式计算器
147 0
|
Java
简洁明了,Java实现数组模拟栈,先进后出,栈顶为出入口
简洁明了,Java实现数组模拟栈,先进后出,栈顶为出入口
284 0
|
Java
使用java多线程模拟一个售票系统
1.基于继承Thread实现 代码实现:
207 0
|
存储 Java
Java 数组模拟 循环队列
循环队列是把顺序队列首尾相连,把存储队列元素的表从逻辑上看成一个环,成为循环队列。
153 0
|
Java
Java数组模拟队列
队列的作用就像电影院前的人们站成的排一样:第一个进入附属的人将最先到达队头买票。最后排队的人最后才能买到票。
104 0
|
存储 安全 Java
Java 模拟二级文件系统 终
本系列将记述使用 Java 实现一个简单的二级文件系统的过程。架构 构建项目的时候,避免代码的最重要的技巧在于区分哪些功能是哪些部分应该实现的,从语义和逻辑上考察这个问题,搞清楚了之后代码就不会变成一团乱。 之前对于文件、用户和磁盘的操作全都在文件系统中实现了。而我们要写的交互界面,将起到操作系统的作用。换句话说它要处理用户的认证、命令解析、打印必要的提示信息、询问命令执行依赖的参数,并最终根据已有的信息调用文件系统或其他代码产生影响。
226 0