在Java中设置阻塞文件读取

7

我想在Java中设置一个阻止文件读取的操作。也就是说,当使用FileInputStream并调用任何read()方法时,调用会被阻塞。

我无法想到一个简单的与操作系统无关的方式 - 在类Unix的操作系统中,我可以尝试使用mkfifo创建一个FIFO并从该文件中读取。一个可能的解决方法是只需创建一个非常大的文件并从其中读取 - 在捕获堆栈之前读取不太可能完成,但它很丑陋且速度较慢(实际上,当缓存时读取仍然可以非常快)。

相应套接字read()情况很容易设置 - 自己创建套接字并从中读取,您可以具有确定性阻止。

目的是检查方法的堆栈以确定这种情况下的顶部框架是什么。想象一下,我有一个组件,定期采样所有正在运行的线程的堆栈跟踪,然后尝试分类线程此刻正在做什么。它可能正在进行文件IO。因此,我需要知道在文件IO期间“堆栈顶部”的外观是什么。我已经通过实验确定了这一点(只是以各种方式读取文件并采样堆栈),但我希望编写一个测试,如果有任何更改,该测试将失败。

编写此类测试的自然方式是启动执行文件读取的线程,然后检查顶部框架。要可靠地执行此操作,我需要进行阻塞读取(否则线程可能在捕获堆栈跟踪之前完成其读取等)。


你能解释一下阻塞吗?它应该阻塞任何Java应用程序,还是仅在您的应用程序的一个实例中阻塞? - maslan
我会在主问题中更新详细信息。 - BeeOnRope
我想不出在Windows上实现你想要的方式(这是你面临的操作系统独立性问题,我认为)。根据你的描述,我认为抓取FileInputStream.getChannel()并不断重置光标不起作用,因为它会改变一段时间的堆栈跟踪。如果你认为可能行得通,我可以在答案中详细说明。同样地,我猜扩展FileInputStream也行不通,因为如果我理解正确,你正试图“指纹识别”FileInputStream.read*方法。 - J Richard Snape
你是否有使用自己的FileInputStream的能力?那么在实际读取文件之前,您可以获取堆栈并验证它。这似乎也意味着您想要防止在不需要的上下文中访问文件。这通常是SecurityManager的工作。 - M.P. Korstanje
我不想在不需要的情况下防止文件访问。我希望能够线程转储(外部)正在运行的线程,并确定“哦,它正在进行文件IO”。我当然可以使用自己的FIS,但问题是我无法从执行读取的线程中获取转储。根据定义,当_native_ read0方法(或任何其他方法)位于堆栈顶部时,我没有控制权,也无法转储该线程。我当然可以在之前完成它,但这确实使我得到了正确的顶部帧或两个。 - BeeOnRope
啊,我现在明白你想要什么了。在这种情况下,不,无论如何都不能独立于操作系统。 - M.P. Korstanje
6个回答

6

要获取一个保证的被阻塞I/O,请从控制台读取,例如在Linux系统中使用/dev/console或在Windows系统中使用CON

为了使其平台无关,您可以通过修改FileInputStreamFileDescriptor来实现:

    // Open a dummy FileInputStream
    File f = File.createTempFile("dummy", ".tmp");
    f.deleteOnExit();
    FileInputStream fis = new FileInputStream(f);

    // Replace FileInputStream's descriptor with stdin
    Field fd = FileInputStream.class.getDeclaredField("fd");
    fd.setAccessible(true);
    fd.set(fis, FileDescriptor.in);

    System.out.println("Reading...");
    fis.read();
    System.out.println("Complete");

更新

我意识到你甚至不需要一个阻止方法。为了获得正确的堆栈跟踪,您可以在无效的FileInputStream上调用read()

    FileInputStream fis = new FileInputStream(new FileDescriptor());
    fis.read(); // This will throw IOException exactly with the right stacktrace

如果你仍需要一个阻塞式的 read(),使用命名管道是可行的:在 POSIX 系统上使用 Runtime.exec 运行 mkfifo,或者在 Windows 上创建 \\.\PIPE\MyPipeName

我也考虑过这个问题(尽管不是反射交换fd的技巧),但除了标准输入(stdin)外,还有其他可以使用的阻塞流吗?问题在于stdin可能实际上会有一些输入,而我不应该消耗它。 - BeeOnRope
使用无效的文件描述符的想法非常棒,谢谢!我会授予你赏金... - BeeOnRope

1

我不知道有什么方法可以以操作系统无关的方式创建一个始终会在读取时阻塞的文件。

如果我想要找到特定函数被调用时的堆栈跟踪,我会在调试器下运行程序,并在该函数上设置断点。但是,方法断点会减慢程序速度,并且如果时间很重要,会给出与通常结果不同的结果。

如果您可以访问程序源代码,您可以制作一个虚假的FileInputStream,它扩展了真正的FileInputStream但总是在读取时阻塞。您只需要在整个代码中切换导入语句即可。但是,这不会捕获您无法切换导入语句的地方,并且如果有大量代码,则可能会很麻烦。

如果您想在不更改程序源代码或编译的情况下使用自己的FileInputStream,您可以制作一个自定义类加载器,该加载器将您的自定义FileInputStream类加载而不是真正的类。您可以通过以下方式在命令行上指定要使用的类加载器:

java -Djava.system.class.loader=com.test.MyClassLoader xxx

