6  sys 和 proc_lib

6 sys 和 proc_lib

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

sysproc_lib 都属于 STDLIB 应用程序。

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]}}}]}]]}
    

本节描述了如何在不使用标准行为的情况下编写符合 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 (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 用于同步启动。

使用其中一个函数启动的进程会存储一些信息(例如,关于祖先和初始调用),这些信息对于监管树中的进程是必要的。

如果进程以除 normalshutdown 以外的其他原因终止,则会生成崩溃报告。有关崩溃报告的更多信息,请参阅 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_ackproc_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) 手册页。
    • EventInfo 直接从 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)

监管树中的进程预计会以与其父进程相同的理由终止。

  • RequestFrom 将直接从系统消息传递到对 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.

为了实现用户定义的行为,请编写类似于特殊进程代码的代码,但是调用回调模块中的函数来处理特定任务。

如果编译器需要警告缺少的回调函数(与 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]).

...

在行为模块中使用 -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 契约的子类型。