Java 模拟二级文件系统 终

简介: 本系列将记述使用 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 {
• // 未知命令
•             }
•             System.out.print(getPromptLine(currentUser, hostname));
•         }
•     }
• }

一上来有一个工具方法 getPromptLine 帮我们拼接出一个提示符,如果当前登录的用户为空,说明没有人登录,就把用户名替换成 nobody,然后在解析命令之前先把这个提示符打印出来,像下面这样:

• nobody@FSS> user login
• root@FSS> file list

这样一个优化能够让简陋的命令行一下子上档次不少。

主函数一上来要先准备数据文件,如果数据文件不存在就要初始化一个,并且提供用户默认账户的信息。之后开始命令的解析工作

帮助

这是系统中两个特殊命令之一。通过忽略大小写判断,如果用户在命令行内输入的内容等于 help,那么就打印一个帮助信息,告诉用户这个系统都支持什么命令:

• System.out.println("user list \t\t 列出所有已知用户");
• System.out.println("user create \t 创建新用户(仅限root)");
• System.out.println("user delete \t 删除已有用户(仅限root)");
• System.out.println("user login \t\t 登录(仅未登录时)");
• System.out.println("user logout \t 登出(仅登录时)");
• System.out.println("user chpasswd \t 修改密码(仅登录时)");
• System.out.println("-".repeat(50));
• System.out.println("file list \t\t 列出文件(仅登录时)");
• System.out.println("file create \t 创建文件(仅登录时)");
• System.out.println("file delete \t 删除文件(仅登录时)");
• System.out.println("file open \t\t 打开文件(仅登录时)");
• System.out.println("file close \t\t 关闭文件(仅登录时)");
• System.out.println("file read \t\t 读文件(仅登录时)");
• System.out.println("file write \t\t 写文件(仅登录时)");
• System.out.println("file cutoff \t 截断文件(仅登录时)");
• System.out.println("-".repeat(50));
• System.out.println("help \t\t\t 打印帮助信息");
• System.out.println("exit \t\t\t 退出系统");

就是一堆打印命令。

退出系统

这个是系统中第二个特殊命令。如果用户输入了 exit,那么就退出系统:

• System.out.println("Bye!");
• return;

因为在主函数里,所以直接打印一个 Bye! 之后 return,主函数结束,系统自然就退出了。

功能命令

这部分是实际执行一些操作的命令。由于规定这些命令都是两个单词构成,所以只要判断单词的个数就能确定是这一类命令:

• 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:
• // 未知命令
•             System.out.println("未知命令");
• break;
• } else {
• // 未知命令
•     System.out.println("命令格式有误");
• }

所有用户相关的命令都以 user 开头,所有文件相关的命令都由 file 开头,所以直接一个 switch 语句就可以将二者区分开。随后为了提高鲁棒性,加一个 default 处理所有不认识的命令。

用户相关命令

直接看第二个字段就可以判断出命令:

• case "user":
• switch (line[1].toLowerCase()) {
• case "list": {
• // 列出当前所有用户
• break;
•         }
• case "create": {
• // 创建
• break;
•         }
• case "delete": {
• // 删除
• break;
•         }
• case "login": {
• // 登录
• break;
•         }
• case "logout": {
• // 登出
• break;
•         }
• case "chpasswd": {
• // 修改密码
• break;
•         }
• default: {
•             System.out.println("未知命令");
• break;
•         }
•     }
• break;

列出用户

• case "list": {
• // 列出当前所有用户
• var userTable = simulator.readUserTable();
• var usernames = userTable.listUsername();
• for (String username : usernames) {
•         System.out.println(username);
•     }
• break;
• }

通过文件系统读出 UserTable,然后列出用户名一一打印即可。

创建用户

• case "create": {
• // 创建
• if (currentUser.equals("root")) {
• var userTable = simulator.readUserTable();
• var username = "";
•         System.out.print("请输入用户名:");
•         username = scanner.nextLine();
• if (username.isBlank()) {
•             System.out.println("用户名不可为空");
• break;
•         }
• if (userTable.get(username) != null) {
•             System.out.println("用户已存在");
• break;
•         }
•         System.out.print("请输入密码:");
• var password = scanner.nextLine();
• var result = simulator.createUser(username, password);
• if (result) {
•             System.out.println("用户创建成功");
•         } else {
•             System.out.println("创建用户失败,请重试");
•         }
•     } else {
•         System.out.println("请使用root账户登录后重试");
•     }
• break;
• }

创建用户要求 root 账户操作,所以要先检查当前登陆的用户是否为 root。不是则直接进入 else 分支打印提示信息。

对于创建流程来说,询问用户意图创建的用户名,然后检查用户名是否为空、用户名是否已经存在。如果用户名不满足条件,直接结束当前语句的执行。一开始是使用 do..while 来做的,后来认为如果用户中途改变主意不想新增用户了,那么没有办法退出。因此只要不满足条件,直接打印失败信息结束命令。对于密码则没有那么多要求,如果高兴的话密码为空也不是不行。最后调用文件系统创建用户,并根据文件系统的结果打印成功或失败的信息。

删除用户