现在我想起来了,我有一个更好的主意,不要制作一个在read()上阻塞的自定义FileInputStream,而是制作一个在read()上打印堆栈跟踪的自定义FileInputStream。然后自定义类可以调用真实版本的read()。这样你就可以得到所有调用的堆栈跟踪。

重点是我想在读取时查看堆栈跟踪。我无法设置断点,因为我不知道确切的方法是什么(而且它可能是本地方法,很难在上面设置断点)。我需要这个自动化测试来验证在进行文件 IO 时顶部帧是否符合预期。 - BeeOnRope
@BeeOnRope 所以你想让读取操作挂起,然后发送一个信号给JVM来转储堆栈跟踪,以确保一切正确?可以将Java断点放在内置于JVM中的方法上。在这里,我认为你想要的是FileInputStream.read()。我正在查看java7的源代码。FileInputStream.read()调用本地函数:read0()。因为听起来你正在进行测试,所以你可能只想以操作系统相关的方式实现它,例如在你的*Nix测试平台上制作一个fifo,在你的Windows平台上制作一个... 咳嗽某些东西。 - OfNothing
是的,虽然我没有使用信号,但是像ThreadMXBean.getThreadInfo()这样的东西可以返回任何线程的堆栈。这是为了自动化测试,所以断点不适用。另一个问题是我试图测试该方法实际上是FileInputStream.read()的整个重点。 - BeeOnRope

1
根据我的理解,您想编写一个测试来检查FileInputStream.read()方法的堆栈跟踪。如果FileInputStream的后代覆盖了read()方法怎么办?
如果您不需要检查后代,则可以使用JVM工具接口在所需方法中运行时插入断点,并在此事件(断点)的事件处理中转储堆栈跟踪。完成转储后,您将删除断点并继续执行。 (所有这些都是在运行时使用此API进行的,没有黑魔法 :) )

0

你可以创建一个单独的线程来监视文件的访问时间变化,并在发生变化时生成JVM线程转储。至于在代码中生成线程转储,我没有尝试过,但看起来这里已经有了答案:在不重启的情况下生成Java线程转储。

我不知道这个解决方案在你的线程之间的时间安排上会有多好,但我想这应该非常接近。我也不能100%确定这个解决方案的操作系统独立性,因为我没有测试过,但它应该适用于大多数现代化的系统。请参阅java.nio.file.attribute.BasicFileAttributes的javadoc,以查看如果不支持将返回什么。


我认为有些误解 - 我想在测试场景中建立一个确定性的阻塞读取,以便我可以拍摄该线程的转储并检查我期望的框架是否在堆栈顶部。倾卸堆栈本身很容易,例如使用ThreadMXBean.getThreadInfo() - BeeOnRope
我理解了"what", 我在尝试回答"How"以获得相同的结果。文件的访问时间将在读取开始时更新,因此如果您在该点生成转储文件,则会按预期获取线程堆栈的顶部,以便您可以根据需要在单元测试中对其进行操作。如果仍然可以获取所需数据,则无需阻止。有阻塞的方法,但很混乱,需要本地交互并使用@OfNothing提到的“假”FileInputStream。 - Foosh
我不认为有任何理由相信,如果我在时间戳更改的那一刻排便,我会得到堆栈顶部的IO调用。我宁愿假设在那个时候IO已经完成了。 - BeeOnRope
当文件被打开进行读取时,访问时间会被更新,而不是像创建和修改操作一样在io操作完成时更新。这很容易通过在*nix系统上使用tail -f命令进行经验测试来验证。 - Foosh

0
一个技巧是:如果有可能修改你的 API 来返回一个 Reader 而非 File,那么你可以用一个自定义的 StringReader(比如说 class SlowAsRubyStringReader extends Reader)来封装一个字符串,并重写各种 int read() 方法,在真正执行操作之前加入 Thread.sleep(500)。当然,这只适用于测试时使用。

@see http://docs.oracle.com/javase/7/docs/api/java/io/StringReader.html

我认为这里存在一个更大的问题,不仅仅是文件:你想要在测试用例中检查调用API时的上下文,对吗?也就是说,你想要能够检查堆栈并说:“啊哈!我抓到你从JustTookABath对象调用MudFactory API了,太过分了!”如果是这种情况,那么你可能需要深入研究动态代理,它将允许你劫持函数调用或使用面向方面的编程(AOP),它允许你以更系统化的方式执行相同的操作。请参见http://en.wikipedia.org/wiki/Pointcut


-1

记录一个跟踪信息是不重要的,重要的是我需要获取实际读取时将位于堆栈顶部的帧(即,顶部框架正在调用本地读取方法)。我不明白为什么我一定要使用本地设置阻塞读取。虽然在链的末端当然有本地方法,但对于套接字来说,在Java中设置起来很容易。 - BeeOnRope
ExceptionUtils 还可以提供堆栈帧。通过查看源代码,您可以完整地了解情况。 - David Soroko
我非常清楚如何获取当前线程的堆栈跟踪。我需要的是,某个线程(称为T1)进入阻塞读取状态,在此时,我将在另一个线程T2上获取T1的堆栈跟踪,以查看读取发生时堆栈顶部有哪些帧。我无法从T1中完成这个操作,因为它被定义为(a)被阻塞和(b)处于无法修改的本地代码中。 - BeeOnRope
这是不可能的,因为T1在一个本地方法中被阻塞了。按定义,在这一点上我不能要求它获取自己的堆栈跟踪... - BeeOnRope
当然,我建议在进入read()之前,T1缓冲堆栈数据。 - David Soroko
显示剩余2条评论

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