虽然说解释执行模式是逐字逐句翻译给目标平台运行的,但这样的过程未免太过缓慢,如果能把字节码说的话做成纸条,运行时只要把对应的纸条交给目标平台就可以了,这样,执行速度就会明显提升。JVM的Hotspot虚拟机的模板解释器就是用这种方法来解释执行的。在开始分析之前,先了解一下JVM的执行方式。

  (1).边解释边运行,即每次解释一条字节码并运行其解释的本地代码,这种执行引擎速度相对很慢 
  (2).JIT(即时编译)具有更快的运行速度但是需要更多的内存,方法被第一次调用时,字节码编译生成的本地代码将会被缓存,这样在该方法下次被调用的时候,将取出缓冲的本地代码直接运行 
  (3).自适应优化,对于经常被调用的方法,则会缓存其编译产生成的本地代码,对于其他较少被调用的代码,仍对其采用解释执行的方法。 
  (4).片上虚拟机,即虚拟机的执行引擎直接嵌入在片上

  HotSpot虚拟机可以配置为以下运行模式: 
    -Xint:解释模式 
    -Xcomp:编译模式 
    -Xmixed:混合模式 
(通过java -version就可以查看虚拟机的运行模式)

  HotSpot在启动时,会为所有字节码创建在目标平台上运行的解释运行的机器码,并存放在CodeCache中,在解释执行字节码的过程中,就会从CodeCache中取出这些本地机器码并执行。

  Hotspot虚拟机的细节技术实现值得借鉴,如果你觉得源码甚至汇编代码比较枯燥的话,也可以大致了解相关模块的组件、工作流程,对相关实现有一定的认识。

  下面就从模板解释器的初始化开始,分析HotSpot的解释代码的生成。
  在创建虚拟机时,在初始化全局模块过程中,会调用interpreter_init()初始化模板解释器,模板解释器的初始化包括抽象解释器AbstractInterpreter的初始化、模板表TemplateTable的初始化、CodeCache的Stub队列StubQueue的初始化、解释器生成器InterpreterGenerator的初始化。

 1 void TemplateInterpreter::initialize() {
 2   if (_code != NULL) return;
 3   // assertions
 4   //...
 5
 6   AbstractInterpreter::initialize();
 7
 8   TemplateTable::initialize();
 9
10   // generate interpreter
11   { ResourceMark rm;
12     TraceTime timer("Interpreter generation", TraceStartupTime);
13     int code_size = InterpreterCodeSize;
14     NOT_PRODUCT(code_size *= 4;)  // debug uses extra interpreter code space
15     _code = new StubQueue(new InterpreterCodeletInterface, code_size, NULL,
16                           "Interpreter");
17     InterpreterGenerator g(_code);
18     if (PrintInterpreter) print();
19   }
20
21   // initialize dispatch table
22   _active_table = _normal_table;
23 }

1.AbstractInterpreter是基于汇编模型的解释器的共同基类,定义了解释器和解释器生成器的抽象接口。 
2.模板表TemplateTable保存了各个字节码的模板(目标代码生成函数和参数)。 
TemplateTable的初始化调用def()将所有字节码的目标代码生成函数和参数保存在_template_table或_template_table_wide(wide指令)模板数组中

  //                              interpr. templates// Java spec bytecodes          ubcp|disp|clvm|iswd  in    out   generator      argumentdef(Bytecodes::_nop           , ____|____|____|____, vtos, vtos, nop           ,  _      );def(Bytecodes::_aconst_null   , ____|____|____|____, vtos, atos, aconst_null   ,  _      );def(Bytecodes::_iconst_m1     , ____|____|____|____, vtos, itos, iconst        , -1      );def(Bytecodes::_iconst_0      , ____|____|____|____, vtos, itos, iconst        ,  0      );def(Bytecodes::_iconst_1      , ____|____|____|____, vtos, itos, iconst        ,  1      );def(Bytecodes::_iconst_2      , ____|____|____|____, vtos, itos, iconst        ,  2      );
//...其他字节码的模板定义

其中,def()是查看数组对应项是否为空,若为空则初始化该数组项。

Template* t = is_wide ? template_for_wide(code) : template_for(code);// setup entryt->initialize(flags, in, out, gen, arg);

_template_table或_template_table_wide的数组项就是Template对象,即字节码的模板,Template的结构如下:

