本系列将记述使用Java实现一个简单的二级文件系统的过程。这个项目是本学期专业课操作系统的第三个实验。上一篇文章说明了项目要做的事情,并讲解基础工具类的设计。本文将记述元数据的代码描述。
Inode和InodeTable
本项目意图效仿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。
下一篇文章我将介绍如何编写一个简单的文件系统,让它利用本文所定义的诸多数据结构正常工作。
-全文完-
【代码札记】Java模拟二级文件系统 中 由 天空 Blond 采用 知识共享 署名 - 非商业性使用 - 相同方式共享 4.0 国际 许可协议进行许可。
本许可协议授权之外的使用权限可以从 https://skyblond.info/about.html 处获得。