feat(vm): 实现 UNIX风格的文件描述符 I/O 操作

- 新增 FDTable 类,用于管理文件描述符和 NIO Channel 的映射
- 重构 SyscallCommand 类,实现多个 I/O 相关的 syscall 子命令
- 支持文件操作(open、close、read、write、seek)、管道、网络套接字、I/O 多路复用等功能
- 优化异常处理机制,统一处理 syscall内部异常
This commit is contained in:
Luke 2025-07-18 18:06:19 +08:00
parent c388edd0cf
commit dad69eabfb
2 changed files with 422 additions and 40 deletions

View File

@ -1,72 +1,392 @@
package org.jcnc.snow.vm.commands.system.control; package org.jcnc.snow.vm.commands.system.control;
import org.jcnc.snow.vm.interfaces.Command; import org.jcnc.snow.vm.interfaces.Command;
import org.jcnc.snow.vm.io.FDTable;
import org.jcnc.snow.vm.module.CallStack; import org.jcnc.snow.vm.module.CallStack;
import org.jcnc.snow.vm.module.LocalVariableStore; import org.jcnc.snow.vm.module.LocalVariableStore;
import org.jcnc.snow.vm.module.OperandStack; import org.jcnc.snow.vm.module.OperandStack;
import java.util.Arrays; import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.file.OpenOption;
import java.nio.file.Paths;
import java.util.*;
import static java.nio.file.StandardOpenOption.*;
/** /**
* SyscallCommand 统一的系统调用入口opcode = 0x0401 * {@code SyscallCommand} I/O syscall 子命令集合指令
* *
* <p>当前支持子命令 * <p>
* 封装类 UNIX 文件描述符File Descriptor, FD操作文件/网络 I/O管道fd 复制多路复用select等系统调用能力
* 基于 Java NIO 统一实现供虚拟机指令集以 SYSCALL 形式扩展使用
* </p>
*
* <b>栈操作约定</b>
* <ul> * <ul>
* <li><b>PRINT</b> 打印不换行</li> * <li>参数按右值先入栈左值后入栈的顺序推入操作数栈执行 {@code SYSCALL <SUBCMD>} 后出栈</li>
* <li><b>PRINTLN</b> 打印并换行</li> * <li>调用 SYSCALL OPEN path flags mode入栈顺序为 path先入 flags mode后入出栈时 modeflagspath 弹出</li>
* </ul> * </ul>
* *
* <p>用法示例VM 指令</p> * <b>返回值说明</b>
* <pre> * <ul>
* 1025 PRINT "Hello, Snow!" * <li>成功压入正值 FD实际数据字节数0 表示成功</li>
* I_PUSH 42 * <li>失败统一压入 -1后续可扩展为 errno 机制</li>
* 1025 PRINTLN * </ul>
* </pre> *
* <b>支持的子命令部分</b>
* <ul>
* <li>PRINT / PRINTLN 控制台输出<b>TODO: 代码未实现</b></li>
* <li>OPEN / CLOSE / READ / WRITE / SEEK 文件 I/O 操作</li>
* <li>PIPE / DUP 管道和 FD 复制</li>
* <li>SOCKET / CONNECT / BIND / LISTEN / ACCEPT 网络套接字</li>
* <li>SELECT I/O 多路复用</li>
* </ul>
*/ */
public class SyscallCommand implements Command { public class SyscallCommand implements Command {
/*------------------------------------ 工具方法 ------------------------------------*/
/**
* <b>POSIX open 标志到 Java NIO OpenOption 的映射</b>
* <p>
* Linux/UNIX open 调用 flags 参数转换为 Java NIO OpenOption 集合
* 目前仅支持 WRITE0x1READCREATE0x40TRUNCATE0x200APPEND0x400等常用标志
* </p>
*
* @param flags POSIX 风格 open 标志 O_WRONLY=0x1, O_CREAT=0x40
* @return 映射后的 OpenOption 集合
*/
private static Set<OpenOption> flagsToOptions(int flags) {
Set<OpenOption> opts = new HashSet<>();
// 0x1 = WRITE否则默认 READ
if ((flags & 0x1) != 0) opts.add(WRITE);
else opts.add(READ);
if ((flags & 0x40) != 0) opts.add(CREATE);
if ((flags & 0x200) != 0) opts.add(TRUNCATE_EXISTING);
if ((flags & 0x400) != 0) opts.add(APPEND);
return opts;
}
/**
* <b>统一异常处理</b>
* <p>
* 捕获 syscall 内部所有异常 -1 压入操作数栈表示系统调用失败暂不区分错误类型
* 常见异常如文件不存在权限不足通道类型不符网络故障等
* </p>
*
* @param stack 当前操作数栈
* @param e 捕获的异常对象
*/
private static void pushErr(OperandStack stack, Exception e) {
stack.push(-1); // 目前统一用 -1后续可按异常类型/errno 映射
}
/*--------------------------------------------------------------------------------*/
/**
* <b>执行 SYSCALL 子命令</b>
* <p>
* 按照 parts[1] 指定的 SYSCALL 子命令结合虚拟机栈与资源表完成对应的系统调用模拟
* 支持基础文件网络管道等 I/O 操作并对异常统一处理
* </p>
*
* @param parts 指令字符串数组parts[1] 为子命令
* @param pc 当前指令计数器
* @param stack 操作数栈
* @param locals 局部变量表
* @param callStack 调用栈
* @return 下一条指令的 pc 默认 pc+1
*/
@Override @Override
public int execute(String[] parts, int currentPC, public int execute(String[] parts, int pc,
OperandStack operandStack, OperandStack stack,
LocalVariableStore localVariableStore, LocalVariableStore locals,
CallStack callStack) { CallStack callStack) {
if (parts.length < 2) if (parts.length < 2)
throw new IllegalArgumentException("SYSCALL requires a sub-command"); throw new IllegalArgumentException("SYSCALL needs sub-command");
String subCmd = parts[1].toUpperCase(); String cmd = parts[1].toUpperCase(Locale.ROOT);
switch (subCmd) { try {
case "PRINT": switch (cmd) {
case "PRINTLN": {
boolean newline = subCmd.equals("PRINTLN");
String output;
// 指令里直接带字符串常量 /*==================== 文件 / 目录 ====================*/
if (parts.length > 2) {
output = String.join(" ", Arrays.copyOfRange(parts, 2, parts.length)); /*
if (output.length() >= 2 && * OPEN 打开文件返回文件描述符File Descriptor, FD
output.startsWith("\"") && * 入栈push顺序: path文件路径, String, flagsint, modeint文件权限暂未用
output.endsWith("\"")) { * 出栈pop顺序: mode flags path
output = output.substring(1, output.length() - 1); // 去掉首尾引号 * 成功返回 fd失败返回 -1
*/
case "OPEN" -> {
int mode = (Integer) stack.pop(); // 目前未用
int flags = (Integer) stack.pop();
String path = String.valueOf(stack.pop());
FileChannel fc = FileChannel.open(Paths.get(path), flagsToOptions(flags));
stack.push(FDTable.register(fc));
}
/*
* CLOSE 关闭文件描述符
* 入栈顺序: fdint
* 出栈顺序: fd
* 返回 0 成功-1 失败
*/
case "CLOSE" -> {
int fd = (Integer) stack.pop();
FDTable.close(fd);
stack.push(0);
}
/*
* READ fd 读取指定字节数
* 入栈顺序: fdint, countint字节数
* 出栈顺序: count fd
* 返回byte[]实际读取的数据EOF 时长度为 0
*/
case "READ" -> {
int count = (Integer) stack.pop();
int fd = (Integer) stack.pop();
Channel ch = FDTable.get(fd);
if (!(ch instanceof ReadableByteChannel rch)) {
stack.push(new byte[0]);
break;
} }
ByteBuffer buf = ByteBuffer.allocate(count);
int n = rch.read(buf);
if (n < 0) n = 0;
buf.flip();
byte[] out = new byte[n];
buf.get(out);
stack.push(out);
} }
// 没带常量则弹栈打印 /*
else { * WRITE fd 写数据
Object value = operandStack.pop(); * 入栈顺序: fdint, databyte[] String
output = String.valueOf(value); * 出栈顺序: data fd
* 返回写入字节数失败 -1
*/
case "WRITE" -> {
Object dataObj = stack.pop();
int fd = (Integer) stack.pop();
byte[] data = (dataObj instanceof byte[] b)
? b
: String.valueOf(dataObj).getBytes();
Channel ch = FDTable.get(fd);
if (!(ch instanceof WritableByteChannel wch)) {
stack.push(-1);
break;
}
int written = wch.write(ByteBuffer.wrap(data));
stack.push(written);
}
/*
* SEEK 文件定位移动文件读写指针
* 入栈顺序: fdint, offsetlong/int, whenceint, 0=SET,1=CUR,2=END
* 出栈顺序: whence offset fd
* 返回新位置long失败 -1
*/
case "SEEK" -> {
int whence = (Integer) stack.pop();
long off = ((Number) stack.pop()).longValue();
int fd = (Integer) stack.pop();
Channel ch = FDTable.get(fd);
if (!(ch instanceof SeekableByteChannel sbc)) {
stack.push(-1);
break;
}
SeekableByteChannel newPos = switch (whence) {
case 0 -> sbc.position(off);
case 1 -> sbc.position(sbc.position() + off);
case 2 -> sbc.position(sbc.size() + off);
default -> throw new IllegalArgumentException("bad whence");
};
stack.push(newPos);
} }
if (newline) System.out.println(output); /*==================== 管道 / FD 相关 ====================*/
else System.out.print(output);
break; /*
* PIPE 创建匿名管道
* 入栈顺序:
* 出栈顺序:
* 返回顺序: write fd先压栈read fd后压栈
*/
case "PIPE" -> {
Pipe p = Pipe.open();
stack.push(FDTable.register(p.sink())); // write fd
stack.push(FDTable.register(p.source())); // read fd
}
/*
* DUP 复制一个已存在的 fddup
* 入栈顺序: oldfdint
* 出栈顺序: oldfd
* 返回新的 fd失败 -1
*/
case "DUP" -> {
int oldfd = (Integer) stack.pop();
stack.push(FDTable.dup(oldfd));
}
/*==================== 网络相关 ====================*/
/*
* SOCKET 创建套接字支持 stream/dgram
* 入栈顺序: domainint, AF_INET=2等, typeint, 1=STREAM,2=DGRAM, protocolint, 预留
* 出栈顺序: protocol type domain
* 返回 fd
*/
case "SOCKET" -> {
int proto = (Integer) stack.pop(); // 预留暂不用
int type = (Integer) stack.pop(); // 1=STREAM,2=DGRAM
int domain = (Integer) stack.pop(); // AF_INET=2
Channel ch = (type == 1)
? SocketChannel.open()
: DatagramChannel.open();
stack.push(FDTable.register(ch));
}
/*
* CONNECT 发起 TCP 连接
* 入栈顺序: fdint, hostString, portint
* 出栈顺序: port host fd
* 返回 0 成功-1 失败
*/
case "CONNECT" -> {
int port = (Integer) stack.pop();
String host = String.valueOf(stack.pop());
int fd = (Integer) stack.pop();
Channel ch = FDTable.get(fd);
if (ch instanceof SocketChannel sc) {
sc.connect(new InetSocketAddress(host, port));
stack.push(0);
} else stack.push(-1);
}
/*
* BIND 绑定端口
* 入栈顺序: fdint, hostString, portint
* 出栈顺序: port host fd
* 返回 0 成功-1 失败
*/
case "BIND" -> {
int port = (Integer) stack.pop();
String host = String.valueOf(stack.pop());
int fd = (Integer) stack.pop();
Channel ch = FDTable.get(fd);
if (ch instanceof ServerSocketChannel ssc) {
ssc.bind(new InetSocketAddress(host, port));
stack.push(0);
} else stack.push(-1);
}
/*
* LISTEN 监听 socket兼容 backlog
* 入栈顺序: fdint, backlogint
* 出栈顺序: backlog fd
* 返回 0 成功-1 失败
* <b>注意Java NIO 打开 ServerSocketChannel 已自动监听 backlog 处理行为和 UNIX 有区别</b>
*/
case "LISTEN" -> {
int backlog = (Integer) stack.pop();
int fd = (Integer) stack.pop();
Channel ch = FDTable.get(fd);
if (ch instanceof ServerSocketChannel) stack.push(0);
else stack.push(-1);
}
/*
* ACCEPT 接收连接
* 入栈顺序: fdint
* 出栈顺序: fd
* 返回新连接 fd失败 -1
*/
case "ACCEPT" -> {
int fd = (Integer) stack.pop();
Channel ch = FDTable.get(fd);
if (ch instanceof ServerSocketChannel ssc) {
SocketChannel cli = ssc.accept();
stack.push(FDTable.register(cli));
} else stack.push(-1);
}
/*==================== 多路复用 ====================*/
/*
* SELECT I/O 多路复用监视多个 fd 是否可读写
* 入栈顺序: fdsList<Integer>, timeout_mslong
* 出栈顺序: timeout_ms fds
* 返回: 就绪 fd 列表List<Integer>
*/
case "SELECT" -> {
long timeout = ((Number) stack.pop()).longValue();
@SuppressWarnings("unchecked")
List<Integer> fds = (List<Integer>) stack.pop();
Selector sel = Selector.open();
for (int fd : fds) {
Channel c = FDTable.get(fd);
if (c instanceof SelectableChannel sc) {
sc.configureBlocking(false);
int ops = (c instanceof ReadableByteChannel ? SelectionKey.OP_READ : 0)
| (c instanceof WritableByteChannel ? SelectionKey.OP_WRITE : 0);
sc.register(sel, ops, fd);
}
}
int ready = sel.select(timeout);
List<Integer> readyFds = new ArrayList<>();
if (ready > 0) {
for (SelectionKey k : sel.selectedKeys())
readyFds.add((Integer) k.attachment());
}
stack.push(readyFds);
sel.close();
}
/*==================== 控制台输出 ====================*/
/*
* PRINT 控制台输出无换行
* 入栈顺序: dataString byte[]
* 出栈顺序: data
* 返回 0
*/
case "PRINT" -> {
Object dataObj = stack.pop();
if (dataObj instanceof byte[] b) {
System.out.print(new String(b));
} else {
System.out.print(dataObj);
}
stack.push(0); // 表示成功
}
/*
* PRINTLN 控制台输出自动换行
* 入栈顺序: dataString byte[]
* 出栈顺序: data
* 返回 0
*/
case "PRINTLN" -> {
Object dataObj = stack.pop();
if (dataObj instanceof byte[] b) {
System.out.println(new String(b));
} else {
System.out.println(dataObj);
}
stack.push(0); // 表示成功
}
/*==================== 其它未实现/扩展命令 ====================*/
/*
* TODO: PRINT / PRINTLN / 其它自定义 syscall 子命令未实现
*/
default -> throw new UnsupportedOperationException("Unsupported SYSCALL: " + cmd);
} }
} catch (Exception e) {
default: // 统一异常处理异常时压入 -1
throw new UnsupportedOperationException("Unsupported SYSCALL: " + subCmd); pushErr(stack, e);
} }
// 下一条指令 // 默认下一条指令
return currentPC + 1; return pc + 1;
} }
} }

