查看源代码 端口驱动程序
本节概述了如何使用链接式端口驱动程序解决问题示例中的示例问题。
端口驱动程序是一种链接式驱动程序,可以从 Erlang 程序中作为端口访问。它是一个共享库(在 UNIX 中为 SO,在 Windows 中为 DLL),具有特殊的入口点。当驱动程序启动以及数据发送到端口时,Erlang 运行时系统会调用这些入口点。端口驱动程序也可以向 Erlang 发送数据。
由于端口驱动程序是动态链接到仿真器进程的,因此这是从 Erlang 调用 C 代码的最快方法。调用端口驱动程序中的函数不需要上下文切换。但它也是最不安全的方法,因为端口驱动程序中的崩溃也会导致仿真器崩溃。
该场景如下图所示
---
title: Port Driver Communication
---
flowchart
subgraph Legend
direction LR
os[OS Process]
erl([Erlang Process])
end
subgraph emulator
direction LR
port{Port} --> erlProc
erlProc([Connected process]) --> port
port --> proc[Port Driver Shared Library]
proc --> port
end
Erlang 程序
与端口程序一样,端口与 Erlang 进程通信。所有通信都通过一个 Erlang 进程进行,该进程是端口驱动程序的连接进程。终止此进程会关闭端口驱动程序。
在创建端口之前,必须加载驱动程序。这是使用函数erl_ddll:load_driver/2
完成的,共享库的名称作为参数。
然后使用 BIF open_port/2
创建端口,其中元组 {spawn, DriverName}
作为第一个参数。字符串 SharedLib
是端口驱动程序的名称。第二个参数是选项列表,在本例中没有。
-module(complex5).
-export([start/1, init/1]).
start(SharedLib) ->
case erl_ddll:load_driver(".", SharedLib) of
ok -> ok;
{error, already_loaded} -> ok;
_ -> exit({error, could_not_load_driver})
end,
spawn(?MODULE, init, [SharedLib]).
init(SharedLib) ->
register(complex, self()),
Port = open_port({spawn, SharedLib}, []),
loop(Port).
现在可以实现 complex5:foo/1
和 complex5:bar/1
。两者都向 complex
进程发送消息并接收以下回复
foo(X) ->
call_port({foo, X}).
bar(Y) ->
call_port({bar, Y}).
call_port(Msg) ->
complex ! {call, self(), Msg},
receive
{complex, Result} ->
Result
end.
complex
进程执行以下操作
- 将消息编码为字节序列。
- 将其发送到端口。
- 等待回复。
- 解码回复。
- 将其发送回调用者
loop(Port) ->
receive
{call, Caller, Msg} ->
Port ! {self(), {command, encode(Msg)}},
receive
{Port, {data, Data}} ->
Caller ! {complex, decode(Data)}
end,
loop(Port)
end.
假设 C 函数的参数和结果都小于 256,则采用简单的编码/解码方案。在此方案中,foo
用字节 1 表示,bar
用 2 表示,参数/结果也用单个字节表示
encode({foo, X}) -> [1, X];
encode({bar, Y}) -> [2, Y].
decode([Int]) -> Int.
生成的 Erlang 程序(包括用于停止端口和检测端口故障的函数)如下所示
-module(complex5).
-export([start/1, stop/0, init/1]).
-export([foo/1, bar/1]).
start(SharedLib) ->
case erl_ddll:load_driver(".", SharedLib) of
ok -> ok;
{error, already_loaded} -> ok;
_ -> exit({error, could_not_load_driver})
end,
spawn(?MODULE, init, [SharedLib]).
init(SharedLib) ->
register(complex, self()),
Port = open_port({spawn, SharedLib}, []),
loop(Port).
stop() ->
complex ! stop.
foo(X) ->
call_port({foo, X}).
bar(Y) ->
call_port({bar, Y}).
call_port(Msg) ->
complex ! {call, self(), Msg},
receive
{complex, Result} ->
Result
end.
loop(Port) ->
receive
{call, Caller, Msg} ->
Port ! {self(), {command, encode(Msg)}},
receive
{Port, {data, Data}} ->
Caller ! {complex, decode(Data)}
end,
loop(Port);
stop ->
Port ! {self(), close},
receive
{Port, closed} ->
exit(normal)
end;
{'EXIT', Port, Reason} ->
io:format("~p ~n", [Reason]),
exit(port_terminated)
end.
encode({foo, X}) -> [1, X];
encode({bar, Y}) -> [2, Y].
decode([Int]) -> Int.
C 驱动程序
C 驱动程序是一个模块,它被编译并链接到共享库中。它使用驱动程序结构并包含头文件 erl_driver.h
。
驱动程序结构填充了驱动程序名称和函数指针。它从特殊入口点返回,使用宏 DRIVER_INIT(<driver_name>)
声明。
接收和发送数据的函数合并为一个函数,由驱动程序结构指出。发送到端口的数据作为参数给出,回复的数据使用 C 函数 driver_output
发送。
由于驱动程序是一个共享模块,而不是一个程序,因此不存在 main 函数。在此示例中未使用所有函数指针,并且 driver_entry
结构中的相应字段设置为 NULL。
驱动程序中的所有函数都采用句柄(从 start
返回),该句柄仅由 Erlang 进程传递。这必须以某种方式引用端口驱动程序实例。
example_drv_start
是唯一一个使用端口实例句柄调用的函数,因此必须保存它。习惯上为此使用分配的驱动程序定义的结构,并将指针作为引用传递回去。
使用全局变量不是一个好主意,因为端口驱动程序可以由多个 Erlang 进程生成。此驱动程序结构将被多次实例化
/* port_driver.c */
#include <stdio.h>
#include "erl_driver.h"
typedef struct {
ErlDrvPort port;
} example_data;
static ErlDrvData example_drv_start(ErlDrvPort port, char *buff)
{
example_data* d = (example_data*)driver_alloc(sizeof(example_data));
d->port = port;
return (ErlDrvData)d;
}
static void example_drv_stop(ErlDrvData handle)
{
driver_free((char*)handle);
}
static void example_drv_output(ErlDrvData handle, char *buff,
ErlDrvSizeT bufflen)
{
example_data* d = (example_data*)handle;
char fn = buff[0], arg = buff[1], res;
if (fn == 1) {
res = foo(arg);
} else if (fn == 2) {
res = bar(arg);
}
driver_output(d->port, &res, 1);
}
ErlDrvEntry example_driver_entry = {
NULL, /* F_PTR init, called when driver is loaded */
example_drv_start, /* L_PTR start, called when port is opened */
example_drv_stop, /* F_PTR stop, called when port is closed */
example_drv_output, /* F_PTR output, called when erlang has sent */
NULL, /* F_PTR ready_input, called when input descriptor ready */
NULL, /* F_PTR ready_output, called when output descriptor ready */
"example_drv", /* char *driver_name, the argument to open_port */
NULL, /* F_PTR finish, called when unloaded */
NULL, /* void *handle, Reserved by VM */
NULL, /* F_PTR control, port_command callback */
NULL, /* F_PTR timeout, reserved */
NULL, /* F_PTR outputv, reserved */
NULL, /* F_PTR ready_async, only for async drivers */
NULL, /* F_PTR flush, called when port is about
to be closed, but there is data in driver
queue */
NULL, /* F_PTR call, much like control, sync call
to driver */
NULL, /* unused */
ERL_DRV_EXTENDED_MARKER, /* int extended marker, Should always be
set to indicate driver versioning */
ERL_DRV_EXTENDED_MAJOR_VERSION, /* int major_version, should always be
set to this value */
ERL_DRV_EXTENDED_MINOR_VERSION, /* int minor_version, should always be
set to this value */
0, /* int driver_flags, see documentation */
NULL, /* void *handle2, reserved for VM use */
NULL, /* F_PTR process_exit, called when a
monitored process dies */
NULL /* F_PTR stop_select, called to close an
event object */
};
DRIVER_INIT(example_drv) /* must match name in driver_entry */
{
return &example_driver_entry;
}
运行示例
步骤 1. 编译 C 代码
unix> gcc -o example_drv.so -fpic -shared complex.c port_driver.c
windows> cl -LD -MD -Fe example_drv.dll complex.c port_driver.c
步骤 2. 启动 Erlang 并编译 Erlang 代码
> erl
Erlang/OTP 26 [erts-14.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit:ns]
Eshell V14.2 (press Ctrl+G to abort, type help(). for help)
1> c(complex5).
{ok,complex5}
步骤 3. 运行示例
2> complex5:start("example_drv").
<0.34.0>
3> complex5:foo(3).
4
4> complex5:bar(5).
10
5> complex5:stop().
stop