Erlang 调试、追踪与动态代码加载技术详解
1. 调试与追踪基础
在 Erlang 中,调试和追踪是理解系统行为的重要手段。我们可以在不特殊编译代码的情况下追踪进程,这对于测试复杂系统且不修改代码非常有用,尤其适用于嵌入式系统。
1.1 调试资源
若想深入了解调试,可参考以下资源:
- 调试器参考手册 :这是一份 46 页的 PDF 文件,介绍了调试器,包含屏幕截图、API 文档等,是调试器重度用户的必读资料。
- 调试器命令 :可在此找到 shell 中可用的调试器命令。
1.2 低级别追踪 BIF
在低级别,我们可通过调用一些 Erlang 内置函数(BIF)来设置追踪。其中, erlang:trace/3 和 erlang:trace_pattern/3 尤为重要。
- erlang:trace(PidSpec, How, FlagList) :用于启动追踪。 PidSpec 告知系统要追踪的内容, How 是布尔值,用于开启或关闭追踪, FlagList 决定要追踪的事件(如函数调用、消息发送、垃圾回收等)。调用此函数后,当追踪事件发生时,调用该 BIF 的进程将收到追踪消息。
- erlang:trace_pattern(MFA, MatchSpec, FlagList) :用于设置追踪模式。 MFA 是 {Module, Function, Args} 元组,指定追踪模式适用的代码; MatchSpec 是每次进入 MFA 指定的函数时测试的模式; FlagList 说明满足追踪条件时要执行的操作。
1.3 简单追踪器示例
以下是一个简单的追踪器示例,用于追踪模块中的所有函数调用和返回值:
trace_module(Mod, StartFun) ->
%% 生成一个进程进行追踪
spawn(fun() -> trace_module1(Mod, StartFun) end).
trace_module1(Mod, StartFun) ->
%% 追踪 Mod 中的所有函数调用和返回值
erlang:trace_pattern({Mod, '_','_'},
[{'_',[],[{return_trace}]}],
[local]),
%% 生成一个函数进行追踪
S = self(),
Pid = spawn(fun() -> do_trace(S, StartFun) end),
%% 设置追踪,告知系统开始追踪进程 Pid
erlang:trace(Pid, true, [call,procs]),
%% 告知 Pid 开始
Pid ! {self(), start},
trace_loop().
%% 当收到 Parent 的指令时,执行 StartFun()
do_trace(Parent, StartFun) ->
receive
{Parent, start} ->
StartFun()
end.
%% 显示函数调用和返回值
trace_loop() ->
receive
{trace,_,call, X} ->
io:format("Call: ~p~n",[X]),
trace_loop();
{trace,_,return_from, Call, Ret} ->
io:format("Return From: ~p => ~p~n",[Call, Ret]),
trace_loop();
Other ->
%% 处理其他消息
io:format("Other = ~p~n",[Other]),
trace_loop()
end.
1.4 测试用例
以下是一个测试用例,用于追踪 tracer_test 模块中的 fib 函数:
test2() ->
trace_module(tracer_test, fun() -> fib(4) end).
fib(0) -> 1;
fib(1) -> 1;
fib(N) -> fib(N-1) + fib(N-2).
运行测试用例的步骤如下:
1. 编译 tracer_test.erl 文件:
1> c(tracer_test).
{ok,tracer_test}
- 运行测试用例:
2> tracer_test:test2().
<0.42.0>Call: {tracer_test,'-trace_module1/2-fun-0-',
[<0.42.0>,#Fun<tracer_test.0.36786085>]}
Call: {tracer_test,do_trace,[<0.42.0>,#Fun<tracer_test.0.36786085>]}
Call: {tracer_test,'-test2/0-fun-0-',[]}
Call: {tracer_test,fib,[4]}
Call: {tracer_test,fib,[3]}
Call: {tracer_test,fib,[2]}
Call: {tracer_test,fib,[1]}
Return From: {tracer_test,fib,1} => 1
Call: {tracer_test,fib,[0]}
Return From: {tracer_test,fib,1} => 1
Return From: {tracer_test,fib,1} => 2
Call: {tracer_test,fib,[1]}
Return From: {tracer_test,fib,1} => 1
Return From: {tracer_test,fib,1} => 3
Call: {tracer_test,fib,[2]}
Call: {tracer_test,fib,[1]}
Return From: {tracer_test,fib,1} => 1
Call: {tracer_test,fib,[0]}
Return From: {tracer_test,fib,1} => 1
Return From: {tracer_test,fib,1} => 2
Return From: {tracer_test,fib,1} => 5
Return From: {tracer_test,'-test2/0-fun-0-',0} => 5
Return From: {tracer_test,do_trace,2} => 5
Return From: {tracer_test,'-trace_module1/2-fun-0-',2} => 5
Other = {trace,<0.43.0>,exit,normal}
1.5 使用库进行追踪
我们也可以使用 dbg 库模块进行相同的追踪,它隐藏了低级别 Erlang BIF 的细节。
test1() ->
dbg:tracer(),
dbg:tpl(tracer_test,fib,'_',
dbg:fun2ms(fun(_) -> return_trace() end)),
dbg:p(all,[c]),
tracer_test:fib(4).
运行此测试用例:
1> tracer_test:test1().
(<0.34.0>) call tracer_test:fib(4)
(<0.34.0>) call tracer_test:fib(3)
(<0.34.0>) call tracer_test:fib(2)
(<0.34.0>) call tracer_test:fib(1)
(<0.34.0>) returned from tracer_test:fib/1 -> 1
(<0.34.0>) call tracer_test:fib(0)
(<0.34.0>) returned from tracer_test:fib/1 -> 1
(<0.34.0>) returned from tracer_test:fib/1 -> 2
(<0.34.0>) call tracer_test:fib(1)
(<0.34.0>) returned from tracer_test:fib/1 -> 1
(<0.34.0>) returned from tracer_test:fib/1 -> 3
(<0.34.0>) call tracer_test:fib(2)
(<0.34.0>) call tracer_test:fib(1)
(<0.34.0>) returned from tracer_test:fib/1 -> 1
(<0.34.0>) call tracer_test:fib(0)
(<0.34.0>) returned from tracer_test:fib/1 -> 1
(<0.34.0>) returned from tracer_test:fib/1 -> 2
(<0.34.0>) returned from tracer_test:fib/1 -> 5
1.6 深入学习追踪
若想深入了解追踪,需阅读以下模块的手册页:
- dbg :为 Erlang 追踪 BIF 提供简化接口。
- ttb :是追踪 BIF 的另一个接口,比 dbg 级别更高。
- ms_transform :用于生成追踪软件中使用的匹配规范。
2. 动态代码加载
动态代码加载是 Erlang 的一个强大特性,每次调用 someModule:someFunction(...) 时,我们总是调用模块中函数的最新版本,即使在模块代码运行时重新编译该模块。
2.1 示例模块
我们编写两个小模块 a 和 b 来演示动态代码加载。
2.1.1 模块 b
-module(b).
-export([x/0]).
x() -> 1.
2.1.2 模块 a
-module(a).
-compile(export_all).
start(Tag) ->
spawn(fun() -> loop(Tag) end).
loop(Tag) ->
sleep(),
Val = b:x(),
io:format("Vsn1 (~p) b:x() = ~p~n",[Tag, Val]),
loop(Tag).
sleep() ->
receive
after 3000 -> true
end.
2.2 编译和运行
编译 a 和 b 模块并启动几个 a 进程:
1> c(b).
{ok, b}
2> c(a).
{ok, a}
3> a:start(one).
<0.41.0>
Vsn1 (one) b:x() = 1
4> a:start(two).
<0.43.0>
Vsn1 (one) b:x() = 1
Vsn1 (two) b:x() = 1
Vsn1 (one) b:x() = 1
Vsn1 (two) b:x() = 1
2.3 动态更新
将模块 b 修改为:
-module(b).
-export([x/0]).
x() -> 2.
重新编译 b :
4> c(b).
{ok,b}
Vsn1 (one) b:x() = 2
Vsn1 (two) b:x() = 2
Vsn1 (one) b:x() = 2
Vsn1 (two) b:x() = 2
...
可以看到,原有的 a 进程现在调用 b 的新版本。
2.4 模块 a 的更新
将模块 a 修改为:
-module(a).
-compile(export_all).
start(Tag) ->
spawn(fun() -> loop(Tag) end).
loop(Tag) ->
sleep(),
Val = b:x(),
io:format("Vsn2 (~p) b:x() = ~p~n",[Tag, Val]),
loop(Tag).
sleep() ->
receive
after 3000 -> true
end.
编译并启动新的 a 进程:
5> c(a).
{ok,a}
Vsn1 (one) b:x() = 2
Vsn1 (two) b:x() = 2
...
6> a:start(three).
<0.53.0>
Vsn1 (one) b:x() = 2
Vsn1 (two) b:x() = 2
Vsn2 (three) b:x() = 2
Vsn1 (one) b:x() = 2
Vsn1 (two) b:x() = 2
Vsn2 (three) b:x() = 2
...
可以看到,新启动的 a 进程运行新版本,而原有的 a 进程仍运行旧版本。
2.5 模块版本管理
Erlang 可以同时运行一个模块的两个版本:当前版本和旧版本。重新编译模块时,运行旧版本代码的进程将被杀死,当前版本变为旧版本,新编译的模块成为当前版本。可阅读 purge_module 文档 了解更多细节。
3. 模块和函数参考
以下是一些常见模块及其函数的简要介绍:
| 模块 | 函数 | 描述 |
| ---- | ---- | ---- |
| application | config_change(Changed, New, Removed) | 更新应用程序的配置参数 |
| application | prep_stop(State) | 为应用程序的终止做准备 |
| application | start(StartType, StartArgs) | 启动应用程序 |
| base64 | encode_to_string(Data) | 将数据编码为 Base64 字符串 |
| base64 | mime_decode_string(Base64) | 将 Base64 编码的字符串解码为数据 |
| beam_lib | chunks(Beam, [ChunkRef]) | 从 BEAM 文件或二进制文件中读取选定的块 |
| c | c(File, Options) | 编译并加载文件中的代码 |
| calendar | date_to_gregorian_days(Year, Month, Day) | 计算从公元 0 年到给定日期的天数 |
这些模块和函数为 Erlang 开发提供了丰富的功能,可根据具体需求进行使用。
3.1 动态代码加载流程图
graph TD;
A[启动 a 进程] --> B[调用 b:x()];
B --> C[输出结果];
C --> D{是否重新编译 b};
D -- 是 --> E[重新编译 b];
E --> F[a 进程调用新的 b:x()];
F --> C;
D -- 否 --> C;
通过以上介绍,我们了解了 Erlang 中的调试、追踪和动态代码加载技术,以及一些常见模块和函数的使用。这些技术和功能为开发和维护复杂的 Erlang 系统提供了强大的支持。
4. 调试与追踪的操作流程总结
4.1 低级别 BIF 追踪流程
使用低级别 Erlang BIF 进行追踪时,可按照以下步骤操作:
1. 设置追踪模式 :使用 erlang:trace_pattern/3 函数设置追踪模式,指定要追踪的代码和满足条件时的操作。
2. 启动追踪进程 :使用 spawn 函数生成一个进程进行追踪。
3. 开始追踪 :使用 erlang:trace/3 函数开始追踪指定的进程,并设置追踪的事件类型。
4. 触发追踪事件 :在追踪进程中执行要追踪的代码。
5. 处理追踪消息 :在追踪进程中使用 receive 语句处理追踪消息,显示函数调用和返回值。
4.2 使用 dbg 库追踪流程
使用 dbg 库进行追踪时,可按照以下步骤操作:
1. 启动追踪器 :使用 dbg:tracer() 函数启动追踪器。
2. 设置追踪模式 :使用 dbg:tpl 函数设置追踪模式,指定要追踪的模块、函数和操作。
3. 指定追踪进程 :使用 dbg:p 函数指定要追踪的进程。
4. 触发追踪事件 :执行要追踪的代码。
5. 查看追踪结果 :追踪器会输出函数调用和返回值的信息。
4.3 调试与追踪操作流程对比
| 操作方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 低级别 BIF 追踪 | 灵活性高,可精确控制追踪的细节 | 代码复杂,需要手动处理追踪消息 | 需要深入了解系统行为,对追踪细节有严格要求的场景 |
dbg 库追踪 | 代码简单,隐藏了低级别 BIF 的细节 | 灵活性相对较低 | 快速调试和追踪,对追踪细节要求不高的场景 |
5. 动态代码加载的应用场景与注意事项
5.1 应用场景
动态代码加载在以下场景中非常有用:
- 系统升级 :在不停止系统运行的情况下,更新模块的代码,实现系统的无缝升级。
- 功能扩展 :在系统运行时,动态加载新的模块,扩展系统的功能。
- 调试和测试 :在调试和测试过程中,快速修改模块的代码,验证修改的效果。
5.2 注意事项
在使用动态代码加载时,需要注意以下几点:
- 模块版本管理 :Erlang 只能同时运行一个模块的两个版本,重新编译模块时,运行旧版本代码的进程将被杀死。因此,在进行模块更新时,需要谨慎处理,避免影响系统的正常运行。
- 代码兼容性 :新编译的模块代码需要与旧版本的代码兼容,否则可能会导致系统出现错误。
- 性能影响 :动态代码加载会带来一定的性能开销,尤其是在频繁更新模块时,需要考虑性能影响。
6. 常见模块函数详细解析
6.1 application 模块
application 模块提供了一系列用于管理应用程序的函数,以下是一些常用函数的详细解析:
- start(Application, Type) :加载并启动指定的应用程序。
%% 启动名为 my_app 的应用程序
application:start(my_app, permanent).
-
stop(Application):停止指定的应用程序。
%% 停止名为 my_app 的应用程序
application:stop(my_app).
-
get_env(Application, Par):获取应用程序的配置参数。
%% 获取 my_app 应用程序的 db_host 配置参数
{ok, DbHost} = application:get_env(my_app, db_host).
6.2 base64 模块
base64 模块用于实现 Base64 编码和解码,以下是一些常用函数的详细解析:
- encode_to_string(Data) :将数据编码为 Base64 字符串。
%% 编码字符串 "hello world"
Base64String = base64:encode_to_string("hello world").
-
mime_decode_string(Base64):将 Base64 编码的字符串解码为数据。
%% 解码 Base64 字符串
Data = base64:mime_decode_string(Base64String).
6.3 beam_lib 模块
beam_lib 模块提供了与 BEAM 文件格式交互的接口,以下是一些常用函数的详细解析:
- chunks(Beam, [ChunkRef]) :从 BEAM 文件或二进制文件中读取选定的块。
%% 读取 BEAM 文件中的摘要块
{ok, {Module, [ChunkData]}} = beam_lib:chunks("my_module.beam", [abstract_code]).
-
info(Beam):获取 BEAM 文件的信息。
%% 获取 BEAM 文件的信息
Info = beam_lib:info("my_module.beam").
6.4 模块函数调用流程图
graph TD;
A[调用 application:start] --> B[加载应用程序];
B --> C[启动应用程序];
C --> D[调用 base64:encode_to_string];
D --> E[编码数据];
E --> F[调用 beam_lib:chunks];
F --> G[读取 BEAM 文件块];
7. 总结
通过本文的介绍,我们深入了解了 Erlang 中的调试、追踪和动态代码加载技术,以及一些常见模块和函数的使用。调试和追踪技术可以帮助我们更好地理解系统行为,快速定位和解决问题;动态代码加载技术则为系统的升级和扩展提供了便利。同时,我们还总结了调试与追踪的操作流程、动态代码加载的应用场景和注意事项,以及常见模块函数的详细解析。希望这些内容对您在 Erlang 开发中有所帮助。
超级会员免费看
879

被折叠的 条评论
为什么被折叠?