• case "delete": {
• // 删除
• if (currentUser.equals("root")) {
• var userTable = simulator.readUserTable();
• var username = "";
•         System.out.print("请输入用户名:");
•         username = scanner.nextLine();
• if (userTable.get(username) == null) {
•             System.out.println("用户不存在");
• break;
•         }
• var result = simulator.deleteUser(username);
• if (result) {
• // 删除成功回收打开的文件
• for (String filename : queryAllOpenedFilename(username)) {
•                 closeFile(username, filename);
•             }
•             System.out.println("用户删除成功");
•         } else {
•             System.out.println("删除用户失败,请重试");
•         }
•     } else {
•         System.out.println("请使用root账户登录后重试");
•     }
• break;
• }

删除用户同样要求 root 操作。如果用户名不存在则直接打印提示并结束命令。否则删除用户,如果删除成功,再回收被删除用户已经打开的文件,这部分内容将在后面打开或关闭文件的时候提到。

用户登录

• case "login": {
• // 登录
• if (!currentUser.isBlank()) {
•         System.out.println("已登录,请先登出");
•     } else {
• var userTable = simulator.readUserTable();
•         User user;
•         System.out.print("请输入用户名:");
• var username = scanner.nextLine();
•         user = userTable.get(username);
• if (user == null) {
•             System.out.println("找不到用户");
• break;
•         }
•         System.out.print("请输入用户密码:");
• var password = scanner.nextLine();
• if (user.getPassword().equals(password)) {
•             currentUser = user.getUsername();
•             System.out.println("登陆成功");
•         } else {
•             System.out.println("密码错误,请重试");
•         }
•     }
• break;
• }

对于用户登录,在逻辑上已经登陆的用户不应该再次登录,所以只有已登录用户名为空的时候才允许登录。首先询问用户名,如果找不到用户则直接结束。找到了就继续询问密码,如果输入的密码和记录的密码相匹配,则成功登录。顺带一说:这里使用的 equals 方法来判断相等,实践上并不安全。一般来说使用 XOR 做对比,如果结果全 0 就说明相等,它与 equals 的区别在于后者会在第一个不相等的地方返回,这样攻击者可以根据运算时间的变化猜测正确的密码,而 XOR 则是要全部过一遍才能得出结果,从运行时间上并不能判断出是哪一位出错。但是由于这个系统偏向演示,所以只用 equals 就足以了。

用户登出

• case "logout": {
• // 登出
• if (currentUser.isBlank()) {
•         System.out.println("请先登录");
•     } else {
•         currentUser = "";
•     }
• break;
• }

有登录就得有登出,登出的话就得先登录。具体操作就是把当前已登录的用户名置为空。

修改密码