View File

@ -0,0 +1,62 @@
package org.jcnc.snow.vm.io;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.nio.channels.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 维护 虚拟 fd Java NIO Channel 的全局映射表
*
* <pre>
* 0 stdin (ReadableByteChannel)
* 1 stdout (WritableByteChannel)
* 2 stderr (WritableByteChannel)
* 3+ 运行期动态分配
* </pre>
*/
public final class FDTable {
private FDTable() {}
/** 下一次可用 fd02 保留给标准流) */
private static final AtomicInteger NEXT_FD = new AtomicInteger(3);
/** 主映射表fd → Channel */
private static final ConcurrentHashMap<Integer, Channel> MAP = new ConcurrentHashMap<>();
static {
// JVM 标准流包装成 NIO Channel 后放入表中
MAP.put(0, Channels.newChannel(new BufferedInputStream(System.in)));
MAP.put(1, Channels.newChannel(new BufferedOutputStream(System.out)));
MAP.put(2, Channels.newChannel(new BufferedOutputStream(System.err)));
}
/** 注册新 Channel返回分配到的虚拟 fd */
public static int register(Channel ch) {
int fd = NEXT_FD.getAndIncrement();
MAP.put(fd, ch);
return fd;
}
/** 取得 Channel如果 fd 不存在则返回 null */
public static Channel get(int fd) {
return MAP.get(fd);
}
/** 关闭并移除 fd02 忽略) */
public static void close(int fd) throws IOException {
if (fd <= 2) return; // 标准流交由宿主 JVM 维护
Channel ch = MAP.remove(fd);
if (ch != null && ch.isOpen()) ch.close();
}
/** 类似 dup(oldfd) —— 返回指向同一 Channel 的新 fd */
public static int dup(int oldfd) {
Channel ch = MAP.get(oldfd);
if (ch == null)
throw new IllegalArgumentException("Bad fd: " + oldfd);
return register(ch); // 多个 fd 引用同一 Channel
}
}