6 sys 和 proc_lib
sys 模块包含用于使用行为实现的进程的简单调试功能。它还包含一些函数,这些函数与 proc_lib 模块中的函数一起使用,可以用来实现一个符合 OTP 设计原则的 **特殊进程**,而无需使用标准行为。这些函数还可以用来实现用户定义的(非标准)行为。
sys 和 proc_lib 都属于 STDLIB 应用程序。
6.1 简单调试
sys 模块包含用于使用行为实现的进程的简单调试功能。来自 gen_statem 行为 的 code_lock 示例用于说明这一点
Erlang/OTP 20 [DEVELOPMENT] [erts-9.0] [source-5ace45e] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:10] [hipe] [kernel-poll:false] Eshell V9.0 (abort with ^G) 1> code_lock:start_link([1,2,3,4]). Lock {ok,<0.63.0>} 2> sys:statistics(code_lock, true). ok 3> sys:trace(code_lock, true). ok 4> code_lock:button(1). *DBG* code_lock receive cast {button,1} in state locked ok *DBG* code_lock consume cast {button,1} in state locked 5> code_lock:button(2). *DBG* code_lock receive cast {button,2} in state locked ok *DBG* code_lock consume cast {button,2} in state locked 6> code_lock:button(3). *DBG* code_lock receive cast {button,3} in state locked ok *DBG* code_lock consume cast {button,3} in state locked 7> code_lock:button(4). *DBG* code_lock receive cast {button,4} in state locked ok Unlock *DBG* code_lock consume cast {button,4} in state locked *DBG* code_lock receive state_timeout lock in state open Lock *DBG* code_lock consume state_timeout lock in state open 8> sys:statistics(code_lock, get). {ok,[{start_time,{{2017,4,21},{16,8,7}}}, {current_time,{{2017,4,21},{16,9,42}}}, {reductions,2973}, {messages_in,5}, {messages_out,0}]} 9> sys:statistics(code_lock, false). ok 10> sys:trace(code_lock, false). ok 11> sys:get_status(code_lock). {status,<0.63.0>, {module,gen_statem}, [[{'$initial_call',{code_lock,init,1}}, {'$ancestors',[<0.61.0>]}], running,<0.61.0>,[], [{header,"Status for state machine code_lock"}, {data,[{"Status",running}, {"Parent",<0.61.0>}, {"Logged Events",[]}, {"Postponed",[]}]}, {data,[{"State", {locked,#{code => [1,2,3,4],remaining => [1,2,3,4]}}}]}]]}
6.2 特殊进程
本节描述了如何在不使用标准行为的情况下编写符合 OTP 设计原则的进程。这样的进程需要
系统消息是指具有特殊含义的消息,用于监管树。典型的系统消息包括跟踪输出请求以及暂停或恢复进程执行的请求(在版本处理期间使用)。使用标准行为实现的进程会自动理解这些消息。
示例
来自 概述 的简单服务器,使用 sys 和 proc_lib 实现,使其适合监管树
-module(ch4). -export([start_link/0]). -export([alloc/0, free/1]). -export([init/1]). -export([system_continue/3, system_terminate/4, write_debug/3, system_get_state/1, system_replace_state/2]). start_link() -> proc_lib:start_link(ch4, init, [self()]). alloc() -> ch4 ! {self(), alloc}, receive {ch4, Res} -> Res end. free(Ch) -> ch4 ! {free, Ch}, ok. init(Parent) -> register(ch4, self()), Chs = channels(), Deb = sys:debug_options([]), proc_lib:init_ack(Parent, {ok, self()}), loop(Chs, Parent, Deb). loop(Chs, Parent, Deb) -> receive {From, alloc} -> Deb2 = sys:handle_debug(Deb, fun ch4:write_debug/3, ch4, {in, alloc, From}), {Ch, Chs2} = alloc(Chs), From ! {ch4, Ch}, Deb3 = sys:handle_debug(Deb2, fun ch4:write_debug/3, ch4, {out, {ch4, Ch}, From}), loop(Chs2, Parent, Deb3); {free, Ch} -> Deb2 = sys:handle_debug(Deb, fun ch4:write_debug/3, ch4, {in, {free, Ch}}), Chs2 = free(Ch, Chs), loop(Chs2, Parent, Deb2); {system, From, Request} -> sys:handle_system_msg(Request, From, Parent, ch4, Deb, Chs) end. system_continue(Parent, Deb, Chs) -> loop(Chs, Parent, Deb). system_terminate(Reason, _Parent, _Deb, _Chs) -> exit(Reason). system_get_state(Chs) -> {ok, Chs}. system_replace_state(StateFun, Chs) -> NChs = StateFun(Chs), {ok, NChs, NChs}. write_debug(Dev, Event, Name) -> io:format(Dev, "~p event = ~p~n", [Name, Event]).
有关如何使用 sys 模块中的简单调试函数的示例,也可以用于 ch4
% erl Erlang (BEAM) emulator version 5.2.3.6 [hipe] [threads:0] Eshell V5.2.3.6 (abort with ^G) 1> ch4:start_link(). {ok,<0.30.0>} 2> sys:statistics(ch4, true). ok 3> sys:trace(ch4, true). ok 4> ch4:alloc(). ch4 event = {in,alloc,<0.25.0>} ch4 event = {out,{ch4,ch1},<0.25.0>} ch1 5> ch4:free(ch1). ch4 event = {in,{free,ch1}} ok 6> sys:statistics(ch4, get). {ok,[{start_time,{{2003,6,13},{9,47,5}}}, {current_time,{{2003,6,13},{9,47,56}}}, {reductions,109}, {messages_in,2}, {messages_out,1}]} 7> sys:statistics(ch4, false). ok 8> sys:trace(ch4, false). ok 9> sys:get_status(ch4). {status,<0.30.0>, {module,ch4}, [[{'$ancestors',[<0.25.0>]},{'$initial_call',{ch4,init,[<0.25.0>]}}], running,<0.25.0>,[], [ch1,ch2,ch3]]}
启动进程
要使用 proc_lib 模块中的函数启动进程。可以使用多种函数,例如,spawn_link/3,4 用于异步启动,而 start_link/3,4,5 用于同步启动。
使用其中一个函数启动的进程会存储一些信息(例如,关于祖先和初始调用),这些信息对于监管树中的进程是必要的。
如果进程以除 normal 或 shutdown 以外的其他原因终止,则会生成崩溃报告。有关崩溃报告的更多信息,请参阅 SASL 用户指南。
在本例中,使用同步启动。进程通过调用 ch4:start_link() 启动
start_link() -> proc_lib:start_link(ch4, init, [self()]).
ch4:start_link 调用函数 proc_lib:start_link。此函数以模块名称、函数名称和参数列表作为参数,生成并链接到一个新进程。新进程通过执行给定的函数启动,此处为 ch4:init(Pid),其中 Pid 是第一个进程(即父进程)的 pid (self())。
所有初始化,包括名称注册,都在 init 中完成。新进程还必须向父进程确认已启动
init(Parent) -> ... proc_lib:init_ack(Parent, {ok, self()}), loop(...).
proc_lib:start_link 是同步的,它不会返回,直到调用了 proc_lib:init_ack 或 proc_lib:init_fail,或者进程退出。
调试
为了支持 sys 中的调试功能,需要一个 **调试结构**。使用 sys:debug_options/1 初始化 Deb 项
init(Parent) -> ... Deb = sys:debug_options([]), ... loop(Chs, Parent, Deb).
sys:debug_options/1 以选项列表作为参数。此处列表为空,这意味着最初没有启用调试。有关可能选项的信息,请参阅 STDLIB 中的 sys(3) 手册页。
然后,对于要记录或跟踪的每个 **系统事件**,需要调用以下函数。
sys:handle_debug(Deb, Func, Info, Event) => Deb1
此处
- Deb 是调试结构。
-
Func 是一个 fun,它指定一个(用户定义的)用于格式化跟踪输出的函数。对于每个系统事件,格式函数将以 Func(Dev, Event, Info) 的形式调用,其中
- Dev 是要打印输出的 I/O 设备。请参阅 STDLIB 中的 io(3) 手册页。
- Event 和 Info 直接从 handle_debug 传递。
- Info 用于向 Func 传递更多信息。它可以是任何项,并将直接传递。
- Event 是系统事件。由用户定义什么是系统事件以及如何表示它。通常情况下,至少传入消息和传出消息被视为系统事件,分别由元组 {in,Msg[,From]} 和 {out,Msg,To[,State]} 表示。
handle_debug 返回更新后的调试结构 Deb1。
在本例中,对于每个传入消息和传出消息,都会调用 handle_debug。格式函数 Func 是函数 ch4:write_debug/3,它使用 io:format/3 打印消息。
loop(Chs, Parent, Deb) -> receive {From, alloc} -> Deb2 = sys:handle_debug(Deb, fun ch4:write_debug/3, ch4, {in, alloc, From}), {Ch, Chs2} = alloc(Chs), From ! {ch4, Ch}, Deb3 = sys:handle_debug(Deb2, fun ch4:write_debug/3, ch4, {out, {ch4, Ch}, From}), loop(Chs2, Parent, Deb3); {free, Ch} -> Deb2 = sys:handle_debug(Deb, fun ch4:write_debug/3, ch4, {in, {free, Ch}}), Chs2 = free(Ch, Chs), loop(Chs2, Parent, Deb2); ... end. write_debug(Dev, Event, Name) -> io:format(Dev, "~p event = ~p~n", [Name, Event]).
处理系统消息
**系统消息** 以以下形式接收
{system, From, Request}
进程不需要解释这些消息的内容和含义。相反,需要调用以下函数
sys:handle_system_msg(Request, From, Parent, Module, Deb, State)
此函数不返回。它会处理系统消息,然后在进程执行需要继续的情况下调用以下内容
Module:system_continue(Parent, Deb, State)
或在进程需要终止的情况下调用以下内容
Module:system_terminate(Reason, Parent, Deb, State)
监管树中的进程预计会以与其父进程相同的理由终止。
- Request 和 From 将直接从系统消息传递到对 handle_system_msg 的调用中。
- Parent 是父进程的 pid。
- Module 是模块的名称。
- Deb 是调试结构。
- State 是一个描述内部状态的项,它被传递到 system_continue/system_terminate/ system_get_state/system_replace_state。
如果进程需要返回其状态,则 handle_system_msg 会调用
Module:system_get_state(State)
如果进程需要使用 fun StateFun 替换其状态,则 handle_system_msg 会调用
Module:system_replace_state(StateFun, State)
在本例中
loop(Chs, Parent, Deb) -> receive ... {system, From, Request} -> sys:handle_system_msg(Request, From, Parent, ch4, Deb, Chs) end. system_continue(Parent, Deb, Chs) -> loop(Chs, Parent, Deb). system_terminate(Reason, Parent, Deb, Chs) -> exit(Reason). system_get_state(Chs) -> {ok, Chs, Chs}. system_replace_state(StateFun, Chs) -> NChs = StateFun(Chs), {ok, NChs, NChs}.
如果特殊进程被设置为捕获退出,并且父进程终止,则预期行为是使用相同的理由终止
init(...) -> ..., process_flag(trap_exit, true), ..., loop(...). loop(...) -> receive ... {'EXIT', Parent, Reason} -> ..maybe some cleaning up here.. exit(Reason); ... end.
6.3 用户定义的行为
为了实现用户定义的行为,请编写类似于特殊进程代码的代码,但是调用回调模块中的函数来处理特定任务。
如果编译器需要警告缺少的回调函数(与 OTP 行为一样),请在行为模块中添加 -callback 属性以描述预期的回调
-callback Name1(Arg1_1, Arg1_2, ..., Arg1_N1) -> Res1. -callback Name2(Arg2_1, Arg2_2, ..., Arg2_N2) -> Res2. ... -callback NameM(ArgM_1, ArgM_2, ..., ArgM_NM) -> ResM.
NameX 是预期回调的名称。 ArgX_Y 和 ResX 是类型,如 类型和函数规范 中所述。 -spec 属性的整个语法都受 -callback 属性支持。
行为用户可以选择实现的可选回调函数通过使用 -optional_callbacks 属性指定
-optional_callbacks([OptName1/OptArity1, ..., OptNameK/OptArityK]).
其中每个 OptName/OptArity 指定回调函数的名称和元数。请注意,-optional_callbacks 属性需要与 -callback 属性一起使用;它不能与下面描述的 behaviour_info() 函数组合使用。
需要了解可选回调函数的工具可以调用 Behaviour:behaviour_info(optional_callbacks) 来获取所有可选回调函数的列表。
建议使用 -callback 属性而不是 behaviour_info() 函数。原因是额外的类型信息可以被工具用来生成文档或查找差异。
作为 -callback 和 -optional_callbacks 属性的替代方案,您可以直接实现和导出 behaviour_info()
behaviour_info(callbacks) -> [{Name1, Arity1},...,{NameN, ArityN}].
其中每个 {Name, Arity} 指定回调函数的名称和元数。此函数在其他情况下会由编译器使用 -callback 属性自动生成。
当编译器在模块 Mod 中遇到模块属性 -behaviour(Behaviour). 时,它会调用 Behaviour:behaviour_info(callbacks) 并将其结果与 Mod 实际导出的函数集进行比较,如果缺少任何回调函数,则会发出警告。
示例
%% User-defined behaviour module -module(simple_server). -export([start_link/2, init/3, ...]). -callback init(State :: term()) -> 'ok'. -callback handle_req(Req :: term(), State :: term()) -> {'ok', Reply :: term()}. -callback terminate() -> 'ok'. -callback format_state(State :: term()) -> term(). -optional_callbacks([format_state/1]). %% Alternatively you may define: %% %% -export([behaviour_info/1]). %% behaviour_info(callbacks) -> %% [{init,1}, %% {handle_req,2}, %% {terminate,0}]. start_link(Name, Module) -> proc_lib:start_link(?MODULE, init, [self(), Name, Module]). init(Parent, Name, Module) -> register(Name, self()), ..., Dbg = sys:debug_options([]), proc_lib:init_ack(Parent, {ok, self()}), loop(Parent, Module, Deb, ...). ...
在回调模块中
-module(db). -behaviour(simple_server). -export([init/1, handle_req/2, terminate/0]). ...
在行为模块中使用 -callback 属性指定的契约可以通过在回调模块中添加 -spec 属性进一步细化。这很有用,因为 -callback 契约通常是通用的。同一个回调模块,可以针对回调定义不同的契约。
-module(db). -behaviour(simple_server). -export([init/1, handle_req/2, terminate/0]). -record(state, {field1 :: [atom()], field2 :: integer()}). -type state() :: #state{}. -type request() :: {'store', term(), term()}; {'lookup', term()}. ... -spec handle_req(request(), state()) -> {'ok', term()}. ...
每个 -spec 契约必须是相应 -callback 契约的子类型。