查看源码 sys 和 proc_lib
sys
模块具有用于简单调试使用行为实现的进程的函数。它还具有一些函数,这些函数与 proc_lib
模块中的函数一起使用,可以实现一个符合 OTP 设计原则的特殊进程,而无需使用标准行为。这些函数也可以用于实现用户定义的(非标准)行为。
简单调试
sys
模块具有用于简单调试使用行为实现的进程的函数。来自 gen_statem 行为 的 code_lock
示例用于说明这一点
Erlang/OTP 27 [erts-15.0] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit]
Eshell V15.0 (press Ctrl+G to abort, type help(). for help)
1> code_lock:start_link([1,2,3,4]).
Lock
{ok,<0.90.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 => open
*DBG* code_lock start_timer {state_timeout,10000,lock,[]} in state open
*DBG* code_lock receive state_timeout lock in state open
Lock
*DBG* code_lock consume state_timeout lock in state open => locked
8> sys:statistics(code_lock, get).
{ok,[{start_time,{{2024,5,3},{8,11,1}}},
{current_time,{{2024,5,3},{8,11,48}}},
{reductions,4098},
{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.90.0>,
{module,gen_statem},
[[{'$initial_call',{code_lock,init,1}},
{'$ancestors',[<0.88.0>,<0.87.0>,<0.70.0>,<0.65.0>,<0.69.0>,
<0.64.0>,kernel_sup,<0.47.0>]}],
running,<0.88.0>,[],
[{header,"Status for state machine code_lock"},
{data,[{"Status",running},
{"Parent",<0.88.0>},
{"Modules",[code_lock]},
{"Time-outs",{0,[]}},
{"Logged Events",[]},
{"Postponed",[]}]},
{data,[{"State",
{locked,#{code => [1,2,3,4],
length => 4,buttons => []}}}]}]]}
特殊进程
本节介绍如何编写符合 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/OTP 27 [erts-15.0] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit]
Eshell V15.0 (press Ctrl+G to abort, type help(). for help)
1> ch4:start_link().
{ok,<0.90.0>}
2> sys:statistics(ch4, true).
ok
3> sys:trace(ch4, true).
ok
4> ch4:alloc().
ch4 event = {in,alloc,<0.88.0>}
ch4 event = {out,{ch4,1},<0.88.0>}
1
5> ch4:free(ch1).
ch4 event = {in,{free,ch1}}
ok
6> sys:statistics(ch4, get).
{ok,[{start_time,{{2024,5,3},{8,26,13}}},
{current_time,{{2024,5,3},{8,26,49}}},
{reductions,202},
{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.90.0>,
{module,ch4},
[[{'$initial_call',{ch4,init,1}},
{'$ancestors',[<0.88.0>,<0.87.0>,<0.70.0>,<0.65.0>,<0.69.0>,
<0.64.0>,kernel_sup,<0.47.0>]}],
running,<0.88.0>,[],
{[1],[2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19|...]}]}
启动进程
要使用 proc_lib
模块中的函数启动进程。有多个函数可用,例如,用于异步启动的 proc_lib:spawn_link/3,4
和用于同步启动的 proc_lib:start_link/3,4,5
。
当通过这些函数之一启动进程时,将存储监督树中进程所需的信息,例如关于祖先和初始调用的详细信息。
如果进程以 normal
或 shutdown
以外的原因终止,则会生成崩溃报告。有关崩溃报告的更多信息,请参阅 Kernel 用户指南中的 日志记录。
在示例中,使用同步启动。进程通过调用 ch4:start_link()
启动
start_link() ->
proc_lib:start_link(ch4, init, [self()]).
ch4:start_link/0
调用 proc_lib:start_link/3
,该函数将模块名称、函数名称和参数列表作为参数。然后,它生成一个新的进程并建立一个链接。新进程通过执行给定的函数启动,这里是 ch4:init(Pid)
,其中 Pid
是父进程的 pid(通过调用 self()
在调用 proc_lib:start_link/3
中获得)。
所有初始化,包括名称注册,都在 init/1
中完成。新进程必须向父进程确认已启动
init(Parent) ->
...
proc_lib:init_ack(Parent, {ok, self()}),
loop(...).
proc_lib:start_link/3
是同步的,并且在调用 proc_lib:init_ack/1,2
或 proc_lib:init_fail/2,3
或进程退出之前不会返回。
调试
要支持 sys
中的调试功能,需要一个调试结构。Deb
术语使用 sys:debug_options/1
初始化
init(Parent) ->
...
Deb = sys:debug_options([]),
...
loop(Chs, Parent, Deb).
sys:debug_options/1
接受一个选项列表。在本例中,给定一个空列表意味着调试最初处于禁用状态。有关可能选项的信息,请参阅 STDLIB 中的 sys
。
对于要记录或跟踪的每个系统事件,都要调用以下函数
sys:handle_debug(Deb, Func, Info, Event) => Deb1
参数具有以下含义
Deb
是从sys:debug_options/1
返回的调试结构。Func
是一个 fun,指定用于格式化跟踪输出的(用户定义)函数。对于每个系统事件,格式函数都会以Func(Dev, Event, Info)
的形式调用,其中Dev
是要将输出打印到的 I/O 设备。请参阅 STDLIB 中的io
。Event
和Info
从对sys:handle_debug/4
的调用按原样传递。
Info
用于将更多信息传递给Func
。它可以是任何术语,并且会按原样传递。Event
是系统事件。用户可以定义系统事件是什么以及如何表示它。通常,至少传入和传出的消息被认为是系统事件,并分别由元组{in,Msg[,From]}
和{out,Msg,To[,State]}
表示。
sys:handle_debug/4
返回更新后的调试结构 Deb1
。
在示例中,对于每个传入和传出的消息都会调用 sys:handle_debug/4
。格式函数 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)
参数具有以下含义
- 来自接收到的系统消息的
Request
和From
应按原样传递给对sys:handle_system_msg/6
的调用。 Parent
是父进程的 pid。Module
是实现特殊进程的模块的名称。Deb
是调试结构。State
是描述内部状态的术语,并传递给Module:system_continue/3
、Module:system_terminate/4
、Module:system_get_state/1
和Module:system_replace_state/2
。
sys:handle_system_msg/6
不会返回。它处理系统消息并最终调用以下函数之一
Module:system_continue(Parent, Deb, State)
- 如果要继续进程执行。Module:system_terminate(Reason, Parent, Deb, State)
- 如果要终止进程。
在处理系统消息时,sys:handle_system_msg/6
可以调用以下函数之一
Module:system_get_state(State)
- 如果进程要返回其状态。Module:system_replace_state(StateFun, State)
- 如果进程要使用 funStateFun
替换其状态。有关更多信息,请参阅sys:replace_state/3
。system_code_change(Misc, Module, OldVsn, Extra)
- 如果进程要执行代码更改。
监督树中的进程应以与其父进程相同的原因终止。
在示例中,系统消息由以下代码处理
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}.
如果将特殊进程配置为捕获退出,则一旦父进程终止,它必须注意来自父进程的“EXIT”消息,并使用相同的退出原因终止。
这是一个示例
init(Parent) ->
...,
process_flag(trap_exit, true),
...,
loop(Parent).
loop(Parent) ->
receive
...
{'EXIT', Parent, Reason} ->
%% Clean up here, if needed.
exit(Reason);
...
end.
用户定义的行为
要实现用户定义的行为,请编写类似于特殊进程代码的代码,但调用回调模块中的函数来处理特定任务。
如果要让编译器警告缺少回调函数(就像对 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]).
...
可以通过在回调模块中添加 -spec
属性来进一步完善在行为模块中使用 -callback
属性指定的协定。这很有用,因为 -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
协定的子类型。