Programming Erlang 第6章 编译和运行(完整)

编译和运行

译者: gashero

目录

上一章并没有详细的说明如何编译和运行程序,而只是使用了Erlang shell。这对小例子是很好的,但是会让你的程序更复杂,你将会需要一个自动处理过程来简化它。我们一般使用Makefile。

事实上有三种不同的方式可以运行程序。本章将会结合特定场合来讲解三种运行方式。

有时候也会遇到问题:Makefile失败、环境变量错误或者搜索路径不对。我们在遇到这些问题时也会帮你处理这些问题(issue)。

1   启动和停止Erlang shell

在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节。

2   修改开发环境

当你启动程序时,一般都是把所有模块和文件都放在同一个目录当中,并且在这个目录启动Erlang。如果这样做当然没问题。不过如果你的应用变得更加复杂时,你可能需要把它们分成便于管理的块,把代码放入不同的目录。而且你从其他项目导入代码时,扩展代码也有其自己的目录结构。

2.1   设置装载代码的搜索路径

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 则把目录加到搜索路径的末尾。

2.2   在系统启动时执行一系列命令

我们刚才把载入路径放到了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

3   运行程序的其他方式

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种方式编译和运行它。

3.1   在Erlang shell中编译和运行

$ erl
...
1> c(hello).
{ok,hello}
2> hello:start().
Hello world
ok

3.2   在命令行编译和运行

$ 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

3.3   以Escript运行

使用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方式运行时,执行速度会明显的比编译方式慢上一个数量级。

3.4   程序的命令行参数

“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
$

4   通过makefile自动编译

当编写一个大型程序时,我希望只在需要的时候自动编译。这里有两个原因。首先,节省那些一遍遍相同的打字。第二,因为经常需要同时进行多个项目,当再次回到这个项目时,我已经忘了该如何编译这些代码。而 make 可以帮助我们解决这个问题。

make 是一个自动工具,用以编译和发布Erlang代码。大多数我的makefile都非常简单,并且我有个模板可以解决大多数问题。

我不想在这里解释makefile的意义。而我会展示如何将makefile用于Erlang程序。推荐看看本书附带的makefile,然后你就会明白他们并且构建你自己的makefile了。

4.1   一个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了解更多。

4.2   实际修改makefile模板

我并不热衷于让自己的程序变得混乱,所以我一般以一个不包含无用行的模板开始。所以得到的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

5   Erlang shell中的命令编辑

Erlang shell包含了内置的行编辑器。它可以执行emacs的一部分行编辑命令,前一行可以以多种方式来调用。可用命令如下,注意 “^Key” 是指按下 “Ctrl+Key” 。

命令 描述
^A 开始一行
^E 最后一行
^F或右箭头 向前一个字符
^B或左箭头 向后一个字符
^P或上箭头 前一行
^N或下箭头 下一行
^T 调换最后两个字符的顺序
Tab 尝试补全模块名或函数名

6   解决错误

Erlang有时会发生一些问题而退出。下面是可能的错误原因:

  1. shell没有响应
  2. Ctrl+C处理器被禁用了
  3. Erlang以 -detached 标志启动,这时你甚至感觉不到他在运行
  4. Erlang以 -heart Cmd 标志启动。这会让OS监控器进程看管Erlang的OS进程。如果Erlang的OS进程死掉,那么就会求值 Cmd 。一般来说 Cmd 只是用于简单的重启Erlang系统。这个是用于生产可容错系统的一个重要技巧,用于结点-如果erlang自己死掉了(基本不可能发生),就会自己重启。这个技巧对Unix类操作系统使用 ps 命令来监控,对Windows使用任务管理器。进行心跳信息检测并且尝试kill掉erlang进程。
  5. 有且确实很无奈的错误,留下一个erlang僵尸进程。

7   当确实出错时

本节列出了一些常见错误和解决方案。

7.1   未定义(丢失)的代码

如果你尝试调用一个模块,而代码载入器却无法找到时(比如搜索路径出错),你将会遇到 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,这四个事物中的一个出了问题。

  1. 有可能不存在模块glurk,找不到或者根本不存在。可能是拼写错误。
  2. 存在模块glurk,但是还没有编译。系统尝试寻找文件 glurk.beam ,但是没有找到。
  3. 有模块glurk,但是包含 glurk.beam 的目录并没有在模块搜索路径中。想要修复这个问题,你必须改变搜索路径,一会再讲。
  4. 在载入路径中有多个不同版本的 “glurk” ,而我们选择了错误的。这是个罕见的错误,但是有可能会发生。如果你担心发生,你可以运行 code:clash() 函数,这将会报告代码搜索路径中的重复模块。

Note

有人看见我的分号了么?

如果你忘记了两个子句之间的分号,或者是放了个句点,那你可就麻烦了。

如果你定义了一个函数 foo/2 在模块bar的1234行,并且在该写分号的地方用了句点,那么编译器会提示:

bar.erl:1234 function foo/2 already defined.

不要这么做,确保你的子句总是以分号分开。

7.2   我的makefile无法make

你怎么能让makefile出错?当然,尽管这本书不是讲makefile的,但是我还是会给出一些常见错误提示。如下是两个常见的错误:

  1. makefile中的空白:makefile是很严格而挑剔的。虽然你看不到他们,但是缩进行必须以一个tab字符开始。如果这里有其他空白,那么make就会出错,而你会看到一些错误。(当然折行,也就是上一行末尾有 “\” 字符的不算)

  2. 丢失的erlang文件。如果在MODS变量中定义的模块丢失了,你会得到错误信息。举例,假设MODS中包含一个模块叫做glurk,但是却没有glurk.erl文件。这种情况下,make会出错,并给出如下信息:

    $ make
    make: *** No rule to make target 'glurk.beam',
                needed by 'compile'. Stop.

另外模块名拼写错误也会导致这个问题。

7.3   shell没有响应了

如果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> $
  1. 第一行告诉shell接收foo消息,当时不会有人发送消息到shell,shell会进入一个无限的等待,所以需要按下 “Ctrl+G” 。
  2. “–> h” ,系统进入了 “shell JCL” 模式(Job Control Language),这里我可以不知道任何命令所以键入 h 来获取帮助。
  3. “–> j” ,列出所有任务。任务的号码1标记为星号,表示为默认的shell。所有命令选项参数 [nn] 使用缺省的shell除非指定了参数。
  4. “–> s” ,键入s以启动一个新的shell,而在其后可以看到已经把2号标记为默认的shell了
  5. “–> c 2″ ,让我连接到新启动的2号shell,然后停止了系统。

有如你所见,你可以拥有多个shell,并且通过按下Ctrl+G以后来切换。你可以通过命令r在远程启动一个shell方便调试。

8   获取帮助

在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文档,可以通过开始菜单访问到。

9   定制环境

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

10   crash dump

当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/ 。你就可以看到刚才发生的问题了。

现在我们看到的只是问题回溯的原料,具体还需要你仔细的分析,并发的程序不太好调试。从这时开始,你将离开熟悉的环境,不过这时也正是历险的开始。

One Response to “Programming Erlang 第6章 编译和运行(完整)”

  1. gashero Says:

    d:
    d:\>erlc a.erl
    d:\>dir
    a.erl
    a.beam
    … …

Leave a Reply