6  端口驱动程序

6 端口驱动程序

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

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

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

以下图示说明了这种情况

IMAGE MISSING

图 6.1:  端口驱动程序通信

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

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

然后使用 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/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.

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

驱动程序结构中填充了驱动程序名称和函数指针。它从特殊入口点返回,使用宏 DRIVER_INIT(<driver_name>) 声明。

用于接收和发送数据的函数被合并到一个函数中,该函数由驱动程序结构指向。发送到端口的数据作为参数给出,回复数据通过 C 函数 driver_output 发送。

由于驱动程序是共享模块,而不是程序,因此没有 main 函数。本示例中未使用所有函数指针,驱动程序结构中的相应字段设置为 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 (BEAM) emulator version 5.1

Eshell V5.1 (abort with ^G)
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