檢視原始碼 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類型與函式規格中所述的類型。-callback 屬性支援 -spec 屬性的完整語法。

使用者可以選擇實作的行為的可選回呼函式由使用 -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 合約的子類型。