编译和运行
目录
上一章并没有详细的说明如何编译和运行程序,而只是使用了Erlang shell。这对小例子是很好的,但是会让你的程序更复杂,你将会需要一个自动处理过程来简化它。我们一般使用Makefile。
事实上有三种不同的方式可以运行程序。本章将会结合特定场合来讲解三种运行方式。
有时候也会遇到问题:Makefile失败、环境变量错误或者搜索路径不对。我们在遇到这些问题时也会帮你处理这些问题(issue)。
在Unix系统(包括Mac OS X),可以从命令行启动Erlang shell:
$ erl
Erlang (BEAM) emulator version 5.5.1 [source] [async-threads:0] [hipe]
Eshell V5.5.1 (abort with ^G)
1>
而在Windows系统,则可以点击erl的图标。
最简单的退出方法是按下 Ctrl+C (对Windows系统则是 Ctrl+Break ),随后按下 A 。如下:
BREAK: (a)bort (c)ontinue (p)roc info (i)nfo (l)oaded
(v)ersion (k)ill (D)b-tables (d)istribution
a
$
另外,你也可以对 erlang:halt() 求值以退出。
erlang:halt() 是一个BIF,可以立即停止系统,也是我最常用的方法。不过这种方法也有个问题。如果正在运行一个大型数据库应用,那么系统在下次启动时就会进入错误回复过程,所以你应该以可控的方式关闭系统。
可控的关闭系统就是如果shell可用时就输入如下:
1> q().
ok
$
这会将所有打开的文件写入磁盘,停止数据库(如果在运行),并且按照顺序关闭所有OTP应用。 q() 其实只是shell上 init:stop() 的别名。
如果这些方法都不工作,可以查阅6.6节。
当你启动程序时,一般都是把所有模块和文件都放在同一个目录当中,并且在这个目录启动Erlang。如果这样做当然没问题。不过如果你的应用变得更加复杂时,你可能需要把它们分成便于管理的块,把代码放入不同的目录。而且你从其他项目导入代码时,扩展代码也有其自己的目录结构。
Erlang运行时系统包含了代码自动重新载入的功能。想要让这个功能正常工作,你必须设置一些搜索路径以便找到正确版本的代码。
代码装载功能是内置在Erlang中的,我们将在E.4节详细讨论。代码的装载仅在需要时才执行。
当系统尝试调用一个模块中的函数,而这个模块还没有装载时,就会发生异常,系统就会尝试这个模块的代码文件。如果需要的模块叫做 myMissingModule ,那么代码装载器将会在当前目录的所有目录中搜索一个叫做 myMissingModule.beam 的文件。寻找到的第一个匹配会停止搜索,然后就会把这个文件中的对象代码载入系统。
你可以在Erlang shell中看看当前装载路径,使用如下命令 code:get_path() 。如下是例子:
code:get_path()
[".",
"/usr/local/lib/erlang/lib/kernel-2.11.3/ebin",
"/usr/local/lib/erlang/lib/stdlib-1.14.3/ebin",
"/usr/local/lib/erlang/lib/xmerl-1.1/ebin",
"/usr/local/lib/erlang/lib/webtool-0.8.3/ebin",
"/usr/local/lib/erlang/lib/typer-0.1.0/ebin",
"/usr/local/lib/erlang/lib/tv-2.1.3/ebin",
"/usr/local/lib/erlang/lib/tools-2.5.3/ebin",
"/usr/local/lib/erlang/lib/toolbar-1.3/ebin",
"/usr/local/lib/erlang/lib/syntax_tools-1.5.2/ebin",
...]
管理装载路径两个最常用的函数如下:
@spec code:add_patha(Dir) => true | {error,bad_directory}
添加新目录Dir到装载路径的开头
@spec code:add_pathz(Dir) => true | {error,bad_directory}
添加新目录Dir到装载路径的末尾
通常并不需要自己来关注。只要注意两个函数产生的结果是不同的。如果你怀疑装载了错误的模块,你可以调用 code:all_loaded() (返回所有已经装载的模块列表),或者 code:clash() 来帮助你研究错误所在。
code 模块中还有其他一些程序可以用于管理路径,但是你可能根本没机会用到它们,除非你正在做一些系统编程。
按照惯例,经常把这些命令放入一个叫做 .erlang 的文件到你的HOME目录。另外,也可以在启动Erlang时的命令行中指定:
> erl -pa Dir1 -pa Dir2 ... -pz DirK1 -pz DirK2
其中 -pa 标志将目录加到搜索路径的开头,而 -pz 则把目录加到搜索路径的末尾。
我们刚才把载入路径放到了HOME目录的 .erlang 文件中。事实上,你可以把任意的Erlang代码放入这个文件,在你启动Erlang时,它就会读取和求值这个文件中的所有命令。
假设我的 .erlang 文件如下:
io:format("Running Erlang~n").
code.add_patha(".")
code.add_pathz("/home/joe/2005/erl/lib/supported").
code.add_pathz("/home/joe/bin").
当我启动系统时,我就可以看到如下输出:
$ erl
Erlang (BEAM) emulator version 5.5.1 [source] [async-threads:0] [hipe]
Running Erlang
Eshell V5.5.1 (abort with ^G)
1>
如果当前目录也有个 .erlang 文件,则会在优先于HOME目录的。这样就可以在不同启动位置定制Erlang的行为,这对特定应用非常有用。在这种情况下,推荐加入一些打印语句到启动文件;否则你可能忘记了本地的启动文件,这可能会很混乱。
某些系统很难确定HOME目录的位置,或者根本就不是你以为的位置。可以看看Erlang认为的HOME目录的位置,通过如下的:
1> init:get_argument(home).
{ok,[["/home/joe"]]}
通过这里,我们可以推断出Erlang认为的HOME目录就是 /home/joe 。
Erlang程序存储在模块中。一旦写好了程序,运行前需要先编译。不过,也可以以脚本的方式直接运行程序,叫做 escript 。
下一节会展示如何用多种方式编译和运行一对程序。这两个程序很不同,启动和停止的方式也不同。
第一个程序 hello.erl 只是打印 “Hello world” ,这不是启动和停止系统的可靠方式,而且他也不需要存取任何命令行参数。与之对比的第二个程序则需要存取命令行参数。
这里是一个简单的程序。它输出 “Hello world” 然后输出换行。 “~n” 在Erlang的io和io_lib模块中解释为换行。
-module(hello).
-export([start/0]).
start() ->
io:format("Hello world~n").
让我们以3种方式编译和运行它。
$ erl
...
1> c(hello).
{ok,hello}
2> hello:start().
Hello world
ok
$ erlc hello.erl
$ erl -noshell -s hello start -s init stop
Hello World
$
Note
快速脚本:
有时我们需要在命令行执行一个函数。可以使用 -eval 参数来快速方便的实现。这里是例子:
erl -eval 'io:format("Memory: ~p~n", [erlang:memory(total)]).'\
-noshell -s init stop
Windows用户:想要让如上工作,你需要把Erlang可执行文件目录加入到环境变量中。否则就要以引号中的全路径来启动,如下:
"C:\Program Files\erl5.5.3\bin\erlc.exe" hello.erl
第一行 erlc hello.erl 会编译文件 hello.erl ,生成叫做 hello.beam 的代码文件。第一个命令拥有三个选项:
-noshell :启动Erlang而没有交互式shell,此时不会得到Erlang的启动信息来提示欢迎
-s hello start :运行函数 hello:start() ,注意使用 -s Mod ... 选项时,相关的模块Mod必须已经编译完成了。
-s init stop :当我们调用 apply(hello,start,[]) 结束时,系统就会对函数 init:stop() 求值。
命令 erl -noshell ... 可以放入shell脚本,所以我们可以写一个shell脚本负责设置路径(使用-pa参数)和启动程序。
在我们的例子中,我们使用了两个 -s 选项,我们可以在一行拥有多个函数。每个 -s 都会使用 apply 语句来求职,而且,在一个执行完成后才会执行下一个。
如下是启动hello.erl的例子:
#! /bin/sh
erl -noshell -pa /home/joe/2006/book/JAERANG/Book/code\
-s hello start -s init stop
Note
这个脚本需要使用绝对路径指向包含 hello.beam 。不过这个脚本是运行在我的电脑上,你使用时应该修改。
运行这个shell脚本,我们需要改变文件属性(chmod),然后运行脚本:
$ chmod u+x hello.sh
$ ./hello.sh
Hello world
$
Note
在Windows上, #! 不会起效。在Windows环境下,可以创建.bat批处理文件,并且使用全路径的Erlang来启动(假如尚未设置PATH环境变量)。
一个典型的Windows批处理文件如下:
"C:\Program Files\erl5.5.3\bin\erl.exe" -noshell -s hello start -s init stop
使用escript,你可以直接运行你的程序,而不需要先编译。
Warning
escript包含在Erlang R11B-4或以后的版本,如果你的Erlang实在太老了,你需要升级到最新版本。
想要以escript方式运行hello,我们需要创建如下文件:
#! /usr/bin/env escript
main(_) ->
io:format("Hello world\n").
Note
开发阶段的导出函数
如果你正在开发代码,可能会非常痛苦于需要不断在导出函数列表增删函数。
一个声明 -compile(export_all) ,告知编译器导出所有函数。使用这个可以让你的开发工作简化一点。
当你完成开发工作时,你需要抓实掉这一行,并添加适当的导出列表。首先,重要的函数需要导出,而其他的都要隐藏起来。隐藏方式可以按照个人喜好,提供接口也是一样的效果。第二,编译器会对需要导出的函数生成更好的代码。
在Unix系统中,我们可以立即按照如下方式运行:
$ chmod u+x hello
$ ./hello
Hello world
$
Note
这里的文件模式在Unix系统中表示可执行,这个通过chmod修改文件属性的步骤只需要执行一次,而不是每次运行程序时。
在Windows系统中可以如下方式运行:
C:\> escript hello
Hello world
C:\>
Note
在以escript方式运行时,执行速度会明显的比编译方式慢上一个数量级。
“Hello world”没有参数。让我们重新来做一个计算阶乘的程序,可以接受一个参数。
首先是代码:
-module(fac).
-export([fac/1]).
fac(0) -> 1;
fac(N) -> N*fac(N-1).
我们可以编译 fac.erl 并且在Erlang中运行:
$ erl
1> c(fac).
{ok,fac}
2> fac:fac(25).
15511210043330985984000000
如果我们想要在命令行运行这个程序,我们需要修改一下他的命令行参数:
-module(fac1).
-export([main/1]).
main([A]) ->
I=list_to_integer(atom_to_list(A)).
F=fac(I),
io:format("factorial ~w= ~w~n",[I,F]),
init:stop().
fac(0) -> 1;
fac(N) -> N*fac(N-1).
让我们编译和运行:
$ erlc fac1.erl
$ erl -noshell -s fac1 main 25
factorial 25 = 15511210043330985984000000
Note
事实上这里的main()函数并没有特殊意义,你可以把它改成任何名字。重要的一点是命令行参数中要使用函数名。
最终,我们可以把它直接作为escript来运行:
#! /usr/bin/env escript
main([A]) ->
I=list_to_integer(A),
F=fac(I),
io:format("factorial ~w = ~w~n",[I,F]).
fac(0) -> 1;
fac(N) -> N*fac(N-1).
运行时无需编译,可以直接运行:
$ ./factorial 25
factorial 25 = 15511210043330985984000000
$
当编写一个大型程序时,我希望只在需要的时候自动编译。这里有两个原因。首先,节省那些一遍遍相同的打字。第二,因为经常需要同时进行多个项目,当再次回到这个项目时,我已经忘了该如何编译这些代码。而 make 可以帮助我们解决这个问题。
make 是一个自动工具,用以编译和发布Erlang代码。大多数我的makefile都非常简单,并且我有个模板可以解决大多数问题。
我不想在这里解释makefile的意义。而我会展示如何将makefile用于Erlang程序。推荐看看本书附带的makefile,然后你就会明白他们并且构建你自己的makefile了。
如下的模板是很常用的,很多时候可以基于他们来做实际的事情:
#让这一行就这么放着
.SUFFIXES: .erl .beam .yrl
.erl.beam:
erlc -W $<
.yrl.erl:
erlc -W $<
ERL=erl -boot start_clean
#如下是需要编译的模块列表
#如果一行写不下可以用反斜线 \ 来折行
#修改如下行
MODS=module1 module2 \
module3 ....
#makefile中的第一个目标是缺省目标
#也就是键入make时默认的目标
all: compile
compile: ${MODS:%=%.beam} subdirs
#指定编译需求,并且添加到这里
special1.beam: special1.erl
${ERL} -Dflag1 -W0 special1.erl
#从makefile运行一个应用
application1: compile
${ERL} -pa Dir1 -s application1 start Arg1 Arg2
#在子目录编译子目录的目标
subdirs:
cd dir1; make
cd dir2; make
...
#清除所有编译后的文件
clean:
rm -rf *.beam erl_crash.dump
cd dir1; make clean
cd dir2; make clean
makefile开始于一些编译Erlang模块的规则,包括扩展名为 .yrl 的文件(是Erlang词法解析器的定义文件)(Erlang的词法解析器生成器为yecc,是yacc的Erlang版本,参考 http://www.erlang.org/contrib/parser_tutorial-1.0.tgz )。
重要的部分开始于这一行:
MODS=module1 module2
这是我们需要编译的Erlang模块的列表。
任何在MODS列表中的模块都会被Erlang命令 erlc Mod.erl 来编译。一些模块可能会需要特殊的待遇,比如模板中的special1。所以会有单独的规则用来处理这些。
makefile中有一些目标。一个目标是一个字符串,随后是冒号”:”。在makefile模板中,all,compile和special1.beam都是目标。想要运行makefile,你可以在shell中执行:
$ make [Target]
参数Target是可选的。如果Target忽略掉了,则假设被第一个目标。在前面的例子中,目标all就是缺省的。
如果我希望构建软件并运行,我们就会使用 make application1 。如果我希望这个作为缺省行为,也就是在我每次键入 make 时都会执行,我就可以把application1目标作为第一个目标。
目标clean会删除所有编译过的Erlang目标代码和 erl_crash.dump 。crashdump包含了帮助调试应用程序的信息。查看6.10了解更多。
我并不热衷于让自己的程序变得混乱,所以我一般以一个不包含无用行的模板开始。所以得到的makefile会更短,而且也更易于阅读。另外,你也可以使用到处都是变量的makefile,以方便定制。
一旦我遍历整个流程,就会得到一个很简单的makefile,有如如下:
.SUFFIXES: .erl .beam
.erl.beam:
erlc -W $<
ERL = erl -boot start_clean
MODS=module1 module2 module3
all: compile
${ERL} -pa '/home/joe/.../this/dir' -s module1 start
compile: ${MODS:%=%.beam}
clean:
rm -rf *.beam erl_crash.dump
Erlang shell包含了内置的行编辑器。它可以执行emacs的一部分行编辑命令,前一行可以以多种方式来调用。可用命令如下,注意 “^Key” 是指按下 “Ctrl+Key” 。
命令 |
描述 |
^A |
开始一行 |
^E |
最后一行 |
^F或右箭头 |
向前一个字符 |
^B或左箭头 |
向后一个字符 |
^P或上箭头 |
前一行 |
^N或下箭头 |
下一行 |
^T |
调换最后两个字符的顺序 |
Tab |
尝试补全模块名或函数名 |
Erlang有时会发生一些问题而退出。下面是可能的错误原因:
- shell没有响应
- Ctrl+C处理器被禁用了
- Erlang以 -detached 标志启动,这时你甚至感觉不到他在运行
- Erlang以 -heart Cmd 标志启动。这会让OS监控器进程看管Erlang的OS进程。如果Erlang的OS进程死掉,那么就会求值 Cmd 。一般来说 Cmd 只是用于简单的重启Erlang系统。这个是用于生产可容错系统的一个重要技巧,用于结点-如果erlang自己死掉了(基本不可能发生),就会自己重启。这个技巧对Unix类操作系统使用 ps 命令来监控,对Windows使用任务管理器。进行心跳信息检测并且尝试kill掉erlang进程。
- 有且确实很无奈的错误,留下一个erlang僵尸进程。
本节列出了一些常见错误和解决方案。
如果你尝试调用一个模块,而代码载入器却无法找到时(比如搜索路径出错),你将会遇到 undef 错误信息,如下是例子:
1> glurk:oops(1,23).
** exited: {undef,[{glurk,oops,[1,23]},
{erl_eval,do_apply,5},
{shell,exprs,6},
{shell,eval_loop,3}]}**
事实上,这里没有一个叫做glurk,但是这里没有关联问题,你只需要关心错误信息就可以了。错误信息告诉我们系统尝试调用glurk模块的函数oops,参数1是23,这四个事物中的一个出了问题。
- 有可能不存在模块glurk,找不到或者根本不存在。可能是拼写错误。
- 存在模块glurk,但是还没有编译。系统尝试寻找文件 glurk.beam ,但是没有找到。
- 有模块glurk,但是包含 glurk.beam 的目录并没有在模块搜索路径中。想要修复这个问题,你必须改变搜索路径,一会再讲。
- 在载入路径中有多个不同版本的 “glurk” ,而我们选择了错误的。这是个罕见的错误,但是有可能会发生。如果你担心发生,你可以运行 code:clash() 函数,这将会报告代码搜索路径中的重复模块。
Note
有人看见我的分号了么?
如果你忘记了两个子句之间的分号,或者是放了个句点,那你可就麻烦了。
如果你定义了一个函数 foo/2 在模块bar的1234行,并且在该写分号的地方用了句点,那么编译器会提示:
bar.erl:1234 function foo/2 already defined.
不要这么做,确保你的子句总是以分号分开。
你怎么能让makefile出错?当然,尽管这本书不是讲makefile的,但是我还是会给出一些常见错误提示。如下是两个常见的错误:
-
makefile中的空白:makefile是很严格而挑剔的。虽然你看不到他们,但是缩进行必须以一个tab字符开始。如果这里有其他空白,那么make就会出错,而你会看到一些错误。(当然折行,也就是上一行末尾有 “\” 字符的不算)
-
丢失的erlang文件。如果在MODS变量中定义的模块丢失了,你会得到错误信息。举例,假设MODS中包含一个模块叫做glurk,但是却没有glurk.erl文件。这种情况下,make会出错,并给出如下信息:
$ make
make: *** No rule to make target 'glurk.beam',
needed by 'compile'. Stop.
另外模块名拼写错误也会导致这个问题。
如果shell对命令不做出响应了,有可能发生一系列的问题。shell进程可能会crash了,或者你可能放任一个命令永远无法终止。你可能忘了输入关闭的括号,或者忘记了 点号回车 。
无论哪种问题,你都可以通过按下 “Ctrl+G” 啦关闭当前shell,并且学着如下的例子:
1> receive foo -> true end.
^G
User switch command
--> h
c [nn] - connect to job
i [nn] - interrupt job
k [nn] - kill job
j - list all jobs
s - start local shell
r [node] - start remote shell
q - quit erlang
? | h - this message
--> j
1* {shell,start,[init]}
--> s
--> j
1 {shell,start,[init]}
2* {shell,start,[]}
--> c 2
Eshell V5.5.1 (abort with ^G)
1> init:stop().
ok
2> $
- 第一行告诉shell接收foo消息,当时不会有人发送消息到shell,shell会进入一个无限的等待,所以需要按下 “Ctrl+G” 。
- “–> h” ,系统进入了 “shell JCL” 模式(Job Control Language),这里我可以不知道任何命令所以键入 h 来获取帮助。
- “–> j” ,列出所有任务。任务的号码1标记为星号,表示为默认的shell。所有命令选项参数 [nn] 使用缺省的shell除非指定了参数。
- “–> s” ,键入s以启动一个新的shell,而在其后可以看到已经把2号标记为默认的shell了
- “–> c 2″ ,让我连接到新启动的2号shell,然后停止了系统。
有如你所见,你可以拥有多个shell,并且通过按下Ctrl+G以后来切换。你可以通过命令r在远程启动一个shell方便调试。
在Unix类系统中,如下:
$ erl -man erl
NAME
erl - The Erlang Emulator
DESCRIPTION
...
你也可以通过这种方式获取模块的帮助文档:
$ erl -man lists
MODULE
lists - List Processing Functions
...
Note
在Unix系统,man手册页并不是默认安装的,如果命令 erl -man ... 没有工作,你需要自己安装man手册页。所有的man手册页都是单独压缩存档的,可以到 http://www.erlang.org/download.html 下载。man手册页可以通过root解压并安装到Erlang的安装目录,通常为 /usr/local/lib/erlang 。
也可以下载HTML文档。在Windows下默认会安装HTML文档,可以通过开始菜单访问到。
Erlang shell拥有一系列的内置命令。你可以通过 help() 来看到这些命令:
1> help().
** shell internal commands **
b() -- display all variables bindings
e(N) -- repeat the expression in query <N>
f() -- forget all variable bindings
f(X) -- forget the binding of variable X
h() -- history
...
所有这些命令都是定义在模块 shell_default 中。
如果你想定义自己的命令,只需要创建一个叫做 user_default 的模块,例如:
-module(user_default).
-compile(export_all).
hello() ->
"Hello joe how are you?".
away(Time) ->
io:format("Joe is away and will be back in ~w minutes~n",[Time]).
一旦你编译了它并且放在了你的载入路径,那么你可以调用 user_default 中的任何函数,而不用给出模块名:
1> hello().
"Hello joe how are you?"
2> away(10).
Joe is away and will be back in 10 minutes
ok
当erlang crash时,他会生成一个文件叫做 erl_crash.dump 。文件内容包含出错的地方的相关信息。想要分析crash dump这里有个基于web的分析器。启动这个分析器,你可以给出如下命令:
1> webtool:start().
WebTool is available at http://localhost:8888/
Or http://127.0.0.1:8888/
{ok,<0.34.0>}
然后让浏览器指向 http://localhost:8888/ 。你就可以看到刚才发生的问题了。
现在我们看到的只是问题回溯的原料,具体还需要你仔细的分析,并发的程序不太好调试。从这时开始,你将离开熟悉的环境,不过这时也正是历险的开始。