使用Java NIO直接访问Windows磁盘

8
我正在使用一个库,它使用Java NIO直接将文件映射到内存中,但我在尝试直接读取磁盘时遇到了问题。
我可以使用UNC(网络文件路径)和FileInputStream直接读取磁盘,例如:
File disk = new File("\\\\.\\PhysicalDrive0\\");
try (FileInputStream fis = new FileInputStream(disk);
    BufferedInputStream bis = new BufferedInputStream(fis)) {
    byte[] somebytes = new byte[10];
    bis.read(somebytes);
} catch (Exception ex) {
    System.out.println("Oh bother");
}

然而,我无法将这个方法扩展到 NIO:

File disk = new File("\\\\.\\PhysicalDrive0\\");
Path path = disk.toPath();
try (FileChannel fc = FileChannel.open(path, StandardOpenOption.READ)){
    System.out.println("No exceptions! Yay!");
} catch (Exception ex) {
    System.out.println("Oh bother");
}

堆栈跟踪(直到原因)为:
java.nio.file.FileSystemException: \\.\PhysicalDrive0\: The parameter is incorrect.

at sun.nio.fs.WindowsException.translateToIOException(WindowsException.java:86)
at sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:97)
at sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:102)
at sun.nio.fs.WindowsFileSystemProvider.newFileChannel(WindowsFileSystemProvider.java:115)
at java.nio.channels.FileChannel.open(FileChannel.java:287)
at java.nio.channels.FileChannel.open(FileChannel.java:334)
at hdreader.HDReader.testcode(HDReader.java:147)

我虽然在如何从Java访问磁盘上特定的原始数据看到了类似的内容,但还是没能找到解决方案。 Daniel Alder的答案建议使用GLOBALROOT,因为该答案在答案中使用了FileChannel,但我似乎无法使用此模式找到驱动器。是否有一种方法可以列出GLOBALROOT下的所有设备或类似于此的东西?
目前,我正在查看使用普通InputStream替换NIO的用法,但如果可能的话,我想避免这样做。首先,NIO有其使用原因,其次,它运行了大量代码并需要大量工作。最后,我想知道如何实现类似于Daniel解决方案的操作,以便将来可以写入设备或使用NIO。
因此,总结一下:如何使用Java NIO直接访问驱动器(而不是InputStream),或者是否有一种方法可以列出通过GLOBALROOT可访问的所有设备,以便我可以使用Daniel Alser的解决方案? 回答总结: 我保留了过去的编辑(下面)以避免混淆。在EJP和Apangin的帮助下,我认为我有一个可行的解决方案。类似于以下内容:
private void rafMethod(long posn) {
    ByteBuffer buffer = ByteBuffer.allocate(512);
    buffer.rewind();
    try (RandomAccessFile raf = new RandomAccessFile(disk.getPath(), "r");
            SeekableByteChannel sbc = raf.getChannel()) {
        sbc.read(buffer);
    } catch (Exception ex) {
        System.out.println("Oh bother: " + ex);
        ex.printStackTrace();
    }

    return buffer;
}

只要posn参数是扇区大小(在这种情况下设置为512)的倍数,这将起作用。请注意,这也适用于Channels.newChannel(FileInputStream),在这种情况下似乎总是返回一个SeekableByteStream,并且将其转换为一个似乎是安全的操作。
从快速而粗略的测试中,这些方法似乎确实进行了查找,而不仅仅是跳过。我在驱动器开头搜索了一千个位置并读取了它们。我做了同样的事情,但增加了磁盘大小的一半的偏移量(以搜索磁盘的后半部分)。我发现:
- 两种方法几乎花费相同的时间。 - 搜索磁盘的开头或结尾并不影响时间。 - 减少地址范围会减少时间。 - 对地址进行排序会减少时间,但不会减少多少。
这向我表明,这确实是寻址而不仅仅是读取和跳过(因为流倾向于那样做)。在这个阶段速度仍然很慢,它使我的硬盘听起来像一台洗衣机,但代码被设计为快速测试,尚未美化。它可能仍然正常工作。
感谢EJP和Apangin的帮助。请在他们各自的答案中阅读更多内容。
编辑: 我后来在Windows 7机器上运行了我的代码(起初我没有),并获得了稍微不同的异常(请参见下文)。这是在管理员权限下运行的,第一段代码在相同的条件下仍然有效。
java.nio.file.FileSystemException: \\.\PhysicalDrive0\: A device attached to the system is not functioning.

    at sun.nio.fs.WindowsException.translateToIOException(WindowsException.java:86)
    at sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:97)
    at sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:102)
    at sun.nio.fs.WindowsFileSystemProvider.newFileChannel(WindowsFileSystemProvider.java:115)
    at java.nio.channels.FileChannel.open(FileChannel.java:287)
    at java.nio.channels.FileChannel.open(FileChannel.java:335)
    at testapp.TestApp.doStuff(TestApp.java:30)
    at testapp.TestApp.main(TestApp.java:24)

编辑2: 针对EJP的回复,我已经尝试过:

    byte[] bytes = new byte[20];
    ByteBuffer bb = ByteBuffer.wrap(bytes);
    bb.rewind();

    File disk = new File("\\\\.\\PhysicalDrive0\\");
    try (FileInputStream fis = new FileInputStream(disk);
            ReadableByteChannel rbc = Channels.newChannel(new FileInputStream(disk))) {

        System.out.println("Channel created");
        int read = rbc.read(bb);
        System.out.println("Read " + read + " bytes");
        System.out.println("No exceptions! Yay!");
    } catch (Exception ex) {
        System.out.println("Oh bother: " + ex);
    }

