【问题标题】:What exactly causes a 'spin on suspend' error in Android?究竟是什么导致了 Android 中的“旋转挂起”错误?
【发布时间】:2013-12-26 19:52:18
【问题描述】:

我目前在调试一些依赖本机库的 Android 代码时遇到了问题。特别是一个本机调用似乎容易出现这种“暂停时旋转”错误。一般表现为:

threadid=2: spin on suspend #2 threadid=48 (pcf=3)

到目前为止,我还无法准确确定这里出了什么问题,除了在大约 10 条这些消息之后,我的应用程序遇到 SIGSTKFLT 并退出。每次,第一个线程是 GC,第二个线程是当前正在执行本机代码的任何线程。与此消息一起打印的堆栈部分始终在堆栈顶部具有本机方法。

Dalvik 抱怨这件事时到底发生了什么,我该如何开始调试原因以便修复它?

编辑:一个有趣的问题——在原生开发者进行一些更改后,我现在有时也会看到以下错误:

PopFrame missed the break
VM aborting
Fatal signal 11 (SIGSEGV) at 0xdeadd00d (code=1)

我也很奇怪,线程转储在堆栈顶部显示了我的本机方法,但线程状态是RUNNABLE,而不是NATIVE——这怎么可能?

【问题讨论】:

    标签: android java-native-interface dalvik


    【解决方案1】:

    基本问题是 Dalvik 是一个安全点挂起 VM,并使用“停止世界”垃圾收集。这意味着,要让 GC 运行,它必须等待所有线程到达可以确定它们不会改变堆的点。

    由于某种原因,您的一个线程没有响应 GC 线程的挂起请求。它实际上并没有在本机代码中执行;如果是,线程将处于NATIVE 状态,这被认为是安全的。 (所有对本机堆的访问都通过 JNI 调用进行门控,并且所有 JNI 调用都会进行挂起检查。)

    出于性能原因,JIT 能够以跳过挂起检查的方式将已编译代码块链接在一起。如果一个线程挂起的时间过长,挂起的线程将“解链”这些块,并等待更长的时间。最终它开始抱怨,并最终放弃并中止虚拟机。

    某些设备使用vendor-modified version of Dalvik 会出错,并且在紧密循环中可能会发生中止。在这种情况下,我不希望在堆栈顶部看到本机方法。

    你最好的调试方法是在它不满意的地方附加 gdb 并尝试找出目标线程在做什么。有可能本机代码以某种方式破坏了 VM 状态或返回堆栈,等等它从本机代码返回时线程被卡住了。

    编辑后更新:dvmPopFrame() 函数用于从托管堆栈弹出堆栈帧。当 VM 调用您的本机方法时,它会插入一个“中断”帧,以便在展开堆栈以进行异常处理时,VM 不会吹过调用站点。 (它也用于 VM 发出的托管代码方法调用,例如用于反射或<clinit>。)消息PopFrame missed the break 表示未找到中断帧。

    中断帧有一个空方法指针。展开堆栈时,dvmPopFrame() 只要看到一个非空方法指针(意味着它不是中断帧)和一个非空前一帧指针(意味着您还没有到达堆栈顶部),就会继续.如果你到达栈顶,你就错过了中断——所有的 Dalvik 栈都以一个真实的方法开始(如果线程通过 JNI 连接到 VM,有时是一个“假”的方法)。

    所以我的猜测是本机代码破坏了堆栈,使前一帧指针为空。解决这个问题的一种技术是让 VM 调用一个调用实际本地方法的本地方法。 “中间人”在堆栈上分配一些东西,将其设置为已知值,调用实际方法,然后在返回之前验证其堆栈分配是否未更改。

    (可能需要使用这些值来防止编译器优化它们;如果你使用类似的东西:

    if (jniEnv == NULL) {
        printf("my stuff is ...", ...);
    }
    

    那么它永远不会真正运行,因为JNIEnv* 永远不会为空......但编译器不知道这一点。)

    有关 Dalvik 堆栈布局的完整说明,请参阅 dalvik/vm/interp/Stack.h

    从本机代码返回时,线程在RUNNABLE 中是正常的。您的本机方法仍在顶部,因为弹出它的代码失败并中止了 VM。

    【讨论】:

    • 那么,如果我的本机代码正在破坏 VM 状态,为什么它不会持续失败?有时调用会很好地返回(可能是因为此时没有发生 GC)。我不应该得到一个段错误或类似的吗?
    • 一切皆有可能。第一步是弄清楚“卡住”的线程实际上在做什么。您将在线程转储中看到一个“sysTid”值——即 Linux 线程 ID,您还将在 gdb 中看到它。
    • 我想我已经掌握了堆栈是如何被丢弃的——我注意到在 JNI 代码中,开发人员将 JNIEnv* 指针隐藏在一个静态字段中,然后在另一个本机方法中重用它调用(发生在不同的线程上)。我之所以发现这一点,是因为当第二个 JNI 方法调用回 Java 时,我有一个线程“复活”了。
    • 如果启用 CheckJNI 功能,当发生这种情况时,您会收到强烈的警报。见developer.android.com/training/articles/…
    • 我刚刚投了赞成票。我认为我从未见过关于我应该知道的主题的如此全面的答案,而且我显然没有。 fadden 有一些很酷的经历!
    猜你喜欢
    • 2013-07-20
    • 2013-06-05
    • 2016-05-18
    • 2023-01-04
    • 2010-11-18
    • 2010-12-29
    • 1970-01-01
    • 2014-06-15
    • 1970-01-01
    相关资源
    最近更新 更多