31、Erlang 调试、追踪与动态代码加载技术详解

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}
  1. 运行测试用例:
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 开发中有所帮助。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值