Erlang 调试与动态代码加载技术解析
1. 调试资源与追踪概述
在调试方面,有两个重要的资源可帮助我们深入了解调试技术:
- 调试器参考手册 :这是一个 46 页的 PDF 文件,它提供了调试器的介绍,包含屏幕截图、API 文档等内容,对于认真使用调试器的用户来说是必读之物。
- 调试器命令手册 :在这里可以找到 shell 中可用的调试器命令。
追踪进程是理解系统行为的强大方式,无需以特殊方式编译代码即可进行。它能用于测试复杂系统,而无需修改代码,这在嵌入式系统或无法修改被测试代码的场景中尤为有用。
2. 低级别追踪的 BIF 函数
在低级别追踪中,有两个特别重要的 BIF 函数:
- erlang:trace/3 :基本作用是“我想监控这个进程,如果发生有趣的事情,请给我发送消息”。其函数原型为 erlang:trace(PidSpec, How, FlagList) ,其中 PidSpec 告知系统要追踪的内容, How 是一个布尔值,用于开启或关闭追踪, FlagList 则控制要追踪的内容,例如所有函数调用、所有发送的消息、垃圾回收发生的时刻等。调用该函数后,当追踪事件发生时,调用此 BIF 的进程将收到追踪消息,而追踪事件本身由 erlang:trace_pattern/3 确定。
- erlang:trace_pattern/3 :用于设置追踪模式。如果模式匹配成功,则执行请求的操作。其函数原型为 erlang:trace_pattern(MFA, MatchSpec, FlagList) ,其中 MFA 是一个 {Module, Function, Args} 元组,指定追踪模式应用的代码; MatchSpec 是一个模式,每次进入 MFA 指定的函数时都会进行测试; FlagList 则说明在追踪条件满足时要执行的操作。
以下是使用这两个 BIF 编写的简单追踪器代码:
trace_module(Mod, StartFun) ->
%% We'll spawn a process to do the tracing
spawn(fun() -> trace_module1(Mod, StartFun) end).
trace_module1(Mod, StartFun) ->
%% The next line says: trace all function calls and return
%% values in Mod
erlang:trace_pattern({Mod, '_','_'},
[{'_',[],[{return_trace}]}],
[local]),
%% spawn a function to do the tracing
S = self(),
Pid = spawn(fun() -> do_trace(S, StartFun) end),
%% setup the trace. Tell the system to start tracing
%% the process Pid
erlang:trace(Pid, true, [call,procs]),
%% Now tell Pid to start
Pid ! {self(), start},
trace_loop().
%% do_trace evaluates StartFun()
%% when it is told to do so by Parent
do_trace(Parent, StartFun) ->
receive
{Parent, start} ->
StartFun()
end.
%% trace_loop displays the function call and return values
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 ->
%% we get some other message - print them
io:format("Other = ~p~n",[Other]),
trace_loop()
end.
测试代码如下:
test2() ->
trace_module(tracer_test, fun() -> fib(4) end).
fib(0) -> 1;
fib(1) -> 1;
fib(N) -> fib(N-1) + fib(N-2).
执行测试的步骤如下:
1. 编译代码:
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}
3. 使用库进行追踪
我们也可以使用 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
4. 深入学习追踪的模块
若要更深入地了解追踪技术,需要阅读以下三个模块的手册页:
- dbg :为 Erlang 追踪 BIF 提供简化接口。
- ttb :是追踪 BIF 的另一个接口,比 dbg 级别更高。
- ms_transform :用于生成追踪软件中使用的匹配规范。
5. 动态代码加载概述
动态代码加载是 Erlang 核心内置的一项令人惊喜的功能。其原理很简单:每次调用 someModule:someFunction(...) 时,我们总是调用模块最新版本中的函数最新版本,即使在模块代码运行时重新编译该模块也是如此。
6. 动态代码加载示例
为了演示动态代码加载的工作原理,我们编写两个小模块 a 和 b 。
6.1 模块 b 的代码
-module(b).
-export([x/0]).
x() -> 1.
6.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.
6.3 编译和运行步骤
- 编译模块
b和a:
1> c(b).
{ok, b}
2> c(a).
{ok, a}
- 启动
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
- 修改模块
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的代码为:
-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
...
- 再次修改模块
b的代码为:
-module(b).
-export([x/0]).
x() -> 3.
- 重新编译模块
b:
7> c(b).
{ok,b}
Vsn1 (one) b:x() = 3
Vsn1 (two) b:x() = 3
Vsn2 (three) b:x() = 3
...
- 第三次修改模块
a的代码为:
-module(a).
-compile(export_all).
start(Tag) ->
spawn(fun() -> loop(Tag) end).
loop(Tag) ->
sleep(),
Val = b:x(),
io:format("Vsn3 (~p) b:x() = ~p~n",[Tag, Val]),
loop(Tag).
sleep() ->
receive
after 3000 -> true
end.
- 编译并启动新的
a进程:
8> c(a).
{ok,a}
Vsn2 (three) b:x() = 3
...
9> a:start(four).
<0.106.0>
Vsn2 (three) b:x() = 3
Vsn3 (four) b:x() = 3
Vsn2 (three) b:x() = 3
Vsn3 (four) b:x() = 3
...
6.4 动态代码加载总结
Erlang 可以同时运行一个模块的两个版本,即当前版本和旧版本。当重新编译一个模块时,运行旧版本代码的任何进程都会被终止,当前版本变为旧版本,新编译的模块成为当前版本。可以将其看作一个包含两个代码版本的移位寄存器,随着新代码的添加,最旧的版本会被丢弃,进程可以同时运行代码的旧版本和新版本。
7. 模块和函数参考
以下是一些常见模块及其函数的简要总结:
7.1 application 模块
| 函数 | 描述 |
|---|---|
Module:config_change(Changed, New, Removed) -> ok | 更新应用程序的配置参数 |
Module:prep_stop(State) -> NewState | 为应用程序的终止做准备 |
Module:start(StartType, StartArgs) -> {ok, Pid} \| {ok, Pid, State} \| {error, Reason} | 启动一个应用程序 |
Module:start_phase(Phase, StartType, PhaseArgs) -> ok \| {error, Reason} | 应用程序的扩展启动 |
Module:stop(State) | 在应用程序终止后进行清理 |
get_all_env(Application) -> Env | 获取应用程序的配置参数 |
get_all_key(Application) -> {ok, Keys} \| undefined | 获取应用程序规范键 |
get_application(Pid \| Module) -> {ok, Application} \| undefined | 获取包含特定进程或模块的应用程序名称 |
get_env(Application, Par) -> {ok, Val} \| undefined | 获取配置参数的值 |
get_key(Application, Key) -> {ok, Val} \| undefined | 获取应用程序规范键的值 |
load(AppDescr, Distributed) -> ok \| {error, Reason} | 加载一个应用程序 |
loaded_applications() -> [{Application, Description, Vsn}] | 获取当前加载的应用程序 |
permit(Application, Bool) -> ok \| {error, Reason} | 更改应用程序在节点上运行的权限 |
set_env(Application, Par, Val, Timeout) -> ok | 设置配置参数的值 |
start(Application, Type) -> ok \| {error, Reason} | 加载并启动一个应用程序 |
start_type() -> StartType \| local \| undefined | 获取正在进行的应用程序启动类型 |
stop(Application) -> ok \| {error, Reason} | 停止一个应用程序 |
takeover(Application, Type) -> ok \| {error, Reason} | 接管一个分布式应用程序 |
unload(Application) -> ok \| {error, Reason} | 卸载一个应用程序 |
unset_env(Application, Par, Timeout) -> ok | 取消设置配置参数的值 |
which_applications(Timeout) -> [{Application, Description, Vsn}] | 获取当前正在运行的应用程序 |
7.2 base64 模块
| 函数 | 描述 |
|---|---|
encode_to_string(Data) -> Base64String | 将数据编码为 Base64 字符串 |
mime_decode_string(Base64) -> DataString | 将 Base64 编码的字符串解码为数据 |
7.3 beam_lib 模块
| 函数 | 描述 |
|---|---|
chunks(Beam, [ChunkRef]) -> {ok, {Module, [ChunkData]}} \| {error, beam_lib, Reason} | 从 BEAM 文件或二进制文件中读取选定的块 |
chunks(Beam, [ChunkRef], [Option]) -> {ok, {Module, [ChunkResult]}} \| {error, beam_lib, Reason} | 从 BEAM 文件或二进制文件中读取选定的块(带选项) |
clear_crypto_key_fun() -> {ok, Result} | 取消注册当前的加密密钥函数 |
cmp(Beam1, Beam2) -> ok \| {error, beam_lib, Reason} | 比较两个 BEAM 文件 |
cmp_dirs(Dir1, Dir2) -> {Only1, Only2, Different} \| {error, beam_lib, Reason1} | 比较两个目录中的 BEAM 文件 |
crypto_key_fun(CryptoKeyFun) -> ok \| {error, Reason} | 注册一个提供加密密钥的函数 |
diff_dirs(Dir1, Dir2) -> ok \| {error, beam_lib, Reason1} | 比较两个目录中的 BEAM 文件 |
format_error(Reason) -> Chars | 返回 BEAM 读取错误回复的英文描述 |
info(Beam) -> [{Item, Info}] \| {error, beam_lib, Reason1} | 获取 BEAM 文件的信息 |
md5(Beam) -> {ok, {Module, MD5}} \| {error, beam_lib, Reason} | 读取 BEAM 文件的模块版本 |
strip(Beam1) -> {ok, {Module, Beam2}} \| {error, beam_lib, Reason1} | 从 BEAM 文件中移除加载器不需要的块 |
strip_files(Files) -> {ok, [{Module, Beam2}]} \| {error, beam_lib, Reason1} | 从多个 BEAM 文件中移除加载器不需要的块 |
strip_release(Dir) -> {ok, [{Module, Filename}]} \| {error, beam_lib, Reason1} | 从一个版本的所有 BEAM 文件中移除加载器不需要的块 |
version(Beam) -> {ok, {Module, [Version]}} \| {error, beam_lib, Reason} | 读取 BEAM 文件的模块版本 |
7.4 c 模块
| 函数 | 描述 |
|---|---|
bt(Pid) -> void() | 进程的堆栈回溯 |
c(File, Options) -> {ok, Module} \| error | 编译并加载文件中的代码 |
cd(Dir) -> void() | 更改工作目录 |
flush() -> void() | 刷新发送到 shell 的任何消息 |
help() -> void() | 显示帮助信息 |
i(X, Y, Z) -> void() | 显示 pid <X.Y.Z> 的信息 |
l(Module) -> void() | 加载或重新加载模块 |
lc(Files) -> ok | 编译文件列表 |
ls() -> void() | 列出当前目录中的文件 |
ls(Dir) -> void() | 列出指定目录中的文件 |
m() -> void() | 显示已加载的模块 |
m(Module) -> void() | 显示模块的信息 |
memory() -> [{Type, Size}] | 显示内存分配信息 |
memory([Type]) -> [{Type, Size}] | 显示指定类型的内存分配信息 |
nc(File, Options) -> {ok, Module} \| error | 在所有节点上编译并加载文件中的代码 |
ni() -> void() | 显示系统信息 |
nl(Module) -> void() | 在所有节点上加载模块 |
nregs() -> void() | 显示注册进程的信息 |
pid(X, Y, Z) -> pid() | 将 X,Y,Z 转换为 PID |
pwd() -> void() | 打印工作目录 |
q() -> void() | 退出,等同于 init:stop() |
xm(ModSpec) -> void() | 对模块进行交叉引用检查 |
y(File) -> YeccRet | 生成一个 LALR - 1 解析器 |
y(File, Options) -> YeccRet | 生成一个 LALR - 1 解析器(带选项) |
7.5 calendar 模块
| 函数 | 描述 |
|---|---|
date_to_gregorian_days(Year, Month, Day) -> Days | 计算从公元 0 年到给定日期的天数 |
datetime_to_gregorian_seconds({Date, Time}) -> Seconds | 计算从公元 0 年到给定日期和时间的秒数 |
day_of_the_week(Year, Month, Day) -> DayNumber | 计算给定日期是星期几 |
gregorian_days_to_date(Days) -> Date | 根据格里高利天数计算日期 |
gregorian_seconds_to_datetime(Seconds) -> {Date, Time} | 根据格里高利秒数计算日期和时间 |
is_leap_year(Year) -> bool() | 检查给定年份是否为闰年 |
last_day_of_the_month(Year, Month) -> int() | 计算给定月份的天数 |
local_time() -> {Date, Time} | 计算本地时间 |
local_time_to_universal_time({Date1, Time1}) -> {Date2, Time2} | 从本地时间转换为通用时间(已弃用) |
local_time_to_universal_time_dst({Date1, Time1}) -> [{Date, Time}] | 从本地时间转换为通用时间(考虑夏令时) |
now_to_datetime(Now) -> {Date, Time} | 将 now 转换为日期和时间 |
now_to_local_time(Now) -> {Date, Time} | 将 now 转换为本地日期和时间 |
seconds_to_daystime(Seconds) -> {Days, Time} | 从秒数计算天数和时间 |
seconds_to_time(Seconds) -> Time | 从秒数计算时间 |
time_difference(T1, T2) -> {Days, Time} | 计算两个时间之间的差值(已弃用) |
time_to_seconds(Time) -> Seconds | 计算从午夜到给定时间的秒数 |
8. 总结
通过本文,我们详细介绍了 Erlang 中的调试技术、动态代码加载功能以及一些常见模块的函数。调试技术可以帮助我们更好地理解系统行为,动态代码加载则为我们提供了在运行时更新代码的便利。同时,了解常见模块的函数可以让我们更高效地开发和维护 Erlang 应用程序。希望这些内容对大家有所帮助。
9. 调试技术的应用场景与优势
9.1 应用场景
- 复杂系统测试 :在开发大型、复杂的系统时,系统内部的交互和逻辑可能非常复杂。通过追踪进程,我们可以详细了解系统在运行过程中的每一个步骤,找出潜在的问题和瓶颈。例如,在一个分布式系统中,多个进程之间的消息传递和函数调用可能会导致各种难以调试的问题,使用追踪技术可以清晰地看到这些交互过程。
- 嵌入式系统调试 :在嵌入式系统中,由于硬件资源的限制和系统的特殊性,修改代码可能会比较困难。追踪技术可以在不修改代码的情况下,对系统进行监控和调试,帮助我们快速定位问题。
9.2 优势
- 无需特殊编译 :追踪进程不需要对代码进行特殊的编译,这使得调试过程更加灵活和方便。我们可以在不影响系统正常运行的情况下,随时开启或关闭追踪功能。
- 深入了解系统行为 :通过追踪函数调用和消息传递,我们可以深入了解系统的运行机制,发现一些隐藏的问题和潜在的优化点。
10. 动态代码加载的实际意义
10.1 实时更新系统
动态代码加载允许我们在系统运行时更新代码,而不需要停止整个系统。这对于一些需要实时响应的系统来说非常重要,例如在线游戏、金融交易系统等。在这些系统中,即使是短暂的停机也可能会导致巨大的损失。
10.2 提高开发效率
在开发过程中,我们可以快速地对代码进行修改和测试,而不需要重新启动整个系统。这大大缩短了开发周期,提高了开发效率。例如,在开发一个 Web 应用程序时,我们可以在修改代码后立即看到效果,而不需要重新部署整个应用。
11. 常见模块的使用建议
11.1 application 模块
- 配置管理 :使用
get_all_env和set_env函数可以方便地管理应用程序的配置参数。在不同的环境中,我们可以通过修改配置参数来调整应用程序的行为。 - 应用程序生命周期管理 :
start和stop函数可以帮助我们管理应用程序的启动和停止过程。在启动应用程序时,我们可以指定启动类型和启动参数;在停止应用程序时,我们可以进行必要的清理工作。
11.2 base64 模块
- 数据传输 :在网络传输中,有时需要将二进制数据转换为文本格式,以便在不同的系统之间进行传输。
encode_to_string和mime_decode_string函数可以方便地实现数据的 Base64 编码和解码。
11.3 beam_lib 模块
- 模块版本管理 :
version和md5函数可以帮助我们获取 BEAM 文件的模块版本信息,从而实现模块的版本管理。在更新模块时,我们可以通过比较版本信息来确保使用的是最新版本的模块。
11.4 c 模块
- 代码编译和加载 :
c和l函数是最常用的代码编译和加载函数。在开发过程中,我们可以使用这些函数快速地编译和加载代码,提高开发效率。
11.5 calendar 模块
- 时间处理 :在处理时间相关的业务逻辑时,
calendar模块提供了丰富的函数。例如,我们可以使用date_to_gregorian_days和datetime_to_gregorian_seconds函数来进行日期和时间的计算。
12. 总结与展望
12.1 总结
本文详细介绍了 Erlang 中的调试技术、动态代码加载功能以及一些常见模块的函数。调试技术通过追踪进程,让我们可以在不修改代码的情况下深入了解系统行为;动态代码加载则允许我们在系统运行时更新代码,提高了系统的实时性和开发效率。常见模块的函数为我们提供了丰富的工具,帮助我们更高效地开发和维护 Erlang 应用程序。
12.2 展望
随着 Erlang 技术的不断发展,调试和代码加载技术也可能会不断完善和优化。未来,我们可能会看到更强大、更易用的调试工具和代码加载机制,进一步提高开发效率和系统的稳定性。同时,随着分布式系统和云计算的发展,Erlang 的应用场景也会越来越广泛,这些技术将在更多的领域发挥重要作用。
13. 流程图:动态代码加载流程
graph TD;
A[编译模块b和a] --> B[启动a进程];
B --> C[修改模块b代码];
C --> D[重新编译模块b];
D --> E[修改模块a代码];
E --> F[编译并启动新的a进程];
F --> G[再次修改模块b代码];
G --> H[重新编译模块b];
H --> I[第三次修改模块a代码];
I --> J[编译并启动新的a进程];
14. 表格:调试与代码加载技术对比
| 技术 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 调试技术(追踪) | 无需特殊编译,深入了解系统行为 | 可能会影响系统性能 | 复杂系统测试、嵌入式系统调试 |
| 动态代码加载 | 实时更新系统,提高开发效率 | 可能会导致旧版本进程终止 | 实时响应系统、快速开发迭代 |
15. 总结
通过本文的介绍,我们对 Erlang 中的调试技术、动态代码加载功能以及常见模块有了更深入的了解。这些技术和工具为我们开发和维护 Erlang 应用程序提供了强大的支持。在实际应用中,我们可以根据具体的需求选择合适的技术和工具,提高开发效率和系统的稳定性。希望大家在使用 Erlang 时,能够充分利用这些技术,开发出更加优秀的应用程序。
超级会员免费看

504

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



