erlang 组件 application
我们日常用到的第三方的库,组件绝大部分是
application
,所以理解并且掌握applicaiton
的一些特性对我们来说非常重要,也非常实用。
1.什么是applicaiton
?,为什么要用application
官方的解释是这个样子的:
When you have written code implementing some specific functionality you might want to make the code into an application, that is, a component that can be started and stopped as a unit, and which can also be reused in other systems.
http://erlang.org/doc/design_principles/applications.html
在我理解其实就2点:
- 为了实现特定功能
- 可以复用
在我们的日常生活中,单项目多application的场景比较少,我们平时写的最多的是module
准确的说是回调模块Callback Module
,也会有少量的进程状态模块Residence Module
,前者只是一个回调函数,从一个状态切换到另一个状态,所以生命周期只在单process
,很少与外界的process
打交道,所以使用link,monitor
这些进程间关系相对较少。反观application
要考虑的方面比较多,不仅要考虑状态的正确与否,还会考虑到进程运行的异常与否,甚至会考虑到怎么来设计监控树来让程序保持健壮。
2. 如何自己实现一个application
?
2.1 目录结构
─ ${application}
├── doc
│ ├── internal
│ ├── examples
│ └── src
├── include
├── priv
├── src
│ └── ${application}.app.src
└── test
-
src
必须,存放源代码(.erl) -
priv
非必须,存放自定义的文件,比如nif的so文件等,还有资源文件 -
include
非必须,存放一些头文件(hrl),方便别的application访问 -
doc
非必须,存放一些文档 -
test
非必须,测试文件,eunit common_test
文件都放在这里
2.2 application 回调模块(callback module)
默认情况下是$APP_NAME_app
,当然自己也可以在${application}.app.src
自行定义,定义方法如下:
{application, $APP_NAME,
[
{description, ""},
{vsn, "1"},
{registered, []},
{applications, [
kernel,
stdlib
]},
% 请在这里自定义回调模块
% 请在这里修改回调参数
{mod, {$CALLBACL_MODULE, Args}},
{env, []}
]}.
下面我来做一个最简单的例子,然后来分析源代码的流程,applicaton
的名称是chapp
% 文件:chapp.app.src
{application, chapp,
[
{description, ""},
{vsn, "1"},
{registered, []},
{applications, [
kernel,
stdlib
]},
{mod, { chapp_app, [myargs]}},
{env, []}
]}.
%文件:chapp_app,erl
-module(chapp_app).
-behaviour(application).
-export([start/2, stop/1]).
-record(state, {
mod
}).
%% 启动的回调函数
start(_StartType, _StartArgs) ->
io:format("~p,~p,~p~n", [?MODULE, ?FUNCTION_NAME, {_StartType, _StartArgs}]),
{ok, Pid} = chapp_sup:start_link(),
{ok, Pid, #state{mod = ?MODULE}}.
%% 停止的回调函数
stop(_State) ->
io:format("~p,~p,~p~n", [?MODULE, ?FUNCTION_NAME, _State]),
ok.
运行结果如下:
Eshell V10.4 (abort with ^G)
1> application:start
start/1 start/2 start_boot/1 start_boot/2 start_type/0
1> application:start(chapp).
% 成功打印出 myargs
chapp_app,start,{normal,[myargs]}
ok
2> application:stop(chapp).
% 还可以自定义pre_stop函数
chapp_app,prep_stop,{state,chapp_app}
% 成功打印出 state
chapp_app,stop,{state,chapp_app}
=INFO REPORT==== 1-Dec-2019::14:58:57.004000 ===
application: chapp
exited: stopped
type: temporary
ok
2.3 $APP_NAME.app.src
文件格式
% application.erl(kernel)
start(Application, RestartType) ->
case load(Application) of
ok ->
% 要先载入 .app.src文件
Name = get_appl_name(Application),
application_controller:start_application(Name, RestartType);
{error, {already_loaded, Name}} ->
application_controller:start_application(Name, RestartType);
Error ->
Error
end.
load1(Application, DistNodes) ->
% 载入application
case application_controller:load_application(Application) of
...
Else ->
Else
end.
% 我们再来看 application_controller.erl
% application_controller.erl
load_application(Application) ->
gen_server:call(?AC, {load_application, Application}, infinity).
make_appl(Name) when is_atom(Name) ->
% 在 path 里寻找 $APP_NANE.app文件,这个文件是 $APP_NAME.app.src转化而来
FName = atom_to_list(Name) ++ ".app",
case code:where_is_file(FName) of
non_existing ->
{error, {file:format_error(enoent), FName}};
FullName ->
case prim_consult(FullName) of
{ok, [Application]} ->
{ok, make_appl_i(Application)};
{error, Reason} ->
{error, {file:format_error(Reason), FName}};
error ->
{error, "bad encoding"}
end
end;
% 这个是$APP_NAME.app.src个文件格式
% {application, Name, Opts}
% 其中 有几个一定要有的,如:description,mod,env
% 我们要特别注意mod里面的参数,因为这个是我们回调的参数
make_appl_i({application, Name, Opts}) when is_atom(Name), is_list(Opts) ->
Descr = get_opt(description, Opts, ""),
Id = get_opt(id, Opts, ""),
Vsn = get_opt(vsn, Opts, ""),
Mods = get_opt(modules, Opts, []),
Regs = get_opt(registered, Opts, []),
Apps = get_opt(applications, Opts, []),
Mod =
case get_opt(mod, Opts, []) of
{M, _A} = MA when is_atom(M) -> MA;
[] -> [];
Other -> throw({error, {badstartspec, Other}})
end,
Phases = get_opt(start_phases, Opts, undefined),
Env = get_opt(env, Opts, []),
MaxP = get_opt(maxP, Opts, infinity),
MaxT = get_opt(maxT, Opts, infinity),
IncApps = get_opt(included_applications, Opts, []),
{#appl_data{name = Name, regs = Regs, mod = Mod, phases = Phases,
mods = Mods, inc_apps = IncApps, maxP = MaxP, maxT = MaxT},
Env, IncApps, Descr, Id, Vsn, Apps};
2.4 application
启动步骤
% application_controller.erl
handle_call({start_application, AppName, RestartType}, From, S) ->
#state{running = Running, starting = Starting, start_p_false = SPF,
started = Started, start_req = Start_req} = S,
%% Check if the commandline environment variables are OK.
%% Incase of erroneous variables do not start the application,
%% if the application is permanent crash the node.
%% Check if the application is already starting.
case lists:keyfind(AppName, 1, Start_req) of
false ->
case catch check_start_cond(AppName, RestartType, Started, Running) of
{ok, Appl} ->
......
{false, undefined} ->
% 正常的入口
spawn_starter(From, Appl, S, normal),
{noreply, S#state{starting = [{AppName, RestartType, normal, From} |
Starting],
start_req = [{AppName, From} | Start_req]}};
......
{error, _R} = Error ->
{reply, Error, S}
end;
{AppName, _FromX} ->
SS = S#state{start_req = [{AppName, From} | Start_req]},
{noreply, SS}
end;
start_appl(Appl, S, Type) ->
ApplData = Appl#appl.appl_data,
case ApplData#appl_data.mod of
[] ->
{ok, undefined};
_ ->
%% Name = ApplData#appl_data.name,
......
% 交给application_master来启动
case application_master:start_link(ApplData, Type) of
{ok, _Pid} = Ok ->
Ok;
{error, _Reason} = Error ->
throw(Error)
end
end.
% application_master.erl
start_link(ApplData, Type) ->
Parent = whereis(application_controller),
proc_lib:start_link(application_master, init, [Parent, self(), ApplData, Type]).
start_it_old(Tag, From, Type, ApplData) ->
{M, A} = ApplData#appl_data.mod,
case catch M:start(Type, A) of
{ok, Pid} ->
% 启动成功了,默认状态 State = []
link(Pid),
From ! {Tag, {ok, self()}},
loop_it(From, Pid, M, []);
{ok, Pid, AppState} ->
% 启动成功了,默认状态 State = AppState
link(Pid),
From ! {Tag, {ok, self()}},
% 启动成功,自己loop进入主循环
loop_it(From, Pid, M, AppState);
{'EXIT', normal} ->
From ! {Tag, {error, {{'EXIT', normal}, {M, start, [Type, A]}}}};
{error, Reason} ->
From ! {Tag, {error, {Reason, {M, start, [Type, A]}}}};
Other ->
From ! {Tag, {error, {bad_return, {{M, start, [Type, A]}, Other}}}}
end.
% 另一个入口,和上面几乎一样的逻辑
start_supervisor(Type, M, A) ->
case catch M:start(Type, A) of
{ok, Pid} ->
{ok, Pid, []};
{ok, Pid, AppState} ->
{ok, Pid, AppState};
{error, Reason} ->
{error, {Reason, {M, start, [Type, A]}}};
{'EXIT', normal} ->
{error, {{'EXIT', normal}, {M, start, [Type, A]}}};
Other ->
{error, {bad_return, {{M, start, [Type, A]}, Other}}}
end.
启动之后的proc之间的拓扑图如下:
|application_controller | --- |(application_master:main_loop)| --- | (application_master:loop_it)| --- | chapp_sup|
到此,我们已经将启动的代码流程走了一遍,一些回调参数也已经很清楚。总结成以下几点
-
application_controller
才是真正启动appcation
的process
,或者叫父进程(process)
-
application
启动是异步的,启动结束才将结果cast
给application_controller
- 启动之前要先载入
(load)
2.5 application
如何停止(stop)
?
% application_controller.erl
handle_call({stop_application, AppName}, _From, S) ->
#state{running = Running, started = Started} = S,
case lists:keyfind(AppName, 1, Running) of
{_AppName, Id} ->
{_AppName2, Type} = lists:keyfind(AppName, 1, Started),
stop_appl(AppName, Id, Type),
NRunning = keydelete(AppName, 1, Running),
NStarted = keydelete(AppName, 1, Started),
cntrl(AppName, S, {ac_application_stopped, AppName}),
{reply, ok, S#state{running = NRunning, started = NStarted}};
false ->
case lists:keymember(AppName, 1, Started) of
true ->
NStarted = keydelete(AppName, 1, Started),
cntrl(AppName, S, {ac_application_stopped, AppName}),
{reply, ok, S#state{started = NStarted}};
false ->
{reply, {error, {not_started, AppName}}, S}
end
% application_master.erl
stop(AppMaster) -> call(AppMaster, stop).
main_loop(Parent, State) ->
receive
......
Other ->
NewState = handle_msg(Other, State),
main_loop(Parent, NewState)
end.
handle_msg({stop, Tag, From}, State) ->
catch terminate(normal, State),
From ! {Tag, ok},
% 自己主动退出
exit(normal);
loop_it(Parent, Child, Mod, AppState) ->
receive
......
{'EXIT', Parent, Reason} ->
% 在stop之前还可以自定义pre_stop阶段
% application_master:main_loop退出了, application_master:loop_it收到消息
% 执行退出逻辑
NewAppState = prep_stop(Mod, AppState),
exit(Child, Reason),
receive
{'EXIT', Child, Reason2} ->
exit(Reason2)
end,
% stop 回调调用点
catch Mod:stop(NewAppState);
......
_ ->
......
end.
至此,application
的停止逻辑已经分析完成,我们通过阅读代码还能找到一个文档中没有的hook:pre_stop
3.总结
本文通过实例加阅读源代码的方式,演示了一遍application
的实现,希望让读者加深对application
的理解,为合理使用application
打下坚实的基础。
4.参考文献:
- http://erlang.org/doc/design_principles/applications.html
- https://github.com/erlang/otp/blob/master/lib/kernel/src/application.erl
- https://github.com/erlang/otp/blob/master/lib/kernel/src/application_controller.erl
- https://github.com/erlang/otp/blob/master/lib/kernel/src/application_master.erl
- https://github.com/erlang/otp/blob/master/lib/kernel/src/application_starter.erl
网友评论