• case "chpasswd": {
• // 修改密码
• if (currentUser.isBlank()) {
•         System.out.println("请先登录");
•     } else {
• var userTable = simulator.readUserTable();
•         User user = userTable.get(currentUser);
• if (user == null) {
•             System.out.println("未找到当前已登录用户信息,已自动登出,请重新登陆后尝试");
•             currentUser = "";
• break;
•         } else {
• var password = "";
•             System.out.print("请输入旧密码:");
•             password = scanner.nextLine();
• if (!user.getPassword().equals(password)) {
•                 System.out.println("旧密码错误,请重试");
• break;
•             }
•             System.out.print("请输入新密码:");
•             password = scanner.nextLine();
• var result = simulator.changePassword(user.getUsername(), password);
• if (result) {
•                 System.out.println("修改密码成功");
•             } else {
•                 System.out.println("修改密码失败,请重试");
•             }
•         }
•     }

虽然实验要求没说要有改密码的功能,但代码都写了,不做浪费了。改密码也很简单,首先得要求用户登录,其次要验证旧密码。为了避免意料之外的事情,先要确保当前登录的用户是存在的,如果不存在直接自动登出。旧密码验证失败也直接结束命令的执行。旧密码验证成功之后,询问新密码并调用文件系统做出相应修改。

文件相关命令

文件相关的命令会有一些共同的操作,例如获取当前用户的目录文件 Inode 等,因此在开始指令之前,先把这些公用的东西获取出来:

• case "file":
• if (currentUser.isBlank()) {
•         System.out.println("请先登录");
•     } else {
• var user = simulator.readUserTable().get(currentUser);
• if (user == null) {
•             System.out.println("找不到当前登录用户的信息,已自动登出,请重新登陆后重试");
•             currentUser = "";
• break;
•         }
• var homeInode = simulator.readInodeTable().get(user.getHomeInodeIndex());
• switch (line[1].toLowerCase()) {
• case "list": {
• // 列出当前所文件
• break;
•             }
• case "create": {
• // 创建一个空文件
• break;
•             }
• case "delete": {
• // 删除一个文件
• break;
•             }
• case "open": {
• // 打开一个文件
• break;
•             }
• case "close": {
• // 关闭一个文件
• break;
•             }
• case "read": {
• // 读一个文件
• break;
•             }
• case "write": {
• // 写一个文件
• break;
•             }
• case "cutoff": {
• // 截断一个文件
• break;
•             }
• default:
•                 System.out.println("未知命令");
• break;
•         }
•     }
• break;

如果用户当前还没有登陆,使用文件相关的命令时会直接提示。如果用户的 Inode 找不到,说明用户不存在,直接自动登出并提示用户。

之后开始正常的命令解析:

列出文件

• case "list": {
• // 列出当前所文件
• var indexEntry = new IndexEntry();
• var entryCount = homeInode.getSize() / indexEntry.byteCount();
• var openedFilename = queryAllOpenedFilename(currentUser);
• var inodeTable = simulator.readInodeTable();
•     simulator.readSomeIndexEntry(homeInode, 0, entryCount)
•         .forEach((f) -> {
•             System.out.print("文件名:" + f.getFileName());
• if (openedFilename.contains(f.getFileName())) {
•                 System.out.print(",已打开");
•             }
•             System.out.println(",大小" + inodeTable.get(f.getInodeIndex()).getSize() + "B");
•         });
• break;
• }

列出文件使用读取目录项的方法。同时为了标记已经打开的文件,还会查询该用户所有已经打开的文件名并加以标注。最后每个文件还会打印它的大小。

创建文件

• case "create": {
• // 创建一个空文件
• var filename = "";
•     System.out.print("请输入文件名:");
•     filename = scanner.nextLine();
• if (filename.isBlank()) {
•         System.out.println("文件名不可为空");
• break;
•     }
• if (simulator.containsFile(homeInode, filename)) {
•         System.out.println("文件已存在");
• break;
•     }
• var result = simulator.createFile(currentUser, filename);
• if (result) {
•         System.out.println("创建成功");
•     } else {
•         System.out.println("创建失败");
•     }
• break;
• }

创建文件默认创建一个空白文件,不分配任何盘块。同样还是询问文件名,如果文件名已经存在或者为空,直接结束命令执行。通过检查则调用文件系统创建文件,并根据结果打印相关信息。

删除文件

• case "delete": {
• // 删除一个文件
• var filename = "";
•     System.out.print("请输入文件名:");
•     filename = scanner.nextLine();
• if (!simulator.containsFile(homeInode, filename)) {
•         System.out.println("文件不存在");
• break;
•     }
• if (queryOpenedFile(currentUser, filename) != null) {
•         System.out.println("文件已打开,请先关闭再删除");
• break;
•     }
• var result = simulator.deleteFile(currentUser, filename);
• if (result) {
•         System.out.println("删除成功");
•     } else {
•         System.out.println("删除失败");
•     }
• break;
• }

删除文件也是一样,如果文件不存在,或者已经打开,则拒绝删除。否则调用文件系统删除文件。

打开文件

• case "open": {
• // 打开一个文件
• var filename = "";
•     System.out.print("请输入文件名:");
•     filename = scanner.nextLine();
• if (!simulator.containsFile(homeInode, filename)) {
•         System.out.println("文件不存在");
• break;
•     }
• if (queryOpenedFile(currentUser, filename) != null) {
•         System.out.println("文件已打开");
• break;
•     }
• var result = openFile(currentUser, filename);
• if (result) {
•         System.out.println("文件打开成功");
•     } else {
•         System.out.println("文件打开失败");
•     }
• break;
• }

打开文件只允许打开已经存在且尚未打开的文件。相关工具方法如下

• /**
•      * 打开的文件,((用户名,文件名),文件描述)
•      */
• private static final Map<Pair<String, String>, OpenedFile> openedFiles = new HashMap<>();
• /**
•      * 打开文件
•      */
• public static boolean openFile(String username, String filename) {
• var user = simulator.readUserTable().get(username);
• if (user == null) {
• return false;
•         }
• var inodeTable = simulator.readInodeTable();
• var homeInode = inodeTable.get(user.getHomeInodeIndex());
• // 不存在的文件直接返回false
• if (!simulator.containsFile(homeInode, filename)) {
• return false;
•         }
• // 如果已经打开也直接返回
• if (openedFiles.containsKey(new Pair<>(username, filename))) {
• return false;
•         }
•         openedFiles.put(new Pair<>(username, filename), new OpenedFile(simulator.findInodeByFilename(homeInode, filename)));
• return true;
•     }
• /**
•      * 关闭文件
•      */
• public static boolean closeFile(String username, String filename) {
• // 如果没打开直接返回
• if (!openedFiles.containsKey(new Pair<>(username, filename))) {
• return false;
•         }
•         openedFiles.remove(new Pair<>(username, filename));
• return true;
•     }
• /**
•      * 通过用户名和文件名查询已打开的文件描述
•      */
• public static OpenedFile queryOpenedFile(String username, String filename) {
• // 如果没打开直接返回
• if (!openedFiles.containsKey(new Pair<>(username, filename))) {
• return null;
•         }
• return openedFiles.get(new Pair<>(username, filename));
•     }
• /**
•      * 获取已经打开的文件名
•      */
• public static List<String> queryAllOpenedFilename(String username) {
• return openedFiles.keySet().stream().filter(p -> p.getFirst().equals(username)).map(Pair::getSecond).collect(Collectors.toList());
•     }

打开文件的核心就是将打开的文件信息存储在内存中。打开的文件信息包括文件的 Inode 引用和一个当前读写指针的位置。后者将提供随机访问的特性。

关闭文件

• case "close": {
• // 关闭一个文件
• var filename = "";
•     System.out.print("请输入文件名:");
•     filename = scanner.nextLine();
• if (queryOpenedFile(currentUser, filename) == null) {
•         System.out.println("文件未打开");
• break;
•     }
• var result = closeFile(currentUser, filename);
• if (result) {
•         System.out.println("文件关闭成功");
•     } else {
•         System.out.println("文件关闭失败");
•     }
• break;
• }

文件打开了就得关闭,要不然会造成内存泄漏。关闭只允许关闭已经打开的文件。操作上就是将对应的信息从内存中移除。

读文件

• case "read": {
• // 读一个文件
• var filename = "";
•     System.out.print("请输入文件名:");
•     filename = scanner.nextLine();
• var openedFile = queryOpenedFile(currentUser, filename);
• if (openedFile == null) {
•         System.out.println("文件未打开");
• break;
•     }
• var delta = 0L;
•     System.out.println("当前文件描述符指针在" + openedFile.getPos());
•     System.out.println("文件大小" + openedFile.getInode().getSize() + "B");
• if (openedFile.getPos() >= openedFile.getInode().getSize()) {
•         System.out.println("已到达文件尾。");
•     }
•     System.out.print("请输入相对偏移量:");
•     delta = scanner.nextLong();
•     scanner.nextLine();
• // 更新偏移量
•     openedFile.setPos(openedFile.getPos() + delta);
•     System.out.println("移动后指针:" + openedFile.getPos());
• var length = 0L;
•     System.out.print("请输入要读取的长度:");
•     length = scanner.nextLong();
•     scanner.nextLine();
• if (length <= 0) {
•         System.out.println("长度必须大于0");
•     }
• try {
• var bytes = simulator.readFile(openedFile.getInode(), openedFile.getPos(), length);
•         System.out.println(new String(bytes, StandardCharsets.UTF_8));
•         openedFile.setPos(openedFile.getPos() + bytes.length);
•         System.out.println("读取后文件指针:" + openedFile.getPos());
•     } catch (Exception e) {
•         System.out.println("读取失败:" + e.getMessage());
•     }
• break;
• }

读文件要求只能读打开的文件,原因是未打开的文件没有读写指针,不知道从哪里开始读。读取的时候会根据提供的文件名寻找已打开的文件信息,并提示用户操作读写指针进行随机读写。

由于这个系统很简陋,所以唯一能读写的东西就是字符。在读取部分简单的包了个异常处理,因为文件系统那里会对传入参数进行检查,如果检查失败直接抛出异常崩溃 JVM,这个异常处理要做的就是拦截异常,将其信息打印给用户,然后结束指令。

写文件

• case "write": {
• // 写一个文件
• var filename = "";
•     System.out.print("请输入文件名:");
•     filename = scanner.nextLine();
• var openedFile = queryOpenedFile(currentUser, filename);
• if (openedFile == null) {
•         System.out.println("文件未打开");
• break;
•     }
• var delta = 0L;
•     System.out.println("当前文件描述符指针在" + openedFile.getPos());
•     System.out.println("文件大小" + openedFile.getInode().getSize() + "B");
• if (openedFile.getPos() >= openedFile.getInode().getSize()) {
•         System.out.println("已到达文件尾");
•     }
•     System.out.print("请输入相对偏移量:");
•     delta = scanner.nextLong();
•     scanner.nextLine();
• // 更新偏移量
•     openedFile.setPos(openedFile.getPos() + delta);
•     System.out.println("移动后指针:" + openedFile.getPos());
• var length = 0L;
•     System.out.println("请在下方输入要写入的内容,单独一行'$EOF'表示结束:");
• // 打印一个字符避免退格时回到已经写入的上一行
•     System.out.print(">");
• var inputLine = scanner.nextLine();
• try {
• while (!inputLine.equals("$EOF")) {
• if (length != 0) {
• // 不是第一行,向前面行追加回车
•                 inputLine = String.format("\n%s", inputLine);
•             }
• var bytes = inputLine.getBytes(StandardCharsets.UTF_8);
• var result = simulator.writeFile(openedFile.getInode(), openedFile.getPos() + length, bytes);
• if (!result) {
•                 System.out.println("写入失败!");
• break;
•             }
• // 更新长度
•             length += bytes.length;
• // 读下一行
•             System.out.print(">");
•             inputLine = scanner.nextLine();
•         }
•     } catch (Exception e) {
•         System.out.println("写入失败:" + e.getMessage());
•     }
•     System.out.println("已写入" + length + "B");
•     openedFile.setPos(openedFile.getPos() + length);
•     System.out.println("当前文件指针:" + openedFile.getPos());
• break;
• }

写文件的交互与读文件的类似。写入支持多行,单行的 $EOF 表示结束。如果中途写入失败,通常是指针越界,文件系统会直接抛出异常。这个异常同样会被异常处理拦截并转换成错误信息。

截断文件

• case "cutoff": {
• // 截断一个文件
• var filename = "";
•     System.out.print("请输入文件名:");
•     filename = scanner.nextLine();
• var openedFile = queryOpenedFile(currentUser, filename);
• if (openedFile == null) {
•         System.out.println("文件未打开");
• break;
•     }
•     System.out.println("文件大小" + openedFile.getInode().getSize() + "B");
• if (openedFile.getPos() >= openedFile.getInode().getSize()) {
•         System.out.println("已到达文件尾");
•     }
• var newSize = 0L;
•     System.out.print("请输入新的大小:");
•     newSize = scanner.nextLong();
•     scanner.nextLine();
• if (newSize > openedFile.getInode().getSize()) {
•         System.out.println("新大小不能超过原来的大小");
• break;
•     }
•     simulator.cutoffFile(openedFile.getInode(), newSize);
•     System.out.println("截断文件成功");
• break;
• }

就是询问文件的新大小,并调用文件系统的相关功能。

效果

• 未检测到数据文件,开始初始化。
• 已创建默认用户:root,密码:password
• nobody@FSS> user login
• 请输入用户名:root
• 请输入用户密码:123
• 密码错误,请重试
• nobody@FSS> user login
• 请输入用户名:root
• 请输入用户密码:password
• 登陆成功
• root@FSS> file list
• root@FSS> file create
• 请输入文件名:123
• 创建成功
• root@FSS> file open
• 请输入文件名:123
• 文件打开成功
• root@FSS> file list
• 文件名:123,已打开,大小0B
• root@FSS> file write
• 请输入文件名:123
• 当前文件描述符指针在0
• 文件大小0B
• 已到达文件尾
• 请输入相对偏移量:1
• 移动后指针:1
• 请在下方输入要写入的内容,单独一行'$EOF'表示结束:
• >123
• 写入失败:Offset must not bigger than size.
• 已写入0B
• 当前文件指针:1
• root@FSS> file write
• 请输入文件名:123
• 当前文件描述符指针在1
• 文件大小0B
• 已到达文件尾
• 请输入相对偏移量:-1
• 移动后指针:0
• 请在下方输入要写入的内容,单独一行'$EOF'表示结束:
• >1234567890
• >abc
• >
• >$EOF
• 已写入15B
• 当前文件指针:15
• root@FSS> file read
• 请输入文件名:123
• 当前文件描述符指针在15
• 文件大小15B
• 已到达文件尾。
• 请输入相对偏移量:-15
• 移动后指针:0
• 请输入要读取的长度:15
• 1234567890
• abc
• 读取后文件指针:15
• root@FSS> file close
• 请输入文件名:1234
• 文件未打开
• root@FSS> file close
• 请输入文件名:123
• 文件关闭成功
• root@FSS> exit
• Bye!

这个效果还不错,至少我简单的测试了一下,没有意料之外的报错。后来请同学以「让这个系统崩溃」为目标可劲儿的尝试,最终看起来代码没有比较严重的 bug,也没有与预期不相符的行为。

总结展开目录

今天程序顺利通过验机,这个系列也就此告一段落。这个实验在 11 月 3 号开始写,到 12 月 3 号写完,正好一个月。实话说这个程序虽然实用性不强,但是其中用到的许多设计想法都是我未曾在实用程序中实践过的。例如用户输入的条件不满足直接抛异常崩 JVM,我大概永远不敢在重要的程序上这样做,通常有一个 default 会有用很多。另外一个例子就是整个 Wrapped 的构造,平时写代码的时候别说字节数组了,Serializable 都很少用,直接一个 Gson 或者 Jackson,反射就把这事儿做了,我也无须关心底层是怎么实现的(当然了,用了 Gson 打完包程序上百 MB 就是另一个故事了)。以及直接使用 RandomAccessFile 像文件系统那样将数据定位到字节的精度去操作,这种深度还是我头一次用 Java 做。

无论如何,这个项目很顺利的完成了。本文作为这个系列的最后一篇文章,实现了人机交互界面,并最终让这个系统运行了起来。

附录:完整代码

• 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();
• /**
•      * 打开的文件,((用户名,文件名),文件描述)
•      */
• private static final Map<Pair<String, String>, OpenedFile> openedFiles = new HashMap<>();
• /**
•      * 打开文件
•      */
• public static boolean openFile(String username, String filename) {
• var user = simulator.readUserTable().get(username);
• if (user == null) {
• return false;
•         }
• var inodeTable = simulator.readInodeTable();
• var homeInode = inodeTable.get(user.getHomeInodeIndex());
• // 不存在的文件直接返回false
• if (!simulator.containsFile(homeInode, filename)) {
• return false;
•         }
• // 如果已经打开也直接返回
• if (openedFiles.containsKey(new Pair<>(username, filename))) {
• return false;
•         }
•         openedFiles.put(new Pair<>(username, filename), new OpenedFile(simulator.findInodeByFilename(homeInode, filename)));
• return true;
•     }
• /**
•      * 关闭文件
•      */
• public static boolean closeFile(String username, String filename) {
• // 如果没打开直接返回
• if (!openedFiles.containsKey(new Pair<>(username, filename))) {
• return false;
•         }
•         openedFiles.remove(new Pair<>(username, filename));
• return true;
•     }
• /**
•      * 通过用户名和文件名查询已打开的文件描述
•      */
• public static OpenedFile queryOpenedFile(String username, String filename) {
• // 如果没打开直接返回
• if (!openedFiles.containsKey(new Pair<>(username, filename))) {
• return null;
•         }
• return openedFiles.get(new Pair<>(username, filename));
•     }
• /**
•      * 获取已经打开的文件名
•      */
• public static List<String> queryAllOpenedFilename(String username) {
• return openedFiles.keySet().stream().filter(p -> p.getFirst().equals(username)).map(Pair::getSecond).collect(Collectors.toList());
•     }
• 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 = "root"; // 默认root用户,调试用
• 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")) {
• // 帮助
•                 System.out.println("user list \t\t 列出所有已知用户");
•                 System.out.println("user create \t 创建新用户(仅限root)");
•                 System.out.println("user delete \t 删除已有用户(仅限root)");
•                 System.out.println("user login \t\t 登录(仅未登录时)");
•                 System.out.println("user logout \t 登出(仅登录时)");
•                 System.out.println("user chpasswd \t 修改密码(仅登录时)");
•                 System.out.println("-".repeat(50));
•                 System.out.println("file list \t\t 列出文件(仅登录时)");
•                 System.out.println("file create \t 创建文件(仅登录时)");
•                 System.out.println("file delete \t 删除文件(仅登录时)");
•                 System.out.println("file open \t\t 打开文件(仅登录时)");
•                 System.out.println("file close \t\t 关闭文件(仅登录时)");
•                 System.out.println("file read \t\t 读文件(仅登录时)");
•                 System.out.println("file write \t\t 写文件(仅登录时)");
•                 System.out.println("file cutoff \t 截断文件(仅登录时)");
•                 System.out.println("-".repeat(50));
•                 System.out.println("help \t\t\t 打印帮助信息");
•                 System.out.println("exit \t\t\t 退出系统");
•             } else if (line[0].equalsIgnoreCase("exit")) {
• // 退出
•                 System.out.println("Bye!");
• return;
•             } else if (line.length == 2) {
• switch (line[0].toLowerCase()) {
• case "user":
• switch (line[1].toLowerCase()) {
• case "list": {
• // 列出当前所有用户
• var userTable = simulator.readUserTable();
• var usernames = userTable.listUsername();
• for (String username : usernames) {
•                                     System.out.println(username);
•                                 }
• break;
•                             }
• case "create": {
• // 创建
• if (currentUser.equals("root")) {
• var userTable = simulator.readUserTable();
• var username = "";
•                                     System.out.print("请输入用户名:");
•                                     username = scanner.nextLine();
• if (username.isBlank()) {
•                                         System.out.println("用户名不可为空");
• break;
•                                     }
• if (userTable.get(username) != null) {
•                                         System.out.println("用户已存在");
• break;
•                                     }
•                                     System.out.print("请输入密码:");
• var password = scanner.nextLine();
• var result = simulator.createUser(username, password);
• if (result) {
•                                         System.out.println("用户创建成功");
•                                     } else {
•                                         System.out.println("创建用户失败,请重试");
•                                     }
•                                 } else {
•                                     System.out.println("请使用root账户登录后重试");
•                                 }
• break;
•                             }
• case "delete": {
• // 删除
• if (currentUser.equals("root")) {
• var userTable = simulator.readUserTable();
• var username = "";
•                                     System.out.print("请输入用户名:");
•                                     username = scanner.nextLine();
• if (userTable.get(username) == null) {
•                                         System.out.println("用户不存在");
• break;
•                                     }
• var result = simulator.deleteUser(username);
• if (result) {
• // 删除成功回收打开的文件
• for (String filename : queryAllOpenedFilename(username)) {
•                                             closeFile(username, filename);
•                                         }
•                                         System.out.println("用户删除成功");
•                                     } else {
•                                         System.out.println("删除用户失败,请重试");
•                                     }
•                                 } else {
•                                     System.out.println("请使用root账户登录后重试");
•                                 }
• break;
•                             }
• case "login": {
• // 登录
• if (!currentUser.isBlank()) {
•                                     System.out.println("已登录,请先登出");
•                                 } else {
• var userTable = simulator.readUserTable();
•                                     User user;
•                                     System.out.print("请输入用户名:");
• var username = scanner.nextLine();
•                                     user = userTable.get(username);
• if (user == null) {
•                                         System.out.println("找不到用户");
• break;
•                                     }
•                                     System.out.print("请输入用户密码:");
• var password = scanner.nextLine();
• if (user.getPassword().equals(password)) {
•                                         currentUser = user.getUsername();
•                                         System.out.println("登陆成功");
•                                     } else {
•                                         System.out.println("密码错误,请重试");
•                                     }
•                                 }
• break;
•                             }
• case "logout": {
• // 登出
• if (currentUser.isBlank()) {
•                                     System.out.println("请先登录");
•                                 } else {
•                                     currentUser = "";
•                                 }
• break;
•                             }
• case "chpasswd": {
• // 修改密码
• if (currentUser.isBlank()) {
•                                     System.out.println("请先登录");
•                                 } else {
• var userTable = simulator.readUserTable();
•                                     User user = userTable.get(currentUser);
• if (user == null) {
•                                         System.out.println("未找到当前已登录用户信息,已自动登出,请重新登陆后尝试");
•                                         currentUser = "";
• break;
•                                     } else {
• var password = "";
•                                         System.out.print("请输入旧密码:");
•                                         password = scanner.nextLine();
• if (!user.getPassword().equals(password)) {
•                                             System.out.println("旧密码错误,请重试");
• break;
•                                         }
•                                         System.out.print("请输入新密码:");
•                                         password = scanner.nextLine();
• var result = simulator.changePassword(user.getUsername(), password);
• if (result) {
•                                             System.out.println("修改密码成功");
•                                         } else {
•                                             System.out.println("修改密码失败,请重试");
•                                         }
•                                     }
•                                 }
• break;
•                             }
• default: {
•                                 System.out.println("未知命令");
• break;
•                             }
•                         }
• break;
• case "file":
• if (currentUser.isBlank()) {
•                             System.out.println("请先登录");
•                         } else {
• var user = simulator.readUserTable().get(currentUser);
• if (user == null) {
•                                 System.out.println("找不到当前登录用户的信息,已自动登出,请重新登陆后重试");
•                                 currentUser = "";
• break;
•                             }
• var homeInode = simulator.readInodeTable().get(user.getHomeInodeIndex());
• switch (line[1].toLowerCase()) {
• case "list": {
• // 列出当前所文件
• var indexEntry = new IndexEntry();
• var entryCount = homeInode.getSize() / indexEntry.byteCount();
• var openedFilename = queryAllOpenedFilename(currentUser);
• var inodeTable = simulator.readInodeTable();
•                                     simulator.readSomeIndexEntry(homeInode, 0, entryCount)
•                                             .forEach((f) -> {
•                                                 System.out.print("文件名:" + f.getFileName());
• if (openedFilename.contains(f.getFileName())) {
•                                                     System.out.print(",已打开");
•                                                 }
•                                                 System.out.println(",大小" + inodeTable.get(f.getInodeIndex()).getSize() + "B");
•                                             });
• break;
•                                 }
• case "create": {
• // 创建一个空文件
• var filename = "";
•                                     System.out.print("请输入文件名:");
•                                     filename = scanner.nextLine();
• if (filename.isBlank()) {
•                                         System.out.println("文件名不可为空");
• break;
•                                     }
• if (simulator.containsFile(homeInode, filename)) {
•                                         System.out.println("文件已存在");
• break;
•                                     }
• var result = simulator.createFile(currentUser, filename);
• if (result) {
•                                         System.out.println("创建成功");
•                                     } else {
•                                         System.out.println("创建失败");
•                                     }
• break;
•                                 }
• case "delete": {
• // 删除一个文件
• var filename = "";
•                                     System.out.print("请输入文件名:");
•                                     filename = scanner.nextLine();
• if (!simulator.containsFile(homeInode, filename)) {
•                                         System.out.println("文件不存在");
• break;
•                                     }
• if (queryOpenedFile(currentUser, filename) != null) {
•                                         System.out.println("文件已打开,请先关闭再删除");
• break;
•                                     }
• var result = simulator.deleteFile(currentUser, filename);
• if (result) {
•                                         System.out.println("删除成功");
•                                     } else {
•                                         System.out.println("删除失败");
•                                     }
• break;
•                                 }
• case "open": {
• // 打开一个文件
• var filename = "";
•                                     System.out.print("请输入文件名:");
•                                     filename = scanner.nextLine();
• if (!simulator.containsFile(homeInode, filename)) {
•                                         System.out.println("文件不存在");
• break;
•                                     }
• if (queryOpenedFile(currentUser, filename) != null) {
•                                         System.out.println("文件已打开");
• break;
•                                     }
• var result = openFile(currentUser, filename);
• if (result) {
•                                         System.out.println("文件打开成功");
•                                     } else {
•                                         System.out.println("文件打开失败");
•                                     }
• break;
•                                 }
• case "close": {
• // 关闭一个文件
• var filename = "";
•                                     System.out.print("请输入文件名:");
•                                     filename = scanner.nextLine();
• if (queryOpenedFile(currentUser, filename) == null) {
•                                         System.out.println("文件未打开");
• break;
•                                     }
• var result = closeFile(currentUser, filename);
• if (result) {
•                                         System.out.println("文件关闭成功");
•                                     } else {
•                                         System.out.println("文件关闭失败");
•                                     }
• break;
•                                 }
• case "read": {
• // 读一个文件
• var filename = "";
•                                     System.out.print("请输入文件名:");
•                                     filename = scanner.nextLine();
• var openedFile = queryOpenedFile(currentUser, filename);
• if (openedFile == null) {
•                                         System.out.println("文件未打开");
• break;
•                                     }
• var delta = 0L;
•                                     System.out.println("当前文件描述符指针在" + openedFile.getPos());
•                                     System.out.println("文件大小" + openedFile.getInode().getSize() + "B");
• if (openedFile.getPos() >= openedFile.getInode().getSize()) {
•                                         System.out.println("已到达文件尾。");
•                                     }
•                                     System.out.print("请输入相对偏移量:");
•                                     delta = scanner.nextLong();
•                                     scanner.nextLine();
• // 更新偏移量
•                                     openedFile.setPos(openedFile.getPos() + delta);
•                                     System.out.println("移动后指针:" + openedFile.getPos());
• var length = 0L;
•                                     System.out.print("请输入要读取的长度:");
•                                     length = scanner.nextLong();
•                                     scanner.nextLine();
• if (length <= 0) {
•                                         System.out.println("长度必须大于0");
•                                     }
• try {
• var bytes = simulator.readFile(openedFile.getInode(), openedFile.getPos(), length);
•                                         System.out.println(new String(bytes, StandardCharsets.UTF_8));
•                                         openedFile.setPos(openedFile.getPos() + bytes.length);
•                                         System.out.println("读取后文件指针:" + openedFile.getPos());
•                                     } catch (Exception e) {
•                                         System.out.println("读取失败:" + e.getMessage());
•                                     }
• break;
•                                 }
• case "write": {
• // 写一个文件
• var filename = "";
•                                     System.out.print("请输入文件名:");
•                                     filename = scanner.nextLine();
• var openedFile = queryOpenedFile(currentUser, filename);
• if (openedFile == null) {
•                                         System.out.println("文件未打开");
• break;
•                                     }
• var delta = 0L;
•                                     System.out.println("当前文件描述符指针在" + openedFile.getPos());
•                                     System.out.println("文件大小" + openedFile.getInode().getSize() + "B");
• if (openedFile.getPos() >= openedFile.getInode().getSize()) {
•                                         System.out.println("已到达文件尾");
•                                     }
•                                     System.out.print("请输入相对偏移量:");
•                                     delta = scanner.nextLong();
•                                     scanner.nextLine();
• // 更新偏移量
•                                     openedFile.setPos(openedFile.getPos() + delta);
•                                     System.out.println("移动后指针:" + openedFile.getPos());
• var length = 0L;
•                                     System.out.println("请在下方输入要写入的内容,单独一行'$EOF'表示结束:");
• // 打印一个字符避免退格时回到已经写入的上一行
•                                     System.out.print(">");
• var inputLine = scanner.nextLine();
• try {
• while (!inputLine.equals("$EOF")) {
• if (length != 0) {
• // 不是第一行,向前面行追加回车
•                                                 inputLine = String.format("\n%s", inputLine);
•                                             }
• var bytes = inputLine.getBytes(StandardCharsets.UTF_8);
• var result = simulator.writeFile(openedFile.getInode(), openedFile.getPos() + length, bytes);
• if (!result) {
•                                                 System.out.println("写入失败!");
• break;
•                                             }
• // 更新长度
•                                             length += bytes.length;
• // 读下一行
•                                             System.out.print(">");
•                                             inputLine = scanner.nextLine();
•                                         }
•                                     } catch (Exception e) {
•                                         System.out.println("写入失败:" + e.getMessage());
•                                     }
•                                     System.out.println("已写入" + length + "B");
•                                     openedFile.setPos(openedFile.getPos() + length);
•                                     System.out.println("当前文件指针:" + openedFile.getPos());
• break;
•                                 }
• case "cutoff": {
• // 截断一个文件
• var filename = "";
•                                     System.out.print("请输入文件名:");
•                                     filename = scanner.nextLine();
• var openedFile = queryOpenedFile(currentUser, filename);
• if (openedFile == null) {
•                                         System.out.println("文件未打开");
• break;
•                                     }
•                                     System.out.println("文件大小" + openedFile.getInode().getSize() + "B");
• if (openedFile.getPos() >= openedFile.getInode().getSize()) {
•                                         System.out.println("已到达文件尾");
•                                     }
• var newSize = 0L;
•                                     System.out.print("请输入新的大小:");
•                                     newSize = scanner.nextLong();
•                                     scanner.nextLine();
• if (newSize > openedFile.getInode().getSize()) {
•                                         System.out.println("新大小不能超过原来的大小");
• break;
•                                     }
•                                     simulator.cutoffFile(openedFile.getInode(), newSize);
•                                     System.out.println("截断文件成功");
• break;
•                                 }
• default:
•                                     System.out.println("未知命令");
• break;
•                             }
•                         }
• break;
• default:
•                         System.out.println("未知命令");
• break;
•                 }
•             } else {
•                 System.out.println("命令格式有误");
•             }
•             System.out.print(getPromptLine(currentUser, hostname));
•         }
•     }
• }