class Template VALUE_OBJ_CLASS_SPEC {private:enum Flags {uses_bcp_bit,                                // set if template needs the bcp pointing to bytecodedoes_dispatch_bit,                           // set if template dispatches on its owncalls_vm_bit,                                // set if template calls the vmwide_bit                                     // set if template belongs to a wide instruction
  };typedef void (*generator)(int arg);int       _flags;                  // describes interpreter template properties (bcp unknown)TosState  _tos_in;                 // tos cache state before template executionTosState  _tos_out;                // tos cache state after  template executiongenerator _gen;                    // template code generatorint       _arg;   

_flags为标志,该项的低四位分别标志:

  • uses_bcp_bit,标志需要使用字节码指针(byte code pointer,数值为字节码基址+字节码偏移量)
  • does_dispatch_bit,标志是否在模板范围内进行转发,如跳转类指令会设置该位
  • calls_vm_bit,标志是否需要调用JVM函数
  • wide_bit,标志是否是wide指令(使用附加字节扩展全局变量索引)

 _tos_in表示模板执行前的TosState(操作数栈栈顶元素的数据类型,TopOfStack,用来检查模板所声明的输出输入类型是否和该函数一致,以确保栈顶元素被正确使用) 
 _tos_out表示模板执行后的TosState 
 _gen表示模板生成器(函数指针) 
 _arg表示模板生成器参数

3.StubQueue是用来保存生成的本地代码的Stub队列,队列每一个元素对应一个InterpreterCodelet对象,InterpreterCodelet对象继承自抽象基类Stub,包含了字节码对应的本地代码以及一些调试和输出信息。 
其内存结构如下:在对齐至CodeEntryAlignment后,紧接着InterpreterCodelet的就是生成的目标代码。

      

4.InterpreterGenerator根据虚拟机使用的解释器模型不同分为别CppInterpreterGenerator和TemplateInterpreterGenerator 
根据不同平台的实现,以x86_64平台为例,TemplateInterpreterGenerator定义在/hotspot/src/cpu/x86/vm/templateInterpreter_x86_64.cpp

1 InterpreterGenerator::InterpreterGenerator(StubQueue* code)
2   : TemplateInterpreterGenerator(code) {
3    generate_all(); // down here so it can be "virtual"
4 }

(1).在TemplateInterpreterGenerator的generate_all()中,将生成一系列JVM运行过程中所执行的一些公共代码和所有字节码的InterpreterCodelet:

  • error exits:出错退出处理入口
  • 字节码追踪入口(配置了-XX:+TraceBytecodes)
  • 函数返回入口
  • JVMTI的EarlyReturn入口
  • 逆优化调用返回入口
  • native调用返回值处理handlers入口
  • continuation入口
  • safepoint入口
  • 异常处理入口
  • 抛出异常入口
  • 方法入口(native方法和非native方法)
  • 字节码入口

(2).其中,set_entry_points_for_all_bytes()会对所有被定义的字节码生成目标代码并设置对应的入口(这里只考虑is_defined的情况)

 1 void TemplateInterpreterGenerator::set_entry_points_for_all_bytes() {
 2   for (int i = 0; i < DispatchTable::length; i++) {
 3     Bytecodes::Code code = (Bytecodes::Code)i;
 4     if (Bytecodes::is_defined(code)) {
 5       set_entry_points(code);
 6     } else {
 7       //未被实现的字节码(操作码)
 8       set_unimplemented(i);
 9     }
10   }
11 }

(3).set_entry_points()将取出该字节码对应的Template模板,并调用set_short_enrty_points()进行处理,并将入口地址保存在转发表(DispatchTable)_normal_table或_wentry_table(使用wide指令)中

