6.端口驱动 | 6. Port Drivers

6 端口驱动程序

本节概述了如何Problem Example使用链接的端口驱动程序解决示例问题的示例。

端口驱动程序是一个链接的驱动程序,可以从Erlang程序中作为端口访问。它是一个共享库(UNIX中的SO,Windows中的DLL),带有特殊的入口点。Erlang运行时系统在启动驱动程序和将数据发送到端口时调用这些入口点。端口驱动程序也可以发送数据给Erlang。

由于端口驱动程序是动态链接到仿真程序的,所以这是从Erlang调用C代码的最快方法。在端口驱动程序中调用函数不需要上下文切换。但它也是最不安全的方式,因为端口驱动程序崩溃也会导致仿真器失效。

该方案如下图所示:

图6.1:端口驱动程序通信

6.1 Erlang程序

像端口程序一样,端口与Erlang进程通信。所有的通信都通过一个Erlang进程,这个进程是端口驱动程序的连接进程。终止此过程将关闭端口驱动程序。

在创建端口之前,必须加载驱动程序。这是通过函数完成的erl_dll:load_driver/1,共享库的名称作为参数。

然后使用BIF创建端口open_port/2,并将元组{spawn, DriverName}作为第一个参数。该字符串SharedLib是端口驱动程序的名称。第二个参数是选项列表,在这种情况下是none:

-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/1complex5: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.

6.2 C驱动器

C驱动程序是一个编译并链接到共享库的模块。它使用驱动程序结构并包含头文件erl_driver.h

驱动程序结构填充了驱动程序名称和函数指针。它是从宏指定的特殊入口点返回的DRIVER_INIT(<driver_name>)。

接收和发送数据的功能被组合成一个由驱动器结构指出的功能。发送到端口的数据作为参数给出,并且回复的数据与C函数一起发送driver_output

由于驱动程序是共享模块,而不是程序,因此不存在主要功能。在这个例子中,所有函数指针都没有使用,并且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, /* F_PTR event, called when an event selected by driver_event() occurs. */ 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; }

6.3运行示例

第一步。编译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

第二步。启动Erlang并编译Erlang代码:

> erl Erlang (BEAM) emulator version 5.1 Eshell V5.1 (abort with ^G) 1> c(complex5). {ok,complex5}

第三步。运行示例:

2> complex5:start("example_drv"). <0.34.0> 3> complex5:foo(3). 4 4> complex5:bar(5). 10 5> complex5:stop(). stop