erlang程序语言
Q&A
Q: erlang otp
是什么
A:
Erlang OTP (Open Telecom Platform)
是一个开源的软件平台,用于构建高可靠性、高可扩展性、分布式、并发的应用程序。Erlang OTP
提供了许多强大的工具和库,使得开发者可以轻松地构建和部署分布式系统,例如,Erlang OTP
提供了进程管理、网络通信、故障转移、代码升级等功能,这些功能使得Erlang OTP
成为构建高可靠性应用程序的首选语言和平台。
Q: erlang OTP
有哪些东西
A:
Erlang OTP
提供了许多强大的工具和库,包括:
- 进程管理:
Erlang OTP
提供了进程管理工具,使得开发者可以轻松地创建、监控和终止进程。这些进程可以独立运行,也可以通过消息传递进行通信,从而实现分布式计算。- 网络通信:
Erlang OTP
提供了网络通信库,包括TCP
、UDP
、SCTP
和TLS
等协议的支持。这些库可以帮助开发者构建高性能、可扩展的网络应用程序。- 共享内存:
Erlang OTP
提供了多种共享内存机制,包括ETS
(Erlang Term Storage)和Mnesia
等。这些机制可以帮助开发者在分布式环境下共享数据。- 代码升级:
Erlang OTP
提供了代码升级工具,使得开发者可以在系统运行时更新代码,而无需停机重启。这使得系统升级变得更加容易和安全。- 故障转移:
Erlang OTP
提供了故障转移机制,使得系统可以在节点故障时自动恢复。这使得系统更加可靠和健壮。- 应用程序框架:
Erlang OTP
提供了应用程序框架,包括gen_server
、gen_statem
、gen_event
、supervisor
等。这些框架可以帮助开发者快速构建高可靠性、高可扩展性的应用程序。
Q: erlang
虚拟机
A: Erlang 虚拟机是
Erlang/OTP
平台的核心组成部分,也是其最重要的技术之一。Erlang 虚拟机在运行时会将 Erlang 代码编译成字节码,然后在虚拟机中执行这些字节码。Erlang 虚拟机具有以下特点:
- 轻量级:Erlang 虚拟机比较轻量级,可以在资源有限的环境下运行,例如嵌入式系统。
- 并发性:Erlang 虚拟机通过进程间的消息传递实现并发,而不是使用共享内存。这样可以避免许多并发编程中常见的问题,例如死锁和竞态条件等。
- 分布式:Erlang 虚拟机天生支持分布式计算,可以轻松地将应用程序部署到多个节点上。
- 热代码升级:Erlang 虚拟机支持热代码升级,也就是在应用程序运行时更新代码而不需要停机。这样可以使得应用程序变得更加可靠和可升级。
- 容错性:Erlang 虚拟机具有强大的容错性,可以在节点故障时自动恢复。这使得 Erlang 应用程序变得更加健壮和可靠。
总之,Erlang 虚拟机是
Erlang/OTP
平台的核心技术之一,具有轻量级、并发性、分布式、热代码升级和容错性等特点,使得它成为构建高可靠性、高可扩展性、分布式的应用程序的首选平台。
Q: erlang
启动参数
A: Erlang 启动参数是指在启动 Erlang 虚拟机时,可以通过命令行或配置文件传递的一些参数,以便控制虚拟机的行为。以下是一些常见的 Erlang 启动参数:
-sname
:指定节点的名称,用于在分布式环境中唯一标识节点。-name
:指定节点的全局名称,可以在全局范围内唯一标识节点。-setcookie
:设置节点之间的通信加密密钥。-kernel
:用于设置 Erlang 虚拟机的内核参数,例如进程数、垃圾回收间隔等。-s
:在启动时执行指定的模块和函数,例如-s mod function。-pa
:添加指定的目录到 Erlang 搜索路径中,用于加载模块。-run
:在启动时执行指定的模块和函数,例如-run mod function。-config
:指定配置文件的路径,用于设置应用程序的配置参数。-boot
:指定引导文件的路径,用于启动应用程序。-detached
:将 Erlang 虚拟机作为守护进程运行。这些启动参数可以通过命令行传递,也可以通过配置文件指定。在启动 Erlang 应用程序时,可以根据需要使用这些参数来控制虚拟机的行为和设置。
erlang
启动参数
erlang
启动参数主要有3种, 分别是emulator flag
, flags
和plain arguments
, 以下为几个常用的参数和相关的例子
emulator flags
是以“+”开头的,用来控制模拟器的行为
flags
是以“-”开头的, 是erlang
运行时系统的参数,可以用init:get_argument/1
获得
plain arguments
普通参数,在第一个flag参数前,或在-- flag
之后,-extra
后的参数都是普通参数。
> erl +W w -sname arnie +R 9 -s my_init -extra +bertie
(arnie@host)1> init:get_argument(sname).
{ok,[["arnie"]]}
(arnie@host)2> init:get_plain_arguments().
["+bertie"]
这里 + W w
和 + R9
是模拟器标志。-s my_init
是一个 init
标志,由 init
解释。- sname arnie
是一个用户标志,由 init
存储。它被内核读取,并将导致 Erlang 运行时系统分布。最后,后面的所有内容(即 + bertie
)都被认为是纯参数
1. 系统运行参数
启动时传参: -extra 参数
这个用的场景比较多, 比如启动的时候想传入一个常量配置的参数, 这个参数可能不大,就几个字符, 不需要单独创一个文件来存, 可以用 -extra
; 传入的参数用 init:get_plain_arguments().
来获取
[root@feng1 ~]# erl -extra test
Erlang/OTP 23 [erts-11.1] [source] [64-bit] [smp:1:1] [ds:1:1:10] [async-threads:1] [hipe]
Eshell V11.1 (abort with ^G)
1> init:get_plain_arguments().
["test"]
当然也可以传多个, 要注意-extra 参数1 参数2
要放在启动命令的最后用
[root@feng1 ~]# erl -extra test1 test2 test3
Erlang/OTP 23 [erts-11.1] [source] [64-bit] [smp:1:1] [ds:1:1:10] [async-threads:1] [hipe]
Eshell V11.1 (abort with ^G)
1> init:get_plain_arguments().
["test1","test2","test3"]
设置环境变量-env Key Value
erl -env DISPLAY gin:0
设置节点名 -sname
-name
-name xx@xx -setcookie xxx
: 设置长节点名
-sname xxx
: 设置短节点名
通过init:get_argument(sname).
E:\zz-Typero>erl -sname lijiang
Eshell V10.7 (abort with ^G)
(lijiang@DESKTOP-D72N5G8)1> node().
'lijiang@DESKTOP-D72N5G8'
(lijiang@DESKTOP-D72N5G8)2> init:get_argument(sname).
{ok,[["lijiang"]]}
(lijiang@DESKTOP-D72N5G8)3> init:get_argument(name).
error
E:\zz-Typero>erl -name lijiang@192.168.118.1
Eshell V10.7 (abort with ^G)
(lijiang@192.168.118.1)1> node().
'lijiang@192.168.118.1'
(lijiang@192.168.118.1)2> init:get_argument(name).
{ok,[["lijiang@192.168.118.1"]]}
(lijiang@192.168.118.1)3> init:get_argument(sname).
error
-id Id
: 给erlang
进程设置一个id,一般和 -sname
和 -name
一起用
启动时编译 -compile
在一个文件夹中放入测试test_start.erl
文件
然后用-compile
启动参数编译启动
启动时编译只是把这个文件编译成test_start.beam
文件并放入相同目录内, 没有启动erlang
shell, 如果要启动编译多个模块, 在后边加上对应模块名即可. 编译多个文件时不能指定放在什么目录, 一般是.ebin
目录. 所以一般使用make:all()
或者mmkae
模块来启动
启动时调用-eval 'Mod:Func(A)'
-eval
后跟的参数格式: ' '
中间的内容 就和在erlang shell
格式一样, 不加最后的.
$ erl -eval 'test_start:test(1)'
Eshell V10.3 (abort with ^G)
test/1,A1 is 1
1>
$ erl -eval 'test_start:test(1,2)'
Eshell V10.3 (abort with ^G)
test/2,A1 is 1, A2 is 2
1>
启动时调用-s Mod Fun Args1 Arg2
-s
后跟的参数 就和 配置表的 M F A 一样, 但是这里有n个A, Mod:Func/n
就是几参的函数;
$ erl -s test_start test 1
test/1,A1 is ['1']
Eshell V10.3 (abort with ^G)
1>
$ erl -s test_start test 1 2
test/1,A1 is ['1','2']
Eshell V10.3 (abort with ^G)
1>
启动时调用-run Mod Func Args
-s
后跟的参数 就和 配置表的 M F A 一样, 不管有多少参数, Mod:Func/1
就是1参的函数, 所有的参数放在一个List中传入调用方法
启动时调用方法小结
-s
和 -run
传入的参数都会被解析成字符串传入; -s
传入的参数数量和调用方法的参数数量 是相同的; -run
传入的参数不管多少都被整合成一个列表, 传入调用方法中
启动时编译 make:all
删掉刚刚编译的test_start.beam
, 调用make:all()
方法试试编译
$ erl -s make all
Recompile: test_start
Eshell V10.3 (abort with ^G)
1>
$ erl -run make all
Recompile: test_start
Eshell V10.3 (abort with ^G)
1>
$ erl -eval 'make:all()'
Eshell V10.3 (abort with ^G)
1> Recompile: test_start
1>
启动时编译 -make
还有一种就是直接带上 -make
, 自动编译当前目录下的*.erl
文件
mmake
模块拓展
并行编译的用法: mmake:all/1
, 比make
多一个参数, 这个参数表示通过n个workers进程来编译代码
erl -eval "case make:files([\"mmake.erl\"], [{outdir, \"ebin\"}]) of error -> halt(1); _ -> ok end"
-eval "case mmake:all(8,[$(MAKE_OPTS)]) of up_to_date -> halt(0); error -> halt(1) end."
打印启动日志-init_debug
E:\zz-Typero>erl -name lijiang@192.168.9.8 -init_debug
{progress,preloaded}
{progress,kernel_load_completed}
{progress,modules_loaded}
{start,heart}
{start,logger}
{start,application_controller}
{progress,init_kernel_started}
{apply,{application,load,[{application,stdlib,[{description,"ERTS CXC 138 10"},{vsn,"3.12"},{id,[]},{modules,[array,base64,beam_lib,binary,c,calendar,dets,dets_server,dets_sup,dets_utils,dets_v9,dict,digraph,digraph_utils,edlin,edlin_expand,epp,eval_bits,erl_abstract_code,erl_anno,erl_bits,erl_compile,erl_error,erl_eval,erl_expand_records,erl_internal,erl_lint,erl_parse,erl_posix_msg,erl_pp,erl_scan,erl_tar,error_logger_file_h,error_logger_tty_h,escript,ets,file_sorter,filelib,filename,gb_trees,gb_sets,gen,gen_event,gen_fsm,gen_server,gen_statem,io,io_lib,io_lib_format,io_lib_fread,io_lib_pretty,lists,log_mf_h,maps,math,ms_transform,orddict,ordsets,otp_internal,pool,proc_lib,proplists,qlc,qlc_pt,queue,rand,random,re,sets,shell,shell_default,slave,sofs,string,supervisor,supervisor_bridge,sys,timer,unicode,unicode_util,uri_string,win32reg,zip]},{registered,[timer_server,rsh_starter,take_over_monitor,pool_master,dets]},{applications,[kernel]},{included_applications,[]},{env,[]},{maxT,infinity},{maxP,infinity}]}]}}
{progress,applications_loaded}
{apply,{application,start_boot,[kernel,permanent]}}
{apply,{application,start_boot,[stdlib,permanent]}}
{apply,{c,erlangrc,[]}}
{progress,started}
Eshell V10.7 (abort with ^G)
(lijiang@192.168.9.8)1>
werl
单独启动eshell
窗口
这个里边的打印的中文不会报错 也不会乱码
-noshell
不启动窗口
跑一些erlang
代码后不启动窗口
E:\zz-Typero>erl -noshell -run io format 123456
123456
禁止终端输入-noinput
启动后后台运行-detached
启动后后台运行, 效果等同于-noshell
和 -noinput
加一块使用, 一般使用-detached
包含启动目录-pa Dir1 Dir2
使用: 一般在 -pa Dir1
后会跟 -s
**节点端口设置 **
-net_dist_listen_min
: 最小端口
-net_dist_listen_max
: 最大端口
启动时带入端口配置
E:\zz-Typero>erl -inet_dist_listen_min 40000 -inet_dist_listen_max 44000
Eshell V10.7 (abort with ^G)
1> init:get_argument(inet_dist_listen_min).
{ok,[["40000"]]}
2> init:get_argument(inet_dist_listen_max).
{ok,[["44000"]]}
远程节点 -remsh
- 本地连接控制台:
先启动一个lj1@127.0.0.1
节点, 再启动lj2@127.0.0.1
节点,Ctrl+G
进入用户切换模式, r 链接对应节点; j 列出当前可用节点列表;c 1
切换对应序号的节点
E:\zz-Typero>erl -name lj1@127.0.0.1
Eshell V10.7 (abort with ^G)
(lj1@127.0.0.1)1>
E:\zz-Typero>erl -name lj2@127.0.0.1
Erlang/OTP 22 [erts-10.7] [64-bit] [smp:12:12] [ds:12:12:10] [async-threads:1]
Eshell V10.7 (abort with ^G)
(lj2@127.0.0.1)1>
User switch command
--> j
1* {shell,start,[init]}
--> r 'lj1@127.0.0.1'
--> j
1 {shell,start,[init]}
2* {'lj1@127.0.0.1',shell,start,[]}
--> c 2
Eshell V10.7 (abort with ^G)
(lj1@127.0.0.1)1>
User switch command
--> j
1 {shell,start,[init]}
2* {'lj1@127.0.0.1',shell,start,[]}
--> c 1
(lj2@127.0.0.1)1>
- 使用
-remsh
链接
启动一个节点lj1@127.0.0.1
, -setcookie xxx
设置cookie, 启动另一个节点lijiang@127.0.0.1
, -remsh
链接lj1@127.0.0.1
E:\zz-Typero>erl -name lj1@127.0.0.1 -setcookie lijiang
Eshell V10.7 (abort with ^G)
(lj1@127.0.0.1)1>
启动的节点必须是未使用的节点lijiang@127.0.0.1
, 可以看到启动后就是-remsh
后边的节点了, 也是可以通过Ctrl G
切换的
E:\zz-Typero>werl -setcookie lijiang -name lijiang@127.0.0.1 -remsh lj1@127.0.0.1
E:\zz-Typero>erl -setcookie lijiang -name lijiang@127.0.0.1 -remsh lj1@127.0.0.1
Protocol 'inet_tcp': the name lijiang@127.0.0.1 seems to be in use by another Erlang node
E:\zz-Typero>werl -setcookie lijiang -name lijiang1@127.0.0.1 -remsh lj1@127.0.0.1
Erlang/OTP 22 [erts-10.7] [64-bit] [smp:12:12] [ds:12:12:10] [async-threads:1]
Eshell V10.7 (abort with ^G)
(lj1@127.0.0.1)1> node().
'lj1@127.0.0.1'
(lj1@127.0.0.1)2>
User switch command
-->
--> j
1* {'lj1@127.0.0.1',shell,start,[]}
--> r 'lijiang@127.0.0.1'
--> j
1 {'lj1@127.0.0.1',shell,start,[]}
2* {'lijiang@127.0.0.1',shell,start,[]}
--> c
Eshell V10.7 (abort with ^G)
(lijiang@127.0.0.1)1>
查看版本 -v
+v
E:\zz-Typero>erl +V
Erlang (SMP,ASYNC_THREADS) (BEAM) emulator version 10.7
E:\zz-Typero>erl -V
Eshell V10.7 (abort with ^G)
1>
-hidden
设置为隐藏节点,该节点会连接集群的所有节点,但是在其他节点执行node/0,不会列出它
-hosts Hosts
: erlang
运行在那些服务器的IP
地址;
2. 模拟器参数(emulator flags)
**+A 100
异步进程数, 为某些port调用服务, 范围为0~1024 **
**+e 2000
: 最大ets
数 **
+K true
: 是否启用kernel
的poll
机制(默认为false),增加调度频率:
+P size
: 最大进程数, 默认为
2
18
2^{18}
218
+Q 65535
最大port数
+sbt db
绑定调度器,让cpu
负载均衡,避免大量跃迁
+sub true
开启cpu
负载均衡(false 则采用cpu
密集调度策略)
+spp true
: 开启并行port调度队列,增加系统吞吐量
+smp false
: 调度器提前开启
+swct eager
: cpu
频繁唤醒,增加cpu
利用率
**+swt very_hight | medium | low
: cpu
唤醒时机 **
+sbwt none
: 调度器休眠机制
+zdbbl 204800
: 端口buffer大小
-heart
: 开启erlang
的心跳检测
+t size
: 最大原子数(默认1048576)
+S Max:Online
: 最大调度线程数和在线调度线程数
3. erlang
启动过程
erl_init
初始化bif
,export
等erlang
数据结构load_perloaded
预加载文件模块erl_first_process_opt
启动OTP
架构SMP scheduler
启动过程(不一定开启)erts_sys_main_thread
启动系统主进程(可以指定启动进程数)erts_get_async_ready_queue
(若开启了SMP
,执行队列)emulator stack
设置仿真器大小emulator
启动仿真器
erlang
系统指令
1> erlang:processes(). %% 查看所有进程id
[<0.0.0>,<0.16384.76>,<0.16384.125>,<0.1.0>,<0.16385.76>,
<0.16385.125>,<0.2.0>,<0.16386.76>,<0.3.0>,<0.16387.125>,
<0.4.0>,<0.16388.125>,<0.5.0>,<0.16389.76>,<0.6.0>,
<0.16390.125>,<0.7.0>,<0.16391.125>,<0.16392.125>,
<0.16393.125>,<0.10.0>,<0.16394.76>,<0.16394.125>,
<0.16395.125>,<0.16396.76>,<0.16397.76>,<0.16397.125>,
<0.16398.125>,<0.16400.76>|...]
2> i(). %% 查看所有进程id
Pid Initial Call Heap Reds Msgs
Registered Current Function Stack
<0.0.0> erl_init:start/2 987 3138 0
init init:loop/1 2
<0.16384.76> zm_file:init/1 215 1365 0
erlang:hibernate/3 0
<0.16384.125> zm_file:init/1 209 587 0
erlang:hibernate/3 0
3> self(). %% 查看自身进程id
<0.26136.294>
4> process_flag(priority,high). %% 设置进程优先级
normal
5> process_flag(priority,low).
high
6> erlang:system_info(min_heap_size). %% 查看进程最小堆大小
{min_heap_size,233}
7> erlang:system_info(fullsweep_after). %% 进程GC计数全局参数
{fullsweep_after,65535}
8> erlang:process_info(self(),garbage_collection). %% 进程参数
{garbage_collection,[{max_heap_size,#{error_logger => true,kill => true,
size => 0}},
{min_bin_vheap_size,46422},
{min_heap_size,233},
{fullsweep_after,65535},
{minor_gcs,1}]}
9> erlang:process_info(<0.17001.30>, status). %% 查看某个进程状态
{status,running}
10> erlang:garbage_collect(P). %% 对某个进程执行GC
11> flush(). %% 打印进程邮箱消息
erlang
进程通信——消息传递
- 每个erlang进程都有独自的进程邮箱
- 消息按照按传递的时间次序存储在邮箱里,新接收的消息被放置在接收队列的尾部
- 发送消息不会失败, 消息只会被丢弃, 不会产生错误
- 消息传递是异步的, 不会等待返回结果而阻塞
- 当一个消息不和recieve 中匹配的时候, 如果所有匹配都失败,则将消息留在消息队列中
热更原理: 不停止系统的情况下对运行的代码进行替换更新
erlang
的热更是模块级别的, 一个模块接一个的更新- 主要过程: 编译新的代码, 清除旧代码, 加载新代码
c(Mod) -> %% 热更
compile:file(Mod),
code:purge(Mod),
code:load_file(Mod).
l(Mod) -> %% 加载代码
code:purge(Mod),
code:load_file(Mod).
-
erlang
每个模块都保存2份代码, 当前版本current
和 旧的版本old
, 模块第一次加载时, 代码是current
版本. 如果有新的代码加载,current
版本就变成了old
, 新的代码旧变成了current
版本 -
当代码在热更中, 有进程正在调用这个模块, 执行的代码不会受影响. 热更完成后, 这个就进程执行的代码没有改变, 只不过代码标记为
old
版本; 当新的进程调用这个模块时, 只会访问current
版本的代码;old
版本代码如果没有进程访问, 就会在下次热更时被系统清除掉 -
Question:
- 如果
old
版本代码总是有进程在调用, 在此时热更会发生什么? - Answer: 如果模块存在
old
版本代码,erlang
会kill所有调用old
版本代码的进程, 然后移除old
版本代码. 然后current
变成old
, 新的代码变成current
- 如果
Erlang垃圾回收
-
内存布局:
- 进程控制模块: 保存一些关于进程的信息, 标识符(PID), 当前状态(运行, 等待), 注册名, 初始和当前调用; 同时PCB也会保存一些执行传入消息的指针
- 栈: 向下生长的存储区, 存储区保存输入和输出参数、返回地址、局部变量和用于evaluating expressions(计算表达式)的临时空间
- 堆: 向上生长的存储区, 保存进程邮箱的物理消息, 像列表、元组和Binaries这种的复合项以及比像浮点数这种一个机器字更大的对象
- 超过64机器字的二进制项(Refc Binary: Reference Counted Binary)不会存储在进程私有堆里, 存储在一个大的共享堆里, 只要有那个Refc Binary指针的进程都可以访问这个堆. 这个储存在进程私有堆中的指针叫作ProcBin
-
分代复制, 独立运行在每个Erlang进程私有堆的内部, 也是发生在全局共享堆中的引用计数垃圾回收
-
私有堆GC
-
分代GC把堆分为了新生和老年代两个部分, 新生代是为新分配的数据准备的,老年代是为了在数次GC启动后生存下来数据的; 如果一个对象在GC循环生存下来,那么它在短期内成为垃圾的几率将会很低,这也是这个划分的依据所在
-
分代帮助了GC减少在还没有成为垃圾数据上的不必要的循环
-
Erlang垃圾回收有两个策略:Generational (分代,Minor)和Fullsweep (全扫描,Major)。分代的GC只收集新生的堆,而fullsweep的堆新老都会收集
-
-
四种GC场景
-
场景 1: 一个生存期较短的进程, 在存活期间使用的堆内存也没有超过 *min_heap_size,*那么在进程结束是全部内存即被回收
Spawn >
No GC
> Terminate -
场景 2: 假设一个新创建的进程,当进程的数据增长超过了min_heap_size时, fullsweep GC即被触发, 因为在此之前还没有任何GC被触发,所以堆区还没有被分成年轻代和年老代. 在第一次fullsweep GC结束以后, 堆区就会被分为年轻代和年老代了, 从这个时候起, GC的策略就被切换为 generational GC了, 直到进程结束
Spawn >
Fullsweep
>Generational
> Terminate -
场景3: 某些情景下, GC策略会从generation再切换回fullsweep.
- 一种情景是, 在运行了一定次数(fullsweep_after计数)的genereration GC之后,系统会再次切换回fullsweep. 参数fullsweep_after可以是全局的也可以是单进程的,
1> erlang:system_info(fullsweep_after). %% 全局参数 {fullsweep_after,65535} 2> erlang:process_info(self(),garbage_collection). %% 进程参数 {garbage_collection,[{max_heap_size,#{error_logger => true,kill => true, size => 0}}, {min_bin_vheap_size,46422}, {min_heap_size,233}, {fullsweep_after,65535}, {minor_gcs,1}]}
- 另外一种情景是, 当generation GC(minor GC)不能够收集到足够的内存空间时
- 最后一种情况是, 当手动调用函数**
garbage_collector(PID)
时. 在运行fullsweep之后, GC策略再次切换回generation GC**, 直到以上的任意一个情景再次出现
Spawn > Fullsweep > Generational > Fullsweep > Generational > … > Terminate
-
场景4: 假设在场景3里面,第二个fullsweep GC依然没有回收到足够的内存, 那么系统就会为进程增加堆内存, 然后该进程就回到第一个场景,像刚创建的进程一样首先开始一个fullsweep,然后循环往复.
Spawn > Fullsweep > Generational > Fullsweep > Increase Heap > Fullsweep > … > Terminate
-
总结: 减少分代GC的次数可以怎么做: 初始化足够大的初始内存;
fullsweep_after
参数控制深扫描的频率
%% 手动执行垃圾回收 gc() -> [erlang:garbage_collect(P) || P <- erlang:processes(), {status, waiting} == erlang:process_info(P, status)], erlang:garbage_collect(), ok.
总结: erlang进程堆的gc是分代gc的,这个只是全局层面的, 在底层erlang还是走了标记清除的路子. 标记清除这种gc方式是定期执行的,首先gc不够及时,其次,在gc执行期间开销比较大,会引起中断. 不过每个erlang进程的堆区域是独立的,gc可以独立进行,加上它内存区域比较小,还有erlang的变量是单次赋值,无需多次追踪,因此,erlang进程gc的延迟不会引起全局的中断
- 这些知识可以帮助你通过调整GC的发生和策略使你的系统运行更快。
- 其次,这是我们明白从垃圾回收的角度使Erlang变成软件实时平台的重要原因的地方。这是因为每个进程都有它自己的堆和它自己的GC, 所以每次GC出现在一个进程中的时候,只是停止正在收集过程中的Erlang进程,但不会停止其他的进程
-
-
共享堆GC(通过引用计数来实现)
- 共享堆的每个对象都有一个引用计数,表示该对象被几个进程持有,当一个对象的引用计数变成0就可以回收。
erlang
NIF: 本地实现的函数()
在Erlang调用C代码时,**NIF(Native Implemented Function)**是比port driver更简单和有效的实现方式. NIF可以使我们可以用C实现相同的程序逻辑,但速度比用纯Erlang的快,跟C的速度很相近
-
基本原理
C语言编译生成的动态库(*.so)在Erlang调用C模块时动态加载到Erlang的进程空间中,所以这是用Erlang调用C代码最高效的方式。调用NIF不用上下文的切换开销,但是安全性不是很高,因为NIF的crash会导致整个Erlang进程crash。
-
编程模式(记不住)
在用NIF时,我们要告知Erlang哪些函数是用C实现的,在NIF中,每个这样的Erlang-C映射函数由一个C的数据结构ErlNifFunc来表示:
typedef struct
{
const char* name; //name表示要在Erlang中用C替换掉的函数
unsigned arity; //arity表示name这个函数的参数个数
//fptr是一个指向函数的指针,它是name函数对应的C语言实现。
ERL_NIF_TERM (*fptr)(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]);
}ErlNifFunc;
//在C语言中实现的NIF函数要有下面的定义方式:
static ERL_NIF_TERM FuncName(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
// 最终,要通过ERL_NIF_INIT宏将C实现和对应的Erlang模块绑定起来,实现NIF的初始化:
ERL_NIF_INIT(MODULE, ErlNifFunc funcs[], load, reload, upgrade, unload)
在Erlang代码中,要用erlang:load_nif/2来加载NIF到当前进程的内存空间中。
-
数据交换: 即函数调用时将Erlang传来的数据转换成C的,函数计算的结果返回时将C的数据转换成Erlang的. 虽然NIF也提供了一系列的
enif_is_*
函数进行判断,主要靠程序员自己根据约定转换成C中具体的数据类型-
所有的输入和输出都由统一的
ERL_NIF_TERM
类型表示, 最后所有的NIF的C函数就可以统一用 Erlang代码 -
从
erlang
到c
(输入的处理): Erlang传给C的参数的转换过程是通过一系列enif_get_*
函数完成的。 -
从
c
到erlang
(输出的处理): C返回给Erlang的数据的转换过程是通过一系列enif_make_*
函数完成的 -
从NIF返回给erlang的这些**
ERL_NIF_TERM
数据将由erlang节点管理并负责垃圾回收**。
-
-
简单例子
用NIF实现一个求某个数N以内的素数的程序
- cprime.c程序
#include <stdbool.h> #include <math.h> #include "erl_nif.h" // 是否是素数 static bool isPrime(int i) { int j; int t = sqrt(i) + 1; for(j = 2; j <= t; ++j) { if(i % j == 0) return false; } return true; } // 找到一个素数(主体函数) static ERL_NIF_TERM findPrime(ErlNifEnv *env, int argc, ERL_NIF_TERM argv[]) { int n; if(!enif_get_int(env, argv[0], &n)) return enif_make_badarg(env); else { int i; ERL_NIF_TERM res = enif_make_list(env, 0); for(i = 2; i < n; ++i) { if(isPrime(i)) res = enif_make_list_cell(env, enif_make_int(env, i), res); } return res; } } // erlang函数到c的映射 static ErlNifFunc nif_funcs[] = { {"findPrime", 1, findPrime} }; //通过ERL_NIF_INIT宏将C实现和对应的Erlang模块绑定起来,实现NIF的初始化 ERL_NIF_INIT(prime, nif_funcs, NULL, NULL, NULL, NULL)
- erlang代码
-module(prime). -export([load/0, findPrime/1]). load() -> erlang:load_nif("./cprime", 0). findPrime(N) -> io:format("this function is not defined!~n").
- make之后运行结果:
1> prime:load(). ok 2> prime:findPrime(50). [47,43,41,37,31,29,23,19,17,13,11,7,5,3]
erlang
timer机制
timer 的实现是基于 receive...after
语法层面的, 一旦timeout,立即把进程加到调度队列,使用频率比较高
-
erlang:start_timer(Time, P, Msg) -> TimeRef
-
erlang:send_after(Time, P, Msg) -> TimeRef
Time: 最大的值为2^32 -1 milliseconds, 大约为49.7天
P : 本地节点, 类型为atom()或者pid()
冷知识:
1. 如果是Dest::atom(), 即便这个Dest 没有绑定一个process 也不会报错, 而是返回正常的TimeRef; 在**进程不存在和进程退出**的情况下,该timer 不是立即取消,而是**在Time 时间结束的时候才取消**的 2. 如果是Name::pid(), 如果进程存在或者进程退出,该timer 会立即取消
-
两个函数都返回 TimerRef。用户可以用这个TimerRef来取消定时器
-
erlang:read_timer(TimeRef) -> Time
读取定时器剩下的时间 -
erlang:cancel_timer(TimeRef) ->Time|false
取消一个定时器 -
区别:
- 在超时的时候发送的消息不同,
send_after <------------->Msg
start_timer <------------->{timeout, TimerRef, Msg}
- 当我们cancel 一个timer的时候, 并不能保证那个Msg 从进程信箱中去除
start_timer <------------->{timeout, TimerRef, Msg} 如果有timerRef , 就可以利用receive…after 0 来匹配TimeRef, 进而去除进程信箱中的Msg
send_after <------------->Msg 但是如果是send_after 我们没有办法做到这一点
-
了解: timer module 中也有起定时器的操作, 但是这个timer 会有一个独立的进程去处理这个定时器,所以如果有很多进程都用这个module 进行起和删除定时器的话, 效率上自然有问题。
erlang
的调度机制
Erlang调度器的细节及其重要性 | Time is all (oschina.io)
- 抢占式:一个抢占式调度器在执行的任务间进行上下文切换,它有权力抢占(中断)任务并且在不需要被抢占任务的配合下的稍后恢复执行它们。实现这样的功能是基于如下几个因素,比如:任务的优先级,时间切片或者规约数。
- 协作式:一个协作式调度器需要任务协作来进行上下文切换。在这种方式下,调度器简单地让任务周期性地或者空闲地时候自愿地释放控制权,然后启动一个新的任务并且再一次等待它自愿地归还控制权。
软实时系统通常采用抢占式调度
Erlang作为一个多任务软实时平台采用的就是抢占式调度。Erlang调度器的职责就是选择一个进程并执行它的代码。它也处理垃圾回收和内存管理。如何选择一个进程来执行是基于每个进程可配置的优先级,并且同一优先级的进程是轮询地被调度的。另外,执行中的进程被抢占的因素是基于自上次该进程被选中执行后一定数量的==规约数==而不管它的优先级如何。 规约数是每个进程的一个计数器,一般每调用一次函数,它就加一。当一个进程的计数器达到最大规约数时,调度器就会抢占进程和进行上下文切换。例如,在Erlang/OTP R12B 计数器的最大值是2000规约数。
R11B之前的调度
一个调度器对应一个运行队列
Erlang还不支持SMP(对称多处理器),因此它只有一个调度器运行在操作系统主进程的线程里,并且相应的只有一个运行队列。 调度器从运行队列选择可运行的Erlang进程和IO任务来执行。
R11B和R12B的调度
多个调度器对应一个运行队列:这时会比上面慢,因为锁保护
SMP支持被加入Erlang虚拟机里,所以它可以有1到1024个运行在操作系统进程的线程里的调度器。然而,这个版本的调度器只能从一个共用运行队列里选取可执行任务。
由于这种方式造成并行,使得所有共享数据结构都要用锁保护起来。例如运行队列本身就是一个必须被保护起来的共享数据结构。虽热锁会造成一些性能损失,但是新的调度器在多核处理器上带来的性能提升还是很可观的。
R13B后的调度
N个调度器,N个运行队列。每个调度器对应一个运行队列
每个调度器有它自己的运行队列。在多核多调度器的系统里,这将减少锁冲突数量并且提升系统整体性能。
这种方式在访问运行队列时锁冲突解决了,不过却引入了一些新问题:
- 如何在运行队列中分配任务做到公平?
- 如果一个调度器被分配了过多的任务而另外的调度器却很清闲,这个问题如何解决?
- 基于什么样的命令一个调度器可以从一个过载的调度器偷任务?
- 要是我们启动了很多调度器,但是却很少任务,如何处理?
这些问题使得Erlang开发团队引入一个概念使得调度公平和高效,这个概念就是迁移逻辑。它尝试在基于从系统收集来的统计数据上控制和平衡运行队列。
调度线程
开启和关闭SMP erl -smp [auto|disable|enable]
当用erl启动脚本启动Erlang模拟器的时候,可以通过给+S标志传递两个用冒号分割的数字来指定最大可用调度线程数和在线调度线程数。
最大可用调度线程数只能在启动的时候指定而且在运行时是固定不变的,但是在线调度线程数可以在启动和运行时被指定和修改。例如我们可以在启动一个模拟器的时候指定16个最大调度线程和8个在线调度线程。
%% erl +S MaxAvailableSchedulers:OnlineSchedulers
$ erl +S 16:8
然后在shell里在线调度线程可以被修改,如下:
> erlang:system_info(schedulers). %% => returns 16
> erlang:system_info(schedulers_online). %% => returns 8
> erlang:system_flag(schedulers_online, 16). %% => returns 8
> erlang:system_info(schedulers_online). %% => returns 16
进程优先级
%% 优先级可以在进程内通过调用erlang:process_flag/2函数来设置。
4> process_flag(priority,high). %% 设置进程优先级
normal
5> process_flag(priority,low).
high
优先级可以是 low、normal、high、max 这些原子中的任何一个。默认优先级是normal,max优先级是保留给Erlang运行时内部使用不应被一般进程使用。
运行队列统计
运行队列持有准备好执行但未被调度器选中执行的进程。可以通过调用erlang:statistics(run_queue)
获取在所有可用运行队列已经准备好可运行的进程数。
启动Erlang模拟器,给它4个在线调度器,并且给它们10个非常消耗CPU的并发进程。这些进程计算一个很大数字的素数。
%% 就绪
> erlang:statistics(online_schedulers). %% => 4 译者注:此处的函数有误,应该是 erlang:system_info(schedulers_online).
> erlang:statistics(run_queue). %% => 0
%% 并发创建10个重型进程
> [spawn(fun() -> calc:prime_numbers(10000000) end) || _ <- lists:seq(1, 10)].
%% 运行队列中还有任务要做
> erlang:statistics(run_queue). %% => 8
%% Erlang shell依然可以响应,非常棒!
> calc:prime_numbers(10). %% => [2, 3, 5, 7]
%% 等一会儿
> erlang:statistics(run_queue). %% => 4
%% 等一会儿
> erlang:statistics(run_queue). %% => 0
因为并发进程数大于在线调度器,这将花些时间让调度器执行运行队列里的进程并最终清空运行队列。
有趣的是,创建了这些重型进程后,Erlang模拟器任然因为它的抢占式调度可以响应其他请求。Erlang的抢占式调度不会让这些重型进程消耗掉所有运行时,其他轻量并且重要的进程也可以被执行,这个特性在实现一个软实时系统的时候是非常棒的。
结论
当在一个软实时系统里系统以高水平的公平性和即时的响应需要扩展到所有处理资源的时候,跟踪、平衡、执行、迁移和抢占进程这些额外的处理成本是完全可负担的。
顺便值得一提的是,完全抢占式调度是几乎所有操作系统都支持的特性,但在高层次的平台,语言或库里,Erlang
虚拟机几乎是唯一完全抢占式调度的,因为JVM
依赖于操作系统的调度器,CAF
这个C++
actor
库用协作式调度,Go
也不是完全抢占式调度,还有诸如Python
的Twisted
,Ruby
的Event Machine
和Node js
也不是完全抢占式调度的。这并不意味着对于所有的挑战这都是最好的选择,而是说我们如果要实现一个低延时的软实时系统,Erlang
是一个好的选择。