erlang是开源的,非常多人都研究过源码。可是。从erlang代码到c代码。这是个不小的跨度。并且代码也比較复杂。

所以这里,我利用一些时间,整理下erlang代码的运行过程。从erlang代码编译过程,到代码运行过程做解说。然后重点讲下虚拟机运行代码的原理。将本篇文章。献给全部喜欢erlang的人。

erlang代码编译过程

erlang对开发人员是友好的。从erlang程序文件编译成能被erlang虚拟机识别的beam文件,在这个编译过程还对开发人员暴露中间代码。借助这个中间代码,我们就能够逐步探究erlang代码的运行过程。

这是erlnag的编译过程,当然,最開始和大多数编译器一样,首先会将程序文件转换成语法树,但这个转换对我们来说阅读的意义不大,所以归结于以上3个过程。

1. erlang核心代码

确切的叫法是Core Erlang,使用了相似Haskell 的语法,每一个变量都用“Let” 声明。在erlang shell通过下面方式能够获取模块的Core Erlang代码。将会生成test.core文件
c(test, to_core).
实际上core文件能够直接编译成beam文件。例如以下:
c(test, from_core).

2. erlang汇编码

这个是erlang代码编译成beam前的汇编代码,尽管在erlang打包成beam。以及载入到VM时会进一步优化。但汇编码实际上能够看成erlang代码到c代码的纽带。但理解汇编码而不是非常easy,这里要知道erlang VM的设计基于寄存器。当中有两类重要的寄存器,传递參数的x寄存器。和在函数内用作本地变量的y寄存器。在erlang shell通过下面方式能够获取模块的汇编代码,将会生成test.S文件
c(test, to_asm). 或是 c(test, 'S').
当然,S文件也支持编译成beam文件,例如以下:
c(test, from_asm).

3. erlang BEAM

beam文件是不可阅读的,仅仅是给VM识别,内容包括了代码。原子,导入导出函数,属性,编译信息等数据块。

4.  erlang运行时代码

运行时代码是指模块载入到VM后的代码,erlang对开发人员暴露了底层的接口。

当模块载入后,在erlang shell下通过下面方式能够获取模块的运行时代码。就会生成test.dis文件

erts_debug:df(test).
这里。细心的同学会发现,通过对照erlang汇编码和运行时代码,发现指令代码是不全然相同的。一方面。erlang会对指令进一步做优化。另外。erlang使用了两种指令集,有限指令集和扩展指令集,在beam文件使用了有限指令集。然后在载入到VM时展开为扩展指令集。

有论文说是为了降低Beam的大小,这点我没有做过实质性的探究,我仅仅是认为有限指令集比較短,更easy阅读被人理解。关于有限指令集和扩展指令集的区别。我在文章最后的拓展阅读做了讨论。

erlang代码从编译到运行过程

前面介绍了erlang代码编译的过程。如今再来说明erlang代码从编译到运行的完整过程。

文章erlang版本号以R16B02作说明。

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvbXljd3E=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="" />

这里。erlang代码先被编译成beam。然后载入到VM中,最后再被模拟器所识别和调用。

当中。beam文件的载入过程会将beam的字节码形式的数据转成Threaded code和数据。前面也提到,beam文件的字节码数据包括有代码块,这里是将指令展开,转成Threaded code(线索化代码),每条指令包括了opcode(操作码)和operands(操作数),另外还对operands做修正。比方调用外部函数。这里会找到这个外部函数的导出地址,这样每次代码运行的时候就不用再去函数表查找到这个函数,就能够直接运行代码。
Beam的载入逻辑是在 beam_load.c 完毕的,指令集的转换在beam_opcodes.c做了映射,而beam_opcodes.c文件是在编译Erlang源码过程有Perl脚本beam_makeops依据ops.tab生成的。全部有限指令集能够在genop.tab找到。
File 
Path
beam_makeops
erts/emulator/utils/
ops.tab
erts/emulator/beam/
beam_opcodes.c
erts/emulator/<machine>/opt/smp/
beam_load.c
erts/emulator/beam/
genop.tab
lib/compiler/src/

erlang 虚拟机运行代码的原理

这里先简单说明下erlang虚拟机、进程、堆栈。寄存器,然后側重从指令调度。代码线索化说明虚拟机代码运行原理。

