ART是如何保证checkpoint 点一定会被跑到的?

关注者
42
被浏览
2012
题主是想问checkpoint还是safepoint?

对Android Runtime(ART)的编译器而言,其编译的代码有一种方式可以定期轮询runtime看要不要做某些特定的操作。这种方式叫做safepoint。编译后的代码里的safepoint既可用于实现stop-the-world(暂停所有Java线程),也可以用于实现checkpoint(暂停一个Java线程);ART的safepoint还可以用于实现deoptimization。
ART的解释器也有对safepoint的实现,原理类似。

关于safepoint,可以跳这俩传送门参考些背景信息:

=========================================

从编译器和解释器的角度看,ART的safepoint有两种:
  • 主动safepoint:编译生成的代码里或者解释代码里有主动检查safepoint的动作,并在发现需要进入safepoint时跳转到相应的处理程序里。
    • ART的解释器安插主动safepoint的位置在循环的回跳处(backedge,具体来说是在跳转前的源头处)以及方法返回处(return / throw exception)。
    • ART Optimizing Compiler安插主动safepoint的位置在循环回跳处(backedge,具体来说是在跳转前的源头处)以及方法入口处(entry)。
  • 被动safepoint:所有未内联的方法调用点(call site)都是被动safepoint。这里并没有任何需要主动执行的代码,而就是个普通的方法调用。
    • 之所以要作为safepoint,是因为执行到方法调用点之后,控制就交给了被调用的方法,而被调用的方法可能会进入safepoint,safepoint中可能需要遍历栈帧,因此caller也必须处于safepoint。

安插safepoint的位置的思路是:程序要能够在runtime发出需要safepoint的请求后,及时地执行到最近的safepoint然后把控制权交给runtime。
怎样算“及时”?只要执行时间是有上限(bounded)就可以了,实时性要求并不是很高
于是进一步假设,向前执行(直线型、带条件分支都算)的代码都会在有限时间内执行完,所以可以不用管;而可能导致长时间执行的代码,要么是循环,要么是方法调用,所以只要在这两种地方插入safepoint就可以保证及时性了。
至于具体在方法入口还是出口、循环回边的源头还是目标处插入safepoint,这是个具体实现的细节,只要选择一边插入就可以了。

ART的Optimizing Compiler的IR里,表示主动safepoint的IR指令是SuspendCheck,而表示被动safepoint的IR指令就是各种call。
到代码生成的时候,SuspendCheck也会被当作一种call看待(kCallOnSlowPath),于是后面的逻辑就是只要有call的地方就要记录下safepoint信息。记录safepoint元数据信息的逻辑在寄存器分配的过程中,具体在 RegisterAllocatorLinearScan::ProcessInstruction(HInstruction* instruction)。

目前的ART Optimizing Compiler为SuspendCheck所生成的代码,大致逻辑是这样的:
  if (Thread::Current()->tls32_.state_and_flags.as_struct.flags != 0) { // explicit poll
    // art_quick_test_suspend
    artTestSuspendFromCode(Thread::Current());              // enter safepoint
  }
这种生成显式的检查+条件分支的做法叫做explicit polling。

以前的ART曾经用过另一种做法,对某个指针做间接访问,如果该指针为nullptr则触发SIGSEGV,然后在signal handler里进入safepoint。这种做法叫做implicit polling,因为生成的代码没有任何条件分支。其生成的代码大致逻辑是这样的:
  temp = *Thread::Current()->tlsPtr_.suspend_trigger;
没有任何条件分支对不对?正常运行时suspend_trigger为一个指向正常可访问地址的指针;当runtime要触发suspend check时,只要把suspend_trigger设为nullptr,就可以触发suspend check一侧发生SIGSEGV,然后在SuspensionHandler::Action()里会把signal handler的continuation address设置为art_quick_test_suspend,也就是artTestSuspendFromCode。
要留意:runtime里的Thread::TriggerSuspend() / Thread::RemoveSuspendTrigger() 都只有在implicit polling的情况下才起作用;在explicit polling下它是不起任何作用的。
最初在ART里实现implicit polling的版本是在这里:android.googlesource.com

在Lollipop版ART里,采用上面哪种做法是可以根据目标架构而配置的,但在官方版ART所支持的平台上都没有开启implicit_suspend_checks。该版本里的ART Quick编译器对应的逻辑是:
/* Check if we need to check for pending suspend request */
void Mir2Lir::GenSuspendTest(int opt_flags) {
  if (!cu_->compiler_driver->GetCompilerOptions().GetImplicitSuspendChecks()) {
    if (NO_SUSPEND || (opt_flags & MIR_IGNORE_SUSPEND_CHECK)) {
      return;
    }
    FlushAllRegs();
    LIR* branch = OpTestSuspend(NULL);
    LIR* cont = NewLIR0(kPseudoTargetLabel);
    AddSlowPath(new (arena_) SuspendCheckSlowPath(this, branch, cont));
  } else {
    if (NO_SUSPEND || (opt_flags & MIR_IGNORE_SUSPEND_CHECK)) {
      return;
    }
    FlushAllRegs();     // TODO: needed?
    LIR* inst = CheckSuspendUsingLoad();
    MarkSafepointPC(inst);
  }
}

最新的ART里已经没有对implicit suspend check的任何实质支持了;没有任何代码检查GetImplicitSuspendChecks()。