 1 void TemplateInterpreterGenerator::set_entry_points(Bytecodes::Code code) {
 2   CodeletMark cm(_masm, Bytecodes::name(code), code);
 3   // initialize entry points
 4   // ... asserts
 5   address bep = _illegal_bytecode_sequence;
 6   address cep = _illegal_bytecode_sequence;
 7   address sep = _illegal_bytecode_sequence;
 8   address aep = _illegal_bytecode_sequence;
 9   address iep = _illegal_bytecode_sequence;
10   address lep = _illegal_bytecode_sequence;
11   address fep = _illegal_bytecode_sequence;
12   address dep = _illegal_bytecode_sequence;
13   address vep = _unimplemented_bytecode;
14   address wep = _unimplemented_bytecode;
15   // code for short & wide version of bytecode
16   if (Bytecodes::is_defined(code)) {
17     Template* t = TemplateTable::template_for(code);
18     assert(t->is_valid(), "just checking");
19     set_short_entry_points(t, bep, cep, sep, aep, iep, lep, fep, dep, vep);
20   }
21   if (Bytecodes::wide_is_defined(code)) {
22     Template* t = TemplateTable::template_for_wide(code);
23     assert(t->is_valid(), "just checking");
24     set_wide_entry_point(t, wep);
25   }
26   // set entry points
27   EntryPoint entry(bep, cep, sep, aep, iep, lep, fep, dep, vep);
28   Interpreter::_normal_table.set_entry(code, entry);
29   Interpreter::_wentry_point[code] = wep;
30 }

这里以非wide指令为例分析set_short_entry_points()。bep(byte entry point), cep, sep, aep, iep, lep, fep, dep, vep分别为指令执行前栈顶元素状态为byte/boolean、char、short、array/reference(对象引用)、int、long、float、double、void类型时的入口地址。

(4).set_short_entry_points()根据操作数栈栈顶元素类型进行判断,首先byte类型、char类型和short类型都应被当做int类型进行处理,对于非void类型将调用generate_and_dispatch()产生目标代码,这里以iconst_0为例对TOS的处理进行介绍: 
对于iconst,其期望的_tos_in(执行前栈顶元素类型)是void类型(vtos),期望的_tos_out(执行后栈顶元素类型)是int类型(itos)

 1 void TemplateInterpreterGenerator::set_short_entry_points(Template* t, address& bep, address& cep, address& sep, address& aep, address& iep, address& lep, address& fep, address& dep, address& vep) {
 2   assert(t->is_valid(), "template must exist");
 3   switch (t->tos_in()) {
 4     case btos:
 5     case ctos:
 6     case stos:
 7       ShouldNotReachHere();  // btos/ctos/stos should use itos.
 8       break;
 9     case atos: vep = __ pc(); __ pop(atos); aep = __ pc(); generate_and_dispatch(t); break;
10     case itos: vep = __ pc(); __ pop(itos); iep = __ pc(); generate_and_dispatch(t); break;
11     case ltos: vep = __ pc(); __ pop(ltos); lep = __ pc(); generate_and_dispatch(t); break;
12     case ftos: vep = __ pc(); __ pop(ftos); fep = __ pc(); generate_and_dispatch(t); break;
13     case dtos: vep = __ pc(); __ pop(dtos); dep = __ pc(); generate_and_dispatch(t); break;
14     case vtos: set_vtos_entry_points(t, bep, cep, sep, aep, iep, lep, fep, dep, vep);     break;
15     default  : ShouldNotReachHere();                                                 break;
16   }
17 }

其中__定义如下:

# define __ _masm->

即模板解释器的宏汇编器

(5).以期望的栈顶状态为vtos状态为例,分析set_vtos_entry_points():

 1 void TemplateInterpreterGenerator::set_vtos_entry_points(Template* t,
 2                                                          address& bep,
 3                                                          address& cep,
 4                                                          address& sep,
 5                                                          address& aep,
 6                                                          address& iep,
 7                                                          address& lep,
 8                                                          address& fep,
 9                                                          address& dep,
10                                                          address& vep) {
11   assert(t->is_valid() && t->tos_in() == vtos, "illegal template");
12   Label L;
13   aep = __ pc();  __ push_ptr();  __ jmp(L);
14   fep = __ pc();  __ push_f();    __ jmp(L);
15   dep = __ pc();  __ push_d();    __ jmp(L);
16   lep = __ pc();  __ push_l();    __ jmp(L);
17   bep = cep = sep =
18   iep = __ pc();  __ push_i();
19   vep = __ pc();
20   __ bind(L);
21   generate_and_dispatch(t);
22 }

以ftos入口类型为例(vtos即当前字节码的实现不关心栈顶元素的状态),分析该入口的处理指令: 
push_f(): 
  定义在 /hotspot/src/cpu/x86/vm/interp_masm_x86_64.cpp中

1 void InterpreterMacroAssembler::push_f(XMMRegister r) {
2   subptr(rsp, wordSize);
3   movflt(Address(rsp, 0), r);
4 }