erlang虚拟机概述

通常我们说的eralng虚拟机。是指BEAM虚拟机模拟器和erlang运行时系统(ERTS)。

ERTS是erlang VM最底层的应用,负责和操作系统交互,管理I/O,实现erlang进程和BIF函数。BEAM模拟器是运行Erlang程序经编译后产出的字节码的地方。

erlang虚拟机最早的版本号是Joe Armstrong编写的,基于栈,叫JAM(Joe's Abstract Machine),非常相似WAM(Warren's Abstract Machine)。

后来改成基于寄存器的虚拟机,也就是如今的BEAM(Bogdan's Abstract Machine),运行效率有了较大幅度提升。这在Joe的erlang VM演变论文有说到。

基于栈和基于寄存器的虚拟机有什么区别?
基于栈(stack-based)的虚拟机的指令长度是固定的。运行多个操作数计算时。会先将操作数做压入栈。由运算指令取出并计算。

而基于寄存器(register-based)的指令长度不是固定的,能够在指令中带多个操作数。这样,基于寄存器能够降低指令数量,降低入栈出栈操作,从而降低了指令派发的次数和内存訪问的次数,相比开销少了非常多。

可是,假设利用寄存器做数据交换,就要常常保存和恢复寄存器的结果。这就导致基于寄存器的虚拟机在实现上要比基于栈的复杂,代码编译也要复杂得多

erlang进程

erlang进程是在代码运行过程中动态创建和销毁。每一个进程都有自己私有的栈和堆。erlang进程是erlang虚拟机进行资源分配和调度的基本单位。erlang代码的运行要通过erlang进程来实现。
1> spawn(fun() -> m:loop() end).
<0.34.0>
也许有人会问,启动erlang节点时没有使用不论什么进程。这是为什么?实际上。启动erlang节点的代码是运行在shell进程。相同受到erlang虚拟机调度。我们看到的是由shell进程运行后返回的结果。

为了实现多进程并发。erlang虚拟机实现了进程挂起和调度机制。进程运行代码时会消耗调度次数(Reductions),当调度次数为0时就会挂起这个进程,然后从调度队列中取出第一个进程运行。

假设进程在等待新消息时也会被挂起,直到这个进程接收到新消息后。就又一次加到调度队列。

进程的栈和堆

erlang进程在运行代码的过程中。栈主要用来存放调用帧的本地变量和返回地址。堆则是用来存放运行过程创建的数据。在实现上,栈和堆是在同一个内存区域的。例如以下图:

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvbXljd3E=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="" />

堆栈的内存空间是先申请一块较大的内存后一点一点使用。不够再又一次申请一大块。这样避免频繁申请释放内存造成开销。以上,在已分配好的内存区域内,堆从最低的地址向上增长,而栈从最高的地址向下增长。中间堆顶和栈顶的空白区域。表示了进程堆栈还未使用到的空间,使用内存时就向里收缩,不够时就运行gc。这样,内存溢出检查就仅仅要比較栈顶和堆顶就好。
堆用于存储复杂的数据结构,如元组。列表或大整数。

栈被用来存储简单的数据,还有指向堆中复杂数据的数据指针。

栈有指针指向堆,但不会有指针从堆到栈。

寄存器

前面也提到。对于基于栈的虚拟机。操作数在使用前都会被压到栈,计算时取出。也就是先将本地变量的值压入栈。然后在计算时从栈取出赋值给本地变量。所以,这里有非常大开销在本地变量和栈之间的交换上(出入栈)。

为此,基于寄存器的虚拟机使用暂时变量来保存这个本地变量,这个暂时变量也就是寄存器。并且,这个寄存器变量通常都被优化成CPU的寄存器变量,这样,虚拟机訪问寄存器变量甚至都不用訪问内存。极大的提高了系统的运行速度。

    /*
* X register zero; also called r(0)
*/
register Eterm x0 REG_x0 = NIL;

register修饰符的作用是暗示编译器。某个变量将被频繁使用,尽可能将其保存在CPU的寄存器中,以加快其存储速度。随着编译程序设计技术的进步,在决定那些变量应该被存到寄存器中时。如今的编译器能比程序猿做出更好的决定,往往会忽略register修饰符。

可是就erlang虚拟机对寄存器变量的使用程度,应该是能够利用到CPU寄存器的优点。

erlang有哪些寄存器?
參数寄存器(R0-R1024) R0是最快的。是独立的寄存器变量,其它以reg[N]訪问。R0还用来保存函数返回值
指令寄存器(IP) 引用当前正在运行的指令,能够通过I[N]取到上下文指令。

返回地址寄存器 (CP。原意Continuation Pointer) 记录当前函数调用的返回地址,在运行完当前函数后返回上一个函数中断处运行后面的代码。
栈寄存器(EP) 指向栈的栈顶。以E[N]数组形式訪问栈帧数据
堆寄存器 (heap top)指向堆的堆顶,以HTOP[N]数组形式訪问堆数据
暂时寄存器(tmp_arg1和tmp_arg2)用于指令实现须要暂时变量的场合(尽可能重用暂时变量,同一时候利用CPU寄存器优化)
浮点寄存器(FR0-FR15)
其它寄存器:
'Live' 表示当前须要的寄存器数量,非常多指令取这个值来推断是否要运行GC申请新的空间
'FCALLS' 表示当前进程剩余的调度次数(Reductions)
若不考虑多调度器,寄存器是全部进程共享的。当虚拟机调度运行某个进程的时候,寄存器就归这个进程使用。

当进程被调出的时候,寄存器就给其它进程使用。(进程切换保存进程上下文时。仅仅须要保存指令寄存器IP和当前函数信息。效率非常高)

指令调度

erlang指令调度实现是一个巨大的switch结构。每一个case语句都相应一个指令操作码(opcode)。这样就能够实现指令的分发和运行。可是。switch调度方式实现简单。但效率比較低下。所以。erlang虚拟机使用了goto语法,避免过多的使用switch造成性能损耗。同一时候,erlang还使用跳转表,在一些高级编译器下(如GCC)。利用label-goto语法。效率比較高(针对跳转表的概念。我之前也有文章说明。见这里)。正由于这点,虚拟机调度时解释指令的代价不容忽视,基于寄存器的虚拟机指令少,就要比基于栈高效。
while(1){
opcode = *vPC++;
switch(opcode){
case i_call_fun:
..
break;
case call_bif_e:
..
break;
//and many more..
}
};

字节码在虚拟机中运行。运行过程相似CPU运行指令过程,分为取指,解码。运行3个过程。通常情况下,每一个操作码相应一段处理函数,然后通过一个无限循环加一个switch的方式进行分派。

erlang进程创建时必须指定运行函数,进程创建后就会运行这个函数。从这个函数開始一直到结束,进程都会被erlang虚拟机调度。

start()->
spawn(fun() -> fun1(1) end). %% 创建进程。运行 fun1/1 fun1(A) ->
A1 = A + 1,
B = trunc(A1), %% 运行 trunc/1
{ok, A1+B}.

以上。进程在运行函数( trunc/1)调用前。会将当前的本地变量和返回地址指针CP写入栈。然后,在运行完这个函数(trunc/1)后再从栈取出CP指令和本地变量,依据CP指针返回调用处,继续运行后面的代码。

这样,每次函数运行结束时,erlang从栈顶检查并取得CP指针(假设函数内过于简单,没有其它函数调用,就直接读取 (Process* c_p)->cp),然后将CP指针的值赋给指令寄存器IP,同一时候删除CP栈帧(依据须要还要回收Live借用的栈空间),继续调度运行。
备注:这里讲到的栈帧删除操作,如CP指针。本地变量数据。删除时仅仅要将栈顶指针向高位移动N个位置,没有GC操作,代价极小。另外,这里也显露出一个问题,假设非尾递归函数调用。erlang须要重复将本地变量和CP指针入栈。easy触发GC和内存复制,引发内存抖动。

另外,在寄存器方面,函数调用时,erlang虚拟机会将传參写到參数寄存器x(N),然后更新返回地址寄存器CP。在函数调用返回时,会将返回值写到x(0)寄存器。

Threaded Code(线索化代码)

前面提到switch指令派发方式,每次处理完一条指令后,都要回到循环的開始,处理下一条指令。可是,每次switch操作,都可能是一次线性搜索(现代编译器能对switch语句进行优化。 以消除这样的线性搜索开销,但也是仅仅限于特定条件。如case的数量和值的跨度范围等)。

假设是少量的switch case,全然能够接受,可是对于虚拟机来说。有着成百上千的switch case,并且运行频繁非常高,运行一条指令就须要一次线性搜索。确定比較耗性能。假设能直接跳转到运行代码位置,就能够省去线性搜索的过程了。于是在字节码的分派方式上,做了新的改进。这项技术叫作 Context Threading(上下文线索化技术。Thread眼下都没有合适的中文翻译。我这里意译为线索化。表示当中的线索关系)。

这里取了Context Threading论文的样例,说明上下文线索化技术(Context Threading)
1.首先,代码会被编译成字节码
2.假设是switch派发指令,效率低下
3.假设是线索化代码(Threaded Code),就直接跳转(goto),无需多次switch

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvbXljd3E=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="" />

4.从字节码到终于运行代码的过程。

左边是编译生成的字节码,中间就是字节码载入后生成的线索化代码,右边是相应的虚拟机实现代码。虚拟机运行时,vpc指向了iload_1指令,在运行iload_1指令操作后依据goto *vpc++ 跳转到下一条指令地址。继续运行,如此重复。这个过程就好像穿针引线。每运行完一条指令,就直接跳转到下一条指令的地址。而不再是Switch Loop那样,每运行一条指令都要做一次switch。(这里,vPC是指虚拟PC指令。在erlang中是IP指针)

拓展阅读

BIF(内建函数)

BIF是erlang的内建函数,由C代码实现,用以实如今erlang层面实现效率不高或无法实现的功能。大多数BIF函数属于erlang模块,也有其它模块的BIF函数。ets或lists,os等
1> erlang:now().
{1433,217791,771000}
2> lists:member(1,[1,2,3]).
true
这里重点解释下。BIF代码怎样被运行的

erlang源码编译时生成bif函数表信息。见 erts\emulator\<machine>\erl_bif_table.c
Export* bif_export[BIF_SIZE];
BifEntry bif_table[] = {
{am_erlang, am_abs, 1, abs_1, abs_1},
{am_erlang, am_adler32, 1, adler32_1, wrap_adler32_1},
{am_erlang, am_adler32, 2, adler32_2, wrap_adler32_2},
{am_erlang, am_adler32_combine, 3, adler32_combine_3, wrap_adler32_combine_3},
{am_erlang, am_apply, 3, apply_3, wrap_apply_3},
{am_erlang, am_atom_to_list, 1, atom_to_list_1, wrap_atom_to_list_1},
typedef struct bif_entry {
Eterm module;
Eterm name;
int arity;
BifFunction f; // bif函数
BifFunction traced; // 函数调用跟踪函数
} BifEntry;

erlang BEAM模拟器启动时会初始化bif函数表,

init_emulator:
{ em_call_error_handler = OpCode(call_error_handler);
em_apply_bif = OpCode(apply_bif); beam_apply[0] = (BeamInstr) OpCode(i_apply);
beam_apply[1] = (BeamInstr) OpCode(normal_exit);
beam_exit[0] = (BeamInstr) OpCode(error_action_code);
beam_continue_exit[0] = (BeamInstr) OpCode(continue_exit);
beam_return_to_trace[0] = (BeamInstr) OpCode(i_return_to_trace);
beam_return_trace[0] = (BeamInstr) OpCode(return_trace);
beam_exception_trace[0] = (BeamInstr) OpCode(return_trace); /* UGLY */
beam_return_time_trace[0] = (BeamInstr) OpCode(i_return_time_trace); /*
* Enter all BIFs into the export table.
*/
for (i = 0; i < BIF_SIZE; i++) {
ep = erts_export_put(bif_table[i].module, //模块名
bif_table[i].name,
bif_table[i].arity);
bif_export[i] = ep;
ep->code[3] = (BeamInstr) OpCode(apply_bif);
ep->code[4] = (BeamInstr) bif_table[i].f; // BIF函数
/* XXX: set func info for bifs */
ep->fake_op_func_info_for_hipe[0] = (BeamInstr) BeamOp(op_i_func_info_IaaI);
}

下面写个简单的样例说明。

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvbXljd3E=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="" />

bif函数编译后,opcode都是 call_bif_e,操作数是函数导出表地址,下面分析下这个opcode的实现:
/*
* 下面截取 bif 处理过程
*/
OpCase(call_bif_e):
{
Eterm (*bf)(Process*, Eterm*, BeamInstr*) = GET_BIF_ADDRESS(Arg(0)); // 依据參数获取bif实际运行函数
Eterm result;
BeamInstr *next;
PRE_BIF_SWAPOUT(c_p);
c_p->fcalls = FCALLS - 1;
if (FCALLS <= 0) {
save_calls(c_p, (Export *) Arg(0));
}
PreFetch(1, next);
ASSERT(!ERTS_PROC_IS_EXITING(c_p));
reg[0] = r(0);
result = (*bf)(c_p, reg, I); // 运行bif函数
ASSERT(!ERTS_PROC_IS_EXITING(c_p) || is_non_value(result));
ERTS_VERIFY_UNUSED_TEMP_ALLOC(c_p);
ERTS_HOLE_CHECK(c_p);
ERTS_SMP_REQ_PROC_MAIN_LOCK(c_p);
PROCESS_MAIN_CHK_LOCKS(c_p);
if (c_p->mbuf || MSO(c_p).overhead >= BIN_VHEAP_SZ(c_p)) {
Uint arity = ((Export *)Arg(0))->code[2];
result = erts_gc_after_bif_call(c_p, result, reg, arity);
E = c_p->stop;
}
HTOP = HEAP_TOP(c_p);
FCALLS = c_p->fcalls;
if (is_value(result)) {
r(0) = result;
CHECK_TERM(r(0));
NextPF(1, next);
} else if (c_p->freason == TRAP) {
SET_CP(c_p, I+2);
SET_I(c_p->i);
SWAPIN;
r(0) = reg[0];
Dispatch();
}

上面涉及到一个宏,就是取得bif函数地址。

#define GET_BIF_ADDRESS(p) ((BifFunction) (((Export *) p)->code[4]))

依据前面提到的。((Export *) p)->code[4] 就是 bif_table表的中BIF函数的地址。

扩展指令集

BEAM文件使用的是有限指令集(limited instruction set),这些指令集会在beam文件被载入时,展开为扩展指令集(extended instruction set)。
get_list -> get_list_rrx
get_list ->get_list_rry
call_bif -> call_bif_e
扩展指令集和有限指令集的区别是,扩展指令集还描写叙述了操作数类型。
Type Description
t An arbitrary term, e.g. {ok,[]}
I An integer literal, e.g. 137
x A register, e.g. R1
y A stack slot
c An immediate term, i.e. atom/small int/nil
a An atom, e.g. 'ok'
f A code label
s Either a literal, a register or a stack slot
d Either a register or a stack slot
r A register R0
P A unsigned integer literal
j An optional code label
e A reference to an export table entry
l A floating-point register
以 call_bif_e 为例, e表示了操作数为函数导出表地址。所以 call_bif_e 能够这样取到bif代码地址
 ((Export *) Arg(0))->code[4]

文献资料:
[2] Virtual Machine Showdown: Stack Versus Registers Yunhe Shi, David Gregg, Andrew Beatty
[3] Context Threading: A flexible and efficient dispatch technique for virtual machine interpreters
參考:
http://blog.csdn.net/mycwq/article/details/45653897

erlang虚拟机代码运行原理的更多相关文章

  1. erlang虚拟机代码执行原理

     转载:http://blog.csdn.NET/mycwq/article/details/45653897 erlang是开源的,很多人都研究过源代码.但是,从erlang代码到c代码,这是个不小 ...

  2. java虚拟机的运行原理

    一.类加载器 首先来看一下java程序的执行过程. 从这个框图很容易大体上了解java程序工作原理.首先,你写好java代码,保存到硬盘当中.然后你在命令行中输入 javac YourClassNam ...

  3. Profile 分析 Erlang 虚拟机源码时要注意的一个问题

    最近用 Intel Vtune 剖析 Erlang 虚拟机的运行,想看看那些函数和语句耗时最多,遇到一个小问题,那就是 Vtune 给出的源码和汇编码对应有问题.这个问题在 profile 或 deb ...

  4. Nodejs的运行原理-科普篇

    前言 Nodejs目前处境稍显尴尬,很多语言都已经拥有异步非阻塞的能力.阿里的思路是比较合适的,但是必须要注意,绝对不能让node做太多的业务逻辑,他只适合接收生成好的数据,然后或渲染后,或直接发送到 ...

  5. 【原创】分布式之数据库和缓存双写一致性方案解析(三) 前端面试送命题(二)-callback,promise,generator,async-await JS的进阶技巧 前端面试送命题(一)-JS三座大山 Nodejs的运行原理-科普篇 优化设计提高sql类数据库的性能 简单理解token机制

    [原创]分布式之数据库和缓存双写一致性方案解析(三)   正文 博主本来觉得,<分布式之数据库和缓存双写一致性方案解析>,一文已经十分清晰.然而这一两天,有人在微信上私聊我,觉得应该要采用 ...

  6. 洗礼灵魂,修炼python(3)--从一个简单的print代码揭露编码问题,运行原理和语法习惯

    前期工作已经准备好后,可以打开IDE编辑器了,你可以选择python自带的IDLE,也可以选择第三方的,这里我使用pycharm--一个专门为python而生的IDE 按照惯例,第一个python代码 ...

  7. python虚拟机运行原理

    近期为了面试想要了解下python的运行原理方面的东西,奈何关于python没有找到一本类似于深入理解Java虚拟机方面的书籍,找到了一本<python源码剖析>电子书,但是觉得相对来说最 ...

  8. python3 源码阅读-虚拟机运行原理

    阅读源码版本python 3.8.3 参考书籍<<Python源码剖析>> 参考书籍<<Python学习手册 第4版>> 官网文档目录介绍 Doc目录主 ...

  9. 理解Web应用程序的代码结构和运行原理(3)

    1.理解Web应用程序的运行原理和机制 Web应用程序是基于浏览器/服务器模式(也称B/S架构)的应用程序,它开发完成后,需要部署到Web服务器上才能正常运行,与用户交互的客户端是网页浏览器. 浏览器 ...

随机推荐

  1. 赵雅智_运用Bitmap和Canvas实现图片显示,缩小,旋转,水印

    上一篇已经介绍了Android种Bitmap和Canvas的使用,以下我们来写一个详细实例 http://blog.csdn.net/zhaoyazhi2129/article/details/321 ...

  2. Highcharts图表插件的简单使用说明

    Highcharts图表控件是眼下使用最为广泛的图表控件.本文将从零開始逐步为你介绍Highcharts图表控件. 通过本文.你将学会怎样配置Highcharts以及动态生成Highchart图表. ...

  3. 16. IntellIJ IDEA 配置 Maven 以及 修改 默认 Repository

    转自:https://www.cnblogs.com/phpdragon/p/7216626.html 今天将IntellIJ IDEA 关于Maven的配置总结一下,方便以后可参考. IDEA版本: ...

  4. 创建带有IN类型参数的存储过程(四十八)

    创建带有IN类型参数的存储过程 我们经常要从数据表中删除记录,一般情况我们删除记录都是根据id来删除的,比如我们通常要输入DELETE FROM 表名 WHERE 后面跟上我们的条件,因为我们要经常写 ...

  5. ipad无法连接到app store怎么办

    之前入手的air2提示无法连接到app store:你需要首先更新系统到最新的ios版本,去通用设置里面,有个update software, 点击即可,然后才能用apple id 联入,否选择提示连 ...

  6. 开源系统源码分析(filter.class.php)

    <?php class baseValidater { //最大参数个数 const MAX_ARGS=3; public static function checkBool($var) { r ...

  7. 【Django】Cookie

    目录 Cookie介绍 操作Cookie 获取Cookie 设置 Cookie 删除Cookie @ Cookie介绍 Cookie的由来 大家都知道==HTTP协议是无状态的==. ==无状态的的意 ...

  8. Firewalld 用法解析

    其实还是我写的啦 https://www.jianshu.com/p/3444d9413461 1.防火墙firewall的基本概述 现在的RedHat/CentOS7版本默认都使用firewall防 ...

  9. Windows Server 2016 辅助域控制器搭建

    Windows Server 2016 主域控制器搭建完成后,继续进行辅助域控制器搭建.1.更改服务器的IP地址2.修改服务器的名称3.打开服务器管理器,选择添加角色和功能4.选择,下一步5.选择,下 ...

  10. visualvm监控tomcat

    https://my.oschina.net/weidedong/blog/787203