目录
相关文章
|
Java
字符串得结果!Java数组模拟栈以实现中缀表达式综合计算器,字符串表达式计算器
字符串得结果!Java数组模拟栈以实现中缀表达式综合计算器,字符串表达式计算器
112 0
|
Java
简洁明了,Java实现数组模拟栈,先进后出,栈顶为出入口
简洁明了,Java实现数组模拟栈,先进后出,栈顶为出入口
254 0
|
存储 Java 索引
不可上位!数据结构队列,老实排队,Java实现数组模拟队列及可复用环形队列
不可上位!数据结构队列,老实排队,Java实现数组模拟队列及可复用环形队列
115 0
不可上位!数据结构队列,老实排队,Java实现数组模拟队列及可复用环形队列
|
Java
使用java多线程模拟一个售票系统
1.基于继承Thread实现 代码实现:
158 0
|
网络协议 NoSQL Java
模拟面试一(Java)
模拟面试一(Java)
136 1
模拟面试一(Java)
|
存储 Java
Java 数组模拟 循环队列
循环队列是把顺序队列首尾相连,把存储队列元素的表从逻辑上看成一个环,成为循环队列。
128 0
|
Java
Java数组模拟队列
队列的作用就像电影院前的人们站成的排一样:第一个进入附属的人将最先到达队头买票。最后排队的人最后才能买到票。
71 0
|
存储 设计模式 Java
【Java作业】模拟停车场(超详细!)
【Java作业】模拟停车场(超详细!)
【Java作业】模拟停车场(超详细!)
|
Java 人机交互 数据安全/隐私保护
Java 模拟二级文件系统 下
本系列将记述使用 Java 实现一个简单的二级文件系统的过程。架构为了避免单个代码文件过于冗长,我决定将文件系统和交互界面分开。其中文件系统使用单例模式,交互界面则是以主程序的方式,与命令行交互并调用文件系统完成用户意图的操作。
207 0
|
存储 Java 程序员
Java 模拟二级文件系统 中
本系列将记述使用 Java 实现一个简单的二级文件系统的过程。本项目意图效仿 Linux 的文件管理,但是学的又没那么像,仅仅是一些皮毛。因此使用类似 Inode 的东西记录文件在磁盘上的信息。
191 1