  其中r的默认值为xmm0,wordSize为机器字长(如64位机器为8字节)

subptr()实际上调用了subq():

1 void MacroAssembler::subptr(Register dst, int32_t imm32) {
2   LP64_ONLY(subq(dst, imm32)) NOT_LP64(subl(dst, imm32));
3 }

subq()的实现如下:

1 void Assembler::subq(Register dst, int32_t imm32) {
2   (void) prefixq_and_encode(dst->encoding());
3   emit_arith(0x81, 0xE8, dst, imm32);
4 }

而emit_arith()将调用emit_byte()/emit_long()写入指令的二进制代码”83 EC 08”(由于8可由8位有符号数表示,第一个字节为0x81 | 0x02,即0x83,rsp的寄存器号为4,第二个字节为0xE8 | 0x04,即0xEC,第三个字节为0x08 & 0xFF,即0x08),该指令即AT&T风格的sub $0x8,%rsp

 1 void Assembler::emit_arith(int op1, int op2, Register dst, int32_t imm32) {
 2   assert(isByte(op1) && isByte(op2), "wrong opcode");
 3   assert((op1 & 0x01) == 1, "should be 32bit operation");
 4   assert((op1 & 0x02) == 0, "sign-extension bit should not be set");
 5   if (is8bit(imm32)) { //iconst_0的操作数为0,即可以用8位二进制数表示
 6     emit_byte(op1 | 0x02); // set sign bit
 7     emit_byte(op2 | encode(dst));
 8     emit_byte(imm32 & 0xFF);
 9   } else {
10     emit_byte(op1);
11     emit_byte(op2 | encode(dst));
12     emit_long(imm32);
13   }
14 }

emit_byte()定义在/hotspot/src/share/vm/asm/assembler.inlilne.hpp中: 
该函数将把该字节复制到_code_pos处

1 inline void AbstractAssembler::emit_byte(int x) {
2   assert(isByte(x), "not a byte");
3   *(unsigned char*)_code_pos = (unsigned char)x;
4   _code_pos += sizeof(unsigned char);
5   sync();
6 }

故subq()向代码缓冲写入了指令sub $0x8,%rsp 
类似地,movflt()向代码缓冲写入了指令 movss %xmm0,(%rsp) 
jmp()向代码缓冲写入了指令jmpq (addr为字节码的本地代码入口)

set_vtos_entry_points()产生的入口部分代码如下:

 1 push %rax        .....(atos entry)
 2 jmpq <addr>
 3 sub $0x8,%rsp     .....(ftos entry)
 4 movss %xmm0,(%rsp)
 5 jmpq <addr>(addr为字节码的本地代码入口)
 6 sub $0x10,%rsp    .....(dtos entry)
 7 movsd %xmm0,(%rsp)
 8 jmpq <addr>
 9 sub $0x10,%rsp     .....(ltos entry)
10 mov %rax,(%rsp)
11 jmpq <addr>
12 push %rax         ...(itos entry)

set_vtos_entry_points()的最后调用generate_and_dispatch()写入了当前字节码的解释代码和跳转到下一个字节码继续执行的逻辑处理部分

generate_and_dispatch()主要内容如下:

 1 void TemplateInterpreterGenerator::generate_and_dispatch(Template* t, TosState tos_out) {
 2   // ...
 3   // generate template
 4   t->generate(_masm);
 5   // advance
 6   if (t->does_dispatch()) {
 7     //asserts
 8   } else {
 9     // dispatch to next bytecode
10     __ dispatch_epilog(tos_out, step);
11   }
12 }

这里我们以iconst()为目标代码生成器为例,分析generate():

1 void Template::generate(InterpreterMacroAssembler* masm) {
2   // parameter passing
3   TemplateTable::_desc = this;
4   TemplateTable::_masm = masm;
5   // code generation
6   _gen(_arg);
7   masm->flush();
8 }

generate()会调用生成器函数_gen(_arg),该函数根据平台而不同,如x86_64平台下,定义在/hotspot/src/cpu/x86/vm/templateTable_x86_64.cpp中

1 void TemplateTable::iconst(int value) {
2   transition(vtos, itos);
3   if (value == 0) {
4     __ xorl(rax, rax);
5   } else {
6     __ movl(rax, value);
7   }
8 }

我们知道,iconst_i指令是将i压入栈,这里生成器函数iconst()在i为0时,没有直接将0写入rax,而是使用异或运算清零,即向代码缓冲区写入指令”xor %rax, %rax”;在i不为0时,写入指令”mov $0xi, %rax”

当不需要转发时,会调用dispatch_epilog()生成取下一条指令和分派的目标代码:

1 void InterpreterMacroAssembler::dispatch_epilog(TosState state, int step) {
2   dispatch_next(state, step);
3 }

dispatch_next()实现如下:

1 void InterpreterMacroAssembler::dispatch_next(TosState state, int step) {
2   // load next bytecode (load before advancing r13 to prevent AGI)
3   load_unsigned_byte(rbx, Address(r13, step));
4   // advance r13
5   increment(r13, step);
6   dispatch_base(state, Interpreter::dispatch_table(state));
7 }

dispatch_next()首先调用load_unsigned_byte()写入指令”movzbl (%r13),%rbx”,再调用increment()写入指令”inc/add (,)%r13”指令,最后调用dispatch_base()写入”jmp *(%r10,%rbx,8)”。这类似于PC自增一条指令的宽度再继续取值运行的过程。

分析到这里,不禁有一个疑问,_code_pos是哪里?之前说过,StubQueue是用来保存生成的本地代码的Stub队列,队列每一个元素对应一个InterpreterCodelet对象,InterpreterCodelet对象包含了字节码对应的本地代码以及一些调试和输出信息。那么_code_pos是如何和InterpreterCodelet对应的呢?

我们注意到无论是为JVM的各种入口函数,还是为字节码生成本地代码,都会构造一个CodeletMark对象

CodeletMark cm(_masm, Bytecodes::name(code), code);

CodeletMark的构造函数如下:在初始值列表中,调用了StubQueue的request()创建了一个InterpreterCodelet对象,并以该InterpreterCodelet目标代码地址和大小为参数构造了一块CodeBuffer用来存放生成的目标代码。

public:CodeletMark(InterpreterMacroAssembler*& masm,const char* description,Bytecodes::Code bytecode = Bytecodes::_illegal):_clet((InterpreterCodelet*)AbstractInterpreter::code()->request(codelet_size())),_cb(_clet->code_begin(), _clet->code_size()){ // request all space (add some slack for Codelet data)assert (_clet != NULL, "we checked not enough space already");// initialize Codelet attributes_clet->initialize(description, bytecode);// create assembler for code generationmasm  = new InterpreterMacroAssembler(&_cb);_masm = &masm;}

但在此时还未生成目标代码,所以并不知道生成的目标代码有多大,所以这里会向StubQueue申请全部的空闲空间(只留有一点用来对齐空间,注意StubQueue实际上是一片连续的内存空间,所有Stub都在该空间上进行分配) 
随后初始化该InterpreterCodelet的描述部分和对应字节码,并以该CodeBuffer为参数构造了一个编译器对象InterpreterMacroAssembler

分析到这里,就应该明白编译器的_code_pos指的就是生成代码在CodeBuffer中的当前写位值 
还需一提的就是CodeletMark的析构函数,这里确认编译器的生产代码完全写入到CodeBuffer中后,就会调用StubQueue的commit()将占用的空间划分为当前Stub(InterpreterCodelet)所有

~CodeletMark() {// align so printing shows nop's instead of random code at the end (Codelets are aligned)(*_masm)->align(wordSize);// make sure all code is in code buffer(*_masm)->flush();// commit CodeletAbstractInterpreter::code()->commit((*_masm)->code()->pure_insts_size());// make sure nobody can use _masm outside a CodeletMark lifespan*_masm = NULL;}

转载于:https://www.cnblogs.com/iceAeterNa/p/4876924.html

HotSpot模板解释器目标代码生成过程源码分析相关推荐

  1. 【JVM源码解析】模板解释器解释执行Java字节码指令(上)

    本文由HeapDump性能社区首席讲师鸠摩(马智)授权整理发布 第17章-x86-64寄存器 不同的CPU都能够解释的机器语言的体系称为指令集架构(ISA,Instruction Set Archit ...

  2. 【SRIO】5、Xilinx RapidIO核例子工程源码分析

    目录 一.软件平台与硬件平台 二.打开例子工程 三.例子工程详解 3.1 工程概述 3.2 工程结构 3.3 工程分析 四.工程源码分析 3.1 顶层模块srio_example_top.v源码分析 ...

  3. 5.Xilinx RapidIO核例子工程源码分析

    https://www.cnblogs.com/liujinggang/p/10091216.html 一.软件平台与硬件平台 软件平台: 操作系统:Windows 8.1 64-bit 开发套件:V ...

  4. 【高速接口-RapidIO】5、Xilinx RapidIO核例子工程源码分析

    期待大家的一键三连,爱你们!!! 总目录:总目录(经验分享) 献上链接: [高速接口-RapidIO]2.RapidIO串行物理层的包与控制符号 [高速接口-RapidIO]3.RapidIO串行物理 ...

  5. 【源码分析】storm拓扑运行全流程源码分析

    [源码分析]storm拓扑运行全流程源码分析 @(STORM)[storm] 源码分析storm拓扑运行全流程源码分析 一拓扑提交流程 一stormpy 1storm jar 2def jar 3ex ...

  6. Android服务查询完整过程源码分析

    Android服务注册完整过程源码分析中从上到下详细分析了Android系统的服务注册过程,本文同样针对AudioService服务来介绍Android服务的查询过程. 客户端进程数据发送过程 pri ...

  7. Android服务注册完整过程源码分析

    前面从不同片段分析了Android的Binder通信机制,本文结合前面介绍的内容,对整个Android的Binder通信过程进行一次完整的分析.分析以AudioService服务的注册过程为例. 由于 ...

  8. Android 数据Parcel序列化过程源码分析

    在Android系统中,所有的服务都必须注册到ServiceManger中,当客户进程需要请求某一服务时,首先从服务管家ServiceManger中查找出该服务,然后通过RPC远程调用的方式使用该服务 ...

  9. OkHttp原理流程源码分析

    OkHttp已经是非常流行的android客户端的网络请求框架,我其实在项目中使用也已经好几年了,之前一直把重心放在如何快速的搞定业务上.迭代的效率上,这一点来讲,对于一个公司优秀员工是没有毛病的.但 ...

最新文章

  1. Mysql---Centos7软件安装
  2. VMware 虚拟化编程(3) —VMware vSphere Web Service API 解析
  3. altium designer寻找未连接飞线
  4. 关于webStrom-11.1配置less且自动生成.css和自动压缩为.min.css/.min.js
  5. android+命令行编译,打包生成apk文件,Android 使用Android Studio + Gradle 或 命令行 进行apk签名打包...
  6. 【大数据】朴素的数据价值观
  7. Mosaic获5.5亿美元住宅太阳能融资贷款
  8. mysql8创建用户并授权_新特性解读 | 从 wireshark 看 MySQL 8.0 加密连接
  9. 32接上拉5v_51单片机P0口上拉电阻的选择
  10. 华为Mate 40新功能上线,抬手即可付款!
  11. TensorFlow工作笔记003---python异常大全IndentationError: unexpected indent_expected indented block
  12. python3.6安装步骤-手动安装python3.6的操作过程详解
  13. javascript 使用canvas绘画
  14. 计算机局域网共享本地安全策略,如何设置局域网共享
  15. Android双系统实现
  16. netbeans莫明其妙的报错
  17. 使用Mailgun WordPress插件增加订户
  18. 赠书 002丨文化改变脑,是玄学?
  19. 怎样学好Python
  20. html图片查看代码实现,如何用HTML5实现图片预览和查看原图的功能

热门文章

  1. python 列表算平均分_python平均列表
  2. 华为鸿蒙系统概念图,华为P50Pro概念图:没有麒麟芯片,鸿蒙系统和7镜头也可以很豪横...
  3. 怎么看b树是几阶_看我在B站上怎么学习的
  4. python-opencv 常用工具总结
  5. 使用Python,OpenCV制作图像Mask——截取ROIs及构建透明的叠加层
  6. mxnet中symbol的网络结构输出(参数维度,和每层输出维度)
  7. gnome硬盘分析_三款基于GUI和终端的实用Linux磁盘扫描工具
  8. 在Ubuntu 16.04.1 LTS上安装ATS 6.2.1 LTS实录
  9. 在Ubuntu 16.04.1 LTS上安装ats 5.3.2
  10. 如何从ATS获取客户端平均响应时间(单位,毫秒)?