erlang 问题随笔

erlang程序语言

Q&A

Q: erlang otp是什么

A: Erlang OTP (Open Telecom Platform) 是一个开源的软件平台,用于构建高可靠性、高可扩展性、分布式、并发的应用程序。Erlang OTP 提供了许多强大的工具和库,使得开发者可以轻松地构建和部署分布式系统,例如,Erlang OTP 提供了进程管理、网络通信、故障转移、代码升级等功能,这些功能使得 Erlang OTP 成为构建高可靠性应用程序的首选语言和平台。

Q: erlang OTP有哪些东西

A: Erlang OTP 提供了许多强大的工具和库,包括:

  1. 进程管理:Erlang OTP 提供了进程管理工具,使得开发者可以轻松地创建、监控和终止进程。这些进程可以独立运行,也可以通过消息传递进行通信,从而实现分布式计算。
  2. 网络通信:Erlang OTP 提供了网络通信库,包括 TCPUDPSCTP TLS 等协议的支持。这些库可以帮助开发者构建高性能、可扩展的网络应用程序。
  3. 共享内存:Erlang OTP 提供了多种共享内存机制,包括 ETS(Erlang Term Storage)和 Mnesia 等。这些机制可以帮助开发者在分布式环境下共享数据。
  4. 代码升级:Erlang OTP 提供了代码升级工具,使得开发者可以在系统运行时更新代码,而无需停机重启。这使得系统升级变得更加容易和安全。
  5. 故障转移:Erlang OTP 提供了故障转移机制,使得系统可以在节点故障时自动恢复。这使得系统更加可靠和健壮。
  6. 应用程序框架:Erlang OTP 提供了应用程序框架,包括 gen_servergen_statemgen_event supervisor等。这些框架可以帮助开发者快速构建高可靠性、高可扩展性的应用程序。

Q: erlang虚拟机

A: Erlang 虚拟机是 Erlang/OTP 平台的核心组成部分,也是其最重要的技术之一。Erlang 虚拟机在运行时会将 Erlang 代码编译成字节码,然后在虚拟机中执行这些字节码。

Erlang 虚拟机具有以下特点:

  1. 轻量级:Erlang 虚拟机比较轻量级,可以在资源有限的环境下运行,例如嵌入式系统。
  2. 并发性:Erlang 虚拟机通过进程间的消息传递实现并发,而不是使用共享内存。这样可以避免许多并发编程中常见的问题,例如死锁和竞态条件等。
  3. 分布式:Erlang 虚拟机天生支持分布式计算,可以轻松地将应用程序部署到多个节点上。
  4. 热代码升级:Erlang 虚拟机支持热代码升级,也就是在应用程序运行时更新代码而不需要停机。这样可以使得应用程序变得更加可靠和可升级。
  5. 容错性:Erlang 虚拟机具有强大的容错性,可以在节点故障时自动恢复。这使得 Erlang 应用程序变得更加健壮和可靠。

总之,Erlang 虚拟机是 Erlang/OTP 平台的核心技术之一,具有轻量级、并发性、分布式、热代码升级和容错性等特点,使得它成为构建高可靠性、高可扩展性、分布式的应用程序的首选平台。

Q: erlang启动参数

A: Erlang 启动参数是指在启动 Erlang 虚拟机时,可以通过命令行或配置文件传递的一些参数,以便控制虚拟机的行为。以下是一些常见的 Erlang 启动参数:

  1. -sname:指定节点的名称,用于在分布式环境中唯一标识节点。
  2. -name:指定节点的全局名称,可以在全局范围内唯一标识节点。
  3. -setcookie:设置节点之间的通信加密密钥。
  4. -kernel:用于设置 Erlang 虚拟机的内核参数,例如进程数、垃圾回收间隔等。
  5. -s:在启动时执行指定的模块和函数,例如-s mod function。
  6. -pa:添加指定的目录到 Erlang 搜索路径中,用于加载模块。
  7. -run:在启动时执行指定的模块和函数,例如-run mod function。
  8. -config:指定配置文件的路径,用于设置应用程序的配置参数。
  9. -boot:指定引导文件的路径,用于启动应用程序。
  10. -detached:将 Erlang 虚拟机作为守护进程运行。

这些启动参数可以通过命令行传递,也可以通过配置文件指定。在启动 Erlang 应用程序时,可以根据需要使用这些参数来控制虚拟机的行为和设置。

erlang启动参数

erlang 启动参数主要有3种, 分别是emulator flag, flagsplain 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:all

启动时编译 -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: 是否启用kernelpoll机制(默认为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启动过程
  1. erl_init 初始化 bifexporterlang数据结构
  2. load_perloaded 预加载文件模块
  3. erl_first_process_opt 启动OTP架构
  4. SMP scheduler 启动过程(不一定开启)
  5. erts_sys_main_thread 启动系统主进程(可以指定启动进程数)
  6. erts_get_async_ready_queue (若开启了SMP,执行队列)
  7. emulator stack 设置仿真器大小
  8. 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就可以回收

erlangNIF: 本地实现的函数()

在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代码

    • erlangc(输入的处理): Erlang传给C的参数的转换过程是通过一系列enif_get_*函数完成的。

    • cerlang(输出的处理): 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也不是完全抢占式调度,还有诸如PythonTwistedRubyEvent MachineNode js也不是完全抢占式调度的。这并不意味着对于所有的挑战这都是最好的选择,而是说我们如果要实现一个低延时的软实时系统,Erlang是一个好的选择。

  • 26
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值