当我尝试这样做时,我得到了以下输出:

通道已创建
哎呀:java.io.IOException: 参数不正确

因此,似乎我可以创建FileChannel或ReadableByteChannel,但无法使用它们;也就是说,错误只是被推迟了。

你没有尝试我的实际代码。像我一样使用4096的缓冲区大小来尝试它。你可能需要读取精确的磁盘块倍数。 - user207421
我敢打赌,原始磁盘驱动程序不支持部分块读取。如果这是您的需求,最好使用FileInput/OutputStreams并在它们周围使用BufferedInput/OutputStreams - user207421
FileInput流不幸不能胜任这项工作。对于从头到尾的读取,它还可以,但是随机访问存在问题。它要么不允许,要么需要很长时间。如果有其他跨平台的Java解决方案,请考虑。我并不局限于NIO。 - timbo
重新编译不会加速您的代码,但使用更新的JDK可能会有所帮助。 - user207421
是的,抱歉,那就是我想说的。使用更新的JDK重新编译。我之所以说重新编译是因为(虽然我可能错了),我认为仅仅更改JRE是不够的。实际上,必须使用更新的JDK重新编译才能产生影响。 - timbo
显示剩余4条评论
3个回答

4

当直接访问物理驱动器而不进行缓冲时,只能读取完整的扇区。这意味着,如果一个扇区大小为512字节,则只能读取512字节的倍数。将您的缓冲区长度更改为512或4096(无论您的扇区大小是多少),FileChannel就可以正常工作:

ByteBuffer buf = ByteBuffer.allocate(512);

try (RandomAccessFile raf = new RandomAccessFile("\\\\.\\PhysicalDrive0", "r");
     FileChannel fc = raf.getChannel()) {
    fc.read(buf);
    System.out.println("It worked! Read bytes: " + buf.position());
} catch (Exception e) {
    e.printStackTrace();
}

请查看对齐和文件访问要求
您的原始FileInputStream代码显然有效,因为其中包含了默认缓冲区大小为8192的BufferedInputStream。如果去掉这部分内容,代码将出现相同的异常。

有没有直接访问磁盘扇区大小的方法?还是必须使用本地方法? - LppEdd
1
@LppEdd 请参考这个问题。或者通过尝试读取并捕获异常来探测常见的大小。 - apangin

1

请以管理员身份运行此程序。它确实有效,因为它只是 java.io 的薄包装器:

    try (FileInputStream fis = new FileInputStream(disk);
        ReadableByteChannel fc = Channels.newChannel(fis))
    {
        System.out.println("No exceptions! Yay!");
        ByteBuffer  bb = ByteBuffer.allocate(4096);
        int count = fc.read(bb);
        System.out.println("read count="+count);
    }
    catch (Exception ex)
    {
        System.out.println("Oh bother: "+ex);
        ex.printStackTrace();
    }
编辑 如果你需要随机访问,就必须使用RandomAccessFile。通过Channels无法实现这种映射。但是上面的解决方案本身并不是NIO,只是在FileInput/OutputStream上加了一层Java NIO。

谢谢EJP,我已经做到了。当我第一次提出问题时,我使用的是XP(没有Windows 7)。当我在Windows 7上运行它时,我得到了你提到的错误。然后我以管理员身份运行,并得到了一个新的错误:“系统连接的设备无法正常工作。”(请参见我的帖子底部)。显然,设备正在工作,因为FileStream可以工作,而且它也是我的操作系统磁盘。顺便说一下,磁盘1、2和3上发生的情况都是一样的。 - timbo
好的,我会尝试剪切和粘贴。 - timbo
“从那里没有通过通道的映射” - RandomAccessFile.getChannel() 呢? - apangin
@EJP,你还没有回答我的问题。你也没有回答OP的问题。 - apangin
如果您无法准确阅读原始问题或我的评论,那么重申所有内容可能毫无意义。但是,为了进一步帮助您,当通过任何方式创建原始磁盘的FileChannel时,OP会收到“系统中连接的设备未能正常运行”的错误消息,并且一旦他解决您提到的琐碎访问问题,他也将通过您的答案得到该错误消息。我同意,我的回答也没有回答问题,因为它不让他执行随机访问。显然,这没有答案。 - user207421
显示剩余5条评论

1
使用NIO,您的原始代码只需要稍作修改即可。
Path disk = Paths.get("d:\\.");
try (ByteChannel bc = Files.newByteChannel(disk, StandardOpenOption.READ)) {
    ByteBuffer buffer = ByteBuffer.allocate(10);
    bc.read(buffer);
} catch (Exception e){
    e.printStackTrace();
}

代码是可以工作的,但是我在你的版本和我的版本中都遇到了访问被拒绝的错误。


谢谢你的回答,史蒂夫。我不认为问题出在引用(\.\PhysicalDrive0\)上,但我会尝试任何方法。我使用你的代码时也遇到了同样的错误。第一段使用FileInputStream的代码确实可以使用该引用,但不幸的是,FileInputStream无法满足我的需求。如果问题出在引用上,我就不会期望它能正常工作。此外,当我使用无效的引用时,我会得到不同的错误提示。 - timbo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接