查看源码 sys 和 proc_lib

sys 模块具有用于简单调试使用行为实现的进程的函数。它还具有一些函数,这些函数与 proc_lib 模块中的函数一起使用,可以实现一个符合 OTP 设计原则的特殊进程,而无需使用标准行为。这些函数也可以用于实现用户定义的(非标准)行为。

sysproc_lib 都属于 STDLIB 应用。

简单调试

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 设计原则的进程,而无需使用标准行为。这样的进程应

系统消息是具有特殊含义的消息,用于监督树中。典型的系统消息是跟踪输出请求以及暂停或恢复进程执行的请求(在发布处理期间使用)。使用标准行为实现的进程会自动理解这些消息。

示例

下面是来自 概述 的简单服务器,使用 sysproc_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

当通过这些函数之一启动进程时,将存储监督树中进程所需的信息,例如关于祖先和初始调用的详细信息。

如果进程以 normalshutdown 以外的原因终止,则会生成崩溃报告。有关崩溃报告的更多信息,请参阅 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,2proc_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
    • EventInfo 从对 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)

参数具有以下含义

  • 来自接收到的系统消息的 RequestFrom 应按原样传递给对 sys:handle_system_msg/6 的调用。
  • Parent 是父进程的 pid。
  • Module 是实现特殊进程的模块的名称。
  • Deb 是调试结构。
  • State 是描述内部状态的术语,并传递给 Module:system_continue/3Module:system_terminate/4Module:system_get_state/1Module: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) - 如果进程要使用 fun StateFun 替换其状态。有关更多信息,请参阅 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_YResX类型和函数规范 中描述的类型。-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 协定的子类型。