@ -0,0 +1,88 @@ | |||
文件命名为 | |||
application_Name.app | |||
格式如下: | |||
{application,"app名字", | |||
[ | |||
{description,"app描述"}, | |||
{vsn ,"版本号"}, | |||
{id ,Id},%%app id 同 erl -id ID | |||
{modules,[Modules]},%%app包含的模块,systools模块使用它来生成script、tar文件 | |||
{maxP,Num},%%进程最大值 | |||
{maxT,Time},%%app运行时间 单位毫秒 | |||
{registered,[mod]},%%指定app 名字模块,systools用来解决名字冲突 | |||
{included_applictions ,[XX]},%%指定子 app,只加载,但是不启动 | |||
{applictions,[xxxx]},%%启动自己的app前,将会首先启动此列表的app | |||
{env,[xxxx]},%%配置app的env,可以使用application:get_env获取 | |||
{mod,{xxx,args}},%%指定app启动模块,参数,对应自己app的application behavior | |||
{start_phases,[{xxx,xxx}]]%%指定启动阶段一些操作,对应otp application start_phase函数 | |||
] | |||
} | |||
必须要配置的为description,vsn,modules,registered,applications。 | |||
Application为应用名, | |||
descripttion为应用的简单描述 | |||
id 产品标识 | |||
vsn 应用版本 | |||
modules 应用所涉及到的module | |||
registered 注册进程 | |||
applications 本应用启动时需要事先启动的其他应用 | |||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% | |||
erlang官网说明 | |||
{application, Application, | |||
[{description, Description}, | |||
{id, Id}, | |||
{vsn, Vsn}, | |||
{modules, Modules}, 本应用程序引入的所有模块。systools 在生成启动脚本和tar文件时使用这个列表。一个模块只能在一个应用程序中定义 | |||
{maxP, MaxP}, 已经弃用了 | |||
{maxT, MaxT}, 时间单位为毫秒 | |||
{registered, Names}, 注册过程的所有名称均在此应用程序中启动。systools使用这个列表来检测不同应用程序之间的名字冲突 | |||
{included_applications, Apps}, 此应用程序包含的所有应用程序 当此应用程序启动时,应用程序控制器会自动加载所有包含的应用程序,但不会启动。假设包含应用程序的最高管理者由本应用程序的主管启动 | |||
{applications, Apps}, 允许启动此应用程序之前必须启动的所有应用程序。systools使用这个列表来生成正确的启动脚本。缺省为空列表,但请注意所有应用程序对(至少)Kernel和STDLIB都有依赖关系 | |||
{env, Env}, 应用程序使用的配置参数。通过调用application:get_env / 1,2来检索配置参数的值 | |||
{mod, Start}, 指定应用程序回调模块和启动参数 | |||
对于作为监督树实施的应用程序,密钥mod是必需的,否则应用程序控制器不知道如何启动它。 对于没有进程的应用程序(通常是代码库,例如STDLIB),可以省略mod | |||
{start_phases, Phases}, | |||
{runtime_dependencies, RTDeps}]}. 应用程序依赖的应用程序版本列表 | |||
Value Default | |||
----- ------- | |||
Application atom() - | |||
Description string() "" | |||
Id string() "" | |||
Vsn string() "" | |||
Modules [Module] [] | |||
MaxP int() infinity | |||
MaxT int() infinity | |||
Names [Name] [] | |||
Apps [App] [] | |||
Env [{Par,Val}] [] | |||
Start {Module,StartArgs} [] | |||
Phases [{Phase,PhaseArgs}] undefined | |||
RTDeps [ApplicationVersion] [] | |||
Module = Name = App = Par = Phase = atom() | |||
Val = StartArgs = PhaseArgs = term() | |||
ApplicationVersion = string() | |||
如果要使用systools中的函数 需要设置下面的key参数 | |||
description vsn modules registered applications | |||
其他的key被systools忽略 | |||
应用的策略 | |||
application:start(Name, Type) | |||
type: | |||
• permanent: if the app terminates, the entire system is taken down, excluding manual termination of the app with application:stop/1. | |||
• transient: if the app terminates for reason normal, that’s ok. Any other reason for termination shuts down the entire system. | |||
• temporary: the application is allowed to stop for any reason. It will be reported, but nothing bad will happen. | |||
@ -0,0 +1,243 @@ | |||
### 监督原则 | |||
主管负责启动,停止和监视其子进程。主管的基本思想是,必须通过在必要时重新启动子进程来保持其子进程活动。 | |||
主管的孩子被定义为孩子规格列表 。当主管启动时,将根据此列表从左到右依次启动子进程。主管终止时,它首先以相反的启动顺序从右到左终止其子进程。 | |||
sup_flags() = | |||
#{strategy => strategy(), % optional | |||
intensity => non_neg_integer(), % optional | |||
period => pos_integer() % optional | |||
} | |||
#### 重启策略 | |||
one_for_one-如果一个子进程终止并要重新启动,则仅影响该子进程。这是默认的重启策略。 | |||
one_for_all-如果一个子进程终止并要重新启动,则所有其他子进程均终止,然后重新启动所有子进程。 | |||
rest_for_one-如果一个子进程终止并要重新启动,则子进程的“剩余”(即,按照启动顺序终止的子进程之后的子进程)将终止。 | |||
然后,终止的子进程以及重新启动后的所有子进程。 | |||
simple_one_for_one-简化的one_for_one 主管,其中所有子进程都是动态添加的,具有相同进程类型(即,运行相同代码)的实例。 | |||
功能 delete_child / 2和 restart_child / 2 是无效simple_one_for_one监事和回报 {错误,simple_one_for_one}如果指定的主管使用此重启策略。 | |||
通过将子级的pid()指定为第二个参数,可以将函数terminate_child / 2用于simple_one_for_one主管下的子级。 | |||
如果改用子规范标识符,则 terminate_child / 2返回 {error,simple_one_for_one}。 | |||
由于simple_one_for_one主管可以有多个子代,因此它会异步关闭所有子代。这意味着孩子们并行进行清理,因此未定义他们停止的顺序 | |||
为了防止主管进入子进程终止和重新启动的无限循环,使用上面映射中的键强度和周期指定的两个整数值来定义最大重新启动强度。 | |||
假设值MAXR为强度 和MAXT为周期,然后,如果超过MAXR 重新启动内发生MAXT秒,监控终止所有的子进程,然后本身。在这种情况下, | |||
主管本身的终止原因将被关闭。 强度默认为1,期间默认为 5。 | |||
#### 子进程规范 | |||
child_spec() = | |||
#{ | |||
id => child_id(), % mandatory | |||
start => mfargs(), % mandatory | |||
restart => restart(), % optional | |||
shutdown => shutdown(), % optional | |||
type => worker(), % optional | |||
modules => modules() % optional | |||
} | |||
id用于由主管内部标识子规范。该ID关键是强制性的。 | |||
请注意,这个在职业上的标识符被称为“名称”。现在尽可能使用术语“标识符”或“ id”,但为了保持向后兼容性,仍然可以找到“名称”的某些出现,例如在错误消息中。 | |||
start定义用于启动子进程的函数调用。它必须是用作apply(M,F,A)的模块功能参数元组{M,F,A}。 | |||
启动函数必须创建并链接到子进程,并且必须返回{ok,Child}或 {ok,Child,Info},其中Child是子进程的pid,Info是主管忽略的任何术语。 | |||
restart定义终止的子进程何时必须重新启动。 | |||
一个permanent 的子进程总是会重启。 | |||
一个temporary 的子进程不会再重新启动(甚至当主管的重启策略是rest_for_one或one_for_all和兄弟姐妹的死亡原因临时进程被终止)。 | |||
一个transient的子进程重新启动,只有当它异常终止,即与另一个出口原因,而不是 normal,shutdown,或{shutdown,Term}。 | |||
该restart是可选的。如果未指定,则默认为permanent.。 | |||
shutdown定义必须终止子进程的方式。 | |||
brutal_kill意味着子进程使用exit(Child,kill)无条件终止 | |||
整数超时值表示主管通过调用exit(Child,shutdown)告诉子进程终止 ,然后等待退出信号,原因是该子进程退出了shutdown。如果在指定的毫秒数内未收到退出信号,则使用exit(Child,kill)无条件终止子进程 。 | |||
infinity 如果子进程是另一个主管,则必须将关闭时间设置为infinity,以使子树有足够的时间关闭。 | |||
对于类型为Supervisor的孩子,将关闭时间设置为无穷大以外的任何时间,都 可能导致比赛状态,在该情况下,所讨论的孩子会取消其自己的孩子的链接,但无法在杀死孩子之前终止他们。 | |||
如果子进程是工作进程,也可以将其设置为infinity。 | |||
当子进程为工作进程时,将关闭时间设置为无穷大时要小心 。因为在这种情况下,监视树的终止取决于子进程,所以它必须以安全的方式实现,并且其清除过程必须始终返回。 | |||
在shutdown 是可选的。如果未指定,则在子类型为worker的情况下默认为5000,在子类型为supervisor的情况下默认为infinity。 | |||
type specifies if the child process is a supervisor or a worker. | |||
该type关键是可选的。如果未指定,则默认为worker。. | |||
modules 在代码替换期间,释放处理程序将使用modules模块来确定哪些进程正在使用某个模块。作为一个经验法则,如果孩子过程是一个 主管,gen_server或, | |||
gen_statem,这是为与一个元素列表[模块],其中模块是回调模块。如果子进程是具有动态回调模块集的事件管理器(gen_event),则 必须使用动态值。 | |||
有关发布处理的更多信息,请参见 OTP设计原则中的发布处理。 | |||
该modules的关键是可选的。如果未指定,则默认为[M],其中M来自孩子的开头{M,F,A}。 | |||
Internally 主管还跟踪PID的 孩子的孩子的过程中,或者不确定如果没有PID存在。 | |||
#### 函数 | |||
count_children(SupRef) -> PropListOfCounts | |||
Types SupRef = sup_ref() | |||
PropListOfCounts = [Count] | |||
Count = | |||
{specs, ChildSpecCount :: integer() >= 0} | 已死或活着的孩子总数。 | |||
{active, ActiveProcessCount :: integer() >= 0} | 此主管管理的所有正在运行的子进程的计数 | |||
对于 simple_one_for_one主管,不会执行任何检查以确保每个子进程仍处于活动状态,尽管除非主管非常重载,否则此处提供的结果可能非常准确。 | |||
{supervisors, ChildSupervisorCount :: integer() >= 0} | 规范列表中标记为child_type =主管的所有子进程的计数 ,无论该子进程是否仍在运行 | |||
{workers, ChildWorkerCount :: integer() >= 0} -标记为所有儿童的数量 CHILD_TYPE =工人在规范列表中,如果不管孩子进程仍然活着。 | |||
delete_child(SupRef, Id) -> Result | |||
Types SupRef = sup_ref() | |||
Id = child_id() | |||
Result = ok | {error, Error} | |||
Error = running | restarting | not_found | simple_one_for_one | |||
告诉主管SupRef删除Id标识的子规范。相应的子进程一定不能运行。使用 terminate_child / 2终止它。 | |||
如果成功,函数将返回ok。如果存在由ID标识的子规范,但相应的子进程正在运行或将要重新启动,则该函数分别返回{error,running}或 {error,restarting}。如果由ID标识的子规范不存在,则该函数返回{error,not_found}。 | |||
get_childspec(SupRef, Id) -> Result | |||
Types | |||
SupRef = sup_ref() | |||
Id = pid() | child_id() | |||
Result = {ok, child_spec()} | {error, Error} | |||
Error = not_found | |||
返回由Id在主管SupRef下标识的子项的子项规范图。返回的映射包含所有键,包括必需键和可选键。 | |||
restart_child(SupRef, Id) -> Result | |||
Types | |||
SupRef = sup_ref() | |||
Id = child_id() | |||
Result = | |||
{ok, Child :: child()} | | |||
{ok, Child :: child(), Info :: term()} | | |||
{error, Error} | |||
Error = | |||
running | restarting | not_found | simple_one_for_one | term() | |||
告诉主管SupRef重新启动与Id标识的子规范相对应的子进程。子规范必须存在,并且相应的子进程一定不能运行。 | |||
注意,对于临时子代,子代说明在子代终止时会自动删除;因此,不可能重新启动此类子级。 | |||
如果由ID标识的子规范不存在,则该函数返回{error,not_found}。如果子规范存在但相应的进程已在运行,则该函数返回{error,running}。 | |||
如果子进程启动函数返回{ok,Child} 或{ok,Child,Info},则将pid添加到主管,并且该函数返回相同的值。 | |||
如果子进程启动函数返回ignore,则pid保持设置为undefined,该函数返回{ok,undefined}。 | |||
如果子进程启动函数返回错误元组或错误值,或者失败,则该函数返回 {error,Error},其中Error是包含有关错误信息的术语。 | |||
start_child(SupRef, ChildSpec) -> startchild_ret() | |||
Types | |||
SupRef = sup_ref() | |||
ChildSpec = child_spec() | (List :: [term()]) | |||
startchild_ret() = | |||
{ok, Child :: child()} | | |||
{ok, Child :: child(), Info :: term()} | | |||
{error, startchild_err()} | |||
startchild_err() = | |||
already_present | {already_started, Child :: child()} | term() | |||
ChildSpec必须是有效的子规范(除非主管是simple_one_for_one 主管;请参见下文)。通过使用子规范中定义的启动功能来启动子进程。 | |||
对于simple_one_for_one主管,将使用Module:init / 1中定义的子规范,而ChildSpec必须改为是List的任意列表。然后,通过将List附加到现有的启动函数参数(即,通过调用apply(M,F,A ++ List))来 启动子进程,其中{M,F,A}是子规范中定义的启动函数。 | |||
如果已经存在带有指定标识符的子规范,则将丢弃ChildSpec,并且该函数将根据相应的子进程是否在运行而返回{error,already_present}或 {error,{already_started,Child}}。 | |||
如果子进程启动函数返回 {ok,Child}或 {ok,Child,Info},则将子规范和pid添加到主管,并且该函数返回相同的值。 | |||
如果子进程启动函数返回ignore,则将子规范添加到主管(除非该主管是simple_one_for_one主管,请参见下文),将pid设置为undefined,并且该函数返回 {ok,undefined}。 | |||
对于simple_one_for_one主管,当子进程启动函数返回ignore时,该函数将返回 {ok,undefined},并且没有子级添加到主管。 | |||
如果子进程启动函数返回错误元组或错误值,或者失败,则子规范被丢弃,函数返回{error,Error},其中 Error是包含有关错误和子规范的信息的术语。 | |||
start_link(模块,Args)-> startlink_ret() | |||
start_link(SupName,Module,Args)-> startlink_ret() | |||
种类 | |||
SupName = sup_name() | |||
模块= module() | |||
Args = term() | |||
startlink_ret()= | |||
{确定,pid()} | 忽略| {错误,startlink_err()} | |||
startlink_err()= | |||
{已经开始,pid()} | {关机,term()} | 术语() | |||
sup_name()= | |||
{本地,名称:: atom()} | | |||
{global,Name :: atom()} | | |||
{via,Module :: module(),Name :: any()} | |||
创建一个监督程序,作为监督树的一部分。例如,该功能可确保主管链接到呼叫过程(其主管)。 | |||
创建的主管进程将调用 Module:init / 1来查找有关重启策略,最大重启强度和子进程的信息。为了确保同步启动过程,在返回Module:init / 1并启动所有子进程之前,不会返回 start_link / 2,3。 | |||
如果SupName = {local,Name},则主管使用register / 2在本地注册为Name。 | |||
如果SupName = {global,Name},则使用 global:register_name / 2将主管全局注册为Name。 | |||
如果 SupName = {via,Module,Name},则使用Module表示的注册表将主管注册为Name。所述模块的回调必须导出功能REGISTER_NAME / 2, unregister_name / 1,和发送/ 2,它必须表现得像在相应的功能 全球。因此, {via,global,Name}是有效的引用。 | |||
如果未提供姓名,则主管未注册。 | |||
模块是回调模块的名称。 | |||
Args是作为参数传递给Module:init / 1的任何术语。 | |||
如果成功创建了主管及其子进程(即,如果所有子进程启动函数都返回 {ok,Child},{ok,Child,Info}或ignore),则该函数返回{ok,Pid},其中Pid是主管的pid。 | |||
如果已经存在具有指定SupName的进程,则 该函数返回 {error,{already_started,Pid}},其中Pid是该进程的pid。 | |||
如果Module:init / 1返回ignore,则此函数也返回ignore,并且supervisor因normal终止。 | |||
如果Module:init / 1失败或返回错误值,则此函数返回{error,Term},其中 Term是包含有关错误信息的术语,而主管则以Term终止。 | |||
如果任何子进程启动功能失败或返回错误的元组或错误值,则主管首先使用原因shutdown终止所有已启动的子进程, 然后终止自身并返回 {error,{shutdown,Reason}}。 | |||
Terminate_child(SupRef,Id)->结果 | |||
种类 | |||
SupRef = sup_ref() | |||
ID = pid()| child_id() | |||
结果=正常| {错误,错误} | |||
错误=找不到| simple_one_for_one | |||
告诉主管SupRef终止指定的孩子。 | |||
如果主管不是simple_one_for_one,则 ID必须是子规范标识符。该过程(如果有的话)将终止,并且除非它是一个临时子进程,否则该子进程规范将由主管保留。子进程以后可以由主管重新启动。子进程也可以通过调用restart_child / 2显式重启 。使用 delete_child / 2 删除子级规范。 | |||
如果子级是临时的,则该子级规范将在过程终止后立即删除。这意味着delete_child / 2没有意义,并且restart_child / 2不能用于这些子级。 | |||
如果主管是simple_one_for_one,则 ID 必须是子进程的pid()。如果指定的进程处于活动状态,但不是指定的主管的子进程,则该函数返回 {error,not_found}。如果指定了子规范标识符而不是pid(),则函数返回{error,simple_one_for_one}。 | |||
如果成功,函数将返回ok。如果没有带有指定ID的子规范,则该函数返回{error,not_found}。 | |||
有关SupRef的描述,请参见 start_child / 2。 | |||
which_children(SupRef)-> [{Id,Child,Type,Modules}] | |||
种类 | |||
SupRef = sup_ref() | |||
ID = child_id() | 未定义 | |||
子= 子() | 重新开始 | |||
类型= worker() | |||
模块= modules() | |||
返回一个新创建的列表,其中包含有关属于主管SupRef的所有子规范和子进程的信息。 | |||
请注意,在内存不足的情况下监视多个子项时调用此函数可能会导致内存不足异常。 | |||
有关SupRef的描述,请参见 start_child / 2。 | |||
为每个子规范/过程提供以下信息: | |||
Id-在子规范中定义,或 为simple_one_for_one主管未定义。 | |||
子 -相应子进程的pid,如果该进程将要重新启动,则原子重新启动;如果没有这样的进程,则未定义。 | |||
类型 -子规范中定义的类型。 | |||
模块 -子规范中定义的模块。 | |||
Module:init(Args)->结果 | |||
种类 | |||
Args = term() | |||
结果= {确定,{SupFlags,[ChildSpec]}} | 忽视 | |||
SupFlags = sup_flags() | |||
ChildSpec = child_spec() | |||
每当使用start_link / 2,3启动管理员时 ,新进程就会调用此函数以查找有关重新启动策略,最大重新启动强度和子级规范的信息。 | |||
Args是提供给start函数的Args参数。 | |||
SupFlags是主管标志,用于定义主管的重新启动策略和最大重新启动强度。[ChildSpec]是有效的子规范的列表,这些规范定义了主管必须启动和监视的子进程。请参阅前面的“ 监督原则”一节中的讨论 。 | |||
请注意,当重新启动策略为 simple_one_for_one时,子规范的列表必须是仅包含一个子规范的列表。(忽略子规范标识符。)然后,在初始化阶段不启动任何子进程,但是假定所有子进程都使用start_child / 2动态启动 。 | |||
该函数还可以返回ignore。 | |||
请注意,此功能也可以作为代码升级过程的一部分来调用。因此,该功能不应有任何副作用。有关管理程序代码升级的更多信息,请参阅《OTP设计原则》中的“ 更改管理程序”部分 。 | |||
@ -0,0 +1,95 @@ | |||
## 二进制语法 | |||
Bin = <<E1, E2, ... En>> | |||
<<E1, E2, ... En>> = Bin | |||
每个E 1..n 指定bitstring的Segment | |||
每个段具有以下一般语法: | |||
Value:Size/TypeSpecifierList | |||
可以省略Size或TypeSpecifier或两者。因此,允许以下变体: | |||
Ei = | |||
Value | | |||
Value:Size | | |||
Value/TypeSpecifierList | | |||
Value:Size/TypeSpecifierList | |||
### Value | |||
当在二进制构造中使用时,Value部分是任何表达式,用于求值为整数,浮点或位串,如果表达式不是单个文字或变量,则将其括在括号中。 | |||
在二进制匹配中使用时,用于位串匹配,Value必须是变量,或整数,浮点或字符串,简单而言就是Value部分必须是文字或变量。 | |||
### Size | |||
在位串构造中使用,Size是要求求整数的表达式。 | |||
用于位串匹配,Size必须是整数,或绑定到整数的变量。 | |||
Size的值以Unit指定段的大小,默认值取决于类型 | |||
• For integer it is 8. | |||
• For float it is 64. | |||
• For binary and bitstring it is the whole binary or bit string. | |||
在匹配中,此默认值仅对最后一个元素有效。匹配中的所有其他位串或二进制元素必须具有大小规范,段的大小Size部分乘以TypeSpecifierList中的unit(稍后描述)给出了段的位数.对于utf8,utf16和utf32类型,不得给出Size的大小。段的大小由类型和值本身隐式确定。 | |||
### TypeSpecifierList | |||
是一个类型说明符列表,按任何顺序,用连字符("-")分隔。默认值用于任何省略的类型说明符 | |||
#### Type | |||
Type= integer | float | binary | bytes | bitstring | bits | utf8 | utf16 | utf32 | |||
默认值为integer。bytes是二进制的简写,bits是bitstring的简写。有关utf类型的更多信息,请参见下文。 | |||
#### Signedness | |||
Signedness= signed | unsigned | |||
只有匹配和类型为整数时才有意义。默认值为无符号。 | |||
#### Endianness | |||
Endianness= big | little | native | |||
Native-endian意味着字节顺序在加载时被解析为big-endian或little-endian,具体取决于运行Erlang机器的CPU的本机内容。仅当Type为integer,utf16,utf32或float时,字节顺序才有意义。默认值为big。 | |||
#### Unit | |||
Unit= unit:IntegerLiteral | |||
允许的范围是1..256。对于integer,float和bitstring,默认值为1;对于binary,默认值为8。对于utf8,utf16和utf32类型,不能给出Unit说明符。 | |||
它与Size说明符相乘,以给出段的有效大小。单位大小指定没有大小的二进制段的对齐方式,二进制类型的段必须具有可被8整除的大小 | |||
### 注意 | |||
构造二进制文件时,如果整数段的大小N太小而不能包含给定的整数,则整数的最高有效位将被静默丢弃,并且只有N个最低有效位被放入二进制。 | |||
#### 例子: | |||
X:4/little-signed-integer-unit:8 | |||
该元素的总大小为4 * 8 = 32位,它包含一个小端序的有符号整数 | |||
### 关于 utf8 utf16 utf32 | |||
构造utf类型的段时,Value必须是0..16#D7FF或16#E000 .... 16#10FFFF范围内的整数。如果Value超出允许范围,则构造将失败并返回badarg异常。生成的二进制段的大小取决于类型或值,或两者: | |||
• For utf8, Value is encoded in 1-4 bytes. | |||
• For utf16, Value is encoded in 2 or 4 bytes. | |||
• For utf32, Value is always be encoded in 4 bytes. | |||
构造时,可以给出一个文字字符串,后跟一个UTF类型,例如:<<“abc”/ utf8 >>,这是<< $ a / utf8,$ b / utf8,$ c / utf8的语法糖>>。 | |||
成功匹配utf类型的段,得到0..16#D7FF或16#E000..16#10FFFF范围内的整数。 | |||
如果返回值超出这些范围,则匹配失败。 | |||
### 如何实现二进制文件 | |||
在内部,二进制和位串以相同的方式实现。 | |||
内部有四种类型的二进制对象: | |||
两个是二进制数据的容器,称为: | |||
• Refc binaries (short for reference-counted binaries) | |||
• Heap binaries | |||
两个仅仅是对二进制文件的一部分的引用,被称为: | |||
• sub binaries | |||
• match contexts | |||
### Refc Binaries | |||
Refc二进制文件由两部分组成: | |||
•存储在进程堆上的对象,称为ProcBin | |||
•二进制对象本身,存储在所有进程堆之外 | |||
任何数量的进程都可以通过任意数量的ProcBins引用二进制对象。该对象包含一个引用计数器,用于跟踪引用的数量,以便在最后一个引用消失时将其删除。 | |||
进程中的所有ProcBin对象都是链表的一部分,因此当ProcBin消失时,垃圾收集器可以跟踪它们并减少二进制文件中的引用计数器。 | |||
### Heap Binaries | |||
堆二进制文件是小型二进制文件,最多64个字节,并直接存储在进程堆上。它们在进程被垃圾收集时以及作为消息发送时被复制。它们不需要垃圾收集器进行任何特殊处理。 | |||
### Sub Binaries | |||
The reference objects sub binaries and match contexts can reference part of a refc binary or heap binary | |||
子二进制文件由split_binary / 2创建或者当二进制文件以二进制模式匹配时。子二进制是对另一个二进制文件(refc或堆二进制文件的一部分,但从不进入另一个子二进制文件)的引用。因此,匹配二进制文件相对便宜,因为实际的二进制数据永远不会被复制。 | |||
### Match Context | |||
匹配上下文类似于子二进制,但针对二进制匹配进行了优化 | |||
### 关于iolist | |||
定义(直接引用霸爷的文章) | |||
1. [] | |||
2. binary | |||
3. 列表, 每个元素是int(0-255)或者binary或者iolist. | |||
其中binary是指 bitsize % 8 == 0 . | |||
int 是0-255 | |||
Iolist的作用是用于往port送数据的时候.由于底层的系统调用如writev支持向量写, 就避免了无谓的iolist_to_binary这样的扁平话操作, 避免了内存拷贝,极大的提高了效率. | |||
另外额外补充: | |||
erlang中列表时在头部添加比较高效,但是binary是在尾部追加更高效 | |||
### 关于消息接收转发解码和发送 | |||
erlang通常会将接收到的消息由网关进程转发给其他工作进程, 建议先匹配消息id, 然后转发二进制消息到工作进程,然后由工作进程解码再处理 | |||
同时广播消息可先编码成二进制之后再广播, 避免重复编码 |
@ -0,0 +1,448 @@ | |||
## gen_tcp 编程接口 | |||
#### listen(Port, Options) -> {ok, ListenSocket} | {error, Reason} | |||
Types | |||
Port = inet:port_number() | |||
Options = [listen_option()] | |||
ListenSocket = socket() | |||
Reason = system_limit | inet:posix() | |||
设置一个套接字以侦听本地主机上的端口Port。 | |||
用法: | |||
listen(Port, Options) -> {ok, ListenSocket} | {error, Reason} | |||
在本地开启一个监听某个端口的套接字(socket)。开启成功的话,会返回一个套接字标识符 Socket,其一般会传递给 get_tcp:accept/1 或 get_tcp:accept/2 调用。 | |||
如果参数 Port 为 0,那么底层操作系统将赋值一个可用的端口号,可以使用 inet:port/1 来获取一个 socket 监听的端口。 | |||
连接到IP地址为Address的主机的TCP端口Port上的服务器。参数 地址可以是主机名或IP地址。 | |||
提供以下选项: | |||
{ip, Address} | |||
如果主机有许多网络接口,则此选项指定要使用的接口。 | |||
{ifaddr, Address} | |||
与{ip,Address}相同。如果主机有许多网络接口,则此选项指定要使用的接口。 | |||
{fd, integer() >= 0} | |||
如果以某种方式未使用gen_tcp连接了套接字 ,请使用此选项传递文件描述符。如果将{ip,Address}和/或 {port,port_number()}与该选项结合使用,则 在连接前将fd绑定到指定的接口和端口。如果未指定这些选项,则假定fd已被适当绑定。 | |||
inet | |||
为IPv4设置套接字。 | |||
inet6 | |||
设置用于IPv6的套接字。 | |||
local | |||
设置Unix域套接字。见 inet:local_address() | |||
{port,Port} | |||
指定要使用的本地端口号。 | |||
{tcp_module, module()} | |||
覆盖使用哪个回调模块。默认为 inet_tcp IPv4和inet6_tcp使用IPv6。 | |||
Opt | |||
参见 inet:setopts / 2。 | |||
可以使用send / 2将数据包发送到返回的套接字Socket。 从对等方发送的数据包将作为消息传递: | |||
{tcp, Socket, Data} | |||
如果套接字处于{active,N}模式(有关详细信息,请参见inet:setopts / 2),并且其消息计数器降至0,则将传递以下消息以指示套接字已转换为被动({active,false}) 模式: | |||
{tcp_passive, Socket} | |||
如果套接字已关闭,则会发出以下消息: | |||
{tcp_closed, Socket} | |||
如果套接字上发生错误,则会传递以下消息(除非在套接字的选项列表中指定了{active,false},在这种情况下,可通过调用recv / 2来检索数据包): | |||
{tcp_error, Socket, Reason} | |||
可选的Timeout参数指定超时(以毫秒为单位)。默认为infinity。 | |||
注意::: | |||
请记住,如果底层OS connect()的调用返回超时,调用gen_tcp:连接也将返回超时(即{错误,ETIMEDOUT} ),即使较大的超时指定。 | |||
指定要连接的选项的默认值会受到内核配置参数 inet_default_connect_options的影响。有关详细信息,请参见 inet(3)。 | |||
参数 Options 的一些常用选项: | |||
{active, true}:套接字设置为主动模式。所有套接字接收到的消息都作为 Erlang 消息转发到拥有这个套接字进程上。当开启一个套接字时,默认是主动模式。 | |||
{active, false}:设置套接字为被动模式。套接字收到的消息被缓存起来,进程必须通过调用函数 gen_tcp:recv/2 或 gen_tcp:recv/3 来读取这些消息。 | |||
{active, once}:将设置套接字为主动模式,但是一旦收到第一条消息,就将其设置为被动模式,并使用 gen_tcp:recv/2 或 gen_tcp:recv/3 函数来读取后续消息。 | |||
{keepalive, true}:当没有转移数据时,确保所连接的套接字发送保持活跃(keepalive)的消息。因为关闭套接字消息可能会丢失,如果没有接收到保持活跃消息的响应,那么该选项可确保这个套接字能被关闭。默认情况下,该标签是关闭的。 | |||
{nodelay, true}:数据包直接发送到套接字,不过它多么小。在默认情况下,此选项处于关闭状态,并且与之相反,数据被聚集而以更大的数据块进行发送。 | |||
{packet_size, Size}:设置数据包允许的最大长度。如果数据包比 Size 还大,那么将认为这个数据包无效。 | |||
{packet, 0}:表示 Erlang 系统会把 TCP 数据原封不动地直接传送给应用程序 | |||
{reuseaddr, true}:允许本地重复使用端口号 | |||
{delay_send, true}:数据不是立即发送,而是存到发送队列里,等 socket 可写的时候再发送 | |||
{backlog, 1024}:缓冲区的长度 | |||
{exit_on_close, false}:设置为 flase,那么 socket 被关闭之后还能将缓冲区中的数据发送出去 | |||
{send_timeout, 15000}:设置一个时间去等待操作系统发送数据,如果底层在这个时间段后还没发出数据,那么就会返回 {error,timeout} | |||
{Rand, _RandSeed} = random:uniform_s(9999, erlang:now()), | |||
Port = 40000 + Rand, | |||
gen_tcp:listen(Port, [binary, {packet, 0}, {active, false}]). | |||
#### accept(ListenSocket) -> {ok, Socket} | {error, Reason} accept(ListenSocket, Timeout) -> {ok, Socket} | {error, Reason} | |||
Types | |||
ListenSocket = socket() Returned by listen/2. | |||
Timeout = timeout() Socket = socket() Reason = closed | timeout | system_limit | inet:posix() | |||
在侦听套接字上接受传入的连接请求。 套接字必须是从listen / 2返回的套接字 。 超时以毫秒为单位指定超时值。默认为infinity。 | |||
返回值: | |||
{ok, Socket} if a connection is established | |||
{error, closed} if ListenSocket is closed | |||
{error, timeout} if no connection is established within the specified time | |||
{error, system_limit} if all available ports in the Erlang emulator are in use | |||
A POSIX error value if something else goes wrong, see inet(3) for possible error values | |||
用法: | |||
该函数会引起进程阻塞,直到有一个连接请求发送到监听的套接字。 | |||
如果连接已建立,则返回 {ok,Socket}; | |||
或如果 ListenSocket 已经关闭,则返回{error,closed}; | |||
或如果在指定的时间内连接没有建立,则返回{error,timeout}; | |||
或如果 Erlang 虚拟机里可用的端口都被使用了,则返回 {error, system_limit}; | |||
如果某些东西出错,也可能返回一个 POSIX 错误。一些有可能的错误请查看 inet 模块的相关说明。 | |||
使用 gen_tcp:send/2 向该函数返回的套接字 Socket 发送数据包。往端口发送的数据包会以下面格式的消息发送: | |||
{tcp, Socket, Data} | |||
如果在建立套接字 Socket 的时候选项列表中指定了 {active,false},这样就只能使用 gen_tcp:recv/2 或 gen_tcp:recv/3 来接收数据包了。 | |||
{Rand, _RandSeed} = random:uniform_s(9999, erlang:now()), | |||
Port = 40000 + Rand, | |||
case gen_tcp:listen(Port, [binary, {packet, 0}, {active, false}]) of | |||
{ok, ListenSocket} -> | |||
case gen_tcp:accept(ListenSocket) of | |||
{ok, Socket} -> | |||
Socket; | |||
{error, SocketAcceptFail} -> | |||
SocketAcceptFail | |||
end; | |||
_ -> | |||
socket_listen_fail | |||
end. | |||
#### connect(Address, Port, Options) -> {ok, Socket} | {error, Reason} | |||
#### connect(Address, Port, Options, Timeout) -> {ok, Socket} | {error, Reason} | |||
Types | |||
Address = inet:socket_address() | | |||
inet:hostname() Port = inet:port_number() | |||
Options = [connect_option()] | |||
Timeout = timeout() | |||
Socket = socket() | |||
Reason = timeout | inet:posix() | |||
连接一个 TCP 端口 | |||
用法: | |||
connect(Address, Port, Options, Timeout) -> {ok, Socket} | {error, Reason} | |||
用给出的端口 Port 和 IP 地址 Address 连接到一个服务器上的 TCP 端口上。参数 Address 即可以是一个主机名,也可以是一个 IP 地址。 | |||
提供以下选项: | |||
{ip, Address} | |||
如果主机有许多网络接口,则此选项指定要使用的接口。 | |||
{ifaddr, Address} | |||
与{ip,Address}相同。如果主机有许多网络接口,则此选项指定要使用的接口。 | |||
{fd, integer() >= 0} | |||
如果以某种方式未使用gen_tcp连接了套接字 ,请使用此选项传递文件描述符。如果将{ip,Address}和/或 {port,port_number()}与该选项结合使用,则 在连接前将fd绑定到指定的接口和端口。如果未指定这些选项,则假定fd已被适当绑定。 | |||
inet | |||
为IPv4设置套接字。 | |||
inet6 | |||
设置用于IPv6的套接字。 | |||
local | |||
设置Unix域套接字。见 inet:local_address() | |||
{port,Port} | |||
指定要使用的本地端口号。 | |||
{tcp_module, module()} | |||
覆盖使用哪个回调模块。默认为 inet_tcp IPv4和inet6_tcp使用IPv6。 | |||
Opt | |||
参见 inet:setopts / 2。 | |||
可以使用send / 2将数据包发送到返回的套接字Socket。 从对等方发送的数据包将作为消息传递: | |||
{tcp, Socket, Data} | |||
如果套接字处于{active,N}模式(有关详细信息,请参见inet:setopts / 2),并且其消息计数器降至0,则将传递以下消息以指示套接字已转换为被动({active,false}) 模式: | |||
{tcp_passive, Socket} | |||
如果套接字已关闭,则会发出以下消息: | |||
{tcp_closed, Socket} | |||
如果套接字上发生错误,则会传递以下消息(除非在套接字的选项列表中指定了{active,false},在这种情况下,可通过调用recv / 2来检索数据包): | |||
{tcp_error, Socket, Reason} | |||
可选的Timeout参数指定超时(以毫秒为单位)。默认为infinity。 | |||
注意::: | |||
请记住,如果底层OS connect()的调用返回超时,调用gen_tcp:连接也将返回超时(即{错误,ETIMEDOUT} ),即使较大的超时指定。 | |||
指定要连接的选项的默认值会受到内核配置参数 inet_default_connect_options的影响。有关详细信息,请参见 inet(3)。 | |||
#### gen_tcp:close/1 | |||
Types | |||
Socket = socket() | |||
关闭一个 TCP 套接字 | |||
请注意,在大多数TCP实现中,执行关闭操作并不能保证在远程端检测到关闭之前,已发送的任何数据都会传递给接收方。如果要保证将数据传递给收件人,可以通过两种常用方法来实现。 | |||
使用gen_tcp:shutdown(Sock,write)发出信号,表明不再发送任何数据,并等待套接字的读取端关闭。 | |||
使用套接字选项{packet,N}(或类似的选项)可以使接收器在知道已接收到所有数据时关闭连接。 | |||
#### recv(Socket, Length) -> {ok, Packet} | {error, Reason} | |||
#### recv(Socket, Length, Timeout) -> {ok, Packet} | {error, Reason} | |||
Types | |||
Socket = socket() | |||
Length = integer() >= 0 | |||
Timeout = timeout() | |||
Packet = string() | binary() | HttpPacket | |||
Reason = closed | timeout | inet:posix() | |||
HttpPacket = term() 看到的描述 HttpPacket中 的erlang:decode_packet / 3 在ERTS。 | |||
在被动模式下从套接字接收数据包。 | |||
返回值{error,closed}指示关闭的套接字。 | |||
Argument Length is only meaningful when the socket is in raw mode and denotes the number of bytes to read. | |||
参数 Length 仅在套接字处于 raw mode 时才有意义,它表示要读取的字节数。 | |||
Length为0,则返回所有可用字节。 | |||
如果Length > 0,则返回确切的 Length字节,否则返回错误; | |||
从另一侧关闭套接字时,可能会丢弃少于长度字节的数据 | |||
可选的Timeout参数指定超时(以毫秒为单位)。默认为infinity。 | |||
用法: | |||
recv(Socket, Length, Timeout) -> {ok, Packet} | {error, Reason} | |||
这个函数是从一个被动模式的套接字接受一个数据包。如果返回一个 {error, closed} 的返回值,那表明 Socket 已经关闭。 | |||
当 Socket 是 raw 模式下,参数 Length 才有意义的,并且 Length 表示接收字节的大小。如果 Length = 0,所有有效的字节数据都会被接收。如果 Length > 0,则只会接收 Length 长度的字节,或发生错误;当另一端 Socket 关闭时,接收的数据长度可能会小于 Length。 | |||
选项 Timeout 是一个以毫秒为单位的超时值,默认值是 infinity。 | |||
{Rand, _RandSeed} = random:uniform_s(9999, erlang:now()), | |||
Port = 40000 + Rand, | |||
case gen_tcp:listen(Port, [binary, {packet, 0}, {active, false}]) of | |||
{ok, ListenSocket} -> | |||
case gen_tcp:accept(ListenSocket) of | |||
{ok, Socket} -> | |||
gen_tcp:recv(Socket, 0, 5000); | |||
{error, SocketAcceptFail} -> | |||
SocketAcceptFail | |||
end; | |||
_ -> | |||
socket_listen_fail | |||
end. | |||
#### send(Socket, Packet) -> ok | {error, Reason} | |||
Types | |||
Socket = socket() | |||
Packet = iodata() | |||
Reason = closed | inet:posix() | |||
在一个套接字 Socket 发送一个数据包 | |||
用法: | |||
send(Socket, Packet) -> ok | {error, Reason} | |||
在一个套接字 Socket 发送一个数据包。 | |||
#### shutdown(Socket, How) -> ok | {error, Reason} | |||
Types | |||
Socket = socket() | |||
How = read | write | read_write | |||
Reason = inet:posix() | |||
在一个或两个方向上关闭socket | |||
以某种方式半关闭一个套接字。 | |||
如果参数 How 为 write 的形式,则套接字 socket 会关闭数据写入,读取仍可以正常执行。 | |||
如果How == read或Socket端口没有缓冲传出数据,则套接字将立即关闭,并且Reason中将返回遇到的任何错误。 | |||
要实现套接字半打开, 那么套接字要设置 {exit_on_close, false} 这个参数。 | |||
如果套接字端口中缓冲了数据,则将尝试关闭套接字的操作推迟到该数据写入内核套接字发送缓冲区中为止。 | |||
如果遇到任何错误,则关闭套接字,并在下一个recv / 2或 send / 2上返回 {error,closed}。 | |||
如果对等方在写端执行了关闭操作,则选项{exit_on_close,false}很有用。 | |||
#### gen_tcp:controlling_process/2 | |||
改变一个套接字的控制进程 | |||
将新的控制过程Pid分配给 Socket。控制过程是从套接字接收消息的过程。 | |||
如果由当前控制进程以外的任何其他进程调用, 则返回{error,not_owner}。 | |||
如果由Pid标识的进程不是现有的本地pid, 则返回{error,badarg}。 | |||
在某些情况下,在执行此函数期间关闭Socket时,也可能返回{error,badarg}。 | |||
如果套接字设置为活动模式,则此功能会将呼叫者邮箱中的所有消息传送到新的控制进程。 | |||
如果在传输过程中有任何其他进程正在与套接字交互,则传输可能无法正常进行,并且消息可能会保留在呼叫者的邮箱中。 | |||
例如,在传输完成之前更改套接字活动模式可能会导致此情况 | |||
#### 套接字选项 | |||
{active, true | false | once | -32768..32767} | | |||
如果值为true,这是默认值,则将从套接字接收的所有内容作为消息发送到接收进程。 | |||
如果值为false(被动模式),则该进程必须通过调用gen_tcp:recv / 2,3, gen_udp:recv / 2,3或gen_sctp:recv / 1,2来显式接收传入的数据 (取决于套接字的类型) )。 | |||
如果该值为一次({active,once}), 则套接字中的一条数据消息将发送到该进程。要接收更多消息, 必须使用选项{active,一次}再次调用 setopts / 2。 | |||
如果该值是-32768到32767(含)之间的整数N,则将该值添加到发送到控制进程的套接字的数据消息计数中。 | |||
套接字的默认消息计数为0。如果指定了负值,并且其大小等于或大于套接字的当前消息计数,则套接字的消息计数将设置为0。 | |||
一旦套接字的消息计数达到0,则可能是由于 向进程发送接收到的数据消息或通过显式设置该消息, | |||
然后通过特定于套接字类型的特殊消息通知该进程套接字已进入被动模式。 一旦套接字进入被动模式,为了接收更多消息, | |||
必须再次调用setopts / 2才能将套接字设置回主动模式。 | |||
如果该值是-32768到32767(含)之间的整数N,则将该值添加到发送到控制进程的套接字的数据消息计数中。套接字的默认消息计数为0。 | |||
如果指定了负值,并且其大小等于或大于套接字的当前消息计数,则套接字的消息计数将设置为0。一旦套接字的消息计数达到0, | |||
要么是由于向进程发送接收到的数据消息,要么是因为已显式设置它,然后通过特定于套接字类型的特殊消息通知该进程该套接字已进入被动模式。 | |||
一旦套接字进入被动模式,为了接收更多消息,必须再次调用setopts / 2才能将套接字设置回主动模式。 | |||
使用{active,一次}或{active,N}时,套接字在接收到数据时会自动更改行为。与面向连接的套接字(即gen_tcp)结合使用时, | |||
可能会造成混淆,因为具有{active,false}行为的套接字报告的关闭方式与具有{active,true} 行为的套接字关闭的方式不同。为了简化编程, | |||
当套接字在{active,false}模式下被关闭且对等方关闭时, 在设置为{active,一旦}时仍会生成消息 {tcp_closed,Socket }, | |||
{active,true}或{active,N}模式。因此可以肯定地假设,当套接字在{active,true}和 {active,false}模式之间来回切换时, | |||
消息 {tcp_closed,Socket}可能最终会出现套接字端口终止(取决于选项exit_on_close)。 | |||
但是, 当检测到对等关闭时,完全取决于基础的TCP / IP堆栈和协议。 | |||
注意{active,true}模式不提供流量控制;快速的发送者可以轻松地使接收者的传入消息溢出。对于 {active,N}模式,消息数大于零时也是如此。 | |||
仅当高级协议提供自己的流控制(例如,确认收到的消息)或交换的数据量很少时,才使用活动模式。{active,false} 模式, | |||
使用{active,一旦}模式或{active,N} 模式(具有适用于应用程序的N值)提供流量控制。另一端发送的速度不能超过接收器可以读取的速度。 | |||
{broadcast, Boolean} (UDP sockets) | |||
启用/禁用发送广播的权限。 | |||
{buffer, integer() >= 0} | | |||
驱动程序使用的用户级缓冲区的大小。不要与sndbuf 和recbuf选项混淆,它们与内核套接字缓冲区相对应。对于TCP,建议使用val(buffer)> = val(recbuf), | |||
以避免由于不必要的复制而导致的性能问题。对于UDP,适用相同的建议,但最大值不应大于网络路径的MTU。 | |||
设置recbuf时,val(buffer)会自动设置为上述最大值。但是,为Recbuf设置的大小 通常变大,建议您使用 getopts / 2 来分析操作系统的行为。 | |||
请注意,这也是从单个recv调用可以接收的最大数据量。如果您使用的MTU高于正常值,请考虑将缓冲区设置为更高。 | |||
{delay_send, boolean()} | | |||
通常,当Erlang进程发送到套接字时,驱动程序会尝试立即发送数据。如果失败,驱动程序将使用任何可用方法将要发送的消息排队, | |||
只要操作系统表示可以处理该消息。设置{delay_send,true} 会使所有消息排队。这样,发送到网络的消息就更大, | |||
但更少。该选项将影响发送请求与Erlang进程的调度,而不是更改套接字的任何实际属性。该选项是特定于实现的。默认为false。 | |||
{deliver, port | term} | | |||
当{active,true}时,数据在以下端口上传递 {S, {data, [H1,..Hsz | Data]}} or term : {tcp, S, [H1..Hsz | Data]}. | |||
{dontroute, boolean()} | | |||
启用/禁用传出消息的路由旁路 | |||
{exit_on_close, boolean()} | | |||
默认情况下,此选项设置为true。 | |||
将其设置为false的唯一原因是,如果要在检测到关闭后继续向套接字发送数据,例如,如果对等方使用 gen_tcp:shutdown / 2 关闭写端。 | |||
{header, integer() >= 0} | | |||
仅当创建套接字时指定了选项binary 时,此选项才有意义。如果指定了选项 header, | |||
则从套接字接收的数据的第一个 Size Number字节是列表的元素,其余数据是指定为同一列表尾部的二进制文件。例如,如果Size == 2,则接收到的数据与[Byte1,Byte2 | Binary]匹配 | |||
{high_msgq_watermark, integer() >= 1} | | |||
当消息队列上的数据量达到此限制时,套接字消息队列将设置为繁忙状态。请注意,此限制仅涉及尚未达到ERTS内部套接字实现的数据。默认为8 kB。 | |||
如果套接字消息队列繁忙或套接字本身繁忙,则挂起套接字的数据发送器。 | |||
有关更多信息,请参见选项low_msgq_watermark, high_watermark和low_watermark。 | |||
Notice that distribution sockets disable the use of high_msgq_watermark and low_msgq_watermark. Instead use the distribution buffer busy limit, which is a similar feature. | |||
{high_watermark, integer() >= 0} | | |||
当ERTS套接字实现在内部排队的数据量达到此限制时,将套接字设置为繁忙状态。默认为8 kB。 | |||
如果套接字消息队列繁忙或套接字本身繁忙,则挂起套接字的数据发送器。 | |||
有关更多信息,请参见选项low_watermark, high_msgq_watermark和low_msqg_watermark。 | |||
{ipv6_v6only, Boolean} | |||
限制套接字仅使用IPv6,禁止任何IPv4连接。这仅适用于IPv6套接字(选项inet6)。 | |||
在大多数平台上,必须先在套接字上设置此选项,然后才能将其与地址关联。因此,仅在创建套接字时指定它,而在调用包含此描述的函数(setopts / 2)时不使用它是合理的。 | |||
将此选项设置为true的套接字的行为 是唯一可移植的行为。现在,FreeBSD不建议使用IPv6的初衷是将IPv6用于所有流量(您可以使用 {ipv6_v6only,false}来覆盖建议的系统默认值),但OpenBSD(受支持的GENERIC内核)禁止使用,并且在Windows(具有单独的IPv4和IPv6协议栈)。大多数Linux发行版的系统默认值仍为false。逐渐改变了操作系统之间从IPv4流量中分离IPv6流量的策略,因为逐渐证明,要确保正确,安全地实现双堆栈实施是困难而复杂的。 | |||
在某些平台上,此选项唯一允许的值为true,例如OpenBSD和Windows。在这种情况下,尝试在创建套接字时将此选项设置为false会失败。 | |||
在不存在的平台上设置此选项将被忽略。使用getopts / 2获取此选项 不会返回任何值,即返回的列表不包含 {ipv6_v6only,_}元组。在Windows上,该选项不存在,但会将其模拟为值为true的只读选项。 | |||
因此, 在创建套接字时将此选项设置为true永远不会失败,除非可能是在您已将内核自定义为仅允许false的平台上进行,但在OpenBSD上这是可行的(但尴尬)。 | |||
如果使用getopts / 2读回选项值 而没有获取任何值,则该选项在主机操作系统中不存在。IPv6和IPv4套接字在同一端口上侦听的行为以及获取IPv4流量的IPv6套接字的行为不再可预测。 | |||
{keepalive, boolean()} | | |||
没有其他数据交换时,启用/禁用连接的套接字上的定期传输。如果另一端没有响应,则认为连接已断开,并且将错误消息发送到控制过程。默认为禁用。 | |||
{linger, {boolean(), integer() >= 0}} | | |||
确定在close / 1套接字调用中刷新未发送数据的超时(以秒为单位)。 | |||
第一个组件是如果启用了延迟,第二个组件是刷新超时(以秒为单位)。有3种选择: | |||
{false,_} | |||
close / 1或shutdown / 2会立即返回,而不是等待刷新数据,而在后台进行关闭。 | |||
{true,0} | |||
关闭连接时中止连接。丢弃仍保留在发送缓冲区中的所有数据,并将RST发送给对等方。 | |||
这避免了TCP的TIME_WAIT状态,但是使创建该连接的另一个“化身”成为可能。 | |||
当时间> 0时,{true,时间} | |||
在成功发送了套接字的所有排队消息或达到了超时(时间)之前,close / 1或shutdown / 2不会返回。 | |||
{low_msgq_watermark, integer() >= 1} | | |||
如果套接字消息队列处于繁忙状态,则当消息队列中排队的数据量低于此限制时,套接字消息队列将设置为不繁忙状态。请注意,此限制仅涉及尚未达到ERTS内部套接字实现的数据。默认为4 kB。 | |||
当套接字消息队列和套接字不繁忙时,将恢复由于繁忙的消息队列或繁忙的套接字而挂起的发件人。 | |||
有关更多信息,请参见选项high_msgq_watermark, high_watermark和low_watermark。 | |||
请注意,分发套接字禁止使用 high_msgq_watermark和low_msgq_watermark。而是使用 分配缓冲区繁忙限制,这是一个类似功能。 | |||
{low_watermark, integer() >= 0} | | |||
如果套接字处于繁忙状态,则当ERTS套接字实现在内部排队的数据量低于此限制时,会将套接字设置为不繁忙状态。默认为4 kB。 | |||
当套接字消息队列和套接字不繁忙时,将恢复由于繁忙的消息队列或繁忙的套接字而挂起的发件人。 | |||
有关更多信息,请参见选项high_watermark, high_msgq_watermark和low_msgq_watermark | |||
{mode, list | binary} | | |||
接收到的数据包按照list或者binary的定义进行传递。 | |||
list | | |||
接收到的数据包以列表形式发送。 | |||
binary | | |||
接收到的数据包以二进制形式传送 | |||
{bind_to_device,Ifname :: binary()} | |||
将套接字绑定到特定的网络接口。必须在创建套接字的函数调用中使用此选项,即 gen_tcp:connect / 3,4, gen_tcp:listen / 2, gen_udp:open / 1,2或 gen_sctp:open / 0,1,2。 | |||
与getifaddrs / 0不同,Ifname编码为二进制。如果系统在网络设备名称中使用非7位ASCII字符(这种情况不太可能发生),则在对该参数进行编码时必须格外小心。 | |||
此选项使用特定于Linux的套接字选项 SO_BINDTODEVICE,例如在Linux内核2.0.30或更高版本中,因此仅在针对此类操作系统编译运行时系统时才存在。 | |||
在Linux 3.8之前,可以设置此套接字选项,但无法使用getopts / 2进行检索。从Linux 3.8开始,它是可读的。 | |||
虚拟机还需要提升的特权,这些特权可以以超级用户身份运行,或者(对于Linux)具有CAP_NET_RAW能力 。 | |||
此选项的主要用例是将套接字绑定到 Linux VRF实例。 | |||
{nodelay, boolean()} | | |||
{nodelay,布尔值}(TCP / IP套接字) | |||
如果Boolean == true, 则为套接字打开选项TCP_NODELAY,这意味着也会立即发送少量数据。 | |||
{nopush,布尔型}(TCP / IP套接字) | |||
这相当于TCP_NOPUSH在BSD和TCP_CORK在Linux上。 | |||
如果Boolean == true,则为套接字打开相应的选项,这意味着将累积少量数据,直到可用完整的MSS数据为止或关闭此选项。 | |||
请注意,虽然OSX上提供了TCP_NOPUSH套接字选项,但其语义却大不相同(例如,取消设置它不会导致立即发送累积的数据)。因此,在OSX上有意忽略了nopush选项 | |||
{packet, 0 | 1 | 2 | 4 | raw | sunrm | asn1 | cdr | fcgi | line | tpkt | http | httph | http_bin | httph_bin} | | |||
raw | 0 没有包装。 | |||
1 | 2 | 4 数据包包含一个标头,该标头指定了数据包中的字节数,然后是该字节数。标头长度可以是一个, | |||
两个或四个字节,并包含一个按big-endian字节顺序排列的无符号整数。每个发送操作都会生成标头,并且在每个接收操作上都会剥离标头。4字节的标头限制为2Gb。 | |||
asn1 | cdr | sunrm | fcgi | tpkt | line | |||
这些数据包类型仅对接收有效。发送数据包时,应用程序有责任提供正确的标头。但是,在接收时,对于接收到的每个完整数据包,将一条消息发送到控制过程,并且类似地,对gen_tcp:recv / 2,3的每次调用都 返回一个完整数据包。标头未剥离。 | |||
数据包类型的含义如下: | |||
asn1 -ASN.1 BER | |||
sunrm -Sun的RPC编码 | |||
CDR -CORBA(GIOP 1.1) | |||
fcgi-快速CGI | |||
tpkt -TPKT格式[RFC1006] | |||
line-行模式,数据包以换行符结尾的行,比接收缓冲区长的行被截断 | |||
http | http_bin | |||
超文本传输协议。按照ERTS的erlang:decode_packet / 3中 描述的 HttpPacket格式返回数据包。被动模式下的套接字从gen_tcp:recv返回{ok,HttpPacket}, 而主动套接字发送诸如 {http,Socket,HttpPacket}之类的消息。 | |||
httph | httph_bin | |||
通常不需要这两种类型,因为在读取第一行之后,套接字会在内部自动从http / http_bin切换到 httph / httph_bin。但是,有时可能有用,例如从分块编码中解析预告片 | |||
{packet_size, integer() >= 0} | | |||
设置数据包主体的最大允许长度。如果数据包头指示数据包的长度大于最大允许长度,则该数据包被视为无效。如果数据包头对于套接字接收缓冲区太大,则会发生相同的情况。 | |||
对于面向行的协议(line,http *),选项packet_size还可以保证接受指定长度的行,并且由于内部缓冲区的限制,该行不会被视为无效。 | |||
{line_delimiter, Char}(TCP/IP sockets) | |||
设置面向行的协议(line)的行定界字符。默认为$ \ n。 | |||
{priority, integer() >= 0} | | |||
在实现此功能的平台上设置SO_PRIORITY套接字级别选项。行为和允许范围在不同系统之间有所不同。该选项在未实现的平台上被忽略。请谨慎使用。 | |||
{raw,Protocol :: integer() >= 0,OptionNum :: integer() >= 0, ValueBin :: binary()} | | |||
{read_packets,Integer}(UDP套接字) | |||
设置在数据可用时无需套接字就可以读取的最大UDP数据包数。当读取了这么多的数据包并将其传送到目标进程后,新的数据包才被读取,直到有可用数据的新通知到达为止。默认为5。如果此参数设置得太高,由于UDP数据包泛洪,系统可能会变得无响应。 | |||
{recbuf, integer() >= 0} | | |||
用于套接字的接收缓冲区的最小大小。建议您使用 getopts / 2 来检索操作系统设置的大小。 | |||
{reuseaddr, boolean()} | | |||
允许或禁止端口号的本地重用。默认情况下,不允许重用。 | |||
{send_timeout, integer() >= 0 | infinity} | | |||
仅允许用于面向连接的套接字。 | |||
指定最长时间等待基础TCP堆栈接受发送操作。当超过限制时,发送操作将返回 {error,timeout}。未知发送了多少数据包;因此,只要发生超时,套接字就将关闭(请参见 下面的send_timeout_close)。默认为infinity。 | |||
{send_timeout_close, boolean()} | | |||
仅允许用于面向连接的套接字。 | |||
与send_timeout一起使用,以指定当send操作返回{error,timeout}时是否自动关闭套接字。推荐的设置为 true,它将自动关闭套接字。由于向后兼容,默认为false。 | |||
{show_econnreset, boolean()} | | |||
当此选项设置为false时(默认情况下),将从TCP对等方接收到的RST视为正常关闭(就像已发送FIN一样)。gen_tcp:recv / 2的调用者 获得{错误,关闭}。在活动模式下,控制进程收到 {tcp_closed,Socket}消息,指示对等方已关闭连接。 | |||
将此选项设置为true可让您区分正常关闭的连接和TCP对等方中止(有意或无意)的连接。调用 gen_tcp:recv / 2 返回{error,econnreset}。在活动模式下,控制过程会在通常的{tcp_closed,Socket}之前接收到 {tcp_error,Socket,econnreset}消息,就像其他套接字错误一样。调用 gen_tcp:send / 2 也会返回{error,econnreset} 当检测到TCP对等体已发送RST时。 | |||
从gen_tcp:accept / 1返回的已连接套接字 从侦听套接字 继承了show_econnreset设置。 | |||
{sndbuf, integer() >= 0} | | |||
用于套接字的发送缓冲区的最小大小。鼓励您使用 getopts / 2来检索操作系统设置的大小。 | |||
{tos, integer() >= 0} | | |||
在实现此功能的平台上设置IP_TOS IP级别选项。行为和允许范围在不同系统之间有所不同。该选项在未实现的平台上被忽略。请谨慎使用。 | |||
{tclass, integer() >= 0} | | |||
在实现此功能的平台上 设置IPV6_TCLASS IP级别选项。行为和允许范围在不同系统之间有所不同。该选项在未实现的平台上被忽略。请谨慎使用。 | |||
{ttl, integer() >= 0} | | |||
{recvtos, boolean()} | | |||
{recvtclass, boolean()} | | |||
{recvttl, boolean()} | | |||
option_name() = | |||
active | buffer | delay_send | deliver | dontroute | | |||
exit_on_close | header | high_msgq_watermark | | |||
high_watermark | keepalive | linger | low_msgq_watermark | | |||
low_watermark | mode | nodelay | packet | packet_size | | |||
pktoptions | priority | | |||
{raw,Protocol :: integer() >= 0, OptionNum :: integer() >= 0, ValueSpec ::(ValueSize :: integer() >= 0) | (ValueBin :: binary())} | | |||
recbuf | reuseaddr | send_timeout | send_timeout_close | | |||
show_econnreset | sndbuf | tos | tclass | ttl | recvtos | | |||
recvtclass | recvttl | pktoptions | ipv6_v6only | |||
connect_option() = | |||
{ip, inet:socket_address()} | | |||
{fd, Fd :: integer() >= 0} | | |||
{ifaddr, inet:socket_address()} | | |||
inet:address_family() | | |||
{port, inet:port_number()} | | |||
{tcp_module, module()} | | |||
{netns, file:filename_all()} | | |||
{bind_to_device, binary()} | | |||
option() | |||
listen_option() = | |||
{ip, inet:socket_address()} | | |||
{fd, Fd :: integer() >= 0} | | |||
{ifaddr, inet:socket_address()} | | |||
inet:address_family() | | |||
{port, inet:port_number()} | | |||
{backlog, B :: integer() >= 0} | | |||
{tcp_module, module()} | | |||
{netns, file:filename_all()} | | |||
{bind_to_device, binary()} | | |||
option() | |||
socket() | |||
As returned by accept/1,2 and connect/3,4. |
@ -0,0 +1,215 @@ | |||
#### erlang 各种 优化设置 | |||
一、 erl启动时参数: | |||
+K true 开启epoll调度,在linux中开启epoll,会大大增加调度的效率 | |||
+A 100 异步线程池,为某些port调用服 | |||
+P 1024000 最大进程数 | |||
+Q 65535 最大port数 | |||
+sbt db 绑定调度器,绑定后调度器的任务队列不会在各个CPU线程之间跃迁,结合sub使用,可以让CPU负载均衡的同时也避免了大量的跃迁发生。 | |||
将scheduler绑定到具体的cpu核心上,再配合erlang进程和port绑定,可以显著提升性能,但是如果绑定错误,反而会有反效果 | |||
( 进程调度器绑定:erlang:process_flag(scheduler, 1),当进程使用了port时,还需要port绑定支持,防止进程在不同调度器间迁移引起性能损失,如cache、跨numa node拷贝等,当进程使用了port时,主要是套接字,若进程与port不在一个scheduler上,可能会引发严重的epoll fd锁竞争及跨numa node拷贝,导致性能严重下降) | |||
注意:一个linux系统中,最好只有一个evm开启此选项,若同时有多个erlang虚拟机在系统中运行,还是关闭为好 | |||
+sub true 开启CPU负载均衡,false的时候是采用的CPU密集调度策略,优先在某个CPU线程上运行任务,直到该CPU负载较高为止。 | |||
+swct eager 此选项设置为eager后,CPU将更频繁的被唤醒,可以增加CPU利用率 | |||
+spp true 开启并行port并行调度队列,当开启后会大大增加系统吞吐量,如果关闭,则会牺牲吞吐量换取更低的延迟。 | |||
+zdbbl 65536 分布式erlang的端口buffer大小,当buffer满的时候,向分布式的远程端口发送消息会阻塞 | |||
二、erlang内部进程启动参数 | |||
示例:创建一个新进程并进行注册,该进程是全局唯一的自增ID生成进程,因此无法做多进程处理,这个时候单进程的性能就是至关重要的 | |||
首先,出于性能和功能考虑,这个进程不是gen_server;其次进行了部分参数调优能 | |||
register(num_generator, spawn_opt(?MODULE, init, [],[{priority,high},{scheduler,0},{min_heap_size, 65536 * 2},{min_bin_vheap_size,65536 * 2}])). | |||
参数讲解: | |||
1.priority | |||
erlang是公平调度策略,因此默认情况下每个进程得到的运行时间片是相同的:2000reductions,但是对于我们的应用场景来说,这个进程应该是优先级较高的,需要得到更多的调度,因此设置为high,还可以设置为max,但是max是系统进程的预留优先级,用high即可 | |||
2. scheduler | |||
将该进程绑定到指定的scheduler上,防止进程的任务被scheduler分配来分配去,可以减少CPU调用,注意这个和+sbt db是不同的,+sbt db是防治调度器的任务队列在CPU线程间跃迁,scheduler是为了防止进程在时间片切换过程中被分配给其它的调度器 | |||
3.min_heap_size | |||
进程初始堆大小,用内存换CPU的典型做法,增大初始大小,可以显著降低GC次数和内存再分配次数, 减少处理过程中产生大量term,尤其是list时的gc次数 | |||
4.min_bin_vheap_size | |||
进程初始二进制堆大小,当该进程对于binary数据的处理交换很多时,可以获得和增大min_heap_size一样的效果, 减少大量消息到达或处理过程中产生大量binary时的gc次数 | |||
三、port(socket)调优 | |||
示例:服务器监听端口,接受客户端请求。典型应用场景web服务器,需要实现高吞吐,低延迟的目标 | |||
Res = gen_tcp:listen(Port, [binary, | |||
{reuseaddr, true}, | |||
{nodelay, true}, | |||
{delay_send,true}, | |||
{high_watermark,64 * 1024}, | |||
{send_timeout, 30000}, | |||
{send_timeout_close, true}, | |||
{keepalive, true}]) | |||
参数详解: | |||
binary: | |||
接收到客户端的消息后,作为binary来处理,binary在erlang中是很高效的数据结构,超过64字节,就是全局保存的,因此在很多操作下是不需要复制的,仅仅复制binary的指针即可,详细请搜索refc binary,注意:binary大量使用需要有丰富的经验,不然可能会内存泄漏 | |||
reuseaddr: | |||
允许系统复用port,对于高吞吐的系统,这个参数很重要,请搜索:linux port 复用 | |||
nodelay: | |||
开启linux中的TCP_NODELAY参数,请搜索:TCP_NODELAY 40毫秒延迟 | |||
delay_send: | |||
默认的erlang port消息发送,是直接发送,若失败则排队处理,然后由调度器进行队列poll操作,如果设置为true,那么就不尝试直接发送,而且扔进队列,等待poll,开启选项会增加一点点消息延迟,换来吞吐量的大量提升 | |||
high_watermark: | |||
port的发送缓存,缓存满了后,下次发送会直接阻塞,直到缓存低于某个阈值low_watermark。如果是密集网络IO系统,请增大该buffer,避免发送阻塞 | |||
send_timeout: | |||
在high_watermark中提到了发送阻塞,如果阻塞超过这个时间,那么就会超时,发送直接返回,停止发送 | |||
send_timeout_close: | |||
如果发生了send_timeout同时设置了send_timeout_close选项,那么超时后,会直接关闭socket.如果发送进程不是很重要,例如web用户进程,强烈建议开启这个选项,当发送30秒超时的时候,就说明该用户出现了很大的麻烦,断开连接是最理想的做法,否则可能出现很多奇怪的bug. | |||
keepalive: | |||
遵循HTTP/1.1协议的keepalive规定,这个根据业务需求选择是否开启,如果同一个客户端会连续发起http请求,那么建议设置为true,避免多次TCP握手 | |||
示例:服务器发起大量的http请求,在优化了参数后,同样的吞吐量所耗费的时间是未优化前的1/3 - 1/2(经过严苛的测试得出的数据) | |||
inets:start(), | |||
httpc:set_options([{max_keep_alive_length,500},{max_sessions,100},{nodelay,true},{reuseaddr,true}]), | |||
参数详解: | |||
max_keep_alive_length: | |||
在同一条http连接上允许发送的最大包数,默认为5,超过5个包,就会重连 | |||
max_sessions: | |||
跟目标服务器之间最大的并行http连接数目,大大的增加了数据上行吞吐量 | |||
nodelay_true: | |||
见上文 | |||
reuseaddr: | |||
6. 数据结构: | |||
减少遍历,尽量使用API提供的操作 | |||
由于各种类型的变量实际可以当做c的指针,因此erlang语言级的操作并不会有太大代价 | |||
lists:reverse为c代码实现,性能较高,依赖于该接口实现的lists API性能都不差,避免list遍历,[||]和foreach性能是foldl的2倍,不在非必要的时候遍历list | |||
dict:find为微秒级操作,内部通过动态hash实现,数据结构先有若干槽位,后根据数据规模变大而逐步增加槽位,fold遍历性能低下 | |||
gb_trees:lookup为微秒级操作,内部通过一个大的元组实现,iterator+next遍历性能低下,比list的foldl还要低2个数量级 | |||
9. 文件预读,批量写,缓存: | |||
这些方式都是局部性的体现: | |||
预读:读空间局部性,文件提供了read_ahead选项 | |||
批量写:写空间局部性 | |||
对于文件写或套接字发送,存在若干级别的批量写: | |||
1. erlang进程级:进程内部通过list缓存数据 | |||
2. erlang虚拟机:不管是efile还是inet的driver,都提供了批量写的选项delayed_write|delay_send, | |||
它们对大量的异步写性能提升很有效 | |||
3. 操作系统级:操作系统内部有文件写缓冲及套接字写缓冲 | |||
4. 硬件级:cache等 | |||
缓存:读写时间局部性,读写空间局部性,主要通过操作系统系统,erlang虚拟机没有内部的缓存 | |||
10.套接字标志设置: | |||
延迟发送:{delay_send, true},聚合若干小消息为一个大消息,性能提升显著 | |||
发送高低水位:{high_watermark, 128 * 1024} | {low_watermark, 64 * 1024},辅助delay_send使用,delay_send的聚合缓冲区大小为high_watermark,数据缓存到high_watermark后,将阻塞port_command,使用send发送数据,直到缓冲区大小降低到low_watermark后,解除阻塞,通常这些值越大越好,但erlang虚拟机允许设置的最大值不超过128K | |||
发送缓冲大小:{sndbuf, 16 * 1024},操作系统对套接字的发送缓冲大小,在延迟发送时有效,越大越好,但有极值 | |||
接收缓冲大小:{recbuf, 16 * 1024},操作系统对套接字的接收缓冲大小 | |||
#### Erlang 虚拟机调优 | |||
目录 | |||
SMP | |||
Schedulers | |||
Port Settings | |||
Asynchronous Thread Pool | |||
Kernel Polling | |||
Warning Messages | |||
Process Limit | |||
Distribution Buffer | |||
Erlang Built-in Storage | |||
Crash Dumps | |||
Net Kernel Tick Time | |||
Shutdown Time | |||
Riak 是用Erlang语言写的,运行在Erlang虚拟机之上.所以Erlang虚拟机的调优对Riak的性能优化就显得尤为重要. Erlang虚拟机本身提供了非常多的配置参数对性能调优, Riak支持其中的一部分参数,你可以在每个node的Riak配置文件中进行设置. | |||
下表列出了其中的一部分,左边一列是Erlang中的参数名称, 右边一列是在Riak中的参数名称. | |||
Erlang parameter Riak parameter | |||
+A erlang.async_threads | |||
+K erlang.K | |||
+P erlang.process_limit | |||
+Q erlang.max_ports | |||
+S erlang.schedulers.total, erlang.schedulers.online | |||
+W erlang.W | |||
+a erlang.async_threads.stack_size | |||
+e erlang.max_ets_tables | |||
+scl erlang.schedulers.compaction_of_load | |||
+sfwi erlang.schedulers.force_wakeup_interval | |||
-smp erlang.smp | |||
+sub erlang.schedulers.utilization_balancing | |||
+zdbbl erlang.distribution_buffer_size | |||
-kernel net_ticktime erlang.distribution.net_ticktime | |||
-env FULLSWEEP_AFTER erlang.fullsweep_after | |||
-env ERL_CRASH_DUMP erlang.crash_dump | |||
-env ERL_MAX_ETS_TABLES erlang.max_ets_tables | |||
-name nodename | |||
Note on upgrading to 2.0 | |||
在Riak2.0版本之前, Erlang虚拟机相关的参数放在配置文件 vm.args 里面. 在2.0及之后的版本中, 所有Erlang虚拟机相关的配置参数放在配置文件 riak.conf 里面. 如果你从Riak2.0之前的版本升级到Riak 2.0, 你仍然可以继续使用旧的配置文件 vm.args. 但是, 如果你同时设置了配置文件 vm.args 和riak.conf, 在 vm.args里面的配置将会覆盖riak.conf里面的配置. | |||
##### SMP | |||
有些操作系统提供Erlang虚拟机对称多处理器能力(SMP)以利用多处理器硬件架构的优势. SMP的支持可以通过设置erlang.smp参数来打开和关闭, 默认是打开的. 下面的例子是关闭SMP的支持. | |||
riak.conf | |||
erlang.smp = disable | |||
由于Riak也可以运行在一些不支持SMP的操作系统上, 所以在使用之前需要确认操作系统是否支持SMP,如果操作系统本身不支持,那么需要在启动Riak集群之前在配置文件riak.conf中关闭SMP的选项. | |||
比较安全的一个选择是把erlang.smp设置成auto, 这个选项会指示Erlang虚拟机启动SMP支持之前检查操作系统是否支持以及是否有一个以上的逻辑处理器,只有这两个条件都满足的时候,Erlang虚拟机才启动SMP支持. | |||
##### Schedulers | |||
Note on missing scheduler flags | |||
We recommend that all users set the +sfwi to 500 (milliseconds) and the +sclflag to false if using the older, vm.args-based configuration system. If you are using the new, riak.conf-based configuration system, the corresponding parameters are erlang.schedulers.force_wakeup_interval anderlang.schedulers.compaction_of_load. | |||
Please note that you will need to uncomment the appropriate lines in your riak.conf for this configuration to take effect. | |||
如果在Erlang虚拟机里已经打开了支持SMP的选项, 比如erlang.smp已经被设置成enabled 或者auto,而且机器本身超过一个逻辑处理器同时也支持SMP, 那么当你启动Riak的时候, 你可以配置逻辑处理器的数量或者调度线程的数量,同时也可以设置online线程的数量. | |||
全部调度线程的数量可以通过参数erlang.schedulers.total来设置, online线程的数量则是通过参数erlang.schedulers.online来配置. 这两个参数可以分别对应到Erlang虚拟机的参数Schedulers 和SchedulersOnline. | |||
两个参数的最大值都是1024, 参数并没有统一的默认值. 但是, Erlang 虚拟机自己会尝试去判定有多少配置的CPU(core)和可用的CPU(core). 如果Erlang虚拟机能够做出这个判定,那么参数schedulers.total会默认设置成配置的CPU(core)数量, | |||
参数schedulers.online会默认设置成可用的CPU(core)数量. 但是, 如果Erlang虚拟机不能做出判定, 两个参数的默认值将会设置成1. | |||
如果两个参数中的任意一个被设置成负数, 那么意味着这个参数值将会被设成默认配置的处理器数量(如果scheduler.total是负数)或者可用的处理器数量(如果schedulers.online是负数) 减去配置的负值. 比如, 如果机器配置有100个cpu(cores)然后参数schedulers.total配置为-50, 计算以后的值就是50. | |||
如果两个参数中的任意一个被设置为0,两个值都会被重新设为默认值. | |||
如果SMP支持被关闭, 比如erlang.smp被设成disabled或者设成auto 但是机器本身不支持SMP或者机器只有一个逻辑处理器,那么两个参数schedulers.total 和 schedulers.online都将会被忽略. | |||
Scheduler Wakeup Interval | |||
调度器唤醒是一个可选处理, 通过这个Erlang 虚拟机调度器被周期性的扫描来判定是否已经陷入睡眠, 比如是否调度器有一个空的运行列表. 这个扫描时间间隔可以通过参数erlang.schedulers.force_wakeup_interval设置, 单位为毫秒.这个参数对应于Erlang虚拟机的+sfwi选项.该参数默认设为0, 不激活调度器唤醒功能. | |||
Erlang在R15Bx版本里有把调度器睡眠过于频繁的倾向,如果你使用的是更新的版本,比如Riak2.0 及以后, 那多数情况下不需要启动唤醒功能. | |||
注: OTP的工程师曾经解释过这个功能,如果需要调度的任务不是很多,没有很多task在运行列表上的话, R15B的Erlang虚拟机会倾向于把这些task尽量集中到尽可能少的调度器上来调度, 睡眠没有调度任务的调度器, 这样可以减少调度器之间的通信花费overhead, 提高CPU的利用率. 但这个也是一个trade off, 具体还是需要用户来根据自己的实际环境来调优. 因为一旦task的数量增加比较多,或者task数量没有增加但是task本身比较耗时,那么很可能就会触发调度器的唤醒, 而唤醒调度器是比较expensive的操作, 如果频繁睡眠唤醒的话,可能会得不偿失. | |||
##### Scheduler Compaction and Balancing | |||
Erlang调度器提供了两种方式来分发负载到不同的调度器上, 集中负载和utilization balancing. | |||
集中负载是默认打开的, 打开的时候Erlang虚拟机会尝试去尽可能多的使调度器繁忙,比如通过把任务集中到有限的几个调度器上(假设这几个有限的调度器充分运行的情况下可以调度完目前的tasks)使这几个调度器一直有工作做(not run out of work). 为了达到这个目的, 当虚拟机分配任务的时候会考虑哪些调度器应该被分配任务. 用户可以设置参数erlang.schedulers.compaction_of_load为false来关闭这个功能. | |||
另外一个选项, utilization balancing, 为了支持负载平衡, 默认是关闭的. 如果打开了这个选项, Erlang虚拟机则努力在不同调度器之间平衡调度器的利用. 如果不考虑每个调度器没有任务可调度的频度的话, 可以打开这个设置, erlang.schedulers.utilization_balancing 设为true(老版本里面通过设置+scl false) | |||
在任何时候, 只可以是使用两个功能中的一个. 如果同时设置这两个选项为false的话, Riak 会默认使用集中负载选项.如果同时设置为true, Riak会使用那个在配置文件riak.conf中最先出现的那个.(如果是旧版本的话,配置文件会是vm.args) | |||
##### Port Settings | |||
Riak 使用epmd, Erlang 端口映射Daemon来进行大多数的节点间的通信. 在这个系统里, 集群里的其他节点使用由nodename参数(或者是name in vm.args)来作为节点ID. 比如, riak@10.9.8.7. 在每个节点上, daemon把这些节点ID解析成一个TCP的端口. 用户可以指定一个端口范围给Riak节点来监听使用,同时也可以知道最大数量的并ports/sockets. | |||
Port Range | |||
默认情况下 , epmd绑定到TCP端口4369上并且侦听通配符接口. epmd 默认使用一个不能预测的端口作为节点间的通信, 通过绑定到端口0上, 意味着会使用第一个可用的端口. 这样就使得防火墙非常难配置. | |||
为了是防火墙配置简化, 用户可以指导Erlang虚拟机使用一个有限范围的端口或者单一端口. 这个最小和最大值可以设置在参数erlang.distribution.port_minimum和erlang.distribution.port_maximum里面. 比如, 下面的值被设为3000和5000. | |||
riak.conf | |||
app.config | |||
erlang.distribution.port_range.minimum = 3000 | |||
erlang.distribution.port_range.maximum = 5000 | |||
用户可以设置Erlang虚拟机使用一个单一端口, 如果只设置了最小值没有设置最大值,则表示使用单一端口. 比如, 下面设置使用单一端口5000. | |||
riak.conf | |||
app.config | |||
erlang.distribution.port_range.minimum = 5000 | |||
如果最小端口没有设置, Erlang虚拟机将会在随机的高编号端口上侦听. | |||
##### Maximum Ports | |||
用户可以通过设置参数erlang.max_ports来指定Erlang虚拟机可以使用的最大并发的 ports/sockets数量, 范围从1024到134217727. 默认值是65536. 在vm.args里面对应的参数是+Q 或者-env ERL_MAX_PORTS. | |||
Asynchronous Thread Pool | |||
如果Erlang虚拟机支持线程可用, 用户可以为Erlang虚拟机设置异步线程池的线程数量, 使用参数erlang.async_threads(+A in vm.args). 线程数量范围从0至1024, 默认值是64,下面的例子是设置成600的情况. | |||
riak.conf | |||
vm.args | |||
erlang.async_threads = 600 | |||
##### Stack Size | |||
除了可以指定异步线程的数量之外, 用户还可以为每个异步线程指定stack size. 参数是erlang.async_threads.stack_size, 对应到Erlang的+a参数. 用户可以在Riak中为这个参数指定size以KB, MB,GB 为单位, 有效的范围值是16至8192个字, 在32位的系统上就是64至32768字节. 该参数没有默认值, 我们建议设置为16K words, 对应为64 KB在32位系统上. 我们建议这么小一个值是考虑到异步线程数量可能会很大. | |||
注:The 64 KB default is enough for drivers delivered with Erlang/OTP but might not be large enough to accommodate drivers that use the driver_async()functionality, documented here. We recommend setting higher values with caution, always keeping the number of available threads in mind. | |||
Kernel Polling | |||
如果系统支持, 用户可以在Erlang中利用内核轮询. 内核轮询可以在使用很多文件描述符的时候提高性能. 在使用中的文件描述符越多, 内核轮询发挥的作用就越大. 该选择在Riak的Erlang虚拟机中是默认打开的, 该参数对应到Erlang虚拟机中的+K参数 | |||
##### Warning Messages | |||
Erlang虚拟机的error_logger 是一个事件管理器, 从Erlang运行时系统注册错误, 告警和信息事件. 默认情况下, error_logger的信息事件被映射为告警,但是用户可以设置映射成错误或者信息. 该设置为参数erlang.W, 可以设置的值为w(warning), errors 或者i(info reports). | |||
##### Process Limit | |||
参数erlang.process_limit可以用来设置系统同时存在的最大进程数量(对应到Erlang的+P参数), 有效范围从1024至134217727. 默认值是256000. | |||
##### Distribution Buffer | |||
用户可以通过参数erlang.distribution_buffer_size设置Erlang虚拟机的distribution buffer busy limit(对应到Erlang的+zdbbl参数). 修改这个参数对那些有许多busy dist port事件的节点可能会有帮助, 默认值是32MB, 最大值是2097151KB. 增大这个参数可以允许进程缓存更多的待发消息, 当缓存满的时候,发送线程被挂起直到缓存减小到设定值. 所以, 更大的缓存有助于降低延迟以及增加吞吐量,代价就是使用了更多的RAM. 用户需要根据机器的RAM资源来考虑设定这个值. | |||
##### Erlang Built-in Storage | |||
Erlang使用一个内置的数据库,ets(Erlang Term Storage)用来快速访问内存(constant access time rather than logarithmic access time). erts 表的最大数量设置在参数erlang.max_erts_tables里面, 默认值是256000,这个值要大于Erlang虚拟机自身的默认值1400(对应到vm.args 的参数e). 更大的erlang.max_erts_tables值可以提供更快的数据访问,代价是消耗更高的内存. | |||
##### Crash Dumps | |||
默认情况下, Riak 的Erlang crash dumps文件是存放在位置./log/erl_crash.dump. 用户可以通过设置参数erlang.crash_dump来更改存放位置. 该参数对应到Erlang虚拟机的ERL_CRASH_DUMP环境变量. | |||
##### Net Kernel Tick Time | |||
网络内核是Erlang的一个系统进程, 提供了不同的网络监视形式. 在一个Riak集群里面, 网络内核的功能之一就是去周期性的检测节点存活. Tick time就是这个检查频度, 可以通过erlang.distribution.net_ticktime设置,单位是秒. 该参数对应到vm.args里面的参数-kernal net_ticktime. | |||
##### Shutdown Time | |||
用户可以设定Erlang虚拟机的关闭时间, 该设置参数为erlang.shutdown_time,默认是10秒, 一旦10秒过了, 所有存在的进程就会被杀掉. 减少关闭时间在某些情景下可能是有帮助的, 比如说在测试的时候需要频繁的启停Riak集群. 在vm.args里参数是shutdown_time, 单位是毫秒. |
@ -0,0 +1,344 @@ | |||
Erlang 进程相关学习 | |||
==================================== | |||
# 目录 | |||
1. [actor模型和进程特性](#actor模型和进程特性) | |||
2. [进程创建](#进程创建) | |||
3. [进程监控与注册](#进程注册与监控) | |||
4. [进程调度](#进程调度) | |||
5. [进程发送消息](#进程发送消息) | |||
6. [进程接收消息](#进程接收消息) | |||
7. [进程GC](#进程GC) | |||
8. [更多](#更多) | |||
## actor模型和进程特性 | |||
### actor模型 | |||
在计算机科学中,它是一个并行计算的数学模型,最初为由大量独立的微处理器组成的高并行计算机所开发,Actor模型的理念非常简单: | |||
天下万物皆为Actor。Actor之间通过发送消息来通信,消息的传送是异步的,通过一个邮件队列(mail queue)来处理消息。每个Actor | |||
是完全独立的,可以同时执行它们的操作。每一个Actor是一个计算实体,映射接收到的消息到以下动作: | |||
1. 发送有限个消息给其它Actor | |||
2. 创建有限个新的Actor | |||
3. 为下一个接收的消息指定行为 | |||
以上三种动作并没有固定的顺序,可以并发地执行。Actor会根据接收到的消息进行不同的处理。 | |||
简而言之: 一个Actor指的是一个最基本的计算单元,它能接收一个消息并且基于其执行计算。 | |||
综上,我们知道可以把系统中的所有事物都抽象成一个Actor,那么在一个系统中,可以将一个大规模的任务分解为一些小任务,这些小任务 | |||
可以由多个Actor并发处理,从而减少任务的完成时间和任务复杂度。 | |||
为什么会在讲Erlang进程的时候讲Actor模型的概念,就是因为对于Erlang的并发编程模型正是基于Actor模型,Erlang的代码运行在 | |||
进程中,而进程就是Erlang称呼Actor的方式,Eralng也是最著名的使用Actor规则的编程的语言。 | |||
### Eralng进程特性 | |||
在Erlang的进程不是我们传统上的进程,Erlang进程是轻量级进程,它的生成、上下文切换和消息传递是由虚拟机管理的,操作系统 | |||
线程进程和Erlang进程之间没有任何联系,这使并发有关的操作不仅独立于底层的操作系统,而且也是非常高效和具有很强可扩展性。 | |||
它运行在 Erlang 虚拟机上,非常小,非常轻,可以瞬间创建上万,甚至几十万个,进程间完全是独立的内存空间执行,不共享内存, | |||
这些独立的内存空间可以独立的进行垃圾回收,基于独立运行,在发生错误的时候也是隔离的,其他不相关的进程可以继续运行。 | |||
在进程运行时若出现错误,由于进程的轻量级,Erlang 采取的措施是“任其崩溃”和“让其他进程修复”。 | |||
在Erlang上查看默认限制数量是26万多,可以进行修改。每个进程创建后都会有一个独一无二的 Pid,这些进程之间通过 Pid 来互相发 | |||
送消息,进程的唯一交互方式也是消息传递,消息也许能被对方收到,也许不能,收到后可以处理该消息。消息发送是异步的如果想知道某 | |||
个消息是否被进程收到,必须向该进程发送一个消息并等待回复。 | |||
## 进程创建 | |||
Erlang 中的并发编程只需要如下几个简单的函数。 | |||
```erlang | |||
Pid = spawn(Mod,Func, Args) | |||
``` | |||
创建一个新的并发进程来执行Mod模块中的 Fun(),Args 是参数。 | |||
跟上面提供的spawn/3功能相同的函数还有: | |||
```erlang | |||
spawn(Fun) -> pid() | |||
spawn(Node, Fun) -> pid() | |||
spawn(Module, Function, Args) -> pid() | |||
spawn(Node, Module, Function, Args) -> pid() | |||
spawn_link(Fun) -> pid() | |||
spawn_link(Node, Fun) -> pid() | |||
spawn_link(Module, Function, Args) -> pid() | |||
spawn_link(Node, Module, Function, Args) -> pid() | |||
spawn_monitor(Fun) -> {pid(), reference()} | |||
spawn_monitor(Module, Function, Args) -> {pid(), reference()} | |||
spawn_opt(Fun, Options) -> pid() | {pid(), reference()} | |||
spawn_opt(Node, Fun, Options) -> pid() | {pid(), reference()} | |||
spawn_opt(Module, Function, Args, Options) ->pid() | {pid(), reference()} | |||
spawn_opt(Node, Module, Function, Args, Options) ->pid() | {pid(), reference()} | |||
``` | |||
创建好进程,返回对应的Pid之后向就可以向进程进程发送消息,erlang用 “!”来发送消息,格式如下。notice:消息发送是异步的, | |||
发送方不等待而是继续之前的工作。 | |||
```erlang | |||
Pid !Message, | |||
Pid1 ! Pid2 ! Pid3 ! Pid..n ! Message. | |||
``` | |||
erlang用 receve ... end 来接受发送给某个进程的消息,匹配后处理,格式如下。 | |||
```erlang | |||
receive | |||
Pattern1 [when Guard1] -> | |||
Expression1; | |||
Pattern2 [when Guard2] -> | |||
Expression2; | |||
... | |||
after T -> | |||
ExpressionTimeout | |||
end | |||
``` | |||
某个消息到达后,会先与 Pattern 进行匹配,匹配相同后执行,若未匹配成功消息则会保存起来待以后处理,进程会开始下一轮操作, | |||
若等待超时T,则会执行表达式 ExpressionTimeout。 | |||
## 进程注册与监控 | |||
### 进程注册 | |||
有些时候使用通过进程Pid来标识进程需要维护进程Pid,出于某些原因维护进程Pid,不方便灵活,比如你给某个服务器进程请求数据, | |||
你还得考虑怎么得到服务器进程的Pid,有些时候进程由于某种异常重启后Pid会发生变化,如果没有及时同步机制,会导致功能异常, | |||
于是乎Erlang提供了一套进程注册管理的机制----注册进程Erlang中管理注册进程的有4个内置函数,register、unregister、 | |||
whereis、registered,它们的用法如下: | |||
1)register(Atom, Pid):将一个进程Pid注册一个名为AnAtom的原子,如果原子AnAtom已经被另一个注册进程所使用, | |||
那么注册就会失败。 | |||
2)unregister(Atom):移除与AnAtom相对应进程的所有注册信息。如果一个注册死亡,那么它也会被自动取消注册。 | |||
3)whereis(Atom) -> Pid | undefined:判断AnAtom是否已经被其他进程注册。如果成功,则返回进程标识符Pid。 | |||
如果AnAtom没有与之相对应的进程,那么就返回原子undefined。 | |||
4)registered() -> [AnAtom ::atom()]:返回一个系统中所有已经注册的名称列表。 | |||
### 进程监控 | |||
Erlang 对于进程处理理念之一是“任其崩溃”和“让其他进程修复”,常规Erlang系统中有很多进程同时运行,进程之间可能相互依赖, | |||
这么复杂的情况之下怎么实现该理念呢?Erlang除了提供exception,try catch等语法,还支持Link和Monitor两种监控进程的机制, | |||
使得所有进程可以连接起来,组成一个整体。当某个进程出错退出时,其他进程都会收到该进程退出的消息通知。有了这些特点,使用erlang | |||
建立一个简单,并且健壮的系统就不是什么难事。 | |||
#### 进程双向监控-Link | |||
相关API link(Pid), A进程调用了link(Pid) 则A进程与Pid之间就建立起了双向连接,如果两个进程相连接,如果其中一个终止时, | |||
讲发送exit信号给另一方,使其终止,同时终止进程会依次发送exit信号给所有与其连接的进程,这使得exit信号在系统内层层蔓延。 | |||
该函数连接不存在的进程时会导致发起连接的进程终止 | |||
spawn_link()系列函数 它与link(Pid)的差别就是 原子性与非原子性 | |||
unlink(Pid) 移除调用进程与Pid的连接 | |||
通过调用process_flag(trap_exit, true)可以设置捕捉exit信号, | |||
假如有A,B两个进程且彼此link | |||
总结... | |||
1.当A的结束原因是normal时(进程正常执行完就是normal),B是不会退出的,此时link机制不发生作用 | |||
2.若A的结束原因是killed,例如调用exit(PidA,kill) ,则无论B是否有设置trap_exit,B都会terminate,此时退出信号捕捉机制是无效的 | |||
3.若A的结束原因不是normal也不是killed(例如exit(PidA,Reason)),那么B在设置了trap_exit时,会捕捉到退出信号, | |||
取而代之的是收取到一条消息{‘EXIT’,Pid,Reason},这时B不会结束,用户可以根据收到的消息对A进程的结束进行处理;若B没有设置trap_exit,B就会terminate | |||
|捕获状态 |退出信号(原因) |动作 | | |||
| :-------------------| ------------------: | :--------------------------------------:| | |||
|false | normal | 不做任何事 | | |||
|false | kill | 消亡,向链接的进程广播退出信号(killed) | | |||
|false | X | 消亡,向链接的进程广播退出信号X | | |||
|true | normal | 接收到{'EXIT', Pid, nomal} | | |||
|true | kill | 消亡,向链接的进程广播退出信号(killed) | | |||
|true | X | 将{'EXIT', Pid, X} 加入到邮箱 | | |||
#### 监视器(monitor) | |||
相关API | |||
monitor(process, monitor_process_identifier()) %monitor_process_identifier() 为Pid或者已注册的进程名称 | |||
demonitor(MonitorRef) | |||
demonitor(MonitorRef, OptionList) | |||
监视器与link不同的是它是单向式观察一些进程终止,各个监视器通过Erlang的引用相互区分,是调用monitor返回的,具有唯一性, | |||
而且A进程可以设置多个对B进程的监视器,每一个通过不同的引用区分。 | |||
当被监视的进程终止时,一条格式{'Down',Reference, process, Pid, Reason}的消息会被发给监视此进程的进程 | |||
调用erlang:demonitor(Reference)可以移除监视器, | |||
调用erlang:demonitor(Reference,[flush])可以让该监视进程邮箱中所有与Reference对应的{'DOWN', Reference,process,Pid,Reason} | |||
的消息被冲刷掉。 | |||
如果尝试监视一个不存在的进程会导致收到一条{'DOWN', process, Pid,Reason}的消息,其中Reason为noproc,这和link()不一样 | |||
## 进程调度 | |||
就目前计算机体系结构而言,任何进程或线程要执行就需要得到CPU资源,对于erlang的进程同样如此。erlang虚拟机同时存在成千上万的进程, | |||
但是cpu核心数又是有限的,所有erlang并发特性就需要一个合适的调度规则来安排各个进程的运行, | |||
简单而言,erlang虚拟机调度程序保留两个队列,准备好运行的就绪队列以及等待接收消息的进程的等待队列。当等待队列中的进程收到消息或获 | |||
得超时时,它将被移动到就绪队列。调度程序从就绪队列中选择第一个进程并将其交给BEAM执行一个时间片。当时间片用完时,BEAM会抢占正在 | |||
运行的进程,并将进程添加到就绪队列的末尾。如果在时间片用完之前在接收中阻止了进程,则会将其添加到等待队列中。 | |||
Erlang调度器主要有以下特点: | |||
1. 进程调度运行在用户空间 :Erlang进程不同于操作系统进程,Erlang的进程调度也跟操作系统完全没有关系,是由Erlang虚拟机来完成的; | |||
2. 调度是抢占式的:每一个进程在创建时,都会分配一个固定数目的reduction(这个数量默认值是2000),每一次操作(函数调用), | |||
reduction就会减少,当这个数量减少到0时或者进程没有匹配的消息时,抢占就会发生(无视优先级); | |||
3. 每个进程公平的使用CPU:每个进程分配相同数量的reduction,可以保证进程可以公平的(不是相等的)使用CPU资源 | |||
4. 调度器保证软实时性:Erlang中的进程有优先级,调度器可以保证在下一次调度发生时,高优先级的进程可以优先得到执行。 | |||
Reduction | |||
受操作系统中基于时间片调度算法的影响,一开始知道有reduction这个概念时,一直想搞清楚这个reduction到底对应多长的绝对时间,不过, | |||
从Erlang本身对reduction的使用来看,完全没有必要纠结这个问题。《Erlang编程指南》一书中对reduction的说明如下: | |||
程序中的每一个命令,无论它是一个函数调用,还是一个算术操作,或者内置函数,都会分配一定数量的reduction。虚拟机使用这个值来衡量一个 | |||
进程的活动水平。 | |||
进程优先级 | |||
Erlang进程有四种优先级:max, high, normal, low(max只在Erlang运行时系统内部使用,普通进程不能使用)。Erlang运行时有两个 | |||
运行队列对应着max和high优先级的运行任务,normal和low在同一个队列中。调度器在调度发生时,总是首先查看具体max优先级的进程队列, | |||
如果队列中有可以进行的进程,就会运行,直到这个队列为空。然后会对high优先级的进程队列做同样的操作(在SMP环境,因为同时有几个调度器,所以在同一时间,可能会有不同优先级的任务在同时运行; | |||
但在同一个调度器中,同一时间,肯定是高优先级的任务优先运行)。普通进程在创建时,一般是normal优先级。normal和low优先级的进程只有 | |||
在系统中没有max和high优先级的进程可运行时才会被调度到。通常情况下,normal和low优先级的进程交替执行,low优先级获得CPU资源相对 | |||
更少(一般情况下):low优先级的任务只有在运行了normal优先级任务特定次数后(在R15B中,这个数字是8)才会被调度到(也就是说只有 | |||
在调度了8个normal优先级的进程后,low优先级的进程才会被调度到,即使low优先级的进程比normal优先级的进程更早进入调度队列,这种 | |||
机制可能会引起优先级反转:假如你有成千上万的活动normal进程,而只有几个low优先级进程,那么相比normal进程,low优先级可能会获得 | |||
更多的CPU资源)。 | |||
## 进程发送消息 | |||
Erlang系统中,进程之间的通信是通过消息传递来完成的。消息使用Pid ! Message的形式发送,通过receive语句获取。每个Erlang进程 | |||
都有用来存储传入消息的信箱。当一个消息发送的时候,它会从发送进程中拷贝到接收进程的信箱,并以它们到达的时间次序存储。消息的传递是 | |||
异步的,一个发送进程不会在发送消息后被暂停。 | |||
上面提到发送消息时,会在两个进程之间存在消息复制,为什么需要复制呢?这就跟进程的堆内存有关。虽然在Erlang的文档(heap_type)中 | |||
说明堆内存有三种类型:private,shared,hybrid,但是在实际的代码中,只有两种private和hybrid | |||
(参见[$R15B_OTP_SRC/erts/emulator/beam/erl_bif_info.c --> system_info_1]), | |||
(参见[$R15B_OTP_SRC/erts/Makefile.in:# Until hybrid is nofrag, don't build it.), | |||
也就是说Erlang目前的堆内存只有一种:private。 | |||
private类型的堆内存是跟shared类型相对的:shared是指所有线程共享同一块内存(比如Java),多个线程对同一块内存的访问需要锁保护; | |||
而private类型的堆内存是指每个进程独享一块内存,对于内存的访问不需要锁保护。 | |||
在Erlang的private堆内存架构下,发送消息需要做三件事件: | |||
1. 计算消息的大小,并在接收进程的内存空间中给消息分配内存; | |||
2. 将消息的内容拷贝到接收进程的堆内存中; | |||
3. 最后将消息的地址添加到接收进程的消息队列。 | |||
从上面的步骤可以看出,拷贝消息的代码是O(n),n是消息的长度,也就是说消息越长,花费越大。所以在使用Erlang时,要避免大数据量的大消息传递。 | |||
在shared堆内存架构下,发送消息只需要O(1)(只传递消息地址),那为什么Erlang要默认选择private类型的堆内存呢? | |||
其实这跟后面要讲到的Erlang的GC相关:private的优势就是GC的延迟很低,可以很快的完成(因为只保存一个进程的数据, | |||
GC扫描时的数据量很小)。在SMP环境下,实际上每个进程有两个消息队列。进程发送消息时,实际上消息是添加到目标进程的公有队列 | |||
(通过锁来保证互斥访问);而目标进程在消费消息时,实际上是在自己的私有消息队列上处理的,从而减小锁带来的访问开销。但是, | |||
如果目标进程在自己的私有消息队列上无法匹配到消息,那么公有队列中的消息将被添加到私有队列。 | |||
## 进程接收消息 | |||
```erlang | |||
receive | |||
Pattern1 [when Guard1] -> | |||
Expression1; | |||
Pattern2 [when Guard2] -> | |||
Expression2; | |||
... | |||
after T -> | |||
ExpressionTimeout | |||
end | |||
``` | |||
整个过程如下 | |||
1. 当我们输入receive语句时,我们启动一个计时器(如果有after T)。 | |||
2. 获取邮箱中的第一个消息,并尝试将其与Pattern1、Pattern2等进行匹配。 | |||
如果匹配成功,则从邮箱中删除消息,并计算模式后面的表达式。 | |||
3. 如果receive语句中的任何模式都不匹配邮箱中的第一个消息,那么第一个消息将从邮箱中删除并放入“save队列”中。 | |||
然后尝试邮箱中的第二条消息。重复此过程,直到找到匹配的消息或检查邮箱中的所有消息为止。 | |||
4. 如果邮箱中的所有消息都不匹配,则进程将被挂起,并在下次将新消息放入邮箱时重新安排执行时间。注意,当新消息到达时, | |||
保存队列中的消息不会重新匹配;只匹配新消息( Erlang的实现是非常“聪明”的,并且能够最小化每个消息被接收方的receive测试的次数) | |||
5. 一旦匹配了消息,那么所有放入save队列的消息都将按照到达进程的顺序重新进入邮箱。如果设置了计时器, | |||
则清除计时器。 | |||
6. 如果计时器在等待消息时超时,则计算表达式ExpressionsTimeout,并按到达进程的顺序将任何保存的消息放回邮箱。 | |||
## 进程GC | |||
erlang 进程GC | |||
Memory Layout 内存分布 | |||
在我们深入垃圾回收机制之前,我们先来看看Erlang进程的内存布局. 一个Erlang进程的内存布局通常分为是三个部分(有人认为是四个部分, | |||
把mailbox作为单独的一个部分), 进程控制块, 堆和栈,和普通的Linux进程的内存布局非常类似. | |||
``` | |||
Shared Heap Erlang Process Memory Layout | |||
+----------------------------------+ +----------------------------------+ | |||
| | | | | |||
| | | PID / Status / Registered Name | Process | |||
| | | | Control | |||
| | | Initial Call / Current Call +----> Block | |||
| | | | (PCB) | |||
| | | Mailbox Pointers | | |||
| | | | | |||
| | +----------------------------------+ | |||
| | | | | |||
| | | Function Parameters | | |||
| | | | Process | |||
| | | Return Addresses +----> Stack | |||
| | | | | |||
| +--------------+ | | Local Variables | | |||
| | | | | | | |||
| | +------------+--+ | +-------------------------------+--+ | |||
| | | | | | | | | |||
| | | +-------------+--+ | | ^ v +----> Free | |||
| | | | | | | | | Space | |||
| | | | +--------------+-+ | +--+-------------------------------+ | |||
| +-+ | | | | | | | |||
| +-+ | Refc Binary | | | Mailbox Messages (Linked List) | | |||
| +-+ | | | | | |||
| +------^---------+ | | Compound Terms (List, Tuples) | Process | |||
| | | | +----> Private | |||
| | | | Terms Larger than a word | Heap | |||
| | | | | | |||
| +--+ ProcBin +-------------+ Pointers to Large Binaries | | |||
| | | | | |||
+----------------------------------+ +----------------------------------+ | |||
``` | |||
进程控制块: 进程控制块持有关于进程的一些信息, 比如PID, 进程状态(running, waitting), 进程注册名, 初始和当前调用, | |||
指向进程mailbox的指针 | |||
栈: 栈是向下增长的, 栈持有函数调用参数,函数返回地址,本地变量以及一些临时空间用来计算表达式. | |||
堆: 堆是向上增长的, 堆持有进程的mailbox, 复合terms(Lists, Tuples, Binaries),以及大于一个机器字的对象(比如浮点数对象). | |||
大于64个字节的二进制terms,被称为Reference Counted Binary, 他们不是存在进程私有堆里面,他们是存在一个大的共享堆里,所有进程 | |||
都可以通过指向RefC Binary的指针来访问该共享堆,RefC Binary指针本身是存在进程私有堆里面的. | |||
GC Details | |||
为了更准确的解释默认的Erlang垃圾回收机制, 实际上运行在每个独立Erlang进程内部的是分代拷贝垃圾回收机制, 还有一个引用计数的 | |||
垃圾回收运行在共享堆上. | |||
Private Heap GC 私有堆垃圾回收 | |||
私有堆的垃圾回收是分代的. 分代机制把进程的堆内存分为两个部分,年轻代和年老代. 区分是基于这样一个考虑, 如果一个对象在运行 | |||
一次垃圾回收之后没有被回收,那么这个对象短期内被回收的可能性就很低. 所以, 年轻代就用来存储新分配的数据,年老代就用来存放运行 | |||
一定次数的垃圾回收之后依然幸存的数据. 这样的区分可以帮助GC减少对那些很可能还不是垃圾的数据不必要的扫描. 对应于此, Erlang的 | |||
GC扫描有两个策略, Generational(Minor) 和 Fullsweep(Major). Generational GC只回收年轻代的区域, 而Fullsweep则同时回收年轻代和 | |||
年老代. | |||
下面我们一起来review一下一个新创建的Erlang进程触发GC的步骤, 假设以下不同的场景: | |||
场景 1: | |||
Spawn > No GC > Terminate | |||
假设一个生存期较短的进程, 在存活期间使用的堆内存也没有超过 min_heap_size,那么在进程结束是全部内存即被回收. | |||
场景 2: | |||
Spawn > Fullsweep > Generational > Terminate | |||
假设一个新创建的进程,当进程的数据增长超过了min_heap_size时, fullsweep GC即被触发, 因为在此之前还没有任何GC被触发,所以堆区 | |||
还没有被分成年轻代和年老代. 在第一次fullsweep GC结束以后, 堆区就会被分为年轻代和年老代了, 从这个时候起, GC的策略就被切换为 | |||
generational GC了, 直到进程结束. | |||
场景 3: | |||
Spawn > Fullsweep > Generational > Fullsweep > Generational > ... > Terminate | |||
在某些情景下, GC策略会从generation再切换回fullsweep. 一种情景是, 在运行了一定次数(fullsweep_after)的genereration GC之后, | |||
系统会再次切换回fullsweep. 这个参数fullsweep_after可以是全局的也可以是单进程的. 全局的值可以通过函数erlang:system_info(fullsweep_after)获取, | |||
进程的可以通过函数erlang:process_info(self(),garbage_collection)来获取. 另外一种情景是, 当generation GC(minor GC)不能够收集到足够的内存空间时. | |||
最后一种情况是, 当手动调用函数garbage_collector(PID)时. 在运行fullsweep之后, GC策略再次切换回generation GC直到以上的任意一个情景再次出现. | |||
场景 4: | |||
Spawn > Fullsweep > Generational > Fullsweep > Increase Heap > Fullsweep > ... > Terminate | |||
假设在场景3里面,第二个fullsweep GC依然没有回收到足够的内存, 那么系统就会为进程增加堆内存, 然后该进程就回到第一个场景,像刚创建的进程一样首先 | |||
开始一个fullsweep,然后循环往复. | |||
那么对Erlang来说, 既然这些垃圾回收机制都是自动完成的, 为什么我们需要花时间去了解学习呢? 首先, 通过调整GC的策略可以使你的系统运行的更快. 其次, | |||
了解GC可以帮助我们从GC的角度来理解为什么Erlang是一个软实时的系统平台. 因为每个进程有自己的私有内存空间和私有GC,所以每次GC发生的时候只在进程 | |||
内部进行,只stop本进程, 不会stop其他进程,这正是一个软实时系统所需要的. | |||
Shared Heap GC 共享堆垃圾回收 | |||
共享堆的GC是通过引用计数来实现的. 共享堆里面的每个对象都有一个引用计数,这个计数就是表示该对象被多少个Erlang进程持有(对象的指针存在进程的私有堆里). | |||
如果一个对象的引入计数变成0的时候就表示该对象不可访问可以被回收了. | |||
进程调度 | |||
## ## 进程调度 | |||
就目前计算机体系结构而言,任何进程或线程要执行就需要得到CPU资源,对于erlang的进程同样如此。erlang虚拟机同时存在成千上万的进程, | |||
但是cpu核心数又是有限的,所有erlang并发特性就需要一个合适的调度规则来安排各个进程的运行, | |||
简单而言,erlang虚拟机调度程序保留两个队列,准备好运行的就绪队列以及等待接收消息的进程的等待队列。当等待队列中的进程收到消息或获 | |||
得超时时,它将被移动到就绪队列。调度程序从就绪队列中选择第一个进程并将其交给BEAM执行一个时间片。当时间片用完时,BEAM会抢占正在 | |||
运行的进程,并将进程添加到就绪队列的末尾。如果在时间片用完之前在接收中阻止了进程,则会将其添加到等待队列中。 | |||
Erlang调度器主要有以下特点: | |||
1. 进程调度运行在用户空间 :Erlang进程不同于操作系统进程,Erlang的进程调度也跟操作系统完全没有关系,是由Erlang虚拟机来完成的; | |||
2. 调度是抢占式的:每一个进程在创建时,都会分配一个固定数目的reduction(这个数量默认值是2000),每一次操作(函数调用), | |||
reduction就会减少,当这个数量减少到0时或者进程没有匹配的消息时,抢占就会发生(无视优先级); | |||
3. 每个进程公平的使用CPU:每个进程分配相同数量的reduction,可以保证进程可以公平的(不是相等的)使用CPU资源 | |||
4. 调度器保证软实时性:Erlang中的进程有优先级,调度器可以保证在下一次调度发生时,高优先级的进程可以优先得到执行。 | |||
1. What operators does Erlang have? | |||
``` | |||
Arithmetic operators: + - * / div rem | |||
Comparison operators: =:= == =/= /= > >= < =< | |||
Logical operators: and andalso or orelse | |||
Bitwise operators: bsl bsr Bitwise logical operators: band Bor bxor bnot | |||
``` | |||
--------------------- | |||