天空Blond - 操作系统
type: channel
link: https://skyblond.info/tag/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/
items: 【代码札记】Java模拟二级文件系统 终
items: 【代码札记】Java模拟二级文件系统 下
items: 【代码札记】Java模拟二级文件系统 中
items: 【代码札记】Java模拟二级文件系统 上

【代码札记】Java模拟二级文件系统 终
type: item
date: 2020-12-16T19:06:00+08:00
description: 本系列将记述使用Java实现一个简单的二级文件系统的过程。这个项目是本学期专业课操作系统的第三个实验。上一篇文章说明了如何利用已经搭建好的代码构造一个简单的文件系统。本文将记述人机交互界面的构建，并最终让整个系统运行起来。架构构建项目的时候，避免代码变成一坨屎的最重要的技巧在于区分哪些功能是哪些部分应该实现的，从语义和逻辑上考察这个问题，搞清楚了之后代码就不会变成一团乱。之前对于文件、用户和磁盘的操作全都在文件系统中实现了。而我们要写的交互界面，将起到操作系统的作用。换句话说它要处理用户的认证、命令解析、打印必要的提示信息、询问命令执行依赖的参数，并最终根据已有的信息调用文件系统或其他代码产生影响。对于命令，我设计了一种比较便于解析的格式：help打印帮助信息exit退出系统user是与用户相关的命令user create创建用户user login用户登录......file是与文件相关的命令file list列出用户的文件file create创建文件......在此基础上，交互界面的大体框架如下：package info.skyblond.os.exp.experiment3; import info.skyblond.os.exp.experiment3.model.IndexEntry; import info.skyblond.os.exp.experiment3.model.OpenedFile; import info.skyblond.os.exp.experiment3.model.User; import info.skyblond.os.exp.utils.Pair; import java.io.File; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Scanner; import java.util.stream.Collectors; /** * 文件系统模拟器的主入口 * 模仿一个简单的操作系统，利用文件系统进行用户鉴权、文件操作 */ public class Exp3 { private static final Simulator simulator = Simulator.getInstance(); public static String getPromptLine(String username, String hostname) { if (username.isBlank()) { return "nobody@" + hostname + "> "; } else { return username + "@" + hostname + "> "; } } public static void main(String[] args) { var dataFile = new File("./data.bin"); simulator.setDataFile(dataFile); if (!dataFile.exists()) { System.out.println("未检测到数据文件，开始初始化。"); simulator.initDataFile(); System.out.println("已创建默认用户：root，密码：" + Simulator.ROOT_DEFAULT_PASSWORD); } var hostname = "FSS"; var currentUser = ""; // prompt System.out.print(getPromptLine(currentUser, hostname)); Scanner scanner = new Scanner(System.in); while (scanner.hasNext()) { String[] line = scanner.nextLine().split(" "); if (line[0].equalsIgnoreCase("help")) { // 帮助 } else if (line[0].equalsIgnoreCase("exit")) { // 退出 } else if (line.length == 2) { // 功能命令 switch (line[0].toLowerCase()) { case "user": // 用户相关命令 break; case "file": // 文件相关命令 break; default: // 未知命令 break; } else…

【代码札记】Java模拟二级文件系统 下
type: item
date: 2020-12-14T12:05:00+08:00
description: 本系列将记述使用Java实现一个简单的二级文件系统的过程。这个项目是本学期专业课操作系统的第三个实验。上一篇文章说明了元数据的代码描述。本文将记述如何利用已经搭建好的代码构造一个简单的文件系统。架构为了避免单个代码文件过于冗长，我决定将文件系统和交互界面分开。其中文件系统使用单例模式，交互界面则是以主程序的方式，与命令行交互并调用文件系统完成用户意图的操作。文件系统模拟器的大体框架如下：package info.skyblond.os.exp.experiment3; import info.skyblond.os.exp.experiment3.model.*; import info.skyblond.os.exp.utils.CommonUtils; import java.io.*; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * 文件系统模拟器，提供对磁盘基本的读写和维护操作。 */ public class Simulator { private static Simulator instance; private Simulator() { } public static synchronized Simulator getInstance() { if (instance == null) { instance = new Simulator(); } return instance; } // TODO .... }如果说元数据的设计是自底向上（从底层的Inode描述到上层已打开的文件），那么文件系统的设计就是自顶向下，从便于测试的角度看，先设计用户相关的内容会比较方便，更进一步说，先是用户的创建、查询和登录，然后是修改密码和删除用户。实践中往往一个功能并不能单独做完，还需要暂时放下，把别的做完再来继续当前的功能。例如删除用户的操作涉及到删除用户的目录文件，因此也要递归的删除用户目录下的所有文件，这时候就要求文件删除的功能。在此之前还得有文件读写的功能（对文件的操作除了撤销Inode、释放盘块，还要读写目录文件）。抛开这些不谈，我们先说说最基本的问题：如何初始化一个磁盘？格式化为了方便后续代码并减少代码重复，本节的铺垫代码如下： /** * 数据文件 */ private File dataFile = null; public void setDataFile(File dataFile) { this.dataFile = dataFile; } /** * 返回一个随机访问文件，mode参见{@link RandomAccessFile} */ public RandomAccessFile getRandomAccessFile(String mode) { try { return new RandomAccessFile(this.dataFile, mode); } catch (Exception e) { throw new RuntimeException(e); } }首先使用一个File作为磁盘文件，默认为null，单例模式下可以通过setter手动指定文件位置。而之后需要随机读写，因此提供一个简单的成员方法getRandomAccessFile，根据给定模式返回一个数据文件的RandomAccessFile对象。铺垫的代码就这么多，下面是负责初始化的代码： /** * root用户默认密码 */ public static String ROOT_DEFAULT_PASSWORD = "password"; /** * 初始化一个数据文件，默认创建一个 */ public void initDataFile() { try { CommonUtils.require(this.dataFile.createNewFile(), "Cannot create new data file."); var randomAccessFile = this.getRandomAccessFile("rw"); randomAccessFile.seek(0); // 初始化足够大的文件 for (int i = 0; i < BitMap.BLOCK_NUM; i++) { randomAccessFile.write(new byte[Inode.BLOCK_SIZE]); } // SystemInfo初始化 var systemInfo = new SystemInfo( UserTable.getInstance().byteCount(), InodeTabl…

【代码札记】Java模拟二级文件系统 中
type: item
date: 2020-12-08T18:21:00+08:00
description: 本系列将记述使用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…

【代码札记】Java模拟二级文件系统 上
type: item
date: 2020-12-06T17:36:00+08:00
description: 本系列将记述使用Java实现一个简单的二级文件系统的过程。这个项目是本学期专业课操作系统的第三个实验。本文将说明项目要做的事情，并讲解基础工具类的设计。二级文件系统实验指导书中的实验要求：用C语言（或其它语言，如Java）实现一个简单的二级文件系统设计，加深对文件系统的内部功能和内部实现的理解。要求模拟采用二级目录结构的磁盘文件系统的文件操作, 实现用户的创建、查询、登录、删除，和文件的添加、删除、打开、关闭、读、写等基本命令，用在屏幕上输入命令来模拟用户程序中调用的文件操作。其中文件的基本操作有：1）创建文件 在创建一个新文件时，系统首先要为新文件分配必要的外存空间，并在文件系统的目录中，为之建立一个目录项。目录项中应该记录新文件的文件名及其在外存的地址等属性。 2）删除文件 当已不再需要某文件时，可将它从文件系统中删除。在删除时，系统应该先从目录中找到要删除的文件的目录项，使之成为空项，然后回收该文件所占用的存储空间。 3）读文件 在读一个文件时，须在相应的系统调用中给出文件名和应该读入的内存目标地址。此时，系统同样要查找目录，找到制定的目录项，从中得到被读文件在外存中的位置。在目录项中，还有一个指针用于对文件的读/写。 4）写文件 在写一个文件时，须在相应的系统调用中给出该文件名及该文件在内存中的（源）地址。为此，也同样须先查找目录，找到指定文件的目录项，在利用目录中的写指针进行写操作。 5）截断文件 如果一个文件的内容已经陈旧而需要全部更新文件的内容时，一种方法是将此文件删除，再重新创建一个新文件。但如果文件名及其属性均无改变时，则可采取另一种所谓的截断文件的方法，此即将原有文件的长度设置为0，或说放弃原有的文件内容。 6）设置文件的读/写位置 前述的文件读/写操作，都只提供了对文件顺序存取的手段，即每次都是从文件的始端读或写。设置文件读/写位置的操作，用于设置文件读/写指针的位置，以便每次读/写文件时，不是从其始端而是从所设置的位置开始操作。也正因如此，才能改顺序存取为随机存取。 分析我将使用Java编写这个项目。其中涉及文件系统对磁盘的操作，肯定要支持随机存取。尽管Java中有常用的Stream的概念，但很遗憾的是Stream并不能随机读写。固然可以通过Stream的skip方法跳过前面的内容，找到我们需要的数据，但这样的操作方式更像是磁带，而非磁盘。如果使用一个二进制文件来模仿磁盘，则可以使用Java对文件的随机读写支持来实现我们的目标。因此在本项目中可能需要抛弃常用且便利的流，而另辟蹊径一种模式了。一个最简单的模式，也是本文使用的模式，就是利用RandomAccessFile，该类可以实现对文件的随机读写：RandomAccessFile randomAccessFile = new RandomAccessFile("path/to/some/file", "rw");其中"rw"与C语言类似，规定如下：r只读，此时试图写会引发异常rw读写，产生的修改不保证会同步写入磁盘rwd读写，对数据产生的修改会同步写入磁盘rws读写，对数据和元数据产生的修改会同步写入磁盘对文件的随机读写如下示范：// 将文件指针移动到200 randomAccessFile.seek(200); // 读取文件指针 long position = randomAccessFile.getFilePointer(); // 读一个字节 byte b = randomAccessFile.read(); // 读10个字节进字节数组 byte[] bytes = new byte[10]; randomAccessFile.read(bytes); // 读两个字节进字节数组，从5开始写 randomAccessFile.read(bytes, 5, 2); // 读一个基本类型 int i = randomAccessFile.readInt(); // 写一个字节 randomAccessFile.write(b); // 从字节数组写10个字节进文件 randomAccessFile.write(bytes); // 写两个字节进字节数组，从5开始读 randomAccessFile.write(bytes, 5, 2); // 写一个基本类型 randomAccessFile.writeInt(i);详细信息可以异步官方JavaDoc：「RandomAccessFile (Java Platform SE 8 )」。现在解决了文件随机读写的问题，现在来说一说如何模拟磁盘。磁盘管理实际上这个项目除了要写一个文件系统，还要模拟一个简单的操作系统来支持人机交互。因此用户信息和文件信息都要存储在磁盘上。暂定磁盘安排如下：|System I…